smartware 0.2.8 → 0.3
Sign up to get free protection for your applications and to get access to all the features.
- data/lib/smartware.rb +2 -0
- data/lib/smartware/drivers/card_reader/ict3_k5.rb +48 -313
- data/lib/smartware/drivers/cash_acceptor/ccnet.rb +212 -144
- data/lib/smartware/drivers/cash_acceptor/dummy.rb +11 -33
- data/lib/smartware/drivers/common/ccnet_connection.rb +135 -0
- data/lib/smartware/drivers/common/command_based_device.rb +101 -0
- data/lib/smartware/drivers/common/sankyo_connection.rb +154 -0
- data/lib/smartware/interfaces/cash_acceptor.rb +53 -101
- data/lib/smartware/service.rb +17 -12
- data/lib/smartware/version.rb +1 -1
- data/smartware.gemspec +1 -0
- metadata +21 -2
@@ -0,0 +1,135 @@
|
|
1
|
+
require "smartware/drivers/common/command_based_device.rb"
|
2
|
+
|
3
|
+
module Smartware
|
4
|
+
class CCNETConnection < CommandBasedDevice
|
5
|
+
|
6
|
+
POLYNOMIAL = 0x8408
|
7
|
+
|
8
|
+
attr_accessor :address
|
9
|
+
|
10
|
+
def initialize
|
11
|
+
super
|
12
|
+
|
13
|
+
@address = 0
|
14
|
+
@command_timer = nil
|
15
|
+
end
|
16
|
+
|
17
|
+
protected
|
18
|
+
|
19
|
+
def remove_timeouts
|
20
|
+
unless @command_timer.nil?
|
21
|
+
EventMachine.cancel_timer @command_timer
|
22
|
+
@command_timer = nil
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def install_timeouts
|
27
|
+
@command_timer = EventMachine.add_timer(1) do
|
28
|
+
@command_timer = nil
|
29
|
+
retry_or_fail
|
30
|
+
post_command unless @executing_command
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def max_retries
|
35
|
+
8
|
36
|
+
end
|
37
|
+
|
38
|
+
def submit_command(command = nil, data = "")
|
39
|
+
if command.kind_of? Array
|
40
|
+
command, subcommand = command
|
41
|
+
else
|
42
|
+
subcommand = nil
|
43
|
+
end
|
44
|
+
|
45
|
+
if data.length <= 250
|
46
|
+
if command.nil?
|
47
|
+
frame = [ 0x02, @address, data.size + 5 ].pack("C*")
|
48
|
+
elsif subcommand.nil?
|
49
|
+
frame = [ 0x02, @address, data.size + 6, command ].pack("C*")
|
50
|
+
else
|
51
|
+
frame = [ 0x02, @address, data.size + 7, command, subcommand ].pack("C*")
|
52
|
+
end
|
53
|
+
else
|
54
|
+
if command.nil?
|
55
|
+
frame = [ 0x02, @address, 0, data.size + 7 ].pack("C3n")
|
56
|
+
elsif subcommand.nil?
|
57
|
+
frame = [ 0x02, @address, 0, command, data.size + 8 ].pack("C4n")
|
58
|
+
else
|
59
|
+
frame = [ 0x02, @address, 0, command, subcommand, data.size + 9 ].pack("C5n")
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
frame << data
|
64
|
+
frame << [ crc_for(frame) ].pack("v")
|
65
|
+
|
66
|
+
send_data frame
|
67
|
+
end
|
68
|
+
|
69
|
+
def crc_for(msg)
|
70
|
+
crc = 0
|
71
|
+
|
72
|
+
msg.each_byte do |byte|
|
73
|
+
crc ^= byte
|
74
|
+
|
75
|
+
8.times do
|
76
|
+
if (crc & 1) != 0
|
77
|
+
crc >>= 1
|
78
|
+
crc ^= POLYNOMIAL
|
79
|
+
else
|
80
|
+
crc >>= 1
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
crc
|
86
|
+
end
|
87
|
+
|
88
|
+
def handle_response
|
89
|
+
while @buffer.size >= 5
|
90
|
+
short_length = @buffer.getbyte(2)
|
91
|
+
if short_length == 0
|
92
|
+
break if @buffer.size < 7
|
93
|
+
|
94
|
+
length, = @buffer.slice(3, 2).unpack("n")
|
95
|
+
else
|
96
|
+
length = short_length
|
97
|
+
end
|
98
|
+
|
99
|
+
break if @buffer.size < length
|
100
|
+
|
101
|
+
message = @buffer.slice! 0, length
|
102
|
+
crc, = message[-2..-1].unpack("v")
|
103
|
+
expected_crc = crc_for(message[0..-3])
|
104
|
+
|
105
|
+
if expected_crc != crc
|
106
|
+
Logging.logger.warn "bad response CRC: #{expected_crc} expected, #{crc} got"
|
107
|
+
|
108
|
+
retry_or_fail if @executing_command
|
109
|
+
next
|
110
|
+
end
|
111
|
+
|
112
|
+
if message.getbyte(1) != @address
|
113
|
+
Logging.logger.warn "unexpected address: #{@buffer.inspect}"
|
114
|
+
|
115
|
+
next
|
116
|
+
end
|
117
|
+
|
118
|
+
if @executing_command
|
119
|
+
# Acknowlege
|
120
|
+
submit_command nil, "\x00"
|
121
|
+
|
122
|
+
if short_length == 0
|
123
|
+
complete message[5..-3]
|
124
|
+
else
|
125
|
+
complete message[3..-3]
|
126
|
+
end
|
127
|
+
else
|
128
|
+
Logging.logger.warn "unexpected message: #{@buffer.inspect}"
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
post_command unless @executing_command
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|
@@ -0,0 +1,101 @@
|
|
1
|
+
module Smartware
|
2
|
+
class CommandBasedDevice < EventMachine::Connection
|
3
|
+
def initialize
|
4
|
+
@ready_for_commands = true
|
5
|
+
@executing_command = false
|
6
|
+
@retries = nil
|
7
|
+
@command = nil
|
8
|
+
@completion_proc = nil
|
9
|
+
@buffer = ""
|
10
|
+
|
11
|
+
@command_queue = []
|
12
|
+
@command_mutex = Mutex.new
|
13
|
+
end
|
14
|
+
|
15
|
+
def receive_data(data)
|
16
|
+
@buffer << data
|
17
|
+
|
18
|
+
handle_response
|
19
|
+
end
|
20
|
+
|
21
|
+
def command(*args, &block)
|
22
|
+
if !block_given?
|
23
|
+
queue = Queue.new
|
24
|
+
command(*args) do |resp|
|
25
|
+
queue.push resp
|
26
|
+
end
|
27
|
+
queue.pop
|
28
|
+
else
|
29
|
+
@command_mutex.synchronize do
|
30
|
+
@command_queue.push [ args, block ]
|
31
|
+
end
|
32
|
+
|
33
|
+
EventMachine.schedule method(:post_command)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
protected
|
38
|
+
|
39
|
+
def ready?
|
40
|
+
@ready_for_commands && !@executing_command
|
41
|
+
end
|
42
|
+
|
43
|
+
def post_command
|
44
|
+
executing_command, command = @command_mutex.synchronize do
|
45
|
+
if ready?
|
46
|
+
[ @command_queue.any?, @command_queue.first ]
|
47
|
+
else
|
48
|
+
[ false, nil ]
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
if executing_command
|
53
|
+
@executing_command = executing_command
|
54
|
+
install_timeouts
|
55
|
+
@retries = max_retries
|
56
|
+
command, = @command_mutex.synchronize { @command_queue.first }
|
57
|
+
submit_command *command
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def kill_queue
|
62
|
+
blocks = nil
|
63
|
+
|
64
|
+
@command_mutex.synchronize do
|
65
|
+
blocks = @command_queue.map { |command, block| block }
|
66
|
+
@command_queue.clear
|
67
|
+
|
68
|
+
@ready_for_commands = false
|
69
|
+
end
|
70
|
+
|
71
|
+
blocks.each do |block|
|
72
|
+
block.call nil
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
def retry_or_fail
|
77
|
+
if @retries == 0
|
78
|
+
complete nil
|
79
|
+
else
|
80
|
+
@retries -= 1
|
81
|
+
command, = @command_mutex.synchronize { @command_queue.first }
|
82
|
+
remove_timeouts
|
83
|
+
install_timeouts
|
84
|
+
submit_command *command
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
def complete(data)
|
89
|
+
command, proc = @command_mutex.synchronize { @command_queue.shift }
|
90
|
+
remove_timeouts
|
91
|
+
@executing_command = false
|
92
|
+
|
93
|
+
begin
|
94
|
+
proc.call data
|
95
|
+
rescue => e
|
96
|
+
Logging.logger.error "Error in completion handler: #{e}"
|
97
|
+
e.backtrace.each { |line| Logging.logger.error line }
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
@@ -0,0 +1,154 @@
|
|
1
|
+
require "smartware/drivers/common/command_based_device"
|
2
|
+
require "digest/crc16_xmodem"
|
3
|
+
|
4
|
+
module Smartware
|
5
|
+
class SankyoConnection < CommandBasedDevice
|
6
|
+
class CommandResponse
|
7
|
+
def initialize(response)
|
8
|
+
@response = response
|
9
|
+
end
|
10
|
+
|
11
|
+
def positive?
|
12
|
+
@response[0] == "P"
|
13
|
+
end
|
14
|
+
|
15
|
+
def negative?
|
16
|
+
@response[0] != "P"
|
17
|
+
end
|
18
|
+
|
19
|
+
def response
|
20
|
+
@response[1..-1]
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
STX = 0xF2
|
25
|
+
ACK = 0x06
|
26
|
+
NAK = 0x15
|
27
|
+
|
28
|
+
attr_accessor :initialize_device
|
29
|
+
|
30
|
+
def initialize
|
31
|
+
super
|
32
|
+
|
33
|
+
@command_timer = nil
|
34
|
+
@initialize_device = nil
|
35
|
+
@ready_for_commands = false
|
36
|
+
@state = :drop
|
37
|
+
|
38
|
+
EventMachine.add_periodic_timer(10) { check_dsr! }
|
39
|
+
EventMachine.next_tick { check_dsr! }
|
40
|
+
end
|
41
|
+
|
42
|
+
def check_dsr!
|
43
|
+
if @io.dsr == 0
|
44
|
+
@state = :drop
|
45
|
+
kill_queue
|
46
|
+
else
|
47
|
+
was_ready, @ready_for_commands = @ready_for_commands, true
|
48
|
+
|
49
|
+
if !was_ready
|
50
|
+
@initialize_device.call
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
protected
|
56
|
+
|
57
|
+
def remove_timeouts
|
58
|
+
unless @command_timer.nil?
|
59
|
+
EventMachine.cancel_timer @command_timer
|
60
|
+
@command_timer = nil
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
def install_timeouts(type = :sync)
|
65
|
+
interval = nil
|
66
|
+
case type
|
67
|
+
when :sync
|
68
|
+
interval = 0.3
|
69
|
+
|
70
|
+
when :command
|
71
|
+
interval = 20
|
72
|
+
|
73
|
+
else
|
74
|
+
raise "invalid timeout type: #{type.inspect}"
|
75
|
+
end
|
76
|
+
|
77
|
+
@command_timer = EventMachine.add_timer(interval) do
|
78
|
+
@command_timer = nil
|
79
|
+
retry_or_fail
|
80
|
+
post_command unless @executing_command
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
def max_retries
|
85
|
+
8
|
86
|
+
end
|
87
|
+
|
88
|
+
def submit_command(*bytes)
|
89
|
+
data = [ STX, bytes.length + 1 ].pack("Cn")
|
90
|
+
data << "C"
|
91
|
+
data << bytes.pack("C*")
|
92
|
+
|
93
|
+
crc = Digest::CRC16XModem.new
|
94
|
+
crc << data
|
95
|
+
data << [ crc.checksum ].pack("n")
|
96
|
+
|
97
|
+
@state = :sync
|
98
|
+
send_data data
|
99
|
+
end
|
100
|
+
|
101
|
+
def complete(response)
|
102
|
+
@state = :drop
|
103
|
+
super
|
104
|
+
end
|
105
|
+
|
106
|
+
def handle_response
|
107
|
+
until @buffer.empty?
|
108
|
+
case @state
|
109
|
+
when :drop
|
110
|
+
@buffer.clear
|
111
|
+
|
112
|
+
when :sync
|
113
|
+
initial_byte = @buffer.slice!(0, 1).ord
|
114
|
+
|
115
|
+
case initial_byte
|
116
|
+
when ACK
|
117
|
+
@state = :response
|
118
|
+
remove_timeouts
|
119
|
+
install_timeouts :command
|
120
|
+
|
121
|
+
when NAK
|
122
|
+
retry_or_fail
|
123
|
+
end
|
124
|
+
|
125
|
+
when :response
|
126
|
+
break if @buffer.length < 5
|
127
|
+
|
128
|
+
leading_byte, length = @buffer[0..2].unpack("Cn")
|
129
|
+
if leading_byte != STX
|
130
|
+
@buffer.slice! 0, 1
|
131
|
+
next
|
132
|
+
end
|
133
|
+
|
134
|
+
full_length = 5 + length
|
135
|
+
|
136
|
+
break if @buffer.length < full_length
|
137
|
+
|
138
|
+
message = @buffer.slice! 0, full_length
|
139
|
+
sum, = message.slice!(full_length - 2, 2).unpack("n")
|
140
|
+
crc = Digest::CRC16XModem.new
|
141
|
+
crc << message
|
142
|
+
if sum == crc.checksum
|
143
|
+
send_data ACK.chr
|
144
|
+
complete CommandResponse.new(message[3..-1])
|
145
|
+
else
|
146
|
+
send_data NAK.chr
|
147
|
+
end
|
148
|
+
end
|
149
|
+
end
|
150
|
+
|
151
|
+
post_command
|
152
|
+
end
|
153
|
+
end
|
154
|
+
end
|
@@ -19,9 +19,28 @@ module Smartware
|
|
19
19
|
CAPACITANCE_CANAL_FAILURE = 15
|
20
20
|
COMMUNICATION_ERROR = 16
|
21
21
|
|
22
|
+
class BillType
|
23
|
+
attr_reader :value
|
24
|
+
attr_reader :country
|
25
|
+
|
26
|
+
def initialize(value, country)
|
27
|
+
@value = value
|
28
|
+
@country = country
|
29
|
+
end
|
30
|
+
|
31
|
+
def to_s
|
32
|
+
"#{@country}:#{@value}"
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
22
36
|
def initialize(config)
|
23
37
|
super
|
24
38
|
|
39
|
+
@device.escrow = method :escrow
|
40
|
+
@device.stacked = method :stacked
|
41
|
+
@device.returned = method :returned
|
42
|
+
@device.status = method :status
|
43
|
+
|
25
44
|
@limit = nil
|
26
45
|
@banknotes = {}
|
27
46
|
@banknotes.default = 0
|
@@ -30,12 +49,6 @@ module Smartware
|
|
30
49
|
@status[:casette] = true
|
31
50
|
end
|
32
51
|
|
33
|
-
@accepting = false
|
34
|
-
@commands = Queue.new
|
35
|
-
@commands.push :close
|
36
|
-
|
37
|
-
Thread.new &method(:dispatch_commands)
|
38
|
-
Thread.new &method(:periodic)
|
39
52
|
Smartware::Logging.logger.info "Cash acceptor monitor started"
|
40
53
|
end
|
41
54
|
|
@@ -45,21 +58,34 @@ module Smartware
|
|
45
58
|
if limit_min.nil? || limit_max.nil?
|
46
59
|
@limit = nil
|
47
60
|
|
48
|
-
Smartware::Logging.logger.
|
61
|
+
Smartware::Logging.logger.debug "Session open, unlimited"
|
49
62
|
else
|
50
63
|
@limit = limit_min..limit_max
|
51
64
|
|
52
|
-
Smartware::Logging.logger.
|
65
|
+
Smartware::Logging.logger.debug "Session open, limit: #{@limit}"
|
53
66
|
end
|
54
67
|
|
55
|
-
|
56
|
-
|
68
|
+
EventMachine.schedule do
|
69
|
+
types = 0
|
70
|
+
@device.bill_types.each_with_index do |type, index|
|
71
|
+
next if type.nil?
|
72
|
+
|
73
|
+
if type.country == 'RUS'
|
74
|
+
types |= 1 << index
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
@device.enabled_types = types
|
79
|
+
end
|
57
80
|
end
|
58
81
|
|
59
82
|
def close_session
|
60
|
-
Smartware::Logging.logger.
|
83
|
+
Smartware::Logging.logger.debug "Session closed"
|
84
|
+
|
85
|
+
EventMachine.schedule do
|
86
|
+
@device.enabled_types = 0
|
87
|
+
end
|
61
88
|
|
62
|
-
@commands.push :close
|
63
89
|
@limit = nil
|
64
90
|
end
|
65
91
|
|
@@ -75,110 +101,36 @@ module Smartware
|
|
75
101
|
|
76
102
|
private
|
77
103
|
|
78
|
-
def limit_satisfied(sum)
|
104
|
+
def limit_satisfied?(sum)
|
79
105
|
@limit.nil? or @limit.include? sum
|
80
106
|
end
|
81
107
|
|
82
|
-
def
|
83
|
-
|
84
|
-
@device.accept
|
85
|
-
Smartware::Logging.logger.info "Cash acceptor open"
|
86
|
-
|
87
|
-
@accepting = true
|
108
|
+
def escrow(banknote)
|
109
|
+
limit_satisfied?(self.cashsum + banknote.value)
|
88
110
|
end
|
89
111
|
|
90
|
-
def
|
91
|
-
|
112
|
+
def stacked(banknote)
|
113
|
+
value = banknote.value
|
92
114
|
|
93
|
-
|
94
|
-
|
95
|
-
if limit_satisfied(self.cashsum + res)
|
96
|
-
@device.stack
|
97
|
-
update_status do
|
98
|
-
@banknotes[res] += 1
|
99
|
-
end
|
100
|
-
Smartware::Logging.logger.info "Cash acceptor bill stacked, #{res}"
|
101
|
-
else
|
102
|
-
@device.return
|
103
|
-
Smartware::Logging.logger.info "Cash acceptor limit violation, return #{res}"
|
104
|
-
end
|
105
|
-
|
106
|
-
when String
|
107
|
-
Smartware::Logging.logger.error "Cash acceptor error #{res}"
|
108
|
-
|
109
|
-
update_status do
|
110
|
-
@status[:error] = res
|
111
|
-
end
|
115
|
+
update_status do
|
116
|
+
@banknotes[value] += 1
|
112
117
|
end
|
113
118
|
|
114
|
-
|
115
|
-
# Close cash acceptor if current cashsum equal max-limit
|
116
|
-
|
117
|
-
execute_close
|
118
|
-
end
|
119
|
+
Smartware::Logging.logger.debug "cash acceptor: bill stacked, #{value}"
|
119
120
|
end
|
120
121
|
|
121
|
-
def
|
122
|
-
|
123
|
-
Smartware::Logging.logger.info "Cash acceptor close"
|
122
|
+
def returned(banknote)
|
123
|
+
value = banknote.value
|
124
124
|
|
125
|
-
|
125
|
+
Smartware::Logging.logger.debug "cash acceptor: bill returned, #{value}"
|
126
126
|
end
|
127
127
|
|
128
|
-
def
|
129
|
-
error = @device.error
|
130
|
-
model = @device.model
|
131
|
-
version = @device.version
|
132
|
-
cassette = @device.cassette?
|
133
|
-
|
128
|
+
def status(error)
|
134
129
|
update_status do
|
135
130
|
@status[:error] = error
|
136
|
-
@status[:model] = model
|
137
|
-
@status[:version] = version
|
138
|
-
@status[:
|
139
|
-
end
|
140
|
-
end
|
141
|
-
|
142
|
-
def dispatch_commands
|
143
|
-
loop do
|
144
|
-
command = @commands.pop
|
145
|
-
|
146
|
-
begin
|
147
|
-
start = Time.now
|
148
|
-
|
149
|
-
send :"execute_#{command}"
|
150
|
-
|
151
|
-
complete = Time.now
|
152
|
-
if complete - start > 1
|
153
|
-
Smartware::Logging.logger.warn "#{command} has been running for #{complete - start} seconds."
|
154
|
-
end
|
155
|
-
|
156
|
-
rescue => e
|
157
|
-
Smartware::Logging.logger.error "Execution of #{command} failed:"
|
158
|
-
Smartware::Logging.logger.error e.to_s
|
159
|
-
e.backtrace.each do |line|
|
160
|
-
Smartware::Logging.logger.error line
|
161
|
-
end
|
162
|
-
end
|
163
|
-
end
|
164
|
-
end
|
165
|
-
|
166
|
-
def periodic
|
167
|
-
loop do
|
168
|
-
begin
|
169
|
-
if @commands.empty?
|
170
|
-
@commands.push :monitor
|
171
|
-
@commands.push :get_money if @accepting
|
172
|
-
end
|
173
|
-
|
174
|
-
sleep 0.5
|
175
|
-
rescue => e
|
176
|
-
Smartware::Logging.logger.error "Error in periodic failed:"
|
177
|
-
Smartware::Logging.logger.error e.to_s
|
178
|
-
e.backtrace.each do |line|
|
179
|
-
Smartware::Logging.logger.error line
|
180
|
-
end
|
181
|
-
end
|
131
|
+
@status[:model] = @device.model
|
132
|
+
@status[:version] = @device.version
|
133
|
+
@status[:casette] = error != DROP_CASETTE_OUT_OF_POSITION
|
182
134
|
end
|
183
135
|
end
|
184
136
|
end
|