-
Notifications
You must be signed in to change notification settings - Fork 7
/
ambarpc.py
317 lines (238 loc) · 9.2 KB
/
ambarpc.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
import blinker
import json
import logging
import pprint
import socket
import time
import hashlib
# Known msg_ids
MSG_CONFIG_GET = 1 # AMBA_GET_SETTING
MSG_CONFIG_SET = 2
MSG_CONFIG_GET_ALL = 3
MSG_FORMAT = 4
MSG_STORAGE_USAGE = 5
MSG_STATUS = 7
MSG_BATTERY = 13
MSG_AUTHENTICATE = 257
MSG_PREVIEW_START = 259
MSG_PREVIEW_STOP = 260 # 258 previously, which ends current session
MSG_RECORD_START = 513
MSG_RECORD_STOP = 514
MSG_CAPTURE = 769
MSG_RECORD_TIME = 515 # Returns param: recording length
# File management messages
MSG_RM = 1281 # Param: path, supports wildcards
MSG_LS = 1282 # (Optional) Param: directory (path to file kills the server)
MSG_CD = 1283 # Param: directory, Returns pwd: current directory
MSG_MEDIAINFO = 1026 # Param: filename, returns media_type, date, duration,
# framerate, size, resolution, ...
MSG_DIGITAL_ZOOM = 15 # type: current returns current zoom value
MSG_DIGITAL_ZOOM_SET = 14 # type: fast, param: zoom level
# Not supported yet
MSG_DOWNLOAD_CHUNK = 1285 # param, offset, fetch_size
MSG_DOWNLOAD_CANCEL = 1287 # param
MSG_UPLOAD_CHUNK = 1286 # md5sum, param (path), size, offset
# Other random msg ids found throughout app / binaries
MSG_GET_SINGLE_SETTING_OPTIONS = 9 # ~same as MSG_CONFIG_GET_ALL with param
MSG_SD_SPEED = 0x1000002 # Returns rval: -13
MSG_SD_TYPE = 0x1000001 # Returns param: sd_hc
MSG_GET_THUMB = 1025 # Type: thumb, param: path, returns -21 if already exists
# No response...?
MSG_QUERY_SESSION_HOLDER = 1793 # ??
MSG_UNKNOW = 0x5000001 # likely non-existent
MSG_BITRATE = 16 # Unknown syntax, param
# Sends wifi_will_shutdown event after that, takes a looong time (up to 2
# minutes)
MSG_RESTART_WIFI = 0x1000009
MSG_SET_SOFTAP_CONFIG = 0x2000001
MSG_GET_SOFTAP_CONFIG = 0x2000002
MSG_RESTART_WEBSERVER = 0x2000003
MSG_UPGRADE = 0x1000003 # param: upgrade file
logger = logging.getLogger(__name__)
class TimeoutException(Exception):
pass
class RPCError(Exception):
pass
class AmbaRPCClient(object):
address = None
port = None
_decoder = None
_buffer = None
_socket = None
token = None
def __init__(self, address='192.168.42.1', port=7878):
self.address = address
self.port = port
self._decoder = json.JSONDecoder()
self._buffer = ""
ns = blinker.Namespace()
self.raw_message = ns.signal('raw-message')
self.event = ns.signal('event')
def connect(self):
"""Connects to RPC service"""
self._socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
logger.info('Connecting...')
self._socket.connect((self.address, self.port))
self._socket.settimeout(1)
logger.info('Connected')
def authenticate(self):
"""Fetches auth token used for all the requests"""
self.token = 0
self.token = self.call(MSG_AUTHENTICATE)['param']
logger.info('Authenticated')
def send_message(self, msg_id, **kwargs):
"""Sends a single RPC message"""
kwargs.setdefault('msg_id', msg_id)
kwargs.setdefault('token', self.token)
logger.debug('[%s] >> %r', self.address, kwargs)
self._socket.send(json.dumps(kwargs))
def parse_message(self):
"""Parses a single message from buffer and returns it, or None if no
message could be parsed"""
try:
data, end_index = self._decoder.raw_decode(self._buffer)
except ValueError:
if self._buffer:
logging.debug('Invalid message')
else:
logging.debug('Buffer empty')
return None
logger.debug('[%s] << %r', self.address, data)
self._buffer = self._buffer[end_index:]
ev_data = data.copy()
msg_id = ev_data.pop('msg_id', None)
self.raw_message.send(msg_id, **ev_data)
if 'type' in data and msg_id == MSG_STATUS:
ev_type = ev_data.pop('type', None)
self.event.send(ev_type, **ev_data)
return data
def wait_for_message(self, msg_id=None, timeout=-1, **kwargs):
"""Waits for a single message matched by msg_id and kwargs, with
possible timeout (-1 means no timeout), and returns it"""
st = time.time()
while True:
msg = True
while msg and self._buffer:
msg = self.parse_message()
if not msg:
break
if msg_id is None or msg['msg_id'] == msg_id and \
all(p in msg.items() for p in kwargs.items()):
return msg
if timeout > 0 and time.time() - st > timeout:
raise TimeoutException()
try:
self._buffer += self._socket.recv(1024)
except socket.timeout:
pass
def call(self, msg_id, raise_on_error=True, timeout=-1, **kwargs):
"""Sends single RPC request, raises RPCError when rval is not 0"""
self.send_message(msg_id, **kwargs)
resp = self.wait_for_message(msg_id, timeout=timeout)
if resp.get('rval', 0) != 0 and raise_on_error:
raise RPCError(resp)
return resp
def run(self):
"""Loops forever parsing all incoming messages"""
while True:
self.wait_for_message()
def config_get(self, param=None):
"""Returns dictionary of config values or single config"""
if param:
return self.call(MSG_CONFIG_GET, type=param)['param']
data = self.call(MSG_CONFIG_GET_ALL)['param']
# Downloaded config is list of single-item dicts
return dict(reduce(lambda o, c: o + c.items(), data, []))
def config_set(self, param, value):
"""Sets single config value"""
# Wicked.
return self.call(MSG_CONFIG_SET, param=value, type=param)
def config_describe(self, param):
"""Returns config type (`settable` or `readonly`) and possible values
when settable"""
resp = self.call(MSG_CONFIG_GET_ALL, param=param)
type, _, values = resp['param'][0][param].partition(':')
return (type, values.split('#') if values else [])
def capture(self):
"""Captures a photo. Blocks until photo is actually saved"""
self.send_message(MSG_CAPTURE)
return self.wait_for_message(MSG_STATUS, type='photo_taken')['param']
def preview_start(self):
"""Starts RTSP preview stream available on rtsp://addr/live"""
return self.call(MSG_PREVIEW_START, param='none_force')
def preview_stop(self):
"""Stops live preview"""
return self.call(MSG_PREVIEW_STOP)
def record_start(self):
"""Starts video recording"""
return self.call(MSG_RECORD_START)
def record_stop(self):
"""Stops video recording"""
return self.call(MSG_RECORD_STOP)
def record_time(self):
"""Returns current recording length"""
return self.call(MSG_RECORD_TIME)['param']
def battery(self):
"""Returns battery status"""
return self.call(MSG_BATTERY)
def storage_usage(self, type='free'):
"""Returns `free` or `total` storage available"""
return self.call(MSG_STORAGE_USAGE, type=type)
def storage_format(self):
"""Formats SD card, use with CAUTION!"""
return self.call(MSG_FORMAT)
def ls(self, path):
"""Returns list of files, adding " -D -S" to path will return more
info"""
return self.call(MSG_LS, param=path)
def cd(self, path):
"""Enters directory"""
return self.call(MSG_CD, param=path)
def rm(self, path):
"""Removes file, supports wildcards"""
return self.call(MSG_RM, param=path)
def upload(self, path, contents, offset=0):
"""Uploads bytes to selected path at offset"""
return self.call(
MSG_UPLOAD_CHUNK,
md5sum=hashlib.md5(contents).hexdigest(),
param=path,
size=len(contents),
offset=offset)
def mediainfo(self, path):
"""Returns information about media file, such as media_type, date,
duration, framerate, size, resolution, ..."""
return self.call(MSG_MEDIAINFO, param=path)
def zoom_get(self):
"""Gets current digital zoom value"""
return int(self.call(MSG_DIGITAL_ZOOM, type='current')['param'])
def zoom_set(self, value):
"""Sets digital zoom"""
return self.call(MSG_DIGITAL_ZOOM_SET, type='fast', param=str(value))
# Deprecated
start_preview = preview_start
stop_preview = preview_stop
start_record = record_start
stop_record = record_stop
get_config = config_get
set_config = config_set
describe_config = config_describe
if __name__ == '__main__':
logging.basicConfig(level=logging.DEBUG)
c = AmbaRPCClient()
c.connect()
c.authenticate()
@c.event.connect_via('vf_start')
def vf_start(*args, **kwargs):
print '*** STARTING ***'
@c.event.connect_via('vf_stop')
def vf_stop(*args, **kwargs):
print '*** STOPPING ***'
@c.event.connect_via('video_record_complete')
def complete(type, param):
print 'File saved in', param
@c.event.connect
def testing(*args, **kwargs):
print 'event:', args, kwargs
pprint.pprint(c.battery())
c.run()