Compare commits

...
Author SHA1 Message Date
93094049+kraemr@users.noreply.github.com 7b4a3b52b3 fixed target_os flags for set_irq_affinity 2025-09-16 15:30:16 +02:00
Robin Krämer 310fd99730 Merge pull request #711 from qitechgmbh/694-over-time-nixos-generations-takes-up-all-diskspace-even-when-deleted
added collect-garbage on each delete
2025-09-15 19:38:31 +02:00
93094049+kraemr@users.noreply.github.com 26473f5596 collect-garbage does not exist, replaced with nix store gc 2025-09-15 19:36:46 +02:00
93094049+kraemr@users.noreply.github.com ead8a02b6a added collect-garbage on each delete 2025-09-15 19:36:46 +02:00
Robin Krämer b2ca9cf792 Merge pull request #712 from qitechgmbh/708-refactor-nix-config-so-that-no-dynamic-installinfonix-is-created
Read git information during build instead of addign new commit via installInfo.nix
2025-09-15 18:29:19 +02:00
nixos-install.sh 301c254446 Pass git info into gitInfo.nix via env vars 2025-09-15 17:57:43 +02:00
Robin Krämer 769091d271 Merge pull request #720 from qitechgmbh/714-pressing-the-same-button-twice-triggers-isloading
714 pressing the same button twice triggers isloading
2025-09-15 16:55:05 +02:00
93094049+kraemr@users.noreply.github.com 1bc7907ba0 all machines now use old emit_state again, but extruder uses custom maybe_emit_state_hash
fixed compile error after merge conflict
2025-09-15 16:49:47 +02:00
93094049+kraemr@users.noreply.github.com 8260c606dc removed maybe_emit_state from every machine except extruder 2025-09-15 16:48:09 +02:00
Christian c0b7e563c5 Merge pull request #713 from qitechgmbh/596-extend-total-power-output-tile-2
596 extend total power output tile
2025-09-15 16:10:45 +02:00
Melvin Spieß 9062e15712 Merge pull request #727 from qitechgmbh/722-remove-estimated-diameter-value
722 remove estimated diameter value
2025-09-15 15:34:57 +02:00
snacj 134da6fad3 fixed formatting 2025-09-15 15:25:31 +02:00
snacj 4cec6ef711 Changed Radius to Factor 2025-09-15 15:13:09 +02:00
snacj 36d64c85fb Removed Estimated Diameter Value 2025-09-15 15:06:30 +02:00
Robin Krämer 8e00019023 Merge pull request #721 from qitechgmbh/as-any-mut
Add `Machin::as_any_mut`
2025-09-15 13:44:51 +02:00
Icedsoda 575d4d2d26 Add Machin::as_any_mut 2025-09-15 13:26:47 +02:00
Robin Krämer 19e9ebc20f Merge pull request #719 from qitechgmbh/718-ethercat-timeout-on-setup
set wait_loop_delay to zero
2025-09-15 09:34:00 +02:00
93094049+kraemr@users.noreply.github.com dd4476b786 set wait_loop_delay to zero 2025-09-15 09:32:20 +02:00
Christian fc283a64a7 Merge branch 'master' into 596-extend-total-power-output-tile-2 2025-09-13 17:05:56 +02:00
Robin Krämer b6dc1d35c6 Merge pull request #710 from qitechgmbh/707-make-adaptive-mode-for-winder-default
changed MinMax to Adaptive by default
2025-09-11 15:27:20 +02:00
93094049+kraemr@users.noreply.github.com 4f542510d6 changed MinMax to Adaptive by default 2025-09-11 15:23:08 +02:00
Oshgnacknak 0ca334decc Disable hardening in nix develop shell (#705)
Hardening causes jmalloc to fail at compilation
2025-09-11 12:57:34 +02:00
Robin Krämer 239a926301 Merge pull request #706 from qitechgmbh/703-fix-permissions-for-set_irq_affinity
703 fix permissions for set irq affinity
2025-09-11 11:01:27 +02:00
93094049+kraemr@users.noreply.github.com 9f4c7fbedd fixed permissions 2025-09-11 10:59:21 +02:00
93094049+kraemr@users.noreply.github.com 6a9de48bb7 added extra logging 2025-09-11 10:59:21 +02:00
93094049+kraemr@users.noreply.github.com d9fc22399a added DAC_OVERRIDE CAP 2025-09-11 10:59:21 +02:00
Robin Krämer f41b0dc0c2 Merge pull request #697 from qitechgmbh/dependabot/npm_and_yarn/electron/vite-6.3.6
Bump vite from 6.3.5 to 6.3.6 in /electron
2025-09-10 15:58:37 +02:00
dependabot[bot] 14233c0aa1 Bump vite from 6.3.5 to 6.3.6 in /electron
Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 6.3.5 to 6.3.6.
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/v6.3.6/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v6.3.6/packages/vite)

---
updated-dependencies:
- dependency-name: vite
  dependency-version: 6.3.6
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-09-10 13:48:41 +00:00
Robin Krämer 58e56c4226 Merge pull request #702 from qitechgmbh/701-fix-optimistic-state-handling
701 fix optimistic state handling
2025-09-10 15:01:24 +02:00
93094049+kraemr@users.noreply.github.com 8ee184475e only return state not optimisticstate 2025-09-10 14:56:05 +02:00
93094049+kraemr@users.noreply.github.com be9c5b1be5 fixed optimistic state handling 2025-09-10 14:55:08 +02:00
Robin Krämer 8411f8f2b9 Merge pull request #698 from qitechgmbh/696-pin-ethernet-interrupt-handler-to-the-same-core-as-ethercrab-task
696 pin ethernet interrupt handler to the same core as ethercrab task
2025-09-10 14:19:59 +02:00
93094049+kraemr@users.noreply.github.com 5441a7a126 changed lint, renamed irq_handling funcs, refactor 2025-09-10 14:08:32 +02:00
93094049+kraemr@users.noreply.github.com b2425de273 fixed non unix compilation 2025-09-10 13:55:59 +02:00
93094049+kraemr@users.noreply.github.com 82d5e73129 removed unused import 2025-09-10 13:45:58 +02:00
93094049+kraemr@users.noreply.github.com 31189ebdef refactored irq_handling into own file 2025-09-10 13:45:58 +02:00
93094049+kraemr@users.noreply.github.com ee1720fa95 added code to set irq handler affinity 2025-09-10 13:45:58 +02:00
Robin Krämer 72dc0634f3 Merge pull request #693 from qitechgmbh/clippy-fix
Use Cargo Clippy
2025-09-10 13:43:51 +02:00
Christian df0f86ce7e feat(extruder): Add total energy consumption tracking and related metrics 2025-08-28 16:55:26 +02:00
46 changed files with 509 additions and 464 deletions

No files matched your search

+1
View File
@@ -17,6 +17,7 @@ resolver = "2"
collapsible_if = "allow"
# Lint groups (priority -1)
correctness = { level = "warn", priority = -1 }
suspicious = { level = "warn", priority = -1 }
complexity = { level = "warn", priority = -1 }
perf = { level = "warn", priority = -1 }
+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:$USER" /dev/ttyUSB*
sudo chown "root:$(groups | cut -f1 -d' ')" /dev/ttyUSB*
fi
# run
+29
View File
@@ -29,3 +29,32 @@ fn build_event_derive2(item: TokenStream2) -> Result<TokenStream2, Error> {
Ok(expanded)
}
#[proc_macro_derive(Machine)]
pub fn machine_derive(item: TokenStream) -> TokenStream {
machine_derive2(item.into()).unwrap().into()
}
fn machine_derive2(item: TokenStream2) -> Result<TokenStream2, Error> {
// Parse the input tokens into a syntax tree
let ast: DeriveInput = syn::parse2(item)?;
let ident = &ast.ident;
let (impl_generics, ty_generics, where_clause) = ast.generics.split_for_impl();
let expanded = quote! {
impl #impl_generics control_core::machines::Machine for #ident #ty_generics #where_clause {}
impl #impl_generics control_core::machines::AnyGetters for #ident #ty_generics #where_clause {
fn as_any(&self) -> &dyn std::any::Any {
self
}
fn as_any_mut(&mut self) -> &mut dyn std::any::Any {
self
}
}
};
Ok(expanded)
}
@@ -227,7 +227,7 @@ pub async fn write_machine_device_identification<'maindevice, const MAX_PDI: usi
/// Returns the EEPROM addresses for the machine device identification
/// based on the subdevice's identity
pub fn get_identification_addresses<'maindevice>(
pub fn get_identification_addresses(
subdevice_identity: &SubDeviceIdentity,
subdevice_name: &str,
) -> Result<MachineIdentificationAddresses, Error> {
+2 -25
View File
@@ -329,32 +329,9 @@ impl<H: Hasher> Serializer for HashSerializer<'_, H> {
}
// A helper that uses Serialize to feed into a Hasher without allocating a buffer:
pub fn hash_with_serde_model<T: Serialize, H: Hasher>(value: T, hasher: H) -> u64 {
let mut h = hasher;
pub fn hash_with_serde_model<T: Serialize>(value: T) -> u64 {
let mut h = DefaultHasher::new();
let ser = HashSerializer(&mut h);
value.serialize(ser).expect("serialize to hasher failed");
h.finish()
}
/// if hash of a and b are different this function returns true
/// a and b need to impl Serialize
pub fn check_hash_different<T: Serialize>(a: T, b: T) -> bool {
// Yes DefaultHasher has to be defined multiple times
// If you use one for both the results will never match even if they are the same value
let hasher = DefaultHasher::new();
let hash = hash_with_serde_model(a, hasher);
let hasher = DefaultHasher::new();
let hash_old = hash_with_serde_model(b, hasher);
hash != hash_old
}
#[test]
fn test_same_values() {
let a = 10;
let b = 10;
assert_eq!(
check_hash_different(a, b),
false,
"Hashes should be equal for identical values"
);
}
+6 -24
View File
@@ -45,22 +45,6 @@ impl DivideByCount for u64 {
}
}
/// A generic moving time window that works with numeric types.
///
/// # Examples
///
/// ```rust
/// use std::time::{Duration, Instant};
/// use control_core::helpers::moving_time_window::MovingTimeWindow;
///
/// let mut window = MovingTimeWindow::new(Duration::from_secs(10), 100);
/// window.update(1.5, Instant::now());
/// window.update(2.5, Instant::now());
///
/// let average = window.average();
/// println!("Average: {}", average);
/// ```
// Type aliases for common use cases
pub type F64Window = MovingTimeWindow<f64>;
pub type F32Window = MovingTimeWindow<f32>;
@@ -194,15 +178,14 @@ where
.samples
.iter()
.map(|(v, _)| *v)
.fold(None, |acc, val| match acc {
None => Some(val),
Some(current_max) => {
.fold(None, |acc, val| {
acc.map_or(Some(val), |current_max| {
if val > current_max {
Some(val)
} else {
Some(current_max)
}
}
})
})
.unwrap_or_default();
@@ -229,15 +212,14 @@ where
.samples
.iter()
.map(|(v, _)| *v)
.fold(None, |acc, val| match acc {
None => Some(val),
Some(current_min) => {
.fold(None, |acc, val| {
acc.map_or(Some(val), |current_min| {
if val < current_min {
Some(val)
} else {
Some(current_min)
}
}
})
})
.unwrap_or_default();
+113
View File
@@ -0,0 +1,113 @@
#[cfg(target_os = "linux")]
use libc;
use std::ffi::CString;
use std::io;
use std::str;
#[cfg(target_os = "linux")]
fn read_proc_interrupts() -> Result<String, io::Error> {
let path = CString::new("/proc/interrupts").unwrap();
// O_RDONLY
let fd = unsafe { libc::open(path.as_ptr(), libc::O_RDONLY) };
if fd < 0 {
return Err(io::Error::from_raw_os_error(-fd));
}
let mut buf: Vec<u8> = Vec::new();
let mut chunk = [0u8; 8192];
loop {
let n = unsafe { libc::read(fd, chunk.as_mut_ptr() as *mut _, chunk.len()) };
if n < 0 {
let e = io::Error::from_raw_os_error(-n as i32);
unsafe { libc::close(fd) };
return Err(e);
} else if n == 0 {
break;
} else {
buf.extend_from_slice(&chunk[..n as usize]);
}
}
unsafe { libc::close(fd) };
Ok(String::from_utf8_lossy(&buf).into_owned())
}
#[cfg(target_os = "linux")]
fn get_interface_irq(proc_content: &str, interface_name: &str) -> Option<u32> {
for line in proc_content.lines() {
if line.contains(interface_name) {
if let Some(colon_pos) = line.find(':') {
let head = &line[..colon_pos].trim();
// try parse head as integer IRQ number
if let Ok(irq) = head.parse::<u32>() {
return Some(irq);
}
}
}
}
None
}
/// Since kernel 3.0 it’s possible to use the /proc/irq/<IRQ-NUMBER>/smp_affinity_list
/// With comma seperated values
/// This function takes the irq identifier and writes the cpu string
/// into /proc/irq/irq_number/smp_affinity_list
#[cfg(target_os = "linux")]
fn set_irq_affinity_raw(irq: u32, cpu: &str) -> Result<(), io::Error> {
let path: String = format!("/proc/irq/{}/smp_affinity_list", irq);
let cpath = CString::new(path.clone()).unwrap();
// We want to completely overwrite the smp affinities, so that we guarentee execution on the specified cores
let file_descriptor = unsafe { libc::open(cpath.as_ptr(), libc::O_WRONLY) };
if file_descriptor < 0 {
tracing::info!("Permission denied to write: {}", path);
return Err(io::Error::new(
io::ErrorKind::PermissionDenied,
format!("failed to open {}: errno {}", path, -file_descriptor),
));
}
// convert to "raw bytes"
// cpu needs to be a string because we need the raw ascii code 48 = '0' and so on as bytes
let bytes = cpu.as_bytes();
let mut bytes_written_total = 0usize;
while bytes_written_total < bytes.len() {
let to_write = &bytes[bytes_written_total..];
let bytes_written = unsafe {
libc::write(
file_descriptor,
to_write.as_ptr() as *const _,
to_write.len(),
)
};
if bytes_written < 0 {
let e = io::Error::from_raw_os_error(-bytes_written as i32);
unsafe { libc::close(file_descriptor) };
return Err(e);
}
bytes_written_total += bytes_written as usize;
}
unsafe { libc::close(file_descriptor) };
Ok(())
}
/// This is used to make sure that a given irq handler runs on the expected cpu
/// On Realtime systems this is the make or break fix for certain usecases like ethercrab
/// On our machines the 99.99th percentile of cycle times was very bad ~5-11 ms while ethercat is supposed to be in the microseconds
/// After pinning the ethernet irq to the core that the ethercat code ran on 99.99th percentile went down to ~200us (microseconds)
/// Example input: irq_name: "eno1" , cpu: 2
/// Remember that cpu cores are counted from 0
#[cfg(target_os = "linux")]
pub fn set_irq_affinity(irq_name: &str, cpu: u32) -> Result<(), anyhow::Error> {
let proc_contents = read_proc_interrupts()?;
let irq = get_interface_irq(&proc_contents, irq_name);
if irq.is_none() {
return Err(anyhow::anyhow!("Couldnt find irq number!"));
}
let res = set_irq_affinity_raw(irq.unwrap(), &cpu.to_string());
if res.is_err() {
return Err(anyhow::anyhow!("Couldnt write affinity list!"));
} else {
return Ok(());
}
}
+1
View File
@@ -2,6 +2,7 @@ pub mod controllers;
pub mod converters;
pub mod ethercat;
pub mod helpers;
pub mod irq_handling;
pub mod machines;
pub mod modbus;
pub mod realtime;
+4 -1
View File
@@ -16,9 +16,12 @@ pub mod manager_iter;
pub mod new;
pub mod registry;
pub trait Machine: MachineAct + MachineNewTrait + MachineApi + Any + Debug + Send {
pub trait Machine: MachineAct + MachineNewTrait + MachineApi + AnyGetters + Debug + Send {}
pub trait AnyGetters: Any {
fn as_any(&self) -> &dyn Any;
fn as_any_mut(&mut self) -> &mut dyn Any;
}
/// Casts a `dyn Machine` to a specific machine type
pub async fn downcast_machine<T: Machine>(
machine: Arc<Mutex<dyn Machine>>,
+5 -4
View File
@@ -95,7 +95,7 @@
"ts-node": "^10.9.2",
"typescript": "^5.7.3",
"typescript-eslint": "^8.24.1",
"vite": "^6.2.6",
"vite": "^6.3.6",
"vitest": "^3.0.6"
}
},
@@ -14552,9 +14552,10 @@
}
},
"node_modules/vite": {
"version": "6.3.5",
"resolved": "https://registry.npmjs.org/vite/-/vite-6.3.5.tgz",
"integrity": "sha512-cZn6NDFE7wdTpINgs++ZJ4N49W2vRp8LCKrn3Ob1kYNtOo21vfDoaV5GzBfLU4MovSAB8uNRm4jgzVQZ+mBzPQ==",
"version": "6.3.6",
"resolved": "https://registry.npmjs.org/vite/-/vite-6.3.6.tgz",
"integrity": "sha512-0msEVHJEScQbhkbVTb/4iHZdJ6SXp/AvxL2sjwYQFfBqleHtnCqv1J3sa9zbWz/6kW1m9Tfzn92vW+kZ1WV6QA==",
"license": "MIT",
"dependencies": {
"esbuild": "^0.25.0",
"fdir": "^6.4.4",
+1 -1
View File
@@ -60,7 +60,7 @@
"ts-node": "^10.9.2",
"typescript": "^5.7.3",
"typescript-eslint": "^8.24.1",
"vite": "^6.2.6",
"vite": "^6.3.6",
"vitest": "^3.0.6"
},
"dependencies": {
+7
View File
@@ -31,6 +31,8 @@ export function getUnitIcon(unit: Unit): IconName {
return "lu:Zap";
case "A":
return "lu:Zap";
case "kWh":
return "lu:BatteryFull";
default:
return "lu:ChartNoAxesColumn";
}
@@ -64,6 +66,8 @@ export function renderUnitSymbol(unit: Unit | undefined): string {
return "V";
case "A":
return "A";
case "kWh":
return "kWh";
default:
return "";
}
@@ -119,6 +123,8 @@ export function renderUnitSymbolLong(unit: Unit): string {
return "volts";
case "A":
return "amperes";
case "kWh":
return "kilowatt-hours";
default:
return "";
}
@@ -138,6 +144,7 @@ export const units = [
"W",
"V",
"A",
"kWh",
] as const;
export type Unit = (typeof units)[number];
@@ -145,7 +145,7 @@ async function deleteNixOSGeneration(generationId: string): Promise<{
const process = spawn("sudo", [
"sh",
"-c",
`nix-env --delete-generations ${generationId} -p /nix/var/nix/profiles/system && /nix/var/nix/profiles/system/bin/switch-to-configuration boot`,
`nix-env --delete-generations ${generationId} -p /nix/var/nix/profiles/system && nix store gc && /nix/var/nix/profiles/system/bin/switch-to-configuration boot`,
]);
let stderr = "";
@@ -50,12 +50,11 @@ export function useBuffer1() {
// Single optimistic state for all state management
const stateOptimistic = useStateOptimistic<StateEvent>();
// Update optimistic state when real state changes
useEffect(() => {
if (state && !stateOptimistic.isOptimistic) {
if (state) {
stateOptimistic.setReal(state);
}
}, [state, stateOptimistic]);
}, [state]);
// Helper function for optimistic updates using produce
const updateStateOptimistically = (
@@ -63,7 +62,7 @@ export function useBuffer1() {
serverRequest: () => void,
) => {
const currentState = stateOptimistic.value;
if (currentState) {
if (currentState && !stateOptimistic.isOptimistic) {
stateOptimistic.setOptimistic(produce(currentState, producer));
}
serverRequest();
@@ -30,6 +30,8 @@ export function Extruder2ControlPage() {
motorScrewRpm,
motorPower,
motorCurrent,
totalEnergyKWh,
combinedPower,
setExtruderMode,
@@ -224,13 +226,31 @@ export function Extruder2ControlPage() {
/>
</ControlCard>
<ControlCard className="bg-blue" title="Total Power Consumption">
<ControlCard className="bg-blue" title="Power Consumption">
<TimeSeriesValueNumeric
label=""
label="Total Power"
unit="W"
renderValue={(value) => roundToDecimals(value, 1)}
renderValue={(value) => roundToDecimals(value, 0)}
timeseries={combinedPower}
/>
<TimeSeriesValueNumeric
label="Motor Power"
unit="W"
renderValue={(value) => roundToDecimals(value, 0)}
timeseries={motorPower}
/>
<TimeSeriesValueNumeric
label="Motor Current"
unit="A"
renderValue={(value) => roundToDecimals(value, 1)}
timeseries={motorCurrent}
/>
<TimeSeriesValueNumeric
label="Total Energy Consumption"
unit="kWh"
renderValue={(value) => roundToDecimals(value, 3)}
timeseries={totalEnergyKWh}
/>
</ControlCard>
</ControlGrid>
</Page>
@@ -43,6 +43,8 @@ export const liveValuesEventDataSchema = z.object({
front_power: z.number(),
back_power: z.number(),
middle_power: z.number(),
combined_power: z.number(),
total_energy_kwh: z.number(),
});
/**
@@ -195,8 +197,9 @@ export type Extruder2NamespaceStore = {
middlePower: TimeSeries;
backPower: TimeSeries;
// Combined power consumption
// Combined power consumption and energy
combinedPower: TimeSeries;
totalEnergyKWh: TimeSeries;
};
// Constants for time durations
@@ -205,13 +208,6 @@ const ONE_SECOND = 1000;
const FIVE_SECOND = 5 * ONE_SECOND;
const ONE_HOUR = 60 * 60 * ONE_SECOND;
const { initialTimeSeries: screwRpm, insert: addScrewRpm } = createTimeSeries(
TWENTY_MILLISECOND,
ONE_SECOND,
FIVE_SECOND,
ONE_HOUR,
);
const { initialTimeSeries: pressure, insert: addPressure } = createTimeSeries(
TWENTY_MILLISECOND,
ONE_SECOND,
@@ -250,6 +246,9 @@ const { initialTimeSeries: backPower, insert: addBackPower } = createTimeSeries(
const { initialTimeSeries: combinedPower, insert: addCombinedPower } =
createTimeSeries(TWENTY_MILLISECOND, ONE_SECOND, FIVE_SECOND, ONE_HOUR);
const { initialTimeSeries: totalEnergyKWh, insert: addTotalEnergyKWh } =
createTimeSeries(TWENTY_MILLISECOND, ONE_SECOND, FIVE_SECOND, ONE_HOUR);
const { initialTimeSeries: motorCurrent, insert: addMotorCurrent } =
createTimeSeries(TWENTY_MILLISECOND, ONE_SECOND, FIVE_SECOND, ONE_HOUR);
@@ -346,12 +345,11 @@ export function extruder2MessageHandler(
timestamp,
}),
combinedPower: addCombinedPower(state.combinedPower, {
value:
liveValuesEvent.data.motor_status.power +
liveValuesEvent.data.nozzle_power +
liveValuesEvent.data.front_power +
liveValuesEvent.data.middle_power +
liveValuesEvent.data.back_power,
value: liveValuesEvent.data.combined_power,
timestamp,
}),
totalEnergyKWh: addTotalEnergyKWh(state.totalEnergyKWh, {
value: liveValuesEvent.data.total_energy_kwh,
timestamp,
}),
}));
@@ -387,6 +385,7 @@ export const createExtruder2NamespaceStore =
backPower,
middlePower,
combinedPower,
totalEnergyKWh,
};
});
@@ -56,17 +56,17 @@ export function useExtruder2() {
middlePower,
backPower,
combinedPower,
totalEnergyKWh,
} = useExtruder2Namespace(machineIdentification);
// Single optimistic state for all state management
const stateOptimistic = useStateOptimistic<StateEvent>();
// Update optimistic state when real state changes
useEffect(() => {
if (state && !stateOptimistic.isOptimistic) {
if (state) {
stateOptimistic.setReal(state);
}
}, [state, stateOptimistic]);
}, [state]);
// Helper function for optimistic updates using produce
const updateStateOptimistically = (
@@ -74,7 +74,7 @@ export function useExtruder2() {
serverRequest: () => void,
) => {
const currentState = stateOptimistic.value;
if (currentState) {
if (currentState && !stateOptimistic.isOptimistic) {
stateOptimistic.setOptimistic(produce(currentState, producer));
}
serverRequest();
@@ -383,6 +383,7 @@ export function useExtruder2() {
middlePower,
backPower,
combinedPower,
totalEnergyKWh,
// Loading states
isLoading: stateOptimistic.isOptimistic,
@@ -18,12 +18,11 @@ function useLaser(machine_identification_unique: MachineIdentificationUnique) {
// Single optimistic state for all state management
const stateOptimistic = useStateOptimistic<StateEvent>();
// Update optimistic state when real state changes
useEffect(() => {
if (state && !stateOptimistic.isOptimistic) {
if (state) {
stateOptimistic.setReal(state);
}
}, [state, stateOptimistic]);
}, [state]);
// Helper function for optimistic updates using produce
const updateStateOptimistically = (
@@ -31,7 +30,7 @@ function useLaser(machine_identification_unique: MachineIdentificationUnique) {
serverRequest: () => void,
) => {
const currentState = stateOptimistic.value;
if (currentState) {
if (currentState && !stateOptimistic.isOptimistic) {
stateOptimistic.setOptimistic(produce(currentState, producer));
}
serverRequest();
+3 -4
View File
@@ -17,12 +17,11 @@ function useMock(machine_identification_unique: MachineIdentificationUnique) {
// Single optimistic state for all state management
const stateOptimistic = useStateOptimistic<StateEvent>();
// Update optimistic state when real state changes
useEffect(() => {
if (state && !stateOptimistic.isOptimistic) {
if (state) {
stateOptimistic.setReal(state);
}
}, [state, stateOptimistic]);
}, [state]);
// Helper function for optimistic updates using produce
const updateStateOptimistically = (
@@ -30,7 +29,7 @@ function useMock(machine_identification_unique: MachineIdentificationUnique) {
serverRequest: () => Promise<any>,
) => {
const currentState = stateOptimistic.value;
if (currentState) {
if (currentState && !stateOptimistic.isOptimistic) {
stateOptimistic.setOptimistic(produce(currentState, producer));
}
serverRequest()
@@ -36,7 +36,6 @@ export function Winder2ControlPage() {
tensionArmAngle,
zeroTensionArmAngle,
spoolRpm,
spoolDiameter,
setMode,
pullerSpeed,
spoolProgress,
@@ -80,12 +79,6 @@ export function Winder2ControlPage() {
timeseries={spoolRpm}
renderValue={(value) => roundToDecimals(value, 0)}
/>
<TimeSeriesValueNumeric
label="Estimated Diameter"
unit="cm"
timeseries={spoolDiameter}
renderValue={(value) => roundToDecimals(value, 1)}
/>
</ControlCard>
<ControlCard className="bg-red" height={2} title="Traverse">
@@ -16,7 +16,6 @@ export function Winder2GraphsPage() {
const {
state,
spoolRpm,
spoolDiameter,
traversePosition,
tensionArmAngle,
pullerSpeed,
@@ -60,13 +59,6 @@ export function Winder2GraphsPage() {
renderValue={(value) => roundToDecimals(value, 1)}
/>
<SpoolDiameterGraph
syncHook={syncHook}
newData={spoolDiameter}
unit="cm"
renderValue={(value) => roundToDecimals(value, 1)}
/>
<SpoolProgressGraph
syncHook={syncHook}
newData={spoolProgress}
@@ -296,37 +288,3 @@ export function PullerSpeedGraph({
/>
);
}
export function SpoolDiameterGraph({
syncHook,
newData,
unit,
renderValue,
}: {
syncHook: ReturnType<typeof useGraphSync>;
newData: TimeSeries | null;
unit?: Unit;
renderValue?: (value: number) => string;
}) {
const config: GraphConfig = {
title: "Estimated Spool Diameter",
icon: "lu:Circle",
colors: {
primary: "#ec4899",
grid: "#e2e8f0",
background: "#ffffff",
},
exportFilename: "spool_diameter_data",
};
return (
<AutoSyncedBigGraph
syncHook={syncHook}
newData={{ newData }}
unit={unit}
renderValue={renderValue}
config={config}
graphId="spool-diameter"
/>
);
}
@@ -61,7 +61,6 @@ export function useWinder2() {
traversePosition,
pullerSpeed,
spoolRpm,
spoolDiameter,
tensionArmAngle,
spoolProgress,
} = useWinder2Namespace(machineIdentification);
@@ -71,10 +70,10 @@ export function useWinder2() {
// Update optimistic state when real state changes
useEffect(() => {
if (state && !stateOptimistic.isOptimistic) {
if (state) {
stateOptimistic.setReal(state);
}
}, [state, stateOptimistic]);
}, [state]);
// Request functions for all operations
const { request: requestTraverseGotoLimitInner } = useMachineMutation(
@@ -178,7 +177,7 @@ export function useWinder2() {
serverRequest: () => void,
) => {
const currentState = stateOptimistic.value;
if (currentState) {
if (currentState && !stateOptimistic.isOptimistic) {
stateOptimistic.setOptimistic(produce(currentState, producer));
}
serverRequest();
@@ -597,7 +596,6 @@ export function useWinder2() {
traversePosition,
pullerSpeed,
spoolRpm,
spoolDiameter,
tensionArmAngle,
spoolProgress,
@@ -22,7 +22,6 @@ import {
TimeSeries,
TimeSeriesValue,
} from "@/lib/timeseries";
import { connected } from "process";
// ========== Event Schema Definitions ==========
@@ -33,7 +32,6 @@ export const liveValuesEventDataSchema = z.object({
traverse_position: z.number().nullable(),
puller_speed: z.number(),
spool_rpm: z.number(),
spool_diameter: z.number(),
tension_arm_angle: z.number(),
spool_progress: z.number(),
});
@@ -190,7 +188,6 @@ export type Winder2NamespaceStore = {
traversePosition: TimeSeries;
pullerSpeed: TimeSeries;
spoolRpm: TimeSeries;
spoolDiameter: TimeSeries;
tensionArmAngle: TimeSeries;
spoolProgress: TimeSeries;
};
@@ -213,8 +210,6 @@ const { initialTimeSeries: spoolRpm, insert: addSpoolRpm } = createTimeSeries(
FIVE_SECOND,
ONE_HOUR,
);
const { initialTimeSeries: spoolDiameter, insert: addSpoolDiameter } =
createTimeSeries(TWENTY_MILLISECOND, ONE_SECOND, FIVE_SECOND, ONE_HOUR);
const { initialTimeSeries: tensionArmAngle, insert: addTensionArmAngle } =
createTimeSeries(TWENTY_MILLISECOND, ONE_SECOND, FIVE_SECOND, ONE_HOUR);
@@ -234,7 +229,6 @@ export const createWinder2NamespaceStore =
traversePosition,
pullerSpeed,
spoolRpm,
spoolDiameter,
tensionArmAngle,
spoolProgress,
};
@@ -285,7 +279,6 @@ export function winder2MessageHandler(
traverse_position,
puller_speed,
spool_rpm,
spool_diameter,
tension_arm_angle,
spool_progress,
} = liveValuesEvent.data;
@@ -333,16 +326,6 @@ export function winder2MessageHandler(
};
newState.spoolRpm = addSpoolRpm(state.spoolRpm, spoolRpmValue);
// Add spool diameter
const spoolDiameterValue: TimeSeriesValue = {
value: spool_diameter,
timestamp,
};
newState.spoolDiameter = addSpoolDiameter(
state.spoolDiameter,
spoolDiameterValue,
);
// Add tension arm angle
const tensionArmAngleValue: TimeSeriesValue = {
value: tension_arm_angle,
+30 -25
View File
@@ -3,23 +3,23 @@
inputs = {
nixpkgs.url = "github:nixos/nixpkgs/nixos-25.05";
# Crane for Rust builds with dependency caching
crane = {
url = "github:ipetkov/crane";
};
home-manager = {
url = "github:nix-community/home-manager";
inputs.nixpkgs.follows = "nixpkgs";
};
# Add the QiTech Control repository
qitech-control = {
url = "github:qitechgmbh/control";
inputs.nixpkgs.follows = "nixpkgs";
};
# Add flake-utils which was missing
flake-utils = {
url = "github:numtide/flake-utils";
@@ -27,31 +27,29 @@
};
outputs = { self, nixpkgs, crane, flake-utils, qitech-control, home-manager, ... }:
let
# Import git info at the top level so it's available everywhere
installInfo = import ./nixos/os/installInfo.nix;
in
flake-utils.lib.eachDefaultSystem (system:
let
overlays = [
overlays = [
# Add our own overlay for QiTech packages
(final: prev: {
qitechPackages = {
server = final.callPackage ./nixos/packages/server.nix {
server = final.callPackage ./nixos/packages/server.nix {
commitHash = builtins.getEnv "QITECH_COMMIT_HASH";
craneLib = crane.mkLib final;
};
electron = final.callPackage ./nixos/packages/electron.nix {
electron = final.callPackage ./nixos/packages/electron.nix {
commitHash = builtins.getEnv "QITECH_COMMIT_HASH";
nodejs = final.nodejs_22;
};
};
})
];
pkgs = import nixpkgs { inherit system overlays; };
gitInfo = import ./nixos/gitInfo.nix { inherit pkgs; };
craneLib = crane.mkLib pkgs;
# Use Rust 1.86 stable from nixpkgs
rust = pkgs.rustc;
in {
@@ -60,7 +58,7 @@
electron = pkgs.qitechPackages.electron;
default = self.packages.${system}.server;
};
devShells.default = craneLib.devShell {
packages = with pkgs; [
pkg-config
@@ -69,7 +67,9 @@
nodejs_22
nodePackages.npm
];
hardeningDisable = [ "fortify" ];
shellHook = ''
echo "QiTech Control Development Environment"
echo "Rust version: $(rustc --version)"
@@ -80,14 +80,19 @@
) // {
nixosModules.qitech = import ./nixos/modules/qitech.nix;
nixosModules.default = self.nixosModules.qitech;
# Define nixosConfigurations outside of eachDefaultSystem
nixosConfigurations = {
nixosConfigurations =
let
system = builtins.currentSystem;
pkgs = import nixpkgs { inherit system; };
gitInfo = import ./nixos/gitInfo.nix { inherit pkgs; };
in {
# Replace "nixos" with your actual hostname
nixos = nixpkgs.lib.nixosSystem {
system = "x86_64-linux"; # Specify the correct system
system = system;
specialArgs = {
installInfo = installInfo; # Pass installInfo to modules
gitInfo = gitInfo; # Pass gitInfo to modules
};
modules = [
# Apply the overlays to the system
@@ -95,24 +100,24 @@
# Add our own overlay for QiTech packages with commit hash support
(final: prev: {
qitechPackages = {
server = final.callPackage ./nixos/packages/server.nix {
commitHash = installInfo.gitCommit;
server = final.callPackage ./nixos/packages/server.nix {
commitHash = gitInfo.gitCommit;
craneLib = crane.mkLib final;
};
electron = final.callPackage ./nixos/packages/electron.nix {
commitHash = installInfo.gitCommit;
commitHash = gitInfo.gitCommit;
nodejs = final.nodejs_22;
};
};
})
];
}
./nixos/os/configuration.nix
# QiTech Control module
self.nixosModules.qitech
# Home Manager module
home-manager.nixosModules.home-manager
{
@@ -124,4 +129,4 @@
};
};
};
}
}
+24 -38
View File
@@ -1,53 +1,39 @@
# write the installInfo.nix while
# we capture information about the current git commit
sudo touch ./nixos/os/installInfo.nix
#!/bin/sh
set -euo pipefail
# 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"
export GIT_TIMESTAMP=$(git --no-pager show -s --format=%cI HEAD) # e.g., "2025-06-10T14:30:45+02:00"
export GIT_COMMIT=$(git rev-parse HEAD) # e.g., "b2c7f6e0b138174770798f84ada8b0aa65afeb"
export 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 "")
export 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)
export 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)
export 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)
export GIT_ABBREVIATION="$GIT_BRANCH" # e.g., "main", "develop" (when on a branch)
fi
fi
# Create escaped version for system.nixos.label
GIT_ABBREVIATION_ESCAPED=$(echo "$GIT_ABBREVIATION" | sed -e 's/+/-/g' -e 's/[^a-zA-Z0-9:_\.-]//g') # e.g., "2-0-0", "main", "b2c7f6e"
export GIT_ABBREVIATION_ESCAPED=$(echo "$GIT_ABBREVIATION" | sed -e 's/+/-/g' -e 's/[^a-zA-Z0-9:_\.-]//g') # e.g., "2-0-0", "main", "b2c7f6e"
sudo tee ./nixos/os/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
sudo \
--preserve-env=GIT_TIMESTAMP \
--preserve-env=GIT_COMMIT \
--preserve-env=GIT_URL \
--preserve-env=GIT_ABBREVIATION \
--preserve-env=GIT_ABBREVIATION_ESCAPED \
nixos-rebuild boot \
--flake .#nixos \
--show-trace \
--impure \
--option sandbox false \
--option eval-cache false
# make sure the installInfo.nix file is tracked by git
# otherwise it will be ignored by nix
# configure a git user
git config --global user.name "nixos-install.sh"
git config --global user.email "nixos-install.sh@localhost"
# add the installInfo.nix file to the git index
git add ./nixos/os/installInfo.nix
# commit the changes
git commit -m "Add installInfo.nix file with current commit information"
# Now we can install the system
# Now we install the new system
if sudo nixos-rebuild boot --flake .#nixos --show-trace --impure --option sandbox false --option eval-cache false; then
reboot
else
exit 1
fi
sudo reboot
+25
View File
@@ -0,0 +1,25 @@
{ pkgs, ... }:
let
# Generate Git info in the store
gitInfoDerivation = pkgs.runCommand "git-info" {} ''
gitTimestamp="${builtins.getEnv "GIT_TIMESTAMP"}"
gitCommit="${builtins.getEnv "GIT_COMMIT"}"
gitUrl="${builtins.getEnv "GIT_URL"}"
gitAbbreviation="${builtins.getEnv "GIT_ABBREVIATION"}"
gitAbbreviationEscaped="${builtins.getEnv "GIT_ABBREVIATION_ESCAPED"}"
mkdir -p $out
cat <<EOF > $out/git-info.json
{
"gitTimestamp": "$gitTimestamp",
"gitCommit": "$gitCommit",
"gitAbbreviation": "$gitAbbreviation",
"gitUrl": "$gitUrl",
"gitAbbreviationEscaped": "$gitAbbreviationEscaped"
}
EOF
'';
in
builtins.fromJSON (builtins.readFile "${gitInfoDerivation}/git-info.json")
+33 -29
View File
@@ -70,36 +70,40 @@ in {
wantedBy = [ "multi-user.target" ];
after = [ "network.target" ];
serviceConfig = {
Type = "simple";
User = cfg.user;
Group = cfg.group;
ExecStart = "${cfg.package}/bin/server";
Restart = "always";
RestartSec = "10s";
# Grant specific capabilities needed for EtherCAT
CapabilityBoundingSet = "CAP_NET_RAW CAP_NET_ADMIN CAP_SYS_NICE";
AmbientCapabilities = "CAP_NET_RAW CAP_NET_ADMIN CAP_SYS_NICE";
# Hardening options
NoNewPrivileges = true;
ProtectSystem = "strict";
ProtectHome = true;
PrivateTmp = true;
PrivateDevices = false; # Need access to devices
ProtectKernelTunables = true;
ProtectControlGroups = true;
RestrictAddressFamilies = "AF_UNIX AF_INET AF_INET6 AF_NETLINK AF_PACKET";
RestrictNamespaces = true;
LockPersonality = true;
MemoryDenyWriteExecute = false;
serviceConfig = {
Type = "simple";
User = cfg.user;
Group = cfg.group;
ExecStart = "${cfg.package}/bin/server";
Restart = "always";
RestartSec = "10s";
# Capabilities (unchanged, as you wanted)
CapabilityBoundingSet = "CAP_NET_RAW CAP_NET_ADMIN CAP_SYS_NICE CAP_DAC_OVERRIDE";
AmbientCapabilities = "CAP_NET_RAW CAP_NET_ADMIN CAP_SYS_NICE CAP_DAC_OVERRIDE";
# Hardening options
NoNewPrivileges = true;
ProtectSystem = "strict";
# Open only /proc/irq explicitly
ReadWritePaths = [ "/proc/irq" ];
ProtectHome = true;
PrivateTmp = true;
PrivateDevices = false;
# Must disable this to allow /proc/irq writes
ProtectKernelTunables = false;
ProtectControlGroups = true;
RestrictAddressFamilies = "AF_UNIX AF_INET AF_INET6 AF_NETLINK AF_PACKET";
RestrictNamespaces = true;
LockPersonality = true;
MemoryDenyWriteExecute = false;
# Logging
StandardOutput = "journal";
StandardError = "journal";
SyslogIdentifier = "qitech-control-server";
};
# Logging
StandardOutput = "journal";
StandardError = "journal";
SyslogIdentifier = "qitech-control-server";
};
environment = {
RUST_BACKTRACE = "full";
+17 -19
View File
@@ -1,9 +1,8 @@
# Edit this configuration file to define what should be installed on
# your system. Help is available in the configuration.nix(5) man page
# and in the NixOS manual (accessible by running ‘nixos-help’).
{ config, pkgs, installInfo, ... }:
{ config, pkgs, ... }:
let
gitInfo = import ../gitInfo.nix { inherit pkgs; };
in
{
imports =
[ # Include the results of the hardware scan.
@@ -20,13 +19,12 @@
boot.kernelPackages = pkgs.linuxPackages-rt_latest;
boot.kernelModules = [ "i915" ];
boot.kernelParams = [
# Graphical
"logo.nologo" # Remove kernel logo during boot
# Performance
# Performance
# Specific Vulnerabilities Addressed by Mitigations:
# - Spectre variants (V1, V2, V4, SWAPGS, SpectreRSB, etc.)
# - Meltdown (Rogue Data Cache Load)
@@ -36,7 +34,7 @@
# - TSX Asynchronous Abort (TAA)
# - iTLB Multihit
# - And others as they're discovered and mitigated
#
#
# With mitigations=off
# - PROS: Maximum performance, equivalent to pre-2018 behavior
# - CONS: Vulnerable to Spectre, Meltdown, Foreshadow, ZombieLoad, etc.
@@ -48,7 +46,7 @@
# Memory Management
"transparent_hugepage=always" # Use larger memory pages for memory intense applications
"nmi_watchdog=0" # Disable NMI watchdog for reduced CPU overhead and realtime execution
# High-throughput ethernet parameters
"pcie_aspm=off" # Disable PCIe power management for NICs
"intel_iommu=off" # Disable IOMMU (performance gain)
@@ -144,7 +142,7 @@
autoSuspend = false;
wayland = true;
};
services.xserver.desktopManager.gnome.enable = true;
# Disable sleep/suspend
@@ -217,7 +215,7 @@
enable = true;
openFirewall = true;
user = "qitech-service";
group = "qitech-service";
group = "qitech-service";
package = pkgs.qitechPackages.server;
};
@@ -259,7 +257,7 @@
pkgs.qitechPackages.electron
htop
wireshark
pciutils
pciutils
neofetch
];
@@ -295,15 +293,15 @@
# Set system wide env variables
environment.variables = {
QITECH_OS = "true";
QITECH_OS_GIT_TIMESTAMP = installInfo.gitTimestamp;
QITECH_OS_GIT_COMMIT = installInfo.gitCommit;
QITECH_OS_GIT_ABBREVIATION = installInfo.gitAbbreviation;
QITECH_OS_GIT_URL = installInfo.gitUrl;
QITECH_OS_GIT_TIMESTAMP = gitInfo.gitTimestamp;
QITECH_OS_GIT_COMMIT = gitInfo.gitCommit;
QITECH_OS_GIT_ABBREVIATION = gitInfo.gitAbbreviation;
QITECH_OS_GIT_URL = gitInfo.gitUrl;
};
# Set revision labe;
system.nixos.label = "${installInfo.gitAbbreviationEscaped}_${installInfo.gitCommit}";
system.nixos.label = "${gitInfo.gitAbbreviationEscaped}_${gitInfo.gitCommit}";
# Some programs need SUID wrappers, can be configured further or are
# started in user sessions.
# programs.mtr.enable = true;
+9 -1
View File
@@ -9,6 +9,8 @@ use crate::{
ethercat::config::{MAX_FRAMES, MAX_PDU_DATA, MAX_SUBDEVICES, PDI_LEN},
};
use control_core::ethercat::eeprom_identification::read_device_identifications;
#[cfg(unix)]
use control_core::irq_handling::set_irq_affinity;
use control_core::machines::identification::{
DeviceHardwareIdentification, DeviceHardwareIdentificationEthercat, DeviceIdentification,
};
@@ -45,6 +47,12 @@ pub async fn setup_loop(
.spawn(move || {
send_panic(thread_panic_tx_clone);
#[cfg(target_os = "linux")]
match set_irq_affinity(&interface, 3) {
Ok(_) => tracing::info!("ethernet interrupt handler now runs on cpu:{}", 3),
Err(e) => tracing::error!("set_irq_handler_affinity failed: {:?}", e),
}
// Set core affinity to 4th core
let _ = set_core_affinity(3);
@@ -84,7 +92,7 @@ pub async fn setup_loop(
// Default 10ms
eeprom: Duration::from_millis(10),
// Default 0ms
wait_loop_delay: Duration::from_millis(1),
wait_loop_delay: Duration::from_millis(0),
// Default 100ms
mailbox_echo: Duration::from_millis(100),
// Default 1000ms
+3 -9
View File
@@ -7,17 +7,17 @@ use api::{Buffer1Namespace, BufferV1Events, LiveValuesEvent, ModeState, StateEve
use buffer_tower_controller::BufferTowerController;
use control_core::{
machines::{
ConnectedMachine, ConnectedMachineData, Machine, downcast_machine,
ConnectedMachine, ConnectedMachineData, downcast_machine,
identification::{MachineIdentification, MachineIdentificationUnique},
manager::MachineManager,
},
socketio::namespace::NamespaceCacheingLogic,
};
use control_core_derive::Machine;
use futures::executor::block_on;
use serde::{Deserialize, Serialize};
use smol::lock::{Mutex, RwLock};
use std::{
any::Any,
sync::{Arc, Weak},
time::Instant,
};
@@ -26,7 +26,7 @@ use crate::machines::{
MACHINE_BUFFER_V1, VENDOR_QITECH, buffer1::api::ConnectedMachineState, winder2::Winder2,
};
#[derive(Debug)]
#[derive(Debug, Machine)]
pub struct BufferV1 {
// controllers
pub buffer_tower_controller: BufferTowerController,
@@ -52,12 +52,6 @@ impl std::fmt::Display for BufferV1 {
}
}
impl Machine for BufferV1 {
fn as_any(&self) -> &dyn Any {
self
}
}
impl BufferV1 {
pub const MACHINE_IDENTIFICATION: MachineIdentification = MachineIdentification {
vendor: VENDOR_QITECH,
+4
View File
@@ -67,6 +67,10 @@ pub struct LiveValuesEvent {
pub back_power: f64,
/// middle heating power in watts
pub middle_power: f64,
/// combined power consumption in watts
pub combined_power: f64,
/// total energy consumption in kWh
pub total_energy_kwh: f64,
}
impl LiveValuesEvent {
@@ -4,6 +4,7 @@ use control_core::modbus::{
modbus_serial_interface::ModbusSerialInterface,
};
use ethercat_hal::io::serial_interface::SerialInterface;
use serde::Serialize;
use std::time::{Duration, Instant};
use uom::si::{
electric_current::centiampere,
@@ -260,7 +261,8 @@ impl From<MitsubishiCS80Requests> for MitsubishiCS80Request {
}
}
#[derive(Debug, Default)]
// Serialize is needed so we can hash it
#[derive(Debug, Default, Clone, Copy, Serialize)]
pub struct MitsubishiCS80Status {
pub running: bool,
pub forward_running: bool,
+75 -21
View File
@@ -4,18 +4,20 @@ use api::{
InverterStatusState, LiveValuesEvent, ModeState, PidSettings, PidSettingsStates, PressureState,
RegulationState, RotationState, ScrewState, StateEvent,
};
use control_core::helpers::hasher_serializer::check_hash_different;
use control_core::helpers::hasher_serializer::hash_with_serde_model;
use control_core::socketio::event::BuildEvent;
use control_core::{
machines::{Machine, identification::MachineIdentification},
socketio::namespace::NamespaceCacheingLogic,
machines::identification::MachineIdentification, socketio::namespace::NamespaceCacheingLogic,
};
use control_core_derive::Machine;
use screw_speed_controller::ScrewSpeedController;
use serde::{Deserialize, Serialize};
use std::{any::Any, time::Instant};
use std::time::Instant;
use temperature_controller::TemperatureController;
use uom::si::{
angular_velocity::{AngularVelocity, revolution_per_minute},
electric_current::ampere,
electric_potential::volt,
f64::{Pressure, ThermodynamicTemperature},
pressure::bar,
thermodynamic_temperature::degree_celsius,
@@ -60,11 +62,11 @@ pub enum HeatingType {
Middle,
}
#[derive(Debug)]
#[derive(Debug, Machine)]
pub struct ExtruderV2 {
namespace: ExtruderV2Namespace,
last_measurement_emit: Instant,
last_state_event: Option<StateEvent>,
last_status_hash: Option<u64>,
mode: ExtruderV2Mode,
screw_speed_controller: ScrewSpeedController,
temperature_controller_front: TemperatureController,
@@ -72,6 +74,10 @@ pub struct ExtruderV2 {
temperature_controller_back: TemperatureController,
temperature_controller_nozzle: TemperatureController,
/// Energy tracking for total consumption calculation
total_energy_kwh: f64,
last_energy_calculation_time: Option<Instant>,
/// will be initalized as false and set to true by `emit_state`
/// This way we can signal to the client that the first state emission is a default state
emitted_default_state: bool,
@@ -173,19 +179,18 @@ impl ExtruderV2 {
}
pub fn maybe_emit_state_event(&mut self) {
let new_state = self.build_state_event();
let old_state = match &self.last_state_event {
let old_status_hash = match self.last_status_hash {
Some(event) => event,
None => {
self.emit_state();
return;
}
};
let should_emit = check_hash_different(&new_state, old_state);
let mut status = self.screw_speed_controller.get_inverter_status();
let new_status_hash = hash_with_serde_model(&mut status);
let should_emit = new_status_hash != old_status_hash;
if should_emit {
self.namespace
.emit(ExtruderV2Events::State(new_state.build()));
self.last_state_event = Some(new_state);
self.emit_state();
}
}
}
@@ -195,11 +200,6 @@ impl std::fmt::Display for ExtruderV2 {
write!(f, "ExtruderV2")
}
}
impl Machine for ExtruderV2 {
fn as_any(&self) -> &dyn Any {
self
}
}
impl ExtruderV2 {
pub const MACHINE_IDENTIFICATION: MachineIdentification = MachineIdentification {
@@ -209,7 +209,47 @@ impl ExtruderV2 {
}
impl ExtruderV2 {
/// Calculate combined power consumption in watts
fn calculate_combined_power(&mut self) -> f64 {
let motor_power = {
let motor_status = &self.screw_speed_controller.inverter.motor_status;
let voltage = motor_status.voltage.get::<volt>();
let current = motor_status.current.get::<ampere>();
voltage * current
};
let nozzle_power = self
.temperature_controller_nozzle
.get_heating_element_wattage();
let front_power = self
.temperature_controller_front
.get_heating_element_wattage();
let back_power = self
.temperature_controller_back
.get_heating_element_wattage();
let middle_power = self
.temperature_controller_middle
.get_heating_element_wattage();
motor_power + nozzle_power + front_power + back_power + middle_power
}
/// Update total energy consumption in kWh
fn update_total_energy(&mut self, current_power_watts: f64, now: Instant) {
if let Some(last_time) = self.last_energy_calculation_time {
let time_delta_hours = now.duration_since(last_time).as_secs_f64() / 3600.0;
let energy_delta_kwh = (current_power_watts / 1000.0) * time_delta_hours;
self.total_energy_kwh += energy_delta_kwh;
}
self.last_energy_calculation_time = Some(now);
}
pub fn emit_live_values(&mut self) {
let now = Instant::now();
let combined_power = self.calculate_combined_power();
// Update energy consumption
self.update_total_energy(combined_power, now);
let live_values = LiveValuesEvent {
motor_status: self.screw_speed_controller.get_motor_status().into(),
pressure: self.screw_speed_controller.get_pressure().get::<bar>(),
@@ -245,6 +285,8 @@ impl ExtruderV2 {
middle_power: self
.temperature_controller_middle
.get_heating_element_wattage(),
combined_power,
total_energy_kwh: self.total_energy_kwh,
};
let event = live_values.build();
@@ -253,15 +295,17 @@ impl ExtruderV2 {
pub fn emit_state(&mut self) {
let state = self.build_state_event();
self.last_state_event = Some(state.clone());
let hash = hash_with_serde_model(self.screw_speed_controller.get_inverter_status());
self.last_status_hash = Some(hash);
let event = state.build();
self.namespace.emit(ExtruderV2Events::State(event));
}
// Extruder Settings Api Impl
const fn set_nozzle_pressure_limit_is_enabled(&mut self, enabled: bool) {
fn set_nozzle_pressure_limit_is_enabled(&mut self, enabled: bool) {
self.screw_speed_controller
.set_nozzle_pressure_limit_is_enabled(enabled);
self.emit_state();
}
/// pressure is represented as bar
@@ -269,6 +313,7 @@ impl ExtruderV2 {
let nozzle_pressure_limit = Pressure::new::<bar>(pressure);
self.screw_speed_controller
.set_nozzle_pressure_limit(nozzle_pressure_limit);
self.emit_state();
}
}
@@ -280,13 +325,15 @@ impl ExtruderV2 {
self.temperature_controller_front.disable();
self.temperature_controller_middle.disable();
self.temperature_controller_nozzle.disable();
self.emit_state();
}
const fn enable_heating(&mut self) {
fn enable_heating(&mut self) {
self.temperature_controller_back.allow_heating();
self.temperature_controller_front.allow_heating();
self.temperature_controller_middle.allow_heating();
self.temperature_controller_nozzle.allow_heating();
self.emit_state();
}
// Turn heating OFF and do nothing
@@ -354,6 +401,7 @@ impl ExtruderV2 {
impl ExtruderV2 {
fn set_rotation_state(&mut self, forward: bool) {
self.screw_speed_controller.set_rotation_direction(forward);
self.emit_state();
}
fn reset_inverter(&mut self) {
@@ -362,6 +410,7 @@ impl ExtruderV2 {
fn set_mode_state(&mut self, mode: ExtruderV2Mode) {
self.switch_mode(mode);
self.emit_state();
}
}
@@ -378,17 +427,20 @@ impl ExtruderV2 {
self.screw_speed_controller.set_uses_rpm(uses_rpm);
self.screw_speed_controller.start_pressure_regulation();
}
self.emit_state();
}
fn set_target_pressure(&mut self, pressure: f64) {
let pressure = Pressure::new::<bar>(pressure);
self.screw_speed_controller.set_target_pressure(pressure);
self.emit_state();
}
fn set_target_rpm(&mut self, rpm: f64) {
let revolution_per_minutes = AngularVelocity::new::<revolution_per_minute>(rpm);
self.screw_speed_controller
.set_target_screw_rpm(revolution_per_minutes);
self.emit_state();
}
}
@@ -414,13 +466,15 @@ impl ExtruderV2 {
.temperature_controller_middle
.set_target_temperature(target_temp),
}
self.emit_state();
}
}
impl ExtruderV2 {
const fn configure_pressure_pid(&mut self, settings: PidSettings) {
fn configure_pressure_pid(&mut self, settings: PidSettings) {
self.screw_speed_controller
.pid
.configure(settings.ki, settings.kp, settings.kd);
self.emit_state();
}
}
+3 -1
View File
@@ -391,13 +391,15 @@ impl MachineNewTrait for ExtruderV2 {
namespace: ExtruderV2Namespace::new(params.socket_queue_tx.clone()),
last_measurement_emit: Instant::now(),
mode: ExtruderV2Mode::Standby,
total_energy_kwh: 0.0,
last_energy_calculation_time: None,
temperature_controller_front,
temperature_controller_middle,
temperature_controller_back,
temperature_controller_nozzle,
screw_speed_controller,
emitted_default_state: false,
last_state_event: None,
last_status_hash: None,
};
extruder.emit_state();
Ok(extruder)
@@ -15,6 +15,8 @@ use uom::si::{
pressure::bar,
};
use crate::machines::extruder1::mitsubishi_cs80::MitsubishiCS80Status;
use super::mitsubishi_cs80::{MitsubishiCS80, MotorStatus};
#[derive(Debug)]
@@ -64,6 +66,10 @@ impl ScrewSpeedController {
}
}
pub const fn get_inverter_status(&mut self) -> MitsubishiCS80Status {
self.inverter.status
}
pub const fn get_motor_enabled(&mut self) -> bool {
self.motor_on
}
-1
View File
@@ -21,7 +21,6 @@ impl MachineAct for LaserMachine {
fn act(&mut self, now: Instant) {
// more than 33ms have passed since last emit (30 "fps" target)
if now.duration_since(self.last_measurement_emit) > Duration::from_secs_f64(1.0 / 30.0) {
self.maybe_emit_state_event();
self.emit_live_values();
self.last_measurement_emit = now;
}
+7 -30
View File
@@ -4,19 +4,18 @@ use crate::{
};
use api::{LaserEvents, LaserMachineNamespace, LaserState, LiveValuesEvent, StateEvent};
use control_core::{
helpers::hasher_serializer::check_hash_different,
machines::{Machine, identification::MachineIdentification},
socketio::namespace::NamespaceCacheingLogic,
machines::identification::MachineIdentification, socketio::namespace::NamespaceCacheingLogic,
};
use control_core_derive::Machine;
use smol::lock::RwLock;
use std::{any::Any, sync::Arc, time::Instant};
use std::{sync::Arc, time::Instant};
use uom::si::{f64::Length, length::millimeter};
pub mod act;
pub mod api;
pub mod new;
#[derive(Debug)]
#[derive(Debug, Machine)]
pub struct LaserMachine {
// drivers
laser: Arc<RwLock<Laser>>,
@@ -31,13 +30,6 @@ pub struct LaserMachine {
/// Will be initialized as false and set to true by emit_state
/// This way we can signal to the client that the first state emission is a default state
emitted_default_state: bool,
last_state_event: Option<StateEvent>,
}
impl Machine for LaserMachine {
fn as_any(&self) -> &dyn Any {
self
}
}
impl LaserMachine {
@@ -78,24 +70,6 @@ impl LaserMachine {
}
}
pub fn maybe_emit_state_event(&mut self) {
let new_state: StateEvent = self.build_state_event();
let old_state: &StateEvent = match &self.last_state_event {
Some(old_state) => old_state,
None => {
self.emit_state();
return;
}
};
let should_emit = check_hash_different(&new_state, old_state);
if should_emit {
let event = &new_state.build();
self.last_state_event = Some(new_state);
self.namespace.emit(LaserEvents::State(event.clone()));
}
}
pub fn emit_state(&mut self) {
let state = StateEvent {
is_default_state: !std::mem::replace(&mut self.emitted_default_state, true),
@@ -111,14 +85,17 @@ impl LaserMachine {
pub fn set_higher_tolerance(&mut self, higher_tolerance: f64) {
self.laser_target.higher_tolerance = Length::new::<millimeter>(higher_tolerance);
self.emit_state();
}
pub fn set_lower_tolerance(&mut self, lower_tolerance: f64) {
self.laser_target.lower_tolerance = Length::new::<millimeter>(lower_tolerance);
self.emit_state();
}
pub fn set_target_diameter(&mut self, target_diameter: f64) {
self.laser_target.diameter = Length::new::<millimeter>(target_diameter);
self.emit_state();
}
}
#[derive(Debug, Clone)]
-1
View File
@@ -39,7 +39,6 @@ impl MachineNewTrait for LaserMachine {
last_measurement_emit: Instant::now(),
laser_target,
emitted_default_state: false,
last_state_event: None,
};
// Emit initial state
-1
View File
@@ -24,7 +24,6 @@ impl MachineAct for MockMachine {
// Only emit live values if machine is in Running mode
// The live values are updated approximately 30 times per second
if now.duration_since(self.last_measurement_emit) > Duration::from_secs_f64(1.0 / 30.0) {
self.maybe_emit_state_event();
self.emit_live_values();
self.last_measurement_emit = now;
}
+8 -36
View File
@@ -2,10 +2,9 @@ use crate::machines::{MACHINE_MOCK, VENDOR_QITECH};
use api::{LiveValuesEvent, MockEvents, MockMachineNamespace, Mode, ModeState, StateEvent};
use control_core::socketio::event::BuildEvent;
use control_core::{
helpers::hasher_serializer::check_hash_different,
machines::{Machine, identification::MachineIdentification},
socketio::namespace::NamespaceCacheingLogic,
machines::identification::MachineIdentification, socketio::namespace::NamespaceCacheingLogic,
};
use control_core_derive::Machine;
use std::time::Instant;
use tracing::info;
@@ -18,7 +17,7 @@ pub mod act;
pub mod api;
pub mod new;
#[derive(Debug)]
#[derive(Debug, Machine)]
pub struct MockMachine {
// socketio
namespace: MockMachineNamespace,
@@ -39,11 +38,6 @@ pub struct MockMachine {
emitted_default_state: bool,
}
impl Machine for MockMachine {
fn as_any(&self) -> &dyn std::any::Any {
self
}
}
impl MockMachine {
pub const MACHINE_IDENTIFICATION: MachineIdentification = MachineIdentification {
vendor: VENDOR_QITECH,
@@ -103,47 +97,25 @@ impl MockMachine {
self.last_emitted_event = Some(current_state);
}
pub fn maybe_emit_state_event(&mut self) {
let new_state = StateEvent {
is_default_state: !std::mem::replace(&mut self.emitted_default_state, true),
frequency1: self.frequency1.get::<millihertz>(),
frequency2: self.frequency2.get::<millihertz>(),
frequency3: self.frequency3.get::<millihertz>(),
mode_state: ModeState {
mode: self.mode.clone(),
},
};
let old_event = match &self.last_emitted_event {
Some(old_event) => old_event,
None => {
self.emit_state();
return;
}
};
let should_emit = check_hash_different(&new_state, old_event);
if should_emit {
self.last_emitted_event = Some(new_state.clone());
self.namespace.emit(MockEvents::State(new_state.build()));
}
}
/// Set the frequencies of the sine waves
pub fn set_frequency1(&mut self, frequency_mhz: f64) {
self.frequency1 = Frequency::new::<millihertz>(frequency_mhz);
self.emit_state();
}
pub fn set_frequency2(&mut self, frequency_mhz: f64) {
self.frequency2 = Frequency::new::<millihertz>(frequency_mhz);
self.emit_state();
}
pub fn set_frequency3(&mut self, frequency_mhz: f64) {
self.frequency3 = Frequency::new::<millihertz>(frequency_mhz);
self.emit_state();
}
/// Set the mode of the mock machine
pub const fn set_mode(&mut self, mode: Mode) {
pub fn set_mode(&mut self, mode: Mode) {
self.mode = mode;
self.emit_state();
}
}
-1
View File
@@ -18,7 +18,6 @@ impl MachineAct for Winder2 {
// more than 33ms have passed since last emit (30 "fps" target)
if now.duration_since(self.last_measurement_emit) > Duration::from_secs_f64(1.0 / 30.0) {
self.maybe_emit_state_event();
self.emit_live_values();
self.last_measurement_emit = now;
}
@@ -39,8 +39,8 @@ pub struct AdaptiveSpoolSpeedController {
filament_calc: FilamentTensionCalculator,
/// Moving window of recent speeds (in rad/s) used for dynamic acceleration limit calculation
speed_time_window: MovingTimeWindow<f64>,
/// Estimated diameter in cm
radius: Length,
/// Factor to control/calculate the feed speed based on filament tension
speed_factor: Length,
/// Timestamp of last max speed factor update, used for time-aware learning rate calculation
last_max_speed_factor_update: Option<Instant>,
@@ -87,9 +87,9 @@ impl AdaptiveSpoolSpeedController {
/// Proportional control gain for adaptive learning (negative: higher tension reduces speed)
const RADIUS_LEARNING_RATE: f64 = 0.5;
const RADIUS_MIN: f64 = 4.25;
const FACTOR_MIN: f64 = 4.25;
const RADIUS_MAX: f64 = 20.0;
const FACTOR_MAX: f64 = 20.0;
/// if the tension is the lowest, the speed can be up to 2x the puller speed
const MAX_SPEED_MULTIPLIER: f64 = 4.0;
@@ -130,7 +130,7 @@ impl AdaptiveSpoolSpeedController {
std::time::Duration::from_secs(Self::SPEED_WINDOW_DURATION_SECS),
Self::SPEED_WINDOW_MAX_SAMPLES,
),
radius: Length::new::<centimeter>(4.25),
speed_factor: Length::new::<centimeter>(4.25),
last_max_speed_factor_update: None,
tension_target: Self::TENSION_TARGET,
radius_learning_rate: Self::RADIUS_LEARNING_RATE,
@@ -162,7 +162,7 @@ impl AdaptiveSpoolSpeedController {
fn get_max_speed(&self, puller_speed_controller: &PullerSpeedController) -> AngularVelocity {
AngularVelocity::new::<radian_per_second>(
(puller_speed_controller.last_speed.get::<meter_per_second>()
/ self.radius.get::<meter>())
/ self.speed_factor.get::<meter>())
* self.max_speed_multiplier,
)
}
@@ -205,7 +205,7 @@ impl AdaptiveSpoolSpeedController {
// 0.0 means minimum tension (low angle, high speed)
let filament_tension = self.filament_calc.calc_filament_tension(clamped_angle);
self.update_radius(filament_tension, t);
self.update_speed_factor(filament_tension, t);
// Calculate speed based on inverted tension (lower tension = higher speed)
@@ -281,7 +281,7 @@ impl AdaptiveSpoolSpeedController {
///
/// # Returns
/// Speed guaranteed to be within safe operational limits
fn clamp_speed(&mut self, speed: AngularVelocity) -> AngularVelocity {
fn clamp_speed(&self, speed: AngularVelocity) -> AngularVelocity {
let min_speed = AngularVelocity::ZERO;
let max_speed = AngularVelocity::new::<revolution_per_minute>(Self::SAFETY_MAX_SPEED_RPM);
@@ -297,7 +297,7 @@ impl AdaptiveSpoolSpeedController {
/// # Parameters
/// - `filament_tension`: Normalized tension value
/// - `t`: Current timestamp for time-aware learning rate
fn update_radius(&mut self, filament_tension: f64, t: Instant) {
fn update_speed_factor(&mut self, filament_tension: f64, t: Instant) {
let delta_t = match self.last_max_speed_factor_update {
Some(last_update) => t.duration_since(last_update).as_secs_f64(),
None => {
@@ -313,13 +313,13 @@ impl AdaptiveSpoolSpeedController {
// Calculate proportional control adjustment
let proportional_gain = self.radius_learning_rate * delta_t;
let radius_change = tension_error * proportional_gain;
let factor_change = tension_error * proportional_gain;
// Update the speed factor directly
let new_radius = (self.radius.get::<centimeter>() + radius_change)
.clamp(Self::RADIUS_MIN, Self::RADIUS_MAX);
let new_factor = (self.speed_factor.get::<centimeter>() + factor_change)
.clamp(Self::FACTOR_MIN, Self::FACTOR_MAX);
self.radius = Length::new::<centimeter>(new_radius); // Convert to cm
self.speed_factor = Length::new::<centimeter>(new_factor); // Convert to cm
self.last_max_speed_factor_update = Some(t);
}
@@ -385,7 +385,7 @@ impl AdaptiveSpoolSpeedController {
pub fn reset(&mut self) {
self.last_speed = AngularVelocity::ZERO;
self.acceleration_controller.reset(AngularVelocity::ZERO);
self.radius = Length::new::<centimeter>(4.25);
self.speed_factor = Length::new::<centimeter>(4.25);
self.last_max_speed_factor_update = None;
self.tension_target = Self::TENSION_TARGET;
self.radius_learning_rate = Self::RADIUS_LEARNING_RATE;
@@ -414,8 +414,8 @@ impl AdaptiveSpoolSpeedController {
self.acceleration_controller.reset(speed);
}
pub fn get_radius(&self) -> Length {
self.radius
pub fn get_speed_factor(&self) -> Length {
self.speed_factor
}
// Getters and setters for the new configurable parameters
-2
View File
@@ -113,8 +113,6 @@ pub struct LiveValuesEvent {
pub puller_speed: f64,
/// spool rpm
pub spool_rpm: f64,
/// spool diameter in mm
pub spool_diameter: f64,
/// tension arm angle in degrees
pub tension_arm_angle: f64,
// spool progress in meters (pulled distance of filament)
+16 -37
View File
@@ -14,18 +14,18 @@ use api::{
SpoolSpeedControllerState, StateEvent, TensionArmState, TraverseState, Winder2Events,
Winder2Namespace,
};
use control_core::helpers::hasher_serializer::check_hash_different;
use control_core::socketio::event::BuildEvent;
use control_core::{
converters::angular_step_converter::AngularStepConverter,
machines::{
ConnectedMachine, ConnectedMachineData, Machine, downcast_machine,
ConnectedMachine, ConnectedMachineData, downcast_machine,
identification::{MachineIdentification, MachineIdentificationUnique},
manager::MachineManager,
},
socketio::namespace::NamespaceCacheingLogic,
uom_extensions::velocity::meter_per_minute,
};
use control_core_derive::Machine;
use ethercat_hal::io::{
digital_input::DigitalInput, digital_output::DigitalOutput,
stepper_velocity_el70x1::StepperVelocityEL70x1,
@@ -64,7 +64,7 @@ pub struct SpoolAutomaticAction {
pub mode: SpoolAutomaticActionMode,
}
#[derive(Debug)]
#[derive(Debug, Machine)]
pub struct Winder2 {
// drivers
pub traverse: StepperVelocityEL70x1,
@@ -107,13 +107,6 @@ pub struct Winder2 {
/// Will be initialized as false and set to true by emit_state
/// This way we can signal to the client that the first state emission is a default state
emitted_default_state: bool,
last_state_event: Option<StateEvent>,
}
impl Machine for Winder2 {
fn as_any(&self) -> &dyn std::any::Any {
self
}
}
impl Winder2 {
@@ -127,6 +120,7 @@ impl Winder2 {
impl Winder2 {
fn set_laser(&mut self, value: bool) {
self.laser.set(value);
self.emit_state();
}
/// Validates that traverse limits maintain proper constraints:
@@ -145,8 +139,8 @@ impl Winder2 {
// Don't update if validation fails - keep the current value
return;
}
self.traverse_controller.set_limit_inner(new_inner);
self.emit_state();
}
pub fn traverse_set_limit_outer(&mut self, limit: f64) {
@@ -160,34 +154,40 @@ impl Winder2 {
}
self.traverse_controller.set_limit_outer(new_outer);
self.emit_state();
}
pub fn traverse_set_step_size(&mut self, step_size: f64) {
let step_size = Length::new::<millimeter>(step_size);
self.traverse_controller.set_step_size(step_size);
self.emit_state();
}
pub fn traverse_set_padding(&mut self, padding: f64) {
let padding = Length::new::<millimeter>(padding);
self.traverse_controller.set_padding(padding);
self.emit_state();
}
pub fn traverse_goto_limit_inner(&mut self) {
if self.can_go_in() {
self.traverse_controller.goto_limit_inner();
}
self.emit_state();
}
pub fn traverse_goto_limit_outer(&mut self) {
if self.can_go_out() {
self.traverse_controller.goto_limit_outer();
}
self.emit_state();
}
pub fn traverse_goto_home(&mut self) {
if self.can_go_home() {
self.traverse_controller.goto_home();
}
self.emit_state();
}
pub fn emit_live_values(&mut self) {
@@ -224,11 +224,6 @@ impl Winder2 {
.map(|x| x.get::<millimeter>()),
puller_speed: puller_speed.get::<meter_per_minute>(),
spool_rpm,
spool_diameter: self
.spool_speed_controller
.get_estimated_radius()
.get::<millimeter>()
* 2.0,
tension_arm_angle: angle_deg,
spool_progress: self.spool_automatic_action.progress.get::<meter>(),
};
@@ -333,30 +328,9 @@ impl Winder2 {
}
}
pub fn maybe_emit_state_event(&mut self) {
let new_state = self.build_state_event();
let old_state = match &self.last_state_event {
Some(old_state) => old_state,
None => {
self.emit_state();
return;
}
};
let should_emit = check_hash_different(&new_state, old_state);
if should_emit {
let event = &new_state.build();
self.last_state_event = Some(new_state);
self.namespace.emit(Winder2Events::State(event.clone()));
}
}
pub fn emit_state(&mut self) {
let state_event = self.build_state_event();
let event = state_event.build();
self.last_state_event = Some(state_event);
self.namespace.emit(Winder2Events::State(event));
}
@@ -429,6 +403,7 @@ impl Winder2 {
self.set_puller_mode(mode);
self.set_traverse_mode(mode);
}
self.emit_state();
}
/// Apply the mode changes to the spool
@@ -481,6 +456,7 @@ impl Winder2 {
// Update the internal state
self.spool_mode = mode;
self.emit_state();
}
/// Apply the mode changes to the spool
@@ -549,6 +525,7 @@ impl Winder2 {
// Update the internal state
self.traverse_mode = mode;
self.emit_state();
}
/// Apply the mode changes to the puller
@@ -600,6 +577,7 @@ impl Winder2 {
// Update the internal state
self.puller_mode = mode;
self.emit_state();
}
}
@@ -609,6 +587,7 @@ impl Winder2 {
self.tension_arm.zero();
self.emit_live_values(); // For angle update
// For state update
self.emit_state();
}
}
-1
View File
@@ -381,7 +381,6 @@ impl MachineNewTrait for Winder2 {
machine_manager: params.machine_manager.clone(),
machine_identification_unique: machine_id,
connected_buffer: None,
last_state_event: None,
};
// initalize events
@@ -3,18 +3,12 @@ use crate::machines::winder2::{
minmax_spool_speed_controller::MinMaxSpoolSpeedController,
puller_speed_controller::PullerSpeedController,
};
use control_core::{
controllers::second_degree_motion::acceleration_position_controller::MotionControllerError,
helpers::moving_time_window::MovingTimeWindow,
};
use control_core::controllers::second_degree_motion::acceleration_position_controller::MotionControllerError;
use super::tension_arm::TensionArm;
use serde::{Deserialize, Serialize};
use std::time::{Duration, Instant};
use uom::si::{
f64::{AngularVelocity, Length},
length::meter,
};
use std::time::Instant;
use uom::si::f64::AngularVelocity;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum SpoolSpeedControllerType {
@@ -27,7 +21,6 @@ pub struct SpoolSpeedController {
adaptive_controller: AdaptiveSpoolSpeedController,
minmax_controller: MinMaxSpoolSpeedController,
r#type: SpoolSpeedControllerType,
radius_history: MovingTimeWindow<f64>,
}
impl Default for SpoolSpeedController {
@@ -41,8 +34,7 @@ impl SpoolSpeedController {
Self {
adaptive_controller: AdaptiveSpoolSpeedController::new(),
minmax_controller: MinMaxSpoolSpeedController::new(),
radius_history: MovingTimeWindow::new(Duration::from_secs(10), 40), // Example size, adjust as needed
r#type: SpoolSpeedControllerType::MinMax,
r#type: SpoolSpeedControllerType::Adaptive,
}
}
@@ -131,32 +123,13 @@ impl SpoolSpeedController {
tension_arm: &TensionArm,
puller_speed_controller: &PullerSpeedController,
) -> AngularVelocity {
let angular_velocity = match self.r#type {
match self.r#type {
SpoolSpeedControllerType::Adaptive => {
self.adaptive_controller
.update_speed(t, tension_arm, puller_speed_controller)
}
SpoolSpeedControllerType::MinMax => self.minmax_controller.update_speed(t, tension_arm),
};
{
let radius = match self.r#type {
SpoolSpeedControllerType::Adaptive => self.adaptive_controller.get_radius(),
SpoolSpeedControllerType::MinMax => {
self.minmax_controller.get_radius(puller_speed_controller)
}
};
// add radius to history
self.radius_history.update(radius.get::<meter>(), t);
}
angular_velocity
}
pub fn get_estimated_radius(&mut self) -> Length {
// Use the average of the last 10 radius measurements
Length::new::<meter>(self.radius_history.average())
}
// Adaptive controller parameter getters and setters