Kontinuierlich Spektrum messen und anzeigen



  • Hallo,

    ich beschäftige mich seit kurzem mit C++/Cli in VS2008 um ein Messprogramm im Rahmen meiner Masterarbeit (Physik) zu schreiben. Seitdem habe ich verstanden, dass C# die bessere Wahl gewesen wäre und habe auch vor, wenn das Programm fertig ist, darauf umzusteigen. Für den Moment ist das aber nicht mehr möglich, da das Programm zu weit forgeschritten ist und ich nicht mehr unbegrenzt Zeit habe. Deswegen brauche ich erstmal hier Hilfe:

    Ich versuche ein Messprogramm zu schreiben, was unter anderem auch ein Spektrometer auslesen soll (OceanOptics USB2000+). Das klappt bisher auch wunderbar. Spektrometer laden, per Knopfdruck Spektrum messen und im Windows.Chart anzeigen ist kein Problem. Jetzt hätte ich aber gerne, dass das Spektrum mit einer vorgegebenen Rate (also z.B. 10 mal pro Sekunden) aufgenommen und jeweils im Chart geupdatet wird.
    Zu dem Zweck habe ich mich mit Multithreading mit der .NET Klasse Thread beschäftigt und mein Ziel auch teilweise erreicht. Ich kann eine Endlosschleife per Knopfdruck starten, woraufhin das Spektrum gemessen wird, dann angezeigt wird, wieder gmessen und so weiter. Durch einen erneuten Knopfdruck wird der Vorgang beendet. In einem eigenen Thread soll das Ganze aufgeführt werden, damit die anderen Funktionen des Programms weiterhin benutzbar sind und natürlich die Funktionalität der Knöpfe gewährleistet bleibt.

    Das Problem was ich nun habe ist folgendes: Das Spektrum soll ca. 10mal pro Sekunde geupdatet werden. Schneller geht nicht, da schon die Integrationszeit des Spektrometers knapp unter 100ms liegt. So wie ich es bis jetzt programmiert habe, geht es aber nicht schneller als ca. 3mal pro Sekunde. Sprich: Es ist zu langsam. Hier erstmal der relevante Code:

    //Continuous Spectrum
    void FROSCH::Form1::button31_Click(System::Object^  sender, System::EventArgs^  e){
    
    	tabPage3->Enabled=false;
    	tabPage5->Enabled=false;
    	button7->Enabled=false;
    	button25->Enabled=false;
    	button31->Enabled=false;
    
    	try{
    	testBox->Text="Measurement Started";
    
    	ContSpecThread= gcnew Thread(gcnew ThreadStart(this, &Form1::ThreadProcContSpec));
    	ContSpecThread->IsBackground=true;
    	ContSpecThread->Start();
    	}
    
    	catch(...){
    		testBox->Text="Error: Measurement Aborted";
    		return;
    	}
    }
    
    //Funktion, die als thread gestartet wird
    void FROSCH::Form1::ThreadProcContSpec(){
    
    	while(true){
    
    		Monitor::Enter(c);
    
    		if(c->stopped){
    			c->stopped=false;
    			Monitor::Exit(c);
    			ContSpecThread->Join();
    			break;
    		}
    
    		measureSpectrum(valuesSpectrum,processor,spectrum);	
    
    		Monitor::Exit(c);
    
    		ContSpecAddXY("Series1", valuesWavelength, valuesSpectrum);
    
    	}
    }
    
    //Im thread auf Steuerelement chart1 durch Invoke zugreifen
    void FROSCH::Form1::ContSpecAddXY(String^ series, array<double>^ X, array<double>^ Y){
    
    	if(this->chart1->InvokeRequired)
            {
    
    	ContSpecAddXYDelegate^ d = gcnew ContSpecAddXYDelegate(this, &Form1::ContSpecAddXY);
            this->BeginInvoke(d, gcnew array<Object^> { series, X, Y });
    
            }
            else
            {
    		this->chart1->Series[series]->Points->Clear();
    		//int tempLength=valuesWavelength->Length;
    		for(int i=0; i<PIXELS; i++)
    		{		
    				this->chart1->Series[series]->Points->AddXY(X[i], Y[i]);
    				this->chart1->Invalidate();
    			}
            }
    }
    

    Das thread Objekt, den Delegaten usw. habe ich in Form1.h initialisiert. ContSpecThread ist der thread in dem das Ganze ausgeführt wird. ThreadProcContSpec ist die Funktion, die in diesem ausgeführt wird. measureSpectrum misst das Spektrum, welches in valuesSpectrum gespeichert wird mit valuesWavelength als entsprechender Wellenlänge. processor und spectrum sind Objecte, die das Spektrometer betreffen. Über die Funktion ContSpecAddXY wird auf das Steuerelement chart1 zugegriffen. BeginInvoke wird benutzt um den Thread zu wechseln.
    Ich habe bereits verschiedene Stopwatches gesetzt, um auszumachen, an welcher Stelle der Code so langsam ist, bisher aber ohne Erfolg. Ich verute, dass es etwas mit dem Invoke zu tun hat. Über eure Hilfe würde ich mich sehr freuen.

    Beste Grüße



  • EffEm schrieb:

    Zu dem Zweck habe ich mich mit Multithreading mit der .NET Klasse Thread beschäftigt und mein Ziel auch teilweise erreicht.

    Was die Verwendung des Monitors in diesem Code angeht, hast du das Ziel in der Tat nicht erreicht. So wie der da verwendet wird, schützt der nichts. Hinter dem Monitor.Exit wird die Funktion aufgerufen, die die Daten an die Oberfläche weitereicht. Während der GUI-Thread die Daten liest, könnte der Thread die wieder beschreiben.

    Dann fällt mir noch dieses ständige Erzeugen des Delegates auf, kann man den nicht woanders anlegen und wiederverwenden ?

    Das sich die Funktion über BeginInvoke selber noch mal aufruft, habe ich so in C# noch nicht gesehen, sollte aber wohl nicht viel stören.

    Stören tut mich auf jeden Fall der Aufruf von Invalidate(), der bewirkt doch, dass sich das Chart nach jedem Pixel neu zeichnet, oder ? Würde mal vermuten, dass es deshalb so langsam wird. Was passiert, wenn man das hinter die for-Schleife setzt ?

    [Nachtrag]

    Als das Chart für VS2008 nachgereicht wurde gab es ein umfangreiches Beispielprojekt dazu. Habs mir gerade noch mal angesehen, die beiden Beispiele unter Working with Data -> RealtimeData haben keine Probleme mit schnellen Aktualisierungen.



  • Danke schonmal für deine Antwort!

    Den Monitor hatte ich ursprünglich angelegt, um c->stopped zu schützen, was ja meine Abbruchsbedingung ist, die von beiden Threads benutzt wird. c ist nur eine Instanz eines ref struct mit einer member variablen stopped, die angibt ob der loop weiter ausgeführt werden soll oder nicht. Wenn ich jetzt den Monitor.Exit hinter mein ContSpecAddXY setze schützt der dann auch den chart im GUI Thread? Immerhin bezieht sich der Monitor ja nur auf c.

    Das Invalidate war in der Tat versehentlich dämlich platziert, macht aber keinen Unterschied das war sowieso nur der nachträgliche Versuch etwas zu ändern.

    Das Erzeugen des Delegaten hatte ich zunächst auch auf dem Zettel, konnte aber per stopwatch nicht als Übeltäter identifiziert werden und wirds somit wohl nicht sein.

    Das mit dem Beispiel ist sehr interessant, wo genau finde ich das? Habs im VC Ordner und durch Googlen nicht finden können. So ein Beispiel hab ich bisher vergeblich gesucht.

    Besten Dank, EffEm



  • EffEm schrieb:

    Wenn ich jetzt den Monitor.Exit hinter mein ContSpecAddXY setze schützt der dann auch den chart im GUI Thread? Immerhin bezieht sich der Monitor ja nur auf c.

    Aua.

    Oh je, es ist also wirklich so schlimm. Das hast du völlig falsch verstanden. Der Monitor schützt nicht das c, sondern das dient dazu verschiedene Sperren zu unterscheiden.

    Stell dir Monitor.Enter(c) als eine Schranke vor, durch die nur ein Thread hindurchkommt. Trifft ein anderer Thread auf ein anderes (oder das selbe) Monitor.Enter(c) (mit der selben Objektinstanz, auf die c verweist), dann kommt der da erst durch, wenn der andere am Exit vorbei ist.

    In C# benutzt man oft ein Dummy Objekt vom Typ Object dafür, so wie hier. lock(){} ist nur die Verkürzte C# Schreibweise für das Monitor Enter/Exit Paar.

    // statements_lock2.cs
    using System;
    using System.Threading;
    
    class Account
    {
        private Object thisLock = new Object();
        int balance;
    
        Random r = new Random();
    
        public Account(int initial)
        {
            balance = initial;
        }
    
        int Withdraw(int amount)
        {
    
            // This condition will never be true unless the lock statement
            // is commented out:
            if (balance < 0)
            {
                throw new Exception("Negative Balance");
            }
    
            // Comment out the next line to see the effect of leaving out 
            // the lock keyword:
            lock(thisLock)
            {
                if (balance >= amount)
                {
                    Console.WriteLine("Balance before Withdrawal :  " + balance);
                    Console.WriteLine("Amount to Withdraw        : -" + amount);
                    balance = balance - amount;
                    Console.WriteLine("Balance after Withdrawal  :  " + balance);
                    return amount;
                }
                else
                {
                    return 0; // transaction rejected
                }
            }
        }
    
        public void DoTransactions()
        {
            for (int i = 0; i < 100; i++)
            {
                Withdraw(r.Next(1, 100));
            }
        }
    }
    
    class Test
    {
        static void Main()
        {
            Thread[] threads = new Thread[10];
            Account acc = new Account(1000);
            for (int i = 0; i < 10; i++)
            {
                Thread t = new Thread(new ThreadStart(acc.DoTransactions));
                threads[i] = t;
            }
            for (int i = 0; i < 10; i++)
            {
                threads[i].Start();
            }
        }
    }
    

    [Nachtrag]
    Man sieht dem Code nicht an, ob da immer die gleichen Arrays vom Thread an die Oberfläche gegeben werden, dann müssten die vor gleichzeitigem Zugriff geschützt werden.
    Oder es werden immer neue Arrays produziert, was einmal ständig Prozessorlast durch das Erzeugen der Arrays und das Abräumen durch den Garbage Collector erzeugt. Viel schwerwiegender ist aber dann die Verwendung von BeginInvoke. Wenn da im Thread wirklich 10 Datensätze pro Sekunde erzeugt werden, aber nur 3 von der Oberfläche abgenommen werden, pumpt das Programm ziemlich schnell den Speicher voll.
    [/Nachtrag]

    Wahrscheinlich ist das der Grund, wo dein Programm hängt, irgendwas falsch verwendetes aus dem Bereich Threading, Invoke oder Synchronisatzion.

    EffEm schrieb:

    Das mit dem Beispiel ist sehr interessant, wo genau finde ich das?

    Gute Frage, das Chart Control ist erst seit Visual Studio 2010 fest dabei. Für das 2008 SP1 war das ein separater Download. Genauer gesagt mehrere, ein MSChart.exe, ein deutsches Language Pack, eine Hilfedatei und zwei zip-Dateien namens WinSamples und WebSamples.

    Allein WinSamples enthält laut Überschrift 200 Codebeispiele.

    Hier zumindest die Codeschnipsel zu den beiden zitierten Beispielen

    using System.Windows.Forms.DataVisualization.Charting;
    ...
    private Thread addDataRunner;
    private Random rand = new Random();
    private System.Windows.Forms.DataVisualization.Charting.Chart chart1;
    public delegate void AddDataDelegate();
    public AddDataDelegate addDataDel;
    ...
    
    private void RealTimeSample_Load(object sender, System.EventArgs e)
    {
    
        // create the Adding Data Thread but do not start until start button clicked
        ThreadStart addDataThreadStart = new ThreadStart(AddDataThreadLoop);
        addDataRunner = new Thread(addDataThreadStart);
    
        // create a delegate for adding data
        addDataDel += new AddDataDelegate(AddData);
    
    }
    
    private void startTrending_Click(object sender, System.EventArgs e)
    {
        // Disable all controls on the form
        startTrending.Enabled = false;
        // and only Enable the Stop button
        stopTrending.Enabled = true;
    
        // Predefine the viewing area of the chart
        minValue = DateTime.Now;
        maxValue = minValue.AddSeconds(120);
    
        chart1.ChartAreas[0].AxisX.Minimum = minValue.ToOADate();
        chart1.ChartAreas[0].AxisX.Maximum = maxValue.ToOADate();
    
        // Reset number of series in the chart.
        chart1.Series.Clear();
    
        // create a line chart series
        Series newSeries = new Series( "Series1" );
        newSeries.ChartType = SeriesChartType.Line;
        newSeries.BorderWidth = 2;
        newSeries.Color = Color.OrangeRed;
        newSeries.XValueType = ChartValueType.DateTime;
        chart1.Series.Add( newSeries );    
    
        // start worker threads.
        if ( addDataRunner.IsAlive == true )
        {
            addDataRunner.Resume();
        }
        else
        {
            addDataRunner.Start();
        }
    
    }
    
    private void stopTrending_Click(object sender, System.EventArgs e)
    {
        if ( addDataRunner.IsAlive == true )
        {
            addDataRunner.Suspend();
        }
    
        // Enable all controls on the form
        startTrending.Enabled = true;
        // and only Disable the Stop button
        stopTrending.Enabled = false;
    }
    
    /// Main loop for the thread that adds data to the chart.
    /// The main purpose of this function is to Invoke AddData
    /// function every 1000ms (1 second).
    private void AddDataThreadLoop()
    {
        while (true)
        {
            chart1.Invoke(addDataDel);
    
            Thread.Sleep(1000);
        }
    }
    
    public void AddData()
    {
        DateTime timeStamp = DateTime.Now;
    
        foreach ( Series ptSeries in chart1.Series )
        {
            AddNewPoint( timeStamp, ptSeries );
        }
    }
    
    /// The AddNewPoint function is called for each series in the chart when
    /// new points need to be added.  The new point will be placed at specified
    /// X axis (Date/Time) position with a Y value in a range +/- 1 from the previous
    /// data point's Y value, and not smaller than zero.
    public void AddNewPoint( DateTime timeStamp, System.Windows.Forms.DataVisualization.Charting.Series ptSeries )
    {
        double newVal = 0;
    
        if ( ptSeries.Points.Count > 0 )
        {
            newVal = ptSeries.Points[ptSeries.Points.Count -1 ].YValues[0] + (( rand.NextDouble() * 2 ) - 1 );
        }
    
        if ( newVal < 0 )
            newVal = 0;
    
        // Add new data point to its series.
        ptSeries.Points.AddXY( timeStamp.ToOADate(), rand.Next(10, 20));
    
        // remove all points from the source series older than 1.5 minutes.
        double removeBefore = timeStamp.AddSeconds( (double)(90) * ( -1 )).ToOADate();
        //remove oldest values to maintain a constant number of data points
        while ( ptSeries.Points[0].XValue < removeBefore )
        {
            ptSeries.Points.RemoveAt(0);
        }
    
        chart1.ChartAreas[0].AxisX.Minimum = ptSeries.Points[0].XValue;
        chart1.ChartAreas[0].AxisX.Maximum = DateTime.FromOADate(ptSeries.Points[0].XValue).AddMinutes(2).ToOADate();
    
        chart1.Invalidate();
    }
    
    /// Clean up any resources being used.
    protected override void Dispose( bool disposing )
    {
        if ( (addDataRunner.ThreadState & ThreadState.Suspended) == ThreadState.Suspended)
        {
            addDataRunner.Resume();
        }
        addDataRunner.Abort();
    
        if( disposing )
        {
            if (components != null) 
            {
                components.Dispose();
            }
        }
        base.Dispose( disposing );
    }        
    ...
    
    using System.Windows.Forms.DataVisualization.Charting;
    ...
    
    private Random    random = new Random();
    private    int        pointIndex = 0;
    ...
    
    private void timerRealTimeData_Tick(object sender, System.EventArgs e)
    {
        // Define some variables
        int numberOfPointsInChart = 200;
        int numberOfPointsAfterRemoval = 150;
    
        // Simulate adding new data points
        int numberOfPointsAddedMin = 5;
        int numberOfPointsAddedMax = 10;
        for(int pointNumber = 0; pointNumber < random.Next(numberOfPointsAddedMin, numberOfPointsAddedMax); pointNumber++)
        {
            chart1.Series[0].Points.AddXY(pointIndex + 1, random.Next(1000, 5000));
            ++pointIndex;
        }
    
        // Adjust Y & X axis scale
        chart1.ResetAutoValues();
    
        // Keep a constant number of points by removing them from the left
        while(chart1.Series[0].Points.Count > numberOfPointsInChart)
        {
            // Remove data points on the left side
            while(chart1.Series[0].Points.Count > numberOfPointsAfterRemoval)
            {
                chart1.Series[0].Points.RemoveAt(0);
            }
    
            // Adjust X axis scale
            chart1.ChartAreas["Default"].AxisX.Minimum = pointIndex - numberOfPointsAfterRemoval;
            chart1.ChartAreas["Default"].AxisX.Maximum = chart1.ChartAreas["Default"].AxisX.Minimum + numberOfPointsInChart;
        }
    
        // Invalidate chart
        chart1.Invalidate();
    }
    ...
    

    Das zweite Beispiel, dass einen Timer statt Invoke verwendet, wäre der Weg, den ich für deine Anwendung auch vorgeschlagen hätte. Sprich ein Workerthread sammelt in einer Schleife Daten und legt sie, threadsicher, irgendwo ab, alte Daten überschreibt er dabei. In der Oberfläche läuft ein Timer, der holt die Daten, threadsicher, ab.

    Wo und ob das heute auf MSDN steht, weiß ich leider nicht.
    [Edit]
    Da
    http://archive.msdn.microsoft.com/mschart


Anmelden zum Antworten