Compare commits
27
Commits
No files matched your search
+43
-5
@@ -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
@@ -596,6 +596,7 @@ dependencies = [
|
||||
"smol",
|
||||
"socketioxide",
|
||||
"textplots",
|
||||
"tokio",
|
||||
"tracing",
|
||||
"uom",
|
||||
]
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
Executable
+43
@@ -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
|
||||
#
|
||||
@@ -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"]
|
||||
@@ -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;
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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(())
|
||||
}
|
||||
|
||||
|
||||
@@ -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>,
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
};
|
||||
|
||||
Executable
+30
@@ -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
|
||||
@@ -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;
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -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
@@ -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";
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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()));
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
@@ -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(),
|
||||
});
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user