1+ using System ;
2+ using System . Collections . Generic ;
3+ using System . IO ;
4+ using System . Text ;
5+ using System . Text . RegularExpressions ;
6+ using System . Threading . Tasks ;
7+
8+ namespace SplitFANUCProgramBackup ;
9+
10+ public static class Fanuc
11+ {
12+ /// <summary>
13+ /// The left side of the OR (|) describes an "O Number", the original CNC program name structure, consisting of an "O" followed by 4 to 8 numbers
14+ /// The right side of the OR (|) describes the new FANUC 32 character alphanumeric program name. Must end in either .NC or .CNC
15+ /// </summary>
16+ public static readonly Regex ONumberPattern = new ( @"^(O\d{4,8}|<\w+.c?nc>)" , RegexOptions . Multiline | RegexOptions . Compiled ) ;
17+
18+ /// <summary>
19+ /// Matches subdirectory flags dispersed throughout the backup file.
20+ /// </summary>
21+ private static readonly Regex DirectoryFlag = new ( "(&F=)" , RegexOptions . Compiled ) ;
22+
23+ /// <summary>
24+ /// Character found at the top and bottom of each program.
25+ /// The CNC control uses this to determine when a program begins and ends.
26+ /// </summary>
27+ public const char ProgramDelimiter = '%' ;
28+ private const string CncProgramFileExtension = ".CNC" ;
29+ private const string DefaultCncProgramName = "Unknown" ;
30+ private static readonly char [ ] SubFolderTrim = [ ' ' , '/' ] ;
31+ private const int MinimumProgramSize = 7 ;
32+ private const char LineFeed = '\n ' ;
33+
34+ /// <summary>
35+ /// Split cnc programs found in the text file and save them as individual files.
36+ /// File subdirectories reflect those on the source CNC controller.
37+ /// </summary>
38+ /// <param name="backupFile">Full path to "ALL-PROG.TXT"</param>
39+ /// <param name="outputFolder">Destination output will be written to.</param>
40+ public static void SplitAllProgTxt ( FileInfo backupFile , string outputFolder )
41+ {
42+ // Use parallel to decouple reading of the old file from writing the new ones. This allows reading at full speed.
43+ Parallel . ForEach ( GetCncPrograms ( backupFile . FullName , outputFolder ) , newFile =>
44+ {
45+ string programFileName = GetProgramNameFromHeader ( newFile . ProgramText ) ;
46+ if ( programFileName . Length < 1 )
47+ programFileName = DefaultCncProgramName ;
48+
49+ string outputFilename =
50+ Path . Combine ( outputFolder , newFile . SubFolder , programFileName + CncProgramFileExtension ) ;
51+ try
52+ {
53+ File . WriteAllText ( outputFilename , newFile . ProgramText ) ;
54+ Console . WriteLine ( $ "CREATED FILE: { outputFilename } ") ;
55+ }
56+ catch ( Exception err )
57+ {
58+ Console . WriteLine ( $ "ERROR { err . HResult } : { err . Message } ") ;
59+ Console . WriteLine ( $ "FAILED TO CREATE FILE: { outputFilename } ") ;
60+ }
61+ } ) ;
62+ }
63+
64+ /// <summary>
65+ /// Searches for program names in CNC program headers
66+ /// </summary>
67+ /// <param name="cncProgramText">The full text of a CNC program</param>
68+ /// <returns>The program name from the header</returns>
69+ private static string GetProgramNameFromHeader ( string cncProgramText )
70+ {
71+ return ONumberPattern . Match ( cncProgramText ) . Value ;
72+ }
73+
74+ /// <summary>
75+ /// Reads a text file and attempts to split out CNC programs found within,
76+ /// while capturing each program's subdirectory as in the source CNC controller.
77+ /// </summary>
78+ /// <param name="fileName">Full path to "ALL-PROG.TXT"</param>
79+ /// <param name="outputFolder">Destination folder.</param>
80+ /// <returns>Each CNC program as a string, and any associated subdirectory</returns>
81+ private static IEnumerable < ( string SubFolder , string ProgramText ) > GetCncPrograms ( string fileName , string outputFolder )
82+ {
83+ StringBuilder content = new ( ) ;
84+ string subFolder = "" ;
85+
86+ // Searches for CNC programs between program name symbols
87+ var lines = File . ReadLines ( fileName ) ;
88+ foreach ( string line in lines )
89+ {
90+ // Checks for subdirectory notation
91+ if ( DirectoryFlag . IsMatch ( line ) )
92+ {
93+ if ( content . Length > MinimumProgramSize )
94+ {
95+ // Return file in buffer, as new subdirectory only applies to subsequent programs
96+ yield return ( subFolder , CncProgramText ( content ) ) ;
97+
98+ // Start a new file
99+ content . Clear ( ) ;
100+ }
101+
102+ // Strip out the directory flag and slashes to get just the folder name.
103+ subFolder = DirectoryFlag . Replace ( line , string . Empty ) . Trim ( SubFolderTrim ) ;
104+ Directory . CreateDirectory ( Path . Combine ( outputFolder , subFolder ) ) ;
105+
106+ // Don't append notation to next program
107+ continue ;
108+ }
109+
110+ if ( ONumberPattern . IsMatch ( line ) )
111+ {
112+ if ( content . Length > MinimumProgramSize )
113+ { // Return the file we have in the buffer
114+ yield return ( subFolder , CncProgramText ( content ) ) ;
115+ }
116+
117+ // Start a new file
118+ content . Clear ( ) ;
119+ }
120+
121+ // Add unix style line terminators
122+ content . Append ( line )
123+ . Append ( LineFeed ) ;
124+ }
125+
126+ // Once we reach the end we will have the final program in the buffer.
127+ yield return ( subFolder , CncProgramText ( content ) ) ;
128+ }
129+
130+ private static string CncProgramText ( StringBuilder content )
131+ {
132+ // Prevent IndexOutOfBounds exceptions if final program is empty
133+ if ( content . Length <= MinimumProgramSize )
134+ return content . ToString ( ) ;
135+
136+ // Add % to the top
137+ if ( content [ 0 ] != ProgramDelimiter )
138+ {
139+ content . Insert ( 0 , LineFeed ) ;
140+ content . Insert ( 0 , ProgramDelimiter ) ;
141+ }
142+
143+ content . TrimEnd ( ) ;
144+
145+ // Add % to the bottom when missing
146+ if ( content [ content . Length - 1 ] != ProgramDelimiter )
147+ {
148+ content . Append ( ProgramDelimiter )
149+ . Append ( LineFeed ) ;
150+ }
151+
152+ return content . ToString ( ) ;
153+ }
154+ }
0 commit comments