Skip to main content

leodos_libcfs/
app.rs

1//! Standard cFS application scaffolding.
2//!
3//! Provides an [`App`] builder that automates the common
4//! boilerplate every cFS app needs: pipe creation, topic
5//! subscription, EVS registration, NoOp/Reset command
6//! handling, and housekeeping telemetry.
7
8use crate::cfe::evs::event;
9use crate::cfe::sb::msg::{CmdHeader, MessageRef, MsgId, TlmHeader};
10use crate::cfe::sb::pipe::Pipe;
11use crate::cfe::sb::send_buf::SendBuffer;
12use crate::error::Result;
13
14/// Function code for NoOp command.
15const FCN_NOOP: u16 = 0;
16/// Function code for counter reset command.
17const FCN_RESET: u16 = 1;
18
19/// Event ID for NoOp command acknowledgement.
20const EVT_NOOP: u16 = 0;
21/// Event ID for counter reset acknowledgement.
22const EVT_RESET: u16 = 1;
23/// Event ID for invalid/unknown command code.
24const EVT_INVALID_CC: u16 = 2;
25/// Event ID for application startup.
26const EVT_STARTUP: u16 = 3;
27
28/// Base housekeeping telemetry payload.
29///
30/// Contains the standard cmd/err counters that every cFS
31/// app reports. Placed after the telemetry header in the
32/// HK packet.
33#[repr(C)]
34#[derive(Debug, Clone, Copy, Default)]
35pub struct HkTlm {
36    /// Total accepted commands since last reset.
37    pub cmd_count: u16,
38    /// Total rejected commands since last reset.
39    pub err_count: u16,
40}
41
42/// Event returned by [`App::recv`].
43pub enum Event<'a> {
44    /// An app-specific command (NoOp/Reset already handled).
45    Command(MessageRef<'a>),
46    /// HK wakeup — the app should publish telemetry.
47    Hk,
48}
49
50/// Standard cFS application with automatic boilerplate.
51///
52/// Handles pipe creation, topic subscriptions, EVS
53/// registration, NoOp (fcn 0), and Reset Counters (fcn 1).
54///
55/// HK wakeups are surfaced as [`Event::Hk`] so the app
56/// controls what telemetry to publish.
57///
58/// # Example
59///
60/// ```ignore
61/// let mut app = App::builder()
62///     .name("MY_APP")
63///     .cmd_topic(MY_CMD_TOPIC)
64///     .send_hk_topic(SEND_HK_TOPIC)
65///     .hk_tlm_topic(MY_HK_TLM_TOPIC)
66///     .version("1.0.0")
67///     .build()?;
68///
69/// let mut buf = [0u8; 256];
70/// loop {
71///     match app.recv(&mut buf).await? {
72///         Event::Hk => app.send_hk(&my_hk)?,
73///         Event::Command(msg) => app.reject(msg)?,
74///     }
75/// }
76/// ```
77pub struct App {
78    pipe: Pipe,
79    version: &'static str,
80    cmd_msg_id: MsgId,
81    send_hk_msg_id: MsgId,
82    hk_tlm_msg_id: MsgId,
83    cmd_count: u16,
84    err_count: u16,
85}
86
87#[bon::bon]
88impl App {
89    /// Creates a new cFS application.
90    ///
91    /// Registers with EVS, creates a Software Bus pipe,
92    /// and subscribes to command and HK wakeup topics.
93    ///
94    /// Both `cmd_topic` and `send_hk_topic` are treated
95    /// as local command topic IDs. `hk_tlm_topic` is a
96    /// local telemetry topic ID.
97    #[builder]
98    pub fn new(
99        name: &'static str,
100        cmd_topic: u16,
101        send_hk_topic: u16,
102        hk_tlm_topic: u16,
103        version: &'static str,
104        #[builder(default = 16)] pipe_depth: u16,
105    ) -> Result<Self> {
106        event::register(&[])?;
107
108        let pipe = Pipe::new(name, pipe_depth)?;
109        let cmd_msg_id = MsgId::local_cmd(cmd_topic);
110        let send_hk_msg_id = MsgId::local_cmd(send_hk_topic);
111        let hk_tlm_msg_id = MsgId::local_tlm(hk_tlm_topic);
112
113        pipe.subscribe(cmd_msg_id)?;
114        pipe.subscribe(send_hk_msg_id)?;
115
116        event::info(EVT_STARTUP, version)?;
117
118        Ok(Self {
119            pipe,
120            version,
121            cmd_msg_id,
122            send_hk_msg_id,
123            hk_tlm_msg_id,
124            cmd_count: 0,
125            err_count: 0,
126        })
127    }
128}
129
130impl App {
131    /// Processes commands and HK in a loop.
132    ///
133    /// Publishes base HK (cmd/err counters) on wakeup,
134    /// rejects unrecognized commands. For custom HK or
135    /// app-specific commands, use [`recv`](Self::recv).
136    pub async fn run(&mut self) -> Result<()> {
137        let mut buf = [0u8; 256];
138        loop {
139            match self.recv(&mut buf).await? {
140                Event::Hk => self.send_hk_base()?,
141                Event::Command(msg) => self.reject(msg)?,
142            }
143        }
144    }
145
146    /// Receives the next event from the Software Bus.
147    ///
148    /// Automatically handles NoOp (fcn 0) and Reset (fcn 1).
149    /// Returns [`Event::Hk`] on HK wakeup so the app can
150    /// publish its own telemetry. Returns [`Event::Command`]
151    /// for app-specific commands.
152    pub async fn recv<'a>(&mut self, buf: &'a mut [u8]) -> Result<Event<'a>> {
153        loop {
154            let len = self.pipe.recv(buf).await?;
155            let msg = MessageRef::new(&buf[..len]);
156            let msg_id = msg.msg_id()?;
157
158            if msg_id == self.send_hk_msg_id {
159                return Ok(Event::Hk);
160            }
161
162            if msg_id == self.cmd_msg_id {
163                let cmd_hdr_size = core::mem::size_of::<CmdHeader>();
164                match msg.fcn_code()? {
165                    FCN_NOOP if msg.size()? == cmd_hdr_size => {
166                        self.cmd_count = self.cmd_count.wrapping_add(1);
167                        event::info(EVT_NOOP, self.version)?;
168                    }
169                    FCN_RESET if msg.size()? == cmd_hdr_size => {
170                        self.cmd_count = 0;
171                        self.err_count = 0;
172                        event::info(EVT_RESET, "Counters reset")?;
173                    }
174                    FCN_NOOP | FCN_RESET => {
175                        self.err_count = self.err_count.wrapping_add(1);
176                        event::error(EVT_INVALID_CC, "Wrong message length")?;
177                    }
178                    _ => return Ok(Event::Command(MessageRef::new(&buf[..len]))),
179                }
180            }
181        }
182    }
183
184    /// Acknowledges successful command processing.
185    ///
186    /// Increments the command counter.
187    pub fn ack(&mut self) {
188        self.cmd_count = self.cmd_count.wrapping_add(1);
189    }
190
191    /// Rejects an unrecognized command.
192    ///
193    /// Increments the error counter and sends an error
194    /// event.
195    pub fn reject(&mut self, _msg: MessageRef<'_>) -> Result<()> {
196        self.err_count = self.err_count.wrapping_add(1);
197        event::error(EVT_INVALID_CC, "Invalid command code")
198    }
199
200    /// Publishes a custom HK telemetry packet.
201    ///
202    /// The payload is serialized via `as_bytes()`. The app
203    /// should include cmd/err counters (from
204    /// [`cmd_count`](Self::cmd_count) /
205    /// [`err_count`](Self::err_count)) in the struct.
206    pub fn send_hk<H: Copy>(&self, payload: &H) -> Result<()> {
207        let hdr = core::mem::size_of::<TlmHeader>();
208        let size = hdr + core::mem::size_of::<H>();
209
210        let mut send_buf = SendBuffer::new(size)?;
211        {
212            let mut msg = send_buf.view();
213            msg.init(self.hk_tlm_msg_id, size)?;
214            *msg.payload::<H>()? = *payload;
215            msg.timestamp();
216        }
217        send_buf.send(true)
218    }
219
220    /// Returns the current command counter.
221    pub fn cmd_count(&self) -> u16 {
222        self.cmd_count
223    }
224
225    /// Returns the current error counter.
226    pub fn err_count(&self) -> u16 {
227        self.err_count
228    }
229
230    /// Publishes base HK (cmd/err counters only).
231    fn send_hk_base(&self) -> Result<()> {
232        self.send_hk(&HkTlm {
233            cmd_count: self.cmd_count,
234            err_count: self.err_count,
235        })
236    }
237}