Skip to content

Commit 396e029

Browse files
author
Manuel Zomer
committed
Add Proactive System
1 parent da1fd34 commit 396e029

13 files changed

+970
-1
lines changed

README.md

+13-1
Original file line numberDiff line numberDiff line change
@@ -1 +1,13 @@
1-
# proactivity
1+
# Proactive System
2+
3+
This repository features the source code and 3D designs for the *proactive system* developed as part of my thesis project **Shaping proactivity - Designing interactions with proactive ambient artefacts**.
4+
5+
It includes following parts:
6+
- Backend system to be used on the server side
7+
- Arduino sketch for the smart coaster
8+
- Arduino sketch for the tabletop robot
9+
- STL files to 3D print the coaster enclosure
10+
- STL files to 3D print the tabletop robot
11+
12+
13+
Note: in order to run the system a credentials.json file for the Google Calendar API has to be provided.

backend/package.json

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"dependencies": {
3+
"express": "^4.17.1",
4+
"googleapis": "^39.2.0"
5+
}
6+
}

backend/server.js

+331
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,331 @@
1+
const fs = require('fs')
2+
const readline = require('readline')
3+
const {google} = require('googleapis')
4+
const https = require('https')
5+
const express = require('express')
6+
var app = express()
7+
8+
const LAMPAPICODE = 'INSERT API CODE HERE'
9+
const STARTOFWORK = 8
10+
const ENDOFWORK = 18
11+
const REACTIONHOURS = 3
12+
const SCOPES = ['https://www.googleapis.com/auth/calendar.readonly']
13+
const TOKEN_PATH = 'token.json'
14+
let calendarObjects = {}
15+
let eventList = {}
16+
let stressLevel = 0
17+
let timeToNextEvent = -1
18+
let standbyTime = 0
19+
let eyeBrightness = 120
20+
let lampState = 0
21+
let brightnessLevel = 0.5
22+
let oldBrightnessLevel = 0.5
23+
24+
fs.readFile('credentials.json', (err, content) => {
25+
if (err) return console.log('Error loading client secret file:', err)
26+
27+
let userId = "user_01"
28+
authorize(JSON.parse(content),userId,getCalendarObject)
29+
})
30+
31+
function authorize(credentials, userId, callback) {
32+
const {client_secret, client_id, redirect_uris} = credentials.installed
33+
const oAuth2Client = new google.auth.OAuth2(
34+
client_id, client_secret, redirect_uris[0])
35+
36+
// Check if we have previously stored a token.
37+
fs.readFile(userId + "-" + TOKEN_PATH, (err, token) => {
38+
if (err) return getAccessToken(oAuth2Client, userId, callback)
39+
oAuth2Client.setCredentials(JSON.parse(token))
40+
callback(oAuth2Client,userId)
41+
})
42+
}
43+
44+
function getAccessToken(oAuth2Client, userId, callback) {
45+
const authUrl = oAuth2Client.generateAuthUrl({
46+
access_type: 'offline',
47+
scope: SCOPES,
48+
})
49+
console.log('Authorize this app by visiting this url:', authUrl)
50+
const rl = readline.createInterface({
51+
input: process.stdin,
52+
output: process.stdout,
53+
})
54+
rl.question('Enter the code from that page here: ', (code) => {
55+
rl.close();
56+
oAuth2Client.getToken(code, (err, token) => {
57+
if (err) return console.error('Error retrieving access token', err)
58+
oAuth2Client.setCredentials(token)
59+
// Store the token to disk for later program executions
60+
fs.writeFile(userId + "-" + TOKEN_PATH, JSON.stringify(token), (err) => {
61+
if (err) return console.error(err)
62+
console.log('Token stored to', userId + "-" + TOKEN_PATH)
63+
})
64+
callback(oAuth2Client,userId)
65+
})
66+
})
67+
}
68+
69+
function getCalendarObject(auth, userId) {
70+
calendarObjects[userId] = google.calendar({version: 'v3', auth})
71+
startScript(userId)
72+
}
73+
74+
async function getEventData(userId, email) {
75+
let today = new Date()
76+
let endTime = new Date()
77+
endTime.setHours(endTime.getHours()+REACTIONHOURS,0,0,0)
78+
let upcomingEventsOfUser = []
79+
80+
let err, res = await calendarObjects[userId].events.list({
81+
calendarId: 'primary',
82+
timeMin: today.toISOString(),
83+
timeMax: endTime.toISOString(),
84+
maxResults: 100,
85+
singleEvents: true,
86+
orderBy: 'startTime',
87+
})
88+
if (err) return console.log('The API returned an error: ' + err)
89+
const events = res.data.items
90+
if (events.length) {
91+
events.map((event, i) => {
92+
const start = event.start.dateTime || event.start.date
93+
const end = event.end.dateTime || event.end.date
94+
let dateStampEventStart = new Date(start)
95+
let dateStampEventEnd = new Date(end)
96+
let eventDuration = Math.floor((dateStampEventEnd.getTime() - dateStampEventStart.getTime())/1000/60)
97+
let accepted = 'no_choice_made'
98+
99+
if (event.attendees && event.attendees.length > 0) {
100+
event.attendees.forEach(attendee => {
101+
if (attendee.email == email || attendee.self) {
102+
accepted = attendee.responseStatus
103+
}
104+
})
105+
}
106+
107+
upcomingEventsOfUser.push({
108+
title: event.summary,
109+
start: dateStampEventStart,
110+
end: dateStampEventEnd,
111+
duration: eventDuration,
112+
attendees: event.attendees ? event.attendees.length : 0,
113+
isOnline: event.conferenceData ? true : false,
114+
isOrganizer: (event.organizer && event.organizer.email && event.organizer.email == email) || (event.organizer && event.organizer.self) ? true : false,
115+
accepted: accepted,
116+
})
117+
})
118+
} else {
119+
console.log('No upcoming events found.')
120+
}
121+
122+
eventList[userId] = upcomingEventsOfUser
123+
}
124+
125+
function calculateStressLevel(userId) {
126+
let totalDuration = 0
127+
let tmpTime = new Date()
128+
let endOfReactionTimestamp = new Date()
129+
endOfReactionTimestamp.setHours(endOfReactionTimestamp.getHours()+REACTIONHOURS)
130+
let foundReminder = false
131+
let tmpTimeToNextEvent = -1
132+
eventList[userId].forEach(event => {
133+
if (tmpTimeToNextEvent < 0) {
134+
tmpTimeToNextEvent = Math.floor((event.start.getTime() - tmpTime.getTime())/1000/60)
135+
}
136+
if (event.end.getTime()<=endOfReactionTimestamp.getTime() && event.start.getTime() >= tmpTime.getTime()) {
137+
// event is completely within the reaction time -> count entire event
138+
//duration in min
139+
console.log("full event found " + event.title)
140+
totalDuration+=event.duration
141+
}
142+
else if (event.end.getTime() > endOfReactionTimestamp.getTime()) {
143+
console.log("partial event found in reactiontime " + event.title)
144+
totalDuration+=Math.floor((endOfReactionTimestamp.getTime() - event.start.getTime())/1000/60)
145+
}
146+
else {
147+
console.log("already ongoing event found " + event.title)
148+
totalDuration+=Math.floor((event.end.getTime() - tmpTime.getTime())/1000/60)
149+
}
150+
151+
let differenceStarting = (event.start.getTime() - tmpTime.getTime())/1000
152+
console.log("test starting " + differenceStarting)
153+
if (differenceStarting >= 450 && differenceStarting <= 600) {
154+
console.log("Found event reminder")
155+
foundReminder = true
156+
}
157+
})
158+
159+
if (tmpTimeToNextEvent < 0) {
160+
tmpTimeToNextEvent = -1
161+
}
162+
let date = new Date()
163+
let secondsUntilEndOfWorkDay = REACTIONHOURS*60*60
164+
if (date.getHours() >= ENDOFWORK) {
165+
return [-1,tmpTimeToNextEvent]
166+
}
167+
if (date.getHours() < STARTOFWORK) {
168+
return [-1,tmpTimeToNextEvent]
169+
}
170+
return [Math.floor(totalDuration*60/secondsUntilEndOfWorkDay*100) > 0 ? Math.floor(totalDuration*60/secondsUntilEndOfWorkDay*100) : 0, tmpTimeToNextEvent]
171+
}
172+
173+
async function startScript(userId) {
174+
let email = await getUserEmail(userId)
175+
176+
setInterval(async function(){
177+
await getEventData(userId, email)
178+
let newValuesCalendar = calculateStressLevel(userId)
179+
180+
stressLevel = newValuesCalendar[0]
181+
timeToNextEvent = newValuesCalendar[1]
182+
if (stressLevel < 0) {
183+
standbyTime = 1
184+
}
185+
else {
186+
standbyTime = 0
187+
}
188+
189+
console.log("Current stress level: " + stressLevel)
190+
console.log("Time to next event: " + timeToNextEvent)
191+
console.log("Outside working hours: " + standbyTime)
192+
193+
if (standbyTime) {
194+
lampState = -1
195+
setLightbulbOff()
196+
}
197+
else if (timeToNextEvent <= 5 && timeToNextEvent >= 0) {
198+
// NOTIFICATION (250 = BLUE)
199+
lampState = 250
200+
setLightbulbHSL(lampState,1,brightnessLevel,2.0)
201+
}
202+
else {
203+
if (stressLevel > 100) {
204+
stressLevel = 100
205+
}
206+
// 100 = GREEN; 0 = RED
207+
lampState = 100-stressLevel
208+
setLightbulbHSL(lampState,1,brightnessLevel,2.0)
209+
}
210+
oldBrightnessLevel = brightnessLevel
211+
}, 30000)
212+
}
213+
214+
async function getUserEmail(userId) {
215+
let err, res = await calendarObjects[userId].calendarList.list({})
216+
if (err) return console.log('The API returned an error: ' + err)
217+
const cal = res.data.items
218+
if (cal.length) {
219+
console.log("user:" + userId + " // email:" + cal[0].id)
220+
return cal[0].id
221+
}
222+
return null
223+
}
224+
225+
function setLightbulbHSL(h, s, l, duration) {
226+
const data = JSON.stringify({
227+
power:"on",
228+
color:`hue:${h} saturation:${s} brightness:${l}`,
229+
duration: duration,
230+
})
231+
232+
const options = {
233+
hostname: 'api.lifx.com',
234+
port: 443,
235+
path: '/v1/lights/all/state',
236+
method: 'PUT',
237+
headers: {
238+
'Content-Type': 'application/json',
239+
'Content-Length': data.length,
240+
'Authorization': 'Bearer ' + LAMPAPICODE,
241+
}
242+
}
243+
const req = https.request(options, res => {
244+
console.log(`statusCode: ${res.statusCode}`)
245+
})
246+
247+
req.on('error', error => {
248+
console.error(error)
249+
})
250+
251+
req.write(data)
252+
req.end()
253+
}
254+
255+
function setLightbulbOff() {
256+
const data = JSON.stringify({
257+
power:"off",
258+
duration: 2.0,
259+
})
260+
261+
const options = {
262+
hostname: 'api.lifx.com',
263+
port: 443,
264+
path: '/v1/lights/all/state',
265+
method: 'PUT',
266+
headers: {
267+
'Content-Type': 'application/json',
268+
'Content-Length': data.length,
269+
'Authorization': 'Bearer ' + LAMPAPICODE,
270+
}
271+
}
272+
273+
const req = https.request(options, res => {
274+
console.log(`statusCode: ${res.statusCode}`)
275+
})
276+
277+
req.on('error', error => {
278+
console.error(error)
279+
})
280+
281+
req.write(data)
282+
req.end()
283+
}
284+
285+
app.get('/mattis', function (req, res) {
286+
res.end(eyeBrightness + ";" + stressLevel + ";" + timeToNextEvent + ";" + standbyTime)
287+
})
288+
289+
app.get('/smart-cup', function (req, res) {
290+
var scale = req.query.scale
291+
var temperature = req.query.temperature
292+
293+
var scaleNumber = 0.0
294+
295+
if (Number.isNaN(Number.parseFloat(scale))) {
296+
scaleNumber = 0.0
297+
}
298+
else {
299+
scaleNumber = (-1 * Number.parseFloat(scale))-84
300+
}
301+
302+
if (scaleNumber > 30) {
303+
//empty standard compatible cup should be around 50 (most cups are a bit lighter, therefore -40)
304+
let tmpBrightness = (scaleNumber-40)/50
305+
306+
if (tmpBrightness < 0.0) {
307+
tmpBrightness = 0.0
308+
}
309+
if (tmpBrightness > 0.8) {
310+
tmpBrightness = 0.8
311+
}
312+
brightnessLevel = (1.0 - tmpBrightness)*0.6
313+
console.log("New brightness level: " + brightnessLevel)
314+
if (Math.abs(brightnessLevel - oldBrightnessLevel) > 0.1) {
315+
if (lampState < 0) {
316+
setLightbulbOff()
317+
}
318+
else {
319+
setLightbulbHSL(lampState,1,brightnessLevel,2.0)
320+
}
321+
oldBrightnessLevel = brightnessLevel
322+
}
323+
}
324+
})
325+
326+
var server = app.listen(9085, function () {
327+
var host = server.address().address
328+
var port = server.address().port
329+
console.log("Example app listening at http://%s:%s", host, port)
330+
})
331+
80 KB
Binary file not shown.
33.4 KB
Binary file not shown.
48.5 KB
Binary file not shown.

0 commit comments

Comments
 (0)