Compare commits
39
Commits
No files matched your search
@@ -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
@@ -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,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> {
|
||||
|
||||
@@ -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"
|
||||
);
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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(());
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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>>,
|
||||
|
||||
Generated
+5
-4
@@ -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",
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
@@ -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
|
||||
@@ -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
@@ -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
@@ -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,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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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)]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in new issue
Block a user