Small MOD add multiple comports, GUI terminals, parameter changes, INI saving, StopBit fix, and auto port detection
Code: Select all
;-TOP
; https://www.purebasic.fr/english/viewtopic.php?p=644565#p644565
; Comment : Comport Manager Over Thread and Callback
; Author : mk-soft
; Version : v0.07.1
; Created : 26.01.2018
; Updated : 03.09.2022
; Modified Mol4un : 28.08.2025 for multiple comports, GUI terminals, parameter changes, INI saving, StopBit fix, and auto port detection
; *****************************************************************************
CompilerIf #PB_Compiler_Thread = 0
CompilerError "Use Option Threadsafe!"
CompilerEndIf
#COMMaxPorts = 256 ; Define maximum COM ports for Windows
Global NewList COMUsablePorts.s() ; Global list for usable COM ports
Prototype ProtoReceiveCB(Text.s, Index.i)
Prototype ProtoStatusCB(Status, *ComData)
Enumeration
#ComThread_Stopped
#ComThread_Startup
#ComThread_Running
EndEnumeration
Enumeration
#ComStatus_Nothing
#ComStatus_OpenPort
#ComStatus_ClosePort
#ComStatus_ErrorOpenPort
#ComStatus_ErrorSend
#ComStatus_ErrorReceive
#ComStatus_ErrorDataSize
EndEnumeration
Structure udtComData
; Header
Index.i
ThreadID.i
Exit.i
Status.i
; Port Data
ComID.i
Port.s
Baud.i
Parity.i
DataBit.i
StopBit.f
Handshake.i
BufferSize.i
; End Of Text
EndOfText.s
; Send Data
SendSignal.i
SendCount.i
SendText.s
SendError.i
; Receive data
ReceiveCount.i
ReceiveText.s
ReceiveError.i
; Callback
*StatusCB.ProtoStatusCB
*ReceiveCB.ProtoReceiveCB
EndStructure
Procedure thComport(*ComData.udtComData)
Protected *Send, *Receive, SendText.s, SendLen, ReceiveText.s, ReceiveLen, Pos
With *ComData
; Startup
\Status = #ComThread_Startup
\SendCount = 0
\ReceiveCount = 0
\ComID = OpenSerialPort(#PB_Any, \Port, \Baud, \Parity, \DataBit, \StopBit, \Handshake, \BufferSize, \BufferSize)
If \ComID
\Status = #ComThread_Running
Else
If \StatusCB
\StatusCB(#ComStatus_ErrorOpenPort, *ComData)
EndIf
\Status = #ComThread_Stopped
ProcedureReturn 0
EndIf
If \StatusCB
\StatusCB(#ComStatus_OpenPort, *ComData)
EndIf
*Send = AllocateMemory(\BufferSize)
*Receive = AllocateMemory(\BufferSize)
; Loop
Repeat
If \SendSignal
SendText = \SendText + \EndOfText
SendLen = StringByteLength(SendText, #PB_Ascii)
If SendLen <= \BufferSize
PokeS(*Send, SendText, SendLen, #PB_Ascii)
If WriteSerialPortData(\ComID, *Send, SendLen) = 0
\SendError = SerialPortError(\ComID)
If \StatusCB
\StatusCB(#ComStatus_ErrorSend, *ComData)
EndIf
Else
\SendError = 0
\SendCount + 1
EndIf
Else
If \StatusCB
\StatusCB(#ComStatus_ErrorDataSize, *ComData)
EndIf
EndIf
\SendSignal = #False
EndIf
ReceiveLen = AvailableSerialPortInput(\ComID)
If ReceiveLen
ReceiveLen = ReadSerialPortData(\ComID, *Receive, ReceiveLen)
If ReceiveLen = 0
\ReceiveError = SerialPortError(\ComID)
If \StatusCB
\StatusCB(#ComStatus_ErrorReceive, *ComData)
EndIf
Else
\ReceiveError = 0
EndIf
ReceiveText + PeekS(*Receive, ReceiveLen, #PB_Ascii)
Repeat
pos = FindString(ReceiveText, \EndOfText, 1, #PB_String_NoCase)
If pos
\ReceiveText = Left(ReceiveText, pos - 1)
ReceiveText = Mid(ReceiveText, pos + Len(\EndOfText))
\ReceiveCount + 1
If \ReceiveCB
\ReceiveCB(\ReceiveText, \Index)
EndIf
EndIf
Until pos = 0
EndIf
Delay(10)
Until \Exit
; Shutdown
CloseSerialPort(\ComID)
If \StatusCB
\StatusCB(#ComStatus_ClosePort, *ComData)
EndIf
FreeMemory(*Send)
FreeMemory(*Receive)
\Status = #ComThread_Stopped
\ComID = 0
\Exit = 0
ProcedureReturn 1
EndWith
EndProcedure
; ----
Procedure.s SerialPortErrorText(ErrorCode)
Protected r1.s
Select ErrorCode
Case #PB_SerialPort_RxOver : r1 = "An input buffer overflow has occurred."
Case #PB_SerialPort_OverRun : r1 = "A character-buffer overrun has occurred."
Case #PB_SerialPort_RxParity : r1 = "The hardware detected a parity error."
Case #PB_SerialPort_Frame : r1 = "The hardware detected a framing error."
Case #PB_SerialPort_Break : r1 = "The hardware detected a break condition."
Case #PB_SerialPort_TxFull : r1 = "The application tried to transmit a character but the output buffer was full."
Case #PB_SerialPort_IOE : r1 = "An I/O error occurred during communications with the device."
Case #PB_SerialPort_WaitingCTS : r1 = "Specifies whether transmission is waiting for the CTS (clear-To-send) signal to be sent."
Case #PB_SerialPort_WaitingDSR : r1 = "Specifies whether transmission is waiting for the DSR (Data-set-ready) signal to be sent."
Case #PB_SerialPort_WaitingRLSD : r1 = "Specifies whether transmission is waiting for the RLSD (receive-line-signal-detect) signal to be sent."
Case #PB_SerialPort_XoffReceived: r1 = "Specifies whether transmission is waiting because the XOFF character was received."
Case #PB_SerialPort_XoffSent : r1 = "Specifies whether transmission is waiting because the XOFF character was transmitted."
Case #PB_SerialPort_EOFSent : r1 = "Specifies whether the end-of-file (EOF) character has been received."
Default : r1 = "ErrorCode " + Hex(ErrorCode)
EndSelect
ProcedureReturn r1
EndProcedure
; ----
; Threaded String Helper
Procedure AllocateString(String.s)
Protected *mem
*mem = AllocateMemory(StringByteLength(String) + SizeOf(Character))
If *mem
PokeS(*mem, String)
EndIf
ProcedureReturn *mem
EndProcedure
Procedure.s FreeString(*Mem)
Protected result.s
If *Mem
result = PeekS(*Mem)
FreeMemory(*Mem)
EndIf
ProcedureReturn result
EndProcedure
; ----
Procedure COMGetAvailablePorts()
Protected NewList COMPortNameList.s()
Protected i.i, Directory.i, Com.i
ClearList(COMUsablePorts()) ; Clear previous usable ports
CompilerIf #PB_Compiler_OS = #PB_OS_Windows
For i = 1 To #COMMaxPorts
AddElement(COMPortNameList())
COMPortNameList() = "COM" + Str(i)
Next i
CompilerEndIf
CompilerIf #PB_Compiler_OS = #PB_OS_Linux
Directory = ExamineDirectory(#PB_Any, "/dev", "ttyUSB*")
If Directory
While NextDirectoryEntry(Directory)
AddElement(COMPortNameList())
COMPortNameList() = "/dev/" + DirectoryEntryName(Directory)
Wend
FinishDirectory(Directory)
EndIf
CompilerEndIf
CompilerIf #PB_Compiler_OS = #PB_OS_MacOS
Directory = ExamineDirectory(#PB_Any, "/dev", "tty.usbserial*")
If Directory
While NextDirectoryEntry(Directory)
AddElement(COMPortNameList())
COMPortNameList() = "/dev/" + DirectoryEntryName(Directory)
Wend
FinishDirectory(Directory)
EndIf
CompilerEndIf
ForEach COMPortNameList()
Com = OpenSerialPort(#PB_Any, COMPortNameList(), 115200, #PB_SerialPort_NoParity, 8, 1, #PB_SerialPort_NoHandshake, 1, 1)
If Com
AddElement(COMUsablePorts())
COMUsablePorts() = COMPortNameList()
CloseSerialPort(Com)
EndIf
Next
FreeList(COMPortNameList())
EndProcedure
; *****************************************************************************
;- Example
CompilerIf #PB_Compiler_IsMainFile
Global Dim ComData.udtComData(2)
Global LastListView.i
Enumeration EventCustomValue #PB_Event_FirstCustomValue
#My_Event_NewData
#My_Event_NewState
EndEnumeration
; ---------------------------------------------------------------------------
Procedure ReceiveCB(Text.s, Index.i)
PostEvent(#My_Event_NewData, 0, 0, Index, AllocateString(Text))
EndProcedure
Procedure MyEventNewDataCB()
Protected Index = EventType()
Protected Text.s = FreeString(EventData())
AddGadgetItem(20 + Index, -1, Text)
SetGadgetState(20 + Index, CountGadgetItems(20 + Index) - 1)
EndProcedure
BindEvent(#My_Event_NewData, @MyEventNewDataCB())
; ---------------------------------------------------------------------------
Procedure StatusCB(Status, *ComData.udtComData)
PostEvent(#My_Event_NewState, 0, 0, Status, *ComData)
EndProcedure
Procedure MyEventNewStateCB()
Protected Text.s, Status, *ComData.udtComData
Status = EventType()
*ComData = EventData()
Protected Index = *ComData\Index
Select Status
Case #ComStatus_OpenPort
Text = "Com" + Str(Index + 1) + " Status: Open Port " + *ComData\Port
SetGadgetText(50 + Index, "Off")
Case #ComStatus_ClosePort
Text = "Com" + Str(Index + 1) + " Status: Close Port " + *ComData\Port
SetGadgetText(50 + Index, "On")
Case #ComStatus_ErrorOpenPort
Text = "Com" + Str(Index + 1) + " Error: Open Port " + *ComData\Port
SetGadgetText(50 + Index, "On")
Case #ComStatus_ErrorSend
Text = "Com" + Str(Index + 1) + " Error Send: Port " + *ComData\Port + " - " + SerialPortErrorText(*ComData\SendError)
Case #ComStatus_ErrorReceive
Text = "Com" + Str(Index + 1) + " Error Receive: Port " + *ComData\Port + " - " + SerialPortErrorText(*ComData\ReceiveError)
Case #ComStatus_ErrorDataSize
Text = "Com" + Str(Index + 1) + " Error Send: Port " + *ComData\Port + " - Send data size too big."
EndSelect
If Bool(Text)
StatusBarText(0, 0, Text)
EndIf
EndProcedure
BindEvent(#My_Event_NewState, @MyEventNewStateCB())
; ---------------------------------------------------------------------------
Procedure LoadSettings()
COMGetAvailablePorts() ; Detect available ports before loading settings
If ListSize(COMUsablePorts()) = 0
MessageRequester("Error", "No usable COM ports detected!", #PB_MessageRequester_Error)
EndIf
If OpenPreferences("comport.ini")
For i = 0 To 2
PreferenceGroup("Com" + Str(i + 1))
Protected port.s = ReadPreferenceString("Port", "")
; Validate port against usable ports
Protected valid_port.b = #False
ForEach COMUsablePorts()
If COMUsablePorts() = port
valid_port = #True
Break
EndIf
Next
If valid_port
ComData(i)\Port = port
ElseIf ListSize(COMUsablePorts())
FirstElement(COMUsablePorts())
ComData(i)\Port = COMUsablePorts()
Else
ComData(i)\Port = ""
EndIf
ComData(i)\Baud = ReadPreferenceInteger("Baud", 115200)
ComData(i)\Parity = ReadPreferenceInteger("Parity", #PB_SerialPort_NoParity)
ComData(i)\DataBit = ReadPreferenceInteger("DataBit", 8)
ComData(i)\StopBit = ReadPreferenceFloat("StopBit", 1.0)
ComData(i)\Handshake = ReadPreferenceInteger("Handshake", #PB_SerialPort_NoHandshake)
ComData(i)\BufferSize = ReadPreferenceInteger("BufferSize", 2048)
Protected eot.s = ReadPreferenceString("EndOfText", "LF")
Select eot
Case "LF" : ComData(i)\EndOfText = #LF$
Case "CR" : ComData(i)\EndOfText = #CR$
Case "CRLF" : ComData(i)\EndOfText = #CRLF$
Default : ComData(i)\EndOfText = ""
EndSelect
Next
ClosePreferences()
Else
For i = 0 To 2
If ListSize(COMUsablePorts())
FirstElement(COMUsablePorts())
ComData(i)\Port = COMUsablePorts()
Else
ComData(i)\Port = ""
EndIf
ComData(i)\Baud = 115200
ComData(i)\Parity = #PB_SerialPort_NoParity
ComData(i)\DataBit = 8
ComData(i)\StopBit = 1.0
ComData(i)\Handshake = #PB_SerialPort_NoHandshake
ComData(i)\BufferSize = 2048
ComData(i)\EndOfText = #LF$
Next
EndIf
EndProcedure
Procedure SaveSettings()
If OpenPreferences("comport.ini");, #PB_Preference_NoTree)
For i = 0 To 2
PreferenceGroup("Com" + Str(i + 1))
WritePreferenceString("Port", ComData(i)\Port)
WritePreferenceInteger("Baud", ComData(i)\Baud)
WritePreferenceInteger("Parity", ComData(i)\Parity)
WritePreferenceInteger("DataBit", ComData(i)\DataBit)
WritePreferenceFloat("StopBit", ComData(i)\StopBit)
WritePreferenceInteger("Handshake", ComData(i)\Handshake)
WritePreferenceInteger("BufferSize", ComData(i)\BufferSize)
Protected eot.s
Select ComData(i)\EndOfText
Case #LF$ : eot = "LF"
Case #CR$ : eot = "CR"
Case #CRLF$ : eot = "CRLF"
Default : eot = "None"
EndSelect
WritePreferenceString("EndOfText", eot)
Next
ClosePreferences()
EndIf
EndProcedure
Procedure InitComport(Index.i)
With ComData(Index)
If \Status
ProcedureReturn 2 ; Already running
EndIf
If \Port = ""
StatusBarText(0, 0, "Com" + Str(Index + 1) + ": No valid port selected")
ProcedureReturn 0
EndIf
\StatusCB = @StatusCB()
\ReceiveCB = @ReceiveCB()
\ThreadID = CreateThread(@thComport(), @ComData(Index))
If Not \ThreadID
ProcedureReturn 0 ; Error create thread
Else
ProcedureReturn 1 ; ok
EndIf
EndWith
EndProcedure
Procedure EditSettings(Index.i)
Protected event.i, ok.i = 0, was_running.i = 0
Protected baudRates.s = "50,75,110,150,300,600,1200,1800,2400,4800,9600,19200,38400,57600,115200"
With ComData(Index)
was_running = Bool(\Status = #ComThread_Running)
If OpenWindow(1, #PB_Ignore, #PB_Ignore, 400, 300, "Settings for Com" + Str(Index + 1), #PB_Window_SystemMenu | #PB_Window_ScreenCentered, WindowID(0))
TextGadget(#PB_Any, 10, 10, 100, 20, "Port:")
ComboBoxGadget(100, 120, 10, 200, 20)
Protected selected_port_index.i = -1, j.i = 0
ForEach COMUsablePorts()
AddGadgetItem(100, -1, COMUsablePorts())
If COMUsablePorts() = \Port
selected_port_index = j
EndIf
j + 1
Next
If selected_port_index >= 0
SetGadgetState(100, selected_port_index)
ElseIf ListSize(COMUsablePorts())
SetGadgetState(100, 0)
Else
AddGadgetItem(100, -1, "No ports available")
SetGadgetState(100, 0)
EndIf
TextGadget(#PB_Any, 10, 40, 100, 20, "Baud Rate:")
ComboBoxGadget(101, 120, 40, 200, 20)
Protected baud.s
For j = 1 To CountString(baudRates, ",") + 1
baud = StringField(baudRates, j, ",")
AddGadgetItem(101, -1, baud)
Next
SetGadgetText(101, Str(\Baud))
TextGadget(#PB_Any, 10, 70, 100, 20, "Parity:")
ComboBoxGadget(102, 120, 70, 200, 20)
AddGadgetItem(102, -1, "None")
AddGadgetItem(102, -1, "Odd")
AddGadgetItem(102, -1, "Even")
AddGadgetItem(102, -1, "Mark")
AddGadgetItem(102, -1, "Space")
SetGadgetState(102, \Parity)
TextGadget(#PB_Any, 10, 100, 100, 20, "Data Bits:")
ComboBoxGadget(103, 120, 100, 200, 20)
AddGadgetItem(103, -1, "5")
AddGadgetItem(103, -1, "6")
AddGadgetItem(103, -1, "7")
AddGadgetItem(103, -1, "8")
SetGadgetState(103, \DataBit - 5)
TextGadget(#PB_Any, 10, 130, 100, 20, "Stop Bits:")
ComboBoxGadget(104, 120, 130, 200, 20)
AddGadgetItem(104, -1, "1")
AddGadgetItem(104, -1, "1.5")
AddGadgetItem(104, -1, "2")
Protected stop_index.i
Select \StopBit
Case 1.0 : stop_index = 0
Case 1.5 : stop_index = 1
Case 2.0 : stop_index = 2
Default : stop_index = 0
EndSelect
SetGadgetState(104, stop_index)
TextGadget(#PB_Any, 10, 160, 100, 20, "Handshake:")
ComboBoxGadget(105, 120, 160, 200, 20)
AddGadgetItem(105, -1, "None")
AddGadgetItem(105, -1, "RTS")
AddGadgetItem(105, -1, "RTS/CTS")
AddGadgetItem(105, -1, "Xon/Xoff")
Select \Handshake
Case #PB_SerialPort_NoHandshake : SetGadgetState(105, 0)
Case #PB_SerialPort_RtsHandshake : SetGadgetState(105, 1)
Case #PB_SerialPort_RtsCtsHandshake : SetGadgetState(105, 2)
Case #PB_SerialPort_XonXoffHandshake : SetGadgetState(105, 3)
EndSelect
TextGadget(#PB_Any, 10, 190, 100, 20, "Buffer Size:")
StringGadget(106, 120, 190, 200, 20, Str(\BufferSize))
TextGadget(#PB_Any, 10, 220, 100, 20, "End Of Text:")
ComboBoxGadget(107, 120, 220, 200, 20)
AddGadgetItem(107, -1, "None")
AddGadgetItem(107, -1, "CR")
AddGadgetItem(107, -1, "LF")
AddGadgetItem(107, -1, "CRLF")
Protected eot_state.i
Select \EndOfText
Case "" : eot_state = 0
Case #CR$ : eot_state = 1
Case #LF$ : eot_state = 2
Case #CRLF$ : eot_state = 3
EndSelect
SetGadgetState(107, eot_state)
ButtonGadget(110, 100, 260, 80, 25, "OK")
ButtonGadget(111, 200, 260, 80, 25, "Cancel")
Repeat
event = WaitWindowEvent()
If event = #PB_Event_Gadget
Select EventGadget()
Case 110 : ok = 1 : Break
Case 111 : Break
EndSelect
EndIf
Until event = #PB_Event_CloseWindow
If ok
If ListSize(COMUsablePorts()) And GetGadgetState(100) >= 0
SelectElement(COMUsablePorts(), GetGadgetState(100))
\Port = COMUsablePorts()
Else
\Port = ""
EndIf
\Baud = Val(GetGadgetText(101))
\Parity = GetGadgetState(102)
\DataBit = Val(GetGadgetItemText(103, GetGadgetState(103)))
Select GetGadgetState(104)
Case 0 : \StopBit = 1.0
Case 1 : \StopBit = 1.5
Case 2 : \StopBit = 2.0
EndSelect
Select GetGadgetState(105)
Case 0 : \Handshake = #PB_SerialPort_NoHandshake
Case 1 : \Handshake = #PB_SerialPort_RtsHandshake
Case 2 : \Handshake = #PB_SerialPort_RtsCtsHandshake
Case 3 : \Handshake = #PB_SerialPort_XonXoffHandshake
EndSelect
\BufferSize = Val(GetGadgetText(106))
Protected s.s = GetGadgetItemText(107, GetGadgetState(107))
Select s
Case "None" : \EndOfText = ""
Case "CR" : \EndOfText = #CR$
Case "LF" : \EndOfText = #LF$
Case "CRLF" : \EndOfText = #CRLF$
EndSelect
EndIf
CloseWindow(1)
EndIf
EndWith
If ok
If ComData(Index)\ThreadID And IsThread(ComData(Index)\ThreadID)
ComData(Index)\Exit = 1
While ComData(Index)\Status <> #ComThread_Stopped
Delay(10)
Wend
EndIf
If was_running
InitComport(Index)
EndIf
EndIf
EndProcedure
Procedure Main()
LoadSettings()
For i = 0 To 2
ComData(i)\Index = i
Next
Protected Event.i, Text.s, j.i, gadget.i
If OpenWindow(0, #PB_Ignore, #PB_Ignore, 800, 600, "Multi Comport Manager", #PB_Window_SystemMenu)
CreateStatusBar(0, WindowID(0))
AddStatusBarField(#PB_Ignore)
PanelGadget(10, 0, 0, 800, 540)
For i = 0 To 2
AddGadgetItem(10, -1, "Com " + Str(i + 1))
ListViewGadget(20 + i, 0, 0, 790, 440)
StringGadget(30 + i, 5, 445, 590, 25, "")
ButtonGadget(40 + i, 605, 445, 90, 25, "Send")
ButtonGadget(50 + i, 700, 445, 90, 25, "On")
ButtonGadget(60 + i, 5, 480, 90, 25, "Settings")
Next
CreatePopupMenu(1)
MenuItem(101, "Copy ListView")
Repeat
Event = WaitWindowEvent()
Select Event
Case #PB_Event_CloseWindow
For i = 0 To 2
If ComData(i)\Status
ComData(i)\Exit = 1
EndIf
Next
For i = 0 To 2
While ComData(i)\Status <> #ComThread_Stopped
Delay(100)
Wend
Next
SaveSettings()
Break
Case #PB_Event_Gadget
gadget = EventGadget()
If gadget >= 20 And gadget <= 22 And EventType() = #PB_EventType_RightClick
LastListView = gadget
DisplayPopupMenu(1, WindowID(0))
ElseIf gadget >= 40 And gadget <= 42 ; Send buttons
Protected send_index.i = gadget - 40
If ComData(send_index)\Status = #ComThread_Running And ComData(send_index)\SendSignal = #False
ComData(send_index)\SendText = GetGadgetText(30 + send_index)
ComData(send_index)\SendSignal = #True
EndIf
ElseIf gadget >= 50 And gadget <= 52 ; On/Off buttons
Protected toggle_index.i = gadget - 50
If ComData(toggle_index)\Status
ComData(toggle_index)\Exit = 1
Else
If Not InitComport(toggle_index)
StatusBarText(0, 0, "Com" + Str(toggle_index + 1) + ": Error Create Thread")
EndIf
EndIf
ElseIf gadget >= 60 And gadget <= 62 ; Settings buttons
Protected settings_index.i = gadget - 60
EditSettings(settings_index)
EndIf
Case #PB_Event_Menu
Select EventMenu()
Case 101 ; popup menu
Text = ""
For j = 0 To CountGadgetItems(LastListView) - 1
Text + GetGadgetItemText(LastListView, j) + #CRLF$
Next
SetClipboardText(Text)
EndSelect
EndSelect
ForEver
EndIf
EndProcedure : Main()
CompilerEndIf