Skip to main content

ghpascon_rust/devices/rfid/x714/
config.rs

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// ── Default configuration ─────────────────────────────────────────────────────
9//
10// All default values for X714Config live here.
11// Edit this JSON to change defaults used by X714Config::default() and
12// as the base for X714Config::from_map().
13//
14pub 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    /// Hard-coded base config used as the starting point for `from_map`.
181    /// These values are the fallback for any key absent from the caller's map.
182    /// Should match `DEFAULT_CONFIG_JSON` – edit both together.
183    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(); // use base() to avoid Default<->from_map recursion
220
221        if let Some(v) = get_string(&params, "name") {
222            config.name = v;
223        }
224        if let Some(v) = get_string(&params, "connection_type") {
225            config.connection_type = ConnectionType::from_str(&v);
226        }
227
228        if let Some(v) = get_string(&params, "port") {
229            config.serial.port = v;
230        }
231        if let Some(v) = get_u32(&params, "baudrate") {
232            config.serial.baudrate = v;
233        }
234        if let Some(v) = get_u16(&params, "vid") {
235            config.serial.vid = v;
236        }
237        if let Some(v) = get_u16(&params, "pid") {
238            config.serial.pid = v;
239        }
240
241        if let Some(v) = get_string(&params, "ip") {
242            config.tcp.ip = v;
243        }
244        if let Some(v) = get_u16(&params, "tcp_port") {
245            config.tcp.port = v;
246        }
247
248        if let Some(v) = get_string(&params, "ble_name") {
249            config.ble.name = v;
250        }
251        if let Some(v) = get_string(&params, "ble_address") {
252            config.ble.address = Some(v);
253        }
254        if let Some(v) = get_string(&params, "ble_service_uuid") {
255            config.ble.service_uuid = v;
256        }
257        if let Some(v) = get_string(&params, "ble_rx_uuid") {
258            config.ble.rx_uuid = v;
259        }
260        if let Some(v) = get_string(&params, "ble_tx_uuid") {
261            config.ble.tx_uuid = v;
262        }
263
264        set_bool(&params, "buzzer", &mut config.buzzer);
265        if let Some(v) = get_u8(&params, "session") {
266            config.session = v.clamp(0, 3);
267        }
268        set_bool(&params, "start_reading", &mut config.start_reading);
269        set_bool(&params, "gpi_start", &mut config.gpi_start);
270        set_bool(&params, "always_send", &mut config.always_send);
271        set_bool(&params, "simple_send", &mut config.simple_send);
272        set_bool(&params, "keyboard", &mut config.keyboard);
273        set_bool(&params, "decode_gtin", &mut config.decode_gtin);
274        set_bool(&params, "hotspot", &mut config.hotspot);
275
276        if let Some(v) = get_u64(&params, "reconnection_time") {
277            config.reconnection_time = v;
278        }
279        if let Some(v) = get_string(&params, "prefix") {
280            config.prefix = normalize_hex_prefix(&v);
281        }
282
283        set_bool(
284            &params,
285            "protected_inventory_active",
286            &mut config.protected_inventory_active,
287        );
288        if let Some(v) = get_string(&params, "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(&params, "active_ant").unwrap_or_else(|| vec![1]);
296            let read_power = get_i32(&params, "read_power").unwrap_or(22);
297            let read_rssi = get_i32(&params, "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}