Skip to content

Example Serial Terminal


Example Serial Terminal

#SingleInstance Off

#Include <GridGUI>
SetBatchLines, -1

WM_DEVICECHANGE := 0x0219

if(!FileExist("plink.exe")) {
    UrlDownloadToFile, https://the.earth.li/~sgtatham/putty/latest/w64/plink.exe, plink.exe
}

Global plinkprocess := 0

ports := GetCOMports()

myGui := new GridGUI("Serial Terminal", "resize -DPIScale")
myGui.GuiSizeDelay := false

console := new ConsoleControl(myGui.hwnd, "w0 h0") ; "/q /k echo off" ;  & powershell -NoExit
console.Run("filter timestamp {""$(Get-Date -Format o): $_""}")
;console.Run("filter timestamp {""$([DateTimeOffset]::UtcNow.ToUnixTimeMilliseconds()),$(Get-Date -Format o): $_""}")
Sleep, 100
console.Run("clear")

cmdline :=      myGui.add("1-16", 1, "Edit", , , 1, , 1)
                myGui.AddControl("1-16", 2, console, 1, 1, 1, 1)

bt_connect :=   myGui.add(1, 3, "Button", "w100", "Connect")

chb_log :=      myGui.add(3, 3, "CheckBox", "checked", "log", , , , , "EC")
chb_time :=     myGui.add(4, 3, "CheckBox", "checked", "timestamp", , , , , "EC")
                myGui.add(5, 3, "Text", , "Port:", , , , , "EC")
port :=         myGui.add(6, 3, "DropDownList", "w60", StrReplace(JoinKey("|", ports), "|", "||", 1), , , , , "WC")

                myGui.add(7, 3, "Text", , "flow control:", , , , , "EC")
flowcontrol :=  myGui.add(8, 3, "DropDownList", "w35", "N|X||R|D", , , , , "WC")

                myGui.add(9, 3, "Text", , "parity:", , , , , "EC")
parity :=       myGui.add(10, 3, "DropDownList", "w35", "n||o|e|m|s", , , , , "WC")

                myGui.add(11, 3, "Text", , "stop bits:", , , , , "EC")
stopbits :=     myGui.add(12, 3, "DropDownList", "w40", "1||1.5|2", , , , , "WC")

                myGui.add(13, 3, "Text", , "data bits:", , , , , "EC")
databits :=     myGui.add(14, 3, "DropDownList", "w35", "5|6|7|8||9", , , , , "WC")

                myGui.add(15, 3, "Text", , "Baud:", , , , , "EC")
baudrate :=     myGui.add(16, 3, "DropDownList", "w70", "300|1200|2400|4800|9600|19200|38400|57600|74880|115200||230400|250000|500000|1000000|2000000", , , , , "WC")

bt_connect.Callback := Func("ConnectPressed").Bind(console, port, baudrate, flowcontrol, parity, stopbits, databits, chb_time, chb_log)

myGui.AutoSize()
myGui.MinSize(myGui.pos.w, 800)
myGui.Show("h800")

OnMessage(WM_DEVICECHANGE, Func("WM_DEVICECHANGE"))
return

#If WinActive("ahk_id " myGui.hwnd)
    Enter::
        hwnd := myGui.ControlGetFocus()
        if(hwnd = cmdline.Hwnd) {
            SendCommand(cmdline, console)
        }
    return
#If

GuiClose:
OnExit:
    if(plinkprocess) {
        Process, Close, % plinkprocess
    }
    Process, Close, % console.pid ; May be a bit forceful? No effect if it already closed.
    ExitApp
return

SendCommand(cmdline, console) {
    console.run(cmdline.vVar)
    cmdline.GuiControl("", "")
}

ConnectPressed(console, port, baudrate, flowcontrol, parity, stopbits, databits, timestamp, log) {
    if(plinkprocess) {
        Process, Close, % plinkprocess
    }

    command := "& """ A_ScriptDir "\plink.exe"" -serial \\.\" port.vVar " -sercfg " baudrate.vVar "," flowcontrol.vVar "," parity.vVar "," stopbits.vVar "," databits.vVar
    if(timestamp.vVar) {
        command .= " | timestamp"
    }
    if(log.vVar) {
        command .= " | tee """ A_Now " - " port.vVar ".log"""
    }

    console.run(command)

    plinkprocess := WaitForProcess("i)plink\.exe", "i)\Q\\.\" port.vVar "\E")
}

getProcess(name) {
    Process, Exist, % name
    return ErrorLevel
}

WaitForProcess(name := "", cmdline := "", timeout := 5000) {
    start := A_TickCount
    pid := ""
    while(A_TickCount - start < timeout) {
        processes := GetRunningProcess(name, cmdline)
        if(processes.Count()) {
            processes._NewEnum().Next(pid)
            return pid
        }
    }
}

GetRunningProcess(name := "", cmdline := "") {
    map := {}
    for proc in ComObjGet("winmgmts:").ExecQuery("Select * from Win32_Process") {
        if((!name || proc.Name ~= name) && (!cmdline || proc.CommandLine ~= cmdline)) {
            map[proc.ProcessId] := proc
        }
    }
    return map
}

GetCOMports() {
    portmap := {}
    Loop, Reg, HKEY_LOCAL_MACHINE\HARDWARE\DEVICEMAP\SERIALCOMM
    {
        RegRead, serial
        if(serial ~= "COM\d+") {
            portmap[serial] := A_LoopRegName
        }
    }
    return portmap
}

UpdateCOMports() {
    Global port
    port.GuiControl("", "|" SelectString(JoinKey("|", GetCOMports()), port.vVar))
}

Class ConsoleControl Extends GridGUI.WindowControl {
    __New(guiHwnd, options := "") {
        Base.__New(guiHwnd, this.__StrartConsole(), options)
    }

    __StrartConsole() {
        ; Launch hidden cmd.exe and store process ID in pid.
        Run, % "powershell", , Hide, pid

        ; Wait for console window to be created, store its ID.
        DetectHiddenWindows, On
        WinWait, % "ahk_pid " pid
        WinGet, hwnd, ID, % "ahk_pid " pid
        this.pid := pid
        return hwnd
    }

    PressEnter() {
        ControlSend, , {Enter}, % "ahk_id " this.hwnd
    }

    Run(cmd) {
        if(this.GuiControl(cmd)) {
            this.PressEnter()
        }
    }

    GuiControl(value) {
        ConsoleSend(value, "ahk_id " this.hwnd)
        if(ErrorLevel) {
            MsgBox, % ErrorLevel
            return false
        }
        return true
    }

    GuiControlGet(subCommand := "", value := "") {
        ;GuiControlGet, OutputVar, % SubCommand, % this.hwnd, % value
        ;return OutputVar
    }

    Control(subCommand, value) {

    }
}

; Sends text to a console's input stream. WinTitle may specify any window in
; the target process. Since each process may be attached to only one console,
; ConsoleSend fails if the script is already attached to a console.
ConsoleSend(text, WinTitle="", WinText="", ExcludeTitle="", ExcludeText="") {
    DetectHiddenWindows, On
    WinGet, pid, PID, %WinTitle%, %WinText%, %ExcludeTitle%, %ExcludeText%
    if !pid
        return false, ErrorLevel:="window"
    ; Attach to the console belonging to %WinTitle%'s process.
    if !DllCall("AttachConsole", "uint", pid)
        return false, ErrorLevel:="AttachConsole"
    hConIn := DllCall("CreateFile", "str", "CONIN$", "uint", 0xC0000000
                , "uint", 0x3, "uint", 0, "uint", 0x3, "uint", 0, "uint", 0)
    if hConIn = -1
        return false, ErrorLevel:="CreateFile"

    VarSetCapacity(ir, 24, 0)       ; ir := new INPUT_RECORD
    NumPut(1, ir, 0, "UShort")      ; ir.EventType := KEY_EVENT
    NumPut(1, ir, 8, "UShort")      ; ir.KeyEvent.wRepeatCount := 1
    ; wVirtualKeyCode, wVirtualScanCode and dwControlKeyState are not needed,
    ; so are left at the default value of zero.

    Loop, Parse, text ; for each character in text
    {
        NumPut(Asc(A_LoopField), ir, 14, "UShort")

        NumPut(true, ir, 4, "Int")  ; ir.KeyEvent.bKeyDown := true
        gosub ConsoleSendWrite

        NumPut(false, ir, 4, "Int") ; ir.KeyEvent.bKeyDown := false
        gosub ConsoleSendWrite
    }
    gosub ConsoleSendCleanup
    return true

    ConsoleSendWrite:
        if ! DllCall("WriteConsoleInput", "uint", hconin, "uint", &ir, "uint", 1, "uint*", 0)
        {
            gosub ConsoleSendCleanup
            return false, ErrorLevel:="WriteConsoleInput"
        }
    return

    ConsoleSendCleanup:
        if (hConIn!="" && hConIn!=-1)
            DllCall("CloseHandle", "uint", hConIn)
        ; Detach from %WinTitle%'s console.
        DllCall("FreeConsole")
    return
}

WM_DEVICECHANGE(wParam, lParam) {                                       ; http://msdn.com/library/aa363480(vs.85,en-us)    WM_DEVICECHANGE message
    static DBT_DEVICEARRIVAL        := 0x8000                           ; A device or piece of media has been inserted and is now available.
    static DBT_DEVICEREMOVECOMPLETE := 0x8004                           ; A device or piece of media has been removed.
    static DBT_DEVTYP_VOLUME        := 0x00000002                       ; Logical volume. This structure is a DEV_BROADCAST_VOLUME structure.
    static DBT_DEVTYP_PORT          := 0x00000003

    if(wParam = DBT_DEVICEARRIVAL) {
        if(NumGet(lParam+0, 4, "UInt") = DBT_DEVTYP_PORT) {             ; http://msdn.com/library/aa363246(vs.85,en-us)    DEV_BROADCAST_HDR structure
            fun := Func("UpdateCOMports")
            SetTimer, % fun, -100
            ;FirstDriveFromMask(NumGet(lParam+0, 12, "UInt"), 1)        ; http://msdn.com/library/aa363249(vs.85,en-us)    DEV_BROADCAST_VOLUME structure
        }
    }
    if(wparam = DBT_DEVICEREMOVECOMPLETE) {
        if(NumGet(lParam+0, 4, "UInt") = DBT_DEVTYP_PORT)   {           ; http://msdn.com/library/aa363246(vs.85,en-us)    DEV_BROADCAST_HDR structure
            fun := Func("UpdateCOMports")
            SetTimer, % fun, -100
            ;FirstDriveFromMask(NumGet(lParam+0, 12, "UInt"), 0)        ; http://msdn.com/library/aa363249(vs.85,en-us)    DEV_BROADCAST_VOLUME structure
        }
    }
}

SelectString(Haystack, Needle) {
    return RegExReplace(Haystack, Needle "\|?", Needle "||")
}

JoinKey(sep, obj) {
    res := ""
    for key in obj {
        res .= key sep
    }
    return SubStr(res, 1, -StrLen(sep))
}
Back to top