Class Serial

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

Class Serial

Post by ShatterCoder » 24 Aug 2022, 15:41

I created a class for sending and receiving ASCII via COM port, designed for ease of use rather than super efficiency. I use this for talking to different hardware at work, but I would imagine that it would be pretty handy for arduino projects too.

simple example usage:

Code: Select all

COM1 := new Serial("COM1")

;the following listens on COM1 and calls MyFunc when ever a newline char is detected. Then passing the message received
;by default it polls the COM port every 100ms to see if anything has been sent
COM1.Event_Parse_Start("MyFunc", "`n") 

MyFunc(Recv) ;called any time a full message is received
{
	msgbox, % "sweet! just recieved the following: `n" Recv
	return
}

Esc::
COM1.Send_Message("Thanks! I'm out now") ;send notification that you are closing your connection
COM1.Event_Parse_Stop()
return
Full Lib:

Code: Select all

/*
Class Serial
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

Purpose:
	This class was created to make sending and recieving serial data as painless as possible using ASCII input and output. It was not desiged for efficiency, but rather ease of use.


Methods:

	***************************************************************************************************************************************************************
        __New( Port := "COM1", Baud := 9600, Parity := "N", Data := 8, Stop := 1)
	
	Used to create an instance of the class, this is where the connection is defined. 
		
		Port - String containing the COM port you wish to connect to 
			Default: COM1
			
		Baud - Connection speed for sending and recieving data
			Default: 9600
		
		Parity - Set parity mode: Valid options are NONE, EVEN, ODD, SPACE, or MARK
			Default: NONE
		
		Data - Set binary data length per character , using 7 will only allow the first 127 ASCII chars only, 8 Allows the full 256 ASCII char set
			Default: 8
		
		Stop - Set the binary bit that ends a character packet, The opposite bit will be used to start it
			Default: 1 (0 is the Start bit)
			
		Example Usage:
			COM3 := New Serial("COM3", 115200) ;This creates a new class instance that will connect to COM3 and set the baud rate at 115200
			
	***************************************************************************************************************************************************************
        
	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 (string) as the only parameter
			See Example Usage below for an example of this

		Delims - This parameter is used to pass the characters you would like to use to define the end of a "chunk" of serial communication
			Note: an array of delims may be passed if you desire to have the data parsed based on more than one criteria
			example: Event_Parse_Start("MyFunc",["`r`r", "%"])
			if the data stream contains:%CAN81NAC%%CAN42NAC%PP000500000000001P004000035000360`r`r
			MyFunc will be triggered 3 times with CAN81NAC, CAN42NAC, PP000500000000001P004000035000360 passed to it respectively
			or using Event_Parse_Start("MyFunc",["`r`r", "NAC%"], "%CAN") you would get 81, 42, PP000500000000001P004000035000360 passed to it respectively

			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, and disconnect from the COM device.

	***************************************************************************************************************************************************************
	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, 60
			{
				MyCom.send_to_stream(A_Now)
				sleep, 1000
			}

			MyCom.Close_Send_Stream()

	***************************************************************************************************************************************************************
	Properties:

		This.COM_FileHandle
			A pointer to the ComPort object -- this will be blank if not connected
		this.Chunk
			String -- Contains the last Chunk recieved while Event_Parse_Start() was active
		This.AllRecieved
			String -- Contains all data recieved while Event_Parse_Start() was active sperated by which ever seperator was specified (`n by default)

*/

Class Serial
{
	__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 := Parity
		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)
	{
		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)
	{
		this.Bytes_Received := 0
	  ;Set the Data buffer size, prefill with 0x55 = ASCII character "U"
		VarSetCapacity(Data, Num_Bytes, 0x55)
		;~ Num_Bytes := Format("{:X}", Num_Bytes)
	  ;###### 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
		}
		i := 0
		Data_HEX := ""
		Loop % this.Bytes_Received
		{
			;First byte into the Rx FIFO ends up at position 0
			Data_HEX_Temp := Format("{:x}", NumGet(Data, i, "UChar")) ;Convert to HEX byte-by-byte
			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
		}
		if (Data_HEX != "")
			this.Last_Recieved_HEX := Data_HEX
		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)
	}

	__start_timer(Poll_Interval)
	{
		this.__timer_handle := mthd := this.__Check_Read_Register.bind(this)
		SetTimer, % mthd, % Poll_Interval
	}

	Event_Parse_Stop()
	{
		if (this.__timer_handle != "")
		{
			mthd := this.__timer_handle
			settimer, % mthd, off
			this.__timer_handle := ""
		}
		if (this.COM_FileHandle != "")
			this.__Close_COM()
	}

	__Check_Read_Register() ;called by the timer, this function reads the serial buffer and updates this.AllRecieved and this._Recieved_Partial. Also watches for "chunks" defined by delims and calls registered function when a chunk is found (passing the chunk to the function)
	{
		static Partial
		mthd := this.__timer_handle
		settimer, % mthd, off
		ReceivedMessage := this.__Read_from_COM("0xFF")
		partial := this._Recieved_Partial .= Translated := this.__HexToASCII(ReceivedMessage)
		Registered_Function := this._Event_Registered_Function
		if (!IsObject(this._Event_delims))
		{
			if (InStr(this._Recieved_Partial, this._Event_delims))
			{
				parts := StrSplit(this._Recieved_Partial, this._Event_delims, this._Event_Omit_Chars )
				loop, % parts.count()
				{
					if (A_index = parts.count() && A_index > 1)
					  this._Recieved_Partial := parts[A_index]
					else
					{
						var := parts[A_index]
						if var is not space
						{
							this.AllRecieved .= this.Chunk := parts[A_index] this._Event_Seperator
							if (this._Event_Registered_Function != "__NONE__")
								%Registered_Function%(this.Chunk)
						}
					}
				}
			}
		}
		else
		{
			loop, % this._Event_delims.count()
			{
				if (InStr(this._Recieved_Partial, this._Event_delims[A_index]))
				{
					parts := StrSplit(this._Recieved_Partial, this._Event_delims[A_index], this._Event_Omit_Chars)
					loop, % parts.count()
					{
						if (A_index = parts.count() && A_index > 1)
						{
							obj := this.__Check_Delims(parts[A_index], this._Event_delims)
							if (obj[1] != "")
								for k, v in obj[1]
								{
									this.AllRecieved .= this.Chunk := v this._Event_Seperator
									if (this._Event_Registered_Function != "__NONE__")
										%Registered_Function%(v)
								}
							this._Recieved_Partial := obj[2]
						}
						else if parts[A_index] != ""
						{
							obj := this.__Check_Delims((this.Chunk := parts[A_index]), this._Event_delims)
							if (obj[1] != "")
								for k, v in obj[1]
								{
									this.Chunk := v
									this.AllRecieved .= v this._Event_Seperator
									if (this._Event_Registered_Function != "__NONE__")
										%Registered_Function%(this.Chunk)
								}
							this.Chunk := obj[2]
							this.AllRecieved .= obj[2] this._Event_Seperator
							if (this._Event_Registered_Function != "__NONE__")
								%Registered_Function%(obj[2])
						}

					}
				}
			}
		}
		SetTimer, % mthd, % Poll_Interval
		return
	}

	__Check_Delims(str, delims)
	{
		ret_obj := []
		chunk := []
		Partial := str
		loop, % delims.count() - 1
		{
			if (pos := InStr(str, delims[A_index + 1]))
			{
				parts := StrSplit(str, delims[A_index + 1])
				loop, % parts.count()
				{
					if (A_index = parts.count() && A_index > 1)
						Partial := parts[A_index]
					else if parts[A_index] != ""
						chunk.push(parts[A_index])
				}
				return ret_obj := [chunk, Partial]
			}
		else
            Partial := str
		}
		return ret_obj := [chunk, Partial]
    }

	__HexToASCII(ReceivedMessage)
	{
		loopcount := StrLen(ReceivedMessage) / 2
		AsciiTranslation := ""
		loop, % loopcount
		{
			CurrentAscii := chr(CurrentHex := "0x" SubStr(ReceivedMessage, 1, 2)) ;take a byte, convert from Hex to Ascii
			ReceivedMessage := SubStr(ReceivedMessage, 3) ;remove translated byte
			AsciiTranslation .= CurrentAscii ;creates a single raw string of translated (into ASCII) text. Further formating may be needed
		}
		return AsciiTranslation
	}

	Send_Message(asciiMessage)
	{

		this.__Open_Port()
		this.send_to_stream(asciiMessage)
		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 := Floor(StrLen(rawHex)/2)
		Message := "0x"
		while, hexLngth>A_Index-1
		{
			if(A_index > 1)
				Message .= ", 0x"
			ValueToAdd := substr(rawHex, A_index *2-1, 2)
			Message .= ValueToAdd
		}
		this.__Write_to_COM(Message)
	}
}
Last edited by ShatterCoder on 07 Sep 2022, 15:58, edited 1 time in total.

hasantr
Posts: 933
Joined: 05 Apr 2016, 14:18
Location: İstanbul

Re: Class Serial

Post by hasantr » 27 Aug 2022, 22:22

Thank you. I will work with it.

ShatterCoder
Posts: 88
Joined: 06 Oct 2016, 15:57

Re: Class Serial

Post by ShatterCoder » 07 Sep 2022, 15:37

I realized I had not documented the initialization effectively. I'm sure i've made other mistakes along the way that are not obvious to me because my use case for this class is pretty limited. Please let me know if you find bugs or have suggestions.

ElectroLund
Posts: 3
Joined: 17 Nov 2020, 12:14

Re: Class Serial

Post by ElectroLund » 04 Jul 2023, 12:10

Amazing work! I was unable to get the Adafruit version (Serial.c and Arduino.c) to work for some reason. Even with a serial port sniffer, no ASCII packets ever got sent with that library. I suspect it had something to do with the packets needing to be binary instead?

Anyway, nice work! It's mostly what I'm looking for. One issue I'm having is with how to properly use the separator feature. Your documentation implies that the event function will segment the received payload by the given separator. How then do we call the segments? Or know how many segments there are? Not clear.

Thanks!!

User avatar
iilabs
Posts: 296
Joined: 07 Jun 2020, 16:57

Re: Class Serial

Post by iilabs » 04 Oct 2023, 09:11

Can you please help me figure out how to connect my Arduino to read serial? I was able to move my com port to 2 since I've read lib cant handle com ports > 9? I am still trying to figure out how to get data from the arduino. When you discuss ASCII basically I need to send ascii codes instead of typical serial.println strings from arduino? Do you have an arduino example code fitting with your post?

Post Reply

Return to “Scripts and Functions (v1)”