Various small changes; fix up the camera trigger, improve logging,
[kiosk.git] / kiosk.py
1 #!/usr/local/bin/python3.7
2
3 import sys
4 import traceback
5 import os
6 from threading import Thread
7 import time
8 from datetime import datetime
9 import constants
10 import renderer
11 import renderer
12 import renderer_catalog
13 import chooser
14 import logging
15 import trigger_catalog
16 import utils
17
18 def thread_change_current():
19     page_chooser = chooser.weighted_random_chooser_with_triggers(
20         trigger_catalog.get_triggers())
21     swap_page_target = 0
22     last_page = ""
23     while True:
24         now = time.time()
25         (page, triggered) = page_chooser.choose_next_page()
26
27         if triggered:
28             print('chooser[%s] - WE ARE TRIGGERED.' % utils.timestamp())
29             if page != last_page:
30                 print('chooser[%s] - EMERGENCY PAGE %s LOAD NEEDED' % (
31                     utils.timestamp(), page))
32                 try:
33                     f = open(os.path.join(constants.pages_dir,
34                                           'current.shtml'), 'w')
35                     emit_wrapped(f, page)
36                     f.close()
37                 except:
38                     print('chooser[%s] - page does not exist?!' % (
39                         utils.timestamp()))
40                     continue
41                 last_page = page
42                 swap_page_target = now + constants.refresh_period_sec
43
44                 # Also notify XMLHTTP clients that they need to refresh now.
45                 path = os.path.join(constants.pages_dir,
46                                     'reload_immediately.html')
47                 f = open(path, 'w')
48                 f.write('Reload, suckers!')
49                 f.close()
50
51                 # Fix this hack... maybe read the webserver logs and see if it
52                 # actually was picked up?
53                 time.sleep(0.750)
54                 os.remove(path)
55
56         elif now >= swap_page_target:
57             if (page == last_page):
58                 print(('chooser[%s] - nominal choice got the same page...' % (
59                     utils.timestamp())))
60                 continue
61             print('chooser[%s] - nominal choice of %s' % (utils.timestamp(), page))
62             try:
63                 f = open(os.path.join(constants.pages_dir,
64                                       'current.shtml'), 'w')
65                 emit_wrapped(f, page)
66                 f.close()
67             except:
68                 print('chooser[%s] - page does not exist?!' % (utils.timestamp()))
69                 continue
70             last_page = page
71             swap_page_target = now + constants.refresh_period_sec
72         time.sleep(1)
73
74 def pick_background_color():
75     now = datetime.now()
76     if now.hour <= 6 or now.hour >= 21:
77         return "E6B8B8"
78     elif now.hour == 7 or now.hour == 20:
79         return "EECDCD"
80     else:
81         return "FFFFFF"
82
83 def emit_wrapped(f, filename):
84     age = utils.describe_age_of_file_briefly("pages/%s" % filename)
85     bgcolor = pick_background_color()
86     f.write("""
87 <HEAD>
88   <TITLE>Kitchen Kiosk</TITLE>
89   <LINK rel="stylesheet" type="text/css" href="style.css">
90   <SCRIPT TYPE="text/javascript">
91
92   // Zoom the 'contents' div to fit without scrollbars and then make
93   // it visible.
94   function zoomScreen() {
95     z = 285;
96     do {
97       document.getElementById("content").style.zoom = z+"%%";
98       var body = document.body;
99       var html = document.documentElement;
100       var height = Math.max(body.scrollHeight,
101                             body.offsetHeight,
102                             html.clientHeight,
103                             html.scrollHeight,
104                             html.offsetHeight);
105       var windowHeight = window.innerHeight;
106       var width = Math.max(body.scrollWidth,
107                            body.offsetWidth,
108                            html.clientWidth,
109                            html.scrollWidth,
110                            html.offsetWidth);
111       var windowWidth = window.innerWidth;
112       var heightRatio = height / windowHeight;
113       var widthRatio = width / windowWidth;
114
115       if (heightRatio <= 1.0 && widthRatio <= 1.0) {
116         break;
117       }
118       z -= 4;
119     } while(z >= 70);
120     document.getElementById("content").style.visibility = "visible";
121   }
122
123   // Load IMG tags with DATA-SRC attributes late.
124   function lateLoadImages() {
125     var image = document.getElementsByTagName('img');
126     for (var i = 0; i < image.length; i++) {
127       if (image[i].getAttribute('DATA-SRC')) {
128         image[i].setAttribute('SRC', image[i].getAttribute('DATA-SRC'));
129       }
130     }
131   }
132
133   // Operate the clock at the top of the page.
134   function runClock() {
135     var today = new Date();
136     var h = today.getHours();
137     var ampm = h >= 12 ? 'pm' : 'am';
138     h = h %% 12;
139     h = h ? h : 12; // the hour '0' should be '12'
140     var m = maybeAddZero(today.getMinutes());
141     var colon = ":";
142     if (today.getSeconds() %% 2 == 0) {
143       colon = "<FONT STYLE='color: #%s; font-size: 4vmin; font-weight: bold'>:</FONT>";
144     }
145     document.getElementById("time").innerHTML = h + colon + m + ampm;
146     document.getElementById("date").innerHTML = today.toDateString();
147     var t = setTimeout(function(){runClock()}, 1000);
148   }
149
150   // Helper method for running the clock.
151   function maybeAddZero(x) {
152     return (x < 10) ? "0" + x : x;
153   }
154
155   // Do something on page load.
156   function addLoadEvent(func) {
157     var oldonload = window.onload;
158     if (typeof window.onload != 'function') {
159       window.onload = func;
160     } else {
161       window.onload = function() {
162         if (oldonload) {
163           oldonload();
164         }
165         func();
166       }
167     }
168   }
169
170   // Sleep thread helper.
171   const sleep = (milliseconds) => {
172     return new Promise(resolve => setTimeout(resolve, milliseconds))
173   }
174
175   var loaded = false;
176   var loadedDate = new Date();
177
178   addLoadEvent(zoomScreen);
179   addLoadEvent(runClock);
180   addLoadEvent(lateLoadImages);
181   addLoadEvent(function() {
182     loaded = true;
183   });
184
185   // Reload the page after a certain amount of time has passed or
186   // immediately if told to do so.
187   (function poll() {
188     setTimeout(
189       function() {
190         var now = new Date();
191         var deltaMs = now.getTime() - loadedDate.getTime();
192
193         // Reload unconditionally after 22 sec.
194         if (deltaMs > %d) {
195           window.location.reload();
196         }
197
198         // Reload immediately if told.
199         var xhr = new XMLHttpRequest();
200         xhr.open('GET',
201                  'http://wannabe.house/kiosk/pages/reload_immediately.html');
202         xhr.onload =
203           function() {
204             if (xhr.status === 200) {
205               window.location.reload();
206             } else {
207               sleep(500).then(() => {
208                 poll();
209               });
210             }
211           };
212         xhr.send();
213       }, 500);
214   })();
215 </SCRIPT>
216 </HEAD>
217 <BODY BGCOLOR="#%s">
218     <TABLE style="height:100%%; width:100%%" BORDER=0>
219     <TR HEIGHT=30>
220         <TD ALIGN="left">
221             <DIV id="date">&nbsp;</DIV>
222         </TD>
223         <TD ALIGN="center"><FONT COLOR=#bbbbbb>
224             <DIV id="info"></DIV></FONT>
225         </TD>
226         <TD ALIGN="right">
227             <DIV id="time">&nbsp;</DIV>
228         </TD>
229     </TR>
230     <TR STYLE="vertical-align:top">
231         <TD COLSPAN=3>
232             <DIV ID="content" STYLE="zoom: 1; visibility: hidden;">
233                 <!-- BEGIN main page contents. -->
234 <!--#include virtual=\"%s\"-->
235                 <!-- END main page contents. -->
236             </DIV>
237             <BR>
238             <P ALIGN="right">
239                 <FONT SIZE=2 COLOR=#bbbbbb>%s @ %s ago.</FONT>
240             </P>
241         </TD>
242     </TR>
243     </TABLE>
244 </BODY>""" % (bgcolor,
245               constants.refresh_period_sec * 1000,
246               bgcolor,
247               filename,
248               filename,
249               age))
250
251 def thread_invoke_renderers():
252     while True:
253         print("renderer[%s]: invoking all renderers in catalog..." % (
254             utils.timestamp()))
255         for r in renderer_catalog.get_renderers():
256             now = time.time()
257             try:
258                 r.render()
259             except Exception as e:
260                 traceback.print_exc()
261                 print("renderer[%s] unknown exception in %s, swallowing it." % (
262                     utils.timestamp(), r.get_name()))
263             except Error as e:
264                 traceback.print_exc()
265                 print("renderer[%s] unknown error in %s, swallowing it." % (
266                     utils.timestamp(), r.get_name()))
267             delta = time.time() - now
268             if (delta > 1.0):
269                 print("renderer[%s]: Warning: %s's rendering took %5.2fs." % (
270                     utils.timestamp(), r.get_name(), delta))
271         print("renderer[%s]: thread having a little break for %ds..." % (
272             utils.timestamp(), constants.render_period_sec))
273         time.sleep(constants.render_period_sec)
274
275 if __name__ == "__main__":
276     logging.basicConfig()
277     changer_thread = None
278     renderer_thread = None
279     while True:
280         if (changer_thread == None or
281             not changer_thread.is_alive()):
282             print("MAIN[%s] - (Re?)initializing chooser thread..." % (
283                 utils.timestamp()))
284             changer_thread = Thread(target = thread_change_current, args=())
285             changer_thread.start()
286         if (renderer_thread == None or
287             not renderer_thread.is_alive()):
288             print("MAIN[%s] - (Re?)initializing render thread..." % (
289                 utils.timestamp()))
290             renderer_thread = Thread(target = thread_invoke_renderers, args=())
291             renderer_thread.start()
292         time.sleep(60)
293     print("Should never get here.")