74adc39a59e161fecb0153039dd8aa9442ef92ff
[pedometerwidget] / pedometer_widget_home.py
1 import gtk
2 import cairo
3 import hildondesktop
4 import gobject
5 import os
6 import time
7 import hildon
8 import gnome.gconf as gconf
9 from threading import Thread
10
11 #gobject.threads_init()
12 #gtk.gdk.threads_init()
13
14 PATH="/apps/pedometerhomewidget"
15 COUNTER=PATH+"/counter"
16 TIMER=PATH+"/timer"
17 MODE=PATH+"/mode"
18 HEIGHT=PATH+"/height"
19 UNIT=PATH+"/unit"
20 ASPECT=PATH+"/aspect"
21 LOGGING=PATH+"/logging"
22
23 ICONSPATH = "/opt/pedometerhomewidget/"
24
25 class PedoIntervalCounter:
26     MIN_THRESHOLD = 500
27     MIN_TIME_STEPS = 0.5
28     x = []
29     y = []
30     z = []
31     t = []
32
33     #TODO: check if last detected step is at the end of the interval
34
35     def __init__(self, coords, tval):
36         self.x = coords[0]
37         self.y = coords[1]
38         self.z = coords[2]
39         self.t = tval
40
41     def setThreshold(self, value):
42         self.MIN_THRESHOLD = value
43
44     def setTimeSteps(self, value):
45         self.MIN_TIME_STEPS = value
46
47     def calc_mean(self, vals):
48         sum = 0
49         for i in vals:
50             sum+=i
51         if len(vals) > 0:
52             return sum / len(vals)
53         return 0
54
55     def calc_stdev(self, vals):
56         rez = 0
57         mean = self.calc_mean(vals)
58         for i in vals:
59             rez+=pow(abs(mean-i),2)
60         return math.sqrt(rez/len(vals))
61
62     def calc_threshold(self, vals):
63         vmax = max(vals)
64         vmin = min(vals)
65         mean = self.calc_mean(vals)
66         threshold = max (abs(mean-vmax), abs(mean-vmin))
67         return threshold
68
69     def count_steps(self, vals, t):
70         threshold = self.MIN_THRESHOLD
71         mean = self.calc_mean(vals)
72         cnt = 0
73
74         i=0
75         while i < len(vals):
76             if abs(vals[i] - mean) > threshold:
77                 cnt+=1
78                 ntime = t[i] + 0.5
79                 while i < len(vals) and t[i] < ntime:
80                     i+=1
81             i+=1
82         return cnt
83
84     def get_best_values(self, x, y, z):
85         dev1 = self.calc_stdev(x)
86         dev2 = self.calc_stdev(y)
87         dev3 = self.calc_stdev(z)
88         dev_max = max(dev1, dev2, dev3)
89
90         if ( abs(dev1 - dev_max ) < 0.001):
91             logger.info("X chosen as best axis, stdev %f" % dev1)
92             return x
93         elif (abs(dev2 - dev_max) < 0.001):
94             logger.info("Y chosen as best axis, stdev %f" % dev2)
95             return y
96         else:
97             logger.info("Z chosen as best axis, stdev %f" % dev3)
98             return z
99
100     def number_steps(self):
101         vals = self.get_best_values(self.x, self.y, self.z)
102         return self.count_steps(vals, self.t)
103
104 class PedoCounter(Thread):
105     COORD_FNAME = "/sys/class/i2c-adapter/i2c-3/3-001d/coord"
106     COORD_FNAME_SDK = "/home/andrei/pedometer-widget-0.1/date.txt"
107     LOGFILE = "/home/user/log_pedometer"
108     COORD_GET_INTERVAL = 0.01
109     COUNT_INTERVAL = 5
110
111     STEP_LENGTH = 0.7
112
113     MIN_THRESHOLD = 500
114     MIN_TIME_STEPS = 0.5
115
116     counter = 0
117     stop_requested = False
118     update_function = None
119     logging = False
120
121     def __init__(self, update_function = None):
122         Thread.__init__(self)
123         if not os.path.exists(self.COORD_FNAME):
124             self.COORD_FNAME = self.COORD_FNAME_SDK
125
126         self.update_function = update_function
127
128     def set_mode(self, mode):
129         #runnig, higher threshold to prevent fake steps
130         if mode == 1:
131             self.MIN_THRESHOLD = 650
132             self.MIN_TIME_STEPS = 0.35
133         #walking
134         else:
135             self.MIN_THRESHOLD = 500
136             self.MIN_TIME_STEPS = 0.5
137         #update set length
138         self.set_height(self.HEIGHT)
139
140     def set_logging(self, value):
141         self.logging = value
142
143     #set height, will affect the distance
144     def set_height(self, height_interval):
145         if height_interval == 0:
146             STEP_LENGTH = 0.59
147         elif height_interval == 1:
148             STEP_LENGTH = 0.64
149         elif height_interval == 2:
150             STEP_LENGTH = 0.71
151         elif height_interval == 3:
152             STEP_LENGTH = 0.77
153         elif height_interval == 4:
154             STEP_LENGTH = 0.83
155         #increase step length if RUNNING
156         if self.mode == 1:
157             STEP_LENGTH *= 1.45
158
159     def get_rotation(self):
160         f = open(self.COORD_FNAME, 'r')
161         coords = [int(w) for w in f.readline().split()]
162         f.close()
163         return coords
164
165     def reset_counter(self):
166         counter = 0
167
168     def get_counter(self):
169         return counter
170
171     def start_interval(self):
172         logger.info("New interval started")
173         stime = time.time()
174         t=[]
175         coords = [[], [], []]
176         while not self.stop_requested and (len(t) == 0 or t[-1] < 5):
177             x,y,z = self.get_rotation()
178             coords[0].append(int(x))
179             coords[1].append(int(y))
180             coords[2].append(int(z))
181             now = time.time()-stime
182             if self.logging:
183                 self.file.write("%d %d %d %f\n" %(coords[0][-1], coords[1][-1], coords[2][-1], now))
184
185             t.append(now)
186             time.sleep(self.COORD_GET_INTERVAL)
187         pic = PedoIntervalCounter(coords, t)
188         cnt = pic.number_steps()
189
190         logger.info("Number of steps detected for last interval %d, number of coords: %d" % (cnt, len(t)))
191
192         self.counter += cnt
193         logger.info("Total number of steps : %d" % self.counter)
194         return cnt
195
196     def request_stop(self):
197         self.stop_requested = True
198
199     def run(self):
200         logger.info("Thread started")
201         if self.logging:
202             fname = "%d_%d_%d_%d_%d_%d" % time.localtime()[0:6]
203             self.file = open(self.LOGFILE + fname + ".txt", "w")
204
205         while 1 and not self.stop_requested:
206             last_cnt = self.start_interval()
207             if self.update_function is not None:
208                 gobject.idle_add(self.update_function, self.counter, last_cnt)
209
210         if self.logging:
211             self.file.close()
212
213         logger.info("Thread has finished")
214
215     def get_distance(self, steps=None):
216         if steps == None:
217             steps = self.counter
218         return self.STEP_LENGTH * steps;
219
220 class CustomButton(hildon.Button):
221     def __init__(self, icon):
222         hildon.Button.__init__(self, gtk.HILDON_SIZE_AUTO_WIDTH, hildon.BUTTON_ARRANGEMENT_VERTICAL)
223         self.icon = icon
224         self.set_size_request(int(32*1.4), int(30*1.0))
225         self.retval = self.connect("expose_event", self.expose)
226
227     def set_icon(self, icon):
228         self.icon = icon
229
230     def expose(self, widget, event):
231         self.context = widget.window.cairo_create()
232         self.context.rectangle(event.area.x, event.area.y,
233                             event.area.width, event.area.height)
234
235         self.context.clip()
236         rect = self.get_allocation()
237         self.context.rectangle(rect.x, rect.y, rect.width, rect.height)
238         self.context.set_source_rgba(1, 1, 1, 0)
239
240         style = self.rc_get_style()
241         color = style.lookup_color("DefaultBackgroundColor")
242         if self.state == gtk.STATE_ACTIVE:
243             style = self.rc_get_style()
244             color = style.lookup_color("SelectionColor")
245             self.context.set_source_rgba (color.red/65535.0, color.green/65335.0, color.blue/65535.0, 0.75);
246         self.context.fill()
247
248         #img = cairo.ImageSurface.create_from_png(self.icon)
249
250         #self.context.set_source_surface(img)
251         #self.context.set_source_surface(img, rect.width/2 - img.get_width() /2, 0)
252         img = gtk.Image()
253         img.set_from_file(self.icon)
254         buf = img.get_pixbuf()
255         buf =  buf.scale_simple(int(32 * 1.5), int(30 * 1.5), gtk.gdk.INTERP_BILINEAR)
256
257         self.context.set_source_pixbuf(buf, rect.x+(event.area.width/2-15)-8, rect.y+1)
258         self.context.scale(200,200)
259         self.context.paint()
260
261         return self.retval
262
263 class PedometerHomePlugin(hildondesktop.HomePluginItem):
264     button = None
265
266     #labels for current steps
267     labels = ["timer", "count", "dist", "avgSpeed"]
268     #labelsC = { "timer" : None, "count" : None, "dist" : None, "avgSpeed" : None }
269
270     #labels for all time steps
271     #labelsT = { "timer" : None, "count" : None, "dist" : None, "avgSpeed" : None }
272     labelsC = {}
273     labelsT = {}
274
275     pedometer = None
276     startTime = None
277
278     totalCounter = 0
279     totalTime = 0
280     mode = 0
281     height = 0
282     unit = 0
283
284     counter = 0
285     time = 0
286     aspect = 0
287     logging = False
288
289     def __init__(self):
290
291         gtk.gdk.threads_init()
292         #gobject.threads_init()
293         hildondesktop.HomePluginItem.__init__(self)
294
295         self.client = gconf.client_get_default()
296         try:
297             self.totalCounter = self.client.get_int(COUNTER)
298             self.totalTime = self.client.get_int(TIMER)
299             self.mode = self.client.get_int(MODE)
300             self.height = self.client.get_int(HEIGHT)
301             self.unit = self.client.get_int(UNIT)
302             self.aspect = self.client.get_int(ASPECT)
303             self.logging = self.client.get_bool(LOGGING)
304         except:
305             self.client.set_int(COUNTER, 0)
306             self.client.set_int(TIMER, 0)
307             self.client.set_int(MODE, 0)
308             self.client.set_int(HEIGHT, 0)
309             self.client.set_int(UNIT, 0)
310             self.client.set_int(ASPECT, 0)
311             self.client.set_bool(LOGGING, False)
312
313         self.pedometer = PedoCounter(self.update_values)
314         self.pedometer.set_mode(self.mode)
315         self.pedometer.set_height(self.height)
316
317         #self.button = gtk.Button("Start")
318         self.button = CustomButton(ICONSPATH + "play.png")
319         self.button.connect("clicked", self.button_clicked)
320
321         self.create_labels(self.labelsC)
322         self.create_labels(self.labelsT)
323
324         self.update_ui_values(self.labelsC, 0, 0)
325         self.update_ui_values(self.labelsT, self.totalTime, self.totalCounter)
326
327         mainHBox = gtk.HBox(spacing=1)
328
329         descVBox = gtk.VBox(spacing=1)
330         descVBox.add(self.new_label_heading())
331         descVBox.add(self.new_label_heading("Time:"))
332         descVBox.add(self.new_label_heading("Steps:"))
333         descVBox.add(self.new_label_heading("Distance:"))
334         descVBox.add(self.new_label_heading("Avg Speed:"))
335
336         currentVBox = gtk.VBox(spacing=1)
337         currentVBox.add(self.new_label_heading("Current"))
338         currentVBox.add(self.labelsC["timer"])
339         currentVBox.add(self.labelsC["count"])
340         currentVBox.add(self.labelsC["dist"])
341         currentVBox.add(self.labelsC["avgSpeed"])
342         self.currentBox = currentVBox
343
344         totalVBox = gtk.VBox(spacing=1)
345         totalVBox.add(self.new_label_heading("Total"))
346         totalVBox.add(self.labelsT["timer"])
347         totalVBox.add(self.labelsT["count"])
348         totalVBox.add(self.labelsT["dist"])
349         totalVBox.add(self.labelsT["avgSpeed"])
350         self.totalBox = totalVBox
351
352         buttonVBox = gtk.VBox(spacing=1)
353         buttonVBox.add(self.new_label_heading(""))
354         buttonVBox.add(self.button)
355         buttonVBox.add(self.new_label_heading(""))
356
357         mainHBox.add(buttonVBox)
358         mainHBox.add(descVBox)
359         mainHBox.add(currentVBox)
360         mainHBox.add(totalVBox)
361
362         self.mainhbox = mainHBox
363
364         mainHBox.show_all()
365         self.add(mainHBox)
366         self.update_aspect()
367
368         self.connect("unrealize", self.close_requested)
369         self.set_settings(True)
370         self.connect("show-settings", self.show_settings)
371
372     def new_label_heading(self, title=""):
373         l = gtk.Label(title)
374         hildon.hildon_helper_set_logical_font(l, "SmallSystemFont")
375         return l
376
377     def create_labels(self, new_labels):
378         for label in self.labels:
379             l = gtk.Label()
380             hildon.hildon_helper_set_logical_font(l, "SmallSystemFont")
381             hildon.hildon_helper_set_logical_color(l, gtk.RC_FG, gtk.STATE_NORMAL, "ActiveTextColor")
382             new_labels[label] = l
383
384     def update_aspect(self):
385         if self.aspect == 0:
386             self.currentBox.show_all()
387             self.totalBox.show_all()
388         elif self.aspect == 1:
389             self.currentBox.show_all()
390             self.totalBox.hide_all()
391         else:
392             self.currentBox.hide_all()
393             self.totalBox.show_all()
394
395     def update_ui_values(self, labels, timer, steps):
396         def get_str_distance(meters):
397             if meters > 1000:
398                 if self.unit == 0:
399                     return "%.2f km" % (meters/1000)
400                 else:
401                     return "%.2f mi" % (meters/1609.344)
402             else:
403                 if self.unit == 0:
404                     return "%d m" % meters
405                 else:
406                     return "%d ft" % int(meters*3.2808)
407
408         def get_avg_speed(timer, dist):
409             suffix = ""
410             conv = 0
411             if self.unit:
412                 suffix = "mi/h"
413                 conv = 2.23693629
414             else:
415                 suffix = "km/h"
416                 conv = 3.6
417
418             if timer == 0:
419                 return "N/A " + suffix
420             speed = 1.0 *dist / timer
421             #convert from meters per second to km/h or mi/h
422             speed *= conv
423             return "%.2f %s" % (speed, suffix)
424
425         tdelta = timer
426         hours = int(tdelta / 3600)
427         tdelta -= 3600 * hours
428         mins = int(tdelta / 60)
429         tdelta -= 60 * mins
430         secs = int(tdelta)
431
432         strtime = "%.2d:%.2d:%.2d" % ( hours, mins, secs)
433
434         labels["timer"].set_label(strtime)
435         labels["count"].set_label(str(steps))
436
437         dist = self.pedometer.get_distance(steps)
438
439         labels["dist"].set_label(get_str_distance(dist))
440         labels["avgSpeed"].set_label(get_avg_speed(timer, dist))
441
442     def update_current(self):
443         self.update_ui_values(self.labelsC, self.time, self.counter)
444
445     def update_total(self):
446         self.update_ui_values(self.labelsT, self.totalTime, self.totalCounter)
447
448     def show_settings(self, widget):
449         def reset_total_counter(arg):
450             widget.totalCounter = 0
451             widget.totalTime = 0
452             widget.update_total()
453             hildon.hildon_banner_show_information(self,"None", "Total counter was resetted")
454
455         def selector_changed(selector, data):
456             widget.mode = selector.get_active(0)
457             widget.client.set_int(MODE, widget.mode)
458
459         def selectorH_changed(selector, data):
460             widget.height = selectorH.get_active(0)
461             widget.client.set_int(HEIGHT, widget.height)
462
463         def selectorUnit_changed(selector, data):
464             widget.unit = selectorUnit.get_active(0)
465             widget.client.set_int(UNIT, widget.unit)
466             widget.update_current()
467             widget.update_total()
468
469         def selectorUI_changed(selector, data):
470             widget.aspect = selectorUI.get_active(0)
471             widget.client.set_int(ASPECT, widget.aspect)
472             widget.update_aspect()
473
474         def logButton_changed(checkButton):
475             widget.logging = checkButton.get_active()
476             widget.client.set_bool(LOGGING, widget.logging)
477
478         dialog = gtk.Dialog()
479         dialog.set_transient_for(self)
480         dialog.set_title("Settings")
481
482         dialog.add_button("OK", gtk.RESPONSE_OK)
483         button = hildon.Button(gtk.HILDON_SIZE_AUTO_WIDTH | gtk.HILDON_SIZE_FINGER_HEIGHT, hildon.BUTTON_ARRANGEMENT_VERTICAL)
484         button.set_title("Reset total counter")
485         button.set_alignment(0, 0.8, 1, 1)
486         button.connect("clicked", reset_total_counter)
487
488         selector = hildon.TouchSelector(text=True)
489         selector.set_column_selection_mode(hildon.TOUCH_SELECTOR_SELECTION_MODE_SINGLE)
490         selector.append_text("Walk")
491         selector.append_text("Run")
492         selector.connect("changed", selector_changed)
493
494         modePicker = hildon.PickerButton(gtk.HILDON_SIZE_AUTO_WIDTH | gtk.HILDON_SIZE_FINGER_HEIGHT, hildon.BUTTON_ARRANGEMENT_VERTICAL)
495         modePicker.set_alignment(0.0, 0.5, 1.0, 1.0)
496         modePicker.set_title("Select mode")
497         modePicker.set_selector(selector)
498         modePicker.set_active(widget.mode)
499
500         selectorH = hildon.TouchSelector(text=True)
501         selectorH.set_column_selection_mode(hildon.TOUCH_SELECTOR_SELECTION_MODE_SINGLE)
502         selectorH.append_text("< 1.50 m")
503         selectorH.append_text("1.50 - 1.65 m")
504         selectorH.append_text("1.66 - 1.80 m")
505         selectorH.append_text("1.81 - 1.95 m")
506         selectorH.append_text(" > 1.95 m")
507         selectorH.connect("changed", selectorH_changed)
508
509         heightPicker = hildon.PickerButton(gtk.HILDON_SIZE_AUTO_WIDTH | gtk.HILDON_SIZE_FINGER_HEIGHT, hildon.BUTTON_ARRANGEMENT_VERTICAL)
510         heightPicker.set_alignment(0.0, 0.5, 1.0, 1.0)
511         heightPicker.set_title("Select height")
512         heightPicker.set_selector(selectorH)
513         heightPicker.set_active(widget.height)
514
515         selectorUnit = hildon.TouchSelector(text=True)
516         selectorUnit.set_column_selection_mode(hildon.TOUCH_SELECTOR_SELECTION_MODE_SINGLE)
517         selectorUnit.append_text("Metric (km)")
518         selectorUnit.append_text("English (mi)")
519         selectorUnit.connect("changed", selectorUnit_changed)
520
521         unitPicker = hildon.PickerButton(gtk.HILDON_SIZE_AUTO_WIDTH | gtk.HILDON_SIZE_FINGER_HEIGHT, hildon.BUTTON_ARRANGEMENT_VERTICAL)
522         unitPicker.set_alignment(0.0, 0.5, 1.0, 1.0)
523         unitPicker.set_title("Units")
524         unitPicker.set_selector(selectorUnit)
525         unitPicker.set_active(widget.unit)
526
527         selectorUI = hildon.TouchSelector(text=True)
528         selectorUI = hildon.TouchSelector(text=True)
529         selectorUI.set_column_selection_mode(hildon.TOUCH_SELECTOR_SELECTION_MODE_SINGLE)
530         selectorUI.append_text("Show current + total")
531         selectorUI.append_text("Show only current")
532         selectorUI.append_text("Show only total")
533         selectorUI.connect("changed", selectorUI_changed)
534
535         UIPicker = hildon.PickerButton(gtk.HILDON_SIZE_AUTO_WIDTH | gtk.HILDON_SIZE_FINGER_HEIGHT, hildon.BUTTON_ARRANGEMENT_VERTICAL)
536         UIPicker.set_alignment(0.0, 0.5, 1.0, 1.0)
537         UIPicker.set_title("Widget aspect")
538         UIPicker.set_selector(selectorUI)
539         UIPicker.set_active(widget.aspect)
540
541         logButton = hildon.CheckButton(gtk.HILDON_SIZE_AUTO_WIDTH | gtk.HILDON_SIZE_FINGER_HEIGHT)
542         logButton.set_label("Log data")
543         logButton.set_active(widget.logging)
544         logButton.connect("toggled", logButton_changed)
545
546         dialog.vbox.add(button)
547         dialog.vbox.add(modePicker)
548         dialog.vbox.add(heightPicker)
549         dialog.vbox.add(unitPicker)
550         dialog.vbox.add(UIPicker)
551         dialog.vbox.add(logButton)
552
553         dialog.show_all()
554         response = dialog.run()
555         hildon.hildon_banner_show_information(self, "None", "You have to Stop/Start the counter to apply the new settings")
556         dialog.destroy()
557
558     def close_requested(self, widget):
559         if self.pedometer is None:
560             return
561
562         self.pedometer.request_stop()
563         if self.pedometer.isAlive():
564             self.pedometer.join()
565
566     def update_values(self, totalCurent, lastInterval):
567         self.totalCounter += lastInterval
568         self.counter = totalCurent
569
570         tdelta = time.time() - self.time - self.startTime
571         self.time += tdelta
572         self.totalTime += tdelta
573
574         self.update_current()
575         self.update_total()
576
577     def button_clicked(self, button):
578         if self.pedometer is not None and self.pedometer.isAlive():
579             #counter is running
580             self.pedometer.request_stop()
581             self.pedometer.join()
582             self.client.set_int(COUNTER, self.totalCounter)
583             self.client.set_int(TIMER, int(self.totalTime))
584             #self.button.set_label("Start")
585             self.button.set_icon(ICONSPATH + "play.png")
586         else:
587             self.pedometer = PedoCounter(self.update_values)
588             self.pedometer.set_mode(self.mode)
589             self.pedometer.set_height(self.height)
590             self.pedometer.set_logging(self.logging)
591
592             self.time = 0
593             self.counter = 0
594
595             self.update_current()
596
597             self.pedometer.start()
598             self.startTime = time.time()
599             #self.button.set_label("Stop")
600             self.button.set_icon(ICONSPATH + "stop.png")
601
602     def do_expose_event(self, event):
603         cr = self.window.cairo_create()
604         cr.region(event.window.get_clip_region())
605         cr.clip()
606         #cr.set_source_rgba(0.4, 0.64, 0.564, 0.5)
607         style = self.rc_get_style()
608         color = style.lookup_color("DefaultBackgroundColor")
609         cr.set_source_rgba (color.red/65535.0, color.green/65335.0, color.blue/65535.0, 0.75);
610
611         radius = 5
612         width = self.allocation.width
613         height = self.allocation.height
614
615         x = self.allocation.x
616         y = self.allocation.y
617
618         cr.move_to(x+radius, y)
619         cr.line_to(x + width - radius, y)
620         cr.curve_to(x + width - radius, y, x + width, y, x + width, y + radius)
621         cr.line_to(x + width, y + height - radius)
622         cr.curve_to(x + width, y + height - radius, x + width, y + height, x + width - radius, y + height)
623         cr.line_to(x + radius, y + height)
624         cr.curve_to(x + radius, y + height, x, y + height, x, y + height - radius)
625         cr.line_to(x, y + radius)
626         cr.curve_to(x, y + radius, x, y, x + radius, y)
627
628         cr.set_operator(cairo.OPERATOR_SOURCE)
629         cr.fill_preserve()
630
631         color = style.lookup_color("ActiveTextColor")
632         cr.set_source_rgba (color.red/65535.0, color.green/65335.0, color.blue/65535.0, 0.5);
633         cr.set_line_width(1)
634         cr.stroke()
635
636         hildondesktop.HomePluginItem.do_expose_event(self, event)
637
638     def do_realize(self):
639         screen = self.get_screen()
640         self.set_colormap(screen.get_rgba_colormap())
641         self.set_app_paintable(True)
642         hildondesktop.HomePluginItem.do_realize(self)
643
644 hd_plugin_type = PedometerHomePlugin
645
646 # The code below is just for testing purposes.
647 # It allows to run the widget as a standalone process.
648 if __name__ == "__main__":
649     import gobject
650     gobject.type_register(hd_plugin_type)
651     obj = gobject.new(hd_plugin_type, plugin_id="plugin_id")
652     obj.show_all()
653     gtk.main()
654
655 ############### old pedometer.py ###
656 import math
657 import logging
658
659 from threading import Thread
660
661 logger = logging.getLogger("pedometer")
662 logger.setLevel(logging.INFO)
663
664 ch = logging.StreamHandler()
665 formatter = logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s")
666 ch.setFormatter(formatter)
667 logger.addHandler(ch)
668