topdata-sdk 0.1.0__tar.gz

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,197 @@
1
+ Metadata-Version: 2.4
2
+ Name: topdata-sdk
3
+ Version: 0.1.0
4
+ Summary: A production-ready asynchronous Python SDK for Topdata facial reader devices (AiFace, Catraca Fit, Revolution, Box, Inner Ponto 4, Inner Acesso 2). WebSocket server model — the device connects to you.
5
+ Author-email: Tulio Amancio <root@tsuriu.com.br>
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://gitlab.com/libandpackages/topdata-sdk
8
+ Project-URL: Source, https://gitlab.com/libandpackages/topdata-sdk
9
+ Project-URL: Bug Tracker, https://gitlab.com/libandpackages/topdata-sdk/-/issues
10
+ Keywords: topdata,facial-reader,access-control,biometrics,iot,async,sdk,websocket
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.10
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Operating System :: OS Independent
18
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
19
+ Classifier: Topic :: System :: Hardware
20
+ Requires-Python: >=3.10
21
+ Description-Content-Type: text/markdown
22
+ Requires-Dist: websockets>=12.0
23
+ Requires-Dist: pydantic>=2.0.0
24
+ Requires-Dist: Pillow>=10.0.0
25
+ Provides-Extra: dev
26
+ Requires-Dist: pytest>=7.0.0; extra == "dev"
27
+ Requires-Dist: pytest-asyncio>=0.21.0; extra == "dev"
28
+
29
+ # Topdata Python SDK
30
+
31
+ A production-ready asynchronous Python SDK for **Topdata facial reader devices** (AiFace, Catraca Fit, Revolution, Box, Inner Ponto 4, Inner Acesso 2).
32
+
33
+ > ⚠️ **Inverted connection model**: Unlike traditional device SDKs where your software connects _to_ the device, Topdata devices connect _to_ your server. This SDK runs a WebSocket server that accepts incoming connections from the readers.
34
+
35
+ ## Installation
36
+
37
+ ```bash
38
+ pip install topdata-sdk
39
+ ```
40
+
41
+ ## Quick Start
42
+
43
+ ```python
44
+ import asyncio
45
+ from topdata import TopdataServer, TopdataDeviceSession, LogEvent
46
+
47
+ async def main():
48
+ server = TopdataServer(host="0.0.0.0", port=7792)
49
+
50
+ async def on_connected(session: TopdataDeviceSession):
51
+ print(f"✅ Device connected: {session.serial_number}")
52
+ print(f" Model: {session.device_info.modelname}")
53
+ print(f" Firmware: {session.device_info.firmware}")
54
+ print(f" Users: {session.device_info.useduser}/{session.device_info.usersize}")
55
+
56
+ async def on_disconnected(sn: str):
57
+ print(f"❌ Device disconnected: {sn}")
58
+
59
+ async def on_event(session: TopdataDeviceSession, event: LogEvent):
60
+ for record in event.record:
61
+ print(f"🔔 Access event from {session.serial_number}:")
62
+ print(f" User: {record.enrollid} ({record.name})")
63
+ print(f" Time: {record.time}")
64
+ print(f" Mode: {record.mode} (8=face, 3=card, 2=password)")
65
+ print(f" Event: {record.event}")
66
+
67
+ # For online mode, return access decision:
68
+ # return {"access": True, "message": "Welcome!"}
69
+ return None # Offline mode — no access decision needed
70
+
71
+ server.on_device_connected = on_connected
72
+ server.on_device_disconnected = on_disconnected
73
+ server.on_event = on_event
74
+
75
+ await server.start()
76
+ print("🚀 Topdata server listening on ws://0.0.0.0:7792/pub/chat")
77
+ print(" Configure your device: MENU → REDE → SERVIDOR → IP/Porta")
78
+
79
+ # Keep running
80
+ try:
81
+ await asyncio.Future() # Run forever
82
+ except KeyboardInterrupt:
83
+ await server.stop()
84
+
85
+ asyncio.run(main())
86
+ ```
87
+
88
+ ## User Management
89
+
90
+ Once a device is connected, you can manage users through the session object:
91
+
92
+ ```python
93
+ async def on_connected(session: TopdataDeviceSession):
94
+ # Create a user (without photo)
95
+ await session.set_user(
96
+ enrollid=1001,
97
+ name="João Silva",
98
+ admin=0, # 0=user, 1=admin, 2=super
99
+ card=25565535, # Wiegand 10 format
100
+ password=1234,
101
+ )
102
+
103
+ # Add a facial photo
104
+ with open("joao.jpg", "rb") as f:
105
+ photo_data = f.read()
106
+ await session.set_user_photo(enrollid=1001, image_data=photo_data, name="João Silva")
107
+
108
+ # List all users
109
+ users = await session.get_user_list()
110
+ print(f"Device has {len(users)} user records")
111
+
112
+ # Get user details
113
+ info = await session.get_user_info(enrollid=1001)
114
+ print(f"User: {info.name}, has face: {info.faceflag}")
115
+
116
+ # Delete a user
117
+ await session.delete_user(enrollid=1001)
118
+ ```
119
+
120
+ ## Image Requirements
121
+
122
+ Photos sent to the device must meet these requirements:
123
+ - **Format**: JPEG only
124
+ - **File size**: < 150 KB
125
+ - **Resolution**: 240×320 to 800×1280 px (recommended: 480×640)
126
+ - **Content**: Single person, vertical face, no mask/hat/sunglasses
127
+
128
+ The SDK **automatically validates and normalizes** images: oversized files are downscaled to 480×640 and re-compressed.
129
+
130
+ ## Wiegand Utilities
131
+
132
+ ```python
133
+ from topdata import wiegand10_to_wiegand26, wiegand26_to_wiegand10
134
+
135
+ # Wiegand 10 (facility=255, card=65535) → Wiegand 26 integer
136
+ w26 = wiegand10_to_wiegand26(255, 65535) # → 16776959
137
+
138
+ # Reverse
139
+ facility, card = wiegand26_to_wiegand10(16776959) # → (255, 65535)
140
+ ```
141
+
142
+ ## Device Configuration
143
+
144
+ ```python
145
+ async def configure_device(session: TopdataDeviceSession):
146
+ # Set online mode (server decides access)
147
+ await session.set_device_info(server_verify=1)
148
+
149
+ # Set volume and door open time
150
+ await session.set_device_info(volume=8, door_opentime=5)
151
+
152
+ # Disable device during bulk operations
153
+ await session.disable()
154
+ # ... do bulk operations ...
155
+ await session.enable()
156
+ ```
157
+
158
+ ## Protocol Reference
159
+
160
+ | Command | Direction | Description |
161
+ |---|---|---|
162
+ | `reg` | device → server | Handshake (serial, capabilities) |
163
+ | `sendlog` | device → server | Access event (face/card/password recognition) |
164
+ | `senduser` | device → server | User registered at device |
165
+ | `enabledevice` | server → device | Re-enable recognition |
166
+ | `disabledevice` | server → device | Suspend recognition |
167
+ | `getuserlist` | server → device | List users (paginated) |
168
+ | `getuserinfo` | server → device | Get user details |
169
+ | `setuserinfo` | server → device | Create/update user |
170
+ | `deleteuser` | server → device | Delete user data |
171
+ | `cleanuser` | server → device | Delete ALL users |
172
+ | `setdevinfo` | server → device | Configure device parameters |
173
+ | `setdevlock` | server → device | Configure card format & time zones |
174
+ | `setuserlock` | server → device | Per-user time restrictions |
175
+ | `getalllog` | server → device | Fetch access logs (paginated) |
176
+ | `cleanlog` | server → device | Delete all logs |
177
+
178
+ ## Architecture
179
+
180
+ ```
181
+ ┌──────────────────┐ WebSocket ┌──────────────────┐
182
+ │ Topdata Device │ ──── connects to ──→│ TopdataServer │
183
+ │ (Facial Reader) │ │ (Your App) │
184
+ │ │ ← reg ──────────── │ │
185
+ │ │ ── ret:reg ───────→ │ │
186
+ │ │ │ │
187
+ │ │ ← sendlog ──────── │ on_event() │
188
+ │ │ ── ret:sendlog ───→ │ │
189
+ │ │ │ │
190
+ │ │ ── cmd:setuserinfo →│ │
191
+ │ │ ← ret:setuserinfo ─ │ │
192
+ └──────────────────┘ └──────────────────┘
193
+ ```
194
+
195
+ ## License
196
+
197
+ MIT
@@ -0,0 +1,169 @@
1
+ # Topdata Python SDK
2
+
3
+ A production-ready asynchronous Python SDK for **Topdata facial reader devices** (AiFace, Catraca Fit, Revolution, Box, Inner Ponto 4, Inner Acesso 2).
4
+
5
+ > ⚠️ **Inverted connection model**: Unlike traditional device SDKs where your software connects _to_ the device, Topdata devices connect _to_ your server. This SDK runs a WebSocket server that accepts incoming connections from the readers.
6
+
7
+ ## Installation
8
+
9
+ ```bash
10
+ pip install topdata-sdk
11
+ ```
12
+
13
+ ## Quick Start
14
+
15
+ ```python
16
+ import asyncio
17
+ from topdata import TopdataServer, TopdataDeviceSession, LogEvent
18
+
19
+ async def main():
20
+ server = TopdataServer(host="0.0.0.0", port=7792)
21
+
22
+ async def on_connected(session: TopdataDeviceSession):
23
+ print(f"✅ Device connected: {session.serial_number}")
24
+ print(f" Model: {session.device_info.modelname}")
25
+ print(f" Firmware: {session.device_info.firmware}")
26
+ print(f" Users: {session.device_info.useduser}/{session.device_info.usersize}")
27
+
28
+ async def on_disconnected(sn: str):
29
+ print(f"❌ Device disconnected: {sn}")
30
+
31
+ async def on_event(session: TopdataDeviceSession, event: LogEvent):
32
+ for record in event.record:
33
+ print(f"🔔 Access event from {session.serial_number}:")
34
+ print(f" User: {record.enrollid} ({record.name})")
35
+ print(f" Time: {record.time}")
36
+ print(f" Mode: {record.mode} (8=face, 3=card, 2=password)")
37
+ print(f" Event: {record.event}")
38
+
39
+ # For online mode, return access decision:
40
+ # return {"access": True, "message": "Welcome!"}
41
+ return None # Offline mode — no access decision needed
42
+
43
+ server.on_device_connected = on_connected
44
+ server.on_device_disconnected = on_disconnected
45
+ server.on_event = on_event
46
+
47
+ await server.start()
48
+ print("🚀 Topdata server listening on ws://0.0.0.0:7792/pub/chat")
49
+ print(" Configure your device: MENU → REDE → SERVIDOR → IP/Porta")
50
+
51
+ # Keep running
52
+ try:
53
+ await asyncio.Future() # Run forever
54
+ except KeyboardInterrupt:
55
+ await server.stop()
56
+
57
+ asyncio.run(main())
58
+ ```
59
+
60
+ ## User Management
61
+
62
+ Once a device is connected, you can manage users through the session object:
63
+
64
+ ```python
65
+ async def on_connected(session: TopdataDeviceSession):
66
+ # Create a user (without photo)
67
+ await session.set_user(
68
+ enrollid=1001,
69
+ name="João Silva",
70
+ admin=0, # 0=user, 1=admin, 2=super
71
+ card=25565535, # Wiegand 10 format
72
+ password=1234,
73
+ )
74
+
75
+ # Add a facial photo
76
+ with open("joao.jpg", "rb") as f:
77
+ photo_data = f.read()
78
+ await session.set_user_photo(enrollid=1001, image_data=photo_data, name="João Silva")
79
+
80
+ # List all users
81
+ users = await session.get_user_list()
82
+ print(f"Device has {len(users)} user records")
83
+
84
+ # Get user details
85
+ info = await session.get_user_info(enrollid=1001)
86
+ print(f"User: {info.name}, has face: {info.faceflag}")
87
+
88
+ # Delete a user
89
+ await session.delete_user(enrollid=1001)
90
+ ```
91
+
92
+ ## Image Requirements
93
+
94
+ Photos sent to the device must meet these requirements:
95
+ - **Format**: JPEG only
96
+ - **File size**: < 150 KB
97
+ - **Resolution**: 240×320 to 800×1280 px (recommended: 480×640)
98
+ - **Content**: Single person, vertical face, no mask/hat/sunglasses
99
+
100
+ The SDK **automatically validates and normalizes** images: oversized files are downscaled to 480×640 and re-compressed.
101
+
102
+ ## Wiegand Utilities
103
+
104
+ ```python
105
+ from topdata import wiegand10_to_wiegand26, wiegand26_to_wiegand10
106
+
107
+ # Wiegand 10 (facility=255, card=65535) → Wiegand 26 integer
108
+ w26 = wiegand10_to_wiegand26(255, 65535) # → 16776959
109
+
110
+ # Reverse
111
+ facility, card = wiegand26_to_wiegand10(16776959) # → (255, 65535)
112
+ ```
113
+
114
+ ## Device Configuration
115
+
116
+ ```python
117
+ async def configure_device(session: TopdataDeviceSession):
118
+ # Set online mode (server decides access)
119
+ await session.set_device_info(server_verify=1)
120
+
121
+ # Set volume and door open time
122
+ await session.set_device_info(volume=8, door_opentime=5)
123
+
124
+ # Disable device during bulk operations
125
+ await session.disable()
126
+ # ... do bulk operations ...
127
+ await session.enable()
128
+ ```
129
+
130
+ ## Protocol Reference
131
+
132
+ | Command | Direction | Description |
133
+ |---|---|---|
134
+ | `reg` | device → server | Handshake (serial, capabilities) |
135
+ | `sendlog` | device → server | Access event (face/card/password recognition) |
136
+ | `senduser` | device → server | User registered at device |
137
+ | `enabledevice` | server → device | Re-enable recognition |
138
+ | `disabledevice` | server → device | Suspend recognition |
139
+ | `getuserlist` | server → device | List users (paginated) |
140
+ | `getuserinfo` | server → device | Get user details |
141
+ | `setuserinfo` | server → device | Create/update user |
142
+ | `deleteuser` | server → device | Delete user data |
143
+ | `cleanuser` | server → device | Delete ALL users |
144
+ | `setdevinfo` | server → device | Configure device parameters |
145
+ | `setdevlock` | server → device | Configure card format & time zones |
146
+ | `setuserlock` | server → device | Per-user time restrictions |
147
+ | `getalllog` | server → device | Fetch access logs (paginated) |
148
+ | `cleanlog` | server → device | Delete all logs |
149
+
150
+ ## Architecture
151
+
152
+ ```
153
+ ┌──────────────────┐ WebSocket ┌──────────────────┐
154
+ │ Topdata Device │ ──── connects to ──→│ TopdataServer │
155
+ │ (Facial Reader) │ │ (Your App) │
156
+ │ │ ← reg ──────────── │ │
157
+ │ │ ── ret:reg ───────→ │ │
158
+ │ │ │ │
159
+ │ │ ← sendlog ──────── │ on_event() │
160
+ │ │ ── ret:sendlog ───→ │ │
161
+ │ │ │ │
162
+ │ │ ── cmd:setuserinfo →│ │
163
+ │ │ ← ret:setuserinfo ─ │ │
164
+ └──────────────────┘ └──────────────────┘
165
+ ```
166
+
167
+ ## License
168
+
169
+ MIT
@@ -0,0 +1,49 @@
1
+ [build-system]
2
+ requires = ["setuptools>=77.0.0"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "topdata-sdk"
7
+ version = "0.1.0"
8
+ authors = [
9
+ { name="Tulio Amancio", email="root@tsuriu.com.br" },
10
+ ]
11
+ description = "A production-ready asynchronous Python SDK for Topdata facial reader devices (AiFace, Catraca Fit, Revolution, Box, Inner Ponto 4, Inner Acesso 2). WebSocket server model — the device connects to you."
12
+ readme = "README.md"
13
+ license = "MIT"
14
+ license-files = ["LICENSE"]
15
+ keywords = ["topdata", "facial-reader", "access-control", "biometrics", "iot", "async", "sdk", "websocket"]
16
+ requires-python = ">=3.10"
17
+ classifiers = [
18
+ "Development Status :: 3 - Alpha",
19
+ "Intended Audience :: Developers",
20
+ "Programming Language :: Python :: 3",
21
+ "Programming Language :: Python :: 3.10",
22
+ "Programming Language :: Python :: 3.11",
23
+ "Programming Language :: Python :: 3.12",
24
+ "Operating System :: OS Independent",
25
+ "Topic :: Software Development :: Libraries :: Python Modules",
26
+ "Topic :: System :: Hardware",
27
+ ]
28
+ dependencies = [
29
+ "websockets>=12.0",
30
+ "pydantic>=2.0.0",
31
+ "Pillow>=10.0.0",
32
+ ]
33
+
34
+ [project.optional-dependencies]
35
+ dev = [
36
+ "pytest>=7.0.0",
37
+ "pytest-asyncio>=0.21.0",
38
+ ]
39
+
40
+ [project.urls]
41
+ "Homepage" = "https://gitlab.com/libandpackages/topdata-sdk"
42
+ "Source" = "https://gitlab.com/libandpackages/topdata-sdk"
43
+ "Bug Tracker" = "https://gitlab.com/libandpackages/topdata-sdk/-/issues"
44
+
45
+ [tool.setuptools.packages.find]
46
+ where = ["src"]
47
+
48
+ [tool.pytest.ini_options]
49
+ asyncio_mode = "auto"
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,60 @@
1
+ from . import constants
2
+ from .server import TopdataServer
3
+ from .session import TopdataDeviceSession
4
+ from .exceptions import (
5
+ CommandTimeoutError,
6
+ DeviceDisconnectedError,
7
+ InvalidImageError,
8
+ ProtocolError,
9
+ TopdataError,
10
+ )
11
+ from .models import (
12
+ DeviceInfo,
13
+ LogEvent,
14
+ LogRecord,
15
+ RegMessage,
16
+ RegResponse,
17
+ SendlogResponse,
18
+ UserInfo,
19
+ UserListItem,
20
+ )
21
+ from .image import prepare_image_record, validate_and_normalize
22
+ from .wiegand import (
23
+ format_wiegand10,
24
+ parse_wiegand10,
25
+ wiegand10_to_wiegand26,
26
+ wiegand26_to_wiegand10,
27
+ )
28
+
29
+ __version__ = "0.1.0"
30
+
31
+ __all__ = [
32
+ # Core classes
33
+ "TopdataServer",
34
+ "TopdataDeviceSession",
35
+ # Constants module
36
+ "constants",
37
+ # Exceptions
38
+ "TopdataError",
39
+ "CommandTimeoutError",
40
+ "DeviceDisconnectedError",
41
+ "InvalidImageError",
42
+ "ProtocolError",
43
+ # Models
44
+ "DeviceInfo",
45
+ "LogEvent",
46
+ "LogRecord",
47
+ "RegMessage",
48
+ "RegResponse",
49
+ "SendlogResponse",
50
+ "UserInfo",
51
+ "UserListItem",
52
+ # Image utilities
53
+ "prepare_image_record",
54
+ "validate_and_normalize",
55
+ # Wiegand utilities
56
+ "format_wiegand10",
57
+ "parse_wiegand10",
58
+ "wiegand10_to_wiegand26",
59
+ "wiegand26_to_wiegand10",
60
+ ]
@@ -0,0 +1,75 @@
1
+ from __future__ import annotations
2
+ """
3
+ Constants for the Topdata Facial Reader SDK.
4
+ """
5
+ from enum import IntEnum
6
+
7
+
8
+ # ─── Backup Number (data type selector for setuserinfo/getuserinfo/deleteuser) ─
9
+ class BackupNum(IntEnum):
10
+ """Backup number values used in setuserinfo, getuserinfo, and deleteuser commands."""
11
+ ALL = 0 # All data (no photo in setuserinfo; all data in getuserinfo/deleteuser)
12
+ PASSWORD = 10 # Password only
13
+ CARD = 11 # Card only
14
+ ALL_EXCEPT_BIOMETRY = 13 # All data except biometry (photo)
15
+ FACE = 50 # Face photo only
16
+
17
+
18
+ # ─── Admin Level ───────────────────────────────────────────────────────────────
19
+ class AdminLevel(IntEnum):
20
+ """User privilege levels on the device."""
21
+ USER = 0 # Standard user (facial recognition only)
22
+ ADMIN = 1 # Administrator (device menu access)
23
+ SUPER_USER = 2 # Super user (not recommended by manufacturer)
24
+
25
+
26
+ # ─── Authentication Mode (mode field in sendlog) ──────────────────────────────
27
+ class AuthMode(IntEnum):
28
+ """Authentication method used in access events (sendlog.record.mode)."""
29
+ PASSWORD = 2
30
+ CARD = 3
31
+ FACE = 8
32
+
33
+
34
+ # ─── Verify Mode (verifymode in setdevinfo / setuserlock) ─────────────────────
35
+ class VerifyMode(IntEnum):
36
+ """Verification mode for device/user access configuration."""
37
+ FACE_CARD_OR_PWD = 0 # Face, Card, or Password
38
+ PWD_ONLY = 2 # Password only
39
+ CARD_ONLY = 3 # Card only
40
+ FACE_ONLY = 8 # Face only
41
+ FACE_AND_PWD = 9 # Face and Password
42
+ CARD_AND_FACE = 10 # Card and Face
43
+ CARD_AND_PWD = 11 # Card and Password
44
+ FACE_AND_CARD_OR_PWD = 14 # Face and (Card or Password)
45
+
46
+
47
+ # ─── Server Verify Mode (server_verify in setdevinfo) ─────────────────────────
48
+ class ServerVerify(IntEnum):
49
+ """Server verification mode for online/offline operation."""
50
+ OFFLINE = 0 # Offline only (device decides access)
51
+ ONLINE = 1 # Online only (server decides access)
52
+ AUTO = 2 # Auto switch between online and offline
53
+
54
+
55
+ # ─── Image Validation Limits ──────────────────────────────────────────────────
56
+ MAX_IMAGE_BYTES = 150 * 1024 # 150 KB
57
+ MIN_RESOLUTION = (240, 320) # width, height
58
+ MAX_RESOLUTION = (800, 1280) # width, height
59
+ RECOMMENDED_RESOLUTION = (480, 640) # width, height
60
+
61
+ # ─── Protocol Defaults ────────────────────────────────────────────────────────
62
+ DEFAULT_WS_PORT = 7792
63
+ DEFAULT_WS_PATH = "/pub/chat"
64
+ DEFAULT_COMMAND_TIMEOUT = 10.0 # seconds
65
+
66
+ # ─── Special Values ───────────────────────────────────────────────────────────
67
+ UNKNOWN_ENROLLID = 99999999 # enrollid used by firmware for unrecognized faces
68
+ MAX_ENROLLID = 999_999_999_999 # 12-digit maximum enrollid
69
+
70
+ # ─── Card Format (setdevlock.cardformat) ──────────────────────────────────────
71
+ class CardFormat(IntEnum):
72
+ """Card number display format on the device."""
73
+ DECIMAL = 0
74
+ WIEGAND = 1
75
+ HEXADECIMAL = 2
@@ -0,0 +1,51 @@
1
+ from __future__ import annotations
2
+
3
+
4
+ class TopdataError(Exception):
5
+ """Base exception for the Topdata SDK."""
6
+ pass
7
+
8
+
9
+ class CommandTimeoutError(TopdataError):
10
+ """Raised when a command sent to the device does not receive a response within the timeout."""
11
+ def __init__(self, command: str, timeout: float):
12
+ self.command = command
13
+ self.timeout = timeout
14
+ super().__init__(f"Command '{command}' timed out after {timeout}s")
15
+
16
+
17
+ class DeviceDisconnectedError(TopdataError):
18
+ """Raised when the WebSocket connection to the device is lost during an operation."""
19
+ def __init__(self, sn: str | None = None, message: str | None = None):
20
+ self.sn = sn
21
+ msg = message or f"Device '{sn}' disconnected"
22
+ super().__init__(msg)
23
+
24
+
25
+ class InvalidImageError(TopdataError):
26
+ """Raised when an image fails validation before being sent to the device.
27
+
28
+ Possible reasons: not JPEG, file too large (>150KB after processing),
29
+ resolution outside 240x320 – 800x1280, or other quality issues.
30
+ """
31
+ pass
32
+
33
+
34
+ class ProtocolError(TopdataError):
35
+ """Raised when the device returns result:false in a command response.
36
+
37
+ Attributes:
38
+ command: The command name (ret field value).
39
+ reason: The numeric reason code, if present.
40
+ msg: The human-readable error message from the device, if present.
41
+ """
42
+ def __init__(self, command: str, reason: int | None = None, msg: str | None = None):
43
+ self.command = command
44
+ self.reason = reason
45
+ self.msg = msg
46
+ parts = [f"Command '{command}' failed"]
47
+ if reason is not None:
48
+ parts.append(f"reason={reason}")
49
+ if msg:
50
+ parts.append(f"msg='{msg}'")
51
+ super().__init__(", ".join(parts))