rbzk 0.1.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,551 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require 'thor'
5
+ require 'yaml'
6
+ require 'fileutils'
7
+ require 'date'
8
+ require 'rbzk'
9
+ require 'rbzk/cli/config'
10
+ require 'terminal-table'
11
+
12
+ module RBZK
13
+ module CLI
14
+ class Commands < Thor
15
+
16
+ # Global options
17
+ class_option :ip, type: :string, desc: 'IP address of the device'
18
+ class_option :port, type: :numeric, desc: 'Port number (default: 4370)'
19
+ class_option :timeout, type: :numeric, desc: 'Connection timeout in seconds (default: 30)'
20
+ class_option :password, type: :numeric, desc: 'Device password (default: 0)'
21
+ class_option :verbose, type: :boolean, desc: 'Enable verbose output'
22
+ class_option :force_udp, type: :boolean, desc: 'Force UDP mode'
23
+ class_option :no_ping, type: :boolean, desc: 'Skip ping check'
24
+ class_option :encoding, type: :string, desc: 'Encoding for strings (default: UTF-8)'
25
+
26
+ desc "info [IP]", "Get device information"
27
+
28
+ def info(ip = nil)
29
+
30
+ # Use IP from options if not provided as argument
31
+ ip ||= options[:ip] || @config['ip']
32
+
33
+ with_connection(ip, options) do |conn|
34
+ # Get device information
35
+ # First read sizes to get user counts and capacities
36
+ conn.read_sizes
37
+
38
+ device_info = {
39
+ 'Serial Number' => conn.get_serialnumber,
40
+ 'MAC Address' => conn.get_mac,
41
+ 'Device Name' => conn.get_device_name,
42
+ 'Firmware Version' => conn.get_firmware_version,
43
+ 'Platform' => conn.get_platform,
44
+ 'Face Version' => conn.get_face_version,
45
+ 'Fingerprint Version' => conn.get_fp_version,
46
+ 'Device Time' => conn.get_time,
47
+ 'Users' => conn.instance_variable_get(:@users),
48
+ 'Fingerprints' => conn.instance_variable_get(:@fingers),
49
+ 'Attendance Records' => conn.instance_variable_get(:@records),
50
+ 'User Capacity' => conn.instance_variable_get(:@users_cap),
51
+ 'Fingerprint Capacity' => conn.instance_variable_get(:@fingers_cap),
52
+ 'Record Capacity' => conn.instance_variable_get(:@rec_cap),
53
+ 'Face Capacity' => conn.instance_variable_get(:@faces_cap),
54
+ 'Faces' => conn.instance_variable_get(:@faces)
55
+ }
56
+
57
+ # Display information
58
+ if defined?(::Terminal) && defined?(::Terminal::Table)
59
+ # Pretty table output
60
+ table = ::Terminal::Table.new do |t|
61
+ t.title = "Device Information"
62
+ device_info.each do |key, value|
63
+ t << [ key, value ]
64
+ end
65
+ end
66
+
67
+ puts table
68
+ else
69
+ # Fallback plain text output
70
+ puts "Device Information:"
71
+ device_info.each do |key, value|
72
+ puts "#{key}: #{value}"
73
+ end
74
+ end
75
+ end
76
+ end
77
+
78
+ desc "users [IP]", "Get users from the device"
79
+
80
+ def users(ip = nil)
81
+ # Use IP from options if not provided as argument
82
+ ip ||= options[:ip] || @config['ip']
83
+ with_connection(ip, options) do |conn|
84
+ puts "Getting users..."
85
+ users = conn.get_users
86
+ display_users(users)
87
+ end
88
+ end
89
+
90
+ desc "logs [IP]", "Get attendance logs"
91
+ method_option :today, type: :boolean, desc: "Get only today's logs"
92
+ method_option :yesterday, type: :boolean, desc: "Get only yesterday's logs"
93
+ method_option :week, type: :boolean, desc: "Get this week's logs"
94
+ method_option :month, type: :boolean, desc: "Get this month's logs"
95
+ method_option :start_date, type: :string, desc: "Start date for custom range (YYYY-MM-DD)"
96
+ method_option :end_date, type: :string, desc: "End date for custom range (YYYY-MM-DD)"
97
+ method_option :limit, type: :numeric, default: 25, desc: "Limit the number of logs displayed (default: 25, use 0 for all)"
98
+
99
+ # Add aliases for common log commands
100
+ desc "logs-today [IP]", "Get today's attendance logs"
101
+ map "logs-today" => "logs"
102
+
103
+ def logs_today(ip = nil)
104
+
105
+ invoke :logs, [ ip ], today: true
106
+ end
107
+
108
+ desc "logs-yesterday [IP]", "Get yesterday's attendance logs"
109
+ map "logs-yesterday" => "logs"
110
+
111
+ def logs_yesterday(ip = nil)
112
+ invoke :logs, [ ip ], yesterday: true
113
+ end
114
+
115
+ desc "logs-week [IP]", "Get this week's attendance logs"
116
+ map "logs-week" => "logs"
117
+
118
+ def logs_week(ip = nil)
119
+ invoke :logs, [ ip ], week: true
120
+ end
121
+
122
+ desc "logs-month [IP]", "Get this month's attendance logs"
123
+ map "logs-month" => "logs"
124
+
125
+ def logs_month(ip = nil)
126
+ invoke :logs, [ ip ], month: true
127
+ end
128
+
129
+ desc "logs-all [IP]", "Get all attendance logs without limit"
130
+
131
+ def logs_all(ip = nil)
132
+ # Use IP from options if not provided as argument
133
+ ip ||= options[:ip] || @config['ip']
134
+
135
+ with_connection(ip, options) do |conn|
136
+ # Get attendance logs
137
+ puts "Getting all attendance logs (this may take a while)..."
138
+ logs = conn.get_attendance_logs
139
+ total_logs = logs.size
140
+ puts "Total logs: #{total_logs}" if options[:verbose]
141
+
142
+ # Display logs
143
+ if logs && !logs.empty?
144
+ puts "\nFound #{logs.size} attendance records:"
145
+
146
+ if defined?(::Terminal) && defined?(::Terminal::Table)
147
+ # Pretty table output
148
+ table = ::Terminal::Table.new do |t|
149
+ t.title = "All Attendance Logs (Showing All Records)"
150
+ t.headings = [ 'User ID', 'Time', 'Status' ]
151
+
152
+ # Show all logs in the table
153
+ logs.each do |log|
154
+ t << [
155
+ log.user_id,
156
+ log.timestamp.strftime('%Y-%m-%d %H:%M:%S'),
157
+ format_status(log.status)
158
+ ]
159
+ end
160
+ end
161
+
162
+ puts table
163
+ else
164
+ # Fallback plain text output
165
+ logs.each do |log|
166
+ puts " User ID: #{log.user_id}, Time: #{log.timestamp.strftime('%Y-%m-%d %H:%M:%S')}, Status: #{format_status(log.status)}"
167
+ end
168
+ end
169
+ else
170
+ puts "\nNo attendance records found"
171
+ end
172
+ end
173
+ end
174
+
175
+ desc "logs-custom START_DATE END_DATE [IP]", "Get logs for a custom date range (YYYY-MM-DD)"
176
+
177
+ def logs_custom(start_date, end_date, ip = nil)
178
+ # Use IP from options if not provided as argument
179
+ ip ||= options[:ip] || @config['ip']
180
+ invoke :logs, [ ip ], start_date: start_date, end_date: end_date
181
+ end
182
+
183
+ desc "logs [IP]", "Get all attendance logs"
184
+
185
+ def logs(ip = nil)
186
+ # Use IP from options if not provided as argument
187
+ ip ||= options[:ip] || @config['ip']
188
+ with_connection(ip, options) do |conn|
189
+ # Get attendance logs
190
+ puts "Getting attendance logs..."
191
+ logs = conn.get_attendance_logs
192
+ total_logs = logs.size
193
+ puts "Total logs: #{total_logs}" if options[:verbose]
194
+
195
+ # Filter logs based on options
196
+ title = if options[:today]
197
+ today = Date.today
198
+ logs = filter_logs_by_date(logs, today, today)
199
+ "Today's Attendance Logs (#{today})"
200
+ elsif options[:yesterday]
201
+ yesterday = Date.today - 1
202
+ logs = filter_logs_by_date(logs, yesterday, yesterday)
203
+ "Yesterday's Attendance Logs (#{yesterday})"
204
+ elsif options[:week]
205
+ today = Date.today
206
+ start_of_week = today - today.wday
207
+ logs = filter_logs_by_date(logs, start_of_week, today)
208
+ "This Week's Attendance Logs (#{start_of_week} to #{today})"
209
+ elsif options[:month]
210
+ today = Date.today
211
+ start_of_month = Date.new(today.year, today.month, 1)
212
+ logs = filter_logs_by_date(logs, start_of_month, today)
213
+ "This Month's Attendance Logs (#{start_of_month} to #{today})"
214
+ elsif options[:start_date] && options[:end_date]
215
+ begin
216
+ start_date = Date.parse(options[:start_date])
217
+ end_date = Date.parse(options[:end_date])
218
+
219
+ # Print debug info
220
+ puts "Filtering logs from #{start_date} to #{end_date}..." if options[:verbose]
221
+
222
+ # Use the filter_logs_by_date method
223
+ logs = filter_logs_by_date(logs, start_date, end_date)
224
+
225
+ "Attendance Logs (#{start_date} to #{end_date})"
226
+ rescue ArgumentError
227
+ puts "Error: Invalid date format. Please use YYYY-MM-DD format."
228
+ return
229
+ end
230
+ elsif options[:start_date]
231
+ begin
232
+ start_date = Date.parse(options[:start_date])
233
+ end_date = Date.today
234
+
235
+ # Print debug info
236
+ puts "Filtering logs from #{start_date} onwards..." if options[:verbose]
237
+
238
+ # Use the filter_logs_by_date method
239
+ logs = filter_logs_by_date(logs, start_date, end_date)
240
+
241
+ "Attendance Logs (#{start_date} to #{end_date})"
242
+ rescue ArgumentError
243
+ puts "Error: Invalid date format. Please use YYYY-MM-DD format."
244
+ return
245
+ end
246
+ elsif options[:end_date]
247
+ begin
248
+ end_date = Date.parse(options[:end_date])
249
+ # Default start date to 30 days before end date
250
+ start_date = end_date - 30
251
+
252
+ # Print debug info
253
+ puts "Filtering logs from #{start_date} to #{end_date}..." if options[:verbose]
254
+
255
+ # Use the filter_logs_by_date method
256
+ logs = filter_logs_by_date(logs, start_date, end_date)
257
+
258
+ "Attendance Logs (#{start_date} to #{end_date})"
259
+ rescue ArgumentError
260
+ puts "Error: Invalid date format. Please use YYYY-MM-DD format."
261
+ return
262
+ end
263
+ else
264
+ "All Attendance Logs"
265
+ end
266
+
267
+ # Display logs
268
+ if logs && !logs.empty?
269
+ puts "\nFound #{logs.size} attendance records:"
270
+
271
+ # Determine how many logs to display
272
+ limit = options[:limit] || 25
273
+ display_logs = limit > 0 ? logs.first(limit) : logs
274
+
275
+ if defined?(::Terminal) && defined?(::Terminal::Table)
276
+ # Pretty table output
277
+ table = ::Terminal::Table.new do |t|
278
+ t.title = title || "Attendance Logs"
279
+ t.headings = [ 'User ID', 'Time', 'Status' ]
280
+
281
+ # Show logs in the table based on limit
282
+ display_logs.each do |log|
283
+ t << [
284
+ log.user_id,
285
+ log.timestamp.strftime('%Y-%m-%d %H:%M:%S'),
286
+ format_status(log.status)
287
+ ]
288
+ end
289
+ end
290
+
291
+ puts table
292
+
293
+ # Show summary if logs were limited
294
+ if limit > 0 && logs.size > limit
295
+ puts "Showing #{display_logs.size} of #{logs.size} records. Use --limit option to change the number of records displayed."
296
+ end
297
+ else
298
+ # Fallback plain text output
299
+ display_logs.each do |log|
300
+ puts " User ID: #{log.user_id}, Time: #{log.timestamp.strftime('%Y-%m-%d %H:%M:%S')}, Status: #{format_status(log.status)}"
301
+ end
302
+
303
+ if logs.size > display_logs.size
304
+ puts " ... and #{logs.size - display_logs.size} more records"
305
+ end
306
+ end
307
+ else
308
+ puts "\nNo attendance records found"
309
+ end
310
+ end
311
+ end
312
+
313
+ desc "clear-logs [IP]", "Clear attendance logs"
314
+ map "clear-logs" => :clear_logs
315
+
316
+ def clear_logs(ip = nil)
317
+
318
+ # Use IP from options if not provided as argument
319
+ ip ||= options[:ip] || @config['ip']
320
+ with_connection(ip, options) do |conn|
321
+ puts "WARNING: This will delete all attendance logs from the device."
322
+ return unless yes?("Are you sure you want to continue? (y/N)")
323
+
324
+ puts "Clearing attendance logs..."
325
+ result = conn.clear_attendance
326
+ puts "✓ Attendance logs cleared successfully!" if result
327
+ end
328
+ end
329
+
330
+ desc "test-voice [IP]", "Test the device voice"
331
+ method_option :index, type: :numeric, desc: "Sound index to play (0-35, default: 0)"
332
+ map "test-voice" => :test_voice
333
+
334
+ def test_voice(ip = nil)
335
+ # Use IP from options if not provided as argument
336
+ ip ||= options[:ip] || @config['ip']
337
+
338
+ # Get the sound index
339
+ index = options[:index] || 0
340
+
341
+ # Print available sound indices if verbose
342
+ if options[:verbose]
343
+ puts "Available sound indices:"
344
+ puts " 0: Thank You"
345
+ puts " 1: Incorrect Password"
346
+ puts " 2: Access Denied"
347
+ puts " 3: Invalid ID"
348
+ puts " 4: Please try again"
349
+ puts " 5: Duplicate ID"
350
+ puts " 6: The clock is flow"
351
+ puts " 7: The clock is full"
352
+ puts " 8: Duplicate finger"
353
+ puts " 9: Duplicated punch"
354
+ puts "10: Beep kuko"
355
+ puts "11: Beep siren"
356
+ puts "13: Beep bell"
357
+ puts "18: Windows(R) opening sound"
358
+ puts "20: Fingerprint not emolt"
359
+ puts "21: Password not emolt"
360
+ puts "22: Badges not emolt"
361
+ puts "23: Face not emolt"
362
+ puts "24: Beep standard"
363
+ puts "30: Invalid user"
364
+ puts "31: Invalid time period"
365
+ puts "32: Invalid combination"
366
+ puts "33: Illegal Access"
367
+ puts "34: Disk space full"
368
+ puts "35: Duplicate fingerprint"
369
+ puts "51: Focus eyes on the green box"
370
+ end
371
+
372
+ with_connection(ip, options) do |conn|
373
+ puts "Testing device voice with index #{index}..."
374
+ result = conn.test_voice(index)
375
+ puts "✓ Voice test successful!" if result
376
+ end
377
+ end
378
+
379
+ desc "config", "Show current configuration"
380
+
381
+ def config
382
+ puts "RBZK Configuration"
383
+ puts "=================="
384
+ @config.to_h.each do |key, value|
385
+ puts "#{key}: #{value}"
386
+ end
387
+ end
388
+
389
+ desc "config-set KEY VALUE", "Set a configuration value"
390
+
391
+ def config_set(key, value)
392
+ # Convert value to appropriate type
393
+ typed_value = case key
394
+ when 'port', 'timeout', 'password'
395
+ value.to_i
396
+ when 'verbose', 'force_udp', 'no_ping'
397
+ value.downcase == 'true'
398
+ else
399
+ value
400
+ end
401
+
402
+ @config[key] = typed_value
403
+ @config.save
404
+ puts "Configuration updated: #{key} = #{typed_value}"
405
+ end
406
+
407
+ desc "config-reset", "Reset configuration to defaults"
408
+
409
+ def config_reset
410
+ if yes?("Are you sure you want to reset all configuration to defaults? (y/N)")
411
+ FileUtils.rm_f(@config.config_file)
412
+ @config = RBZK::CLI::Config.new
413
+ puts "Configuration reset to defaults."
414
+ invoke :config
415
+ else
416
+ puts "Operation cancelled."
417
+ end
418
+ end
419
+
420
+ private
421
+
422
+ def initialize(*args)
423
+ super
424
+ @config = RBZK::CLI::Config.new
425
+ end
426
+
427
+ def with_connection(ip, options = {})
428
+ puts "Connecting to ZKTeco device at #{ip}:#{options[:port] || @config['port'] || 4370}..."
429
+ puts "Please ensure the device is powered on and connected to the network."
430
+
431
+ begin
432
+ # Create ZK instance with options from config and command line
433
+ zk_options = {
434
+ port: options[:port] || @config['port'] || 4370,
435
+ timeout: options[:timeout] || @config['timeout'] || 30,
436
+ password: options[:password] || @config['password'] || 0,
437
+ verbose: options[:verbose] || @config['verbose'] || false,
438
+ force_udp: options[:force_udp] || @config['force_udp'] || false,
439
+ omit_ping: options[:no_ping] || @config['no_ping'] || false,
440
+ encoding: options[:encoding] || @config['encoding'] || 'UTF-8'
441
+ }
442
+
443
+ zk = RBZK::ZK.new(ip, **zk_options)
444
+ conn = zk.connect
445
+
446
+ if conn.connected?
447
+ puts "✓ Connected successfully!" unless options[:quiet]
448
+ yield conn if block_given?
449
+ else
450
+ puts "✗ Failed to connect to device."
451
+ end
452
+ rescue RBZK::ZKNetworkError => e
453
+ puts "✗ Network Error: #{e.message}"
454
+ puts "Please check the IP address and ensure the device is reachable."
455
+ rescue RBZK::ZKErrorResponse => e
456
+ puts "✗ Device Error: #{e.message}"
457
+ puts "The device returned an error response."
458
+ rescue => e
459
+ puts "✗ Unexpected Error: #{e.message}"
460
+ puts "An unexpected error occurred while communicating with the device."
461
+ puts e.backtrace.join("\n") if options[:verbose]
462
+ ensure
463
+ if conn && conn.connected?
464
+ puts "Disconnecting from device..." unless options[:quiet]
465
+ conn.disconnect
466
+ puts "✓ Disconnected" unless options[:quiet]
467
+ end
468
+ end
469
+ end
470
+
471
+ def display_users(users)
472
+ if users && !users.empty?
473
+ puts "✓ Found #{users.size} users:"
474
+
475
+ # Use Terminal::Table for pretty output
476
+ table = ::Terminal::Table.new do |t|
477
+ t.title = "Users"
478
+ t.headings = [ 'UID', 'User ID', 'Name', 'Privilege', 'Password', 'Group ID', 'Card' ]
479
+
480
+ users.each do |user|
481
+ t << [
482
+ user.uid,
483
+ user.user_id,
484
+ user.name,
485
+ format_privilege(user.privilege),
486
+ (user.password.nil? || user.password.empty?) ? '(none)' : '(set)',
487
+ user.group_id,
488
+ user.card
489
+ ]
490
+ end
491
+ end
492
+
493
+ puts table
494
+ else
495
+ puts "✓ No users found"
496
+ end
497
+ end
498
+
499
+ def filter_logs_by_date(logs, start_date, end_date)
500
+ # Convert Date objects to strings for comparison
501
+ total_logs = logs.size
502
+ start_date_str = start_date.strftime('%Y-%m-%d')
503
+ end_date_str = end_date.strftime('%Y-%m-%d')
504
+
505
+ if options[:verbose]
506
+ puts "Filtering logs from #{start_date_str} to #{end_date_str}..."
507
+ puts "Total logs before filtering: #{total_logs}"
508
+ end
509
+
510
+ # Filter logs by date range using string comparison
511
+ filtered_logs = []
512
+ logs.each do |log|
513
+ log_date_str = log.timestamp.strftime('%Y-%m-%d')
514
+ if log_date_str >= start_date_str && log_date_str <= end_date_str
515
+ filtered_logs << log
516
+ end
517
+ end
518
+
519
+ if options[:verbose]
520
+ puts "Filtered logs: #{filtered_logs.size} of #{total_logs}"
521
+ end
522
+
523
+ # Return the filtered logs
524
+ filtered_logs
525
+ end
526
+
527
+ def format_status(status)
528
+ case status
529
+ when 0 then "Check In"
530
+ when 1 then "Check Out"
531
+ when 2 then "Break Out"
532
+ when 3 then "Break In"
533
+ when 4 then "Overtime In"
534
+ when 5 then "Overtime Out"
535
+ else "Unknown (#{status})"
536
+ end
537
+ end
538
+
539
+ def format_privilege(privilege)
540
+ case privilege
541
+ when 0 then "User"
542
+ when 1 then "Enroller"
543
+ when 2 then "Manager"
544
+ when 3 then "Administrator"
545
+ when 14 then "Super Admin"
546
+ else "Unknown (#{privilege})"
547
+ end
548
+ end
549
+ end
550
+ end
551
+ end
@@ -0,0 +1,85 @@
1
+ require 'yaml'
2
+ require 'fileutils'
3
+
4
+ module RBZK
5
+ module CLI
6
+ # Configuration handler for the CLI
7
+ class Config
8
+ # Default configuration values
9
+ DEFAULT_CONFIG = {
10
+ 'ip' => '192.168.100.201',
11
+ 'port' => 4370,
12
+ 'timeout' => 30,
13
+ 'password' => 0,
14
+ 'verbose' => false,
15
+ 'force_udp' => false,
16
+ 'no_ping' => true,
17
+ 'encoding' => 'UTF-8'
18
+ }.freeze
19
+
20
+ # Initialize a new configuration
21
+ # @param config_file [String] Path to the configuration file
22
+ def initialize(config_file = nil)
23
+ @config_file = config_file || default_config_file
24
+ @config = load_config
25
+ end
26
+
27
+ # Get a configuration value
28
+ # @param key [String, Symbol] Configuration key
29
+ # @return [Object] Configuration value
30
+ def [](key)
31
+ @config[key.to_s]
32
+ end
33
+
34
+ # Set a configuration value
35
+ # @param key [String, Symbol] Configuration key
36
+ # @param value [Object] Configuration value
37
+ def []=(key, value)
38
+ @config[key.to_s] = value
39
+ end
40
+
41
+ # Save the configuration to the file
42
+ def save
43
+ # Create the directory if it doesn't exist
44
+ FileUtils.mkdir_p(File.dirname(@config_file))
45
+
46
+ # Save the configuration
47
+ File.open(@config_file, 'w') do |f|
48
+ f.write(YAML.dump(@config))
49
+ end
50
+ end
51
+
52
+ # Get the default configuration file path
53
+ # @return [String] Default configuration file path
54
+ def default_config_file
55
+ if ENV['XDG_CONFIG_HOME']
56
+ File.join(ENV['XDG_CONFIG_HOME'], 'rbzk', 'config.yml')
57
+ elsif ENV['HOME']
58
+ File.join(ENV['HOME'], '.config', 'rbzk', 'config.yml')
59
+ else
60
+ File.join(Dir.pwd, '.rbzk.yml')
61
+ end
62
+ end
63
+
64
+ # Load the configuration from the file
65
+ # @return [Hash] Configuration values
66
+ def load_config
67
+ if File.exist?(@config_file)
68
+ begin
69
+ config = YAML.load_file(@config_file)
70
+ return DEFAULT_CONFIG.merge(config) if config.is_a?(Hash)
71
+ rescue => e
72
+ warn "Error loading configuration file: #{e.message}"
73
+ end
74
+ end
75
+ DEFAULT_CONFIG.dup
76
+ end
77
+
78
+ # Get all configuration values
79
+ # @return [Hash] All configuration values
80
+ def to_h
81
+ @config.dup
82
+ end
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,14 @@
1
+ require 'rbzk/cli/commands'
2
+
3
+ module RBZK
4
+ # Command Line Interface module for RBZK
5
+ # Provides methods for interacting with ZKTeco devices from the command line
6
+ module CLI
7
+ # Start the CLI with the given arguments
8
+ # @param args [Array<String>] Command line arguments
9
+ # @return [Integer] Exit code
10
+ def self.start(args = ARGV)
11
+ Commands.start(args)
12
+ end
13
+ end
14
+ end
@@ -48,7 +48,6 @@ module RBZK
48
48
  CMD_DOORSTATE_RRQ = 75 # Obtain the door condition
49
49
  CMD_WRITE_MIFARE = 76 # Write the Mifare card
50
50
  CMD_EMPTY_MIFARE = 78 # Clear the Mifare card
51
- CMD_PREPARE_BUFFER = 80 # Prepare buffer for data transfer
52
51
  CMD_READFILE_DATA = 81 # Read data from buffer
53
52
  CMD_GET_USERTEMP = 88 # Get an specific user template (uid, fid)
54
53
  CMD_SAVE_USERTEMPS = 110 # Save user and multiple templates