Example #1
0
	def onShow(self):
		#Try, _again_, to set the drop-down to the correct value. Since this widget is
		#repopulated when the partitions change and on show, this is really hard. >_<
		api.externalPartitions.observe(lambda *_: self.setPreferredSavingDevice(
			settings.value('preferredFileSavingUUID', '') ))
		delay(self, 16, lambda:
			api.externalPartitions.observe(lambda *_: self.setPreferredSavingDevice(
				settings.value('preferredFileSavingUUID', '') )) )
Example #2
0
def sizeForMp4(frames: int,
               hRes: Optional[int] = None,
               vRes: Optional[int] = None,
               *,
               frameRate: Optional[float] = None,
               bpp: Optional[float] = None,
               maxBitrate: Optional[float] = None) -> int:
    """Estimate the size of a video saved as MP4.
		
		
		Args:
			frames (int): Number of frames in the video.
			hRes (int, optional): Horizontal size of the video. Defaults to
				the current size of video being recorded.
			vRes (int, optional): Vertical size of the video. Defaults to
				the current size of video being recorded.
			frameRate (float, optional, keyword-only): Number of frames-per-
				second, used for calculating bitrate internally. Computed
				value is capped by maxBitrate. Defaults to settings'
				savedFileBPP, which itself defaults to 0.7.
			bpp (float, optional, keyword-only): Bits-per-pixel of the video
				being recorded. Computed value is capped by maxBitrate.
				Defaults to settings' savedFileMaxBitrate, which itself
				defaults to 40.
			maxBitrate (float, optional, keyword-only): Cap megabits per
				second for the saved video. Mbps depends on resolution,
				frameRate, and bpp.

		Yields:
			int: The estimated size, in bytes, that the saved video might be.
		
		Note:
			This is less precise than other video formats' estimations due to
				the compression used by the format. Videos with more motion
				can take more room than videos with less, which we can't
				compensate for without analysis we can't afford.
	"""

    if hRes is None:
        hRes = api.apiValues.get('resolution')['hRes']
    if vRes is None:
        vRes = api.apiValues.get('resolution')['vRes']
    if frameRate is None:
        frameRate = api.apiValues.get('frameRate')
    if bpp is None:
        bpp = settings.value('savedFileBPP', 0.7)
    if maxBitrate is None:
        maxBitrate = settings.value('savedFileMaxBitrate', 40)

    maxBitrate *= 1e6  #convert from mbps to bps
    bitrate = min(maxBitrate, hRes * vRes * frameRate * bpp)
    return frames / frameRate * bitrate + 200  #Add a little bit, 200 bytes, for file overhead.
Example #3
0
	def setVirtualTrigger(state: bool):
		"""Set the virtual trigger signal high or low.
			
			May or may not start a recording, depending on how
			trigger/io is set up.
			
			(See trigger/io screen for details.)"""
		
		#Can't use self.uiRecord.setText text here becasue we don't
		#know what the virtual trigger is actually hooked up to, on
		#what delay, so we have to wait for the signal. (We could,
		#but writing that simulation would be a lot of work.)
		
		if RECORDING_MODE == RECORDING_MODES['START/STOP']:
			pass #Taken care of by publicToggleRecordingState
		
		elif RECORDING_MODE == RECORDING_MODES['SOFT_TRIGGER']:
			raise ValueError('Soft trigger not available in FPGA.')
		
		elif RECORDING_MODE == RECORDING_MODES['VIRTUAL_TRIGGER']:
			virtuals = settings.value('virtually triggered actions', {})
			if virtuals:
				self.control.setSync('ioMapping', dump('new io mapping', {
					action: { 
						'source': 'alwaysHigh' if state else 'none',
						'invert': config['invert'],
						'debounce': config['debounce'],
					} for action, config in virtuals.items()
				}))
			else:
				#TODO: Log warning visually for operator, not just to console.
				log.warn('No virtual triggers configured!')
		
		else:
			raise ValueError(F'Unknown RECORDING_MODE: {RECORDING_MODE}.')
Example #4
0
def size(frames: int,
         hRes: Optional[int] = None,
         vRes: Optional[int] = None) -> int:
    """Estimate the size of a video saved with the current camera settings.
		
		Args:
			frames (int): Number of frames in the video.
			hRes (int, optional): Horizontal size of the video. Defaults to
				the current size of video being recorded.
			vRes (int, optional): Vertical size of the video. Defaults to
				the current size of video being recorded.

		Yields:
			int: The estimated size, in bytes, that the saved video will be.
	"""

    return ({
        '.raw':
        sizeForRaw12,
        '.dng':
        sizeForDng,
        '.tiff':
        sizeForMonoTiff if api.apiValues.get('sensorColorPattern') == 'mono'
        else sizeForRgbTiff,
        '.tiff.raw':
        sizeForTiffraw,
        '.mp4':
        sizeForMp4,
    }[settings.value('savedVideoFileExtention', '.mp4')](**locals()))
Example #5
0
	def onStateChangeWhenScreenActive2(self, status):
		#Filesave doesn't actually affect anything, just the transition to/from playback.
		if status['filesave']:
			return
		
		#Reset number of frames.
		self.uiSeekSlider.setMaximum(status['totalFrames'])
		self.uiSeekSlider.setMaximum(status['totalFrames'])
		self.updateMotionHeatmap()
		
		
		self.totalRecordedFrames = status['totalFrames']
		self.uiCurrentFrame.setMaximum(status['totalFrames'])
		self.uiCurrentFrame.setSuffix(
			self.uiCurrentFrame.suffixFormatString % status['totalFrames']
		)
		geom = self.uiCurrentFrame.geometry()
		geom.setLeft(
			geom.right() 
			- 10*2 - 5 #qss margin, magic
			- self.uiCurrentFrame.fontMetrics().width(
				self.uiCurrentFrame.prefix()
				+ str(status['totalFrames'])
				+ self.uiCurrentFrame.suffixFormatString % status['totalFrames']
			)
		)
		self.uiCurrentFrame.setGeometry(geom)
		
		
		if settings.value('autoSaveVideo', False) and not self.markedRegions: #[autosave]
			self.markedStart = self.uiSeekSlider.minimum()
			self.markedEnd = self.uiSeekSlider.maximum()
			self.addMarkedRegion()
			self.saveMarkedRegion()
Example #6
0
	def onEOF(self, state):
		log.print(f'onEOF {state}')
		if state['filesave'] and not self.saveCancelled: #Check event is relevant to saving.
			unsavedRegionsLeft = [r for r in self.markedRegions if r['saved'] == 0]
			savedRegion = [r for r in self.markedRegions if r['region id'] == self.regionBeingSaved][:1]
			if unsavedRegionsLeft: #Save the next region, so we don't have to sit and babysit a camera saving multiple events.
				log.print(f'ovsca region {savedRegion}')
				if savedRegion: #Protect against resets in the middle of saving.
					savedRegion = savedRegion[0]
					#{'region id': 'aaaaaaaa', 'hue': 240, 'mark end': 199, 'mark start': 130, 'saved': 0.0, 'highlight': 0, 'segment ids': ['KxIjG09V'], 'region name': 'Clip 1'},
					savedRegion['saved'] = 1.0
					try:
						log.print(f'ovsca saving next {savedRegion}')
						self.saveMarkedRegion()
					except type(self).NoRegionMarked:
						log.print(f'ovsca not saving next {savedRegion}')
						self.regionBeingSaved = None
						self.uiSeekSlider.setEnabled(True)
			else: #Don't advance to next region, stop.
				self.regionBeingSaved = None
				self.saveCancelled = False
				
				self.uiSeekSlider.setEnabled(True)
				self.uiSave.show()
				self.uiSaveCancel.hasFocus() and self.uiSave.setFocus()
				self.uiSaveCancel.hide()
			
				#Close this screen and return to the main screen for more recording, now that we're done saving. [autosave]
				if settings.value('resumeRecordingAfterSave', False) and not self.saveCancelled:
					self._window.show('main')
Example #7
0
def duration(filesize: int,
             hRes: Optional[int] = None,
             vRes: Optional[int] = None) -> timedelta:
    """Estimate the duration of a video saved using the current camera settings.
		
		Args:
			size (int): Number of bytes in the video file.
			hRes (int, optional): Horizontal size of the video. Defaults to
				the current size of video being recorded.
			vRes (int, optional): Vertical size of the video. Defaults to
				the current size of video being recorded.
			frameRate (float, optional): The number of frames per second the
				video being estimated plays back at. Defaults to settings'
				savedFileFramerate, which defaults to 30.

		Yields:
			timedelta: The estimated run time of the video.
	"""

    return ({
        '.raw':
        durationForRaw12,
        '.dng':
        durationForDng,
        '.tiff':
        durationForMonoTiff if api.apiValues.get('sensorColorPattern')
        == 'mono' else durationForRgbTiff,
        '.tiff.raw':
        durationForTiffraw,
        '.mp4':
        durationForMp4,
    }[settings.value('savedVideoFileExtention', '.mp4')](**locals()))
Example #8
0
def durationForTiffraw(filesize: int,
                       hRes: Optional[int] = None,
                       vRes: Optional[int] = None,
                       frameRate: Optional[float] = None) -> timedelta:
    """Estimate the duration of a TIFFRAW video of a known filesize.
		
		Args:
			size (int): Number of bytes in the video file.
			hRes (int, optional): Horizontal size of the video. Defaults to
				the current size of video being recorded.
			vRes (int, optional): Vertical size of the video. Defaults to
				the current size of video being recorded.
			frameRate (float, optional): The number of frames per second the
				video being estimated plays back at. Defaults to settings'
				savedFileFramerate, which defaults to 30.

		Yields:
			timedelta: The estimated run time of the video.
		
		Note:
			Unlike TIFF, there is no difference between color and mono
				footage in this format.
	"""

    if frameRate is None:
        frameRate = settings.value('savedFileFramerate', 30)

    sizeOfOneFrame = sizeForTiffraw(
        2000, hRes, vRes
    ) / 2000  #Lower error due to nonscaling overhead, ie, one-off headers are a significant chunk of a one-frame video, but irrelevant for a 100,000 frame video.
    totalFrames = int(filesize / sizeOfOneFrame)
    return timedelta(seconds=totalFrames / frameRate)
	def __init__(self, window):
		super().__init__()
		self.setupUi(self)

		# API init.
		self.control = api.control()
		
		# Panel init.
		self.setFixedSize(window.app.primaryScreen().virtualSize())
		self.setWindowFlags(QtCore.Qt.FramelessWindowHint)
		self.setAttribute(QtCore.Qt.WA_TranslucentBackground, True)
		
		# Button binding.
		self.window_ = window
		
		#Side and rotated are not quite correct, as askBeforeDiscarding is, but they are correct enough for now. Having the final result come from two values confused things a bit.
		self.uiInterfaceSide.setCurrentIndex(
			int(settings.value('interface handedness', None) == 'left'))
		self.uiInterfaceSide.currentIndexChanged.connect(lambda index:
			settings.setValue('interface handedness', 'left' if index else 'right') )
		settings.observe('interface handedness', None, self.updateInterfaceSide)
		
		self.uiInterfaceRotated.setCurrentIndex(
			int(settings.value('interface rotation', None) == '180'))
		self.uiInterfaceRotated.currentIndexChanged.connect(lambda index:
			settings.setValue('interface rotation', '180' if index else '0') )
		settings.observe('interface rotation', None, self.updateInterfaceSide)
		
		settings.observe('theme', 'dark', self.updateInterfaceSide)
		
		#Note the operations attached here:
		#	- We must observe a silenced callback to update the state. This prevents an infinite loop.
		#	- We update the state from a callback attached to the widget.
		settings.observe('ask before discarding', 'if not reviewed', self.updateAskBeforeDiscarding)
		self.uiAskBeforeDiscarding.currentIndexChanged.connect(lambda index:
			settings.setValue('ask before discarding',
				["always", "if not reviewed", "never"][index] ) )
		
		
		api.observe('dateTime', self.stopEditingDate) #When the date is changed, always display the update even if an edit is in progress. Someone probably set the date some other way instead of this, or this was being edited in error.
		self.uiSystemTime.focusInEvent = self.sysTimeFocusIn
		self.uiSystemTime.editingFinished.connect(self.sysTimeFocusOut)
		self._timeUpdateTimer = QtCore.QTimer()
		self._timeUpdateTimer.timeout.connect(self.updateDisplayedSystemTime)
		
		
		self.uiDone.clicked.connect(window.back)
Example #10
0
	def __init__(self, window):
		super().__init__()
		self.setupUi(self)
		
		# API init.
		self.control = api.control()

		# Panel init.
		self.setFixedSize(window.app.primaryScreen().virtualSize())
		self.setWindowFlags(QtCore.Qt.FramelessWindowHint)
		self.setAttribute(QtCore.Qt.WA_TranslucentBackground, True)
		
		#Color Temperature
		try:
			api.observe('wbTemperature', lambda temp:
				self.uiColorTemperature.setValue(temp) )
			self.uiColorTemperature.valueChanged.connect(lambda temp:
				self.control.set('wbTemperature', temp) )
		except AssertionError as e:
			log.error(f'Could not connect widgets to wbTemperature.\n{e}')
		
		#White Balance Matrix
		try:
			api.observe('wbColor', lambda color: (
				self.uiWBRed.setValue(color[0]), 
				self.uiWBGreen.setValue(color[1]), 
				self.uiWBBlue.setValue(color[2]),
			))
			for wbInput in [self.uiWBRed, self.uiWBGreen, self.uiWBBlue]:
				wbInput.valueChanged.connect(lambda *_:
					self.control.set('wbColor', [
						self.uiWBRed.value(), 
						self.uiWBGreen.value(), 
						self.uiWBBlue.value(),
					]) )
		except AssertionError as e:
			log.error(f'Could not connect widgets to wbColor.\n{e}')
		
		#Color Matrix Preset
		self.uiColorMatrixPreset.clear()
		self.uiColorMatrixPreset.insertItem(0, self.tr('CIECAM02/D55'), 'CIECAM02/D55')
		self.uiColorMatrixPreset.insertItem(1, self.tr('CIECAM16/D55'), 'CIECAM16/D55')
		self.uiColorMatrixPreset.insertItem(2, self.tr('Identity'), 'identity')
		self.uiColorMatrixPreset.insertItem(3, self.tr('Custom'), None)
		self.uiColorMatrixPreset.currentIndexChanged.connect(lambda index:
			self.setCM(
				presets[self.uiColorMatrixPreset.itemData(index)]
				if self.uiColorMatrixPreset.itemData(index) else
				settings.value('customColorMatrix', presets['identity'])
			)
		)
		
		#Color Matrix
		api.observe('colorMatrix', self.updateCMInputs)
		for cmInput in self.colorMatrixInputs():
			cmInput.valueChanged.connect(self.cmUpdated)
		
		#Navigation
		self.uiDone.clicked.connect(window.back)
	def deletePreset(self, *_):
		settings.setValue('customRecordingPresets', [
			setting
			for setting in settings.value('customRecordingPresets', [])
			if setting != self.uiPresets.currentData()
		])
		self.uiPresets.removeItem(self.uiPresets.currentIndex())
		self.selectCorrectPreset() #Select Custom again.
Example #12
0
	def onStateChange(self, state):
		#This text update takes a little while to fire, so we do it in the start and stop recording functions as well so it's more responsive when the operator clicks.
		self.uiRecord.isRecording = state == 'idle'
		self.uiRecord.update() #Redraw the icon, maybe start the timer.
		if self.uiRecord.isRecording:
			self.uiRecord.setText(
				self.uiRecordTemplateNoTime.format(state='Record') )
		
		if state == 'idle' and settings.value('autoSaveVideo', False): #[autosave]
			self._window.show('play_and_save')
	def savePreset(self):
		itemData = self.uiPresets.currentData()
		itemData['temporary'] = False
		itemData['name'] = f"{self.uiHRes.value()}×{self.uiVRes.value()} @ {int(self.uiFps.value())}fps" #Store name, probably will be editable one day.
		presets = [itemData] + settings.value('customRecordingPresets', [])
		settings.setValue('customRecordingPresets', presets)
		self.uiPresets.setItemData(self.uiPresets.currentIndex(), itemData)
		self.uiPresets.setItemText(self.uiPresets.currentIndex(), itemData['name'])
		
		self.updatePresetDropdownButtons()
Example #14
0
	def onTriggerChanged(self, index: int):
		activeAction = actionData[self.uiActionList.selectionModel().currentIndex().data(Qt.UserRole)]
		newSource = triggerData[self.uiTriggerList.itemData(index)]['id']
		activeMapping = self.newIOMapping[activeAction['id']]
		virtuals = settings.value('virtually triggered actions', {})
		oldSource = api.apiValues.get('ioMapping')[activeAction['id']]['source']
		if oldSource == newSource and activeAction['id'] not in virtuals: #If this was a virtual item, don't delete it if it's the same as what's corporeal. (Virtual defaults to "none", so if we switch from "virtual" to "none" we're actually switching from "none" to "none" which gets optimized out, which means we don't actually switch.
			if activeMapping['source']: #Clear key if it exists, no need to set. Otherwise, when we switched actions, the trigger would always get set to what it was, which is pointless.
				del activeMapping['source']
		else:
			activeMapping['source'] = triggerData[self.uiTriggerList.itemData(index)]['id']
Example #15
0
	def onShow(self):
		self.video.call('configure', {
			'xoff': self.uiPinchToZoomGestureInterceptionPanel.x(),
			'yoff': self.uiPinchToZoomGestureInterceptionPanel.y(),
			'hres': self.uiPinchToZoomGestureInterceptionPanel.width(),
			'vres': self.uiPinchToZoomGestureInterceptionPanel.height(),
		})
		self.video.call('livedisplay', {})
		self._batteryChargeUpdateTimer.start() #ms
		
		if api.apiValues.get('state') == 'idle' and settings.value('resumeRecordingAfterSave', False):
			self.startRecording()
Example #16
0
	def __init__(self, window):
		super().__init__()
		self.setupUi(self)

		# API init.
		self.control = api.control()
		
		# Panel init.
		self.setFixedSize(window.app.primaryScreen().virtualSize())
		self.setWindowFlags(QtCore.Qt.FramelessWindowHint)
		self.setAttribute(QtCore.Qt.WA_TranslucentBackground, True)
		
		# Set up panel switching.
		#DDR 2018-07-24: It's impossible to associate an identifier with anything in QT Designer. Painfully load the identifiers here. Also check everything because I will mess this up next time I add a trigger.
		#DDR 2019-08-01: Aaaand the safety check paid off. Told myself.
		if(self.uiRecordMode.count() != len(self.availableRecordModeIds)):
			raise Exception("Record mode screen available record mode IDs does not match the number of textual entries in uiRecordMode.")
		if(self.uiRecordModePanes.count() != len(self.availableRecordModeIds)):
			raise Exception("Record mode screen available record mode IDs does not match the number of uiRecordModePanes panes.")
		
		currentScreenId = settings.value('active record mode', self.availableRecordModeIds[0])
		if(currentScreenId not in self.availableRecordModeIds):
			print(f'{currentScreenId} is not a known record mode ID, defaulting to {self.availableRecordModeIds[0]}')
			currentScreenId = self.availableRecordModeIds[0]
		
		#Disable run-n-gun mode screen until it's added.
		self.uiRecordMode.removeItem(self.availableRecordModeIds.index('runAndGun'))
		
		self.uiRunNGunTimeInSeconds.template = self.uiRunNGunTimeInSeconds.text()
		self.uiRunNGunTimeInFrames.template = self.uiRunNGunTimeInFrames.text()
		self.uiRegularLengthInSeconds.template = self.uiRegularLengthInSeconds.text()
		self.uiRegularLengthInFrames.template = self.uiRegularLengthInFrames.text()
		self.uiBurstTimeInSeconds.template = self.uiBurstTimeInSeconds.text()
		self.uiBurstTimeInFrames.template = self.uiBurstTimeInFrames.text()
		
		# Widget behavour.
		api.observe('recMode', self.setCurrentScreenIndexFromRecordMode)
		self.uiRecordMode.currentIndexChanged.connect(self.changeShownTrigger)
		
		api.observe('cameraMaxFrames', self.recalculateEverything)
		api.observe('recSegments',     self.recalculateEverything)
		api.observe('recMaxFrames',    self.recalculateEverything)
		api.observe('framePeriod',     self.recalculateEverything)
		
		self.uiSegmentLengthInSeconds.valueChanged.connect(lambda sec:
			self.uiSegmentLengthInFrames.setValue(
				int(sec * 1e9 / api.apiValues.get('framePeriod') * api.apiValues.get('recSegments')) ) )
		self.uiSegmentLengthNumSegments.valueChanged.connect(lambda segments:
			self.control.setSync('recSegments', segments) )
		self.uiSegmentLengthInFrames.valueChanged.connect(lambda frames:
			self.control.setSync('recMaxFrames', frames * api.apiValues.get('recSegments')) )
		
		self.uiDone.clicked.connect(window.back)
Example #17
0
	def debug(self, *_):
		print()
		print()
		print('existing:')
		pp(api.apiValues.get('ioMapping'))
		print()
		print('pending:')
		pp(self.newIOMapping)
		print()
		print('virtual:')
		pp(settings.value('virtually triggered actions', {}))
		print()
		dbg()
Example #18
0
	def saveChanges(self, *_):
		ioMapping = api.apiValues.get('ioMapping')
		
		existingVirtuals = settings.value('virtually triggered actions', {})
		newVirtuals = {
			trigger
			for trigger, configuration in self.newIOMapping.items()
			if 'virtual' in configuration.values()
		}
		
		virtuals = { #Merge old and new virtuals configurations, filtering out old virtuals which are no longer and adding new virtuals.
			action: { #Merge the keys of the old and new virtual, since someone could have updated only, say, invert.
				key: default(
					self.newIOMapping.get(action, {}).get(key), #Find "most recent" value, defaulting to false if it was never set.
					existingVirtuals.get(action, {}).get(key),
					False )
				for key in self.newIOMapping.get(action, {}).keys() | existingVirtuals.get(action, {}).keys()
			}
			for action in existingVirtuals.keys() | newVirtuals #Consider only actions which are or were virtuals. Now, we need to filter out items which aren't verticals any more, and merge those which are.
			if self.newIOMapping.get(action, {}).get('source') in (None, 'virtual') #Filter out newly non-virtual actions, allowing unchanged and newly-virtual actions.
		}
		settings.setValue('virtually triggered actions', virtuals)
		pp({
			'newIOMapping': self.newIOMapping,
			'existingVirtuals': existingVirtuals, 
			'newVirtuals': newVirtuals, 
			'virtuals': virtuals,
		})
		
		self.control.set('ioMapping', dump('sending IO mapping', { #Send along the update delta. Strictly speaking, we could send the whole thing and not a delta, but it seems more correct to only send what we want to change. data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACMAAAAjCAMAAAApB0NrAAAAdVBMVEUAAAAAFBwCJTYbKjQjOUwoPlI/QT5GTFRDVmVQY3N4XGhgZGbUPobePYXgQY2BZnedZ4GBeH7EYqCXdYjbX5yigJOIjI+NkZTgfai5jZ2gnaHkire6m6fknsPxm8CtsrToqczrr8S9wsTwttHztszzuNPe4N2lYYACAAAAAXRSTlMAQObYZgAAAaBJREFUOMt9kwFTgzAMhVGn4ibMuqql40WFxP//E01TNgpu5o47Lnx9SV9CVS3Dp6iuRVPEZeK1RJrWN43/V+WKWinjg2/zyzUZDxGGvyA0MwkhypC/zHgiFgiqNeObkiGAyXIFo00W3QBdB5h0wWieMtVi7GJ0255XjCcRIR8ABMHh/WOIyEy1ZIRF0Onjhrr+jFbreWZaZGDvemX7n7h7ykxRrDkyCXo3DIOa0+/cw+YddnnvX086XjvBNsbaKfPtNjf3W3xpeuEOhPpt/Nnd98yEt/1LMnE1CO0azkX3eNCqzNBL0Hr31Dp+c5uHu4OoxToMnWr1FwpdrG83qZY5gYkBUMzUd67ew0RERhCMSHgx94A+pcxPQqoG40umBfOYETtPoHwCxf4cNat7ocshpgAUTDY1cD5MYypmTU2qCVHtYEs6B86t2cXUZBYOQRaBUWYPIKPtT26QTufJoMkd8PS1bNNq8Oxkdk3s69HTCdKBnZ0BzU1Q285Ci2mdC6TFn2+3nOpUjo/JyCtMZZNh2+HARUMrSP21/zWMf3V+AVFTVrq9UKSZAAAAAElFTkSuQmCC
			action: 
				{
					key: default(newConfig[key], value)
					for key, value in ioMapping[action].items()
				}
				if action not in virtuals else
				{ 
					'source': 'none', #Disable the input of any virtual element. It is set to "always" to implement the virtual trigger firing.
					'invert': default(newConfig['invert'], ioMapping[action]['invert']),
					'debounce': default(newConfig['debounce'], ioMapping[action]['debounce']),
				}
			for action, newConfig in self.newIOMapping.items()
			if [value for value in newConfig.values() if value is not None]
		})).then(self.saveChanges2)
	def populatePresets(self):
		formatString = self.uiPresets.currentText()
		self.uiPresets.clear()
		
		for preset in settings.value('customRecordingPresets', []):
			self.uiPresets.insertItem(9999, preset['name'], preset)
		
		#Load from API.
		for preset in self.presets:
			self.uiPresets.insertItem(
				9999,
				formatString % (preset["hRes"], preset["vRes"], preset["framerate"]),
				{
					'custom': False, #Indicates value was saved by user.
					'temporary': False, #Indicates the "custom", the unsaved, preset.
					'values': {
						'uiHRes': preset["hRes"],
						'uiVRes': preset["vRes"],
						'uiFps': preset["framerate"],
					},
				}
			)
Example #20
0
	def reloadSSH(self):
		if not settings.value('ssh enabled', False):
			return
		
		self.uiSSHStatus.showMessage(
			f"Status: Working…",
			timeout = 0 )
		
		external_process.run(self,
			['service', 'ssh', 'reload'],
			
			lambda err: (
				self.uiSSHStatus.showError(
					f"Status: Error. (See journalctl -xn.)", 
					timeout = 0 ),
				log.error(f'Internal command failed with code {err}.'),
			),
			
			lambda *_:
				self.uiSSHStatus.showMessage(
					f"Status: Running.",
					timeout = 0 )
		)
Example #21
0
	def reloadHTTP(self):
		if not settings.value('http enabled', True):
			return
		
		self.uiHTTPStatus.showMessage(
			f"Status: Working…",
			timeout = 0 )
		
		external_process.run(self,
			['service', 'chronos-web-api', 'restart'],
			
			lambda err: (
				self.uiHTTPStatus.showError(
					f"Status: Error. See journalctl.", 
					timeout = 0 ),
				log.error(f'[4wQyPn] external process returned {err}'),
			),
			
			lambda *_:
				self.uiHTTPStatus.showMessage(
					f"Status: Running.",
					timeout = 0 )
		)
Example #22
0
def durationForMp4(filesize: int,
                   hRes: Optional[int] = None,
                   vRes: Optional[int] = None,
                   frameRate: Optional[float] = None,
                   *,
                   bpp: Optional[float] = None,
                   maxBitrate: Optional[float] = None) -> timedelta:
    """Estimate the duration of an MP4 video of a known filesize.
		
		Args:
			size (int): Number of bytes in the video file.
			hRes (int, optional): Horizontal size of the video. Defaults to
				the current size of video being recorded.
			vRes (int, optional): Vertical size of the video. Defaults to
				the current size of video being recorded.
			frameRate (float, optional): The number of frames per second the
				video being estimated plays back at. Defaults to settings'
				savedFileFramerate, which defaults to 30.

		Yields:
			timedelta: The roughly estimated run time of the video.
		
		Note:
			This is less precise than other video formats' estimations due to
				the compression used by the format. Videos with more motion
				can take more room than videos with less, which we can't
				compensate for without analysis we can't afford.
	"""

    if frameRate is None:
        frameRate = settings.value('savedFileFramerate', 30)

    sizeOfOneFrame = sizeForMp4(
        2000, hRes, vRes, frameRate=frameRate, bpp=bpp, maxBitrate=maxBitrate
    ) / 2000  #Lower error due to nonscaling overhead, ie, one-off headers are a significant chunk of a one-frame video, but irrelevant for a 100,000 frame video.
    totalFrames = int(filesize / sizeOfOneFrame)
    return timedelta(seconds=totalFrames / frameRate)
Example #23
0
	def __init__(self, app):
		super().__init__()
		
		self.app = app
		app.window = self #Yuck. Couldn't find a decent way to plumb self.showInput through to the widgets otherwise.
		
		
		
		################################
		# Screen-loading functionality #
		################################
		
		from .screens.main2 import Main
		from .screens.play_and_save import PlayAndSave
		from .screens.recording_settings import RecordingSettings
		from .screens.storage import Storage
		from .screens.color import Color
		from .screens.about_camera import AboutCamera
		from .screens.file_settings import FileSettings
		from .screens.power import Power
		from .screens.primary_settings import PrimarySettings
		from .screens.record_mode import RecordMode
		from .screens.remote_access import RemoteAccess
		from .screens.replay import Replay
		from .screens.scripts import Scripts
		from .screens.service_screen import ServiceScreenLocked, ServiceScreenUnlocked
		from .screens.stamp import Stamp
		from .screens.test import Test
		from .screens.trigger_delay import TriggerDelay
		from .screens.triggers_and_io import TriggersAndIO
		from .screens.update_firmware import UpdateFirmware
		from .screens.user_settings import UserSettings
		
		self._availableScreens = {
			'main': Main, #load order, load items on main screen first, main screen submenus next, and it doesn't really matter after that.
			'play_and_save': PlayAndSave,
			'recording_settings': RecordingSettings,
			'about_camera': AboutCamera,
			'color': Color,
			'storage': Storage,
			'file_settings': FileSettings,
			'power': Power,
			'primary_settings': PrimarySettings,
			'record_mode': RecordMode,
			'remote_access': RemoteAccess,
			'replay': Replay,
			'scripts': Scripts,
			'service_screen.locked': ServiceScreenLocked,
			'service_screen.unlocked': ServiceScreenUnlocked,
			'stamp': Stamp,
			'test': Test,
			'trigger_delay': TriggerDelay,
			'triggers_and_io': TriggersAndIO,
			'update_firmware': UpdateFirmware,
			'user_settings': UserSettings,
		}
		
		self._screens = {}
		
		# Set the initial screen. If in dev mode, due to the frequent restarts,
		# reopen the previous screen. If in the hands of an end-user, always
		# open the main screen when rebooting to provide an escape route for a
		# confusing or broken screen.
		
		#settings.setValue('current screen', 'widget_test')
		
		if settings.value('debug controls enabled', False):
			self.currentScreen = settings.value('current screen', 'main')
		else:
			self.currentScreen = 'main'
		
		if self.currentScreen not in self._availableScreens: 
			self.currentScreen = 'main'
		
		self._screenStack = ['main', self.currentScreen] if self.currentScreen != 'main' else ['main'] #Start off with main loaded into history, since we don't always start on main during development and going back should get you *somewhere* useful rather than crashing.
		
		self._ensureInstantiated(self.currentScreen)
		
		#for screen in self._availableScreens:
		#	self._ensureInstantiated(screen)
		
		if hasattr(self._screens[self.currentScreen], 'onShow'):
			self._screens[self.currentScreen].onShow()
		self._screens[self.currentScreen].show()
		
		settings.setValue('current screen', self.currentScreen)
		
		#Cache all screens, cached screens load about 150-200ms faster I think.
		self._lazyLoadTimer = QtCore.QTimer()
		self._lazyLoadTimer.timeout.connect(self._loadAScreen)
		self._lazyLoadTimer.setSingleShot(True)
		PRECACHE_ALL_SCREENS and self._lazyLoadTimer.start(0) #ms
		
		
		from chronosGui2.input_panels import KeyboardNumericWithUnits, KeyboardNumericWithoutUnits
		from chronosGui2.input_panels import KeyboardAlphanumeric
		self._keyboards = {
			"numeric_with_units": KeyboardNumericWithUnits(self),
			"numeric_without_units": KeyboardNumericWithoutUnits(self),
			"alphanumeric": KeyboardAlphanumeric(self),
		}
		
		self._activeKeyboard = ''
Example #24
0
	def saveMarkedRegion(self):
		"""Save the next marked region.
			
			Note: This method is invoked from a button and from a state-change
			watcher. This means that, once started, it will be called for each
			marked region sequentially as saving completes."""
		uuid = settings.value('preferredFileSavingUUID', '')
		if uuid in [part['uuid'] for part in api.externalPartitions.list()]:
			#Use the operator-set partition.
			partition = [part for part in api.externalPartitions.list() if part['uuid'] == uuid][0]
		elif api.externalPartitions.list()[-1:]:
			#The operator-set partition is not mounted. Use the most recent partition instead.
			partition = api.externalPartitions.list()[-1]
		else:
			#No media is usable for saving.
			raise type(self).NoSaveMedia()
		
		roi = [r for r in self.markedRegions if r['saved'] == 0][:1]
		if not roi:
			self.markedStart = self.uiSeekSlider.minimum()
			self.markedEnd = self.uiSeekSlider.maximum()
			self.addMarkedRegion()
			roi = [r for r in self.markedRegions if r['saved'] == 0][:1]
			if not roi:
				raise Exception()
		roi = roi[0]
		self.regionBeingSaved = roi['region id']
		self.uiSeekSlider.setEnabled(False)
		
		now = datetime.now()
		res = self.control.getSync('resolution') #TODO DDR 2019-07-26 Get this from the segment metadata we don't have as of writing.
		roi['file'] = f'''{
			partition['path'].decode('utf-8')
		}/{
			(settings.value('savedVideoName', '') or r'vid_%date%_%time%')
				.replace(r'%region name%', str(roi['region name']))
				.replace(r'%date%', now.strftime("%Y-%m-%d"))
				.replace(r'%time%', now.strftime("%H-%M-%S"))
				.replace(r'%start frame%', str(roi['mark start']))
				.replace(r'%end frame%', str(roi['mark end']))
		}{
			settings.value('savedVideoFileExtention', '.mp4')
		}'''
		
		self.uiSaveCancel.show()
		self.uiSave.hasFocus() and self.uiSaveCancel.setFocus()
		self.uiSave.hide()
		
		#regions are like {'region id': 'aaaaaaag', 'hue': 390, 'mark end': 42587, 'mark start': 16716, 'saved': 0.0, 'highlight': 0, 'segment ids': ['KxIjG09V'], 'region name': 'Clip 7'},
		self.video.callSync('recordfile', {
			'filename': roi['file'],
			'format': {
				'.mp4':'h264', 
				'.dng':'dng', 
				'.tiff':'tiff', 
				'.raw':'byr2',
			}[settings.value('savedVideoFileExtention', '.mp4')],
			'start': roi['mark start'],
			'length': roi['mark end'] - roi['mark start'],
			'framerate': settings.value('savedFileFramerate', 30), #TODO DDR 2019-07-24 read this from recording settings
			'bitrate': min(
				res['hRes'] * res['vRes'] * self.control.getSync('frameRate') * settings.value('savedFileBPP', 0.7),
				settings.value('savedFileMaxBitrate', 40) * 1000000.0,
			)
		})
Example #25
0
# General imports
import os, subprocess
import time

# QT-specific imports
from PyQt5 import QtWidgets, QtCore, QtGui

from chronosGui2.stats import report
from chronosGui2.debugger import *; dbg #imported for occasional use debugging, ignore "unused" warning
from chronosGui2 import settings
from chronosGui2.widgets.focus_ring import FocusRing
from chronosGui2.widgets.toaster_notification import ToasterNotificationQueue
import logging

#App performance settings
PRECACHE_ALL_SCREENS = not settings.value('debug controls enabled', False) #Precached screens are faster to open the first time, but impact UI performance for a few seconds on startup. Since during development I usually want the only screen I'm working on -now-, I added this option to turn it off. --DDR 2019-05-30

guiLog = logging.getLogger('Chronos.gui')
perfLog = logging.getLogger('Chronos.perf')

#Script setup
perf_start_time = time.perf_counter()

QtWidgets.QApplication.setAttribute(QtCore.Qt.AA_EnableHighDpiScaling, True)

#Make the UI run smoother by taking more resources.
with open(os.devnull, 'w') as null:
	subprocess.call(
		['renice', '-n', '-19', '-p', str(os.getpid())],
		stdout=null,
	)
Example #26
0
	def onActionChanged(self, selected: QItemSelection, deselected: QItemSelection = []):
		"""Update the UI when the selected action is changed.
			
			This function updates the trigger list, invert,
				and debounce elements. The trigger list will
				sync the active configuration pane, because
				it needs to do that anyway when the operator
				changes the trigger."""
		
		action = actionData[selected.indexes()[0].data(Qt.UserRole)]
		config = api.apiValues.get('ioMapping')[action['id']]
		newConfig = self.newIOMapping[action['id']]
		virtuals = settings.value('virtually triggered actions', {})
		
		if action['id'] in virtuals:
			source = newConfig['source'] or 'virtual'
			virtual = source == 'virtual'
		else:
			source = newConfig['source'] or config['source']
			virtual = None
		
		dataIndex = [trigger['id'] for trigger in triggerData].index(source)
		
		listIndex = self.uiTriggerList.findData(dataIndex)
		assert listIndex is not -1, f"Could not find index for {config['source']} ({dataIndex}) in uiTriggerList."
		
		try: #Fix for state getting marked dirty when we view a different action.
			self.uiTriggerList.currentIndexChanged.disconnect(self.markStateDirty)
			self.uiTriggerList.setCurrentIndex(listIndex)
			self.uiTriggerList.currentIndexChanged.connect(self.markStateDirty)
		except TypeError: #'method' object is not connected, whatever, just set index then.
			self.uiTriggerList.setCurrentIndex(listIndex)
		
		self.uiInvertCondition.blockSignals(True) #We can block these signals because nothing else depends on them. A few things depend on the Trigger for Action combobox, so it is just a little smarter about it's updates to deal with this changing.
		self.uiInvertCondition.setChecked(bool(default(
			newConfig['invert'], 
			(virtuals[action['id']].get('invert') or False) if virtual else None, #.get returns False or None, we always want False so default stops here if this is a virtual trigger.
			config['invert'],
		)))
		self.uiInvertCondition.blockSignals(False)
		
		self.uiDebounce.blockSignals(True)
		self.uiDebounce.setChecked(bool(default(
			newConfig['debounce'], 
			(virtuals[action['id']].get('debounce') or False) if virtual else None,
			config['debounce'],
		)))
		self.uiDebounce.blockSignals(False)
		
		#Update action label text for action level/edge triggering.
		assert ('level' in action['tags']) != ('edge' in action['tags']), f"actionData['{action['id']}'] needs to be tagged as either 'level' or 'edge' triggered."
		if not deselected or not deselected.indexes():
			return
		if 'level' in action['tags'] == 'level' in actionData[deselected.indexes()[0].data(Qt.UserRole)]['tags']:
			return
		
		whenLevelOrEdge = 'whenLevelTriggered' if 'level' in action['tags'] else 'whenEdgeTriggered'
		for triggerListIndex in range(self.uiTriggerList.count()):
			triggerDataIndex = self.uiTriggerList.itemData(triggerListIndex)
			self.uiTriggerList.setItemText(triggerListIndex,
				triggerData[triggerDataIndex]['name'][whenLevelOrEdge] )
Example #27
0
	def onNewIOMapping(self, ioMapping: dict):
		"""Update the IO display with new values, overriding any pending changes."""
		selectedAction = actionData[self.uiActionList.selectionModel().currentIndex().data(Qt.UserRole) or 0]['id']
		virtuals = settings.value('virtually triggered actions', {}) #Different from soft trigger, that's a hardware-based interrupt thing. The virtual trigger is a gui2 hallucination.
		
		for action in ioMapping:
			if ioMapping[action] == self.oldIOMapping[action]:
				continue
			
			if updateMode == DISCARD_CHANGES:
				#Override any pending changes to this trigger, since it's been updated elsewhere.
				try: 
					del self.newIOMapping[action]
				except KeyError:
					pass
			
			state = ioMapping[action]
			delta = self.newIOMapping[action]
			
			if action in virtuals:
				virtual = virtuals[action]
				
				self.uiInvertCondition.blockSignals(True)
				self.uiInvertCondition.setChecked(default(
					delta['invert'], virtual.get('invert'), False ))
				self.uiInvertCondition.blockSignals(False)
				
				self.uiDebounce.blockSignals(True)
				self.uiDebounce.setChecked(default(
					delta['debounce'], virtual.get('debounce'), False ))
				self.uiDebounce.blockSignals(False)
				
				continue
			
			#If the trigger is active, update the invert and debounce common conditions.
			if action == selectedAction:
				self.uiInvertCondition.blockSignals(True)
				self.uiInvertCondition.setChecked(bool(default(
					delta['invert'], state['invert'] ))) #tristate checkboxes
				self.uiInvertCondition.blockSignals(False)
				
				self.uiDebounce.blockSignals(True)
				self.uiDebounce.setChecked(bool(default(
					delta['debounce'], state['debounce'] )))
				self.uiDebounce.blockSignals(False)
			
			if action == "io1":
				self.uiIo11MAPullup.setChecked(bool(1 & default(
					delta['driveStrength'], state['driveStrength'] )))
				self.uiIo120MAPullup.setChecked(bool(2 & default(
					delta['driveStrength'], state['driveStrength'] )))
			elif action == "io1In":
				self.uiIo1ThresholdVoltage.setValue(default(
					delta['threshold'], state['threshold'] ))
			elif action == "io2":
				self.uiIo220MAPullup.setChecked(bool(default(
					delta['driveStrength'], state['driveStrength'] )))
			elif action == "io2In":
				self.uiIo2ThresholdVoltage.setValue(default(
					delta['threshold'], state['threshold'] ))
			elif action == "delay":
				self.uiDelayAmount.setValue(default(
					delta['delayTime'], state['delayTime'] ))
		
		self.oldIOMapping = ioMapping
		
		#Check if all operator changes have been overwritten by updates.
		if not (value for value in self.newIOMapping if value):
			self.markStateClean()
Example #28
0
	def onStateChange(self, state):
		#This text update takes a little while to fire, so we do it in the start and stop recording functions as well so it's more responsive when the operator clicks.
		self.uiRecord.setText('Rec' if state == 'idle' else 'Stop')
		
		if state == 'idle' and settings.value('autoSaveVideo', False): #[autosave]
			self._window.show('play_and_save')
Example #29
0
	def __init__(self, window):
		super().__init__()
		self.setupUi(self)
		
		# Panel init.
		self.setFixedSize(window.app.primaryScreen().virtualSize())
		self.setWindowFlags(QtCore.Qt.FramelessWindowHint)
		self.setAttribute(QtCore.Qt.WA_TranslucentBackground, True)
		
		# Hide some stuff we haven't programmed yet. (Filesize, file name preview.)
		for id_ in {
			'headerlabel_2', 'uiDiskSpaceVisualization',
			'frame', 'frame_2', 'frame_3',
			'uiDiskSpaceFree', 'uiDiskSpaceRequired', 'uiDiskSpaceUsed',
			'label_4', 'widget_2', 'label_5'
		}:
			getattr(self, id_).deleteLater()
			
			
			
		
		# Button binding.
		#The preferred external partition is the one set in settings' preferredFileSavingUUID, OR the most recent partition.
		settings.observe('preferredFileSavingUUID', '', self.setPreferredSavingDevice)
		api.externalPartitions.observe(lambda *_: self.setPreferredSavingDevice(
			settings.value('preferredFileSavingUUID', '') ))
		self.uiSavedVideoLocation.currentIndexChanged.connect(lambda *_: 
			self.uiSavedVideoLocation.hasFocus() and settings.setValue(
				'preferredFileSavingUUID',
				(self.uiSavedVideoLocation.currentData() or {}).get('uuid') ) )
		
		
		self.uiSavedVideoName.setText(
			settings.value('savedVideoName', self.uiSavedVideoName.text()) )
		self.uiSavedVideoName.textChanged.connect(lambda value:
			settings.setValue('savedVideoName', value) )
		
		self.uiSavedVideoFileExtention.setCurrentText(
			settings.value('savedVideoFileExtention', self.uiSavedVideoFileExtention.currentText()) )
		self.uiSavedVideoFileExtention.currentTextChanged.connect(lambda value:
			settings.setValue('savedVideoFileExtention', value) )
		
		
		self.uiSavedFileFramerate.setValue(
			settings.value('savedFileFramerate', self.uiSavedFileFramerate.value()) )
		self.uiSavedFileFramerate.valueChanged.connect(lambda value:
			settings.setValue('savedFileFramerate', value) )
		
		self.uiSavedFileBPP.setValue(
			settings.value('savedFileBPP', self.uiSavedFileBPP.value()) )
		self.uiSavedFileBPP.valueChanged.connect(lambda value:
			settings.setValue('savedFileBPP', value) )
		
		self.uiSavedFileMaxBitrate.setValue(
			settings.value('savedFileMaxBitrate', self.uiSavedFileMaxBitrate.value()) )
		self.uiSavedFileMaxBitrate.valueChanged.connect(lambda value:
			settings.setValue('savedFileMaxBitrate', value) )
		
		
		self.uiAutoSaveVideo.setCheckState( #[autosave]
			bool(settings.value('autoSaveVideo', self.uiAutoSaveVideo.checkState())) * 2 )
		self.uiAutoSaveVideo.stateChanged.connect(lambda value:
			settings.setValue('autoSaveVideo', bool(value)) )
		
		self.uiResumeRecordingAfterSave.setCheckState( #[autosave]
			bool(settings.value('resumeRecordingAfterSave', self.uiResumeRecordingAfterSave.checkState())) * 2 )
		self.uiResumeRecordingAfterSave.stateChanged.connect(lambda value:
			settings.setValue('resumeRecordingAfterSave', bool(value)) )
		
		
		self.uiFormatStorage.clicked.connect(lambda: window.show('storage'))
		self.uiDone.clicked.connect(window.back)
	
	
		self.uiSafelyRemove.clicked.connect(lambda: window.show('storage'))