JamesBremner / complot

High performance plotting of spectrum data from COM port
1 stars 0 forks source link

Frame handling #4

Closed JamesBremner closed 8 months ago

JamesBremner commented 8 months ago

Currently, the data arrives from the device simulator as one double per frame.

This issue is for discussing how best to handle larger and more complex frames.

Motivation in https://github.com/JamesBremner/windex/issues/9

Question: Please describe your frames.

JamesBremner commented 8 months ago

I have added a user option to set the frame length. Minimum length is 8 bytes, so as to hold a 64 bit floating point number.

To make this more relevant to your work I need details of your frames. In particular, the offset to the data you want to plot.

JamesBremner commented 8 months ago

Perhaps you have never before had to document a frame design? Here is an example of how I do it:

Field Length bits Description
Header 16 0xF00D
Length 16 frame length in bytes
Blast 8 blast number
ID 8 message type
repeat 4 repeat number, max 0xF.
Packed into hi order bits of third message byte
number 4 packet number, max 0xF.
Packed into lo order bits of third message byte
Data0 8
Data1 8
CRC 8
drmcnelson commented 8 months ago

The frame itself is flat, it is simply

     double frame[NPIXELS].

There is a ring of struct, with points to preallocated frame buffers. Each frame is preceded by a counter and elapsed time.

To be completely, unambiguous, here is the code (work in progress) for the dispatcher. It is intended to run in a thread. I have not tried to run it yet.

 typedef struct {
       unsigned int elapsed;
       unsigned int counter;
       unsigned int shutter;
       unsigned int length;
       unsigned int accumulate;
       size_t nsize;
       double *data;
    } Frame_t;

`Frame_t frames[FRAME_RING_SIZE] = { { 0}}; unsigned int frame_in = FRAME_RING_SIZE_SIZE-1;

uint16_t rawbuffer[8192] = { 0 }; `

 bool mallocFrames(unsigned int npixels)
 {
      for (int n = 0; n < FRAME_RING_SIZE ; n++)
     {
          if (!mallocFrame_( &frames[n], npixels)) {
             return false;
           }
       }
      return true;
  }

`DWORD WINAPI dispatcher( LPVOID lpParam ) {
char pc, pc0; int n = 0; unsigned int npixels, nbytes; unsigned long int nread; double dark = 0.;

Frame_t *fp = &frames[0];
frame_in = 0;

clearFrame_(fp);

while (true) {

    // Text logging ring buffer
    pc = pc0 = textring[textring_index];

    // Read one line from the spectrometer
    if ((n=readLine(pc0,TEXT_RING_BUFSIZ)))
    {
        textring_index = (textring_index + 1)%TEXT_RING;

        // First thing, check for data coming in the next packet
        if ((pc=stringMatch(pc0,"BINARY16"))&&(stringUint(pc,&npixels)))
        {
            nbytes = 2*npixels; // actually, superferlous, it is not going to change
            nread = 0;

            // Read the data packet
            if ( ReadFile(hSerial,rawbuffer,nbytes,&nread, NULL) ) {
                if (nread == nbytes)
                {
                    // We have the packet, check for the end of packet message
                    nread = readLine(buff, sizeof(buff) - 1);
                    if (stringMatch(buff, "END DATA"))
                    {
                        // Scale to volts
                        for (n = 0; n < npixels; n++)
                        {
                            fp->data[n] = rawbuffer[n] * SpectrometerCtl.vperbit;
                        }
                       fp->length = npixels;

                        // Subtract dark level from the masked pixels
                        if (SpectrometerCtl.do_dark && SpectrometerCtl.darkpixels)
                        {
                            dark = avg(fp->data, SpectrometerCtl.darkpixels);
                            for (n = 0; n < SpectrometerCtl.darkpixels; n++)
                            {
                                fp->data[n] -= dark;
                            }
                        }

                        // Enqueue to graphics here

                        // Next pointer for the next data frame
                        frame_in = (frame_in + 1) % FRAME_RING_SIZE;
                        fp = &frames[frame_in];
                    }
                }
            }
        }

        // Frame header values
        else if ((pc=stringMatch(pc0,"ELAPSED"))) {
            stringUint(pc,&(fp->elapsed));
        }
        else if ((pc=stringMatch(pc0,"COUNTER"))) {
            stringUint(pc,&(fp->counter));
        }
        else if ((pc=stringMatch(pc0,"SHUTTER"))) {
            stringUint(pc,&(fp->shutter));
        }

        //  Starts
        else if ((pc=stringMatch(pc0,"START"))) {
            frame_in = 0;
            fp = &frames[0];
            clearFrame_(fp);
        }

        else if ((pc=stringMatch(pc0,"TRIGGERED SINGLES START"))) {
            strcpy( SpectrometerCtl.mode, "TRIGGER" );
            frame_in = 0;
            fp = &frames[0];
            clearFrame_(fp);

        else if ((pc=stringMatch(pc0,"TRIGGERED SETS START"))) {
            strcpy( SpectrometerCtl.mode, "TRIGGERSETS" );
            frame_in = 0;
            fp = &frames[0];
            clearFrame_(fp);
        }

        else if ((pc=stringMatch(pc0,"CLOCKED START"))) {
            strcpy( SpectrometerCtl.mode, "CLOCKED" );
            frame_in = 0;
            fp = &frames[0];
            clearFrame_(fp);
        }

        else if ((pc=stringMatch(pc0,"GATE START"))) {
            strcpy( SpectrometerCtl.mode, "GATE" );
            frame_in = 0;
            fp = &frames[0];
            clearFrame_(fp);
        }

        // Controller settings
        else if ((pc=stringMatch(pc0,"CLOCK"))) {
            stringUint(pc,&(SpectrometerCtl.clock));
        }

        else if ((pc=stringMatch(pc0,"OUTER CLOCK"))) {
            stringUint(pc,&(SpectrometerCtl.outerclock));
        }

        else if ((pc=stringMatch(pc0,"INTERVAL"))) {
            stringUint(pc,&(SpectrometerCtl.interval));
        }

        else if ((pc=stringMatch(pc0,"FRAMES"))) {
            stringUint(pc,&(SpectrometerCtl.frames));
        }

        else if ((pc=stringMatch(pc0,"SETS"))) {
            stringUint(pc,&(SpectrometerCtl.sets));
        }

        else if ((pc=stringMatch(pc0,"EVERY"))) {
            stringUint(pc,&(SpectrometerCtl.every));
        }

        else {
            // Screen for special configuration values

        }
    }
}

}

`

JamesBremner commented 8 months ago

TLDR

Please provide a succinct description of your frame, preferably in a format similar to the one I use, see previous post.

drmcnelson commented 8 months ago

In the above, mallocFrames() is run once, after getting the frame size from the spectrometer. After that everything is reused as is, as you see there.

Normally when I graph a frame, I update a text in a corner of the display to show the counters for the frame and frameset (i.e. for a kinetic series).

In case it is of interest, here is the spectrometer control structure.

  typedef struct {
       char name[64];
       char version[64];
       unsigned int pixels;
       unsigned int darkpixels;
       unsigned int bits;
       double vfs;
       double vperbit;
       double coefficients[8];
       unsigned int ncoeffs;
       double *wavelengths;
       unsigned int frameset;
       unsigned int clock;
       unsigned int outerclock;
       unsigned int interval;
       unsigned int frames;
       unsigned int sets;
       unsigned int every;
       double chiptemperature;
       char mode[32];
      bool do_dark;
  } SpectrometerCtl_t;
drmcnelson commented 8 months ago

Field Length

data npixel * sizof(double)

drmcnelson commented 8 months ago

Field Length Content

data npixel*sizeof(double) voltages, one for each pixel.

JamesBremner commented 8 months ago

OK, let's do Q & A.

How long are your frames? Constant or variable? Do you have a CRC check? What is the header ID? What is the bit offset from start of header to start of data point to be plotted? What is the width and format of the data point?

drmcnelson commented 8 months ago

Frames are npixels. The value npixels is set at startup (by query to the spectrometer) and after that it is constant forever. No, there is no CRC There is no header Bit offset is zero, 0. Each data point is a c language double, whatever that is on a windows PC.

drmcnelson commented 8 months ago

Frame length is npixels.

JamesBremner commented 8 months ago

No, there is no CRC There is no header

What a terrible, terrible design! No error checking at all?

drmcnelson commented 8 months ago

Npixels is usually in the range of 2,000 to 4,000, depending on the sensor.

The instrument we want to provide for the undefunded school to teach science to impoverished kids, is 2048. The one I use in my lab is 3694.

drmcnelson commented 8 months ago

It is not necessary. The data comes over USB, I think there is a crc there, it is not needed anywhere else.

drmcnelson commented 8 months ago

There is a super high end front circuit for the CCD, all differential to a 16bit ADC, whatever error appears there is nature. After that it is shipped over the USB and then it is digital again.

JamesBremner commented 8 months ago

Each data point is a c language double, whatever that is on a windows PC.

Usually 4 bytes or 8 bytes. You will have to let me know which? Alternatively I can make it a user option.

Bit offset is zero, 0.

What does this mean? Does it mean that the 4 or 8 byte data point is the first 4 or 8 bytes of the frame? If so, then the current complot code does what you need!

drmcnelson commented 8 months ago

I.e. digital data again. It is up to the pc to deal with memory errors. If I ever see one, I will be motivated to do something about a crc

drmcnelson commented 8 months ago

I imagine a c++ double on a PC is at least 8 bytes.

Easy to check, just a moment.

drmcnelson commented 8 months ago

Bit offset 0, the data begins at the beggining. No header, no padding, nothing. It is just a double array in C.

drmcnelson commented 8 months ago

But the argument for trace.set() is a std::vector, yes? Did you change it already?

JamesBremner commented 8 months ago

Bit offset 0, the data begins at the beggining. No header, no padding, nothing. It is just a double array in C.

OK, then the current code does what you need. It assumes a 8 byte floating point number. You can specify the total frame length at startup.

drmcnelson commented 8 months ago

Oh, okay, thank you, let me take a look. You updated this, yes?

JamesBremner commented 8 months ago

Here is the code that extracts the data point from the frame

https://github.com/JamesBremner/complot/blob/61c2a110d32ba37eae112e815a9189971586d2bd/src/complotter.cpp#L62-L70

drmcnelson commented 8 months ago

Can you give me a line number? I am looking at the complot/src/complotter.cpp. I think I am missing it It looks like it adds points one at a time.

JamesBremner commented 8 months ago

It looks like it adds points one at a time.

Exactly. One data point per frame.

drmcnelson commented 8 months ago

Oh, but a frame is a few thousand points.

drmcnelson commented 8 months ago

It is a spectrum, from 200 nm to 1100nm, one point every 1/2 or 1/10 nm or so.

drmcnelson commented 8 months ago

Would std::array be a better container for this than vector?

drmcnelson commented 8 months ago

(Sorry, I know where are talking about doubles now. But one point at a time hints that the guts of it are the vector).

JamesBremner commented 8 months ago

It is a spectrum,

But only one point is plotted. You have to choose which frequency you want to plot. This is why I keep asking what the offest is to the point you want to plot.

drmcnelson commented 8 months ago

What frequency? You meant the increment along the x axis? That is a fair point.

When I have written low level graphics (many years ago), I usually create a map from index to screen coordinate, if I am passing y values only. And if I am passing x and y, I create a map from x coordiantes to screen coordinates.

And then there is another cute step where you check that your point is actually a different pixel from the previous point. That saves you a lot of overhead when the data is much denser than the screen

drmcnelson commented 8 months ago

You mean the offset from the screen left side? I though you meant the offset into the data packet.

JamesBremner commented 8 months ago

What frequency? You meant the increment along the x axis? That is a fair point.

No. The x axis is time.

The Y axis is the value for the frequency recorded at that time.

drmcnelson commented 8 months ago

That is not what I am plotting. I am plotting intensity versus wavelength, It is a spectrum.

JamesBremner commented 8 months ago

That is not a real time plot. That is a static plot.

drmcnelson commented 8 months ago

We need an x-y plot.

drmcnelson commented 8 months ago

Okay, it is a static plot.

Do you have that?

JamesBremner commented 8 months ago

How often do you need an update of your spectrum plot? Each time a new packet arrives?

drmcnelson commented 8 months ago

Yes, that is it! Eureka. The fastest is 100 times a second, but I routinely fall behind and catch up through the ring buffer.

JamesBremner commented 8 months ago

Suggestion: A static spectrum plot updated at 1 HZ, with whatever data arrived most recently.

drmcnelson commented 8 months ago

I usuall add buttions forward and back, so that after a burst we can step through the set of spectra.

drmcnelson commented 8 months ago

I would set the update loop at whatever it can bear without bogging down the machine overall. But yes I do that somettimes, graph the most recent and step back after

JamesBremner commented 8 months ago

It is impossible to update a spectrum plot with a significant number of values at 100Hz. No-one could see such a thing and get anything meaningful out of it.

I can do a static spectrum plot updated at 1 HZ, with whatever data arrived most recently ( data arriving at up to 100Hz )

drmcnelson commented 8 months ago

Of course, I agree 100% and more. What you are describing is close to what I usually do.

JamesBremner commented 8 months ago

graph the most recent and step back after

I can log the data as it arrives, but not at 100Hz. Maybe somewhere in range 1 - 10 Hz, with a timestamp on each record, so you can do a replay.

drmcnelson commented 8 months ago

Except I think I set the update at 10/sec.

Since I discovered python, I have been doing it with matplotlib animation, It does 10Hz very nicely. In the days when I progammed this in X, it was not that much faster.

JamesBremner commented 8 months ago

I am going offline for dinner. Chat tomorrow

drmcnelson commented 8 months ago

Yes, of course, absolutely. But, checking a buffer or a pointer for a new graph once per loop would be enough.

drmcnelson commented 8 months ago

Okay, thank you. Same here, I need to cook for myself, stir fry tofu tonight

drmcnelson commented 8 months ago

In the python system, in the animation, I check a queue. If empty, do nothing. Else push everything onto the history list and plot the last item off the queue.

drmcnelson commented 8 months ago

Really, the timer event structure you have already, is almost everything I need.

It needs to be a static plot - which I think we can do easily. And, I need to copy the double *data to a vector - I think I know how to do that too, will try an experiment in an about an hour.

For a general purpose sort of 2d graphics, it would be good to be able to plot x,y, rather than just y with an assumed constant spacing.

Maybe something like: set(vector,vector), and, set(vector), where the first takes both x and y.

Are there calls to set labels and tics and font sizes?

(After it is working I might work on some of the dress up for the graphs. If I do, I am happy to contribute that work.)