Beispiel #1
0
	def startThreads( self ):
		self.grabFrameOK = False
		
		self.listenerThread = SocketListener( self.requestQ, self.messageQ )
		error = self.listenerThread.test()
		if error:
			wx.MessageBox('Socket Error:\n\n{}\n\nIs another CrossMgrVideo or CrossMgrCamera running on this computer?'.format(error),
				"Socket Error",
				wx.OK | wx.ICON_ERROR
			)
			wx.Exit()
		
		self.camInQ, self.camReader = CamServer.getCamServer( self.getCameraInfo() )
		self.cameraThread = threading.Thread( target=self.processCamera )
		self.cameraThread.daemon = True
		
		self.eventThread = threading.Thread( target=self.processRequests )
		self.eventThread.daemon = True
		
		self.dbWriterThread = threading.Thread( target=DBWriter, args=(self.dbWriterQ, lambda: wx.CallAfter(self.delayRefreshTriggers), self.db.fname) )
		self.dbWriterThread.daemon = True
		
		self.cameraThread.start()
		self.eventThread.start()
		self.dbWriterThread.start()
		self.listenerThread.start()
		
		self.grabFrameOK = True
		self.messageQ.put( ('threads', 'Successfully Launched') )
		
		self.primaryFreq = 5
		self.camInQ.put( {'cmd':'send_update', 'name':'primary', 'freq':self.primaryFreq} )
		return True
Beispiel #2
0
    def startThreads(self):
        self.grabFrameOK = False

        self.listenerThread = SocketListener(self.requestQ, self.messageQ)
        error = self.listenerThread.test()
        if error:
            wx.MessageBox(
                'Socket Error:\n\n{}\n\nIs another CrossMgrVideo or CrossMgrCamera running on this computer?'
                .format(error), "Socket Error", wx.OK | wx.ICON_ERROR)
            wx.Exit()

        self.dbWriterThread = threading.Thread(target=DBWriter,
                                               args=(self.dbWriterQ, ))
        self.dbWriterThread.daemon = True

        self.dbReaderThread = threading.Thread(target=DBReader,
                                               args=(self.dbReaderQ,
                                                     self.setFinishStripJpgs))
        self.dbReaderThread.daemon = True

        self.fcb = FrameCircBuf(int(self.bufferSecs * self.fps))

        self.listenerThread.start()
        self.dbWriterThread.start()
        self.dbReaderThread.start()

        self.grabFrameOK = True
        self.messageQ.put(('threads', 'Successfully Launched'))
        return True
Beispiel #3
0
	def startThreads( self ):
		self.grabFrameOK = False
		
		self.listenerThread = SocketListener( self.requestQ, self.messageQ )
		error = self.listenerThread.test()
		if error:
			wx.MessageBox('Socket Error:\n\n{}\n\nIs another CrossMgrVideo or CrossMgrCamera running on this computer?'.format(error),
				"Socket Error",
				wx.OK | wx.ICON_ERROR
			)
			wx.Exit()
				
		self.writerThread = threading.Thread( target=PhotoWriter, args=(self.writerQ, self.messageQ, self.ftpQ) )
		self.writerThread.daemon = True
		
		self.renamerThread = threading.Thread( target=PhotoRenamer, args=(self.renamerQ, self.writerQ, self.messageQ) )
		self.renamerThread.daemon = True
		
		self.ftpThread = threading.Thread( target=FTPWriter, args=(self.ftpQ, self.messageQ) )
		self.ftpThread.daemon = True
		
		self.fcb = FrameCircBuf( int(self.bufferSecs * self.fps) )
		
		self.listenerThread.start()
		self.writerThread.start()
		self.renamerThread.start()
		self.ftpThread.start()
		
		self.grabFrameOK = True
		self.messageQ.put( ('threads', 'Successfully Launched') )
		return True
Beispiel #4
0
class MainWin( wx.Frame ):
	def __init__( self, parent, id = wx.ID_ANY, title='', size=(1000,800) ):
		wx.Frame.__init__(self, parent, id, title, size=size)
		
		self.fps = 25
		self.frameDelay = 1.0 / self.fps
		self.bufferSecs = 10
		
		self.tFrameCount = self.tLaunch = self.tLast = now()
		self.frameCount = 0
		self.frameCountUpdate = self.fps * 2
		self.fpsActual = 0.0
		self.fpt = timedelta(seconds=0)
		
		self.fcb = FrameCircBuf( self.bufferSecs * self.fps )
		
		self.config = wx.Config(appName="CrossMgrCamera",
						vendorName="SmartCyclingSolutions",
						#style=wx.CONFIG_USE_LOCAL_FILE
		)
		
		self.requestQ = Queue()			# Select photos from photobuf.
		self.writerQ = Queue( 400 )		# Selected photos waiting to be written out.
		self.ftpQ = Queue()				# Photos waiting to be ftp'd.
		self.renamerQ = Queue()			# Photos waiting to be renamed and possibly ftp'd.
		self.messageQ = Queue()			# Collection point for all status/failure messages.
		
		self.SetBackgroundColour( wx.Colour(232,232,232) )
		
		mainSizer = wx.BoxSizer( wx.VERTICAL )
		
		self.primaryImage = ScaledImage( self, style=wx.BORDER_SUNKEN, size=(imageWidth, imageHeight) )
		self.beforeImage = ScaledImage( self, style=wx.BORDER_SUNKEN, size=(imageWidth, imageHeight) )
		self.afterImage = ScaledImage( self, style=wx.BORDER_SUNKEN, size=(imageWidth, imageHeight) )
		self.beforeAfterImages = [self.beforeImage, self.afterImage]
		
		#------------------------------------------------------------------------------------------------
		headerSizer = wx.BoxSizer( wx.HORIZONTAL )
		
		self.logo = Utils.GetPngBitmap('CrossMgrHeader.png')
		headerSizer.Add( wx.StaticBitmap(self, wx.ID_ANY, self.logo) )
		
		self.title = wx.StaticText(self, label='CrossMgr Camera\nVersion {}'.format(AppVerName.split()[1]), style=wx.ALIGN_RIGHT )
		self.title.SetFont( wx.Font( (0,28), wx.FONTFAMILY_SWISS, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL ) )
		headerSizer.Add( self.title, flag=wx.ALL, border=10 )
		
		#------------------------------------------------------------------------------
		self.cameraDeviceLabel = wx.StaticText(self, label='Camera Device:')
		self.cameraDevice = wx.StaticText( self )
		boldFont = self.cameraDevice.GetFont()
		boldFont.SetWeight( wx.BOLD )
		self.cameraDevice.SetFont( boldFont )
		self.cameraResolution = wx.StaticText( self )
		self.cameraResolution.SetFont( boldFont )
		bitmap = wx.Bitmap( clipboard_xpm )
		self.copyLogToClipboard = wx.BitmapButton( self, bitmap=bitmap )
		self.copyLogToClipboard.Bind( wx.EVT_BUTTON, self.onCopyLogToClipboard )
		self.reset = wx.Button( self, label="Reset Camera" )
		self.reset.Bind( wx.EVT_BUTTON, self.resetCamera )
		cameraDeviceSizer = wx.BoxSizer( wx.HORIZONTAL )
		cameraDeviceSizer.Add( self.cameraDeviceLabel, flag=wx.ALIGN_CENTRE_VERTICAL|wx.ALIGN_RIGHT )
		cameraDeviceSizer.Add( self.cameraDevice, flag=wx.ALIGN_CENTRE_VERTICAL|wx.LEFT, border=8 )
		cameraDeviceSizer.Add( self.cameraResolution, flag=wx.ALIGN_CENTRE_VERTICAL|wx.LEFT, border=8 )
		cameraDeviceSizer.Add( self.copyLogToClipboard, flag=wx.ALIGN_CENTRE_VERTICAL|wx.LEFT, border=8 )
		cameraDeviceSizer.Add( self.reset, flag=wx.ALIGN_CENTRE_VERTICAL|wx.LEFT, border=24 )

		#------------------------------------------------------------------------------
		self.targetProcessingTimeLabel = wx.StaticText(self, label='Target Frames:')
		self.targetProcessingTime = wx.StaticText(self, label=u'{:.3f}'.format(self.fps))
		self.targetProcessingTime.SetFont( boldFont )
		self.targetProcessingTimeUnit = wx.StaticText(self, label='per sec')
		
		self.framesPerSecondLabel = wx.StaticText(self, label='Actual Frames:')
		self.framesPerSecond = wx.StaticText(self, label='25.000')
		self.framesPerSecond.SetFont( boldFont )
		self.framesPerSecondUnit = wx.StaticText(self, label='per sec')
		
		self.availableMsPerFrameLabel = wx.StaticText(self, label='Available Time Per Frame:')
		self.availableMsPerFrame = wx.StaticText(self, label=u'{:.0f}'.format(1000.0*self.frameDelay))
		self.availableMsPerFrame.SetFont( boldFont )
		self.availableMsPerFrameUnit = wx.StaticText(self, label='ms')
		
		self.frameProcessingTimeLabel = wx.StaticText(self, label='Actual Frame Processing:')
		self.frameProcessingTime = wx.StaticText(self, label='20')
		self.frameProcessingTime.SetFont( boldFont )
		self.frameProcessingTimeUnit = wx.StaticText(self, label='ms')
		
		pfgs = wx.FlexGridSizer( rows=0, cols=6, vgap=4, hgap=8 )
		fRight = wx.ALIGN_CENTRE_VERTICAL|wx.ALIGN_RIGHT
		fLeft = wx.ALIGN_CENTRE_VERTICAL
		
		#------------------- Row 1 ------------------------------
		pfgs.Add( self.targetProcessingTimeLabel, flag=fRight )
		pfgs.Add( self.targetProcessingTime, flag=fRight )
		pfgs.Add( self.targetProcessingTimeUnit, flag=fLeft )
		pfgs.Add( self.availableMsPerFrameLabel, flag=fRight )
		pfgs.Add( self.availableMsPerFrame, flag=fRight )
		pfgs.Add( self.availableMsPerFrameUnit, flag=fLeft )
		
		#------------------- Row 2 ------------------------------
		pfgs.Add( self.framesPerSecondLabel, flag=fRight )
		pfgs.Add( self.framesPerSecond, flag=fRight )
		pfgs.Add( self.framesPerSecondUnit, flag=fLeft )
		pfgs.Add( self.frameProcessingTimeLabel, flag=fRight )
		pfgs.Add( self.frameProcessingTime, flag=fRight )
		pfgs.Add( self.frameProcessingTimeUnit, flag=fLeft )

		statsSizer = wx.BoxSizer( wx.VERTICAL )
		statsSizer.Add( cameraDeviceSizer )
		statsSizer.Add( pfgs, flag=wx.TOP, border=8 )
		headerSizer.Add( statsSizer, flag=wx.ALL, border=4 )
		mainSizer.Add( headerSizer )
		
		self.messagesText = wx.TextCtrl( self, style=wx.TE_READONLY|wx.TE_MULTILINE|wx.HSCROLL, size=(350,imageHeight) )
		self.messageManager = MessageManager( self.messagesText )
		
		border = 2
		row1Sizer = wx.BoxSizer( wx.HORIZONTAL )
		row1Sizer.Add( self.primaryImage, flag=wx.ALL, border=border )
		row1Sizer.Add( self.messagesText, 1, flag=wx.TOP|wx.BOTTOM|wx.RIGHT|wx.EXPAND, border=border )
		mainSizer.Add( row1Sizer, 1, flag=wx.EXPAND )
		
		row2Sizer = wx.BoxSizer( wx.HORIZONTAL )
		row2Sizer.Add( self.beforeImage, flag=wx.LEFT|wx.RIGHT|wx.BOTTOM, border=border )
		row2Sizer.Add( self.afterImage, flag=wx.RIGHT|wx.BOTTOM, border=border )
		mainSizer.Add( row2Sizer )
		
		#------------------------------------------------------------------------------------------------
		# Create a timer to update the frame loop.
		#
		self.timer = wx.Timer()
		self.timer.Bind( wx.EVT_TIMER, self.frameLoop )
		
		self.Bind(wx.EVT_CLOSE, self.onCloseWindow)

		self.readOptions()
		self.SetSizerAndFit( mainSizer )
		
		# Start the message reporting thread so we can see what is going on.
		self.messageThread = threading.Thread( target=self.showMessages )
		self.messageThread.daemon = True
		self.messageThread.start()
		
		self.grabFrameOK = False
		
		for i in [self.primaryImage, self.beforeImage, self.afterImage]:
			i.SetTestImage()
		
		# Start the frame loop.
		delayAdjustment = 0.80 if 'win' in sys.platform else 0.98
		ms = int(1000 * self.frameDelay * delayAdjustment)
		self.timer.Start( ms, False )
		
	def Start( self ):
		self.messageQ.put( ('', '************************************************') )
		self.messageQ.put( ('started', now().strftime('%Y/%m/%d %H:%M:%S')) )
		self.startThreads()
		self.startCamera()
	
	def onCopyLogToClipboard( self, event ):
		with open( redirectFileName, 'r' ) as fp:
			logData = fp.read()
		dataObj = wx.TextDataObject()
		dataObj.SetText(logData)
		if wx.TheClipboard.Open():
			wx.TheClipboard.SetData( dataObj )
			wx.TheClipboard.Close()
			Utils.MessageOK(self, u'\n\n'.join( [_("Log file copied to clipboard."), _("You can now paste it into an email.")] ), _("Success") )
		else:
			Utils.MessageOK(self, _("Unable to open the clipboard."), _("Error"), wx.ICON_ERROR )
	
	def showMessages( self ):
		while 1:
			message = self.messageQ.get()
			assert len(message) == 2, 'Incorrect message length'
			cmd, info = message
			wx.CallAfter( self.messageManager.write, '{}:  {}'.format(cmd, info) if cmd else info )
		
	def startCamera( self ):
		self.camera = None
		try:
			self.camera = Device( max(self.getCameraDeviceNum(), 0) )
		except Exception as e:
			self.messageQ.put( ('camera', 'Error: {}'.format(e)) )
			return False
		
		try:
			self.camera.set_resolution( *self.getCameraResolution() )
			self.messageQ.put( ('camera', '{}x{} Supported'.format(*self.getCameraResolution())) )
		except Exception as e:
			self.messageQ.put( ('camera', '{}x{} Unsupported Resolution'.format(*self.getCameraResolution())) )
			
		self.messageQ.put( ('camera', 'Successfully Connected: Device: {}'.format(self.getCameraDeviceNum()) ) )
		for i in self.beforeAfterImages:
			i.SetTestImage()
		return True
	
	def startThreads( self ):
		self.grabFrameOK = False
		
		self.listenerThread = SocketListener( self.requestQ, self.messageQ )
		error = self.listenerThread.test()
		if error:
			wx.MessageBox('Socket Error:\n\n{}\n\nIs another CrossMgrVideo or CrossMgrCamera running on this computer?'.format(error),
				"Socket Error",
				wx.OK | wx.ICON_ERROR
			)
			wx.Exit()
				
		self.writerThread = threading.Thread( target=PhotoWriter, args=(self.writerQ, self.messageQ, self.ftpQ) )
		self.writerThread.daemon = True
		
		self.renamerThread = threading.Thread( target=PhotoRenamer, args=(self.renamerQ, self.writerQ, self.messageQ) )
		self.renamerThread.daemon = True
		
		self.ftpThread = threading.Thread( target=FTPWriter, args=(self.ftpQ, self.messageQ) )
		self.ftpThread.daemon = True
		
		self.fcb = FrameCircBuf( int(self.bufferSecs * self.fps) )
		
		self.listenerThread.start()
		self.writerThread.start()
		self.renamerThread.start()
		self.ftpThread.start()
		
		self.grabFrameOK = True
		self.messageQ.put( ('threads', 'Successfully Launched') )
		return True
	
	def frameLoop( self, event=None ):
		if not self.grabFrameOK:
			return
			
		self.frameCount += 1
		tNow = now()
		
		# Compute frame rate statistics.
		if self.frameCount == self.frameCountUpdate:
			self.fpsActual = self.frameCount / (tNow - self.tFrameCount).total_seconds()
			self.frameCount = 0
			self.tFrameCount = tNow
			self.framesPerSecond.SetLabel( u'{:.3f}'.format(self.fpsActual) )
			self.availableMsPerFrame.SetLabel( u'{:.0f}'.format(1000.0/self.fpsActual) )
			self.frameProcessingTime.SetLabel( u'{:.0f}'.format(1000.0*self.fpt.total_seconds()) )
		
		try:
			image = self.camera.getImage()
		except Exception as e:
			self.messageQ.put( ('error', 'Webcam Failure: {}'.format(e) ) )
			self.grabFrameOK = False
			return
		
		if not image:
			return
		
		image = AddPhotoHeader( PilImageToWxImage(image),
			time=tNow, raceSeconds=(tNow - self.tLaunch).total_seconds(),
			bib=999,
			firstNameTxt=u'Firstname', lastNameTxt=u'LASTNAME', teamTxt=u'Team',
			raceNameTxt='Racename'
		)
		self.fcb.append( tNow, image )
		wx.CallAfter( self.primaryImage.SetImage, image )
		
		# Process any save messages
		while 1:
			try:
				message = self.requestQ.get(False)
			except Empty:
				break
			
			tSearch = message['time']
			advanceSeconds = message.get('advanceSeconds', 0.0)
			if advanceSeconds:
				tSearch += timedelta(seconds=advanceSeconds)
			times, frames = self.fcb.findBeforeAfter( tSearch, 1, 1 )
			if not frames:
				self.messageQ.put( ('error', 'No photos for {} at {}'.format(message.get('bib', None), message['time'].isoformat()) ) )
				
			lastImage = None
			for i, f in enumerate(frames):
				
				fname = GetPhotoFName( message['dirName'], message.get('bib',None), message.get('raceSeconds',None), i, times[i] )
				image = AddPhotoHeader(
					f,
					message.get('bib', None),
					message.get('time', None),
					message.get('raceSeconds', None),
					message.get('firstName',u''),
					message.get('lastName',u''),
					message.get('team',u''),
					message.get('raceName',u'')
				)
				
				#if lastImage is not None and image.GetData() == lastImage.GetData():
				#	self.messageQ.put( ('duplicate', '"{}"'.format(os.path.basename(fname))) )
				#	continue
				
				wx.CallAfter( self.beforeAfterImages[i].SetImage, image )
				
				SaveImage( fname, image, message.get('ftpInfo', None), self.messageQ, self.writerQ )
				lastImage = image
				
			if (now() - tNow).total_seconds() > self.frameDelay / 2.0:
				break
				
		self.fpt = now() - tNow
		self.tLast = tNow
	
	def shutdown( self ):
		# Ensure that all images in the queue are saved.
		self.grabFrameOK = False
		if hasattr(self, 'writerThread'):
			self.writerQ.put( ('terminate', ) )
			self.writerThread.join()
	
	def resetCamera( self, event ):
		dlg = ConfigDialog( self, self.getCameraDeviceNum(), self.getCameraResolution() )
		ret = dlg.ShowModal()
		cameraDeviceNum = dlg.GetCameraDeviceNum()
		cameraResolution = dlg.GetCameraResolution()
		dlg.Destroy()
		if ret != wx.ID_OK:
			return False
		
		self.setCameraDeviceNum( cameraDeviceNum )
		self.setCameraResolution( *cameraResolution )
		self.writeOptions()

		self.grabFrameOK = self.startCamera()
		return True
		
	def setCameraDeviceNum( self, num ):
		self.cameraDevice.SetLabel( unicode(num) )
		
	def setCameraResolution( self, width, height ):
		self.cameraResolution.SetLabel( u'{}x{}'.format(width, height) )
			
	def getCameraDeviceNum( self ):
		return int(self.cameraDevice.GetLabel())
		
	def getCameraResolution( self ):
		try:
			resolution = [int(v) for v in self.cameraResolution.GetLabel().split('x')]
			return resolution[0], resolution[1]
		except:
			return 640, 400
		
	def onCloseWindow( self, event ):
		self.shutdown()
		wx.Exit()
		
	def writeOptions( self ):
		self.config.Write( 'CameraDevice', self.cameraDevice.GetLabel() )
		self.config.Write( 'CameraResolution', self.cameraResolution.GetLabel() )
		self.config.Flush()
	
	def readOptions( self ):
		self.cameraDevice.SetLabel( self.config.Read('CameraDevice', u'0') )
		self.cameraResolution.SetLabel( self.config.Read('CameraResolution', u'640x480') )
Beispiel #5
0
class MainWin(wx.Frame):
    def __init__(self, parent, id=wx.ID_ANY, title='', size=(1000, 800)):
        wx.Frame.__init__(self, parent, id, title, size=size)

        self.fps = 25
        self.frameDelay = 1.0 / self.fps
        self.bufferSecs = 10

        self.tFrameCount = self.tLaunch = self.tLast = now()
        self.frameCount = 0
        self.frameCountUpdate = self.fps * 2
        self.fpsActual = 0.0
        self.fpt = timedelta(seconds=0)

        self.fcb = FrameCircBuf(self.bufferSecs * self.fps)

        self.config = wx.Config(
            appName="CrossMgrCamera",
            vendorName="SmartCyclingSolutions",
            #style=wx.CONFIG_USE_LOCAL_FILE
        )

        self.requestQ = Queue()  # Select photos from photobuf.
        self.writerQ = Queue(400)  # Selected photos waiting to be written out.
        self.ftpQ = Queue()  # Photos waiting to be ftp'd.
        self.renamerQ = Queue(
        )  # Photos waiting to be renamed and possibly ftp'd.
        self.messageQ = Queue(
        )  # Collection point for all status/failure messages.

        self.SetBackgroundColour(wx.Colour(232, 232, 232))

        mainSizer = wx.BoxSizer(wx.VERTICAL)

        self.primaryImage = ScaledImage(self,
                                        style=wx.BORDER_SUNKEN,
                                        size=(imageWidth, imageHeight))
        self.beforeImage = ScaledImage(self,
                                       style=wx.BORDER_SUNKEN,
                                       size=(imageWidth, imageHeight))
        self.afterImage = ScaledImage(self,
                                      style=wx.BORDER_SUNKEN,
                                      size=(imageWidth, imageHeight))
        self.beforeAfterImages = [self.beforeImage, self.afterImage]

        #------------------------------------------------------------------------------------------------
        headerSizer = wx.BoxSizer(wx.HORIZONTAL)

        self.logo = Utils.GetPngBitmap('CrossMgrHeader.png')
        headerSizer.Add(wx.StaticBitmap(self, wx.ID_ANY, self.logo))

        self.title = wx.StaticText(self,
                                   label='CrossMgr Camera\nVersion {}'.format(
                                       AppVerName.split()[1]),
                                   style=wx.ALIGN_RIGHT)
        self.title.SetFont(
            wx.Font((0, 28), wx.FONTFAMILY_SWISS, wx.FONTSTYLE_NORMAL,
                    wx.FONTWEIGHT_NORMAL))
        headerSizer.Add(self.title, flag=wx.ALL, border=10)

        #------------------------------------------------------------------------------
        self.cameraDeviceLabel = wx.StaticText(self, label='Camera Device:')
        self.cameraDevice = wx.StaticText(self)
        boldFont = self.cameraDevice.GetFont()
        boldFont.SetWeight(wx.BOLD)
        self.cameraDevice.SetFont(boldFont)
        self.cameraResolution = wx.StaticText(self)
        self.cameraResolution.SetFont(boldFont)
        bitmap = wx.Bitmap(clipboard_xpm)
        self.copyLogToClipboard = wx.BitmapButton(self, bitmap=bitmap)
        self.copyLogToClipboard.Bind(wx.EVT_BUTTON, self.onCopyLogToClipboard)
        self.reset = wx.Button(self, label="Reset Camera")
        self.reset.Bind(wx.EVT_BUTTON, self.resetCamera)
        cameraDeviceSizer = wx.BoxSizer(wx.HORIZONTAL)
        cameraDeviceSizer.Add(self.cameraDeviceLabel,
                              flag=wx.ALIGN_CENTRE_VERTICAL | wx.ALIGN_RIGHT)
        cameraDeviceSizer.Add(self.cameraDevice,
                              flag=wx.ALIGN_CENTRE_VERTICAL | wx.LEFT,
                              border=8)
        cameraDeviceSizer.Add(self.cameraResolution,
                              flag=wx.ALIGN_CENTRE_VERTICAL | wx.LEFT,
                              border=8)
        cameraDeviceSizer.Add(self.copyLogToClipboard,
                              flag=wx.ALIGN_CENTRE_VERTICAL | wx.LEFT,
                              border=8)
        cameraDeviceSizer.Add(self.reset,
                              flag=wx.ALIGN_CENTRE_VERTICAL | wx.LEFT,
                              border=24)

        #------------------------------------------------------------------------------
        self.targetProcessingTimeLabel = wx.StaticText(self,
                                                       label='Target Frames:')
        self.targetProcessingTime = wx.StaticText(self,
                                                  label=u'{:.3f}'.format(
                                                      self.fps))
        self.targetProcessingTime.SetFont(boldFont)
        self.targetProcessingTimeUnit = wx.StaticText(self, label='per sec')

        self.framesPerSecondLabel = wx.StaticText(self, label='Actual Frames:')
        self.framesPerSecond = wx.StaticText(self, label='25.000')
        self.framesPerSecond.SetFont(boldFont)
        self.framesPerSecondUnit = wx.StaticText(self, label='per sec')

        self.availableMsPerFrameLabel = wx.StaticText(
            self, label='Available Time Per Frame:')
        self.availableMsPerFrame = wx.StaticText(self,
                                                 label=u'{:.0f}'.format(
                                                     1000.0 * self.frameDelay))
        self.availableMsPerFrame.SetFont(boldFont)
        self.availableMsPerFrameUnit = wx.StaticText(self, label='ms')

        self.frameProcessingTimeLabel = wx.StaticText(
            self, label='Actual Frame Processing:')
        self.frameProcessingTime = wx.StaticText(self, label='20')
        self.frameProcessingTime.SetFont(boldFont)
        self.frameProcessingTimeUnit = wx.StaticText(self, label='ms')

        pfgs = wx.FlexGridSizer(rows=0, cols=6, vgap=4, hgap=8)
        fRight = wx.ALIGN_CENTRE_VERTICAL | wx.ALIGN_RIGHT
        fLeft = wx.ALIGN_CENTRE_VERTICAL

        #------------------- Row 1 ------------------------------
        pfgs.Add(self.targetProcessingTimeLabel, flag=fRight)
        pfgs.Add(self.targetProcessingTime, flag=fRight)
        pfgs.Add(self.targetProcessingTimeUnit, flag=fLeft)
        pfgs.Add(self.availableMsPerFrameLabel, flag=fRight)
        pfgs.Add(self.availableMsPerFrame, flag=fRight)
        pfgs.Add(self.availableMsPerFrameUnit, flag=fLeft)

        #------------------- Row 2 ------------------------------
        pfgs.Add(self.framesPerSecondLabel, flag=fRight)
        pfgs.Add(self.framesPerSecond, flag=fRight)
        pfgs.Add(self.framesPerSecondUnit, flag=fLeft)
        pfgs.Add(self.frameProcessingTimeLabel, flag=fRight)
        pfgs.Add(self.frameProcessingTime, flag=fRight)
        pfgs.Add(self.frameProcessingTimeUnit, flag=fLeft)

        statsSizer = wx.BoxSizer(wx.VERTICAL)
        statsSizer.Add(cameraDeviceSizer)
        statsSizer.Add(pfgs, flag=wx.TOP, border=8)
        headerSizer.Add(statsSizer, flag=wx.ALL, border=4)
        mainSizer.Add(headerSizer)

        self.messagesText = wx.TextCtrl(self,
                                        style=wx.TE_READONLY | wx.TE_MULTILINE
                                        | wx.HSCROLL,
                                        size=(350, imageHeight))
        self.messageManager = MessageManager(self.messagesText)

        border = 2
        row1Sizer = wx.BoxSizer(wx.HORIZONTAL)
        row1Sizer.Add(self.primaryImage, flag=wx.ALL, border=border)
        row1Sizer.Add(self.messagesText,
                      1,
                      flag=wx.TOP | wx.BOTTOM | wx.RIGHT | wx.EXPAND,
                      border=border)
        mainSizer.Add(row1Sizer, 1, flag=wx.EXPAND)

        row2Sizer = wx.BoxSizer(wx.HORIZONTAL)
        row2Sizer.Add(self.beforeImage,
                      flag=wx.LEFT | wx.RIGHT | wx.BOTTOM,
                      border=border)
        row2Sizer.Add(self.afterImage,
                      flag=wx.RIGHT | wx.BOTTOM,
                      border=border)
        mainSizer.Add(row2Sizer)

        #------------------------------------------------------------------------------------------------
        # Create a timer to update the frame loop.
        #
        self.timer = wx.Timer()
        self.timer.Bind(wx.EVT_TIMER, self.frameLoop)

        self.Bind(wx.EVT_CLOSE, self.onCloseWindow)

        self.readOptions()
        self.SetSizerAndFit(mainSizer)

        # Start the message reporting thread so we can see what is going on.
        self.messageThread = threading.Thread(target=self.showMessages)
        self.messageThread.daemon = True
        self.messageThread.start()

        self.grabFrameOK = False

        for i in [self.primaryImage, self.beforeImage, self.afterImage]:
            i.SetTestImage()

        # Start the frame loop.
        delayAdjustment = 0.80 if 'win' in sys.platform else 0.98
        ms = int(1000 * self.frameDelay * delayAdjustment)
        self.timer.Start(ms, False)

    def Start(self):
        self.messageQ.put(
            ('', '************************************************'))
        self.messageQ.put(('started', now().strftime('%Y/%m/%d %H:%M:%S')))
        self.startThreads()
        self.startCamera()

    def onCopyLogToClipboard(self, event):
        with open(redirectFileName, 'r') as fp:
            logData = fp.read()
        dataObj = wx.TextDataObject()
        dataObj.SetText(logData)
        if wx.TheClipboard.Open():
            wx.TheClipboard.SetData(dataObj)
            wx.TheClipboard.Close()
            Utils.MessageOK(
                self, u'\n\n'.join([
                    _("Log file copied to clipboard."),
                    _("You can now paste it into an email.")
                ]), _("Success"))
        else:
            Utils.MessageOK(self, _("Unable to open the clipboard."),
                            _("Error"), wx.ICON_ERROR)

    def showMessages(self):
        while 1:
            message = self.messageQ.get()
            assert len(message) == 2, 'Incorrect message length'
            cmd, info = message
            wx.CallAfter(self.messageManager.write,
                         '{}:  {}'.format(cmd, info) if cmd else info)

    def startCamera(self):
        self.camera = None
        try:
            self.camera = Device(max(self.getCameraDeviceNum(), 0))
        except Exception as e:
            self.messageQ.put(('camera', 'Error: {}'.format(e)))
            return False

        try:
            self.camera.set_resolution(*self.getCameraResolution())
            self.messageQ.put(
                ('camera',
                 '{}x{} Supported'.format(*self.getCameraResolution())))
        except Exception as e:
            self.messageQ.put(('camera', '{}x{} Unsupported Resolution'.format(
                *self.getCameraResolution())))

        self.messageQ.put(
            ('camera', 'Successfully Connected: Device: {}'.format(
                self.getCameraDeviceNum())))
        for i in self.beforeAfterImages:
            i.SetTestImage()
        return True

    def startThreads(self):
        self.grabFrameOK = False

        self.listenerThread = SocketListener(self.requestQ, self.messageQ)
        error = self.listenerThread.test()
        if error:
            wx.MessageBox(
                'Socket Error:\n\n{}\n\nIs another CrossMgrVideo or CrossMgrCamera running on this computer?'
                .format(error), "Socket Error", wx.OK | wx.ICON_ERROR)
            wx.Exit()

        self.writerThread = threading.Thread(target=PhotoWriter,
                                             args=(self.writerQ, self.messageQ,
                                                   self.ftpQ))
        self.writerThread.daemon = True

        self.renamerThread = threading.Thread(target=PhotoRenamer,
                                              args=(self.renamerQ,
                                                    self.writerQ,
                                                    self.messageQ))
        self.renamerThread.daemon = True

        self.ftpThread = threading.Thread(target=FTPWriter,
                                          args=(self.ftpQ, self.messageQ))
        self.ftpThread.daemon = True

        self.fcb = FrameCircBuf(int(self.bufferSecs * self.fps))

        self.listenerThread.start()
        self.writerThread.start()
        self.renamerThread.start()
        self.ftpThread.start()

        self.grabFrameOK = True
        self.messageQ.put(('threads', 'Successfully Launched'))
        return True

    def frameLoop(self, event=None):
        if not self.grabFrameOK:
            return

        self.frameCount += 1
        tNow = now()

        # Compute frame rate statistics.
        if self.frameCount == self.frameCountUpdate:
            self.fpsActual = self.frameCount / (
                tNow - self.tFrameCount).total_seconds()
            self.frameCount = 0
            self.tFrameCount = tNow
            self.framesPerSecond.SetLabel(u'{:.3f}'.format(self.fpsActual))
            self.availableMsPerFrame.SetLabel(u'{:.0f}'.format(1000.0 /
                                                               self.fpsActual))
            self.frameProcessingTime.SetLabel(u'{:.0f}'.format(
                1000.0 * self.fpt.total_seconds()))

        try:
            image = self.camera.getImage()
        except Exception as e:
            self.messageQ.put(('error', 'Webcam Failure: {}'.format(e)))
            self.grabFrameOK = False
            return

        if not image:
            return

        image = AddPhotoHeader(PilImageToWxImage(image),
                               time=tNow,
                               raceSeconds=(tNow -
                                            self.tLaunch).total_seconds(),
                               bib=999,
                               firstNameTxt=u'Firstname',
                               lastNameTxt=u'LASTNAME',
                               teamTxt=u'Team',
                               raceNameTxt='Racename')
        self.fcb.append(tNow, image)
        wx.CallAfter(self.primaryImage.SetImage, image)

        # Process any save messages
        while 1:
            try:
                message = self.requestQ.get(False)
            except Empty:
                break

            tSearch = message['time']
            advanceSeconds = message.get('advanceSeconds', 0.0)
            if advanceSeconds:
                tSearch += timedelta(seconds=advanceSeconds)
            times, frames = self.fcb.findBeforeAfter(tSearch, 1, 1)
            if not frames:
                self.messageQ.put(('error', 'No photos for {} at {}'.format(
                    message.get('bib', None), message['time'].isoformat())))

            lastImage = None
            for i, f in enumerate(frames):

                fname = GetPhotoFName(message['dirName'],
                                      message.get('bib', None),
                                      message.get('raceSeconds', None), i,
                                      times[i])
                image = AddPhotoHeader(f, message.get('bib', None),
                                       message.get('time', None),
                                       message.get('raceSeconds', None),
                                       message.get('firstName', u''),
                                       message.get('lastName', u''),
                                       message.get('team', u''),
                                       message.get('raceName', u''))

                #if lastImage is not None and image.GetData() == lastImage.GetData():
                #	self.messageQ.put( ('duplicate', '"{}"'.format(os.path.basename(fname))) )
                #	continue

                wx.CallAfter(self.beforeAfterImages[i].SetImage, image)

                SaveImage(fname, image, message.get('ftpInfo', None),
                          self.messageQ, self.writerQ)
                lastImage = image

            if (now() - tNow).total_seconds() > self.frameDelay / 2.0:
                break

        self.fpt = now() - tNow
        self.tLast = tNow

    def shutdown(self):
        # Ensure that all images in the queue are saved.
        self.grabFrameOK = False
        if hasattr(self, 'writerThread'):
            self.writerQ.put(('terminate', ))
            self.writerThread.join()

    def resetCamera(self, event):
        dlg = ConfigDialog(self, self.getCameraDeviceNum(),
                           self.getCameraResolution())
        ret = dlg.ShowModal()
        cameraDeviceNum = dlg.GetCameraDeviceNum()
        cameraResolution = dlg.GetCameraResolution()
        dlg.Destroy()
        if ret != wx.ID_OK:
            return False

        self.setCameraDeviceNum(cameraDeviceNum)
        self.setCameraResolution(*cameraResolution)
        self.writeOptions()

        self.grabFrameOK = self.startCamera()
        return True

    def setCameraDeviceNum(self, num):
        self.cameraDevice.SetLabel(unicode(num))

    def setCameraResolution(self, width, height):
        self.cameraResolution.SetLabel(u'{}x{}'.format(width, height))

    def getCameraDeviceNum(self):
        return int(self.cameraDevice.GetLabel())

    def getCameraResolution(self):
        try:
            resolution = [
                int(v) for v in self.cameraResolution.GetLabel().split('x')
            ]
            return resolution[0], resolution[1]
        except:
            return 640, 400

    def onCloseWindow(self, event):
        self.shutdown()
        wx.Exit()

    def writeOptions(self):
        self.config.Write('CameraDevice', self.cameraDevice.GetLabel())
        self.config.Write('CameraResolution', self.cameraResolution.GetLabel())
        self.config.Flush()

    def readOptions(self):
        self.cameraDevice.SetLabel(self.config.Read('CameraDevice', u'0'))
        self.cameraResolution.SetLabel(
            self.config.Read('CameraResolution', u'640x480'))
Beispiel #6
0
    def __init__(self, config, logger):
        super(MainWidget, self).__init__()

        self.config = config
        self.logger = logger

        self.imageDir = self.config.get("display",
                                        "image_dir",
                                        fallback="images")

        self.alarm = None
        self.route = ([], None, None)
        self.seenPager = False
        self.seenXml = False
        self.seenJson = False
        self.reportDone = False
        self.alarmDateTime = None
        self.forwarder = Forwarder(config, logger)
        self.notifier = Notifier(config, logger)
        self.sound = Sound(config, logger)
        self.gpioControl = GpioControl(config, logger)
        self.tts = TextToSpeech(config, logger)

        self.reportTimer = QTimer(self)
        self.reportTimer.setInterval( \
            self.config.getint("report", "timeout", fallback = 60) * 1000)
        self.reportTimer.setSingleShot(True)
        self.reportTimer.timeout.connect(self.generateReport)

        self.simTimer = QTimer(self)
        self.simTimer.setInterval(10000)
        self.simTimer.setSingleShot(True)
        self.simTimer.timeout.connect(self.simTimeout)
        #self.simTimer.start()

        self.idleTimer = QTimer(self)
        idleTimeout = self.config.getint("display",
                                         "idle_timeout",
                                         fallback=30)
        self.idleTimer.setInterval(idleTimeout * 60000)
        self.idleTimer.setSingleShot(True)
        self.idleTimer.timeout.connect(self.idleTimeout)

        self.screenTimer = QTimer(self)
        screenTimeout = self.config.getint("display",
                                           "screen_timeout",
                                           fallback=0)
        self.screenTimer.setInterval(screenTimeout * 60000)
        self.screenTimer.setSingleShot(True)
        self.screenTimer.timeout.connect(self.screenTimeout)
        if self.screenTimer.interval() > 0:
            self.screenTimer.start()

        # Presence -----------------------------------------------------------

        self.presenceTimer = QTimer(self)
        self.presenceTimer.setInterval(1000)
        self.presenceTimer.setSingleShot(False)
        self.presenceTimer.timeout.connect(self.checkPresence)
        self.presenceTimer.start()

        self.switchOnTimes = []
        self.switchOffTimes = []

        if self.config.has_section('presence'):
            onRe = re.compile('on[0-9]+')
            offRe = re.compile('off[0-9]+')

            for key, value in self.config.items('presence'):
                ma = onRe.fullmatch(key)
                if ma:
                    tup = self.parsePresence(key, value)
                    if tup:
                        self.switchOnTimes.append(tup)
                    continue
                ma = offRe.fullmatch(key)
                if ma:
                    tup = self.parsePresence(key, value)
                    if tup:
                        self.switchOffTimes.append(tup)
                    continue

        self.updateNextSwitchTimes()

        # Appearance ---------------------------------------------------------

        self.logger.info('Setting up X server...')

        subprocess.call(['xset', 's', 'off'])
        subprocess.call(['xset', 's', 'noblank'])
        subprocess.call(['xset', 's', '0', '0'])
        subprocess.call(['xset', '-dpms'])

        self.move(0, 0)
        self.resize(1920, 1080)

        self.setWindowTitle('Alarmdisplay')

        self.setStyleSheet("""
            font-size: 60px;
            background-color: rgb(0, 34, 44);
            color: rgb(2, 203, 255);
            font-family: "DejaVu Sans";
            """)

        # Sub-widgets --------------------------------------------------------

        layout = QVBoxLayout(self)
        layout.setSpacing(0)
        layout.setContentsMargins(0, 0, 0, 0)

        self.stackedWidget = QStackedWidget(self)
        layout.addWidget(self.stackedWidget)

        self.idleWidget = IdleWidget(self)
        self.idleWidget.start()
        self.stackedWidget.addWidget(self.idleWidget)

        self.alarmWidget = AlarmWidget(self)
        self.stackedWidget.addWidget(self.alarmWidget)

        self.errorWidget = QLabel(self)
        self.errorWidget.setGeometry(self.contentsRect())
        self.errorWidget.setAlignment(Qt.AlignCenter | Qt.AlignVCenter)
        self.errorWidget.setStyleSheet("""
            background-color: transparent;
            font-size: 20px;
            color: red;
            """)

        # Shortcuts ----------------------------------------------------------

        action = QAction(self)
        action.setShortcut(QKeySequence("1"))
        action.setShortcutContext(Qt.ApplicationShortcut)
        action.triggered.connect(self.exampleJugend)
        self.addAction(action)

        action = QAction(self)
        action.setShortcut(QKeySequence("2"))
        action.setShortcutContext(Qt.ApplicationShortcut)
        action.triggered.connect(self.exampleEngels)
        self.addAction(action)

        action = QAction(self)
        action.setShortcut(QKeySequence("3"))
        action.setShortcutContext(Qt.ApplicationShortcut)
        action.triggered.connect(self.exampleSack)
        self.addAction(action)

        action = QAction(self)
        action.setShortcut(QKeySequence("4"))
        action.setShortcutContext(Qt.ApplicationShortcut)
        action.triggered.connect(self.exampleWolfsgrabenPager)
        self.addAction(action)

        action = QAction(self)
        action.setShortcut(QKeySequence("5"))
        action.setShortcutContext(Qt.ApplicationShortcut)
        action.triggered.connect(self.exampleWolfsgrabenMail)
        self.addAction(action)

        action = QAction(self)
        action.setShortcut(QKeySequence("6"))
        action.setShortcutContext(Qt.ApplicationShortcut)
        action.triggered.connect(self.exampleWald)
        self.addAction(action)

        action = QAction(self)
        action.setShortcut(QKeySequence("7"))
        action.setShortcutContext(Qt.ApplicationShortcut)
        action.triggered.connect(self.exampleStadtwerkePager)
        self.addAction(action)

        action = QAction(self)
        action.setShortcut(QKeySequence("8"))
        action.setShortcutContext(Qt.ApplicationShortcut)
        action.triggered.connect(self.exampleLebenshilfe)
        self.addAction(action)

        action = QAction(self)
        action.setShortcut(QKeySequence("9"))
        action.setShortcutContext(Qt.ApplicationShortcut)
        action.triggered.connect(self.exampleHuissen)
        self.addAction(action)

        # Threads ------------------------------------------------------------

        self.receiverThread = QThread()
        self.alarmReceiver = AlarmReceiver(self.config, self.logger)
        self.alarmReceiver.receivedAlarm.connect(self.receivedPagerAlarm)
        self.alarmReceiver.finished.connect(self.receiverThread.quit)
        self.alarmReceiver.errorMessage.connect(self.receiverError)
        self.alarmReceiver.moveToThread(self.receiverThread)
        self.receiverThread.started.connect(self.alarmReceiver.receive)
        self.receiverThread.start()

        self.websocketReceiverThread = QThread()
        self.websocketReceiver = WebsocketReceiver(self.config, self.logger)
        self.websocketReceiver.receivedAlarm.connect( \
                self.receivedWebsocketAlarm)
        self.websocketReceiver.finished.connect( \
                self.websocketReceiverThread.quit)
        self.websocketReceiver.moveToThread(self.websocketReceiverThread)
        self.websocketReceiverThread.started.connect( \
                self.websocketReceiver.receive)
        self.websocketReceiverThread.start()

        if self.websocketReceiver.status:
            self.statusWidget = StatusWidget(self)
            layout.addWidget(self.statusWidget)
            self.websocketReceiver.receivedStatus.connect( \
                    self.statusWidget.setStatus)

        if self.config.has_section('email') and \
                self.config.get("email", "imap_host", fallback = ''):
            from ImapMonitor import ImapMonitor
            self.imapThread = QThread()
            self.imapMonitor = ImapMonitor(self.config, self.logger)
            self.imapMonitor.receivedAlarm.connect(self.receivedXmlAlarm)
            self.imapMonitor.moveToThread(self.imapThread)
            self.imapMonitor.finished.connect(self.imapThread.quit)
            self.imapThread.started.connect(self.imapMonitor.start)
            self.imapThread.start()

        self.socketListener = SocketListener(self.logger)
        self.socketListener.pagerAlarm.connect(self.receivedPagerAlarm)
        self.socketListener.xmlAlarm.connect(self.receivedXmlAlarm)

        self.cecThread = QThread()
        self.cecThread.start()
        self.cecCommand = CecCommand(self.logger)
        self.cecCommand.moveToThread(self.cecThread)

        self.report = AlarmReport(self.config, self.logger)

        try:
            self.notifier.startup()
        except:
            self.logger.error('Startup notification failed:', exc_info=True)

        self.logger.info('Setup finished.')
Beispiel #7
0
class MainWin( wx.Frame ):
	def __init__( self, parent, id = wx.ID_ANY, title='', size=(1000,800) ):
		wx.Frame.__init__(self, parent, id, title, size=size)
		
		self.db = Database()
		
		self.bufferSecs = 10
		self.setFPS( 30 )
		self.xFinish = None
		
		self.tFrameCount = self.tLaunch = self.tLast = now()
		self.frameCount = 0
		self.fpt = timedelta(seconds=0)
		self.iTriggerSelect = None
		self.triggerInfo = None
		self.tsMax = None
		
		self.captureTimer = wx.CallLater( 10, self.stopCapture )
		
		self.tdCaptureBefore = tdCaptureBeforeDefault
		self.tdCaptureAfter = tdCaptureAfterDefault

		self.config = wx.Config()
		
		self.requestQ = Queue()		# Select photos from photobuf.
		self.dbWriterQ = Queue()	# Photos waiting to be written
		self.messageQ = Queue()		# Collection point for all status/failure messages.
		
		self.SetBackgroundColour( wx.Colour(232,232,232) )
		
		self.focusDialog = FocusDialog( self )
		self.photoDialog = PhotoDialog( self )
		self.autoCaptureDialog = AutoCaptureDialog( self )
		self.triggerDialog = TriggerDialog( self )
				
		mainSizer = wx.BoxSizer( wx.VERTICAL )
		
		#------------------------------------------------------------------------------------------------
		headerSizer = wx.BoxSizer( wx.HORIZONTAL )
		
		self.logo = Utils.GetPngBitmap('CrossMgrHeader.png')
		headerSizer.Add( wx.StaticBitmap(self, wx.ID_ANY, self.logo) )
		
		self.title = wx.StaticText(self, label='CrossMgr Video\nVersion {}'.format(AppVerName.split()[1]), style=wx.ALIGN_RIGHT )
		self.title.SetFont( wx.Font( (0,28), wx.FONTFAMILY_SWISS, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL ) )
		headerSizer.Add( self.title, flag=wx.ALL, border=10 )
		
		clock = Clock( self, size=(90,90) )
		clock.SetBackgroundColour( self.GetBackgroundColour() )
		clock.Start()

		headerSizer.Add( clock, flag=wx.ALIGN_CENTER_VERTICAL|wx.LEFT|wx.RIGHT, border=4 )
		
		#------------------------------------------------------------------------------
		self.cameraDevice = wx.StaticText( self )
		self.cameraResolution = wx.StaticText( self )
		self.targetFPS = wx.StaticText( self, label='30 fps' )
		self.actualFPS = wx.StaticText( self, label='30.0 fps' )
		
		boldFont = self.cameraDevice.GetFont()
		boldFont.SetWeight( wx.BOLD )
		for w in (self.cameraDevice, self.cameraResolution, self.targetFPS, self.actualFPS):
			w.SetFont( boldFont )
		
		fgs = wx.FlexGridSizer( 2, 2, 2 )	# 2 Cols
		fgs.Add( wx.StaticText(self, label='Camera Device:'), flag=wx.ALIGN_RIGHT )
		fgs.Add( self.cameraDevice )
		
		fgs.Add( wx.StaticText(self, label='Resolution:'), flag=wx.ALIGN_RIGHT )
		fgs.Add( self.cameraResolution )
		
		fgs.Add( wx.StaticText(self, label='Target:'), flag=wx.ALIGN_RIGHT )
		fgs.Add( self.targetFPS, flag=wx.ALIGN_RIGHT )
		
		fgs.Add( wx.StaticText(self, label='Actual:'), flag=wx.ALIGN_RIGHT )
		fgs.Add( self.actualFPS, flag=wx.ALIGN_RIGHT )
		
		self.focus = wx.Button( self, label="Focus..." )
		self.focus.Bind( wx.EVT_BUTTON, self.onFocus )
		
		self.reset = wx.Button( self, label="Reset Camera" )
		self.reset.Bind( wx.EVT_BUTTON, self.resetCamera )
		
		self.manage = wx.Button( self, label="Manage Database" )
		self.manage.Bind( wx.EVT_BUTTON, self.manageDatabase )
		
		self.autoCaptureBtn = wx.Button( self, label="Config Auto Capture" )
		self.autoCaptureBtn.Bind( wx.EVT_BUTTON, self.autoCaptureConfig )
		
		self.help = wx.Button( self, wx.ID_HELP )
		self.help.Bind( wx.EVT_BUTTON, self.onHelp )
		
		self.snapshot, self.autoCapture, self.capture = CreateCaptureButtons( self )
		
		self.snapshot.Bind( wx.EVT_LEFT_DOWN, self.onStartSnapshot )
		self.focusDialog.snapshot.Bind( wx.EVT_LEFT_DOWN, self.onStartSnapshot )
		self.autoCapture.Bind( wx.EVT_LEFT_DOWN, self.onStartAutoCapture )
		self.focusDialog.autoCapture.Bind( wx.EVT_LEFT_DOWN, self.onStartAutoCapture )
		self.capture.Bind( wx.EVT_LEFT_DOWN, self.onStartCapture )
		self.capture.Bind( wx.EVT_LEFT_UP, self.onStopCapture )
		self.focusDialog.capture.Bind( wx.EVT_LEFT_DOWN, self.onStartCapture )
		self.focusDialog.capture.Bind( wx.EVT_LEFT_UP, self.onStopCapture )
		
		headerSizer.Add( fgs, flag=wx.ALIGN_CENTER_VERTICAL )
		
		fgs = wx.FlexGridSizer( rows=2, cols=0, hgap=8, vgap=4 )
		
		fgs.Add( self.focus, flag=wx.EXPAND )
		fgs.Add( self.reset, flag=wx.EXPAND )
		fgs.Add( self.manage, flag=wx.EXPAND )
		fgs.Add( self.autoCaptureBtn, flag=wx.EXPAND )
		fgs.Add( self.help, flag=wx.EXPAND )
		
		headerSizer.Add( fgs, flag=wx.ALIGN_CENTRE|wx.LEFT, border=4 )
		headerSizer.AddStretchSpacer()
		
		headerSizer.Add( self.snapshot, flag=wx.ALIGN_CENTRE_VERTICAL|wx.LEFT, border=8 )
		headerSizer.Add( self.autoCapture, flag=wx.ALIGN_CENTRE_VERTICAL|wx.LEFT, border=8 )
		headerSizer.Add( self.capture, flag=wx.ALIGN_CENTRE_VERTICAL|wx.LEFT|wx.RIGHT, border=8 )

		#------------------------------------------------------------------------------
		mainSizer.Add( headerSizer, flag=wx.EXPAND )
		
		#------------------------------------------------------------------------------------------------
		self.finishStrip = FinishStripPanel( self, size=(-1,wx.GetDisplaySize()[1]//2) )
		self.finishStrip.finish.Bind( wx.EVT_RIGHT_DOWN, self.onRightClick )
		
		self.primaryBitmap = ScaledBitmap( self, style=wx.BORDER_SUNKEN, size=(int(imageWidth*0.75), int(imageHeight*0.75)) )
		self.primaryBitmap.SetTestBitmap()
		self.primaryBitmap.Bind( wx.EVT_LEFT_UP, self.onFocus )
		self.primaryBitmap.Bind( wx.EVT_RIGHT_UP, self.onFocus )
		
		hsDate = wx.BoxSizer( wx.HORIZONTAL )
		hsDate.Add( wx.StaticText(self, label='Show Triggers for'), flag=wx.ALIGN_CENTER_VERTICAL )
		tQuery = now()
		self.date = wx.adv.DatePickerCtrl(
			self,
			dt=wx.DateTime.FromDMY( tQuery.day, tQuery.month-1, tQuery.year ),
			style=wx.adv.DP_DROPDOWN|wx.adv.DP_SHOWCENTURY
		)
		self.date.Bind( wx.adv.EVT_DATE_CHANGED, self.onQueryDateChanged )
		hsDate.Add( self.date, flag=wx.LEFT, border=2 )
		
		self.dateSelect = wx.Button( self, label='Select Date' )
		hsDate.Add( self.dateSelect, flag=wx.ALIGN_CENTER_VERTICAL|wx.LEFT, border=2 )
		self.dateSelect.Bind( wx.EVT_BUTTON, self.onDateSelect )
		
		hsDate.Add( wx.StaticText(self, label='Filter by Bib'), flag=wx.ALIGN_CENTER_VERTICAL|wx.LEFT, border=12 )
		self.bib = wx.lib.intctrl.IntCtrl( self, style=wx.TE_PROCESS_ENTER, size=(64,-1), min=1, allow_none=True, value=None )
		self.bib.Bind( wx.EVT_TEXT_ENTER, self.onQueryBibChanged )
		hsDate.Add( self.bib, flag=wx.LEFT, border=2 )
		
		self.tsQueryLower = datetime(tQuery.year, tQuery.month, tQuery.day)
		self.tsQueryUpper = self.tsQueryLower + timedelta(days=1)
		self.bibQuery = None
		
		self.triggerList = AutoWidthListCtrl( self, style=wx.LC_REPORT|wx.BORDER_SUNKEN|wx.LC_SORT_ASCENDING|wx.LC_HRULES )
		
		self.il = wx.ImageList(16, 16)
		self.sm_close = []
		for bm in getCloseFinishBitmaps():
			self.sm_close.append( self.il.Add(bm) )
		self.sm_up = self.il.Add( Utils.GetPngBitmap('SmallUpArrow.png'))
		self.sm_up = self.il.Add( Utils.GetPngBitmap('SmallUpArrow.png'))
		self.sm_dn = self.il.Add( Utils.GetPngBitmap('SmallDownArrow.png'))
		self.triggerList.SetImageList(self.il, wx.IMAGE_LIST_SMALL)
		
		self.fieldCol = {f:c for c, f in enumerate('ts bib name team wave race_name note kmh mph frames'.split())}
		headers = ['Time', 'Bib', 'Name', 'Team', 'Wave', 'Race', 'Note', 'km/h', 'mph', 'Frames']
		for i, h in enumerate(headers):
			self.triggerList.InsertColumn(
				i, h,
				wx.LIST_FORMAT_RIGHT if h in ('Bib','km/h','mph','Frames') else wx.LIST_FORMAT_LEFT
			)
		self.itemDataMap = {}
		
		self.triggerList.Bind( wx.EVT_LIST_ITEM_SELECTED, self.onTriggerSelected )
		self.triggerList.Bind( wx.EVT_LIST_ITEM_ACTIVATED, self.onTriggerEdit )
		self.triggerList.Bind( wx.EVT_LIST_ITEM_RIGHT_CLICK, self.onTriggerRightClick )
		#self.triggerList.Bind( wx.EVT_LIST_DELETE_ITEM, self.onTriggerDelete )
		
		vsTriggers = wx.BoxSizer( wx.VERTICAL )
		vsTriggers.Add( hsDate )
		vsTriggers.Add( self.triggerList, 1, flag=wx.EXPAND|wx.TOP, border=2)
		
		#------------------------------------------------------------------------------------------------
		mainSizer.Add( self.finishStrip, 1, flag=wx.EXPAND )
		
		border=2
		row1Sizer = wx.BoxSizer( wx.HORIZONTAL )
		row1Sizer.Add( self.primaryBitmap, flag=wx.ALL, border=border )
		row1Sizer.Add( vsTriggers, 1, flag=wx.TOP|wx.BOTTOM|wx.RIGHT|wx.EXPAND, border=border )
		mainSizer.Add( row1Sizer, flag=wx.EXPAND )
				
		self.Bind(wx.EVT_CLOSE, self.onCloseWindow)

		self.readOptions()
		self.updateFPS( int(float(self.targetFPS.GetLabel().split()[0])) )
		self.updateAutoCaptureLabel()
		self.SetSizerAndFit( mainSizer )
		
		# Start the message reporting thread so we can see what is going on.
		self.messageThread = threading.Thread( target=self.showMessages )
		self.messageThread.daemon = True
		self.messageThread.start()
		
		wx.CallLater( 300, self.refreshTriggers )
	
	def onHelp( self, event ):
		OpenHelp()
	
	def setFPS( self, fps ):
		self.fps = int(fps if fps > 0 else 30)
		self.frameDelay = 1.0 / self.fps
		self.frameCountUpdate = int(self.fps * 2)
	
	def updateFPS( self, fps ):
		self.setFPS( fps )
		self.targetFPS.SetLabel( u'{} fps'.format(self.fps) )

	def updateActualFPS( self, actualFPS ):
		self.actualFPS.SetLabel( '{:.1f} fps'.format(actualFPS) )

	def updateAutoCaptureLabel( self ):
		def f( n ):
			s = '{:0.1f}'.format( n )
			return s[:-2] if s.endswith('.0') else s
		
		label = u'\n'.join( [u'AUTO',u'CAPTURE',u'{} .. {}'.format(f(-self.tdCaptureBefore.total_seconds()), f(self.tdCaptureAfter.total_seconds()))] )
		for btn in (self.autoCapture, self.focusDialog.autoCapture):
			btn.SetLabel( label )
			btn.SetFontToFitLabel()
			wx.CallAfter( btn.Refresh )

	def setQueryDate( self, d ):
		self.tsQueryLower = d
		self.tsQueryUpper = self.tsQueryLower + timedelta( days=1 )
		self.refreshTriggers( True )
		
	def onDateSelect( self, event ):
		triggerDates = self.db.getTriggerDates()
		triggerDates.sort( reverse=True )
		with DateSelectDialog( self, triggerDates ) as dlg:
			if dlg.ShowModal() == wx.ID_OK and dlg.GetDate():
				self.setQueryDate( dlg.GetDate() )
			
	def onQueryDateChanged( self, event ):
		v = self.date.GetValue()
		self.setQueryDate( datetime( v.GetYear(), v.GetMonth() + 1, v.GetDay() ) )
	
	def onQueryBibChanged( self, event ):
		self.bibQuery = self.bib.GetValue()
		self.refreshTriggers( True )
	
	def GetListCtrl( self ):
		return self.triggerList
	
	def GetSortImages(self):
		return (self.sm_dn, self.sm_up)
	
	def getItemData( self, i ):
		data = self.triggerList.GetItemData( i )
		return self.itemDataMap[data]
	
	def getTriggerRowFromID( self, id ):
		for row in six.moves.range(self.triggerList.GetItemCount()-1, -1, -1):
			if self.itemDataMap[row][0] == id:
				return row
		return None

	def updateTriggerRow( self, row, fields ):
		if 'last_name' in fields and 'first_name' in fields:
			fields['name'] = u', '.join( n for n in (fields['last_name'], fields['first_name']) if n )
		for k, v in six.iteritems(fields):
			if k in self.fieldCol:
				if k == 'bib':
					v = u'{:>6}'.format(v)
				elif k == 'frames':
					v = six.text_type(v) if v else u''
				else:
					v = six.text_type(v)
				self.triggerList.SetItem( row, self.fieldCol[k], v )
				
	def updateTriggerRowID( self, id, fields ):
		row = self.getTriggerRowFromID( id )
		if row is not None:
			self.updateTriggerRow( row, fields )
	
	def getTriggerInfo( self, row ):
		data = self.itemDataMap[self.triggerList.GetItemData(row)]
		return {
			a:data[i] for i, a in enumerate((
				'id','ts','s_before','s_after','ts_start',
				'bib','name','team','wave','race_name',
				'first_name','last_name','note','kmh','frames'))
		}
	
	def refreshTriggers( self, replace=False, iTriggerRow=None ):
		tNow = now()
		self.lastTriggerRefresh = tNow
		
		# replace = True
		if replace:
			tsLower = self.tsQueryLower
			tsUpper = self.tsQueryUpper
			self.triggerList.DeleteAllItems()
			self.itemDataMap = {}
			self.tsMax = None
			self.iTriggerSelect = None
			self.triggerInfo = {}
			self.finishStrip.SetTsJpgs( None, None )
		else:
			tsLower = (self.tsMax or datetime(tNow.year, tNow.month, tNow.day)) + timedelta(seconds=0.00001)
			tsUpper = tsLower + timedelta(days=1)

		triggers = self.db.getTriggers( tsLower, tsUpper, self.bibQuery )
			
		tsPrev = (self.tsMax or datetime(2000,1,1))
		if triggers:
			self.tsMax = triggers[-1][1] # id,ts,s_before,s_after,ts_start,bib,first_name,last_name,team,wave,race_name,note,kmh,frames
		
		zeroFrames, tsLower, tsUpper = [], datetime.max, datetime.min
		for i, (id,ts,s_before,s_after,ts_start,bib,first_name,last_name,team,wave,race_name,note,kmh,frames) in enumerate(triggers):
			if s_before == 0.0 and s_after == 0.0:
				s_before,s_after = tdCaptureBeforeDefault.total_seconds(),tdCaptureAfterDefault.total_seconds()
			
			dtFinish = (ts-tsPrev).total_seconds()
			itemImage = self.sm_close[min(len(self.sm_close)-1, int(len(self.sm_close) * dtFinish / closeFinishThreshold))]		
			row = self.triggerList.InsertItem( 999999, ts.strftime('%H:%M:%S.%f')[:-3], itemImage )
			
			if not frames:
				tsLower = min( tsLower, ts-timedelta(seconds=s_before) )
				tsU = ts + timedelta(seconds=s_after)
				tsUpper = max( tsUpper,tsU )
				zeroFrames.append( (row, id, tsU) )
			
			kmh_text, mph_text = (u'{:.2f}'.format(kmh), u'{:.2f}'.format(kmh * 0.621371)) if kmh else (u'', u'')
			fields = {
				'bib':			bib,
				'last_name':	last_name,
				'first_name':	first_name,
				'team':			team,
				'wave':			wave,
				'race_name':	race_name,
				'note':			note,
				'kmh':			kmh_text,
				'mph':			mph_text,
				'frames':		frames,
			}
			self.updateTriggerRow( row, fields )
			
			self.triggerList.SetItemData( row, row )
			self.itemDataMap[row] = (id,ts,s_before,s_after,ts_start,bib,fields['name'],team,wave,race_name,first_name,last_name,note,kmh,frames)
			tsPrev = ts
		
		if zeroFrames:
			counts = self.db.getTriggerPhotoCounts( tsLower, tsUpper )
			values = {'frames':0}
			for row, id, tsU in zeroFrames:
				values['frames'] = counts[id]
				self.updateTriggerRow( row, values )
				# Don't update the trigger if the number of frames is possibly not known yet.
				if (tNow - tsU).total_seconds() < 5.0*60.0:
					del counts[id]
			self.db.updateTriggerPhotoCounts( counts )
			
		for i in six.moves.range(self.triggerList.GetColumnCount()):
			self.triggerList.SetColumnWidth(i, wx.LIST_AUTOSIZE)

		if iTriggerRow is not None:
			iTriggerRow = min( max(0, iTriggerRow), self.triggerList.GetItemCount()-1 )
			self.triggerList.EnsureVisible( iTriggerRow )
			self.triggerList.Select( iTriggerRow )
		else:
			if self.triggerList.GetItemCount() >= 1:
				self.triggerList.EnsureVisible( self.triggerList.GetItemCount()-1 )

	def Start( self ):
		self.messageQ.put( ('', '************************************************') )
		self.messageQ.put( ('started', now().strftime('%Y/%m/%d %H:%M:%S')) )
		self.startThreads()

	def updateSnapshot( self, t, f ):
		self.snapshotCount = getattr(self, 'snapshotCount', 0) + 1
		self.dbWriterQ.put( ('photo', t, f) )
		self.dbWriterQ.put( (
			'trigger',
			t,
			0.00001,		# s_before
			0.00001,		# s_after
			t,
			self.snapshotCount,	# bib
			u'', 			# first_name
			u'Snapshot',	# last_name
			u'',			# team
			u'',			# save
			u'',			# race_name
		) )
		self.doUpdateAutoCapture( t, self.snapshotCount, [self.snapshot, self.focusDialog.snapshot], snapshotEnableColour )
		
	def onStartSnapshot( self, event ):
		event.GetEventObject().SetForegroundColour( snapshotDisableColour )
		wx.CallAfter( event.GetEventObject().Refresh )
		self.camInQ.put( {'cmd':'snapshot'} )
		
	def doUpdateAutoCapture( self, tStartCapture, count, btn, colour ):
		self.dbWriterQ.put( ('flush',) )
		self.dbWriterQ.join()
		triggers = self.db.getTriggers( tStartCapture, tStartCapture, count )
		if triggers:
			id = triggers[0][0]
			self.db.initCaptureTriggerData( id )
			self.refreshTriggers( iTriggerRow=999999, replace=True )
			self.showLastTrigger()
			self.onTriggerSelected( iTriggerSelect=self.triggerList.GetItemCount()-1 )
		for b in (btn if isinstance(btn, list) else [btn]):
			b.SetForegroundColour( colour )
			wx.CallAfter( b.Refresh )

	def onStartAutoCapture( self, event ):
		tNow = now()
		
		event.GetEventObject().SetForegroundColour( autoCaptureDisableColour )
		wx.CallAfter( event.GetEventObject().Refresh )
		
		self.autoCaptureCount = getattr(self, 'autoCaptureCount', 0) + 1
		s_before, s_after = self.tdCaptureBefore.total_seconds(), self.tdCaptureAfter.total_seconds()
		self.requestQ.put( {
				'time':tNow,
				's_before':s_before,
				's_after':s_after,
				'ts_start':tNow,
				'bib':self.autoCaptureCount,
				'last_name':u'Auto',
			}
		)
		
		wx.CallLater( int(CamServer.EstimateQuerySeconds(tNow, s_before, s_after, self.fps)*1000.0) + 80,
			self.doUpdateAutoCapture, tNow, self.autoCaptureCount, self.autoCapture, autoCaptureEnableColour
		)
		
	def onStartCapture( self, event ):
		tNow = self.tStartCapture = now()
		
		event.GetEventObject().SetForegroundColour( captureDisableColour )
		wx.CallAfter( event.GetEventObject().Refresh )
		wx.BeginBusyCursor()
		
		self.captureCount = getattr(self, 'captureCount', 0) + 1
		self.requestQ.put( {
				'time':tNow,
				's_before':0.0,
				's_after':self.tdCaptureAfter.total_seconds(),
				'ts_start':tNow,
				'bib':self.captureCount,
				'last_name':u'Capture',
			}
		)
		self.camInQ.put( {'cmd':'start_capture', 'tStart':tNow-self.tdCaptureBefore} )
	
	def showLastTrigger( self ):
		iTriggerRow = self.triggerList.GetItemCount() - 1
		if iTriggerRow < 0:
			return
		self.triggerList.EnsureVisible( iTriggerRow )
		for r in six.moves.range(self.triggerList.GetItemCount()-1):
			self.triggerList.Select(r, 0)
		self.triggerList.Select( iTriggerRow )		
	
	def onStopCapture( self, event ):
		self.camInQ.put( {'cmd':'stop_capture'} )
		triggers = self.db.getTriggers( self.tStartCapture, self.tStartCapture, self.captureCount )
		if triggers:
			id = triggers[0][0]
			self.db.updateTriggerBeforeAfter(
				id,
				0.0,
				(now() - self.tStartCapture).total_seconds()
			)
			self.db.initCaptureTriggerData( id )
			self.refreshTriggers( iTriggerRow=999999, replace=True )
		
		self.showLastTrigger()
		
		wx.EndBusyCursor()
		event.GetEventObject().SetForegroundColour( captureEnableColour )
		wx.CallAfter( event.GetEventObject().Refresh )
		
		def updateFS():
			# Wait for all the photos to be written.
			self.dbWriterQ.put( ('flush',) )
			self.dbWriterQ.join()
			# Update the finish strip.
			wx.CallAfter( self.onTriggerSelected, iTriggerSelect=self.triggerList.GetItemCount() - 1 )

		threading.Thread( target=updateFS ).start()

	def autoCaptureConfig( self, event ):
		self.autoCaptureDialog.set( self.tdCaptureBefore.total_seconds(), self.tdCaptureAfter.total_seconds() )
		if self.autoCaptureDialog.ShowModal() == wx.ID_OK:
			s_before, s_after = self.autoCaptureDialog.get()
			self.tdCaptureBefore = timedelta(seconds=s_before) if s_before is not None else tdCaptureBeforeDefault
			self.tdCaptureAfter  = timedelta(seconds=s_after)  if s_after  is not None else tdCaptureAfterDefault
			self.writeOptions()
			self.updateAutoCaptureLabel()
 		
	def onFocus( self, event ):
		if self.focusDialog.IsShown():
			return
		self.focusDialog.Move((4,4))
		self.camInQ.put( {'cmd':'send_update', 'name':'focus', 'freq':1} )
		self.focusDialog.Show()
	
	def onRightClick( self, event ):
		if not self.triggerInfo:
			return
		
		self.xFinish = event.GetX()
		self.photoDialog.set( self.finishStrip.finish.getIJpg(self.xFinish), self.triggerInfo, self.finishStrip.GetTsJpgs(), self.fps,
			self.doTriggerEdit
		)
		self.photoDialog.CenterOnParent()
		self.photoDialog.Move( self.photoDialog.GetScreenPosition().x, 0 )
		self.photoDialog.ShowModal()
		if self.triggerInfo['kmh'] != (self.photoDialog.kmh or 0.0):
			self.db.updateTriggerKMH( self.triggerInfo['id'], self.photoDialog.kmh or 0.0 )
			self.refreshTriggers( replace=True, iTriggerRow=self.iTriggerSelect )
		self.photoDialog.clear()

	def onTriggerSelected( self, event=None, iTriggerSelect=None ):
		self.iTriggerSelect = event.Index if iTriggerSelect is None else iTriggerSelect
		if self.iTriggerSelect >= self.triggerList.GetItemCount():
			self.ts = None
			self.tsJpg = []
			self.finishStrip.SetTsJpgs( self.tsJpg, self.ts, {} )
			return
		
		data = self.itemDataMap[self.triggerList.GetItemData(self.iTriggerSelect)]
		self.triggerInfo = self.getTriggerInfo( self.iTriggerSelect )
		self.ts = self.triggerInfo['ts']
		s_before, s_after = abs(self.triggerInfo['s_before']), abs(self.triggerInfo['s_after'])
		if s_before == 0.0 and s_after == 0.0:
			s_before, s_after = tdCaptureBeforeDefault.total_seconds(), tdCaptureAfterDefault.total_seconds()
		
		# Update the screen in the background so we don't freeze the UI.
		def updateFS( triggerInfo ):
			self.ts = triggerInfo['ts']
			self.tsJpg = self.db.clone().getPhotos( self.ts - timedelta(seconds=s_before), self.ts + timedelta(seconds=s_after) )
			triggerInfo['frames'] = len(self.tsJpg)
			wx.CallAfter( self.finishStrip.SetTsJpgs, self.tsJpg, self.ts, triggerInfo )
			
		threading.Thread( target=updateFS, args=(self.triggerInfo,) ).start()
	
	def onTriggerRightClick( self, event ):
		self.iTriggerSelect = event.Index
		if not hasattr(self, "triggerDeleteID"):
			self.triggerDeleteID = wx.NewId()
			self.triggerEditID = wx.NewId()
			self.Bind(wx.EVT_MENU, lambda event: self.doTriggerDelete(), id=self.triggerDeleteID)
			self.Bind(wx.EVT_MENU, lambda event: self.doTriggerEdit(),   id=self.triggerEditID)

		menu = wx.Menu()
		menu.Append(self.triggerEditID,   "Edit...")
		menu.Append(self.triggerDeleteID, "Delete...")

		self.PopupMenu(menu)
		menu.Destroy()
		
	def doTriggerDelete( self, confirm=True ):
		triggerInfo = self.getTriggerInfo( self.iTriggerSelect )
		message = u', '.join( f for f in (triggerInfo['ts'].strftime('%H:%M:%S.%f')[:-3], six.text_type(triggerInfo['bib']),
			triggerInfo['name'], triggerInfo['team'], triggerInfo['wave'], triggerInfo['race_name']) if f )
		if not confirm or wx.MessageDialog( self, u'{}:\n\n{}'.format(u'Confirm Delete', message), u'Confirm Delete',
				style=wx.OK|wx.CANCEL|wx.ICON_QUESTION ).ShowModal() == wx.ID_OK:		
			self.db.deleteTrigger( triggerInfo['id'], self.tdCaptureBefore.total_seconds(), self.tdCaptureAfter.total_seconds() )
			self.refreshTriggers( replace=True, iTriggerRow=self.iTriggerSelect )
	
	def onTriggerDelete( self, event ):
		self.iTriggerSelect = event.Index
		self.doTriggerDelete()
		
	def doTriggerEdit( self ):
		data = self.itemDataMap[self.triggerList.GetItemData(self.iTriggerSelect)]
		self.triggerDialog.set( self.db, data[0] )
		self.triggerDialog.CenterOnParent()
		if self.triggerDialog.ShowModal() == wx.ID_OK:
			row = self.iTriggerSelect
			fields = {f:v for f, v in zip(Database.triggerEditFields,self.triggerDialog.get())}
			self.updateTriggerRow( row, fields )
			self.triggerInfo.update( fields )
		return self.triggerInfo
	
	def onTriggerEdit( self, event ):
		self.iTriggerSelect = event.Index
		self.doTriggerEdit()
	
	def showMessages( self ):
		while 1:
			message = self.messageQ.get()
			assert len(message) == 2, 'Incorrect message length'
			cmd, info = message
			six.print_( 'Message:', '{}:  {}'.format(cmd, info) if cmd else info )
			#wx.CallAfter( self.messageManager.write, '{}:  {}'.format(cmd, info) if cmd else info )
	
	def delayRefreshTriggers( self ):
		if not hasattr(self, 'refreshTimer') or not self.refreshTimer.IsRunning():
			self.resetTimer = wx.CallLater( 1000, self.refreshTriggers )

	def startThreads( self ):
		self.grabFrameOK = False
		
		self.listenerThread = SocketListener( self.requestQ, self.messageQ )
		error = self.listenerThread.test()
		if error:
			wx.MessageBox('Socket Error:\n\n{}\n\nIs another CrossMgrVideo or CrossMgrCamera running on this computer?'.format(error),
				"Socket Error",
				wx.OK | wx.ICON_ERROR
			)
			wx.Exit()
		
		self.camInQ, self.camReader = CamServer.getCamServer( self.getCameraInfo() )
		self.cameraThread = threading.Thread( target=self.processCamera )
		self.cameraThread.daemon = True
		
		self.eventThread = threading.Thread( target=self.processRequests )
		self.eventThread.daemon = True
		
		self.dbWriterThread = threading.Thread( target=DBWriter, args=(self.dbWriterQ, lambda: wx.CallAfter(self.delayRefreshTriggers), self.db.fname) )
		self.dbWriterThread.daemon = True
		
		self.cameraThread.start()
		self.eventThread.start()
		self.dbWriterThread.start()
		self.listenerThread.start()
		
		self.grabFrameOK = True
		self.messageQ.put( ('threads', 'Successfully Launched') )
		
		self.primaryFreq = 5
		self.camInQ.put( {'cmd':'send_update', 'name':'primary', 'freq':self.primaryFreq} )
		return True
	
	def stopCapture( self ):
		self.dbWriterQ.put( ('flush',) )
	
	def processCamera( self ):
		lastFrame = None
		lastPrimaryTime = now()
		primaryCount = 0
		while 1:
			try:
				msg = self.camReader.recv()
			except EOFError:
				break
			
			cmd = msg['cmd']
			if cmd == 'response':
				for t, f in msg['ts_frames']:
					self.dbWriterQ.put( ('photo', t, f) )
					lastFrame = f
			elif cmd == 'update':
				name, lastFrame = msg['name'], lastFrame if msg['frame'] is None else msg['frame']
				if lastFrame is not None:
					if name == 'primary':
						wx.CallAfter( self.primaryBitmap.SetBitmap, CVUtil.frameToBitmap(lastFrame) )
						
						primaryCount += self.primaryFreq
						primaryTime = now()
						primaryDelta = (primaryTime - lastPrimaryTime).total_seconds()
						if primaryDelta > 2.5:
							wx.CallAfter( self.updateActualFPS, primaryCount / primaryDelta )
							lastPrimaryTime = primaryTime
							primaryCount = 0
					elif name == 'focus':
						if self.focusDialog.IsShown():
							wx.CallAfter( self.focusDialog.SetBitmap, CVUtil.frameToBitmap(lastFrame) )
						else:
							self.camInQ.put( {'cmd':'cancel_update', 'name':'focus'} )
			elif cmd == 'snapshot':
				lastFrame = lastFrame if msg['frame'] is None else msg['frame']
				wx.CallAfter( self.updateSnapshot,  msg['ts'], lastFrame )
			elif cmd == 'terminate':
				break
		
	def processRequests( self ):
		def refresh():
			self.dbWriterQ.put( ('flush',) )
	
		while 1:
			msg = self.requestQ.get()
			
			tSearch = msg['time']
			advanceSeconds = msg.get('advanceSeconds', 0.0)
			tSearch += timedelta(seconds=advanceSeconds)
			
			# Record this trigger.
			self.dbWriterQ.put( (
				'trigger',
				tSearch - timedelta(seconds=advanceSeconds),
				msg.get('s_before', self.tdCaptureBefore.total_seconds()),	# Use the configured capture interval, not the default.
				msg.get('s_after', self.tdCaptureAfter.total_seconds()),
				msg.get('ts_start', None) or now(),
				msg.get('bib', 99999),
				msg.get('first_name',u'') or msg.get('firstName',u''),
				msg.get('last_name',u'') or msg.get('lastName',u''),
				msg.get('team',u''),
				msg.get('wave',u''),
				msg.get('race_name',u'') or msg.get('raceName',u''),
			) )
			# Record the video frames for the trigger.
			tStart, tEnd = tSearch-self.tdCaptureBefore, tSearch+self.tdCaptureAfter
			self.camInQ.put( { 'cmd':'query', 'tStart':tStart, 'tEnd':tEnd,} )
			wx.CallAfter( wx.CallLater, max(100, int(100+1000*(tEnd-now()).total_seconds())), refresh )
	
	def shutdown( self ):
		# Ensure that all images in the queue are saved.
		if hasattr(self, 'dbWriterThread'):
			self.camInQ.put( {'cmd':'terminate'} )
			self.dbWriterQ.put( ('terminate', ) )
			self.dbWriterThread.join( 2.0 )
			
	def setDBName( self, dbName ):
		if dbName != self.db.fname:
			if hasattr(self, 'dbWriterThread'):
				self.dbWriterQ.put( ('terminate', ) )
				self.dbWriterThread.join()
			try:
				self.db = Database( dbName )
			except:
				self.db = Database()
			
			self.dbWriterQ = Queue()
			self.dbWriterThread = threading.Thread( target=DBWriter, args=(self.dbWriterQ, lambda: wx.CallAfter(self.delayRefreshTriggers), self.db.fname) )
			self.dbWriterThread.daemon = True
			self.dbWriterThread.start()
	
	def resetCamera( self, event=None ):
		dlg = ConfigDialog( self, self.getCameraDeviceNum(), self.fps, self.getCameraResolution() )
		ret = dlg.ShowModal()
		cameraDeviceNum = dlg.GetCameraDeviceNum()
		cameraResolution = dlg.GetCameraResolution()
		fps = dlg.GetFPS()
		dlg.Destroy()
		if ret != wx.ID_OK:
			return False
		
		self.setCameraDeviceNum( cameraDeviceNum )
		self.setCameraResolution( *cameraResolution )
		self.updateFPS( fps )
		self.writeOptions()
		
		if hasattr(self, 'camInQ'):
			self.camInQ.put( {'cmd':'cam_info', 'info':self.getCameraInfo(),} )
			
		self.GetSizer().Layout()
		return True
	
	def manageDatabase( self, event ):
		trigFirst, trigLast = self.db.getTimestampRange()
		dlg = ManageDatabase( self, self.db.getsize(), self.db.fname, trigFirst, trigLast, title='Manage Database' )
		if dlg.ShowModal() == wx.ID_OK:
			work = wx.BusyCursor()
			tsLower, tsUpper, vacuum, dbName = dlg.GetValues()
			self.setDBName( dbName )
			if tsUpper:
				tsUpper = datetime.combine( tsUpper, time(23,59,59,999999) )
			self.db.cleanBetween( tsLower, tsUpper )
			if vacuum:
				self.db.vacuum()
			wx.CallAfter( self.finishStrip.Clear )
			wx.CallAfter( self.refreshTriggers, True )
		dlg.Destroy()
	
	def setCameraDeviceNum( self, num ):
		self.cameraDevice.SetLabel( six.text_type(num) )
		
	def setCameraResolution( self, width, height ):
		self.cameraResolution.SetLabel( u'{}x{}'.format(width, height) )
			
	def getCameraDeviceNum( self ):
		return int(self.cameraDevice.GetLabel())
		
	def getCameraFPS( self ):
		return int(float(self.targetFPS.GetLabel().split()[0]))
		
	def getCameraResolution( self ):
		try:
			resolution = [int(v) for v in self.cameraResolution.GetLabel().split('x')]
			return resolution[0], resolution[1]
		except:
			return 640, 480
		
	def onCloseWindow( self, event ):
		self.shutdown()
		wx.Exit()
		
	def writeOptions( self ):
		self.config.Write( 'DBName', self.db.fname )
		self.config.Write( 'CameraDevice', self.cameraDevice.GetLabel() )
		self.config.Write( 'CameraResolution', self.cameraResolution.GetLabel() )
		self.config.Write( 'FPS', self.targetFPS.GetLabel() )
		self.config.Write( 'SecondsBefore', '{:.3f}'.format(self.tdCaptureBefore.total_seconds()) )
		self.config.Write( 'SecondsAfter', '{:.3f}'.format(self.tdCaptureAfter.total_seconds()) )
		self.config.Flush()
	
	def readOptions( self ):
		self.setDBName( self.config.Read('DBName', '') )
		self.cameraDevice.SetLabel( self.config.Read('CameraDevice', u'0') )
		self.cameraResolution.SetLabel( self.config.Read('CameraResolution', u'640x480') )
		self.targetFPS.SetLabel( self.config.Read('FPS', u'30.000') )
		s_before = self.config.Read('SecondsBefore', u'0.5')
		s_after = self.config.Read('SecondsAfter', u'2.0')
		try:
			self.tdCaptureBefore = timedelta(seconds=abs(float(s_before)))
		except:
			pass
		try:
			self.tdCaptureAfter = timedelta(seconds=abs(float(s_after)))
		except:
			pass
		
	def getCameraInfo( self ):
		width, height = self.getCameraResolution()
		return {'usb':self.getCameraDeviceNum(), 'fps':self.getCameraFPS(), 'width':width, 'height':height}
Beispiel #8
0
class MainWin( wx.Frame ):
	def __init__( self, parent, id = wx.ID_ANY, title='', size=(1000,800) ):
		wx.Frame.__init__(self, parent, id, title, size=size)
		
		self.db = Database()
		
		self.fps = 25
		self.frameDelay = 1.0 / self.fps
		self.bufferSecs = 10
		self.xFinish = None
		
		self.tFrameCount = self.tLaunch = self.tLast = now()
		self.frameCount = 0
		self.frameCountUpdate = int(self.fps * 2)
		self.fpsActual = 0.0
		self.fpt = timedelta(seconds=0)
		self.iTriggerSelect = None
		self.triggerInfo = None
		
		self.captureTimer = wx.CallLater( 10, self.stopCapture )
		
		self.fcb = FrameCircBuf( self.bufferSecs * self.fps )
		
		self.config = wx.Config(appName="CrossMgrVideo",
						vendorName="SmartCyclingSolutions",
						style=wx.CONFIG_USE_LOCAL_FILE)
		
		self.requestQ = Queue()		# Select photos from photobuf.
		self.dbWriterQ = Queue()	# Photos waiting to be renamed and possibly ftp'd.
		self.dbReaderQ = Queue()	# Photos read as requested from user.
		self.messageQ = Queue()		# Collection point for all status/failure messages.
		
		self.SetBackgroundColour( wx.Colour(232,232,232) )
		
		mainSizer = wx.BoxSizer( wx.VERTICAL )
		
		#------------------------------------------------------------------------------------------------
		headerSizer = wx.BoxSizer( wx.HORIZONTAL )
		
		self.logo = Utils.GetPngBitmap('CrossMgrHeader.png')
		headerSizer.Add( wx.StaticBitmap(self, bitmap=self.logo) )
		
		self.title = wx.StaticText(self, label='CrossMgr Video\nVersion {}'.format(AppVerName.split()[1]), style=wx.ALIGN_RIGHT )
		self.title.SetFont( wx.FontFromPixelSize( wx.Size(0,28), wx.FONTFAMILY_SWISS, wx.NORMAL, wx.FONTWEIGHT_NORMAL ) )
		headerSizer.Add( self.title, flag=wx.ALL, border=10 )
		
		#------------------------------------------------------------------------------
		self.cameraDeviceLabel = wx.StaticText(self, label='Camera Device:')
		self.cameraDevice = wx.StaticText( self )
		boldFont = self.cameraDevice.GetFont()
		boldFont.SetWeight( wx.BOLD )
		self.cameraDevice.SetFont( boldFont )
		self.reset = wx.Button( self, label="Reset Camera" )
		self.reset.Bind( wx.EVT_BUTTON, self.resetCamera )
		
		self.manage = wx.Button( self, label="Manage Database" )
		self.manage.Bind( wx.EVT_BUTTON, self.manageDatabase )
		
		self.test = wx.Button( self, label="Test" )
		self.test.Bind( wx.EVT_BUTTON, self.onTest )
		
		cameraDeviceSizer = wx.BoxSizer( wx.HORIZONTAL )
		cameraDeviceSizer.Add( self.cameraDeviceLabel, flag=wx.ALIGN_CENTRE_VERTICAL|wx.ALIGN_RIGHT )
		cameraDeviceSizer.Add( self.cameraDevice, flag=wx.ALIGN_CENTRE_VERTICAL|wx.LEFT, border=8 )
		cameraDeviceSizer.Add( self.reset, flag=wx.ALIGN_CENTRE_VERTICAL|wx.LEFT, border=8 )
		cameraDeviceSizer.Add( self.manage, flag=wx.ALIGN_CENTRE_VERTICAL|wx.LEFT, border=16 )
		cameraDeviceSizer.Add( self.test, flag=wx.ALIGN_CENTRE_VERTICAL|wx.LEFT, border=16 )

		#------------------------------------------------------------------------------
		self.targetProcessingTimeLabel = wx.StaticText(self, label='Target Frames:')
		self.targetProcessingTime = wx.StaticText(self, label=u'{:.3f}'.format(self.fps))
		self.targetProcessingTime.SetFont( boldFont )
		self.targetProcessingTimeUnit = wx.StaticText(self, label='/ sec')
		
		self.framesPerSecondLabel = wx.StaticText(self, label='Actual Frames:')
		self.framesPerSecond = wx.StaticText(self, label='25.000')
		self.framesPerSecond.SetFont( boldFont )
		self.framesPerSecondUnit = wx.StaticText(self, label='/ sec')
		
		self.availableMsPerFrameLabel = wx.StaticText(self, label='Available Time Per Frame:')
		self.availableMsPerFrame = wx.StaticText(self, label=u'{:.0f}'.format(1000.0*self.frameDelay))
		self.availableMsPerFrame.SetFont( boldFont )
		self.availableMsPerFrameUnit = wx.StaticText(self, label='ms')
		
		self.frameProcessingTimeLabel = wx.StaticText(self, label='Frame Processing Time:')
		self.frameProcessingTime = wx.StaticText(self, label='20')
		self.frameProcessingTime.SetFont( boldFont )
		self.frameProcessingTimeUnit = wx.StaticText(self, label='ms')
		
		pfgs = wx.FlexGridSizer( rows=0, cols=6, vgap=4, hgap=8 )
		fRight = wx.ALIGN_CENTRE_VERTICAL|wx.ALIGN_RIGHT
		fLeft = wx.ALIGN_CENTRE_VERTICAL
		
		#------------------- Row 1 ------------------------------
		pfgs.Add( self.targetProcessingTimeLabel, flag=fRight )
		pfgs.Add( self.targetProcessingTime, flag=fRight )
		pfgs.Add( self.targetProcessingTimeUnit, flag=fLeft )
		pfgs.Add( self.availableMsPerFrameLabel, flag=fRight )
		pfgs.Add( self.availableMsPerFrame, flag=fRight )
		pfgs.Add( self.availableMsPerFrameUnit, flag=fLeft )
		
		#------------------- Row 2 ------------------------------
		pfgs.Add( self.framesPerSecondLabel, flag=fRight )
		pfgs.Add( self.framesPerSecond, flag=fRight )
		pfgs.Add( self.framesPerSecondUnit, flag=fLeft )
		pfgs.Add( self.frameProcessingTimeLabel, flag=fRight )
		pfgs.Add( self.frameProcessingTime, flag=fRight )
		pfgs.Add( self.frameProcessingTimeUnit, flag=fLeft )

		statsSizer = wx.BoxSizer( wx.VERTICAL )
		statsSizer.Add( cameraDeviceSizer )
		statsSizer.Add( pfgs, flag=wx.TOP, border=8 )
		headerSizer.Add( statsSizer, flag=wx.ALL, border=4 )
		
		mainSizer.Add( headerSizer )
		
		#------------------------------------------------------------------------------------------------
		self.finishStrip = FinishStripPanel( self, size=(-1,wx.GetDisplaySize()[1]//2) )
		self.finishStrip.finish.Bind( wx.EVT_RIGHT_DOWN, self.onRightClick )
		
		self.primaryImage = ScaledImage( self, style=wx.BORDER_SUNKEN, size=(imageWidth, imageHeight) )
		self.primaryImage.SetTestImage()
		
		hsDate = wx.BoxSizer( wx.HORIZONTAL )
		hsDate.Add( wx.StaticText(self, label='Show Triggers for'), flag=wx.ALIGN_CENTER_VERTICAL )
		tQuery = now()
		self.date = wx.DatePickerCtrl(
			self,
			dt=wx.DateTimeFromDMY( tQuery.day, tQuery.month-1, tQuery.year ),
			style=wx.DP_DROPDOWN|wx.DP_SHOWCENTURY
		)
		self.date.Bind( wx.EVT_DATE_CHANGED, self.onQueryDateChanged )
		hsDate.Add( self.date, flag=wx.LEFT, border=2 )
		
		hsDate.Add( wx.StaticText(self, label='Filter by Bib'), flag=wx.ALIGN_CENTER_VERTICAL|wx.LEFT, border=12 )
		self.bib = wx.lib.intctrl.IntCtrl( self, style=wx.TE_PROCESS_ENTER, size=(64,-1), min=1, allow_none=True, value=None )
		self.bib.Bind( wx.EVT_TEXT_ENTER, self.onQueryBibChanged )
		hsDate.Add( self.bib, flag=wx.LEFT, border=2 )
		
		self.tsQueryLower = datetime(tQuery.year, tQuery.month, tQuery.day)
		self.tsQueryUpper = self.tsQueryLower + timedelta(days=1)
		self.bibQuery = None
		
		self.triggerList = AutoWidthListCtrl( self, style=wx.LC_REPORT|wx.BORDER_SUNKEN|wx.LC_SORT_ASCENDING )
		
		self.il = wx.ImageList(16, 16)
		self.sm_check = self.il.Add( Utils.GetPngBitmap('check_icon.png'))
		self.sm_close = self.il.Add( Utils.GetPngBitmap('flame_icon.png'))
		self.sm_up = self.il.Add( Utils.GetPngBitmap('SmallUpArrow.png'))
		self.sm_dn = self.il.Add( Utils.GetPngBitmap('SmallDownArrow.png'))
		self.triggerList.SetImageList(self.il, wx.IMAGE_LIST_SMALL)
		
		headers = ['Time', 'Bib', 'Name', 'Team', 'Wave']
		for i, h in enumerate(headers):
			self.triggerList.InsertColumn(i, h, wx.LIST_FORMAT_RIGHT if h == 'Bib' else wx.LIST_FORMAT_LEFT)
		self.itemDataMap = {}
		
		self.triggerList.Bind( wx.EVT_LIST_ITEM_SELECTED, self.onTriggerSelected )
		
		self.messagesText = wx.TextCtrl( self, style=wx.TE_READONLY|wx.TE_MULTILINE|wx.HSCROLL, size=(250,-1) )
		self.messageManager = MessageManager( self.messagesText )
		
		vsTriggers = wx.BoxSizer( wx.VERTICAL )
		vsTriggers.Add( hsDate )
		vsTriggers.Add( self.triggerList, 1, flag=wx.EXPAND|wx.TOP, border=2)
		
		#------------------------------------------------------------------------------------------------
		mainSizer.Add( self.finishStrip, 1, flag=wx.EXPAND )
		
		border = 2
		row1Sizer = wx.BoxSizer( wx.HORIZONTAL )
		row1Sizer.Add( self.primaryImage, flag=wx.ALL, border=border )
		row1Sizer.Add( vsTriggers, 1, flag=wx.TOP|wx.BOTTOM|wx.RIGHT|wx.EXPAND, border=border )
		row1Sizer.Add( self.messagesText, flag=wx.TOP|wx.BOTTOM|wx.RIGHT|wx.EXPAND, border=border )
		mainSizer.Add( row1Sizer, flag=wx.EXPAND )
		
		#------------------------------------------------------------------------------------------------
		# Create a timer to update the frame loop.
		#
		self.timer = wx.Timer()
		self.timer.Bind( wx.EVT_TIMER, self.frameLoop )
		
		self.Bind(wx.EVT_CLOSE, self.onCloseWindow)

		self.readOptions()
		self.SetSizerAndFit( mainSizer )
		
		# Start the message reporting thread so we can see what is going on.
		self.messageThread = threading.Thread( target=self.showMessages )
		self.messageThread.daemon = True
		self.messageThread.start()
		
		self.grabFrameOK = False
		self.tsMax = None
		
		# Start the frame loop.
		delayAdjustment = 0.80 if 'win' in sys.platform else 0.98
		ms = int(1000 * self.frameDelay * delayAdjustment)
		self.timer.Start( ms, False )
		
		wx.CallLater( 300, self.refreshTriggers )
	
	def onQueryDateChanged( self, event ):
		v = self.date.GetValue()
		self.tsQueryLower = datetime( v.GetYear(), v.GetMonth() + 1, v.GetDay() )
		self.tsQueryUpper = self.tsQueryLower + timedelta( days=1 )
		self.refreshTriggers( True )
	
	def onQueryBibChanged( self, event ):
		self.bibQuery = self.bib.GetValue()
		self.refreshTriggers( True )
	
	def GetListCtrl( self ):
		return self.triggerList
	
	def GetSortImages(self):
		return (self.sm_dn, self.sm_up)
	
	def getItemData( self, i ):
		data = self.triggerList.GetItemData( i )
		return self.itemDataMap[data]
	
	def refreshTriggers( self, replace=False ):
		tNow = now()
		self.lastTriggerRefresh = tNow
		
		if replace:
			tsLower = self.tsQueryLower
			tsUpper = self.tsQueryUpper
			self.triggerList.DeleteAllItems()
			self.itemDataMap = {}
			self.tsMax = None
			self.iTriggerSelect = None
			self.triggerInfo = {}
		else:
			tsLower = (self.tsMax or datetime(tNow.year, tNow.month, tNow.day)) + timedelta(seconds=0.00001)
			tsUpper = tsLower + timedelta(days=1)

		triggers = self.db.getTriggers( tsLower, tsUpper, self.bibQuery )
		if not triggers:
			return
			
		tsPrev = (self.tsMax or datetime(2000,1,1))
		self.tsMax = triggers[-1][0]
		
		for i, (ts,bib,first_name,last_name,team,wave,race_name) in enumerate(triggers):
			closeFinish = ((ts-tsPrev).total_seconds() < 0.3)
			row = self.triggerList.InsertImageStringItem( sys.maxint, ts.strftime('%H:%M:%S.%f')[:-3], self.sm_close if closeFinish else self.sm_check )
			if closeFinish and row > 0:
				self.triggerList.SetItemImage( row-1, self.sm_close )
			self.triggerList.SetStringItem( row, 1, u'{:>6}'.format(bib) )
			name = u', '.join( n for n in (last_name, first_name) if n )
			self.triggerList.SetStringItem( row, 2, name )
			self.triggerList.SetStringItem( row, 3, team )
			self.triggerList.SetStringItem( row, 4, wave )
			
			self.triggerList.SetItemData( row, row )
			self.itemDataMap[row] = (ts,bib,name,team,wave,race_name,first_name,last_name)
			tsPrev = ts
			
		for i in xrange(5):
			self.triggerList.SetColumnWidth(i, wx.LIST_AUTOSIZE)

		self.triggerList.EnsureVisible( self.triggerList.GetItemCount() - 1 )

	def Start( self ):
		self.messageQ.put( ('', '************************************************') )
		self.messageQ.put( ('started', now().strftime('%Y/%m/%d %H:%M:%S')) )
		self.startThreads()
		self.startCamera()

	def onTest( self, event ):
		self.testCount = getattr(self, 'testCount', 0) + 1
		self.requestQ.put( {
				'time':now(),
				'bib':self.testCount,
				'firstName':u'Test',
				'lastName':u'Test',
				'team':u'Test',
				'wave':u'Test',
				'raceName':u'Test',				
			}
		)
		wx.CallLater( 500, self.dbWriterQ.put, ('flush',) )
		wx.CallLater( int(100+1000*int(tdCaptureBefore.total_seconds())), self.refreshTriggers )
	
	def onRightClick( self, event ):
		if not self.triggerInfo:
			return
		self.xFinish = event.GetX()
		pd = PhotoDialog( self, self.finishStrip.finish.getJpg(self.xFinish), self.triggerInfo, self.finishStrip.GetTsJpgs(), self.fps )
		pd.ShowModal()
		pd.Destroy()

	def setFinishStripJpgs( self, jpgs ):
		self.tsJpg = jpgs
		wx.CallAfter( self.finishStrip.SetTsJpgs, self.tsJpg, self.ts, self.triggerInfo )

	def onTriggerSelected( self, event ):
		self.iTriggerSelect = event.m_itemIndex
		data = self.itemDataMap[self.triggerList.GetItemData(self.iTriggerSelect)]
		self.triggerInfo = {
			a:data[i] for i, a in enumerate(('ts','bib','name','team','wave','raceName','firstName','lastName'))
		}
		self.ts = data[0]
		self.dbReaderQ.put( ('getphotos', self.ts-tdCaptureBefore, self.ts+tdCaptureAfter) )
		
	def showMessages( self ):
		while 1:
			message = self.messageQ.get()
			assert len(message) == 2, 'Incorrect message length'
			cmd, info = message
			wx.CallAfter( self.messageManager.write, '{}:  {}'.format(cmd, info) if cmd else info )
		
	def startCamera( self ):
		self.camera = None
		try:
			self.camera = Device( max(self.getCameraDeviceNum(), 0) )
		except Exception as e:
			self.messageQ.put( ('camera', 'Error: {}'.format(e)) )
			return False
		
		#self.camera.set_resolution( 640, 480 )
		self.messageQ.put( ('camera', 'Successfully Connected: Device: {}'.format(self.getCameraDeviceNum()) ) )
		return True
	
	def startThreads( self ):
		self.grabFrameOK = False
		
		self.listenerThread = SocketListener( self.requestQ, self.messageQ )		
		error = self.listenerThread.test()
		if error:
			wx.MessageBox('Socket Error:\n\n{}\n\nIs another CrossMgrVideo or CrossMgrCamera running on this computer?'.format(error),
				"Socket Error",
				wx.OK | wx.ICON_ERROR
			)
			wx.Exit()
		
		self.dbWriterThread = threading.Thread( target=DBWriter, args=(self.dbWriterQ,) )
		self.dbWriterThread.daemon = True
		
		self.dbReaderThread = threading.Thread( target=DBReader, args=(self.dbReaderQ, self.setFinishStripJpgs) )
		self.dbReaderThread.daemon = True
		
		self.fcb = FrameCircBuf( int(self.bufferSecs * self.fps) )
		
		self.listenerThread.start()
		self.dbWriterThread.start()
		self.dbReaderThread.start()
		
		self.grabFrameOK = True
		self.messageQ.put( ('threads', 'Successfully Launched') )
		return True
	
	def stopCapture( self ):
		self.dbWriterQ.put( ('flush',) )
	
	def frameLoop( self, event=None ):
		if not self.grabFrameOK:
			return
			
		self.frameCount += 1
		tNow = now()
		
		# Compute frame rate statistics.
		if self.frameCount == self.frameCountUpdate:
			self.fpsActual = self.frameCount / (tNow - self.tFrameCount).total_seconds()
			self.frameCount = 0
			self.tFrameCount = tNow
			self.framesPerSecond.SetLabel( u'{:.3f}'.format(self.fpsActual) )
			self.availableMsPerFrame.SetLabel( u'{:.0f}'.format(1000.0/self.fpsActual) )
			self.frameProcessingTime.SetLabel( u'{:.0f}'.format(1000.0*self.fpt.total_seconds()) )
		
		try:
			image = self.camera.getImage()
		except Exception as e:
			self.messageQ.put( ('error', 'Webcam Failure: {}'.format(e) ) )
			self.grabFrameOK = False
			return
		
		image = PilImageToWxImage( image )
		
		# Add the image to the circular buffer.
		self.fcb.append( tNow, image )
		
		# Update the monitor screen.
		if self.frameCount & 3 == 0:
			wx.CallAfter( self.primaryImage.SetImage, image )
		
		# Record images if the timer is running.
		if self.captureTimer.IsRunning():
			self.dbWriterQ.put( ('photo', tNow, image) )
		
		# Periodically update events.
		if (tNow - self.lastTriggerRefresh).total_seconds() > 5.0:
			self.refreshTriggers()
			self.lastTriggerRefresh = tNow
			return
		
		# Process event messages
		while 1:
			try:
				message = self.requestQ.get(False)
			except Empty:
				break
			
			tSearch = message['time']
			advanceSeconds = message.get('advanceSeconds', 0.0)
			tSearch += timedelta(seconds=advanceSeconds)
			
			# Record this trigger.
			self.dbWriterQ.put( (
				'trigger',
				tSearch - timedelta(seconds=advanceSeconds),
				message.get('bib', 99999),
				message.get('firstName',u''),
				message.get('lastName',u''),
				message.get('team',u''),
				message.get('wave',u''),
				message.get('raceName',u'')
			) )

			# If we are not currently capturing, make sure we record past frames.
			if not self.captureTimer.IsRunning():
				times, frames = self.fcb.getBackFrames( tSearch - tdCaptureBefore )
				for t, f in zip(times, frames):
					if f:
						self.dbWriterQ.put( ('photo', t, f) )
			else:
				self.captureTimer.Stop()
			
			# Set a timer to stop recording after the capture window.
			millis = int((tSearch - now() + tdCaptureAfter).total_seconds() * 1000.0)
			if millis > 0:
				self.captureTimer.Start( millis )
			
			if (now() - tNow).total_seconds() > self.frameDelay / 2.0:
				break
				
		self.fpt = now() - tNow
		self.tLast = tNow
	
	def shutdown( self ):
		# Ensure that all images in the queue are saved.
		self.grabFrameOK = False
		if hasattr(self, 'dbWriterThread'):
			self.dbWriterQ.put( ('terminate', ) )
			self.dbWriterThread.join()
			self.dbReaderQ.put( ('terminate', ) )
			self.dbReaderThread.join()
	
	def resetCamera( self, event ):
		self.writeOptions()
		
		dlg = ConfigDialog( self, self.getCameraDeviceNum() )
		ret = dlg.ShowModal()
		cameraDeviceNum = dlg.GetCameraDeviceNum()
		dlg.Destroy()
		if ret != wx.ID_OK:
			return
		
		self.setCameraDeviceNum( cameraDeviceNum )
		self.grabFrameOK = self.startCamera()
	
	def manageDatabase( self, event ):
		dlg = ManageDatabase( self, self.db.getsize(), self.db.fname, title='Manage Database' )
		if dlg.ShowModal() == wx.ID_OK:
			tsLower, tsUpper = dlg.GetDates()
			self.db.cleanBetween( tsLower, tsUpper )
			wx.CallAfter( self.finishStrip.Clear )
			wx.CallAfter( self.refreshTriggers, True )
		dlg.Destroy()
	
	def setCameraDeviceNum( self, num ):
		self.cameraDevice.SetLabel( unicode(num) )
		
	def getCameraDeviceNum( self ):
		return int(self.cameraDevice.GetLabel())
		
	def onCloseWindow( self, event ):
		self.shutdown()
		wx.Exit()
		
	def writeOptions( self ):
		self.config.Write( 'CameraDevice', self.cameraDevice.GetLabel() )
		self.config.Flush()
	
	def readOptions( self ):
		self.cameraDevice.SetLabel( self.config.Read('CameraDevice', u'0') )
Beispiel #9
0
class MainWin(wx.Frame):
    def __init__(self, parent, id=wx.ID_ANY, title='', size=(1000, 800)):
        wx.Frame.__init__(self, parent, id, title, size=size)

        self.db = Database()

        self.fps = 25
        self.frameDelay = 1.0 / self.fps
        self.bufferSecs = 10
        self.xFinish = None

        self.tFrameCount = self.tLaunch = self.tLast = now()
        self.frameCount = 0
        self.frameCountUpdate = int(self.fps * 2)
        self.fpsActual = 0.0
        self.fpt = timedelta(seconds=0)
        self.iTriggerSelect = None
        self.triggerInfo = None

        self.captureTimer = wx.CallLater(10, self.stopCapture)

        self.fcb = FrameCircBuf(self.bufferSecs * self.fps)

        self.config = wx.Config(
            appName="CrossMgrVideo",
            vendorName="SmartCyclingSolutions",
            #style=wx.CONFIG_USE_LOCAL_FILE
        )

        self.requestQ = Queue()  # Select photos from photobuf.
        self.dbWriterQ = Queue()  # Photos waiting to be written
        self.dbReaderQ = Queue()  # Photos read as requested from user.
        self.messageQ = Queue(
        )  # Collection point for all status/failure messages.

        self.SetBackgroundColour(wx.Colour(232, 232, 232))

        mainSizer = wx.BoxSizer(wx.VERTICAL)

        #------------------------------------------------------------------------------------------------
        headerSizer = wx.BoxSizer(wx.HORIZONTAL)

        self.logo = Utils.GetPngBitmap('CrossMgrHeader.png')
        headerSizer.Add(wx.StaticBitmap(self, wx.ID_ANY, self.logo))

        self.title = wx.StaticText(self,
                                   label='CrossMgr Video\nVersion {}'.format(
                                       AppVerName.split()[1]),
                                   style=wx.ALIGN_RIGHT)
        self.title.SetFont(
            wx.Font((0, 28), wx.FONTFAMILY_SWISS, wx.FONTSTYLE_NORMAL,
                    wx.FONTWEIGHT_NORMAL))
        headerSizer.Add(self.title, flag=wx.ALL, border=10)

        #------------------------------------------------------------------------------
        self.cameraDeviceLabel = wx.StaticText(self, label='Camera Device:')
        self.cameraDevice = wx.StaticText(self)
        boldFont = self.cameraDevice.GetFont()
        boldFont.SetWeight(wx.BOLD)
        self.cameraDevice.SetFont(boldFont)
        self.cameraResolution = wx.StaticText(self)
        self.cameraResolution.SetFont(boldFont)
        self.reset = wx.Button(self, label="Reset Camera")
        self.reset.Bind(wx.EVT_BUTTON, self.resetCamera)

        self.manage = wx.Button(self, label="Manage Database")
        self.manage.Bind(wx.EVT_BUTTON, self.manageDatabase)

        self.test = wx.Button(self, label="Test")
        self.test.Bind(wx.EVT_BUTTON, self.onTest)

        self.focus = wx.Button(self, label="Focus...")
        self.focus.Bind(wx.EVT_BUTTON, self.onFocus)

        cameraDeviceSizer = wx.BoxSizer(wx.HORIZONTAL)
        cameraDeviceSizer.Add(self.cameraDeviceLabel,
                              flag=wx.ALIGN_CENTRE_VERTICAL | wx.ALIGN_RIGHT)
        cameraDeviceSizer.Add(self.cameraDevice,
                              flag=wx.ALIGN_CENTRE_VERTICAL | wx.LEFT,
                              border=8)
        cameraDeviceSizer.Add(self.cameraResolution,
                              flag=wx.ALIGN_CENTRE_VERTICAL | wx.LEFT,
                              border=8)
        cameraDeviceSizer.Add(self.reset,
                              flag=wx.ALIGN_CENTRE_VERTICAL | wx.LEFT,
                              border=32)
        cameraDeviceSizer.Add(self.manage,
                              flag=wx.ALIGN_CENTRE_VERTICAL | wx.LEFT,
                              border=16)
        cameraDeviceSizer.Add(self.test,
                              flag=wx.ALIGN_CENTRE_VERTICAL | wx.LEFT,
                              border=16)
        cameraDeviceSizer.Add(self.focus,
                              flag=wx.ALIGN_CENTRE_VERTICAL | wx.LEFT,
                              border=16)

        #------------------------------------------------------------------------------
        self.targetProcessingTimeLabel = wx.StaticText(self,
                                                       label='Target Frames:')
        self.targetProcessingTime = wx.StaticText(self,
                                                  label=u'{:.3f}'.format(
                                                      self.fps))
        self.targetProcessingTime.SetFont(boldFont)
        self.targetProcessingTimeUnit = wx.StaticText(self, label='/ sec')

        self.framesPerSecondLabel = wx.StaticText(self, label='Actual Frames:')
        self.framesPerSecond = wx.StaticText(self, label='25.000')
        self.framesPerSecond.SetFont(boldFont)
        self.framesPerSecondUnit = wx.StaticText(self, label='/ sec')

        self.availableMsPerFrameLabel = wx.StaticText(
            self, label='Available Time Per Frame:')
        self.availableMsPerFrame = wx.StaticText(self,
                                                 label=u'{:.0f}'.format(
                                                     1000.0 * self.frameDelay))
        self.availableMsPerFrame.SetFont(boldFont)
        self.availableMsPerFrameUnit = wx.StaticText(self, label='ms')

        self.frameProcessingTimeLabel = wx.StaticText(
            self, label='Frame Processing Time:')
        self.frameProcessingTime = wx.StaticText(self, label='20')
        self.frameProcessingTime.SetFont(boldFont)
        self.frameProcessingTimeUnit = wx.StaticText(self, label='ms')

        pfgs = wx.FlexGridSizer(rows=0, cols=6, vgap=4, hgap=8)
        fRight = wx.ALIGN_CENTRE_VERTICAL | wx.ALIGN_RIGHT
        fLeft = wx.ALIGN_CENTRE_VERTICAL

        #------------------- Row 1 ------------------------------
        pfgs.Add(self.targetProcessingTimeLabel, flag=fRight)
        pfgs.Add(self.targetProcessingTime, flag=fRight)
        pfgs.Add(self.targetProcessingTimeUnit, flag=fLeft)
        pfgs.Add(self.availableMsPerFrameLabel, flag=fRight)
        pfgs.Add(self.availableMsPerFrame, flag=fRight)
        pfgs.Add(self.availableMsPerFrameUnit, flag=fLeft)

        #------------------- Row 2 ------------------------------
        pfgs.Add(self.framesPerSecondLabel, flag=fRight)
        pfgs.Add(self.framesPerSecond, flag=fRight)
        pfgs.Add(self.framesPerSecondUnit, flag=fLeft)
        pfgs.Add(self.frameProcessingTimeLabel, flag=fRight)
        pfgs.Add(self.frameProcessingTime, flag=fRight)
        pfgs.Add(self.frameProcessingTimeUnit, flag=fLeft)

        statsSizer = wx.BoxSizer(wx.VERTICAL)
        statsSizer.Add(cameraDeviceSizer)
        statsSizer.Add(pfgs, flag=wx.TOP, border=8)
        headerSizer.Add(statsSizer, flag=wx.ALL, border=4)

        mainSizer.Add(headerSizer)

        #------------------------------------------------------------------------------------------------
        self.finishStrip = FinishStripPanel(self,
                                            size=(-1,
                                                  wx.GetDisplaySize()[1] // 2))
        self.finishStrip.finish.Bind(wx.EVT_RIGHT_DOWN, self.onRightClick)

        self.primaryImage = ScaledImage(self,
                                        style=wx.BORDER_SUNKEN,
                                        size=(imageWidth, imageHeight))
        self.primaryImage.SetTestImage()

        self.focusDialog = FocusDialog(self)

        hsDate = wx.BoxSizer(wx.HORIZONTAL)
        hsDate.Add(wx.StaticText(self, label='Show Triggers for'),
                   flag=wx.ALIGN_CENTER_VERTICAL)
        tQuery = now()
        self.date = wx.adv.DatePickerCtrl(
            self,
            dt=wx.DateTime.FromDMY(tQuery.day, tQuery.month - 1, tQuery.year),
            style=wx.adv.DP_DROPDOWN | wx.adv.DP_SHOWCENTURY)
        self.date.Bind(wx.adv.EVT_DATE_CHANGED, self.onQueryDateChanged)
        hsDate.Add(self.date, flag=wx.LEFT, border=2)

        hsDate.Add(wx.StaticText(self, label='Filter by Bib'),
                   flag=wx.ALIGN_CENTER_VERTICAL | wx.LEFT,
                   border=12)
        self.bib = wx.lib.intctrl.IntCtrl(self,
                                          style=wx.TE_PROCESS_ENTER,
                                          size=(64, -1),
                                          min=1,
                                          allow_none=True,
                                          value=None)
        self.bib.Bind(wx.EVT_TEXT_ENTER, self.onQueryBibChanged)
        hsDate.Add(self.bib, flag=wx.LEFT, border=2)

        self.tsQueryLower = datetime(tQuery.year, tQuery.month, tQuery.day)
        self.tsQueryUpper = self.tsQueryLower + timedelta(days=1)
        self.bibQuery = None

        self.triggerList = AutoWidthListCtrl(
            self, style=wx.LC_REPORT | wx.BORDER_SUNKEN | wx.LC_SORT_ASCENDING)

        self.il = wx.ImageList(16, 16)
        self.sm_close = []
        for bm in getCloseFinishBitmaps():
            self.sm_close.append(self.il.Add(bm))
        self.sm_up = self.il.Add(Utils.GetPngBitmap('SmallUpArrow.png'))
        self.sm_up = self.il.Add(Utils.GetPngBitmap('SmallUpArrow.png'))
        self.sm_dn = self.il.Add(Utils.GetPngBitmap('SmallDownArrow.png'))
        self.triggerList.SetImageList(self.il, wx.IMAGE_LIST_SMALL)

        headers = ['Time', 'Bib', 'Name', 'Team', 'Wave', 'km/h', 'mph']
        for i, h in enumerate(headers):
            self.triggerList.InsertColumn(
                i, h,
                wx.LIST_FORMAT_RIGHT if h in ('Bib', 'km/h',
                                              'mph') else wx.LIST_FORMAT_LEFT)
        self.itemDataMap = {}

        self.triggerList.Bind(wx.EVT_LIST_ITEM_SELECTED,
                              self.onTriggerSelected)

        self.messagesText = wx.TextCtrl(self,
                                        style=wx.TE_READONLY | wx.TE_MULTILINE
                                        | wx.HSCROLL,
                                        size=(250, -1))
        self.messageManager = MessageManager(self.messagesText)

        vsTriggers = wx.BoxSizer(wx.VERTICAL)
        vsTriggers.Add(hsDate)
        vsTriggers.Add(self.triggerList, 1, flag=wx.EXPAND | wx.TOP, border=2)

        #------------------------------------------------------------------------------------------------
        mainSizer.Add(self.finishStrip, 1, flag=wx.EXPAND)

        border = 2
        row1Sizer = wx.BoxSizer(wx.HORIZONTAL)
        row1Sizer.Add(self.primaryImage, flag=wx.ALL, border=border)
        row1Sizer.Add(vsTriggers,
                      1,
                      flag=wx.TOP | wx.BOTTOM | wx.RIGHT | wx.EXPAND,
                      border=border)
        row1Sizer.Add(self.messagesText,
                      flag=wx.TOP | wx.BOTTOM | wx.RIGHT | wx.EXPAND,
                      border=border)
        mainSizer.Add(row1Sizer, flag=wx.EXPAND)

        #------------------------------------------------------------------------------------------------
        # Create a timer to update the frame loop.
        #
        self.timer = wx.Timer()
        self.timer.Bind(wx.EVT_TIMER, self.frameLoop)

        self.Bind(wx.EVT_CLOSE, self.onCloseWindow)

        self.readOptions()
        self.SetSizerAndFit(mainSizer)

        # Start the message reporting thread so we can see what is going on.
        self.messageThread = threading.Thread(target=self.showMessages)
        self.messageThread.daemon = True
        self.messageThread.start()

        self.grabFrameOK = False
        self.tsMax = None

        # Start the frame loop.
        delayAdjustment = 0.80 if 'win' in sys.platform else 0.98
        ms = int(1000 * self.frameDelay * delayAdjustment)
        self.timer.Start(ms, False)

        wx.CallLater(300, self.refreshTriggers)

    def onQueryDateChanged(self, event):
        v = self.date.GetValue()
        self.tsQueryLower = datetime(v.GetYear(), v.GetMonth() + 1, v.GetDay())
        self.tsQueryUpper = self.tsQueryLower + timedelta(days=1)
        self.refreshTriggers(True)

    def onQueryBibChanged(self, event):
        self.bibQuery = self.bib.GetValue()
        self.refreshTriggers(True)

    def GetListCtrl(self):
        return self.triggerList

    def GetSortImages(self):
        return (self.sm_dn, self.sm_up)

    def getItemData(self, i):
        data = self.triggerList.GetItemData(i)
        return self.itemDataMap[data]

    def refreshTriggers(self, replace=False, iTriggerRow=None):
        tNow = now()
        self.lastTriggerRefresh = tNow

        if replace:
            tsLower = self.tsQueryLower
            tsUpper = self.tsQueryUpper
            self.triggerList.DeleteAllItems()
            self.itemDataMap = {}
            self.tsMax = None
            self.iTriggerSelect = None
            self.triggerInfo = {}
        else:
            tsLower = (self.tsMax or datetime(
                tNow.year, tNow.month, tNow.day)) + timedelta(seconds=0.00001)
            tsUpper = tsLower + timedelta(days=1)

        triggers = self.db.getTriggers(tsLower, tsUpper, self.bibQuery)

        if not triggers:
            return

        tsPrev = (self.tsMax or datetime(2000, 1, 1))
        self.tsMax = triggers[-1][
            1]  # id,ts,bib,first_name,last_name,team,wave,race_name,kmh

        for i, (id, ts, bib, first_name, last_name, team, wave, race_name,
                kmh) in enumerate(triggers):
            dtFinish = (ts - tsPrev).total_seconds()
            itemImage = self.sm_close[min(
                len(self.sm_close) - 1,
                int(len(self.sm_close) * dtFinish / closeFinishThreshold))]
            row = self.triggerList.InsertItem(sys.maxint,
                                              ts.strftime('%H:%M:%S.%f')[:-3],
                                              itemImage)
            self.triggerList.SetItem(row, 1, u'{:>6}'.format(bib))
            name = u', '.join(n for n in (last_name, first_name) if n)
            self.triggerList.SetItem(row, 2, name)
            self.triggerList.SetItem(row, 3, team)
            self.triggerList.SetItem(row, 4, wave)
            if kmh:
                kmh_text, mph_text = u'{:.2f}'.format(kmh), u'{:.2f}'.format(
                    kmh * 0.621371)
            else:
                kmh_text = mph_text = u''
            self.triggerList.SetItem(row, 5, kmh_text)
            self.triggerList.SetItem(row, 6, mph_text)

            self.triggerList.SetItemData(row, row)
            self.itemDataMap[row] = (id, ts, bib, name, team, wave, race_name,
                                     first_name, last_name, kmh)
            tsPrev = ts

        for i in xrange(5):
            self.triggerList.SetColumnWidth(i, wx.LIST_AUTOSIZE)

        if iTriggerRow is not None and (0 <= iTriggerRow <
                                        self.triggerList.GetItemCount()):
            self.triggerList.EnsureVisible(iTriggerRow)
            self.triggerList.Select(iTriggerRow)
        else:
            self.triggerList.EnsureVisible(self.triggerList.GetItemCount() - 1)

    def Start(self):
        self.messageQ.put(
            ('', '************************************************'))
        self.messageQ.put(('started', now().strftime('%Y/%m/%d %H:%M:%S')))
        self.startThreads()
        self.startCamera()

    def onTest(self, event):
        self.testCount = getattr(self, 'testCount', 0) + 1
        self.requestQ.put({
            'time': now(),
            'bib': self.testCount,
            'firstName': u'Test',
            'lastName': u'Test',
            'team': u'Test',
            'wave': u'Test',
            'raceName': u'Test',
        })
        wx.CallLater(500, self.dbWriterQ.put, ('flush', ))
        wx.CallLater(int(100 + 1000 * int(tdCaptureBefore.total_seconds())),
                     self.refreshTriggers)

    def onFocus(self, event):
        self.focusDialog.Move((4, 4))
        self.focusDialog.ShowModal()

    def onRightClick(self, event):
        if not self.triggerInfo:
            return
        self.xFinish = event.GetX()

        pd = PhotoDialog(self, self.finishStrip.finish.getJpg(self.xFinish),
                         self.triggerInfo, self.finishStrip.GetTsJpgs(),
                         self.fps)
        pd.ShowModal()
        if self.triggerInfo['kmh'] != (pd.kmh or 0.0):
            self.dbWriterQ.put(('kmh', self.triggerInfo['id'], pd.kmh or 0.0))
            wx.CallLater(300,
                         self.refreshTriggers,
                         replace=True,
                         iTriggerRow=self.iTriggerSelect)
        pd.Destroy()

    def setFinishStripJpgs(self, jpgs):
        self.tsJpg = jpgs
        wx.CallAfter(self.finishStrip.SetTsJpgs, self.tsJpg, self.ts,
                     self.triggerInfo)

    def onTriggerSelected(self, event):
        self.iTriggerSelect = event.Index
        data = self.itemDataMap[self.triggerList.GetItemData(
            self.iTriggerSelect)]
        self.triggerInfo = {
            a: data[i]
            for i, a in enumerate(('id', 'ts', 'bib', 'name', 'team', 'wave',
                                   'raceName', 'firstName', 'lastName', 'kmh'))
        }
        self.ts = self.triggerInfo['ts']
        self.dbReaderQ.put(
            ('getphotos', self.ts - tdCaptureBefore, self.ts + tdCaptureAfter))

    def showMessages(self):
        while 1:
            message = self.messageQ.get()
            assert len(message) == 2, 'Incorrect message length'
            cmd, info = message
            wx.CallAfter(self.messageManager.write,
                         '{}:  {}'.format(cmd, info) if cmd else info)

    def startCamera(self):
        self.camera = None
        try:
            self.camera = Device(max(self.getCameraDeviceNum(), 0))
        except Exception as e:
            self.messageQ.put(('camera', 'Error: {}'.format(e)))
            return False

        try:
            self.camera.set_resolution(*self.getCameraResolution())
            self.messageQ.put(
                ('camera',
                 '{}x{} Supported'.format(*self.getCameraResolution())))
        except Exception as e:
            self.messageQ.put(('camera', '{}x{} Unsupported Resolution'.format(
                *self.getCameraResolution())))

        self.messageQ.put(
            ('camera', 'Successfully Connected: Device: {}'.format(
                self.getCameraDeviceNum())))
        return True

    def startThreads(self):
        self.grabFrameOK = False

        self.listenerThread = SocketListener(self.requestQ, self.messageQ)
        error = self.listenerThread.test()
        if error:
            wx.MessageBox(
                'Socket Error:\n\n{}\n\nIs another CrossMgrVideo or CrossMgrCamera running on this computer?'
                .format(error), "Socket Error", wx.OK | wx.ICON_ERROR)
            wx.Exit()

        self.dbWriterThread = threading.Thread(target=DBWriter,
                                               args=(self.dbWriterQ, ))
        self.dbWriterThread.daemon = True

        self.dbReaderThread = threading.Thread(target=DBReader,
                                               args=(self.dbReaderQ,
                                                     self.setFinishStripJpgs))
        self.dbReaderThread.daemon = True

        self.fcb = FrameCircBuf(int(self.bufferSecs * self.fps))

        self.listenerThread.start()
        self.dbWriterThread.start()
        self.dbReaderThread.start()

        self.grabFrameOK = True
        self.messageQ.put(('threads', 'Successfully Launched'))
        return True

    def stopCapture(self):
        self.dbWriterQ.put(('flush', ))

    def frameLoop(self, event=None):
        if not self.grabFrameOK:
            return

        self.frameCount += 1
        tNow = now()

        # Compute frame rate statistics.
        if self.frameCount == self.frameCountUpdate:
            self.fpsActual = self.frameCount / (
                tNow - self.tFrameCount).total_seconds()
            self.frameCount = 0
            self.tFrameCount = tNow
            self.framesPerSecond.SetLabel(u'{:.3f}'.format(self.fpsActual))
            self.availableMsPerFrame.SetLabel(u'{:.0f}'.format(1000.0 /
                                                               self.fpsActual))
            self.frameProcessingTime.SetLabel(u'{:.0f}'.format(
                1000.0 * self.fpt.total_seconds()))

        try:
            image = self.camera.getImage()
        except Exception as e:
            self.messageQ.put(('error', 'Webcam Failure: {}'.format(e)))
            self.grabFrameOK = False
            return

        if not image:
            return

        image = PilImageToWxImage(image)

        # Add the image to the circular buffer.
        self.fcb.append(tNow, image)

        # Update the monitor screen.
        if self.frameCount & 3 == 0:
            wx.CallAfter(self.primaryImage.SetImage, image)
        if self.focusDialog.IsShown():
            wx.CallAfter(self.focusDialog.SetImage, image)

        # Record images if the timer is running.
        if self.captureTimer.IsRunning():
            self.dbWriterQ.put(('photo', tNow, image))

        # Periodically update events.
        if (tNow - self.lastTriggerRefresh).total_seconds() > 5.0:
            self.refreshTriggers()
            self.lastTriggerRefresh = tNow
            return

        # Process event messages
        while 1:
            try:
                message = self.requestQ.get(False)
            except Empty:
                break

            tSearch = message['time']
            advanceSeconds = message.get('advanceSeconds', 0.0)
            tSearch += timedelta(seconds=advanceSeconds)

            # Record this trigger.
            self.dbWriterQ.put(
                ('trigger', tSearch - timedelta(seconds=advanceSeconds),
                 message.get('bib', 99999), message.get('firstName', u''),
                 message.get('lastName', u''), message.get('team', u''),
                 message.get('wave', u''), message.get('raceName', u'')))

            # If we are not currently capturing, make sure we record past frames.
            if not self.captureTimer.IsRunning():
                times, frames = self.fcb.getBackFrames(tSearch -
                                                       tdCaptureBefore)
                for t, f in zip(times, frames):
                    if f:
                        self.dbWriterQ.put(('photo', t, f))
            else:
                self.captureTimer.Stop()

            # Set a timer to stop recording after the capture window.
            millis = int(
                (tSearch - now() + tdCaptureAfter).total_seconds() * 1000.0)
            if millis > 0:
                self.captureTimer.Start(millis)

            if (now() - tNow).total_seconds() > self.frameDelay / 2.0:
                break

        self.fpt = now() - tNow
        self.tLast = tNow

    def shutdown(self):
        # Ensure that all images in the queue are saved.
        self.grabFrameOK = False
        if hasattr(self, 'dbWriterThread'):
            self.dbWriterQ.put(('terminate', ))
            self.dbWriterThread.join()
            self.dbReaderQ.put(('terminate', ))
            self.dbReaderThread.join()

    def resetCamera(self, event=None):
        dlg = ConfigDialog(self, self.getCameraDeviceNum(),
                           self.getCameraResolution())
        ret = dlg.ShowModal()
        cameraDeviceNum = dlg.GetCameraDeviceNum()
        cameraResolution = dlg.GetCameraResolution()
        dlg.Destroy()
        if ret != wx.ID_OK:
            return False

        self.setCameraDeviceNum(cameraDeviceNum)
        self.setCameraResolution(*cameraResolution)
        self.writeOptions()

        self.grabFrameOK = self.startCamera()
        return True

    def manageDatabase(self, event):
        dlg = ManageDatabase(self,
                             self.db.getsize(),
                             self.db.fname,
                             title='Manage Database')
        if dlg.ShowModal() == wx.ID_OK:
            tsLower, tsUpper = dlg.GetDates()
            self.db.cleanBetween(tsLower, tsUpper)
            wx.CallAfter(self.finishStrip.Clear)
            wx.CallAfter(self.refreshTriggers, True)
        dlg.Destroy()

    def setCameraDeviceNum(self, num):
        self.cameraDevice.SetLabel(unicode(num))

    def setCameraResolution(self, width, height):
        self.cameraResolution.SetLabel(u'{}x{}'.format(width, height))

    def getCameraDeviceNum(self):
        return int(self.cameraDevice.GetLabel())

    def getCameraResolution(self):
        try:
            resolution = [
                int(v) for v in self.cameraResolution.GetLabel().split('x')
            ]
            return resolution[0], resolution[1]
        except:
            return 640, 400

    def onCloseWindow(self, event):
        self.shutdown()
        wx.Exit()

    def writeOptions(self):
        self.config.Write('CameraDevice', self.cameraDevice.GetLabel())
        self.config.Write('CameraResolution', self.cameraResolution.GetLabel())
        self.config.Flush()

    def readOptions(self):
        self.cameraDevice.SetLabel(self.config.Read('CameraDevice', u'0'))
        self.cameraResolution.SetLabel(
            self.config.Read('CameraResolution', u'640x480'))
Beispiel #10
0
class MainWin(wx.Frame):
    def __init__(self, parent, id=wx.ID_ANY, title="", size=(1000, 800)):
        wx.Frame.__init__(self, parent, id, title, size=size)

        self.fps = 25
        self.frameDelay = 1.0 / self.fps
        self.bufferSecs = 10

        self.tFrameCount = self.tLaunch = self.tLast = now()
        self.frameCount = 0
        self.frameCountUpdate = self.fps * 2
        self.fpsActual = 0.0
        self.fpt = timedelta(seconds=0)

        self.fcb = FrameCircBuf(self.bufferSecs * self.fps)

        self.config = wx.Config(
            appName="CrossMgrCamera", vendorName="SmartCyclingSolutions", style=wx.CONFIG_USE_LOCAL_FILE
        )

        self.requestQ = Queue()  # Select photos from photobuf.
        self.writerQ = Queue(400)  # Selected photos waiting to be written out.
        self.ftpQ = Queue()  # Photos waiting to be ftp'd.
        self.renamerQ = Queue()  # Photos waiting to be renamed and possibly ftp'd.
        self.messageQ = Queue()  # Collection point for all status/failure messages.

        self.SetBackgroundColour(wx.Colour(232, 232, 232))

        mainSizer = wx.BoxSizer(wx.VERTICAL)

        self.primaryImage = ScaledImage(self, style=wx.BORDER_SUNKEN, size=(imageWidth, imageHeight))
        self.beforeImage = ScaledImage(self, style=wx.BORDER_SUNKEN, size=(imageWidth, imageHeight))
        self.afterImage = ScaledImage(self, style=wx.BORDER_SUNKEN, size=(imageWidth, imageHeight))
        self.beforeAfterImages = [self.beforeImage, self.afterImage]

        # ------------------------------------------------------------------------------------------------
        headerSizer = wx.BoxSizer(wx.HORIZONTAL)

        self.logo = Utils.GetPngBitmap("CrossMgrHeader.png")
        headerSizer.Add(wx.StaticBitmap(self, bitmap=self.logo))

        self.title = wx.StaticText(
            self, label="CrossMgr Camera\nVersion {}".format(AppVerName.split()[1]), style=wx.ALIGN_RIGHT
        )
        self.title.SetFont(wx.FontFromPixelSize(wx.Size(0, 28), wx.FONTFAMILY_SWISS, wx.NORMAL, wx.FONTWEIGHT_NORMAL))
        headerSizer.Add(self.title, flag=wx.ALL, border=10)

        # ------------------------------------------------------------------------------
        self.cameraDeviceLabel = wx.StaticText(self, label="Camera Device:")
        self.cameraDevice = wx.StaticText(self)
        boldFont = self.cameraDevice.GetFont()
        boldFont.SetWeight(wx.BOLD)
        self.cameraDevice.SetFont(boldFont)
        self.reset = wx.Button(self, label="Reset Camera")
        self.reset.Bind(wx.EVT_BUTTON, self.resetCamera)
        cameraDeviceSizer = wx.BoxSizer(wx.HORIZONTAL)
        cameraDeviceSizer.Add(self.cameraDeviceLabel, flag=wx.ALIGN_CENTRE_VERTICAL | wx.ALIGN_RIGHT)
        cameraDeviceSizer.Add(self.cameraDevice, flag=wx.ALIGN_CENTRE_VERTICAL | wx.LEFT, border=8)
        cameraDeviceSizer.Add(self.reset, flag=wx.ALIGN_CENTRE_VERTICAL | wx.LEFT, border=8)

        # ------------------------------------------------------------------------------
        self.targetProcessingTimeLabel = wx.StaticText(self, label="Target Frames:")
        self.targetProcessingTime = wx.StaticText(self, label=u"{:.3f}".format(self.fps))
        self.targetProcessingTime.SetFont(boldFont)
        self.targetProcessingTimeUnit = wx.StaticText(self, label="per sec")

        self.framesPerSecondLabel = wx.StaticText(self, label="Actual Frames:")
        self.framesPerSecond = wx.StaticText(self, label="25.000")
        self.framesPerSecond.SetFont(boldFont)
        self.framesPerSecondUnit = wx.StaticText(self, label="per sec")

        self.availableMsPerFrameLabel = wx.StaticText(self, label="Available Time Per Frame:")
        self.availableMsPerFrame = wx.StaticText(self, label=u"{:.0f}".format(1000.0 * self.frameDelay))
        self.availableMsPerFrame.SetFont(boldFont)
        self.availableMsPerFrameUnit = wx.StaticText(self, label="ms")

        self.frameProcessingTimeLabel = wx.StaticText(self, label="Actual Frame Processing:")
        self.frameProcessingTime = wx.StaticText(self, label="20")
        self.frameProcessingTime.SetFont(boldFont)
        self.frameProcessingTimeUnit = wx.StaticText(self, label="ms")

        pfgs = wx.FlexGridSizer(rows=0, cols=6, vgap=4, hgap=8)
        fRight = wx.ALIGN_CENTRE_VERTICAL | wx.ALIGN_RIGHT
        fLeft = wx.ALIGN_CENTRE_VERTICAL

        # ------------------- Row 1 ------------------------------
        pfgs.Add(self.targetProcessingTimeLabel, flag=fRight)
        pfgs.Add(self.targetProcessingTime, flag=fRight)
        pfgs.Add(self.targetProcessingTimeUnit, flag=fLeft)
        pfgs.Add(self.availableMsPerFrameLabel, flag=fRight)
        pfgs.Add(self.availableMsPerFrame, flag=fRight)
        pfgs.Add(self.availableMsPerFrameUnit, flag=fLeft)

        # ------------------- Row 2 ------------------------------
        pfgs.Add(self.framesPerSecondLabel, flag=fRight)
        pfgs.Add(self.framesPerSecond, flag=fRight)
        pfgs.Add(self.framesPerSecondUnit, flag=fLeft)
        pfgs.Add(self.frameProcessingTimeLabel, flag=fRight)
        pfgs.Add(self.frameProcessingTime, flag=fRight)
        pfgs.Add(self.frameProcessingTimeUnit, flag=fLeft)

        statsSizer = wx.BoxSizer(wx.VERTICAL)
        statsSizer.Add(cameraDeviceSizer)
        statsSizer.Add(pfgs, flag=wx.TOP, border=8)
        headerSizer.Add(statsSizer, flag=wx.ALL, border=4)
        mainSizer.Add(headerSizer)

        self.messagesText = wx.TextCtrl(
            self, style=wx.TE_READONLY | wx.TE_MULTILINE | wx.HSCROLL, size=(350, imageHeight)
        )
        self.messageManager = MessageManager(self.messagesText)

        border = 2
        row1Sizer = wx.BoxSizer(wx.HORIZONTAL)
        row1Sizer.Add(self.primaryImage, flag=wx.ALL, border=border)
        row1Sizer.Add(self.messagesText, 1, flag=wx.TOP | wx.BOTTOM | wx.RIGHT | wx.EXPAND, border=border)
        mainSizer.Add(row1Sizer, 1, flag=wx.EXPAND)

        row2Sizer = wx.BoxSizer(wx.HORIZONTAL)
        row2Sizer.Add(self.beforeImage, flag=wx.LEFT | wx.RIGHT | wx.BOTTOM, border=border)
        row2Sizer.Add(self.afterImage, flag=wx.RIGHT | wx.BOTTOM, border=border)
        mainSizer.Add(row2Sizer)

        # ------------------------------------------------------------------------------------------------
        # Create a timer to update the frame loop.
        #
        self.timer = wx.Timer()
        self.timer.Bind(wx.EVT_TIMER, self.frameLoop)

        self.Bind(wx.EVT_CLOSE, self.onCloseWindow)

        self.readOptions()
        self.SetSizerAndFit(mainSizer)

        # Start the message reporting thread so we can see what is going on.
        self.messageThread = threading.Thread(target=self.showMessages)
        self.messageThread.daemon = True
        self.messageThread.start()

        self.grabFrameOK = False

        for i in [self.primaryImage, self.beforeImage, self.afterImage]:
            i.SetTestImage()

            # Start the frame loop.
        delayAdjustment = 0.80 if "win" in sys.platform else 0.98
        ms = int(1000 * self.frameDelay * delayAdjustment)
        self.timer.Start(ms, False)

    def Start(self):
        self.messageQ.put(("", "************************************************"))
        self.messageQ.put(("started", now().strftime("%Y/%m/%d %H:%M:%S")))
        self.startThreads()
        self.startCamera()

    def showMessages(self):
        while 1:
            message = self.messageQ.get()
            assert len(message) == 2, "Incorrect message length"
            cmd, info = message
            wx.CallAfter(self.messageManager.write, "{}:  {}".format(cmd, info) if cmd else info)

    def startCamera(self):
        self.camera = None
        try:
            self.camera = Device(max(self.getCameraDeviceNum(), 0))
        except Exception as e:
            self.messageQ.put(("camera", "Error: {}".format(e)))
            return False

            # self.camera.set_resolution( 640, 480 )
        self.messageQ.put(("camera", "Successfully Connected: Device: {}".format(self.getCameraDeviceNum())))
        for i in self.beforeAfterImages:
            i.SetTestImage()
        return True

    def startThreads(self):
        self.grabFrameOK = False

        self.listenerThread = SocketListener(self.requestQ, self.messageQ)
        error = self.listenerThread.test()
        if error:
            wx.MessageBox(
                "Socket Error:\n\n{}\n\nIs another CrossMgrVideo or CrossMgrCamera running on this computer?".format(
                    error
                ),
                "Socket Error",
                wx.OK | wx.ICON_ERROR,
            )
            wx.Exit()

        self.writerThread = threading.Thread(target=PhotoWriter, args=(self.writerQ, self.messageQ, self.ftpQ))
        self.writerThread.daemon = True

        self.renamerThread = threading.Thread(target=PhotoRenamer, args=(self.renamerQ, self.writerQ, self.messageQ))
        self.renamerThread.daemon = True

        self.ftpThread = threading.Thread(target=FTPWriter, args=(self.ftpQ, self.messageQ))
        self.ftpThread.daemon = True

        self.fcb = FrameCircBuf(int(self.bufferSecs * self.fps))

        self.listenerThread.start()
        self.writerThread.start()
        self.renamerThread.start()
        self.ftpThread.start()

        self.grabFrameOK = True
        self.messageQ.put(("threads", "Successfully Launched"))
        return True

    def frameLoop(self, event=None):
        if not self.grabFrameOK:
            return

        self.frameCount += 1
        tNow = now()

        # Compute frame rate statistics.
        if self.frameCount == self.frameCountUpdate:
            self.fpsActual = self.frameCount / (tNow - self.tFrameCount).total_seconds()
            self.frameCount = 0
            self.tFrameCount = tNow
            self.framesPerSecond.SetLabel(u"{:.3f}".format(self.fpsActual))
            self.availableMsPerFrame.SetLabel(u"{:.0f}".format(1000.0 / self.fpsActual))
            self.frameProcessingTime.SetLabel(u"{:.0f}".format(1000.0 * self.fpt.total_seconds()))

        try:
            image = self.camera.getImage()
        except Exception as e:
            self.messageQ.put(("error", "Webcam Failure: {}".format(e)))
            self.grabFrameOK = False
            return

        image = AddPhotoHeader(
            PilImageToWxImage(image),
            time=tNow,
            raceSeconds=(tNow - self.tLaunch).total_seconds(),
            bib=999,
            firstNameTxt=u"Firstname",
            lastNameTxt=u"LASTNAME",
            teamTxt=u"Team",
            raceNameTxt="Racename",
        )
        self.fcb.append(tNow, image)
        wx.CallAfter(self.primaryImage.SetImage, image)

        # Process any save messages
        while 1:
            try:
                message = self.requestQ.get(False)
            except Empty:
                break

            tSearch = message["time"]
            advanceSeconds = message.get("advanceSeconds", 0.0)
            if advanceSeconds:
                tSearch += timedelta(seconds=advanceSeconds)
            times, frames = self.fcb.findBeforeAfter(tSearch, 1, 1)
            if not frames:
                self.messageQ.put(
                    ("error", "No photos for {} at {}".format(message.get("bib", None), message["time"].isoformat()))
                )

            lastImage = None
            for i, f in enumerate(frames):

                fname = GetPhotoFName(
                    message["dirName"], message.get("bib", None), message.get("raceSeconds", None), i, times[i]
                )
                image = AddPhotoHeader(
                    f,
                    message.get("bib", None),
                    message.get("time", None),
                    message.get("raceSeconds", None),
                    message.get("firstName", u""),
                    message.get("lastName", u""),
                    message.get("team", u""),
                    message.get("raceName", u""),
                )

                # if lastImage is not None and image.GetData() == lastImage.GetData():
                # 	self.messageQ.put( ('duplicate', '"{}"'.format(os.path.basename(fname))) )
                # 	continue

                wx.CallAfter(self.beforeAfterImages[i].SetImage, image)

                SaveImage(fname, image, message.get("ftpInfo", None), self.messageQ, self.writerQ)
                lastImage = image

            if (now() - tNow).total_seconds() > self.frameDelay / 2.0:
                break

        self.fpt = now() - tNow
        self.tLast = tNow

    def shutdown(self):
        # Ensure that all images in the queue are saved.
        self.grabFrameOK = False
        if hasattr(self, "writerThread"):
            self.writerQ.put(("terminate",))
            self.writerThread.join()

    def resetCamera(self, event):
        self.writeOptions()

        dlg = ConfigDialog(self, self.getCameraDeviceNum())
        ret = dlg.ShowModal()
        cameraDeviceNum = dlg.GetCameraDeviceNum()
        dlg.Destroy()
        if ret != wx.ID_OK:
            return

        self.setCameraDeviceNum(cameraDeviceNum)
        self.grabFrameOK = self.startCamera()

    def setCameraDeviceNum(self, num):
        self.cameraDevice.SetLabel(unicode(num))

    def getCameraDeviceNum(self):
        return int(self.cameraDevice.GetLabel())

    def onCloseWindow(self, event):
        self.shutdown()
        wx.Exit()

    def writeOptions(self):
        self.config.Write("CameraDevice", self.cameraDevice.GetLabel())
        self.config.Flush()

    def readOptions(self):
        self.cameraDevice.SetLabel(self.config.Read("CameraDevice", u"0"))