ichen_openprotocol/messages.rs
1use super::filters::Filters;
2use super::utils::*;
3use super::{
4 ActionID, Controller, Error, JobCard, JobMode, KeyValuePair, Language, OpMode, Result,
5 StateValues, TextID, TextName, ID, R32,
6};
7use chrono::{DateTime, FixedOffset};
8use indexmap::IndexMap;
9use serde::{Deserialize, Serialize};
10use std::borrow::Cow;
11use std::convert::TryInto;
12use std::sync::atomic::{AtomicU64, Ordering};
13use Message::*;
14
15// Auto-incrementing global counter for message sequence numbers.
16static SEQ: AtomicU64 = AtomicU64::new(1);
17
18/// Common options of an Open Protocol message.
19///
20#[derive(Debug, Hash, Clone, Serialize, Deserialize)]
21#[serde(rename_all = "camelCase")]
22pub struct MessageOptions<'a> {
23 /// Unique ID (if any) of the message for tracking and storage retrieval purposes.
24 ///
25 /// The iChen Server may tag certain messages with a unique tracking key that can be used to
26 /// retrieve the message from persistent storage later.
27 #[serde(skip_serializing_if = "Option::is_none")]
28 #[serde(borrow)]
29 id: Option<TextID<'a>>,
30 //
31 /// Ever-increasing message sequence number.
32 ///
33 /// This number is usually auto-incremented with each message created, starting from 1.
34 sequence: u64,
35 //
36 /// Priority of the message, smaller number is higher priority. Default = 0.
37 #[serde(skip_serializing_if = "is_zero")]
38 #[serde(default)]
39 priority: i32,
40}
41
42impl<'a> MessageOptions<'a> {
43 /// Get the message ID, if any.
44 ///
45 /// # Examples
46 ///
47 /// ~~~
48 /// # use ichen_openprotocol::*;
49 /// # fn main() -> std::result::Result<(), String> {
50 /// let mut opt = MessageOptions::new();
51 /// opt.set_id("hello")?;
52 /// assert_eq!(Some("hello"), opt.id());
53 /// opt.clear_id();
54 /// assert_eq!(None, opt.id());
55 /// # Ok(())
56 /// # }
57 /// ~~~
58 pub fn id(&self) -> Option<&str> {
59 self.id.as_ref().map(|x| x.get())
60 }
61
62 // Get the message sequence number.
63 ///
64 /// # Examples
65 ///
66 /// ~~~
67 /// # use ichen_openprotocol::*;
68 /// let opt1 = MessageOptions::new();
69 /// assert_eq!(1, opt1.sequence());
70 ///
71 /// let opt2 = MessageOptions::new();
72 /// assert_eq!(2, opt2.sequence()); // `sequence` auto-increments.
73 /// ~~~
74 pub fn sequence(&self) -> u64 {
75 self.sequence
76 }
77
78 /// Get the message priority.
79 ///
80 /// # Examples
81 ///
82 /// ~~~
83 /// # use ichen_openprotocol::*;
84 /// let opt1 = MessageOptions::new_with_priority(100);
85 /// assert_eq!(100, opt1.priority());
86 ///
87 /// let opt2 = MessageOptions::new_with_priority(-42);
88 /// assert_eq!(-42, opt2.priority());
89 /// ~~~
90 pub fn priority(&self) -> i32 {
91 self.priority
92 }
93
94 /// Set the message ID.
95 ///
96 /// # Errors
97 ///
98 /// Returns `Err(String)` if the ID string is empty or all-whitespace.
99 ///
100 /// ## Error Examples
101 ///
102 /// ~~~
103 /// # use ichen_openprotocol::*;
104 /// let mut opt = MessageOptions::new();
105 /// assert_eq!(Err("invalid value: a non-empty, non-whitespace, all-ASCII string required".into()), opt.set_id(""));
106 /// ~~~
107 ///
108 /// # Examples
109 ///
110 /// ~~~
111 /// # use ichen_openprotocol::*;
112 /// # fn main() -> std::result::Result<(), String> {
113 /// let mut opt = MessageOptions::new();
114 /// opt.set_id("hello")?;
115 /// assert_eq!(Some("hello"), opt.id());
116 /// opt.clear_id();
117 /// assert_eq!(None, opt.id());
118 /// # Ok(())
119 /// # }
120 /// ~~~
121 pub fn set_id(&mut self, id: &'a str) -> std::result::Result<(), String> {
122 self.id = Some(id.try_into()?);
123 Ok(())
124 }
125
126 /// Set the message ID to `None`.
127 ///
128 /// # Examples
129 ///
130 /// ~~~
131 /// # use ichen_openprotocol::*;
132 /// # fn main() -> std::result::Result<(), String> {
133 /// let mut opt = MessageOptions::new();
134 /// opt.set_id("hello")?;
135 /// assert_eq!(Some("hello"), opt.id());
136 /// opt.clear_id();
137 /// assert_eq!(None, opt.id());
138 /// # Ok(())
139 /// # }
140 /// ~~~
141 pub fn clear_id(&mut self) {
142 self.id = None;
143 }
144
145 /// Create a `MessageOptions` with default values (for example, the `sequence` field
146 /// auto-increments).
147 ///
148 /// # Examples
149 ///
150 /// ~~~
151 /// # use ichen_openprotocol::*;
152 /// let opt1 = MessageOptions::new();
153 /// assert_eq!(1, opt1.sequence());
154 /// assert_eq!(0, opt1.priority());
155 ///
156 /// let opt2 = MessageOptions::new();
157 /// assert_eq!(2, opt2.sequence()); // `sequence` auto-increments.
158 /// assert_eq!(0, opt2.priority());
159 /// ~~~
160 pub fn new() -> Self {
161 Default::default()
162 }
163
164 /// Create a `MessageOptions` with a particular `priority` but otherwise
165 /// default values (for example, the `sequence` field auto-increments).
166 ///
167 /// # Examples
168 ///
169 /// ~~~
170 /// # use ichen_openprotocol::*;
171 /// let opt1 = MessageOptions::new_with_priority(100);
172 /// assert_eq!(1, opt1.sequence());
173 /// assert_eq!(100, opt1.priority());
174 ///
175 /// let opt2 = MessageOptions::new_with_priority(-42);
176 /// assert_eq!(2, opt2.sequence()); // `sequence` auto-increments.
177 /// assert_eq!(-42, opt2.priority());
178 /// ~~~
179 pub fn new_with_priority(priority: i32) -> Self {
180 Self { priority, ..Self::new() }
181 }
182}
183
184impl Default for MessageOptions<'_> {
185 /// Default value for `MessageOptions`.
186 ///
187 /// The `sequence` field is auto-incrementing.
188 ///
189 /// # Examples
190 ///
191 /// ~~~
192 /// # use ichen_openprotocol::*;
193 /// let opt1: MessageOptions = Default::default();
194 /// assert_eq!(1, opt1.sequence());
195 /// assert_eq!(0, opt1.priority());
196 ///
197 /// let opt2: MessageOptions = Default::default();
198 /// assert_eq!(2, opt2.sequence()); // `sequence` auto-increments.
199 /// assert_eq!(0, opt2.priority());
200 /// ~~~
201 fn default() -> Self {
202 Self { id: None, sequence: SEQ.fetch_add(1, Ordering::SeqCst), priority: 0 }
203 }
204}
205
206/// All Open Protocol message types.
207///
208/// See [this document] for details.
209///
210/// [this document]: https://github.com/chenhsong/OpenProtocol/blob/master/cs/doc/messages_reference.md
211///
212#[derive(Debug, Clone, Serialize, Deserialize)]
213#[serde(tag = "$type")]
214pub enum Message<'a> {
215 /// The `ALIVE` message, sent periodically as the keep-alive mechanism.
216 #[serde(rename_all = "camelCase")]
217 Alive {
218 /// Message configuration options.
219 #[serde(flatten)]
220 options: MessageOptions<'a>,
221 },
222 //
223 /// The `CNTRLER_ACTION` message, sent by the server whenever the current *action* of a controller changes.
224 #[serde(rename_all = "camelCase")]
225 ControllerAction {
226 /// Unique ID of the controller.
227 controller_id: ID,
228 /// Unique action code.
229 ///
230 /// See [this document] for details.
231 ///
232 /// [this document]: https://github.com/chenhsong/OpenProtocol/blob/master/doc/actions.md
233 action_id: ActionID,
234 //
235 /// Time-stamp of the event.
236 timestamp: DateTime<FixedOffset>,
237 //
238 /// Message configuration options.
239 #[serde(flatten)]
240 options: MessageOptions<'a>,
241 },
242 //
243 /// The `REQ_CNTRLER_LIST` message, sent to the server to request a list of controllers (i.e. machines)
244 /// within the user's organization.
245 ///
246 /// # Response
247 ///
248 /// The Server should reply with a [`ControllersList`] message.
249 ///
250 /// [`ControllersList`]: enum.Message.html#variant.ControllersList
251 ///
252 #[serde(rename_all = "camelCase")]
253 RequestControllersList {
254 /// Unique ID of the controller to request.
255 ///
256 /// If omitted, all controllers of the user's organization will be returned.
257 #[serde(skip_serializing_if = "Option::is_none")]
258 controller_id: Option<ID>,
259 //
260 /// Message configuration options.
261 #[serde(flatten)]
262 options: MessageOptions<'a>,
263 },
264 //
265 /// The `RESP_CNTRLER_LIST` message, sent by the server in response to a
266 /// [`RequestControllersList`] message.
267 ///
268 /// [`RequestControllersList`]: enum.Message.html#variant.RequestControllersList
269 ///
270 #[serde(rename_all = "camelCase")]
271 ControllersList {
272 /// List of controllers requested by a previous `RequestControllersList` message.
273 ///
274 /// Each controller data structure contains the last-known values of the controller's state.
275 //
276 // Custom deserialization of string into integer key.
277 // No need for custom serialization because ID to string is fine.
278 #[serde(deserialize_with = "deserialize_indexmap")]
279 data: IndexMap<ID, Controller<'a>>,
280 //
281 /// Message configuration options.
282 #[serde(flatten)]
283 options: MessageOptions<'a>,
284 },
285 //
286 /// The `UPD_CNTRLER` message, sent by the server whenever the status of a connected controller changes.
287 ///
288 /// Only the changed fields will be set, with other fields/properties being set to
289 /// `None` as they are not relevant.
290 #[serde(rename_all = "camelCase")]
291 ControllerStatus {
292 /// Unique ID of the controller.
293 controller_id: ID,
294 //
295 /// Human-friendly name for display (or `None` if not relevant).
296 #[serde(skip_serializing_if = "Option::is_none")]
297 display_name: Option<Box<TextName<'a>>>,
298 //
299 /// If true, the controller has disconnected from the iChenĀ® Server.
300 #[serde(skip_serializing_if = "Option::is_none")]
301 is_disconnected: Option<bool>,
302 //
303 /// Current operation mode of the controller (or `None` if not relevant).
304 #[serde(skip_serializing_if = "Option::is_none")]
305 op_mode: Option<OpMode>,
306 //
307 /// Current job mode of the controller (or `None` if not relevant).
308 #[serde(skip_serializing_if = "Option::is_none")]
309 job_mode: Option<JobMode>,
310 //
311 /// State of an alarm (if any) on the controller (or `None` if not relevant).
312 ///
313 /// See [this document] for valid alarm codes.
314 ///
315 /// [this document]: https://github.com/chenhsong/OpenProtocol/blob/master/doc/alarms.md
316 #[serde(skip_serializing_if = "Option::is_none")]
317 alarm: Option<Box<KeyValuePair<TextID<'a>, bool>>>,
318 //
319 /// Change of a setting (if any) on the controller for audit trail purpose
320 /// (or `None` if not relevant).
321 #[serde(skip_serializing_if = "Option::is_none")]
322 audit: Option<Box<KeyValuePair<TextID<'a>, R32>>>,
323 //
324 /// Change of a variable (if any) on the controller (or `None` if not relevant).
325 #[serde(skip_serializing_if = "Option::is_none")]
326 variable: Option<Box<KeyValuePair<TextID<'a>, R32>>>,
327 //
328 /// Unique ID of the current logged-on user, `Some(None)` if a user has logged out
329 /// (or `None` if not relevant).
330 #[serde(serialize_with = "serialize_some_none_to_invalid")]
331 #[serde(deserialize_with = "deserialize_invalid_to_some_none")]
332 #[serde(skip_serializing_if = "Option::is_none")]
333 #[serde(default)]
334 operator_id: Option<Option<ID>>,
335 //
336 /// Name of the current logged-on user, `Some(None)` if the current user has no name
337 /// (or `None` if not relevant).
338 #[serde(deserialize_with = "deserialize_null_to_some_none")]
339 #[serde(skip_serializing_if = "Option::is_none")]
340 #[serde(default)]
341 operator_name: Option<Option<Box<TextName<'a>>>>,
342 //
343 /// Unique ID of the current job card loaded, `Some(None)` if no job card is currently loaded
344 /// (or `None` if not relevant).
345 #[serde(deserialize_with = "deserialize_null_to_some_none")]
346 #[serde(skip_serializing_if = "Option::is_none")]
347 #[serde(default)]
348 #[serde(borrow)]
349 job_card_id: Option<Option<Box<TextName<'a>>>>,
350 //
351 /// Unique ID of the current mold data set loaded, `Some(None)` if no mold data set is currently loaded
352 /// (or `None` if not relevant).
353 #[serde(deserialize_with = "deserialize_null_to_some_none")]
354 #[serde(skip_serializing_if = "Option::is_none")]
355 #[serde(default)]
356 #[serde(borrow)]
357 mold_id: Option<Option<Box<TextName<'a>>>>,
358 //
359 /// Snapshot of the current known states of the controller.
360 state: StateValues<'a>,
361 //
362 /// A [`Controller`] data structure containing the last-known state of the controller.
363 ///
364 /// This field is only sent once by the server as soon as a new controller has connected
365 /// to the network.
366 /// All subsequent messages have this field set to `None`.
367 ///
368 /// If this field is not `None`, then all other info fields should be `None` or have values
369 /// equal to the matching fields in `controller`.
370 ///
371 /// [`Controller`]: struct.Controller.html
372 #[serde(skip_serializing_if = "Option::is_none")]
373 controller: Option<Box<Controller<'a>>>,
374 //
375 /// Message configuration options.
376 #[serde(flatten)]
377 options: MessageOptions<'a>,
378 },
379 //
380 /// The `CYCLE_DATA` message, sent by the server at the end of each machine cycle.
381 #[serde(rename_all = "camelCase")]
382 CycleData {
383 /// Unique ID of the controller.
384 controller_id: ID,
385 //
386 /// A data dictionary containing a set of cycle data.
387 ///
388 /// See [this document] for examples.
389 ///
390 /// [this document]: https://github.com/chenhsong/OpenProtocol/blob/master/doc/cycledata.md
391 data: IndexMap<TextID<'a>, R32>,
392 //
393 /// Time-stamp of the event.
394 timestamp: DateTime<FixedOffset>,
395 //
396 /// Snapshot of the current known states of the controller.
397 #[serde(flatten)]
398 state: StateValues<'a>,
399 //
400 /// Message configuration options.
401 #[serde(flatten)]
402 options: MessageOptions<'a>,
403 },
404 //
405 /// The `REQ_JOBCARDS_LIST` message, sent by the server when a connected controller
406 /// requests a list of job cards.
407 ///
408 /// # Action Required
409 ///
410 /// The user should send a [`JobCardsList`] message to the Server as a reply.
411 ///
412 /// [`JobCardsList`]: enum.Message.html#variant.JobCardsList
413 #[serde(rename_all = "camelCase")]
414 RequestJobCardsList {
415 /// Unique ID of the controller.
416 controller_id: ID,
417 //
418 /// Message configuration options.
419 #[serde(flatten)]
420 options: MessageOptions<'a>,
421 },
422 //
423 /// The `RESP_JOBSLIST` message, sent to the server in response to a [`RequestJobCardsList`] message.
424 ///
425 /// [`RequestJobCardsList`]: enum.Message.html#variant.RequestJobCardsList
426 #[serde(rename_all = "camelCase")]
427 JobCardsList {
428 /// Unique ID of the controller.
429 controller_id: ID,
430 //
431 /// A data dictionary containing a set of `JobCard` data structures.
432 data: IndexMap<TextName<'a>, JobCard<'a>>,
433 //
434 /// Message configuration options.
435 #[serde(flatten)]
436 options: MessageOptions<'a>,
437 },
438 //
439 /// The `JOIN` message, sent to log onto the server.
440 ///
441 /// # Response
442 ///
443 /// The Server should reply with a [`JoinResponse`] message.
444 ///
445 /// [`JoinResponse`]: enum.Message.html#variant.JoinResponse
446 #[serde(rename_all = "camelCase")]
447 Join {
448 /// Organization ID (if any).
449 #[serde(skip_serializing_if = "Option::is_none")]
450 org_id: Option<TextID<'a>>,
451 //
452 /// The maximum protocol version supported, in the format `x.x.x.x`.
453 ///
454 /// The current protocol version implemented is in the constant `PROTOCOL_VERSION`.
455 version: TextID<'a>,
456 //
457 /// Password to log onto the server.
458 password: &'a str,
459 //
460 /// Language encoding.
461 language: Language,
462 //
463 /// A collection of [`Filter`] values containing what type(s) of messages to receive.
464 ///
465 /// [`Filter`]: struct.Filters.html
466 filter: Filters,
467 //
468 /// Message configuration options.
469 #[serde(flatten)]
470 options: MessageOptions<'a>,
471 },
472 //
473 /// The `RESP_JOIN` message, sent by the Server in response to a [`Join`] message.
474 ///
475 /// [`Join`]: enum.Message.html#variant.Join
476 #[serde(rename_all = "camelCase")]
477 JoinResponse {
478 /// Result code, >= 100 indicates success.
479 result: u32,
480 #[serde(skip_serializing_if = "Option::is_none")]
481 //
482 /// The allowed access level for this client.
483 level: Option<u32>,
484 //
485 /// A message (mostly likely an error message in case of failure), if any.
486 #[serde(skip_serializing_if = "Option::is_none")]
487 #[serde(borrow)]
488 message: Option<Box<Cow<'a, str>>>,
489 //
490 /// Message configuration options.
491 #[serde(flatten)]
492 options: MessageOptions<'a>,
493 },
494 //
495 /// The `REQ_MOLD` message, sent to the server to request the set of mold settings data of a controller.
496 ///
497 /// # Response
498 ///
499 /// The Server should reply with a [`MoldData`] message.
500 ///
501 /// [`MoldData`]: enum.Message.html#variant.MoldData
502 #[serde(rename_all = "camelCase")]
503 RequestMoldData {
504 /// Unique ID of the controller.
505 controller_id: ID,
506 //
507 /// Message configuration options.
508 #[serde(flatten)]
509 options: MessageOptions<'a>,
510 },
511 //
512 /// The `RESP_MOLD` message, sent by the server in response to a [`RequestMoldData`] message
513 /// or a [`ReadMoldData`] message with `field` set to `None` (meaning read all).
514 ///
515 /// [`RequestMoldData`]: enum.Message.html#variant.RequestMoldData
516 /// [`ReadMoldData`]: enum.Message.html#variant.ReadMoldData
517 #[serde(rename_all = "camelCase")]
518 MoldData {
519 /// Unique ID of the controller.
520 controller_id: ID,
521 //
522 /// A data dictionary containing a set of mold settings.
523 data: IndexMap<TextID<'a>, R32>,
524 //
525 /// Time-stamp of the event.
526 timestamp: DateTime<FixedOffset>,
527 //
528 /// Snapshot of the current known states of the controller.
529 #[serde(flatten)]
530 state: StateValues<'a>,
531 //
532 /// Message configuration options.
533 #[serde(flatten)]
534 options: MessageOptions<'a>,
535 },
536 //
537 /// The `READ_MOLD_DATA` message, sent to the server to read the current value of a
538 /// particular mold setting.
539 ///
540 /// The server keeps a cache of the states of all mold settings for each controller.
541 /// The value returned is based on the server cache.
542 /// No command is sent to controller to poll the latest value.
543 ///
544 /// # Response
545 ///
546 /// The Server should reply with a [`MoldData`] message if `field` is `None`,
547 /// or a [`MoldDataValue`] message.
548 ///
549 /// [`MoldData`]: enum.Message.html#variant.MoldData
550 /// [`MoldDataValue`]: enum.Message.html#variant.MoldDataValue
551 #[serde(rename_all = "camelCase")]
552 ReadMoldData {
553 /// Unique ID of the controller.
554 controller_id: ID,
555 //
556 /// Name of the mold setting to read, `None` for all.
557 field: Option<TextID<'a>>,
558 //
559 /// Message configuration options.
560 #[serde(flatten)]
561 options: MessageOptions<'a>,
562 },
563 //
564 /// The `RESP_MOLD_DATA_VALUE` message, sent by the server in response to a
565 /// [`ReadMoldData`] message.
566 ///
567 /// [`ReadMoldData`]: enum.Message.html#variant.ReadMoldData
568 #[serde(rename_all = "camelCase")]
569 MoldDataValue {
570 /// Unique ID of the controller.
571 controller_id: ID,
572 //
573 /// Name of the mold setting to read.
574 field: TextID<'a>,
575 //
576 /// Current cached value of the mold setting.
577 value: R32,
578 //
579 /// Message configuration options.
580 #[serde(flatten)]
581 options: MessageOptions<'a>,
582 },
583 //
584 /// The `REQ_PWD_LEVEL` message, sent by server when a connected controller attempts to
585 /// authenticate and authorize a user password.
586 ///
587 /// # Action Required
588 ///
589 /// The user should send an [`OperatorInfo`] message to the Server as a reply.
590 ///
591 /// [`OperatorInfo`]: enum.Message.html#variant.OperatorInfo
592 #[serde(rename_all = "camelCase")]
593 LoginOperator {
594 /// Unique ID of the controller.
595 controller_id: ID,
596 //
597 /// User password.
598 password: &'a str,
599 //
600 /// Message configuration options.
601 #[serde(flatten)]
602 options: MessageOptions<'a>,
603 },
604 //
605 /// The `RESP_PWD_LEVEL` message, sent to the server in response to a
606 /// [`LoginOperator`] message.
607 ///
608 /// [`LoginOperator`]: enum.Message.html#variant.LoginOperator
609 #[serde(rename_all = "camelCase")]
610 OperatorInfo {
611 /// Unique ID of the controller.
612 controller_id: ID,
613 //
614 /// Unique ID of the authenticated user.
615 #[serde(skip_serializing_if = "Option::is_none")]
616 operator_id: Option<ID>,
617 //
618 /// Name of the user.
619 name: TextName<'a>,
620 //
621 /// User password.
622 password: TextName<'a>,
623 //
624 /// Allowed access level for the user.
625 ///
626 /// Valid values are from 0 to [`MAX_OPERATOR_LEVEL`] (usually 10).
627 ///
628 /// [`MAX_OPERATOR_LEVEL`]: enum.Message.html#associatedconstant.MAX_OPERATOR_LEVEL
629 level: u8,
630 //
631 /// Message configuration options.
632 #[serde(flatten)]
633 options: MessageOptions<'a>,
634 },
635}
636
637impl<'a> Message<'a> {
638 /// Current protocol version: 4.0.
639 pub const PROTOCOL_VERSION: &'static str = "4.0";
640
641 /// Default language to use: `EN` (English).
642 pub const DEFAULT_LANGUAGE: Language = Language::EN;
643
644 /// Maximum operator level: 10.
645 pub const MAX_OPERATOR_LEVEL: u8 = 10;
646
647 /// Parse a JSON string into a `Message`.
648 ///
649 /// # Errors
650 ///
651 /// Return `Err(`[`OpenProtocolError`]`)` if there is an error during parsing.
652 ///
653 /// [`OpenProtocolError`]: enum.OpenProtocolError.html
654 ///
655 pub fn parse_from_json_str(json: &'a str) -> Result<'a, Self> {
656 let m = serde_json::from_str::<Message>(json).map_err(Error::JsonError)?;
657 m.validate()?;
658 Ok(m)
659 }
660
661 /// Validate all the fields in the `Message`, then serialize it into a JSON string.
662 ///
663 /// # Errors
664 ///
665 /// Return `Err(`[`OpenProtocolError`]`)` if there is an error.
666 ///
667 /// [`OpenProtocolError`]: enum.OpenProtocolError.html
668 ///
669 /// # Examples
670 ///
671 /// ~~~
672 /// # use ichen_openprotocol::*;
673 /// # fn main() -> std::result::Result<(), String> {
674 /// let msg = Message::try_new_join_with_org("MyPassword", Filters::Status + Filters::Cycle, "MyCompany")?;
675 /// assert_eq!(
676 /// r#"{"$type":"Join","orgId":"MyCompany","version":"4.0","password":"MyPassword","language":"EN","filter":"Status, Cycle","sequence":1}"#,
677 /// msg.to_json_str()?
678 /// );
679 /// # Ok(())
680 /// # }
681 /// ~~~
682 pub fn to_json_str(&self) -> Result<'_, String> {
683 self.validate()?;
684 serde_json::to_string(self).map_err(Error::JsonError)
685 }
686
687 /// Create an `ALIVE` message.
688 ///
689 /// # Examples
690 ///
691 /// ~~~
692 /// # use ichen_openprotocol::*;
693 /// let msg = Message::new_alive();
694 /// if let Message::Alive { options } = msg {
695 /// assert_eq!(1, options.sequence());
696 /// assert_eq!(0, options.priority());
697 /// assert_eq!(None, options.id());
698 /// } else {
699 /// panic!();
700 /// }
701 /// ~~~
702 pub fn new_alive() -> Self {
703 Alive { options: Default::default() }
704 }
705
706 /// Create a `JOIN` message with default language and protocol version.
707 ///
708 /// The default language is [`DEFAULT_LANGUAGE`] (usually `EN`).
709 ///
710 /// The default protocol version is given in [`PROTOCOL_VERSION`].
711 ///
712 /// [`DEFAULT_LANGUAGE`]: enum.Message.html#associatedconstant.DEFAULT_LANGUAGE
713 /// [`PROTOCOL_VERSION`]: enum.Message.html#associatedconstant.PROTOCOL_VERSION
714 ///
715 /// # Examples
716 ///
717 /// ~~~
718 /// # use ichen_openprotocol::*;
719 /// let msg = Message::new_join("MyPassword", Filters::Status + Filters::Cycle);
720 /// if let Message::Join { org_id, version, password, language, filter, options } = msg {
721 /// assert_eq!(None, org_id);
722 /// assert_eq!(Message::PROTOCOL_VERSION, &version);
723 /// assert_eq!("MyPassword", password);
724 /// assert_eq!(Message::DEFAULT_LANGUAGE, language);
725 /// assert_eq!(Filters::Status + Filters::Cycle, filter);
726 /// assert_eq!(1, options.sequence());
727 /// assert_eq!(0, options.priority());
728 /// assert_eq!(None, options.id());
729 /// } else {
730 /// panic!();
731 /// }
732 /// ~~~
733 pub fn new_join(password: &'a str, filter: Filters) -> Self {
734 Join {
735 org_id: None,
736 version: Self::PROTOCOL_VERSION.try_into().unwrap(),
737 password,
738 language: Self::DEFAULT_LANGUAGE,
739 filter,
740 options: Default::default(),
741 }
742 }
743
744 /// Create a `JOIN` message with non-default organization.
745 ///
746 /// # Errors
747 ///
748 /// Returns `Err(String)` if the organization ID is empty or all-whitespace or contains
749 /// any non-ASCII characters.
750 ///
751 /// ## Error Examples
752 ///
753 /// ~~~
754 /// # use ichen_openprotocol::*;
755 /// match Message::try_new_join_with_org("MyPassword", Filters::Status + Filters::Cycle, "") {
756 /// Err(e) => assert_eq!("invalid value: a non-empty, non-whitespace, all-ASCII string required", e),
757 /// _ => ()
758 /// }
759 /// ~~~
760 ///
761 /// # Examples
762 ///
763 /// ~~~
764 /// # use ichen_openprotocol::*;
765 /// # fn main() -> std::result::Result<(), String> {
766 /// let msg = Message::try_new_join_with_org("MyPassword", Filters::Status + Filters::Cycle, "MyCompany")?;
767 ///
768 /// if let Message::Join { org_id, version, password, language, filter, options } = msg {
769 /// assert_eq!(Some("MyCompany"), org_id.as_ref().map(|x| x.get()));
770 /// assert_eq!(Message::PROTOCOL_VERSION, version.get());
771 /// assert_eq!("MyPassword", password);
772 /// assert_eq!(Message::DEFAULT_LANGUAGE, language);
773 /// assert_eq!(Filters::Status + Filters::Cycle, filter);
774 /// assert_eq!(1, options.sequence());
775 /// assert_eq!(0, options.priority());
776 /// assert_eq!(None, options.id());
777 /// } else {
778 /// panic!();
779 /// }
780 /// # Ok(())
781 /// # }
782 /// ~~~
783 pub fn try_new_join_with_org(
784 password: &'a str,
785 filter: Filters,
786 org: &'a str,
787 ) -> std::result::Result<Self, String> {
788 let mut msg = Self::new_join(password, filter);
789
790 if let Join { ref mut org_id, .. } = msg {
791 *org_id = Some(org.try_into()?);
792 }
793
794 Ok(msg)
795 }
796
797 /// Get the optional message ID from the `options` field.
798 pub fn id(&self) -> Option<&str> {
799 match self {
800 Alive { options }
801 | ControllerAction { options, .. }
802 | RequestControllersList { options, .. }
803 | ControllersList { options, .. }
804 | ControllerStatus { options, .. }
805 | CycleData { options, .. }
806 | RequestJobCardsList { options, .. }
807 | JobCardsList { options, .. }
808 | Join { options, .. }
809 | JoinResponse { options, .. }
810 | RequestMoldData { options, .. }
811 | MoldData { options, .. }
812 | ReadMoldData { options, .. }
813 | MoldDataValue { options, .. }
814 | LoginOperator { options, .. }
815 | OperatorInfo { options, .. } => options.id(),
816 }
817 }
818
819 /// Get the message sequence number from the `options` field.
820 pub fn sequence(&self) -> u64 {
821 match self {
822 Alive { options }
823 | ControllerAction { options, .. }
824 | RequestControllersList { options, .. }
825 | ControllersList { options, .. }
826 | ControllerStatus { options, .. }
827 | CycleData { options, .. }
828 | RequestJobCardsList { options, .. }
829 | JobCardsList { options, .. }
830 | Join { options, .. }
831 | JoinResponse { options, .. }
832 | RequestMoldData { options, .. }
833 | MoldData { options, .. }
834 | ReadMoldData { options, .. }
835 | MoldDataValue { options, .. }
836 | LoginOperator { options, .. }
837 | OperatorInfo { options, .. } => options.sequence(),
838 }
839 }
840
841 /// Get the message priority from the `options` field.
842 pub fn priority(&self) -> i32 {
843 match self {
844 Alive { options, .. }
845 | ControllerAction { options, .. }
846 | RequestControllersList { options, .. }
847 | ControllersList { options, .. }
848 | ControllerStatus { options, .. }
849 | CycleData { options, .. }
850 | RequestJobCardsList { options, .. }
851 | JobCardsList { options, .. }
852 | Join { options, .. }
853 | JoinResponse { options, .. }
854 | RequestMoldData { options, .. }
855 | MoldData { options, .. }
856 | ReadMoldData { options, .. }
857 | MoldDataValue { options, .. }
858 | LoginOperator { options, .. }
859 | OperatorInfo { options, .. } => options.priority(),
860 }
861 }
862
863 /// Validate the `Message` data structure.
864 ///
865 /// # Errors
866 ///
867 /// Returns `Err(`[`OpenProtocolError`]`)` if some fields in the `Message` are not valid.
868 ///
869 /// [`OpenProtocolError`]: enum.OpenProtocolError.html
870 ///
871 /// # Examples
872 ///
873 /// ~~~
874 /// # use ichen_openprotocol::*;
875 /// let msg = Message::ControllerStatus {
876 /// controller_id: ID::from_u32(12345),
877 /// display_name: None,
878 /// is_disconnected: None,
879 /// op_mode: None,
880 /// job_mode: None,
881 /// job_card_id: Some(None),
882 /// mold_id: Some(Some(Box::new(TextName::new_from_str("Test-123").unwrap()))), // Value is "Test-123"
883 /// operator_id: None,
884 /// operator_name: None,
885 /// variable: None,
886 /// audit: None,
887 /// alarm: None,
888 /// controller: None,
889 /// state: StateValues::try_new_with_all(
890 /// OpMode::Automatic,
891 /// JobMode::ID02,
892 /// None,
893 /// None,
894 /// Some("Test-FooBar"), // Notice that this state value should be "Test-123"
895 /// ).unwrap(),
896 /// options: Default::default(),
897 /// };
898 ///
899 /// // Validation should error because `state.mold_id` is not the same as the `mold_id` field.
900 /// assert_eq!(
901 /// Err(Error::InconsistentState("mold_id")),
902 /// msg.validate()
903 /// );
904 /// ~~~
905 pub fn validate(&self) -> Result<'a, ()> {
906 match self {
907 Alive { .. }
908 | ControllerAction { .. }
909 | RequestControllersList { .. }
910 | RequestJobCardsList { .. }
911 | JoinResponse { .. }
912 | RequestMoldData { .. }
913 | ControllersList { .. }
914 | CycleData { .. }
915 | ReadMoldData { .. }
916 | MoldDataValue { .. }
917 | LoginOperator { .. }
918 | JobCardsList { .. }
919 | MoldData { .. } => (),
920
921 ControllerStatus {
922 display_name,
923 is_disconnected,
924 op_mode,
925 job_mode,
926 alarm,
927 audit,
928 variable,
929 operator_id,
930 operator_name,
931 job_card_id,
932 mold_id,
933 state,
934 controller,
935 ..
936 } => {
937 if let Some(c) = controller {
938 // If controller is present, some fields must be None
939 if !is_disconnected.is_none()
940 || !alarm.is_none()
941 || !audit.is_none()
942 || !variable.is_none()
943 {
944 return Err(Error::ConstraintViolated(
945 "All other fields must be set to None if controller is present.".into(),
946 ));
947 }
948
949 // Check controller fields with specified fields
950 if display_name.is_some()
951 && display_name.as_ref().unwrap().get() != &c.display_name
952 {
953 return Err(Error::InconsistentField("display_name"));
954 }
955 if operator_name.is_some()
956 && operator_name.as_ref().unwrap().as_ref().map(|x| x.get())
957 != c.operator.as_ref().map(|u| u.name()).flatten()
958 {
959 return Err(Error::InconsistentField("operator_name"));
960 }
961
962 // Check controller fields with the state
963 if state.op_mode() != c.op_mode {
964 return Err(Error::InconsistentState("op_mode"));
965 }
966 if state.job_mode() != c.job_mode {
967 return Err(Error::InconsistentState("job_mode"));
968 }
969 if state.operator_id() != c.operator.as_ref().map(|user| user.id()) {
970 return Err(Error::InconsistentState("operator_id"));
971 }
972 if state.job_card_id() != c.job_card_id.as_ref().map(|x| x.as_ref().as_ref()) {
973 return Err(Error::InconsistentState("job_card_id"));
974 }
975 if state.mold_id() != c.mold_id.as_ref().map(|x| x.as_ref().as_ref()) {
976 return Err(Error::InconsistentField("mold_id"));
977 }
978 }
979
980 // Check controller fields with the state
981 if op_mode.is_some() && op_mode.unwrap() != state.op_mode() {
982 return Err(Error::InconsistentState("op_mode"));
983 }
984 if job_mode.is_some() && job_mode.unwrap() != state.job_mode() {
985 return Err(Error::InconsistentState("job_mode"));
986 }
987 if operator_id.is_some() && operator_id.unwrap() != state.operator_id() {
988 return Err(Error::InconsistentState("operator_id"));
989 }
990 if job_card_id.is_some()
991 && state.job_card_id()
992 != job_card_id.as_ref().unwrap().as_ref().map(|jc| jc.get())
993 {
994 return Err(Error::InconsistentState("job_card_id"));
995 }
996 if mold_id.is_some()
997 && state.mold_id() != mold_id.as_ref().unwrap().as_ref().map(|m| m.get())
998 {
999 return Err(Error::InconsistentState("mold_id"));
1000 }
1001 }
1002
1003 Join { language, .. } => {
1004 // Check for invalid language
1005 if *language == Language::Unknown {
1006 return Err(Error::InvalidField {
1007 field: "language",
1008 value: "Unknown".into(),
1009 description: "language cannot be Unknown".into(),
1010 });
1011 }
1012 }
1013
1014 OperatorInfo { level, .. } => {
1015 if *level > Self::MAX_OPERATOR_LEVEL {
1016 return Err(Error::ConstraintViolated(
1017 format!(
1018 "Level {} is too high - must be between 0 and {}.",
1019 level,
1020 Self::MAX_OPERATOR_LEVEL
1021 )
1022 .into(),
1023 ));
1024 }
1025 }
1026 }
1027
1028 Ok(())
1029 }
1030}
1031
1032// Tests
1033
1034#[cfg(test)]
1035mod test {
1036 use super::*;
1037 use std::result::Result;
1038
1039 impl<'a> MessageOptions<'a> {
1040 /// A private constructor function that creates a `MessageOptions` structure
1041 /// with `sequence` always set to 1 (for testing purposes).
1042 fn default_new() -> Self {
1043 Self { sequence: 1, ..Self::new() }
1044 }
1045 }
1046
1047 #[test]
1048 fn test_message_alive_to_json() -> Result<(), String> {
1049 let mut options = MessageOptions::new_with_priority(20);
1050 options.sequence = 999;
1051 options.set_id("hello")?;
1052
1053 let msg = Alive { options };
1054
1055 let serialized = serde_json::to_string(&msg).map_err(|x| x.to_string())?;
1056
1057 assert_eq!(r#"{"$type":"Alive","id":"hello","sequence":999,"priority":20}"#, serialized);
1058
1059 Ok(())
1060 }
1061
1062 #[test]
1063 fn test_message_mold_data_to_json() -> Result<(), String> {
1064 let mut map: IndexMap<TextID, R32> = IndexMap::new();
1065
1066 map.insert("Hello".try_into().unwrap(), R32::new(123.0));
1067 map.insert("World".try_into().unwrap(), R32::new(-987.6543));
1068 map.insert("foo".try_into().unwrap(), R32::new(0.0));
1069
1070 let mut options = MessageOptions::new_with_priority(-20);
1071 options.sequence = 999;
1072
1073 let msg = MoldData {
1074 controller_id: ID::from_u32(123),
1075 data: map,
1076
1077 timestamp: DateTime::parse_from_rfc3339("2019-02-26T02:03:04+08:00")
1078 .map_err(|x| x.to_string())?,
1079
1080 state: StateValues::try_new_with_all(
1081 OpMode::SemiAutomatic,
1082 JobMode::Offline,
1083 Some(ID::from_u32(42)),
1084 Some("Hello World!"),
1085 None,
1086 )?,
1087
1088 options,
1089 };
1090
1091 let serialized = serde_json::to_string(&msg).map_err(|x| x.to_string())?;
1092
1093 assert_eq!(
1094 r#"{"$type":"MoldData","controllerId":123,"data":{"Hello":123.0,"World":-987.6543,"foo":0.0},"timestamp":"2019-02-26T02:03:04+08:00","opMode":"SemiAutomatic","jobMode":"Offline","operatorId":42,"jobCardId":"Hello World!","sequence":999,"priority":-20}"#,
1095 serialized
1096 );
1097
1098 let m2 = Message::parse_from_json_str(&serialized).map_err(|x| x.to_string())?;
1099
1100 assert_eq!(format!("{:?}", msg), format!("{:?}", m2));
1101
1102 Ok(())
1103 }
1104
1105 #[test]
1106 fn test_message_controllers_list_from_json() -> Result<(), String> {
1107 let json = r#"{"$type":"ControllersList","data":{"12345":{"controllerId":12345,"displayName":"Hello","controllerType":"Ai12","version":"1.0.0","model":"JM128-Ai","IP":"192.168.5.1:123","opMode":"Manual","jobMode":"ID11","lastCycleData":{"Z_QDGODCNT":8567,"Z_QDCYCTIM":979,"Z_QDINJTIM":5450,"Z_QDPLSTIM":7156,"Z_QDINJENDPOS":8449,"Z_QDPLSENDPOS":2212,"Z_QDFLAG":8988,"Z_QDPRDCNT":65500,"Z_QDCOLTIM":4435,"Z_QDMLDOPNTIM":652,"Z_QDMLDCLSTIM":2908,"Z_QDVPPOS":4732,"Z_QDMLDOPNENDPOS":6677,"Z_QDMAXINJSPD":7133,"Z_QDMAXPLSRPM":641,"Z_QDNOZTEMP":6693,"Z_QDTEMPZ01":9964,"Z_QDTEMPZ02":7579,"Z_QDTEMPZ03":4035,"Z_QDTEMPZ04":5510,"Z_QDTEMPZ05":8460,"Z_QDTEMPZ06":9882,"Z_QDBCKPRS":2753,"Z_QDHLDTIM":9936},"lastConnectionTime":"2016-03-06T23:11:27.1442177+08:00"},"22334":{"controllerId":22334,"displayName":"World","controllerType":"Ai01","version":"1.0.0","model":"JM128-Ai","IP":"192.168.5.2:234","opMode":"SemiAutomatic","jobMode":"ID12","lastCycleData":{"Z_QDGODCNT":6031,"Z_QDCYCTIM":7526,"Z_QDINJTIM":4896,"Z_QDPLSTIM":5196,"Z_QDINJENDPOS":1250,"Z_QDPLSENDPOS":8753,"Z_QDFLAG":3314,"Z_QDPRDCNT":65500,"Z_QDCOLTIM":3435,"Z_QDMLDOPNTIM":7854,"Z_QDMLDCLSTIM":4582,"Z_QDVPPOS":7504,"Z_QDMLDOPNENDPOS":7341,"Z_QDMAXINJSPD":7322,"Z_QDMAXPLSRPM":6024,"Z_QDNOZTEMP":3406,"Z_QDTEMPZ01":3067,"Z_QDTEMPZ02":9421,"Z_QDTEMPZ03":2080,"Z_QDTEMPZ04":8845,"Z_QDTEMPZ05":4478,"Z_QDTEMPZ06":3126,"Z_QDBCKPRS":2807,"Z_QDHLDTIM":3928},"lastConnectionTime":"2016-03-06T23:11:27.149218+08:00"}},"sequence":68568}"#;
1108
1109 let msg = Message::parse_from_json_str(&json).map_err(|x| x.to_string())?;
1110
1111 if let ControllersList { data, .. } = &msg {
1112 assert_eq!(2, data.len());
1113 let c = data.get(&ID::from_u32(12345)).unwrap();
1114 assert_eq!("Hello", &c.display_name);
1115 Ok(())
1116 } else {
1117 Err(format!("Expected ControllersList, got {:#?}", msg))
1118 }
1119 }
1120
1121 #[test]
1122 fn test_message_cycle_data_from_json() -> Result<(), String> {
1123 let json = r#"{"$type":"CycleData","timestamp":"2016-02-26T01:12:23+08:00","opMode":"Automatic","jobMode":"ID02","controllerId":123,"data":{"Z_QDGODCNT":123,"Z_QDCYCTIM":12.33,"Z_QDINJTIM":3,"Z_QDPLSTIM":4.4,"Z_QDINJENDPOS":30.1,"Z_QDPLSENDPOS":20.3,"Z_QDFLAG":1,"Z_QDPRDCNT":500,"Z_QDCOLTIM":12.12,"Z_QDMLDOPNTIM":2.1,"Z_QDMLDCLSTIM":1.3,"Z_QDVPPOS":12.11,"Z_QDMLDOPNENDPOS":130.1,"Z_QDMAXINJSPD":213.12,"Z_QDMAXPLSRPM":551,"Z_QDNOZTEMP":256,"Z_QDTEMPZ01":251,"Z_QDTEMPZ02":252,"Z_QDTEMPZ03":253,"Z_QDTEMPZ04":254,"Z_QDTEMPZ05":255,"Z_QDTEMPZ06":256,"Z_QDBCKPRS":54,"Z_QDHLDTIM":2.3,"Z_QDCPT01":231,"Z_QDCPT02":232,"Z_QDCPT03":233,"Z_QDCPT04":234,"Z_QDCPT05":235,"Z_QDCPT06":236,"Z_QDCPT07":237,"Z_QDCPT08":238,"Z_QDCPT09":239,"Z_QDCPT10":240,"Z_QDCPT11":241,"Z_QDCPT12":242,"Z_QDCPT13":243,"Z_QDCPT14":244,"Z_QDCPT15":245,"Z_QDCPT16":246,"Z_QDCPT17":247,"Z_QDCPT18":248,"Z_QDCPT19":249,"Z_QDCPT20":250,"Z_QDCPT21":251,"Z_QDCPT22":252,"Z_QDCPT23":253,"Z_QDCPT24":254,"Z_QDCPT25":255,"Z_QDCPT26":256,"Z_QDCPT27":257,"Z_QDCPT28":258,"Z_QDCPT29":259,"Z_QDCPT30":260,"Z_QDCPT31":261,"Z_QDCPT32":262,"Z_QDCPT33":263,"Z_QDCPT34":264,"Z_QDCPT35":265,"Z_QDCPT36":266,"Z_QDCPT37":267,"Z_QDCPT38":268,"Z_QDCPT39":269,"Z_QDCPT40":270},"sequence":1}"#;
1124
1125 let msg = Message::parse_from_json_str(&json).map_err(|x| x.to_string())?;
1126
1127 if let CycleData { controller_id, data, .. } = &msg {
1128 assert_eq!(0, msg.priority());
1129 assert_eq!(123, *controller_id);
1130 assert_eq!(64, data.len());
1131 assert!(*data.get(&TextID::new("Z_QDCPT13").unwrap()).unwrap() == R32::new(243.0));
1132 Ok(())
1133 } else {
1134 Err(format!("Expected CycleData, got {:#?}", msg))
1135 }
1136 }
1137
1138 #[test]
1139 fn test_message_controller_status_without_controller_from_json() -> Result<(), String> {
1140 let json = r#"{"$type":"ControllerStatus","controllerId":123,"displayName":"Testing","opMode":"Automatic","alarm":{"key":"hello","value":true},"jobMode":"ID05","jobCardId":"XYZ","moldId":"Mold-123","state":{"opMode":"Automatic","jobMode":"ID05","jobCardId":"XYZ","moldId":"Mold-123"},"sequence":1,"priority":50}"#;
1141
1142 let msg = Message::parse_from_json_str(&json).map_err(|x| x.to_string())?;
1143
1144 if let ControllerStatus { controller_id, display_name, controller, alarm, .. } = &msg {
1145 assert_eq!(50, msg.priority());
1146 assert_eq!(1, msg.sequence());
1147 assert_eq!(123, *controller_id);
1148 assert_eq!(Some(Box::new("Testing".try_into().unwrap())), *display_name);
1149 assert!(controller.is_none());
1150 assert_eq!(
1151 Some(Box::new(KeyValuePair::new("hello".try_into().unwrap(), true))),
1152 *alarm
1153 );
1154 Ok(())
1155 } else {
1156 Err(format!("Expected ControllerStatus, got {:#?}", msg))
1157 }
1158 }
1159
1160 #[test]
1161 fn test_message_controller_status_with_controller_from_json() -> Result<(), String> {
1162 let json = r#"{"$type":"ControllerStatus","controllerId":123,"state":{"opMode":"Automatic","jobMode":"ID05","jobCardId":"XYZ","moldId":"Mold-123"},"controller":{"controllerId":123,"displayName":"Testing","controllerType":"Ai02","version":"2.2","model":"JM138Ai","IP":"192.168.1.1:12345","geoLatitude":23.0,"geoLongitude":-121.0,"opMode":"Automatic","jobMode":"ID05","jobCardId":"XYZ","lastCycleData":{"INJ":5,"CLAMP":400},"moldId":"Mold-123"},"sequence":1}"#;
1163
1164 let msg = Message::parse_from_json_str(&json).map_err(|x| x.to_string())?;
1165
1166 if let ControllerStatus { controller_id, display_name, state, controller, .. } = &msg {
1167 assert_eq!(0, msg.priority());
1168 assert_eq!(1, msg.sequence());
1169 assert_eq!(123, *controller_id);
1170 assert_eq!(None, *display_name);
1171 assert_eq!(OpMode::Automatic, state.op_mode());
1172 assert_eq!(JobMode::ID05, state.job_mode());
1173 assert_eq!(Some("XYZ"), state.job_card_id());
1174 let c = controller.as_ref().unwrap();
1175 assert_eq!("JM138Ai", &c.model);
1176 let d = &c.last_cycle_data;
1177 assert!(c.operator.is_none());
1178 assert_eq!(2, d.len());
1179 assert!(*d.get(&TextID::new("INJ").unwrap()).unwrap() == R32::new(5.0));
1180 Ok(())
1181 } else {
1182 Err(format!("Expected ControllerStatus, got {:#?}", msg))
1183 }
1184 }
1185
1186 #[test]
1187 fn test_message_controller_status_to_json() -> Result<(), String> {
1188 let status: Message = ControllerStatus {
1189 controller_id: ID::from_u32(12345),
1190 display_name: None,
1191 is_disconnected: None,
1192 op_mode: None,
1193 job_mode: None,
1194 job_card_id: None,
1195 mold_id: Some(None),
1196 operator_id: Some(Some(ID::from_u32(123))),
1197 operator_name: Some(None),
1198 variable: None,
1199 audit: None,
1200 alarm: Some(Box::new(KeyValuePair::new("hello".try_into().unwrap(), true))),
1201 controller: None,
1202 state: StateValues::try_new_with_all(
1203 OpMode::Automatic,
1204 JobMode::ID02,
1205 Some(ID::from_u32(123)),
1206 None,
1207 None,
1208 )?,
1209 options: MessageOptions::default_new(),
1210 };
1211
1212 let msg = status.to_json_str()?;
1213 assert_eq!(
1214 r#"{"$type":"ControllerStatus","controllerId":12345,"alarm":{"key":"hello","value":true},"operatorId":123,"operatorName":null,"moldId":null,"state":{"opMode":"Automatic","jobMode":"ID02","operatorId":123},"sequence":1}"#,
1215 msg
1216 );
1217 Ok(())
1218 }
1219
1220 #[test]
1221 fn test_message_controller_status_to_json2() -> Result<(), String> {
1222 let status = ControllerStatus {
1223 controller_id: ID::from_u32(12345),
1224 display_name: None,
1225 is_disconnected: Some(true),
1226 op_mode: None,
1227 job_mode: None,
1228 job_card_id: Some(None),
1229 mold_id: Some(Some(Box::new("Test".try_into().unwrap()))),
1230 operator_id: Some(None),
1231 operator_name: Some(None),
1232 variable: None,
1233 audit: None,
1234 alarm: None,
1235 controller: None,
1236 state: StateValues::try_new_with_all(
1237 OpMode::Automatic,
1238 JobMode::ID02,
1239 None,
1240 None,
1241 Some("Test"),
1242 )?,
1243 options: MessageOptions::default_new(),
1244 };
1245
1246 let msg = status.to_json_str()?;
1247 assert_eq!(
1248 r#"{"$type":"ControllerStatus","controllerId":12345,"isDisconnected":true,"operatorId":0,"operatorName":null,"jobCardId":null,"moldId":"Test","state":{"opMode":"Automatic","jobMode":"ID02","moldId":"Test"},"sequence":1}"#,
1249 msg
1250 );
1251 Ok(())
1252 }
1253}