Cyton, Wifi Shield, Python (wifi.py): bad raw sample values / but fine using GUI

cmellocmello United States
edited February 2019 in Software
I'm using a Mac (macOS 10.14) with Python 3.6.  I have a Cyton board coupled with the WiFi Shield - 8 channels.  As of the date of this post, I have the latest firmware installed on both the Cyton and the Wifi Shield.  Everything is configured and attached to the Ultracortex "Mark IV" headset using dry electrodes.  With the latest version of OpenBCI GUI (4.0.3) everything works well.  All 8 channels stream as expected.

However, I have a problem with using the wifi.py for streaming with Python (3.6).  First, I want to point out that the square wave test works fine - I'm able to stream and display in realtime using matplotlib.  When I switch over to streaming the raw data, it seems to "work", but it does not seem to contain any real "data"; I'm getting consistent sine waves from all the channels - almost seems like test data.  Also, the values are very small float values, like .0283746734, etc.  I notice that when I jiggle my head (while wearing headset), the waves look more like you would expect, but the "normal" stream for everything is very consistent sine waves.  

See image below: this is a snapshot of what I'm getting.  This image shows streaming at 250hz with a bandpass of 3Hz to 50Htz applied.  Keep in mind that if I switch over to using the GUI software, all looks good.  I can only assume that I'm doing something incorrectly.  Ideas?  Am I supposed to be taking voltage offset and gain into account?  The way the python code is written in wifi.py and parese.py, I would say no.  By the way (not that it made much of a difference), I fiddled with the parse.py and turned micro_volts = Ture, and scaled_output =False in the ParseRaw class; those values are usually the opposite of what comes out of the box.  Final note: I'm using the latest python source code as supplied in the Git repository.
 
What gives?


image

Comments

  • cmellocmello United States
    In case it's helpful, here are a couple of simple routines I'm using to initiate and handle wifi streaming.  Note that wifi_init() is called on a background thread, and I'm not concerned with skipped samples at the moment - it works well for the most part:

    def wifi_init():

    global wifi
    global file

    file =open(file_name,'wb')

    #wifi = OpenBCICyton()

    wifi= OpenBCIWiFi(ip_address=shield_ip,
    shield_name=shield_name,
    sample_rate=sample_rate,
    log=shield_logging_on,
    timeout=15,
    max_packets_to_skip=10,
    latency=5000,
    high_speed=shield_high_speed,
    ssdp_attempts=20,
    num_channels=num_channels)

    time.sleep(2)
    wifi.connect()
    time.sleep(2)

    wifi.start_streaming(handle_streamed_data)

    wifi.loop()
    def handle_streamed_data(sample):
    try:
    global num_misses

    if len(sample.channel_data)==0:
    sample.channel_data = list(np.zeros(8))

    # insert timestap so we can write it with the data values
    sample.channel_data.insert(0,float(sample.timestamp))

    # Create a buffer to hold bytes for 9 doubles, little-endian.
    # Note: this will FORCE doubles to be written using
    # 8 bytes each - no matter how they're stored
    # in memory on any given platform.
    frmt = '<%sd' % len(sample.channel_data)

    buf2 = struct.pack(frmt, *sample.channel_data)

    # write buffer out to storage.
    file.write(buf2)

    except Exception as e:
    print("ERROR: ", str(e))
  • wjcroftwjcroft Mount Shasta, CA
    CMello, hi.

    Can you open an issue for this at,


    And use the edited thread title that I adjusted for you? Point back to this thread in your issue post.

    Thanks,

    William

  • cmellocmello United States
    Done.  Thank you!
  • I noticed it says # Create a buffer to hold bytes for 9 doubles, little-endian
    But the OpenBCI boards transmit their data in a bigendian format--so make sure that your build included the big to little endian byte swapping code that is already in the Python libraries.

  • cmellocmello United States
    edited February 2019
    This is not a serious piece of code yet.  I'm just writing to a binary file irrespective of where the data comes, and just chose little-endian for the time being.  Anyone wanting to read this file would simply have to know this.  Ultimately, this file will not only be used in Python; it will be used on potentially many platforms with any language. 
  • If the raw data comes in in littleendian format it will need to be converted before it will display properly.
  • wjcroftwjcroft Mount Shasta, CA
    Billh, yes I was wondering about the same thing. Unless Cmello's 'matplotlib' is READING in little endian format.

  • Exactly. I seem to recall the Java code swapped the lsb and msb of the 24-bit values within the input loop while processing the incoming packet . On the other hand if his code is treating network-order data as if it were littleendian that might explain his viewing problem above.
  • cmellocmello United States
    I suppose I could be doing something stupid, but I doubt reading the file in incorrectly will produce nearly perfect graphs like I show on the screenshot.  I read the data into memory the same way I write it out, little-endian.  See code below, and let me know if you see anything that might be amiss. Multiple eyes are better than (my) two.  This is the code I use to populate/animate the matplotlib graphs - it happens every 20ms, while I'm streaming data in on a different thread:
    def animate_stream_plot(i):
    try:

    # Get window of latest samples
    val = get_latest_samples()

    if (not val is None) and len(val)>0:
    n=1

    # Set data for each channel
    # into respective graph
    for line in lines:
    x = np.arange(0, len(val))
    y = val[0:,n]

    # Reverse samples for displaying
    # in plot from left to right
    y = y[::-1]
    line.set_data(x, y)
    n+=1

    return lines
    except Exception as e:
    print("ERROR: ", str(e))
    def get_latest_samples(nsamples = win_size, fname=file_name):

    # Flush current stream buffer to storage
    file.flush()

    fsize = os.stat(fname).st_size

    if fsize==0:
    return None

    # Seek to the latest data from the end of the file
    rec_seek = fsize - (rec_size * nsamples)

    if rec_seek < 0:
    rec_seek = 0

    # Read whatever number of records
    # are currently in the file.
    recs_to_read = int(fsize / rec_size)
    else:
    # Read the max number of records
    # from the end of the file.
    recs_to_read = win_size

    # open sample file for binary read
    file_read = open(file_name, mode="rb")

    file_read.seek(rec_seek)

    # read data into memory
    frmt = '<%sd' % 9 # read 9 (eight-byte) doubles, little endian
    val = np.fromfile(file_read, frmt, count=recs_to_read)

    file_read.close()

    val = edf.butter_bandpass_filter(val,.5,40,sample_rate)
    return val
  • cmellocmello United States
    Here is some additional info (const):
    num_channels=8
    rec_size = (num_channels * 9) #9 (8-byte) doubles. 1: timestamp, 2-9: one sample for each channel
    win_seconds = 10
    sample_rate = 250

    dc_offset = 4.5
    gain = 24

    #sample+(offset*gain)
    win_size = sample_rate * win_seconds #number of samples to display/read at once
  • edited February 2019
    The data as originally written is, I think, in the form of 4 byte signed binary integer. You are converting the data to floats before graphing it. Let's assume your version of wifi.py does the big to little endian stuff right (I believe it does).

    I still think there is something going on with how the the 32-bit integer data is being written to file and then, in particular, how it is then read into the double floats you graph. Perhaps the read window for the 4 bytes read for each double is not aligned with the data. What is rec_size above?

  • edited February 2019
    Maybe you need to convert sample.channel_data to doubles before writing it the first time? or write it and read it as integer not double to file? It depends on the type of channel_data in your code.

  • cmellocmello United States
    The data is coming over as floats from wifi.py.  As I wrote in the original post, the data samples, as they come in, are in the form of .02388495, .00485866, etc.  I think we might be chasing rabbits.  I'll graph the data as it comes in, without saving or loading, so as to avoid focusing on that.  To me, no matter how I save it, so long as I load it the same way, it should not matter.  But... who knows?  I'll change the code and post my findings.  At this point, I'm willing to entertain anything. 
  • cmellocmello United States
    @Billh: So, I experimented in several ways - one way is just dumping incoming data into a queue, and then displaying it. The other way is going from little endian to big endian.  None of these made a difference.  This is because it doesn't matter how data is stored, so long as I load it into memory properly.  You can load binary data in from any file so long as you follow the convention the file was created with...

    Having said that, this might be some other issue.  I went back into OpenBCI GUI and turned off all filters (this includes turning off the notch filter).  Now, I'm seeing the same patter there that I do in my own UI (see images below).  Now, I don't know what to think... I could swear I confirmed all of this before, but.... who knows.  I'm too flustered to think straight at the moment.

    image

    image
  • wjcroftwjcroft Mount Shasta, CA
    So the numbers coming in on the stream from the shield must be in Volts, not Microvolts? GUI CSV output file is in microvolts.

    The other filter that the GUI applies is a high pass at about .5 Hz or .1 Hz, this removes the DC offset. So at the least you will want to apply this highpass and the mains notch.

  • cmellocmello United States
    wjcroft, you may be right.  Values coming in from wifi.py are very small numbers - probably volts.  I went into the parser.py code and turned "micro-volts" on in the raw parser's constructor.  wifi.py does not allow you to do this directly, so by default micro-volts is set to False... I assume this means everything coming in is in volts.  Makes sense now that you mention it.  I'm still unsure if any of this changes the overall pattern I'm getting... just sine waves with no real data in it that I can see.  After setting everything back to "normal" in the GUI, 60hz notch and some sort of filter selected (does not really matter which), the GUI does seem to have waves consistent with EEG data.  On the other hand, wifi.py stuff does not... 

    My next course of action is to just write everything directly to a file WITHOUT attempting to display it in realtime.  After I create the file, I will then select portions of it to display in a still plot.  This should make my test far simpler; reducing any inadvertent stupidity on my part. Even though I see no evidence that I'm messing things up, I don't trust myself now.
  • wjcroftwjcroft Mount Shasta, CA
    "just sine waves with no real data in it that I can see"

    What you are seeing is the mains noise, which is huge compared to the EEG microvolts (which at most are around 80 microvolts). The DC offset can be in the tens of millivolts range. You have to do those previously mentioned filters (notch and highpass) to get actual EEG. An alternative some use is just a bandpass from say .5 Hz to 40 Hz. There are various Python DSP libs available,

  • wjcroftwjcroft Mount Shasta, CA
    If you actually get your expected EEG, please close or delete the opened issue,

  • cmellocmello United States
    Ah!  That could explain why I see similar-looking waves in the GUI when I turn off all filters...  I must be applying my filters incorrectly in python, because I tried applying a bandpass before.  Let me test this theory a bit more.  If this is the issue, I'll be sure to close out the actual issue #117.
  • edited February 2019

    In the docs:

    it says: 

    Overview

    The startStreaming function of the Board object takes a callback function and begins streaming data from the board. Each packet it receives is then parsed as an OpenBCISample which is passed to the callback function as an argument.

    OpenBCISample members:

    -id: int from 0-255. Used to tell if packets were skipped.

    -channel_data: 8 int array with current voltage value of each channel (1-8)

    -aux_data: 3 int array with current auxiliary data. (0s by default)


    So if your channel_data is as you say,  float not int, you must be running a different version of the code than is documented on this site. Sorry for my confusion :).
  • cmellocmello United States
    Yeah, I saw that, too; it's wrong.  I have the latest wifi.py from the git repository, and it comes through as floats.  It's been rather frustrating... outdated documentation, NO documentation in some cases, no good examples to go by (at least regarding python and wifi.py), etc...  I cobbled together what I could, and now it's a matter of tuning and doing some sanity checks.  I appreciate that you've been willing to help, though.  ANY ideas can be a big help.  Thanks to both you and wjcroft.  I have not had time to try out the latest ides (that maybe I'm applying filters incorrectly).  I will know this in another day or so.
  • edited March 2019
    One last comment thing:  after looking at the Github repository, it remains true that the packet is integer in the github code. However, Python itself does not care that much about the type of the data, and should convert to doubles on the fly at the time the data file is first written. 

    So the problem is not in the file creation after all.

     It may just be a matter of working on the plotting and filtering parameters to get the filters and axis proportions right. 
  • cmellocmello United States
    So, it looks like wifi.py is only half-baked, after all this time I thought I was doing something wrong.  It was never fully tested, nor was it completed - and I think whoever left it is such a state should apologize - not for being half-assed (although you were), but for not CLEARLY STATING that it wasn't finished, and that it DOES NOT WORK as-is.  There is also a bug with scaling.

    With that said, I will finish it... because I have too much vested into it at this stage.  More to come...
  • cmellocmello United States
    Ok.  Got things working.  I'll follow up in a day or so, submit new code to repository, and close out issue 117.  Two issues with wifi.py and one major problem on my side.  All are fixed.
  • cmellocmello United States
    Changes that will soon be available in the git repository:

    (1) Scale factor bug fix according to https://docs.openbci.com/Hardware/03-Cyton_Data_Format#cyton-data-format-binary-format
    (2) OpenBCIWiFi now applies the sample rate passed into the constructor
    (3) New option to prevent OpenBCIWiFi from automatically connecting to board upon instantiation
    (4) scaled_output and micro_volts can now be set in OpenBCIWiFi constructor
    (5) prevent connection exception during OpenBCIWiFi instantiation by placing small time delay between shield discovery and connecting to the board
    (6) New test to demonstrate how these changes/new options work
  • cmellocmello United States
    A pull request was made; however, to get at fixes now, you can go to my fork: https://github.com/cmello418/OpenBCI_Python
Sign In or Register to comment.