Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2d28c18b1a | ||
|
|
b19764281a | ||
|
|
c25d63f781 | ||
|
|
d79efc412f | ||
|
|
7ad74cf089 | ||
|
|
6073d37892 | ||
|
|
c04937553f | ||
|
|
e08bdfa3c7 | ||
|
|
d282678b19 | ||
|
|
9ff4243350 | ||
|
|
6c79b33752 | ||
|
|
861b5da7a3 | ||
|
|
7dd3d50653 | ||
|
|
ba8e04c641 | ||
|
|
cece245a55 | ||
|
|
fab4d2810e | ||
|
|
5bff3ca929 | ||
|
|
afe1d3b80e | ||
|
|
6c8005dba9 | ||
|
|
9f399ca6f3 | ||
|
|
a7574dd348 | ||
|
|
b39f3597c8 | ||
|
|
bb9a86d52e | ||
|
|
e5329e40c6 | ||
|
|
319611f69d | ||
|
|
4b16253c8c | ||
|
|
b60423fde6 | ||
|
|
5d3399c72e | ||
|
|
d247bafcbb | ||
|
|
5227aad4c6 | ||
|
|
da4c54a99e | ||
|
|
8d42bd93f7 | ||
|
|
37e467272b | ||
|
|
ed0b9cd5ae | ||
|
|
8045f799f0 | ||
|
|
ca6ea16f89 | ||
|
|
452614abf1 | ||
|
|
f63c797cd1 | ||
|
|
de791f6ea8 |
No files matched your search
@@ -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
File diff suppressed because it is too large.
Load diff
+5
-9
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
-1
@@ -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
-1
@@ -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,
|
||||
|
||||
+3
-3
@@ -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);
|
||||
|
||||
+7
-7
@@ -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,2 +1 @@
|
||||
pub mod eeprom_identification;
|
||||
pub mod interface_discovery;
|
||||
@@ -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;
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
@@ -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>,
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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 +0,0 @@
|
||||
pub mod mutation;
|
||||
@@ -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,3 +1,2 @@
|
||||
pub mod event;
|
||||
pub mod namespace;
|
||||
pub mod namespace_id;
|
||||
@@ -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,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";
|
||||
}
|
||||
@@ -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";
|
||||
}
|
||||
@@ -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";
|
||||
}
|
||||
@@ -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
|
||||
|
||||

|
||||
|
||||
## The Application is Split Into 3 Main Layers
|
||||
|
||||
@@ -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,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.
@@ -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.
|
||||
@@ -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)
|
||||
@@ -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.
|
||||
Generated
+12
-36
@@ -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:
|
||||

|
||||
|
||||

|
||||
|
||||
## Mechanical Diagrams
|
||||
|
||||

|
||||
@@ -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,
|
||||
});
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
) => {
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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::{
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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::*;
|
||||
|
||||
|
||||
@@ -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"]
|
||||
@@ -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(),
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
File renamed without changes.
@@ -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 ¶ms.hardware {
|
||||
MachineNewHardware::Ethercat(x) => x,
|
||||
_ => {
|
||||
return Err(anyhow::anyhow!(
|
||||
"[{}::MachineNewTrait/Buffer::new] MachineNewHardware is not Ethercat",
|
||||
module_path!()
|
||||
));
|
||||
}
|
||||
};
|
||||
let hardware: &&MachineNewHardwareEthercat<'_, '_, '_> = match ¶ms.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();
|
||||
+4
-6
@@ -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
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
+7
-1
@@ -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 ¶ms.hardware {
|
||||
MachineNewHardware::Ethercat(x) => x,
|
||||
_ => {
|
||||
return Err(anyhow::anyhow!(
|
||||
"[{}::MachineNewTrait/Extruder2::new] MachineNewHardware is not Ethercat",
|
||||
module_path!()
|
||||
));
|
||||
}
|
||||
};
|
||||
let hardware: &&MachineNewHardwareEthercat<'_, '_, '_> = match ¶ms.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(),
|
||||
+6
-8
@@ -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
-1
@@ -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)]
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
+114
-4
@@ -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
|
||||
};
|
||||
}
|
||||
+6
-7
@@ -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
Reference in new issue
Block a user