Public/Install-PsGadgetMpyScript.ps1

# Install-PsGadgetMpyScript.ps1
#Requires -Version 5.1

function Install-PsGadgetMpyScript {
    <#
    .SYNOPSIS
        Deploy a bundled PsGadget MicroPython script to an ESP32 device.
 
    .DESCRIPTION
        Pushes the bundled espnow_receiver.py or espnow_transmitter.py to an
        ESP32 device as main.py using mpremote, along with an optional
        config.json for pin and timing overrides.
 
        After push the device is reset so main.py starts immediately.
 
        Requires mpremote on PATH: pip install mpremote
 
    .PARAMETER SerialPort
        Serial port the ESP32 is connected to (e.g. COM4, /dev/ttyUSB0).
 
    .PARAMETER Role
        Script role to deploy: Receiver or Transmitter.
        - Receiver: wired to FT232H via UART; forwards ESP-NOW traffic to host.
        - Transmitter: wireless node; sends telemetry to the receiver.
 
    .PARAMETER ConfigPath
        Optional path to a custom config.json to deploy alongside main.py.
        If omitted, the bundled mpy/scripts/config.json is used.
        Pass '-ConfigPath $null' to skip deploying any config file.
 
    .PARAMETER Force
        Skip confirmation prompt.
 
    .EXAMPLE
        Install-PsGadgetMpyScript -SerialPort "COM4" -Role Receiver
 
    .EXAMPLE
        Install-PsGadgetMpyScript -SerialPort "/dev/ttyUSB0" -Role Transmitter -Force
 
    .EXAMPLE
        Install-PsGadgetMpyScript -SerialPort "COM4" -Role Receiver -ConfigPath "./lab_pins.json"
 
    .OUTPUTS
        [PSCustomObject] with fields: Role, SerialPort, ScriptDeployed, ConfigDeployed, Success, Message
    #>

    [CmdletBinding(SupportsShouldProcess = $true)]
    param(
        [Parameter(Mandatory = $true)]
        [string]$SerialPort,

        [Parameter(Mandatory = $true)]
        [ValidateSet('Receiver', 'Transmitter')]
        [string]$Role,

        [Parameter(Mandatory = $false)]
        [AllowNull()]
        [string]$ConfigPath,

        [Parameter(Mandatory = $false)]
        [switch]$Force
    )

    $result = [PSCustomObject]@{
        Role           = $Role
        SerialPort     = $SerialPort
        ScriptDeployed = $false
        ConfigDeployed = $false
        Success        = $false
        Message        = ''
    }

    # -- locate bundled scripts directory -----------------------------------
    $scriptsDir = Join-Path $PSScriptRoot ".." "mpy" "scripts"
    $scriptsDir = [System.IO.Path]::GetFullPath($scriptsDir)

    $sourceScript = Join-Path $scriptsDir ("espnow_{0}.py" -f $Role.ToLower())

    if (-not (Test-Path -Path $sourceScript)) {
        $result.Message = "Bundled script not found: $sourceScript"
        Write-Error $result.Message
        return $result
    }

    # -- resolve config file ------------------------------------------------
    $configToDeploy = $null
    if ($PSBoundParameters.ContainsKey('ConfigPath')) {
        if (-not [string]::IsNullOrWhiteSpace($ConfigPath)) {
            if (-not (Test-Path -Path $ConfigPath)) {
                $result.Message = "ConfigPath not found: $ConfigPath"
                Write-Error $result.Message
                return $result
            }
            $configToDeploy = $ConfigPath
        }
        # else: explicitly passed $null -- skip config deploy
    } else {
        # use bundled default
        $bundledConfig = Join-Path $scriptsDir "config.json"
        if (Test-Path -Path $bundledConfig) {
            $configToDeploy = $bundledConfig
        }
    }

    # -- check mpremote available -------------------------------------------
    if (-not (Test-NativeCommand 'mpremote')) {
        $result.Message = "mpremote not found on PATH. Install with: pip install mpremote"
        Write-Error $result.Message
        return $result
    }

    # -- confirm ---------------------------------------------------------------
    if (-not $Force) {
        $prompt = "Deploy PsGadget-{0} to {1}? This will overwrite main.py on the device." -f $Role, $SerialPort
        if (-not $PSCmdlet.ShouldProcess($SerialPort, $prompt)) {
            $result.Message = "Cancelled by user."
            return $result
        }
    }

    Write-Verbose ("Deploying {0} script to {1}" -f $Role, $SerialPort)

    # -- push main.py -------------------------------------------------------
    $pushScript = Invoke-NativeProcess -FilePath 'mpremote' `
        -ArgumentList @('connect', $SerialPort, 'cp', $sourceScript, ':main.py') `
        -TimeoutSeconds 30

    if (-not $pushScript.Success) {
        $result.Message = ("Failed to push main.py: {0}" -f $pushScript.StandardError)
        Write-Error $result.Message
        return $result
    }

    $result.ScriptDeployed = $true
    Write-Verbose ("main.py deployed from: {0}" -f $sourceScript)

    # -- push config.json ---------------------------------------------------
    if ($null -ne $configToDeploy) {
        $pushConfig = Invoke-NativeProcess -FilePath 'mpremote' `
            -ArgumentList @('connect', $SerialPort, 'cp', $configToDeploy, ':config.json') `
            -TimeoutSeconds 15

        if ($pushConfig.Success) {
            $result.ConfigDeployed = $true
            Write-Verbose ("config.json deployed from: {0}" -f $configToDeploy)
        } else {
            Write-Warning ("config.json push failed (non-fatal): {0}" -f $pushConfig.StandardError)
        }
    }

    # -- reset device -------------------------------------------------------
    Write-Verbose ("Resetting device on {0}" -f $SerialPort)
    $reset = Invoke-NativeProcess -FilePath 'mpremote' `
        -ArgumentList @('connect', $SerialPort, 'reset') `
        -TimeoutSeconds 10

    if (-not $reset.Success) {
        Write-Warning ("Device reset failed (non-fatal): {0}" -f $reset.StandardError)
    }

    $result.Success = $true
    $result.Message = ("PsGadget-{0} deployed to {1}. Device reset." -f $Role, $SerialPort)
    Write-Verbose $result.Message
    return $result
}