Compare commits

...
Author SHA1 Message Date
Robin Krämer 6b5cb7d87e Merge pull request #737 from qitechgmbh/fix_state_handling
Fix state handling
2025-09-17 18:26:01 +02:00
93094049+kraemr@users.noreply.github.com 4894bbc231 fixed all state issues 2025-09-17 18:20:31 +02:00
Robin Krämer 721c5a99ee Merge pull request #736 from qitechgmbh/less_amps_spool_test
Less amps spool test
2025-09-17 17:28:23 +02:00
93094049+kraemr@users.noreply.github.com 36fccc80cb set puller amps to 2.7A 2025-09-17 17:21:15 +02:00
93094049+kraemr@users.noreply.github.com e14fe86306 testing with less current 2025-09-17 17:21:15 +02:00
Robin Krämer 95c9f99961 Merge pull request #735 from qitechgmbh/fix_compiler_flags
Fix compiler flags
2025-09-17 12:53:50 +02:00
93094049+kraemr@users.noreply.github.com 546189b9c8 removed log::error 2025-09-17 12:47:46 +02:00
93094049+kraemr@users.noreply.github.com dfbc2a65c0 hopefully all compiler flags now work as expected 2025-09-17 12:47:46 +02:00
Robin Krämer 2038d79e18 Merge pull request #734 from qitechgmbh/changelog-v2.7.0
updated Changelog for 2.7.0
2025-09-17 11:47:54 +02:00
93094049+kraemr@users.noreply.github.com 188caf397b updated Changelog for 2.7.0 2025-09-17 11:44:22 +02:00
Robin Krämer 13138b1d7d Merge pull request #726 from qitechgmbh/video-streaming
Add video streaming api
2025-09-16 18:25:34 +02:00
Icedsoda 5466efd8c3 add video streaming api 2025-09-16 18:23:36 +02:00
Robin Krämer 61f0dc92d1 Merge pull request #733 from qitechgmbh/685-use-nixos-cache-server-for-private-packages
685 use nixos cache server for private packages
2025-09-16 17:49:32 +02:00
93094049+kraemr@users.noreply.github.com 073c174203 removed unnecessary comment 2025-09-16 17:44:03 +02:00
93094049+kraemr@users.noreply.github.com 1d0f7231e5 added substituter
fixed nix config

actually fixed config now

test

removed git hash as version

nix sucks

removed gitcommitHash from overlays

added fake version

fixed compile script

test

fixed script

updated ip

test

test 2

uppdated script and fixed config

to check for caching

test

fixed config

test

test

extra-subs changed to trusted-public-keys

test

test

removed substituter

removed print statements
2025-09-16 17:44:03 +02:00
93094049+kraemr@users.noreply.github.com 9401c81581 added scripts to compile our nix packages 2025-09-16 17:44:03 +02:00
Robin Krämer 936e54f3e2 Merge pull request #731 from qitechgmbh/666-implement-laserv2
666 implement laserv2
2025-09-16 16:54:29 +02:00
snacj 0c02fc1ce0 Added Comment do explain 45deg rotation 2025-09-16 16:52:12 +02:00
snacj c723f1df00 Fixed Graphs to work with 2axis and 1 axis 2025-09-16 16:52:12 +02:00
snacj c65a3f7fb1 Renamed Values and changed to one Graph 2025-09-16 16:52:12 +02:00
snacj a0c59f9bb6 Added Const for graph color. 2025-09-16 16:52:12 +02:00
snacj 069dcd65bb Removed tracing::info! 2025-09-16 16:52:12 +02:00
snacj 5331d39c1d Changed Visual of Diameter to use x and y values 2025-09-16 16:52:12 +02:00
snacj 2c928e6158 Implemented 2-Axis Laser 2025-09-16 16:52:12 +02:00
snacj 3ae17479cb Adjusted Frontend to work with 2-Axis Laser. 2025-09-16 16:52:12 +02:00
93094049+kraemr@users.noreply.github.com b3589755f5 fixed winder events 2025-09-16 16:41:38 +02:00
Robin Krämer e7f97ff4d4 Merge pull request #729 from qitechgmbh/728-set_irq_handler_affinity-doesnt-work-on-macos
fixed target_os flags for set_irq_affinity
2025-09-16 15:37:40 +02:00
30 changed files with 716 additions and 226 deletions

No files matched your search

+43 -5
View File
@@ -1,57 +1,94 @@
# `2.7.0`
_17.09.2025_
## Extruder V2
- [#695](https://github.com/qitechgmbh/control/pull/695) Pressure and Rpm Regulation properly switch again
## Winder V2
- [#727](https://github.com/qitechgmbh/control/pull/727) Removed estimated diameter
## Laser V1/V2
- [#731](https://github.com/qitechgmbh/control/pull/731) Added LaserV2 with two-axis measurements
## General
- [#679](https://github.com/qitechgmbh/control/pull/679),[#677](https://github.com/qitechgmbh/control/pull/677),[#698](https://github.com/qitechgmbh/control/pull/698), [#684](https://github.com/qitechgmbh/control/pull/684),[#706](https://github.com/qitechgmbh/control/pull/706) Greatly improved overall system latency and stability
- [#720](https://github.com/qitechgmbh/control/pull/720), [#686](https://github.com/qitechgmbh/control/pull/686) Improved User Interface
# `2.6.0`
_03.09.2025_
## Extruder V2
- [#658](https://github.com/qitechgmbh/control/pull/658) Added manual
- [#639](https://github.com/qitechgmbh/control/pull/639) Swapped order of screw speed regulation buttons
## Laser V1
- [#642](https://github.com/qitechgmbh/control/pull/642) Improved stability of laser connection
## General
- [#657](https://github.com/qitechgmbh/control/pull/657) Time frame for graphs is now remembered
- [#641](https://github.com/qitechgmbh/control/pull/641) Removed duplicate scrollbar from manuals
# `2.5.0`
_27.08.2025_
## Extruder V2
- [#595](https://github.com/qitechgmbh/control/pull/600) Simplified power graphs
- [#593](https://github.com/qitechgmbh/control/pull/599) Added identifiers for temperature sensors in temperature graph
## Winder V2
- [#598](https://github.com/qitechgmbh/control/pull/601) Images now load properly in the manual
## General
- [#540](https://github.com/qitechgmbh/control/pull/592) Improved readability of the presets
- [#591](https://github.com/qitechgmbh/control/pull/626) Significantly improved user interface performance and stability
# `2.4.0`
_14.08.2025_
## Winder V2
## Winder V2
- [#550](https://github.com/qitechgmbh/control/pull/551) Can now reset wound distance while winding
## General
- [#568](https://github.com/qitechgmbh/control/pull/569),[#572](https://github.com/qitechgmbh/control/pull/572), [#574](https://github.com/qitechgmbh/control/pull/587) Improved EtherCAT performance
- [#568](https://github.com/qitechgmbh/control/pull/569),[#572](https://github.com/qitechgmbh/control/pull/572), [#574](https://github.com/qitechgmbh/control/pull/587) Improved EtherCAT performance
- [#588](https://github.com/qitechgmbh/control/pull/589) Improved frontend performance
# `2.3.0`
_06.08.2025_
## Extruder V2
- [#520](https://github.com/qitechgmbh/control/pull/523) added warning message incase inverter is overloaded
- [#501](https://github.com/qitechgmbh/control/pull/508) raised pressure limit of pressured PID mode for extruder
- [#519](https://github.com/qitechgmbh/control/pull/549) added total power consumption monitor and graphs for voltage,current and wattage of the motor
## Winder V2
## Winder V2
- [#64](https://github.com/qitechgmbh/control/pull/504) added winder handbook
## Laser V1
- [#497](https://github.com/qitechgmbh/control/pull/498) improved diameter visualisation
## General
- [#62](https://github.com/qitechgmbh/control/pull/514),[#524](https://github.com/qitechgmbh/control/pull/528) added machine presets to load configurations
- [#526](https://github.com/qitechgmbh/control/pull/541) added import and export of presets
@@ -67,6 +104,7 @@ _15.07.2025_
- [#169](https://github.com/qitechgmbh/control/pull/492), Added a Confirmation Dialog when extruder has not reached 90% of target temperature
## Winder V2
- [#471](https://github.com/qitechgmbh/control/pull/480), Added a live value to track the pulled distance and the option of switching to a different Mode after a given distance.
## General
Generated
+1
View File
@@ -596,6 +596,7 @@ dependencies = [
"smol",
"socketioxide",
"textplots",
"tokio",
"tracing",
"uom",
]
+1 -1
View File
@@ -24,7 +24,7 @@ if [ "$1" != "mock-machine" ]; then
sudo setcap 'cap_net_raw,cap_sys_nice=eip' ./target/debug/server
echo "Setting permissions for serial ports"
sudo chown "root:$(groups | cut -f1 -d' ')" /dev/ttyUSB*
# sudo chown "root:$(groups | cut -f1 -d' ')" /dev/ttyUSB*
fi
# run
+43
View File
@@ -0,0 +1,43 @@
#!/bin/bash
# Nix store uses sqlite ... SO HAHAHAHAHH NO CONCURRENT ACCESS AHAHAHHAHAHAHHAHAHAHAH
time nix build .#packages.x86_64-linux.server --impure --extra-experimental-features nix-command --extra-experimental-features flakes --out-link /tmp/server-result
serverstore=$(readlink -f /tmp/server-result)
time nix build .#packages.x86_64-linux.electron --impure --option sandbox false --extra-experimental-features nix-command --extra-experimental-features flakes --out-link /tmp/electron-result
electronstore=$(readlink -f /tmp/electron-result)
nix copy --to "file:///opt/dev-cache" \
$(readlink -f /tmp/server-result) \
$(readlink -f /tmp/electron-result) --extra-experimental-features nix-command
nix store sign \
--key-file /opt/dev-cache/cache-private-key.pem \
--store "file:///opt/dev-cache" \
--extra-experimental-features nix-command \
"$serverstore" "$electronstore"
nix path-info "$serverstore" --extra-experimental-features nix-command --extra-experimental-features flakes
# nix verify --store "file:///opt/dev-cache" \
# --trusted-public-keys "$(cat /opt/dev-cache/cache-public-key.pem)" --extra-experimental-features nix-command --extra-experimental-features flakes \
# "$FIRST_PATH" || echo "Verification failed!"
#time nix build .#packages.x86_64-linux.electron --extra-experimental-features nix-command --extra-experimental-features flakes --out-link /tmp/electron-result
#electronstore=$(readlink -f /tmp/electron-result)
#nix-store --generate-binary-cache-key cache.key cache.pem --extra-experimental-features nix-command
#nix store sign --key-file cache.key "$serverstore" --extra-experimental-features nix-command
#nix path-info --json "$serverstore" --extra-experimental-features nix-command --extra-experimental-features flakes
#export
#nix-store --export /nix/store/<hash>-server > nix-cache/server.nar
#nix-store --export /nix/store/<hash>-electron > nix-cache/electron.nar
# Any http server
# cd ~/nix-cache
# python3 -m http.server 8080
#
+7
View File
@@ -25,6 +25,7 @@ tracing = "0.1.41"
erased-serde = "0.4.6"
core_affinity = "0.8.3"
control_core_derive = { version = "0.1.0", path = "../control-core-derive" }
tokio = { version = "1.45.1", features = ["sync"], optional = true }
[target.'cfg(unix)'.dependencies]
@@ -33,3 +34,9 @@ libc = "0.2.175"
[dev-dependencies]
approx = "0.5.1"
textplots = "0.8"
tokio = { version = "1.45.1", features = ["rt-multi-thread", "macros", "time"] }
[features]
default = []
video-streaming = ["dep:tokio"]
+3
View File
@@ -12,5 +12,8 @@ pub mod socketio;
pub mod transmission;
pub mod uom_extensions;
#[cfg(feature = "video-streaming")]
pub mod video_streaming;
#[macro_use]
extern crate uom;
+17
View File
@@ -5,4 +5,21 @@ use crate::socketio::namespace::Namespace;
pub trait MachineApi {
fn api_mutate(&mut self, value: Value) -> Result<(), anyhow::Error>;
fn api_event_namespace(&mut self) -> &mut Namespace;
/// Returns a list of available video stream identifiers for this machine
#[cfg(feature = "video-streaming")]
fn api_video_streams(&self) -> Vec<String> {
Vec::new()
}
/// Get a video stream receiver for the specified stream ID
/// Returns None if the stream doesn't exist
#[cfg(feature = "video-streaming")]
fn api_get_video_stream(
&mut self,
stream_id: &str,
) -> Option<tokio::sync::broadcast::Receiver<Vec<u8>>> {
let _ = stream_id;
None
}
}
+7 -7
View File
@@ -1,7 +1,7 @@
use tracing::debug;
use tracing::error;
#[cfg(unix)]
#[cfg(target_os = "linux")]
/// Makes the current thread a real-time thread using the PREEMPT_RT capabilities of Linux.
///
/// This function configures the current thread with real-time scheduling properties by:
@@ -47,7 +47,7 @@ use tracing::error;
///
/// - `Ok(())` if the real-time priority was successfully set
/// - `Err` with the error message if setting the priority failed
#[cfg(unix)]
#[cfg(target_os = "linux")]
pub fn set_realtime_priority() -> Result<(), anyhow::Error> {
use anyhow::anyhow;
use libc::{SCHED_FIFO, pthread_setschedparam, sched_param};
@@ -84,9 +84,9 @@ pub fn set_realtime_priority() -> Result<(), anyhow::Error> {
Ok(())
}
#[cfg(not(unix))]
#[cfg(not(target_os = "linux"))]
pub fn set_realtime_priority() -> Result<(), anyhow::Error> {
log::error!("This platform does not support realtime threads");
tracing::error!("This platform does not support realtime threads");
Ok(())
}
@@ -137,7 +137,7 @@ pub fn set_realtime_priority() -> Result<(), anyhow::Error> {
/// # How it works
///
/// [Rest of documentation as before...]
#[cfg(unix)]
#[cfg(target_os = "linux")]
pub fn lock_memory() -> Result<(), Box<dyn std::error::Error>> {
use libc::{MCL_FUTURE, mlockall};
@@ -154,9 +154,9 @@ pub fn lock_memory() -> Result<(), Box<dyn std::error::Error>> {
Ok(())
}
#[cfg(not(unix))]
#[cfg(not(target_os = "linux"))]
pub fn lock_memory() -> Result<(), Box<dyn std::error::Error>> {
log::error!("This platform does not support memory locking");
tracing::error!("This platform does not support memory locking");
Ok(())
}
+16
View File
@@ -33,3 +33,19 @@ where
pub machine_identification_unique: MachineIdentificationUnique,
pub data: T,
}
#[derive(Debug, serde::Deserialize)]
pub struct MachineVideoStreamListBody {
pub machine_identification_unique: MachineIdentificationUnique,
}
#[derive(Debug, serde::Deserialize)]
pub struct MachineVideoStreamBody {
pub machine_identification_unique: MachineIdentificationUnique,
pub stream_id: String,
}
#[derive(Debug, Serialize)]
pub struct VideoStreamListResponse {
pub streams: Vec<String>,
}
+208
View File
@@ -0,0 +1,208 @@
use anyhow::Error;
use std::collections::HashMap;
use tokio::sync::broadcast;
/// Video stream manager that handles multiple video streams per machine with true broadcasting
/// Uses tokio broadcast channels to ensure all subscribers receive all messages without stealing
#[derive(Debug)]
pub struct VideoStreamManager {
/// Each stream has its own broadcast transmitter and template receiver
streams: HashMap<String, (broadcast::Sender<Vec<u8>>, broadcast::Receiver<Vec<u8>>)>,
}
impl VideoStreamManager {
pub fn new() -> Self {
Self {
streams: HashMap::new(),
}
}
/// Send a frame to all subscribers of a stream
/// Returns Ok if the frame was sent, Err if the stream doesn't exist
pub fn send_frame(&self, stream_id: &str, frame: Vec<u8>) -> Result<(), Error> {
if let Some((tx, _rx)) = self.streams.get(stream_id) {
match tx.send(frame) {
Ok(_) => Ok(()),
Err(_) => Ok(()), // No receivers, but that's not an error
}
} else {
Err(anyhow::anyhow!("Stream '{}' not found", stream_id))
}
}
/// Register a new video stream with the given ID
pub fn add_stream(&mut self, stream_id: String) {
let (tx, rx) = broadcast::channel::<Vec<u8>>(256); // 256 frame buffer capacity
self.streams.insert(stream_id, (tx, rx));
}
/// Get list of available stream IDs
pub fn get_streams(&self) -> Vec<String> {
self.streams.keys().cloned().collect()
}
/// Check if a stream exists
pub fn has_stream(&self, stream_id: &str) -> bool {
self.streams.contains_key(stream_id)
}
/// Remove a stream and all its subscribers
pub fn remove_stream(&mut self, stream_id: &str) -> bool {
self.streams.remove(stream_id).is_some()
}
/// Subscribe to a video stream and get a receiver
/// Returns the receiver for the new subscription
pub fn subscribe_to_stream(&self, stream_id: &str) -> Option<broadcast::Receiver<Vec<u8>>> {
self.streams
.get(stream_id)
.map(|(_tx, rx)| rx.resubscribe()) // Resubscribe to create a new independent receiver
}
/// Get subscriber count for a stream
pub fn get_subscriber_count(&self, stream_id: &str) -> usize {
self.streams
.get(stream_id)
.map_or(0, |(tx, _rx)| tx.receiver_count())
}
/// Get a stream transmitter for sending frames (used by machines to send video)
/// Returns None if the stream doesn't exist
pub fn get_sender(&self, stream_id: &str) -> Option<broadcast::Sender<Vec<u8>>> {
self.streams.get(stream_id).map(|(tx, _rx)| tx.clone())
}
}
impl Default for VideoStreamManager {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_video_stream_manager() {
let mut manager = VideoStreamManager::new();
// Register streams
manager.register_stream("camera1".to_string());
manager.register_stream("camera2".to_string());
assert_eq!(manager.get_stream_ids().len(), 2);
assert!(manager.has_stream("camera1"));
assert!(manager.has_stream("camera2"));
assert!(!manager.has_stream("camera3"));
// Subscribe to streams
let mut rx1 = manager.subscribe_to_stream("camera1").unwrap();
let mut rx2 = manager.subscribe_to_stream("camera1").unwrap();
let mut rx3 = manager.subscribe_to_stream("camera2").unwrap();
assert_eq!(manager.get_subscriber_count("camera1"), 3); // Original + 2 clones
assert_eq!(manager.get_subscriber_count("camera2"), 2); // Original + 1 clone
assert_eq!(manager.get_subscriber_count("nonexistent"), 0);
// Send frames
let frame1 = vec![1, 2, 3, 4];
let frame2 = vec![5, 6, 7, 8];
assert!(manager.send_single_frame("camera1", frame1.clone()));
assert!(manager.send_single_frame("camera2", frame2.clone()));
assert!(!manager.send_single_frame("nonexistent", vec![]));
// Both camera1 subscribers should receive the frame
assert_eq!(rx1.recv().await.unwrap(), frame1);
assert_eq!(rx2.recv().await.unwrap(), frame1);
// Camera2 subscriber should receive its frame
assert_eq!(rx3.recv().await.unwrap(), frame2);
}
#[tokio::test]
async fn test_video_frame_broadcasting() {
let mut manager = VideoStreamManager::new();
manager.register_stream("main_camera".to_string());
// Multiple clients subscribe to the same camera
let mut rx1 = manager.subscribe_to_stream("main_camera").unwrap();
let mut rx2 = manager.subscribe_to_stream("main_camera").unwrap();
let mut rx3 = manager.subscribe_to_stream("main_camera").unwrap();
// Simulate video frames
let frames = vec![
vec![0xFF, 0xD8, 0xFF, 0xE0], // JPEG header
vec![0x00, 0x01, 0x02, 0x03], // Frame 1
vec![0x04, 0x05, 0x06, 0x07], // Frame 2
];
for frame in &frames {
assert!(manager.send_single_frame("main_camera", frame.clone()));
}
// All clients should receive all frames in order
for frame in &frames {
assert_eq!(rx1.recv().await.unwrap(), *frame);
assert_eq!(rx2.recv().await.unwrap(), *frame);
assert_eq!(rx3.recv().await.unwrap(), *frame);
}
}
#[test]
fn test_stream_management() {
let mut manager = VideoStreamManager::new();
// Initially no streams
assert_eq!(manager.get_stream_ids().len(), 0);
// Add streams
manager.register_stream("front_camera".to_string());
manager.register_stream("rear_camera".to_string());
manager.register_stream("side_camera".to_string());
let mut stream_ids = manager.get_stream_ids();
stream_ids.sort(); // Sort for consistent testing
assert_eq!(
stream_ids,
vec!["front_camera", "rear_camera", "side_camera"]
);
// Remove a stream
assert!(manager.remove_stream("rear_camera"));
assert!(!manager.remove_stream("rear_camera")); // Already removed
assert!(!manager.has_stream("rear_camera"));
assert_eq!(manager.get_stream_ids().len(), 2);
}
#[tokio::test]
async fn test_send_frame_results() {
let mut manager = VideoStreamManager::new();
manager.register_stream("test_camera".to_string());
// One initial receiver (the original one created in register_stream)
let result = manager.send_frame("test_camera", vec![1, 2, 3]).unwrap();
assert_eq!(result, 1); // Number of subscribers
// Add subscribers by cloning the receiver
let _rx1 = manager.subscribe_to_stream("test_camera").unwrap();
let _rx2 = manager.subscribe_to_stream("test_camera").unwrap();
// Send frame to multiple subscribers (original + 2 clones = 3 total)
let result = manager.send_frame("test_camera", vec![4, 5, 6]).unwrap();
assert_eq!(result, 3); // Number of subscribers
// Try sending to non-existent stream
let result = manager.send_frame("nonexistent", vec![7, 8, 9]);
assert!(result.is_err());
assert_eq!(result.unwrap_err(), "Stream 'nonexistent' not found");
}
#[test]
fn test_default_video_stream_manager() {
let manager = VideoStreamManager::default();
assert_eq!(manager.get_stream_ids().len(), 0);
}
}
@@ -2,11 +2,13 @@ import React from "react";
import { TimeSeries } from "@/lib/timeseries";
import { cva } from "class-variance-authority";
type AnimateCableProps = {
type DiameterVisualisationProps = {
targetDiameter: number;
lowTolerance: number;
highTolerance: number;
diameter: TimeSeries;
x_value?: TimeSeries;
y_value?: TimeSeries;
};
// Ring color depending on tolerance
@@ -24,7 +26,7 @@ const dashedRingClass = cva(
"absolute rounded-full border border-dashed border-black z-15",
);
// Dynamic animated circle
// Dynamic animated circle/ellipse
const dynamicCircleClass = cva(
"absolute rounded-full border-2 border-black bg-transparent z-30 transition-all duration-300 ease-in-out",
);
@@ -34,103 +36,86 @@ export function DiameterVisualisation({
lowTolerance,
highTolerance,
diameter,
}: AnimateCableProps) {
x_value,
y_value,
}: DiameterVisualisationProps) {
const actualDiameter = diameter.current?.value ?? 0.0;
const actualX = x_value?.current?.value ?? actualDiameter;
const actualY = y_value?.current?.value ?? actualDiameter;
const minDia = targetDiameter - lowTolerance;
const maxDia = targetDiameter + highTolerance;
const totalTolerance = lowTolerance + highTolerance;
const midDia = minDia + (maxDia - minDia) * (lowTolerance / totalTolerance);
const pixelMin = 100;
const pixelMax = 200;
const lerp = (
value: number,
inputMin: number,
inputMax: number,
outputMin: number,
outputMax: number,
) => {
return (
outputMin +
((value - inputMin) * (outputMax - outputMin)) / (inputMax - inputMin)
);
};
const pixelMid = lerp(midDia, minDia, maxDia, pixelMin, pixelMax);
let dynamicDiameterPx: number | null = null;
) =>
outputMin +
((value - inputMin) * (outputMax - outputMin)) / (inputMax - inputMin);
const clamp = (value: number, min: number, max: number) =>
Math.min(Math.max(value, min), max);
if (actualDiameter > 0) {
if (actualDiameter < minDia) {
dynamicDiameterPx = lerp(actualDiameter, 0, minDia, 0, pixelMin);
} else if (actualDiameter <= maxDia) {
dynamicDiameterPx = lerp(
actualDiameter,
minDia,
maxDia,
pixelMin,
pixelMax,
);
} else {
const dynamicDiameterPxLerp = lerp(
actualDiameter,
maxDia,
maxDia * 1.5,
pixelMax,
pixelMax * 1.1,
);
dynamicDiameterPx = clamp(
dynamicDiameterPxLerp,
pixelMin,
pixelMax * 1.1,
);
}
}
const radiusX = clamp(
lerp(actualX, minDia, maxDia, pixelMin / 2, pixelMax / 2),
pixelMin / 2,
pixelMax / 2,
);
const radiusY = clamp(
lerp(actualY, minDia, maxDia, pixelMin / 2, pixelMax / 2),
pixelMin / 2,
pixelMax / 2,
);
const bigRadius = pixelMax / 2;
const smallRadius = pixelMin / 2;
const midRadius = pixelMid / 2;
const dynamicRadius = dynamicDiameterPx ? dynamicDiameterPx / 2 : 0;
const inTolerance = actualDiameter >= minDia && actualDiameter <= maxDia;
const centerStyle = (radius: number) => ({
width: radius * 2,
height: radius * 2,
top: `${bigRadius - radius}px`,
left: `${bigRadius - radius}px`,
// Center the shape
const centerStyle = (rx: number, ry: number) => ({
width: rx * 2,
height: ry * 2,
top: `${bigRadius - ry}px`,
left: `${bigRadius - rx}px`,
});
// Check if current measurement is in tolerance
const inTolerance =
actualX >= minDia &&
actualX <= maxDia &&
actualY >= minDia &&
actualY <= maxDia;
return (
<div
className="relative mx-auto"
style={{ width: pixelMax, height: pixelMax }}
>
{/* Outer ring background */}
<div
className={`${ringClass({
state: inTolerance ? "inRange" : "outOfRange",
})} top-0 left-0 z-10`}
className={`${ringClass({ state: inTolerance ? "inRange" : "outOfRange" })} top-0 left-0 z-10`}
style={{ width: pixelMax, height: pixelMax }}
/>
{/* Inner white circle to cut out the center */}
<div
className="absolute z-20 rounded-full bg-white"
style={centerStyle(smallRadius)}
style={centerStyle(pixelMin / 2, pixelMin / 2)}
/>
<div
className={dashedRingClass()}
style={centerStyle(pixelMax / 2, pixelMax / 2)}
/>
<div
className={dynamicCircleClass()}
style={{
...centerStyle(radiusX, radiusY),
/* rotate by 45deg to mimic the rotated axis of the Laser */
transform: "rotate(45deg)",
}}
/>
{/* Dashed ring in the middle */}
<div className={dashedRingClass()} style={centerStyle(midRadius)} />
{/* Dynamic circle */}
{dynamicDiameterPx !== null && (
<div
className={dynamicCircleClass()}
style={centerStyle(dynamicRadius)}
/>
)}
</div>
);
}
@@ -14,6 +14,8 @@ import { DiameterVisualisation } from "../DiameterVisualisation";
export function Laser1ControlPage() {
const {
diameter,
x_value,
y_value,
state,
defaultState,
setTargetDiameter,
@@ -34,15 +36,37 @@ export function Laser1ControlPage() {
lowTolerance={lowerTolerance}
highTolerance={higherTolerance}
diameter={diameter}
x_value={x_value}
y_value={y_value}
/>
<div className="flex flex-row items-center gap-6">
<TimeSeriesValueNumeric
label="Current Diameter"
label="Diameter"
unit="mm"
timeseries={diameter}
renderValue={(value) => value.toFixed(3)}
/>
</div>
{x_value?.current && (
<div className="flex flex-row items-center gap-6">
<TimeSeriesValueNumeric
label="X-Diameter"
unit="mm"
timeseries={x_value}
renderValue={(value) => value.toFixed(3)}
/>
</div>
)}
{y_value?.current && (
<div className="flex flex-row items-center gap-6">
<TimeSeriesValueNumeric
label="Y-Diameter"
unit="mm"
timeseries={y_value}
renderValue={(value) => value.toFixed(3)}
/>
</div>
)}
</ControlCard>
<ControlCard title="Settings">
<Label label="Set Target Diameter">
@@ -10,63 +10,124 @@ import React from "react";
import { useLaser1 } from "./useLaser1";
export function Laser1GraphsPage() {
const { diameter, state } = useLaser1();
const { diameter, x_value, y_value, state } = useLaser1();
const syncHook = useGraphSync("diameter-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_value?.current || !!y_value?.current;
const diameterColor = "#3b82f6";
const xDiameterColor = "#ef4444";
const yDiameterColor = "#22c55e";
const config: GraphConfig = {
title: "Diameter",
defaultTimeWindow: 30 * 60 * 1000, // 30 minutes
exportFilename: "diameter_data",
colors: {
primary: "#3b82f6",
primary: diameterColor,
grid: "#e2e8f0",
axis: "#64748b",
background: "#ffffff",
},
};
return (
<Page className="pb-27">
<div className="flex flex-col gap-4">
<AutoSyncedBigGraph
syncHook={syncHook}
newData={{
newData: diameter,
color: "#3b82f6",
lines: [
if (!isTwoAxis) {
return (
<Page className="pb-27">
<div className="flex flex-col gap-4">
<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={[
{
type: "threshold",
value: targetDiameter + higherTolerance,
label: "Upper Threshold",
color: "#3b82f6",
dash: [5, 5],
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,
},
],
},
{
type: "threshold",
value: targetDiameter - lowerTolerance,
label: "Lower Threshold",
color: "#3b82f6",
dash: [5, 5],
newData: x_value,
color: xDiameterColor,
title: "X-Diameter",
},
{
type: "target",
value: targetDiameter,
label: "Target",
color: "#3b82f6",
newData: y_value,
color: yDiameterColor,
title: "Y-Diameter",
},
],
}}
unit="mm"
renderValue={(value) => value.toFixed(3)}
config={config}
graphId="diameter-graph"
/>
</div>
<SyncedFloatingControlPanel controlProps={syncHook.controlProps} />
</Page>
);
]}
unit="mm"
renderValue={(value) => value.toFixed(3)}
config={config}
graphId="diameter-graph"
/>
</div>
<SyncedFloatingControlPanel controlProps={syncHook.controlProps} />
</Page>
);
}
}
@@ -28,6 +28,8 @@ import {
*/
export const liveValuesEventDataSchema = z.object({
diameter: z.number(),
x_value: z.number().nullable(),
y_value: z.number().nullable(),
});
/**
@@ -57,6 +59,8 @@ export type Laser1NamespaceStore = {
// Time series data for live values
diameter: TimeSeries;
x_value: TimeSeries;
y_value: TimeSeries;
};
// Constants for time durations
@@ -70,6 +74,20 @@ const { initialTimeSeries: diameter, insert: addDiameter } = createTimeSeries(
FIVE_SECOND,
ONE_HOUR,
);
const { initialTimeSeries: x_value, insert: addXValue } = createTimeSeries(
TWENTY_MILLISECOND,
ONE_SECOND,
FIVE_SECOND,
ONE_HOUR,
);
const { initialTimeSeries: y_value, insert: addYValue } = createTimeSeries(
TWENTY_MILLISECOND,
ONE_SECOND,
FIVE_SECOND,
ONE_HOUR,
);
/**
* Factory function to create a new Laser1 namespace store
* @returns A new Zustand store instance for Laser1 namespace
@@ -80,6 +98,8 @@ export const createLaser1NamespaceStore = (): StoreApi<Laser1NamespaceStore> =>
state: null,
defaultState: null,
diameter: diameter,
x_value: x_value,
y_value: y_value,
};
});
@@ -119,13 +139,34 @@ export function laser1MessageHandler(
// Live values events (keep for 1 hour)
else if (eventName === "LiveValuesEvent") {
const liveValuesEvent = liveValuesEventSchema.parse(event);
const timeseriesValue: TimeSeriesValue = {
const diameterValue: TimeSeriesValue = {
value: liveValuesEvent.data.diameter,
timestamp: event.ts,
};
if (liveValuesEvent.data.x_value !== null) {
const xValue: TimeSeriesValue = {
value: liveValuesEvent.data.x_value,
timestamp: event.ts,
};
updateStore((state) => ({
...state,
x_value: addXValue(state.x_value, xValue),
}));
}
if (liveValuesEvent.data.y_value !== null) {
const yValue: TimeSeriesValue = {
value: liveValuesEvent.data.y_value,
timestamp: event.ts,
};
updateStore((state) => ({
...state,
y_value: addYValue(state.y_value, yValue),
}));
}
updateStore((state) => ({
...state,
diameter: addDiameter(state.diameter, timeseriesValue),
diameter: addDiameter(state.diameter, diameterValue),
}));
} else {
handleUnhandledEventError(eventName);
@@ -11,9 +11,8 @@ import { produce } from "immer";
function useLaser(machine_identification_unique: MachineIdentificationUnique) {
// Get consolidated state and live values from namespace
const { state, defaultState, diameter } = useLaser1Namespace(
machine_identification_unique,
);
const { state, defaultState, diameter, x_value, y_value } =
useLaser1Namespace(machine_identification_unique);
// Single optimistic state for all state management
const stateOptimistic = useStateOptimistic<StateEvent>();
@@ -111,6 +110,8 @@ function useLaser(machine_identification_unique: MachineIdentificationUnique) {
// Live values (TimeSeries)
diameter,
x_value,
y_value,
// Loading states
isLoading: stateOptimistic.isOptimistic,
@@ -112,15 +112,14 @@ export function Winder2ControlPage() {
maxLabel="OUT"
max={180}
renderValue={(value) => roundToDecimals(value, 0)}
inverted
onChange={setTraverseLimitOuter}
/>
<TouchButton
variant="outline"
icon="lu:ArrowLeftToLine"
onClick={gotoTraverseLimitOuter}
disabled={isDisabled || !state?.traverse_state?.can_go_out}
isLoading={isLoading || state?.traverse_state?.is_going_out}
disabled={isDisabled}
isLoading={isLoading}
>
Go to Outer Limit
</TouchButton>
@@ -149,8 +148,8 @@ export function Winder2ControlPage() {
variant="outline"
icon="lu:ArrowRightToLine"
onClick={gotoTraverseLimitInner}
disabled={isDisabled || !state?.traverse_state?.can_go_in}
isLoading={isLoading || state?.traverse_state?.is_going_in}
disabled={isDisabled}
isLoading={isLoading}
>
Go to Inner Limit
</TouchButton>
@@ -171,8 +170,8 @@ export function Winder2ControlPage() {
variant="outline"
icon="lu:House"
onClick={() => gotoTraverseHome()}
disabled={isDisabled || !state?.traverse_state?.can_go_home}
isLoading={isLoading || state?.traverse_state?.is_going_home}
disabled={isDisabled}
isLoading={isLoading}
>
Go to Home
</TouchButton>
@@ -259,6 +259,7 @@ export function winder2MessageHandler(
try {
if (eventName === "StateEvent") {
console.log(event);
// Parse and validate the state event
const stateEvent = stateEventSchema.parse(event);
-4
View File
@@ -34,11 +34,9 @@
(final: prev: {
qitechPackages = {
server = final.callPackage ./nixos/packages/server.nix {
commitHash = builtins.getEnv "QITECH_COMMIT_HASH";
craneLib = crane.mkLib final;
};
electron = final.callPackage ./nixos/packages/electron.nix {
commitHash = builtins.getEnv "QITECH_COMMIT_HASH";
nodejs = final.nodejs_22;
};
};
@@ -101,11 +99,9 @@
(final: prev: {
qitechPackages = {
server = final.callPackage ./nixos/packages/server.nix {
commitHash = gitInfo.gitCommit;
craneLib = crane.mkLib final;
};
electron = final.callPackage ./nixos/packages/electron.nix {
commitHash = gitInfo.gitCommit;
nodejs = final.nodejs_22;
};
};
+30
View File
@@ -0,0 +1,30 @@
touch /tmp/installInfo.nix
# Capture all git information
GIT_TIMESTAMP=$(git --no-pager show -s --format=%cI HEAD) # e.g., "2025-06-10T14:30:45+02:00"
GIT_COMMIT=$(git rev-parse HEAD) # e.g., "b2c7f6e0b138174770798f84ada8b0aa65afeb"
GIT_URL=$(git config --get remote.origin.url) # e.g., "https://github.com/qitechindustries/control.git"
# Determine the git abbreviation (tag, branch, or commit hash)
GIT_TAG=$(git describe --tags --exact-match HEAD 2>/dev/null || echo "")
if [ -n "$GIT_TAG" ]; then
GIT_ABBREVIATION="$GIT_TAG" # e.g., "2.0.0" (when on a tag)
else
GIT_BRANCH=$(git rev-parse --abbrev-ref HEAD)
if [ "$GIT_BRANCH" = "HEAD" ]; then
GIT_ABBREVIATION=$(git rev-parse --short HEAD) # e.g., "b2c7f6e" (when on detached HEAD/commit)
else
GIT_ABBREVIATION="$GIT_BRANCH" # e.g., "main", "develop" (when on a branch)
fi
fi
tee /tmp/installInfo.nix > /dev/null << EOF
{
gitTimestamp = "$GIT_TIMESTAMP";
gitCommit = "$GIT_COMMIT";
gitAbbreviation = "$GIT_ABBREVIATION";
gitUrl = "$GIT_URL";
# Like abbreviation but can be used in system.nixos.label
gitAbbreviationEscaped = "$GIT_ABBREVIATION_ESCAPED";
}
EOF
+3 -2
View File
@@ -15,10 +15,11 @@ in
consoleMode = "max"; # Use the highest available resolution
};
boot.loader.efi.canTouchEfiVariables = true;
boot.kernelPackages = pkgs.linuxPackages-rt_latest;
boot.kernelModules = [ "i915" ];
boot.kernelParams = [
# Graphical
"logo.nologo" # Remove kernel logo during boot
@@ -76,7 +77,7 @@ in
experimental-features = nix-command flakes
'';
settings = {
sandbox = false;
sandbox = false;
};
};
+1 -3
View File
@@ -7,13 +7,11 @@
, git
, cacert
, fetchFromGitHub
, commitHash
}:
stdenv.mkDerivation rec {
pname = "qitech-control-electron";
version = commitHash;
version = "1.0.0";
src = lib.cleanSource ../../electron;
nativeBuildInputs = [
+22 -50
View File
@@ -3,64 +3,36 @@
, pkg-config
, libudev-zero
, libpcap
, commitHash
, craneLib
}:
let
# Use crane's source cleaning which is more intelligent for Cargo projects
# Bind variables so they can be inherited inside inner calls
pname = "server";
version = "1.0.0";
strictDeps = true;
nativeBuildInputs = [ pkg-config ];
buildInputs = [ libpcap libudev-zero ];
# Cleaned source
src = craneLib.cleanCargoSource ../..;
# Common arguments for both dependency and app builds
commonArgs = {
inherit src;
strictDeps = true;
nativeBuildInputs = [ pkg-config ];
buildInputs = [ libpcap libudev-zero ];
# Build only the server package with journald logging for NixOS
pname = "server";
version = commitHash;
# Reduce memory usage during build
CARGO_BUILD_JOBS = "2";
};
# Build *just* the cargo dependencies (of the entire workspace),
# so we can reuse all of that work when running in CI
cargoArtifacts = craneLib.buildDepsOnly commonArgs;
# Safe fallback for CARGO_BUILD_JOBS in impure builds
cargoJobs = if (builtins.tryEval (builtins.getEnv "CARGO_BUILD_JOBS")).success
then builtins.getEnv "CARGO_BUILD_JOBS"
else "2";
in
# Uses Rust 1.86 stable from nixpkgs 25.05 with Crane for dependency caching
craneLib.buildPackage (commonArgs // {
inherit cargoArtifacts;
# Enable journald logging feature for NixOS systems and build only server package
# Anbale io_uring support
cargoExtraArgs = "-p server --features tracing-journald,io-uring --no-default-features";
# Create a swap file if building on a memory-constrained system
preBuild = ''
if [ $(free -m | grep Mem | awk '{print $2}') -lt 6000 ]; then
mkdir -p $TMPDIR/swap
dd if=/dev/zero of=$TMPDIR/swap/swapfile bs=1M count=8192
chmod 600 $TMPDIR/swap/swapfile
mkswap $TMPDIR/swap/swapfile
swapon $TMPDIR/swap/swapfile
fi
'';
craneLib.buildPackage {
inherit pname version src strictDeps nativeBuildInputs buildInputs;
postBuild = ''
if [ -f $TMPDIR/swap/swapfile ]; then
swapoff $TMPDIR/swap/swapfile
rm $TMPDIR/swap/swapfile
fi
'';
# Use the safe cargoJobs variable
CARGO_BUILD_JOBS = cargoJobs;
meta = with lib; {
description = "QiTech Control Server";
homepage = "https://qitech.de";
platforms = platforms.linux;
# Build cargo dependencies once for caching
cargoArtifacts = craneLib.buildDepsOnly {
inherit src strictDeps nativeBuildInputs buildInputs pname version;
};
})
cargoExtraArgs = "-p server --features tracing-journald,io-uring --no-default-features";
}
+1 -1
View File
@@ -9,7 +9,7 @@ use crate::{
ethercat::config::{MAX_FRAMES, MAX_PDU_DATA, MAX_SUBDEVICES, PDI_LEN},
};
use control_core::ethercat::eeprom_identification::read_device_identifications;
#[cfg(unix)]
#[cfg(target_os = "linux")]
use control_core::irq_handling::set_irq_affinity;
use control_core::machines::identification::{
DeviceHardwareIdentification, DeviceHardwareIdentificationEthercat, DeviceIdentification,
+2 -3
View File
@@ -186,8 +186,8 @@ impl ExtruderV2 {
return;
}
};
let mut status = self.screw_speed_controller.get_inverter_status();
let new_status_hash = hash_with_serde_model(&mut status);
let status = self.screw_speed_controller.get_inverter_status();
let new_status_hash = hash_with_serde_model(status);
let should_emit = new_status_hash != old_status_hash;
if should_emit {
self.emit_state();
@@ -325,7 +325,6 @@ impl ExtruderV2 {
self.temperature_controller_front.disable();
self.temperature_controller_middle.disable();
self.temperature_controller_nozzle.disable();
self.emit_state();
}
fn enable_heating(&mut self) {
+2
View File
@@ -21,6 +21,8 @@ use tracing::instrument;
pub struct LiveValuesEvent {
/// diameter measurement in mm
pub diameter: f64,
pub x_value: Option<f64>,
pub y_value: Option<f64>,
}
impl LiveValuesEvent {
+19 -9
View File
@@ -42,16 +42,26 @@ impl LaserMachine {
impl LaserMachine {
///diameter in mm
pub fn emit_live_values(&mut self) {
let diameter = smol::block_on(async {
self.laser
.read()
.await
.get_data()
.await
.map(|laser_data| laser_data.diameter.get::<millimeter>())
});
let laser_data = smol::block_on(async { self.laser.read().await.get_data().await });
let diameter = laser_data
.as_ref()
.map(|data| data.diameter.get::<millimeter>())
.unwrap_or(0.0);
let x_value = laser_data
.as_ref()
.and_then(|data| data.x_axis.as_ref())
.map(|x| x.get::<millimeter>());
let y_value = laser_data
.as_ref()
.and_then(|data| data.y_axis.as_ref())
.map(|y| y.get::<millimeter>());
let live_values = LiveValuesEvent {
diameter: diameter.unwrap_or(0.0),
diameter,
x_value,
y_value,
};
self.namespace
.emit(LaserEvents::LiveValues(live_values.build()));
+24 -10
View File
@@ -456,7 +456,6 @@ impl Winder2 {
// Update the internal state
self.spool_mode = mode;
self.emit_state();
}
/// Apply the mode changes to the spool
@@ -577,7 +576,6 @@ impl Winder2 {
// Update the internal state
self.puller_mode = mode;
self.emit_state();
}
}
@@ -608,10 +606,12 @@ impl Winder2 {
pub fn set_spool_automatic_required_meters(&mut self, meters: f64) {
self.spool_automatic_action.target_length = Length::new::<meter>(meters);
self.emit_state();
}
pub const fn set_spool_automatic_mode(&mut self, mode: SpoolAutomaticActionMode) {
pub fn set_spool_automatic_mode(&mut self, mode: SpoolAutomaticActionMode) {
self.spool_automatic_action.mode = mode;
self.emit_state();
}
pub fn stop_or_pull_spool(&mut self, now: Instant) {
@@ -684,9 +684,10 @@ impl Winder2 {
let _ = self.puller.set_speed(steps_per_second);
}
pub const fn puller_set_regulation(&mut self, puller_regulation_mode: PullerRegulationMode) {
pub fn puller_set_regulation(&mut self, puller_regulation_mode: PullerRegulationMode) {
self.puller_speed_controller
.set_regulation_mode(puller_regulation_mode);
self.emit_state();
}
/// Set target speed in m/min
@@ -694,6 +695,7 @@ impl Winder2 {
// Convert m/min to velocity
let target_speed = Velocity::new::<meter_per_minute>(target_speed);
self.puller_speed_controller.set_target_speed(target_speed);
self.emit_state();
}
/// Set target diameter in mm
@@ -702,11 +704,13 @@ impl Winder2 {
let target_diameter = Length::new::<millimeter>(target_diameter);
self.puller_speed_controller
.set_target_diameter(target_diameter);
self.emit_state();
}
/// Set forward direction
pub const fn puller_set_forward(&mut self, forward: bool) {
pub fn puller_set_forward(&mut self, forward: bool) {
self.puller_speed_controller.set_forward(forward);
self.emit_state();
}
// Spool Speed Controller API methods
@@ -715,6 +719,7 @@ impl Winder2 {
regulation_mode: spool_speed_controller::SpoolSpeedControllerType,
) {
self.spool_speed_controller.set_type(regulation_mode);
self.emit_state();
}
/// Set minimum speed for minmax mode in RPM
@@ -723,6 +728,7 @@ impl Winder2 {
if let Err(e) = self.spool_speed_controller.set_minmax_min_speed(min_speed) {
tracing::error!("Failed to set spool min speed: {:?}", e);
}
self.emit_state();
}
/// Set maximum speed for minmax mode in RPM
@@ -731,39 +737,45 @@ impl Winder2 {
if let Err(e) = self.spool_speed_controller.set_minmax_max_speed(max_speed) {
tracing::error!("Failed to set spool max speed: {:?}", e);
}
self.emit_state();
}
/// Set tension target for adaptive mode (0.0-1.0)
pub const fn spool_set_adaptive_tension_target(&mut self, tension_target: f64) {
pub fn spool_set_adaptive_tension_target(&mut self, tension_target: f64) {
self.spool_speed_controller
.set_adaptive_tension_target(tension_target);
self.emit_state();
}
/// Set radius learning rate for adaptive mode
pub const fn spool_set_adaptive_radius_learning_rate(&mut self, radius_learning_rate: f64) {
pub fn spool_set_adaptive_radius_learning_rate(&mut self, radius_learning_rate: f64) {
self.spool_speed_controller
.set_adaptive_radius_learning_rate(radius_learning_rate);
self.emit_state();
}
/// Set max speed multiplier for adaptive mode
pub const fn spool_set_adaptive_max_speed_multiplier(&mut self, max_speed_multiplier: f64) {
pub fn spool_set_adaptive_max_speed_multiplier(&mut self, max_speed_multiplier: f64) {
self.spool_speed_controller
.set_adaptive_max_speed_multiplier(max_speed_multiplier);
self.emit_state();
}
/// Set acceleration factor for adaptive mode
pub const fn spool_set_adaptive_acceleration_factor(&mut self, acceleration_factor: f64) {
pub fn spool_set_adaptive_acceleration_factor(&mut self, acceleration_factor: f64) {
self.spool_speed_controller
.set_adaptive_acceleration_factor(acceleration_factor);
self.emit_state();
}
/// Set deacceleration urgency multiplier for adaptive mode
pub const fn spool_set_adaptive_deacceleration_urgency_multiplier(
pub fn spool_set_adaptive_deacceleration_urgency_multiplier(
&mut self,
deacceleration_urgency_multiplier: f64,
) {
self.spool_speed_controller
.set_adaptive_deacceleration_urgency_multiplier(deacceleration_urgency_multiplier);
self.emit_state();
}
}
@@ -804,6 +816,7 @@ impl Winder2 {
machine_identification_unique,
machine,
});
self.emit_state();
self.reverse_connect();
}
@@ -832,6 +845,7 @@ impl Winder2 {
}
}
self.connected_buffer = None;
self.emit_state();
}
/// initiate connection from buffer to winder
+2 -2
View File
@@ -185,7 +185,7 @@ impl MachineNewTrait for Winder2 {
..Default::default()
},
stm_motor: StmMotorConfiguration {
max_current: 6000,
max_current: 2800,
..Default::default()
},
..Default::default()
@@ -309,7 +309,7 @@ impl MachineNewTrait for Winder2 {
..Default::default()
},
stm_motor: StmMotorConfiguration {
max_current: 1500,
max_current: 2700,
..Default::default()
},
pdo_assignment: EL7031_0030PredefinedPdoAssignment::VelocityControlCompact,
-13
View File
@@ -45,7 +45,6 @@ pub mod jemalloc_stats;
fn main() {
// Initialize panic handling
let thread_panic_tx = init_panic();
logging::init_tracing();
tracing::info!("Tracing initialized successfully");
@@ -127,15 +126,3 @@ fn init_dhat_heap_profiling() {
}
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_main() {
// setup logging
logging::init_tracing();
::tracing::info!("Running main test");
}
}
+36
View File
@@ -42,6 +42,8 @@ enum LaserModbusRequsts {
struct LaserDiameterResponse {
pub diameter: Length,
pub x_axis: Option<Length>,
pub y_axis: Option<Length>,
}
impl TryFrom<ModbusResponse> for LaserDiameterResponse {
@@ -55,8 +57,22 @@ impl TryFrom<ModbusResponse> for LaserDiameterResponse {
));
}
let diameter = u16::from_be_bytes([value.data[1], value.data[2]]) as f64 / 1000.0;
// Depending on if its a 2 axis Laser we get more values out of the data
let (x_axis, y_axis) = if value.data.len() >= 7 {
let x = u16::from_be_bytes([value.data[3], value.data[4]]) as f64 / 1000.0;
let y = u16::from_be_bytes([value.data[5], value.data[6]]) as f64 / 1000.0;
(
Some(Length::new::<uom::si::length::millimeter>(x)),
Some(Length::new::<uom::si::length::millimeter>(y)),
)
} else {
(None, None)
};
Ok(Self {
diameter: Length::new::<uom::si::length::millimeter>(diameter),
x_axis,
y_axis,
})
}
}
@@ -79,6 +95,8 @@ impl SerialDeviceNew for Laser {
) -> Result<(DeviceIdentification, Arc<RwLock<Self>>), anyhow::Error> {
let laser_data = Some(LaserData {
diameter: Length::new::<uom::si::length::millimeter>(0.0),
x_axis: None,
y_axis: None,
last_timestamp: Instant::now(),
});
let hash = hash_djb2(params.path.as_bytes());
@@ -138,6 +156,8 @@ impl SerialDeviceNew for Laser {
#[derive(Debug, Clone)]
pub struct LaserData {
pub diameter: Length,
pub x_axis: Option<Length>,
pub y_axis: Option<Length>,
pub last_timestamp: Instant,
}
@@ -149,6 +169,20 @@ impl Laser {
}
}
pub async fn get_x(&self) -> Result<Option<Length>, String> {
match &self.data {
Some(data) => Ok(data.x_axis),
None => Err("No data from Laser".to_string()),
}
}
pub async fn get_y(&self) -> Result<Option<Length>, String> {
match &self.data {
Some(data) => Ok(data.y_axis),
None => Err("No data from Laser".to_string()),
}
}
pub async fn get_data(&self) -> Option<LaserData> {
self.data.clone()
}
@@ -204,6 +238,8 @@ impl Laser {
let mut self_guard = _self.write().await;
self_guard.data = Some(LaserData {
diameter: diameter_response.diameter,
x_axis: diameter_response.x_axis,
y_axis: diameter_response.y_axis,
last_timestamp: Instant::now(),
});
}