/// <summary>
        /// Refresh the UI at a fixed period specified by the timer
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void UpdateUITimer_Tick(object sender, EventArgs e)
        {
            m_UIUpdateTimer.Stop();
            double interframeDuration = m_UIUpdateTimer.Duration();

            QueryPerfCounter uiIntraFrameTimer = new QueryPerfCounter();

            uiIntraFrameTimer.Start();

            // the frame queue is a shared resource with the FrameOfMocap delivery thread, so lock it while reading
            // note this can block the frame delivery thread.  In a production application frame queue management would be optimized.
            lock (syncLock)
            {
                while (m_FrameQueue.Count > 0)
                {
                    m_FrameOfData = m_FrameQueue.Dequeue();

                    if (m_FrameQueue.Count > 0)
                    {
                        continue;
                    }

                    if (m_FrameOfData != null)
                    {
                        // for servers that only use timestamps, not frame numbers, calculate a
                        // frame number from the time delta between frames
                        if (desc.HostApp.Contains("TrackingTools"))
                        {
                            m_fCurrentMocapFrameTimestamp = m_FrameOfData.fLatency;
                            if (m_fCurrentMocapFrameTimestamp == m_fLastFrameTimestamp)
                            {
                                continue;
                            }
                            if (m_fFirstMocapFrameTimestamp == 0.0f)
                            {
                                m_fFirstMocapFrameTimestamp = m_fCurrentMocapFrameTimestamp;
                            }
                            m_FrameOfData.iFrame = (int)((m_fCurrentMocapFrameTimestamp - m_fFirstMocapFrameTimestamp) * m_ServerFramerate);
                        }

                        // update the data grid
                        UpdateDataGrid();

                        // update the chart
                        UpdateChart(m_FrameOfData.iFrame);

                        // only redraw chart when necessary, not for every frame
                        if (m_FrameQueue.Count == 0)
                        {
                            chart1.ChartAreas[0].RecalculateAxesScale();
                            chart1.ChartAreas[0].AxisX.Minimum = 0;
                            chart1.ChartAreas[0].AxisX.Maximum = GraphFrames;
                            chart1.Invalidate();
                        }

                        // Mocap server timestamp (in seconds)
                        m_fLastFrameTimestamp = m_FrameOfData.fTimestamp;
                        TimestampValue.Text   = m_FrameOfData.fTimestamp.ToString("F3");

                        // SMPTE timecode (if timecode generator present)
                        int  hour, minute, second, frame, subframe;
                        bool bSuccess = m_NatNet.DecodeTimecode(m_FrameOfData.Timecode, m_FrameOfData.TimecodeSubframe, out hour, out minute, out second, out frame, out subframe);
                        if (bSuccess)
                        {
                            TimecodeValue.Text = string.Format("{0:D2}:{1:D2}:{2:D2}:{3:D2}.{4:D2}", hour, minute, second, frame, subframe);
                        }

                        if (m_FrameOfData.bRecording)
                        {
                            chart1.BackColor = Color.Red;
                        }
                        else
                        {
                            chart1.BackColor = Color.White;
                        }
                    }
                }
            }

            uiIntraFrameTimer.Stop();
            double uiIntraFrameDuration = uiIntraFrameTimer.Duration();

            m_UIUpdateTimer.Start();
        }