Private/Mpy.Backend.ps1
|
# Mpy.Backend.ps1 # MicroPython backend functionality using mpremote # Known USB Vendor IDs for MicroPython / CircuitPython boards $script:MpyKnownVids = @{ '303A' = @{ Manufacturer = 'Espressif (ESP32)'; IsMicroPython = $true } '2E8A' = @{ Manufacturer = 'Raspberry Pi (RP2040/RP2350)'; IsMicroPython = $true } '0483' = @{ Manufacturer = 'STMicroelectronics'; IsMicroPython = $true } '239A' = @{ Manufacturer = 'Adafruit (CircuitPython)'; IsMicroPython = $true } '1D50' = @{ Manufacturer = 'MicroPython (Pyboard)'; IsMicroPython = $true } '0403' = @{ Manufacturer = 'FTDI'; IsMicroPython = $false } '1A86' = @{ Manufacturer = 'WCH (CH340)'; IsMicroPython = $false } '10C4' = @{ Manufacturer = 'Silicon Labs (CP210x)'; IsMicroPython = $false } } function Get-MpyPortList { # Platform-aware serial port enumeration for MicroPython device discovery. # Default: returns port name strings. With -Detailed: returns enriched objects. [CmdletBinding()] param( [switch]$Detailed ) $isWindows = [System.Environment]::OSVersion.Platform -eq 'Win32NT' if ($isWindows) { if ($Detailed) { return Invoke-MpyWindowsPortList } else { # Basic list - just port names sorted $ports = [System.IO.Ports.SerialPort]::GetPortNames() | Sort-Object if (-not $ports -or @($ports).Count -eq 0) { Write-Verbose "No real serial ports found on Windows; returning stub port" return @('COM99 (STUB)') } return $ports } } else { # Unix: basic .NET enumeration for now $ports = [System.IO.Ports.SerialPort]::GetPortNames() | Sort-Object if (-not $Detailed) { if (-not $ports -or @($ports).Count -eq 0) { Write-Verbose "No real serial ports found on Unix; returning stub port" return @('/dev/ttyUSB0 (STUB)') } return $ports } # Build minimal objects for Unix (no WMI available) if (-not $ports -or @($ports).Count -eq 0) { Write-Verbose "No real serial ports found on Unix; returning stub detailed object" return @([PSCustomObject]@{ Port = '/dev/ttyUSB0' FriendlyName = '/dev/ttyUSB0 (STUB)' VID = 'N/A' PID = 'N/A' Manufacturer = 'Unknown (STUB)' IsMicroPython = $false Status = 'Stub' }) } return $ports | ForEach-Object { [PSCustomObject]@{ Port = $_ FriendlyName = $_ VID = 'N/A' PID = 'N/A' Manufacturer = 'Unknown' IsMicroPython = $false Status = 'Unknown' } } } } function Invoke-MpyWindowsPortList { # Windows-specific: enrich serial ports with VID/PID and board identification via WMI. [CmdletBinding()] [OutputType([System.Object[]])] param() $results = @() try { $wmiPorts = Get-WmiObject -Class Win32_SerialPort -ErrorAction SilentlyContinue foreach ($port in $wmiPorts) { $vid = $null $pid = $null if ($port.PNPDeviceID -match 'VID_([0-9A-Fa-f]{4})&PID_([0-9A-Fa-f]{4})') { $vid = $Matches[1].ToUpper() $pid = $Matches[2].ToUpper() } if ($vid -and $script:MpyKnownVids.ContainsKey($vid)) { $mfgInfo = $script:MpyKnownVids[$vid] $mfg = $mfgInfo.Manufacturer $isMpy = $mfgInfo.IsMicroPython } else { $mfg = if ($port.Manufacturer) { $port.Manufacturer } else { 'Unknown' } $isMpy = $false } $results += [PSCustomObject]@{ Port = $port.DeviceID FriendlyName = $port.Name VID = if ($vid) { "0x$vid" } else { 'N/A' } PID = if ($pid) { "0x$pid" } else { 'N/A' } Manufacturer = $mfg IsMicroPython = $isMpy Status = $port.Status } } } catch { Write-Verbose "WMI serial port enumeration failed: $($_.Exception.Message)" } # MicroPython devices first, then alphabetical by port name return $results | Sort-Object -Property @{Expression = 'IsMicroPython'; Descending = $true}, Port } function Invoke-MpyBackendGetInfo { [CmdletBinding()] [OutputType([hashtable])] param( [Parameter(Mandatory = $true)] [string]$SerialPort ) $hasMpremote = Test-NativeCommand -Command 'mpremote' if (-not $hasMpremote) { Write-Verbose "mpremote not found in PATH - returning stub device info" return @{ Port = $SerialPort PythonVersion = "MicroPython v1.20.0 on 2023-04-26 (STUB)" Board = "Generic MicroPython board (STUB)" ChipFamily = "Unknown (STUB)" FreeMemory = 102400 Connected = $true Stub = $true } } try { $code = 'import sys, gc; print(sys.version); print(sys.implementation.name); print(gc.mem_free())' $safeCode = '"' + $code.Replace('"', '\"') + '"' $result = Invoke-NativeProcess -FilePath 'mpremote' ` -ArgumentList @('connect', $SerialPort, 'exec', $safeCode) ` -TimeoutSeconds 10 if (-not $result.Success) { $errMsg = if ($result.TimedOut) { "mpremote timed out" } else { $result.StandardError.Trim() } throw "mpremote getinfo failed: $errMsg" } $lines = ($result.StandardOutput -split '[\r\n]+') | Where-Object { $_ -ne '' } $version = if ($lines.Count -ge 1) { $lines[0].Trim() } else { 'Unknown' } $implName = if ($lines.Count -ge 2) { $lines[1].Trim() } else { 'micropython' } $freeMem = if ($lines.Count -ge 3) { [int]($lines[2].Trim()) } else { 0 } return @{ Port = $SerialPort PythonVersion = $version Board = $implName ChipFamily = 'Unknown' FreeMemory = $freeMem Connected = $true Stub = $false } } catch { Write-Warning "Failed to get MicroPython device info: $($_.Exception.Message)" throw } } function Invoke-MpyBackendExecute { [CmdletBinding()] [OutputType([string])] param( [Parameter(Mandatory = $true)] [string]$SerialPort, [Parameter(Mandatory = $true)] [string]$Code ) $hasMpremote = Test-NativeCommand -Command 'mpremote' if (-not $hasMpremote) { Write-Verbose "mpremote not found in PATH - returning stub execution result" return "# mpremote not found - STUB MODE`n# Code not executed: $Code" } try { # Wrap code in double-quotes so spaces and semicolons survive arg parsing $safeCode = '"' + $Code.Replace('"', '\"') + '"' Write-Verbose ("mpremote exec on {0}: {1}" -f $SerialPort, $Code) $result = Invoke-NativeProcess -FilePath 'mpremote' ` -ArgumentList @('connect', $SerialPort, 'exec', $safeCode) ` -TimeoutSeconds 15 if (-not $result.Success) { $errMsg = if ($result.TimedOut) { "mpremote timed out after 15s" } else { $result.StandardError.Trim() } throw "mpremote exec failed: $errMsg" } return $result.StandardOutput } catch { Write-Warning "Failed to execute MicroPython code: $($_.Exception.Message)" throw } } function Invoke-MpyBackendPushFile { [CmdletBinding()] param( [Parameter(Mandatory = $true)] [string]$SerialPort, [Parameter(Mandatory = $true)] [string]$LocalPath, [Parameter(Mandatory = $false)] [string]$RemotePath ) if ([string]::IsNullOrEmpty($RemotePath)) { $RemotePath = Split-Path -Leaf $LocalPath } $hasMpremote = Test-NativeCommand -Command 'mpremote' if (-not $hasMpremote) { $FileItem = Get-Item -Path $LocalPath -ErrorAction SilentlyContinue $FileSize = if ($FileItem) { $FileItem.Length } else { 0 } Write-Verbose "mpremote not found - STUB: would push $LocalPath -> $RemotePath ($FileSize bytes)" return } if (-not (Test-Path -Path $LocalPath)) { throw "Local file not found: $LocalPath" } try { $FileItem = Get-Item -Path $LocalPath Write-Verbose "Pushing $LocalPath ($($FileItem.Length) bytes) -> :$RemotePath on $SerialPort" $result = Invoke-NativeProcess -FilePath 'mpremote' ` -ArgumentList @('connect', $SerialPort, 'cp', $LocalPath, ":$RemotePath") ` -TimeoutSeconds 30 if (-not $result.Success) { $errMsg = if ($result.TimedOut) { "mpremote timed out after 30s" } else { $result.StandardError.Trim() } throw "mpremote cp failed: $errMsg" } Write-Verbose "Pushed $LocalPath -> :$RemotePath successfully" } catch { Write-Warning "Failed to push file via MicroPython: $($_.Exception.Message)" throw } } function Test-MpyBackendConnection { [CmdletBinding()] [OutputType([bool])] param( [Parameter(Mandatory = $true)] [string]$SerialPort ) $hasMpremote = Test-NativeCommand -Command 'mpremote' if (-not $hasMpremote) { Write-Verbose "mpremote not found in PATH - connection test returns stub true" return $true } try { $result = Invoke-NativeProcess -FilePath 'mpremote' ` -ArgumentList @('connect', $SerialPort, 'exec', '"print(1)"') ` -TimeoutSeconds 5 if ($result.TimedOut) { Write-Verbose "mpremote connection test timed out on $SerialPort" return $false } # mpremote exits 0 and prints '1' on a live board $output = $result.StandardOutput.Trim() return ($result.Success -and $output -eq '1') } catch { Write-Verbose "mpremote connection test error: $($_.Exception.Message)" return $false } } |