|
@@ -34,10 +34,6 @@
|
|
|
# The second line is the error message.
|
|
# The second line is the error message.
|
|
|
# Either or both lines can be blank without consequence.
|
|
# Either or both lines can be blank without consequence.
|
|
|
#
|
|
#
|
|
|
-# QR Codes are logged in:
|
|
|
|
|
-#
|
|
|
|
|
-# /home/bus/log/qrcode.log
|
|
|
|
|
-#
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
import sys
|
|
import sys
|
|
@@ -60,6 +56,8 @@ from imutils.video import FPS
|
|
|
from picamera.array import PiRGBArray
|
|
from picamera.array import PiRGBArray
|
|
|
from picamera import PiCamera
|
|
from picamera import PiCamera
|
|
|
|
|
|
|
|
|
|
+# signal handlers to capture ctrl-c etc. for "graceful" exit
|
|
|
|
|
+#
|
|
|
def handler(ev):
|
|
def handler(ev):
|
|
|
root.destroy()
|
|
root.destroy()
|
|
|
|
|
|
|
@@ -69,6 +67,11 @@ def check():
|
|
|
class MainWindow():
|
|
class MainWindow():
|
|
|
def __init__(self, window, pistream):
|
|
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 = ""
|
|
|
self.display_msg_error = ""
|
|
self.display_msg_error = ""
|
|
|
self.display_msg_idle = "READY"
|
|
self.display_msg_idle = "READY"
|
|
@@ -76,11 +79,21 @@ class MainWindow():
|
|
|
self.display_msg_start = 0
|
|
self.display_msg_start = 0
|
|
|
self.display_msg_end = 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
|
|
self.pistream = pistream
|
|
|
|
|
|
|
|
pad = 0
|
|
pad = 0
|
|
@@ -97,19 +110,28 @@ class MainWindow():
|
|
|
|
|
|
|
|
self.window = window
|
|
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 = 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.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.RATE_LIMIT_MS = 1500.0
|
|
|
self.LAST_TOK = ""
|
|
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_prv = time.time()*1000.0
|
|
|
self.t_now = self.t_prv
|
|
self.t_now = self.t_prv
|
|
|
|
|
|
|
|
|
|
+ # kick off our main loop
|
|
|
|
|
+ #
|
|
|
self.update_loop()
|
|
self.update_loop()
|
|
|
|
|
|
|
|
def log_barcode(self, data):
|
|
def log_barcode(self, data):
|
|
@@ -117,6 +139,11 @@ class MainWindow():
|
|
|
ofp.write( data + "\n")
|
|
ofp.write( data + "\n")
|
|
|
ofp.flush()
|
|
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):
|
|
def message_scan(self):
|
|
|
msg_fns = []
|
|
msg_fns = []
|
|
|
for rootdir, dirs, fns in os.walk( self.message_dir ):
|
|
for rootdir, dirs, fns in os.walk( self.message_dir ):
|
|
@@ -125,7 +152,6 @@ class MainWindow():
|
|
|
|
|
|
|
|
msg_fns.sort()
|
|
msg_fns.sort()
|
|
|
for msg_fn in msg_fns:
|
|
for msg_fn in msg_fns:
|
|
|
- print(">>>", msg_fn)
|
|
|
|
|
src_fq_msg_fn = os.path.join(self.message_dir, 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)
|
|
dst_fq_msg_fn = os.path.join(self.processed_message_dir, msg_fn)
|
|
|
|
|
|
|
@@ -142,82 +168,142 @@ class MainWindow():
|
|
|
def message_update(self):
|
|
def message_update(self):
|
|
|
t = time.time() * 1000.0
|
|
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:
|
|
if t > self.display_msg_end:
|
|
|
self.display_msg = ""
|
|
self.display_msg = ""
|
|
|
self.display_msg_error = ""
|
|
self.display_msg_error = ""
|
|
|
|
|
|
|
|
self.display_msg = self.display_msg_idle
|
|
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):
|
|
def update_loop(self):
|
|
|
|
|
+ self.watchdog()
|
|
|
|
|
|
|
|
|
|
+ # scan for PIU messages to display on-screen
|
|
|
|
|
+ #
|
|
|
self.message_scan()
|
|
self.message_scan()
|
|
|
self.message_update()
|
|
self.message_update()
|
|
|
|
|
|
|
|
|
|
+ # grab our camera and resize apporpriately
|
|
|
|
|
+ #
|
|
|
img_cv2 = None
|
|
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
|
|
## 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:
|
|
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_data = barcode.data.decode("utf-8")
|
|
|
barcode_type = barcode.type
|
|
barcode_type = barcode.type
|
|
|
-
|
|
|
|
|
show_text = "{} ({})".format(barcode_data, 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
|
|
self.t_now = time.time()*1000.0
|
|
|
-
|
|
|
|
|
if self.LAST_TOK != barcode_data:
|
|
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.log_barcode( str(int(time.time())) + ": " + barcode_data )
|
|
|
self.t_prv = self.t_now
|
|
self.t_prv = self.t_now
|
|
|
self.LAST_TOK = barcode_data
|
|
self.LAST_TOK = barcode_data
|
|
|
elif (self.t_now - self.t_prv) >= self.RATE_LIMIT_MS:
|
|
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.log_barcode( str(int(time.time())) + ": " + barcode_data )
|
|
|
self.t_prv = self.t_now
|
|
self.t_prv = self.t_now
|
|
|
self.LAST_TOK = barcode_data
|
|
self.LAST_TOK = barcode_data
|
|
|
else:
|
|
else:
|
|
|
pass
|
|
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.cvtColor(img_cv2, cv2.COLOR_BGR2RGB)
|
|
|
self.image = cv2.flip(self.image, 1)
|
|
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 = Image.fromarray(self.image)
|
|
|
self.image = ImageTk.PhotoImage(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)
|
|
midx = int(self.screen_width/2)
|
|
|
|
|
|
|
|
|
|
+ # Do some formatting of date-time.
|
|
|
|
|
+ # "Blink" the ':' each second.
|
|
|
|
|
+ #
|
|
|
_now = datetime.datetime.now()
|
|
_now = datetime.datetime.now()
|
|
|
yyyymmdd = _now.strftime("%Y-%m-%d")
|
|
yyyymmdd = _now.strftime("%Y-%m-%d")
|
|
|
time_str = _now.strftime("%a %I") + [":", " "][int(_now.strftime("%S"))%2] + _now.strftime("%M%p")
|
|
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 = [ yyyymmdd, time_str, self.display_msg, self.display_msg_error ]
|
|
|
text_msg_dy = [ 60, 80, 80, 60 , 0 ]
|
|
text_msg_dy = [ 60, 80, 80, 60 , 0 ]
|
|
|
|
|
|
|
|
self.canvas.delete("all")
|
|
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_dy = 80
|
|
|
txt_x = midx
|
|
txt_x = midx
|
|
|
txt_y = int(txt_dy/2)
|
|
txt_y = int(txt_dy/2)
|
|
@@ -229,18 +315,22 @@ class MainWindow():
|
|
|
font = tkfont.Font(family="monospace", weight="bold", size=50)
|
|
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"
|
|
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)
|
|
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]
|
|
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)
|
|
self.window.after(self.interval, self.update_loop)
|
|
|
|
|
|
|
|
if __name__ == "__main__":
|
|
if __name__ == "__main__":
|
|
@@ -249,6 +339,8 @@ if __name__ == "__main__":
|
|
|
time.sleep(2.0)
|
|
time.sleep(2.0)
|
|
|
MainWindow(root, pistream)
|
|
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))
|
|
signal.signal(signal.SIGINT, lambda x,y : print('terminal ^C') or handler(None))
|
|
|
root.after(500, check)
|
|
root.after(500, check)
|
|
|
root.bind_all('<Control-c>', handler)
|
|
root.bind_all('<Control-c>', handler)
|