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