Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
578adb9d09 | ||
|
|
1ec20074e9 | ||
|
|
4d796f8422 | ||
|
|
1406d6c724 | ||
|
|
33441b95a5 | ||
|
|
8a3f312675 |
No files matched your search
@@ -1,3 +1,24 @@
|
||||
# `2.13.0`
|
||||
_07.01.2026_
|
||||
|
||||
## General
|
||||
- [#925](https://github.com/qitechgmbh/control/pull/925) Switch to Setup when selected machine is disconnected
|
||||
- [#937](https://github.com/qitechgmbh/control/pull/937) Added minimal Support for WAGO
|
||||
- [#945](https://github.com/qitechgmbh/control/pull/945) Improvements for On-Screen Keyboard usage in Ethercat Setup Page
|
||||
- []
|
||||
|
||||
## Extruder V3
|
||||
- [#930](https://github.com/qitechgmbh/control/pull/930) Added Extruder V3
|
||||
|
||||
## Laser
|
||||
- [#924](https://github.com/qitechgmbh/control/pull/924) Roundness is now displayed as a percentage
|
||||
- [#885](https://github.com/qitechgmbh/control/pull/885) Added Min and Max Diameter
|
||||
- [#849](https://github.com/qitechgmbh/control/pull/849) Added a Warning if Filament is out of Spec
|
||||
|
||||
## Aquapath
|
||||
- [#976](https://github.com/qitechgmbh/control/pull/976) Improved Stability
|
||||
|
||||
|
||||
# `2.12.0`
|
||||
_17.11.2025_
|
||||
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
use std::process::Command;
|
||||
|
||||
use anyhow::Result;
|
||||
use interfaces::Interface;
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,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;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
pub mod modbus_serial_interface;
|
||||
pub mod tcp;
|
||||
|
||||
use anyhow::Error;
|
||||
use crc::{CRC_16_MODBUS, Crc};
|
||||
|
||||
@@ -0,0 +1,263 @@
|
||||
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<()> {
|
||||
if self.read_u16().await? != self.transactions {
|
||||
bail!("Modbus device sent unexpected transaction id!");
|
||||
}
|
||||
|
||||
if self.read_u16().await? != PROTOCOL_ID {
|
||||
bail!("Modbus device sent unexpected protocol id!");
|
||||
}
|
||||
|
||||
let length = self.read_u16().await?;
|
||||
if length < 2 {
|
||||
bail!("Modbus device will send too few bytes to understand the response!");
|
||||
}
|
||||
|
||||
if self.read_u8().await? != UNIT_ID {
|
||||
bail!("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;
|
||||
if count * 2 != num_data_bytes {
|
||||
bail!(
|
||||
"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?;
|
||||
|
||||
if self.read_u16().await? != addr {
|
||||
bail!("Modbus device wrote to wrong register address!");
|
||||
}
|
||||
|
||||
if self.read_u16().await? != count as u16 {
|
||||
bail!("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!");
|
||||
|
||||
todo!()
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
# Machine Documentation
|
||||
|
||||
This directory contains documentation for QiTech machines.
|
||||
|
||||
## Structure
|
||||
|
||||
```
|
||||
docs/machines/
|
||||
├── manuals/ # Machine operation manuals (markdown files)
|
||||
│ ├── extruder.md
|
||||
│ ├── winder.md
|
||||
│ ├── laser.md
|
||||
│ └── mock.md
|
||||
├── extruder/ # Machine-specific documentation
|
||||
├── winder/
|
||||
├── laser/
|
||||
└── mock/
|
||||
```
|
||||
|
||||
## Images
|
||||
|
||||
All manual images are stored in: **`electron/public/images/manuals/[machine-name]/`**
|
||||
|
||||
For example:
|
||||
- Winder images: `electron/public/images/manuals/winder/`
|
||||
- Extruder images: `electron/public/images/manuals/extruder/`
|
||||
- Laser images: `electron/public/images/manuals/laser/`
|
||||
|
||||
### Image References in Manuals
|
||||
|
||||
When referencing images in manual markdown files (located in `docs/machines/manuals/`), use relative paths:
|
||||
|
||||
```markdown
|
||||

|
||||
```
|
||||
|
||||
## Adding Images
|
||||
|
||||
1. Place images in: `electron/public/images/manuals/[machine-name]/`
|
||||
2. Reference in markdown: `../../../../electron/public/images/manuals/[machine-name]/your-image.png`
|
||||
|
Before Width: | Height: | Size: 72 KiB |
|
Before Width: | Height: | Size: 84 KiB |
|
Before Width: | Height: | Size: 94 KiB |
|
Before Width: | Height: | Size: 118 KiB |
|
Before Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 45 KiB |
|
Before Width: | Height: | Size: 387 KiB |
@@ -1,65 +1,4 @@
|
||||

|
||||
|
||||
<!-- Datasheet: Added below Titelseite. Placeholder images to be replaced with final exports. -->
|
||||
|
||||
# QiTech Pro Extruder – Datasheet
|
||||
|
||||

|
||||
|
||||
## Included Assemblies
|
||||
Electrical cabinet · Temperature controller · Frequency inverter · Cable routing + connection to QiTech Control (digital control)
|
||||
|
||||
## Motor
|
||||
| Specification | Value |
|
||||
| ------------- | ----- |
|
||||
| Motor power | 1.5 kW |
|
||||
| Output speed | 41 rpm |
|
||||
| Output torque | 336 Nm |
|
||||
| Transmission ratio | 34:2 |
|
||||
| Service factor cG | 1.4 |
|
||||
| Voltage / Frequency | 400 V / 50 Hz |
|
||||
| Efficiency class | IE3 (Premium Efficiency) |
|
||||
| Efficiency @ 100% rated load / % rated current (std. voltage) | 5.98 / 3.45 A |
|
||||
| Protection class | IP55, Insulation class F |
|
||||
| Construction design | Flange design, terminal box top |
|
||||
| Shaft end (d×L) | 40 × 80 mm |
|
||||
|
||||
## Melt Pressure Sensor (DSI1-1/2-500-F-15)
|
||||
| Specification | Value |
|
||||
| ------------- | ----- |
|
||||
| Output signal | 4–20 mA (2-conductor) |
|
||||
| Sensor system | Mercury-free |
|
||||
| Sensor | DMS Vollbrückentechnik |
|
||||
| Measuring accuracy | 0.5 % |
|
||||
| Calibration signal | 80 % |
|
||||
| Supply voltage | 15–32 VDC |
|
||||
| Measuring range | 0–500 bar |
|
||||
| Temperature range (membrane) | max. 410 °C |
|
||||
| Temperature range (sensor head) | max. 85 °C |
|
||||
| Screw-in thread | 1/2"–20 UNF |
|
||||
|
||||
## Extruder Barrel & Screw
|
||||
Nitrided extruder barrel (Ø 65 mm). Screw hardened from high‑alloyed steel (Ø 25 mm).
|
||||
|
||||
## Heating Elements & Temperature Sensors
|
||||
3 heating zones (max. 350 °C), Heater 415 V, 3 × 750 W. 3× Type K temperature sensors, 1/8" NPT.
|
||||
|
||||
## Hopper
|
||||
Volume: 4 L · Material: Aluminum · Easily detachable.
|
||||
|
||||
## Dimensions
|
||||
|
||||

|
||||
|
||||
Overall length 1365 mm · Width 220 mm · Height 384 mm.
|
||||
|
||||
Fully wired and assembled (raised on 2 aluminum profiles).
|
||||
|
||||
Delivery time after completion: 2–5 working days.
|
||||
|
||||
Warranty: 24 months after delivery (excluding wearing parts).
|
||||
|
||||
<br>
|
||||

|
||||
|
||||
<br>
|
||||
|
||||
@@ -116,7 +55,7 @@ Darmstadt, 10/06/2025
|
||||
|
||||
64392 Darmstadt,
|
||||
|
||||
<img src="/images/manuals/extruder/signature.jpeg" width="300" height="60"/>
|
||||
<img src="images/manuals/extruder/signature.jpeg" width="300" height="60"/>
|
||||
|
||||
Milan von dem Bussche
|
||||
|
||||
@@ -162,7 +101,7 @@ Operating instructions
|
||||
|
||||
# 1 Machine description
|
||||
|
||||

|
||||

|
||||
|
||||
<br><br><br>
|
||||
|
||||
@@ -229,7 +168,7 @@ Accuracy class: 2
|
||||
|
||||
| Picture | Danger! |
|
||||
| --------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
|  | **Danger to life due to technically faulty machine.** <br> The machine may only be used when it is in perfect technical condition and in accordance with its intended use, in a safety-conscious and hazard-conscious manner and in compliance with these operating instructions. All faults, especially those that could impair safety, must be rectified immediately before starting the winder. Everyone who works on or with the Qitech Winder is responsible for observing these safety aspects! |
|
||||
|  | **Danger to life due to technically faulty machine.** <br> The machine may only be used when it is in perfect technical condition and in accordance with its intended use, in a safety-conscious and hazard-conscious manner and in compliance with these operating instructions. All faults, especially those that could impair safety, must be rectified immediately before starting the winder. Everyone who works on or with the Qitech Winder is responsible for observing these safety aspects! |
|
||||
|
||||
<br><br><br>
|
||||
|
||||
@@ -237,17 +176,17 @@ Accuracy class: 2
|
||||
|
||||
| Pictogram | Description of pictogram | Pictogram | Pictogram Description |
|
||||
| ------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
|  | W001 General warning sign The combination of the symbol W001 and the signal word **"Danger"** indicates an imminently hazardous situation which, if not avoided death or serious injury if it is not avoided. The combination of the symbol W001 and the signal word **"Warning"** indicates an imminently hazardous situation which, if not avoided, will result in death or serious injury. death or serious injury if it is not avoided. The combination of the symbol W001 and the signal word **"Caution"** indicates an imminently hazardous situation which, if not avoided death or serious injury if not avoided. |  | M001 General warning sign Mandatory sign This combination of symbol and signal word indicates a potentially hazardous situation which, if not avoided, may result in damage to property or the environment. not avoided. |
|
||||
|  | W008 Warning of risk of falling |  | M003 Use hearing protection |
|
||||
|  | W011 Warning of danger of slipping |  | M004 Use eye protection |
|
||||
|  | W004 Warning of laser beam |  | M008 Use foot protection |
|
||||
|  | W012 Warning of electrical voltage |  | M009 Use hand protection |
|
||||
|  | W017 Warning of hot surface |  | M010 Use protective clothing |
|
||||
|  | W018 Warning against automatic start-up |  | M012 Use handrail |
|
||||
|  | W019 Warning of crushing hazard |  | M021 Switch off before maintenance or repair |
|
||||
|  | W024 Warning against hand injuries |  | M014 Use head protection |
|
||||
|  | W025 Warning against counter-rotating rollers | | |
|
||||
|  | W016 Warning of toxic substances | | |
|
||||
|  | W001 General warning sign The combination of the symbol W001 and the signal word **"Danger"** indicates an imminently hazardous situation which, if not avoided death or serious injury if it is not avoided. The combination of the symbol W001 and the signal word **"Warning"** indicates an imminently hazardous situation which, if not avoided, will result in death or serious injury. death or serious injury if it is not avoided. The combination of the symbol W001 and the signal word **"Caution"** indicates an imminently hazardous situation which, if not avoided death or serious injury if not avoided. |  | M001 General warning sign Mandatory sign This combination of symbol and signal word indicates a potentially hazardous situation which, if not avoided, may result in damage to property or the environment. not avoided. |
|
||||
|  | W008 Warning of risk of falling |  | M003 Use hearing protection |
|
||||
|  | W011 Warning of danger of slipping |  | M004 Use eye protection |
|
||||
|  | W004 Warning of laser beam |  | M008 Use foot protection |
|
||||
|  | W012 Warning of electrical voltage |  | M009 Use hand protection |
|
||||
|  | W017 Warning of hot surface |  | M010 Use protective clothing |
|
||||
|  | W018 Warning against automatic start-up |  | M012 Use handrail |
|
||||
|  | W019 Warning of crushing hazard |  | M021 Switch off before maintenance or repair |
|
||||
|  | W024 Warning against hand injuries |  | M014 Use head protection |
|
||||
|  | W025 Warning against counter-rotating rollers | | |
|
||||
|  | W016 Warning of toxic substances | | |
|
||||
|
||||
<br><br><br>
|
||||
|
||||
@@ -255,15 +194,15 @@ Accuracy class: 2
|
||||
|
||||
| Prohibition signs Pictogram | Pictogram description | International pictograms / other Pictogram | Pictogram Description |
|
||||
| ------------------------------------------------------------------------- | --------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------- | --------------------------------------------------------------------------- |
|
||||
| <img src="/images/manuals/extruder/p001.jpeg" width="158" height="158"/> | P001 General prohibition sign | <img src="/images/manuals/extruder/f001.jpeg" width="158" height="158"> | F001 Fire extinguisher |
|
||||
| <img src="/images/manuals/extruder/p002.jpeg" width="158" height="158"/> | P002 No smoking | <img src="/images/manuals/extruder/ANIS_danger.jpeg" width="232" height="60"> | ANSI Z535.3 - DANGER |
|
||||
| <img src="/images/manuals/extruder/p003.jpeg" width="158" height="158"/> | P003 No open flames; fire, open source of ignition and smoking prohibited | <img src="/images/manuals/extruder/ANIS_warning.jpeg" width="232" height="60"> | ANSI Z535.3 - WARNING |
|
||||
| <img src="/images/manuals/extruder/p006.jpeg" width="158" height="158"/> | P006 Prohibited for industrial trucks | <img src="/images/manuals/extruder/ANIS_caution.jpeg" width="232" height="60"> | ANSI Z535.3 - CAUTION |
|
||||
| <img src="/images/manuals/extruder/p009.jpeg" width="158" height="158"/> | P009 Climbing prohibited - In the sense of climbing prohibited for unauthorized persons | <img src="/images/manuals/extruder/ANIS_notice.jpeg" width="232" height="60"> | ANSI Z535.3 - NOTICE |
|
||||
| <img src="/images/manuals/extruder/p022.jpeg" width="158" height="158"/> | P022 Eating and drinking prohibited | <img src="/images/manuals/extruder/ANIS_safetyinstructions.jpeg" width="232" height="60"> | ANSI Z535.3 - SAFETY INSTRUCTIONS |
|
||||
| <img src="/images/manuals/extruder/p024.jpeg" width="158" height="158"/> | P024 Entering the area prohibited | <img src="/images/manuals/extruder/trashcan.jpeg" width="158" height="158"> | Symbol for separate collection of waste electrical and electronic equipment |
|
||||
| <img src="/images/manuals/extruder/p031.jpeg" width="158" height="158"/> | P031 Switching prohibited | | |
|
||||
| <img src="/images/manuals/extruder/D-p006.jpeg" width="158" height="158"/> | D-P006 Access prohibited for unauthorized persons | | |
|
||||
| <img src="images/manuals/extruder/p001.jpeg" width="158" height="158"/> | P001 General prohibition sign | <img src="images/manuals/extruder/f001.jpeg" width="158" height="158"> | F001 Fire extinguisher |
|
||||
| <img src="images/manuals/extruder/p002.jpeg" width="158" height="158"/> | P002 No smoking | <img src="images/manuals/extruder/ANIS_danger.jpeg" width="232" height="60"> | ANSI Z535.3 - DANGER |
|
||||
| <img src="images/manuals/extruder/p003.jpeg" width="158" height="158"/> | P003 No open flames; fire, open source of ignition and smoking prohibited | <img src="images/manuals/extruder/ANIS_warning.jpeg" width="232" height="60"> | ANSI Z535.3 - WARNING |
|
||||
| <img src="images/manuals/extruder/p006.jpeg" width="158" height="158"/> | P006 Prohibited for industrial trucks | <img src="images/manuals/extruder/ANIS_caution.jpeg" width="232" height="60"> | ANSI Z535.3 - CAUTION |
|
||||
| <img src="images/manuals/extruder/p009.jpeg" width="158" height="158"/> | P009 Climbing prohibited - In the sense of climbing prohibited for unauthorized persons | <img src="images/manuals/extruder/ANIS_notice.jpeg" width="232" height="60"> | ANSI Z535.3 - NOTICE |
|
||||
| <img src="images/manuals/extruder/p022.jpeg" width="158" height="158"/> | P022 Eating and drinking prohibited | <img src="images/manuals/extruder/ANIS_safetyinstructions.jpeg" width="232" height="60"> | ANSI Z535.3 - SAFETY INSTRUCTIONS |
|
||||
| <img src="images/manuals/extruder/p024.jpeg" width="158" height="158"/> | P024 Entering the area prohibited | <img src="images/manuals/extruder/trashcan.jpeg" width="158" height="158"> | Symbol for separate collection of waste electrical and electronic equipment |
|
||||
| <img src="images/manuals/extruder/p031.jpeg" width="158" height="158"/> | P031 Switching prohibited | | |
|
||||
| <img src="images/manuals/extruder/D-p006.jpeg" width="158" height="158"/> | D-P006 Access prohibited for unauthorized persons | | |
|
||||
|
||||
<br><br><br>
|
||||
|
||||
@@ -336,7 +275,7 @@ The machine has a main switch that disconnects/connects the mains supply. The ON
|
||||
|
||||
The emergency stop switch is used to quickly stop the machine in the event of danger. This is designed with redundant emergency stop contactors. An emergency stop switching device also prevents the machine from being switched on again after a power failure. By activating the emergency stop device, the entire machine is shut down and disconnected from the mains supply.
|
||||
|
||||

|
||||

|
||||
|
||||
Hazardous movements are brought to a controlled standstill as quickly as possible. The machine can only be restarted by physically resetting the emergency stop switch and then starting one of the operating modes in the software.
|
||||
|
||||
@@ -418,11 +357,11 @@ Depending on the activity or the duration of the activity, the operator must det
|
||||
<br><br>
|
||||
| Picture | Danger |
|
||||
|---------|--------|
|
||||
| |**Risk of injury due to improper transport** <br> Be careful when loading the machine, do not place any limbs under the parts when setting them down, do not stand under suspended loads or between moving loads and walls or machine parts. Wear personal protective equipment: safety shoes, protective work clothing, protective gloves.
|
||||
| |**Risk of injury due to improper transport** <br> Be careful when loading the machine, do not place any limbs under the parts when setting them down, do not stand under suspended loads or between moving loads and walls or machine parts. Wear personal protective equipment: safety shoes, protective work clothing, protective gloves.
|
||||
|
||||
| Picture | Caution! |
|
||||
| --------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
|  | **Danger to life due to incorrect installation and commissioning!** <brErrors during installation can lead to life-threatening situations or cause considerable damage to property. Installation and commissioning may only be carried out by the manufacturer's employees. |
|
||||
|  | **Danger to life due to incorrect installation and commissioning!** <brErrors during installation can lead to life-threatening situations or cause considerable damage to property. Installation and commissioning may only be carried out by the manufacturer's employees. |
|
||||
|
||||
<br>
|
||||
<br>
|
||||
@@ -451,7 +390,7 @@ The CEE plug of the extruder must be plugged into a suitable three-phase socket
|
||||
|
||||
The machines are delivered with the newest version of the software installed. Updates, as well as previous updates and the software version can be viewed under Setup-> Updates. If a new version of the software is available, it will be displayed here. To perform the update an internet connection is required. In the next step, click the red "Select" button next to the chosen update. The display will now shut down, and when it is turned back on, the latest version of the software will be installed.
|
||||
|
||||

|
||||

|
||||
|
||||
<br>
|
||||
|
||||
@@ -492,7 +431,7 @@ Then start the machine as follows:
|
||||
- Set the “target temperature” of the individual heating zones according to the melting material in the 3 input fields
|
||||
- Set the “target temperature” of the nozzle
|
||||
- After reaching the target temperature, wait 15 minutes until the machine has heated through and the plastic in the screw barrel has completely melted.
|
||||

|
||||

|
||||
|
||||
<br>
|
||||
|
||||
@@ -504,7 +443,7 @@ Then start the machine as follows:
|
||||
- Press the "Extrude" button in the “mode” box
|
||||
- Wait until old melt has been "flushed" out of the extruder, i.e. no more discolouration of the filament is no longer discoloured and a constant material flow has been achieved
|
||||
|
||||

|
||||

|
||||
|
||||
<br>
|
||||
|
||||
@@ -532,7 +471,7 @@ Then start the machine as follows:
|
||||
- Go to "Config" in the funktion bar
|
||||
- To set the screw direction click on "Forward" or "Backward" in the "Invert settings" box
|
||||
|
||||

|
||||

|
||||
|
||||
<br>
|
||||
|
||||
@@ -556,7 +495,7 @@ Then start the machine as follows:
|
||||
|
||||
| Picture | DANGER! |
|
||||
| --------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
|  | **Danger to life due to operation of the machine in a faulty condition.** <br> If safety-relevant changes or defects occur on the machine, shut it down immediately, secure it against being switched on again and report the incident to the responsible office/person! |
|
||||
|  | **Danger to life due to operation of the machine in a faulty condition.** <br> If safety-relevant changes or defects occur on the machine, shut it down immediately, secure it against being switched on again and report the incident to the responsible office/person! |
|
||||
|
||||
<br>
|
||||
<br>
|
||||
@@ -594,11 +533,11 @@ If further work is necessary, such as replacing the extruder screw, please conta
|
||||
|
||||
| Picture | Caution! |
|
||||
| --------------------------------------------- | ----------------------------------------------------------------------------- |
|
||||
|  | **Observe the instructions for maintenance work when servicing the machine!** |
|
||||
|  | **Observe the instructions for maintenance work when servicing the machine!** |
|
||||
|
||||
| Picture | Caution! |
|
||||
| --------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- |
|
||||
|  | **Risk of screw breakage if disassembled while hot!** <br> Screw connections may only be loosened when the system has cooled down. |
|
||||
|  | **Risk of screw breakage if disassembled while hot!** <br> Screw connections may only be loosened when the system has cooled down. |
|
||||
|
||||
<br>
|
||||
<br>
|
||||
@@ -681,15 +620,15 @@ Appliances with this logo on the packaging or on the appliance must be disposed
|
||||
|
||||
| Picture | Danger! |
|
||||
| ----------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| <img src="/images/manuals/extruder/w001.jpeg" width="150" height="150"/> | **Danger of getting caught, pulled in or pulled along by moving parts of the machine** <br> Do not wear loose long hair, loose clothing or jewelry! There is always a risk of getting caught, pulled in or pulled along by moving parts! |
|
||||
| <img src="/images/manuals/extruder/w001.jpeg" width="150" height="150"/> | **Operation only permitted with protective devices fitted!** <br> The extruder may only be operated with properly fitted and functioning safety guards. The operator is responsible for ensuring that all protective devices are properly fitted. |
|
||||
| <img src="/images/manuals/extruder/w001.jpeg" width="150" height="150"/> | **Danger of cutting tensioned filament** <br> The stretched filament must not be touched between the puller and the spool. Danger of cutting! Do not wind any pointed, sharp-edged or particularly thin materials! |
|
||||
| <img src="/images/manuals/extruder/w012.jpeg" width="150" height="150"/> | **Electric shock** <br> Before carrying out any repair, set-up or maintenance work, the extruder must be de-energised by disconnecting the mains plug! Pull out the mains plug! |
|
||||
| <img src="/images/manuals/extruder/w012.jpeg" width="150" height="150"/> | **Electrostatic charges on the puller and spool** <br> Friction of the filament on the puller rollers and on the spool can lead to electrostatic charges. There is a risk of minor electric shocks! Do not touch the affected areas, but keep a sufficient distance! |
|
||||
| <img src="/images/manuals/extruder/w018.jpeg" width="150" height="150"/> | **Automatic speed adjustment of puller, reel and traverse** <br> The bobbin automatically adjusts its speed to keep the winding pressure constant. The traverse automatically adjusts its speed to the reel in order to maintain the same travel distance per revolution. |
|
||||
| <img src="/images/manuals/extruder/w025.jpeg" width="150" height="150"/> | **Feeding of body parts** <br> Extreme caution is required in the area of the puller and spool axle! There is an acute danger of getting caught or pulled in! Make sure you wear close-fitting clothing! Always keep the emergency stop switch within easy reach so that it can be activated immediately in an emergency. |
|
||||
| <img src="/images/manuals/extruder/w024.jpeg" width="150" height="150"/> | **Risk of crushing when threading the filament** <br> Exercise extreme caution when threading the filament through the puller rollers! There is a risk of crushing between the upper and lower pair of rollers. |
|
||||
| <img src="/images/manuals/extruder/w024.jpeg" width="150" height="150"/> | **Risk of crushing between the traversing unit and machine housing** <br> Exercise extreme caution when moving the traverser towards the housing! Do not place any limbs in the gap! |
|
||||
| <img src="images/manuals/extruder/w001.jpeg" width="150" height="150"/> | **Danger of getting caught, pulled in or pulled along by moving parts of the machine** <br> Do not wear loose long hair, loose clothing or jewelry! There is always a risk of getting caught, pulled in or pulled along by moving parts! |
|
||||
| <img src="images/manuals/extruder/w001.jpeg" width="150" height="150"/> | **Operation only permitted with protective devices fitted!** <br> The extruder may only be operated with properly fitted and functioning safety guards. The operator is responsible for ensuring that all protective devices are properly fitted. |
|
||||
| <img src="images/manuals/extruder/w001.jpeg" width="150" height="150"/> | **Danger of cutting tensioned filament** <br> The stretched filament must not be touched between the puller and the spool. Danger of cutting! Do not wind any pointed, sharp-edged or particularly thin materials! |
|
||||
| <img src="images/manuals/extruder/w012.jpeg" width="150" height="150"/> | **Electric shock** <br> Before carrying out any repair, set-up or maintenance work, the extruder must be de-energised by disconnecting the mains plug! Pull out the mains plug! |
|
||||
| <img src="images/manuals/extruder/w012.jpeg" width="150" height="150"/> | **Electrostatic charges on the puller and spool** <br> Friction of the filament on the puller rollers and on the spool can lead to electrostatic charges. There is a risk of minor electric shocks! Do not touch the affected areas, but keep a sufficient distance! |
|
||||
| <img src="images/manuals/extruder/w018.jpeg" width="150" height="150"/> | **Automatic speed adjustment of puller, reel and traverse** <br> The bobbin automatically adjusts its speed to keep the winding pressure constant. The traverse automatically adjusts its speed to the reel in order to maintain the same travel distance per revolution. |
|
||||
| <img src="images/manuals/extruder/w025.jpeg" width="150" height="150"/> | **Feeding of body parts** <br> Extreme caution is required in the area of the puller and spool axle! There is an acute danger of getting caught or pulled in! Make sure you wear close-fitting clothing! Always keep the emergency stop switch within easy reach so that it can be activated immediately in an emergency. |
|
||||
| <img src="images/manuals/extruder/w024.jpeg" width="150" height="150"/> | **Risk of crushing when threading the filament** <br> Exercise extreme caution when threading the filament through the puller rollers! There is a risk of crushing between the upper and lower pair of rollers. |
|
||||
| <img src="images/manuals/extruder/w024.jpeg" width="150" height="150"/> | **Risk of crushing between the traversing unit and machine housing** <br> Exercise extreme caution when moving the traverser towards the housing! Do not place any limbs in the gap! |
|
||||
|
||||
<br>
|
||||
<br>
|
||||
@@ -735,7 +674,7 @@ The following persons must not work on the machine:
|
||||
|
||||
| Picture | DANGER! |
|
||||
| ----------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
|  | **Risk of injury due to insufficient qualification!** Improper handling can lead to considerable personal injury and damage to property. |
|
||||
|  | **Risk of injury due to insufficient qualification!** Improper handling can lead to considerable personal injury and damage to property. |
|
||||
| - Special activities may only be carried out by the persons specified in the respective chapters of these instructions. |
|
||||
|
||||
<br>
|
||||
@@ -764,24 +703,24 @@ The following persons must not work on the machine:
|
||||
|
||||
## Electrical Diagrams
|
||||
|
||||

|
||||

|
||||
|
||||

|
||||

|
||||
|
||||

|
||||

|
||||
|
||||

|
||||

|
||||
|
||||

|
||||

|
||||
|
||||

|
||||

|
||||
|
||||

|
||||

|
||||
|
||||

|
||||

|
||||
|
||||

|
||||

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

|
||||

|
||||
@@ -1,88 +1,4 @@
|
||||

|
||||
|
||||
<br>
|
||||
|
||||
# Data sheet: QiTech Pro Winder
|
||||
|
||||

|
||||
|
||||
Short description:
|
||||
|
||||
The QiTech Pro Winder is a machine for winding and rewinding 3D printing filaments and other strand materials. It is an improvement upon the proven QiTech Winder with support for multiple filament diameters, spools up to 5 kg, angular speeds up to 20 m/min and automatic adjustment of the winding speed depending on the filament tension.
|
||||
|
||||
It consists of a pulling unit, a tension arm, a filament guiding traverse and the spool axis. It is controlled by a computer using the QiTech Control software. The traverse movement ensures a clean wind of the filament on the spool. Automatic load adjustment allows for tight winding without having to manually readjust the winding speed each layer. A laser in the traverse allows for easy calibration of the spool width.
|
||||
|
||||
<br>
|
||||
|
||||
## Scope of delivery
|
||||
|
||||
| # | Item |
|
||||
| - | ----------------------------------------- |
|
||||
| 1 | QiTech Pro Winder |
|
||||
| 2 | 2x Spool Carrier-Pins |
|
||||
| 3 | 4x Weights for tension arm |
|
||||
| 4 | 230V/110V to 24V Power Supply |
|
||||
| 5 | USB C to USB A cable |
|
||||
| 6 | USB Drive with QiTech Control software |
|
||||
| 7 | Operating manual |
|
||||
| 8 | optional: wheeled frame |
|
||||
|
||||
<br>
|
||||
|
||||
## Dimensions and Weight
|
||||
|
||||

|
||||

|
||||
|
||||
| | no frame | incl. frame |
|
||||
| -------------- | -------- | ----------- |
|
||||
| Weight | 20 kg | 35 kg |
|
||||
| Length (1) | 91 cm | 91 cm |
|
||||
| Width (2) | 50 cm | 50 cm |
|
||||
| Height (3) | 42 cm | 134 cm |
|
||||
|
||||
<br>
|
||||
|
||||
## Requirements of the Spool
|
||||
|
||||
The QiTech Pro Winder is compatible with almost all spools. The spool is rotated by a Carrier-Pin and secured against lateral slipping by a quick release mechanism. The maximum spool weight is 5 kg.
|
||||
|
||||

|
||||
|
||||
<br>
|
||||
|
||||
## Winding Speed
|
||||
|
||||
How many meters of filament can be wound per minute at maximum speed depends on the inner diameter of the spool/how much filament has already been wound. The resulting path speed determines how much filament can be wound up in a given time. We take a usual spool with an inner diameter of 100 mm to calculate an expected maximum speed. If the inner diameter is larger, the maximum speed shifts upwards.
|
||||
|
||||
Winding Speed:
|
||||
|
||||
- Min.: 0.5 Meters/Minute
|
||||
- Max.: 20 Meters/Minute
|
||||
|
||||
<br>
|
||||
|
||||
## Friction of the puller rolls
|
||||
|
||||
Since the diameter of the extruded filament depends primarily on the pulling speed of the Pro Winder, it is important that it remains constant. This is ensured on the one hand by the use of a stepper motor as drive. At the same time, a vulcanized PU coating of the rolls and a spring-increased contact pressure make the filament only slip through from approx. 30 N tension.
|
||||
|
||||
<br>
|
||||
|
||||
## Auto-Stop function
|
||||
|
||||
When extruding or rewinding, it is often the case that a specific amount of meters must be wound onto a spool. The QiTech Pro Winder has an integrated filament length measurement. For example, after 100 m, it can automatically switch from "wind" to "pull" mode. In addition, the operator is alerted acoustically that the spool has finished winding.
|
||||
|
||||

|
||||
|
||||
<br>
|
||||
|
||||
## Electronics / Power consumption
|
||||
|
||||
| Specification | Value |
|
||||
| --------------------- | -------------------------- |
|
||||
| Mains connection | 230V/50Hz or 110V/60Hz |
|
||||
| Ø Load in operation | 3A at 24V |
|
||||
| Ø Consumption in use | 72 Wh |
|
||||

|
||||
|
||||
<br>
|
||||
|
||||
@@ -139,7 +55,7 @@ Darmstadt, 10/06/2025
|
||||
|
||||
64392 Darmstadt,
|
||||
|
||||
<img src="/images/manuals/winder/signature.jpeg" width="300" height="60"/>
|
||||
<img src="images/manuals/winder/signature.jpeg" width="300" height="60"/>
|
||||
|
||||
Milan von dem Bussche
|
||||
|
||||
@@ -185,7 +101,7 @@ Operating instructions
|
||||
|
||||
# 1 Machine description
|
||||
|
||||

|
||||

|
||||
|
||||
| Item number | Item number Description |
|
||||
| ----------- | ----------------------------------------- |
|
||||
@@ -270,7 +186,7 @@ Accuracy class: 2
|
||||
|
||||
| Picture | Danger! |
|
||||
| ------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
|  | **Danger to life due to technically faulty machine.** <br> The machine may only be used when it is in perfect technical condition and in accordance with its intended use, in a safety-conscious and hazard-conscious manner and in compliance with these operating instructions. All faults, especially those that could impair safety, must be rectified immediately before starting the winder. Everyone who works on or with the Qitech Winder is responsible for observing these safety aspects! |
|
||||
|  | **Danger to life due to technically faulty machine.** <br> The machine may only be used when it is in perfect technical condition and in accordance with its intended use, in a safety-conscious and hazard-conscious manner and in compliance with these operating instructions. All faults, especially those that could impair safety, must be rectified immediately before starting the winder. Everyone who works on or with the Qitech Winder is responsible for observing these safety aspects! |
|
||||
|
||||
<br><br><br>
|
||||
|
||||
@@ -278,17 +194,17 @@ Accuracy class: 2
|
||||
|
||||
| Pictogram | Description of pictogram | Pictogram | Pictogram Description |
|
||||
| ---------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
|  | W001 General warning sign The combination of the symbol W001 and the signal word **"Danger"** indicates an imminently hazardous situation which, if not avoided death or serious injury if it is not avoided. The combination of the symbol W001 and the signal word **"Warning"** indicates an imminently hazardous situation which, if not avoided, will result in death or serious injury. death or serious injury if it is not avoided. The combination of the symbol W001 and the signal word **"Caution"** indicates an imminently hazardous situation which, if not avoided death or serious injury if not avoided. |  | M001 General warning sign Mandatory sign This combination of symbol and signal word indicates a potentially hazardous situation which, if not avoided, may result in damage to property or the environment. not avoided. |
|
||||
|  | W008 Warning of risk of falling |  | M003 Use hearing protection |
|
||||
|  | W011 Warning of danger of slipping |  | M004 Use eye protection |
|
||||
|  | W004 Warning of laser beam |  | M008 Use foot protection |
|
||||
|  | W012 Warning of electrical voltage |  | M009 Use hand protection |
|
||||
|  | W017 Warning of hot surface |  | M010 Use protective clothing |
|
||||
|  | W018 Warning against automatic start-up |  | M012 Use handrail |
|
||||
|  | W019 Warning of crushing hazard |  | M021 Switch off before maintenance or repair |
|
||||
|  | W024 Warning against hand injuries |  | M014 Use head protection |
|
||||
|  | W025 Warning against counter-rotating rollers | | |
|
||||
|  | W016 Warning of toxic substances | | |
|
||||
|  | W001 General warning sign The combination of the symbol W001 and the signal word **"Danger"** indicates an imminently hazardous situation which, if not avoided death or serious injury if it is not avoided. The combination of the symbol W001 and the signal word **"Warning"** indicates an imminently hazardous situation which, if not avoided, will result in death or serious injury. death or serious injury if it is not avoided. The combination of the symbol W001 and the signal word **"Caution"** indicates an imminently hazardous situation which, if not avoided death or serious injury if not avoided. |  | M001 General warning sign Mandatory sign This combination of symbol and signal word indicates a potentially hazardous situation which, if not avoided, may result in damage to property or the environment. not avoided. |
|
||||
|  | W008 Warning of risk of falling |  | M003 Use hearing protection |
|
||||
|  | W011 Warning of danger of slipping |  | M004 Use eye protection |
|
||||
|  | W004 Warning of laser beam |  | M008 Use foot protection |
|
||||
|  | W012 Warning of electrical voltage |  | M009 Use hand protection |
|
||||
|  | W017 Warning of hot surface |  | M010 Use protective clothing |
|
||||
|  | W018 Warning against automatic start-up |  | M012 Use handrail |
|
||||
|  | W019 Warning of crushing hazard |  | M021 Switch off before maintenance or repair |
|
||||
|  | W024 Warning against hand injuries |  | M014 Use head protection |
|
||||
|  | W025 Warning against counter-rotating rollers | | |
|
||||
|  | W016 Warning of toxic substances | | |
|
||||
|
||||
<br><br><br>
|
||||
|
||||
@@ -296,15 +212,15 @@ Accuracy class: 2
|
||||
|
||||
| Prohibition signs Pictogram | Pictogram description | International pictograms / other Pictogram | Pictogram Description |
|
||||
| ----------------------------------------------------------------------- | --------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------- | --------------------------------------------------------------------------- |
|
||||
| <img src="/images/manuals/winder/p001.jpeg" width="158" height="158"/> | P001 General prohibition sign | <img src="/images/manuals/winder/f001.jpeg" width="158" height="158"> | F001 Fire extinguisher |
|
||||
| <img src="/images/manuals/winder/p002.jpeg" width="158" height="158"/> | P002 No smoking | <img src="/images/manuals/winder/ANIS_danger.jpeg" width="232" height="60"> | ANSI Z535.3 - DANGER |
|
||||
| <img src="/images/manuals/winder/p003.jpeg" width="158" height="158"/> | P003 No open flames; fire, open source of ignition and smoking prohibited | <img src="/images/manuals/winder/ANIS_warning.jpeg" width="232" height="60"> | ANSI Z535.3 - WARNING |
|
||||
| <img src="/images/manuals/winder/p006.jpeg" width="158" height="158"/> | P006 Prohibited for industrial trucks | <img src="/images/manuals/winder/ANIS_caution.jpeg" width="232" height="60"> | ANSI Z535.3 - CAUTION |
|
||||
| <img src="/images/manuals/winder/p009.jpeg" width="158" height="158"/> | P009 Climbing prohibited - In the sense of climbing prohibited for unauthorized persons | <img src="/images/manuals/winder/ANIS_notice.jpeg" width="232" height="60"> | ANSI Z535.3 - NOTICE |
|
||||
| <img src="/images/manuals/winder/p022.jpeg" width="158" height="158"/> | P022 Eating and drinking prohibited | <img src="/images/manuals/winder/ANIS_safetyinstructions.jpeg" width="232" height="60"> | ANSI Z535.3 - SAFETY INSTRUCTIONS |
|
||||
| <img src="/images/manuals/winder/p024.jpeg" width="158" height="158"/> | P024 Entering the area prohibited | <img src="/images/manuals/winder/trashcan.jpeg" width="158" height="158"> | Symbol for separate collection of waste electrical and electronic equipment |
|
||||
| <img src="/images/manuals/winder/p031.jpeg" width="158" height="158"/> | P031 Switching prohibited | | |
|
||||
| <img src="/images/manuals/winder/D-p006.jpeg" width="158" height="158"/> | D-P006 Access prohibited for unauthorized persons | | |
|
||||
| <img src="images/manuals/winder/p001.jpeg" width="158" height="158"/> | P001 General prohibition sign | <img src="images/manuals/winder/f001.jpeg" width="158" height="158"> | F001 Fire extinguisher |
|
||||
| <img src="images/manuals/winder/p002.jpeg" width="158" height="158"/> | P002 No smoking | <img src="images/manuals/winder/ANIS_danger.jpeg" width="232" height="60"> | ANSI Z535.3 - DANGER |
|
||||
| <img src="images/manuals/winder/p003.jpeg" width="158" height="158"/> | P003 No open flames; fire, open source of ignition and smoking prohibited | <img src="images/manuals/winder/ANIS_warning.jpeg" width="232" height="60"> | ANSI Z535.3 - WARNING |
|
||||
| <img src="images/manuals/winder/p006.jpeg" width="158" height="158"/> | P006 Prohibited for industrial trucks | <img src="images/manuals/winder/ANIS_caution.jpeg" width="232" height="60"> | ANSI Z535.3 - CAUTION |
|
||||
| <img src="images/manuals/winder/p009.jpeg" width="158" height="158"/> | P009 Climbing prohibited - In the sense of climbing prohibited for unauthorized persons | <img src="images/manuals/winder/ANIS_notice.jpeg" width="232" height="60"> | ANSI Z535.3 - NOTICE |
|
||||
| <img src="images/manuals/winder/p022.jpeg" width="158" height="158"/> | P022 Eating and drinking prohibited | <img src="images/manuals/winder/ANIS_safetyinstructions.jpeg" width="232" height="60"> | ANSI Z535.3 - SAFETY INSTRUCTIONS |
|
||||
| <img src="images/manuals/winder/p024.jpeg" width="158" height="158"/> | P024 Entering the area prohibited | <img src="images/manuals/winder/trashcan.jpeg" width="158" height="158"> | Symbol for separate collection of waste electrical and electronic equipment |
|
||||
| <img src="images/manuals/winder/p031.jpeg" width="158" height="158"/> | P031 Switching prohibited | | |
|
||||
| <img src="images/manuals/winder/D-p006.jpeg" width="158" height="158"/> | D-P006 Access prohibited for unauthorized persons | | |
|
||||
|
||||
<br><br><br>
|
||||
|
||||
@@ -342,7 +258,7 @@ In addition, the following procedures must be regarded as misuse:
|
||||
|
||||
<br><br><br>
|
||||
|
||||

|
||||

|
||||
|
||||
<br><br><br>
|
||||
|
||||
@@ -370,7 +286,7 @@ Hazardous movements are brought to a controlled standstill as quickly as possibl
|
||||
|
||||
<br>
|
||||
|
||||
<img src="/images/manuals/winder/emergency_stop.jpeg" width="300" height="300"/>
|
||||
<img src="images/manuals/winder/emergency_stop.jpeg" width="300" height="300"/>
|
||||
|
||||
<br>
|
||||
|
||||
@@ -427,8 +343,8 @@ Depending on the activity or the duration of the activity, the operator must det
|
||||
<br><br>
|
||||
| Picture | Danger |
|
||||
|---------|--------|
|
||||
| <img src="/images/manuals/winder/w001.jpeg" width="150" height="150"/> | **Risk of injury due to improper transport** <br> Be careful when loading the machine, do not place any limbs under the parts when setting them down, do not stand under suspended loads or between moving loads and walls or machine parts. Wear personal protective equipment: safety shoes, protective work clothing, protective gloves. |
|
||||
| <img src="/images/manuals/winder/w001.jpeg" width="150" height="150"/> | **Danger to life due to incorrect installation and commissioning!** <br> Errors during installation can lead to life-threatening situations or cause considerable damage to property. Installation and commissioning may only be carried out by the manufacturer's employees. |
|
||||
| <img src="images/manuals/winder/w001.jpeg" width="150" height="150"/> | **Risk of injury due to improper transport** <br> Be careful when loading the machine, do not place any limbs under the parts when setting them down, do not stand under suspended loads or between moving loads and walls or machine parts. Wear personal protective equipment: safety shoes, protective work clothing, protective gloves. |
|
||||
| <img src="images/manuals/winder/w001.jpeg" width="150" height="150"/> | **Danger to life due to incorrect installation and commissioning!** <br> Errors during installation can lead to life-threatening situations or cause considerable damage to property. Installation and commissioning may only be carried out by the manufacturer's employees. |
|
||||
|
||||
<br><br>
|
||||
|
||||
@@ -448,7 +364,7 @@ The machines are delivered with the newest version of the software installed. Up
|
||||
|
||||
<br>
|
||||
|
||||

|
||||

|
||||
|
||||
<br>
|
||||
|
||||
@@ -496,7 +412,7 @@ Then start the machine as follows:
|
||||
|
||||
<br>
|
||||
|
||||

|
||||

|
||||
|
||||
<br>
|
||||
|
||||
@@ -519,7 +435,7 @@ The **padding** parameter creates a safety margin that prevents the filament fro
|
||||
|
||||
The default padding is 0.88mm, but you can adjust this value in the Control settings based on your filament diameter and winding requirements. Larger filaments may require more padding, while smaller filaments can use less padding for maximum spool utilization.
|
||||
|
||||

|
||||

|
||||
|
||||
<br>
|
||||
|
||||
@@ -532,7 +448,7 @@ The default padding is 0.88mm, but you can adjust this value in the Control sett
|
||||
|
||||
<br>
|
||||
|
||||

|
||||

|
||||
|
||||
<br>
|
||||
|
||||
@@ -598,7 +514,7 @@ All settings such as the position of the spool sides, the winding speed or the f
|
||||
|
||||
| Picture | Danger! |
|
||||
| --------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| <img src="/images/manuals/winder/w001.jpeg" width="150" height="150"/> | **Danger to life due to operation of the machine in faulty condition** <br> If safety-relevant changes or defects occur on the machine, shut down immediately, secure against restarting and report the process to the responsible office/person! |
|
||||
| <img src="images/manuals/winder/w001.jpeg" width="150" height="150"/> | **Danger to life due to operation of the machine in faulty condition** <br> If safety-relevant changes or defects occur on the machine, shut down immediately, secure against restarting and report the process to the responsible office/person! |
|
||||
|
||||
<br><br>
|
||||
|
||||
@@ -642,14 +558,14 @@ The polyurethane coating of the puller rollers will inevitably wear out over tim
|
||||
|
||||
Now remove the black plastic caps and simply pull out the puller rollers. To install the new rollers back on, slide them on the axes, put the plastic cap back on and screw the screws.
|
||||
|
||||
<img src="/images/manuals/winder/swapping_out_rollers.jpeg" width="300" height="400"/>
|
||||
<img src="images/manuals/winder/swapping_out_rollers.jpeg" width="300" height="400"/>
|
||||
|
||||
<br>
|
||||
<br>
|
||||
|
||||
| Picture | Caution! |
|
||||
| --------------------------------------------------------------------- | ------------------------------------------------------------------------- |
|
||||
| <img src="/images/manuals/winder/w001.jpeg" width="150" height="150"/> | Observe the instructions for maintenance work when servicing the machine! |
|
||||
| <img src="images/manuals/winder/w001.jpeg" width="150" height="150"/> | Observe the instructions for maintenance work when servicing the machine! |
|
||||
|
||||
<br>
|
||||
<br>
|
||||
@@ -714,7 +630,7 @@ The machine consists largely of aluminum and plastic and can be easily separated
|
||||
|
||||
| Picture | Disposal Instructions |
|
||||
| ------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| <img src="/images/manuals/winder/trashcan.jpeg" width="150" height="150"/> | Appliances with this logo on the packaging or on the appliance must be disposed of separately. These devices must not be disposed of with normal household waste. You are responsible for ensuring that any electrical or electronic waste is disposed of at the appropriate locations, e.g., recycling centers. |
|
||||
| <img src="images/manuals/winder/trashcan.jpeg" width="150" height="150"/> | Appliances with this logo on the packaging or on the appliance must be disposed of separately. These devices must not be disposed of with normal household waste. You are responsible for ensuring that any electrical or electronic waste is disposed of at the appropriate locations, e.g., recycling centers. |
|
||||
|
||||
<br><br><br>
|
||||
|
||||
@@ -722,15 +638,15 @@ The machine consists largely of aluminum and plastic and can be easily separated
|
||||
|
||||
| Picture | Danger! |
|
||||
| --------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| <img src="/images/manuals/winder/w001.jpeg" width="150" height="150"/> | **Danger of getting caught, pulled in or pulled along by moving parts of the machine** <br> Do not wear loose long hair, loose clothing or jewelry! There is always a risk of getting caught, pulled in or pulled along by moving parts! |
|
||||
| <img src="/images/manuals/winder/w001.jpeg" width="150" height="150"/> | **Operation only permitted with protective devices fitted!** <br> The winder may only be operated with properly fitted and functioning safety guards. The operator is responsible for ensuring that all protective devices are properly fitted. |
|
||||
| <img src="/images/manuals/winder/w001.jpeg" width="150" height="150"/> | **Danger of cutting tensioned filament** <br> The stretched filament must not be touched between the puller and the spool. Danger of cutting! Do not wind any pointed, sharp-edged or particularly thin materials! |
|
||||
| <img src="/images/manuals/winder/w012.jpeg" width="150" height="150"/> | **Electric shock** <br> Before carrying out any repair, set-up or maintenance work, the winder must be de-energised by disconnecting the mains plug! Pull out the mains plug! |
|
||||
| <img src="/images/manuals/winder/w012.jpeg" width="150" height="150"/> | **Electrostatic charges on the puller and spool** <br> Friction of the filament on the puller rollers and on the spool can lead to electrostatic charges. There is a risk of minor electric shocks! Do not touch the affected areas, but keep a sufficient distance! |
|
||||
| <img src="/images/manuals/winder/w018.jpeg" width="150" height="150"/> | **Automatic speed adjustment of puller, reel and traverse** <br> The bobbin automatically adjusts its speed to keep the winding pressure constant. The traverse automatically adjusts its speed to the reel in order to maintain the same travel distance per revolution. |
|
||||
| <img src="/images/manuals/winder/w025.jpeg" width="150" height="150"/> | **Feeding of body parts** <br> Extreme caution is required in the area of the puller and spool axle! There is an acute danger of getting caught or pulled in! Make sure you wear close-fitting clothing! Always keep the emergency stop switch within easy reach so that it can be activated immediately in an emergency. |
|
||||
| <img src="/images/manuals/winder/w024.jpeg" width="150" height="150"/> | **Risk of crushing when threading the filament** <br> Exercise extreme caution when threading the filament through the puller rollers! There is a risk of crushing between the upper and lower pair of rollers. |
|
||||
| <img src="/images/manuals/winder/w024.jpeg" width="150" height="150"/> | **Risk of crushing between the traversing unit and machine housing** <br> Exercise extreme caution when moving the traverser towards the housing! Do not place any limbs in the gap! |
|
||||
| <img src="images/manuals/winder/w001.jpeg" width="150" height="150"/> | **Danger of getting caught, pulled in or pulled along by moving parts of the machine** <br> Do not wear loose long hair, loose clothing or jewelry! There is always a risk of getting caught, pulled in or pulled along by moving parts! |
|
||||
| <img src="images/manuals/winder/w001.jpeg" width="150" height="150"/> | **Operation only permitted with protective devices fitted!** <br> The winder may only be operated with properly fitted and functioning safety guards. The operator is responsible for ensuring that all protective devices are properly fitted. |
|
||||
| <img src="images/manuals/winder/w001.jpeg" width="150" height="150"/> | **Danger of cutting tensioned filament** <br> The stretched filament must not be touched between the puller and the spool. Danger of cutting! Do not wind any pointed, sharp-edged or particularly thin materials! |
|
||||
| <img src="images/manuals/winder/w012.jpeg" width="150" height="150"/> | **Electric shock** <br> Before carrying out any repair, set-up or maintenance work, the winder must be de-energised by disconnecting the mains plug! Pull out the mains plug! |
|
||||
| <img src="images/manuals/winder/w012.jpeg" width="150" height="150"/> | **Electrostatic charges on the puller and spool** <br> Friction of the filament on the puller rollers and on the spool can lead to electrostatic charges. There is a risk of minor electric shocks! Do not touch the affected areas, but keep a sufficient distance! |
|
||||
| <img src="images/manuals/winder/w018.jpeg" width="150" height="150"/> | **Automatic speed adjustment of puller, reel and traverse** <br> The bobbin automatically adjusts its speed to keep the winding pressure constant. The traverse automatically adjusts its speed to the reel in order to maintain the same travel distance per revolution. |
|
||||
| <img src="images/manuals/winder/w025.jpeg" width="150" height="150"/> | **Feeding of body parts** <br> Extreme caution is required in the area of the puller and spool axle! There is an acute danger of getting caught or pulled in! Make sure you wear close-fitting clothing! Always keep the emergency stop switch within easy reach so that it can be activated immediately in an emergency. |
|
||||
| <img src="images/manuals/winder/w024.jpeg" width="150" height="150"/> | **Risk of crushing when threading the filament** <br> Exercise extreme caution when threading the filament through the puller rollers! There is a risk of crushing between the upper and lower pair of rollers. |
|
||||
| <img src="images/manuals/winder/w024.jpeg" width="150" height="150"/> | **Risk of crushing between the traversing unit and machine housing** <br> Exercise extreme caution when moving the traverser towards the housing! Do not place any limbs in the gap! |
|
||||
|
||||
# 4 Glossary and Definitions
|
||||
|
||||
@@ -771,7 +687,7 @@ The following persons may not work on the machine:
|
||||
|
||||
| Picture | Danger! |
|
||||
| --------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| <img src="/images/manuals/winder/w001.jpeg" width="150" height="150"/> | **Risk of injury if insufficiently qualified!** <br> Improper handling can lead to considerable personal injury and damage to property. Special activities may only be carried out by the persons specified in the relevant sections of these instructions. |
|
||||
| <img src="images/manuals/winder/w001.jpeg" width="150" height="150"/> | **Risk of injury if insufficiently qualified!** <br> Improper handling can lead to considerable personal injury and damage to property. Special activities may only be carried out by the persons specified in the relevant sections of these instructions. |
|
||||
|
||||
<br>
|
||||
<br>
|
||||
@@ -800,12 +716,12 @@ The following persons may not work on the machine:
|
||||
|
||||
## Electrical Diagrams
|
||||
|
||||

|
||||

|
||||
|
||||

|
||||

|
||||
|
||||

|
||||

|
||||
|
||||

|
||||

|
||||
|
||||

|
||||

|
||||
@@ -1,67 +0,0 @@
|
||||
import React from "react";
|
||||
import { Markdown } from "./Markdown";
|
||||
|
||||
type ManualMarkdownProps = {
|
||||
/**
|
||||
* The markdown content to render
|
||||
*/
|
||||
markdownContent: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* A specialized Markdown component for rendering machine manuals.
|
||||
*
|
||||
* This component transforms image paths in markdown to work correctly in the Electron app.
|
||||
* Manual markdown files are stored in `docs/machines/manuals/` with relative image paths
|
||||
* pointing to `../../../../electron/public/images/manuals/[machine-name]/...`
|
||||
*
|
||||
* This component transforms those paths to absolute paths `/images/manuals/[machine-name]/...`
|
||||
* that work correctly in the Electron app's context.
|
||||
*
|
||||
* Path transformation examples:
|
||||
* - `../../../../electron/public/images/manuals/winder/image.png` → `/images/manuals/winder/image.png`
|
||||
* - `images/manuals/winder/image.png` → `/images/manuals/winder/image.png`
|
||||
*/
|
||||
export function ManualMarkdown({ markdownContent }: ManualMarkdownProps) {
|
||||
const transformedContent = React.useMemo(() => {
|
||||
// Transform image paths to work in Electron context
|
||||
// Pattern 1: ../../../../electron/public/images/manuals/... → /images/manuals/...
|
||||
let content = markdownContent.replace(
|
||||
/!\[([^\]]*)\]\(\.\.\/\.\.\/\.\.\/\.\.\/electron\/public\/(images\/manuals\/[^)]+)\)/g,
|
||||
"",
|
||||
);
|
||||
|
||||
// Pattern 2: ../../../electron/public/images/manuals/... → /images/manuals/... (backward compatibility)
|
||||
content = content.replace(
|
||||
/!\[([^\]]*)\]\(\.\.\/\.\.\/\.\.\/electron\/public\/(images\/manuals\/[^)]+)\)/g,
|
||||
"",
|
||||
);
|
||||
|
||||
// Pattern 3: images/manuals/... → /images/manuals/... (for backward compatibility)
|
||||
content = content.replace(
|
||||
/!\[([^\]]*)\]\(images\/manuals\/([^)]+)\)/g,
|
||||
"",
|
||||
);
|
||||
|
||||
// Pattern 4: Handle <img> tags with src attribute (new path)
|
||||
content = content.replace(
|
||||
/<img\s+([^>]*)src="\.\.\/\.\.\/\.\.\/\.\.\/electron\/public\/(images\/manuals\/[^"]+)"/g,
|
||||
'<img $1src="/$2"',
|
||||
);
|
||||
|
||||
// Pattern 5: Handle <img> tags with src attribute (old path)
|
||||
content = content.replace(
|
||||
/<img\s+([^>]*)src="\.\.\/\.\.\/\.\.\/electron\/public\/(images\/manuals\/[^"]+)"/g,
|
||||
'<img $1src="/$2"',
|
||||
);
|
||||
|
||||
content = content.replace(
|
||||
/<img\s+([^>]*)src="images\/manuals\/([^"]+)"/g,
|
||||
'<img $1src="/images/manuals/$2"',
|
||||
);
|
||||
|
||||
return content;
|
||||
}, [markdownContent]);
|
||||
|
||||
return <Markdown text={transformedContent} />;
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
import React from "react";
|
||||
import { Markdown } from "./Markdown";
|
||||
import { ManualMarkdown } from "./ManualMarkdown";
|
||||
import { MarkdownToc } from "./MarkdownToc";
|
||||
|
||||
type MarkdownWithTocProps = {
|
||||
@@ -27,11 +26,6 @@ type MarkdownWithTocProps = {
|
||||
* @default "top-4"
|
||||
*/
|
||||
stickyTop?: string;
|
||||
/**
|
||||
* Whether this is a machine manual (enables image path transformation)
|
||||
* @default false
|
||||
*/
|
||||
isManual?: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -50,19 +44,12 @@ export function MarkdownWithToc({
|
||||
tocWidth = "w-64",
|
||||
gap = "gap-6",
|
||||
stickyTop = "top-6",
|
||||
isManual = false,
|
||||
}: MarkdownWithTocProps) {
|
||||
const MarkdownComponent = isManual ? ManualMarkdown : Markdown;
|
||||
|
||||
return (
|
||||
<div className={`flex ${gap}`}>
|
||||
{/* Main Content */}
|
||||
<div className="min-w-0 flex-1">
|
||||
{isManual ? (
|
||||
<ManualMarkdown markdownContent={markdownContent} />
|
||||
) : (
|
||||
<Markdown text={markdownContent} />
|
||||
)}
|
||||
<Markdown text={markdownContent} />
|
||||
</div>
|
||||
|
||||
{/* Table of Contents Sidebar */}
|
||||
|
||||
@@ -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":
|
||||
@@ -105,11 +106,11 @@ export function renderUnitSymbol(unit: Unit | undefined): string {
|
||||
}
|
||||
}
|
||||
|
||||
// this function will add prefix/suffix to the valueString but without unit symbol.
|
||||
// example
|
||||
// value: 10.0
|
||||
// -> valueString: "10" (custom input)
|
||||
// this function will add prefix/suffix to the valueString but without unit symbol
|
||||
// -> "10°"
|
||||
// const f = 3.1415;
|
||||
// renderUnitSyntax(f.toFixed(2), "deg")
|
||||
// result: "3.14°"
|
||||
export function renderUnitSyntax(
|
||||
valueString: string | undefined,
|
||||
unit: Unit | undefined,
|
||||
@@ -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",
|
||||
|
||||
@@ -1,15 +1,12 @@
|
||||
import { Page } from "@/components/Page";
|
||||
import { MarkdownWithToc } from "@/components/MarkdownWithToc";
|
||||
import React from "react";
|
||||
import extruderManualContent from "@root/docs/machines/manuals/extruder.md?raw";
|
||||
import extruderManualContent from "@/assets/markdown/extruder/manual.md?raw";
|
||||
|
||||
export function ExtruderV2ManualPage() {
|
||||
return (
|
||||
<Page>
|
||||
<MarkdownWithToc
|
||||
markdownContent={extruderManualContent}
|
||||
isManual={true}
|
||||
/>
|
||||
<MarkdownWithToc markdownContent={extruderManualContent} />
|
||||
</Page>
|
||||
);
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Page } from "@/components/Page";
|
||||
import { MarkdownWithToc } from "@/components/MarkdownWithToc";
|
||||
import React from "react";
|
||||
import extruderManualContent from "@root/docs/machines/manuals/extruder.md?raw";
|
||||
import extruderManualContent from "@/assets/markdown/extruder/manual.md?raw";
|
||||
|
||||
export function ExtruderV3ManualPage() {
|
||||
return (
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { Page } from "@/components/Page";
|
||||
import { MarkdownWithToc } from "@/components/MarkdownWithToc";
|
||||
import React from "react";
|
||||
import laserManualContent from "@root/docs/machines/manuals/laser.md?raw";
|
||||
import laserManualContent from "@/assets/markdown/laser/manual.md?raw";
|
||||
|
||||
export function Laser1ManualPage() {
|
||||
return (
|
||||
<Page>
|
||||
<MarkdownWithToc markdownContent={laserManualContent} isManual={true} />
|
||||
<MarkdownWithToc markdownContent={laserManualContent} />
|
||||
</Page>
|
||||
);
|
||||
}
|
||||
@@ -1,12 +1,12 @@
|
||||
import { Page } from "@/components/Page";
|
||||
import { MarkdownWithToc } from "@/components/MarkdownWithToc";
|
||||
import React from "react";
|
||||
import mockManualContent from "@root/docs/machines/manuals/mock.md?raw";
|
||||
import winderManualContent from "@/assets/markdown/winder/manual.md?raw";
|
||||
|
||||
export function Mock1ManualPage() {
|
||||
return (
|
||||
<Page>
|
||||
<MarkdownWithToc markdownContent={mockManualContent} isManual={true} />
|
||||
<MarkdownWithToc markdownContent={winderManualContent} />
|
||||
</Page>
|
||||
);
|
||||
}
|
||||
@@ -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,135 @@
|
||||
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 { initialTimeSeries: voltage, insert: addVoltage } = createTimeSeries();
|
||||
const { initialTimeSeries: current, insert: addCurrent } = createTimeSeries();
|
||||
|
||||
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,12 +1,12 @@
|
||||
import { Page } from "@/components/Page";
|
||||
import { MarkdownWithToc } from "@/components/MarkdownWithToc";
|
||||
import React from "react";
|
||||
import winderManualContent from "@root/docs/machines/manuals/winder.md?raw";
|
||||
import winderManualContent from "@/assets/markdown/winder/manual.md?raw";
|
||||
|
||||
export function Winder2ManualPage() {
|
||||
return (
|
||||
<Page>
|
||||
<MarkdownWithToc markdownContent={winderManualContent} isManual={true} />
|
||||
<MarkdownWithToc markdownContent={winderManualContent} />
|
||||
</Page>
|
||||
);
|
||||
}
|
||||
@@ -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]),
|
||||
]),
|
||||
]),
|
||||
|
||||
@@ -16,8 +16,7 @@
|
||||
"strict": true,
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["./src/*"],
|
||||
"@root/*": ["../*"]
|
||||
"@/*": ["./src/*"]
|
||||
},
|
||||
"outDir": "dist",
|
||||
"moduleResolution": "node",
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
pub mod ip20_ec_di8_do8;
|
||||
pub mod wago_750_1506;
|
||||
pub mod wago_750_455;
|
||||
pub mod wago_750_501;
|
||||
pub mod wago_750_652;
|
||||
@@ -0,0 +1,100 @@
|
||||
use units::{ElectricCurrent, electric_current::milliampere};
|
||||
|
||||
use crate::{
|
||||
devices::{
|
||||
DynamicEthercatDevice, EthercatDevice, EthercatDeviceProcessing, EthercatDeviceUsed,
|
||||
EthercatDynamicPDO, Module, NewEthercatDevice, SubDeviceProductTuple,
|
||||
},
|
||||
helpers::signing_converter_u16::U16SigningConverter,
|
||||
io::{
|
||||
analog_input::{AnalogInputDevice, AnalogInputInput, physical::AnalogInputRange},
|
||||
digital_input::DigitalInputInput,
|
||||
},
|
||||
};
|
||||
|
||||
Wago750_455Presentation {
|
||||
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Wago750_455 {
|
||||
configuration: (),
|
||||
txpdo: Wago750_455RxPdo,
|
||||
isUsed: bool,
|
||||
}
|
||||
|
||||
pub struct Wago750_455Configuration {
|
||||
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum Wago750_455InputPort {
|
||||
AI1,
|
||||
AI2,
|
||||
AI3,
|
||||
AI4,
|
||||
}
|
||||
|
||||
impl From<Wago750_455InputPort> for usize {
|
||||
fn from(value: Wago750_455InputPort) -> Self {
|
||||
match value {
|
||||
Wago750_455InputPort::AI1 => 0,
|
||||
Wago750_455InputPort::AI2 => 1,
|
||||
Wago750_455InputPort::AI3 => 2,
|
||||
Wago750_455InputPort::AI4 => 3,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Default)]
|
||||
/**
|
||||
# Data Word, 16 bits
|
||||
- B15 ... B4 hold the measured value (B15 is the sign bit which should always be 0)
|
||||
- B3 and B2 are 00 when measuring <~3.0mA, 11 when measuring >~21.0mA and undefined inbetween.
|
||||
- B1 and B0 are 11' on an overflow, underflow or wire break. 00' otherwise
|
||||
*/
|
||||
pub struct Wago750_455RxPdo {
|
||||
/** Low Byte: D0, High Byte: D1 */
|
||||
channel_1: u16,
|
||||
/** Low Byte: D2, High Byte: D3 */
|
||||
channel_2: u16,
|
||||
/** Low Byte: D4, High Byte: D5 */
|
||||
channel_3: u16,
|
||||
/** Low Byte: D6, High Byte: D7 */
|
||||
channel_4: u16,
|
||||
}
|
||||
|
||||
impl AnalogInputDevice<Wago750_455InputPort> for Wago750_455 {
|
||||
fn get_input(&self, port: Wago750_455InputPort) -> AnalogInputInput {
|
||||
let raw_value = match port {
|
||||
Wago750_455InputPort::AI1 => self.txpdo.channel_1,
|
||||
Wago750_455InputPort::AI2 => self.txpdo.channel_2,
|
||||
Wago750_455InputPort::AI3 => self.txpdo.channel_3,
|
||||
Wago750_455InputPort::AI4 => self.txpdo.channel_4,
|
||||
};
|
||||
|
||||
let signing_bit = raw_value & 0x8000;
|
||||
// The 4 least significant bits are reserved and diagnostic and the MSB is the signing bit.
|
||||
let shifted_raw_value = (raw_value & 0x7FF0) >> 4;
|
||||
// Since only 11 bits actually represent the measurement (ignoring the signing bit) only 2048 different states can be measured.
|
||||
let prepared_value = U16SigningConverter::load_raw(shifted_raw_value | signing_bit);
|
||||
|
||||
let value = prepared_value.as_signed() as i16;
|
||||
|
||||
let normalized = f32::from(value) / f32::from(2047);
|
||||
|
||||
AnalogInputInput {
|
||||
normalized,
|
||||
wiring_error: false,
|
||||
}
|
||||
}
|
||||
|
||||
fn analog_input_range(&self) -> AnalogInputRange {
|
||||
AnalogInputRange::Current {
|
||||
min: ElectricCurrent::new::<milliampere>(4.0),
|
||||
max: ElectricCurrent::new::<milliampere>(20.0),
|
||||
min_raw: 0,
|
||||
max_raw: 2047,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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!()
|
||||
));
|
||||
}
|
||||
@@ -51,7 +51,7 @@ impl MachineNewTrait for AnalogInputTestMachine {
|
||||
api_sender: sender,
|
||||
machine_identification_unique: params.get_machine_identification_unique(),
|
||||
main_sender: params.main_thread_channel.clone(),
|
||||
namespace: namespace,
|
||||
namespace,
|
||||
|
||||
last_measurement: Instant::now(),
|
||||
measurement_rate_hz: 1.0,
|
||||
|
||||
@@ -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!()
|
||||
));
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
@@ -257,7 +259,7 @@ pub fn get_ethercat_device_by_index<'maindevice>(
|
||||
.clone())
|
||||
}
|
||||
|
||||
pub trait MachineNewTrait {
|
||||
pub trait MachineNewTrait: Machine {
|
||||
fn new(params: &MachineNewParams<'_, '_, '_, '_, '_, '_, '_>) -> 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>>;
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
Machine, MachineNewParams, MachineNewTrait, machine_identification::MachineIdentification,
|
||||
};
|
||||
|
||||
#[cfg(not(feature = "mock-machine"))]
|
||||
@@ -44,7 +45,7 @@ impl MachineRegistry {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn register<T: Machine + 'static>(
|
||||
pub fn register<T: MachineNewTrait + 'static>(
|
||||
&mut self,
|
||||
machine_identficiation: MachineIdentification,
|
||||
) {
|
||||
@@ -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
|
||||
|
||||
@@ -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 / 30.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();
|
||||
|
||||
@@ -108,6 +108,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";
|
||||
@@ -314,9 +321,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;
|
||||
|
||||
|
||||
@@ -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,
|
||||
@@ -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_machines_if_not_exists(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(
|
||||
|
||||
@@ -481,9 +481,9 @@ pub async fn setup_loop(
|
||||
main_namespace.emit(MainNamespaceEvents::EthercatDevicesEvent(event));
|
||||
}
|
||||
|
||||
return Ok(EthercatSetup {
|
||||
Ok(EthercatSetup {
|
||||
devices,
|
||||
group: group_op,
|
||||
maindevice,
|
||||
});
|
||||
})
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
use super::LaserMachine;
|
||||
use control_core::machines::new::MachineAct;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
/// Implements the `MachineAct` trait for the `LaserMachine`.
|
||||
///
|
||||
/// # Parameters
|
||||
/// - `_now_ts`: The current timestamp of type `Instant`.
|
||||
///
|
||||
/// # Returns
|
||||
/// A pinned `Future` that resolves to `()` and is `Send`-safe. The future encapsulates the asynchronous behavior of the `act` method.
|
||||
///
|
||||
/// # Description
|
||||
/// This method is called to perform periodic actions for the `LaserMachine`. Specifically:
|
||||
/// - It checks if the time elapsed since the last measurement emission exceeds 20 milliseconds.
|
||||
/// - If the condition is met, it asynchronously emits live values at 60 FPS.
|
||||
///
|
||||
/// The method ensures that the diameter value is updated approximately 60 times per second.
|
||||
///
|
||||
impl MachineAct for LaserMachine {
|
||||
fn act(&mut self, now: Instant) {
|
||||
self.update();
|
||||
if self.did_change_state {
|
||||
self.emit_state();
|
||||
}
|
||||
// more than 33ms have passed since last emit (30 "fps" target)
|
||||
if now.duration_since(self.last_measurement_emit) > Duration::from_secs_f64(1.0 / 30.0) {
|
||||
self.emit_live_values();
|
||||
self.last_measurement_emit = now;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,7 @@
|
||||
use crate::metrics::collector::{RuntimeMetricsConfig, spawn_runtime_metrics_sampler};
|
||||
use crate::{
|
||||
metrics::collector::{RuntimeMetricsConfig, spawn_runtime_metrics_sampler},
|
||||
socketio::main_namespace::machines_event::MachineObj,
|
||||
};
|
||||
use machines::{
|
||||
AsyncThreadMessage, MachineConnection, MachineNewHardware, MachineNewHardwareSerial,
|
||||
MachineNewParams, SerialDevice, SerialDeviceIdentification, SerialDeviceNew,
|
||||
@@ -26,7 +29,6 @@ use smol::{
|
||||
future,
|
||||
lock::RwLock,
|
||||
};
|
||||
use socketio::main_namespace::machines_event::MachineObj;
|
||||
use socketioxide::extract::SocketRef;
|
||||
use std::{collections::HashMap, sync::Arc, time::Duration};
|
||||
|
||||
@@ -38,6 +40,7 @@ use crate::{
|
||||
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 +52,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;
|
||||
@@ -80,7 +84,7 @@ pub async fn add_serial_device(
|
||||
.clone();
|
||||
|
||||
let new_machine = machine_registry.new_machine(&MachineNewParams {
|
||||
device_group: &vec![device_identification_identified.clone()],
|
||||
device_group: &vec![device_identification_identified],
|
||||
hardware: &MachineNewHardware::Serial(&hardware),
|
||||
socket_queue_tx,
|
||||
namespace: None,
|
||||
@@ -350,6 +354,8 @@ fn main() {
|
||||
#[cfg(not(feature = "mock-machine"))]
|
||||
smol::spawn(start_interface_discovery(app_state.clone(), sender)).detach();
|
||||
|
||||
smol::spawn(start_modbus_tcp_discovery(app_state.clone())).detach();
|
||||
|
||||
smol::block_on(async {
|
||||
send_empty_machines_event(app_state.clone()).await;
|
||||
send_ethercat_discovering(app_state.clone()).await;
|
||||
|
||||
@@ -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";
|
||||
}
|
||||
|
||||