diff --git a/Cargo.toml b/Cargo.toml
index 8c2de1bc18277d46b2809a58c2c77efa82b714e7..56988286f6da94d065211d205aa4196de3fe1e83 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -7,7 +7,7 @@ authors = [
     "Niccolò Betto <niccolo.betto@skywarder.eu>",
 ]
 edition = "2024"
-description = "Skyward Enhanced Ground Software"
+edescription = "Skyward Enhanced Ground Software"
 license = "MIT"
 
 [dependencies]
diff --git a/icons/valve_control/dark/aperture.svg b/icons/valve_control/dark/aperture.svg
new file mode 100644
index 0000000000000000000000000000000000000000..75a1a4fe0ffad4b43a00481a80ed617a16dc3797
--- /dev/null
+++ b/icons/valve_control/dark/aperture.svg
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
+<svg fill="#ffffff" width="800px" height="800px" viewBox="0 0 14 14" role="img" focusable="false" aria-hidden="true"
+    xmlns="http://www.w3.org/2000/svg">
+    <path
+        d="m 9.8075842,8.125795 c -0.047226,-0.0818 -0.1653177,-0.0818 -0.2125819,-2e-5 l -2.7091686,4.69002 c -0.047101,0.0815 0.011348,0.18406 0.1055111,0.18419 0.00288,0 0.00577,0 0.00866,0 1.7426489,0 3.3117022,-0.74298 4.4078492,-1.92938 0.03656,-0.0396 0.04318,-0.0983 0.01627,-0.14492 L 9.807592,8.125795 Z m -2.4888999,1.68471 -5.4097573,0.005 c -0.094226,8e-5 -0.1536307,0.10217 -0.1064545,0.18373 0.8246515,1.42592 2.2192034,2.4809 3.8722553,2.85359 0.052573,0.0119 0.1068068,-0.0117 0.1337538,-0.0583 l 1.6166069,-2.80007 c 0.047277,-0.0819 -0.011863,-0.18422 -0.1064042,-0.18411 z m 4.8812207,-5.80581 c -0.825356,-1.42976 -2.2234427,-2.48728 -3.8807595,-2.85911 -0.052561,-0.0118 -0.1067061,0.0117 -0.1336405,0.0584 l -1.6174875,2.80157 c -0.047252,0.0819 0.011813,0.18415 0.1063413,0.18412 l 5.4189662,-0.001 c 0.0942,-2e-5 0.153693,-0.10205 0.10658,-0.18365 z m 0.413678,1.12713 -3.2344588,0 c -0.094503,0 -0.1535677,0.10233 -0.1062909,0.18415 l 2.7089927,4.6888 c 0.04735,0.0819 0.165368,0.0815 0.212808,-3.1e-4 C 12.706741,9.120925 13,8.094715 13,7.000015 c 0,-0.62043 -0.09423,-1.2188 -0.269055,-1.7817 -0.01595,-0.0514 -0.06352,-0.0865 -0.117362,-0.0865 z M 7.0021135,1.000015 c -7.045e-4,0 -0.00142,0 -0.00213,0 -1.7423595,0 -3.311199,0.74275 -4.4073209,1.92881 -0.036558,0.0395 -0.043188,0.0983 -0.016254,0.14494 l 1.6162797,2.79947 c 0.047264,0.0819 0.1654436,0.0818 0.2126575,-9e-5 l 2.7023877,-4.6891 c 0.04695,-0.0815 -0.011498,-0.18401 -0.1056242,-0.18403 z m -5.6144011,7.87237 3.2356039,0 c 0.094503,0 0.1535552,-0.10231 0.106291,-0.18416 L 2.0185518,3.994515 c -0.047327,-0.082 -0.1653304,-0.0816 -0.2127959,3e-4 C 1.2933853,4.878505 1,5.904985 1,7.000015 c 0,0.62198 0.094717,1.22181 0.2703885,1.78596 0.01599,0.0514 0.063518,0.0864 0.1173239,0.0864 z" />
+</svg>
diff --git a/icons/valve_control/dark/timing.svg b/icons/valve_control/dark/timing.svg
new file mode 100644
index 0000000000000000000000000000000000000000..e267bdeeb256b7bf7670fdeb4c289da263f2372a
--- /dev/null
+++ b/icons/valve_control/dark/timing.svg
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
+<svg fill="#ffffff" width="800px" height="800px" viewBox="0 0 56 56" xmlns="http://www.w3.org/2000/svg">
+    <path
+        d="M 27.9999 51.9063 C 41.0546 51.9063 51.9063 41.0781 51.9063 28 C 51.9063 14.9453 41.0780 4.0937 28.0234 4.0937 C 26.7812 4.0937 26.1718 4.8437 26.1718 6.0625 L 26.1718 15.1563 C 26.1718 16.1641 26.8514 16.9844 27.8827 16.9844 C 28.9140 16.9844 29.6171 16.1641 29.6171 15.1563 L 29.6171 8.1484 C 39.9296 8.9688 47.8983 17.5 47.8983 28 C 47.8983 39.0625 39.0390 47.9219 27.9999 47.9219 C 16.9374 47.9219 8.0546 39.0625 8.0780 28 C 8.1014 23.0781 9.8593 18.6016 12.7890 15.1563 C 13.5155 14.2422 13.5624 13.1406 12.7890 12.3203 C 12.0155 11.4766 10.7030 11.5469 9.8593 12.6016 C 6.2733 16.7734 4.0937 22.1641 4.0937 28 C 4.0937 41.0781 14.9218 51.9063 27.9999 51.9063 Z M 31.7499 31.6094 C 33.6014 29.6875 33.2265 27.0625 30.9999 25.5156 L 18.6014 16.8672 C 17.4296 16.0469 16.2109 17.2656 17.0312 18.4375 L 25.6796 30.8359 C 27.2265 33.0625 29.8514 33.4609 31.7499 31.6094 Z" />
+</svg>
diff --git a/icons/valve_control/light/aperture.svg b/icons/valve_control/light/aperture.svg
new file mode 100644
index 0000000000000000000000000000000000000000..7975283833187be7f0c63597d21e3cfa19b0d586
--- /dev/null
+++ b/icons/valve_control/light/aperture.svg
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
+<svg fill="#000000" width="800px" height="800px" viewBox="0 0 14 14" role="img" focusable="false" aria-hidden="true"
+    xmlns="http://www.w3.org/2000/svg">
+    <path
+        d="m 9.8075842,8.125795 c -0.047226,-0.0818 -0.1653177,-0.0818 -0.2125819,-2e-5 l -2.7091686,4.69002 c -0.047101,0.0815 0.011348,0.18406 0.1055111,0.18419 0.00288,0 0.00577,0 0.00866,0 1.7426489,0 3.3117022,-0.74298 4.4078492,-1.92938 0.03656,-0.0396 0.04318,-0.0983 0.01627,-0.14492 L 9.807592,8.125795 Z m -2.4888999,1.68471 -5.4097573,0.005 c -0.094226,8e-5 -0.1536307,0.10217 -0.1064545,0.18373 0.8246515,1.42592 2.2192034,2.4809 3.8722553,2.85359 0.052573,0.0119 0.1068068,-0.0117 0.1337538,-0.0583 l 1.6166069,-2.80007 c 0.047277,-0.0819 -0.011863,-0.18422 -0.1064042,-0.18411 z m 4.8812207,-5.80581 c -0.825356,-1.42976 -2.2234427,-2.48728 -3.8807595,-2.85911 -0.052561,-0.0118 -0.1067061,0.0117 -0.1336405,0.0584 l -1.6174875,2.80157 c -0.047252,0.0819 0.011813,0.18415 0.1063413,0.18412 l 5.4189662,-0.001 c 0.0942,-2e-5 0.153693,-0.10205 0.10658,-0.18365 z m 0.413678,1.12713 -3.2344588,0 c -0.094503,0 -0.1535677,0.10233 -0.1062909,0.18415 l 2.7089927,4.6888 c 0.04735,0.0819 0.165368,0.0815 0.212808,-3.1e-4 C 12.706741,9.120925 13,8.094715 13,7.000015 c 0,-0.62043 -0.09423,-1.2188 -0.269055,-1.7817 -0.01595,-0.0514 -0.06352,-0.0865 -0.117362,-0.0865 z M 7.0021135,1.000015 c -7.045e-4,0 -0.00142,0 -0.00213,0 -1.7423595,0 -3.311199,0.74275 -4.4073209,1.92881 -0.036558,0.0395 -0.043188,0.0983 -0.016254,0.14494 l 1.6162797,2.79947 c 0.047264,0.0819 0.1654436,0.0818 0.2126575,-9e-5 l 2.7023877,-4.6891 c 0.04695,-0.0815 -0.011498,-0.18401 -0.1056242,-0.18403 z m -5.6144011,7.87237 3.2356039,0 c 0.094503,0 0.1535552,-0.10231 0.106291,-0.18416 L 2.0185518,3.994515 c -0.047327,-0.082 -0.1653304,-0.0816 -0.2127959,3e-4 C 1.2933853,4.878505 1,5.904985 1,7.000015 c 0,0.62198 0.094717,1.22181 0.2703885,1.78596 0.01599,0.0514 0.063518,0.0864 0.1173239,0.0864 z" />
+</svg>
diff --git a/icons/valve_control/light/timing.svg b/icons/valve_control/light/timing.svg
new file mode 100644
index 0000000000000000000000000000000000000000..dd2d1e16f08a64043f4e32efed01eba0e4b47454
--- /dev/null
+++ b/icons/valve_control/light/timing.svg
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
+<svg fill="#000000" width="800px" height="800px" viewBox="0 0 56 56" xmlns="http://www.w3.org/2000/svg">
+    <path
+        d="M 27.9999 51.9063 C 41.0546 51.9063 51.9063 41.0781 51.9063 28 C 51.9063 14.9453 41.0780 4.0937 28.0234 4.0937 C 26.7812 4.0937 26.1718 4.8437 26.1718 6.0625 L 26.1718 15.1563 C 26.1718 16.1641 26.8514 16.9844 27.8827 16.9844 C 28.9140 16.9844 29.6171 16.1641 29.6171 15.1563 L 29.6171 8.1484 C 39.9296 8.9688 47.8983 17.5 47.8983 28 C 47.8983 39.0625 39.0390 47.9219 27.9999 47.9219 C 16.9374 47.9219 8.0546 39.0625 8.0780 28 C 8.1014 23.0781 9.8593 18.6016 12.7890 15.1563 C 13.5155 14.2422 13.5624 13.1406 12.7890 12.3203 C 12.0155 11.4766 10.7030 11.5469 9.8593 12.6016 C 6.2733 16.7734 4.0937 22.1641 4.0937 28 C 4.0937 41.0781 14.9218 51.9063 27.9999 51.9063 Z M 31.7499 31.6094 C 33.6014 29.6875 33.2265 27.0625 30.9999 25.5156 L 18.6014 16.8672 C 17.4296 16.0469 16.2109 17.2656 17.0312 18.4375 L 25.6796 30.8359 C 27.2265 33.0625 29.8514 33.4609 31.7499 31.6094 Z" />
+</svg>
diff --git a/src/ui/panes/messages_viewer.rs b/src/ui/panes/messages_viewer.rs
index 3d85aef8ecefb2c54be85d088e1e52fd13327540..cfee6c67ab5601d72b53852581f51c76d07c21da 100644
--- a/src/ui/panes/messages_viewer.rs
+++ b/src/ui/panes/messages_viewer.rs
@@ -5,24 +5,14 @@ use crate::ui::app::PaneResponse;
 
 use super::PaneBehavior;
 
-#[derive(Clone, Debug, Default, Serialize, Deserialize)]
-pub struct MessagesViewerPane {
-    #[serde(skip)]
-    contains_pointer: bool,
-}
-
-impl PartialEq for MessagesViewerPane {
-    fn eq(&self, _other: &Self) -> bool {
-        true
-    }
-}
+#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq)]
+pub struct MessagesViewerPane;
 
 impl PaneBehavior for MessagesViewerPane {
     #[profiling::function]
     fn ui(&mut self, ui: &mut Ui) -> PaneResponse {
         let mut response = PaneResponse::default();
         let label = ui.add_sized(ui.available_size(), Label::new("This is a label"));
-        self.contains_pointer = label.contains_pointer();
         if label.drag_started() {
             response.set_drag_started();
         }
diff --git a/src/ui/panes/plot/source_window.rs b/src/ui/panes/plot/source_window.rs
index a62d1f9e41bcb3154c7bd4197327fc62c37a7306..c3098783f3ccad53e7ebf0e8e1b4c49ef381e447 100644
--- a/src/ui/panes/plot/source_window.rs
+++ b/src/ui/panes/plot/source_window.rs
@@ -14,6 +14,7 @@ pub fn sources_window(ui: &mut egui::Ui, plot_settings: &mut PlotSettings) {
             egui::DragValue::new(&mut points_lifespan_sec)
                 .range(5..=1800)
                 .speed(1)
+                .update_while_editing(false)
                 .suffix(" seconds"),
         );
         res1.union(res2)
diff --git a/src/ui/panes/valve_control.rs b/src/ui/panes/valve_control.rs
index 4fdf47d0f52a9a540e6c7c2534deec32a5f9f12f..1e781c90d3377ad6575f55dcaf25e0a9281a7607 100644
--- a/src/ui/panes/valve_control.rs
+++ b/src/ui/panes/valve_control.rs
@@ -1,17 +1,323 @@
-use egui::Ui;
+mod commands;
+mod icons;
+mod valves;
+
+use std::time::{Duration, Instant};
+
+use egui::{
+    Color32, DragValue, Frame, Label, Rect, RichText, Sense, Stroke, Ui, UiBuilder, Vec2, Widget,
+    vec2,
+};
+use egui_extras::{Size, StripBuilder};
+use itertools::Itertools;
 use serde::{Deserialize, Serialize};
+use skyward_mavlink::{
+    mavlink::MessageData,
+    orion::{ACK_TM_DATA, NACK_TM_DATA, WACK_TM_DATA},
+};
+use strum::IntoEnumIterator;
+use tracing::info;
 
-use crate::ui::app::PaneResponse;
+use crate::{
+    mavlink::{MavMessage, TimedMessage},
+    ui::app::PaneResponse,
+};
 
 use super::PaneBehavior;
 
-mod enums;
+use commands::CommandSM;
+use icons::Icon;
+use valves::{Valve, ValveStateManager};
+
+const DEFAULT_AUTO_REFRESH_RATE: Duration = Duration::from_secs(1);
 
 #[derive(Clone, PartialEq, Default, Serialize, Deserialize, Debug)]
-pub struct ValveControlPane {}
+pub struct ValveControlPane {
+    // INTERNAL
+    #[serde(skip)]
+    valves_state: ValveStateManager,
+
+    // VALVE COMMANDS LIST
+    #[serde(skip)]
+    commands: Vec<CommandSM>,
+
+    // REFRESH SETTINGS
+    auto_refresh: Option<Duration>,
+
+    #[serde(skip)]
+    manual_refresh: bool,
+
+    #[serde(skip)]
+    last_refresh: Option<Instant>,
+
+    // UI SETTINGS
+    #[serde(skip)]
+    is_settings_window_open: bool,
+}
 
 impl PaneBehavior for ValveControlPane {
     fn ui(&mut self, ui: &mut Ui) -> PaneResponse {
-        todo!()
+        let mut pane_response = PaneResponse::default();
+
+        let res = ui
+            .scope_builder(UiBuilder::new().sense(Sense::click_and_drag()), |ui| {
+                self.pane_ui()(ui);
+                ui.allocate_space(ui.available_size());
+            })
+            .response;
+
+        // Show the menu when the user right-clicks the pane
+        res.context_menu(self.menu_ui());
+
+        // Check if the user started dragging the pane
+        if res.drag_started() {
+            pane_response.set_drag_started();
+        }
+
+        egui::Window::new("Settings")
+            .id(ui.auto_id_with("settings"))
+            .auto_sized()
+            .collapsible(true)
+            .movable(true)
+            .open(&mut self.is_settings_window_open)
+            .show(ui.ctx(), Self::window_ui(&mut self.auto_refresh));
+
+        pane_response
+    }
+
+    fn get_message_subscriptions(&self) -> Box<dyn Iterator<Item = u32>> {
+        let mut subscriptions = vec![];
+        if self.needs_refresh() {
+            // TODO
+            // subscriptions.push();
+        }
+
+        // Subscribe to ACK, NACK, WACK messages if any command is waiting for a response
+        if self.commands.iter().any(CommandSM::is_waiting_for_response) {
+            subscriptions.push(ACK_TM_DATA::ID);
+            subscriptions.push(NACK_TM_DATA::ID);
+            subscriptions.push(WACK_TM_DATA::ID);
+        }
+
+        Box::new(subscriptions.into_iter())
+    }
+
+    fn update(&mut self, messages: &[&TimedMessage]) {
+        if self.needs_refresh() {
+            // TODO
+        }
+
+        // Capture any ACK/NACK/WACK messages and update the valve state
+        for message in messages {
+            for cmd in self.commands.iter_mut() {
+                // intercept all ACK/NACK/WACK messages
+                cmd.capture_response(&message.message);
+                // If a response was captured, consume the command and update the valve state
+                if let Some((valve, parameter)) = cmd.consume_response() {
+                    self.valves_state.set_parameter_of(valve, parameter);
+                }
+            }
+
+            // Remove consumed commands
+            self.commands.retain(|cmd| !cmd.is_consumed());
+        }
+
+        self.reset_last_refresh();
+    }
+
+    fn drain_outgoing_messages(&mut self) -> Vec<MavMessage> {
+        let mut outgoing = vec![];
+
+        // Pack and send the next command
+        for cmd in self.commands.iter_mut() {
+            if let Some(message) = cmd.pack_and_wait() {
+                outgoing.push(message);
+            }
+        }
+
+        outgoing
+    }
+}
+
+// ┌────────────────────────┐
+// │       UI METHODS       │
+// └────────────────────────┘
+impl ValveControlPane {
+    fn pane_ui(&mut self) -> impl FnOnce(&mut Ui) {
+        |ui| {
+            let valve_chunks = Valve::iter().chunks(3);
+            StripBuilder::new(ui)
+                .sizes(Size::remainder(), 3)
+                .vertical(|mut strip| {
+                    for chunk in &valve_chunks {
+                        strip.strip(|builder| {
+                            builder.sizes(Size::remainder(), 3).horizontal(|mut strip| {
+                                for valve in chunk {
+                                    strip.cell(self.valve_frame_ui(valve));
+                                }
+                            });
+                        });
+                    }
+                });
+        }
+    }
+
+    fn menu_ui(&mut self) -> impl FnOnce(&mut Ui) {
+        |ui| {
+            if ui.button("Refresh now").clicked() {
+                self.manual_refresh = true;
+                ui.close_menu();
+            }
+            if ui.button("Settings").clicked() {
+                self.is_settings_window_open = true;
+                ui.close_menu();
+            }
+        }
+    }
+
+    fn window_ui(auto_refresh_setting: &mut Option<Duration>) -> impl FnOnce(&mut Ui) {
+        |ui| {
+            // Display auto refresh setting
+            let mut auto_refresh = auto_refresh_setting.is_some();
+            ui.horizontal(|ui| {
+                ui.checkbox(&mut auto_refresh, "Auto Refresh");
+                if auto_refresh {
+                    let auto_refresh_duration =
+                        auto_refresh_setting.get_or_insert(DEFAULT_AUTO_REFRESH_RATE);
+                    let mut auto_refresh_rate = 1. / auto_refresh_duration.as_secs_f32();
+                    DragValue::new(&mut auto_refresh_rate)
+                        .speed(0.1)
+                        .range(0.1..=10.0)
+                        .fixed_decimals(1)
+                        .update_while_editing(false)
+                        .suffix(" Hz")
+                        .ui(ui);
+                    *auto_refresh_duration = Duration::from_secs_f32(1. / auto_refresh_rate);
+                } else {
+                    *auto_refresh_setting = None;
+                }
+            });
+        }
+    }
+
+    fn valve_frame_ui(&self, valve: Valve) -> impl FnOnce(&mut Ui) {
+        move |ui| {
+            let valve_str = valve.to_string();
+            let timing = self.valves_state.get_timing_for(valve);
+            let aperture = self.valves_state.get_aperture_for(valve);
+
+            let timing_str = match timing {
+                valves::ParameterValue::Valid(value) => {
+                    format!("{} [ms]", value)
+                }
+                valves::ParameterValue::Missing => "N/A".to_owned(),
+                valves::ParameterValue::Invalid(err_id) => {
+                    format!("ERROR: {}", err_id)
+                }
+            };
+            let aperture_str = match aperture {
+                valves::ParameterValue::Valid(value) => {
+                    format!("{}", value)
+                }
+                valves::ParameterValue::Missing => "N/A".to_owned(),
+                valves::ParameterValue::Invalid(err_id) => {
+                    format!("ERROR: {}", err_id)
+                }
+            };
+
+            let inside_frame = |ui: &mut Ui| {
+                let response = ui.response();
+                let visuals = ui.style().interact(&response);
+                let text_color = visuals.text_color();
+                let icon_size = Vec2::splat(17.);
+
+                StripBuilder::new(ui)
+                    .size(Size::exact(20.))
+                    .size(Size::exact(20.))
+                    .vertical(|mut strip| {
+                        strip.cell(|ui| {
+                            Label::new(
+                                RichText::new(valve_str.to_ascii_uppercase())
+                                    .color(text_color)
+                                    .size(16.0),
+                            )
+                            .selectable(false)
+                            .ui(ui);
+                        });
+                        strip.cell(|ui| {
+                            ui.vertical(|ui| {
+                                ui.horizontal(|ui| {
+                                    let rect = Rect::from_min_size(ui.cursor().min, icon_size);
+                                    Icon::Timing.paint(ui, rect);
+                                    ui.allocate_rect(rect, Sense::hover());
+                                    Label::new(format!("Timing: {}", timing_str))
+                                        .selectable(false)
+                                        .ui(ui);
+                                });
+                                ui.horizontal(|ui| {
+                                    let rect = Rect::from_min_size(ui.cursor().min, icon_size);
+                                    Icon::Aperture.paint(ui, rect);
+                                    ui.allocate_rect(rect, Sense::hover());
+                                    Label::new(format!("Aperture: {}", aperture_str))
+                                        .selectable(false)
+                                        .ui(ui);
+                                });
+                            });
+                        });
+                    });
+            };
+
+            ui.scope_builder(
+                UiBuilder::new()
+                    .id_salt("valve_".to_owned() + &valve_str)
+                    .sense(Sense::click()),
+                |ui| {
+                    ui.set_width(200.);
+                    ui.set_height(60.);
+                    let response = ui.response();
+                    let visuals = ui.style().interact(&response);
+
+                    let fill_color = if response.hovered() {
+                        visuals.bg_fill
+                    } else {
+                        visuals.bg_fill.gamma_multiply(0.3)
+                    };
+
+                    Frame::canvas(ui.style())
+                        .fill(fill_color)
+                        .stroke(Stroke::NONE)
+                        .inner_margin(ui.spacing().menu_margin)
+                        .show(ui, inside_frame);
+
+                    if response.clicked() {
+                        info!("Clicked!");
+                    }
+                },
+            );
+        }
+    }
+}
+
+// ┌───────────────────────────┐
+// │       UTILS METHODS       │
+// └───────────────────────────┘
+impl ValveControlPane {
+    fn needs_refresh(&self) -> bool {
+        // manual trigger of refresh
+        let manual_triggered = self.manual_refresh;
+        // automatic trigger of refresh
+        let auto_triggered = if let Some(auto_refresh) = self.auto_refresh {
+            self.last_refresh
+                .is_none_or(|last| last.elapsed() >= auto_refresh)
+        } else {
+            false
+        };
+
+        manual_triggered || auto_triggered
+    }
+
+    fn reset_last_refresh(&mut self) {
+        self.last_refresh = Some(Instant::now());
+        self.manual_refresh = false;
     }
 }
diff --git a/src/ui/panes/valve_control/commands.rs b/src/ui/panes/valve_control/commands.rs
new file mode 100644
index 0000000000000000000000000000000000000000..8593fd9be94a5e594de9cae0dc8a635bc81e1209
--- /dev/null
+++ b/src/ui/panes/valve_control/commands.rs
@@ -0,0 +1,163 @@
+use crate::mavlink::{
+    ACK_TM_DATA, MavMessage, MessageData, NACK_TM_DATA, SET_ATOMIC_VALVE_TIMING_TC_DATA,
+    SET_VALVE_MAXIMUM_APERTURE_TC_DATA, WACK_TM_DATA,
+};
+
+use super::valves::{ParameterValue, Valve, ValveParameter};
+
+#[derive(Debug, Clone, PartialEq)]
+pub enum CommandSM {
+    Request(Command),
+    WaitingForResponse(Command),
+    Response((Valve, ValveParameter)),
+    Consumed,
+}
+
+impl CommandSM {
+    pub fn pack_and_wait(&mut self) -> Option<MavMessage> {
+        match self {
+            CommandSM::Request(command) => {
+                let message = MavMessage::from(command.clone());
+                *self = CommandSM::WaitingForResponse(command.clone());
+                Some(message)
+            }
+            _ => None,
+        }
+    }
+
+    pub fn capture_response(&mut self, message: &MavMessage) {
+        if let CommandSM::WaitingForResponse(Command { kind, valve }) = self {
+            let id = kind.message_id() as u8;
+            match message {
+                MavMessage::ACK_TM(ACK_TM_DATA { recv_msgid, .. }) if *recv_msgid == id => {
+                    *self = CommandSM::Response((*valve, kind.to_valid_parameter()));
+                }
+                MavMessage::NACK_TM(NACK_TM_DATA {
+                    err_id, recv_msgid, ..
+                }) if *recv_msgid == id => {
+                    *self = CommandSM::Response((*valve, kind.to_invalid_parameter(*err_id)));
+                }
+                MavMessage::WACK_TM(WACK_TM_DATA {
+                    err_id, recv_msgid, ..
+                }) if *recv_msgid == id => {
+                    *self = CommandSM::Response((*valve, kind.to_invalid_parameter(*err_id)));
+                }
+                _ => {}
+            }
+        }
+    }
+
+    pub fn consume_response(&mut self) -> Option<(Valve, ValveParameter)> {
+        match self {
+            CommandSM::Response((valve, parameter)) => {
+                let res = Some((*valve, parameter.clone()));
+                *self = CommandSM::Consumed;
+                res
+            }
+            _ => None,
+        }
+    }
+
+    pub fn is_waiting_for_response(&self) -> bool {
+        matches!(self, CommandSM::WaitingForResponse(_))
+    }
+
+    pub fn is_consumed(&self) -> bool {
+        matches!(self, CommandSM::Consumed)
+    }
+}
+
+impl From<Command> for CommandSM {
+    fn from(value: Command) -> Self {
+        CommandSM::Request(value)
+    }
+}
+
+trait ControllableValves {
+    fn set_atomic_valve_timing(self, timing: u32) -> Command;
+    fn set_valve_maximum_aperture(self, aperture: f32) -> Command;
+}
+
+impl ControllableValves for Valve {
+    fn set_atomic_valve_timing(self, timing: u32) -> Command {
+        Command {
+            kind: CommandKind::SetAtomicValveTiming(timing),
+            valve: self,
+        }
+    }
+
+    fn set_valve_maximum_aperture(self, aperture: f32) -> Command {
+        Command {
+            kind: CommandKind::SetValveMaximumAperture(aperture),
+            valve: self,
+        }
+    }
+}
+
+#[derive(Debug, Clone, PartialEq)]
+pub struct Command {
+    kind: CommandKind,
+    valve: Valve,
+}
+
+impl From<Command> for MavMessage {
+    fn from(value: Command) -> Self {
+        match value.kind {
+            CommandKind::SetAtomicValveTiming(timing) => {
+                MavMessage::SET_ATOMIC_VALVE_TIMING_TC(SET_ATOMIC_VALVE_TIMING_TC_DATA {
+                    servo_id: value.valve.into(),
+                    maximum_timing: timing,
+                })
+            }
+            CommandKind::SetValveMaximumAperture(aperture) => {
+                MavMessage::SET_VALVE_MAXIMUM_APERTURE_TC(SET_VALVE_MAXIMUM_APERTURE_TC_DATA {
+                    servo_id: value.valve.into(),
+                    maximum_aperture: aperture,
+                })
+            }
+        }
+    }
+}
+
+#[derive(Debug, Clone, Copy, PartialEq)]
+enum CommandKind {
+    SetAtomicValveTiming(u32),
+    SetValveMaximumAperture(f32),
+}
+
+impl CommandKind {
+    fn message_id(&self) -> u32 {
+        match self {
+            CommandKind::SetAtomicValveTiming(_) => SET_ATOMIC_VALVE_TIMING_TC_DATA::ID,
+            CommandKind::SetValveMaximumAperture(_) => SET_VALVE_MAXIMUM_APERTURE_TC_DATA::ID,
+        }
+    }
+
+    fn to_valid_parameter(&self) -> ValveParameter {
+        (*self).into()
+    }
+
+    fn to_invalid_parameter(&self, error: u16) -> ValveParameter {
+        match self {
+            CommandKind::SetAtomicValveTiming(_) => {
+                ValveParameter::AtomicValveTiming(ParameterValue::Invalid(error))
+            }
+            CommandKind::SetValveMaximumAperture(_) => {
+                ValveParameter::ValveMaximumAperture(ParameterValue::Invalid(error))
+            }
+        }
+    }
+}
+
+impl From<CommandKind> for ValveParameter {
+    fn from(value: CommandKind) -> Self {
+        match value {
+            CommandKind::SetAtomicValveTiming(timing) => {
+                ValveParameter::AtomicValveTiming(ParameterValue::Valid(timing))
+            }
+            CommandKind::SetValveMaximumAperture(aperture) => {
+                ValveParameter::ValveMaximumAperture(ParameterValue::Valid(aperture))
+            }
+        }
+    }
+}
diff --git a/src/ui/panes/valve_control/enums.rs b/src/ui/panes/valve_control/enums.rs
deleted file mode 100644
index a1bf8fcdd341d13f6c306c69de358c3d5912122f..0000000000000000000000000000000000000000
--- a/src/ui/panes/valve_control/enums.rs
+++ /dev/null
@@ -1,68 +0,0 @@
-use std::fmt::Display;
-
-use strum_macros::EnumIter;
-
-use crate::mavlink::{
-    MessageData, SET_ATOMIC_VALVE_TIMING_TC_DATA, SET_VALVE_MAXIMUM_APERTURE_TC_DATA, Servoslist,
-};
-
-#[allow(non_camel_case_types)]
-#[derive(Clone, Copy, Debug, PartialEq, Eq, EnumIter)]
-pub enum Valve {
-    OxFilling,
-    OxRelease,
-    OxVenting,
-    N2Filling,
-    N2Release,
-    N2Quenching,
-    N23Way,
-    Main,
-    Nitrogen,
-}
-
-impl From<Valve> for Servoslist {
-    fn from(valve: Valve) -> Servoslist {
-        match valve {
-            Valve::OxFilling => Servoslist::OX_FILLING_VALVE,
-            Valve::OxRelease => Servoslist::OX_RELEASE_VALVE,
-            Valve::OxVenting => Servoslist::OX_VENTING_VALVE,
-            Valve::N2Filling => Servoslist::N2_FILLING_VALVE,
-            Valve::N2Release => Servoslist::N2_RELEASE_VALVE,
-            Valve::N2Quenching => Servoslist::N2_QUENCHING_VALVE,
-            Valve::N23Way => Servoslist::N2_3WAY_VALVE,
-            Valve::Main => Servoslist::MAIN_VALVE,
-            Valve::Nitrogen => Servoslist::NITROGEN_VALVE,
-        }
-    }
-}
-
-impl Display for Valve {
-    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
-        match self {
-            Valve::OxFilling => write!(f, "Oxidizer Filling"),
-            Valve::OxRelease => write!(f, "Oxidizer Release"),
-            Valve::OxVenting => write!(f, "Oxidizer Venting"),
-            Valve::N2Filling => write!(f, "Nitrogen Filling"),
-            Valve::N2Release => write!(f, "Nitrogen Release"),
-            Valve::N2Quenching => write!(f, "Nitrogen Quenching"),
-            Valve::N23Way => write!(f, "Nitrogen 3-Way"),
-            Valve::Main => write!(f, "Main"),
-            Valve::Nitrogen => write!(f, "Nitrogen"),
-        }
-    }
-}
-
-#[derive(Clone, Copy, Debug, PartialEq, Eq, EnumIter)]
-pub enum ValveCommands {
-    AtomicValveTiming,
-    ValveMaximumAperture,
-}
-
-impl From<ValveCommands> for u32 {
-    fn from(command: ValveCommands) -> u32 {
-        match command {
-            ValveCommands::AtomicValveTiming => SET_ATOMIC_VALVE_TIMING_TC_DATA::ID,
-            ValveCommands::ValveMaximumAperture => SET_VALVE_MAXIMUM_APERTURE_TC_DATA::ID,
-        }
-    }
-}
diff --git a/src/ui/panes/valve_control/icons.rs b/src/ui/panes/valve_control/icons.rs
new file mode 100644
index 0000000000000000000000000000000000000000..064fee550c449c9a1a8e7ff479068f79f942d0b5
--- /dev/null
+++ b/src/ui/panes/valve_control/icons.rs
@@ -0,0 +1,45 @@
+use egui::{ImageSource, Rect, Theme, Ui};
+
+#[derive(Debug, Clone, Copy)]
+pub enum Icon {
+    Aperture,
+    Timing,
+}
+
+impl Icon {
+    fn get_image(&self, theme: Theme) -> ImageSource {
+        match (&self, theme) {
+            (Icon::Aperture, Theme::Light) => {
+                egui::include_image!(concat!(
+                    env!("CARGO_MANIFEST_DIR"),
+                    "/icons/valve_control/light/aperture.svg"
+                ))
+            }
+            (Icon::Aperture, Theme::Dark) => {
+                egui::include_image!(concat!(
+                    env!("CARGO_MANIFEST_DIR"),
+                    "/icons/valve_control/dark/aperture.svg"
+                ))
+            }
+            (Icon::Timing, Theme::Light) => {
+                egui::include_image!(concat!(
+                    env!("CARGO_MANIFEST_DIR"),
+                    "/icons/valve_control/light/timing.svg"
+                ))
+            }
+            (Icon::Timing, Theme::Dark) => {
+                egui::include_image!(concat!(
+                    env!("CARGO_MANIFEST_DIR"),
+                    "/icons/valve_control/dark/timing.svg"
+                ))
+            }
+        }
+    }
+}
+
+impl Icon {
+    pub fn paint(&mut self, ui: &mut Ui, image_rect: Rect) {
+        let theme = ui.ctx().theme();
+        egui::Image::new(self.get_image(theme)).paint_at(ui, image_rect);
+    }
+}
diff --git a/src/ui/panes/valve_control/valves.rs b/src/ui/panes/valve_control/valves.rs
new file mode 100644
index 0000000000000000000000000000000000000000..9580dc169d8000ea01aaa283c596c18e68c12231
--- /dev/null
+++ b/src/ui/panes/valve_control/valves.rs
@@ -0,0 +1,139 @@
+//! Valve Control Pane
+//!
+//! NOTE: We assume that no more than one entity will sent messages to control valves at a time.
+
+use std::fmt::Display;
+
+use strum::IntoEnumIterator;
+use strum_macros::EnumIter;
+
+use crate::{error::ErrInstrument, mavlink::Servoslist};
+
+#[derive(Clone, Debug, PartialEq)]
+pub struct ValveStateManager {
+    settings: Vec<(Valve, ValveParameter)>,
+}
+
+impl Default for ValveStateManager {
+    fn default() -> Self {
+        Self::new()
+    }
+}
+
+impl ValveStateManager {
+    pub fn new() -> Self {
+        let settings = Valve::iter()
+            .flat_map(|valve| ValveParameter::iter().map(move |parameter| (valve, parameter)))
+            .collect();
+        Self { settings }
+    }
+
+    pub fn set_parameter_of(&mut self, valve: Valve, parameter: ValveParameter) {
+        let (_, par) = self
+            .settings
+            .iter_mut()
+            .find(|(v, _)| *v == valve)
+            .log_unwrap();
+        *par = parameter;
+    }
+
+    pub fn get_timing_for(&self, valve: Valve) -> ParameterValue<u32, u16> {
+        for (_, par) in self.settings.iter().filter(|(v, _)| *v == valve) {
+            match par {
+                ValveParameter::AtomicValveTiming(parameter_value) => {
+                    return parameter_value.clone();
+                }
+                _ => continue,
+            };
+        }
+        unreachable!()
+    }
+
+    pub fn get_aperture_for(&self, valve: Valve) -> ParameterValue<f32, u16> {
+        for (_, par) in self.settings.iter().filter(|(v, _)| *v == valve) {
+            match par {
+                ValveParameter::ValveMaximumAperture(parameter_value) => {
+                    return parameter_value.clone();
+                }
+                _ => continue,
+            };
+        }
+        unreachable!()
+    }
+}
+
+#[allow(non_camel_case_types)]
+#[derive(Clone, Copy, Debug, PartialEq, Eq, EnumIter)]
+pub enum Valve {
+    OxFilling,
+    OxRelease,
+    OxVenting,
+    N2Filling,
+    N2Release,
+    N2Quenching,
+    N23Way,
+    Main,
+    Nitrogen,
+}
+
+impl From<Valve> for Servoslist {
+    fn from(valve: Valve) -> Servoslist {
+        match valve {
+            Valve::OxFilling => Servoslist::OX_FILLING_VALVE,
+            Valve::OxRelease => Servoslist::OX_RELEASE_VALVE,
+            Valve::OxVenting => Servoslist::OX_VENTING_VALVE,
+            Valve::N2Filling => Servoslist::N2_FILLING_VALVE,
+            Valve::N2Release => Servoslist::N2_RELEASE_VALVE,
+            Valve::N2Quenching => Servoslist::N2_QUENCHING_VALVE,
+            Valve::N23Way => Servoslist::N2_3WAY_VALVE,
+            Valve::Main => Servoslist::MAIN_VALVE,
+            Valve::Nitrogen => Servoslist::NITROGEN_VALVE,
+        }
+    }
+}
+
+impl From<Valve> for u8 {
+    fn from(valve: Valve) -> u8 {
+        Servoslist::from(valve) as u8
+    }
+}
+
+impl Display for Valve {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        match self {
+            Valve::OxFilling => write!(f, "Oxidizer Filling"),
+            Valve::OxRelease => write!(f, "Oxidizer Release"),
+            Valve::OxVenting => write!(f, "Oxidizer Venting"),
+            Valve::N2Filling => write!(f, "Nitrogen Filling"),
+            Valve::N2Release => write!(f, "Nitrogen Release"),
+            Valve::N2Quenching => write!(f, "Nitrogen Quenching"),
+            Valve::N23Way => write!(f, "Nitrogen 3-Way"),
+            Valve::Main => write!(f, "Main"),
+            Valve::Nitrogen => write!(f, "Nitrogen"),
+        }
+    }
+}
+
+#[derive(Clone, Debug, PartialEq, EnumIter)]
+pub enum ValveParameter {
+    AtomicValveTiming(ParameterValue<u32, u16>),
+    ValveMaximumAperture(ParameterValue<f32, u16>),
+}
+
+#[derive(Clone, Debug, PartialEq, Default)]
+pub enum ParameterValue<T, E> {
+    Valid(T), // T is the valid parameter value
+    #[default]
+    Missing, // The parameter is missing
+    Invalid(E), // E is the reason why the parameter is invalid
+}
+
+impl<T: Display, E: Display> Display for ParameterValue<T, E> {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        match self {
+            ParameterValue::Valid(value) => write!(f, "{}", value),
+            ParameterValue::Missing => write!(f, "MISSING"),
+            ParameterValue::Invalid(error) => write!(f, "INVALID: {}", error),
+        }
+    }
+}