rbzk 0.1.7 → 0.1.8
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.
- checksums.yaml +4 -4
- data/lib/rbzk/cli/commands.rb +35 -16
- data/lib/rbzk/exceptions.rb +6 -0
- data/lib/rbzk/version.rb +1 -1
- data/lib/rbzk/zk.rb +497 -652
- metadata +2 -2
data/lib/rbzk/zk.rb
CHANGED
|
@@ -14,48 +14,45 @@ module RBZK
|
|
|
14
14
|
end
|
|
15
15
|
|
|
16
16
|
def test_ping
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
return true
|
|
22
|
-
end
|
|
23
|
-
rescue Timeout::Error, Errno::ECONNREFUSED, Errno::EHOSTUNREACH, SocketError => e
|
|
24
|
-
return false
|
|
25
|
-
rescue => e
|
|
26
|
-
return false
|
|
17
|
+
Timeout.timeout(5) do
|
|
18
|
+
s = TCPSocket.new(@ip, @port)
|
|
19
|
+
s.close
|
|
20
|
+
return true
|
|
27
21
|
end
|
|
22
|
+
rescue Timeout::Error, Errno::ECONNREFUSED, Errno::EHOSTUNREACH, SocketError
|
|
23
|
+
false
|
|
24
|
+
rescue StandardError
|
|
25
|
+
false
|
|
28
26
|
end
|
|
29
27
|
|
|
30
28
|
def test_tcp
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
client.setsockopt(Socket::SOL_SOCKET, Socket::SO_RCVTIMEO, [ 10, 0 ].pack('l_*'))
|
|
29
|
+
client = Socket.new(Socket::AF_INET, Socket::SOCK_STREAM)
|
|
30
|
+
client.setsockopt(Socket::SOL_SOCKET, Socket::SO_RCVTIMEO, [ 10, 0 ].pack('l_*'))
|
|
34
31
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
# Some exceptions (e.g., Socket::ResolutionError) don't provide errno
|
|
43
|
-
result = e.respond_to?(:errno) ? e.errno : 1 # Connection failed
|
|
44
|
-
end
|
|
45
|
-
|
|
46
|
-
client.close
|
|
47
|
-
return result
|
|
48
|
-
rescue => e
|
|
32
|
+
sockaddr = Socket.pack_sockaddr_in(@port, @ip)
|
|
33
|
+
begin
|
|
34
|
+
client.connect(sockaddr)
|
|
35
|
+
result = 0 # Success code
|
|
36
|
+
rescue Errno::EISCONN
|
|
37
|
+
result = 0 # Already connected
|
|
38
|
+
rescue StandardError => e
|
|
49
39
|
# Some exceptions (e.g., Socket::ResolutionError) don't provide errno
|
|
50
|
-
|
|
40
|
+
result = e.respond_to?(:errno) ? e.errno : 1 # Connection failed
|
|
51
41
|
end
|
|
42
|
+
|
|
43
|
+
client.close
|
|
44
|
+
result
|
|
45
|
+
rescue StandardError => e
|
|
46
|
+
# Some exceptions (e.g., Socket::ResolutionError) don't provide errno
|
|
47
|
+
e.respond_to?(:errno) ? e.errno : 1
|
|
52
48
|
end
|
|
53
49
|
end
|
|
54
50
|
|
|
55
51
|
class ZK
|
|
56
52
|
include RBZK::Constants
|
|
57
53
|
|
|
58
|
-
def initialize(ip, port: 4370, timeout: 60, password: 0, force_udp: false, omit_ping: false, verbose: false,
|
|
54
|
+
def initialize(ip, port: 4370, timeout: 60, password: 0, force_udp: false, omit_ping: false, verbose: false,
|
|
55
|
+
encoding: 'UTF-8')
|
|
59
56
|
# Initialize the ZK device connection
|
|
60
57
|
RBZK::User.encoding = encoding
|
|
61
58
|
@address = [ ip, port ]
|
|
@@ -79,6 +76,8 @@ module RBZK
|
|
|
79
76
|
@data = nil
|
|
80
77
|
@connected = false
|
|
81
78
|
@next_uid = 1
|
|
79
|
+
@next_user_id = '1'
|
|
80
|
+
@user_packet_size = 28
|
|
82
81
|
|
|
83
82
|
# Storage for user and attendance data
|
|
84
83
|
@users = {}
|
|
@@ -104,22 +103,20 @@ module RBZK
|
|
|
104
103
|
# Create helper for ping and TCP tests
|
|
105
104
|
@helper = ZKHelper.new(ip, port)
|
|
106
105
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
106
|
+
return unless @verbose
|
|
107
|
+
|
|
108
|
+
puts "ZK instance created for device at #{@ip}:#{@port}"
|
|
109
|
+
puts "Using #{@force_udp ? 'UDP' : 'TCP'} mode"
|
|
111
110
|
end
|
|
112
111
|
|
|
113
112
|
def connect
|
|
114
113
|
return self if @connected
|
|
115
114
|
|
|
116
115
|
# Skip ping check if requested
|
|
117
|
-
if !@omit_ping && !@helper.test_ping
|
|
118
|
-
raise RBZK::ZKNetworkError, "Can't reach device (ping #{@ip})"
|
|
119
|
-
end
|
|
116
|
+
raise RBZK::ZKNetworkError, "Can't reach device (ping #{@ip})" if !@omit_ping && !@helper.test_ping
|
|
120
117
|
|
|
121
118
|
# Set user packet size if TCP connection is available
|
|
122
|
-
if !@force_udp && @helper.test_tcp
|
|
119
|
+
if !@force_udp && @helper.test_tcp.zero?
|
|
123
120
|
@user_packet_size = 72 # Default for ZK8
|
|
124
121
|
end
|
|
125
122
|
|
|
@@ -129,9 +126,7 @@ module RBZK
|
|
|
129
126
|
@session_id = 0
|
|
130
127
|
@reply_id = USHRT_MAX - 1
|
|
131
128
|
|
|
132
|
-
if @verbose
|
|
133
|
-
puts "Sending connect command to device"
|
|
134
|
-
end
|
|
129
|
+
puts 'Sending connect command to device' if @verbose
|
|
135
130
|
|
|
136
131
|
begin
|
|
137
132
|
cmd_response = send_command(CMD_CONNECT)
|
|
@@ -139,9 +134,7 @@ module RBZK
|
|
|
139
134
|
|
|
140
135
|
# Authenticate if needed
|
|
141
136
|
if cmd_response[:code] == CMD_ACK_UNAUTH
|
|
142
|
-
if @verbose
|
|
143
|
-
puts "try auth"
|
|
144
|
-
end
|
|
137
|
+
puts 'try auth' if @verbose
|
|
145
138
|
|
|
146
139
|
command_string = make_commkey(@password, @session_id)
|
|
147
140
|
cmd_response = send_command(CMD_AUTH, command_string)
|
|
@@ -150,23 +143,17 @@ module RBZK
|
|
|
150
143
|
# Check response status
|
|
151
144
|
if cmd_response[:status]
|
|
152
145
|
@connected = true
|
|
153
|
-
|
|
146
|
+
self
|
|
154
147
|
else
|
|
155
|
-
if cmd_response[:code] == CMD_ACK_UNAUTH
|
|
156
|
-
raise RBZK::ZKErrorResponse, "Unauthenticated"
|
|
157
|
-
end
|
|
148
|
+
raise RBZK::ZKErrorResponse, 'Unauthenticated' if cmd_response[:code] == CMD_ACK_UNAUTH
|
|
158
149
|
|
|
159
|
-
if @verbose
|
|
160
|
-
puts "Connect error response: #{cmd_response[:code]}"
|
|
161
|
-
end
|
|
150
|
+
puts "Connect error response: #{cmd_response[:code]}" if @verbose
|
|
162
151
|
|
|
163
152
|
raise RBZK::ZKErrorResponse, "Invalid response: Can't connect"
|
|
164
153
|
end
|
|
165
|
-
rescue => e
|
|
154
|
+
rescue StandardError => e
|
|
166
155
|
@connected = false
|
|
167
|
-
if @verbose
|
|
168
|
-
puts "Connection error: #{e.message}"
|
|
169
|
-
end
|
|
156
|
+
puts "Connection error: #{e.message}" if @verbose
|
|
170
157
|
raise e
|
|
171
158
|
end
|
|
172
159
|
end
|
|
@@ -178,11 +165,11 @@ module RBZK
|
|
|
178
165
|
def disconnect
|
|
179
166
|
return unless @connected
|
|
180
167
|
|
|
181
|
-
|
|
182
|
-
|
|
168
|
+
send_command(CMD_EXIT)
|
|
169
|
+
recv_reply
|
|
183
170
|
|
|
184
171
|
@connected = false
|
|
185
|
-
@socket
|
|
172
|
+
@socket&.close
|
|
186
173
|
|
|
187
174
|
@socket = nil
|
|
188
175
|
@tcp = nil
|
|
@@ -191,32 +178,28 @@ module RBZK
|
|
|
191
178
|
end
|
|
192
179
|
|
|
193
180
|
def enable_device
|
|
194
|
-
|
|
195
|
-
|
|
181
|
+
send_command(CMD_ENABLEDEVICE)
|
|
182
|
+
recv_reply
|
|
196
183
|
true
|
|
197
184
|
end
|
|
198
185
|
|
|
199
186
|
def disable_device
|
|
200
|
-
cmd_response =
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
raise RBZK::ZKErrorResponse, "Can't disable device"
|
|
206
|
-
end
|
|
187
|
+
cmd_response = send_command(CMD_DISABLEDEVICE)
|
|
188
|
+
raise RBZK::ZKErrorResponse, "Can't disable device" unless cmd_response[:status]
|
|
189
|
+
|
|
190
|
+
@is_enabled = false
|
|
191
|
+
true
|
|
207
192
|
end
|
|
208
193
|
|
|
209
194
|
def get_firmware_version
|
|
210
195
|
command = CMD_GET_VERSION
|
|
211
196
|
response_size = 1024
|
|
212
|
-
response =
|
|
197
|
+
response = send_command(command, '', response_size)
|
|
213
198
|
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
raise RBZK::ZKErrorResponse, "Can't read firmware version"
|
|
219
|
-
end
|
|
199
|
+
raise RBZK::ZKErrorResponse, "Can't read firmware version" unless response && response[:status]
|
|
200
|
+
|
|
201
|
+
firmware_version = @data.split("\x00")[0]
|
|
202
|
+
firmware_version.to_s
|
|
220
203
|
end
|
|
221
204
|
|
|
222
205
|
def get_serialnumber
|
|
@@ -224,15 +207,14 @@ module RBZK
|
|
|
224
207
|
command_string = "~SerialNumber\x00".b
|
|
225
208
|
response_size = 1024
|
|
226
209
|
|
|
227
|
-
response =
|
|
210
|
+
response = send_command(command, command_string, response_size)
|
|
228
211
|
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
end
|
|
212
|
+
raise RBZK::ZKErrorResponse, "Can't read serial number" unless response && response[:status]
|
|
213
|
+
|
|
214
|
+
serialnumber_field = @data.split('=', 2)[1]
|
|
215
|
+
serialnumber = serialnumber_field ? serialnumber_field.split("\x00")[0] : ''
|
|
216
|
+
serialnumber = serialnumber.gsub('=', '')
|
|
217
|
+
serialnumber.to_s
|
|
236
218
|
end
|
|
237
219
|
|
|
238
220
|
def get_mac
|
|
@@ -240,14 +222,12 @@ module RBZK
|
|
|
240
222
|
command_string = "MAC\x00".b
|
|
241
223
|
response_size = 1024
|
|
242
224
|
|
|
243
|
-
response =
|
|
225
|
+
response = send_command(command, command_string, response_size)
|
|
244
226
|
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
raise RBZK::ZKErrorResponse, "Can't read MAC address"
|
|
250
|
-
end
|
|
227
|
+
raise RBZK::ZKErrorResponse, "Can't read MAC address" unless response && response[:status]
|
|
228
|
+
|
|
229
|
+
mac = @data.split('=', 2)[1]&.split("\x00")&.[](0) || ''
|
|
230
|
+
mac.to_s
|
|
251
231
|
end
|
|
252
232
|
|
|
253
233
|
def get_device_name
|
|
@@ -255,13 +235,14 @@ module RBZK
|
|
|
255
235
|
command_string = "~DeviceName\x00".b
|
|
256
236
|
response_size = 1024
|
|
257
237
|
|
|
258
|
-
response =
|
|
238
|
+
response = send_command(command, command_string, response_size)
|
|
259
239
|
|
|
260
240
|
if response && response[:status]
|
|
261
|
-
|
|
241
|
+
device_field = @data.split('=', 2)[1]
|
|
242
|
+
device = device_field ? device_field.split("\x00")[0] : ''
|
|
262
243
|
device.to_s
|
|
263
244
|
else
|
|
264
|
-
|
|
245
|
+
''
|
|
265
246
|
end
|
|
266
247
|
end
|
|
267
248
|
|
|
@@ -270,13 +251,16 @@ module RBZK
|
|
|
270
251
|
command_string = "ZKFaceVersion\x00".b
|
|
271
252
|
response_size = 1024
|
|
272
253
|
|
|
273
|
-
response =
|
|
254
|
+
response = send_command(command, command_string, response_size)
|
|
274
255
|
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
256
|
+
return unless response && response[:status]
|
|
257
|
+
|
|
258
|
+
version_field = @data.split('=', 2)[1]
|
|
259
|
+
version = version_field ? version_field.split("\x00")[0] : ''
|
|
260
|
+
begin
|
|
261
|
+
version.to_i
|
|
262
|
+
rescue StandardError
|
|
263
|
+
0
|
|
280
264
|
end
|
|
281
265
|
end
|
|
282
266
|
|
|
@@ -285,13 +269,16 @@ module RBZK
|
|
|
285
269
|
command_string = "~ExtendFmt\x00".b
|
|
286
270
|
response_size = 1024
|
|
287
271
|
|
|
288
|
-
response =
|
|
272
|
+
response = send_command(command, command_string, response_size)
|
|
289
273
|
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
274
|
+
return unless response && response[:status]
|
|
275
|
+
|
|
276
|
+
fmt_field = @data.split('=', 2)[1]
|
|
277
|
+
fmt = fmt_field ? fmt_field.split("\x00")[0] : ''
|
|
278
|
+
begin
|
|
279
|
+
fmt.to_i
|
|
280
|
+
rescue StandardError
|
|
281
|
+
0
|
|
295
282
|
end
|
|
296
283
|
end
|
|
297
284
|
|
|
@@ -300,15 +287,14 @@ module RBZK
|
|
|
300
287
|
command_string = "~Platform\x00".b
|
|
301
288
|
response_size = 1024
|
|
302
289
|
|
|
303
|
-
response =
|
|
290
|
+
response = send_command(command, command_string, response_size)
|
|
304
291
|
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
end
|
|
292
|
+
raise RBZK::ZKErrorResponse, "Can't read platform name" unless response && response[:status]
|
|
293
|
+
|
|
294
|
+
platform_field = @data.split('=', 2)[1]
|
|
295
|
+
platform = platform_field ? platform_field.split("\x00")[0] : ''
|
|
296
|
+
platform = platform.gsub('=', '')
|
|
297
|
+
platform.to_s
|
|
312
298
|
end
|
|
313
299
|
|
|
314
300
|
def get_fp_version
|
|
@@ -316,32 +302,35 @@ module RBZK
|
|
|
316
302
|
command_string = "~ZKFPVersion\x00".b
|
|
317
303
|
response_size = 1024
|
|
318
304
|
|
|
319
|
-
response =
|
|
305
|
+
response = send_command(command, command_string, response_size)
|
|
320
306
|
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
307
|
+
raise RBZK::ZKErrorResponse, "Can't read fingerprint version" unless response && response[:status]
|
|
308
|
+
|
|
309
|
+
version_field = @data.split('=', 2)[1]
|
|
310
|
+
version = version_field ? version_field.split("\x00")[0] : ''
|
|
311
|
+
version = version.gsub('=', '')
|
|
312
|
+
begin
|
|
313
|
+
version.to_i
|
|
314
|
+
rescue StandardError
|
|
315
|
+
0
|
|
327
316
|
end
|
|
328
317
|
end
|
|
329
318
|
|
|
330
319
|
def restart
|
|
331
|
-
|
|
332
|
-
|
|
320
|
+
send_command(CMD_RESTART)
|
|
321
|
+
recv_reply
|
|
333
322
|
true
|
|
334
323
|
end
|
|
335
324
|
|
|
336
325
|
def poweroff
|
|
337
|
-
|
|
338
|
-
|
|
326
|
+
send_command(CMD_POWEROFF)
|
|
327
|
+
recv_reply
|
|
339
328
|
true
|
|
340
329
|
end
|
|
341
330
|
|
|
342
331
|
def test_voice(index = 0)
|
|
343
332
|
command_string = [ index ].pack('L<')
|
|
344
|
-
response =
|
|
333
|
+
response = send_command(CMD_TESTVOICE, command_string)
|
|
345
334
|
|
|
346
335
|
if response && response[:status]
|
|
347
336
|
true
|
|
@@ -354,20 +343,18 @@ module RBZK
|
|
|
354
343
|
# @param time [Integer] define delay in seconds
|
|
355
344
|
# @return [Boolean] true if successful, raises exception otherwise
|
|
356
345
|
def unlock(time = 3)
|
|
357
|
-
command_string = [time * 10].pack('L<')
|
|
358
|
-
response =
|
|
346
|
+
command_string = [ time * 10 ].pack('L<')
|
|
347
|
+
response = send_command(CMD_UNLOCK, command_string)
|
|
359
348
|
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
raise RBZK::ZKErrorResponse, "Can't open door"
|
|
364
|
-
end
|
|
349
|
+
raise RBZK::ZKErrorResponse, "Can't open door" unless response && response[:status]
|
|
350
|
+
|
|
351
|
+
true
|
|
365
352
|
end
|
|
366
353
|
|
|
367
354
|
# Get the lock state
|
|
368
355
|
# @return [Boolean] true if door is open, false otherwise
|
|
369
356
|
def get_lock_state
|
|
370
|
-
response =
|
|
357
|
+
response = send_command(CMD_DOORSTATE_RRQ)
|
|
371
358
|
|
|
372
359
|
if response && response[:status]
|
|
373
360
|
true
|
|
@@ -381,38 +368,32 @@ module RBZK
|
|
|
381
368
|
# @param text [String] text to write
|
|
382
369
|
# @return [Boolean] true if successful, raises exception otherwise
|
|
383
370
|
def write_lcd(line_number, text)
|
|
384
|
-
command_string = [line_number, 0].pack('s<c')
|
|
385
|
-
response =
|
|
371
|
+
command_string = "#{[ line_number, 0 ].pack('s<c')} #{text.encode(@encoding, invalid: :replace, undef: :replace)}"
|
|
372
|
+
response = send_command(CMD_WRITE_LCD, command_string)
|
|
386
373
|
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
raise RBZK::ZKErrorResponse, "Can't write lcd"
|
|
391
|
-
end
|
|
374
|
+
raise RBZK::ZKErrorResponse, "Can't write lcd" unless response && response[:status]
|
|
375
|
+
|
|
376
|
+
true
|
|
392
377
|
end
|
|
393
378
|
|
|
394
379
|
# Clear LCD
|
|
395
380
|
# @return [Boolean] true if successful, raises exception otherwise
|
|
396
381
|
def clear_lcd
|
|
397
|
-
response =
|
|
382
|
+
response = send_command(CMD_CLEAR_LCD)
|
|
398
383
|
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
raise RBZK::ZKErrorResponse, "Can't clear lcd"
|
|
403
|
-
end
|
|
384
|
+
raise RBZK::ZKErrorResponse, "Can't clear lcd" unless response && response[:status]
|
|
385
|
+
|
|
386
|
+
true
|
|
404
387
|
end
|
|
405
388
|
|
|
406
389
|
# Refresh the device data
|
|
407
390
|
# @return [Boolean] true if successful, raises exception otherwise
|
|
408
391
|
def refresh_data
|
|
409
|
-
response =
|
|
392
|
+
response = send_command(CMD_REFRESHDATA)
|
|
410
393
|
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
raise RBZK::ZKErrorResponse, "Can't refresh data"
|
|
415
|
-
end
|
|
394
|
+
raise RBZK::ZKErrorResponse, "Can't refresh data" unless response && response[:status]
|
|
395
|
+
|
|
396
|
+
true
|
|
416
397
|
end
|
|
417
398
|
|
|
418
399
|
# Create or update user by uid
|
|
@@ -425,23 +406,17 @@ module RBZK
|
|
|
425
406
|
# @param card [Integer] card number
|
|
426
407
|
# @return [Boolean] true if successful, raises exception otherwise
|
|
427
408
|
def set_user(uid: nil, name: '', privilege: 0, password: '', group_id: '', user_id: '', card: 0)
|
|
428
|
-
# If uid is not provided, use next_uid
|
|
429
409
|
if uid.nil?
|
|
410
|
+
ensure_user_metadata!
|
|
430
411
|
uid = @next_uid
|
|
431
|
-
if user_id.empty?
|
|
432
|
-
user_id = @next_user_id
|
|
433
|
-
end
|
|
412
|
+
user_id = @next_user_id if user_id.empty? && @next_user_id
|
|
434
413
|
end
|
|
435
414
|
|
|
436
|
-
# If
|
|
437
|
-
if user_id.empty?
|
|
438
|
-
user_id = uid.to_s # ZK6 needs uid2 == uid
|
|
439
|
-
end
|
|
415
|
+
# If uid is not provided, use next_uid
|
|
416
|
+
user_id = uid.to_s if user_id.nil? || user_id.empty? # ZK6 needs uid2 == uid
|
|
440
417
|
|
|
441
418
|
# Validate privilege
|
|
442
|
-
if privilege != USER_DEFAULT && privilege != USER_ADMIN
|
|
443
|
-
privilege = USER_DEFAULT
|
|
444
|
-
end
|
|
419
|
+
privilege = USER_DEFAULT if privilege != USER_DEFAULT && privilege != USER_ADMIN
|
|
445
420
|
privilege = privilege.to_i
|
|
446
421
|
|
|
447
422
|
# Create command string based on user_packet_size
|
|
@@ -449,73 +424,79 @@ module RBZK
|
|
|
449
424
|
group_id = 0 if group_id.empty?
|
|
450
425
|
|
|
451
426
|
begin
|
|
452
|
-
command_string = [uid, privilege].pack('S<C') +
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
rescue => e
|
|
457
|
-
if @verbose
|
|
458
|
-
puts "Error packing user: #{e.message}"
|
|
459
|
-
end
|
|
427
|
+
command_string = [ uid, privilege ].pack('S<C') +
|
|
428
|
+
password.encode(@encoding, invalid: :replace, undef: :replace).ljust(5, "\x00")[0...5] +
|
|
429
|
+
name.encode(@encoding, invalid: :replace, undef: :replace).ljust(8, "\x00")[0...8] +
|
|
430
|
+
[ card.to_i, 0, group_id.to_i, 0, user_id.to_i ].pack('L<CS<S<L<')
|
|
431
|
+
rescue StandardError => e
|
|
432
|
+
puts "Error packing user: #{e.message}" if @verbose
|
|
460
433
|
raise RBZK::ZKErrorResponse, "Can't pack user"
|
|
461
434
|
end
|
|
462
435
|
else
|
|
463
436
|
# For other firmware versions
|
|
464
437
|
name_pad = name.encode(@encoding, invalid: :replace, undef: :replace).ljust(24, "\x00")[0...24]
|
|
465
|
-
card_str = [card.to_i].pack('L<')[0...4]
|
|
466
|
-
command_string = [uid,
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
user_id.encode(@encoding, invalid: :replace, undef: :replace).ljust(24, "\x00")[0...24]
|
|
438
|
+
card_str = [ card.to_i ].pack('L<')[0...4]
|
|
439
|
+
command_string = "#{[ uid,
|
|
440
|
+
privilege ].pack('S<C')}#{password.encode(@encoding, invalid: :replace, undef: :replace).ljust(8,
|
|
441
|
+
"\x00")[0...8]}#{name_pad}#{card_str}\u0000#{group_id.encode(@encoding, invalid: :replace, undef: :replace).ljust(7,
|
|
442
|
+
"\x00")[0...7]}\u0000#{user_id.encode(@encoding, invalid: :replace, undef: :replace).ljust(
|
|
443
|
+
24, "\x00"
|
|
444
|
+
)[0...24]}"
|
|
473
445
|
end
|
|
474
446
|
|
|
475
447
|
# Send command
|
|
476
|
-
response =
|
|
448
|
+
response = send_command(CMD_USER_WRQ, command_string, 1024)
|
|
477
449
|
|
|
478
450
|
if response && response[:status]
|
|
479
451
|
# Update next_uid and next_user_id if necessary
|
|
480
|
-
|
|
481
|
-
if @next_uid == uid
|
|
482
|
-
|
|
483
|
-
end
|
|
484
|
-
if @next_user_id == user_id
|
|
485
|
-
@next_user_id = @next_uid.to_s
|
|
486
|
-
end
|
|
452
|
+
refresh_data
|
|
453
|
+
@next_uid += 1 if @next_uid == uid
|
|
454
|
+
@next_user_id = @next_uid.to_s if @next_user_id == user_id
|
|
487
455
|
true
|
|
488
456
|
else
|
|
489
|
-
|
|
457
|
+
code = response[:code] if response
|
|
458
|
+
data_msg = @data && !@data.empty? ? " Data: #{format_as_python_bytes(@data)}" : ''
|
|
459
|
+
raise RBZK::ZKErrorResponse, "Can't set user (device response: #{code || 'NO_CODE'})#{data_msg}"
|
|
490
460
|
end
|
|
491
461
|
end
|
|
492
462
|
|
|
463
|
+
def ensure_user_metadata!
|
|
464
|
+
@next_uid ||= 1
|
|
465
|
+
@next_user_id ||= '1'
|
|
466
|
+
@user_packet_size ||= 28
|
|
467
|
+
|
|
468
|
+
return unless @next_uid <= 1 || @next_user_id.nil? || @user_packet_size.nil?
|
|
469
|
+
|
|
470
|
+
get_users
|
|
471
|
+
rescue StandardError => e
|
|
472
|
+
puts "Warning: unable to refresh user metadata before creating user: #{e.message}" if @verbose
|
|
473
|
+
@next_uid ||= 1
|
|
474
|
+
@next_user_id ||= '1'
|
|
475
|
+
@user_packet_size ||= 28
|
|
476
|
+
end
|
|
477
|
+
private :ensure_user_metadata!
|
|
478
|
+
|
|
493
479
|
# Delete user by uid
|
|
494
480
|
# @param uid [Integer] user ID that are generated from device
|
|
495
481
|
# @return [Boolean] true if successful, raises exception otherwise
|
|
496
482
|
def delete_user(uid: 0)
|
|
497
483
|
# Send command
|
|
498
|
-
command_string = [uid].pack('S<')
|
|
499
|
-
response =
|
|
484
|
+
command_string = [ uid ].pack('S<')
|
|
485
|
+
response = send_command(CMD_DELETE_USER, command_string)
|
|
500
486
|
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
# Update next_uid if necessary
|
|
504
|
-
if uid == (@next_uid - 1)
|
|
505
|
-
@next_uid = uid
|
|
506
|
-
end
|
|
507
|
-
true
|
|
508
|
-
else
|
|
509
|
-
raise RBZK::ZKErrorResponse, "Can't delete user"
|
|
487
|
+
unless response && response[:status]
|
|
488
|
+
raise RBZK::ZKErrorResponse, "Can't delete user. User not found or other error."
|
|
510
489
|
end
|
|
490
|
+
|
|
491
|
+
refresh_data
|
|
492
|
+
# Update next_uid if necessary
|
|
493
|
+
@next_uid = uid if uid == (@next_uid - 1)
|
|
494
|
+
true
|
|
511
495
|
end
|
|
512
496
|
|
|
513
497
|
# Helper method to read data with buffer (ZK6: 1503)
|
|
514
498
|
def read_with_buffer(command, fct = 0, ext = 0)
|
|
515
|
-
|
|
516
|
-
if @verbose
|
|
517
|
-
puts "Reading data with buffer: command=#{command}, fct=#{fct}, ext=#{ext}"
|
|
518
|
-
end
|
|
499
|
+
puts "Reading data with buffer: command=#{command}, fct=#{fct}, ext=#{ext}" if @verbose
|
|
519
500
|
|
|
520
501
|
# Set max chunk size based on connection type
|
|
521
502
|
max_chunk = @tcp ? 0xFFc0 : 16 * 1024
|
|
@@ -525,54 +506,39 @@ module RBZK
|
|
|
525
506
|
# All in little-endian format
|
|
526
507
|
command_string = [ 1, command, fct, ext ].pack('cs<l<l<')
|
|
527
508
|
|
|
528
|
-
if @verbose
|
|
529
|
-
puts "Command string: #{python_format(command_string)}"
|
|
530
|
-
end
|
|
509
|
+
puts "Command string: #{python_format(command_string)}" if @verbose
|
|
531
510
|
|
|
532
511
|
# Send the command to prepare the buffer
|
|
533
512
|
response_size = 1024
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
response = self.send_command(CMD_PREPARE_BUFFER, command_string, response_size)
|
|
513
|
+
# local variables
|
|
514
|
+
response = send_command(CMD_PREPARE_BUFFER, command_string, response_size)
|
|
537
515
|
|
|
538
|
-
if !response || !response[:status]
|
|
539
|
-
raise RBZK::ZKErrorResponse, "Read with buffer not supported"
|
|
540
|
-
end
|
|
516
|
+
raise RBZK::ZKErrorResponse, 'Read with buffer not supported' if !response || !response[:status]
|
|
541
517
|
|
|
542
518
|
# Get data from the response
|
|
543
519
|
data = @data
|
|
544
520
|
|
|
545
|
-
if @verbose
|
|
546
|
-
puts "Received #{data.size} bytes of data"
|
|
547
|
-
end
|
|
521
|
+
puts "Received #{data.size} bytes of data" if @verbose
|
|
548
522
|
|
|
549
523
|
# Check if we need more data
|
|
550
524
|
if response[:code] == CMD_DATA
|
|
551
525
|
if @tcp
|
|
552
|
-
if @verbose
|
|
553
|
-
puts "DATA! is #{data.size} bytes, tcp length is #{@tcp_length}"
|
|
554
|
-
end
|
|
526
|
+
puts "DATA! is #{data.size} bytes, tcp length is #{@tcp_length}" if @verbose
|
|
555
527
|
|
|
556
528
|
if data.size < (@tcp_length - 8)
|
|
557
529
|
need = (@tcp_length - 8) - data.size
|
|
558
|
-
if @verbose
|
|
559
|
-
puts "need more data: #{need}"
|
|
560
|
-
end
|
|
530
|
+
puts "need more data: #{need}" if @verbose
|
|
561
531
|
|
|
562
532
|
# Receive more data to complete the buffer
|
|
563
533
|
more_data = receive_raw_data(need)
|
|
564
534
|
|
|
565
|
-
if @verbose
|
|
566
|
-
puts "Read #{more_data.size} more bytes"
|
|
567
|
-
end
|
|
535
|
+
puts "Read #{more_data.size} more bytes" if @verbose
|
|
568
536
|
|
|
569
537
|
# Combine the data
|
|
570
538
|
result = data + more_data
|
|
571
539
|
return result, data.size + more_data.size
|
|
572
540
|
else
|
|
573
|
-
if @verbose
|
|
574
|
-
puts "Enough data"
|
|
575
|
-
end
|
|
541
|
+
puts 'Enough data' if @verbose
|
|
576
542
|
size = data.size
|
|
577
543
|
return data, size
|
|
578
544
|
end
|
|
@@ -583,38 +549,30 @@ module RBZK
|
|
|
583
549
|
end
|
|
584
550
|
|
|
585
551
|
# Get the size from the first 4 bytes (unsigned long, little-endian)
|
|
586
|
-
size = data[1..4].
|
|
552
|
+
size = data[1..4].unpack1('L<')
|
|
587
553
|
|
|
588
|
-
if @verbose
|
|
589
|
-
puts "size fill be #{size}"
|
|
590
|
-
end
|
|
554
|
+
puts "size fill be #{size}" if @verbose
|
|
591
555
|
|
|
592
556
|
# Calculate chunks
|
|
593
557
|
remain = size % max_chunk
|
|
594
558
|
# Calculate number of full-sized packets (integer division)
|
|
595
559
|
packets = (size - remain).div(max_chunk)
|
|
596
560
|
|
|
597
|
-
if @verbose
|
|
598
|
-
puts "rwb: ##{packets} packets of max #{max_chunk} bytes, and extra #{remain} bytes remain"
|
|
599
|
-
end
|
|
561
|
+
puts "rwb: ##{packets} packets of max #{max_chunk} bytes, and extra #{remain} bytes remain" if @verbose
|
|
600
562
|
|
|
601
563
|
# Read chunks
|
|
602
564
|
result_data = []
|
|
603
565
|
start = 0
|
|
604
566
|
|
|
605
567
|
packets.times do
|
|
606
|
-
if @verbose
|
|
607
|
-
puts "recieve chunk: prepare data size is #{max_chunk}"
|
|
608
|
-
end
|
|
568
|
+
puts "recieve chunk: prepare data size is #{max_chunk}" if @verbose
|
|
609
569
|
chunk = read_chunk(start, max_chunk)
|
|
610
570
|
result_data << chunk
|
|
611
571
|
start += max_chunk
|
|
612
572
|
end
|
|
613
573
|
|
|
614
|
-
if remain
|
|
615
|
-
if @verbose
|
|
616
|
-
puts "recieve chunk: prepare data size is #{remain}"
|
|
617
|
-
end
|
|
574
|
+
if remain.positive?
|
|
575
|
+
puts "recieve chunk: prepare data size is #{remain}" if @verbose
|
|
618
576
|
chunk = read_chunk(start, remain)
|
|
619
577
|
result_data << chunk
|
|
620
578
|
start += remain
|
|
@@ -623,9 +581,7 @@ module RBZK
|
|
|
623
581
|
# Free data (equivalent to Python's self.free_data())
|
|
624
582
|
free_data
|
|
625
583
|
|
|
626
|
-
if @verbose
|
|
627
|
-
puts "_read w/chunk #{start} bytes"
|
|
628
|
-
end
|
|
584
|
+
puts "_read w/chunk #{start} bytes" if @verbose
|
|
629
585
|
|
|
630
586
|
# In Python: return b''.join(data), start
|
|
631
587
|
result = result_data.join
|
|
@@ -635,8 +591,8 @@ module RBZK
|
|
|
635
591
|
# Helper method to get data size from the current data
|
|
636
592
|
def get_data_size
|
|
637
593
|
if @data && @data.size >= 4
|
|
638
|
-
|
|
639
|
-
|
|
594
|
+
@data[0...4].unpack1('L<')
|
|
595
|
+
|
|
640
596
|
else
|
|
641
597
|
0
|
|
642
598
|
end
|
|
@@ -650,12 +606,12 @@ module RBZK
|
|
|
650
606
|
puts "tcp_length #{tcp_length}, size #{size}" if @verbose
|
|
651
607
|
|
|
652
608
|
if tcp_length <= 0
|
|
653
|
-
puts
|
|
654
|
-
return nil,
|
|
609
|
+
puts 'Incorrect tcp packet' if @verbose
|
|
610
|
+
return nil, ''.b
|
|
655
611
|
end
|
|
656
612
|
|
|
657
613
|
if (tcp_length - 8) < size
|
|
658
|
-
puts
|
|
614
|
+
puts 'tcp length too small... retrying' if @verbose
|
|
659
615
|
|
|
660
616
|
# Recursive call to handle smaller packet
|
|
661
617
|
resp, bh = receive_tcp_data(data_recv, tcp_length - 8)
|
|
@@ -684,7 +640,7 @@ module RBZK
|
|
|
684
640
|
|
|
685
641
|
# In Python: response = unpack('HHHH', data_recv[8:16])[0]
|
|
686
642
|
# This unpacks 4 shorts (8 bytes) but only uses the first one
|
|
687
|
-
response = data_recv[8...16].
|
|
643
|
+
response = data_recv[8...16].unpack1('S<S<S<S<')
|
|
688
644
|
|
|
689
645
|
if received >= (size + 32)
|
|
690
646
|
if response == CMD_DATA
|
|
@@ -692,28 +648,26 @@ module RBZK
|
|
|
692
648
|
|
|
693
649
|
puts "resp complete len #{resp.size}" if @verbose
|
|
694
650
|
|
|
695
|
-
|
|
651
|
+
[ resp, data_recv[(size + 16)..-1] ]
|
|
696
652
|
else
|
|
697
653
|
puts "incorrect response!!! #{response}" if @verbose
|
|
698
654
|
|
|
699
|
-
|
|
655
|
+
[ nil, ''.b ]
|
|
700
656
|
end
|
|
701
657
|
else
|
|
702
658
|
puts "try DATA incomplete (actual valid #{received - 16})" if @verbose
|
|
703
659
|
|
|
704
660
|
data << data_recv[16...(size + 16)]
|
|
705
661
|
size -= received - 16
|
|
706
|
-
broken_header =
|
|
662
|
+
broken_header = ''.b
|
|
707
663
|
|
|
708
|
-
if size
|
|
709
|
-
broken_header = data_recv[size
|
|
664
|
+
if size.negative?
|
|
665
|
+
broken_header = data_recv[size..-1]
|
|
710
666
|
|
|
711
|
-
if @verbose
|
|
712
|
-
puts "broken: #{broken_header.bytes.map { |b| format('%02x', b) }.join}"
|
|
713
|
-
end
|
|
667
|
+
puts "broken: #{broken_header.bytes.map { |b| format('%02x', b) }.join}" if @verbose
|
|
714
668
|
end
|
|
715
669
|
|
|
716
|
-
if size
|
|
670
|
+
if size.positive?
|
|
717
671
|
data_recv = receive_raw_data(size)
|
|
718
672
|
data << data_recv
|
|
719
673
|
end
|
|
@@ -732,14 +686,14 @@ module RBZK
|
|
|
732
686
|
need = (@tcp_length - 8) - @data.size
|
|
733
687
|
puts "need more data: #{need}" if @verbose
|
|
734
688
|
more_data = receive_raw_data(need)
|
|
735
|
-
|
|
689
|
+
@data + more_data
|
|
736
690
|
else
|
|
737
|
-
puts
|
|
738
|
-
|
|
691
|
+
puts 'Enough data' if @verbose
|
|
692
|
+
@data
|
|
739
693
|
end
|
|
740
694
|
else
|
|
741
695
|
puts "_rc len is #{@data.size}" if @verbose
|
|
742
|
-
|
|
696
|
+
@data
|
|
743
697
|
end
|
|
744
698
|
elsif @response == CMD_PREPARE_DATA
|
|
745
699
|
data = []
|
|
@@ -749,10 +703,10 @@ module RBZK
|
|
|
749
703
|
|
|
750
704
|
if @tcp
|
|
751
705
|
if @data.size >= (8 + size)
|
|
752
|
-
data_recv = @data[8
|
|
706
|
+
data_recv = @data[8..-1]
|
|
753
707
|
else
|
|
754
708
|
# [80, 80, 130, 125, 92, 7, 0, 0, 221, 5, 142, 172, 0, 0, 4, 0, 80, 7, 0, 0, 1, 0, 14, 0, 0, 0, 0, 0, 0, 0, 0, 65, 98, 100, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 49, 0, 0, 0, 1, 0, 0, 0, 25, 0, 0, 0, 0, 254, 6, 119, 255, 255, 255, 255, 212, 10, 159, 127, 2, 0, 14, 0, 0, 0, 0, 0, 0, 0, 0, 65, 110, 97, 115, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 50, 0, 0, 0, 1, 0, 0, 0, 25, 0, 0, 0, 0, 254, 6, 119, 255, 255, 255, 255, 212, 10, 159, 127, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 83, 111, 110, 100, 111, 115, 44, 65, 98, 117, 107, 104, 100, 97, 105, 114, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 51, 0, 0, 0, 1, 0, 0, 0, 25, 0, 0, 0, 0, 254, 6, 119, 255, 255, 255, 255, 212, 10, 159, 127, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 65, 116, 97, 0, 111, 115, 44, 65, 98, 117, 107, 104, 100, 97, 105, 114, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 52, 0, 0, 0, 1, 0, 0, 0, 25, 0, 0, 0, 0, 254, 6, 119, 255, 255, 255, 255, 212, 10, 159, 127, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 77, 97, 121, 115, 0, 115, 44, 65, 98, 117, 107, 104, 100, 97, 105, 114, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 53, 0, 0, 0, 1, 0, 0, 0, 25, 0, 0, 0, 0, 254, 6, 119, 255, 255, 255, 255, 212, 10, 159, 127, 6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 82, 97, 119, 97, 110, 0, 44, 65, 98, 117, 107, 104, 100, 97, 105, 114, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 54, 0, 0, 0, 1, 0, 0, 0, 25, 0, 0, 0, 0, 254, 6, 119, 255, 255, 255, 255, 212, 10, 159, 127, 7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 74, 101, 110, 97, 110, 0, 44, 65, 98, 117, 107, 104, 100, 97, 105, 114, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 55, 0, 0, 0, 1, 0, 0, 0, 25, 0, 0, 0, 0, 254, 6, 119, 255, 255, 255, 255, 212, 10, 159, 127, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 70, 97, 114, 97, 104, 0, 44, 65, 98, 117, 107, 104, 100, 97, 105, 114, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 56, 0, 0, 0, 1, 0, 0, 0, 25, 0, 0, 0, 0, 254, 6, 119, 255, 255, 255, 255, 212, 10, 159, 127, 9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 83, 97, 98, 114, 101, 101, 110, 0, 98, 117, 107, 104, 100, 97, 105, 114, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 57, 0, 0, 0, 1, 0, 0, 0, 25, 0, 0, 0, 0, 254, 6, 119, 255, 255, 255, 255, 212, 10, 159, 127, 10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 83, 97, 101, 101, 100, 0, 110, 0, 98, 117, 107, 104, 100, 97, 105, 114, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 49, 48, 0, 0, 1, 0, 0, 0, 25, 0, 0, 0, 0, 254, 6, 119, 255, 255, 255, 255, 212, 10, 159, 127, 11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 71, 111, 102, 114, 97, 110, 0, 0, 98, 117, 107, 104, 100, 97, 105, 114, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 49, 49, 0, 0, 1, 0, 0, 0, 25, 0, 0, 0, 0, 254, 6, 119, 255, 255, 255, 255, 212, 10, 159, 127, 12, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 68, 97, 110, 105, 97, 0, 0, 0, 98, 117, 107, 104, 100, 97, 105, 114, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 49, 50, 0, 0, 1, 0, 0, 0, 25, 0, 0, 0, 0, 254, 6, 119, 255, 255, 255, 255, 212, 10, 159, 127, 13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 83, 97, 109, 105, 97, 0, 0, 0, 98, 117, 107, 104, 100, 97, 105, 114, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 49, 51, 0, 0, 1, 0, 0, 0, 25, 0, 0, 0, 0, 254, 6, 119, 255, 255, 255, 255, 212, 10, 159, 127, 14, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 82, 101, 101, 109, 0, 0, 0, 0, 98, 117, 107, 104, 100, 97, 105, 114, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 49, 52, 0, 0, 1, 0, 0, 0, 25, 0, 0, 0, 0, 254, 6, 119, 255, 255, 255, 255, 212, 10, 159, 127, 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 87, 97, 108, 97, 97, 0, 0, 0, 98, 117, 107, 104, 100, 97, 105, 114, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 49, 53, 0, 0, 1, 0, 0, 0, 25, 0, 0, 0, 0, 254, 6, 119, 255, 255, 255, 255, 212, 10, 159, 127, 16, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 82, 97, 110, 101, 101, 109, 0, 0, 98, 117, 107, 104, 100, 97, 105, 114, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 49, 54, 0, 0, 1, 0, 0, 0, 25, 0, 0, 0, 0, 254, 6, 119, 255, 255, 255, 255, 212, 10, 159, 127, 17, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 79, 108, 97, 0, 101, 109, 0, 0, 98, 117, 107, 104, 100, 97, 105, 114, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 49, 55, 0, 0, 1, 0, 0, 0, 25, 0, 0, 0, 0, 254, 6, 119, 255, 255, 255, 255, 212, 10, 159, 127, 18, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 77, 97, 114, 97, 104, 0, 0, 0, 98, 117, 107, 104, 100, 97, 105, 114, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 49, 56, 0, 0, 1, 0, 0, 0, 25, 0, 0, 0, 0, 254, 6, 119, 255, 255, 255, 255, 212, 10, 159, 127, 19, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 83, 111, 110, 100, 111, 115, 72, 65, 0, 117, 107, 104, 100, 97, 105, 114, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 49, 57, 0, 0, 1, 0, 0, 0, 25, 0, 0, 0, 0, 254, 6, 119, 255, 255, 255, 255, 212, 10, 159, 127, 20, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 77, 111, 0, 100, 111, 115, 72, 65, 0, 117, 107, 104, 100, 97, 105, 114, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 49, 52, 53, 0, 1, 0, 0, 0, 25, 0, 0, 0, 0, 254, 6, 119, 255, 255, 255, 255, 212, 10, 159, 127, 21, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 65, 119, 115, 0, 111, 115, 72, 65, 0, 117, 107, 104, 100, 97, 105, 114, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 49, 52, 54, 0, 1, 0, 0, 0, 25, 0, 0, 0, 0, 254, 6, 119, 255, 255, 255, 255, 212, 10, 159, 127, 22, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 83, 97, 109, 97, 114, 97, 0, 65, 0, 117, 107, 104, 100, 97, 105, 114, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 52, 53, 50, 0, 1, 0, 0, 0, 25, 0, 0, 0, 0, 254, 6, 119, 255, 255, 255, 255, 212, 10, 159, 127, 23, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 84, 97, 115, 110, 101, 101, 109, 0, 0, 117, 107, 104, 100, 97, 105, 114, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 50, 48, 0, 0, 1, 0, 0, 0, 25, 0, 0, 0, 0, 254, 6, 119, 255, 255, 255, 255, 212, 10, 159, 127, 24, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 69, 115, 114, 97, 97, 0, 109, 0, 0, 117, 107, 104, 100, 97, 105, 114, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 50, 49, 0, 0, 1, 0, 0, 0, 25, 0, 0, 0, 0, 254, 6, 119, 255, 255, 255, 255, 212, 10, 159, 127, 25, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 83, 97, 114, 97, 106, 0, 109, 0, 0, 117, 107, 104, 100, 97, 105, 114, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 50, 49, 51, 53, 0, 0, 0, 0, 25, 0, 0, 0, 0, 254, 6, 119, 255, 255, 255, 255, 212, 10, 159, 127, 26, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 65, 108, 97, 97, 110, 97, 115, 115, 101, 114, 0, 104, 100, 97, 105, 114, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 50, 50, 0, 53, 0, 0, 0, 0, 25, 0, 0, 0, 0, 254, 6, 119, 255, 255, 255, 255, 212, 10, 159, 127, 80, 80, 130, 125, 8, 0, 0, 0, 208, 7, 73, 81, 226, 166, 4, 0]
|
|
755
|
-
data_recv = @data[8
|
|
709
|
+
data_recv = @data[8..-1] + @socket.recv(size + 32)
|
|
756
710
|
end
|
|
757
711
|
|
|
758
712
|
puts "data_recv: #{python_format(data_recv)}" if @verbose
|
|
@@ -762,36 +716,30 @@ module RBZK
|
|
|
762
716
|
data << resp if resp
|
|
763
717
|
|
|
764
718
|
# get CMD_ACK_OK
|
|
765
|
-
if broken_header.size < 16
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
719
|
+
data_recv = if broken_header.size < 16
|
|
720
|
+
broken_header + @socket.recv(16)
|
|
721
|
+
else
|
|
722
|
+
broken_header
|
|
723
|
+
end
|
|
770
724
|
|
|
771
725
|
if data_recv.size < 16
|
|
772
726
|
puts "trying to complete broken ACK #{data_recv.size} /16"
|
|
773
|
-
if @verbose
|
|
774
|
-
puts "data_recv: #{data_recv.bytes.map { |b| "0x#{b.to_s(16).rjust(2, '0')}" }.join(' ')}"
|
|
775
|
-
end
|
|
727
|
+
puts "data_recv: #{data_recv.bytes.map { |b| "0x#{b.to_s(16).rjust(2, '0')}" }.join(' ')}" if @verbose
|
|
776
728
|
data_recv += @socket.recv(16 - data_recv.size) # TODO: CHECK HERE_!
|
|
777
729
|
end
|
|
778
730
|
|
|
779
|
-
|
|
780
|
-
if @verbose
|
|
781
|
-
puts "invalid chunk tcp ACK OK"
|
|
782
|
-
end
|
|
731
|
+
unless test_tcp_top(data_recv)
|
|
732
|
+
puts 'invalid chunk tcp ACK OK' if @verbose
|
|
783
733
|
return nil
|
|
784
734
|
end
|
|
785
735
|
|
|
786
736
|
# In Python: response = unpack('HHHH', data_recv[8:16])[0]
|
|
787
737
|
# This unpacks 4 shorts (8 bytes) but only uses the first one
|
|
788
738
|
# In Ruby, we need to use 'S<4' to unpack 4 shorts in little-endian format
|
|
789
|
-
response = data_recv[8...16].
|
|
739
|
+
response = data_recv[8...16].unpack1('S<4')
|
|
790
740
|
|
|
791
741
|
if response == CMD_ACK_OK
|
|
792
|
-
if @verbose
|
|
793
|
-
puts "chunk tcp ACK OK!"
|
|
794
|
-
end
|
|
742
|
+
puts 'chunk tcp ACK OK!' if @verbose
|
|
795
743
|
return data.join
|
|
796
744
|
end
|
|
797
745
|
|
|
@@ -800,40 +748,32 @@ module RBZK
|
|
|
800
748
|
puts "data: #{data.map { |d| format_as_python_bytes(d) }.join(', ')}"
|
|
801
749
|
end
|
|
802
750
|
|
|
803
|
-
|
|
751
|
+
nil
|
|
804
752
|
else
|
|
805
753
|
# Non-TCP implementation
|
|
806
754
|
loop do
|
|
807
755
|
data_recv = @socket.recv(1024 + 8)
|
|
808
|
-
response = data_recv[0...8].
|
|
756
|
+
response = data_recv[0...8].unpack1('S<S<S<S<')
|
|
809
757
|
|
|
810
|
-
if @verbose
|
|
811
|
-
puts "# packet response is: #{response}"
|
|
812
|
-
end
|
|
758
|
+
puts "# packet response is: #{response}" if @verbose
|
|
813
759
|
|
|
814
760
|
if response == CMD_DATA
|
|
815
|
-
data << data_recv[8
|
|
761
|
+
data << data_recv[8..-1]
|
|
816
762
|
size -= 1024
|
|
817
763
|
elsif response == CMD_ACK_OK
|
|
818
764
|
break
|
|
819
765
|
else
|
|
820
|
-
if @verbose
|
|
821
|
-
puts "broken!"
|
|
822
|
-
end
|
|
766
|
+
puts 'broken!' if @verbose
|
|
823
767
|
break
|
|
824
768
|
end
|
|
825
769
|
|
|
826
|
-
if @verbose
|
|
827
|
-
puts "still needs #{size}"
|
|
828
|
-
end
|
|
770
|
+
puts "still needs #{size}" if @verbose
|
|
829
771
|
end
|
|
830
772
|
|
|
831
|
-
|
|
773
|
+
data.join
|
|
832
774
|
end
|
|
833
775
|
else
|
|
834
|
-
if @verbose
|
|
835
|
-
puts "invalid response #{@response}"
|
|
836
|
-
end
|
|
776
|
+
puts "invalid response #{@response}" if @verbose
|
|
837
777
|
nil
|
|
838
778
|
end
|
|
839
779
|
end
|
|
@@ -841,27 +781,21 @@ module RBZK
|
|
|
841
781
|
# Helper method to receive raw data (like Python's __recieve_raw_data)
|
|
842
782
|
def receive_raw_data(size)
|
|
843
783
|
data = []
|
|
844
|
-
if @verbose
|
|
845
|
-
puts "expecting #{size} bytes raw data"
|
|
846
|
-
end
|
|
784
|
+
puts "expecting #{size} bytes raw data" if @verbose
|
|
847
785
|
|
|
848
|
-
while size
|
|
786
|
+
while size.positive?
|
|
849
787
|
data_recv = @socket.recv(size)
|
|
850
788
|
received = data_recv.size
|
|
851
789
|
|
|
852
790
|
if @verbose
|
|
853
791
|
puts "partial recv #{received}"
|
|
854
|
-
if received < 100
|
|
855
|
-
puts " recv #{data_recv.bytes.map { |b| "0x#{b.to_s(16).rjust(2, '0')}" }.join(' ')}"
|
|
856
|
-
end
|
|
792
|
+
puts " recv #{data_recv.bytes.map { |b| "0x#{b.to_s(16).rjust(2, '0')}" }.join(' ')}" if received < 100
|
|
857
793
|
end
|
|
858
794
|
|
|
859
795
|
data << data_recv
|
|
860
796
|
size -= received
|
|
861
797
|
|
|
862
|
-
if @verbose
|
|
863
|
-
puts "still need #{size}"
|
|
864
|
-
end
|
|
798
|
+
puts "still need #{size}" if @verbose
|
|
865
799
|
end
|
|
866
800
|
|
|
867
801
|
data.join
|
|
@@ -870,13 +804,11 @@ module RBZK
|
|
|
870
804
|
# Helper method to clear buffer (like Python's free_data)
|
|
871
805
|
def free_data
|
|
872
806
|
command = CMD_FREE_DATA
|
|
873
|
-
response =
|
|
807
|
+
response = send_command(command)
|
|
874
808
|
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
raise RBZK::ZKErrorResponse, "Can't free data"
|
|
879
|
-
end
|
|
809
|
+
raise RBZK::ZKErrorResponse, "Can't free data" unless response && response[:status]
|
|
810
|
+
|
|
811
|
+
true
|
|
880
812
|
end
|
|
881
813
|
|
|
882
814
|
# Send data with buffer
|
|
@@ -885,15 +817,13 @@ module RBZK
|
|
|
885
817
|
def send_with_buffer(buffer)
|
|
886
818
|
max_chunk = 1024
|
|
887
819
|
size = buffer.size
|
|
888
|
-
|
|
820
|
+
free_data
|
|
889
821
|
|
|
890
822
|
command = CMD_PREPARE_DATA
|
|
891
|
-
command_string = [size].pack('L<')
|
|
892
|
-
response =
|
|
823
|
+
command_string = [ size ].pack('L<')
|
|
824
|
+
response = send_command(command, command_string)
|
|
893
825
|
|
|
894
|
-
if !response || !response[:status]
|
|
895
|
-
raise RBZK::ZKErrorResponse, "Can't prepare data"
|
|
896
|
-
end
|
|
826
|
+
raise RBZK::ZKErrorResponse, "Can't prepare data" if !response || !response[:status]
|
|
897
827
|
|
|
898
828
|
remain = size % max_chunk
|
|
899
829
|
packets = (size - remain) / max_chunk
|
|
@@ -904,7 +834,7 @@ module RBZK
|
|
|
904
834
|
start += max_chunk
|
|
905
835
|
end
|
|
906
836
|
|
|
907
|
-
send_chunk(buffer[start, remain]) if remain
|
|
837
|
+
send_chunk(buffer[start, remain]) if remain.positive?
|
|
908
838
|
|
|
909
839
|
true
|
|
910
840
|
end
|
|
@@ -914,22 +844,18 @@ module RBZK
|
|
|
914
844
|
# @return [Boolean] true if successful, raises exception otherwise
|
|
915
845
|
def send_chunk(command_string)
|
|
916
846
|
command = CMD_DATA
|
|
917
|
-
response =
|
|
847
|
+
response = send_command(command, command_string)
|
|
918
848
|
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
raise RBZK::ZKErrorResponse, "Can't send chunk"
|
|
923
|
-
end
|
|
849
|
+
raise RBZK::ZKErrorResponse, "Can't send chunk" unless response && response[:status]
|
|
850
|
+
|
|
851
|
+
true
|
|
924
852
|
end
|
|
925
853
|
|
|
926
854
|
# Helper method to read a chunk of data
|
|
927
855
|
def read_chunk(start, size)
|
|
928
|
-
if @verbose
|
|
929
|
-
puts "Reading chunk: start=#{start}, size=#{size}"
|
|
930
|
-
end
|
|
856
|
+
puts "Reading chunk: start=#{start}, size=#{size}" if @verbose
|
|
931
857
|
|
|
932
|
-
3.times do |
|
|
858
|
+
3.times do |attempt|
|
|
933
859
|
# In Python: command = const._CMD_READ_BUFFER (which is 1504)
|
|
934
860
|
# In Ruby, we should use CMD_READ_BUFFER (1504) instead of CMD_READFILE_DATA (81)
|
|
935
861
|
command = 1504 # CMD_READ_BUFFER
|
|
@@ -941,25 +867,21 @@ module RBZK
|
|
|
941
867
|
response_size = @tcp ? size + 32 : 1024 + 8
|
|
942
868
|
|
|
943
869
|
# In Python: cmd_response = self.__send_command(command, command_string, response_size)
|
|
944
|
-
response =
|
|
870
|
+
response = send_command(command, command_string, response_size)
|
|
945
871
|
|
|
946
872
|
if !response || !response[:status]
|
|
947
|
-
if @verbose
|
|
948
|
-
puts "Failed to read chunk on attempt #{_retries + 1}"
|
|
949
|
-
end
|
|
873
|
+
puts "Failed to read chunk on attempt #{attempt + 1}" if @verbose
|
|
950
874
|
next
|
|
951
875
|
end
|
|
952
876
|
|
|
953
877
|
# In Python: data = self.__recieve_chunk()
|
|
954
878
|
data = receive_chunk
|
|
955
879
|
|
|
956
|
-
|
|
957
|
-
if @verbose
|
|
958
|
-
puts "Received chunk of #{data.size} bytes"
|
|
959
|
-
end
|
|
880
|
+
next unless data
|
|
960
881
|
|
|
961
|
-
|
|
962
|
-
|
|
882
|
+
puts "Received chunk of #{data.size} bytes" if @verbose
|
|
883
|
+
|
|
884
|
+
return data
|
|
963
885
|
end
|
|
964
886
|
|
|
965
887
|
# If we get here, all retries failed
|
|
@@ -968,12 +890,12 @@ module RBZK
|
|
|
968
890
|
|
|
969
891
|
def get_users
|
|
970
892
|
# Read sizes
|
|
971
|
-
|
|
893
|
+
read_sizes
|
|
972
894
|
|
|
973
895
|
puts "Device has #{@users} users" if @verbose
|
|
974
896
|
|
|
975
897
|
# If no users, return empty array
|
|
976
|
-
if @users
|
|
898
|
+
if @users.zero?
|
|
977
899
|
@next_uid = 1
|
|
978
900
|
@next_user_id = '1'
|
|
979
901
|
return []
|
|
@@ -981,46 +903,48 @@ module RBZK
|
|
|
981
903
|
|
|
982
904
|
users = []
|
|
983
905
|
max_uid = 0
|
|
984
|
-
userdata, size =
|
|
906
|
+
userdata, size = read_with_buffer(CMD_USERTEMP_RRQ, FCT_USER)
|
|
985
907
|
puts "user size #{size} (= #{userdata.length})" if @verbose
|
|
986
908
|
|
|
987
909
|
if size <= 4
|
|
988
|
-
puts
|
|
910
|
+
puts 'WRN: missing user data'
|
|
989
911
|
return []
|
|
990
912
|
end
|
|
991
913
|
|
|
992
914
|
total_size = userdata[0, 4].unpack1('L<')
|
|
993
915
|
@user_packet_size = total_size / @users
|
|
994
916
|
|
|
995
|
-
if ![ 28, 72 ].include?(@user_packet_size)
|
|
996
|
-
puts "WRN packet size would be #{@user_packet_size}" if @verbose
|
|
997
|
-
end
|
|
917
|
+
puts "WRN packet size would be #{@user_packet_size}" if ![ 28, 72 ].include?(@user_packet_size) && @verbose
|
|
998
918
|
|
|
999
919
|
userdata = userdata[4..-1]
|
|
1000
920
|
|
|
1001
921
|
if @user_packet_size == 28
|
|
1002
922
|
while userdata.length >= 28
|
|
1003
|
-
uid, privilege, password, name, card, group_id, timezone, user_id = userdata.ljust(28, "\x00")[0,
|
|
923
|
+
uid, privilege, password, name, card, group_id, timezone, user_id = userdata.ljust(28, "\x00")[0,
|
|
924
|
+
28].unpack('S<Ca5a8L<xCs<L<')
|
|
1004
925
|
max_uid = uid if uid > max_uid
|
|
1005
926
|
password = password.split("\x00").first&.force_encoding(@encoding)&.encode('UTF-8', invalid: :replace)
|
|
1006
927
|
name = name.split("\x00").first&.force_encoding(@encoding)&.encode('UTF-8', invalid: :replace)&.strip
|
|
1007
928
|
group_id = group_id.to_s
|
|
1008
929
|
user_id = user_id.to_s
|
|
1009
|
-
name
|
|
930
|
+
name ||= "NN-#{user_id}"
|
|
1010
931
|
user = User.new(uid, name, privilege, password, group_id, user_id, card)
|
|
1011
932
|
users << user
|
|
1012
|
-
|
|
933
|
+
if @verbose
|
|
934
|
+
puts "[6]user: #{uid}, #{privilege}, #{password}, #{name}, #{card}, #{group_id}, #{timezone}, #{user_id}"
|
|
935
|
+
end
|
|
1013
936
|
userdata = userdata[28..-1]
|
|
1014
937
|
end
|
|
1015
938
|
else
|
|
1016
939
|
while userdata.length >= 72
|
|
1017
|
-
uid, privilege, password, name, card, group_id, user_id = userdata.ljust(72, "\x00")[0,
|
|
940
|
+
uid, privilege, password, name, card, group_id, user_id = userdata.ljust(72, "\x00")[0,
|
|
941
|
+
72].unpack('S<Ca8a24L<xa7xa24')
|
|
1018
942
|
max_uid = uid if uid > max_uid
|
|
1019
943
|
password = password.split("\x00").first&.force_encoding(@encoding)&.encode('UTF-8', invalid: :replace)
|
|
1020
944
|
name = name.split("\x00").first&.force_encoding(@encoding)&.encode('UTF-8', invalid: :replace)&.strip
|
|
1021
945
|
group_id = group_id.split("\x00").first&.force_encoding(@encoding)&.encode('UTF-8', invalid: :replace)&.strip
|
|
1022
946
|
user_id = user_id.split("\x00").first&.force_encoding(@encoding)&.encode('UTF-8', invalid: :replace)
|
|
1023
|
-
name
|
|
947
|
+
name ||= "NN-#{user_id}"
|
|
1024
948
|
user = User.new(uid, name, privilege, password, group_id, user_id, card)
|
|
1025
949
|
users << user
|
|
1026
950
|
userdata = userdata[72..-1]
|
|
@@ -1032,12 +956,10 @@ module RBZK
|
|
|
1032
956
|
@next_user_id = max_uid.to_s
|
|
1033
957
|
|
|
1034
958
|
loop do
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
break
|
|
1040
|
-
end
|
|
959
|
+
break unless users.any? { |u| u.user_id == @next_user_id }
|
|
960
|
+
|
|
961
|
+
max_uid += 1
|
|
962
|
+
@next_user_id = max_uid.to_s
|
|
1041
963
|
end
|
|
1042
964
|
|
|
1043
965
|
users
|
|
@@ -1045,41 +967,33 @@ module RBZK
|
|
|
1045
967
|
|
|
1046
968
|
def get_attendance_logs
|
|
1047
969
|
# First, read device sizes to get record count
|
|
1048
|
-
|
|
970
|
+
read_sizes
|
|
1049
971
|
|
|
1050
972
|
# If no records, return empty array
|
|
1051
|
-
if @records
|
|
1052
|
-
return []
|
|
1053
|
-
end
|
|
973
|
+
return [] if @records.zero?
|
|
1054
974
|
|
|
1055
975
|
# Get users for lookup
|
|
1056
|
-
users =
|
|
976
|
+
users = get_users
|
|
1057
977
|
|
|
1058
|
-
if @verbose
|
|
1059
|
-
puts "Found #{users.size} users"
|
|
1060
|
-
end
|
|
978
|
+
puts "Found #{users.size} users" if @verbose
|
|
1061
979
|
|
|
1062
980
|
logs = []
|
|
1063
981
|
|
|
1064
982
|
# Read attendance data with buffer
|
|
1065
|
-
attendance_data, size =
|
|
983
|
+
attendance_data, size = read_with_buffer(CMD_ATTLOG_RRQ)
|
|
1066
984
|
|
|
1067
985
|
if size < 4
|
|
1068
|
-
if @verbose
|
|
1069
|
-
puts "WRN: no attendance data"
|
|
1070
|
-
end
|
|
986
|
+
puts 'WRN: no attendance data' if @verbose
|
|
1071
987
|
return []
|
|
1072
988
|
end
|
|
1073
989
|
|
|
1074
990
|
# Get total size from first 4 bytes
|
|
1075
|
-
total_size = attendance_data[0...4].
|
|
991
|
+
total_size = attendance_data[0...4].unpack1('I')
|
|
1076
992
|
|
|
1077
993
|
# Calculate record size
|
|
1078
|
-
record_size = @records
|
|
994
|
+
record_size = @records.positive? ? total_size / @records : 0
|
|
1079
995
|
|
|
1080
|
-
if @verbose
|
|
1081
|
-
puts "record_size is #{record_size}"
|
|
1082
|
-
end
|
|
996
|
+
puts "record_size is #{record_size}" if @verbose
|
|
1083
997
|
|
|
1084
998
|
# Remove the first 4 bytes (total size)
|
|
1085
999
|
attendance_data = attendance_data[4..-1]
|
|
@@ -1091,18 +1005,20 @@ module RBZK
|
|
|
1091
1005
|
uid, status, timestamp_raw, punch = attendance_data[0...8].ljust(8, "\x00".b).unpack('S<C4sC')
|
|
1092
1006
|
|
|
1093
1007
|
if @verbose
|
|
1094
|
-
puts "Attendance data (hex): #{attendance_data[0...8].bytes.map
|
|
1008
|
+
puts "Attendance data (hex): #{attendance_data[0...8].bytes.map do |b|
|
|
1009
|
+
"0x#{b.to_s(16).rjust(2, '0')}"
|
|
1010
|
+
end.join(' ')}"
|
|
1095
1011
|
end
|
|
1096
1012
|
|
|
1097
1013
|
attendance_data = attendance_data[8..-1]
|
|
1098
1014
|
|
|
1099
1015
|
# Look up user by uid
|
|
1100
1016
|
tuser = users.find { |u| u.uid == uid }
|
|
1101
|
-
if !tuser
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1017
|
+
user_id = if !tuser
|
|
1018
|
+
uid.to_s
|
|
1019
|
+
else
|
|
1020
|
+
tuser.user_id
|
|
1021
|
+
end
|
|
1106
1022
|
|
|
1107
1023
|
# Decode timestamp
|
|
1108
1024
|
timestamp = decode_time(timestamp_raw)
|
|
@@ -1115,10 +1031,13 @@ module RBZK
|
|
|
1115
1031
|
# Handle 16-byte records
|
|
1116
1032
|
while attendance_data && attendance_data.size >= 16
|
|
1117
1033
|
# In Python: user_id, timestamp, status, punch, reserved, workcode = unpack('<I4sBB2sI', attendance_data.ljust(16, b'\x00')[:16])
|
|
1118
|
-
user_id_raw, timestamp_raw, status, punch,
|
|
1034
|
+
user_id_raw, timestamp_raw, status, punch, _reserved, _workcode = attendance_data[0...16].ljust(16,
|
|
1035
|
+
"\x00".b).unpack('L<4sCCa2L<')
|
|
1119
1036
|
|
|
1120
1037
|
if @verbose
|
|
1121
|
-
puts "Attendance data (hex): #{attendance_data[0...16].bytes.map
|
|
1038
|
+
puts "Attendance data (hex): #{attendance_data[0...16].bytes.map do |b|
|
|
1039
|
+
"0x#{b.to_s(16).rjust(2, '0')}"
|
|
1040
|
+
end.join(' ')}"
|
|
1122
1041
|
end
|
|
1123
1042
|
|
|
1124
1043
|
attendance_data = attendance_data[16..-1]
|
|
@@ -1129,9 +1048,7 @@ module RBZK
|
|
|
1129
1048
|
# Look up user by user_id and uid
|
|
1130
1049
|
tuser = users.find { |u| u.user_id == user_id }
|
|
1131
1050
|
if !tuser
|
|
1132
|
-
if @verbose
|
|
1133
|
-
puts "no uid #{user_id}"
|
|
1134
|
-
end
|
|
1051
|
+
puts "no uid #{user_id}" if @verbose
|
|
1135
1052
|
uid = user_id
|
|
1136
1053
|
tuser = users.find { |u| u.uid.to_s == user_id }
|
|
1137
1054
|
if !tuser
|
|
@@ -1155,10 +1072,13 @@ module RBZK
|
|
|
1155
1072
|
# Handle 40-byte records (default)
|
|
1156
1073
|
while attendance_data && attendance_data.size >= 40
|
|
1157
1074
|
# In Python: uid, user_id, status, timestamp, punch, space = unpack('<H24sB4sB8s', attendance_data.ljust(40, b'\x00')[:40])
|
|
1158
|
-
uid, user_id_raw, status, timestamp_raw, punch,
|
|
1075
|
+
uid, user_id_raw, status, timestamp_raw, punch, _space = attendance_data[0...40].ljust(40,
|
|
1076
|
+
"\x00".b).unpack('S<a24Ca4Ca8')
|
|
1159
1077
|
|
|
1160
1078
|
if @verbose
|
|
1161
|
-
puts "Attendance data (hex): #{attendance_data[0...40].bytes.map
|
|
1079
|
+
puts "Attendance data (hex): #{attendance_data[0...40].bytes.map do |b|
|
|
1080
|
+
"0x#{b.to_s(16).rjust(2, '0')}"
|
|
1081
|
+
end.join(' ')}"
|
|
1162
1082
|
end
|
|
1163
1083
|
|
|
1164
1084
|
# Extract user_id from null-terminated string
|
|
@@ -1180,23 +1100,23 @@ module RBZK
|
|
|
1180
1100
|
|
|
1181
1101
|
def decode_time(t)
|
|
1182
1102
|
# Convert binary timestamp to integer
|
|
1183
|
-
t = t.
|
|
1103
|
+
t = t.unpack1('L<')
|
|
1184
1104
|
|
|
1185
1105
|
# Extract time components
|
|
1186
1106
|
second = t % 60
|
|
1187
|
-
t
|
|
1107
|
+
t /= 60
|
|
1188
1108
|
|
|
1189
1109
|
minute = t % 60
|
|
1190
|
-
t
|
|
1110
|
+
t /= 60
|
|
1191
1111
|
|
|
1192
1112
|
hour = t % 24
|
|
1193
|
-
t
|
|
1113
|
+
t /= 24
|
|
1194
1114
|
|
|
1195
1115
|
day = t % 31 + 1
|
|
1196
|
-
t
|
|
1116
|
+
t /= 31
|
|
1197
1117
|
|
|
1198
1118
|
month = t % 12 + 1
|
|
1199
|
-
t
|
|
1119
|
+
t /= 12
|
|
1200
1120
|
|
|
1201
1121
|
year = t + 2000
|
|
1202
1122
|
|
|
@@ -1208,7 +1128,7 @@ module RBZK
|
|
|
1208
1128
|
# Match Python's __decode_timehex method
|
|
1209
1129
|
def decode_timehex(timehex)
|
|
1210
1130
|
# Extract time components
|
|
1211
|
-
year, month, day, hour, minute, second = timehex.unpack(
|
|
1131
|
+
year, month, day, hour, minute, second = timehex.unpack('C6')
|
|
1212
1132
|
year += 2000
|
|
1213
1133
|
|
|
1214
1134
|
# Create Time object
|
|
@@ -1217,23 +1137,20 @@ module RBZK
|
|
|
1217
1137
|
|
|
1218
1138
|
def encode_time(t)
|
|
1219
1139
|
# Calculate encoded timestamp
|
|
1220
|
-
|
|
1140
|
+
(
|
|
1221
1141
|
((t.year % 100) * 12 * 31 + ((t.month - 1) * 31) + t.day - 1) *
|
|
1222
1142
|
(24 * 60 * 60) + (t.hour * 60 + t.minute) * 60 + t.second
|
|
1223
1143
|
)
|
|
1224
|
-
d
|
|
1225
1144
|
end
|
|
1226
1145
|
|
|
1227
1146
|
def get_time
|
|
1228
1147
|
command = CMD_GET_TIME
|
|
1229
1148
|
response_size = 1032
|
|
1230
|
-
response =
|
|
1149
|
+
response = send_command(command, '', response_size)
|
|
1231
1150
|
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
raise RBZK::ZKErrorResponse, "Can't get time"
|
|
1236
|
-
end
|
|
1151
|
+
raise RBZK::ZKErrorResponse, "Can't get time" unless response && response[:status]
|
|
1152
|
+
|
|
1153
|
+
decode_time(@data[0...4])
|
|
1237
1154
|
end
|
|
1238
1155
|
|
|
1239
1156
|
def set_time(timestamp = nil)
|
|
@@ -1242,24 +1159,22 @@ module RBZK
|
|
|
1242
1159
|
|
|
1243
1160
|
command = CMD_SET_TIME
|
|
1244
1161
|
command_string = [ encode_time(timestamp) ].pack('L<')
|
|
1245
|
-
response =
|
|
1162
|
+
response = send_command(command, command_string)
|
|
1246
1163
|
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
raise RBZK::ZKErrorResponse, "Can't set time"
|
|
1251
|
-
end
|
|
1164
|
+
raise RBZK::ZKErrorResponse, "Can't set time" unless response && response[:status]
|
|
1165
|
+
|
|
1166
|
+
true
|
|
1252
1167
|
end
|
|
1253
1168
|
|
|
1254
1169
|
def clear_attendance_logs
|
|
1255
|
-
|
|
1256
|
-
|
|
1170
|
+
send_command(CMD_CLEAR_ATTLOG)
|
|
1171
|
+
recv_reply
|
|
1257
1172
|
true
|
|
1258
1173
|
end
|
|
1259
1174
|
|
|
1260
1175
|
def clear_data
|
|
1261
|
-
|
|
1262
|
-
|
|
1176
|
+
send_command(CMD_CLEAR_DATA)
|
|
1177
|
+
recv_reply
|
|
1263
1178
|
true
|
|
1264
1179
|
end
|
|
1265
1180
|
|
|
@@ -1269,33 +1184,33 @@ module RBZK
|
|
|
1269
1184
|
|
|
1270
1185
|
result = "b'"
|
|
1271
1186
|
binary_string.each_byte do |byte|
|
|
1272
|
-
case byte
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1187
|
+
result += case byte
|
|
1188
|
+
when 0x0d # Carriage return - Python shows as \r
|
|
1189
|
+
'\\r'
|
|
1190
|
+
when 0x0a # Line feed - Python shows as \n
|
|
1191
|
+
'\\n'
|
|
1192
|
+
when 0x09 # Tab - Python shows as \t
|
|
1193
|
+
'\\t'
|
|
1194
|
+
when 0x07 # Bell - Python can show as \a or \x07
|
|
1195
|
+
'\\x07'
|
|
1196
|
+
when 0x08 # Backspace - Python shows as \b
|
|
1197
|
+
'\\b'
|
|
1198
|
+
when 0x0c # Form feed - Python shows as \f
|
|
1199
|
+
'\\f'
|
|
1200
|
+
when 0x0b # Vertical tab - Python shows as \v
|
|
1201
|
+
'\\v'
|
|
1202
|
+
when 0x5c # Backslash - Python shows as \\
|
|
1203
|
+
'\\\\'
|
|
1204
|
+
when 0x27 # Single quote - Python shows as \'
|
|
1205
|
+
"\\'"
|
|
1206
|
+
when 0x22 # Double quote - Python shows as \"
|
|
1207
|
+
'\"'
|
|
1208
|
+
when 32..126 # Printable ASCII
|
|
1209
|
+
byte.chr
|
|
1210
|
+
else
|
|
1211
|
+
# All other bytes - Python shows as \xHH
|
|
1212
|
+
"\\x#{byte.to_s(16).rjust(2, '0')}"
|
|
1213
|
+
end
|
|
1299
1214
|
end
|
|
1300
1215
|
result += "'"
|
|
1301
1216
|
result
|
|
@@ -1310,7 +1225,7 @@ module RBZK
|
|
|
1310
1225
|
puts "Python expected: #{python_expected}"
|
|
1311
1226
|
|
|
1312
1227
|
if ruby_formatted != python_expected
|
|
1313
|
-
puts
|
|
1228
|
+
puts 'DIFFERENCE DETECTED!'
|
|
1314
1229
|
# Show byte-by-byte comparison
|
|
1315
1230
|
ruby_bytes = binary_string.bytes
|
|
1316
1231
|
# Parse Python bytes string (format: b'\x01\x02')
|
|
@@ -1346,16 +1261,27 @@ module RBZK
|
|
|
1346
1261
|
end
|
|
1347
1262
|
|
|
1348
1263
|
# Show differences
|
|
1349
|
-
puts
|
|
1264
|
+
puts 'Byte-by-byte comparison:'
|
|
1350
1265
|
max_len = [ ruby_bytes.length, python_bytes.length ].max
|
|
1351
1266
|
(0...max_len).each do |j|
|
|
1352
1267
|
ruby_byte = j < ruby_bytes.length ? ruby_bytes[j] : nil
|
|
1353
1268
|
python_byte = j < python_bytes.length ? python_bytes[j] : nil
|
|
1354
|
-
match = ruby_byte == python_byte ?
|
|
1355
|
-
puts " Byte #{j}: Ruby=#{
|
|
1269
|
+
match = ruby_byte == python_byte ? '✓' : '✗'
|
|
1270
|
+
puts " Byte #{j}: Ruby=#{if ruby_byte.nil?
|
|
1271
|
+
'nil'
|
|
1272
|
+
else
|
|
1273
|
+
"0x#{ruby_byte.to_s(16).rjust(2,
|
|
1274
|
+
'0')}"
|
|
1275
|
+
end}, Python=#{if python_byte.nil?
|
|
1276
|
+
'nil'
|
|
1277
|
+
else
|
|
1278
|
+
"0x#{python_byte.to_s(16).rjust(
|
|
1279
|
+
2, '0'
|
|
1280
|
+
)}"
|
|
1281
|
+
end} #{match}"
|
|
1356
1282
|
end
|
|
1357
1283
|
else
|
|
1358
|
-
puts
|
|
1284
|
+
puts 'Binary data matches exactly!'
|
|
1359
1285
|
end
|
|
1360
1286
|
end
|
|
1361
1287
|
|
|
@@ -1373,70 +1299,62 @@ module RBZK
|
|
|
1373
1299
|
def read_sizes
|
|
1374
1300
|
command = CMD_GET_FREE_SIZES
|
|
1375
1301
|
response_size = 1024
|
|
1376
|
-
cmd_response =
|
|
1302
|
+
cmd_response = send_command(command, '', response_size)
|
|
1377
1303
|
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1304
|
+
raise RBZK::ZKErrorResponse, "Can't read sizes" unless cmd_response && cmd_response[:status]
|
|
1305
|
+
|
|
1306
|
+
if @verbose
|
|
1307
|
+
puts "Data hex: #{@data.bytes.map { |b| "0x#{b.to_s(16).rjust(2, '0')}" }.join(' ')}"
|
|
1308
|
+
puts "Data Python format: #{python_format(@data)}"
|
|
1309
|
+
end
|
|
1310
|
+
|
|
1311
|
+
size = @data.size
|
|
1312
|
+
puts "Data size: #{size} bytes" if @verbose
|
|
1313
|
+
|
|
1314
|
+
if @data.size >= 80
|
|
1315
|
+
# In Python: fields = unpack('20i', self.__data[:80])
|
|
1316
|
+
# In Ruby, 'l<' is a signed 32-bit integer (4 bytes) in little-endian format, which matches Python's 'i'
|
|
1317
|
+
fields = @data[0...80].unpack('l<20')
|
|
1318
|
+
|
|
1319
|
+
puts "Unpacked fields: #{fields.inspect}" if @verbose
|
|
1320
|
+
|
|
1321
|
+
@users = fields[4]
|
|
1322
|
+
@fingers = fields[6]
|
|
1323
|
+
@records = fields[8]
|
|
1324
|
+
@dummy = fields[10] # ???
|
|
1325
|
+
@cards = fields[12]
|
|
1326
|
+
@fingers_cap = fields[14]
|
|
1327
|
+
@users_cap = fields[15]
|
|
1328
|
+
@rec_cap = fields[16]
|
|
1329
|
+
@fingers_av = fields[17]
|
|
1330
|
+
@users_av = fields[18]
|
|
1331
|
+
@rec_av = fields[19]
|
|
1332
|
+
@data = @data[80..-1]
|
|
1333
|
+
|
|
1334
|
+
# Check for face information (added to match Python implementation)
|
|
1335
|
+
if @data.size >= 12 # face info
|
|
1336
|
+
# In Python: fields = unpack('3i', self.__data[:12]) #dirty hack! we need more information
|
|
1337
|
+
face_fields = @data[0...12].unpack('l<3')
|
|
1338
|
+
@faces = face_fields[0]
|
|
1339
|
+
@faces_cap = face_fields[2]
|
|
1340
|
+
|
|
1341
|
+
puts "Face info: faces=#{@faces}, capacity=#{@faces_cap}" if @verbose
|
|
1382
1342
|
end
|
|
1383
1343
|
|
|
1384
|
-
size = @data.size
|
|
1385
1344
|
if @verbose
|
|
1386
|
-
puts "
|
|
1345
|
+
puts "Device info: users=#{@users}, fingers=#{@fingers}, records=#{@records}"
|
|
1346
|
+
puts "Capacity: users=#{@users_cap}, fingers=#{@fingers_cap}, records=#{@rec_cap}"
|
|
1387
1347
|
end
|
|
1388
1348
|
|
|
1389
|
-
|
|
1390
|
-
# In Python: fields = unpack('20i', self.__data[:80])
|
|
1391
|
-
# In Ruby, 'l<' is a signed 32-bit integer (4 bytes) in little-endian format, which matches Python's 'i'
|
|
1392
|
-
fields = @data[0...80].unpack('l<20')
|
|
1393
|
-
|
|
1394
|
-
if @verbose
|
|
1395
|
-
puts "Unpacked fields: #{fields.inspect}"
|
|
1396
|
-
end
|
|
1397
|
-
|
|
1398
|
-
@users = fields[4]
|
|
1399
|
-
@fingers = fields[6]
|
|
1400
|
-
@records = fields[8]
|
|
1401
|
-
@dummy = fields[10] # ???
|
|
1402
|
-
@cards = fields[12]
|
|
1403
|
-
@fingers_cap = fields[14]
|
|
1404
|
-
@users_cap = fields[15]
|
|
1405
|
-
@rec_cap = fields[16]
|
|
1406
|
-
@fingers_av = fields[17]
|
|
1407
|
-
@users_av = fields[18]
|
|
1408
|
-
@rec_av = fields[19]
|
|
1409
|
-
@data = @data[80..-1]
|
|
1410
|
-
|
|
1411
|
-
# Check for face information (added to match Python implementation)
|
|
1412
|
-
if @data.size >= 12 # face info
|
|
1413
|
-
# In Python: fields = unpack('3i', self.__data[:12]) #dirty hack! we need more information
|
|
1414
|
-
face_fields = @data[0...12].unpack('l<3')
|
|
1415
|
-
@faces = face_fields[0]
|
|
1416
|
-
@faces_cap = face_fields[2]
|
|
1417
|
-
|
|
1418
|
-
if @verbose
|
|
1419
|
-
puts "Face info: faces=#{@faces}, capacity=#{@faces_cap}"
|
|
1420
|
-
end
|
|
1421
|
-
end
|
|
1422
|
-
|
|
1423
|
-
if @verbose
|
|
1424
|
-
puts "Device info: users=#{@users}, fingers=#{@fingers}, records=#{@records}"
|
|
1425
|
-
puts "Capacity: users=#{@users_cap}, fingers=#{@fingers_cap}, records=#{@rec_cap}"
|
|
1426
|
-
end
|
|
1427
|
-
|
|
1428
|
-
return true
|
|
1429
|
-
end
|
|
1430
|
-
else
|
|
1431
|
-
raise RBZK::ZKErrorResponse, "Can't read sizes"
|
|
1349
|
+
return true
|
|
1432
1350
|
end
|
|
1433
1351
|
|
|
1434
1352
|
false
|
|
1435
1353
|
end
|
|
1436
1354
|
|
|
1437
1355
|
def get_free_sizes
|
|
1438
|
-
|
|
1439
|
-
reply =
|
|
1356
|
+
send_command(CMD_GET_FREE_SIZES)
|
|
1357
|
+
reply = recv_reply
|
|
1440
1358
|
|
|
1441
1359
|
if reply && reply.size >= 8
|
|
1442
1360
|
sizes_data = reply[8..-1].unpack('S<*')
|
|
@@ -1456,11 +1374,11 @@ module RBZK
|
|
|
1456
1374
|
def get_templates
|
|
1457
1375
|
fingers = []
|
|
1458
1376
|
|
|
1459
|
-
|
|
1460
|
-
|
|
1377
|
+
send_command(CMD_PREPARE_DATA, [ FCT_FINGERTMP ].pack('C'))
|
|
1378
|
+
recv_reply
|
|
1461
1379
|
|
|
1462
|
-
data_size =
|
|
1463
|
-
templates_data =
|
|
1380
|
+
data_size = recv_long
|
|
1381
|
+
templates_data = recv_chunk(data_size)
|
|
1464
1382
|
|
|
1465
1383
|
if templates_data && !templates_data.empty?
|
|
1466
1384
|
offset = 0
|
|
@@ -1480,8 +1398,8 @@ module RBZK
|
|
|
1480
1398
|
end
|
|
1481
1399
|
|
|
1482
1400
|
def get_user_template(uid, finger_id)
|
|
1483
|
-
|
|
1484
|
-
reply =
|
|
1401
|
+
send_command(CMD_GET_USERTEMP, [ uid, finger_id ].pack('S<S<'))
|
|
1402
|
+
reply = recv_reply
|
|
1485
1403
|
|
|
1486
1404
|
if reply && reply.size >= 8
|
|
1487
1405
|
template_data = reply[8..-1]
|
|
@@ -1512,9 +1430,7 @@ module RBZK
|
|
|
1512
1430
|
# Use connect_ex like Python (returns error code instead of raising exception)
|
|
1513
1431
|
sockaddr = Socket.pack_sockaddr_in(@port, @ip)
|
|
1514
1432
|
@socket.connect_nonblock(sockaddr)
|
|
1515
|
-
if @verbose
|
|
1516
|
-
puts "TCP socket connected successfully"
|
|
1517
|
-
end
|
|
1433
|
+
puts 'TCP socket connected successfully' if @verbose
|
|
1518
1434
|
rescue IO::WaitWritable
|
|
1519
1435
|
# Socket is in progress of connecting
|
|
1520
1436
|
ready = IO.select(nil, [ @socket ], nil, @timeout)
|
|
@@ -1523,42 +1439,30 @@ module RBZK
|
|
|
1523
1439
|
@socket.connect_nonblock(sockaddr)
|
|
1524
1440
|
rescue Errno::EISCONN
|
|
1525
1441
|
# Already connected, which is fine
|
|
1526
|
-
if @verbose
|
|
1527
|
-
|
|
1528
|
-
end
|
|
1529
|
-
rescue => e
|
|
1442
|
+
puts 'TCP socket connected successfully' if @verbose
|
|
1443
|
+
rescue StandardError => e
|
|
1530
1444
|
# Connection failed
|
|
1531
|
-
if @verbose
|
|
1532
|
-
puts "TCP socket connection failed: #{e.message}"
|
|
1533
|
-
end
|
|
1445
|
+
puts "TCP socket connection failed: #{e.message}" if @verbose
|
|
1534
1446
|
raise e
|
|
1535
1447
|
end
|
|
1536
1448
|
else
|
|
1537
1449
|
# Connection timed out
|
|
1538
|
-
if @verbose
|
|
1539
|
-
puts "TCP socket connection timed out"
|
|
1540
|
-
end
|
|
1450
|
+
puts 'TCP socket connection timed out' if @verbose
|
|
1541
1451
|
raise Errno::ETIMEDOUT
|
|
1542
1452
|
end
|
|
1543
1453
|
rescue Errno::EISCONN
|
|
1544
1454
|
# Already connected, which is fine
|
|
1545
|
-
if @verbose
|
|
1546
|
-
puts "TCP socket already connected"
|
|
1547
|
-
end
|
|
1455
|
+
puts 'TCP socket already connected' if @verbose
|
|
1548
1456
|
end
|
|
1549
1457
|
else
|
|
1550
1458
|
# Create UDP socket (like Python's socket(AF_INET, SOCK_DGRAM))
|
|
1551
1459
|
@socket = Socket.new(Socket::AF_INET, Socket::SOCK_DGRAM)
|
|
1552
1460
|
@socket.setsockopt(Socket::SOL_SOCKET, Socket::SO_RCVTIMEO, [ @timeout, 0 ].pack('l_*'))
|
|
1553
1461
|
|
|
1554
|
-
if @verbose
|
|
1555
|
-
puts "UDP socket created successfully"
|
|
1556
|
-
end
|
|
1557
|
-
end
|
|
1558
|
-
rescue => e
|
|
1559
|
-
if @verbose
|
|
1560
|
-
puts "Socket creation failed: #{e.message}"
|
|
1462
|
+
puts 'UDP socket created successfully' if @verbose
|
|
1561
1463
|
end
|
|
1464
|
+
rescue StandardError => e
|
|
1465
|
+
puts "Socket creation failed: #{e.message}" if @verbose
|
|
1562
1466
|
raise RBZK::ZKNetworkError, "Failed to create socket: #{e.message}"
|
|
1563
1467
|
end
|
|
1564
1468
|
|
|
@@ -1584,24 +1488,18 @@ module RBZK
|
|
|
1584
1488
|
end
|
|
1585
1489
|
|
|
1586
1490
|
def ping
|
|
1587
|
-
if @verbose
|
|
1588
|
-
puts "Pinging device at #{@ip}:#{@port}..."
|
|
1589
|
-
end
|
|
1491
|
+
puts "Pinging device at #{@ip}:#{@port}..." if @verbose
|
|
1590
1492
|
|
|
1591
1493
|
# Try TCP ping first
|
|
1592
1494
|
begin
|
|
1593
1495
|
Timeout.timeout(5) do
|
|
1594
1496
|
s = TCPSocket.new(@ip, @port)
|
|
1595
1497
|
s.close
|
|
1596
|
-
if @verbose
|
|
1597
|
-
puts "TCP ping successful"
|
|
1598
|
-
end
|
|
1498
|
+
puts 'TCP ping successful' if @verbose
|
|
1599
1499
|
return true
|
|
1600
1500
|
end
|
|
1601
1501
|
rescue Timeout::Error, Errno::ECONNREFUSED, Errno::EHOSTUNREACH => e
|
|
1602
|
-
if @verbose
|
|
1603
|
-
puts "TCP ping failed: #{e.message}"
|
|
1604
|
-
end
|
|
1502
|
+
puts "TCP ping failed: #{e.message}" if @verbose
|
|
1605
1503
|
end
|
|
1606
1504
|
|
|
1607
1505
|
# If TCP ping fails, try UDP ping
|
|
@@ -1612,23 +1510,17 @@ module RBZK
|
|
|
1612
1510
|
ready = IO.select([ udp_socket ], nil, nil, 5)
|
|
1613
1511
|
|
|
1614
1512
|
if ready
|
|
1615
|
-
if @verbose
|
|
1616
|
-
puts "UDP ping successful"
|
|
1617
|
-
end
|
|
1513
|
+
puts 'UDP ping successful' if @verbose
|
|
1618
1514
|
udp_socket.close
|
|
1619
|
-
|
|
1515
|
+
true
|
|
1620
1516
|
else
|
|
1621
|
-
if @verbose
|
|
1622
|
-
puts "UDP ping timed out"
|
|
1623
|
-
end
|
|
1517
|
+
puts 'UDP ping timed out' if @verbose
|
|
1624
1518
|
udp_socket.close
|
|
1625
|
-
|
|
1626
|
-
end
|
|
1627
|
-
rescue => e
|
|
1628
|
-
if @verbose
|
|
1629
|
-
puts "UDP ping failed: #{e.message}"
|
|
1519
|
+
false
|
|
1630
1520
|
end
|
|
1631
|
-
|
|
1521
|
+
rescue StandardError => e
|
|
1522
|
+
puts "UDP ping failed: #{e.message}" if @verbose
|
|
1523
|
+
false
|
|
1632
1524
|
end
|
|
1633
1525
|
end
|
|
1634
1526
|
|
|
@@ -1647,24 +1539,18 @@ module RBZK
|
|
|
1647
1539
|
|
|
1648
1540
|
# In Python: if checksum > const.USHRT_MAX: checksum -= const.USHRT_MAX
|
|
1649
1541
|
# Handle overflow immediately after each addition
|
|
1650
|
-
if checksum > USHRT_MAX
|
|
1651
|
-
checksum -= USHRT_MAX
|
|
1652
|
-
end
|
|
1542
|
+
checksum -= USHRT_MAX if checksum > USHRT_MAX
|
|
1653
1543
|
|
|
1654
1544
|
l -= 2
|
|
1655
1545
|
end
|
|
1656
1546
|
|
|
1657
1547
|
# Handle odd byte if present
|
|
1658
1548
|
# In Python: if l: checksum = checksum + p[-1]
|
|
1659
|
-
if l
|
|
1660
|
-
checksum += buf[i]
|
|
1661
|
-
end
|
|
1549
|
+
checksum += buf[i] if l.positive?
|
|
1662
1550
|
|
|
1663
1551
|
# Handle overflow
|
|
1664
1552
|
# In Python: while checksum > const.USHRT_MAX: checksum -= const.USHRT_MAX
|
|
1665
|
-
while checksum > USHRT_MAX
|
|
1666
|
-
checksum -= USHRT_MAX
|
|
1667
|
-
end
|
|
1553
|
+
checksum -= USHRT_MAX while checksum > USHRT_MAX
|
|
1668
1554
|
|
|
1669
1555
|
# Bitwise complement
|
|
1670
1556
|
# In Python: checksum = ~checksum
|
|
@@ -1672,9 +1558,7 @@ module RBZK
|
|
|
1672
1558
|
|
|
1673
1559
|
# Handle negative values
|
|
1674
1560
|
# In Python: while checksum < 0: checksum += const.USHRT_MAX
|
|
1675
|
-
while checksum
|
|
1676
|
-
checksum += USHRT_MAX
|
|
1677
|
-
end
|
|
1561
|
+
checksum += USHRT_MAX while checksum.negative?
|
|
1678
1562
|
|
|
1679
1563
|
# Return the checksum
|
|
1680
1564
|
checksum
|
|
@@ -1683,6 +1567,7 @@ module RBZK
|
|
|
1683
1567
|
# Helper method to debug binary data in both Python and Ruby formats
|
|
1684
1568
|
def debug_binary(name, data)
|
|
1685
1569
|
return unless @verbose
|
|
1570
|
+
|
|
1686
1571
|
puts "#{name} (hex): #{data.bytes.map { |b| "\\x#{b.to_s(16).rjust(2, '0')}" }.join('')}"
|
|
1687
1572
|
puts "#{name} (Ruby): #{data.bytes.map { |b| "0x#{b.to_s(16).rjust(2, '0')}" }.join(' ')}"
|
|
1688
1573
|
puts "#{name} (Python): #{format_as_python_bytes(data)}"
|
|
@@ -1694,18 +1579,18 @@ module RBZK
|
|
|
1694
1579
|
top = [ MACHINE_PREPARE_DATA_1, MACHINE_PREPARE_DATA_2, length ].pack('S<S<I<')
|
|
1695
1580
|
|
|
1696
1581
|
if @verbose
|
|
1697
|
-
puts
|
|
1582
|
+
puts 'TCP header components:'
|
|
1698
1583
|
puts " MACHINE_PREPARE_DATA_1: 0x#{MACHINE_PREPARE_DATA_1.to_s(16)}"
|
|
1699
1584
|
puts " MACHINE_PREPARE_DATA_2: 0x#{MACHINE_PREPARE_DATA_2.to_s(16)}"
|
|
1700
1585
|
puts " packet length: #{length}"
|
|
1701
|
-
debug_binary(
|
|
1702
|
-
debug_binary(
|
|
1586
|
+
debug_binary('TCP header', top)
|
|
1587
|
+
debug_binary('Full TCP packet', top + packet)
|
|
1703
1588
|
end
|
|
1704
1589
|
|
|
1705
1590
|
top + packet
|
|
1706
1591
|
end
|
|
1707
1592
|
|
|
1708
|
-
def create_header(command, command_string =
|
|
1593
|
+
def create_header(command, command_string = ''.b, session_id = 0, reply_id = 0)
|
|
1709
1594
|
# Ensure command_string is a binary string
|
|
1710
1595
|
command_string = command_string.to_s.b
|
|
1711
1596
|
|
|
@@ -1720,35 +1605,33 @@ module RBZK
|
|
|
1720
1605
|
|
|
1721
1606
|
# Update reply_id
|
|
1722
1607
|
reply_id += 1
|
|
1723
|
-
if reply_id >= USHRT_MAX
|
|
1724
|
-
reply_id -= USHRT_MAX
|
|
1725
|
-
end
|
|
1608
|
+
reply_id -= USHRT_MAX if reply_id >= USHRT_MAX
|
|
1726
1609
|
|
|
1727
1610
|
# Create final header with updated values
|
|
1728
1611
|
buf = [ command, checksum, session_id, reply_id ].pack('v4')
|
|
1729
1612
|
|
|
1730
1613
|
if @verbose
|
|
1731
|
-
puts
|
|
1614
|
+
puts 'Header components:'
|
|
1732
1615
|
puts " Command: #{command}"
|
|
1733
1616
|
puts " Checksum: #{checksum}"
|
|
1734
1617
|
puts " Session ID: #{session_id}"
|
|
1735
1618
|
puts " Reply ID: #{reply_id}"
|
|
1736
1619
|
|
|
1737
1620
|
if !command_string.empty?
|
|
1738
|
-
debug_binary(
|
|
1621
|
+
debug_binary('Command string', command_string)
|
|
1739
1622
|
else
|
|
1740
|
-
puts
|
|
1623
|
+
puts 'Command string: (empty)'
|
|
1741
1624
|
end
|
|
1742
|
-
debug_binary(
|
|
1625
|
+
debug_binary('Final header', buf)
|
|
1743
1626
|
end
|
|
1744
1627
|
|
|
1745
1628
|
buf + command_string
|
|
1746
1629
|
end
|
|
1747
1630
|
|
|
1748
|
-
def send_command(command, command_string =
|
|
1631
|
+
def send_command(command, command_string = ''.b, response_size = 8)
|
|
1749
1632
|
# Check connection status (except for connect and auth commands)
|
|
1750
1633
|
if command != CMD_CONNECT && command != CMD_AUTH && !@connected
|
|
1751
|
-
raise RBZK::ZKErrorConnection,
|
|
1634
|
+
raise RBZK::ZKErrorConnection, 'Instance are not connected.'
|
|
1752
1635
|
end
|
|
1753
1636
|
|
|
1754
1637
|
# In Python, command_string is a bytes object (b'')
|
|
@@ -1782,9 +1665,9 @@ module RBZK
|
|
|
1782
1665
|
if @verbose
|
|
1783
1666
|
puts "\nSending TCP packet:"
|
|
1784
1667
|
puts "Note: In send_command, 'top' variable contains the full packet (header + command packet)"
|
|
1785
|
-
puts
|
|
1786
|
-
debug_binary(
|
|
1787
|
-
debug_binary(
|
|
1668
|
+
puts 'This is because create_tcp_top returns the full packet, not just the header'
|
|
1669
|
+
debug_binary('Command packet (buf)', buf)
|
|
1670
|
+
debug_binary('Full TCP packet (top)', top) # 'top' contains the full packet here
|
|
1788
1671
|
end
|
|
1789
1672
|
|
|
1790
1673
|
@socket.send(top, 0)
|
|
@@ -1793,12 +1676,10 @@ module RBZK
|
|
|
1793
1676
|
|
|
1794
1677
|
if @verbose
|
|
1795
1678
|
puts "\nReceived TCP response:"
|
|
1796
|
-
debug_binary(
|
|
1679
|
+
debug_binary('TCP response', @tcp_data_recv)
|
|
1797
1680
|
end
|
|
1798
1681
|
|
|
1799
|
-
if @tcp_length
|
|
1800
|
-
raise RBZK::ZKNetworkError, "TCP packet invalid"
|
|
1801
|
-
end
|
|
1682
|
+
raise RBZK::ZKNetworkError, 'TCP packet invalid' if @tcp_length.zero?
|
|
1802
1683
|
|
|
1803
1684
|
@header = @tcp_data_recv[8..15].unpack('v4')
|
|
1804
1685
|
@data_recv = @tcp_data_recv[8..-1]
|
|
@@ -1808,10 +1689,8 @@ module RBZK
|
|
|
1808
1689
|
@data_recv = @socket.recv(response_size)
|
|
1809
1690
|
@header = @data_recv[0..7].unpack('S<4')
|
|
1810
1691
|
end
|
|
1811
|
-
rescue => e
|
|
1812
|
-
if @verbose
|
|
1813
|
-
puts "Connection error during send: #{e.message}"
|
|
1814
|
-
end
|
|
1692
|
+
rescue StandardError => e
|
|
1693
|
+
puts "Connection error during send: #{e.message}" if @verbose
|
|
1815
1694
|
raise RBZK::ZKNetworkError, e.message
|
|
1816
1695
|
end
|
|
1817
1696
|
|
|
@@ -1836,9 +1715,7 @@ module RBZK
|
|
|
1836
1715
|
|
|
1837
1716
|
def recv_reply
|
|
1838
1717
|
begin
|
|
1839
|
-
if @verbose
|
|
1840
|
-
puts "Waiting for TCP reply"
|
|
1841
|
-
end
|
|
1718
|
+
puts 'Waiting for TCP reply' if @verbose
|
|
1842
1719
|
|
|
1843
1720
|
# Set a timeout for the read operation
|
|
1844
1721
|
Timeout.timeout(5) do
|
|
@@ -1849,15 +1726,11 @@ module RBZK
|
|
|
1849
1726
|
# Parse TCP header
|
|
1850
1727
|
tcp_format1, tcp_format2, tcp_length = tcp_header.unpack('S<S<I<')
|
|
1851
1728
|
|
|
1852
|
-
if @verbose
|
|
1853
|
-
puts "TCP header: format1=#{tcp_format1}, format2=#{tcp_format2}, length=#{tcp_length}"
|
|
1854
|
-
end
|
|
1729
|
+
puts "TCP header: format1=#{tcp_format1}, format2=#{tcp_format2}, length=#{tcp_length}" if @verbose
|
|
1855
1730
|
|
|
1856
1731
|
# Verify TCP header format
|
|
1857
1732
|
if tcp_format1 != MACHINE_PREPARE_DATA_1 || tcp_format2 != MACHINE_PREPARE_DATA_2
|
|
1858
|
-
if @verbose
|
|
1859
|
-
puts "Invalid TCP header format: #{tcp_format1}, #{tcp_format2}"
|
|
1860
|
-
end
|
|
1733
|
+
puts "Invalid TCP header format: #{tcp_format1}, #{tcp_format2}" if @verbose
|
|
1861
1734
|
return nil
|
|
1862
1735
|
end
|
|
1863
1736
|
|
|
@@ -1876,30 +1749,24 @@ module RBZK
|
|
|
1876
1749
|
data_size = tcp_length - 8
|
|
1877
1750
|
|
|
1878
1751
|
# Read data if available
|
|
1879
|
-
data =
|
|
1880
|
-
if data_size
|
|
1881
|
-
if @verbose
|
|
1882
|
-
puts "Reading #{data_size} bytes of data"
|
|
1883
|
-
end
|
|
1752
|
+
data = ''
|
|
1753
|
+
if data_size.positive?
|
|
1754
|
+
puts "Reading #{data_size} bytes of data" if @verbose
|
|
1884
1755
|
|
|
1885
1756
|
# Read data in chunks to handle large responses (like Python implementation)
|
|
1886
1757
|
remaining = data_size
|
|
1887
|
-
while remaining
|
|
1758
|
+
while remaining.positive?
|
|
1888
1759
|
chunk_size = [ remaining, 4096 ].min
|
|
1889
1760
|
chunk = @socket.read(chunk_size)
|
|
1890
1761
|
if chunk.nil? || chunk.empty?
|
|
1891
|
-
if @verbose
|
|
1892
|
-
puts "Failed to read data chunk, got #{chunk.inspect}"
|
|
1893
|
-
end
|
|
1762
|
+
puts "Failed to read data chunk, got #{chunk.inspect}" if @verbose
|
|
1894
1763
|
break
|
|
1895
1764
|
end
|
|
1896
1765
|
|
|
1897
1766
|
data += chunk
|
|
1898
1767
|
remaining -= chunk.size
|
|
1899
1768
|
|
|
1900
|
-
if @verbose && remaining
|
|
1901
|
-
puts "Read #{chunk.size} bytes, #{remaining} remaining"
|
|
1902
|
-
end
|
|
1769
|
+
puts "Read #{chunk.size} bytes, #{remaining} remaining" if @verbose && remaining.positive?
|
|
1903
1770
|
end
|
|
1904
1771
|
end
|
|
1905
1772
|
|
|
@@ -1910,47 +1777,33 @@ module RBZK
|
|
|
1910
1777
|
@session_id = session_id
|
|
1911
1778
|
|
|
1912
1779
|
# Check command type and handle accordingly (like Python implementation)
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
end
|
|
1780
|
+
case command
|
|
1781
|
+
when CMD_ACK_OK
|
|
1782
|
+
puts 'Received ACK_OK' if @verbose
|
|
1917
1783
|
return cmd_header + data
|
|
1918
|
-
|
|
1919
|
-
if @verbose
|
|
1920
|
-
puts "Received ACK_ERROR"
|
|
1921
|
-
end
|
|
1784
|
+
when CMD_ACK_ERROR
|
|
1785
|
+
puts 'Received ACK_ERROR' if @verbose
|
|
1922
1786
|
return nil
|
|
1923
|
-
|
|
1924
|
-
if @verbose
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
else
|
|
1930
|
-
return nil
|
|
1931
|
-
end
|
|
1787
|
+
when CMD_ACK_DATA
|
|
1788
|
+
puts 'Received ACK_DATA' if @verbose
|
|
1789
|
+
return cmd_header + data if data_size.positive?
|
|
1790
|
+
|
|
1791
|
+
return nil
|
|
1792
|
+
|
|
1932
1793
|
else
|
|
1933
|
-
if @verbose
|
|
1934
|
-
puts "Received unknown command: #{command}"
|
|
1935
|
-
end
|
|
1794
|
+
puts "Received unknown command: #{command}" if @verbose
|
|
1936
1795
|
return cmd_header + data
|
|
1937
1796
|
end
|
|
1938
1797
|
end
|
|
1939
1798
|
rescue Timeout::Error => e
|
|
1940
|
-
if @verbose
|
|
1941
|
-
|
|
1942
|
-
end
|
|
1943
|
-
raise RBZK::ZKErrorResponse, "Timeout waiting for response"
|
|
1799
|
+
puts "Timeout waiting for response: #{e.message}" if @verbose
|
|
1800
|
+
raise RBZK::ZKErrorResponse, 'Timeout waiting for response'
|
|
1944
1801
|
rescue Errno::ECONNRESET, Errno::EPIPE => e
|
|
1945
|
-
if @verbose
|
|
1946
|
-
puts "Connection error during receive: #{e.message}"
|
|
1947
|
-
end
|
|
1802
|
+
puts "Connection error during receive: #{e.message}" if @verbose
|
|
1948
1803
|
raise RBZK::ZKNetworkError, "Connection error: #{e.message}"
|
|
1949
1804
|
rescue Errno::EAGAIN, Errno::EWOULDBLOCK => e
|
|
1950
|
-
if @verbose
|
|
1951
|
-
|
|
1952
|
-
end
|
|
1953
|
-
raise RBZK::ZKErrorResponse, "Timeout waiting for response"
|
|
1805
|
+
puts "Timeout waiting for response: #{e.message}" if @verbose
|
|
1806
|
+
raise RBZK::ZKErrorResponse, 'Timeout waiting for response'
|
|
1954
1807
|
end
|
|
1955
1808
|
|
|
1956
1809
|
nil
|
|
@@ -1960,31 +1813,25 @@ module RBZK
|
|
|
1960
1813
|
if @data_recv && @data_recv.size >= 4
|
|
1961
1814
|
data = @data_recv[0..3]
|
|
1962
1815
|
@data_recv = @data_recv[4..-1]
|
|
1963
|
-
return data.
|
|
1816
|
+
return data.unpack1('L<')
|
|
1964
1817
|
end
|
|
1965
1818
|
|
|
1966
1819
|
0
|
|
1967
1820
|
end
|
|
1968
1821
|
|
|
1969
1822
|
def recv_chunk(size)
|
|
1970
|
-
if @verbose
|
|
1971
|
-
puts "Receiving chunk of #{size} bytes"
|
|
1972
|
-
end
|
|
1823
|
+
puts "Receiving chunk of #{size} bytes" if @verbose
|
|
1973
1824
|
|
|
1974
1825
|
if @data_recv && @data_recv.size >= size
|
|
1975
1826
|
data = @data_recv[0...size]
|
|
1976
1827
|
@data_recv = @data_recv[size..-1]
|
|
1977
1828
|
|
|
1978
|
-
if @verbose
|
|
1979
|
-
puts "Received #{data.size} bytes from buffer"
|
|
1980
|
-
end
|
|
1829
|
+
puts "Received #{data.size} bytes from buffer" if @verbose
|
|
1981
1830
|
|
|
1982
1831
|
return data
|
|
1983
1832
|
end
|
|
1984
1833
|
|
|
1985
|
-
if @verbose
|
|
1986
|
-
puts "Warning: No data available in buffer"
|
|
1987
|
-
end
|
|
1834
|
+
puts 'Warning: No data available in buffer' if @verbose
|
|
1988
1835
|
|
|
1989
1836
|
# Return empty string if no data is available
|
|
1990
1837
|
''
|
|
@@ -2001,11 +1848,11 @@ module RBZK
|
|
|
2001
1848
|
k = 0
|
|
2002
1849
|
|
|
2003
1850
|
32.times do |i|
|
|
2004
|
-
if (key & (1 << i)) != 0
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
|
|
1851
|
+
k = if (key & (1 << i)) != 0
|
|
1852
|
+
(k << 1 | 1)
|
|
1853
|
+
else
|
|
1854
|
+
k << 1
|
|
1855
|
+
end
|
|
2009
1856
|
end
|
|
2010
1857
|
|
|
2011
1858
|
k += session_id
|
|
@@ -2038,9 +1885,7 @@ module RBZK
|
|
|
2038
1885
|
# Note: The third byte is just B, not k[2] ^ B
|
|
2039
1886
|
result = [ k_bytes[0] ^ b, k_bytes[1] ^ b, b, k_bytes[3] ^ b ].pack('C4')
|
|
2040
1887
|
|
|
2041
|
-
if @verbose
|
|
2042
|
-
puts "Final commkey bytes: #{result.bytes.map { |b| "0x#{b.to_s(16).rjust(2, '0')}" }.join(' ')}"
|
|
2043
|
-
end
|
|
1888
|
+
puts "Final commkey bytes: #{result.bytes.map { |b| "0x#{b.to_s(16).rjust(2, '0')}" }.join(' ')}" if @verbose
|
|
2044
1889
|
|
|
2045
1890
|
result
|
|
2046
1891
|
end
|