|
|
@@ -0,0 +1,256 @@
|
|
|
+#!/usr/bin/python3
|
|
|
+#
|
|
|
+# Copyright (c) 2021 Clementine Computing LLC.
|
|
|
+#
|
|
|
+# This file is part of PopuFare.
|
|
|
+#
|
|
|
+# PopuFare is free software: you can redistribute it and/or modify
|
|
|
+# it under the terms of the GNU Affero General Public License as published by
|
|
|
+# the Free Software Foundation, either version 3 of the License, or
|
|
|
+# (at your option) any later version.
|
|
|
+#
|
|
|
+# PopuFare is distributed in the hope that it will be useful,
|
|
|
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
|
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
|
+# GNU Affero General Public License for more details.
|
|
|
+#
|
|
|
+# You should have received a copy of the GNU Affero General Public License
|
|
|
+# along with PopuFare. If not, see <https://www.gnu.org/licenses/>.
|
|
|
+#
|
|
|
+
|
|
|
+# This is the front end interface to the PIU that will run as
|
|
|
+# a Python application.
|
|
|
+# It displays the video stream and overlays the barcode, if it finds one.
|
|
|
+# It scans in the directory:
|
|
|
+#
|
|
|
+# /home/bus/queue
|
|
|
+#
|
|
|
+# for messages. If found, it then moves them to:
|
|
|
+#
|
|
|
+# /home/bus/log/messages
|
|
|
+#
|
|
|
+# Only the last message (after filename sorting) is chosen.
|
|
|
+# The first line in the message file is the status message.
|
|
|
+# 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
|
|
|
+import os
|
|
|
+
|
|
|
+import tkinter as tk
|
|
|
+from PIL import Image, ImageTk
|
|
|
+import cv2
|
|
|
+import signal
|
|
|
+import tkinter.font as tkfont
|
|
|
+
|
|
|
+import numpy as np
|
|
|
+from pyzbar import pyzbar
|
|
|
+import datetime
|
|
|
+import imutils
|
|
|
+import time
|
|
|
+
|
|
|
+from imutils.video.pivideostream import PiVideoStream
|
|
|
+from imutils.video import FPS
|
|
|
+from picamera.array import PiRGBArray
|
|
|
+from picamera import PiCamera
|
|
|
+
|
|
|
+def handler(ev):
|
|
|
+ root.destroy()
|
|
|
+
|
|
|
+def check():
|
|
|
+ root.after(500, check)
|
|
|
+
|
|
|
+class MainWindow():
|
|
|
+ def __init__(self, window, pistream):
|
|
|
+
|
|
|
+ self.display_msg = ""
|
|
|
+ self.display_msg_error = ""
|
|
|
+ self.display_msg_idle = "READY"
|
|
|
+ self.display_msg_t = 10000
|
|
|
+ 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")
|
|
|
+
|
|
|
+ self.pistream = pistream
|
|
|
+
|
|
|
+ pad = 0
|
|
|
+ self.disp_pad = pad
|
|
|
+ self._orig_geom = "{0}x{1}+0+0".format( window.winfo_screenwidth()-pad, window.winfo_screenheight()-pad )
|
|
|
+
|
|
|
+ self.screen_width = window.winfo_screenwidth()
|
|
|
+ self.screen_height = window.winfo_screenheight()
|
|
|
+
|
|
|
+ window.attributes("-fullscreen", True)
|
|
|
+
|
|
|
+ self.cap_width = 400
|
|
|
+ self.cap_height = 300
|
|
|
+
|
|
|
+ self.window = window
|
|
|
+
|
|
|
+ self.interval = 30
|
|
|
+
|
|
|
+ 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)
|
|
|
+
|
|
|
+ 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
|
|
|
+
|
|
|
+ self.update_loop()
|
|
|
+
|
|
|
+ def log_barcode(self, data):
|
|
|
+ with open( self.barcode_ofn, "a" ) as ofp:
|
|
|
+ ofp.write( data + "\n")
|
|
|
+ ofp.flush()
|
|
|
+
|
|
|
+ def message_scan(self):
|
|
|
+ msg_fns = []
|
|
|
+ for rootdir, dirs, fns in os.walk( self.message_dir ):
|
|
|
+ for fn in fns:
|
|
|
+ msg_fns.append(fn)
|
|
|
+
|
|
|
+ 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)
|
|
|
+
|
|
|
+ with open(src_fq_msg_fn) as fp:
|
|
|
+ msg_a = fp.read().split("\n")
|
|
|
+ self.display_msg = msg_a[0][0:10]
|
|
|
+ if len(msg_a) > 1:
|
|
|
+ self.display_msg_error = msg_a[1][0:10]
|
|
|
+ self.display_msg_start = time.time() * 1000.0
|
|
|
+ self.display_msg_end = self.display_msg_start + self.display_msg_t
|
|
|
+
|
|
|
+ os.replace(src_fq_msg_fn, dst_fq_msg_fn)
|
|
|
+
|
|
|
+ def message_update(self):
|
|
|
+ t = time.time() * 1000.0
|
|
|
+
|
|
|
+ if t > self.display_msg_end:
|
|
|
+ self.display_msg = ""
|
|
|
+ self.display_msg_error = ""
|
|
|
+
|
|
|
+ self.display_msg = self.display_msg_idle
|
|
|
+
|
|
|
+
|
|
|
+ def update_loop(self):
|
|
|
+
|
|
|
+ self.message_scan()
|
|
|
+ self.message_update()
|
|
|
+
|
|
|
+ img_cv2 = None
|
|
|
+ img_cv2 = self.pistream.read()
|
|
|
+ img_cv2 = imutils.resize(img_cv2, width=self.cap_width)
|
|
|
+
|
|
|
+ ## overlay barcode rectanges and text on cv2 image
|
|
|
+ ##
|
|
|
+ barcodes = pyzbar.decode(img_cv2)
|
|
|
+ for barcode in barcodes:
|
|
|
+ (x,y,w,h) = barcode.rect
|
|
|
+ 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)
|
|
|
+
|
|
|
+ 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
|
|
|
+ #
|
|
|
+
|
|
|
+ #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)
|
|
|
+ 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
|
|
|
+
|
|
|
+ midx = int(self.screen_width/2)
|
|
|
+
|
|
|
+ _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 = [ yyyymmdd, time_str, self.display_msg, self.display_msg_error ]
|
|
|
+ text_msg_dy = [ 60, 80, 80, 60 , 0 ]
|
|
|
+
|
|
|
+ self.canvas.delete("all")
|
|
|
+
|
|
|
+ txt_dy = 80
|
|
|
+ txt_x = midx
|
|
|
+ txt_y = int(txt_dy/2)
|
|
|
+ for idx,msg in enumerate(text_msg):
|
|
|
+ font = None
|
|
|
+ if idx==0:
|
|
|
+ font = tkfont.Font(family="monospace", weight="bold", size=60)
|
|
|
+ else:
|
|
|
+ font = tkfont.Font(family="monospace", weight="bold", size=50)
|
|
|
+
|
|
|
+
|
|
|
+ color = "#aaa"
|
|
|
+ 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)
|
|
|
+ self.window.after(self.interval, self.update_loop)
|
|
|
+
|
|
|
+if __name__ == "__main__":
|
|
|
+ root = tk.Tk()
|
|
|
+ pistream = PiVideoStream().start()
|
|
|
+ time.sleep(2.0)
|
|
|
+ MainWindow(root, pistream)
|
|
|
+
|
|
|
+ signal.signal(signal.SIGINT, lambda x,y : print('terminal ^C') or handler(None))
|
|
|
+ root.after(500, check)
|
|
|
+ root.bind_all('<Control-c>', handler)
|
|
|
+ root.configure(bg='#000')
|
|
|
+ root.mainloop()
|