ModbusDirectConnect.psm1

# ModbusDirectConnect PowerShell Module
# Lightweight cmdlets backed by the ModbusDirectConnect .NET runtime.

$script:ModbusRuntimePath = $null
$script:ModbusRuntimeLoaded = $false

function Get-ModbusRuntimePath {
    if ($null -ne $script:ModbusRuntimePath -and (Test-Path $script:ModbusRuntimePath)) {
        return $script:ModbusRuntimePath
    }

    $repoRoot = Split-Path -Parent $PSScriptRoot
    $candidates = @(
        (Join-Path $PSScriptRoot 'lib/mbdc.dll'),
        (Join-Path $repoRoot 'ModbusDirectConnect.CLI/bin/Release/net10.0/mbdc.dll'),
        (Join-Path $repoRoot 'ModbusDirectConnect.CLI/bin/Debug/net10.0/mbdc.dll')
    )

    foreach ($candidate in $candidates) {
        if (Test-Path $candidate) {
            $script:ModbusRuntimePath = $candidate
            return $script:ModbusRuntimePath
        }
    }

    throw "Modbus runtime assembly not found. Expected mbdc.dll in '$PSScriptRoot/lib' for packaged modules."
}

function Initialize-ModbusRuntime {
    if ($script:ModbusRuntimeLoaded) {
        return
    }

    $runtimePath = Get-ModbusRuntimePath
    Add-Type -Path $runtimePath
    $script:ModbusRuntimeLoaded = $true
}

function Test-SerialTarget {
    param(
        [string]$Target
    )

    if ([string]::IsNullOrWhiteSpace($Target)) {
        return $false
    }

    return $Target -match '^(?:(?:\\\\\.\\)?COM\d+|/dev/.+)$'
}

function Resolve-ModbusTransport {
    param(
        [Parameter(Mandatory = $true)]
        [string]$Transport,

        [string]$Target
    )

    $normalized = $Transport.Trim().ToLowerInvariant()

    switch ($normalized) {
        'auto' {
            if (Test-SerialTarget -Target $Target) {
                return 'rtu-serial'
            }

            return 'tcp'
        }
        'tcp' { return 'tcp' }
        'tcp/ip' { return 'tcp' }
        'rtu-tcp' { return 'rtu-tcp' }
        'tcp/ip-rtu' { return 'rtu-tcp' }
        'rtu-serial' { return 'rtu-serial' }
        'serial' { return 'rtu-serial' }
        default {
            throw "Unsupported transport '$Transport'."
        }
    }
}

function New-ModbusSession {
    param(
        [string]$Target,
        [int]$Port,
        [byte]$SlaveId,
        [int]$TimeoutMs,
        [string]$Transport,
        [int]$Baud
    )

    Initialize-ModbusRuntime

    if ($TimeoutMs -lt 0) {
        throw 'TimeoutMs must be zero or greater.'
    }

    $resolvedTransport = Resolve-ModbusTransport -Transport $Transport -Target $Target

    $serialBaud = $null
    if ($resolvedTransport -eq 'rtu-serial') {
        if ($Baud -le 0) {
            throw 'Baud must be a positive integer.'
        }

        $serialBaud = $Baud
    }

    $options = [ModbusDirectConnect.CLI.Transport.ConnectionOptions]::new(
        $Target,
        $null,
        $Port,
        $SlaveId,
        $TimeoutMs,
        0,
        $resolvedTransport,
        $null,
        $serialBaud,
        8,
        'N',
        '1'
    )

    $connection = [ModbusDirectConnect.CLI.Transport.EndpointResolver]::Resolve($options)
    $client = [ModbusDirectConnect.CLI.Client.ModbusClientFactory]::CreateClient($connection)

    return [pscustomobject]@{
        Client = $client
        Connection = $connection
    }
}

function Invoke-WithModbusSession {
    param(
        [string]$Target,
        [int]$Port,
        [byte]$SlaveId,
        [int]$TimeoutMs,
        [string]$Transport,
        [int]$Baud,
        [scriptblock]$Operation
    )

    $session = $null
    try {
        $session = New-ModbusSession -Target $Target -Port $Port -SlaveId $SlaveId -TimeoutMs $TimeoutMs -Transport $Transport -Baud $Baud
        return & $Operation $session
    }
    finally {
        if ($null -ne $session -and $null -ne $session.Client) {
            $session.Client.Dispose()
        }
    }
}

function New-ModbusReadObject {
    param(
        [string]$Operation,
        [int]$FunctionCode,
        [object]$Connection,
        [byte]$SlaveId,
        [uint16]$Address,
        [uint16]$Count,
        [object[]]$Values
    )

    [pscustomobject]@{
        Request = [pscustomobject]@{
            Operation = $Operation
            FunctionCode = $FunctionCode
            Transport = $Connection.Transport.ToString()
            Target = $Connection.DisplayTarget
            UnitId = [int]$SlaveId
            Address = [int]$Address
            Count = [int]$Count
        }
        Response = [pscustomobject]@{
            Values = $Values
            Count = $Values.Count
            TimestampUtc = [datetime]::UtcNow
        }
    }
}

function New-ModbusWriteObject {
    param(
        [string]$Operation,
        [int]$FunctionCode,
        [object]$Connection,
        [byte]$SlaveId,
        [uint16]$Address,
        [object[]]$Values
    )

    [pscustomobject]@{
        Request = [pscustomobject]@{
            Operation = $Operation
            FunctionCode = $FunctionCode
            Transport = $Connection.Transport.ToString()
            Target = $Connection.DisplayTarget
            UnitId = [int]$SlaveId
            Address = [int]$Address
            Count = $Values.Count
            Values = $Values
        }
        Response = [pscustomobject]@{
            Acknowledged = $true
            TimestampUtc = [datetime]::UtcNow
        }
    }
}

function Get-ModbusCoil {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [uint16]$Address,

        [Parameter(Mandatory = $true)]
        [uint16]$Count,

        [Alias('Host')]
        [string]$Target = 'localhost',

        [ValidateSet('auto', 'tcp', 'tcp/ip', 'rtu-tcp', 'tcp/ip-rtu', 'rtu-serial', 'serial')]
        [string]$Transport = 'auto',

        [int]$Port = 502,
        [byte]$SlaveId = 1,
        [int]$TimeoutMs = 5000,
        [int]$Baud = 9600
    )

    Invoke-WithModbusSession -Target $Target -Port $Port -SlaveId $SlaveId -TimeoutMs $TimeoutMs -Transport $Transport -Baud $Baud -Operation {
        param($session)
        $values = $session.Client.ReadCoilsAsync($Address, $Count).GetAwaiter().GetResult()
        New-ModbusReadObject -Operation 'ReadCoils' -FunctionCode 1 -Connection $session.Connection -SlaveId $SlaveId -Address $Address -Count $Count -Values @($values)
    }
}

function Get-ModbusDiscreteInput {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [uint16]$Address,

        [Parameter(Mandatory = $true)]
        [uint16]$Count,

        [Alias('Host')]
        [string]$Target = 'localhost',

        [ValidateSet('auto', 'tcp', 'tcp/ip', 'rtu-tcp', 'tcp/ip-rtu', 'rtu-serial', 'serial')]
        [string]$Transport = 'auto',

        [int]$Port = 502,
        [byte]$SlaveId = 1,
        [int]$TimeoutMs = 5000,
        [int]$Baud = 9600
    )

    Invoke-WithModbusSession -Target $Target -Port $Port -SlaveId $SlaveId -TimeoutMs $TimeoutMs -Transport $Transport -Baud $Baud -Operation {
        param($session)
        $values = $session.Client.ReadDiscreteInputsAsync($Address, $Count).GetAwaiter().GetResult()
        New-ModbusReadObject -Operation 'ReadDiscreteInputs' -FunctionCode 2 -Connection $session.Connection -SlaveId $SlaveId -Address $Address -Count $Count -Values @($values)
    }
}

function Get-ModbusHoldingRegister {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [uint16]$Address,

        [Parameter(Mandatory = $true)]
        [uint16]$Count,

        [Alias('Host')]
        [string]$Target = 'localhost',

        [ValidateSet('auto', 'tcp', 'tcp/ip', 'rtu-tcp', 'tcp/ip-rtu', 'rtu-serial', 'serial')]
        [string]$Transport = 'auto',

        [int]$Port = 502,
        [byte]$SlaveId = 1,
        [int]$TimeoutMs = 5000,
        [int]$Baud = 9600
    )

    Invoke-WithModbusSession -Target $Target -Port $Port -SlaveId $SlaveId -TimeoutMs $TimeoutMs -Transport $Transport -Baud $Baud -Operation {
        param($session)
        $values = $session.Client.ReadHoldingRegistersAsync($Address, $Count).GetAwaiter().GetResult()
        New-ModbusReadObject -Operation 'ReadHoldingRegisters' -FunctionCode 3 -Connection $session.Connection -SlaveId $SlaveId -Address $Address -Count $Count -Values @($values)
    }
}

function Get-ModbusInputRegister {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [uint16]$Address,

        [Parameter(Mandatory = $true)]
        [uint16]$Count,

        [Alias('Host')]
        [string]$Target = 'localhost',

        [ValidateSet('auto', 'tcp', 'tcp/ip', 'rtu-tcp', 'tcp/ip-rtu', 'rtu-serial', 'serial')]
        [string]$Transport = 'auto',

        [int]$Port = 502,
        [byte]$SlaveId = 1,
        [int]$TimeoutMs = 5000,
        [int]$Baud = 9600
    )

    Invoke-WithModbusSession -Target $Target -Port $Port -SlaveId $SlaveId -TimeoutMs $TimeoutMs -Transport $Transport -Baud $Baud -Operation {
        param($session)
        $values = $session.Client.ReadInputRegistersAsync($Address, $Count).GetAwaiter().GetResult()
        New-ModbusReadObject -Operation 'ReadInputRegisters' -FunctionCode 4 -Connection $session.Connection -SlaveId $SlaveId -Address $Address -Count $Count -Values @($values)
    }
}

function Set-ModbusCoil {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [uint16]$Address,

        [Parameter(Mandatory = $true)]
        [bool]$Value,

        [Alias('Host')]
        [string]$Target = 'localhost',

        [ValidateSet('auto', 'tcp', 'tcp/ip', 'rtu-tcp', 'tcp/ip-rtu', 'rtu-serial', 'serial')]
        [string]$Transport = 'auto',

        [int]$Port = 502,
        [byte]$SlaveId = 1,
        [int]$TimeoutMs = 5000,
        [int]$Baud = 9600
    )

    Invoke-WithModbusSession -Target $Target -Port $Port -SlaveId $SlaveId -TimeoutMs $TimeoutMs -Transport $Transport -Baud $Baud -Operation {
        param($session)
        $session.Client.WriteSingleCoilAsync($Address, $Value).GetAwaiter().GetResult()
        New-ModbusWriteObject -Operation 'WriteSingleCoil' -FunctionCode 5 -Connection $session.Connection -SlaveId $SlaveId -Address $Address -Values @($Value)
    }
}

function Set-ModbusRegister {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [uint16]$Address,

        [Parameter(Mandatory = $true)]
        [uint16]$Value,

        [Alias('Host')]
        [string]$Target = 'localhost',

        [ValidateSet('auto', 'tcp', 'tcp/ip', 'rtu-tcp', 'tcp/ip-rtu', 'rtu-serial', 'serial')]
        [string]$Transport = 'auto',

        [int]$Port = 502,
        [byte]$SlaveId = 1,
        [int]$TimeoutMs = 5000,
        [int]$Baud = 9600
    )

    Invoke-WithModbusSession -Target $Target -Port $Port -SlaveId $SlaveId -TimeoutMs $TimeoutMs -Transport $Transport -Baud $Baud -Operation {
        param($session)
        $session.Client.WriteSingleRegisterAsync($Address, $Value).GetAwaiter().GetResult()
        New-ModbusWriteObject -Operation 'WriteSingleRegister' -FunctionCode 6 -Connection $session.Connection -SlaveId $SlaveId -Address $Address -Values @($Value)
    }
}

function Set-ModbusCoils {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [uint16]$Address,

        [Parameter(Mandatory = $true)]
        [bool[]]$Values,

        [Alias('Host')]
        [string]$Target = 'localhost',

        [ValidateSet('auto', 'tcp', 'tcp/ip', 'rtu-tcp', 'tcp/ip-rtu', 'rtu-serial', 'serial')]
        [string]$Transport = 'auto',

        [int]$Port = 502,
        [byte]$SlaveId = 1,
        [int]$TimeoutMs = 5000,
        [int]$Baud = 9600
    )

    Invoke-WithModbusSession -Target $Target -Port $Port -SlaveId $SlaveId -TimeoutMs $TimeoutMs -Transport $Transport -Baud $Baud -Operation {
        param($session)
        $session.Client.WriteMultipleCoilsAsync($Address, $Values).GetAwaiter().GetResult()
        New-ModbusWriteObject -Operation 'WriteMultipleCoils' -FunctionCode 15 -Connection $session.Connection -SlaveId $SlaveId -Address $Address -Values @($Values)
    }
}

function Set-ModbusRegisters {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [uint16]$Address,

        [Parameter(Mandatory = $true)]
        [uint16[]]$Values,

        [Alias('Host')]
        [string]$Target = 'localhost',

        [ValidateSet('auto', 'tcp', 'tcp/ip', 'rtu-tcp', 'tcp/ip-rtu', 'rtu-serial', 'serial')]
        [string]$Transport = 'auto',

        [int]$Port = 502,
        [byte]$SlaveId = 1,
        [int]$TimeoutMs = 5000,
        [int]$Baud = 9600
    )

    Invoke-WithModbusSession -Target $Target -Port $Port -SlaveId $SlaveId -TimeoutMs $TimeoutMs -Transport $Transport -Baud $Baud -Operation {
        param($session)
        $session.Client.WriteMultipleRegistersAsync($Address, $Values).GetAwaiter().GetResult()
        New-ModbusWriteObject -Operation 'WriteMultipleRegisters' -FunctionCode 16 -Connection $session.Connection -SlaveId $SlaveId -Address $Address -Values @($Values)
    }
}

Export-ModuleMember -Function @(
    'Get-ModbusCoil',
    'Get-ModbusDiscreteInput',
    'Get-ModbusHoldingRegister',
    'Get-ModbusInputRegister',
    'Set-ModbusCoil',
    'Set-ModbusRegister',
    'Set-ModbusCoils',
    'Set-ModbusRegisters'
)