Compare commits
19
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2cdc5f791e | ||
|
|
1f426b698b | ||
|
|
fd2790ad10 | ||
|
|
b98f92426c | ||
|
|
a759970488 | ||
|
|
26c91c7ddd | ||
|
|
1f6f43c7ec | ||
|
|
2c84aeed5b | ||
|
|
a0625b376d | ||
|
|
866dfcec34 | ||
|
|
977927810e | ||
|
|
82d62be4ef | ||
|
|
9008f77b8b | ||
|
|
793b0ebf37 | ||
|
|
2df42517a0 | ||
|
|
026549c37f | ||
|
|
c6a85d4fe0 | ||
|
|
07d04f3204 | ||
|
|
3c325ee386 |
No files matched your search
@@ -1,189 +0,0 @@
|
||||
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
|
||||
const IFACE_DISCOVERY_MAX_PDU_DATA: usize = PduStorage::element_size(1100);
|
||||
const IFACE_DISCOVERY_MAX_FRAMES: usize = 16;
|
||||
const IFACE_DISCOVERY_MAX_PDI_LEN: usize = 128;
|
||||
|
||||
/// Sets a network interface to unmanaged by NetworkManager.
|
||||
/// Returns true if the command succeeded.
|
||||
pub fn set_interface_managed(interface: &str, managed: bool) -> bool {
|
||||
let managed_str = match managed {
|
||||
true => "yes",
|
||||
false => "no",
|
||||
};
|
||||
tracing::info!(
|
||||
"set_interface_managed for {} managed was set to: {}",
|
||||
interface,
|
||||
managed_str
|
||||
);
|
||||
let status = Command::new("nmcli")
|
||||
.args(["dev", "set", interface, "managed", managed_str])
|
||||
.status();
|
||||
|
||||
matches!(status, Ok(s) if s.success())
|
||||
}
|
||||
|
||||
/// Returns true if the given network interface is Ethernet.
|
||||
/// Prevents testing wlan,loopback and other non ethernet devices
|
||||
fn is_ethernet(interface: &str) -> std::io::Result<bool> {
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
let base_path = format!("/sys/class/net/{}", interface);
|
||||
let type_path = std::path::Path::new(&base_path).join("type");
|
||||
let iface_type = std::fs::read_to_string(&type_path)?.trim().to_string();
|
||||
// 1 means Ethernet
|
||||
let reports_as_ethernet = iface_type == "1";
|
||||
// Double-check that it's not a wireless interface
|
||||
let uevent_path = std::path::Path::new(&base_path).join("uevent");
|
||||
let uevent = std::fs::read_to_string(&uevent_path).unwrap_or_default();
|
||||
// If "DEVTYPE=wlan" appears, it's lying
|
||||
let actually_wifi = uevent.contains("DEVTYPE=wlan");
|
||||
return Ok(reports_as_ethernet && !actually_wifi);
|
||||
}
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
return Ok(true);
|
||||
}
|
||||
|
||||
pub async fn discover_ethercat_interface() -> Result<String, anyhow::Error> {
|
||||
tracing::info!("Discovering EtherCAT interface...");
|
||||
|
||||
// Set up a custom panic hook that suppresses panic output
|
||||
let default_hook = std::panic::take_hook();
|
||||
std::panic::set_hook(Box::new(move |panic_info| {
|
||||
// Optional: log the panic in a controlled way
|
||||
if let Some(location) = panic_info.location() {
|
||||
tracing::debug!(
|
||||
"Suppressed panic in interface test: {} (at {}:{})",
|
||||
panic_info,
|
||||
location.file(),
|
||||
location.line()
|
||||
);
|
||||
} else {
|
||||
tracing::debug!("Suppressed panic in interface test: {}", panic_info);
|
||||
}
|
||||
}));
|
||||
|
||||
// Get eligible interfaces
|
||||
let mut interfaces = Interface::get_all()
|
||||
.map_err(|e| anyhow::anyhow!("Failed to get network interfaces: {}", e))?
|
||||
.into_iter()
|
||||
.filter(|iface| {
|
||||
iface.is_up()
|
||||
&& iface.is_running()
|
||||
&& !iface.is_loopback()
|
||||
&& is_ethernet(&iface.name).unwrap_or(false)
|
||||
&& !iface.name.starts_with("bridge")
|
||||
&& !iface.name.starts_with("utun") // Exclude tunnel interfaces
|
||||
&& !iface.name.starts_with("awdl") // Exclude Apple Wireless Direct Link
|
||||
&& !iface.name.starts_with("anpi") // Exclude Apple Network Privacy Interface
|
||||
&& !iface.name.starts_with("llw") // Exclude Apple low-latency WLAN
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
interfaces.sort_by(|a, b| a.name.cmp(&b.name));
|
||||
let mut interface: Option<&str> = None;
|
||||
|
||||
for i in 0..interfaces.len() {
|
||||
match test_interface(&interfaces[i].name) {
|
||||
Ok(_) => {
|
||||
// if interface found with ethercat Exit early, we expect only one interface with ethercat
|
||||
interface = Some(&interfaces[i].name);
|
||||
break;
|
||||
}
|
||||
Err(_) => (),
|
||||
}
|
||||
}
|
||||
|
||||
for i in 0..interfaces.len() {
|
||||
if interface.is_some() && interface.unwrap() == &interfaces[i].name {
|
||||
set_interface_managed(&interfaces[i].name, false);
|
||||
} else {
|
||||
set_interface_managed(&interfaces[i].name, true);
|
||||
}
|
||||
}
|
||||
|
||||
// Restore the default panic hook
|
||||
std::panic::set_hook(default_hook);
|
||||
|
||||
match interface {
|
||||
Some(interface) => return Ok(interface.to_string()),
|
||||
None => return Err(anyhow::anyhow!("No suitable EtherCAT interface found")),
|
||||
}
|
||||
}
|
||||
|
||||
fn test_interface(interface: &str) -> Result<(), anyhow::Error> {
|
||||
tracing::info!("Testing interface: {}", interface);
|
||||
|
||||
let pdu_storage = Box::leak(Box::new(PduStorage::<
|
||||
IFACE_DISCOVERY_MAX_FRAMES,
|
||||
IFACE_DISCOVERY_MAX_PDU_DATA,
|
||||
>::new()));
|
||||
let (tx, rx, pdu_loop) = pdu_storage
|
||||
.try_split()
|
||||
.map_err(|e| anyhow::anyhow!("Failed to split PDU storage: {:?}", e))?;
|
||||
|
||||
let rt = smol::LocalExecutor::new();
|
||||
|
||||
let result = rt.run(async {
|
||||
let tx_rx_handle = rt.spawn(
|
||||
tx_rx_task(interface, tx, rx)
|
||||
.map_err(|e| anyhow::anyhow!("Failed to spawn TX/RX task: {}", e))?,
|
||||
);
|
||||
|
||||
let maindevice = Arc::new(MainDevice::new(
|
||||
pdu_loop,
|
||||
Timeouts {
|
||||
// Default 5000ms
|
||||
state_transition: Duration::from_millis(5000),
|
||||
// Default 30_000us
|
||||
pdu: Duration::from_micros(30_000),
|
||||
// Default 10ms
|
||||
eeprom: Duration::from_millis(10),
|
||||
// Default 0ms
|
||||
wait_loop_delay: Duration::from_millis(0),
|
||||
// Default 100ms
|
||||
mailbox_echo: Duration::from_millis(100),
|
||||
// Default 1000ms
|
||||
mailbox_response: Duration::from_millis(1000),
|
||||
},
|
||||
MainDeviceConfig {
|
||||
// Default 10000
|
||||
dc_static_sync_iterations: 10000,
|
||||
// Default None
|
||||
retry_behaviour: ethercrab::RetryBehaviour::Count(3),
|
||||
},
|
||||
));
|
||||
|
||||
let result = maindevice
|
||||
.init_single_group::<IFACE_DISCOVERY_MAX_SUBDEVICES, IFACE_DISCOVERY_MAX_PDI_LEN>(
|
||||
ethercat_now,
|
||||
)
|
||||
.await
|
||||
.map(|_| ())
|
||||
.map_err(|e| anyhow::anyhow!("Failed to initialize group: {}", e));
|
||||
|
||||
tx_rx_handle.cancel().await;
|
||||
|
||||
result
|
||||
});
|
||||
|
||||
// await the result of the async block
|
||||
let result = smol::block_on(result);
|
||||
|
||||
if let Err(e) = result {
|
||||
return Err(anyhow::anyhow!(
|
||||
"Failed to initialize EtherCAT on interface {}: {:?}",
|
||||
interface,
|
||||
e
|
||||
));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
pub mod interface_discovery;
|
||||
@@ -0,0 +1,126 @@
|
||||
use anyhow::{Context, Result};
|
||||
use ethercrab::{
|
||||
MainDevice, MainDeviceConfig, PduStorage, RetryBehaviour, Timeouts,
|
||||
std::{ethercat_now, tx_rx_task},
|
||||
};
|
||||
|
||||
use crate::{
|
||||
ethernet::{get_interfaces, set_interface_managed},
|
||||
futures::FutureIteratorExt,
|
||||
};
|
||||
|
||||
// Constants for EtherCAT configuration
|
||||
const IFACE_DISCOVERY_MAX_SUBDEVICES: usize = 16; // Must be power of 2 > 1
|
||||
const IFACE_DISCOVERY_MAX_PDU_DATA: usize = PduStorage::element_size(1100);
|
||||
const IFACE_DISCOVERY_MAX_FRAMES: usize = 16;
|
||||
const IFACE_DISCOVERY_MAX_PDI_LEN: usize = 128;
|
||||
|
||||
pub async fn probe_ethercat() -> Option<String> {
|
||||
let interfaces = match get_interfaces() {
|
||||
Ok(x) => x,
|
||||
Err(_) => return None,
|
||||
};
|
||||
|
||||
interfaces
|
||||
.iter()
|
||||
.map(|iface| iface.name.to_owned())
|
||||
.map(probe_ethercat_interface)
|
||||
.join_all()
|
||||
.await
|
||||
.into_iter()
|
||||
.find_map(|x| x.ok())
|
||||
}
|
||||
|
||||
async fn probe_ethercat_interface(interface: String) -> Result<String> {
|
||||
tracing::info!("Testing ethercat on interface: {}", interface);
|
||||
|
||||
let pdu_storage = Box::leak(Box::new(PduStorage::<
|
||||
IFACE_DISCOVERY_MAX_FRAMES,
|
||||
IFACE_DISCOVERY_MAX_PDU_DATA,
|
||||
>::new()));
|
||||
|
||||
let (tx, rx, pdu_loop) = pdu_storage
|
||||
.try_split()
|
||||
.expect("Failed to split PDU storage");
|
||||
|
||||
let tx_rx_handle =
|
||||
smol::spawn(tx_rx_task(&interface, tx, rx).context("Failed to spawn TX/RX task")?);
|
||||
|
||||
let maindevice = MainDevice::new(
|
||||
pdu_loop,
|
||||
Timeouts::default(),
|
||||
MainDeviceConfig {
|
||||
// Default 10000
|
||||
dc_static_sync_iterations: 10000,
|
||||
// Default None
|
||||
retry_behaviour: RetryBehaviour::Count(3),
|
||||
},
|
||||
);
|
||||
|
||||
let res = maindevice
|
||||
.init_single_group::<IFACE_DISCOVERY_MAX_SUBDEVICES, IFACE_DISCOVERY_MAX_PDI_LEN>(
|
||||
ethercat_now,
|
||||
)
|
||||
.await
|
||||
.context(format!(
|
||||
"Failed to initialize EtherCAT on interface {}",
|
||||
interface
|
||||
));
|
||||
|
||||
tx_rx_handle.cancel().await;
|
||||
|
||||
res?;
|
||||
Ok(interface)
|
||||
}
|
||||
|
||||
pub async fn discover_ethercat_interface() -> Result<String, anyhow::Error> {
|
||||
tracing::info!("Discovering EtherCAT interface...");
|
||||
|
||||
// Set up a custom panic hook that suppresses panic output
|
||||
let default_hook = std::panic::take_hook();
|
||||
std::panic::set_hook(Box::new(move |panic_info| {
|
||||
// Optional: log the panic in a controlled way
|
||||
if let Some(location) = panic_info.location() {
|
||||
tracing::debug!(
|
||||
"Suppressed panic in interface test: {} (at {}:{})",
|
||||
panic_info,
|
||||
location.file(),
|
||||
location.line()
|
||||
);
|
||||
} else {
|
||||
tracing::debug!("Suppressed panic in interface test: {}", panic_info);
|
||||
}
|
||||
}));
|
||||
|
||||
let mut interfaces = get_interfaces()?;
|
||||
|
||||
interfaces.sort_by(|a, b| a.name.cmp(&b.name));
|
||||
let mut interface: Option<&str> = None;
|
||||
|
||||
for i in 0..interfaces.len() {
|
||||
match probe_ethercat_interface(interfaces[i].name.to_string()).await {
|
||||
Ok(_) => {
|
||||
// if interface found with ethercat Exit early, we expect only one interface with ethercat
|
||||
interface = Some(&interfaces[i].name);
|
||||
break;
|
||||
}
|
||||
Err(_) => (),
|
||||
}
|
||||
}
|
||||
|
||||
for i in 0..interfaces.len() {
|
||||
if interface.is_some() && interface.unwrap() == &interfaces[i].name {
|
||||
set_interface_managed(&interfaces[i].name, false);
|
||||
} else {
|
||||
set_interface_managed(&interfaces[i].name, true);
|
||||
}
|
||||
}
|
||||
|
||||
// Restore the default panic hook
|
||||
std::panic::set_hook(default_hook);
|
||||
|
||||
match interface {
|
||||
Some(interface) => Ok(interface.to_string()),
|
||||
None => Err(anyhow::anyhow!("No suitable EtherCAT interface found")),
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
use std::process::Command;
|
||||
|
||||
use anyhow::Result;
|
||||
use interfaces::Interface;
|
||||
|
||||
pub mod ethercat_interface_discovery;
|
||||
pub mod modbus_tcp_discovery;
|
||||
|
||||
/// Returns true if the given network interface is Ethernet.
|
||||
/// Prevents testing wlan,loopback and other non ethernet devices
|
||||
fn is_ethernet(interface: &str) -> std::io::Result<bool> {
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
let base_path = format!("/sys/class/net/{}", interface);
|
||||
let type_path = std::path::Path::new(&base_path).join("type");
|
||||
let iface_type = std::fs::read_to_string(&type_path)?.trim().to_string();
|
||||
// 1 means Ethernet
|
||||
let reports_as_ethernet = iface_type == "1";
|
||||
// Double-check that it's not a wireless interface
|
||||
let uevent_path = std::path::Path::new(&base_path).join("uevent");
|
||||
let uevent = std::fs::read_to_string(&uevent_path).unwrap_or_default();
|
||||
// If "DEVTYPE=wlan" appears, it's lying
|
||||
let actually_wifi = uevent.contains("DEVTYPE=wlan");
|
||||
Ok(reports_as_ethernet && !actually_wifi)
|
||||
}
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
return Ok(true);
|
||||
}
|
||||
|
||||
pub fn get_interfaces() -> Result<Vec<Interface>> {
|
||||
let vec = Interface::get_all()
|
||||
.map_err(|e| anyhow::anyhow!("Failed to get network interfaces: {}", e))?
|
||||
.into_iter()
|
||||
.filter(|iface| {
|
||||
iface.is_up()
|
||||
&& iface.is_running()
|
||||
&& !iface.is_loopback()
|
||||
&& is_ethernet(&iface.name).unwrap_or(false)
|
||||
&& !iface.name.starts_with("bridge")
|
||||
&& !iface.name.starts_with("utun") // Exclude tunnel interfaces
|
||||
&& !iface.name.starts_with("awdl") // Exclude Apple Wireless Direct Link
|
||||
&& !iface.name.starts_with("anpi") // Exclude Apple Network Privacy Interface
|
||||
&& !iface.name.starts_with("llw") // Exclude Apple low-latency WLAN
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
Ok(vec)
|
||||
}
|
||||
|
||||
/// Sets a network interface to unmanaged by NetworkManager.
|
||||
/// Returns true if the command succeeded.
|
||||
pub fn set_interface_managed(interface: &str, managed: bool) -> bool {
|
||||
let managed_str = match managed {
|
||||
true => "yes",
|
||||
false => "no",
|
||||
};
|
||||
tracing::info!(
|
||||
"set_interface_managed for {} managed was set to: {}",
|
||||
interface,
|
||||
managed_str
|
||||
);
|
||||
let status = Command::new("nmcli")
|
||||
.args(["dev", "set", interface, "managed", managed_str])
|
||||
.status();
|
||||
|
||||
matches!(status, Ok(s) if s.success())
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
use crate::ethernet::get_interfaces;
|
||||
use crate::{futures::FutureIteratorExt, modbus::tcp::ModbusTcpDevice};
|
||||
use anyhow::{Result, bail};
|
||||
use interfaces::Interface;
|
||||
use std::cmp::min;
|
||||
use std::net::{IpAddr, Ipv4Addr, SocketAddr};
|
||||
|
||||
pub struct ModbusTcpProbe {
|
||||
pub addr: SocketAddr,
|
||||
pub serial: u16,
|
||||
}
|
||||
|
||||
pub async fn probe_modbus_tcp() -> Vec<ModbusTcpProbe> {
|
||||
let interfaces = match get_interfaces() {
|
||||
Ok(x) => x,
|
||||
Err(_) => return vec![],
|
||||
};
|
||||
|
||||
interfaces
|
||||
.into_iter()
|
||||
.map(probe_modbus_tcp_addresses)
|
||||
.join_all()
|
||||
.await
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.collect()
|
||||
}
|
||||
|
||||
async fn probe_modbus_tcp_addresses(interface: Interface) -> Vec<ModbusTcpProbe> {
|
||||
let out: Vec<ModbusTcpProbe> = interface
|
||||
.addresses
|
||||
.iter()
|
||||
.filter_map(|addr| {
|
||||
let a = addr.addr.map(|a| a.ip());
|
||||
let m = addr.mask.map(|a| a.ip());
|
||||
|
||||
match (a, m) {
|
||||
(Some(IpAddr::V4(addr)), Some(IpAddr::V4(mask))) => Some((addr, mask)),
|
||||
_ => None,
|
||||
}
|
||||
})
|
||||
.flat_map(|(addr, mask)| {
|
||||
let a = u32::from(addr);
|
||||
let m = u32::from(mask);
|
||||
let network = a & m;
|
||||
|
||||
let prefix = min(24, m.count_ones()); // mask → prefix length
|
||||
let size = 1u32 << (32 - prefix); // number of addresses
|
||||
|
||||
(200..size).map(move |i| SocketAddr::new(Ipv4Addr::from(network + i).into(), 502))
|
||||
})
|
||||
.map(|addr| smol::spawn(ping_modbus_device(addr)))
|
||||
.join_all()
|
||||
.await
|
||||
.into_iter()
|
||||
.filter_map(|x| x.ok())
|
||||
.collect();
|
||||
|
||||
out
|
||||
}
|
||||
|
||||
async fn ping_modbus_device(addr: SocketAddr) -> Result<ModbusTcpProbe> {
|
||||
let mut device = ModbusTcpDevice::new(addr).await?;
|
||||
|
||||
let module_number1 = device.get_u32(0x2).await?;
|
||||
let module_number2 = device.get_u32(0x4).await?;
|
||||
|
||||
if module_number1 != 0x2787_2144 || module_number2 != 0x0000_0000 {
|
||||
bail!("Unknown modbus tcp device!");
|
||||
}
|
||||
|
||||
let serial = device.get_u32(0x000A).await?;
|
||||
|
||||
Ok(ModbusTcpProbe {
|
||||
addr,
|
||||
serial: serial as u16,
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
use core::future::Future;
|
||||
|
||||
// Extension trait for iterators of futures
|
||||
pub trait FutureIteratorExt<F>: Iterator<Item = F> + Sized
|
||||
where
|
||||
F: Future,
|
||||
{
|
||||
/// Await all futures and return their outputs
|
||||
fn join_all(self) -> impl Future<Output = Vec<F::Output>>;
|
||||
}
|
||||
|
||||
impl<I, F> FutureIteratorExt<F> for I
|
||||
where
|
||||
I: Iterator<Item = F> + Sized,
|
||||
F: Future,
|
||||
{
|
||||
async fn join_all(self) -> Vec<F::Output> {
|
||||
let mut out = Vec::new();
|
||||
|
||||
for fut in self {
|
||||
out.push(fut.await);
|
||||
}
|
||||
|
||||
out
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,8 @@
|
||||
pub mod controllers;
|
||||
pub mod converters;
|
||||
pub mod downcast;
|
||||
pub mod ethercat;
|
||||
pub mod ethernet;
|
||||
pub mod futures;
|
||||
pub mod helpers;
|
||||
pub mod irq_handling;
|
||||
pub mod modbus;
|
||||
|
||||
@@ -5,6 +5,8 @@ use crc::{CRC_16_MODBUS, Crc};
|
||||
use serialport::SerialPort;
|
||||
use std::time::Duration;
|
||||
|
||||
pub mod tcp;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum ParityType {
|
||||
Even,
|
||||
|
||||
@@ -0,0 +1,274 @@
|
||||
use std::fmt::Debug;
|
||||
use std::io::ErrorKind;
|
||||
use std::net::SocketAddr;
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::Context;
|
||||
use anyhow::Result;
|
||||
use anyhow::bail;
|
||||
use smol::Timer;
|
||||
use smol::future::FutureExt;
|
||||
use smol::io;
|
||||
use smol::io::AsyncReadExt;
|
||||
use smol::io::AsyncWriteExt;
|
||||
use smol::net::TcpStream;
|
||||
|
||||
const PROTOCOL_ID: u16 = 0;
|
||||
const UNIT_ID: u8 = 0;
|
||||
const READ_HOLDING_FUNCTION_CODE: u8 = 3;
|
||||
const WRITE_HOLDING_FUNCTION_CODE: u8 = 16;
|
||||
const READ_HOLDING_LENGTH: u16 = 6; // Unit ID 00 + Func Code 03 + Start Address XXXX + Quantity XXXX = 6 bytes
|
||||
const WRITE_HOLDING_LENGTH_WITHOUT_DATA: u16 = 7; // Unit ID 00 + Func Code 16 + Start Address XXXX + Quantity XXXX + Num Data Bytes XX = 7 bytes
|
||||
|
||||
struct Packet {
|
||||
buf: Vec<u8>,
|
||||
}
|
||||
|
||||
impl Packet {
|
||||
pub const fn new() -> Self {
|
||||
Self { buf: Vec::new() }
|
||||
}
|
||||
|
||||
pub fn add_u8(&mut self, x: u8) {
|
||||
self.buf.push(x);
|
||||
}
|
||||
|
||||
pub fn add_u16(&mut self, x: u16) {
|
||||
let buf = x.to_be_bytes();
|
||||
self.buf.push(buf[0]);
|
||||
self.buf.push(buf[1]);
|
||||
}
|
||||
|
||||
pub fn as_bytes(&self) -> &[u8] {
|
||||
&self.buf
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ModbusTcpDevice {
|
||||
stream: TcpStream,
|
||||
transactions: u16,
|
||||
}
|
||||
|
||||
impl Debug for ModbusTcpDevice {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self.stream.peer_addr() {
|
||||
Ok(addr) => write!(f, "ModbusTcpDevice({:?})", addr),
|
||||
Err(_) => std::fmt::Result::Err(std::fmt::Error),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ModbusTcpDevice {
|
||||
pub async fn new(addr: SocketAddr) -> Result<Self> {
|
||||
let timeout = async {
|
||||
Timer::after(Duration::from_millis(10)).await;
|
||||
Err(io::Error::from(ErrorKind::TimedOut))
|
||||
};
|
||||
|
||||
let stream = TcpStream::connect(addr)
|
||||
.or(timeout)
|
||||
.await
|
||||
.context("Could not connect to modbus device!")?;
|
||||
|
||||
Ok(Self {
|
||||
stream,
|
||||
transactions: 0,
|
||||
})
|
||||
}
|
||||
|
||||
async fn read_and_check_responce_header(&mut self, func_code: u8) -> Result<()> {
|
||||
assert_eq!(
|
||||
self.read_u16().await?,
|
||||
self.transactions,
|
||||
"Modbus device sent unexpected transaction id!"
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
self.read_u16().await?,
|
||||
PROTOCOL_ID,
|
||||
"Modbus device sent unexpected protocol id!"
|
||||
);
|
||||
|
||||
let length = self.read_u16().await?;
|
||||
assert!(
|
||||
length >= 2,
|
||||
"Modbus device will send too few bytes to understand the response!"
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
self.read_u8().await?,
|
||||
UNIT_ID,
|
||||
"Modbus device sent unexpected unit id!"
|
||||
);
|
||||
|
||||
let func_code_res = self.read_u8().await?;
|
||||
if func_code != func_code_res {
|
||||
let error_code = self.read_u8().await?;
|
||||
bail!(
|
||||
"Modbus device sent unexpected function code 0x{:x}! It probably encountered an error. Error code: 0x{:x}",
|
||||
func_code_res,
|
||||
error_code
|
||||
);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn send_read_holding_request(&mut self, addr: u16, count: u16) -> Result<u16> {
|
||||
let mut packet = Packet::new();
|
||||
|
||||
self.transactions += 1;
|
||||
packet.add_u16(self.transactions);
|
||||
packet.add_u16(PROTOCOL_ID);
|
||||
packet.add_u16(READ_HOLDING_LENGTH);
|
||||
|
||||
packet.add_u8(UNIT_ID);
|
||||
packet.add_u8(READ_HOLDING_FUNCTION_CODE);
|
||||
|
||||
packet.add_u16(addr);
|
||||
packet.add_u16(count);
|
||||
|
||||
self.send(packet).await?;
|
||||
self.read_and_check_responce_header(READ_HOLDING_FUNCTION_CODE)
|
||||
.await?;
|
||||
|
||||
let num_data_bytes = self.read_u8().await? as u16;
|
||||
assert_eq!(
|
||||
count * 2,
|
||||
num_data_bytes,
|
||||
"Modbus device wants to send only a portion of the requested range - UNIMPLEMENTED!"
|
||||
);
|
||||
|
||||
Ok(num_data_bytes)
|
||||
}
|
||||
|
||||
pub async fn get_holding_registers(&mut self, addr: u16, count: u16) -> Result<Vec<u16>> {
|
||||
self.send_read_holding_request(addr, count).await?;
|
||||
|
||||
let mut res: Vec<u16> = Vec::new();
|
||||
while res.len() < count as usize {
|
||||
res.push(self.read_u16().await?);
|
||||
}
|
||||
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
pub async fn set_holding_registers(&mut self, addr: u16, values: &[u16]) -> Result<()> {
|
||||
let count = values.len();
|
||||
let num_bytes = count * 2;
|
||||
assert!(
|
||||
count <= 128,
|
||||
"Cannot send that many bytes to modbus device in one go! Trying to send {} bytes of data.",
|
||||
num_bytes
|
||||
);
|
||||
|
||||
let mut packet = Packet::new();
|
||||
|
||||
self.transactions += 1;
|
||||
packet.add_u16(self.transactions);
|
||||
packet.add_u16(PROTOCOL_ID);
|
||||
packet.add_u16(WRITE_HOLDING_LENGTH_WITHOUT_DATA + num_bytes as u16);
|
||||
|
||||
packet.add_u8(UNIT_ID);
|
||||
packet.add_u8(WRITE_HOLDING_FUNCTION_CODE);
|
||||
|
||||
packet.add_u16(addr);
|
||||
packet.add_u16(count as u16);
|
||||
|
||||
packet.add_u8(num_bytes as u8);
|
||||
|
||||
for value in values {
|
||||
packet.add_u16(value.to_owned());
|
||||
}
|
||||
|
||||
self.send(packet).await?;
|
||||
self.read_and_check_responce_header(WRITE_HOLDING_FUNCTION_CODE)
|
||||
.await?;
|
||||
|
||||
assert_eq!(
|
||||
self.read_u16().await?,
|
||||
addr,
|
||||
"Modbus device wrote to wrong register address!"
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
self.read_u16().await?,
|
||||
count as u16,
|
||||
"Modbus device wrote wrong number of registers!"
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn get_string<const N: usize>(&mut self, addr: u16) -> Result<String> {
|
||||
assert!(N % 2 == 0, "Strings are always of even length!");
|
||||
assert!(N <= 256, "Cannot read long strings in a single swoop!");
|
||||
let count = N / 2;
|
||||
|
||||
self.send_read_holding_request(addr, count as u16).await?;
|
||||
let buf = self.read_bytes::<N>().await?;
|
||||
|
||||
let s = String::from_utf8_lossy(&buf);
|
||||
let s = match s.split_once('\0') {
|
||||
None => s.to_string(),
|
||||
Some((l, _)) => l.to_string(),
|
||||
};
|
||||
|
||||
Ok(s)
|
||||
}
|
||||
|
||||
pub async fn set_string<const N: usize>(&mut self, _addr: u16, s: &str) -> Result<()> {
|
||||
assert!(s.len() <= N, "String is too long to fit!");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn get_u16(&mut self, addr: u16) -> Result<u16> {
|
||||
let length = self.send_read_holding_request(addr, 1).await?;
|
||||
assert_eq!(
|
||||
length, 2,
|
||||
"Expected modbus device to send exactly 2 bytes of data!"
|
||||
);
|
||||
|
||||
self.read_u16().await
|
||||
}
|
||||
|
||||
pub async fn get_u32(&mut self, addr: u16) -> Result<u32> {
|
||||
let length = self.send_read_holding_request(addr, 2).await?;
|
||||
assert_eq!(
|
||||
length, 4,
|
||||
"Expected modbus device to send exactly 4 bytes of data!"
|
||||
);
|
||||
|
||||
let buf = self.read_bytes::<4>().await?;
|
||||
Ok(u32::from_be_bytes(buf))
|
||||
}
|
||||
|
||||
// We have to send the request in a single packet,
|
||||
// otherwise the device may fail to respond correctly.
|
||||
async fn send(&mut self, packet: Packet) -> Result<()> {
|
||||
self.stream
|
||||
.write_all(packet.as_bytes())
|
||||
.await
|
||||
.context("Could write to modbus device!")
|
||||
}
|
||||
|
||||
async fn read_u8(&mut self) -> Result<u8> {
|
||||
let buf = self.read_bytes::<1>().await?;
|
||||
Ok(u8::from_be_bytes(buf))
|
||||
}
|
||||
|
||||
async fn read_u16(&mut self) -> Result<u16> {
|
||||
let buf = self.read_bytes::<2>().await?;
|
||||
Ok(u16::from_be_bytes(buf))
|
||||
}
|
||||
|
||||
async fn read_bytes<const N: usize>(&mut self) -> Result<[u8; N]> {
|
||||
let mut buf = [0; N];
|
||||
self.stream
|
||||
.read_exact(&mut buf)
|
||||
.await
|
||||
.context("Could not read from modbus device!")?;
|
||||
Ok(buf)
|
||||
}
|
||||
}
|
||||
@@ -28,10 +28,9 @@ export function getUnitIcon(unit: Unit): IconName {
|
||||
case "Hz":
|
||||
return "lu:AudioWaveform";
|
||||
case "W":
|
||||
return "lu:Zap";
|
||||
case "V":
|
||||
return "lu:Zap";
|
||||
case "A":
|
||||
case "mA":
|
||||
return "lu:Zap";
|
||||
case "kWh":
|
||||
return "lu:BatteryFull";
|
||||
@@ -82,6 +81,8 @@ export function renderUnitSymbol(unit: Unit | undefined): string {
|
||||
return "W";
|
||||
case "V":
|
||||
return "V";
|
||||
case "mA":
|
||||
return "mA";
|
||||
case "A":
|
||||
return "A";
|
||||
case "kWh":
|
||||
@@ -155,6 +156,8 @@ export function renderUnitSymbolLong(unit: Unit): string {
|
||||
return "watts";
|
||||
case "V":
|
||||
return "volts";
|
||||
case "mA":
|
||||
return "milliamperes";
|
||||
case "A":
|
||||
return "amperes";
|
||||
case "kWh":
|
||||
@@ -192,6 +195,7 @@ export const units = [
|
||||
"Hz",
|
||||
"W",
|
||||
"V",
|
||||
"mA",
|
||||
"A",
|
||||
"kWh",
|
||||
"l/min",
|
||||
|
||||
@@ -462,6 +462,17 @@ export const testmachine: MachineProperties = {
|
||||
},
|
||||
],
|
||||
};
|
||||
export const wagoPower1: MachineProperties = {
|
||||
name: "WAGO Power",
|
||||
version: "V1",
|
||||
slug: "wago_power1",
|
||||
icon: "lu:PlugZap",
|
||||
machine_identification: {
|
||||
vendor: VENDOR_QITECH,
|
||||
machine: 0x000a,
|
||||
},
|
||||
device_roles: [],
|
||||
};
|
||||
|
||||
export const analogInputTestMachine: MachineProperties = {
|
||||
name: "AnalogTest",
|
||||
@@ -529,6 +540,7 @@ export const machineProperties: MachineProperties[] = [
|
||||
laser1,
|
||||
mock1,
|
||||
buffer1,
|
||||
wagoPower1,
|
||||
aquapath1,
|
||||
testmachine,
|
||||
analogInputTestMachine,
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
import { Page } from "@/components/Page";
|
||||
import React from "react";
|
||||
import { useWagoPower1 } from "./useWagoPower1";
|
||||
import { ControlGrid } from "@/control/ControlGrid";
|
||||
import { ControlCard } from "@/control/ControlCard";
|
||||
import { TimeSeriesValueNumeric } from "@/control/TimeSeriesValue";
|
||||
import { SelectionGroup } from "@/control/SelectionGroup";
|
||||
import { Mode } from "./wagoPower1Namespace";
|
||||
|
||||
export function WagoPower1ControlPage() {
|
||||
const {
|
||||
state,
|
||||
|
||||
voltage,
|
||||
current,
|
||||
|
||||
setMode,
|
||||
|
||||
isLoading,
|
||||
isDisabled,
|
||||
} = useWagoPower1();
|
||||
|
||||
return (
|
||||
<Page>
|
||||
<ControlGrid columns={2}>
|
||||
<ControlCard title="Power">
|
||||
<TimeSeriesValueNumeric
|
||||
label="Voltage"
|
||||
unit="V"
|
||||
timeseries={voltage}
|
||||
renderValue={(value) => value.toFixed(2)}
|
||||
/>
|
||||
<TimeSeriesValueNumeric
|
||||
label="Current"
|
||||
unit="mA"
|
||||
timeseries={current}
|
||||
renderValue={(value) => value.toFixed(2)}
|
||||
/>
|
||||
</ControlCard>
|
||||
<ControlCard title="Mode">
|
||||
<SelectionGroup<Mode>
|
||||
value={state?.mode}
|
||||
disabled={isDisabled}
|
||||
loading={isLoading}
|
||||
onChange={setMode}
|
||||
orientation="vertical"
|
||||
className="grid h-full grid-cols-2 gap-2"
|
||||
options={{
|
||||
Off: {
|
||||
children: "Off",
|
||||
icon: "lu:PowerOff",
|
||||
isActiveClassName: "bg-gray-600",
|
||||
className: "h-full",
|
||||
},
|
||||
On24V: {
|
||||
children: "On",
|
||||
icon: "lu:Power",
|
||||
isActiveClassName: "bg-green-600",
|
||||
className: "h-full",
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</ControlCard>
|
||||
</ControlGrid>
|
||||
</Page>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import { Topbar } from "@/components/Topbar";
|
||||
import { wagoPower1SerialRoute } from "@/routes/routes";
|
||||
import React from "react";
|
||||
|
||||
export function WagoPower1Page() {
|
||||
const { serial } = wagoPower1SerialRoute.useParams();
|
||||
return (
|
||||
<Topbar
|
||||
pathname={`/_sidebar/machines/wago_power1/${serial}`}
|
||||
items={[
|
||||
{
|
||||
link: "control",
|
||||
activeLink: "control",
|
||||
title: "Control",
|
||||
icon: "lu:PlugZap",
|
||||
},
|
||||
]}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
import { toastError } from "@/components/Toast";
|
||||
import { wagoPower1 } from "@/machines/properties";
|
||||
import { MachineIdentificationUnique } from "@/machines/types";
|
||||
import { wagoPower1SerialRoute } from "@/routes/routes";
|
||||
import { useEffect, useMemo } from "react";
|
||||
import {
|
||||
Mode,
|
||||
modeSchema,
|
||||
StateEvent,
|
||||
useWagoPower1Namespace,
|
||||
} from "./wagoPower1Namespace";
|
||||
import { useStateOptimistic } from "@/lib/useStateOptimistic";
|
||||
import { produce } from "immer";
|
||||
import z from "zod";
|
||||
import { useMachineMutate } from "@/client/useClient";
|
||||
|
||||
export function useWagoPower1() {
|
||||
const { serial: serialString } = wagoPower1SerialRoute.useParams();
|
||||
|
||||
// Memoize the machine identification to keep it stable between renders
|
||||
const machineIdentification: MachineIdentificationUnique = useMemo(() => {
|
||||
const serial = parseInt(serialString);
|
||||
|
||||
if (isNaN(serial)) {
|
||||
toastError(
|
||||
"Invalid Serial Number",
|
||||
`"${serialString}" is not a valid serial number.`,
|
||||
);
|
||||
|
||||
return {
|
||||
machine_identification: {
|
||||
vendor: 0,
|
||||
machine: 0,
|
||||
},
|
||||
serial: 0,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
machine_identification: wagoPower1.machine_identification,
|
||||
serial,
|
||||
};
|
||||
}, [serialString]);
|
||||
|
||||
const { request: requestModeSet } = useMachineMutate(
|
||||
z.object({ SetMode: modeSchema }),
|
||||
);
|
||||
|
||||
const { state, defaultState, voltage, current } = useWagoPower1Namespace(
|
||||
machineIdentification,
|
||||
);
|
||||
//
|
||||
// Single optimistic state for all state management
|
||||
const stateOptimistic = useStateOptimistic<StateEvent>();
|
||||
|
||||
// Update optimistic state when real state changes
|
||||
useEffect(() => {
|
||||
if (state) {
|
||||
stateOptimistic.setReal(state);
|
||||
}
|
||||
}, [state]);
|
||||
|
||||
// Helper function for optimistic updates using produce
|
||||
const updateStateOptimistically = (
|
||||
producer: (current: StateEvent) => void,
|
||||
serverRequest: () => void,
|
||||
) => {
|
||||
const currentState = stateOptimistic.value;
|
||||
if (currentState && !stateOptimistic.isOptimistic) {
|
||||
stateOptimistic.setOptimistic(produce(currentState, producer));
|
||||
}
|
||||
serverRequest();
|
||||
};
|
||||
|
||||
const setMode = (mode: Mode) => {
|
||||
updateStateOptimistically(
|
||||
(current) => {
|
||||
current.data.mode = mode;
|
||||
},
|
||||
() =>
|
||||
requestModeSet({
|
||||
machine_identification_unique: machineIdentification,
|
||||
data: { SetMode: mode },
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
// Calculate loading states
|
||||
const isLoading = stateOptimistic.isOptimistic;
|
||||
const isDisabled = !stateOptimistic.isInitialized;
|
||||
|
||||
return {
|
||||
state: stateOptimistic.value?.data,
|
||||
defaultState: defaultState?.data,
|
||||
|
||||
current,
|
||||
voltage,
|
||||
|
||||
setMode,
|
||||
|
||||
isLoading,
|
||||
isDisabled,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,149 @@
|
||||
import { StoreApi } from "zustand";
|
||||
import { create } from "zustand";
|
||||
import { z } from "zod";
|
||||
import {
|
||||
EventHandler,
|
||||
eventSchema,
|
||||
Event,
|
||||
handleUnhandledEventError,
|
||||
NamespaceId,
|
||||
createNamespaceHookImplementation,
|
||||
ThrottledStoreUpdater,
|
||||
} from "../../../client/socketioStore";
|
||||
import { MachineIdentificationUnique } from "@/machines/types";
|
||||
import { useMemo } from "react";
|
||||
import {
|
||||
createTimeSeries,
|
||||
TimeSeries,
|
||||
TimeSeriesValue,
|
||||
} from "@/lib/timeseries";
|
||||
|
||||
export const liveValuesEventSchema = eventSchema(
|
||||
z.object({
|
||||
voltage: z.number(),
|
||||
current: z.number(),
|
||||
}),
|
||||
);
|
||||
|
||||
export type LiveValuesEvent = z.infer<typeof liveValuesEventSchema>;
|
||||
|
||||
export const modeSchema = z.enum(["Off", "On24V"]);
|
||||
export type Mode = z.infer<typeof modeSchema>;
|
||||
|
||||
export const stateEventSchema = eventSchema(
|
||||
z.object({
|
||||
mode: modeSchema,
|
||||
is_default_state: z.boolean(),
|
||||
}),
|
||||
);
|
||||
|
||||
export type StateEvent = z.infer<typeof stateEventSchema>;
|
||||
|
||||
export type WagoPower1NamespaceStore = {
|
||||
state: StateEvent | null;
|
||||
defaultState: StateEvent | null;
|
||||
|
||||
current: TimeSeries;
|
||||
voltage: TimeSeries;
|
||||
};
|
||||
|
||||
const TWENTY_MILLISECOND = 20;
|
||||
const ONE_SECOND = 1000;
|
||||
const FIVE_SECOND = 5 * ONE_SECOND;
|
||||
const ONE_HOUR = 60 * 60 * ONE_SECOND;
|
||||
const { initialTimeSeries: voltage, insert: addVoltage } = createTimeSeries(
|
||||
TWENTY_MILLISECOND,
|
||||
ONE_SECOND,
|
||||
FIVE_SECOND,
|
||||
ONE_HOUR,
|
||||
);
|
||||
const { initialTimeSeries: current, insert: addCurrent } = createTimeSeries(
|
||||
TWENTY_MILLISECOND,
|
||||
ONE_SECOND,
|
||||
FIVE_SECOND,
|
||||
ONE_HOUR,
|
||||
);
|
||||
|
||||
const createWagoPower1NamespaceStore = (): StoreApi<WagoPower1NamespaceStore> =>
|
||||
create<WagoPower1NamespaceStore>(() => {
|
||||
return {
|
||||
state: null,
|
||||
defaultState: null,
|
||||
voltage,
|
||||
current,
|
||||
};
|
||||
});
|
||||
|
||||
function wagoPower1MessageHandler(
|
||||
store: StoreApi<WagoPower1NamespaceStore>,
|
||||
throttledUpdater: ThrottledStoreUpdater<WagoPower1NamespaceStore>,
|
||||
): EventHandler {
|
||||
return (event: Event<any>) => {
|
||||
const eventName = event.name;
|
||||
|
||||
// Helper function to update store through buffer
|
||||
const updateStore = (
|
||||
updater: (state: WagoPower1NamespaceStore) => WagoPower1NamespaceStore,
|
||||
) => {
|
||||
throttledUpdater.updateWith(updater);
|
||||
};
|
||||
|
||||
try {
|
||||
if (eventName === "StateEvent") {
|
||||
const stateEvent = stateEventSchema.parse(event);
|
||||
|
||||
updateStore((state) => ({
|
||||
...state,
|
||||
state: stateEvent,
|
||||
// only set default state if is_default_state is true
|
||||
defaultState: stateEvent.data.is_default_state
|
||||
? stateEvent
|
||||
: state.defaultState,
|
||||
}));
|
||||
} else if (eventName === "LiveValuesEvent") {
|
||||
console.log(event);
|
||||
const liveValues = liveValuesEventSchema.parse(event);
|
||||
const { voltage, current } = liveValues.data;
|
||||
const voltageValue: TimeSeriesValue = {
|
||||
value: voltage,
|
||||
timestamp: liveValues.ts,
|
||||
};
|
||||
const currentValue: TimeSeriesValue = {
|
||||
value: current,
|
||||
timestamp: liveValues.ts,
|
||||
};
|
||||
updateStore((store: WagoPower1NamespaceStore) => ({
|
||||
...store,
|
||||
voltage: addVoltage(store.voltage, voltageValue),
|
||||
current: addCurrent(store.current, currentValue),
|
||||
}));
|
||||
} else {
|
||||
handleUnhandledEventError(eventName);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const useWagoPower1NamespaceImplementation =
|
||||
createNamespaceHookImplementation<WagoPower1NamespaceStore>({
|
||||
createStore: createWagoPower1NamespaceStore,
|
||||
createEventHandler: wagoPower1MessageHandler,
|
||||
});
|
||||
|
||||
export function useWagoPower1Namespace(
|
||||
machine_identification_unique: MachineIdentificationUnique,
|
||||
): WagoPower1NamespaceStore {
|
||||
// Generate namespace ID from validated machine ID
|
||||
const namespaceId = useMemo<NamespaceId>(
|
||||
() => ({
|
||||
type: "machine",
|
||||
machine_identification_unique,
|
||||
}),
|
||||
[machine_identification_unique],
|
||||
);
|
||||
|
||||
// Use the implementation with validated namespace ID
|
||||
return useWagoPower1NamespaceImplementation(namespaceId);
|
||||
}
|
||||
@@ -1,8 +1,3 @@
|
||||
/**
|
||||
* @file winder2Namespace.ts
|
||||
* @description TypeScript implementation of Winder2 namespace with Zod schema validation.
|
||||
*/
|
||||
|
||||
import { StoreApi } from "zustand";
|
||||
import { create } from "zustand";
|
||||
import { z } from "zod";
|
||||
|
||||
@@ -42,6 +42,7 @@ import { Buffer1SettingsPage } from "@/machines/buffer/buffer1/Buffer1Settings";
|
||||
import { Laser1ControlPage } from "@/machines/laser/laser1/Laser1ControlPage";
|
||||
import { Laser1GraphsPage } from "@/machines/laser/laser1/Laser1Graph";
|
||||
import { Laser1Page } from "@/machines/laser/laser1/Laser1Page";
|
||||
import { Laser1PresetsPage } from "@/machines/laser/laser1/Laser1PresetsPage";
|
||||
|
||||
import { Mock1ControlPage } from "@/machines/mock/mock1/Mock1ControlPage";
|
||||
import { Mock1GraphPage } from "@/machines/mock/mock1/Mock1Graph";
|
||||
@@ -55,7 +56,7 @@ import { Aquapath1GraphPage } from "@/machines/aquapath/aquapath1/Aquapath1Graph
|
||||
|
||||
import { TestMachinePage } from "@/machines/testmachine/TestMachinePage";
|
||||
import { TestMachineControlPage } from "@/machines/testmachine/TestMachineControlPage";
|
||||
import { Laser1PresetsPage } from "@/machines/laser/laser1/Laser1PresetsPage";
|
||||
|
||||
import { AnalogInputTestMachine } from "@/machines/analoginputtestmachine/AnalogInputTestMachinePage";
|
||||
import { AnalogInputTestMachineControl } from "@/machines/analoginputtestmachine/AnalogInputTestMachineControlPage";
|
||||
import { IP20TestMachinePage } from "@/machines/ip20testmachine/IP20TestMachinePage";
|
||||
@@ -64,6 +65,9 @@ import { IP20TestMachineControlPage } from "@/machines/ip20testmachine/IP20TestM
|
||||
import { MetricsGraphsPage } from "@/metrics/MetricsGraphsPage";
|
||||
import { MetricsControlPage } from "@/metrics/MetricsControlPage";
|
||||
|
||||
import { WagoPower1Page } from "@/machines/wago_power/wago_power1/WagoPower1Page";
|
||||
import { WagoPower1ControlPage } from "@/machines/wago_power/wago_power1/WagoPower1ControlPage";
|
||||
|
||||
// make a route tree like this
|
||||
// _mainNavigation/machines/winder2/$serial/control
|
||||
// _mainNavigation/configuration/a
|
||||
@@ -317,6 +321,18 @@ export const buffer1SettingsRoute = createRoute({
|
||||
component: () => <Buffer1SettingsPage />,
|
||||
});
|
||||
|
||||
export const wagoPower1SerialRoute = createRoute({
|
||||
getParentRoute: () => machinesRoute,
|
||||
path: "wago_power1/$serial",
|
||||
component: () => <WagoPower1Page />,
|
||||
});
|
||||
|
||||
export const wagoPower1ControlRoute = createRoute({
|
||||
getParentRoute: () => wagoPower1SerialRoute,
|
||||
path: "control",
|
||||
component: () => <WagoPower1ControlPage />,
|
||||
});
|
||||
|
||||
export const setupRoute = createRoute({
|
||||
getParentRoute: () => sidebarRoute,
|
||||
path: "setup",
|
||||
@@ -462,6 +478,8 @@ export const rootTree = RootRoute.addChildren([
|
||||
mock1PresetsRoute,
|
||||
]),
|
||||
|
||||
wagoPower1SerialRoute.addChildren([wagoPower1ControlRoute]),
|
||||
|
||||
buffer1SerialRoute.addChildren([buffer1ControlRoute]),
|
||||
]),
|
||||
]),
|
||||
|
||||
@@ -8,13 +8,13 @@ use ethercat_hal::{
|
||||
use smol::{block_on, channel::unbounded};
|
||||
|
||||
use crate::{
|
||||
MachineNewHardware, MachineNewParams, MachineNewTrait,
|
||||
EtherCATMachine, EtherCATParams, MachineNewHardware,
|
||||
analog_input_test_machine::{AnalogInputTestMachine, api::AnalogInputTestMachineNamespace},
|
||||
get_ethercat_device, validate_no_role_dublicates, validate_same_machine_identification_unique,
|
||||
};
|
||||
|
||||
impl MachineNewTrait for AnalogInputTestMachine {
|
||||
fn new<'maindevice>(params: &MachineNewParams) -> Result<Self, Error> {
|
||||
impl EtherCATMachine for AnalogInputTestMachine {
|
||||
fn new<'maindevice>(params: &EtherCATParams) -> Result<Self, Error> {
|
||||
// validate general stuff
|
||||
let device_identification = params
|
||||
.device_group
|
||||
@@ -28,7 +28,7 @@ impl MachineNewTrait for AnalogInputTestMachine {
|
||||
MachineNewHardware::Ethercat(x) => x,
|
||||
_ => {
|
||||
return Err(anyhow::anyhow!(
|
||||
"[{}::MachineNewTrait/TestMachine::new] MachineNewHardware is not Ethercat",
|
||||
"[{}::EtherCATMachine/TestMachine::new] MachineNewHardware is not Ethercat",
|
||||
module_path!()
|
||||
));
|
||||
}
|
||||
@@ -50,8 +50,8 @@ impl MachineNewTrait for AnalogInputTestMachine {
|
||||
api_receiver: receiver,
|
||||
api_sender: sender,
|
||||
machine_identification_unique: params.get_machine_identification_unique(),
|
||||
main_sender: params.main_thread_channel.clone(),
|
||||
namespace: namespace,
|
||||
main_sender: params.main_sender.clone(),
|
||||
namespace,
|
||||
|
||||
last_measurement: Instant::now(),
|
||||
measurement_rate_hz: 1.0,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use crate::{
|
||||
MachineNewHardware, MachineNewParams, MachineNewTrait, get_ethercat_device,
|
||||
EtherCATMachine, EtherCATParams, MachineNewHardware, get_ethercat_device,
|
||||
get_subdevice_by_index, validate_no_role_dublicates,
|
||||
validate_same_machine_identification_unique,
|
||||
};
|
||||
@@ -28,8 +28,8 @@ use ethercat_hal::{
|
||||
use std::time::{Duration, Instant};
|
||||
use units::thermodynamic_temperature::{ThermodynamicTemperature, degree_celsius};
|
||||
|
||||
impl MachineNewTrait for AquaPathV1 {
|
||||
fn new<'maindevice>(params: &MachineNewParams) -> Result<Self, Error> {
|
||||
impl EtherCATMachine for AquaPathV1 {
|
||||
fn new<'maindevice>(params: &EtherCATParams) -> Result<Self, Error> {
|
||||
// validate general stuff
|
||||
let device_identification = params
|
||||
.device_group
|
||||
@@ -162,7 +162,7 @@ impl MachineNewTrait for AquaPathV1 {
|
||||
);
|
||||
let (sender, receiver) = smol::channel::unbounded();
|
||||
let mut water_cooling = Self {
|
||||
main_sender: params.main_thread_channel.clone(),
|
||||
main_sender: params.main_sender.clone(),
|
||||
api_receiver: receiver,
|
||||
api_sender: sender,
|
||||
machine_identification_unique: params.get_machine_identification_unique(),
|
||||
|
||||
@@ -18,16 +18,16 @@ use ethercat_hal::{
|
||||
};
|
||||
|
||||
use crate::{
|
||||
MachineNewHardware, MachineNewHardwareEthercat, MachineNewParams, MachineNewTrait,
|
||||
EtherCATMachine, EtherCATParams, MachineNewHardware, MachineNewHardwareEthercat,
|
||||
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};
|
||||
|
||||
impl MachineNewTrait for BufferV1 {
|
||||
impl EtherCATMachine for BufferV1 {
|
||||
fn new<'maindevice>(
|
||||
params: &MachineNewParams<'maindevice, '_, '_, '_, '_, '_, '_>,
|
||||
params: &EtherCATParams<'maindevice, '_, '_, '_, '_, '_, '_>,
|
||||
) -> Result<Self, Error> {
|
||||
// validate general stuff
|
||||
let device_identification = params.device_group.to_vec();
|
||||
@@ -128,7 +128,7 @@ impl MachineNewTrait for BufferV1 {
|
||||
|
||||
// create buffer instance
|
||||
let mut buffer: BufferV1 = Self {
|
||||
main_sender: params.main_thread_channel.clone(),
|
||||
main_sender: params.main_sender.clone(),
|
||||
api_receiver: receiver,
|
||||
api_sender: sender,
|
||||
namespace: Buffer1Namespace {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use crate::{
|
||||
MachineNewHardware, MachineNewParams, MachineNewTrait,
|
||||
EtherCATMachine, EtherCATParams, MachineNewHardware,
|
||||
extruder1::{
|
||||
ExtruderV2Mode,
|
||||
api::{
|
||||
@@ -12,8 +12,8 @@ use crate::{
|
||||
},
|
||||
};
|
||||
|
||||
impl MachineNewTrait for ExtruderV2 {
|
||||
fn new(params: &MachineNewParams<'_, '_, '_, '_, '_, '_, '_>) -> Result<Self, anyhow::Error>
|
||||
impl EtherCATMachine for ExtruderV2 {
|
||||
fn new(params: &EtherCATParams<'_, '_, '_, '_, '_, '_, '_>) -> Result<Self, anyhow::Error>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
@@ -35,7 +35,7 @@ impl MachineNewTrait for ExtruderV2 {
|
||||
let (sender, receiver) = smol::channel::unbounded();
|
||||
|
||||
let mut extruder_mock_machine = Self {
|
||||
main_sender: params.main_thread_channel.clone(),
|
||||
main_sender: params.main_sender.clone(),
|
||||
api_receiver: receiver,
|
||||
api_sender: sender,
|
||||
machine_identification_unique: params.get_machine_identification_unique(),
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#[cfg(not(feature = "mock-machine"))]
|
||||
use crate::{MachineNewParams, MachineNewTrait, get_ethercat_device};
|
||||
use crate::{EtherCATMachine, EtherCATParams, get_ethercat_device};
|
||||
|
||||
#[cfg(not(feature = "mock-machine"))]
|
||||
use anyhow::Error;
|
||||
@@ -56,8 +56,8 @@ use super::{
|
||||
};
|
||||
|
||||
#[cfg(not(feature = "mock-machine"))]
|
||||
impl MachineNewTrait for ExtruderV2 {
|
||||
fn new<'maindevice>(params: &MachineNewParams) -> Result<Self, Error> {
|
||||
impl EtherCATMachine for ExtruderV2 {
|
||||
fn new<'maindevice>(params: &EtherCATParams) -> Result<Self, Error> {
|
||||
// validate general stuff
|
||||
use crate::{
|
||||
MachineNewHardware, MachineNewHardwareEthercat, validate_no_role_dublicates,
|
||||
@@ -224,7 +224,7 @@ impl MachineNewTrait for ExtruderV2 {
|
||||
let (sender, receiver) = smol::channel::unbounded();
|
||||
|
||||
let mut extruder: ExtruderV2 = Self {
|
||||
main_sender: params.main_thread_channel.clone(),
|
||||
main_sender: params.main_sender.clone(),
|
||||
api_receiver: receiver,
|
||||
api_sender: sender,
|
||||
machine_identification_unique: params.get_machine_identification_unique(),
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use crate::{
|
||||
MachineNewHardware, MachineNewParams, MachineNewTrait,
|
||||
EtherCATMachine, EtherCATParams, MachineNewHardware,
|
||||
extruder1::{
|
||||
ExtruderV2Mode,
|
||||
api::{
|
||||
@@ -12,8 +12,8 @@ use crate::{
|
||||
extruder2::mock::ExtruderV2,
|
||||
};
|
||||
|
||||
impl MachineNewTrait for ExtruderV2 {
|
||||
fn new(params: &MachineNewParams<'_, '_, '_, '_, '_, '_, '_>) -> Result<Self, anyhow::Error>
|
||||
impl EtherCATMachine for ExtruderV2 {
|
||||
fn new(params: &EtherCATParams<'_, '_, '_, '_, '_, '_, '_>) -> Result<Self, anyhow::Error>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
@@ -35,7 +35,7 @@ impl MachineNewTrait for ExtruderV2 {
|
||||
let (sender, receiver) = smol::channel::unbounded();
|
||||
|
||||
let mut extruder_mock_machine = Self {
|
||||
main_sender: params.main_thread_channel.clone(),
|
||||
main_sender: params.main_sender.clone(),
|
||||
api_receiver: receiver,
|
||||
api_sender: sender,
|
||||
machine_identification_unique: params.get_machine_identification_unique(),
|
||||
|
||||
@@ -31,10 +31,15 @@ use crate::{
|
||||
machine_identification::{MachineIdentification, MachineIdentificationUnique},
|
||||
};
|
||||
|
||||
#[cfg(not(feature = "mock-machine"))]
|
||||
pub mod act;
|
||||
#[cfg(not(feature = "mock-machine"))]
|
||||
pub mod api;
|
||||
#[cfg(not(feature = "mock-machine"))]
|
||||
pub mod emit;
|
||||
#[cfg(feature = "mock-machine")]
|
||||
pub mod mock;
|
||||
#[cfg(not(feature = "mock-machine"))]
|
||||
pub mod new;
|
||||
pub mod temperature_controller;
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#[cfg(not(feature = "mock-machine"))]
|
||||
use crate::{MachineNewParams, MachineNewTrait, get_ethercat_device};
|
||||
use crate::{EtherCATMachine, EtherCATParams, get_ethercat_device};
|
||||
|
||||
#[cfg(not(feature = "mock-machine"))]
|
||||
use anyhow::Error;
|
||||
@@ -51,8 +51,8 @@ use crate::extruder1::temperature_controller::TemperatureController;
|
||||
use super::ExtruderV3;
|
||||
|
||||
#[cfg(not(feature = "mock-machine"))]
|
||||
impl MachineNewTrait for ExtruderV3 {
|
||||
fn new<'maindevice>(params: &MachineNewParams) -> Result<Self, Error> {
|
||||
impl EtherCATMachine for ExtruderV3 {
|
||||
fn new<'maindevice>(params: &EtherCATParams) -> Result<Self, Error> {
|
||||
// validate general stuff
|
||||
|
||||
use crate::{
|
||||
@@ -223,7 +223,7 @@ impl MachineNewTrait for ExtruderV3 {
|
||||
let (sender, receiver) = smol::channel::unbounded();
|
||||
|
||||
let mut extruder: ExtruderV3 = Self {
|
||||
main_sender: params.main_thread_channel.clone(),
|
||||
main_sender: params.main_sender.clone(),
|
||||
api_receiver: receiver,
|
||||
api_sender: sender,
|
||||
machine_identification_unique: params.get_machine_identification_unique(),
|
||||
|
||||
@@ -4,7 +4,7 @@ use smol::block_on;
|
||||
use std::time::Instant;
|
||||
|
||||
use crate::{
|
||||
MachineNewHardware, MachineNewParams, MachineNewTrait, get_ethercat_device,
|
||||
EtherCATMachine, EtherCATParams, MachineNewHardware, get_ethercat_device,
|
||||
validate_no_role_dublicates, validate_same_machine_identification_unique,
|
||||
};
|
||||
|
||||
@@ -15,8 +15,8 @@ use ethercat_hal::devices::wago_modules::ip20_ec_di8_do8::{
|
||||
use ethercat_hal::io::digital_input::DigitalInput;
|
||||
use ethercat_hal::io::digital_output::DigitalOutput;
|
||||
|
||||
impl MachineNewTrait for IP20TestMachine {
|
||||
fn new<'maindevice>(params: &MachineNewParams) -> Result<Self, Error> {
|
||||
impl EtherCATMachine for IP20TestMachine {
|
||||
fn new<'maindevice>(params: &EtherCATParams) -> Result<Self, Error> {
|
||||
// validate general stuff
|
||||
let device_identification = params
|
||||
.device_group
|
||||
@@ -30,7 +30,7 @@ impl MachineNewTrait for IP20TestMachine {
|
||||
MachineNewHardware::Ethercat(x) => x,
|
||||
_ => {
|
||||
return Err(anyhow::anyhow!(
|
||||
"[{}::MachineNewTrait/IP20TestMachine::new] MachineNewHardware is not Ethercat",
|
||||
"[{}::EtherCATMachine/IP20TestMachine::new] MachineNewHardware is not Ethercat",
|
||||
module_path!()
|
||||
));
|
||||
}
|
||||
@@ -78,7 +78,7 @@ impl MachineNewTrait for IP20TestMachine {
|
||||
last_live_values_emit: Instant::now(),
|
||||
outputs: [false; 8],
|
||||
inputs: [false; 8],
|
||||
main_sender: params.main_thread_channel.clone(),
|
||||
main_sender: params.main_sender.clone(),
|
||||
douts: [do1, do2, do3, do4, do5, do6, do7, do8],
|
||||
dins: [di1, di2, di3, di4, di5, di6, di7, di8],
|
||||
};
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
use std::time::Instant;
|
||||
|
||||
use crate::serial::{devices::laser::Laser, registry::SERIAL_DEVICE_REGISTRY};
|
||||
use crate::{MachineNewHardware, MachineNewTrait};
|
||||
use crate::{EtherCATMachine, MachineNewHardware};
|
||||
|
||||
use super::{LaserMachine, LaserTarget, api::LaserMachineNamespace};
|
||||
use anyhow::Error;
|
||||
use units::ConstZero;
|
||||
use units::length::{Length, millimeter};
|
||||
|
||||
impl MachineNewTrait for LaserMachine {
|
||||
fn new(params: &crate::MachineNewParams<'_, '_, '_, '_, '_, '_, '_>) -> Result<Self, Error>
|
||||
impl EtherCATMachine for LaserMachine {
|
||||
fn new(params: &crate::EtherCATParams<'_, '_, '_, '_, '_, '_, '_>) -> Result<Self, Error>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
@@ -35,7 +35,7 @@ impl MachineNewTrait for LaserMachine {
|
||||
let (sender, receiver) = smol::channel::unbounded();
|
||||
|
||||
let laser_machine = Self {
|
||||
main_sender: params.main_thread_channel.clone(),
|
||||
main_sender: params.main_sender.clone(),
|
||||
api_receiver: receiver,
|
||||
api_sender: sender,
|
||||
machine_identification_unique: params.get_machine_identification_unique(),
|
||||
|
||||
+137
-12
@@ -1,6 +1,6 @@
|
||||
use anyhow::Error;
|
||||
use anyhow::{Error, Result};
|
||||
use control_core::socketio::event::GenericEvent;
|
||||
use control_core::socketio::namespace::Namespace;
|
||||
use control_core::socketio::namespace::{CacheableEvents, Namespace, NamespaceCacheingLogic};
|
||||
use ethercat_hal::devices::{
|
||||
EthercatDevice, SubDeviceIdentityTuple, downcast_device, subdevice_identity_to_tuple,
|
||||
};
|
||||
@@ -11,7 +11,7 @@ use machine_identification::{
|
||||
DeviceIdentificationIdentified, MachineIdentificationUnique,
|
||||
};
|
||||
use serde::Serialize;
|
||||
use smol::channel::Sender;
|
||||
use smol::channel::{Receiver, Sender};
|
||||
use socketioxide::extract::SocketRef;
|
||||
use std::fmt::Debug;
|
||||
use std::{any::Any, sync::Arc, time::Instant};
|
||||
@@ -28,6 +28,7 @@ pub mod mock;
|
||||
pub mod registry;
|
||||
pub mod serial;
|
||||
pub mod test_machine;
|
||||
pub mod wago_power;
|
||||
pub mod winder2;
|
||||
|
||||
pub const VENDOR_QITECH: u16 = 0x0001;
|
||||
@@ -35,9 +36,10 @@ 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;
|
||||
pub const MACHINE_AQUAPATH_V1: u16 = 0x0009;
|
||||
pub const MACHINE_WAGO_POWER_V1: u16 = 0x000A;
|
||||
pub const MACHINE_EXTRUDER_V2: u16 = 0x0016;
|
||||
pub const TEST_MACHINE: u16 = 0x0033;
|
||||
pub const IP20_TEST_MACHINE: u16 = 0x0034;
|
||||
@@ -63,7 +65,7 @@ pub enum AsyncThreadMessage {
|
||||
DisconnectMachines(CrossConnection),
|
||||
}
|
||||
|
||||
pub struct MachineNewParams<
|
||||
pub struct EtherCATParams<
|
||||
'maindevice,
|
||||
'subdevices,
|
||||
'device_identifications_identified,
|
||||
@@ -86,11 +88,11 @@ pub struct MachineNewParams<
|
||||
'machine_new_hardware_serial,
|
||||
>,
|
||||
pub socket_queue_tx: Sender<(SocketRef, Arc<GenericEvent>)>,
|
||||
pub main_thread_channel: Option<Sender<AsyncThreadMessage>>,
|
||||
pub main_sender: Option<Sender<AsyncThreadMessage>>,
|
||||
pub namespace: Option<Namespace>,
|
||||
}
|
||||
|
||||
impl MachineNewParams<'_, '_, '_, '_, '_, '_, '_> {
|
||||
impl EtherCATParams<'_, '_, '_, '_, '_, '_, '_> {
|
||||
pub fn get_machine_identification_unique(&self) -> MachineIdentificationUnique {
|
||||
self.device_group
|
||||
.first()
|
||||
@@ -257,8 +259,8 @@ pub fn get_ethercat_device_by_index<'maindevice>(
|
||||
.clone())
|
||||
}
|
||||
|
||||
pub trait MachineNewTrait {
|
||||
fn new(params: &MachineNewParams<'_, '_, '_, '_, '_, '_, '_>) -> Result<Self, anyhow::Error>
|
||||
pub trait EtherCATMachine: Machine {
|
||||
fn new(params: &EtherCATParams<'_, '_, '_, '_, '_, '_, '_>) -> Result<Self, anyhow::Error>
|
||||
where
|
||||
Self: Sized;
|
||||
}
|
||||
@@ -293,7 +295,7 @@ pub trait MachineApi {
|
||||
fn api_event_namespace(&mut self) -> Option<Namespace>;
|
||||
}
|
||||
|
||||
pub trait Machine: MachineAct + MachineNewTrait + MachineApi + Any + Debug + Send + Sync {
|
||||
pub trait Machine: MachineAct + MachineApi + Any + Debug + Send + Sync {
|
||||
fn get_machine_identification_unique(&self) -> MachineIdentificationUnique;
|
||||
fn get_main_sender(&self) -> Option<Sender<AsyncThreadMessage>>;
|
||||
}
|
||||
@@ -312,7 +314,7 @@ async fn get_device_ident<
|
||||
'machine_new_hardware_serial,
|
||||
'machine_new_hardware,
|
||||
>(
|
||||
params: &MachineNewParams<
|
||||
params: &EtherCATParams<
|
||||
'maindevice,
|
||||
'subdevices,
|
||||
'device_identifications_identified,
|
||||
@@ -351,7 +353,7 @@ async fn get_ethercat_device<
|
||||
T,
|
||||
>(
|
||||
hardware: &&MachineNewHardwareEthercat<'maindevice, 'subdevices, 'ethercat_devices>,
|
||||
params: &MachineNewParams<
|
||||
params: &EtherCATParams<
|
||||
'maindevice,
|
||||
'subdevices,
|
||||
'device_identifications_identified,
|
||||
@@ -406,3 +408,126 @@ where
|
||||
|
||||
Ok((device, subdevice))
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct MachineChannel {
|
||||
api_receiver: Receiver<MachineMessage>,
|
||||
api_sender: Sender<MachineMessage>,
|
||||
machine_identification_unique: MachineIdentificationUnique,
|
||||
main_sender: Option<Sender<AsyncThreadMessage>>,
|
||||
namespace: Option<Namespace>,
|
||||
}
|
||||
|
||||
impl MachineChannel {
|
||||
pub fn new(machine_identification_unique: MachineIdentificationUnique) -> Self {
|
||||
let (sender, receiver) = smol::channel::unbounded();
|
||||
|
||||
Self {
|
||||
api_sender: sender,
|
||||
api_receiver: receiver,
|
||||
machine_identification_unique,
|
||||
main_sender: None,
|
||||
namespace: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<E> NamespaceCacheingLogic<E> for MachineChannel
|
||||
where
|
||||
E: CacheableEvents<E>,
|
||||
{
|
||||
fn emit(&mut self, events: E) {
|
||||
let event = Arc::new(events.event_value());
|
||||
let cache_fn = events.event_cache_fn();
|
||||
|
||||
if let Some(ns) = &mut self.namespace {
|
||||
ns.emit(event, &cache_fn);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub trait MachineWithChannel: Send + Debug + Sync {
|
||||
fn get_machine_channel(&self) -> &MachineChannel;
|
||||
fn get_machine_channel_mut(&mut self) -> &mut MachineChannel;
|
||||
|
||||
fn on_namespace(&mut self) {}
|
||||
|
||||
fn update(&mut self, now: std::time::Instant) -> Result<()>;
|
||||
fn mutate(&mut self, value: Value) -> Result<()>;
|
||||
}
|
||||
|
||||
impl<C> MachineApi for C
|
||||
where
|
||||
C: MachineWithChannel,
|
||||
{
|
||||
fn api_get_sender(&self) -> Sender<MachineMessage> {
|
||||
self.get_machine_channel().api_sender.clone()
|
||||
}
|
||||
|
||||
fn api_mutate(&mut self, value: Value) -> Result<()> {
|
||||
let res = self.mutate(value);
|
||||
|
||||
if let Err(ref e) = res {
|
||||
tracing::error!("Machine errored while mutating: {}, ", e);
|
||||
}
|
||||
|
||||
res
|
||||
}
|
||||
|
||||
fn api_event_namespace(&mut self) -> Option<Namespace> {
|
||||
self.get_machine_channel().namespace.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl<C> MachineAct for C
|
||||
where
|
||||
C: MachineWithChannel,
|
||||
{
|
||||
fn act(&mut self, now: Instant) {
|
||||
while let Ok(msg) = self.get_machine_channel_mut().api_receiver.try_recv() {
|
||||
self.act_machine_message(msg);
|
||||
}
|
||||
|
||||
if let Err(e) = self.update(now) {
|
||||
tracing::error!("Machine errored while updating: {}, ", e);
|
||||
}
|
||||
}
|
||||
|
||||
fn act_machine_message(&mut self, msg: MachineMessage) {
|
||||
let channel = self.get_machine_channel_mut();
|
||||
|
||||
match msg {
|
||||
MachineMessage::SubscribeNamespace(namespace) => {
|
||||
channel.namespace = Some(namespace);
|
||||
self.on_namespace();
|
||||
}
|
||||
MachineMessage::UnsubscribeNamespace => {
|
||||
channel.namespace = None;
|
||||
}
|
||||
MachineMessage::HttpApiJsonRequest(value) => {
|
||||
let _ = self.api_mutate(value);
|
||||
}
|
||||
MachineMessage::ConnectToMachine(_machine_connection) => {
|
||||
todo!();
|
||||
}
|
||||
MachineMessage::DisconnectMachine(_machine_connection) => {
|
||||
todo!();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<C> Machine for C
|
||||
where
|
||||
C: MachineWithChannel + 'static,
|
||||
{
|
||||
fn get_machine_identification_unique(&self) -> MachineIdentificationUnique {
|
||||
self.get_machine_channel()
|
||||
.machine_identification_unique
|
||||
.clone()
|
||||
}
|
||||
|
||||
fn get_main_sender(&self) -> Option<Sender<AsyncThreadMessage>> {
|
||||
self.get_machine_channel().main_sender.clone()
|
||||
}
|
||||
}
|
||||
@@ -4,14 +4,14 @@ use super::{
|
||||
MockMachine,
|
||||
api::{MockMachineNamespace, Mode},
|
||||
};
|
||||
use crate::{MachineNewHardware, MachineNewParams, MachineNewTrait};
|
||||
use crate::{EtherCATMachine, EtherCATParams, MachineNewHardware};
|
||||
use anyhow::Error;
|
||||
use units::f64::Frequency;
|
||||
use units::frequency::hertz;
|
||||
|
||||
impl MachineNewTrait for MockMachine {
|
||||
impl EtherCATMachine for MockMachine {
|
||||
fn new<'maindevice, 'subdevices>(
|
||||
params: &MachineNewParams<'maindevice, 'subdevices, '_, '_, '_, '_, '_>,
|
||||
params: &EtherCATParams<'maindevice, 'subdevices, '_, '_, '_, '_, '_>,
|
||||
) -> Result<Self, Error>
|
||||
where
|
||||
Self: Sized,
|
||||
@@ -33,7 +33,7 @@ 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(),
|
||||
main_sender: params.main_sender.clone(),
|
||||
api_receiver: receiver,
|
||||
api_sender: sender,
|
||||
machine_identification_unique: params.get_machine_identification_unique(),
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
use crate::analog_input_test_machine::AnalogInputTestMachine;
|
||||
use crate::ip20_test_machine::IP20TestMachine;
|
||||
#[cfg(feature = "mock-machine")]
|
||||
use crate::{
|
||||
extruder1::mock::ExtruderV2 as ExtruderV2Mock1, extruder2::mock::ExtruderV2 as ExtruderV2Mock2,
|
||||
@@ -5,8 +7,7 @@ use crate::{
|
||||
};
|
||||
|
||||
use crate::{
|
||||
Machine, MachineNewParams, analog_input_test_machine::AnalogInputTestMachine,
|
||||
ip20_test_machine::IP20TestMachine, machine_identification::MachineIdentification,
|
||||
EtherCATMachine, EtherCATParams, Machine, machine_identification::MachineIdentification,
|
||||
};
|
||||
|
||||
#[cfg(not(feature = "mock-machine"))]
|
||||
@@ -25,7 +26,7 @@ use anyhow::Error;
|
||||
use std::{any::TypeId, collections::HashMap};
|
||||
|
||||
pub type MachineNewClosure =
|
||||
Box<dyn Fn(&MachineNewParams) -> Result<Box<dyn Machine>, Error> + Send + Sync>;
|
||||
Box<dyn Fn(&EtherCATParams) -> Result<Box<dyn Machine>, Error> + Send + Sync>;
|
||||
|
||||
pub struct MachineRegistry {
|
||||
type_map: HashMap<TypeId, (MachineIdentification, MachineNewClosure)>,
|
||||
@@ -44,7 +45,7 @@ impl MachineRegistry {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn register<T: Machine + 'static>(
|
||||
pub fn register<T: EtherCATMachine + 'static>(
|
||||
&mut self,
|
||||
machine_identficiation: MachineIdentification,
|
||||
) {
|
||||
@@ -60,7 +61,7 @@ impl MachineRegistry {
|
||||
|
||||
pub fn new_machine(
|
||||
&self,
|
||||
machine_new_params: &MachineNewParams,
|
||||
machine_new_params: &EtherCATParams,
|
||||
) -> Result<Box<dyn Machine>, anyhow::Error> {
|
||||
// get machiine identification
|
||||
let device_identification =
|
||||
@@ -122,7 +123,9 @@ lazy_static! {
|
||||
mc.register::<AquaPathV1>(AquaPathV1::MACHINE_IDENTIFICATION);
|
||||
|
||||
mc.register::<TestMachine>(TestMachine::MACHINE_IDENTIFICATION);
|
||||
|
||||
mc.register::<IP20TestMachine>(IP20TestMachine::MACHINE_IDENTIFICATION);
|
||||
|
||||
mc.register::<AnalogInputTestMachine>(AnalogInputTestMachine::MACHINE_IDENTIFICATION);
|
||||
|
||||
mc
|
||||
|
||||
@@ -4,7 +4,7 @@ use smol::block_on;
|
||||
use std::time::Instant;
|
||||
|
||||
use crate::{
|
||||
MachineNewHardware, MachineNewParams, MachineNewTrait, get_ethercat_device,
|
||||
EtherCATMachine, EtherCATParams, MachineNewHardware, get_ethercat_device,
|
||||
validate_no_role_dublicates, validate_same_machine_identification_unique,
|
||||
};
|
||||
|
||||
@@ -20,8 +20,8 @@ use smol::lock::RwLock;
|
||||
use std::sync::Arc;
|
||||
*/
|
||||
|
||||
impl MachineNewTrait for TestMachine {
|
||||
fn new<'maindevice>(params: &MachineNewParams) -> Result<Self, Error> {
|
||||
impl EtherCATMachine for TestMachine {
|
||||
fn new<'maindevice>(params: &EtherCATParams) -> Result<Self, Error> {
|
||||
// validate general stuff
|
||||
let device_identification = params
|
||||
.device_group
|
||||
@@ -89,7 +89,7 @@ impl MachineNewTrait for TestMachine {
|
||||
},
|
||||
last_state_emit: Instant::now(),
|
||||
led_on: [false; 4],
|
||||
main_sender: params.main_thread_channel.clone(),
|
||||
main_sender: params.main_sender.clone(),
|
||||
douts: [do1, do2, do3, do4],
|
||||
};
|
||||
my_test.emit_state();
|
||||
|
||||
@@ -0,0 +1,226 @@
|
||||
use crate::{MachineChannel, MachineWithChannel};
|
||||
use anyhow::Result;
|
||||
use control_core::{
|
||||
modbus::tcp::ModbusTcpDevice,
|
||||
socketio::{
|
||||
event::{BuildEvent, GenericEvent},
|
||||
namespace::{
|
||||
CacheFn, CacheableEvents, NamespaceCacheingLogic, cache_duration,
|
||||
cache_first_and_last_event,
|
||||
},
|
||||
},
|
||||
};
|
||||
use control_core_derive::BuildEvent;
|
||||
use serde::*;
|
||||
use smol::lock::Mutex;
|
||||
use std::{
|
||||
net::SocketAddr,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
use units::{
|
||||
electric_current::milliampere,
|
||||
electric_potential::{millivolt, volt},
|
||||
*,
|
||||
};
|
||||
|
||||
const MODBUS_DC_OFF: u16 = 0;
|
||||
const MODBUS_DC_ON: u16 = 1;
|
||||
const MODBUS_HICCUP_POWER: u16 = 1 << 8;
|
||||
|
||||
#[derive(Serialize, Debug, Clone, BuildEvent)]
|
||||
pub struct LiveValuesEvent {
|
||||
voltage: f64,
|
||||
current: f64,
|
||||
}
|
||||
|
||||
impl CacheableEvents<Self> for LiveValuesEvent {
|
||||
fn event_value(&self) -> GenericEvent {
|
||||
self.build().into()
|
||||
}
|
||||
|
||||
fn event_cache_fn(&self) -> CacheFn {
|
||||
cache_duration(Duration::from_secs(60 * 60), Duration::from_secs(1))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub enum Mode {
|
||||
Off,
|
||||
On24V,
|
||||
}
|
||||
|
||||
impl Mode {
|
||||
pub fn as_u16(&self) -> u16 {
|
||||
match self {
|
||||
Mode::Off => MODBUS_DC_OFF,
|
||||
Mode::On24V => MODBUS_HICCUP_POWER | MODBUS_DC_ON,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Debug, Clone, BuildEvent)]
|
||||
pub struct StateEvent {
|
||||
mode: Mode,
|
||||
is_default_state: bool,
|
||||
}
|
||||
|
||||
impl CacheableEvents<Self> for StateEvent {
|
||||
fn event_value(&self) -> GenericEvent {
|
||||
self.build().into()
|
||||
}
|
||||
|
||||
fn event_cache_fn(&self) -> CacheFn {
|
||||
cache_first_and_last_event()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize)]
|
||||
pub enum Mutation {
|
||||
SetMode(Mode),
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct WagoPower {
|
||||
mode: Mode,
|
||||
channel: MachineChannel,
|
||||
#[cfg(not(feature = "mock-machine"))]
|
||||
device: Mutex<ModbusTcpDevice>,
|
||||
last_emit: Instant,
|
||||
emitted_default_state: bool,
|
||||
}
|
||||
|
||||
impl WagoPower {
|
||||
pub async fn new(
|
||||
channel: MachineChannel,
|
||||
#[cfg(not(feature = "mock-machine"))] addr: SocketAddr,
|
||||
) -> Result<Self> {
|
||||
Ok(Self {
|
||||
mode: Mode::Off,
|
||||
channel,
|
||||
#[cfg(not(feature = "mock-machine"))]
|
||||
device: Mutex::new(ModbusTcpDevice::new(addr).await?),
|
||||
last_emit: Instant::now(),
|
||||
emitted_default_state: false,
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(feature = "mock-machine")]
|
||||
fn get_live_values(&mut self) -> Result<LiveValuesEvent> {
|
||||
match self.mode {
|
||||
Mode::Off => Ok(LiveValuesEvent {
|
||||
voltage: 0.0,
|
||||
current: 0.0,
|
||||
}),
|
||||
Mode::On24V => Ok(LiveValuesEvent {
|
||||
voltage: 24.0,
|
||||
current: 5000.0,
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "mock-machine"))]
|
||||
fn get_live_values(&mut self) -> Result<LiveValuesEvent> {
|
||||
let electric = smol::block_on(async {
|
||||
let mut dev = self.device.lock().await;
|
||||
dev.get_holding_registers(0x0500, 2).await
|
||||
})?;
|
||||
|
||||
let voltage = ElectricPotential::new::<millivolt>(f64::from(electric[0]));
|
||||
let current = ElectricCurrent::new::<milliampere>(f64::from(electric[1]));
|
||||
|
||||
Ok(LiveValuesEvent {
|
||||
voltage: voltage.get::<volt>(),
|
||||
current: current.get::<milliampere>(),
|
||||
})
|
||||
}
|
||||
|
||||
fn emit_state(&mut self) {
|
||||
let event = StateEvent {
|
||||
mode: self.mode.clone(),
|
||||
is_default_state: !self.emitted_default_state,
|
||||
};
|
||||
self.channel.emit(event);
|
||||
}
|
||||
|
||||
fn set_mode(&mut self, mode: Mode) -> Result<()> {
|
||||
self.mode = mode;
|
||||
|
||||
#[cfg(not(feature = "mock-machine"))]
|
||||
smol::block_on(self.transmit_voltage())?;
|
||||
|
||||
self.emit_state();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "mock-machine"))]
|
||||
async fn transmit_voltage(&mut self) -> Result<()> {
|
||||
let mut dev = self.device.lock().await;
|
||||
|
||||
let voltage = 24000;
|
||||
let warning_threshold = 5000; // For now
|
||||
let control_bits = self.mode.as_u16();
|
||||
let delay_ms = 100; // For now
|
||||
|
||||
dev.set_holding_registers(
|
||||
0x0088,
|
||||
&[voltage, warning_threshold, control_bits, delay_ms],
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(feature = "mock-machine")]
|
||||
pub async fn get_serial(&mut self) -> Result<u16> {
|
||||
Ok(0xbeef)
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "mock-machine"))]
|
||||
pub async fn get_serial(&mut self) -> Result<u16> {
|
||||
let mut dev = self.device.lock().await;
|
||||
dev.get_u16(0x000B).await
|
||||
}
|
||||
}
|
||||
|
||||
impl MachineWithChannel for WagoPower {
|
||||
fn get_machine_channel(&self) -> &MachineChannel {
|
||||
&self.channel
|
||||
}
|
||||
|
||||
fn get_machine_channel_mut(&mut self) -> &mut MachineChannel {
|
||||
&mut self.channel
|
||||
}
|
||||
|
||||
fn mutate(&mut self, value: serde_json::Value) -> Result<()> {
|
||||
let mutation: Mutation = serde_json::from_value(value)?;
|
||||
|
||||
match mutation {
|
||||
Mutation::SetMode(mode) => self.set_mode(mode)?,
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn on_namespace(&mut self) {
|
||||
self.emit_state();
|
||||
}
|
||||
|
||||
fn update(&mut self, now: Instant) -> Result<()> {
|
||||
if !self.emitted_default_state {
|
||||
self.set_mode(Mode::Off)?;
|
||||
|
||||
self.emit_state();
|
||||
self.emitted_default_state = true;
|
||||
}
|
||||
|
||||
if now.duration_since(self.last_emit) > Duration::from_secs_f64(1.0 / 3.0) {
|
||||
if let Ok(event) = self.get_live_values() {
|
||||
self.channel.emit(event);
|
||||
}
|
||||
|
||||
self.last_emit = now;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -4,8 +4,8 @@ use super::Winder2;
|
||||
use crate::{MachineAct, MachineMessage};
|
||||
#[cfg(not(feature = "mock-machine"))]
|
||||
use std::time::{Duration, Instant};
|
||||
#[cfg(not(feature = "mock-machine"))]
|
||||
|
||||
#[cfg(not(feature = "mock-machine"))]
|
||||
impl MachineAct for Winder2 {
|
||||
fn act(&mut self, now: Instant) {
|
||||
let machine_message = self.api_receiver.try_recv();
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use crate::{
|
||||
MachineNewParams, MachineNewTrait,
|
||||
EtherCATMachine, EtherCATParams,
|
||||
winder2::api::{
|
||||
ModeState, PullerState, SpoolAutomaticActionState, SpoolSpeedControllerState,
|
||||
TensionArmState, TraverseState, Winder2Namespace,
|
||||
@@ -8,8 +8,8 @@ use crate::{
|
||||
|
||||
use super::Winder2;
|
||||
|
||||
impl MachineNewTrait for Winder2 {
|
||||
fn new(params: &MachineNewParams<'_, '_, '_, '_, '_, '_, '_>) -> Result<Self, anyhow::Error>
|
||||
impl EtherCATMachine for Winder2 {
|
||||
fn new(params: &EtherCATParams<'_, '_, '_, '_, '_, '_, '_>) -> Result<Self, anyhow::Error>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
@@ -31,7 +31,7 @@ impl MachineNewTrait for Winder2 {
|
||||
|
||||
let (sender, receiver) = smol::channel::unbounded();
|
||||
let mut winder_mock_machine = Self {
|
||||
main_sender: params.main_thread_channel.clone(),
|
||||
main_sender: params.main_sender.clone(),
|
||||
max_connected_machines: 2,
|
||||
api_receiver: receiver,
|
||||
api_sender: sender,
|
||||
|
||||
@@ -7,7 +7,7 @@ mod winder2_imports {
|
||||
pub use crate::winder2::spool_speed_controller::SpoolSpeedController;
|
||||
pub use crate::winder2::traverse_controller::TraverseController;
|
||||
pub use crate::{
|
||||
MachineNewHardware, MachineNewParams, MachineNewTrait, validate_no_role_dublicates,
|
||||
EtherCATMachine, EtherCATParams, MachineNewHardware, validate_no_role_dublicates,
|
||||
validate_same_machine_identification_unique,
|
||||
};
|
||||
pub use anyhow::Error;
|
||||
@@ -53,8 +53,8 @@ pub use winder2_imports::*;
|
||||
use crate::get_ethercat_device;
|
||||
|
||||
#[cfg(not(feature = "mock-machine"))]
|
||||
impl MachineNewTrait for Winder2 {
|
||||
fn new<'maindevice>(params: &MachineNewParams) -> Result<Self, Error> {
|
||||
impl EtherCATMachine for Winder2 {
|
||||
fn new<'maindevice>(params: &EtherCATParams) -> Result<Self, Error> {
|
||||
// validate general stuff
|
||||
|
||||
let device_identification = params.device_group.to_vec();
|
||||
@@ -198,7 +198,7 @@ impl MachineNewTrait for Winder2 {
|
||||
.clone();
|
||||
let (sender, receiver) = smol::channel::unbounded();
|
||||
let mut new = Self {
|
||||
main_sender: params.main_thread_channel.clone(),
|
||||
main_sender: params.main_sender.clone(),
|
||||
max_connected_machines: 2,
|
||||
api_receiver: receiver,
|
||||
api_sender: sender,
|
||||
|
||||
@@ -112,6 +112,13 @@ in
|
||||
networking.hostName = "nixos"; # Define your hostname.
|
||||
# networking.wireless.enable = true; # Enables wireless support via wpa_supplicant.
|
||||
|
||||
networking.interfaces.enp1s0.ipv4.addresses = [
|
||||
{
|
||||
address = "10.10.10.1";
|
||||
prefixLength = 24;
|
||||
}
|
||||
];
|
||||
|
||||
# Configure network proxy if necessary
|
||||
# networking.proxy.default = "http://user:password@proxy:port/";
|
||||
# networking.proxy.noProxy = "127.0.0.1,localhost,internal.domain";
|
||||
@@ -319,9 +326,27 @@ in
|
||||
# Enable the OpenSSH daemon.
|
||||
# services.openssh.enable = true;
|
||||
|
||||
services.dnsmasq = {
|
||||
enable = true;
|
||||
|
||||
settings = {
|
||||
interface = "enp1s0"; # only this interface
|
||||
bind-interfaces = true;
|
||||
|
||||
# DHCP subnet + pool
|
||||
dhcp-range = "10.10.10.50,10.10.10.250,255.255.255.0,12h";
|
||||
|
||||
# Default gateway (router)
|
||||
dhcp-option = [
|
||||
"3,10.10.10.1" # option 3 = router
|
||||
"6,1.1.1.1,8.8.8.8" # option 6 = DNS servers
|
||||
];
|
||||
};
|
||||
};
|
||||
|
||||
# Open ports in the firewall.
|
||||
# networking.firewall.allowedTCPPorts = [ ... ];
|
||||
# networking.firewall.allowedUDPPorts = [ ... ];
|
||||
networking.firewall.allowedUDPPorts = [ 53 67 69 ];
|
||||
# Or disable the firewall altogether.
|
||||
# networking.firewall.enable = false;
|
||||
|
||||
|
||||
+54
-2
@@ -133,6 +133,8 @@ impl SharedState {
|
||||
let mut current_machines = self.current_machines_meta.lock().await;
|
||||
// Retain only machines that do not match the given ID
|
||||
current_machines.retain(|m| &m.machine_identification_unique != machine_id);
|
||||
drop(current_machines);
|
||||
|
||||
tracing::info!(
|
||||
"remove_machine {:?} {:?}",
|
||||
self.current_machines_meta,
|
||||
@@ -140,9 +142,9 @@ impl SharedState {
|
||||
);
|
||||
}
|
||||
|
||||
pub async fn add_machines_if_not_exists(&self, machines: Vec<MachineObj>) {
|
||||
async fn add_machine_objs(&self, machines: Vec<MachineObj>) {
|
||||
let mut current_machines = self.current_machines_meta.lock().await;
|
||||
tracing::info!("add_machines_if_not_exists: {:?}", current_machines);
|
||||
|
||||
// Track existing machine identifiers for quick lookup
|
||||
let existing_ids: HashSet<_> = current_machines
|
||||
.iter()
|
||||
@@ -154,6 +156,56 @@ impl SharedState {
|
||||
current_machines.push(machine);
|
||||
}
|
||||
}
|
||||
drop(current_machines);
|
||||
|
||||
self.send_machines_event().await;
|
||||
}
|
||||
|
||||
pub async fn report_machine_error(
|
||||
&self,
|
||||
machine_identification_unique: MachineIdentificationUnique,
|
||||
error: String,
|
||||
) {
|
||||
let mut current_machines = self.current_machines_meta.lock().await;
|
||||
|
||||
for machine in current_machines.iter_mut() {
|
||||
if machine.machine_identification_unique == machine_identification_unique {
|
||||
machine.error = Some(error);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
current_machines.push(MachineObj {
|
||||
machine_identification_unique,
|
||||
error: Some(error),
|
||||
});
|
||||
}
|
||||
|
||||
pub async fn add_machines(&self, machines: Vec<Box<dyn Machine>>) {
|
||||
let mut api_machines = self.api_machines.lock().await;
|
||||
for machine in machines.iter() {
|
||||
api_machines.insert(
|
||||
machine.get_machine_identification_unique(),
|
||||
machine.api_get_sender(),
|
||||
);
|
||||
}
|
||||
drop(api_machines);
|
||||
|
||||
let objs = machines
|
||||
.iter()
|
||||
.map(|m| MachineObj {
|
||||
machine_identification_unique: m.get_machine_identification_unique(),
|
||||
error: None,
|
||||
})
|
||||
.collect();
|
||||
self.add_machine_objs(objs).await;
|
||||
|
||||
self.rt_machine_creation_channel
|
||||
.send(HotThreadMessage::AddMachines(machines))
|
||||
.await
|
||||
.expect("Could not send to real-time channel");
|
||||
|
||||
self.send_machines_event().await;
|
||||
}
|
||||
|
||||
pub fn new(
|
||||
|
||||
+10
-13
@@ -1,30 +1,27 @@
|
||||
use control_core::ethercat::interface_discovery::discover_ethercat_interface;
|
||||
use control_core::ethernet::ethercat_interface_discovery::probe_ethercat;
|
||||
use smol::Timer;
|
||||
use std::{sync::Arc, time::Duration};
|
||||
|
||||
use crate::{
|
||||
app_state::SharedState,
|
||||
ethercat::{ethercat_discovery_info::send_ethercat_found, setup::setup_loop},
|
||||
metrics::io::set_ethercat_iface,
|
||||
};
|
||||
|
||||
pub async fn find_ethercat_interface() -> String {
|
||||
loop {
|
||||
match discover_ethercat_interface().await {
|
||||
Ok(interface) => {
|
||||
tracing::info!("Found EtherCAT Interface at: {}", interface);
|
||||
set_ethercat_iface(&interface);
|
||||
return interface;
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::warn!("No working interface found: {}. Retrying...", e);
|
||||
Timer::after(Duration::from_secs(1)).await;
|
||||
}
|
||||
let res = probe_ethercat().await;
|
||||
|
||||
if let Some(interface) = res {
|
||||
tracing::info!("Found EtherCAT Interface at: {}", interface);
|
||||
return interface;
|
||||
}
|
||||
|
||||
tracing::warn!("No working interface found. Retrying...");
|
||||
Timer::after(Duration::from_secs(1)).await;
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn start_interface_discovery(app_state: Arc<SharedState>) {
|
||||
pub async fn start_ethercat_discovery(app_state: Arc<SharedState>) {
|
||||
let interface = find_ethercat_interface().await;
|
||||
|
||||
tracing::info!("Calling setup_loop");
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
use crate::app_state::{EtherCatDeviceMetaData, EthercatSetup};
|
||||
use crate::socketio::main_namespace::MainNamespaceEvents;
|
||||
use crate::socketio::main_namespace::ethercat_devices_event::EthercatDevicesEventBuilder;
|
||||
use crate::socketio::main_namespace::machines_event::MachineObj;
|
||||
use crate::{
|
||||
app_state::SharedState,
|
||||
ethercat::config::{MAX_FRAMES, MAX_PDU_DATA, MAX_SUBDEVICES, PDI_LEN},
|
||||
@@ -26,7 +25,7 @@ use machines::machine_identification::{
|
||||
DeviceIdentificationIdentified, MachineIdentificationUnique, read_device_identifications,
|
||||
};
|
||||
use machines::registry::{MACHINE_REGISTRY, MachineRegistry};
|
||||
use machines::{Machine, MachineNewHardware, MachineNewHardwareEthercat, MachineNewParams};
|
||||
use machines::{EtherCATParams, Machine, MachineNewHardware, MachineNewHardwareEthercat};
|
||||
use smol::channel::Sender;
|
||||
use socketioxide::extract::SocketRef;
|
||||
use std::{sync::Arc, time::Duration};
|
||||
@@ -120,7 +119,6 @@ pub async fn set_ethercat_devices<const MAX_SUBDEVICES: usize, const MAX_PDI: us
|
||||
let machine_new_hardware = MachineNewHardware::Ethercat(hardware);
|
||||
|
||||
let mut machines: Vec<Box<dyn Machine>> = vec![];
|
||||
let mut machine_objs: Vec<MachineObj> = vec![];
|
||||
|
||||
for device_group in device_grouping_result.device_groups.iter() {
|
||||
let machine_identification_unique: MachineIdentificationUnique = match device_group.first()
|
||||
@@ -132,40 +130,25 @@ pub async fn set_ethercat_devices<const MAX_SUBDEVICES: usize, const MAX_PDI: us
|
||||
None => continue, // Skip this group if empty
|
||||
};
|
||||
|
||||
let new_machine = machine_registry.new_machine(&MachineNewParams {
|
||||
let new_machine = machine_registry.new_machine(&EtherCATParams {
|
||||
device_group,
|
||||
hardware: &machine_new_hardware,
|
||||
socket_queue_tx: socket_queue_tx.clone(),
|
||||
namespace: None,
|
||||
main_thread_channel: Some(shared_state.main_channel.clone()),
|
||||
main_sender: Some(shared_state.main_channel.clone()),
|
||||
});
|
||||
|
||||
match new_machine {
|
||||
Ok(machine) => {
|
||||
shared_state.clone().api_machines.lock().await.insert(
|
||||
machine_identification_unique.clone(),
|
||||
machine.api_get_sender(),
|
||||
);
|
||||
machine_objs.push(MachineObj {
|
||||
machine_identification_unique,
|
||||
error: None,
|
||||
});
|
||||
machines.push(machine);
|
||||
Ok(machine) => machines.push(machine),
|
||||
Err(e) => {
|
||||
shared_state
|
||||
.report_machine_error(machine_identification_unique.clone(), e.to_string())
|
||||
.await
|
||||
}
|
||||
Err(e) => machine_objs.push(MachineObj {
|
||||
machine_identification_unique,
|
||||
error: Some(e.to_string()),
|
||||
}),
|
||||
}
|
||||
}
|
||||
let _ = shared_state
|
||||
.rt_machine_creation_channel
|
||||
.send(crate::app_state::HotThreadMessage::AddMachines(machines))
|
||||
.await;
|
||||
|
||||
shared_state.add_machines_if_not_exists(machine_objs).await;
|
||||
shared_state.clone().send_machines_event().await;
|
||||
|
||||
shared_state.add_machines(machines).await;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -478,9 +461,9 @@ pub async fn setup_loop(
|
||||
main_namespace.emit(MainNamespaceEvents::EthercatDevicesEvent(event));
|
||||
}
|
||||
|
||||
return Ok(EthercatSetup {
|
||||
Ok(EthercatSetup {
|
||||
devices,
|
||||
group: group_op,
|
||||
maindevice,
|
||||
});
|
||||
})
|
||||
}
|
||||
+14
-31
@@ -1,7 +1,7 @@
|
||||
use crate::metrics::collector::{RuntimeMetricsConfig, spawn_runtime_metrics_sampler};
|
||||
use machines::{
|
||||
AsyncThreadMessage, MachineConnection, MachineNewHardware, MachineNewHardwareSerial,
|
||||
MachineNewParams, SerialDevice, SerialDeviceIdentification, SerialDeviceNew,
|
||||
AsyncThreadMessage, EtherCATParams, MachineConnection, MachineNewHardware,
|
||||
MachineNewHardwareSerial, SerialDevice, SerialDeviceIdentification, SerialDeviceNew,
|
||||
SerialDeviceNewParams,
|
||||
laser::LaserMachine,
|
||||
machine_identification::{
|
||||
@@ -26,18 +26,20 @@ use smol::{
|
||||
future,
|
||||
lock::RwLock,
|
||||
};
|
||||
use socketio::main_namespace::machines_event::MachineObj;
|
||||
use socketioxide::extract::SocketRef;
|
||||
use std::{collections::HashMap, sync::Arc, time::Duration};
|
||||
|
||||
#[cfg(feature = "mock-machine")]
|
||||
use mock_init::init_mock;
|
||||
|
||||
#[cfg(not(feature = "mock-machine"))]
|
||||
use crate::ethercat::init::start_ethercat_discovery;
|
||||
use crate::{
|
||||
ethercat::{
|
||||
ethercat_discovery_info::send_ethercat_found, init::find_ethercat_interface,
|
||||
setup::setup_loop,
|
||||
},
|
||||
modbus_tcp::start_modbus_tcp_discovery,
|
||||
socketio::queue::socketio_queue_worker,
|
||||
};
|
||||
|
||||
@@ -49,6 +51,7 @@ pub mod ethercat;
|
||||
pub mod logging;
|
||||
pub mod r#loop;
|
||||
pub mod metrics;
|
||||
pub mod modbus_tcp;
|
||||
pub mod panic;
|
||||
pub mod performance_metrics;
|
||||
pub mod rest;
|
||||
@@ -74,17 +77,12 @@ pub async fn add_serial_device(
|
||||
.try_into()
|
||||
.expect("Serial devices always have machine identification");
|
||||
|
||||
let machine_identification: MachineIdentificationUnique = device_identification_identified
|
||||
.device_machine_identification
|
||||
.machine_identification_unique
|
||||
.clone();
|
||||
|
||||
let new_machine = machine_registry.new_machine(&MachineNewParams {
|
||||
device_group: &vec![device_identification_identified.clone()],
|
||||
let new_machine = machine_registry.new_machine(&EtherCATParams {
|
||||
device_group: &vec![device_identification_identified],
|
||||
hardware: &MachineNewHardware::Serial(&hardware),
|
||||
socket_queue_tx,
|
||||
namespace: None,
|
||||
main_thread_channel: Some(shared_state.main_channel.clone()),
|
||||
main_sender: Some(shared_state.main_channel.clone()),
|
||||
});
|
||||
|
||||
let machine = match new_machine {
|
||||
@@ -95,24 +93,8 @@ pub async fn add_serial_device(
|
||||
}
|
||||
};
|
||||
|
||||
shared_state
|
||||
.add_machines_if_not_exists(vec![MachineObj {
|
||||
machine_identification_unique: machine_identification.clone(),
|
||||
error: None,
|
||||
}])
|
||||
.await;
|
||||
|
||||
shared_state
|
||||
.api_machines
|
||||
.lock()
|
||||
.await
|
||||
.insert(machine_identification, machine.api_get_sender());
|
||||
|
||||
let _ = shared_state
|
||||
.rt_machine_creation_channel
|
||||
.send(HotThreadMessage::AddMachines(vec![machine]))
|
||||
.await;
|
||||
shared_state.clone().send_machines_event().await;
|
||||
let machines = vec![machine];
|
||||
shared_state.add_machines(machines).await;
|
||||
}
|
||||
|
||||
pub async fn start_serial_discovery(app_state: Arc<SharedState>) {
|
||||
@@ -135,7 +117,6 @@ pub async fn start_interface_discovery(
|
||||
set_ethercat_iface(interface.clone());
|
||||
|
||||
let res = setup_loop(&interface, app_state.clone()).await;
|
||||
|
||||
match res {
|
||||
Ok(setup) => {
|
||||
let _ = sender.send(HotThreadMessage::AddEtherCatSetup(setup)).await;
|
||||
@@ -348,7 +329,9 @@ fn main() {
|
||||
));
|
||||
|
||||
#[cfg(not(feature = "mock-machine"))]
|
||||
smol::spawn(start_interface_discovery(app_state.clone(), sender)).detach();
|
||||
smol::spawn(start_ethercat_discovery(app_state.clone())).detach();
|
||||
|
||||
smol::spawn(start_modbus_tcp_discovery(app_state.clone())).detach();
|
||||
|
||||
smol::block_on(async {
|
||||
send_empty_machines_event(app_state.clone()).await;
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
use crate::add_serial_device;
|
||||
use crate::app_state::SharedState;
|
||||
use crate::socketio::main_namespace::MainNamespaceEvents;
|
||||
use crate::socketio::main_namespace::machines_event::MachinesEventBuilder;
|
||||
use machines::registry::MACHINE_REGISTRY;
|
||||
use std::sync::Arc;
|
||||
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
use crate::app_state::SharedState;
|
||||
use control_core::ethernet::modbus_tcp_discovery::probe_modbus_tcp;
|
||||
use control_core::futures::FutureIteratorExt;
|
||||
use machines::{
|
||||
MACHINE_WAGO_POWER_V1, Machine, MachineChannel, VENDOR_QITECH,
|
||||
machine_identification::{MachineIdentification, MachineIdentificationUnique},
|
||||
wago_power::WagoPower,
|
||||
};
|
||||
use smol::Timer;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
#[cfg(not(feature = "mock-machine"))]
|
||||
pub async fn start_modbus_tcp_discovery(shared_state: Arc<SharedState>) {
|
||||
loop {
|
||||
let addresses = probe_modbus_tcp().await;
|
||||
|
||||
if addresses.is_empty() {
|
||||
Timer::after(Duration::from_secs(1)).await;
|
||||
continue;
|
||||
}
|
||||
|
||||
let machines: Vec<Box<dyn Machine>> = addresses
|
||||
.into_iter()
|
||||
.map(|probe| {
|
||||
smol::spawn(async move {
|
||||
let machine_identification_unique = MachineIdentificationUnique {
|
||||
machine_identification: MachineIdentification {
|
||||
vendor: VENDOR_QITECH,
|
||||
machine: MACHINE_WAGO_POWER_V1,
|
||||
},
|
||||
serial: probe.serial,
|
||||
};
|
||||
|
||||
let channel = MachineChannel::new(machine_identification_unique);
|
||||
let power = WagoPower::new(channel, probe.addr)
|
||||
.await
|
||||
.expect("Failed to initialize wago power supply");
|
||||
|
||||
Box::new(power) as Box<dyn Machine>
|
||||
})
|
||||
})
|
||||
.join_all()
|
||||
.await;
|
||||
|
||||
shared_state.add_machines(machines).await;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "mock-machine")]
|
||||
pub async fn start_modbus_tcp_discovery(shared_state: Arc<SharedState>) {
|
||||
let machine_identification_unique = MachineIdentificationUnique {
|
||||
machine_identification: MachineIdentification {
|
||||
vendor: VENDOR_QITECH,
|
||||
machine: MACHINE_WAGO_POWER_V1,
|
||||
},
|
||||
serial: 0xbeef,
|
||||
};
|
||||
|
||||
let channel = MachineChannel::new(machine_identification_unique);
|
||||
let power = WagoPower::new(channel)
|
||||
.await
|
||||
.expect("Failed to initialize wago power supply");
|
||||
|
||||
let machines: Vec<Box<dyn Machine>> = vec![Box::new(power)];
|
||||
|
||||
shared_state.add_machines(machines).await;
|
||||
}
|
||||
@@ -98,7 +98,6 @@ fn setup_disconnection(socket: SocketRef, namespace_id: NamespaceId, app_state:
|
||||
None => tracing::info!("sender doesnt exist for: {}",ident),
|
||||
};
|
||||
}else{
|
||||
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
@@ -147,11 +146,10 @@ fn setup_connection(socket: SocketRef, namespace_id: NamespaceId, app_state: Arc
|
||||
},
|
||||
None => tracing::info!("sender doesnt exist for: {}",ident),
|
||||
};
|
||||
}else{
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
}
|
||||
Err(err) => {
|
||||
tracing::warn!(
|
||||
"Couldn't subscribe socket to namespace, disconnecting socket={:?} namespace={} error={:?}",
|
||||
socket_clone.id,
|
||||
|
||||
@@ -11,6 +11,7 @@ quantity! {
|
||||
Z0, // amount of substance
|
||||
Z0>; // luminous intensity
|
||||
units {
|
||||
@millivolt: 1.0e-3; "mV", "millivolt", "millivolts";
|
||||
@centivolt: 1.0e-2; "cV", "centivolt", "centivolts";
|
||||
@volt: 1.0; "V", "volt", "volts";
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user