-
Notifications
You must be signed in to change notification settings - Fork 1
/
client.py
325 lines (273 loc) · 11.7 KB
/
client.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
import RPi.GPIO as GPIO
class Controls:
api_key = 'rem'
host = "http://localhost:8080/"
# Store for each matched user:
# {
# user: {
# read ['/path/to/audio', ...],
# unread['/path/to/audio', ...]
# },
# ...
# }
data = {}
# Current Matched User
cmu = None
# Path to the current message for current user
current_message = None
# The pins to invoke method calls on the raspberry PI
rec_button = 7
play_button = 11
prev_button = 13
next_button = 15
user_button = 16
proc = None
recording = False
playing = False
def __init__(self):
# Setup pins using board numbers
GPIO.setmode(GPIO.BOARD)
GPIO.setup(self.rec_button, GPIO.IN, pull_up_down=GPIO.PUD_UP)
GPIO.setup(self.play_button, GPIO.IN, pull_up_down=GPIO.PUD_UP)
GPIO.setup(self.prev_button, GPIO.IN, pull_up_down=GPIO.PUD_UP)
GPIO.setup(self.next_button, GPIO.IN, pull_up_down=GPIO.PUD_UP)
GPIO.setup(self.user_button, GPIO.IN, pull_up_down=GPIO.PUD_UP)
def record(self):
"""
Records the message when record button is held down.
"""
if (self.recording):
return
import os
import subprocess
import time
self.current_message = ('audios/' + self.cmu + '/' +
str(int(time.time() + (60*60))) + '.wav')
rec = "arecord -f dat -D plughw:1,0 " + self.current_message
self.proc = subprocess.Popen(rec, stdout=subprocess.PIPE,
shell=True, preexec_fn=os.setsid)
self.recording = True
def stop_record(self):
"""
Stops recording when button is released, then uploads an audio message.
"""
if (not self.recording):
return
import os
import signal
os.killpg(os.getpgid(self.proc.pid), signal.SIGTERM)
self.recording = False
self.proc = None
self.__upload(self.current_message)
self.__notify("We have uploaded a recording")
self.data[self.cmu]['read'].append(self.current_message)
def play(self, channel, filepath_to_message=None):
"""
Plays an audio message from a given path.
Args:
channel (int): the current pin engaged
filepath_to_message (str): the path to the audio message.
"""
# Check if the method is initialised by the play_button
# Check for the button again and do an early return
if (channel == self.play_button and
GPIO.input(self.play_button) != False):
return
if not filepath_to_message:
filepath_to_message = self.current_message
import subprocess
self.__notify("We are playing a recording")
self.proc = subprocess.call("aplay " + filepath_to_message, shell=True)
self.__notify("We are stopping a recording")
def next(self, channel):
"""
Plays the next unread message (if any) for the matched user otherwise
plays the next read message, including looping back to the start.
Args:
channel (int): the current pin engaged
"""
if GPIO.input(self.next_button) != False:
return
if self.data[self.cmu]['unread']:
# then add to the end of the read list and remove from unread list
self.current_message = self.data[self.cmu]['unread'][0]
self.data[self.cmu]['read'].append(self.data[self.cmu]['unread'][0])
self.data[self.cmu]['unread'].pop(0)
else:
# Play the next message based on the position of the current read
# Add one for comparison with len and next selection
pos = self.data[self.cmu]['read'].index(self.current_message) + 1
# We have read all the messages, so start from the beginning!
if pos >= len(self.data[self.cmu]['read']):
# Set the current message to the start message
self.current_message = self.data[self.cmu]['read'][0]
else:
# Otherwise we want the next read audio
self.current_message = self.data[self.cmu]['read'][pos]
self.play(self.current_message)
def previous(self, channel):
"""
Plays the previous message for the current matched user (cmu).
Args:
channel (int): the current pin engaged
"""
if GPIO.input(self.prev_button) != False:
return
pos = self.data[self.cmu]['read'].index(self.current_message)
self.current_message = self.data[self.cmu]['read'][pos - 1]
self.play(self.current_message)
def users(self, channel):
"""
Switches the current matched user (cmu) to the next and loops back.
Args:
channel (int): the current pin engaged
"""
if GPIO.input(self.user_button) != False:
return
matches = [i for i in self.data.iterkeys()]
# If there are no matches; no need to update state
if matches:
pos = matches.index(self.cmu) + 1
# Ensure there are no out-of-bounds errors when the
# last list item is checked; set next as first.
if (pos) >= len(matches):
self.cmu = self.data.keys()[0]
else:
self.cmu = self.data.keys()[pos]
# Once the user switches we must change the current message
if self.data[self.cmu]['read']:
self.current_message = self.data[self.cmu]['read'][-1]
else:
self.current_message = self.data[self.cmu]['unread'][0]
else:
return
def update_state(self):
"""
Used to modify the current state for all matched users, includes:
new matched users and unread messages
"""
# Prevents multiple requests as we must assign cmu below
matches = self.__matches()
print matches
for user in matches:
# As there may be multiple conversations user => matches
# We must find all read/unread for each conversation.
am = self.__all_messages(user)
self.data[user] = {
'read': am,
'unread': self.__unread_messages(user, len(am))}
# This assumes we are matched with at least one, which is
# true given we will send an artefact to a users home!
self.cmu = matches[-1]
if self.data[self.cmu]['unread']:
self.current_message = self.data[self.cmu]['unread'][0]
elif self.data[self.cmu]['read']:
self.current_message = self.data[self.cmu]['read'][0]
else:
self.__notify("There are currently no messages. Record one?")
import threading
# Check for new matches and messages every five minute
threading.Timer(60 * 5, self.update_state).start()
def __upload(self, filepath_to_message):
"""
Uploads the users audio message to the server from a given path,
which uses the current matched user within the request.
Args:
filepath_to_message (str): the path to the audio message.
"""
import base64
import requests
# NOTE: messages are stored locally for instant playback and browsing.
with open(filepath_to_message, 'rb') as af:
voice_message = af.read()
# NOTE: encode to binary to send with json in one request
res = requests.post(url=self.host + "api/upload",
json={'sender': self.api_key,
'receiver': self.cmu,
'filename': filepath_to_message.split('/')[-1],
'message': base64.b64encode(voice_message)},
headers={'Content-Type': 'application/json'})
if res.status_code is not 201:
print "TODO: blink a red light for UPLOAD."
def __unread_messages(self, receiver, number_of_messages):
"""
Used to request and download unread messages for a given conversation.
Messages are streamed and unzipped in memory and stored locally in a
unique folder representing a conversation, i.e. client_receiver
Args:
receiver (str): the other user in the conversation.
num_messages (int): number of messages in conversation with receiver
Returns:
list: The paths to the unread (new) audio messages by the receiver.
"""
import requests
data = ("?sender=" + self.api_key +
"&receiver=" + receiver +
"&latest=" + str(number_of_messages))
res = requests.get(url=self.host + "api/download" + data)
if res.status_code == 200:
import io
import zipfile
# In memory-stream used as ZipFile constructor expects a file.
# Prevents a zip file being saved locally that need not be removed.
path = 'audios/' + receiver + '/'
with zipfile.ZipFile(io.BytesIO(res.content), 'r') as zf:
zf.extractall(path)
# A list of locations to unread messages from sender to receiver
return [path + fname for fname in zf.namelist()]
elif res.status_code == 204:
print "Note: there are no new messages to download"
else:
print "TODO: blink a red light for DOWNLOAD."
def __matches(self):
"""
Determines who is matched with the client -- they who own the pi.
This is used when initialising and updating the client's state.
Returns:
list: the unique identifies (tokens) of the matched users
"""
import json
import requests
res = requests.get(
url=self.host + "api/matches" + "?sender=" + self.api_key)
# TODO: the service returns unicode; we could simplify this to strings.
return [str(match) for match in json.loads(res.content)['matches']]
def __all_messages(self, user):
"""
Obtains a list of paths to all audios messages in a conversation
between the client and a specific user.
Args:
user (str): the token of the user to search for
Returns:
list: a list paths to each message in the conversation.
"""
import os
# We must create the path for when the first message is retrieved.
matched_user_path = "audios/" + user
if not os.path.exists(matched_user_path):
os.makedirs(matched_user_path)
# NOTE: messages are stored by token; unique for each conversation.
return [matched_user_path + "/" + f
for f in os.listdir(matched_user_path) if ".wav" in f]
def __notify(self, message):
print "Flash: " + message
def main():
# TODO: check every N minutes if there are new messages/matches:
# invoke __update_state every 5 minutes;
controller = Controls()
controller.update_state()
GPIO.add_event_detect(controller.user_button, GPIO.FALLING,
callback=controller.users, bouncetime=300)
GPIO.add_event_detect(controller.play_button, GPIO.FALLING,
callback=controller.play, bouncetime=300)
GPIO.add_event_detect(controller.prev_button, GPIO.FALLING,
callback=controller.previous, bouncetime=300)
GPIO.add_event_detect(controller.next_button, GPIO.FALLING,
callback=controller.next, bouncetime=300)
while True:
if (GPIO.input(controller.rec_button) == False):
controller.record()
else:
controller.stop_record()
if __name__ == "__main__":
main()