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 normalized: std::borrow::Cow<str> = if tag.contains(',') {
20 std::borrow::Cow::Borrowed(tag)
21 } else {
22 let bytes = tag.as_bytes();
25 let sep = bytes.windows(2).enumerate().find_map(|(i, w)| {
26 if w[0].is_ascii_digit() && w[1].is_ascii_alphabetic() {
27 Some(i + 1) } else {
29 None
30 }
31 });
32 if let Some(pos) = sep {
34 if bytes.get(pos) == Some(&b'.') {
35 let mut s = tag.to_string();
36 s.replace_range(pos..pos + 1, ",");
37 std::borrow::Cow::Owned(s)
38 } else {
39 std::borrow::Cow::Borrowed(tag)
40 }
41 } else {
42 std::borrow::Cow::Borrowed(tag)
43 }
44 };
45
46 let parts: Vec<&str> = normalized.splitn(2, ',').collect();
47 if parts.len() != 2 {
48 return Err(Error::PlcError {
49 code: 0,
50 message: format!("invalid tag: {}", tag),
51 });
52 }
53 let area_part = parts[0].to_uppercase();
54 let type_part = parts[1].to_uppercase();
55
56 let (area, db_number) = if let Some(rest) = area_part.strip_prefix("DB") {
57 let n: u16 = rest.parse().map_err(|_| Error::PlcError {
58 code: 0,
59 message: format!("invalid DB number in tag: {}", tag),
60 })?;
61 (Area::DataBlock, n)
62 } else {
63 return Err(Error::PlcError {
64 code: 0,
65 message: format!("unsupported area in tag: {}", tag),
66 });
67 };
68
69 if type_part.starts_with(|c: char| c.is_ascii_digit()) {
73 let bits: Vec<&str> = type_part.split('.').collect();
74 if bits.len() == 2 {
75 let byte_offset: u32 = bits[0].parse().map_err(|_| Error::PlcError {
76 code: 0,
77 message: format!("invalid byte offset in tag: {}", tag),
78 })?;
79 let bit_offset: u8 = bits[1].parse().map_err(|_| Error::PlcError {
80 code: 0,
81 message: format!("invalid bit offset in tag: {}", tag),
82 })?;
83 if bit_offset > 7 {
84 return Err(Error::PlcError {
85 code: 0,
86 message: format!("bit offset must be 0-7: {}", tag),
87 });
88 }
89 return Ok(TagAddress {
90 area,
91 db_number,
92 byte_offset,
93 bit_offset,
94 transport: TransportSize::Bit,
95 element_count: 1,
96 });
97 }
98 }
99
100 let (transport, byte_offset) = if let Some(rest) = type_part.strip_prefix("REAL") {
101 (TransportSize::Real, rest.parse().unwrap_or(0))
102 } else if let Some(rest) = type_part.strip_prefix("DWORD") {
103 (TransportSize::DWord, rest.parse().unwrap_or(0))
104 } else if let Some(rest) = type_part.strip_prefix("DINT") {
105 (TransportSize::DInt, rest.parse().unwrap_or(0))
106 } else if let Some(rest) = type_part.strip_prefix("WORD") {
107 (TransportSize::Word, rest.parse().unwrap_or(0))
108 } else if let Some(rest) = type_part.strip_prefix("INT") {
109 (TransportSize::Int, rest.parse().unwrap_or(0))
110 } else if let Some(rest) = type_part.strip_prefix("BYTE") {
111 (TransportSize::Byte, rest.parse().unwrap_or(0))
112 } else {
113 return Err(Error::PlcError {
114 code: 0,
115 message: format!("unsupported type in tag: {}", tag),
116 });
117 };
118
119 Ok(TagAddress {
120 area,
121 db_number,
122 byte_offset,
123 bit_offset: 0,
124 transport,
125 element_count: 1,
126 })
127}
128
129#[cfg(test)]
130mod tests {
131 use super::*;
132
133 #[test]
134 fn parse_db_real() {
135 let tag = parse_tag("DB1,REAL4").unwrap();
136 assert_eq!(tag.db_number, 1);
137 assert_eq!(tag.byte_offset, 4);
138 assert_eq!(tag.transport, TransportSize::Real);
139 }
140
141 #[test]
142 fn parse_db_word() {
143 let tag = parse_tag("DB2,WORD10").unwrap();
144 assert_eq!(tag.db_number, 2);
145 assert_eq!(tag.byte_offset, 10);
146 assert_eq!(tag.transport, TransportSize::Word);
147 }
148
149 #[test]
150 fn parse_db_dint() {
151 let tag = parse_tag("DB70,DINT0").unwrap();
152 assert_eq!(tag.db_number, 70);
153 assert_eq!(tag.byte_offset, 0);
154 assert_eq!(tag.transport, TransportSize::DInt);
155 assert_eq!(tag.bit_offset, 0);
156 }
157
158 #[test]
159 fn parse_db_bit_access() {
160 let tag = parse_tag("DB70,332.0").unwrap();
161 assert_eq!(tag.db_number, 70);
162 assert_eq!(tag.byte_offset, 332);
163 assert_eq!(tag.bit_offset, 0);
164 assert_eq!(tag.transport, TransportSize::Bit);
165 }
166
167 #[test]
168 fn parse_db_bit_access_bit7() {
169 let tag = parse_tag("DB70,332.7").unwrap();
170 assert_eq!(tag.db_number, 70);
171 assert_eq!(tag.byte_offset, 332);
172 assert_eq!(tag.bit_offset, 7);
173 assert_eq!(tag.transport, TransportSize::Bit);
174 }
175
176 #[test]
177 fn parse_db_bit_invalid_bit() {
178 let result = parse_tag("DB70,332.8");
179 assert!(result.is_err());
180 }
181
182 #[test]
183 fn parse_invalid_returns_err() {
184 assert!(parse_tag("NOTVALID").is_err());
185 }
186
187 #[test]
188 fn parse_dot_separator_real() {
189 let tag = parse_tag("DB170.REAL262").unwrap();
190 assert_eq!(tag.db_number, 170);
191 assert_eq!(tag.byte_offset, 262);
192 assert_eq!(tag.transport, TransportSize::Real);
193 }
194
195 #[test]
196 fn parse_dot_separator_word() {
197 let tag = parse_tag("DB1.WORD10").unwrap();
198 assert_eq!(tag.db_number, 1);
199 assert_eq!(tag.byte_offset, 10);
200 assert_eq!(tag.transport, TransportSize::Word);
201 }
202
203 #[test]
204 fn parse_comma_separator_unchanged() {
205 let a = parse_tag("DB170,REAL262").unwrap();
206 let b = parse_tag("DB170.REAL262").unwrap();
207 assert_eq!(a.db_number, b.db_number);
208 assert_eq!(a.byte_offset, b.byte_offset);
209 assert_eq!(a.transport, b.transport);
210 }
211
212 #[test]
213 fn parse_bit_access_dot_not_confused() {
214 let tag = parse_tag("DB70,332.0").unwrap();
216 assert_eq!(tag.transport, TransportSize::Bit);
217 assert_eq!(tag.byte_offset, 332);
218 assert_eq!(tag.bit_offset, 0);
219 }
220}