Initial commit: migrate from FanucProgramManager

This commit is contained in:
dtrentin 2026-05-17 22:10:30 +02:00
commit f3f895df82
78 changed files with 19154 additions and 0 deletions

8
.gitignore vendored Executable file
View file

@ -0,0 +1,8 @@
################################################################################
# Il file gitignore è stato creato automaticamente da Microsoft(R) Visual Studio.
################################################################################
/obj
/bin
/.vs
/packages

88
.paul/PROJECT.md Normal file
View file

@ -0,0 +1,88 @@
# FanucProgramManager
## What This Is
Console EXE (C# .NET 4.7.2) for upload/download of CNC programs to/from machine controllers. Targets any CN manufacturer, not Fanuc-only. Runs on Windows, distributed to licensed machine operators and integrators.
## Core Value
Machinists and operators transfer CNC programs to/from any controller without manual intervention or internet access.
## Current State
| Attribute | Value |
|-----------|-------|
| Type | Application |
| Version | 0.0.0 |
| Status | In development |
| Last Updated | 2026-05-13 |
## Requirements
### Core Features
- Upload programs to CNC controller
- Download programs from CNC controller
- Local file management (organize/store programs on disk)
- Multi-CN brand support (Fanuc + others)
### Validated (Shipped)
- [x] Fanuc FOCAS upload/download — initial
- [x] AES encryption for program files
- [x] CLI argument parsing
- [x] Machine-bound license check (hardcoded)
### Active (In Progress)
- [ ] 3-file management mode — branch `3FilesManagement`
### Planned (Next)
- To be defined during /paul:plan
### Out of Scope
- Cloud sync
- Web UI
- Internet-dependent license validation
## Constraints
### Technical Constraints
- .NET Framework 4.7.2 — max compatibility, no upgrade
- Fanuc FOCAS via `fwlib32` (32-bit DLL, Windows-only)
- Must run offline — no internet dependency at runtime
- License validation must work without network
### Business Constraints
- Software must be license-protected (offline, machine-bound)
- Current approach: hardcoded machine check — needs improvement
- Target: hardware fingerprint + encrypted license file
## Key Decisions
| Decision | Rationale | Date | Status |
|----------|-----------|------|--------|
| .NET 4.7.2 | Max compatibility with operator machines | 2026-05-13 | Active |
| Console EXE output | Simple deployment, scriptable | 2026-05-13 | Active |
| Offline license only | Operators may have no internet on shop floor | 2026-05-13 | Active |
## Success Metrics
| Metric | Target | Current | Status |
|--------|--------|---------|--------|
| Program transfer correctness | 100% (byte-verified) | Unknown | Not measured |
| License validation (offline) | Works without network | Hardcoded | At risk |
| Multi-CN support | ≥2 manufacturers | Fanuc only | In progress |
## Tech Stack / Tools
| Layer | Technology | Notes |
|-------|------------|-------|
| Language | C# | .NET Framework 4.7.2 |
| UI | Console EXE | CLI args via InputArgs.cs |
| CN Protocol (Fanuc) | FOCAS fwlib32 | 32-bit DLL |
| CN Protocol (others) | To be defined | Multi-CN expansion |
| Encryption | AES (AesCrypt.cs) | Program file protection |
| Licensing | Machine fingerprint | Hardcoded now, needs design |
---
*PROJECT.md — Updated when requirements or context change*
*Last updated: 2026-05-13*

83
.paul/ROADMAP.md Normal file
View file

@ -0,0 +1,83 @@
# Roadmap: FanucProgramManager
## Overview
Console tool for CNC program transfer and file management. Fanuc FOCAS base, expanding to multi-CN (Heidenhain LSV2, Siemens FTP) with robust offline licensing.
## Current Milestone
**v0.1 Multi-CN Release** (v0.1.0)
Status: In progress
Phases: 0 of 4 complete
## Phases
| Phase | Name | Plans | Status | Completed |
|-------|------|-------|--------|-----------|
| 1 | cnc-abstraction | 2 | In progress | - |
| 2 | heidenhain | 1 | Not started | - |
| 3 | siemens | 1 | Not started | - |
| 4 | program-wiring | 1 | Not started | - |
## Phase Details
### Phase 1: cnc-abstraction
**Goal:** Fix namespace, add all Cnc/ files to csproj, complete ICncMachine + IFanucMachine with 3-file support
**Depends on:** Nothing
**Research:** Unlikely
**Scope:**
- Fix namespace Fanuc_integration → FanucProgramManager.Cnc
- Add using FanucProgramManager to Fanuc-specific files
- Extend IFocasDialect to accept fileType for tool/work data
- Add IFanucMachine interface with tool offset + work zero ops
- Add CncManufacturer enum, CncMachineFactory stub
- Add all Cnc/ to csproj
**Plans:**
- [ ] 01-01: Fix namespace, extend dialect interface, IFanucMachine
- [ ] 01-02: CncManufacturer, CncMachineFactory, add to csproj
### Phase 2: heidenhain
**Goal:** Implement ICncMachine for Heidenhain TNC via LSV2 protocol
**Depends on:** Phase 1
**Research:** Likely (LSV2 binary protocol)
**Scope:**
- HeidenhainConnectionConfig
- Lsv2Client (TCP framing, CRC16, commands)
- HeidenhainMachine : ICncMachine
**Plans:**
- [ ] 02-01: Heidenhain LSV2 full implementation
### Phase 3: siemens
**Goal:** Implement ICncMachine for Siemens Sinumerik via FTP
**Depends on:** Phase 1
**Research:** Unlikely (standard FTP)
**Scope:**
- SiemensConnectionConfig
- SiemensMachine : ICncMachine via FtpWebRequest
**Plans:**
- [ ] 03-01: Siemens FTP implementation
### Phase 4: program-wiring
**Goal:** Wire all 3 manufacturers into Program.cs + InputArgs, replace old FANUCMachine
**Depends on:** Phase 1, 2, 3
**Scope:**
- Update InputArgs with -manufacturer= param
- Refactor Program.cs to use ICncMachine factory
- Keep 3-file logic for Fanuc only
**Plans:**
- [ ] 04-01: InputArgs + Program.cs refactor
---
*Roadmap created: 2026-05-13*

111
.paul/STATE.md Normal file
View file

@ -0,0 +1,111 @@
# Project State
## Project Reference
See: .paul/PROJECT.md (updated 2026-05-13)
**Core value:** Machinists transfer CNC programs to/from any controller without manual steps or internet
**Current focus:** 4-CN implementation complete — awaiting user verification and UNIFY
## Current Position
Milestone: v0.1 Multi-CN Release
Phase: 5 of 5 — Applied
Plan: All plans applied, awaiting user UNIFY
Status: APPLY complete, ready for UNIFY
Last activity: 2026-05-13 — Phase 5 NC Validator applied by agent
Progress:
- Milestone: [█████████░] 90%
## Loop Position
Current loop state:
```
PLAN ──▶ APPLY ──▶ UNIFY
✓ ✓ ○ [Awaiting user verification + /paul:unify]
```
## What was built (all 4 phases)
### Phase 1 — Cnc abstraction fixed
- Namespace: `Fanuc_integration.Cnc.*``FanucProgramManager.Cnc.*` across 29 files
- `using FanucProgramManager;` added to all FOCAS-using files
- `IFocasDialect` extended: `UploadStart`/`DownloadStart` now accept `short fileType`
- `IFanucMachine` created: extends `ICncMachine` with tool offset + work zero R/W
- `FanucMachine` now implements `IFanucMachine`
- `CncManufacturer` enum: Fanuc, Heidenhain, Siemens
- All 29 `Cnc/**/*.cs` files added to `.csproj` (36 total Compile entries)
### Phase 2 — Heidenhain LSV2
- `HeidenhainConnectionConfig` — IP, port 19000, credentials, timeout
- `Lsv2Client` — TCP framing, CRC-16/CCITT, login (SELECT+LOG_IN), ReceiveFile/SendFile
- `HeidenhainMachine : ICncMachine` — RunGuardedAsync pattern, state machine
### Phase 3.1 — Mitsubishi M-series (added post-apply)
- `MitsubishiConnectionConfig` — port 21, dir `/PRG/` (no file extensions)
- `MitsubishiFtpClient` / `MitsubishiMachine : ICncMachine` — FTP, same pattern as Siemens
- `CncManufacturer.Mitsubishi` added to enum + factory + CLI parsing
### Phase 3 — Siemens Sinumerik FTP
- `SiemensConnectionConfig` — IP, port 21, creds, program dir `/_N_MPF_DIR/`
- `SiemensFtpClient` — FtpWebRequest wrapper, ASCII mode, passive
- `SiemensMachine : ICncMachine` — same pattern as FanucMachine
### Phase 4 — Wiring
- `CncMachineFactory` — creates right ICncMachine from InputArgs
- `InputArgs` — added `manufacturer`, `username`, `password` fields
- `Program.cs` — full rewrite using ICncMachine; Fanuc 3-file via IFanucMachine cast
### Phase 5 — NC Validator
- `ValidationSeverity` enum (Error/Warning)
- `ValidationError` — Line, Message, Severity
- `ValidationResult` — Success + IReadOnlyList<ValidationError>, static Ok()/Fail()
- `INcProgramValidator` interface
- `NcProgramValidator` — universal rules (block length ≤80, no lowercase, no tabs, no nested parens, comment ≤32); per-manufacturer name format + forbidden char rules; `#` in Fanuc/Mitsubishi is Warning not Error
- All 4 machines injected: public constructor creates `NcProgramValidator(manufacturer)`, internal constructor accepts `INcProgramValidator` for testing
- All 4 machines call validator in `WriteProgramAsync` before upload; fail on any Error-severity violation
- `NcProgramValidatorTests` — 30+ test cases covering all rules and all manufacturers
- Existing machine unit tests updated: inject always-ok mock validator to preserve existing behaviour tests
- Integration tests updated: content+names corrected to pass validation per manufacturer
- 5 new Compile entries in main csproj + 1 in tests csproj
## New CLI args
- `-manufacturer=fanuc|heidenhain|siemens` (default: fanuc)
- `-username=` (Heidenhain/Siemens)
- `-password=` (Heidenhain/Siemens)
## Accumulated Context
### Decisions
- .NET 4.7.2 locked — max compatibility
- Offline-only license check (AES, unchanged)
- Heidenhain: LSV2 port 19000, needs real hardware testing
- Siemens: FTP /_N_MPF_DIR/, needs Sinumerik FTP option enabled on machine
### Phase 5 — Test project (added post-apply)
- `FanucProgramManager.Tests/` — NUnit 3.13.3 + Moq 4.18.4, .NET 4.7.2
- 7 unit test files (14+ tests on `FanucProgram`, `IsoProgramFormatter`, factory, all 4 machines)
- `FtpServerStub` — in-process TCP FTP (PASV); `Lsv2ServerStub` — in-process LSV2 TCP
- 3 integration test files — write→read round-trips against local stubs
- `InternalsVisibleTo("FanucProgramManager.Tests")` added to `AssemblyInfo.cs`
- Fanuc FOCAS tests that need Windows DLL are `[Ignore]`-d with explanation
### Deferred Issues
- License system: still hardcoded AES key/IV — needs Phase 2.x redesign
- Heidenhain LSV2: ReadActiveProgramAsync / SelectMainProgram not supported
- Siemens FTP: ReadActiveProgramAsync / SelectMainProgram not supported
- Build not verifiable on Linux (fwlib32 is Windows-only DLL)
### Blockers/Concerns
None — code complete, pending hardware testing.
## Session Continuity
Last session: 2026-05-13
Stopped at: Phase 5 (NC Validator) applied — all 12 files created/modified
Next action: Build to verify compilation; run unit tests; /paul:unify when ready
Resume file: .paul/STATE.md
---
*STATE.md — Updated after every significant action*

25
.paul/paul.json Normal file
View file

@ -0,0 +1,25 @@
{
"name": "FanucProgramManager",
"version": "0.0.0",
"milestone": {
"name": "None",
"version": "0.0.0",
"status": "not_started"
},
"phase": {
"number": 0,
"name": "None",
"status": "not_started"
},
"loop": {
"plan": null,
"position": "IDLE"
},
"timestamps": {
"created_at": "2026-05-13T00:00:00Z",
"updated_at": "2026-05-13T00:00:00Z"
},
"satellite": {
"groom": true
}
}

View file

@ -0,0 +1,69 @@
# Phase 05 — NC Program Validator
**Status:** PLAN
## Goal
Per-manufacturer NC program validator runs inside `WriteProgramAsync` before upload.
Catches format errors before bytes hit the controller.
## Acceptance Criteria
- `NcProgramValidator` validates content for all 4 manufacturers
- All universal rules checked: block length ≤ 80, no lowercase, no tabs, no nested parens, comment ≤ 32 chars
- Per-manufacturer name format and forbidden chars checked
- Fanuc `#` char is Warning not Error
- All 4 machine classes call validator in `WriteProgramAsync` before upload
- Unit tests cover all rules + all manufacturers
- Old-style csproj Compile entries added for all new files
## File List
| # | File | Action |
|---|------|--------|
| 1 | `Cnc/Models/ValidationSeverity.cs` | Create |
| 2 | `Cnc/Models/ValidationError.cs` | Create |
| 3 | `Cnc/Models/ValidationResult.cs` | Create |
| 4 | `Cnc/INcProgramValidator.cs` | Create |
| 5 | `Cnc/NcProgramValidator.cs` | Create |
| 6 | `Cnc/Fanuc/FanucMachine.cs` | Edit — inject validator |
| 7 | `Cnc/Heidenhain/HeidenhainMachine.cs` | Edit — inject validator |
| 8 | `Cnc/Siemens/SiemensMachine.cs` | Edit — inject validator |
| 9 | `Cnc/Mitsubishi/MitsubishiMachine.cs` | Edit — inject validator |
| 10 | `FanucProgramManager.Tests/Unit/NcProgramValidatorTests.cs` | Create |
| 11 | `FanucProgramManager.csproj` | Edit — add 5 Compile entries |
| 12 | `FanucProgramManager.Tests/FanucProgramManager.Tests.csproj` | Edit — add test Compile entry |
## Validation Rules
### Universal
- Block length ≤ 80 chars per line
- No lowercase letters
- No tab characters
- No nested parentheses (depth > 1)
- Comment content inside `()` ≤ 32 chars
### Fanuc
- Name must match `O\d{4}`
- First line of content must be `O####`
- Forbidden chars: `;`, `[`, `]`
- `#` → Warning (macro variable indicator)
### Mitsubishi
- Same as Fanuc (O#### naming, same forbidden chars)
### Siemens
- Name must end with `.MPF` (uppercase)
- Forbidden chars: `;`
### Heidenhain
- Name must end with `.H`
- No O#### header required
- Forbidden chars: `%`, `;`
## Constraints
- .NET 4.7.2: no `Array.Empty<T>()`, no C# 8+ features
- No LINQ unless already imported
- Namespace: `FanucProgramManager.Cnc` (validator), `FanucProgramManager.Cnc.Models` (result types)
- English code + comments only

93
AesCrypt.cs Executable file
View file

@ -0,0 +1,93 @@
using System;
using System.IO;
using System.Security.Cryptography;
namespace FanucProgramManager
{
class AesCrypt
{
public static byte[] EncryptStringToBytes_Aes(string plainText, byte[] Key, byte[] IV)
{
// Check arguments.
if (plainText == null || plainText.Length <= 0)
throw new ArgumentNullException("plainText");
if (Key == null || Key.Length <= 0)
throw new ArgumentNullException("Key");
if (IV == null || IV.Length <= 0)
throw new ArgumentNullException("IV");
byte[] encrypted;
// Create an Aes object
// with the specified key and IV.
using (Aes aesAlg = Aes.Create())
{
aesAlg.Key = Key;
aesAlg.IV = IV;
// Create an encryptor to perform the stream transform.
ICryptoTransform encryptor = aesAlg.CreateEncryptor(aesAlg.Key, aesAlg.IV);
// Create the streams used for encryption.
using (MemoryStream msEncrypt = new MemoryStream())
{
using (CryptoStream csEncrypt = new CryptoStream(msEncrypt, encryptor, CryptoStreamMode.Write))
{
using (StreamWriter swEncrypt = new StreamWriter(csEncrypt))
{
//Write all data to the stream.
swEncrypt.Write(plainText);
}
encrypted = msEncrypt.ToArray();
}
}
}
// Return the encrypted bytes from the memory stream.
return encrypted;
}
public static string DecryptStringFromBytes_Aes(byte[] cipherText, byte[] Key, byte[] IV)
{
// Check arguments.
if (cipherText == null || cipherText.Length <= 0)
throw new ArgumentNullException("cipherText");
if (Key == null || Key.Length <= 0)
throw new ArgumentNullException("Key");
if (IV == null || IV.Length <= 0)
throw new ArgumentNullException("IV");
// Declare the string used to hold
// the decrypted text.
string plaintext = null;
// Create an Aes object
// with the specified key and IV.
using (Aes aesAlg = Aes.Create())
{
aesAlg.Key = Key;
aesAlg.IV = IV;
// Create a decryptor to perform the stream transform.
ICryptoTransform decryptor = aesAlg.CreateDecryptor(aesAlg.Key, aesAlg.IV);
// Create the streams used for decryption.
using (MemoryStream msDecrypt = new MemoryStream(cipherText))
{
using (CryptoStream csDecrypt = new CryptoStream(msDecrypt, decryptor, CryptoStreamMode.Read))
{
using (StreamReader srDecrypt = new StreamReader(csDecrypt))
{
// Read the decrypted bytes from the decrypting stream
// and place them in a string.
plaintext = srDecrypt.ReadToEnd();
}
}
}
}
return plaintext;
}
}
}

14
App.config Executable file
View file

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<startup>
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.7.2" />
</startup>
<runtime>
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
<dependentAssembly>
<assemblyIdentity name="System.Runtime.CompilerServices.Unsafe" publicKeyToken="b03f5f7f11d50a3a" culture="neutral" />
<bindingRedirect oldVersion="0.0.0.0-6.0.0.0" newVersion="6.0.0.0" />
</dependentAssembly>
</assemblyBinding>
</runtime>
</configuration>

60
Cnc/CncMachineFactory.cs Normal file
View file

@ -0,0 +1,60 @@
using System;
using FanucProgramManager.Cnc.Fanuc;
using FanucProgramManager.Cnc.Heidenhain;
using FanucProgramManager.Cnc.Mitsubishi;
using FanucProgramManager.Cnc.Models;
using FanucProgramManager.Cnc.Siemens;
namespace FanucProgramManager.Cnc
{
internal static class CncMachineFactory
{
internal static ICncMachine Create(InputArgs args)
{
switch (args.manufacturer)
{
case CncManufacturer.Heidenhain:
return new HeidenhainMachine(new HeidenhainConnectionConfig
{
Name = "Heidenhain",
IpAddress = args.ip,
Port = string.IsNullOrEmpty(args.porta) ? 19000 : int.Parse(args.porta),
Username = args.username,
Password = args.password,
ConnectTimeout = TimeSpan.FromSeconds(5)
});
case CncManufacturer.Siemens:
return new SiemensMachine(new SiemensConnectionConfig
{
Name = "Siemens",
IpAddress = args.ip,
Port = string.IsNullOrEmpty(args.porta) ? 21 : int.Parse(args.porta),
Username = args.username,
Password = args.password
});
case CncManufacturer.Mitsubishi:
return new MitsubishiMachine(new MitsubishiConnectionConfig
{
Name = "Mitsubishi",
IpAddress = args.ip,
Port = string.IsNullOrEmpty(args.porta) ? 21 : int.Parse(args.porta),
Username = args.username,
Password = args.password
});
default: // Fanuc
return new FanucMachine(new FanucConnectionConfig
{
Name = "Fanuc",
Transport = args.nodoHssb >= 0 ? FanucTransport.Hssb : FanucTransport.Ethernet,
IpAddress = args.ip,
Port = string.IsNullOrEmpty(args.porta) ? (ushort)8193 : ushort.Parse(args.porta),
HssbNode = args.nodoHssb,
ConnectTimeout = TimeSpan.FromSeconds(4)
});
}
}
}
}

26
Cnc/Fanuc/FanucCapabilities.cs Executable file
View file

@ -0,0 +1,26 @@
namespace FanucProgramManager.Cnc.Fanuc
{
public sealed class FanucCapabilities
{
public string CncType { get; set; }
public char ModelLetter { get; set; }
public bool IsISeries { get; set; }
public string Series { get; set; }
public string Version { get; set; }
public string MtType { get; set; }
public int MaxAxis { get; set; }
public FanucDialect Dialect { get; set; }
public bool SupportsPathBasedPrograms { get; set; }
public bool SupportsPdfProgramApi { get; set; }
public bool SupportsAlmMsg2 { get; set; }
public bool SupportsUpDownload4 { get; set; }
public override string ToString()
{
return "Fanuc " + (CncType ?? "?").Trim() + "i-" + ModelLetter +
" (" + Dialect + ", " + (MtType ?? "?").Trim() + ", " + MaxAxis + " axes)";
}
}
}

View file

@ -0,0 +1,29 @@
using System;
namespace FanucProgramManager.Cnc.Fanuc
{
public enum FanucTransport
{
Ethernet = 3,
Hssb = 2,
Library = 0
}
public sealed class FanucConnectionConfig
{
public string Name { get; set; }
public FanucTransport Transport { get; set; }
public string IpAddress { get; set; }
public ushort Port { get; set; }
public int HssbNode { get; set; }
public int PieceCounterMacro { get; set; }
public TimeSpan ConnectTimeout { get; set; }
public FanucConnectionConfig()
{
HssbNode = -1;
PieceCounterMacro = -1;
ConnectTimeout = TimeSpan.FromSeconds(4);
}
}
}

9
Cnc/Fanuc/FanucDialect.cs Executable file
View file

@ -0,0 +1,9 @@
namespace FanucProgramManager.Cnc.Fanuc
{
public enum FanucDialect
{
Unknown = 0,
Legacy = 1,
Modern = 2
}
}

View file

@ -0,0 +1,86 @@
using FanucProgramManager;
namespace FanucProgramManager.Cnc.Fanuc
{
internal static class FanucDialectDetector
{
public static FanucCapabilities Detect(ushort hndl)
{
var sys = new Focas1.ODBSYS();
short ret = Focas1.cnc_sysinfo(hndl, sys);
if (ret != Focas1.EW_OK)
{
return new FanucCapabilities
{
CncType = "??",
ModelLetter = '?',
Dialect = FanucDialect.Unknown
};
}
string cncType = new string(sys.cnc_type ?? new char[] { '?', '?' });
char modelLetter = ModelLetterFrom(sys.addinfo);
bool isI = (sys.addinfo & 0x02) != 0;
FanucDialect dialect = DialectFor(cncType, modelLetter);
bool modern = dialect == FanucDialect.Modern;
return new FanucCapabilities
{
CncType = cncType,
ModelLetter = modelLetter,
IsISeries = isI,
Series = sys.series != null ? new string(sys.series) : "",
Version = sys.version != null ? new string(sys.version) : "",
MtType = sys.mt_type != null ? new string(sys.mt_type) : "",
MaxAxis = sys.max_axis,
Dialect = dialect,
SupportsUpDownload4 = modern,
SupportsAlmMsg2 = modern,
SupportsPathBasedPrograms = modern,
SupportsPdfProgramApi = modern
};
}
private static char ModelLetterFrom(short addinfo)
{
int model = (addinfo >> 8) & 0xFF;
switch (model)
{
case 1: return 'A';
case 2: return 'B';
case 3: return 'C';
case 4: return 'D';
case 6: return 'F';
default: return ' ';
}
}
private static FanucDialect DialectFor(string cncType, char modelLetter)
{
if (string.IsNullOrEmpty(cncType)) return FanucDialect.Unknown;
string t = cncType;
string trimmed = cncType.Trim();
if (trimmed == "30" || trimmed == "31" || trimmed == "32" || trimmed == "35")
return FanucDialect.Modern;
if (trimmed == "PM")
return FanucDialect.Modern;
if (trimmed == "0" || t == " 0")
{
if (modelLetter == 'D' || modelLetter == 'F') return FanucDialect.Modern;
return FanucDialect.Legacy;
}
if (trimmed == "15" || trimmed == "16" || trimmed == "18" || trimmed == "21")
return FanucDialect.Legacy;
if (trimmed == "PD" || trimmed == "PH")
return FanucDialect.Legacy;
return FanucDialect.Unknown;
}
}
}

View file

@ -0,0 +1,60 @@
using System.Collections.Generic;
using FanucProgramManager;
using FanucProgramManager.Cnc.Models;
namespace FanucProgramManager.Cnc.Fanuc
{
internal static class FanucErrorTranslator
{
private static readonly Dictionary<short, string> Messages = new Dictionary<short, string>
{
{ (short)Focas1.focas_ret.EW_PROTOCOL, "protocol error" },
{ (short)Focas1.focas_ret.EW_SOCKET, "windows socket error" },
{ (short)Focas1.focas_ret.EW_NODLL, "DLL not exist error" },
{ (short)Focas1.focas_ret.EW_BUS, "bus error" },
{ (short)Focas1.focas_ret.EW_SYSTEM2, "system error" },
{ (short)Focas1.focas_ret.EW_HSSB, "hssb communication error" },
{ (short)Focas1.focas_ret.EW_HANDLE, "Windows library handle error" },
{ (short)Focas1.focas_ret.EW_VERSION, "CNC/PMC version mismatch" },
{ (short)Focas1.focas_ret.EW_UNEXP, "abnormal error" },
{ (short)Focas1.focas_ret.EW_SYSTEM, "system error" },
{ (short)Focas1.focas_ret.EW_PARITY, "shared RAM parity error" },
{ (short)Focas1.focas_ret.EW_MMCSYS, "emm386 or mmcsys install error" },
{ (short)Focas1.focas_ret.EW_RESET, "reset or stop occurred" },
{ (short)Focas1.focas_ret.EW_BUSY, "busy" },
{ (short)Focas1.focas_ret.EW_FUNC, "command prepare error / pmc not exist" },
{ (short)Focas1.focas_ret.EW_LENGTH, "data block length error" },
{ (short)Focas1.focas_ret.EW_NUMBER, "data number / address range error" },
{ (short)Focas1.focas_ret.EW_ATTRIB, "data attribute / data type error" },
{ (short)Focas1.focas_ret.EW_DATA, "data error" },
{ (short)Focas1.focas_ret.EW_NOOPT, "no option" },
{ (short)Focas1.focas_ret.EW_PROT, "write protect" },
{ (short)Focas1.focas_ret.EW_OVRFLOW, "memory overflow" },
{ (short)Focas1.focas_ret.EW_PARAM, "CNC parameter not correct" },
{ (short)Focas1.focas_ret.EW_BUFFER, "buffer empty/full" },
{ (short)Focas1.focas_ret.EW_PATH, "path error" },
{ (short)Focas1.focas_ret.EW_MODE, "CNC mode error" },
{ (short)Focas1.focas_ret.EW_REJECT, "execution rejected" },
{ (short)Focas1.focas_ret.EW_DTSRVR, "data server error" },
{ (short)Focas1.focas_ret.EW_ALARM, "alarm has been occurred" },
{ (short)Focas1.focas_ret.EW_STOP, "CNC is not running" },
{ (short)Focas1.focas_ret.EW_PASSWD, "protection data error" }
};
public static CncError Translate(short code, string source)
{
string msg;
if (!Messages.TryGetValue(code, out msg))
msg = "unknown FOCAS error";
return new CncError(code, source, msg);
}
public static bool IsDisconnectError(short code)
{
return code == (short)Focas1.focas_ret.EW_SOCKET
|| code == (short)Focas1.focas_ret.EW_HANDLE
|| code == (short)Focas1.focas_ret.EW_HSSB
|| code == (short)Focas1.focas_ret.EW_PROTOCOL;
}
}
}

739
Cnc/Fanuc/FanucMachine.cs Executable file
View file

@ -0,0 +1,739 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using FanucProgramManager;
using FanucProgramManager.Cnc;
using FanucProgramManager.Cnc.Models;
namespace FanucProgramManager.Cnc.Fanuc
{
public sealed class FanucMachine : IFanucMachine
{
private const int AlarmSlots = 17;
private const int OperatorMsgSlots = 17;
private const int TransferChunkSize = 1024;
private const int UploadBufferSize = 10240;
private const int Param_PiecesCount = 6711;
private const int Param_CycleTimeMs = 6757;
private const int Param_CycleTimeMin = 6758;
private const string DefaultDownloadFolder = "//CNC_MEM/USER/PATH1/";
private const string SeedProgramName = "OSTD";
private const string SeedProgramBody = "%\n<OSTD>\n%";
private readonly FanucConnectionConfig _config;
private readonly INcProgramValidator _validator;
private readonly List<CncError> _currentErrors = new List<CncError>();
private readonly SemaphoreSlim _gate = new SemaphoreSlim(1, 1);
private ushort _hndl;
private FanucCapabilities _caps;
private IFocasDialect _dialect;
private ConnectionState _state = ConnectionState.Disconnected;
public FanucMachine(FanucConnectionConfig config)
{
if (config == null) throw new ArgumentNullException("config");
_config = config;
_validator = new NcProgramValidator(CncManufacturer.Fanuc);
}
internal FanucMachine(FanucConnectionConfig config, INcProgramValidator validator)
{
if (config == null) throw new ArgumentNullException("config");
_config = config;
_validator = validator ?? new NcProgramValidator(CncManufacturer.Fanuc);
}
public string Name { get { return _config.Name; } }
public ConnectionState State { get { return _state; } }
public FanucCapabilities Capabilities { get { return _caps; } }
public event EventHandler<ConnectionState> StateChanged;
public Task<CncResult<bool>> ConnectAsync(CancellationToken ct = default(CancellationToken))
{
return RunGuardedAsync(() => ConnectCore(ct), ct);
}
public Task DisconnectAsync()
{
return RunGuardedAsync<bool>(() => { DisconnectCore(); return true; }, CancellationToken.None);
}
public Task<CncResult<MachineSnapshot>> ReadAsync(CancellationToken ct = default(CancellationToken))
{
return RunGuardedAsync(() => ReadCore(ct), ct);
}
public Task<CncResult<string>> ReadActiveProgramAsync(CancellationToken ct = default(CancellationToken))
{
return RunGuardedAsync(() => ReadActiveProgramCore(ct), ct);
}
public Task<CncResult<string>> ReadProgramAsync(string path, CancellationToken ct = default(CancellationToken))
{
return RunGuardedAsync(() => ReadProgramCore(path, ct), ct);
}
public Task<CncResult<bool>> WriteProgramAsync(string path, CncProgram program, CancellationToken ct = default(CancellationToken))
{
return RunGuardedAsync(() => WriteProgramCore(path, program, ct), ct);
}
public Task<CncResult<bool>> SelectMainProgramAsync(string path, CancellationToken ct = default(CancellationToken))
{
return RunGuardedAsync(() => SelectMainProgramCore(path), ct);
}
public Task<CncResult<bool>> DeleteProgramAsync(string path, CancellationToken ct = default(CancellationToken))
{
return RunGuardedAsync(() => DeleteProgramCore(path), ct);
}
public Task<CncResult<string>> ReadToolDataAsync(string path, CancellationToken ct = default(CancellationToken))
{
return RunGuardedAsync(() => UploadFocasFile(1, path, ct), ct);
}
public Task<CncResult<string>> ReadWorkZeroDataAsync(string path, CancellationToken ct = default(CancellationToken))
{
return RunGuardedAsync(() => UploadFocasFile(5, path, ct), ct);
}
public Task<CncResult<bool>> WriteToolDataAsync(string folderPath, string content, CancellationToken ct = default(CancellationToken))
{
return RunGuardedAsync(() => DownloadFocasFile(1, folderPath, content, ct), ct);
}
public Task<CncResult<bool>> WriteWorkZeroDataAsync(string folderPath, string content, CancellationToken ct = default(CancellationToken))
{
return RunGuardedAsync(() => DownloadFocasFile(5, folderPath, content, ct), ct);
}
public void Dispose()
{
try { DisconnectAsync().GetAwaiter().GetResult(); } catch { }
try { _gate.Dispose(); } catch { }
}
private async Task<CncResult<T>> RunGuardedAsync<T>(Func<T> body, CancellationToken ct)
{
await _gate.WaitAsync(ct).ConfigureAwait(false);
try
{
return await Task.Run(() =>
{
_currentErrors.Clear();
try
{
T value = body();
if (_currentErrors.Count > 0)
return CncResult<T>.Fail(_currentErrors.ToArray());
return CncResult<T>.Ok(value);
}
catch (OperationCanceledException)
{
return CncResult<T>.Cancelled();
}
catch (Exception ex)
{
_currentErrors.Add(new CncError(0, "FanucMachine", ex.Message));
return CncResult<T>.Fail(_currentErrors.ToArray());
}
}, ct).ConfigureAwait(false);
}
finally
{
_gate.Release();
}
}
private bool ConnectCore(CancellationToken ct)
{
if (_state == ConnectionState.Connected) return true;
SetState(ConnectionState.Connecting);
ct.ThrowIfCancellationRequested();
short ret;
switch (_config.Transport)
{
case FanucTransport.Ethernet:
short timeoutSec = (short)Math.Max(1, (int)_config.ConnectTimeout.TotalSeconds);
ret = Focas1.cnc_allclibhndl3(_config.IpAddress, _config.Port, timeoutSec, out _hndl);
break;
case FanucTransport.Hssb:
ret = Focas1.cnc_allclibhndl2(_config.HssbNode, out _hndl);
break;
case FanucTransport.Library:
ret = Focas1.cnc_allclibhndl(out _hndl);
break;
default:
Record((short)Focas1.focas_ret.EW_ATTRIB, "Connect");
SetState(ConnectionState.Faulted);
return false;
}
if (ret != Focas1.EW_OK)
{
Record(ret, "cnc_allclibhndl");
SetState(ConnectionState.Faulted);
return false;
}
if (ct.IsCancellationRequested)
{
try { Focas1.cnc_freelibhndl(_hndl); } catch { }
SetState(ConnectionState.Disconnected);
ct.ThrowIfCancellationRequested();
}
_caps = FanucDialectDetector.Detect(_hndl);
_dialect = FocasDialectFactory.Create(_caps);
if (_caps.Dialect == FanucDialect.Unknown)
_currentErrors.Add(new CncError(0, "cnc_sysinfo", "Unknown CNC model, falling back to Legacy dialect"));
SetState(ConnectionState.Connected);
return true;
}
private void DisconnectCore()
{
if (_state == ConnectionState.Disconnected) return;
short ret = Focas1.cnc_freelibhndl(_hndl);
if (ret != Focas1.EW_OK)
Record(ret, "cnc_freelibhndl");
SetState(ConnectionState.Disconnected);
}
private MachineSnapshot ReadCore(CancellationToken ct)
{
EnsureConnected();
ct.ThrowIfCancellationRequested();
Focas1.cnc_setpath(_hndl, 1);
var snap = new MachineSnapshot
{
CapturedAt = DateTime.Now,
Alarms = new List<string>(),
OperatorMessages = new List<string>(),
Mode = MachineMode.Unknown,
Run = RunState.Unknown,
Motion = MotionState.Unknown
};
snap.CycleTime = ReadCycleTime();
ct.ThrowIfCancellationRequested();
snap.PieceCount = ReadPieceCount();
ct.ThrowIfCancellationRequested();
snap.Alarms = ReadAlarms();
ct.ThrowIfCancellationRequested();
snap.OperatorMessages = ReadOperatorMessages();
ct.ThrowIfCancellationRequested();
var stateInfo = ReadMachineState();
snap.Mode = stateInfo.Mode;
snap.Run = stateInfo.Run;
snap.Motion = stateInfo.Motion;
snap.Emergency = stateInfo.Emergency;
snap.AlarmActive = stateInfo.Alarm;
if (snap.Mode != MachineMode.Edit)
{
ct.ThrowIfCancellationRequested();
var prog = ReadProgramInfo();
snap.MainProgram = prog.MainProgram;
snap.SubProgram = prog.SubProgram;
snap.ProgramComment = prog.Comment;
}
return snap;
}
private string ReadActiveProgramCore(CancellationToken ct)
{
EnsureConnected();
short ret;
string identifier = _dialect.ReadMainProgramIdentifier(_hndl, out ret);
if (ret != Focas1.EW_OK)
{
Record(ret, "ReadMainProgramIdentifier");
return null;
}
return UploadProgramByIdentifier(identifier, ct);
}
private string ReadProgramCore(string path, CancellationToken ct)
{
EnsureConnected();
if (string.IsNullOrEmpty(path))
{
_currentErrors.Add(new CncError(0, "ReadProgram", "Path is empty"));
return null;
}
return UploadProgramByIdentifier(path, ct);
}
private bool WriteProgramCore(string folderPath, CncProgram program, CancellationToken ct)
{
EnsureConnected();
if (program == null || string.IsNullOrEmpty(program.Content))
{
_currentErrors.Add(new CncError(0, "WriteProgram", "Program content is empty"));
return false;
}
var vr = _validator.Validate(program.Name ?? folderPath, program.Content);
if (!vr.Success)
{
string msg = string.Join("; ", vr.Errors
.Where(e => e.Severity == ValidationSeverity.Error)
.Select(e => e.Message));
_currentErrors.Add(new CncError(0, "Validator", msg));
return false;
}
if (!CanDownload()) return false;
if (string.IsNullOrEmpty(folderPath)) folderPath = DefaultDownloadFolder;
string fileName = ExtractFileNameFromContent(program);
if (string.IsNullOrEmpty(fileName))
{
_currentErrors.Add(new CncError(0, "WriteProgram", "Cannot extract program name from content"));
return false;
}
EnsureSeedProgram(folderPath, ct);
ct.ThrowIfCancellationRequested();
if (_dialect.SupportsPathBasedPrograms)
Focas1.cnc_pdf_del(_hndl, folderPath + fileName);
short ret = _dialect.DownloadStart(_hndl, folderPath);
if (ret != Focas1.EW_OK)
{
Record(ret, "DownloadStart");
_dialect.DownloadEnd(_hndl);
return false;
}
try
{
if (!TransferChunked(program.Content, ct)) return false;
}
finally
{
_dialect.DownloadEnd(_hndl);
}
if (_dialect.SupportsPathBasedPrograms)
{
ret = Focas1.cnc_pdf_slctmain(_hndl, folderPath + fileName);
if (ret != Focas1.EW_OK)
{
Record(ret, "cnc_pdf_slctmain");
return false;
}
}
return true;
}
private bool SelectMainProgramCore(string path)
{
EnsureConnected();
if (!_dialect.SupportsPathBasedPrograms)
{
_currentErrors.Add(new CncError((short)Focas1.focas_ret.EW_NOOPT,
"SelectMainProgram", "Path-based selection not supported on this CNC"));
return false;
}
short ret = Focas1.cnc_pdf_slctmain(_hndl, path);
if (ret != Focas1.EW_OK) { Record(ret, "cnc_pdf_slctmain"); return false; }
return true;
}
private bool DeleteProgramCore(string path)
{
EnsureConnected();
if (!_dialect.SupportsPathBasedPrograms)
{
_currentErrors.Add(new CncError((short)Focas1.focas_ret.EW_NOOPT,
"DeleteProgram", "Path-based delete not supported on this CNC"));
return false;
}
short ret = Focas1.cnc_pdf_del(_hndl, path);
if (ret != Focas1.EW_OK) { Record(ret, "cnc_pdf_del"); return false; }
return true;
}
private string UploadFocasFile(short fileType, string path, CancellationToken ct)
{
EnsureConnected();
if (string.IsNullOrEmpty(path))
{
_currentErrors.Add(new CncError(0, "UploadFocasFile", "Path is empty"));
return null;
}
long programNumber = TryParseProgramNumber(path);
short ret = _dialect.UploadStart(_hndl, path, programNumber, fileType);
if (ret != Focas1.EW_OK)
{
Record(ret, "UploadStart");
_dialect.UploadEnd(_hndl);
return null;
}
var output = new StringBuilder();
char[] buffer = new char[UploadBufferSize + 1];
try
{
while (true)
{
ct.ThrowIfCancellationRequested();
int len = UploadBufferSize;
short r = _dialect.UploadData(_hndl, ref len, buffer);
if (r == (short)Focas1.focas_ret.EW_BUFFER)
{
if (ct.WaitHandle.WaitOne(100)) ct.ThrowIfCancellationRequested();
continue;
}
if (r == Focas1.EW_OK && len > 0)
{
output.Append(buffer, 0, len);
if (buffer[len - 1] == '%') break;
}
else
{
if (r != Focas1.EW_OK) Record(r, "UploadData");
break;
}
}
}
finally { _dialect.UploadEnd(_hndl); }
return output.ToString();
}
private bool DownloadFocasFile(short fileType, string folderPath, string content, CancellationToken ct)
{
EnsureConnected();
if (string.IsNullOrEmpty(content))
{
_currentErrors.Add(new CncError(0, "DownloadFocasFile", "Content is empty"));
return false;
}
if (string.IsNullOrEmpty(folderPath)) folderPath = DefaultDownloadFolder;
short ret = _dialect.DownloadStart(_hndl, folderPath, fileType);
if (ret != Focas1.EW_OK)
{
Record(ret, "DownloadStart");
_dialect.DownloadEnd(_hndl);
return false;
}
try { return TransferChunked(content, ct); }
finally { _dialect.DownloadEnd(_hndl); }
}
private TimeSpan ReadCycleTime()
{
var ms = new Focas1.IODBPSD();
var min = new Focas1.IODBPSD();
short r1 = Focas1.cnc_rdparam(_hndl, Param_CycleTimeMs, 0, 8, ms);
short r2 = Focas1.cnc_rdparam(_hndl, Param_CycleTimeMin, 0, 8, min);
if (r1 != Focas1.EW_OK) Record(r1, "cnc_rdparam(cycle ms)");
if (r2 != Focas1.EW_OK) Record(r2, "cnc_rdparam(cycle min)");
return TimeSpan.FromMinutes(min.u.ldata) + TimeSpan.FromMilliseconds(ms.u.ldata);
}
private long ReadPieceCount()
{
try
{
if (_config.PieceCounterMacro == -1)
{
var p = new Focas1.IODBPSD();
short ret = Focas1.cnc_rdparam(_hndl, Param_PiecesCount, 0, 8, p);
if (ret != Focas1.EW_OK) { Record(ret, "cnc_rdparam(pieces)"); return 0; }
return p.u.ldata;
}
var macro = new Focas1.ODBM();
short macRet = Focas1.cnc_rdmacro(_hndl, (short)_config.PieceCounterMacro, 10, macro);
if (macRet != Focas1.EW_OK) { Record(macRet, "cnc_rdmacro(pieces)"); return 0; }
long val = Math.Abs(macro.mcr_val);
if (macro.dec_val > 0) val = val / (long)Math.Pow(10, macro.dec_val);
return macro.mcr_val < 0 ? -val : val;
}
catch (Exception ex)
{
_currentErrors.Add(new CncError(0, "ReadPieceCount", ex.Message));
return 0;
}
}
private IReadOnlyList<string> ReadAlarms()
{
string[] buf = new string[AlarmSlots];
short num = AlarmSlots;
short ret = _dialect.ReadAlarmMessages(_hndl, ref num, buf);
if (ret != Focas1.EW_OK) Record(ret, "ReadAlarmMessages");
var list = new List<string>(num);
for (int i = 0; i < num; i++)
if (!string.IsNullOrEmpty(buf[i])) list.Add(buf[i]);
return list;
}
private IReadOnlyList<string> ReadOperatorMessages()
{
string[] buf = new string[OperatorMsgSlots];
short num = 1;
short ret = _dialect.ReadOperatorMessages(_hndl, ref num, buf);
if (ret != Focas1.EW_OK) Record(ret, "ReadOperatorMessages");
var list = new List<string>(num);
for (int i = 0; i < num; i++)
if (!string.IsNullOrEmpty(buf[i])) list.Add(buf[i]);
return list;
}
private struct StateInfo
{
public MachineMode Mode;
public RunState Run;
public MotionState Motion;
public bool Emergency;
public bool Alarm;
}
private StateInfo ReadMachineState()
{
var s = new StateInfo
{
Mode = MachineMode.Unknown,
Run = RunState.Unknown,
Motion = MotionState.Unknown
};
var odbst = new Focas1.ODBST();
short ret = Focas1.cnc_statinfo(_hndl, odbst);
if (ret != Focas1.EW_OK) { Record(ret, "cnc_statinfo"); return s; }
switch (odbst.aut)
{
case 0: s.Mode = MachineMode.Mdi; break;
case 1: s.Mode = MachineMode.Auto; break;
case 3: s.Mode = MachineMode.Edit; break;
case 4: s.Mode = MachineMode.Handle; break;
case 5: s.Mode = MachineMode.Jog; break;
}
switch (odbst.run)
{
case 1: s.Run = RunState.Stop; break;
case 3: s.Run = RunState.ExecutingIso; break;
}
switch (odbst.motion)
{
case 0: s.Motion = MotionState.Idle; break;
case 1: s.Motion = MotionState.Moving; break;
case 2: s.Motion = MotionState.Hold; break;
}
s.Emergency = odbst.emergency == 1;
s.Alarm = odbst.alarm == 1;
return s;
}
private struct ProgramInfo
{
public string MainProgram;
public string SubProgram;
public string Comment;
}
private ProgramInfo ReadProgramInfo()
{
var info = new ProgramInfo { MainProgram = "", SubProgram = "", Comment = "" };
var prog = new Focas1.ODBPRO();
short ret = Focas1.cnc_rdprgnum(_hndl, prog);
if (ret != Focas1.EW_OK) { Record(ret, "cnc_rdprgnum"); return info; }
info.SubProgram = "O" + prog.data.ToString();
info.MainProgram = "O" + prog.mdata.ToString();
if (_dialect.SupportsPathBasedPrograms)
{
byte[] mainPath = new byte[242];
short pathRet = Focas1.cnc_pdf_rdmain(_hndl, mainPath);
if (pathRet == Focas1.EW_OK)
{
string str = ASCIIEncoding.ASCII.GetString(mainPath).TrimEnd('\0');
string[] parts = str.Split('/');
if (parts.Length > 0) info.MainProgram = parts[parts.Length - 1];
}
}
byte[] commentBuf = new byte[100];
int charsRead;
short cret = _dialect.ReadActiveProgramComment(_hndl, commentBuf, out charsRead);
if (cret != Focas1.EW_OK) { Record(cret, "ReadActiveProgramComment"); return info; }
string raw = ASCIIEncoding.ASCII.GetString(commentBuf).TrimEnd('\0');
int open = raw.IndexOf('(');
int close = raw.IndexOf(')');
if (open >= 0 && close > open)
info.Comment = raw.Substring(open + 1, close - open - 1);
else
info.Comment = "";
return info;
}
private string UploadProgramByIdentifier(string identifier, CancellationToken ct)
{
long programNumber = TryParseProgramNumber(identifier);
short ret = _dialect.UploadStart(_hndl, identifier, programNumber);
if (ret != Focas1.EW_OK)
{
Record(ret, "UploadStart");
_dialect.UploadEnd(_hndl);
return null;
}
var output = new StringBuilder();
char[] buffer = new char[UploadBufferSize + 1];
try
{
while (true)
{
ct.ThrowIfCancellationRequested();
int len = UploadBufferSize;
short r = _dialect.UploadData(_hndl, ref len, buffer);
if (r == (short)Focas1.focas_ret.EW_BUFFER)
{
if (ct.WaitHandle.WaitOne(100)) ct.ThrowIfCancellationRequested();
continue;
}
if (r == Focas1.EW_OK && len > 0)
{
output.Append(buffer, 0, len);
if (len > 0 && buffer[len - 1] == '%') break;
}
else
{
if (r != Focas1.EW_OK) Record(r, "UploadData");
break;
}
}
}
finally
{
_dialect.UploadEnd(_hndl);
}
return output.ToString();
}
private bool TransferChunked(string content, CancellationToken ct)
{
int len = content.Length;
int startPos = 0;
while (len > 0)
{
ct.ThrowIfCancellationRequested();
int num = len >= TransferChunkSize ? TransferChunkSize : len;
char[] chunk = new char[num];
content.CopyTo(startPos, chunk, 0, num);
short ret = _dialect.DownloadData(_hndl, ref num, chunk);
if (ret == (short)Focas1.focas_ret.EW_BUFFER)
{
if (ct.WaitHandle.WaitOne(100)) ct.ThrowIfCancellationRequested();
continue;
}
if (ret == Focas1.EW_OK)
{
startPos += num;
len -= num;
continue;
}
Record(ret, "DownloadData");
var dtl = new Focas1.ODBERR();
Focas1.cnc_getdtailerr(_hndl, dtl);
Debug.WriteLine("FOCAS detail err: " + dtl.err_no + " " + dtl.err_dtno);
return false;
}
return true;
}
private bool CanDownload()
{
var odbst = new Focas1.ODBST();
short ret = Focas1.cnc_statinfo(_hndl, odbst);
if (ret != Focas1.EW_OK) { Record(ret, "cnc_statinfo(canDownload)"); return false; }
if (odbst.aut != 0 && odbst.aut != 3)
{
Record((short)Focas1.focas_ret.EW_MODE, "CanDownload");
return false;
}
return true;
}
private void EnsureSeedProgram(string folderPath, CancellationToken ct)
{
if (!_dialect.SupportsPathBasedPrograms) return;
short selRet = Focas1.cnc_pdf_slctmain(_hndl, folderPath + SeedProgramName);
if (selRet == Focas1.EW_OK) return;
Focas1.cnc_pdf_del(_hndl, folderPath + SeedProgramName);
short sStart = _dialect.DownloadStart(_hndl, folderPath);
if (sStart != Focas1.EW_OK) { _dialect.DownloadEnd(_hndl); return; }
TransferChunked(SeedProgramBody, ct);
_dialect.DownloadEnd(_hndl);
Focas1.cnc_pdf_slctmain(_hndl, folderPath + SeedProgramName);
}
private static string ExtractFileNameFromContent(CncProgram program)
{
if (!string.IsNullOrEmpty(program.Name)) return program.Name;
string content = program.Content ?? "";
int start = content.IndexOf('<');
int end = content.IndexOf('>');
if (start >= 0 && end > start) return content.Substring(start + 1, end - start - 1);
return null;
}
private static long TryParseProgramNumber(string identifier)
{
if (string.IsNullOrEmpty(identifier)) return 0;
string s = identifier;
int slash = s.LastIndexOf('/');
if (slash >= 0 && slash < s.Length - 1) s = s.Substring(slash + 1);
if (s.Length > 0 && (s[0] == 'O' || s[0] == 'o')) s = s.Substring(1);
long n;
return long.TryParse(s, out n) ? n : 0;
}
private void EnsureConnected()
{
if (_state != ConnectionState.Connected || _dialect == null)
throw new InvalidOperationException("FanucMachine is not connected.");
}
private void Record(short code, string source)
{
var err = FanucErrorTranslator.Translate(code, source);
_currentErrors.Add(err);
if (FanucErrorTranslator.IsDisconnectError(code))
{
SetState(ConnectionState.Faulted);
try { Focas1.cnc_freelibhndl(_hndl); } catch { }
SetState(ConnectionState.Disconnected);
}
}
private void SetState(ConnectionState next)
{
if (_state == next) return;
_state = next;
var h = StateChanged;
if (h != null)
{
try { h(this, next); } catch { }
}
}
}
}

View file

@ -0,0 +1,21 @@
using System;
namespace FanucProgramManager.Cnc.Fanuc
{
internal static class FocasDialectFactory
{
public static IFocasDialect Create(FanucCapabilities caps)
{
if (caps == null) return new LegacyFocasDialect();
switch (caps.Dialect)
{
case FanucDialect.Modern: return new ModernFocasDialect();
case FanucDialect.Legacy:
case FanucDialect.Unknown:
return new LegacyFocasDialect();
default:
throw new InvalidOperationException("Unsupported FanucDialect: " + caps.Dialect);
}
}
}
}

23
Cnc/Fanuc/IFocasDialect.cs Executable file
View file

@ -0,0 +1,23 @@
namespace FanucProgramManager.Cnc.Fanuc
{
internal interface IFocasDialect
{
FanucDialect Dialect { get; }
bool SupportsPathBasedPrograms { get; }
bool SupportsAlmMsg2 { get; }
short ReadAlarmMessages(ushort hndl, ref short num, string[] outBuffer);
short ReadOperatorMessages(ushort hndl, ref short num, string[] outBuffer);
short DownloadStart(ushort hndl, string folderPath, short fileType = 0);
short DownloadData(ushort hndl, ref int len, char[] data);
short DownloadEnd(ushort hndl);
short UploadStart(ushort hndl, string fileName, long programNumber, short fileType = 0);
short UploadData(ushort hndl, ref int len, char[] data);
short UploadEnd(ushort hndl);
string ReadMainProgramIdentifier(ushort hndl, out short ret);
short ReadActiveProgramComment(ushort hndl, byte[] buffer, out int charsRead);
}
}

View file

@ -0,0 +1,95 @@
namespace FanucProgramManager.Cnc.Fanuc
{
public static class IsoProgramFormatter
{
public static string EnsureTitleBrackets(string program)
{
if (string.IsNullOrEmpty(program)) return program;
string head, body;
if (!SplitFirstNonEmptyLine(program, out head, out body)) return program;
int openAngle = head.IndexOf('<');
if (openAngle >= 0) return program;
int openPar = head.IndexOf('(');
int closePar = head.IndexOf(')');
int titleStart = FindTitleStart(head);
head = head.Insert(titleStart, "<");
if (closePar > 0)
{
openPar = head.IndexOf('(');
head = head.Insert(openPar, ">");
}
else
{
head = head + ">";
}
return head + body;
}
public static string InsertOrReplaceComment(string program, string comment)
{
if (string.IsNullOrEmpty(program)) return program;
program = EnsureTitleBrackets(program);
string head, body;
if (!SplitFirstNonEmptyLine(program, out head, out body)) return program;
int openPar = head.IndexOf('(');
int closePar = head.IndexOf(')');
if (openPar > 0 && closePar > openPar)
{
head = head.Remove(openPar + 1, closePar - openPar - 1);
head = head.Insert(openPar + 1, comment ?? "");
}
else
{
head = head + "(" + (comment ?? "") + ")";
}
return head + body;
}
public static string RenameAndComment(string program, string name, string comment)
{
if (string.IsNullOrEmpty(program)) return program;
string head, body;
if (!SplitFirstNonEmptyLine(program, out head, out body)) return program;
string newHead = "%\n<" + (name ?? "") + ">(" + (comment ?? "") + ")";
return newHead + body;
}
private static bool SplitFirstNonEmptyLine(string program, out string head, out string body)
{
head = program;
body = "";
bool sawFirstLf = false;
int lastLf = -1;
for (int i = 0; i < program.Length; i++)
{
if (program[i] == '\n')
{
if (sawFirstLf)
{
head = program.Substring(0, i);
body = program.Substring(i);
return true;
}
sawFirstLf = true;
lastLf = i;
}
}
return false;
}
private static int FindTitleStart(string head)
{
for (int i = 0; i < head.Length; i++)
{
if (head[i] == '\n') return i + 1;
}
return 0;
}
}
}

76
Cnc/Fanuc/LegacyFocasDialect.cs Executable file
View file

@ -0,0 +1,76 @@
using FanucProgramManager;
namespace FanucProgramManager.Cnc.Fanuc
{
internal sealed class LegacyFocasDialect : IFocasDialect
{
public FanucDialect Dialect { get { return FanucDialect.Legacy; } }
public bool SupportsPathBasedPrograms { get { return false; } }
public bool SupportsAlmMsg2 { get { return false; } }
public short DownloadStart(ushort hndl, string folderPath, short fileType = 0)
{
return Focas1.cnc_dwnstart3(hndl, fileType);
}
public short DownloadData(ushort hndl, ref int len, char[] data)
{
return Focas1.cnc_download3(hndl, ref len, data);
}
public short DownloadEnd(ushort hndl)
{
return Focas1.cnc_dwnend3(hndl);
}
public short UploadStart(ushort hndl, string fileName, long programNumber, short fileType = 0)
{
int pn = (int)programNumber;
return Focas1.cnc_upstart3(hndl, fileType, pn, pn);
}
public short UploadData(ushort hndl, ref int len, char[] data)
{
return Focas1.cnc_upload3(hndl, ref len, data);
}
public short UploadEnd(ushort hndl)
{
return Focas1.cnc_upend3(hndl);
}
public short ReadAlarmMessages(ushort hndl, ref short num, string[] outBuffer)
{
var arr = new Focas1.ODBALMMSG_data_custom[outBuffer.Length];
short ret = Focas1.cnc_rdalmmsg_custom(hndl, 0, ref num, arr);
for (int i = 0; i < num && i < outBuffer.Length; i++)
outBuffer[i] = arr[i].msg_len > 0 ? arr[i].alm_msg : "";
return ret;
}
public short ReadOperatorMessages(ushort hndl, ref short num, string[] outBuffer)
{
var arr = new Focas1.OPMSG3_data_custom[outBuffer.Length];
short ret = Focas1.cnc_rdopmsg3_custom(hndl, 4, ref num, arr);
for (int i = 0; i < num && i < outBuffer.Length; i++)
outBuffer[i] = arr[i].char_num > 0 ? arr[i].data : "";
return ret;
}
public string ReadMainProgramIdentifier(ushort hndl, out short ret)
{
var prog = new Focas1.ODBPRO();
ret = Focas1.cnc_rdprgnum(hndl, prog);
return "O" + prog.mdata.ToString();
}
public short ReadActiveProgramComment(ushort hndl, byte[] buffer, out int charsRead)
{
ushort len = (ushort)buffer.Length;
short blk;
short ret = Focas1.cnc_rdexecprog(hndl, ref len, out blk, buffer);
charsRead = len;
return ret;
}
}
}

79
Cnc/Fanuc/ModernFocasDialect.cs Executable file
View file

@ -0,0 +1,79 @@
using System.Text;
using FanucProgramManager;
namespace FanucProgramManager.Cnc.Fanuc
{
internal sealed class ModernFocasDialect : IFocasDialect
{
public FanucDialect Dialect { get { return FanucDialect.Modern; } }
public bool SupportsPathBasedPrograms { get { return true; } }
public bool SupportsAlmMsg2 { get { return true; } }
public short DownloadStart(ushort hndl, string folderPath, short fileType = 0)
{
return Focas1.cnc_dwnstart4(hndl, fileType, folderPath);
}
public short DownloadData(ushort hndl, ref int len, char[] data)
{
return Focas1.cnc_download4(hndl, ref len, data);
}
public short DownloadEnd(ushort hndl)
{
return Focas1.cnc_dwnend4(hndl);
}
public short UploadStart(ushort hndl, string fileName, long programNumber, short fileType = 0)
{
return Focas1.cnc_upstart4(hndl, fileType, fileName);
}
public short UploadData(ushort hndl, ref int len, char[] data)
{
return Focas1.cnc_upload4(hndl, ref len, data);
}
public short UploadEnd(ushort hndl)
{
return Focas1.cnc_upend4(hndl);
}
public short ReadAlarmMessages(ushort hndl, ref short num, string[] outBuffer)
{
var arr = new Focas1.ODBALMMSG2_data_custom[outBuffer.Length];
short ret = Focas1.cnc_rdalmmsg2_custom(hndl, -1, ref num, arr);
for (int i = 0; i < num && i < outBuffer.Length; i++)
outBuffer[i] = arr[i].msg_len > 0 ? arr[i].alm_msg : "";
return ret;
}
public short ReadOperatorMessages(ushort hndl, ref short num, string[] outBuffer)
{
var arr = new Focas1.OPMSG3_data_custom[outBuffer.Length];
short ret = Focas1.cnc_rdopmsg3_custom(hndl, 4, ref num, arr);
for (int i = 0; i < num && i < outBuffer.Length; i++)
outBuffer[i] = arr[i].char_num > 0 ? arr[i].data : "";
return ret;
}
public string ReadMainProgramIdentifier(ushort hndl, out short ret)
{
byte[] buf = new byte[242];
ret = Focas1.cnc_pdf_rdmain(hndl, buf);
return ASCIIEncoding.ASCII.GetString(buf).TrimEnd('\0');
}
public short ReadActiveProgramComment(ushort hndl, byte[] buffer, out int charsRead)
{
uint len = (uint)buffer.Length;
uint rows = 1;
byte[] main = new byte[242];
Focas1.cnc_pdf_rdmain(hndl, main);
string mainPath = ASCIIEncoding.ASCII.GetString(main).TrimEnd('\0');
short ret = Focas1.cnc_rdpdf_line(hndl, mainPath, 0, buffer, ref rows, ref len);
charsRead = (int)len;
return ret;
}
}
}

BIN
Cnc/Fanuc/libs/Fwlib32.dll Executable file

Binary file not shown.

BIN
Cnc/Fanuc/libs/fwlibNCG.dll Executable file

Binary file not shown.

BIN
Cnc/Fanuc/libs/fwlibe1.dll Executable file

Binary file not shown.

View file

@ -0,0 +1,15 @@
using System;
namespace FanucProgramManager.Cnc.Heidenhain
{
public sealed class HeidenhainConnectionConfig
{
public string Name { get; set; }
public string IpAddress { get; set; }
/// <summary>LSV2 data port — default 19000.</summary>
public int Port { get; set; } = 19000;
public string Username { get; set; } = string.Empty;
public string Password { get; set; } = string.Empty;
public TimeSpan ConnectTimeout { get; set; } = TimeSpan.FromSeconds(5);
}
}

View file

@ -0,0 +1,201 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using FanucProgramManager.Cnc;
using FanucProgramManager.Cnc.Models;
namespace FanucProgramManager.Cnc.Heidenhain
{
public sealed class HeidenhainMachine : ICncMachine
{
private readonly HeidenhainConnectionConfig _config;
private readonly INcProgramValidator _validator;
private readonly List<CncError> _errors = new List<CncError>();
private readonly SemaphoreSlim _gate = new SemaphoreSlim(1, 1);
private ILsv2Client _client;
private ConnectionState _state = ConnectionState.Disconnected;
public HeidenhainMachine(HeidenhainConnectionConfig config)
{
if (config == null) throw new ArgumentNullException("config");
_config = config;
_validator = new NcProgramValidator(CncManufacturer.Heidenhain);
}
internal HeidenhainMachine(HeidenhainConnectionConfig config, ILsv2Client client,
INcProgramValidator validator = null)
{
_config = config ?? throw new ArgumentNullException("config");
_client = client ?? throw new ArgumentNullException("client");
_validator = validator ?? new NcProgramValidator(CncManufacturer.Heidenhain);
_state = ConnectionState.Connected;
}
public string Name => _config.Name;
public ConnectionState State => _state;
public event EventHandler<ConnectionState> StateChanged;
public Task<CncResult<bool>> ConnectAsync(CancellationToken ct = default(CancellationToken))
{
return RunGuardedAsync(() =>
{
SetState(ConnectionState.Connecting);
_client = new Lsv2Client(_config);
if (!_client.Connect())
{
SetState(ConnectionState.Faulted);
_errors.Add(new CncError(0, "Connect", "LSV2 connection or login failed"));
return false;
}
SetState(ConnectionState.Connected);
return true;
}, ct);
}
public Task DisconnectAsync()
{
return RunGuardedAsync<bool>(() =>
{
if (_client != null)
{
_client.Disconnect();
_client = null;
}
SetState(ConnectionState.Disconnected);
return true;
}, CancellationToken.None);
}
public Task<CncResult<string>> ReadProgramAsync(string path, CancellationToken ct = default(CancellationToken))
{
return RunGuardedAsync(() =>
{
EnsureConnected();
string content = _client.ReceiveFile(path);
if (content == null)
_errors.Add(new CncError(0, "ReadProgram", "ReceiveFile returned null for: " + path));
return content;
}, ct);
}
public Task<CncResult<bool>> WriteProgramAsync(string path, CncProgram program, CancellationToken ct = default(CancellationToken))
{
return RunGuardedAsync(() =>
{
EnsureConnected();
if (program == null || string.IsNullOrEmpty(program.Content))
{
_errors.Add(new CncError(0, "WriteProgram", "Program content is empty"));
return false;
}
var vr = _validator.Validate(program.Name ?? path, program.Content);
if (!vr.Success)
{
string msg = string.Join("; ", vr.Errors
.Where(e => e.Severity == ValidationSeverity.Error)
.Select(e => e.Message));
_errors.Add(new CncError(0, "Validator", msg));
return false;
}
string ncPath = BuildNcPath(path, program.Name);
bool ok = _client.SendFile(ncPath, program.Content);
if (!ok) _errors.Add(new CncError(0, "WriteProgram", "SendFile failed for: " + ncPath));
return ok;
}, ct);
}
public Task<CncResult<MachineSnapshot>> ReadAsync(CancellationToken ct = default(CancellationToken))
{
return RunGuardedAsync(() =>
{
EnsureConnected();
return new MachineSnapshot
{
CapturedAt = DateTime.Now,
Mode = MachineMode.Unknown,
Run = RunState.Unknown,
Motion = MotionState.Unknown,
Alarms = new List<string>(),
OperatorMessages = new List<string>()
};
}, ct);
}
public Task<CncResult<string>> ReadActiveProgramAsync(CancellationToken ct = default(CancellationToken)) =>
Task.FromResult(CncResult<string>.Fail("ReadActiveProgram", "Not supported via basic LSV2"));
public Task<CncResult<bool>> SelectMainProgramAsync(string path, CancellationToken ct = default(CancellationToken)) =>
Task.FromResult(CncResult<bool>.Fail("SelectMainProgram", "Not supported via basic LSV2"));
public Task<CncResult<bool>> DeleteProgramAsync(string path, CancellationToken ct = default(CancellationToken)) =>
Task.FromResult(CncResult<bool>.Fail("DeleteProgram", "Not supported via basic LSV2"));
public void Dispose()
{
try { DisconnectAsync().GetAwaiter().GetResult(); } catch { }
try { _gate.Dispose(); } catch { }
}
// ── private ──────────────────────────────────────────────────────────
private async Task<CncResult<T>> RunGuardedAsync<T>(Func<T> body, CancellationToken ct)
{
await _gate.WaitAsync(ct).ConfigureAwait(false);
try
{
return await Task.Run(() =>
{
_errors.Clear();
try
{
T value = body();
return _errors.Count > 0
? CncResult<T>.Fail(_errors.ToArray())
: CncResult<T>.Ok(value);
}
catch (OperationCanceledException)
{
return CncResult<T>.Cancelled();
}
catch (Exception ex)
{
_errors.Add(new CncError(0, "HeidenhainMachine", ex.Message));
return CncResult<T>.Fail(_errors.ToArray());
}
}, ct).ConfigureAwait(false);
}
finally
{
_gate.Release();
}
}
private void EnsureConnected()
{
if (_state != ConnectionState.Connected || _client == null)
throw new InvalidOperationException("HeidenhainMachine is not connected.");
}
private void SetState(ConnectionState next)
{
if (_state == next) return;
_state = next;
var h = StateChanged;
if (h != null)
{
try { h(this, next); } catch { }
}
}
private static string BuildNcPath(string folder, string name)
{
if (string.IsNullOrEmpty(name)) return folder;
string sep = (folder.EndsWith("\\") || folder.EndsWith("/")) ? "" : "\\";
string ext = name.Contains(".") ? "" : ".H";
return folder + sep + name + ext;
}
}
}

View file

@ -0,0 +1,10 @@
namespace FanucProgramManager.Cnc.Heidenhain
{
internal interface ILsv2Client
{
bool Connect();
void Disconnect();
string ReceiveFile(string ncPath);
bool SendFile(string ncPath, string content);
}
}

View file

@ -0,0 +1,180 @@
using System;
using System.Net.Sockets;
using System.Text;
namespace FanucProgramManager.Cnc.Heidenhain
{
/// <summary>LSV2 protocol client for Heidenhain TNC controls.</summary>
internal sealed class Lsv2Client : ILsv2Client, IDisposable
{
private TcpClient _tcp;
private NetworkStream _stream;
private readonly HeidenhainConnectionConfig _config;
private const ushort T_OK = 0x0000;
private const ushort T_SE = 0x0001;
private const ushort T_LS_SELECT = 0x00A0;
private const ushort T_LS_LOG_IN = 0x00A1;
private const ushort T_LS_LOG_OUT = 0x00A2;
private const ushort T_R_FL = 0x0023;
private const ushort T_S_FL = 0x0024;
private const ushort T_FD = 0x0025;
public Lsv2Client(HeidenhainConnectionConfig config)
{
_config = config;
}
public bool Connect()
{
_tcp = new TcpClient();
_tcp.SendTimeout = (int)_config.ConnectTimeout.TotalMilliseconds;
_tcp.ReceiveTimeout = (int)_config.ConnectTimeout.TotalMilliseconds;
if (!_tcp.ConnectAsync(_config.IpAddress, _config.Port).Wait(_config.ConnectTimeout))
return false;
_stream = _tcp.GetStream();
return Login();
}
public void Disconnect()
{
try { SendCommand(T_LS_LOG_OUT, new byte[0]); } catch { }
try { if (_stream != null) _stream.Close(); } catch { }
try { if (_tcp != null) _tcp.Close(); } catch { }
}
/// <summary>Upload file from NC to PC. Returns file content or null on failure.</summary>
public string ReceiveFile(string ncPath)
{
byte[] pathBytes = Encoding.ASCII.GetBytes(ncPath + "\0");
SendCommand(T_R_FL, pathBytes);
var sb = new StringBuilder();
while (true)
{
ushort cmd;
byte[] data = ReadFrame(out cmd);
if (cmd == T_SE || data == null) return null;
if (cmd == T_OK) break;
if (cmd == T_FD) sb.Append(Encoding.ASCII.GetString(data));
}
return sb.ToString();
}
/// <summary>Download file from PC to NC.</summary>
public bool SendFile(string ncPath, string content)
{
byte[] pathBytes = Encoding.ASCII.GetBytes(ncPath + "\0");
byte[] contentBytes = Encoding.ASCII.GetBytes(content);
byte[] payload = new byte[pathBytes.Length + contentBytes.Length];
Buffer.BlockCopy(pathBytes, 0, payload, 0, pathBytes.Length);
Buffer.BlockCopy(contentBytes, 0, payload, pathBytes.Length, contentBytes.Length);
SendCommand(T_S_FL, payload);
ushort cmd;
ReadFrame(out cmd);
return cmd == T_OK;
}
public void Dispose() => Disconnect();
// ── private ──────────────────────────────────────────────────────────
private bool Login()
{
// Select programming mode (3)
byte[] mode = { 0x03 };
SendCommand(T_LS_SELECT, mode);
ushort cmd;
ReadFrame(out cmd);
if (cmd != T_OK) return false;
// Authenticate
byte[] user = Encoding.ASCII.GetBytes(_config.Username + "\0");
byte[] pass = Encoding.ASCII.GetBytes(_config.Password + "\0");
byte[] cred = new byte[user.Length + pass.Length];
Buffer.BlockCopy(user, 0, cred, 0, user.Length);
Buffer.BlockCopy(pass, 0, cred, user.Length, pass.Length);
SendCommand(T_LS_LOG_IN, cred);
ReadFrame(out cmd);
return cmd == T_OK;
}
private void SendCommand(ushort cmdCode, byte[] data)
{
int dataLen = data.Length;
ushort size = (ushort)(2 + dataLen);
byte sizeHi = (byte)(size >> 8);
byte sizeLo = (byte)(size & 0xFF);
byte cmdHi = (byte)(cmdCode >> 8);
byte cmdLo = (byte)(cmdCode & 0xFF);
byte[] crcInput = new byte[4 + dataLen];
crcInput[0] = sizeHi; crcInput[1] = sizeLo;
crcInput[2] = cmdHi; crcInput[3] = cmdLo;
Buffer.BlockCopy(data, 0, crcInput, 4, dataLen);
ushort crc = Crc16(crcInput);
byte[] frame = new byte[1 + 4 + dataLen + 2];
int i = 0;
frame[i++] = 0x02;
frame[i++] = sizeHi;
frame[i++] = sizeLo;
frame[i++] = cmdHi;
frame[i++] = cmdLo;
Buffer.BlockCopy(data, 0, frame, i, dataLen);
i += dataLen;
frame[i++] = (byte)(crc >> 8);
frame[i] = (byte)(crc & 0xFF);
_stream.Write(frame, 0, frame.Length);
}
private byte[] ReadFrame(out ushort cmd)
{
cmd = 0xFFFF;
int stx = _stream.ReadByte();
if (stx != 0x02) return null;
byte sizeHi = (byte)_stream.ReadByte();
byte sizeLo = (byte)_stream.ReadByte();
int size = (sizeHi << 8) | sizeLo;
if (size < 2) return null;
byte cmdHi = (byte)_stream.ReadByte();
byte cmdLo = (byte)_stream.ReadByte();
cmd = (ushort)((cmdHi << 8) | cmdLo);
int dataLen = size - 2;
byte[] data = new byte[dataLen];
int read = 0;
while (read < dataLen)
{
int n = _stream.Read(data, read, dataLen - read);
if (n == 0) break;
read += n;
}
// consume CRC bytes
_stream.ReadByte();
_stream.ReadByte();
return data;
}
private static ushort Crc16(byte[] data)
{
ushort crc = 0xFFFF;
foreach (byte b in data)
{
crc ^= (ushort)(b << 8);
for (int i = 0; i < 8; i++)
crc = (crc & 0x8000) != 0
? (ushort)((crc << 1) ^ 0x1021)
: (ushort)(crc << 1);
}
return crc;
}
}
}

26
Cnc/ICncMachine.cs Executable file
View file

@ -0,0 +1,26 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using FanucProgramManager.Cnc.Models;
namespace FanucProgramManager.Cnc
{
public interface ICncMachine : IDisposable
{
string Name { get; }
ConnectionState State { get; }
event EventHandler<ConnectionState> StateChanged;
Task<CncResult<bool>> ConnectAsync(CancellationToken ct = default);
Task DisconnectAsync();
Task<CncResult<MachineSnapshot>> ReadAsync(CancellationToken ct = default);
Task<CncResult<string>> ReadActiveProgramAsync(CancellationToken ct = default);
Task<CncResult<string>> ReadProgramAsync(string path, CancellationToken ct = default);
Task<CncResult<bool>> WriteProgramAsync(string path, CncProgram program, CancellationToken ct = default);
Task<CncResult<bool>> SelectMainProgramAsync(string path, CancellationToken ct = default);
Task<CncResult<bool>> DeleteProgramAsync(string path, CancellationToken ct = default);
}
}

15
Cnc/IFanucMachine.cs Normal file
View file

@ -0,0 +1,15 @@
using System.Threading;
using System.Threading.Tasks;
using FanucProgramManager.Cnc.Models;
namespace FanucProgramManager.Cnc
{
/// <summary>Fanuc-specific extensions: tool offset and work zero offset transfer.</summary>
public interface IFanucMachine : ICncMachine
{
Task<CncResult<string>> ReadToolDataAsync(string path, CancellationToken ct = default(CancellationToken));
Task<CncResult<string>> ReadWorkZeroDataAsync(string path, CancellationToken ct = default(CancellationToken));
Task<CncResult<bool>> WriteToolDataAsync(string folderPath, string content, CancellationToken ct = default(CancellationToken));
Task<CncResult<bool>> WriteWorkZeroDataAsync(string folderPath, string content, CancellationToken ct = default(CancellationToken));
}
}

View file

@ -0,0 +1,12 @@
using FanucProgramManager.Cnc.Models;
namespace FanucProgramManager.Cnc
{
/// <summary>Validates NC program content before upload to a CNC controller.</summary>
public interface INcProgramValidator
{
/// <param name="programName">File name or program identifier (used for name-format checks).</param>
/// <param name="content">Full program content string.</param>
ValidationResult Validate(string programName, string content);
}
}

View file

@ -0,0 +1,10 @@
namespace FanucProgramManager.Cnc.Mitsubishi
{
internal interface IMitsubishiFtpClient
{
bool Ping();
string DownloadFile(string ncPath);
void UploadFile(string ncPath, string content);
void DeleteFile(string ncPath);
}
}

View file

@ -0,0 +1,16 @@
using System;
namespace FanucProgramManager.Cnc.Mitsubishi
{
public sealed class MitsubishiConnectionConfig
{
public string Name { get; set; }
public string IpAddress { get; set; }
public int Port { get; set; } = 21;
public string Username { get; set; } = string.Empty;
public string Password { get; set; } = string.Empty;
/// <summary>NC directory for programs on the Mitsubishi FTP server.</summary>
public string ProgramDirectory { get; set; } = "/PRG/";
public TimeSpan RequestTimeout { get; set; } = TimeSpan.FromSeconds(10);
}
}

View file

@ -0,0 +1,71 @@
using System;
using System.IO;
using System.Net;
using System.Text;
namespace FanucProgramManager.Cnc.Mitsubishi
{
/// <summary>FTP client for Mitsubishi M-series CNC program transfer.</summary>
internal sealed class MitsubishiFtpClient : IMitsubishiFtpClient
{
private readonly MitsubishiConnectionConfig _config;
public MitsubishiFtpClient(MitsubishiConnectionConfig config)
{
_config = config ?? throw new ArgumentNullException("config");
}
/// <summary>Verify FTP server is reachable by listing the program directory.</summary>
public bool Ping()
{
try
{
var req = CreateRequest(_config.ProgramDirectory, WebRequestMethods.Ftp.ListDirectory);
using (var resp = (FtpWebResponse)req.GetResponse())
return resp.StatusCode == FtpStatusCode.DataAlreadyOpen
|| resp.StatusCode == FtpStatusCode.OpeningData
|| (int)resp.StatusCode / 100 == 2;
}
catch { return false; }
}
/// <summary>Download file from Mitsubishi to string.</summary>
public string DownloadFile(string ncPath)
{
var req = CreateRequest(ncPath, WebRequestMethods.Ftp.DownloadFile);
using (var resp = (FtpWebResponse)req.GetResponse())
using (var reader = new StreamReader(resp.GetResponseStream(), Encoding.ASCII))
return reader.ReadToEnd();
}
/// <summary>Upload string content to Mitsubishi.</summary>
public void UploadFile(string ncPath, string content)
{
var req = CreateRequest(ncPath, WebRequestMethods.Ftp.UploadFile);
byte[] data = Encoding.ASCII.GetBytes(content);
req.ContentLength = data.Length;
using (var stream = req.GetRequestStream())
stream.Write(data, 0, data.Length);
using ((FtpWebResponse)req.GetResponse()) { }
}
public void DeleteFile(string ncPath)
{
var req = CreateRequest(ncPath, WebRequestMethods.Ftp.DeleteFile);
using ((FtpWebResponse)req.GetResponse()) { }
}
private FtpWebRequest CreateRequest(string path, string method)
{
string uri = string.Format("ftp://{0}:{1}{2}", _config.IpAddress, _config.Port, path);
var req = (FtpWebRequest)WebRequest.Create(uri);
req.Method = method;
req.Credentials = new NetworkCredential(_config.Username, _config.Password);
req.Timeout = (int)_config.RequestTimeout.TotalMilliseconds;
req.KeepAlive = false;
req.UseBinary = false;
req.UsePassive = true;
return req;
}
}
}

View file

@ -0,0 +1,210 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using FanucProgramManager.Cnc;
using FanucProgramManager.Cnc.Models;
namespace FanucProgramManager.Cnc.Mitsubishi
{
public sealed class MitsubishiMachine : ICncMachine
{
private readonly MitsubishiConnectionConfig _config;
private readonly IMitsubishiFtpClient _ftp;
private readonly INcProgramValidator _validator;
private readonly List<CncError> _errors = new List<CncError>();
private readonly SemaphoreSlim _gate = new SemaphoreSlim(1, 1);
private ConnectionState _state = ConnectionState.Disconnected;
public MitsubishiMachine(MitsubishiConnectionConfig config)
{
_config = config ?? throw new ArgumentNullException("config");
_ftp = new MitsubishiFtpClient(config);
_validator = new NcProgramValidator(CncManufacturer.Mitsubishi);
}
internal MitsubishiMachine(MitsubishiConnectionConfig config, IMitsubishiFtpClient ftpClient,
INcProgramValidator validator = null)
{
_config = config ?? throw new ArgumentNullException("config");
_ftp = ftpClient ?? throw new ArgumentNullException("ftpClient");
_validator = validator ?? new NcProgramValidator(CncManufacturer.Mitsubishi);
}
public string Name { get { return _config.Name; } }
public ConnectionState State { get { return _state; } }
public event EventHandler<ConnectionState> StateChanged;
public Task<CncResult<bool>> ConnectAsync(CancellationToken ct = default(CancellationToken))
{
return RunGuardedAsync(() =>
{
SetState(ConnectionState.Connecting);
if (!_ftp.Ping())
{
SetState(ConnectionState.Faulted);
_errors.Add(new CncError(0, "Connect",
string.Format("FTP server unreachable at {0}:{1}", _config.IpAddress, _config.Port)));
return false;
}
SetState(ConnectionState.Connected);
return true;
}, ct);
}
public Task DisconnectAsync()
{
return RunGuardedAsync<bool>(() =>
{
SetState(ConnectionState.Disconnected);
return true;
}, CancellationToken.None);
}
public Task<CncResult<string>> ReadProgramAsync(string path, CancellationToken ct = default(CancellationToken))
{
return RunGuardedAsync(() =>
{
EnsureConnected();
string ncPath = ResolvePath(path, null);
try { return _ftp.DownloadFile(ncPath); }
catch (Exception ex)
{
_errors.Add(new CncError(0, "ReadProgram", ex.Message));
return null;
}
}, ct);
}
public Task<CncResult<bool>> WriteProgramAsync(string path, CncProgram program, CancellationToken ct = default(CancellationToken))
{
return RunGuardedAsync(() =>
{
EnsureConnected();
if (program == null || string.IsNullOrEmpty(program.Content))
{
_errors.Add(new CncError(0, "WriteProgram", "Content is empty"));
return false;
}
var vr = _validator.Validate(program.Name ?? path, program.Content);
if (!vr.Success)
{
string msg = string.Join("; ", vr.Errors
.Where(e => e.Severity == ValidationSeverity.Error)
.Select(e => e.Message));
_errors.Add(new CncError(0, "Validator", msg));
return false;
}
string ncPath = ResolvePath(path, program.Name);
try { _ftp.UploadFile(ncPath, program.Content); return true; }
catch (Exception ex)
{
_errors.Add(new CncError(0, "WriteProgram", ex.Message));
return false;
}
}, ct);
}
public Task<CncResult<bool>> DeleteProgramAsync(string path, CancellationToken ct = default(CancellationToken))
{
return RunGuardedAsync(() =>
{
EnsureConnected();
try { _ftp.DeleteFile(path); return true; }
catch (Exception ex)
{
_errors.Add(new CncError(0, "DeleteProgram", ex.Message));
return false;
}
}, ct);
}
public Task<CncResult<MachineSnapshot>> ReadAsync(CancellationToken ct = default(CancellationToken))
{
return RunGuardedAsync(() =>
{
EnsureConnected();
return new MachineSnapshot
{
CapturedAt = DateTime.Now,
Mode = MachineMode.Unknown,
Run = RunState.Unknown,
Motion = MotionState.Unknown,
Alarms = new List<string>(),
OperatorMessages = new List<string>()
};
}, ct);
}
public Task<CncResult<string>> ReadActiveProgramAsync(CancellationToken ct = default(CancellationToken))
{
return Task.FromResult(CncResult<string>.Fail("ReadActiveProgram", "Not supported via FTP"));
}
public Task<CncResult<bool>> SelectMainProgramAsync(string path, CancellationToken ct = default(CancellationToken))
{
return Task.FromResult(CncResult<bool>.Fail("SelectMainProgram", "Not supported via FTP"));
}
public void Dispose()
{
try { DisconnectAsync().GetAwaiter().GetResult(); } catch { }
try { _gate.Dispose(); } catch { }
}
private async Task<CncResult<T>> RunGuardedAsync<T>(Func<T> body, CancellationToken ct)
{
await _gate.WaitAsync(ct).ConfigureAwait(false);
try
{
return await Task.Run(() =>
{
_errors.Clear();
try
{
T value = body();
return _errors.Count > 0
? CncResult<T>.Fail(_errors.ToArray())
: CncResult<T>.Ok(value);
}
catch (OperationCanceledException) { return CncResult<T>.Cancelled(); }
catch (Exception ex)
{
_errors.Add(new CncError(0, "MitsubishiMachine", ex.Message));
return CncResult<T>.Fail(_errors.ToArray());
}
}, ct).ConfigureAwait(false);
}
finally { _gate.Release(); }
}
private void EnsureConnected()
{
if (_state != ConnectionState.Connected)
throw new InvalidOperationException("MitsubishiMachine is not connected.");
}
private void SetState(ConnectionState next)
{
if (_state == next) return;
_state = next;
var h = StateChanged;
if (h != null)
{
try { h(this, next); } catch { }
}
}
/// <summary>Build full FTP path. Mitsubishi programs have no extension.</summary>
private string ResolvePath(string folder, string name)
{
if (string.IsNullOrEmpty(name))
return folder.Contains("/") ? folder : _config.ProgramDirectory + folder;
string dir = folder ?? _config.ProgramDirectory;
if (!dir.EndsWith("/")) dir += "/";
return dir + name;
}
}
}

25
Cnc/Models/CncError.cs Executable file
View file

@ -0,0 +1,25 @@
using System;
namespace FanucProgramManager.Cnc.Models
{
public sealed class CncError
{
public int Code { get; }
public string Source { get; }
public string Message { get; }
public DateTime At { get; }
public CncError(int code, string source, string message)
{
Code = code;
Source = source ?? "";
Message = message ?? "";
At = DateTime.Now;
}
public override string ToString()
{
return "[" + Code + "] " + Source + ": " + Message;
}
}
}

View file

@ -0,0 +1,4 @@
namespace FanucProgramManager.Cnc.Models
{
public enum CncManufacturer { Fanuc, Heidenhain, Siemens, Mitsubishi }
}

10
Cnc/Models/CncProgram.cs Executable file
View file

@ -0,0 +1,10 @@
namespace FanucProgramManager.Cnc.Models
{
public sealed class CncProgram
{
public string Name { get; set; }
public string Comment { get; set; }
public string Content { get; set; }
public string Path { get; set; }
}
}

41
Cnc/Models/CncResult.cs Executable file
View file

@ -0,0 +1,41 @@
using System;
using System.Collections.Generic;
namespace FanucProgramManager.Cnc.Models
{
/// <summary>Result wrapper for all CNC operations.</summary>
public sealed class CncResult<T>
{
public bool Success { get; }
public T Value { get; }
public IReadOnlyList<CncError> Errors { get; }
public bool WasCancelled { get; }
private CncResult(bool success, T value, IReadOnlyList<CncError> errors, bool cancelled)
{
Success = success;
Value = value;
Errors = errors ?? (IReadOnlyList<CncError>)Array.Empty<CncError>();
WasCancelled = cancelled;
}
public static CncResult<T> Ok(T value) =>
new CncResult<T>(true, value, null, false);
public static CncResult<T> Fail(IReadOnlyList<CncError> errors) =>
new CncResult<T>(false, default(T), errors, false);
public static CncResult<T> Fail(string source, string message) =>
Fail(new[] { new CncError(0, source, message) });
public static CncResult<T> Cancelled() =>
new CncResult<T>(false, default(T), null, true);
public override string ToString()
{
if (WasCancelled) return $"CncResult<{typeof(T).Name}>: Cancelled";
if (!Success) return $"CncResult<{typeof(T).Name}>: Fail ({Errors.Count} errors)";
return $"CncResult<{typeof(T).Name}>: Ok";
}
}
}

10
Cnc/Models/ConnectionState.cs Executable file
View file

@ -0,0 +1,10 @@
namespace FanucProgramManager.Cnc.Models
{
public enum ConnectionState
{
Disconnected,
Connecting,
Connected,
Faulted
}
}

12
Cnc/Models/MachineMode.cs Executable file
View file

@ -0,0 +1,12 @@
namespace FanucProgramManager.Cnc.Models
{
public enum MachineMode
{
Mdi = 0,
Auto = 1,
Edit = 3,
Handle = 4,
Jog = 5,
Unknown = 255
}
}

22
Cnc/Models/MachineSnapshot.cs Executable file
View file

@ -0,0 +1,22 @@
using System;
using System.Collections.Generic;
namespace FanucProgramManager.Cnc.Models
{
public sealed class MachineSnapshot
{
public string MainProgram { get; set; }
public string SubProgram { get; set; }
public string ProgramComment { get; set; }
public MachineMode Mode { get; set; }
public RunState Run { get; set; }
public MotionState Motion { get; set; }
public bool Emergency { get; set; }
public bool AlarmActive { get; set; }
public TimeSpan CycleTime { get; set; }
public long PieceCount { get; set; }
public IReadOnlyList<string> Alarms { get; set; }
public IReadOnlyList<string> OperatorMessages { get; set; }
public DateTime CapturedAt { get; set; }
}
}

10
Cnc/Models/MotionState.cs Executable file
View file

@ -0,0 +1,10 @@
namespace FanucProgramManager.Cnc.Models
{
public enum MotionState
{
Idle = 0,
Moving = 1,
Hold = 2,
Unknown = 255
}
}

9
Cnc/Models/RunState.cs Executable file
View file

@ -0,0 +1,9 @@
namespace FanucProgramManager.Cnc.Models
{
public enum RunState
{
Stop = 1,
ExecutingIso = 3,
Unknown = 255
}
}

View file

@ -0,0 +1,21 @@
namespace FanucProgramManager.Cnc.Models
{
public sealed class ValidationError
{
public int Line { get; }
public string Message { get; }
public ValidationSeverity Severity { get; }
public ValidationError(int line, string message, ValidationSeverity severity)
{
Line = line;
Message = message ?? "";
Severity = severity;
}
public override string ToString()
{
return string.Format("[{0}] Line {1}: {2}", Severity, Line, Message);
}
}
}

View file

@ -0,0 +1,32 @@
using System.Collections.Generic;
namespace FanucProgramManager.Cnc.Models
{
public sealed class ValidationResult
{
public bool Success { get; }
public IReadOnlyList<ValidationError> Errors { get; }
private ValidationResult(bool success, IReadOnlyList<ValidationError> errors)
{
Success = success;
Errors = errors ?? new ValidationError[0];
}
public static ValidationResult Ok()
{
return new ValidationResult(true, new ValidationError[0]);
}
public static ValidationResult Fail(IReadOnlyList<ValidationError> errors)
{
return new ValidationResult(false, errors);
}
/// <summary>Success result that still carries warning-level entries.</summary>
public static ValidationResult OkWithWarnings(IReadOnlyList<ValidationError> warnings)
{
return new ValidationResult(true, warnings);
}
}
}

View file

@ -0,0 +1,4 @@
namespace FanucProgramManager.Cnc.Models
{
public enum ValidationSeverity { Error, Warning }
}

215
Cnc/NcProgramValidator.cs Normal file
View file

@ -0,0 +1,215 @@
using System;
using System.Collections.Generic;
using System.Text.RegularExpressions;
using FanucProgramManager.Cnc.Models;
namespace FanucProgramManager.Cnc
{
/// <summary>Validates NC program content per-manufacturer before upload.</summary>
public sealed class NcProgramValidator : INcProgramValidator
{
private const int MaxBlockLength = 80;
private const int MaxCommentLength = 32;
private static readonly Regex FanucNamePattern = new Regex(@"^O\d{4}$", RegexOptions.Compiled);
private static readonly Regex FanucFirstLinePattern = new Regex(@"^O\d{4}", RegexOptions.Compiled);
private readonly CncManufacturer _manufacturer;
public NcProgramValidator(CncManufacturer manufacturer)
{
_manufacturer = manufacturer;
}
public ValidationResult Validate(string programName, string content)
{
var errors = new List<ValidationError>();
// Name-format check (uses just the bare filename without path)
string name = ExtractName(programName);
ValidateName(name, errors);
if (string.IsNullOrEmpty(content))
return errors.Count == 0 ? ValidationResult.Ok() : ValidationResult.Fail(errors);
// Normalise: strip CR, split on LF
string normalized = content.Replace("\r", "");
string[] lines = normalized.Split('\n');
// First-line check (Fanuc/Mitsubishi)
if ((_manufacturer == CncManufacturer.Fanuc || _manufacturer == CncManufacturer.Mitsubishi)
&& lines.Length > 0)
{
string first = lines[0].Trim();
if (!FanucFirstLinePattern.IsMatch(first))
errors.Add(new ValidationError(1, "First line must be O#### program number", ValidationSeverity.Error));
}
for (int i = 0; i < lines.Length; i++)
{
int lineNum = i + 1;
string line = lines[i];
// Tab check
if (line.IndexOf('\t') >= 0)
errors.Add(new ValidationError(lineNum, "Tab character not allowed", ValidationSeverity.Error));
// Lowercase check
for (int ci = 0; ci < line.Length; ci++)
{
if (char.IsLower(line[ci]))
{
errors.Add(new ValidationError(lineNum, "Lowercase letters not allowed", ValidationSeverity.Error));
break;
}
}
// Block length
if (line.Length > MaxBlockLength)
errors.Add(new ValidationError(lineNum,
string.Format("Block length {0} exceeds 80 chars", line.Length),
ValidationSeverity.Error));
// Nested parens + comment length
ValidateParensAndComments(line, lineNum, errors);
// Forbidden chars per manufacturer
ValidateForbiddenChars(line, lineNum, errors);
}
bool hasErrors = false;
for (int i = 0; i < errors.Count; i++)
if (errors[i].Severity == ValidationSeverity.Error) { hasErrors = true; break; }
if (hasErrors)
return ValidationResult.Fail(errors);
// Preserve warnings even when overall result is Success
return errors.Count == 0 ? ValidationResult.Ok() : ValidationResult.OkWithWarnings(errors);
}
// ── private helpers ───────────────────────────────────────────────────
private void ValidateName(string name, List<ValidationError> errors)
{
if (string.IsNullOrEmpty(name)) return;
switch (_manufacturer)
{
case CncManufacturer.Fanuc:
case CncManufacturer.Mitsubishi:
if (!FanucNamePattern.IsMatch(name))
errors.Add(new ValidationError(0,
string.Format("Program name '{0}' must match O####", name),
ValidationSeverity.Error));
break;
case CncManufacturer.Siemens:
if (!name.EndsWith(".MPF"))
errors.Add(new ValidationError(0,
string.Format("Program name '{0}' must end with .MPF", name),
ValidationSeverity.Error));
break;
case CncManufacturer.Heidenhain:
if (!name.EndsWith(".H"))
errors.Add(new ValidationError(0,
string.Format("Program name '{0}' must end with .H", name),
ValidationSeverity.Error));
break;
}
}
private void ValidateParensAndComments(string line, int lineNum, List<ValidationError> errors)
{
int depth = 0;
int commentStart = -1;
for (int i = 0; i < line.Length; i++)
{
char c = line[i];
if (c == '(')
{
depth++;
if (depth > 1)
{
errors.Add(new ValidationError(lineNum, "Nested parentheses not allowed", ValidationSeverity.Error));
// Don't keep adding the same error for this line
return;
}
commentStart = i;
}
else if (c == ')')
{
if (depth == 1 && commentStart >= 0)
{
// Extract comment content between the parens
int len = i - commentStart - 1;
if (len > MaxCommentLength)
errors.Add(new ValidationError(lineNum,
string.Format("Comment length {0} exceeds 32 chars", len),
ValidationSeverity.Error));
}
if (depth > 0) depth--;
commentStart = -1;
}
}
}
private void ValidateForbiddenChars(string line, int lineNum, List<ValidationError> errors)
{
switch (_manufacturer)
{
case CncManufacturer.Fanuc:
case CncManufacturer.Mitsubishi:
for (int i = 0; i < line.Length; i++)
{
char c = line[i];
if (c == ';' || c == '[' || c == ']')
{
errors.Add(new ValidationError(lineNum,
string.Format("Forbidden char '{0}' for Fanuc/Mitsubishi", c),
ValidationSeverity.Error));
}
else if (c == '#')
{
errors.Add(new ValidationError(lineNum,
"Macro variable '#' detected — verify macro support on target CNC",
ValidationSeverity.Warning));
}
}
break;
case CncManufacturer.Siemens:
for (int i = 0; i < line.Length; i++)
{
if (line[i] == ';')
errors.Add(new ValidationError(lineNum,
"Forbidden char ';' for Siemens (use () for comments)",
ValidationSeverity.Error));
}
break;
case CncManufacturer.Heidenhain:
for (int i = 0; i < line.Length; i++)
{
char c = line[i];
if (c == '%' || c == ';')
errors.Add(new ValidationError(lineNum,
string.Format("Forbidden char '{0}' for Heidenhain", c),
ValidationSeverity.Error));
}
break;
}
}
/// <summary>Extracts bare filename from a path (strips directory prefix).</summary>
private static string ExtractName(string programName)
{
if (string.IsNullOrEmpty(programName)) return programName;
int slash = programName.LastIndexOf('/');
int backslash = programName.LastIndexOf('\\');
int sep = slash > backslash ? slash : backslash;
return sep >= 0 ? programName.Substring(sep + 1) : programName;
}
}
}

View file

@ -0,0 +1,10 @@
namespace FanucProgramManager.Cnc.Siemens
{
internal interface ISiemensFtpClient
{
bool Ping();
string DownloadFile(string ncPath);
void UploadFile(string ncPath, string content);
void DeleteFile(string ncPath);
}
}

View file

@ -0,0 +1,16 @@
using System;
namespace FanucProgramManager.Cnc.Siemens
{
public sealed class SiemensConnectionConfig
{
public string Name { get; set; }
public string IpAddress { get; set; }
public int Port { get; set; } = 21;
public string Username { get; set; } = string.Empty;
public string Password { get; set; } = string.Empty;
/// <summary>NC directory for main programs on the CNC FTP server.</summary>
public string ProgramDirectory { get; set; } = "/_N_MPF_DIR/";
public TimeSpan RequestTimeout { get; set; } = TimeSpan.FromSeconds(10);
}
}

View file

@ -0,0 +1,71 @@
using System;
using System.IO;
using System.Net;
using System.Text;
namespace FanucProgramManager.Cnc.Siemens
{
/// <summary>FTP client for Siemens Sinumerik CNC program transfer.</summary>
internal sealed class SiemensFtpClient : ISiemensFtpClient
{
private readonly SiemensConnectionConfig _config;
public SiemensFtpClient(SiemensConnectionConfig config)
{
_config = config ?? throw new ArgumentNullException("config");
}
/// <summary>Verify FTP server is reachable by listing the program directory.</summary>
public bool Ping()
{
try
{
var req = CreateRequest(_config.ProgramDirectory, WebRequestMethods.Ftp.ListDirectory);
using (var resp = (FtpWebResponse)req.GetResponse())
return resp.StatusCode == FtpStatusCode.DataAlreadyOpen
|| resp.StatusCode == FtpStatusCode.OpeningData
|| (int)resp.StatusCode / 100 == 2;
}
catch { return false; }
}
/// <summary>Download file from Sinumerik to string (upload from CNC perspective).</summary>
public string DownloadFile(string ncPath)
{
var req = CreateRequest(ncPath, WebRequestMethods.Ftp.DownloadFile);
using (var resp = (FtpWebResponse)req.GetResponse())
using (var reader = new StreamReader(resp.GetResponseStream(), Encoding.ASCII))
return reader.ReadToEnd();
}
/// <summary>Upload string content to Sinumerik (download from CNC perspective).</summary>
public void UploadFile(string ncPath, string content)
{
var req = CreateRequest(ncPath, WebRequestMethods.Ftp.UploadFile);
byte[] data = Encoding.ASCII.GetBytes(content);
req.ContentLength = data.Length;
using (var stream = req.GetRequestStream())
stream.Write(data, 0, data.Length);
using ((FtpWebResponse)req.GetResponse()) { }
}
public void DeleteFile(string ncPath)
{
var req = CreateRequest(ncPath, WebRequestMethods.Ftp.DeleteFile);
using ((FtpWebResponse)req.GetResponse()) { }
}
private FtpWebRequest CreateRequest(string path, string method)
{
string uri = string.Format("ftp://{0}:{1}{2}", _config.IpAddress, _config.Port, path);
var req = (FtpWebRequest)WebRequest.Create(uri);
req.Method = method;
req.Credentials = new NetworkCredential(_config.Username, _config.Password);
req.Timeout = (int)_config.RequestTimeout.TotalMilliseconds;
req.KeepAlive = false;
req.UseBinary = false;
req.UsePassive = true;
return req;
}
}
}

View file

@ -0,0 +1,211 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using FanucProgramManager.Cnc;
using FanucProgramManager.Cnc.Models;
namespace FanucProgramManager.Cnc.Siemens
{
public sealed class SiemensMachine : ICncMachine
{
private readonly SiemensConnectionConfig _config;
private readonly ISiemensFtpClient _ftp;
private readonly INcProgramValidator _validator;
private readonly List<CncError> _errors = new List<CncError>();
private readonly SemaphoreSlim _gate = new SemaphoreSlim(1, 1);
private ConnectionState _state = ConnectionState.Disconnected;
public SiemensMachine(SiemensConnectionConfig config)
{
_config = config ?? throw new ArgumentNullException("config");
_ftp = new SiemensFtpClient(config);
_validator = new NcProgramValidator(CncManufacturer.Siemens);
}
internal SiemensMachine(SiemensConnectionConfig config, ISiemensFtpClient ftpClient,
INcProgramValidator validator = null)
{
_config = config ?? throw new ArgumentNullException("config");
_ftp = ftpClient ?? throw new ArgumentNullException("ftpClient");
_validator = validator ?? new NcProgramValidator(CncManufacturer.Siemens);
}
public string Name { get { return _config.Name; } }
public ConnectionState State { get { return _state; } }
public event EventHandler<ConnectionState> StateChanged;
public Task<CncResult<bool>> ConnectAsync(CancellationToken ct = default(CancellationToken))
{
return RunGuardedAsync(() =>
{
SetState(ConnectionState.Connecting);
if (!_ftp.Ping())
{
SetState(ConnectionState.Faulted);
_errors.Add(new CncError(0, "Connect",
string.Format("FTP server unreachable at {0}:{1}", _config.IpAddress, _config.Port)));
return false;
}
SetState(ConnectionState.Connected);
return true;
}, ct);
}
public Task DisconnectAsync()
{
return RunGuardedAsync<bool>(() =>
{
SetState(ConnectionState.Disconnected);
return true;
}, CancellationToken.None);
}
public Task<CncResult<string>> ReadProgramAsync(string path, CancellationToken ct = default(CancellationToken))
{
return RunGuardedAsync(() =>
{
EnsureConnected();
string ncPath = ResolvePath(path, null);
try { return _ftp.DownloadFile(ncPath); }
catch (Exception ex)
{
_errors.Add(new CncError(0, "ReadProgram", ex.Message));
return null;
}
}, ct);
}
public Task<CncResult<bool>> WriteProgramAsync(string path, CncProgram program, CancellationToken ct = default(CancellationToken))
{
return RunGuardedAsync(() =>
{
EnsureConnected();
if (program == null || string.IsNullOrEmpty(program.Content))
{
_errors.Add(new CncError(0, "WriteProgram", "Content is empty"));
return false;
}
var vr = _validator.Validate(program.Name ?? path, program.Content);
if (!vr.Success)
{
string msg = string.Join("; ", vr.Errors
.Where(e => e.Severity == ValidationSeverity.Error)
.Select(e => e.Message));
_errors.Add(new CncError(0, "Validator", msg));
return false;
}
string ncPath = ResolvePath(path, program.Name);
try { _ftp.UploadFile(ncPath, program.Content); return true; }
catch (Exception ex)
{
_errors.Add(new CncError(0, "WriteProgram", ex.Message));
return false;
}
}, ct);
}
public Task<CncResult<bool>> DeleteProgramAsync(string path, CancellationToken ct = default(CancellationToken))
{
return RunGuardedAsync(() =>
{
EnsureConnected();
try { _ftp.DeleteFile(path); return true; }
catch (Exception ex)
{
_errors.Add(new CncError(0, "DeleteProgram", ex.Message));
return false;
}
}, ct);
}
public Task<CncResult<MachineSnapshot>> ReadAsync(CancellationToken ct = default(CancellationToken))
{
return RunGuardedAsync(() =>
{
EnsureConnected();
return new MachineSnapshot
{
CapturedAt = DateTime.Now,
Mode = MachineMode.Unknown,
Run = RunState.Unknown,
Motion = MotionState.Unknown,
Alarms = new List<string>(),
OperatorMessages = new List<string>()
};
}, ct);
}
public Task<CncResult<string>> ReadActiveProgramAsync(CancellationToken ct = default(CancellationToken))
{
return Task.FromResult(CncResult<string>.Fail("ReadActiveProgram", "Not supported via FTP"));
}
public Task<CncResult<bool>> SelectMainProgramAsync(string path, CancellationToken ct = default(CancellationToken))
{
return Task.FromResult(CncResult<bool>.Fail("SelectMainProgram", "Not supported via FTP"));
}
public void Dispose()
{
try { DisconnectAsync().GetAwaiter().GetResult(); } catch { }
try { _gate.Dispose(); } catch { }
}
private async Task<CncResult<T>> RunGuardedAsync<T>(Func<T> body, CancellationToken ct)
{
await _gate.WaitAsync(ct).ConfigureAwait(false);
try
{
return await Task.Run(() =>
{
_errors.Clear();
try
{
T value = body();
return _errors.Count > 0
? CncResult<T>.Fail(_errors.ToArray())
: CncResult<T>.Ok(value);
}
catch (OperationCanceledException) { return CncResult<T>.Cancelled(); }
catch (Exception ex)
{
_errors.Add(new CncError(0, "SiemensMachine", ex.Message));
return CncResult<T>.Fail(_errors.ToArray());
}
}, ct).ConfigureAwait(false);
}
finally { _gate.Release(); }
}
private void EnsureConnected()
{
if (_state != ConnectionState.Connected)
throw new InvalidOperationException("SiemensMachine is not connected.");
}
private void SetState(ConnectionState next)
{
if (_state == next) return;
_state = next;
var h = StateChanged;
if (h != null)
{
try { h(this, next); } catch { }
}
}
/// <summary>Build full FTP path. If name given, appends it with .MPF extension.</summary>
private string ResolvePath(string folder, string name)
{
if (string.IsNullOrEmpty(name))
return folder.Contains("/") ? folder : _config.ProgramDirectory + folder + ".MPF";
string dir = folder ?? _config.ProgramDirectory;
if (!dir.EndsWith("/")) dir += "/";
string ext = name.Contains(".") ? "" : ".MPF";
return dir + name + ext;
}
}
}

37
Dockerfile.test Normal file
View file

@ -0,0 +1,37 @@
FROM mono:6.12
WORKDIR /build
# Sync Mozilla root certs into Mono trust store — required for NuGet HTTPS on Buster
RUN cert-sync /etc/ssl/certs/ca-certificates.crt
# Copy solution + projects first for layer caching
COPY FanucProgramManager.csproj ./
COPY packages.config ./
COPY FanucProgramManager.Tests/FanucProgramManager.Tests.csproj ./FanucProgramManager.Tests/
COPY FanucProgramManager.Tests/packages.config ./FanucProgramManager.Tests/
COPY FanucProgramManager.sln ./
# Restore all packages (sln now includes test project)
RUN nuget restore FanucProgramManager.sln -NonInteractive
# Explicit fallback: restore test packages.config directly to same packages dir
RUN nuget restore FanucProgramManager.Tests/packages.config \
-OutputDirectory packages -NonInteractive
# Grab NUnit console runner
RUN nuget install NUnit.ConsoleRunner -Version 3.16.3 -OutputDirectory packages -NonInteractive
# Copy source
COPY . .
# Make libs dir explicit (native Win32 DLLs — needed by csproj Content entries, not executed on Linux)
RUN ls Cnc/Fanuc/libs/ || true
# Build test project (Debug)
RUN msbuild FanucProgramManager.Tests/FanucProgramManager.Tests.csproj \
/p:Configuration=Debug /p:Platform=AnyCPU \
/v:minimal /nologo
# Run tests — skip Fanuc FOCAS tests (marked [Ignore] — need fwlib32.dll)
CMD mono packages/NUnit.ConsoleRunner.3.16.3/tools/nunit3-console.exe \
FanucProgramManager.Tests/bin/Debug/FanucProgramManager.Tests.dll \
--labels=All

1094
FANUCMachine.cs Executable file

File diff suppressed because it is too large Load diff

192
FanucProgram.cs Executable file
View file

@ -0,0 +1,192 @@
using System;
using System.Text.RegularExpressions;
namespace FanucProgramManager
{
internal class FanucProgram
{
private static Regex titleRegex = new Regex(@"(\b[O]+\d{4}\b)|(\b\d{4}\b)"); //cerca per O#### o #### dove # è un carattere numerico
private static Regex oldTitleRegex = new Regex(@"\<((\b[O]+\d{4}\b)|(\b\d{4}\b))\>");//cerca per <O####> o #### dove # è un carattere numerico
private static Regex titleAndCommentRegex = new Regex(@"((\b[O]+\d{4}\b)|(\b\d{4}\b))+(\n|(\s*\((.*?)\)))");
private static Regex commentRegex = new Regex(@"\((.*?)\)");
private const string programStringName = "program:";
private const string toolOffsetDataName = "tooloffsetdata:";
private const string workZeroOffsetDataName = "workzerooffset:";
private static Regex programRegex = new Regex(@"program:\{\n((?:.*?|\n)*?)\}");
private static Regex toolOffsetDataRegex = new Regex(@"tooloffsetdata:\{\n((?:.*?|\n)*?)\}");
private static Regex workZeroOffsetDataRegex = new Regex(@"workzerooffset:\{\n((?:.*?|\n)*?)\}");
public String Program;
public String ToolOffsetData;
public String WorkZeroOffsetData;
public void setValue(String value, FILE_TYPE type)
{
switch (type)
{
case FILE_TYPE.CNC_PROGRAM: Program = value; break;
case FILE_TYPE.TOOL_OFFEST_DATA: ToolOffsetData = value; break;
case FILE_TYPE.WORK_ZERO_OFFSET: WorkZeroOffsetData = value; break;
}
Standardize();
}
public enum FILE_TYPE : short
{
CNC_PROGRAM = 0,
TOOL_OFFEST_DATA = 1,
WORK_ZERO_OFFSET = 5
}
public FanucProgram(string _program)
{
var programMatch = programRegex.Match(_program);
if (programMatch.Success)
{
Program = programMatch.Value.Substring(9);
Program = Program.Trim('}');
}
else Program = null;
var toolOffsetDataMatch = toolOffsetDataRegex.Match(_program);
if (toolOffsetDataMatch.Success)
{
ToolOffsetData = toolOffsetDataMatch.Value.Substring(16);
ToolOffsetData = ToolOffsetData.Trim('}');
}
else ToolOffsetData = null;
var workZeroOffsetDataMatch = workZeroOffsetDataRegex.Match(_program);
if (workZeroOffsetDataMatch.Success)
{
WorkZeroOffsetData = workZeroOffsetDataMatch.Value.Substring(16);
WorkZeroOffsetData = WorkZeroOffsetData.Trim('}');
}
else WorkZeroOffsetData = null;
//Old program format with ONLY the program outside of brachets
if (!programMatch.Success && !toolOffsetDataMatch.Success && !workZeroOffsetDataMatch.Success)
{
Program = _program;
}
Standardize();
}
public string GetProgramTitle()
{
var match = titleRegex.Match(Program);
if (match.Success)
{
return match.Value;
}
throw new InvalidOperationException("Regex could not match any program title in the file, check program formatting for a O#### patern");
}
public static string GetProgramTitle(string program)
{
var match = titleRegex.Match(program);
if (match.Success)
{
return match.Value;
}
throw new InvalidOperationException("Regex could not match any program title in the file, check program formatting for a O#### patern");
}
public string GetProgramComment()
{
var groupMatch = titleAndCommentRegex.Match(Program);
if (groupMatch.Success)
{
Match match = commentRegex.Match(groupMatch.Value);
if (match.Success)
{
return match.Value.Trim(new Char[] { '(', ')' });
}
}
return "";
}
private void Standardize()
{
//removes % from start of file
int percent_index = Program.IndexOf('%');
if (percent_index == 0)
{
Program = Program.Remove(percent_index, 1);
}
//removes % from start of file
if (ToolOffsetData != null)
{
percent_index = ToolOffsetData.IndexOf('%');
if (percent_index == 0)
{
ToolOffsetData = ToolOffsetData.Remove(percent_index, 1);
}
}
//removes % from start of file
if (WorkZeroOffsetData != null)
{
percent_index = WorkZeroOffsetData.IndexOf('%');
if (percent_index == 0)
{
WorkZeroOffsetData = WorkZeroOffsetData.Remove(percent_index, 1);
}
}
//replace <O####> with O####
var oldTitleMatch = oldTitleRegex.Match(Program);
if (oldTitleMatch.Success)
{
Program = Program.Replace(oldTitleMatch.Value, GetProgramTitle());
}
//adds \n to file beginning if missing
int new_line_index = Program.IndexOf('\n');
if (new_line_index != 0)
{
Program = Program.Substring(Program.IndexOf(GetProgramTitle()));
Program = Program.Insert(0, "\n");
}
}
//fornendo un programma CNC e un commento in INPUT restituisce un output nel formato corretto per il caricamento con la funzione download
public void ChangeComment(string comment)
{
string previousComment = GetProgramComment();
if (previousComment != "")
Program = Program.Replace(previousComment, "(" + comment + ")");
else
{
Program = Program.Replace(GetProgramTitle(), GetProgramTitle() + "(" + comment + ")");
}
}
public void ChangeProgName(string progName)
{
var match = titleRegex.Match(progName);
if (!match.Success)
{
throw new InvalidOperationException("New Program Name does Not respect the proper formattings O#### patern");
}
Program.Replace(GetProgramTitle(), progName);
}
public void ChangeNameAndComment(string progName, string comment)
{
ChangeProgName(progName);
ChangeComment(comment);
}
public override string ToString()
{
return programStringName + "{\n" + Program + "\n}\n" +
toolOffsetDataName + "{\n" + ToolOffsetData + "\n}\n" +
workZeroOffsetDataName + "{\n" + WorkZeroOffsetData + "\n}\n";
}
}
}

View file

@ -0,0 +1,70 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props"
Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
<ProjectGuid>{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}</ProjectGuid>
<OutputType>Library</OutputType>
<RootNamespace>FanucProgramManager.Tests</RootNamespace>
<AssemblyName>FanucProgramManager.Tests</AssemblyName>
<TargetFrameworkVersion>v4.7.2</TargetFrameworkVersion>
<FileAlignment>512</FileAlignment>
<Deterministic>true</Deterministic>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<PlatformTarget>AnyCPU</PlatformTarget>
<DebugSymbols>true</DebugSymbols>
<DebugType>full</DebugType>
<Optimize>false</Optimize>
<OutputPath>bin\Debug\</OutputPath>
<DefineConstants>DEBUG;TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<PlatformTarget>AnyCPU</PlatformTarget>
<DebugType>pdbonly</DebugType>
<Optimize>true</Optimize>
<OutputPath>bin\Release\</OutputPath>
<DefineConstants>TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<ItemGroup>
<Reference Include="System" />
<Reference Include="System.Net" />
<Reference Include="nunit.framework">
<HintPath>..\packages\NUnit.3.13.3\lib\net45\nunit.framework.dll</HintPath>
</Reference>
<Reference Include="Moq">
<HintPath>..\packages\Moq.4.18.4\lib\net462\Moq.dll</HintPath>
</Reference>
<Reference Include="Castle.Core">
<HintPath>..\packages\Castle.Core.5.1.1\lib\net462\Castle.Core.dll</HintPath>
</Reference>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\FanucProgramManager.csproj">
<Project>{1032188B-3321-42B2-87ED-343BF718CAF5}</Project>
<Name>FanucProgramManager</Name>
</ProjectReference>
</ItemGroup>
<ItemGroup>
<Compile Include="Unit\NcProgramValidatorTests.cs" />
<Compile Include="Unit\FanucProgramParsingTests.cs" />
<Compile Include="Unit\IsoProgramFormatterTests.cs" />
<Compile Include="Unit\CncMachineFactoryTests.cs" />
<Compile Include="Unit\FanucMachineTests.cs" />
<Compile Include="Unit\SiemensMachineTests.cs" />
<Compile Include="Unit\MitsubishiMachineTests.cs" />
<Compile Include="Unit\HeidenhainMachineTests.cs" />
<Compile Include="Integration\Stubs\FtpServerStub.cs" />
<Compile Include="Integration\Stubs\Lsv2ServerStub.cs" />
<Compile Include="Integration\SiemensMachineIntegrationTests.cs" />
<Compile Include="Integration\MitsubishiMachineIntegrationTests.cs" />
<Compile Include="Integration\HeidenhainMachineIntegrationTests.cs" />
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
</Project>

View file

@ -0,0 +1,80 @@
using System;
using NUnit.Framework;
using FanucProgramManager.Cnc.Models;
using FanucProgramManager.Cnc.Heidenhain;
using FanucProgramManager.Tests.Integration.Stubs;
namespace FanucProgramManager.Tests.Integration
{
[TestFixture]
[Category("Integration")]
public class HeidenhainMachineIntegrationTests
{
private Lsv2ServerStub _stub;
private HeidenhainMachine _machine;
[SetUp]
public void SetUp()
{
_stub = new Lsv2ServerStub();
var config = new HeidenhainConnectionConfig
{
Name = "HeidenhainIntegration",
IpAddress = "127.0.0.1",
Port = _stub.Port,
Username = "user",
Password = "pass",
ConnectTimeout = TimeSpan.FromSeconds(5)
};
_machine = new HeidenhainMachine(config);
}
[TearDown]
public void TearDown()
{
_machine?.Dispose();
_stub?.Dispose();
}
[Test]
public void ConnectAsync_StubHandlesLoginHandshake_ResultOk()
{
var result = _machine.ConnectAsync().GetAwaiter().GetResult();
Assert.That(result.Success, Is.True);
Assert.That(_machine.State, Is.EqualTo(ConnectionState.Connected));
}
[Test]
public void WriteThenRead_ProgramRoundTrips()
{
_machine.ConnectAsync().GetAwaiter().GetResult();
// Heidenhain: name must end with .H; no % or ; forbidden chars; no lowercase; no O#### required
string content = "L X+0 Y+0 R0 FMAX\nM30";
var prog = new CncProgram { Name = "O1234.H", Content = content };
string folder = "TNC:/nc_prog/";
var writeResult = _machine.WriteProgramAsync(folder, prog).GetAwaiter().GetResult();
Assert.That(writeResult.Success, Is.True);
// HeidenhainMachine appends .H only if name has no extension — name already has .H
string storedPath = folder + "O1234.H";
Assert.That(_stub.Files.ContainsKey(storedPath), Is.True,
"File should be in stub at " + storedPath);
var readResult = _machine.ReadProgramAsync(storedPath).GetAwaiter().GetResult();
Assert.That(readResult.Success, Is.True);
Assert.That(readResult.Value, Is.EqualTo(content));
}
[Test]
public void ReadProgramAsync_UnknownPath_ResultIsFail()
{
_machine.ConnectAsync().GetAwaiter().GetResult();
var result = _machine.ReadProgramAsync("TNC:/nc_prog/DOESNOTEXIST.H").GetAwaiter().GetResult();
Assert.That(result.Success, Is.False);
}
}
}

View file

@ -0,0 +1,96 @@
using System;
using NUnit.Framework;
using FanucProgramManager.Cnc.Models;
using FanucProgramManager.Cnc.Mitsubishi;
using FanucProgramManager.Tests.Integration.Stubs;
namespace FanucProgramManager.Tests.Integration
{
[TestFixture]
[Category("Integration")]
public class MitsubishiMachineIntegrationTests
{
private FtpServerStub _stub;
private MitsubishiMachine _machine;
private const string ProgramDir = "/PRG/";
[SetUp]
public void SetUp()
{
_stub = new FtpServerStub();
var config = new MitsubishiConnectionConfig
{
Name = "MitsubishiIntegration",
IpAddress = "127.0.0.1",
Port = _stub.Port,
Username = "user",
Password = "pass",
ProgramDirectory = ProgramDir,
RequestTimeout = TimeSpan.FromSeconds(5)
};
_machine = new MitsubishiMachine(config);
}
[TearDown]
public void TearDown()
{
_machine?.Dispose();
_stub?.Dispose();
}
[Test]
public void ConnectAsync_StubListens_ResultOk()
{
var result = _machine.ConnectAsync().GetAwaiter().GetResult();
Assert.That(result.Success, Is.True);
Assert.That(_machine.State, Is.EqualTo(ConnectionState.Connected));
}
[Test]
public void WriteThenRead_ProgramRoundTripsNoExtension()
{
_machine.ConnectAsync().GetAwaiter().GetResult();
// Mitsubishi: name O\d{4}; first line must be O####; no semicolons/brackets; no lowercase
string content = "O1234\nG0 X0 Y0\nM30";
var prog = new CncProgram { Name = "O1234", Content = content };
var writeResult = _machine.WriteProgramAsync(ProgramDir, prog).GetAwaiter().GetResult();
Assert.That(writeResult.Success, Is.True);
// Mitsubishi: no extension
string storedPath = ProgramDir + "O1234";
Assert.That(_stub.Files.ContainsKey(storedPath), Is.True,
"File should be stored without extension at " + storedPath);
Assert.That(_stub.Files.ContainsKey(storedPath + ".MPF"), Is.False,
"File must NOT be stored with .MPF extension");
var readResult = _machine.ReadProgramAsync(storedPath).GetAwaiter().GetResult();
Assert.That(readResult.Success, Is.True);
Assert.That(readResult.Value, Is.EqualTo(content));
}
[Test]
public void DeleteProgramAsync_FileExistsInStub_RemovedFromStub()
{
_machine.ConnectAsync().GetAwaiter().GetResult();
string path = ProgramDir + "O5678";
_stub.Files[path] = "SOME_CONTENT";
var result = _machine.DeleteProgramAsync(path).GetAwaiter().GetResult();
Assert.That(result.Success, Is.True);
Assert.That(_stub.Files.ContainsKey(path), Is.False);
}
[Test]
public void ReadProgramAsync_FileNotInStub_ResultFail()
{
_machine.ConnectAsync().GetAwaiter().GetResult();
var result = _machine.ReadProgramAsync(ProgramDir + "O9999").GetAwaiter().GetResult();
Assert.That(result.Success, Is.False);
}
}
}

View file

@ -0,0 +1,104 @@
using System;
using NUnit.Framework;
using FanucProgramManager.Cnc.Models;
using FanucProgramManager.Cnc.Siemens;
using FanucProgramManager.Tests.Integration.Stubs;
namespace FanucProgramManager.Tests.Integration
{
[TestFixture]
[Category("Integration")]
public class SiemensMachineIntegrationTests
{
private FtpServerStub _stub;
private SiemensMachine _machine;
private const string ProgramDir = "/_N_MPF_DIR/";
[SetUp]
public void SetUp()
{
_stub = new FtpServerStub();
var config = new SiemensConnectionConfig
{
Name = "SiemensIntegration",
IpAddress = "127.0.0.1",
Port = _stub.Port,
Username = "user",
Password = "pass",
ProgramDirectory = ProgramDir,
RequestTimeout = TimeSpan.FromSeconds(5)
};
_machine = new SiemensMachine(config);
}
[TearDown]
public void TearDown()
{
_machine?.Dispose();
_stub?.Dispose();
}
[Test]
public void ConnectAsync_StubListens_ResultOk()
{
var result = _machine.ConnectAsync().GetAwaiter().GetResult();
Assert.That(result.Success, Is.True);
Assert.That(_machine.State, Is.EqualTo(ConnectionState.Connected));
}
[Test]
public void WriteThenRead_ProgramRoundTrips()
{
_machine.ConnectAsync().GetAwaiter().GetResult();
// Siemens: name must end with .MPF; no semicolons; no lowercase
string content = "G0 X0 Y0\nM30";
var prog = new CncProgram { Name = "O1234.MPF", Content = content };
var writeResult = _machine.WriteProgramAsync(ProgramDir, prog).GetAwaiter().GetResult();
Assert.That(writeResult.Success, Is.True);
// Determine where SiemensMachine stored the file
string storedPath = ProgramDir + "O1234.MPF";
var readResult = _machine.ReadProgramAsync(storedPath).GetAwaiter().GetResult();
Assert.That(readResult.Success, Is.True);
Assert.That(readResult.Value, Is.EqualTo(content));
}
[Test]
public void DeleteProgramAsync_FileExistsInStub_RemovedFromStub()
{
_machine.ConnectAsync().GetAwaiter().GetResult();
string path = ProgramDir + "O5678.MPF";
_stub.Files[path] = "SOME_CONTENT";
var result = _machine.DeleteProgramAsync(path).GetAwaiter().GetResult();
Assert.That(result.Success, Is.True);
Assert.That(_stub.Files.ContainsKey(path), Is.False);
}
[Test]
public void ReadProgramAsync_FileNotInStub_ResultFail()
{
_machine.ConnectAsync().GetAwaiter().GetResult();
var result = _machine.ReadProgramAsync(ProgramDir + "DOESNOTEXIST.MPF").GetAwaiter().GetResult();
Assert.That(result.Success, Is.False);
}
[Test]
public void WriteProgramAsync_InvalidName_ValidationFails()
{
_machine.ConnectAsync().GetAwaiter().GetResult();
// Siemens requires .MPF extension — validator rejects before any FTP call
var prog = new CncProgram { Name = "INVALID", Content = "G00 X10\nM30" };
var result = _machine.WriteProgramAsync(ProgramDir, prog).GetAwaiter().GetResult();
Assert.That(result.Success, Is.False);
}
}
}

View file

@ -0,0 +1,255 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;
namespace FanucProgramManager.Tests.Integration.Stubs
{
/// <summary>
/// Minimal in-process FTP server using passive mode.
/// Supports USER, PASS, SYST, PWD, CWD, TYPE, PASV, LIST, NLST, RETR, STOR, DELE, QUIT.
/// Also accepts OPTS, FEAT, NOOP, CLNT, MODE, STRU as no-ops.
/// All replies use CRLF (per RFC 959), independent of host platform.
/// </summary>
public sealed class FtpServerStub : IDisposable
{
private const string CRLF = "\r\n";
private readonly TcpListener _listener;
private readonly Thread _acceptThread;
private volatile bool _running;
public int Port { get; }
/// <summary>In-memory file store: full path → content.</summary>
public Dictionary<string, string> Files { get; } = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
public FtpServerStub()
{
_listener = new TcpListener(IPAddress.Loopback, 0);
_listener.Start();
Port = ((IPEndPoint)_listener.LocalEndpoint).Port;
_running = true;
_acceptThread = new Thread(AcceptLoop) { IsBackground = true, Name = "FtpStub-Accept" };
_acceptThread.Start();
}
public void Dispose()
{
_running = false;
try { _listener.Stop(); } catch { }
}
private void AcceptLoop()
{
while (_running)
{
TcpClient client;
try { client = _listener.AcceptTcpClient(); }
catch { break; }
var t = new Thread(() => HandleClient(client)) { IsBackground = true, Name = "FtpStub-Client" };
t.Start();
}
}
private static void SendLine(Stream stream, string line)
{
byte[] bytes = Encoding.ASCII.GetBytes(line + CRLF);
stream.Write(bytes, 0, bytes.Length);
stream.Flush();
}
private static string ReadLine(Stream stream)
{
var sb = new StringBuilder();
int prev = -1;
while (true)
{
int b = stream.ReadByte();
if (b < 0) return sb.Length == 0 ? null : sb.ToString();
if (b == '\n')
{
if (prev == '\r')
sb.Length = sb.Length - 1; // strip CR
return sb.ToString();
}
sb.Append((char)b);
prev = b;
}
}
private void HandleClient(TcpClient client)
{
using (client)
using (var stream = client.GetStream())
{
string cwd = "/";
TcpListener dataListener = null;
SendLine(stream, "220 FtpServerStub ready");
string line;
while ((line = ReadLine(stream)) != null)
{
line = line.Trim();
if (string.IsNullOrEmpty(line)) continue;
string cmd, arg;
int sp = line.IndexOf(' ');
if (sp >= 0)
{
cmd = line.Substring(0, sp).ToUpperInvariant();
arg = line.Substring(sp + 1).Trim();
}
else
{
cmd = line.ToUpperInvariant();
arg = "";
}
switch (cmd)
{
case "USER":
SendLine(stream, "331 Password required");
break;
case "PASS":
SendLine(stream, "230 Logged in");
break;
case "SYST":
SendLine(stream, "215 UNIX Type: L8");
break;
case "FEAT":
SendLine(stream, "211-Features:");
SendLine(stream, " UTF8");
SendLine(stream, "211 End");
break;
case "OPTS":
case "CLNT":
case "NOOP":
case "MODE":
case "STRU":
case "ALLO":
SendLine(stream, "200 OK");
break;
case "PWD":
SendLine(stream, "257 \"" + cwd + "\"");
break;
case "CWD":
cwd = arg.StartsWith("/") ? arg : cwd.TrimEnd('/') + "/" + arg;
if (!cwd.EndsWith("/")) cwd += "/";
SendLine(stream, "250 CWD successful");
break;
case "TYPE":
SendLine(stream, "200 Type set");
break;
case "PASV":
dataListener = new TcpListener(IPAddress.Loopback, 0);
dataListener.Start();
int dataPort = ((IPEndPoint)dataListener.LocalEndpoint).Port;
int p1 = dataPort >> 8;
int p2 = dataPort & 0xFF;
SendLine(stream, "227 Entering Passive Mode (127,0,0,1," + p1 + "," + p2 + ")");
break;
case "LIST":
case "NLST":
if (dataListener == null) { SendLine(stream, "425 No data connection"); break; }
SendLine(stream, "150 Opening data connection");
try
{
using (var dc = dataListener.AcceptTcpClient())
using (var ds = dc.GetStream())
{
// Empty listing — enough for Ping()
}
}
catch { }
try { dataListener.Stop(); } catch { }
dataListener = null;
SendLine(stream, "226 Transfer complete");
break;
case "RETR":
if (dataListener == null) { SendLine(stream, "425 No data connection"); break; }
string retrPath = NormalizePath(cwd, arg);
string retrContent;
if (!Files.TryGetValue(retrPath, out retrContent))
{
try { dataListener.Stop(); } catch { }
dataListener = null;
SendLine(stream, "550 File not found");
break;
}
SendLine(stream, "150 Opening data connection");
try
{
using (var dc = dataListener.AcceptTcpClient())
using (var ds = dc.GetStream())
{
byte[] bytes = Encoding.ASCII.GetBytes(retrContent);
ds.Write(bytes, 0, bytes.Length);
}
}
catch { }
try { dataListener.Stop(); } catch { }
dataListener = null;
SendLine(stream, "226 Transfer complete");
break;
case "STOR":
if (dataListener == null) { SendLine(stream, "425 No data connection"); break; }
string storPath = NormalizePath(cwd, arg);
SendLine(stream, "150 Opening data connection");
var sb = new StringBuilder();
try
{
using (var dc = dataListener.AcceptTcpClient())
using (var ds = dc.GetStream())
using (var dr = new StreamReader(ds, Encoding.ASCII))
{
sb.Append(dr.ReadToEnd());
}
}
catch { }
Files[storPath] = sb.ToString();
try { dataListener.Stop(); } catch { }
dataListener = null;
SendLine(stream, "226 Transfer complete");
break;
case "DELE":
string delePath = NormalizePath(cwd, arg);
Files.Remove(delePath);
SendLine(stream, "250 DELE successful");
break;
case "QUIT":
SendLine(stream, "221 Goodbye");
return;
default:
SendLine(stream, "500 Unknown command");
break;
}
}
}
}
private static string NormalizePath(string cwd, string arg)
{
if (arg.StartsWith("/")) return arg;
return cwd.TrimEnd('/') + "/" + arg;
}
}
}

View file

@ -0,0 +1,212 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;
namespace FanucProgramManager.Tests.Integration.Stubs
{
/// <summary>
/// Minimal in-process LSV2 server.
/// Frame format: [0x02][SIZE_HI][SIZE_LO][CMD_HI][CMD_LO][data...][CRC_HI][CRC_LO]
/// SIZE = len(CMD) + len(data) = 2 + dataLen
/// </summary>
public sealed class Lsv2ServerStub : IDisposable
{
private const ushort T_OK = 0x0000;
private const ushort T_SE = 0x0001;
private const ushort T_LS_SELECT = 0x00A0;
private const ushort T_LS_LOG_IN = 0x00A1;
private const ushort T_LS_LOG_OUT = 0x00A2;
private const ushort T_R_FL = 0x0023;
private const ushort T_S_FL = 0x0024;
private const ushort T_FD = 0x0025;
private readonly TcpListener _listener;
private readonly Thread _acceptThread;
private volatile bool _running;
public int Port { get; }
/// <summary>In-memory file store: full path → content.</summary>
public Dictionary<string, string> Files { get; } = new Dictionary<string, string>(StringComparer.Ordinal);
public Lsv2ServerStub()
{
_listener = new TcpListener(IPAddress.Loopback, 0);
_listener.Start();
Port = ((IPEndPoint)_listener.LocalEndpoint).Port;
_running = true;
_acceptThread = new Thread(AcceptLoop) { IsBackground = true, Name = "Lsv2Stub-Accept" };
_acceptThread.Start();
}
public void Dispose()
{
_running = false;
try { _listener.Stop(); } catch { }
}
private void AcceptLoop()
{
while (_running)
{
TcpClient client;
try { client = _listener.AcceptTcpClient(); }
catch { break; }
var t = new Thread(() => HandleClient(client)) { IsBackground = true, Name = "Lsv2Stub-Client" };
t.Start();
}
}
private void HandleClient(TcpClient client)
{
using (client)
using (var stream = client.GetStream())
{
while (true)
{
ushort cmd;
byte[] data = ReadFrame(stream, out cmd);
if (data == null) break;
switch (cmd)
{
case T_LS_SELECT:
case T_LS_LOG_IN:
SendFrame(stream, T_OK, new byte[0]);
break;
case T_LS_LOG_OUT:
SendFrame(stream, T_OK, new byte[0]);
return;
case T_R_FL:
{
// data = path\0
string path = ExtractString(data, 0);
string content;
if (!Files.TryGetValue(path, out content))
{
SendFrame(stream, T_SE, new byte[0]);
break;
}
byte[] contentBytes = Encoding.ASCII.GetBytes(content);
SendFrame(stream, T_FD, contentBytes);
SendFrame(stream, T_OK, new byte[0]);
break;
}
case T_S_FL:
{
// data = path\0 + content
string path = ExtractString(data, 0);
int pathLen = Encoding.ASCII.GetByteCount(path) + 1; // +1 for \0
int contentLen = data.Length - pathLen;
string content = contentLen > 0
? Encoding.ASCII.GetString(data, pathLen, contentLen)
: "";
Files[path] = content;
SendFrame(stream, T_OK, new byte[0]);
break;
}
default:
SendFrame(stream, T_SE, new byte[0]);
break;
}
}
}
}
private static byte[] ReadFrame(Stream stream, out ushort cmd)
{
cmd = 0xFFFF;
int stx = stream.ReadByte();
if (stx != 0x02) return null;
int sizeHi = stream.ReadByte();
int sizeLo = stream.ReadByte();
if (sizeHi < 0 || sizeLo < 0) return null;
int size = (sizeHi << 8) | sizeLo;
if (size < 2) return null;
int cmdHi = stream.ReadByte();
int cmdLo = stream.ReadByte();
if (cmdHi < 0 || cmdLo < 0) return null;
cmd = (ushort)((cmdHi << 8) | cmdLo);
int dataLen = size - 2;
byte[] data = new byte[dataLen];
int read = 0;
while (read < dataLen)
{
int n = stream.Read(data, read, dataLen - read);
if (n == 0) return null;
read += n;
}
// consume CRC
stream.ReadByte();
stream.ReadByte();
return data;
}
private static void SendFrame(Stream stream, ushort cmdCode, byte[] data)
{
int dataLen = data.Length;
ushort size = (ushort)(2 + dataLen);
byte sizeHi = (byte)(size >> 8);
byte sizeLo = (byte)(size & 0xFF);
byte cmdHi = (byte)(cmdCode >> 8);
byte cmdLo = (byte)(cmdCode & 0xFF);
byte[] crcInput = new byte[4 + dataLen];
crcInput[0] = sizeHi; crcInput[1] = sizeLo;
crcInput[2] = cmdHi; crcInput[3] = cmdLo;
Buffer.BlockCopy(data, 0, crcInput, 4, dataLen);
ushort crc = Crc16(crcInput);
byte[] frame = new byte[1 + 4 + dataLen + 2];
int i = 0;
frame[i++] = 0x02;
frame[i++] = sizeHi;
frame[i++] = sizeLo;
frame[i++] = cmdHi;
frame[i++] = cmdLo;
Buffer.BlockCopy(data, 0, frame, i, dataLen);
i += dataLen;
frame[i++] = (byte)(crc >> 8);
frame[i] = (byte)(crc & 0xFF);
stream.Write(frame, 0, frame.Length);
stream.Flush();
}
private static string ExtractString(byte[] data, int offset)
{
int end = offset;
while (end < data.Length && data[end] != 0) end++;
return Encoding.ASCII.GetString(data, offset, end - offset);
}
// CRC16 CCITT — same poly/init as Lsv2Client
private static ushort Crc16(byte[] data)
{
ushort crc = 0xFFFF;
foreach (byte b in data)
{
crc ^= (ushort)(b << 8);
for (int i = 0; i < 8; i++)
crc = (crc & 0x8000) != 0
? (ushort)((crc << 1) ^ 0x1021)
: (ushort)(crc << 1);
}
return crc;
}
}
}

View file

@ -0,0 +1,113 @@
using NUnit.Framework;
using FanucProgramManager.Cnc;
using FanucProgramManager.Cnc.Fanuc;
using FanucProgramManager.Cnc.Heidenhain;
using FanucProgramManager.Cnc.Mitsubishi;
using FanucProgramManager.Cnc.Models;
using FanucProgramManager.Cnc.Siemens;
namespace FanucProgramManager.Tests.Unit
{
[TestFixture]
public class CncMachineFactoryTests
{
private static T GetRequiredField<T>(object target, string name)
{
var f = target.GetType().GetField(name,
System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance)
?? throw new InvalidOperationException(
string.Format("Field '{0}' not found on {1}. Field may have been renamed.", name, target.GetType().Name));
return (T)f.GetValue(target);
}
private InputArgs BaseArgs()
{
return new InputArgs
{
ip = "192.168.1.1",
porta = "",
username = "user",
password = "pass",
nodoHssb = -1
};
}
[Test]
public void Create_Fanuc_ReturnsFanucMachine()
{
var args = BaseArgs();
args.manufacturer = CncManufacturer.Fanuc;
using (var m = CncMachineFactory.Create(args))
{
Assert.That(m, Is.InstanceOf<FanucMachine>());
}
}
[Test]
public void Create_Heidenhain_ReturnsHeidenhainMachine()
{
var args = BaseArgs();
args.manufacturer = CncManufacturer.Heidenhain;
using (var m = CncMachineFactory.Create(args))
{
Assert.That(m, Is.InstanceOf<HeidenhainMachine>());
}
}
[Test]
public void Create_Siemens_ReturnsSiemensMachine()
{
var args = BaseArgs();
args.manufacturer = CncManufacturer.Siemens;
using (var m = CncMachineFactory.Create(args))
{
Assert.That(m, Is.InstanceOf<SiemensMachine>());
}
}
[Test]
public void Create_Mitsubishi_ReturnsMitsubishiMachine()
{
var args = BaseArgs();
args.manufacturer = CncManufacturer.Mitsubishi;
using (var m = CncMachineFactory.Create(args))
{
Assert.That(m, Is.InstanceOf<MitsubishiMachine>());
}
}
[Test]
public void Create_FanucHssbNode_TransportIsHssb()
{
var args = BaseArgs();
args.manufacturer = CncManufacturer.Fanuc;
args.nodoHssb = 0;
using (var m = CncMachineFactory.Create(args) as FanucMachine)
{
var config = GetRequiredField<FanucConnectionConfig>(m, "_config");
Assert.That(config.Transport, Is.EqualTo(FanucTransport.Hssb));
}
}
[Test]
public void Create_FanucNoHssb_TransportIsEthernet()
{
var args = BaseArgs();
args.manufacturer = CncManufacturer.Fanuc;
args.nodoHssb = -1;
using (var m = CncMachineFactory.Create(args) as FanucMachine)
{
var config = GetRequiredField<FanucConnectionConfig>(m, "_config");
Assert.That(config.Transport, Is.EqualTo(FanucTransport.Ethernet));
}
}
}
}

View file

@ -0,0 +1,221 @@
using System;
using NUnit.Framework;
using Moq;
using FanucProgramManager.Cnc.Fanuc;
using FanucProgramManager.Cnc.Models;
namespace FanucProgramManager.Tests.Unit
{
/// <summary>
/// FanucMachine calls real FOCAS DLL in ConnectCore and most methods guard
/// via EnsureConnected. Tests that require an active FOCAS handle are marked Ignore.
/// The tests here verify dialect call patterns by setting machine state via
/// reflection (bypass EnsureConnected) after injecting a mock dialect.
/// </summary>
[TestFixture]
public class FanucMachineTests
{
// Used in Returns() to modify the ref int len parameter — lambdas cannot have ref params.
private delegate short UploadDataDelegate(ushort hndl, ref int len, char[] data);
private FanucConnectionConfig EthernetConfig()
{
return new FanucConnectionConfig
{
Name = "TestFanuc",
Transport = FanucTransport.Ethernet,
IpAddress = "127.0.0.1",
Port = 8193,
ConnectTimeout = TimeSpan.FromSeconds(1)
};
}
private static T GetRequiredField<T>(object target, string name)
{
var f = target.GetType().GetField(name,
System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance)
?? throw new InvalidOperationException(
string.Format("Field '{0}' not found on {1}. Field may have been renamed.", name, target.GetType().Name));
return (T)f.GetValue(target);
}
private static void SetRequiredField<T>(object target, string name, T value)
{
var f = target.GetType().GetField(name,
System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance)
?? throw new InvalidOperationException(
string.Format("Field '{0}' not found on {1}. Field may have been renamed.", name, target.GetType().Name));
f.SetValue(target, value);
}
private FanucMachine CreateConnectedWithMockDialect(Mock<IFocasDialect> dialectMock)
{
var machine = new FanucMachine(EthernetConfig());
SetRequiredField(machine, "_state", ConnectionState.Connected);
SetRequiredField(machine, "_dialect", dialectMock.Object);
return machine;
}
[Test]
[Ignore("Requires Fanuc FOCAS DLL — no mock injection path for ConnectCore")]
public void ConnectAsync_FocasReturnsOk_ResultSuccess()
{
// Would need real FOCAS DLL or a way to intercept Focas1.cnc_allclibhndl3
}
[Test]
[Ignore("Requires Fanuc FOCAS DLL")]
public void ConnectAsync_FocasReturnsError_ResultFail()
{
// Same — Focas1 static calls cannot be intercepted without DLL
}
[Test]
// Moq default: ref int len stays 0 after UploadData call, loop exits via else-branch (len == 0).
// This test verifies call sequence only, not data content.
// See ReadProgramAsync_UploadDataReturnsContent_ContentInResult for data-reading coverage.
public void ReadProgramAsync_CallsUploadStartThenDataThenEnd()
{
var dialect = new Mock<IFocasDialect>();
dialect.Setup(d => d.UploadStart(It.IsAny<ushort>(), It.IsAny<string>(), It.IsAny<long>(), It.IsAny<short>()))
.Returns(Focas1.EW_OK);
dialect.Setup(d => d.UploadData(It.IsAny<ushort>(), ref It.Ref<int>.IsAny, It.IsAny<char[]>()))
.Returns(Focas1.EW_OK);
dialect.Setup(d => d.UploadEnd(It.IsAny<ushort>())).Returns(Focas1.EW_OK);
using (var machine = CreateConnectedWithMockDialect(dialect))
{
machine.ReadProgramAsync("O1234").GetAwaiter().GetResult();
}
dialect.Verify(d => d.UploadStart(It.IsAny<ushort>(), "O1234", It.IsAny<long>(), 0), Times.Once);
dialect.Verify(d => d.UploadEnd(It.IsAny<ushort>()), Times.Once);
}
[Test]
public void ReadProgramAsync_UploadDataReturnsContent_ContentInResult()
{
var programContent = "O1234\nG0 X0\nM30\n%";
var chars = programContent.ToCharArray();
var dialect = new Mock<IFocasDialect>();
dialect.Setup(d => d.UploadStart(It.IsAny<ushort>(), It.IsAny<string>(), It.IsAny<long>(), It.IsAny<short>()))
.Returns(Focas1.EW_OK);
dialect.Setup(d => d.UploadData(It.IsAny<ushort>(), ref It.Ref<int>.IsAny, It.IsAny<char[]>()))
.Returns(new UploadDataDelegate(delegate(ushort h, ref int len, char[] buf)
{
Array.Copy(chars, buf, chars.Length);
len = chars.Length;
return Focas1.EW_OK;
}));
dialect.Setup(d => d.UploadEnd(It.IsAny<ushort>())).Returns(Focas1.EW_OK);
using (var machine = CreateConnectedWithMockDialect(dialect))
{
var result = machine.ReadProgramAsync("O1234").GetAwaiter().GetResult();
Assert.That(result.Success, Is.True);
Assert.That(result.Value, Is.EqualTo(programContent));
}
}
[Test]
public void ReadToolDataAsync_CallsUploadStartWithFileType1()
{
var dialect = new Mock<IFocasDialect>();
dialect.Setup(d => d.UploadStart(It.IsAny<ushort>(), It.IsAny<string>(), It.IsAny<long>(), It.IsAny<short>()))
.Returns(Focas1.EW_OK);
dialect.Setup(d => d.UploadData(It.IsAny<ushort>(), ref It.Ref<int>.IsAny, It.IsAny<char[]>()))
.Returns(Focas1.EW_OK);
dialect.Setup(d => d.UploadEnd(It.IsAny<ushort>())).Returns(Focas1.EW_OK);
using (var machine = CreateConnectedWithMockDialect(dialect))
{
machine.ReadToolDataAsync("O1234").GetAwaiter().GetResult();
}
dialect.Verify(d => d.UploadStart(It.IsAny<ushort>(), "O1234", It.IsAny<long>(), (short)1), Times.Once);
}
[Test]
public void ReadWorkZeroDataAsync_CallsUploadStartWithFileType5()
{
var dialect = new Mock<IFocasDialect>();
dialect.Setup(d => d.UploadStart(It.IsAny<ushort>(), It.IsAny<string>(), It.IsAny<long>(), It.IsAny<short>()))
.Returns(Focas1.EW_OK);
dialect.Setup(d => d.UploadData(It.IsAny<ushort>(), ref It.Ref<int>.IsAny, It.IsAny<char[]>()))
.Returns(Focas1.EW_OK);
dialect.Setup(d => d.UploadEnd(It.IsAny<ushort>())).Returns(Focas1.EW_OK);
using (var machine = CreateConnectedWithMockDialect(dialect))
{
machine.ReadWorkZeroDataAsync("O1234").GetAwaiter().GetResult();
}
dialect.Verify(d => d.UploadStart(It.IsAny<ushort>(), "O1234", It.IsAny<long>(), (short)5), Times.Once);
}
[Test]
public void ReadProgramAsync_UploadStartFails_ResultIsFail()
{
var dialect = new Mock<IFocasDialect>();
dialect.Setup(d => d.UploadStart(It.IsAny<ushort>(), It.IsAny<string>(), It.IsAny<long>(), It.IsAny<short>()))
.Returns(-3); // EW_SOCKET or any error
dialect.Setup(d => d.UploadEnd(It.IsAny<ushort>())).Returns(Focas1.EW_OK);
using (var machine = CreateConnectedWithMockDialect(dialect))
{
var result = machine.ReadProgramAsync("O1234").GetAwaiter().GetResult();
Assert.That(result.Success, Is.False);
}
}
[Test]
public void WriteProgramAsync_NullContent_ResultFail()
{
var dialect = new Mock<IFocasDialect>();
using (var machine = CreateConnectedWithMockDialect(dialect))
{
var prog = new CncProgram { Name = "O1234", Content = null };
var result = machine.WriteProgramAsync("//CNC_MEM/USER/PATH1/", prog).GetAwaiter().GetResult();
Assert.That(result.Success, Is.False);
}
}
[Test]
public void WriteProgramAsync_InvalidProgramName_ValidationFails()
{
var dialect = new Mock<IFocasDialect>();
using (var machine = CreateConnectedWithMockDialect(dialect))
{
// "PROG" does not match Fanuc O#### name rule — validator rejects before any FOCAS call
var prog = new CncProgram { Name = "PROG", Content = "O1234\nG00 X0\nM30" };
var result = machine.WriteProgramAsync("//CNC_MEM/USER/PATH1/", prog).GetAwaiter().GetResult();
Assert.That(result.Success, Is.False);
}
}
[Test]
public void State_InitialState_IsDisconnected()
{
using (var machine = new FanucMachine(EthernetConfig()))
{
Assert.That(machine.State, Is.EqualTo(ConnectionState.Disconnected));
}
}
[Test]
public void Name_ReturnsConfigName()
{
using (var machine = new FanucMachine(EthernetConfig()))
{
Assert.That(machine.Name, Is.EqualTo("TestFanuc"));
}
}
}
}

View file

@ -0,0 +1,185 @@
using NUnit.Framework;
using FanucProgramManager;
namespace FanucProgramManager.Tests.Unit
{
[TestFixture]
public class FanucProgramParsingTests
{
private const string SimpleProgram =
"\nO1234(COMMENT)\n" +
"G0 X0 Y0\n" +
"M30\n" +
"%";
private const string ThreeFileInput =
"program:{\n" +
"\nO1234(COMMENT)\n" +
"G0 X0 Y0\n" +
"M30\n" +
"%\n" +
"}\n" +
"tooloffsetdata:{\n" +
"TOOL_DATA_LINE1\n" +
"}\n" +
"workzerooffset:{\n" +
"WORK_ZERO_LINE1\n" +
"}\n";
[Test]
public void Constructor_OldFormat_ProgramFieldSet()
{
var p = new FanucProgram(SimpleProgram);
Assert.That(p.Program, Is.Not.Null);
Assert.That(p.ToolOffsetData, Is.Null);
Assert.That(p.WorkZeroOffsetData, Is.Null);
}
[Test]
public void Constructor_ThreeFileFormat_AllSectionsSet()
{
var p = new FanucProgram(ThreeFileInput);
Assert.That(p.Program, Is.Not.Null);
Assert.That(p.ToolOffsetData, Is.Not.Null);
Assert.That(p.WorkZeroOffsetData, Is.Not.Null);
}
[Test]
public void Constructor_ThreeFileFormat_ToolOffsetDataContent()
{
var p = new FanucProgram(ThreeFileInput);
Assert.That(p.ToolOffsetData, Does.Contain("TOOL_DATA_LINE1"));
}
[Test]
public void Constructor_ThreeFileFormat_WorkZeroOffsetDataContent()
{
var p = new FanucProgram(ThreeFileInput);
Assert.That(p.WorkZeroOffsetData, Does.Contain("WORK_ZERO_LINE1"));
}
[Test]
public void GetProgramTitle_StandardProgram_ReturnsO1234()
{
var p = new FanucProgram(SimpleProgram);
string title = p.GetProgramTitle();
Assert.That(title, Is.EqualTo("O1234"));
}
[Test]
public void GetProgramTitle_Static_ReturnsTitle()
{
string title = FanucProgram.GetProgramTitle("\nO5678\nG0\n%");
Assert.That(title, Is.EqualTo("O5678"));
}
[Test]
public void GetProgramComment_ProgramWithComment_ReturnsComment()
{
var p = new FanucProgram(SimpleProgram);
string comment = p.GetProgramComment();
Assert.That(comment, Is.EqualTo("COMMENT"));
}
[Test]
public void GetProgramComment_ProgramWithoutComment_ReturnsEmpty()
{
var p = new FanucProgram("\nO1234\nG0 X0\nM30\n%");
string comment = p.GetProgramComment();
Assert.That(comment, Is.EqualTo(""));
}
[Test]
public void ChangeComment_ReplaceExistingComment_CommentUpdated()
{
var p = new FanucProgram(SimpleProgram);
p.ChangeComment("NEW_COMMENT");
Assert.That(p.GetProgramComment(), Is.EqualTo("NEW_COMMENT"));
}
[Test]
public void ChangeComment_NoExistingComment_CommentInserted()
{
var p = new FanucProgram("\nO1234\nG0 X0\nM30\n%");
p.ChangeComment("INSERTED");
Assert.That(p.GetProgramComment(), Is.EqualTo("INSERTED"));
}
[Test]
public void Standardize_LeadingPercent_Removed()
{
var p = new FanucProgram("%\nO1234\nG0\n%");
Assert.That(p.Program, Does.Not.StartWith("%"));
}
[Test]
public void Standardize_AngleBracketTitle_ConvertedToPlain()
{
var p = new FanucProgram("\n<O1234>\nG0\n%");
Assert.That(p.Program, Does.Contain("O1234"));
Assert.That(p.Program, Does.Not.Contain("<O1234>"));
}
[Test]
public void ToString_ThreeFileFormat_ContainsAllSections()
{
var p = new FanucProgram(ThreeFileInput);
string output = p.ToString();
Assert.That(output, Does.Contain("program:{"));
Assert.That(output, Does.Contain("tooloffsetdata:{"));
Assert.That(output, Does.Contain("workzerooffset:{"));
}
[Test]
public void ToString_ProgramContent_RoundTrips()
{
var p = new FanucProgram(ThreeFileInput);
string output = p.ToString();
var p2 = new FanucProgram(output);
Assert.That(p2.GetProgramTitle(), Is.EqualTo(p.GetProgramTitle()));
Assert.That(p2.ToolOffsetData, Does.Contain("TOOL_DATA_LINE1"));
Assert.That(p2.WorkZeroOffsetData, Does.Contain("WORK_ZERO_LINE1"));
}
[Test]
public void SetValue_ToolOffsetData_ValueStored()
{
var p = new FanucProgram(SimpleProgram);
p.setValue("TOOL_DATA\n%", FanucProgram.FILE_TYPE.TOOL_OFFEST_DATA);
Assert.That(p.ToolOffsetData, Does.Contain("TOOL_DATA"));
}
[Test]
public void SetValue_WorkZeroOffset_ValueStored()
{
var p = new FanucProgram(SimpleProgram);
p.setValue("WORK_DATA\n%", FanucProgram.FILE_TYPE.WORK_ZERO_OFFSET);
Assert.That(p.WorkZeroOffsetData, Does.Contain("WORK_DATA"));
}
}
}

View file

@ -0,0 +1,141 @@
using System;
using NUnit.Framework;
using Moq;
using FanucProgramManager.Cnc;
using FanucProgramManager.Cnc.Models;
using FanucProgramManager.Cnc.Heidenhain;
namespace FanucProgramManager.Tests.Unit
{
[TestFixture]
public class HeidenhainMachineTests
{
private HeidenhainConnectionConfig _config;
private Mock<ILsv2Client> _clientMock;
private Mock<INcProgramValidator> _validatorMock;
private HeidenhainMachine _machine;
[SetUp]
public void SetUp()
{
_config = new HeidenhainConnectionConfig
{
Name = "TestHeidenhain",
IpAddress = "192.168.1.4",
Port = 19000
};
_clientMock = new Mock<ILsv2Client>(MockBehavior.Strict);
// Always-ok validator so existing write tests are not affected by validation rules
_validatorMock = new Mock<INcProgramValidator>();
_validatorMock.Setup(v => v.Validate(It.IsAny<string>(), It.IsAny<string>()))
.Returns(ValidationResult.Ok());
// Internal constructor sets state = Connected immediately
_machine = new HeidenhainMachine(_config, _clientMock.Object, _validatorMock.Object);
}
[TearDown]
public void TearDown()
{
_clientMock.Setup(c => c.Disconnect());
_machine?.Dispose();
}
// State on construction
[Test]
public void Constructor_WithInjectedClient_StateIsConnected()
{
Assert.That(_machine.State, Is.EqualTo(ConnectionState.Connected));
}
// ReadProgramAsync
[Test]
public void ReadProgramAsync_ReceiveFileReturnsContent_ResultOk()
{
_clientMock.Setup(c => c.ReceiveFile("TNC:/nc_prog/O1234.H")).Returns("NC_CONTENT");
var result = _machine.ReadProgramAsync("TNC:/nc_prog/O1234.H").GetAwaiter().GetResult();
Assert.That(result.Success, Is.True);
Assert.That(result.Value, Is.EqualTo("NC_CONTENT"));
_clientMock.Verify(c => c.ReceiveFile("TNC:/nc_prog/O1234.H"), Times.Once);
}
[Test]
public void ReadProgramAsync_ReceiveFileReturnsNull_ResultIsFail()
{
_clientMock.Setup(c => c.ReceiveFile(It.IsAny<string>())).Returns((string)null);
var result = _machine.ReadProgramAsync("TNC:/nc_prog/MISSING.H").GetAwaiter().GetResult();
Assert.That(result.Success, Is.False);
}
// WriteProgramAsync
[Test]
public void WriteProgramAsync_WithName_AppendsDotHExtension()
{
_clientMock.Setup(c => c.SendFile("TNC:/nc_prog/O1234.H", "CONTENT")).Returns(true);
var prog = new CncProgram { Name = "O1234", Content = "CONTENT" };
var result = _machine.WriteProgramAsync("TNC:/nc_prog/", prog).GetAwaiter().GetResult();
Assert.That(result.Success, Is.True);
_clientMock.Verify(c => c.SendFile("TNC:/nc_prog/O1234.H", "CONTENT"), Times.Once);
}
[Test]
public void WriteProgramAsync_NameAlreadyHasExtension_NoDoubleExtension()
{
_clientMock.Setup(c => c.SendFile("TNC:/nc_prog/O1234.H", "CONTENT")).Returns(true);
var prog = new CncProgram { Name = "O1234.H", Content = "CONTENT" };
_machine.WriteProgramAsync("TNC:/nc_prog/", prog).GetAwaiter().GetResult();
_clientMock.Verify(c => c.SendFile("TNC:/nc_prog/O1234.H", "CONTENT"), Times.Once);
_clientMock.Verify(c => c.SendFile(It.Is<string>(s => s.EndsWith(".H.H")), It.IsAny<string>()), Times.Never);
}
[Test]
public void WriteProgramAsync_EmptyContent_FailsWithoutCallingSendFile()
{
var prog = new CncProgram { Name = "O1234", Content = "" };
var result = _machine.WriteProgramAsync("TNC:/nc_prog/", prog).GetAwaiter().GetResult();
Assert.That(result.Success, Is.False);
_clientMock.Verify(c => c.SendFile(It.IsAny<string>(), It.IsAny<string>()), Times.Never);
}
[Test]
public void WriteProgramAsync_NullProgram_FailsWithoutCallingSendFile()
{
var result = _machine.WriteProgramAsync("TNC:/nc_prog/", null).GetAwaiter().GetResult();
Assert.That(result.Success, Is.False);
_clientMock.Verify(c => c.SendFile(It.IsAny<string>(), It.IsAny<string>()), Times.Never);
}
[Test]
public void WriteProgramAsync_SendFileFails_ResultIsFail()
{
_clientMock.Setup(c => c.SendFile(It.IsAny<string>(), It.IsAny<string>())).Returns(false);
var prog = new CncProgram { Name = "O1234", Content = "DATA" };
var result = _machine.WriteProgramAsync("TNC:/nc_prog/", prog).GetAwaiter().GetResult();
Assert.That(result.Success, Is.False);
}
// ReadActiveProgramAsync
[Test]
public void ReadActiveProgramAsync_ReturnsFailNotSupported()
{
var result = _machine.ReadActiveProgramAsync().GetAwaiter().GetResult();
Assert.That(result.Success, Is.False);
}
}
}

View file

@ -0,0 +1,105 @@
using NUnit.Framework;
using FanucProgramManager.Cnc.Fanuc;
namespace FanucProgramManager.Tests.Unit
{
[TestFixture]
public class IsoProgramFormatterTests
{
// EnsureTitleBrackets
// The formatter works on programs with a leading blank line then a title line.
// e.g. "%\nO1234(COMMENT)\nG0...\n"
// SplitFirstNonEmptyLine splits after the SECOND newline, so we need at least 2 newlines.
private const string ProgramWithBrackets =
"%\n<O1234>(COMMENT)\nG0 X0\nM30\n";
private const string ProgramNoBrackets =
"%\nO1234(COMMENT)\nG0 X0\nM30\n";
private const string ProgramNoComment =
"%\nO1234\nG0 X0\nM30\n";
[Test]
public void EnsureTitleBrackets_NoBrackets_AddsBrackets()
{
string result = IsoProgramFormatter.EnsureTitleBrackets(ProgramNoBrackets);
Assert.That(result, Does.Contain("<O1234>"));
}
[Test]
public void EnsureTitleBrackets_AlreadyHasBrackets_NoChange()
{
string result = IsoProgramFormatter.EnsureTitleBrackets(ProgramWithBrackets);
Assert.That(result, Is.EqualTo(ProgramWithBrackets));
}
[Test]
public void EnsureTitleBrackets_NullOrEmpty_ReturnsInput()
{
Assert.That(IsoProgramFormatter.EnsureTitleBrackets(null), Is.Null);
Assert.That(IsoProgramFormatter.EnsureTitleBrackets(""), Is.EqualTo(""));
}
// InsertOrReplaceComment
[Test]
public void InsertOrReplaceComment_NoComment_InsertsComment()
{
string result = IsoProgramFormatter.InsertOrReplaceComment(ProgramNoComment, "TEST");
Assert.That(result, Does.Contain("(TEST)"));
}
[Test]
public void InsertOrReplaceComment_ExistingComment_ReplacesComment()
{
string result = IsoProgramFormatter.InsertOrReplaceComment(ProgramNoBrackets, "NEW");
Assert.That(result, Does.Contain("(NEW)"));
Assert.That(result, Does.Not.Contain("(COMMENT)"));
}
[Test]
public void InsertOrReplaceComment_EmptyComment_InsertsEmptyParens()
{
string result = IsoProgramFormatter.InsertOrReplaceComment(ProgramNoComment, "");
Assert.That(result, Does.Contain("()"));
}
[Test]
public void InsertOrReplaceComment_NullOrEmpty_ReturnsInput()
{
Assert.That(IsoProgramFormatter.InsertOrReplaceComment(null, "X"), Is.Null);
Assert.That(IsoProgramFormatter.InsertOrReplaceComment("", "X"), Is.EqualTo(""));
}
// RenameAndComment
[Test]
public void RenameAndComment_ProducesCorrectHeader()
{
string result = IsoProgramFormatter.RenameAndComment(ProgramNoBrackets, "O9999", "DESC");
Assert.That(result, Does.StartWith("%\n<O9999>(DESC)"));
}
[Test]
public void RenameAndComment_BodyAppendedAfterHeader()
{
string result = IsoProgramFormatter.RenameAndComment(ProgramNoBrackets, "O9999", "DESC");
Assert.That(result, Does.Contain("G0 X0"));
}
[Test]
public void RenameAndComment_NullOrEmpty_ReturnsInput()
{
Assert.That(IsoProgramFormatter.RenameAndComment(null, "X", "Y"), Is.Null);
Assert.That(IsoProgramFormatter.RenameAndComment("", "X", "Y"), Is.EqualTo(""));
}
}
}

View file

@ -0,0 +1,163 @@
using System;
using NUnit.Framework;
using Moq;
using FanucProgramManager.Cnc;
using FanucProgramManager.Cnc.Models;
using FanucProgramManager.Cnc.Mitsubishi;
namespace FanucProgramManager.Tests.Unit
{
[TestFixture]
public class MitsubishiMachineTests
{
private MitsubishiConnectionConfig _config;
private Mock<IMitsubishiFtpClient> _ftpMock;
private Mock<INcProgramValidator> _validatorMock;
private MitsubishiMachine _machine;
[SetUp]
public void SetUp()
{
_config = new MitsubishiConnectionConfig
{
Name = "TestMitsubishi",
IpAddress = "192.168.1.3",
Port = 21,
ProgramDirectory = "/PRG/"
};
_ftpMock = new Mock<IMitsubishiFtpClient>(MockBehavior.Strict);
// Always-ok validator so existing write tests are not affected by validation rules
_validatorMock = new Mock<INcProgramValidator>();
_validatorMock.Setup(v => v.Validate(It.IsAny<string>(), It.IsAny<string>()))
.Returns(ValidationResult.Ok());
_machine = new MitsubishiMachine(_config, _ftpMock.Object, _validatorMock.Object);
}
[TearDown]
public void TearDown()
{
_machine?.Dispose();
}
// ConnectAsync
[Test]
public void ConnectAsync_PingTrue_StateConnectedAndResultOk()
{
_ftpMock.Setup(f => f.Ping()).Returns(true);
var result = _machine.ConnectAsync().GetAwaiter().GetResult();
Assert.That(result.Success, Is.True);
Assert.That(_machine.State, Is.EqualTo(ConnectionState.Connected));
}
[Test]
public void ConnectAsync_PingFalse_StateFaultedAndResultFail()
{
_ftpMock.Setup(f => f.Ping()).Returns(false);
var result = _machine.ConnectAsync().GetAwaiter().GetResult();
Assert.That(result.Success, Is.False);
Assert.That(_machine.State, Is.EqualTo(ConnectionState.Faulted));
}
// ReadProgramAsync — no .MPF extension for Mitsubishi
[Test]
public void ReadProgramAsync_FullPath_CallsDownloadFileNoExtension()
{
_ftpMock.Setup(f => f.Ping()).Returns(true);
_ftpMock.Setup(f => f.DownloadFile("/PRG/O1234")).Returns("NC_DATA");
_machine.ConnectAsync().GetAwaiter().GetResult();
var result = _machine.ReadProgramAsync("/PRG/O1234").GetAwaiter().GetResult();
Assert.That(result.Success, Is.True);
Assert.That(result.Value, Is.EqualTo("NC_DATA"));
_ftpMock.Verify(f => f.DownloadFile("/PRG/O1234"), Times.Once);
}
[Test]
public void ReadProgramAsync_ShortName_ResolvesWithDirectoryNoExtension()
{
_ftpMock.Setup(f => f.Ping()).Returns(true);
_ftpMock.Setup(f => f.DownloadFile("/PRG/O1234")).Returns("DATA");
_machine.ConnectAsync().GetAwaiter().GetResult();
_machine.ReadProgramAsync("O1234").GetAwaiter().GetResult();
// Must NOT append .MPF
_ftpMock.Verify(f => f.DownloadFile("/PRG/O1234"), Times.Once);
_ftpMock.Verify(f => f.DownloadFile(It.Is<string>(s => s.EndsWith(".MPF"))), Times.Never);
}
// WriteProgramAsync
[Test]
public void WriteProgramAsync_ValidContent_CallsUploadFileNoExtension()
{
_ftpMock.Setup(f => f.Ping()).Returns(true);
_ftpMock.Setup(f => f.UploadFile("/PRG/O1234", "CONTENT"));
_machine.ConnectAsync().GetAwaiter().GetResult();
var prog = new CncProgram { Name = "O1234", Content = "CONTENT" };
var result = _machine.WriteProgramAsync("/PRG/", prog).GetAwaiter().GetResult();
Assert.That(result.Success, Is.True);
_ftpMock.Verify(f => f.UploadFile("/PRG/O1234", "CONTENT"), Times.Once);
_ftpMock.Verify(f => f.UploadFile(It.Is<string>(s => s.EndsWith(".MPF")), It.IsAny<string>()), Times.Never);
}
[Test]
public void WriteProgramAsync_EmptyContent_FailsWithoutCallingUploadFile()
{
_ftpMock.Setup(f => f.Ping()).Returns(true);
_machine.ConnectAsync().GetAwaiter().GetResult();
var prog = new CncProgram { Name = "O1234", Content = "" };
var result = _machine.WriteProgramAsync("/PRG/", prog).GetAwaiter().GetResult();
Assert.That(result.Success, Is.False);
_ftpMock.Verify(f => f.UploadFile(It.IsAny<string>(), It.IsAny<string>()), Times.Never);
}
[Test]
public void WriteProgramAsync_NullProgram_FailsWithoutCallingUploadFile()
{
_ftpMock.Setup(f => f.Ping()).Returns(true);
_machine.ConnectAsync().GetAwaiter().GetResult();
var result = _machine.WriteProgramAsync("/PRG/", null).GetAwaiter().GetResult();
Assert.That(result.Success, Is.False);
_ftpMock.Verify(f => f.UploadFile(It.IsAny<string>(), It.IsAny<string>()), Times.Never);
}
// DeleteProgramAsync
[Test]
public void DeleteProgramAsync_ValidPath_CallsDeleteFile()
{
_ftpMock.Setup(f => f.Ping()).Returns(true);
_ftpMock.Setup(f => f.DeleteFile("/PRG/O1234"));
_machine.ConnectAsync().GetAwaiter().GetResult();
var result = _machine.DeleteProgramAsync("/PRG/O1234").GetAwaiter().GetResult();
Assert.That(result.Success, Is.True);
_ftpMock.Verify(f => f.DeleteFile("/PRG/O1234"), Times.Once);
}
// ReadActiveProgramAsync
[Test]
public void ReadActiveProgramAsync_ReturnsFailNotSupported()
{
var result = _machine.ReadActiveProgramAsync().GetAwaiter().GetResult();
Assert.That(result.Success, Is.False);
}
}
}

View file

@ -0,0 +1,280 @@
using NUnit.Framework;
using FanucProgramManager.Cnc;
using FanucProgramManager.Cnc.Models;
namespace FanucProgramManager.Tests.Unit
{
[TestFixture]
public class NcProgramValidatorTests
{
// ── Universal rules ───────────────────────────────────────────────────
[Test]
public void Validate_BlockTooLong_ReturnsError()
{
var v = new NcProgramValidator(CncManufacturer.Fanuc);
string longLine = "O1234\n" + new string('X', 81);
var r = v.Validate("O1234", longLine);
Assert.That(r.Success, Is.False);
Assert.That(HasErrorContaining(r, "80"), Is.True);
}
[Test]
public void Validate_BlockExactly80_NoLengthError()
{
var v = new NcProgramValidator(CncManufacturer.Fanuc);
string line80 = new string('G', 80);
var r = v.Validate("O1234", "O1234\n" + line80);
// Should not have a block-length error (may still have other errors)
Assert.That(HasErrorContaining(r, "Block length"), Is.False);
}
[Test]
public void Validate_LowercaseLetter_ReturnsError()
{
var v = new NcProgramValidator(CncManufacturer.Siemens);
var r = v.Validate("PROG.MPF", "PROG.MPF\ng00 X10");
Assert.That(r.Success, Is.False);
Assert.That(HasErrorContaining(r, "Lowercase"), Is.True);
}
[Test]
public void Validate_NoLowercase_NoLowercaseError()
{
var v = new NcProgramValidator(CncManufacturer.Siemens);
var r = v.Validate("PROG.MPF", "G00 X10 Y20");
Assert.That(HasErrorContaining(r, "Lowercase"), Is.False);
}
[Test]
public void Validate_TabCharacter_ReturnsError()
{
var v = new NcProgramValidator(CncManufacturer.Siemens);
var r = v.Validate("PROG.MPF", "G00\tX10");
Assert.That(r.Success, Is.False);
Assert.That(HasErrorContaining(r, "Tab"), Is.True);
}
[Test]
public void Validate_NestedParentheses_ReturnsError()
{
var v = new NcProgramValidator(CncManufacturer.Fanuc);
var r = v.Validate("O1234", "O1234\nG00 ((NESTED))");
Assert.That(r.Success, Is.False);
Assert.That(HasErrorContaining(r, "Nested"), Is.True);
}
[Test]
public void Validate_SingleParenComment_NoNestedError()
{
var v = new NcProgramValidator(CncManufacturer.Fanuc);
var r = v.Validate("O1234", "O1234\nG00 (COMMENT) X10");
Assert.That(HasErrorContaining(r, "Nested"), Is.False);
}
[Test]
public void Validate_CommentExceeds32Chars_ReturnsError()
{
var v = new NcProgramValidator(CncManufacturer.Fanuc);
string longComment = "(" + new string('A', 33) + ")";
var r = v.Validate("O1234", "O1234\n" + longComment);
Assert.That(r.Success, Is.False);
Assert.That(HasErrorContaining(r, "Comment length"), Is.True);
}
[Test]
public void Validate_CommentExactly32Chars_NoCommentLengthError()
{
var v = new NcProgramValidator(CncManufacturer.Fanuc);
string comment = "(" + new string('A', 32) + ")";
var r = v.Validate("O1234", "O1234\n" + comment);
Assert.That(HasErrorContaining(r, "Comment length"), Is.False);
}
// ── Fanuc rules ───────────────────────────────────────────────────────
[Test]
public void Validate_Fanuc_ValidNameAndContent_Succeeds()
{
var v = new NcProgramValidator(CncManufacturer.Fanuc);
var r = v.Validate("O1234", "O1234\nG00 X10 Y20\nM30");
Assert.That(r.Success, Is.True);
}
[Test]
public void Validate_Fanuc_NameNotOFourDigits_ReturnsError()
{
var v = new NcProgramValidator(CncManufacturer.Fanuc);
var r = v.Validate("PROG", "O1234\nG00");
Assert.That(r.Success, Is.False);
Assert.That(HasErrorContaining(r, "O####"), Is.True);
}
[Test]
public void Validate_Fanuc_NameO12345_ReturnsError()
{
var v = new NcProgramValidator(CncManufacturer.Fanuc);
// 5 digits — invalid for Fanuc
var r = v.Validate("O12345", "O1234\nG00");
Assert.That(r.Success, Is.False);
Assert.That(HasErrorContaining(r, "O####"), Is.True);
}
[Test]
public void Validate_Fanuc_FirstLineNotONumber_ReturnsError()
{
var v = new NcProgramValidator(CncManufacturer.Fanuc);
var r = v.Validate("O1234", "G00 X10\nM30");
Assert.That(r.Success, Is.False);
Assert.That(HasErrorContaining(r, "First line"), Is.True);
}
[TestCase(CncManufacturer.Fanuc, "O1234", "O1234\nG00 X10;")]
[TestCase(CncManufacturer.Mitsubishi, "O1234", "O1234\nG00 X10;")]
[TestCase(CncManufacturer.Siemens, "PROG.MPF", "G00 X10;")]
[TestCase(CncManufacturer.Heidenhain, "PROG.H", "L X+10;")]
public void Validate_SemicolonForbidden_ReturnsError(CncManufacturer m, string name, string content)
{
var r = new NcProgramValidator(m).Validate(name, content);
Assert.That(r.Success, Is.False);
Assert.That(HasErrorContaining(r, ";"), Is.True);
}
[Test]
public void Validate_Fanuc_BracketForbidden_ReturnsError()
{
var v = new NcProgramValidator(CncManufacturer.Fanuc);
var r = v.Validate("O1234", "O1234\nG00 [X10]");
Assert.That(r.Success, Is.False);
Assert.That(HasErrorContaining(r, "["), Is.True);
}
[Test]
public void Validate_Fanuc_MacroHash_ReturnsWarningNotError()
{
var v = new NcProgramValidator(CncManufacturer.Fanuc);
var r = v.Validate("O1234", "O1234\n#100=10");
// Warnings alone do not cause failure
Assert.That(r.Success, Is.True);
Assert.That(HasWarningContaining(r, "#"), Is.True);
}
[Test]
public void Validate_Fanuc_NameFromPathStripped()
{
var v = new NcProgramValidator(CncManufacturer.Fanuc);
// Full path — name part is O1234
var r = v.Validate("//CNC_MEM/USER/PATH1/O1234", "O1234\nG00 X10");
Assert.That(HasErrorContaining(r, "O####"), Is.False);
}
// ── Mitsubishi rules ──────────────────────────────────────────────────
[Test]
public void Validate_Mitsubishi_ValidNameAndContent_Succeeds()
{
var v = new NcProgramValidator(CncManufacturer.Mitsubishi);
var r = v.Validate("O1234", "O1234\nG00 X10\nM30");
Assert.That(r.Success, Is.True);
}
[Test]
public void Validate_Mitsubishi_BadName_ReturnsError()
{
var v = new NcProgramValidator(CncManufacturer.Mitsubishi);
var r = v.Validate("PROG", "O1234\nG00");
Assert.That(r.Success, Is.False);
Assert.That(HasErrorContaining(r, "O####"), Is.True);
}
// ── Siemens rules ─────────────────────────────────────────────────────
[Test]
public void Validate_Siemens_ValidNameAndContent_Succeeds()
{
var v = new NcProgramValidator(CncManufacturer.Siemens);
var r = v.Validate("PROG.MPF", "G00 X10 Y20\nM30");
Assert.That(r.Success, Is.True);
}
[Test]
public void Validate_Siemens_NameNoDotMpf_ReturnsError()
{
var v = new NcProgramValidator(CncManufacturer.Siemens);
var r = v.Validate("PROG", "G00 X10");
Assert.That(r.Success, Is.False);
Assert.That(HasErrorContaining(r, ".MPF"), Is.True);
}
[Test]
public void Validate_Siemens_LowercaseMpf_ReturnsNameError()
{
var v = new NcProgramValidator(CncManufacturer.Siemens);
var r = v.Validate("prog.mpf", "G00 X10");
Assert.That(r.Success, Is.False);
Assert.That(HasErrorContaining(r, ".MPF"), Is.True);
}
[Test]
public void Validate_Siemens_NoFirstLineRequired()
{
var v = new NcProgramValidator(CncManufacturer.Siemens);
var r = v.Validate("PROG.MPF", "G00 X10\nM30");
Assert.That(HasErrorContaining(r, "First line"), Is.False);
}
// ── Heidenhain rules ──────────────────────────────────────────────────
[Test]
public void Validate_Heidenhain_ValidNameAndContent_Succeeds()
{
var v = new NcProgramValidator(CncManufacturer.Heidenhain);
var r = v.Validate("PROG.H", "L X+10 Y+20 R0 FMAX\nL Z-5 F500");
Assert.That(r.Success, Is.True);
}
[Test]
public void Validate_Heidenhain_NameNoDotH_ReturnsError()
{
var v = new NcProgramValidator(CncManufacturer.Heidenhain);
var r = v.Validate("PROG", "L X+10");
Assert.That(r.Success, Is.False);
Assert.That(HasErrorContaining(r, ".H"), Is.True);
}
[Test]
public void Validate_Heidenhain_PercentForbidden_ReturnsError()
{
var v = new NcProgramValidator(CncManufacturer.Heidenhain);
var r = v.Validate("PROG.H", "L X+10 % COMMENT");
Assert.That(r.Success, Is.False);
Assert.That(HasErrorContaining(r, "%"), Is.True);
}
[Test]
public void Validate_Heidenhain_NoFirstLineRequired()
{
var v = new NcProgramValidator(CncManufacturer.Heidenhain);
var r = v.Validate("PROG.H", "L X+10\nM30");
Assert.That(HasErrorContaining(r, "First line"), Is.False);
}
// ── Helpers ───────────────────────────────────────────────────────────
private static bool HasErrorContaining(ValidationResult r, string fragment)
{
foreach (var e in r.Errors)
if (e.Severity == ValidationSeverity.Error && e.Message.Contains(fragment))
return true;
return false;
}
private static bool HasWarningContaining(ValidationResult r, string fragment)
{
foreach (var e in r.Errors)
if (e.Severity == ValidationSeverity.Warning && e.Message.Contains(fragment))
return true;
return false;
}
}
}

View file

@ -0,0 +1,161 @@
using System;
using NUnit.Framework;
using Moq;
using FanucProgramManager.Cnc;
using FanucProgramManager.Cnc.Models;
using FanucProgramManager.Cnc.Siemens;
namespace FanucProgramManager.Tests.Unit
{
[TestFixture]
public class SiemensMachineTests
{
private SiemensConnectionConfig _config;
private Mock<ISiemensFtpClient> _ftpMock;
private Mock<INcProgramValidator> _validatorMock;
private SiemensMachine _machine;
[SetUp]
public void SetUp()
{
_config = new SiemensConnectionConfig
{
Name = "TestSiemens",
IpAddress = "192.168.1.2",
Port = 21,
ProgramDirectory = "/_N_MPF_DIR/"
};
_ftpMock = new Mock<ISiemensFtpClient>(MockBehavior.Strict);
// Always-ok validator so existing write tests are not affected by validation rules
_validatorMock = new Mock<INcProgramValidator>();
_validatorMock.Setup(v => v.Validate(It.IsAny<string>(), It.IsAny<string>()))
.Returns(ValidationResult.Ok());
_machine = new SiemensMachine(_config, _ftpMock.Object, _validatorMock.Object);
}
[TearDown]
public void TearDown()
{
_machine?.Dispose();
}
// ConnectAsync
[Test]
public void ConnectAsync_PingTrue_StateConnectedAndResultOk()
{
_ftpMock.Setup(f => f.Ping()).Returns(true);
var result = _machine.ConnectAsync().GetAwaiter().GetResult();
Assert.That(result.Success, Is.True);
Assert.That(result.Value, Is.True);
Assert.That(_machine.State, Is.EqualTo(ConnectionState.Connected));
}
[Test]
public void ConnectAsync_PingFalse_StateFaultedAndResultFail()
{
_ftpMock.Setup(f => f.Ping()).Returns(false);
var result = _machine.ConnectAsync().GetAwaiter().GetResult();
Assert.That(result.Success, Is.False);
Assert.That(_machine.State, Is.EqualTo(ConnectionState.Faulted));
}
// ReadProgramAsync
[Test]
public void ReadProgramAsync_FullPath_CallsDownloadFileWithPath()
{
_ftpMock.Setup(f => f.Ping()).Returns(true);
_ftpMock.Setup(f => f.DownloadFile("/_N_MPF_DIR/O1234.MPF")).Returns("PROGRAM_CONTENT");
_machine.ConnectAsync().GetAwaiter().GetResult();
var result = _machine.ReadProgramAsync("/_N_MPF_DIR/O1234.MPF").GetAwaiter().GetResult();
Assert.That(result.Success, Is.True);
Assert.That(result.Value, Is.EqualTo("PROGRAM_CONTENT"));
_ftpMock.Verify(f => f.DownloadFile("/_N_MPF_DIR/O1234.MPF"), Times.Once);
}
[Test]
public void ReadProgramAsync_ShortName_ResolvesWithDirectoryAndMpfExtension()
{
_ftpMock.Setup(f => f.Ping()).Returns(true);
_ftpMock.Setup(f => f.DownloadFile("/_N_MPF_DIR/O1234.MPF")).Returns("DATA");
_machine.ConnectAsync().GetAwaiter().GetResult();
_machine.ReadProgramAsync("O1234").GetAwaiter().GetResult();
_ftpMock.Verify(f => f.DownloadFile("/_N_MPF_DIR/O1234.MPF"), Times.Once);
}
// WriteProgramAsync
[Test]
public void WriteProgramAsync_ValidContent_CallsUploadFile()
{
_ftpMock.Setup(f => f.Ping()).Returns(true);
_ftpMock.Setup(f => f.UploadFile(It.IsAny<string>(), "CONTENT"));
_machine.ConnectAsync().GetAwaiter().GetResult();
var prog = new CncProgram { Name = "O1234", Content = "CONTENT" };
var result = _machine.WriteProgramAsync("/_N_MPF_DIR/", prog).GetAwaiter().GetResult();
Assert.That(result.Success, Is.True);
_ftpMock.Verify(f => f.UploadFile(It.IsAny<string>(), "CONTENT"), Times.Once);
}
[Test]
public void WriteProgramAsync_EmptyContent_FailsWithoutCallingUploadFile()
{
_ftpMock.Setup(f => f.Ping()).Returns(true);
_machine.ConnectAsync().GetAwaiter().GetResult();
var prog = new CncProgram { Name = "O1234", Content = "" };
var result = _machine.WriteProgramAsync("/_N_MPF_DIR/", prog).GetAwaiter().GetResult();
Assert.That(result.Success, Is.False);
_ftpMock.Verify(f => f.UploadFile(It.IsAny<string>(), It.IsAny<string>()), Times.Never);
}
[Test]
public void WriteProgramAsync_NullProgram_FailsWithoutCallingUploadFile()
{
_ftpMock.Setup(f => f.Ping()).Returns(true);
_machine.ConnectAsync().GetAwaiter().GetResult();
var result = _machine.WriteProgramAsync("/_N_MPF_DIR/", null).GetAwaiter().GetResult();
Assert.That(result.Success, Is.False);
_ftpMock.Verify(f => f.UploadFile(It.IsAny<string>(), It.IsAny<string>()), Times.Never);
}
// DeleteProgramAsync
[Test]
public void DeleteProgramAsync_ValidPath_CallsDeleteFile()
{
_ftpMock.Setup(f => f.Ping()).Returns(true);
_ftpMock.Setup(f => f.DeleteFile("/_N_MPF_DIR/O1234.MPF"));
_machine.ConnectAsync().GetAwaiter().GetResult();
var result = _machine.DeleteProgramAsync("/_N_MPF_DIR/O1234.MPF").GetAwaiter().GetResult();
Assert.That(result.Success, Is.True);
_ftpMock.Verify(f => f.DeleteFile("/_N_MPF_DIR/O1234.MPF"), Times.Once);
}
// ReadActiveProgramAsync
[Test]
public void ReadActiveProgramAsync_ReturnsFailNotSupported()
{
var result = _machine.ReadActiveProgramAsync().GetAwaiter().GetResult();
Assert.That(result.Success, Is.False);
}
}
}

View file

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<packages>
<package id="NUnit" version="3.13.3" targetFramework="net472" />
<package id="NUnit3TestAdapter" version="4.2.1" targetFramework="net472" />
<package id="Moq" version="4.18.4" targetFramework="net472" />
<package id="Castle.Core" version="5.1.1" targetFramework="net472" />
</packages>

151
FanucProgramManager.csproj Executable file
View file

@ -0,0 +1,151 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
<ProjectGuid>{1032188B-3321-42B2-87ED-343BF718CAF5}</ProjectGuid>
<OutputType>Exe</OutputType>
<RootNamespace>FanucProgramManager</RootNamespace>
<AssemblyName>FanucProgramManager</AssemblyName>
<TargetFrameworkVersion>v4.7.2</TargetFrameworkVersion>
<FileAlignment>512</FileAlignment>
<AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects>
<Deterministic>true</Deterministic>
<IsWebBootstrapper>false</IsWebBootstrapper>
<PublishUrl>publish\</PublishUrl>
<Install>true</Install>
<InstallFrom>Disk</InstallFrom>
<UpdateEnabled>false</UpdateEnabled>
<UpdateMode>Foreground</UpdateMode>
<UpdateInterval>7</UpdateInterval>
<UpdateIntervalUnits>Days</UpdateIntervalUnits>
<UpdatePeriodically>false</UpdatePeriodically>
<UpdateRequired>false</UpdateRequired>
<MapFileExtensions>true</MapFileExtensions>
<ApplicationRevision>0</ApplicationRevision>
<ApplicationVersion>1.0.0.%2a</ApplicationVersion>
<UseApplicationTrust>false</UseApplicationTrust>
<BootstrapperEnabled>true</BootstrapperEnabled>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<PlatformTarget>AnyCPU</PlatformTarget>
<DebugSymbols>true</DebugSymbols>
<DebugType>full</DebugType>
<Optimize>false</Optimize>
<OutputPath>bin\Debug\</OutputPath>
<DefineConstants>DEBUG;TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<PlatformTarget>AnyCPU</PlatformTarget>
<DebugType>pdbonly</DebugType>
<Optimize>true</Optimize>
<OutputPath>bin\Release\</OutputPath>
<DefineConstants>TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<PropertyGroup>
<SignManifests>false</SignManifests>
</PropertyGroup>
<ItemGroup>
<Reference Include="Microsoft.Bcl.AsyncInterfaces, Version=7.0.0.0, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51, processorArchitecture=MSIL">
<HintPath>packages\Microsoft.Bcl.AsyncInterfaces.7.0.0\lib\net462\Microsoft.Bcl.AsyncInterfaces.dll</HintPath>
<Private>False</Private>
</Reference>
<Reference Include="System" />
<Reference Include="System.Core" />
<Reference Include="System.Management" />
<Reference Include="System.Memory, Version=4.0.1.2, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51, processorArchitecture=MSIL">
<HintPath>packages\System.Memory.4.5.5\lib\net461\System.Memory.dll</HintPath>
<Private>False</Private>
</Reference>
<Reference Include="System.Runtime.CompilerServices.Unsafe, Version=6.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
<HintPath>packages\System.Runtime.CompilerServices.Unsafe.6.0.0\lib\net461\System.Runtime.CompilerServices.Unsafe.dll</HintPath>
<Private>False</Private>
</Reference>
<Reference Include="System.Data.DataSetExtensions" />
<Reference Include="Microsoft.CSharp" />
<Reference Include="System.Data" />
<Reference Include="System.Net.Http" />
</ItemGroup>
<ItemGroup>
<Compile Include="AesCrypt.cs" />
<Compile Include="FanucProgram.cs" />
<Compile Include="InputArgs.cs" />
<Compile Include="FANUCMachine.cs" />
<Compile Include="fwlib32.cs" />
<Compile Include="Program.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="Cnc\ICncMachine.cs" />
<Compile Include="Cnc\IFanucMachine.cs" />
<Compile Include="Cnc\Fanuc\FanucCapabilities.cs" />
<Compile Include="Cnc\Fanuc\FanucConnectionConfig.cs" />
<Compile Include="Cnc\Fanuc\FanucDialect.cs" />
<Compile Include="Cnc\Fanuc\FanucDialectDetector.cs" />
<Compile Include="Cnc\Fanuc\FanucErrorTranslator.cs" />
<Compile Include="Cnc\Fanuc\FanucMachine.cs" />
<Compile Include="Cnc\Fanuc\FocasDialectFactory.cs" />
<Compile Include="Cnc\Fanuc\IFocasDialect.cs" />
<Compile Include="Cnc\Fanuc\IsoProgramFormatter.cs" />
<Compile Include="Cnc\Fanuc\LegacyFocasDialect.cs" />
<Compile Include="Cnc\Fanuc\ModernFocasDialect.cs" />
<Compile Include="Cnc\Models\CncError.cs" />
<Compile Include="Cnc\Models\CncManufacturer.cs" />
<Compile Include="Cnc\Models\CncProgram.cs" />
<Compile Include="Cnc\Models\CncResult.cs" />
<Compile Include="Cnc\Models\ConnectionState.cs" />
<Compile Include="Cnc\Models\MachineMode.cs" />
<Compile Include="Cnc\Models\MachineSnapshot.cs" />
<Compile Include="Cnc\Models\MotionState.cs" />
<Compile Include="Cnc\Models\RunState.cs" />
<Compile Include="Cnc\Models\ValidationSeverity.cs" />
<Compile Include="Cnc\Models\ValidationError.cs" />
<Compile Include="Cnc\Models\ValidationResult.cs" />
<Compile Include="Cnc\INcProgramValidator.cs" />
<Compile Include="Cnc\NcProgramValidator.cs" />
<Compile Include="Cnc\CncMachineFactory.cs" />
<Compile Include="Cnc\Heidenhain\HeidenhainConnectionConfig.cs" />
<Compile Include="Cnc\Heidenhain\ILsv2Client.cs" />
<Compile Include="Cnc\Heidenhain\Lsv2Client.cs" />
<Compile Include="Cnc\Heidenhain\HeidenhainMachine.cs" />
<Compile Include="Cnc\Siemens\SiemensConnectionConfig.cs" />
<Compile Include="Cnc\Siemens\ISiemensFtpClient.cs" />
<Compile Include="Cnc\Siemens\SiemensFtpClient.cs" />
<Compile Include="Cnc\Siemens\SiemensMachine.cs" />
<Compile Include="Cnc\Mitsubishi\MitsubishiConnectionConfig.cs" />
<Compile Include="Cnc\Mitsubishi\IMitsubishiFtpClient.cs" />
<Compile Include="Cnc\Mitsubishi\MitsubishiFtpClient.cs" />
<Compile Include="Cnc\Mitsubishi\MitsubishiMachine.cs" />
</ItemGroup>
<ItemGroup>
<None Include="App.config" />
<None Include="packages.config" />
</ItemGroup>
<ItemGroup>
<Content Include="Cnc\Fanuc\libs\Fwlib32.dll">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="Cnc\Fanuc\libs\fwlibe1.dll">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="Cnc\Fanuc\libs\fwlibNCG.dll">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
</ItemGroup>
<ItemGroup>
<BootstrapperPackage Include=".NETFramework,Version=v4.7.2">
<Visible>False</Visible>
<ProductName>Microsoft .NET Framework 4.7.2 %28x86 e x64%29</ProductName>
<Install>true</Install>
</BootstrapperPackage>
<BootstrapperPackage Include="Microsoft.Net.Framework.3.5.SP1">
<Visible>False</Visible>
<ProductName>.NET Framework 3.5 SP1</ProductName>
<Install>false</Install>
</BootstrapperPackage>
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
</Project>

20
FanucProgramManager.csproj.user Executable file
View file

@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="Current" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Debug|AnyCPU'">
<StartArguments>-chiave=YQXnxVoz6MjuH18d9/XDHzfTyxd/prRciP3C8bwMYb1vv3oUDjR250JKBh1X84f9 -azione=invia -pathprogramma=c:\temp\FanucProgrammiSalvati\M0108.txt -nodo=9 -tipo=2 -tooloffset -workzero -debug</StartArguments>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Release|AnyCPU'">
<StartArguments>-chiave=YQXnxVoz6MjuH18d9/XDHzfTyxd/prRciP3C8bwMYb1vv3oUDjR250JKBh1X84f9 -azione=invia -tipo=2 -nodo=9 -porta=8193 -path=2 -pathprogramma\temp\FanucProgrammiSalvati\programma.txt -debug</StartArguments>
</PropertyGroup>
<PropertyGroup>
<PublishUrlHistory>publish\</PublishUrlHistory>
<InstallUrlHistory />
<SupportUrlHistory />
<UpdateUrlHistory />
<BootstrapperUrlHistory />
<ErrorReportUrlHistory />
<FallbackCulture>it-IT</FallbackCulture>
<VerifyUploadedFiles>false</VerifyUploadedFiles>
<ProjectView>ShowAllFiles</ProjectView>
</PropertyGroup>
</Project>

31
FanucProgramManager.sln Executable file
View file

@ -0,0 +1,31 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.5.33414.496
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FanucProgramManager", "FanucProgramManager.csproj", "{1032188B-3321-42B2-87ED-343BF718CAF5}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FanucProgramManager.Tests", "FanucProgramManager.Tests\FanucProgramManager.Tests.csproj", "{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{1032188B-3321-42B2-87ED-343BF718CAF5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{1032188B-3321-42B2-87ED-343BF718CAF5}.Debug|Any CPU.Build.0 = Debug|Any CPU
{1032188B-3321-42B2-87ED-343BF718CAF5}.Release|Any CPU.ActiveCfg = Release|Any CPU
{1032188B-3321-42B2-87ED-343BF718CAF5}.Release|Any CPU.Build.0 = Release|Any CPU
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|Any CPU.ActiveCfg = Debug|AnyCPU
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|Any CPU.Build.0 = Debug|AnyCPU
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|Any CPU.ActiveCfg = Release|AnyCPU
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|Any CPU.Build.0 = Release|AnyCPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {79CE0E5A-A8EA-4EAD-B78D-EE8E0E9BADAB}
EndGlobalSection
EndGlobal

25
InputArgs.cs Executable file
View file

@ -0,0 +1,25 @@
using FanucProgramManager.Cnc.Models;
namespace FanucProgramManager
{
internal class InputArgs
{
public CncManufacturer manufacturer = CncManufacturer.Fanuc;
public int tipo = 0; // kept for legacy parsing only
public string azione = "";
public string ip = "";
public string porta = "";
public short pathCNC = 1;
public int nodoHssb = -1;
public string chiave = "";
public string username = "";
public string password = "";
public string commentoProgramma = "";
public string pathLocaleProgramma = "";
public bool compatibilityMode = false;
public bool hasToolOffsetData = false;
public bool hasWorkZeroOffsetData = false;
public bool mostraHelp = false;
public bool debugMode = false;
}
}

447
Program.cs Executable file
View file

@ -0,0 +1,447 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Management;
using System.Security.Cryptography;
using System.Text.RegularExpressions;
using FanucProgramManager.Cnc;
using FanucProgramManager.Cnc.Models;
namespace FanucProgramManager
{
internal class Program
{
private static InputArgs inputArgs = new InputArgs();
public static int Main(string[] args)
{
try
{
parseArgomenti(args);
}
catch (Exception e)
{
Console.Write("Gestore Programmi FANUC: ");
Console.WriteLine(e.Message);
Console.WriteLine("Errore parametri input. Consulta `-help' per maggiori informazioni.");
vediErrore(-101);
return -101;
}
if (inputArgs.mostraHelp)
{
MostraAiuto();
vediErrore(0);
return 0;
}
if (!checkLicense(inputArgs.chiave))
{
Console.WriteLine("Licenza ERRATA");
vediErrore(-100);
return -1;
}
// Validate required params
bool needsIp = inputArgs.manufacturer != CncManufacturer.Fanuc || inputArgs.nodoHssb == -1;
if (inputArgs.azione == "" || (needsIp && string.IsNullOrEmpty(inputArgs.ip)) || inputArgs.pathLocaleProgramma == "")
{
vediErrore(-102);
return -102;
}
Console.WriteLine(
"manufacturer: " + inputArgs.manufacturer + "\n" +
"IndirizzoIP: " + inputArgs.ip + "\n" +
"Porta: " + inputArgs.porta + "\n" +
"Compatibility: " + inputArgs.compatibilityMode + "\n" +
"commentoProgramma: " + inputArgs.commentoProgramma + "\n" +
"pathCNC: " + inputArgs.pathCNC + "\n" +
"pathLocaleProgramma: " + inputArgs.pathLocaleProgramma + "\n"
);
ICncMachine machine = null;
try
{
machine = CncMachineFactory.Create(inputArgs);
switch (inputArgs.azione.ToUpperInvariant())
{
case "SCARICA": return Scarica(machine);
case "INVIA": return Invia(machine);
default:
vediErrore(-103);
return -103;
}
}
finally
{
if (machine != null)
machine.Dispose();
}
}
static int Scarica(ICncMachine macchina)
{
Console.WriteLine("RICEVI DA CNC-->");
try
{
var connResult = macchina.ConnectAsync().GetAwaiter().GetResult();
if (!connResult.Success)
{
Console.WriteLine("Macchina non connessa");
vediErrore(-201);
return -201;
}
Console.WriteLine("Inizio RICEZIONE NCProgram");
var progResult = macchina.ReadProgramAsync(inputArgs.pathCNC.ToString()).GetAwaiter().GetResult();
Console.WriteLine("Fine RICEZIONE NCProgram");
if (!progResult.Success || string.IsNullOrEmpty(progResult.Value))
{
macchina.DisconnectAsync().GetAwaiter().GetResult();
vediErrore(-204);
return -204;
}
FanucProgram programma = new FanucProgram(progResult.Value);
IFanucMachine fanuc = macchina as IFanucMachine;
if (inputArgs.hasToolOffsetData)
{
if (fanuc == null)
{
Console.WriteLine("ToolOffset non supportato per questo produttore");
macchina.DisconnectAsync().GetAwaiter().GetResult();
vediErrore(-205);
return -205;
}
Console.WriteLine("Inizio RICEZIONE ToolOffsetData");
var toolResult = fanuc.ReadToolDataAsync(inputArgs.pathCNC.ToString()).GetAwaiter().GetResult();
Console.WriteLine("Fine RICEZIONE ToolOffsetData");
if (!toolResult.Success || string.IsNullOrEmpty(toolResult.Value))
{
macchina.DisconnectAsync().GetAwaiter().GetResult();
vediErrore(-205);
return -205;
}
programma.setValue(toolResult.Value, FanucProgram.FILE_TYPE.TOOL_OFFEST_DATA);
}
if (inputArgs.hasWorkZeroOffsetData)
{
if (fanuc == null)
{
Console.WriteLine("WorkZeroOffset non supportato per questo produttore");
macchina.DisconnectAsync().GetAwaiter().GetResult();
vediErrore(-206);
return -206;
}
Console.WriteLine("Inizio RICEZIONE WorkZeroOffsetData");
var workResult = fanuc.ReadWorkZeroDataAsync(inputArgs.pathCNC.ToString()).GetAwaiter().GetResult();
Console.WriteLine("Fine RICEZIONE WorkZeroOffsetData");
if (!workResult.Success || string.IsNullOrEmpty(workResult.Value))
{
macchina.DisconnectAsync().GetAwaiter().GetResult();
vediErrore(-206);
return -206;
}
programma.setValue(workResult.Value, FanucProgram.FILE_TYPE.WORK_ZERO_OFFSET);
}
macchina.DisconnectAsync().GetAwaiter().GetResult();
File.WriteAllText(inputArgs.pathLocaleProgramma, programma.ToString());
vediErrore(0);
return 0;
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
vediErrore(-202);
return -202;
}
}
static int Invia(ICncMachine macchina)
{
Console.WriteLine("INVIA A CNC-->");
try
{
var connResult = macchina.ConnectAsync().GetAwaiter().GetResult();
if (!connResult.Success)
{
Console.WriteLine("Macchina non connessa");
vediErrore(-201);
return -201;
}
FanucProgram programma = new FanucProgram(File.ReadAllText(inputArgs.pathLocaleProgramma));
if (!string.IsNullOrEmpty(inputArgs.commentoProgramma))
programma.ChangeComment(inputArgs.commentoProgramma);
if (programma.Program != null)
{
Console.WriteLine("Inizio INVIO NCProgram");
var cncProg = new CncProgram
{
Name = programma.GetProgramTitle(),
Comment = programma.GetProgramComment(),
Content = programma.Program,
Path = inputArgs.pathCNC.ToString()
};
var writeResult = macchina.WriteProgramAsync(inputArgs.pathCNC.ToString(), cncProg).GetAwaiter().GetResult();
Console.WriteLine("Fine INVIO NCProgram");
if (!writeResult.Success)
{
macchina.DisconnectAsync().GetAwaiter().GetResult();
vediErrore(-204);
return -204;
}
}
IFanucMachine fanuc = macchina as IFanucMachine;
if (programma.ToolOffsetData != null && inputArgs.hasToolOffsetData)
{
if (fanuc == null)
{
Console.WriteLine("ToolOffset non supportato per questo produttore");
macchina.DisconnectAsync().GetAwaiter().GetResult();
vediErrore(-205);
return -205;
}
Console.WriteLine("Inizio INVIO ToolOffsetData");
var toolResult = fanuc.WriteToolDataAsync(inputArgs.pathCNC.ToString(), programma.ToolOffsetData).GetAwaiter().GetResult();
Console.WriteLine("Fine INVIO ToolOffsetData");
if (!toolResult.Success)
{
macchina.DisconnectAsync().GetAwaiter().GetResult();
vediErrore(-205);
return -205;
}
}
if (programma.WorkZeroOffsetData != null && inputArgs.hasWorkZeroOffsetData)
{
if (fanuc == null)
{
Console.WriteLine("WorkZeroOffset non supportato per questo produttore");
macchina.DisconnectAsync().GetAwaiter().GetResult();
vediErrore(-206);
return -206;
}
Console.WriteLine("Inizio INVIO WorkZeroOffsetData");
var workResult = fanuc.WriteWorkZeroDataAsync(inputArgs.pathCNC.ToString(), programma.WorkZeroOffsetData).GetAwaiter().GetResult();
Console.WriteLine("Fine INVIO WorkZeroOffsetData");
if (!workResult.Success)
{
macchina.DisconnectAsync().GetAwaiter().GetResult();
vediErrore(-206);
return -206;
}
}
macchina.DisconnectAsync().GetAwaiter().GetResult();
vediErrore(0);
return 0;
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
vediErrore(-202);
return -202;
}
}
static void vediErrore(int errore)
{
Console.WriteLine("Errore:" + errore);
Console.WriteLine();
Console.WriteLine("Codici Errori:");
Console.WriteLine("-100 : Licenza Errata/non valida.");
Console.WriteLine("-101 : Impossibile parsare i parametri.");
Console.WriteLine("-102 : Parametri mancanti. ");
Console.WriteLine("-103 : Codice connessione errato: 3->Ethernet 2->HSSB. ");
Console.WriteLine("-201 : Macchina non connessa. ");
Console.WriteLine("-202 : Eccezione nell'esecuzione dell'AZIONE. ");
Console.WriteLine("-203 : L'azione è fallita. ");
Console.WriteLine("-204 : Errore in fase invio/ricezione Programma CN. ");
Console.WriteLine("-205 : Errore in fase invio/ricezione tool offset data ");
Console.WriteLine("-206 : Errore in fase invio/ricezione work zero offest data");
Console.WriteLine("-207 : Posizionare il CN in MDI o EDIT per eseguire l'invio di un programma");
Console.WriteLine("-208 : Programma CN Caricato ma non settato a main.");
if (inputArgs.debugMode)
Console.ReadLine();
}
static Boolean checkLicense(string chiave)
{
string pcInfo = string.Empty;
ManagementClass mc = new ManagementClass("win32_processor");
ManagementObjectCollection moc = mc.GetInstances();
foreach (ManagementObject mo in moc)
{
//Get only the first CPU's ID
pcInfo = mo.Properties["processorID"].Value.ToString();
}
ManagementObjectSearcher baseboardSearcher = new ManagementObjectSearcher("root\\CIMV2", "SELECT * FROM Win32_BaseBoard");
foreach (ManagementObject queryObj in baseboardSearcher.Get())
{
//Get motherboardSerialNumber
pcInfo += queryObj["SerialNumber"].ToString();
break;
}
using (Aes myAes = Aes.Create())
{
if (chiave == "")
return false;
myAes.IV = Convert.FromBase64String("qqxNM8Li99Cwx61VX/QoWA==");
myAes.Key = Convert.FromBase64String("7/12mP3dFLGqPGjujlwBCVblURxBaBcGGQ2c2FatfP4=");
byte[] encrypted = AesCrypt.EncryptStringToBytes_Aes(pcInfo, myAes.Key, myAes.IV);
string roundtrip = AesCrypt.DecryptStringFromBytes_Aes(encrypted, myAes.Key, myAes.IV);
string decoded = "";
try
{
decoded = AesCrypt.DecryptStringFromBytes_Aes(Convert.FromBase64String(chiave), myAes.Key, myAes.IV);
}
catch (Exception ex)
{
Console.WriteLine(ex.ToString());
}
return pcInfo == decoded;
}
}
static void MostraAiuto()
{
Console.WriteLine("Utilizzo del Software gestore Programmi FANUC");
Console.WriteLine("Le funzioni pricipali sono due.");
Console.WriteLine("INVIA: caricamento di programma su CNC.");
Console.WriteLine("SCARICA: scaricamento di programma su CNC.");
Console.WriteLine();
Console.WriteLine();
Console.WriteLine("Lista dei parametri da fornire all'eseguibile\nDevono essere separati da uno spazio tra l'uno e l'altro,\nnon andare a capo e non lasciare spazi dopo l'= ");
Console.WriteLine();
Console.WriteLine("-chiave=");
Console.WriteLine(" la stringa chiave per eseguire il software.");
Console.WriteLine("-azione=");
Console.WriteLine(" l'{AZIONE} da eseguire.");
Console.WriteLine("-manufacturer=");
Console.WriteLine(" Il produttore del CNC: fanuc (default), heidenhain, siemens, mitsubishi");
Console.WriteLine("-tipo=");
Console.WriteLine(" Il tipo di connessione utilizzato dalla macchina: 3 per ethernet\n 2 per HSSB (solo Fanuc, legacy)");
Console.WriteLine("-ip=");
Console.WriteLine(" Dato per Ethernet indirizzo IP della macchina");
Console.WriteLine("-porta=");
Console.WriteLine(" Dato per Ethernet PORTA di connessione");
Console.WriteLine("-nodo=");
Console.WriteLine(" Dato per HSSB Nodo di connesione (solo Fanuc)");
Console.WriteLine("-username=");
Console.WriteLine(" Username per autenticazione (Heidenhain/Siemens)");
Console.WriteLine("-password=");
Console.WriteLine(" Password per autenticazione (Heidenhain/Siemens)");
Console.WriteLine("-compatibility");
Console.WriteLine(" Attivazione modalità compatibilità");
Console.WriteLine("-commento=");
Console.WriteLine(" Commento da applicare al programma scelto");
Console.WriteLine("-path=");
Console.WriteLine(" Path dei programmi del CNC dove vengono salvati i programmi, se non definito è impostato a 1 cioè //CNC_MEM/USER/PATH1/");
Console.WriteLine("-pathprogramma=");
Console.WriteLine(" Necessario per INVIO: Path del programma locale da caricare sul CNC");
Console.WriteLine("-workzero=");
Console.WriteLine(" Invia/Scarica anche i dati relativi al workzero offset");
Console.WriteLine("-tooloffset=");
Console.WriteLine(" Invia/Scarica anche i dati relativi al tool offset");
Console.WriteLine("-debug=");
Console.WriteLine(" Mantiene il terminale aperto alla fine delle operazioni");
Console.WriteLine("-help");
Console.WriteLine(" Mostra questo messaggio e termina");
Console.WriteLine();
Console.WriteLine("Codici Errori:");
Console.WriteLine("-100 : Licenza Errata/non valida.");
Console.WriteLine("-101 : Impossibile parsare i parametri.");
Console.WriteLine("-102 : Parametri mancanti. ");
Console.WriteLine("-103 : Codice connessione errato: 3->Ethernet 2->HSSB. ");
Console.WriteLine("-201 : Macchina non connessa. ");
Console.WriteLine("-202 : Eccezione nell'esecuzione dell'AZIONE. ");
Console.WriteLine("-203 : L'azione è fallita. ");
Console.WriteLine("-204 : Errore in fase invio/ricezione Programma CN. ");
Console.WriteLine("-205 : Errore in fase invio/ricezione tool offset data ");
Console.WriteLine("-206 : Errore in fase invio/ricezione work zero offest data");
Console.WriteLine("-207 : Posizionare il CN in MDI o EDIT per eseguire l'invio di un programma");
Console.WriteLine("-208 : Programma CN Caricato ma non settato a main.");
}
private static void parseArgomenti(string[] args)
{
List<string> arguments = new List<string>(args);
var help = arguments.Find(it => new Regex("-help.*", RegexOptions.IgnoreCase).IsMatch(it));
if (!string.IsNullOrEmpty(help))
{
inputArgs.mostraHelp = true;
return;
}
var chiave = arguments.Find(it => new Regex("-chiave=.*", RegexOptions.IgnoreCase).IsMatch(it));
inputArgs.chiave = chiave.Substring(8);
var azione = arguments.Find(it => new Regex("-azione=.*", RegexOptions.IgnoreCase).IsMatch(it));
inputArgs.azione = azione.Substring(8);
var manufacturer = arguments.Find(it => new Regex("-manufacturer=.*", RegexOptions.IgnoreCase).IsMatch(it));
if (!string.IsNullOrEmpty(manufacturer))
{
string mfgVal = manufacturer.Substring(14).Trim().ToLowerInvariant();
switch (mfgVal)
{
case "heidenhain": inputArgs.manufacturer = CncManufacturer.Heidenhain; break;
case "siemens": inputArgs.manufacturer = CncManufacturer.Siemens; break;
case "mitsubishi": inputArgs.manufacturer = CncManufacturer.Mitsubishi; break;
default: inputArgs.manufacturer = CncManufacturer.Fanuc; break;
}
}
string tipo = arguments.Find(it => new Regex("-tipo=.*", RegexOptions.IgnoreCase).IsMatch(it));
if (!string.IsNullOrEmpty(tipo))
inputArgs.tipo = Int32.Parse(tipo.Substring(6));
else inputArgs.tipo = 3;
var ip = arguments.Find(it => new Regex("-ip=.*", RegexOptions.IgnoreCase).IsMatch(it));
if (!string.IsNullOrEmpty(ip))
inputArgs.ip = ip.Substring(4);
var porta = arguments.Find(it => new Regex("-porta=.*", RegexOptions.IgnoreCase).IsMatch(it));
if (!string.IsNullOrEmpty(porta))
inputArgs.porta = porta.Substring(7);
var nodo = arguments.Find(it => new Regex("-nodo=.*", RegexOptions.IgnoreCase).IsMatch(it));
if (!string.IsNullOrEmpty(nodo))
inputArgs.nodoHssb = Int32.Parse(nodo.Substring(6));
else inputArgs.nodoHssb = -1;
var username = arguments.Find(it => new Regex("-username=.*", RegexOptions.IgnoreCase).IsMatch(it));
if (!string.IsNullOrEmpty(username))
inputArgs.username = username.Substring(10);
var password = arguments.Find(it => new Regex("-password=.*", RegexOptions.IgnoreCase).IsMatch(it));
if (!string.IsNullOrEmpty(password))
inputArgs.password = password.Substring(10);
var compatibility = arguments.Find(it => new Regex("-compatibility.*", RegexOptions.IgnoreCase).IsMatch(it));
if (!string.IsNullOrEmpty(compatibility))
inputArgs.compatibilityMode = true;
var commento = arguments.Find(it => new Regex("-commento=.*", RegexOptions.IgnoreCase).IsMatch(it));
if (!string.IsNullOrEmpty(commento))
inputArgs.commentoProgramma = commento.Substring(10);
var path = arguments.Find(it => new Regex("-path=.*", RegexOptions.IgnoreCase).IsMatch(it));
if (!string.IsNullOrEmpty(path))
inputArgs.pathCNC = Int16.Parse(path.Substring(6));
var pathprogramma = arguments.Find(it => new Regex("-pathprogramma=.*", RegexOptions.IgnoreCase).IsMatch(it));
if (!string.IsNullOrEmpty(pathprogramma))
inputArgs.pathLocaleProgramma = pathprogramma.Substring(15);
var hasToolOffestData = arguments.Find(it => new Regex("-tooloffset.*", RegexOptions.IgnoreCase).IsMatch(it));
if (!string.IsNullOrEmpty(hasToolOffestData))
inputArgs.hasToolOffsetData = true;
var hasWorkZeroOffsetData = arguments.Find(it => new Regex("-workzero.*", RegexOptions.IgnoreCase).IsMatch(it));
if (!string.IsNullOrEmpty(hasWorkZeroOffsetData))
inputArgs.hasWorkZeroOffsetData = true;
var debugMode = arguments.Find(it => new Regex("-debug.*", RegexOptions.IgnoreCase).IsMatch(it));
if (!string.IsNullOrEmpty(debugMode))
inputArgs.debugMode = true;
}
}
}

39
Properties/AssemblyInfo.cs Executable file
View file

@ -0,0 +1,39 @@
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
[assembly: InternalsVisibleTo("FanucProgramManager.Tests")]
[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")]
// Le informazioni generali relative a un assembly sono controllate dal seguente
// set di attributi. Modificare i valori di questi attributi per modificare le informazioni
// associate a un assembly.
[assembly: AssemblyTitle("FanucProgramManager")]
[assembly: AssemblyDescription("")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("")]
[assembly: AssemblyProduct("FanucProgramManager")]
[assembly: AssemblyCopyright("Copyright © 2023")]
[assembly: AssemblyTrademark("")]
[assembly: AssemblyCulture("")]
// Se si imposta ComVisible su false, i tipi in questo assembly non saranno visibili
// ai componenti COM. Se è necessario accedere a un tipo in questo assembly da
// COM, impostare su true l'attributo ComVisible per tale tipo.
[assembly: ComVisible(false)]
// Se il progetto viene esposto a COM, il GUID seguente verrà utilizzato come ID della libreria dei tipi
[assembly: Guid("1032188b-3321-42b2-87ed-343bf718caf5")]
// Le informazioni sulla versione di un assembly sono costituite dai seguenti quattro valori:
//
// Versione principale
// Versione secondaria
// Numero di build
// Revisione
//
// È possibile specificare tutti i valori oppure impostare valori predefiniti per i numeri relativi alla revisione e alla build
// usando l'asterisco '*' come illustrato di seguito:
// [assembly: AssemblyVersion("1.0.*")]
[assembly: AssemblyVersion("1.1.0.0")]
[assembly: AssemblyFileVersion("1.1.0.0")]

11569
fwlib32.cs Executable file

File diff suppressed because it is too large Load diff

12
nunit.runsettings Normal file
View file

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<RunSettings>
<NUnit>
<DefaultTestNamePattern>{m}</DefaultTestNamePattern>
</NUnit>
<!--
Filter examples:
Unit only: dotnet test FanucProgramManager.Tests --filter "Category!=Integration"
Integration only: dotnet test FanucProgramManager.Tests --filter "Category=Integration"
All: dotnet test FanucProgramManager.Tests
-->
</RunSettings>

61
packages.config Executable file
View file

@ -0,0 +1,61 @@
<?xml version="1.0" encoding="utf-8"?>
<packages>
<package id="Costura.Fody" version="5.7.0" targetFramework="net472" developmentDependency="true" />
<package id="Fody" version="6.6.4" targetFramework="net472" developmentDependency="true" />
<package id="Microsoft.Bcl.AsyncInterfaces" version="7.0.0" targetFramework="net472" />
<package id="Microsoft.NETCore.Platforms" version="7.0.2" targetFramework="net472" />
<package id="Microsoft.Win32.Primitives" version="4.3.0" targetFramework="net472" />
<package id="NETStandard.Library" version="2.0.3" targetFramework="net472" />
<package id="System.AppContext" version="4.3.0" targetFramework="net472" />
<package id="System.Buffers" version="4.5.1" targetFramework="net472" />
<package id="System.CodeDom" version="7.0.0" targetFramework="net472" />
<package id="System.Collections" version="4.3.0" targetFramework="net472" />
<package id="System.Collections.Concurrent" version="4.3.0" targetFramework="net472" />
<package id="System.Console" version="4.3.1" targetFramework="net472" />
<package id="System.Diagnostics.Debug" version="4.3.0" targetFramework="net472" />
<package id="System.Diagnostics.DiagnosticSource" version="7.0.2" targetFramework="net472" />
<package id="System.Diagnostics.Tools" version="4.3.0" targetFramework="net472" />
<package id="System.Diagnostics.Tracing" version="4.3.0" targetFramework="net472" />
<package id="System.Globalization" version="4.3.0" targetFramework="net472" />
<package id="System.Globalization.Calendars" version="4.3.0" targetFramework="net472" />
<package id="System.IO" version="4.3.0" targetFramework="net472" />
<package id="System.IO.Compression" version="4.3.0" targetFramework="net472" />
<package id="System.IO.Compression.ZipFile" version="4.3.0" targetFramework="net472" />
<package id="System.IO.FileSystem" version="4.3.0" targetFramework="net472" />
<package id="System.IO.FileSystem.Primitives" version="4.3.0" targetFramework="net472" />
<package id="System.Linq" version="4.3.0" targetFramework="net472" />
<package id="System.Linq.Expressions" version="4.3.0" targetFramework="net472" />
<package id="System.Management" version="7.0.1" targetFramework="net472" />
<package id="System.Memory" version="4.5.5" targetFramework="net472" />
<package id="System.Net.Http" version="4.3.4" targetFramework="net472" />
<package id="System.Net.Primitives" version="4.3.1" targetFramework="net472" />
<package id="System.Net.Sockets" version="4.3.0" targetFramework="net472" />
<package id="System.Numerics.Vectors" version="4.5.0" targetFramework="net472" />
<package id="System.ObjectModel" version="4.3.0" targetFramework="net472" />
<package id="System.Reflection" version="4.3.0" targetFramework="net472" />
<package id="System.Reflection.Extensions" version="4.3.0" targetFramework="net472" />
<package id="System.Reflection.Primitives" version="4.3.0" targetFramework="net472" />
<package id="System.Resources.ResourceManager" version="4.3.0" targetFramework="net472" />
<package id="System.Runtime" version="4.3.1" targetFramework="net472" />
<package id="System.Runtime.CompilerServices.Unsafe" version="6.0.0" targetFramework="net472" />
<package id="System.Runtime.Extensions" version="4.3.1" targetFramework="net472" />
<package id="System.Runtime.Handles" version="4.3.0" targetFramework="net472" />
<package id="System.Runtime.InteropServices" version="4.3.0" targetFramework="net472" />
<package id="System.Runtime.InteropServices.RuntimeInformation" version="4.3.0" targetFramework="net472" />
<package id="System.Runtime.Numerics" version="4.3.0" targetFramework="net472" />
<package id="System.Security.Cryptography.Algorithms" version="4.3.1" targetFramework="net472" />
<package id="System.Security.Cryptography.Encoding" version="4.3.0" targetFramework="net472" />
<package id="System.Security.Cryptography.Primitives" version="4.3.0" targetFramework="net472" />
<package id="System.Security.Cryptography.X509Certificates" version="4.3.2" targetFramework="net472" />
<package id="System.Text.Encoding" version="4.3.0" targetFramework="net472" />
<package id="System.Text.Encoding.Extensions" version="4.3.0" targetFramework="net472" />
<package id="System.Text.Encodings.Web" version="7.0.0" targetFramework="net472" />
<package id="System.Text.RegularExpressions" version="4.3.1" targetFramework="net472" />
<package id="System.Threading" version="4.3.0" targetFramework="net472" />
<package id="System.Threading.Tasks" version="4.3.0" targetFramework="net472" />
<package id="System.Threading.Tasks.Extensions" version="4.5.4" targetFramework="net472" />
<package id="System.Threading.Timer" version="4.3.0" targetFramework="net472" />
<package id="System.ValueTuple" version="4.5.0" targetFramework="net472" />
<package id="System.Xml.ReaderWriter" version="4.3.1" targetFramework="net472" />
<package id="System.Xml.XDocument" version="4.3.0" targetFramework="net472" />
</packages>