Compare commits

..

No commits in common. "68c5a555f3c05af75d3ba15c026ac57a557abcd7" and "60ce1c9d578ba96a0b254dd172534046876f5d7f" have entirely different histories.

24 changed files with 48 additions and 1299 deletions

View file

@ -108,80 +108,6 @@ Enhancements and known issues discovered during execution and codebase analysis.
--- ---
### ISS-012: Fagor `ReadActiveProgramAsync` / `SelectMainProgramAsync` not implemented
- **Discovered:** Code review v0.6 (2026-05-25)
- **Type:** Feature / Incomplete Implementation
- **Description:** `FagorMachine.ReadActiveProgramAsync()` e `SelectMainProgramAsync()` ritornano `CncResult.Fail("Not supported via FTP")`. Analogo a ISS-009 (Heidenhain) e ISS-010 (Siemens). Fagor espone stato runtime via DNC/OPC-UA non FTP.
- **Files:** `Cnc/Fagor/FagorMachine.cs`
- **Impact:** Low — caso d'uso corrente richiede path esplicito
- **Effort:** Substantial (richiede protocollo DNC/OPC-UA Fagor)
- **Suggested phase:** Future Fagor advanced features
---
### ISS-013: `Program.Invia` accoppia `FanucProgram` parsing a tutti i produttori
- **Discovered:** Code review v0.6 (2026-05-25)
- **Type:** Bug / Refactoring
- **Description:** `Program.Invia` crea `new FanucProgram(File.ReadAllText(...))` e chiama `GetProgramTitle()` con regex `O####` o `\d{4}` per tutti i produttori. Per Siemens/Mitsubishi/Fagor con programmi non in formato Fanuc, lancia `InvalidOperationException`. Fagor incrementa esposizione del bug.
- **Files:** `Program.cs` (linee 188-202), `FanucProgram.cs`
- **Fix:** Per non-Fanuc usare `Path.GetFileNameWithoutExtension(inputArgs.pathLocaleProgramma)` come `cncProg.Name`. Bypassare parsing Fanuc.
- **Impact:** Medium — CLI `Invia` rotta per tutti i non-Fanuc (Siemens/Mitsubishi/Fagor)
- **Effort:** Medium (1-2h) — refactor Program.Invia con switch su manufacturer
- **Suggested phase:** Future CLI cleanup phase
---
### ISS-014: `ResolvePath` null-folder NRE shared pattern (Siemens/Mitsubishi/Fagor)
- **Discovered:** Code review v0.6 (2026-05-25)
- **Type:** Bug / Defensive coding
- **Description:** `ResolvePath(folder, name)` in tutti e 3 le macchine FTP dereferenzia `folder` senza null-guard nel ramo `IsNullOrEmpty(name)`. Caller che passa `path = null` (es. `ReadProgramAsync(null)`) provoca NullReferenceException.
- **Files:** `Cnc/Siemens/SiemensMachine.cs`, `Cnc/Mitsubishi/MitsubishiMachine.cs`, `Cnc/Fagor/FagorMachine.cs`
- **Fix:** `if (folder == null) folder = _config.ProgramDirectory;` all'inizio del metodo. Pattern condiviso → considerare helper unitario.
- **Impact:** Low — null path raro in pratica, ma viola contratto API
- **Effort:** Quick (10 min) — refactor uniforme
---
### ISS-015: Encoding source-file rischio `ñ` su build Windows non-UTF8
- **Discovered:** Code review v0.6 (2026-05-25)
- **Type:** Build / Encoding
- **Description:** File sorgenti UTF-8 senza BOM. `csc.exe` su Windows con codepage 1252 (Italian) può decodificare letterali non-ASCII come Mojibake. Risolto in v0.6 (Phase 16-02) per `ñ` in `NcProgramValidator.cs` usando `ñ`. Altri file (`Program.cs` con `è`, `à` in stringhe italiane) ancora potenzialmente a rischio. Funziona se build box codepage compatibile, ma fragile.
- **Files:** `Program.cs`, `Cnc/NcProgramValidator.cs`, file `.cs` con letterali italiani
- **Fix:** (a) Aggiungere UTF-8 BOM a tutti i sorgenti, OR (b) sostituire tutti letterali non-ASCII con escape `\uXXXX`, OR (c) aggiungere `/codepage:65001` ai compile args MSBuild.
- **Impact:** Medium — silent corruption se build box ha codepage diversa
- **Effort:** Medium (audit completo file + correzione)
- **Suggested phase:** Future build hardening phase
---
### ISS-016: `WriteProgramAsync` validator riceve `path` se `program.Name` null (Siemens/Mitsubishi/Fagor)
- **Discovered:** Code review v0.6 (2026-05-25)
- **Type:** API contract / Validation
- **Description:** `_validator.Validate(program.Name ?? path, program.Content)` — se `program.Name` null, validator valida l'intero path FTP come fosse il nome programma. Errori validatore confusi.
- **Files:** `Cnc/Siemens/SiemensMachine.cs`, `Cnc/Mitsubishi/MitsubishiMachine.cs`, `Cnc/Fagor/FagorMachine.cs`
- **Fix:** Fail-fast con `CncError("WriteProgram", "Program.Name is required")` quando `program.Name == null`. Documentare in `CncProgram.cs`.
- **Impact:** Low — caller di solito imposta `Name`
- **Effort:** Quick
---
### ISS-017: `FagorFtpClient.Ping` swallow exception senza log
- **Discovered:** Code review v0.6 (2026-05-25)
- **Type:** Diagnostics
- **Description:** `Ping()` catch generico ritorna `false` senza log. Stesso pattern in Siemens/Mitsubishi. Diagnostica ridotta su problemi di connessione (timeout vs credenziali vs path mancante).
- **Files:** `Cnc/Siemens/SiemensFtpClient.cs`, `Cnc/Mitsubishi/MitsubishiFtpClient.cs`, `Cnc/Fagor/FagorFtpClient.cs`
- **Fix:** Aggiungere campo `_log` + `_log.Debug(ex, "FTP ping failed")` nel catch. Mantenere return false. Pattern condiviso.
- **Impact:** Low — diagnostica
- **Effort:** Quick
---
## Closed Issues ## Closed Issues
| ID | Summary | Phase | Commit | | ID | Summary | Phase | Commit |
@ -195,5 +121,5 @@ Enhancements and known issues discovered during execution and codebase analysis.
--- ---
*Last updated: 2026-05-25* *Last updated: 2026-05-18*
*Sources: STATE.md deferred issues + CONCERNS.md codebase analysis* *Sources: STATE.md deferred issues + CONCERNS.md codebase analysis*

View file

@ -206,25 +206,4 @@ Completed: 2026-05-18
- [x] 15-02: Replace all Console.Write/WriteLine with _log.Info; console target Info-only plain layout - [x] 15-02: Replace all Console.Write/WriteLine with _log.Info; console target Info-only plain layout
--- ---
*Roadmap updated: 2026-05-18 — v0.5 milestone complete; Phase 15 (logging) complete.*
## Milestone v0.6 — Fagor Support ✅
**Status:** Complete
**Phases:** 1 of 1 complete
**Completed:** 2026-05-25
| Phase | Name | Plans | Status | Completed |
|-------|------|-------|--------|-----------|
| 16 | fagor-ftp | 2 | ✅ Complete | 2026-05-25 |
### Phase 16: fagor-ftp ✅
**Goal:** Aggiungere supporto Fagor CNC via FTP. Riuso pattern Siemens/Mitsubishi. Refactor validator per gating per-produttore. Modelli supportati: 8060/8065/8070 nativi, 8055 con opzione Ethernet.
**Issues addressed:** —
**Completed:** 2026-05-25
**Plans:**
- [x] 16-01: Fagor FTP machine + validator gating + CLI integration + test scaffold
- [x] 16-02: Fagor fixup post-review (validator extension fix, ñ escape, lock tests Siemens/Heidenhain, factory test)
---
*Roadmap updated: 2026-05-25 — v0.6 milestone complete; Phase 16 (fagor-ftp) 16-01 + 16-02 complete.*

View file

@ -5,15 +5,15 @@
See: .paul/PROJECT.md (updated 2026-05-18) See: .paul/PROJECT.md (updated 2026-05-18)
**Core value:** Machinists transfer CNC programs to/from any controller without manual steps or internet **Core value:** Machinists transfer CNC programs to/from any controller without manual steps or internet
**Current focus:** v0.6 Fagor Support complete; ready for v0.7 planning **Current focus:** v0.5 complete — all milestones shipped; ready for v0.6 planning
## Current Position ## Current Position
Milestone: v0.6 Fagor Support — **Complete** Milestone: v0.5 Operational Quality — **Complete**
Phase: 16 of 16 (fagor-ftp) — Complete Phase: 15 of 15 (logging) — Complete
Plan: 16-02 complete Plan: 15-02 complete
Status: Milestone complete — ready for v0.7 planning Status: Milestone complete — ready for v0.6 planning
Last activity: 2026-05-25 — Phase 16-02 fixup complete (post-review); v0.6 milestone complete Last activity: 2026-05-18 — Phase 15 complete; v0.5 milestone complete
Progress: Progress:
- Milestone v0.1: [██████████] 100% (complete) - Milestone v0.1: [██████████] 100% (complete)
@ -21,55 +21,15 @@ Progress:
- Milestone v0.3: [██████████] 100% (complete) - Milestone v0.3: [██████████] 100% (complete)
- Milestone v0.4: [██████████] 100% (complete) - Milestone v0.4: [██████████] 100% (complete)
- Milestone v0.5: [██████████] 100% (complete) - Milestone v0.5: [██████████] 100% (complete)
- Milestone v0.6: [██████████] 100% (complete)
- Phase 15: [██████████] 100% - Phase 15: [██████████] 100%
- Phase 16: [██████████] 100%
## Loop Position ## Loop Position
``` ```
PLAN ──▶ APPLY ──▶ UNIFY PLAN ──▶ APPLY ──▶ UNIFY
✓ ✓ ✓ [Loop complete — v0.6 milestone complete] ✓ ✓ ✓ [Loop complete — v0.5 milestone complete]
``` ```
## What Was Built (v0.6 Fagor Support — complete)
### Phase 16 — Fagor FTP
- `Cnc/Models/CncManufacturer.cs` — aggiunto `Fagor` all'enum
- `Cnc/Fagor/FagorConnectionConfig.cs` — nuovo, ProgramDirectory `/Users/Prg/`, ProgramExtension `.nc` default
- `Cnc/Fagor/IFagorFtpClient.cs` — nuovo, interfaccia mirror Siemens
- `Cnc/Fagor/FagorFtpClient.cs` — nuovo, FtpWebRequest + ASCII, mirror Siemens 1:1
- `Cnc/Fagor/FagorMachine.cs` — nuovo, implementa `ICncMachine` con `ResolvePath` adattato per `ProgramExtension` configurabile
- `Cnc/CncMachineFactory.cs` — case `Fagor` con porta default 21
- `Cnc/NcProgramValidator.cs` — refactor: helper `IsRestrictiveDialect()` (Fanuc+Mitsubishi); check generic (block 80, no tab, no lowercase, nested parens, comment 32) gated per-produttore. Case `Fagor` aggiunto in `ValidateName` (max 24 char, no `ñ`, solo lettere/cifre/spazi) e `ValidateForbiddenChars` (nessuna restrizione contenuto)
- `ArgParser.cs` — case `"fagor"` aggiunto
- `Program.cs` — help text aggiornato (manufacturer/username/password)
- `NcProgramManager.csproj` — 4 `<Compile Include>` per Cnc\Fagor\*.cs
- `NcProgramManager.Tests/Unit/FagorMachineTests.cs` — nuovo, 12 unit test con `Mock<IFagorFtpClient>` + 2 test con validator reale (`ñ`, lunghezza)
- `NcProgramManager.Tests/Integration/FagorMachineIntegrationTests.cs` — nuovo, 5 stub-based + 1 hardware skip (`FAGOR_TEST_IP`)
- `NcProgramManager.Tests/Unit/NcProgramValidatorTests.cs` — 8 nuovi test Fagor; 3 test esistenti spostati da Siemens a Fanuc dopo refactor gating
- `NcProgramManager.Tests/NcProgramManager.Tests.csproj` — 2 `<Compile Include>` aggiunti
- `README.md` — sezione "Controller Fagor" in italiano: modelli supportati (8060/8065/8070), non supportati (8055 base, 8025-8050), regole nome programma, env var test
Modelli supportati: Fagor 8060, 8065, 8070 (FTP nativo); Fagor 8055 (richiede opzione Ethernet).
Modelli non supportati: 8025/8030/8040/8050 (solo RS232, modulo seriale non implementato).
### Phase 16-02 — Fagor Fixup (post-review)
Fix derivati da code review v0.6:
- `Cnc/NcProgramValidator.cs` — Fagor `ValidateName`: split stem/extension via `LastIndexOf('.')`; whitelist letter/digit/space applicata solo allo stem (consente `.nc` o altre estensioni). Letterali `ñ` sostituiti con escape Unicode `ñ` (codepage safety)
- `NcProgramManager.Tests/Unit/NcProgramValidatorTests.cs`:
- 3 nuovi test lock Siemens (lowercase/tab/long-block ora esenti)
- 3 nuovi test lock Heidenhain (lowercase/tab/long-block ora esenti)
- 4 nuovi test Fagor (`.nc` extension success, `.pim` extension success, stem con simbolo fail, `ñ` escape)
- 1 test esistente Fagor `ñ` migrato a escape `ñ`
- `NcProgramManager.Tests/Unit/CncMachineFactoryTests.cs` — aggiunto `Create_Fagor_ReturnsFagorMachine`
- `.paul/ISSUES.md` — aggiunte ISS-012..ISS-017 da review findings (non risolti in v0.6, deferiti)
Issue follow-up aperti: ISS-012, ISS-013, ISS-014, ISS-015, ISS-016, ISS-017
## What Was Built (v0.5 Operational Quality — complete) ## What Was Built (v0.5 Operational Quality — complete)
### Phase 15 — Logging ### Phase 15 — Logging
@ -214,7 +174,7 @@ Key open items:
None — v0.5 complete. None — v0.5 complete.
### Git State ### Git State
Last commit: `60ce1c9` — feat(15-logging): NLog structured logging — all output through NLog (v0.5 complete) Last commit: (Phase 15 commit — see below after git commit)
LicenseGenerator repo: commit `2ef68dd` — feat: initial LicenseGenerator LicenseGenerator repo: commit `2ef68dd` — feat: initial LicenseGenerator
Branch: main Branch: main

View file

@ -1,14 +1,14 @@
{ {
"name": "NcProgramManager", "name": "NcProgramManager",
"version": "0.6.0", "version": "0.5.0",
"milestone": { "milestone": {
"name": "v0.6 Fagor Support", "name": "v0.5 Operational Quality",
"version": "0.6.0", "version": "0.5.0",
"status": "complete" "status": "complete"
}, },
"phase": { "phase": {
"number": 16, "number": 15,
"name": "fagor-ftp", "name": "logging",
"status": "complete" "status": "complete"
}, },
"loop": { "loop": {
@ -17,7 +17,7 @@
}, },
"timestamps": { "timestamps": {
"created_at": "2026-05-13T00:00:00Z", "created_at": "2026-05-13T00:00:00Z",
"updated_at": "2026-05-25T00:00:00Z" "updated_at": "2026-05-18T00:00:00Z"
}, },
"satellite": { "satellite": {
"groom": true "groom": true

View file

@ -1,60 +0,0 @@
# Phase 16-01 — Fagor FTP Support
## Goal
Aggiungere supporto Fagor CNC via FTP a `NcProgramManager`. Riusare pattern Siemens/Mitsubishi (FtpWebRequest + ASCII). Refactor validator per gating per-produttore dei check generic (block 80, no tab, no lowercase, nested parens, comment 32) — applicati ora solo a Fanuc+Mitsubishi (Fagor esente). Estendere CLI con `-manufacturer=fagor`.
## Modelli supportati
| Modello | Supporto | Note |
|---------|----------|------|
| 8055 | Solo con opzione Ethernet | Richiede attivazione server FTP via parametri macchina |
| 8060 | Sì | FTP nativo (Windows-based) |
| 8065 | Sì | FTP nativo (Windows-based) |
| 8070 | Sì | FTP nativo (Windows embedded) |
## Modelli non supportati
| Modello | Motivo |
|---------|--------|
| 8055 base (senza Ethernet) | Solo RS232; protocollo seriale non implementato |
| 8025 / 8030 / 8040 / 8050 | Legacy, esclusivamente RS232 |
## Files creati
- `Cnc/Fagor/FagorConnectionConfig.cs`
- `Cnc/Fagor/IFagorFtpClient.cs`
- `Cnc/Fagor/FagorFtpClient.cs`
- `Cnc/Fagor/FagorMachine.cs`
- `NcProgramManager.Tests/Unit/FagorMachineTests.cs`
- `NcProgramManager.Tests/Integration/FagorMachineIntegrationTests.cs`
## Files modificati
- `Cnc/Models/CncManufacturer.cs` — enum aggiunto `Fagor`
- `Cnc/CncMachineFactory.cs` — case `Fagor`
- `Cnc/NcProgramValidator.cs` — refactor gating + case Fagor
- `ArgParser.cs` — case `"fagor"`
- `Program.cs` — help text
- `NcProgramManager.csproj` — 4 `<Compile Include>`
- `NcProgramManager.Tests/NcProgramManager.Tests.csproj` — 2 `<Compile Include>`
- `NcProgramManager.Tests/Unit/NcProgramValidatorTests.cs` — 3 test esistenti spostati Siemens → Fanuc, 8 nuovi Fagor
- `README.md` — sezione Controller Fagor
## Regole validatore Fagor
- Nome programma: max 24 caratteri, no `ñ`, solo lettere/cifre/spazi
- Contenuto programma: nessuna restrizione (Fagor permette ISO + linguaggio alto livello)
- Check generic (block 80, tab, lowercase, nested parens, comment 32) NON applicati a Fagor
## Funzioni non supportate via FTP
- `ReadActiveProgramAsync` — richiede DNC/OPC-UA
- `SelectMainProgramAsync` — richiede DNC
- `ReadAsync` ritorna `MachineSnapshot` con valori `Unknown`
## Variabili d'ambiente test integration
- `FAGOR_TEST_IP` — indirizzo IP macchina (test skippato se assente)
- `FAGOR_USER` — username FTP (default `user`)
- `FAGOR_PASS` — password FTP (default `pass`)

View file

@ -1,28 +0,0 @@
# Phase 16-01 — Summary
**Status:** Complete
**Completed:** 2026-05-25
## Risultato
Supporto Fagor CNC via FTP integrato in `NcProgramManager`. Pattern Siemens/Mitsubishi riutilizzato 1:1 con adattamenti per `ProgramExtension` configurabile. Validator refactored con `IsRestrictiveDialect()` helper per gating per-produttore.
## Decisioni chiave
1. Check generic (block 80, tab, lowercase, nested parens, comment 32) applicati solo a Fanuc+Mitsubishi. Heidenhain/Siemens/Fagor esentati. Refactor immediato.
2. Modelli 8025-8050 e 8055 base esclusi (solo RS232, modulo seriale non implementato). Documentato in README.
3. Default `ProgramDirectory = /Users/Prg/`, `ProgramExtension = .nc` per Fagor 8060/8065/8070. Configurabili tramite `FagorConnectionConfig`.
4. Nessun `FagorFtpStub` dedicato — riusato `FtpServerStub` generico esistente per round-trip integration test.
5. Validator nome Fagor: max 24 char, no `ñ`, solo lettere/cifre/spazi (verificato da manuali Fagor online).
## Test coverage
- 12 unit test su `FagorMachine` (10 con mock + 2 con validator reale)
- 5 integration test stub-based + 1 hardware skip
- 8 unit test nuovi su validator Fagor
- 3 test esistenti aggiornati per refactor gating (Siemens → Fanuc)
## Issue follow-up potenziali
- ISS-009 candidate: monitoraggio falsi positivi validator su produttori esentati (Heidenhain/Siemens/Fagor). Da aprire se field reports si lamentano.
- ISS-010 candidate: `ProgramExtension` `.nc` non verificato vs `.pim`/`.pit` su tutti i firmware Fagor 8055/8060.

View file

@ -1,56 +0,0 @@
# Phase 16-02 — Fagor Fixup (Post-Review)
## Origine
Code review post-implementazione v0.6 Fagor Support ha identificato:
**Critical:**
1. `NcProgramValidator` case Fagor rifiuta `.` nel nome → default `ProgramExtension = ".nc"` contraddice regola validator
2. (Pre-existing, deferito) `Program.Invia` accoppia `FanucProgram` parsing a tutti i produttori
**Major:**
1. (Deferito) `ResolvePath` NRE su null folder
2. (Deferito) Validator riceve `path` se `program.Name` null
3. `CncMachineFactoryTests` manca case Fagor
4. Behavior change Heidenhain/Siemens (esenzione check generic) non lockato in test
5. Letterali `ñ` in `NcProgramValidator.cs` rischio codepage Windows non-UTF8
## Scope
Fix immediato per:
- Critical #1 — bug introdotto v0.6
- Major #3 — test mancante
- Major #4 — lock comportamento con test
- Major #5 — escape `ñ``ñ`
Deferiti a ISS:
- Critical #2 → ISS-013
- Major #1 → ISS-014
- Major #2 → ISS-016
## Tasks
1. Riscrittura case Fagor in `ValidateName`: split stem/extension, whitelist su stem
2. Escape `'ñ'``'ñ'` (codice + messaggio errore)
3. 4 nuovi test Fagor coverage estensioni
4. 6 nuovi test lock Siemens/Heidenhain
5. `Create_Fagor_ReturnsFagorMachine` in `CncMachineFactoryTests`
6. ISSUES.md updated con ISS-012..ISS-017
## Files modificati
- `Cnc/NcProgramValidator.cs`
- `NcProgramManager.Tests/Unit/NcProgramValidatorTests.cs`
- `NcProgramManager.Tests/Unit/CncMachineFactoryTests.cs`
- `.paul/ISSUES.md`
- `.paul/STATE.md`
- `.paul/ROADMAP.md`
- `.paul/paul.json`
- `.paul/phases/16-fagor-ftp/16-02-PLAN.md` (questo file)
- `.paul/phases/16-fagor-ftp/16-02-SUMMARY.md`
## Vincoli
- Nessuna modifica `.csproj` (file già listati in v0.6 16-01)
- Pattern Siemens preservato come reference (no cross-impact)
- Tests esistenti pre-refactor preservati o migrati esplicitamente

View file

@ -1,39 +0,0 @@
# Phase 16-02 — Summary
**Status:** Complete
**Completed:** 2026-05-25
## Risultato
Critical bug Fagor name validator vs `.nc` extension risolto. `ñ` literal escapato. Behavior change Siemens/Heidenhain lockato con 6 test nuovi. Factory test Fagor aggiunto.
## Issue identificati ma deferiti (ISS-012..ISS-017)
| ID | Tema | Effort |
|----|------|--------|
| ISS-012 | Fagor `ReadActiveProgramAsync`/`SelectMainProgram` non implementate | Substantial |
| ISS-013 | `Program.Invia` accoppia `FanucProgram` a tutti i produttori | Medium 1-2h |
| ISS-014 | `ResolvePath` null-folder NRE pattern condiviso | Quick 10min |
| ISS-015 | Source file encoding rischio codepage Windows | Medium |
| ISS-016 | Validator riceve `path` se `program.Name` null | Quick |
| ISS-017 | `FagorFtpClient.Ping` swallow senza log | Quick |
## Test coverage v0.6 finale
- 12 unit test `FagorMachine` (10 mock + 2 validator reale)
- 6 integration test Fagor (5 stub + 1 hardware skip)
- 12 nuovi unit test `NcProgramValidator` Fagor (8 originali + 4 fixup)
- 6 nuovi unit test lock Siemens/Heidenhain
- 1 test factory Fagor
- 3 test pre-existing migrati Siemens→Fanuc (16-01)
## Build verification
Non eseguita in ambiente Linux (no dotnet toolchain). Compilazione e run test richiede ambiente Windows con MSBuild/VS o `dotnet build`. Sintassi C# 7.3 verificata manualmente.
## Decisioni chiave
1. Nome Fagor: stem alphanumerico + estensione separata. Default `.nc` compatibile.
2. `ñ` escape su tutti i sorgenti per `ñ` (codice + test + messaggi)
3. Behavior change validator gating Siemens/Heidenhain confermato come voluto. Lockato con 6 test esplicit.
4. CLI bug Fanuc-parsing (Critical #2) deferito a ISS-013 — pre-existing, non Fagor-specific, fix in phase separata.

View file

@ -31,7 +31,6 @@ namespace NcProgramManager
case "heidenhain": inputArgs.manufacturer = CncManufacturer.Heidenhain; break; case "heidenhain": inputArgs.manufacturer = CncManufacturer.Heidenhain; break;
case "siemens": inputArgs.manufacturer = CncManufacturer.Siemens; break; case "siemens": inputArgs.manufacturer = CncManufacturer.Siemens; break;
case "mitsubishi": inputArgs.manufacturer = CncManufacturer.Mitsubishi; break; case "mitsubishi": inputArgs.manufacturer = CncManufacturer.Mitsubishi; break;
case "fagor": inputArgs.manufacturer = CncManufacturer.Fagor; break;
default: inputArgs.manufacturer = CncManufacturer.Fanuc; break; default: inputArgs.manufacturer = CncManufacturer.Fanuc; break;
} }
} }

View file

@ -1,5 +1,4 @@
using System; using System;
using NcProgramManager.Cnc.Fagor;
using NcProgramManager.Cnc.Fanuc; using NcProgramManager.Cnc.Fanuc;
using NcProgramManager.Cnc.Heidenhain; using NcProgramManager.Cnc.Heidenhain;
using NcProgramManager.Cnc.Mitsubishi; using NcProgramManager.Cnc.Mitsubishi;
@ -45,16 +44,6 @@ namespace NcProgramManager.Cnc
Password = args.password Password = args.password
}); });
case CncManufacturer.Fagor:
return new FagorMachine(new FagorConnectionConfig
{
Name = "Fagor",
IpAddress = args.ip,
Port = string.IsNullOrEmpty(args.porta) ? 21 : int.Parse(args.porta),
Username = args.username,
Password = args.password
});
default: // Fanuc default: // Fanuc
return new FanucMachine(new FanucConnectionConfig return new FanucMachine(new FanucConnectionConfig
{ {

View file

@ -1,18 +0,0 @@
using System;
namespace NcProgramManager.Cnc.Fagor
{
public sealed class FagorConnectionConfig
{
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>Directory dei programmi sul server FTP Fagor. Default valido per 8060/8065/8070.</summary>
public string ProgramDirectory { get; set; } = "/Users/Prg/";
/// <summary>Estensione applicata ai nomi programma senza estensione. Default `.nc` per Fagor 8060/8065/8070.</summary>
public string ProgramExtension { get; set; } = ".nc";
public TimeSpan RequestTimeout { get; set; } = TimeSpan.FromSeconds(10);
}
}

View file

@ -1,71 +0,0 @@
using System;
using System.IO;
using System.Net;
using System.Text;
namespace NcProgramManager.Cnc.Fagor
{
/// <summary>Client FTP per trasferimento programmi Fagor CNC.</summary>
internal sealed class FagorFtpClient : IFagorFtpClient
{
private readonly FagorConnectionConfig _config;
public FagorFtpClient(FagorConnectionConfig config)
{
_config = config ?? throw new ArgumentNullException("config");
}
/// <summary>Verifica raggiungibilità server FTP listando la directory programmi.</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>Scarica file dal Fagor in stringa.</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>Carica contenuto stringa su Fagor.</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

@ -1,218 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using NLog;
using NcProgramManager.Cnc;
using NcProgramManager.Cnc.Models;
namespace NcProgramManager.Cnc.Fagor
{
/// <summary>
/// Fagor CNC machine — trasferimento programmi via FTP standard.
/// Supportato su Fagor 8060/8065/8070 con server FTP integrato e su 8055 con opzione Ethernet attiva.
/// </summary>
public sealed class FagorMachine : ICncMachine
{
private static readonly Logger _log = LogManager.GetCurrentClassLogger();
private readonly FagorConnectionConfig _config;
private readonly IFagorFtpClient _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 FagorMachine(FagorConnectionConfig config)
{
_config = config ?? throw new ArgumentNullException("config");
_ftp = new FagorFtpClient(config);
_validator = new NcProgramValidator(CncManufacturer.Fagor);
}
internal FagorMachine(FagorConnectionConfig config, IFagorFtpClient ftpClient,
INcProgramValidator validator = null)
{
_config = config ?? throw new ArgumentNullException("config");
_ftp = ftpClient ?? throw new ArgumentNullException("ftpClient");
_validator = validator ?? new NcProgramValidator(CncManufacturer.Fagor);
}
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 { /* cleanup — intentionally swallowed */ }
try { _gate.Dispose(); } catch { /* cleanup — intentionally swallowed */ }
}
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, "FagorMachine", ex.Message));
return CncResult<T>.Fail(_errors.ToArray());
}
}, ct).ConfigureAwait(false);
}
finally { _gate.Release(); }
}
private void EnsureConnected()
{
if (_state != ConnectionState.Connected)
throw new InvalidOperationException("FagorMachine 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 (Exception ex) { _log.Warn(ex, "StateChanged handler threw"); }
}
}
/// <summary>Costruisce path FTP completo. Se name è fornito, append `_config.ProgramExtension` se manca estensione.</summary>
private string ResolvePath(string folder, string name)
{
if (string.IsNullOrEmpty(name))
return folder.Contains("/") ? folder : _config.ProgramDirectory + folder + _config.ProgramExtension;
string dir = folder ?? _config.ProgramDirectory;
if (!dir.EndsWith("/")) dir += "/";
string ext = name.Contains(".") ? "" : _config.ProgramExtension;
return dir + name + ext;
}
}
}

View file

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

View file

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

View file

@ -45,39 +45,34 @@ namespace NcProgramManager.Cnc
errors.Add(new ValidationError(1, "First line must be O#### program number", ValidationSeverity.Error)); errors.Add(new ValidationError(1, "First line must be O#### program number", ValidationSeverity.Error));
} }
bool restrictive = IsRestrictiveDialect();
for (int i = 0; i < lines.Length; i++) for (int i = 0; i < lines.Length; i++)
{ {
int lineNum = i + 1; int lineNum = i + 1;
string line = lines[i]; string line = lines[i];
if (restrictive) // 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++)
{ {
// Tab check if (char.IsLower(line[ci]))
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;
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);
} }
// 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 // Forbidden chars per manufacturer
ValidateForbiddenChars(line, lineNum, errors); ValidateForbiddenChars(line, lineNum, errors);
} }
@ -94,12 +89,6 @@ namespace NcProgramManager.Cnc
// ── private helpers ─────────────────────────────────────────────────── // ── private helpers ───────────────────────────────────────────────────
private bool IsRestrictiveDialect()
{
return _manufacturer == CncManufacturer.Fanuc
|| _manufacturer == CncManufacturer.Mitsubishi;
}
private void ValidateName(string name, List<ValidationError> errors) private void ValidateName(string name, List<ValidationError> errors)
{ {
if (string.IsNullOrEmpty(name)) return; if (string.IsNullOrEmpty(name)) return;
@ -127,33 +116,6 @@ namespace NcProgramManager.Cnc
string.Format("Program name '{0}' must end with .H", name), string.Format("Program name '{0}' must end with .H", name),
ValidationSeverity.Error)); ValidationSeverity.Error));
break; break;
case CncManufacturer.Fagor:
// Estrai stem ed estensione (solo ultima estensione conta)
int dotIdx = name.LastIndexOf('.');
string stem = dotIdx >= 0 ? name.Substring(0, dotIdx) : name;
// Lunghezza: regola applicata sul nome completo (incluso eventuale .ext)
if (name.Length > 24)
errors.Add(new ValidationError(0,
string.Format("Nome programma '{0}' supera 24 caratteri", name),
ValidationSeverity.Error));
if (name.IndexOf('\u00f1') >= 0)
errors.Add(new ValidationError(0,
"Carattere '\u00f1' non ammesso nel nome programma Fagor",
ValidationSeverity.Error));
// Stem deve contenere solo lettere/cifre/spazi
for (int i = 0; i < stem.Length; i++)
{
char c = stem[i];
if (!(char.IsLetterOrDigit(c) || c == ' '))
{
errors.Add(new ValidationError(0,
string.Format("Carattere '{0}' non ammesso nel nome programma Fagor (solo lettere/cifre/spazi)", c),
ValidationSeverity.Error));
break;
}
}
break;
} }
} }
@ -237,10 +199,6 @@ namespace NcProgramManager.Cnc
ValidationSeverity.Error)); ValidationSeverity.Error));
} }
break; break;
case CncManufacturer.Fagor:
// Fagor: nessuna restrizione su contenuto programma (ISO + linguaggio alto livello)
break;
} }
} }

View file

@ -1,127 +0,0 @@
using System;
using NUnit.Framework;
using NcProgramManager.Cnc.Models;
using NcProgramManager.Cnc.Fagor;
using NcProgramManager.Tests.Integration.Stubs;
namespace NcProgramManager.Tests.Integration
{
[TestFixture]
[Category("Integration")]
public class FagorMachineIntegrationTests
{
private FtpServerStub _stub;
private FagorMachine _machine;
private const string ProgramDir = "/Users/Prg/";
[SetUp]
public void SetUp()
{
_stub = new FtpServerStub();
var config = new FagorConnectionConfig
{
Name = "FagorIntegration",
IpAddress = "127.0.0.1",
Port = _stub.Port,
Username = "user",
Password = "pass",
ProgramDirectory = ProgramDir,
ProgramExtension = ".nc",
RequestTimeout = TimeSpan.FromSeconds(5)
};
_machine = new FagorMachine(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();
string content = "G0 X0 Y0\nM30";
var prog = new CncProgram { Name = "PROG1", Content = content };
var writeResult = _machine.WriteProgramAsync(ProgramDir, prog).GetAwaiter().GetResult();
Assert.That(writeResult.Success, Is.True);
// ProgramExtension ".nc" applied by FagorMachine
string storedPath = ProgramDir + "PROG1.nc";
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 + "PROG2.nc";
_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.nc").GetAwaiter().GetResult();
Assert.That(result.Success, Is.False);
}
[Test]
public void WriteProgramAsync_InvalidName_ValidationFails()
{
_machine.ConnectAsync().GetAwaiter().GetResult();
// Fagor name max 24 chars — 25 'A' triggers validator failure
var prog = new CncProgram { Name = new string('A', 25), Content = "G00 X10\nM30" };
var result = _machine.WriteProgramAsync(ProgramDir, prog).GetAwaiter().GetResult();
Assert.That(result.Success, Is.False);
}
[Test]
[Category("Hardware")]
public void Hardware_ConnectAsync_RealMachine()
{
string ip = HardwareTestHelper.GetIpOrSkip("FAGOR_TEST_IP");
var config = new FagorConnectionConfig
{
Name = "FagorHardware",
IpAddress = ip,
Port = 21,
Username = Environment.GetEnvironmentVariable("FAGOR_USER") ?? "user",
Password = Environment.GetEnvironmentVariable("FAGOR_PASS") ?? "pass",
ProgramDirectory = "/Users/Prg/",
ProgramExtension = ".nc",
RequestTimeout = TimeSpan.FromSeconds(10)
};
using (var machine = new FagorMachine(config))
{
var result = machine.ConnectAsync().GetAwaiter().GetResult();
Assert.That(result.Success, Is.True, "ConnectAsync failed: " + result.Error);
}
}
}
}

View file

@ -61,14 +61,12 @@
<Compile Include="Unit\SiemensMachineTests.cs" /> <Compile Include="Unit\SiemensMachineTests.cs" />
<Compile Include="Unit\MitsubishiMachineTests.cs" /> <Compile Include="Unit\MitsubishiMachineTests.cs" />
<Compile Include="Unit\HeidenhainMachineTests.cs" /> <Compile Include="Unit\HeidenhainMachineTests.cs" />
<Compile Include="Unit\FagorMachineTests.cs" />
<Compile Include="Integration\Stubs\FtpServerStub.cs" /> <Compile Include="Integration\Stubs\FtpServerStub.cs" />
<Compile Include="Integration\Stubs\Lsv2ServerStub.cs" /> <Compile Include="Integration\Stubs\Lsv2ServerStub.cs" />
<Compile Include="Integration\HardwareTestHelper.cs" /> <Compile Include="Integration\HardwareTestHelper.cs" />
<Compile Include="Integration\SiemensMachineIntegrationTests.cs" /> <Compile Include="Integration\SiemensMachineIntegrationTests.cs" />
<Compile Include="Integration\MitsubishiMachineIntegrationTests.cs" /> <Compile Include="Integration\MitsubishiMachineIntegrationTests.cs" />
<Compile Include="Integration\HeidenhainMachineIntegrationTests.cs" /> <Compile Include="Integration\HeidenhainMachineIntegrationTests.cs" />
<Compile Include="Integration\FagorMachineIntegrationTests.cs" />
<Compile Include="Unit\LicenseFileTests.cs" /> <Compile Include="Unit\LicenseFileTests.cs" />
</ItemGroup> </ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" /> <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />

View file

@ -6,7 +6,6 @@ using NcProgramManager.Cnc.Heidenhain;
using NcProgramManager.Cnc.Mitsubishi; using NcProgramManager.Cnc.Mitsubishi;
using NcProgramManager.Cnc.Models; using NcProgramManager.Cnc.Models;
using NcProgramManager.Cnc.Siemens; using NcProgramManager.Cnc.Siemens;
using NcProgramManager.Cnc.Fagor;
namespace NcProgramManager.Tests.Unit namespace NcProgramManager.Tests.Unit
{ {
@ -82,18 +81,6 @@ namespace NcProgramManager.Tests.Unit
} }
} }
[Test]
public void Create_Fagor_ReturnsFagorMachine()
{
var args = BaseArgs();
args.manufacturer = CncManufacturer.Fagor;
using (var m = CncMachineFactory.Create(args))
{
Assert.That(m, Is.InstanceOf<FagorMachine>());
}
}
[Test] [Test]
public void Create_FanucHssbNode_TransportIsHssb() public void Create_FanucHssbNode_TransportIsHssb()
{ {

View file

@ -1,212 +0,0 @@
using System;
using NUnit.Framework;
using Moq;
using NcProgramManager.Cnc;
using NcProgramManager.Cnc.Models;
using NcProgramManager.Cnc.Fagor;
namespace NcProgramManager.Tests.Unit
{
[TestFixture]
public class FagorMachineTests
{
private FagorConnectionConfig _config;
private Mock<IFagorFtpClient> _ftpMock;
private Mock<INcProgramValidator> _validatorMock;
private FagorMachine _machine;
[SetUp]
public void SetUp()
{
_config = new FagorConnectionConfig
{
Name = "TestFagor",
IpAddress = "192.168.1.2",
Port = 21,
ProgramDirectory = "/Users/Prg/",
ProgramExtension = ".nc"
};
_ftpMock = new Mock<IFagorFtpClient>(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 FagorMachine(_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("/Users/Prg/PROG1.nc")).Returns("CONTENT");
_machine.ConnectAsync().GetAwaiter().GetResult();
var result = _machine.ReadProgramAsync("/Users/Prg/PROG1.nc").GetAwaiter().GetResult();
Assert.That(result.Success, Is.True);
Assert.That(result.Value, Is.EqualTo("CONTENT"));
_ftpMock.Verify(f => f.DownloadFile("/Users/Prg/PROG1.nc"), Times.Once);
}
[Test]
public void ReadProgramAsync_ShortName_ResolvesWithDirectoryAndExtension()
{
_ftpMock.Setup(f => f.Ping()).Returns(true);
_ftpMock.Setup(f => f.DownloadFile("/Users/Prg/PROG1.nc")).Returns("DATA");
_machine.ConnectAsync().GetAwaiter().GetResult();
_machine.ReadProgramAsync("PROG1").GetAwaiter().GetResult();
_ftpMock.Verify(f => f.DownloadFile("/Users/Prg/PROG1.nc"), 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 = "PROG1", Content = "CONTENT" };
var result = _machine.WriteProgramAsync("/Users/Prg/", 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 = "PROG1", Content = "" };
var result = _machine.WriteProgramAsync("/Users/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("/Users/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("/Users/Prg/PROG1.nc"));
_machine.ConnectAsync().GetAwaiter().GetResult();
var result = _machine.DeleteProgramAsync("/Users/Prg/PROG1.nc").GetAwaiter().GetResult();
Assert.That(result.Success, Is.True);
_ftpMock.Verify(f => f.DeleteFile("/Users/Prg/PROG1.nc"), Times.Once);
}
// ReadActiveProgramAsync
[Test]
public void ReadActiveProgramAsync_ReturnsFailNotSupported()
{
var result = _machine.ReadActiveProgramAsync().GetAwaiter().GetResult();
Assert.That(result.Success, Is.False);
}
// SelectMainProgramAsync
[Test]
public void SelectMainProgramAsync_ReturnsFailNotSupported()
{
var result = _machine.SelectMainProgramAsync("/Users/Prg/PROG1.nc").GetAwaiter().GetResult();
Assert.That(result.Success, Is.False);
}
// Validator-real tests (NcProgramValidator with CncManufacturer.Fagor)
[Test]
public void WriteProgramAsync_NameTooLong_ValidationFails()
{
var realMachine = new FagorMachine(_config, _ftpMock.Object,
new NcProgramValidator(CncManufacturer.Fagor));
try
{
_ftpMock.Setup(f => f.Ping()).Returns(true);
realMachine.ConnectAsync().GetAwaiter().GetResult();
var prog = new CncProgram { Name = new string('A', 25), Content = "G00 X10" };
var result = realMachine.WriteProgramAsync("/Users/Prg/", prog).GetAwaiter().GetResult();
Assert.That(result.Success, Is.False);
_ftpMock.Verify(f => f.UploadFile(It.IsAny<string>(), It.IsAny<string>()), Times.Never);
}
finally { realMachine.Dispose(); }
}
[Test]
public void WriteProgramAsync_NameContainsTilde_ValidationFails()
{
var realMachine = new FagorMachine(_config, _ftpMock.Object,
new NcProgramValidator(CncManufacturer.Fagor));
try
{
_ftpMock.Setup(f => f.Ping()).Returns(true);
realMachine.ConnectAsync().GetAwaiter().GetResult();
var prog = new CncProgram { Name = "MAñANA", Content = "G00 X10" };
var result = realMachine.WriteProgramAsync("/Users/Prg/", prog).GetAwaiter().GetResult();
Assert.That(result.Success, Is.False);
_ftpMock.Verify(f => f.UploadFile(It.IsAny<string>(), It.IsAny<string>()), Times.Never);
}
finally { realMachine.Dispose(); }
}
}
}

View file

@ -32,8 +32,8 @@ namespace NcProgramManager.Tests.Unit
[Test] [Test]
public void Validate_LowercaseLetter_ReturnsError() public void Validate_LowercaseLetter_ReturnsError()
{ {
var v = new NcProgramValidator(CncManufacturer.Fanuc); var v = new NcProgramValidator(CncManufacturer.Siemens);
var r = v.Validate("O1234", "O1234\ng00 X10"); var r = v.Validate("PROG.MPF", "PROG.MPF\ng00 X10");
Assert.That(r.Success, Is.False); Assert.That(r.Success, Is.False);
Assert.That(HasErrorContaining(r, "Lowercase"), Is.True); Assert.That(HasErrorContaining(r, "Lowercase"), Is.True);
} }
@ -41,16 +41,16 @@ namespace NcProgramManager.Tests.Unit
[Test] [Test]
public void Validate_NoLowercase_NoLowercaseError() public void Validate_NoLowercase_NoLowercaseError()
{ {
var v = new NcProgramValidator(CncManufacturer.Fanuc); var v = new NcProgramValidator(CncManufacturer.Siemens);
var r = v.Validate("O1234", "O1234\nG00 X10 Y20"); var r = v.Validate("PROG.MPF", "G00 X10 Y20");
Assert.That(HasErrorContaining(r, "Lowercase"), Is.False); Assert.That(HasErrorContaining(r, "Lowercase"), Is.False);
} }
[Test] [Test]
public void Validate_TabCharacter_ReturnsError() public void Validate_TabCharacter_ReturnsError()
{ {
var v = new NcProgramValidator(CncManufacturer.Fanuc); var v = new NcProgramValidator(CncManufacturer.Siemens);
var r = v.Validate("O1234", "O1234\nG00\tX10"); var r = v.Validate("PROG.MPF", "G00\tX10");
Assert.That(r.Success, Is.False); Assert.That(r.Success, Is.False);
Assert.That(HasErrorContaining(r, "Tab"), Is.True); Assert.That(HasErrorContaining(r, "Tab"), Is.True);
} }
@ -223,32 +223,6 @@ namespace NcProgramManager.Tests.Unit
Assert.That(HasErrorContaining(r, "First line"), Is.False); Assert.That(HasErrorContaining(r, "First line"), Is.False);
} }
[Test]
public void Validate_Siemens_LowercaseContent_NoLowercaseError()
{
// Dopo refactor: Siemens esente da check lowercase
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_Siemens_TabInContent_NoTabError()
{
var v = new NcProgramValidator(CncManufacturer.Siemens);
var r = v.Validate("PROG.MPF", "G00\tX10");
Assert.That(HasErrorContaining(r, "Tab"), Is.False);
}
[Test]
public void Validate_Siemens_LongBlock_NoBlockLengthError()
{
var v = new NcProgramValidator(CncManufacturer.Siemens);
string longLine = new string('G', 120);
var r = v.Validate("PROG.MPF", longLine);
Assert.That(HasErrorContaining(r, "Block length"), Is.False);
}
// ── Heidenhain rules ────────────────────────────────────────────────── // ── Heidenhain rules ──────────────────────────────────────────────────
[Test] [Test]
@ -285,138 +259,6 @@ namespace NcProgramManager.Tests.Unit
Assert.That(HasErrorContaining(r, "First line"), Is.False); Assert.That(HasErrorContaining(r, "First line"), Is.False);
} }
[Test]
public void Validate_Heidenhain_LowercaseContent_NoLowercaseError()
{
var v = new NcProgramValidator(CncManufacturer.Heidenhain);
var r = v.Validate("PROG.H", "l x+10 y+20");
Assert.That(HasErrorContaining(r, "Lowercase"), Is.False);
}
[Test]
public void Validate_Heidenhain_TabInContent_NoTabError()
{
var v = new NcProgramValidator(CncManufacturer.Heidenhain);
var r = v.Validate("PROG.H", "L\tX+10");
Assert.That(HasErrorContaining(r, "Tab"), Is.False);
}
[Test]
public void Validate_Heidenhain_LongBlock_NoBlockLengthError()
{
var v = new NcProgramValidator(CncManufacturer.Heidenhain);
string longLine = "L X+" + new string('1', 100);
var r = v.Validate("PROG.H", longLine);
Assert.That(HasErrorContaining(r, "Block length"), Is.False);
}
// ── Fagor rules ───────────────────────────────────────────────────────
[Test]
public void Validate_Fagor_ValidNameAndContent_Succeeds()
{
var v = new NcProgramValidator(CncManufacturer.Fagor);
var r = v.Validate("PROG1", "G00 X10 Y20\nM30");
Assert.That(r.Success, Is.True);
}
[Test]
public void Validate_Fagor_NameTooLong_ReturnsError()
{
var v = new NcProgramValidator(CncManufacturer.Fagor);
var r = v.Validate(new string('A', 25), "G00 X10");
Assert.That(r.Success, Is.False);
Assert.That(HasErrorContaining(r, "24"), Is.True);
}
[Test]
public void Validate_Fagor_NameContainsTilde_ReturnsError()
{
var v = new NcProgramValidator(CncManufacturer.Fagor);
var r = v.Validate("MA\u00f1ANA", "G00 X10");
Assert.That(r.Success, Is.False);
Assert.That(HasErrorContaining(r, "\u00f1"), Is.True);
}
[Test]
public void Validate_Fagor_NameWithSymbol_ReturnsError()
{
var v = new NcProgramValidator(CncManufacturer.Fagor);
var r = v.Validate("PROG@1", "G00 X10");
Assert.That(r.Success, Is.False);
}
[Test]
public void Validate_Fagor_LongBlock_NoBlockLengthError()
{
// Fagor esentato da check block 80
var v = new NcProgramValidator(CncManufacturer.Fagor);
string longLine = new string('G', 120);
var r = v.Validate("PROG1", longLine);
Assert.That(HasErrorContaining(r, "Block length"), Is.False);
Assert.That(HasErrorContaining(r, "80"), Is.False);
}
[Test]
public void Validate_Fagor_Lowercase_NoLowercaseError()
{
var v = new NcProgramValidator(CncManufacturer.Fagor);
var r = v.Validate("PROG1", "g00 x10");
Assert.That(HasErrorContaining(r, "Lowercase"), Is.False);
}
[Test]
public void Validate_Fagor_Tab_NoTabError()
{
var v = new NcProgramValidator(CncManufacturer.Fagor);
var r = v.Validate("PROG1", "G00\tX10");
Assert.That(HasErrorContaining(r, "Tab"), Is.False);
}
[Test]
public void Validate_Fagor_NestedParens_NoNestedError()
{
var v = new NcProgramValidator(CncManufacturer.Fagor);
var r = v.Validate("PROG1", "G00 ((NESTED))");
Assert.That(HasErrorContaining(r, "Nested"), Is.False);
}
[Test]
public void Validate_Fagor_NameWithNcExtension_Succeeds()
{
// Regression: default ProgramExtension `.nc` non deve essere rifiutato
var v = new NcProgramValidator(CncManufacturer.Fagor);
var r = v.Validate("PROG1.nc", "G00 X10");
Assert.That(r.Success, Is.True);
}
[Test]
public void Validate_Fagor_NameWithCustomExtension_Succeeds()
{
var v = new NcProgramValidator(CncManufacturer.Fagor);
var r = v.Validate("PROG1.pim", "G00 X10");
Assert.That(r.Success, Is.True);
}
[Test]
public void Validate_Fagor_StemWithSymbol_ReturnsError()
{
// Symbol nello stem invariato come errore
var v = new NcProgramValidator(CncManufacturer.Fagor);
var r = v.Validate("PROG@1.nc", "G00 X10");
Assert.That(r.Success, Is.False);
}
[Test]
public void Validate_Fagor_NameContainsTildeEscaped_ReturnsError()
{
// Test con escape unicode esplicito \u00f1 per evitare codepage issues
var v = new NcProgramValidator(CncManufacturer.Fagor);
var r = v.Validate("MA\u00f1ANA", "G00 X10");
Assert.That(r.Success, Is.False);
Assert.That(HasErrorContaining(r, "\u00f1"), Is.True);
}
// ── Helpers ─────────────────────────────────────────────────────────── // ── Helpers ───────────────────────────────────────────────────────────
private static bool HasErrorContaining(ValidationResult r, string fragment) private static bool HasErrorContaining(ValidationResult r, string fragment)

View file

@ -122,10 +122,6 @@
<Compile Include="Cnc\Mitsubishi\IMitsubishiFtpClient.cs" /> <Compile Include="Cnc\Mitsubishi\IMitsubishiFtpClient.cs" />
<Compile Include="Cnc\Mitsubishi\MitsubishiFtpClient.cs" /> <Compile Include="Cnc\Mitsubishi\MitsubishiFtpClient.cs" />
<Compile Include="Cnc\Mitsubishi\MitsubishiMachine.cs" /> <Compile Include="Cnc\Mitsubishi\MitsubishiMachine.cs" />
<Compile Include="Cnc\Fagor\FagorConnectionConfig.cs" />
<Compile Include="Cnc\Fagor\IFagorFtpClient.cs" />
<Compile Include="Cnc\Fagor\FagorFtpClient.cs" />
<Compile Include="Cnc\Fagor\FagorMachine.cs" />
<Compile Include="Licensing\IMachineFingerprint.cs" /> <Compile Include="Licensing\IMachineFingerprint.cs" />
<Compile Include="Licensing\ILicenseValidator.cs" /> <Compile Include="Licensing\ILicenseValidator.cs" />
<Compile Include="Licensing\LicenseFile.cs" /> <Compile Include="Licensing\LicenseFile.cs" />

View file

@ -308,7 +308,7 @@ namespace NcProgramManager
_log.Info("-azione="); _log.Info("-azione=");
_log.Info(" l'{AZIONE} da eseguire."); _log.Info(" l'{AZIONE} da eseguire.");
_log.Info("-manufacturer="); _log.Info("-manufacturer=");
_log.Info(" Il produttore del CNC: fanuc (default), heidenhain, siemens, mitsubishi, fagor"); _log.Info(" Il produttore del CNC: fanuc (default), heidenhain, siemens, mitsubishi");
_log.Info("-tipo="); _log.Info("-tipo=");
_log.Info(" Il tipo di connessione utilizzato dalla macchina: 3 per ethernet\n 2 per HSSB (solo Fanuc, legacy)"); _log.Info(" Il tipo di connessione utilizzato dalla macchina: 3 per ethernet\n 2 per HSSB (solo Fanuc, legacy)");
_log.Info("-ip="); _log.Info("-ip=");
@ -318,9 +318,9 @@ namespace NcProgramManager
_log.Info("-nodo="); _log.Info("-nodo=");
_log.Info(" Dato per HSSB Nodo di connesione (solo Fanuc)"); _log.Info(" Dato per HSSB Nodo di connesione (solo Fanuc)");
_log.Info("-username="); _log.Info("-username=");
_log.Info(" Username per autenticazione (Heidenhain/Siemens/Mitsubishi/Fagor)"); _log.Info(" Username per autenticazione (Heidenhain/Siemens)");
_log.Info("-password="); _log.Info("-password=");
_log.Info(" Password per autenticazione (Heidenhain/Siemens/Mitsubishi/Fagor)"); _log.Info(" Password per autenticazione (Heidenhain/Siemens)");
_log.Info("-compatibility"); _log.Info("-compatibility");
_log.Info(" Attivazione modalità compatibilità"); _log.Info(" Attivazione modalità compatibilità");
_log.Info("-commento="); _log.Info("-commento=");

View file

@ -1,6 +1,6 @@
# NcProgramManager # NcProgramManager
Software per il trasferimento di programmi CNC da e verso controller di macchine utensili. Supporta Fanuc (FOCAS), Heidenhain (LSV2), Siemens Sinumerik (FTP), Mitsubishi (FTP) e Fagor (FTP). Funziona completamente offline come applicazione console per Windows. Software per il trasferimento di programmi CNC da e verso controller di macchine utensili. Supporta Fanuc (FOCAS), Heidenhain (LSV2), Siemens Sinumerik (FTP) e Mitsubishi (FTP). Funziona completamente offline come applicazione console per Windows.
## Requisiti ## Requisiti
@ -25,13 +25,13 @@ NcProgramManager.exe -manufacturer=<tipo> -azione=<azione> -ip=<ip> -porta=<port
| Parametro | Valori | Descrizione | | Parametro | Valori | Descrizione |
|-----------|--------|-------------| |-----------|--------|-------------|
| `-azione=` | `Scarica` / `Invia` | Azione da eseguire | | `-azione=` | `Scarica` / `Invia` | Azione da eseguire |
| `-manufacturer=` | `fanuc` / `heidenhain` / `siemens` / `mitsubishi` / `fagor` | Tipo di controller CNC (predefinito: `fanuc`) | | `-manufacturer=` | `fanuc` / `heidenhain` / `siemens` / `mitsubishi` | Tipo di controller CNC (predefinito: `fanuc`) |
| `-ip=` | indirizzo IP | Indirizzo IP del controller — connessione Ethernet | | `-ip=` | indirizzo IP | Indirizzo IP del controller — connessione Ethernet |
| `-porta=` | numero porta | Porta di connessione Ethernet | | `-porta=` | numero porta | Porta di connessione Ethernet |
| `-tipo=` | `3` / `2` | Tipo di connessione: `3` = Ethernet, `2` = HSSB (solo Fanuc, legacy) | | `-tipo=` | `3` / `2` | Tipo di connessione: `3` = Ethernet, `2` = HSSB (solo Fanuc, legacy) |
| `-nodo=` | numero nodo | Nodo HSSB (solo Fanuc, connessione HSSB) | | `-nodo=` | numero nodo | Nodo HSSB (solo Fanuc, connessione HSSB) |
| `-username=` | nome utente | Credenziale per Heidenhain, Siemens, Mitsubishi, Fagor | | `-username=` | nome utente | Credenziale per Heidenhain, Siemens, Mitsubishi |
| `-password=` | password | Credenziale per Heidenhain, Siemens, Mitsubishi, Fagor | | `-password=` | password | Credenziale per Heidenhain, Siemens, Mitsubishi |
| `-pathprogramma=` | percorso file | Percorso del file locale da caricare sul CNC (richiesto per `Invia`) | | `-pathprogramma=` | percorso file | Percorso del file locale da caricare sul CNC (richiesto per `Invia`) |
| `-path=` | numero | Percorso programmi sul CNC; predefinito `1` = `//CNC_MEM/USER/PATH1/` | | `-path=` | numero | Percorso programmi sul CNC; predefinito `1` = `//CNC_MEM/USER/PATH1/` |
| `-workzero=` | qualsiasi valore | Se presente: include dati work zero offset nell'operazione | | `-workzero=` | qualsiasi valore | Se presente: include dati work zero offset nell'operazione |
@ -63,12 +63,6 @@ Scarica programma da Siemens Sinumerik:
NcProgramManager.exe -manufacturer=siemens -azione=Scarica -ip=192.168.1.200 -porta=21 -username=admin -password=secret NcProgramManager.exe -manufacturer=siemens -azione=Scarica -ip=192.168.1.200 -porta=21 -username=admin -password=secret
``` ```
Invia programma a Fagor 8065:
```
NcProgramManager.exe -manufacturer=fagor -azione=Invia -ip=192.168.1.150 -porta=21 -username=user -password=pass -pathprogramma=C:\programmi\PROG1.nc
```
## Controller supportati ## Controller supportati
| Produttore | Protocollo | Porta predefinita | Note | | Produttore | Protocollo | Porta predefinita | Note |
@ -77,46 +71,6 @@ NcProgramManager.exe -manufacturer=fagor -azione=Invia -ip=192.168.1.150 -porta=
| Heidenhain | LSV2 (TCP) | 19000 | Testato su TNC series | | Heidenhain | LSV2 (TCP) | 19000 | Testato su TNC series |
| Siemens | FTP | 21 | Directory programmi: `/_N_MPF_DIR/` — richiede opzione FTP abilitata sul Sinumerik | | Siemens | FTP | 21 | Directory programmi: `/_N_MPF_DIR/` — richiede opzione FTP abilitata sul Sinumerik |
| Mitsubishi | FTP | 21 | Directory programmi: `/PRG/` | | Mitsubishi | FTP | 21 | Directory programmi: `/PRG/` |
| Fagor | FTP | 21 | Directory programmi: `/Users/Prg/`. Modelli supportati: 8060, 8065, 8070 (FTP nativo); 8055 (richiede opzione Ethernet) |
### Controller Fagor
#### Modelli supportati
| Modello | Supporto | Note |
|---------|----------|------|
| 8055 | Solo con opzione Ethernet | Richiede attivazione esplicita del server FTP via parametri macchina |
| 8060 | Sì | FTP nativo (sistema Windows-based) |
| 8065 | Sì | FTP nativo (sistema Windows-based) |
| 8070 | Sì | FTP nativo (sistema Windows embedded) |
#### Modelli NON supportati
| Modello | Motivo |
|---------|--------|
| 8055 base (senza Ethernet) | Solo trasferimento RS232; il programma non implementa il protocollo seriale |
| 8025 / 8030 / 8040 / 8050 | Legacy, esclusivamente RS232 |
#### Regole nome programma
- Lunghezza massima 24 caratteri
- Caratteri ammessi: lettere (A-Z, a-z), cifre (0-9), spazi
- Carattere `ñ` NON ammesso
- Estensione predefinita: `.nc` (configurabile via codice)
#### Funzioni non supportate via FTP
- `ReadActiveProgramAsync` — Fagor espone lo stato di esecuzione via DNC/OPC-UA, non via FTP
- `SelectMainProgramAsync` — selezione del programma principale richiede DNC, non disponibile via FTP
- Lettura runtime stato macchina (`ReadAsync` ritorna sempre `Unknown`)
#### Variabili d'ambiente per test integration con macchina reale
| Variabile | Valore | Default |
|-----------|--------|---------|
| `FAGOR_TEST_IP` | indirizzo IP macchina Fagor | (test skippato se assente) |
| `FAGOR_USER` | username FTP | `user` |
| `FAGOR_PASS` | password FTP | `pass` |
## Validazione del programma NC ## Validazione del programma NC