Skip to main content

plc_comm_slmp/
model.rs

1use std::fmt;
2use std::time::Duration;
3
4#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
5pub enum SlmpTransportMode {
6    Tcp,
7    Udp,
8}
9
10#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
11pub enum SlmpFrameType {
12    Frame3E,
13    Frame4E,
14}
15
16#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
17pub enum SlmpCompatibilityMode {
18    Legacy,
19    Iqr,
20}
21
22#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
23pub enum SlmpPlcFamily {
24    IqF,
25    IqR,
26    IqL,
27    MxF,
28    MxR,
29    QCpu,
30    LCpu,
31    QnU,
32    QnUDV,
33}
34
35#[derive(Debug, Clone, Copy, PartialEq, Eq)]
36pub struct SlmpPlcFamilyDefaults {
37    pub frame_type: SlmpFrameType,
38    pub compatibility_mode: SlmpCompatibilityMode,
39}
40
41impl SlmpPlcFamily {
42    pub fn canonical_name(self) -> &'static str {
43        match self {
44            Self::IqF => "iq-f",
45            Self::IqR => "iq-r",
46            Self::IqL => "iq-l",
47            Self::MxF => "mx-f",
48            Self::MxR => "mx-r",
49            Self::QCpu => "qcpu",
50            Self::LCpu => "lcpu",
51            Self::QnU => "qnu",
52            Self::QnUDV => "qnudv",
53        }
54    }
55
56    pub fn parse_label(value: &str) -> Option<Self> {
57        match value
58            .trim()
59            .to_ascii_lowercase()
60            .replace(['-', '_'], "")
61            .as_str()
62        {
63            "iqf" => Some(Self::IqF),
64            "iqr" => Some(Self::IqR),
65            "iql" => Some(Self::IqL),
66            "mxf" => Some(Self::MxF),
67            "mxr" => Some(Self::MxR),
68            "qcpu" => Some(Self::QCpu),
69            "lcpu" => Some(Self::LCpu),
70            "qnu" => Some(Self::QnU),
71            "qnudv" => Some(Self::QnUDV),
72            _ => None,
73        }
74    }
75
76    pub fn defaults(self) -> SlmpPlcFamilyDefaults {
77        match self {
78            Self::IqF => SlmpPlcFamilyDefaults {
79                frame_type: SlmpFrameType::Frame3E,
80                compatibility_mode: SlmpCompatibilityMode::Legacy,
81            },
82            Self::IqR | Self::IqL | Self::MxF | Self::MxR => SlmpPlcFamilyDefaults {
83                frame_type: SlmpFrameType::Frame4E,
84                compatibility_mode: SlmpCompatibilityMode::Iqr,
85            },
86            Self::QCpu | Self::LCpu | Self::QnU | Self::QnUDV => SlmpPlcFamilyDefaults {
87                frame_type: SlmpFrameType::Frame3E,
88                compatibility_mode: SlmpCompatibilityMode::Legacy,
89            },
90        }
91    }
92
93    pub fn address_family(self) -> Self {
94        match self {
95            // iQ-L keeps its own PLC family and device-range family. We only
96            // reuse iQ-R-style address parsing rules here where the notation
97            // grammar matches.
98            Self::IqL => Self::IqR,
99            other => other,
100        }
101    }
102
103    pub fn uses_iqf_xy_octal(self) -> bool {
104        matches!(self.address_family(), Self::IqF)
105    }
106}
107
108#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
109pub enum SlmpTraceDirection {
110    Send,
111    Receive,
112}
113
114#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
115pub struct SlmpTrafficStats {
116    pub request_count: u64,
117    pub tx_bytes: u64,
118    pub rx_bytes: u64,
119}
120
121#[derive(Debug, Clone, Copy, PartialEq, Eq)]
122#[repr(u16)]
123pub enum SlmpCommand {
124    DeviceRead = 0x0401,
125    DeviceWrite = 0x1401,
126    DeviceReadRandom = 0x0403,
127    DeviceWriteRandom = 0x1402,
128    DeviceReadBlock = 0x0406,
129    DeviceWriteBlock = 0x1406,
130    MonitorRegister = 0x0801,
131    Monitor = 0x0802,
132    ReadTypeName = 0x0101,
133    LabelArrayRead = 0x041A,
134    LabelArrayWrite = 0x141A,
135    LabelReadRandom = 0x041C,
136    LabelWriteRandom = 0x141B,
137    MemoryRead = 0x0613,
138    MemoryWrite = 0x1613,
139    ExtendUnitRead = 0x0601,
140    ExtendUnitWrite = 0x1601,
141    RemoteRun = 0x1001,
142    RemoteStop = 0x1002,
143    RemotePause = 0x1003,
144    RemoteLatchClear = 0x1005,
145    RemoteReset = 0x1006,
146    RemotePasswordUnlock = 0x1630,
147    RemotePasswordLock = 0x1631,
148    SelfTest = 0x0619,
149    ClearError = 0x1617,
150}
151
152impl SlmpCommand {
153    pub fn as_u16(self) -> u16 {
154        self as u16
155    }
156}
157
158#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
159#[repr(u16)]
160pub enum SlmpDeviceCode {
161    SM = 0x0091,
162    SD = 0x00A9,
163    X = 0x009C,
164    Y = 0x009D,
165    M = 0x0090,
166    L = 0x0092,
167    F = 0x0093,
168    V = 0x0094,
169    B = 0x00A0,
170    D = 0x00A8,
171    W = 0x00B4,
172    TS = 0x00C1,
173    TC = 0x00C0,
174    TN = 0x00C2,
175    LTS = 0x0051,
176    LTC = 0x0050,
177    LTN = 0x0052,
178    STS = 0x00C7,
179    STC = 0x00C6,
180    STN = 0x00C8,
181    LSTS = 0x0059,
182    LSTC = 0x0058,
183    LSTN = 0x005A,
184    LCC = 0x0054,
185    LCS = 0x0055,
186    LCN = 0x0056,
187    CS = 0x00C4,
188    CC = 0x00C3,
189    CN = 0x00C5,
190    SB = 0x00A1,
191    SW = 0x00B5,
192    DX = 0x00A2,
193    DY = 0x00A3,
194    Z = 0x00CC,
195    LZ = 0x0062,
196    R = 0x00AF,
197    ZR = 0x00B0,
198    RD = 0x002C,
199    G = 0x00AB,
200    HG = 0x002E,
201}
202
203impl SlmpDeviceCode {
204    pub fn as_u16(self) -> u16 {
205        self as u16
206    }
207
208    pub fn as_u8(self) -> u8 {
209        (self.as_u16() & 0x00FF) as u8
210    }
211
212    pub fn prefix(self) -> &'static str {
213        match self {
214            Self::SM => "SM",
215            Self::SD => "SD",
216            Self::X => "X",
217            Self::Y => "Y",
218            Self::M => "M",
219            Self::L => "L",
220            Self::F => "F",
221            Self::V => "V",
222            Self::B => "B",
223            Self::D => "D",
224            Self::W => "W",
225            Self::TS => "TS",
226            Self::TC => "TC",
227            Self::TN => "TN",
228            Self::LTS => "LTS",
229            Self::LTC => "LTC",
230            Self::LTN => "LTN",
231            Self::STS => "STS",
232            Self::STC => "STC",
233            Self::STN => "STN",
234            Self::LSTS => "LSTS",
235            Self::LSTC => "LSTC",
236            Self::LSTN => "LSTN",
237            Self::LCC => "LCC",
238            Self::LCS => "LCS",
239            Self::LCN => "LCN",
240            Self::CS => "CS",
241            Self::CC => "CC",
242            Self::CN => "CN",
243            Self::SB => "SB",
244            Self::SW => "SW",
245            Self::DX => "DX",
246            Self::DY => "DY",
247            Self::Z => "Z",
248            Self::LZ => "LZ",
249            Self::R => "R",
250            Self::ZR => "ZR",
251            Self::RD => "RD",
252            Self::G => "G",
253            Self::HG => "HG",
254        }
255    }
256
257    pub fn is_hex_addressed(self) -> bool {
258        matches!(
259            self,
260            Self::X | Self::Y | Self::B | Self::W | Self::SB | Self::SW | Self::DX | Self::DY
261        )
262    }
263
264    pub fn is_bit_device(self) -> bool {
265        matches!(
266            self,
267            Self::SM
268                | Self::X
269                | Self::Y
270                | Self::M
271                | Self::L
272                | Self::F
273                | Self::V
274                | Self::B
275                | Self::TS
276                | Self::TC
277                | Self::LTS
278                | Self::LTC
279                | Self::STS
280                | Self::STC
281                | Self::LSTS
282                | Self::LSTC
283                | Self::CS
284                | Self::CC
285                | Self::LCS
286                | Self::LCC
287                | Self::SB
288                | Self::DX
289                | Self::DY
290        )
291    }
292
293    pub fn is_word_device(self) -> bool {
294        matches!(
295            self,
296            Self::SD
297                | Self::D
298                | Self::W
299                | Self::TN
300                | Self::LTN
301                | Self::STN
302                | Self::LSTN
303                | Self::CN
304                | Self::LCN
305                | Self::SW
306                | Self::Z
307                | Self::LZ
308                | Self::R
309                | Self::ZR
310                | Self::RD
311                | Self::G
312                | Self::HG
313        )
314    }
315
316    pub fn is_word_batchable(self) -> bool {
317        matches!(
318            self,
319            Self::SD
320                | Self::D
321                | Self::W
322                | Self::TN
323                | Self::LTN
324                | Self::STN
325                | Self::LSTN
326                | Self::CN
327                | Self::LCN
328                | Self::SW
329                | Self::Z
330                | Self::LZ
331                | Self::R
332                | Self::ZR
333                | Self::RD
334        )
335    }
336
337    pub fn parse_prefix(prefix: &str) -> Option<Self> {
338        match prefix {
339            "LSTS" => Some(Self::LSTS),
340            "LSTC" => Some(Self::LSTC),
341            "LSTN" => Some(Self::LSTN),
342            "LTS" => Some(Self::LTS),
343            "LTC" => Some(Self::LTC),
344            "LTN" => Some(Self::LTN),
345            "STS" => Some(Self::STS),
346            "STC" => Some(Self::STC),
347            "STN" => Some(Self::STN),
348            "SM" => Some(Self::SM),
349            "SD" => Some(Self::SD),
350            "TS" => Some(Self::TS),
351            "TC" => Some(Self::TC),
352            "TN" => Some(Self::TN),
353            "CS" => Some(Self::CS),
354            "CC" => Some(Self::CC),
355            "CN" => Some(Self::CN),
356            "SB" => Some(Self::SB),
357            "SW" => Some(Self::SW),
358            "DX" => Some(Self::DX),
359            "DY" => Some(Self::DY),
360            "LCS" => Some(Self::LCS),
361            "LCC" => Some(Self::LCC),
362            "LCN" => Some(Self::LCN),
363            "LZ" => Some(Self::LZ),
364            "ZR" => Some(Self::ZR),
365            "RD" => Some(Self::RD),
366            "HG" => Some(Self::HG),
367            "X" => Some(Self::X),
368            "Y" => Some(Self::Y),
369            "M" => Some(Self::M),
370            "L" => Some(Self::L),
371            "F" => Some(Self::F),
372            "V" => Some(Self::V),
373            "B" => Some(Self::B),
374            "D" => Some(Self::D),
375            "W" => Some(Self::W),
376            "Z" => Some(Self::Z),
377            "R" => Some(Self::R),
378            "G" => Some(Self::G),
379            _ => None,
380        }
381    }
382}
383
384impl fmt::Display for SlmpDeviceCode {
385    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
386        f.write_str(self.prefix())
387    }
388}
389
390#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
391pub struct SlmpTargetAddress {
392    pub network: u8,
393    pub station: u8,
394    pub module_io: u16,
395    pub multidrop: u8,
396}
397
398impl Default for SlmpTargetAddress {
399    fn default() -> Self {
400        Self {
401            network: 0x00,
402            station: 0xFF,
403            module_io: 0x03FF,
404            multidrop: 0x00,
405        }
406    }
407}
408
409#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
410pub struct SlmpDeviceAddress {
411    pub code: SlmpDeviceCode,
412    pub number: u32,
413}
414
415impl SlmpDeviceAddress {
416    pub const fn new(code: SlmpDeviceCode, number: u32) -> Self {
417        Self { code, number }
418    }
419}
420
421impl fmt::Display for SlmpDeviceAddress {
422    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
423        write!(f, "{}{}", self.code, self.number)
424    }
425}
426
427#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
428pub struct SlmpTypeNameInfo {
429    pub model: String,
430    pub model_code: u16,
431    pub has_model_code: bool,
432}
433
434#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
435pub enum SlmpCpuOperationStatus {
436    Unknown,
437    Run,
438    Stop,
439    Pause,
440}
441
442#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
443pub struct SlmpCpuOperationState {
444    pub status: SlmpCpuOperationStatus,
445    pub raw_status_word: u16,
446    pub raw_code: u8,
447}
448
449#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
450pub struct SlmpBlockRead {
451    pub device: SlmpDeviceAddress,
452    pub points: u16,
453}
454
455#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
456pub struct SlmpBlockWrite {
457    pub device: SlmpDeviceAddress,
458    pub values: Vec<u16>,
459}
460
461#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize, Default)]
462pub struct SlmpBlockWriteOptions {
463    pub split_mixed_blocks: bool,
464    pub retry_mixed_on_error: bool,
465}
466
467#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize, Default)]
468pub struct SlmpRandomReadResult {
469    pub word_values: Vec<u16>,
470    pub dword_values: Vec<u32>,
471}
472
473#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize, Default)]
474pub struct SlmpBlockReadResult {
475    pub word_values: Vec<u16>,
476    pub bit_values: Vec<u16>,
477}
478
479#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
480pub struct SlmpLabelArrayReadPoint {
481    pub label: String,
482    pub unit_specification: u8,
483    pub array_data_length: u16,
484}
485
486#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
487pub struct SlmpLabelArrayWritePoint {
488    pub label: String,
489    pub unit_specification: u8,
490    pub array_data_length: u16,
491    pub data: Vec<u8>,
492}
493
494#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
495pub struct SlmpLabelRandomWritePoint {
496    pub label: String,
497    pub data: Vec<u8>,
498}
499
500#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
501pub struct SlmpLabelArrayReadResult {
502    pub data_type_id: u8,
503    pub unit_specification: u8,
504    pub array_data_length: u16,
505    pub data: Vec<u8>,
506}
507
508#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
509pub struct SlmpLabelRandomReadResult {
510    pub data_type_id: u8,
511    pub spare: u8,
512    pub read_data_length: u16,
513    pub data: Vec<u8>,
514}
515
516#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
517pub struct SlmpLongTimerResult {
518    pub index: u32,
519    pub device: String,
520    pub current_value: u32,
521    pub contact: bool,
522    pub coil: bool,
523    pub status_word: u16,
524    pub raw_words: Vec<u16>,
525}
526
527#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
528pub struct SlmpExtensionSpec {
529    pub extension_specification: u16,
530    pub extension_specification_modification: u8,
531    pub device_modification_index: u8,
532    pub device_modification_flags: u8,
533    pub direct_memory_specification: u8,
534}
535
536impl Default for SlmpExtensionSpec {
537    fn default() -> Self {
538        Self {
539            extension_specification: 0x0000,
540            extension_specification_modification: 0x00,
541            device_modification_index: 0x00,
542            device_modification_flags: 0x00,
543            direct_memory_specification: 0x00,
544        }
545    }
546}
547
548#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
549pub struct SlmpQualifiedDeviceAddress {
550    pub device: SlmpDeviceAddress,
551    pub extension_specification: Option<u16>,
552    pub direct_memory_specification: Option<u8>,
553}
554
555#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
556pub struct SlmpNamedTarget {
557    pub name: String,
558    pub target: SlmpTargetAddress,
559}
560
561#[derive(Debug, Clone)]
562pub struct SlmpConnectionOptions {
563    pub host: String,
564    pub port: u16,
565    pub timeout: Duration,
566    pub plc_family: SlmpPlcFamily,
567    pub frame_type: SlmpFrameType,
568    pub compatibility_mode: SlmpCompatibilityMode,
569    pub target: SlmpTargetAddress,
570    pub transport_mode: SlmpTransportMode,
571    pub monitoring_timer: u16,
572}
573
574impl SlmpConnectionOptions {
575    pub fn new(host: impl Into<String>, family: SlmpPlcFamily) -> Self {
576        let defaults = family.defaults();
577        Self {
578            host: host.into(),
579            port: 1025,
580            timeout: Duration::from_secs(3),
581            plc_family: family,
582            frame_type: defaults.frame_type,
583            compatibility_mode: defaults.compatibility_mode,
584            target: SlmpTargetAddress::default(),
585            transport_mode: SlmpTransportMode::Tcp,
586            monitoring_timer: 0x0010,
587        }
588    }
589
590    pub fn set_plc_family(&mut self, family: SlmpPlcFamily) {
591        let defaults = family.defaults();
592        self.plc_family = family;
593        self.frame_type = defaults.frame_type;
594        self.compatibility_mode = defaults.compatibility_mode;
595    }
596}