Enumerating serial ports

Linux specific forum
User avatar
dhouston
Enthusiast
Enthusiast
Posts: 430
Joined: Tue Aug 21, 2007 2:44 pm
Location: USA (Cincinnati)
Contact:

Enumerating serial ports

Post by dhouston »

I have an application, originally written in VisualBasic that I have about 90% ported to PB so I can release it for both Windows and Linux users. It communicates with an embedded device using RS232.

Under Windows, there is an API function (EnumPorts) which returns a list of all the printer ports. Since all the serial ports are a subset of the printer ports, all I have to do is filter out those that are not COM ports, are already in use (see Note below), or cannot be opened. I present the list of available ports in a menu for the user to select a port. This has proven to be reliable and (thanks to ABB's help) it's working under PB/Windows.

As I want to keep the look and feel as similar as possible on all platforms, I would like to do something similar under Linux. From a terminal, I can manually get information about the ports with 'dmesg | fgrep tty' and can pipe the output to a file, if necessary. I would like to find a way to do this automatically when my application starts. So far, I have not found a way to use RunProgram and either catch its output or pipe it to a file which I can then parse to find the ports. I'm a complete novice when it comes to Linux and am not even sure whether 'fgrep' is a commandline parameter or is a separate program.

Is there a way to do this under Linux using RunProgram or any other method?

Note: While testing serial comms under Linux, I accidentally discovered that more than one program can open and control a port at the same time. As neat as this might be in certain circumstances, I want to put a stop to such fraternizing. How do I block other apps from accessing a port once it's opened? The test code is in my first post to the thread Serial Communications via API http://www.purebasic.fr/english/viewtopic.php?t=16847 in this forum. If you run two instances, both toggle DTR.
User avatar
dhouston
Enthusiast
Enthusiast
Posts: 430
Joined: Tue Aug 21, 2007 2:44 pm
Location: USA (Cincinnati)
Contact:

Post by dhouston »

Thanks to some help and suggestions from a couple of experienced Linux users, I have a solution. This snippet shows how to use dmesg, fgrep & tee with RunProgram(). The result is a text file (found.txt) in the current directory (where the executable lives) that can be parsed to find all the serial ports.

Code: Select all

Dmesg = RunProgram("/bin/dmesg","","",#PB_Program_Open|#PB_Program_Read)
Fgrep = RunProgram("/bin/fgrep","tty","",#PB_Program_Open|#PB_Program_Connect|#PB_Program_Read,Dmesg)
Tee =
RunProgram("/usr/bin/tee","found.txt",GetCurrentDirectory(),#PB_Program_Open|#PB_Program_Connect|#PB_Program_Read,Fgrep)
CloseProgram(Dmesg):CloseProgram(Fgrep):CloseProgram(Tee)
I still need to learn how to lock the device so only my app can access it and I discovered yet another problem - multiple USB adapters can switch designations from one boot to the next. That is, the one that's ttyUSB0 at one boot, might be ttyUSB1 at the next and vice versa.
walker
Enthusiast
Enthusiast
Posts: 634
Joined: Wed May 05, 2004 4:04 pm
Location: Germany

Post by walker »

...Googled a bit and I guess the solution is a lock file created in /var/lock called LCK..ttyS0 or whatever port you want to lock...
(not sure about the name.. but that was what I'd found... and yes, there are 2 dots in this name...)

EDIT: the contents of the file is the PID of the locking prog expanded to 10 ASCII chars followed by a LF. If the PID is 1234 then you have to write spacespacespacespacespacespace1234LF (6 times a space, then the PID and then the LF)

EDIT 2: The name is corrrect! the prefix is LCK..
User avatar
dhouston
Enthusiast
Enthusiast
Posts: 430
Joined: Tue Aug 21, 2007 2:44 pm
Location: USA (Cincinnati)
Contact:

Post by dhouston »

That (locking the device) seems easy enough but how do I get the PID? If this is a dumb question, please forgive me - I'm new to Linux. I assume the lock file needs to be deleted when my program ends. I found this, which verifies what you posted, but doesn't tell me how to get the PID...
  • Arbitration of serial ports is provided by the use of lock files with
    the names /var/lock/LCK..ttyX#. The contents of the lock file should
    be the PID of the locking process as an ASCII number.
Here's simple example of a 'found.txt' file...
  • [ 16.526050] serial8250: ttyS0 at I/O 0x3f8 (irq = 4) is a 16550A
    [ 16.530019] 00:0a: ttyS0 at I/O 0x3f8 (irq = 4) is a 16550A
    [ 46.872000] usb 1-1.3: FTDI USB Serial Device converter now attached to ttyUSB0
    [16175.892000] usb 1-1.4: pl2303 converter now attached to ttyUSB1
The last entry is for an adapter that was hot-plugged to make sure this method discovers devices added after boot. It's a simple matter to parse the file (avoiding duplicate instances of a device name) to get a list of the serial ports that can be tested before adding to a menu or combobox.

At the next boot, the last two entries may be swapped with the PL2303 adapter becoming ttyUSB0 and and the FTDI adapter becoming ttyUSB1. I'm still searching for a way to deal with this, especially if there are multiple FTDI or PL2303 (or other) adapters.

The Tibbo ethernet-serial adapter uses ttyVSPn and there may be other variations.

Also, I'm not sure that all devices with tty in their name will be serial ports.
walker
Enthusiast
Enthusiast
Posts: 634
Joined: Wed May 05, 2004 4:04 pm
Location: Germany

Post by walker »

simply use ps -A and redirect the output to a file; then search for your prog... and voila you have the PID :D (type ps -A in a terminal to see what I mean.. it must be an uppercase A .. otherwise you'll only get the processes started from the actual terminal)
User avatar
dhouston
Enthusiast
Enthusiast
Posts: 430
Joined: Tue Aug 21, 2007 2:44 pm
Location: USA (Cincinnati)
Contact:

Post by dhouston »

Does this warrant a feature request for a native function to get the program's PID?
walker
Enthusiast
Enthusiast
Posts: 634
Joined: Wed May 05, 2004 4:04 pm
Location: Germany

Post by walker »

Well a feature request is only successfull if it could be implemented in all 3 OS-Versions.... I guess it's worth a try...

meanwhile ... here is a procedure I use by myself to determine the PID and test if my prog is already running

Code: Select all

ProcedureDLL.s find_proc(proc_name.s);find a running process with the given name - returns a LF seperated string
mypgm1= RunProgram("ps", "-A","",#PB_Program_Open|#PB_Program_Read)
If proc_name=""
    mypgm= RunProgram("more", "","",#PB_Program_Open|#PB_Program_Connect|#PB_Program_Read,mypgm1)
Else
    mypgm= RunProgram("grep", "-i "+proc_name.s,"",#PB_Program_Open|#PB_Program_Connect|#PB_Program_Read,mypgm1)
EndIf
While ProgramRunning(mypgm)
      output$ + ReadProgramString(mypgm) +#LF$
Wend
Trim(output$)
While Right(output$,1)=#LF$
    output$=Left(output$,Len(output$)-1)
Wend

If IsProgram(mypgm) And IsProgram(mypgm1)
    KillProgram(mypgm1)
    KillProgram(mypgm)
    CloseProgram(mypgm1)
    CloseProgram(mypgm)
EndIf

ProcedureReturn output$

EndProcedure

it returns a string which must be parsed by you if you want only the PID...(but thats easy :D )
Last edited by walker on Wed Feb 13, 2008 2:02 am, edited 1 time in total.
User avatar
dhouston
Enthusiast
Enthusiast
Posts: 430
Joined: Tue Aug 21, 2007 2:44 pm
Location: USA (Cincinnati)
Contact:

Post by dhouston »

Thanks. That fills one more gap for me. :D

EDIT: It seems XP uses Process IDs but earlier Windows versions apparently do not so I guess this is specific to Linux and Mac. But, Windows doesn't have the problem of multiple programs (or instances of the same program) accessing serial devices so it's not really necessary.
User avatar
dhouston
Enthusiast
Enthusiast
Posts: 430
Joined: Tue Aug 21, 2007 2:44 pm
Location: USA (Cincinnati)
Contact:

Post by dhouston »

While the series of RunProgram calls to enumerate the serial ports works fine in the IDE, it creates an empty file after compiling.

Same for getting the PID. From the IDE it gets all the PIDs, although not for the application code, instead getting PB's PID. When compiled it creates an empty file.

Why the difference?

EDIT: Adding WaitProgram(Tee) fixed both and I now get files with the serial ports and PID for the compiled app.
User avatar
dhouston
Enthusiast
Enthusiast
Posts: 430
Joined: Tue Aug 21, 2007 2:44 pm
Location: USA (Cincinnati)
Contact:

Post by dhouston »

Here's an update to the code for enumerating ports, locking the selected port and sending/receiving using various methods. You can test with a loopback adapter.

Code: Select all

;------------------------------------------------------------------------------------------------------
;
;   PureBasic - LINUX Serial Communications
;   based on post to PureBasic Linux Forum by Larry Duarte
;     http://www.purebasic.fr/english/viewtopic.php?t=16847&highlight=linux+serial
;
;   User must add dev entry to the serial.prf preference file
; 
;   Locking the port only works when compiled (need compiled app PID)
;-------------------------------------------------------------------------------------------------------
;
EnableExplicit

#O_RDONLY   = 0
#O_WRONLY   = 1
#O_RDWR      = 2
#O_NOCTTY   = $100
#O_NONBLOCK = $800
#TIOCMGET   = $5415
#TIOCMSET   = $5418
#SET_DTR    = %000000000010    ;$002
#CLR_DTR    = %111111111101    ;~#SET_DTR
#WNDW_MAIN  = 0
#EDT_RCV    = 1
#STR_TX     = 2
#DTR_LED     = 3
#STATUS =4
#CBO_PORTS=5
#PB_SerialPort_NoParity=0
#PB_SerialPort_NoHandshake=0

Procedure ScrollToEnd(gadget)              ;scrolls text window to last line
  Protected end_mark,*buffer, end_iter.GtkTextIter
  *buffer=gtk_text_view_get_buffer_(GadgetID(gadget))
  gtk_text_buffer_get_end_iter_(*buffer,@end_iter)
  ;gtk_text_buffer_place_cursor_(*buffer,@end_iter)
  end_mark=gtk_text_buffer_create_mark_(*buffer,"end_mark",@end_iter,#False)
  gtk_text_view_scroll_mark_onscreen_(GadgetID(gadget),end_mark)
EndProcedure

Define.s buf=Space(5),out=Space(10),dev,Pid,output,LineIn,found,ports,key,bad
Define.l EventID,sec,i,n,Quit,returncode,fd,flags,ps,dmesg,fgrep,tee,t,s,u,c,ComID,mode,*buffer
*buffer=AllocateMemory(10)

mode=1  ;0=Linux API, 1=Mattias Groß library. 2=Native PB

If OpenWindow(#WNDW_MAIN,100,100,188,235,"SerialTest",#PB_Window_MinimizeGadget|#PB_Window_ScreenCentered)

  TextGadget(#PB_Any,10,50,150,20,"Receive Data")
  EditorGadget(#EDT_RCV,10,70,168,100)
  TextGadget(#PB_Any,10,180,150,20,"Send Data")
  StringGadget(#STR_TX,10,200,168,20,"",#PB_Text_Center)
  TextGadget(#PB_Any,10,4,200,22,"Port:")
  ComboBoxGadget(#CBO_PORTS,42,0,135,27)
  TextGadget(#PB_Any,10,33,50,22,"DTR"):TextGadget(#DTR_LED,40,36,15,15,"",#PB_Text_Border)
  TextGadget(#PB_Any,65,33,50,22,"Flags"):TextGadget(#STATUS,100,33,77,22,RSet(Bin(flags),9,"0"),#PB_Text_Border)
  ;-----get PID-----ONLY WORKS FOR COMPILED APPLICATION
  ps=RunProgram("/bin/ps", "-A","",#PB_Program_Open|#PB_Program_Read)
  fgrep=RunProgram("/bin/fgrep", "linux-serial","",#PB_Program_Open|#PB_Program_Connect|#PB_Program_Read,ps)
  tee = RunProgram("/usr/bin/tee","PID.txt",GetCurrentDirectory(),#PB_Program_Open|#PB_Program_Connect|#PB_Program_Read,fgrep)
  While ProgramRunning(tee)
    Pid=StringField(Trim(ReadProgramString(tee)),1," ")
  Wend
  WaitProgram(tee)
  CloseProgram(ps):CloseProgram(tee)
  ;-----enum ports-----
  DeleteFile("tty.txt")
  dmesg = RunProgram("/bin/dmesg","","",#PB_Program_Open|#PB_Program_Read)
  fgrep = RunProgram("/bin/fgrep","tty","",#PB_Program_Open|#PB_Program_Connect|#PB_Program_Read,Dmesg)
  tee = RunProgram("/usr/bin/tee","tty.txt",GetCurrentDirectory(),#PB_Program_Open|#PB_Program_Connect|#PB_Program_Read,Fgrep)
  WaitProgram(tee)
  CloseProgram(Dmesg):CloseProgram(Fgrep):CloseProgram(Tee)
  ;-----parse tty.txt-----
  ports=""
  If ReadFile(0,"tty.txt")
    While Eof(0) = 0 
      LineIn=ReadString(0)+" "
      t=FindString(LineIn,"tty",1)
      If t>0
        s=FindString(LineIn," ",t)
        dev=Trim(Mid(LineIn,t,s-t))     
        If Right(dev,1)=":":dev=Left(dev,Len(dev)-1):EndIf
        If Not IsNumeric(Mid(dev,4,1))
          c=CountString(LineIn,"tty")
          If c=1      
            RunProgram("stty","-F /dev/"+dev+" 19200 cread -echo -icanon","")
            fd=open_("/dev/"+dev,#O_NONBLOCK|#O_NOCTTY|#O_RDWR,0)  
            If fd>0
              If FindString(ports,dev,1)=0
                If Len(ports):ports+",":EndIf
                ports+"/dev/"+dev
              EndIf  
              close_(fd)
            EndIf             
          ElseIf (c=2) 
            s=Val(Mid(dev,5,2))                                     ;first index
            t=FindString(LineIn,"tty",t+Len(dev))                   ;find second tty?xx
            c=Val(Mid(LineIn,t+4,2))                                ;second index
            If (c>s)
              For i=s To c
                dev=Left(dev,4)+Str(i)       
                RunProgram("stty","-F /dev/"+dev+" 19200 cread -echo -icanon","")
                fd=open_("/dev/"+dev,#O_NONBLOCK|#O_NOCTTY|#O_RDWR,0)  
                If fd>0
                  If FindString(ports,dev,1)=0
                    If Len(ports):ports+",":EndIf
                    ports+"/dev/"+dev
                  EndIf  
                  close_(fd)
                EndIf                 
              Next
            ElseIf FindString(LineIn,"-",1)>0
              MessageRequester("Serial Port Enumeration","Unexpected dmesg format - Copy tty.txt to roZetta author"+#CRLF$+LineIn)
            EndIf
          Else
            MessageRequester("Serial Port Enumeration","Unexpected dmesg format - Copy tty.txt to roZetta author"+#CRLF$+LineIn)
          EndIf  
        EndIf
      EndIf
    Wend
    CloseFile(0)
  EndIf 
  ;-----lock file----
  OpenPreferences("serial.prf"):PreferenceGroup("port")
    WritePreferenceString("ports",ports)
    dev=ReadPreferenceString("dev","NONE")  ;ADD THE dev KEY MANUALLY
  ClosePreferences()
  SetGadgetText(#CBO_PORTS,dev)
  Select mode
    Case 0
      RunProgram("stty","-F "+dev+" 19200 cread -echo -icanon","")
      fd=open_(dev,#O_NONBLOCK|#O_NOCTTY|#O_RDWR,0)
    Case 1
      fd=ComOpen(dev+":19200,N,8,1",0,1024,1024)
    Case 2
      fd=OpenSerialPort(#PB_Any,"/dev/"+dev,19200,#PB_SerialPort_NoParity,8,1,#PB_SerialPort_NoHandshake,1024,1024)
  EndSelect   
  If (fd<1)
   MessageRequester("ERROR!","Unable to open "+dev)
  Else
    Repeat
      EventID=WindowEvent()
      If (sec<>Second(Date()))
        sec=Second(Date())
        out=FormatDate("%hh:%ii:%ss",Date())+Chr(10)
        Select mode
          Case 0
            n=write_(fd,out,9)
            returncode=ioctl_(fd,#TIOCMGET,@flags)
            SetGadgetText(#STATUS,RSet(Bin(flags),9,"0"))
            If (flags & 2)=#SET_DTR
              flags & #CLR_DTR
              SetGadgetColor(#DTR_LED,#PB_Gadget_BackColor,RGB(255,0,0))
            Else 
              flags | #SET_DTR
              SetGadgetColor(#DTR_LED,#PB_Gadget_BackColor,RGB(0,255,0))
            EndIf 
            returncode=ioctl_(fd,#TIOCMSET,@flags)       
          Case 1
            n=ComOutput(fd,out)
            If sec % 2 = 1
              ComSetDTR(fd,1)
              SetGadgetColor(#DTR_LED,#PB_Gadget_BackColor,RGB(255,0,0))
            Else 
              ComSetDTR(fd,0)
              SetGadgetColor(#DTR_LED,#PB_Gadget_BackColor,RGB(0,255,0))
            EndIf 
          Case 2
            n=WriteSerialPortString(fd, out)
        EndSelect   
        If (n<1)
          SetGadgetText(#STR_TX,"Write() failed! ")
        Else
          SetGadgetText(#STR_TX,Left(out,8))
        EndIf
      EndIf
   
      Select mode
        Case 0
          n=read_(fd,buf,1)
        Case 1
          n=ComInput(fd,buf)         
        Case 2
          If AvailableSerialPortInput(fd)
            n=AvailableSerialPortInput(fd)
            n=ReadSerialPortData(fd, *buffer, n)
            buf=""
            For i=0 To n-1
              buf=buf+PeekS(*buffer+i)
            Next
          EndIf       
      EndSelect   
      If (n > 0)
        SetGadgetText(#EDT_RCV,GetGadgetText(#EDT_RCV)+Left(buf,1))
        ScrollToEnd(#EDT_RCV)
      EndIf
      If EventID = #PB_Event_CloseWindow
        Quit = 1
      EndIf
    Until Quit=1
    close_(fd)
  EndIf
EndIf

End
Post Reply