/ Raspberry_Pi_Braincraft_Radio / setup-kiosk.sh
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