piu_app 7.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256
  1. #!/usr/bin/python3
  2. #
  3. # Copyright (c) 2021 Clementine Computing LLC.
  4. #
  5. # This file is part of PopuFare.
  6. #
  7. # PopuFare is free software: you can redistribute it and/or modify
  8. # it under the terms of the GNU Affero General Public License as published by
  9. # the Free Software Foundation, either version 3 of the License, or
  10. # (at your option) any later version.
  11. #
  12. # PopuFare is distributed in the hope that it will be useful,
  13. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  14. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  15. # GNU Affero General Public License for more details.
  16. #
  17. # You should have received a copy of the GNU Affero General Public License
  18. # along with PopuFare. If not, see <https://www.gnu.org/licenses/>.
  19. #
  20. # This is the front end interface to the PIU that will run as
  21. # a Python application.
  22. # It displays the video stream and overlays the barcode, if it finds one.
  23. # It scans in the directory:
  24. #
  25. # /home/bus/queue
  26. #
  27. # for messages. If found, it then moves them to:
  28. #
  29. # /home/bus/log/messages
  30. #
  31. # Only the last message (after filename sorting) is chosen.
  32. # The first line in the message file is the status message.
  33. # The second line is the error message.
  34. # Either or both lines can be blank without consequence.
  35. #
  36. # QR Codes are logged in:
  37. #
  38. # /home/bus/log/qrcode.log
  39. #
  40. import sys
  41. import os
  42. import tkinter as tk
  43. from PIL import Image, ImageTk
  44. import cv2
  45. import signal
  46. import tkinter.font as tkfont
  47. import numpy as np
  48. from pyzbar import pyzbar
  49. import datetime
  50. import imutils
  51. import time
  52. from imutils.video.pivideostream import PiVideoStream
  53. from imutils.video import FPS
  54. from picamera.array import PiRGBArray
  55. from picamera import PiCamera
  56. def handler(ev):
  57. root.destroy()
  58. def check():
  59. root.after(500, check)
  60. class MainWindow():
  61. def __init__(self, window, pistream):
  62. self.display_msg = ""
  63. self.display_msg_error = ""
  64. self.display_msg_idle = "READY"
  65. self.display_msg_t = 10000
  66. self.display_msg_start = 0
  67. self.display_msg_end = 0
  68. self.message_dir = "/home/bus/queue"
  69. self.processed_message_dir = "/home/bus/log/message"
  70. self.log_dir = "/home/bus/log"
  71. self.barcode_ofn = os.path.join(self.log_dir, "qrcode.log")
  72. self.pistream = pistream
  73. pad = 0
  74. self.disp_pad = pad
  75. self._orig_geom = "{0}x{1}+0+0".format( window.winfo_screenwidth()-pad, window.winfo_screenheight()-pad )
  76. self.screen_width = window.winfo_screenwidth()
  77. self.screen_height = window.winfo_screenheight()
  78. window.attributes("-fullscreen", True)
  79. self.cap_width = 400
  80. self.cap_height = 300
  81. self.window = window
  82. self.interval = 30
  83. self.canvas = tk.Canvas(self.window, width=self.screen_width, height=self.screen_height, bg='#000', bd=0, highlightthickness=0, relief='ridge')
  84. self.canvas.grid(row=0, column=0)
  85. self.RATE_LIMIT_MS = 1500.0
  86. self.LAST_TOK = ""
  87. #self.barcode_ofp = open("/tmp/qrcode.log", "a")
  88. #self.barcode_ofp = open("/home/bus/log/qrcode.log", "a")
  89. self.t_prv = time.time()*1000.0
  90. self.t_now = self.t_prv
  91. self.update_loop()
  92. def log_barcode(self, data):
  93. with open( self.barcode_ofn, "a" ) as ofp:
  94. ofp.write( data + "\n")
  95. ofp.flush()
  96. def message_scan(self):
  97. msg_fns = []
  98. for rootdir, dirs, fns in os.walk( self.message_dir ):
  99. for fn in fns:
  100. msg_fns.append(fn)
  101. msg_fns.sort()
  102. for msg_fn in msg_fns:
  103. print(">>>", msg_fn)
  104. src_fq_msg_fn = os.path.join(self.message_dir, msg_fn)
  105. dst_fq_msg_fn = os.path.join(self.processed_message_dir, msg_fn)
  106. with open(src_fq_msg_fn) as fp:
  107. msg_a = fp.read().split("\n")
  108. self.display_msg = msg_a[0][0:10]
  109. if len(msg_a) > 1:
  110. self.display_msg_error = msg_a[1][0:10]
  111. self.display_msg_start = time.time() * 1000.0
  112. self.display_msg_end = self.display_msg_start + self.display_msg_t
  113. os.replace(src_fq_msg_fn, dst_fq_msg_fn)
  114. def message_update(self):
  115. t = time.time() * 1000.0
  116. if t > self.display_msg_end:
  117. self.display_msg = ""
  118. self.display_msg_error = ""
  119. self.display_msg = self.display_msg_idle
  120. def update_loop(self):
  121. self.message_scan()
  122. self.message_update()
  123. img_cv2 = None
  124. img_cv2 = self.pistream.read()
  125. img_cv2 = imutils.resize(img_cv2, width=self.cap_width)
  126. ## overlay barcode rectanges and text on cv2 image
  127. ##
  128. barcodes = pyzbar.decode(img_cv2)
  129. for barcode in barcodes:
  130. (x,y,w,h) = barcode.rect
  131. cv2.rectangle(img_cv2, (x,y), (x+w, y+h), (0, 0, 255), 2)
  132. barcode_data = barcode.data.decode("utf-8")
  133. barcode_type = barcode.type
  134. show_text = "{} ({})".format(barcode_data, barcode_type)
  135. cv2.putText(img_cv2, show_text, (x, y - 10),
  136. cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 0, 255), 2)
  137. self.t_now = time.time()*1000.0
  138. if self.LAST_TOK != barcode_data:
  139. #self.barcode_ofp.write( str(int(time.time())) + ": " + barcode_data + "\n")
  140. #self.barcode_ofp.flush()
  141. self.log_barcode( str(int(time.time())) + ": " + barcode_data )
  142. self.t_prv = self.t_now
  143. self.LAST_TOK = barcode_data
  144. elif (self.t_now - self.t_prv) >= self.RATE_LIMIT_MS:
  145. #self.barcode_ofp.write( str(int(time.time())) + ": " + barcode_data + "\n")
  146. #self.barcode_ofp.flush()
  147. self.log_barcode( str(int(time.time())) + ": " + barcode_data )
  148. self.t_prv = self.t_now
  149. self.LAST_TOK = barcode_data
  150. else:
  151. pass
  152. # convert cv2 image to something that tkinter can display
  153. #
  154. #self.image = cv2.cvtColor(self.cap.read()[1], cv2.COLOR_BGR2RGB)
  155. self.image = cv2.cvtColor(img_cv2, cv2.COLOR_BGR2RGB)
  156. self.image = cv2.flip(self.image, 1)
  157. self.image = Image.fromarray(self.image)
  158. self.image = ImageTk.PhotoImage(self.image)
  159. dy = int(self.screen_height - self.cap_height)
  160. dx = int((self.screen_width - self.cap_width)/2)
  161. dx -= 1
  162. if dx<0: dx=0
  163. midx = int(self.screen_width/2)
  164. _now = datetime.datetime.now()
  165. yyyymmdd = _now.strftime("%Y-%m-%d")
  166. time_str = _now.strftime("%a %I") + [":", " "][int(_now.strftime("%S"))%2] + _now.strftime("%M%p")
  167. char_width = 10
  168. #text_msg = [ "2021-06-01", "Tue 10:53AM", "See Driver", "No Passes", "on Card" ]
  169. text_msg = [ yyyymmdd, time_str, self.display_msg, self.display_msg_error ]
  170. text_msg_dy = [ 60, 80, 80, 60 , 0 ]
  171. self.canvas.delete("all")
  172. txt_dy = 80
  173. txt_x = midx
  174. txt_y = int(txt_dy/2)
  175. for idx,msg in enumerate(text_msg):
  176. font = None
  177. if idx==0:
  178. font = tkfont.Font(family="monospace", weight="bold", size=60)
  179. else:
  180. font = tkfont.Font(family="monospace", weight="bold", size=50)
  181. color = "#aaa"
  182. if idx == 2:
  183. color = "#6194fa"
  184. elif idx > 2:
  185. color = "#fa6171"
  186. self.canvas.create_text(txt_x, txt_y, anchor=tk.CENTER, text=msg, font=font, fill=color)
  187. #txt_y += txt_dy
  188. txt_y += text_msg_dy[idx]
  189. self.canvas.create_image(dx, dy, anchor=tk.NW, image=self.image)
  190. self.window.after(self.interval, self.update_loop)
  191. if __name__ == "__main__":
  192. root = tk.Tk()
  193. pistream = PiVideoStream().start()
  194. time.sleep(2.0)
  195. MainWindow(root, pistream)
  196. signal.signal(signal.SIGINT, lambda x,y : print('terminal ^C') or handler(None))
  197. root.after(500, check)
  198. root.bind_all('<Control-c>', handler)
  199. root.configure(bg='#000')
  200. root.mainloop()