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