printobserver_types/action.rs
1//! The closed vocabulary an actor may ask for, and the record of one request.
2//!
3//! [`PrintAction`] has no arbitrary-command variant, no G-code field and no
4//! escape hatch, which is the whole point of the vocabulary being closed: it is
5//! the reason an agent is allowed near a machine that can destroy itself. The
6//! only free-form text any variant carries is the `reason` every action
7//! carries, and the only caller-supplied text that reaches the printer's own
8//! file API is a [`FileName`](crate::FileName), which validates itself.
9//!
10//! Filtration is deliberately not a variant: nobody has yet confirmed that the
11//! target printer exposes filtration through `OctoPrint` at all, and an action
12//! the machine cannot perform is one an agent can call that silently does
13//! nothing.
14
15use serde::{Deserialize, Serialize};
16
17use schemars::JsonSchema;
18
19use crate::file_name::FileName;
20use crate::ids::{ActionId, EventId, PrintId};
21use crate::manifest::JobManifest;
22use crate::policy::PolicyDecision;
23use crate::timestamp::Timestamp;
24
25/// Who asked for something.
26#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
27#[serde(rename_all = "snake_case", deny_unknown_fields)]
28pub enum Actor {
29 /// The supervising agent, naming its session.
30 Agent {
31 /// The supervision session the agent is acting in.
32 session_name: String,
33 },
34 /// A person.
35 Operator,
36 /// The supervisor itself.
37 System,
38}
39
40impl Actor {
41 /// The class this actor belongs to.
42 #[must_use]
43 pub const fn class(&self) -> ActorClass {
44 match self {
45 Self::Agent { .. } => ActorClass::Agent,
46 Self::Operator => ActorClass::Operator,
47 Self::System => ActorClass::System,
48 }
49 }
50}
51
52/// An actor class, which is what a safety envelope grants actions to.
53#[derive(
54 Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, JsonSchema,
55)]
56#[serde(rename_all = "snake_case", deny_unknown_fields)]
57pub enum ActorClass {
58 /// The supervising agent.
59 Agent,
60 /// A person.
61 Operator,
62 /// The supervisor itself.
63 System,
64}
65
66/// What an operator's acknowledgement of a failure event asks for next.
67#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
68#[serde(rename_all = "snake_case", deny_unknown_fields)]
69pub enum AcknowledgementDisposition {
70 /// Carry on printing.
71 Continue,
72 /// Carry on printing, watched more closely.
73 Watch,
74 /// Stop the print.
75 Stop,
76}
77
78/// One action of the closed vocabulary, named without its payload.
79///
80/// This is what a safety envelope grants and what a policy rejection names; the
81/// payload lives on [`PrintAction`] itself.
82#[derive(
83 Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, JsonSchema,
84)]
85#[serde(rename_all = "snake_case", deny_unknown_fields)]
86pub enum ActionKind {
87 /// Pause the print.
88 Pause,
89 /// Resume the print.
90 Resume,
91 /// Cancel the print.
92 Cancel,
93 /// Start a print of a named file.
94 StartPrint,
95 /// Set the feedrate factor.
96 SetFeedrateFactor,
97 /// Set the flowrate factor.
98 SetFlowrateFactor,
99 /// Set a tool's target temperature.
100 SetToolTargetC,
101 /// Set the bed's target temperature.
102 SetBedTargetC,
103 /// Set the fan percentage.
104 SetFanPercent,
105 /// Acknowledge a failure event.
106 AcknowledgeFailure,
107}
108
109/// The whole vocabulary an actor may ask for, and there is no other.
110#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
111#[serde(tag = "action", rename_all = "snake_case", deny_unknown_fields)]
112pub enum PrintAction {
113 /// Pause the print.
114 Pause {
115 /// Why the actor is asking for this.
116 reason: String,
117 /// Who is asking.
118 actor: Actor,
119 },
120 /// Resume the print.
121 Resume {
122 /// Why the actor is asking for this.
123 reason: String,
124 /// Who is asking.
125 actor: Actor,
126 },
127 /// Cancel the print.
128 Cancel {
129 /// Why the actor is asking for this.
130 reason: String,
131 /// Who is asking.
132 actor: Actor,
133 },
134 /// Start a print of a named file, with a manifest.
135 StartPrint {
136 /// The file to print, validated as a name a file API can be asked for.
137 file_name: FileName,
138 /// The manifest this print is bounded by.
139 manifest: JobManifest,
140 /// Why the actor is asking for this.
141 reason: String,
142 /// Who is asking.
143 actor: Actor,
144 },
145 /// Set the feedrate factor, where one means one hundred percent.
146 SetFeedrateFactor {
147 /// The multiplier asked for.
148 factor: f64,
149 /// How long the change stands for, in whole seconds.
150 #[serde(default, skip_serializing_if = "Option::is_none")]
151 duration_s: Option<i64>,
152 /// Why the actor is asking for this.
153 reason: String,
154 /// Who is asking.
155 actor: Actor,
156 },
157 /// Set the flowrate factor, where one means one hundred percent.
158 SetFlowrateFactor {
159 /// The multiplier asked for.
160 factor: f64,
161 /// How long the change stands for, in whole seconds.
162 #[serde(default, skip_serializing_if = "Option::is_none")]
163 duration_s: Option<i64>,
164 /// Why the actor is asking for this.
165 reason: String,
166 /// Who is asking.
167 actor: Actor,
168 },
169 /// Set one tool's target temperature.
170 SetToolTargetC {
171 /// The tool, in the printer's own numbering.
172 tool: i64,
173 /// The target temperature asked for, in degrees Celsius.
174 target_c: f64,
175 /// How long the change stands for, in whole seconds.
176 #[serde(default, skip_serializing_if = "Option::is_none")]
177 duration_s: Option<i64>,
178 /// Why the actor is asking for this.
179 reason: String,
180 /// Who is asking.
181 actor: Actor,
182 },
183 /// Set the bed's target temperature.
184 SetBedTargetC {
185 /// The target temperature asked for, in degrees Celsius.
186 target_c: f64,
187 /// How long the change stands for, in whole seconds.
188 #[serde(default, skip_serializing_if = "Option::is_none")]
189 duration_s: Option<i64>,
190 /// Why the actor is asking for this.
191 reason: String,
192 /// Who is asking.
193 actor: Actor,
194 },
195 /// Set the part-cooling fan percentage.
196 SetFanPercent {
197 /// The percentage asked for.
198 percent: f64,
199 /// How long the change stands for, in whole seconds.
200 #[serde(default, skip_serializing_if = "Option::is_none")]
201 duration_s: Option<i64>,
202 /// Why the actor is asking for this.
203 reason: String,
204 /// Who is asking.
205 actor: Actor,
206 },
207 /// Acknowledge a failure event, with a disposition.
208 AcknowledgeFailure {
209 /// The event being acknowledged.
210 event_id: EventId,
211 /// What to do next.
212 disposition: AcknowledgementDisposition,
213 /// Why the actor is asking for this.
214 reason: String,
215 /// Who is asking.
216 actor: Actor,
217 },
218}
219
220impl PrintAction {
221 /// Which action this is, without its payload.
222 #[must_use]
223 pub const fn kind(&self) -> ActionKind {
224 match self {
225 Self::Pause { .. } => ActionKind::Pause,
226 Self::Resume { .. } => ActionKind::Resume,
227 Self::Cancel { .. } => ActionKind::Cancel,
228 Self::StartPrint { .. } => ActionKind::StartPrint,
229 Self::SetFeedrateFactor { .. } => ActionKind::SetFeedrateFactor,
230 Self::SetFlowrateFactor { .. } => ActionKind::SetFlowrateFactor,
231 Self::SetToolTargetC { .. } => ActionKind::SetToolTargetC,
232 Self::SetBedTargetC { .. } => ActionKind::SetBedTargetC,
233 Self::SetFanPercent { .. } => ActionKind::SetFanPercent,
234 Self::AcknowledgeFailure { .. } => ActionKind::AcknowledgeFailure,
235 }
236 }
237
238 /// Why the actor asked for this.
239 #[must_use]
240 pub fn reason(&self) -> &str {
241 match self {
242 Self::Pause { reason, .. }
243 | Self::Resume { reason, .. }
244 | Self::Cancel { reason, .. }
245 | Self::StartPrint { reason, .. }
246 | Self::SetFeedrateFactor { reason, .. }
247 | Self::SetFlowrateFactor { reason, .. }
248 | Self::SetToolTargetC { reason, .. }
249 | Self::SetBedTargetC { reason, .. }
250 | Self::SetFanPercent { reason, .. }
251 | Self::AcknowledgeFailure { reason, .. } => reason,
252 }
253 }
254
255 /// Who asked for this.
256 #[must_use]
257 pub const fn actor(&self) -> &Actor {
258 match self {
259 Self::Pause { actor, .. }
260 | Self::Resume { actor, .. }
261 | Self::Cancel { actor, .. }
262 | Self::StartPrint { actor, .. }
263 | Self::SetFeedrateFactor { actor, .. }
264 | Self::SetFlowrateFactor { actor, .. }
265 | Self::SetToolTargetC { actor, .. }
266 | Self::SetBedTargetC { actor, .. }
267 | Self::SetFanPercent { actor, .. }
268 | Self::AcknowledgeFailure { actor, .. } => actor,
269 }
270 }
271}
272
273/// What happened when an accepted action reached the printer.
274#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
275#[serde(rename_all = "snake_case", deny_unknown_fields)]
276pub enum ExecutionOutcome {
277 /// The printer took it.
278 Succeeded,
279 /// The printer did not, for this reason.
280 Failed {
281 /// Why it failed.
282 reason: String,
283 },
284}
285
286/// One request an actor made, at the instant it made it.
287#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
288#[serde(deny_unknown_fields)]
289pub struct ActionRequest {
290 /// What was asked for.
291 pub action: PrintAction,
292 /// Who asked.
293 pub actor: Actor,
294 /// When they asked.
295 pub requested_at: Timestamp,
296}
297
298/// One request, the decision taken on it, and what became of it.
299///
300/// `executed_at` and `outcome` are both optional and are absent together: a
301/// record is written when the decision is taken, which is before — and, for a
302/// rejected request, instead of — anything reaching the printer.
303#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
304#[serde(deny_unknown_fields)]
305pub struct ActionRecord {
306 /// This record's identifier, minted by the store.
307 pub id: ActionId,
308 /// The print this request was made against.
309 pub print_id: PrintId,
310 /// The request itself.
311 pub request: ActionRequest,
312 /// The decision policy took on it.
313 pub decision: PolicyDecision,
314 /// When it reached the printer, if it did.
315 #[serde(default, skip_serializing_if = "Option::is_none")]
316 pub executed_at: Option<Timestamp>,
317 /// What the printer made of it, if it reached the printer.
318 #[serde(default, skip_serializing_if = "Option::is_none")]
319 pub outcome: Option<ExecutionOutcome>,
320}