tochka 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,470 @@
1
+ module Tochka
2
+ require "sdl"
3
+ require "socket"
4
+ require "pidfile"
5
+
6
+ class TochkaMiniUI
7
+ DEBUG=true
8
+
9
+ DEFAULT_FONT_PATH="/usr/share/fonts/truetype/freefont/FreeSansBold.ttf"
10
+ DEFAULT_LOG_FILE="/var/log/tochka-miniui.log"
11
+
12
+ WIDTH=320
13
+ HEIGHT=240
14
+ COLOR=24
15
+
16
+ CAPTION="TochikaMini"
17
+
18
+ COLOR_RED=:red
19
+ COLOR_GREEN=:green
20
+ COLOR_BLUE=:blue
21
+ COLOR_WHITE=:white
22
+ COLOR_BLACK=:black
23
+
24
+ LEVEL1_BASE_X = 15
25
+ LEVEL1_BASE_Y = 5
26
+ LEVEL1_BASE_X_C2 = 160
27
+
28
+ VAR_OFFSET_X = 2
29
+ VAR_OFFSET_Y = 14
30
+
31
+ OFFSET_X = 110
32
+ OFFSET_Y = 35
33
+ LEVEL2_BASE_X = LEVEL1_BASE_X
34
+ LEVEL2_BASE_Y = LEVEL1_BASE_Y + OFFSET_Y
35
+
36
+ LEVEL3_BASE_X = LEVEL1_BASE_X
37
+ LEVEL3_BASE_Y = 190
38
+
39
+ LEVEL4_BASE_X = 0
40
+ LEVEL4_BASE_Y = HEIGHT-VAR_OFFSET_Y
41
+
42
+ def self.default_options
43
+ return {
44
+ :font_path => DEFAULT_FONT_PATH,
45
+ :log_file => DEFAULT_LOG_FILE,
46
+ }
47
+ end
48
+
49
+ def initialize font_path
50
+ @font_path = font_path
51
+ check_priviledge() # need to be root
52
+ init_sdl()
53
+ init_var()
54
+
55
+ begin
56
+ @pf = PidFile.new(:piddir=> "/var/run", :pidfile => "tochka-miniui.pid")
57
+ rescue => e
58
+ $log.err("pid file is in trouble (#{e})")
59
+ end
60
+ end
61
+
62
+ def draw_lattice
63
+ @screen.draw_line(9, 35, 306 + 9, 35, @colors[COLOR_WHITE], true)
64
+ @screen.draw_line(9, 185, 306 + 9, 185, @colors[COLOR_WHITE], true)
65
+ @screen.draw_line(9, 225, 306 + 9, 225, @colors[COLOR_WHITE], true)
66
+ end
67
+
68
+ def draw_base_text
69
+ draw_base_text_level1
70
+ draw_base_text_level2
71
+ draw_base_text_level3
72
+ draw_base_text_level4
73
+ end
74
+
75
+ def draw_text level, idx, text, color=COLOR_WHITE
76
+ @font.draw_solid_utf8(@screen, text.to_s, *get_text_pos(level, idx),
77
+ 255, 255, 255)
78
+ end
79
+
80
+ def draw_base_text_level1
81
+ draw_text(1, 0, "Date")
82
+ draw_text(1, 1, "Host Name")
83
+ end
84
+
85
+ def draw_base_text_level3
86
+ draw_text(3, 0, "Disk Usage")
87
+ draw_text(3, 1, "Memory Usage")
88
+ draw_text(3, 2, "CPU Usage")
89
+ end
90
+
91
+ def draw_base_text_level2
92
+ draw_text(2, 0, "State")
93
+ draw_text(2, 1, "File Name")
94
+ draw_text(2, 2, "File Size")
95
+ draw_text(2, 3, "Duration")
96
+ draw_text(2, 4, "Current Channel")
97
+ draw_text(2, 5, "Channel Walk")
98
+ draw_text(2, 6, "Size per sec")
99
+ draw_text(2, 7, "Utilization")
100
+ end
101
+
102
+ def draw_base_text_level4
103
+ @font.draw_solid_utf8(@screen, "START | STOP | MODE1 | MODE2",
104
+ LEVEL4_BASE_X, LEVEL4_BASE_Y, 255, 255, 255)
105
+ @font.draw_solid_utf8(@screen, "stop count => #{@stop_count}",
106
+ LEVEL4_BASE_X + 200, LEVEL4_BASE_Y, 255, 0, 0)
107
+ end
108
+
109
+ def get_level1_pos idx
110
+ # 0 1
111
+ offset_x = LEVEL1_BASE_X
112
+ offset_y = 0
113
+ if idx % 2 == 1
114
+ offset_x = 160
115
+ end
116
+ offset_y = LEVEL1_BASE_Y
117
+ return [offset_x, offset_y]
118
+ end
119
+
120
+ def get_level2_pos idx
121
+ # 0 1
122
+ # 2 3
123
+ # 4 5
124
+ # 6 7
125
+ offset_x = LEVEL1_BASE_X
126
+ offset_y = 0
127
+ if idx % 2 == 1
128
+ offset_x += OFFSET_X
129
+ end
130
+ offset_y = LEVEL2_BASE_Y + OFFSET_Y * (idx / 2)
131
+ return [offset_x, offset_y]
132
+ end
133
+
134
+ def get_level3_pos idx
135
+ # 0 1 2
136
+ offset_x = LEVEL3_BASE_X + 100 * (idx % 3)
137
+ offset_y = LEVEL3_BASE_Y
138
+ return [offset_x, offset_y]
139
+ end
140
+
141
+ def get_text_pos level, idx
142
+ case level
143
+ when 1
144
+ return get_level1_pos(idx)
145
+ when 2
146
+ return get_level2_pos(idx)
147
+ when 3
148
+ return get_level3_pos(idx)
149
+ else
150
+ raise "idx #{idx} is invalid"
151
+ end
152
+ end
153
+
154
+ def get_level1_var_pos idx
155
+ x, y = get_level1_pos(idx)
156
+ return [x + VAR_OFFSET_X, y + VAR_OFFSET_Y]
157
+ end
158
+
159
+ def get_level2_var_pos idx
160
+ x, y = get_level2_pos(idx)
161
+ return [x + VAR_OFFSET_X, y + VAR_OFFSET_Y]
162
+ end
163
+
164
+ def get_level3_var_pos idx
165
+ x, y = get_level3_pos(idx)
166
+ return [x + VAR_OFFSET_X, y + VAR_OFFSET_Y]
167
+ end
168
+
169
+ def get_var_pos level, idx
170
+ case level
171
+ when 1
172
+ return get_level1_var_pos(idx)
173
+ when 2
174
+ return get_level2_var_pos(idx)
175
+ when 3
176
+ return get_level3_var_pos(idx)
177
+ else
178
+ raise "idx #{idx} is invalid"
179
+ end
180
+ end
181
+
182
+ def draw_var level, idx, var, color=COLOR_GREEN
183
+ @font.draw_solid_utf8(@screen, var.to_s, *get_var_pos(level, idx),
184
+ 0, 255, 0)
185
+ end
186
+
187
+ def draw_all
188
+ #back ground
189
+ @screen.fill_rect(0, 0, WIDTH, HEIGHT, @colors[COLOR_BLACK])
190
+
191
+ draw_lattice
192
+ draw_base_text
193
+
194
+ # update text
195
+ draw_var(1, 0, @date)
196
+ draw_var(1, 1, @hostname)
197
+
198
+ draw_var(2, 0, @state)
199
+ draw_var(2, 1, @file_name)
200
+ draw_var(2, 2, file_size_to_h(@file_size))
201
+ draw_var(2, 3, duration_to_h(@duration))
202
+ draw_var(2, 4, @current_channel)
203
+ draw_var(2, 5, @channel_walk)
204
+ draw_var(2, 6, file_size_to_h(@size_per_sec))
205
+ draw_var(2, 7, "#{@utilization}% (#{@utilization_channel}ch)")
206
+
207
+ draw_var(3, 0, "#{@disk_usage}GB (#{@disk_usage_perc}%)")
208
+ draw_var(3, 1, "#{@mem_usage}MB (#{@mem_usage_perc}%)")
209
+ draw_var(3, 2, "#{@cpu_usage_perc}%")
210
+ end
211
+
212
+ def run
213
+ draw_all
214
+ # background
215
+ @screen.fill_rect(0, 0, WIDTH, HEIGHT, @colors[COLOR_BLACK])
216
+ draw_lattice
217
+ draw_base_text
218
+ prev = Time.now.to_i
219
+
220
+ while true
221
+ sleep 0.05
222
+
223
+ while event = SDL::Event.poll
224
+ end
225
+
226
+ buttons = @pitft_button.button_all_edge()
227
+ if buttons[0] == true
228
+ @ca.start_capture()
229
+ elsif buttons[1] == true
230
+ case @stop_count
231
+ when 0
232
+ $log.info("TOCHKA-UI: stop stage 1")
233
+ @stop_count = 1
234
+ when 3
235
+ $log.info("TOCHKA-UI: stop stage done, activate halt")
236
+ @ca.stop_capture
237
+ @stop_count = 0
238
+ else
239
+ @stop_count = 0
240
+ end
241
+ elsif buttons[2] == true
242
+ case @stop_count
243
+ when 1
244
+ $log.info("TOCHKA-UI: stop stage 2")
245
+ @stop_count = 2
246
+ else
247
+ @stop_count = 0
248
+ @pitft_button.backlight_off()
249
+ end
250
+ elsif buttons[3] == true
251
+ case @stop_count
252
+ when 2
253
+ $log.info("TOCHKA-UI: stop stage 3")
254
+ @stop_count = 3
255
+ else
256
+ @stop_count = 0
257
+ @pitft_button.backlight_on()
258
+ end
259
+ end
260
+
261
+ now = Time.now.to_i
262
+ next if prev == now
263
+ prev = now
264
+
265
+ update_var
266
+
267
+ draw_all
268
+ @screen.update_rect(0, 0, 0, 0)
269
+ end
270
+ end
271
+
272
+ private
273
+ def check_priviledge
274
+ return false if DEBUG
275
+ raise "Must run as root" unless Process.uid == 0
276
+ return true
277
+ end
278
+
279
+ def init_sdl
280
+ SDL.putenv("SDL_VIDEODRIVER=fbdev")
281
+ SDL.putenv("SDL_FBDEV=/dev/fb1")
282
+ SDL.putenv("SDL_MOUSEDEV=/dev/input/touchscreen")
283
+ SDL.putenv("SDL_MOUSEDRV=TSLIB")
284
+
285
+
286
+ SDL.init(SDL::INIT_VIDEO)
287
+ @screen = SDL::Screen.open(WIDTH, HEIGHT, COLOR, SDL::SWSURFACE)
288
+ @surface = SDL::Surface.new(SDL::SWSURFACE, WIDTH, HEIGHT, @screen)
289
+
290
+ @colors = {
291
+ COLOR_RED => @screen.format.map_rgb(255, 0, 0),
292
+ COLOR_GREEN => @screen.format.map_rgb(0, 255, 0),
293
+ COLOR_BLUE => @screen.format.map_rgb(0, 0, 255),
294
+ COLOR_BLACK => @screen.format.map_rgb(0, 0, 0),
295
+ COLOR_WHITE => @screen.format.map_rgb(255, 255, 255),
296
+ }
297
+
298
+ SDL::WM::set_caption CAPTION, CAPTION
299
+
300
+ SDL::TTF::init
301
+ @font = SDL::TTF::open(@font_path, 12)
302
+
303
+ @pitft_button = PiTFTButton.new
304
+ @stop_count = 0
305
+ end
306
+
307
+ def init_var
308
+ @ca = Tochka::Agent.new
309
+
310
+ @date = get_date_str
311
+ @hostname = get_hostname
312
+ @disk_usage = 0
313
+ @disk_usage_perc = 0
314
+ @mem_usage = 0
315
+ @mem_usage_perc = 0
316
+ @cpu_usage_perc = 0
317
+ @size_per_sec = 0
318
+ @captured_pid = -1
319
+
320
+ # from agent
321
+ @state = @ca.state
322
+ @file_name = @ca.file_name
323
+ @file_size = @ca.file_size
324
+ @duration = @ca.duration
325
+ @current_channel = @ca.current_channel
326
+ @channel_walk = @ca.channel_walk
327
+ @frame_count = @ca.frame_count
328
+ @utilization = @ca.utilization
329
+ @utilization_channel = @ca.utilization_channel
330
+
331
+ @last_update_time = Time.now.to_i
332
+ @last_full_update_time = Time.now.to_i
333
+ end
334
+
335
+ def update_var
336
+ now = Time.now.to_i
337
+ if now == @last_update_time # too soon
338
+ return
339
+ end
340
+
341
+ # light update
342
+ @date = get_date_str
343
+ @last_update_time = now
344
+
345
+ if now < (@last_full_update_time + 1)
346
+ return
347
+ end
348
+
349
+ # heavy update
350
+ update_var_host
351
+ update_var_agent
352
+ @last_full_update_time = now
353
+ end
354
+
355
+ def update_var_host
356
+ @hostname = get_hostname
357
+ # disk
358
+ @disk_usage, @disk_usage_perc = get_disk_usage()
359
+ @mem_usage, @mem_usage_perc = get_mem_usage()
360
+ @cpu_usage_perc = get_cpu_usage()
361
+ end
362
+
363
+ def update_var_agent
364
+ unless @ca.get_status
365
+ $log.err("TOCHKA-UI: upsate failed")
366
+ end
367
+ @state = @ca.state
368
+ @file_name = @ca.file_name
369
+ @file_size = @ca.file_size
370
+ @duration = @ca.duration
371
+ @current_channel = @ca.current_channel
372
+ @channel_walk = @ca.channel_walk
373
+ @frame_count = @ca.frame_count
374
+ @size_per_sec = @file_size / @duration if @duration != 0
375
+ @utilization = @ca.utilization
376
+ @utilization_channel = @ca.utilization_channel
377
+ end
378
+
379
+ def get_date_str
380
+ Time.now.strftime("%Y/%m/%d %H:%M:%S")
381
+ end
382
+
383
+ def get_hostname
384
+ Socket.gethostname
385
+ end
386
+
387
+ def get_mem_usage
388
+ total = 0
389
+ free = 0
390
+ File.open("/proc/meminfo") do |file|
391
+ total = file.gets.split[1].to_i
392
+ free = file.gets.split[1].to_i
393
+ end
394
+
395
+ used = total - free
396
+ used_perc = used * 100 / total
397
+ return [used/1000, used_perc]
398
+ end
399
+
400
+ def get_cpu_usage
401
+ current = []
402
+ File.open("/proc/stat") do |file|
403
+ current = file.gets.split[1..4].map{|elm| elm.to_i}
404
+ end
405
+ if @prev_cpu == nil
406
+ @prev_cpu = current
407
+ return 0
408
+ end
409
+
410
+ usage_sub = current[0..2].inject(0){|sum, elm| sum += elm} -
411
+ @prev_cpu[0..2].inject(0){|sum, elm| sum += elm}
412
+ total_sub = current.inject(0){|sum, elm| sum += elm} -
413
+ @prev_cpu.inject(0){|sum, elm| sum += elm}
414
+
415
+ @prev_cpu = current
416
+ return ((usage_sub * 100) / total_sub)
417
+ end
418
+
419
+ def get_disk_usage
420
+ line = `df -h`.split("\n")[1].split
421
+ used_str = line[2]
422
+ perc_str = line[4]
423
+
424
+ match = used_str.match(/^([1-9\.]+)([GMK])(i|)$/)
425
+ unless match
426
+ raise "failed to get disk usage"
427
+ end
428
+
429
+ used = match[1].to_f
430
+ used = used / 1000 if match[2] == "M"
431
+ used = used / 1000 / 1000 if match[2] == "K"
432
+
433
+ perc = perc_str.to_i
434
+
435
+ return [used, perc]
436
+ rescue => e
437
+ $log.err("TOCHKA-UI: failed to acquire disk usage (#{e})")
438
+ return [0, 0]
439
+ end
440
+
441
+ def file_size_to_h bytes
442
+ return "0 B" if bytes == 0
443
+ kb = (bytes.to_f / 1000)
444
+ mb = kb.to_f / 1000
445
+ if mb < 1.0
446
+ return "#{(kb * 100).to_i.to_f/100} KB"
447
+ end
448
+ return "#{(mb * 100).to_i.to_f/100} MB"
449
+ end
450
+
451
+ def duration_to_h sec
452
+ return "0s" if sec == 0
453
+ time = sec
454
+ d_sec = sec % 60
455
+ time = time / 60
456
+ d_min = time % 60
457
+ time = time / 60
458
+ d_hour = time % 24
459
+ d_day = time / 24
460
+
461
+ str = ""
462
+ str += "#{d_day}d " if d_day > 0
463
+ str += "#{d_hour}h " if d_hour > 0
464
+ str += "#{d_min}m " if d_min > 0
465
+ str += "#{d_sec}s" if d_sec > 0
466
+
467
+ return str
468
+ end
469
+ end
470
+ end
@@ -0,0 +1,209 @@
1
+ module Tochka
2
+ require "socket"
3
+ require "json"
4
+ require "thread"
5
+ require "pidfile"
6
+
7
+ require "tochka/channel"
8
+ require "tochka/wlan"
9
+ require "tochka/log"
10
+
11
+ class Daemon
12
+ DEFAULT_CAP_PATH="/cap"
13
+ DEFAULT_IFNAME="wlan0"
14
+ DEFAULT_LOG_FILE="/var/log/tochkad.log"
15
+
16
+ CMD_GET_STATUS="get_status"
17
+ CMD_START_CAPTURE="start_capture"
18
+ CMD_STOP_CAPTURE="stop_capture"
19
+
20
+ STATE_INIT="init"
21
+ STATE_RUNNING="running"
22
+ STATE_STOP="stop"
23
+
24
+ def self.default_options
25
+ return {
26
+ :ifname => DEFAULT_IFNAME,
27
+ :cap_path => DEFAULT_CAP_PATH,
28
+ :log_file => DEFAULT_LOG_FILE,
29
+ }
30
+ end
31
+
32
+ def initialize ifname=DEFAULT_IFNAME, cap_path=DEFAULT_CAP_PATH
33
+ @cap_path = cap_path || DEFAULT_CAP_PATH
34
+ @ifname = ifname || DEFAULT_IFNAME
35
+
36
+ check_requirements()
37
+ init_status()
38
+
39
+ @wlan = Tochka::Wlan.new(@ifname)
40
+
41
+ @th_capture = nil
42
+ @event_q = Queue.new
43
+ @start_time = 0
44
+
45
+ @mutex = Mutex.new
46
+ @cv = ConditionVariable.new
47
+
48
+ begin
49
+ @pf = PidFile.new(:piddir => "/var/run", :pidfile => "tochkad.pid")
50
+ rescue => e
51
+ $log.err("pid file is in trouble (#{e})")
52
+ end
53
+ end
54
+
55
+ def run
56
+ # start various connection
57
+ @unix_sock = Tochka::UnixSocketChannel.new(Proc.new {|msg|
58
+ recv_handler(msg)
59
+ })
60
+ @unix_sock.start
61
+
62
+ loop do
63
+ @mutex.synchronize {
64
+ @cv.wait(@mutex) if @event_q.empty?
65
+ event = @event_q.pop
66
+ $log.debug("received event (#{event})")
67
+ handle_event(event)
68
+ }
69
+ end
70
+ end
71
+
72
+ def check_requirements
73
+ # root privilege
74
+ # tshark exists?
75
+ end
76
+
77
+ def init_status new_state=STATE_INIT
78
+ @state = new_state
79
+ @file_name = ""
80
+ end
81
+
82
+ def recv_handler msg
83
+ json = JSON.parse(msg)
84
+
85
+ resp = {}
86
+
87
+ case json["command"]
88
+ when CMD_GET_STATUS
89
+ resp = recv_get_status()
90
+ when CMD_START_CAPTURE
91
+ resp = recv_start_capture()
92
+ when CMD_STOP_CAPTURE
93
+ resp = recv_stop_capture()
94
+ else
95
+ $log.err("discarded unknown command (req='#{json['command']}')")
96
+ resp = {"error" => "unknown command"}
97
+ end
98
+
99
+ return JSON.dump(resp)
100
+ rescue => e
101
+ $log.err("recv_handler has unknown error (#{e})")
102
+ end
103
+
104
+ def recv_get_status
105
+ $log.debug("accepted command (get_status)")
106
+ return status_hash()
107
+ end
108
+
109
+ def recv_start_capture
110
+ $log.debug("accepted command (start_capture")
111
+
112
+ @mutex.synchronize {
113
+ @event_q.push(CMD_START_CAPTURE)
114
+ @cv.signal if @cv
115
+ $log.debug("requested defered start_capture")
116
+ }
117
+
118
+ return {"status" => "start capture enqueued"}
119
+ end
120
+
121
+ def recv_stop_capture
122
+ $log.debug("accepted command (stop_capture")
123
+
124
+ @mutex.synchronize {
125
+ @event_q.push(CMD_STOP_CAPTURE)
126
+ @cv.signal if @cv
127
+ $log.debug("requested defered stop_capture")
128
+ }
129
+
130
+ return {"status" => "stop capture enqueued"}
131
+ end
132
+
133
+ def status_hash
134
+ return {
135
+ "state" => @state,
136
+ "file_name" => @file_name,
137
+ "file_size" => @wlan.file_size,
138
+ "duration" => @wlan.duration,
139
+ "current_channel" => @wlan.current_channel,
140
+ "channel_walk" => @wlan.channel_walk,
141
+ "utilization" => @wlan.utilization,
142
+ "utilization_channel" => @wlan.utilization_channel,
143
+ }
144
+ end
145
+
146
+ def handle_event event
147
+ $log.debug("invoke defered event handler (event => #{event})")
148
+ case event
149
+ when CMD_START_CAPTURE
150
+ start_capture
151
+ when CMD_STOP_CAPTURE
152
+ stop_capture
153
+ else
154
+ $log.err("defered handler has unknown event (#{event})")
155
+ end
156
+ rescue => e
157
+ $log.err("defered handler detected unknown error (#{e})")
158
+ end
159
+
160
+ def start_capture
161
+ if @state == STATE_RUNNING and @th_capture
162
+ $log.info("discarded start_capture (already initiated)")
163
+ return # do nothing
164
+ end
165
+ init_status() # refresh
166
+
167
+ @state = STATE_RUNNING
168
+ do_start_capture()
169
+ return
170
+ end
171
+
172
+ def stop_capture
173
+ if @state != STATE_RUNNING
174
+ $log.info("discarded stop_capture (not running)")
175
+ return
176
+ end
177
+
178
+ do_stop_capture()
179
+ @state = STATE_STOP
180
+ return
181
+ end
182
+
183
+ def do_start_capture
184
+ @file_name = generate_new_filename()
185
+
186
+ $log.debug("invoke capture thread (file=#{@file_name})")
187
+ @th_capture = Thread.new do
188
+ file_path = "#{@cap_path}/#{@file_name}"
189
+ @wlan.run_capture(file_path) # block until stopped
190
+ end
191
+ end
192
+
193
+ def do_stop_capture
194
+ @wlan.stop_capture
195
+
196
+ $log.debug("kill capture thread (#{@th_capture})")
197
+ @th_capture.kill if @th_capture
198
+ end
199
+
200
+ def generate_new_filename()
201
+ return "#{Time.now.strftime("%Y%m%d%H%m%S")}_#{@ifname}_#{$$}.pcapng"
202
+ end
203
+
204
+ def move_channel current
205
+ # this is shit
206
+ return (current + 1) % 13 + 1
207
+ end
208
+ end
209
+ end
data/lib/tochka/log.rb ADDED
@@ -0,0 +1,38 @@
1
+ class Log
2
+ require "logger"
3
+ def initialize opts={}
4
+ @debug_mode = opts[:debug_mode] || false
5
+ @output = opts[:output] || STDOUT
6
+
7
+ case @output
8
+ when "STDOUT"
9
+ @output = STDOUT
10
+ when "STDERR"
11
+ @output = STDERR
12
+ end
13
+ @logger = Logger.new(@output)
14
+
15
+ @logger.datetime_format = "%Y%m%d%H%m%S"
16
+ @logger.formatter = proc { |severity, datetime, progname, msg|
17
+ "[#{datetime}] #{progname}\t#{severity}: #{msg}\n"
18
+ }
19
+ end
20
+
21
+ def warn str
22
+ @logger.err(str)
23
+ end
24
+
25
+ def err str
26
+ @logger.error(str)
27
+ end
28
+
29
+ def info str
30
+ @logger.info(str)
31
+ end
32
+
33
+ def debug str
34
+ @logger.debug(str)
35
+ end
36
+ end
37
+
38
+