Skip to content

Commit 878f623

Browse files
committed
initial commit
0 parents  commit 878f623

15 files changed

+418
-0
lines changed

.xbindkeysrc

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
"left-click"
2+
b:1
3+
"middle-click"
4+
b:2
5+
"right-click"
6+
b:3

.xinitrc

+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
# Add all scripts to path
2+
PATH="/home/pi/bus-timetables:$PATH"
3+
timetable-server-proxy &
4+
# We are in sh, must use test builtin
5+
if [ -e /home/pi/kiosk-debug ]; then
6+
KIOSK_FLAG=""
7+
else
8+
KIOSK_FLAG="--kiosk"
9+
autoscroll &
10+
xbindkeys
11+
fi
12+
/usr/bin/chromium-browser $KIOSK_FLAG --disable-web-security --temp-profile \
13+
http://localhost:5000 \
14+
file:///home/pi/tab3.html

README.md

+99
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
# Setup
2+
3+
Clone this repo into the home directory of the pi, s.t. there is a directory
4+
called /home/pi/bus-timetables.
5+
6+
Create symlinks to `.xinitrc` and `.xbindkeysrc`:
7+
8+
```
9+
$ ln -s ~/bus-timetables/.xinitrc ~/.xinitrc
10+
$ ln -s ~/bus-timetables/.xbindkeysrc ~/.xbindkeysrc
11+
```
12+
13+
Add the following line to `/etc/profile` to launch the X server on startup, but
14+
not when logging in from SSH.
15+
16+
```
17+
if [[ -z $SSH_CONNECTION ]]; then xinit -- -nocursor; fi
18+
```
19+
20+
# How It Works
21+
22+
In essence, we launch an instance of chromium with the tab bar hidden. Each tab
23+
acts as a "screen", which renders a webpage that takes up the whole monitor when
24+
displayed. We define macros which automatically scroll through the tabs, giving
25+
the impression of cycling through screens, which are actually just webpages.
26+
27+
This way, each screen is a webpage on a tab in chrome - the system is built to
28+
make this not too obvious.
29+
30+
## Step by Step
31+
32+
At startup, the X server is run with xinit. This runs the commands in .xinitrc
33+
from our home directory when the X server is done initializing. Because we
34+
symlink'd ~/.xinitrc to ~/bus-timetables/.xinitrc in the setup, the .xinitrc in
35+
this repo gets run.
36+
37+
The .xinitrc starts by adding the bus-timetables repository to its path, so all
38+
of the scripts in the repository's top level are available to run.
39+
40+
Then it launches and forks off the Python server via `timetable-server-proxy &`
41+
that serves the bus timetable, and if we aren't in debug mode launches
42+
`xbindkeys` and `autoscroll`.
43+
44+
xbindkeys is a regular program that lets us bind mouse clicks and keyboard
45+
events to scripts via the ~/.xbindkeysrc file, which again is symlink'd to the
46+
file in our repo. In our case, the .xbindkeysrc file overrides the three mouse
47+
clicks (left, right, middle) to run the three scripts we've defined in the repo:
48+
left-click, right-click, and middle-click. These three buttons are rebound to
49+
run the next-tab and prev-tab scripts, which tells Chrome to cycle to the next
50+
tab or previous tab by sending it the Ctrl+tab or Ctrl+shift+tab keys
51+
respectively.
52+
53+
autoscroll is a script which cycles to the next-tab once every 10 seconds, again
54+
using the next-tab script. This keeps chrome cycling through screens.
55+
56+
Finally, the chrome browser is run. It is run in kiosk mode (unless debug mode
57+
is on), which means that it is full screened and has no tab list. We also
58+
disable web security (we assume we will only run our own websites with hand
59+
written code and inputs) in order to access the filesystem easily.
60+
61+
The chrome browser is handed a list of URLs (these can be localhost, or file
62+
urls, or anything else that it can open). Each URL will open in a separate tab,
63+
which will act as a "screen" that we can flick through with right or left
64+
clicking. Currently, chrome is pointed at the bus timetable server on
65+
localhost:5000 and the under construction page in ./under-construction
66+
67+
## The Bus Timetable Webpage
68+
69+
The bus timetable webpage itself is served from localhost:5000 by
70+
timetable-server-proxy. It consists of two parts:
71+
72+
- A Flask webserver, in proxy.py, which serves the index.html webpage and
73+
proxies the TfE API
74+
- A webpage, index.html, which requests the TfE API via the proxy, and renders
75+
it into a template using VueJS
76+
77+
# Development
78+
79+
## Adding a new screen
80+
81+
To add a new screen, add a new URL to the list of URLs that chromium opens at
82+
the end of `.xinitrc`, and make sure that URL resolves to the thing you'd like
83+
to show.
84+
85+
## Debug Mode
86+
87+
It can be annoying when debugging a new screen to have to contend with the
88+
automatic screen switcher, a missing cursor, and overridden right and left
89+
click. In order to disable these, you can create the file ~/kiosk-debug, which
90+
.xinitrc will check for.
91+
92+
Make sure to remove the file once you're done debugging!
93+
94+
# TODO
95+
96+
- We still need to make middle-click pause the autoscroll.
97+
- Showing some indicator of the time to next autoscroll would be nice.
98+
- The autoscroll should back off when the next/prev screen buttons have recently
99+
been clicked.

autoscroll

+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
#!/usr/bin/env bash
2+
sleep 5
3+
while sleep 20; do /home/pi/tab-right; done
4+
5+
# DOESNT WORK FOR SOME REASON LOL
6+
#touch lasttabmove
7+
#while sleep 0.5; do
8+
# LAST=$(date -d "$(stat lasttabmove | grep Change: | cut -d' ' -f2-)" +%s)
9+
# if [[ "$((LAST + 5000))" -gt "$(date +%s)" ]]; then
10+
# /home/pi/tab-right
11+
# fi
12+
#done

index.html

+231
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,231 @@
1+
<html>
2+
<head>
3+
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
4+
<style>
5+
* {
6+
font-size: 20px;
7+
line-height: 1.25;
8+
}
9+
body {
10+
padding: 0;
11+
margin: 0;
12+
color: ghostwhite;
13+
font-weight: bold;
14+
background-color: #222;
15+
}
16+
.main {
17+
height: 100vh;
18+
width: 100vw;
19+
box-sizing: border-box;
20+
border: 3px solid red;
21+
display: flex;
22+
flex-direction: column;
23+
}
24+
.header {
25+
display: flex;
26+
flex-direction: row;
27+
justify-content: center;
28+
width: 100%;
29+
}
30+
.stopTitle {
31+
font-weight: bold;
32+
}
33+
.stopTitle, .lastUpdated {
34+
padding-top: 1ch;
35+
}
36+
.lastUpdated {
37+
font-size: 18px;
38+
}
39+
.notUpToDate {
40+
color: red;
41+
}
42+
.serviceName {
43+
text-align: center;
44+
width: 5ch;
45+
}
46+
.destinationName {
47+
padding-right: 4ch;
48+
}
49+
</style>
50+
<script>
51+
let serviceColors = {};
52+
let stops = [
53+
];
54+
55+
const app = Vue.createApp({
56+
data () {
57+
return {
58+
stops: [
59+
{
60+
id: 6200240020,
61+
name: "Bernard Terrace, NW",
62+
services: [],
63+
lastUpdated: 0,
64+
},
65+
{
66+
id: 6200208550,
67+
name: "Bernard Terrace, SE (1)",
68+
services: [],
69+
lastUpdated: 0,
70+
},
71+
{
72+
id: 6200208580,
73+
name: "Bernard Terrace, SE (2)",
74+
services: [],
75+
lastUpdated: 0,
76+
},
77+
{
78+
id: 6200243440,
79+
name: "Buccleuch Terrace, NW",
80+
services: [],
81+
lastUpdated: 0,
82+
},
83+
{
84+
id: 6200206460,
85+
name: "Buccleuch Terrace, SE",
86+
services: [],
87+
lastUpdated: 0,
88+
},
89+
],
90+
stopIndices: {
91+
6200240020: 0,
92+
6200208550: 1,
93+
6200208580: 2,
94+
6200243440: 3,
95+
6200206460: 4,
96+
},
97+
time: "",
98+
timestamp: 0,
99+
};
100+
},
101+
102+
methods: {
103+
setStop (id, data) {
104+
this.stops[this.stopIndices[id]].services = data;
105+
this.stops[this.stopIndices[id]].lastUpdated = Date.now();
106+
},
107+
108+
setTime (timestamp, timeString) {
109+
this.time = timeString;
110+
this.timestamp = timestamp;
111+
}
112+
}
113+
});
114+
115+
async function updateAllFromAPI() {
116+
for (let stop of mounted.stops) {
117+
loadStop(stop.id).then(data => {
118+
if (data != null) mounted.setStop(stop.id, data)
119+
});
120+
}
121+
}
122+
123+
async function loadColors () {
124+
let res = await fetch(`/api/colors`);
125+
let json = await res.json();
126+
for (let route of json.routes) {
127+
serviceColors[route.name] = {
128+
bg: route.color,
129+
fg: route.textColor,
130+
};
131+
}
132+
}
133+
134+
async function loadStop (stop) {
135+
let res;
136+
try {
137+
res = await fetch(`/api/stop/${stop}`);
138+
} catch (e) {
139+
return null;
140+
}
141+
let json = await res.json();
142+
143+
if (res.status != 200) return null;
144+
145+
let services = {};
146+
147+
function mkService(service, destination) {
148+
if (services[service] == null) {
149+
services[service] = {
150+
name: service,
151+
destinations: {},
152+
...serviceColors[service]
153+
};
154+
}
155+
156+
if (services[service].destinations[destination] == null) {
157+
services[service].destinations[destination] = {
158+
name: destination,
159+
times: []
160+
};
161+
}
162+
163+
return services[service].destinations[destination].times;
164+
}
165+
166+
for (let service of json.services) {
167+
for (let departure of service.departures) {
168+
let busList = mkService(service.service_name, departure.destination);
169+
let departureString = "ERR";
170+
if (departure.minutes > 60) {
171+
let departureTime = new Date(Date.now() + departure.minutes * 60 * 1000);
172+
departureString = departureTime.toLocaleTimeString().slice(0, -3)
173+
} else if (departure.minutes > 0) {
174+
departureString = departure.minutes.toString() + "m";
175+
} else {
176+
departureString = "DUE";
177+
}
178+
busList.push(departureString);
179+
}
180+
}
181+
182+
return services;
183+
}
184+
185+
window.onload = async () => {
186+
setInterval(() => {
187+
let timestamp = Date.now();
188+
let dateString = (new Date()).toLocaleString();
189+
mounted.setTime(timestamp, dateString);
190+
}, 100);
191+
192+
await loadColors();
193+
await updateAllFromAPI(stops);
194+
setInterval(() => {
195+
updateAllFromAPI(stops);
196+
}, 60000);
197+
}
198+
</script>
199+
</head>
200+
<body>
201+
<div id="app">
202+
<div class="header">
203+
<span id="time">{{time}}</span>
204+
</div>
205+
<div class="busLists">
206+
<table class="services">
207+
<template v-for="stop in stops">
208+
<tr>
209+
<td class="stopTitle" colspan="2">{{ stop.name }}</td>
210+
<td class="lastUpdated" colspan="1" :class="{ notUpToDate: (timestamp - stop.lastUpdated) > 70000 }">
211+
(Last updated {{ Math.floor((timestamp - stop.lastUpdated) / 60000) }}m ago)
212+
</td>
213+
</tr>
214+
<template v-for="service of stop.services">
215+
<template v-for="destination of service.destinations">
216+
<tr>
217+
<td class="serviceName" :style="{ backgroundColor: service.bg, color: service.fg }">{{ service.name }}</td>
218+
<td class="destinationName">{{ destination.name }}</td>
219+
<td class="destinationTimes" :class="{ notUpToDate: (timestamp - stop.lastUpdated) > 70000 }">{{ destination.times.join(', ') }}</td>
220+
</tr>
221+
</template>
222+
</template>
223+
</template>
224+
</table>
225+
</div>
226+
</div>
227+
<script>
228+
const mounted = app.mount('#app');
229+
</script>
230+
</body>
231+
</html>

left-click

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
# on left click move left one tab
2+
/home/pi/tab-left

middle-click

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
BROWSER_WINDOW=$(xdotool search --class '^chromium-browser$')
2+
# on middle click, pause autoscroll

right-click

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
# on right click move right one tab
2+
/home/pi/tab-right

tab-left

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
# move left one tab
2+
BROWSER_WINDOW=$(xdotool search --class '^chromium-browser$')
3+
xdotool key --window $BROWSER_WINDOW ctrl+Shift+Tab
4+
touch lasttabmove

tab-right

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
# move right one tab
2+
BROWSER_WINDOW=$(xdotool search --class '^chromium-browser$')
3+
xdotool key --window $BROWSER_WINDOW ctrl+Tab
4+
touch lasttabmove

0 commit comments

Comments
 (0)