diff --git a/docs/rest-api.md b/docs/rest-api.md new file mode 100644 index 000000000..b9ea48f27 --- /dev/null +++ b/docs/rest-api.md @@ -0,0 +1,235 @@ +# REST API `/api/v2` + +The Qitech Control Panel exposes a small HTTP interface for discovering machines and reading their current values. All examples below assume you are connected to the panel’s Ethernet subnet and talk directly to the panel at: + +- **Base URL:** `http://10.10.10.1:3001` + +> **Schema note:** This documentation intentionally stays light on field details beyond what is shown in the examples. For the exact datatypes and complete payload shapes, refer to the corresponding Rust types (linked below). + +--- + +## Authentication + +The panel does **not** perform HTTP authentication. The expected security model is **network-level isolation**: anything that can send packets to the panel’s Ethernet interface is treated as trusted. This matches common security assumptions in EtherCAT-style control networks. + +The panel is configured to administer its own subnet `10.10.10.0/24` via DHCP, while Wi-Fi can be used for upstream internet connectivity (if configured). If you need authentication or access from outside the isolated subnet, place a router/device in **client/bridge mode** on the panel network and expose the panel through a **reverse proxy** where you can add authentication, logging, rate limiting, etc. + +If DNS is available on the subnet, you may be able to resolve `qitech.control`; otherwise use the static address `10.10.10.1`. + +--- + +## List machines `GET /api/v2/machine` + +Returns the set of machines currently known/connected to the panel. + +Machines are identified by: + +- `slug`: the machine type / model identifier (string) +- `serial`: the specific machine instance identifier (int) + +Each machine also includes a `legacy_id` to support older **v1** workflows. If a machine reports an issue, the `error` field may be present and non-null (containing an error message). + +### Example request + +```bash +curl -X GET "http://10.10.10.1:3001/api/v2/machine" +``` + +### Example response + +```json +{ + "machines": [ + { + "legacy_id": { + "machine_identification": { + "vendor": 1, + "machine": 7 + }, + "serial": 57922 + }, + "serial": 57922, + "vendor": "QiTech", + "slug": "mock", + "error": null + }, + { + "legacy_id": { + "machine_identification": { + "vendor": 1, + "machine": 4 + }, + "serial": 57922 + }, + "serial": 57922, + "vendor": "QiTech", + "slug": "extruder_v1", + "error": null + }, + { + "legacy_id": { + "machine_identification": { + "vendor": 1, + "machine": 2 + }, + "serial": 57922 + }, + "serial": 57922, + "vendor": "QiTech", + "slug": "winder_v1", + "error": null + }, + { + "legacy_id": { + "machine_identification": { + "vendor": 1, + "machine": 10 + }, + "serial": 48879 + }, + "serial": 48879, + "vendor": "QiTech", + "slug": "wago_power_v1", + "error": null + } + ] +} +``` + +--- + +## Get current values `GET /api/v2/machine//` + +Returns all currently known values for a single machine. + +Values are categorized into two groups: + +- **State**: requested/commanded values (these typically change only after a state-change request, or if another controller updates them) +- **Live Values**: measured/observed values coming from the machine and potentially changing quickly + +This REST endpoint returns **only the current snapshot**, not a stream of live values. +To receive continuous updates (via WebSockets), subscribe to the machine namespace (see **WebSockets** below). + +### Example request (mock machine) + +```bash +curl -X GET "http://10.10.10.1:3001/api/v2/machine/mock/57922" +``` + +### Example response + +```json +{ + "machine": { + "legacy_id": { + "machine_identification": { + "vendor": 1, + "machine": 7 + }, + "serial": 57922 + }, + "serial": 57922, + "vendor": "QiTech", + "slug": "mock", + "error": null + }, + "state": { + "frequency1": 100.0, + "frequency2": 200.0, + "frequency3": 500.0, + "is_default_state": false, + "mode_state": { + "mode": "Running" + } + }, + "live_values": { + "amplitude1": -0.03438523433309566, + "amplitude2": -0.06872980145477608, + "amplitude3": -0.1711138370170743, + "amplitude_sum": -0.27422887280494607 + } +} +``` + +--- + +## Change machine state `POST /api/v1/machine//` + +State changes are submitted as **mutations**. The mutation payload is defined per machine type in Rust. Conceptually, each item in the mutation list represents a setter-style operation that is applied by the real-time control loop. + +The API does **not** return the newly-applied state in the POST response. The panel runs a real-time loop and generally won’t block waiting for the physical system to converge. Instead: + +- Submit the mutation via `POST` +- Poll `GET /api/v2/machine//` to observe the updated state and/or any reported errors + +### Example request (mock machine) + +```bash +curl -X POST \ + -d \ + -H "Content-Type: application/json" \ + "http://10.10.10.1:3001/api/v1/machine/mock/57922" +``` + +### Example response + +```json +null +``` + +--- + +## WebSockets + +For continuous updates, subscribe to a machine-specific namespace derived from its `legacy_id`: + +- Namespace: `/machine///` + +The stream emits events for: + +- state changes (`StateEvent`) +- live value updates (`LiveValuesEvent`) + +Both event payloads use the same machine-specific schema as the `/api/v2` REST responses. + +--- + +## List of all machines + +Below is a template you can fill with links to the relevant Rust types (mutations + state/live structs). +For each machine, link to: + +- **Mutations:** the request payload type used by `POST /api/v1/machine//` +- **State / Live Values:** the response payload types returned by `GET /api/v2/machine//` + +### Machines + +- **winder_v1** + + - Mutations: + - State: + - Live Values: + +- **extruder_v1** + + - Mutations: + - State: + - Live Values: + +- **laser_v1** + + - Mutations: + - State: + - Live Values: + +- **mock** + + - Mutations: + - State: + - Live Values: + +- **extruder_v2** + + - Mutations: + - State: + - Live Values: diff --git a/machines/src/analog_input_test_machine/act.rs b/machines/src/analog_input_test_machine/act.rs index 2637f147c..7e69b2b78 100644 --- a/machines/src/analog_input_test_machine/act.rs +++ b/machines/src/analog_input_test_machine/act.rs @@ -1,21 +1,32 @@ use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH}; -use crate::{MachineAct, analog_input_test_machine::AnalogInputTestMachine}; +use crate::{ + MachineAct, MachineMessage, MachineValues, analog_input_test_machine::AnalogInputTestMachine, +}; impl MachineAct for AnalogInputTestMachine { - fn act_machine_message(&mut self, msg: crate::MachineMessage) { + fn act_machine_message(&mut self, msg: MachineMessage) { match msg { - crate::MachineMessage::SubscribeNamespace(namespace) => { + MachineMessage::SubscribeNamespace(namespace) => { self.namespace.namespace = Some(namespace); self.emit_measurement_rate(); } - crate::MachineMessage::UnsubscribeNamespace => self.namespace.namespace = None, - crate::MachineMessage::HttpApiJsonRequest(value) => { + MachineMessage::UnsubscribeNamespace => self.namespace.namespace = None, + MachineMessage::HttpApiJsonRequest(value) => { use crate::MachineApi; let _res = self.api_mutate(value); } crate::MachineMessage::ConnectToMachine(_machine_connection) => {} - crate::MachineMessage::DisconnectMachine(_machine_connection) => {} + MachineMessage::DisconnectMachine(_machine_connection) => {} + MachineMessage::RequestValues(sender) => { + sender + .send_blocking(MachineValues { + state: serde_json::Value::Null, + live_values: serde_json::Value::Null, + }) + .expect("Failed to send values"); + sender.close(); + } } } diff --git a/machines/src/aquapath1/act.rs b/machines/src/aquapath1/act.rs index 78f157ce7..fb7711b7a 100644 --- a/machines/src/aquapath1/act.rs +++ b/machines/src/aquapath1/act.rs @@ -1,5 +1,5 @@ use super::{AquaPathV1, AquaPathV1Mode}; -use crate::{MachineAct, MachineMessage}; +use crate::{MachineAct, MachineMessage, MachineValues}; use std::time::{Duration, Instant}; impl MachineAct for AquaPathV1 { @@ -45,14 +45,21 @@ impl MachineAct for AquaPathV1 { let _res = self.api_mutate(value); } MachineMessage::ConnectToMachine(_machine_connection) => - /*Doesnt connect to any Machine so do nothing*/ - { - () - } + /*Doesnt connect to any Machine so do nothing*/ + {} MachineMessage::DisconnectMachine(_machine_connection) => - /*Doesnt connect to any Machine so do nothing*/ - { - () + /*Doesnt connect to any Machine so do nothing*/ + {} + MachineMessage::RequestValues(sender) => { + sender + .send_blocking(MachineValues { + state: serde_json::to_value(self.get_state()) + .expect("Failed to serialize state"), + live_values: serde_json::to_value(self.get_live_values()) + .expect("Failed to serialize live values"), + }) + .expect("Failed to send values"); + sender.close(); } } } diff --git a/machines/src/aquapath1/mod.rs b/machines/src/aquapath1/mod.rs index 204367b92..6ef6cfad4 100644 --- a/machines/src/aquapath1/mod.rs +++ b/machines/src/aquapath1/mod.rs @@ -109,8 +109,8 @@ impl std::fmt::Display for AquaPathV1 { } impl AquaPathV1 { - pub fn emit_live_values(&mut self) { - let live_values = LiveValuesEvent { + pub fn get_live_values(&self) -> LiveValuesEvent { + LiveValuesEvent { front_temperature: self .front_controller .current_temperature @@ -123,13 +123,16 @@ impl AquaPathV1 { back_flow: self.back_controller.current_flow.get::(), front_temp_reservoir: self.front_controller.temp_reservoir.get::(), back_temp_reservoir: self.back_controller.temp_reservoir.get::(), - }; - let event = live_values.build(); + } + } + + pub fn emit_live_values(&mut self) { + let event = self.get_live_values().build(); self.namespace.emit(AquaPathV1Events::LiveValues(event)); } - pub fn emit_state(&mut self) { - let state = StateEvent { + pub fn get_state(&self) -> StateEvent { + StateEvent { is_default_state: false, mode_state: ModeState { mode: self.mode.clone(), @@ -167,9 +170,11 @@ impl AquaPathV1 { should_flow: self.back_controller.should_pump, }, }, - }; + } + } - let event = state.build(); + pub fn emit_state(&mut self) { + let event = self.get_state().build(); self.namespace.emit(AquaPathV1Events::State(event)); } } diff --git a/machines/src/buffer1/act.rs b/machines/src/buffer1/act.rs index 273a34df4..835fd3637 100644 --- a/machines/src/buffer1/act.rs +++ b/machines/src/buffer1/act.rs @@ -1,5 +1,5 @@ use super::BufferV1; -use crate::{MachineAct, MachineMessage}; +use crate::{MachineAct, MachineMessage, MachineValues}; use std::time::{Duration, Instant}; impl MachineAct for BufferV1 { @@ -33,14 +33,21 @@ impl MachineAct for BufferV1 { let _res = self.api_mutate(value); } MachineMessage::ConnectToMachine(_machine_connection) => - /*Doesnt connect to any Machine so do nothing*/ - { - () - } + /*Doesnt connect to any Machine so do nothing*/ + {} MachineMessage::DisconnectMachine(_machine_connection) => - /*Doesnt connect to any Machine so do nothing*/ - { - () + /*Doesnt connect to any Machine so do nothing*/ + {} + MachineMessage::RequestValues(sender) => { + sender + .send_blocking(MachineValues { + state: serde_json::to_value(self.get_state()) + .expect("Failed to serialize state"), + live_values: serde_json::to_value(self.get_live_values()) + .expect("Failed to serialize live values"), + }) + .expect("Failed to send values"); + sender.close(); } } } diff --git a/machines/src/buffer1/mod.rs b/machines/src/buffer1/mod.rs index cd58387e4..61b0cf030 100644 --- a/machines/src/buffer1/mod.rs +++ b/machines/src/buffer1/mod.rs @@ -48,21 +48,27 @@ impl BufferV1 { vendor: VENDOR_QITECH, machine: MACHINE_BUFFER_V1, }; - pub fn emit_live_values(&mut self) { - let live_values = LiveValuesEvent {}; - let event = live_values.build(); + pub fn get_live_values(&mut self) -> LiveValuesEvent { + LiveValuesEvent {} + } + + pub fn emit_live_values(&mut self) { + let event = self.get_live_values().build(); self.namespace.emit(BufferV1Events::LiveValues(event)); } - pub fn emit_state(&mut self) { - let state = StateEvent { + + pub fn get_state(&self) -> StateEvent { + StateEvent { mode_state: ModeState { mode: self.mode.clone(), }, // connected_machine_state: self.connected_winder.to_state(), - }; + } + } - let event = state.build(); + pub fn emit_state(&mut self) { + let event = self.get_state().build(); self.namespace.emit(BufferV1Events::State(event)); } diff --git a/machines/src/extruder1/act.rs b/machines/src/extruder1/act.rs index 1fcf141cb..ed62784d7 100644 --- a/machines/src/extruder1/act.rs +++ b/machines/src/extruder1/act.rs @@ -1,7 +1,7 @@ #[cfg(not(feature = "mock-machine"))] use crate::extruder1::ExtruderV2; #[cfg(not(feature = "mock-machine"))] -use crate::{MachineAct, MachineMessage}; +use crate::{MachineAct, MachineMessage, MachineValues}; #[cfg(not(feature = "mock-machine"))] use std::time::{Duration, Instant}; @@ -41,6 +41,7 @@ impl MachineAct for ExtruderV2 { // 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.update_total_energy(now); self.maybe_emit_state_event(); // Emit live values at 30 FPS self.emit_live_values(); @@ -61,11 +62,20 @@ impl MachineAct for ExtruderV2 { let _res = self.api_mutate(value); } - MachineMessage::ConnectToMachine(_machine_connection) => (), + MachineMessage::ConnectToMachine(_machine_connection) => {} MachineMessage::DisconnectMachine(_machine_connection) => - /*Doesnt connect to any Machine so do nothing*/ - { - () + /*Doesnt connect to any Machine so do nothing*/ + {} + MachineMessage::RequestValues(sender) => { + sender + .send_blocking(MachineValues { + state: serde_json::to_value(self.get_state()) + .expect("Failed to serialize state"), + live_values: serde_json::to_value(self.get_live_values()) + .expect("Failed to serialize live values"), + }) + .expect("Failed to send values"); + sender.close(); } } } diff --git a/machines/src/extruder1/emit.rs b/machines/src/extruder1/emit.rs index 2e673eda3..7246c08b3 100644 --- a/machines/src/extruder1/emit.rs +++ b/machines/src/extruder1/emit.rs @@ -25,11 +25,11 @@ use units::{angular_velocity::revolution_per_minute, thermodynamic_temperature:: #[cfg(not(feature = "mock-machine"))] impl ExtruderV2 { - pub fn build_state_event(&mut self) -> StateEvent { - use crate::extruder1::api::{TemperaturePid, TemperaturePidStates}; + pub fn get_state(&self) -> StateEvent { + use crate::extruder1::api::TemperaturePidStates; StateEvent { - is_default_state: !std::mem::replace(&mut self.emitted_default_state, true), + is_default_state: !self.emitted_default_state, rotation_state: RotationState { forward: self.screw_speed_controller.get_rotation_direction(), }, @@ -141,16 +141,14 @@ impl ExtruderV2 { }, } } -} -#[cfg(not(feature = "mock-machine"))] -impl ExtruderV2 { pub fn emit_state(&mut self) { - let state = self.build_state_event(); + let state = self.get_state(); 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)); + self.emitted_default_state = true; } pub fn maybe_emit_state_event(&mut self) { @@ -168,13 +166,8 @@ impl ExtruderV2 { } } - pub fn emit_live_values(&mut self) { - use std::time::Instant; - let now = Instant::now(); - let combined_power = self.calculate_combined_power(); - self.update_total_energy(combined_power, now); - - let live_values = LiveValuesEvent { + pub fn get_live_values(&self) -> LiveValuesEvent { + LiveValuesEvent { motor_status: self.screw_speed_controller.get_motor_status().into(), pressure: self.screw_speed_controller.get_pressure().get::(), nozzle_temperature: self @@ -209,11 +202,13 @@ impl ExtruderV2 { middle_power: self .temperature_controller_middle .get_heating_element_wattage(), - combined_power, + combined_power: self.calculate_combined_power(), total_energy_kwh: self.total_energy_kwh, - }; + } + } - let event = live_values.build(); + pub fn emit_live_values(&mut self) { + let event = self.get_live_values().build(); self.namespace.emit(ExtruderV2Events::LiveValues(event)); } diff --git a/machines/src/extruder1/mock/act.rs b/machines/src/extruder1/mock/act.rs index fcb0298cd..cd4ab27cc 100644 --- a/machines/src/extruder1/mock/act.rs +++ b/machines/src/extruder1/mock/act.rs @@ -1,6 +1,6 @@ use super::ExtruderV2; use crate::MachineAct; -use crate::MachineMessage; +use crate::{MachineMessage, MachineValues}; use std::time::{Duration, Instant}; impl MachineAct for ExtruderV2 { @@ -40,6 +40,17 @@ impl MachineAct for ExtruderV2 { { () } + MachineMessage::RequestValues(sender) => { + sender + .send_blocking(MachineValues { + state: serde_json::to_value(self.build_state_event()) + .expect("Failed to serialize state"), + live_values: serde_json::to_value(self.get_live_values()) + .expect("Failed to serialize live values"), + }) + .expect("Failed to send values"); + sender.close(); + } } } } diff --git a/machines/src/extruder1/mock/mock_emit.rs b/machines/src/extruder1/mock/mock_emit.rs index ffbfd8622..a7249aca2 100644 --- a/machines/src/extruder1/mock/mock_emit.rs +++ b/machines/src/extruder1/mock/mock_emit.rs @@ -51,8 +51,8 @@ impl ExtruderV2 { } } - pub fn emit_live_values(&mut self) { - let live_values = LiveValuesEvent { + pub fn get_live_values(&mut self) -> LiveValuesEvent { + LiveValuesEvent { motor_status: self.motor_status.clone(), pressure: self.pressure, nozzle_temperature: self.nozzle_temperature, @@ -65,9 +65,11 @@ impl ExtruderV2 { middle_power: self.middle_power, combined_power: self.combined_power, total_energy_kwh: self.total_energy_kwh, - }; + } + } - let event = live_values.build(); + pub fn emit_live_values(&mut self) { + let event = self.get_live_values().build(); self.namespace.emit(ExtruderV2Events::LiveValues(event)); } diff --git a/machines/src/extruder1/mod.rs b/machines/src/extruder1/mod.rs index 3f0c37c05..5b4eb812d 100644 --- a/machines/src/extruder1/mod.rs +++ b/machines/src/extruder1/mod.rs @@ -100,6 +100,9 @@ pub struct ExtruderV2 { emitted_default_state: bool, } +#[cfg(feature = "mock-machine")] +pub use mock::ExtruderV2; + #[cfg(not(feature = "mock-machine"))] impl Machine for ExtruderV2 { fn get_machine_identification_unique(&self) -> MachineIdentificationUnique { @@ -129,7 +132,7 @@ impl ExtruderV2 { #[cfg(not(feature = "mock-machine"))] impl ExtruderV2 { /// Calculate combined power consumption in watts - fn calculate_combined_power(&mut self) -> f64 { + fn calculate_combined_power(&self) -> f64 { let motor_power = { let motor_status = &self.screw_speed_controller.inverter.motor_status; let voltage = motor_status.voltage.get::(); @@ -153,10 +156,11 @@ impl ExtruderV2 { } /// Update total energy consumption in kWh - fn update_total_energy(&mut self, current_power_watts: f64, now: Instant) { + fn update_total_energy(&mut self, now: Instant) { + let power_watts = self.calculate_combined_power(); 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; + let energy_delta_kwh = (power_watts / 1000.0) * time_delta_hours; self.total_energy_kwh += energy_delta_kwh; } self.last_energy_calculation_time = Some(now); diff --git a/machines/src/extruder1/screw_speed_controller.rs b/machines/src/extruder1/screw_speed_controller.rs index 65f0b4b73..a66cd1a17 100644 --- a/machines/src/extruder1/screw_speed_controller.rs +++ b/machines/src/extruder1/screw_speed_controller.rs @@ -77,11 +77,11 @@ impl ScrewSpeedController { self.nozzle_pressure_limit = pressure; } - pub fn get_nozzle_pressure_limit(&mut self) -> Pressure { + pub fn get_nozzle_pressure_limit(&self) -> Pressure { self.nozzle_pressure_limit } - pub const fn get_nozzle_pressure_limit_enabled(&mut self) -> bool { + pub const fn get_nozzle_pressure_limit_enabled(&self) -> bool { self.nozzle_pressure_limit_enabled } @@ -89,11 +89,11 @@ impl ScrewSpeedController { self.nozzle_pressure_limit_enabled = enabled; } - pub fn get_target_rpm(&mut self) -> AngularVelocity { + pub fn get_target_rpm(&self) -> AngularVelocity { self.target_rpm } - pub const fn get_rotation_direction(&mut self) -> bool { + pub const fn get_rotation_direction(&self) -> bool { self.forward_rotation } @@ -129,7 +129,7 @@ impl ScrewSpeedController { self.inverter.set_frequency_target(target_frequency); } - pub const fn get_uses_rpm(&mut self) -> bool { + pub const fn get_uses_rpm(&self) -> bool { self.uses_rpm } @@ -148,7 +148,7 @@ impl ScrewSpeedController { self.motor_on = true; } - pub fn get_motor_status(&mut self) -> MotorStatus { + pub fn get_motor_status(&self) -> MotorStatus { let frequency = self.inverter.motor_status.frequency; let rpm = AngularVelocity::new::(frequency.get::()); @@ -197,7 +197,7 @@ impl ScrewSpeedController { self.pid.reset() } - pub fn get_pressure(&mut self) -> Pressure { + pub fn get_pressure(&self) -> Pressure { let current_result = self.get_sensor_current(); let current = match current_result { Ok(current) => current.get::(), diff --git a/machines/src/extruder1/temperature_controller.rs b/machines/src/extruder1/temperature_controller.rs index 323e29dcc..976638d1b 100644 --- a/machines/src/extruder1/temperature_controller.rs +++ b/machines/src/extruder1/temperature_controller.rs @@ -70,7 +70,7 @@ impl TemperatureController { self.heating_allowed = true; } - pub fn get_heating_element_wattage(&mut self) -> f64 { + pub fn get_heating_element_wattage(&self) -> f64 { self.temperature_pid_output * self.heating_element_wattage } diff --git a/machines/src/extruder2/act.rs b/machines/src/extruder2/act.rs index 71f2c9934..a38e511d8 100644 --- a/machines/src/extruder2/act.rs +++ b/machines/src/extruder2/act.rs @@ -2,7 +2,7 @@ use std::time::Instant; #[cfg(not(feature = "mock-machine"))] -use crate::{MachineAct, MachineMessage}; +use crate::{MachineAct, MachineMessage, MachineValues}; #[cfg(not(feature = "mock-machine"))] use super::ExtruderV3; @@ -65,11 +65,20 @@ impl MachineAct for ExtruderV3 { let _res = self.api_mutate(value); } - MachineMessage::ConnectToMachine(_machine_connection) => (), + MachineMessage::ConnectToMachine(_machine_connection) => {} MachineMessage::DisconnectMachine(_machine_connection) => - /*Doesnt connect to any Machine so do nothing*/ - { - () + /*Doesnt connect to any Machine so do nothing*/ + {} + MachineMessage::RequestValues(sender) => { + sender + .send_blocking(MachineValues { + state: serde_json::to_value(self.build_state_event()) + .expect("Failed to serialize state"), + live_values: serde_json::to_value(self.get_live_values()) + .expect("Failed to serialize live values"), + }) + .expect("Failed to send values"); + sender.close(); } } } diff --git a/machines/src/extruder2/emit.rs b/machines/src/extruder2/emit.rs index a062efba4..d409e36b9 100644 --- a/machines/src/extruder2/emit.rs +++ b/machines/src/extruder2/emit.rs @@ -9,6 +9,8 @@ use crate::extruder1::{ }, }; #[cfg(not(feature = "mock-machine"))] +use crate::extruder2::api::{LiveValuesEvent, StateEvent}; +#[cfg(not(feature = "mock-machine"))] use control_core::helpers::hasher_serializer::hash_with_serde_model; #[cfg(not(feature = "mock-machine"))] use control_core::socketio::event::BuildEvent; @@ -24,7 +26,7 @@ use units::thermodynamic_temperature::ThermodynamicTemperature; use units::{angular_velocity::revolution_per_minute, thermodynamic_temperature::degree_celsius}; #[cfg(not(feature = "mock-machine"))] -use super::{ExtruderV3, ExtruderV3Mode, api::StateEvent}; +use super::{ExtruderV3, ExtruderV3Mode}; #[cfg(not(feature = "mock-machine"))] impl ExtruderV3 { @@ -176,15 +178,14 @@ impl ExtruderV3 { } } - pub fn emit_live_values(&mut self) { + pub fn get_live_values(&mut self) -> LiveValuesEvent { use std::time::Instant; - use crate::extruder2::api::{ExtruderV3Events, LiveValuesEvent}; let now = Instant::now(); let combined_power = self.calculate_combined_power(); self.update_total_energy(combined_power, now); - let live_values = LiveValuesEvent { + LiveValuesEvent { motor_status: self.screw_speed_controller.get_motor_status().into(), pressure: self.screw_speed_controller.get_pressure().get::(), nozzle_temperature: self @@ -221,9 +222,13 @@ impl ExtruderV3 { .get_heating_element_wattage(), combined_power, total_energy_kwh: self.total_energy_kwh, - }; + } + } + + pub fn emit_live_values(&mut self) { + use crate::extruder2::api::ExtruderV3Events; - let event = live_values.build(); + let event = self.get_live_values().build(); self.namespace.emit(ExtruderV3Events::LiveValues(event)); } diff --git a/machines/src/extruder2/mock/act.rs b/machines/src/extruder2/mock/act.rs index fcb0298cd..ea79c88c5 100644 --- a/machines/src/extruder2/mock/act.rs +++ b/machines/src/extruder2/mock/act.rs @@ -1,6 +1,5 @@ use super::ExtruderV2; -use crate::MachineAct; -use crate::MachineMessage; +use crate::{MachineAct, MachineMessage, MachineValues}; use std::time::{Duration, Instant}; impl MachineAct for ExtruderV2 { @@ -34,11 +33,20 @@ impl MachineAct for ExtruderV2 { let _res = self.api_mutate(value); } - MachineMessage::ConnectToMachine(_machine_connection) => (), + MachineMessage::ConnectToMachine(_machine_connection) => {} MachineMessage::DisconnectMachine(_machine_connection) => - /*Doesnt connec to any Machine do nothing*/ - { - () + /*Doesnt connec to any Machine do nothing*/ + {} + MachineMessage::RequestValues(sender) => { + sender + .send_blocking(MachineValues { + state: serde_json::to_value(self.get_state()) + .expect("Failed to serialize state"), + live_values: serde_json::to_value(self.get_live_values()) + .expect("Failed to serialize live values"), + }) + .expect("Failed to send values"); + sender.close(); } } } diff --git a/machines/src/extruder2/mock/mock_emit.rs b/machines/src/extruder2/mock/mock_emit.rs index 24f36c1a1..1bd50b2bb 100644 --- a/machines/src/extruder2/mock/mock_emit.rs +++ b/machines/src/extruder2/mock/mock_emit.rs @@ -10,10 +10,9 @@ use control_core::{ }; impl ExtruderV2 { - pub fn build_state_event(&mut self) -> StateEvent { - // bad performance wise, but doesnt matter its only a mock machine + pub fn get_state(&self) -> StateEvent { StateEvent { - is_default_state: !std::mem::replace(&mut self.emitted_default_state, true), + is_default_state: !self.emitted_default_state, rotation_state: self.rotation_state.clone(), mode_state: self.mode_state.clone(), regulation_state: self.regulation_state.clone(), @@ -25,13 +24,12 @@ impl ExtruderV2 { pid_settings: self.pid_settings.clone(), } } -} -impl ExtruderV2 { pub fn emit_state(&mut self) { - let state = self.build_state_event(); + let state = self.get_state(); let hash = hash_with_serde_model(self.inverter_status_state.clone()); self.last_status_hash = Some(hash); + self.emitted_default_state = true; let event = state.build(); self.namespace.emit(ExtruderV2Events::State(event)); } @@ -51,8 +49,8 @@ impl ExtruderV2 { } } - pub fn emit_live_values(&mut self) { - let live_values = LiveValuesEvent { + pub fn get_live_values(&self) -> LiveValuesEvent { + LiveValuesEvent { motor_status: self.motor_status.clone(), pressure: self.pressure, nozzle_temperature: self.nozzle_temperature, @@ -65,9 +63,11 @@ impl ExtruderV2 { middle_power: self.middle_power, combined_power: self.combined_power, total_energy_kwh: self.total_energy_kwh, - }; + } + } - let event = live_values.build(); + pub fn emit_live_values(&mut self) { + let event = self.get_live_values().build(); self.namespace.emit(ExtruderV2Events::LiveValues(event)); } diff --git a/machines/src/ip20_test_machine/act.rs b/machines/src/ip20_test_machine/act.rs index ac87b37d0..86af7c69f 100644 --- a/machines/src/ip20_test_machine/act.rs +++ b/machines/src/ip20_test_machine/act.rs @@ -1,5 +1,5 @@ use super::IP20TestMachine; -use crate::{MachineAct, MachineMessage}; +use crate::{MachineAct, MachineMessage, MachineValues}; use std::time::{Duration, Instant}; impl MachineAct for IP20TestMachine { @@ -42,6 +42,17 @@ impl MachineAct for IP20TestMachine { MachineMessage::DisconnectMachine(_machine_connection) => { // Does not connect to any Machine; do nothing } + MachineMessage::RequestValues(sender) => { + sender + .send_blocking(MachineValues { + state: serde_json::to_value(self.get_state()) + .expect("Failed to serialize state"), + live_values: serde_json::to_value(self.get_live_values()) + .expect("Failed to serialize live values"), + }) + .expect("Failed to send values"); + sender.close(); + } } } } diff --git a/machines/src/ip20_test_machine/mod.rs b/machines/src/ip20_test_machine/mod.rs index 53cefd61a..33bc99af8 100644 --- a/machines/src/ip20_test_machine/mod.rs +++ b/machines/src/ip20_test_machine/mod.rs @@ -45,21 +45,25 @@ impl IP20TestMachine { } impl IP20TestMachine { - pub fn emit_state(&mut self) { - let event = StateEvent { + pub fn get_state(&self) -> StateEvent { + StateEvent { outputs: self.outputs, } - .build(); + } + pub fn emit_state(&mut self) { + let event = self.get_state().build(); self.namespace.emit(IP20TestMachineEvents::State(event)); } - pub fn emit_live_values(&mut self) { - let event = LiveValuesEvent { + pub fn get_live_values(&self) -> LiveValuesEvent { + LiveValuesEvent { inputs: self.inputs, } - .build(); + } + pub fn emit_live_values(&mut self) { + let event = self.get_live_values().build(); self.namespace .emit(IP20TestMachineEvents::LiveValues(event)); } diff --git a/machines/src/laser/act.rs b/machines/src/laser/act.rs index 501dafc20..2dbb5ca6d 100644 --- a/machines/src/laser/act.rs +++ b/machines/src/laser/act.rs @@ -1,6 +1,7 @@ use super::LaserMachine; use crate::MachineAct; use crate::MachineMessage; +use crate::MachineValues; use std::time::{Duration, Instant}; /// Implements the `MachineAct` trait for the `LaserMachine`. @@ -41,7 +42,6 @@ impl MachineAct for LaserMachine { } fn act_machine_message(&mut self, msg: MachineMessage) { - tracing::info!("{:?}", msg); match msg { MachineMessage::SubscribeNamespace(namespace) => { self.namespace.namespace = Some(namespace); @@ -61,13 +61,22 @@ impl MachineAct for LaserMachine { let _res = self.api_mutate(value); } MachineMessage::ConnectToMachine(_machine_connection) => - /*Doesnt connect to any Machine so do nothing*/ - { - () - } + /*Doesnt connect to any Machine so do nothing*/ + {} MachineMessage::DisconnectMachine(_machine_connection) => - /*Doesnt connect to any Machine so do nothing*/ - { + /*Doesnt connect to any Machine so do nothing*/ + {} + MachineMessage::RequestValues(sender) => { + sender + .send_blocking(MachineValues { + state: serde_json::to_value(self.get_state()) + .expect("Failed to serialize state"), + live_values: serde_json::to_value(self.get_live_values()) + .expect("Failed to serialize live values"), + }) + .expect("Failed to send values"); + sender.close(); + () } } diff --git a/machines/src/laser/mod.rs b/machines/src/laser/mod.rs index 26ffc0a0d..5613e86b0 100644 --- a/machines/src/laser/mod.rs +++ b/machines/src/laser/mod.rs @@ -97,22 +97,24 @@ impl LaserMachine { machine: MACHINE_LASER_V1, }; - ///diameter in mm - pub fn emit_live_values(&mut self) { + pub fn get_live_values(&self) -> LiveValuesEvent { let diameter = self.diameter.get::(); let x_diameter = self.x_diameter.map(|x| x.get::()); let y_diameter = self.y_diameter.map(|y| y.get::()); let roundness = self.roundness; - let live_values = LiveValuesEvent { + LiveValuesEvent { diameter, x_diameter, y_diameter, roundness, - }; + } + } - self.namespace - .emit(LaserEvents::LiveValues(live_values.build())); + ///diameter in mm + pub fn emit_live_values(&mut self) { + let event = self.get_live_values().build(); + self.namespace.emit(LaserEvents::LiveValues(event)); } pub fn build_state_event(&self) -> StateEvent { @@ -129,19 +131,23 @@ impl LaserMachine { } } - pub fn emit_state(&mut self) { - let state = StateEvent { - is_default_state: !std::mem::replace(&mut self.emitted_default_state, true), + pub fn get_state(&self) -> StateEvent { + StateEvent { + is_default_state: !self.emitted_default_state, laser_state: LaserState { higher_tolerance: self.laser_target.higher_tolerance.get::(), lower_tolerance: self.laser_target.lower_tolerance.get::(), target_diameter: self.laser_target.diameter.get::(), in_tolerance: self.in_tolerance, }, - }; + } + } - self.namespace.emit(LaserEvents::State(state.build())); + pub fn emit_state(&mut self) { + let event = self.get_state().build(); + self.namespace.emit(LaserEvents::State(event)); self.did_change_state = false; + self.emitted_default_state = true; } pub fn set_higher_tolerance(&mut self, higher_tolerance: f64) { diff --git a/machines/src/lib.rs b/machines/src/lib.rs index 29d39a5eb..2fd00c8e6 100644 --- a/machines/src/lib.rs +++ b/machines/src/lib.rs @@ -13,8 +13,10 @@ use machine_identification::{ use serde::Serialize; use smol::channel::{Receiver, Sender}; use socketioxide::extract::SocketRef; +use std::any::Any; use std::fmt::Debug; -use std::{any::Any, sync::Arc, time::Instant}; +use std::sync::Arc; +use std::time::Instant; pub mod analog_input_test_machine; pub mod aquapath1; #[cfg(not(feature = "mock-machine"))] @@ -36,7 +38,6 @@ pub const MACHINE_WINDER_V1: u16 = 0x0002; pub const MACHINE_EXTRUDER_V1: u16 = 0x0004; pub const MACHINE_LASER_V1: u16 = 0x0006; pub const MACHINE_MOCK: u16 = 0x0007; -#[cfg(not(feature = "mock-machine"))] pub const MACHINE_BUFFER_V1: u16 = 0x0008; pub const MACHINE_AQUAPATH_V1: u16 = 0x0009; pub const MACHINE_WAGO_POWER_V1: u16 = 0x000A; @@ -276,6 +277,12 @@ pub trait MachineAct { fn act(&mut self, now: Instant); } +#[derive(Serialize, Debug, Clone)] +pub struct MachineValues { + pub state: serde_json::Value, + pub live_values: serde_json::Value, +} + // generic MachineMessage allows us to implement actions // to manage or mutate machines with simple messages sent to the Recv Channel of the given Machine, // which the machine itself will handle to avoid locking @@ -287,6 +294,7 @@ pub enum MachineMessage { HttpApiJsonRequest(serde_json::Value), ConnectToMachine(MachineConnection), DisconnectMachine(MachineConnection), + RequestValues(Sender), } pub trait MachineApi { @@ -447,6 +455,9 @@ where } pub trait MachineWithChannel: Send + Debug + Sync { + type State: serde::Serialize; + type LiveValues: serde::Serialize; + fn get_machine_channel(&self) -> &MachineChannel; fn get_machine_channel_mut(&mut self) -> &mut MachineChannel; @@ -454,6 +465,11 @@ pub trait MachineWithChannel: Send + Debug + Sync { fn update(&mut self, now: std::time::Instant) -> Result<()>; fn mutate(&mut self, value: Value) -> Result<()>; + + fn get_state(&self) -> Self::State; + fn get_live_values(&self) -> Option { + None + } } impl MachineApi for C @@ -513,6 +529,17 @@ where MachineMessage::DisconnectMachine(_machine_connection) => { todo!(); } + MachineMessage::RequestValues(sender) => { + sender + .send_blocking(MachineValues { + state: serde_json::to_value(self.get_state()) + .expect("Failed to serialize state"), + live_values: serde_json::to_value(self.get_live_values()) + .expect("Failed to serialize live values"), + }) + .expect("Failed to send values"); + sender.close(); + } } } } diff --git a/machines/src/machine_identification.rs b/machines/src/machine_identification.rs index 7895ca7ec..2de7f6e86 100644 --- a/machines/src/machine_identification.rs +++ b/machines/src/machine_identification.rs @@ -41,6 +41,30 @@ impl MachineIdentification { pub const fn is_valid(&self) -> bool { self.vendor != 0 && self.machine != 0 } + + pub fn vendor_str(&self) -> String { + match self.vendor { + x if x == VENDOR_QITECH => "QiTech".to_string(), + _ => "N/A".to_string(), + } + } + + pub fn slug(&self) -> String { + match self.machine { + x if x == MACHINE_WINDER_V1 => "winder_v1".to_string(), + x if x == MACHINE_EXTRUDER_V1 => "extruder_v1".to_string(), + x if x == MACHINE_LASER_V1 => "laser_v1".to_string(), + x if x == MACHINE_MOCK => "mock".to_string(), + x if x == MACHINE_AQUAPATH_V1 => "aquapath_v1".to_string(), + x if x == MACHINE_BUFFER_V1 => "buffer_v1".to_string(), + x if x == MACHINE_EXTRUDER_V2 => "extruder_v2".to_string(), + x if x == MACHINE_WAGO_POWER_V1 => "wago_power_v1".to_string(), + x if x == TEST_MACHINE => "test_machine".to_string(), + x if x == IP20_TEST_MACHINE => "ip20_test_machine".to_string(), + x if x == ANALOG_INPUT_TEST_MACHINE => "analog_input_test_machine".to_string(), + _ => unreachable!("Unknown machine id"), + } + } } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] @@ -149,6 +173,19 @@ use ethercat_hal::helpers::ethercrab_types::{ use ethercrab::MainDevice; use ethercrab::SubDeviceIdentity; +use crate::ANALOG_INPUT_TEST_MACHINE; +use crate::IP20_TEST_MACHINE; +use crate::MACHINE_AQUAPATH_V1; +use crate::MACHINE_BUFFER_V1; +use crate::MACHINE_EXTRUDER_V1; +use crate::MACHINE_EXTRUDER_V2; +use crate::MACHINE_LASER_V1; +use crate::MACHINE_MOCK; +use crate::MACHINE_WAGO_POWER_V1; +use crate::MACHINE_WINDER_V1; +use crate::TEST_MACHINE; +use crate::VENDOR_QITECH; + #[derive(Debug)] pub struct MachineIdentificationAddresses { pub vendor_word: u16, diff --git a/machines/src/mock/act.rs b/machines/src/mock/act.rs index b6fdd9b93..13abf25f0 100644 --- a/machines/src/mock/act.rs +++ b/machines/src/mock/act.rs @@ -1,5 +1,5 @@ use super::MockMachine; -use crate::{MachineAct, MachineMessage}; +use crate::{MachineAct, MachineMessage, MachineValues}; use std::time::{Duration, Instant}; /// Implements the `MachineAct` trait for the `MockMachine`. @@ -57,6 +57,17 @@ impl MachineAct for MockMachine { { () } + MachineMessage::RequestValues(sender) => { + sender + .send_blocking(MachineValues { + state: serde_json::to_value(self.get_state()) + .expect("Failed to serialize state"), + live_values: serde_json::to_value(self.get_live_values()) + .expect("Failed to serialize live values"), + }) + .expect("Failed to send values"); + sender.close(); + } } } } diff --git a/machines/src/mock/mod.rs b/machines/src/mock/mod.rs index 7ec03c951..a3f4ce0fa 100644 --- a/machines/src/mock/mod.rs +++ b/machines/src/mock/mod.rs @@ -58,8 +58,7 @@ impl MockMachine { vendor: VENDOR_QITECH, }; - /// Emit live values data event with the current sine wave amplitude - pub fn emit_live_values(&mut self) { + pub fn get_live_values(&self) -> LiveValuesEvent { let now = Instant::now(); let elapsed = now.duration_since(self.t_0).as_secs_f64(); let freq1_hz = self.frequency1.get::(); @@ -76,37 +75,44 @@ impl MockMachine { let amplitude2 = (t * freq2_hz).sin(); let amplitude3 = (t * freq3_hz).sin(); - let live_values = LiveValuesEvent { + LiveValuesEvent { amplitude_sum: amplitude1 + amplitude2 + amplitude3, amplitude1, amplitude2, amplitude3, - }; + } + } - self.namespace - .emit(MockEvents::LiveValues(live_values.build())); + /// Emit live values data event with the current sine wave amplitude + pub fn emit_live_values(&mut self) { + let event = self.get_live_values().build(); + self.namespace.emit(MockEvents::LiveValues(event)); } - /// Emit the current state of the mock machine only if values have changed - pub fn emit_state(&mut self) { + pub fn get_state(&self) -> StateEvent { info!( "Emitting state for MockMachine, is default state: {}", !self.emitted_default_state ); - let current_state = StateEvent { - is_default_state: !std::mem::replace(&mut self.emitted_default_state, true), + StateEvent { + is_default_state: !self.emitted_default_state, frequency1: self.frequency1.get::(), frequency2: self.frequency2.get::(), frequency3: self.frequency3.get::(), mode_state: ModeState { mode: self.mode.clone(), }, - }; + } + } - self.namespace - .emit(MockEvents::State(current_state.build())); - self.last_emitted_event = Some(current_state); + /// Emit the current state of the mock machine only if values have changed + pub fn emit_state(&mut self) { + let state = self.get_state(); + let event = state.build(); + self.namespace.emit(MockEvents::State(event)); + self.emitted_default_state = true; + self.last_emitted_event = Some(state); } /// Set the frequencies of the sine waves diff --git a/machines/src/test_machine/act.rs b/machines/src/test_machine/act.rs index 946a49000..d366b7acf 100644 --- a/machines/src/test_machine/act.rs +++ b/machines/src/test_machine/act.rs @@ -1,5 +1,5 @@ use super::TestMachine; -use crate::{MachineAct, MachineMessage}; +use crate::{MachineAct, MachineMessage, MachineValues}; use std::time::{Duration, Instant}; impl MachineAct for TestMachine { @@ -31,6 +31,16 @@ impl MachineAct for TestMachine { MachineMessage::DisconnectMachine(_machine_connection) => { // Does not connect to any Machine; do nothing } + MachineMessage::RequestValues(sender) => { + sender + .send_blocking(MachineValues { + state: serde_json::to_value(self.get_state()) + .expect("Failed to serialize state"), + live_values: serde_json::Value::Null, + }) + .expect("Failed to send values"); + sender.close(); + } } } } diff --git a/machines/src/test_machine/mod.rs b/machines/src/test_machine/mod.rs index 03490a955..e0ce74f47 100644 --- a/machines/src/test_machine/mod.rs +++ b/machines/src/test_machine/mod.rs @@ -40,12 +40,14 @@ impl TestMachine { } impl TestMachine { - pub fn emit_state(&mut self) { - let event = StateEvent { + pub fn get_state(&self) -> StateEvent { + StateEvent { led_on: self.led_on, } - .build(); + } + pub fn emit_state(&mut self) { + let event = self.get_state().build(); self.namespace.emit(TestMachineEvents::State(event)); } diff --git a/machines/src/wago_power/mod.rs b/machines/src/wago_power/mod.rs index 3bb25c639..4b5131e2a 100644 --- a/machines/src/wago_power/mod.rs +++ b/machines/src/wago_power/mod.rs @@ -1,39 +1,45 @@ -use crate::{MachineChannel, MachineWithChannel}; +use crate::{ + MACHINE_WAGO_POWER_V1, MachineChannel, MachineWithChannel, VENDOR_QITECH, + machine_identification::MachineIdentification, +}; use anyhow::Result; -use control_core::{ - modbus::tcp::ModbusTcpDevice, - socketio::{ - event::{BuildEvent, GenericEvent}, - namespace::{ - CacheFn, CacheableEvents, NamespaceCacheingLogic, cache_duration, - cache_first_and_last_event, - }, +use control_core::socketio::{ + event::{BuildEvent, GenericEvent}, + namespace::{ + CacheFn, CacheableEvents, NamespaceCacheingLogic, cache_duration, + cache_first_and_last_event, }, }; use control_core_derive::BuildEvent; use serde::*; -use smol::lock::Mutex; -use std::{ - net::SocketAddr, - time::{Duration, Instant}, -}; -use units::{ - electric_current::milliampere, - electric_potential::{millivolt, volt}, - *, -}; +use std::time::{Duration, Instant}; + +#[cfg(not(feature = "mock-machine"))] +mod imports { + pub use control_core::modbus::tcp::ModbusTcpDevice; + pub use smol::lock::Mutex; + pub use std::net::SocketAddr; + pub use units::{ + electric_current::milliampere, + electric_potential::{millivolt, volt}, + *, + }; +} + +#[cfg(not(feature = "mock-machine"))] +use imports::*; const MODBUS_DC_OFF: u16 = 0; const MODBUS_DC_ON: u16 = 1; const MODBUS_HICCUP_POWER: u16 = 1 << 8; #[derive(Serialize, Debug, Clone, BuildEvent)] -pub struct LiveValuesEvent { +pub struct LiveValues { voltage: f64, current: f64, } -impl CacheableEvents for LiveValuesEvent { +impl CacheableEvents for LiveValues { fn event_value(&self) -> GenericEvent { self.build().into() } @@ -59,12 +65,12 @@ impl Mode { } #[derive(Serialize, Debug, Clone, BuildEvent)] -pub struct StateEvent { +pub struct State { mode: Mode, is_default_state: bool, } -impl CacheableEvents for StateEvent { +impl CacheableEvents for State { fn event_value(&self) -> GenericEvent { self.build().into() } @@ -87,6 +93,7 @@ pub struct WagoPower { device: Mutex, last_emit: Instant, emitted_default_state: bool, + last_live_values: Option, } impl WagoPower { @@ -101,44 +108,12 @@ impl WagoPower { device: Mutex::new(ModbusTcpDevice::new(addr).await?), last_emit: Instant::now(), emitted_default_state: false, - }) - } - - #[cfg(feature = "mock-machine")] - fn get_live_values(&mut self) -> Result { - match self.mode { - Mode::Off => Ok(LiveValuesEvent { - voltage: 0.0, - current: 0.0, - }), - Mode::On24V => Ok(LiveValuesEvent { - voltage: 24.0, - current: 5000.0, - }), - } - } - - #[cfg(not(feature = "mock-machine"))] - fn get_live_values(&mut self) -> Result { - let electric = smol::block_on(async { - let mut dev = self.device.lock().await; - dev.get_holding_registers(0x0500, 2).await - })?; - - let voltage = ElectricPotential::new::(f64::from(electric[0])); - let current = ElectricCurrent::new::(f64::from(electric[1])); - - Ok(LiveValuesEvent { - voltage: voltage.get::(), - current: current.get::(), + last_live_values: None, }) } fn emit_state(&mut self) { - let event = StateEvent { - mode: self.mode.clone(), - is_default_state: !self.emitted_default_state, - }; + let event = self.get_state(); self.channel.emit(event); } @@ -180,9 +155,49 @@ impl WagoPower { let mut dev = self.device.lock().await; dev.get_u16(0x000B).await } + + #[cfg(feature = "mock-machine")] + fn read_live_values(&mut self) -> Result { + match self.mode { + Mode::Off => Ok(LiveValues { + voltage: 0.0, + current: 0.0, + }), + Mode::On24V => Ok(LiveValues { + voltage: 24.0, + current: 5000.0, + }), + } + } + + #[cfg(not(feature = "mock-machine"))] + fn read_live_values(&mut self) -> Result { + let electric = smol::block_on(async { + let mut dev = self.device.lock().await; + dev.get_holding_registers(0x0500, 2).await + })?; + + let voltage = ElectricPotential::new::(f64::from(electric[0])); + let current = ElectricCurrent::new::(f64::from(electric[1])); + + Ok(LiveValues { + voltage: voltage.get::(), + current: current.get::(), + }) + } +} + +impl WagoPower { + pub const MACHINE_IDENTIFICATION: MachineIdentification = MachineIdentification { + vendor: VENDOR_QITECH, + machine: MACHINE_WAGO_POWER_V1, + }; } impl MachineWithChannel for WagoPower { + type State = State; + type LiveValues = LiveValues; + fn get_machine_channel(&self) -> &MachineChannel { &self.channel } @@ -214,13 +229,24 @@ impl MachineWithChannel for WagoPower { } if now.duration_since(self.last_emit) > Duration::from_secs_f64(1.0 / 30.0) { - if let Ok(event) = self.get_live_values() { - self.channel.emit(event); - } + let live_values = self.read_live_values()?; + self.channel.emit(live_values.clone()); + self.last_live_values = Some(live_values); self.last_emit = now; } Ok(()) } + + fn get_state(&self) -> Self::State { + State { + mode: self.mode.clone(), + is_default_state: !self.emitted_default_state, + } + } + + fn get_live_values(&self) -> Option { + self.last_live_values.clone() + } } diff --git a/machines/src/winder2/act.rs b/machines/src/winder2/act.rs index 0033102bc..05e449ffb 100644 --- a/machines/src/winder2/act.rs +++ b/machines/src/winder2/act.rs @@ -1,7 +1,7 @@ #[cfg(not(feature = "mock-machine"))] use super::Winder2; #[cfg(not(feature = "mock-machine"))] -use crate::{MachineAct, MachineMessage}; +use crate::{MachineAct, MachineMessage, MachineValues}; #[cfg(not(feature = "mock-machine"))] use std::time::{Duration, Instant}; @@ -61,6 +61,17 @@ impl MachineAct for Winder2 { MachineMessage::DisconnectMachine(_machine_connection) => { self.connected_machines.clear(); } + MachineMessage::RequestValues(sender) => { + sender + .send_blocking(MachineValues { + state: serde_json::to_value(self.build_state_event()) + .expect("Failed to serialize state"), + live_values: serde_json::to_value(self.get_live_values()) + .expect("Failed to serialize live values"), + }) + .expect("Failed to send values"); + sender.close(); + } } } } diff --git a/machines/src/winder2/emit.rs b/machines/src/winder2/emit.rs index f557fc680..b33888cfc 100644 --- a/machines/src/winder2/emit.rs +++ b/machines/src/winder2/emit.rs @@ -161,7 +161,7 @@ impl Winder2 { self.emit_state(); } - pub fn emit_live_values(&mut self) { + pub fn get_live_values(&self) -> LiveValuesEvent { let angle_deg = self.tension_arm.get_angle().get::(); // Wrap [270;<360] to [-90; 0] @@ -192,7 +192,7 @@ impl Winder2 { .get::() .abs(); - let live_values = LiveValuesEvent { + LiveValuesEvent { traverse_position: self .traverse_controller .get_current_position() @@ -201,9 +201,11 @@ impl Winder2 { spool_rpm, tension_arm_angle: angle_deg, spool_progress: self.spool_automatic_action.progress.get::(), - }; + } + } - let event = live_values.build(); + pub fn emit_live_values(&mut self) { + let event = self.get_live_values().build(); self.namespace.emit(Winder2Events::LiveValues(event)); } diff --git a/machines/src/winder2/mock/act.rs b/machines/src/winder2/mock/act.rs index bf020e531..c1b33f7e2 100644 --- a/machines/src/winder2/mock/act.rs +++ b/machines/src/winder2/mock/act.rs @@ -1,6 +1,7 @@ use super::Winder2; use crate::MachineAct; use crate::MachineMessage; +use crate::MachineValues; use std::time::{Duration, Instant}; impl MachineAct for Winder2 { @@ -38,6 +39,19 @@ impl MachineAct for Winder2 { { () } + MachineMessage::RequestValues(sender) => { + sender + .send_blocking(MachineValues { + state: serde_json::to_value(self.build_state_event()) + .expect("Failed to serialize state"), + live_values: serde_json::to_value(self.get_live_values()) + .expect("Failed to serialize live values"), + }) + .expect("Failed to send values"); + sender.close(); + + () + } } } } diff --git a/machines/src/winder2/mock/mock_emit.rs b/machines/src/winder2/mock/mock_emit.rs index a869abd19..375e29c3b 100644 --- a/machines/src/winder2/mock/mock_emit.rs +++ b/machines/src/winder2/mock/mock_emit.rs @@ -69,17 +69,18 @@ impl Winder2 { } pub fn emit_live_values(&mut self) { - let event = LiveValuesEvent { + let event = self.get_live_values().build(); + self.namespace.emit(Winder2Events::LiveValues(event)); + } + + pub fn get_live_values(&self) -> LiveValuesEvent { + LiveValuesEvent { traverse_position: Some(0.0), puller_speed: 0.0, spool_rpm: 0.0, tension_arm_angle: 0.0, spool_progress: 0.0, - }; - - let event = event.build(); - - self.namespace.emit(Winder2Events::LiveValues(event)); + } } pub fn build_state_event(&mut self) -> StateEvent { diff --git a/machines/src/winder2/mod.rs b/machines/src/winder2/mod.rs index 893ad190d..08b09de64 100644 --- a/machines/src/winder2/mod.rs +++ b/machines/src/winder2/mod.rs @@ -336,6 +336,9 @@ impl Winder2 { } } +#[cfg(feature = "mock-machine")] +pub use mock::Winder2; + #[derive(Debug, Clone, PartialEq, Eq)] pub enum Winder2Mode { Standby, diff --git a/server/src/app_state.rs b/server/src/app_state.rs index c6820a2f6..12b954fa1 100644 --- a/server/src/app_state.rs +++ b/server/src/app_state.rs @@ -3,6 +3,7 @@ use crate::rest::handlers::write_machine_device_identification::MachineDeviceInf use crate::socketio::main_namespace::MainNamespaceEvents; use crate::socketio::main_namespace::machines_event::{MachineObj, MachinesEventBuilder}; use crate::socketio::namespaces::Namespaces; +use anyhow::{Result, bail}; use control_core::socketio::event::GenericEvent; use ethercat_hal::devices::EthercatDevice; use ethercrab::SubDeviceRef; @@ -123,11 +124,31 @@ impl EthercatSetup { impl SharedState { pub async fn send_machines_event(&self) { - let event = MachinesEventBuilder().build(self.current_machines_meta.lock().await.clone()); + let event = MachinesEventBuilder().build(self.get_machines_meta().await); let main_namespace = &mut self.socketio_setup.namespaces.write().await.main_namespace; main_namespace.emit(MainNamespaceEvents::MachinesEvent(event)); } + pub async fn get_machines_meta(&self) -> Vec { + self.current_machines_meta.lock().await.clone() + } + + pub async fn message_machine( + &self, + machine_identification_unique: &MachineIdentificationUnique, + message: MachineMessage, + ) -> Result<()> { + let machines = self.api_machines.lock().await; + let sender = machines.get(machine_identification_unique); + + if let Some(sender) = sender { + sender.send(message).await?; + return Ok(()); + } + + bail!("Unknown machine!") + } + /// Removes a machine by its unique identifier pub async fn remove_machine(&self, machine_id: &MachineIdentificationUnique) { let mut current_machines = self.current_machines_meta.lock().await; diff --git a/server/src/mock_init.rs b/server/src/mock_init.rs index f861a2c7a..1487e33c9 100644 --- a/server/src/mock_init.rs +++ b/server/src/mock_init.rs @@ -1,7 +1,5 @@ use crate::add_serial_device; use crate::app_state::SharedState; -use crate::socketio::main_namespace::MainNamespaceEvents; -use crate::socketio::main_namespace::machines_event::MachinesEventBuilder; use machines::registry::MACHINE_REGISTRY; use std::sync::Arc; diff --git a/server/src/modbus_tcp/mod.rs b/server/src/modbus_tcp/mod.rs index 5c69d30a7..d56b8db7d 100644 --- a/server/src/modbus_tcp/mod.rs +++ b/server/src/modbus_tcp/mod.rs @@ -1,14 +1,20 @@ use crate::app_state::SharedState; -use control_core::ethernet::modbus_tcp_discovery::probe_modbus_tcp; -use control_core::futures::FutureIteratorExt; use machines::{ - MACHINE_WAGO_POWER_V1, Machine, MachineChannel, VENDOR_QITECH, - machine_identification::{MachineIdentification, MachineIdentificationUnique}, + Machine, MachineChannel, machine_identification::MachineIdentificationUnique, wago_power::WagoPower, }; -use smol::Timer; use std::sync::Arc; -use std::time::Duration; + +#[cfg(not(feature = "mock-machine"))] +mod imports { + pub use control_core::ethernet::modbus_tcp_discovery::probe_modbus_tcp; + pub use control_core::futures::FutureIteratorExt; + pub use smol::Timer; + pub use std::time::Duration; +} + +#[cfg(not(feature = "mock-machine"))] +use imports::*; #[cfg(not(feature = "mock-machine"))] pub async fn start_modbus_tcp_discovery(shared_state: Arc) { @@ -25,10 +31,7 @@ pub async fn start_modbus_tcp_discovery(shared_state: Arc) { .map(|probe| { smol::spawn(async move { let machine_identification_unique = MachineIdentificationUnique { - machine_identification: MachineIdentification { - vendor: VENDOR_QITECH, - machine: MACHINE_WAGO_POWER_V1, - }, + machine_identification: WagoPower::MACHINE_IDENTIFICATION, serial: probe.serial, }; @@ -51,10 +54,7 @@ pub async fn start_modbus_tcp_discovery(shared_state: Arc) { #[cfg(feature = "mock-machine")] pub async fn start_modbus_tcp_discovery(shared_state: Arc) { let machine_identification_unique = MachineIdentificationUnique { - machine_identification: MachineIdentification { - vendor: VENDOR_QITECH, - machine: MACHINE_WAGO_POWER_V1, - }, + machine_identification: WagoPower::MACHINE_IDENTIFICATION, serial: 0xbeef, }; diff --git a/server/src/rest/init.rs b/server/src/rest/init.rs index ba039b4b8..0359f1756 100644 --- a/server/src/rest/init.rs +++ b/server/src/rest/init.rs @@ -9,6 +9,7 @@ use tracing::Level; use super::handlers::machine_mutation::post_machine_mutate; use super::handlers::write_machine_device_identification::post_write_machine_device_identification; use crate::app_state::SharedState; +use crate::rest::rest_api::rest_api_router; use crate::socketio::init::init_socketio; use crate::rest::handlers::metrics::metrics_router; @@ -29,6 +30,7 @@ async fn init_api(app_state: Arc) -> Result<()> { ) .route("/api/v1/machine/mutate", post(post_machine_mutate)) .nest("/api/v1/metrics", metrics_router()) + .nest("/api/v2", rest_api_router()) .layer(socketio_layer) .layer(cors) .layer(trace_layer) @@ -53,7 +55,7 @@ pub fn start_api_thread(app_state: Arc) -> std::thread::JoinHandle< .build() .expect("Failed to create Tokio runtime"); - if let Err(err) = rt.block_on(init_api(app_state.clone())) { + if let Err(err) = rt.block_on(init_api(app_state)) { eprintln!("API server exited with error: {err:?}"); } }) diff --git a/server/src/rest/mod.rs b/server/src/rest/mod.rs index 735c4dc3b..2ff968614 100644 --- a/server/src/rest/mod.rs +++ b/server/src/rest/mod.rs @@ -1,3 +1,5 @@ pub mod handlers; pub mod init; +pub mod response; +pub mod rest_api; pub mod util; diff --git a/server/src/rest/response.rs b/server/src/rest/response.rs new file mode 100644 index 000000000..163ca67ca --- /dev/null +++ b/server/src/rest/response.rs @@ -0,0 +1,53 @@ +use axum::{Json, body::Body, http::StatusCode}; +use serde_json::json; + +pub enum ApiError { + ErrBadRequest(String), + ErrNotFound(String), + ErrInternal(String), +} + +impl axum::response::IntoResponse for ApiError { + fn into_response(self) -> axum::response::Response { + let json = match self { + Self::ErrBadRequest(ref e) => serde_json::to_string(&json!({ "error_bad_request": e })), + Self::ErrNotFound(ref e) => serde_json::to_string(&json!({ "error_not_found": e })), + Self::ErrInternal(ref e) => serde_json::to_string(&json!({ "error_internal": e })), + }; + + let body = match json { + Ok(s) => Body::from(s), + Err(_) => Body::empty(), + }; + + let status = match self { + Self::ErrBadRequest(_) => StatusCode::BAD_REQUEST, + Self::ErrNotFound(_) => StatusCode::NOT_FOUND, + Self::ErrInternal(_) => StatusCode::INTERNAL_SERVER_ERROR, + }; + + axum::response::Response::builder() + .status(status) + .header("Content-Type", "application/json") + .body(body) + .expect("Failed to build error response") + } +} + +pub type Result = axum::response::Result, ApiError>; + +pub fn json(t: T) -> Result { + Result::Ok(Json(t)) +} + +pub fn bad_request(e: E) -> ApiError { + ApiError::ErrBadRequest(e.to_string()) +} + +pub fn not_found(e: E) -> ApiError { + ApiError::ErrNotFound(e.to_string()) +} + +pub fn internal_error(e: E) -> ApiError { + ApiError::ErrInternal(e.to_string()) +} diff --git a/server/src/rest/rest_api.rs b/server/src/rest/rest_api.rs new file mode 100644 index 000000000..a441e5111 --- /dev/null +++ b/server/src/rest/rest_api.rs @@ -0,0 +1,166 @@ +use std::sync::Arc; + +use axum::extract::{Path, State}; +use axum::routing::{get, post}; +use axum::{Extension, Json, Router, debug_handler}; +use machines::MachineMessage; +use machines::analog_input_test_machine::AnalogInputTestMachine; +use machines::aquapath1::AquaPathV1; +use machines::extruder1::ExtruderV2; +use machines::ip20_test_machine::IP20TestMachine; +use machines::laser::LaserMachine; +use machines::machine_identification::{MachineIdentification, MachineIdentificationUnique}; +use machines::mock::MockMachine; +use machines::test_machine::TestMachine; +use machines::wago_power::WagoPower; +use machines::winder2::Winder2; +use serde::Serialize; + +use crate::app_state::SharedState; +use crate::rest::response::*; + +#[derive(Serialize, Debug, PartialEq)] +struct MachineResponce { + legacy_id: MachineIdentificationUnique, + serial: u16, + vendor: String, + slug: String, + error: Option, +} + +impl From for MachineResponce { + fn from(machine_identification_unique: MachineIdentificationUnique) -> Self { + let vendor = machine_identification_unique + .machine_identification + .vendor_str(); + let slug = machine_identification_unique.machine_identification.slug(); + let serial = machine_identification_unique.serial; + + Self { + legacy_id: machine_identification_unique, + serial, + vendor, + slug, + error: None, + } + } +} + +#[derive(Serialize, Debug, PartialEq)] +struct GetMachinesResponce { + machines: Vec, +} + +#[debug_handler] +async fn get_machines_handler( + State(shared_state): State>, +) -> Result { + let machines = shared_state + .get_machines_meta() + .await + .into_iter() + .map(|m| { + let vendor = m + .machine_identification_unique + .machine_identification + .vendor_str(); + let slug = m + .machine_identification_unique + .machine_identification + .slug(); + let serial = m.machine_identification_unique.serial; + + MachineResponce { + legacy_id: m.machine_identification_unique, + serial, + vendor, + slug, + error: m.error, + } + }) + .collect(); + + json(GetMachinesResponce { machines }) +} + +#[derive(Serialize, Debug, PartialEq)] +struct GetMachineResponce { + machine: MachineResponce, + state: serde_json::Value, + live_values: serde_json::Value, +} + +#[debug_handler] +async fn get_machine_handler( + Extension(id): Extension, + State(shared_state): State>, + Path(serial): Path, +) -> Result { + let id = MachineIdentificationUnique { + machine_identification: id, + serial, + }; + + let (sender, receiver) = smol::channel::bounded(1); + shared_state + .message_machine(&id, MachineMessage::RequestValues(sender)) + .await + .map_err(not_found)?; + + let values = receiver.recv().await.map_err(internal_error)?; + + json(GetMachineResponce { + machine: MachineResponce::from(id), + state: values.state, + live_values: values.live_values, + }) +} + +type PostMachineRequest = Vec; + +#[debug_handler] +async fn post_machine_handler( + Extension(id): Extension, + State(shared_state): State>, + Path(serial): Path, + Json(request): Json, +) -> Result<()> { + let id = MachineIdentificationUnique { + machine_identification: id, + serial, + }; + + for value in request { + shared_state + .message_machine(&id, MachineMessage::HttpApiJsonRequest(value)) + .await + .map_err(not_found)?; + } + + json(()) +} + +fn make_machine_router(id: MachineIdentification) -> Router> { + let slug = id.slug(); + let path = format!("/machine/{slug}/{{serial}}"); + Router::new() + .route(&path, get(get_machine_handler)) + .route(&path, post(post_machine_handler)) + .layer(Extension(id)) +} + +pub fn rest_api_router() -> Router> { + Router::new() + .route("/machine", get(get_machines_handler)) + .merge(make_machine_router(LaserMachine::MACHINE_IDENTIFICATION)) + .merge(make_machine_router(Winder2::MACHINE_IDENTIFICATION)) + .merge(make_machine_router(MockMachine::MACHINE_IDENTIFICATION)) + .merge(make_machine_router(ExtruderV2::MACHINE_IDENTIFICATION)) + .merge(make_machine_router(AquaPathV1::MACHINE_IDENTIFICATION)) + .merge(make_machine_router(TestMachine::MACHINE_IDENTIFICATION)) + .merge(make_machine_router(WagoPower::MACHINE_IDENTIFICATION)) + .merge(make_machine_router(IP20TestMachine::MACHINE_IDENTIFICATION)) + .merge(make_machine_router( + AnalogInputTestMachine::MACHINE_IDENTIFICATION, + )) +}