looking for feedback on my Class SerialCOM Lib

Post your working scripts, libraries and tools for AHK v1.1 and older
ShatterCoder
Posts: 69
Joined: 06 Oct 2016, 15:57

looking for feedback on my Class SerialCOM Lib

Post by ShatterCoder » 17 Nov 2021, 12:06

I have been doing a fair bit of work with serial communications at work lately and decided to bite the bullet and write up a class lib for that purpose. I'm basing my class lib off a script that I had found here in the forums years ago here: https://autohotkey.com/board/topic/26231-serial-com-port-console-script/page-2
Right Now the lib is in a usable state. I'm currently looking for feedback on the overall structure and usability of the lib from you guys. My own use case has been along some pretty set lines, so I expect i'm missing some usability.
In the spirit of helping everyone else, my ambition is to make this a class lib that makes it relatively easy for anyone with a bit of experience with ahk to get rolling with any given serial/COM project they may wish to pursue.

So here is my initial offer as far as methods I believe will be useful (keep in mind there are still a few lines in the code which use OLD obsolete commands which I have not yet replaced. The most obvious of these being the SetFormat command. I do plan to replace these but have not had a chance to do full testing with the updated code yet) Again I'm mostly looking for feedback on structure and usability. But I'll take any constructive criticism.

Code: Select all

/*
Class SerialCOM
Created by ShatterCoder
This class was built on the original work of the creator of Seral.ahk https://autohotkey.com/board/topic/26231-serial-com-port-console-script/page-2

Methods:
	
	***************************************************************************************************************************************************************
	Event_Parse_Start(Register_Function := "__NONE__", Delims := "`r`n", Omit_Chars := "", Seperator := "`n", Poll_Interval := 100)
	
	This Method is intended to continuously recieve and parse data from the COM port specified by the obj.COM_port property
	
		Resister_Function - Pass the name of a function no paren's () to register it to be called each time a "chunk" of data has been recieved 
			Chunks are defined by the Delims Param defined below
			Default: No function is called, instead all data is stored in obj.AllRecieved and obj._Recieved_Partial will have any partial messages remaining
			if you specify a function name each time a chunk is parsed it will call your function and pass it the chunk as the only parameter
			
		Delims - This parameter is used to pass the characters you would like to use to define the end of a "chunk" of serial communication 
			Defualt: `r`n
		Omit_Chars - Used to signify characters you would like to ommit from your comminication
			Default: by default no chars are ommited
		Seperator - Used to specify how chunks are seperated in this.AllRecieved 
			Default: `n
		Poll_Interval - interval in ms to wait between each reading of the serial register
			Default: 100 ms
			
		Example Usage: 
			Com3 := New SerialCOM("COM3")
			Com3.Event_Parse_Start("MyFunc", "`n")
			
			MyFunc(Input)
			{
				msgbox, % "sweet! just recieved the following: `n" Input
				return
			}
			
			Esc::
			Com3.Event_Parse_Stop()
			return
	
	***************************************************************************************************************************************************************
	Event_Parse_Stop()
		Simple method to stop the event reader.
		
	***************************************************************************************************************************************************************	
	Send_Message(asciiMessage)
		One stop method will open the COM port, send the message, then close the COM port once finished, 
		meant for infrequently sent communications.
	
	***************************************************************************************************************************************************************
	Begin_Send_Stream() ; send_to_stream(asciiMessage) ; Close_Send_Stream()
		These 3 methods are used to send constant data, obj.Begin_Send_Stream() Opens the port and allows you to send data via
		obj.send_to_stream() When you are finished sending simply call the obj.Close_Send_Stream() method to Close out the port. 
		
		asciiMessage - any number or string you wish to send over the COM port. it's automatically converted to hex and sent
		
		Example Usage:
			MyCom := New SerialCOM()
			MyCom.Begin_Send_Stream()
			
			Loop, 1000
			{
				MyCom.send_to_stream(A_Now)
				sleep, 1000
			}
			
			MyCom.Close_Send_Stream()
		
		***************************************************************************************************************************************************************

*/

Class SerialCOM
{
	__AscToHex(str) {
		Return str="" ? "":Chr((Asc(str)>>4)+48) Chr((x:=Asc(str)&15)+(x>9 ? 55:48)) this.__AscToHex(SubStr(str,2))
	}
	
	__New( Port := "COM1", Baud := 9600, Parity := "N", Data := 8, Stop := 1)
	{
		this._Recieved_Partial := ""
		this.COM_Port := Port
		this.Baud_Rate := Baud
		this.Parity := "N"
		this.Data_Bits := Data
		this.Stop_Bits := Stop
		this.COM_FileHandle := ""
		this.Bytes_Recieved := ""
	}
	
	__Delete()
	{
		if  this.COM_FileHandle != ""
			this.__Close_COM()
	}
	__Open_Port() 
	{
		if this.COM_FileHandle != ""
			return -1
		Settings_String := this.COM_Port ":baud=" this.Baud_Rate " parity=" this.Parity " data=" this.Data_Bits " stop=" this.Stop_Bits " dtr=Off"
		;###### Build COM DCB ######
		;Creates the structure that contains the COM Port number, baud rate,...
		VarSetCapacity(DCB, 28)
		While (BCD_Result != 1)
		{
			BCD_Result := DllCall("BuildCommDCB" ,"str" , Settings_String,"UInt", &DCB)
			if (BCD_Result != 1)
			{
				MsgBox, 262196,COM failure ,it appears that the device may not be connected to %COM_Port%. Would you like to change COM ports?
				IfMsgBox, Yes
				{
					InputBox, COM_Port, Select New COM port, Which COM port should be selected(COM1`, COM2 etc. Note ALLCAPS and no spaces)?
				}
				IfMsgBox, No
					return 0
			}
		}
		COM_Port_Len := StrLen(This.COM_Port)  ;For COM Ports > 9 \\.\ needs to prepended to the COM Port name.
		If COM_Port_Len > 4)                   ;So the valid names are
			This.COM_Port := "\\.\" This.COM_Port             ; ... COM8  COM9   \\.\COM10  \\.\COM11  \\.\COM12 and so on...
		Else                                          ;
			This.COM_Port := This.COM_Port  
		
		
		This.COM_FileHandle := DllCall("CreateFile"
       ,"Str" , This.COM_Port     ;File Name         
       ,"UInt", 0xC0000000   ;Desired Access
       ,"UInt", 3            ;Share Mode
       ,"UInt", 0            ;Security Attributes
       ,"UInt", 3            ;Creation Disposition
       ,"UInt", 0            ;Flags And Attributes
       ,"UInt", 0            ;Template File
       ,"Cdecl Int")
		While (this.COM_FileHandle < 1)
		{
			This.COM_FileHandle := DllCall("CreateFile"
		   ,"Str" , This.COM_Port     ;File Name         
		   ,"UInt", 0xC0000000   ;Desired Access
		   ,"UInt", 3            ;Share Mode
		   ,"UInt", 0            ;Security Attributes
		   ,"UInt", 3            ;Creation Disposition
		   ,"UInt", 0            ;Flags And Attributes
		   ,"UInt", 0            ;Template File
		   ,"Cdecl Int")
		   if (A_index > 20) ;times out after ~ 2 seconds
			{
				MsgBox, % "There is a problem with Serial Port communication. `nFailed Dll CreateFile, COM_FileHandle=" this.COM_FileHandle " `nThe Script Will Now Exit."
				Exit
			}
			sleep, 100
		}
		SCS_Result := DllCall("SetCommState"
			   ,"UInt", this.COM_FileHandle ;File Handle
			   ,"UInt", &DCB)          ;Pointer to DCB structure
		If (SCS_Result <> 1)
		{
			MsgBox, There is a problem with Serial Port communication. `nFailed Dll SetCommState, SCS_Result=%SCS_Result% `nThe Script Will Now Exit.
			this.__Close_COM()
			ErrorDrivenRetry := 1
			return
		}

		;###### Create the SetCommTimeouts Structure ######
		ReadIntervalTimeout        = 0xffffffff
		ReadTotalTimeoutMultiplier = 0x00000000
		ReadTotalTimeoutConstant   = 0x00000000
		WriteTotalTimeoutMultiplier= 0x00000000
		WriteTotalTimeoutConstant  = 0x00000000

		VarSetCapacity(Data, 20, 0) ; 5 * sizeof(DWORD)
		NumPut(ReadIntervalTimeout,         Data,  0, "UInt")
		NumPut(ReadTotalTimeoutMultiplier,  Data,  4, "UInt")
		NumPut(ReadTotalTimeoutConstant,    Data,  8, "UInt")
		NumPut(WriteTotalTimeoutMultiplier, Data, 12, "UInt")
		NumPut(WriteTotalTimeoutConstant,   Data, 16, "UInt")

		;###### Set the COM Timeouts ######
		SCT_result := DllCall("SetCommTimeouts"
			 ,"UInt", this.COM_FileHandle ;File Handle
			 ,"UInt", &Data)         ;Pointer to the data structure
		If (SCT_result <> 1)
		{
			MsgBox, There is a problem with Serial Port communication. `nFailed Dll SetCommState, SCT_result=%SCT_result% `nThe Script Will Now Exit.
			this.__Close_COM()
			Exit
		}
		
	}
	
	__Close_COM()
	{
		;###### Close the COM File ######
		CH_result := DllCall("CloseHandle", "UInt", this.COM_FileHandle)
		If (CH_result <> 1)
			MsgBox, % "Failed Dll CloseHandle CH_result=" CH_result
		this.COM_FileHandle := ""
	  Return
	}
	
	__Write_to_COM(Message)
	{
		SetFormat, Integer, DEC
		Data_Length := 1
		Loop, Parse, Message, `,
		{
			Data_Length ++
		}
	  
	  ;Set the Data buffer size, prefill with 0xFF.
		VarSetCapacity(Data, Data_Length, 0xFF)
	  
		Loop, Parse, Message, `,
		{
			NumPut(A_loopfield, Data, (A_index-1), "UChar")
		}
	  ;###### Write the data to the COM Port ######
		WF_Result := DllCall("WriteFile"
		   ,"UInt" , this.COM_FileHandle ;File Handle
		   ,"UInt" , &Data          ;Pointer to string to send
		   ,"UInt" , Data_Length    ;Data Length
		   ,"UInt*", Bytes_Sent     ;Returns pointer to num bytes sent
		   ,"Int"  , "NULL")
		If (WF_Result != 1 or Bytes_Sent != Data_Length)
		{
			Sleep, 10
			WF_Result := DllCall("WriteFile"
			   ,"UInt" , this.COM_FileHandle ;File Handle
			   ,"UInt" , &Data          ;Pointer to string to send
			   ,"UInt" , Data_Length    ;Data Length
			   ,"UInt*", Bytes_Sent     ;Returns pointer to num bytes sent
			   ,"Int"  , "NULL")
			If (WF_Result <> 1 or Bytes_Sent <> Data_Length)
				MsgBox, % "Failed Dll WriteFile to " this.COM_Port ", result=" WF_Result " `nData Length=" Data_Length " `nBytes_Sent=" Bytes_Sent
		}
	}
	
	__Read_from_COM(Num_Bytes)
	{
		SetFormat, Integer, HEX
		this.Bytes_Received := 0
	  ;Set the Data buffer size, prefill with 0x55 = ASCII character "U"
	  ;VarSetCapacity won't assign anything less than 3 bytes. Meaning: If you
	  ;  tell it you want 1 or 2 byte size variable it will give you 3.
		Data_Length  := VarSetCapacity(Data, Num_Bytes, 0x55)
		;~ msgbox, Data_Length=%Num_Bytes%
		;~ MsgBox, % this.COM_FileHandle
	  ;###### Read the data from the COM Port ######
		Read_Result := DllCall("ReadFile"
		   ,"UInt" , this.COM_FileHandle   ; hFile
		   ,"Str"  , Data             ; lpBuffer
		   ,"Int"  , Num_Bytes        ; nNumberOfBytesToRead
		   ,"UInt*", Bytes_Received   ; lpNumberOfBytesReceived
		   ,"Int"  , 0)               ; lpOverlapped
		   sleep, 10
		   this.Bytes_Received := Bytes_Received
		If (Read_Result != 1)
		{
			MsgBox, % "There is a problem with Serial Port communication. `nFailed Dll ReadFile on " COM_Port ", result=" Read_Result " - The Script Will Now Exit."
			this.__Close_COM()
			Exit
		}
		;~ if this.Bytes_Received != 0
			;~ MsgBox, % this.Bytes_Received
		i := 0
		Data_HEX := ""
		Loop % this.Bytes_Received
		{
			;~ MsgBox, % Data
			;First byte into the Rx FIFO ends up at position 0
			Data_HEX_Temp := NumGet(Data, i, "UChar") ;Convert to HEX byte-by-byte
			Data_HEX_Temp := SubStr(Data_HEX_Temp, 3)
			;~ StringTrimLeft, Data_HEX_Temp, Data_HEX_Temp, 2 ;Remove the 0x (added by the above line) from the front
			;If there is only 1 character then add the leading "0'
			Length := StrLen(Data_HEX_Temp)
			If (Length =1)
				Data_HEX_Temp := "0" Data_HEX_Temp
			i++
			;Put it all together
			Data_HEX := Data_HEX  Data_HEX_Temp
		}
		SetFormat, Integer, DEC
		if (Data_HEX != "")
		{
			this.Last_Recieved_HEX := Data_HEX
			;~ Clipboard := Data_HEX
			;~ MsgBox, pause
		}
		Return Data_HEX
	}
	
	Event_Parse_Start(Register_Function := "__NONE__", Delims := "`r`n", Omit_Chars := "", Seperator := "`n", Poll_Interval := 100)
	{
		this._Event_delims := Delims
		this._Event_Omit_Chars := Omit_Chars
		this._Event_Registered_Function := Register_Function
		this._Event_Seperator := Seperator
		this.__Open_Port() 
		this.__start_timer(Poll_Interval)
		;~ settimer, this.__Check_Read_Register, Poll_Interval
	}
	
	__start_timer(Poll_Interval)
	{
		mthd := this.__Check_Read_Register.bind(this)
		this.__timer_handle := mthd
		SetTimer, % mthd, % Poll_Interval
	}
	
	Event_Parse_Stop()
	{
		mthd := this.__timer_handle
		settimer, % mthd, off
		this.__Close_COM()
	}
	
	__Check_Read_Register()
	{
		mthd := this.__timer_handle
		settimer, % mthd, off
		ReceivedMessage := this.__Read_from_COM("0xFF")
		Translated := this.__HexToASCII(ReceivedMessage)
		AsciiOut := ""
		Registered_Function := this._Event_Registered_Function
		if this._Recieved_Partial != ""
			Translated := this._Recieved_Partial Translated
		test := StrSplit(Translated, this._Event_delims)
		if (test.count() > 1)
		{
			loop, Parse, Translated,% this._Event_delims, % this._Event_Omit_Chars
			{
				if (A_index = test.count() && A_LoopField != "" ) 
					this._Recieved_Partial := A_Loopfield ;this is wrong need a clear head to figure out the right way to do it. 
				if (A_loopfield != "")
				{
					AsciiOut .= A_loopfield this._Event_Seperator
					this._Recieved_Partial := ""
					this.AllRecieved .= AsciiOut
				}
				if (this._Event_Registered_Function != "__NONE__" && A_LoopField != "")
					%Registered_Function%(A_loopfield)
				
				
			}
		}
		else
			this._Recieved_Partial := Translated
		
		SetTimer, % mthd, % Poll_Interval
		return
	}
	
	__HexToASCII(ReceivedMessage)
	{
		loopcount := StrLen(ReceivedMessage) / 2
		AsciiTranslation := ""
		loop, % loopcount
		{
			CurrentHex := SubStr(ReceivedMessage, 1, 2)
			;~ StringMid, CurrentHex, ReceivedMessage, 1, 2
			ReceivedMessage := SubStr(ReceivedMessage, 3)
			;~ StringTrimLeft, ReceivedMessage, ReceivedMessage, 2
			CurrentHex := "0x" CurrentHex ;format the hex code so ahk will recognize it as hex
			CurrentAscii := chr(CurrentHex)
			AsciiTranslation .= CurrentAscii ;creates a single raw string of translated (into ASCII) text. Further formating may be needed
		}
		return AsciiTranslation
	}
	
	Send_Message(asciiMessage)
	{
		rawHex := this.__AscToHex(asciiMessage)
		hexLngth := % StrLen(rawHex)/2
		hexLngth :=Floor(hexLngth)
		this.__Open_Port() 
		Message := "0x"
		while, hexLngth>A_Index-1
		{
		  if(A_index > 1)
			Message .= ", 0x"
		  StringMid, ValueToAdd, rawHex, A_Index*2-1, 2
		  Message .= ValueToAdd
		}
		this.__Write_to_COM(Message)
		this.__Close_COM()
	}
	
	Begin_Send_Stream()
	{
		this.__Open_Port()
		return 1
	}
	
	Close_Send_Stream()
	{
		this.__Close_COM()
		return 1
	}
	
	send_to_stream(asciiMessage)
	{
		rawHex := this.__AscToHex(asciiMessage)
		hexLngth := % StrLen(rawHex)/2
		hexLngth :=Floor(hexLngth)
		this.__Open_Port() 
		Message := "0x"
		while, hexLngth>A_Index-1
		{
		  if(A_index > 1)
			Message .= ", 0x"
		  StringMid, ValueToAdd, rawHex, A_Index*2-1, 2
		  Message .= ValueToAdd
		}
		this.__Write_to_COM(Message)
	}
}

Return to “Scripts and Functions (v1)”