smartware 0.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.
- data/.gitignore +17 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +29 -0
- data/Rakefile +1 -0
- data/bin/smartware +28 -0
- data/lib/smartware.rb +25 -0
- data/lib/smartware/clients/cash_acceptor.rb +40 -0
- data/lib/smartware/clients/modem.rb +50 -0
- data/lib/smartware/clients/printer.rb +40 -0
- data/lib/smartware/drivers/cash_acceptor/ccnet.rb +199 -0
- data/lib/smartware/drivers/cash_acceptor/dummy.rb +52 -0
- data/lib/smartware/drivers/modem/dummy.rb +39 -0
- data/lib/smartware/drivers/modem/standart.rb +74 -0
- data/lib/smartware/drivers/printer/dummy.rb +25 -0
- data/lib/smartware/drivers/printer/tg24xx.rb +57 -0
- data/lib/smartware/interfaces/cash_acceptor.rb +131 -0
- data/lib/smartware/interfaces/modem.rb +80 -0
- data/lib/smartware/interfaces/printer.rb +73 -0
- data/lib/smartware/logging.rb +31 -0
- data/lib/smartware/service.rb +29 -0
- data/lib/smartware/version.rb +3 -0
- data/smartware.gemspec +23 -0
- metadata +93 -0
data/.gitignore
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2012 ya.jeks
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
# Smartware
|
2
|
+
|
3
|
+
TODO: Write a gem description
|
4
|
+
|
5
|
+
## Installation
|
6
|
+
|
7
|
+
Add this line to your application's Gemfile:
|
8
|
+
|
9
|
+
gem 'smartware'
|
10
|
+
|
11
|
+
And then execute:
|
12
|
+
|
13
|
+
$ bundle
|
14
|
+
|
15
|
+
Or install it yourself as:
|
16
|
+
|
17
|
+
$ gem install smartware
|
18
|
+
|
19
|
+
## Usage
|
20
|
+
|
21
|
+
TODO: Write usage instructions here
|
22
|
+
|
23
|
+
## Contributing
|
24
|
+
|
25
|
+
1. Fork it
|
26
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
27
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
28
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
29
|
+
5. Create new Pull Request
|
data/Rakefile
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require "bundler/gem_tasks"
|
data/bin/smartware
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
trap 'TTIN' do
|
4
|
+
Thread.list.each do |thread|
|
5
|
+
if thread.backtrace
|
6
|
+
puts thread.backtrace.join("\n")
|
7
|
+
else
|
8
|
+
puts "<no backtrace available>"
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
require 'smartware/service'
|
14
|
+
require 'dante'
|
15
|
+
|
16
|
+
runner = Dante::Runner.new('smartware')
|
17
|
+
runner.description = "Smartkiosk hardware control daemon"
|
18
|
+
|
19
|
+
runner.execute do |opts|
|
20
|
+
begin
|
21
|
+
Smartware::Service.start
|
22
|
+
rescue => e
|
23
|
+
raise e if $DEBUG
|
24
|
+
STDERR.puts e.message
|
25
|
+
STDERR.puts e.backtrace.join("\n")
|
26
|
+
exit 1
|
27
|
+
end
|
28
|
+
end
|
data/lib/smartware.rb
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
require 'smartware/clients/cash_acceptor'
|
2
|
+
require 'smartware/clients/printer'
|
3
|
+
require 'smartware/clients/modem'
|
4
|
+
|
5
|
+
module Smartware
|
6
|
+
|
7
|
+
def self.devices
|
8
|
+
yield self
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.cash_acceptor
|
12
|
+
Smartware::Client::CashAcceptor
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.printer
|
16
|
+
Smartware::Client::Printer
|
17
|
+
end
|
18
|
+
|
19
|
+
def self.modem
|
20
|
+
Smartware::Client::Modem
|
21
|
+
end
|
22
|
+
|
23
|
+
end
|
24
|
+
|
25
|
+
|
@@ -0,0 +1,40 @@
|
|
1
|
+
require 'drb'
|
2
|
+
|
3
|
+
module Smartware
|
4
|
+
module Client
|
5
|
+
|
6
|
+
module CashAcceptor
|
7
|
+
|
8
|
+
def self.configure(port, driver)
|
9
|
+
DRb.start_service
|
10
|
+
@device = DRbObject.new_with_uri('druby://localhost:6001')
|
11
|
+
@device.configure!(port, driver)
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.open(limit_min = nil, limit_max = nil)
|
15
|
+
@device.open_session(limit_min, limit_max)
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.close
|
19
|
+
@device.close_session
|
20
|
+
end
|
21
|
+
|
22
|
+
def self.status
|
23
|
+
@device.status
|
24
|
+
end
|
25
|
+
|
26
|
+
def self.model
|
27
|
+
@device.model
|
28
|
+
end
|
29
|
+
|
30
|
+
def self.banknotes
|
31
|
+
@device.banknotes
|
32
|
+
end
|
33
|
+
|
34
|
+
def self.sum
|
35
|
+
@device.cashsum
|
36
|
+
end
|
37
|
+
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
require 'drb'
|
2
|
+
|
3
|
+
module Smartware
|
4
|
+
module Client
|
5
|
+
|
6
|
+
module Modem
|
7
|
+
|
8
|
+
def self.configure(port, driver)
|
9
|
+
DRb.start_service
|
10
|
+
@device = DRbObject.new_with_uri('druby://localhost:6002')
|
11
|
+
@device.configure!(port, driver)
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.error
|
15
|
+
@device.error
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.model
|
19
|
+
@device.model
|
20
|
+
end
|
21
|
+
|
22
|
+
def self.balance
|
23
|
+
@device.balance
|
24
|
+
end
|
25
|
+
|
26
|
+
def self.signal_level
|
27
|
+
@device.signal_level
|
28
|
+
end
|
29
|
+
|
30
|
+
def self.d_model
|
31
|
+
@device.d_model
|
32
|
+
end
|
33
|
+
|
34
|
+
def self.d_signal
|
35
|
+
@device.d_signal
|
36
|
+
end
|
37
|
+
|
38
|
+
def self.d_balance
|
39
|
+
@device.d_balance
|
40
|
+
end
|
41
|
+
|
42
|
+
def self.d_stop
|
43
|
+
@device.d_stop
|
44
|
+
end
|
45
|
+
|
46
|
+
|
47
|
+
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
require 'drb'
|
2
|
+
|
3
|
+
module Smartware
|
4
|
+
module Client
|
5
|
+
|
6
|
+
module Printer
|
7
|
+
|
8
|
+
def self.configure(port, driver)
|
9
|
+
DRb.start_service
|
10
|
+
@device = DRbObject.new_with_uri('druby://localhost:6005')
|
11
|
+
@device.configure!(port, driver)
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.error
|
15
|
+
@device.error
|
16
|
+
rescue => e
|
17
|
+
'No device'
|
18
|
+
end
|
19
|
+
|
20
|
+
def self.model
|
21
|
+
@device.model
|
22
|
+
rescue => e
|
23
|
+
'No device'
|
24
|
+
end
|
25
|
+
|
26
|
+
def self.test
|
27
|
+
@device.test
|
28
|
+
rescue => e
|
29
|
+
'No device'
|
30
|
+
end
|
31
|
+
|
32
|
+
def self.print(filepath)
|
33
|
+
@device.print filepath
|
34
|
+
rescue => e
|
35
|
+
'No device'
|
36
|
+
end
|
37
|
+
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,199 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
require 'serialport'
|
3
|
+
|
4
|
+
module Smartware
|
5
|
+
module Driver
|
6
|
+
module CashAcceptor
|
7
|
+
|
8
|
+
class CCNET
|
9
|
+
|
10
|
+
RESET = 0x30
|
11
|
+
GET_STATUS = 0x31
|
12
|
+
SET_SECURITY = 0x32
|
13
|
+
POLL = 0x33
|
14
|
+
ENABLE_BILL_TYPES = 0x34
|
15
|
+
STACK = 0x35
|
16
|
+
RETURN = 0x36
|
17
|
+
IDENTIFICATION = 0x37
|
18
|
+
HOLD = 0x38
|
19
|
+
SET_BARCODE_PARAMETERS = 0x39
|
20
|
+
EXTRACT_BARCODE_DATA = 0x3A
|
21
|
+
GET_BILL_TABLE = 0x41
|
22
|
+
GET_CRC32_OF_THE_CODE = 0x51
|
23
|
+
DOWNLOAD = 0x50
|
24
|
+
REQUEST_STATISTICS = 0x60
|
25
|
+
ACK = 0x00
|
26
|
+
DISPENCE = 0x3C
|
27
|
+
|
28
|
+
STATUSES = {
|
29
|
+
"10" => "10", # 'Power up',
|
30
|
+
"11" => "11", # 'Power Up with Bill in Validator',
|
31
|
+
"12" => "12", # 'Power Up with Bill in Stacker',
|
32
|
+
"13" => "13", # 'Initialize',
|
33
|
+
"14" => "14", # 'Idling',
|
34
|
+
"15" => "15", # 'Accepting',
|
35
|
+
"17" => "16", # 'Stacking',
|
36
|
+
"18" => "17", # 'Returning',
|
37
|
+
"19" => "18", # 'Unit Disabled',
|
38
|
+
"1a" => "19", # 'Holding',
|
39
|
+
"1b" => "20", # 'Device Busy',
|
40
|
+
"1c" => "21", # 'Rejecting',
|
41
|
+
"82" => "22" # 'Bill returned'
|
42
|
+
}
|
43
|
+
|
44
|
+
ERRORS = {
|
45
|
+
"41" => "23", # 'Drop Cassette Full',
|
46
|
+
"42" => "24", # 'Drop Cassette out of position. The Bill Validator has detected the drop cassette to be open or removed.',
|
47
|
+
"43" => "25", # 'Validator Jammed. A bill(s) has jammed in the acceptance path.',
|
48
|
+
"44" => "26", # 'Drop Cassette Jammed. A bill has jammed in drop cassette.',
|
49
|
+
"45" => "27", # 'Cheated. Intentions of the user to deceive the Bill Validator are detected',
|
50
|
+
"46" => "28", # 'Pause. Bill Validator stops motion of the second bill until the second bill is removed.',
|
51
|
+
"47" => "29", # 'Bill validator failure',
|
52
|
+
"50" => "30", # 'Stack Motor Failure',
|
53
|
+
"51" => "31", # 'Transport Motor Speed Failure',
|
54
|
+
"52" => "32", # 'Transport Motor Failure',
|
55
|
+
"53" => "33", # 'Aligning Motor Failure',
|
56
|
+
"54" => "34", # 'Initial Cassette Status Failure',
|
57
|
+
"55" => "35", # 'Optic Canal Failure',
|
58
|
+
"56" => "36", # 'Magnetic Canal Failure',
|
59
|
+
"5f" => "37" # 'Capacitance Canal Failure'
|
60
|
+
}
|
61
|
+
|
62
|
+
NOMINALS = { "2" => 10, "3" => 50, "4" => 100, "5" => 500, "6" => 1000, "7" => 5000 }
|
63
|
+
|
64
|
+
def initialize(port)
|
65
|
+
@port = port
|
66
|
+
end
|
67
|
+
|
68
|
+
def cassette?
|
69
|
+
return false if error == "24"
|
70
|
+
true
|
71
|
+
end
|
72
|
+
|
73
|
+
def model
|
74
|
+
if answer = send([IDENTIFICATION], false)
|
75
|
+
answer = answer[2..answer.length]
|
76
|
+
return "#{answer[0..15]} #{answer[16..27]} #{answer[28..34].unpack("C*")}"
|
77
|
+
else
|
78
|
+
return "Unknown device answer"
|
79
|
+
end
|
80
|
+
rescue
|
81
|
+
-1
|
82
|
+
end
|
83
|
+
|
84
|
+
def error
|
85
|
+
res = poll
|
86
|
+
ack
|
87
|
+
return false if res != nil and CCNET::STATUSES.keys.include?(res[3])
|
88
|
+
return false if res == nil
|
89
|
+
|
90
|
+
result = check_error(res)
|
91
|
+
rescue
|
92
|
+
-1
|
93
|
+
end
|
94
|
+
|
95
|
+
def current_banknote
|
96
|
+
poll
|
97
|
+
ack
|
98
|
+
hold
|
99
|
+
res = poll
|
100
|
+
ack
|
101
|
+
|
102
|
+
result = check_error(res)
|
103
|
+
if res != nil and res[2] == "7" and res[3] == "80" and CCNET::NOMINALS.keys.include?(res[4]) # has money?
|
104
|
+
result = CCNET::NOMINALS[res[4]]
|
105
|
+
end
|
106
|
+
result
|
107
|
+
end
|
108
|
+
|
109
|
+
def get_status
|
110
|
+
send([GET_STATUS])
|
111
|
+
end
|
112
|
+
|
113
|
+
def reset
|
114
|
+
send([RESET])
|
115
|
+
end
|
116
|
+
|
117
|
+
def ack
|
118
|
+
send([ACK])
|
119
|
+
end
|
120
|
+
|
121
|
+
def stack
|
122
|
+
send([STACK])
|
123
|
+
end
|
124
|
+
|
125
|
+
def return
|
126
|
+
send([RETURN])
|
127
|
+
end
|
128
|
+
|
129
|
+
def hold
|
130
|
+
send([])
|
131
|
+
end
|
132
|
+
|
133
|
+
def poll
|
134
|
+
send([POLL])
|
135
|
+
end
|
136
|
+
|
137
|
+
def accept
|
138
|
+
send([ENABLE_BILL_TYPES,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF])
|
139
|
+
end
|
140
|
+
|
141
|
+
def cancel_accept
|
142
|
+
send([ENABLE_BILL_TYPES,0x00,0x00,0x00,0x00,0x00,0x00])
|
143
|
+
get_status
|
144
|
+
end
|
145
|
+
|
146
|
+
private
|
147
|
+
def check_error(res)
|
148
|
+
if CCNET::ERRORS.keys.include? res[3]
|
149
|
+
res[3] == "47" ? CCNET::ERRORS[res[4]] : CCNET::ERRORS[res[3]] # More details for 47 error
|
150
|
+
else
|
151
|
+
false
|
152
|
+
end
|
153
|
+
end
|
154
|
+
|
155
|
+
def crc_for(msg)
|
156
|
+
polynom = 0x8408.to_i
|
157
|
+
msg = msg.collect{|o| o.to_i}
|
158
|
+
res = 0
|
159
|
+
tmpcrc = 0
|
160
|
+
0.upto(msg.length-1){|i|
|
161
|
+
tmpcrc = res ^ msg[i]
|
162
|
+
0.upto(7){
|
163
|
+
if tmpcrc & 1 != 0
|
164
|
+
tmpcrc = tmpcrc >> 1
|
165
|
+
tmpcrc = tmpcrc ^ polynom
|
166
|
+
else
|
167
|
+
tmpcrc = tmpcrc >> 1
|
168
|
+
end
|
169
|
+
}
|
170
|
+
res = tmpcrc
|
171
|
+
}
|
172
|
+
crc = tmpcrc
|
173
|
+
crc = ("%02x" % crc).rjust(4,"0")
|
174
|
+
crc = [Integer("0x"+crc[2..3]), Integer("0x"+crc[0..1])]
|
175
|
+
end
|
176
|
+
|
177
|
+
def send(msg, parse_answer = true)
|
178
|
+
sp = SerialPort.new(@port, 9600, 8, 1, SerialPort::NONE)
|
179
|
+
sp.read_timeout = 100
|
180
|
+
message = [0x02, 0x03, 5 + msg.length]
|
181
|
+
message += msg
|
182
|
+
crc = crc_for(message)
|
183
|
+
message += crc
|
184
|
+
message = message.pack("C*")
|
185
|
+
sp.write message
|
186
|
+
ans = sp.gets
|
187
|
+
if ans
|
188
|
+
parse_answer ? res = ans.unpack("C*").map{|e| e.to_s(16) } : res = ans
|
189
|
+
else
|
190
|
+
res = nil
|
191
|
+
end
|
192
|
+
sp.close
|
193
|
+
res
|
194
|
+
end
|
195
|
+
end
|
196
|
+
|
197
|
+
end
|
198
|
+
end
|
199
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
module Smartware
|
3
|
+
module Driver
|
4
|
+
module CashAcceptor
|
5
|
+
|
6
|
+
class Dummy
|
7
|
+
|
8
|
+
def initialize(port)
|
9
|
+
@port = port
|
10
|
+
end
|
11
|
+
|
12
|
+
def model
|
13
|
+
"Dummy cash acceptor v1.0"
|
14
|
+
end
|
15
|
+
|
16
|
+
def cassette?
|
17
|
+
true
|
18
|
+
end
|
19
|
+
|
20
|
+
def error
|
21
|
+
false
|
22
|
+
end
|
23
|
+
|
24
|
+
def current_banknote
|
25
|
+
return false if ( rand(9) < 6 )
|
26
|
+
[20, 40, 60, 80].sample
|
27
|
+
end
|
28
|
+
|
29
|
+
def accept
|
30
|
+
|
31
|
+
end
|
32
|
+
|
33
|
+
def cancel_accept
|
34
|
+
|
35
|
+
end
|
36
|
+
|
37
|
+
def stack
|
38
|
+
|
39
|
+
end
|
40
|
+
|
41
|
+
def return
|
42
|
+
|
43
|
+
end
|
44
|
+
|
45
|
+
def reset
|
46
|
+
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
module Smartware
|
3
|
+
module Driver
|
4
|
+
module Modem
|
5
|
+
|
6
|
+
class Dummy
|
7
|
+
|
8
|
+
def initialize(port)
|
9
|
+
@port = port
|
10
|
+
end
|
11
|
+
|
12
|
+
def model
|
13
|
+
'Dummy modem v1.0'
|
14
|
+
end
|
15
|
+
|
16
|
+
def error
|
17
|
+
false
|
18
|
+
end
|
19
|
+
|
20
|
+
def signal_level
|
21
|
+
"-#{rand(90)*10+rand(9)} dbm"
|
22
|
+
end
|
23
|
+
|
24
|
+
#
|
25
|
+
# Method send ussd to operator and return only valid answer body
|
26
|
+
# Don`t call with method synchronously from app, method waits USSD answer 3 sec,
|
27
|
+
# Use some scheduler and buffer for balance value
|
28
|
+
#
|
29
|
+
def ussd(code="*100#")
|
30
|
+
sleep 3
|
31
|
+
"#{rand(90)*100+rand(9)} " + %w(руб. dollars тэньге).sample
|
32
|
+
end
|
33
|
+
|
34
|
+
end
|
35
|
+
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
@@ -0,0 +1,74 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
require 'serialport'
|
3
|
+
|
4
|
+
module Smartware
|
5
|
+
module Driver
|
6
|
+
module Modem
|
7
|
+
|
8
|
+
class Standart
|
9
|
+
|
10
|
+
def initialize(port)
|
11
|
+
# @port Class variable needs for ussd request
|
12
|
+
@port = port
|
13
|
+
@sp = SerialPort.new(@port, 115200, 8, 1, SerialPort::NONE)
|
14
|
+
end
|
15
|
+
|
16
|
+
def error
|
17
|
+
false
|
18
|
+
end
|
19
|
+
|
20
|
+
def model
|
21
|
+
res = send 'ATI' rescue -1
|
22
|
+
return -1 unless res.last == "OK"
|
23
|
+
|
24
|
+
res.shift
|
25
|
+
res.pop
|
26
|
+
res.join(' ')
|
27
|
+
rescue
|
28
|
+
-1
|
29
|
+
end
|
30
|
+
|
31
|
+
#
|
32
|
+
# Returns signal level in dbm
|
33
|
+
#
|
34
|
+
def signal_level
|
35
|
+
res = send 'AT+CSQ'
|
36
|
+
return -1 if res.last != "OK"
|
37
|
+
value = res[1].gsub("+CSQ: ",'').split(',')[0].to_i
|
38
|
+
"#{(-113 + value * 2)} dbm"
|
39
|
+
rescue
|
40
|
+
-1
|
41
|
+
end
|
42
|
+
|
43
|
+
#
|
44
|
+
# Method send ussd to operator and return only valid answer body
|
45
|
+
# Don`t call with method synchronously from app, method waits USSD answer 3 sec,
|
46
|
+
# Use some scheduler and buffer for balance value
|
47
|
+
#
|
48
|
+
def ussd(code="*100#")
|
49
|
+
port = SerialPort.new(@port, 115200, 8, 1, SerialPort::NONE)
|
50
|
+
port.read_timeout = 3000
|
51
|
+
port.write "AT+CUSD=1,\"#{code}\",15\r\n"
|
52
|
+
ussd_body = port.read.split(/[\r\n]+/).last.split(",")[1].gsub('"','') # Get only USSD message body
|
53
|
+
port.close
|
54
|
+
ussd_body.scan(/\w{4}/).map{|i| [i.hex].pack("U") }.join # Encode USSD message from broken ucs2 to utf-8
|
55
|
+
rescue
|
56
|
+
-1
|
57
|
+
end
|
58
|
+
|
59
|
+
private
|
60
|
+
def send cmd
|
61
|
+
@sp.write "#{ cmd }\r\n"
|
62
|
+
answer = ''
|
63
|
+
while IO.select [@sp], [], [], 0.25
|
64
|
+
chr = @sp.getc.chr
|
65
|
+
answer << chr
|
66
|
+
end
|
67
|
+
answer.split(/[\r\n]+/)
|
68
|
+
end
|
69
|
+
|
70
|
+
end
|
71
|
+
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
require 'serialport'
|
3
|
+
|
4
|
+
module Smartware
|
5
|
+
module Driver
|
6
|
+
module Printer
|
7
|
+
|
8
|
+
class TG24XX
|
9
|
+
|
10
|
+
def initialize(port)
|
11
|
+
@sp = SerialPort.new(port, 115200, 8, 1, SerialPort::NONE)
|
12
|
+
@sp.read_timeout = 100
|
13
|
+
end
|
14
|
+
|
15
|
+
def error
|
16
|
+
res = send "\x1Dr1"
|
17
|
+
return "1" unless res # No answer
|
18
|
+
return "" if res == "0" # No error
|
19
|
+
return "2" if res == "f" # Paper not present
|
20
|
+
return "3" if res == "3" # Paper near end
|
21
|
+
rescue
|
22
|
+
-1
|
23
|
+
end
|
24
|
+
|
25
|
+
def model
|
26
|
+
model_code = send "\x1DI1"
|
27
|
+
rom_version = send "\x1DI3", false
|
28
|
+
model_name = case model_code
|
29
|
+
when "a7" then 'Custom TG2460H'
|
30
|
+
when "a8" then 'Custom TG2480H'
|
31
|
+
when "ac" then 'Custom TL80'
|
32
|
+
when "ad" then 'Custom TL60'
|
33
|
+
else 'Unknown printer'
|
34
|
+
end
|
35
|
+
"#{model_name}, ROM v#{rom_version}"
|
36
|
+
rescue
|
37
|
+
-1
|
38
|
+
end
|
39
|
+
|
40
|
+
private
|
41
|
+
def send(message, parse_answer = true)
|
42
|
+
@sp.write message
|
43
|
+
ans = @sp.gets
|
44
|
+
if ans
|
45
|
+
parse_answer ? res = ans.unpack("C*").map{|e| e.to_s(16) } : res = ans
|
46
|
+
else
|
47
|
+
res = nil
|
48
|
+
end
|
49
|
+
res.is_a?(Array) ? res[0] : res
|
50
|
+
end
|
51
|
+
|
52
|
+
end
|
53
|
+
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
@@ -0,0 +1,131 @@
|
|
1
|
+
require 'drb'
|
2
|
+
require 'smartware/drivers/cash_acceptor/ccnet'
|
3
|
+
require 'smartware/drivers/cash_acceptor/dummy'
|
4
|
+
|
5
|
+
module Smartware
|
6
|
+
module Interface
|
7
|
+
|
8
|
+
module CashAcceptor
|
9
|
+
|
10
|
+
#
|
11
|
+
# Init vars
|
12
|
+
#
|
13
|
+
@configured = false
|
14
|
+
@no_limit = true
|
15
|
+
@banknotes = {}
|
16
|
+
@commands = %w(monitor)
|
17
|
+
@status = { :cassette => true }
|
18
|
+
|
19
|
+
#
|
20
|
+
# (re)configure device
|
21
|
+
#
|
22
|
+
def self.configure!(port, driver)
|
23
|
+
@device = Smartware::Driver::CashAcceptor.const_get(driver).new(port)
|
24
|
+
@session.kill if @session and @session.alive?
|
25
|
+
@device.cancel_accept
|
26
|
+
@session = self.start_poll!
|
27
|
+
Smartware::Logging.logger.info "Cash acceptor monitor started"
|
28
|
+
@configured = true
|
29
|
+
rescue => e
|
30
|
+
@configured = false
|
31
|
+
Smartware::Logging.logger.error e.message
|
32
|
+
Smartware::Logging.logger.error e.backtrace.join("\n")
|
33
|
+
end
|
34
|
+
|
35
|
+
def self.open_session(limit_min, limit_max)
|
36
|
+
@commands << 'open'
|
37
|
+
@banknotes = {}
|
38
|
+
if !limit_min.nil? && !limit_max.nil?
|
39
|
+
@no_limit = false
|
40
|
+
@limit_min = limit_min
|
41
|
+
@limit_max = limit_max
|
42
|
+
else
|
43
|
+
@no_limit = true
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def self.close_session
|
48
|
+
@commands << 'close'
|
49
|
+
@limit_min = nil
|
50
|
+
@limit_max = nil
|
51
|
+
@no_limit = true
|
52
|
+
end
|
53
|
+
|
54
|
+
def self.configured?
|
55
|
+
@configured
|
56
|
+
end
|
57
|
+
|
58
|
+
def self.status
|
59
|
+
@status
|
60
|
+
end
|
61
|
+
|
62
|
+
def self.model
|
63
|
+
@status[:model]
|
64
|
+
end
|
65
|
+
|
66
|
+
def self.cashsum
|
67
|
+
@banknotes.inject(0){ |result, (key, value)| result + key.to_i*value.to_i }
|
68
|
+
end
|
69
|
+
|
70
|
+
def self.banknotes
|
71
|
+
@banknotes
|
72
|
+
end
|
73
|
+
|
74
|
+
#
|
75
|
+
# Session private method
|
76
|
+
#
|
77
|
+
private
|
78
|
+
def self.start_poll!
|
79
|
+
t = Thread.new do
|
80
|
+
loop do
|
81
|
+
case @commands[0]
|
82
|
+
when 'open'
|
83
|
+
@device.reset
|
84
|
+
@device.accept
|
85
|
+
Smartware::Logging.logger.info "Cash acceptor open: #{@commands}"
|
86
|
+
|
87
|
+
@commands.shift
|
88
|
+
@commands << 'get_money'
|
89
|
+
when 'get_money'
|
90
|
+
res = @device.current_banknote
|
91
|
+
if res.is_a? Integer # Have a banknote
|
92
|
+
if @no_limit or (@limit_min..@limit_max).include?(self.cashsum + res)
|
93
|
+
@device.stack
|
94
|
+
@banknotes[res] = (@banknotes[res]||0) + 1
|
95
|
+
Smartware::Logging.logger.info "Cash acceptor bill stacked, #{res}"
|
96
|
+
else
|
97
|
+
@device.return
|
98
|
+
Smartware::Logging.logger.info "Cash acceptor limit violation, return #{res}"
|
99
|
+
end
|
100
|
+
elsif res.is_a? String # Have a error, errors always as String
|
101
|
+
Smartware::Logging.logger.error "Cash acceptor error #{res}"
|
102
|
+
@status[:error] = res
|
103
|
+
end
|
104
|
+
@device.cancel_accept if !@no_limit && (@limit_max == self.cashsum and @limit_max > 0) # Close cash acceptor if current cashsum equal max-limit
|
105
|
+
|
106
|
+
@commands.shift
|
107
|
+
@commands << 'get_money' if @commands.empty?
|
108
|
+
when 'close'
|
109
|
+
@device.cancel_accept
|
110
|
+
Smartware::Logging.logger.info "Cash acceptor close: #{@commands}"
|
111
|
+
|
112
|
+
@commands.shift
|
113
|
+
when 'monitor'
|
114
|
+
@status[:error] = @device.error
|
115
|
+
@status[:model] = @device.model
|
116
|
+
@status[:cassette] = @device.cassette?
|
117
|
+
@commands.shift
|
118
|
+
else
|
119
|
+
@commands << 'monitor'
|
120
|
+
end
|
121
|
+
sleep 0.5
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
end
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
DRb.start_service('druby://localhost:6001', Smartware::Interface::CashAcceptor)
|
131
|
+
DRb.thread.join
|
@@ -0,0 +1,80 @@
|
|
1
|
+
require 'drb'
|
2
|
+
require 'smartware/drivers/modem/standart'
|
3
|
+
require 'smartware/drivers/modem/dummy'
|
4
|
+
|
5
|
+
module Smartware
|
6
|
+
module Interface
|
7
|
+
|
8
|
+
module Modem
|
9
|
+
|
10
|
+
@configured = false
|
11
|
+
@status = {}
|
12
|
+
|
13
|
+
def self.configure!(port, driver)
|
14
|
+
@device = Smartware::Driver::Modem.const_get(driver).new(port)
|
15
|
+
@session.kill if @session
|
16
|
+
@session = self.poll_status!
|
17
|
+
@configured = true
|
18
|
+
@status = {}
|
19
|
+
rescue => e
|
20
|
+
Smartware::Logging.logger.error e.message
|
21
|
+
Smartware::Logging.logger.error e.backtrace.join("\n")
|
22
|
+
@configured = false
|
23
|
+
end
|
24
|
+
|
25
|
+
def self.configured?
|
26
|
+
@configured
|
27
|
+
end
|
28
|
+
|
29
|
+
def self.error
|
30
|
+
@status[:error] || ''
|
31
|
+
end
|
32
|
+
|
33
|
+
def self.model
|
34
|
+
@status[:model]
|
35
|
+
end
|
36
|
+
|
37
|
+
def self.balance
|
38
|
+
@status[:balance]
|
39
|
+
end
|
40
|
+
|
41
|
+
def self.signal_level
|
42
|
+
@status[:signal_level]
|
43
|
+
end
|
44
|
+
|
45
|
+
def self.d_model
|
46
|
+
@device.model
|
47
|
+
end
|
48
|
+
|
49
|
+
def self.d_signal
|
50
|
+
@device.signal_level
|
51
|
+
end
|
52
|
+
|
53
|
+
def self.d_balance
|
54
|
+
@device.ussd
|
55
|
+
end
|
56
|
+
|
57
|
+
def self.d_stop
|
58
|
+
@session.kill
|
59
|
+
end
|
60
|
+
|
61
|
+
private
|
62
|
+
def self.poll_status!
|
63
|
+
t = Thread.new do
|
64
|
+
loop do
|
65
|
+
@status[:signal_level] = @device.signal_level
|
66
|
+
@status[:model] = @device.model
|
67
|
+
@status[:error] = @device.error
|
68
|
+
@status[:balance] = @device.ussd('*100#')
|
69
|
+
sleep 30
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
DRb.start_service('druby://localhost:6002', Smartware::Interface::Modem)
|
79
|
+
DRb.thread.join
|
80
|
+
|
@@ -0,0 +1,73 @@
|
|
1
|
+
require 'drb'
|
2
|
+
require 'smartware/drivers/printer/dummy'
|
3
|
+
require 'smartware/drivers/printer/tg24xx'
|
4
|
+
|
5
|
+
module Smartware
|
6
|
+
module Interface
|
7
|
+
|
8
|
+
module Printer
|
9
|
+
|
10
|
+
@configured = false
|
11
|
+
@status = {:error => ''}
|
12
|
+
@queue = []
|
13
|
+
|
14
|
+
def self.configure!(port, driver)
|
15
|
+
@device = Smartware::Driver::Printer.const_get(driver).new(port)
|
16
|
+
@session.kill if @session and @session.alive?
|
17
|
+
@session = self.start_monitor!
|
18
|
+
Smartware::Logging.logger.info 'Printer monitor started'
|
19
|
+
@configured = true
|
20
|
+
rescue => e
|
21
|
+
@configured = false
|
22
|
+
end
|
23
|
+
|
24
|
+
def self.configured?
|
25
|
+
@configured
|
26
|
+
end
|
27
|
+
|
28
|
+
def self.error
|
29
|
+
@status[:error]
|
30
|
+
end
|
31
|
+
|
32
|
+
def self.model
|
33
|
+
@status[:model]
|
34
|
+
end
|
35
|
+
|
36
|
+
def self.print(filepath)
|
37
|
+
@queue << filepath
|
38
|
+
end
|
39
|
+
|
40
|
+
def self.test
|
41
|
+
@queue << '/usr/share/cups/data/testprint'
|
42
|
+
end
|
43
|
+
|
44
|
+
private
|
45
|
+
def self.start_monitor!
|
46
|
+
t = Thread.new do
|
47
|
+
loop do
|
48
|
+
if @queue.empty?
|
49
|
+
@status[:error] = @device.error || ''
|
50
|
+
@status[:model] = @device.model
|
51
|
+
else
|
52
|
+
begin
|
53
|
+
`lpr #{@queue[0]} >> #{File.expand_path(Smartware::Logging.logfile)} 2>&1` # Turn lpr-log from STDOUT to smartware
|
54
|
+
Smartware::Logging.logger.info "Printed #{@queue[0]}"
|
55
|
+
@queue.shift
|
56
|
+
sleep 5
|
57
|
+
rescue => e
|
58
|
+
Smartware::Logging.logger.error e.message
|
59
|
+
Smartware::Logging.logger.error e.backtrace.join("\n")
|
60
|
+
end
|
61
|
+
end
|
62
|
+
sleep 0.2
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
DRb.start_service('druby://localhost:6005', Smartware::Interface::Printer)
|
72
|
+
DRb.thread.join
|
73
|
+
|
@@ -0,0 +1,31 @@
|
|
1
|
+
require 'logger'
|
2
|
+
|
3
|
+
module Smartware
|
4
|
+
module Logging
|
5
|
+
|
6
|
+
def self.logdir=(dir)
|
7
|
+
@logdir = dir
|
8
|
+
end
|
9
|
+
|
10
|
+
def self.logger
|
11
|
+
@logger ||= begin
|
12
|
+
log = Logger.new(STDOUT)
|
13
|
+
log.level = Logger::INFO
|
14
|
+
log
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.logger=(val)
|
19
|
+
@logger = (val ? val : Logger.new('/dev/null'))
|
20
|
+
end
|
21
|
+
|
22
|
+
def self.logfile=(val)
|
23
|
+
@logfile = val
|
24
|
+
end
|
25
|
+
|
26
|
+
def self.logfile
|
27
|
+
@logfile
|
28
|
+
end
|
29
|
+
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
require 'thread'
|
2
|
+
require 'smartware/logging'
|
3
|
+
|
4
|
+
module Smartware
|
5
|
+
module Service
|
6
|
+
def self.start
|
7
|
+
$stdout.sync = true
|
8
|
+
|
9
|
+
Smartware::Logging.logger = Logger.new($stdout)
|
10
|
+
Smartware::Logging.logger.info "Smartware started at #{Time.now}"
|
11
|
+
|
12
|
+
@threads = %w(smartware/interfaces/cash_acceptor
|
13
|
+
smartware/interfaces/printer
|
14
|
+
smartware/interfaces/modem).inject([]){|arr, iface| arr << Thread.new{ require iface } }
|
15
|
+
|
16
|
+
@threads.map(&:join)
|
17
|
+
rescue => e
|
18
|
+
Smartware::Logging.logger.fatal e.message
|
19
|
+
Smartware::Logging.logger.fatal e.backtrace.join("\n")
|
20
|
+
end
|
21
|
+
|
22
|
+
def self.stop
|
23
|
+
@threads.map(&:kill)
|
24
|
+
Smartware::Logging.logger.info "Smartware shutdown at #{Time.now}"
|
25
|
+
exit 0
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
data/smartware.gemspec
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'smartware/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |gem|
|
7
|
+
gem.name = "smartware"
|
8
|
+
gem.version = Smartware::VERSION
|
9
|
+
gem.authors = ["Evgeni Sudarchikov", "Boris Staal"]
|
10
|
+
gem.email = ["e.sudarchikov@roundlake.ru", "boris@roundlake.ru"]
|
11
|
+
gem.description = %q{Smartware is the Smartkiosk hardware control daemon}
|
12
|
+
gem.summary = gem.description
|
13
|
+
gem.homepage = ""
|
14
|
+
|
15
|
+
gem.files = `git ls-files`.split($/)
|
16
|
+
gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
|
17
|
+
gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
|
18
|
+
gem.require_paths = ["lib"]
|
19
|
+
|
20
|
+
gem.add_dependency 'serialport'
|
21
|
+
gem.add_dependency 'dante'
|
22
|
+
|
23
|
+
end
|
metadata
ADDED
@@ -0,0 +1,93 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: smartware
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: '0.1'
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Evgeni Sudarchikov
|
9
|
+
- Boris Staal
|
10
|
+
autorequire:
|
11
|
+
bindir: bin
|
12
|
+
cert_chain: []
|
13
|
+
date: 2012-12-17 00:00:00.000000000 Z
|
14
|
+
dependencies:
|
15
|
+
- !ruby/object:Gem::Dependency
|
16
|
+
name: serialport
|
17
|
+
requirement: &70261534685920 !ruby/object:Gem::Requirement
|
18
|
+
none: false
|
19
|
+
requirements:
|
20
|
+
- - ! '>='
|
21
|
+
- !ruby/object:Gem::Version
|
22
|
+
version: '0'
|
23
|
+
type: :runtime
|
24
|
+
prerelease: false
|
25
|
+
version_requirements: *70261534685920
|
26
|
+
- !ruby/object:Gem::Dependency
|
27
|
+
name: dante
|
28
|
+
requirement: &70261534685280 !ruby/object:Gem::Requirement
|
29
|
+
none: false
|
30
|
+
requirements:
|
31
|
+
- - ! '>='
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: *70261534685280
|
37
|
+
description: Smartware is the Smartkiosk hardware control daemon
|
38
|
+
email:
|
39
|
+
- e.sudarchikov@roundlake.ru
|
40
|
+
- boris@roundlake.ru
|
41
|
+
executables:
|
42
|
+
- smartware
|
43
|
+
extensions: []
|
44
|
+
extra_rdoc_files: []
|
45
|
+
files:
|
46
|
+
- .gitignore
|
47
|
+
- Gemfile
|
48
|
+
- LICENSE.txt
|
49
|
+
- README.md
|
50
|
+
- Rakefile
|
51
|
+
- bin/smartware
|
52
|
+
- lib/smartware.rb
|
53
|
+
- lib/smartware/clients/cash_acceptor.rb
|
54
|
+
- lib/smartware/clients/modem.rb
|
55
|
+
- lib/smartware/clients/printer.rb
|
56
|
+
- lib/smartware/drivers/cash_acceptor/ccnet.rb
|
57
|
+
- lib/smartware/drivers/cash_acceptor/dummy.rb
|
58
|
+
- lib/smartware/drivers/modem/dummy.rb
|
59
|
+
- lib/smartware/drivers/modem/standart.rb
|
60
|
+
- lib/smartware/drivers/printer/dummy.rb
|
61
|
+
- lib/smartware/drivers/printer/tg24xx.rb
|
62
|
+
- lib/smartware/interfaces/cash_acceptor.rb
|
63
|
+
- lib/smartware/interfaces/modem.rb
|
64
|
+
- lib/smartware/interfaces/printer.rb
|
65
|
+
- lib/smartware/logging.rb
|
66
|
+
- lib/smartware/service.rb
|
67
|
+
- lib/smartware/version.rb
|
68
|
+
- smartware.gemspec
|
69
|
+
homepage: ''
|
70
|
+
licenses: []
|
71
|
+
post_install_message:
|
72
|
+
rdoc_options: []
|
73
|
+
require_paths:
|
74
|
+
- lib
|
75
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
76
|
+
none: false
|
77
|
+
requirements:
|
78
|
+
- - ! '>='
|
79
|
+
- !ruby/object:Gem::Version
|
80
|
+
version: '0'
|
81
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
82
|
+
none: false
|
83
|
+
requirements:
|
84
|
+
- - ! '>='
|
85
|
+
- !ruby/object:Gem::Version
|
86
|
+
version: '0'
|
87
|
+
requirements: []
|
88
|
+
rubyforge_project:
|
89
|
+
rubygems_version: 1.8.15
|
90
|
+
signing_key:
|
91
|
+
specification_version: 3
|
92
|
+
summary: Smartware is the Smartkiosk hardware control daemon
|
93
|
+
test_files: []
|