Compare commits

...
Author SHA1 Message Date
Oshgnacknak 2cdc5f791e Loop until modbus device has been found 2025-12-25 21:55:08 +01:00
nixos-install.sh 1f426b698b Enable dhcp and dns server on lan interface 2025-12-25 21:55:08 +01:00
Oshgnacknak fd2790ad10 Fix loop logic 2025-12-25 21:55:08 +01:00
Oshgnacknak b98f92426c Use actual serial number from device 2025-12-25 21:55:08 +01:00
Oshgnacknak a759970488 Fix mock-compile 2025-12-25 21:55:08 +01:00
Oshgnacknak 26c91c7ddd Turn power on/off in UI 2025-12-25 21:55:08 +01:00
Oshgnacknak 1f6f43c7ec Show error if mutation fails 2025-12-25 21:55:08 +01:00
Oshgnacknak 2c84aeed5b Actually set registers correctly 2025-12-25 21:55:08 +01:00
Oshgnacknak a0625b376d Allow setting mode 2025-12-25 21:55:08 +01:00
Oshgnacknak 866dfcec34 Allow turning power on and off 2025-12-25 21:55:08 +01:00
Oshgnacknak 977927810e Read live values from wago power supply 2025-12-25 21:55:08 +01:00
Oshgnacknak 82d62be4ef Display values from namespace 2025-12-25 21:55:08 +01:00
Oshgnacknak 9008f77b8b Basic machine now known its namespace 2025-12-25 21:55:08 +01:00
Oshgnacknak 793b0ebf37 Most basic WAGO power page 2025-12-25 21:55:08 +01:00
Oshgnacknak 2df42517a0 Initialize very basic WAGO machine 2025-12-25 21:55:08 +01:00
Oshgnacknak 026549c37f Fix mock compile 2025-12-25 21:55:08 +01:00
Oshgnacknak c6a85d4fe0 Remove machine new trait from Machine trait
Machines should not need to know, how they where created.
Also, no machine should by assumption be considered an EtherCAT machine.
2025-12-25 21:55:08 +01:00
Oshgnacknak 07d04f3204 Add timeout to Modbus TCP connection 2025-12-25 21:55:08 +01:00
Oshgnacknak 3c325ee386 Discover both EtherCAT and Modbus TCP devices 2025-12-25 21:55:08 +01:00
44 changed files with 1571 additions and 354 deletions

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
View File
@@ -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")),
}
}
+66
View File
@@ -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,
})
}
+26
View File
@@ -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
}
}
+2 -1
View File
@@ -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;
+2
View File
@@ -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,
+274
View File
@@ -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)
}
}
+6 -2
View File
@@ -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",
+12
View File
@@ -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";
+19 -1
View File
@@ -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,
+4 -4
View File
@@ -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(),
+4 -4
View File
@@ -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 {
+4 -4
View File
@@ -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(),
+4 -4
View File
@@ -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(),
+4 -4
View File
@@ -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(),
+5
View File
@@ -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;
+4 -4
View File
@@ -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(),
+5 -5
View File
@@ -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],
};
+4 -4
View File
@@ -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
View File
@@ -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 -4
View File
@@ -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(),
+8 -5
View File
@@ -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 -4
View File
@@ -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();
+226
View File
@@ -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(())
}
}
+1 -1
View File
@@ -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();
+4 -4
View File
@@ -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,
+4 -4
View File
@@ -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,
+26 -1
View File
@@ -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
View File
@@ -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
View File
@@ -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");
+11 -28
View File
@@ -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
View File
@@ -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;
-2
View File
@@ -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;
+69
View File
@@ -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;
}
+3 -5
View File
@@ -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,
+1
View File
@@ -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";
}