# 2>NUL & @set "MESHCORE_TERM_SCRIPT=%~f0" & @set "MESHCORE_TERM_ARG=%~1" & @powershell -NoProfile -ExecutionPolicy Bypass -Command "Invoke-Expression ((Get-Content -LiteralPath $env:MESHCORE_TERM_SCRIPT) -join [Environment]::NewLine)" & @set "MESHCORE_TERM_SCRIPT=" & @set "MESHCORE_TERM_ARG=" & goto :eof Set-StrictMode -Version Latest $ErrorActionPreference = 'Stop' $script:BaudRate = 115200 $script:StartToken = '+++MESHCORE-TERM-START' $script:StopToken = '+++MESHCORE-TERM-STOP' function Remove-HybridLauncherEcho { # cmd.exe echoes the harmless PowerShell/CMD bridge before PowerShell takes # over. Remove that one line when running in an interactive console. try { if (-not [Console]::IsOutputRedirected -and [Console]::CursorTop -gt 0) { $row = [Console]::CursorTop - 1 [Console]::SetCursorPosition(0, $row) [Console]::Write(' ' * [Math]::Max(1, [Console]::WindowWidth - 1)) [Console]::SetCursorPosition(0, $row) } } catch { } } Remove-HybridLauncherEcho function Get-SerialPortInventory { # Match firmware.cmd: only accept Plug-and-Play COM devices whose Windows # device ID identifies them as USB. This excludes motherboard COM1 ports, # Bluetooth serial ports, and other unrelated serial devices. $ports = @() try { $devices = @( Get-CimInstance -ClassName Win32_PnPEntity -ErrorAction Stop | Where-Object { $_.DeviceID -like '*USB*' -and $_.Name -match '\(COM\d+\)' } ) foreach ($device in $devices) { if ($device.Name -match 'COM(\d+)') { $portName = $matches[0].ToUpperInvariant() $portNumber = [int]$matches[1] $hardwareId = if ($device.HardwareID) { ($device.HardwareID -split '\\')[-1] } else { '--' } $ports += [pscustomobject]@{ Port = $portName Label = $device.Name DeviceName = $hardwareId SortOrder = $portNumber } } } } catch { Write-Host ("Unable to query Windows USB serial devices: {0}" -f $_.Exception.Message) -ForegroundColor Yellow } return @($ports | Sort-Object SortOrder, Label) } function Show-Ports { $ports = @(Get-SerialPortInventory) if ($ports.Count -eq 0) { Write-Host 'No USB serial ports are currently available.' -ForegroundColor Yellow return } Write-Host 'Available USB serial ports:' foreach ($item in $ports) { Write-Host (" {0,-7} {1} [{2}]" -f $item.Port, $item.Label, $item.DeviceName) } } function Select-SerialPort { param([string]$RequestedPort) if ($RequestedPort) { $candidate = $RequestedPort.Trim().ToUpperInvariant() if ($candidate -match '^\d+$') { $candidate = "COM$candidate" } if ($candidate -notmatch '^COM\d+$') { throw "Invalid COM port '$RequestedPort'. Use a value such as COM7." } $usbPorts = @(Get-SerialPortInventory | Select-Object -ExpandProperty Port) if ($usbPorts -notcontains $candidate) { $available = if ($usbPorts.Count -gt 0) { $usbPorts -join ', ' } else { 'none' } throw "$candidate is not a currently connected USB COM port. Available USB ports: $available." } return $candidate } while ($true) { $ports = @(Get-SerialPortInventory) if ($ports.Count -eq 0) { Write-Host 'No USB serial ports found. Connect the node with a USB data cable.' -ForegroundColor Yellow $answer = Read-Host 'Press Enter to scan again, or type Q to quit' if ($answer -match '^[Qq]$') { return '' } continue } if ($ports.Count -eq 1) { Write-Host ("Using {0}: {1}" -f $ports[0].Port, $ports[0].Label) return $ports[0].Port } Write-Host 'Select the MeshCore USB serial port:' for ($index = 0; $index -lt $ports.Count; $index++) { Write-Host (" {0,2}) {1,-7} {2}" -f ($index + 1), $ports[$index].Port, $ports[$index].Label) } Write-Host ' R) Scan again' Write-Host ' Q) Quit' $answer = (Read-Host 'Choice').Trim() if ($answer -match '^[Rr]$') { continue } if ($answer -match '^[Qq]$') { return '' } $selection = 0 if ([int]::TryParse($answer, [ref]$selection) -and $selection -ge 1 -and $selection -le $ports.Count) { return $ports[$selection - 1].Port } Write-Host 'Invalid selection.' -ForegroundColor Yellow } } function Read-CompanionModeChoice { Write-Host '' Write-Host 'Firmware type:' Write-Host ' 1) USB/full Companion firmware (default)' Write-Host ' 2) Terminal Chat firmware' $answer = (Read-Host 'Choice [1]').Trim() switch ($answer) { '' { return $true } '1' { return $true } '2' { return $false } default { Write-Host 'Using USB/full Companion mode.' -ForegroundColor Yellow return $true } } } function Open-SerialPort { param([Parameter(Mandatory)][string]$PortName) $port = [System.IO.Ports.SerialPort]::new( $PortName, $script:BaudRate, [System.IO.Ports.Parity]::None, 8, [System.IO.Ports.StopBits]::One ) $port.Handshake = [System.IO.Ports.Handshake]::None $port.Encoding = [System.Text.UTF8Encoding]::new($false, $false) $port.NewLine = "`r`n" $port.ReadTimeout = 100 $port.WriteTimeout = 1000 $port.DtrEnable = $true $port.RtsEnable = $true $port.Open() return $port } function Enter-CompanionTerminalMode { param([Parameter(Mandatory)][System.IO.Ports.SerialPort]$Port) try { $Port.DiscardInBuffer() } catch { } $Port.Write($script:StartToken) # Do not render Binary Companion frames as console control characters while # the device changes modes. Start displaying once the text banner arrives. $watch = [System.Diagnostics.Stopwatch]::StartNew() $idle = [System.Diagnostics.Stopwatch]::StartNew() $buffer = [System.Text.StringBuilder]::new() $bannerText = 'MeshCore Chat Terminal' $bannerSeen = $false while ($watch.ElapsedMilliseconds -lt 5000) { $received = $Port.ReadExisting() if ($received) { [void]$buffer.Append($received) $idle.Restart() $allText = $buffer.ToString() $bannerIndex = $allText.IndexOf($bannerText, [System.StringComparison]::OrdinalIgnoreCase) if ($bannerIndex -ge 0) { $bannerSeen = $true } } # The device can already have several Binary Companion frames queued # when it recognizes the switch token. Drain the entire transition and # wait for a quiet period before rendering live terminal output. if ($bannerSeen -and $idle.ElapsedMilliseconds -ge 350) { return $true } Start-Sleep -Milliseconds 20 } if (-not $bannerSeen) { Write-Host 'Terminal switch sent; no banner was received.' -ForegroundColor Yellow return $false } Write-Host 'MeshCore responded, but startup traffic did not become quiet.' -ForegroundColor Yellow return $false } function Confirm-DedicatedTerminalChatMode { param([Parameter(Mandatory)][System.IO.Ports.SerialPort]$Port) # A dedicated Terminal Chat build may have printed its banner before the # port was opened. "ver" provides a harmless live firmware check. On a # brand-new node its LF also completes the initial key-generation prompt. $buffer = [System.Text.StringBuilder]::new() try { $startupText = $Port.ReadExisting() if ($startupText) { [void]$buffer.Append($startupText) } $Port.Write("ver`r`n") } catch { return $false } $watch = [System.Diagnostics.Stopwatch]::StartNew() $idle = [System.Diagnostics.Stopwatch]::StartNew() $terminalSeen = $false while ($watch.ElapsedMilliseconds -lt 5000) { $received = $Port.ReadExisting() if ($received) { [void]$buffer.Append($received) $idle.Restart() } $text = $buffer.ToString() if ($text -match '(?i)MeshCore Chat Terminal' -or $text -match '(?im)^\s*v\d+(?:\.\d+)*(?:\s|$)' -or $text -match '(?i)\bbuild\s*:') { $terminalSeen = $true } if ($terminalSeen -and $idle.ElapsedMilliseconds -ge 350) { return $true } Start-Sleep -Milliseconds 20 } return ($terminalSeen -and $Port.BytesToRead -eq 0) } function Start-TerminalSession { param( [Parameter(Mandatory)][string]$PortName, [Parameter(Mandatory)][bool]$SwitchCompanionMode ) $port = $null $oldTreatControlCAsInput = [Console]::TreatControlCAsInput $lineCharacterCount = 0 $pendingHighSurrogate = '' try { Write-Host '' Write-Host "Opening $PortName at $($script:BaudRate) baud..." $port = Open-SerialPort -PortName $PortName # Some USB-to-UART boards reset when the port opens. Start-Sleep -Milliseconds 1500 if ($SwitchCompanionMode) { $verified = Enter-CompanionTerminalMode -Port $port } else { $verified = Confirm-DedicatedTerminalChatMode -Port $port } if (-not $verified) { throw "$PortName opened, but it did not respond as the selected MeshCore terminal firmware." } Write-Host "Verified MeshCore terminal on $PortName." -ForegroundColor DarkGray Write-Host '' Write-Host 'Connected. Type help for commands. Press Ctrl+] or Ctrl+C to exit.' -ForegroundColor Green Write-Host '' [Console]::Write('> ') [Console]::TreatControlCAsInput = $true while ($port.IsOpen) { $received = $port.ReadExisting() if ($received) { [Console]::Write($received) } while ([Console]::KeyAvailable) { $key = [Console]::ReadKey($true) $characterCode = [int]$key.KeyChar if ($characterCode -eq 29 -or $characterCode -eq 3) { return } if ($key.Key -eq [ConsoleKey]::Enter) { $pendingHighSurrogate = '' $port.Write("`r`n") $lineCharacterCount = 0 continue } if ($key.Key -eq [ConsoleKey]::Backspace) { if ($pendingHighSurrogate) { $pendingHighSurrogate = '' continue } $port.Write([string][char]8) if ($lineCharacterCount -gt 0) { $lineCharacterCount-- } continue } if ($characterCode -ne 0) { if ([char]::IsHighSurrogate($key.KeyChar)) { $pendingHighSurrogate = [string]$key.KeyChar continue } if ($pendingHighSurrogate) { if ([char]::IsLowSurrogate($key.KeyChar)) { $port.Write($pendingHighSurrogate + [string]$key.KeyChar) $pendingHighSurrogate = '' $lineCharacterCount++ continue } $port.Write($pendingHighSurrogate) $pendingHighSurrogate = '' $lineCharacterCount++ } $port.Write([string]$key.KeyChar) $lineCharacterCount++ } } Start-Sleep -Milliseconds 10 } } finally { [Console]::TreatControlCAsInput = $oldTreatControlCAsInput if ($port) { if ($port.IsOpen -and $SwitchCompanionMode) { try { # Clear an unfinished input line so the exact stop token is recognized. for ($index = 0; $index -lt $lineCharacterCount; $index++) { $port.Write([string][char]8) } $port.Write($script:StopToken) Start-Sleep -Milliseconds 150 } catch { } } try { if ($port.IsOpen) { $port.Close() } } finally { $port.Dispose() } } Write-Host '' Write-Host 'Terminal closed.' } } $requested = if ($env:MESHCORE_TERM_ARG) { $env:MESHCORE_TERM_ARG.Trim() } else { '' } if ($requested -match '^(\/list|-list|--list)$') { Show-Ports return } if ($requested -match '^(\/\?|-h|--help)$') { Write-Host 'MeshCore terminal chat' Write-Host '' Write-Host 'Usage:' Write-Host ' meshcore-terminal-chat.cmd' Write-Host ' meshcore-terminal-chat.cmd COM7' Write-Host ' meshcore-terminal-chat.cmd /list' return } try { $selectedPort = Select-SerialPort -RequestedPort $requested if (-not $selectedPort) { return } $switchCompanionMode = Read-CompanionModeChoice Start-TerminalSession -PortName $selectedPort -SwitchCompanionMode $switchCompanionMode } catch { Write-Host '' Write-Host ("Unable to start terminal chat: {0}" -f $_.Exception.Message) -ForegroundColor Red Write-Host 'Close any MeshCore app, browser console, or other program using the COM port.' -ForegroundColor Yellow Read-Host 'Press Enter to close' | Out-Null $global:LASTEXITCODE = 1 }