示例#1
0
class apiBase():
	"""Call the D-Bus camera APIs, asynchronously.
		
		Methods:
			- call(function[, arg1[ ,arg2[, ...]]])
				Call the remote function.
			- get([value[, ...]])
				Get the named values from the API.
			- set({key: value[, ...]}])
				Set the named values in the API.
		
		All methods return an A* promise-like, in that you use
		`.then(cb(value))` and `.catch(cb(error))` to get the results
		of calling the function.
	"""
	def __init__(self, service, path, interface="", bus=QDBusConnection.systemBus()):
		if not QDBusConnection.systemBus().isConnected():
			log.error("Can not connect to D-Bus. Is D-Bus itself running?")
			raise Exception("D-Bus Setup Error")
		
		self.name = type(self).__name__
		self.iface = QDBusInterface(service, path, interface, bus)

		# For Asynchronous call handling.
		self.enqueuedCalls = []
		self.callInProgress = False
		self.activeCall = None
		
		log.info("Connected to D-Bus %s API at %s", self.name, self.iface.path())

		# Check for errors.
		if not self.iface.isValid():
			# Otherwise, an error occured.
			log.error("Can not connect to %s D-Bus API at %s. (%s: %s)",
				self.name, self.iface.service(),
				self.iface.lastError().name(),
				self.iface.lastError().message())
		else:
			self.iface.setTimeout(API_TIMEOUT_MS)
	
	def callSync(self, *args, warnWhenCallIsSlow=True, **kwargs):
		"""Call a camera DBus API. First arg is the function name.
			
			This is the synchronous version of the call() method. It
			is much slower to call synchronously than asynchronously!
		
			See http://doc.qt.io/qt-5/qdbusabstractinterface.html#call for details about calling.
			See https://github.com/krontech/chronos-cli/tree/master/src/api for implementation details about the API being called.
			See README.md at https://github.com/krontech/chronos-cli/tree/master/src/daemon for API documentation.
		"""
		
		#Unwrap D-Bus errors from message.
		log.debug("%s.callSync %s", self.name, tuple(args))
		
		start = perf_counter()
		msg = QDBusReply(self.iface.call(*args, **kwargs))
		end = perf_counter()
		if warnWhenCallIsSlow and (end - start > API_SLOW_WARN_MS / 1000):
			log.warn(f'slow call: {self.name}.callSync{tuple(args)} took {(end-start)*1000:.0f}ms/{API_SLOW_WARN_MS}ms.')
		
		if msg.isValid():
			return msg.value()
		else:
			if msg.error().name() == 'org.freedesktop.DBus.Error.NoReply':
				raise DBusException(f"{self.name}.callSync{tuple(args)} timed out ({API_TIMEOUT_MS}ms)")
			else:
				raise DBusException("%s: %s" % (msg.error().name(), msg.error().message()))

	def getSync(self, keyOrKeys):
		"""Call a camera API DBus get method synchronously.
		
			Convenience method for `getSync('get', [value])[0]`.
			
			Accepts key or [key, …], where keys are strings.
			
			Returns value or {key:value, …}, respectively.
			
			See control's `availableKeys` for a list of valid inputs.
		"""	
		valueList = self.callSync('get',
			[keyOrKeys] if isinstance(keyOrKeys, str) else keyOrKeys )
		return valueList[keyOrKeys] if isinstance(keyOrKeys, str) else valueList

	def setSync(self, *args):
		"""Call a camera API DBus set method synchronously.
			
			Accepts {str: value, ...} or a key and a value.
			Returns either a map of set values or the set
				value, if the second form was used.
		"""
		if len(args) == 1:
			return self.callSync('set', *args)
		elif len(args) == 2:
			return self.callSync('set', {args[0]:args[1]})[args[0]]
		else:
			raise valueError('bad args')

	def enqueueCall(self, pendingCall, coalesce: bool=True): #pendingCall is CallPromise
		"""Enqueue callback. Squash and elide calls to set for efficiency."""
		
		#Step 1: Will this call actually do anything? Elide it if not.
		anticipitoryUpdates = False #Emit update signals before sending the update to the API. Results in faster UI updates but poorer framerate.
		if coalesce and pendingCall._args[0] == 'set':
			#Elide this call if it would not change known state.
			hasNewInformation = False
			newItems = pendingCall._args[1].items()
			for key, value in newItems:
				if _camState[key] != value:
					hasNewInformation = True
					if not anticipitoryUpdates:
						break
					#Update known cam state in advance of state transition.
					log.info(f'Anticipating {key} → {value}.')
					_camState[key] = value
					for callback in apiValues._callbacks[key]:
						callback(value)
			if not hasNewInformation:
				return
		
		if coalesce and pendingCall._args[0] == 'playback':
			#Always merge playback states.
			#Take the playback state already enqueued, {}, and overlay the current playback state. (so, {a:1, b:1} + {b:2} = {a:1, b:2})
			assert type(pendingCall._args[1]) is dict, f"playback() takes a {{key:value}} dict, got {pendingCall._args[1]} of type {type(pendingCall._args[1])}."
			existingParams = [call._args[1] for call in self.enqueuedCalls if call._args[0] == 'playback']
			if not existingParams:
				self.enqueuedCalls += [pendingCall]
			else:
				#Update the parameters of the next playback call instead of enqueueing a new call.
				for k, v in pendingCall._args[1].items():
					existingParams[-1][k] = v
				
			return
		
		#Step 2: Is there already a set call pending? (Note that non-set calls act as set barriers; two sets won't get coalesced if a non-set call is between them.)
		if coalesce and [pendingCall] == self.enqueuedCalls[:1]:
			self.enqueuedCalls[-1] = pendingCall
		else:
			self.enqueuedCalls += [pendingCall]
	
	def _startNextCallback(self):
		"""Check for pending callbacks.
			
			If none are found, simply stop.
			
			Note: Needs to be manually pumped.
		"""
		if self.enqueuedCalls:
			self.callInProgress = True
			self.enqueuedCalls.pop(0)._startAsyncCall()
		else:
			self.callInProgress = False

	def call(self, *args):
		"""Call a camera DBus API. First arg is the function name. Returns a promise.
		
			See http://doc.qt.io/qt-5/qdbusabstractinterface.html#call for details about calling.
			See https://github.com/krontech/chronos-cli/tree/master/src/api for implementation details about the API being called.
			See README.md at https://github.com/krontech/chronos-cli/tree/master/src/daemon for API documentation.
		"""
		promise = CallPromise(*args, api=self)

		log.debug(f'enquing {promise}')
		self.enqueueCall(promise)
		if not self.callInProgress:
			#Don't start multiple callbacks at once, the most recent one will block.
			self._startNextCallback()
		
		return promise

	def get(self, keyOrKeys):
		"""Call a camera DBus API get method.
		
			Convenience method for `control('get', [value])[0]`.
			
			Accepts key or [key, …], where keys are strings.
			
			Returns value or {key:value, …}, respectively.
			
			See control's `availableKeys` for a list of valid inputs.
		"""
		
		return self.call(
			'get', [keyOrKeys] if isinstance(keyOrKeys, str) else keyOrKeys
		).then(lambda valueList:
			valueList[keyOrKeys] if isinstance(keyOrKeys, str) else valueList
		)

	def set(self, *args):
		"""Call a camera DBus API set method.
			
			Accepts {str: value, ...} or a key and a value.
			Returns either a map of set values or the set
				value, if the second form was used.
		"""
		
		log.debug(f'simple set call: {args}')
		if len(args) == 1:
			return self.call('set', *args)
		elif len(args) == 2:
			return self.call(
				'set', {args[0]:args[1]}
			).then(lambda valueDict: 
				valueDict[args[0]]
			)
		else:
			raise valueError('bad args')
示例#2
0
	f"ca.krontech.chronos.{'control_mock' if USE_MOCK else 'control'}", #Service
	f"/ca/krontech/chronos/{'control_mock' if USE_MOCK else 'control'}", #Path
	f"", #Interface
	QDBusConnection.systemBus() )
cameraVideoAPI = QDBusInterface(
	f"ca.krontech.chronos.{'video_mock' if USE_MOCK else 'video'}", #Service
	f"/ca/krontech/chronos/{'video_mock' if USE_MOCK else 'video'}", #Path
	f"", #Interface
	QDBusConnection.systemBus() )

cameraControlAPI.setTimeout(API_TIMEOUT_MS) #Default is -1, which means 25000ms. 25 seconds is too long to go without some sort of feedback, and the only real long-running operation we have - saving - can take upwards of 5 minutes. Instead of setting the timeout to half an hour, we use events which are emitted as the task progresses. One frame (at 15fps) should be plenty of time for the API to respond, and also quick enough that we'll notice any slowness.
cameraVideoAPI.setTimeout(API_TIMEOUT_MS)

if not cameraControlAPI.isValid():
	print("Error: Can not connect to control D-Bus API at %s. (%s: %s)" % (
		cameraControlAPI.service(), 
		cameraControlAPI.lastError().name(), 
		cameraControlAPI.lastError().message(),
	), file=sys.stderr)
	raise Exception("D-Bus Setup Error")

if not cameraVideoAPI.isValid():
	print("Error: Can not connect to video D-Bus API at %s. (%s: %s)" % (
		cameraVideoAPI.service(), 
		cameraVideoAPI.lastError().name(), 
		cameraVideoAPI.lastError().message(),
	), file=sys.stderr)
	raise Exception("D-Bus Setup Error")