Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5fbdf7b9ab | ||
|
|
85bd4b0d96 | ||
|
|
dfce760521 | ||
|
|
30c97bf9aa | ||
|
|
2c8373adcf | ||
|
|
d08972254c | ||
|
|
a76d39e0cd | ||
|
|
a74d0dbb80 | ||
|
|
17ab24739d | ||
|
|
09f0ea3454 | ||
|
|
0dd2494ea0 | ||
|
|
57c1cda229 | ||
|
|
8c2b20b148 | ||
|
|
12e7f245ce | ||
|
|
671e36fa0f | ||
|
|
1ffd7eade6 | ||
|
|
703bbfc9e6 | ||
|
|
057afb0b2e | ||
|
|
94bd29c734 | ||
|
|
d078b7b143 | ||
|
|
acbeb98f4f | ||
|
|
cab8bf062d | ||
|
|
9e04a7fa33 | ||
|
|
c92d4847ca | ||
|
|
aceeaa08c2 | ||
|
|
35946d0c88 | ||
|
|
a5b9a0b1d5 | ||
|
|
412d625138 | ||
|
|
1ec20074e9 | ||
|
|
4d796f8422 | ||
|
|
1406d6c724 | ||
|
|
33441b95a5 | ||
|
|
8a3f312675 | ||
|
|
5c327428d6 | ||
|
|
4e4d31cad1 | ||
|
|
031fdf25a9 | ||
|
|
857cda1455 | ||
|
|
de5f3a5cfc | ||
|
|
2da7af522c | ||
|
|
712284e2f0 | ||
|
|
402c47a81b | ||
|
|
54a000a280 | ||
|
|
41fe118144 | ||
|
|
aaa6ff604f | ||
|
|
a350bb378f | ||
|
|
aa2212820e | ||
|
|
578e70119d | ||
|
|
e8796e00af | ||
|
|
735f27770a | ||
|
|
700a255157 | ||
|
|
ec06275c08 | ||
|
|
11d0c8bb53 | ||
|
|
535f24f1d1 | ||
|
|
172913d8ec | ||
|
|
0f99c54587 | ||
|
|
f89d70c5f6 | ||
|
|
0fc070be69 | ||
|
|
b93eaf530e | ||
|
|
76d8504651 | ||
|
|
06acd43ff9 | ||
|
|
870090c950 | ||
|
|
65777f10ab | ||
|
|
67d145de56 | ||
|
|
d45d1ed01a | ||
|
|
0d6c7bc0a5 | ||
|
|
15f013f710 | ||
|
|
a32e26a041 | ||
|
|
41c7e1f74e | ||
|
|
5a856e6e2b | ||
|
|
8ca16d447e | ||
|
|
b99549895b | ||
|
|
07cbaa4518 | ||
|
|
c78f5ff5a0 | ||
|
|
33946a6fa3 | ||
|
|
02dc67bb85 | ||
|
|
b5ae86b19c | ||
|
|
35567b967f | ||
|
|
c45718e670 | ||
|
|
74dcd027e0 | ||
|
|
c793d3fa50 | ||
|
|
04fdc8a9d1 | ||
|
|
fd702ff0bc | ||
|
|
f25bf71753 | ||
|
|
4c33873095 | ||
|
|
8dbcb5efc3 | ||
|
|
36426aafe0 | ||
|
|
21cf9314be | ||
|
|
4f2af7b90d | ||
|
|
06795dcf78 | ||
|
|
1241d0a4a7 | ||
|
|
3791ee785d | ||
|
|
7341dee976 |
No files matched your search
@@ -3,50 +3,73 @@ name: Electron
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- "**"
|
||||
- master
|
||||
pull_request:
|
||||
branches:
|
||||
- "*"
|
||||
- master
|
||||
|
||||
defaults:
|
||||
run:
|
||||
working-directory: electron
|
||||
|
||||
jobs:
|
||||
build-electron:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
defaults:
|
||||
run:
|
||||
working-directory: electron
|
||||
|
||||
steps:
|
||||
- name: Setup Node.js
|
||||
- &checkout
|
||||
name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- &setup-node
|
||||
name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "18" # or '20' for latest LTS
|
||||
node-version: 20
|
||||
cache: "npm"
|
||||
cache-dependency-path: |
|
||||
electron/package-lock.json
|
||||
|
||||
- uses: actions/checkout@v4
|
||||
- &install
|
||||
name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Cache NPM dependencies
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: |
|
||||
~/.npm
|
||||
node_modules/
|
||||
key: ${{ runner.os }}-npm-${{ hashFiles('**/package-lock.json') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-npm-
|
||||
|
||||
- name: Install
|
||||
run: npm i
|
||||
|
||||
- name: Build
|
||||
- name: Compile
|
||||
run: tsc
|
||||
|
||||
- name: Format
|
||||
run: npm run format
|
||||
- name: Build
|
||||
run: npm run build
|
||||
|
||||
- name: Lint
|
||||
run: npm run lint
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
needs: build
|
||||
|
||||
steps:
|
||||
- *checkout
|
||||
- *setup-node
|
||||
- *install
|
||||
|
||||
- name: Test
|
||||
run: npm run test
|
||||
|
||||
- name: Package
|
||||
run: npm run package
|
||||
format:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- *checkout
|
||||
- *setup-node
|
||||
- *install
|
||||
|
||||
- name: Format
|
||||
run: npm run format
|
||||
|
||||
lint:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- *checkout
|
||||
- *setup-node
|
||||
- *install
|
||||
|
||||
- name: Lint
|
||||
run: npm run lint
|
||||
@@ -0,0 +1,96 @@
|
||||
name: Nix CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
pull_request:
|
||||
branches:
|
||||
- master
|
||||
|
||||
env:
|
||||
NIX_PATH: nixpkgs=channel:nixos-25.05
|
||||
|
||||
jobs:
|
||||
electron:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- &checkout
|
||||
name: Check out repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- &nix-install
|
||||
uses: nixbuild/nix-quick-install-action@v34
|
||||
with:
|
||||
nix_conf: |
|
||||
keep-env-derivations = true
|
||||
keep-outputs = true
|
||||
|
||||
- uses: nix-community/cache-nix-action@v6
|
||||
with:
|
||||
primary-key: nix-electron-${{ runner.os }}-${{ hashFiles('**/*.nix', '**/flake.lock', './electron/package-lock.json') }}
|
||||
restore-prefixes-first-match: nix-${{ runner.os }}-
|
||||
purge: true
|
||||
purge-prefixes: nix-electron-${{ runner.os }}-
|
||||
|
||||
- name: Build Electron in Nix
|
||||
run: nix build ".#packages.x86_64-linux.electron"
|
||||
|
||||
rust:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- *checkout
|
||||
- *nix-install
|
||||
|
||||
- uses: nix-community/cache-nix-action@v6
|
||||
with:
|
||||
primary-key: nix-server-${{ runner.os }}-${{ hashFiles('**/*.nix', '**/flake.lock', './Cargo.lock') }}
|
||||
restore-prefixes-first-match: nix-${{ runner.os }}-
|
||||
purge: true
|
||||
purge-prefixes: nix-server-${{ runner.os }}-
|
||||
|
||||
- name: Build Server in Nix
|
||||
run: nix build ".#packages.x86_64-linux.server"
|
||||
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
needs: [electron, rust]
|
||||
|
||||
steps:
|
||||
- *checkout
|
||||
- *nix-install
|
||||
|
||||
- name: Use both Electron and Server Nix Store
|
||||
uses: nix-community/cache-nix-action@v6
|
||||
with:
|
||||
primary-key: nix-${{ runner.os }}-${{ hashFiles('**/*.nix', '**/flake.lock', './Cargo.lock', './electron/package-lock.json') }}
|
||||
|
||||
# if no hit on the primary key, merge individual caches
|
||||
restore-prefixes-all-matches: |
|
||||
nix-electron-${{ runner.os }}-
|
||||
nix-server-${{ runner.os }}-
|
||||
|
||||
- name: Build Flake and System Configuration
|
||||
run: nix build ".#nixosConfigurations.nixos.config.system.build.toplevel" --impure
|
||||
|
||||
format:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- *checkout
|
||||
- *nix-install
|
||||
|
||||
- name: Format
|
||||
run: nix run 'nixpkgs#nixfmt-classic' $(find . nixos -name '*.nix')
|
||||
|
||||
check:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- *checkout
|
||||
- *nix-install
|
||||
|
||||
- name: Build Flake and Configuration.nix
|
||||
run: nix flake check "." --impure
|
||||
@@ -3,9 +3,10 @@ name: Rust
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- "**"
|
||||
- master
|
||||
pull_request:
|
||||
branches: "*"
|
||||
branches:
|
||||
- master
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
@@ -16,14 +17,24 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Install libudev development package
|
||||
- &udev-install
|
||||
name: Install libudev development package
|
||||
run: sudo apt-get update && sudo apt-get install -y libudev-dev
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set Rust version
|
||||
run: rustup override set ${{ env.RUST_VERSION }}
|
||||
- name: Install Formatter
|
||||
run: rustup component add rustfmt
|
||||
- name: Cache Rust dependencies
|
||||
|
||||
- &checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- &setup-rust
|
||||
name: Install Rust toolchain
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: ${{ env.RUST_VERSION }}
|
||||
profile: minimal
|
||||
override: true
|
||||
components: rustfmt
|
||||
|
||||
- &cargo-cache
|
||||
name: Cache Rust dependencies
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: |
|
||||
@@ -41,11 +52,36 @@ jobs:
|
||||
RUSTFLAGS: "-D warnings"
|
||||
run: cargo build --verbose
|
||||
|
||||
format:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- *checkout
|
||||
- *setup-rust
|
||||
|
||||
- name: Format
|
||||
run: cargo fmt --check --verbose
|
||||
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- *udev-install
|
||||
- *checkout
|
||||
- *setup-rust
|
||||
- *cargo-cache
|
||||
|
||||
- name: Run tests
|
||||
run: cargo test --verbose
|
||||
|
||||
mock:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- *udev-install
|
||||
- *checkout
|
||||
- *setup-rust
|
||||
- *cargo-cache
|
||||
|
||||
- name: Mock Machine
|
||||
run: cargo build --features development-build --features mock-machine
|
||||
run: cargo build --features "development-build,mock-machine"
|
||||
@@ -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_
|
||||
|
||||
|
||||
@@ -2825,9 +2825,9 @@ checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3"
|
||||
|
||||
[[package]]
|
||||
name = "tracing"
|
||||
version = "0.1.41"
|
||||
version = "0.1.44"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0"
|
||||
checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100"
|
||||
dependencies = [
|
||||
"log",
|
||||
"pin-project-lite",
|
||||
@@ -2837,9 +2837,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tracing-attributes"
|
||||
version = "0.1.30"
|
||||
version = "0.1.31"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903"
|
||||
checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -2848,9 +2848,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tracing-core"
|
||||
version = "0.1.34"
|
||||
version = "0.1.36"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678"
|
||||
checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a"
|
||||
dependencies = [
|
||||
"once_cell",
|
||||
"valuable",
|
||||
|
||||
@@ -2,7 +2,12 @@
|
||||
|
||||
# QiTech Control
|
||||
|
||||
QiTech Control is an industrial control panel software for the next generation of QiTech recycling machines built on top of Beckhoff Automation hardware.
|
||||
QiTech Control is an open-source framework designed to bring modern software paradigms to certified industrial hardware.
|
||||
|
||||
It frees developers from proprietary, license-heavy PLC ecosystems and rigid "point-and-click" workflows that are no longer adequate for today's complex automation challenges.
|
||||
|
||||
QiTech Control combines the modularity and reliability of standard EtherCAT terminals (e.g., WAGO, Beckhoff) with the power of a modern Rust & React stack.
|
||||
|
||||
|
||||
[](https://www.youtube.com/watch?v=KI3YeBwfV-s)
|
||||
*Click here to watch a video demo of our software.*
|
||||
|
||||
@@ -21,7 +21,7 @@ smol = "2.0.2"
|
||||
socketioxide = "0.17.2"
|
||||
bitvec = { version = "1.0.1", features = ["alloc"] }
|
||||
crc = "3.3.0"
|
||||
tracing = "0.1.41"
|
||||
tracing = "0.1.44"
|
||||
erased-serde = "0.4.8"
|
||||
core_affinity = "0.8.3"
|
||||
control_core_derive = { version = "0.1.0", path = "../control-core-derive" }
|
||||
|
||||
@@ -0,0 +1,189 @@
|
||||
use ethercrab::{
|
||||
MainDevice, MainDeviceConfig, PduStorage, Timeouts,
|
||||
std::{ethercat_now, tx_rx_task},
|
||||
};
|
||||
use interfaces::Interface;
|
||||
use std::{process::Command, sync::Arc, time::Duration};
|
||||
|
||||
// Constants for EtherCAT configuration
|
||||
const IFACE_DISCOVERY_MAX_SUBDEVICES: usize = 16; // Must be power of 2 > 1
|
||||
const IFACE_DISCOVERY_MAX_PDU_DATA: usize = PduStorage::element_size(1100);
|
||||
const IFACE_DISCOVERY_MAX_FRAMES: usize = 16;
|
||||
const IFACE_DISCOVERY_MAX_PDI_LEN: usize = 128;
|
||||
|
||||
/// Sets a network interface to unmanaged by NetworkManager.
|
||||
/// Returns true if the command succeeded.
|
||||
pub fn set_interface_managed(interface: &str, managed: bool) -> bool {
|
||||
let managed_str = match managed {
|
||||
true => "yes",
|
||||
false => "no",
|
||||
};
|
||||
tracing::info!(
|
||||
"set_interface_managed for {} managed was set to: {}",
|
||||
interface,
|
||||
managed_str
|
||||
);
|
||||
let status = Command::new("nmcli")
|
||||
.args(["dev", "set", interface, "managed", managed_str])
|
||||
.status();
|
||||
|
||||
matches!(status, Ok(s) if s.success())
|
||||
}
|
||||
|
||||
/// Returns true if the given network interface is Ethernet.
|
||||
/// Prevents testing wlan,loopback and other non ethernet devices
|
||||
fn is_ethernet(interface: &str) -> std::io::Result<bool> {
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
let base_path = format!("/sys/class/net/{}", interface);
|
||||
let type_path = std::path::Path::new(&base_path).join("type");
|
||||
let iface_type = std::fs::read_to_string(&type_path)?.trim().to_string();
|
||||
// 1 means Ethernet
|
||||
let reports_as_ethernet = iface_type == "1";
|
||||
// Double-check that it's not a wireless interface
|
||||
let uevent_path = std::path::Path::new(&base_path).join("uevent");
|
||||
let uevent = std::fs::read_to_string(&uevent_path).unwrap_or_default();
|
||||
// If "DEVTYPE=wlan" appears, it's lying
|
||||
let actually_wifi = uevent.contains("DEVTYPE=wlan");
|
||||
return Ok(reports_as_ethernet && !actually_wifi);
|
||||
}
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
return Ok(true);
|
||||
}
|
||||
|
||||
pub async fn discover_ethercat_interface() -> Result<String, anyhow::Error> {
|
||||
tracing::info!("Discovering EtherCAT interface...");
|
||||
|
||||
// Set up a custom panic hook that suppresses panic output
|
||||
let default_hook = std::panic::take_hook();
|
||||
std::panic::set_hook(Box::new(move |panic_info| {
|
||||
// Optional: log the panic in a controlled way
|
||||
if let Some(location) = panic_info.location() {
|
||||
tracing::debug!(
|
||||
"Suppressed panic in interface test: {} (at {}:{})",
|
||||
panic_info,
|
||||
location.file(),
|
||||
location.line()
|
||||
);
|
||||
} else {
|
||||
tracing::debug!("Suppressed panic in interface test: {}", panic_info);
|
||||
}
|
||||
}));
|
||||
|
||||
// Get eligible interfaces
|
||||
let mut interfaces = Interface::get_all()
|
||||
.map_err(|e| anyhow::anyhow!("Failed to get network interfaces: {}", e))?
|
||||
.into_iter()
|
||||
.filter(|iface| {
|
||||
iface.is_up()
|
||||
&& iface.is_running()
|
||||
&& !iface.is_loopback()
|
||||
&& is_ethernet(&iface.name).unwrap_or(false)
|
||||
&& !iface.name.starts_with("bridge")
|
||||
&& !iface.name.starts_with("utun") // Exclude tunnel interfaces
|
||||
&& !iface.name.starts_with("awdl") // Exclude Apple Wireless Direct Link
|
||||
&& !iface.name.starts_with("anpi") // Exclude Apple Network Privacy Interface
|
||||
&& !iface.name.starts_with("llw") // Exclude Apple low-latency WLAN
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
interfaces.sort_by(|a, b| a.name.cmp(&b.name));
|
||||
let mut interface: Option<&str> = None;
|
||||
|
||||
for i in 0..interfaces.len() {
|
||||
match test_interface(&interfaces[i].name) {
|
||||
Ok(_) => {
|
||||
// if interface found with ethercat Exit early, we expect only one interface with ethercat
|
||||
interface = Some(&interfaces[i].name);
|
||||
break;
|
||||
}
|
||||
Err(_) => (),
|
||||
}
|
||||
}
|
||||
|
||||
for i in 0..interfaces.len() {
|
||||
if interface.is_some() && interface.unwrap() == &interfaces[i].name {
|
||||
set_interface_managed(&interfaces[i].name, false);
|
||||
} else {
|
||||
set_interface_managed(&interfaces[i].name, true);
|
||||
}
|
||||
}
|
||||
|
||||
// Restore the default panic hook
|
||||
std::panic::set_hook(default_hook);
|
||||
|
||||
match interface {
|
||||
Some(interface) => return Ok(interface.to_string()),
|
||||
None => return Err(anyhow::anyhow!("No suitable EtherCAT interface found")),
|
||||
}
|
||||
}
|
||||
|
||||
fn test_interface(interface: &str) -> Result<(), anyhow::Error> {
|
||||
tracing::info!("Testing interface: {}", interface);
|
||||
|
||||
let pdu_storage = Box::leak(Box::new(PduStorage::<
|
||||
IFACE_DISCOVERY_MAX_FRAMES,
|
||||
IFACE_DISCOVERY_MAX_PDU_DATA,
|
||||
>::new()));
|
||||
let (tx, rx, pdu_loop) = pdu_storage
|
||||
.try_split()
|
||||
.map_err(|e| anyhow::anyhow!("Failed to split PDU storage: {:?}", e))?;
|
||||
|
||||
let rt = smol::LocalExecutor::new();
|
||||
|
||||
let result = rt.run(async {
|
||||
let tx_rx_handle = rt.spawn(
|
||||
tx_rx_task(interface, tx, rx)
|
||||
.map_err(|e| anyhow::anyhow!("Failed to spawn TX/RX task: {}", e))?,
|
||||
);
|
||||
|
||||
let maindevice = Arc::new(MainDevice::new(
|
||||
pdu_loop,
|
||||
Timeouts {
|
||||
// Default 5000ms
|
||||
state_transition: Duration::from_millis(5000),
|
||||
// Default 30_000us
|
||||
pdu: Duration::from_micros(30_000),
|
||||
// Default 10ms
|
||||
eeprom: Duration::from_millis(10),
|
||||
// Default 0ms
|
||||
wait_loop_delay: Duration::from_millis(0),
|
||||
// Default 100ms
|
||||
mailbox_echo: Duration::from_millis(100),
|
||||
// Default 1000ms
|
||||
mailbox_response: Duration::from_millis(1000),
|
||||
},
|
||||
MainDeviceConfig {
|
||||
// Default 10000
|
||||
dc_static_sync_iterations: 10000,
|
||||
// Default None
|
||||
retry_behaviour: ethercrab::RetryBehaviour::Count(3),
|
||||
},
|
||||
));
|
||||
|
||||
let result = maindevice
|
||||
.init_single_group::<IFACE_DISCOVERY_MAX_SUBDEVICES, IFACE_DISCOVERY_MAX_PDI_LEN>(
|
||||
ethercat_now,
|
||||
)
|
||||
.await
|
||||
.map(|_| ())
|
||||
.map_err(|e| anyhow::anyhow!("Failed to initialize group: {}", e));
|
||||
|
||||
tx_rx_handle.cancel().await;
|
||||
|
||||
result
|
||||
});
|
||||
|
||||
// await the result of the async block
|
||||
let result = smol::block_on(result);
|
||||
|
||||
if let Err(e) = result {
|
||||
return Err(anyhow::anyhow!(
|
||||
"Failed to initialize EtherCAT on interface {}: {:?}",
|
||||
interface,
|
||||
e
|
||||
));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
pub mod interface_discovery;
|
||||
@@ -1,126 +0,0 @@
|
||||
use anyhow::{Context, Result};
|
||||
use ethercrab::{
|
||||
MainDevice, MainDeviceConfig, PduStorage, RetryBehaviour, Timeouts,
|
||||
std::{ethercat_now, tx_rx_task},
|
||||
};
|
||||
|
||||
use crate::{
|
||||
ethernet::{get_interfaces, set_interface_managed},
|
||||
futures::FutureIteratorExt,
|
||||
};
|
||||
|
||||
// Constants for EtherCAT configuration
|
||||
const IFACE_DISCOVERY_MAX_SUBDEVICES: usize = 16; // Must be power of 2 > 1
|
||||
const IFACE_DISCOVERY_MAX_PDU_DATA: usize = PduStorage::element_size(1100);
|
||||
const IFACE_DISCOVERY_MAX_FRAMES: usize = 16;
|
||||
const IFACE_DISCOVERY_MAX_PDI_LEN: usize = 128;
|
||||
|
||||
pub async fn probe_ethercat() -> Option<String> {
|
||||
let interfaces = match get_interfaces() {
|
||||
Ok(x) => x,
|
||||
Err(_) => return None,
|
||||
};
|
||||
|
||||
interfaces
|
||||
.iter()
|
||||
.map(|iface| iface.name.to_owned())
|
||||
.map(probe_ethercat_interface)
|
||||
.join_all()
|
||||
.await
|
||||
.into_iter()
|
||||
.find_map(|x| x.ok())
|
||||
}
|
||||
|
||||
async fn probe_ethercat_interface(interface: String) -> Result<String> {
|
||||
tracing::info!("Testing ethercat on interface: {}", interface);
|
||||
|
||||
let pdu_storage = Box::leak(Box::new(PduStorage::<
|
||||
IFACE_DISCOVERY_MAX_FRAMES,
|
||||
IFACE_DISCOVERY_MAX_PDU_DATA,
|
||||
>::new()));
|
||||
|
||||
let (tx, rx, pdu_loop) = pdu_storage
|
||||
.try_split()
|
||||
.expect("Failed to split PDU storage");
|
||||
|
||||
let tx_rx_handle =
|
||||
smol::spawn(tx_rx_task(&interface, tx, rx).context("Failed to spawn TX/RX task")?);
|
||||
|
||||
let maindevice = MainDevice::new(
|
||||
pdu_loop,
|
||||
Timeouts::default(),
|
||||
MainDeviceConfig {
|
||||
// Default 10000
|
||||
dc_static_sync_iterations: 10000,
|
||||
// Default None
|
||||
retry_behaviour: RetryBehaviour::Count(3),
|
||||
},
|
||||
);
|
||||
|
||||
let res = maindevice
|
||||
.init_single_group::<IFACE_DISCOVERY_MAX_SUBDEVICES, IFACE_DISCOVERY_MAX_PDI_LEN>(
|
||||
ethercat_now,
|
||||
)
|
||||
.await
|
||||
.context(format!(
|
||||
"Failed to initialize EtherCAT on interface {}",
|
||||
interface
|
||||
));
|
||||
|
||||
tx_rx_handle.cancel().await;
|
||||
|
||||
res?;
|
||||
Ok(interface)
|
||||
}
|
||||
|
||||
pub async fn discover_ethercat_interface() -> Result<String, anyhow::Error> {
|
||||
tracing::info!("Discovering EtherCAT interface...");
|
||||
|
||||
// Set up a custom panic hook that suppresses panic output
|
||||
let default_hook = std::panic::take_hook();
|
||||
std::panic::set_hook(Box::new(move |panic_info| {
|
||||
// Optional: log the panic in a controlled way
|
||||
if let Some(location) = panic_info.location() {
|
||||
tracing::debug!(
|
||||
"Suppressed panic in interface test: {} (at {}:{})",
|
||||
panic_info,
|
||||
location.file(),
|
||||
location.line()
|
||||
);
|
||||
} else {
|
||||
tracing::debug!("Suppressed panic in interface test: {}", panic_info);
|
||||
}
|
||||
}));
|
||||
|
||||
let mut interfaces = get_interfaces()?;
|
||||
|
||||
interfaces.sort_by(|a, b| a.name.cmp(&b.name));
|
||||
let mut interface: Option<&str> = None;
|
||||
|
||||
for i in 0..interfaces.len() {
|
||||
match probe_ethercat_interface(interfaces[i].name.to_string()).await {
|
||||
Ok(_) => {
|
||||
// if interface found with ethercat Exit early, we expect only one interface with ethercat
|
||||
interface = Some(&interfaces[i].name);
|
||||
break;
|
||||
}
|
||||
Err(_) => (),
|
||||
}
|
||||
}
|
||||
|
||||
for i in 0..interfaces.len() {
|
||||
if interface.is_some() && interface.unwrap() == &interfaces[i].name {
|
||||
set_interface_managed(&interfaces[i].name, false);
|
||||
} else {
|
||||
set_interface_managed(&interfaces[i].name, true);
|
||||
}
|
||||
}
|
||||
|
||||
// Restore the default panic hook
|
||||
std::panic::set_hook(default_hook);
|
||||
|
||||
match interface {
|
||||
Some(interface) => Ok(interface.to_string()),
|
||||
None => Err(anyhow::anyhow!("No suitable EtherCAT interface found")),
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,6 @@ use std::process::Command;
|
||||
use anyhow::Result;
|
||||
use interfaces::Interface;
|
||||
|
||||
pub mod ethercat_interface_discovery;
|
||||
pub mod modbus_tcp_discovery;
|
||||
|
||||
/// Returns true if the given network interface is Ethernet.
|
||||
|
||||
@@ -47,7 +47,7 @@ async fn probe_modbus_tcp_addresses(interface: Interface) -> Vec<ModbusTcpProbe>
|
||||
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))
|
||||
(0..size).map(move |i| SocketAddr::new(Ipv4Addr::from(network + i).into(), 502))
|
||||
})
|
||||
.map(|addr| smol::spawn(ping_modbus_device(addr)))
|
||||
.join_all()
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
pub mod controllers;
|
||||
pub mod converters;
|
||||
pub mod downcast;
|
||||
pub mod ethercat;
|
||||
pub mod ethernet;
|
||||
pub mod futures;
|
||||
pub mod helpers;
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
pub mod modbus_serial_interface;
|
||||
pub mod tcp;
|
||||
|
||||
use anyhow::Error;
|
||||
use crc::{CRC_16_MODBUS, Crc};
|
||||
use serialport::SerialPort;
|
||||
use std::time::Duration;
|
||||
|
||||
pub mod tcp;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum ParityType {
|
||||
Even,
|
||||
|
||||
@@ -77,29 +77,22 @@ impl ModbusTcpDevice {
|
||||
}
|
||||
|
||||
async fn read_and_check_responce_header(&mut self, func_code: u8) -> Result<()> {
|
||||
assert_eq!(
|
||||
self.read_u16().await?,
|
||||
self.transactions,
|
||||
"Modbus device sent unexpected transaction id!"
|
||||
);
|
||||
if self.read_u16().await? != self.transactions {
|
||||
bail!("Modbus device sent unexpected transaction id!");
|
||||
}
|
||||
|
||||
assert_eq!(
|
||||
self.read_u16().await?,
|
||||
PROTOCOL_ID,
|
||||
"Modbus device sent unexpected protocol id!"
|
||||
);
|
||||
if self.read_u16().await? != PROTOCOL_ID {
|
||||
bail!("Modbus device sent unexpected protocol id!");
|
||||
}
|
||||
|
||||
let length = self.read_u16().await?;
|
||||
assert!(
|
||||
length >= 2,
|
||||
"Modbus device will send too few bytes to understand the response!"
|
||||
);
|
||||
if length < 2 {
|
||||
bail!("Modbus device will send too few bytes to understand the response!");
|
||||
}
|
||||
|
||||
assert_eq!(
|
||||
self.read_u8().await?,
|
||||
UNIT_ID,
|
||||
"Modbus device sent unexpected unit id!"
|
||||
);
|
||||
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 {
|
||||
@@ -133,11 +126,11 @@ impl ModbusTcpDevice {
|
||||
.await?;
|
||||
|
||||
let num_data_bytes = self.read_u8().await? as u16;
|
||||
assert_eq!(
|
||||
count * 2,
|
||||
num_data_bytes,
|
||||
"Modbus device wants to send only a portion of the requested range - UNIMPLEMENTED!"
|
||||
);
|
||||
if count * 2 != num_data_bytes {
|
||||
bail!(
|
||||
"Modbus device wants to send only a portion of the requested range - UNIMPLEMENTED!"
|
||||
);
|
||||
}
|
||||
|
||||
Ok(num_data_bytes)
|
||||
}
|
||||
@@ -185,17 +178,13 @@ impl ModbusTcpDevice {
|
||||
self.read_and_check_responce_header(WRITE_HOLDING_FUNCTION_CODE)
|
||||
.await?;
|
||||
|
||||
assert_eq!(
|
||||
self.read_u16().await?,
|
||||
addr,
|
||||
"Modbus device wrote to wrong register address!"
|
||||
);
|
||||
if self.read_u16().await? != addr {
|
||||
bail!("Modbus device wrote to wrong register address!");
|
||||
}
|
||||
|
||||
assert_eq!(
|
||||
self.read_u16().await?,
|
||||
count as u16,
|
||||
"Modbus device wrote wrong number of registers!"
|
||||
);
|
||||
if self.read_u16().await? != count as u16 {
|
||||
bail!("Modbus device wrote wrong number of registers!");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -220,7 +209,7 @@ impl ModbusTcpDevice {
|
||||
pub async fn set_string<const N: usize>(&mut self, _addr: u16, s: &str) -> Result<()> {
|
||||
assert!(s.len() <= N, "String is too long to fit!");
|
||||
|
||||
Ok(())
|
||||
todo!()
|
||||
}
|
||||
|
||||
pub async fn get_u16(&mut self, addr: u16) -> Result<u16> {
|
||||
|
||||
@@ -0,0 +1,139 @@
|
||||
# XTREM Communication Protocol
|
||||
|
||||
**Version:** 3.007
|
||||
**Device:** XTREM / XTREM-S Weighing Module
|
||||
**Standard:** OIML R76:2006 / EN45501:2015 compliant
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
The **XTREM communication protocol** allows a terminal or host controller to exchange data and commands with the XTREM weighing module.
|
||||
The device transmits and receives ASCII-coded frames over a serial interface (RS232, RS485, Wi-Fi, or Ethernet, depending on configuration).
|
||||
|
||||
Since we are using the Ethernet interface we need to provide some kind of DHCP to assign an IP - Address to each XTREM Device.
|
||||
They then may be addressed via a **broadcast** IP (e.g.: 192.168.4.255/24).
|
||||
A **Unicast** communication is applied through the device ID inside the XTREM Frame.
|
||||
|
||||
---
|
||||
|
||||
## Frame Structure
|
||||
|
||||
Each message—both query and response—has the same format:
|
||||
|
||||
```
|
||||
STX ID_O ID_D F D_ADDRESS D_L DATA LRC ETX
|
||||
```
|
||||
|
||||
| Field | Description | Length (bytes) |
|
||||
| :------------ | :------------------------------------------------ | :------------: |
|
||||
| **STX** | Start of message (ASCII `02h`) | 1 |
|
||||
| **ID_O** | Sender address (2 ASCII hex chars) | 2 |
|
||||
| **ID_D** | Destination address (2 ASCII hex chars) | 2 |
|
||||
| **F** | Function code (`R`, `W`, `E`, etc.) | 1 |
|
||||
| **D_ADDRESS** | Data address (register, 4 ASCII hex chars) | 4 |
|
||||
| **D_L** | Data length (2 ASCII hex chars) | 2 |
|
||||
| **DATA** | Optional data field | Variable |
|
||||
| **LRC** | Longitudinal Redundancy Check (2 ASCII hex chars) | 2 |
|
||||
| **ETX** | End of message (ASCII `03h`) | 1 |
|
||||
|
||||
In the default configuration the Frame also requires a CRLF at the end.
|
||||
|
||||
**Example:**
|
||||
|
||||
```
|
||||
STX ‘1’ ‘7’ ‘0’ ‘1’ ‘R’ ‘0’ ‘1’ ‘0’ ‘1’ ‘0’ ‘0’ LRC ETX CR LF
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Function Codes
|
||||
|
||||
| Function | ASCII | Hex | Description |
|
||||
| :------- | :---- | :---| :-------------- |
|
||||
| **R** | R | 52h | Read request |
|
||||
| **r** | r | 72h | Read response |
|
||||
| **W** | W | 57h | Write request |
|
||||
| **w** | w | 77h | Write response |
|
||||
| **E** | E | 45h | Execute command |
|
||||
| **e** | e | 65h | Execute response|
|
||||
|
||||
### Write response codes
|
||||
|
||||
* `'0'` – OK
|
||||
* `'1'` – Write denied (sealed)
|
||||
* `'2'` – Read-only register
|
||||
* `'3'` – Invalid value / out of range
|
||||
|
||||
### Execute response codes
|
||||
|
||||
* `'0'` – Executed successfully
|
||||
* `'1'` – Blocked by seal switch
|
||||
|
||||
---
|
||||
|
||||
## LRC Calculation
|
||||
|
||||
The **Longitudinal Redundancy Check** is a one-byte XOR of all message bytes, excluding:
|
||||
|
||||
* `STX`
|
||||
* `ETX`
|
||||
* The LRC itself
|
||||
|
||||
Example C function:
|
||||
|
||||
```c
|
||||
unsigned char LRC(unsigned char* s) {
|
||||
unsigned char lrc = 0;
|
||||
while (*s) {
|
||||
lrc ^= *(s++);
|
||||
}
|
||||
return lrc;
|
||||
}
|
||||
```
|
||||
|
||||
Example Rust function:
|
||||
```rust
|
||||
pub fn compute_lrc(data: &[u8]) -> u8 {
|
||||
data.iter().fold(0u8, |acc, &b| acc ^ b)
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Key Data Registers
|
||||
|
||||
| Address (hex) | Description | Type | Notes |
|
||||
| :------------ | :------------------------- | :--: | :------------------------------------ |
|
||||
| **0000h** | Serial number | R | Factory-set |
|
||||
| **0001h** | Device ID | RW | Network address |
|
||||
| **0007h** | Hardware version | R | Read-only |
|
||||
| **0008h** | Software version | R | Read-only |
|
||||
| **0009h** | Sealing switch status | R | `0=UNLOCK`, `1=LOCK` |
|
||||
| **0010h** | Baud rate | RW | `0–4` → 9600–115200 baud |
|
||||
| **0013h** | Stream mode output rate | RW | Time in ms |
|
||||
| **0100h** | Device state | R | Status bits (power, Wi-Fi, ADC, etc.) |
|
||||
| **0101h** | Weight value | R | Gross weight |
|
||||
| **0102h** | Tare value | RE | Read/Execute |
|
||||
| **0103h** | Net weight | R | Gross − Tare |
|
||||
| **0110h** | ADC counts (instant) | R | Raw ADC data |
|
||||
| **0111h** | ADC counts (filtered) | R | Post-filter data |
|
||||
| **1010h** | Stop stream mode | E | Execute |
|
||||
| **1011h** | Start stream mode (weight) | E | Continuous output |
|
||||
| **9999h** | Device reset | E | Software reset |
|
||||
| **EEEEh** | Factory reset | E | Restores defaults |
|
||||
|
||||
---
|
||||
|
||||
## Post-Factory Reset Configuration
|
||||
|
||||
After performing a factory reset, several registers must be configured to ensure proper operation of the device.
|
||||
|
||||
Note: The unit may remain set to 'g' after a factory reset, but the values received may be in 'kg' depending on the device configuration.
|
||||
|
||||
The following registers should be reviewed and set as required:
|
||||
1. **Address: 0022 | Max (Max1) (scale maximum capacity)**
|
||||
The default value for this register may vary between devices (e.g., 60000 or 6000). Set this value to 6000 to ensure correct operation.
|
||||
2. **Address: 0023 | e (e1) (scale interval)**
|
||||
3. **Address: 0026 | Decimal position**
|
||||
@@ -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`
|
||||
@@ -10,7 +10,7 @@ This is why we chose nixos
|
||||
# How we do updates
|
||||
1. The user selects a version (commit/branch/tag) of the software they want to install
|
||||
2. The repo is cloned to the local machine
|
||||
3. the `installInfo.nix` is created and commited locally. It contains infos like the commit hash, branch, tag, timestamp etc. These are later available as environment variables `QITECH_OS...`
|
||||
3. Information like the commit hash, branch, tag, timestamp etc. is passed to the nix build process as environment variables `QITECH_OS...`
|
||||
4. The `nixos-switch` command is run. After rebuilding & compiling the software the system reboots.
|
||||
- The nixos-switch command does the following:
|
||||
1. The system is built with the new configuration
|
||||
@@ -21,4 +21,4 @@ This is why we chose nixos
|
||||
|
||||
# More Docs
|
||||
- [Implementation Details](./details.md)
|
||||
- [Fresh Installation Quick Start guide](./quick-start.md)
|
||||
- [Fresh Installation Quick Start guide](./quick-start.md)
|
||||
@@ -0,0 +1,235 @@
|
||||
# REST API `/api/v2`
|
||||
|
||||
The Qitech Control Panel exposes a small HTTP interface for discovering machines and reading their current values. All examples below assume you are connected to the panel’s Ethernet subnet and talk directly to the panel at:
|
||||
|
||||
- **Base URL:** `http://10.10.10.1:3001`
|
||||
|
||||
> **Schema note:** This documentation intentionally stays light on field details beyond what is shown in the examples. For the exact datatypes and complete payload shapes, refer to the corresponding Rust types (linked below).
|
||||
|
||||
---
|
||||
|
||||
## Authentication
|
||||
|
||||
The panel does **not** perform HTTP authentication. The expected security model is **network-level isolation**: anything that can send packets to the panel’s Ethernet interface is treated as trusted. This matches common security assumptions in EtherCAT-style control networks.
|
||||
|
||||
The panel is configured to administer its own subnet `10.10.10.0/24` via DHCP, while Wi-Fi can be used for upstream internet connectivity (if configured). If you need authentication or access from outside the isolated subnet, place a router/device in **client/bridge mode** on the panel network and expose the panel through a **reverse proxy** where you can add authentication, logging, rate limiting, etc.
|
||||
|
||||
If DNS is available on the subnet, you may be able to resolve `qitech.control`; otherwise use the static address `10.10.10.1`.
|
||||
|
||||
---
|
||||
|
||||
## List machines `GET /api/v2/machine`
|
||||
|
||||
Returns the set of machines currently known/connected to the panel.
|
||||
|
||||
Machines are identified by:
|
||||
|
||||
- `slug`: the machine type / model identifier (string)
|
||||
- `serial`: the specific machine instance identifier (int)
|
||||
|
||||
Each machine also includes a `legacy_id` to support older **v1** workflows. If a machine reports an issue, the `error` field may be present and non-null (containing an error message).
|
||||
|
||||
### Example request
|
||||
|
||||
```bash
|
||||
curl -X GET "http://10.10.10.1:3001/api/v2/machine"
|
||||
```
|
||||
|
||||
### Example response
|
||||
|
||||
```json
|
||||
{
|
||||
"machines": [
|
||||
{
|
||||
"legacy_id": {
|
||||
"machine_identification": {
|
||||
"vendor": 1,
|
||||
"machine": 7
|
||||
},
|
||||
"serial": 57922
|
||||
},
|
||||
"serial": 57922,
|
||||
"vendor": "QiTech",
|
||||
"slug": "mock",
|
||||
"error": null
|
||||
},
|
||||
{
|
||||
"legacy_id": {
|
||||
"machine_identification": {
|
||||
"vendor": 1,
|
||||
"machine": 4
|
||||
},
|
||||
"serial": 57922
|
||||
},
|
||||
"serial": 57922,
|
||||
"vendor": "QiTech",
|
||||
"slug": "extruder_v1",
|
||||
"error": null
|
||||
},
|
||||
{
|
||||
"legacy_id": {
|
||||
"machine_identification": {
|
||||
"vendor": 1,
|
||||
"machine": 2
|
||||
},
|
||||
"serial": 57922
|
||||
},
|
||||
"serial": 57922,
|
||||
"vendor": "QiTech",
|
||||
"slug": "winder_v1",
|
||||
"error": null
|
||||
},
|
||||
{
|
||||
"legacy_id": {
|
||||
"machine_identification": {
|
||||
"vendor": 1,
|
||||
"machine": 10
|
||||
},
|
||||
"serial": 48879
|
||||
},
|
||||
"serial": 48879,
|
||||
"vendor": "QiTech",
|
||||
"slug": "wago_power_v1",
|
||||
"error": null
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Get current values `GET /api/v2/machine/<slug>/<serial>`
|
||||
|
||||
Returns all currently known values for a single machine.
|
||||
|
||||
Values are categorized into two groups:
|
||||
|
||||
- **State**: requested/commanded values (these typically change only after a state-change request, or if another controller updates them)
|
||||
- **Live Values**: measured/observed values coming from the machine and potentially changing quickly
|
||||
|
||||
This REST endpoint returns **only the current snapshot**, not a stream of live values.
|
||||
To receive continuous updates (via WebSockets), subscribe to the machine namespace (see **WebSockets** below).
|
||||
|
||||
### Example request (mock machine)
|
||||
|
||||
```bash
|
||||
curl -X GET "http://10.10.10.1:3001/api/v2/machine/mock/57922"
|
||||
```
|
||||
|
||||
### Example response
|
||||
|
||||
```json
|
||||
{
|
||||
"machine": {
|
||||
"legacy_id": {
|
||||
"machine_identification": {
|
||||
"vendor": 1,
|
||||
"machine": 7
|
||||
},
|
||||
"serial": 57922
|
||||
},
|
||||
"serial": 57922,
|
||||
"vendor": "QiTech",
|
||||
"slug": "mock",
|
||||
"error": null
|
||||
},
|
||||
"state": {
|
||||
"frequency1": 100.0,
|
||||
"frequency2": 200.0,
|
||||
"frequency3": 500.0,
|
||||
"is_default_state": false,
|
||||
"mode_state": {
|
||||
"mode": "Running"
|
||||
}
|
||||
},
|
||||
"live_values": {
|
||||
"amplitude1": -0.03438523433309566,
|
||||
"amplitude2": -0.06872980145477608,
|
||||
"amplitude3": -0.1711138370170743,
|
||||
"amplitude_sum": -0.27422887280494607
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Change machine state `POST /api/v1/machine/<slug>/<serial>`
|
||||
|
||||
State changes are submitted as **mutations**. The mutation payload is defined per machine type in Rust. Conceptually, each item in the mutation list represents a setter-style operation that is applied by the real-time control loop.
|
||||
|
||||
The API does **not** return the newly-applied state in the POST response. The panel runs a real-time loop and generally won’t block waiting for the physical system to converge. Instead:
|
||||
|
||||
- Submit the mutation via `POST`
|
||||
- Poll `GET /api/v2/machine/<slug>/<serial>` to observe the updated state and/or any reported errors
|
||||
|
||||
### Example request (mock machine)
|
||||
|
||||
```bash
|
||||
curl -X POST \
|
||||
-d \
|
||||
-H "Content-Type: application/json" \
|
||||
"http://10.10.10.1:3001/api/v1/machine/mock/57922"
|
||||
```
|
||||
|
||||
### Example response
|
||||
|
||||
```json
|
||||
null
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## WebSockets
|
||||
|
||||
For continuous updates, subscribe to a machine-specific namespace derived from its `legacy_id`:
|
||||
|
||||
- Namespace: `/machine/<vendor>/<id>/<serial>`
|
||||
|
||||
The stream emits events for:
|
||||
|
||||
- state changes (`StateEvent`)
|
||||
- live value updates (`LiveValuesEvent`)
|
||||
|
||||
Both event payloads use the same machine-specific schema as the `/api/v2` REST responses.
|
||||
|
||||
---
|
||||
|
||||
## List of all machines
|
||||
|
||||
Below is a template you can fill with links to the relevant Rust types (mutations + state/live structs).
|
||||
For each machine, link to:
|
||||
|
||||
- **Mutations:** the request payload type used by `POST /api/v1/machine/<slug>/<serial>`
|
||||
- **State / Live Values:** the response payload types returned by `GET /api/v2/machine/<slug>/<serial>`
|
||||
|
||||
### Machines
|
||||
|
||||
- **winder_v1**
|
||||
|
||||
- Mutations: <https://github.com/qitechgmbh/control/blob/1ec20074e9030a0ed1739ca9d9a77e298a2652a3/machines/src/winder2/api.rs#L87>
|
||||
- State: <https://github.com/qitechgmbh/control/blob/1ec20074e9030a0ed1739ca9d9a77e298a2652a3/machines/src/winder2/api.rs#L163>
|
||||
- Live Values: <https://github.com/qitechgmbh/control/blob/1ec20074e9030a0ed1739ca9d9a77e298a2652a3/machines/src/winder2/api.rs#L143>
|
||||
|
||||
- **extruder_v1**
|
||||
|
||||
- Mutations: <https://github.com/qitechgmbh/control/blob/1ec20074e9030a0ed1739ca9d9a77e298a2652a3/machines/src/extruder1/api.rs#L214>
|
||||
- State: <https://github.com/qitechgmbh/control/blob/1ec20074e9030a0ed1739ca9d9a77e298a2652a3/machines/src/extruder1/api.rs#L89>
|
||||
- Live Values: <https://github.com/qitechgmbh/control/blob/1ec20074e9030a0ed1739ca9d9a77e298a2652a3/machines/src/extruder1/api.rs#L55>
|
||||
|
||||
- **laser_v1**
|
||||
|
||||
- Mutations: <https://github.com/qitechgmbh/control/blob/1ec20074e9030a0ed1739ca9d9a77e298a2652a3/machines/src/laser/api.rs#L88>
|
||||
- State: <https://github.com/qitechgmbh/control/blob/1ec20074e9030a0ed1739ca9d9a77e298a2652a3/machines/src/laser/api.rs#L32>
|
||||
- Live Values: <https://github.com/qitechgmbh/control/blob/1ec20074e9030a0ed1739ca9d9a77e298a2652a3/machines/src/laser/api.rs#L17>
|
||||
|
||||
- **mock**
|
||||
|
||||
- Mutations: <https://github.com/qitechgmbh/control/blob/1ec20074e9030a0ed1739ca9d9a77e298a2652a3/machines/src/mock/api.rs#L84>
|
||||
- State: <https://github.com/qitechgmbh/control/blob/1ec20074e9030a0ed1739ca9d9a77e298a2652a3/machines/src/mock/api.rs#L38>
|
||||
- Live Values: <https://github.com/qitechgmbh/control/blob/1ec20074e9030a0ed1739ca9d9a77e298a2652a3/machines/src/mock/api.rs#L24>
|
||||
|
||||
- **extruder_v2**
|
||||
|
||||
- Mutations: <https://github.com/qitechgmbh/control/blob/1ec20074e9030a0ed1739ca9d9a77e298a2652a3/machines/src/extruder2/api.rs#L126>
|
||||
- State: <https://github.com/qitechgmbh/control/blob/1ec20074e9030a0ed1739ca9d9a77e298a2652a3/machines/src/extruder2/api.rs#L93>
|
||||
- Live Values: <https://github.com/qitechgmbh/control/blob/1ec20074e9030a0ed1739ca9d9a77e298a2652a3/machines/src/extruder2/api.rs#L59>
|
||||
@@ -88,6 +88,7 @@ typings/
|
||||
# Vite
|
||||
.vite/
|
||||
dist/
|
||||
dist-electron/
|
||||
|
||||
# Electron-Forge
|
||||
out/
|
||||
|
||||
@@ -1,59 +0,0 @@
|
||||
import type { ForgeConfig } from "@electron-forge/shared-types";
|
||||
import { MakerSquirrel } from "@electron-forge/maker-squirrel";
|
||||
import { MakerZIP } from "@electron-forge/maker-zip";
|
||||
import { MakerDeb } from "@electron-forge/maker-deb";
|
||||
import { MakerRpm } from "@electron-forge/maker-rpm";
|
||||
import { VitePlugin } from "@electron-forge/plugin-vite";
|
||||
import { FusesPlugin } from "@electron-forge/plugin-fuses";
|
||||
import { FuseV1Options, FuseVersion } from "@electron/fuses";
|
||||
|
||||
const config: ForgeConfig = {
|
||||
packagerConfig: {
|
||||
executableName: "electron-shadcn",
|
||||
asar: true,
|
||||
icon: "src/assets/icon",
|
||||
},
|
||||
rebuildConfig: {},
|
||||
makers: [
|
||||
new MakerSquirrel({}),
|
||||
new MakerZIP({}, ["darwin"]),
|
||||
new MakerRpm({}),
|
||||
new MakerDeb({}),
|
||||
],
|
||||
plugins: [
|
||||
new VitePlugin({
|
||||
// `build` can specify multiple entry builds, which can be Main process, Preload scripts, Worker process, etc.
|
||||
// If you are familiar with Vite configuration, it will look really familiar.
|
||||
build: [
|
||||
{
|
||||
// `entry` is just an alias for `build.lib.entry` in the corresponding file of `config`.
|
||||
entry: "src/main.ts",
|
||||
config: "vite.main.config.ts",
|
||||
},
|
||||
{
|
||||
entry: "src/preload.ts",
|
||||
config: "vite.preload.config.ts",
|
||||
},
|
||||
],
|
||||
renderer: [
|
||||
{
|
||||
name: "main_window",
|
||||
config: "vite.renderer.config.ts",
|
||||
},
|
||||
],
|
||||
}),
|
||||
// Fuses are used to enable/disable various Electron functionality
|
||||
// at package time, before code signing the application
|
||||
new FusesPlugin({
|
||||
version: FuseVersion.V1,
|
||||
[FuseV1Options.RunAsNode]: false,
|
||||
[FuseV1Options.EnableCookieEncryption]: true,
|
||||
[FuseV1Options.EnableNodeOptionsEnvironmentVariable]: false,
|
||||
[FuseV1Options.EnableNodeCliInspectArguments]: false,
|
||||
[FuseV1Options.EnableEmbeddedAsarIntegrityValidation]: true,
|
||||
[FuseV1Options.OnlyLoadAppFromAsar]: true,
|
||||
}),
|
||||
],
|
||||
};
|
||||
|
||||
export default config;
|
||||
@@ -1,35 +0,0 @@
|
||||
export {}; // Make this a module
|
||||
|
||||
declare global {
|
||||
// This allows TypeScript to pick up the magic constants that's auto-generated by Forge's Vite
|
||||
// plugin that tells the Electron app where to look for the Vite-bundled app code (depending on
|
||||
// whether you're running in development or production).
|
||||
const MAIN_WINDOW_VITE_DEV_SERVER_URL: string;
|
||||
const MAIN_WINDOW_VITE_NAME: string;
|
||||
|
||||
namespace NodeJS {
|
||||
interface Process {
|
||||
// Used for hot reload after preload scripts.
|
||||
viteDevServers: Record<string, import("vite").ViteDevServer>;
|
||||
}
|
||||
}
|
||||
|
||||
type VitePluginConfig = ConstructorParameters<
|
||||
typeof import("@electron-forge/plugin-vite").VitePlugin
|
||||
>[0];
|
||||
|
||||
interface VitePluginRuntimeKeys {
|
||||
VITE_DEV_SERVER_URL: `${string}_VITE_DEV_SERVER_URL`;
|
||||
VITE_NAME: `${string}_VITE_NAME`;
|
||||
}
|
||||
}
|
||||
|
||||
declare module "vite" {
|
||||
interface ConfigEnv<
|
||||
K extends keyof VitePluginConfig = keyof VitePluginConfig,
|
||||
> {
|
||||
root: string;
|
||||
forgeConfig: VitePluginConfig;
|
||||
forgeConfigSelf: VitePluginConfig[K][number];
|
||||
}
|
||||
}
|
||||
@@ -6,10 +6,8 @@
|
||||
"main": ".vite/build/main.js",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"start": "electron-forge start",
|
||||
"package": "electron-forge package",
|
||||
"make": "electron-forge make",
|
||||
"publish": "electron-forge publish",
|
||||
"start": "vite",
|
||||
"build": "vite build",
|
||||
"lint": "eslint .",
|
||||
"lint:write": "eslint . --fix",
|
||||
"format": "prettier --check .",
|
||||
@@ -22,15 +20,6 @@
|
||||
},
|
||||
"author": "",
|
||||
"devDependencies": {
|
||||
"@electron-forge/cli": "^7.6.1",
|
||||
"@electron-forge/maker-deb": "^7.8.0",
|
||||
"@electron-forge/maker-rpm": "^7.8.3",
|
||||
"@electron-forge/maker-squirrel": "^7.6.1",
|
||||
"@electron-forge/maker-zip": "^7.6.1",
|
||||
"@electron-forge/plugin-auto-unpack-natives": "^7.6.1",
|
||||
"@electron-forge/plugin-fuses": "^7.10.2",
|
||||
"@electron-forge/plugin-vite": "^7.6.1",
|
||||
"@electron-forge/shared-types": "^7.4.0",
|
||||
"@electron/fuses": "^1.8.0",
|
||||
"@eslint/compat": "^1.2.6",
|
||||
"@eslint/js": "^9.18.0",
|
||||
@@ -62,6 +51,8 @@
|
||||
"typescript": "^5.7.3",
|
||||
"typescript-eslint": "^8.24.1",
|
||||
"vite": "^6.4.1",
|
||||
"vite-plugin-electron": "^0.29.0",
|
||||
"vite-plugin-electron-renderer": "^0.14.6",
|
||||
"vitest": "^3.0.6"
|
||||
},
|
||||
"dependencies": {
|
||||
@@ -84,7 +75,6 @@
|
||||
"@tanstack/react-router": "^1.130.17",
|
||||
"@tanstack/react-table": "^8.21.2",
|
||||
"@tanstack/router-devtools": "^1.105.0",
|
||||
"@tanstack/zod-adapter": "^1.116.0",
|
||||
"@vitejs/plugin-react": "^4.5.2",
|
||||
"babel-plugin-react-compiler": "^19.0.0-beta-714736e-20250131",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
@@ -109,9 +99,9 @@
|
||||
"tailwind-merge": "^3.0.1",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"uplot": "^1.6.32",
|
||||
"xlsx": "^0.18.5",
|
||||
"zod": "^3.24.2",
|
||||
"zod-validation-error": "^3.4.0",
|
||||
"xlsx": "https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz",
|
||||
"zod": "^4.3.5",
|
||||
"zod-validation-error": "^5.0.0",
|
||||
"zustand": "^5.0.8"
|
||||
}
|
||||
}
|
||||
|
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
|
||||
|
||||

|
||||

|
||||
|
||||

|
||||

|
||||
|
||||

|
||||

|
||||
|
||||

|
||||

|
||||
|
||||

|
||||

|
||||
@@ -15,25 +15,31 @@ import {
|
||||
machineIdentificationUnique,
|
||||
} from "@/machines/types";
|
||||
import { useRef } from "react";
|
||||
import { rustEnumSchema } from "@/lib/types";
|
||||
import { rustEnum } from "@/lib/types";
|
||||
|
||||
export type EthercatDevices = z.infer<typeof ethercatDevicesSchema>;
|
||||
|
||||
export const ethercatDevicesSchema = z.object({
|
||||
devices: z.array(
|
||||
z.object({
|
||||
configured_address: z.number().int(),
|
||||
name: z.string(),
|
||||
vendor_id: z.number().int(),
|
||||
product_id: z.number().int(),
|
||||
revision: z.number().int(),
|
||||
device_identification: deviceIdentification,
|
||||
}),
|
||||
),
|
||||
});
|
||||
|
||||
// Update the EthercatDevicesEventData schema
|
||||
export const ethercatDevicesEventDataSchema = rustEnumSchema({
|
||||
Initializing: z.boolean(),
|
||||
Done: z.object({
|
||||
devices: z.array(
|
||||
z.object({
|
||||
configured_address: z.number().int(),
|
||||
name: z.string(),
|
||||
vendor_id: z.number().int(),
|
||||
product_id: z.number().int(),
|
||||
revision: z.number().int(),
|
||||
device_identification: deviceIdentification,
|
||||
}),
|
||||
),
|
||||
}),
|
||||
Error: z.string(),
|
||||
});
|
||||
export const ethercatDevicesEventDataSchema = z
|
||||
.object({
|
||||
Initializing: z.boolean(),
|
||||
Done: ethercatDevicesSchema,
|
||||
Error: z.string(),
|
||||
})
|
||||
.check(rustEnum);
|
||||
|
||||
export type EthercatDevicesEventData = z.infer<
|
||||
typeof ethercatDevicesEventDataSchema
|
||||
@@ -62,10 +68,12 @@ export const machinesEventSchema = eventSchema(machinesEventDataSchema);
|
||||
export type MachinesEvent = z.infer<typeof machinesEventSchema>;
|
||||
|
||||
// Keep the EthercatInterfaceDiscovery event
|
||||
export const ethercatInterfaceDiscoveryEventDataSchema = rustEnumSchema({
|
||||
Discovering: z.boolean(),
|
||||
Done: z.string(),
|
||||
});
|
||||
export const ethercatInterfaceDiscoveryEventDataSchema = z
|
||||
.object({
|
||||
Discovering: z.boolean(),
|
||||
Done: z.string(),
|
||||
})
|
||||
.check(rustEnum);
|
||||
|
||||
export type EthercatInterfaceDiscoveryEventData = z.infer<
|
||||
typeof ethercatInterfaceDiscoveryEventDataSchema
|
||||
@@ -110,23 +118,19 @@ export function mainMessageHandler(
|
||||
try {
|
||||
// Apply appropriate caching strategy based on event type
|
||||
if (eventName === "EthercatDevicesEvent") {
|
||||
const validatedEvent = ethercatDevicesEventSchema.parse(event);
|
||||
console.log("EthercatDevicesEvent", validatedEvent);
|
||||
const validatedEvent = event;
|
||||
store.setState((state) => ({
|
||||
...state,
|
||||
ethercatDevices: validatedEvent,
|
||||
}));
|
||||
} else if (eventName === "MachinesEvent") {
|
||||
const validatedEvent = machinesEventSchema.parse(event);
|
||||
console.log("MachinesEvent", validatedEvent);
|
||||
store.setState((state) => ({
|
||||
...state,
|
||||
machines: validatedEvent,
|
||||
}));
|
||||
} else if (eventName === "EthercatInterfaceDiscoveryEvent") {
|
||||
const validatedEvent =
|
||||
ethercatInterfaceDiscoveryEventSchema.parse(event);
|
||||
console.log("EthercatInterfaceDiscoveryEvent", validatedEvent);
|
||||
const validatedEvent = event;
|
||||
store.setState((state) => ({
|
||||
...state,
|
||||
ethercatInterfaceDiscovery: validatedEvent,
|
||||
|
||||
@@ -8,8 +8,7 @@ import { z } from "zod";
|
||||
import { toastError, toastZodError } from "@/components/Toast";
|
||||
import { MachineIdentificationUnique } from "@/machines/types";
|
||||
import { FPS_30 } from "@/lib/constants";
|
||||
import { mainNamespaceStore, createMainNamespaceStore } from "./mainNamespace";
|
||||
import { softReloadMainNamespaceStore } from "./softReload";
|
||||
import { mainNamespaceStore } from "./mainNamespace";
|
||||
|
||||
/**
|
||||
* Simple buffer-based store updater to limit React re-renders to ~30 FPS
|
||||
@@ -164,6 +163,7 @@ export const handleEventValidationError = (
|
||||
error: z.ZodError,
|
||||
eventName: string,
|
||||
) => {
|
||||
console.log(error);
|
||||
toastZodError(error, `Event Validation Error for ${eventName}`);
|
||||
throw new Error(`Event validation failed for ${eventName}`);
|
||||
};
|
||||
@@ -385,11 +385,11 @@ const useSocketioStore = create<SocketioStore>()((set, get) => ({
|
||||
},
|
||||
|
||||
decrementNamespace: (namespaceId: NamespaceId) => {
|
||||
const namespace_path = serializeNamespaceId(namespaceId);
|
||||
/*const namespace_path = serializeNamespaceId(namespaceId);
|
||||
|
||||
// check if the namespace exists
|
||||
const namespace = get().namespaces[namespace_path];
|
||||
/*if (namespace) {
|
||||
if (namespace) {
|
||||
set(
|
||||
produce((state: SocketioStore) => {
|
||||
// decrement the count
|
||||
|
||||
@@ -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 */}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { PresetData, PresetSchema } from "@/lib/preset/preset";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
@@ -14,13 +13,13 @@ import { Separator } from "../ui/separator";
|
||||
import { PresetPreviewEntries, PresetPreviewTable } from "./PresetPreviewTable";
|
||||
import { Input } from "../ui/input";
|
||||
|
||||
export type NewPresetDialogProps<T extends PresetSchema> = {
|
||||
currentState?: PresetData<T>;
|
||||
export type NewPresetDialogProps<T> = {
|
||||
currentState?: T;
|
||||
previewEntries: PresetPreviewEntries<T>;
|
||||
onSave: (name: string) => void;
|
||||
};
|
||||
|
||||
export function NewPresetDialog<T extends PresetSchema>({
|
||||
export function NewPresetDialog<T>({
|
||||
currentState,
|
||||
onSave,
|
||||
previewEntries,
|
||||
|
||||
@@ -60,7 +60,7 @@ export function PresetCardMenu({
|
||||
);
|
||||
}
|
||||
|
||||
export type PresetCardProps<T extends PresetSchema> = {
|
||||
export type PresetCardProps<T> = {
|
||||
preset: Preset<T>;
|
||||
onApply: (preset: Preset<T>) => void;
|
||||
onOverwrite: (preset: Preset<T>) => void;
|
||||
@@ -72,7 +72,7 @@ export type PresetCardProps<T extends PresetSchema> = {
|
||||
isActive?: boolean;
|
||||
};
|
||||
|
||||
export function PresetCard<T extends PresetSchema>({
|
||||
export function PresetCard<T>({
|
||||
preset,
|
||||
onApply,
|
||||
onOverwrite,
|
||||
|
||||
@@ -1,28 +1,27 @@
|
||||
import React, { Fragment } from "react";
|
||||
import { PresetData, PresetSchema } from "@/lib/preset/preset";
|
||||
import { renderUnitSymbol, renderUnitSyntax, Unit } from "@/control/units";
|
||||
|
||||
export const previewSeparator = undefined;
|
||||
|
||||
type PreviewSeparator = typeof previewSeparator;
|
||||
|
||||
type PresetPreviewEntry<T extends PresetSchema> = {
|
||||
type PresetPreviewEntry<T> = {
|
||||
name: string;
|
||||
unit?: Unit;
|
||||
renderValue: (data: PresetData<T>) => string | undefined;
|
||||
renderValue: (data: T) => string | undefined;
|
||||
};
|
||||
|
||||
export type PresetPreviewEntries<T extends PresetSchema> = (
|
||||
export type PresetPreviewEntries<T> = (
|
||||
| PresetPreviewEntry<T>
|
||||
| PreviewSeparator
|
||||
)[];
|
||||
|
||||
export type PresetPreviewTableProps<T extends PresetSchema> = {
|
||||
data: PresetData<T>;
|
||||
export type PresetPreviewTableProps<T> = {
|
||||
data?: T;
|
||||
entries: PresetPreviewEntries<T>;
|
||||
};
|
||||
|
||||
export function PresetPreviewTable<T extends PresetSchema>({
|
||||
export function PresetPreviewTable<T>({
|
||||
data,
|
||||
entries,
|
||||
}: PresetPreviewTableProps<T>) {
|
||||
@@ -39,7 +38,7 @@ export function PresetPreviewTable<T extends PresetSchema>({
|
||||
);
|
||||
}
|
||||
|
||||
const value = entry.renderValue(data);
|
||||
const value = data !== undefined ? entry.renderValue(data) : undefined;
|
||||
return (
|
||||
<Fragment key={entry.name}>
|
||||
<div>{entry.name}</div>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Preset, PresetSchema } from "@/lib/preset/preset";
|
||||
import { Preset } from "@/lib/preset/preset";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
@@ -13,14 +13,14 @@ import { Icon } from "../Icon";
|
||||
import { Separator } from "../ui/separator";
|
||||
import { PresetPreviewEntries, PresetPreviewTable } from "./PresetPreviewTable";
|
||||
|
||||
export type PresetShowDialogProps<T extends PresetSchema> = {
|
||||
export type PresetShowDialogProps<T> = {
|
||||
preset: Preset<T>;
|
||||
previewEntries: PresetPreviewEntries<T>;
|
||||
onApply: (preset: Preset<T>) => void;
|
||||
hideDate?: boolean;
|
||||
};
|
||||
|
||||
export function PresetShowDialog<T extends PresetSchema>({
|
||||
export function PresetShowDialog<T>({
|
||||
preset,
|
||||
onApply,
|
||||
previewEntries,
|
||||
|
||||
@@ -2,19 +2,19 @@ import React from "react";
|
||||
import { ControlGrid } from "@/control/ControlGrid";
|
||||
import { Page } from "@/components/Page";
|
||||
import { usePresets, UsePresetsParams } from "@/lib/preset/usePresets";
|
||||
import { Preset, PresetSchema } from "@/lib/preset/preset";
|
||||
import { Preset } from "@/lib/preset/preset";
|
||||
import { PresetCard } from "./PresetCard";
|
||||
import { PresetPreviewEntries } from "./PresetPreviewTable";
|
||||
import { NewPresetDialog } from "./NewPresetDialog";
|
||||
import { downloadJson } from "@/lib/download";
|
||||
import { JsonFileInput } from "../FileInput";
|
||||
|
||||
type PresetsPageProps<T extends PresetSchema> = UsePresetsParams<T> & {
|
||||
type PresetsPageProps<T> = UsePresetsParams<T> & {
|
||||
applyPreset: (preset: Preset<T>) => void;
|
||||
previewEntries: PresetPreviewEntries<T>;
|
||||
};
|
||||
|
||||
export function PresetsPage<T extends PresetSchema>({
|
||||
export function PresetsPage<T>({
|
||||
applyPreset,
|
||||
machine_identification,
|
||||
currentState,
|
||||
|
||||
@@ -36,7 +36,6 @@ type Props = {
|
||||
maxSlider?: number; // Override the slider max value
|
||||
maxLabel?: string;
|
||||
step?: number;
|
||||
valueSchema?: z.ZodType<number>;
|
||||
inverted?: boolean;
|
||||
confirmation?: string;
|
||||
renderValue: (value: number) => string;
|
||||
@@ -89,7 +88,6 @@ export function EditValue({
|
||||
description,
|
||||
defaultValue,
|
||||
step = 1,
|
||||
valueSchema: schema,
|
||||
inverted,
|
||||
min,
|
||||
max,
|
||||
@@ -104,7 +102,7 @@ export function EditValue({
|
||||
|
||||
// Form setup
|
||||
const formSchema = z.object({
|
||||
value: schema ?? z.number(),
|
||||
value: z.number(),
|
||||
});
|
||||
type FormSchema = z.infer<typeof formSchema>;
|
||||
|
||||
@@ -191,7 +189,6 @@ export function EditValue({
|
||||
const numericValue = parseFloat(normalizedInput);
|
||||
const hasValidationError =
|
||||
isNaN(numericValue) ||
|
||||
(schema && !schema.safeParse(numericValue).success) ||
|
||||
(max !== undefined && numericValue > max) ||
|
||||
(min !== undefined && numericValue < min);
|
||||
|
||||
@@ -222,15 +219,7 @@ export function EditValue({
|
||||
setValueStringError(false);
|
||||
preventFormSyncRef.current = false;
|
||||
}
|
||||
}, [
|
||||
valueString,
|
||||
valueStringDirty,
|
||||
schema,
|
||||
max,
|
||||
min,
|
||||
form,
|
||||
roundToStepDecimals,
|
||||
]);
|
||||
}, [valueString, valueStringDirty, max, min, form, roundToStepDecimals]);
|
||||
|
||||
// Reset input state to clean form value
|
||||
const resetInput = React.useCallback(() => {
|
||||
|
||||
@@ -106,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,
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
export function deepEquals<T>(a: T, b: T): boolean {
|
||||
export function deepEquals(
|
||||
a: object | undefined,
|
||||
b: object | undefined,
|
||||
): boolean {
|
||||
if (a === b) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -1,20 +1,16 @@
|
||||
import { machineIdentification } from "@/machines/types";
|
||||
import { z } from "zod";
|
||||
|
||||
export type PresetSchema = z.ZodTypeAny;
|
||||
export type PresetSchema<T> = z.ZodType<T>;
|
||||
|
||||
export const presetSchema = <S extends PresetSchema>(dataSchema: S) =>
|
||||
export const presetSchema = <T>(dataSchema: PresetSchema<T>) =>
|
||||
z.object({
|
||||
id: z.number().int().nonnegative().optional(),
|
||||
name: z.string().nonempty(),
|
||||
lastModified: z.coerce.date(),
|
||||
machineIdentification,
|
||||
schemaVersion: z.number().int().positive(),
|
||||
data: dataSchema,
|
||||
data: dataSchema.optional(),
|
||||
});
|
||||
|
||||
export type Preset<S extends PresetSchema> = z.infer<
|
||||
ReturnType<typeof presetSchema<S>>
|
||||
>;
|
||||
|
||||
export type PresetData<S extends PresetSchema> = z.infer<S>;
|
||||
export type Preset<T> = z.infer<ReturnType<typeof presetSchema<T>>>;
|
||||
@@ -1,6 +1,6 @@
|
||||
import { create } from "zustand";
|
||||
import { persist, PersistStorage, StorageValue } from "zustand/middleware";
|
||||
import { Preset, PresetSchema, presetSchema } from "./preset";
|
||||
import { Preset, presetSchema } from "./preset";
|
||||
import { MachineIdentification } from "@/machines/types";
|
||||
import { z } from "zod";
|
||||
import { toastError } from "@/components/Toast";
|
||||
@@ -33,9 +33,9 @@ type PresetStoreData = {
|
||||
};
|
||||
|
||||
export type PresetStore = PresetStoreData & {
|
||||
insert: <T extends PresetSchema>(preset: Preset<T>) => PersistedPreset;
|
||||
update: <T extends PresetSchema>(preset: Preset<T>) => void;
|
||||
remove: <T extends PresetSchema>(preset: Preset<T>) => void;
|
||||
insert: <T>(preset: Preset<T>) => PersistedPreset;
|
||||
update: <T>(preset: Preset<T>) => void;
|
||||
remove: <T>(preset: Preset<T>) => void;
|
||||
getById: (id: number) => PersistedPreset | undefined;
|
||||
|
||||
setLatestPresetId: (
|
||||
@@ -100,7 +100,7 @@ export const usePresetStore = create<PresetStore>()(
|
||||
presets: [],
|
||||
latestPresetIds: new Map(),
|
||||
|
||||
insert: <T extends PresetSchema>(preset: Preset<T>) => {
|
||||
insert: <T>(preset: Preset<T>) => {
|
||||
const state = get();
|
||||
const { presets } = state;
|
||||
const ids = presets.map((p: PersistedPreset) => p.id);
|
||||
@@ -115,7 +115,7 @@ export const usePresetStore = create<PresetStore>()(
|
||||
return persistedPreset;
|
||||
},
|
||||
|
||||
update: <T extends PresetSchema>(preset: Preset<T>) => {
|
||||
update: <T>(preset: Preset<T>) => {
|
||||
const state = get();
|
||||
const presets: PersistedPreset[] = state.presets.map(
|
||||
(p: PersistedPreset) =>
|
||||
@@ -124,7 +124,7 @@ export const usePresetStore = create<PresetStore>()(
|
||||
set({ ...state, presets });
|
||||
},
|
||||
|
||||
remove: <T extends PresetSchema>(preset: Preset<T>) => {
|
||||
remove: <T>(preset: Preset<T>) => {
|
||||
const state = get();
|
||||
const presets = state.presets.filter(
|
||||
(p: Preset<any>) => p.id !== preset.id,
|
||||
|
||||
@@ -3,13 +3,13 @@ import {
|
||||
machineIdentificationEquals,
|
||||
} from "@/machines/types";
|
||||
import { usePresetStore } from "./presetStore";
|
||||
import { Preset, PresetData, presetSchema, PresetSchema } from "./preset";
|
||||
import { Preset, presetSchema, PresetSchema } from "./preset";
|
||||
import { deepEquals } from "@/lib/objects";
|
||||
import { useEffect } from "react";
|
||||
import { toastError } from "@/components/Toast";
|
||||
import { z } from "zod";
|
||||
|
||||
export type Presets<T extends PresetSchema> = {
|
||||
export type Presets<T> = {
|
||||
get: () => Preset<T>[];
|
||||
createFromCurrentState: (name: string) => Preset<T>;
|
||||
updateFromCurrentState: (preset: Preset<T>) => Preset<T>;
|
||||
@@ -20,15 +20,15 @@ export type Presets<T extends PresetSchema> = {
|
||||
import: (json: any) => void;
|
||||
};
|
||||
|
||||
export type UsePresetsParams<T extends PresetSchema> = {
|
||||
export type UsePresetsParams<T> = {
|
||||
machine_identification: MachineIdentification;
|
||||
schemas: Map<number, T>;
|
||||
schemas: Map<number, PresetSchema<T>>;
|
||||
schemaVersion: number;
|
||||
currentState?: PresetData<T>;
|
||||
defaultState?: PresetData<T>;
|
||||
currentState?: T;
|
||||
defaultState?: T;
|
||||
};
|
||||
|
||||
export function usePresets<T extends PresetSchema>({
|
||||
export function usePresets<T>({
|
||||
machine_identification,
|
||||
schemas,
|
||||
schemaVersion,
|
||||
@@ -49,7 +49,10 @@ export function usePresets<T extends PresetSchema>({
|
||||
}
|
||||
|
||||
try {
|
||||
const parsed = presetSchema(schema).parse(preset);
|
||||
const parsed: Preset<T> = {
|
||||
...preset,
|
||||
data: schema.parse(preset.data),
|
||||
};
|
||||
|
||||
if (parsed.schemaVersion < schemaVersion) {
|
||||
parsed.schemaVersion = schemaVersion;
|
||||
@@ -93,7 +96,11 @@ export function usePresets<T extends PresetSchema>({
|
||||
return { ...preset, data: currentState };
|
||||
};
|
||||
|
||||
const updateFromCurrentState = (preset: Preset<T>) => {
|
||||
const updateFromCurrentState = (preset: Preset<T>): Preset<T> => {
|
||||
if (!currentState) {
|
||||
return preset;
|
||||
}
|
||||
|
||||
const data = currentState;
|
||||
|
||||
const newPreset = {
|
||||
@@ -153,12 +160,16 @@ export function usePresets<T extends PresetSchema>({
|
||||
};
|
||||
|
||||
const isActive = (preset: Preset<T>) => {
|
||||
return deepEquals(currentState, preset.data);
|
||||
if (!currentState) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return deepEquals(currentState, preset.data ?? {});
|
||||
};
|
||||
|
||||
const importPreset = (json: any) => {
|
||||
try {
|
||||
const anyPreset = presetSchema(z.any()).parse(json);
|
||||
const anyPreset = presetSchema(z.object()).parse(json);
|
||||
|
||||
if (
|
||||
!machineIdentificationEquals(
|
||||
|
||||
@@ -38,6 +38,28 @@ export interface TimeSeriesWithInsert {
|
||||
insert: (series: TimeSeries, valueObj: TimeSeriesValue) => TimeSeries;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configuration object for creating a time series
|
||||
*/
|
||||
export interface TimeSeriesConfig {
|
||||
sampleIntervalShort: number;
|
||||
sampleIntervalLong: number;
|
||||
retentionDurationShort: number;
|
||||
retentionDurationLong: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Default configuration values
|
||||
* Short: 20ms sample, 5s retention
|
||||
* Long: 1s sample, 1h retention
|
||||
*/
|
||||
export const DEFAULT_TIMESERIES_CONFIG: TimeSeriesConfig = {
|
||||
sampleIntervalShort: 20, // 20ms
|
||||
sampleIntervalLong: 1000, // 1s
|
||||
retentionDurationShort: 5000, // 5s
|
||||
retentionDurationLong: 60 * 60 * 1000, // 1h
|
||||
};
|
||||
|
||||
/**
|
||||
* Extract data with time window filtering in chronological order
|
||||
*/
|
||||
@@ -177,13 +199,18 @@ export function seriesToUPlotData(
|
||||
|
||||
/**
|
||||
* Factory function to create a new time series with circular buffers
|
||||
* @param config Optional configuration object. If omitted or partial, defaults from DEFAULT_TIMESERIES_CONFIG are used.
|
||||
*/
|
||||
export const createTimeSeries = (
|
||||
sampleIntervalShort: number,
|
||||
sampleIntervalLong: number,
|
||||
retentionDurationShort: number,
|
||||
retentionDurationLong: number,
|
||||
config: Partial<TimeSeriesConfig> = {},
|
||||
): TimeSeriesWithInsert => {
|
||||
const {
|
||||
sampleIntervalShort,
|
||||
sampleIntervalLong,
|
||||
retentionDurationShort,
|
||||
retentionDurationLong,
|
||||
} = { ...DEFAULT_TIMESERIES_CONFIG, ...config };
|
||||
|
||||
const shortSize = Math.ceil(retentionDurationShort / sampleIntervalShort);
|
||||
const longSize = Math.ceil(retentionDurationLong / sampleIntervalLong);
|
||||
|
||||
|
||||
@@ -1,38 +1,17 @@
|
||||
import { z } from "zod";
|
||||
|
||||
/**
|
||||
* Creates a Zod schema for objects where exactly one property must be defined.
|
||||
* This is useful for handling Rust enum JSON representations where only one variant exists at a time.
|
||||
*
|
||||
* @param schemas A record mapping property names to their Zod schemas
|
||||
* @returns A Zod schema that validates that exactly one property is defined
|
||||
*/
|
||||
export function rustEnumSchema<T extends Record<string, z.ZodType>>(
|
||||
schemas: T,
|
||||
): z.ZodType<{ [K in keyof T]?: z.infer<T[K]> }> {
|
||||
// Create an object schema where all properties are optional
|
||||
const schemaEntries = Object.entries(schemas).map(([key, schema]) => [
|
||||
key,
|
||||
schema.optional(),
|
||||
]);
|
||||
|
||||
const objectSchema = z.object(
|
||||
Object.fromEntries(schemaEntries) as {
|
||||
[K in keyof T]: z.ZodOptional<T[K]>;
|
||||
},
|
||||
export const rustEnum: z.core.CheckFn<object> = (ctx) => {
|
||||
const definedKeys = Object.keys(ctx.value).filter(
|
||||
(key) => ctx.value[key] !== undefined,
|
||||
);
|
||||
|
||||
// Add refinement to ensure exactly one property is defined
|
||||
return objectSchema.refine(
|
||||
(data) => {
|
||||
const definedKeys = Object.keys(data).filter(
|
||||
(key) => data[key] !== undefined,
|
||||
);
|
||||
return definedKeys.length === 1;
|
||||
},
|
||||
{
|
||||
message: "Exactly one property must be defined",
|
||||
path: ["_exclusive"], // Add a path to help with error identification
|
||||
},
|
||||
);
|
||||
}
|
||||
if (definedKeys.length === 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
ctx.issues.push({
|
||||
code: "custom",
|
||||
message: "Exactly one property must be defined",
|
||||
input: definedKeys,
|
||||
});
|
||||
};
|
||||
@@ -1,19 +0,0 @@
|
||||
import { z } from "zod";
|
||||
|
||||
// super refine function to validate if a string is a valid u16 number
|
||||
export function validateU16<T>(): (value: T, ctx: z.RefinementCtx) => boolean {
|
||||
const refine = (value: T, ctx: z.RefinementCtx) => {
|
||||
console.log("value", value);
|
||||
if (typeof value === "string") {
|
||||
const number = parseInt(value, 10);
|
||||
if (number < 0 || number > 65535) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: "Wert muss u16 integer sein",
|
||||
});
|
||||
}
|
||||
}
|
||||
return true;
|
||||
};
|
||||
return refine;
|
||||
}
|
||||
@@ -76,12 +76,12 @@ export function useAnalogInputTestMachine() {
|
||||
);
|
||||
};
|
||||
|
||||
const timeSeries: TimeSeriesWithInsert = createTimeSeries(
|
||||
20,
|
||||
1000,
|
||||
5000,
|
||||
60 * 60 * 1000,
|
||||
);
|
||||
const timeSeries: TimeSeriesWithInsert = createTimeSeries({
|
||||
sampleIntervalShort: 20,
|
||||
sampleIntervalLong: 1000,
|
||||
retentionDurationShort: 5000,
|
||||
retentionDurationLong: 60 * 60 * 1000,
|
||||
});
|
||||
|
||||
const [seriesData, setSeriesData] = useState<TimeSeries | null>(
|
||||
timeSeries.initialTimeSeries,
|
||||
|
||||
@@ -131,6 +131,9 @@ export function Aquapath1ControlPage() {
|
||||
title="Set Reservoir 1 Target Temperature"
|
||||
value={frontTargetTemperature}
|
||||
unit="C"
|
||||
step={0.1}
|
||||
min={0.0}
|
||||
max={80.0}
|
||||
renderValue={(value) => value.toFixed(1)}
|
||||
onChange={(val) => {
|
||||
setFrontTemperature(val);
|
||||
@@ -154,6 +157,9 @@ export function Aquapath1ControlPage() {
|
||||
title="Set Reservoir 2 Target Temperature"
|
||||
value={backTargetTemperature}
|
||||
unit="C"
|
||||
step={0.1}
|
||||
min={0.0}
|
||||
max={80.0}
|
||||
renderValue={(value) => value.toFixed(1)}
|
||||
onChange={(val) => {
|
||||
setBackTemperature(val);
|
||||
|
||||
@@ -98,37 +98,17 @@ export type Aquapath1NamespaceStore = {
|
||||
back_temp_reservoir: TimeSeries;
|
||||
};
|
||||
|
||||
// Constants for time durations
|
||||
const TWENTY_MILLISECOND = 20;
|
||||
const ONE_SECOND = 1000;
|
||||
const FIVE_SECOND = 5 * ONE_SECOND;
|
||||
const ONE_HOUR = 60 * 60 * ONE_SECOND;
|
||||
|
||||
const { initialTimeSeries: front_temperature, insert: addTemperature1 } =
|
||||
createTimeSeries(TWENTY_MILLISECOND, ONE_SECOND, FIVE_SECOND, ONE_HOUR);
|
||||
|
||||
createTimeSeries();
|
||||
const { initialTimeSeries: back_temperature, insert: addTemperature2 } =
|
||||
createTimeSeries(TWENTY_MILLISECOND, ONE_SECOND, FIVE_SECOND, ONE_HOUR);
|
||||
|
||||
createTimeSeries();
|
||||
const { initialTimeSeries: front_temp_reservoir, insert: addTempReserv1 } =
|
||||
createTimeSeries(TWENTY_MILLISECOND, ONE_SECOND, FIVE_SECOND, ONE_HOUR);
|
||||
|
||||
createTimeSeries();
|
||||
const { initialTimeSeries: back_temp_reservoir, insert: addTempReserv2 } =
|
||||
createTimeSeries(TWENTY_MILLISECOND, ONE_SECOND, FIVE_SECOND, ONE_HOUR);
|
||||
createTimeSeries();
|
||||
const { initialTimeSeries: front_flow, insert: addFlow1 } = createTimeSeries();
|
||||
const { initialTimeSeries: back_flow, insert: addFlow2 } = createTimeSeries();
|
||||
|
||||
const { initialTimeSeries: front_flow, insert: addFlow1 } = createTimeSeries(
|
||||
TWENTY_MILLISECOND,
|
||||
ONE_SECOND,
|
||||
FIVE_SECOND,
|
||||
ONE_HOUR,
|
||||
);
|
||||
|
||||
const { initialTimeSeries: back_flow, insert: addFlow2 } = createTimeSeries(
|
||||
TWENTY_MILLISECOND,
|
||||
ONE_SECOND,
|
||||
FIVE_SECOND,
|
||||
ONE_HOUR,
|
||||
);
|
||||
/**
|
||||
* Factory function to create a new Aquapath namespace store
|
||||
* @returns A new Zustand store instance for Aquapath namespace
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
PresetPreviewEntries,
|
||||
previewSeparator,
|
||||
} from "@/components/preset/PresetPreviewTable";
|
||||
import { Preset, PresetData } from "@/lib/preset/preset";
|
||||
import { Preset } from "@/lib/preset/preset";
|
||||
|
||||
const extruder1PresetDataSchema = z
|
||||
.object({
|
||||
@@ -31,7 +31,7 @@ const extruder1PresetDataSchema = z
|
||||
})
|
||||
.partial();
|
||||
|
||||
type Extruder2 = typeof extruder1PresetDataSchema;
|
||||
type Extruder2 = z.infer<typeof extruder1PresetDataSchema>;
|
||||
|
||||
const schemas = new Map([[1, extruder1PresetDataSchema]]);
|
||||
|
||||
@@ -39,76 +39,69 @@ const previewEntries: PresetPreviewEntries<Extruder2> = [
|
||||
{
|
||||
name: "Target Front Temperature",
|
||||
unit: "C",
|
||||
renderValue: (data: PresetData<Extruder2>) =>
|
||||
renderValue: (data: Extruder2) =>
|
||||
data.targetFrontHeatingTemperature?.toFixed(1),
|
||||
},
|
||||
{
|
||||
name: "Target Middle Temperature",
|
||||
unit: "C",
|
||||
renderValue: (data: PresetData<Extruder2>) =>
|
||||
renderValue: (data: Extruder2) =>
|
||||
data.targetMiddleHeatingTemperature?.toFixed(1),
|
||||
},
|
||||
{
|
||||
name: "Target Back Temperature",
|
||||
unit: "C",
|
||||
renderValue: (data: PresetData<Extruder2>) =>
|
||||
renderValue: (data: Extruder2) =>
|
||||
data.targetBackHeatingTemperature?.toFixed(1),
|
||||
},
|
||||
{
|
||||
name: "Target Nozzle Temperature",
|
||||
unit: "C",
|
||||
renderValue: (data: PresetData<Extruder2>) =>
|
||||
renderValue: (data: Extruder2) =>
|
||||
data.targetNozzleHeatingTemperature?.toFixed(1),
|
||||
},
|
||||
previewSeparator,
|
||||
{
|
||||
name: "Inverter Regulation",
|
||||
renderValue: (data: PresetData<Extruder2>) => data.inverterRegulation,
|
||||
renderValue: (data: Extruder2) => data.inverterRegulation,
|
||||
},
|
||||
{
|
||||
name: "Target Inverter RPM",
|
||||
unit: "rpm",
|
||||
renderValue: (data: PresetData<Extruder2>) =>
|
||||
data.targetInverterRpm?.toFixed(2),
|
||||
renderValue: (data: Extruder2) => data.targetInverterRpm?.toFixed(2),
|
||||
},
|
||||
{
|
||||
name: "Target Inverter Pressure",
|
||||
unit: "bar",
|
||||
renderValue: (data: PresetData<Extruder2>) =>
|
||||
data.targetInverterPressure?.toFixed(1),
|
||||
renderValue: (data: Extruder2) => data.targetInverterPressure?.toFixed(1),
|
||||
},
|
||||
{
|
||||
name: "Inverter Direction",
|
||||
renderValue: (data: PresetData<Extruder2>) =>
|
||||
data.inverterRotationDirection,
|
||||
renderValue: (data: Extruder2) => data.inverterRotationDirection,
|
||||
},
|
||||
previewSeparator,
|
||||
{
|
||||
name: "Pressure Limit",
|
||||
unit: "bar",
|
||||
renderValue: (data: PresetData<Extruder2>) =>
|
||||
data.pressureLimit?.toFixed(1),
|
||||
renderValue: (data: Extruder2) => data.pressureLimit?.toFixed(1),
|
||||
},
|
||||
{
|
||||
name: "Enable Pressure Limit",
|
||||
renderValue: (data: PresetData<Extruder2>) =>
|
||||
renderValue: (data: Extruder2) =>
|
||||
data.pressureLimitEnabled ? "on" : "off",
|
||||
},
|
||||
previewSeparator,
|
||||
{
|
||||
name: "PID Pressue Kp",
|
||||
renderValue: (data: PresetData<Extruder2>) =>
|
||||
data.pidPressureKp?.toFixed(4),
|
||||
renderValue: (data: Extruder2) => data.pidPressureKp?.toFixed(4),
|
||||
},
|
||||
{
|
||||
name: "PID Pressue Ki",
|
||||
renderValue: (data: PresetData<Extruder2>) =>
|
||||
data.pidPressureKi?.toFixed(4),
|
||||
renderValue: (data: Extruder2) => data.pidPressureKi?.toFixed(4),
|
||||
},
|
||||
{
|
||||
name: "PID Pressue Kd",
|
||||
renderValue: (data: PresetData<Extruder2>) =>
|
||||
data.pidPressureKd?.toFixed(4),
|
||||
renderValue: (data: Extruder2) => data.pidPressureKd?.toFixed(4),
|
||||
},
|
||||
];
|
||||
|
||||
@@ -135,7 +128,7 @@ export function Extruder2PresetsPage() {
|
||||
setPressurePidKp,
|
||||
} = useExtruder2();
|
||||
|
||||
const toPresetData = (s?: typeof state): PresetData<Extruder2> => ({
|
||||
const toPresetData = (s?: typeof state): Extruder2 => ({
|
||||
targetFrontHeatingTemperature: s?.heating_states.front.target_temperature,
|
||||
targetMiddleHeatingTemperature: s?.heating_states.middle.target_temperature,
|
||||
targetBackHeatingTemperature: s?.heating_states.back.target_temperature,
|
||||
@@ -161,32 +154,32 @@ export function Extruder2PresetsPage() {
|
||||
|
||||
const applyPreset = (preset: Preset<Extruder2>) => {
|
||||
setFrontHeatingTemperature(
|
||||
preset.data.targetFrontHeatingTemperature ?? 150.0,
|
||||
preset?.data?.targetFrontHeatingTemperature ?? 150.0,
|
||||
);
|
||||
setMiddleHeatingTemperature(
|
||||
preset.data.targetMiddleHeatingTemperature ?? 150.0,
|
||||
preset?.data?.targetMiddleHeatingTemperature ?? 150.0,
|
||||
);
|
||||
setBackHeatingTemperature(
|
||||
preset.data.targetBackHeatingTemperature ?? 150.0,
|
||||
preset?.data?.targetBackHeatingTemperature ?? 150.0,
|
||||
);
|
||||
setNozzleHeatingTemperature(
|
||||
preset.data.targetNozzleHeatingTemperature ?? 150.0,
|
||||
preset?.data?.targetNozzleHeatingTemperature ?? 150.0,
|
||||
);
|
||||
|
||||
setInverterTargetRpm(preset.data.targetInverterRpm ?? 0);
|
||||
setInverterTargetPressure(preset.data.targetInverterPressure ?? 0);
|
||||
setInverterRegulation(preset.data.inverterRegulation === "RPM");
|
||||
setInverterTargetRpm(preset?.data?.targetInverterRpm ?? 0);
|
||||
setInverterTargetPressure(preset?.data?.targetInverterPressure ?? 0);
|
||||
setInverterRegulation(preset?.data?.inverterRegulation === "RPM");
|
||||
|
||||
setInverterRotationDirection(
|
||||
preset.data.inverterRotationDirection === "Forward",
|
||||
preset?.data?.inverterRotationDirection === "Forward",
|
||||
);
|
||||
|
||||
setExtruderPressureLimit(preset.data.pressureLimit ?? 100.0);
|
||||
setExtruderPressureLimitEnabled(preset.data.pressureLimitEnabled ?? true);
|
||||
setExtruderPressureLimit(preset?.data?.pressureLimit ?? 100.0);
|
||||
setExtruderPressureLimitEnabled(preset?.data?.pressureLimitEnabled ?? true);
|
||||
|
||||
setPressurePidKp(preset.data.pidPressureKp ?? 0.16);
|
||||
setPressurePidKi(preset.data.pidPressureKi ?? 0.0);
|
||||
setPressurePidKd(preset.data.pidPressureKd ?? 0.008);
|
||||
setPressurePidKp(preset?.data?.pidPressureKp ?? 0.16);
|
||||
setPressurePidKi(preset?.data?.pidPressureKi ?? 0.0);
|
||||
setPressurePidKd(preset?.data?.pidPressureKd ?? 0.008);
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
@@ -223,64 +223,35 @@ export type Extruder2NamespaceStore = {
|
||||
totalEnergyKWh: TimeSeries;
|
||||
};
|
||||
|
||||
// Constants for time durations
|
||||
const TWENTY_MILLISECOND = 20;
|
||||
const ONE_SECOND = 1000;
|
||||
const FIVE_SECOND = 5 * ONE_SECOND;
|
||||
const ONE_HOUR = 60 * 60 * ONE_SECOND;
|
||||
|
||||
const { initialTimeSeries: pressure, insert: addPressure } = createTimeSeries(
|
||||
TWENTY_MILLISECOND,
|
||||
ONE_SECOND,
|
||||
FIVE_SECOND,
|
||||
ONE_HOUR,
|
||||
);
|
||||
|
||||
const { initialTimeSeries: pressure, insert: addPressure } = createTimeSeries();
|
||||
const { initialTimeSeries: backTemperature, insert: addBackTemperature } =
|
||||
createTimeSeries(TWENTY_MILLISECOND, ONE_SECOND, FIVE_SECOND, ONE_HOUR);
|
||||
|
||||
createTimeSeries();
|
||||
const { initialTimeSeries: frontTemperature, insert: addFrontTemperature } =
|
||||
createTimeSeries(TWENTY_MILLISECOND, ONE_SECOND, FIVE_SECOND, ONE_HOUR);
|
||||
|
||||
createTimeSeries();
|
||||
const { initialTimeSeries: middleTemperature, insert: addMiddleTemperature } =
|
||||
createTimeSeries(TWENTY_MILLISECOND, ONE_SECOND, FIVE_SECOND, ONE_HOUR);
|
||||
|
||||
createTimeSeries();
|
||||
const { initialTimeSeries: nozzleTemperature, insert: addNozzleTemperature } =
|
||||
createTimeSeries(TWENTY_MILLISECOND, ONE_SECOND, FIVE_SECOND, ONE_HOUR);
|
||||
|
||||
createTimeSeries();
|
||||
const { initialTimeSeries: nozzlePower, insert: addNozzlePower } =
|
||||
createTimeSeries(TWENTY_MILLISECOND, ONE_SECOND, FIVE_SECOND, ONE_HOUR);
|
||||
|
||||
createTimeSeries();
|
||||
const { initialTimeSeries: frontPower, insert: addFrontPower } =
|
||||
createTimeSeries(TWENTY_MILLISECOND, ONE_SECOND, FIVE_SECOND, ONE_HOUR);
|
||||
|
||||
createTimeSeries();
|
||||
const { initialTimeSeries: middlePower, insert: addMiddlePower } =
|
||||
createTimeSeries(TWENTY_MILLISECOND, ONE_SECOND, FIVE_SECOND, ONE_HOUR);
|
||||
|
||||
const { initialTimeSeries: backPower, insert: addBackPower } = createTimeSeries(
|
||||
TWENTY_MILLISECOND,
|
||||
ONE_SECOND,
|
||||
FIVE_SECOND,
|
||||
ONE_HOUR,
|
||||
);
|
||||
|
||||
createTimeSeries();
|
||||
const { initialTimeSeries: backPower, insert: addBackPower } =
|
||||
createTimeSeries();
|
||||
const { initialTimeSeries: combinedPower, insert: addCombinedPower } =
|
||||
createTimeSeries(TWENTY_MILLISECOND, ONE_SECOND, FIVE_SECOND, ONE_HOUR);
|
||||
|
||||
createTimeSeries();
|
||||
const { initialTimeSeries: totalEnergyKWh, insert: addTotalEnergyKWh } =
|
||||
createTimeSeries(TWENTY_MILLISECOND, ONE_SECOND, FIVE_SECOND, ONE_HOUR);
|
||||
|
||||
createTimeSeries();
|
||||
const { initialTimeSeries: motorCurrent, insert: addMotorCurrent } =
|
||||
createTimeSeries(TWENTY_MILLISECOND, ONE_SECOND, FIVE_SECOND, ONE_HOUR);
|
||||
|
||||
createTimeSeries();
|
||||
const { initialTimeSeries: motorFrequency, insert: addMotorFrequency } =
|
||||
createTimeSeries(TWENTY_MILLISECOND, ONE_SECOND, FIVE_SECOND, ONE_HOUR);
|
||||
|
||||
createTimeSeries();
|
||||
const { initialTimeSeries: motorScrewRpm, insert: addMotorScrewRpm } =
|
||||
createTimeSeries(TWENTY_MILLISECOND, ONE_SECOND, FIVE_SECOND, ONE_HOUR);
|
||||
|
||||
createTimeSeries();
|
||||
const { initialTimeSeries: motorPower, insert: addMotorPower } =
|
||||
createTimeSeries(TWENTY_MILLISECOND, ONE_SECOND, FIVE_SECOND, ONE_HOUR);
|
||||
createTimeSeries();
|
||||
|
||||
export function extruder2MessageHandler(
|
||||
store: StoreApi<Extruder2NamespaceStore>,
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
PresetPreviewEntries,
|
||||
previewSeparator,
|
||||
} from "@/components/preset/PresetPreviewTable";
|
||||
import { Preset, PresetData } from "@/lib/preset/preset";
|
||||
import { Preset } from "@/lib/preset/preset";
|
||||
|
||||
const extruder3PresetDataSchema = z
|
||||
.object({
|
||||
@@ -31,7 +31,7 @@ const extruder3PresetDataSchema = z
|
||||
})
|
||||
.partial();
|
||||
|
||||
type Extruder3 = typeof extruder3PresetDataSchema;
|
||||
type Extruder3 = z.infer<typeof extruder3PresetDataSchema>;
|
||||
|
||||
const schemas = new Map([[1, extruder3PresetDataSchema]]);
|
||||
|
||||
@@ -39,76 +39,69 @@ const previewEntries: PresetPreviewEntries<Extruder3> = [
|
||||
{
|
||||
name: "Target Front Temperature",
|
||||
unit: "C",
|
||||
renderValue: (data: PresetData<Extruder3>) =>
|
||||
renderValue: (data: Extruder3) =>
|
||||
data.targetFrontHeatingTemperature?.toFixed(1),
|
||||
},
|
||||
{
|
||||
name: "Target Middle Temperature",
|
||||
unit: "C",
|
||||
renderValue: (data: PresetData<Extruder3>) =>
|
||||
renderValue: (data: Extruder3) =>
|
||||
data.targetMiddleHeatingTemperature?.toFixed(1),
|
||||
},
|
||||
{
|
||||
name: "Target Back Temperature",
|
||||
unit: "C",
|
||||
renderValue: (data: PresetData<Extruder3>) =>
|
||||
renderValue: (data: Extruder3) =>
|
||||
data.targetBackHeatingTemperature?.toFixed(1),
|
||||
},
|
||||
{
|
||||
name: "Target Nozzle Temperature",
|
||||
unit: "C",
|
||||
renderValue: (data: PresetData<Extruder3>) =>
|
||||
renderValue: (data: Extruder3) =>
|
||||
data.targetNozzleHeatingTemperature?.toFixed(1),
|
||||
},
|
||||
previewSeparator,
|
||||
{
|
||||
name: "Inverter Regulation",
|
||||
renderValue: (data: PresetData<Extruder3>) => data.inverterRegulation,
|
||||
renderValue: (data: Extruder3) => data.inverterRegulation,
|
||||
},
|
||||
{
|
||||
name: "Target Inverter RPM",
|
||||
unit: "rpm",
|
||||
renderValue: (data: PresetData<Extruder3>) =>
|
||||
data.targetInverterRpm?.toFixed(2),
|
||||
renderValue: (data: Extruder3) => data.targetInverterRpm?.toFixed(2),
|
||||
},
|
||||
{
|
||||
name: "Target Inverter Pressure",
|
||||
unit: "bar",
|
||||
renderValue: (data: PresetData<Extruder3>) =>
|
||||
data.targetInverterPressure?.toFixed(1),
|
||||
renderValue: (data: Extruder3) => data.targetInverterPressure?.toFixed(1),
|
||||
},
|
||||
{
|
||||
name: "Inverter Direction",
|
||||
renderValue: (data: PresetData<Extruder3>) =>
|
||||
data.inverterRotationDirection,
|
||||
renderValue: (data: Extruder3) => data.inverterRotationDirection,
|
||||
},
|
||||
previewSeparator,
|
||||
{
|
||||
name: "Pressure Limit",
|
||||
unit: "bar",
|
||||
renderValue: (data: PresetData<Extruder3>) =>
|
||||
data.pressureLimit?.toFixed(1),
|
||||
renderValue: (data: Extruder3) => data.pressureLimit?.toFixed(1),
|
||||
},
|
||||
{
|
||||
name: "Enable Pressure Limit",
|
||||
renderValue: (data: PresetData<Extruder3>) =>
|
||||
renderValue: (data: Extruder3) =>
|
||||
data.pressureLimitEnabled ? "on" : "off",
|
||||
},
|
||||
previewSeparator,
|
||||
{
|
||||
name: "PID Pressue Kp",
|
||||
renderValue: (data: PresetData<Extruder3>) =>
|
||||
data.pidPressureKp?.toFixed(4),
|
||||
renderValue: (data: Extruder3) => data.pidPressureKp?.toFixed(4),
|
||||
},
|
||||
{
|
||||
name: "PID Pressue Ki",
|
||||
renderValue: (data: PresetData<Extruder3>) =>
|
||||
data.pidPressureKi?.toFixed(4),
|
||||
renderValue: (data: Extruder3) => data.pidPressureKi?.toFixed(4),
|
||||
},
|
||||
{
|
||||
name: "PID Pressue Kd",
|
||||
renderValue: (data: PresetData<Extruder3>) =>
|
||||
data.pidPressureKd?.toFixed(4),
|
||||
renderValue: (data: Extruder3) => data.pidPressureKd?.toFixed(4),
|
||||
},
|
||||
];
|
||||
|
||||
@@ -135,7 +128,7 @@ export function Extruder3PresetsPage() {
|
||||
setPressurePidKp,
|
||||
} = useExtruder3();
|
||||
|
||||
const toPresetData = (s?: typeof state): PresetData<Extruder3> => ({
|
||||
const toPresetData = (s?: typeof state): Extruder3 => ({
|
||||
targetFrontHeatingTemperature: s?.heating_states.front.target_temperature,
|
||||
targetMiddleHeatingTemperature: s?.heating_states.middle.target_temperature,
|
||||
targetBackHeatingTemperature: s?.heating_states.back.target_temperature,
|
||||
@@ -161,32 +154,32 @@ export function Extruder3PresetsPage() {
|
||||
|
||||
const applyPreset = (preset: Preset<Extruder3>) => {
|
||||
setFrontHeatingTemperature(
|
||||
preset.data.targetFrontHeatingTemperature ?? 150.0,
|
||||
preset?.data?.targetFrontHeatingTemperature ?? 150.0,
|
||||
);
|
||||
setMiddleHeatingTemperature(
|
||||
preset.data.targetMiddleHeatingTemperature ?? 150.0,
|
||||
preset?.data?.targetMiddleHeatingTemperature ?? 150.0,
|
||||
);
|
||||
setBackHeatingTemperature(
|
||||
preset.data.targetBackHeatingTemperature ?? 150.0,
|
||||
preset?.data?.targetBackHeatingTemperature ?? 150.0,
|
||||
);
|
||||
setNozzleHeatingTemperature(
|
||||
preset.data.targetNozzleHeatingTemperature ?? 150.0,
|
||||
preset?.data?.targetNozzleHeatingTemperature ?? 150.0,
|
||||
);
|
||||
|
||||
setInverterTargetRpm(preset.data.targetInverterRpm ?? 0);
|
||||
setInverterTargetPressure(preset.data.targetInverterPressure ?? 0);
|
||||
setInverterRegulation(preset.data.inverterRegulation === "RPM");
|
||||
setInverterTargetRpm(preset?.data?.targetInverterRpm ?? 0);
|
||||
setInverterTargetPressure(preset?.data?.targetInverterPressure ?? 0);
|
||||
setInverterRegulation(preset?.data?.inverterRegulation === "RPM");
|
||||
|
||||
setInverterRotationDirection(
|
||||
preset.data.inverterRotationDirection === "Forward",
|
||||
preset?.data?.inverterRotationDirection === "Forward",
|
||||
);
|
||||
|
||||
setExtruderPressureLimit(preset.data.pressureLimit ?? 100.0);
|
||||
setExtruderPressureLimitEnabled(preset.data.pressureLimitEnabled ?? true);
|
||||
setExtruderPressureLimit(preset?.data?.pressureLimit ?? 100.0);
|
||||
setExtruderPressureLimitEnabled(preset?.data?.pressureLimitEnabled ?? true);
|
||||
|
||||
setPressurePidKp(preset.data.pidPressureKp ?? 0.16);
|
||||
setPressurePidKi(preset.data.pidPressureKi ?? 0.0);
|
||||
setPressurePidKd(preset.data.pidPressureKd ?? 0.008);
|
||||
setPressurePidKp(preset?.data?.pidPressureKp ?? 0.16);
|
||||
setPressurePidKi(preset?.data?.pidPressureKi ?? 0.0);
|
||||
setPressurePidKd(preset?.data?.pidPressureKd ?? 0.008);
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
@@ -223,64 +223,35 @@ export type Extruder3NamespaceStore = {
|
||||
totalEnergyKWh: TimeSeries;
|
||||
};
|
||||
|
||||
// Constants for time durations
|
||||
const TWENTY_MILLISECOND = 20;
|
||||
const ONE_SECOND = 1000;
|
||||
const FIVE_SECOND = 5 * ONE_SECOND;
|
||||
const ONE_HOUR = 60 * 60 * ONE_SECOND;
|
||||
|
||||
const { initialTimeSeries: pressure, insert: addPressure } = createTimeSeries(
|
||||
TWENTY_MILLISECOND,
|
||||
ONE_SECOND,
|
||||
FIVE_SECOND,
|
||||
ONE_HOUR,
|
||||
);
|
||||
|
||||
const { initialTimeSeries: pressure, insert: addPressure } = createTimeSeries();
|
||||
const { initialTimeSeries: backTemperature, insert: addBackTemperature } =
|
||||
createTimeSeries(TWENTY_MILLISECOND, ONE_SECOND, FIVE_SECOND, ONE_HOUR);
|
||||
|
||||
createTimeSeries();
|
||||
const { initialTimeSeries: frontTemperature, insert: addFrontTemperature } =
|
||||
createTimeSeries(TWENTY_MILLISECOND, ONE_SECOND, FIVE_SECOND, ONE_HOUR);
|
||||
|
||||
createTimeSeries();
|
||||
const { initialTimeSeries: middleTemperature, insert: addMiddleTemperature } =
|
||||
createTimeSeries(TWENTY_MILLISECOND, ONE_SECOND, FIVE_SECOND, ONE_HOUR);
|
||||
|
||||
createTimeSeries();
|
||||
const { initialTimeSeries: nozzleTemperature, insert: addNozzleTemperature } =
|
||||
createTimeSeries(TWENTY_MILLISECOND, ONE_SECOND, FIVE_SECOND, ONE_HOUR);
|
||||
|
||||
createTimeSeries();
|
||||
const { initialTimeSeries: nozzlePower, insert: addNozzlePower } =
|
||||
createTimeSeries(TWENTY_MILLISECOND, ONE_SECOND, FIVE_SECOND, ONE_HOUR);
|
||||
|
||||
createTimeSeries();
|
||||
const { initialTimeSeries: frontPower, insert: addFrontPower } =
|
||||
createTimeSeries(TWENTY_MILLISECOND, ONE_SECOND, FIVE_SECOND, ONE_HOUR);
|
||||
|
||||
createTimeSeries();
|
||||
const { initialTimeSeries: middlePower, insert: addMiddlePower } =
|
||||
createTimeSeries(TWENTY_MILLISECOND, ONE_SECOND, FIVE_SECOND, ONE_HOUR);
|
||||
|
||||
const { initialTimeSeries: backPower, insert: addBackPower } = createTimeSeries(
|
||||
TWENTY_MILLISECOND,
|
||||
ONE_SECOND,
|
||||
FIVE_SECOND,
|
||||
ONE_HOUR,
|
||||
);
|
||||
|
||||
createTimeSeries();
|
||||
const { initialTimeSeries: backPower, insert: addBackPower } =
|
||||
createTimeSeries();
|
||||
const { initialTimeSeries: combinedPower, insert: addCombinedPower } =
|
||||
createTimeSeries(TWENTY_MILLISECOND, ONE_SECOND, FIVE_SECOND, ONE_HOUR);
|
||||
|
||||
createTimeSeries();
|
||||
const { initialTimeSeries: totalEnergyKWh, insert: addTotalEnergyKWh } =
|
||||
createTimeSeries(TWENTY_MILLISECOND, ONE_SECOND, FIVE_SECOND, ONE_HOUR);
|
||||
|
||||
createTimeSeries();
|
||||
const { initialTimeSeries: motorCurrent, insert: addMotorCurrent } =
|
||||
createTimeSeries(TWENTY_MILLISECOND, ONE_SECOND, FIVE_SECOND, ONE_HOUR);
|
||||
|
||||
createTimeSeries();
|
||||
const { initialTimeSeries: motorFrequency, insert: addMotorFrequency } =
|
||||
createTimeSeries(TWENTY_MILLISECOND, ONE_SECOND, FIVE_SECOND, ONE_HOUR);
|
||||
|
||||
createTimeSeries();
|
||||
const { initialTimeSeries: motorScrewRpm, insert: addMotorScrewRpm } =
|
||||
createTimeSeries(TWENTY_MILLISECOND, ONE_SECOND, FIVE_SECOND, ONE_HOUR);
|
||||
|
||||
createTimeSeries();
|
||||
const { initialTimeSeries: motorPower, insert: addMotorPower } =
|
||||
createTimeSeries(TWENTY_MILLISECOND, ONE_SECOND, FIVE_SECOND, ONE_HOUR);
|
||||
createTimeSeries();
|
||||
|
||||
export function extruder3MessageHandler(
|
||||
store: StoreApi<Extruder3NamespaceStore>,
|
||||
|
||||
@@ -12,14 +12,14 @@ import { useLaser1 } from "./useLaser1";
|
||||
export function Laser1GraphsPage() {
|
||||
const { diameter, x_diameter, y_diameter, roundness, state } = useLaser1();
|
||||
|
||||
const syncHook = useGraphSync("diameter-group");
|
||||
const syncHook = useGraphSync("diameter-roundness-group");
|
||||
const targetDiameter = state?.laser_state?.target_diameter ?? 0;
|
||||
const lowerTolerance = state?.laser_state?.lower_tolerance ?? 0;
|
||||
const higherTolerance = state?.laser_state?.higher_tolerance ?? 0;
|
||||
|
||||
const isTwoAxis = !!x_diameter?.current || !!y_diameter?.current;
|
||||
|
||||
// Transform roundness from ratio (0-1) to percentage (0-100)
|
||||
// Convert roundness from ratio (0-1) to percentage (0-100)
|
||||
const roundnessPercent = React.useMemo(() => {
|
||||
if (!roundness) return null;
|
||||
|
||||
@@ -45,120 +45,110 @@ export function Laser1GraphsPage() {
|
||||
};
|
||||
}, [roundness]);
|
||||
|
||||
const diameterColor = "#3b82f6";
|
||||
const xDiameterColor = "#ef4444";
|
||||
const yDiameterColor = "#22c55e";
|
||||
const roundnessColor = "#eab308";
|
||||
const diameterColor = "#3b82f6"; // blue
|
||||
const xDiameterColor = "#ef4444"; // red
|
||||
const yDiameterColor = "#22c55e"; // green
|
||||
const roundnessColor = "#eab308"; // yellow
|
||||
|
||||
const config: GraphConfig = {
|
||||
title: "Diameter",
|
||||
defaultTimeWindow: 30 * 60 * 1000, // 30 minutes
|
||||
exportFilename: "diameter_data",
|
||||
// config for all graphs
|
||||
const baseGraphConfig: GraphConfig = {
|
||||
title: "",
|
||||
defaultTimeWindow: 30 * 60 * 1000, // 30 minutes Standard
|
||||
colors: {
|
||||
primary: diameterColor,
|
||||
grid: "#e2e8f0",
|
||||
axis: "#64748b",
|
||||
background: "#ffffff",
|
||||
},
|
||||
};
|
||||
if (!isTwoAxis) {
|
||||
return (
|
||||
<Page className="pb-27">
|
||||
<div className="flex flex-col gap-4">
|
||||
|
||||
// Diameter-Graph (Diameter, X-Diameter, Y-Diameter)
|
||||
const diameterGraphData = [
|
||||
{
|
||||
newData: diameter,
|
||||
color: diameterColor,
|
||||
title: "Diameter",
|
||||
lines: [
|
||||
// Tolerance for diameter
|
||||
{
|
||||
type: "threshold" as const,
|
||||
value: targetDiameter + higherTolerance,
|
||||
label: "Upper Tolerance",
|
||||
color: diameterColor,
|
||||
dash: [5, 5],
|
||||
},
|
||||
{
|
||||
type: "threshold" as const,
|
||||
value: targetDiameter - lowerTolerance,
|
||||
label: "Lower Tolerance",
|
||||
color: diameterColor,
|
||||
dash: [5, 5],
|
||||
},
|
||||
{
|
||||
type: "target" as const,
|
||||
value: targetDiameter,
|
||||
label: "Target",
|
||||
color: diameterColor,
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
...(isTwoAxis && x_diameter
|
||||
? [{ newData: x_diameter, color: xDiameterColor, title: "X-Diameter" }]
|
||||
: []),
|
||||
...(isTwoAxis && y_diameter
|
||||
? [{ newData: y_diameter, color: yDiameterColor, title: "Y-Diameter" }]
|
||||
: []),
|
||||
];
|
||||
|
||||
// Roundness-Graph
|
||||
const roundnessGraphData = {
|
||||
newData: roundnessPercent,
|
||||
color: roundnessColor,
|
||||
title: "Roundness (%)",
|
||||
};
|
||||
|
||||
return (
|
||||
<Page className="pb-27">
|
||||
<div className="flex flex-col gap-4">
|
||||
{/* Graph 1: Diameter */}
|
||||
<AutoSyncedBigGraph
|
||||
syncHook={syncHook}
|
||||
newData={diameterGraphData}
|
||||
unit="mm"
|
||||
renderValue={(value) => value.toFixed(3)}
|
||||
config={{
|
||||
...baseGraphConfig,
|
||||
title: "Diameter (mm)",
|
||||
exportFilename: "diameter_data",
|
||||
colors: {
|
||||
...baseGraphConfig.colors,
|
||||
primary: diameterColor,
|
||||
},
|
||||
}}
|
||||
graphId="diameter-graph"
|
||||
/>
|
||||
|
||||
{/* Graph 2: Roundness */}
|
||||
{isTwoAxis && roundnessPercent && (
|
||||
<AutoSyncedBigGraph
|
||||
syncHook={syncHook}
|
||||
newData={{
|
||||
newData: diameter,
|
||||
color: diameterColor,
|
||||
lines: [
|
||||
{
|
||||
type: "threshold",
|
||||
value: targetDiameter + higherTolerance,
|
||||
label: "Upper Threshold",
|
||||
color: diameterColor,
|
||||
dash: [5, 5],
|
||||
},
|
||||
{
|
||||
type: "threshold",
|
||||
value: targetDiameter - lowerTolerance,
|
||||
label: "Lower Threshold",
|
||||
color: diameterColor,
|
||||
dash: [5, 5],
|
||||
},
|
||||
{
|
||||
type: "target",
|
||||
value: targetDiameter,
|
||||
label: "Target",
|
||||
color: diameterColor,
|
||||
},
|
||||
],
|
||||
}}
|
||||
unit="mm"
|
||||
renderValue={(value) => value.toFixed(3)}
|
||||
config={config}
|
||||
graphId="diameter-graph"
|
||||
/>
|
||||
</div>
|
||||
<SyncedFloatingControlPanel controlProps={syncHook.controlProps} />
|
||||
</Page>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<Page className="pb-27">
|
||||
<div className="flex flex-col gap-4">
|
||||
<AutoSyncedBigGraph
|
||||
syncHook={syncHook}
|
||||
newData={[
|
||||
{
|
||||
newData: diameter,
|
||||
color: diameterColor,
|
||||
title: "Diameter",
|
||||
lines: [
|
||||
{
|
||||
type: "threshold",
|
||||
value: targetDiameter + higherTolerance,
|
||||
label: "Upper Threshold",
|
||||
color: diameterColor,
|
||||
dash: [5, 5],
|
||||
},
|
||||
{
|
||||
type: "threshold",
|
||||
value: targetDiameter - lowerTolerance,
|
||||
label: "Lower Threshold",
|
||||
color: diameterColor,
|
||||
dash: [5, 5],
|
||||
},
|
||||
{
|
||||
type: "target",
|
||||
value: targetDiameter,
|
||||
label: "Target",
|
||||
color: diameterColor,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
newData: x_diameter,
|
||||
color: xDiameterColor,
|
||||
title: "X-Diameter",
|
||||
},
|
||||
{
|
||||
newData: y_diameter,
|
||||
color: yDiameterColor,
|
||||
title: "Y-Diameter",
|
||||
},
|
||||
{
|
||||
newData: roundnessPercent,
|
||||
color: roundnessColor,
|
||||
title: "Roundness (%)",
|
||||
},
|
||||
]}
|
||||
newData={roundnessGraphData}
|
||||
unit="%"
|
||||
renderValue={(value) => value.toFixed(2)}
|
||||
config={config}
|
||||
graphId="diameter-graph"
|
||||
config={{
|
||||
...baseGraphConfig,
|
||||
title: "Roundness (%)",
|
||||
exportFilename: "roundness_data",
|
||||
colors: {
|
||||
...baseGraphConfig.colors,
|
||||
primary: roundnessColor,
|
||||
},
|
||||
}}
|
||||
graphId="roundness-graph"
|
||||
/>
|
||||
</div>
|
||||
<SyncedFloatingControlPanel controlProps={syncHook.controlProps} />
|
||||
</Page>
|
||||
);
|
||||
}
|
||||
)}
|
||||
</div>
|
||||
<SyncedFloatingControlPanel controlProps={syncHook.controlProps} />
|
||||
</Page>
|
||||
);
|
||||
}
|
||||
@@ -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,7 +1,7 @@
|
||||
import React from "react";
|
||||
import { z } from "zod";
|
||||
import { useLaser1 } from "./useLaser1";
|
||||
import { Preset, PresetData } from "@/lib/preset/preset";
|
||||
import { Preset } from "@/lib/preset/preset";
|
||||
import { laser1 } from "@/machines/properties";
|
||||
import { PresetPreviewEntries } from "@/components/preset/PresetPreviewTable";
|
||||
import { PresetsPage } from "@/components/preset/PresetsPage";
|
||||
@@ -14,7 +14,7 @@ const laser1PresetDataSchema = z
|
||||
})
|
||||
.partial();
|
||||
|
||||
type Laser1 = typeof laser1PresetDataSchema;
|
||||
type Laser1 = z.infer<typeof laser1PresetDataSchema>;
|
||||
|
||||
const schemas = new Map([[1, laser1PresetDataSchema]]);
|
||||
|
||||
@@ -22,18 +22,17 @@ const previewEntries: PresetPreviewEntries<Laser1> = [
|
||||
{
|
||||
name: "Target Diameter",
|
||||
unit: "mm",
|
||||
renderValue: (data: PresetData<Laser1>) => data?.targetDiameter?.toFixed(3),
|
||||
renderValue: (data: Laser1) => data?.targetDiameter?.toFixed(3),
|
||||
},
|
||||
{
|
||||
name: "Lower Tolerance",
|
||||
unit: "mm",
|
||||
renderValue: (data: PresetData<Laser1>) => data?.lowerTolerance?.toFixed(3),
|
||||
renderValue: (data: Laser1) => data?.lowerTolerance?.toFixed(3),
|
||||
},
|
||||
{
|
||||
name: "Higher Tolerance",
|
||||
unit: "mm",
|
||||
renderValue: (data: PresetData<Laser1>) =>
|
||||
data?.higherTolerance?.toFixed(3),
|
||||
renderValue: (data: Laser1) => data?.higherTolerance?.toFixed(3),
|
||||
},
|
||||
];
|
||||
|
||||
@@ -47,16 +46,16 @@ export function Laser1PresetsPage() {
|
||||
} = useLaser1();
|
||||
|
||||
const applyPreset = (preset: Preset<Laser1>) => {
|
||||
const targetDiameter = preset.data.targetDiameter ?? 1.75;
|
||||
const lowerTolerance = preset.data?.lowerTolerance ?? 0.05;
|
||||
const higherTolerance = preset.data?.higherTolerance ?? 0.05;
|
||||
const targetDiameter = preset?.data?.targetDiameter ?? 1.75;
|
||||
const lowerTolerance = preset?.data?.lowerTolerance ?? 0.05;
|
||||
const higherTolerance = preset?.data?.higherTolerance ?? 0.05;
|
||||
|
||||
setTargetDiameter(targetDiameter);
|
||||
setLowerTolerance(lowerTolerance);
|
||||
setHigherTolerance(higherTolerance);
|
||||
};
|
||||
|
||||
const toPresetData = (s: typeof state): PresetData<Laser1> => ({
|
||||
const toPresetData = (s: typeof state): Laser1 => ({
|
||||
targetDiameter: s?.laser_state.target_diameter,
|
||||
lowerTolerance: s?.laser_state.lower_tolerance,
|
||||
higherTolerance: s?.laser_state.higher_tolerance,
|
||||
|
||||
@@ -67,29 +67,17 @@ export type Laser1NamespaceStore = {
|
||||
};
|
||||
|
||||
// Constants for time durations
|
||||
const TWENTY_MILLISECOND = 20;
|
||||
const ONE_SECOND = 1000;
|
||||
const HALF_SECOND = 500;
|
||||
const FIVE_SECOND = 5 * ONE_SECOND;
|
||||
const ONE_HOUR = 60 * 60 * ONE_SECOND;
|
||||
const { initialTimeSeries: diameter, insert: addDiameter } = createTimeSeries(
|
||||
TWENTY_MILLISECOND,
|
||||
HALF_SECOND,
|
||||
FIVE_SECOND,
|
||||
ONE_HOUR,
|
||||
);
|
||||
|
||||
const { initialTimeSeries: diameter, insert: addDiameter } = createTimeSeries({
|
||||
sampleIntervalLong: HALF_SECOND,
|
||||
});
|
||||
const { initialTimeSeries: x_diameter, insert: addXDiameter } =
|
||||
createTimeSeries(TWENTY_MILLISECOND, HALF_SECOND, FIVE_SECOND, ONE_HOUR);
|
||||
|
||||
createTimeSeries({ sampleIntervalLong: HALF_SECOND });
|
||||
const { initialTimeSeries: y_diameter, insert: addYDiameter } =
|
||||
createTimeSeries(TWENTY_MILLISECOND, HALF_SECOND, FIVE_SECOND, ONE_HOUR);
|
||||
|
||||
createTimeSeries({ sampleIntervalLong: HALF_SECOND });
|
||||
const { initialTimeSeries: roundness, insert: addRoundness } = createTimeSeries(
|
||||
TWENTY_MILLISECOND,
|
||||
HALF_SECOND,
|
||||
FIVE_SECOND,
|
||||
ONE_HOUR,
|
||||
{ sampleIntervalLong: HALF_SECOND },
|
||||
);
|
||||
|
||||
/**
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -3,7 +3,7 @@ import { useMock1 } from "./useMock";
|
||||
import { mock1 } from "@/machines/properties";
|
||||
|
||||
import { PresetsPage } from "@/components/preset/PresetsPage";
|
||||
import { Preset, PresetData } from "@/lib/preset/preset";
|
||||
import { Preset } from "@/lib/preset/preset";
|
||||
import { PresetPreviewEntries } from "@/components/preset/PresetPreviewTable";
|
||||
import { z } from "zod";
|
||||
|
||||
@@ -15,7 +15,7 @@ const mock1PresetDataSchema = z
|
||||
})
|
||||
.partial();
|
||||
|
||||
type Mock1 = typeof mock1PresetDataSchema;
|
||||
type Mock1 = z.infer<typeof mock1PresetDataSchema>;
|
||||
|
||||
const schemas = new Map([[1, mock1PresetDataSchema]]);
|
||||
|
||||
@@ -23,17 +23,17 @@ const previewEntries: PresetPreviewEntries<Mock1> = [
|
||||
{
|
||||
name: "Frequency 1",
|
||||
unit: "mHz",
|
||||
renderValue: (data: PresetData<Mock1>) => data?.frequency1?.toFixed(3),
|
||||
renderValue: (data: Mock1) => data?.frequency1?.toFixed(3),
|
||||
},
|
||||
{
|
||||
name: "Frequency 2",
|
||||
unit: "mHz",
|
||||
renderValue: (data: PresetData<Mock1>) => data?.frequency2?.toFixed(3),
|
||||
renderValue: (data: Mock1) => data?.frequency2?.toFixed(3),
|
||||
},
|
||||
{
|
||||
name: "Frequency 3",
|
||||
unit: "mHz",
|
||||
renderValue: (data: PresetData<Mock1>) => data?.frequency3?.toFixed(3),
|
||||
renderValue: (data: Mock1) => data?.frequency3?.toFixed(3),
|
||||
},
|
||||
];
|
||||
|
||||
@@ -51,7 +51,7 @@ export function Mock1PresetsPage() {
|
||||
setFrequency3(frequency3);
|
||||
};
|
||||
|
||||
const toPresetData = (s: typeof state): PresetData<Mock1> => ({
|
||||
const toPresetData = (s: typeof state): Mock1 => ({
|
||||
frequency1: s?.frequency1 ?? defaultState?.frequency1,
|
||||
frequency2: s?.frequency2 ?? defaultState?.frequency2,
|
||||
frequency3: s?.frequency3 ?? defaultState?.frequency3,
|
||||
|
||||
@@ -78,18 +78,7 @@ export type Mock1NamespaceStore = {
|
||||
sineWave3: TimeSeries;
|
||||
};
|
||||
|
||||
// Constants for time durations
|
||||
const TWENTY_MILLISECOND = 20;
|
||||
const ONE_SECOND = 1000;
|
||||
const FIVE_SECOND = 5 * ONE_SECOND;
|
||||
const ONE_HOUR = 60 * 60 * ONE_SECOND;
|
||||
|
||||
const { initialTimeSeries: sineWave, insert: addSineWave } = createTimeSeries(
|
||||
TWENTY_MILLISECOND,
|
||||
ONE_SECOND,
|
||||
FIVE_SECOND,
|
||||
ONE_HOUR,
|
||||
);
|
||||
const { initialTimeSeries: sineWave, insert: addSineWave } = createTimeSeries();
|
||||
|
||||
/**
|
||||
* Factory function to create a new Mock1 namespace store
|
||||
|
||||
@@ -104,7 +104,7 @@ export const winder2: MachineProperties = {
|
||||
};
|
||||
|
||||
export const extruder3: MachineProperties = {
|
||||
name: "Extruder V3",
|
||||
name: "Extruder",
|
||||
version: "V3",
|
||||
slug: "extruder3",
|
||||
icon: "qi:Extruder",
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
// each role can only be given once
|
||||
|
||||
import { IconName } from "@/components/Icon";
|
||||
import { rustEnumSchema } from "@/lib/types";
|
||||
import { rustEnum } from "@/lib/types";
|
||||
import { z } from "zod";
|
||||
|
||||
// EK1100 should have role 0
|
||||
@@ -62,9 +62,11 @@ export type DeviceHardwareIdentificationEthercat = z.infer<
|
||||
typeof deviceHardwareIdentificationEthercatSchema
|
||||
>;
|
||||
|
||||
export const deviceHardwareIdentificationSchema = rustEnumSchema({
|
||||
Ethercat: deviceHardwareIdentificationEthercatSchema,
|
||||
});
|
||||
export const deviceHardwareIdentificationSchema = z
|
||||
.object({
|
||||
Ethercat: deviceHardwareIdentificationEthercatSchema,
|
||||
})
|
||||
.check(rustEnum);
|
||||
|
||||
export type DeviceHardwareIdentification = z.infer<
|
||||
typeof deviceHardwareIdentificationSchema
|
||||
|
||||
@@ -47,22 +47,8 @@ export type WagoPower1NamespaceStore = {
|
||||
voltage: TimeSeries;
|
||||
};
|
||||
|
||||
const TWENTY_MILLISECOND = 20;
|
||||
const ONE_SECOND = 1000;
|
||||
const FIVE_SECOND = 5 * ONE_SECOND;
|
||||
const ONE_HOUR = 60 * 60 * ONE_SECOND;
|
||||
const { initialTimeSeries: voltage, insert: addVoltage } = createTimeSeries(
|
||||
TWENTY_MILLISECOND,
|
||||
ONE_SECOND,
|
||||
FIVE_SECOND,
|
||||
ONE_HOUR,
|
||||
);
|
||||
const { initialTimeSeries: current, insert: addCurrent } = createTimeSeries(
|
||||
TWENTY_MILLISECOND,
|
||||
ONE_SECOND,
|
||||
FIVE_SECOND,
|
||||
ONE_HOUR,
|
||||
);
|
||||
const { initialTimeSeries: voltage, insert: addVoltage } = createTimeSeries();
|
||||
const { initialTimeSeries: current, insert: addCurrent } = createTimeSeries();
|
||||
|
||||
const createWagoPower1NamespaceStore = (): StoreApi<WagoPower1NamespaceStore> =>
|
||||
create<WagoPower1NamespaceStore>(() => {
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -3,7 +3,7 @@ import { useWinder2 } from "./useWinder";
|
||||
import { winder2 } from "@/machines/properties";
|
||||
|
||||
import { PresetsPage } from "@/components/preset/PresetsPage";
|
||||
import { Preset, PresetData } from "@/lib/preset/preset";
|
||||
import { Preset } from "@/lib/preset/preset";
|
||||
import {
|
||||
pullerStateSchema,
|
||||
spoolSpeedControllerStateSchema,
|
||||
@@ -16,19 +16,21 @@ import {
|
||||
|
||||
const winder2PresetDataSchema = z
|
||||
.object({
|
||||
traverse_state: z.object({
|
||||
limit_inner: z.number(),
|
||||
limit_outer: z.number(),
|
||||
step_size: z.number(),
|
||||
padding: z.number(),
|
||||
laserpointer: z.boolean(),
|
||||
}),
|
||||
puller_state: pullerStateSchema,
|
||||
spool_speed_controller_state: spoolSpeedControllerStateSchema,
|
||||
traverse_state: z
|
||||
.object({
|
||||
limit_inner: z.number(),
|
||||
limit_outer: z.number(),
|
||||
step_size: z.number(),
|
||||
padding: z.number(),
|
||||
laserpointer: z.boolean(),
|
||||
})
|
||||
.partial(),
|
||||
puller_state: pullerStateSchema.partial(),
|
||||
spool_speed_controller_state: spoolSpeedControllerStateSchema.partial(),
|
||||
})
|
||||
.deepPartial();
|
||||
.partial();
|
||||
|
||||
type Winder2 = typeof winder2PresetDataSchema;
|
||||
type Winder2 = z.infer<typeof winder2PresetDataSchema>;
|
||||
|
||||
const schemas = new Map([[1, winder2PresetDataSchema]]);
|
||||
|
||||
@@ -36,40 +38,39 @@ const previewEntries: PresetPreviewEntries<Winder2> = [
|
||||
{
|
||||
name: "Inner Traverse Limit",
|
||||
unit: "mm",
|
||||
renderValue: (data: PresetData<Winder2>) =>
|
||||
data?.traverse_state?.limit_inner?.toFixed(1) ?? "N/A",
|
||||
renderValue: (data: Winder2) =>
|
||||
data.traverse_state?.limit_inner?.toFixed(1) ?? "N/A",
|
||||
},
|
||||
{
|
||||
name: "Outer Traverse Limit",
|
||||
unit: "mm",
|
||||
renderValue: (data: PresetData<Winder2>) =>
|
||||
renderValue: (data: Winder2) =>
|
||||
data?.traverse_state?.limit_outer?.toFixed(1) ?? "N/A",
|
||||
},
|
||||
{
|
||||
name: "Traverse Step Size",
|
||||
unit: "mm",
|
||||
renderValue: (data: PresetData<Winder2>) =>
|
||||
renderValue: (data: Winder2) =>
|
||||
data?.traverse_state?.step_size?.toFixed(1) ?? "N/A",
|
||||
},
|
||||
{
|
||||
name: "Traverse Padding",
|
||||
unit: "mm",
|
||||
renderValue: (data: PresetData<Winder2>) =>
|
||||
data?.traverse_state?.padding?.toFixed(1),
|
||||
renderValue: (data: Winder2) => data?.traverse_state?.padding?.toFixed(1),
|
||||
},
|
||||
previewSeparator,
|
||||
{
|
||||
name: "Puller Regulation",
|
||||
renderValue: (data: PresetData<Winder2>) => data?.puller_state?.regulation,
|
||||
renderValue: (data: Winder2) => data?.puller_state?.regulation,
|
||||
},
|
||||
{
|
||||
name: "Puller Direction",
|
||||
renderValue: (data: PresetData<Winder2>) =>
|
||||
renderValue: (data: Winder2) =>
|
||||
data.puller_state?.forward ? "Forward" : "Backward",
|
||||
},
|
||||
{
|
||||
name: "Puller Gear Ratio",
|
||||
renderValue: (data: PresetData<Winder2>) => {
|
||||
renderValue: (data: Winder2) => {
|
||||
const ratio = data.puller_state?.gear_ratio;
|
||||
if (ratio === "OneToOne") return "1:1";
|
||||
if (ratio === "OneToFive") return "1:5";
|
||||
@@ -80,74 +81,73 @@ const previewEntries: PresetPreviewEntries<Winder2> = [
|
||||
{
|
||||
name: "Puller Target Speed",
|
||||
unit: "m/min",
|
||||
renderValue: (data: PresetData<Winder2>) =>
|
||||
data.puller_state?.target_speed?.toFixed(2),
|
||||
renderValue: (data: Winder2) => data.puller_state?.target_speed?.toFixed(2),
|
||||
},
|
||||
{
|
||||
name: "Puller Target Diameter",
|
||||
unit: "mm",
|
||||
renderValue: (data: PresetData<Winder2>) =>
|
||||
renderValue: (data: Winder2) =>
|
||||
data.puller_state?.target_diameter?.toFixed(1),
|
||||
},
|
||||
{
|
||||
name: "Puller Target Diameter",
|
||||
unit: "mm",
|
||||
renderValue: (data: PresetData<Winder2>) =>
|
||||
renderValue: (data: Winder2) =>
|
||||
data.puller_state?.target_diameter?.toFixed(1),
|
||||
},
|
||||
previewSeparator,
|
||||
{
|
||||
name: "Spool Regulation",
|
||||
renderValue: (data: PresetData<Winder2>) =>
|
||||
renderValue: (data: Winder2) =>
|
||||
data.spool_speed_controller_state?.regulation_mode,
|
||||
},
|
||||
{
|
||||
name: "Spool Direction",
|
||||
renderValue: (data: PresetData<Winder2>) =>
|
||||
renderValue: (data: Winder2) =>
|
||||
data.spool_speed_controller_state?.forward ? "Forward" : "Reverse",
|
||||
},
|
||||
{
|
||||
name: "Spool Min Speed",
|
||||
unit: "rpm",
|
||||
renderValue: (data: PresetData<Winder2>) =>
|
||||
renderValue: (data: Winder2) =>
|
||||
data.spool_speed_controller_state?.minmax_min_speed?.toFixed(2),
|
||||
},
|
||||
{
|
||||
name: "Spool Max Speed",
|
||||
unit: "rpm",
|
||||
renderValue: (data: PresetData<Winder2>) =>
|
||||
renderValue: (data: Winder2) =>
|
||||
data.spool_speed_controller_state?.minmax_max_speed?.toFixed(2),
|
||||
},
|
||||
previewSeparator,
|
||||
{
|
||||
name: "Adaptive Spool Tension Target",
|
||||
renderValue: (data: PresetData<Winder2>) =>
|
||||
renderValue: (data: Winder2) =>
|
||||
data.spool_speed_controller_state?.adaptive_tension_target?.toFixed(2),
|
||||
},
|
||||
{
|
||||
name: "Adaptive Spool Learning Rate",
|
||||
renderValue: (data: PresetData<Winder2>) =>
|
||||
renderValue: (data: Winder2) =>
|
||||
data.spool_speed_controller_state?.adaptive_radius_learning_rate?.toFixed(
|
||||
2,
|
||||
),
|
||||
},
|
||||
{
|
||||
name: "Adaptive Spool Max Speed Multiplier",
|
||||
renderValue: (data: PresetData<Winder2>) =>
|
||||
renderValue: (data: Winder2) =>
|
||||
data.spool_speed_controller_state?.adaptive_max_speed_multiplier?.toFixed(
|
||||
1,
|
||||
),
|
||||
},
|
||||
{
|
||||
name: "Adaptive Spool Acceleration Factor",
|
||||
renderValue: (data: PresetData<Winder2>) =>
|
||||
renderValue: (data: Winder2) =>
|
||||
data.spool_speed_controller_state?.adaptive_acceleration_factor?.toFixed(
|
||||
2,
|
||||
),
|
||||
},
|
||||
{
|
||||
name: "Adaptive Spool Deaccel. Urgency",
|
||||
renderValue: (data: PresetData<Winder2>) =>
|
||||
renderValue: (data: Winder2) =>
|
||||
data.spool_speed_controller_state?.adaptive_deacceleration_urgency_multiplier?.toFixed(
|
||||
1,
|
||||
),
|
||||
@@ -230,11 +230,11 @@ export function Winder2PresetsPage() {
|
||||
// );
|
||||
|
||||
enableTraverseLaserpointer(
|
||||
preset.data.traverse_state?.laserpointer ?? false,
|
||||
preset.data?.traverse_state?.laserpointer ?? false,
|
||||
);
|
||||
};
|
||||
|
||||
const toPresetData = (s: typeof state): PresetData<Winder2> => ({
|
||||
const toPresetData = (s: typeof state): Winder2 => ({
|
||||
traverse_state: {
|
||||
limit_inner: s?.traverse_state?.limit_inner,
|
||||
limit_outer: s?.traverse_state?.limit_outer,
|
||||
|
||||
@@ -213,26 +213,16 @@ export type Winder2NamespaceStore = {
|
||||
spoolProgress: TimeSeries;
|
||||
};
|
||||
|
||||
// Constants for time durations
|
||||
const TWENTY_MILLISECOND = 20;
|
||||
const ONE_SECOND = 1000;
|
||||
const FIVE_SECOND = 5 * ONE_SECOND;
|
||||
const ONE_HOUR = 60 * 60 * ONE_SECOND;
|
||||
|
||||
//Store Factory and Message Handler -> no param, so default values
|
||||
const { initialTimeSeries: spoolProgress, insert: addSpoolProgress } =
|
||||
createTimeSeries(TWENTY_MILLISECOND, ONE_SECOND, FIVE_SECOND, ONE_HOUR);
|
||||
createTimeSeries();
|
||||
const { initialTimeSeries: traversePosition, insert: addTraversePosition } =
|
||||
createTimeSeries(TWENTY_MILLISECOND, ONE_SECOND, FIVE_SECOND, ONE_HOUR);
|
||||
createTimeSeries();
|
||||
const { initialTimeSeries: pullerSpeed, insert: addPullerSpeed } =
|
||||
createTimeSeries(TWENTY_MILLISECOND, ONE_SECOND, FIVE_SECOND, ONE_HOUR);
|
||||
const { initialTimeSeries: spoolRpm, insert: addSpoolRpm } = createTimeSeries(
|
||||
TWENTY_MILLISECOND,
|
||||
ONE_SECOND,
|
||||
FIVE_SECOND,
|
||||
ONE_HOUR,
|
||||
);
|
||||
createTimeSeries();
|
||||
const { initialTimeSeries: spoolRpm, insert: addSpoolRpm } = createTimeSeries();
|
||||
const { initialTimeSeries: tensionArmAngle, insert: addTensionArmAngle } =
|
||||
createTimeSeries(TWENTY_MILLISECOND, ONE_SECOND, FIVE_SECOND, ONE_HOUR);
|
||||
createTimeSeries();
|
||||
|
||||
/**
|
||||
* Factory function to create a new Winder2 namespace store
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import { app, BrowserWindow } from "electron";
|
||||
import registerListeners from "./helpers/ipc/listeners-register";
|
||||
// "electron-squirrel-startup" seems broken when packaging with vite
|
||||
//import started from "electron-squirrel-startup";
|
||||
import path from "path";
|
||||
import {
|
||||
installExtension,
|
||||
REACT_DEVELOPER_TOOLS,
|
||||
} from "electron-devtools-installer";
|
||||
|
||||
const ARG1 = process.argv[1] ?? "./main.js";
|
||||
const DIR = path.dirname(ARG1);
|
||||
|
||||
// Set consistent app ID for Windows taskbar and GNOME dock integration
|
||||
app.setAppUserModelId("de.qitech.control-electron");
|
||||
|
||||
@@ -29,7 +30,9 @@ if (!gotTheLock) {
|
||||
}
|
||||
|
||||
function createWindow() {
|
||||
const preload = path.join(__dirname, "preload.js");
|
||||
const preload = path.join(DIR, "preload.js");
|
||||
console.log("Setting preload.js to", preload);
|
||||
|
||||
const mainWindow = new BrowserWindow({
|
||||
width: 1200,
|
||||
height: 800,
|
||||
@@ -43,7 +46,7 @@ function createWindow() {
|
||||
preload: preload,
|
||||
},
|
||||
// Add icon path for better integration
|
||||
icon: path.join(__dirname, "../assets/icon.png"),
|
||||
icon: path.join(DIR, "icon.png"),
|
||||
// Set window class explicitly for Linux/GNOME integration
|
||||
title: "QiTech Control",
|
||||
});
|
||||
@@ -53,12 +56,11 @@ function createWindow() {
|
||||
|
||||
registerListeners(mainWindow);
|
||||
|
||||
if (MAIN_WINDOW_VITE_DEV_SERVER_URL) {
|
||||
mainWindow.loadURL(MAIN_WINDOW_VITE_DEV_SERVER_URL);
|
||||
if (process.env.VITE_DEV_SERVER_URL) {
|
||||
mainWindow.loadURL(process.env.VITE_DEV_SERVER_URL);
|
||||
} else {
|
||||
mainWindow.loadFile(
|
||||
path.join(__dirname, `../renderer/${MAIN_WINDOW_VITE_NAME}/index.html`),
|
||||
);
|
||||
const index = path.join(DIR, "index.html");
|
||||
mainWindow.loadFile(index);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -108,10 +108,10 @@ export function MetricsControlPage() {
|
||||
|
||||
{/* Total page faults (composite, no single unit) */}
|
||||
<MetricCard
|
||||
title="Total Page Faults"
|
||||
title="Page Faults"
|
||||
value={
|
||||
minorFaults != null || majorFaults != null
|
||||
? `${minorFaults ?? "–"} minor | ${majorFaults ?? "–"} major`
|
||||
? `${minorFaults ?? "–"} minor per second | ${majorFaults ?? "–"} major`
|
||||
: "—"
|
||||
}
|
||||
/>
|
||||
|
||||
@@ -4,37 +4,24 @@ import type { RuntimeMetricsSample } from "./useRuntimeMetrics";
|
||||
|
||||
const ONE_SECOND = 1000;
|
||||
const FIVE_SECOND = 5 * ONE_SECOND;
|
||||
const ONE_HOUR = 60 * 60 * ONE_SECOND;
|
||||
|
||||
// 5 s window, 1 s bucket, 1 h history
|
||||
const { initialTimeSeries: jitterInitial, insert: insertJitter } =
|
||||
createTimeSeries(FIVE_SECOND, ONE_SECOND, FIVE_SECOND, ONE_HOUR);
|
||||
const { initialTimeSeries: cpuInitial, insert: insertCpu } = createTimeSeries(
|
||||
FIVE_SECOND,
|
||||
ONE_SECOND,
|
||||
FIVE_SECOND,
|
||||
ONE_HOUR,
|
||||
);
|
||||
const { initialTimeSeries: memInitial, insert: insertMem } = createTimeSeries(
|
||||
FIVE_SECOND,
|
||||
ONE_SECOND,
|
||||
FIVE_SECOND,
|
||||
ONE_HOUR,
|
||||
);
|
||||
const { initialTimeSeries: rxInitial, insert: insertRx } = createTimeSeries(
|
||||
FIVE_SECOND,
|
||||
ONE_SECOND,
|
||||
FIVE_SECOND,
|
||||
ONE_HOUR,
|
||||
);
|
||||
const { initialTimeSeries: txInitial, insert: insertTx } = createTimeSeries(
|
||||
FIVE_SECOND,
|
||||
ONE_SECOND,
|
||||
FIVE_SECOND,
|
||||
ONE_HOUR,
|
||||
);
|
||||
createTimeSeries({ sampleIntervalShort: FIVE_SECOND });
|
||||
const { initialTimeSeries: cpuInitial, insert: insertCpu } = createTimeSeries({
|
||||
sampleIntervalShort: FIVE_SECOND,
|
||||
});
|
||||
const { initialTimeSeries: memInitial, insert: insertMem } = createTimeSeries({
|
||||
sampleIntervalShort: FIVE_SECOND,
|
||||
});
|
||||
const { initialTimeSeries: rxInitial, insert: insertRx } = createTimeSeries({
|
||||
sampleIntervalShort: FIVE_SECOND,
|
||||
});
|
||||
const { initialTimeSeries: txInitial, insert: insertTx } = createTimeSeries({
|
||||
sampleIntervalShort: FIVE_SECOND,
|
||||
});
|
||||
const { initialTimeSeries: preemptInitial, insert: insertPreempt } =
|
||||
createTimeSeries(FIVE_SECOND, ONE_SECOND, FIVE_SECOND, ONE_HOUR);
|
||||
createTimeSeries({ sampleIntervalShort: FIVE_SECOND });
|
||||
|
||||
export type RuntimeSeriesState = {
|
||||
jitter: TimeSeries | null;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import exposeContexts from "./helpers/ipc/context-exposer";
|
||||
|
||||
console.log("preload.js loaded");
|
||||
exposeContexts();
|
||||
@@ -4,7 +4,6 @@ import React from "react";
|
||||
import { z } from "zod";
|
||||
|
||||
import { ChooseVersionPage } from "@/setup/ChooseVersionPage";
|
||||
import { fallback, zodValidator } from "@tanstack/zod-adapter";
|
||||
import { githubSourceSchema } from "@/setup/GithubSourceDialog";
|
||||
import { SidebarLayout } from "@/components/SidebarLayout";
|
||||
import { SetupPage } from "@/setup/SetupPage";
|
||||
@@ -381,9 +380,9 @@ export const metricsRoute = createRoute({
|
||||
|
||||
export const versionSearchSchema = z
|
||||
.object({
|
||||
branch: fallback(z.string().optional(), undefined),
|
||||
commit: fallback(z.string().optional(), undefined),
|
||||
tag: fallback(z.string().optional(), undefined),
|
||||
branch: z.string().optional(),
|
||||
commit: z.string().optional(),
|
||||
tag: z.string().optional(),
|
||||
})
|
||||
.merge(githubSourceSchema)
|
||||
.refine(
|
||||
@@ -405,14 +404,14 @@ export const updateChangelogRoute = createRoute({
|
||||
getParentRoute: () => updateRoute,
|
||||
path: "changelog",
|
||||
component: () => <ChangelogPage />,
|
||||
validateSearch: zodValidator(versionSearchSchema),
|
||||
validateSearch: versionSearchSchema,
|
||||
});
|
||||
|
||||
export const updateExecuteRoute = createRoute({
|
||||
getParentRoute: () => updateRoute,
|
||||
path: "execute",
|
||||
component: () => <UpdateExecutePage />,
|
||||
validateSearch: zodValidator(versionSearchSchema),
|
||||
validateSearch: versionSearchSchema,
|
||||
});
|
||||
|
||||
export const rootTree = RootRoute.addChildren([
|
||||
|
||||
@@ -37,7 +37,6 @@ import {
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { useFormValues } from "@/lib/useFormValues";
|
||||
import { validateU16 } from "@/lib/validation";
|
||||
import { DeviceRoleComponent } from "@/components/DeviceRole";
|
||||
import { Alert } from "@/components/Alert";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
@@ -54,9 +53,15 @@ type Props = {
|
||||
};
|
||||
|
||||
const formSchema = z.object({
|
||||
machine: z.string().superRefine(validateU16),
|
||||
serial: z.string().superRefine(validateU16),
|
||||
role: z.string().superRefine(validateU16),
|
||||
machine: z
|
||||
.string()
|
||||
.refine((v) => parseInt(v) < 0xffff, { error: "Value too big" }),
|
||||
serial: z
|
||||
.string()
|
||||
.refine((v) => parseInt(v) < 0xffff, { error: "Value too big" }),
|
||||
role: z
|
||||
.string()
|
||||
.refine((v) => parseInt(v) < 0xffff, { error: "Value too big" }),
|
||||
});
|
||||
|
||||
type FormSchema = z.infer<typeof formSchema>;
|
||||
|
||||
@@ -24,7 +24,6 @@ import { Input } from "@/components/ui/input";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { Icon } from "@/components/Icon";
|
||||
import { TouchButton } from "@/components/touch/TouchButton";
|
||||
import { fallback } from "@tanstack/zod-adapter";
|
||||
|
||||
type Props = {
|
||||
value: GithubSource;
|
||||
@@ -32,9 +31,9 @@ type Props = {
|
||||
};
|
||||
|
||||
export const githubSourceSchema = z.object({
|
||||
githubRepoOwner: fallback(z.string(), "qitechgmbh"),
|
||||
githubRepoName: fallback(z.string(), "control"),
|
||||
githubToken: fallback(z.string().optional().nullable(), undefined),
|
||||
githubRepoOwner: z.string().catch("qitechgmbh"),
|
||||
githubRepoName: z.string().catch("control"),
|
||||
githubToken: z.string().optional(),
|
||||
});
|
||||
|
||||
export type GithubSource = z.infer<typeof githubSourceSchema>;
|
||||
@@ -91,7 +90,7 @@ export function GithubSourceDialogContent({
|
||||
const onSubmit = (values: GithubSource) => {
|
||||
onChange({
|
||||
...values,
|
||||
githubToken: values.githubToken === "" ? null : values.githubToken,
|
||||
githubToken: values.githubToken === "" ? undefined : values.githubToken,
|
||||
});
|
||||
setOpen(false);
|
||||
};
|
||||
|
||||
@@ -16,8 +16,7 @@
|
||||
"strict": true,
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["./src/*"],
|
||||
"@root/*": ["../*"]
|
||||
"@/*": ["./src/*"]
|
||||
},
|
||||
"outDir": "dist",
|
||||
"moduleResolution": "node",
|
||||
|
||||
@@ -1,110 +0,0 @@
|
||||
import { builtinModules } from "node:module";
|
||||
import type { AddressInfo } from "node:net";
|
||||
import type { ConfigEnv, Plugin, UserConfig } from "vite";
|
||||
import pkg from "./package.json";
|
||||
|
||||
export const builtins = [
|
||||
"electron",
|
||||
...builtinModules.map((m) => [m, `node:${m}`]).flat(),
|
||||
];
|
||||
|
||||
export const external = [
|
||||
...builtins,
|
||||
...Object.keys(
|
||||
"dependencies" in pkg ? (pkg.dependencies as Record<string, unknown>) : {},
|
||||
),
|
||||
];
|
||||
|
||||
export function getBuildConfig(env: ConfigEnv<"build">): UserConfig {
|
||||
const { root, mode, command } = env;
|
||||
|
||||
return {
|
||||
root,
|
||||
mode,
|
||||
build: {
|
||||
// Prevent multiple builds from interfering with each other.
|
||||
emptyOutDir: false,
|
||||
// 🚧 Multiple builds may conflict.
|
||||
outDir: ".vite/build",
|
||||
watch: command === "serve" ? {} : null,
|
||||
minify: command === "build",
|
||||
},
|
||||
clearScreen: false,
|
||||
};
|
||||
}
|
||||
|
||||
export function getDefineKeys(names: string[]) {
|
||||
const define: { [name: string]: VitePluginRuntimeKeys } = {};
|
||||
|
||||
return names.reduce((acc, name) => {
|
||||
const NAME = name.toUpperCase();
|
||||
const keys: VitePluginRuntimeKeys = {
|
||||
VITE_DEV_SERVER_URL: `${NAME}_VITE_DEV_SERVER_URL`,
|
||||
VITE_NAME: `${NAME}_VITE_NAME`,
|
||||
};
|
||||
|
||||
return { ...acc, [name]: keys };
|
||||
}, define);
|
||||
}
|
||||
|
||||
export function getBuildDefine(env: ConfigEnv<"build">) {
|
||||
const { command, forgeConfig } = env;
|
||||
const names = forgeConfig.renderer
|
||||
.filter(({ name }) => name != null)
|
||||
.map(({ name }) => name!);
|
||||
const defineKeys = getDefineKeys(names);
|
||||
const define = Object.entries(defineKeys).reduce(
|
||||
(acc, [name, keys]) => {
|
||||
const { VITE_DEV_SERVER_URL, VITE_NAME } = keys;
|
||||
const def = {
|
||||
[VITE_DEV_SERVER_URL]:
|
||||
command === "serve"
|
||||
? JSON.stringify(process.env[VITE_DEV_SERVER_URL])
|
||||
: undefined,
|
||||
[VITE_NAME]: JSON.stringify(name),
|
||||
};
|
||||
return { ...acc, ...def };
|
||||
},
|
||||
{} as Record<string, any>,
|
||||
);
|
||||
|
||||
return define;
|
||||
}
|
||||
|
||||
export function pluginExposeRenderer(name: string): Plugin {
|
||||
const { VITE_DEV_SERVER_URL } = getDefineKeys([name])[name];
|
||||
|
||||
return {
|
||||
name: "@electron-forge/plugin-vite:expose-renderer",
|
||||
configureServer(server) {
|
||||
process.viteDevServers ??= {};
|
||||
// Expose server for preload scripts hot reload.
|
||||
process.viteDevServers[name] = server;
|
||||
|
||||
server.httpServer?.once("listening", () => {
|
||||
const addressInfo = server.httpServer!.address() as AddressInfo;
|
||||
// Expose env constant for main process use.
|
||||
process.env[VITE_DEV_SERVER_URL] =
|
||||
`http://localhost:${addressInfo?.port}`;
|
||||
});
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function pluginHotRestart(command: "reload" | "restart"): Plugin {
|
||||
return {
|
||||
name: "@electron-forge/plugin-vite:hot-restart",
|
||||
closeBundle() {
|
||||
if (command === "reload") {
|
||||
for (const server of Object.values(process.viteDevServers)) {
|
||||
// Preload scripts hot reload.
|
||||
server.ws.send({ type: "full-reload" });
|
||||
}
|
||||
} else {
|
||||
// Main process hot restart.
|
||||
// https://github.com/electron/forge/blob/v7.2.0/packages/api/core/src/api/start.ts#L216-L223
|
||||
process.stdin.emit("data", "rs");
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import react from "@vitejs/plugin-react";
|
||||
import electron from "vite-plugin-electron/simple";
|
||||
import path from "path";
|
||||
|
||||
// https://vitejs.dev/config
|
||||
export default {
|
||||
publicDir: path.resolve(__dirname, "public"),
|
||||
resolve: {
|
||||
preserveSymlinks: true,
|
||||
alias: {
|
||||
"@": path.resolve(__dirname, "./src"),
|
||||
"@root": path.resolve(__dirname, ".."),
|
||||
},
|
||||
},
|
||||
clearScreen: false,
|
||||
plugins: [
|
||||
electron({
|
||||
main: {
|
||||
entry: "src/main.ts",
|
||||
onstart: ({ startup }) => {
|
||||
startup(["./dist-electron/main.js"]);
|
||||
},
|
||||
},
|
||||
preload: {
|
||||
// Shortcut of `build.rollupOptions.input`
|
||||
input: "src/preload.ts",
|
||||
},
|
||||
// Optional: Use Node.js API in the Renderer process
|
||||
renderer: {},
|
||||
}),
|
||||
react({
|
||||
babel: {
|
||||
plugins: [["babel-plugin-react-compiler"]],
|
||||
},
|
||||
}),
|
||||
],
|
||||
};
|
||||
@@ -1,35 +0,0 @@
|
||||
import type { ConfigEnv, UserConfig } from "vite";
|
||||
import { defineConfig, mergeConfig } from "vite";
|
||||
import {
|
||||
getBuildConfig,
|
||||
getBuildDefine,
|
||||
external,
|
||||
pluginHotRestart,
|
||||
} from "./vite.base.config";
|
||||
|
||||
// https://vitejs.dev/config
|
||||
export default defineConfig((env) => {
|
||||
const forgeEnv = env as ConfigEnv<"build">;
|
||||
const { forgeConfigSelf } = forgeEnv;
|
||||
const define = getBuildDefine(forgeEnv);
|
||||
const config: UserConfig = {
|
||||
build: {
|
||||
lib: {
|
||||
entry: forgeConfigSelf.entry!,
|
||||
fileName: () => "[name].js",
|
||||
formats: ["cjs"],
|
||||
},
|
||||
rollupOptions: {
|
||||
external,
|
||||
},
|
||||
},
|
||||
plugins: [pluginHotRestart("restart")],
|
||||
define,
|
||||
resolve: {
|
||||
// Load the Node.js entry.
|
||||
mainFields: ["module", "jsnext:main", "jsnext"],
|
||||
},
|
||||
};
|
||||
|
||||
return mergeConfig(getBuildConfig(forgeEnv), config);
|
||||
});
|
||||
@@ -1,29 +0,0 @@
|
||||
import type { ConfigEnv, UserConfig } from "vite";
|
||||
import { defineConfig, mergeConfig } from "vite";
|
||||
import { getBuildConfig, external, pluginHotRestart } from "./vite.base.config";
|
||||
|
||||
// https://vitejs.dev/config
|
||||
export default defineConfig((env) => {
|
||||
const forgeEnv = env as ConfigEnv<"build">;
|
||||
const { forgeConfigSelf } = forgeEnv;
|
||||
const config: UserConfig = {
|
||||
build: {
|
||||
rollupOptions: {
|
||||
external,
|
||||
// Preload scripts may contain Web assets, so use the `build.rollupOptions.input` instead `build.lib.entry`.
|
||||
input: forgeConfigSelf.entry!,
|
||||
output: {
|
||||
format: "cjs",
|
||||
// It should not be split chunks.
|
||||
inlineDynamicImports: true,
|
||||
entryFileNames: "[name].js",
|
||||
chunkFileNames: "[name].js",
|
||||
assetFileNames: "[name].[ext]",
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [pluginHotRestart("reload")],
|
||||
};
|
||||
|
||||
return mergeConfig(getBuildConfig(forgeEnv), config);
|
||||
});
|
||||
@@ -1,38 +0,0 @@
|
||||
import path from "path";
|
||||
import react from "@vitejs/plugin-react";
|
||||
import type { ConfigEnv, UserConfig } from "vite";
|
||||
import { defineConfig } from "vite";
|
||||
import { pluginExposeRenderer } from "./vite.base.config";
|
||||
|
||||
// https://vitejs.dev/config
|
||||
export default defineConfig((env) => {
|
||||
const forgeEnv = env as ConfigEnv<"renderer">;
|
||||
const { root, mode, forgeConfigSelf } = forgeEnv;
|
||||
const name = forgeConfigSelf.name ?? "";
|
||||
|
||||
return {
|
||||
root,
|
||||
mode,
|
||||
base: "./",
|
||||
publicDir: path.resolve(__dirname, "public"),
|
||||
build: {
|
||||
outDir: `.vite/renderer/${name}`,
|
||||
},
|
||||
plugins: [
|
||||
pluginExposeRenderer(name),
|
||||
react({
|
||||
babel: {
|
||||
plugins: [["babel-plugin-react-compiler"]],
|
||||
},
|
||||
}),
|
||||
],
|
||||
resolve: {
|
||||
preserveSymlinks: true,
|
||||
alias: {
|
||||
"@": path.resolve(__dirname, "./src"),
|
||||
"@root": path.resolve(__dirname, ".."),
|
||||
},
|
||||
},
|
||||
clearScreen: false,
|
||||
} as UserConfig;
|
||||
});
|
||||
@@ -14,7 +14,7 @@ units = { path = "../units" }
|
||||
bitvec = { version = "1.0.1", features = ["alloc"] }
|
||||
smol = "2.0.2"
|
||||
rand = "0.9.2"
|
||||
tracing = "0.1.41"
|
||||
tracing = "0.1.44"
|
||||
|
||||
[dev-dependencies]
|
||||
approx = "0.5.1"
|
||||
@@ -328,6 +328,8 @@ impl TxPdoObject for Standard22ByteMdp600Input {
|
||||
self.status.overrun_error = bits[6];
|
||||
// Bit7 is reserved/unused
|
||||
self.length = bits[8..8 + 8].load_le::<u8>();
|
||||
// the serial bytes start at bit 16, then we calculate the offset of when the serial bytes buffer is over, which for Standard22ByteMdp600Input is 22bytes
|
||||
// to get the size in bits we do 22 * 8 -> 176bits
|
||||
let serial_bytes = bits[16..(16 + 22 * 8_usize)].chunks_exact(8);
|
||||
for (i, val) in serial_bytes.enumerate() {
|
||||
self.data[i] = val.load_le();
|
||||
|
||||
@@ -8,6 +8,7 @@ use crate::{
|
||||
DynamicEthercatDevice, Module,
|
||||
wago_modules::{
|
||||
wago_750_501::{WAGO_750_501_MODULE_IDENT, WAGO_750_501_PRODUCT_ID},
|
||||
wago_750_530::{WAGO_750_530_MODULE_IDENT, WAGO_750_530_PRODUCT_ID},
|
||||
wago_750_652::{WAGO_750_652_MODULE_IDENT, WAGO_750_652_PRODUCT_ID},
|
||||
wago_750_1506::{WAGO_750_1506_MODULE_IDENT, WAGO_750_1506_PRODUCT_ID},
|
||||
},
|
||||
@@ -231,6 +232,10 @@ impl Wago750_354 {
|
||||
module.has_tx = false;
|
||||
module.has_rx = true;
|
||||
}
|
||||
WAGO_750_530_PRODUCT_ID => {
|
||||
module.has_tx = false;
|
||||
module.has_rx = true;
|
||||
}
|
||||
WAGO_750_652_PRODUCT_ID => {
|
||||
module.has_tx = true;
|
||||
module.has_rx = true;
|
||||
@@ -259,6 +264,8 @@ impl Wago750_354 {
|
||||
let _ = self.get_pdo_offsets(device, false).await;
|
||||
});
|
||||
|
||||
println!("{:?}\n\n{:?}", self.tx_offsets, self.rx_offsets);
|
||||
|
||||
for module in self.slots {
|
||||
match module {
|
||||
Some(m) => {
|
||||
@@ -270,6 +277,9 @@ impl Wago750_354 {
|
||||
WAGO_750_501_MODULE_IDENT => {
|
||||
Arc::new(RwLock::new(wago_750_501::Wago750_501::new()))
|
||||
}
|
||||
WAGO_750_530_MODULE_IDENT => {
|
||||
Arc::new(RwLock::new(wago_750_530::Wago750_530::new()))
|
||||
}
|
||||
WAGO_750_1506_MODULE_IDENT => {
|
||||
Arc::new(RwLock::new(wago_750_1506::Wago750_1506::new()))
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
pub mod ip20_ec_di8_do8;
|
||||
pub mod wago_750_1506;
|
||||
pub mod wago_750_501;
|
||||
pub mod wago_750_530;
|
||||
pub mod wago_750_652;
|
||||
@@ -0,0 +1,260 @@
|
||||
use crate::devices::{
|
||||
DynamicEthercatDevice, EthercatDevice, EthercatDeviceUsed, EthercatDynamicPDO, Module,
|
||||
SubDeviceProductTuple,
|
||||
};
|
||||
use crate::devices::{EthercatDeviceProcessing, NewEthercatDevice};
|
||||
use crate::io::digital_output::{DigitalOutputDevice, DigitalOutputOutput};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Wago750_530 {
|
||||
is_used: bool,
|
||||
tx_bit_offset: usize,
|
||||
rx_bit_offset: usize,
|
||||
pub rxpdo: Wago750_530RxPdo,
|
||||
module: Option<Module>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum Wago750_530Port {
|
||||
Port1,
|
||||
Port2,
|
||||
Port3,
|
||||
Port4,
|
||||
Port5,
|
||||
Port6,
|
||||
Port7,
|
||||
Port8,
|
||||
}
|
||||
|
||||
impl From<Wago750_530Port> for usize {
|
||||
fn from(value: Wago750_530Port) -> Self {
|
||||
match value {
|
||||
Wago750_530Port::Port1 => 0,
|
||||
Wago750_530Port::Port2 => 1,
|
||||
Wago750_530Port::Port3 => 2,
|
||||
Wago750_530Port::Port4 => 3,
|
||||
Wago750_530Port::Port5 => 4,
|
||||
Wago750_530Port::Port6 => 5,
|
||||
Wago750_530Port::Port7 => 6,
|
||||
Wago750_530Port::Port8 => 7,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Default)]
|
||||
pub struct Wago750_530RxPdo {
|
||||
pub port1: bool,
|
||||
pub port2: bool,
|
||||
pub port3: bool,
|
||||
pub port4: bool,
|
||||
pub port5: bool,
|
||||
pub port6: bool,
|
||||
pub port7: bool,
|
||||
pub port8: bool,
|
||||
}
|
||||
|
||||
impl DigitalOutputDevice<Wago750_530Port> for Wago750_530 {
|
||||
/// Writes the new output value into the device's RXPDO structure (in-memory PDI).
|
||||
fn set_output(&mut self, port: Wago750_530Port, value: DigitalOutputOutput) {
|
||||
// The DigitalOutputOutput is converted to a bool using the From trait
|
||||
let output_value: bool = value.into();
|
||||
match port {
|
||||
Wago750_530Port::Port1 => {
|
||||
// Modify the internal PDO representation
|
||||
self.rxpdo.port1 = output_value;
|
||||
}
|
||||
Wago750_530Port::Port2 => {
|
||||
// Modify the internal PDO representation
|
||||
self.rxpdo.port2 = output_value;
|
||||
}
|
||||
Wago750_530Port::Port3 => {
|
||||
// Modify the internal PDO representation
|
||||
self.rxpdo.port3 = output_value;
|
||||
}
|
||||
Wago750_530Port::Port4 => {
|
||||
// Modify the internal PDO representation
|
||||
self.rxpdo.port4 = output_value;
|
||||
}
|
||||
Wago750_530Port::Port5 => {
|
||||
// Modify the internal PDO representation
|
||||
self.rxpdo.port5 = output_value;
|
||||
}
|
||||
Wago750_530Port::Port6 => {
|
||||
// Modify the internal PDO representation
|
||||
self.rxpdo.port6 = output_value;
|
||||
}
|
||||
Wago750_530Port::Port7 => {
|
||||
// Modify the internal PDO representation
|
||||
self.rxpdo.port7 = output_value;
|
||||
}
|
||||
Wago750_530Port::Port8 => {
|
||||
// Modify the internal PDO representation
|
||||
self.rxpdo.port8 = output_value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Reads the current output value from the device's RXPDO structure (in-memory PDI).
|
||||
fn get_output(&self, port: Wago750_530Port) -> DigitalOutputOutput {
|
||||
let current_value = match port {
|
||||
Wago750_530Port::Port1 => self.rxpdo.port1,
|
||||
Wago750_530Port::Port2 => self.rxpdo.port2,
|
||||
Wago750_530Port::Port3 => self.rxpdo.port3,
|
||||
Wago750_530Port::Port4 => self.rxpdo.port4,
|
||||
Wago750_530Port::Port5 => self.rxpdo.port5,
|
||||
Wago750_530Port::Port6 => self.rxpdo.port6,
|
||||
Wago750_530Port::Port7 => self.rxpdo.port7,
|
||||
Wago750_530Port::Port8 => self.rxpdo.port8,
|
||||
};
|
||||
|
||||
// Wrap the bool back into the type-safe wrapper
|
||||
DigitalOutputOutput(current_value)
|
||||
}
|
||||
}
|
||||
|
||||
impl EthercatDeviceUsed for Wago750_530 {
|
||||
fn is_used(&self) -> bool {
|
||||
self.is_used
|
||||
}
|
||||
|
||||
fn set_used(&mut self, used: bool) {
|
||||
self.is_used = used;
|
||||
}
|
||||
}
|
||||
|
||||
impl DynamicEthercatDevice for Wago750_530 {}
|
||||
|
||||
impl EthercatDynamicPDO for Wago750_530 {
|
||||
fn get_tx_offset(&self) -> usize {
|
||||
self.tx_bit_offset
|
||||
}
|
||||
|
||||
fn get_rx_offset(&self) -> usize {
|
||||
self.rx_bit_offset
|
||||
}
|
||||
|
||||
fn set_tx_offset(&mut self, offset: usize) {
|
||||
self.tx_bit_offset = offset
|
||||
}
|
||||
|
||||
fn set_rx_offset(&mut self, offset: usize) {
|
||||
self.rx_bit_offset = offset
|
||||
}
|
||||
}
|
||||
|
||||
impl EthercatDevice for Wago750_530 {
|
||||
fn input(
|
||||
&mut self,
|
||||
_input: &bitvec::prelude::BitSlice<u8, bitvec::prelude::Lsb0>,
|
||||
) -> Result<(), anyhow::Error> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn input_len(&self) -> usize {
|
||||
0
|
||||
}
|
||||
|
||||
fn output(
|
||||
&self,
|
||||
output: &mut bitvec::prelude::BitSlice<u8, bitvec::prelude::Lsb0>,
|
||||
) -> Result<(), anyhow::Error> {
|
||||
output.set(
|
||||
self.rx_bit_offset + Into::<usize>::into(Wago750_530Port::Port1),
|
||||
self.rxpdo.port1,
|
||||
);
|
||||
output.set(
|
||||
self.rx_bit_offset + Into::<usize>::into(Wago750_530Port::Port2),
|
||||
self.rxpdo.port2,
|
||||
);
|
||||
output.set(
|
||||
self.rx_bit_offset + Into::<usize>::into(Wago750_530Port::Port3),
|
||||
self.rxpdo.port3,
|
||||
);
|
||||
output.set(
|
||||
self.rx_bit_offset + Into::<usize>::into(Wago750_530Port::Port4),
|
||||
self.rxpdo.port4,
|
||||
);
|
||||
output.set(
|
||||
self.rx_bit_offset + Into::<usize>::into(Wago750_530Port::Port5),
|
||||
self.rxpdo.port5,
|
||||
);
|
||||
output.set(
|
||||
self.rx_bit_offset + Into::<usize>::into(Wago750_530Port::Port6),
|
||||
self.rxpdo.port6,
|
||||
);
|
||||
output.set(
|
||||
self.rx_bit_offset + Into::<usize>::into(Wago750_530Port::Port7),
|
||||
self.rxpdo.port7,
|
||||
);
|
||||
output.set(
|
||||
self.rx_bit_offset + Into::<usize>::into(Wago750_530Port::Port8),
|
||||
self.rxpdo.port8,
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn output_len(&self) -> usize {
|
||||
8
|
||||
}
|
||||
|
||||
fn as_any(&self) -> &dyn std::any::Any {
|
||||
self
|
||||
}
|
||||
|
||||
fn as_any_mut(&mut self) -> &mut dyn std::any::Any {
|
||||
self
|
||||
}
|
||||
|
||||
fn is_module(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn input_checked(
|
||||
&mut self,
|
||||
_input: &bitvec::prelude::BitSlice<u8, bitvec::prelude::Lsb0>,
|
||||
) -> Result<(), anyhow::Error> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn output_checked(
|
||||
&self,
|
||||
_output: &mut bitvec::prelude::BitSlice<u8, bitvec::prelude::Lsb0>,
|
||||
) -> Result<(), anyhow::Error> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn get_module(&self) -> Option<crate::devices::Module> {
|
||||
self.module.clone()
|
||||
}
|
||||
|
||||
fn set_module(&mut self, module: crate::devices::Module) {
|
||||
self.tx_bit_offset = module.tx_offset;
|
||||
self.rx_bit_offset = module.rx_offset;
|
||||
self.module = Some(module)
|
||||
}
|
||||
}
|
||||
|
||||
impl EthercatDeviceProcessing for Wago750_530 {}
|
||||
|
||||
impl NewEthercatDevice for Wago750_530 {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
is_used: false,
|
||||
tx_bit_offset: 0,
|
||||
rx_bit_offset: 0,
|
||||
module: None,
|
||||
rxpdo: Wago750_530RxPdo::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for Wago750_530 {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "Wago750_530")
|
||||
}
|
||||
}
|
||||
|
||||
pub const WAGO_750_530_VENDOR_ID: u32 = 0x00000021;
|
||||
pub const WAGO_750_530_PRODUCT_ID: u32 = 2147483778;
|
||||
pub const WAGO_750_530_MODULE_IDENT: SubDeviceProductTuple =
|
||||
(WAGO_750_530_VENDOR_ID, WAGO_750_530_PRODUCT_ID);
|
||||
@@ -1,8 +1,12 @@
|
||||
use bitvec::field::BitField;
|
||||
|
||||
use crate::devices::{
|
||||
DynamicEthercatDevice, EthercatDevice, EthercatDeviceUsed, EthercatDynamicPDO, Module,
|
||||
SubDeviceProductTuple,
|
||||
};
|
||||
use crate::devices::{EthercatDeviceProcessing, NewEthercatDevice};
|
||||
use crate::io::serial_interface::SerialInterfaceDevice;
|
||||
const WAGO750_652_MAX_BUF_LENGTH: usize = 48;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Wago750_652 {
|
||||
@@ -10,6 +14,62 @@ pub struct Wago750_652 {
|
||||
tx_bit_offset: usize,
|
||||
rx_bit_offset: usize,
|
||||
module: Option<Module>,
|
||||
started_init: bool,
|
||||
pub rx_pdo: Wago750_652RxPdo,
|
||||
pub tx_pdo: Wago750_652TxPdo,
|
||||
has_messages_last_toggle: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Default, Debug)]
|
||||
pub struct Wago750_652Control {
|
||||
pub transmit_request: bool, // Transmit request
|
||||
pub receive_ack: bool, // Receive acknowledge
|
||||
pub init_request: bool, // Initialization request
|
||||
pub send_continuous: bool, // Send continuous
|
||||
pub output_length: usize,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Wago750_652RxPdo {
|
||||
pub control: Wago750_652Control,
|
||||
pub out_buffer: [u8; WAGO750_652_MAX_BUF_LENGTH],
|
||||
}
|
||||
|
||||
impl Default for Wago750_652RxPdo {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
control: Default::default(),
|
||||
out_buffer: [0u8; WAGO750_652_MAX_BUF_LENGTH],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Default, Debug)]
|
||||
pub struct Wago750_652Status {
|
||||
pub transmit_ack: bool, //
|
||||
pub receive_request: bool,
|
||||
pub init_ack: bool,
|
||||
pub buf_full: bool,
|
||||
pub buf_empty: bool, // Buffer is empty (Sending inactive)
|
||||
pub error_parity: bool,
|
||||
pub error_framing: bool, // Frame is corrupt?
|
||||
pub error_overrun: bool, // Byte/s was lost on receive
|
||||
pub input_length: usize,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Wago750_652TxPdo {
|
||||
pub status: Wago750_652Status,
|
||||
pub in_buffer: [u8; WAGO750_652_MAX_BUF_LENGTH],
|
||||
}
|
||||
|
||||
impl Default for Wago750_652TxPdo {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
status: Default::default(),
|
||||
in_buffer: [0u8; WAGO750_652_MAX_BUF_LENGTH],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl EthercatDeviceUsed for Wago750_652 {
|
||||
@@ -23,7 +83,6 @@ impl EthercatDeviceUsed for Wago750_652 {
|
||||
}
|
||||
|
||||
impl DynamicEthercatDevice for Wago750_652 {}
|
||||
|
||||
impl EthercatDynamicPDO for Wago750_652 {
|
||||
fn get_tx_offset(&self) -> usize {
|
||||
self.tx_bit_offset
|
||||
@@ -48,24 +107,84 @@ impl EthercatDevice for Wago750_652 {
|
||||
*/
|
||||
fn input(
|
||||
&mut self,
|
||||
_input: &bitvec::prelude::BitSlice<u8, bitvec::prelude::Lsb0>,
|
||||
input: &bitvec::prelude::BitSlice<u8, bitvec::prelude::Lsb0>,
|
||||
) -> Result<(), anyhow::Error> {
|
||||
self.tx_pdo.in_buffer.fill(0u8);
|
||||
let base = self.tx_bit_offset;
|
||||
|
||||
self.tx_pdo.status.transmit_ack = *input.get(base + 0).expect("Missing buf_empty bit");
|
||||
self.tx_pdo.status.receive_request = *input.get(base + 1).expect("Missing buf_empty bit");
|
||||
self.tx_pdo.status.init_ack = *input.get(base + 2).expect("Missing buf_empty bit");
|
||||
self.tx_pdo.status.buf_full = *input.get(base + 6).expect("Missing buf_empty bit");
|
||||
self.tx_pdo.status.buf_empty = *input.get(base + 11).expect("Missing buf_empty bit");
|
||||
self.tx_pdo.status.error_parity = *input.get(base + 13).expect("Missing buf_empty bit");
|
||||
self.tx_pdo.status.error_framing = *input.get(base + 14).expect("Missing buf_empty bit");
|
||||
self.tx_pdo.status.error_overrun = *input.get(base + 15).expect("Missing buf_empty bit");
|
||||
// Im sorry but this is what you get by splitting the len across different bytes ...
|
||||
self.tx_pdo.status.input_length = ((*input.get(base + 3).expect("Missing buf_empty bit")
|
||||
as u8)
|
||||
^ ((*input.get(base + 4).expect("Missing buf_empty bit") as u8) << 1)
|
||||
^ ((*input.get(base + 5).expect("Missing buf_empty bit") as u8) << 2)
|
||||
^ ((*input.get(base + 8).expect("Missing buf_empty bit") as u8) << 3)
|
||||
^ ((*input.get(base + 9).expect("Missing buf_empty bit") as u8) << 4)
|
||||
^ ((*input.get(base + 10).expect("Missing buf_empty bit") as u8) << 5))
|
||||
as usize;
|
||||
|
||||
// the serial bytes start at bit 16,
|
||||
// then we calculate the offset of when the serial bytes buffer is over, which for the 24byte pdo is 22bytes
|
||||
// to get the size in bits we do 22 * 8 -> 176bits
|
||||
let serial_bytes = input[(base + 16)..((base + 16) + 22 * 8_usize)].chunks_exact(8);
|
||||
for (i, val) in serial_bytes.enumerate() {
|
||||
self.tx_pdo.in_buffer[i] = val.load_le();
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn input_len(&self) -> usize {
|
||||
0
|
||||
24
|
||||
}
|
||||
|
||||
fn output(
|
||||
&self,
|
||||
_output: &mut bitvec::prelude::BitSlice<u8, bitvec::prelude::Lsb0>,
|
||||
output: &mut bitvec::prelude::BitSlice<u8, bitvec::prelude::Lsb0>,
|
||||
) -> Result<(), anyhow::Error> {
|
||||
let base = 32;
|
||||
|
||||
let mut ol0_i = 5; // Ol0 starts at bit 5 and goes to bit 7 (inclusive)
|
||||
let mut ol1_i = 8;
|
||||
|
||||
output.set(base + 0, self.rx_pdo.control.transmit_request);
|
||||
output.set(base + 1, self.rx_pdo.control.receive_ack);
|
||||
output.set(base + 2, self.rx_pdo.control.init_request);
|
||||
output.set(base + 3, self.rx_pdo.control.send_continuous);
|
||||
|
||||
for i in 0..3 {
|
||||
// Rust type system proceeds to annoy once again, we need to add a != at the end, because a cast to bool is just too dangeerous obviously
|
||||
output.set(
|
||||
base + ol0_i,
|
||||
((self.rx_pdo.control.output_length >> i) & 1) != 0,
|
||||
);
|
||||
ol0_i += 1;
|
||||
}
|
||||
|
||||
for i in 3..6 {
|
||||
output.set(
|
||||
base + ol1_i,
|
||||
((self.rx_pdo.control.output_length >> i) & 1) != 0,
|
||||
);
|
||||
ol1_i += 1;
|
||||
}
|
||||
println!("{}", self.rx_pdo.control.output_length);
|
||||
let data_start = base + 16;
|
||||
for (i, &byte) in self.rx_pdo.out_buffer.iter().take(22).enumerate() {
|
||||
output[(data_start + i * 8)..(data_start + (i + 1) * 8)].store_le(byte);
|
||||
}
|
||||
//println!("{:?}",self.rx_pdo.out_buffer);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn output_len(&self) -> usize {
|
||||
0
|
||||
24
|
||||
}
|
||||
|
||||
fn as_any(&self) -> &dyn std::any::Any {
|
||||
@@ -84,19 +203,6 @@ impl EthercatDevice for Wago750_652 {
|
||||
&mut self,
|
||||
input: &bitvec::prelude::BitSlice<u8, bitvec::prelude::Lsb0>,
|
||||
) -> Result<(), anyhow::Error> {
|
||||
// validate input has correct length
|
||||
let expected = self.input_len();
|
||||
let actual = input.len();
|
||||
if actual != expected {
|
||||
return Err(anyhow::anyhow!(
|
||||
"[{}::Device::input_checked] Input length is {} ({} bytes) and must be {} bits ({} bytes)",
|
||||
module_path!(),
|
||||
actual,
|
||||
actual / 8,
|
||||
expected,
|
||||
expected / 8
|
||||
));
|
||||
}
|
||||
self.input(input)
|
||||
}
|
||||
|
||||
@@ -105,21 +211,6 @@ impl EthercatDevice for Wago750_652 {
|
||||
output: &mut bitvec::prelude::BitSlice<u8, bitvec::prelude::Lsb0>,
|
||||
) -> Result<(), anyhow::Error> {
|
||||
self.output(output)?;
|
||||
|
||||
// validate input has correct length
|
||||
let expected = self.output_len();
|
||||
let actual = output.len();
|
||||
if output.len() != expected {
|
||||
return Err(anyhow::anyhow!(
|
||||
"[{}::Device::output_checked] Output length is {} ({} bytes) and must be {} bits ({} bytes)",
|
||||
module_path!(),
|
||||
actual,
|
||||
actual / 8,
|
||||
expected,
|
||||
expected / 8
|
||||
));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -143,6 +234,10 @@ impl NewEthercatDevice for Wago750_652 {
|
||||
tx_bit_offset: 0,
|
||||
rx_bit_offset: 0,
|
||||
module: None,
|
||||
rx_pdo: Wago750_652RxPdo::default(),
|
||||
tx_pdo: Wago750_652TxPdo::default(),
|
||||
started_init: false,
|
||||
has_messages_last_toggle: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -153,6 +248,108 @@ impl std::fmt::Debug for Wago750_652 {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub enum Wago750_652Port {
|
||||
SI1, // Serial
|
||||
}
|
||||
|
||||
impl SerialInterfaceDevice<Wago750_652Port> for Wago750_652 {
|
||||
fn serial_interface_read_message(&mut self, port: Wago750_652Port) -> Option<Vec<u8>> {
|
||||
if !self.serial_interface_has_messages(port) {
|
||||
return None;
|
||||
} else {
|
||||
let in_len = self.tx_pdo.status.input_length;
|
||||
let received_data = self.tx_pdo.in_buffer[0..in_len].to_vec();
|
||||
|
||||
if received_data.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
self.has_messages_last_toggle = self.tx_pdo.status.receive_request;
|
||||
self.rx_pdo.control.receive_ack = !self.rx_pdo.control.receive_ack;
|
||||
|
||||
Some(received_data)
|
||||
}
|
||||
}
|
||||
|
||||
fn serial_interface_write_message(
|
||||
&mut self,
|
||||
_port: Wago750_652Port,
|
||||
message: Vec<u8>,
|
||||
) -> Result<bool, anyhow::Error> {
|
||||
if message.len() > WAGO750_652_MAX_BUF_LENGTH {
|
||||
return Err(anyhow::anyhow!(
|
||||
"Message is too long for RxPdo Buffer of 22 bytes!"
|
||||
));
|
||||
}
|
||||
|
||||
if message.is_empty() {
|
||||
return Ok(self.rx_pdo.control.transmit_request == self.tx_pdo.status.transmit_ack);
|
||||
}
|
||||
|
||||
let bytes = message.as_slice();
|
||||
self.rx_pdo.control.output_length = message.len();
|
||||
self.rx_pdo.out_buffer.fill(0u8);
|
||||
self.rx_pdo.out_buffer[0..message.len()].copy_from_slice(&bytes[0..message.len()]);
|
||||
self.rx_pdo.control.transmit_request = !self.rx_pdo.control.transmit_request;
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
fn serial_interface_has_messages(&mut self, _port: Wago750_652Port) -> bool {
|
||||
return self.tx_pdo.status.receive_request != self.has_messages_last_toggle;
|
||||
}
|
||||
|
||||
fn get_serial_encoding(
|
||||
&self,
|
||||
_port: Wago750_652Port,
|
||||
) -> Option<crate::io::serial_interface::SerialEncoding> {
|
||||
None
|
||||
}
|
||||
|
||||
fn get_baudrate(&self, _port: Wago750_652Port) -> Option<u32> {
|
||||
// Right now we cant change it on the fly because WAGO ONLY supports setting via
|
||||
// Codesys OR WAGO I/O Check the way it is set with Codesys is also NOT documented ...
|
||||
// My Hunch is that the 0x2010 0x2011 register can be used to configure that with COE its not documented though
|
||||
None
|
||||
}
|
||||
|
||||
/*
|
||||
Needs to be called multiple times until it returns true, at which point it is ready to be used
|
||||
*/
|
||||
fn serial_interface_initialize(&mut self, port: Wago750_652Port) -> bool {
|
||||
match port {
|
||||
Wago750_652Port::SI1 => {
|
||||
if !self.rx_pdo.control.init_request
|
||||
&& !self.tx_pdo.status.init_ack
|
||||
&& !self.started_init
|
||||
{
|
||||
self.rx_pdo.control.init_request = true;
|
||||
self.started_init = true;
|
||||
return false;
|
||||
}
|
||||
|
||||
if self.rx_pdo.control.init_request && self.tx_pdo.status.init_ack {
|
||||
self.rx_pdo.control.init_request = false;
|
||||
return false;
|
||||
}
|
||||
|
||||
if !self.rx_pdo.control.init_request && self.tx_pdo.status.init_ack {
|
||||
return false;
|
||||
}
|
||||
|
||||
if !self.rx_pdo.control.init_request
|
||||
&& !self.tx_pdo.status.init_ack
|
||||
&& self.started_init
|
||||
{
|
||||
self.has_messages_last_toggle = self.tx_pdo.status.receive_request;
|
||||
return true;
|
||||
}
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub const WAGO_750_652_VENDOR_ID: u32 = 0x00000021;
|
||||
pub const WAGO_750_652_PRODUCT_ID: u32 = 106043250;
|
||||
pub const WAGO_750_652_MODULE_IDENT: SubDeviceProductTuple =
|
||||
|
||||
@@ -5,9 +5,7 @@
|
||||
nixpkgs.url = "github:nixos/nixpkgs/nixos-25.05";
|
||||
|
||||
# Crane for Rust builds with dependency caching
|
||||
crane = {
|
||||
url = "github:ipetkov/crane";
|
||||
};
|
||||
crane = { url = "github:ipetkov/crane"; };
|
||||
|
||||
home-manager = {
|
||||
url = "github:nix-community/home-manager";
|
||||
@@ -21,24 +19,21 @@
|
||||
};
|
||||
|
||||
# Add flake-utils which was missing
|
||||
flake-utils = {
|
||||
url = "github:numtide/flake-utils";
|
||||
};
|
||||
flake-utils = { url = "github:numtide/flake-utils"; };
|
||||
};
|
||||
|
||||
outputs = { self, nixpkgs, crane, flake-utils, qitech-control, home-manager, ... }:
|
||||
outputs =
|
||||
{ self, nixpkgs, crane, flake-utils, qitech-control, home-manager, ... }:
|
||||
flake-utils.lib.eachDefaultSystem (system:
|
||||
let
|
||||
overlays = [
|
||||
# Add our own overlay for QiTech packages
|
||||
# Add our own overlay for QiTech packages with commit hash support
|
||||
(final: prev: {
|
||||
qitechPackages = {
|
||||
server = final.callPackage ./nixos/packages/server.nix {
|
||||
craneLib = crane.mkLib final;
|
||||
};
|
||||
electron = final.callPackage ./nixos/packages/electron.nix {
|
||||
nodejs = final.nodejs_22;
|
||||
craneLib = crane.mkLib pkgs;
|
||||
};
|
||||
electron = final.callPackage ./nixos/packages/electron.nix { };
|
||||
};
|
||||
})
|
||||
];
|
||||
@@ -46,8 +41,6 @@
|
||||
pkgs = import nixpkgs { inherit system overlays; };
|
||||
gitInfo = import ./nixos/gitInfo.nix { inherit pkgs; };
|
||||
|
||||
craneLib = crane.mkLib pkgs;
|
||||
|
||||
# Use Rust 1.86 stable from nixpkgs
|
||||
rust = pkgs.rustc;
|
||||
in {
|
||||
@@ -57,16 +50,21 @@
|
||||
default = self.packages.${system}.server;
|
||||
};
|
||||
|
||||
devShells.default = craneLib.devShell {
|
||||
devShells.default = pkgs.mkShell {
|
||||
packages = with pkgs; [
|
||||
cargo
|
||||
rustc
|
||||
pkg-config
|
||||
libudev-zero
|
||||
libpcap
|
||||
nodejs_22
|
||||
nodePackages.npm
|
||||
lldb
|
||||
electron
|
||||
];
|
||||
|
||||
ELECTON_SKIP_BINARY_DOWNLOAD = 1;
|
||||
|
||||
hardeningDisable = [ "fortify" ];
|
||||
|
||||
shellHook = ''
|
||||
@@ -75,55 +73,53 @@
|
||||
echo "Node version: $(${pkgs.nodejs_22}/bin/node --version)"
|
||||
'';
|
||||
};
|
||||
}
|
||||
) // {
|
||||
nixosModules.qitech = import ./nixos/modules/qitech.nix;
|
||||
nixosModules.default = self.nixosModules.qitech;
|
||||
}) // {
|
||||
nixosModules.qitech = import ./nixos/modules/qitech.nix;
|
||||
nixosModules.default = self.nixosModules.qitech;
|
||||
|
||||
# Define nixosConfigurations outside of eachDefaultSystem
|
||||
nixosConfigurations =
|
||||
let
|
||||
system = builtins.currentSystem;
|
||||
pkgs = import nixpkgs { inherit system; };
|
||||
gitInfo = import ./nixos/gitInfo.nix { inherit pkgs; };
|
||||
in {
|
||||
# Replace "nixos" with your actual hostname
|
||||
nixos = nixpkgs.lib.nixosSystem {
|
||||
system = system;
|
||||
specialArgs = {
|
||||
gitInfo = gitInfo; # Pass gitInfo to modules
|
||||
# Define nixosConfigurations outside of eachDefaultSystem
|
||||
nixosConfigurations = let
|
||||
system = builtins.currentSystem;
|
||||
pkgs = import nixpkgs { inherit system; };
|
||||
gitInfo = import ./nixos/gitInfo.nix { inherit pkgs; };
|
||||
in {
|
||||
# Replace "nixos" with your actual hostname
|
||||
nixos = nixpkgs.lib.nixosSystem {
|
||||
system = system;
|
||||
specialArgs = {
|
||||
gitInfo = gitInfo; # Pass gitInfo to modules
|
||||
};
|
||||
modules = [
|
||||
# Apply the overlays to the system
|
||||
{
|
||||
nixpkgs.overlays = [
|
||||
# Add our own overlay for QiTech packages with commit hash support
|
||||
(final: prev: {
|
||||
qitechPackages = {
|
||||
server = final.callPackage ./nixos/packages/server.nix {
|
||||
craneLib = crane.mkLib final;
|
||||
};
|
||||
electron =
|
||||
final.callPackage ./nixos/packages/electron.nix { };
|
||||
};
|
||||
})
|
||||
];
|
||||
}
|
||||
|
||||
./nixos/os/configuration.nix
|
||||
|
||||
# QiTech Control module
|
||||
self.nixosModules.qitech
|
||||
|
||||
# Home Manager module
|
||||
home-manager.nixosModules.home-manager
|
||||
{
|
||||
home-manager.useGlobalPkgs = true;
|
||||
home-manager.useUserPackages = true;
|
||||
home-manager.users.qitech = import ./nixos/os/home.nix;
|
||||
}
|
||||
];
|
||||
};
|
||||
modules = [
|
||||
# Apply the overlays to the system
|
||||
{ nixpkgs.overlays = [
|
||||
# Add our own overlay for QiTech packages with commit hash support
|
||||
(final: prev: {
|
||||
qitechPackages = {
|
||||
server = final.callPackage ./nixos/packages/server.nix {
|
||||
craneLib = crane.mkLib final;
|
||||
};
|
||||
electron = final.callPackage ./nixos/packages/electron.nix {
|
||||
nodejs = final.nodejs_22;
|
||||
};
|
||||
};
|
||||
})
|
||||
];
|
||||
}
|
||||
|
||||
./nixos/os/configuration.nix
|
||||
|
||||
# QiTech Control module
|
||||
self.nixosModules.qitech
|
||||
|
||||
# Home Manager module
|
||||
home-manager.nixosModules.home-manager
|
||||
{
|
||||
home-manager.useGlobalPkgs = true;
|
||||
home-manager.useUserPackages = true;
|
||||
home-manager.users.qitech = import ./nixos/os/home.nix;
|
||||
}
|
||||
];
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -28,7 +28,7 @@ socketioxide = { version = "0.17.2", features = ["msgpack"] }
|
||||
serialport = "4.7.3"
|
||||
|
||||
#tracing
|
||||
tracing = { version = "0.1.41", features = ["attributes"] }
|
||||
tracing = { version = "0.1.44", features = ["attributes"] }
|
||||
tonic = { version = "0.13.1", optional = true }
|
||||
|
||||
[dev-dependencies]
|
||||
|
||||
@@ -1,21 +1,32 @@
|
||||
use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
|
||||
|
||||
use crate::{MachineAct, analog_input_test_machine::AnalogInputTestMachine};
|
||||
use crate::{
|
||||
MachineAct, MachineMessage, MachineValues, analog_input_test_machine::AnalogInputTestMachine,
|
||||
};
|
||||
|
||||
impl MachineAct for AnalogInputTestMachine {
|
||||
fn act_machine_message(&mut self, msg: crate::MachineMessage) {
|
||||
fn act_machine_message(&mut self, msg: MachineMessage) {
|
||||
match msg {
|
||||
crate::MachineMessage::SubscribeNamespace(namespace) => {
|
||||
MachineMessage::SubscribeNamespace(namespace) => {
|
||||
self.namespace.namespace = Some(namespace);
|
||||
self.emit_measurement_rate();
|
||||
}
|
||||
crate::MachineMessage::UnsubscribeNamespace => self.namespace.namespace = None,
|
||||
crate::MachineMessage::HttpApiJsonRequest(value) => {
|
||||
MachineMessage::UnsubscribeNamespace => self.namespace.namespace = None,
|
||||
MachineMessage::HttpApiJsonRequest(value) => {
|
||||
use crate::MachineApi;
|
||||
let _res = self.api_mutate(value);
|
||||
}
|
||||
crate::MachineMessage::ConnectToMachine(_machine_connection) => {}
|
||||
crate::MachineMessage::DisconnectMachine(_machine_connection) => {}
|
||||
MachineMessage::DisconnectMachine(_machine_connection) => {}
|
||||
MachineMessage::RequestValues(sender) => {
|
||||
sender
|
||||
.send_blocking(MachineValues {
|
||||
state: serde_json::Value::Null,
|
||||
live_values: serde_json::Value::Null,
|
||||
})
|
||||
.expect("Failed to send values");
|
||||
sender.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -8,13 +8,13 @@ use ethercat_hal::{
|
||||
use smol::{block_on, channel::unbounded};
|
||||
|
||||
use crate::{
|
||||
EtherCATMachine, EtherCATParams, MachineNewHardware,
|
||||
MachineNewHardware, MachineNewParams, MachineNewTrait,
|
||||
analog_input_test_machine::{AnalogInputTestMachine, api::AnalogInputTestMachineNamespace},
|
||||
get_ethercat_device, validate_no_role_dublicates, validate_same_machine_identification_unique,
|
||||
};
|
||||
|
||||
impl EtherCATMachine for AnalogInputTestMachine {
|
||||
fn new<'maindevice>(params: &EtherCATParams) -> Result<Self, Error> {
|
||||
impl MachineNewTrait for AnalogInputTestMachine {
|
||||
fn new<'maindevice>(params: &MachineNewParams) -> Result<Self, Error> {
|
||||
// validate general stuff
|
||||
let device_identification = params
|
||||
.device_group
|
||||
@@ -50,7 +50,7 @@ impl EtherCATMachine for AnalogInputTestMachine {
|
||||
api_receiver: receiver,
|
||||
api_sender: sender,
|
||||
machine_identification_unique: params.get_machine_identification_unique(),
|
||||
main_sender: params.main_sender.clone(),
|
||||
main_sender: params.main_thread_channel.clone(),
|
||||
namespace,
|
||||
|
||||
last_measurement: Instant::now(),
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use super::{AquaPathV1, AquaPathV1Mode};
|
||||
use crate::{MachineAct, MachineMessage};
|
||||
use crate::{MachineAct, MachineMessage, MachineValues};
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
impl MachineAct for AquaPathV1 {
|
||||
@@ -45,14 +45,21 @@ impl MachineAct for AquaPathV1 {
|
||||
let _res = self.api_mutate(value);
|
||||
}
|
||||
MachineMessage::ConnectToMachine(_machine_connection) =>
|
||||
/*Doesnt connect to any Machine so do nothing*/
|
||||
{
|
||||
()
|
||||
}
|
||||
/*Doesnt connect to any Machine so do nothing*/
|
||||
{}
|
||||
MachineMessage::DisconnectMachine(_machine_connection) =>
|
||||
/*Doesnt connect to any Machine so do nothing*/
|
||||
{
|
||||
()
|
||||
/*Doesnt connect to any Machine so do nothing*/
|
||||
{}
|
||||
MachineMessage::RequestValues(sender) => {
|
||||
sender
|
||||
.send_blocking(MachineValues {
|
||||
state: serde_json::to_value(self.get_state())
|
||||
.expect("Failed to serialize state"),
|
||||
live_values: serde_json::to_value(self.get_live_values())
|
||||
.expect("Failed to serialize live values"),
|
||||
})
|
||||
.expect("Failed to send values");
|
||||
sender.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -109,8 +109,8 @@ impl std::fmt::Display for AquaPathV1 {
|
||||
}
|
||||
|
||||
impl AquaPathV1 {
|
||||
pub fn emit_live_values(&mut self) {
|
||||
let live_values = LiveValuesEvent {
|
||||
pub fn get_live_values(&self) -> LiveValuesEvent {
|
||||
LiveValuesEvent {
|
||||
front_temperature: self
|
||||
.front_controller
|
||||
.current_temperature
|
||||
@@ -123,13 +123,16 @@ impl AquaPathV1 {
|
||||
back_flow: self.back_controller.current_flow.get::<liter_per_minute>(),
|
||||
front_temp_reservoir: self.front_controller.temp_reservoir.get::<degree_celsius>(),
|
||||
back_temp_reservoir: self.back_controller.temp_reservoir.get::<degree_celsius>(),
|
||||
};
|
||||
let event = live_values.build();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn emit_live_values(&mut self) {
|
||||
let event = self.get_live_values().build();
|
||||
self.namespace.emit(AquaPathV1Events::LiveValues(event));
|
||||
}
|
||||
|
||||
pub fn emit_state(&mut self) {
|
||||
let state = StateEvent {
|
||||
pub fn get_state(&self) -> StateEvent {
|
||||
StateEvent {
|
||||
is_default_state: false,
|
||||
mode_state: ModeState {
|
||||
mode: self.mode.clone(),
|
||||
@@ -167,9 +170,11 @@ impl AquaPathV1 {
|
||||
should_flow: self.back_controller.should_pump,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
let event = state.build();
|
||||
pub fn emit_state(&mut self) {
|
||||
let event = self.get_state().build();
|
||||
self.namespace.emit(AquaPathV1Events::State(event));
|
||||
}
|
||||
}
|
||||
|
||||