-1

I have this continuous serial data stream:

----------------------------------------
 
SENSOR COORDINATE         = 0
 
MEASURED RESISTANCE       = 3.70 kOhm
 
----------------------------------------
 
----------------------------------------
 
SENSOR COORDINATE         = 1
 
MEASURED RESISTANCE       = 3.70 kOhm
 
----------------------------------------
 
----------------------------------------
 
SENSOR COORDINATE         = 2
 
MEASURED RESISTANCE       = 3.69 kOhm
 
----------------------------------------

For each iteration, i want to be able to grab the values. The sensor coordinate value, and the resistance value.

I found solutions using .split() and with using regular expressions ( Find string between two substrings), but the problem is that in my case, there is not one string that i want to filter, but a continuous stream.

For example, .split() will find my string, but it will split the stream in half. This does not work, in a continuous stream, for more than one time.

NOTE: After the sensor coordinate value, i have a carriage return character.

EDIT 1/3: This is the snippet of code that grabs the serial data:

def readSerial():
    global after_id
    while ser.in_waiting:
        try:
            ser_bytes = ser.readline() #read data from the serial line
            ser_bytes = ser_bytes.decode("utf-8")
            text.insert("end", ser_bytes)
        except UnicodeDecodeError:
            print("UnicodeDecodeError")
    else:
        print("No data received")
    after_id=root.after(50,readSerial)

And if someone wants, to know, this is the C code on the arduino side, that sends the data:

Serial.println("----------------------------------------");
Serial.print("SENSOR COORDINATE         = ");
Serial.println(sensor_coord);
Serial.print("MEASURED RESISTANCE       = ");
double resistanse = ((period * GAIN_VALUE * 1000) / (4 * CAPACITOR_VALUE)) - R_BIAS_VALUE;
Serial.print(resistanse);
Serial.println(" kOhm");

EDIT 2/3: This is a previous approach:

def readSerial():
        global after_id
        while ser.in_waiting:
            try:
                ser_bytes = ser.readline() #read data from the serial line
                ser_bytes = ser_bytes.decode("utf-8")
                text.insert("end", ser_bytes)
                result = re.search.(, ser_bytes)
                print(result)
            except UnicodeDecodeError:
                print("UnicodeDecodeError")
        else:
            print("No data received")
        after_id=root.after(50,readSerial)

And in another attempt, i changed this line result = re.search.(, ser_bytes) to result =ser_bytes.split("TE = ").

This is a picture of the data i receive (this is a tkinter text frame). enter image description here

EDIT 3/3: This is my code implementing dracarys algorithm:

def readSerial():
    global after_id
    while ser.in_waiting:
        try:
            ser_bytes = ser.readline() 
            print(ser_bytes)
            ser_bytes = ser_bytes.decode("utf-8")
            print(ser_bytes)
            text.insert("end", ser_bytes)
           
            if "SENSOR COORDINATE" in ser_bytes:
               found_coordinate = True
               coordinate = int(ser_bytes.split("=")[1].strip())
               print("Coordinate",coordinate)
            if "MEASURED RESISTANCE" in ser_bytes and found_coordinate:
               found_coordinate = False
               resistance = float(ser_bytes.split("=")[1].split("kOhm")[0].strip())
               print("Resistance",resistance)
        
        except UnicodeDecodeError:
            print("UnicodeDecodeError")
    else:
        print("No data received")
    after_id=root.after(50,readSerial)

This is the error i get, after the code runs for about ten seconds succesfully (i have included normal operation output as well for reference):

No data received
b'SENSOR COORDINATE         = 2\r\n'
SENSOR COORDINATE         = 2

Coordinate 2
b'MEASURED RESISTANCE       = 3.67 kOhm\r\n'
MEASURED RESISTANCE       = 3.67 kOhm

Resistance 3.67
b'----------------------------------------\r\n'
----------------------------------------

b'----------------------------------------\r\n'
----------------------------------------

b'SENSOR COORDINATE         = 3\r\n'
SENSOR COORDINATE         = 3

Coordinate 3
No data received
b'MEASURED RESISTANCE       = 3.78 kOhm\r\n'
MEASURED RESISTANCE       = 3.78 kOhm

Exception in Tkinter callback
Traceback (most recent call last):
  File "C:\Users\User1\AppData\Local\Programs\Python\Python38-32\lib\tkinter\__i
nit__.py", line 1883, in __call__
    return self.func(*args)
  File "C:\Users\User1\AppData\Local\Programs\Python\Python38-32\lib\tkinter\__i
nit__.py", line 804, in callit
    func(*args)
  File "tkinterWithPortsExperiment.py", line 73, in readSerial
    if "MEASURED RESISTANCE" in ser_bytes and found_coordinate:
UnboundLocalError: local variable 'found_coordinate' referenced before assignment
user1584421
  • 3,499
  • 11
  • 46
  • 86
  • 2
    Can you share what you have tried? With this limited information I would suggest to implement a ring buffer of some sort (e.g. with deques) and perform the processing on the buffer contents. – DocDriven May 08 '21 at 23:34
  • shouldn't you implement some state machine and process line by line? – Dyno Fu May 09 '21 at 00:55
  • 5
    how does your stream come into your program? reading a log file? `stdin`? socket? – ti7 May 10 '21 at 23:14
  • 1
    A hint to get you started: You don't need "split" at all. Since all you get at a time is a line, parse each line and interpret the result. If you found the sensor coordinate, keep your eye out for a line that gives measured distance. Once you have both, record them and keep an eye out for a new sensor coordinate. – Pranav Hosangadi May 11 '21 at 00:04
  • @PranavHosangadi I had exactly that algorithm in my mind. The problem is, i don't know how to parse and interpret the data. But thank you. I don't expect people to write code for me. But your hint to dump split() is excellent. Can you suggest a function best suited for what i want? – user1584421 May 11 '21 at 01:06
  • @user1584421 Just a suggestion, if you are using python to parse your arduino data then why do you need to print ```"____________"``` and spaces in the serial monitor. Is'nt that useful just for readability. – dracarys May 11 '21 at 06:47
  • 1
    If you are the maintainer of the arduino side as well, it might be better to write a one line JSON for each sensor event, that then can be easily processed line by line. – oliver_t May 11 '21 at 07:02
  • How is `text` defined in your snippet? – Booboo May 11 '21 at 10:45
  • @Booboo the text is the tkinter area where the data is printed. It does not interefere at all. `text = tk.Text(text_frame)` – user1584421 May 11 '21 at 14:48
  • 1
    You probably have downvotes because you didn't include your attempt to solve the problem in your question. As it stands, it looks awfully like a "code this for me, I'll give you 100 worthless internet points" question. You should [edit] your attempt into your question and ask about a specific problem you had with it. @user1584421 – Pranav Hosangadi May 11 '21 at 14:54
  • @PranavHosangadi There was a second answer here, that just dissapeared. Was it yours? – user1584421 May 11 '21 at 15:01
  • Yes. I will edit it to address the problems with your attempt once you add that to your question, and then make it visible. I undeleted it accidentally when I was editing it earlier today. – Pranav Hosangadi May 11 '21 at 15:04
  • @user1584421 I have unhidden my answer. Your code is very similar to mine, since we were following a similar approach. The error happens because your `found_coordinate` isn't `global`, so is removed every time the function finishes running. – Pranav Hosangadi May 13 '21 at 14:19

4 Answers4

8

As I said in my comments, I feel the Arduino output should be simplified. As @oliver_t said, a one line JSON for each sensor event would be perfect.

If you can't do that, here is the code to parse this.

As I do not have any way of receive your serial monitor output line by line, I have simulated that by storing the output in a txt file and then reading it line by line. I hope this helps as your question is how to parse the input.

f = open('stream.txt', 'r')
global found_coordinate
found_coordinate = False
while True:
    line = f.readline()
    if not line:
        break
    
    if "SENSOR COORDINATE" in line:
        found_coordinate = True
        coordinate = int(line.split("=")[1].strip())
        print("Coordinate",coordinate)
    
    if "MEASURED RESISTANCE" in line and found_coordinate:
        found_coordinate = False
        resistance = float(line.split("=")[1].split("kOhm")[0].strip())
        print("Resistance",resistance)

I hope this helps, if there is any discrepancy in me understanding your requirement, let me know, so I can fix my code.

Note: you actually might not require .strip() as typecasting to a int or float takes care of that, however I have still put it there as a sanity check

dracarys
  • 1,071
  • 1
  • 15
  • 31
  • 1
    Thank you! It worked! But after a while i got this error: ` if "MEASURED RESISTANCE" in ser_bytes and found_coordinate: UnboundLocalError: local variable 'found_coordinate' referenced before assignment` – user1584421 May 12 '21 at 23:04
  • It runs fine for some seconds, then it gives me this error! – user1584421 May 12 '21 at 23:05
  • I'll look into it, can you give me the full traceback? Or show it how you integrated this logic in your code? – dracarys May 13 '21 at 02:56
  • Yes, i will do it in a couple of hours. – user1584421 May 13 '21 at 07:02
  • @user1584421 I had added the found_coordinate variable just to ensure that it only detects a resistance value after it has found a coordinate value. But if you do not need that, you can remove that (I can update my code with that). But I wanted to see some more details so that I can give you a more informed suggestion – dracarys May 13 '21 at 09:04
  • No this step is absolutely necessary and i had thought about it myself before i get stuck. I edited the question. I have included the full function that reads serial data, prints to tkinter and uses your code. Below is the traceback. I don't understand why this error happens. Everything should work fine..... – user1584421 May 13 '21 at 11:50
  • Thank you very much! With the help of user Pranav Hosangadi, i made `found_coordinate` a global variable and it worked! With your joint effort and his help, i was able to solve this! Since you both helped me, is there an option to split the points between both of you? Half each? I really don't know how to split the points, and i don't want to brind down anyone. – user1584421 May 13 '21 at 22:55
  • @user1584421 I do not think you can split the bounty, you have to choose. https://stackoverflow.com/help/bounty – dracarys May 14 '21 at 09:55
3

If you can change your Arduino code, then you might be able to leverage the json.load() method in python to turn a string into something more manageable.

I'm not up on Arduinos (despite having one sitting with arms reach, box unopened, for the best part of two years...) so the following might be closer to pseudo-code than actual code:

# This should (fingers crossed) build and send a separate message for each variable.
# It could relatively easily be combined into one message.

double resistanse = ((period * GAIN_VALUE * 1000) / (4 * CAPACITOR_VALUE)) - R_BIAS_VALUE;

##########################################################
# If you want to send a separate message for each variable
String sSensor = "{\"sensorcoord\":";
String sResistance = "{\"resistance\":";
String sEnd = "}"

String sensor_output = sSensor + sensor_coord + sEnd
Serial.println(sensor_output)
# output will be {"sensorcoord":1}

String resistance_output = sResistance + resistanse + sEnd
Serial.println(resistance_output)
# output will be {"resistance":3.7}

########################################################
# If you want to send one message holding both variables

String sSensor = "{\"sensorcoord\":";
String sResistance = ",\"resistance\":";
String sEnd = "}"

String combined_output = sSensor + sensor_coord + sResistance + resistance + sEnd
Serial.println(combined_output)
# output will be {"sensorcoord": 1,"resistance":3.7}

Once you get the string into Python, you can then use json.loads() to take a (properly formatted) text string and turn it into an object that you can access more easily:

import json

data = json.loads(textstringFromArduino)

# If you sent each value separately, you now need to work out which value you are receiving.
for key, value in data.items():
    if key == "sensorcoord":
      print(value)
    elif key == "resistance":
      print(value)


# If you sent both values in one message, it's a lot easier...

print(data['sensorcoord'])
print(data['resistance'])


EvillerBob
  • 164
  • 7
3

So here is your readSerial function:

def readSerial():
    global after_id
    while ser.in_waiting:
        try:
            ser_bytes = ser.readline() 
            print(ser_bytes)
            ser_bytes = ser_bytes.decode("utf-8")
            print(ser_bytes)
            text.insert("end", ser_bytes)
           
            if "SENSOR COORDINATE" in ser_bytes:
               found_coordinate = True
               coordinate = int(ser_bytes.split("=")[1].strip())
               print("Coordinate",coordinate)
            if "MEASURED RESISTANCE" in ser_bytes and found_coordinate:
               found_coordinate = False
               resistance = float(ser_bytes.split("=")[1].split("kOhm")[0].strip())
               print("Resistance",resistance)
        
        except UnicodeDecodeError:
            print("UnicodeDecodeError")
    else:
        print("No data received")
    after_id=root.after(50,readSerial)

As you know, you have found_coordinate defined in each of the two if statements. But lets see where you reference the found_coordinate variable:

            if "MEASURED RESISTANCE" in ser_bytes and found_coordinate:

That is the only place where you use the found_coordinate variable, and it's also where the error occurred. Now consider this, if the

            if "SENSOR COORDINATE" in ser_bytes:

never evaluated to True, then the found_coordinate = True line never met, meaning the found_coordinate never got defined. Yes, there is another line where you define it, but it can only be executed with this condition:

            if "MEASURED RESISTANCE" in ser_bytes and found_coordinate:

which again, the found_coordinate variable didn't get defined yet, causing the error. You might be wondering: how did it run successfully for 10 seconds with no error? It's simple:

For 10 seconds, the if "SENSOR COORDINATE" in ser_bytes: all evaluated to False so the found_coordinate variable never got defined. But at the same time, for 10 seconds, the "MEASURED RESISTANCE" in ser_bytes also all evaluated to False, so the program didn't continue to the and found_coordinate, as there was no need. At the time of the error is when the "MEASURED RESISTANCE" in ser_bytes evaluated to True, making the program parse the and found_coordinate, where found_coordinate haven't gotten defined.

Red
  • 26,798
  • 7
  • 36
  • 58
2

You were almost there with your attempt. The UnboundLocalError happens because the variable found_coordinate isn't defined in your function if the line is a resistance line. You should define that as a global variable too, because you need to keep track of it over multiple function calls. I'm intrigued that the first set of coordinate/resistance worked. So do

global after_id, found_coordinate

at the beginning of your function.


I wrote this answer before you posted your attempt. The approach is very similar to yours. Use from it what you find useful!

You don't need split() at all. Since all you get at a time is a line, parse each line and interpret the result. If you found the sensor coordinate, keep your eye out for a line that gives measured distance. Once you have both, record them and keep an eye out for a new sensor coordinate.

  1. Receive line
  2. Does line contain "SENSOR COORDINATE"?
    • Yes: parse the number and save it for later use.
    • No: Do nothing? Or print an error message?
  3. Does line contain "MEASURED RESISTANCE"?
    • Yes: parse the number and save it.
      • Now we should have both items of our pair. Save them for later.
    • No: Do nothing? Or print an error message?

First, let's define a function to extract only the numbers from each line:

import re

def get_numbers_from_line(line):
    try:
        numbers_rex = r"(\d+(?:\.\d+)*)"
        matched_text = re.findall(numbers_rex, line)[0]
        parsed_number = float(matched_text)
        return parsed_number
    except IndexError:
        print(f"No numbers found on line {line}")
    except ValueError:
        print(f"Couldn't parse number {matched_text}")
    
    # Only come here if error occurred. Not required, but for clarity we return None
    return None

Next, let's define a function to parse each line and decide what to do with it. It will use a couple of global variables to keep track of its state:

all_pairs = []
parsed_pair = []
def parse_line(line):
    global all_pairs, parsed_pair
    if "SENSOR COORDINATE" in line:
        sensor_coord = get_numbers_from_line(line)
        
        if parsed_pair:
            # Something already exists in parsed_pair. Tell user we are discarding it
            print("Data already existed in parsed_pair when a new SENSOR COORDINATE was received")
            print("Existing data was discarded")
            print(f"parsed_pair: {parsed_pair}")
        
        if sensor_coord is not None:
            # Make a new list containing only this newly parsed number
            parsed_pair = [sensor_coord]
    
    elif "MEASURED RESISTANCE" in line:
        resistance = get_numbers_from_line(line)
        if not parsed_pair:
            # parsed_pair is empty, so no sensor coordinate was recorded. 
            # Ignore this line and wait for the start of the next pair of data
            print("Received measured resistance without corresponding sensor coordinate")
            print("Received data was discarded")
            print(f"Received data: {resistance}")
        elif resistance is not None:
            parsed_pair.append(resistance) # Add resistance to the pair
            all_pairs.append(parsed_pair)  # Add the pair to all_pairs 
            parsed_pair = []  # Make a new empty list for the next pair
            print(f"Added pair {parsed_pair}")

Then, in def readSerial():after text.insert("end", ser_bytes), call this function.

def readSerial():
    global after_id
    while ser.in_waiting:
        try:
            ser_bytes = ser.readline() #read data from the serial line
            ser_bytes = ser_bytes.decode("utf-8")
            text.insert("end", ser_bytes)
            parse_line(ser_bytes)
        except UnicodeDecodeError:
            print("UnicodeDecodeError")
    else:
        print("No data received")
    after_id=root.after(50,readSerial)
Pranav Hosangadi
  • 23,755
  • 7
  • 44
  • 70
  • Thank you very much! Yes, i made it a global variable and it worked! With your joint effort and user dracarys, i was able to solve this! Since you both helped me, is there an option to split the points between both of you? Half each? I really don't know how to split the points, and i don't want to brind down anyone. – user1584421 May 13 '21 at 22:53
  • Do you have any info on what i can do with the bounty? The majority of help came from user dracarys, but you helped me with making the variable global, and you had a nice big post. – user1584421 May 17 '21 at 00:30
  • I'm not sure whether you can split the bounty. If you can't, you should award it to dracarys, since their post helped you most. – Pranav Hosangadi May 17 '21 at 14:25
  • I am so sorry, but there is no way to split the bounty. I want to thank you personally, for taking the time to help me out. Very much appreciated sir! I wish you all the best! – user1584421 May 17 '21 at 14:28
  • No worries, glad to help! You seem to have awarded the wrong answer though :) – Pranav Hosangadi May 17 '21 at 14:29