tamashii-agent 0.1.11 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/bin/console +4 -4
- data/lib/tamashii/agent/adapter/lcd.rb +22 -0
- data/lib/tamashii/agent/buzzer.rb +6 -5
- data/lib/tamashii/agent/card_reader.rb +16 -10
- data/lib/tamashii/agent/common.rb +0 -5
- data/lib/tamashii/agent/component.rb +29 -27
- data/lib/tamashii/agent/config.rb +4 -0
- data/lib/tamashii/agent/connection.rb +156 -32
- data/lib/tamashii/agent/device/fake_lcd.rb +30 -0
- data/lib/tamashii/agent/device/lcd.rb +86 -0
- data/lib/tamashii/agent/event.rb +27 -0
- data/lib/tamashii/agent/handler/buzzer.rb +2 -2
- data/lib/tamashii/agent/handler/lcd.rb +19 -0
- data/lib/tamashii/agent/handler/remote_response.rb +13 -0
- data/lib/tamashii/agent/handler/system.rb +2 -2
- data/lib/tamashii/agent/handler.rb +2 -1
- data/lib/tamashii/agent/lcd.rb +72 -0
- data/lib/tamashii/agent/master.rb +23 -14
- data/lib/tamashii/agent/version.rb +1 -1
- data/lib/tamashii/agent.rb +0 -1
- data/tamashii-agent.gemspec +4 -1
- metadata +51 -6
- data/lib/tamashii/agent/handler/request_pool_response.rb +0 -14
- data/lib/tamashii/agent/request_pool/request.rb +0 -38
- data/lib/tamashii/agent/request_pool/response.rb +0 -18
- data/lib/tamashii/agent/request_pool.rb +0 -80
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: bfc8a55dfec1ccbc0d4d7f6c26fb1331120b9924
|
4
|
+
data.tar.gz: 5ecc7b988ce09c76058a48c410d1b3d7d5bf1afa
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 8689f7b4ed689a28abe1137bab775e80e78a6ec01f8958c2bce1fe3e0f812a2d06b421deb32ac4337283d719d698a1df41ac72147e8d4ef822aef6f97b2723d3
|
7
|
+
data.tar.gz: 7ba6892db8cd54b5a8eb1333302ee470552a65c8b40f093625fa29b7b800a5a4eac3dc57d3f63d97bf6c1b0988e6245f0720445ab2eef12c76c5b6e5cb716a29
|
data/bin/console
CHANGED
@@ -7,8 +7,8 @@ require "tamashii/agent"
|
|
7
7
|
# with your gem easier. You can also use a different console, if you like.
|
8
8
|
|
9
9
|
# (If you use this, don't forget to add pry to your Gemfile!)
|
10
|
-
|
11
|
-
|
10
|
+
require "pry"
|
11
|
+
Pry.start
|
12
12
|
|
13
|
-
require "irb"
|
14
|
-
IRB.start
|
13
|
+
#require "irb"
|
14
|
+
#IRB.start
|
@@ -0,0 +1,22 @@
|
|
1
|
+
require 'tamashii/agent/adapter/base'
|
2
|
+
require 'tamashii/agent/device/lcd'
|
3
|
+
require 'tamashii/agent/device/fake_lcd'
|
4
|
+
|
5
|
+
module Tamashii
|
6
|
+
module Agent
|
7
|
+
module Adapter
|
8
|
+
# :nodoc:
|
9
|
+
class LCD < Base
|
10
|
+
class << self
|
11
|
+
def real_class
|
12
|
+
Device::LCD
|
13
|
+
end
|
14
|
+
|
15
|
+
def fake_class
|
16
|
+
Device::FakeLCD
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -1,4 +1,5 @@
|
|
1
1
|
require 'tamashii/agent/component'
|
2
|
+
require 'tamashii/agent/event'
|
2
3
|
require 'tamashii/agent/adapter/buzzer'
|
3
4
|
|
4
5
|
module Tamashii
|
@@ -10,11 +11,11 @@ module Tamashii
|
|
10
11
|
logger.debug "Using buzzer instance: #{@buzzer.class}"
|
11
12
|
end
|
12
13
|
|
13
|
-
def process_event(
|
14
|
-
case
|
15
|
-
when
|
16
|
-
logger.debug "Beep: #{
|
17
|
-
case
|
14
|
+
def process_event(event)
|
15
|
+
case event.type
|
16
|
+
when Event::BEEP
|
17
|
+
logger.debug "Beep: #{event.body}"
|
18
|
+
case event.body
|
18
19
|
when "ok"
|
19
20
|
@buzzer.play_ok
|
20
21
|
when "no"
|
@@ -1,6 +1,7 @@
|
|
1
1
|
require 'mfrc522'
|
2
2
|
|
3
3
|
require 'tamashii/agent/component'
|
4
|
+
require 'tamashii/agent/event'
|
4
5
|
require 'tamashii/agent/adapter/card_reader'
|
5
6
|
|
6
7
|
|
@@ -17,19 +18,23 @@ module Tamashii
|
|
17
18
|
# override
|
18
19
|
def worker_loop
|
19
20
|
loop do
|
20
|
-
|
21
|
-
|
21
|
+
if !handle_new_event(true)
|
22
|
+
# no event available
|
23
|
+
sleep 0.1
|
24
|
+
end
|
25
|
+
if handle_card
|
26
|
+
# card is sent, sleep to prevent duplicate sent
|
27
|
+
sleep 1.0
|
28
|
+
else
|
29
|
+
# no card available
|
30
|
+
sleep 0.1
|
31
|
+
end
|
22
32
|
end
|
23
33
|
end
|
24
34
|
|
25
|
-
def handle_io
|
26
|
-
ready = @selector.select(0.1)
|
27
|
-
ready.each { |m| m.value.call } if ready
|
28
|
-
end
|
29
|
-
|
30
35
|
def handle_card
|
31
36
|
# read card
|
32
|
-
return unless @reader.picc_request(MFRC522::PICC_REQA)
|
37
|
+
return false unless @reader.picc_request(MFRC522::PICC_REQA)
|
33
38
|
|
34
39
|
begin
|
35
40
|
uid, sak = @reader.picc_select
|
@@ -40,15 +45,16 @@ module Tamashii
|
|
40
45
|
logger.error "GemError when selecting card: #{e.message}"
|
41
46
|
end
|
42
47
|
@reader.picc_halt
|
48
|
+
true
|
43
49
|
end
|
44
50
|
|
45
51
|
def process_uid(uid)
|
46
52
|
logger.info "New card detected, UID: #{uid}"
|
47
|
-
@master.send_event(
|
53
|
+
@master.send_event(Event.new(Event::CARD_DATA, uid))
|
48
54
|
end
|
49
55
|
|
50
56
|
# override
|
51
|
-
def process_event(
|
57
|
+
def process_event(event)
|
52
58
|
# silent is gold
|
53
59
|
end
|
54
60
|
end
|
@@ -1,5 +1,6 @@
|
|
1
|
-
require 'nio'
|
2
1
|
require 'tamashii/agent/common'
|
2
|
+
require 'tamashii/agent/event'
|
3
|
+
|
3
4
|
|
4
5
|
module Tamashii
|
5
6
|
module Agent
|
@@ -7,27 +8,33 @@ module Tamashii
|
|
7
8
|
include Common::Loggable
|
8
9
|
|
9
10
|
def initialize
|
10
|
-
@
|
11
|
+
@event_queue = Queue.new
|
11
12
|
end
|
12
13
|
|
13
|
-
def send_event(
|
14
|
-
|
15
|
-
@pipe_w.write(str)
|
14
|
+
def send_event(event)
|
15
|
+
@event_queue.push(event)
|
16
16
|
end
|
17
17
|
|
18
|
-
def
|
19
|
-
|
20
|
-
|
21
|
-
|
18
|
+
def check_new_event(non_block = false)
|
19
|
+
@event_queue.pop(non_block)
|
20
|
+
rescue ThreadError => e
|
21
|
+
nil
|
22
|
+
end
|
23
|
+
|
24
|
+
def handle_new_event(non_block = false)
|
25
|
+
if ev = check_new_event(non_block)
|
26
|
+
process_event(ev)
|
27
|
+
end
|
28
|
+
ev
|
22
29
|
end
|
23
30
|
|
24
|
-
def process_event(
|
25
|
-
logger.debug "Got event: #{
|
31
|
+
def process_event(event)
|
32
|
+
logger.debug "Got event: #{event.type}, #{event.body}"
|
26
33
|
end
|
27
34
|
|
28
35
|
# worker
|
29
36
|
def run
|
30
|
-
@
|
37
|
+
@worker_thr = Thread.start { run_worker_loop }
|
31
38
|
end
|
32
39
|
|
33
40
|
def run!
|
@@ -36,36 +43,31 @@ module Tamashii
|
|
36
43
|
|
37
44
|
def stop
|
38
45
|
logger.info "Stopping component"
|
39
|
-
|
40
|
-
@thr = nil
|
46
|
+
stop_threads
|
41
47
|
clean_up
|
42
48
|
end
|
43
49
|
|
50
|
+
def stop_threads
|
51
|
+
@worker_thr.exit if @worker_thr
|
52
|
+
@worker_thr = nil
|
53
|
+
end
|
54
|
+
|
44
55
|
def clean_up
|
45
56
|
end
|
46
57
|
|
47
58
|
def run_worker_loop
|
48
|
-
create_selector
|
49
|
-
register_event_io
|
50
59
|
worker_loop
|
51
60
|
end
|
52
61
|
|
53
62
|
# a default implementation
|
54
63
|
def worker_loop
|
55
64
|
loop do
|
56
|
-
|
57
|
-
|
65
|
+
if !handle_new_event
|
66
|
+
logger.error "Thread error. Worker loop terminated"
|
67
|
+
break
|
68
|
+
end
|
58
69
|
end
|
59
70
|
end
|
60
|
-
|
61
|
-
def register_event_io
|
62
|
-
_monitor = @selector.register(@pipe_r, :r)
|
63
|
-
_monitor.value = method(:receive_event)
|
64
|
-
end
|
65
|
-
|
66
|
-
def create_selector
|
67
|
-
@selector = NIO::Selector.new
|
68
|
-
end
|
69
71
|
end
|
70
72
|
end
|
71
73
|
end
|
@@ -10,6 +10,10 @@ module Tamashii
|
|
10
10
|
register :entry_point, "/tamashii"
|
11
11
|
register :manager_host, "localhost"
|
12
12
|
register :manager_port, 3000
|
13
|
+
register :connection_timeout, 3
|
14
|
+
|
15
|
+
register :lcd_path, '/dev/i2c-1'
|
16
|
+
register :lcd_address, 0x27
|
13
17
|
|
14
18
|
def auth_type(type = nil)
|
15
19
|
return @auth_type ||= :none if type.nil?
|
@@ -2,18 +2,52 @@ require 'socket'
|
|
2
2
|
require 'websocket/driver'
|
3
3
|
require 'aasm'
|
4
4
|
require 'openssl'
|
5
|
+
require 'json'
|
6
|
+
require 'concurrent'
|
7
|
+
require 'nio'
|
5
8
|
|
6
9
|
require 'tamashii/common'
|
7
10
|
|
8
11
|
require 'tamashii/agent/config'
|
12
|
+
require 'tamashii/agent/event'
|
9
13
|
require 'tamashii/agent/component'
|
10
|
-
require 'tamashii/agent/request_pool'
|
11
14
|
|
12
15
|
require 'tamashii/agent/handler'
|
13
16
|
|
14
17
|
module Tamashii
|
15
18
|
module Agent
|
16
19
|
class Connection < Component
|
20
|
+
|
21
|
+
class RequestTimeoutError < RuntimeError; end
|
22
|
+
|
23
|
+
class RequestObserver
|
24
|
+
include Common::Loggable
|
25
|
+
def initialize(connection, id, ev_type, ev_body, future)
|
26
|
+
@connection = connection
|
27
|
+
@id = id
|
28
|
+
@ev_type = ev_type
|
29
|
+
@ev_body = ev_body
|
30
|
+
@future = future
|
31
|
+
end
|
32
|
+
|
33
|
+
def update(time, ev_data, reason)
|
34
|
+
if @future.fulfilled?
|
35
|
+
res_ev_type = ev_data[:ev_type]
|
36
|
+
res_ev_body = ev_data[:ev_body]
|
37
|
+
case res_ev_type
|
38
|
+
when Type::RFID_RESPONSE_JSON
|
39
|
+
logger.debug "Handled: #{res_ev_type}: #{res_ev_body}"
|
40
|
+
@connection.handle_card_result(JSON.parse(res_ev_body))
|
41
|
+
else
|
42
|
+
logger.warn "Unhandled packet result: #{res_ev_type}: #{res_ev_body}"
|
43
|
+
end
|
44
|
+
else
|
45
|
+
logger.error "#{@id} Failed with #{reason}"
|
46
|
+
@connection.on_request_timeout(@ev_type, @ev_body)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
17
51
|
include AASM
|
18
52
|
|
19
53
|
aasm do
|
@@ -41,7 +75,6 @@ module Tamashii
|
|
41
75
|
|
42
76
|
attr_reader :url
|
43
77
|
attr_reader :master
|
44
|
-
attr_reader :request_pool
|
45
78
|
|
46
79
|
def initialize(master, host, port)
|
47
80
|
super()
|
@@ -53,57 +86,70 @@ module Tamashii
|
|
53
86
|
@port = port
|
54
87
|
@tag = 0
|
55
88
|
|
56
|
-
@
|
57
|
-
@
|
58
|
-
@request_pool.set_handler(:request_meet, method(:handle_request_meet))
|
59
|
-
@request_pool.set_handler(:send_request, method(:handle_send_request))
|
89
|
+
@future_ivar_pool = Concurrent::Map.new
|
90
|
+
@driver_lock = Mutex.new
|
60
91
|
|
92
|
+
setup_resolver
|
93
|
+
end
|
94
|
+
|
95
|
+
def create_selector
|
96
|
+
@selector = NIO::Selector.new
|
97
|
+
end
|
98
|
+
|
99
|
+
def setup_resolver
|
61
100
|
env_data = {connection: self}
|
62
101
|
Resolver.config do
|
63
102
|
[Type::REBOOT, Type::POWEROFF, Type::RESTART, Type::UPDATE].each do |type|
|
64
103
|
handle type, Handler::System, env_data
|
65
104
|
end
|
105
|
+
[Type::LCD_MESSAGE, Type::LCD_SET_IDLE_TEXT].each do |type|
|
106
|
+
handle type, Handler::LCD, env_data
|
107
|
+
end
|
66
108
|
handle Type::BUZZER_SOUND, Handler::Buzzer, env_data
|
67
|
-
handle Type::RFID_RESPONSE_JSON, Handler::
|
109
|
+
handle Type::RFID_RESPONSE_JSON, Handler::RemoteResponse, env_data
|
68
110
|
end
|
69
111
|
end
|
70
112
|
|
71
|
-
def
|
72
|
-
@master.send_event(
|
73
|
-
end
|
74
|
-
|
75
|
-
def handle_request_meet(req, res)
|
76
|
-
logger.debug "Got packet: #{res.ev_type}: #{res.ev_body}"
|
77
|
-
case res.ev_type
|
78
|
-
when Type::RFID_RESPONSE_JSON
|
79
|
-
json = JSON.parse(res.ev_body)
|
80
|
-
handle_card_result(json)
|
81
|
-
else
|
82
|
-
logger.warn "Unhandled packet result: #{res.ev_type}: #{res.ev_body}"
|
83
|
-
end
|
113
|
+
def on_request_timeout(ev_type, ev_body)
|
114
|
+
@master.send_event(Event.new(Event::CONNECTION_NOT_READY, "Connection not ready for #{ev_type}:#{ev_body}"))
|
84
115
|
end
|
85
116
|
|
86
117
|
def handle_card_result(result)
|
87
118
|
if result["auth"]
|
88
|
-
@master.send_event(
|
119
|
+
@master.send_event(Event.new(Event::BEEP, "ok"))
|
89
120
|
else
|
90
|
-
@master.send_event(
|
121
|
+
@master.send_event(Event.new(Event::BEEP, "no"))
|
122
|
+
end
|
123
|
+
if result["message"]
|
124
|
+
@master.send_event(Event.new(Event::LCD_MESSAGE, result["message"]))
|
91
125
|
end
|
92
126
|
end
|
93
127
|
|
94
|
-
def
|
128
|
+
def try_send_request(ev_type, ev_body)
|
95
129
|
if self.ready?
|
96
|
-
@
|
130
|
+
@driver_lock.synchronize do
|
131
|
+
@driver.binary(Packet.new(ev_type, @tag, ev_body).dump)
|
132
|
+
end
|
97
133
|
true
|
98
134
|
else
|
99
135
|
false
|
100
136
|
end
|
101
137
|
end
|
102
138
|
|
103
|
-
|
104
|
-
|
139
|
+
def stop_threads
|
140
|
+
super
|
141
|
+
@websocket_thr.exit if @websocket_thr
|
142
|
+
@websocket_thr = nil
|
143
|
+
end
|
144
|
+
|
145
|
+
def run
|
146
|
+
super
|
147
|
+
@websocket_thr = Thread.start { run_websocket_loop }
|
148
|
+
end
|
149
|
+
|
150
|
+
def run_websocket_loop
|
151
|
+
create_selector
|
105
152
|
loop do
|
106
|
-
@request_pool.update
|
107
153
|
ready = @selector.select(1)
|
108
154
|
ready.each { |m| m.value.call } if ready
|
109
155
|
if @io.nil?
|
@@ -218,11 +264,75 @@ module Tamashii
|
|
218
264
|
end
|
219
265
|
end
|
220
266
|
|
221
|
-
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
|
267
|
+
# override
|
268
|
+
def process_event(event)
|
269
|
+
case event.type
|
270
|
+
when Event::CARD_DATA
|
271
|
+
id = event.body
|
272
|
+
wrapped_body = {
|
273
|
+
id: id,
|
274
|
+
ev_body: event.body
|
275
|
+
}.to_json
|
276
|
+
new_remote_request(id, Type::RFID_NUMBER, wrapped_body)
|
277
|
+
end
|
278
|
+
end
|
279
|
+
|
280
|
+
def schedule_task_runner(id, ev_type, ev_body, start_time, times)
|
281
|
+
logger.debug "Schedule send attemp #{id} : #{times + 1} time(s)"
|
282
|
+
if try_send_request(ev_type, ev_body)
|
283
|
+
# Request sent, do nothing
|
284
|
+
logger.debug "Request sent for id = #{id}"
|
285
|
+
else
|
286
|
+
if Time.now - start_time < Config.connection_timeout
|
287
|
+
# Re-schedule self
|
288
|
+
logger.warn "Reschedule #{id} after 1 sec"
|
289
|
+
schedule_next_task(1, id, ev_type, ev_body, start_time, times + 1)
|
290
|
+
else
|
291
|
+
# This job is expired. Do nothing
|
292
|
+
logger.warn "Abort scheduling #{id}"
|
293
|
+
end
|
294
|
+
end
|
295
|
+
end
|
296
|
+
|
297
|
+
def schedule_next_task(interval, id, ev_type, ev_body, start_time, times)
|
298
|
+
Concurrent::ScheduledTask.execute(interval, args: [id, ev_type, ev_body, start_time, times], &method(:schedule_task_runner))
|
299
|
+
end
|
300
|
+
|
301
|
+
def create_request_scheduler_task(id, ev_type, ev_body)
|
302
|
+
schedule_next_task(0, id, ev_type, ev_body, Time.now, 0)
|
303
|
+
end
|
304
|
+
|
305
|
+
def create_request_async(id, ev_type, ev_body)
|
306
|
+
req = Concurrent::Future.new do
|
307
|
+
# Create IVar for store result
|
308
|
+
ivar = Concurrent::IVar.new
|
309
|
+
@future_ivar_pool[id] = ivar
|
310
|
+
# Schedule to get the result
|
311
|
+
create_request_scheduler_task(id, ev_type, ev_body)
|
312
|
+
# Wait for result
|
313
|
+
if result = ivar.value(Config.connection_timeout)
|
314
|
+
# IVar is already removed from pool
|
315
|
+
result
|
316
|
+
else
|
317
|
+
# Manually remove IVar
|
318
|
+
# Any fulfill at this point is useless
|
319
|
+
logger.error "Timeout when getting IVar for #{id}"
|
320
|
+
@future_ivar_pool.delete(id)
|
321
|
+
raise RequestTimeoutError, "Request Timeout"
|
322
|
+
end
|
323
|
+
end
|
324
|
+
req.add_observer(RequestObserver.new(self, id, ev_type, ev_body, req))
|
325
|
+
req.execute
|
326
|
+
req
|
327
|
+
end
|
328
|
+
|
329
|
+
def new_remote_request(id, ev_type, ev_body)
|
330
|
+
# enqueue if not exists
|
331
|
+
if !@future_ivar_pool[id]
|
332
|
+
create_request_async(id, ev_type, ev_body)
|
333
|
+
logger.debug "Request created: #{id}"
|
334
|
+
else
|
335
|
+
logger.warn "Duplicated id: #{id}, ignored"
|
226
336
|
end
|
227
337
|
end
|
228
338
|
|
@@ -235,6 +345,20 @@ module Tamashii
|
|
235
345
|
rescue => e
|
236
346
|
logger.warn "Error occured when clean up: #{e.to_s}"
|
237
347
|
end
|
348
|
+
|
349
|
+
# When data is back
|
350
|
+
def handle_remote_response(ev_type, wrapped_ev_body)
|
351
|
+
logger.debug "Remote packet back: #{ev_type} #{wrapped_ev_body}"
|
352
|
+
result = JSON.parse(wrapped_ev_body)
|
353
|
+
id = result["id"]
|
354
|
+
ev_body = result["ev_body"]
|
355
|
+
# fetch ivar and delete it
|
356
|
+
if ivar = @future_ivar_pool.delete(id)
|
357
|
+
ivar.set(ev_type: ev_type, ev_body: ev_body)
|
358
|
+
else
|
359
|
+
logger.warn "IVar #{id} not in pool"
|
360
|
+
end
|
361
|
+
end
|
238
362
|
end
|
239
363
|
end
|
240
364
|
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
module Tamashii
|
2
|
+
module Agent
|
3
|
+
module Device
|
4
|
+
# :nodoc:
|
5
|
+
class FakeLCD
|
6
|
+
WIDTH = 16
|
7
|
+
|
8
|
+
attr_accessor :backlight
|
9
|
+
|
10
|
+
def initialize
|
11
|
+
@backlight = true
|
12
|
+
end
|
13
|
+
|
14
|
+
def print_message(message)
|
15
|
+
lines = message.lines.map{|l| l.delete("\n")}
|
16
|
+
puts "LCD Display(BACKLIGHT: #{@backlight}):"
|
17
|
+
puts lines.take(2).map { |line| print_line(line) }.join("\n")
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
|
22
|
+
def print_line(message)
|
23
|
+
message = '' unless message
|
24
|
+
message = message.ljust(WIDTH, ' ')
|
25
|
+
message.split('').take(WIDTH).join('')
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,86 @@
|
|
1
|
+
require 'i2c'
|
2
|
+
|
3
|
+
module Tamashii
|
4
|
+
module Agent
|
5
|
+
module Device
|
6
|
+
# :nodoc:
|
7
|
+
class LCD
|
8
|
+
WIDTH = 16
|
9
|
+
|
10
|
+
OP_CHR = 1
|
11
|
+
OP_CMD = 0
|
12
|
+
|
13
|
+
LINES = [
|
14
|
+
0x80,
|
15
|
+
0xC0
|
16
|
+
].freeze
|
17
|
+
|
18
|
+
BACKLIGHT_ON = 0x08
|
19
|
+
BACKLIGHT_OFF = 0x00
|
20
|
+
|
21
|
+
ENABLE = 0b00000100
|
22
|
+
|
23
|
+
PULSE = 0.0005
|
24
|
+
DELAY = 0.0005
|
25
|
+
|
26
|
+
attr_accessor :backlight
|
27
|
+
|
28
|
+
def initialize
|
29
|
+
@lcd = I2C.create(Config.lcd_path)
|
30
|
+
@address = Config.lcd_address
|
31
|
+
@backlight = true
|
32
|
+
|
33
|
+
byte(0x33, OP_CMD)
|
34
|
+
byte(0x32, OP_CMD)
|
35
|
+
byte(0x06, OP_CMD)
|
36
|
+
byte(0x0C, OP_CMD)
|
37
|
+
byte(0x28, OP_CMD)
|
38
|
+
byte(0x01, OP_CMD)
|
39
|
+
sleep(DELAY)
|
40
|
+
end
|
41
|
+
|
42
|
+
def print_message(message)
|
43
|
+
lines = message.lines.map{|l| l.delete("\n")}
|
44
|
+
2.times.each { |line| print_line(lines[line], LINES[line]) }
|
45
|
+
end
|
46
|
+
|
47
|
+
private
|
48
|
+
|
49
|
+
def backlight_mode
|
50
|
+
return BACKLIGHT_ON if @backlight
|
51
|
+
BACKLIGHT_OFF
|
52
|
+
end
|
53
|
+
|
54
|
+
def print_line(message, line)
|
55
|
+
message = '' unless message
|
56
|
+
message = message.ljust(WIDTH, ' ')
|
57
|
+
byte(line, OP_CMD)
|
58
|
+
WIDTH.times.each { |pos| byte(message[pos].ord, OP_CHR) }
|
59
|
+
end
|
60
|
+
|
61
|
+
def write(bits)
|
62
|
+
@lcd.write(@address, bits)
|
63
|
+
end
|
64
|
+
|
65
|
+
def byte(bits, mode)
|
66
|
+
high = mode | (bits & 0xF0) | backlight_mode
|
67
|
+
low = mode | (bits << 4) & 0xF0 | backlight_mode
|
68
|
+
|
69
|
+
write(high)
|
70
|
+
toggle(high)
|
71
|
+
|
72
|
+
write(low)
|
73
|
+
toggle(low)
|
74
|
+
end
|
75
|
+
|
76
|
+
def toggle(bits)
|
77
|
+
sleep(DELAY)
|
78
|
+
write(bits | ENABLE)
|
79
|
+
sleep(PULSE)
|
80
|
+
write(bits & ~ENABLE)
|
81
|
+
sleep(DELAY)
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module Tamashii
|
2
|
+
module Agent
|
3
|
+
class Event
|
4
|
+
|
5
|
+
BEEP = 1
|
6
|
+
SYSTEM_COMMAND = 2
|
7
|
+
AUTH_RESULT = 3
|
8
|
+
CARD_DATA = 4
|
9
|
+
LCD_MESSAGE = 5
|
10
|
+
LCD_SET_IDLE_TEXT = 6
|
11
|
+
|
12
|
+
CONNECTION_NOT_READY = 255
|
13
|
+
|
14
|
+
attr_reader :type, :body
|
15
|
+
|
16
|
+
def initialize(type, body)
|
17
|
+
@type = type
|
18
|
+
@body = body
|
19
|
+
self.freeze
|
20
|
+
end
|
21
|
+
|
22
|
+
def ==(other)
|
23
|
+
@type == other.type && @body == other.body
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -1,4 +1,4 @@
|
|
1
|
-
require 'tamashii/agent/
|
1
|
+
require 'tamashii/agent/event'
|
2
2
|
require 'tamashii/agent/handler/base'
|
3
3
|
|
4
4
|
module Tamashii
|
@@ -6,7 +6,7 @@ module Tamashii
|
|
6
6
|
module Handler
|
7
7
|
class Buzzer < Base
|
8
8
|
def resolve(data)
|
9
|
-
@master.send_event(
|
9
|
+
@master.send_event(Event.new(Event::BEEP, data))
|
10
10
|
end
|
11
11
|
end
|
12
12
|
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
require 'tamashii/agent/event'
|
2
|
+
require 'tamashii/agent/handler/base'
|
3
|
+
|
4
|
+
module Tamashii
|
5
|
+
module Agent
|
6
|
+
module Handler
|
7
|
+
class LCD < Base
|
8
|
+
def resolve(data)
|
9
|
+
case type
|
10
|
+
when Type::LCD_MESSAGE
|
11
|
+
@master.send_event(Event.new(Event::LCD_MESSAGE, data))
|
12
|
+
when Type::LCD_SET_IDLE_TEXT
|
13
|
+
@master.send_event(Event.new(Event::LCD_SET_IDLE_TEXT, data))
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -1,4 +1,4 @@
|
|
1
|
-
require 'tamashii/agent/
|
1
|
+
require 'tamashii/agent/event'
|
2
2
|
require 'tamashii/agent/handler/base'
|
3
3
|
|
4
4
|
module Tamashii
|
@@ -6,7 +6,7 @@ module Tamashii
|
|
6
6
|
module Handler
|
7
7
|
class System < Base
|
8
8
|
def resolve(data)
|
9
|
-
@master.send_event(
|
9
|
+
@master.send_event(Event.new(Event::SYSTEM_COMMAND, type.to_s))
|
10
10
|
end
|
11
11
|
end
|
12
12
|
end
|
@@ -0,0 +1,72 @@
|
|
1
|
+
require 'concurrent'
|
2
|
+
|
3
|
+
require 'tamashii/agent/common'
|
4
|
+
require 'tamashii/agent/event'
|
5
|
+
require 'tamashii/agent/adapter/lcd'
|
6
|
+
|
7
|
+
|
8
|
+
|
9
|
+
module Tamashii
|
10
|
+
module Agent
|
11
|
+
class LCD < Component
|
12
|
+
def initialize
|
13
|
+
super
|
14
|
+
load_lcd_device
|
15
|
+
@device_lock = Mutex.new
|
16
|
+
@idle_message = "[Tamashii]\nIdle..."
|
17
|
+
logger.debug "Using LCD instance: #{@lcd.class}"
|
18
|
+
@lcd.print_message("Initializing\nPlease wait...")
|
19
|
+
schedule_to_print_idle
|
20
|
+
end
|
21
|
+
|
22
|
+
def load_lcd_device
|
23
|
+
@lcd = Adapter::LCD.object
|
24
|
+
rescue => e
|
25
|
+
logger.error "Unable to load LCD instance: #{Adapter::LCD.current_class}"
|
26
|
+
logger.error "Use #{Adapter::LCD.fake_class} instead"
|
27
|
+
@lcd = Adapter::LCD.fake_class.new
|
28
|
+
end
|
29
|
+
|
30
|
+
def schedule_to_print_idle(delay = 5)
|
31
|
+
@back_to_idle_task = Concurrent::ScheduledTask.execute(delay) do
|
32
|
+
@device_lock.synchronize do
|
33
|
+
@lcd.print_message(@idle_message)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def process_event(event)
|
39
|
+
case event.type
|
40
|
+
when Event::LCD_MESSAGE
|
41
|
+
logger.debug "Show message: #{event.body}"
|
42
|
+
@back_to_idle_task&.cancel
|
43
|
+
@device_lock.synchronize do
|
44
|
+
@lcd.print_message(event.body)
|
45
|
+
schedule_to_print_idle
|
46
|
+
end
|
47
|
+
when Event::LCD_SET_IDLE_TEXT
|
48
|
+
logger.debug "Idle text set to #{event.body}"
|
49
|
+
@idle_message = event.body
|
50
|
+
@device_lock.synchronize do
|
51
|
+
@lcd.print_message(event.body)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def clear_screen
|
57
|
+
@device_lock.synchronize do
|
58
|
+
@lcd.print_message("")
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
def clean_up
|
63
|
+
clear_screen
|
64
|
+
super
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
|
71
|
+
|
72
|
+
|
@@ -1,6 +1,9 @@
|
|
1
|
+
require 'tamashii/agent/common'
|
1
2
|
require 'tamashii/agent/connection'
|
3
|
+
require 'tamashii/agent/lcd'
|
2
4
|
require 'tamashii/agent/buzzer'
|
3
5
|
require 'tamashii/agent/card_reader'
|
6
|
+
require 'tamashii/agent/event'
|
4
7
|
|
5
8
|
require 'thread'
|
6
9
|
|
@@ -41,6 +44,7 @@ module Tamashii
|
|
41
44
|
@components = {}
|
42
45
|
@components[:connection] = create_component(Connection, self, @host, @port)
|
43
46
|
@components[:buzzer] = create_component(Buzzer)
|
47
|
+
@components[:lcd] = create_component(LCD)
|
44
48
|
@components[:card_reader] = create_component(CardReader, self)
|
45
49
|
end
|
46
50
|
|
@@ -53,12 +57,12 @@ module Tamashii
|
|
53
57
|
end
|
54
58
|
|
55
59
|
# override
|
56
|
-
def process_event(
|
60
|
+
def process_event(event)
|
57
61
|
super
|
58
|
-
case
|
59
|
-
when
|
60
|
-
logger.info "System command code: #{
|
61
|
-
case
|
62
|
+
case event.type
|
63
|
+
when Event::SYSTEM_COMMAND
|
64
|
+
logger.info "System command code: #{event.body}"
|
65
|
+
case event.body.to_i
|
62
66
|
when Tamashii::Type::REBOOT
|
63
67
|
system_reboot
|
64
68
|
when Tamashii::Type::POWEROFF
|
@@ -68,30 +72,35 @@ module Tamashii
|
|
68
72
|
when Tamashii::Type::UPDATE
|
69
73
|
system_update
|
70
74
|
end
|
71
|
-
when
|
72
|
-
broadcast_event(
|
75
|
+
when Event::CONNECTION_NOT_READY
|
76
|
+
broadcast_event(Event.new(Event::BEEP, "error"))
|
73
77
|
else
|
74
|
-
broadcast_event(
|
78
|
+
broadcast_event(event)
|
75
79
|
end
|
76
80
|
end
|
77
81
|
|
82
|
+
def show_message(message)
|
83
|
+
logger.info message
|
84
|
+
broadcast_event(Event.new(Event::LCD_MESSAGE, message))
|
85
|
+
end
|
86
|
+
|
78
87
|
def system_reboot
|
79
|
-
|
88
|
+
show_message "Rebooting"
|
80
89
|
system("reboot &")
|
81
90
|
end
|
82
91
|
|
83
92
|
def system_poweroff
|
84
|
-
|
93
|
+
show_message "Powering Off"
|
85
94
|
system("poweroff &")
|
86
95
|
end
|
87
96
|
|
88
97
|
def system_restart
|
89
|
-
|
98
|
+
show_message "Restarting"
|
90
99
|
system("systemctl restart tamashii-agent.service &")
|
91
100
|
end
|
92
101
|
|
93
102
|
def system_update
|
94
|
-
|
103
|
+
show_message("Updating")
|
95
104
|
system("gem update tamashii-agent")
|
96
105
|
system_restart
|
97
106
|
end
|
@@ -105,9 +114,9 @@ module Tamashii
|
|
105
114
|
end
|
106
115
|
|
107
116
|
|
108
|
-
def broadcast_event(
|
117
|
+
def broadcast_event(event)
|
109
118
|
@components.each_value do |c|
|
110
|
-
c.send_event(
|
119
|
+
c.send_event(event)
|
111
120
|
end
|
112
121
|
end
|
113
122
|
end
|
data/lib/tamashii/agent.rb
CHANGED
data/tamashii-agent.gemspec
CHANGED
@@ -35,12 +35,15 @@ Gem::Specification.new do |spec|
|
|
35
35
|
spec.add_development_dependency "simplecov"
|
36
36
|
spec.add_development_dependency "guard"
|
37
37
|
spec.add_development_dependency "guard-rspec"
|
38
|
+
spec.add_development_dependency "pry"
|
38
39
|
|
39
40
|
|
40
|
-
spec.add_runtime_dependency "tamashii-common"
|
41
|
+
spec.add_runtime_dependency "tamashii-common"
|
41
42
|
spec.add_runtime_dependency "websocket-driver"
|
42
43
|
spec.add_runtime_dependency "nio4r"
|
43
44
|
spec.add_runtime_dependency "pi_piper"
|
44
45
|
spec.add_runtime_dependency "mfrc522"
|
46
|
+
spec.add_runtime_dependency "i2c"
|
45
47
|
spec.add_runtime_dependency "aasm"
|
48
|
+
spec.add_runtime_dependency "concurrent-ruby"
|
46
49
|
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: tamashii-agent
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- 蒼時弦也
|
@@ -10,7 +10,7 @@ authors:
|
|
10
10
|
autorequire:
|
11
11
|
bindir: exe
|
12
12
|
cert_chain: []
|
13
|
-
date: 2017-
|
13
|
+
date: 2017-07-27 00:00:00.000000000 Z
|
14
14
|
dependencies:
|
15
15
|
- !ruby/object:Gem::Dependency
|
16
16
|
name: bundler
|
@@ -96,6 +96,20 @@ dependencies:
|
|
96
96
|
- - ">="
|
97
97
|
- !ruby/object:Gem::Version
|
98
98
|
version: '0'
|
99
|
+
- !ruby/object:Gem::Dependency
|
100
|
+
name: pry
|
101
|
+
requirement: !ruby/object:Gem::Requirement
|
102
|
+
requirements:
|
103
|
+
- - ">="
|
104
|
+
- !ruby/object:Gem::Version
|
105
|
+
version: '0'
|
106
|
+
type: :development
|
107
|
+
prerelease: false
|
108
|
+
version_requirements: !ruby/object:Gem::Requirement
|
109
|
+
requirements:
|
110
|
+
- - ">="
|
111
|
+
- !ruby/object:Gem::Version
|
112
|
+
version: '0'
|
99
113
|
- !ruby/object:Gem::Dependency
|
100
114
|
name: tamashii-common
|
101
115
|
requirement: !ruby/object:Gem::Requirement
|
@@ -166,6 +180,20 @@ dependencies:
|
|
166
180
|
- - ">="
|
167
181
|
- !ruby/object:Gem::Version
|
168
182
|
version: '0'
|
183
|
+
- !ruby/object:Gem::Dependency
|
184
|
+
name: i2c
|
185
|
+
requirement: !ruby/object:Gem::Requirement
|
186
|
+
requirements:
|
187
|
+
- - ">="
|
188
|
+
- !ruby/object:Gem::Version
|
189
|
+
version: '0'
|
190
|
+
type: :runtime
|
191
|
+
prerelease: false
|
192
|
+
version_requirements: !ruby/object:Gem::Requirement
|
193
|
+
requirements:
|
194
|
+
- - ">="
|
195
|
+
- !ruby/object:Gem::Version
|
196
|
+
version: '0'
|
169
197
|
- !ruby/object:Gem::Dependency
|
170
198
|
name: aasm
|
171
199
|
requirement: !ruby/object:Gem::Requirement
|
@@ -180,6 +208,20 @@ dependencies:
|
|
180
208
|
- - ">="
|
181
209
|
- !ruby/object:Gem::Version
|
182
210
|
version: '0'
|
211
|
+
- !ruby/object:Gem::Dependency
|
212
|
+
name: concurrent-ruby
|
213
|
+
requirement: !ruby/object:Gem::Requirement
|
214
|
+
requirements:
|
215
|
+
- - ">="
|
216
|
+
- !ruby/object:Gem::Version
|
217
|
+
version: '0'
|
218
|
+
type: :runtime
|
219
|
+
prerelease: false
|
220
|
+
version_requirements: !ruby/object:Gem::Requirement
|
221
|
+
requirements:
|
222
|
+
- - ">="
|
223
|
+
- !ruby/object:Gem::Version
|
224
|
+
version: '0'
|
183
225
|
description: The agent module for RubyConfTW checkin system.
|
184
226
|
email:
|
185
227
|
- elct9620@frost.tw
|
@@ -205,6 +247,7 @@ files:
|
|
205
247
|
- lib/tamashii/agent/adapter/base.rb
|
206
248
|
- lib/tamashii/agent/adapter/buzzer.rb
|
207
249
|
- lib/tamashii/agent/adapter/card_reader.rb
|
250
|
+
- lib/tamashii/agent/adapter/lcd.rb
|
208
251
|
- lib/tamashii/agent/buzzer.rb
|
209
252
|
- lib/tamashii/agent/card_reader.rb
|
210
253
|
- lib/tamashii/agent/common.rb
|
@@ -214,16 +257,18 @@ files:
|
|
214
257
|
- lib/tamashii/agent/connection.rb
|
215
258
|
- lib/tamashii/agent/device/fake_buzzer.rb
|
216
259
|
- lib/tamashii/agent/device/fake_card_reader.rb
|
260
|
+
- lib/tamashii/agent/device/fake_lcd.rb
|
261
|
+
- lib/tamashii/agent/device/lcd.rb
|
217
262
|
- lib/tamashii/agent/device/pi_buzzer.rb
|
263
|
+
- lib/tamashii/agent/event.rb
|
218
264
|
- lib/tamashii/agent/handler.rb
|
219
265
|
- lib/tamashii/agent/handler/base.rb
|
220
266
|
- lib/tamashii/agent/handler/buzzer.rb
|
221
|
-
- lib/tamashii/agent/handler/
|
267
|
+
- lib/tamashii/agent/handler/lcd.rb
|
268
|
+
- lib/tamashii/agent/handler/remote_response.rb
|
222
269
|
- lib/tamashii/agent/handler/system.rb
|
270
|
+
- lib/tamashii/agent/lcd.rb
|
223
271
|
- lib/tamashii/agent/master.rb
|
224
|
-
- lib/tamashii/agent/request_pool.rb
|
225
|
-
- lib/tamashii/agent/request_pool/request.rb
|
226
|
-
- lib/tamashii/agent/request_pool/response.rb
|
227
272
|
- lib/tamashii/agent/version.rb
|
228
273
|
- tamashii-agent.gemspec
|
229
274
|
homepage: https://github.com/5xruby/tamashii-agent
|
@@ -1,14 +0,0 @@
|
|
1
|
-
require 'tamashii/agent/handler/base'
|
2
|
-
require 'tamashii/agent/request_pool'
|
3
|
-
|
4
|
-
module Tamashii
|
5
|
-
module Agent
|
6
|
-
module Handler
|
7
|
-
class RequestPoolResponse < Base
|
8
|
-
def resolve(data)
|
9
|
-
@connection.request_pool.add_response(RequestPool::Response.new(self.type, data))
|
10
|
-
end
|
11
|
-
end
|
12
|
-
end
|
13
|
-
end
|
14
|
-
end
|
@@ -1,38 +0,0 @@
|
|
1
|
-
require 'json'
|
2
|
-
module Tamashii
|
3
|
-
module Agent
|
4
|
-
class RequestPool
|
5
|
-
class Request
|
6
|
-
attr_accessor :id
|
7
|
-
attr_accessor :ev_type
|
8
|
-
attr_accessor :ev_body
|
9
|
-
attr_accessor :state
|
10
|
-
|
11
|
-
STATE_PENDING = :pending
|
12
|
-
STATE_SENT = :sent
|
13
|
-
|
14
|
-
def initialize(ev_type, ev_body, id)
|
15
|
-
@ev_type = ev_type
|
16
|
-
@ev_body = ev_body
|
17
|
-
@id = id
|
18
|
-
@state = STATE_PENDING
|
19
|
-
end
|
20
|
-
|
21
|
-
def wrap_body
|
22
|
-
{
|
23
|
-
id: @id,
|
24
|
-
ev_body: @ev_body
|
25
|
-
}.to_json
|
26
|
-
end
|
27
|
-
|
28
|
-
def sent!
|
29
|
-
@state = STATE_SENT
|
30
|
-
end
|
31
|
-
|
32
|
-
def sent?
|
33
|
-
@state == STATE_SENT
|
34
|
-
end
|
35
|
-
end
|
36
|
-
end
|
37
|
-
end
|
38
|
-
end
|
@@ -1,18 +0,0 @@
|
|
1
|
-
require 'json'
|
2
|
-
module Tamashii
|
3
|
-
module Agent
|
4
|
-
class RequestPool
|
5
|
-
class Response
|
6
|
-
attr_accessor :ev_type, :ev_body, :id
|
7
|
-
|
8
|
-
def initialize(ev_type, wrapped_body)
|
9
|
-
@ev_type = ev_type
|
10
|
-
data = JSON.parse(wrapped_body)
|
11
|
-
@id = data["id"]
|
12
|
-
@ev_body = data["ev_body"]
|
13
|
-
end
|
14
|
-
|
15
|
-
end
|
16
|
-
end
|
17
|
-
end
|
18
|
-
end
|
@@ -1,80 +0,0 @@
|
|
1
|
-
require 'tamashii/agent/common'
|
2
|
-
require 'tamashii/agent/request_pool/request'
|
3
|
-
require 'tamashii/agent/request_pool/response'
|
4
|
-
|
5
|
-
|
6
|
-
module Tamashii
|
7
|
-
module Agent
|
8
|
-
class RequestPool
|
9
|
-
include Common::Loggable
|
10
|
-
def initialize
|
11
|
-
@pool = {}
|
12
|
-
@handlers = {}
|
13
|
-
end
|
14
|
-
|
15
|
-
def set_handler(sym, method)
|
16
|
-
@handlers[sym] = method
|
17
|
-
end
|
18
|
-
|
19
|
-
def call_handler(sym, *args)
|
20
|
-
if handle?(sym)
|
21
|
-
@handlers[sym].call(*args)
|
22
|
-
else
|
23
|
-
logger.warn "WARN: un-handled event: #{sym}"
|
24
|
-
end
|
25
|
-
end
|
26
|
-
|
27
|
-
def handle?(sym)
|
28
|
-
@handlers.has_key? sym
|
29
|
-
end
|
30
|
-
|
31
|
-
def add_request(req, timedout = 3)
|
32
|
-
@pool[req.id] = {req: req, timestamp: Time.now, timedout: timedout}
|
33
|
-
try_send_request(req)
|
34
|
-
end
|
35
|
-
|
36
|
-
def add_response(res)
|
37
|
-
# find the same id
|
38
|
-
req_data = @pool[res.id]
|
39
|
-
if req_data
|
40
|
-
@pool.delete(res.id)
|
41
|
-
call_handler(:request_meet, req_data[:req], res)
|
42
|
-
else
|
43
|
-
# unmatched response
|
44
|
-
# discard
|
45
|
-
logger.warn "WARN: un-matched response (id=#{res.id}): #{res.inspect}"
|
46
|
-
end
|
47
|
-
end
|
48
|
-
|
49
|
-
def update
|
50
|
-
process_pending
|
51
|
-
check_timedout
|
52
|
-
end
|
53
|
-
|
54
|
-
def check_timedout
|
55
|
-
now = Time.now
|
56
|
-
@pool.each do |id, req_data|
|
57
|
-
if now - req_data[:timestamp] >= req_data[:timedout]
|
58
|
-
# timedout
|
59
|
-
@pool.delete(id)
|
60
|
-
call_handler(:request_timedout, req_data[:req])
|
61
|
-
end
|
62
|
-
end
|
63
|
-
end
|
64
|
-
|
65
|
-
def process_pending
|
66
|
-
@pool.each_value do |data|
|
67
|
-
try_send_request(data[:req]) unless data[:req].sent?
|
68
|
-
end
|
69
|
-
end
|
70
|
-
|
71
|
-
def try_send_request(req)
|
72
|
-
if handle?(:send_request)
|
73
|
-
req.sent! if call_handler(:send_request, req)
|
74
|
-
end
|
75
|
-
end
|
76
|
-
end
|
77
|
-
end
|
78
|
-
end
|
79
|
-
|
80
|
-
|