forked from OpenSolo/shotmanager
-
Notifications
You must be signed in to change notification settings - Fork 0
/
appManager.py
255 lines (214 loc) · 10.4 KB
/
appManager.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
#
# appManager.py
# shotmanager
#
# Handles app connection state and IO.
#
# Created by Will Silva on 3/5/2016.
# Copyright (c) 2016 3D Robotics.
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import errno
import os
import platform
import select
import socket
import string
import sys
import threading
import monotonic
import time
sys.path.append(os.path.realpath(''))
import modes
import settings
import shots
import Queue
import struct
from dronekit.lib import LocationGlobalRelative
from sololink import btn_msg
import app_packet
import GoProManager
import shotLogger
import GeoFenceManager
logger = shotLogger.logger
APP_SERVER_PORT = 5507
APP_TCP_BUFSIZE = 1024
class appManager():
def __init__(self, shotMgr):
self.shotMgr = shotMgr
self.connected = False
self.client = None
self.client_address = None
self.clientQueue = None
self.packetBuffer = ""
self.bindServer()
def bindServer(self):
self.server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
if platform.system() != 'Darwin':
self.server.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1) # After 1 second, start KEEPALIVE
self.server.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPIDLE, 1) # TCP Idle true
self.server.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPINTVL, 5) # 5 seconds in between keepalive pings
self.server.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPCNT, 5) # 5 max fails
# Bind the socket to the port
while True:
try:
self.server.bind(('', APP_SERVER_PORT))
except:
logger.log("[app]: Can't bind, address in use. Retrying in 1 second.")
time.sleep(1.0)
else:
break
logger.log("[app]: Ready for connections from app.")
# Listen for incoming connections
self.server.listen(0)
# establishes a connection to a Solo app
def connectClient(self):
client, client_address = self.server.accept()
if self.isAppConnected():
if client_address[0] != self.client_address[0]:
logger.log("[app]: Already connected to client %s - rejecting %s." % (self.client_address, client_address))
#TO DO:send system INFO packet to app with rejection flag True
client.close()
return
else:
logger.log("[app]: Detected re-connection attempt for client %s - reconnecting.")
self.disconnectClient()
self.client = client
self.client_address = client_address
logger.log("[app]: Connected to App. %s" % (self.client_address,))
#TO DO:send system INFO packet to app with rejection flag False
self.connected = True
self.client.setblocking(0)
self.shotMgr.inputs.append(self.client)
self.clientQueue = Queue.Queue()
self.broadcastShotToApp(self.shotMgr.currentShot)
self.shotMgr.buttonManager.setButtonMappings() # called to un-grey out Artoo buttons
self.shotMgr.goproManager.sendState() # send gopro state to app
def disconnectClient(self):
if self.isAppConnected():
logger.log("[app]: Closing client connection with %s." % (self.client_address,))
self.connected = False
if self.client in self.shotMgr.outputs:
self.shotMgr.outputs.remove(self.client)
if self.client in self.shotMgr.inputs:
self.shotMgr.inputs.remove(self.client)
self.client.close()
self.client = None
self.clientQueue = None
self.shotMgr.buttonManager.setButtonMappings() # called to grey-out Artoo buttons
# if this type of shot requires a client present at all times, then kill the shot
if self.shotMgr.currentShot in shots.ALWAYS_NEEDS_APP_CONNECTION:
self.shotMgr.enterShot(shots.APP_SHOT_NONE)
# Clear Geofence when app disconnect
self.shotMgr.geoFenceManager.clearGeoFence()
else:
logger.log('[app]: Attempted to close app connection, but no app was connected!')
def isAppConnected(self):
return self.connected
def sendPacket(self, pkt):
if self.isAppConnected():
self.clientQueue.put(pkt)
if self.client not in self.shotMgr.outputs:
self.shotMgr.outputs.append(self.client)
else:
logger.log('[app]: Can\'t send packet - app is not connected!')
def broadcastShotToApp(self, shot):
packet = struct.pack('<IIi', app_packet.SOLO_MESSAGE_GET_CURRENT_SHOT, 4, shot)
self.sendPacket(packet)
def exception(self):
logger.log("[app]: Exception with " + self.client.getpeername())
self.appMgr.disconnectClient()
def write(self):
if self.clientQueue:
try:
msg = self.clientQueue.get_nowait()
except Queue.Empty:
# no messages left, stop checking
self.shotMgr.outputs.remove(self.client)
else:
try:
self.client.send(msg)
except Exception as ex:
logger.log("[app]: Exception on send. (%s)" % ex)
self.disconnectClient()
def parse(self):
try:
data = self.client.recv(APP_TCP_BUFSIZE) # grab one kB
if not data:
raise socket.error()
except socket.error:
logger.log('[app]: Data from client %s is nil.' % (self.client_address,))
self.disconnectClient()
return
self.packetBuffer += data
while self.packetBuffer is not '':
if len(self.packetBuffer) < app_packet.SOLO_MESSAGE_HEADER_LENGTH:
logger.log('[app]: Not enough data for a Solo packet header yet.')
return
(packetType, packetLength) = struct.unpack('<II', self.packetBuffer[:app_packet.SOLO_MESSAGE_HEADER_LENGTH])
if len(self.packetBuffer) < app_packet.SOLO_MESSAGE_HEADER_LENGTH + packetLength:
logger.log('[app]: Not enough data for a Solo packet (ID: %s) yet.' % (packetType,))
return
# extract packet value from TLV based on known packetLength and packetType
packetValue = self.packetBuffer[app_packet.SOLO_MESSAGE_HEADER_LENGTH:(app_packet.SOLO_MESSAGE_HEADER_LENGTH+packetLength)]
handled = False
# if a shot is active, pass the TLV packet to the shot's handlePacket function
if self.shotMgr.curController:
handled = self.shotMgr.curController.handlePacket(packetType, packetLength, packetValue)
# if the packet wasn't understood by the shot, then try to handle it
if not handled:
handled = self.handlePacket(packetType, packetLength, packetValue)
# crop out the packet from the buffer and move on
self.packetBuffer = self.packetBuffer[app_packet.SOLO_MESSAGE_HEADER_LENGTH+packetLength:]
def handlePacket(self, packetType, packetLength, packetValue):
try:
if packetType == app_packet.SOLO_MESSAGE_SET_CURRENT_SHOT:
shot = struct.unpack('<i', packetValue)[0]
if self.shotMgr.currentShot != shot:
logger.log("[app]: App requested shot : %s." % shots.SHOT_NAMES[shot])
self.shotMgr.enterShot(shot)
elif packetType == app_packet.SOLO_MESSAGE_GET_BUTTON_SETTING:
# This is a request for the current button mapping of the
# A & B single press. This needs to work the same as it always
# has from 3DR to maintain compatibility with the 3DR Solo app
# that is no longer being developed. So we look at press for
# backwards compatibility and clickRelease for Open Solo.
(button, event, shot, APMmode) = struct.unpack('<iiii', packetValue)
if event == btn_msg.Press or event == btn_msg.ClickRelease:
(mappedShot, mappedMode) = self.shotMgr.buttonManager.getFreeButtonMapping(button)
logger.log("[app]: App requested button mapping for %d"%(button))
# send back to the app
packet = struct.pack('<IIiiii', app_packet.SOLO_MESSAGE_GET_BUTTON_SETTING, 16, button, event, mappedShot, mappedMode)
self.sendPacket(packet)
# App is trying to map the single press of A or B. We have to look
# for the press event since that's what the legacy 3DR app uses. But
# we really use the clickRelease event in Open Solo now.
elif packetType == app_packet.SOLO_MESSAGE_SET_BUTTON_SETTING:
(button, event, shot, APMmode) = struct.unpack('<iiii', packetValue)
if event == btn_msg.Press or event == btn_msg.ClickRelease:
self.shotMgr.buttonManager.setFreeButtonMapping( button, shot, APMmode )
# Gopromanager handles these messages
elif packetType in GoProManager.GOPROMESSAGES:
self.shotMgr.goproManager.handlePacket( packetType, packetValue )
elif packetType == app_packet.SOLO_REWIND_OPTIONS or packetType == app_packet.SOLO_HOME_LOCATION:
self.shotMgr.rewindManager.handlePacket( packetType, packetLength, packetValue )
# Geofence messages
elif packetType in GeoFenceManager.GEO_FENCE_MESSAGES:
self.shotMgr.geoFenceManager.handleFenceData(packetType, packetValue)
else:
logger.log("[app]: Got an unknown packet type: %d." % (packetType,))
except Exception as e:
logger.log('[app]: Error handling packet. (%s)' % e)
return False
else:
return True