smartx-rfid 0.4.0__py3-none-any.whl → 1.1.6__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.
@@ -13,6 +13,8 @@ from .reader_config_example import R700_IOT_config_example
13
13
 
14
14
 
15
15
  class R700_IOT(OnEvent, ReaderHelpers, WriteCommands):
16
+ """Impinj R700 RFID reader using HTTP REST API."""
17
+
16
18
  def __init__(
17
19
  self,
18
20
  # READER CONFIG
@@ -26,6 +28,18 @@ class R700_IOT(OnEvent, ReaderHelpers, WriteCommands):
26
28
  # Firmware Version
27
29
  firmware_version: str = "8.4.1",
28
30
  ):
31
+ """
32
+ Create R700 RFID reader.
33
+
34
+ Args:
35
+ reading_config: Configuration for tag reading
36
+ name: Device name
37
+ ip: IP address of reader
38
+ username: Login username
39
+ password: Login password
40
+ start_reading: Start reading tags automatically
41
+ firmware_version: Expected firmware version
42
+ """
29
43
  self.name = name
30
44
  self.device_type = "rfid"
31
45
 
@@ -62,6 +76,7 @@ class R700_IOT(OnEvent, ReaderHelpers, WriteCommands):
62
76
  self.on_event = on_event
63
77
 
64
78
  async def disconnect(self):
79
+ """Safely disconnect from reader and stop reading."""
65
80
  """Desconecta o reader de forma segura."""
66
81
  logging.info(f"{self.name} 🔌 Disconnecting reader")
67
82
  self._stop_connection = True
@@ -79,6 +94,7 @@ class R700_IOT(OnEvent, ReaderHelpers, WriteCommands):
79
94
  self.on_event(self.name, "connection", False)
80
95
 
81
96
  async def connect(self):
97
+ """Connect to R700 reader and start tag reading."""
82
98
  self._stop_connection = False
83
99
  while not self._stop_connection:
84
100
  async with httpx.AsyncClient(auth=self.auth, verify=False, timeout=10.0) as session:
@@ -130,6 +146,7 @@ class R700_IOT(OnEvent, ReaderHelpers, WriteCommands):
130
146
  await self.get_tag_list(session)
131
147
 
132
148
  async def clear_tags(self):
149
+ """Clear all stored tags from memory."""
133
150
  self.tags = {}
134
151
 
135
152
  async def write_gpo(
@@ -141,6 +158,15 @@ class R700_IOT(OnEvent, ReaderHelpers, WriteCommands):
141
158
  *args,
142
159
  **kwargs,
143
160
  ):
161
+ """
162
+ Control GPO (output) pins on reader.
163
+
164
+ Args:
165
+ pin: GPO pin number
166
+ state: Turn pin on (True) or off (False)
167
+ control: Control type (static or pulse)
168
+ time: Pulse duration in milliseconds
169
+ """
144
170
  gpo_command = await self.get_gpo_command(pin=pin, state=state, control=control, time=time)
145
171
  try:
146
172
  async with httpx.AsyncClient(auth=self.auth, verify=False, timeout=10.0) as session:
@@ -149,6 +175,15 @@ class R700_IOT(OnEvent, ReaderHelpers, WriteCommands):
149
175
  logging.warning(f"{self.name} - Failed to set GPO: {e}")
150
176
 
151
177
  def write_epc(self, target_identifier: str | None, target_value: str | None, new_epc: str, password: str):
178
+ """
179
+ Write new EPC code to RFID tag.
180
+
181
+ Args:
182
+ target_identifier: How to find tag (epc, tid, user)
183
+ target_value: Current tag value to match
184
+ new_epc: New EPC code to write
185
+ password: Tag access password
186
+ """
152
187
  """
153
188
  Writes a new EPC (Electronic Product Code) to RFID tags.
154
189
  """
@@ -1,13 +1,22 @@
1
1
  class OnEvent:
2
+ """Handle R700 reader events."""
3
+
2
4
  async def on_start(self):
5
+ """Called when reader starts reading tags."""
3
6
  self.is_reading = True
4
7
  self.on_event(self.name, "reading", True)
5
8
 
6
9
  async def on_stop(self):
10
+ """Called when reader stops reading tags."""
7
11
  self.is_reading = False
8
12
  self.on_event(self.name, "reading", False)
9
13
 
10
14
  async def on_tag(self, tag):
15
+ """Process detected RFID tag data.
16
+
17
+ Args:
18
+ tag: Raw tag data from reader API
19
+ """
11
20
  current_tag = {
12
21
  "epc": tag.get("epcHex"),
13
22
  "tid": tag.get("tidHex"),
@@ -6,7 +6,17 @@ import httpx
6
6
 
7
7
 
8
8
  class ReaderHelpers:
9
+ """Helper methods for R700 reader management."""
10
+
9
11
  async def check_firmware_version(self, session: httpx.AsyncClient | None = None):
12
+ """Check if reader firmware version is compatible.
13
+
14
+ Args:
15
+ session: HTTP session to use
16
+
17
+ Returns:
18
+ bool: True if firmware is compatible
19
+ """
10
20
  endpoint = self.check_version_endpoint
11
21
 
12
22
  try:
@@ -4,7 +4,17 @@ import httpx
4
4
 
5
5
 
6
6
  class WriteCommands:
7
+ """RFID tag write commands for R700 reader."""
8
+
7
9
  async def get_write_cmd(self, tag):
10
+ """Generate write command for tag programming.
11
+
12
+ Args:
13
+ tag: Tag data with target and new EPC
14
+
15
+ Returns:
16
+ dict: Write command for R700 API
17
+ """
8
18
  identifier = tag.target_identifier
9
19
  target = tag.target_value
10
20
  epc = tag.new_epc
@@ -21,6 +21,8 @@ ant_default_config = {
21
21
 
22
22
 
23
23
  class X714(SerialProtocol, OnReceive, RfidCommands, BLEProtocol, WriteCommands, TCPProtocol):
24
+ """RFID reader that supports SERIAL, TCP and BLE connections."""
25
+
24
26
  def __init__(
25
27
  self,
26
28
  name: str = "X714",
@@ -56,6 +58,37 @@ class X714(SerialProtocol, OnReceive, RfidCommands, BLEProtocol, WriteCommands,
56
58
  read_power: int = 22,
57
59
  read_rssi: int = -120,
58
60
  ):
61
+ """
62
+ Create X714 RFID reader.
63
+
64
+ Args:
65
+ name: Device name
66
+ connection_type: How to connect (SERIAL, TCP, BLE)
67
+ port: Serial port or AUTO to detect
68
+ baudrate: Serial speed
69
+ vid: USB vendor ID for auto-detect
70
+ pid: USB product ID for auto-detect
71
+ ip: IP address for TCP connection
72
+ tcp_port: TCP port number
73
+ ble_name: Bluetooth device name
74
+ buzzer: Make sound when reading tags
75
+ session: EPC session number (0-3)
76
+ start_reading: Start reading tags automatically
77
+ gpi_start: Use GPI input to start reading
78
+ ignore_read: Skip duplicate tags
79
+ always_send: Send all tag events
80
+ simple_send: Use simple tag format
81
+ keyboard: Act like keyboard input
82
+ decode_gtin: Decode GTIN barcodes
83
+ hotspot: Enable hotspot mode
84
+ reconnection_time: Seconds to wait before reconnect
85
+ prefix: Text to add before tag data
86
+ protected_inventory_password: Password for protected reading
87
+ ant_dict: Custom antenna settings
88
+ active_ant: Which antennas to use
89
+ read_power: TX power in dBm
90
+ read_rssi: RSSI threshold in dBm
91
+ """
59
92
  # Name
60
93
  self.name = name
61
94
  self.device_type = "rfid"
@@ -125,6 +158,12 @@ class X714(SerialProtocol, OnReceive, RfidCommands, BLEProtocol, WriteCommands,
125
158
  self.on_event: Callable = on_event
126
159
 
127
160
  def write(self, to_send, verbose=True):
161
+ """Send data to reader using current connection type.
162
+
163
+ Args:
164
+ to_send: Data to send
165
+ verbose: Show sent data in logs
166
+ """
128
167
  if self.connection_type == "SERIAL":
129
168
  self.write_serial(to_send, verbose)
130
169
  elif self.connection_type == "BLE":
@@ -133,6 +172,7 @@ class X714(SerialProtocol, OnReceive, RfidCommands, BLEProtocol, WriteCommands,
133
172
  asyncio.create_task(self.write_tcp(to_send, verbose))
134
173
 
135
174
  async def connect(self):
175
+ """Connect to reader using configured connection type."""
136
176
  if self.connection_type == "SERIAL":
137
177
  await self.connect_serial()
138
178
  elif self.connection_type == "BLE":
@@ -141,6 +181,6 @@ class X714(SerialProtocol, OnReceive, RfidCommands, BLEProtocol, WriteCommands,
141
181
  await self.connect_tcp(self.ip, self.tcp_port)
142
182
 
143
183
  def on_connected(self):
144
- """Callback chamado quando a conexão é estabelecida."""
184
+ """Called when connection is established. Sets up reader."""
145
185
  self.config_reader()
146
186
  self.on_event(self.name, "connected", True)
@@ -3,7 +3,15 @@ import logging
3
3
 
4
4
 
5
5
  class OnReceive:
6
+ """Handle incoming data from X714 reader."""
7
+
6
8
  def on_receive(self, data, verbose: bool = False):
9
+ """Process data received from reader.
10
+
11
+ Args:
12
+ data: Raw data from reader
13
+ verbose: Show received data in logs
14
+ """
7
15
  if not isinstance(data, str):
8
16
  data = data.decode(errors="ignore")
9
17
  data = data.replace("\r", "").replace("\n", "")
@@ -39,13 +47,25 @@ class OnReceive:
39
47
  self.on_event(self.name, "tags_cleared", True)
40
48
 
41
49
  def on_start(self):
50
+ """Called when reader starts reading tags."""
42
51
  self.clear_tags()
43
52
  self.on_event(self.name, "reading", True)
44
53
 
45
54
  def on_stop(self):
55
+ """Called when reader stops reading tags."""
46
56
  self.on_event(self.name, "reading", False)
47
57
 
48
58
  def on_tag(self, tag: dict):
59
+ """Process detected RFID tag data.
60
+
61
+ Args:
62
+ tag: Tag information dictionary
63
+ """
64
+ """Process detected RFID tag data.
65
+
66
+ Args:
67
+ tag: Tag information dictionary
68
+ """
49
69
  try:
50
70
  tag_data = TagSchema(**tag)
51
71
  tag = tag_data.model_dump()
@@ -2,16 +2,22 @@ import asyncio
2
2
 
3
3
 
4
4
  class RfidCommands:
5
+ """RFID reader control commands for X714."""
6
+
5
7
  def start_inventory(self):
8
+ """Start reading RFID tags."""
6
9
  self.write("#READ:ON")
7
10
 
8
11
  def stop_inventory(self):
12
+ """Stop reading RFID tags."""
9
13
  self.write("#READ:OFF")
10
14
 
11
15
  def clear_tags(self):
16
+ """Clear all stored tags from memory."""
12
17
  self.write("#CLEAR")
13
18
 
14
19
  def config_reader(self):
20
+ """Configure reader settings like antennas, session, etc."""
15
21
  set_cmd = "#set_cmd:"
16
22
 
17
23
  # ANTENNAS
@@ -26,6 +32,8 @@ class RfidCommands:
26
32
 
27
33
  # START_READING
28
34
  set_cmd += f"|START_READING:{self.start_reading}"
35
+ if self.start_reading:
36
+ self.is_reading = True
29
37
 
30
38
  # GPI_START
31
39
  set_cmd += f"|GPI_START:{self.gpi_start}"
@@ -3,7 +3,17 @@ from smartx_rfid.schemas.tag import WriteTagValidator
3
3
 
4
4
 
5
5
  class WriteCommands:
6
+ """RFID tag write commands for X714."""
7
+
6
8
  def write_epc(self, target_identifier: str | None, target_value: str | None, new_epc: str, password: str):
9
+ """Write new EPC code to RFID tag.
10
+
11
+ Args:
12
+ target_identifier: How to find tag (epc, tid, user)
13
+ target_value: Current tag value to match
14
+ new_epc: New EPC code to write
15
+ password: Tag access password
16
+ """
7
17
  try:
8
18
  validated_tag = WriteTagValidator(
9
19
  target_identifier=target_identifier,
@@ -137,13 +137,11 @@ class SERIAL(asyncio.Protocol):
137
137
 
138
138
  def write(self, to_send, verbose=True):
139
139
  """
140
- Send data through the serial connection.
141
-
142
- Automatically adds newline to strings and calculates CRC16 for bytes.
140
+ Send data through serial port.
143
141
 
144
142
  Args:
145
- to_send: Data to send (str or bytes)
146
- verbose: Enable logging of sent data
143
+ to_send: Data to send (string or bytes)
144
+ verbose: Show sent data in logs
147
145
  """
148
146
  if self.transport:
149
147
  if isinstance(to_send, str):
@@ -167,6 +165,7 @@ class SERIAL(asyncio.Protocol):
167
165
  logging.warning("❌ Send attempt failed: connection not established.")
168
166
 
169
167
  async def connect(self):
168
+ """Connect to serial port and keep connection alive."""
170
169
  """
171
170
  Establish and maintain serial connection with automatic reconnection.
172
171
 
@@ -216,6 +215,16 @@ class SERIAL(asyncio.Protocol):
216
215
  await asyncio.sleep(3)
217
216
 
218
217
  def crc16(self, data: bytes, poly=0x8408):
218
+ """
219
+ Calculate CRC16 checksum for data validation.
220
+
221
+ Args:
222
+ data: Input bytes to calculate checksum
223
+ poly: CRC polynomial value
224
+
225
+ Returns:
226
+ int: 16-bit checksum value
227
+ """
219
228
  """
220
229
  Calculate CRC-16/CCITT-FALSE checksum.
221
230
 
@@ -7,12 +7,22 @@ from typing import Callable
7
7
 
8
8
 
9
9
  class TCP(Helpers):
10
+ """TCP connection handler for network communication."""
11
+
10
12
  def __init__(
11
13
  self,
12
14
  name: str = "GENERIC_TCP",
13
15
  ip: str = "192.168.1.101",
14
16
  port: int = 23,
15
17
  ):
18
+ """
19
+ Create TCP connection.
20
+
21
+ Args:
22
+ name: Device name
23
+ ip: IP address to connect
24
+ port: TCP port number
25
+ """
16
26
  self.name = name
17
27
  self.device_type = "generic"
18
28
 
@@ -26,6 +36,7 @@ class TCP(Helpers):
26
36
  self.on_event: Callable = on_event
27
37
 
28
38
  async def connect(self):
39
+ """Connect to TCP server and keep connection alive."""
29
40
  while True:
30
41
  try:
31
42
  logging.info(f"Connecting: {self.name} - {self.ip}:{self.port}")
@@ -59,6 +70,13 @@ class TCP(Helpers):
59
70
  await asyncio.sleep(3)
60
71
 
61
72
  async def write(self, data: str, verbose=True):
73
+ """
74
+ Send data through TCP connection.
75
+
76
+ Args:
77
+ data: Text to send
78
+ verbose: Show sent data in logs
79
+ """
62
80
  if self.is_connected and self.writer:
63
81
  try:
64
82
  data = data + "\n"
@@ -3,7 +3,10 @@ import logging
3
3
 
4
4
 
5
5
  class Helpers:
6
+ """Helper functions for TCP connection management."""
7
+
6
8
  async def monitor_connection(self):
9
+ """Check if TCP connection is still alive."""
7
10
  while self.is_connected:
8
11
  await asyncio.sleep(3)
9
12
  if (self.writer and self.writer.is_closing()) or (self.reader and self.reader.at_eof()):
@@ -14,6 +17,7 @@ class Helpers:
14
17
  await self.write("ping", verbose=False)
15
18
 
16
19
  async def receive_data(self):
20
+ """Receive and process incoming TCP data."""
17
21
  buffer = ""
18
22
  try:
19
23
  while True:
@@ -0,0 +1,6 @@
1
+ from pydantic import BaseModel, Field
2
+
3
+
4
+ class EventSchema(BaseModel):
5
+ event_type: str = Field("event", description="Type of the event")
6
+ event_data = Field(None, description="Associated data with the event")
@@ -5,9 +5,9 @@ from pydantic import BaseModel, Field, field_validator
5
5
 
6
6
  class TagSchema(BaseModel):
7
7
  epc: str = Field("000000000000000000000001")
8
- tid: Optional[str | None] = Field("E28000000000000000000001")
9
- ant: Optional[int | None] = 0
10
- rssi: Optional[int | None] = 0
8
+ tid: Optional[str | None] = Field(None)
9
+ ant: Optional[int | None] = None
10
+ rssi: Optional[int | None] = None
11
11
 
12
12
  @field_validator("epc", "tid")
13
13
  def validate_epc_length_and_hex(cls, v, field):
@@ -0,0 +1,2 @@
1
+ from .tag_list import TagList
2
+ from .logger_manager import LoggerManager
@@ -0,0 +1,167 @@
1
+ import logging
2
+ import os
3
+ import queue
4
+ import threading
5
+ import sys
6
+ import asyncio
7
+ from datetime import datetime, timezone
8
+
9
+
10
+ class LoggerManager:
11
+ """
12
+ Simple plug-and-play daily rotating logger.
13
+
14
+ Usage:
15
+ logger_manager = LoggerManager("logs", "myapp", storage_days=7)
16
+ logging.info("App started")
17
+ # Exceptions, even uncaught, are automatically logged
18
+ """
19
+
20
+ def __init__(self, log_path: str, base_filename: str, storage_days: int = 7, use_utc: bool = False):
21
+ self.log_path = os.path.abspath(log_path)
22
+ self.base_filename = base_filename
23
+ self.storage_days = storage_days
24
+ self.use_utc = use_utc
25
+
26
+ os.makedirs(self.log_path, exist_ok=True)
27
+
28
+ # Queue and thread for async logging
29
+ self.log_queue: queue.Queue[str] = queue.Queue(maxsize=10_000)
30
+ self.stop_event = threading.Event()
31
+ self.current_date = None
32
+ self.filename = self._get_filename_for_date(self._now().date())
33
+
34
+ self.worker_thread = threading.Thread(target=self._worker, name="LogWriterThread", daemon=True)
35
+ self.worker_thread.start()
36
+
37
+ # Setup logging
38
+ self._setup_logging()
39
+
40
+ # Global exception handlers
41
+ sys.excepthook = self._handle_exception
42
+ try:
43
+ loop = asyncio.get_event_loop()
44
+ loop.set_exception_handler(self._asyncio_exception_handler)
45
+ except RuntimeError:
46
+ pass
47
+
48
+ logging.info(f"Logger initialized: {self.filename}")
49
+
50
+ # -------------------
51
+ # Date & filename
52
+ # -------------------
53
+ def _now(self):
54
+ return datetime.now(timezone.utc) if self.use_utc else datetime.now()
55
+
56
+ def _get_filename_for_date(self, date: datetime.date):
57
+ # Date before filename: YYYY-MM-DD_myapp.log
58
+ return os.path.join(self.log_path, f"{date:%Y-%m-%d}_{self.base_filename}.log")
59
+
60
+ # -------------------
61
+ # Worker
62
+ # -------------------
63
+ def _worker(self):
64
+ while not self.stop_event.is_set() or not self.log_queue.empty():
65
+ try:
66
+ msg = self.log_queue.get(timeout=0.5)
67
+ self._write(msg)
68
+ except queue.Empty:
69
+ continue
70
+
71
+ def _write(self, msg: str):
72
+ today = self._now().date()
73
+ if today != self.current_date:
74
+ self.current_date = today
75
+ self.filename = self._get_filename_for_date(today)
76
+ self._cleanup_old_logs()
77
+ with open(self.filename, "a", encoding="utf-8", errors="replace") as f:
78
+ f.write(msg)
79
+
80
+ # -------------------
81
+ # Cleanup old logs
82
+ # -------------------
83
+ def _cleanup_old_logs(self):
84
+ files = []
85
+ for f in os.listdir(self.log_path):
86
+ if f.endswith(".log") and f.startswith(f"{self.current_date:%Y-%m-%d}"):
87
+ # skip current file
88
+ continue
89
+ if f.startswith("_".join([self.base_filename, ""])): # ignore wrongly named files
90
+ continue
91
+ if f.endswith(".log"):
92
+ try:
93
+ date_str = f.split("_")[0]
94
+ file_date = datetime.strptime(date_str, "%Y-%m-%d").date()
95
+ files.append((file_date, os.path.join(self.log_path, f)))
96
+ except Exception:
97
+ continue
98
+ files.sort(key=lambda x: x[0])
99
+ for _, old_file in files[: -self.storage_days]:
100
+ try:
101
+ os.remove(old_file)
102
+ except Exception:
103
+ pass
104
+
105
+ # -------------------
106
+ # Logging setup
107
+ # -------------------
108
+ def _setup_logging(self):
109
+ logger = logging.getLogger()
110
+ logger.setLevel(logging.INFO)
111
+
112
+ # Remove old handlers
113
+ for handler in logger.handlers[:]:
114
+ logger.removeHandler(handler)
115
+
116
+ # Console
117
+ ch = logging.StreamHandler()
118
+ ch.setFormatter(logging.Formatter("%(asctime)s - %(levelname)s - %(message)s"))
119
+ logger.addHandler(ch)
120
+
121
+ # File (async)
122
+ fh = logging.Handler()
123
+ fh.setFormatter(logging.Formatter("%(asctime)s - %(levelname)s - %(message)s"))
124
+ fh.emit = self._enqueue
125
+ logger.addHandler(fh)
126
+
127
+ # -------------------
128
+ # Enqueue log messages
129
+ # -------------------
130
+ def _enqueue(self, record: logging.LogRecord):
131
+ try:
132
+ # Include exception info if present
133
+ msg = logging.Formatter("%(asctime)s - %(levelname)s - %(message)s").format(record)
134
+ if record.exc_info:
135
+ msg += "\n" + logging.Formatter().formatException(record.exc_info)
136
+ msg += "\n"
137
+ self.log_queue.put_nowait(msg)
138
+ except queue.Full:
139
+ pass
140
+ except Exception:
141
+ self.handleError(record)
142
+
143
+ # -------------------
144
+ # Exception hooks
145
+ # -------------------
146
+ @staticmethod
147
+ def _handle_exception(exc_type, exc_value, exc_traceback):
148
+ if issubclass(exc_type, KeyboardInterrupt):
149
+ sys.__excepthook__(exc_type, exc_value, exc_traceback)
150
+ return
151
+ logging.getLogger().error("Uncaught exception", exc_info=(exc_type, exc_value, exc_traceback))
152
+
153
+ @staticmethod
154
+ def _asyncio_exception_handler(loop, context):
155
+ exception = context.get("exception")
156
+ logger = logging.getLogger()
157
+ if exception:
158
+ logger.error("Unhandled asyncio exception", exc_info=exception)
159
+ else:
160
+ logger.error("Unhandled asyncio error: %s", context.get("message"))
161
+
162
+ # -------------------
163
+ # Close
164
+ # -------------------
165
+ def close(self):
166
+ self.stop_event.set()
167
+ self.worker_thread.join(timeout=3)
@@ -0,0 +1,117 @@
1
+ import importlib
2
+ import logging
3
+ import sys
4
+ from pathlib import Path
5
+
6
+
7
+ def get_frozen_path(relative_path: str) -> Path:
8
+ """
9
+ Return the absolute path to a file or directory, taking into account
10
+ whether the application is running from source or as a frozen executable.
11
+
12
+ Args:
13
+ relative_path: Relative path to the file or directory.
14
+
15
+ Returns:
16
+ The resolved absolute path.
17
+ """
18
+ if getattr(sys, "frozen", False):
19
+ base_path = Path(sys._MEIPASS)
20
+ else:
21
+ base_path = Path(sys.argv[0]).resolve().parent
22
+
23
+ return base_path / relative_path
24
+
25
+
26
+ def get_prefix_from_path(current_file: str, base_dir: str = "routers") -> str:
27
+ """
28
+ Automatically generate an API router prefix based on the folder structure
29
+ starting from the given base directory.
30
+
31
+ Args:
32
+ current_file: Usually __file__.
33
+ base_dir: Root directory name for routers.
34
+
35
+ Returns:
36
+ The router prefix (e.g. "/rfid/get").
37
+ """
38
+ path = Path(current_file).resolve()
39
+ parts = path.parts
40
+
41
+ if base_dir not in parts:
42
+ raise ValueError(f"'{base_dir}' not found in path: {path}")
43
+
44
+ base_index = parts.index(base_dir)
45
+ prefix_parts = parts[base_index + 1 :]
46
+ prefix_string = "/" + "/".join(prefix_parts)
47
+
48
+ return prefix_string.replace(".py", "")
49
+
50
+
51
+ def include_all_routers(current_path: str, app) -> None:
52
+ """
53
+ Recursively discover and include all API routers found in the given path.
54
+ """
55
+ routes_path = get_frozen_path(current_path)
56
+
57
+ for entry in Path(routes_path).iterdir():
58
+ if entry.is_dir() and entry.name != "__pycache__":
59
+ include_all_routers(str(Path(current_path) / entry.name), app)
60
+
61
+ elif entry.is_file() and entry.suffix == ".py" and entry.name != "__init__.py":
62
+ module_name = entry.stem
63
+ file_path = entry
64
+
65
+ spec = importlib.util.spec_from_file_location(f"app.routers.{module_name}", str(file_path))
66
+ module = importlib.util.module_from_spec(spec)
67
+
68
+ try:
69
+ spec.loader.exec_module(module)
70
+
71
+ if hasattr(module, "router"):
72
+ prefix = getattr(module.router, "prefix", "") or ""
73
+ app.include_router(
74
+ module.router,
75
+ include_in_schema=prefix.startswith("/api"),
76
+ )
77
+
78
+ try:
79
+ routers_dir = Path(routes_path).resolve()
80
+ relative_path = file_path.resolve().relative_to(routers_dir.parent)
81
+ except Exception:
82
+ relative_path = file_path.name
83
+
84
+ logging.info(f"✅ Route loaded: {relative_path}")
85
+
86
+ else:
87
+ logging.warning(f"⚠️ File {current_path} does not define a 'router'")
88
+
89
+ except Exception as e:
90
+ logging.error(f"❌ Error loading {current_path}: {e}", exc_info=True)
91
+
92
+
93
+ def load_file(file_path: str | Path) -> str:
94
+ """
95
+ Load the content of a file as a string.
96
+
97
+ Args:
98
+ file_path (str | Path): Path to the file.
99
+
100
+ Returns:
101
+ str: The content of the file, or a default message if loading fails.
102
+ """
103
+ file_path = Path(file_path)
104
+
105
+ if not file_path.exists():
106
+ logging.warning(f"File not found: {file_path}. Using default content.")
107
+ return "File not found."
108
+
109
+ if not file_path.is_file():
110
+ logging.warning(f"Path is not a file: {file_path}. Using default content.")
111
+ return "Invalid file path."
112
+
113
+ try:
114
+ return file_path.read_text(encoding="utf-8")
115
+ except Exception as e:
116
+ logging.error(f"Error reading file {file_path}: {e}", exc_info=True)
117
+ return "Error loading file content."
@@ -0,0 +1,227 @@
1
+ from typing import Literal, Dict, Any, Optional, Tuple
2
+ from datetime import datetime
3
+ from threading import Lock
4
+ import logging
5
+ from pyepc import SGTIN
6
+
7
+
8
+ class TagList:
9
+ """
10
+ Thread-safe container for RFID tags.
11
+
12
+ Tags are stored as dictionaries to allow flexible schemas per client.
13
+ Each tag is uniquely identified by either EPC or TID.
14
+ """
15
+
16
+ def __init__(self, unique_identifier: Literal["epc", "tid"] = "epc"):
17
+ """
18
+ Initialize the tag list.
19
+
20
+ Args:
21
+ unique_identifier: Field used as the unique tag identifier ("epc" or "tid").
22
+ """
23
+ if unique_identifier not in ("epc", "tid"):
24
+ raise ValueError("unique_identifier must be 'epc' or 'tid'")
25
+
26
+ self.unique_identifier = unique_identifier
27
+ self._tags: Dict[str, Dict[str, Any]] = {}
28
+ self._lock = Lock()
29
+
30
+ def __len__(self) -> int:
31
+ """
32
+ Return the number of stored tags.
33
+ """
34
+ return len(self._tags)
35
+
36
+ def __contains__(self, identifier: str) -> bool:
37
+ """
38
+ Check if a tag identifier exists in the list.
39
+ """
40
+ return identifier in self._tags
41
+
42
+ def __repr__(self) -> str:
43
+ """
44
+ Return a string representation of the stored tags.
45
+ """
46
+ return repr(self.get_all())
47
+
48
+ def add(self, tag: Dict[str, Any], device: str = "Unknown") -> Tuple[bool, Optional[Dict[str, Any]]]:
49
+ """
50
+ Add or update a tag.
51
+
52
+ Returns:
53
+ (True, tag_dict) if the tag is new;
54
+ (False, tag_dict) if the tag already exists;
55
+ (False, None) if an error occurs;
56
+ """
57
+ try:
58
+ identifier_value = tag.get(self.unique_identifier)
59
+ if not identifier_value:
60
+ raise ValueError(f"Tag missing '{self.unique_identifier}'")
61
+
62
+ with self._lock:
63
+ if identifier_value not in self._tags:
64
+ stored = self._new_tag(tag, device)
65
+ return True, stored
66
+ else:
67
+ stored = self._existing_tag(tag)
68
+ return False, stored
69
+
70
+ except Exception as e:
71
+ logging.warning(f"[ TAG ERROR ] {e}")
72
+ return False, None
73
+
74
+ def _new_tag(self, tag: Dict[str, Any], device: str) -> Dict[str, Any]:
75
+ """
76
+ Create and store a new tag.
77
+
78
+ Args:
79
+ tag: Raw tag data.
80
+ device: Source device identifier.
81
+
82
+ Returns:
83
+ The stored tag dictionary.
84
+ """
85
+ now = datetime.now()
86
+
87
+ try:
88
+ gtin = SGTIN.decode(tag.get("epc")).gtin
89
+ except Exception:
90
+ gtin = None
91
+
92
+ stored_tag = {
93
+ **tag,
94
+ "device": device,
95
+ "timestamp": now,
96
+ "count": 1,
97
+ "gtin": gtin,
98
+ }
99
+
100
+ self._tags[tag[self.unique_identifier]] = stored_tag
101
+
102
+ return stored_tag
103
+
104
+ def _existing_tag(self, tag: Dict[str, Any]) -> Dict[str, Any]:
105
+ """
106
+ Update an existing tag.
107
+
108
+ Args:
109
+ tag: Incoming tag data.
110
+
111
+ Returns:
112
+ The updated stored tag.
113
+ """
114
+ current = self._tags[tag[self.unique_identifier]]
115
+
116
+ current["count"] += 1
117
+ current["timestamp"] = datetime.now()
118
+
119
+ new_rssi = tag.get("rssi")
120
+ if new_rssi is not None:
121
+ old_rssi = current.get("rssi")
122
+ if old_rssi is None or abs(new_rssi) < abs(old_rssi):
123
+ current["rssi"] = new_rssi
124
+ current["ant"] = tag.get("ant")
125
+
126
+ return current
127
+
128
+ def get_all(self) -> list[Dict[str, Any]]:
129
+ """
130
+ Retrieve all stored tags.
131
+
132
+ Returns:
133
+ A list of tag dictionaries.
134
+ """
135
+ with self._lock:
136
+ return list(self._tags.values())
137
+
138
+ def get_by_identifier(self, identifier_value: str, identifier_type: str = "epc") -> Optional[Dict[str, Any]]:
139
+ """
140
+ Retrieve a tag by its identifier.
141
+ Args:
142
+ identifier_value: The value of the identifier (EPC or TID).
143
+ identifier_type: The type of identifier ("epc" or "tid").
144
+ Returns:
145
+ The tag dictionary if found, otherwise None.
146
+ """
147
+ if identifier_type not in ("epc", "tid"):
148
+ identifier_type = "epc"
149
+
150
+ if self.unique_identifier == identifier_type:
151
+ return self._tags.get(identifier_value)
152
+
153
+ for tag in self._tags.values():
154
+ if tag.get(identifier_type) == identifier_value:
155
+ return tag
156
+
157
+ return None
158
+
159
+ def clear(self) -> None:
160
+ """
161
+ Remove all stored tags.
162
+ """
163
+ with self._lock:
164
+ self._tags.clear()
165
+
166
+ def remove_tags_before_timestamp(self, timestamp: datetime) -> None:
167
+ """
168
+ Remove tags older than a given timestamp.
169
+
170
+ Args:
171
+ timestamp: Minimum timestamp to keep.
172
+ """
173
+ with self._lock:
174
+ self._tags = {k: v for k, v in self._tags.items() if v.get("timestamp") and v["timestamp"] >= timestamp}
175
+
176
+ def remove_tags_by_device(self, device: str) -> None:
177
+ """
178
+ Remove all tags associated with a specific device.
179
+
180
+ Args:
181
+ device: Device identifier.
182
+ """
183
+ with self._lock:
184
+ self._tags = {k: v for k, v in self._tags.items() if v.get("device") != device}
185
+
186
+ def get_tid_from_epc(self, epc: str) -> Optional[str]:
187
+ """
188
+ Retrieve the TID associated with a given EPC.
189
+
190
+ Args:
191
+ epc: EPC value.
192
+
193
+ Returns:
194
+ The TID if found, otherwise None.
195
+ """
196
+ with self._lock:
197
+ tag = self._tags.get(epc)
198
+ if tag:
199
+ return tag.get("tid")
200
+ return None
201
+
202
+ def get_epcs(self) -> list[str]:
203
+ """
204
+ Retrieve a list of all stored EPCs.
205
+
206
+ Returns:
207
+ A list of EPC strings.
208
+ """
209
+ with self._lock:
210
+ return [tag["epc"] for tag in self._tags.values() if "epc" in tag]
211
+
212
+ def get_gtin_counts(self) -> Dict[str, int]:
213
+ """
214
+ Retrieve counts of tags grouped by GTIN.
215
+
216
+ Returns:
217
+ A dictionary mapping GTINs to their respective counts.
218
+ """
219
+ gtin_counts: Dict[str, int] = {}
220
+ with self._lock:
221
+ for tag in self._tags.values():
222
+ gtin = tag.get("gtin")
223
+ if gtin is None:
224
+ gtin = "UNKNOWN"
225
+ gtin_counts[gtin] = gtin_counts.get(gtin, 0) + 1
226
+
227
+ return gtin_counts
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: smartx-rfid
3
- Version: 0.4.0
3
+ Version: 1.1.6
4
4
  Summary: SmartX RFID library
5
5
  License: MIT
6
6
  License-File: LICENSE
@@ -23,7 +23,6 @@ Requires-Dist: pydantic (>=2.12.5,<3.0.0)
23
23
  Requires-Dist: pyepc (==0.5.0)
24
24
  Requires-Dist: pyserial (==3.5)
25
25
  Requires-Dist: pyserial-asyncio (==0.6)
26
- Requires-Dist: ruff (>=0.14.11,<0.15.0)
27
26
  Project-URL: Documentation, https://github.com/ghpascon/smartx_rfid#readme
28
27
  Project-URL: Homepage, https://github.com/ghpascon/smartx_rfid
29
28
  Project-URL: Repository, https://github.com/ghpascon/smartx_rfid
@@ -0,0 +1,28 @@
1
+ smartx_rfid/__init__.py,sha256=XCdKdP8o5LT3VjJtWOuFHJzztc0FXt3Ms89qsx0p6sM,282
2
+ smartx_rfid/devices/RFID/R700_IOT/_main.py,sha256=VL2yDBCXp8Avb3PXKZyBNwtBRhwlLQb5TSs3aTUeC6w,7458
3
+ smartx_rfid/devices/RFID/R700_IOT/on_event.py,sha256=bG_33gY2ZHXquUbezXyMN0dyHO9SBwc36DPTc8iXc1U,799
4
+ smartx_rfid/devices/RFID/R700_IOT/reader_config_example.py,sha256=oi4vg3_zjYFDUeq1Y_d5InUx_Rq5M2CB5RDVkK3WCgU,2264
5
+ smartx_rfid/devices/RFID/R700_IOT/reader_helpers.py,sha256=8CvvYeKGplWqceVH5mEMN2_lA2Qgw7Wo1jpYuXLQlXo,5940
6
+ smartx_rfid/devices/RFID/R700_IOT/write_commands.py,sha256=GMROeq_daTScj4NZygF45UM9t0Pr3E6ugmrb7OlF73w,2284
7
+ smartx_rfid/devices/RFID/X714/_main.py,sha256=LP1X_w6Sy9m5kHLvbaRUNZbzQJU37fS8OIEWvOfrk7M,6371
8
+ smartx_rfid/devices/RFID/X714/ble_protocol.py,sha256=M3Cs66WX6oa_EPdMTLEjD-IRxAjpfrKzGj4hn1vxlxQ,6951
9
+ smartx_rfid/devices/RFID/X714/on_receive.py,sha256=Tr8KISrblB4fsLCgGTIi7TltXlyxMS8YMXLRhM9SVTg,2194
10
+ smartx_rfid/devices/RFID/X714/rfid.py,sha256=XgHl2evlN3tGB6hhCUYzpEy_jw2dyplxUcDYsuPec2Q,2285
11
+ smartx_rfid/devices/RFID/X714/serial_protocol.py,sha256=eOGM5sK35SxXfS9KjB8JQzYhR1qs4H3vw3-n3oPOjQQ,3681
12
+ smartx_rfid/devices/RFID/X714/tcp_protocol.py,sha256=GthynyN6YMl6R16nrAnlrDK61QnuZAOhYhPqycBCwJA,4804
13
+ smartx_rfid/devices/RFID/X714/write_commands.py,sha256=tKLTATb4vDkghpw5rQuJwcnPNkViT4753fwToNPfKh0,1304
14
+ smartx_rfid/devices/__init__.py,sha256=EVhtb-IBLpits8dr-I1ZYYuWHw9ddqiu-98myg-iyFg,251
15
+ smartx_rfid/devices/generic/SERIAL/_main.py,sha256=7rEgviwxfd4uWpWjVXsV0qnOAET-MxJux9PcTKiE8G8,8562
16
+ smartx_rfid/devices/generic/TCP/_main.py,sha256=DY9c9m2ZchvUJ9n2TtPENL2GqCYS4hvdjkCAe6yGXxM,2770
17
+ smartx_rfid/devices/generic/TCP/helpers.py,sha256=GQ_yIvmSlx_yZci6pVvZfdo1wyKcS5gtZh-VDwt3pGs,1552
18
+ smartx_rfid/schemas/events.py,sha256=0rdXXww6v1B50pGksNsGt5QhavwKpe1Jb_5f8o_SaVk,215
19
+ smartx_rfid/schemas/tag.py,sha256=iSQkWI4MhSSVEzWhezX6UG4KJJ-FUahrfj0Hnz-H_u0,2269
20
+ smartx_rfid/utils/__init__.py,sha256=lJA0KqFIFVDtietXGH9WwnklKkiTNvhIG6YJdDd11vk,72
21
+ smartx_rfid/utils/event.py,sha256=7QOdiSfiMscoAaTq4U3E3asYGSVnolr9OD3FSYGh6bg,469
22
+ smartx_rfid/utils/logger_manager.py,sha256=61HJ40wgX0hVwG4xW66IQ2jdJp8CFM_S0eoGzx8-FwU,5653
23
+ smartx_rfid/utils/path.py,sha256=7U619vOw4BXGAKbSVdA6ZIhpCN0f-dccVax9LCclbNc,3758
24
+ smartx_rfid/utils/tag_list.py,sha256=b_qx0-ZkjhzKiAuCM6uuBo3QpzbmB2U1Z3CNdStCyxc,6570
25
+ smartx_rfid-1.1.6.dist-info/METADATA,sha256=pFSEmk-GgfHWxJJ_68oIrl6xd3UfjeBwbG18n419K6M,2636
26
+ smartx_rfid-1.1.6.dist-info/WHEEL,sha256=zp0Cn7JsFoX2ATtOhtaFYIiE2rmFAD4OcMhtUki8W3U,88
27
+ smartx_rfid-1.1.6.dist-info/licenses/LICENSE,sha256=npGJO3HuHM3JdFNEXv1jRmP_wRYq9-ujkqJPmLxrtEw,1080
28
+ smartx_rfid-1.1.6.dist-info/RECORD,,
@@ -1,24 +0,0 @@
1
- smartx_rfid/__init__.py,sha256=XCdKdP8o5LT3VjJtWOuFHJzztc0FXt3Ms89qsx0p6sM,282
2
- smartx_rfid/devices/RFID/R700_IOT/_main.py,sha256=xd9roD7ovnJAXLp6r7PtxYz98CgKO1t6gz3t4V0KIMU,6293
3
- smartx_rfid/devices/RFID/R700_IOT/on_event.py,sha256=JmJ6RcGgX34b2gkuqyZ4G1LSDliRRALg_DdvenHOZVk,538
4
- smartx_rfid/devices/RFID/R700_IOT/reader_config_example.py,sha256=oi4vg3_zjYFDUeq1Y_d5InUx_Rq5M2CB5RDVkK3WCgU,2264
5
- smartx_rfid/devices/RFID/R700_IOT/reader_helpers.py,sha256=UC4ozq9FmuQtCK5Nv8Mjg4tZyuFetjx1eMWiw5yb0XY,5692
6
- smartx_rfid/devices/RFID/R700_IOT/write_commands.py,sha256=d1GXBaBVaNqvPWitYNZ94uZJOy2wwbj-2uzCxWOflPo,2037
7
- smartx_rfid/devices/RFID/X714/_main.py,sha256=ERMeQnbLevGXtZ-8Roykb67jbt53yacawibPkCs4eGo,4753
8
- smartx_rfid/devices/RFID/X714/ble_protocol.py,sha256=M3Cs66WX6oa_EPdMTLEjD-IRxAjpfrKzGj4hn1vxlxQ,6951
9
- smartx_rfid/devices/RFID/X714/on_receive.py,sha256=yjkxWL1ZGzcZKZXKwQ85cvl33fsjhXATOwg9QQBk-GQ,1641
10
- smartx_rfid/devices/RFID/X714/rfid.py,sha256=W-8-ADYv9oYE9PHDEynQhHCXyVecDRfplWRIxb3cqts,1974
11
- smartx_rfid/devices/RFID/X714/serial_protocol.py,sha256=eOGM5sK35SxXfS9KjB8JQzYhR1qs4H3vw3-n3oPOjQQ,3681
12
- smartx_rfid/devices/RFID/X714/tcp_protocol.py,sha256=GthynyN6YMl6R16nrAnlrDK61QnuZAOhYhPqycBCwJA,4804
13
- smartx_rfid/devices/RFID/X714/write_commands.py,sha256=gkfsQ--bijZWw0e1FP8Wb0qtFtexM4hP6HJz6y5hvOM,987
14
- smartx_rfid/devices/__init__.py,sha256=EVhtb-IBLpits8dr-I1ZYYuWHw9ddqiu-98myg-iyFg,251
15
- smartx_rfid/devices/generic/SERIAL/_main.py,sha256=BB4b6t6NI7tQDZH1ZAl-ZqSG8WH_HmGJIrIs5hIxpS8,8356
16
- smartx_rfid/devices/generic/TCP/_main.py,sha256=BzpIiCxamtauQCa7GCldNlG_fhSLJoSxDs2lhGEc31M,2318
17
- smartx_rfid/devices/generic/TCP/helpers.py,sha256=5ySy0Wv6D3q9AEV8kVLw1aMbe3kMDm4lFxP3KLIMMWM,1386
18
- smartx_rfid/schemas/tag.py,sha256=gq38yKPqPBPaF-wx_0_lnhttz0neM_bob94puv-5Z7Y,2285
19
- smartx_rfid/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
20
- smartx_rfid/utils/event.py,sha256=7QOdiSfiMscoAaTq4U3E3asYGSVnolr9OD3FSYGh6bg,469
21
- smartx_rfid-0.4.0.dist-info/METADATA,sha256=OPKyl9QnD1r2um-C9pwMXuKsMxEji8qv7xItz5T8MsQ,2676
22
- smartx_rfid-0.4.0.dist-info/WHEEL,sha256=zp0Cn7JsFoX2ATtOhtaFYIiE2rmFAD4OcMhtUki8W3U,88
23
- smartx_rfid-0.4.0.dist-info/licenses/LICENSE,sha256=npGJO3HuHM3JdFNEXv1jRmP_wRYq9-ujkqJPmLxrtEw,1080
24
- smartx_rfid-0.4.0.dist-info/RECORD,,