Skip to main content

snap7_client/
tag.rs

1use crate::proto::s7::header::{Area, TransportSize};
2
3use crate::error::{Error, Result};
4
5#[derive(Debug, Clone)]
6pub struct TagAddress {
7    pub area: Area,
8    pub db_number: u16,
9    pub byte_offset: u32,
10    pub bit_offset: u8,
11    pub transport: TransportSize,
12    pub element_count: u16,
13}
14
15pub fn parse_tag(tag: &str) -> Result<TagAddress> {
16    let parts: Vec<&str> = tag.splitn(2, ',').collect();
17    if parts.len() != 2 {
18        return Err(Error::PlcError {
19            code: 0,
20            message: format!("invalid tag: {}", tag),
21        });
22    }
23    let area_part = parts[0].to_uppercase();
24    let type_part = parts[1].to_uppercase();
25
26    let (area, db_number) = if let Some(rest) = area_part.strip_prefix("DB") {
27        let n: u16 = rest.parse().map_err(|_| Error::PlcError {
28            code: 0,
29            message: format!("invalid DB number in tag: {}", tag),
30        })?;
31        (Area::DataBlock, n)
32    } else {
33        return Err(Error::PlcError {
34            code: 0,
35            message: format!("unsupported area in tag: {}", tag),
36        });
37    };
38
39    // Support bit access format: DB70,332.0 (byte 332, bit 0)
40    // type_part is already uppercased from line 24
41    // For bit access, type_part starts with a digit (byte offset)
42    if type_part.starts_with(|c: char| c.is_ascii_digit()) {
43        let bits: Vec<&str> = type_part.split('.').collect();
44        if bits.len() == 2 {
45            let byte_offset: u32 = bits[0].parse().map_err(|_| Error::PlcError {
46                code: 0,
47                message: format!("invalid byte offset in tag: {}", tag),
48            })?;
49            let bit_offset: u8 = bits[1].parse().map_err(|_| Error::PlcError {
50                code: 0,
51                message: format!("invalid bit offset in tag: {}", tag),
52            })?;
53            if bit_offset > 7 {
54                return Err(Error::PlcError {
55                    code: 0,
56                    message: format!("bit offset must be 0-7: {}", tag),
57                });
58            }
59            return Ok(TagAddress {
60                area,
61                db_number,
62                byte_offset,
63                bit_offset,
64                transport: TransportSize::Bit,
65                element_count: 1,
66            });
67        }
68    }
69
70    let (transport, byte_offset) = if let Some(rest) = type_part.strip_prefix("REAL") {
71        (TransportSize::Real, rest.parse().unwrap_or(0))
72    } else if let Some(rest) = type_part.strip_prefix("DWORD") {
73        (TransportSize::DWord, rest.parse().unwrap_or(0))
74    } else if let Some(rest) = type_part.strip_prefix("DINT") {
75        (TransportSize::DInt, rest.parse().unwrap_or(0))
76    } else if let Some(rest) = type_part.strip_prefix("WORD") {
77        (TransportSize::Word, rest.parse().unwrap_or(0))
78    } else if let Some(rest) = type_part.strip_prefix("INT") {
79        (TransportSize::Int, rest.parse().unwrap_or(0))
80    } else if let Some(rest) = type_part.strip_prefix("BYTE") {
81        (TransportSize::Byte, rest.parse().unwrap_or(0))
82    } else {
83        return Err(Error::PlcError {
84            code: 0,
85            message: format!("unsupported type in tag: {}", tag),
86        });
87    };
88
89    Ok(TagAddress {
90        area,
91        db_number,
92        byte_offset,
93        bit_offset: 0,
94        transport,
95        element_count: 1,
96    })
97}
98
99#[cfg(test)]
100mod tests {
101    use super::*;
102
103    #[test]
104    fn parse_db_real() {
105        let tag = parse_tag("DB1,REAL4").unwrap();
106        assert_eq!(tag.db_number, 1);
107        assert_eq!(tag.byte_offset, 4);
108        assert_eq!(tag.transport, TransportSize::Real);
109    }
110
111    #[test]
112    fn parse_db_word() {
113        let tag = parse_tag("DB2,WORD10").unwrap();
114        assert_eq!(tag.db_number, 2);
115        assert_eq!(tag.byte_offset, 10);
116        assert_eq!(tag.transport, TransportSize::Word);
117    }
118
119    #[test]
120    fn parse_db_dint() {
121        let tag = parse_tag("DB70,DINT0").unwrap();
122        assert_eq!(tag.db_number, 70);
123        assert_eq!(tag.byte_offset, 0);
124        assert_eq!(tag.transport, TransportSize::DInt);
125        assert_eq!(tag.bit_offset, 0);
126    }
127
128    #[test]
129    fn parse_db_bit_access() {
130        let tag = parse_tag("DB70,332.0").unwrap();
131        assert_eq!(tag.db_number, 70);
132        assert_eq!(tag.byte_offset, 332);
133        assert_eq!(tag.bit_offset, 0);
134        assert_eq!(tag.transport, TransportSize::Bit);
135    }
136
137    #[test]
138    fn parse_db_bit_access_bit7() {
139        let tag = parse_tag("DB70,332.7").unwrap();
140        assert_eq!(tag.db_number, 70);
141        assert_eq!(tag.byte_offset, 332);
142        assert_eq!(tag.bit_offset, 7);
143        assert_eq!(tag.transport, TransportSize::Bit);
144    }
145
146    #[test]
147    fn parse_db_bit_invalid_bit() {
148        let result = parse_tag("DB70,332.8");
149        assert!(result.is_err());
150    }
151
152    #[test]
153    fn parse_invalid_returns_err() {
154        assert!(parse_tag("NOTVALID").is_err());
155    }
156}