Compare commits

..
Author SHA1 Message Date
snacj 2d28c18b1a Added Heading for new Mechanical Diagram 2025-11-18 14:04:32 +01:00
snacj b19764281a Added Nozzle docs 2025-11-18 13:58:51 +01:00
snacj c25d63f781 Added Nozzle docs 2025-11-18 13:58:51 +01:00
Robin Krämer d79efc412f Merge pull request #904 from qitechgmbh/898-mock-machine-doesnt-work
898 mock machine doesnt work
2025-11-18 12:53:24 +01:00
rob 7ad74cf089 fixed formatting 2025-11-17 18:00:43 +01:00
rob 6073d37892 fixed remaining compile errors 2025-11-17 18:00:43 +01:00
rob c04937553f machines show up but dont get subscribed to 2025-11-17 18:00:43 +01:00
rob e08bdfa3c7 removed unnecessary and outdated docs 2025-11-17 18:00:43 +01:00
rob d282678b19 fixed compile errors for mock-machines 2025-11-17 18:00:43 +01:00
Robin Krämer 9ff4243350 Merge pull request #902 from qitechgmbh/2.12-releasenotes
updated Changelog
2025-11-17 16:38:48 +01:00
rob 6c79b33752 updated Changelog 2025-11-17 16:36:41 +01:00
rob 861b5da7a3 updated Changelog 2025-11-17 16:35:28 +01:00
Robin Krämer 7dd3d50653 Merge pull request #899 from qitechgmbh/add-debugging-utils-folder
added debugging utils and fixed memory leak
2025-11-14 23:30:55 +01:00
kraemr ba8e04c641 removed unused imports 2025-11-14 23:28:05 +01:00
kraemr cece245a55 removed one_hour caching on backend for apis 2025-11-14 21:26:38 +01:00
kraemr fab4d2810e fixed memory leak and added debugging utilities 2025-11-14 21:18:51 +01:00
Robin Krämer 5bff3ca929 Merge pull request #897 from qitechgmbh/fix-async-yield-timeout
Fix async yield timeout
2025-11-14 16:45:59 +01:00
kraemr afe1d3b80e fixed display of time taken on failing loop 2025-11-14 14:06:32 +01:00
kraemr 6c8005dba9 log cycle time on failure 2025-11-14 13:36:50 +01:00
kraemr 9f399ca6f3 fixed compile error 2025-11-14 13:25:44 +01:00
kraemr a7574dd348 better error handling for copy ethercat outputs and inputs 2025-11-14 13:22:27 +01:00
kraemr b39f3597c8 fixed loop 2025-11-14 12:20:53 +01:00
Robin Krämer bb9a86d52e Merge pull request #893 from qitechgmbh/fix-serial-hotplugging
fixed laser hotplug issue
2025-11-13 13:57:27 +01:00
kraemr e5329e40c6 fixed ci error 2025-11-13 13:55:44 +01:00
kraemr 319611f69d fixed laser hotplug issue 2025-11-13 13:55:44 +01:00
Robin Krämer 4b16253c8c Merge pull request #887 from qitechgmbh/841-remove-all-locking-and-as-much-async-code-as-possible
841 remove all locking and as much async code as possible
2025-11-13 10:01:47 +01:00
kraemr b60423fde6 removed unnecessary variable from shared_state and fixed serial hotplug 2025-11-13 09:33:40 +01:00
kraemr 5d3399c72e fixed formatting electron 2025-11-13 01:03:10 +01:00
kraemr d247bafcbb fixed formatting 2025-11-13 01:00:28 +01:00
kraemr 5227aad4c6 removed all warnings 2025-11-13 00:56:59 +01:00
kraemr da4c54a99e fixed compile errors 2025-11-13 00:04:15 +01:00
kraemr 8d42bd93f7 started refactoring so machines use channels instead of locks
backend works flawlessly now

added machine_connection and a main_thread actor

added back write_machine_device_ident

error handling for machines restored

correctly handles machine connections/disconnections now

removed unused deps
2025-11-12 17:54:11 +01:00
kraemr 37e467272b removed potential "deadlock" 2025-11-12 17:41:03 +01:00
Oshgnacknak ed0b9cd5ae Merge pull request #859 from qitechgmbh/789-create-minimal-set-of-uom-units
Use minimal set of units instead of full uom::si module
2025-11-11 15:47:45 +01:00
Oshgnacknak 8045f799f0 Use minimal set of units instead of full uom::si module 2025-11-10 17:15:51 +01:00
Robin Krämer ca6ea16f89 Merge pull request #881 from qitechgmbh/880-add-nozzle-pid-for-temperature-at-extruder
880 add nozzle pid for temperature at extruder
2025-11-06 12:16:58 +01:00
snacj 452614abf1 Fixed Frontend Formatting 2025-11-06 11:59:52 +01:00
snacj f63c797cd1 Fixed Formatting 2025-11-06 11:57:56 +01:00
snacj de791f6ea8 Added PID for extruder nozzle. 2025-11-06 11:57:34 +01:00
183 changed files with 3722 additions and 5512 deletions

No files matched your search

+16
View File
@@ -1,3 +1,19 @@
# `2.12.0`
_17.11.2025_
## General
- [#847](https://github.com/qitechgmbh/control/pull/847) Improved discovery and initialization
- [#859](https://github.com/qitechgmbh/control/pull/859) Improved update speed
- [#897](https://github.com/qitechgmbh/control/pull/897), [#899](https://github.com/qitechgmbh/control/pull/899), [#852](https://github.com/qitechgmbh/control/pull/852) Improved system stability
- [#817](https://github.com/qitechgmbh/control/pull/817) Added an update bar
## Laser
- [#857](https://github.com/qitechgmbh/control/pull/857) Fixed potential issues with 2 Axis Laser
## Extruder V2
- [#881](https://github.com/qitechgmbh/control/pull/881) Extended Temperature PID Configuration
# `2.11.0`
_21.10.2025_
Generated
+118 -714
View File
File diff suppressed because it is too large. Load diff
+5 -9
View File
@@ -1,18 +1,14 @@
[workspace]
members = [
# Hardware Abstraction Layer (Framework)
"server",
"ethercat-hal",
"ethercat-hal-derive",
# Control Framework (Framework)
"control-core",
"machines",
#"ethercat-eeprom-dump",
"control-core-derive",
# Machine Implementations (QiTech-Specific)
"server",
# Utilities
"ethercat-eeprom-dump",
"units",
"utils",
]
exclude = []
resolver = "2"
+13 -32
View File
@@ -12,37 +12,24 @@ QiTech Control is an industrial control panel software for the next generation o
# Repo Structure
This repository is organized as a monorepo with clear separation of concerns. See [Repository Organization](./docs/repository-organization.md) for detailed architecture information.
Frontend
## Hardware Abstraction Layer (Framework)
- `/electron`: Frontend code for the control software built with React and Electron.
- `/ethercat-hal`: Hardware abstraction layer for Beckhoff and other EtherCAT devices and protocols
- `/ethercat-hal-derive`: Derive macros for `ethercat-hal`
Backend
## Control Framework (Framework)
- `/server`: Glue between Beckhoff and Electron. Implements machine logic.
- `/ethercat-hal`: Hardware abstraction layer for Beckhoff (and possibly other EtherCat) devices and protocols.
- `/ethercat-hal-derive`: Macros for `ethercat-hal`
- `/control-core`: Core control logic for the server.
- `/control-core`: Generic control logic, actor system, and API abstractions
- `/control-core-derive`: Derive macros for `control-core`
Operating System
## Machine Implementations (QiTech-Specific)
- `/nixos`: Custom Linux with realtime kernel & preconfigured for touchscreens.
- `/server`: QiTech-specific machine implementations and glue code that ties everything together
Other
## User Interface
- `/electron`: Electron-based control panel UI built with React and TypeScript
## Operating System
- `/nixos`: Custom NixOS configuration with real-time kernel & touchscreen support
## Utilities
- `/ethercat-eeprom-dump`: CLI tool for dumping EtherCAT device EEPROM data
## Documentation
- `/docs`: Project documentation, architecture guides, and developer resources
- `/docs`: Documentation for the project.
# Technology Choices
@@ -89,15 +76,9 @@ We could combine the code of the frontend and backend using [Doxius](https://dio
# More Docs
## Architecture & Organization
- [x] [Architecture & Data Flow](./docs/architecture-overview.md)
- [x] [**Organization Summary**](./docs/organization-summary.md) - **Quick reference** for understanding the repository structure
- [x] [Architecture & Data Flow](./docs/architecture-overview.md) - System architecture overview with diagrams
- [x] [Repository Organization](./docs/repository-organization.md) - Monorepo structure, component boundaries, and modularization options
- [x] [Component Interfaces](./docs/component-interfaces.md) - How components interact and interface boundaries
- [x] [Migration Guide for Splitting](./docs/migration-guide-splitting.md) - Step-by-step guide for splitting into multiple repositories (if needed)
## Component Documentation
- [x] Example Winder V2
- [ ] Electron
+1 -3
View File
@@ -12,17 +12,15 @@ anyhow = "1.0.100"
axum = "0.8.6"
ethercat_hal = { path = "../ethercat-hal" }
units = { path = "../units" }
ethercrab = "0.6"
interfaces = "0.0.9"
serde = "1.0.219"
serde_json = "1.0.143"
serialport = "4.7.3"
smol = "2.0.2"
socketioxide = "0.17.2"
uom = { version = "0.36.0", default-features = false, features = ["f64"] }
bitvec = { version = "1.0.1", features = ["alloc"] }
crc = "3.3.0"
serial = "0.4.0"
tracing = "0.1.41"
erased-serde = "0.4.8"
core_affinity = "0.8.3"
@@ -1,6 +1,6 @@
use std::time::Instant;
use uom::si::{
use units::{
angular_acceleration::radian_per_second_squared,
angular_velocity::radian_per_second,
f64::{AngularAcceleration, AngularVelocity},
@@ -1,6 +1,6 @@
use std::time::Instant;
use uom::si::{
use units::{
acceleration::meter_per_second_squared,
f64::{Acceleration, Velocity},
velocity::meter_per_second,
@@ -1,6 +1,6 @@
use std::time::Instant;
use uom::si::{
use units::{
angle::radian,
angular_acceleration::radian_per_second_squared,
angular_velocity::radian_per_second,
@@ -199,8 +199,8 @@ impl AngularAccelerationPositionController {
/// # Example
/// ```ignore
/// use control_core::controllers::second_degree_motion::angular_acceleration_position_controller::AngularAccelerationPositionController;
/// use uom::si::f64::Angle;
/// use uom::si::angle::degree;
/// use units::f64::Angle;
/// use units::angle::degree;
///
/// let mut controller = AngularAccelerationPositionController::new_simple(
/// Some(Angle::new::<degree>(180.0)),
@@ -1,6 +1,6 @@
use std::time::Instant;
use uom::si::{
use units::{
angular_acceleration::radian_per_second_squared,
angular_jerk::radian_per_second_cubed,
angular_velocity::radian_per_second,
@@ -23,7 +23,7 @@ use super::jerk_speed_controller::JerkSpeedController;
///
/// # Example
/// ```ignore
/// use uom::si::{
/// use units::{
/// angular_velocity::revolution_per_minute,
/// angular_acceleration::revolution_per_minute_per_second,
/// angular_jerk::revolution_per_minute_per_second_squared,
@@ -65,7 +65,7 @@ impl AngularJerkSpeedController {
///
/// # Example
/// ```ignore
/// use uom::si::{
/// use units::{
/// angular_velocity::revolution_per_minute,
/// angular_acceleration::revolution_per_minute_per_second,
/// angular_jerk::revolution_per_minute_per_second_squared,
@@ -127,7 +127,7 @@ impl AngularJerkSpeedController {
///
/// # Example
/// ```ignore
/// use uom::si::{
/// use units::{
/// angular_velocity::revolution_per_minute,
/// angular_acceleration::revolution_per_minute_per_second,
/// angular_jerk::revolution_per_minute_per_second_squared,
@@ -185,7 +185,7 @@ impl AngularJerkSpeedController {
/// # Example
/// ```ignore
/// use std::time::Instant;
/// use uom::si::{angular_velocity::revolution_per_minute, f64::AngularVelocity};
/// use units::{angular_velocity::revolution_per_minute, f64::AngularVelocity};
///
/// let mut controller = AngularJerkSpeedController::new_simple(/* ... */);
/// let target = AngularVelocity::new::<revolution_per_minute>(1500.0);
@@ -1,6 +1,6 @@
use std::time::Instant;
use uom::si::{
use units::{
acceleration::meter_per_second_squared,
f64::{Acceleration, Length, Velocity},
length::meter,
@@ -22,8 +22,8 @@ use super::acceleration_position_controller::{
///
/// # Example
/// ```ignore
/// use uom::si::{length::meter, velocity::meter_per_second, acceleration::meter_per_second_squared};
/// use uom::si::f64::{Length, Velocity, Acceleration};
/// use units::{length::meter, velocity::meter_per_second, acceleration::meter_per_second_squared};
/// use units::f64::{Length, Velocity, Acceleration};
///
/// let position_limit = Length::new::<meter>(10.0);
/// let max_speed = Velocity::new::<meter_per_second>(2.0);
@@ -101,8 +101,8 @@ impl LinearAccelerationPositionController {
///
/// # Example
/// ```ignore
/// use uom::si::{length::meter, velocity::meter_per_second, acceleration::meter_per_second_squared};
/// use uom::si::f64::{Length, Velocity, Acceleration};
/// use units::{length::meter, velocity::meter_per_second, acceleration::meter_per_second_squared};
/// use units::f64::{Length, Velocity, Acceleration};
///
/// let position_limit = Length::new::<meter>(5.0); // Creates [-5.0, +5.0] meter range
/// let max_speed = Velocity::new::<meter_per_second>(1.0); // Creates [-1.0, +1.0] m/s range
@@ -150,7 +150,7 @@ impl LinearAccelerationPositionController {
/// # Example
/// ```ignore
/// use std::time::Instant;
/// use uom::si::{length::meter, f64::Length};
/// use units::{length::meter, f64::Length};
///
/// let mut controller = LinearAccelerationPositionController::new_simple(
/// Some(Length::new::<meter>(10.0)),
@@ -375,7 +375,7 @@ impl LinearAccelerationPositionController {
///
/// # Example
/// ```ignore
/// use uom::si::{length::meter, velocity::meter_per_second, f64::{Length, Velocity}};
/// use units::{length::meter, velocity::meter_per_second, f64::{Length, Velocity}};
///
/// let new_position = Length::new::<meter>(0.0);
///
@@ -1,6 +1,6 @@
use std::time::Instant;
use uom::si::{
use units::{
acceleration::meter_per_second_squared,
f64::{Acceleration, Jerk, Velocity},
jerk::meter_per_second_cubed,
@@ -23,7 +23,7 @@ use super::jerk_speed_controller::JerkSpeedController;
///
/// # Example
/// ```ignore
/// use uom::si::{
/// use units::{
/// velocity::meter_per_second,
/// acceleration::meter_per_second_squared,
/// jerk::meter_per_second_cubed,
@@ -65,7 +65,7 @@ impl LinearJerkSpeedController {
///
/// # Example
/// ```ignore
/// use uom::si::{
/// use units::{
/// velocity::meter_per_second,
/// acceleration::meter_per_second_squared,
/// jerk::meter_per_second_cubed,
@@ -127,7 +127,7 @@ impl LinearJerkSpeedController {
///
/// # Example
/// ```ignore
/// use uom::si::{
/// use units::{
/// velocity::meter_per_second,
/// acceleration::meter_per_second_squared,
/// jerk::meter_per_second_cubed,
@@ -1,4 +1,4 @@
use uom::si::{angle::radian, f64::Angle};
use units::{angle::radian, f64::Angle};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct AngleConverter {
@@ -1,4 +1,4 @@
use uom::si::{
use units::{
angle::revolution,
angular_acceleration::radian_per_second_squared,
angular_velocity::revolution_per_second,
@@ -90,7 +90,7 @@ mod tests {
use super::*;
use approx::assert_relative_eq;
use std::f64::EPSILON;
use uom::si::{
use units::{
angle::degree, angular_acceleration::degree_per_second_squared,
angular_velocity::degree_per_second,
};
@@ -14,14 +14,14 @@
//!
//! ```rust
//! use control_core::converters::circular_converter::CircularConverter;
//! use uom::si::{f64::{Length, Velocity}, length::meter, velocity::meter_per_second};
//! use units::{f64::{Length, Velocity}, length::meter, velocity::meter_per_second};
//!
//! let converter = CircularConverter::from_radius(Length::new::<meter>(0.1));
//! let velocity = Velocity::new::<meter_per_second>(1.0);
//! let angular_velocity = converter.linear_to_angular_velocity(velocity);
//! ```
use uom::si::{
use units::{
acceleration::meter_per_second_squared,
angle::radian,
angular_acceleration::radian_per_second_squared,
@@ -199,7 +199,7 @@ impl CircularConverter {
#[cfg(test)]
mod tests {
use super::*;
use uom::si::length::centimeter;
use units::length::centimeter;
#[test]
fn test_constructors() {
@@ -1,4 +1,4 @@
use uom::si::{
use units::{
angle::revolution,
angular_velocity::revolution_per_second,
f64::{Acceleration, Angle, AngularAcceleration, AngularVelocity, Length, Velocity},
@@ -251,7 +251,7 @@ mod tests {
use super::*;
use approx::assert_relative_eq;
use std::f64::EPSILON;
use uom::si::{
use units::{
acceleration::meter_per_second_squared, angular_acceleration::radian_per_second_squared,
length::meter, velocity::meter_per_second,
};
@@ -1,10 +1,9 @@
use std::{process::Command, sync::Arc, time::Duration};
use ethercrab::{
MainDevice, MainDeviceConfig, PduStorage, Timeouts,
std::{ethercat_now, tx_rx_task},
};
use interfaces::Interface;
use std::{process::Command, sync::Arc, time::Duration};
// Constants for EtherCAT configuration
const IFACE_DISCOVERY_MAX_SUBDEVICES: usize = 16; // Must be power of 2 > 1
@@ -92,9 +91,6 @@ pub async fn discover_ethercat_interface() -> Result<String, anyhow::Error> {
let mut interface: Option<&str> = None;
for i in 0..interfaces.len() {
// Avoid NetworkManager hangups
set_interface_managed(&interfaces[i].name, false);
match test_interface(&interfaces[i].name) {
Ok(_) => {
// if interface found with ethercat Exit early, we expect only one interface with ethercat
@@ -107,7 +103,7 @@ pub async fn discover_ethercat_interface() -> Result<String, anyhow::Error> {
for i in 0..interfaces.len() {
if interface.is_some() && interface.unwrap() == &interfaces[i].name {
continue;
set_interface_managed(&interfaces[i].name, false);
} else {
set_interface_managed(&interfaces[i].name, true);
}
-1
View File
@@ -1,2 +1 @@
pub mod eeprom_identification;
pub mod interface_discovery;
-6
View File
@@ -4,17 +4,11 @@ pub mod downcast;
pub mod ethercat;
pub mod helpers;
pub mod irq_handling;
pub mod machines;
pub mod modbus;
pub mod realtime;
pub mod rest;
pub mod serial;
pub mod socketio;
pub mod transmission;
pub mod uom_extensions;
#[cfg(feature = "video-streaming")]
pub mod video_streaming;
#[macro_use]
extern crate uom;
-27
View File
@@ -1,27 +0,0 @@
use serde_json::Value;
use smol::lock::Mutex;
use std::sync::Arc;
use crate::socketio::namespace::Namespace;
pub trait MachineApi {
fn api_mutate(&mut self, value: Value) -> Result<(), anyhow::Error>;
fn api_event_namespace(&mut self) -> Arc<Mutex<Namespace>>;
/// Returns a list of available video stream identifiers for this machine
#[cfg(feature = "video-streaming")]
fn api_video_streams(&self) -> Vec<String> {
Vec::new()
}
/// Get a video stream receiver for the specified stream ID
/// Returns None if the stream doesn't exist
#[cfg(feature = "video-streaming")]
fn api_get_video_stream(
&mut self,
stream_id: &str,
) -> Option<tokio::sync::broadcast::Receiver<Vec<u8>>> {
let _ = stream_id;
None
}
}
-208
View File
@@ -1,208 +0,0 @@
use crate::downcast::Downcast;
use crate::machines::identification::MachineIdentificationUnique;
use crate::machines::manager::MachineManager;
use crate::socketio::{event::GenericEvent, namespace::Namespace};
use serde::Serialize;
use smol::block_on;
use smol::lock::RwLock;
use std::sync::Arc;
use std::sync::Weak;
use super::Machine;
use anyhow::anyhow;
use smol::{channel::Sender, lock::Mutex};
use socketioxide::extract::SocketRef;
#[derive(Debug)]
pub enum MachineConnection<M: Machine + ?Sized>
where
Self: Sized,
{
Error(anyhow::Error),
Disconnected,
Connected(Arc<Mutex<M>>),
}
#[derive(Debug)]
pub struct MachineSlot<M: Machine + ?Sized>
where
Self: Sized,
{
pub machine_connection: MachineConnection<M>,
pub namespace: Arc<Mutex<Namespace>>,
}
pub type MachineConnectionGeneric = MachineConnection<dyn Machine>;
pub type MachineSlotGeneric = MachineSlot<dyn Machine>;
impl<M: Machine> Clone for MachineConnection<M> {
fn clone(&self) -> Self {
match self {
Self::Connected(m) => Self::Connected(m.clone()),
Self::Error(e) => Self::Error(anyhow!(e.to_string())),
Self::Disconnected => Self::Disconnected,
}
}
}
impl<M: Machine + ?Sized> MachineConnection<M> {
pub fn to_error(&self) -> Option<anyhow::Error> {
match self {
MachineConnection::Error(err) => Some(anyhow!(err.to_string())),
Self::Disconnected => Some(anyhow!("Machine is disconnected")),
_ => None,
}
}
pub fn to_machine(&self) -> Option<Arc<Mutex<M>>> {
match self {
Self::Connected(machine) => Some(machine.clone()),
_ => None,
}
}
}
impl<M: Machine + ?Sized> MachineSlot<M> {
pub fn new(socket_queue_tx: Sender<(SocketRef, Arc<GenericEvent>)>) -> Self {
let namespace = Namespace::new(socket_queue_tx);
Self {
machine_connection: MachineConnection::Disconnected,
namespace: Arc::new(Mutex::new(namespace)),
}
}
pub const fn is_connected(&self) -> bool {
matches!(self.machine_connection, MachineConnection::Connected(_))
}
}
pub trait CrossConnectableMachine<
F: CrossConnectableMachine<F, T>,
T: CrossConnectableMachine<T, F>,
>: Machine
{
fn get_cross_connection(&mut self) -> &mut MachineCrossConnection<F, T>;
}
#[derive(Debug)]
pub struct MachineCrossConnection<
F: CrossConnectableMachine<F, T>,
T: CrossConnectableMachine<T, F>,
> {
machine_manager: Weak<RwLock<MachineManager>>,
machine_identification_unique: MachineIdentificationUnique,
connected_machine: Weak<Mutex<MachineSlot<T>>>,
_make_compiler_happy: std::marker::PhantomData<F>, // This marks tells the typechecker, that we
// need F in our type arguemnts. We are ok with
// it being used only in a fake field. Without
// F being fixed, we would not be anble to implement
// reverse connecting and disconnecting properly,
// as the type on the other side would decay
// to an unknown type. See also https://github.com/qitechgmbh/control/pull/625#discussion_r2379566315
}
#[derive(Serialize, Debug, Clone, Default)]
pub struct MachineCrossConnectionState {
/// Connected Machine
pub machine_identification_unique: Option<MachineIdentificationUnique>,
pub is_available: bool,
}
impl<F: CrossConnectableMachine<F, T>, T: CrossConnectableMachine<T, F>>
MachineCrossConnection<F, T>
{
pub fn new(
manager: Weak<RwLock<MachineManager>>,
machine_identification_unique: &MachineIdentificationUnique,
) -> Self {
Self {
machine_manager: manager,
connected_machine: Weak::new(),
machine_identification_unique: machine_identification_unique.clone(),
_make_compiler_happy: std::marker::PhantomData,
}
}
pub fn set_connected_machine(
&mut self,
machine_identification_unique: &MachineIdentificationUnique,
) {
if let Some(machine_manager) = self.machine_manager.upgrade() {
let machine_manager = block_on(machine_manager.read());
let slot: Arc<Mutex<MachineSlotGeneric>> =
match machine_manager.get(&machine_identification_unique) {
Some(c) => c,
None => return,
};
let slot: Arc<Mutex<MachineSlot<T>>> = match Downcast::downcast(&slot) {
Ok(s) => s,
Err(_) => return,
};
self.connected_machine = Arc::downgrade(&slot);
} else {
panic!("The machine manager has died, cannot do anything at this point");
}
}
pub fn reverse_connect(&self) {
if let Some(slot) = self.connected_machine.upgrade() {
let slot = &mut slot.lock_blocking();
if let MachineConnection::Connected(machine) = &slot.machine_connection {
let mut machine = machine.lock_blocking();
let cross_connection = machine.get_cross_connection();
if !cross_connection.is_connected() {
cross_connection.set_connected_machine(&self.machine_identification_unique);
}
};
}
}
pub fn disconnect(&mut self) {
self.connected_machine = Weak::new();
}
pub fn reverse_disconnect(&mut self) {
if let Some(slot) = self.connected_machine.upgrade() {
let slot = slot.lock_blocking();
if let MachineConnection::Connected(machine) = &slot.machine_connection {
let mut machine = machine.lock_blocking();
let cross_connection = machine.get_cross_connection();
cross_connection.disconnect();
};
}
}
pub fn is_connected(&self) -> bool {
let arc = self.connected_machine.upgrade();
arc.is_some()
}
pub fn to_state(&self) -> MachineCrossConnectionState {
if let Some(slot) = self.connected_machine.upgrade() {
let connection = &slot.lock_blocking().machine_connection;
if let MachineConnection::Connected(machine) = connection {
let machine = machine.lock_blocking();
return MachineCrossConnectionState {
machine_identification_unique: Some(
machine.get_machine_identification_unique(),
),
is_available: true,
};
}
}
MachineCrossConnectionState {
machine_identification_unique: None,
is_available: false,
}
}
}
-113
View File
@@ -1,113 +0,0 @@
use std::fmt::Display;
use serde::Deserialize;
use serde::Serialize;
/// Identifies a spacifi machine
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
pub struct MachineIdentificationUnique {
pub machine_identification: MachineIdentification,
pub serial: u16,
}
impl MachineIdentificationUnique {
/// Check if values are non-zero
pub const fn is_valid(&self) -> bool {
self.machine_identification.is_valid() && self.serial != 0
}
}
impl Display for MachineIdentificationUnique {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{}/{}/{}",
self.machine_identification.vendor, self.machine_identification.machine, self.serial
)
}
}
/// Identifies a machine
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
pub struct MachineIdentification {
pub vendor: u16,
pub machine: u16,
}
impl MachineIdentification {
/// Check if values are non-zero
pub const fn is_valid(&self) -> bool {
self.vendor != 0 && self.machine != 0
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct DeviceMachineIdentification {
pub machine_identification_unique: MachineIdentificationUnique,
pub role: u16,
}
impl DeviceMachineIdentification {
/// Check if values are non-zero
pub const fn is_valid(&self) -> bool {
self.machine_identification_unique.is_valid()
&& self
.machine_identification_unique
.machine_identification
.machine
!= 0
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct DeviceIdentification {
pub device_machine_identification: Option<DeviceMachineIdentification>,
pub device_hardware_identification: DeviceHardwareIdentification,
}
impl From<DeviceIdentificationIdentified> for DeviceIdentification {
fn from(value: DeviceIdentificationIdentified) -> Self {
Self {
device_machine_identification: Some(value.device_machine_identification),
device_hardware_identification: value.device_hardware_identification,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct DeviceIdentificationIdentified {
pub device_machine_identification: DeviceMachineIdentification,
pub device_hardware_identification: DeviceHardwareIdentification,
}
impl TryFrom<DeviceIdentification> for DeviceIdentificationIdentified {
type Error = anyhow::Error;
fn try_from(value: DeviceIdentification) -> Result<Self, Self::Error> {
let device_machine_identification =
value.device_machine_identification.ok_or(anyhow::anyhow!(
"[{}::DeviceIdentificationIdentified::try_from] No device machine identification",
module_path!()
))?;
Ok(Self {
device_machine_identification,
device_hardware_identification: value.device_hardware_identification,
})
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub enum DeviceHardwareIdentification {
Ethercat(DeviceHardwareIdentificationEthercat),
Serial(DeviceHardwareIdentificationSerial),
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct DeviceHardwareIdentificationEthercat {
pub subdevice_index: usize,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct DeviceHardwareIdentificationSerial {
pub path: String,
}
-297
View File
@@ -1,297 +0,0 @@
use super::{
Machine,
identification::{DeviceIdentification, DeviceIdentificationIdentified},
new::MachineNewHardwareSerial,
registry::MachineRegistry,
};
use smol::{
channel::Sender,
lock::{Mutex, RwLock},
};
use socketioxide::extract::SocketRef;
use crate::{
machines::{
connection::{
MachineConnection, MachineConnectionGeneric, MachineSlot, MachineSlotGeneric,
},
identification::MachineIdentificationUnique,
new::{MachineNewHardware, MachineNewHardwareEthercat, MachineNewParams},
},
serial::SerialDevice,
socketio::event::GenericEvent,
};
use std::{
collections::HashMap,
sync::{Arc, Weak},
};
#[derive(Debug)]
pub struct MachineManager {
pub ethercat_machines: HashMap<MachineIdentificationUnique, Arc<Mutex<MachineSlotGeneric>>>,
pub serial_machines: HashMap<MachineIdentificationUnique, Arc<Mutex<MachineSlotGeneric>>>,
}
impl Default for MachineManager {
fn default() -> Self {
Self::new()
}
}
impl MachineManager {
pub fn new() -> Self {
Self {
ethercat_machines: HashMap::new(),
serial_machines: HashMap::new(),
}
}
pub fn set_ethercat_devices<const MAX_SUBDEVICES: usize, const MAX_PDI: usize>(
&mut self,
device_identifications: &Vec<DeviceIdentification>,
machine_registry: &MachineRegistry,
hardware: &MachineNewHardwareEthercat,
socket_queue_tx: Sender<(SocketRef, Arc<GenericEvent>)>,
machine_manager: Weak<RwLock<Self>>,
) {
// group devices by machine device identification
let device_grouping_result = group_devices_by_identification(device_identifications);
tracing::info!(
"Device Groups {}",
device_grouping_result.device_groups.len()
);
let machine_new_hardware = MachineNewHardware::Ethercat(hardware);
// iterate over all identified device groups but ignore unaffected machines
for device_group in device_grouping_result.device_groups.iter() {
// get the machine identification
let machine_identification_unique: MachineIdentificationUnique =
match device_group.first() {
Some(device_identification) => device_identification
.device_machine_identification
.machine_identification_unique
.clone(),
None => continue, // Skip this group if empty
};
let slot =
self.get_or_create_slot(socket_queue_tx.clone(), machine_identification_unique);
let mut slot = slot.lock_blocking();
// create the machine
let new_machine = machine_registry.new_machine(&MachineNewParams {
device_group,
hardware: &machine_new_hardware,
socket_queue_tx: socket_queue_tx.clone(),
machine_manager: machine_manager.clone(),
namespace: slot.namespace.clone(),
});
slot.machine_connection = match new_machine {
Err(err) => MachineConnectionGeneric::Error(err),
Ok(machine) => MachineConnectionGeneric::Connected(machine),
};
}
}
fn get_or_create_slot(
&mut self,
socket_queue_tx: Sender<(SocketRef, Arc<GenericEvent>)>,
machine_identification: MachineIdentificationUnique,
) -> Arc<Mutex<MachineSlotGeneric>> {
if let Some(slot) = self.get(&machine_identification) {
return slot;
}
let slot = Arc::new(Mutex::new(MachineSlot::new(socket_queue_tx)));
self.ethercat_machines
.insert(machine_identification, slot.clone());
return slot;
}
pub fn get(
&self,
machine_identification: &MachineIdentificationUnique,
) -> Option<Arc<Mutex<MachineSlotGeneric>>> {
self.ethercat_machines
.get(machine_identification)
.or_else(|| self.serial_machines.get(machine_identification))
.map(|x| x.clone())
}
pub fn add_serial_device(
&mut self,
device_identification: &DeviceIdentification,
device: Arc<RwLock<dyn SerialDevice>>,
machine_registry: &MachineRegistry,
socket_queue_tx: Sender<(SocketRef, Arc<GenericEvent>)>,
machine_manager: Weak<RwLock<Self>>,
) {
let hardware = MachineNewHardwareSerial { device };
let device_identification_identified: DeviceIdentificationIdentified =
device_identification
.clone()
.try_into()
.expect("Serial devices always have machine identification");
let machine_identification: MachineIdentificationUnique = device_identification_identified
.device_machine_identification
.machine_identification_unique
.clone();
let slot = self.get_or_create_slot(socket_queue_tx.clone(), machine_identification);
let mut slot = slot.lock_blocking();
let new_machine = machine_registry.new_machine(&MachineNewParams {
device_group: &vec![device_identification_identified.clone()],
hardware: &MachineNewHardware::Serial(&hardware),
socket_queue_tx,
machine_manager: machine_manager.clone(),
namespace: slot.namespace.clone(),
});
slot.machine_connection = match new_machine {
Err(err) => MachineConnectionGeneric::Error(err),
Ok(machine) => MachineConnectionGeneric::Connected(machine),
};
tracing::info!("Adding serial machine {:?}", slot);
}
pub fn remove_serial_device(&mut self, device_identification: &DeviceIdentification) {
let device_identification_identified: DeviceIdentificationIdentified =
device_identification
.clone()
.try_into()
.expect("Serial devices always have machine identification");
tracing::info!(
"Removing serial machine {:?}",
device_identification_identified
);
let machine_identification_unique = &device_identification_identified
.device_machine_identification
.machine_identification_unique;
if let Some(slot) = self.serial_machines.get(machine_identification_unique) {
let mut slot = slot.lock_blocking();
slot.machine_connection = MachineConnection::Disconnected;
}
}
pub fn get_ethercat_weak(
&self,
machine_identification: &MachineIdentificationUnique,
) -> Option<Weak<Mutex<dyn Machine>>> {
self.get_weak(&self.ethercat_machines, machine_identification)
}
pub fn get_serial_weak(
&self,
machine_identification: &MachineIdentificationUnique,
) -> Option<Weak<Mutex<dyn Machine>>> {
self.get_weak(&self.serial_machines, machine_identification)
}
fn get_weak(
&self,
machines: &HashMap<MachineIdentificationUnique, Arc<Mutex<MachineSlotGeneric>>>,
machine_identification: &MachineIdentificationUnique,
) -> Option<Weak<Mutex<dyn Machine>>> {
let slot = machines.get(machine_identification).clone();
return slot.and_then(|slot| {
let connection = &slot.lock_blocking().machine_connection;
match connection {
MachineConnectionGeneric::Error(_) => None,
MachineConnectionGeneric::Disconnected => None,
MachineConnectionGeneric::Connected(machine) => Some(Arc::downgrade(&machine)),
}
});
}
}
/// Groups devices by machine identification
///
/// Returns a DeviceGroupingResult containing:
/// - device_groups: a vector of devices grouped by machine identification
/// - unidentified_devices: a vector of devices that could not be identified
pub fn group_devices_by_identification(
device_identifications: &Vec<DeviceIdentification>,
) -> DeviceGroupingResult {
let mut device_groups: Vec<Vec<DeviceIdentificationIdentified>> = Vec::new();
let mut unidentified_devices: Vec<DeviceIdentification> = Vec::new();
for device_identification in device_identifications {
// if vendor or serial or machine is 0, it is not a valid machine device
if let Some(device_machine_identification) =
device_identification.device_machine_identification.as_ref()
{
if !device_machine_identification.is_valid() {
unidentified_devices.push(device_identification.clone());
continue;
}
} else {
unidentified_devices.push(device_identification.clone());
continue;
}
// scan over all deice groups
// get the first DeviceMachineIdentification
// compare and append to the group
let mut found = false;
for check_group in device_groups.iter_mut() {
// get first device in group
let first_device = check_group.first().expect("group to not be empty");
let first_device_machine_identification = &first_device
.device_machine_identification
.machine_identification_unique;
// chek if it has machine identification
if let Some(device_machine_identification) =
device_identification.device_machine_identification.as_ref()
{
// compare with the current device
if first_device_machine_identification
== &device_machine_identification.machine_identification_unique
{
let device_identification_identified = device_identification
.clone()
.try_into()
.expect("should have Some(DeviceMachineIdentification)");
check_group.push(device_identification_identified);
found = true;
break;
}
}
}
if !found {
let device_identification_identified = device_identification
.clone()
.try_into()
.expect("should have Some(DeviceMachineIdentification)");
device_groups.push(vec![device_identification_identified]);
}
}
DeviceGroupingResult {
device_groups,
unidentified_devices,
}
}
/// Structure to hold the result of grouping devices by identification
#[derive(Debug)]
pub struct DeviceGroupingResult {
/// Devices grouped by machine identification
pub device_groups: Vec<Vec<DeviceIdentificationIdentified>>,
/// Devices that could not be identified
pub unidentified_devices: Vec<DeviceIdentification>,
}
-84
View File
@@ -1,84 +0,0 @@
use std::collections::HashMap;
use std::sync::Arc;
use smol::lock::Mutex;
use crate::machines::connection::MachineSlotGeneric;
use super::{identification::MachineIdentificationUnique, manager::MachineManager};
pub enum MachineManagerIter<'a> {
EthercatMachines {
iter: std::collections::hash_map::Iter<
'a,
MachineIdentificationUnique,
Arc<Mutex<MachineSlotGeneric>>,
>,
},
SerialMachines {
iter: std::collections::hash_map::Iter<
'a,
MachineIdentificationUnique,
Arc<Mutex<MachineSlotGeneric>>,
>,
},
Done,
}
pub struct MachineManagerIterator<'a> {
iter: MachineManagerIter<'a>,
serial_machines: &'a HashMap<MachineIdentificationUnique, Arc<Mutex<MachineSlotGeneric>>>,
}
impl<'a> Iterator for MachineManagerIterator<'a> {
type Item = (
&'a MachineIdentificationUnique,
&'a Arc<Mutex<MachineSlotGeneric>>,
);
fn next(&mut self) -> Option<Self::Item> {
loop {
match &mut self.iter {
MachineManagerIter::EthercatMachines { iter } => {
// Try to get the next item from ethercat_machines
if let Some(item) = iter.next() {
return Some(item);
} else {
// If ethercat_machines is exhausted, move to serial_machines
self.iter = MachineManagerIter::SerialMachines {
iter: self.serial_machines.iter(),
};
// Continue to the next iteration of the loop
continue;
}
}
MachineManagerIter::SerialMachines { iter } => {
// Return the next item from serial_machines or None if exhausted
if let Some(item) = iter.next() {
return Some(item);
} else {
// If serial_machines is also exhausted, we're done
self.iter = MachineManagerIter::Done;
return None;
}
}
MachineManagerIter::Done => {
// We've exhausted both collections
return None;
}
}
}
}
}
impl MachineManager {
// Returns an iterator over all machines (both ethercat and serial)
pub fn iter(&self) -> MachineManagerIterator<'_> {
MachineManagerIterator {
iter: MachineManagerIter::EthercatMachines {
iter: self.ethercat_machines.iter(),
},
serial_machines: &self.serial_machines,
}
}
}
-23
View File
@@ -1,23 +0,0 @@
use new::MachineNewTrait;
use std::any::Any;
use std::fmt::Debug;
use crate::machines::{
api::MachineApi, identification::MachineIdentificationUnique, new::MachineAct,
};
pub mod api;
pub mod connection;
pub mod identification;
pub mod manager;
pub mod manager_iter;
pub mod new;
pub mod registry;
pub trait Machine: MachineAct + MachineNewTrait + MachineApi + Any + Debug + Send + Sync {
fn get_machine_identification_unique(&self) -> MachineIdentificationUnique;
}
pub trait AnyGetters: Any {
fn as_any(&self) -> &dyn Any;
fn as_any_mut(&mut self) -> &mut dyn Any;
}
-279
View File
@@ -1,279 +0,0 @@
use crate::{
machines::{identification::MachineIdentificationUnique, manager::MachineManager},
serial::SerialDevice,
socketio::namespace::Namespace,
};
use super::identification::DeviceIdentificationIdentified;
use anyhow::Error;
use ethercat_hal::{
devices::EthercatDevice, helpers::ethercrab_types::EthercrabSubDevicePreoperational,
};
use ethercrab::{SubDevice, SubDeviceRef};
use smol::{
channel::Sender,
lock::{Mutex, RwLock},
};
use socketioxide::extract::SocketRef;
use std::{
sync::{Arc, Weak},
time::Instant,
};
use crate::socketio::event::GenericEvent;
pub trait MachineNewTrait {
fn new(params: &MachineNewParams<'_, '_, '_, '_, '_, '_, '_>) -> Result<Self, Error>
where
Self: Sized;
}
pub trait MachineAct {
fn act(&mut self, now: Instant);
}
pub struct MachineNewParams<
'maindevice,
'subdevices,
'device_identifications_identified,
'ethercat_devices,
'machine_new_hardware_etehrcat,
'machine_new_hardware_serial,
'machine_new_hardware,
> where
'maindevice: 'machine_new_hardware,
'subdevices: 'machine_new_hardware,
'ethercat_devices: 'machine_new_hardware,
'machine_new_hardware_etehrcat: 'machine_new_hardware,
{
pub device_group: &'device_identifications_identified Vec<DeviceIdentificationIdentified>,
pub hardware: &'machine_new_hardware MachineNewHardware<
'maindevice,
'subdevices,
'ethercat_devices,
'machine_new_hardware_etehrcat,
'machine_new_hardware_serial,
>,
pub socket_queue_tx: Sender<(SocketRef, Arc<GenericEvent>)>,
pub machine_manager: Weak<RwLock<MachineManager>>,
pub namespace: Arc<Mutex<Namespace>>,
}
impl MachineNewParams<'_, '_, '_, '_, '_, '_, '_> {
pub fn get_machine_identification_unique(&self) -> MachineIdentificationUnique {
self.device_group
.first()
.expect("device group must have at least one device")
.device_machine_identification
.machine_identification_unique
.clone()
}
}
pub enum MachineNewHardware<
'maindevice,
'subdevices,
'ethercat_devices,
'machine_new_hardware_etehrcat,
'machine_new_hardware_serial,
> where
'maindevice: 'machine_new_hardware_etehrcat,
'subdevices: 'machine_new_hardware_etehrcat,
'ethercat_devices: 'machine_new_hardware_etehrcat,
{
Ethercat(
&'machine_new_hardware_etehrcat MachineNewHardwareEthercat<
'maindevice,
'subdevices,
'ethercat_devices,
>,
),
Serial(&'machine_new_hardware_serial MachineNewHardwareSerial),
}
pub struct MachineNewHardwareEthercat<'maindevice, 'subdevices, 'ethercat_devices> {
pub subdevices:
&'subdevices Vec<&'subdevices SubDeviceRef<'maindevice, &'subdevices SubDevice>>,
pub ethercat_devices: &'ethercat_devices Vec<Arc<RwLock<dyn EthercatDevice>>>,
}
pub struct MachineNewHardwareSerial {
pub device: Arc<RwLock<dyn SerialDevice>>,
}
// validates that all devices in the group have the same machine identification
pub fn validate_same_machine_identification_unique(
identified_device_group: &Vec<DeviceIdentificationIdentified>,
) -> Result<(), Error> {
let machine_identification_unique = &identified_device_group
.first()
.ok_or_else(|| {
anyhow::anyhow!(
"[{}::validate_same_machine_identification] No devices in group",
module_path!()
)
})?
.device_machine_identification
.machine_identification_unique;
for device in identified_device_group.iter() {
if device
.device_machine_identification
.machine_identification_unique
!= *machine_identification_unique
{
return Err(anyhow::anyhow!(
"[{}::validate_same_machine_identification] Different machine identifications",
module_path!()
));
}
}
Ok(())
}
/// validates that every role is unique
pub fn validate_no_role_dublicates(
identified_device_group: &Vec<DeviceIdentificationIdentified>,
) -> Result<(), Error> {
let mut roles = vec![];
for device in identified_device_group.iter() {
if roles.contains(&device.device_machine_identification.role) {
return Err(anyhow::anyhow!(
"[{}::validate_no_role_dublicates] Role dublicate",
module_path!(),
));
}
roles.push(device.device_machine_identification.role);
}
Ok(())
}
// Inside control_core::machines::new module:
pub fn get_device_identification_by_role(
identified_device_group: &Vec<DeviceIdentificationIdentified>,
role: u16,
) -> Result<&DeviceIdentificationIdentified, Error> {
for device in identified_device_group.iter() {
if device.device_machine_identification.role == role {
return Ok(device);
}
}
Err(anyhow::anyhow!(
"[{}::get_device_identification_by_role] Role {} not found",
module_path!(),
role
))
}
pub fn get_device_by_index<'maindevice>(
devices: &Vec<Arc<RwLock<dyn EthercatDevice>>>,
subdevice_index: usize,
) -> Result<Arc<RwLock<dyn EthercatDevice>>, Error> {
Ok(devices
.get(subdevice_index)
.ok_or(anyhow::anyhow!(
"[{}::get_device_by_index] Index {} out of bounds for devices",
module_path!(),
subdevice_index
))?
.clone())
}
pub fn get_subdevice_by_index<'subdevices, 'maindevice>(
subdevices: &'subdevices Vec<&EthercrabSubDevicePreoperational<'maindevice>>,
subdevice_index: usize,
) -> Result<&'subdevices EthercrabSubDevicePreoperational<'maindevice>, Error> {
Ok(subdevices.get(subdevice_index).ok_or(anyhow::anyhow!(
"Index {} out of bounds for subdevices",
subdevice_index
))?)
}
pub fn get_ethercat_device_by_index<'maindevice>(
ethercat_devices: &Vec<Arc<RwLock<dyn EthercatDevice>>>,
subdevice_index: usize,
) -> Result<Arc<RwLock<dyn EthercatDevice>>, Error> {
Ok(ethercat_devices
.get(subdevice_index)
.ok_or(anyhow::anyhow!(
"[{}::get_ethercat_device_by_index] Index {} out of bounds for ethercat devices",
module_path!(),
subdevice_index
))?
.clone())
}
#[cfg(test)]
mod tests {
use crate::machines::identification::{
DeviceHardwareIdentification, DeviceHardwareIdentificationEthercat,
DeviceMachineIdentification, MachineIdentification, MachineIdentificationUnique,
};
pub use super::*;
#[test]
fn test_get_device_identification_by_role() {
let device_identifications = vec![
// role 0
DeviceIdentificationIdentified {
device_machine_identification: DeviceMachineIdentification {
machine_identification_unique: MachineIdentificationUnique {
machine_identification: MachineIdentification {
vendor: 1,
machine: 1,
},
serial: 1,
},
role: 0,
},
device_hardware_identification: DeviceHardwareIdentification::Ethercat(
DeviceHardwareIdentificationEthercat { subdevice_index: 0 },
),
},
// role 1
DeviceIdentificationIdentified {
device_machine_identification: DeviceMachineIdentification {
machine_identification_unique: MachineIdentificationUnique {
machine_identification: MachineIdentification {
vendor: 2,
machine: 2,
},
serial: 2,
},
role: 2,
},
device_hardware_identification: DeviceHardwareIdentification::Ethercat(
DeviceHardwareIdentificationEthercat { subdevice_index: 1 },
),
},
// role 2
DeviceIdentificationIdentified {
device_machine_identification: DeviceMachineIdentification {
machine_identification_unique: MachineIdentificationUnique {
machine_identification: MachineIdentification {
vendor: 3,
machine: 3,
},
serial: 3,
},
role: 1,
},
device_hardware_identification: DeviceHardwareIdentification::Ethercat(
DeviceHardwareIdentificationEthercat { subdevice_index: 2 },
),
},
];
// search for role 0
let result = get_device_identification_by_role(&device_identifications, 0);
assert_eq!(result.unwrap().device_machine_identification.role, 0,);
// search for role 1
let result = get_device_identification_by_role(&device_identifications, 1);
assert_eq!(result.unwrap().device_machine_identification.role, 1,);
// search for role 2
let result = get_device_identification_by_role(&device_identifications, 2);
assert_eq!(result.unwrap().device_machine_identification.role, 2,);
}
}
-1
View File
@@ -1 +0,0 @@
pub mod mutation;
+2 -2
View File
@@ -1,11 +1,10 @@
use std::{any::Any, sync::Arc};
/*use std::{any::Any, sync::Arc};
use smol::lock::RwLock;
use std::fmt::Debug;
use crate::machines::identification::DeviceIdentification;
pub mod registry;
pub mod serial_detection;
pub trait SerialDevice: Any + Send + Sync + SerialDeviceNew + Debug {}
@@ -31,3 +30,4 @@ pub struct SerialDeviceIdentification {
pub vendor_id: u16,
pub product_id: u16,
}
*/
-1
View File
@@ -1,3 +1,2 @@
pub mod event;
pub mod namespace;
pub mod namespace_id;
+2 -2
View File
@@ -4,7 +4,7 @@ use socketioxide::extract::SocketRef;
use std::{collections::HashMap, sync::Arc, time::Duration};
use tracing::instrument;
#[derive(Debug)]
#[derive(Debug, Clone)]
pub struct Namespace {
pub sockets: Vec<SocketRef>,
pub events: HashMap<String, Vec<Arc<GenericEvent>>>,
@@ -167,7 +167,7 @@ impl Namespace {
impl Drop for Namespace {
fn drop(&mut self) {
self.disconnect_all();
//self.disconnect_all();
}
}
+1 -1
View File
@@ -1,4 +1,4 @@
use uom::si::{
use units::{
angle::radian,
angular_velocity::radian_per_second,
f64::{Angle, AngularVelocity, Length, Velocity},
@@ -1,7 +0,0 @@
unit! {
system: uom::si;
quantity: uom::si::acceleration;
@meter_per_minute_per_second: 0.016_666_666_666_666_67; "m/min/s", "meters per minute per second",
"meters per minute per second";
}
@@ -1,7 +0,0 @@
unit! {
system: uom::si;
quantity: uom::si::angular_acceleration;
@revolution_per_minute_per_second: 0.104_719_755_119_659_77; "rev/min/s", "revolutions per minute per second",
"revolutions per minute per second";
}
@@ -1,7 +0,0 @@
unit! {
system: uom::si;
quantity: uom::si::angular_jerk;
@revolution_per_minute_per_second_squared: 0.104_719_755_119_659_77; "rev/min/s²", "revolution per minute per second squared",
"revolution per minute per second squared";
}
-7
View File
@@ -1,7 +0,0 @@
unit! {
system: uom::si;
quantity: uom::si::jerk;
@meter_per_minute_per_second_squared: 0.016_666_666_666_666_666_67; "m/min/s²", "meters per minute per second squared",
"meters per minute per second squared";
}
-5
View File
@@ -1,5 +0,0 @@
pub mod acceleration;
pub mod angular_acceleration;
pub mod angular_jerk;
pub mod jerk;
pub mod velocity;
@@ -1,7 +0,0 @@
unit! {
system: uom::si;
quantity: uom::si::velocity;
@meter_per_minute: 0.016_666_666_666_666_67; "m/min", "meters per minute",
"meters per minute";
}
-5
View File
@@ -1,9 +1,4 @@
# Architecture Overview
For detailed information about repository organization and component separation, see:
- [Repository Organization](./repository-organization.md) - Monorepo structure and modularization options
- [Component Interfaces](./component-interfaces.md) - How components interact and interface boundaries
![](./assets/architecture-overview.png)
## The Application is Split Into 3 Main Layers
-346
View File
@@ -1,346 +0,0 @@
# Component Interfaces and Boundaries
This document describes the interfaces between major components in the QiTech Control system and how they interact with each other.
## Component Layering
The system follows a clear layered architecture:
```
┌─────────────────────────────────────────────────────┐
│ User Interface │
│ (Electron) │
└────────────────────┬────────────────────────────────┘
│ REST API / SocketIO
▼
┌─────────────────────────────────────────────────────┐
│ Machine Implementations │
│ (server) │
└────────────────────┬────────────────────────────────┘
│ Trait implementations
▼
┌─────────────────────────────────────────────────────┐
│ Control Framework │
│ (control-core) │
└────────────────────┬────────────────────────────────┘
│ Device traits
▼
┌─────────────────────────────────────────────────────┐
│ Hardware Abstraction Layer │
│ (ethercat-hal) │
└─────────────────────────────────────────────────────┘
```
## 1. Hardware Abstraction Layer (ethercat-hal)
### Public Interface
The HAL provides traits and types for working with EtherCAT devices:
**Core Traits:**
- `Device`: Base trait for all EtherCAT devices
- `Group`: Trait for grouping related devices
- `PDO`: Process Data Object handling
- `CoE`: CANopen over EtherCAT configuration
**Key Types:**
- Input/Output primitives (digital, analog)
- Device configurations
- Error types
**Dependencies:**
- External: `ethercrab`, `smol`, `uom`, `bitvec`
- Internal: None (lowest layer)
**Usage Example:**
```rust
use ethercat_hal::{Device, Group};
// Implement Device trait for a custom EtherCAT device
impl Device for MyCustomDevice {
// Implementation details
}
```
### Design Principles
- **Generic**: No knowledge of specific machines or control logic
- **Stateless**: Devices are primarily data structures for I/O
- **Type-safe**: Uses Rust's type system for safe I/O operations
- **Zero-cost abstractions**: Minimal runtime overhead
## 2. Control Framework (control-core)
### Public Interface
The control framework provides abstractions for building control systems:
**Core Traits:**
- `Actor`: Trait for control logic components that run in the control loop
- `Machine`: Trait for machine implementations
- `MachineValidation`: Trait for validating machine configurations
**Key Features:**
- Actor system for modular control logic
- SocketIO namespace abstractions
- REST API helpers
- Device identification and grouping
- Caching mechanisms
**Dependencies:**
- Internal: `ethercat-hal` (for device types)
- External: `socketioxide`, `axum`, `serde`, `smol`
**Usage Example:**
```rust
use control_core::{Actor, Machine, Group};
// Implement Actor for a control component
struct PIDController {
// ... fields
}
impl Actor for PIDController {
fn act(&mut self, group: &Group) -> anyhow::Result<()> {
// Control logic that runs each cycle
Ok(())
}
}
// Implement Machine for a specific machine type
struct MyMachine {
actors: Vec<Box<dyn Actor>>,
}
impl Machine for MyMachine {
// Implementation details
}
```
### Design Principles
- **Generic**: Machine-agnostic, works for any industrial control application
- **Composable**: Actors can be combined to build complex control logic
- **Real-time capable**: Designed for deterministic execution
- **API-ready**: Built-in SocketIO and REST abstractions
## 3. Machine Implementations (server)
### Responsibilities
The server crate contains QiTech-specific implementations:
1. **Machine Implementations** (`src/machines/`)
- Winder, Extruder, Laser, etc.
- Each machine implements the `Machine` trait
- Contains machine-specific actors and logic
2. **EtherCAT Setup** (`src/ethercat/`)
- Interface discovery
- Device enumeration
- Group creation
3. **Serial Devices** (`src/serial/`)
- Non-EtherCAT devices (e.g., laser sensors)
- Serial protocol implementations
4. **API Handlers** (`src/rest/`, `src/socketio/`)
- Machine mutation endpoints
- Real-time status updates
- Device identification management
5. **Control Loop** (`src/loop.rs`)
- Main control loop execution
- Thread management
- Actor coordination
**Dependencies:**
- Internal: `control-core`, `ethercat-hal`
- External: `axum`, `socketioxide`, `tokio`, `smol`
**Example Machine Implementation:**
```rust
use control_core::{Machine, Actor, Group};
use ethercat_hal::Device;
pub struct WinderV2 {
// Actors
puller: Puller,
traverse: Traverse,
// ... other components
}
impl Machine for WinderV2 {
fn act(&mut self, group: &Group) -> anyhow::Result<()> {
// Coordinate all actors
self.puller.act(group)?;
self.traverse.act(group)?;
Ok(())
}
}
```
### Design Principles
- **Machine-specific**: Contains QiTech business logic
- **Uses framework**: Builds on `control-core` and `ethercat-hal`
- **API implementation**: Provides REST and SocketIO endpoints
- **Integration point**: Ties together hardware, framework, and UI
## 4. User Interface (electron)
### Interface with Backend
The Electron UI communicates with the server through two channels:
#### REST API
Used for request/response operations:
- **Machine mutations**: Start, stop, change parameters
- **Device identification**: Read/write device IDs
- **Configuration**: System settings
**Example:**
```typescript
// POST /machine/mutation
const response = await fetch('http://localhost:3001/machine/mutation', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
mutation: 'Start',
parameters: { /* ... */ }
})
});
```
#### SocketIO
Used for real-time updates:
- **Main namespace** (`/`): System-wide events (machine discovery, errors)
- **Machine namespaces** (`/machine/<vendor>/<id>`): Machine-specific state updates
**Example:**
```typescript
import { io } from 'socket.io-client';
const socket = io('http://localhost:3001/machine/1/2');
socket.on('state', (machineState) => {
// Update UI with new machine state
updateDisplay(machineState);
});
```
### Design Principles
- **Decoupled**: Communicates via APIs, not tight coupling
- **Reactive**: Uses SocketIO for real-time updates
- **Machine-aware**: Contains UI for each machine type
- **Reusable**: Built with standard web technologies
## 5. Operating System (nixos)
### Role
Provides the runtime environment:
- Real-time kernel for deterministic control loop execution
- System services for the server
- Touchscreen configuration
- Package management
### Integration
- Builds and packages the `server` binary
- Configures systemd service for automatic startup
- Provides system dependencies (libraries, drivers)
## Interface Guidelines
### For Framework Development (ethercat-hal, control-core)
When working on framework crates:
1. **No QiTech-specific code**: Keep implementations generic
2. **Document public APIs**: Assume external users
3. **Semantic versioning**: Changes should follow semver
4. **Minimal dependencies**: Only add well-maintained, stable crates
5. **Examples**: Provide usage examples in documentation
6. **Testing**: Write tests that don't depend on specific machines
### For Machine Development (server)
When implementing machines:
1. **Use framework traits**: Implement `Machine` and `Actor` traits
2. **Isolate machine code**: Keep machine-specific code in `src/machines/<machine_name>/`
3. **API design**: Design REST/SocketIO APIs with UI in mind
4. **Error handling**: Use `anyhow::Result` for error propagation
5. **Documentation**: Document machine-specific behavior
### For UI Development (electron)
When working on the UI:
1. **API contracts**: Respect REST/SocketIO API contracts
2. **Type safety**: Use TypeScript types for API responses
3. **Machine components**: Keep machine-specific UI in `src/machines/<machine_name>/`
4. **Reusable components**: Extract common patterns to shared components
5. **Error handling**: Handle network errors gracefully
## Cross-Cutting Concerns
### Error Handling
- **HAL**: Returns `anyhow::Result` for device operations
- **Framework**: Uses `anyhow::Result` with context
- **Server**: Propagates errors, logs with `tracing`
- **UI**: Displays user-friendly error messages
### Logging and Tracing
- **Backend**: Uses `tracing` crate with structured logging
- **UI**: Uses console logging and optional telemetry
### Testing
- **Unit tests**: Each crate has unit tests for internal logic
- **Integration tests**: Server has tests that span multiple components
- **E2E tests**: Electron has Playwright tests for UI workflows
### Configuration
- **Server**: TOML configuration file (`config.toml`)
- **UI**: Electron store for user preferences
- **NixOS**: System configuration in Nix files
## Future Considerations
### If Splitting into Multiple Repositories
1. **Version Pinning**: Server would depend on specific versions of framework crates
2. **Release Coordination**: Framework releases need stability guarantees
3. **Breaking Changes**: Would require careful migration planning
4. **Documentation**: Each repository needs comprehensive docs
5. **CI/CD**: Separate pipelines for each repository
### Maintaining Current Structure
1. **Clean Boundaries**: Continue enforcing layer separation
2. **Interface Stability**: Treat framework interfaces as if they were versioned APIs
3. **Documentation**: Keep this document updated
4. **Testing**: Ensure framework crates can be tested independently
## Summary
The QiTech Control system has a well-defined layered architecture:
- **ethercat-hal**: Provides hardware abstraction (reusable)
- **control-core**: Provides control framework (reusable)
- **server**: Implements QiTech machines (specific)
- **electron**: Provides user interface (adaptable)
- **nixos**: Provides runtime environment (adaptable)
Each layer depends only on layers below it, creating clear separation of concerns and enabling potential future modularization.
+1 -13
View File
@@ -1,16 +1,4 @@
# Getting Started
This guide will help you set up your development environment for QiTech Control.
## Understanding the Repository
Before starting, it's recommended to understand the repository organization:
- [Repository Organization](../repository-organization.md) - Monorepo structure and component boundaries
- [Architecture Overview](../architecture-overview.md) - System architecture and data flow
- [Component Interfaces](../component-interfaces.md) - How components interact
## Environment Setup
# Environment Setup
Working on this repository requires you to use cargo and npm. In the following document the installation process will be shown
Binary file not shown.
-498
View File
@@ -1,498 +0,0 @@
# Migration Guide: Splitting the Monorepo
This guide provides a step-by-step approach for splitting the QiTech Control monorepo into multiple repositories, should that become necessary in the future.
## Prerequisites
Before considering a split:
1. **Framework Stability**: Ensure `ethercat-hal` and `control-core` APIs are stable
2. **Documentation**: All framework crates have comprehensive documentation
3. **Tests**: Framework crates have thorough test coverage
4. **Versioning Strategy**: Decide on semantic versioning approach
5. **Team Consensus**: All stakeholders agree on the split
## Recommended Approach: Gradual Migration
### Phase 1: Prepare Framework for External Use
**Goal**: Make framework crates publishable while keeping monorepo structure.
#### Step 1.1: Clean Up Framework Dependencies
Ensure framework crates only depend on:
- Each other (via path dependencies)
- Well-maintained external crates
- No `server` or machine-specific code
```bash
# Check dependency tree
cd control
cargo tree -p ethercat-hal
cargo tree -p control-core
```
#### Step 1.2: Add Package Metadata
Update `ethercat-hal/Cargo.toml` and `control-core/Cargo.toml`:
```toml
[package]
name = "ethercat-hal"
version = "0.1.0"
edition = "2024"
authors = ["QiTech Industries GmbH"]
license = "MIT OR Apache-2.0" # Choose appropriate license
description = "Hardware abstraction layer for EtherCAT devices"
documentation = "https://docs.rs/ethercat-hal"
repository = "https://github.com/qitechgmbh/control-framework"
keywords = ["ethercat", "industrial", "automation", "beckhoff"]
categories = ["embedded", "hardware-support"]
readme = "README.md"
```
#### Step 1.3: Write Framework Documentation
For each framework crate:
1. Create `README.md` with:
- Purpose and features
- Installation instructions
- Quick start example
- Link to full documentation
2. Add crate-level documentation to `src/lib.rs`:
```rust
//! # Ethercat HAL
//!
//! Hardware abstraction layer for EtherCAT devices.
//!
//! ## Quick Start
//!
//! ```no_run
//! use ethercat_hal::{Device, Group};
//! // ... example code
//! ```
```
3. Document all public APIs with examples
#### Step 1.4: Test Framework Isolation
```bash
# Try building framework crates independently
cd ethercat-hal && cargo build
cd ../control-core && cargo build
# Run tests
cd ../ethercat-hal && cargo test
cd ../control-core && cargo test
```
#### Step 1.5: Publish to crates.io (Optional Dry Run)
```bash
cd ethercat-hal
cargo publish --dry-run
cd ../control-core
cargo publish --dry-run
```
### Phase 2: Extract Framework to Separate Repository
**Goal**: Move framework crates to a new repository.
#### Step 2.1: Create Framework Repository
```bash
# Create new repository
git clone https://github.com/qitechgmbh/control-framework.git
cd control-framework
# Set up structure
mkdir -p ethercat-hal ethercat-hal-derive control-core control-core-derive
touch Cargo.toml README.md LICENSE
```
#### Step 2.2: Copy Framework Code with History
Use `git filter-repo` to preserve commit history:
```bash
# Install git-filter-repo
pip install git-filter-repo
# Clone original repo
git clone https://github.com/qitechgmbh/control.git control-temp
cd control-temp
# Extract ethercat-hal with history
git filter-repo --subdirectory-filter ethercat-hal --force
# Push to framework repo
git remote add framework https://github.com/qitechgmbh/control-framework.git
git push framework main:ethercat-hal-history
# Repeat for other framework crates
```
#### Step 2.3: Set Up Framework Workspace
In `control-framework/Cargo.toml`:
```toml
[workspace]
members = [
"ethercat-hal",
"ethercat-hal-derive",
"control-core",
"control-core-derive",
]
resolver = "2"
[workspace.package]
version = "0.1.0"
edition = "2024"
authors = ["QiTech Industries GmbH"]
license = "MIT OR Apache-2.0"
repository = "https://github.com/qitechgmbh/control-framework"
[workspace.lints.clippy]
# ... copy from original
```
#### Step 2.4: Update Internal Dependencies
Update `control-core/Cargo.toml`:
```toml
[dependencies]
ethercat_hal = { version = "0.1.0", path = "../ethercat-hal" }
# ... other deps
```
#### Step 2.5: Add CI/CD
Create `.github/workflows/ci.yml`:
```yaml
name: CI
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: dtolnay/rust-toolchain@stable
- run: cargo test --all-features
- run: cargo clippy -- -D warnings
- run: cargo doc --no-deps
publish:
if: startsWith(github.ref, 'refs/tags/v')
needs: test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: dtolnay/rust-toolchain@stable
- run: cargo publish --token ${CRATES_TOKEN}
env:
CRATES_TOKEN: ${{ secrets.CRATES_TOKEN }}
```
#### Step 2.6: Publish Framework to crates.io
```bash
cd control-framework
# Publish in dependency order
cd ethercat-hal-derive
cargo publish
cd ../ethercat-hal
cargo publish
cd ../control-core-derive
cargo publish
cd ../control-core
cargo publish
```
### Phase 3: Update QiTech Repository
**Goal**: Make QiTech repo depend on published framework crates.
#### Step 3.1: Remove Framework Code
In the original `control` repository:
```bash
cd control
git rm -r ethercat-hal ethercat-hal-derive control-core control-core-derive
```
#### Step 3.2: Update server/Cargo.toml
```toml
[dependencies]
# Replace path dependencies with crates.io dependencies
ethercat_hal = "0.1.0"
control_core = "0.1.0"
control_core_derive = "0.1.0"
# ... other deps remain same
```
#### Step 3.3: Update Workspace
Update root `Cargo.toml`:
```toml
[workspace]
members = [
"server",
"ethercat-eeprom-dump",
]
```
#### Step 3.4: Test Migration
```bash
cd server
cargo build
cargo test
```
#### Step 3.5: Update Documentation
Update `README.md`:
```markdown
# QiTech Control
This repository contains QiTech-specific machine implementations using the
[control-framework](https://github.com/qitechgmbh/control-framework).
## Dependencies
- [ethercat-hal](https://crates.io/crates/ethercat-hal) - Hardware abstraction layer
- [control-core](https://crates.io/crates/control-core) - Control framework
```
### Phase 4: Extract UI (Optional)
**Goal**: Separate Electron UI into its own repository.
#### Step 4.1: Create UI Repository
```bash
git clone https://github.com/qitechgmbh/control-ui.git
cd control-ui
# Copy electron directory
cp -r ../control/electron/* .
```
#### Step 4.2: Make UI Generic
1. Remove QiTech-specific machine UIs or make them plugins
2. Create configuration for API endpoints
3. Add documentation for customization
#### Step 4.3: Publish as npm Package
```json
{
"name": "@qitech/control-ui",
"version": "1.0.0",
"description": "Electron UI for industrial control systems",
"main": "dist/main.js",
"repository": "github:qitechgmbh/control-ui"
}
```
```bash
npm publish --access public
```
#### Step 4.4: Use UI in QiTech Repo
Option A: Git Submodule:
```bash
cd control
git rm -r electron
git submodule add https://github.com/qitechgmbh/control-ui.git electron
```
Option B: npm Package:
```json
{
"dependencies": {
"@qitech/control-ui": "^1.0.0"
}
}
```
## Alternative: Gradual Separation without Multiple Repositories
If full split is not necessary, maintain logical separation within monorepo:
### Use Cargo Workspaces Effectively
```toml
[workspace]
members = [
# Framework (don't depend on server)
"framework/ethercat-hal",
"framework/ethercat-hal-derive",
"framework/control-core",
"framework/control-core-derive",
# Application (depends on framework)
"app/server",
# UI
"ui/electron",
# Utils
"utils/ethercat-eeprom-dump",
]
```
### Enforce Boundaries with Linting
Add a CI check to ensure framework doesn't depend on application:
```bash
#!/bin/bash
# check-dependencies.sh
if grep -r "server" framework/*/Cargo.toml; then
echo "Error: Framework should not depend on server!"
exit 1
fi
```
## Rollback Plan
If migration causes issues:
### Roll Back Phase 3
```bash
cd control
git revert <migration-commit>
git push
```
### Revert to Path Dependencies
Update `server/Cargo.toml`:
```toml
[dependencies]
ethercat_hal = { path = "../ethercat-hal" }
control_core = { path = "../control-core" }
```
### Keep Framework Repo
The framework repository can remain as a mirror/backup even if not actively used.
## Post-Migration Maintenance
### Version Updates
When updating framework:
1. **Framework Repository**:
```bash
cd control-framework
# Make changes
cargo test
# Update version in Cargo.toml
git tag v0.2.0
git push --tags
cargo publish
```
2. **QiTech Repository**:
```bash
cd control/server
# Update Cargo.toml
ethercat_hal = "0.2.0"
cargo update
cargo test
```
### Breaking Changes
When framework has breaking changes:
1. Bump major version (0.1.0 → 0.2.0 or 1.0.0)
2. Write migration guide
3. Support old version for transition period
4. Update QiTech repo when ready
### Documentation
Keep both repositories' documentation in sync:
- Framework: API documentation
- QiTech: Machine implementation examples using framework
## Timeline Estimate
- **Phase 1** (Preparation): 2-4 weeks
- Documentation and cleanup
- Testing isolation
- **Phase 2** (Framework Extraction): 1-2 weeks
- Repository setup
- History migration
- Publishing to crates.io
- **Phase 3** (QiTech Update): 1 week
- Update dependencies
- Testing
- Documentation updates
- **Phase 4** (UI Extraction, Optional): 2-3 weeks
- Repository setup
- Generalization
- Integration testing
**Total: 6-10 weeks** depending on scope
## Success Criteria
- ✅ Framework crates build independently
- ✅ All tests pass in both repositories
- ✅ Documentation is comprehensive
- ✅ CI/CD pipelines are working
- ✅ QiTech machines work with published framework
- ✅ Version management is clear
- ✅ Team is trained on new workflow
## Conclusion
Splitting the monorepo is a significant undertaking that should only be done when:
1. Framework is stable and well-documented
2. There's clear need for independent versioning
3. External users want to use the framework
4. Team has bandwidth to maintain multiple repositories
The gradual approach outlined here minimizes risk by:
- Preparing framework first (Phase 1 can be done immediately)
- Publishing framework while keeping monorepo (reversible)
- Only removing framework code once proven stable
- Maintaining clear rollback options
For now, continue with the monorepo approach while following best practices that keep the option open for future splitting.
-157
View File
@@ -1,157 +0,0 @@
# Repository Organization Summary
This document provides a quick reference for understanding the QiTech Control repository structure.
## Quick Navigation
- **[Repository Organization](./repository-organization.md)** - Complete guide to the monorepo structure
- **[Component Interfaces](./component-interfaces.md)** - How components interact
- **[Migration Guide](./migration-guide-splitting.md)** - How to split into multiple repos (if needed)
- **[Architecture Overview](./architecture-overview.md)** - System architecture diagrams
## At a Glance
### Framework Layer (Reusable)
These components form a generic control framework that could be used by other projects:
```
ethercat-hal + ethercat-hal-derive
↓
control-core + control-core-derive
```
- **Purpose**: Provide abstractions for building industrial control systems
- **Audience**: Could be published to crates.io for external use
- **Dependencies**: Only on external crates and each other
### Application Layer (QiTech-Specific)
```
server/
├── machines/ # Machine implementations (winder, extruder, etc.)
├── ethercat/ # EtherCAT setup
├── serial/ # Serial device handling
├── rest/ # REST API handlers
└── socketio/ # SocketIO handlers
```
- **Purpose**: QiTech-specific machine implementations
- **Audience**: QiTech internal use
- **Dependencies**: Uses framework + external crates
### User Interface
```
electron/
├── src/machines/ # Machine-specific UIs
├── components/ # Reusable UI components
├── routes/ # Application routing
└── ...
```
- **Purpose**: Control panel user interface
- **Technology**: Electron, React, TypeScript
- **Communication**: REST + SocketIO APIs
## Key Principles
1. **Clean Layering**: Framework doesn't depend on application code
2. **Type Safety**: Rust's type system ensures safe hardware access
3. **Real-time Capable**: Designed for deterministic control loops
4. **API-First**: UI communicates through well-defined APIs
5. **Testable**: Each layer can be tested independently
## When to Use What
### Working on Hardware Support
- Edit: `ethercat-hal/`
- Purpose: Adding new EtherCAT devices or improving HAL
### Working on Framework Features
- Edit: `control-core/`
- Purpose: Adding actors, improving control logic abstractions
### Working on Machines
- Edit: `server/src/machines/`
- Purpose: Implementing or modifying machine behavior
### Working on UI
- Edit: `electron/`
- Purpose: UI changes, new screens, UX improvements
### Working on System Configuration
- Edit: `nixos/`
- Purpose: OS configuration, deployment setup
## Common Questions
### Q: Can I depend on `server` from `control-core`?
**A: No.** Framework crates should never depend on application code. This maintains reusability.
### Q: Where should I put reusable control logic?
**A: In `control-core`.** If it's generic and could be used by multiple machines, it belongs in the framework.
### Q: Where should I put QiTech-specific logic?
**A: In `server`.** Machine-specific implementations go in `server/src/machines/`.
### Q: Should we split into multiple repositories?
**A: Not yet.** The current monorepo structure works well for the current development phase. See the [Migration Guide](./migration-guide-splitting.md) for when and how to split if needed.
### Q: How do I ensure my changes maintain clean boundaries?
**A: Follow these guidelines:**
1. Framework code should be generic (no QiTech-specific assumptions)
2. Document public APIs as if for external users
3. Keep machine code isolated in appropriate directories
4. Write tests that verify isolation
## Quick Reference: Dependency Rules
```
✅ Allowed:
- server → control-core → ethercat-hal
- control-core → ethercat-hal
- electron → server (via APIs only)
❌ Not Allowed:
- ethercat-hal → control-core
- control-core → server
- ethercat-hal → server
- server → electron
```
## Getting Started
New to the project? Start here:
1. Read [Getting Started](./developer-docs/getting-started.md)
2. Review [Architecture Overview](./architecture-overview.md)
3. Read [Repository Organization](./repository-organization.md)
4. Look at [Component Interfaces](./component-interfaces.md) for detailed API information
## Contributing
When making changes:
1. **Understand the layer** you're working in
2. **Follow the dependencies** - only depend on lower layers
3. **Document public APIs** - especially in framework crates
4. **Write tests** - unit tests for each layer, integration tests across layers
5. **Keep it clean** - maintain separation of concerns
## Future Considerations
The repository is structured to allow future splitting if needed:
- **Short term**: Continue with monorepo, maintain clean boundaries
- **Medium term**: Prepare framework crates for publication (add docs, examples)
- **Long term**: Consider splitting when framework is stable and there are external users
See [Migration Guide](./migration-guide-splitting.md) for the full plan.
---
For more detailed information, see the complete documentation:
- [Repository Organization](./repository-organization.md)
- [Component Interfaces](./component-interfaces.md)
- [Migration Guide](./migration-guide-splitting.md)
-282
View File
@@ -1,282 +0,0 @@
# Repository Organization
This document explains the current repository structure and provides guidance on how the codebase is organized for maintainability and potential future modularization.
## Overview
The QiTech Control repository is currently a monorepo that contains multiple logical components:
1. **Hardware Abstraction Layer (HAL)** - Low-level EtherCAT device interfaces
2. **Control Framework** - Generic control logic and abstractions
3. **Machine Implementations** - QiTech-specific machine logic
4. **User Interface** - Electron-based control panel UI
5. **Operating System** - NixOS configuration for deployment
## Current Structure
### 1. Hardware Abstraction Layer
**Crates:** `ethercat-hal`, `ethercat-hal-derive`
**Purpose:** Provides low-level abstractions for interfacing with Beckhoff and other EtherCAT devices.
**Key Features:**
- Device trait definitions
- PDO (Process Data Object) handling
- CoE (CANopen over EtherCAT) configuration
- I/O primitives
- Built on top of [ethercrab](https://github.com/ethercrab-rs/ethercrab)
**Dependencies:** External crates only (ethercrab, smol, uom, etc.)
**Reusability:** Highly reusable for any EtherCAT-based control system
### 2. Control Framework
**Crates:** `control-core`, `control-core-derive`
**Purpose:** Provides generic control logic abstractions that can be used across different machine types.
**Key Features:**
- Actor system for control logic components
- SocketIO and REST API abstractions
- Machine trait definitions
- Device identification and validation
- Group management
- Video streaming support (optional)
**Dependencies:** `ethercat-hal`, external crates
**Reusability:** Reusable for building various industrial control systems
### 3. Machine Implementations (QiTech-Specific)
**Crate:** `server`
**Purpose:** Contains QiTech-specific machine implementations and glue code that ties everything together.
**Key Components:**
- **Machine implementations** (`src/machines/`):
- Winder V2
- Extruder V1/V2
- Aquapath (Waterway) V1
- Laser V1
- Buffer V1
- Mock machines for testing
- **EtherCAT setup** (`src/ethercat/`): Interface discovery, configuration
- **Serial device handling** (`src/serial/`): Non-EtherCAT devices
- **REST API handlers** (`src/rest/`): Machine mutations, device identification
- **SocketIO handlers** (`src/socketio/`): Real-time communication
- **Control loop** (`src/loop.rs`): Main control loop execution
**Dependencies:** `control-core`, `ethercat-hal`, external crates
**Reusability:** QiTech-specific, but demonstrates how to use the framework
### 4. User Interface
**Directory:** `electron/`
**Purpose:** Electron-based user interface for controlling and monitoring machines.
**Key Features:**
- React-based UI with TypeScript
- Real-time updates via SocketIO
- REST API client for machine control
- Machine-specific UI components (`src/machines/`)
- Shadcn/UI component library
- TanStack Router for navigation
- Theming and internationalization support
**Dependencies:** Node.js, Electron, React ecosystem
**Reusability:** Could be reused with modifications for similar control systems
### 5. Operating System Configuration
**Directory:** `nixos/`
**Purpose:** NixOS configuration for deployment on control panels.
**Key Features:**
- Real-time kernel configuration
- Touchscreen support
- System service definitions
- Package management
**Reusability:** Reusable for similar embedded Linux deployments
### 6. Utility Crates
**Crate:** `ethercat-eeprom-dump`
**Purpose:** Command-line utility for dumping EtherCAT device EEPROM data.
**Dependencies:** `ethercat-hal`, `control-core`
## Dependency Graph
```
┌─────────────────────┐
│ ethercat-hal │◄─── Low-level HAL
│ + derive macros │
└──────────┬──────────┘
│
│ depends on
▼
┌─────────────────────┐
│ control-core │◄─── Generic framework
│ + derive macros │
└──────────┬──────────┘
│
│ depends on
▼
┌─────────────────────┐
│ server │◄─── QiTech-specific
│ (machine impls) │ implementations
└─────────────────────┘
┌─────────────────────┐
│ electron │◄─── UI (communicates via
│ (UI) │ REST/SocketIO APIs)
└─────────────────────┘
┌─────────────────────┐
│ nixos │◄─── OS configuration
│ (deployment) │
└─────────────────────┘
```
## Conceptual Separation
If this repository were to be split into separate repositories, a logical separation would be:
### Option A: Three Repository Split
1. **`control-framework`** (or `industrial-control-rs`)
- Contains: `ethercat-hal`, `ethercat-hal-derive`, `control-core`, `control-core-derive`
- Purpose: Reusable Rust framework for building industrial control systems
- Audience: Other companies/projects building EtherCAT control systems
- Published to: crates.io
2. **`control-ui`** (or `control-electron-ui`)
- Contains: `electron/` directory
- Purpose: Reusable Electron-based UI for control systems
- Audience: Projects that want a pre-built control panel UI
- Could be: npm package or standalone repository
3. **`control-qitech`**
- Contains: `server/` (machine implementations), `nixos/` (deployment config)
- Dependencies: Uses `control-framework` crates, includes `control-ui` as git submodule
- Purpose: QiTech-specific machine implementations and deployment
- Audience: QiTech internal use
### Option B: Two Repository Split (Simpler)
1. **`control-framework`**
- Contains: `ethercat-hal`, `control-core`, derive macros, `electron/`, `nixos/`
- Purpose: Complete reusable control system framework (backend + UI + OS)
- Published: Core crates to crates.io, UI could be npm package
2. **`control-qitech`**
- Contains: `server/` machine implementations
- Dependencies: Uses `control-framework` crates and UI
- Purpose: QiTech-specific implementations
## Advantages of Current Monorepo Structure
1. **Faster Development**: No need to coordinate versions across repositories
2. **Atomic Changes**: Changes that span multiple components can be done in a single commit
3. **Simplified CI/CD**: Single build pipeline for all components
4. **Easier Testing**: Integration tests can span all components
5. **No Submodule Complexity**: Git submodules can be challenging to work with
6. **Unified Documentation**: All docs in one place
7. **Consistent Versioning**: Everything versioned together
## Advantages of Multi-Repository Structure
1. **Clear Boundaries**: Enforces separation of concerns at repository level
2. **Independent Versioning**: Framework can have stable releases while QiTech-specific code evolves
3. **Access Control**: Different teams can have different permissions
4. **Selective Cloning**: Users only clone what they need
5. **Public/Private Split**: Framework could be open-source while keeping QiTech-specific code private
6. **Clearer Licensing**: Different licenses for different components
7. **Smaller Repositories**: Faster clone times, less clutter
## Recommendations
### For Current Development (Monorepo)
The monorepo structure is appropriate for the current stage of development because:
1. The framework is still evolving rapidly alongside machine implementations
2. The team is small and benefits from unified development workflow
3. Breaking changes across layers can be handled atomically
4. There's no immediate need to share the framework externally
**Best Practices to Maintain:**
- Keep dependencies clean (framework crates should not depend on `server`)
- Document public APIs in framework crates as if they were standalone
- Write framework code generically without QiTech-specific assumptions
- Keep machine-specific code isolated in `server/src/machines/`
### For Future (Multi-Repository)
Consider splitting when:
1. **Framework Stabilization**: When `ethercat-hal` and `control-core` APIs are stable enough for versioned releases
2. **External Users**: When other companies/projects want to use the framework
3. **Team Growth**: When separate teams are working on framework vs. machines
4. **Open Source Plans**: If planning to open-source the framework while keeping machine implementations private
**Migration Path:**
1. Extract `ethercat-hal` and `control-core` to `control-framework` repository
2. Publish to crates.io with semantic versioning
3. Update `server/Cargo.toml` to use published crates instead of path dependencies
4. Optionally extract `electron/` as a separate UI package
5. Keep `server/` as QiTech-specific implementation repository
## Current Approach: Virtual Separation
To get benefits of both approaches:
1. **Maintain Clean Boundaries**: Continue enforcing that framework crates don't depend on `server`
2. **Documentation**: Document framework crates as standalone libraries
3. **Testing**: Write tests for framework crates that don't depend on machine implementations
4. **Cargo Workspaces**: Use workspace organization to group related crates
5. **Future-Ready**: Write code assuming eventual split, making future extraction easier
## Workspace Organization
The workspace is organized to reflect logical grouping:
```toml
[workspace]
members = [
# Framework crates (HAL layer)
"ethercat-hal",
"ethercat-hal-derive",
# Framework crates (Core layer)
"control-core",
"control-core-derive",
# Machine implementations
"server",
# Utilities
"ethercat-eeprom-dump",
]
```
This organization makes it clear which crates are part of the reusable framework and which are application-specific.
## Conclusion
The current monorepo structure is well-suited for QiTech's needs at this stage. The codebase already has good logical separation between:
- Reusable framework components (`ethercat-hal`, `control-core`)
- Machine-specific implementations (`server`)
- User interface (`electron`)
- Deployment configuration (`nixos`)
The key is to maintain clean boundaries and follow best practices that would make a future split straightforward if needed. This gives QiTech the flexibility to evolve the architecture as requirements change without prematurely optimizing for a multi-repository structure.
+12 -36
View File
@@ -175,7 +175,6 @@
"version": "7.28.0",
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.0.tgz",
"integrity": "sha512-UlLAnTPrFdNGoFtbSXwcGFQBtQZJCNjaN6hQNP3UPvuNXT1i82N26KL3dZeIpNalWywr9IuQuncaAfUaS1g6sQ==",
"peer": true,
"dependencies": {
"@ampproject/remapping": "^2.2.0",
"@babel/code-frame": "^7.27.1",
@@ -639,7 +638,6 @@
"url": "https://opencollective.com/csstools"
}
],
"peer": true,
"engines": {
"node": ">=18"
},
@@ -662,7 +660,6 @@
"url": "https://opencollective.com/csstools"
}
],
"peer": true,
"engines": {
"node": ">=18"
}
@@ -1148,7 +1145,6 @@
"integrity": "sha512-zx0EIq78WlY/lBb1uXlziZmDZI4ubcCXIMJ4uGjXzZW0nS19TjSPeXPAjzzTmKQlJUZm0SbmZhPKP7tuQ1SsEw==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"chalk": "^4.1.1",
"fs-extra": "^9.0.1",
@@ -4095,7 +4091,6 @@
"resolved": "https://registry.npmjs.org/@tanstack/react-router/-/react-router-1.130.17.tgz",
"integrity": "sha512-F1tyMEbX7PJclnA25hAFXhLKq4jqeODvvaogsd0/NX02Kmz3QoT9GV+rgpv2TxamU6RwjRgihSH06Dn5hM6SOw==",
"license": "MIT",
"peer": true,
"dependencies": {
"@tanstack/history": "1.130.12",
"@tanstack/react-store": "^0.7.0",
@@ -4177,7 +4172,6 @@
"resolved": "https://registry.npmjs.org/@tanstack/router-core/-/router-core-1.130.17.tgz",
"integrity": "sha512-0AH4s/52X2shYLifr9hR+6R6NpGsBp1M3LyjBTZm6Jlkud+nOhkkRxKxYS7l+JErRlffdxVHxMdoTnKcQ+duiQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"@tanstack/history": "1.130.12",
"@tanstack/store": "^0.7.0",
@@ -4411,7 +4405,8 @@
"version": "5.0.4",
"resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz",
"integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==",
"dev": true
"dev": true,
"peer": true
},
"node_modules/@types/babel__core": {
"version": "7.20.5",
@@ -4568,7 +4563,6 @@
"integrity": "sha512-DRh5K+ka5eJic8CjH7td8QpYEV6Zo10gfRkjHCO3weqZHWDtAaSTFtl4+VMqOJ4N5jcuhZ9/l+yy8rVgw7BQeQ==",
"devOptional": true,
"license": "MIT",
"peer": true,
"dependencies": {
"undici-types": "~7.10.0"
}
@@ -4585,7 +4579,6 @@
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.16.tgz",
"integrity": "sha512-WBM/nDbEZmDUORKnh5i1bTnAz6vTohUf9b8esSMu+b24+srbaxa04UbJgWx78CVfNXA20sNu0odEIluZDFdCog==",
"license": "MIT",
"peer": true,
"dependencies": {
"csstype": "^3.0.2"
}
@@ -4595,7 +4588,6 @@
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.1.7.tgz",
"integrity": "sha512-i5ZzwYpqjmrKenzkoLM2Ibzt6mAsM7pxB6BCIouEVVmgiqaMj1TjaK7hnA36hbW5aZv20kx7Lw6hWzPWg0Rurw==",
"devOptional": true,
"peer": true,
"peerDependencies": {
"@types/react": "^19.0.0"
}
@@ -4678,7 +4670,6 @@
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.38.0.tgz",
"integrity": "sha512-Zhy8HCvBUEfBECzIl1PKqF4p11+d0aUJS1GeUiuqK9WmOug8YCmC4h4bjyBvMyAMI9sbRczmrYL5lKg/YMbrcQ==",
"dev": true,
"peer": true,
"dependencies": {
"@typescript-eslint/scope-manager": "8.38.0",
"@typescript-eslint/types": "8.38.0",
@@ -5033,7 +5024,6 @@
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
"dev": true,
"peer": true,
"bin": {
"acorn": "bin/acorn"
},
@@ -5481,7 +5471,6 @@
"url": "https://github.com/sponsors/ai"
}
],
"peer": true,
"dependencies": {
"caniuse-lite": "^1.0.30001726",
"electron-to-chromium": "^1.5.173",
@@ -6174,8 +6163,7 @@
"node_modules/csstype": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
"peer": true
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="
},
"node_modules/data-urls": {
"version": "5.0.0",
@@ -6448,7 +6436,8 @@
"version": "0.5.16",
"resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz",
"integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==",
"dev": true
"dev": true,
"peer": true
},
"node_modules/dom-helpers": {
"version": "5.2.1",
@@ -7448,7 +7437,6 @@
"resolved": "https://registry.npmjs.org/eslint/-/eslint-9.32.0.tgz",
"integrity": "sha512-LSehfdpgMeWcTZkWZVIJl+tkZ2nuSkyyB9C27MZqFWXuph7DvaowgcTvKqxvpLW1JZIk8PN7hFY3Rj9LQ7m7lg==",
"dev": true,
"peer": true,
"dependencies": {
"@eslint-community/eslint-utils": "^4.2.0",
"@eslint-community/regexpp": "^4.12.1",
@@ -7509,7 +7497,6 @@
"resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-10.1.8.tgz",
"integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==",
"dev": true,
"peer": true,
"bin": {
"eslint-config-prettier": "bin/cli.js"
},
@@ -8816,7 +8803,6 @@
"url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project"
}
],
"peer": true,
"dependencies": {
"@babel/runtime": "^7.27.6"
},
@@ -8879,7 +8865,6 @@
"version": "10.1.1",
"resolved": "https://registry.npmjs.org/immer/-/immer-10.1.1.tgz",
"integrity": "sha512-s2MPrmjovJcoMaHtx6K11Ra7oD05NT97w1IC5zpMkT6Atjr7H8LjaDd81iIxUYpMKSRRNMJE703M1Fhr/TctHw==",
"peer": true,
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/immer"
@@ -9554,7 +9539,6 @@
"resolved": "https://registry.npmjs.org/jsdom/-/jsdom-26.1.0.tgz",
"integrity": "sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==",
"dev": true,
"peer": true,
"dependencies": {
"cssstyle": "^4.2.1",
"data-urls": "^5.0.0",
@@ -10120,6 +10104,7 @@
"resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz",
"integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==",
"dev": true,
"peer": true,
"bin": {
"lz-string": "bin/bin.js"
}
@@ -12064,7 +12049,6 @@
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.2.tgz",
"integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==",
"dev": true,
"peer": true,
"bin": {
"prettier": "bin/prettier.cjs"
},
@@ -12178,6 +12162,7 @@
"resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz",
"integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==",
"dev": true,
"peer": true,
"dependencies": {
"ansi-regex": "^5.0.1",
"ansi-styles": "^5.0.0",
@@ -12192,6 +12177,7 @@
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz",
"integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==",
"dev": true,
"peer": true,
"engines": {
"node": ">=10"
},
@@ -12320,7 +12306,6 @@
"version": "19.1.1",
"resolved": "https://registry.npmjs.org/react/-/react-19.1.1.tgz",
"integrity": "sha512-w8nqGImo45dmMIfljjMwOGtbmC/mk4CMYhWIicdSflH91J9TyCyczcPFXJzrZ/ZXcgGRFeP6BU0BEJTw6tZdfQ==",
"peer": true,
"engines": {
"node": ">=0.10.0"
}
@@ -12329,7 +12314,6 @@
"version": "19.1.1",
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.1.tgz",
"integrity": "sha512-Dlq/5LAZgF0Gaz6yiqZCf6VCcZs1ghAJyrsu84Q/GT0gV+mCxbfmKNoGRKBYMJ8IEdGPqu49YWXD02GCknEDkw==",
"peer": true,
"dependencies": {
"scheduler": "^0.26.0"
},
@@ -12341,7 +12325,6 @@
"version": "7.62.0",
"resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.62.0.tgz",
"integrity": "sha512-7KWFejc98xqG/F4bAxpL41NB3o1nnvQO1RWZT3TqRZYL8RryQETGfEdVnJN2fy1crCiBLLjkRBVK05j24FxJGA==",
"peer": true,
"engines": {
"node": ">=18.0.0"
},
@@ -12382,7 +12365,8 @@
"version": "17.0.2",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
"integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==",
"dev": true
"dev": true,
"peer": true
},
"node_modules/react-lifecycles-compat": {
"version": "3.0.4",
@@ -13133,7 +13117,6 @@
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/seroval/-/seroval-1.3.2.tgz",
"integrity": "sha512-RbcPH1n5cfwKrru7v7+zrZvjLurgHhGyso3HTyGtRivGWgYjbOmGuivCQaORNELjNONoK35nj28EoWul9sb1zQ==",
"peer": true,
"engines": {
"node": ">=10"
}
@@ -13923,8 +13906,7 @@
"node_modules/tailwindcss": {
"version": "4.1.11",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.11.tgz",
"integrity": "sha512-2E9TBm6MDD/xKYe+dvJZAmg3yxIEDNRc0jwlNyDg/4Fil2QcSLjFKGVff0lAf1jjeaArlG/M75Ey/EYr/OJtBA==",
"peer": true
"integrity": "sha512-2E9TBm6MDD/xKYe+dvJZAmg3yxIEDNRc0jwlNyDg/4Fil2QcSLjFKGVff0lAf1jjeaArlG/M75Ey/EYr/OJtBA=="
},
"node_modules/tailwindcss-animate": {
"version": "1.0.7",
@@ -14026,8 +14008,7 @@
"node_modules/tiny-invariant": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz",
"integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==",
"peer": true
"integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg=="
},
"node_modules/tiny-warning": {
"version": "1.0.3",
@@ -14078,7 +14059,6 @@
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"peer": true,
"engines": {
"node": ">=12"
},
@@ -14390,7 +14370,6 @@
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz",
"integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==",
"devOptional": true,
"peer": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
@@ -14752,7 +14731,6 @@
"resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz",
"integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==",
"license": "MIT",
"peer": true,
"dependencies": {
"esbuild": "^0.25.0",
"fdir": "^6.4.4",
@@ -14874,7 +14852,6 @@
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"peer": true,
"engines": {
"node": ">=12"
},
@@ -15418,7 +15395,6 @@
"version": "3.25.76",
"resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz",
"integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==",
"peer": true,
"funding": {
"url": "https://github.com/sponsors/colinhacks"
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

@@ -720,3 +720,7 @@ The following persons must not work on the machine:
![QiTech Pro Extruder Electrical Diagram 2025 - Sheet 8](images/manuals/extruder/QiTech_Pro_Extruder_Electrical_Diagram_2025_8-008.png)
![QiTech Pro Extruder Electrical Diagram 2025 - Sheet 9](images/manuals/extruder/QiTech_Pro_Extruder_Electrical_Diagram_2025_9-009.png)
## Mechanical Diagrams
![QiTech Pro Extruder Nozzle 2025](images/manuals/extruder/Nozzle.png)
+10 -1
View File
@@ -145,8 +145,17 @@ export function mainMessageHandler(
};
}
// Create the store instance once and export it
export const mainNamespaceStore: StoreApi<MainNamespaceStore> =
createMainNamespaceStore();
// Message handler stays the same
export const mainMessageHandlerInstance: EventHandler =
mainMessageHandler(mainNamespaceStore);
// Hook that uses the exported store
const mainRoomImplementation = createNamespaceHookImplementation({
createStore: createMainNamespaceStore,
createStore: () => mainNamespaceStore, // use the exported store
createEventHandler: mainMessageHandler,
});
+24 -21
View File
@@ -8,6 +8,8 @@ import { z } from "zod";
import { toastError, toastZodError } from "@/components/Toast";
import { MachineIdentificationUnique } from "@/machines/types";
import { FPS_30 } from "@/lib/constants";
import { mainNamespaceStore, createMainNamespaceStore } from "./mainNamespace";
import { softReloadMainNamespaceStore } from "./softReload";
/**
* Simple buffer-based store updater to limit React re-renders to ~30 FPS
@@ -305,27 +307,13 @@ const useSocketioStore = create<SocketioStore>()((set, get) => ({
// add handlers
socket.on("connect", () => {
console.log(`Connected to ${namespace_path}`);
// reset the store
resetStore(set);
});
socket.on("disconnect", (reason) => {
socket.disconnect();
console.warn(`Disconnected from ${namespace_path}, reason: ${reason}`);
// reset the store
resetStore(set);
// Attempt to reconnect after a short delay for any disconnect reason
console.log(
`Disconnected from ${namespace_path}, attempting reconnect in 1s...`,
);
setTimeout(() => {
if (get().hasNamespace(namespaceId) && !socket.connected) {
console.log(`Reconnecting to ${namespace_path}...`);
socket.connect();
}
}, 1000);
// Its hacky but i do not care, electron + react is annoying ...
window.location.reload();
});
socket.on("event", (event: unknown) => {
@@ -334,11 +322,9 @@ const useSocketioStore = create<SocketioStore>()((set, get) => ({
if (!event_parsed.success) {
toastZodError(event_parsed.error, "Invalid event");
return;
}
// handle the event
} // handle the event
get().namespaces[namespace_path].handler(event_parsed.data);
});
// store the namespace initally
set(
produce((state: SocketioStore) => {
@@ -356,8 +342,25 @@ const useSocketioStore = create<SocketioStore>()((set, get) => ({
}),
);
// finally connect
socket.connect();
if (namespace_path !== "/main") {
const intervalId = setInterval(() => {
const mainState = mainNamespaceStore.getState();
const machineExists =
namespaceId.type === "machine" &&
mainState.machines?.data?.machines.some(
(m) =>
m.machine_identification_unique.serial ===
namespaceId.machine_identification_unique.serial,
);
//console.log(machineExists);
if (machineExists && !socket.connected) {
socket.connect();
clearInterval(intervalId); // stop polling
}
}, 500);
} else {
socket.connect();
}
},
incrementNamespace: (namespaceId: NamespaceId) => {
const namespace_path = serializeNamespaceId(namespaceId);
+17
View File
@@ -0,0 +1,17 @@
import { mainNamespaceStore, mainMessageHandler } from "./mainNamespace";
// Soft reset function
export function softReloadMainNamespaceStore() {
// 1️⃣ Reset the store to initial state
mainNamespaceStore.setState({
ethercatDevices: null,
machines: null,
ethercatInterfaceDiscovery: null,
});
// 2️⃣ Re-attach the message handler if needed
// This ensures future socket events still update the store
mainMessageHandler(mainNamespaceStore);
console.log("MainNamespaceStore soft reloaded");
}
@@ -62,7 +62,6 @@ export const connectedMachineStateSchema = z.object({
/**
* Consolidated state event schema (state changes only)
*/
export const stateEventDataSchema = z.object({
mode_state: modeStateSchema,
connected_machine_state: connectedMachineStateSchema,
@@ -239,6 +239,50 @@ export function Extruder2SettingsPage() {
/>
</Label>
</ControlCard>
<ControlCard title="Temperature PID Settings (Nozzle) ">
<Label label="Kp">
<EditValue
value={state?.pid_settings.temperature.nozzle.kp}
defaultValue={
defaultState?.pid_settings.temperature.nozzle.kp
}
min={0}
max={100}
step={0.001}
renderValue={(v) => roundToDecimals(v, 3)}
onChange={(v) => setTemperaturePidValue("nozzle", "kp", v)}
title="Temperature PID KP"
/>
</Label>
<Label label="Ki">
<EditValue
value={state?.pid_settings.temperature.nozzle.ki}
defaultValue={
defaultState?.pid_settings.temperature.nozzle.ki
}
min={0}
max={100}
step={0.001}
renderValue={(v) => roundToDecimals(v, 3)}
onChange={(v) => setTemperaturePidValue("nozzle", "ki", v)}
title="Temperature PID KI"
/>
</Label>
<Label label="Kd">
<EditValue
value={state?.pid_settings.temperature.nozzle.kd}
defaultValue={
defaultState?.pid_settings.temperature.nozzle.kd
}
min={0}
max={100}
step={0.001}
renderValue={(v) => roundToDecimals(v, 3)}
onChange={(v) => setTemperaturePidValue("nozzle", "kd", v)}
title="Temperature PID KD"
/>
</Label>
</ControlCard>
</ControlGrid>
</>
)}
@@ -149,6 +149,12 @@ export const pidSettingsSchema = z.object({
kd: z.number(),
zone: z.string(),
}),
nozzle: z.object({
ki: z.number(),
kp: z.number(),
kd: z.number(),
zone: z.string(),
}),
}),
pressure: z.object({
ki: z.number(),
@@ -294,7 +294,7 @@ export function useExtruder2() {
};
const setTemperaturePidValue = (
zone: "front" | "middle" | "back",
zone: "front" | "middle" | "back" | "nozzle",
key: "kp" | "ki" | "kd",
value: number,
) => {
+1 -1
View File
@@ -10,9 +10,9 @@ workspace = true
anyhow = "1.0.100"
ethercrab = "0.6"
ethercat_hal_derive = { version = "0.1.0", path = "../ethercat-hal-derive" }
units = { path = "../units" }
bitvec = { version = "1.0.1", features = ["alloc"] }
smol = "2.0.2"
uom = { version = "0.36.0", default-features = false, features = ["f64","si"] }
rand = "0.9.2"
futures = "0.3.31"
tracing = "0.1.41"
+1 -1
View File
@@ -14,7 +14,7 @@ use crate::{
io::analog_input::{AnalogInputDevice, AnalogInputInput},
};
use ethercat_hal_derive::{EthercatDevice, RxPdo, TxPdo};
use uom::si::{electric_potential::volt, f64::ElectricPotential};
use units::{electric_potential::volt, f64::ElectricPotential};
#[derive(EthercatDevice)]
pub struct EL3001 {
+2 -2
View File
@@ -17,8 +17,8 @@ use crate::{
io::analog_input::{AnalogInputDevice, AnalogInputInput},
};
use ethercat_hal_derive::{EthercatDevice, RxPdo, TxPdo};
use uom::si::electric_current::milliampere;
use uom::si::f64::ElectricCurrent;
use units::electric_current::milliampere;
use units::f64::ElectricCurrent;
#[derive(Debug, Clone)]
pub struct EL3021Configuration {
+2 -2
View File
@@ -17,8 +17,8 @@ use crate::{
io::analog_input::{AnalogInputDevice, AnalogInputInput},
};
use ethercat_hal_derive::{EthercatDevice, RxPdo, TxPdo};
use uom::si::electric_current::milliampere;
use uom::si::f64::ElectricCurrent;
use units::electric_current::milliampere;
use units::f64::ElectricCurrent;
#[derive(Debug, Clone)]
pub struct EL3024Configuration {
+1 -1
View File
@@ -15,7 +15,7 @@ use crate::{
io::analog_input::{AnalogInputDevice, AnalogInputInput},
};
use ethercat_hal_derive::EthercatDevice;
use uom::si::{electric_potential::volt, f64::ElectricPotential};
use units::{electric_potential::volt, f64::ElectricPotential};
#[derive(Debug, Clone)]
pub struct EL3062_0030Configuration {
+1 -1
View File
@@ -5,7 +5,7 @@ use anyhow::anyhow;
use coe::EL7031_0030Configuration;
use ethercat_hal_derive::EthercatDevice;
use pdo::{EL7031_0030RxPdo, EL7031_0030TxPdo};
use uom::si::{electric_potential::volt, f64::ElectricPotential};
use units::{electric_potential::volt, f64::ElectricPotential};
use crate::{
helpers::{
+4 -4
View File
@@ -1,4 +1,4 @@
use uom::si::f64::{ElectricCurrent, ElectricPotential};
use units::f64::{ElectricCurrent, ElectricPotential};
#[derive(Debug, Clone)]
pub enum AnalogInputValue {
@@ -77,9 +77,9 @@ mod tests {
use super::*;
use approx::assert_relative_eq;
use uom::si::electric_current::milliampere;
use uom::si::electric_potential::volt;
use uom::si::f64::{ElectricCurrent, ElectricPotential};
use units::electric_current::milliampere;
use units::electric_potential::volt;
use units::f64::{ElectricCurrent, ElectricPotential};
#[test]
fn test_analog_input_getter_voltage() {
+1 -1
View File
@@ -57,7 +57,7 @@ impl AnalogInputDummy {
#[cfg(test)]
mod tests {
use uom::si::{electric_potential::volt, f64::ElectricPotential};
use units::{electric_potential::volt, f64::ElectricPotential};
use super::*;
+53
View File
@@ -0,0 +1,53 @@
[package]
name = "machines"
version = "0.1.0"
edition = "2024"
[dependencies]
ethercrab = "0.6"
ethercat_hal = { path = "../ethercat-hal" }
control_core = { path = "../control-core" }
control_core_derive = { path = "../control-core-derive" }
units = { path = "../units" }
serde = "1.0.217"
anyhow = "1.0.100"
bitvec = "1.0.1"
lazy_static = "1.5.0"
euclid = "0.22.11"
# concurrency
smol = "2.0.2"
futures = "0.3.31"
# web
serde_json = "1.0.143"
socketioxide = { version = "0.17.2", features = ["msgpack"] }
# serial
serialport = "4.7.3"
#tracing
tracing = { version = "0.1.41", features = ["attributes"] }
tonic = { version = "0.13.1", optional = true }
[dev-dependencies]
approx = "0.5.1"
textplots = "0.8.7"
[features]
default = ["tracing-fmt"]
never_type = []
tracing-fmt = []
mock-machine = []
memory-locking = []
io-uring = []
development-build = []
[package.metadata.cargo-machete]
ignored = ["tonic"]
+59
View File
@@ -0,0 +1,59 @@
use super::{AquaPathV1, AquaPathV1Mode};
use crate::{MachineAct, MachineMessage};
use std::time::{Duration, Instant};
impl MachineAct for AquaPathV1 {
fn act(&mut self, now_ts: Instant) {
let msg = self.api_receiver.try_recv();
match msg {
Ok(msg) => {
let _res = self.act_machine_message(msg);
}
Err(_) => (),
};
match self.mode {
AquaPathV1Mode::Standby => {
self.switch_to_standby();
}
AquaPathV1Mode::Auto => {
self.switch_to_auto();
}
}
let now = Instant::now();
self.front_controller.update(now_ts);
self.back_controller.update(now_ts);
if now.duration_since(self.last_measurement_emit) > Duration::from_secs_f64(1.0 / 30.0) {
self.emit_live_values();
self.last_measurement_emit = now;
}
}
fn act_machine_message(&mut self, msg: MachineMessage) {
match msg {
MachineMessage::SubscribeNamespace(namespace) => {
self.namespace.namespace = Some(namespace);
self.emit_state();
}
MachineMessage::UnsubscribeNamespace => self.namespace.namespace = None,
MachineMessage::HttpApiJsonRequest(value) => {
use crate::MachineApi;
let _res = self.api_mutate(value);
}
MachineMessage::ConnectToMachine(_machine_connection) =>
/*Doesnt connec to any Machine do nothing*/
{
()
}
MachineMessage::DisconnectMachine(_machine_connection) =>
/*Doesnt connec to any Machine do nothing*/
{
()
}
}
}
}
@@ -1,19 +1,15 @@
use std::{sync::Arc, time::Duration};
use super::{AquaPathV1, AquaPathV1Mode};
use control_core::{
machines::api::MachineApi,
socketio::{
event::{Event, GenericEvent},
namespace::{
CacheFn, CacheableEvents, Namespace, NamespaceCacheingLogic, cache_duration,
cache_first_and_last_event,
},
use crate::{MachineApi, MachineMessage};
use control_core::socketio::{
event::{Event, GenericEvent},
namespace::{
CacheFn, CacheableEvents, Namespace, NamespaceCacheingLogic, cache_first_and_last_event,
},
};
use serde::{Deserialize, Serialize};
use serde_json::Value;
use smol::lock::Mutex;
use smol::channel::Sender;
use std::sync::Arc;
use tracing::instrument;
#[derive(Serialize, Debug, Clone, Default)]
@@ -91,9 +87,9 @@ enum Mutation {
SetBackFlow(bool),
}
#[derive(Debug)]
#[derive(Debug, Clone)]
pub struct AquaPathV1Namespace {
pub namespace: Arc<Mutex<Namespace>>,
pub namespace: Option<Namespace>,
}
impl NamespaceCacheingLogic<AquaPathV1Events> for AquaPathV1Namespace {
@@ -101,9 +97,10 @@ impl NamespaceCacheingLogic<AquaPathV1Events> for AquaPathV1Namespace {
fn emit(&mut self, events: AquaPathV1Events) {
let event = Arc::new(events.event_value());
let buffer_fn = events.event_cache_fn();
let mut namespace = self.namespace.lock_blocking();
namespace.emit(event, &buffer_fn);
match &mut self.namespace {
Some(ns) => ns.emit(event, &buffer_fn),
None => (),
}
}
}
@@ -116,17 +113,20 @@ impl CacheableEvents<AquaPathV1Events> for AquaPathV1Events {
}
fn event_cache_fn(&self) -> CacheFn {
let cache_one_hour = cache_duration(Duration::from_secs(60 * 60), Duration::from_secs(1));
let cache_first_and_last = cache_first_and_last_event();
match self {
AquaPathV1Events::LiveValues(_) => cache_one_hour,
AquaPathV1Events::LiveValues(_) => cache_first_and_last,
AquaPathV1Events::State(_) => cache_first_and_last,
}
}
}
impl MachineApi for AquaPathV1 {
fn api_get_sender(&self) -> Sender<MachineMessage> {
self.api_sender.clone()
}
fn api_mutate(&mut self, request_body: Value) -> Result<(), anyhow::Error> {
let control: Mutation = serde_json::from_value(request_body)?;
match control {
@@ -149,7 +149,7 @@ impl MachineApi for AquaPathV1 {
Ok(())
}
fn api_event_namespace(&mut self) -> Arc<Mutex<Namespace>> {
fn api_event_namespace(&mut self) -> Option<Namespace> {
self.namespace.namespace.clone()
}
}
@@ -1,15 +1,14 @@
use crate::machines::aquapath1::VolumeRate;
use crate::machines::aquapath1::{Flow, Temperature};
use crate::aquapath1::VolumeRate;
use crate::aquapath1::{Flow, Temperature};
use control_core::controllers::pid::PidController;
use ethercat_hal::io::encoder_input::EncoderInput;
use ethercat_hal::io::{
analog_output::AnalogOutput, digital_output::DigitalOutput, temperature_input::TemperatureInput,
};
use std::time::{Duration, Instant};
use uom::si::f64::ThermodynamicTemperature;
use uom::si::thermodynamic_temperature::degree_celsius;
use uom::si::volume_rate::liter_per_minute;
use units::f64::ThermodynamicTemperature;
use units::thermodynamic_temperature::degree_celsius;
use units::volume_rate::liter_per_minute;
#[derive(Debug)]
pub struct Controller {
@@ -1,18 +1,11 @@
use control_core::{
machines::identification::{MachineIdentification, MachineIdentificationUnique},
socketio::namespace::NamespaceCacheingLogic,
};
use control_core_derive::Machine;
use control_core::socketio::namespace::NamespaceCacheingLogic;
use serde::{Deserialize, Serialize};
use std::time::Instant;
use uom::si::{
f64::{ThermodynamicTemperature, VolumeRate},
thermodynamic_temperature::degree_celsius,
volume_rate::liter_per_minute,
};
use units::f64::*;
use units::{thermodynamic_temperature::degree_celsius, volume_rate::liter_per_minute};
use crate::machines::{
use crate::{AsyncThreadMessage, Machine, MachineMessage};
use crate::{
MACHINE_AQUAPATH_V1, VENDOR_QITECH,
aquapath1::{
api::{
@@ -20,10 +13,13 @@ use crate::machines::{
ModeState, StateEvent, TempState, TempStates,
},
controller::Controller,
// flow_controller::FlowController,
// temperature_controller::TemperatureController,
},
machine_identification::MachineIdentification,
};
use smol::channel::{Receiver, Sender};
use super::machine_identification::MachineIdentificationUnique;
pub mod act;
pub mod api;
pub mod controller;
@@ -50,6 +46,16 @@ pub struct Temperature {
pub target_temperature: ThermodynamicTemperature,
}
impl Machine for AquaPathV1 {
fn get_machine_identification_unique(&self) -> MachineIdentificationUnique {
self.machine_identification_unique.clone()
}
fn get_main_sender(&self) -> Option<Sender<AsyncThreadMessage>> {
self.main_sender.clone()
}
}
impl Default for Temperature {
fn default() -> Self {
Self {
@@ -78,14 +84,17 @@ impl Default for Flow {
}
}
#[derive(Debug, Machine)]
#[derive(Debug)]
pub struct AquaPathV1 {
api_receiver: Receiver<MachineMessage>,
api_sender: Sender<MachineMessage>,
machine_identification_unique: MachineIdentificationUnique,
namespace: AquaPathV1Namespace,
mode: AquaPathV1Mode,
last_measurement_emit: Instant,
front_controller: Controller,
back_controller: Controller,
main_sender: Option<Sender<AsyncThreadMessage>>,
}
impl AquaPathV1 {
@@ -1,13 +1,13 @@
use super::{AquaPathV1, AquaPathV1Mode};
use crate::machines::{
aquapath1::{Flow, Temperature, api::AquaPathV1Namespace, controller::Controller},
get_ethercat_device,
use crate::{
MachineNewHardware, MachineNewParams, MachineNewTrait, get_ethercat_device,
get_subdevice_by_index, validate_no_role_dublicates,
validate_same_machine_identification_unique,
};
use super::{
AquaPathV1, AquaPathV1Mode, Flow, Temperature, api::AquaPathV1Namespace, controller::Controller,
};
use anyhow::Error;
use control_core::machines::new::{
MachineNewHardware, MachineNewParams, MachineNewTrait, get_subdevice_by_index,
validate_no_role_dublicates, validate_same_machine_identification_unique,
};
use ethercat_hal::{
coe::ConfigurableDevice,
devices::{
@@ -26,7 +26,7 @@ use ethercat_hal::{
},
};
use std::time::{Duration, Instant};
use uom::si::{f64::ThermodynamicTemperature, thermodynamic_temperature::degree_celsius};
use units::thermodynamic_temperature::{ThermodynamicTemperature, degree_celsius};
impl MachineNewTrait for AquaPathV1 {
fn new<'maindevice>(params: &MachineNewParams) -> Result<Self, Error> {
@@ -160,8 +160,11 @@ impl MachineNewTrait for AquaPathV1 {
do2,
enc2,
);
let (sender, receiver) = smol::channel::unbounded();
let mut water_cooling = Self {
main_sender: params.main_thread_channel.clone(),
api_receiver: receiver,
api_sender: sender,
machine_identification_unique: params.get_machine_identification_unique(),
namespace: AquaPathV1Namespace {
namespace: params.namespace.clone(),
+47
View File
@@ -0,0 +1,47 @@
use super::BufferV1;
use crate::{MachineAct, MachineMessage};
use std::time::{Duration, Instant};
impl MachineAct for BufferV1 {
fn act(&mut self, now: Instant) {
let msg = self.api_receiver.try_recv();
match msg {
Ok(msg) => {
let _res = self.act_machine_message(msg);
}
Err(_) => (),
};
// if last measurement is older than 1 second, emit a new measurement
if now.duration_since(self.last_measurement_emit) > Duration::from_secs_f64(1.0 / 30.0) {
// Emit live values at 30 FPS
self.emit_live_values();
self.last_measurement_emit = now;
}
}
fn act_machine_message(&mut self, msg: MachineMessage) {
match msg {
MachineMessage::SubscribeNamespace(namespace) => {
self.namespace.namespace = Some(namespace);
self.emit_state();
}
MachineMessage::UnsubscribeNamespace => self.namespace.namespace = None,
MachineMessage::HttpApiJsonRequest(value) => {
use crate::MachineApi;
let _res = self.api_mutate(value);
}
MachineMessage::ConnectToMachine(_machine_connection) =>
/*Doesnt connec to any Macine do nothing*/
{
()
}
MachineMessage::DisconnectMachine(_machine_connection) =>
/*Doesnt connec to any Machine do nothing*/
{
()
}
}
}
}
@@ -1,22 +1,13 @@
use std::{sync::Arc, time::Duration};
use super::{BufferV1, BufferV1Mode};
use control_core::{
machines::{
api::MachineApi, connection::MachineCrossConnectionState,
identification::MachineIdentificationUnique,
},
socketio::{
event::{Event, GenericEvent},
namespace::{
CacheFn, CacheableEvents, Namespace, NamespaceCacheingLogic, cache_duration,
cache_one_event,
},
},
use crate::{MachineApi, MachineMessage, machine_identification::MachineIdentificationUnique};
use control_core::socketio::{
event::{Event, GenericEvent},
namespace::{CacheFn, CacheableEvents, Namespace, NamespaceCacheingLogic, cache_one_event},
};
use serde::{Deserialize, Serialize};
use serde_json::Value;
use smol::lock::Mutex;
use smol::channel::Sender;
use std::sync::Arc;
use tracing::instrument;
#[derive(Serialize, Debug, Clone, Default)]
@@ -31,9 +22,7 @@ impl LiveValuesEvent {
#[derive(Serialize, Debug, Clone)]
pub struct StateEvent {
/// mode state
pub mode_state: ModeState,
/// connected machine state
pub connected_machine_state: MachineCrossConnectionState,
pub mode_state: ModeState, // connected machine state
}
impl StateEvent {
@@ -80,7 +69,7 @@ enum Mutation {
#[derive(Debug)]
pub struct Buffer1Namespace {
pub namespace: Arc<Mutex<Namespace>>,
pub namespace: Option<Namespace>,
}
impl NamespaceCacheingLogic<BufferV1Events> for Buffer1Namespace {
@@ -89,8 +78,10 @@ impl NamespaceCacheingLogic<BufferV1Events> for Buffer1Namespace {
let event = Arc::new(events.event_value());
let buffer_fn = events.event_cache_fn();
let mut namespace = self.namespace.lock_blocking();
namespace.emit(event, &buffer_fn);
match &mut self.namespace {
Some(ns) => ns.emit(event, &buffer_fn),
None => (),
}
}
}
@@ -103,32 +94,31 @@ impl CacheableEvents<Self> for BufferV1Events {
}
fn event_cache_fn(&self) -> CacheFn {
let cache_one_hour = cache_duration(Duration::from_secs(60 * 60), Duration::from_secs(1));
let cache_one = cache_one_event();
match self {
Self::LiveValues(_) => cache_one_hour,
Self::LiveValues(_) => cache_one,
Self::State(_) => cache_one,
}
}
}
impl MachineApi for BufferV1 {
fn api_get_sender(&self) -> Sender<MachineMessage> {
self.api_sender.clone()
}
fn api_mutate(&mut self, request_body: Value) -> Result<(), anyhow::Error> {
let mutation: Mutation = serde_json::from_value(request_body)?;
match mutation {
Mutation::SetBufferMode(mode) => self.set_mode_state(mode),
Mutation::SetConnectedMachine(machine_identification_unique) => {
self.set_connected_winder(machine_identification_unique);
}
Mutation::DisconnectMachine(machine_identification_unique) => {
self.disconnect_winder(machine_identification_unique);
}
Mutation::SetConnectedMachine(_machine_identification_unique) => {}
Mutation::DisconnectMachine(_machine_identification_unique) => {}
}
Ok(())
}
fn api_event_namespace(&mut self) -> Arc<Mutex<Namespace>> {
fn api_event_namespace(&mut self) -> Option<Namespace> {
self.namespace.namespace.clone()
}
}
@@ -3,46 +3,37 @@ pub mod api;
pub mod buffer_tower_controller;
pub mod new;
use super::machine_identification::{MachineIdentification, MachineIdentificationUnique};
use crate::{AsyncThreadMessage, Machine, MachineMessage};
#[cfg(not(feature = "mock-machine"))]
use crate::machines::{MACHINE_BUFFER_V1, VENDOR_QITECH, winder2::Winder2};
use crate::{MACHINE_BUFFER_V1, VENDOR_QITECH};
use api::{Buffer1Namespace, BufferV1Events, LiveValuesEvent, ModeState, StateEvent};
use buffer_tower_controller::BufferTowerController;
use control_core::machines::connection::{CrossConnectableMachine, MachineCrossConnection};
use control_core::{
machines::{
identification::{MachineIdentification, MachineIdentificationUnique},
manager::MachineManager,
},
socketio::namespace::NamespaceCacheingLogic,
};
use control_core_derive::Machine;
use control_core::socketio::namespace::NamespaceCacheingLogic;
use serde::{Deserialize, Serialize};
use smol::lock::RwLock;
use std::{sync::Weak, time::Instant};
use smol::channel::{Receiver, Sender};
use std::time::Instant;
#[derive(Debug, Machine)]
#[derive(Debug)]
pub struct BufferV1 {
// controllers
pub buffer_tower_controller: BufferTowerController,
api_receiver: Receiver<MachineMessage>,
api_sender: Sender<MachineMessage>,
// socketio
pub buffer_tower_controller: BufferTowerController,
namespace: Buffer1Namespace,
last_measurement_emit: Instant,
// machine connection
pub machine_manager: Weak<RwLock<MachineManager>>,
pub machine_identification_unique: MachineIdentificationUnique,
// connected machines
pub connected_winder: MachineCrossConnection<BufferV1, Winder2>,
// mode
mode: BufferV1Mode,
main_sender: Option<Sender<AsyncThreadMessage>>,
}
impl CrossConnectableMachine<BufferV1, Winder2> for BufferV1 {
fn get_cross_connection(&mut self) -> &mut MachineCrossConnection<BufferV1, Winder2> {
&mut self.connected_winder
impl Machine for BufferV1 {
fn get_machine_identification_unique(&self) -> MachineIdentificationUnique {
self.machine_identification_unique.clone()
}
fn get_main_sender(&self) -> Option<Sender<AsyncThreadMessage>> {
self.main_sender.clone()
}
}
@@ -63,13 +54,12 @@ impl BufferV1 {
let event = live_values.build();
self.namespace.emit(BufferV1Events::LiveValues(event));
}
pub fn emit_state(&mut self) {
let state = StateEvent {
mode_state: ModeState {
mode: self.mode.clone(),
},
connected_machine_state: self.connected_winder.to_state(),
// connected_machine_state: self.connected_winder.to_state(),
};
let event = state.build();
@@ -133,43 +123,6 @@ impl BufferV1 {
self.switch_mode(mode);
self.emit_state();
}
/// Connecting/Disconnecting machine
/// set connected winder
pub fn set_connected_winder(
&mut self,
machine_identification_unique: MachineIdentificationUnique,
) {
if !matches!(
machine_identification_unique.machine_identification,
Winder2::MACHINE_IDENTIFICATION
) {
return;
}
self.connected_winder
.set_connected_machine(&machine_identification_unique);
self.emit_state();
self.connected_winder.reverse_connect();
}
/// disconnect winder
pub fn disconnect_winder(
&mut self,
machine_identification_unique: MachineIdentificationUnique,
) {
if !matches!(
machine_identification_unique.machine_identification,
Winder2::MACHINE_IDENTIFICATION
) {
return;
}
self.connected_winder.disconnect();
self.emit_state();
}
}
#[derive(Deserialize, Serialize, Debug, Clone, PartialEq, Eq)]
@@ -1,11 +1,6 @@
use std::time::Instant;
use anyhow::Error;
use control_core::machines::connection::MachineCrossConnection;
use control_core::machines::new::{
MachineNewHardware, MachineNewParams, MachineNewTrait, validate_no_role_dublicates,
validate_same_machine_identification_unique,
};
use ethercat_hal::{
coe::ConfigurableDevice,
devices::el7031_0030::coe::EL7031_0030Configuration,
@@ -22,9 +17,11 @@ use ethercat_hal::{
shared_config::el70x1::{EL70x1OperationMode, StmMotorConfiguration},
};
use crate::machines::buffer1::BufferV1Mode;
use crate::machines::buffer1::buffer_tower_controller::BufferTowerController;
use crate::machines::get_ethercat_device;
use crate::{
MachineNewHardware, MachineNewHardwareEthercat, MachineNewParams, MachineNewTrait,
buffer1::BufferV1Mode, get_ethercat_device, validate_same_machine_identification_unique,
};
use crate::{buffer1::buffer_tower_controller::BufferTowerController, validate_no_role_dublicates};
use super::{BufferV1, api::Buffer1Namespace};
@@ -37,16 +34,15 @@ impl MachineNewTrait for BufferV1 {
validate_same_machine_identification_unique(&device_identification)?;
validate_no_role_dublicates(&device_identification)?;
let hardware: &&control_core::machines::new::MachineNewHardwareEthercat<'_, '_, '_> =
match &params.hardware {
MachineNewHardware::Ethercat(x) => x,
_ => {
return Err(anyhow::anyhow!(
"[{}::MachineNewTrait/Buffer::new] MachineNewHardware is not Ethercat",
module_path!()
));
}
};
let hardware: &&MachineNewHardwareEthercat<'_, '_, '_> = match &params.hardware {
MachineNewHardware::Ethercat(x) => x,
_ => {
return Err(anyhow::anyhow!(
"[{}::MachineNewTrait/Buffer::new] MachineNewHardware is not Ethercat",
module_path!()
));
}
};
smol::block_on(async {
// Role 0 - Buscoupler EK1100
@@ -128,21 +124,20 @@ impl MachineNewTrait for BufferV1 {
));
let machine_identification_unique = params.get_machine_identification_unique();
let (sender, receiver) = smol::channel::unbounded();
// create buffer instance
let mut buffer: BufferV1 = Self {
main_sender: params.main_thread_channel.clone(),
api_receiver: receiver,
api_sender: sender,
namespace: Buffer1Namespace {
namespace: params.namespace.clone(),
},
last_measurement_emit: Instant::now(),
mode: BufferV1Mode::Standby,
buffer_tower_controller,
machine_manager: params.machine_manager.clone(),
machine_identification_unique: machine_identification_unique.clone(),
connected_winder: MachineCrossConnection::new(
params.machine_manager.clone(),
&machine_identification_unique,
),
};
buffer.emit_state();
Ok(buffer)
@@ -1,13 +1,21 @@
#[cfg(not(feature = "mock-machine"))]
use crate::machines::extruder1::ExtruderV2;
use crate::extruder1::ExtruderV2;
#[cfg(not(feature = "mock-machine"))]
use control_core::machines::new::MachineAct;
use crate::{MachineAct, MachineMessage};
#[cfg(not(feature = "mock-machine"))]
use std::time::{Duration, Instant};
#[cfg(not(feature = "mock-machine"))]
impl MachineAct for ExtruderV2 {
fn act(&mut self, now: Instant) {
let msg = self.api_receiver.try_recv();
match msg {
Ok(msg) => {
let _res = self.act_machine_message(msg);
}
Err(_) => (),
};
self.temperature_controller_back.update(now);
self.temperature_controller_nozzle.update(now);
self.temperature_controller_front.update(now);
@@ -39,4 +47,26 @@ impl MachineAct for ExtruderV2 {
self.last_measurement_emit = now;
}
}
fn act_machine_message(&mut self, msg: MachineMessage) {
match msg {
MachineMessage::SubscribeNamespace(namespace) => {
self.namespace.namespace = Some(namespace);
self.emit_state();
tracing::info!("extruder1 received subscribe");
}
MachineMessage::UnsubscribeNamespace => self.namespace.namespace = None,
MachineMessage::HttpApiJsonRequest(value) => {
use crate::MachineApi;
let _res = self.api_mutate(value);
}
MachineMessage::ConnectToMachine(_machine_connection) => (),
MachineMessage::DisconnectMachine(_machine_connection) =>
/*Doesnt connec to any Machine do nothing*/
{
()
}
}
}
}
@@ -4,28 +4,28 @@ use super::{ExtruderV2Mode, mitsubishi_cs80::MotorStatus};
use super::ExtruderV2;
#[cfg(not(feature = "mock-machine"))]
use crate::machines::extruder1::HeatingType;
use crate::{MachineMessage, extruder1::HeatingType};
#[cfg(not(feature = "mock-machine"))]
use control_core::machines::api::MachineApi;
use crate::MachineApi;
use control_core::socketio::{
event::{Event, GenericEvent},
namespace::{
CacheFn, CacheableEvents, Namespace, NamespaceCacheingLogic, cache_duration,
cache_first_and_last_event,
CacheFn, CacheableEvents, Namespace, NamespaceCacheingLogic, cache_first_and_last_event,
},
};
use control_core_derive::BuildEvent;
use serde::{Deserialize, Serialize};
#[cfg(not(feature = "mock-machine"))]
use serde_json::Value;
use smol::lock::Mutex;
use std::{sync::Arc, time::Duration};
#[cfg(not(feature = "mock-machine"))]
use smol::channel::Sender;
use std::sync::Arc;
use tracing::instrument;
use uom::si::{
angular_velocity::revolution_per_minute, electric_current::ampere, electric_potential::volt,
frequency::hertz,
};
use units::angular_velocity::revolution_per_minute;
use units::electric_current::ampere;
use units::electric_potential::volt;
use units::frequency::hertz;
#[derive(Debug, Clone, Default, Serialize)]
pub struct MotorStatusValues {
@@ -188,6 +188,7 @@ pub struct TemperaturePidStates {
pub front: TemperaturePid,
pub middle: TemperaturePid,
pub back: TemperaturePid,
pub nozzle: TemperaturePid,
}
#[derive(Deserialize, Serialize, Debug, Clone, PartialEq)]
@@ -240,7 +241,7 @@ pub enum Mutation {
#[derive(Debug)]
pub struct ExtruderV2Namespace {
pub namespace: Arc<Mutex<Namespace>>,
pub namespace: Option<Namespace>,
}
impl NamespaceCacheingLogic<ExtruderV2Events> for ExtruderV2Namespace {
@@ -249,8 +250,10 @@ impl NamespaceCacheingLogic<ExtruderV2Events> for ExtruderV2Namespace {
let event = Arc::new(events.event_value());
let buffer_fn = events.event_cache_fn();
let mut namespace = self.namespace.lock_blocking();
namespace.emit(event, &buffer_fn);
match &mut self.namespace {
Some(ns) => ns.emit(event, &buffer_fn),
None => (),
}
}
}
@@ -263,11 +266,9 @@ impl CacheableEvents<Self> for ExtruderV2Events {
}
fn event_cache_fn(&self) -> CacheFn {
let cache_one_hour = cache_duration(Duration::from_secs(60 * 60), Duration::from_secs(1));
let cache_first_and_last = cache_first_and_last_event();
match self {
Self::LiveValues(_) => cache_one_hour,
Self::LiveValues(_) => cache_first_and_last,
Self::State(_) => cache_first_and_last,
}
}
@@ -275,6 +276,10 @@ impl CacheableEvents<Self> for ExtruderV2Events {
#[cfg(not(feature = "mock-machine"))]
impl MachineApi for ExtruderV2 {
fn api_get_sender(&self) -> Sender<MachineMessage> {
self.api_sender.clone()
}
fn api_mutate(&mut self, request_body: Value) -> Result<(), anyhow::Error> {
// there are multiple Modbus Frames that are "prebuilt"
let control: Mutation = serde_json::from_value(request_body)?;
@@ -316,7 +321,7 @@ impl MachineApi for ExtruderV2 {
Ok(())
}
fn api_event_namespace(&mut self) -> Arc<Mutex<Namespace>> {
fn api_event_namespace(&mut self) -> Option<Namespace> {
self.namespace.namespace.clone()
}
}
@@ -1,6 +1,6 @@
#[cfg(not(feature = "mock-machine"))]
// Contains Implementations for All functions that use emit_state
use crate::machines::extruder1::{
use crate::extruder1::{
ExtruderV2, ExtruderV2Mode, HeatingType,
api::{
ExtruderSettingsState, ExtruderV2Events, HeatingState, HeatingStates, InverterStatusState,
@@ -15,19 +15,18 @@ use control_core::socketio::event::BuildEvent;
#[cfg(not(feature = "mock-machine"))]
use control_core::socketio::namespace::NamespaceCacheingLogic;
#[cfg(not(feature = "mock-machine"))]
use uom::si::{angular_velocity::revolution_per_minute, thermodynamic_temperature::degree_celsius};
use units::angular_velocity::AngularVelocity;
#[cfg(not(feature = "mock-machine"))]
use uom::si::angular_velocity::AngularVelocity;
use units::pressure::{Pressure, bar};
#[cfg(not(feature = "mock-machine"))]
use uom::si::pressure::{Pressure, bar};
use units::thermodynamic_temperature::ThermodynamicTemperature;
#[cfg(not(feature = "mock-machine"))]
use uom::si::thermodynamic_temperature::ThermodynamicTemperature;
use units::{angular_velocity::revolution_per_minute, thermodynamic_temperature::degree_celsius};
#[cfg(not(feature = "mock-machine"))]
impl ExtruderV2 {
pub fn build_state_event(&mut self) -> StateEvent {
use crate::machines::extruder1::api::{TemperaturePid, TemperaturePidStates};
use crate::extruder1::api::{TemperaturePid, TemperaturePidStates};
StateEvent {
is_default_state: !std::mem::replace(&mut self.emitted_default_state, true),
@@ -127,6 +126,12 @@ impl ExtruderV2 {
kd: self.temperature_controller_back.pid.get_kd(),
zone: String::from("back"),
},
nozzle: TemperaturePid {
ki: self.temperature_controller_nozzle.pid.get_ki(),
kp: self.temperature_controller_nozzle.pid.get_kp(),
kd: self.temperature_controller_nozzle.pid.get_kd(),
zone: String::from("nozzle"),
},
},
pressure: PidSettings {
ki: self.screw_speed_controller.pid.get_ki(),
@@ -320,6 +325,13 @@ impl ExtruderV2 {
settings.kd,
);
}
"nozzle" => {
self.temperature_controller_nozzle.pid.configure(
settings.ki,
settings.kp,
settings.kd,
);
}
_ => tracing::warn!("Unknown zone: {}", settings.zone),
}
self.emit_state();
@@ -6,12 +6,10 @@ use control_core::modbus::{
use ethercat_hal::io::serial_interface::SerialInterface;
use serde::Serialize;
use std::time::{Duration, Instant};
use uom::si::{
electric_current::centiampere,
electric_potential::centivolt,
f64::{AngularVelocity, ElectricCurrent, ElectricPotential, Frequency},
frequency::centihertz,
};
use units::electric_current::centiampere;
use units::electric_potential::centivolt;
use units::f64::*;
use units::frequency::centihertz;
/// Specifies all System environment Variables
/// Register addresses are calculated as follows: Register-value 40002 -> address: 40002-40001 -> actual address in request:0x1
+45
View File
@@ -0,0 +1,45 @@
use super::ExtruderV2;
use crate::MachineAct;
use crate::MachineMessage;
use std::time::{Duration, Instant};
impl MachineAct for ExtruderV2 {
fn act(&mut self, now: Instant) {
let msg = self.api_receiver.try_recv();
match msg {
Ok(msg) => {
let _res = self.act_machine_message(msg);
}
Err(_) => (),
};
// more than 33ms have passed since last emit (30 "fps" target)
if now.duration_since(self.last_measurement_emit) > Duration::from_secs_f64(1.0 / 30.0) {
self.maybe_emit_state_event();
// Emit live values at 30 FPS
self.emit_live_values();
self.last_measurement_emit = now;
}
}
fn act_machine_message(&mut self, msg: crate::MachineMessage) {
match msg {
MachineMessage::SubscribeNamespace(namespace) => {
self.namespace.namespace = Some(namespace);
self.emit_state();
tracing::info!("extruder1 received subscribe");
}
MachineMessage::UnsubscribeNamespace => self.namespace.namespace = None,
MachineMessage::HttpApiJsonRequest(value) => {
use crate::MachineApi;
let _res = self.api_mutate(value);
}
MachineMessage::ConnectToMachine(_machine_connection) => (),
MachineMessage::DisconnectMachine(_machine_connection) =>
/*Doesnt connec to any Machine do nothing*/
{
()
}
}
}
}
@@ -1,8 +1,8 @@
use crate::machines::extruder1::{HeatingType, api::Mutation, mock::ExtruderV2};
use control_core::machines::api::MachineApi;
use control_core::socketio::namespace::Namespace;
use smol::lock::Mutex;
use std::sync::Arc;
use super::ExtruderV2;
use crate::{
MachineApi,
extruder1::{HeatingType, api::Mutation},
};
impl MachineApi for ExtruderV2 {
fn api_mutate(&mut self, request_body: serde_json::Value) -> Result<(), anyhow::Error> {
@@ -45,7 +45,13 @@ impl MachineApi for ExtruderV2 {
Ok(())
}
fn api_event_namespace(&mut self) -> Arc<Mutex<Namespace>> {
fn api_event_namespace(
&mut self,
) -> std::option::Option<control_core::socketio::namespace::Namespace> {
self.namespace.namespace.clone()
}
fn api_get_sender(&self) -> smol::channel::Sender<crate::MachineMessage> {
self.api_sender.clone()
}
}
@@ -1,8 +1,9 @@
use crate::machines::extruder1::{
use crate::extruder1::{
ExtruderV2Mode, HeatingType,
api::{ExtruderV2Events, LiveValuesEvent, ModeState, PidSettings, StateEvent, TemperaturePid},
mock::ExtruderV2,
};
use control_core::{
helpers::hasher_serializer::hash_with_serde_model,
socketio::{event::BuildEvent, namespace::NamespaceCacheingLogic},
@@ -151,6 +152,11 @@ impl ExtruderV2 {
self.pid_settings.temperature.back.kp = settings.kp;
self.pid_settings.temperature.back.kd = settings.kd;
}
"nozzle" => {
self.pid_settings.temperature.nozzle.ki = settings.ki;
self.pid_settings.temperature.nozzle.kp = settings.kp;
self.pid_settings.temperature.nozzle.kd = settings.kd;
}
_ => tracing::warn!("Unknown zone: {}", settings.zone),
}
self.emit_state();
@@ -1,14 +1,21 @@
#[cfg(feature = "mock-machine")]
use control_core::machines::identification::MachineIdentification;
use smol::channel::Receiver;
#[cfg(feature = "mock-machine")]
use control_core::machines::identification::MachineIdentificationUnique;
use smol::channel::Sender;
#[cfg(feature = "mock-machine")]
use control_core_derive::Machine;
use crate::{AsyncThreadMessage, Machine};
#[cfg(feature = "mock-machine")]
use crate::{
MachineIdentificationUnique, MachineMessage, machine_identification::MachineIdentification,
};
#[cfg(feature = "mock-machine")]
use std::time::Instant;
#[cfg(feature = "mock-machine")]
use crate::machines::{
use crate::{
MACHINE_EXTRUDER_V1, VENDOR_QITECH,
extruder1::{
ExtruderV2Mode,
@@ -31,9 +38,25 @@ pub mod mock_emit;
pub mod new;
#[cfg(feature = "mock-machine")]
#[derive(Debug, Machine)]
impl Machine for ExtruderV2 {
fn get_machine_identification_unique(&self) -> MachineIdentificationUnique {
self.machine_identification_unique.clone()
}
fn get_main_sender(&self) -> Option<Sender<AsyncThreadMessage>> {
self.main_sender.clone()
}
}
#[cfg(feature = "mock-machine")]
#[derive(Debug)]
pub struct ExtruderV2 {
api_receiver: Receiver<MachineMessage>,
api_sender: Sender<MachineMessage>,
main_sender: Option<Sender<AsyncThreadMessage>>,
machine_identification_unique: MachineIdentificationUnique,
namespace: ExtruderV2Namespace,
last_measurement_emit: Instant,
last_status_hash: Option<u64>,
@@ -1,18 +1,19 @@
use crate::machines::extruder1::{
ExtruderV2Mode,
api::{
ExtruderSettingsState, ExtruderV2Namespace, HeatingState, HeatingStates,
InverterStatusState, ModeState, MotorStatusValues, PidSettings, PidSettingsStates,
PressureState, RegulationState, RotationState, ScrewState, TemperaturePid,
TemperaturePidStates,
use crate::{
MachineNewHardware, MachineNewParams, MachineNewTrait,
extruder1::{
ExtruderV2Mode,
api::{
ExtruderSettingsState, ExtruderV2Namespace, HeatingState, HeatingStates,
InverterStatusState, ModeState, MotorStatusValues, PidSettings, PidSettingsStates,
PressureState, RegulationState, RotationState, ScrewState, TemperaturePid,
TemperaturePidStates,
},
mock::ExtruderV2,
},
mock::ExtruderV2,
};
impl control_core::machines::new::MachineNewTrait for ExtruderV2 {
fn new(
params: &control_core::machines::new::MachineNewParams<'_, '_, '_, '_, '_, '_, '_>,
) -> Result<Self, anyhow::Error>
impl MachineNewTrait for ExtruderV2 {
fn new(params: &MachineNewParams<'_, '_, '_, '_, '_, '_, '_>) -> Result<Self, anyhow::Error>
where
Self: Sized,
{
@@ -20,19 +21,23 @@ impl control_core::machines::new::MachineNewTrait for ExtruderV2 {
// For the mock machine, we don't need to actually use the hardware
// We just validate that we have the expected hardware type
match params.hardware {
control_core::machines::new::MachineNewHardware::Serial(_) => {
MachineNewHardware::Serial(_) => {
// For serial mode, we could potentially use the serial device if needed
// but for a mock machine, we'll just note it and proceed
}
control_core::machines::new::MachineNewHardware::Ethercat(_) => {
MachineNewHardware::Ethercat(_) => {
// For ethercat mode, we could potentially use the ethercat devices
// but for a mock machine, we'll just note it and proceed
}
}
let now = std::time::Instant::now();
let (sender, receiver) = smol::channel::unbounded();
let mut extruder_mock_machine = Self {
main_sender: params.main_thread_channel.clone(),
api_receiver: receiver,
api_sender: sender,
machine_identification_unique: params.get_machine_identification_unique(),
namespace: ExtruderV2Namespace {
namespace: params.namespace.clone(),
@@ -91,23 +96,29 @@ impl control_core::machines::new::MachineNewTrait for ExtruderV2 {
pid_settings: PidSettingsStates {
temperature: TemperaturePidStates {
front: TemperaturePid {
ki: 0.0,
kp: 0.0,
kd: 0.0,
ki: 0.1,
kp: 0.1,
kd: 0.1,
zone: String::from("front"),
},
middle: TemperaturePid {
ki: 0.0,
kp: 0.0,
kd: 0.0,
ki: 0.1,
kp: 0.1,
kd: 0.1,
zone: String::from("middle"),
},
back: TemperaturePid {
ki: 0.0,
kp: 0.0,
kd: 0.0,
ki: 0.1,
kp: 0.1,
kd: 0.1,
zone: String::from("back"),
},
nozzle: TemperaturePid {
ki: 0.1,
kp: 0.1,
kd: 0.1,
zone: String::from("nozzle"),
},
},
pressure: PidSettings {
ki: 0.0,
@@ -2,23 +2,30 @@
use std::time::Instant;
#[cfg(not(feature = "mock-machine"))]
use control_core::machines::identification::{MachineIdentification, MachineIdentificationUnique};
use smol::channel::Receiver;
#[cfg(not(feature = "mock-machine"))]
use control_core_derive::Machine;
use smol::channel::Sender;
use serde::{Deserialize, Serialize};
#[cfg(not(feature = "mock-machine"))]
use uom::si::electric_current::ampere;
#[cfg(not(feature = "mock-machine"))]
use uom::si::electric_potential::volt;
use uom::si::{f64::ThermodynamicTemperature, thermodynamic_temperature::degree_celsius};
#[cfg(not(feature = "mock-machine"))]
use crate::machines::{
MACHINE_EXTRUDER_V1, VENDOR_QITECH,
use units::electric_current::ampere;
#[cfg(not(feature = "mock-machine"))]
use units::electric_potential::volt;
use crate::{AsyncThreadMessage, Machine};
use units::f64::*;
use units::thermodynamic_temperature::degree_celsius;
#[cfg(not(feature = "mock-machine"))]
use crate::{
MACHINE_EXTRUDER_V1, MachineMessage, VENDOR_QITECH,
extruder1::{
api::ExtruderV2Namespace, screw_speed_controller::ScrewSpeedController,
temperature_controller::TemperatureController,
},
machine_identification::{MachineIdentification, MachineIdentificationUnique},
};
pub mod act;
@@ -64,13 +71,19 @@ pub enum HeatingType {
}
#[cfg(not(feature = "mock-machine"))]
#[derive(Debug, Machine)]
#[derive(Debug)]
pub struct ExtruderV2 {
api_receiver: Receiver<MachineMessage>,
api_sender: Sender<MachineMessage>,
main_sender: Option<Sender<AsyncThreadMessage>>,
machine_identification_unique: MachineIdentificationUnique,
namespace: ExtruderV2Namespace,
last_measurement_emit: Instant,
last_status_hash: Option<u64>,
mode: ExtruderV2Mode,
screw_speed_controller: ScrewSpeedController,
temperature_controller_front: TemperatureController,
temperature_controller_middle: TemperatureController,
@@ -86,6 +99,17 @@ pub struct ExtruderV2 {
emitted_default_state: bool,
}
#[cfg(not(feature = "mock-machine"))]
impl Machine for ExtruderV2 {
fn get_machine_identification_unique(&self) -> MachineIdentificationUnique {
self.machine_identification_unique.clone()
}
fn get_main_sender(&self) -> Option<Sender<AsyncThreadMessage>> {
self.main_sender.clone()
}
}
#[cfg(not(feature = "mock-machine"))]
impl std::fmt::Display for ExtruderV2 {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
@@ -1,15 +1,9 @@
#[cfg(not(feature = "mock-machine"))]
use crate::machines::get_ethercat_device;
use crate::{MachineNewParams, MachineNewTrait, get_ethercat_device};
#[cfg(not(feature = "mock-machine"))]
use anyhow::Error;
#[cfg(not(feature = "mock-machine"))]
use control_core::machines::new::MachineNewHardware;
#[cfg(not(feature = "mock-machine"))]
use control_core::machines::new::{MachineNewParams, MachineNewTrait};
#[cfg(not(feature = "mock-machine"))]
use control_core::machines::new::{
validate_no_role_dublicates, validate_same_machine_identification_unique,
};
#[cfg(not(feature = "mock-machine"))]
use ethercat_hal::coe::ConfigurableDevice;
#[cfg(not(feature = "mock-machine"))]
@@ -29,13 +23,13 @@ use std::time::Duration;
#[cfg(not(feature = "mock-machine"))]
use std::time::Instant;
#[cfg(not(feature = "mock-machine"))]
use uom::si::angular_velocity::AngularVelocity;
use units::angular_velocity::AngularVelocity;
#[cfg(not(feature = "mock-machine"))]
use uom::si::angular_velocity::revolution_per_minute;
use units::angular_velocity::revolution_per_minute;
#[cfg(not(feature = "mock-machine"))]
use uom::si::pressure::Pressure;
use units::pressure::Pressure;
#[cfg(not(feature = "mock-machine"))]
use uom::si::pressure::bar;
use units::pressure::bar;
#[cfg(not(feature = "mock-machine"))]
use ethercat_hal::{
@@ -50,10 +44,10 @@ use ethercat_hal::{
},
};
#[cfg(not(feature = "mock-machine"))]
use uom::si::thermodynamic_temperature::{ThermodynamicTemperature, degree_celsius};
use units::thermodynamic_temperature::{ThermodynamicTemperature, degree_celsius};
#[cfg(not(feature = "mock-machine"))]
use crate::machines::extruder1::temperature_controller::TemperatureController;
use crate::extruder1::temperature_controller::TemperatureController;
#[cfg(not(feature = "mock-machine"))]
use super::{
@@ -66,21 +60,25 @@ impl MachineNewTrait for ExtruderV2 {
fn new<'maindevice>(params: &MachineNewParams) -> Result<Self, Error> {
// validate general stuff
use crate::{
MachineNewHardware, MachineNewHardwareEthercat, validate_no_role_dublicates,
validate_same_machine_identification_unique,
};
let device_identification = params.device_group.to_vec();
validate_same_machine_identification_unique(&device_identification)?;
validate_no_role_dublicates(&device_identification)?;
let hardware: &&control_core::machines::new::MachineNewHardwareEthercat<'_, '_, '_> =
match &params.hardware {
MachineNewHardware::Ethercat(x) => x,
_ => {
return Err(anyhow::anyhow!(
"[{}::MachineNewTrait/Extruder2::new] MachineNewHardware is not Ethercat",
module_path!()
));
}
};
let hardware: &&MachineNewHardwareEthercat<'_, '_, '_> = match &params.hardware {
MachineNewHardware::Ethercat(x) => x,
_ => {
return Err(anyhow::anyhow!(
"[{}::MachineNewTrait/Extruder2::new] MachineNewHardware is not Ethercat",
module_path!()
));
}
};
// using block_on because making this funciton async creates a lifetime issue
// if its async the compiler thinks &subdevices is persisted in the future which might never execute
@@ -218,8 +216,12 @@ impl MachineNewTrait for ExtruderV2 {
let screw_speed_controller =
ScrewSpeedController::new(inverter, target_pressure, target_rpm, pressure_sensor);
let (sender, receiver) = smol::channel::unbounded();
let mut extruder: ExtruderV2 = Self {
main_sender: params.main_thread_channel.clone(),
api_receiver: receiver,
api_sender: sender,
machine_identification_unique: params.get_machine_identification_unique(),
namespace: ExtruderV2Namespace {
namespace: params.namespace.clone(),
@@ -7,15 +7,13 @@ use control_core::{
};
use ethercat_hal::io::analog_input::AnalogInput;
use futures::executor::block_on;
use uom::si::{
angular_velocity::revolution_per_minute,
electric_current::milliampere,
f64::{AngularVelocity, ElectricCurrent, Frequency, Pressure},
frequency::{cycle_per_minute, hertz},
pressure::bar,
};
use units::angular_velocity::revolution_per_minute;
use units::electric_current::milliampere;
use units::f64::*;
use units::frequency::{cycle_per_minute, hertz};
use units::pressure::bar;
use crate::machines::extruder1::mitsubishi_cs80::MitsubishiCS80Status;
use crate::extruder1::mitsubishi_cs80::MitsubishiCS80Status;
use super::mitsubishi_cs80::{MitsubishiCS80, MotorStatus};
@@ -2,7 +2,8 @@ use super::Heating;
use control_core::controllers::pid::PidController;
use ethercat_hal::io::{digital_output::DigitalOutput, temperature_input::TemperatureInput};
use std::time::{Duration, Instant};
use uom::si::{f64::ThermodynamicTemperature, thermodynamic_temperature::degree_celsius};
use units::f64::*;
use units::thermodynamic_temperature::degree_celsius;
#[derive(Debug)]
+71
View File
@@ -0,0 +1,71 @@
use super::LaserMachine;
use crate::MachineAct;
use crate::MachineMessage;
use std::time::{Duration, Instant};
/// Implements the `MachineAct` trait for the `LaserMachine`.
///
/// # Parameters
/// - `_now_ts`: The current timestamp of type `Instant`.
///
/// # Returns
/// A pinned `Future` that resolves to `()` and is `Send`-safe. The future encapsulates the asynchronous behavior of the `act` method.
///
/// # Description
/// This method is called to perform periodic actions for the `LaserMachine`. Specifically:
/// - It checks if the time elapsed since the last measurement emission exceeds 20 milliseconds.
/// - If the condition is met, it asynchronously emits live values at 60 FPS.
///
/// The method ensures that the diameter value is updated approximately 60 times per second.
///
impl MachineAct for LaserMachine {
fn act(&mut self, now: Instant) {
let msg = self.api_receiver.try_recv();
match msg {
Ok(msg) => {
let _res = self.act_machine_message(msg);
}
Err(_) => (),
};
self.update();
// more than 33ms have passed since last emit (30 "fps" target)
if now.duration_since(self.last_measurement_emit) > Duration::from_secs_f64(1.0 / 30.0) {
self.emit_live_values();
self.last_measurement_emit = now;
}
}
fn act_machine_message(&mut self, msg: MachineMessage) {
tracing::info!("{:?}", msg);
match msg {
MachineMessage::SubscribeNamespace(namespace) => {
self.namespace.namespace = Some(namespace);
self.emit_state();
}
MachineMessage::UnsubscribeNamespace => match &mut self.namespace.namespace {
Some(namespace) => {
tracing::info!("UnsubscribeNamespace");
namespace.socket_queue_tx.close();
namespace.sockets.clear();
namespace.events.clear();
}
None => todo!(),
},
MachineMessage::HttpApiJsonRequest(value) => {
use crate::MachineApi;
let _res = self.api_mutate(value);
}
MachineMessage::ConnectToMachine(_machine_connection) =>
/*Doesnt connec to any Macine do nothing*/
{
()
}
MachineMessage::DisconnectMachine(_machine_connection) =>
/*Doesnt connec to any Machine do nothing*/
{
()
}
}
}
}
@@ -1,19 +1,16 @@
use crate::{MachineApi, MachineMessage};
use super::LaserMachine;
use control_core::{
machines::api::MachineApi,
socketio::{
event::{Event, GenericEvent},
namespace::{
CacheFn, CacheableEvents, Namespace, NamespaceCacheingLogic, cache_duration,
cache_first_and_last_event,
},
use control_core::socketio::{
event::{Event, GenericEvent},
namespace::{
CacheFn, CacheableEvents, Namespace, NamespaceCacheingLogic, cache_first_and_last_event,
},
};
use control_core_derive::BuildEvent;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use smol::lock::Mutex;
use std::{sync::Arc, time::Duration};
use std::sync::Arc;
use tracing::instrument;
#[derive(Serialize, Debug, Clone, Default)]
@@ -61,7 +58,7 @@ pub enum LaserEvents {
#[derive(Debug)]
pub struct LaserMachineNamespace {
pub namespace: Arc<Mutex<Namespace>>,
pub namespace: Option<Namespace>,
}
impl CacheableEvents<Self> for LaserEvents {
@@ -73,11 +70,10 @@ impl CacheableEvents<Self> for LaserEvents {
}
fn event_cache_fn(&self) -> CacheFn {
let cache_one_hour = cache_duration(Duration::from_secs(60 * 60), Duration::from_secs(1));
let cache_first_and_last = cache_first_and_last_event();
match self {
Self::LiveValues(_) => cache_one_hour,
Self::LiveValues(_) => cache_first_and_last,
Self::State(_) => cache_first_and_last,
}
}
@@ -99,8 +95,10 @@ impl NamespaceCacheingLogic<LaserEvents> for LaserMachineNamespace {
let event = Arc::new(events.event_value());
let buffer_fn = events.event_cache_fn();
let mut namespace = self.namespace.lock_blocking();
namespace.emit(event, &buffer_fn);
match &mut self.namespace {
Some(ns) => ns.emit(event, &buffer_fn),
None => (),
}
}
}
@@ -121,7 +119,11 @@ impl MachineApi for LaserMachine {
Ok(())
}
fn api_event_namespace(&mut self) -> Arc<Mutex<Namespace>> {
fn api_event_namespace(&mut self) -> Option<Namespace> {
self.namespace.namespace.clone()
}
fn api_get_sender(&self) -> smol::channel::Sender<MachineMessage> {
self.api_sender.clone()
}
}
@@ -1,24 +1,32 @@
use crate::serial::devices::laser::Laser;
use crate::{
machines::{MACHINE_LASER_V1, VENDOR_QITECH},
serial::devices::laser::Laser,
MACHINE_LASER_V1, VENDOR_QITECH,
machine_identification::{MachineIdentification, MachineIdentificationUnique},
};
use crate::{Machine, MachineMessage};
use api::{LaserEvents, LaserMachineNamespace, LaserState, LiveValuesEvent, StateEvent};
use control_core::{
machines::identification::{MachineIdentification, MachineIdentificationUnique},
socketio::namespace::NamespaceCacheingLogic,
use control_core::socketio::namespace::NamespaceCacheingLogic;
use smol::{
channel::{Receiver, Sender},
lock::RwLock,
};
use control_core_derive::Machine;
use smol::lock::RwLock;
use socketioxide::extract::SocketRef;
use crate::AsyncThreadMessage;
use std::{sync::Arc, time::Instant};
use uom::si::{f64::Length, length::millimeter};
use units::f64::*;
use units::length::millimeter;
pub mod act;
pub mod api;
pub mod new;
#[derive(Debug, Machine)]
#[derive(Debug)]
pub struct LaserMachine {
api_receiver: Receiver<MachineMessage>,
api_sender: Sender<MachineMessage>,
machine_identification_unique: MachineIdentificationUnique,
main_sender: Option<Sender<AsyncThreadMessage>>,
// drivers
laser: Arc<RwLock<Laser>>,
@@ -41,6 +49,42 @@ pub struct LaserMachine {
emitted_default_state: bool,
}
impl Machine for LaserMachine {
fn get_machine_identification_unique(&self) -> MachineIdentificationUnique {
self.machine_identification_unique.clone()
}
fn get_main_sender(&self) -> Option<Sender<AsyncThreadMessage>> {
self.main_sender.clone()
}
}
impl LaserMachineNamespace {
pub async fn disconnect_all(&self) {
for socket in self.connected_sockets().await {
let _ = socket.disconnect();
}
}
async fn connected_sockets(&self) -> Vec<SocketRef> {
if self.namespace.is_none() {
return vec![];
}
let sockets = self.namespace.clone().unwrap().sockets.clone();
sockets
}
}
impl Drop for LaserMachine {
fn drop(&mut self) {
tracing::info!(
"[LaserMachine::{:?}] Dropping machine and disconnecting clients...",
self.machine_identification_unique
);
smol::block_on(self.namespace.disconnect_all());
}
}
impl LaserMachine {
pub const MACHINE_IDENTIFICATION: MachineIdentification = MachineIdentification {
vendor: VENDOR_QITECH,
@@ -60,6 +104,7 @@ impl LaserMachine {
y_diameter,
roundness,
};
self.namespace
.emit(LaserEvents::LiveValues(live_values.build()));
}
@@ -1,17 +1,15 @@
use std::time::Instant;
use crate::serial::{devices::laser::Laser, registry::SERIAL_DEVICE_REGISTRY};
use crate::{MachineNewHardware, MachineNewTrait};
use super::{LaserMachine, LaserTarget, api::LaserMachineNamespace};
use anyhow::Error;
use control_core::machines::new::{MachineNewHardware, MachineNewTrait};
use uom::ConstZero;
use uom::si::{f64::Length, length::millimeter};
use units::ConstZero;
use units::length::{Length, millimeter};
impl MachineNewTrait for LaserMachine {
fn new(
params: &control_core::machines::new::MachineNewParams<'_, '_, '_, '_, '_, '_, '_>,
) -> Result<Self, Error>
fn new(params: &crate::MachineNewParams<'_, '_, '_, '_, '_, '_, '_>) -> Result<Self, Error>
where
Self: Sized,
{
@@ -34,7 +32,12 @@ impl MachineNewTrait for LaserMachine {
lower_tolerance: Length::new::<millimeter>(0.05),
diameter: Length::new::<millimeter>(1.75),
};
let mut laser_machine = Self {
let (sender, receiver) = smol::channel::unbounded();
let laser_machine = Self {
main_sender: params.main_thread_channel.clone(),
api_receiver: receiver,
api_sender: sender,
machine_identification_unique: params.get_machine_identification_unique(),
laser,
namespace: LaserMachineNamespace {
@@ -49,9 +52,6 @@ impl MachineNewTrait for LaserMachine {
roundness: None,
};
// Emit initial state
laser_machine.emit_state();
Ok(laser_machine)
}
}
+400
View File
@@ -0,0 +1,400 @@
use anyhow::Error;
use control_core::socketio::event::GenericEvent;
use control_core::socketio::namespace::Namespace;
use ethercat_hal::devices::{
EthercatDevice, SubDeviceIdentityTuple, downcast_device, subdevice_identity_to_tuple,
};
use ethercat_hal::helpers::ethercrab_types::EthercrabSubDevicePreoperational;
use ethercrab::{SubDevice, SubDeviceRef};
use machine_identification::{
DeviceHardwareIdentification, DeviceHardwareIdentificationEthercat, DeviceIdentification,
DeviceIdentificationIdentified, MachineIdentificationUnique,
};
use serde::Serialize;
use smol::channel::Sender;
use socketioxide::extract::SocketRef;
use std::fmt::Debug;
use std::{any::Any, sync::Arc, time::Instant};
pub mod aquapath1;
#[cfg(not(feature = "mock-machine"))]
pub mod buffer1;
pub mod extruder1;
pub mod laser;
pub mod machine_identification;
pub mod mock;
pub mod registry;
pub mod serial;
pub mod winder2;
pub const VENDOR_QITECH: u16 = 0x0001;
pub const MACHINE_WINDER_V1: u16 = 0x0002;
pub const MACHINE_EXTRUDER_V1: u16 = 0x0004;
pub const MACHINE_LASER_V1: u16 = 0x0006;
pub const MACHINE_MOCK: u16 = 0x0007;
pub const MACHINE_AQUAPATH_V1: u16 = 0x0009;
#[cfg(not(feature = "mock-machine"))]
pub const MACHINE_BUFFER_V1: u16 = 0x0008;
use serde_json::Value;
use smol::lock::RwLock;
#[derive(Serialize, Debug, Clone)]
pub struct MachineCrossConnectionState {
machine_identification_unique: Option<MachineIdentificationUnique>,
is_available: bool,
}
pub struct CrossConnection {
pub src: MachineIdentificationUnique,
pub dest: MachineIdentificationUnique,
}
pub enum AsyncThreadMessage {
NoMsg,
ConnectOneWayRequest(CrossConnection),
DisconnectMachines(CrossConnection),
}
pub struct MachineNewParams<
'maindevice,
'subdevices,
'device_identifications_identified,
'ethercat_devices,
'machine_new_hardware_etehrcat,
'machine_new_hardware_serial,
'machine_new_hardware,
> where
'maindevice: 'machine_new_hardware,
'subdevices: 'machine_new_hardware,
'ethercat_devices: 'machine_new_hardware,
'machine_new_hardware_etehrcat: 'machine_new_hardware,
{
pub device_group: &'device_identifications_identified Vec<DeviceIdentificationIdentified>,
pub hardware: &'machine_new_hardware MachineNewHardware<
'maindevice,
'subdevices,
'ethercat_devices,
'machine_new_hardware_etehrcat,
'machine_new_hardware_serial,
>,
pub socket_queue_tx: Sender<(SocketRef, Arc<GenericEvent>)>,
pub main_thread_channel: Option<Sender<AsyncThreadMessage>>,
pub namespace: Option<Namespace>,
}
impl MachineNewParams<'_, '_, '_, '_, '_, '_, '_> {
pub fn get_machine_identification_unique(&self) -> MachineIdentificationUnique {
self.device_group
.first()
.expect("device group must have at least one device")
.device_machine_identification
.machine_identification_unique
.clone()
}
}
pub enum MachineNewHardware<
'maindevice,
'subdevices,
'ethercat_devices,
'machine_new_hardware_etehrcat,
'machine_new_hardware_serial,
> where
'maindevice: 'machine_new_hardware_etehrcat,
'subdevices: 'machine_new_hardware_etehrcat,
'ethercat_devices: 'machine_new_hardware_etehrcat,
{
Ethercat(
&'machine_new_hardware_etehrcat MachineNewHardwareEthercat<
'maindevice,
'subdevices,
'ethercat_devices,
>,
),
Serial(&'machine_new_hardware_serial MachineNewHardwareSerial),
}
pub struct MachineNewHardwareEthercat<'maindevice, 'subdevices, 'ethercat_devices> {
pub subdevices:
&'subdevices Vec<&'subdevices SubDeviceRef<'maindevice, &'subdevices SubDevice>>,
pub ethercat_devices: &'ethercat_devices Vec<Arc<RwLock<dyn EthercatDevice>>>,
}
pub trait SerialDevice: Any + Send + Sync + SerialDeviceNew + Debug {}
pub trait SerialDeviceNew {
fn new_serial(
params: &SerialDeviceNewParams,
) -> Result<(DeviceIdentification, Arc<RwLock<Self>>), anyhow::Error>
where
Self: Sized;
}
pub trait SerialDeviceThread {
fn start_thread() -> Result<(), anyhow::Error>;
}
pub struct SerialDeviceNewParams {
pub path: String,
}
#[derive(PartialEq, Eq, Clone, Debug, Hash)]
pub struct SerialDeviceIdentification {
pub vendor_id: u16,
pub product_id: u16,
}
pub struct MachineNewHardwareSerial {
pub device: Arc<RwLock<dyn SerialDevice>>,
}
// validates that all devices in the group have the same machine identification
pub fn validate_same_machine_identification_unique(
identified_device_group: &Vec<DeviceIdentificationIdentified>,
) -> Result<(), Error> {
let machine_identification_unique = &identified_device_group
.first()
.ok_or_else(|| {
anyhow::anyhow!(
"[{}::validate_same_machine_identification] No devices in group",
module_path!()
)
})?
.device_machine_identification
.machine_identification_unique;
for device in identified_device_group.iter() {
if device
.device_machine_identification
.machine_identification_unique
!= *machine_identification_unique
{
return Err(anyhow::anyhow!(
"[{}::validate_same_machine_identification] Different machine identifications",
module_path!()
));
}
}
Ok(())
}
/// validates that every role is unique
pub fn validate_no_role_dublicates(
identified_device_group: &Vec<DeviceIdentificationIdentified>,
) -> Result<(), Error> {
let mut roles = vec![];
for device in identified_device_group.iter() {
if roles.contains(&device.device_machine_identification.role) {
return Err(anyhow::anyhow!(
"[{}::validate_no_role_dublicates] Role dublicate",
module_path!(),
));
}
roles.push(device.device_machine_identification.role);
}
Ok(())
}
// Inside control_core::machines::new module:
pub fn get_device_identification_by_role(
identified_device_group: &Vec<DeviceIdentificationIdentified>,
role: u16,
) -> Result<&DeviceIdentificationIdentified, Error> {
for device in identified_device_group.iter() {
if device.device_machine_identification.role == role {
return Ok(device);
}
}
Err(anyhow::anyhow!(
"[{}::get_device_identification_by_role] Role {} not found",
module_path!(),
role
))
}
pub fn get_device_by_index<'maindevice>(
devices: &Vec<Arc<RwLock<dyn EthercatDevice>>>,
subdevice_index: usize,
) -> Result<Arc<RwLock<dyn EthercatDevice>>, Error> {
Ok(devices
.get(subdevice_index)
.ok_or(anyhow::anyhow!(
"[{}::get_device_by_index] Index {} out of bounds for devices",
module_path!(),
subdevice_index
))?
.clone())
}
pub fn get_subdevice_by_index<'subdevices, 'maindevice>(
subdevices: &'subdevices Vec<&EthercrabSubDevicePreoperational<'maindevice>>,
subdevice_index: usize,
) -> Result<&'subdevices EthercrabSubDevicePreoperational<'maindevice>, Error> {
Ok(subdevices.get(subdevice_index).ok_or(anyhow::anyhow!(
"Index {} out of bounds for subdevices",
subdevice_index
))?)
}
pub fn get_ethercat_device_by_index<'maindevice>(
ethercat_devices: &Vec<Arc<RwLock<dyn EthercatDevice>>>,
subdevice_index: usize,
) -> Result<Arc<RwLock<dyn EthercatDevice>>, Error> {
Ok(ethercat_devices
.get(subdevice_index)
.ok_or(anyhow::anyhow!(
"[{}::get_ethercat_device_by_index] Index {} out of bounds for ethercat devices",
module_path!(),
subdevice_index
))?
.clone())
}
pub trait MachineNewTrait {
fn new(params: &MachineNewParams<'_, '_, '_, '_, '_, '_, '_>) -> Result<Self, anyhow::Error>
where
Self: Sized;
}
#[derive(Debug)]
pub struct MachineConnection {
pub ident: MachineIdentificationUnique,
pub connection: Sender<MachineMessage>,
}
pub trait MachineAct {
fn act_machine_message(&mut self, msg: MachineMessage);
fn act(&mut self, now: Instant);
}
// generic MachineMessage allows us to implement actions
// to manage or mutate machines with simple messages sent to the Recv Channel of the given Machine,
// which the machine itself will handle to avoid locking
// This also allows for simplified "CrossConnections"
#[derive(Debug)]
pub enum MachineMessage {
SubscribeNamespace(Namespace),
UnsubscribeNamespace,
HttpApiJsonRequest(serde_json::Value),
ConnectToMachine(MachineConnection),
DisconnectMachine(MachineConnection),
}
pub trait MachineApi {
fn api_get_sender(&self) -> Sender<MachineMessage>;
fn api_mutate(&mut self, value: Value) -> Result<(), anyhow::Error>;
fn api_event_namespace(&mut self) -> Option<Namespace>;
}
pub trait Machine: MachineAct + MachineNewTrait + MachineApi + Any + Debug + Send + Sync {
fn get_machine_identification_unique(&self) -> MachineIdentificationUnique;
fn get_main_sender(&self) -> Option<Sender<AsyncThreadMessage>>;
}
pub trait AnyGetters: Any {
fn as_any(&self) -> &dyn Any;
fn as_any_mut(&mut self) -> &mut dyn Any;
}
async fn get_device_ident<
'maindevice,
'subdevices,
'device_identifications_identified,
'ethercat_devices,
'machine_new_hardware_etehrcat,
'machine_new_hardware_serial,
'machine_new_hardware,
>(
params: &MachineNewParams<
'maindevice,
'subdevices,
'device_identifications_identified,
'ethercat_devices,
'machine_new_hardware_etehrcat,
'machine_new_hardware_serial,
'machine_new_hardware,
>,
role: u16,
) -> Result<DeviceHardwareIdentificationEthercat, anyhow::Error> {
let device_identification = get_device_identification_by_role(params.device_group, role)?;
let device_hardware_identification_ethercat =
match &device_identification.device_hardware_identification {
DeviceHardwareIdentification::Ethercat(device_hardware_identification_ethercat) => {
device_hardware_identification_ethercat
}
_ => {
return Err(anyhow::anyhow!(
"[{}::MachineNewTrait/ExtruderV2::new] Device with role {} is not Ethercat",
module_path!(),
role
));
}
};
return Ok(device_hardware_identification_ethercat.clone());
}
async fn get_ethercat_device<
'maindevice,
'subdevices,
'device_identifications_identified,
'ethercat_devices,
'machine_new_hardware_etehrcat,
'machine_new_hardware_serial,
'machine_new_hardware,
T,
>(
hardware: &&MachineNewHardwareEthercat<'maindevice, 'subdevices, 'ethercat_devices>,
params: &MachineNewParams<
'maindevice,
'subdevices,
'device_identifications_identified,
'ethercat_devices,
'machine_new_hardware_etehrcat,
'machine_new_hardware_serial,
'machine_new_hardware,
>,
role: u16,
expected_identities: Vec<SubDeviceIdentityTuple>,
) -> Result<
(
Arc<RwLock<T>>,
&'subdevices SubDeviceRef<'subdevices, &'subdevices SubDevice>,
),
anyhow::Error,
>
where
T: 'static + Send + Sync + EthercatDevice,
{
let device_hardware_identification_ethercat = get_device_ident(params, role).await?;
let subdevice_index = device_hardware_identification_ethercat.subdevice_index;
let subdevice = get_subdevice_by_index(hardware.subdevices, subdevice_index)?;
let subdevice_identity = subdevice.identity();
let actual_identity = subdevice_identity_to_tuple(&subdevice_identity);
let mut matched_any_identity = false;
for identity in expected_identities.clone() {
if actual_identity == identity {
matched_any_identity = true;
}
}
if !matched_any_identity {
return Err(anyhow::anyhow!(
"[{}::MachineNewTrait/ExtruderV2::new] Device identity mismatch: expected {:?}",
module_path!(),
expected_identities
));
}
let ethercat_device =
get_ethercat_device_by_index(&hardware.ethercat_devices, subdevice_index)?;
let device = downcast_device::<T>(ethercat_device).await?;
{
let mut device_guard = device.write().await;
device_guard.set_used(true);
}
Ok((device, subdevice))
}
@@ -1,3 +1,117 @@
use std::fmt::Display;
use serde::Deserialize;
use serde::Serialize;
/// Identifies a spacifi machine
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
pub struct MachineIdentificationUnique {
pub machine_identification: MachineIdentification,
pub serial: u16,
}
impl MachineIdentificationUnique {
/// Check if values are non-zero
pub const fn is_valid(&self) -> bool {
self.machine_identification.is_valid() && self.serial != 0
}
}
impl Display for MachineIdentificationUnique {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{}/{}/{}",
self.machine_identification.vendor, self.machine_identification.machine, self.serial
)
}
}
/// Identifies a machine
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
pub struct MachineIdentification {
pub vendor: u16,
pub machine: u16,
}
impl MachineIdentification {
/// Check if values are non-zero
pub const fn is_valid(&self) -> bool {
self.vendor != 0 && self.machine != 0
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct DeviceMachineIdentification {
pub machine_identification_unique: MachineIdentificationUnique,
pub role: u16,
}
impl DeviceMachineIdentification {
/// Check if values are non-zero
pub const fn is_valid(&self) -> bool {
self.machine_identification_unique.is_valid()
&& self
.machine_identification_unique
.machine_identification
.machine
!= 0
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct DeviceIdentification {
pub device_machine_identification: Option<DeviceMachineIdentification>,
pub device_hardware_identification: DeviceHardwareIdentification,
}
impl From<DeviceIdentificationIdentified> for DeviceIdentification {
fn from(value: DeviceIdentificationIdentified) -> Self {
Self {
device_machine_identification: Some(value.device_machine_identification),
device_hardware_identification: value.device_hardware_identification,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct DeviceIdentificationIdentified {
pub device_machine_identification: DeviceMachineIdentification,
pub device_hardware_identification: DeviceHardwareIdentification,
}
impl TryFrom<DeviceIdentification> for DeviceIdentificationIdentified {
type Error = anyhow::Error;
fn try_from(value: DeviceIdentification) -> Result<Self, Self::Error> {
let device_machine_identification =
value.device_machine_identification.ok_or(anyhow::anyhow!(
"[{}::DeviceIdentificationIdentified::try_from] No device machine identification",
module_path!()
))?;
Ok(Self {
device_machine_identification,
device_hardware_identification: value.device_hardware_identification,
})
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub enum DeviceHardwareIdentification {
Ethercat(DeviceHardwareIdentificationEthercat),
Serial(DeviceHardwareIdentificationSerial),
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct DeviceHardwareIdentificationEthercat {
pub subdevice_index: usize,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct DeviceHardwareIdentificationSerial {
pub path: String,
}
use anyhow::Error;
use anyhow::anyhow;
use ethercat_hal::devices::ek1100::EK1100_IDENTITY_A;
@@ -33,10 +147,6 @@ use ethercat_hal::helpers::ethercrab_types::{
use ethercrab::MainDevice;
use ethercrab::SubDeviceIdentity;
use crate::machines::identification::DeviceMachineIdentification;
use crate::machines::identification::MachineIdentification;
use crate::machines::identification::MachineIdentificationUnique;
#[derive(Debug)]
pub struct MachineIdentificationAddresses {
pub vendor_word: u16,
@@ -1,6 +1,5 @@
use control_core::machines::new::MachineAct;
use super::MockMachine;
use crate::{MachineAct, MachineMessage};
use std::time::{Duration, Instant};
/// Implements the `MachineAct` trait for the `MockMachine`.
@@ -21,6 +20,13 @@ use std::time::{Duration, Instant};
///
impl MachineAct for MockMachine {
fn act(&mut self, now: Instant) {
let msg = self.api_receiver.try_recv();
match msg {
Ok(msg) => {
let _res = self.act_machine_message(msg);
}
Err(_) => (),
};
// Only emit live values if machine is in Running mode
// The live values are updated approximately 30 times per second
if now.duration_since(self.last_measurement_emit) > Duration::from_secs_f64(1.0 / 30.0) {
@@ -28,4 +34,29 @@ impl MachineAct for MockMachine {
self.last_measurement_emit = now;
}
}
fn act_machine_message(&mut self, msg: MachineMessage) {
match msg {
MachineMessage::SubscribeNamespace(namespace) => {
self.namespace.namespace = Some(namespace);
self.emit_state();
}
MachineMessage::UnsubscribeNamespace => self.namespace.namespace = None,
MachineMessage::HttpApiJsonRequest(value) => {
use crate::MachineApi;
let _res = self.api_mutate(value);
}
MachineMessage::ConnectToMachine(_machine_connection) =>
/*Doesnt connec to any Macine do nothing*/
{
()
}
MachineMessage::DisconnectMachine(_machine_connection) =>
/*Doesnt connec to any Machine do nothing*/
{
()
}
}
}
}
@@ -1,19 +1,17 @@
use crate::{MachineApi, MachineMessage};
use super::MockMachine;
use control_core::{
machines::api::MachineApi,
socketio::{
event::{Event, GenericEvent},
namespace::{
CacheFn, CacheableEvents, Namespace, NamespaceCacheingLogic, cache_duration,
cache_first_and_last_event,
},
use control_core::socketio::{
event::{Event, GenericEvent},
namespace::{
CacheFn, CacheableEvents, Namespace, NamespaceCacheingLogic, cache_first_and_last_event,
},
};
use control_core_derive::BuildEvent;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use smol::lock::Mutex;
use std::{sync::Arc, time::Duration};
use smol::channel::Sender;
use std::sync::Arc;
use tracing::instrument;
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
@@ -60,7 +58,7 @@ pub enum MockEvents {
#[derive(Debug)]
pub struct MockMachineNamespace {
pub namespace: Arc<Mutex<Namespace>>,
pub namespace: Option<Namespace>,
}
impl CacheableEvents<Self> for MockEvents {
@@ -72,11 +70,10 @@ impl CacheableEvents<Self> for MockEvents {
}
fn event_cache_fn(&self) -> CacheFn {
let cache_one_hour = cache_duration(Duration::from_secs(60 * 60), Duration::from_secs(1));
let cache_first_and_last = cache_first_and_last_event();
match self {
Self::LiveValues(_) => cache_one_hour,
Self::LiveValues(_) => cache_first_and_last,
Self::State(_) => cache_first_and_last,
}
}
@@ -98,12 +95,18 @@ impl NamespaceCacheingLogic<MockEvents> for MockMachineNamespace {
let event = Arc::new(events.event_value());
let buffer_fn = events.event_cache_fn();
let mut namespace = self.namespace.lock_blocking();
namespace.emit(event, &buffer_fn);
match &mut self.namespace {
Some(ns) => ns.emit(event, &buffer_fn),
None => (),
}
}
}
impl MachineApi for MockMachine {
fn api_get_sender(&self) -> Sender<MachineMessage> {
self.api_sender.clone()
}
fn api_mutate(&mut self, request_body: Value) -> Result<(), anyhow::Error> {
let mutation: Mutation = serde_json::from_value(request_body)?;
match mutation {
@@ -123,7 +126,7 @@ impl MachineApi for MockMachine {
Ok(())
}
fn api_event_namespace(&mut self) -> Arc<Mutex<Namespace>> {
fn api_event_namespace(&mut self) -> Option<Namespace> {
self.namespace.namespace.clone()
}
}
@@ -1,26 +1,25 @@
use crate::machines::{MACHINE_MOCK, VENDOR_QITECH};
use api::{LiveValuesEvent, MockEvents, MockMachineNamespace, Mode, ModeState, StateEvent};
use control_core::machines::identification::MachineIdentificationUnique;
use control_core::socketio::event::BuildEvent;
use control_core::{
machines::identification::MachineIdentification, socketio::namespace::NamespaceCacheingLogic,
use crate::AsyncThreadMessage;
use crate::{
MACHINE_MOCK, Machine, MachineMessage, VENDOR_QITECH,
machine_identification::{MachineIdentification, MachineIdentificationUnique},
};
use control_core_derive::Machine;
use api::{LiveValuesEvent, MockEvents, MockMachineNamespace, Mode, ModeState, StateEvent};
use control_core::socketio::event::BuildEvent;
use control_core::socketio::namespace::NamespaceCacheingLogic;
use smol::channel::{Receiver, Sender};
use std::time::Instant;
use tracing::info;
use uom::si::{
f64::Frequency,
frequency::{hertz, millihertz},
};
use units::f64::*;
use units::frequency::{hertz, millihertz};
pub mod act;
pub mod api;
pub mod new;
#[derive(Debug, Machine)]
#[derive(Debug)]
pub struct MockMachine {
machine_identification_unique: MachineIdentificationUnique,
main_sender: Option<Sender<AsyncThreadMessage>>,
// socketio
namespace: MockMachineNamespace,
@@ -39,6 +38,18 @@ pub struct MockMachine {
/// Will be initialized as false and set to true by emit_state
/// This way we can signal to the client that the first state emission is a default state
emitted_default_state: bool,
api_sender: Sender<MachineMessage>,
api_receiver: Receiver<MachineMessage>,
}
impl Machine for MockMachine {
fn get_machine_identification_unique(&self) -> MachineIdentificationUnique {
self.machine_identification_unique.clone()
}
fn get_main_sender(&self) -> Option<Sender<AsyncThreadMessage>> {
self.main_sender.clone()
}
}
impl MockMachine {
@@ -4,9 +4,10 @@ use super::{
MockMachine,
api::{MockMachineNamespace, Mode},
};
use crate::{MachineNewHardware, MachineNewParams, MachineNewTrait};
use anyhow::Error;
use control_core::machines::new::{MachineNewHardware, MachineNewParams, MachineNewTrait};
use uom::si::{f64::Frequency, frequency::hertz};
use units::f64::Frequency;
use units::frequency::hertz;
impl MachineNewTrait for MockMachine {
fn new<'maindevice, 'subdevices>(
@@ -30,8 +31,11 @@ impl MachineNewTrait for MockMachine {
}
let now = Instant::now();
let (sender, receiver) = smol::channel::unbounded();
let mut mock_machine = Self {
main_sender: params.main_thread_channel.clone(),
api_receiver: receiver,
api_sender: sender,
machine_identification_unique: params.get_machine_identification_unique(),
namespace: MockMachineNamespace {
namespace: params.namespace.clone(),
@@ -1,10 +1,20 @@
use super::{Machine, identification::MachineIdentification, new::MachineNewParams};
#[cfg(feature = "mock-machine")]
use crate::{extruder1::mock::ExtruderV2, mock::MockMachine, winder2::mock::Winder2};
use crate::{Machine, MachineNewParams, machine_identification::MachineIdentification};
#[cfg(not(feature = "mock-machine"))]
use crate::{
aquapath1::AquaPathV1, buffer1::BufferV1, extruder1::ExtruderV2, laser::LaserMachine,
winder2::Winder2,
};
use lazy_static::lazy_static;
use anyhow::Error;
use smol::lock::Mutex;
use std::{any::TypeId, collections::HashMap, sync::Arc};
use std::{any::TypeId, collections::HashMap};
pub type MachineNewClosure =
Box<dyn Fn(&MachineNewParams) -> Result<Arc<Mutex<dyn Machine>>, Error> + Send + Sync>;
Box<dyn Fn(&MachineNewParams) -> Result<Box<dyn Machine>, Error> + Send + Sync>;
pub struct MachineRegistry {
type_map: HashMap<TypeId, (MachineIdentification, MachineNewClosure)>,
@@ -32,9 +42,7 @@ impl MachineRegistry {
(
machine_identficiation.clone(),
// create a machine construction closure
Box::new(|machine_new_params| {
Ok(Arc::new(Mutex::new(T::new(machine_new_params)?)))
}),
Box::new(|machine_new_params| Ok(Box::new(T::new(machine_new_params)?))),
),
);
}
@@ -42,7 +50,7 @@ impl MachineRegistry {
pub fn new_machine(
&self,
machine_new_params: &MachineNewParams,
) -> Result<Arc<Mutex<dyn Machine>>, anyhow::Error> {
) -> Result<Box<dyn Machine>, anyhow::Error> {
// get machiine identification
let device_identification =
&machine_new_params
@@ -72,3 +80,24 @@ impl MachineRegistry {
(machine_new_closure)(machine_new_params)
}
}
lazy_static! {
pub static ref MACHINE_REGISTRY: MachineRegistry = {
let mut mc = MachineRegistry::new();
mc.register::<Winder2>(Winder2::MACHINE_IDENTIFICATION);
mc.register::<ExtruderV2>(ExtruderV2::MACHINE_IDENTIFICATION);
#[cfg(feature = "mock-machine")]
mc.register::<MockMachine>(MockMachine::MACHINE_IDENTIFICATION);
#[cfg(not(feature = "mock-machine"))]
mc.register::<LaserMachine>(LaserMachine::MACHINE_IDENTIFICATION);
#[cfg(not(feature = "mock-machine"))]
mc.register::<BufferV1>(BufferV1::MACHINE_IDENTIFICATION);
#[cfg(not(feature = "mock-machine"))]
mc.register::<AquaPathV1>(AquaPathV1::MACHINE_IDENTIFICATION);
mc
};
}
@@ -1,16 +1,15 @@
use std::sync::Arc;
use control_core::{
helpers::hashing::{byte_folding_u16, hash_djb2},
machines::identification::{
use control_core::helpers::hashing::{byte_folding_u16, hash_djb2};
use smol::lock::RwLock;
use crate::{
MACHINE_EXTRUDER_V1, SerialDevice, SerialDeviceNew, SerialDeviceNewParams, VENDOR_QITECH,
machine_identification::{
DeviceHardwareIdentification, DeviceHardwareIdentificationSerial, DeviceIdentification,
DeviceMachineIdentification, MachineIdentification, MachineIdentificationUnique,
},
serial::{SerialDevice, SerialDeviceNew, SerialDeviceNewParams},
};
use smol::lock::RwLock;
use crate::machines::{MACHINE_EXTRUDER_V1, VENDOR_QITECH};
/// Mock serial device for testing MockMachine
/// This provides a minimal SerialDevice implementation that doesn't require actual hardware
@@ -1,34 +1,35 @@
use anyhow::anyhow;
use control_core::helpers::hashing::{byte_folding_u16, hash_djb2};
use control_core::helpers::retry::retry_n_times;
use control_core::modbus::ModbusResponse;
use control_core::modbus::{self, ModbusRequest};
use serialport::SerialPort;
use serialport::{ClearBuffer, DataBits, FlowControl, Parity, StopBits};
use smol::lock::RwLock;
use std::sync::atomic::{AtomicBool, Ordering};
use std::{
io::Write,
sync::Arc,
thread,
time::{Duration, Instant},
};
use units::f64::Length;
use crate::machines::{MACHINE_LASER_V1, VENDOR_QITECH};
use anyhow::anyhow;
use control_core::{
helpers::{
hashing::{byte_folding_u16, hash_djb2},
retry::retry_n_times,
},
machines::identification::{
DeviceHardwareIdentification, DeviceHardwareIdentificationSerial, DeviceIdentification,
DeviceMachineIdentification, MachineIdentification, MachineIdentificationUnique,
},
modbus::{self, ModbusRequest, ModbusResponse},
serial::{SerialDevice, SerialDeviceNew, SerialDeviceNewParams},
use crate::machine_identification::{
DeviceHardwareIdentification, DeviceHardwareIdentificationSerial, DeviceIdentification,
DeviceMachineIdentification, MachineIdentification, MachineIdentificationUnique,
};
use serialport::SerialPort;
use serialport::{ClearBuffer, DataBits, FlowControl, Parity, StopBits};
use smol::lock::RwLock;
use uom::si::f64::Length;
use crate::{
MACHINE_LASER_V1, SerialDevice, SerialDeviceNew, SerialDeviceNewParams, VENDOR_QITECH,
};
use units::length::millimeter;
/// The struct of Laser Device
#[derive(Debug)]
pub struct Laser {
pub data: Option<LaserData>,
pub path: String,
pub shutdown_flag: Arc<AtomicBool>,
}
impl SerialDevice for Laser {}
@@ -59,15 +60,15 @@ impl TryFrom<ModbusResponse> for LaserDiameterResponse {
let x = u16::from_be_bytes([value.data[3], value.data[4]]) as f64 / 1000.0;
let y = u16::from_be_bytes([value.data[5], value.data[6]]) as f64 / 1000.0;
(
Some(Length::new::<uom::si::length::millimeter>(x)),
Some(Length::new::<uom::si::length::millimeter>(y)),
Some(Length::new::<millimeter>(x)),
Some(Length::new::<millimeter>(y)),
)
} else {
(None, None)
};
Ok(Self {
diameter: Length::new::<uom::si::length::millimeter>(diameter),
diameter: Length::new::<millimeter>(diameter),
x_axis,
y_axis,
})
@@ -94,7 +95,7 @@ impl SerialDeviceNew for Laser {
params: &SerialDeviceNewParams,
) -> Result<(DeviceIdentification, Arc<RwLock<Self>>), anyhow::Error> {
let laser_data = Some(LaserData {
diameter: Length::new::<uom::si::length::millimeter>(0.0),
diameter: Length::new::<millimeter>(0.0),
x_axis: None,
y_axis: None,
last_timestamp: Instant::now(),
@@ -119,15 +120,17 @@ impl SerialDeviceNew for Laser {
),
};
let shutdown_flag: Arc<AtomicBool> = AtomicBool::new(false).into();
// Create a new Laser instance
let _self = Arc::new(RwLock::new(Self {
data: laser_data,
path: params.path.clone(),
shutdown_flag: shutdown_flag.clone(),
}));
//// Spawn the device thread
let _self_clone = _self.clone();
thread::Builder::new()
let _ = thread::Builder::new()
.name("laser".to_owned())
.spawn(move || {
smol::block_on(async {
@@ -139,6 +142,14 @@ impl SerialDeviceNew for Laser {
}
}
impl Drop for Laser {
fn drop(&mut self) {
// Signal shutdown
self.shutdown_flag.store(true, Ordering::SeqCst);
println!("Laser struct dropped, thread stopped");
}
}
#[derive(Debug, Clone)]
pub struct LaserData {
pub diameter: Length,
@@ -197,7 +208,7 @@ impl Laser {
port.clear(ClearBuffer::All).ok();
loop {
while !_self.read().await.shutdown_flag.load(Ordering::SeqCst) {
// send diameter request
let response = retry_n_times(10, || {
if let Err(e) = port.write_all(&request_buffer) {
@@ -211,7 +222,6 @@ impl Laser {
38400,
8,
));
modbus::receive_data_modbus(&mut *port)?
.map(ModbusResponse::try_from)
.transpose()
@@ -230,5 +240,6 @@ impl Laser {
});
}
}
Ok(())
}
}
@@ -1,16 +1,15 @@
use std::sync::Arc;
use control_core::{
helpers::hashing::{byte_folding_u16, hash_djb2},
machines::identification::{
use control_core::helpers::hashing::{byte_folding_u16, hash_djb2};
use smol::lock::RwLock;
use crate::{
MACHINE_MOCK, SerialDevice, SerialDeviceNew, SerialDeviceNewParams, VENDOR_QITECH,
machine_identification::{
DeviceHardwareIdentification, DeviceHardwareIdentificationSerial, DeviceIdentification,
DeviceMachineIdentification, MachineIdentification, MachineIdentificationUnique,
},
serial::{SerialDevice, SerialDeviceNew, SerialDeviceNewParams},
};
use smol::lock::RwLock;
use crate::machines::{MACHINE_MOCK, VENDOR_QITECH};
/// Mock serial device for testing MockMachine
/// This provides a minimal SerialDevice implementation that doesn't require actual hardware
File renamed without changes.
Loaded 100 of 183 files, more files were not shown because too many files have changed in this diff. Show more