aquarium_control/dispatch/messaging_domain.rs
1/* Copyright 2025 Uwe Martin
2
3Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4
5The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6
7THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
8*/
9
10//! Defines the domains (subsystems) that can be controlled via Inter-Process Communication (IPC).
11//!
12//! This module serves as a dictionary for the application's controllable parts, such as
13//! `Refill`, `Heating`, and `Feed`. It provides both the raw integer constants used in the
14//! low-level POSIX message queue protocol and a type-safe Rust `enum` for use in the
15//! application's internal logic.
16//!
17//! This entire module is conditionally compiled and is only available on `target_os = "linux"`.
18//!
19//! ## Key Components
20//!
21//! - **`messaging_domains` Module**: A collection of `i32` constants that represent the
22//! unique identifier for each domain in the raw byte message. These are the "on-the-wire"
23//! values that form the external contract with command-line tools.
24//!
25//! - **`MessagingDomain` Enum**: A type-safe Rust representation of the domains. The main
26//! `Messaging` thread translates the raw integer from an incoming message into one of
27//! these variants before dispatching a command. This prevents logic errors related
28//! to "magic numbers."
29//!
30//! ## Design and Purpose
31//!
32//! The separation between the raw constants and the type-safe enum is a deliberate design
33//! choice that enhances robustness.
34//!
35//! - **Protocol Stability**: The `messaging_domains` constants define a stable, language-agnostic
36//! protocol. External C or Python scripts can use these same integer values
37//! to communicate with the application.
38//!
39//! - **Type Safety**: By converting the raw integer to a `MessagingDomain` enum at the
40//! earliest opportunity, the rest of the application can use pattern matching and
41//! benefit from the compiler's checks, eliminating a whole class of potential bugs.
42//!
43//! - **Runtime Validation**: The `is_domain_thread_executed` method provides a way to
44//! check if a target domain's thread is actually running before attempting to send
45//! a message, preventing unnecessary channel errors for disabled features.
46//!
47//! ### Example Flow
48//!
49//! 1. An external tool sends a message with the domain field set to `1`.
50//! 2. The `Messaging` thread receives the message and reads the integer `1`.
51//! 3. It matches `1` to `messaging_domains::REFILL`.
52//! 4. It converts this into the `MessagingDomain::Refill` enum variant.
53//! 5. It then uses this enum variant to route the command to the correct channel.
54
55#[cfg(target_os = "linux")]
56use crate::launch::execution_config::ExecutionConfig;
57#[cfg(target_os = "linux")]
58use std::fmt;
59
60#[derive(PartialEq, Debug, Clone)]
61/// Identifies the addressee of the message
62#[cfg(target_os = "linux")]
63#[repr(i32)]
64pub enum MessagingDomain {
65 /// refill control
66 Refill = 1,
67
68 /// ventilation control
69 Ventilation = 3,
70
71 /// feed control
72 Feed = 8,
73
74 /// heating control
75 Heating = 9,
76
77 /// Balling dosing control
78 Balling = 11,
79
80 /// monitors
81 Monitors = 12,
82
83 /// watchdog communication
84 Watchdog = 13,
85
86 /// in case the numeric value in the message could not be assigned to one of the above
87 Unknown = -1,
88}
89
90#[cfg(target_os = "linux")]
91impl From<i32> for MessagingDomain {
92 fn from(value: i32) -> Self {
93 match value {
94 x if x == MessagingDomain::Refill as i32 => MessagingDomain::Refill,
95 x if x == MessagingDomain::Ventilation as i32 => MessagingDomain::Ventilation,
96 x if x == MessagingDomain::Feed as i32 => MessagingDomain::Feed,
97 x if x == MessagingDomain::Heating as i32 => MessagingDomain::Heating,
98 x if x == MessagingDomain::Balling as i32 => MessagingDomain::Balling,
99 x if x == MessagingDomain::Monitors as i32 => MessagingDomain::Monitors,
100 x if x == MessagingDomain::Watchdog as i32 => MessagingDomain::Watchdog,
101 _ => MessagingDomain::Unknown,
102 }
103 }
104}
105
106#[cfg(all(target_os = "linux", test))]
107impl From<MessagingDomain> for i32 {
108 fn from(domain: MessagingDomain) -> Self {
109 match domain {
110 MessagingDomain::Refill => 1,
111 MessagingDomain::Ventilation => 3,
112 MessagingDomain::Feed => 8,
113 MessagingDomain::Heating => 9,
114 MessagingDomain::Balling => 11,
115 MessagingDomain::Monitors => 12,
116 MessagingDomain::Watchdog => 13,
117 MessagingDomain::Unknown => -1,
118 }
119 }
120}
121
122#[cfg(target_os = "linux")]
123impl MessagingDomain {
124 /// Checks if the thread corresponding to a specific messaging domain is active.
125 ///
126 /// This method consults the provided `ExecutionConfig` to determine if the thread
127 /// responsible for handling a given `MessagingDomain` was started at application launch.
128 /// This is useful for preventing attempts to send messages to threads that are not
129 /// running, which would otherwise result in channel errors.
130 ///
131 /// # Arguments
132 /// * `execution_config` - A reference to the application's runtime execution
133 /// configuration, which holds the active status of each thread.
134 ///
135 /// # Returns
136 /// Returns `true` if the thread for the domain is configured as active, and `false`
137 /// if the thread is inactive or if the domain is `MessagingDomain::Unknown`.
138 pub fn is_domain_thread_executed(&self, execution_config: &ExecutionConfig) -> bool {
139 match self {
140 MessagingDomain::Refill => execution_config.refill,
141 MessagingDomain::Ventilation => execution_config.ventilation,
142 MessagingDomain::Feed => execution_config.feed,
143 MessagingDomain::Heating => execution_config.heating,
144 MessagingDomain::Balling => execution_config.balling,
145 MessagingDomain::Monitors => execution_config.monitors,
146 MessagingDomain::Watchdog => execution_config.watchdog,
147 MessagingDomain::Unknown => false,
148 }
149 }
150}
151
152#[cfg(target_os = "linux")]
153impl fmt::Display for MessagingDomain {
154 /// Formats the `MessagingDomain` enum into its human-readable string representation.
155 ///
156 /// This implementation allows `MessagingDomain` variants to be displayed directly
157 /// using macros like `println!` or `format!`, returning the name of the domain.
158 ///
159 /// # Arguments
160 /// * `f` - A mutable reference to the formatter, as required by the `fmt::Display` trait.
161 ///
162 /// # Returns
163 /// A `fmt::Result` indicating whether the formatting operation was successful.
164 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
165 match self {
166 MessagingDomain::Refill => write!(f, "Refill"),
167 MessagingDomain::Ventilation => write!(f, "Ventilation"),
168 MessagingDomain::Feed => write!(f, "Feed"),
169 MessagingDomain::Heating => write!(f, "Heating"),
170 MessagingDomain::Balling => write!(f, "Balling"),
171 MessagingDomain::Monitors => write!(f, "Monitors"),
172 MessagingDomain::Watchdog => write!(f, "Watchdog"),
173 MessagingDomain::Unknown => write!(f, "Unknown"),
174 }
175 }
176}