PSChiaPlotter.psm1

enum KSize{
    K32 = 32
    K33 = 33
    K34 = 34
    K35 = 35
}

class MaximizedKSize {
    [KSize]$KSize
    [int]$MaxPlots
    [Decimal]$RemainingBytes
    [Decimal]$KSizeBytes
    [int64]$TotalBytes

    static [Decimal]$K35 = 884.1 * 1gb
    static [Decimal]$K34 = 429.8 * 1gb
    static [Decimal]$K33 = 208.8 * 1gb
    static [Decimal]$K32 = 101.4 * 1gb

    MaximizedKSize(
        [KSize]$KSize,
        [int64]$TotalBytes
    ){
        $this.KSize = $Ksize
        $this.TotalBytes = $TotalBytes

        $this.KSizeBytes = switch ($this.KSize){
            "K35" {[MaximizedKSize]::K35}
            "K34" {[MaximizedKSize]::K34}
            "K33" {[MaximizedKSize]::K33}
            "K32" {[MaximizedKSize]::K32}
        }
        $this.MaxPlots = [math]::Floor([decimal]($this.TotalBytes / $this.KSizeBytes))
        $this.RemainingBytes = $Totalbytes - (([math]::Floor([decimal]($this.TotalBytes / $this.KSizeBytes))) * $this.KSizeBytes)
    }
}
class OptimizedKPlots {
    [int]$K35
    [int]$K34
    [int]$K33
    [int]$K32
    [decimal]$RemainingBytes
    [double]$RemainingGB

    OptimizedKPlots (
        [int]$K35,
        [int]$K34,
        [int]$K33,
        [int64]$Totalbytes
    ){
        $sizeremaining = $TotalBytes - (($K35 * [MaximizedKSize]::K35) + ($K34 * [MaximizedKSize]::K34) + ($K33 * [MaximizedKSize]::K33))
        $k32max = Get-MaxKSize -Totalbytes $sizeremaining -KSize "K32"
        $this.K35 = $K35
        $this.K34 = $K34
        $this.K33 = $K33
        $this.K32 = $k32max.MaxPlots
        $this.RemainingBytes = $k32max.RemainingBytes
        $this.RemainingGB = [math]::Round($k32max.RemainingBytes / 1gb,2)
    }
}
function ConvertTo-FriendlyTimeSpan {
    [CmdletBinding()]
    param(
        [int32]$Seconds
    )

    $TimeSpan = New-TimeSpan -Seconds $Seconds
    switch ($TimeSpan){
        {$_.Days -ge 1} {return "$([math]::Round($TimeSpan.TotalDays,2)) days";break}
        {$_.Hours -ge 1} {return "$([math]::Round($TimeSpan.TotalHours,2)) hrs";break}
        {$_.Minutes -ge 1} {return "$([math]::Round($TimeSpan.TotalMinutes,2)) mins";break}
        {$_.seconds -ge 1} {return "$([math]::Round($TimeSpan.TotalSeconds,2)) sec";break}
    }
}
function Get-ChiaHarvesterActivity {
    [CmdletBinding()]
    param(
        [string[]]$DebugLogFilePath = (Get-ChildItem -Path "$ENV:USERPROFILE\.chia\mainnet\log" -filter "debug.log*").FullName,
        [switch]$Summary
    )
    $chiaharvesterlog = "([0-9:.\-T]*) harvester chia.harvester.harvester: INFO\s*([0-9]*) plots were eligible for farming ([a-z0-9.]*) Found ([0-9]*) proofs. Time: ([0-9.]*) s. Total ([0-9]*) plots"
    foreach ($logfile in $DebugLogFilePath){
        try{
            $SummaryLog = New-Object 'System.Collections.Generic.List[System.Object]'
            Get-Content -Path $logfile | foreach-object {
                switch -Regex ($_){
                    $chiaharvesterlog {
                        $harvesterActivity = [pscustomobject]@{
                            PSTypeName = "PSChiaPlotter.ChiaHarvesterActivity"
                            Time = [datetime]::parse($Matches[1])
                            EligiblePlots = $Matches[2]
                            LookUpTime = [double]$Matches[5]
                            ProofsFound = $Matches[4]
                            TotalPlots = $Matches[6]
                            FilterRatio = $Matches[2] / $Matches[6]
                        } #psobject
                        if (-not$Summary){
                            $harvesterActivity
                        }
                        else{
                            $SummaryLog.Add($harvesterActivity)
                        }
                    }
                } #switch
            } #foreach line
            if ($Summary){
                $FirstandLast = $SummaryLog | sort Time -Descending | select -First 1 -Last 1 | sort -Descending
                $RunTime = $FirstandLast[1].Time - $FirstandLast[0].Time
                if ($RunTime -ne 0){$ChallengesPerMinute = $SummaryLog.Count / $RunTime.TotalMinutes}
                [PSCustomObject]@{
                    PSTypeName = "PSChiaPlotter.ChiaHarvesterSummary"
                    RunTime = $RunTime
                    TotalEligiblePlots = ($SummaryLog | Measure-Object EligiblePlots -Sum).Sum
                    BestLookUpTime = ($SummaryLog | Measure-Object LookUpTime -Minimum).Minimum
                    WorstLookUpTime = ($SummaryLog | Measure-Object LookUpTime -Maximum).Maximum
                    AverageLookUpTime = ($SummaryLog | Measure-Object LookUpTime -Average).Average
                    ProofsFound = ($SummaryLog | Measure-Object -Property ProofsFound -Sum).Sum
                    FilterRatio = ($SummaryLog | Measure-Object -Property FilterRatio -Average).Average
                    ChallengesPerMinute = $ChallengesPerMinute
                }
            }
        }
        catch{
            $PSCmdlet.WriteError($_)
        }
    } #foreach
}
function Get-ChiaKPlotCombination{
    [CmdletBinding(DefaultParameterSetName = "DriveLetter")]
    param(
        [Parameter(ParameterSetName="FreeSpace")]
        [int64[]]$FreeSpace,
        [Parameter(ParameterSetName="DriveLetter")]
        [string[]]$DriveLetter = (Get-Volume).DriveLetter
    )

    if ($PSCmdlet.ParameterSetName -eq "FreeSpace"){
        foreach ($space in $FreeSpace){
            $Max = Get-MaxKSize -TotalBytes $space
            $AllCombos = Get-OptimizedKSizePlotNumbers $Max | sort RemainingBytes
            $AllCombos | Add-Member -MemberType NoteProperty -Name "StartingFreeSpace" -Value $space
            $AllCombos
        }
    }
    elseif ($PSCmdlet.ParameterSetName -eq "DriveLetter"){
        foreach ($letter in $DriveLetter){
            $Drive = Get-Volume -DriveLetter $letter
            $Max = Get-MaxKSize -TotalBytes $Drive.SizeRemaining
            $AllCombos = Get-OptimizedKSizePlotNumbers $Max | sort RemainingBytes
            $AllCombos | Add-Member -NotePropertyMembers @{
                DriveLetter = $letter
                FriendlyName = $Drive.FileSystemLabel
            }
            $AllCombos | foreach {$_.psobject.TypeNames.Insert(0,"PSChiaPlotter.KSizeCombination")}
            $AllCombos
        }
    }
}
function Get-ChiaMaxParallelCount {
    [CmdletBinding()]
    param(
        [Parameter()]
        [ValidateRange(1,128)]
        [int]$ThreadCount = 2,
        [Parameter()]
        [ValidateRange(1, [int]::MaxValue)]
        [int]$BufferMiB = 3390
    )

    if (!$PSBoundParameters.ContainsKey("ThreadCount") -and !$PSBoundParameters.ContainsKey("BufferMiB")){
        Write-Warning "All calculations based on plotting k32 plot size only. SSD TB suggestion rounded up to the nearest TB."
    }
    else{
        Write-Warning "SSD TB suggestion rounded up to the nearest TB."
    }
    $Processor = Get-CimInstance -ClassName Win32_Processor
    $Threads = ($Processor | measure -Property ThreadCount -Sum).Sum
    $MaxParallelCountCPU = [math]::Floor($Threads / $ThreadCount)
    #1mb = 1048576 bytes
    $RAM = (Get-CimInstance -ClassName Win32_PhysicalMemory | measure -Property Capacity -Sum).Sum / 1mb
    $MaxParallelCountRAM = [Math]::Floor([decimal]($RAM / $BufferMiB))

    $SystemDisk = Get-CimInstance -Namespace ROOT/Microsoft/Windows/Storage -ClassName MSFT_Disk -Filter "IsSystem=True"
    $SSDs = Get-CimInstance -Namespace root/microsoft/windows/storage -ClassName MSFT_PhysicalDisk -Filter "MediaType=4" #4 -eq SSD
    $SSDs = $SSDs | where UniqueId -ne $SystemDisk.UniqueId | select 
    $One_TB = 1000000000000
    $One_GB = 1000000000
    $TotalSSDspace = ($SSDs | measure -Property Size -Sum).Sum
    $SSD_Count = ($SSDs | Measure-Object).Count
    if ($SSD_Count -eq 0){
        Write-Warning "No non-system SSD found, therefore Current_MaxParallelPlots will be 0. (Ignore if using mutiple HDDs)"
    }

    if ($Threads -gt ($Processor.NumberOfCores * 2)){
        Write-Warning "Threads may actually only be half what is reported and therefore all calculations are off."
    }

    $SSD_MAX = [math]::Floor([decimal]($TotalSSDspace / (256.6 * $One_GB)))

    if ($MaxParallelCountCPU -le $MaxParallelCountRAM){
        $MAXCount = $MaxParallelCountCPU
        $BottleNeck = "CPU"        
    }
    else{
        $MAXCount = $MaxParallelCountRAM
        $BottleNeck = "RAM"        
    }
    $Suggested_SSD_TB = [math]::Ceiling([decimal](256.6 * $MAXCount) / 1000)

    if ($SSD_MAX -le $MAXCount){
        $CurrentMax = $SSD_MAX
        $BottleNeck = "SSD"
    }
    else{
        $CurrentMax = $MAXCount
    }

    $Suggested_SSD_TB = [math]::Ceiling([decimal](256.6 * $MAXCount) / 1000)

    [PSCustomObject]@{
        ThreadCount = $ThreadCount
        Buffer = $BufferMiB
        CPUTotalThreads = $Threads
        CPUCores = ($Processor | Measure -Property NumberOfCores -Sum).Sum
        NumberOfProcessors = ($Processor | measure).Count
        TotalRAM_MiB = $RAM
        BottleNeck = $BottleNeck
        Current_SSD_SPACE_TB = [math]::Round(($TotalSSDspace / $One_TB),2)
        Current_SSD_Count = $SSD_Count
        Suggested_SSD_SPACE_TB = $Suggested_SSD_TB
        Current_MaxParallelPlots = $CurrentMax
        Potential_MAXParallelPlots = $MAXCount
    }
}
function Get-ChiaPlotProgress {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [ValidateScript({Test-Path -Path $_})]
        [string]$LogPath
    )

    if ([System.IO.Directory]::Exists($LogPath)){
        Write-Error "You provided a directory path and not a file path to the log file" -ErrorAction Stop
    }

    #base code from https://github.com/swar/Swar-Chia-Plot-Manager/blob/7287eef4796dbfa4cc009086c6502d19f0706f3e/config.yaml.default
    $phase1_line_end = 801
    $phase2_line_end = 834
    $phase3_line_end = 2474
    $phase4_line_end = 2620
    $copyfile_line_end = 2627
    $phase1_weight = 33
    $phase2_weight = 20
    $phase3_weight = 42
    $phase4_weight = 3
    $copyphase_weight = 2

    $LogItem = Get-Item -Path $LogPath
    $StartTime = $LogItem.CreationTime
    $EndTime = Get-Date
    $ElaspedTime = New-TimeSpan -Start $StartTime -End $EndTime

    $LogFile = Get-Content -Path $LogPath
    $plotId = $LogFile | Select-String -SimpleMatch "ID: " | foreach {$_.ToString().Split(" ")[1]}
    $line_count = $LogFile.Count

    if ($line_count -ge $phase1_line_end){
        $progress += $phase1_weight
    }
    else{
        $progress += $phase1_weight * ($line_count / $phase1_line_end)
        $Est_TimeRemaining = ($ElaspedTime.TotalSeconds * 100) / $progress
        $secondsRemaining = [int]($Est_TimeRemaining - $ElaspedTime.TotalSeconds)
        return [PSCustomObject]@{
            Progress = [math]::Round($progress,2)
            Phase = "Phase 1"
            ElaspedTime = $ElaspedTime
            EST_TimeReamining = New-TimeSpan -Seconds $secondsRemaining
            PlotId = $plotId
        }
    }
    if ($line_count -ge $phase2_line_end){
        $progress += $phase2_weight
    }
    else{
        $progress += $phase2_weight * (($line_count - $phase1_line_end) / ($phase2_line_end - $phase1_line_end))
        $Est_TimeRemaining = ($ElaspedTime.TotalSeconds * 100) / $progress
        $secondsRemaining = [int]($Est_TimeRemaining - $ElaspedTime.TotalSeconds)
        return [PSCustomObject]@{
            Progress = [math]::Round($progress,2)
            Phase = "Phase 2"
            ElaspedTime = $ElaspedTime
            EST_TimeReamining = New-TimeSpan -Seconds $secondsRemaining   
            PlotId = $plotId
        }
    }
    if ($line_count -ge $phase3_line_end){
        $progress += $phase3_weight
    }
    else{
        $progress += $phase3_weight * (($line_count - $phase2_line_end) / ($phase3_line_end - $phase2_line_end))
        $Est_TimeRemaining = ($ElaspedTime.TotalSeconds * 100) / $progress
        $secondsRemaining = [int]($Est_TimeRemaining - $ElaspedTime.TotalSeconds)
        return [PSCustomObject]@{
            Progress = [math]::Round($progress,2)
            Phase = "Phase 3"
            ElaspedTime = $ElaspedTime
            EST_TimeReamining = New-TimeSpan -Seconds $secondsRemaining
            PlotId = $plotId
        }
    }
    if ($line_count -ge $phase4_line_end){
        $progress += $phase4_weight
    }
    else{
        $progress += $phase4_weight * (($line_count - $phase3_line_end) / ($phase4_line_end - $phase3_line_end))
        $Est_TimeRemaining = ($ElaspedTime.TotalSeconds * 100) / $progress
        $secondsRemaining = [int]($Est_TimeRemaining - $ElaspedTime.TotalSeconds)
        return [PSCustomObject]@{
            Progress = [math]::Round($progress,2)
            Phase = "Phase 4"
            ElaspedTime = $ElaspedTime
            EST_TimeReamining = New-TimeSpan -Seconds $secondsRemaining
            PlotId = $plotId
        }
    }
    if ($line_count -lt $copyfile_line_end){
        $Est_TimeRemaining = ($ElaspedTime.TotalSeconds * 100) / $progress
        $secondsRemaining = [int]($Est_TimeRemaining - $ElaspedTime.TotalSeconds)
        return [PSCustomObject]@{
            Progress = [math]::Round($progress,2)
            Phase = "Copying"
            ElaspedTime = $ElaspedTime
            EST_TimeReamining = New-TimeSpan -Seconds $secondsRemaining
            PlotId = $plotId
        }
    }
    $progress += $copyphase_weight
    return [PSCustomObject]@{
        Progress = [math]::Round($progress,2)
        Phase = "Completed"
        ElaspedTime = New-TimeSpan -Start $StartTime -End $LogItem.LastWriteTime
        EST_TimeReamining = 0
        PlotId = $plotId
    }
}
function Get-ChiaPlottingStatistic {
    [CmdletBinding()]
    param(
        [Parameter(ValueFromPipeline,ValueFromPipelineByPropertyName)]
        [string[]]$Path = (Get-ChildItem -Path $env:USERPROFILE\.chia\mainnet\plotter\ | sort CreationTime -Descending).FullName
    )

    Process{
        foreach ($log in $path){
            if (Test-Path $log){
                $Content = Get-Content -Path $log | Select-String "Time for phase","Total time","Plot size","Buffer size","threads of stripe","Copy time","Copied final file from","Starting plotting progress into temporary dirs" | foreach {$_.ToString()}
                foreach ($line in $Content){
                    switch -Wildcard ($line){
                        "Plot size*" {$PlotSize = $line.split(' ') | select -Skip 3} #using select for these since indexing will error if empty
                        "Buffer Size*" {$BufferSize = ($line.Split(' ') | select -Skip 3).split("M") | select -First 1}
                        "*threads*" {$ThreadCount = $line.split(' ') | select -First 1 -Skip 1}
                        "*phase 1*" {$Phase_1 = $line.Split(' ') | select -First 1 -Skip 5}
                        "*phase 2*" {$Phase_2 = $line.Split(' ') | select -First 1 -Skip 5}
                        "*phase 3*" {$Phase_3 = $line.Split(' ') | select -First 1 -Skip 5}
                        "*phase 4*" {$phase_4 = $line.Split(' ') | select -First 1 -Skip 5}
                        "Total time*" {$TotalTime = $line.Split(' ') | select -First 1 -Skip 3}
                        "Copy time*" {$CopyTime = $line.Split(' ') | select -First 1 -Skip 3}
                        "Starting plotting progress into temporary dirs*" {$TempDrive = ($line.Split(' ') | select -First 1 -Skip 6).Split('\') | select -First 1 }
                        "Copied final file from*" {$FinalDrive = ($line.Split(' ') | select -First 1 -Skip 6).Split('\').Replace('"', '') | select -First 1}
                        default {Write-Information "Could not match line: $line"}
                    }
                }
                [PSCustomObject]@{
                    PSTypeName = "PSChiaPlotter.ChiaPlottingStatistic"
                    KSize = $PlotSize
                    "RAM(MiB)" = $BufferSize
                    Threads = $ThreadCount
                    "Phase_1_sec" = [int]$Phase_1
                    "Phase_2_sec" = [int]$Phase_2
                    "Phase_3_sec" = [int]$Phase_3
                    "Phase_4_sec" = [int]$phase_4
                    "TotalTime_sec" = [int]$TotalTime
                    "CopyTime_sec" = [int]$CopyTime
                    "PlotAndCopyTime_sec" = ([int]$CopyTime + [int]$TotalTime)
                    "Time_Started" = (Get-Item -Path $log).CreationTime 
                    "Temp_drive" = $TempDrive
                    "Final_drive" = $FinalDrive
                }
                Clear-Variable -Name "Phase_1","Phase_2","Phase_3","Phase_4","TotalTime","CopyTime" -ErrorAction SilentlyContinue
            }
        }
    }
}
function Get-ChiaProcessCounter{
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [int[]]$ChiaPID
    )

    foreach ($ID in $ChiaPID){
        $QueryString += "OR IDProcess=$ID "
    }

    $Performance = Get-CimInstance -Query "Select workingSetPrivate,PercentProcessorTime,IDProcess FROM Win32_PerfFormattedData_PerfProc_Process WHERE NAME='_Total' $QueryString"
    $TotalCPU = $Performance | where {$_.Name -eq '_Total'}
    $ChiaProcesses = $Performance | where {$_.Name -ne '_Total'}
    foreach ($process in $ChiaProcesses){
        if ($process.PercentProcessorTime -ne 0){
            $CPUPer = ($process.PercentProcessorTime / $TotalCPU.PercentProcessorTime) * 100
            $RoundedCPU = [math]::Round($CPUPer,2)
        }
        else{$CPUPer = 0}

        [PSCustomObject]@{
            ChiaPID = $process.IDProcess
            CPUPercent = $RoundedCPU
        }
    }
}
function Get-ChiaRAMInfo {
    [CmdletBinding()]
    param(
    )

    Begin{

    } #Begin

    Process{
    
        $Array = Get-CimInstance -Class Win32_PhysicalMemoryArray
        $CurrentRAM = Get-CimInstance -Class Win32_PhysicalMemory

        [PSCustomObject]@{
            PSTypeName = "PSChiaPlotter.RAMInfo"
            ComputerName = $ENV:COMPUTERNAME
            SlotsInUse = ($CurrentRAM | Measure).Count
            SlotsFree = $Array.MemoryDevices - ($CurrentRAM | Measure).Count
            CurrentSize_GB = (($CurrentRAM).Capacity | Measure -Sum).Sum / 1gb
            MaxSize_GB = $Array.MaxCapacityEx / 1mb
            PartNumber = ($CurrentRAM.PartNumber | Select -Unique | foreach {$_.Trim()})
            Manufacturer = ($CurrentRAM.Manufacturer | Select -Unique | foreach {$_.Trim()})
            TotalSlots = $Array.MemoryDevices
            RAMDevices = $CurrentRAM
        }
    } #Process
}
function Start-ChiaHarvesterWatcher {
    [CmdletBinding()]
    param(
        [string]$DebugLogFilePath = (Get-ChildItem -Path "$ENV:USERPROFILE\.chia\mainnet\log" -filter "debug.log").FullName,
        [ValidateRange(1,1000)]
        [int]$Sensitivity = 1,
        [Parameter()]
        [ValidateScript({[System.IO.Directory]::Exists((Split-Path -Path $_ -Parent))})]
        [string]$ExportCSVPath
    )

    if ($PSBoundParameters.ContainsKey("ExportCSVPath")){
        if (-not($ExportCSVPath.EndsWith('.csv'))){
            Write-Warning "Export CSV Path does not end with .csv, please provide a valid CSV path and run the command again... exiting."
            return
        }
    }


    $chiaharvesterlog = "([0-9:.\-T]*) harvester chia.harvester.harvester: INFO\s*([0-9]*) plots were eligible for farming ([a-z0-9.]*) Found ([0-9]*) proofs. Time: ([0-9.]*) s. Total ([0-9]*) plots"
    $BestSpeed = 1000
    $WorstSpeed = 0
    $Over1Seconds = 0
    $Over5Seconds = 0
    $Over30Seconds = 0
    $TotalAttempts = 0
    $TotalFilterRatio = 0
    $TotalLookupTime = 0
    $proofsFound = 0

    Get-Content -Path $DebugLogFilePath -Wait | foreach-object {
        switch -Regex ($_){
            $chiaharvesterlog {
                $harvesterActivity = [pscustomobject]@{
                    Time = [datetime]::parse($Matches[1])
                    EligiblePlots = $Matches[2]
                    LookUpTime = [double]$Matches[5]
                    ProofsFound = $Matches[4]
                    TotalPlots = $Matches[6]
                    FilterRatio = $Matches[2] / $Matches[6]
                }
                $TotalAttempts++
                switch ($harvesterActivity.LookUpTime) {
                    {$_ -lt $BestSpeed} {$BestSpeed = $_}
                    {$_ -gt $WorstSpeed} {$WorstSpeed = $_}
                    {$_ -ge 1} {$Over1Seconds++}
                    {$_ -ge 5} {$Over5Seconds++}
                    {$_ -ge 30} {$Over30Seconds++}
                }
                if ($PSBoundParameters.ContainsKey("ExportCSVPath")){
                    $harvesterActivity | Export-Csv -Path $ExportCSVPath -Append
                }
                $proofsFound += $harvesterActivity.ProofsFound
                $TotalLookupTime += $harvesterActivity.LookUpTime
                $AverageSpeed = [math]::Round(($TotalLookupTime / $TotalAttempts),5)
                $TotalFilterRatio += $harvesterActivity.FilterRatio
                $newRatio = [math]::Round(($TotalFilterRatio / $TotalAttempts),5)
                $RGB = [math]::Round((255 * $harvesterActivity.LookUpTime * $Sensitivity) / 5)
                $eligibleplots = " "
                if ($harvesterActivity.EligiblePlots -gt 0){
                    $eligibleplots = $harvesterActivity.EligiblePlots
                }
                $host.UI.RawUI.WindowTitle = "Total Attempts: $TotalAttempts || LookUp Time - Best: $BestSpeed, Worst: $WorstSpeed, Avg: $AverageSpeed || Over 1 Sec:$Over1Seconds, Over 5 Sec: $Over5Seconds, Over 30 Sec: $Over30Seconds || FilterRatio: $newRatio || Proofs Found: $proofsFound || RGB: $RGB"
                Write-RGBText -Text "$eligibleplots|" -bRed ([math]::Min($RGB,255)) -bGreen ([math]::max([math]::Min(255,(510 - $RGB)),0)) -NoNewLine -UnderLine
            }
        } #switch
    } #foreach
}
function Start-ChiaParallelPlotting {
    param(
        [ValidateRange(1,128)]
        [int]$ParallelCount = 1,

        [ValidateRange(0,[int]::MaxValue)]
        [Alias("Delay")]
        [int]$DelayInSeconds = 3600,

        [int]$PlotsPerQueue = 1,
        [ValidateRange(3390,[int]::MaxValue)]
        [int]$Buffer = 3390,
        [ValidateRange(1,128)]
        [int]$Threads = 2,

        [Parameter(Mandatory)]
        [ValidateScript({[System.IO.Directory]::Exists($_)})]
        [string]$TempDirectoryPath,
        [Parameter(Mandatory)]
        [ValidateScript({[System.IO.Directory]::Exists($_)})]
        [string]$FinalDirectoryPath,
        [Parameter(Mandatory)]
        [ValidateScript({[System.IO.Directory]::Exists($_)})]
        [string]$LogDirectoryPath = "$ENV:USERPROFILE\.chia\mainnet\plotter",

        [switch]$NoExit,

        [ValidateNotNullOrEmpty()]
        [string]$WindowTitle
    )

    if ($PSBoundParameters.ContainsKey("WindowTitle")){
        $AddTitle = "-WindowTitle $WindowTitle"
    }

    for ($Queue = 1; $Queue -le $ParallelCount;$Queue++){
        if ($NoExit){
            $NoExitFlag = "-NoExit"
        }
        $processParam = @{
            FilePath = "powershell.exe"
            ArgumentList = "$NoExitFlag -Command Start-ChiaPlotting -TotalPlots $plotsperQueue -Buffer $Buffer -Threads $Threads -TempDirectoryPath $TempDirectoryPath -FinalDirectoryPath $FinalDirectoryPath -LogDirectoryPath $LogDirectoryPath -QueueName Queue_$Queue $AddTitle"
        }
        Start-Process @processParam
        if ($Queue -lt $ParallelCount){
            Start-Sleep -Seconds $DelayInSeconds
        }
    } #for
}
function Start-ChiaPlotting {
    [CmdletBinding()]
    param(
        [ValidateRange(32,35)]
        [int]$KSize = 32,
    
        [ValidateRange(1,5000)]
        [int]$TotalPlots = 1,
    
        [int]$Buffer,

        [ValidateRange(1,256)]
        [int]$Threads = 2,

        [switch]$DisableBitfield,
        [switch]$ExcludeFinalDirectory,
    
        [Parameter(Mandatory)]
        [ValidateScript({[System.IO.Directory]::Exists($_)})]
        [string]$TempDirectoryPath,

        [Parameter()]
        [ValidateScript({[System.IO.Directory]::Exists($_)})]
        [string]$SecondTempDirecoryPath,

        [Parameter(Mandatory)]
        [ValidateScript({[System.IO.Directory]::Exists($_)})]
        [string]$FinalDirectoryPath,

        #$FarmerPublicKey,
        #$PoolPublicKey,

        [ValidateScript({[System.IO.Directory]::Exists($_)})]
        [string]$LogDirectoryPath = "$ENV:USERPROFILE\.chia\mainnet\plotter",

        [switch]$NewWindow,

        [string]$QueueName = "Default_Queue",

        [string]$WindowTitle
    )

    if (-not$PSBoundParameters.ContainsKey("Buffer")){
        switch ($KSize){
            32 {$Buffer = 3390}
            33 {$Buffer = 7400}
            34 {$Buffer = 14800}
            35 {$Buffer = 29600}
        }
        Write-Information "Buffer set to: $Buffer"
    }

    if ($PSBoundParameters.ContainsKey("WindowTitle")){
        $WindowTitle = $WindowTitle + " |"
    }

    $E = if ($DisableBitfield){"-e"}
    $X = if ($ExcludeFinalDirectory){"-x"}

    #remove any trailing '\' since chia.exe hates them
    $TempDirectoryPath = $TempDirectoryPath.TrimEnd('\')
    $FinalDirectoryPath = $FinalDirectoryPath.TrimEnd('\')

    #path to chia.exe
    $ChiaPath = (Get-Item -Path "$ENV:LOCALAPPDATA\chia-blockchain\app-*\resources\app.asar.unpacked\daemon\chia.exe").FullName
    $ChiaArguments = "plots create -k $KSize -b $Buffer -r $Threads -t `"$TempDirectoryPath`" -d `"$FinalDirectoryPath`" $E $X"


    if ($PSBoundParameters.ContainsKey("SecondTempDirecoryPath")){
        $SecondTempDirecoryPath = $SecondTempDirecoryPath.TrimEnd('\')
        $ChiaArguments += " -2 $SecondTempDirecoryPath"
        Write-Information "Added 2nd Temp Dir to Chia ArguementList"
    }

    if (Test-Path $LogDirectoryPath){
        $LogPath = Join-Path $LogDirectoryPath ((Get-Date -Format yyyy_MM_dd_hh-mm-ss-tt_) + "plotlog" + ".log")
    }
    else{
        $Message = "The log path provided was not found: $LogDirectoryPath"
        $ErrorRecord = [System.Management.Automation.ErrorRecord]::new(
                [System.IO.FileNotFoundException]::new($Message,$SErvicePath),
                'LogPathInvalid',
                [System.Management.Automation.ErrorCategory]::ObjectNotFound,
                "$LogDirectoryPath"
            )
            $PSCmdlet.ThrowTerminatingError($ErrorRecord)
        $PSCmdlet.ThrowTerminatingError("Invalid Log Path Directory: $LogDirectoryPath")
    }

    if ($ChiaPath){
        Write-Information "Chia path exists, starting the plotting process"
        if (!$NewWindow){
            for ($plotNumber = 1;$plotNumber -le $TotalPlots;$plotNumber++){
                try{
                    $LogPath = Join-Path $LogDirectoryPath ((Get-Date -Format yyyy_MM_dd_hh-mm-ss-tt_) + "plotlog-" + $plotNumber + ".log")
                    $PlottingParam = @{
                        FilePath = $ChiaPath
                        ArgumentList = $ChiaArguments
                        RedirectStandardOutput = $LogPath
                        NoNewWindow = $true
                    }
                    $chiaProcess = Start-Process @PlottingParam -PassThru
                    $host.ui.RawUI.WindowTitle = "$WindowTitle $QueueName - Plot $plotNumber out of $TotalPlots | Chia Process Id - $($chiaProcess.id)"

                    #Have noticed that giving the process a second to start before checking the logs works better
                    Start-Sleep 1
                
                    while (!$chiaProcess.HasExited){
                        try{
                            $progress = Get-ChiaPlotProgress -LogPath $LogPath -ErrorAction Stop
                            $plotid = $progress.PlotId
                            #write-progress will fail if secondsremaining is less than 0...
                            $secondsRemaining = $progress.EST_TimeReamining.TotalSeconds
                            if ($progress.EST_TimeReamining.TotalSeconds -le 0){
                                $secondsRemaining = 0
                            }
                            Write-Progress -Activity "Queue $($QueueName): Plot $plotNumber out of $TotalPlots" -Status "$($progress.phase) - $($progress.Progress)%" -PercentComplete $progress.progress -SecondsRemaining $secondsRemaining
                            Start-Sleep 5
                        }
                        catch{
                            Write-Progress -Activity "Queue $($QueueName): Plot $plotNumber out of $TotalPlots" -Status "WARNING! PROGRESS UPDATES HAS FAILED! $($progress.phase) - $($progress.Progress)%" -PercentComplete $progress.progress -SecondsRemaining $secondsRemaining
                            Start-Sleep 30
                        }
                    } #while
                    if ($chiaProcess.ExitCode -ne 0){
                        Get-ChildItem -Path $TempDirectoryPath -Filter "*$plotid*.tmp" | Remove-Item -Force
                    }
                }
                catch{
                    $PSCmdlet.WriteError($_)
                }
            } #for
        } #if noNewWindow
        else{
            $ChiaArguments += " -n $TotalPlots"
            $PlottingParam = @{
                FilePath = $ChiaPath
                ArgumentList = $ChiaArguments
                RedirectStandardOutput = $LogPath
            }
            $PlottingProcess = Start-Process @PlottingParam -PassThru
            [PSCustomObject]@{
                KSize = $KSize
                Buffer = $Buffer
                Threads = $Threads
                PID = $PlottingProcess.Id
                StartTime = $PlottingProcess.StartTime
                TempDir = $TempDirectoryPath
                FinalDir = $FinalDirectoryPath
                TempDir2 = $SecondTempDirecoryPath
                LogPath = $LogPath
                TotalPlotCount = $TotalPlots
                BitfieldEnabled = !$DisableBitfield.IsPresent
                ExcludeFinalDir = $ExcludeFinalDirectory.IsPresent
            }
            Write-Information "Plotting started, PID = $PID"
        } # else
    } #if chia path exits
}
function Get-MaxKSize {
    [CmdletBinding()]
    param(
        [ValidateSet("K32","K33","K34","K35")]
        [string[]]$KSize = ("K32","K33","K34","K35"),

        [Parameter(Mandatory)]
        [int64]$TotalBytes
    )

    foreach ($size in $KSize){
        [MaximizedKSize]::new($size,$TotalBytes)
    } #foreach
}
function Get-OptimizedKSizePlotNumbers {
    [CmdletBinding()]
    param(
        [MaximizedKSize[]]$MaximizedKSize
    )

    foreach ($size in $MaximizedKSize){
        switch ($size.KSize){
            "K32" {
                [OptimizedKPlots]::new(0,0,0,$Size.TotalBytes)
            }

            "K33" {
                for ($K33Count = 1; $K33Count -le $size.MaxPlots; $K33Count++){
                    [OptimizedKPlots]::new(0,0,$K33Count,$Size.TotalBytes)
                } #for
            }
            "K34" {
                for ($K34Count = 1; $K34Count -le $size.maxplots; $K34Count++){
                    [OptimizedKPlots]::new(0,$K34Count,0,$Size.TotalBytes)

                    $k34sizeremaining = $Size.TotalBytes - ($K34Count * $size.KSizeBytes)
                    $K33Max = Get-MaxKSize -TotalBytes $k34sizeremaining -KSize "K33"
                    for ($k33 = 1; $k33 -le $k33max.MaxPlots; $k33++){
                        [OptimizedKPlots]::new(0,$K34Count,$k33,$Size.TotalBytes)
                    } #for 33
                } #for 34
            } #34

            "K35" {
                for ($k35count = 1; $k35count -le $size.maxplots; $k35count++){

                    [OptimizedKPlots]::new($k35count,0,0,$Size.TotalBytes)

                    $k35sizeremaining = $Size.TotalBytes - ($k35count * $size.KSizeBytes)
                    $k33max = Get-MaxKSize -Totalbytes $k35sizeremaining -KSize "K33"

                    for ($k33 = 1; $k33 -le $k33max.MaxPlots; $k33++){
                        [OptimizedKPlots]::new($k35count,0,$k33,$Size.TotalBytes)
                    } #for 33

                    $k34max = Get-MaxKSize -Totalbytes $k35sizeremaining -KSize "K34"
                    for ($k34 = 1; $k34 -le $k34max.maxplots; $k34++){
                        [OptimizedKPlots]::new($k35count,$k34,0,$Size.TotalBytes)

                        $sizeremaining = $Size.TotalBytes - (($k35count * $size.KSizeBytes) + ($k34 * $k34max.KSizeBytes))
                        $K33max = Get-MaxKSize -TotalBytes $sizeremaining -KSize "K33"

                        for ($k33 = 1;$k33 -le $k33max.maxplots; $k33++){
                            [OptimizedKPlots]::new($k35count,$k34,$k33,$Size.TotalBytes)
                        }
                    }
                }
            }
        } #switch
    } #foreach
}
function Write-RGBText {
    [CmdletBinding()]
    param(
        [string]$Text,

        [Parameter(Position = 1)]
        [int]$fRed = 0,
        [int]$fGreen = 0,
        [int]$fBlue = 0,

        [int]$bRed = 0,
        [int]$bGreen = 0,
        [int]$bBlue = 0,
        
        # No newline after the text.
        [Parameter()]
        [switch] $NoNewLine,

        [switch]$UnderLine
    )

    $escape = [char]27 + '['
    $resetAttributes = "$($escape)0m"

    if ($UnderLine){
        $UL = "$($escape)4m"
    }
    
    $foreground = "$($escape)38;2;$($fRed);$($fGreen);$($fBlue)m"
    $background = "$($escape)48;2;$($bRed);$($bGreen);$($bBlue)m"
    
    Write-Host ($foreground + $background + $UL + $Text + $resetAttributes) -NoNewline:$NoNewLine
}
Export-ModuleMember -function ConvertTo-FriendlyTimeSpan, Get-ChiaHarvesterActivity, Get-ChiaKPlotCombination, Get-ChiaMaxParallelCount, Get-ChiaPlotProgress, Get-ChiaPlottingStatistic, Get-ChiaProcessCounter, Get-ChiaRAMInfo, Start-ChiaHarvesterWatcher, Start-ChiaParallelPlotting, Start-ChiaPlotting