diff --git a/Cargo.toml b/Cargo.toml index fa1e35c..c9ef5c1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,6 +19,7 @@ name = "slonk" version = "0.1.0" edition = "2021" rust-version = "1.65" +default-run = "slonk" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html diff --git a/api.md b/api.md index 3aa26df..8705b1a 100644 --- a/api.md +++ b/api.md @@ -44,20 +44,20 @@ and similar. ## Example timeline 1. Controller and dashboard both start. - The controller begins listening for an incoming connection on its TCP server. + The controller begins listening for an incoming connection on its TCP server. 1. User enters the IP address of the controller, and then presses "Connect to Controller" or similar - button on dashboard. + button on dashboard. 1. Dashboard connects to the specified IP address for the controller. 1. Controller transmits a configuration message immediately. 1. Controller sends a series of status messages containing sensor data, and each is plotted on the - dashboard. + dashboard. 1. User begins an ignition sequence. - Ignition start message is sent to controller. + Ignition start message is sent to controller. 1. Controller completes ignition process. @@ -74,7 +74,7 @@ The fields of the main configuration object are as follows: When a log buffer is full, its data will be flushed into a log file. - `sensor_groups` - array: A list describing each set of sensors and the threads that manage them. -It also includes calibration information. + It also includes calibration information. - `pre_ignite_time` - number. The duration of the pre-ignition period in milliseconds. During pre-ignition, sensors log data at a high frequency, but the ignition procedure has not yet @@ -93,6 +93,8 @@ It also includes calibration information. - `estop_sequence` - array: A list of objects describing each sequential operation to be taken during the shutoff sequence. +- `pin_heartbeat` - number: The GPIO pin ID of the pin to be lit on and off for the heartbeat light. + ### Drivers Each driver is represented by an object in the `drivers` list. @@ -100,6 +102,14 @@ It will have the following keys: - `label` - string: A human-readable name for the driver. +- `label_actuate` - string: A human-readable name describing what will happen when the driver is + actuated. + For example, this could be `Open` or `Ignite`. + +- `label_deactuate` - string: A human-readable name describing what will happen when the driver is + deactuated. + For example, this could be `Close` or `Shutoff`. + - `pin` - int: The GPIO pin that the driver controls. Note that the GPIO pin is by software standards, and it is _not_ the phyiscal pinout on the Raspberry Pi. @@ -129,7 +139,7 @@ following fields: ignition), messages will be sent on a time scale according to how often they were sampled. - `sensors` - array: The set of sensors. Each sensor will be an object containing the following - keys: + keys: - `label` - string: The unique identifier for the sensor. May not be shared across sensor groups. @@ -234,6 +244,8 @@ However, it makes the syntax and structure of a configuration apparent. "drivers": [ { "label": "OXI_FILL", + "label_actuate": "Open", + "label_deactuate": "Close", "pin": 33 } ], diff --git a/config/titan-karca.json b/config/titan-karca.json new file mode 100644 index 0000000..29b9b7c --- /dev/null +++ b/config/titan-karca.json @@ -0,0 +1,194 @@ +{ + "frequency_status": 10, + "log_buffer_size": 256, + "sensor_groups": [ + { + "label": "Thermocouples", + "frequency_standby": 5, + "frequency_ignition": 20, + "frequency_transmission": 5, + "sensors": [ + { + "label": "TC1: Oxidizer tank", + "color": "#FC6453", + "units": "°C", + "calibration_intercept": -250, + "calibration_slope": 0.2441, + "adc": 0, + "channel": 0 + }, + { + "label": "TC2: Combustion chamber", + "color": "#EF3B9E", + "units": "°C", + "calibration_intercept": -250, + "calibration_slope": 0.2441, + "adc": 0, + "channel": 1 + } + ] + }, + { + "label": "Pressure transducers", + "frequency_standby": 5, + "frequency_ignition": 2000, + "frequency_transmission": 5, + "sensors": [ + { + "label": "PT1: Combustion chamber", + "units": "psi", + "color": "#EBF927", + "calibration_intercept": -249.8, + "calibration_slope": 0.339, + "rolling_average_width": 10, + "range": [-300, 700], + "adc": 1, + "channel": 0 + }, + { + "label": "PT2: Oxidizer feedline", + "units": "psi", + "color": "#1D8718", + "calibration_intercept": -249.8, + "calibration_slope": 0.339, + "adc": 1, + "channel": 1 + }, + { + "label": "PT3: Injector", + "units": "psi", + "color": "#4104D1", + "calibration_intercept": -249.8, + "calibration_slope": 0.339, + "rolling_average_width": 10, + "adc": 1, + "channel": 2 + }, + { + "label": "PT4: Oxidizer tank", + "color": "#F9864D", + "units": "psi", + "calibration_intercept": -249.8, + "calibration_slope": 0.339, + "adc": 1, + "channel": 3 + } + ] + }, + { + "label": "Load cells", + "frequency_standby": 5, + "frequency_ignition": 2000, + "frequency_transmission": 5, + "sensors": [ + { + "label": "Main axial cell", + "color": "#3292FF", + "units": "lb", + "calibration_intercept": -304.38, + "calibration_slope": 0.967, + "adc": 2, + "channel": 0 + } + ] + } + ], + "drivers": [ + { + "label": "Feedline", + "label_actuate": "Open", + "label_deactuate": "Close", + "pin": 19, + "protected": false + }, + { + "label": "Ox tank vent", + "label_actuate": "Open", + "label_deactuate": "Close", + "pin": 13, + "protected": false + }, + { + "label": "Ground vent", + "label_actuate": "Close", + "label_deactuate": "Open", + "pin": 6, + "protected": false + }, + { + "label": "Ignition", + "label_actuate": "Ignite", + "label_deactuate": "Shufoff", + "pin": 17, + "protected": true + } + ], + "pre_ignite_time": 1000, + "post_ignite_time": 5000, + "ignition_sequence": [ + { + "type": "Actuate", + "driver_id": 0, + "value": false + }, + { + "type": "Actuate", + "driver_id": 1, + "value": false + }, + { + "type": "Actuate", + "driver_id": 2, + "value": true + }, + { + "type": "Actuate", + "driver_id": 3, + "value": true + }, + { + "type": "Sleep", + "duration": { + "secs": 10, + "nanos": 0 + } + }, + { + "type": "Actuate", + "driver_id": 3, + "value": false + } + ], + "estop_sequence": [ + { + "type": "Actuate", + "driver_id": 0, + "value": false + }, + { + "type": "Actuate", + "driver_id": 1, + "value": false + }, + { + "type": "Actuate", + "driver_id": 2, + "value": true + }, + { + "type": "Actuate", + "driver_id": 3, + "value": false + } + ], + "spi_mosi": 10, + "spi_miso": 9, + "spi_clk": 11, + "spi_frequency_clk": 100000, + "adc_cs": [ + 7, + 8, + 25 + ], + "pin_heartbeat": 5 +} \ No newline at end of file diff --git a/config/titan-mk1.json b/config/titan-mk1.json index ad7f662..dea5ae5 100644 --- a/config/titan-mk1.json +++ b/config/titan-mk1.json @@ -12,8 +12,8 @@ "label": "TC1: Oxidizer tank", "color": "#FC6453", "units": "°C", - "calibration_intercept": 300, - "calibration_slope": -0.175, + "calibration_intercept": 308.4, + "calibration_slope": -0.1676, "adc": 0, "channel": 0 }, @@ -38,8 +38,8 @@ "label": "PT1: Combustion chamber", "units": "psi", "color": "#EBF927", - "calibration_intercept": -270, - "calibration_slope": 0.417, + "calibration_intercept": -250.33, + "calibration_slope": 0.378, "rolling_average_width": 10, "adc": 1, "channel": 0 @@ -48,8 +48,8 @@ "label": "PT2: Oxidizer feedline", "units": "psi", "color": "#1D8718", - "calibration_intercept": 1027, - "calibration_slope": -0.286, + "calibration_intercept": 1020, + "calibration_slope": -0.2834, "rolling_average_width": 10, "adc": 1, "channel": 1 @@ -58,8 +58,8 @@ "label": "PT3: Injector", "units": "psi", "color": "#4104D1", - "calibration_intercept": 1548, - "calibration_slope": -0.415, + "calibration_intercept": 1277, + "calibration_slope": -0.3431, "rolling_average_width": 10, "adc": 1, "channel": 2 @@ -68,8 +68,8 @@ "label": "PT4: Oxidizer tank", "color": "#F9864D", "units": "psi", - "calibration_intercept": 1382, - "calibration_slope": -0.372, + "calibration_intercept": 1180, + "calibration_slope": -0.3178, "rolling_average_width": 10, "adc": 1, "channel": 3 @@ -86,8 +86,8 @@ "label": "Main axial cell", "color": "#3292FF", "units": "lb", - "calibration_intercept": 0.34, - "calibration_slope": 33.2, + "calibration_intercept": -304.38, + "calibration_slope": 0.4321, "adc": 2, "channel": 0 } @@ -97,33 +97,33 @@ "drivers": [ { "label": "Feedline", - "pin": 19, + "pin": 17, "protected": false }, { "label": "Ox tank vent", - "pin": 13, + "pin": 27, "protected": false }, { "label": "Ground vent", - "pin": 24, + "pin": 22, "protected": false }, { "label": "Unused (1)", - "pin": 22, + "pin": 24, "protected": true }, { "label": "Unused (2)", - "pin": 27, + "pin": 13, "protected": true }, { "label": "Ignition", - "pin": 17, - "protected": true + "pin": 23, + "protected": false } ], "pre_ignite_time": 1000, @@ -174,8 +174,9 @@ "spi_clk": 11, "spi_frequency_clk": 50000, "adc_cs": [ - 25, + 7, 8, - 7 - ] + 25 + ], + "pin_heartbeat": 6 } \ No newline at end of file diff --git a/src/bin/main.rs b/src/bin/slonk.rs similarity index 100% rename from src/bin/main.rs rename to src/bin/slonk.rs diff --git a/src/config.rs b/src/config.rs index 2fa8686..ba887bb 100644 --- a/src/config.rs +++ b/src/config.rs @@ -70,6 +70,8 @@ pub struct Configuration { /// The chip select pins for each device. /// For now, we assume that all ADCs are MCP3208s. pub adc_cs: Vec, + /// The GPIO pin ID of the heartbeat LED. + pub pin_heartbeat: u8, } #[derive(Serialize, Deserialize, Debug, PartialEq, Eq)] @@ -77,6 +79,10 @@ pub struct Configuration { pub struct Driver { /// The human-readable name of the driver. pub label: String, + /// The label for the action that will be performed when the driver is turned on. + pub label_actuate: String, + /// The label for the action that will be performed when the driver is turned off. + pub label_deactuate: String, /// The pin actuated by the driver. pub pin: u8, /// Whether this driver is protected from user access. @@ -311,6 +317,8 @@ mod tests { "drivers": [ { "label": "OXI_FILL", + "label_actuate": "Open", + "label_deactuate": "Close", "pin": 21, "protected": false } @@ -347,7 +355,8 @@ mod tests { "spi_frequency_clk": 50000, "adc_cs": [ 20 - ] + ], + "pin_heartbeat": 0 }"##; let config = Configuration { frequency_status: 10, @@ -386,6 +395,8 @@ mod tests { post_ignite_time: 5000, drivers: vec![Driver { label: "OXI_FILL".into(), + label_actuate: "Open".into(), + label_deactuate: "Close".into(), pin: 21, protected: false, }], @@ -411,6 +422,7 @@ mod tests { spi_clk: 24, spi_frequency_clk: 50_000, adc_cs: vec![20], + pin_heartbeat: 0, }; let mut cursor = Cursor::new(config_str); diff --git a/src/data.rs b/src/data.rs index 9d8e9f7..c00c8f5 100644 --- a/src/data.rs +++ b/src/data.rs @@ -429,7 +429,8 @@ mod tests { "spi_miso": 12, "spi_clk": 13, "spi_frequency_clk": 50000, - "adc_cs": [14, 15] + "adc_cs": [14, 15], + "pin_heartbeat": 0 }"##; let adcs: Vec> = (0..2).map(|n| Mutex::new(ReturnsNumber(n))).collect(); @@ -570,7 +571,8 @@ mod tests { "spi_miso": 12, "spi_clk": 13, "spi_frequency_clk": 50000, - "adc_cs": [14] + "adc_cs": [14], + "pin_heartbeat": 0 }"##; let adc = Mutex::new(ReturnsNumber(100)); diff --git a/src/execution.rs b/src/execution.rs index 950472c..28ab9ac 100644 --- a/src/execution.rs +++ b/src/execution.rs @@ -275,7 +275,8 @@ mod tests { "spi_miso": 13, "spi_clk": 14, "spi_frequency_clk": 50000, - "adc_cs": [] + "adc_cs": [], + "pin_heartbeat": 0 }"#; let mut cfg_cursor = Cursor::new(config); @@ -314,6 +315,8 @@ mod tests { "post_ignite_time": 0, "drivers": [{ "label": "OXI_FILL", + "label_actuate": "Open", + "label_deactuate": "Close", "pin": 21, "protected": false }], @@ -334,7 +337,8 @@ mod tests { "spi_miso": 12, "spi_clk": 13, "spi_frequency_clk": 50000, - "adc_cs": [] + "adc_cs": [], + "pin_heartbeat": 0 }"#; let mut cfg_cursor = Cursor::new(config); @@ -374,7 +378,8 @@ mod tests { "spi_miso": 12, "spi_clk": 13, "spi_frequency_clk": 50000, - "adc_cs": [] + "adc_cs": [], + "pin_heartbeat": 0 }"#; let mut cfg_cursor = Cursor::new(config); @@ -423,7 +428,8 @@ mod tests { "spi_miso": 12, "spi_clk": 13, "spi_frequency_clk": 50000, - "adc_cs": [] + "adc_cs": [], + "pin_heartbeat": 0 }"#; let mut cfg_cursor = Cursor::new(config); diff --git a/src/heartbeat.rs b/src/heartbeat.rs new file mode 100644 index 0000000..4a7e8a1 --- /dev/null +++ b/src/heartbeat.rs @@ -0,0 +1,48 @@ +use crate::{ + hardware::GpioPin, + state::{Guard, State}, + ControllerError, +}; + +use std::{thread::sleep, time::Duration}; + +/// Perform the heartbeat thread for the controller. +/// This will alternate the output value on `pin` for as long as the server is running. +pub fn heartbeat(pin: &mut impl GpioPin, state: &Guard) -> Result<(), ControllerError> { + while state.status()? != State::Quit { + pin.write(true)?; + sleep(Duration::from_millis(50)); + pin.write(false)?; + sleep(Duration::from_millis(50)); + pin.write(true)?; + sleep(Duration::from_millis(50)); + pin.write(false)?; + sleep(Duration::from_millis(850)); + } + + Ok(()) +} + +#[cfg(test)] +mod tests { + use std::thread::scope; + + use crate::hardware::ListenerPin; + + use super::*; + + #[test] + fn heartbeat_write() { + let mut pin = ListenerPin::new(false); + let guard = Guard::new(State::Standby); + + scope(|s| { + s.spawn(|| heartbeat(&mut pin, &guard)); + + sleep(Duration::from_millis(200)); + guard.move_to(State::Quit).unwrap(); + }); + + assert_eq!(pin.history(), &vec![false, true, false, true, false]); + } +} diff --git a/src/lib.rs b/src/lib.rs index 337d2a8..5f9d7b5 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -25,6 +25,7 @@ mod console; mod data; mod execution; pub mod hardware; +mod heartbeat; mod incoming; mod outgoing; pub mod server; @@ -36,8 +37,6 @@ pub mod state; pub enum ControllerError { /// The controller failed because a lock was poisoned, likely due to a panicked thread. Poison, - /// The library `serde_json` failed to deserialize a structure because it was malformed. - MalformedDeserialize(serde_json::Error), /// There was an I/O error when writing to a log file. Console(std::io::Error), /// There was an error while writing an outgoing message to the dashboard. diff --git a/src/server.rs b/src/server.rs index 0fa5a3e..0606bb7 100644 --- a/src/server.rs +++ b/src/server.rs @@ -37,6 +37,7 @@ use crate::{ spi::{Bus, Device}, Adc, GpioPin, ListenerPin, Mcp3208, ReturnsNumber, }, + heartbeat::heartbeat, incoming::{self, Command}, outgoing::{DashChannel, Message}, state::{Guard, State}, @@ -95,6 +96,16 @@ pub trait MakeHardware { config: &Configuration, chip: &mut Self::Chip, ) -> Result, ControllerError>; + + /// Get a the heartbeat GPIO pin from the configuration. + /// + /// # Errors + /// + /// This function may return an error if it is unable to acquire the GPIO needed. + fn heartbeat( + config: &Configuration, + chip: &mut Self::Chip, + ) -> Result; } /// A hardware maker for actually interfacing with the Raspberry Pi. @@ -117,33 +128,32 @@ impl MakeHardware for RaspberryPi { chip: &mut Self::Chip, bus: &'a Self::Bus, ) -> Result>>, ControllerError> { - let mut adcs = Vec::new(); - for &cs_pin in &config.adc_cs { - adcs.push(Mutex::new(Mcp3208::new(Device::new( - bus, - chip.get_line(u32::from(cs_pin))? - .request(LineRequestFlags::OUTPUT, 1, "slonk")?, - )))); - } - - Ok(adcs) + config + .adc_cs + .iter() + .flat_map(|&pin| { + chip.get_line(u32::from(pin)).map(|line| { + line.request(LineRequestFlags::OUTPUT, 1, "slonk") + .map(|handle| Mutex::new(Mcp3208::new(Device::new(bus, handle)))) + }) + }) + .map(|r| r.map_err(std::convert::Into::into)) + .collect() } fn drivers( config: &Configuration, chip: &mut Self::Chip, ) -> Result, ControllerError> { - let mut lines = Vec::new(); - - for driver in &config.drivers { - lines.push(chip.get_line(u32::from(driver.pin))?.request( - LineRequestFlags::OUTPUT, - 0, - "slonk", - )?); - } - - Ok(lines) + config + .drivers + .iter() + .flat_map(|driver| { + chip.get_line(u32::from(driver.pin)) + .map(|l| l.request(LineRequestFlags::OUTPUT, 0, "slonk")) + }) + .map(|r| r.map_err(std::convert::Into::into)) + .collect() } fn bus(config: &Configuration, chip: &mut Self::Chip) -> Result { @@ -166,6 +176,17 @@ impl MakeHardware for RaspberryPi { )?, })) } + + fn heartbeat( + config: &Configuration, + chip: &mut Self::Chip, + ) -> Result { + Ok(chip.get_line(u32::from(config.pin_heartbeat))?.request( + LineRequestFlags::OUTPUT, + 0, + "slonk", + )?) + } } /// A dummy hardware maker for testing on any Linux computer. @@ -206,6 +227,10 @@ impl MakeHardware for Dummy { .map(|_| ListenerPin::new(false)) .collect()) } + + fn heartbeat(_: &Configuration, _: &mut Self::Chip) -> Result { + Ok(ListenerPin::new(false)) + } } #[allow(clippy::too_many_lines, clippy::cast_possible_truncation)] @@ -291,7 +316,6 @@ pub fn run() -> Result<(), ControllerError> { let cmd_file_ref = &cmd_file; let mut drivers_file = file_create_new(PathBuf::from_iter([logs_path, "drivers.csv"]))?; - let drivers_file_ref = &mut drivers_file; // when a client connects, the inner value of this mutex will be `Some` containing a TCP stream // to the dashboard @@ -311,6 +335,7 @@ pub fn run() -> Result<(), ControllerError> { let bus = M::bus(&config, &mut gpio_chip)?; let adcs = M::adcs(&config, &mut gpio_chip, &bus)?; let adcs_ref = &adcs; + let mut pin_heartbeat = M::heartbeat(&config, &mut gpio_chip)?; let driver_lines = Mutex::new(M::drivers(&config, &mut gpio_chip)?); let driver_lines_ref = &driver_lines; @@ -319,14 +344,14 @@ pub fn run() -> Result<(), ControllerError> { user_log.debug("Now spawning sensor listener threads...")?; std::thread::scope(|s| { - for (group_id, mut log_file_group) in sensor_log_files.into_iter().enumerate() { + for (group_id, log_file_group) in sensor_log_files.iter_mut().enumerate() { s.spawn(move || { sensor_listen( s, group_id as u8, config_ref, driver_lines_ref, - &mut log_file_group, + log_file_group, user_log_ref, adcs_ref, state_ref, @@ -335,17 +360,19 @@ pub fn run() -> Result<(), ControllerError> { }); } - s.spawn(move || { + s.spawn(|| { driver_status_listen( - config_ref, - driver_lines_ref, - drivers_file_ref, - user_log_ref, - state_ref, - to_dash_ref, + &config, + &driver_lines, + &mut drivers_file, + &user_log, + &state, + &to_dash, ) }); + s.spawn(|| heartbeat(&mut pin_heartbeat, state_ref)); + user_log.debug("Successfully spawned sensor listener threads.")?; user_log.debug("Opening network...")?; @@ -442,8 +469,7 @@ fn handle_client<'a>( return Ok(()); } user_log.warn(&format!( - "Encountered I/O error while parsing message: {:?}", - e + "Encountered I/O error while parsing message: {e:?}" ))?; } }