smartx-rfid 0.4.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,159 @@
1
+ import asyncio
2
+ import logging
3
+ import sys
4
+ import threading
5
+ from typing import Optional
6
+
7
+ from bleak import BleakClient, BleakScanner
8
+
9
+ if sys.platform == "win32":
10
+ from bleak.backends.winrt.util import allow_sta
11
+ else:
12
+ allow_sta = None
13
+ from bleak.exc import BleakError
14
+
15
+ if allow_sta:
16
+ allow_sta()
17
+
18
+ # ---------------- Settings ----------------
19
+ SERVICE_UUID = "6E400001-B5A3-F393-E0A9-E50E24DCCA9E"
20
+ CHARACTERISTIC_RX = "6E400002-B5A3-F393-E0A9-E50E24DCCA9E" # Write (ESP32 receives)
21
+ CHARACTERISTIC_TX = "6E400003-B5A3-F393-E0A9-E50E24DCCA9E" # Notify (ESP32 sends)
22
+
23
+
24
+ class BLEProtocol:
25
+ def init_ble_vars(self):
26
+ self.client_ble: Optional[BleakClient] = None
27
+ self.client_ble_lock = asyncio.Lock()
28
+ self.connected_ble_event = asyncio.Event()
29
+ self.ble_stop = False
30
+ self.notify_enabled = False
31
+
32
+ # ---------------- Utilities ----------------
33
+ async def write_ble(self, data: bytes, verbose: bool = False) -> bool:
34
+ """Send data via BLE with connection check and lock."""
35
+ if not self.client_ble or not self.client_ble.is_connected:
36
+ logging.warning(f"{self.name} - ⚠️ BLE client not connected")
37
+ return False
38
+ async with self.client_ble_lock:
39
+ try:
40
+ await self.client_ble.write_gatt_char(CHARACTERISTIC_RX, data)
41
+ if verbose:
42
+ logging.info(f"{self.name} - [BLE TX] {data}")
43
+ return True
44
+ except Exception as e:
45
+ logging.warning(f"{self.name} - [BLE Write Error] {e}")
46
+ return False
47
+
48
+ async def scan_for_device(self) -> Optional[str]:
49
+ """Scan for devices whose name starts with the defined prefix."""
50
+ while not self.ble_stop:
51
+ logging.info(f"{self.name} - 🔍 Scanning BLE devices...")
52
+ try:
53
+ devices = await BleakScanner.discover(timeout=5.0)
54
+ for d in devices:
55
+ if d.name and d.name.startswith(self.ble_name):
56
+ logging.info(f"{self.name} - ✅ Device found: {d.address} ({d.name})")
57
+ return d.address
58
+ logging.warning(f"{self.name} - ❌ Device not found, retrying in 3s...")
59
+ except Exception as e:
60
+ logging.warning(f"{self.name} - [Scan Error] {e}")
61
+ await asyncio.sleep(self.reconnection_time)
62
+ return None
63
+
64
+ # ---------------- Main Connection ----------------
65
+ async def connect_and_run(self):
66
+ """Main BLE connection and operation loop."""
67
+ while not self.ble_stop:
68
+ try:
69
+ # Se já estava conectado antes, emite o evento de desconexão
70
+ if self.is_connected:
71
+ self.is_connected = False
72
+ self.on_event(self.name, "connection", False)
73
+
74
+ # Escolhe o endereço conforme o modo
75
+ if self.is_auto:
76
+ address = await self.scan_for_device()
77
+ if not address:
78
+ continue
79
+ else:
80
+ address = self.connection # Usa o MAC address fixo
81
+ logging.info(f"{self.name} - 🔗 Using fixed BLE address: {address}")
82
+
83
+ logging.info(f"{self.name} - Attempting to connect to {address}...")
84
+ client = BleakClient(address)
85
+
86
+ try:
87
+ await asyncio.wait_for(client.connect(), timeout=5.0)
88
+ except asyncio.TimeoutError:
89
+ logging.warning(f"{self.name} - ⏰ Connection attempt timed out")
90
+ await asyncio.sleep(self.reconnection_time)
91
+ continue
92
+
93
+ if not client.is_connected:
94
+ logging.warning(f"{self.name} - ❌ Failed to connect.")
95
+ await asyncio.sleep(self.reconnection_time)
96
+ continue
97
+
98
+ async with client:
99
+ logging.info(f"{self.name} - 🔗 Connected to device")
100
+ self.client_ble = client
101
+ self.connected_ble_event.set()
102
+
103
+ # Notification callback
104
+ def handle_notification(sender, data: bytearray):
105
+ decoded = data.decode(errors="ignore")
106
+ self.on_receive(decoded)
107
+
108
+ # Habilita notificações automaticamente
109
+ self.notify_enabled = False
110
+ for service in client.services:
111
+ for char in service.characteristics:
112
+ if "notify" in char.properties:
113
+ try:
114
+ await client.start_notify(char.uuid, handle_notification)
115
+ self.is_connected = True
116
+ self.on_event(self.name, "connection", True)
117
+ logging.info(f"{self.name} - ✅ BLE connection successfully established.")
118
+ self.config_reader()
119
+ self.notify_enabled = True
120
+ except Exception as e:
121
+ logging.warning(f"{self.name} - [Notify Error] {char.uuid}: {e}")
122
+ if not self.notify_enabled:
123
+ logging.warning(f"{self.name} - ⚠️ No characteristics with notify property found!")
124
+ # Loop principal de manutenção da conexão
125
+ last_ping = 0
126
+ while client.is_connected and not self.ble_stop:
127
+ now = asyncio.get_event_loop().time()
128
+ if now - last_ping >= 5:
129
+ await self.write_ble(b"#ping")
130
+ last_ping = now
131
+ await asyncio.sleep(1)
132
+
133
+ logging.info(f"{self.name} - 🔌 Disconnected from device.")
134
+
135
+ except BleakError as e:
136
+ logging.warning(f"{self.name} - [BLE Error] {e}")
137
+ await asyncio.sleep(self.reconnection_time)
138
+ except Exception as e:
139
+ logging.warning(f"{self.name} - [Unexpected BLE Error] {e}")
140
+ await asyncio.sleep(self.reconnection_time)
141
+ finally:
142
+ self.connected_ble_event.clear()
143
+ self.client_ble = None
144
+
145
+ # ---------------- Thread Wrapper ----------------
146
+ def connect_ble(self):
147
+ """Run BLE loop in a separate thread (ideal for FastAPI)."""
148
+
149
+ def run_loop():
150
+ loop = asyncio.new_event_loop()
151
+ asyncio.set_event_loop(loop)
152
+ loop.run_until_complete(self.connect_and_run())
153
+
154
+ threading.Thread(target=run_loop, daemon=True).start()
155
+
156
+ def stop(self):
157
+ """Request BLE loop stop."""
158
+ logging.info(f"{self.name} - 🛑 Stopping BLE loop...")
159
+ self.ble_stop = True
@@ -0,0 +1,54 @@
1
+ from smartx_rfid.schemas.tag import TagSchema
2
+ import logging
3
+
4
+
5
+ class OnReceive:
6
+ def on_receive(self, data, verbose: bool = False):
7
+ if not isinstance(data, str):
8
+ data = data.decode(errors="ignore")
9
+ data = data.replace("\r", "").replace("\n", "")
10
+ data = data.lower()
11
+ if verbose:
12
+ self.on_event(self.name, "receive", data)
13
+
14
+ if data.startswith("#read:"):
15
+ self.is_reading = data.endswith("on")
16
+ if self.is_reading:
17
+ self.on_start()
18
+ else:
19
+ self.on_stop()
20
+
21
+ elif data.startswith("#t+@"):
22
+ tag = data[4:]
23
+ epc, tid, ant, rssi = tag.split("|")
24
+ current_tag = {
25
+ "epc": epc,
26
+ "tid": tid,
27
+ "ant": int(ant),
28
+ "rssi": int(rssi) * (-1),
29
+ }
30
+ self.on_tag(current_tag)
31
+
32
+ elif len(data) == 24:
33
+ self.on_tag(data)
34
+
35
+ elif data.startswith("#set_cmd:"):
36
+ logging.info(f"{self.name} - CONFIG -> {data[data.index(':') + 1 :]}")
37
+
38
+ elif data == "#tags_cleared":
39
+ self.on_event(self.name, "tags_cleared", True)
40
+
41
+ def on_start(self):
42
+ self.clear_tags()
43
+ self.on_event(self.name, "reading", True)
44
+
45
+ def on_stop(self):
46
+ self.on_event(self.name, "reading", False)
47
+
48
+ def on_tag(self, tag: dict):
49
+ try:
50
+ tag_data = TagSchema(**tag)
51
+ tag = tag_data.model_dump()
52
+ self.on_event(self.name, "tag", tag)
53
+ except Exception as e:
54
+ logging.error(f"{self.name} - Invalid tag data: {e}")
@@ -0,0 +1,73 @@
1
+ import asyncio
2
+
3
+
4
+ class RfidCommands:
5
+ def start_inventory(self):
6
+ self.write("#READ:ON")
7
+
8
+ def stop_inventory(self):
9
+ self.write("#READ:OFF")
10
+
11
+ def clear_tags(self):
12
+ self.write("#CLEAR")
13
+
14
+ def config_reader(self):
15
+ set_cmd = "#set_cmd:"
16
+
17
+ # ANTENNAS
18
+ antennas = self.ant_dict
19
+ for antenna in antennas:
20
+ ant = antennas.get(antenna)
21
+ ant_cmd = f"|set_ant:{antenna},{ant.get('active')},{ant.get('power')},{abs(ant.get('rssi'))}"
22
+ set_cmd += ant_cmd
23
+
24
+ # SESSION
25
+ set_cmd += f"|SESSION:{self.session}"
26
+
27
+ # START_READING
28
+ set_cmd += f"|START_READING:{self.start_reading}"
29
+
30
+ # GPI_START
31
+ set_cmd += f"|GPI_START:{self.gpi_start}"
32
+
33
+ # IGNORE_READ
34
+ set_cmd += f"|IGNORE_READ:{self.ignore_read}"
35
+
36
+ # ALWAYS_SEND
37
+ set_cmd += f"|ALWAYS_SEND:{self.always_send}"
38
+
39
+ # SIMPLE_SEND
40
+ set_cmd += f"|SIMPLE_SEND:{self.simple_send}"
41
+
42
+ # KEYBOARD
43
+ set_cmd += f"|KEYBOARD:{self.keyboard}"
44
+
45
+ # BUZZER
46
+ set_cmd += f"|BUZZER:{self.buzzer}"
47
+
48
+ # DECODE_GTIN
49
+ set_cmd += f"|DECODE_GTIN:{self.decode_gtin}"
50
+
51
+ set_cmd = set_cmd.lower()
52
+ set_cmd = set_cmd.replace("true", "on").replace("false", "off")
53
+ self.write(set_cmd)
54
+
55
+ # OTHER CONFIG
56
+ self.write(f"#hotspot:{'on' if self.hotspot else 'off'}")
57
+ self.write(f"#prefix:{self.prefix}")
58
+ if self.protected_inventory_password is not None:
59
+ self.write(f"#protected_inventory:on;{self.protected_inventory_password}")
60
+ else:
61
+ self.write("#protected_inventory:off")
62
+
63
+ # Start Reading
64
+ if self.start_reading:
65
+ self.start_inventory()
66
+ else:
67
+ self.stop_inventory()
68
+
69
+ async def auto_clear(self):
70
+ while True:
71
+ await asyncio.sleep(30)
72
+ if self.is_connected:
73
+ self.clear_tags()
@@ -0,0 +1,89 @@
1
+ import asyncio
2
+ import logging
3
+
4
+ import serial.tools.list_ports
5
+ import serial_asyncio
6
+
7
+
8
+ class SerialProtocol(asyncio.Protocol):
9
+ def connection_made(self, transport):
10
+ self.transport = transport
11
+ self.is_connected = True
12
+ logging.info(f"{self.name} - ✅ Serial connection successfully established.")
13
+ self.on_connected()
14
+
15
+ def data_received(self, data):
16
+ self.rx_buffer += data
17
+
18
+ while b"\n" in self.rx_buffer:
19
+ idx = self.rx_buffer.index(b"\n")
20
+ packet = self.rx_buffer[:idx]
21
+ self.rx_buffer = self.rx_buffer[idx + 1 :]
22
+ self.on_receive(packet)
23
+
24
+ def connection_lost(self, exc):
25
+ logging.warning(f"{self.name} - ⚠️ Serial connection lost.")
26
+ self.transport = None
27
+ self.is_connected = False
28
+ self.on_event(self.name, "connection", False)
29
+
30
+ if self.on_con_lost:
31
+ self.on_con_lost.set()
32
+
33
+ def write_serial(self, to_send, verbose=True):
34
+ if self.transport:
35
+ if verbose:
36
+ logging.info(f"{self.name} - 📤 Sending: {to_send}")
37
+ if isinstance(to_send, str):
38
+ to_send += "\n"
39
+ to_send = to_send.encode() # convert string to bytes
40
+ self.transport.write(to_send)
41
+ else:
42
+ logging.warning(f"{self.name} - ❌ Send attempt failed: connection not established.")
43
+
44
+ async def connect_serial(self):
45
+ """Serial connection/reconnection loop"""
46
+ loop = asyncio.get_running_loop()
47
+
48
+ asyncio.create_task(self.auto_clear())
49
+
50
+ while True:
51
+ self.on_con_lost = asyncio.Event()
52
+
53
+ # If AUTO mode, try to detect port by VID/PID
54
+ if self.is_auto:
55
+ logging.info(f"{self.name} - 🔍 Auto-detecting port by VID={self.vid:04x} and PID={self.pid:04x}...")
56
+ ports = serial.tools.list_ports.comports()
57
+ found_port = None
58
+ for p in ports:
59
+ # p.vid and p.pid are integers (e.g. 0x0001 == 1 decimal)
60
+ if p.vid == self.vid and p.pid == self.pid:
61
+ found_port = p.device
62
+ logging.info(f"{self.name} - ✅ Detected port: {found_port}")
63
+ break
64
+
65
+ if found_port is None:
66
+ logging.info(f"{self.name} - ⚠️ No port with VID={self.vid} and PID={self.pid} found.")
67
+ logging.info(f"{self.name} - ⏳ Retrying in {self.reconnection_time} seconds...")
68
+ await asyncio.sleep(self.reconnection_time)
69
+ continue # try to detect again in next loop
70
+ else:
71
+ self.connection = found_port
72
+
73
+ try:
74
+ logging.info(f"{self.name} - 🔌 Trying to connect to {self.connection} at {self.baudrate} bps...")
75
+ await serial_asyncio.create_serial_connection(
76
+ loop, lambda: self, self.connection, baudrate=self.baudrate
77
+ )
78
+ logging.info(f"{self.name} - 🟢 Successfully connected.")
79
+ await self.on_con_lost.wait()
80
+ logging.info(f"{self.name} - 🔄 Connection lost. Attempting to reconnect...")
81
+ except Exception as e:
82
+ logging.warning(f"{self.name} - ❌ Connection error: {e}")
83
+
84
+ # If in AUTO mode, reset port to "AUTO" to force detection next loop
85
+ if self.is_auto:
86
+ self.connection = "AUTO"
87
+
88
+ logging.info(f"{self.name} - ⏳ Waiting {self.reconnection_time} seconds before retrying...")
89
+ await asyncio.sleep(self.reconnection_time)
@@ -0,0 +1,127 @@
1
+ import asyncio
2
+ import logging
3
+ import socket
4
+
5
+
6
+ class TCPHelpers:
7
+ async def monitor_connection(self):
8
+ while self.is_connected:
9
+ await asyncio.sleep(self.reconnection_time)
10
+ if (self.writer and self.writer.is_closing()) or (self.reader and self.reader.at_eof()):
11
+ self.is_connected = False
12
+ logging.info(f"{self.name} - [DISCONNECTED] Socket closed.")
13
+ break
14
+
15
+ await self.write_tcp("ping", verbose=False)
16
+
17
+ async def receive_data_tcp(self):
18
+ buffer = ""
19
+ try:
20
+ while True:
21
+ try:
22
+ data = await asyncio.wait_for(self.reader.read(1024), timeout=0.1)
23
+ except asyncio.TimeoutError:
24
+ # Timeout: process what's in the buffer as a command
25
+ if buffer:
26
+ self.on_receive(buffer.strip())
27
+ buffer = ""
28
+ continue
29
+
30
+ if not data:
31
+ raise ConnectionError("Connection lost")
32
+
33
+ buffer += data.decode(errors="ignore")
34
+
35
+ while "\n" in buffer:
36
+ line, buffer = buffer.split("\n", 1)
37
+ self.on_receive(line.strip())
38
+
39
+ except Exception as e:
40
+ if self.is_connected:
41
+ self.is_connected = False
42
+ logging.warning(f"[RECEIVE ERROR] {e}")
43
+
44
+
45
+ class TCPProtocol(TCPHelpers):
46
+ async def connect_tcp(self, ip, port):
47
+ while True:
48
+ try:
49
+ logging.info(f"Connecting: {self.name} - {ip}:{port}")
50
+
51
+ # Verifica IP antes (evita travar no DNS)
52
+ try:
53
+ resolved_ip = socket.gethostbyname(ip)
54
+ except OSError:
55
+ raise ValueError(f"Invalid IP address: {ip}")
56
+
57
+ # Tenta abrir conexão com timeout real
58
+ connect_task = asyncio.open_connection(resolved_ip, port)
59
+ self.reader, self.writer = await asyncio.wait_for(connect_task, timeout=3)
60
+
61
+ self.is_connected = True
62
+ self.on_connected()
63
+ logging.info(f"✅ [CONNECTED] {self.name} - {ip}:{port}")
64
+
65
+ # Cria tasks de leitura e monitoramento
66
+ tasks = [
67
+ asyncio.create_task(self.receive_data_tcp()),
68
+ asyncio.create_task(self.monitor_connection()),
69
+ asyncio.create_task(self.periodic_ping(10)),
70
+ ]
71
+
72
+ # Espera até que uma delas finalize
73
+ done, pending = await asyncio.wait(tasks, return_when=asyncio.FIRST_COMPLETED)
74
+
75
+ # Cancela o resto
76
+ for t in pending:
77
+ t.cancel()
78
+
79
+ self.is_connected = False
80
+ self.on_event(self.name, "connection", False)
81
+ logging.info(f"🔌 [DISCONNECTED] {self.name} - Reconnecting...")
82
+
83
+ except asyncio.TimeoutError:
84
+ logging.warning(f"⏱️ [TIMEOUT] {self.name} - No response from {ip}:{port}")
85
+ continue
86
+ except ValueError as e:
87
+ logging.warning(f"❌ [INVALID IP] {self.name}: {e}")
88
+ continue
89
+ except OSError as e:
90
+ logging.warning(f"💥 [NETWORK ERROR] {self.name}: {e}")
91
+ continue
92
+ except Exception as e:
93
+ logging.warning(f"❌ [UNEXPECTED ERROR] {self.name}: {e}")
94
+ continue
95
+
96
+ # Garante desconexão limpa
97
+ if self.writer:
98
+ try:
99
+ self.writer.close()
100
+ await self.writer.wait_closed()
101
+ except Exception:
102
+ pass
103
+ self.writer = None
104
+ self.reader = None
105
+ self.is_connected = False
106
+
107
+ logging.info(f"🔁 Retrying {self.name} in {self.reconnection_time}s...")
108
+ await asyncio.sleep(self.reconnection_time)
109
+
110
+ async def write_tcp(self, data: str, verbose: bool = True):
111
+ if self.is_connected and self.writer:
112
+ try:
113
+ data = data + "\n"
114
+ self.writer.write(data.encode())
115
+ await self.writer.drain()
116
+ if verbose:
117
+ logging.info(f"{self.name} - [SENT] {data.strip()}")
118
+ except Exception as e:
119
+ logging.warning(f"{self.name} - [SEND ERROR] {e}")
120
+ if self.is_connected:
121
+ self.is_connected = False
122
+ self.on_event(self.name, "connection", False)
123
+
124
+ async def periodic_ping(self, interval: int):
125
+ while self.is_connected:
126
+ await asyncio.sleep(interval)
127
+ await self.write_tcp("ping", verbose=False)
@@ -0,0 +1,25 @@
1
+ import logging
2
+ from smartx_rfid.schemas.tag import WriteTagValidator
3
+
4
+
5
+ class WriteCommands:
6
+ def write_epc(self, target_identifier: str | None, target_value: str | None, new_epc: str, password: str):
7
+ try:
8
+ validated_tag = WriteTagValidator(
9
+ target_identifier=target_identifier,
10
+ target_value=target_value,
11
+ new_epc=new_epc,
12
+ password=password,
13
+ )
14
+ except Exception as e:
15
+ logging.warning(f"{self.name} - {e}")
16
+ return
17
+ identifier = validated_tag.target_identifier
18
+ value = validated_tag.target_value
19
+ epc = validated_tag.new_epc
20
+ password = validated_tag.password
21
+ logging.info(f"{self.name} - Writing EPC: {epc} (Current: {identifier}={value})")
22
+ if identifier is None:
23
+ self.write(f"#WRITE:{epc};{password}", False)
24
+ else:
25
+ self.write(f"#WRITE:{epc};{password};{identifier};{value}", False)
@@ -0,0 +1,8 @@
1
+ # GENERIC
2
+ from .generic.SERIAL._main import SERIAL
3
+ from .generic.TCP._main import TCP
4
+
5
+ # RFID DEVICES
6
+ from .RFID.X714._main import X714
7
+ from .RFID.R700_IOT._main import R700_IOT
8
+ from .RFID.R700_IOT.reader_config_example import R700_IOT_config_example