Compare commits
2 commits
60ce1c9d57
...
68c5a555f3
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
68c5a555f3 | ||
|
|
b3d7402498 |
24 changed files with 1299 additions and 48 deletions
|
|
@ -108,6 +108,80 @@ 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
|
||||
|
||||
| ID | Summary | Phase | Commit |
|
||||
|
|
@ -121,5 +195,5 @@ Enhancements and known issues discovered during execution and codebase analysis.
|
|||
|
||||
---
|
||||
|
||||
*Last updated: 2026-05-18*
|
||||
*Last updated: 2026-05-25*
|
||||
*Sources: STATE.md deferred issues + CONCERNS.md codebase analysis*
|
||||
|
|
|
|||
|
|
@ -206,4 +206,25 @@ Completed: 2026-05-18
|
|||
- [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.*
|
||||
|
|
|
|||
|
|
@ -5,15 +5,15 @@
|
|||
See: .paul/PROJECT.md (updated 2026-05-18)
|
||||
|
||||
**Core value:** Machinists transfer CNC programs to/from any controller without manual steps or internet
|
||||
**Current focus:** v0.5 complete — all milestones shipped; ready for v0.6 planning
|
||||
**Current focus:** v0.6 Fagor Support complete; ready for v0.7 planning
|
||||
|
||||
## Current Position
|
||||
|
||||
Milestone: v0.5 Operational Quality — **Complete**
|
||||
Phase: 15 of 15 (logging) — Complete
|
||||
Plan: 15-02 complete
|
||||
Status: Milestone complete — ready for v0.6 planning
|
||||
Last activity: 2026-05-18 — Phase 15 complete; v0.5 milestone complete
|
||||
Milestone: v0.6 Fagor Support — **Complete**
|
||||
Phase: 16 of 16 (fagor-ftp) — Complete
|
||||
Plan: 16-02 complete
|
||||
Status: Milestone complete — ready for v0.7 planning
|
||||
Last activity: 2026-05-25 — Phase 16-02 fixup complete (post-review); v0.6 milestone complete
|
||||
|
||||
Progress:
|
||||
- Milestone v0.1: [██████████] 100% (complete)
|
||||
|
|
@ -21,15 +21,55 @@ Progress:
|
|||
- Milestone v0.3: [██████████] 100% (complete)
|
||||
- Milestone v0.4: [██████████] 100% (complete)
|
||||
- Milestone v0.5: [██████████] 100% (complete)
|
||||
- Milestone v0.6: [██████████] 100% (complete)
|
||||
- Phase 15: [██████████] 100%
|
||||
- Phase 16: [██████████] 100%
|
||||
|
||||
## Loop Position
|
||||
|
||||
```
|
||||
PLAN ──▶ APPLY ──▶ UNIFY
|
||||
✓ ✓ ✓ [Loop complete — v0.5 milestone complete]
|
||||
✓ ✓ ✓ [Loop complete — v0.6 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)
|
||||
|
||||
### Phase 15 — Logging
|
||||
|
|
@ -174,7 +214,7 @@ Key open items:
|
|||
None — v0.5 complete.
|
||||
|
||||
### Git State
|
||||
Last commit: (Phase 15 commit — see below after git commit)
|
||||
Last commit: `60ce1c9` — feat(15-logging): NLog structured logging — all output through NLog (v0.5 complete)
|
||||
LicenseGenerator repo: commit `2ef68dd` — feat: initial LicenseGenerator
|
||||
Branch: main
|
||||
|
||||
|
|
|
|||
|
|
@ -1,14 +1,14 @@
|
|||
{
|
||||
"name": "NcProgramManager",
|
||||
"version": "0.5.0",
|
||||
"version": "0.6.0",
|
||||
"milestone": {
|
||||
"name": "v0.5 Operational Quality",
|
||||
"version": "0.5.0",
|
||||
"name": "v0.6 Fagor Support",
|
||||
"version": "0.6.0",
|
||||
"status": "complete"
|
||||
},
|
||||
"phase": {
|
||||
"number": 15,
|
||||
"name": "logging",
|
||||
"number": 16,
|
||||
"name": "fagor-ftp",
|
||||
"status": "complete"
|
||||
},
|
||||
"loop": {
|
||||
|
|
@ -17,7 +17,7 @@
|
|||
},
|
||||
"timestamps": {
|
||||
"created_at": "2026-05-13T00:00:00Z",
|
||||
"updated_at": "2026-05-18T00:00:00Z"
|
||||
"updated_at": "2026-05-25T00:00:00Z"
|
||||
},
|
||||
"satellite": {
|
||||
"groom": true
|
||||
|
|
|
|||
60
.paul/phases/16-fagor-ftp/16-01-PLAN.md
Normal file
60
.paul/phases/16-fagor-ftp/16-01-PLAN.md
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
# 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`)
|
||||
28
.paul/phases/16-fagor-ftp/16-01-SUMMARY.md
Normal file
28
.paul/phases/16-fagor-ftp/16-01-SUMMARY.md
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
# 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.
|
||||
56
.paul/phases/16-fagor-ftp/16-02-PLAN.md
Normal file
56
.paul/phases/16-fagor-ftp/16-02-PLAN.md
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
# 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
|
||||
39
.paul/phases/16-fagor-ftp/16-02-SUMMARY.md
Normal file
39
.paul/phases/16-fagor-ftp/16-02-SUMMARY.md
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
# 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.
|
||||
|
|
@ -31,6 +31,7 @@ namespace NcProgramManager
|
|||
case "heidenhain": inputArgs.manufacturer = CncManufacturer.Heidenhain; break;
|
||||
case "siemens": inputArgs.manufacturer = CncManufacturer.Siemens; break;
|
||||
case "mitsubishi": inputArgs.manufacturer = CncManufacturer.Mitsubishi; break;
|
||||
case "fagor": inputArgs.manufacturer = CncManufacturer.Fagor; break;
|
||||
default: inputArgs.manufacturer = CncManufacturer.Fanuc; break;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
using System;
|
||||
using NcProgramManager.Cnc.Fagor;
|
||||
using NcProgramManager.Cnc.Fanuc;
|
||||
using NcProgramManager.Cnc.Heidenhain;
|
||||
using NcProgramManager.Cnc.Mitsubishi;
|
||||
|
|
@ -44,6 +45,16 @@ namespace NcProgramManager.Cnc
|
|||
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
|
||||
return new FanucMachine(new FanucConnectionConfig
|
||||
{
|
||||
|
|
|
|||
18
Cnc/Fagor/FagorConnectionConfig.cs
Normal file
18
Cnc/Fagor/FagorConnectionConfig.cs
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
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);
|
||||
}
|
||||
}
|
||||
71
Cnc/Fagor/FagorFtpClient.cs
Normal file
71
Cnc/Fagor/FagorFtpClient.cs
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
218
Cnc/Fagor/FagorMachine.cs
Normal file
218
Cnc/Fagor/FagorMachine.cs
Normal file
|
|
@ -0,0 +1,218 @@
|
|||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
10
Cnc/Fagor/IFagorFtpClient.cs
Normal file
10
Cnc/Fagor/IFagorFtpClient.cs
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
namespace NcProgramManager.Cnc.Fagor
|
||||
{
|
||||
internal interface IFagorFtpClient
|
||||
{
|
||||
bool Ping();
|
||||
string DownloadFile(string ncPath);
|
||||
void UploadFile(string ncPath, string content);
|
||||
void DeleteFile(string ncPath);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
namespace NcProgramManager.Cnc.Models
|
||||
{
|
||||
public enum CncManufacturer { Fanuc, Heidenhain, Siemens, Mitsubishi }
|
||||
public enum CncManufacturer { Fanuc, Heidenhain, Siemens, Mitsubishi, Fagor }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -45,34 +45,39 @@ namespace NcProgramManager.Cnc
|
|||
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++)
|
||||
{
|
||||
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 (restrictive)
|
||||
{
|
||||
if (char.IsLower(line[ci]))
|
||||
// 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++)
|
||||
{
|
||||
errors.Add(new ValidationError(lineNum, "Lowercase letters not allowed", ValidationSeverity.Error));
|
||||
break;
|
||||
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);
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
|
@ -89,6 +94,12 @@ namespace NcProgramManager.Cnc
|
|||
|
||||
// ── private helpers ───────────────────────────────────────────────────
|
||||
|
||||
private bool IsRestrictiveDialect()
|
||||
{
|
||||
return _manufacturer == CncManufacturer.Fanuc
|
||||
|| _manufacturer == CncManufacturer.Mitsubishi;
|
||||
}
|
||||
|
||||
private void ValidateName(string name, List<ValidationError> errors)
|
||||
{
|
||||
if (string.IsNullOrEmpty(name)) return;
|
||||
|
|
@ -116,6 +127,33 @@ namespace NcProgramManager.Cnc
|
|||
string.Format("Program name '{0}' must end with .H", name),
|
||||
ValidationSeverity.Error));
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -199,6 +237,10 @@ namespace NcProgramManager.Cnc
|
|||
ValidationSeverity.Error));
|
||||
}
|
||||
break;
|
||||
|
||||
case CncManufacturer.Fagor:
|
||||
// Fagor: nessuna restrizione su contenuto programma (ISO + linguaggio alto livello)
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,127 @@
|
|||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -61,12 +61,14 @@
|
|||
<Compile Include="Unit\SiemensMachineTests.cs" />
|
||||
<Compile Include="Unit\MitsubishiMachineTests.cs" />
|
||||
<Compile Include="Unit\HeidenhainMachineTests.cs" />
|
||||
<Compile Include="Unit\FagorMachineTests.cs" />
|
||||
<Compile Include="Integration\Stubs\FtpServerStub.cs" />
|
||||
<Compile Include="Integration\Stubs\Lsv2ServerStub.cs" />
|
||||
<Compile Include="Integration\HardwareTestHelper.cs" />
|
||||
<Compile Include="Integration\SiemensMachineIntegrationTests.cs" />
|
||||
<Compile Include="Integration\MitsubishiMachineIntegrationTests.cs" />
|
||||
<Compile Include="Integration\HeidenhainMachineIntegrationTests.cs" />
|
||||
<Compile Include="Integration\FagorMachineIntegrationTests.cs" />
|
||||
<Compile Include="Unit\LicenseFileTests.cs" />
|
||||
</ItemGroup>
|
||||
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ using NcProgramManager.Cnc.Heidenhain;
|
|||
using NcProgramManager.Cnc.Mitsubishi;
|
||||
using NcProgramManager.Cnc.Models;
|
||||
using NcProgramManager.Cnc.Siemens;
|
||||
using NcProgramManager.Cnc.Fagor;
|
||||
|
||||
namespace NcProgramManager.Tests.Unit
|
||||
{
|
||||
|
|
@ -81,6 +82,18 @@ 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]
|
||||
public void Create_FanucHssbNode_TransportIsHssb()
|
||||
{
|
||||
|
|
|
|||
212
NcProgramManager.Tests/Unit/FagorMachineTests.cs
Normal file
212
NcProgramManager.Tests/Unit/FagorMachineTests.cs
Normal file
|
|
@ -0,0 +1,212 @@
|
|||
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(); }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -32,8 +32,8 @@ namespace NcProgramManager.Tests.Unit
|
|||
[Test]
|
||||
public void Validate_LowercaseLetter_ReturnsError()
|
||||
{
|
||||
var v = new NcProgramValidator(CncManufacturer.Siemens);
|
||||
var r = v.Validate("PROG.MPF", "PROG.MPF\ng00 X10");
|
||||
var v = new NcProgramValidator(CncManufacturer.Fanuc);
|
||||
var r = v.Validate("O1234", "O1234\ng00 X10");
|
||||
Assert.That(r.Success, Is.False);
|
||||
Assert.That(HasErrorContaining(r, "Lowercase"), Is.True);
|
||||
}
|
||||
|
|
@ -41,16 +41,16 @@ namespace NcProgramManager.Tests.Unit
|
|||
[Test]
|
||||
public void Validate_NoLowercase_NoLowercaseError()
|
||||
{
|
||||
var v = new NcProgramValidator(CncManufacturer.Siemens);
|
||||
var r = v.Validate("PROG.MPF", "G00 X10 Y20");
|
||||
var v = new NcProgramValidator(CncManufacturer.Fanuc);
|
||||
var r = v.Validate("O1234", "O1234\nG00 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");
|
||||
var v = new NcProgramValidator(CncManufacturer.Fanuc);
|
||||
var r = v.Validate("O1234", "O1234\nG00\tX10");
|
||||
Assert.That(r.Success, Is.False);
|
||||
Assert.That(HasErrorContaining(r, "Tab"), Is.True);
|
||||
}
|
||||
|
|
@ -223,6 +223,32 @@ namespace NcProgramManager.Tests.Unit
|
|||
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 ──────────────────────────────────────────────────
|
||||
|
||||
[Test]
|
||||
|
|
@ -259,6 +285,138 @@ namespace NcProgramManager.Tests.Unit
|
|||
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 ───────────────────────────────────────────────────────────
|
||||
|
||||
private static bool HasErrorContaining(ValidationResult r, string fragment)
|
||||
|
|
|
|||
|
|
@ -122,6 +122,10 @@
|
|||
<Compile Include="Cnc\Mitsubishi\IMitsubishiFtpClient.cs" />
|
||||
<Compile Include="Cnc\Mitsubishi\MitsubishiFtpClient.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\ILicenseValidator.cs" />
|
||||
<Compile Include="Licensing\LicenseFile.cs" />
|
||||
|
|
|
|||
|
|
@ -308,7 +308,7 @@ namespace NcProgramManager
|
|||
_log.Info("-azione=");
|
||||
_log.Info(" l'{AZIONE} da eseguire.");
|
||||
_log.Info("-manufacturer=");
|
||||
_log.Info(" Il produttore del CNC: fanuc (default), heidenhain, siemens, mitsubishi");
|
||||
_log.Info(" Il produttore del CNC: fanuc (default), heidenhain, siemens, mitsubishi, fagor");
|
||||
_log.Info("-tipo=");
|
||||
_log.Info(" Il tipo di connessione utilizzato dalla macchina: 3 per ethernet\n 2 per HSSB (solo Fanuc, legacy)");
|
||||
_log.Info("-ip=");
|
||||
|
|
@ -318,9 +318,9 @@ namespace NcProgramManager
|
|||
_log.Info("-nodo=");
|
||||
_log.Info(" Dato per HSSB Nodo di connesione (solo Fanuc)");
|
||||
_log.Info("-username=");
|
||||
_log.Info(" Username per autenticazione (Heidenhain/Siemens)");
|
||||
_log.Info(" Username per autenticazione (Heidenhain/Siemens/Mitsubishi/Fagor)");
|
||||
_log.Info("-password=");
|
||||
_log.Info(" Password per autenticazione (Heidenhain/Siemens)");
|
||||
_log.Info(" Password per autenticazione (Heidenhain/Siemens/Mitsubishi/Fagor)");
|
||||
_log.Info("-compatibility");
|
||||
_log.Info(" Attivazione modalità compatibilità");
|
||||
_log.Info("-commento=");
|
||||
|
|
|
|||
54
README.md
54
README.md
|
|
@ -1,6 +1,6 @@
|
|||
# NcProgramManager
|
||||
|
||||
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.
|
||||
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.
|
||||
|
||||
## Requisiti
|
||||
|
||||
|
|
@ -25,13 +25,13 @@ NcProgramManager.exe -manufacturer=<tipo> -azione=<azione> -ip=<ip> -porta=<port
|
|||
| Parametro | Valori | Descrizione |
|
||||
|-----------|--------|-------------|
|
||||
| `-azione=` | `Scarica` / `Invia` | Azione da eseguire |
|
||||
| `-manufacturer=` | `fanuc` / `heidenhain` / `siemens` / `mitsubishi` | Tipo di controller CNC (predefinito: `fanuc`) |
|
||||
| `-manufacturer=` | `fanuc` / `heidenhain` / `siemens` / `mitsubishi` / `fagor` | Tipo di controller CNC (predefinito: `fanuc`) |
|
||||
| `-ip=` | indirizzo IP | Indirizzo IP del controller — connessione Ethernet |
|
||||
| `-porta=` | numero porta | Porta di connessione Ethernet |
|
||||
| `-tipo=` | `3` / `2` | Tipo di connessione: `3` = Ethernet, `2` = HSSB (solo Fanuc, legacy) |
|
||||
| `-nodo=` | numero nodo | Nodo HSSB (solo Fanuc, connessione HSSB) |
|
||||
| `-username=` | nome utente | Credenziale per Heidenhain, Siemens, Mitsubishi |
|
||||
| `-password=` | password | Credenziale per Heidenhain, Siemens, Mitsubishi |
|
||||
| `-username=` | nome utente | Credenziale per Heidenhain, Siemens, Mitsubishi, Fagor |
|
||||
| `-password=` | password | Credenziale per Heidenhain, Siemens, Mitsubishi, Fagor |
|
||||
| `-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/` |
|
||||
| `-workzero=` | qualsiasi valore | Se presente: include dati work zero offset nell'operazione |
|
||||
|
|
@ -63,6 +63,12 @@ Scarica programma da Siemens Sinumerik:
|
|||
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
|
||||
|
||||
| Produttore | Protocollo | Porta predefinita | Note |
|
||||
|
|
@ -71,6 +77,46 @@ NcProgramManager.exe -manufacturer=siemens -azione=Scarica -ip=192.168.1.200 -po
|
|||
| Heidenhain | LSV2 (TCP) | 19000 | Testato su TNC series |
|
||||
| Siemens | FTP | 21 | Directory programmi: `/_N_MPF_DIR/` — richiede opzione FTP abilitata sul Sinumerik |
|
||||
| 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
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue