Ver Fonte

further updates to piu_app

* text overlaid is flipped appropriatly, so it's readable
* watchdog to file output for the possibility of monitoring
* qr code scans on original images size
* comments
clementinecomputing há 4 anos atrás
pai
commit
607dca12dc
1 ficheiros alterados com 134 adições e 42 exclusões
  1. 134 42
      busunit-PIU/scripts/piu_app

+ 134 - 42
busunit-PIU/scripts/piu_app

@@ -34,10 +34,6 @@
 # The second line is the error message.
 # Either or both lines can be blank without consequence.
 #
-# QR Codes are logged in:
-#
-#  /home/bus/log/qrcode.log
-#
 
 
 import sys
@@ -60,6 +56,8 @@ from imutils.video import FPS
 from picamera.array import PiRGBArray
 from picamera import PiCamera
 
+# signal handlers to capture ctrl-c etc. for "graceful" exit
+#
 def handler(ev):
   root.destroy()
 
@@ -69,6 +67,11 @@ def check():
 class MainWindow():
   def __init__(self, window, pistream):
 
+    # variables relating to display messages,
+    # how long to display them for, when to
+    # remove them from display and what to
+    # display when idle.
+    #
     self.display_msg = ""
     self.display_msg_error = ""
     self.display_msg_idle = "READY"
@@ -76,11 +79,21 @@ class MainWindow():
     self.display_msg_start = 0
     self.display_msg_end = 0
 
-    self.message_dir = "/home/bus/queue"
-    self.processed_message_dir = "/home/bus/log/message"
-    self.log_dir = "/home/bus/log"
-    self.barcode_ofn = os.path.join(self.log_dir, "qrcode.log")
+    # log files and locations of communication files
+    #
+    self.message_dir            = "/home/bus/queue"
+    self.processed_message_dir  = "/home/bus/log/message"
+    self.log_dir                = "/home/bus/log"
+    self.barcode_ofn            = os.path.join(self.log_dir, "qrcode.log")
+    self.watchdog_ofn           = os.path.join(self.log_dir, "piu_app.watchdog")
 
+    # watchdog rate limit variables
+    #
+    self.watchdog_rate_ms = 2000
+    self.watchdog_next = 0
+
+    # our camera stream
+    #
     self.pistream = pistream
 
     pad = 0
@@ -97,19 +110,28 @@ class MainWindow():
 
     self.window = window
 
-    self.interval = 30
+    # Camera processing and graphical display rate, in milliseconds.
+    # This is optimisitic and it looks like Python, or whatever else,
+    # is not going to really acheive sub 100ms.
+    #
+    self.interval = 50
 
     self.canvas = tk.Canvas(self.window, width=self.screen_width, height=self.screen_height, bg='#000', bd=0, highlightthickness=0, relief='ridge')
     self.canvas.grid(row=0, column=0)
 
+    # variables for bar/qr code scanning.
+    # * rate limiting to no log multiple quick reads as independent
+    # * save last token read so we aren't limited by the rate limit
+    # * `t_prv` and `t_now` for rate limiting caluclations
+    #
     self.RATE_LIMIT_MS = 1500.0
     self.LAST_TOK = ""
-    #self.barcode_ofp = open("/tmp/qrcode.log", "a")
-    #self.barcode_ofp = open("/home/bus/log/qrcode.log", "a")
 
     self.t_prv = time.time()*1000.0
     self.t_now = self.t_prv
 
+    # kick off our main loop
+    #
     self.update_loop()
 
   def log_barcode(self, data):
@@ -117,6 +139,11 @@ class MainWindow():
       ofp.write( data  + "\n")
       ofp.flush()
 
+  # Look for new messages in the `message` directory
+  # (stored in `self.message_dir`) for new messages.
+  # Once read, move to the `self.processed_message_dir`
+  # directory.
+  #
   def message_scan(self):
     msg_fns = []
     for rootdir, dirs, fns in os.walk( self.message_dir ):
@@ -125,7 +152,6 @@ class MainWindow():
 
     msg_fns.sort()
     for msg_fn in msg_fns:
-      print(">>>", msg_fn)
       src_fq_msg_fn = os.path.join(self.message_dir, msg_fn)
       dst_fq_msg_fn = os.path.join(self.processed_message_dir, msg_fn)
 
@@ -142,82 +168,142 @@ class MainWindow():
   def message_update(self):
     t = time.time() * 1000.0
 
+    # Clear the current message if we've exceeded
+    # the `self.display_msg_end` time.
+    # Put in the idle message.
+    #
     if t > self.display_msg_end:
       self.display_msg = ""
       self.display_msg_error = ""
 
       self.display_msg = self.display_msg_idle
 
+  # @rite to watchdog file with current time (in seconds, since
+  # epoch) at a rate of `self.watchdog_rate_ms` (in milliseconds).
+  # Monitoring processes can check this file to make sure the
+  # piu_app is still running.
+  #
+  def watchdog(self):
+    t = time.time() * 1000.0
+    if t > self.watchdog_next:
+      with open( self.watchdog_ofn, "w" ) as ofp:
+        ofp.write( "{}".format(int(t)) + "\n" )
+        ofp.flush()
+      self.watchdog_next = t + self.watchdog_rate_ms
 
   def update_loop(self):
+    self.watchdog()
 
+    # scan for PIU messages to display on-screen
+    #
     self.message_scan()
     self.message_update()
 
+    # grab our camera and resize apporpriately
+    #
     img_cv2 = None
-    img_cv2 = self.pistream.read()
-    img_cv2 = imutils.resize(img_cv2, width=self.cap_width)
+    img_cv2_o = self.pistream.read()
+    img_cv2 = imutils.resize(img_cv2_o, width=self.cap_width)
+
+    # keep oriignal image size to process barcodes, resize
+    # for display and keep rescale factor to properly overla
+    # the rectangle and other indicator text.
+    #
+    _r = float(self.cap_width) / float(img_cv2_o.shape[1])
 
     ## overlay barcode rectanges and text on cv2 image
     ##
-    barcodes = pyzbar.decode(img_cv2)
+    barcodes = pyzbar.decode(img_cv2_o)
+
+    show_text = None
     for barcode in barcodes:
-      (x,y,w,h) = barcode.rect
-      cv2.rectangle(img_cv2, (x,y), (x+w, y+h), (0, 0, 255), 2)
 
+      # Show overlay of scanned barcode with text information
+      # on what information was scanned.
+      #
+      (x_o,y_o,w_o,h_o) = barcode.rect
+      (x,y,w,h) = ( int(float(x_o)*_r), int(float(y_o)*_r), int(float(w_o)*_r), int(float(h_o)*_r) )
+      cv2.rectangle(img_cv2, (x,y), (x+w, y+h), (0, 0, 255), 2)
       barcode_data = barcode.data.decode("utf-8")
       barcode_type = barcode.type
-
       show_text = "{} ({})".format(barcode_data, barcode_type)
 
-      cv2.putText(img_cv2, show_text, (x, y - 10),
-        cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 0, 255), 2)
-
+      # we flip the image (below), so we need to transform the x
+      # coordinate
+      #
+      _w = img_cv2.shape[1]
+      cam_img_txt_org = (_w - x - w, y - 10)
+
+      # We will be sensing bar/qr codes continuiously (50ms ideally,
+      # though probably slower due to Python inefficiencies), so
+      # we only consider bar/qr code data if the scanned code is
+      # different from our last scan _or_ if we've waited past
+      # our 'RATE_LIMIT_MS' limit.
+      # Codes scanned are logged to the barcode file, which another
+      # process should pickup and process.
+      #
       self.t_now = time.time()*1000.0
-
       if self.LAST_TOK != barcode_data:
-        #self.barcode_ofp.write( str(int(time.time())) + ": " + barcode_data + "\n")
-        #self.barcode_ofp.flush()
         self.log_barcode( str(int(time.time())) + ": " + barcode_data )
         self.t_prv = self.t_now
         self.LAST_TOK = barcode_data
       elif (self.t_now - self.t_prv) >= self.RATE_LIMIT_MS:
-        #self.barcode_ofp.write( str(int(time.time())) + ": " + barcode_data + "\n")
-        #self.barcode_ofp.flush()
         self.log_barcode( str(int(time.time())) + ": " + barcode_data )
         self.t_prv = self.t_now
         self.LAST_TOK = barcode_data
       else:
         pass
 
-
-    # convert cv2 image to something that tkinter can display
+    # We need to convert from whatever native format opencv returns
+    # to the format that tkinter expects.
+    # We also flip the image so that the camera is much more like
+    # a "mirror", for ease of media placement under/in front of the
+    # camera.
     #
-       
-    #self.image = cv2.cvtColor(self.cap.read()[1], cv2.COLOR_BGR2RGB)
     self.image = cv2.cvtColor(img_cv2, cv2.COLOR_BGR2RGB)
     self.image = cv2.flip(self.image, 1)
+
+    # to get the barcode text to not be reversed, we overlay
+    #the bar/qr code text here.
+    #
+    if show_text is not None:
+      _w = self.image.shape[1]
+      cv2.putText(self.image, show_text, cam_img_txt_org,
+        cv2.FONT_HERSHEY_SIMPLEX, 0.5, (235, 116, 108), 2 )
+
+
     self.image = Image.fromarray(self.image)
     self.image = ImageTk.PhotoImage(self.image)
 
-    dy = int(self.screen_height - self.cap_height)
-    dx = int((self.screen_width - self.cap_width)/2)
-    dx -= 1
-    if dx<0: dx=0
+    disp_cam_dy = int(self.screen_height - self.cap_height)
+    disp_cam_dx = int((self.screen_width - self.cap_width)/2)
+    disp_cam_dx -= 1
+    if disp_cam_dx<0: disp_cam_dx=0
 
     midx = int(self.screen_width/2)
 
+    # Do some formatting of date-time.
+    # "Blink" the ':' each second.
+    #
     _now = datetime.datetime.now()
     yyyymmdd = _now.strftime("%Y-%m-%d")
     time_str = _now.strftime("%a %I") + [":", " "][int(_now.strftime("%S"))%2] + _now.strftime("%M%p")
 
-    char_width = 10
-    #text_msg = [ "2021-06-01", "Tue 10:53AM", "See Driver", "No Passes", "on Card" ]
+    # text_msg are the messages to be displayed.
+    # The position in the array is the vertical position and 'type' of message
+    #
+    # 0-1:  date-time
+    # 2:    status
+    # 3-4:  error
+    #
     text_msg = [ yyyymmdd, time_str, self.display_msg, self.display_msg_error ]
     text_msg_dy = [ 60, 80, 80, 60 , 0 ]
 
     self.canvas.delete("all")
 
+    # txt_dy is the amount to shift the text down by
+    # (txt_x, txt_y) is the current (center) position of displayed text
+    #
     txt_dy = 80
     txt_x = midx
     txt_y = int(txt_dy/2)
@@ -229,18 +315,22 @@ class MainWindow():
         font = tkfont.Font(family="monospace", weight="bold", size=50)
 
 
+      # change color depending on position
+      # top two rows are normal text (date and time)
+      # third row is status message (blue)
+      # fourth and fifth row are error messages
+      #
       color = "#aaa"
-      if idx == 2:
-        color = "#6194fa"
-      elif idx > 2:
-        color = "#fa6171"
+      if idx == 2:  color = "#6194fa"
+      elif idx > 2: color = "#fa6171"
 
       self.canvas.create_text(txt_x, txt_y, anchor=tk.CENTER, text=msg, font=font, fill=color)
-      #txt_y += txt_dy
       txt_y += text_msg_dy[idx]
 
 
-    self.canvas.create_image(dx, dy, anchor=tk.NW, image=self.image)
+    # draw our built up tk canvas and setupt the interval callback
+    #
+    self.canvas.create_image(disp_cam_dx, disp_cam_dy, anchor=tk.NW, image=self.image)
     self.window.after(self.interval, self.update_loop)
 
 if __name__ == "__main__":
@@ -249,6 +339,8 @@ if __name__ == "__main__":
     time.sleep(2.0)
     MainWindow(root, pistream)
 
+    # catch signals so we can gracefully-ish exit on a ctrl-c
+    #
     signal.signal(signal.SIGINT, lambda x,y : print('terminal ^C') or handler(None))
     root.after(500, check)
     root.bind_all('<Control-c>', handler)