Enumerating serial ports
Enumerating serial ports
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.
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.
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.
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.
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)
...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..
(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..
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...
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.
- 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.
- [ 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
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.
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
it returns a string which must be parsed by you if you want only the PID...(but thats easy
)
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

Last edited by walker on Wed Feb 13, 2008 2:02 am, edited 1 time in total.
Thanks. That fills one more gap for me. 
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.

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.
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.
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.
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