setup-kiosk.sh
  1  # Inspired by https://pimylifeup.com/raspberry-pi-kiosk/
  2  # Start with a raspberry pi "full" installation that boots to desktop and
  3  # can run chromium browser.
  4  #
  5  # Follow braincraft setup instructions sections:
  6  #  - raspberry pi setup
  7  #  - blinka setup
  8  #  - audio setup
  9  #  - fan service setup (optional)
 10  #
 11  # Now run this script, restart, and satisfy yourself that the player works
 12  # properly using a HDMI monitor.  Troubleshooting is easier at this point!
 13  #
 14  # Now run the braincraft setup setps:
 15  #  - display module install
 16  #  - enable "overlay mode" in raspi-config so that it's safe to just power off
 17  #    the pi anytime
 18  #
 19  # Before working with the system, remember to
 20  #  - disable "overlay mode"
 21  #  - optionally disable the display module to get a regular desktop back
 22  
 23  cd $HOME
 24  
 25  sudo apt-get install -y xdotool unclutter sed
 26  cat > kiosk.sh <<EOF
 27  #!/bin/bash
 28  set -x
 29  DISPLAY=:0; export DISPLAY
 30  xset s noblank
 31  xset s off
 32  xset -dpms
 33  unclutter -idle 0.5 -root &
 34  sudo python3 /home/pi/braincraftKeys.py &
 35  while true; do
 36      # Set speaker volume down -7dB (~40% volume)
 37      amixer -c 2 -- sset Speaker -7dB
 38      amixer -c 2 -- sset Headphone -7dB
 39  
 40      # Wait for network to come up
 41      while ! ping -c 1 youtube.com; do sleep 1; done
 42  
 43      sed -i 's/"exited_cleanly":false/"exited_cleanly":true/' /home/pi/.config/chromium/Default/Preferences
 44      sed -i 's/"exit_type":"Crashed"/"exit_type":"Normal"/' /home/pi/.config/chromium/Default/Preferences
 45  
 46      /usr/bin/chromium-browser --autoplay-policy=no-user-gesture-required --noerrdialogs --disable-infobars --kiosk --remote-debugging-port=9992 file:///home/pi/kioskvideo.html &
 47      sleep 15
 48      xdotool keydown f; xdotool keyup f
 49      wait %%
 50  done
 51  EOF
 52  chmod +x kiosk.sh
 53  
 54  cat > braincraftKeys.py <<"EOF"
 55  #!/usr/bin/env python3
 56  # Credit to Pimoroni for Picade HAT scripts as starting point.
 57  
 58  import time
 59  import signal
 60  import os
 61  import sys
 62  from datetime import datetime
 63  from collections import deque
 64  import re
 65  import fileinput
 66  
 67  try:
 68      from evdev import uinput, UInput, ecodes as e
 69  except ImportError:
 70      exit("This library requires the evdev module\nInstall with: sudo pip install evdev")
 71  
 72  import digitalio
 73  import board
 74  
 75  def detect_rotation():
 76      rotation_pattern = "^dtoverlay=drm-minipitft13,rotation=([0-9]+)"
 77      hdmi_pattern = "^display_hdmi_rotate=([0-9])"
 78      param_rotation = None
 79      param_hdmi = None
 80      for line in fileinput.FileInput("/boot/config.txt"):
 81          rotation_match = re.search(rotation_pattern, line)
 82          hdmi_match = re.search(hdmi_pattern, line)
 83          if rotation_match:
 84              param_rotation = int(rotation_match.group(1))
 85          if hdmi_match:
 86              param_hdmi = int(hdmi_match.group(1))
 87          if param_rotation is not None and param_hdmi is not None:
 88              break
 89      if param_hdmi == 3:
 90          return 180
 91      if param_hdmi == 2:
 92          return 270
 93      if param_hdmi == 1:
 94          return 0
 95      return param_rotation
 96  
 97  arrow_pins = deque((
 98      board.D23, # up
 99      board.D24, # right
100      board.D27, # down
101      board.D22, # left
102  ))
103  
104  arrow_pins.rotate((detect_rotation() // 90) - 1)
105  
106  DEBUG = False
107  BOUNCE_TIME = 0.01 # Debounce time in seconds
108  POWEROFF_TIMEOUT = 5
109  
110  KEYS= [ # EDIT KEYCODES IN THIS TABLE TO YOUR PREFERENCES:
111  	# See /usr/include/linux/input.h for keycode names
112  	# Keyboard  Action (tuple = press together, no repeat)
113      (board.D17, (e.KEY_K,)), # button - play/pause
114      (board.D16, (e.KEY_LEFTCTRL, e.KEY_R)), # push stick - reload
115  	(arrow_pins[3], (e.KEY_LEFTSHIFT, e.KEY_P)), # left - previous in playlist
116  	(arrow_pins[1], (e.KEY_LEFTSHIFT, e.KEY_N)),  # right - next in playlist
117  	(arrow_pins[0], e.KEY_EQUAL),        # up - volume up
118  	(arrow_pins[2], e.KEY_MINUS),        # down - volume down
119  ]
120  
121  key_values = set()
122  for pin, action in KEYS:
123      if isinstance(action, int):
124          key_values.add(action)
125      else:
126          key_values.update(action)
127  
128  class KeyManager:
129      def __init__(self, pin, action):
130          self.pin = digitalio.DigitalInOut(pin)
131          self.pin.switch_to_output(digitalio.Pull.UP)
132          self.action = action
133          self.old_value = False
134  
135      @property
136      def value(self):
137          return not self.pin.value  # buttons are active-low
138  
139      def tick(self):
140          value = self.value
141          if value != self.old_value:
142              if isinstance(self.action, int):
143                  ui.write(e.EV_KEY, self.action, value)
144                  ui.syn()
145              elif value:
146                  for key in self.action:
147                      ui.write(e.EV_KEY, key, 1)
148                      ui.syn()
149                  for key in self.action[::-1]:
150                      ui.write(e.EV_KEY, key, 0)
151                      ui.syn()
152          self.old_value = value
153  
154  class ShutdownManager:
155      def __init__(self, manager):
156          self.manager = manager
157          self.old_value = False
158          self.press_start = 0
159  
160      @property
161      def value(self):
162          return self.manager.value
163  
164      def tick(self):
165          now = time.time()
166          value = self.value
167          if value:
168              if self.old_value:
169                  if now > self.press_start + POWEROFF_TIMEOUT:
170                      os.system("env DISPLAY=:0 XAUTHORITY=/home/pi/.Xauthority xset dpms force off")
171                      os.system("sudo poweroff")
172              else:
173                  self.press_start = now
174          self.old_value = value
175  
176  managers = [KeyManager(k, v) for k, v in KEYS]
177  managers.append(ShutdownManager(managers[0]))
178  
179  os.system("sudo modprobe uinput")
180  
181  try:
182      ui = UInput({e.EV_KEY: key_values}, name="braincraft-hat", bustype=e.BUS_USB)
183  except uinput.UInputError as e:
184      sys.stdout.write(repr(e))
185      sys.stdout.write("Have you tried running as root? sudo {}".format(sys.argv[0]))
186      sys.exit(0)
187  
188  def log(msg):
189      sys.stdout.write(str(datetime.now()))
190      sys.stdout.write(": ")
191      sys.stdout.write(msg)
192      sys.stdout.write("\n")
193      sys.stdout.flush()
194  
195  while True:
196      for manager in managers:
197          manager.tick()
198      time.sleep(BOUNCE_TIME)
199  EOF
200  chmod +x braincraftKeys.py
201  
202  cat > kiosk.service <<EOF
203  [Unit]
204  Description=Chromium Kiosk
205  Wants=graphical.target
206  After=graphical.target
207  
208  [Service]
209  Environment=DISPLAY=:0.0
210  Environment=XAUTHORITY=/home/pi/.Xauthority
211  Type=simple
212  ExecStart=/bin/bash /home/pi/kiosk.sh
213  Restart=on-abort
214  User=pi
215  Group=pi
216  
217  [Install]
218  WantedBy=graphical.target
219  EOF
220  
221  sudo mv kiosk.service /etc/systemd/system/
222  sudo systemctl enable kiosk.service
223  sudo systemctl start kiosk.service
224  
225  cat > kioskvideo.html <<"EOF"
226  <html>
227  <head>
228  <title>Braincraft Hat - lofi hip hop radio - betas to relax/study to</title>
229  <!--
230  Embed the youtube video for best viewing on the square (virtual 480x480)
231  Adafruit Braincraft Hat and react to (virtual) keystrokes to control playback
232  
233  https://developers.google.com/youtube/iframe_api_reference
234  -->
235  </head>
236  <body>
237  <div style="position:absolute; top:0px; left:0px; width:480px; height:480px; overflow:hidden">
238  <iframe id="player" type="text/html" width="854" height="480" style="margin-left: -68px" src="http://www.youtube.com/embed/5qap5aO4i9A?enablejsapi=1&autoplay=1" frameborder="0" allow="autoplay"></iframe></div>
239  <div style="height:480px"></div>
240  NOTE: Chromium must be launched with --autoplay-policy=no-user-gesture-required or the video will not autoplay and the play/pause keystrokes will not work properly.  Clicking in the video will also affect keystroke functionality!
241  <script type="text/javascript">
242    // playlist[0] needs to match the initial iframe src to work right
243    var playlist = [
244      '5qap5aO4i9A', // study
245      'DWcJFNfaw9c', // sleep
246      '5yx6BWlEVcY', // jazzy
247    ]
248    // in unboxed mode, pan to a different horizontal position depending on video
249    var pan = [
250      -68, -208, -187
251    ]
252    var playlist_idx = 0;
253    var tag = document.createElement('script');
254    tag.id = 'iframe-demo';
255    tag.src = 'https://www.youtube.com/iframe_api';
256    var firstScriptTag = document.getElementsByTagName('script')[0];
257    firstScriptTag.parentNode.insertBefore(tag, firstScriptTag);
258  
259    var player;
260    var unboxed=true;
261    function onYouTubeIframeAPIReady() {
262      player = new YT.Player('player', {
263          events: {
264            'onReady': onPlayerReady,
265          }
266      });
267    }
268    function onPlayerReady(event) {
269      event.target.setVolume(40);
270    }
271  
272    function fixPan() {
273      if(unboxed) {
274          document.getElementById('player').width=854
275          document.getElementById('player').style="margin-left:"+pan[playlist_idx]+"px"
276      } else {
277          document.getElementById('player').width=480
278          document.getElementById('player').style=""
279      }
280    }
281    document.addEventListener('keydown', (event) => {
282      if (event.ctrlKey || event.metaKey || event.altKey) {
283          return
284      }
285  
286      switch(event.key) {
287      case 'k': // toggle play-pause
288          if(player.getPlayerState() == 1) {
289              console.log("pause")
290              player.pauseVideo()
291          } else {
292              player.playVideo()
293              console.log("play")
294          }
295          break;
296  
297      case 'p': // unconditional pause
298          player.pauseVideo()
299          break;
300  
301      case '[': // previous video in playlist
302      case 'P': // previous video in playlist
303          playlist_idx = (playlist_idx + playlist.length - 1) % playlist.length
304          player.loadVideoById({'videoId' : playlist[playlist_idx]});
305          fixPan()
306          break;
307  
308      case ']': // previous video in playlist
309      case 'N': // next video in playlist
310          playlist_idx = (playlist_idx + 1) % playlist.length
311          player.loadVideoById({'videoId' : playlist[playlist_idx]});
312          fixPan()
313          break;
314  
315      case '+': // volume up
316      case '=': // volume up
317          player.setVolume(Math.min(100, player.getVolume() + 10));
318          break;
319  
320      case '-': // volume down
321          player.setVolume(Math.max(0, player.getVolume() - 10));
322          break;
323  
324      case 'm': // toggle mute
325          if(player.isMuted()) {
326              player.unMute()
327          } else {
328              player.mute()
329          }
330          break;
331  
332      case 'l':
333          unboxed = !unboxed;
334          fixPan()
335      }
336  
337      
338    }, false)
339  </script>
340  </body>
341  EOF