rbzk 0.1.0
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 +7 -0
- data/CHANGELOG.md +13 -0
- data/LICENSE.txt +21 -0
- data/README.md +213 -0
- data/lib/rbzk/attendance.rb +19 -0
- data/lib/rbzk/constants.rb +129 -0
- data/lib/rbzk/exceptions.rb +23 -0
- data/lib/rbzk/finger.rb +18 -0
- data/lib/rbzk/user.rb +34 -0
- data/lib/rbzk/version.rb +5 -0
- data/lib/rbzk/zk.rb +2416 -0
- data/lib/rbzk.rb +19 -0
- metadata +125 -0
data/lib/rbzk/zk.rb
ADDED
@@ -0,0 +1,2416 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'socket'
|
4
|
+
require 'timeout'
|
5
|
+
require 'date'
|
6
|
+
|
7
|
+
module RBZK
|
8
|
+
# Helper class for ZK (like Python's ZK_helper)
|
9
|
+
class ZKHelper
|
10
|
+
def initialize(ip, port = 4370)
|
11
|
+
@ip = ip
|
12
|
+
@port = port
|
13
|
+
@address = [ ip, port ]
|
14
|
+
end
|
15
|
+
|
16
|
+
def test_ping
|
17
|
+
# Like Python's test_ping
|
18
|
+
begin
|
19
|
+
system("ping -c 1 -W 5 #{@ip} > /dev/null 2>&1")
|
20
|
+
return $?.success?
|
21
|
+
rescue => e
|
22
|
+
return false
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def test_tcp
|
27
|
+
# Match Python's test_tcp method exactly
|
28
|
+
begin
|
29
|
+
# Create socket like Python's socket(AF_INET, SOCK_STREAM)
|
30
|
+
client = Socket.new(Socket::AF_INET, Socket::SOCK_STREAM)
|
31
|
+
|
32
|
+
# Set timeout like Python's settimeout(10)
|
33
|
+
client.setsockopt(Socket::SOL_SOCKET, Socket::SO_RCVTIMEO, [ 10, 0 ].pack('l_*'))
|
34
|
+
|
35
|
+
# Use connect_ex like Python (returns error code instead of raising exception)
|
36
|
+
sockaddr = Socket.pack_sockaddr_in(@port, @ip)
|
37
|
+
begin
|
38
|
+
client.connect(sockaddr)
|
39
|
+
result = 0 # Success, like Python's connect_ex returns 0 on success
|
40
|
+
rescue Errno::EISCONN
|
41
|
+
# Already connected
|
42
|
+
result = 0
|
43
|
+
rescue => e
|
44
|
+
# Connection failed, return error code
|
45
|
+
result = e.errno || 1
|
46
|
+
end
|
47
|
+
|
48
|
+
# Close socket
|
49
|
+
client.close
|
50
|
+
|
51
|
+
# Return result code (0 = success, non-zero = error)
|
52
|
+
return result
|
53
|
+
rescue => e
|
54
|
+
# Something went wrong with socket creation
|
55
|
+
return e.errno || 1
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
class ZK
|
61
|
+
include RBZK::Constants
|
62
|
+
|
63
|
+
def initialize(ip, port: 4370, timeout: 60, password: 0, force_udp: false, omit_ping: false, verbose: false, encoding: 'UTF-8')
|
64
|
+
# Match Python's __init__ method
|
65
|
+
RBZK::User.encoding = encoding
|
66
|
+
@address = [ ip, port ]
|
67
|
+
|
68
|
+
@ip = ip
|
69
|
+
@port = port
|
70
|
+
@timeout = timeout
|
71
|
+
@password = password
|
72
|
+
@force_udp = force_udp
|
73
|
+
@omit_ping = omit_ping
|
74
|
+
@verbose = verbose
|
75
|
+
@encoding = encoding
|
76
|
+
|
77
|
+
# Set TCP mode based on force_udp (like Python's self.tcp = not force_udp)
|
78
|
+
@tcp = !force_udp
|
79
|
+
|
80
|
+
# Socket will be created during connect
|
81
|
+
@socket = nil
|
82
|
+
|
83
|
+
# Initialize session variables (like Python)
|
84
|
+
@session_id = 0
|
85
|
+
@reply_id = USHRT_MAX - 1
|
86
|
+
@data_recv = nil
|
87
|
+
@data = nil
|
88
|
+
@connected = false
|
89
|
+
@next_uid = 1
|
90
|
+
|
91
|
+
# Storage for user and attendance data
|
92
|
+
@users = {}
|
93
|
+
@attendances = []
|
94
|
+
@fingers = {}
|
95
|
+
@tcp_header_size = 8
|
96
|
+
|
97
|
+
# Initialize device info variables (like Python)
|
98
|
+
@users = 0
|
99
|
+
@fingers = 0
|
100
|
+
@records = 0
|
101
|
+
@dummy = 0
|
102
|
+
@cards = 0
|
103
|
+
@fingers_cap = 0
|
104
|
+
@users_cap = 0
|
105
|
+
@rec_cap = 0
|
106
|
+
@faces = 0
|
107
|
+
@faces_cap = 0
|
108
|
+
@fingers_av = 0
|
109
|
+
@users_av = 0
|
110
|
+
@rec_av = 0
|
111
|
+
|
112
|
+
# Create helper for ping and TCP tests
|
113
|
+
@helper = ZKHelper.new(ip, port)
|
114
|
+
|
115
|
+
if @verbose
|
116
|
+
puts "ZK instance created for device at #{@ip}:#{@port}"
|
117
|
+
puts "Using #{@force_udp ? 'UDP' : 'TCP'} mode"
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
# Remove this method as we now use the helper class
|
122
|
+
# def test_tcp
|
123
|
+
# end
|
124
|
+
|
125
|
+
def connect
|
126
|
+
# Match Python's connect method
|
127
|
+
return self if @connected
|
128
|
+
|
129
|
+
# Skip ping check if requested (like Python's ommit_ping)
|
130
|
+
if !@omit_ping && !@helper.test_ping
|
131
|
+
raise RBZK::ZKNetworkError, "Can't reach device (ping #{@ip})"
|
132
|
+
end
|
133
|
+
|
134
|
+
# Test TCP connection (like Python's connect)
|
135
|
+
if !@force_udp && @helper.test_tcp == 0
|
136
|
+
# Default user packet size for ZK8
|
137
|
+
@user_packet_size = 72
|
138
|
+
end
|
139
|
+
|
140
|
+
# Create socket (like Python's __create_socket)
|
141
|
+
create_socket
|
142
|
+
|
143
|
+
# Reset session variables (like Python's connect)
|
144
|
+
@session_id = 0
|
145
|
+
@reply_id = USHRT_MAX - 1
|
146
|
+
|
147
|
+
# Send connect command (like Python's connect)
|
148
|
+
if @verbose
|
149
|
+
puts "Sending connect command to device"
|
150
|
+
end
|
151
|
+
|
152
|
+
begin
|
153
|
+
# Send connect command (like Python's connect)
|
154
|
+
# In Python: cmd_response = self.__send_command(const.CMD_CONNECT)
|
155
|
+
# No command string is needed for the connect command
|
156
|
+
cmd_response = send_command(CMD_CONNECT)
|
157
|
+
|
158
|
+
# Update session ID from header (like Python's connect)
|
159
|
+
@session_id = @header[2]
|
160
|
+
|
161
|
+
# Authenticate if needed (like Python's connect)
|
162
|
+
if cmd_response[:code] == CMD_ACK_UNAUTH
|
163
|
+
if @verbose
|
164
|
+
puts "try auth"
|
165
|
+
end
|
166
|
+
|
167
|
+
# Create auth command string (like Python's make_commkey)
|
168
|
+
command_string = make_commkey(@password, @session_id)
|
169
|
+
|
170
|
+
# Send auth command
|
171
|
+
cmd_response = send_command(CMD_AUTH, command_string)
|
172
|
+
end
|
173
|
+
|
174
|
+
# Check response status (like Python's connect)
|
175
|
+
if cmd_response[:status]
|
176
|
+
@connected = true
|
177
|
+
return self
|
178
|
+
else
|
179
|
+
if cmd_response[:code] == CMD_ACK_UNAUTH
|
180
|
+
raise RBZK::ZKErrorResponse, "Unauthenticated"
|
181
|
+
end
|
182
|
+
|
183
|
+
if @verbose
|
184
|
+
puts "Connect error response: #{cmd_response[:code]}"
|
185
|
+
end
|
186
|
+
|
187
|
+
raise RBZK::ZKErrorResponse, "Invalid response: Can't connect"
|
188
|
+
end
|
189
|
+
rescue => e
|
190
|
+
@connected = false
|
191
|
+
if @verbose
|
192
|
+
puts "Connection error: #{e.message}"
|
193
|
+
end
|
194
|
+
raise e
|
195
|
+
end
|
196
|
+
end
|
197
|
+
|
198
|
+
def connected?
|
199
|
+
@connected
|
200
|
+
end
|
201
|
+
|
202
|
+
def disconnect
|
203
|
+
return unless @connected
|
204
|
+
|
205
|
+
self.send_command(CMD_EXIT)
|
206
|
+
self.recv_reply
|
207
|
+
|
208
|
+
@connected = false
|
209
|
+
@socket.close if @socket
|
210
|
+
|
211
|
+
@socket = nil
|
212
|
+
@tcp = nil
|
213
|
+
|
214
|
+
true
|
215
|
+
end
|
216
|
+
|
217
|
+
def enable_device
|
218
|
+
self.send_command(CMD_ENABLEDEVICE)
|
219
|
+
self.recv_reply
|
220
|
+
true
|
221
|
+
end
|
222
|
+
|
223
|
+
def disable_device
|
224
|
+
# Match Python's disable_device method exactly
|
225
|
+
# In Python:
|
226
|
+
# def disable_device(self):
|
227
|
+
# cmd_response = self.__send_command(const.CMD_DISABLEDEVICE)
|
228
|
+
# if cmd_response.get('status'):
|
229
|
+
# self.is_enabled = False
|
230
|
+
# return True
|
231
|
+
# else:
|
232
|
+
# raise ZKErrorResponse("Can't disable device")
|
233
|
+
|
234
|
+
cmd_response = self.send_command(CMD_DISABLEDEVICE)
|
235
|
+
if cmd_response[:status]
|
236
|
+
@is_enabled = false
|
237
|
+
return true
|
238
|
+
else
|
239
|
+
raise RBZK::ZKErrorResponse, "Can't disable device"
|
240
|
+
end
|
241
|
+
end
|
242
|
+
|
243
|
+
def get_firmware_version
|
244
|
+
# Match Python's get_firmware_version method exactly
|
245
|
+
# In Python:
|
246
|
+
# def get_firmware_version(self):
|
247
|
+
# cmd_response = self.__send_command(const.CMD_GET_VERSION,b'', 1024)
|
248
|
+
# if cmd_response.get('status'):
|
249
|
+
# firmware_version = self.__data.split(b'\x00')[0]
|
250
|
+
# return firmware_version.decode()
|
251
|
+
# else:
|
252
|
+
# raise ZKErrorResponse("Can't read frimware version")
|
253
|
+
|
254
|
+
command = CMD_GET_VERSION
|
255
|
+
response_size = 1024
|
256
|
+
response = self.send_command(command, "", response_size)
|
257
|
+
|
258
|
+
if response && response[:status]
|
259
|
+
firmware_version = @data.split("\x00")[0]
|
260
|
+
return firmware_version.to_s
|
261
|
+
else
|
262
|
+
raise RBZK::ZKErrorResponse, "Can't read firmware version"
|
263
|
+
end
|
264
|
+
end
|
265
|
+
|
266
|
+
def get_serialnumber
|
267
|
+
# Match Python's get_serialnumber method exactly
|
268
|
+
# In Python:
|
269
|
+
# def get_serialnumber(self):
|
270
|
+
# command = const.CMD_OPTIONS_RRQ
|
271
|
+
# command_string = b'~SerialNumber\x00'
|
272
|
+
# response_size = 1024
|
273
|
+
# cmd_response = self.__send_command(command, command_string, response_size)
|
274
|
+
# if cmd_response.get('status'):
|
275
|
+
# serialnumber = self.__data.split(b'=', 1)[-1].split(b'\x00')[0]
|
276
|
+
# serialnumber = serialnumber.replace(b'=', b'')
|
277
|
+
# return serialnumber.decode() # string?
|
278
|
+
# else:
|
279
|
+
# raise ZKErrorResponse("Can't read serial number")
|
280
|
+
|
281
|
+
command = CMD_OPTIONS_RRQ
|
282
|
+
command_string = "~SerialNumber\x00".b
|
283
|
+
response_size = 1024
|
284
|
+
|
285
|
+
response = self.send_command(command, command_string, response_size)
|
286
|
+
|
287
|
+
if response && response[:status]
|
288
|
+
serialnumber = @data.split("=", 2)[1]&.split("\x00")[0] || ""
|
289
|
+
serialnumber = serialnumber.gsub("=", "")
|
290
|
+
return serialnumber.to_s
|
291
|
+
else
|
292
|
+
raise RBZK::ZKErrorResponse, "Can't read serial number"
|
293
|
+
end
|
294
|
+
end
|
295
|
+
|
296
|
+
def get_mac
|
297
|
+
# Match Python's get_mac method exactly
|
298
|
+
# In Python:
|
299
|
+
# def get_mac(self):
|
300
|
+
# command = const.CMD_OPTIONS_RRQ
|
301
|
+
# command_string = b'MAC\x00'
|
302
|
+
# response_size = 1024
|
303
|
+
# cmd_response = self.__send_command(command, command_string, response_size)
|
304
|
+
# if cmd_response.get('status'):
|
305
|
+
# mac = self.__data.split(b'=', 1)[-1].split(b'\x00')[0]
|
306
|
+
# return mac.decode()
|
307
|
+
# else:
|
308
|
+
# raise ZKErrorResponse("can't read mac address")
|
309
|
+
|
310
|
+
command = CMD_OPTIONS_RRQ
|
311
|
+
command_string = "MAC\x00".b
|
312
|
+
response_size = 1024
|
313
|
+
|
314
|
+
response = self.send_command(command, command_string, response_size)
|
315
|
+
|
316
|
+
if response && response[:status]
|
317
|
+
mac = @data.split("=", 2)[1]&.split("\x00")[0] || ""
|
318
|
+
return mac.to_s
|
319
|
+
else
|
320
|
+
raise RBZK::ZKErrorResponse, "Can't read MAC address"
|
321
|
+
end
|
322
|
+
end
|
323
|
+
|
324
|
+
def get_device_name
|
325
|
+
# Match Python's get_device_name method exactly
|
326
|
+
# In Python:
|
327
|
+
# def get_device_name(self):
|
328
|
+
# command = const.CMD_OPTIONS_RRQ
|
329
|
+
# command_string = b'~DeviceName\x00'
|
330
|
+
# response_size = 1024
|
331
|
+
# cmd_response = self.__send_command(command, command_string, response_size)
|
332
|
+
# if cmd_response.get('status'):
|
333
|
+
# device = self.__data.split(b'=', 1)[-1].split(b'\x00')[0]
|
334
|
+
# return device.decode()
|
335
|
+
# else:
|
336
|
+
# return ""
|
337
|
+
|
338
|
+
command = CMD_OPTIONS_RRQ
|
339
|
+
command_string = "~DeviceName\x00".b
|
340
|
+
response_size = 1024
|
341
|
+
|
342
|
+
response = self.send_command(command, command_string, response_size)
|
343
|
+
|
344
|
+
if response && response[:status]
|
345
|
+
device = @data.split("=", 2)[1]&.split("\x00")[0] || ""
|
346
|
+
return device.to_s
|
347
|
+
else
|
348
|
+
return ""
|
349
|
+
end
|
350
|
+
end
|
351
|
+
|
352
|
+
def get_face_version
|
353
|
+
# Match Python's get_face_version method exactly
|
354
|
+
# In Python:
|
355
|
+
# def get_face_version(self):
|
356
|
+
# command = const.CMD_OPTIONS_RRQ
|
357
|
+
# command_string = b'ZKFaceVersion\x00'
|
358
|
+
# response_size = 1024
|
359
|
+
# cmd_response = self.__send_command(command, command_string, response_size)
|
360
|
+
# if cmd_response.get('status'):
|
361
|
+
# response = self.__data.split(b'=', 1)[-1].split(b'\x00')[0]
|
362
|
+
# return safe_cast(response, int, 0) if response else 0
|
363
|
+
# else:
|
364
|
+
# return None
|
365
|
+
|
366
|
+
command = CMD_OPTIONS_RRQ
|
367
|
+
command_string = "ZKFaceVersion\x00".b
|
368
|
+
response_size = 1024
|
369
|
+
|
370
|
+
response = self.send_command(command, command_string, response_size)
|
371
|
+
|
372
|
+
if response && response[:status]
|
373
|
+
version = @data.split("=", 2)[1]&.split("\x00")[0] || ""
|
374
|
+
return version.to_i rescue 0
|
375
|
+
else
|
376
|
+
return nil
|
377
|
+
end
|
378
|
+
end
|
379
|
+
|
380
|
+
def get_extend_fmt
|
381
|
+
# Match Python's get_extend_fmt method exactly
|
382
|
+
# In Python:
|
383
|
+
# def get_extend_fmt(self):
|
384
|
+
# command = const.CMD_OPTIONS_RRQ
|
385
|
+
# command_string = b'~ExtendFmt\x00'
|
386
|
+
# response_size = 1024
|
387
|
+
# cmd_response = self.__send_command(command, command_string, response_size)
|
388
|
+
# if cmd_response.get('status'):
|
389
|
+
# fmt = (self.__data.split(b'=', 1)[-1].split(b'\x00')[0])
|
390
|
+
# return safe_cast(fmt, int, 0) if fmt else 0
|
391
|
+
# else:
|
392
|
+
# self._clear_error(command_string)
|
393
|
+
# return None
|
394
|
+
|
395
|
+
command = CMD_OPTIONS_RRQ
|
396
|
+
command_string = "~ExtendFmt\x00".b
|
397
|
+
response_size = 1024
|
398
|
+
|
399
|
+
response = self.send_command(command, command_string, response_size)
|
400
|
+
|
401
|
+
if response && response[:status]
|
402
|
+
fmt = @data.split("=", 2)[1]&.split("\x00")[0] || ""
|
403
|
+
return fmt.to_i rescue 0
|
404
|
+
else
|
405
|
+
# In Python, this would call self._clear_error(command_string)
|
406
|
+
# We don't have that method, so we'll just return nil
|
407
|
+
return nil
|
408
|
+
end
|
409
|
+
end
|
410
|
+
|
411
|
+
def get_platform
|
412
|
+
# Match Python's get_platform method exactly
|
413
|
+
# In Python:
|
414
|
+
# def get_platform(self):
|
415
|
+
# command = const.CMD_OPTIONS_RRQ
|
416
|
+
# command_string = b'~Platform\x00'
|
417
|
+
# response_size = 1024
|
418
|
+
# cmd_response = self.__send_command(command, command_string, response_size)
|
419
|
+
# if cmd_response.get('status'):
|
420
|
+
# platform = self.__data.split(b'=', 1)[-1].split(b'\x00')[0]
|
421
|
+
# platform = platform.replace(b'=', b'')
|
422
|
+
# return platform.decode()
|
423
|
+
# else:
|
424
|
+
# raise ZKErrorResponse("Can't read platform name")
|
425
|
+
|
426
|
+
command = CMD_OPTIONS_RRQ
|
427
|
+
command_string = "~Platform\x00".b
|
428
|
+
response_size = 1024
|
429
|
+
|
430
|
+
response = self.send_command(command, command_string, response_size)
|
431
|
+
|
432
|
+
if response && response[:status]
|
433
|
+
platform = @data.split("=", 2)[1]&.split("\x00")[0] || ""
|
434
|
+
platform = platform.gsub("=", "")
|
435
|
+
return platform.to_s
|
436
|
+
else
|
437
|
+
raise RBZK::ZKErrorResponse, "Can't read platform name"
|
438
|
+
end
|
439
|
+
end
|
440
|
+
|
441
|
+
def get_fp_version
|
442
|
+
# Match Python's get_fp_version method exactly
|
443
|
+
# In Python:
|
444
|
+
# def get_fp_version(self):
|
445
|
+
# command = const.CMD_OPTIONS_RRQ
|
446
|
+
# command_string = b'~ZKFPVersion\x00'
|
447
|
+
# response_size = 1024
|
448
|
+
# cmd_response = self.__send_command(command, command_string, response_size)
|
449
|
+
# if cmd_response.get('status'):
|
450
|
+
# response = self.__data.split(b'=', 1)[-1].split(b'\x00')[0]
|
451
|
+
# response = response.replace(b'=', b'')
|
452
|
+
# return safe_cast(response, int, 0) if response else 0
|
453
|
+
# else:
|
454
|
+
# raise ZKErrorResponse("can't read fingerprint version")
|
455
|
+
|
456
|
+
command = CMD_OPTIONS_RRQ
|
457
|
+
command_string = "~ZKFPVersion\x00".b
|
458
|
+
response_size = 1024
|
459
|
+
|
460
|
+
response = self.send_command(command, command_string, response_size)
|
461
|
+
|
462
|
+
if response && response[:status]
|
463
|
+
version = @data.split("=", 2)[1]&.split("\x00")[0] || ""
|
464
|
+
version = version.gsub("=", "")
|
465
|
+
return version.to_i rescue 0
|
466
|
+
else
|
467
|
+
raise RBZK::ZKErrorResponse, "Can't read fingerprint version"
|
468
|
+
end
|
469
|
+
end
|
470
|
+
|
471
|
+
def restart
|
472
|
+
self.send_command(CMD_RESTART)
|
473
|
+
self.recv_reply
|
474
|
+
true
|
475
|
+
end
|
476
|
+
|
477
|
+
def poweroff
|
478
|
+
self.send_command(CMD_POWEROFF)
|
479
|
+
self.recv_reply
|
480
|
+
true
|
481
|
+
end
|
482
|
+
|
483
|
+
def test_voice(index = 0)
|
484
|
+
# Match Python's test_voice method exactly
|
485
|
+
# In Python:
|
486
|
+
# def test_voice(self, index=0):
|
487
|
+
# command = const.CMD_TESTVOICE
|
488
|
+
# command_string = pack("I", index)
|
489
|
+
# cmd_response = self.__send_command(command, command_string)
|
490
|
+
# if cmd_response.get('status'):
|
491
|
+
# return True
|
492
|
+
# else:
|
493
|
+
# return False
|
494
|
+
|
495
|
+
command_string = [ index ].pack('L<')
|
496
|
+
response = self.send_command(CMD_TESTVOICE, command_string)
|
497
|
+
|
498
|
+
if response && response[:status]
|
499
|
+
return true
|
500
|
+
else
|
501
|
+
return false
|
502
|
+
end
|
503
|
+
end
|
504
|
+
|
505
|
+
# Helper method to read data with buffer, similar to Python's read_with_buffer
|
506
|
+
def read_with_buffer(command, fct = 0, ext = 0)
|
507
|
+
# Match Python's read_with_buffer method exactly
|
508
|
+
# In Python:
|
509
|
+
# def read_with_buffer(self, command, fct=0 ,ext=0):
|
510
|
+
# """
|
511
|
+
# Test read info with buffered command (ZK6: 1503)
|
512
|
+
# """
|
513
|
+
# if self.tcp:
|
514
|
+
# MAX_CHUNK = 0xFFc0
|
515
|
+
# else:
|
516
|
+
# MAX_CHUNK = 16 * 1024
|
517
|
+
# command_string = pack('<bhii', 1, command, fct, ext)
|
518
|
+
# if self.verbose: print ("rwb cs", command_string)
|
519
|
+
# response_size = 1024
|
520
|
+
# data = []
|
521
|
+
# start = 0
|
522
|
+
# cmd_response = self.__send_command(const._CMD_PREPARE_BUFFER, command_string, response_size)
|
523
|
+
# if not cmd_response.get('status'):
|
524
|
+
# raise ZKErrorResponse("RWB Not supported")
|
525
|
+
# if cmd_response['code'] == const.CMD_DATA:
|
526
|
+
# if self.tcp:
|
527
|
+
# if self.verbose: print ("DATA! is {} bytes, tcp length is {}".format(len(self.__data), self.__tcp_length))
|
528
|
+
# if len(self.__data) < (self.__tcp_length - 8):
|
529
|
+
# need = (self.__tcp_length - 8) - len(self.__data)
|
530
|
+
# if self.verbose: print ("need more data: {}".format(need))
|
531
|
+
# more_data = self.__recieve_raw_data(need)
|
532
|
+
# return b''.join([self.__data, more_data]), len(self.__data) + len(more_data)
|
533
|
+
# else:
|
534
|
+
# if self.verbose: print ("Enough data")
|
535
|
+
# size = len(self.__data)
|
536
|
+
# return self.__data, size
|
537
|
+
# else:
|
538
|
+
# size = len(self.__data)
|
539
|
+
# return self.__data, size
|
540
|
+
# size = unpack('I', self.__data[1:5])[0]
|
541
|
+
# if self.verbose: print ("size fill be %i" % size)
|
542
|
+
# remain = size % MAX_CHUNK
|
543
|
+
# packets = (size-remain) // MAX_CHUNK # should be size /16k
|
544
|
+
# if self.verbose: print ("rwb: #{} packets of max {} bytes, and extra {} bytes remain".format(packets, MAX_CHUNK, remain))
|
545
|
+
# for _wlk in range(packets):
|
546
|
+
# data.append(self.__read_chunk(start,MAX_CHUNK))
|
547
|
+
# start += MAX_CHUNK
|
548
|
+
# if remain:
|
549
|
+
# data.append(self.__read_chunk(start, remain))
|
550
|
+
# start += remain
|
551
|
+
# self.free_data()
|
552
|
+
# if self.verbose: print ("_read w/chunk %i bytes" % start)
|
553
|
+
# return b''.join(data), start
|
554
|
+
|
555
|
+
if @verbose
|
556
|
+
puts "Reading data with buffer: command=#{command}, fct=#{fct}, ext=#{ext}"
|
557
|
+
end
|
558
|
+
|
559
|
+
# Set max chunk size based on connection type
|
560
|
+
max_chunk = @tcp ? 0xFFc0 : 16 * 1024
|
561
|
+
|
562
|
+
# In Python: command_string = pack('<bhii', 1, command, fct, ext)
|
563
|
+
# Note: In Python, the format '<bhii' means:
|
564
|
+
# < - little endian
|
565
|
+
# b - signed char (1 byte)
|
566
|
+
# h - short (2 bytes)
|
567
|
+
# i - int (4 bytes)
|
568
|
+
# i - int (4 bytes)
|
569
|
+
# In Ruby, we need to use:
|
570
|
+
# c - signed char (1 byte) to match Python's 'b'
|
571
|
+
# s - short (2 bytes)
|
572
|
+
# l - long (4 bytes)
|
573
|
+
# l - long (4 bytes)
|
574
|
+
# with < for little-endian
|
575
|
+
command_string = [ 1, command, fct, ext ].pack('cs<l<l<')
|
576
|
+
|
577
|
+
if @verbose
|
578
|
+
puts "Command string: #{python_format(command_string)}"
|
579
|
+
end
|
580
|
+
|
581
|
+
# In Python: cmd_response = self.__send_command(const._CMD_PREPARE_BUFFER, command_string, response_size)
|
582
|
+
# Note: In Python, const._CMD_PREPARE_BUFFER is 1503
|
583
|
+
response_size = 1024
|
584
|
+
data = []
|
585
|
+
start = 0
|
586
|
+
response = self.send_command(CMD_PREPARE_BUFFER, command_string, response_size)
|
587
|
+
|
588
|
+
if !response || !response[:status]
|
589
|
+
raise RBZK::ZKErrorResponse, "Read with buffer not supported"
|
590
|
+
end
|
591
|
+
|
592
|
+
# Get data from the response
|
593
|
+
data = @data
|
594
|
+
|
595
|
+
if @verbose
|
596
|
+
puts "Received #{data.size} bytes of data"
|
597
|
+
end
|
598
|
+
|
599
|
+
# Check if we need more data
|
600
|
+
if response[:code] == CMD_DATA
|
601
|
+
if @tcp
|
602
|
+
if @verbose
|
603
|
+
puts "DATA! is #{data.size} bytes, tcp length is #{@tcp_length}"
|
604
|
+
end
|
605
|
+
|
606
|
+
if data.size < (@tcp_length - 8)
|
607
|
+
need = (@tcp_length - 8) - data.size
|
608
|
+
if @verbose
|
609
|
+
puts "need more data: #{need}"
|
610
|
+
end
|
611
|
+
|
612
|
+
# In Python: more_data = self.__recieve_raw_data(need)
|
613
|
+
more_data = receive_raw_data(need)
|
614
|
+
|
615
|
+
if @verbose
|
616
|
+
puts "Read #{more_data.size} more bytes"
|
617
|
+
end
|
618
|
+
|
619
|
+
# Combine the data
|
620
|
+
result = data + more_data
|
621
|
+
return result, data.size + more_data.size
|
622
|
+
else
|
623
|
+
if @verbose
|
624
|
+
puts "Enough data"
|
625
|
+
end
|
626
|
+
size = data.size
|
627
|
+
return data, size
|
628
|
+
end
|
629
|
+
else
|
630
|
+
size = data.size
|
631
|
+
return data, size
|
632
|
+
end
|
633
|
+
end
|
634
|
+
|
635
|
+
# Get the size from the first 4 bytes
|
636
|
+
# In Python: size = unpack('I', self.__data[1:5])[0]
|
637
|
+
# In Ruby, 'L<' is an unsigned long (4 bytes) in little-endian format, which matches Python's 'I'
|
638
|
+
size = data[1..4].unpack('L<')[0]
|
639
|
+
|
640
|
+
if @verbose
|
641
|
+
puts "size fill be #{size}"
|
642
|
+
end
|
643
|
+
|
644
|
+
# Calculate chunks
|
645
|
+
remain = size % max_chunk
|
646
|
+
# In Python: packets = (size-remain) // MAX_CHUNK # should be size /16k
|
647
|
+
# In Ruby, we need to use integer division to match Python's // operator
|
648
|
+
packets = (size - remain).div(max_chunk)
|
649
|
+
|
650
|
+
if @verbose
|
651
|
+
puts "rwb: ##{packets} packets of max #{max_chunk} bytes, and extra #{remain} bytes remain"
|
652
|
+
end
|
653
|
+
|
654
|
+
# Read chunks
|
655
|
+
result_data = []
|
656
|
+
start = 0
|
657
|
+
|
658
|
+
packets.times do
|
659
|
+
if @verbose
|
660
|
+
puts "recieve chunk: prepare data size is #{max_chunk}"
|
661
|
+
end
|
662
|
+
chunk = read_chunk(start, max_chunk)
|
663
|
+
result_data << chunk
|
664
|
+
start += max_chunk
|
665
|
+
end
|
666
|
+
|
667
|
+
if remain > 0
|
668
|
+
if @verbose
|
669
|
+
puts "recieve chunk: prepare data size is #{remain}"
|
670
|
+
end
|
671
|
+
chunk = read_chunk(start, remain)
|
672
|
+
result_data << chunk
|
673
|
+
start += remain
|
674
|
+
end
|
675
|
+
|
676
|
+
# Free data (equivalent to Python's self.free_data())
|
677
|
+
free_data
|
678
|
+
|
679
|
+
if @verbose
|
680
|
+
puts "_read w/chunk #{start} bytes"
|
681
|
+
end
|
682
|
+
|
683
|
+
# In Python: return b''.join(data), start
|
684
|
+
result = result_data.join
|
685
|
+
return result, start
|
686
|
+
end
|
687
|
+
|
688
|
+
# Helper method to get data size from the current data
|
689
|
+
def get_data_size
|
690
|
+
# Match Python's __get_data_size method exactly
|
691
|
+
# In Python:
|
692
|
+
# def __get_data_size(self):
|
693
|
+
# """internal function to get data size from the packet"""
|
694
|
+
# if len(self.__data) >= 4:
|
695
|
+
# size = unpack('I', self.__data[:4])[0]
|
696
|
+
# return size
|
697
|
+
# else:
|
698
|
+
# return 0
|
699
|
+
|
700
|
+
if @data && @data.size >= 4
|
701
|
+
size = @data[0...4].unpack('L<')[0]
|
702
|
+
return size
|
703
|
+
else
|
704
|
+
return 0
|
705
|
+
end
|
706
|
+
end
|
707
|
+
|
708
|
+
# Helper method to test TCP header
|
709
|
+
def test_tcp_top(data)
|
710
|
+
# Match Python's __test_tcp_top method exactly
|
711
|
+
# In Python:
|
712
|
+
# def __test_tcp_top(self, packet):
|
713
|
+
# """test a TCP packet header"""
|
714
|
+
# if not packet:
|
715
|
+
# return False
|
716
|
+
# if len(packet) < 8:
|
717
|
+
# self.__tcp_length = 0
|
718
|
+
# self.__response = const.CMD_TCP_STILL_ALIVE
|
719
|
+
# return True
|
720
|
+
#
|
721
|
+
# if len(packet) < 8:
|
722
|
+
# self.__tcp_length = 0
|
723
|
+
# self.__response = const.CMD_TCP_STILL_ALIVE
|
724
|
+
# return True
|
725
|
+
# top, self.__session_id, self.__reply_id, self.__tcp_length = unpack('<HHHI', packet[:8])
|
726
|
+
# self.__response = top
|
727
|
+
# if self.verbose: print ("tcp top is {}, session id is {}, reply id is {}, tcp length is {}".format(
|
728
|
+
# self.__response, self.__session_id, self.__reply_id, self.__tcp_length))
|
729
|
+
# return True
|
730
|
+
|
731
|
+
if !data || data.empty?
|
732
|
+
return false
|
733
|
+
end
|
734
|
+
|
735
|
+
if data.size < 8
|
736
|
+
@tcp_length = 0
|
737
|
+
@response = CMD_TCP_STILL_ALIVE
|
738
|
+
return true
|
739
|
+
end
|
740
|
+
|
741
|
+
top, @session_id, @reply_id, @tcp_length = data[0...8].unpack('S<S<S<L<')
|
742
|
+
@response = top
|
743
|
+
|
744
|
+
if @verbose
|
745
|
+
puts "tcp top is #{@response}, session id is #{@session_id}, reply id is #{@reply_id}, tcp length is #{@tcp_length}"
|
746
|
+
end
|
747
|
+
|
748
|
+
return true
|
749
|
+
end
|
750
|
+
|
751
|
+
# Helper method to receive TCP data
|
752
|
+
def receive_tcp_data(data_recv, size)
|
753
|
+
data = []
|
754
|
+
tcp_length = test_tcp_top(data_recv)
|
755
|
+
|
756
|
+
puts "tcp_length #{tcp_length}, size #{size}" if @verbose
|
757
|
+
|
758
|
+
if tcp_length <= 0
|
759
|
+
puts "Incorrect tcp packet" if @verbose
|
760
|
+
return nil, "".b
|
761
|
+
end
|
762
|
+
|
763
|
+
if (tcp_length - 8) < size
|
764
|
+
puts "tcp length too small... retrying" if @verbose
|
765
|
+
|
766
|
+
# Recursive call to handle smaller packet
|
767
|
+
resp, bh = receive_tcp_data(data_recv, tcp_length - 8)
|
768
|
+
data << resp if resp
|
769
|
+
size -= resp.size
|
770
|
+
|
771
|
+
puts "new tcp DATA packet to fill misssing #{size}" if @verbose
|
772
|
+
|
773
|
+
# Get more data to fill missing
|
774
|
+
data_recv = bh + @socket.recv(size + 16)
|
775
|
+
|
776
|
+
puts "new tcp DATA starting with #{data_recv.size} bytes" if @verbose
|
777
|
+
|
778
|
+
# Another recursive call with new data
|
779
|
+
resp, bh = receive_tcp_data(data_recv, size)
|
780
|
+
data << resp
|
781
|
+
|
782
|
+
puts "for misssing #{size} recieved #{resp ? resp.size : 0} with extra #{bh.size}" if @verbose
|
783
|
+
|
784
|
+
return data.join, bh
|
785
|
+
end
|
786
|
+
|
787
|
+
received = data_recv.size
|
788
|
+
|
789
|
+
puts "received #{received}, size #{size}" if @verbose
|
790
|
+
|
791
|
+
# In Python: response = unpack('HHHH', data_recv[8:16])[0]
|
792
|
+
# This unpacks 4 shorts (8 bytes) but only uses the first one
|
793
|
+
response = data_recv[8...16].unpack('S<S<S<S<')[0]
|
794
|
+
|
795
|
+
if received >= (size + 32)
|
796
|
+
if response == CMD_DATA
|
797
|
+
resp = data_recv[16...(size + 16)]
|
798
|
+
|
799
|
+
puts "resp complete len #{resp.size}" if @verbose
|
800
|
+
|
801
|
+
return resp, data_recv[(size + 16)..]
|
802
|
+
else
|
803
|
+
puts "incorrect response!!! #{response}" if @verbose
|
804
|
+
|
805
|
+
return nil, "".b
|
806
|
+
end
|
807
|
+
else
|
808
|
+
puts "try DATA incomplete (actual valid #{received - 16})" if @verbose
|
809
|
+
|
810
|
+
data << data_recv[16...(size + 16)]
|
811
|
+
size -= received - 16
|
812
|
+
broken_header = "".b
|
813
|
+
|
814
|
+
if size < 0
|
815
|
+
broken_header = data_recv[size..]
|
816
|
+
|
817
|
+
if @verbose
|
818
|
+
puts "broken: #{broken_header.bytes.map { |b| format('%02x', b) }.join}"
|
819
|
+
end
|
820
|
+
end
|
821
|
+
|
822
|
+
if size > 0
|
823
|
+
data_recv = receive_raw_data(size)
|
824
|
+
data << data_recv
|
825
|
+
end
|
826
|
+
|
827
|
+
[ data.join, broken_header ]
|
828
|
+
end
|
829
|
+
end
|
830
|
+
|
831
|
+
# Helper method to receive a chunk (like Python's __recieve_chunk)
|
832
|
+
def receive_chunk
|
833
|
+
if @response == CMD_DATA
|
834
|
+
if @tcp
|
835
|
+
puts "_rc_DATA! is #{@data.size} bytes, tcp length is #{@tcp_length}" if @verbose
|
836
|
+
|
837
|
+
if @data.size < (@tcp_length - 8)
|
838
|
+
need = (@tcp_length - 8) - @data.size
|
839
|
+
puts "need more data: #{need}" if @verbose
|
840
|
+
more_data = receive_raw_data(need)
|
841
|
+
return @data + more_data
|
842
|
+
else
|
843
|
+
puts "Enough data" if @verbose
|
844
|
+
return @data
|
845
|
+
end
|
846
|
+
else
|
847
|
+
puts "_rc len is #{@data.size}" if @verbose
|
848
|
+
return @data
|
849
|
+
end
|
850
|
+
elsif @response == CMD_PREPARE_DATA
|
851
|
+
data = []
|
852
|
+
size = get_data_size
|
853
|
+
|
854
|
+
puts "recieve chunk: prepare data size is #{size}" if @verbose
|
855
|
+
|
856
|
+
if @tcp
|
857
|
+
if @data.size >= (8 + size)
|
858
|
+
data_recv = @data[8..]
|
859
|
+
else
|
860
|
+
# [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]
|
861
|
+
data_recv = @data[8..] + @socket.recv(size + 32)
|
862
|
+
end
|
863
|
+
|
864
|
+
puts "data_recv: #{python_format(data_recv)}" if @verbose
|
865
|
+
|
866
|
+
# hault_for_debuggg_DO_NOT_REMOVE
|
867
|
+
resp, broken_header = receive_tcp_data(data_recv, size)
|
868
|
+
data << resp if resp
|
869
|
+
|
870
|
+
# get CMD_ACK_OK
|
871
|
+
if broken_header.size < 16
|
872
|
+
data_recv = broken_header + @socket.recv(16)
|
873
|
+
else
|
874
|
+
data_recv = broken_header
|
875
|
+
end
|
876
|
+
|
877
|
+
if data_recv.size < 16
|
878
|
+
puts "trying to complete broken ACK #{data_recv.size} /16"
|
879
|
+
if @verbose
|
880
|
+
puts "data_recv: #{data_recv.bytes.map { |b| "0x#{b.to_s(16).rjust(2, '0')}" }.join(' ')}"
|
881
|
+
end
|
882
|
+
data_recv += @socket.recv(16 - data_recv.size) # TODO: CHECK HERE_!
|
883
|
+
end
|
884
|
+
|
885
|
+
if !test_tcp_top(data_recv)
|
886
|
+
if @verbose
|
887
|
+
puts "invalid chunk tcp ACK OK"
|
888
|
+
end
|
889
|
+
return nil
|
890
|
+
end
|
891
|
+
|
892
|
+
# In Python: response = unpack('HHHH', data_recv[8:16])[0]
|
893
|
+
# This unpacks 4 shorts (8 bytes) but only uses the first one
|
894
|
+
# In Ruby, we need to use 'S<4' to unpack 4 shorts in little-endian format
|
895
|
+
response = data_recv[8...16].unpack('S<4')[0]
|
896
|
+
|
897
|
+
if response == CMD_ACK_OK
|
898
|
+
if @verbose
|
899
|
+
puts "chunk tcp ACK OK!"
|
900
|
+
end
|
901
|
+
return data.join
|
902
|
+
end
|
903
|
+
|
904
|
+
if @verbose
|
905
|
+
puts "bad response #{format_as_python_bytes(data_recv)}"
|
906
|
+
puts "data: #{data.map { |d| format_as_python_bytes(d) }.join(', ')}"
|
907
|
+
end
|
908
|
+
|
909
|
+
return nil
|
910
|
+
else
|
911
|
+
# Non-TCP implementation
|
912
|
+
loop do
|
913
|
+
data_recv = @socket.recv(1024 + 8)
|
914
|
+
response = data_recv[0...8].unpack('S<S<S<S<')[0]
|
915
|
+
|
916
|
+
if @verbose
|
917
|
+
puts "# packet response is: #{response}"
|
918
|
+
end
|
919
|
+
|
920
|
+
if response == CMD_DATA
|
921
|
+
data << data_recv[8..]
|
922
|
+
size -= 1024
|
923
|
+
elsif response == CMD_ACK_OK
|
924
|
+
break
|
925
|
+
else
|
926
|
+
if @verbose
|
927
|
+
puts "broken!"
|
928
|
+
end
|
929
|
+
break
|
930
|
+
end
|
931
|
+
|
932
|
+
if @verbose
|
933
|
+
puts "still needs #{size}"
|
934
|
+
end
|
935
|
+
end
|
936
|
+
|
937
|
+
return data.join
|
938
|
+
end
|
939
|
+
else
|
940
|
+
if @verbose
|
941
|
+
puts "invalid response #{@response}"
|
942
|
+
end
|
943
|
+
return nil
|
944
|
+
end
|
945
|
+
end
|
946
|
+
|
947
|
+
# Helper method to receive raw data (like Python's __recieve_raw_data)
|
948
|
+
def receive_raw_data(size)
|
949
|
+
# Match Python's __recieve_raw_data method exactly
|
950
|
+
# In Python:
|
951
|
+
# def __recieve_raw_data(self, size):
|
952
|
+
# """ partial data ? """
|
953
|
+
# data = []
|
954
|
+
# if self.verbose: print ("expecting {} bytes raw data".format(size))
|
955
|
+
# while size > 0:
|
956
|
+
# data_recv = self.__sock.recv(size)
|
957
|
+
# recieved = len(data_recv)
|
958
|
+
# if self.verbose: print ("partial recv {}".format(recieved))
|
959
|
+
# if recieved < 100 and self.verbose: print (" recv {}".format(codecs.encode(data_recv, 'hex')))
|
960
|
+
# data.append(data_recv)
|
961
|
+
# size -= recieved
|
962
|
+
# if self.verbose: print ("still need {}".format(size))
|
963
|
+
# return b''.join(data)
|
964
|
+
|
965
|
+
data = []
|
966
|
+
if @verbose
|
967
|
+
puts "expecting #{size} bytes raw data"
|
968
|
+
end
|
969
|
+
|
970
|
+
while size > 0
|
971
|
+
data_recv = @socket.recv(size)
|
972
|
+
received = data_recv.size
|
973
|
+
|
974
|
+
if @verbose
|
975
|
+
puts "partial recv #{received}"
|
976
|
+
if received < 100
|
977
|
+
puts " recv #{data_recv.bytes.map { |b| "0x#{b.to_s(16).rjust(2, '0')}" }.join(' ')}"
|
978
|
+
end
|
979
|
+
end
|
980
|
+
|
981
|
+
data << data_recv
|
982
|
+
size -= received
|
983
|
+
|
984
|
+
if @verbose
|
985
|
+
puts "still need #{size}"
|
986
|
+
end
|
987
|
+
end
|
988
|
+
|
989
|
+
data.join
|
990
|
+
end
|
991
|
+
|
992
|
+
# Helper method to clear buffer (like Python's free_data)
|
993
|
+
def free_data
|
994
|
+
# Match Python's free_data method exactly
|
995
|
+
# In Python:
|
996
|
+
# def free_data(self):
|
997
|
+
# """
|
998
|
+
# clear buffer
|
999
|
+
#
|
1000
|
+
# :return: bool
|
1001
|
+
# """
|
1002
|
+
# command = const.CMD_FREE_DATA
|
1003
|
+
# cmd_response = self.__send_command(command)
|
1004
|
+
# if cmd_response.get('status'):
|
1005
|
+
# return True
|
1006
|
+
# else:
|
1007
|
+
# raise ZKErrorResponse("can't free data")
|
1008
|
+
|
1009
|
+
command = CMD_FREE_DATA
|
1010
|
+
response = self.send_command(command)
|
1011
|
+
|
1012
|
+
if response && response[:status]
|
1013
|
+
return true
|
1014
|
+
else
|
1015
|
+
raise RBZK::ZKErrorResponse, "Can't free data"
|
1016
|
+
end
|
1017
|
+
end
|
1018
|
+
|
1019
|
+
# Helper method to read a chunk of data
|
1020
|
+
def read_chunk(start, size)
|
1021
|
+
# Match Python's __read_chunk method exactly
|
1022
|
+
# In Python:
|
1023
|
+
# def __read_chunk(self, start, size):
|
1024
|
+
# """
|
1025
|
+
# read a chunk from buffer
|
1026
|
+
# """
|
1027
|
+
# for _retries in range(3):
|
1028
|
+
# command = const._CMD_READ_BUFFER
|
1029
|
+
# command_string = pack('<ii', start, size)
|
1030
|
+
# if self.tcp:
|
1031
|
+
# response_size = size + 32
|
1032
|
+
# else:
|
1033
|
+
# response_size = 1024 + 8
|
1034
|
+
# cmd_response = self.__send_command(command, command_string, response_size)
|
1035
|
+
# data = self.__recieve_chunk()
|
1036
|
+
# if data is not None:
|
1037
|
+
# return data
|
1038
|
+
# else:
|
1039
|
+
# raise ZKErrorResponse("can't read chunk %i:[%i]" % (start, size))
|
1040
|
+
|
1041
|
+
if @verbose
|
1042
|
+
puts "Reading chunk: start=#{start}, size=#{size}"
|
1043
|
+
end
|
1044
|
+
|
1045
|
+
3.times do |_retries|
|
1046
|
+
# In Python: command = const._CMD_READ_BUFFER (which is 1504)
|
1047
|
+
# In Ruby, we should use CMD_READ_BUFFER (1504) instead of CMD_READFILE_DATA (81)
|
1048
|
+
command = 1504 # CMD_READ_BUFFER
|
1049
|
+
|
1050
|
+
# In Python: command_string = pack('<ii', start, size)
|
1051
|
+
command_string = [ start, size ].pack('l<l<')
|
1052
|
+
|
1053
|
+
# In Python: response_size = size + 32 if self.tcp else 1024 + 8
|
1054
|
+
response_size = @tcp ? size + 32 : 1024 + 8
|
1055
|
+
|
1056
|
+
# In Python: cmd_response = self.__send_command(command, command_string, response_size)
|
1057
|
+
response = self.send_command(command, command_string, response_size)
|
1058
|
+
|
1059
|
+
if !response || !response[:status]
|
1060
|
+
if @verbose
|
1061
|
+
puts "Failed to read chunk on attempt #{_retries + 1}"
|
1062
|
+
end
|
1063
|
+
next
|
1064
|
+
end
|
1065
|
+
|
1066
|
+
# In Python: data = self.__recieve_chunk()
|
1067
|
+
data = receive_chunk
|
1068
|
+
|
1069
|
+
if data
|
1070
|
+
if @verbose
|
1071
|
+
puts "Received chunk of #{data.size} bytes"
|
1072
|
+
end
|
1073
|
+
|
1074
|
+
return data
|
1075
|
+
end
|
1076
|
+
end
|
1077
|
+
|
1078
|
+
# If we get here, all retries failed
|
1079
|
+
raise RBZK::ZKErrorResponse, "can't read chunk #{start}:[#{size}]"
|
1080
|
+
end
|
1081
|
+
|
1082
|
+
def get_users
|
1083
|
+
# Read sizes
|
1084
|
+
self.read_sizes
|
1085
|
+
|
1086
|
+
puts "Device has #{@users} users" if @verbose
|
1087
|
+
|
1088
|
+
# If no users, return empty array
|
1089
|
+
if @users == 0
|
1090
|
+
@next_uid = 1
|
1091
|
+
@next_user_id = '1'
|
1092
|
+
return []
|
1093
|
+
end
|
1094
|
+
|
1095
|
+
users = []
|
1096
|
+
max_uid = 0
|
1097
|
+
userdata, size = self.read_with_buffer(CMD_USERTEMP_RRQ, FCT_USER)
|
1098
|
+
puts "user size #{size} (= #{userdata.length})" if @verbose
|
1099
|
+
|
1100
|
+
if size <= 4
|
1101
|
+
puts "WRN: missing user data"
|
1102
|
+
return []
|
1103
|
+
end
|
1104
|
+
|
1105
|
+
total_size = userdata[0, 4].unpack1('L<')
|
1106
|
+
@user_packet_size = total_size / @users
|
1107
|
+
|
1108
|
+
if ![ 28, 72 ].include?(@user_packet_size)
|
1109
|
+
puts "WRN packet size would be #{@user_packet_size}" if @verbose
|
1110
|
+
end
|
1111
|
+
|
1112
|
+
userdata = userdata[4..-1]
|
1113
|
+
|
1114
|
+
if @user_packet_size == 28
|
1115
|
+
while userdata.length >= 28
|
1116
|
+
uid, privilege, password, name, card, group_id, timezone, user_id = userdata.ljust(28, "\x00")[0, 28].unpack('S<Ca5a8L<xCs<L<')
|
1117
|
+
max_uid = uid if uid > max_uid
|
1118
|
+
password = password.split("\x00").first&.force_encoding(@encoding)&.encode('UTF-8', invalid: :replace)
|
1119
|
+
name = name.split("\x00").first&.force_encoding(@encoding)&.encode('UTF-8', invalid: :replace)&.strip
|
1120
|
+
group_id = group_id.to_s
|
1121
|
+
user_id = user_id.to_s
|
1122
|
+
name = "NN-#{user_id}" if !name
|
1123
|
+
user = User.new(uid, name, privilege, password, group_id, user_id, card)
|
1124
|
+
users << user
|
1125
|
+
puts "[6]user: #{uid}, #{privilege}, #{password}, #{name}, #{card}, #{group_id}, #{timezone}, #{user_id}" if @verbose
|
1126
|
+
userdata = userdata[28..-1]
|
1127
|
+
end
|
1128
|
+
else
|
1129
|
+
while userdata.length >= 72
|
1130
|
+
uid, privilege, password, name, card, group_id, user_id = userdata.ljust(72, "\x00")[0, 72].unpack('S<Ca8a24L<xa7xa24')
|
1131
|
+
max_uid = uid if uid > max_uid
|
1132
|
+
password = password.split("\x00").first&.force_encoding(@encoding)&.encode('UTF-8', invalid: :replace)
|
1133
|
+
name = name.split("\x00").first&.force_encoding(@encoding)&.encode('UTF-8', invalid: :replace)&.strip
|
1134
|
+
group_id = group_id.split("\x00").first&.force_encoding(@encoding)&.encode('UTF-8', invalid: :replace)&.strip
|
1135
|
+
user_id = user_id.split("\x00").first&.force_encoding(@encoding)&.encode('UTF-8', invalid: :replace)
|
1136
|
+
name = "NN-#{user_id}" if !name
|
1137
|
+
user = User.new(uid, name, privilege, password, group_id, user_id, card)
|
1138
|
+
users << user
|
1139
|
+
userdata = userdata[72..-1]
|
1140
|
+
end
|
1141
|
+
end
|
1142
|
+
|
1143
|
+
max_uid += 1
|
1144
|
+
@next_uid = max_uid
|
1145
|
+
@next_user_id = max_uid.to_s
|
1146
|
+
|
1147
|
+
loop do
|
1148
|
+
if users.any? { |u| u.user_id == @next_user_id }
|
1149
|
+
max_uid += 1
|
1150
|
+
@next_user_id = max_uid.to_s
|
1151
|
+
else
|
1152
|
+
break
|
1153
|
+
end
|
1154
|
+
end
|
1155
|
+
|
1156
|
+
users
|
1157
|
+
end
|
1158
|
+
|
1159
|
+
def get_attendance_logs
|
1160
|
+
# Match Python's get_attendance method exactly
|
1161
|
+
# In Python:
|
1162
|
+
# def get_attendance(self):
|
1163
|
+
# self.read_sizes()
|
1164
|
+
# if self.records == 0:
|
1165
|
+
# return []
|
1166
|
+
# users = self.get_users()
|
1167
|
+
# if self.verbose: print (users)
|
1168
|
+
# attendances = []
|
1169
|
+
# attendance_data, size = self.read_with_buffer(const.CMD_ATTLOG_RRQ)
|
1170
|
+
# if size < 4:
|
1171
|
+
# if self.verbose: print ("WRN: no attendance data")
|
1172
|
+
# return []
|
1173
|
+
# total_size = unpack("I", attendance_data[:4])[0]
|
1174
|
+
# record_size = total_size // self.records
|
1175
|
+
# if self.verbose: print ("record_size is ", record_size)
|
1176
|
+
# attendance_data = attendance_data[4:]
|
1177
|
+
# if record_size == 8:
|
1178
|
+
# while len(attendance_data) >= 8:
|
1179
|
+
# uid, status, timestamp, punch = unpack('HB4sB', attendance_data.ljust(8, b'\x00')[:8])
|
1180
|
+
# if self.verbose: print (codecs.encode(attendance_data[:8], 'hex'))
|
1181
|
+
# attendance_data = attendance_data[8:]
|
1182
|
+
# tuser = list(filter(lambda x: x.uid == uid, users))
|
1183
|
+
# if not tuser:
|
1184
|
+
# user_id = str(uid)
|
1185
|
+
# else:
|
1186
|
+
# user_id = tuser[0].user_id
|
1187
|
+
# timestamp = self.__decode_time(timestamp)
|
1188
|
+
# attendance = Attendance(user_id, timestamp, status, punch, uid)
|
1189
|
+
# attendances.append(attendance)
|
1190
|
+
# elif record_size == 16:
|
1191
|
+
# while len(attendance_data) >= 16:
|
1192
|
+
# user_id, timestamp, status, punch, reserved, workcode = unpack('<I4sBB2sI', attendance_data.ljust(16, b'\x00')[:16])
|
1193
|
+
# user_id = str(user_id)
|
1194
|
+
# if self.verbose: print(codecs.encode(attendance_data[:16], 'hex'))
|
1195
|
+
# attendance_data = attendance_data[16:]
|
1196
|
+
# tuser = list(filter(lambda x: x.user_id == user_id, users))
|
1197
|
+
# if not tuser:
|
1198
|
+
# if self.verbose: print("no uid {}", user_id)
|
1199
|
+
# uid = str(user_id)
|
1200
|
+
# tuser = list(filter(lambda x: x.uid == user_id, users))
|
1201
|
+
# if not tuser:
|
1202
|
+
# uid = str(user_id)
|
1203
|
+
# else:
|
1204
|
+
# uid = tuser[0].uid
|
1205
|
+
# user_id = tuser[0].user_id
|
1206
|
+
# else:
|
1207
|
+
# uid = tuser[0].uid
|
1208
|
+
# timestamp = self.__decode_time(timestamp)
|
1209
|
+
# attendance = Attendance(user_id, timestamp, status, punch, uid)
|
1210
|
+
# attendances.append(attendance)
|
1211
|
+
# else:
|
1212
|
+
# while len(attendance_data) >= 40:
|
1213
|
+
# uid, user_id, status, timestamp, punch, space = unpack('<H24sB4sB8s', attendance_data.ljust(40, b'\x00')[:40])
|
1214
|
+
# if self.verbose: print (codecs.encode(attendance_data[:40], 'hex'))
|
1215
|
+
# user_id = (user_id.split(b'\x00')[0]).decode(errors='ignore')
|
1216
|
+
# timestamp = self.__decode_time(timestamp)
|
1217
|
+
# attendance = Attendance(user_id, timestamp, status, punch, uid)
|
1218
|
+
# attendances.append(attendance)
|
1219
|
+
# attendance_data = attendance_data[record_size:]
|
1220
|
+
# return attendances
|
1221
|
+
|
1222
|
+
# First, read device sizes to get record count
|
1223
|
+
self.read_sizes
|
1224
|
+
|
1225
|
+
# If no records, return empty array
|
1226
|
+
if @records == 0
|
1227
|
+
return []
|
1228
|
+
end
|
1229
|
+
|
1230
|
+
# Get users for lookup
|
1231
|
+
users = self.get_users
|
1232
|
+
|
1233
|
+
if @verbose
|
1234
|
+
puts "Found #{users.size} users"
|
1235
|
+
end
|
1236
|
+
|
1237
|
+
logs = []
|
1238
|
+
|
1239
|
+
# Read attendance data with buffer
|
1240
|
+
attendance_data, size = self.read_with_buffer(CMD_ATTLOG_RRQ)
|
1241
|
+
|
1242
|
+
if size < 4
|
1243
|
+
if @verbose
|
1244
|
+
puts "WRN: no attendance data"
|
1245
|
+
end
|
1246
|
+
return []
|
1247
|
+
end
|
1248
|
+
|
1249
|
+
# Get total size from first 4 bytes
|
1250
|
+
total_size = attendance_data[0...4].unpack('I')[0]
|
1251
|
+
|
1252
|
+
# Calculate record size
|
1253
|
+
record_size = @records > 0 ? total_size / @records : 0
|
1254
|
+
|
1255
|
+
if @verbose
|
1256
|
+
puts "record_size is #{record_size}"
|
1257
|
+
end
|
1258
|
+
|
1259
|
+
# Remove the first 4 bytes (total size)
|
1260
|
+
attendance_data = attendance_data[4..-1]
|
1261
|
+
|
1262
|
+
if record_size == 8
|
1263
|
+
# Handle 8-byte records
|
1264
|
+
while attendance_data && attendance_data.size >= 8
|
1265
|
+
# In Python: uid, status, timestamp, punch = unpack('HB4sB', attendance_data.ljust(8, b'\x00')[:8])
|
1266
|
+
uid, status, timestamp_raw, punch = attendance_data[0...8].ljust(8, "\x00".b).unpack('S<C4sC')
|
1267
|
+
|
1268
|
+
if @verbose
|
1269
|
+
puts "Attendance data (hex): #{attendance_data[0...8].bytes.map { |b| "0x#{b.to_s(16).rjust(2, '0')}" }.join(' ')}"
|
1270
|
+
end
|
1271
|
+
|
1272
|
+
attendance_data = attendance_data[8..-1]
|
1273
|
+
|
1274
|
+
# Look up user by uid
|
1275
|
+
tuser = users.find { |u| u.uid == uid }
|
1276
|
+
if !tuser
|
1277
|
+
user_id = uid.to_s
|
1278
|
+
else
|
1279
|
+
user_id = tuser.user_id
|
1280
|
+
end
|
1281
|
+
|
1282
|
+
# Decode timestamp
|
1283
|
+
timestamp = decode_time(timestamp_raw)
|
1284
|
+
|
1285
|
+
# Create attendance record
|
1286
|
+
attendance = RBZK::Attendance.new(user_id, timestamp, status, punch, uid)
|
1287
|
+
logs << attendance
|
1288
|
+
end
|
1289
|
+
elsif record_size == 16
|
1290
|
+
# Handle 16-byte records
|
1291
|
+
while attendance_data && attendance_data.size >= 16
|
1292
|
+
# In Python: user_id, timestamp, status, punch, reserved, workcode = unpack('<I4sBB2sI', attendance_data.ljust(16, b'\x00')[:16])
|
1293
|
+
user_id_raw, timestamp_raw, status, punch, reserved, workcode = attendance_data[0...16].ljust(16, "\x00".b).unpack('L<4sCCa2L<')
|
1294
|
+
|
1295
|
+
if @verbose
|
1296
|
+
puts "Attendance data (hex): #{attendance_data[0...16].bytes.map { |b| "0x#{b.to_s(16).rjust(2, '0')}" }.join(' ')}"
|
1297
|
+
end
|
1298
|
+
|
1299
|
+
attendance_data = attendance_data[16..-1]
|
1300
|
+
|
1301
|
+
# Convert user_id to string
|
1302
|
+
user_id = user_id_raw.to_s
|
1303
|
+
|
1304
|
+
# Look up user by user_id and uid
|
1305
|
+
tuser = users.find { |u| u.user_id == user_id }
|
1306
|
+
if !tuser
|
1307
|
+
if @verbose
|
1308
|
+
puts "no uid #{user_id}"
|
1309
|
+
end
|
1310
|
+
uid = user_id
|
1311
|
+
tuser = users.find { |u| u.uid.to_s == user_id }
|
1312
|
+
if !tuser
|
1313
|
+
uid = user_id
|
1314
|
+
else
|
1315
|
+
uid = tuser.uid
|
1316
|
+
user_id = tuser.user_id
|
1317
|
+
end
|
1318
|
+
else
|
1319
|
+
uid = tuser.uid
|
1320
|
+
end
|
1321
|
+
|
1322
|
+
# Decode timestamp
|
1323
|
+
timestamp = decode_time(timestamp_raw)
|
1324
|
+
|
1325
|
+
# Create attendance record
|
1326
|
+
attendance = RBZK::Attendance.new(user_id, timestamp, status, punch, uid)
|
1327
|
+
logs << attendance
|
1328
|
+
end
|
1329
|
+
else
|
1330
|
+
# Handle 40-byte records (default)
|
1331
|
+
while attendance_data && attendance_data.size >= 40
|
1332
|
+
# In Python: uid, user_id, status, timestamp, punch, space = unpack('<H24sB4sB8s', attendance_data.ljust(40, b'\x00')[:40])
|
1333
|
+
uid, user_id_raw, status, timestamp_raw, punch, space = attendance_data[0...40].ljust(40, "\x00".b).unpack('S<a24Ca4Ca8')
|
1334
|
+
|
1335
|
+
if @verbose
|
1336
|
+
puts "Attendance data (hex): #{attendance_data[0...40].bytes.map { |b| "0x#{b.to_s(16).rjust(2, '0')}" }.join(' ')}"
|
1337
|
+
end
|
1338
|
+
|
1339
|
+
# Extract user_id from null-terminated string
|
1340
|
+
user_id = user_id_raw.split("\x00")[0].to_s
|
1341
|
+
|
1342
|
+
# Decode timestamp
|
1343
|
+
timestamp = decode_time(timestamp_raw)
|
1344
|
+
|
1345
|
+
# Create attendance record
|
1346
|
+
attendance = RBZK::Attendance.new(user_id, timestamp, status, punch, uid)
|
1347
|
+
logs << attendance
|
1348
|
+
|
1349
|
+
attendance_data = attendance_data[record_size..-1]
|
1350
|
+
end
|
1351
|
+
end
|
1352
|
+
|
1353
|
+
logs
|
1354
|
+
end
|
1355
|
+
|
1356
|
+
# Decode a timestamp retrieved from the timeclock
|
1357
|
+
# Match Python's __decode_time method exactly
|
1358
|
+
# In Python:
|
1359
|
+
# def __decode_time(self, t):
|
1360
|
+
# t = unpack("<I", t)[0]
|
1361
|
+
# second = t % 60
|
1362
|
+
# t = t // 60
|
1363
|
+
#
|
1364
|
+
# minute = t % 60
|
1365
|
+
# t = t // 60
|
1366
|
+
#
|
1367
|
+
# hour = t % 24
|
1368
|
+
# t = t // 24
|
1369
|
+
#
|
1370
|
+
# day = t % 31 + 1
|
1371
|
+
# t = t // 31
|
1372
|
+
#
|
1373
|
+
# month = t % 12 + 1
|
1374
|
+
# t = t // 12
|
1375
|
+
#
|
1376
|
+
# year = t + 2000
|
1377
|
+
#
|
1378
|
+
# d = datetime(year, month, day, hour, minute, second)
|
1379
|
+
#
|
1380
|
+
# return d
|
1381
|
+
def decode_time(t)
|
1382
|
+
# Convert binary timestamp to integer
|
1383
|
+
t = t.unpack("L<")[0]
|
1384
|
+
|
1385
|
+
# Extract time components
|
1386
|
+
second = t % 60
|
1387
|
+
t = t / 60
|
1388
|
+
|
1389
|
+
minute = t % 60
|
1390
|
+
t = t / 60
|
1391
|
+
|
1392
|
+
hour = t % 24
|
1393
|
+
t = t / 24
|
1394
|
+
|
1395
|
+
day = t % 31 + 1
|
1396
|
+
t = t / 31
|
1397
|
+
|
1398
|
+
month = t % 12 + 1
|
1399
|
+
t = t / 12
|
1400
|
+
|
1401
|
+
year = t + 2000
|
1402
|
+
|
1403
|
+
# Create Time object
|
1404
|
+
Time.new(year, month, day, hour, minute, second)
|
1405
|
+
end
|
1406
|
+
|
1407
|
+
# Decode a timestamp in hex format (6 bytes)
|
1408
|
+
# Match Python's __decode_timehex method
|
1409
|
+
# In Python:
|
1410
|
+
# def __decode_timehex(self, timehex):
|
1411
|
+
# year, month, day, hour, minute, second = unpack("6B", timehex)
|
1412
|
+
# year += 2000
|
1413
|
+
# d = datetime(year, month, day, hour, minute, second)
|
1414
|
+
# return d
|
1415
|
+
def decode_timehex(timehex)
|
1416
|
+
# Extract time components
|
1417
|
+
year, month, day, hour, minute, second = timehex.unpack("C6")
|
1418
|
+
year += 2000
|
1419
|
+
|
1420
|
+
# Create Time object
|
1421
|
+
Time.new(year, month, day, hour, minute, second)
|
1422
|
+
end
|
1423
|
+
|
1424
|
+
# Encode a timestamp for the device
|
1425
|
+
# Match Python's __encode_time method
|
1426
|
+
# In Python:
|
1427
|
+
# def __encode_time(self, t):
|
1428
|
+
# d = (
|
1429
|
+
# ((t.year % 100) * 12 * 31 + ((t.month - 1) * 31) + t.day - 1) *
|
1430
|
+
# (24 * 60 * 60) + (t.hour * 60 + t.minute) * 60 + t.second
|
1431
|
+
# )
|
1432
|
+
# return d
|
1433
|
+
def encode_time(t)
|
1434
|
+
# Calculate encoded timestamp
|
1435
|
+
d = (
|
1436
|
+
((t.year % 100) * 12 * 31 + ((t.month - 1) * 31) + t.day - 1) *
|
1437
|
+
(24 * 60 * 60) + (t.hour * 60 + t.minute) * 60 + t.second
|
1438
|
+
)
|
1439
|
+
d
|
1440
|
+
end
|
1441
|
+
|
1442
|
+
def get_time
|
1443
|
+
# Match Python's get_time method exactly
|
1444
|
+
# In Python:
|
1445
|
+
# def get_time(self):
|
1446
|
+
# command = const.CMD_GET_TIME
|
1447
|
+
# response_size = 1032
|
1448
|
+
# cmd_response = self.__send_command(command, b'', response_size)
|
1449
|
+
# if cmd_response.get('status'):
|
1450
|
+
# return self.__decode_time(self.__data[:4])
|
1451
|
+
# else:
|
1452
|
+
# raise ZKErrorResponse("can't get time")
|
1453
|
+
|
1454
|
+
command = CMD_GET_TIME
|
1455
|
+
response_size = 1032
|
1456
|
+
response = self.send_command(command, "", response_size)
|
1457
|
+
|
1458
|
+
if response && response[:status]
|
1459
|
+
return decode_time(@data[0...4])
|
1460
|
+
else
|
1461
|
+
raise RBZK::ZKErrorResponse, "Can't get time"
|
1462
|
+
end
|
1463
|
+
end
|
1464
|
+
|
1465
|
+
def set_time(timestamp = nil)
|
1466
|
+
# Match Python's set_time method exactly
|
1467
|
+
# In Python:
|
1468
|
+
# def set_time(self, timestamp):
|
1469
|
+
# command = const.CMD_SET_TIME
|
1470
|
+
# command_string = pack(b'I', self.__encode_time(timestamp))
|
1471
|
+
# cmd_response = self.__send_command(command, command_string)
|
1472
|
+
# if cmd_response.get('status'):
|
1473
|
+
# return True
|
1474
|
+
# else:
|
1475
|
+
# raise ZKErrorResponse("can't set time")
|
1476
|
+
|
1477
|
+
# Default to current time if not provided
|
1478
|
+
timestamp ||= Time.now
|
1479
|
+
|
1480
|
+
command = CMD_SET_TIME
|
1481
|
+
command_string = [ encode_time(timestamp) ].pack('L<')
|
1482
|
+
response = self.send_command(command, command_string)
|
1483
|
+
|
1484
|
+
if response && response[:status]
|
1485
|
+
return true
|
1486
|
+
else
|
1487
|
+
raise RBZK::ZKErrorResponse, "Can't set time"
|
1488
|
+
end
|
1489
|
+
end
|
1490
|
+
|
1491
|
+
def clear_attendance_logs
|
1492
|
+
self.send_command(CMD_CLEAR_ATTLOG)
|
1493
|
+
self.recv_reply
|
1494
|
+
true
|
1495
|
+
end
|
1496
|
+
|
1497
|
+
def clear_data
|
1498
|
+
self.send_command(CMD_CLEAR_DATA)
|
1499
|
+
self.recv_reply
|
1500
|
+
true
|
1501
|
+
end
|
1502
|
+
|
1503
|
+
# Helper method to print binary data in Python format
|
1504
|
+
def format_as_python_bytes(binary_string)
|
1505
|
+
return "b''" if binary_string.nil? || binary_string.empty?
|
1506
|
+
|
1507
|
+
result = "b'"
|
1508
|
+
binary_string.each_byte do |byte|
|
1509
|
+
case byte
|
1510
|
+
when 0x0d # Carriage return - Python shows as \r
|
1511
|
+
result += "\\r"
|
1512
|
+
when 0x0a # Line feed - Python shows as \n
|
1513
|
+
result += "\\n"
|
1514
|
+
when 0x09 # Tab - Python shows as \t
|
1515
|
+
result += "\\t"
|
1516
|
+
when 0x07 # Bell - Python can show as \a or \x07
|
1517
|
+
result += "\\x07"
|
1518
|
+
when 0x08 # Backspace - Python shows as \b
|
1519
|
+
result += "\\b"
|
1520
|
+
when 0x0c # Form feed - Python shows as \f
|
1521
|
+
result += "\\f"
|
1522
|
+
when 0x0b # Vertical tab - Python shows as \v
|
1523
|
+
result += "\\v"
|
1524
|
+
when 0x5c # Backslash - Python shows as \\
|
1525
|
+
result += "\\\\"
|
1526
|
+
when 0x27 # Single quote - Python shows as \'
|
1527
|
+
result += "\\'"
|
1528
|
+
when 0x22 # Double quote - Python shows as \"
|
1529
|
+
result += "\\\""
|
1530
|
+
when 32..126 # Printable ASCII
|
1531
|
+
result += byte.chr
|
1532
|
+
else
|
1533
|
+
# All other bytes - Python shows as \xHH
|
1534
|
+
result += "\\x#{byte.to_s(16).rjust(2, '0')}"
|
1535
|
+
end
|
1536
|
+
end
|
1537
|
+
result += "'"
|
1538
|
+
result
|
1539
|
+
end
|
1540
|
+
|
1541
|
+
# Helper method to compare binary data between Python and Ruby
|
1542
|
+
def compare_binary(binary_string, python_expected)
|
1543
|
+
ruby_formatted = format_as_python_bytes(binary_string)
|
1544
|
+
|
1545
|
+
if @verbose
|
1546
|
+
puts "Ruby binary: #{ruby_formatted}"
|
1547
|
+
puts "Python expected: #{python_expected}"
|
1548
|
+
|
1549
|
+
if ruby_formatted != python_expected
|
1550
|
+
puts "DIFFERENCE DETECTED!"
|
1551
|
+
# Show byte-by-byte comparison
|
1552
|
+
ruby_bytes = binary_string.bytes
|
1553
|
+
# Parse Python bytes string (format: b'\x01\x02')
|
1554
|
+
python_bytes = []
|
1555
|
+
python_str = python_expected[2..-2] # Remove b'' wrapper
|
1556
|
+
i = 0
|
1557
|
+
while i < python_str.length
|
1558
|
+
if python_str[i] == '\\' && python_str[i + 1] == 'x'
|
1559
|
+
# Handle \xNN format
|
1560
|
+
hex_val = python_str[i + 2..i + 3]
|
1561
|
+
python_bytes << hex_val.to_i(16)
|
1562
|
+
i += 4
|
1563
|
+
elsif python_str[i] == '\\'
|
1564
|
+
# Handle escape sequences
|
1565
|
+
case python_str[i + 1]
|
1566
|
+
when 't'
|
1567
|
+
python_bytes << 9
|
1568
|
+
when 'n'
|
1569
|
+
python_bytes << 10
|
1570
|
+
when 'r'
|
1571
|
+
python_bytes << 13
|
1572
|
+
when '\\'
|
1573
|
+
python_bytes << 92
|
1574
|
+
when "'"
|
1575
|
+
python_bytes << 39
|
1576
|
+
end
|
1577
|
+
i += 2
|
1578
|
+
else
|
1579
|
+
# Regular character
|
1580
|
+
python_bytes << python_str[i].ord
|
1581
|
+
i += 1
|
1582
|
+
end
|
1583
|
+
end
|
1584
|
+
|
1585
|
+
# Show differences
|
1586
|
+
puts "Byte-by-byte comparison:"
|
1587
|
+
max_len = [ ruby_bytes.length, python_bytes.length ].max
|
1588
|
+
(0...max_len).each do |j|
|
1589
|
+
ruby_byte = j < ruby_bytes.length ? ruby_bytes[j] : nil
|
1590
|
+
python_byte = j < python_bytes.length ? python_bytes[j] : nil
|
1591
|
+
match = ruby_byte == python_byte ? "✓" : "✗"
|
1592
|
+
puts " Byte #{j}: Ruby=#{ruby_byte.nil? ? 'nil' : "0x#{ruby_byte.to_s(16).rjust(2, '0')}"}, Python=#{python_byte.nil? ? 'nil' : "0x#{python_byte.to_s(16).rjust(2, '0')}"} #{match}"
|
1593
|
+
end
|
1594
|
+
else
|
1595
|
+
puts "Binary data matches exactly!"
|
1596
|
+
end
|
1597
|
+
end
|
1598
|
+
|
1599
|
+
ruby_formatted == python_expected
|
1600
|
+
end
|
1601
|
+
|
1602
|
+
# Alias for backward compatibility
|
1603
|
+
alias python_format format_as_python_bytes
|
1604
|
+
|
1605
|
+
# Helper method to debug binary data in Python format only
|
1606
|
+
def debug_python_binary(label, data)
|
1607
|
+
puts "#{label}: #{format_as_python_bytes(data)}"
|
1608
|
+
end
|
1609
|
+
|
1610
|
+
def read_sizes
|
1611
|
+
# Match Python's read_sizes method exactly
|
1612
|
+
# In Python:
|
1613
|
+
# def read_sizes(self):
|
1614
|
+
# command = const.CMD_GET_FREE_SIZES
|
1615
|
+
# response_size = 1024
|
1616
|
+
# cmd_response = self.__send_command(command,b'', response_size)
|
1617
|
+
# if cmd_response.get('status'):
|
1618
|
+
# if self.verbose: print(codecs.encode(self.__data,'hex'))
|
1619
|
+
# size = len(self.__data)
|
1620
|
+
# if len(self.__data) >= 80:
|
1621
|
+
# fields = unpack('20i', self.__data[:80])
|
1622
|
+
# self.users = fields[4]
|
1623
|
+
# self.fingers = fields[6]
|
1624
|
+
# self.records = fields[8]
|
1625
|
+
# self.dummy = fields[10] #???
|
1626
|
+
# self.cards = fields[12]
|
1627
|
+
# self.fingers_cap = fields[14]
|
1628
|
+
# self.users_cap = fields[15]
|
1629
|
+
# self.rec_cap = fields[16]
|
1630
|
+
# self.fingers_av = fields[17]
|
1631
|
+
# self.users_av = fields[18]
|
1632
|
+
# self.rec_av = fields[19]
|
1633
|
+
# self.__data = self.__data[80:]
|
1634
|
+
|
1635
|
+
command = CMD_GET_FREE_SIZES
|
1636
|
+
response_size = 1024
|
1637
|
+
cmd_response = self.send_command(command, "", response_size)
|
1638
|
+
|
1639
|
+
if cmd_response && cmd_response[:status]
|
1640
|
+
if @verbose
|
1641
|
+
puts "Data hex: #{@data.bytes.map { |b| "0x#{b.to_s(16).rjust(2, '0')}" }.join(' ')}"
|
1642
|
+
puts "Data Python format: #{python_format(@data)}"
|
1643
|
+
end
|
1644
|
+
|
1645
|
+
size = @data.size
|
1646
|
+
if @verbose
|
1647
|
+
puts "Data size: #{size} bytes"
|
1648
|
+
end
|
1649
|
+
|
1650
|
+
if @data.size >= 80
|
1651
|
+
# In Python: fields = unpack('20i', self.__data[:80])
|
1652
|
+
# In Ruby, 'l<' is a signed 32-bit integer (4 bytes) in little-endian format, which matches Python's 'i'
|
1653
|
+
fields = @data[0...80].unpack('l<20')
|
1654
|
+
|
1655
|
+
if @verbose
|
1656
|
+
puts "Unpacked fields: #{fields.inspect}"
|
1657
|
+
end
|
1658
|
+
|
1659
|
+
@users = fields[4]
|
1660
|
+
@fingers = fields[6]
|
1661
|
+
@records = fields[8]
|
1662
|
+
@dummy = fields[10] # ???
|
1663
|
+
@cards = fields[12]
|
1664
|
+
@fingers_cap = fields[14]
|
1665
|
+
@users_cap = fields[15]
|
1666
|
+
@rec_cap = fields[16]
|
1667
|
+
@fingers_av = fields[17]
|
1668
|
+
@users_av = fields[18]
|
1669
|
+
@rec_av = fields[19]
|
1670
|
+
@data = @data[80..-1]
|
1671
|
+
|
1672
|
+
# Check for face information (added to match Python implementation)
|
1673
|
+
if @data.size >= 12 # face info
|
1674
|
+
# In Python: fields = unpack('3i', self.__data[:12]) #dirty hack! we need more information
|
1675
|
+
face_fields = @data[0...12].unpack('l<3')
|
1676
|
+
@faces = face_fields[0]
|
1677
|
+
@faces_cap = face_fields[2]
|
1678
|
+
|
1679
|
+
if @verbose
|
1680
|
+
puts "Face info: faces=#{@faces}, capacity=#{@faces_cap}"
|
1681
|
+
end
|
1682
|
+
end
|
1683
|
+
|
1684
|
+
if @verbose
|
1685
|
+
puts "Device info: users=#{@users}, fingers=#{@fingers}, records=#{@records}"
|
1686
|
+
puts "Capacity: users=#{@users_cap}, fingers=#{@fingers_cap}, records=#{@rec_cap}"
|
1687
|
+
end
|
1688
|
+
|
1689
|
+
return true
|
1690
|
+
end
|
1691
|
+
else
|
1692
|
+
raise RBZK::ZKErrorResponse, "Can't read sizes"
|
1693
|
+
end
|
1694
|
+
|
1695
|
+
false
|
1696
|
+
end
|
1697
|
+
|
1698
|
+
def get_free_sizes
|
1699
|
+
self.send_command(CMD_GET_FREE_SIZES)
|
1700
|
+
reply = self.recv_reply
|
1701
|
+
|
1702
|
+
if reply && reply.size >= 8
|
1703
|
+
sizes_data = reply[8..-1].unpack('S<*')
|
1704
|
+
|
1705
|
+
return {
|
1706
|
+
users: sizes_data[0],
|
1707
|
+
fingers: sizes_data[2],
|
1708
|
+
capacity: sizes_data[4],
|
1709
|
+
logs: sizes_data[6],
|
1710
|
+
passwords: sizes_data[8]
|
1711
|
+
}
|
1712
|
+
end
|
1713
|
+
|
1714
|
+
nil
|
1715
|
+
end
|
1716
|
+
|
1717
|
+
def get_templates
|
1718
|
+
fingers = []
|
1719
|
+
|
1720
|
+
self.send_command(CMD_PREPARE_DATA, [ FCT_FINGERTMP ].pack('C'))
|
1721
|
+
self.recv_reply
|
1722
|
+
|
1723
|
+
data_size = self.recv_long
|
1724
|
+
templates_data = self.recv_chunk(data_size)
|
1725
|
+
|
1726
|
+
if templates_data && !templates_data.empty?
|
1727
|
+
offset = 0
|
1728
|
+
while offset < data_size
|
1729
|
+
if data_size - offset >= 608
|
1730
|
+
template_data = templates_data[offset..offset + 608]
|
1731
|
+
uid, fid, valid, template = template_data.unpack('S<S<C a*')
|
1732
|
+
|
1733
|
+
fingers << RBZK::Finger.new(uid, fid, valid, template)
|
1734
|
+
end
|
1735
|
+
|
1736
|
+
offset += 608
|
1737
|
+
end
|
1738
|
+
end
|
1739
|
+
|
1740
|
+
fingers
|
1741
|
+
end
|
1742
|
+
|
1743
|
+
def get_user_template(uid, finger_id)
|
1744
|
+
self.send_command(CMD_GET_USERTEMP, [ uid, finger_id ].pack('S<S<'))
|
1745
|
+
reply = self.recv_reply
|
1746
|
+
|
1747
|
+
if reply && reply.size >= 8
|
1748
|
+
template_data = reply[8..-1]
|
1749
|
+
valid = template_data[0].ord
|
1750
|
+
template = template_data[1..-1]
|
1751
|
+
|
1752
|
+
return RBZK::Finger.new(uid, finger_id, valid, template)
|
1753
|
+
end
|
1754
|
+
|
1755
|
+
nil
|
1756
|
+
end
|
1757
|
+
|
1758
|
+
private
|
1759
|
+
|
1760
|
+
def create_socket
|
1761
|
+
# Match Python's __create_socket method exactly
|
1762
|
+
if @verbose
|
1763
|
+
puts "Creating socket for #{@tcp ? 'TCP' : 'UDP'} connection"
|
1764
|
+
end
|
1765
|
+
|
1766
|
+
if @tcp
|
1767
|
+
# Create TCP socket (like Python's socket(AF_INET, SOCK_STREAM))
|
1768
|
+
@socket = Socket.new(Socket::AF_INET, Socket::SOCK_STREAM)
|
1769
|
+
@socket.setsockopt(Socket::SOL_SOCKET, Socket::SO_RCVTIMEO, [ @timeout, 0 ].pack('l_*'))
|
1770
|
+
|
1771
|
+
# Connect with connect_ex (like Python's connect_ex)
|
1772
|
+
begin
|
1773
|
+
# Use connect_ex like Python (returns error code instead of raising exception)
|
1774
|
+
sockaddr = Socket.pack_sockaddr_in(@port, @ip)
|
1775
|
+
@socket.connect_nonblock(sockaddr)
|
1776
|
+
if @verbose
|
1777
|
+
puts "TCP socket connected successfully"
|
1778
|
+
end
|
1779
|
+
rescue IO::WaitWritable
|
1780
|
+
# Socket is in progress of connecting
|
1781
|
+
ready = IO.select(nil, [ @socket ], nil, @timeout)
|
1782
|
+
if ready
|
1783
|
+
begin
|
1784
|
+
@socket.connect_nonblock(sockaddr)
|
1785
|
+
rescue Errno::EISCONN
|
1786
|
+
# Already connected, which is fine
|
1787
|
+
if @verbose
|
1788
|
+
puts "TCP socket connected successfully"
|
1789
|
+
end
|
1790
|
+
rescue => e
|
1791
|
+
# Connection failed
|
1792
|
+
if @verbose
|
1793
|
+
puts "TCP socket connection failed: #{e.message}"
|
1794
|
+
end
|
1795
|
+
raise e
|
1796
|
+
end
|
1797
|
+
else
|
1798
|
+
# Connection timed out
|
1799
|
+
if @verbose
|
1800
|
+
puts "TCP socket connection timed out"
|
1801
|
+
end
|
1802
|
+
raise Errno::ETIMEDOUT
|
1803
|
+
end
|
1804
|
+
rescue Errno::EISCONN
|
1805
|
+
# Already connected, which is fine
|
1806
|
+
if @verbose
|
1807
|
+
puts "TCP socket already connected"
|
1808
|
+
end
|
1809
|
+
end
|
1810
|
+
else
|
1811
|
+
# Create UDP socket (like Python's socket(AF_INET, SOCK_DGRAM))
|
1812
|
+
@socket = Socket.new(Socket::AF_INET, Socket::SOCK_DGRAM)
|
1813
|
+
@socket.setsockopt(Socket::SOL_SOCKET, Socket::SO_RCVTIMEO, [ @timeout, 0 ].pack('l_*'))
|
1814
|
+
|
1815
|
+
if @verbose
|
1816
|
+
puts "UDP socket created successfully"
|
1817
|
+
end
|
1818
|
+
end
|
1819
|
+
rescue => e
|
1820
|
+
if @verbose
|
1821
|
+
puts "Socket creation failed: #{e.message}"
|
1822
|
+
end
|
1823
|
+
raise RBZK::ZKNetworkError, "Failed to create socket: #{e.message}"
|
1824
|
+
end
|
1825
|
+
|
1826
|
+
def test_tcp_top(packet)
|
1827
|
+
# If packet is nil or too small, return 0
|
1828
|
+
return 0 if packet.nil? || packet.size <= 8
|
1829
|
+
|
1830
|
+
# Ensure packet is a binary string
|
1831
|
+
# packet = packet.to_s.b
|
1832
|
+
|
1833
|
+
# Unpack the TCP header - equivalent to Python's unpack('<HHI', packet[:8])
|
1834
|
+
# S< - unsigned short (2 bytes) little-endian - matches Python's H
|
1835
|
+
# L< - unsigned long (4 bytes) little-endian - matches Python's I
|
1836
|
+
tcp_header = packet[0...8].unpack('S<S<L<')
|
1837
|
+
|
1838
|
+
# Check if the header matches the expected values
|
1839
|
+
if tcp_header[0] == MACHINE_PREPARE_DATA_1 && tcp_header[1] == MACHINE_PREPARE_DATA_2
|
1840
|
+
return tcp_header[2] # Return the size (3rd element)
|
1841
|
+
end
|
1842
|
+
|
1843
|
+
# Default return 0
|
1844
|
+
return 0
|
1845
|
+
end
|
1846
|
+
|
1847
|
+
def ping
|
1848
|
+
if @verbose
|
1849
|
+
puts "Pinging device at #{@ip}:#{@port}..."
|
1850
|
+
end
|
1851
|
+
|
1852
|
+
# Try TCP ping first
|
1853
|
+
begin
|
1854
|
+
Timeout.timeout(5) do
|
1855
|
+
s = TCPSocket.new(@ip, @port)
|
1856
|
+
s.close
|
1857
|
+
if @verbose
|
1858
|
+
puts "TCP ping successful"
|
1859
|
+
end
|
1860
|
+
return true
|
1861
|
+
end
|
1862
|
+
rescue Timeout::Error, Errno::ECONNREFUSED, Errno::EHOSTUNREACH => e
|
1863
|
+
if @verbose
|
1864
|
+
puts "TCP ping failed: #{e.message}"
|
1865
|
+
end
|
1866
|
+
end
|
1867
|
+
|
1868
|
+
# If TCP ping fails, try UDP ping
|
1869
|
+
begin
|
1870
|
+
udp_socket = UDPSocket.new
|
1871
|
+
udp_socket.connect(@ip, @port)
|
1872
|
+
udp_socket.send("\x00" * 8, 0)
|
1873
|
+
ready = IO.select([ udp_socket ], nil, nil, 5)
|
1874
|
+
|
1875
|
+
if ready
|
1876
|
+
if @verbose
|
1877
|
+
puts "UDP ping successful"
|
1878
|
+
end
|
1879
|
+
udp_socket.close
|
1880
|
+
return true
|
1881
|
+
else
|
1882
|
+
if @verbose
|
1883
|
+
puts "UDP ping timed out"
|
1884
|
+
end
|
1885
|
+
udp_socket.close
|
1886
|
+
return false
|
1887
|
+
end
|
1888
|
+
rescue => e
|
1889
|
+
if @verbose
|
1890
|
+
puts "UDP ping failed: #{e.message}"
|
1891
|
+
end
|
1892
|
+
return false
|
1893
|
+
end
|
1894
|
+
end
|
1895
|
+
|
1896
|
+
def calculate_checksum(buf)
|
1897
|
+
# Match Python's __create_checksum method exactly
|
1898
|
+
# In Python:
|
1899
|
+
# def __create_checksum(self, p):
|
1900
|
+
# l = len(p)
|
1901
|
+
# checksum = 0
|
1902
|
+
# while l > 1:
|
1903
|
+
# checksum += unpack('H', pack('BB', p[0], p[1]))[0]
|
1904
|
+
# p = p[2:]
|
1905
|
+
# if checksum > const.USHRT_MAX:
|
1906
|
+
# checksum -= const.USHRT_MAX
|
1907
|
+
# l -= 2
|
1908
|
+
# if l:
|
1909
|
+
# checksum = checksum + p[-1]
|
1910
|
+
# while checksum > const.USHRT_MAX:
|
1911
|
+
# checksum -= const.USHRT_MAX
|
1912
|
+
# checksum = ~checksum
|
1913
|
+
# while checksum < 0:
|
1914
|
+
# checksum += const.USHRT_MAX
|
1915
|
+
# return pack('H', checksum)
|
1916
|
+
|
1917
|
+
# Get the length of the buffer
|
1918
|
+
l = buf.size
|
1919
|
+
checksum = 0
|
1920
|
+
i = 0
|
1921
|
+
|
1922
|
+
# Process pairs of bytes
|
1923
|
+
while l > 1
|
1924
|
+
# In Python: checksum += unpack('H', pack('BB', p[0], p[1]))[0]
|
1925
|
+
# This is combining two bytes into a 16-bit value (little-endian)
|
1926
|
+
checksum += (buf[i] | (buf[i + 1] << 8))
|
1927
|
+
i += 2
|
1928
|
+
|
1929
|
+
# In Python: if checksum > const.USHRT_MAX: checksum -= const.USHRT_MAX
|
1930
|
+
# Handle overflow immediately after each addition
|
1931
|
+
if checksum > USHRT_MAX
|
1932
|
+
checksum -= USHRT_MAX
|
1933
|
+
end
|
1934
|
+
|
1935
|
+
l -= 2
|
1936
|
+
end
|
1937
|
+
|
1938
|
+
# Handle odd byte if present
|
1939
|
+
# In Python: if l: checksum = checksum + p[-1]
|
1940
|
+
if l > 0
|
1941
|
+
checksum += buf[i]
|
1942
|
+
end
|
1943
|
+
|
1944
|
+
# Handle overflow
|
1945
|
+
# In Python: while checksum > const.USHRT_MAX: checksum -= const.USHRT_MAX
|
1946
|
+
while checksum > USHRT_MAX
|
1947
|
+
checksum -= USHRT_MAX
|
1948
|
+
end
|
1949
|
+
|
1950
|
+
# Bitwise complement
|
1951
|
+
# In Python: checksum = ~checksum
|
1952
|
+
checksum = ~checksum
|
1953
|
+
|
1954
|
+
# Handle negative values
|
1955
|
+
# In Python: while checksum < 0: checksum += const.USHRT_MAX
|
1956
|
+
while checksum < 0
|
1957
|
+
checksum += USHRT_MAX
|
1958
|
+
end
|
1959
|
+
|
1960
|
+
# Return the checksum
|
1961
|
+
checksum
|
1962
|
+
end
|
1963
|
+
|
1964
|
+
# Helper method to debug binary data in both Python and Ruby formats
|
1965
|
+
def debug_binary(name, data)
|
1966
|
+
return unless @verbose
|
1967
|
+
puts "#{name} (hex): #{data.bytes.map { |b| "\\x#{b.to_s(16).rjust(2, '0')}" }.join('')}"
|
1968
|
+
puts "#{name} (Ruby): #{data.bytes.map { |b| "0x#{b.to_s(16).rjust(2, '0')}" }.join(' ')}"
|
1969
|
+
puts "#{name} (Python): #{format_as_python_bytes(data)}"
|
1970
|
+
end
|
1971
|
+
|
1972
|
+
def create_tcp_top(packet)
|
1973
|
+
# Match Python's __create_tcp_top method exactly
|
1974
|
+
puts "\n*** DEBUG: create_tcp_top called ***" if @verbose
|
1975
|
+
length = packet.size
|
1976
|
+
# In Python: pack('<HHI', const.MACHINE_PREPARE_DATA_1, const.MACHINE_PREPARE_DATA_2, length)
|
1977
|
+
# In Ruby: [MACHINE_PREPARE_DATA_1, MACHINE_PREPARE_DATA_2, length].pack('S<S<I<')
|
1978
|
+
top = [ MACHINE_PREPARE_DATA_1, MACHINE_PREPARE_DATA_2, length ].pack('S<S<I<')
|
1979
|
+
|
1980
|
+
if @verbose
|
1981
|
+
puts "\nTCP header components:"
|
1982
|
+
puts " MACHINE_PREPARE_DATA_1: 0x#{MACHINE_PREPARE_DATA_1.to_s(16)} (#{MACHINE_PREPARE_DATA_1}) - should be 'PP' in ASCII"
|
1983
|
+
puts " MACHINE_PREPARE_DATA_2: 0x#{MACHINE_PREPARE_DATA_2.to_s(16)} (#{MACHINE_PREPARE_DATA_2}) - should be '\\x82\\x7d' in hex"
|
1984
|
+
puts " packet length: #{length}"
|
1985
|
+
|
1986
|
+
# Show the expected Python representation
|
1987
|
+
expected_python_header = "PP\\x82\\x7d\\x#{(length & 0xFF).to_s(16).rjust(2, '0')}\\x#{((length >> 8) & 0xFF).to_s(16).rjust(2, '0')}\\x#{((length >> 16) & 0xFF).to_s(16).rjust(2, '0')}\\x#{((length >> 24) & 0xFF).to_s(16).rjust(2, '0')}"
|
1988
|
+
puts " Expected Python header: #{expected_python_header}"
|
1989
|
+
|
1990
|
+
# Show the actual bytes of the constants
|
1991
|
+
puts " MACHINE_PREPARE_DATA_1 bytes: #{[ MACHINE_PREPARE_DATA_1 ].pack('S<').bytes.map { |b| "0x#{b.to_s(16).rjust(2, '0')}" }.join(' ')}"
|
1992
|
+
puts " MACHINE_PREPARE_DATA_2 bytes: #{[ MACHINE_PREPARE_DATA_2 ].pack('S<').bytes.map { |b| "0x#{b.to_s(16).rjust(2, '0')}" }.join(' ')}"
|
1993
|
+
|
1994
|
+
debug_binary("TCP header only", top) # This is just the 8-byte header
|
1995
|
+
debug_binary("Full TCP packet (what Python calls 'top')", top + packet) # This is what we return
|
1996
|
+
end
|
1997
|
+
|
1998
|
+
# Print debug info right before returning
|
1999
|
+
if @verbose
|
2000
|
+
puts "\n*** FINAL TCP PACKET DEBUG ***"
|
2001
|
+
puts "In both Python and Ruby, the variable 'top' is just the TCP header, but the method returns 'top + packet':"
|
2002
|
+
puts "TCP header format: b'PP\\x82\\x7d\\x#{(length & 0xFF).to_s(16).rjust(2, '0')}\\x#{((length >> 8) & 0xFF).to_s(16).rjust(2, '0')}\\x#{((length >> 16) & 0xFF).to_s(16).rjust(2, '0')}\\x#{((length >> 24) & 0xFF).to_s(16).rjust(2, '0')}'"
|
2003
|
+
puts "Return value format (top + packet): TCP header + command packet"
|
2004
|
+
puts "Ruby 'top + packet' format: #{(top + packet).bytes.map { |b| "0x#{b.to_s(16).rjust(2, '0')}" }.join(' ')}"
|
2005
|
+
puts "Hex format: #{(top + packet).bytes.map { |b| "\\x#{b.to_s(16).rjust(2, '0')}" }.join('')}"
|
2006
|
+
end
|
2007
|
+
|
2008
|
+
# Return top + packet (like Python's return top + packet)
|
2009
|
+
result = top + packet
|
2010
|
+
|
2011
|
+
if @verbose
|
2012
|
+
puts "\n*** SUPER EXPLICIT DEBUG ***"
|
2013
|
+
puts "top bytes: #{top.bytes.map { |b| "0x#{b.to_s(16).rjust(2, '0')}" }.join(' ')}"
|
2014
|
+
puts "packet bytes: #{packet.bytes.map { |b| "0x#{b.to_s(16).rjust(2, '0')}" }.join(' ')}"
|
2015
|
+
puts "result bytes: #{result.bytes.map { |b| "0x#{b.to_s(16).rjust(2, '0')}" }.join(' ')}"
|
2016
|
+
puts "result size: #{result.size} bytes"
|
2017
|
+
end
|
2018
|
+
|
2019
|
+
result
|
2020
|
+
end
|
2021
|
+
|
2022
|
+
def create_header(command, command_string = "".b, session_id = 0, reply_id = 0)
|
2023
|
+
# Match Python's __create_header method exactly
|
2024
|
+
# In Python:
|
2025
|
+
# def __create_header(self, command, command_string, session_id, reply_id):
|
2026
|
+
# buf = pack('<4H', command, 0, session_id, reply_id) + command_string
|
2027
|
+
# buf = unpack('8B' + '%sB' % len(command_string), buf)
|
2028
|
+
# checksum = unpack('H', self.__create_checksum(buf))[0]
|
2029
|
+
# reply_id += 1
|
2030
|
+
# if reply_id >= const.USHRT_MAX:
|
2031
|
+
# reply_id -= const.USHRT_MAX
|
2032
|
+
# buf = pack('<4H', command, checksum, session_id, reply_id)
|
2033
|
+
# return buf + command_string
|
2034
|
+
|
2035
|
+
# Ensure command_string is a binary string
|
2036
|
+
command_string = command_string.to_s.b
|
2037
|
+
|
2038
|
+
# Step 1: Create initial header and combine with command_string
|
2039
|
+
# In Python: buf = pack('<4H', command, 0, session_id, reply_id) + command_string
|
2040
|
+
buf = [ command, 0, session_id, reply_id ].pack('v4') + command_string
|
2041
|
+
|
2042
|
+
# Step 2: Convert to bytes array for checksum calculation
|
2043
|
+
# In Python: buf = unpack('8B' + '%sB' % len(command_string), buf)
|
2044
|
+
# This unpacks the buffer into individual bytes
|
2045
|
+
# In Ruby, we can use String#bytes to get an array of bytes
|
2046
|
+
buf = buf.unpack("C#{8 + command_string.length}")
|
2047
|
+
|
2048
|
+
# Step 3: Calculate checksum
|
2049
|
+
# In Python: checksum = unpack('H', self.__create_checksum(buf))[0]
|
2050
|
+
checksum = calculate_checksum(buf)
|
2051
|
+
|
2052
|
+
# Step 4: Update reply_id
|
2053
|
+
# In Python: reply_id += 1; if reply_id >= const.USHRT_MAX: reply_id -= const.USHRT_MAX
|
2054
|
+
reply_id += 1
|
2055
|
+
if reply_id >= USHRT_MAX
|
2056
|
+
reply_id -= USHRT_MAX
|
2057
|
+
end
|
2058
|
+
|
2059
|
+
# Step 5: Create final header with updated values
|
2060
|
+
# In Python: buf = pack('<4H', command, checksum, session_id, reply_id)
|
2061
|
+
buf = [ command, checksum, session_id, reply_id ].pack('v4')
|
2062
|
+
|
2063
|
+
if @verbose
|
2064
|
+
puts "Header components:"
|
2065
|
+
puts " Command: #{command}"
|
2066
|
+
puts " Checksum: #{checksum}"
|
2067
|
+
puts " Session ID: #{session_id}"
|
2068
|
+
puts " Reply ID: #{reply_id}"
|
2069
|
+
|
2070
|
+
if !command_string.empty?
|
2071
|
+
debug_binary("Command string", command_string)
|
2072
|
+
else
|
2073
|
+
puts "Command string: (empty)"
|
2074
|
+
end
|
2075
|
+
debug_binary("Final header", buf)
|
2076
|
+
end
|
2077
|
+
|
2078
|
+
buf + command_string
|
2079
|
+
end
|
2080
|
+
|
2081
|
+
def send_command(command, command_string = "".b, response_size = 8)
|
2082
|
+
# Match Python's __send_command method exactly
|
2083
|
+
# In Python:
|
2084
|
+
# def __send_command(self, command, command_string=b'', response_size=8):
|
2085
|
+
# if command not in [const.CMD_CONNECT, const.CMD_AUTH] and not self.is_connect:
|
2086
|
+
# raise ZKErrorConnection("instance are not connected.")
|
2087
|
+
# buf = self.__create_header(command, command_string, self.__session_id, self.__reply_id)
|
2088
|
+
# try:
|
2089
|
+
# if self.tcp:
|
2090
|
+
# top = self.__create_tcp_top(buf)
|
2091
|
+
# self.__sock.send(top)
|
2092
|
+
# self.__tcp_data_recv = self.__sock.recv(response_size + 8)
|
2093
|
+
# self.__tcp_length = self.__test_tcp_top(self.__tcp_data_recv)
|
2094
|
+
# if self.__tcp_length == 0:
|
2095
|
+
# raise ZKNetworkError("TCP packet invalid")
|
2096
|
+
# self.__header = unpack('<4H', self.__tcp_data_recv[8:16])
|
2097
|
+
# self.__data_recv = self.__tcp_data_recv[8:]
|
2098
|
+
# else:
|
2099
|
+
# self.__sock.sendto(buf, self.__address)
|
2100
|
+
# self.__data_recv = self.__sock.recv(response_size)
|
2101
|
+
# self.__header = unpack('<4H', self.__data_recv[:8])
|
2102
|
+
# except Exception as e:
|
2103
|
+
# raise ZKNetworkError(str(e))
|
2104
|
+
# self.__response = self.__header[0]
|
2105
|
+
# self.__reply_id = self.__header[3]
|
2106
|
+
# self.__data = self.__data_recv[8:]
|
2107
|
+
# if self.__response in [const.CMD_ACK_OK, const.CMD_PREPARE_DATA, const.CMD_DATA]:
|
2108
|
+
# return {
|
2109
|
+
# 'status': True,
|
2110
|
+
# 'code': self.__response
|
2111
|
+
# }
|
2112
|
+
# return {
|
2113
|
+
# 'status': False,
|
2114
|
+
# 'code': self.__response
|
2115
|
+
# }
|
2116
|
+
|
2117
|
+
# Check connection status (except for connect and auth commands)
|
2118
|
+
if command != CMD_CONNECT && command != CMD_AUTH && !@connected
|
2119
|
+
raise RBZK::ZKErrorConnection, "Instance are not connected."
|
2120
|
+
end
|
2121
|
+
|
2122
|
+
# In Python, command_string is a bytes object (b'')
|
2123
|
+
# In Ruby, we use binary strings (ASCII-8BIT encoding)
|
2124
|
+
command_string = command_string.to_s.b
|
2125
|
+
|
2126
|
+
if @verbose
|
2127
|
+
puts "command_string class: #{command_string.class}"
|
2128
|
+
puts "command_string encoding: #{command_string.encoding}"
|
2129
|
+
puts "command_string bytes: #{python_format(command_string)}"
|
2130
|
+
end
|
2131
|
+
|
2132
|
+
# Create command header (like Python's __create_header)
|
2133
|
+
buf = create_header(command, command_string, @session_id, @reply_id)
|
2134
|
+
|
2135
|
+
if @verbose
|
2136
|
+
puts "\nSending command #{command} with session id #{@session_id} and reply id #{@reply_id}"
|
2137
|
+
puts "buf: #{python_format(buf)}"
|
2138
|
+
end
|
2139
|
+
|
2140
|
+
begin
|
2141
|
+
puts "\n*** DEBUG: Using #{@tcp ? 'TCP' : 'UDP'} mode ***" if @verbose
|
2142
|
+
if @tcp
|
2143
|
+
# Create TCP header (like Python's __create_tcp_top)
|
2144
|
+
puts "\n*** Before create_tcp_top ***" if @verbose
|
2145
|
+
puts "buf size: #{buf.size} bytes" if @verbose
|
2146
|
+
top = create_tcp_top(buf)
|
2147
|
+
puts "\n*** After create_tcp_top ***" if @verbose
|
2148
|
+
puts "top size: #{top.size} bytes" if @verbose
|
2149
|
+
|
2150
|
+
if @verbose
|
2151
|
+
puts "\nSending TCP packet:"
|
2152
|
+
puts "Note: In send_command, 'top' variable contains the full packet (header + command packet)"
|
2153
|
+
puts "This is because create_tcp_top returns the full packet, not just the header"
|
2154
|
+
debug_binary("Command packet (buf)", buf)
|
2155
|
+
debug_binary("Full TCP packet (top)", top) # 'top' contains the full packet here
|
2156
|
+
end
|
2157
|
+
|
2158
|
+
@socket.send(top, 0)
|
2159
|
+
@tcp_data_recv = @socket.recv(response_size + 8)
|
2160
|
+
@tcp_length = test_tcp_top(@tcp_data_recv)
|
2161
|
+
|
2162
|
+
if @verbose
|
2163
|
+
puts "\nReceived TCP response:"
|
2164
|
+
debug_binary("TCP response", @tcp_data_recv)
|
2165
|
+
end
|
2166
|
+
|
2167
|
+
if @tcp_length == 0
|
2168
|
+
raise RBZK::ZKNetworkError, "TCP packet invalid"
|
2169
|
+
end
|
2170
|
+
|
2171
|
+
@header = @tcp_data_recv[8..15].unpack('v4')
|
2172
|
+
@data_recv = @tcp_data_recv[8..-1]
|
2173
|
+
else
|
2174
|
+
# Send UDP packet
|
2175
|
+
@socket.send(buf, 0, @ip, @port)
|
2176
|
+
@data_recv = @socket.recv(response_size)
|
2177
|
+
@header = @data_recv[0..7].unpack('S<4')
|
2178
|
+
end
|
2179
|
+
rescue => e
|
2180
|
+
if @verbose
|
2181
|
+
puts "Connection error during send: #{e.message}"
|
2182
|
+
end
|
2183
|
+
raise RBZK::ZKNetworkError, e.message
|
2184
|
+
end
|
2185
|
+
|
2186
|
+
# Process response (like Python's __send_command)
|
2187
|
+
@response = @header[0]
|
2188
|
+
@reply_id = @header[3]
|
2189
|
+
@data = @data_recv[8..-1] # This is the key line that matches Python's self.__data = self.__data_recv[8:]
|
2190
|
+
|
2191
|
+
# Return response status (like Python's __send_command)
|
2192
|
+
if @response == CMD_ACK_OK || @response == CMD_PREPARE_DATA || @response == CMD_DATA
|
2193
|
+
return {
|
2194
|
+
status: true,
|
2195
|
+
code: @response
|
2196
|
+
}
|
2197
|
+
else
|
2198
|
+
return {
|
2199
|
+
status: false,
|
2200
|
+
code: @response
|
2201
|
+
}
|
2202
|
+
end
|
2203
|
+
end
|
2204
|
+
|
2205
|
+
def recv_reply
|
2206
|
+
begin
|
2207
|
+
if @verbose
|
2208
|
+
puts "Waiting for TCP reply"
|
2209
|
+
end
|
2210
|
+
|
2211
|
+
# Set a timeout for the read operation
|
2212
|
+
Timeout.timeout(5) do
|
2213
|
+
# Read TCP header (8 bytes)
|
2214
|
+
tcp_header = @socket.read(8)
|
2215
|
+
return nil unless tcp_header && tcp_header.size >= 8
|
2216
|
+
|
2217
|
+
# Parse TCP header
|
2218
|
+
tcp_format1, tcp_format2, tcp_length = tcp_header.unpack('S<S<I<')
|
2219
|
+
|
2220
|
+
if @verbose
|
2221
|
+
puts "TCP header: format1=#{tcp_format1}, format2=#{tcp_format2}, length=#{tcp_length}"
|
2222
|
+
end
|
2223
|
+
|
2224
|
+
# Verify TCP header format
|
2225
|
+
if tcp_format1 != MACHINE_PREPARE_DATA_1 || tcp_format2 != MACHINE_PREPARE_DATA_2
|
2226
|
+
if @verbose
|
2227
|
+
puts "Invalid TCP header format: #{tcp_format1}, #{tcp_format2}"
|
2228
|
+
end
|
2229
|
+
return nil
|
2230
|
+
end
|
2231
|
+
|
2232
|
+
# Read command header (8 bytes)
|
2233
|
+
cmd_header = @socket.read(8)
|
2234
|
+
return nil unless cmd_header && cmd_header.size >= 8
|
2235
|
+
|
2236
|
+
# Parse command header
|
2237
|
+
command, checksum, session_id, reply_id = cmd_header.unpack('S<4')
|
2238
|
+
|
2239
|
+
if @verbose
|
2240
|
+
puts "Command header: cmd=#{command}, checksum=#{checksum}, session=#{session_id}, reply=#{reply_id}"
|
2241
|
+
end
|
2242
|
+
|
2243
|
+
# Calculate data size (TCP length - 8 bytes for command header)
|
2244
|
+
data_size = tcp_length - 8
|
2245
|
+
|
2246
|
+
# Read data if available
|
2247
|
+
data = ""
|
2248
|
+
if data_size > 0
|
2249
|
+
if @verbose
|
2250
|
+
puts "Reading #{data_size} bytes of data"
|
2251
|
+
end
|
2252
|
+
|
2253
|
+
# Read data in chunks to handle large responses (like Python implementation)
|
2254
|
+
remaining = data_size
|
2255
|
+
while remaining > 0
|
2256
|
+
chunk_size = [ remaining, 4096 ].min
|
2257
|
+
chunk = @socket.read(chunk_size)
|
2258
|
+
if chunk.nil? || chunk.empty?
|
2259
|
+
if @verbose
|
2260
|
+
puts "Failed to read data chunk, got #{chunk.inspect}"
|
2261
|
+
end
|
2262
|
+
break
|
2263
|
+
end
|
2264
|
+
|
2265
|
+
data += chunk
|
2266
|
+
remaining -= chunk.size
|
2267
|
+
|
2268
|
+
if @verbose && remaining > 0
|
2269
|
+
puts "Read #{chunk.size} bytes, #{remaining} remaining"
|
2270
|
+
end
|
2271
|
+
end
|
2272
|
+
end
|
2273
|
+
|
2274
|
+
# Store data for later use
|
2275
|
+
@data_recv = data
|
2276
|
+
|
2277
|
+
# Update session ID
|
2278
|
+
@session_id = session_id
|
2279
|
+
|
2280
|
+
# Check command type and handle accordingly (like Python implementation)
|
2281
|
+
if command == CMD_ACK_OK
|
2282
|
+
if @verbose
|
2283
|
+
puts "Received ACK_OK"
|
2284
|
+
end
|
2285
|
+
return cmd_header + data
|
2286
|
+
elsif command == CMD_ACK_ERROR
|
2287
|
+
if @verbose
|
2288
|
+
puts "Received ACK_ERROR"
|
2289
|
+
end
|
2290
|
+
return nil
|
2291
|
+
elsif command == CMD_ACK_DATA
|
2292
|
+
if @verbose
|
2293
|
+
puts "Received ACK_DATA"
|
2294
|
+
end
|
2295
|
+
if data_size > 0
|
2296
|
+
return cmd_header + data
|
2297
|
+
else
|
2298
|
+
return nil
|
2299
|
+
end
|
2300
|
+
else
|
2301
|
+
if @verbose
|
2302
|
+
puts "Received unknown command: #{command}"
|
2303
|
+
end
|
2304
|
+
return cmd_header + data
|
2305
|
+
end
|
2306
|
+
end
|
2307
|
+
rescue Timeout::Error => e
|
2308
|
+
if @verbose
|
2309
|
+
puts "Timeout waiting for response: #{e.message}"
|
2310
|
+
end
|
2311
|
+
raise RBZK::ZKErrorResponse, "Timeout waiting for response"
|
2312
|
+
rescue Errno::ECONNRESET, Errno::EPIPE => e
|
2313
|
+
if @verbose
|
2314
|
+
puts "Connection error during receive: #{e.message}"
|
2315
|
+
end
|
2316
|
+
raise RBZK::ZKNetworkError, "Connection error: #{e.message}"
|
2317
|
+
rescue Errno::EAGAIN, Errno::EWOULDBLOCK => e
|
2318
|
+
if @verbose
|
2319
|
+
puts "Timeout waiting for response: #{e.message}"
|
2320
|
+
end
|
2321
|
+
raise RBZK::ZKErrorResponse, "Timeout waiting for response"
|
2322
|
+
end
|
2323
|
+
|
2324
|
+
nil
|
2325
|
+
end
|
2326
|
+
|
2327
|
+
def recv_long
|
2328
|
+
if @data_recv && @data_recv.size >= 4
|
2329
|
+
data = @data_recv[0..3]
|
2330
|
+
@data_recv = @data_recv[4..-1]
|
2331
|
+
return data.unpack('L<')[0]
|
2332
|
+
end
|
2333
|
+
|
2334
|
+
0
|
2335
|
+
end
|
2336
|
+
|
2337
|
+
def recv_chunk(size)
|
2338
|
+
if @verbose
|
2339
|
+
puts "Receiving chunk of #{size} bytes"
|
2340
|
+
end
|
2341
|
+
|
2342
|
+
if @data_recv && @data_recv.size >= size
|
2343
|
+
data = @data_recv[0...size]
|
2344
|
+
@data_recv = @data_recv[size..-1]
|
2345
|
+
|
2346
|
+
if @verbose
|
2347
|
+
puts "Received #{data.size} bytes from buffer"
|
2348
|
+
end
|
2349
|
+
|
2350
|
+
return data
|
2351
|
+
end
|
2352
|
+
|
2353
|
+
if @verbose
|
2354
|
+
puts "Warning: No data available in buffer"
|
2355
|
+
end
|
2356
|
+
|
2357
|
+
# Return empty string if no data is available
|
2358
|
+
''
|
2359
|
+
end
|
2360
|
+
|
2361
|
+
def make_commkey(key, session_id, ticks = 50)
|
2362
|
+
if @verbose
|
2363
|
+
puts "\n*** DEBUG: make_commkey called ***"
|
2364
|
+
puts "key: #{key}, session_id: #{session_id}, ticks: #{ticks}"
|
2365
|
+
end
|
2366
|
+
|
2367
|
+
key = key.to_i
|
2368
|
+
session_id = session_id.to_i
|
2369
|
+
k = 0
|
2370
|
+
|
2371
|
+
32.times do |i|
|
2372
|
+
if (key & (1 << i)) != 0
|
2373
|
+
k = (k << 1 | 1)
|
2374
|
+
else
|
2375
|
+
k = k << 1
|
2376
|
+
end
|
2377
|
+
end
|
2378
|
+
|
2379
|
+
k += session_id
|
2380
|
+
|
2381
|
+
# Pack the integer into 4 bytes and unpack as individual bytes
|
2382
|
+
k_bytes = [ k ].pack('L<').unpack('C4')
|
2383
|
+
|
2384
|
+
# XOR with 'ZKSO'
|
2385
|
+
k_bytes = [
|
2386
|
+
k_bytes[0] ^ 'Z'.ord,
|
2387
|
+
k_bytes[1] ^ 'K'.ord,
|
2388
|
+
k_bytes[2] ^ 'S'.ord,
|
2389
|
+
k_bytes[3] ^ 'O'.ord
|
2390
|
+
]
|
2391
|
+
|
2392
|
+
# Pack the bytes back into a string and unpack as 2 shorts
|
2393
|
+
k_shorts = k_bytes.pack('C4').unpack('S<2')
|
2394
|
+
|
2395
|
+
# Swap the shorts
|
2396
|
+
k_shorts = [ k_shorts[1], k_shorts[0] ]
|
2397
|
+
|
2398
|
+
# Get the low byte of ticks
|
2399
|
+
b = 0xff & ticks
|
2400
|
+
|
2401
|
+
# Pack the shorts back into a string and unpack as 4 bytes
|
2402
|
+
k_bytes = k_shorts.pack('S<2').unpack('C4')
|
2403
|
+
|
2404
|
+
# XOR with ticks and pack back into a string
|
2405
|
+
# In Python: pack(b'BBBB', k[0] ^ B, k[1] ^ B, B, k[3] ^ B)
|
2406
|
+
# Note: The third byte is just B, not k[2] ^ B
|
2407
|
+
result = [ k_bytes[0] ^ b, k_bytes[1] ^ b, b, k_bytes[3] ^ b ].pack('C4')
|
2408
|
+
|
2409
|
+
if @verbose
|
2410
|
+
puts "Final commkey bytes: #{result.bytes.map { |b| "0x#{b.to_s(16).rjust(2, '0')}" }.join(' ')}"
|
2411
|
+
end
|
2412
|
+
|
2413
|
+
result
|
2414
|
+
end
|
2415
|
+
end
|
2416
|
+
end
|