#Requires -Version 5.1 <# .SYNOPSIS eDNC Special Character Fix - Real-time file watcher to strip invalid characters from DNC files. .DESCRIPTION Monitors a specified folder for DNC program files (.pun, .nc, etc.) and automatically removes special characters (0xFF and others) that cause issues with CNC machines. Some legacy DNC systems and communication protocols add padding or termination bytes that modern CNC controllers cannot process correctly. .PARAMETER WatchFolder The folder to monitor for DNC files. Default: C:\Dnc_Files\Q .PARAMETER FileFilter File pattern to watch. Default: *.pun .PARAMETER IncludeSubfolders Whether to watch subfolders. Default: $true .PARAMETER CharactersToRemove Array of byte values to strip from files. Default: @(255) for 0xFF .EXAMPLE .\eDNC-SpecialCharFix.ps1 Runs with default settings, watching C:\Dnc_Files\Q for *.pun files .EXAMPLE .\eDNC-SpecialCharFix.ps1 -WatchFolder "D:\DNC\Programs" -FileFilter "*.nc" Watches D:\DNC\Programs for .nc files .EXAMPLE .\eDNC-SpecialCharFix.ps1 -CharactersToRemove @(255, 0, 26) Removes 0xFF, NULL, and SUB (Ctrl+Z) characters .NOTES Author: GE Aerospace - Rutland Version: 1.2.0 Date: 2025-12-12 v1.2.0 - Immediate processing: - Process file immediately when eDNC releases it (50ms initial delay) - Aggressive retry: 100ms -> 200ms -> 400ms -> 800ms (15 attempts) - Debouncing to prevent duplicate processing - Exclusive file lock for atomic read/write Common problematic characters: - 0xFF (255) - Padding/fill character - 0x00 (0) - NULL character - 0x1A (26) - SUB/Ctrl+Z (EOF marker in some systems) - 0x7F (127) - DEL character #> [CmdletBinding()] param( [Parameter()] [string]$WatchFolder = "C:\Dnc_Files\Q", [Parameter()] [string]$FileFilter = "*.pun", [Parameter()] [bool]$IncludeSubfolders = $true, [Parameter()] [int[]]$CharactersToRemove = @(255) ) # Script info $ScriptVersion = "1.2.0" $ScriptName = "eDNC Special Character Fix" # Display banner Write-Host "" Write-Host "========================================" -ForegroundColor Cyan Write-Host " $ScriptName v$ScriptVersion" -ForegroundColor Cyan Write-Host "========================================" -ForegroundColor Cyan Write-Host "" # Validate watch folder exists if (-not (Test-Path $WatchFolder)) { Write-Host "[ERROR] Watch folder does not exist: $WatchFolder" -ForegroundColor Red Write-Host "Please create the folder or specify a different path." -ForegroundColor Yellow exit 1 } # Display configuration Write-Host "Configuration:" -ForegroundColor Yellow Write-Host " Watch Folder: $WatchFolder" Write-Host " File Filter: $FileFilter" Write-Host " Subfolders: $IncludeSubfolders" Write-Host " Removing bytes: $($CharactersToRemove -join ', ') (0x$($CharactersToRemove | ForEach-Object { '{0:X2}' -f $_ } | Join-String -Separator ', 0x'))" Write-Host "" Write-Host "Watching for files... Press Ctrl+C to stop" -ForegroundColor Green Write-Host "" # Statistics $script:FilesProcessed = 0 $script:BytesRemoved = 0 $script:FailedFiles = 0 # Debounce tracking - prevent processing same file multiple times $script:RecentlyProcessed = @{} $script:DebounceSeconds = 5 # Create file system watcher $watcher = New-Object System.IO.FileSystemWatcher $watcher.Path = $WatchFolder $watcher.Filter = $FileFilter $watcher.IncludeSubdirectories = $IncludeSubfolders $watcher.EnableRaisingEvents = $true # Define the cleanup action $action = { $path = $Event.SourceEventArgs.FullPath $changeType = $Event.SourceEventArgs.ChangeType $fileName = Split-Path $path -Leaf # Get parameters from message data $charsToRemove = $Event.MessageData.CharsToRemove $debounceSeconds = $Event.MessageData.DebounceSeconds # Debounce check - skip if we processed this file recently $now = Get-Date if ($script:RecentlyProcessed.ContainsKey($path)) { $lastProcessed = $script:RecentlyProcessed[$path] if (($now - $lastProcessed).TotalSeconds -lt $debounceSeconds) { return # Skip duplicate event } } Write-Host "$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') | $changeType | $fileName" -ForegroundColor White # Brief initial delay to let eDNC finish (50ms) Start-Sleep -Milliseconds 50 # Try to acquire exclusive access - process immediately when available $maxRetries = 15 $retryCount = 0 $baseDelay = 100 # Start with 100ms, doubles each retry (100, 200, 400, 800...) while ($retryCount -lt $maxRetries) { $fileStream = $null try { # Try to open file with exclusive access - this confirms it's available $fileStream = [System.IO.File]::Open( $path, [System.IO.FileMode]::Open, [System.IO.FileAccess]::ReadWrite, [System.IO.FileShare]::None ) # Read all bytes while we have exclusive lock $fileStream.Position = 0 $bytes = New-Object byte[] $fileStream.Length $null = $fileStream.Read($bytes, 0, $bytes.Length) $originalCount = $bytes.Length # Remove specified bytes $cleaned = [System.Collections.Generic.List[byte]]::new() foreach ($b in $bytes) { if ($b -notin $charsToRemove) { $cleaned.Add($b) } } $newCount = $cleaned.Count # Only rewrite if we found characters to remove if ($originalCount -ne $newCount) { # Truncate and rewrite $fileStream.Position = 0 $fileStream.SetLength(0) $cleanedArray = $cleaned.ToArray() $fileStream.Write($cleanedArray, 0, $cleanedArray.Length) $fileStream.Flush() $removed = $originalCount - $newCount Write-Host " [CLEANED] Removed $removed byte(s)" -ForegroundColor Green $script:FilesProcessed++ $script:BytesRemoved += $removed } else { Write-Host " [OK] No special characters found" -ForegroundColor Gray } # Mark as recently processed $script:RecentlyProcessed[$path] = Get-Date break } catch [System.IO.IOException] { $retryCount++ if ($retryCount -lt $maxRetries) { # Exponential backoff: 500ms, 1s, 2s, 4s, 8s, 16s... $delay = [math]::Min($baseDelay * [math]::Pow(2, $retryCount - 1), 16000) Write-Host " [RETRY] File locked, waiting $([math]::Round($delay/1000, 1))s... ($retryCount/$maxRetries)" -ForegroundColor Yellow Start-Sleep -Milliseconds $delay } else { Write-Host " [FAILED] Could not access file after $maxRetries attempts" -ForegroundColor Red $script:FailedFiles++ } } catch { Write-Host " [ERROR] $($_.Exception.Message)" -ForegroundColor Red $script:FailedFiles++ break } finally { if ($fileStream) { $fileStream.Close() $fileStream.Dispose() } } } } # Create message data object to pass parameters to the action $messageData = @{ CharsToRemove = $CharactersToRemove DebounceSeconds = $script:DebounceSeconds } # Register event handlers $null = Register-ObjectEvent -InputObject $watcher -EventName Created -Action $action -MessageData $messageData $null = Register-ObjectEvent -InputObject $watcher -EventName Changed -Action $action -MessageData $messageData # Keep script running try { while ($true) { Start-Sleep -Seconds 1 } } finally { # Cleanup on exit $watcher.EnableRaisingEvents = $false $watcher.Dispose() Get-EventSubscriber | Unregister-Event Write-Host "" Write-Host "========================================" -ForegroundColor Cyan Write-Host " Session Summary" -ForegroundColor Cyan Write-Host "========================================" -ForegroundColor Cyan Write-Host " Files Cleaned: $script:FilesProcessed" Write-Host " Bytes Removed: $script:BytesRemoved" if ($script:FailedFiles -gt 0) { Write-Host " Failed Files: $script:FailedFiles" -ForegroundColor Red } Write-Host "" Write-Host "Stopped watching folder" -ForegroundColor Yellow }