Serial Port - use in threads

Just starting out? Need help? Post your questions and find answers here.
ss3e55
New User
New User
Posts: 5
Joined: Mon Nov 10, 2014 9:17 pm

Serial Port - use in threads

Post by ss3e55 »

NEWBIE - so my apologies in advance for any mistakes in approach.

I am trying to interact with a remote program via a serial port, basically I thought that putting all the serial port handling in one thread would be a good architectural separation (especially as at first it was mixed into the event loop). I am having issues with the serial port available and read commands.
My understanding is to use available as the call to see how many characters have been received and are therefore ready to read.

First question does the serial port opening etc need to be in the same thread (at first it wasn't and I got red lines in the code on debug to say it was uninitialized) Why is this so?

I need to do two things in my program with the received data..
a) find the next start of a line (just after LF) to "sync" with the lines being sent to me (line oriented replies)
b) then break these received characters into separate lines as many can arrive at once or spaced out in time (which I will later process according to the first 3 characters - right now I deposit them in two different list gadgets)

--- so this is getting hard for a number of reasons which YOU might be able to help be resolve (please!) - The variable length input and the time outs conspire to give me incomplete input lines (so using StringField to split on chr(10) sometimes gives me the "rest of the line" and I have to come around and find the next chr(10) on the subsequent read...
-- and StringField might or might not have "swallowed" the chr(10) - if it was there in the string read so far then it did, if we reached end of the data received so far it didn't (how can I tell??) - I need this to keep track of how far through I am in the currently read piece whose size I got from available call (and this piece might have part, one or many "lines").
-- reading here on serial port topics I find every example uses a SINGLE byte read from the input (ReadSerialPortData(port, *buf, 1)) ) - this can work of course but isn't it very inefficient??
-- should I read just one character at a time - forget stringfield and just search for line ends myself - is there a better way??
-- different topic (related for me) SHARED - what amongst the serial port handling must be shared explicitly??
-- also would it be cleaner (better or safer too) to send the received and split data out of the serial port handling thread via a eventPost or allow shared data strings??


thanks for reading this far and for any help (or pointers to where I missed a relevant topic) you can give me
Phil
example thread :-

Code: Select all

Procedure serial_rd(Parameter)
  Shared *Pointer, nx$
  Port$ = "COM4"
  If OpenSerialPort(0, Port$, 230400, #PB_SerialPort_NoParity, 8, 1, #PB_SerialPort_NoHandshake, 1024, 1024)
  	MessageRequester("Information", "SerialPort opened with success - "+Port$)
  Else
    MessageRequester("Error", "Can't open the serial port: "+Port$)
    ;Break(1)
  EndIf
  position = 0
  Repeat
    ;  receive serial data
    size = AvailableSerialPortInput(0)
    reslen = ReadSerialPortData(0, *Pointer, size)
    If reslen > 0
      Rx$ = PeekS(*Pointer+position, reslen, #PB_Ascii)
      position = 1
      i = 1 ; trying to keep track of the segments supplied by stringfield calls
      While position < reslen
        nx$ = StringField(Rx$, i, Chr(10))    ; find between \n characters
        i = i+1
        position = position + StringByteLength(nx$,#PB_Ascii) +1 ; removed \n so real length one greater (help unless at end prematurely)
        ; send the nx$ out to rest of code
        ; if the string starts with "sfs" its for us 
        l$ = Left(nx$,3)
        If  l$ = "sfs"
          ; send it to main
          AddGadgetItem(3, -1, nx$)
        Else
          ; send it to other
          AddGadgetItem(2, -1, nx$)
        EndIf
      Wend
      reslen = 0
    EndIf
  ForEver
  CloseSerialPort(0) 
EndProcedure
infratec
Always Here
Always Here
Posts: 7577
Joined: Sun Sep 07, 2008 12:45 pm
Location: Germany

Re: Serial Port - use in threads

Post by infratec »

Hi,

1. Serial stuff in a thread is the best you can do.

2. You also can open the port outside of the thread.
But without code, I can not tell you why it failed.

3. To read only one byte has several advantages.
And if the serial speed is slower than your code, it doesn't matter.
If you have a continious stream at high speed, than it makes sense to think about
the additional work to fiddle out the strings. (You need than a so called ring buffer)

4. You can use Gadget stuff inside the thread, but ...
You will run into problems, at least when you close the program and the thread is at a gadget command.
He waits than for the eventloop, which is not handled anymore.

My prefered version:

Code: Select all

EnableExplicit

Enumeration #PB_Event_FirstCustomValue
  #Serial_Event_StringForUs
  #Serial_Event_StringForOthers
  #Serial_Event_Error_PortOpening
  #Serial_Event_Exit
EndEnumeration



Structure ThreadParameterStructure
  Port$
  Semaphore.i
  Port.i
  nx$
  Exit.i
EndStructure


Procedure serial_rd(*Parameter.ThreadParameterStructure)
  
  Protected.i position, size, reslen, i, State
  Protected Port$, Rx$, l$
  Protected Byte.a
  
  
  *Parameter\Port = OpenSerialPort(#PB_Any, *Parameter\Port$, 230400, #PB_SerialPort_NoParity, 8, 1, #PB_SerialPort_NoHandshake, 1024, 1024)
  If *Parameter\Port    
    
    position = 0
    Repeat
      ;  receive serial data
      size = AvailableSerialPortInput(*Parameter\Port)
      If size > 0     ; only if someting to read
        
        If ReadSerialPortData(*Parameter\Port, @Byte, 1)
          
          Select State
            Case 0
              If Byte = #LF
                State = 1
              EndIf
              
            Case 1
              If Byte <> #LF
                Rx$ + Chr(Byte)
                Debug Rx$
              Else
                *Parameter\nx$ = Rx$
                If Left(*Parameter\nx$, 3) = "sfs"
                  PostEvent(#Serial_Event_StringForUs)
                Else
                  PostEvent(#Serial_Event_StringForOthers)
                EndIf
                WaitSemaphore(*Parameter\Semaphore)
                Rx$ = ""
              EndIf
              
          EndSelect
          
        EndIf
      Else
        Delay(1)    ; to avoid a high CPU load if nothing to receive
      EndIf
    Until *Parameter\Exit
    CloseSerialPort(*Parameter\Port)
  Else
    PostEvent(#Serial_Event_Error_PortOpening)
  EndIf
  
  PostEvent(#Serial_Event_Exit)
  
EndProcedure


Define.i Exit, Event, Thread
Define ThreadParameter.ThreadParameterStructure


CompilerIf Not #PB_Compiler_Thread
  MessageRequester("Info", "You have to enable the Thread-Save flag.")
  End
CompilerEndIf



If OpenWindow(0, 0, 0, 400, 500, "Test", #PB_Window_SystemMenu|#PB_Window_ScreenCentered)
  
  ListIconGadget(0, 10, 10, 380, 200, "For Us", 270)
  ListIconGadget(1, 10, 220, 380, 200, "For Other", 270)
  
  
  ThreadParameter\Port$ = "COM4"
  ThreadParameter\Semaphore = CreateSemaphore()
  
  Thread = CreateThread(@serial_rd(), @ThreadParameter)
  
  Repeat
    
    Event = WaitWindowEvent()
    
    Select Event
        
      Case #Serial_Event_StringForUs
        AddGadgetItem(0, -1, ThreadParameter\nx$)
        SignalSemaphore(ThreadParameter\Semaphore)
        
      Case #Serial_Event_StringForOthers
        AddGadgetItem(1, -1, ThreadParameter\nx$)
        SignalSemaphore(ThreadParameter\Semaphore)
        
      Case #Serial_Event_Error_PortOpening
        MessageRequester("Error", "Serial port open failed.")
        
      Case #Serial_Event_Exit
        ; do something or not
        Exit = #True
        
      Case #PB_Event_CloseWindow
        Exit = #True
        
    EndSelect
    
  Until Exit
  
  If IsThread(Thread)
    ThreadParameter\Exit = #True
    SignalSemaphore(ThreadParameter\Semaphore)  ; to be sure it is not waiting
    If WaitThread(Thread, 1000) = 0
      KillThread(Thread)
    EndIf
  EndIf
  
EndIf
Bernd
infratec
Always Here
Always Here
Posts: 7577
Joined: Sun Sep 07, 2008 12:45 pm
Location: Germany

Re: Serial Port - use in threads

Post by infratec »

Version with port opened outside the thread:

Code: Select all

EnableExplicit

Enumeration #PB_Event_FirstCustomValue
  #Serial_Event_StringForUs
  #Serial_Event_StringForOthers
  #Serial_Event_Exit
EndEnumeration



Structure ThreadParameterStructure
  Port$
  Semaphore.i
  Port.i
  nx$
  Exit.i
EndStructure


Procedure serial_rd(*Parameter.ThreadParameterStructure)
  
  Protected.i position, size, reslen, i, State
  Protected Port$, Rx$, l$
  Protected Byte.a
  
  
  
  If IsSerialPort(*Parameter\Port)
    
    position = 0
    Repeat
      ;  receive serial data
      size = AvailableSerialPortInput(*Parameter\Port)
      If size > 0     ; only if someting to read
        
        If ReadSerialPortData(*Parameter\Port, @Byte, 1)
          
          Select State
            Case 0
              If Byte = #LF
                State = 1
              EndIf
              
            Case 1
              If Byte <> #LF
                Rx$ + Chr(Byte)
                Debug Rx$
              Else
                *Parameter\nx$ = Rx$
                If Left(*Parameter\nx$, 3) = "sfs"
                  PostEvent(#Serial_Event_StringForUs)
                Else
                  PostEvent(#Serial_Event_StringForOthers)
                EndIf
                WaitSemaphore(*Parameter\Semaphore)
                Rx$ = ""
              EndIf
              
          EndSelect
          
        EndIf
      Else
        Delay(1)    ; to avoid a high CPU load if nothing to receive
      EndIf
    Until *Parameter\Exit
    
  EndIf
  
  PostEvent(#Serial_Event_Exit)
  
EndProcedure


Define.i Exit, Event, Thread
Define ThreadParameter.ThreadParameterStructure


CompilerIf Not #PB_Compiler_Thread
  MessageRequester("Info", "You have to enable the Thread-Save flag.")
  End
CompilerEndIf



If OpenWindow(0, 0, 0, 400, 500, "Test", #PB_Window_SystemMenu|#PB_Window_ScreenCentered)
  
  ListIconGadget(0, 10, 10, 380, 200, "For Us", 270)
  ListIconGadget(1, 10, 220, 380, 200, "For Other", 270)
  
  
  ThreadParameter\Port$ = "COM4"
  ThreadParameter\Semaphore = CreateSemaphore()
  
  ThreadParameter\Port = OpenSerialPort(#PB_Any, ThreadParameter\Port$, 230400, #PB_SerialPort_NoParity, 8, 1, #PB_SerialPort_NoHandshake, 1024, 1024)
  If ThreadParameter\Port
    
    Thread = CreateThread(@serial_rd(), @ThreadParameter)
    
    Repeat
      
      Event = WaitWindowEvent()
      
      Select Event
        
        Case #Serial_Event_StringForUs
          AddGadgetItem(0, -1, ThreadParameter\nx$)
          SignalSemaphore(ThreadParameter\Semaphore)
          
        Case #Serial_Event_StringForOthers
          AddGadgetItem(1, -1, ThreadParameter\nx$)
          SignalSemaphore(ThreadParameter\Semaphore)
          
        Case #Serial_Event_Exit
          ; do something or not
          Exit = #True
          
        Case #PB_Event_CloseWindow
          Exit = #True
          
      EndSelect
      
    Until Exit
    
    If IsThread(Thread)
      ThreadParameter\Exit = #True
      SignalSemaphore(ThreadParameter\Semaphore)  ; to be sure it is not waiting
      If WaitThread(Thread, 1000) = 0
        KillThread(Thread)
      EndIf
    EndIf
    
    CloseSerialPort(ThreadParameter\Port)
    
  Else
    
    MessageRequester("Error", "Was not able to open " + ThreadParameter\Port$)
    
  EndIf
    
EndIf
Bernd
ss3e55
New User
New User
Posts: 5
Joined: Mon Nov 10, 2014 9:17 pm

Re: Serial Port - use in threads

Post by ss3e55 »

Thank you so much for the kind suggestions
I did work on this a little the last day or so...I made a serial handler that was single char based, but which broke the returned character stream into larger pieces to feed onward - this worked well and cleared several issues I had had.

The thread structure works well enough, its the passing of data into and out of a thread that concerned me most after the serial port issues - BTW did you notice I ran the serial port at 230400 baud - twice the limit mentioned in the user guide - it seems to work fine. I figured today's faster machines should have no issues. Since no code seems to have checked against a 115200 limit this worked.

Thank you also for the examples. (and yes I remembered to compile thread safe :-)

BTW big thank you to the folks that wrote Purebasic - its a great tool, easy (relatively) to learn, has major functional pieces (from Networking to sound to 3D to an entire editor Scintilla etc etc.) the examples are great and support too.. Well worth the low cost. I recommend it over much more grandiose IDE/language systems costing thousands more.

Thanks
Phil
Post Reply