1use std::collections::{BTreeMap, HashMap};
2
3use serde::{Deserialize, Serialize};
4use serde_json::{Map, Number, Value};
5
6pub type ParamMap = HashMap<String, Value>;
7
8#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
9pub enum ConnectionType {
10 Serial,
11 Tcp,
12 Ble,
13}
14
15impl ConnectionType {
16 pub fn from_str(value: &str) -> Self {
17 match value.trim().to_uppercase().as_str() {
18 "TCP" => Self::Tcp,
19 "BLE" => Self::Ble,
20 _ => Self::Serial,
21 }
22 }
23
24 pub fn as_wire_str(&self) -> &'static str {
25 match self {
26 Self::Serial => "SERIAL",
27 Self::Tcp => "TCP",
28 Self::Ble => "BLE",
29 }
30 }
31}
32
33#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
34pub struct SerialConfig {
35 pub port: String,
36 pub baudrate: u32,
37 pub vid: u16,
38 pub pid: u16,
39}
40
41impl Default for SerialConfig {
42 fn default() -> Self {
43 Self {
44 port: "AUTO".to_string(),
45 baudrate: 115_200,
46 vid: 1,
47 pid: 1,
48 }
49 }
50}
51
52#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
53pub struct TcpConfig {
54 pub ip: String,
55 pub port: u16,
56}
57
58impl Default for TcpConfig {
59 fn default() -> Self {
60 Self {
61 ip: "192.168.1.100".to_string(),
62 port: 23,
63 }
64 }
65}
66
67#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
68pub struct BleConfig {
69 pub name: String,
70 pub address: Option<String>,
71 pub service_uuid: String,
72 pub rx_uuid: String,
73 pub tx_uuid: String,
74}
75
76impl Default for BleConfig {
77 fn default() -> Self {
78 Self {
79 name: "SMTX".to_string(),
80 address: None,
81 service_uuid: "6E400001-B5A3-F393-E0A9-E50E24DCCA9E".to_string(),
82 rx_uuid: "6E400002-B5A3-F393-E0A9-E50E24DCCA9E".to_string(),
83 tx_uuid: "6E400003-B5A3-F393-E0A9-E50E24DCCA9E".to_string(),
84 }
85 }
86}
87
88#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
89pub struct AntennaConfig {
90 pub active: bool,
91 pub power: i32,
92 pub rssi: i32,
93}
94
95impl Default for AntennaConfig {
96 fn default() -> Self {
97 Self {
98 active: true,
99 power: 22,
100 rssi: -120,
101 }
102 }
103}
104
105#[derive(Debug, Clone, Serialize, Deserialize)]
106pub struct X714Config {
107 pub name: String,
108 pub connection_type: ConnectionType,
109 pub serial: SerialConfig,
110 pub tcp: TcpConfig,
111 pub ble: BleConfig,
112 pub buzzer: bool,
113 pub session: u8,
114 pub start_reading: bool,
115 pub gpi_start: bool,
116 pub always_send: bool,
117 pub simple_send: bool,
118 pub keyboard: bool,
119 pub decode_gtin: bool,
120 pub hotspot: bool,
121 pub reconnection_time: u64,
122 pub prefix: String,
123 pub protected_inventory_active: bool,
124 pub protected_inventory_password: String,
125 pub ant_dict: BTreeMap<String, AntennaConfig>,
126}
127
128impl Default for X714Config {
129 fn default() -> Self {
130 let mut ant_dict = BTreeMap::new();
131 for (key, active) in [("1", true), ("2", false), ("3", false), ("4", false)] {
132 ant_dict.insert(
133 key.to_string(),
134 AntennaConfig {
135 active,
136 ..AntennaConfig::default()
137 },
138 );
139 }
140
141 Self {
142 name: "X714".to_string(),
143 connection_type: ConnectionType::Serial,
144 serial: SerialConfig::default(),
145 tcp: TcpConfig::default(),
146 ble: BleConfig::default(),
147 buzzer: false,
148 session: 1,
149 start_reading: false,
150 gpi_start: false,
151 always_send: true,
152 simple_send: false,
153 keyboard: false,
154 decode_gtin: false,
155 hotspot: true,
156 reconnection_time: 3,
157 prefix: String::new(),
158 protected_inventory_active: false,
159 protected_inventory_password: "12345678".to_string(),
160 ant_dict,
161 }
162 }
163}
164
165impl X714Config {
166 pub fn from_map(params: ParamMap) -> Result<Self, String> {
167 let mut config = Self::default();
168
169 if let Some(v) = get_string(¶ms, "name") {
170 config.name = v;
171 }
172 if let Some(v) = get_string(¶ms, "connection_type") {
173 config.connection_type = ConnectionType::from_str(&v);
174 }
175
176 if let Some(v) = get_string(¶ms, "port") {
177 config.serial.port = v;
178 }
179 if let Some(v) = get_u32(¶ms, "baudrate") {
180 config.serial.baudrate = v;
181 }
182 if let Some(v) = get_u16(¶ms, "vid") {
183 config.serial.vid = v;
184 }
185 if let Some(v) = get_u16(¶ms, "pid") {
186 config.serial.pid = v;
187 }
188
189 if let Some(v) = get_string(¶ms, "ip") {
190 config.tcp.ip = v;
191 }
192 if let Some(v) = get_u16(¶ms, "tcp_port") {
193 config.tcp.port = v;
194 }
195
196 if let Some(v) = get_string(¶ms, "ble_name") {
197 config.ble.name = v;
198 }
199 if let Some(v) = get_string(¶ms, "ble_address") {
200 config.ble.address = Some(v);
201 }
202 if let Some(v) = get_string(¶ms, "ble_service_uuid") {
203 config.ble.service_uuid = v;
204 }
205 if let Some(v) = get_string(¶ms, "ble_rx_uuid") {
206 config.ble.rx_uuid = v;
207 }
208 if let Some(v) = get_string(¶ms, "ble_tx_uuid") {
209 config.ble.tx_uuid = v;
210 }
211
212 set_bool(¶ms, "buzzer", &mut config.buzzer);
213 if let Some(v) = get_u8(¶ms, "session") {
214 config.session = v.clamp(0, 3);
215 }
216 set_bool(¶ms, "start_reading", &mut config.start_reading);
217 set_bool(¶ms, "gpi_start", &mut config.gpi_start);
218 set_bool(¶ms, "always_send", &mut config.always_send);
219 set_bool(¶ms, "simple_send", &mut config.simple_send);
220 set_bool(¶ms, "keyboard", &mut config.keyboard);
221 set_bool(¶ms, "decode_gtin", &mut config.decode_gtin);
222 set_bool(¶ms, "hotspot", &mut config.hotspot);
223
224 if let Some(v) = get_u64(¶ms, "reconnection_time") {
225 config.reconnection_time = v;
226 }
227 if let Some(v) = get_string(¶ms, "prefix") {
228 config.prefix = normalize_hex_prefix(&v);
229 }
230
231 set_bool(
232 ¶ms,
233 "protected_inventory_active",
234 &mut config.protected_inventory_active,
235 );
236 if let Some(v) = get_string(¶ms, "protected_inventory_password") {
237 config.protected_inventory_password = v;
238 }
239
240 if let Some(raw_ant) = params.get("ant_dict") {
241 config.ant_dict = parse_ant_dict(raw_ant)?;
242 } else {
243 let active_ant = get_u8_vec(¶ms, "active_ant").unwrap_or_else(|| vec![1]);
244 let read_power = get_i32(¶ms, "read_power").unwrap_or(22);
245 let read_rssi = get_i32(¶ms, "read_rssi").unwrap_or(-120);
246 for ant in [1_u8, 2, 3, 4] {
247 config.ant_dict.insert(
248 ant.to_string(),
249 AntennaConfig {
250 active: active_ant.contains(&ant),
251 power: read_power,
252 rssi: read_rssi,
253 },
254 );
255 }
256 }
257
258 if config.gpi_start {
259 config.start_reading = false;
260 }
261
262 Ok(config)
263 }
264
265 pub fn to_map(&self) -> ParamMap {
266 let mut out = HashMap::new();
267 out.insert("name".to_string(), Value::String(self.name.clone()));
268 out.insert(
269 "connection_type".to_string(),
270 Value::String(self.connection_type.as_wire_str().to_string()),
271 );
272 out.insert("port".to_string(), Value::String(self.serial.port.clone()));
273 out.insert(
274 "baudrate".to_string(),
275 Value::Number(Number::from(self.serial.baudrate)),
276 );
277 out.insert(
278 "vid".to_string(),
279 Value::Number(Number::from(self.serial.vid)),
280 );
281 out.insert(
282 "pid".to_string(),
283 Value::Number(Number::from(self.serial.pid)),
284 );
285 out.insert("ip".to_string(), Value::String(self.tcp.ip.clone()));
286 out.insert(
287 "tcp_port".to_string(),
288 Value::Number(Number::from(self.tcp.port)),
289 );
290 out.insert("ble_name".to_string(), Value::String(self.ble.name.clone()));
291 out.insert(
292 "ble_address".to_string(),
293 self.ble
294 .address
295 .clone()
296 .map(Value::String)
297 .unwrap_or(Value::Null),
298 );
299 out.insert(
300 "ble_service_uuid".to_string(),
301 Value::String(self.ble.service_uuid.clone()),
302 );
303 out.insert(
304 "ble_rx_uuid".to_string(),
305 Value::String(self.ble.rx_uuid.clone()),
306 );
307 out.insert(
308 "ble_tx_uuid".to_string(),
309 Value::String(self.ble.tx_uuid.clone()),
310 );
311 out.insert("buzzer".to_string(), Value::Bool(self.buzzer));
312 out.insert(
313 "session".to_string(),
314 Value::Number(Number::from(self.session)),
315 );
316 out.insert("start_reading".to_string(), Value::Bool(self.start_reading));
317 out.insert("gpi_start".to_string(), Value::Bool(self.gpi_start));
318 out.insert("always_send".to_string(), Value::Bool(self.always_send));
319 out.insert("simple_send".to_string(), Value::Bool(self.simple_send));
320 out.insert("keyboard".to_string(), Value::Bool(self.keyboard));
321 out.insert("decode_gtin".to_string(), Value::Bool(self.decode_gtin));
322 out.insert("hotspot".to_string(), Value::Bool(self.hotspot));
323 out.insert(
324 "reconnection_time".to_string(),
325 Value::Number(Number::from(self.reconnection_time)),
326 );
327 out.insert("prefix".to_string(), Value::String(self.prefix.clone()));
328 out.insert(
329 "protected_inventory_active".to_string(),
330 Value::Bool(self.protected_inventory_active),
331 );
332 out.insert(
333 "protected_inventory_password".to_string(),
334 Value::String(self.protected_inventory_password.clone()),
335 );
336 out.insert("ant_dict".to_string(), ant_dict_to_value(&self.ant_dict));
337 out
338 }
339}
340
341pub fn ant_dict_to_value(ant_dict: &BTreeMap<String, AntennaConfig>) -> Value {
342 let mut outer = Map::new();
343 for (k, v) in ant_dict {
344 let mut ant = Map::new();
345 ant.insert("active".to_string(), Value::Bool(v.active));
346 ant.insert("power".to_string(), Value::Number(Number::from(v.power)));
347 ant.insert("rssi".to_string(), Value::Number(Number::from(v.rssi)));
348 outer.insert(k.clone(), Value::Object(ant));
349 }
350 Value::Object(outer)
351}
352
353pub fn parse_ant_dict(value: &Value) -> Result<BTreeMap<String, AntennaConfig>, String> {
354 let Some(obj) = value.as_object() else {
355 return Err("ant_dict must be an object".to_string());
356 };
357
358 let mut out = BTreeMap::new();
359 for (k, v) in obj {
360 let Some(ant) = v.as_object() else {
361 return Err(format!("ant_dict['{k}'] must be an object"));
362 };
363 let active = ant.get("active").and_then(Value::as_bool).unwrap_or(false);
364 let power = ant
365 .get("power")
366 .and_then(Value::as_i64)
367 .and_then(|v| i32::try_from(v).ok())
368 .unwrap_or(22);
369 let rssi = ant
370 .get("rssi")
371 .and_then(Value::as_i64)
372 .and_then(|v| i32::try_from(v).ok())
373 .unwrap_or(-120);
374 out.insert(
375 k.clone(),
376 AntennaConfig {
377 active,
378 power,
379 rssi,
380 },
381 );
382 }
383
384 if out.is_empty() {
385 return Err("ant_dict must not be empty".to_string());
386 }
387
388 Ok(out)
389}
390
391pub fn normalize_hex_prefix(value: &str) -> String {
392 let trimmed = value.trim().to_lowercase();
393 if trimmed.chars().all(|c| c.is_ascii_hexdigit()) {
394 trimmed
395 } else {
396 String::new()
397 }
398}
399
400fn get_string(params: &ParamMap, key: &str) -> Option<String> {
401 params.get(key).and_then(Value::as_str).map(str::to_string)
402}
403
404fn get_bool(params: &ParamMap, key: &str) -> Option<bool> {
405 params.get(key).and_then(Value::as_bool)
406}
407
408fn set_bool(params: &ParamMap, key: &str, target: &mut bool) {
409 if let Some(v) = get_bool(params, key) {
410 *target = v;
411 }
412}
413
414fn get_u8(params: &ParamMap, key: &str) -> Option<u8> {
415 params
416 .get(key)
417 .and_then(Value::as_u64)
418 .and_then(|v| u8::try_from(v).ok())
419}
420
421fn get_u16(params: &ParamMap, key: &str) -> Option<u16> {
422 params
423 .get(key)
424 .and_then(Value::as_u64)
425 .and_then(|v| u16::try_from(v).ok())
426}
427
428fn get_u32(params: &ParamMap, key: &str) -> Option<u32> {
429 params
430 .get(key)
431 .and_then(Value::as_u64)
432 .and_then(|v| u32::try_from(v).ok())
433}
434
435fn get_u64(params: &ParamMap, key: &str) -> Option<u64> {
436 params.get(key).and_then(Value::as_u64)
437}
438
439fn get_i32(params: &ParamMap, key: &str) -> Option<i32> {
440 params
441 .get(key)
442 .and_then(Value::as_i64)
443 .and_then(|v| i32::try_from(v).ok())
444}
445
446fn get_u8_vec(params: &ParamMap, key: &str) -> Option<Vec<u8>> {
447 let values = params.get(key)?.as_array()?;
448 Some(
449 values
450 .iter()
451 .filter_map(|v| v.as_u64().and_then(|n| u8::try_from(n).ok()))
452 .collect::<Vec<_>>(),
453 )
454}