PSMPyTools.psm1


if($PSScriptRoot)  { $thisScriptRoot  = $PSScriptRoot  }
if($PSCommandPath) { $thisCommandPath = $PSCommandPath }

$Script:ModuleItem = Get-Item $thisCommandPath

$Script:ModuleName = $Script:ModuleItem.BaseName

#region State

$Script:State = [PSCustomObject]@{

    ModuleName = $Script:ModuleName
    ModuleItem = $Script:ModuleItem
}

$Script:State | Add-Member -Force -MemberType   NoteProperty -Name MPyBoard    -Value ([ordered]@{})
$Script:State | Add-Member -Force -MemberType   NoteProperty -Name MPyFirmware -Value ([ordered]@{})

$Script:State | Add-Member -Force -MemberType   NoteProperty -Name PnPEntity   -Value   $null
$Script:State | Add-Member -Force -MemberType ScriptProperty -Name MPyDevice   -Value { $this.PnPEntity.MPyDevice }
$Script:State | Add-Member -Force -MemberType ScriptProperty -Name SerialPort  -Value { $this.PnPEntity.MPyDevice.SerialPort }
$Script:State | Add-Member -Force -MemberType ScriptProperty -Name PortName    -Value { $this.PnPEntity.MPyDevice.SerialPort.PortName }
$Script:State | Add-Member -Force -MemberType ScriptProperty -Name IsOpen      -Value { $this.PnPEntity.MPyDevice.SerialPort.IsOpen }

function Get-MPyState
{
    $Script:State
}

#endregion

function Get-MPySample
{
    [CmdletBinding()]
    param(
        [string]$Name = "*.py"
    )

    Get-ChildItem -Path "$($Script:ModuleItem.DirectoryName)\Python\$($Name)" -File
}

function Get-MPyUri
{
    [CmdletBinding()]
    param(
        [string]$Path
    )

    "https://micropython.org/$($Path)".TrimEnd("/")
}

function Get-MPyDownloadUri
{
    [CmdletBinding()]
    param(
        [string]$Board
    )

    Get-MPyUri -Path "download/$($Board)"
}

function Open-MPyWebSite
{
    [CmdletBinding()]
    param(
        [Parameter(ValueFromPipeline,ValueFromPipelineByPropertyName)][string]$Board,
        [Parameter()]                                                 [switch]$Notes,
        [Parameter(ValueFromPipeline)]                                [object]$InputObject
    )

    if($Notes -and $InputObject.NotesUri)
    {
        Start-Process -Verb Open -FilePath $InputObject.NotesUri
    }    
    else
    {
        Start-Process -Verb Open -FilePath (Get-MPyDownloadUri -Board $Board)
    }
}

function Find-MPyBoard
{
    [CmdletBinding()]
    param(
        [switch]$AsDictionary,
        [switch]$Force
    )

    if($Force -or ($Script:State.MPyBoard.Count -eq 0))
    {
        $WebSite = Invoke-WebRequest -Uri (Get-MPyDownloadUri)

        $Lines = $WebSite.RawContent.Substring($WebSite.RawContent.IndexOf('<a class="board-card" href="')).Split([System.Environment]::NewLine) | ForEach-Object Trim | Where-Object { $_ }

        $Script:State.MPyBoard = [ordered]@{}

        foreach($Line in $Lines)
        {
            switch -Regex ($Line)
            {
                '^<footer>$'                                  { break }

                '^\<a class="board-card" href="(.*)"\>$'      { $Board = "" | Select-Object @{ Name = "Board"; Expression = { $Matches[1] } }, Product, Vendor, ImageUri }

                '^\<img src="(.*)"\>'                         { $Board.ImageUri = $Matches[1] }
                '^\<div class="board-product"\>(.*)\</div\>$' { $Board.Product  = $Matches[1] }
                '^\<div class="board-vendor"\>(.*)\</div\>$'  { $Board.Vendor   = $Matches[1]; $Script:State.MPyBoard[$Board.Board] = $Board }
            }
        }
    }

    switch($AsDictionary)
    {
        $true  { $Script:State.MPyBoard        }
        $false { $Script:State.MPyBoard.Values }
    }
}

function Find-MPyFirmware
{
    [CmdletBinding(DefaultParameterSetName="Online")]
    param(
        [Parameter(ParameterSetName="Online")]
        [Parameter()]                                                           [switch]$Online,

        [Parameter(ParameterSetName="Cache" )]
        [Parameter()]                                                           [switch]$Cache,

        [Parameter(ParameterSetName="Online")]
        [Parameter(ParameterSetName="Cache" )]
        [Parameter(Mandatory,ValueFromPipeline,ValueFromPipelineByPropertyName)][string]$Board,

        [Parameter(ParameterSetName="Online")]
        [Parameter(ParameterSetName="Cache" )]
        [Parameter()]                                                           [string]$Variant,

        [Parameter(ParameterSetName="Online")]
        [Parameter(ParameterSetName="Cache" )]
        [Parameter()]                         [ValidateSet("Release","Preview")][string]$State = "Release",

        [Parameter(ParameterSetName="Online")]
        [Parameter()]                                                           [switch]$Latest,

        [Parameter(ParameterSetName="Online")]
        [Parameter()]                                                           [switch]$AsDictionary,

        [Parameter(ParameterSetName="Online")]
        [Parameter()]                                                           [switch]$Force
    )

    $MyInvocationName = $MyInvocation.MyCommand.Name

    switch($PSCmdlet.ParameterSetName)
    {
        "Online" {

            if($Force -or (-not $Script:State.MPyFirmware[$Board]))
            {
                $Script:State.MPyFirmware[$Board] = Invoke-WebRequest -Uri (Get-MPyDownloadUri -Board $Board)
            }

            $WebSite = $Script:State.MPyFirmware[$Board]

            $FirmwareCollection = [ordered]@{}

            $Lines = $WebSite.RawContent.Substring($WebSite.RawContent.IndexOf("<h2>Firmware")).Split([System.Environment]::NewLine) | ForEach-Object Trim | Where-Object { $_ }

            foreach($Line in $Lines)
            {
                switch -Regex ($Line)
                {
                    '^<footer>$' { break }

                    '^\<div\>$' {
            
                        $Firmware = "" | Select-Object @{ Name = "Board"; Expression = { $Board } }, Variant, State, Version, Date, Uri, NotesUri

                        $Collect = $true
                     }

                    '^\<h2\>Firmware(.*)\<\/h2\>$' {
            
                        $Firmware_Variant = if($Matches[1]) { $Matches[1].Trim([char[]]" ()") }  else { "Default" }

                        $FirmwareCollection[$Firmware_Variant] = [ordered]@{}
                    }

                    '^\<h3\>(.*)\<\/h3\>$' {
            
                        $Firmware_State = ($Matches[1].Split(" ") | Select-Object -First 1).Trim("s")
                
                        $FirmwareCollection[$Firmware_Variant][$Firmware_State] = [ordered]@{}
                    }

                    '^\<\/div\>$' {

                        if($Collect)
                        {
                            if((-not $Variant) -or ($Firmware_Variant -match $Variant))
                            {
                                if((-not $State) -or ($Firmware_State -eq $State))
                                {
                                    if((-not $Latest) -or (($FirmwareCollection[$Firmware_Variant][$Firmware_State].Count -eq 0)))
                                    {
                                        $FirmwareCollection[$Firmware_Variant][$Firmware_State][$Firmware.Version] = $Firmware

                                        if(-not $AsDictionary) { $Firmware }
                                    }
                                }
                            }

                            $Collect = $false
                        }
                    }

                    '\<a href="(.*)"\>(.*)\<\/a\>$' {

                        $href      = $Matches[1]
                        $textAll   = $Matches[2].Trim([char[]]"[].").Split(' ')
                        $textSplit = $textAll

                        switch -Wildcard ($textAll)
                        {
                            "v*"     { $Firmware.Version  = $textSplit[0]
                                       $Firmware.Date     = $textSplit[1].Trim([char[]]"()") }
                            "*bin"   { $Firmware.Uri      = "https://micropython.org$href" }
                            "*notes" { $Firmware.NotesUri = $href }
                        }

                        $Firmware.Variant = $Firmware_Variant
                        $Firmware.State   = $Firmware_State  
                    }
                }
            }
            
            if($AsDictionary) { $FirmwareCollection }
        }

        "Cache" {

            $StateFilter = switch($State)
            {
                Release {}

                Preview { "-$($State).*" }
            }

            $FirmwareFilter = "$($env:TEMP)\MPyFirmware\$($Board)-*$($Variant)*-????????-v?.??.?$($StateFilter).bin"

            Write-Verbose "$($MyInvocationName): $FirmwareFilter"

            Get-ChildItem -Path $FirmwareFilter
        }
    }
}

function Get-MPyFirmware
{
    [CmdletBinding()]
    param(
        [Parameter(ValueFromPipeline,ValueFromPipelineByPropertyName)][string]$Uri,
        [Parameter()]                                                 [string]$OutPath,
        [Parameter(ValueFromPipeline)]                                [object]$InputObject,
        [Parameter()]                                                 [switch]$ReUse
    )

    $MyInvocationName = $MyInvocation.MyCommand.Name

    if(-not $OutPath)
    {
        [void](mkdir ($OutPath = "$($env:TEMP)\MPyFirmware"))

        $OutPath = "$OutPath\{0}"
    }

    $OutPath = $OutPath -f (Split-Path $uri -Leaf)

    Write-Verbose "$($MyInvocationName): $('Uri({0})' -f $Uri)"
    Write-Verbose "$($MyInvocationName): $('OutPath({0})' -f $OutPath)"

    if($ReUse -and (Test-Path -Path $OutPath -PathType Leaf))
    {
    }
    else
    {
        Invoke-WebRequest -Uri $Uri -OutFile $OutPath
    }

    if(Test-Path -Path $OutPath -PathType Leaf)
    {
        if($InputObject.Uri)
        {
            $InputObject | Add-Member -MemberType NoteProperty -Name Path -Value $OutPath -Force -PassThru
        }
    }
}

function Start-Thonny
{
    Start-Process -FilePath "C:\Program Files\Thonny\thonny.exe"
}

function Start-ProcessWithProgress
{
    [CmdLetBinding()]
    param(
        [Parameter(Mandatory=$true)][ValidateNotNullOrEmpty()][string]$FilePath,
        [Parameter(Mandatory=$false)]                         [string]$ArgumentList,
        [Parameter(Mandatory=$false)]                         [string]$Verb
    )

    $startInfo = [System.Diagnostics.ProcessStartInfo]::new($FilePath, $ArgumentList)

    $startInfo.CreateNoWindow         = $true
    $startInfo.UseShellExecute        = $false
    $startInfo.RedirectStandardOutput = $true
    $startInfo.RedirectStandardError  = $true

    $process = [System.Diagnostics.Process]::new()

    $process.StartInfo           = $startInfo
    $process.EnableRaisingEvents = $true

    $global:Progress = [PSCustomObject]@{
    
        Activity = ""
    }

    $dataReceived = {

        if($EventArgs.Data)
        {
            if($EventArgs.Data -match "(\d*)%")
            {
                Write-Progress -Activity $global:Progress.Activity -PercentComplete $Matches[1] -Status "$($Matches[1])% Complete"
            }
            else
            {
                $global:Progress.Activity = $EventArgs.Data

                Write-Progress -Activity $global:Progress.Activity -PercentComplete 0
            }
        }
    }

    $eventStdOut = Register-ObjectEvent -InputObject $process -Action $dataReceived -EventName 'OutputDataReceived'
    $eventStdErr = Register-ObjectEvent -InputObject $process -Action $dataReceived -EventName  'ErrorDataReceived'

    [void]$process.Start()

    $process.BeginOutputReadLine()
    $process.BeginErrorReadLine()

    do
    {
        sleep -Milliseconds 10
    }
    until($process.HasExited)

    [void]$process.WaitForExit()

    Unregister-Event -SourceIdentifier $eventStdOut.Name
    Unregister-Event -SourceIdentifier $eventStdErr.Name
}

function Install-MPyFirmware
{
    [CmdletBinding()]
    param(
        [Parameter(Mandatory,ValueFromPipeline,ValueFromPipelineByPropertyName)]               [string]$Path, # [Alias("FullName")]
        [Parameter(                            ValueFromPipelineByPropertyName)][Alias("Port")]    [string]$PortName
    )

    $MyInvocationName = $MyInvocation.MyCommand.Name

    if($MPyDevice = $PortName | Get-MPyPnPEntity)
    {
        if(Test-Path -Path $Path -PathType Leaf)
        {
            $runThis = @{

                FilePath     = "$thisScriptRoot\bin\esplink.exe"
                ArgumentList = '{0} "{1}" {2}' -f $MPyDevice.PortName, $Path, "0x00000000"
            }

            Write-Verbose "$($MyInvocationName): $($runThis.FilePath) $($runThis.ArgumentList)"

            Start-ProcessWithProgress @runThis
        }
    }
}

class MPyDevice
{
<#
    ox https://github.com/scientifichackers/ampy/blob/master/ampy/pyboard.py
#>

    [object]$PnPEntity

    [System.IO.Ports.SerialPort]$SerialPort

    [int32]$BufferSize = 64

    [bool]$Verbose = $false

    MPyDevice( [object]$PnPEntity)                   { $this.Init($PnPEntity, 115200   ) }
    MPyDevice( [object]$PnPEntity, [int32]$BaudRate) { $this.Init($PnPEntity, $BaudRate) }

    [void]Init([object]$PnPEntity, [int32]$BaudRate)
    {
        $this.PnPEntity  = $PnPEntity

        $this.SerialPort = [System.IO.Ports.SerialPort]::new($this.PnPEntity.PortName, $BaudRate)
    }

    [string]ToString() { return $this.PnPEntity.InstanceID }

    [void]Write([string]$Text) { $this.SerialPort.Write($Text) }

    [bool]IsOpen() { return $this.SerialPort.IsOpen  }
    [void]  Open() {        $this.SerialPort.Open()  }
    [void] Close() {        $this.SerialPort.Close() }

    [string]ReadExisting() { return $this.SerialPort.ReadExisting() }

    hidden [PSCustomObject]$ExpectedResponse = [PSCustomObject]@{

        "A" = "raw REPL; CTRL-B to exit`r`n>"                         # Ctrl-A: SOH > start of heading > enter raw repl
        "B" = ">>>"                                                   # Ctrl-B: STX > start of text > exit raw repl
        "C" = $null                                                   # Ctrl-C: ETX > end of text > stop running program
        "D" = "MPY: soft reboot`r`n"                                  # Ctrl-D: EOT > end of transmission > soft reboot
        "E" = "paste mode; Ctrl-C to cancel, Ctrl-D to finish`r`n===" # Ctrl-E: ENQ > enquiry > paste mode
    }

    [void]EnterRawRepl()
    {
        if(-not $this.SerialPort.IsOpen) { $this.Open() }

        $this.Ctrl("C", 5) # 2 for simple ESP32, 5 for Elecrow display
        $this.Ctrl("A", 4, $this.ExpectedResponse.A)
        $this.Ctrl("D", 1, $this.ExpectedResponse.D)
        $this.Ctrl("C", 2) # 2 for simple ESP32, 5 for Elecrow display
    }

    [void]ExitRawRepl()
    {
        $this.Ctrl("B", 1, $this.ExpectedResponse.B)

        $this.Close()
    }

    [void]EnterPasteMode()
    {
        if(-not $this.SerialPort.IsOpen) { $this.Open() }

        $this.Ctrl("E", 1, $this.ExpectedResponse.E)
    }

    [void]FinishPasteMode()
    {
        $this.Ctrl("D", 1, "`r`n>>>")

        $this.Close()
    }

    [void]CancelPasteMode()
    {
        $this.Ctrl("C", 2)

        $this.Close()
    }

    [void]Ctrl([string]$Key) { $this.SerialPort.Write([char[]]@(," ABCDE".IndexOf($Key)), 0, 1) }

    [void]Ctrl([string]$Key, [int]$MaxTries) { $this.Ctrl($Key, $MaxTries, $null) }

    [void]Ctrl([string]$Key, [int]$MaxTries, [string]$ExpectedResponse)
    {
        $Expected = $ExpectedResponse.Replace("`r`n",'`r`n')

        $Response = ""

        foreach($zz in (1..$MaxTries))
        {
            $this.SerialPort.Write([char[]]@(," ABCDE".IndexOf($Key)), 0, 1)
        
            sleep -Milliseconds 200
        
            $Response = $this.ReadExisting().Trim(" ").Replace("`r`n",'`r`n')

            $ValidResponse = . {

                if($Expected)
                {
                    $Response.IndexOf($Expected, [System.StringComparison]::OrdinalIgnoreCase) -ne -1
                }
                else
                {
                    $true
                }
            }

            if($this.Verbose)
            {
                Write-Host "Ctrl($Key) $zz/$MaxTries Valid($("➖✔"[$ValidResponse])) Expected($Expected) Response($Response)"
            }

            if($Expected)
            {
                if($ValidResponse)
                {
                    return
                }
            }
            elseif($zz -eq $MaxTries)
            {
                return
            }
        }

        $this.Close()

        throw [System.Exception]::new("Sent Ctrl($Key) received($($Response)) but expected($($Expected))")
    }

    [object]Execute([string]$Command)
    {
        for($zz = 0; $zz -lt $Command.Length; $zz += $this.BufferSize)
        {
            try
            {
                $this.SerialPort.Write($Command.Substring($zz, $this.BufferSize))
            }
            catch
            {
                try
                {
                    $this.SerialPort.Write($Command.Substring($zz))
                }
                catch
                {
                }
            }

            sleep -Milliseconds 10
        }

        $this.Ctrl("D")

        sleep -Milliseconds 500

        $Response = $this.SerialPort.ReadExisting()

        $Response -match "(?smi)(?<OK>..)(?<Result>.*?)$([char]0x04)(?<Error>.*?)$([char]0x04)`>"

        return [PSCustomObject]@{

            OK        = $Matches.OK
            Result    = $Matches.Result
            Error     = $Matches.Error
            Raw       = $Response
            Exception = ( . { if($Matches.Error) { [System.Exception]::new($Matches.Error) } } )
        }
    }
<#
    ox https://github.com/scientifichackers/ampy/blob/master/ampy/files.py
#>

    hidden [PSCustomObject]$Python = [PSCustomObject]@{

        GetMPyContent = @'
import sys
import ubinascii
 
with open("{0}", 'rb') as infile:
    while True:
        result = infile.read({1})
        if result == b'': break
        sys.stdout.write(ubinascii.hexlify(result))
'@


        OutMPyFile = @'
with open("{0}", "w") as file:
    file.write('''{1}''')
'@


        RemoveMPyItem = @'
try:
    import os
except ImportError:
    import uos as os
 
def RemoveDirectory(path, recurse=False):
    if recurse:
        os.chdir(path)
        for item in os.ilistdir():
            if (item[1] & (1<<15)) != 0: os.remove(item[0]) # 0x8000 = File
        for item in os.ilistdir():
            if (item[1] & (1<<14)) != 0: os.rmdir (item[0]) # 0x4000 = Directory
        os.chdir('..')
    os.rmdir(path)
 
def RemoveItem(path, recurse=False):
    mode = os.stat(path)
    isFile = (mode[0] & (1<<15)) != 0 # 0x8000 = File
    isDir = (mode[0] & (1<<14)) != 0 # 0x4000 = Directory
 
    if isFile: os.remove('{0}')
    if isDir: RemoveDirectory(path, recurse)
 
RemoveItem('{0}', {1})
'@


        NewMPyPath = @'
import re
 
try:
    import os
except ImportError:
    import uos as os
 
path = ""
for p in re.compile("[/]").split('{0}'.strip('/')):
    path += f"/{{p}}"
    try:
        os.mkdir(path)
    except OSError:
        pass
'@


        GetMPyChildItem = @'
import re
 
try:
    import os
except ImportError:
    import uos as os
 
def GetChildItem(path, recurse=False):
    try:
        path = ("/" + path.lstrip("/")).rstrip("/") + "/"
        for item in os.ilistdir(path):
            file = item[0]
            isFile = (item[1] & (1<<15)) != 0 # 0x8000 = File
            isDir = (item[1] & (1<<14)) != 0 # 0x4000 = Directory
            size = item[3] if isFile else 0
 
            if isFile: print(f'{{{{ "Path": "{{path}}", "File": "{{file}}", "Size": {{size}} }}}}')
            if isDir: print(f'{{{{ "Path": "{{path}}{{file}}/" }}}}')
 
            if isDir and recurse: GetChildItem(f'{{path}}{{file}}', recurse)
    except OSError:
        try:
            path = path.rstrip("/")
            mode = os.stat(path)
            isFile = (mode[0] & (1<<15)) != 0 # 0x8000 = File
            isDir = (mode[0] & (1<<14)) != 0 # 0x4000 = Directory
            size = mode[6] if isFile else 0
 
            matches = re.match("(.*/)(.*)", path)
            path = matches.group(1)
            file = matches.group(2)
 
            if isFile: print(f'{{{{ "Path": "{{path}}", "File": "{{file}}", "Size": {{size}} }}}}')
            if isDir: print(f'{{{{ "Path": "{{path}}{{file}}/" }}}}')
 
        except:
            pass
 
GetChildItem('{0}', {1})
'@


        RestartMPyDevice = @'
import machine
if {0}:
    machine.soft_reset()
else:
    machine.reset()
'@


        GetMPyDeviceDetails = @'
import sys
import uos
import platform as pf
import machine
import ubinascii
 
try:
    import lvgl as lv
    lvgl_version = "%d.%d.%d" % (lv.version_major(), lv.version_minor(), lv.version_patch())
    lvgl = f''' "lvgl": {{{{ "version": "{{lvgl_version}}"
                 }}}},'''
except:
    lvgl = ""
 
 
uname = uos.uname() # print("uos.uname: %s" % uname)
platform = pf.platform() # print("platform.platform: %s" % platform)
libc_ver = pf.libc_ver() # print("platform.libc_ver: %s %s" % libc_ver)
python_compiler = pf.python_compiler() # print("platform.python_compiler: %s" % python_compiler)
sys_version = sys.version # print("sys.version: %s" % sys_version)
unique_id = ubinascii.hexlify(machine.unique_id()).decode() # print("machine.unique_id: %s" % unique_id)
 
print(f'''
{{{{
    "uname": {{{{ "sysname": "{{uname.sysname}}",
                   "nodename": "{{uname.nodename}}",
                   "release": "{{uname.release}}",
                   "version": "{{uname.version}}",
                   "machine": "{{uname.machine}}"
                 }}}},
    "plattform": {{{{ "platform": "{{platform}}",
                   "libc_ver": "{{libc_ver[0]}} {{libc_ver[1]}}",
                   "python_compiler": "{{python_compiler}}"
                 }}}},
    "sys": {{{{ "version": "{{sys_version}}"
                 }}}},
{{lvgl}}
    "machine": {{{{ "unique_id": "{{unique_id}}"
                 }}}}
}}}}
''')
'@

    }

    [object]GetMPyContent([string]$Path)
    {
        $this.EnterRawRepl()

        $out = $this.Execute(($this.Python.GetMPyContent -f ($Path, $this.BufferSize)))

        $this.ExitRawRepl()

        $out.Result = $this.Unhexlify($out.Result)

        return $out
    }

    [object]OutMPyFile([string]$Path, [string]$Data)
    {
        $this.EnterRawRepl()

        $out = $this.Execute(($this.Python.OutMPyFile -f ($Path, $Data)))

        $this.ExitRawRepl()

        return $out
    }

    [object]RemoveMPyItem([string]$Path, [switch]$Recurse)
    {
        $this.EnterRawRepl()

        $out = $this.Execute(($this.Python.RemoveMPyItem -f ($Path, $Recurse)))

        $this.ExitRawRepl()

        return $out
    }

    [object]NewMPyPath([string]$Path)
    {
        $this.EnterRawRepl()

        $out = $this.Execute(($this.Python.NewMPyPath -f ($Path)))

        $this.ExitRawRepl()

        return $out
    }

    [object]GetMPyChildItem([string]$Path, [switch]$Recurse)
    {
        $this.EnterRawRepl()

        $out = $this.Execute(($this.Python.GetMPyChildItem -f ($Path, $Recurse)))

        $this.ExitRawRepl()
        
        $out.Result = "[ $($out.Result.Split([char[]]"`r`n", [System.StringSplitOptions]::RemoveEmptyEntries) -join ',') ]" | ConvertFrom-Json

        return $out
    }

    [object]RestartMPyDevice([switch]$Soft)
    {
        $this.EnterRawRepl()

        $out = $this.Execute(($this.Python.RestartMPyDevice -f ($Soft)))

        if($Soft)
        {
            $this.ExitRawRepl()
        }
        else
        {
            # $this.ExitRawRepl() # >>> not necessary after restart / reboot

            $this.Close() # >>> required after restart / reboot instead of "$this.ExitRawRepl()"
        }

        return $out
    }

    [object]GetMPyDeviceDetails()
    {
        $this.EnterRawRepl()

        $out = $this.Execute(($this.Python.GetMPyDeviceDetails -f ($null)))

        $this.ExitRawRepl()

        $out.Result = $out.Result | ConvertFrom-Json

        return $out
    }

    [string]Unhexlify([string]$HexString)
    {
        return (. { 

            for($zz = 0; $zz -lt $HexString.Length; $zz += 2)
            {
                [char]([Convert]::ToInt32($HexString.Substring($zz, 2), 16))
            }

        }) -join ""
    }
}

function Get-MPyPnPEntity
{
    [CmdletBinding()]
    param(
        [Parameter(ValueFromPipeline,ValueFromPipelineByPropertyName)][Alias("Port")][string]$PortName,
        [Parameter(                  ValueFromPipelineByPropertyName)]               [switch]$ToString
    )

    if($Script:State.PortName)
    {
        if($PortName -and ($PortName -ne $Script:State.PortName))
        {
            $Script:State.PnPEntity = $PortName | Find-MPyDevice
        }
    }
    else
    {
        $Script:State.PnPEntity = $PortName | Find-MPyDevice
    }

    $Script:State.PnPEntity
}

function Find-MPyDevice
{
    [CmdletBinding()]
    param(
        [Parameter(ValueFromPipelineByPropertyName,ValueFromPipeline)][Alias("Port")][string]$InstanceId
    )

    $MyInvocationName = $MyInvocation.MyCommand.Name

    $PortPattern = ".*\((?<PortName>COM\d{1,})\)"

    $PnPEntity = . {

        if($InstanceId -like "COM*")
        {
            Write-Verbose "$($MyInvocationName): by Port ($($InstanceId))"

            Get-PnpDevice -Class Ports -Status OK -FriendlyName "*($InstanceId)"
        }
        elseif($InstanceId)
        {
            Write-Verbose "$($MyInvocationName): by InstanceId ($($InstanceId))"

            Get-PnpDevice -Class Ports -Status OK -InstanceId "*$($InstanceId)*"
        }
        else
        {
            Write-Verbose "$($MyInvocationName): by FriendlyName & Manufacturer"

            Get-PnpDevice -Class Ports -Status OK -FriendlyName "USB*SERIAL*CH*(COM*)" | Where-Object Manufacturer -eq "wch.cn"
        }

    } | Where-Object Name -match $PortPattern | Select-Object -First 1

    if($PnPEntity.Name -match $PortPattern)
    {
        $PnPEntity | Add-Member -MemberType  NoteProperty -Name PortName  -Value $Matches.PortName
        $PnPEntity | Add-Member -MemberType AliasProperty -Name Port      -Value PortName
        $PnPEntity | Add-Member -MemberType  NoteProperty -Name MPyDevice -Value ([MPyDevice]::new($PnPEntity))
        $PnPEntity

        $Script:State.PnPEntity = $PnPEntity

        Write-Verbose "$($MyInvocationName): Found on PortName($($PnPEntity.PortName)) InstanceId($($PnPEntity.InstanceId))"
    }
}

function Stop-MPyProcess
{
    [CmdletBinding()]
    param(
        [Parameter(ValueFromPipeline,ValueFromPipelineByPropertyName)][Alias("Port")][string]$PortName,
        [Parameter()]                                                                [switch]$PassThru
    )

    if($PnPEntity = $PortName | Get-MPyPnPEntity)
    {
        if(-not $PnPEntity.MPyDevice.IsOpen()) { $PnPEntity.MPyDevice.Open() }

        foreach($zz in (1..10))
        {
            $PnPEntity.MPyDevice.Ctrl("C")

            sleep -Milliseconds 50
        }

        $Response = $PnPEntity.MPyDevice.ReadExisting().Trim()

        $PnPEntity.MPyDevice.Close()

        if($Response.EndsWith(">>>"))
        {
            $result = [PSCustomObject]@{

                OK        = "OK"
                Result    = $Response
                Error     = ""
                Raw       = $Response
                Exception = $null
            }
        }
        else
        {
            $result = [PSCustomObject]@{

                OK        = ""
                Result    = ""
                Error     = $Response
                Raw       = $Response
                Exception = [System.Exception]::new($Response)
            }
        }

        if($PassThru)         { return $result }
        if($result.Exception) { throw  $result.Exception }
    }
}

function Get-MPyContent
{
    [CmdletBinding()]
    param(
        [Parameter(          ValueFromPipeline,ValueFromPipelineByPropertyName)][Alias("Port")][string]$PortName,
        [Parameter(Mandatory,ValueFromPipeline,ValueFromPipelineByPropertyName)]               [string]$Path,
        [Parameter()]                                                                          [switch]$PassThru
    )

    if($PnPEntity = $PortName | Get-MPyPnPEntity)
    {
        $result = $PnPEntity.MPyDevice.GetMPyContent($Path)

        if($PassThru)         { return $result }
        if($result.Exception) { throw  $result.Exception }
        
        return $result.Result
    }
}

function Out-MPyFile
{
    [CmdletBinding()]
    param(
        [Parameter(          ValueFromPipeline,ValueFromPipelineByPropertyName)][Alias("Port")][string]$PortName,
        [Parameter(Mandatory,ValueFromPipeline,ValueFromPipelineByPropertyName)]               [string]$Path,
        [Parameter(Mandatory)]                                                                 [string]$Data,
        [Parameter()]                                                                          [switch]$PassThru
    )

    if($PnPEntity = $PortName | Get-MPyPnPEntity)
    {
        $result = $PnPEntity.MPyDevice.OutMPyFile($Path, $Data)

        if($PassThru)         { return $result }
        if($result.Exception) { throw  $result.Exception }
    }
}

function Copy-MPyItem
{
    [CmdletBinding()]
    param(
        [Parameter(ParameterSetName="MPyToPC")]
        [Parameter(ParameterSetName="PCToMPy")]
        [Parameter(                           ValueFromPipeline,ValueFromPipelineByPropertyName)][Alias("Port")][string]$PortName,

        [Parameter(ParameterSetName="MPyToPC",ValueFromPipeline,ValueFromPipelineByPropertyName,Mandatory)]     [string]$FromMPy,
        [Parameter(ParameterSetName="MPyToPC",ValueFromPipeline,ValueFromPipelineByPropertyName,Mandatory)]     [string]$ToPath,

        [Parameter(ParameterSetName="PCToMPy",ValueFromPipeline,ValueFromPipelineByPropertyName,Mandatory)]     [string]$FromPath,
        [Parameter(ParameterSetName="PCToMPy",ValueFromPipeline,ValueFromPipelineByPropertyName,Mandatory)]     [string]$ToMPy,

        [Parameter()]                                                                                           [switch]$PassThru
    )

    $MyInvocationName = $MyInvocation.MyCommand.Name

    if($PnPEntity = $PortName | Get-MPyPnPEntity)
    {
        Write-Verbose "$($MyInvocationName): $($PnPEntity.PortName) > $($PnPEntity.InstanceID) > $($PSCmdlet.ParameterSetName)"

        switch($PSCmdlet.ParameterSetName)
        {
            "MPyToPC" {
            
                $result = $PnPEntity.MPyDevice.GetMPyContent($FromMpy)

                if(-not $result.Exception)
                {
                    Set-Content -Path $ToPath -Value $result.Result
                }
            } 

            "PCToMPy" {

                $Data = Get-Content -Path $FromPath -Raw
            
                $result = $PnPEntity.MPyDevice.OutMPyFile($ToMPy, $Data)
            }
        }

        if($PassThru)         { return $result }
        if($result.Exception) { throw  $result.Exception }
    }
}

function Remove-MPyItem
{
    [CmdletBinding()]
    param(
        [Parameter(          ValueFromPipeline,ValueFromPipelineByPropertyName)][Alias("Port")][string]$PortName,
        [Parameter(Mandatory,ValueFromPipeline,ValueFromPipelineByPropertyName)]               [string]$Path,
        [Parameter()]                                                                          [switch]$Recurse,
        [Parameter()]                                                                          [switch]$PassThru
    )

    if($PnPEntity = $PortName | Get-MPyPnPEntity)
    {
        $result = $PnPEntity.MPyDevice.RemoveMPyItem($Path, $Recurse)

        if($PassThru)         { return $result }
        if($result.Exception) { throw  $result.Exception }
    }
}

function New-MPyPath
{
    [CmdletBinding()]
    param(
        [Parameter(          ValueFromPipeline,ValueFromPipelineByPropertyName)][Alias("Port")][string]$PortName,
        [Parameter(Mandatory,ValueFromPipeline,ValueFromPipelineByPropertyName)]               [string]$Path,
        [Parameter()]                                                                          [switch]$PassThru
    )

    if($PnPEntity = $PortName | Get-MPyPnPEntity)
    {
        $result = $PnPEntity.MPyDevice.NewMPyPath($Path)

        if($PassThru)         { return $result }
        if($result.Exception) { throw  $result.Exception }
    }
}

function Get-MPyChildItem
{
    [CmdletBinding()]
    param(
        [Parameter(ValueFromPipeline,ValueFromPipelineByPropertyName)][Alias("Port")][string]$PortName,
        [Parameter(ValueFromPipeline,ValueFromPipelineByPropertyName)]               [string]$Path,
        [Parameter()]                                                                [switch]$Recurse,
        [Parameter()]                                                                [switch]$PassThru
    )

    if($PnPEntity = $PortName | Get-MPyPnPEntity)
    {
        $result = $PnPEntity.MPyDevice.GetMPyChildItem($Path, $Recurse)

        if($PassThru)         { return $result }
        if($result.Exception) { throw  $result.Exception }
        
        return $result.Result
    }
}

function Restart-MPyDevice
{
    [CmdletBinding()]
    param(
        [Parameter(ValueFromPipeline,ValueFromPipelineByPropertyName)][Alias("Port")][string]$PortName,
        [Parameter()]                                                                [switch]$Soft,
        [Parameter()]                                                                [switch]$PassThru
    )

    if($PnPEntity = $PortName | Get-MPyPnPEntity)
    {
        $result = $PnPEntity.MPyDevice.RestartMPyDevice($Soft)

        if($PassThru)         { return $result }
        if($result.Exception) { throw  $result.Exception }
    }
}

function Get-MPyDeviceDetails
{
    [CmdletBinding()]
    param(
        [Parameter(ValueFromPipeline,ValueFromPipelineByPropertyName)][Alias("Port")][string]$PortName,
        [Parameter()]                                                                [switch]$PassThru
    )

    if($PnPEntity = $PortName | Get-MPyPnPEntity)
    {
        $result = $PnPEntity.MPyDevice.GetMPyDeviceDetails()

        if($PassThru)         { return $result }
        if($result.Exception) { throw  $result.Exception }
        
        return $result.Result
    }
}

function Invoke-MPyExpression
{
    [CmdletBinding()]
    param(
        [Parameter(ValueFromPipeline,ValueFromPipelineByPropertyName)][Alias("Port")][string]  $PortName,
        [Parameter()]                                                                [string[]]$Command,
        [Parameter()]                                                                [int]     $WaitMS4Result = 100
    )

    if($PnPEntity = $PortName | Get-MPyPnPEntity)
    {
        foreach($cmd in $Command)
        {
            $PnPEntity.MPyDevice.Write("$($cmd)$([char]0x0d)")
        }

        sleep -Milliseconds $WaitMS4Result

        $PnPEntity.MPyDevice.ReadExisting()
    }
}

function Invoke-MPyScript
{
    [CmdletBinding()]
    param(
        [Parameter(ValueFromPipeline,ValueFromPipelineByPropertyName)][Alias("Port")][string]$PortName,
        [Parameter()]                                                                [string]$Path,
        [Parameter()]                                                                [int]   $WaitMS4Result = 100
    )

    if($PnPEntity = $PortName | Get-MPyPnPEntity)
    {
        if(-not $Path.EndsWith(".py", [System.StringComparison]::OrdinalIgnoreCase)) { $Path += ".py" }

        $PnPEntity.MPyDevice.Write("execfile('$($Path)')$([char]0x0d)")

        sleep -Milliseconds $WaitMS4Result

        $PnPEntity.MPyDevice.ReadExisting()
    }
}

<#
    ox https://www.amazon.de/dp/B0DMN5T1GQ # Simple ESP32-S3 board
        USB-Enhanced-SERIAL CH343
        USB\VID_1A86&PID_55D3\595B066834
#>