mirror of
https://github.com/mikecarper/MeshCore.git
synced 2026-09-02 20:13:51 +00:00
421 lines
14 KiB
Batchfile
421 lines
14 KiB
Batchfile
# 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
|
|
}
|