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.
- topdata_sdk-0.1.0/PKG-INFO +197 -0
- topdata_sdk-0.1.0/README.md +169 -0
- topdata_sdk-0.1.0/pyproject.toml +49 -0
- topdata_sdk-0.1.0/setup.cfg +4 -0
- topdata_sdk-0.1.0/src/topdata/__init__.py +60 -0
- topdata_sdk-0.1.0/src/topdata/constants.py +75 -0
- topdata_sdk-0.1.0/src/topdata/exceptions.py +51 -0
- topdata_sdk-0.1.0/src/topdata/image.py +120 -0
- topdata_sdk-0.1.0/src/topdata/models.py +137 -0
- topdata_sdk-0.1.0/src/topdata/server.py +307 -0
- topdata_sdk-0.1.0/src/topdata/session.py +495 -0
- topdata_sdk-0.1.0/src/topdata/wiegand.py +93 -0
- topdata_sdk-0.1.0/src/topdata_sdk.egg-info/PKG-INFO +197 -0
- topdata_sdk-0.1.0/src/topdata_sdk.egg-info/SOURCES.txt +18 -0
- topdata_sdk-0.1.0/src/topdata_sdk.egg-info/dependency_links.txt +1 -0
- topdata_sdk-0.1.0/src/topdata_sdk.egg-info/requires.txt +7 -0
- topdata_sdk-0.1.0/src/topdata_sdk.egg-info/top_level.txt +1 -0
- topdata_sdk-0.1.0/tests/test_image.py +113 -0
- topdata_sdk-0.1.0/tests/test_session.py +376 -0
- topdata_sdk-0.1.0/tests/test_wiegand.py +110 -0
|
@@ -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,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))
|