Initial commit: migrate from FanucProgramManager
This commit is contained in:
commit
f3f895df82
78 changed files with 19154 additions and 0 deletions
8
.gitignore
vendored
Executable file
8
.gitignore
vendored
Executable file
|
|
@ -0,0 +1,8 @@
|
||||||
|
################################################################################
|
||||||
|
# Il file gitignore è stato creato automaticamente da Microsoft(R) Visual Studio.
|
||||||
|
################################################################################
|
||||||
|
|
||||||
|
/obj
|
||||||
|
/bin
|
||||||
|
/.vs
|
||||||
|
/packages
|
||||||
88
.paul/PROJECT.md
Normal file
88
.paul/PROJECT.md
Normal file
|
|
@ -0,0 +1,88 @@
|
||||||
|
# FanucProgramManager
|
||||||
|
|
||||||
|
## What This Is
|
||||||
|
|
||||||
|
Console EXE (C# .NET 4.7.2) for upload/download of CNC programs to/from machine controllers. Targets any CN manufacturer, not Fanuc-only. Runs on Windows, distributed to licensed machine operators and integrators.
|
||||||
|
|
||||||
|
## Core Value
|
||||||
|
|
||||||
|
Machinists and operators transfer CNC programs to/from any controller without manual intervention or internet access.
|
||||||
|
|
||||||
|
## Current State
|
||||||
|
|
||||||
|
| Attribute | Value |
|
||||||
|
|-----------|-------|
|
||||||
|
| Type | Application |
|
||||||
|
| Version | 0.0.0 |
|
||||||
|
| Status | In development |
|
||||||
|
| Last Updated | 2026-05-13 |
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
### Core Features
|
||||||
|
|
||||||
|
- Upload programs to CNC controller
|
||||||
|
- Download programs from CNC controller
|
||||||
|
- Local file management (organize/store programs on disk)
|
||||||
|
- Multi-CN brand support (Fanuc + others)
|
||||||
|
|
||||||
|
### Validated (Shipped)
|
||||||
|
- [x] Fanuc FOCAS upload/download — initial
|
||||||
|
- [x] AES encryption for program files
|
||||||
|
- [x] CLI argument parsing
|
||||||
|
- [x] Machine-bound license check (hardcoded)
|
||||||
|
|
||||||
|
### Active (In Progress)
|
||||||
|
- [ ] 3-file management mode — branch `3FilesManagement`
|
||||||
|
|
||||||
|
### Planned (Next)
|
||||||
|
- To be defined during /paul:plan
|
||||||
|
|
||||||
|
### Out of Scope
|
||||||
|
- Cloud sync
|
||||||
|
- Web UI
|
||||||
|
- Internet-dependent license validation
|
||||||
|
|
||||||
|
## Constraints
|
||||||
|
|
||||||
|
### Technical Constraints
|
||||||
|
- .NET Framework 4.7.2 — max compatibility, no upgrade
|
||||||
|
- Fanuc FOCAS via `fwlib32` (32-bit DLL, Windows-only)
|
||||||
|
- Must run offline — no internet dependency at runtime
|
||||||
|
- License validation must work without network
|
||||||
|
|
||||||
|
### Business Constraints
|
||||||
|
- Software must be license-protected (offline, machine-bound)
|
||||||
|
- Current approach: hardcoded machine check — needs improvement
|
||||||
|
- Target: hardware fingerprint + encrypted license file
|
||||||
|
|
||||||
|
## Key Decisions
|
||||||
|
|
||||||
|
| Decision | Rationale | Date | Status |
|
||||||
|
|----------|-----------|------|--------|
|
||||||
|
| .NET 4.7.2 | Max compatibility with operator machines | 2026-05-13 | Active |
|
||||||
|
| Console EXE output | Simple deployment, scriptable | 2026-05-13 | Active |
|
||||||
|
| Offline license only | Operators may have no internet on shop floor | 2026-05-13 | Active |
|
||||||
|
|
||||||
|
## Success Metrics
|
||||||
|
|
||||||
|
| Metric | Target | Current | Status |
|
||||||
|
|--------|--------|---------|--------|
|
||||||
|
| Program transfer correctness | 100% (byte-verified) | Unknown | Not measured |
|
||||||
|
| License validation (offline) | Works without network | Hardcoded | At risk |
|
||||||
|
| Multi-CN support | ≥2 manufacturers | Fanuc only | In progress |
|
||||||
|
|
||||||
|
## Tech Stack / Tools
|
||||||
|
|
||||||
|
| Layer | Technology | Notes |
|
||||||
|
|-------|------------|-------|
|
||||||
|
| Language | C# | .NET Framework 4.7.2 |
|
||||||
|
| UI | Console EXE | CLI args via InputArgs.cs |
|
||||||
|
| CN Protocol (Fanuc) | FOCAS fwlib32 | 32-bit DLL |
|
||||||
|
| CN Protocol (others) | To be defined | Multi-CN expansion |
|
||||||
|
| Encryption | AES (AesCrypt.cs) | Program file protection |
|
||||||
|
| Licensing | Machine fingerprint | Hardcoded now, needs design |
|
||||||
|
|
||||||
|
---
|
||||||
|
*PROJECT.md — Updated when requirements or context change*
|
||||||
|
*Last updated: 2026-05-13*
|
||||||
83
.paul/ROADMAP.md
Normal file
83
.paul/ROADMAP.md
Normal file
|
|
@ -0,0 +1,83 @@
|
||||||
|
# Roadmap: FanucProgramManager
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
Console tool for CNC program transfer and file management. Fanuc FOCAS base, expanding to multi-CN (Heidenhain LSV2, Siemens FTP) with robust offline licensing.
|
||||||
|
|
||||||
|
## Current Milestone
|
||||||
|
|
||||||
|
**v0.1 Multi-CN Release** (v0.1.0)
|
||||||
|
Status: In progress
|
||||||
|
Phases: 0 of 4 complete
|
||||||
|
|
||||||
|
## Phases
|
||||||
|
|
||||||
|
| Phase | Name | Plans | Status | Completed |
|
||||||
|
|-------|------|-------|--------|-----------|
|
||||||
|
| 1 | cnc-abstraction | 2 | In progress | - |
|
||||||
|
| 2 | heidenhain | 1 | Not started | - |
|
||||||
|
| 3 | siemens | 1 | Not started | - |
|
||||||
|
| 4 | program-wiring | 1 | Not started | - |
|
||||||
|
|
||||||
|
## Phase Details
|
||||||
|
|
||||||
|
### Phase 1: cnc-abstraction
|
||||||
|
|
||||||
|
**Goal:** Fix namespace, add all Cnc/ files to csproj, complete ICncMachine + IFanucMachine with 3-file support
|
||||||
|
**Depends on:** Nothing
|
||||||
|
**Research:** Unlikely
|
||||||
|
|
||||||
|
**Scope:**
|
||||||
|
- Fix namespace Fanuc_integration → FanucProgramManager.Cnc
|
||||||
|
- Add using FanucProgramManager to Fanuc-specific files
|
||||||
|
- Extend IFocasDialect to accept fileType for tool/work data
|
||||||
|
- Add IFanucMachine interface with tool offset + work zero ops
|
||||||
|
- Add CncManufacturer enum, CncMachineFactory stub
|
||||||
|
- Add all Cnc/ to csproj
|
||||||
|
|
||||||
|
**Plans:**
|
||||||
|
- [ ] 01-01: Fix namespace, extend dialect interface, IFanucMachine
|
||||||
|
- [ ] 01-02: CncManufacturer, CncMachineFactory, add to csproj
|
||||||
|
|
||||||
|
### Phase 2: heidenhain
|
||||||
|
|
||||||
|
**Goal:** Implement ICncMachine for Heidenhain TNC via LSV2 protocol
|
||||||
|
**Depends on:** Phase 1
|
||||||
|
**Research:** Likely (LSV2 binary protocol)
|
||||||
|
|
||||||
|
**Scope:**
|
||||||
|
- HeidenhainConnectionConfig
|
||||||
|
- Lsv2Client (TCP framing, CRC16, commands)
|
||||||
|
- HeidenhainMachine : ICncMachine
|
||||||
|
|
||||||
|
**Plans:**
|
||||||
|
- [ ] 02-01: Heidenhain LSV2 full implementation
|
||||||
|
|
||||||
|
### Phase 3: siemens
|
||||||
|
|
||||||
|
**Goal:** Implement ICncMachine for Siemens Sinumerik via FTP
|
||||||
|
**Depends on:** Phase 1
|
||||||
|
**Research:** Unlikely (standard FTP)
|
||||||
|
|
||||||
|
**Scope:**
|
||||||
|
- SiemensConnectionConfig
|
||||||
|
- SiemensMachine : ICncMachine via FtpWebRequest
|
||||||
|
|
||||||
|
**Plans:**
|
||||||
|
- [ ] 03-01: Siemens FTP implementation
|
||||||
|
|
||||||
|
### Phase 4: program-wiring
|
||||||
|
|
||||||
|
**Goal:** Wire all 3 manufacturers into Program.cs + InputArgs, replace old FANUCMachine
|
||||||
|
**Depends on:** Phase 1, 2, 3
|
||||||
|
|
||||||
|
**Scope:**
|
||||||
|
- Update InputArgs with -manufacturer= param
|
||||||
|
- Refactor Program.cs to use ICncMachine factory
|
||||||
|
- Keep 3-file logic for Fanuc only
|
||||||
|
|
||||||
|
**Plans:**
|
||||||
|
- [ ] 04-01: InputArgs + Program.cs refactor
|
||||||
|
|
||||||
|
---
|
||||||
|
*Roadmap created: 2026-05-13*
|
||||||
111
.paul/STATE.md
Normal file
111
.paul/STATE.md
Normal file
|
|
@ -0,0 +1,111 @@
|
||||||
|
# Project State
|
||||||
|
|
||||||
|
## Project Reference
|
||||||
|
|
||||||
|
See: .paul/PROJECT.md (updated 2026-05-13)
|
||||||
|
|
||||||
|
**Core value:** Machinists transfer CNC programs to/from any controller without manual steps or internet
|
||||||
|
**Current focus:** 4-CN implementation complete — awaiting user verification and UNIFY
|
||||||
|
|
||||||
|
## Current Position
|
||||||
|
|
||||||
|
Milestone: v0.1 Multi-CN Release
|
||||||
|
Phase: 5 of 5 — Applied
|
||||||
|
Plan: All plans applied, awaiting user UNIFY
|
||||||
|
Status: APPLY complete, ready for UNIFY
|
||||||
|
Last activity: 2026-05-13 — Phase 5 NC Validator applied by agent
|
||||||
|
|
||||||
|
Progress:
|
||||||
|
- Milestone: [█████████░] 90%
|
||||||
|
|
||||||
|
## Loop Position
|
||||||
|
|
||||||
|
Current loop state:
|
||||||
|
```
|
||||||
|
PLAN ──▶ APPLY ──▶ UNIFY
|
||||||
|
✓ ✓ ○ [Awaiting user verification + /paul:unify]
|
||||||
|
```
|
||||||
|
|
||||||
|
## What was built (all 4 phases)
|
||||||
|
|
||||||
|
### Phase 1 — Cnc abstraction fixed
|
||||||
|
- Namespace: `Fanuc_integration.Cnc.*` → `FanucProgramManager.Cnc.*` across 29 files
|
||||||
|
- `using FanucProgramManager;` added to all FOCAS-using files
|
||||||
|
- `IFocasDialect` extended: `UploadStart`/`DownloadStart` now accept `short fileType`
|
||||||
|
- `IFanucMachine` created: extends `ICncMachine` with tool offset + work zero R/W
|
||||||
|
- `FanucMachine` now implements `IFanucMachine`
|
||||||
|
- `CncManufacturer` enum: Fanuc, Heidenhain, Siemens
|
||||||
|
- All 29 `Cnc/**/*.cs` files added to `.csproj` (36 total Compile entries)
|
||||||
|
|
||||||
|
### Phase 2 — Heidenhain LSV2
|
||||||
|
- `HeidenhainConnectionConfig` — IP, port 19000, credentials, timeout
|
||||||
|
- `Lsv2Client` — TCP framing, CRC-16/CCITT, login (SELECT+LOG_IN), ReceiveFile/SendFile
|
||||||
|
- `HeidenhainMachine : ICncMachine` — RunGuardedAsync pattern, state machine
|
||||||
|
|
||||||
|
### Phase 3.1 — Mitsubishi M-series (added post-apply)
|
||||||
|
- `MitsubishiConnectionConfig` — port 21, dir `/PRG/` (no file extensions)
|
||||||
|
- `MitsubishiFtpClient` / `MitsubishiMachine : ICncMachine` — FTP, same pattern as Siemens
|
||||||
|
- `CncManufacturer.Mitsubishi` added to enum + factory + CLI parsing
|
||||||
|
|
||||||
|
### Phase 3 — Siemens Sinumerik FTP
|
||||||
|
- `SiemensConnectionConfig` — IP, port 21, creds, program dir `/_N_MPF_DIR/`
|
||||||
|
- `SiemensFtpClient` — FtpWebRequest wrapper, ASCII mode, passive
|
||||||
|
- `SiemensMachine : ICncMachine` — same pattern as FanucMachine
|
||||||
|
|
||||||
|
### Phase 4 — Wiring
|
||||||
|
- `CncMachineFactory` — creates right ICncMachine from InputArgs
|
||||||
|
- `InputArgs` — added `manufacturer`, `username`, `password` fields
|
||||||
|
- `Program.cs` — full rewrite using ICncMachine; Fanuc 3-file via IFanucMachine cast
|
||||||
|
|
||||||
|
### Phase 5 — NC Validator
|
||||||
|
- `ValidationSeverity` enum (Error/Warning)
|
||||||
|
- `ValidationError` — Line, Message, Severity
|
||||||
|
- `ValidationResult` — Success + IReadOnlyList<ValidationError>, static Ok()/Fail()
|
||||||
|
- `INcProgramValidator` interface
|
||||||
|
- `NcProgramValidator` — universal rules (block length ≤80, no lowercase, no tabs, no nested parens, comment ≤32); per-manufacturer name format + forbidden char rules; `#` in Fanuc/Mitsubishi is Warning not Error
|
||||||
|
- All 4 machines injected: public constructor creates `NcProgramValidator(manufacturer)`, internal constructor accepts `INcProgramValidator` for testing
|
||||||
|
- All 4 machines call validator in `WriteProgramAsync` before upload; fail on any Error-severity violation
|
||||||
|
- `NcProgramValidatorTests` — 30+ test cases covering all rules and all manufacturers
|
||||||
|
- Existing machine unit tests updated: inject always-ok mock validator to preserve existing behaviour tests
|
||||||
|
- Integration tests updated: content+names corrected to pass validation per manufacturer
|
||||||
|
- 5 new Compile entries in main csproj + 1 in tests csproj
|
||||||
|
|
||||||
|
## New CLI args
|
||||||
|
- `-manufacturer=fanuc|heidenhain|siemens` (default: fanuc)
|
||||||
|
- `-username=` (Heidenhain/Siemens)
|
||||||
|
- `-password=` (Heidenhain/Siemens)
|
||||||
|
|
||||||
|
## Accumulated Context
|
||||||
|
|
||||||
|
### Decisions
|
||||||
|
- .NET 4.7.2 locked — max compatibility
|
||||||
|
- Offline-only license check (AES, unchanged)
|
||||||
|
- Heidenhain: LSV2 port 19000, needs real hardware testing
|
||||||
|
- Siemens: FTP /_N_MPF_DIR/, needs Sinumerik FTP option enabled on machine
|
||||||
|
|
||||||
|
### Phase 5 — Test project (added post-apply)
|
||||||
|
- `FanucProgramManager.Tests/` — NUnit 3.13.3 + Moq 4.18.4, .NET 4.7.2
|
||||||
|
- 7 unit test files (14+ tests on `FanucProgram`, `IsoProgramFormatter`, factory, all 4 machines)
|
||||||
|
- `FtpServerStub` — in-process TCP FTP (PASV); `Lsv2ServerStub` — in-process LSV2 TCP
|
||||||
|
- 3 integration test files — write→read round-trips against local stubs
|
||||||
|
- `InternalsVisibleTo("FanucProgramManager.Tests")` added to `AssemblyInfo.cs`
|
||||||
|
- Fanuc FOCAS tests that need Windows DLL are `[Ignore]`-d with explanation
|
||||||
|
|
||||||
|
### Deferred Issues
|
||||||
|
- License system: still hardcoded AES key/IV — needs Phase 2.x redesign
|
||||||
|
- Heidenhain LSV2: ReadActiveProgramAsync / SelectMainProgram not supported
|
||||||
|
- Siemens FTP: ReadActiveProgramAsync / SelectMainProgram not supported
|
||||||
|
- Build not verifiable on Linux (fwlib32 is Windows-only DLL)
|
||||||
|
|
||||||
|
### Blockers/Concerns
|
||||||
|
None — code complete, pending hardware testing.
|
||||||
|
|
||||||
|
## Session Continuity
|
||||||
|
|
||||||
|
Last session: 2026-05-13
|
||||||
|
Stopped at: Phase 5 (NC Validator) applied — all 12 files created/modified
|
||||||
|
Next action: Build to verify compilation; run unit tests; /paul:unify when ready
|
||||||
|
Resume file: .paul/STATE.md
|
||||||
|
|
||||||
|
---
|
||||||
|
*STATE.md — Updated after every significant action*
|
||||||
25
.paul/paul.json
Normal file
25
.paul/paul.json
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
{
|
||||||
|
"name": "FanucProgramManager",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"milestone": {
|
||||||
|
"name": "None",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"status": "not_started"
|
||||||
|
},
|
||||||
|
"phase": {
|
||||||
|
"number": 0,
|
||||||
|
"name": "None",
|
||||||
|
"status": "not_started"
|
||||||
|
},
|
||||||
|
"loop": {
|
||||||
|
"plan": null,
|
||||||
|
"position": "IDLE"
|
||||||
|
},
|
||||||
|
"timestamps": {
|
||||||
|
"created_at": "2026-05-13T00:00:00Z",
|
||||||
|
"updated_at": "2026-05-13T00:00:00Z"
|
||||||
|
},
|
||||||
|
"satellite": {
|
||||||
|
"groom": true
|
||||||
|
}
|
||||||
|
}
|
||||||
69
.paul/phases/05-nc-validator/PLAN.md
Normal file
69
.paul/phases/05-nc-validator/PLAN.md
Normal file
|
|
@ -0,0 +1,69 @@
|
||||||
|
# Phase 05 — NC Program Validator
|
||||||
|
|
||||||
|
**Status:** PLAN
|
||||||
|
|
||||||
|
## Goal
|
||||||
|
|
||||||
|
Per-manufacturer NC program validator runs inside `WriteProgramAsync` before upload.
|
||||||
|
Catches format errors before bytes hit the controller.
|
||||||
|
|
||||||
|
## Acceptance Criteria
|
||||||
|
|
||||||
|
- `NcProgramValidator` validates content for all 4 manufacturers
|
||||||
|
- All universal rules checked: block length ≤ 80, no lowercase, no tabs, no nested parens, comment ≤ 32 chars
|
||||||
|
- Per-manufacturer name format and forbidden chars checked
|
||||||
|
- Fanuc `#` char is Warning not Error
|
||||||
|
- All 4 machine classes call validator in `WriteProgramAsync` before upload
|
||||||
|
- Unit tests cover all rules + all manufacturers
|
||||||
|
- Old-style csproj Compile entries added for all new files
|
||||||
|
|
||||||
|
## File List
|
||||||
|
|
||||||
|
| # | File | Action |
|
||||||
|
|---|------|--------|
|
||||||
|
| 1 | `Cnc/Models/ValidationSeverity.cs` | Create |
|
||||||
|
| 2 | `Cnc/Models/ValidationError.cs` | Create |
|
||||||
|
| 3 | `Cnc/Models/ValidationResult.cs` | Create |
|
||||||
|
| 4 | `Cnc/INcProgramValidator.cs` | Create |
|
||||||
|
| 5 | `Cnc/NcProgramValidator.cs` | Create |
|
||||||
|
| 6 | `Cnc/Fanuc/FanucMachine.cs` | Edit — inject validator |
|
||||||
|
| 7 | `Cnc/Heidenhain/HeidenhainMachine.cs` | Edit — inject validator |
|
||||||
|
| 8 | `Cnc/Siemens/SiemensMachine.cs` | Edit — inject validator |
|
||||||
|
| 9 | `Cnc/Mitsubishi/MitsubishiMachine.cs` | Edit — inject validator |
|
||||||
|
| 10 | `FanucProgramManager.Tests/Unit/NcProgramValidatorTests.cs` | Create |
|
||||||
|
| 11 | `FanucProgramManager.csproj` | Edit — add 5 Compile entries |
|
||||||
|
| 12 | `FanucProgramManager.Tests/FanucProgramManager.Tests.csproj` | Edit — add test Compile entry |
|
||||||
|
|
||||||
|
## Validation Rules
|
||||||
|
|
||||||
|
### Universal
|
||||||
|
- Block length ≤ 80 chars per line
|
||||||
|
- No lowercase letters
|
||||||
|
- No tab characters
|
||||||
|
- No nested parentheses (depth > 1)
|
||||||
|
- Comment content inside `()` ≤ 32 chars
|
||||||
|
|
||||||
|
### Fanuc
|
||||||
|
- Name must match `O\d{4}`
|
||||||
|
- First line of content must be `O####`
|
||||||
|
- Forbidden chars: `;`, `[`, `]`
|
||||||
|
- `#` → Warning (macro variable indicator)
|
||||||
|
|
||||||
|
### Mitsubishi
|
||||||
|
- Same as Fanuc (O#### naming, same forbidden chars)
|
||||||
|
|
||||||
|
### Siemens
|
||||||
|
- Name must end with `.MPF` (uppercase)
|
||||||
|
- Forbidden chars: `;`
|
||||||
|
|
||||||
|
### Heidenhain
|
||||||
|
- Name must end with `.H`
|
||||||
|
- No O#### header required
|
||||||
|
- Forbidden chars: `%`, `;`
|
||||||
|
|
||||||
|
## Constraints
|
||||||
|
|
||||||
|
- .NET 4.7.2: no `Array.Empty<T>()`, no C# 8+ features
|
||||||
|
- No LINQ unless already imported
|
||||||
|
- Namespace: `FanucProgramManager.Cnc` (validator), `FanucProgramManager.Cnc.Models` (result types)
|
||||||
|
- English code + comments only
|
||||||
93
AesCrypt.cs
Executable file
93
AesCrypt.cs
Executable file
|
|
@ -0,0 +1,93 @@
|
||||||
|
using System;
|
||||||
|
using System.IO;
|
||||||
|
using System.Security.Cryptography;
|
||||||
|
|
||||||
|
namespace FanucProgramManager
|
||||||
|
{
|
||||||
|
class AesCrypt
|
||||||
|
{
|
||||||
|
|
||||||
|
public static byte[] EncryptStringToBytes_Aes(string plainText, byte[] Key, byte[] IV)
|
||||||
|
{
|
||||||
|
// Check arguments.
|
||||||
|
if (plainText == null || plainText.Length <= 0)
|
||||||
|
throw new ArgumentNullException("plainText");
|
||||||
|
if (Key == null || Key.Length <= 0)
|
||||||
|
throw new ArgumentNullException("Key");
|
||||||
|
if (IV == null || IV.Length <= 0)
|
||||||
|
throw new ArgumentNullException("IV");
|
||||||
|
byte[] encrypted;
|
||||||
|
|
||||||
|
// Create an Aes object
|
||||||
|
// with the specified key and IV.
|
||||||
|
using (Aes aesAlg = Aes.Create())
|
||||||
|
{
|
||||||
|
aesAlg.Key = Key;
|
||||||
|
aesAlg.IV = IV;
|
||||||
|
|
||||||
|
// Create an encryptor to perform the stream transform.
|
||||||
|
ICryptoTransform encryptor = aesAlg.CreateEncryptor(aesAlg.Key, aesAlg.IV);
|
||||||
|
|
||||||
|
// Create the streams used for encryption.
|
||||||
|
using (MemoryStream msEncrypt = new MemoryStream())
|
||||||
|
{
|
||||||
|
using (CryptoStream csEncrypt = new CryptoStream(msEncrypt, encryptor, CryptoStreamMode.Write))
|
||||||
|
{
|
||||||
|
using (StreamWriter swEncrypt = new StreamWriter(csEncrypt))
|
||||||
|
{
|
||||||
|
//Write all data to the stream.
|
||||||
|
swEncrypt.Write(plainText);
|
||||||
|
}
|
||||||
|
encrypted = msEncrypt.ToArray();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return the encrypted bytes from the memory stream.
|
||||||
|
return encrypted;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string DecryptStringFromBytes_Aes(byte[] cipherText, byte[] Key, byte[] IV)
|
||||||
|
{
|
||||||
|
// Check arguments.
|
||||||
|
if (cipherText == null || cipherText.Length <= 0)
|
||||||
|
throw new ArgumentNullException("cipherText");
|
||||||
|
if (Key == null || Key.Length <= 0)
|
||||||
|
throw new ArgumentNullException("Key");
|
||||||
|
if (IV == null || IV.Length <= 0)
|
||||||
|
throw new ArgumentNullException("IV");
|
||||||
|
|
||||||
|
// Declare the string used to hold
|
||||||
|
// the decrypted text.
|
||||||
|
string plaintext = null;
|
||||||
|
|
||||||
|
// Create an Aes object
|
||||||
|
// with the specified key and IV.
|
||||||
|
using (Aes aesAlg = Aes.Create())
|
||||||
|
{
|
||||||
|
aesAlg.Key = Key;
|
||||||
|
aesAlg.IV = IV;
|
||||||
|
|
||||||
|
// Create a decryptor to perform the stream transform.
|
||||||
|
ICryptoTransform decryptor = aesAlg.CreateDecryptor(aesAlg.Key, aesAlg.IV);
|
||||||
|
|
||||||
|
// Create the streams used for decryption.
|
||||||
|
using (MemoryStream msDecrypt = new MemoryStream(cipherText))
|
||||||
|
{
|
||||||
|
using (CryptoStream csDecrypt = new CryptoStream(msDecrypt, decryptor, CryptoStreamMode.Read))
|
||||||
|
{
|
||||||
|
using (StreamReader srDecrypt = new StreamReader(csDecrypt))
|
||||||
|
{
|
||||||
|
|
||||||
|
// Read the decrypted bytes from the decrypting stream
|
||||||
|
// and place them in a string.
|
||||||
|
plaintext = srDecrypt.ReadToEnd();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return plaintext;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
14
App.config
Executable file
14
App.config
Executable file
|
|
@ -0,0 +1,14 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<configuration>
|
||||||
|
<startup>
|
||||||
|
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.7.2" />
|
||||||
|
</startup>
|
||||||
|
<runtime>
|
||||||
|
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
|
||||||
|
<dependentAssembly>
|
||||||
|
<assemblyIdentity name="System.Runtime.CompilerServices.Unsafe" publicKeyToken="b03f5f7f11d50a3a" culture="neutral" />
|
||||||
|
<bindingRedirect oldVersion="0.0.0.0-6.0.0.0" newVersion="6.0.0.0" />
|
||||||
|
</dependentAssembly>
|
||||||
|
</assemblyBinding>
|
||||||
|
</runtime>
|
||||||
|
</configuration>
|
||||||
60
Cnc/CncMachineFactory.cs
Normal file
60
Cnc/CncMachineFactory.cs
Normal file
|
|
@ -0,0 +1,60 @@
|
||||||
|
using System;
|
||||||
|
using FanucProgramManager.Cnc.Fanuc;
|
||||||
|
using FanucProgramManager.Cnc.Heidenhain;
|
||||||
|
using FanucProgramManager.Cnc.Mitsubishi;
|
||||||
|
using FanucProgramManager.Cnc.Models;
|
||||||
|
using FanucProgramManager.Cnc.Siemens;
|
||||||
|
|
||||||
|
namespace FanucProgramManager.Cnc
|
||||||
|
{
|
||||||
|
internal static class CncMachineFactory
|
||||||
|
{
|
||||||
|
internal static ICncMachine Create(InputArgs args)
|
||||||
|
{
|
||||||
|
switch (args.manufacturer)
|
||||||
|
{
|
||||||
|
case CncManufacturer.Heidenhain:
|
||||||
|
return new HeidenhainMachine(new HeidenhainConnectionConfig
|
||||||
|
{
|
||||||
|
Name = "Heidenhain",
|
||||||
|
IpAddress = args.ip,
|
||||||
|
Port = string.IsNullOrEmpty(args.porta) ? 19000 : int.Parse(args.porta),
|
||||||
|
Username = args.username,
|
||||||
|
Password = args.password,
|
||||||
|
ConnectTimeout = TimeSpan.FromSeconds(5)
|
||||||
|
});
|
||||||
|
|
||||||
|
case CncManufacturer.Siemens:
|
||||||
|
return new SiemensMachine(new SiemensConnectionConfig
|
||||||
|
{
|
||||||
|
Name = "Siemens",
|
||||||
|
IpAddress = args.ip,
|
||||||
|
Port = string.IsNullOrEmpty(args.porta) ? 21 : int.Parse(args.porta),
|
||||||
|
Username = args.username,
|
||||||
|
Password = args.password
|
||||||
|
});
|
||||||
|
|
||||||
|
case CncManufacturer.Mitsubishi:
|
||||||
|
return new MitsubishiMachine(new MitsubishiConnectionConfig
|
||||||
|
{
|
||||||
|
Name = "Mitsubishi",
|
||||||
|
IpAddress = args.ip,
|
||||||
|
Port = string.IsNullOrEmpty(args.porta) ? 21 : int.Parse(args.porta),
|
||||||
|
Username = args.username,
|
||||||
|
Password = args.password
|
||||||
|
});
|
||||||
|
|
||||||
|
default: // Fanuc
|
||||||
|
return new FanucMachine(new FanucConnectionConfig
|
||||||
|
{
|
||||||
|
Name = "Fanuc",
|
||||||
|
Transport = args.nodoHssb >= 0 ? FanucTransport.Hssb : FanucTransport.Ethernet,
|
||||||
|
IpAddress = args.ip,
|
||||||
|
Port = string.IsNullOrEmpty(args.porta) ? (ushort)8193 : ushort.Parse(args.porta),
|
||||||
|
HssbNode = args.nodoHssb,
|
||||||
|
ConnectTimeout = TimeSpan.FromSeconds(4)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
26
Cnc/Fanuc/FanucCapabilities.cs
Executable file
26
Cnc/Fanuc/FanucCapabilities.cs
Executable file
|
|
@ -0,0 +1,26 @@
|
||||||
|
namespace FanucProgramManager.Cnc.Fanuc
|
||||||
|
{
|
||||||
|
public sealed class FanucCapabilities
|
||||||
|
{
|
||||||
|
public string CncType { get; set; }
|
||||||
|
public char ModelLetter { get; set; }
|
||||||
|
public bool IsISeries { get; set; }
|
||||||
|
public string Series { get; set; }
|
||||||
|
public string Version { get; set; }
|
||||||
|
public string MtType { get; set; }
|
||||||
|
public int MaxAxis { get; set; }
|
||||||
|
|
||||||
|
public FanucDialect Dialect { get; set; }
|
||||||
|
|
||||||
|
public bool SupportsPathBasedPrograms { get; set; }
|
||||||
|
public bool SupportsPdfProgramApi { get; set; }
|
||||||
|
public bool SupportsAlmMsg2 { get; set; }
|
||||||
|
public bool SupportsUpDownload4 { get; set; }
|
||||||
|
|
||||||
|
public override string ToString()
|
||||||
|
{
|
||||||
|
return "Fanuc " + (CncType ?? "?").Trim() + "i-" + ModelLetter +
|
||||||
|
" (" + Dialect + ", " + (MtType ?? "?").Trim() + ", " + MaxAxis + " axes)";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
29
Cnc/Fanuc/FanucConnectionConfig.cs
Executable file
29
Cnc/Fanuc/FanucConnectionConfig.cs
Executable file
|
|
@ -0,0 +1,29 @@
|
||||||
|
using System;
|
||||||
|
|
||||||
|
namespace FanucProgramManager.Cnc.Fanuc
|
||||||
|
{
|
||||||
|
public enum FanucTransport
|
||||||
|
{
|
||||||
|
Ethernet = 3,
|
||||||
|
Hssb = 2,
|
||||||
|
Library = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class FanucConnectionConfig
|
||||||
|
{
|
||||||
|
public string Name { get; set; }
|
||||||
|
public FanucTransport Transport { get; set; }
|
||||||
|
public string IpAddress { get; set; }
|
||||||
|
public ushort Port { get; set; }
|
||||||
|
public int HssbNode { get; set; }
|
||||||
|
public int PieceCounterMacro { get; set; }
|
||||||
|
public TimeSpan ConnectTimeout { get; set; }
|
||||||
|
|
||||||
|
public FanucConnectionConfig()
|
||||||
|
{
|
||||||
|
HssbNode = -1;
|
||||||
|
PieceCounterMacro = -1;
|
||||||
|
ConnectTimeout = TimeSpan.FromSeconds(4);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
9
Cnc/Fanuc/FanucDialect.cs
Executable file
9
Cnc/Fanuc/FanucDialect.cs
Executable file
|
|
@ -0,0 +1,9 @@
|
||||||
|
namespace FanucProgramManager.Cnc.Fanuc
|
||||||
|
{
|
||||||
|
public enum FanucDialect
|
||||||
|
{
|
||||||
|
Unknown = 0,
|
||||||
|
Legacy = 1,
|
||||||
|
Modern = 2
|
||||||
|
}
|
||||||
|
}
|
||||||
86
Cnc/Fanuc/FanucDialectDetector.cs
Executable file
86
Cnc/Fanuc/FanucDialectDetector.cs
Executable file
|
|
@ -0,0 +1,86 @@
|
||||||
|
using FanucProgramManager;
|
||||||
|
|
||||||
|
namespace FanucProgramManager.Cnc.Fanuc
|
||||||
|
{
|
||||||
|
internal static class FanucDialectDetector
|
||||||
|
{
|
||||||
|
public static FanucCapabilities Detect(ushort hndl)
|
||||||
|
{
|
||||||
|
var sys = new Focas1.ODBSYS();
|
||||||
|
short ret = Focas1.cnc_sysinfo(hndl, sys);
|
||||||
|
if (ret != Focas1.EW_OK)
|
||||||
|
{
|
||||||
|
return new FanucCapabilities
|
||||||
|
{
|
||||||
|
CncType = "??",
|
||||||
|
ModelLetter = '?',
|
||||||
|
Dialect = FanucDialect.Unknown
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
string cncType = new string(sys.cnc_type ?? new char[] { '?', '?' });
|
||||||
|
char modelLetter = ModelLetterFrom(sys.addinfo);
|
||||||
|
bool isI = (sys.addinfo & 0x02) != 0;
|
||||||
|
FanucDialect dialect = DialectFor(cncType, modelLetter);
|
||||||
|
|
||||||
|
bool modern = dialect == FanucDialect.Modern;
|
||||||
|
return new FanucCapabilities
|
||||||
|
{
|
||||||
|
CncType = cncType,
|
||||||
|
ModelLetter = modelLetter,
|
||||||
|
IsISeries = isI,
|
||||||
|
Series = sys.series != null ? new string(sys.series) : "",
|
||||||
|
Version = sys.version != null ? new string(sys.version) : "",
|
||||||
|
MtType = sys.mt_type != null ? new string(sys.mt_type) : "",
|
||||||
|
MaxAxis = sys.max_axis,
|
||||||
|
Dialect = dialect,
|
||||||
|
SupportsUpDownload4 = modern,
|
||||||
|
SupportsAlmMsg2 = modern,
|
||||||
|
SupportsPathBasedPrograms = modern,
|
||||||
|
SupportsPdfProgramApi = modern
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static char ModelLetterFrom(short addinfo)
|
||||||
|
{
|
||||||
|
int model = (addinfo >> 8) & 0xFF;
|
||||||
|
switch (model)
|
||||||
|
{
|
||||||
|
case 1: return 'A';
|
||||||
|
case 2: return 'B';
|
||||||
|
case 3: return 'C';
|
||||||
|
case 4: return 'D';
|
||||||
|
case 6: return 'F';
|
||||||
|
default: return ' ';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static FanucDialect DialectFor(string cncType, char modelLetter)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(cncType)) return FanucDialect.Unknown;
|
||||||
|
|
||||||
|
string t = cncType;
|
||||||
|
string trimmed = cncType.Trim();
|
||||||
|
|
||||||
|
if (trimmed == "30" || trimmed == "31" || trimmed == "32" || trimmed == "35")
|
||||||
|
return FanucDialect.Modern;
|
||||||
|
|
||||||
|
if (trimmed == "PM")
|
||||||
|
return FanucDialect.Modern;
|
||||||
|
|
||||||
|
if (trimmed == "0" || t == " 0")
|
||||||
|
{
|
||||||
|
if (modelLetter == 'D' || modelLetter == 'F') return FanucDialect.Modern;
|
||||||
|
return FanucDialect.Legacy;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (trimmed == "15" || trimmed == "16" || trimmed == "18" || trimmed == "21")
|
||||||
|
return FanucDialect.Legacy;
|
||||||
|
|
||||||
|
if (trimmed == "PD" || trimmed == "PH")
|
||||||
|
return FanucDialect.Legacy;
|
||||||
|
|
||||||
|
return FanucDialect.Unknown;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
60
Cnc/Fanuc/FanucErrorTranslator.cs
Executable file
60
Cnc/Fanuc/FanucErrorTranslator.cs
Executable file
|
|
@ -0,0 +1,60 @@
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using FanucProgramManager;
|
||||||
|
using FanucProgramManager.Cnc.Models;
|
||||||
|
|
||||||
|
namespace FanucProgramManager.Cnc.Fanuc
|
||||||
|
{
|
||||||
|
internal static class FanucErrorTranslator
|
||||||
|
{
|
||||||
|
private static readonly Dictionary<short, string> Messages = new Dictionary<short, string>
|
||||||
|
{
|
||||||
|
{ (short)Focas1.focas_ret.EW_PROTOCOL, "protocol error" },
|
||||||
|
{ (short)Focas1.focas_ret.EW_SOCKET, "windows socket error" },
|
||||||
|
{ (short)Focas1.focas_ret.EW_NODLL, "DLL not exist error" },
|
||||||
|
{ (short)Focas1.focas_ret.EW_BUS, "bus error" },
|
||||||
|
{ (short)Focas1.focas_ret.EW_SYSTEM2, "system error" },
|
||||||
|
{ (short)Focas1.focas_ret.EW_HSSB, "hssb communication error" },
|
||||||
|
{ (short)Focas1.focas_ret.EW_HANDLE, "Windows library handle error" },
|
||||||
|
{ (short)Focas1.focas_ret.EW_VERSION, "CNC/PMC version mismatch" },
|
||||||
|
{ (short)Focas1.focas_ret.EW_UNEXP, "abnormal error" },
|
||||||
|
{ (short)Focas1.focas_ret.EW_SYSTEM, "system error" },
|
||||||
|
{ (short)Focas1.focas_ret.EW_PARITY, "shared RAM parity error" },
|
||||||
|
{ (short)Focas1.focas_ret.EW_MMCSYS, "emm386 or mmcsys install error" },
|
||||||
|
{ (short)Focas1.focas_ret.EW_RESET, "reset or stop occurred" },
|
||||||
|
{ (short)Focas1.focas_ret.EW_BUSY, "busy" },
|
||||||
|
{ (short)Focas1.focas_ret.EW_FUNC, "command prepare error / pmc not exist" },
|
||||||
|
{ (short)Focas1.focas_ret.EW_LENGTH, "data block length error" },
|
||||||
|
{ (short)Focas1.focas_ret.EW_NUMBER, "data number / address range error" },
|
||||||
|
{ (short)Focas1.focas_ret.EW_ATTRIB, "data attribute / data type error" },
|
||||||
|
{ (short)Focas1.focas_ret.EW_DATA, "data error" },
|
||||||
|
{ (short)Focas1.focas_ret.EW_NOOPT, "no option" },
|
||||||
|
{ (short)Focas1.focas_ret.EW_PROT, "write protect" },
|
||||||
|
{ (short)Focas1.focas_ret.EW_OVRFLOW, "memory overflow" },
|
||||||
|
{ (short)Focas1.focas_ret.EW_PARAM, "CNC parameter not correct" },
|
||||||
|
{ (short)Focas1.focas_ret.EW_BUFFER, "buffer empty/full" },
|
||||||
|
{ (short)Focas1.focas_ret.EW_PATH, "path error" },
|
||||||
|
{ (short)Focas1.focas_ret.EW_MODE, "CNC mode error" },
|
||||||
|
{ (short)Focas1.focas_ret.EW_REJECT, "execution rejected" },
|
||||||
|
{ (short)Focas1.focas_ret.EW_DTSRVR, "data server error" },
|
||||||
|
{ (short)Focas1.focas_ret.EW_ALARM, "alarm has been occurred" },
|
||||||
|
{ (short)Focas1.focas_ret.EW_STOP, "CNC is not running" },
|
||||||
|
{ (short)Focas1.focas_ret.EW_PASSWD, "protection data error" }
|
||||||
|
};
|
||||||
|
|
||||||
|
public static CncError Translate(short code, string source)
|
||||||
|
{
|
||||||
|
string msg;
|
||||||
|
if (!Messages.TryGetValue(code, out msg))
|
||||||
|
msg = "unknown FOCAS error";
|
||||||
|
return new CncError(code, source, msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool IsDisconnectError(short code)
|
||||||
|
{
|
||||||
|
return code == (short)Focas1.focas_ret.EW_SOCKET
|
||||||
|
|| code == (short)Focas1.focas_ret.EW_HANDLE
|
||||||
|
|| code == (short)Focas1.focas_ret.EW_HSSB
|
||||||
|
|| code == (short)Focas1.focas_ret.EW_PROTOCOL;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
739
Cnc/Fanuc/FanucMachine.cs
Executable file
739
Cnc/Fanuc/FanucMachine.cs
Executable file
|
|
@ -0,0 +1,739 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using FanucProgramManager;
|
||||||
|
using FanucProgramManager.Cnc;
|
||||||
|
using FanucProgramManager.Cnc.Models;
|
||||||
|
|
||||||
|
namespace FanucProgramManager.Cnc.Fanuc
|
||||||
|
{
|
||||||
|
public sealed class FanucMachine : IFanucMachine
|
||||||
|
{
|
||||||
|
private const int AlarmSlots = 17;
|
||||||
|
private const int OperatorMsgSlots = 17;
|
||||||
|
private const int TransferChunkSize = 1024;
|
||||||
|
private const int UploadBufferSize = 10240;
|
||||||
|
private const int Param_PiecesCount = 6711;
|
||||||
|
private const int Param_CycleTimeMs = 6757;
|
||||||
|
private const int Param_CycleTimeMin = 6758;
|
||||||
|
private const string DefaultDownloadFolder = "//CNC_MEM/USER/PATH1/";
|
||||||
|
private const string SeedProgramName = "OSTD";
|
||||||
|
private const string SeedProgramBody = "%\n<OSTD>\n%";
|
||||||
|
|
||||||
|
private readonly FanucConnectionConfig _config;
|
||||||
|
private readonly INcProgramValidator _validator;
|
||||||
|
private readonly List<CncError> _currentErrors = new List<CncError>();
|
||||||
|
private readonly SemaphoreSlim _gate = new SemaphoreSlim(1, 1);
|
||||||
|
|
||||||
|
private ushort _hndl;
|
||||||
|
private FanucCapabilities _caps;
|
||||||
|
private IFocasDialect _dialect;
|
||||||
|
private ConnectionState _state = ConnectionState.Disconnected;
|
||||||
|
|
||||||
|
public FanucMachine(FanucConnectionConfig config)
|
||||||
|
{
|
||||||
|
if (config == null) throw new ArgumentNullException("config");
|
||||||
|
_config = config;
|
||||||
|
_validator = new NcProgramValidator(CncManufacturer.Fanuc);
|
||||||
|
}
|
||||||
|
|
||||||
|
internal FanucMachine(FanucConnectionConfig config, INcProgramValidator validator)
|
||||||
|
{
|
||||||
|
if (config == null) throw new ArgumentNullException("config");
|
||||||
|
_config = config;
|
||||||
|
_validator = validator ?? new NcProgramValidator(CncManufacturer.Fanuc);
|
||||||
|
}
|
||||||
|
|
||||||
|
public string Name { get { return _config.Name; } }
|
||||||
|
public ConnectionState State { get { return _state; } }
|
||||||
|
public FanucCapabilities Capabilities { get { return _caps; } }
|
||||||
|
|
||||||
|
public event EventHandler<ConnectionState> StateChanged;
|
||||||
|
|
||||||
|
public Task<CncResult<bool>> ConnectAsync(CancellationToken ct = default(CancellationToken))
|
||||||
|
{
|
||||||
|
return RunGuardedAsync(() => ConnectCore(ct), ct);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task DisconnectAsync()
|
||||||
|
{
|
||||||
|
return RunGuardedAsync<bool>(() => { DisconnectCore(); return true; }, CancellationToken.None);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<CncResult<MachineSnapshot>> ReadAsync(CancellationToken ct = default(CancellationToken))
|
||||||
|
{
|
||||||
|
return RunGuardedAsync(() => ReadCore(ct), ct);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<CncResult<string>> ReadActiveProgramAsync(CancellationToken ct = default(CancellationToken))
|
||||||
|
{
|
||||||
|
return RunGuardedAsync(() => ReadActiveProgramCore(ct), ct);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<CncResult<string>> ReadProgramAsync(string path, CancellationToken ct = default(CancellationToken))
|
||||||
|
{
|
||||||
|
return RunGuardedAsync(() => ReadProgramCore(path, ct), ct);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<CncResult<bool>> WriteProgramAsync(string path, CncProgram program, CancellationToken ct = default(CancellationToken))
|
||||||
|
{
|
||||||
|
return RunGuardedAsync(() => WriteProgramCore(path, program, ct), ct);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<CncResult<bool>> SelectMainProgramAsync(string path, CancellationToken ct = default(CancellationToken))
|
||||||
|
{
|
||||||
|
return RunGuardedAsync(() => SelectMainProgramCore(path), ct);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<CncResult<bool>> DeleteProgramAsync(string path, CancellationToken ct = default(CancellationToken))
|
||||||
|
{
|
||||||
|
return RunGuardedAsync(() => DeleteProgramCore(path), ct);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<CncResult<string>> ReadToolDataAsync(string path, CancellationToken ct = default(CancellationToken))
|
||||||
|
{
|
||||||
|
return RunGuardedAsync(() => UploadFocasFile(1, path, ct), ct);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<CncResult<string>> ReadWorkZeroDataAsync(string path, CancellationToken ct = default(CancellationToken))
|
||||||
|
{
|
||||||
|
return RunGuardedAsync(() => UploadFocasFile(5, path, ct), ct);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<CncResult<bool>> WriteToolDataAsync(string folderPath, string content, CancellationToken ct = default(CancellationToken))
|
||||||
|
{
|
||||||
|
return RunGuardedAsync(() => DownloadFocasFile(1, folderPath, content, ct), ct);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<CncResult<bool>> WriteWorkZeroDataAsync(string folderPath, string content, CancellationToken ct = default(CancellationToken))
|
||||||
|
{
|
||||||
|
return RunGuardedAsync(() => DownloadFocasFile(5, folderPath, content, ct), ct);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
try { DisconnectAsync().GetAwaiter().GetResult(); } catch { }
|
||||||
|
try { _gate.Dispose(); } catch { }
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<CncResult<T>> RunGuardedAsync<T>(Func<T> body, CancellationToken ct)
|
||||||
|
{
|
||||||
|
await _gate.WaitAsync(ct).ConfigureAwait(false);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return await Task.Run(() =>
|
||||||
|
{
|
||||||
|
_currentErrors.Clear();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
T value = body();
|
||||||
|
if (_currentErrors.Count > 0)
|
||||||
|
return CncResult<T>.Fail(_currentErrors.ToArray());
|
||||||
|
return CncResult<T>.Ok(value);
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException)
|
||||||
|
{
|
||||||
|
return CncResult<T>.Cancelled();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_currentErrors.Add(new CncError(0, "FanucMachine", ex.Message));
|
||||||
|
return CncResult<T>.Fail(_currentErrors.ToArray());
|
||||||
|
}
|
||||||
|
}, ct).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_gate.Release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool ConnectCore(CancellationToken ct)
|
||||||
|
{
|
||||||
|
if (_state == ConnectionState.Connected) return true;
|
||||||
|
SetState(ConnectionState.Connecting);
|
||||||
|
ct.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
|
short ret;
|
||||||
|
switch (_config.Transport)
|
||||||
|
{
|
||||||
|
case FanucTransport.Ethernet:
|
||||||
|
short timeoutSec = (short)Math.Max(1, (int)_config.ConnectTimeout.TotalSeconds);
|
||||||
|
ret = Focas1.cnc_allclibhndl3(_config.IpAddress, _config.Port, timeoutSec, out _hndl);
|
||||||
|
break;
|
||||||
|
case FanucTransport.Hssb:
|
||||||
|
ret = Focas1.cnc_allclibhndl2(_config.HssbNode, out _hndl);
|
||||||
|
break;
|
||||||
|
case FanucTransport.Library:
|
||||||
|
ret = Focas1.cnc_allclibhndl(out _hndl);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
Record((short)Focas1.focas_ret.EW_ATTRIB, "Connect");
|
||||||
|
SetState(ConnectionState.Faulted);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ret != Focas1.EW_OK)
|
||||||
|
{
|
||||||
|
Record(ret, "cnc_allclibhndl");
|
||||||
|
SetState(ConnectionState.Faulted);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ct.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
try { Focas1.cnc_freelibhndl(_hndl); } catch { }
|
||||||
|
SetState(ConnectionState.Disconnected);
|
||||||
|
ct.ThrowIfCancellationRequested();
|
||||||
|
}
|
||||||
|
|
||||||
|
_caps = FanucDialectDetector.Detect(_hndl);
|
||||||
|
_dialect = FocasDialectFactory.Create(_caps);
|
||||||
|
if (_caps.Dialect == FanucDialect.Unknown)
|
||||||
|
_currentErrors.Add(new CncError(0, "cnc_sysinfo", "Unknown CNC model, falling back to Legacy dialect"));
|
||||||
|
|
||||||
|
SetState(ConnectionState.Connected);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DisconnectCore()
|
||||||
|
{
|
||||||
|
if (_state == ConnectionState.Disconnected) return;
|
||||||
|
short ret = Focas1.cnc_freelibhndl(_hndl);
|
||||||
|
if (ret != Focas1.EW_OK)
|
||||||
|
Record(ret, "cnc_freelibhndl");
|
||||||
|
SetState(ConnectionState.Disconnected);
|
||||||
|
}
|
||||||
|
|
||||||
|
private MachineSnapshot ReadCore(CancellationToken ct)
|
||||||
|
{
|
||||||
|
EnsureConnected();
|
||||||
|
ct.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
|
Focas1.cnc_setpath(_hndl, 1);
|
||||||
|
|
||||||
|
var snap = new MachineSnapshot
|
||||||
|
{
|
||||||
|
CapturedAt = DateTime.Now,
|
||||||
|
Alarms = new List<string>(),
|
||||||
|
OperatorMessages = new List<string>(),
|
||||||
|
Mode = MachineMode.Unknown,
|
||||||
|
Run = RunState.Unknown,
|
||||||
|
Motion = MotionState.Unknown
|
||||||
|
};
|
||||||
|
|
||||||
|
snap.CycleTime = ReadCycleTime();
|
||||||
|
ct.ThrowIfCancellationRequested();
|
||||||
|
snap.PieceCount = ReadPieceCount();
|
||||||
|
ct.ThrowIfCancellationRequested();
|
||||||
|
snap.Alarms = ReadAlarms();
|
||||||
|
ct.ThrowIfCancellationRequested();
|
||||||
|
snap.OperatorMessages = ReadOperatorMessages();
|
||||||
|
ct.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
|
var stateInfo = ReadMachineState();
|
||||||
|
snap.Mode = stateInfo.Mode;
|
||||||
|
snap.Run = stateInfo.Run;
|
||||||
|
snap.Motion = stateInfo.Motion;
|
||||||
|
snap.Emergency = stateInfo.Emergency;
|
||||||
|
snap.AlarmActive = stateInfo.Alarm;
|
||||||
|
|
||||||
|
if (snap.Mode != MachineMode.Edit)
|
||||||
|
{
|
||||||
|
ct.ThrowIfCancellationRequested();
|
||||||
|
var prog = ReadProgramInfo();
|
||||||
|
snap.MainProgram = prog.MainProgram;
|
||||||
|
snap.SubProgram = prog.SubProgram;
|
||||||
|
snap.ProgramComment = prog.Comment;
|
||||||
|
}
|
||||||
|
|
||||||
|
return snap;
|
||||||
|
}
|
||||||
|
|
||||||
|
private string ReadActiveProgramCore(CancellationToken ct)
|
||||||
|
{
|
||||||
|
EnsureConnected();
|
||||||
|
short ret;
|
||||||
|
string identifier = _dialect.ReadMainProgramIdentifier(_hndl, out ret);
|
||||||
|
if (ret != Focas1.EW_OK)
|
||||||
|
{
|
||||||
|
Record(ret, "ReadMainProgramIdentifier");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return UploadProgramByIdentifier(identifier, ct);
|
||||||
|
}
|
||||||
|
|
||||||
|
private string ReadProgramCore(string path, CancellationToken ct)
|
||||||
|
{
|
||||||
|
EnsureConnected();
|
||||||
|
if (string.IsNullOrEmpty(path))
|
||||||
|
{
|
||||||
|
_currentErrors.Add(new CncError(0, "ReadProgram", "Path is empty"));
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return UploadProgramByIdentifier(path, ct);
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool WriteProgramCore(string folderPath, CncProgram program, CancellationToken ct)
|
||||||
|
{
|
||||||
|
EnsureConnected();
|
||||||
|
if (program == null || string.IsNullOrEmpty(program.Content))
|
||||||
|
{
|
||||||
|
_currentErrors.Add(new CncError(0, "WriteProgram", "Program content is empty"));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
var vr = _validator.Validate(program.Name ?? folderPath, program.Content);
|
||||||
|
if (!vr.Success)
|
||||||
|
{
|
||||||
|
string msg = string.Join("; ", vr.Errors
|
||||||
|
.Where(e => e.Severity == ValidationSeverity.Error)
|
||||||
|
.Select(e => e.Message));
|
||||||
|
_currentErrors.Add(new CncError(0, "Validator", msg));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!CanDownload()) return false;
|
||||||
|
if (string.IsNullOrEmpty(folderPath)) folderPath = DefaultDownloadFolder;
|
||||||
|
|
||||||
|
string fileName = ExtractFileNameFromContent(program);
|
||||||
|
if (string.IsNullOrEmpty(fileName))
|
||||||
|
{
|
||||||
|
_currentErrors.Add(new CncError(0, "WriteProgram", "Cannot extract program name from content"));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
EnsureSeedProgram(folderPath, ct);
|
||||||
|
ct.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
|
if (_dialect.SupportsPathBasedPrograms)
|
||||||
|
Focas1.cnc_pdf_del(_hndl, folderPath + fileName);
|
||||||
|
|
||||||
|
short ret = _dialect.DownloadStart(_hndl, folderPath);
|
||||||
|
if (ret != Focas1.EW_OK)
|
||||||
|
{
|
||||||
|
Record(ret, "DownloadStart");
|
||||||
|
_dialect.DownloadEnd(_hndl);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (!TransferChunked(program.Content, ct)) return false;
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_dialect.DownloadEnd(_hndl);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_dialect.SupportsPathBasedPrograms)
|
||||||
|
{
|
||||||
|
ret = Focas1.cnc_pdf_slctmain(_hndl, folderPath + fileName);
|
||||||
|
if (ret != Focas1.EW_OK)
|
||||||
|
{
|
||||||
|
Record(ret, "cnc_pdf_slctmain");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool SelectMainProgramCore(string path)
|
||||||
|
{
|
||||||
|
EnsureConnected();
|
||||||
|
if (!_dialect.SupportsPathBasedPrograms)
|
||||||
|
{
|
||||||
|
_currentErrors.Add(new CncError((short)Focas1.focas_ret.EW_NOOPT,
|
||||||
|
"SelectMainProgram", "Path-based selection not supported on this CNC"));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
short ret = Focas1.cnc_pdf_slctmain(_hndl, path);
|
||||||
|
if (ret != Focas1.EW_OK) { Record(ret, "cnc_pdf_slctmain"); return false; }
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool DeleteProgramCore(string path)
|
||||||
|
{
|
||||||
|
EnsureConnected();
|
||||||
|
if (!_dialect.SupportsPathBasedPrograms)
|
||||||
|
{
|
||||||
|
_currentErrors.Add(new CncError((short)Focas1.focas_ret.EW_NOOPT,
|
||||||
|
"DeleteProgram", "Path-based delete not supported on this CNC"));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
short ret = Focas1.cnc_pdf_del(_hndl, path);
|
||||||
|
if (ret != Focas1.EW_OK) { Record(ret, "cnc_pdf_del"); return false; }
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private string UploadFocasFile(short fileType, string path, CancellationToken ct)
|
||||||
|
{
|
||||||
|
EnsureConnected();
|
||||||
|
if (string.IsNullOrEmpty(path))
|
||||||
|
{
|
||||||
|
_currentErrors.Add(new CncError(0, "UploadFocasFile", "Path is empty"));
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
long programNumber = TryParseProgramNumber(path);
|
||||||
|
short ret = _dialect.UploadStart(_hndl, path, programNumber, fileType);
|
||||||
|
if (ret != Focas1.EW_OK)
|
||||||
|
{
|
||||||
|
Record(ret, "UploadStart");
|
||||||
|
_dialect.UploadEnd(_hndl);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
var output = new StringBuilder();
|
||||||
|
char[] buffer = new char[UploadBufferSize + 1];
|
||||||
|
try
|
||||||
|
{
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
ct.ThrowIfCancellationRequested();
|
||||||
|
int len = UploadBufferSize;
|
||||||
|
short r = _dialect.UploadData(_hndl, ref len, buffer);
|
||||||
|
if (r == (short)Focas1.focas_ret.EW_BUFFER)
|
||||||
|
{
|
||||||
|
if (ct.WaitHandle.WaitOne(100)) ct.ThrowIfCancellationRequested();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (r == Focas1.EW_OK && len > 0)
|
||||||
|
{
|
||||||
|
output.Append(buffer, 0, len);
|
||||||
|
if (buffer[len - 1] == '%') break;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (r != Focas1.EW_OK) Record(r, "UploadData");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally { _dialect.UploadEnd(_hndl); }
|
||||||
|
return output.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool DownloadFocasFile(short fileType, string folderPath, string content, CancellationToken ct)
|
||||||
|
{
|
||||||
|
EnsureConnected();
|
||||||
|
if (string.IsNullOrEmpty(content))
|
||||||
|
{
|
||||||
|
_currentErrors.Add(new CncError(0, "DownloadFocasFile", "Content is empty"));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (string.IsNullOrEmpty(folderPath)) folderPath = DefaultDownloadFolder;
|
||||||
|
short ret = _dialect.DownloadStart(_hndl, folderPath, fileType);
|
||||||
|
if (ret != Focas1.EW_OK)
|
||||||
|
{
|
||||||
|
Record(ret, "DownloadStart");
|
||||||
|
_dialect.DownloadEnd(_hndl);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
try { return TransferChunked(content, ct); }
|
||||||
|
finally { _dialect.DownloadEnd(_hndl); }
|
||||||
|
}
|
||||||
|
|
||||||
|
private TimeSpan ReadCycleTime()
|
||||||
|
{
|
||||||
|
var ms = new Focas1.IODBPSD();
|
||||||
|
var min = new Focas1.IODBPSD();
|
||||||
|
short r1 = Focas1.cnc_rdparam(_hndl, Param_CycleTimeMs, 0, 8, ms);
|
||||||
|
short r2 = Focas1.cnc_rdparam(_hndl, Param_CycleTimeMin, 0, 8, min);
|
||||||
|
if (r1 != Focas1.EW_OK) Record(r1, "cnc_rdparam(cycle ms)");
|
||||||
|
if (r2 != Focas1.EW_OK) Record(r2, "cnc_rdparam(cycle min)");
|
||||||
|
return TimeSpan.FromMinutes(min.u.ldata) + TimeSpan.FromMilliseconds(ms.u.ldata);
|
||||||
|
}
|
||||||
|
|
||||||
|
private long ReadPieceCount()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (_config.PieceCounterMacro == -1)
|
||||||
|
{
|
||||||
|
var p = new Focas1.IODBPSD();
|
||||||
|
short ret = Focas1.cnc_rdparam(_hndl, Param_PiecesCount, 0, 8, p);
|
||||||
|
if (ret != Focas1.EW_OK) { Record(ret, "cnc_rdparam(pieces)"); return 0; }
|
||||||
|
return p.u.ldata;
|
||||||
|
}
|
||||||
|
var macro = new Focas1.ODBM();
|
||||||
|
short macRet = Focas1.cnc_rdmacro(_hndl, (short)_config.PieceCounterMacro, 10, macro);
|
||||||
|
if (macRet != Focas1.EW_OK) { Record(macRet, "cnc_rdmacro(pieces)"); return 0; }
|
||||||
|
long val = Math.Abs(macro.mcr_val);
|
||||||
|
if (macro.dec_val > 0) val = val / (long)Math.Pow(10, macro.dec_val);
|
||||||
|
return macro.mcr_val < 0 ? -val : val;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_currentErrors.Add(new CncError(0, "ReadPieceCount", ex.Message));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private IReadOnlyList<string> ReadAlarms()
|
||||||
|
{
|
||||||
|
string[] buf = new string[AlarmSlots];
|
||||||
|
short num = AlarmSlots;
|
||||||
|
short ret = _dialect.ReadAlarmMessages(_hndl, ref num, buf);
|
||||||
|
if (ret != Focas1.EW_OK) Record(ret, "ReadAlarmMessages");
|
||||||
|
var list = new List<string>(num);
|
||||||
|
for (int i = 0; i < num; i++)
|
||||||
|
if (!string.IsNullOrEmpty(buf[i])) list.Add(buf[i]);
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
|
||||||
|
private IReadOnlyList<string> ReadOperatorMessages()
|
||||||
|
{
|
||||||
|
string[] buf = new string[OperatorMsgSlots];
|
||||||
|
short num = 1;
|
||||||
|
short ret = _dialect.ReadOperatorMessages(_hndl, ref num, buf);
|
||||||
|
if (ret != Focas1.EW_OK) Record(ret, "ReadOperatorMessages");
|
||||||
|
var list = new List<string>(num);
|
||||||
|
for (int i = 0; i < num; i++)
|
||||||
|
if (!string.IsNullOrEmpty(buf[i])) list.Add(buf[i]);
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
|
||||||
|
private struct StateInfo
|
||||||
|
{
|
||||||
|
public MachineMode Mode;
|
||||||
|
public RunState Run;
|
||||||
|
public MotionState Motion;
|
||||||
|
public bool Emergency;
|
||||||
|
public bool Alarm;
|
||||||
|
}
|
||||||
|
|
||||||
|
private StateInfo ReadMachineState()
|
||||||
|
{
|
||||||
|
var s = new StateInfo
|
||||||
|
{
|
||||||
|
Mode = MachineMode.Unknown,
|
||||||
|
Run = RunState.Unknown,
|
||||||
|
Motion = MotionState.Unknown
|
||||||
|
};
|
||||||
|
var odbst = new Focas1.ODBST();
|
||||||
|
short ret = Focas1.cnc_statinfo(_hndl, odbst);
|
||||||
|
if (ret != Focas1.EW_OK) { Record(ret, "cnc_statinfo"); return s; }
|
||||||
|
|
||||||
|
switch (odbst.aut)
|
||||||
|
{
|
||||||
|
case 0: s.Mode = MachineMode.Mdi; break;
|
||||||
|
case 1: s.Mode = MachineMode.Auto; break;
|
||||||
|
case 3: s.Mode = MachineMode.Edit; break;
|
||||||
|
case 4: s.Mode = MachineMode.Handle; break;
|
||||||
|
case 5: s.Mode = MachineMode.Jog; break;
|
||||||
|
}
|
||||||
|
switch (odbst.run)
|
||||||
|
{
|
||||||
|
case 1: s.Run = RunState.Stop; break;
|
||||||
|
case 3: s.Run = RunState.ExecutingIso; break;
|
||||||
|
}
|
||||||
|
switch (odbst.motion)
|
||||||
|
{
|
||||||
|
case 0: s.Motion = MotionState.Idle; break;
|
||||||
|
case 1: s.Motion = MotionState.Moving; break;
|
||||||
|
case 2: s.Motion = MotionState.Hold; break;
|
||||||
|
}
|
||||||
|
s.Emergency = odbst.emergency == 1;
|
||||||
|
s.Alarm = odbst.alarm == 1;
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
|
||||||
|
private struct ProgramInfo
|
||||||
|
{
|
||||||
|
public string MainProgram;
|
||||||
|
public string SubProgram;
|
||||||
|
public string Comment;
|
||||||
|
}
|
||||||
|
|
||||||
|
private ProgramInfo ReadProgramInfo()
|
||||||
|
{
|
||||||
|
var info = new ProgramInfo { MainProgram = "", SubProgram = "", Comment = "" };
|
||||||
|
|
||||||
|
var prog = new Focas1.ODBPRO();
|
||||||
|
short ret = Focas1.cnc_rdprgnum(_hndl, prog);
|
||||||
|
if (ret != Focas1.EW_OK) { Record(ret, "cnc_rdprgnum"); return info; }
|
||||||
|
info.SubProgram = "O" + prog.data.ToString();
|
||||||
|
info.MainProgram = "O" + prog.mdata.ToString();
|
||||||
|
|
||||||
|
if (_dialect.SupportsPathBasedPrograms)
|
||||||
|
{
|
||||||
|
byte[] mainPath = new byte[242];
|
||||||
|
short pathRet = Focas1.cnc_pdf_rdmain(_hndl, mainPath);
|
||||||
|
if (pathRet == Focas1.EW_OK)
|
||||||
|
{
|
||||||
|
string str = ASCIIEncoding.ASCII.GetString(mainPath).TrimEnd('\0');
|
||||||
|
string[] parts = str.Split('/');
|
||||||
|
if (parts.Length > 0) info.MainProgram = parts[parts.Length - 1];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
byte[] commentBuf = new byte[100];
|
||||||
|
int charsRead;
|
||||||
|
short cret = _dialect.ReadActiveProgramComment(_hndl, commentBuf, out charsRead);
|
||||||
|
if (cret != Focas1.EW_OK) { Record(cret, "ReadActiveProgramComment"); return info; }
|
||||||
|
|
||||||
|
string raw = ASCIIEncoding.ASCII.GetString(commentBuf).TrimEnd('\0');
|
||||||
|
int open = raw.IndexOf('(');
|
||||||
|
int close = raw.IndexOf(')');
|
||||||
|
if (open >= 0 && close > open)
|
||||||
|
info.Comment = raw.Substring(open + 1, close - open - 1);
|
||||||
|
else
|
||||||
|
info.Comment = "";
|
||||||
|
return info;
|
||||||
|
}
|
||||||
|
|
||||||
|
private string UploadProgramByIdentifier(string identifier, CancellationToken ct)
|
||||||
|
{
|
||||||
|
long programNumber = TryParseProgramNumber(identifier);
|
||||||
|
short ret = _dialect.UploadStart(_hndl, identifier, programNumber);
|
||||||
|
if (ret != Focas1.EW_OK)
|
||||||
|
{
|
||||||
|
Record(ret, "UploadStart");
|
||||||
|
_dialect.UploadEnd(_hndl);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var output = new StringBuilder();
|
||||||
|
char[] buffer = new char[UploadBufferSize + 1];
|
||||||
|
try
|
||||||
|
{
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
ct.ThrowIfCancellationRequested();
|
||||||
|
int len = UploadBufferSize;
|
||||||
|
short r = _dialect.UploadData(_hndl, ref len, buffer);
|
||||||
|
if (r == (short)Focas1.focas_ret.EW_BUFFER)
|
||||||
|
{
|
||||||
|
if (ct.WaitHandle.WaitOne(100)) ct.ThrowIfCancellationRequested();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (r == Focas1.EW_OK && len > 0)
|
||||||
|
{
|
||||||
|
output.Append(buffer, 0, len);
|
||||||
|
if (len > 0 && buffer[len - 1] == '%') break;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (r != Focas1.EW_OK) Record(r, "UploadData");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_dialect.UploadEnd(_hndl);
|
||||||
|
}
|
||||||
|
return output.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool TransferChunked(string content, CancellationToken ct)
|
||||||
|
{
|
||||||
|
int len = content.Length;
|
||||||
|
int startPos = 0;
|
||||||
|
while (len > 0)
|
||||||
|
{
|
||||||
|
ct.ThrowIfCancellationRequested();
|
||||||
|
int num = len >= TransferChunkSize ? TransferChunkSize : len;
|
||||||
|
char[] chunk = new char[num];
|
||||||
|
content.CopyTo(startPos, chunk, 0, num);
|
||||||
|
|
||||||
|
short ret = _dialect.DownloadData(_hndl, ref num, chunk);
|
||||||
|
if (ret == (short)Focas1.focas_ret.EW_BUFFER)
|
||||||
|
{
|
||||||
|
if (ct.WaitHandle.WaitOne(100)) ct.ThrowIfCancellationRequested();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (ret == Focas1.EW_OK)
|
||||||
|
{
|
||||||
|
startPos += num;
|
||||||
|
len -= num;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
Record(ret, "DownloadData");
|
||||||
|
var dtl = new Focas1.ODBERR();
|
||||||
|
Focas1.cnc_getdtailerr(_hndl, dtl);
|
||||||
|
Debug.WriteLine("FOCAS detail err: " + dtl.err_no + " " + dtl.err_dtno);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool CanDownload()
|
||||||
|
{
|
||||||
|
var odbst = new Focas1.ODBST();
|
||||||
|
short ret = Focas1.cnc_statinfo(_hndl, odbst);
|
||||||
|
if (ret != Focas1.EW_OK) { Record(ret, "cnc_statinfo(canDownload)"); return false; }
|
||||||
|
if (odbst.aut != 0 && odbst.aut != 3)
|
||||||
|
{
|
||||||
|
Record((short)Focas1.focas_ret.EW_MODE, "CanDownload");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void EnsureSeedProgram(string folderPath, CancellationToken ct)
|
||||||
|
{
|
||||||
|
if (!_dialect.SupportsPathBasedPrograms) return;
|
||||||
|
short selRet = Focas1.cnc_pdf_slctmain(_hndl, folderPath + SeedProgramName);
|
||||||
|
if (selRet == Focas1.EW_OK) return;
|
||||||
|
|
||||||
|
Focas1.cnc_pdf_del(_hndl, folderPath + SeedProgramName);
|
||||||
|
short sStart = _dialect.DownloadStart(_hndl, folderPath);
|
||||||
|
if (sStart != Focas1.EW_OK) { _dialect.DownloadEnd(_hndl); return; }
|
||||||
|
TransferChunked(SeedProgramBody, ct);
|
||||||
|
_dialect.DownloadEnd(_hndl);
|
||||||
|
Focas1.cnc_pdf_slctmain(_hndl, folderPath + SeedProgramName);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string ExtractFileNameFromContent(CncProgram program)
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrEmpty(program.Name)) return program.Name;
|
||||||
|
string content = program.Content ?? "";
|
||||||
|
int start = content.IndexOf('<');
|
||||||
|
int end = content.IndexOf('>');
|
||||||
|
if (start >= 0 && end > start) return content.Substring(start + 1, end - start - 1);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static long TryParseProgramNumber(string identifier)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(identifier)) return 0;
|
||||||
|
string s = identifier;
|
||||||
|
int slash = s.LastIndexOf('/');
|
||||||
|
if (slash >= 0 && slash < s.Length - 1) s = s.Substring(slash + 1);
|
||||||
|
if (s.Length > 0 && (s[0] == 'O' || s[0] == 'o')) s = s.Substring(1);
|
||||||
|
long n;
|
||||||
|
return long.TryParse(s, out n) ? n : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void EnsureConnected()
|
||||||
|
{
|
||||||
|
if (_state != ConnectionState.Connected || _dialect == null)
|
||||||
|
throw new InvalidOperationException("FanucMachine is not connected.");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Record(short code, string source)
|
||||||
|
{
|
||||||
|
var err = FanucErrorTranslator.Translate(code, source);
|
||||||
|
_currentErrors.Add(err);
|
||||||
|
if (FanucErrorTranslator.IsDisconnectError(code))
|
||||||
|
{
|
||||||
|
SetState(ConnectionState.Faulted);
|
||||||
|
try { Focas1.cnc_freelibhndl(_hndl); } catch { }
|
||||||
|
SetState(ConnectionState.Disconnected);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SetState(ConnectionState next)
|
||||||
|
{
|
||||||
|
if (_state == next) return;
|
||||||
|
_state = next;
|
||||||
|
var h = StateChanged;
|
||||||
|
if (h != null)
|
||||||
|
{
|
||||||
|
try { h(this, next); } catch { }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
21
Cnc/Fanuc/FocasDialectFactory.cs
Executable file
21
Cnc/Fanuc/FocasDialectFactory.cs
Executable file
|
|
@ -0,0 +1,21 @@
|
||||||
|
using System;
|
||||||
|
|
||||||
|
namespace FanucProgramManager.Cnc.Fanuc
|
||||||
|
{
|
||||||
|
internal static class FocasDialectFactory
|
||||||
|
{
|
||||||
|
public static IFocasDialect Create(FanucCapabilities caps)
|
||||||
|
{
|
||||||
|
if (caps == null) return new LegacyFocasDialect();
|
||||||
|
switch (caps.Dialect)
|
||||||
|
{
|
||||||
|
case FanucDialect.Modern: return new ModernFocasDialect();
|
||||||
|
case FanucDialect.Legacy:
|
||||||
|
case FanucDialect.Unknown:
|
||||||
|
return new LegacyFocasDialect();
|
||||||
|
default:
|
||||||
|
throw new InvalidOperationException("Unsupported FanucDialect: " + caps.Dialect);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
23
Cnc/Fanuc/IFocasDialect.cs
Executable file
23
Cnc/Fanuc/IFocasDialect.cs
Executable file
|
|
@ -0,0 +1,23 @@
|
||||||
|
namespace FanucProgramManager.Cnc.Fanuc
|
||||||
|
{
|
||||||
|
internal interface IFocasDialect
|
||||||
|
{
|
||||||
|
FanucDialect Dialect { get; }
|
||||||
|
bool SupportsPathBasedPrograms { get; }
|
||||||
|
bool SupportsAlmMsg2 { get; }
|
||||||
|
|
||||||
|
short ReadAlarmMessages(ushort hndl, ref short num, string[] outBuffer);
|
||||||
|
short ReadOperatorMessages(ushort hndl, ref short num, string[] outBuffer);
|
||||||
|
|
||||||
|
short DownloadStart(ushort hndl, string folderPath, short fileType = 0);
|
||||||
|
short DownloadData(ushort hndl, ref int len, char[] data);
|
||||||
|
short DownloadEnd(ushort hndl);
|
||||||
|
|
||||||
|
short UploadStart(ushort hndl, string fileName, long programNumber, short fileType = 0);
|
||||||
|
short UploadData(ushort hndl, ref int len, char[] data);
|
||||||
|
short UploadEnd(ushort hndl);
|
||||||
|
|
||||||
|
string ReadMainProgramIdentifier(ushort hndl, out short ret);
|
||||||
|
short ReadActiveProgramComment(ushort hndl, byte[] buffer, out int charsRead);
|
||||||
|
}
|
||||||
|
}
|
||||||
95
Cnc/Fanuc/IsoProgramFormatter.cs
Executable file
95
Cnc/Fanuc/IsoProgramFormatter.cs
Executable file
|
|
@ -0,0 +1,95 @@
|
||||||
|
namespace FanucProgramManager.Cnc.Fanuc
|
||||||
|
{
|
||||||
|
public static class IsoProgramFormatter
|
||||||
|
{
|
||||||
|
public static string EnsureTitleBrackets(string program)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(program)) return program;
|
||||||
|
string head, body;
|
||||||
|
if (!SplitFirstNonEmptyLine(program, out head, out body)) return program;
|
||||||
|
|
||||||
|
int openAngle = head.IndexOf('<');
|
||||||
|
if (openAngle >= 0) return program;
|
||||||
|
|
||||||
|
int openPar = head.IndexOf('(');
|
||||||
|
int closePar = head.IndexOf(')');
|
||||||
|
|
||||||
|
int titleStart = FindTitleStart(head);
|
||||||
|
head = head.Insert(titleStart, "<");
|
||||||
|
if (closePar > 0)
|
||||||
|
{
|
||||||
|
openPar = head.IndexOf('(');
|
||||||
|
head = head.Insert(openPar, ">");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
head = head + ">";
|
||||||
|
}
|
||||||
|
return head + body;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string InsertOrReplaceComment(string program, string comment)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(program)) return program;
|
||||||
|
program = EnsureTitleBrackets(program);
|
||||||
|
|
||||||
|
string head, body;
|
||||||
|
if (!SplitFirstNonEmptyLine(program, out head, out body)) return program;
|
||||||
|
|
||||||
|
int openPar = head.IndexOf('(');
|
||||||
|
int closePar = head.IndexOf(')');
|
||||||
|
if (openPar > 0 && closePar > openPar)
|
||||||
|
{
|
||||||
|
head = head.Remove(openPar + 1, closePar - openPar - 1);
|
||||||
|
head = head.Insert(openPar + 1, comment ?? "");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
head = head + "(" + (comment ?? "") + ")";
|
||||||
|
}
|
||||||
|
return head + body;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string RenameAndComment(string program, string name, string comment)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(program)) return program;
|
||||||
|
string head, body;
|
||||||
|
if (!SplitFirstNonEmptyLine(program, out head, out body)) return program;
|
||||||
|
|
||||||
|
string newHead = "%\n<" + (name ?? "") + ">(" + (comment ?? "") + ")";
|
||||||
|
return newHead + body;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool SplitFirstNonEmptyLine(string program, out string head, out string body)
|
||||||
|
{
|
||||||
|
head = program;
|
||||||
|
body = "";
|
||||||
|
bool sawFirstLf = false;
|
||||||
|
int lastLf = -1;
|
||||||
|
for (int i = 0; i < program.Length; i++)
|
||||||
|
{
|
||||||
|
if (program[i] == '\n')
|
||||||
|
{
|
||||||
|
if (sawFirstLf)
|
||||||
|
{
|
||||||
|
head = program.Substring(0, i);
|
||||||
|
body = program.Substring(i);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
sawFirstLf = true;
|
||||||
|
lastLf = i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int FindTitleStart(string head)
|
||||||
|
{
|
||||||
|
for (int i = 0; i < head.Length; i++)
|
||||||
|
{
|
||||||
|
if (head[i] == '\n') return i + 1;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
76
Cnc/Fanuc/LegacyFocasDialect.cs
Executable file
76
Cnc/Fanuc/LegacyFocasDialect.cs
Executable file
|
|
@ -0,0 +1,76 @@
|
||||||
|
using FanucProgramManager;
|
||||||
|
|
||||||
|
namespace FanucProgramManager.Cnc.Fanuc
|
||||||
|
{
|
||||||
|
internal sealed class LegacyFocasDialect : IFocasDialect
|
||||||
|
{
|
||||||
|
public FanucDialect Dialect { get { return FanucDialect.Legacy; } }
|
||||||
|
public bool SupportsPathBasedPrograms { get { return false; } }
|
||||||
|
public bool SupportsAlmMsg2 { get { return false; } }
|
||||||
|
|
||||||
|
public short DownloadStart(ushort hndl, string folderPath, short fileType = 0)
|
||||||
|
{
|
||||||
|
return Focas1.cnc_dwnstart3(hndl, fileType);
|
||||||
|
}
|
||||||
|
|
||||||
|
public short DownloadData(ushort hndl, ref int len, char[] data)
|
||||||
|
{
|
||||||
|
return Focas1.cnc_download3(hndl, ref len, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
public short DownloadEnd(ushort hndl)
|
||||||
|
{
|
||||||
|
return Focas1.cnc_dwnend3(hndl);
|
||||||
|
}
|
||||||
|
|
||||||
|
public short UploadStart(ushort hndl, string fileName, long programNumber, short fileType = 0)
|
||||||
|
{
|
||||||
|
int pn = (int)programNumber;
|
||||||
|
return Focas1.cnc_upstart3(hndl, fileType, pn, pn);
|
||||||
|
}
|
||||||
|
|
||||||
|
public short UploadData(ushort hndl, ref int len, char[] data)
|
||||||
|
{
|
||||||
|
return Focas1.cnc_upload3(hndl, ref len, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
public short UploadEnd(ushort hndl)
|
||||||
|
{
|
||||||
|
return Focas1.cnc_upend3(hndl);
|
||||||
|
}
|
||||||
|
|
||||||
|
public short ReadAlarmMessages(ushort hndl, ref short num, string[] outBuffer)
|
||||||
|
{
|
||||||
|
var arr = new Focas1.ODBALMMSG_data_custom[outBuffer.Length];
|
||||||
|
short ret = Focas1.cnc_rdalmmsg_custom(hndl, 0, ref num, arr);
|
||||||
|
for (int i = 0; i < num && i < outBuffer.Length; i++)
|
||||||
|
outBuffer[i] = arr[i].msg_len > 0 ? arr[i].alm_msg : "";
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
public short ReadOperatorMessages(ushort hndl, ref short num, string[] outBuffer)
|
||||||
|
{
|
||||||
|
var arr = new Focas1.OPMSG3_data_custom[outBuffer.Length];
|
||||||
|
short ret = Focas1.cnc_rdopmsg3_custom(hndl, 4, ref num, arr);
|
||||||
|
for (int i = 0; i < num && i < outBuffer.Length; i++)
|
||||||
|
outBuffer[i] = arr[i].char_num > 0 ? arr[i].data : "";
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
public string ReadMainProgramIdentifier(ushort hndl, out short ret)
|
||||||
|
{
|
||||||
|
var prog = new Focas1.ODBPRO();
|
||||||
|
ret = Focas1.cnc_rdprgnum(hndl, prog);
|
||||||
|
return "O" + prog.mdata.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
public short ReadActiveProgramComment(ushort hndl, byte[] buffer, out int charsRead)
|
||||||
|
{
|
||||||
|
ushort len = (ushort)buffer.Length;
|
||||||
|
short blk;
|
||||||
|
short ret = Focas1.cnc_rdexecprog(hndl, ref len, out blk, buffer);
|
||||||
|
charsRead = len;
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
79
Cnc/Fanuc/ModernFocasDialect.cs
Executable file
79
Cnc/Fanuc/ModernFocasDialect.cs
Executable file
|
|
@ -0,0 +1,79 @@
|
||||||
|
using System.Text;
|
||||||
|
using FanucProgramManager;
|
||||||
|
|
||||||
|
namespace FanucProgramManager.Cnc.Fanuc
|
||||||
|
{
|
||||||
|
internal sealed class ModernFocasDialect : IFocasDialect
|
||||||
|
{
|
||||||
|
public FanucDialect Dialect { get { return FanucDialect.Modern; } }
|
||||||
|
public bool SupportsPathBasedPrograms { get { return true; } }
|
||||||
|
public bool SupportsAlmMsg2 { get { return true; } }
|
||||||
|
|
||||||
|
public short DownloadStart(ushort hndl, string folderPath, short fileType = 0)
|
||||||
|
{
|
||||||
|
return Focas1.cnc_dwnstart4(hndl, fileType, folderPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
public short DownloadData(ushort hndl, ref int len, char[] data)
|
||||||
|
{
|
||||||
|
return Focas1.cnc_download4(hndl, ref len, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
public short DownloadEnd(ushort hndl)
|
||||||
|
{
|
||||||
|
return Focas1.cnc_dwnend4(hndl);
|
||||||
|
}
|
||||||
|
|
||||||
|
public short UploadStart(ushort hndl, string fileName, long programNumber, short fileType = 0)
|
||||||
|
{
|
||||||
|
return Focas1.cnc_upstart4(hndl, fileType, fileName);
|
||||||
|
}
|
||||||
|
|
||||||
|
public short UploadData(ushort hndl, ref int len, char[] data)
|
||||||
|
{
|
||||||
|
return Focas1.cnc_upload4(hndl, ref len, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
public short UploadEnd(ushort hndl)
|
||||||
|
{
|
||||||
|
return Focas1.cnc_upend4(hndl);
|
||||||
|
}
|
||||||
|
|
||||||
|
public short ReadAlarmMessages(ushort hndl, ref short num, string[] outBuffer)
|
||||||
|
{
|
||||||
|
var arr = new Focas1.ODBALMMSG2_data_custom[outBuffer.Length];
|
||||||
|
short ret = Focas1.cnc_rdalmmsg2_custom(hndl, -1, ref num, arr);
|
||||||
|
for (int i = 0; i < num && i < outBuffer.Length; i++)
|
||||||
|
outBuffer[i] = arr[i].msg_len > 0 ? arr[i].alm_msg : "";
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
public short ReadOperatorMessages(ushort hndl, ref short num, string[] outBuffer)
|
||||||
|
{
|
||||||
|
var arr = new Focas1.OPMSG3_data_custom[outBuffer.Length];
|
||||||
|
short ret = Focas1.cnc_rdopmsg3_custom(hndl, 4, ref num, arr);
|
||||||
|
for (int i = 0; i < num && i < outBuffer.Length; i++)
|
||||||
|
outBuffer[i] = arr[i].char_num > 0 ? arr[i].data : "";
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
public string ReadMainProgramIdentifier(ushort hndl, out short ret)
|
||||||
|
{
|
||||||
|
byte[] buf = new byte[242];
|
||||||
|
ret = Focas1.cnc_pdf_rdmain(hndl, buf);
|
||||||
|
return ASCIIEncoding.ASCII.GetString(buf).TrimEnd('\0');
|
||||||
|
}
|
||||||
|
|
||||||
|
public short ReadActiveProgramComment(ushort hndl, byte[] buffer, out int charsRead)
|
||||||
|
{
|
||||||
|
uint len = (uint)buffer.Length;
|
||||||
|
uint rows = 1;
|
||||||
|
byte[] main = new byte[242];
|
||||||
|
Focas1.cnc_pdf_rdmain(hndl, main);
|
||||||
|
string mainPath = ASCIIEncoding.ASCII.GetString(main).TrimEnd('\0');
|
||||||
|
short ret = Focas1.cnc_rdpdf_line(hndl, mainPath, 0, buffer, ref rows, ref len);
|
||||||
|
charsRead = (int)len;
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
Cnc/Fanuc/libs/Fwlib32.dll
Executable file
BIN
Cnc/Fanuc/libs/Fwlib32.dll
Executable file
Binary file not shown.
BIN
Cnc/Fanuc/libs/fwlibNCG.dll
Executable file
BIN
Cnc/Fanuc/libs/fwlibNCG.dll
Executable file
Binary file not shown.
BIN
Cnc/Fanuc/libs/fwlibe1.dll
Executable file
BIN
Cnc/Fanuc/libs/fwlibe1.dll
Executable file
Binary file not shown.
15
Cnc/Heidenhain/HeidenhainConnectionConfig.cs
Normal file
15
Cnc/Heidenhain/HeidenhainConnectionConfig.cs
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
using System;
|
||||||
|
|
||||||
|
namespace FanucProgramManager.Cnc.Heidenhain
|
||||||
|
{
|
||||||
|
public sealed class HeidenhainConnectionConfig
|
||||||
|
{
|
||||||
|
public string Name { get; set; }
|
||||||
|
public string IpAddress { get; set; }
|
||||||
|
/// <summary>LSV2 data port — default 19000.</summary>
|
||||||
|
public int Port { get; set; } = 19000;
|
||||||
|
public string Username { get; set; } = string.Empty;
|
||||||
|
public string Password { get; set; } = string.Empty;
|
||||||
|
public TimeSpan ConnectTimeout { get; set; } = TimeSpan.FromSeconds(5);
|
||||||
|
}
|
||||||
|
}
|
||||||
201
Cnc/Heidenhain/HeidenhainMachine.cs
Normal file
201
Cnc/Heidenhain/HeidenhainMachine.cs
Normal file
|
|
@ -0,0 +1,201 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using FanucProgramManager.Cnc;
|
||||||
|
using FanucProgramManager.Cnc.Models;
|
||||||
|
|
||||||
|
namespace FanucProgramManager.Cnc.Heidenhain
|
||||||
|
{
|
||||||
|
public sealed class HeidenhainMachine : ICncMachine
|
||||||
|
{
|
||||||
|
private readonly HeidenhainConnectionConfig _config;
|
||||||
|
private readonly INcProgramValidator _validator;
|
||||||
|
private readonly List<CncError> _errors = new List<CncError>();
|
||||||
|
private readonly SemaphoreSlim _gate = new SemaphoreSlim(1, 1);
|
||||||
|
private ILsv2Client _client;
|
||||||
|
private ConnectionState _state = ConnectionState.Disconnected;
|
||||||
|
|
||||||
|
public HeidenhainMachine(HeidenhainConnectionConfig config)
|
||||||
|
{
|
||||||
|
if (config == null) throw new ArgumentNullException("config");
|
||||||
|
_config = config;
|
||||||
|
_validator = new NcProgramValidator(CncManufacturer.Heidenhain);
|
||||||
|
}
|
||||||
|
|
||||||
|
internal HeidenhainMachine(HeidenhainConnectionConfig config, ILsv2Client client,
|
||||||
|
INcProgramValidator validator = null)
|
||||||
|
{
|
||||||
|
_config = config ?? throw new ArgumentNullException("config");
|
||||||
|
_client = client ?? throw new ArgumentNullException("client");
|
||||||
|
_validator = validator ?? new NcProgramValidator(CncManufacturer.Heidenhain);
|
||||||
|
_state = ConnectionState.Connected;
|
||||||
|
}
|
||||||
|
|
||||||
|
public string Name => _config.Name;
|
||||||
|
public ConnectionState State => _state;
|
||||||
|
|
||||||
|
public event EventHandler<ConnectionState> StateChanged;
|
||||||
|
|
||||||
|
public Task<CncResult<bool>> ConnectAsync(CancellationToken ct = default(CancellationToken))
|
||||||
|
{
|
||||||
|
return RunGuardedAsync(() =>
|
||||||
|
{
|
||||||
|
SetState(ConnectionState.Connecting);
|
||||||
|
_client = new Lsv2Client(_config);
|
||||||
|
if (!_client.Connect())
|
||||||
|
{
|
||||||
|
SetState(ConnectionState.Faulted);
|
||||||
|
_errors.Add(new CncError(0, "Connect", "LSV2 connection or login failed"));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
SetState(ConnectionState.Connected);
|
||||||
|
return true;
|
||||||
|
}, ct);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task DisconnectAsync()
|
||||||
|
{
|
||||||
|
return RunGuardedAsync<bool>(() =>
|
||||||
|
{
|
||||||
|
if (_client != null)
|
||||||
|
{
|
||||||
|
_client.Disconnect();
|
||||||
|
_client = null;
|
||||||
|
}
|
||||||
|
SetState(ConnectionState.Disconnected);
|
||||||
|
return true;
|
||||||
|
}, CancellationToken.None);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<CncResult<string>> ReadProgramAsync(string path, CancellationToken ct = default(CancellationToken))
|
||||||
|
{
|
||||||
|
return RunGuardedAsync(() =>
|
||||||
|
{
|
||||||
|
EnsureConnected();
|
||||||
|
string content = _client.ReceiveFile(path);
|
||||||
|
if (content == null)
|
||||||
|
_errors.Add(new CncError(0, "ReadProgram", "ReceiveFile returned null for: " + path));
|
||||||
|
return content;
|
||||||
|
}, ct);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<CncResult<bool>> WriteProgramAsync(string path, CncProgram program, CancellationToken ct = default(CancellationToken))
|
||||||
|
{
|
||||||
|
return RunGuardedAsync(() =>
|
||||||
|
{
|
||||||
|
EnsureConnected();
|
||||||
|
if (program == null || string.IsNullOrEmpty(program.Content))
|
||||||
|
{
|
||||||
|
_errors.Add(new CncError(0, "WriteProgram", "Program content is empty"));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
var vr = _validator.Validate(program.Name ?? path, program.Content);
|
||||||
|
if (!vr.Success)
|
||||||
|
{
|
||||||
|
string msg = string.Join("; ", vr.Errors
|
||||||
|
.Where(e => e.Severity == ValidationSeverity.Error)
|
||||||
|
.Select(e => e.Message));
|
||||||
|
_errors.Add(new CncError(0, "Validator", msg));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
string ncPath = BuildNcPath(path, program.Name);
|
||||||
|
bool ok = _client.SendFile(ncPath, program.Content);
|
||||||
|
if (!ok) _errors.Add(new CncError(0, "WriteProgram", "SendFile failed for: " + ncPath));
|
||||||
|
return ok;
|
||||||
|
}, ct);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<CncResult<MachineSnapshot>> ReadAsync(CancellationToken ct = default(CancellationToken))
|
||||||
|
{
|
||||||
|
return RunGuardedAsync(() =>
|
||||||
|
{
|
||||||
|
EnsureConnected();
|
||||||
|
return new MachineSnapshot
|
||||||
|
{
|
||||||
|
CapturedAt = DateTime.Now,
|
||||||
|
Mode = MachineMode.Unknown,
|
||||||
|
Run = RunState.Unknown,
|
||||||
|
Motion = MotionState.Unknown,
|
||||||
|
Alarms = new List<string>(),
|
||||||
|
OperatorMessages = new List<string>()
|
||||||
|
};
|
||||||
|
}, ct);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<CncResult<string>> ReadActiveProgramAsync(CancellationToken ct = default(CancellationToken)) =>
|
||||||
|
Task.FromResult(CncResult<string>.Fail("ReadActiveProgram", "Not supported via basic LSV2"));
|
||||||
|
|
||||||
|
public Task<CncResult<bool>> SelectMainProgramAsync(string path, CancellationToken ct = default(CancellationToken)) =>
|
||||||
|
Task.FromResult(CncResult<bool>.Fail("SelectMainProgram", "Not supported via basic LSV2"));
|
||||||
|
|
||||||
|
public Task<CncResult<bool>> DeleteProgramAsync(string path, CancellationToken ct = default(CancellationToken)) =>
|
||||||
|
Task.FromResult(CncResult<bool>.Fail("DeleteProgram", "Not supported via basic LSV2"));
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
try { DisconnectAsync().GetAwaiter().GetResult(); } catch { }
|
||||||
|
try { _gate.Dispose(); } catch { }
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── private ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private async Task<CncResult<T>> RunGuardedAsync<T>(Func<T> body, CancellationToken ct)
|
||||||
|
{
|
||||||
|
await _gate.WaitAsync(ct).ConfigureAwait(false);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return await Task.Run(() =>
|
||||||
|
{
|
||||||
|
_errors.Clear();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
T value = body();
|
||||||
|
return _errors.Count > 0
|
||||||
|
? CncResult<T>.Fail(_errors.ToArray())
|
||||||
|
: CncResult<T>.Ok(value);
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException)
|
||||||
|
{
|
||||||
|
return CncResult<T>.Cancelled();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_errors.Add(new CncError(0, "HeidenhainMachine", ex.Message));
|
||||||
|
return CncResult<T>.Fail(_errors.ToArray());
|
||||||
|
}
|
||||||
|
}, ct).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_gate.Release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void EnsureConnected()
|
||||||
|
{
|
||||||
|
if (_state != ConnectionState.Connected || _client == null)
|
||||||
|
throw new InvalidOperationException("HeidenhainMachine is not connected.");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SetState(ConnectionState next)
|
||||||
|
{
|
||||||
|
if (_state == next) return;
|
||||||
|
_state = next;
|
||||||
|
var h = StateChanged;
|
||||||
|
if (h != null)
|
||||||
|
{
|
||||||
|
try { h(this, next); } catch { }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string BuildNcPath(string folder, string name)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(name)) return folder;
|
||||||
|
string sep = (folder.EndsWith("\\") || folder.EndsWith("/")) ? "" : "\\";
|
||||||
|
string ext = name.Contains(".") ? "" : ".H";
|
||||||
|
return folder + sep + name + ext;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
10
Cnc/Heidenhain/ILsv2Client.cs
Normal file
10
Cnc/Heidenhain/ILsv2Client.cs
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
namespace FanucProgramManager.Cnc.Heidenhain
|
||||||
|
{
|
||||||
|
internal interface ILsv2Client
|
||||||
|
{
|
||||||
|
bool Connect();
|
||||||
|
void Disconnect();
|
||||||
|
string ReceiveFile(string ncPath);
|
||||||
|
bool SendFile(string ncPath, string content);
|
||||||
|
}
|
||||||
|
}
|
||||||
180
Cnc/Heidenhain/Lsv2Client.cs
Normal file
180
Cnc/Heidenhain/Lsv2Client.cs
Normal file
|
|
@ -0,0 +1,180 @@
|
||||||
|
using System;
|
||||||
|
using System.Net.Sockets;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace FanucProgramManager.Cnc.Heidenhain
|
||||||
|
{
|
||||||
|
/// <summary>LSV2 protocol client for Heidenhain TNC controls.</summary>
|
||||||
|
internal sealed class Lsv2Client : ILsv2Client, IDisposable
|
||||||
|
{
|
||||||
|
private TcpClient _tcp;
|
||||||
|
private NetworkStream _stream;
|
||||||
|
private readonly HeidenhainConnectionConfig _config;
|
||||||
|
|
||||||
|
private const ushort T_OK = 0x0000;
|
||||||
|
private const ushort T_SE = 0x0001;
|
||||||
|
private const ushort T_LS_SELECT = 0x00A0;
|
||||||
|
private const ushort T_LS_LOG_IN = 0x00A1;
|
||||||
|
private const ushort T_LS_LOG_OUT = 0x00A2;
|
||||||
|
private const ushort T_R_FL = 0x0023;
|
||||||
|
private const ushort T_S_FL = 0x0024;
|
||||||
|
private const ushort T_FD = 0x0025;
|
||||||
|
|
||||||
|
public Lsv2Client(HeidenhainConnectionConfig config)
|
||||||
|
{
|
||||||
|
_config = config;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool Connect()
|
||||||
|
{
|
||||||
|
_tcp = new TcpClient();
|
||||||
|
_tcp.SendTimeout = (int)_config.ConnectTimeout.TotalMilliseconds;
|
||||||
|
_tcp.ReceiveTimeout = (int)_config.ConnectTimeout.TotalMilliseconds;
|
||||||
|
if (!_tcp.ConnectAsync(_config.IpAddress, _config.Port).Wait(_config.ConnectTimeout))
|
||||||
|
return false;
|
||||||
|
_stream = _tcp.GetStream();
|
||||||
|
return Login();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Disconnect()
|
||||||
|
{
|
||||||
|
try { SendCommand(T_LS_LOG_OUT, new byte[0]); } catch { }
|
||||||
|
try { if (_stream != null) _stream.Close(); } catch { }
|
||||||
|
try { if (_tcp != null) _tcp.Close(); } catch { }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Upload file from NC to PC. Returns file content or null on failure.</summary>
|
||||||
|
public string ReceiveFile(string ncPath)
|
||||||
|
{
|
||||||
|
byte[] pathBytes = Encoding.ASCII.GetBytes(ncPath + "\0");
|
||||||
|
SendCommand(T_R_FL, pathBytes);
|
||||||
|
|
||||||
|
var sb = new StringBuilder();
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
ushort cmd;
|
||||||
|
byte[] data = ReadFrame(out cmd);
|
||||||
|
if (cmd == T_SE || data == null) return null;
|
||||||
|
if (cmd == T_OK) break;
|
||||||
|
if (cmd == T_FD) sb.Append(Encoding.ASCII.GetString(data));
|
||||||
|
}
|
||||||
|
return sb.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Download file from PC to NC.</summary>
|
||||||
|
public bool SendFile(string ncPath, string content)
|
||||||
|
{
|
||||||
|
byte[] pathBytes = Encoding.ASCII.GetBytes(ncPath + "\0");
|
||||||
|
byte[] contentBytes = Encoding.ASCII.GetBytes(content);
|
||||||
|
byte[] payload = new byte[pathBytes.Length + contentBytes.Length];
|
||||||
|
Buffer.BlockCopy(pathBytes, 0, payload, 0, pathBytes.Length);
|
||||||
|
Buffer.BlockCopy(contentBytes, 0, payload, pathBytes.Length, contentBytes.Length);
|
||||||
|
SendCommand(T_S_FL, payload);
|
||||||
|
|
||||||
|
ushort cmd;
|
||||||
|
ReadFrame(out cmd);
|
||||||
|
return cmd == T_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose() => Disconnect();
|
||||||
|
|
||||||
|
// ── private ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private bool Login()
|
||||||
|
{
|
||||||
|
// Select programming mode (3)
|
||||||
|
byte[] mode = { 0x03 };
|
||||||
|
SendCommand(T_LS_SELECT, mode);
|
||||||
|
ushort cmd;
|
||||||
|
ReadFrame(out cmd);
|
||||||
|
if (cmd != T_OK) return false;
|
||||||
|
|
||||||
|
// Authenticate
|
||||||
|
byte[] user = Encoding.ASCII.GetBytes(_config.Username + "\0");
|
||||||
|
byte[] pass = Encoding.ASCII.GetBytes(_config.Password + "\0");
|
||||||
|
byte[] cred = new byte[user.Length + pass.Length];
|
||||||
|
Buffer.BlockCopy(user, 0, cred, 0, user.Length);
|
||||||
|
Buffer.BlockCopy(pass, 0, cred, user.Length, pass.Length);
|
||||||
|
SendCommand(T_LS_LOG_IN, cred);
|
||||||
|
ReadFrame(out cmd);
|
||||||
|
return cmd == T_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SendCommand(ushort cmdCode, byte[] data)
|
||||||
|
{
|
||||||
|
int dataLen = data.Length;
|
||||||
|
ushort size = (ushort)(2 + dataLen);
|
||||||
|
|
||||||
|
byte sizeHi = (byte)(size >> 8);
|
||||||
|
byte sizeLo = (byte)(size & 0xFF);
|
||||||
|
byte cmdHi = (byte)(cmdCode >> 8);
|
||||||
|
byte cmdLo = (byte)(cmdCode & 0xFF);
|
||||||
|
|
||||||
|
byte[] crcInput = new byte[4 + dataLen];
|
||||||
|
crcInput[0] = sizeHi; crcInput[1] = sizeLo;
|
||||||
|
crcInput[2] = cmdHi; crcInput[3] = cmdLo;
|
||||||
|
Buffer.BlockCopy(data, 0, crcInput, 4, dataLen);
|
||||||
|
ushort crc = Crc16(crcInput);
|
||||||
|
|
||||||
|
byte[] frame = new byte[1 + 4 + dataLen + 2];
|
||||||
|
int i = 0;
|
||||||
|
frame[i++] = 0x02;
|
||||||
|
frame[i++] = sizeHi;
|
||||||
|
frame[i++] = sizeLo;
|
||||||
|
frame[i++] = cmdHi;
|
||||||
|
frame[i++] = cmdLo;
|
||||||
|
Buffer.BlockCopy(data, 0, frame, i, dataLen);
|
||||||
|
i += dataLen;
|
||||||
|
frame[i++] = (byte)(crc >> 8);
|
||||||
|
frame[i] = (byte)(crc & 0xFF);
|
||||||
|
|
||||||
|
_stream.Write(frame, 0, frame.Length);
|
||||||
|
}
|
||||||
|
|
||||||
|
private byte[] ReadFrame(out ushort cmd)
|
||||||
|
{
|
||||||
|
cmd = 0xFFFF;
|
||||||
|
int stx = _stream.ReadByte();
|
||||||
|
if (stx != 0x02) return null;
|
||||||
|
|
||||||
|
byte sizeHi = (byte)_stream.ReadByte();
|
||||||
|
byte sizeLo = (byte)_stream.ReadByte();
|
||||||
|
int size = (sizeHi << 8) | sizeLo;
|
||||||
|
if (size < 2) return null;
|
||||||
|
|
||||||
|
byte cmdHi = (byte)_stream.ReadByte();
|
||||||
|
byte cmdLo = (byte)_stream.ReadByte();
|
||||||
|
cmd = (ushort)((cmdHi << 8) | cmdLo);
|
||||||
|
|
||||||
|
int dataLen = size - 2;
|
||||||
|
byte[] data = new byte[dataLen];
|
||||||
|
int read = 0;
|
||||||
|
while (read < dataLen)
|
||||||
|
{
|
||||||
|
int n = _stream.Read(data, read, dataLen - read);
|
||||||
|
if (n == 0) break;
|
||||||
|
read += n;
|
||||||
|
}
|
||||||
|
|
||||||
|
// consume CRC bytes
|
||||||
|
_stream.ReadByte();
|
||||||
|
_stream.ReadByte();
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ushort Crc16(byte[] data)
|
||||||
|
{
|
||||||
|
ushort crc = 0xFFFF;
|
||||||
|
foreach (byte b in data)
|
||||||
|
{
|
||||||
|
crc ^= (ushort)(b << 8);
|
||||||
|
for (int i = 0; i < 8; i++)
|
||||||
|
crc = (crc & 0x8000) != 0
|
||||||
|
? (ushort)((crc << 1) ^ 0x1021)
|
||||||
|
: (ushort)(crc << 1);
|
||||||
|
}
|
||||||
|
return crc;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
26
Cnc/ICncMachine.cs
Executable file
26
Cnc/ICncMachine.cs
Executable file
|
|
@ -0,0 +1,26 @@
|
||||||
|
using System;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using FanucProgramManager.Cnc.Models;
|
||||||
|
|
||||||
|
namespace FanucProgramManager.Cnc
|
||||||
|
{
|
||||||
|
public interface ICncMachine : IDisposable
|
||||||
|
{
|
||||||
|
string Name { get; }
|
||||||
|
ConnectionState State { get; }
|
||||||
|
|
||||||
|
event EventHandler<ConnectionState> StateChanged;
|
||||||
|
|
||||||
|
Task<CncResult<bool>> ConnectAsync(CancellationToken ct = default);
|
||||||
|
Task DisconnectAsync();
|
||||||
|
|
||||||
|
Task<CncResult<MachineSnapshot>> ReadAsync(CancellationToken ct = default);
|
||||||
|
|
||||||
|
Task<CncResult<string>> ReadActiveProgramAsync(CancellationToken ct = default);
|
||||||
|
Task<CncResult<string>> ReadProgramAsync(string path, CancellationToken ct = default);
|
||||||
|
Task<CncResult<bool>> WriteProgramAsync(string path, CncProgram program, CancellationToken ct = default);
|
||||||
|
Task<CncResult<bool>> SelectMainProgramAsync(string path, CancellationToken ct = default);
|
||||||
|
Task<CncResult<bool>> DeleteProgramAsync(string path, CancellationToken ct = default);
|
||||||
|
}
|
||||||
|
}
|
||||||
15
Cnc/IFanucMachine.cs
Normal file
15
Cnc/IFanucMachine.cs
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using FanucProgramManager.Cnc.Models;
|
||||||
|
|
||||||
|
namespace FanucProgramManager.Cnc
|
||||||
|
{
|
||||||
|
/// <summary>Fanuc-specific extensions: tool offset and work zero offset transfer.</summary>
|
||||||
|
public interface IFanucMachine : ICncMachine
|
||||||
|
{
|
||||||
|
Task<CncResult<string>> ReadToolDataAsync(string path, CancellationToken ct = default(CancellationToken));
|
||||||
|
Task<CncResult<string>> ReadWorkZeroDataAsync(string path, CancellationToken ct = default(CancellationToken));
|
||||||
|
Task<CncResult<bool>> WriteToolDataAsync(string folderPath, string content, CancellationToken ct = default(CancellationToken));
|
||||||
|
Task<CncResult<bool>> WriteWorkZeroDataAsync(string folderPath, string content, CancellationToken ct = default(CancellationToken));
|
||||||
|
}
|
||||||
|
}
|
||||||
12
Cnc/INcProgramValidator.cs
Normal file
12
Cnc/INcProgramValidator.cs
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
using FanucProgramManager.Cnc.Models;
|
||||||
|
|
||||||
|
namespace FanucProgramManager.Cnc
|
||||||
|
{
|
||||||
|
/// <summary>Validates NC program content before upload to a CNC controller.</summary>
|
||||||
|
public interface INcProgramValidator
|
||||||
|
{
|
||||||
|
/// <param name="programName">File name or program identifier (used for name-format checks).</param>
|
||||||
|
/// <param name="content">Full program content string.</param>
|
||||||
|
ValidationResult Validate(string programName, string content);
|
||||||
|
}
|
||||||
|
}
|
||||||
10
Cnc/Mitsubishi/IMitsubishiFtpClient.cs
Normal file
10
Cnc/Mitsubishi/IMitsubishiFtpClient.cs
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
namespace FanucProgramManager.Cnc.Mitsubishi
|
||||||
|
{
|
||||||
|
internal interface IMitsubishiFtpClient
|
||||||
|
{
|
||||||
|
bool Ping();
|
||||||
|
string DownloadFile(string ncPath);
|
||||||
|
void UploadFile(string ncPath, string content);
|
||||||
|
void DeleteFile(string ncPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
16
Cnc/Mitsubishi/MitsubishiConnectionConfig.cs
Normal file
16
Cnc/Mitsubishi/MitsubishiConnectionConfig.cs
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
using System;
|
||||||
|
|
||||||
|
namespace FanucProgramManager.Cnc.Mitsubishi
|
||||||
|
{
|
||||||
|
public sealed class MitsubishiConnectionConfig
|
||||||
|
{
|
||||||
|
public string Name { get; set; }
|
||||||
|
public string IpAddress { get; set; }
|
||||||
|
public int Port { get; set; } = 21;
|
||||||
|
public string Username { get; set; } = string.Empty;
|
||||||
|
public string Password { get; set; } = string.Empty;
|
||||||
|
/// <summary>NC directory for programs on the Mitsubishi FTP server.</summary>
|
||||||
|
public string ProgramDirectory { get; set; } = "/PRG/";
|
||||||
|
public TimeSpan RequestTimeout { get; set; } = TimeSpan.FromSeconds(10);
|
||||||
|
}
|
||||||
|
}
|
||||||
71
Cnc/Mitsubishi/MitsubishiFtpClient.cs
Normal file
71
Cnc/Mitsubishi/MitsubishiFtpClient.cs
Normal file
|
|
@ -0,0 +1,71 @@
|
||||||
|
using System;
|
||||||
|
using System.IO;
|
||||||
|
using System.Net;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace FanucProgramManager.Cnc.Mitsubishi
|
||||||
|
{
|
||||||
|
/// <summary>FTP client for Mitsubishi M-series CNC program transfer.</summary>
|
||||||
|
internal sealed class MitsubishiFtpClient : IMitsubishiFtpClient
|
||||||
|
{
|
||||||
|
private readonly MitsubishiConnectionConfig _config;
|
||||||
|
|
||||||
|
public MitsubishiFtpClient(MitsubishiConnectionConfig config)
|
||||||
|
{
|
||||||
|
_config = config ?? throw new ArgumentNullException("config");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Verify FTP server is reachable by listing the program directory.</summary>
|
||||||
|
public bool Ping()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var req = CreateRequest(_config.ProgramDirectory, WebRequestMethods.Ftp.ListDirectory);
|
||||||
|
using (var resp = (FtpWebResponse)req.GetResponse())
|
||||||
|
return resp.StatusCode == FtpStatusCode.DataAlreadyOpen
|
||||||
|
|| resp.StatusCode == FtpStatusCode.OpeningData
|
||||||
|
|| (int)resp.StatusCode / 100 == 2;
|
||||||
|
}
|
||||||
|
catch { return false; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Download file from Mitsubishi to string.</summary>
|
||||||
|
public string DownloadFile(string ncPath)
|
||||||
|
{
|
||||||
|
var req = CreateRequest(ncPath, WebRequestMethods.Ftp.DownloadFile);
|
||||||
|
using (var resp = (FtpWebResponse)req.GetResponse())
|
||||||
|
using (var reader = new StreamReader(resp.GetResponseStream(), Encoding.ASCII))
|
||||||
|
return reader.ReadToEnd();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Upload string content to Mitsubishi.</summary>
|
||||||
|
public void UploadFile(string ncPath, string content)
|
||||||
|
{
|
||||||
|
var req = CreateRequest(ncPath, WebRequestMethods.Ftp.UploadFile);
|
||||||
|
byte[] data = Encoding.ASCII.GetBytes(content);
|
||||||
|
req.ContentLength = data.Length;
|
||||||
|
using (var stream = req.GetRequestStream())
|
||||||
|
stream.Write(data, 0, data.Length);
|
||||||
|
using ((FtpWebResponse)req.GetResponse()) { }
|
||||||
|
}
|
||||||
|
|
||||||
|
public void DeleteFile(string ncPath)
|
||||||
|
{
|
||||||
|
var req = CreateRequest(ncPath, WebRequestMethods.Ftp.DeleteFile);
|
||||||
|
using ((FtpWebResponse)req.GetResponse()) { }
|
||||||
|
}
|
||||||
|
|
||||||
|
private FtpWebRequest CreateRequest(string path, string method)
|
||||||
|
{
|
||||||
|
string uri = string.Format("ftp://{0}:{1}{2}", _config.IpAddress, _config.Port, path);
|
||||||
|
var req = (FtpWebRequest)WebRequest.Create(uri);
|
||||||
|
req.Method = method;
|
||||||
|
req.Credentials = new NetworkCredential(_config.Username, _config.Password);
|
||||||
|
req.Timeout = (int)_config.RequestTimeout.TotalMilliseconds;
|
||||||
|
req.KeepAlive = false;
|
||||||
|
req.UseBinary = false;
|
||||||
|
req.UsePassive = true;
|
||||||
|
return req;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
210
Cnc/Mitsubishi/MitsubishiMachine.cs
Normal file
210
Cnc/Mitsubishi/MitsubishiMachine.cs
Normal file
|
|
@ -0,0 +1,210 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using FanucProgramManager.Cnc;
|
||||||
|
using FanucProgramManager.Cnc.Models;
|
||||||
|
|
||||||
|
namespace FanucProgramManager.Cnc.Mitsubishi
|
||||||
|
{
|
||||||
|
public sealed class MitsubishiMachine : ICncMachine
|
||||||
|
{
|
||||||
|
private readonly MitsubishiConnectionConfig _config;
|
||||||
|
private readonly IMitsubishiFtpClient _ftp;
|
||||||
|
private readonly INcProgramValidator _validator;
|
||||||
|
private readonly List<CncError> _errors = new List<CncError>();
|
||||||
|
private readonly SemaphoreSlim _gate = new SemaphoreSlim(1, 1);
|
||||||
|
private ConnectionState _state = ConnectionState.Disconnected;
|
||||||
|
|
||||||
|
public MitsubishiMachine(MitsubishiConnectionConfig config)
|
||||||
|
{
|
||||||
|
_config = config ?? throw new ArgumentNullException("config");
|
||||||
|
_ftp = new MitsubishiFtpClient(config);
|
||||||
|
_validator = new NcProgramValidator(CncManufacturer.Mitsubishi);
|
||||||
|
}
|
||||||
|
|
||||||
|
internal MitsubishiMachine(MitsubishiConnectionConfig config, IMitsubishiFtpClient ftpClient,
|
||||||
|
INcProgramValidator validator = null)
|
||||||
|
{
|
||||||
|
_config = config ?? throw new ArgumentNullException("config");
|
||||||
|
_ftp = ftpClient ?? throw new ArgumentNullException("ftpClient");
|
||||||
|
_validator = validator ?? new NcProgramValidator(CncManufacturer.Mitsubishi);
|
||||||
|
}
|
||||||
|
|
||||||
|
public string Name { get { return _config.Name; } }
|
||||||
|
public ConnectionState State { get { return _state; } }
|
||||||
|
public event EventHandler<ConnectionState> StateChanged;
|
||||||
|
|
||||||
|
public Task<CncResult<bool>> ConnectAsync(CancellationToken ct = default(CancellationToken))
|
||||||
|
{
|
||||||
|
return RunGuardedAsync(() =>
|
||||||
|
{
|
||||||
|
SetState(ConnectionState.Connecting);
|
||||||
|
if (!_ftp.Ping())
|
||||||
|
{
|
||||||
|
SetState(ConnectionState.Faulted);
|
||||||
|
_errors.Add(new CncError(0, "Connect",
|
||||||
|
string.Format("FTP server unreachable at {0}:{1}", _config.IpAddress, _config.Port)));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
SetState(ConnectionState.Connected);
|
||||||
|
return true;
|
||||||
|
}, ct);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task DisconnectAsync()
|
||||||
|
{
|
||||||
|
return RunGuardedAsync<bool>(() =>
|
||||||
|
{
|
||||||
|
SetState(ConnectionState.Disconnected);
|
||||||
|
return true;
|
||||||
|
}, CancellationToken.None);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<CncResult<string>> ReadProgramAsync(string path, CancellationToken ct = default(CancellationToken))
|
||||||
|
{
|
||||||
|
return RunGuardedAsync(() =>
|
||||||
|
{
|
||||||
|
EnsureConnected();
|
||||||
|
string ncPath = ResolvePath(path, null);
|
||||||
|
try { return _ftp.DownloadFile(ncPath); }
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_errors.Add(new CncError(0, "ReadProgram", ex.Message));
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}, ct);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<CncResult<bool>> WriteProgramAsync(string path, CncProgram program, CancellationToken ct = default(CancellationToken))
|
||||||
|
{
|
||||||
|
return RunGuardedAsync(() =>
|
||||||
|
{
|
||||||
|
EnsureConnected();
|
||||||
|
if (program == null || string.IsNullOrEmpty(program.Content))
|
||||||
|
{
|
||||||
|
_errors.Add(new CncError(0, "WriteProgram", "Content is empty"));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
var vr = _validator.Validate(program.Name ?? path, program.Content);
|
||||||
|
if (!vr.Success)
|
||||||
|
{
|
||||||
|
string msg = string.Join("; ", vr.Errors
|
||||||
|
.Where(e => e.Severity == ValidationSeverity.Error)
|
||||||
|
.Select(e => e.Message));
|
||||||
|
_errors.Add(new CncError(0, "Validator", msg));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
string ncPath = ResolvePath(path, program.Name);
|
||||||
|
try { _ftp.UploadFile(ncPath, program.Content); return true; }
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_errors.Add(new CncError(0, "WriteProgram", ex.Message));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}, ct);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<CncResult<bool>> DeleteProgramAsync(string path, CancellationToken ct = default(CancellationToken))
|
||||||
|
{
|
||||||
|
return RunGuardedAsync(() =>
|
||||||
|
{
|
||||||
|
EnsureConnected();
|
||||||
|
try { _ftp.DeleteFile(path); return true; }
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_errors.Add(new CncError(0, "DeleteProgram", ex.Message));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}, ct);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<CncResult<MachineSnapshot>> ReadAsync(CancellationToken ct = default(CancellationToken))
|
||||||
|
{
|
||||||
|
return RunGuardedAsync(() =>
|
||||||
|
{
|
||||||
|
EnsureConnected();
|
||||||
|
return new MachineSnapshot
|
||||||
|
{
|
||||||
|
CapturedAt = DateTime.Now,
|
||||||
|
Mode = MachineMode.Unknown,
|
||||||
|
Run = RunState.Unknown,
|
||||||
|
Motion = MotionState.Unknown,
|
||||||
|
Alarms = new List<string>(),
|
||||||
|
OperatorMessages = new List<string>()
|
||||||
|
};
|
||||||
|
}, ct);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<CncResult<string>> ReadActiveProgramAsync(CancellationToken ct = default(CancellationToken))
|
||||||
|
{
|
||||||
|
return Task.FromResult(CncResult<string>.Fail("ReadActiveProgram", "Not supported via FTP"));
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<CncResult<bool>> SelectMainProgramAsync(string path, CancellationToken ct = default(CancellationToken))
|
||||||
|
{
|
||||||
|
return Task.FromResult(CncResult<bool>.Fail("SelectMainProgram", "Not supported via FTP"));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
try { DisconnectAsync().GetAwaiter().GetResult(); } catch { }
|
||||||
|
try { _gate.Dispose(); } catch { }
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<CncResult<T>> RunGuardedAsync<T>(Func<T> body, CancellationToken ct)
|
||||||
|
{
|
||||||
|
await _gate.WaitAsync(ct).ConfigureAwait(false);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return await Task.Run(() =>
|
||||||
|
{
|
||||||
|
_errors.Clear();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
T value = body();
|
||||||
|
return _errors.Count > 0
|
||||||
|
? CncResult<T>.Fail(_errors.ToArray())
|
||||||
|
: CncResult<T>.Ok(value);
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException) { return CncResult<T>.Cancelled(); }
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_errors.Add(new CncError(0, "MitsubishiMachine", ex.Message));
|
||||||
|
return CncResult<T>.Fail(_errors.ToArray());
|
||||||
|
}
|
||||||
|
}, ct).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
finally { _gate.Release(); }
|
||||||
|
}
|
||||||
|
|
||||||
|
private void EnsureConnected()
|
||||||
|
{
|
||||||
|
if (_state != ConnectionState.Connected)
|
||||||
|
throw new InvalidOperationException("MitsubishiMachine is not connected.");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SetState(ConnectionState next)
|
||||||
|
{
|
||||||
|
if (_state == next) return;
|
||||||
|
_state = next;
|
||||||
|
var h = StateChanged;
|
||||||
|
if (h != null)
|
||||||
|
{
|
||||||
|
try { h(this, next); } catch { }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Build full FTP path. Mitsubishi programs have no extension.</summary>
|
||||||
|
private string ResolvePath(string folder, string name)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(name))
|
||||||
|
return folder.Contains("/") ? folder : _config.ProgramDirectory + folder;
|
||||||
|
|
||||||
|
string dir = folder ?? _config.ProgramDirectory;
|
||||||
|
if (!dir.EndsWith("/")) dir += "/";
|
||||||
|
return dir + name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
25
Cnc/Models/CncError.cs
Executable file
25
Cnc/Models/CncError.cs
Executable file
|
|
@ -0,0 +1,25 @@
|
||||||
|
using System;
|
||||||
|
|
||||||
|
namespace FanucProgramManager.Cnc.Models
|
||||||
|
{
|
||||||
|
public sealed class CncError
|
||||||
|
{
|
||||||
|
public int Code { get; }
|
||||||
|
public string Source { get; }
|
||||||
|
public string Message { get; }
|
||||||
|
public DateTime At { get; }
|
||||||
|
|
||||||
|
public CncError(int code, string source, string message)
|
||||||
|
{
|
||||||
|
Code = code;
|
||||||
|
Source = source ?? "";
|
||||||
|
Message = message ?? "";
|
||||||
|
At = DateTime.Now;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override string ToString()
|
||||||
|
{
|
||||||
|
return "[" + Code + "] " + Source + ": " + Message;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
4
Cnc/Models/CncManufacturer.cs
Normal file
4
Cnc/Models/CncManufacturer.cs
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
namespace FanucProgramManager.Cnc.Models
|
||||||
|
{
|
||||||
|
public enum CncManufacturer { Fanuc, Heidenhain, Siemens, Mitsubishi }
|
||||||
|
}
|
||||||
10
Cnc/Models/CncProgram.cs
Executable file
10
Cnc/Models/CncProgram.cs
Executable file
|
|
@ -0,0 +1,10 @@
|
||||||
|
namespace FanucProgramManager.Cnc.Models
|
||||||
|
{
|
||||||
|
public sealed class CncProgram
|
||||||
|
{
|
||||||
|
public string Name { get; set; }
|
||||||
|
public string Comment { get; set; }
|
||||||
|
public string Content { get; set; }
|
||||||
|
public string Path { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
41
Cnc/Models/CncResult.cs
Executable file
41
Cnc/Models/CncResult.cs
Executable file
|
|
@ -0,0 +1,41 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace FanucProgramManager.Cnc.Models
|
||||||
|
{
|
||||||
|
/// <summary>Result wrapper for all CNC operations.</summary>
|
||||||
|
public sealed class CncResult<T>
|
||||||
|
{
|
||||||
|
public bool Success { get; }
|
||||||
|
public T Value { get; }
|
||||||
|
public IReadOnlyList<CncError> Errors { get; }
|
||||||
|
public bool WasCancelled { get; }
|
||||||
|
|
||||||
|
private CncResult(bool success, T value, IReadOnlyList<CncError> errors, bool cancelled)
|
||||||
|
{
|
||||||
|
Success = success;
|
||||||
|
Value = value;
|
||||||
|
Errors = errors ?? (IReadOnlyList<CncError>)Array.Empty<CncError>();
|
||||||
|
WasCancelled = cancelled;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static CncResult<T> Ok(T value) =>
|
||||||
|
new CncResult<T>(true, value, null, false);
|
||||||
|
|
||||||
|
public static CncResult<T> Fail(IReadOnlyList<CncError> errors) =>
|
||||||
|
new CncResult<T>(false, default(T), errors, false);
|
||||||
|
|
||||||
|
public static CncResult<T> Fail(string source, string message) =>
|
||||||
|
Fail(new[] { new CncError(0, source, message) });
|
||||||
|
|
||||||
|
public static CncResult<T> Cancelled() =>
|
||||||
|
new CncResult<T>(false, default(T), null, true);
|
||||||
|
|
||||||
|
public override string ToString()
|
||||||
|
{
|
||||||
|
if (WasCancelled) return $"CncResult<{typeof(T).Name}>: Cancelled";
|
||||||
|
if (!Success) return $"CncResult<{typeof(T).Name}>: Fail ({Errors.Count} errors)";
|
||||||
|
return $"CncResult<{typeof(T).Name}>: Ok";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
10
Cnc/Models/ConnectionState.cs
Executable file
10
Cnc/Models/ConnectionState.cs
Executable file
|
|
@ -0,0 +1,10 @@
|
||||||
|
namespace FanucProgramManager.Cnc.Models
|
||||||
|
{
|
||||||
|
public enum ConnectionState
|
||||||
|
{
|
||||||
|
Disconnected,
|
||||||
|
Connecting,
|
||||||
|
Connected,
|
||||||
|
Faulted
|
||||||
|
}
|
||||||
|
}
|
||||||
12
Cnc/Models/MachineMode.cs
Executable file
12
Cnc/Models/MachineMode.cs
Executable file
|
|
@ -0,0 +1,12 @@
|
||||||
|
namespace FanucProgramManager.Cnc.Models
|
||||||
|
{
|
||||||
|
public enum MachineMode
|
||||||
|
{
|
||||||
|
Mdi = 0,
|
||||||
|
Auto = 1,
|
||||||
|
Edit = 3,
|
||||||
|
Handle = 4,
|
||||||
|
Jog = 5,
|
||||||
|
Unknown = 255
|
||||||
|
}
|
||||||
|
}
|
||||||
22
Cnc/Models/MachineSnapshot.cs
Executable file
22
Cnc/Models/MachineSnapshot.cs
Executable file
|
|
@ -0,0 +1,22 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace FanucProgramManager.Cnc.Models
|
||||||
|
{
|
||||||
|
public sealed class MachineSnapshot
|
||||||
|
{
|
||||||
|
public string MainProgram { get; set; }
|
||||||
|
public string SubProgram { get; set; }
|
||||||
|
public string ProgramComment { get; set; }
|
||||||
|
public MachineMode Mode { get; set; }
|
||||||
|
public RunState Run { get; set; }
|
||||||
|
public MotionState Motion { get; set; }
|
||||||
|
public bool Emergency { get; set; }
|
||||||
|
public bool AlarmActive { get; set; }
|
||||||
|
public TimeSpan CycleTime { get; set; }
|
||||||
|
public long PieceCount { get; set; }
|
||||||
|
public IReadOnlyList<string> Alarms { get; set; }
|
||||||
|
public IReadOnlyList<string> OperatorMessages { get; set; }
|
||||||
|
public DateTime CapturedAt { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
10
Cnc/Models/MotionState.cs
Executable file
10
Cnc/Models/MotionState.cs
Executable file
|
|
@ -0,0 +1,10 @@
|
||||||
|
namespace FanucProgramManager.Cnc.Models
|
||||||
|
{
|
||||||
|
public enum MotionState
|
||||||
|
{
|
||||||
|
Idle = 0,
|
||||||
|
Moving = 1,
|
||||||
|
Hold = 2,
|
||||||
|
Unknown = 255
|
||||||
|
}
|
||||||
|
}
|
||||||
9
Cnc/Models/RunState.cs
Executable file
9
Cnc/Models/RunState.cs
Executable file
|
|
@ -0,0 +1,9 @@
|
||||||
|
namespace FanucProgramManager.Cnc.Models
|
||||||
|
{
|
||||||
|
public enum RunState
|
||||||
|
{
|
||||||
|
Stop = 1,
|
||||||
|
ExecutingIso = 3,
|
||||||
|
Unknown = 255
|
||||||
|
}
|
||||||
|
}
|
||||||
21
Cnc/Models/ValidationError.cs
Normal file
21
Cnc/Models/ValidationError.cs
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
namespace FanucProgramManager.Cnc.Models
|
||||||
|
{
|
||||||
|
public sealed class ValidationError
|
||||||
|
{
|
||||||
|
public int Line { get; }
|
||||||
|
public string Message { get; }
|
||||||
|
public ValidationSeverity Severity { get; }
|
||||||
|
|
||||||
|
public ValidationError(int line, string message, ValidationSeverity severity)
|
||||||
|
{
|
||||||
|
Line = line;
|
||||||
|
Message = message ?? "";
|
||||||
|
Severity = severity;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override string ToString()
|
||||||
|
{
|
||||||
|
return string.Format("[{0}] Line {1}: {2}", Severity, Line, Message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
32
Cnc/Models/ValidationResult.cs
Normal file
32
Cnc/Models/ValidationResult.cs
Normal file
|
|
@ -0,0 +1,32 @@
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace FanucProgramManager.Cnc.Models
|
||||||
|
{
|
||||||
|
public sealed class ValidationResult
|
||||||
|
{
|
||||||
|
public bool Success { get; }
|
||||||
|
public IReadOnlyList<ValidationError> Errors { get; }
|
||||||
|
|
||||||
|
private ValidationResult(bool success, IReadOnlyList<ValidationError> errors)
|
||||||
|
{
|
||||||
|
Success = success;
|
||||||
|
Errors = errors ?? new ValidationError[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
public static ValidationResult Ok()
|
||||||
|
{
|
||||||
|
return new ValidationResult(true, new ValidationError[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static ValidationResult Fail(IReadOnlyList<ValidationError> errors)
|
||||||
|
{
|
||||||
|
return new ValidationResult(false, errors);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Success result that still carries warning-level entries.</summary>
|
||||||
|
public static ValidationResult OkWithWarnings(IReadOnlyList<ValidationError> warnings)
|
||||||
|
{
|
||||||
|
return new ValidationResult(true, warnings);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
4
Cnc/Models/ValidationSeverity.cs
Normal file
4
Cnc/Models/ValidationSeverity.cs
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
namespace FanucProgramManager.Cnc.Models
|
||||||
|
{
|
||||||
|
public enum ValidationSeverity { Error, Warning }
|
||||||
|
}
|
||||||
215
Cnc/NcProgramValidator.cs
Normal file
215
Cnc/NcProgramValidator.cs
Normal file
|
|
@ -0,0 +1,215 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
using FanucProgramManager.Cnc.Models;
|
||||||
|
|
||||||
|
namespace FanucProgramManager.Cnc
|
||||||
|
{
|
||||||
|
/// <summary>Validates NC program content per-manufacturer before upload.</summary>
|
||||||
|
public sealed class NcProgramValidator : INcProgramValidator
|
||||||
|
{
|
||||||
|
private const int MaxBlockLength = 80;
|
||||||
|
private const int MaxCommentLength = 32;
|
||||||
|
|
||||||
|
private static readonly Regex FanucNamePattern = new Regex(@"^O\d{4}$", RegexOptions.Compiled);
|
||||||
|
private static readonly Regex FanucFirstLinePattern = new Regex(@"^O\d{4}", RegexOptions.Compiled);
|
||||||
|
|
||||||
|
private readonly CncManufacturer _manufacturer;
|
||||||
|
|
||||||
|
public NcProgramValidator(CncManufacturer manufacturer)
|
||||||
|
{
|
||||||
|
_manufacturer = manufacturer;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ValidationResult Validate(string programName, string content)
|
||||||
|
{
|
||||||
|
var errors = new List<ValidationError>();
|
||||||
|
|
||||||
|
// Name-format check (uses just the bare filename without path)
|
||||||
|
string name = ExtractName(programName);
|
||||||
|
ValidateName(name, errors);
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(content))
|
||||||
|
return errors.Count == 0 ? ValidationResult.Ok() : ValidationResult.Fail(errors);
|
||||||
|
|
||||||
|
// Normalise: strip CR, split on LF
|
||||||
|
string normalized = content.Replace("\r", "");
|
||||||
|
string[] lines = normalized.Split('\n');
|
||||||
|
|
||||||
|
// First-line check (Fanuc/Mitsubishi)
|
||||||
|
if ((_manufacturer == CncManufacturer.Fanuc || _manufacturer == CncManufacturer.Mitsubishi)
|
||||||
|
&& lines.Length > 0)
|
||||||
|
{
|
||||||
|
string first = lines[0].Trim();
|
||||||
|
if (!FanucFirstLinePattern.IsMatch(first))
|
||||||
|
errors.Add(new ValidationError(1, "First line must be O#### program number", ValidationSeverity.Error));
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 0; i < lines.Length; i++)
|
||||||
|
{
|
||||||
|
int lineNum = i + 1;
|
||||||
|
string line = lines[i];
|
||||||
|
|
||||||
|
// Tab check
|
||||||
|
if (line.IndexOf('\t') >= 0)
|
||||||
|
errors.Add(new ValidationError(lineNum, "Tab character not allowed", ValidationSeverity.Error));
|
||||||
|
|
||||||
|
// Lowercase check
|
||||||
|
for (int ci = 0; ci < line.Length; ci++)
|
||||||
|
{
|
||||||
|
if (char.IsLower(line[ci]))
|
||||||
|
{
|
||||||
|
errors.Add(new ValidationError(lineNum, "Lowercase letters not allowed", ValidationSeverity.Error));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Block length
|
||||||
|
if (line.Length > MaxBlockLength)
|
||||||
|
errors.Add(new ValidationError(lineNum,
|
||||||
|
string.Format("Block length {0} exceeds 80 chars", line.Length),
|
||||||
|
ValidationSeverity.Error));
|
||||||
|
|
||||||
|
// Nested parens + comment length
|
||||||
|
ValidateParensAndComments(line, lineNum, errors);
|
||||||
|
|
||||||
|
// Forbidden chars per manufacturer
|
||||||
|
ValidateForbiddenChars(line, lineNum, errors);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool hasErrors = false;
|
||||||
|
for (int i = 0; i < errors.Count; i++)
|
||||||
|
if (errors[i].Severity == ValidationSeverity.Error) { hasErrors = true; break; }
|
||||||
|
|
||||||
|
if (hasErrors)
|
||||||
|
return ValidationResult.Fail(errors);
|
||||||
|
// Preserve warnings even when overall result is Success
|
||||||
|
return errors.Count == 0 ? ValidationResult.Ok() : ValidationResult.OkWithWarnings(errors);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── private helpers ───────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private void ValidateName(string name, List<ValidationError> errors)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(name)) return;
|
||||||
|
|
||||||
|
switch (_manufacturer)
|
||||||
|
{
|
||||||
|
case CncManufacturer.Fanuc:
|
||||||
|
case CncManufacturer.Mitsubishi:
|
||||||
|
if (!FanucNamePattern.IsMatch(name))
|
||||||
|
errors.Add(new ValidationError(0,
|
||||||
|
string.Format("Program name '{0}' must match O####", name),
|
||||||
|
ValidationSeverity.Error));
|
||||||
|
break;
|
||||||
|
|
||||||
|
case CncManufacturer.Siemens:
|
||||||
|
if (!name.EndsWith(".MPF"))
|
||||||
|
errors.Add(new ValidationError(0,
|
||||||
|
string.Format("Program name '{0}' must end with .MPF", name),
|
||||||
|
ValidationSeverity.Error));
|
||||||
|
break;
|
||||||
|
|
||||||
|
case CncManufacturer.Heidenhain:
|
||||||
|
if (!name.EndsWith(".H"))
|
||||||
|
errors.Add(new ValidationError(0,
|
||||||
|
string.Format("Program name '{0}' must end with .H", name),
|
||||||
|
ValidationSeverity.Error));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ValidateParensAndComments(string line, int lineNum, List<ValidationError> errors)
|
||||||
|
{
|
||||||
|
int depth = 0;
|
||||||
|
int commentStart = -1;
|
||||||
|
|
||||||
|
for (int i = 0; i < line.Length; i++)
|
||||||
|
{
|
||||||
|
char c = line[i];
|
||||||
|
if (c == '(')
|
||||||
|
{
|
||||||
|
depth++;
|
||||||
|
if (depth > 1)
|
||||||
|
{
|
||||||
|
errors.Add(new ValidationError(lineNum, "Nested parentheses not allowed", ValidationSeverity.Error));
|
||||||
|
// Don't keep adding the same error for this line
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
commentStart = i;
|
||||||
|
}
|
||||||
|
else if (c == ')')
|
||||||
|
{
|
||||||
|
if (depth == 1 && commentStart >= 0)
|
||||||
|
{
|
||||||
|
// Extract comment content between the parens
|
||||||
|
int len = i - commentStart - 1;
|
||||||
|
if (len > MaxCommentLength)
|
||||||
|
errors.Add(new ValidationError(lineNum,
|
||||||
|
string.Format("Comment length {0} exceeds 32 chars", len),
|
||||||
|
ValidationSeverity.Error));
|
||||||
|
}
|
||||||
|
if (depth > 0) depth--;
|
||||||
|
commentStart = -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ValidateForbiddenChars(string line, int lineNum, List<ValidationError> errors)
|
||||||
|
{
|
||||||
|
switch (_manufacturer)
|
||||||
|
{
|
||||||
|
case CncManufacturer.Fanuc:
|
||||||
|
case CncManufacturer.Mitsubishi:
|
||||||
|
for (int i = 0; i < line.Length; i++)
|
||||||
|
{
|
||||||
|
char c = line[i];
|
||||||
|
if (c == ';' || c == '[' || c == ']')
|
||||||
|
{
|
||||||
|
errors.Add(new ValidationError(lineNum,
|
||||||
|
string.Format("Forbidden char '{0}' for Fanuc/Mitsubishi", c),
|
||||||
|
ValidationSeverity.Error));
|
||||||
|
}
|
||||||
|
else if (c == '#')
|
||||||
|
{
|
||||||
|
errors.Add(new ValidationError(lineNum,
|
||||||
|
"Macro variable '#' detected — verify macro support on target CNC",
|
||||||
|
ValidationSeverity.Warning));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case CncManufacturer.Siemens:
|
||||||
|
for (int i = 0; i < line.Length; i++)
|
||||||
|
{
|
||||||
|
if (line[i] == ';')
|
||||||
|
errors.Add(new ValidationError(lineNum,
|
||||||
|
"Forbidden char ';' for Siemens (use () for comments)",
|
||||||
|
ValidationSeverity.Error));
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case CncManufacturer.Heidenhain:
|
||||||
|
for (int i = 0; i < line.Length; i++)
|
||||||
|
{
|
||||||
|
char c = line[i];
|
||||||
|
if (c == '%' || c == ';')
|
||||||
|
errors.Add(new ValidationError(lineNum,
|
||||||
|
string.Format("Forbidden char '{0}' for Heidenhain", c),
|
||||||
|
ValidationSeverity.Error));
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Extracts bare filename from a path (strips directory prefix).</summary>
|
||||||
|
private static string ExtractName(string programName)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(programName)) return programName;
|
||||||
|
int slash = programName.LastIndexOf('/');
|
||||||
|
int backslash = programName.LastIndexOf('\\');
|
||||||
|
int sep = slash > backslash ? slash : backslash;
|
||||||
|
return sep >= 0 ? programName.Substring(sep + 1) : programName;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
10
Cnc/Siemens/ISiemensFtpClient.cs
Normal file
10
Cnc/Siemens/ISiemensFtpClient.cs
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
namespace FanucProgramManager.Cnc.Siemens
|
||||||
|
{
|
||||||
|
internal interface ISiemensFtpClient
|
||||||
|
{
|
||||||
|
bool Ping();
|
||||||
|
string DownloadFile(string ncPath);
|
||||||
|
void UploadFile(string ncPath, string content);
|
||||||
|
void DeleteFile(string ncPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
16
Cnc/Siemens/SiemensConnectionConfig.cs
Normal file
16
Cnc/Siemens/SiemensConnectionConfig.cs
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
using System;
|
||||||
|
|
||||||
|
namespace FanucProgramManager.Cnc.Siemens
|
||||||
|
{
|
||||||
|
public sealed class SiemensConnectionConfig
|
||||||
|
{
|
||||||
|
public string Name { get; set; }
|
||||||
|
public string IpAddress { get; set; }
|
||||||
|
public int Port { get; set; } = 21;
|
||||||
|
public string Username { get; set; } = string.Empty;
|
||||||
|
public string Password { get; set; } = string.Empty;
|
||||||
|
/// <summary>NC directory for main programs on the CNC FTP server.</summary>
|
||||||
|
public string ProgramDirectory { get; set; } = "/_N_MPF_DIR/";
|
||||||
|
public TimeSpan RequestTimeout { get; set; } = TimeSpan.FromSeconds(10);
|
||||||
|
}
|
||||||
|
}
|
||||||
71
Cnc/Siemens/SiemensFtpClient.cs
Normal file
71
Cnc/Siemens/SiemensFtpClient.cs
Normal file
|
|
@ -0,0 +1,71 @@
|
||||||
|
using System;
|
||||||
|
using System.IO;
|
||||||
|
using System.Net;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace FanucProgramManager.Cnc.Siemens
|
||||||
|
{
|
||||||
|
/// <summary>FTP client for Siemens Sinumerik CNC program transfer.</summary>
|
||||||
|
internal sealed class SiemensFtpClient : ISiemensFtpClient
|
||||||
|
{
|
||||||
|
private readonly SiemensConnectionConfig _config;
|
||||||
|
|
||||||
|
public SiemensFtpClient(SiemensConnectionConfig config)
|
||||||
|
{
|
||||||
|
_config = config ?? throw new ArgumentNullException("config");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Verify FTP server is reachable by listing the program directory.</summary>
|
||||||
|
public bool Ping()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var req = CreateRequest(_config.ProgramDirectory, WebRequestMethods.Ftp.ListDirectory);
|
||||||
|
using (var resp = (FtpWebResponse)req.GetResponse())
|
||||||
|
return resp.StatusCode == FtpStatusCode.DataAlreadyOpen
|
||||||
|
|| resp.StatusCode == FtpStatusCode.OpeningData
|
||||||
|
|| (int)resp.StatusCode / 100 == 2;
|
||||||
|
}
|
||||||
|
catch { return false; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Download file from Sinumerik to string (upload from CNC perspective).</summary>
|
||||||
|
public string DownloadFile(string ncPath)
|
||||||
|
{
|
||||||
|
var req = CreateRequest(ncPath, WebRequestMethods.Ftp.DownloadFile);
|
||||||
|
using (var resp = (FtpWebResponse)req.GetResponse())
|
||||||
|
using (var reader = new StreamReader(resp.GetResponseStream(), Encoding.ASCII))
|
||||||
|
return reader.ReadToEnd();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Upload string content to Sinumerik (download from CNC perspective).</summary>
|
||||||
|
public void UploadFile(string ncPath, string content)
|
||||||
|
{
|
||||||
|
var req = CreateRequest(ncPath, WebRequestMethods.Ftp.UploadFile);
|
||||||
|
byte[] data = Encoding.ASCII.GetBytes(content);
|
||||||
|
req.ContentLength = data.Length;
|
||||||
|
using (var stream = req.GetRequestStream())
|
||||||
|
stream.Write(data, 0, data.Length);
|
||||||
|
using ((FtpWebResponse)req.GetResponse()) { }
|
||||||
|
}
|
||||||
|
|
||||||
|
public void DeleteFile(string ncPath)
|
||||||
|
{
|
||||||
|
var req = CreateRequest(ncPath, WebRequestMethods.Ftp.DeleteFile);
|
||||||
|
using ((FtpWebResponse)req.GetResponse()) { }
|
||||||
|
}
|
||||||
|
|
||||||
|
private FtpWebRequest CreateRequest(string path, string method)
|
||||||
|
{
|
||||||
|
string uri = string.Format("ftp://{0}:{1}{2}", _config.IpAddress, _config.Port, path);
|
||||||
|
var req = (FtpWebRequest)WebRequest.Create(uri);
|
||||||
|
req.Method = method;
|
||||||
|
req.Credentials = new NetworkCredential(_config.Username, _config.Password);
|
||||||
|
req.Timeout = (int)_config.RequestTimeout.TotalMilliseconds;
|
||||||
|
req.KeepAlive = false;
|
||||||
|
req.UseBinary = false;
|
||||||
|
req.UsePassive = true;
|
||||||
|
return req;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
211
Cnc/Siemens/SiemensMachine.cs
Normal file
211
Cnc/Siemens/SiemensMachine.cs
Normal file
|
|
@ -0,0 +1,211 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using FanucProgramManager.Cnc;
|
||||||
|
using FanucProgramManager.Cnc.Models;
|
||||||
|
|
||||||
|
namespace FanucProgramManager.Cnc.Siemens
|
||||||
|
{
|
||||||
|
public sealed class SiemensMachine : ICncMachine
|
||||||
|
{
|
||||||
|
private readonly SiemensConnectionConfig _config;
|
||||||
|
private readonly ISiemensFtpClient _ftp;
|
||||||
|
private readonly INcProgramValidator _validator;
|
||||||
|
private readonly List<CncError> _errors = new List<CncError>();
|
||||||
|
private readonly SemaphoreSlim _gate = new SemaphoreSlim(1, 1);
|
||||||
|
private ConnectionState _state = ConnectionState.Disconnected;
|
||||||
|
|
||||||
|
public SiemensMachine(SiemensConnectionConfig config)
|
||||||
|
{
|
||||||
|
_config = config ?? throw new ArgumentNullException("config");
|
||||||
|
_ftp = new SiemensFtpClient(config);
|
||||||
|
_validator = new NcProgramValidator(CncManufacturer.Siemens);
|
||||||
|
}
|
||||||
|
|
||||||
|
internal SiemensMachine(SiemensConnectionConfig config, ISiemensFtpClient ftpClient,
|
||||||
|
INcProgramValidator validator = null)
|
||||||
|
{
|
||||||
|
_config = config ?? throw new ArgumentNullException("config");
|
||||||
|
_ftp = ftpClient ?? throw new ArgumentNullException("ftpClient");
|
||||||
|
_validator = validator ?? new NcProgramValidator(CncManufacturer.Siemens);
|
||||||
|
}
|
||||||
|
|
||||||
|
public string Name { get { return _config.Name; } }
|
||||||
|
public ConnectionState State { get { return _state; } }
|
||||||
|
public event EventHandler<ConnectionState> StateChanged;
|
||||||
|
|
||||||
|
public Task<CncResult<bool>> ConnectAsync(CancellationToken ct = default(CancellationToken))
|
||||||
|
{
|
||||||
|
return RunGuardedAsync(() =>
|
||||||
|
{
|
||||||
|
SetState(ConnectionState.Connecting);
|
||||||
|
if (!_ftp.Ping())
|
||||||
|
{
|
||||||
|
SetState(ConnectionState.Faulted);
|
||||||
|
_errors.Add(new CncError(0, "Connect",
|
||||||
|
string.Format("FTP server unreachable at {0}:{1}", _config.IpAddress, _config.Port)));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
SetState(ConnectionState.Connected);
|
||||||
|
return true;
|
||||||
|
}, ct);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task DisconnectAsync()
|
||||||
|
{
|
||||||
|
return RunGuardedAsync<bool>(() =>
|
||||||
|
{
|
||||||
|
SetState(ConnectionState.Disconnected);
|
||||||
|
return true;
|
||||||
|
}, CancellationToken.None);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<CncResult<string>> ReadProgramAsync(string path, CancellationToken ct = default(CancellationToken))
|
||||||
|
{
|
||||||
|
return RunGuardedAsync(() =>
|
||||||
|
{
|
||||||
|
EnsureConnected();
|
||||||
|
string ncPath = ResolvePath(path, null);
|
||||||
|
try { return _ftp.DownloadFile(ncPath); }
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_errors.Add(new CncError(0, "ReadProgram", ex.Message));
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}, ct);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<CncResult<bool>> WriteProgramAsync(string path, CncProgram program, CancellationToken ct = default(CancellationToken))
|
||||||
|
{
|
||||||
|
return RunGuardedAsync(() =>
|
||||||
|
{
|
||||||
|
EnsureConnected();
|
||||||
|
if (program == null || string.IsNullOrEmpty(program.Content))
|
||||||
|
{
|
||||||
|
_errors.Add(new CncError(0, "WriteProgram", "Content is empty"));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
var vr = _validator.Validate(program.Name ?? path, program.Content);
|
||||||
|
if (!vr.Success)
|
||||||
|
{
|
||||||
|
string msg = string.Join("; ", vr.Errors
|
||||||
|
.Where(e => e.Severity == ValidationSeverity.Error)
|
||||||
|
.Select(e => e.Message));
|
||||||
|
_errors.Add(new CncError(0, "Validator", msg));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
string ncPath = ResolvePath(path, program.Name);
|
||||||
|
try { _ftp.UploadFile(ncPath, program.Content); return true; }
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_errors.Add(new CncError(0, "WriteProgram", ex.Message));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}, ct);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<CncResult<bool>> DeleteProgramAsync(string path, CancellationToken ct = default(CancellationToken))
|
||||||
|
{
|
||||||
|
return RunGuardedAsync(() =>
|
||||||
|
{
|
||||||
|
EnsureConnected();
|
||||||
|
try { _ftp.DeleteFile(path); return true; }
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_errors.Add(new CncError(0, "DeleteProgram", ex.Message));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}, ct);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<CncResult<MachineSnapshot>> ReadAsync(CancellationToken ct = default(CancellationToken))
|
||||||
|
{
|
||||||
|
return RunGuardedAsync(() =>
|
||||||
|
{
|
||||||
|
EnsureConnected();
|
||||||
|
return new MachineSnapshot
|
||||||
|
{
|
||||||
|
CapturedAt = DateTime.Now,
|
||||||
|
Mode = MachineMode.Unknown,
|
||||||
|
Run = RunState.Unknown,
|
||||||
|
Motion = MotionState.Unknown,
|
||||||
|
Alarms = new List<string>(),
|
||||||
|
OperatorMessages = new List<string>()
|
||||||
|
};
|
||||||
|
}, ct);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<CncResult<string>> ReadActiveProgramAsync(CancellationToken ct = default(CancellationToken))
|
||||||
|
{
|
||||||
|
return Task.FromResult(CncResult<string>.Fail("ReadActiveProgram", "Not supported via FTP"));
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<CncResult<bool>> SelectMainProgramAsync(string path, CancellationToken ct = default(CancellationToken))
|
||||||
|
{
|
||||||
|
return Task.FromResult(CncResult<bool>.Fail("SelectMainProgram", "Not supported via FTP"));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
try { DisconnectAsync().GetAwaiter().GetResult(); } catch { }
|
||||||
|
try { _gate.Dispose(); } catch { }
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<CncResult<T>> RunGuardedAsync<T>(Func<T> body, CancellationToken ct)
|
||||||
|
{
|
||||||
|
await _gate.WaitAsync(ct).ConfigureAwait(false);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return await Task.Run(() =>
|
||||||
|
{
|
||||||
|
_errors.Clear();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
T value = body();
|
||||||
|
return _errors.Count > 0
|
||||||
|
? CncResult<T>.Fail(_errors.ToArray())
|
||||||
|
: CncResult<T>.Ok(value);
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException) { return CncResult<T>.Cancelled(); }
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_errors.Add(new CncError(0, "SiemensMachine", ex.Message));
|
||||||
|
return CncResult<T>.Fail(_errors.ToArray());
|
||||||
|
}
|
||||||
|
}, ct).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
finally { _gate.Release(); }
|
||||||
|
}
|
||||||
|
|
||||||
|
private void EnsureConnected()
|
||||||
|
{
|
||||||
|
if (_state != ConnectionState.Connected)
|
||||||
|
throw new InvalidOperationException("SiemensMachine is not connected.");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SetState(ConnectionState next)
|
||||||
|
{
|
||||||
|
if (_state == next) return;
|
||||||
|
_state = next;
|
||||||
|
var h = StateChanged;
|
||||||
|
if (h != null)
|
||||||
|
{
|
||||||
|
try { h(this, next); } catch { }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Build full FTP path. If name given, appends it with .MPF extension.</summary>
|
||||||
|
private string ResolvePath(string folder, string name)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(name))
|
||||||
|
return folder.Contains("/") ? folder : _config.ProgramDirectory + folder + ".MPF";
|
||||||
|
|
||||||
|
string dir = folder ?? _config.ProgramDirectory;
|
||||||
|
if (!dir.EndsWith("/")) dir += "/";
|
||||||
|
string ext = name.Contains(".") ? "" : ".MPF";
|
||||||
|
return dir + name + ext;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
37
Dockerfile.test
Normal file
37
Dockerfile.test
Normal file
|
|
@ -0,0 +1,37 @@
|
||||||
|
FROM mono:6.12
|
||||||
|
|
||||||
|
WORKDIR /build
|
||||||
|
|
||||||
|
# Sync Mozilla root certs into Mono trust store — required for NuGet HTTPS on Buster
|
||||||
|
RUN cert-sync /etc/ssl/certs/ca-certificates.crt
|
||||||
|
|
||||||
|
# Copy solution + projects first for layer caching
|
||||||
|
COPY FanucProgramManager.csproj ./
|
||||||
|
COPY packages.config ./
|
||||||
|
COPY FanucProgramManager.Tests/FanucProgramManager.Tests.csproj ./FanucProgramManager.Tests/
|
||||||
|
COPY FanucProgramManager.Tests/packages.config ./FanucProgramManager.Tests/
|
||||||
|
COPY FanucProgramManager.sln ./
|
||||||
|
|
||||||
|
# Restore all packages (sln now includes test project)
|
||||||
|
RUN nuget restore FanucProgramManager.sln -NonInteractive
|
||||||
|
# Explicit fallback: restore test packages.config directly to same packages dir
|
||||||
|
RUN nuget restore FanucProgramManager.Tests/packages.config \
|
||||||
|
-OutputDirectory packages -NonInteractive
|
||||||
|
# Grab NUnit console runner
|
||||||
|
RUN nuget install NUnit.ConsoleRunner -Version 3.16.3 -OutputDirectory packages -NonInteractive
|
||||||
|
|
||||||
|
# Copy source
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
# Make libs dir explicit (native Win32 DLLs — needed by csproj Content entries, not executed on Linux)
|
||||||
|
RUN ls Cnc/Fanuc/libs/ || true
|
||||||
|
|
||||||
|
# Build test project (Debug)
|
||||||
|
RUN msbuild FanucProgramManager.Tests/FanucProgramManager.Tests.csproj \
|
||||||
|
/p:Configuration=Debug /p:Platform=AnyCPU \
|
||||||
|
/v:minimal /nologo
|
||||||
|
|
||||||
|
# Run tests — skip Fanuc FOCAS tests (marked [Ignore] — need fwlib32.dll)
|
||||||
|
CMD mono packages/NUnit.ConsoleRunner.3.16.3/tools/nunit3-console.exe \
|
||||||
|
FanucProgramManager.Tests/bin/Debug/FanucProgramManager.Tests.dll \
|
||||||
|
--labels=All
|
||||||
1094
FANUCMachine.cs
Executable file
1094
FANUCMachine.cs
Executable file
File diff suppressed because it is too large
Load diff
192
FanucProgram.cs
Executable file
192
FanucProgram.cs
Executable file
|
|
@ -0,0 +1,192 @@
|
||||||
|
using System;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
|
||||||
|
namespace FanucProgramManager
|
||||||
|
{
|
||||||
|
internal class FanucProgram
|
||||||
|
{
|
||||||
|
|
||||||
|
private static Regex titleRegex = new Regex(@"(\b[O]+\d{4}\b)|(\b\d{4}\b)"); //cerca per O#### o #### dove # è un carattere numerico
|
||||||
|
private static Regex oldTitleRegex = new Regex(@"\<((\b[O]+\d{4}\b)|(\b\d{4}\b))\>");//cerca per <O####> o #### dove # è un carattere numerico
|
||||||
|
private static Regex titleAndCommentRegex = new Regex(@"((\b[O]+\d{4}\b)|(\b\d{4}\b))+(\n|(\s*\((.*?)\)))");
|
||||||
|
private static Regex commentRegex = new Regex(@"\((.*?)\)");
|
||||||
|
|
||||||
|
private const string programStringName = "program:";
|
||||||
|
private const string toolOffsetDataName = "tooloffsetdata:";
|
||||||
|
private const string workZeroOffsetDataName = "workzerooffset:";
|
||||||
|
|
||||||
|
private static Regex programRegex = new Regex(@"program:\{\n((?:.*?|\n)*?)\}");
|
||||||
|
private static Regex toolOffsetDataRegex = new Regex(@"tooloffsetdata:\{\n((?:.*?|\n)*?)\}");
|
||||||
|
private static Regex workZeroOffsetDataRegex = new Regex(@"workzerooffset:\{\n((?:.*?|\n)*?)\}");
|
||||||
|
|
||||||
|
|
||||||
|
public String Program;
|
||||||
|
public String ToolOffsetData;
|
||||||
|
public String WorkZeroOffsetData;
|
||||||
|
|
||||||
|
public void setValue(String value, FILE_TYPE type)
|
||||||
|
{
|
||||||
|
switch (type)
|
||||||
|
{
|
||||||
|
case FILE_TYPE.CNC_PROGRAM: Program = value; break;
|
||||||
|
case FILE_TYPE.TOOL_OFFEST_DATA: ToolOffsetData = value; break;
|
||||||
|
case FILE_TYPE.WORK_ZERO_OFFSET: WorkZeroOffsetData = value; break;
|
||||||
|
}
|
||||||
|
Standardize();
|
||||||
|
}
|
||||||
|
public enum FILE_TYPE : short
|
||||||
|
{
|
||||||
|
CNC_PROGRAM = 0,
|
||||||
|
TOOL_OFFEST_DATA = 1,
|
||||||
|
WORK_ZERO_OFFSET = 5
|
||||||
|
}
|
||||||
|
|
||||||
|
public FanucProgram(string _program)
|
||||||
|
{
|
||||||
|
var programMatch = programRegex.Match(_program);
|
||||||
|
if (programMatch.Success)
|
||||||
|
{
|
||||||
|
Program = programMatch.Value.Substring(9);
|
||||||
|
Program = Program.Trim('}');
|
||||||
|
}
|
||||||
|
else Program = null;
|
||||||
|
|
||||||
|
var toolOffsetDataMatch = toolOffsetDataRegex.Match(_program);
|
||||||
|
if (toolOffsetDataMatch.Success)
|
||||||
|
{
|
||||||
|
ToolOffsetData = toolOffsetDataMatch.Value.Substring(16);
|
||||||
|
ToolOffsetData = ToolOffsetData.Trim('}');
|
||||||
|
}
|
||||||
|
else ToolOffsetData = null;
|
||||||
|
|
||||||
|
var workZeroOffsetDataMatch = workZeroOffsetDataRegex.Match(_program);
|
||||||
|
if (workZeroOffsetDataMatch.Success)
|
||||||
|
{
|
||||||
|
WorkZeroOffsetData = workZeroOffsetDataMatch.Value.Substring(16);
|
||||||
|
WorkZeroOffsetData = WorkZeroOffsetData.Trim('}');
|
||||||
|
}
|
||||||
|
else WorkZeroOffsetData = null;
|
||||||
|
|
||||||
|
//Old program format with ONLY the program outside of brachets
|
||||||
|
if (!programMatch.Success && !toolOffsetDataMatch.Success && !workZeroOffsetDataMatch.Success)
|
||||||
|
{
|
||||||
|
Program = _program;
|
||||||
|
}
|
||||||
|
|
||||||
|
Standardize();
|
||||||
|
}
|
||||||
|
|
||||||
|
public string GetProgramTitle()
|
||||||
|
{
|
||||||
|
var match = titleRegex.Match(Program);
|
||||||
|
if (match.Success)
|
||||||
|
{
|
||||||
|
return match.Value;
|
||||||
|
}
|
||||||
|
throw new InvalidOperationException("Regex could not match any program title in the file, check program formatting for a O#### patern");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string GetProgramTitle(string program)
|
||||||
|
{
|
||||||
|
var match = titleRegex.Match(program);
|
||||||
|
if (match.Success)
|
||||||
|
{
|
||||||
|
return match.Value;
|
||||||
|
}
|
||||||
|
throw new InvalidOperationException("Regex could not match any program title in the file, check program formatting for a O#### patern");
|
||||||
|
}
|
||||||
|
|
||||||
|
public string GetProgramComment()
|
||||||
|
{
|
||||||
|
var groupMatch = titleAndCommentRegex.Match(Program);
|
||||||
|
if (groupMatch.Success)
|
||||||
|
{
|
||||||
|
Match match = commentRegex.Match(groupMatch.Value);
|
||||||
|
if (match.Success)
|
||||||
|
{
|
||||||
|
return match.Value.Trim(new Char[] { '(', ')' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Standardize()
|
||||||
|
{
|
||||||
|
//removes % from start of file
|
||||||
|
int percent_index = Program.IndexOf('%');
|
||||||
|
if (percent_index == 0)
|
||||||
|
{
|
||||||
|
Program = Program.Remove(percent_index, 1);
|
||||||
|
}
|
||||||
|
//removes % from start of file
|
||||||
|
if (ToolOffsetData != null)
|
||||||
|
{
|
||||||
|
percent_index = ToolOffsetData.IndexOf('%');
|
||||||
|
if (percent_index == 0)
|
||||||
|
{
|
||||||
|
ToolOffsetData = ToolOffsetData.Remove(percent_index, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//removes % from start of file
|
||||||
|
if (WorkZeroOffsetData != null)
|
||||||
|
{
|
||||||
|
percent_index = WorkZeroOffsetData.IndexOf('%');
|
||||||
|
if (percent_index == 0)
|
||||||
|
{
|
||||||
|
WorkZeroOffsetData = WorkZeroOffsetData.Remove(percent_index, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//replace <O####> with O####
|
||||||
|
var oldTitleMatch = oldTitleRegex.Match(Program);
|
||||||
|
if (oldTitleMatch.Success)
|
||||||
|
{
|
||||||
|
Program = Program.Replace(oldTitleMatch.Value, GetProgramTitle());
|
||||||
|
}
|
||||||
|
|
||||||
|
//adds \n to file beginning if missing
|
||||||
|
int new_line_index = Program.IndexOf('\n');
|
||||||
|
if (new_line_index != 0)
|
||||||
|
{
|
||||||
|
Program = Program.Substring(Program.IndexOf(GetProgramTitle()));
|
||||||
|
Program = Program.Insert(0, "\n");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//fornendo un programma CNC e un commento in INPUT restituisce un output nel formato corretto per il caricamento con la funzione download
|
||||||
|
public void ChangeComment(string comment)
|
||||||
|
{
|
||||||
|
string previousComment = GetProgramComment();
|
||||||
|
if (previousComment != "")
|
||||||
|
Program = Program.Replace(previousComment, "(" + comment + ")");
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Program = Program.Replace(GetProgramTitle(), GetProgramTitle() + "(" + comment + ")");
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ChangeProgName(string progName)
|
||||||
|
{
|
||||||
|
var match = titleRegex.Match(progName);
|
||||||
|
if (!match.Success)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("New Program Name does Not respect the proper formattings O#### patern");
|
||||||
|
}
|
||||||
|
Program.Replace(GetProgramTitle(), progName);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ChangeNameAndComment(string progName, string comment)
|
||||||
|
{
|
||||||
|
ChangeProgName(progName);
|
||||||
|
ChangeComment(comment);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override string ToString()
|
||||||
|
{
|
||||||
|
return programStringName + "{\n" + Program + "\n}\n" +
|
||||||
|
toolOffsetDataName + "{\n" + ToolOffsetData + "\n}\n" +
|
||||||
|
workZeroOffsetDataName + "{\n" + WorkZeroOffsetData + "\n}\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
70
FanucProgramManager.Tests/FanucProgramManager.Tests.csproj
Normal file
70
FanucProgramManager.Tests/FanucProgramManager.Tests.csproj
Normal file
|
|
@ -0,0 +1,70 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||||
|
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props"
|
||||||
|
Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
|
||||||
|
<PropertyGroup>
|
||||||
|
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
|
||||||
|
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
|
||||||
|
<ProjectGuid>{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}</ProjectGuid>
|
||||||
|
<OutputType>Library</OutputType>
|
||||||
|
<RootNamespace>FanucProgramManager.Tests</RootNamespace>
|
||||||
|
<AssemblyName>FanucProgramManager.Tests</AssemblyName>
|
||||||
|
<TargetFrameworkVersion>v4.7.2</TargetFrameworkVersion>
|
||||||
|
<FileAlignment>512</FileAlignment>
|
||||||
|
<Deterministic>true</Deterministic>
|
||||||
|
</PropertyGroup>
|
||||||
|
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
|
||||||
|
<PlatformTarget>AnyCPU</PlatformTarget>
|
||||||
|
<DebugSymbols>true</DebugSymbols>
|
||||||
|
<DebugType>full</DebugType>
|
||||||
|
<Optimize>false</Optimize>
|
||||||
|
<OutputPath>bin\Debug\</OutputPath>
|
||||||
|
<DefineConstants>DEBUG;TRACE</DefineConstants>
|
||||||
|
<ErrorReport>prompt</ErrorReport>
|
||||||
|
<WarningLevel>4</WarningLevel>
|
||||||
|
</PropertyGroup>
|
||||||
|
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
|
||||||
|
<PlatformTarget>AnyCPU</PlatformTarget>
|
||||||
|
<DebugType>pdbonly</DebugType>
|
||||||
|
<Optimize>true</Optimize>
|
||||||
|
<OutputPath>bin\Release\</OutputPath>
|
||||||
|
<DefineConstants>TRACE</DefineConstants>
|
||||||
|
<ErrorReport>prompt</ErrorReport>
|
||||||
|
<WarningLevel>4</WarningLevel>
|
||||||
|
</PropertyGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<Reference Include="System" />
|
||||||
|
<Reference Include="System.Net" />
|
||||||
|
<Reference Include="nunit.framework">
|
||||||
|
<HintPath>..\packages\NUnit.3.13.3\lib\net45\nunit.framework.dll</HintPath>
|
||||||
|
</Reference>
|
||||||
|
<Reference Include="Moq">
|
||||||
|
<HintPath>..\packages\Moq.4.18.4\lib\net462\Moq.dll</HintPath>
|
||||||
|
</Reference>
|
||||||
|
<Reference Include="Castle.Core">
|
||||||
|
<HintPath>..\packages\Castle.Core.5.1.1\lib\net462\Castle.Core.dll</HintPath>
|
||||||
|
</Reference>
|
||||||
|
</ItemGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\FanucProgramManager.csproj">
|
||||||
|
<Project>{1032188B-3321-42B2-87ED-343BF718CAF5}</Project>
|
||||||
|
<Name>FanucProgramManager</Name>
|
||||||
|
</ProjectReference>
|
||||||
|
</ItemGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<Compile Include="Unit\NcProgramValidatorTests.cs" />
|
||||||
|
<Compile Include="Unit\FanucProgramParsingTests.cs" />
|
||||||
|
<Compile Include="Unit\IsoProgramFormatterTests.cs" />
|
||||||
|
<Compile Include="Unit\CncMachineFactoryTests.cs" />
|
||||||
|
<Compile Include="Unit\FanucMachineTests.cs" />
|
||||||
|
<Compile Include="Unit\SiemensMachineTests.cs" />
|
||||||
|
<Compile Include="Unit\MitsubishiMachineTests.cs" />
|
||||||
|
<Compile Include="Unit\HeidenhainMachineTests.cs" />
|
||||||
|
<Compile Include="Integration\Stubs\FtpServerStub.cs" />
|
||||||
|
<Compile Include="Integration\Stubs\Lsv2ServerStub.cs" />
|
||||||
|
<Compile Include="Integration\SiemensMachineIntegrationTests.cs" />
|
||||||
|
<Compile Include="Integration\MitsubishiMachineIntegrationTests.cs" />
|
||||||
|
<Compile Include="Integration\HeidenhainMachineIntegrationTests.cs" />
|
||||||
|
</ItemGroup>
|
||||||
|
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
|
||||||
|
</Project>
|
||||||
|
|
@ -0,0 +1,80 @@
|
||||||
|
using System;
|
||||||
|
using NUnit.Framework;
|
||||||
|
using FanucProgramManager.Cnc.Models;
|
||||||
|
using FanucProgramManager.Cnc.Heidenhain;
|
||||||
|
using FanucProgramManager.Tests.Integration.Stubs;
|
||||||
|
|
||||||
|
namespace FanucProgramManager.Tests.Integration
|
||||||
|
{
|
||||||
|
[TestFixture]
|
||||||
|
[Category("Integration")]
|
||||||
|
public class HeidenhainMachineIntegrationTests
|
||||||
|
{
|
||||||
|
private Lsv2ServerStub _stub;
|
||||||
|
private HeidenhainMachine _machine;
|
||||||
|
|
||||||
|
[SetUp]
|
||||||
|
public void SetUp()
|
||||||
|
{
|
||||||
|
_stub = new Lsv2ServerStub();
|
||||||
|
var config = new HeidenhainConnectionConfig
|
||||||
|
{
|
||||||
|
Name = "HeidenhainIntegration",
|
||||||
|
IpAddress = "127.0.0.1",
|
||||||
|
Port = _stub.Port,
|
||||||
|
Username = "user",
|
||||||
|
Password = "pass",
|
||||||
|
ConnectTimeout = TimeSpan.FromSeconds(5)
|
||||||
|
};
|
||||||
|
_machine = new HeidenhainMachine(config);
|
||||||
|
}
|
||||||
|
|
||||||
|
[TearDown]
|
||||||
|
public void TearDown()
|
||||||
|
{
|
||||||
|
_machine?.Dispose();
|
||||||
|
_stub?.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void ConnectAsync_StubHandlesLoginHandshake_ResultOk()
|
||||||
|
{
|
||||||
|
var result = _machine.ConnectAsync().GetAwaiter().GetResult();
|
||||||
|
|
||||||
|
Assert.That(result.Success, Is.True);
|
||||||
|
Assert.That(_machine.State, Is.EqualTo(ConnectionState.Connected));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void WriteThenRead_ProgramRoundTrips()
|
||||||
|
{
|
||||||
|
_machine.ConnectAsync().GetAwaiter().GetResult();
|
||||||
|
// Heidenhain: name must end with .H; no % or ; forbidden chars; no lowercase; no O#### required
|
||||||
|
string content = "L X+0 Y+0 R0 FMAX\nM30";
|
||||||
|
var prog = new CncProgram { Name = "O1234.H", Content = content };
|
||||||
|
string folder = "TNC:/nc_prog/";
|
||||||
|
|
||||||
|
var writeResult = _machine.WriteProgramAsync(folder, prog).GetAwaiter().GetResult();
|
||||||
|
Assert.That(writeResult.Success, Is.True);
|
||||||
|
|
||||||
|
// HeidenhainMachine appends .H only if name has no extension — name already has .H
|
||||||
|
string storedPath = folder + "O1234.H";
|
||||||
|
Assert.That(_stub.Files.ContainsKey(storedPath), Is.True,
|
||||||
|
"File should be in stub at " + storedPath);
|
||||||
|
|
||||||
|
var readResult = _machine.ReadProgramAsync(storedPath).GetAwaiter().GetResult();
|
||||||
|
Assert.That(readResult.Success, Is.True);
|
||||||
|
Assert.That(readResult.Value, Is.EqualTo(content));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void ReadProgramAsync_UnknownPath_ResultIsFail()
|
||||||
|
{
|
||||||
|
_machine.ConnectAsync().GetAwaiter().GetResult();
|
||||||
|
|
||||||
|
var result = _machine.ReadProgramAsync("TNC:/nc_prog/DOESNOTEXIST.H").GetAwaiter().GetResult();
|
||||||
|
|
||||||
|
Assert.That(result.Success, Is.False);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,96 @@
|
||||||
|
using System;
|
||||||
|
using NUnit.Framework;
|
||||||
|
using FanucProgramManager.Cnc.Models;
|
||||||
|
using FanucProgramManager.Cnc.Mitsubishi;
|
||||||
|
using FanucProgramManager.Tests.Integration.Stubs;
|
||||||
|
|
||||||
|
namespace FanucProgramManager.Tests.Integration
|
||||||
|
{
|
||||||
|
[TestFixture]
|
||||||
|
[Category("Integration")]
|
||||||
|
public class MitsubishiMachineIntegrationTests
|
||||||
|
{
|
||||||
|
private FtpServerStub _stub;
|
||||||
|
private MitsubishiMachine _machine;
|
||||||
|
private const string ProgramDir = "/PRG/";
|
||||||
|
|
||||||
|
[SetUp]
|
||||||
|
public void SetUp()
|
||||||
|
{
|
||||||
|
_stub = new FtpServerStub();
|
||||||
|
var config = new MitsubishiConnectionConfig
|
||||||
|
{
|
||||||
|
Name = "MitsubishiIntegration",
|
||||||
|
IpAddress = "127.0.0.1",
|
||||||
|
Port = _stub.Port,
|
||||||
|
Username = "user",
|
||||||
|
Password = "pass",
|
||||||
|
ProgramDirectory = ProgramDir,
|
||||||
|
RequestTimeout = TimeSpan.FromSeconds(5)
|
||||||
|
};
|
||||||
|
_machine = new MitsubishiMachine(config);
|
||||||
|
}
|
||||||
|
|
||||||
|
[TearDown]
|
||||||
|
public void TearDown()
|
||||||
|
{
|
||||||
|
_machine?.Dispose();
|
||||||
|
_stub?.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void ConnectAsync_StubListens_ResultOk()
|
||||||
|
{
|
||||||
|
var result = _machine.ConnectAsync().GetAwaiter().GetResult();
|
||||||
|
|
||||||
|
Assert.That(result.Success, Is.True);
|
||||||
|
Assert.That(_machine.State, Is.EqualTo(ConnectionState.Connected));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void WriteThenRead_ProgramRoundTripsNoExtension()
|
||||||
|
{
|
||||||
|
_machine.ConnectAsync().GetAwaiter().GetResult();
|
||||||
|
// Mitsubishi: name O\d{4}; first line must be O####; no semicolons/brackets; no lowercase
|
||||||
|
string content = "O1234\nG0 X0 Y0\nM30";
|
||||||
|
var prog = new CncProgram { Name = "O1234", Content = content };
|
||||||
|
|
||||||
|
var writeResult = _machine.WriteProgramAsync(ProgramDir, prog).GetAwaiter().GetResult();
|
||||||
|
Assert.That(writeResult.Success, Is.True);
|
||||||
|
|
||||||
|
// Mitsubishi: no extension
|
||||||
|
string storedPath = ProgramDir + "O1234";
|
||||||
|
Assert.That(_stub.Files.ContainsKey(storedPath), Is.True,
|
||||||
|
"File should be stored without extension at " + storedPath);
|
||||||
|
Assert.That(_stub.Files.ContainsKey(storedPath + ".MPF"), Is.False,
|
||||||
|
"File must NOT be stored with .MPF extension");
|
||||||
|
|
||||||
|
var readResult = _machine.ReadProgramAsync(storedPath).GetAwaiter().GetResult();
|
||||||
|
Assert.That(readResult.Success, Is.True);
|
||||||
|
Assert.That(readResult.Value, Is.EqualTo(content));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void DeleteProgramAsync_FileExistsInStub_RemovedFromStub()
|
||||||
|
{
|
||||||
|
_machine.ConnectAsync().GetAwaiter().GetResult();
|
||||||
|
string path = ProgramDir + "O5678";
|
||||||
|
_stub.Files[path] = "SOME_CONTENT";
|
||||||
|
|
||||||
|
var result = _machine.DeleteProgramAsync(path).GetAwaiter().GetResult();
|
||||||
|
|
||||||
|
Assert.That(result.Success, Is.True);
|
||||||
|
Assert.That(_stub.Files.ContainsKey(path), Is.False);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void ReadProgramAsync_FileNotInStub_ResultFail()
|
||||||
|
{
|
||||||
|
_machine.ConnectAsync().GetAwaiter().GetResult();
|
||||||
|
|
||||||
|
var result = _machine.ReadProgramAsync(ProgramDir + "O9999").GetAwaiter().GetResult();
|
||||||
|
|
||||||
|
Assert.That(result.Success, Is.False);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,104 @@
|
||||||
|
using System;
|
||||||
|
using NUnit.Framework;
|
||||||
|
using FanucProgramManager.Cnc.Models;
|
||||||
|
using FanucProgramManager.Cnc.Siemens;
|
||||||
|
using FanucProgramManager.Tests.Integration.Stubs;
|
||||||
|
|
||||||
|
namespace FanucProgramManager.Tests.Integration
|
||||||
|
{
|
||||||
|
[TestFixture]
|
||||||
|
[Category("Integration")]
|
||||||
|
public class SiemensMachineIntegrationTests
|
||||||
|
{
|
||||||
|
private FtpServerStub _stub;
|
||||||
|
private SiemensMachine _machine;
|
||||||
|
private const string ProgramDir = "/_N_MPF_DIR/";
|
||||||
|
|
||||||
|
[SetUp]
|
||||||
|
public void SetUp()
|
||||||
|
{
|
||||||
|
_stub = new FtpServerStub();
|
||||||
|
var config = new SiemensConnectionConfig
|
||||||
|
{
|
||||||
|
Name = "SiemensIntegration",
|
||||||
|
IpAddress = "127.0.0.1",
|
||||||
|
Port = _stub.Port,
|
||||||
|
Username = "user",
|
||||||
|
Password = "pass",
|
||||||
|
ProgramDirectory = ProgramDir,
|
||||||
|
RequestTimeout = TimeSpan.FromSeconds(5)
|
||||||
|
};
|
||||||
|
_machine = new SiemensMachine(config);
|
||||||
|
}
|
||||||
|
|
||||||
|
[TearDown]
|
||||||
|
public void TearDown()
|
||||||
|
{
|
||||||
|
_machine?.Dispose();
|
||||||
|
_stub?.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void ConnectAsync_StubListens_ResultOk()
|
||||||
|
{
|
||||||
|
var result = _machine.ConnectAsync().GetAwaiter().GetResult();
|
||||||
|
|
||||||
|
Assert.That(result.Success, Is.True);
|
||||||
|
Assert.That(_machine.State, Is.EqualTo(ConnectionState.Connected));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void WriteThenRead_ProgramRoundTrips()
|
||||||
|
{
|
||||||
|
_machine.ConnectAsync().GetAwaiter().GetResult();
|
||||||
|
// Siemens: name must end with .MPF; no semicolons; no lowercase
|
||||||
|
string content = "G0 X0 Y0\nM30";
|
||||||
|
var prog = new CncProgram { Name = "O1234.MPF", Content = content };
|
||||||
|
|
||||||
|
var writeResult = _machine.WriteProgramAsync(ProgramDir, prog).GetAwaiter().GetResult();
|
||||||
|
Assert.That(writeResult.Success, Is.True);
|
||||||
|
|
||||||
|
// Determine where SiemensMachine stored the file
|
||||||
|
string storedPath = ProgramDir + "O1234.MPF";
|
||||||
|
var readResult = _machine.ReadProgramAsync(storedPath).GetAwaiter().GetResult();
|
||||||
|
|
||||||
|
Assert.That(readResult.Success, Is.True);
|
||||||
|
Assert.That(readResult.Value, Is.EqualTo(content));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void DeleteProgramAsync_FileExistsInStub_RemovedFromStub()
|
||||||
|
{
|
||||||
|
_machine.ConnectAsync().GetAwaiter().GetResult();
|
||||||
|
string path = ProgramDir + "O5678.MPF";
|
||||||
|
_stub.Files[path] = "SOME_CONTENT";
|
||||||
|
|
||||||
|
var result = _machine.DeleteProgramAsync(path).GetAwaiter().GetResult();
|
||||||
|
|
||||||
|
Assert.That(result.Success, Is.True);
|
||||||
|
Assert.That(_stub.Files.ContainsKey(path), Is.False);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void ReadProgramAsync_FileNotInStub_ResultFail()
|
||||||
|
{
|
||||||
|
_machine.ConnectAsync().GetAwaiter().GetResult();
|
||||||
|
|
||||||
|
var result = _machine.ReadProgramAsync(ProgramDir + "DOESNOTEXIST.MPF").GetAwaiter().GetResult();
|
||||||
|
|
||||||
|
Assert.That(result.Success, Is.False);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void WriteProgramAsync_InvalidName_ValidationFails()
|
||||||
|
{
|
||||||
|
_machine.ConnectAsync().GetAwaiter().GetResult();
|
||||||
|
// Siemens requires .MPF extension — validator rejects before any FTP call
|
||||||
|
var prog = new CncProgram { Name = "INVALID", Content = "G00 X10\nM30" };
|
||||||
|
|
||||||
|
var result = _machine.WriteProgramAsync(ProgramDir, prog).GetAwaiter().GetResult();
|
||||||
|
|
||||||
|
Assert.That(result.Success, Is.False);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
255
FanucProgramManager.Tests/Integration/Stubs/FtpServerStub.cs
Normal file
255
FanucProgramManager.Tests/Integration/Stubs/FtpServerStub.cs
Normal file
|
|
@ -0,0 +1,255 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Net;
|
||||||
|
using System.Net.Sockets;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading;
|
||||||
|
|
||||||
|
namespace FanucProgramManager.Tests.Integration.Stubs
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Minimal in-process FTP server using passive mode.
|
||||||
|
/// Supports USER, PASS, SYST, PWD, CWD, TYPE, PASV, LIST, NLST, RETR, STOR, DELE, QUIT.
|
||||||
|
/// Also accepts OPTS, FEAT, NOOP, CLNT, MODE, STRU as no-ops.
|
||||||
|
/// All replies use CRLF (per RFC 959), independent of host platform.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class FtpServerStub : IDisposable
|
||||||
|
{
|
||||||
|
private const string CRLF = "\r\n";
|
||||||
|
|
||||||
|
private readonly TcpListener _listener;
|
||||||
|
private readonly Thread _acceptThread;
|
||||||
|
private volatile bool _running;
|
||||||
|
|
||||||
|
public int Port { get; }
|
||||||
|
|
||||||
|
/// <summary>In-memory file store: full path → content.</summary>
|
||||||
|
public Dictionary<string, string> Files { get; } = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
public FtpServerStub()
|
||||||
|
{
|
||||||
|
_listener = new TcpListener(IPAddress.Loopback, 0);
|
||||||
|
_listener.Start();
|
||||||
|
Port = ((IPEndPoint)_listener.LocalEndpoint).Port;
|
||||||
|
_running = true;
|
||||||
|
_acceptThread = new Thread(AcceptLoop) { IsBackground = true, Name = "FtpStub-Accept" };
|
||||||
|
_acceptThread.Start();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
_running = false;
|
||||||
|
try { _listener.Stop(); } catch { }
|
||||||
|
}
|
||||||
|
|
||||||
|
private void AcceptLoop()
|
||||||
|
{
|
||||||
|
while (_running)
|
||||||
|
{
|
||||||
|
TcpClient client;
|
||||||
|
try { client = _listener.AcceptTcpClient(); }
|
||||||
|
catch { break; }
|
||||||
|
var t = new Thread(() => HandleClient(client)) { IsBackground = true, Name = "FtpStub-Client" };
|
||||||
|
t.Start();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void SendLine(Stream stream, string line)
|
||||||
|
{
|
||||||
|
byte[] bytes = Encoding.ASCII.GetBytes(line + CRLF);
|
||||||
|
stream.Write(bytes, 0, bytes.Length);
|
||||||
|
stream.Flush();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string ReadLine(Stream stream)
|
||||||
|
{
|
||||||
|
var sb = new StringBuilder();
|
||||||
|
int prev = -1;
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
int b = stream.ReadByte();
|
||||||
|
if (b < 0) return sb.Length == 0 ? null : sb.ToString();
|
||||||
|
if (b == '\n')
|
||||||
|
{
|
||||||
|
if (prev == '\r')
|
||||||
|
sb.Length = sb.Length - 1; // strip CR
|
||||||
|
return sb.ToString();
|
||||||
|
}
|
||||||
|
sb.Append((char)b);
|
||||||
|
prev = b;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void HandleClient(TcpClient client)
|
||||||
|
{
|
||||||
|
using (client)
|
||||||
|
using (var stream = client.GetStream())
|
||||||
|
{
|
||||||
|
string cwd = "/";
|
||||||
|
TcpListener dataListener = null;
|
||||||
|
|
||||||
|
SendLine(stream, "220 FtpServerStub ready");
|
||||||
|
|
||||||
|
string line;
|
||||||
|
while ((line = ReadLine(stream)) != null)
|
||||||
|
{
|
||||||
|
line = line.Trim();
|
||||||
|
if (string.IsNullOrEmpty(line)) continue;
|
||||||
|
|
||||||
|
string cmd, arg;
|
||||||
|
int sp = line.IndexOf(' ');
|
||||||
|
if (sp >= 0)
|
||||||
|
{
|
||||||
|
cmd = line.Substring(0, sp).ToUpperInvariant();
|
||||||
|
arg = line.Substring(sp + 1).Trim();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
cmd = line.ToUpperInvariant();
|
||||||
|
arg = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (cmd)
|
||||||
|
{
|
||||||
|
case "USER":
|
||||||
|
SendLine(stream, "331 Password required");
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "PASS":
|
||||||
|
SendLine(stream, "230 Logged in");
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "SYST":
|
||||||
|
SendLine(stream, "215 UNIX Type: L8");
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "FEAT":
|
||||||
|
SendLine(stream, "211-Features:");
|
||||||
|
SendLine(stream, " UTF8");
|
||||||
|
SendLine(stream, "211 End");
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "OPTS":
|
||||||
|
case "CLNT":
|
||||||
|
case "NOOP":
|
||||||
|
case "MODE":
|
||||||
|
case "STRU":
|
||||||
|
case "ALLO":
|
||||||
|
SendLine(stream, "200 OK");
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "PWD":
|
||||||
|
SendLine(stream, "257 \"" + cwd + "\"");
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "CWD":
|
||||||
|
cwd = arg.StartsWith("/") ? arg : cwd.TrimEnd('/') + "/" + arg;
|
||||||
|
if (!cwd.EndsWith("/")) cwd += "/";
|
||||||
|
SendLine(stream, "250 CWD successful");
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "TYPE":
|
||||||
|
SendLine(stream, "200 Type set");
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "PASV":
|
||||||
|
dataListener = new TcpListener(IPAddress.Loopback, 0);
|
||||||
|
dataListener.Start();
|
||||||
|
int dataPort = ((IPEndPoint)dataListener.LocalEndpoint).Port;
|
||||||
|
int p1 = dataPort >> 8;
|
||||||
|
int p2 = dataPort & 0xFF;
|
||||||
|
SendLine(stream, "227 Entering Passive Mode (127,0,0,1," + p1 + "," + p2 + ")");
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "LIST":
|
||||||
|
case "NLST":
|
||||||
|
if (dataListener == null) { SendLine(stream, "425 No data connection"); break; }
|
||||||
|
SendLine(stream, "150 Opening data connection");
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using (var dc = dataListener.AcceptTcpClient())
|
||||||
|
using (var ds = dc.GetStream())
|
||||||
|
{
|
||||||
|
// Empty listing — enough for Ping()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch { }
|
||||||
|
try { dataListener.Stop(); } catch { }
|
||||||
|
dataListener = null;
|
||||||
|
SendLine(stream, "226 Transfer complete");
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "RETR":
|
||||||
|
if (dataListener == null) { SendLine(stream, "425 No data connection"); break; }
|
||||||
|
string retrPath = NormalizePath(cwd, arg);
|
||||||
|
string retrContent;
|
||||||
|
if (!Files.TryGetValue(retrPath, out retrContent))
|
||||||
|
{
|
||||||
|
try { dataListener.Stop(); } catch { }
|
||||||
|
dataListener = null;
|
||||||
|
SendLine(stream, "550 File not found");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
SendLine(stream, "150 Opening data connection");
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using (var dc = dataListener.AcceptTcpClient())
|
||||||
|
using (var ds = dc.GetStream())
|
||||||
|
{
|
||||||
|
byte[] bytes = Encoding.ASCII.GetBytes(retrContent);
|
||||||
|
ds.Write(bytes, 0, bytes.Length);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch { }
|
||||||
|
try { dataListener.Stop(); } catch { }
|
||||||
|
dataListener = null;
|
||||||
|
SendLine(stream, "226 Transfer complete");
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "STOR":
|
||||||
|
if (dataListener == null) { SendLine(stream, "425 No data connection"); break; }
|
||||||
|
string storPath = NormalizePath(cwd, arg);
|
||||||
|
SendLine(stream, "150 Opening data connection");
|
||||||
|
var sb = new StringBuilder();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using (var dc = dataListener.AcceptTcpClient())
|
||||||
|
using (var ds = dc.GetStream())
|
||||||
|
using (var dr = new StreamReader(ds, Encoding.ASCII))
|
||||||
|
{
|
||||||
|
sb.Append(dr.ReadToEnd());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch { }
|
||||||
|
Files[storPath] = sb.ToString();
|
||||||
|
try { dataListener.Stop(); } catch { }
|
||||||
|
dataListener = null;
|
||||||
|
SendLine(stream, "226 Transfer complete");
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "DELE":
|
||||||
|
string delePath = NormalizePath(cwd, arg);
|
||||||
|
Files.Remove(delePath);
|
||||||
|
SendLine(stream, "250 DELE successful");
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "QUIT":
|
||||||
|
SendLine(stream, "221 Goodbye");
|
||||||
|
return;
|
||||||
|
|
||||||
|
default:
|
||||||
|
SendLine(stream, "500 Unknown command");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string NormalizePath(string cwd, string arg)
|
||||||
|
{
|
||||||
|
if (arg.StartsWith("/")) return arg;
|
||||||
|
return cwd.TrimEnd('/') + "/" + arg;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
212
FanucProgramManager.Tests/Integration/Stubs/Lsv2ServerStub.cs
Normal file
212
FanucProgramManager.Tests/Integration/Stubs/Lsv2ServerStub.cs
Normal file
|
|
@ -0,0 +1,212 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Net;
|
||||||
|
using System.Net.Sockets;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading;
|
||||||
|
|
||||||
|
namespace FanucProgramManager.Tests.Integration.Stubs
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Minimal in-process LSV2 server.
|
||||||
|
/// Frame format: [0x02][SIZE_HI][SIZE_LO][CMD_HI][CMD_LO][data...][CRC_HI][CRC_LO]
|
||||||
|
/// SIZE = len(CMD) + len(data) = 2 + dataLen
|
||||||
|
/// </summary>
|
||||||
|
public sealed class Lsv2ServerStub : IDisposable
|
||||||
|
{
|
||||||
|
private const ushort T_OK = 0x0000;
|
||||||
|
private const ushort T_SE = 0x0001;
|
||||||
|
private const ushort T_LS_SELECT = 0x00A0;
|
||||||
|
private const ushort T_LS_LOG_IN = 0x00A1;
|
||||||
|
private const ushort T_LS_LOG_OUT = 0x00A2;
|
||||||
|
private const ushort T_R_FL = 0x0023;
|
||||||
|
private const ushort T_S_FL = 0x0024;
|
||||||
|
private const ushort T_FD = 0x0025;
|
||||||
|
|
||||||
|
private readonly TcpListener _listener;
|
||||||
|
private readonly Thread _acceptThread;
|
||||||
|
private volatile bool _running;
|
||||||
|
|
||||||
|
public int Port { get; }
|
||||||
|
|
||||||
|
/// <summary>In-memory file store: full path → content.</summary>
|
||||||
|
public Dictionary<string, string> Files { get; } = new Dictionary<string, string>(StringComparer.Ordinal);
|
||||||
|
|
||||||
|
public Lsv2ServerStub()
|
||||||
|
{
|
||||||
|
_listener = new TcpListener(IPAddress.Loopback, 0);
|
||||||
|
_listener.Start();
|
||||||
|
Port = ((IPEndPoint)_listener.LocalEndpoint).Port;
|
||||||
|
_running = true;
|
||||||
|
_acceptThread = new Thread(AcceptLoop) { IsBackground = true, Name = "Lsv2Stub-Accept" };
|
||||||
|
_acceptThread.Start();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
_running = false;
|
||||||
|
try { _listener.Stop(); } catch { }
|
||||||
|
}
|
||||||
|
|
||||||
|
private void AcceptLoop()
|
||||||
|
{
|
||||||
|
while (_running)
|
||||||
|
{
|
||||||
|
TcpClient client;
|
||||||
|
try { client = _listener.AcceptTcpClient(); }
|
||||||
|
catch { break; }
|
||||||
|
var t = new Thread(() => HandleClient(client)) { IsBackground = true, Name = "Lsv2Stub-Client" };
|
||||||
|
t.Start();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void HandleClient(TcpClient client)
|
||||||
|
{
|
||||||
|
using (client)
|
||||||
|
using (var stream = client.GetStream())
|
||||||
|
{
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
ushort cmd;
|
||||||
|
byte[] data = ReadFrame(stream, out cmd);
|
||||||
|
if (data == null) break;
|
||||||
|
|
||||||
|
switch (cmd)
|
||||||
|
{
|
||||||
|
case T_LS_SELECT:
|
||||||
|
case T_LS_LOG_IN:
|
||||||
|
SendFrame(stream, T_OK, new byte[0]);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case T_LS_LOG_OUT:
|
||||||
|
SendFrame(stream, T_OK, new byte[0]);
|
||||||
|
return;
|
||||||
|
|
||||||
|
case T_R_FL:
|
||||||
|
{
|
||||||
|
// data = path\0
|
||||||
|
string path = ExtractString(data, 0);
|
||||||
|
string content;
|
||||||
|
if (!Files.TryGetValue(path, out content))
|
||||||
|
{
|
||||||
|
SendFrame(stream, T_SE, new byte[0]);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
byte[] contentBytes = Encoding.ASCII.GetBytes(content);
|
||||||
|
SendFrame(stream, T_FD, contentBytes);
|
||||||
|
SendFrame(stream, T_OK, new byte[0]);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case T_S_FL:
|
||||||
|
{
|
||||||
|
// data = path\0 + content
|
||||||
|
string path = ExtractString(data, 0);
|
||||||
|
int pathLen = Encoding.ASCII.GetByteCount(path) + 1; // +1 for \0
|
||||||
|
int contentLen = data.Length - pathLen;
|
||||||
|
string content = contentLen > 0
|
||||||
|
? Encoding.ASCII.GetString(data, pathLen, contentLen)
|
||||||
|
: "";
|
||||||
|
Files[path] = content;
|
||||||
|
SendFrame(stream, T_OK, new byte[0]);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
SendFrame(stream, T_SE, new byte[0]);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static byte[] ReadFrame(Stream stream, out ushort cmd)
|
||||||
|
{
|
||||||
|
cmd = 0xFFFF;
|
||||||
|
int stx = stream.ReadByte();
|
||||||
|
if (stx != 0x02) return null;
|
||||||
|
|
||||||
|
int sizeHi = stream.ReadByte();
|
||||||
|
int sizeLo = stream.ReadByte();
|
||||||
|
if (sizeHi < 0 || sizeLo < 0) return null;
|
||||||
|
int size = (sizeHi << 8) | sizeLo;
|
||||||
|
if (size < 2) return null;
|
||||||
|
|
||||||
|
int cmdHi = stream.ReadByte();
|
||||||
|
int cmdLo = stream.ReadByte();
|
||||||
|
if (cmdHi < 0 || cmdLo < 0) return null;
|
||||||
|
cmd = (ushort)((cmdHi << 8) | cmdLo);
|
||||||
|
|
||||||
|
int dataLen = size - 2;
|
||||||
|
byte[] data = new byte[dataLen];
|
||||||
|
int read = 0;
|
||||||
|
while (read < dataLen)
|
||||||
|
{
|
||||||
|
int n = stream.Read(data, read, dataLen - read);
|
||||||
|
if (n == 0) return null;
|
||||||
|
read += n;
|
||||||
|
}
|
||||||
|
|
||||||
|
// consume CRC
|
||||||
|
stream.ReadByte();
|
||||||
|
stream.ReadByte();
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void SendFrame(Stream stream, ushort cmdCode, byte[] data)
|
||||||
|
{
|
||||||
|
int dataLen = data.Length;
|
||||||
|
ushort size = (ushort)(2 + dataLen);
|
||||||
|
|
||||||
|
byte sizeHi = (byte)(size >> 8);
|
||||||
|
byte sizeLo = (byte)(size & 0xFF);
|
||||||
|
byte cmdHi = (byte)(cmdCode >> 8);
|
||||||
|
byte cmdLo = (byte)(cmdCode & 0xFF);
|
||||||
|
|
||||||
|
byte[] crcInput = new byte[4 + dataLen];
|
||||||
|
crcInput[0] = sizeHi; crcInput[1] = sizeLo;
|
||||||
|
crcInput[2] = cmdHi; crcInput[3] = cmdLo;
|
||||||
|
Buffer.BlockCopy(data, 0, crcInput, 4, dataLen);
|
||||||
|
ushort crc = Crc16(crcInput);
|
||||||
|
|
||||||
|
byte[] frame = new byte[1 + 4 + dataLen + 2];
|
||||||
|
int i = 0;
|
||||||
|
frame[i++] = 0x02;
|
||||||
|
frame[i++] = sizeHi;
|
||||||
|
frame[i++] = sizeLo;
|
||||||
|
frame[i++] = cmdHi;
|
||||||
|
frame[i++] = cmdLo;
|
||||||
|
Buffer.BlockCopy(data, 0, frame, i, dataLen);
|
||||||
|
i += dataLen;
|
||||||
|
frame[i++] = (byte)(crc >> 8);
|
||||||
|
frame[i] = (byte)(crc & 0xFF);
|
||||||
|
|
||||||
|
stream.Write(frame, 0, frame.Length);
|
||||||
|
stream.Flush();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string ExtractString(byte[] data, int offset)
|
||||||
|
{
|
||||||
|
int end = offset;
|
||||||
|
while (end < data.Length && data[end] != 0) end++;
|
||||||
|
return Encoding.ASCII.GetString(data, offset, end - offset);
|
||||||
|
}
|
||||||
|
|
||||||
|
// CRC16 CCITT — same poly/init as Lsv2Client
|
||||||
|
private static ushort Crc16(byte[] data)
|
||||||
|
{
|
||||||
|
ushort crc = 0xFFFF;
|
||||||
|
foreach (byte b in data)
|
||||||
|
{
|
||||||
|
crc ^= (ushort)(b << 8);
|
||||||
|
for (int i = 0; i < 8; i++)
|
||||||
|
crc = (crc & 0x8000) != 0
|
||||||
|
? (ushort)((crc << 1) ^ 0x1021)
|
||||||
|
: (ushort)(crc << 1);
|
||||||
|
}
|
||||||
|
return crc;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
113
FanucProgramManager.Tests/Unit/CncMachineFactoryTests.cs
Normal file
113
FanucProgramManager.Tests/Unit/CncMachineFactoryTests.cs
Normal file
|
|
@ -0,0 +1,113 @@
|
||||||
|
using NUnit.Framework;
|
||||||
|
using FanucProgramManager.Cnc;
|
||||||
|
using FanucProgramManager.Cnc.Fanuc;
|
||||||
|
using FanucProgramManager.Cnc.Heidenhain;
|
||||||
|
using FanucProgramManager.Cnc.Mitsubishi;
|
||||||
|
using FanucProgramManager.Cnc.Models;
|
||||||
|
using FanucProgramManager.Cnc.Siemens;
|
||||||
|
|
||||||
|
namespace FanucProgramManager.Tests.Unit
|
||||||
|
{
|
||||||
|
[TestFixture]
|
||||||
|
public class CncMachineFactoryTests
|
||||||
|
{
|
||||||
|
private static T GetRequiredField<T>(object target, string name)
|
||||||
|
{
|
||||||
|
var f = target.GetType().GetField(name,
|
||||||
|
System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance)
|
||||||
|
?? throw new InvalidOperationException(
|
||||||
|
string.Format("Field '{0}' not found on {1}. Field may have been renamed.", name, target.GetType().Name));
|
||||||
|
return (T)f.GetValue(target);
|
||||||
|
}
|
||||||
|
|
||||||
|
private InputArgs BaseArgs()
|
||||||
|
{
|
||||||
|
return new InputArgs
|
||||||
|
{
|
||||||
|
ip = "192.168.1.1",
|
||||||
|
porta = "",
|
||||||
|
username = "user",
|
||||||
|
password = "pass",
|
||||||
|
nodoHssb = -1
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void Create_Fanuc_ReturnsFanucMachine()
|
||||||
|
{
|
||||||
|
var args = BaseArgs();
|
||||||
|
args.manufacturer = CncManufacturer.Fanuc;
|
||||||
|
|
||||||
|
using (var m = CncMachineFactory.Create(args))
|
||||||
|
{
|
||||||
|
Assert.That(m, Is.InstanceOf<FanucMachine>());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void Create_Heidenhain_ReturnsHeidenhainMachine()
|
||||||
|
{
|
||||||
|
var args = BaseArgs();
|
||||||
|
args.manufacturer = CncManufacturer.Heidenhain;
|
||||||
|
|
||||||
|
using (var m = CncMachineFactory.Create(args))
|
||||||
|
{
|
||||||
|
Assert.That(m, Is.InstanceOf<HeidenhainMachine>());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void Create_Siemens_ReturnsSiemensMachine()
|
||||||
|
{
|
||||||
|
var args = BaseArgs();
|
||||||
|
args.manufacturer = CncManufacturer.Siemens;
|
||||||
|
|
||||||
|
using (var m = CncMachineFactory.Create(args))
|
||||||
|
{
|
||||||
|
Assert.That(m, Is.InstanceOf<SiemensMachine>());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void Create_Mitsubishi_ReturnsMitsubishiMachine()
|
||||||
|
{
|
||||||
|
var args = BaseArgs();
|
||||||
|
args.manufacturer = CncManufacturer.Mitsubishi;
|
||||||
|
|
||||||
|
using (var m = CncMachineFactory.Create(args))
|
||||||
|
{
|
||||||
|
Assert.That(m, Is.InstanceOf<MitsubishiMachine>());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void Create_FanucHssbNode_TransportIsHssb()
|
||||||
|
{
|
||||||
|
var args = BaseArgs();
|
||||||
|
args.manufacturer = CncManufacturer.Fanuc;
|
||||||
|
args.nodoHssb = 0;
|
||||||
|
|
||||||
|
using (var m = CncMachineFactory.Create(args) as FanucMachine)
|
||||||
|
{
|
||||||
|
var config = GetRequiredField<FanucConnectionConfig>(m, "_config");
|
||||||
|
|
||||||
|
Assert.That(config.Transport, Is.EqualTo(FanucTransport.Hssb));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void Create_FanucNoHssb_TransportIsEthernet()
|
||||||
|
{
|
||||||
|
var args = BaseArgs();
|
||||||
|
args.manufacturer = CncManufacturer.Fanuc;
|
||||||
|
args.nodoHssb = -1;
|
||||||
|
|
||||||
|
using (var m = CncMachineFactory.Create(args) as FanucMachine)
|
||||||
|
{
|
||||||
|
var config = GetRequiredField<FanucConnectionConfig>(m, "_config");
|
||||||
|
|
||||||
|
Assert.That(config.Transport, Is.EqualTo(FanucTransport.Ethernet));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
221
FanucProgramManager.Tests/Unit/FanucMachineTests.cs
Normal file
221
FanucProgramManager.Tests/Unit/FanucMachineTests.cs
Normal file
|
|
@ -0,0 +1,221 @@
|
||||||
|
using System;
|
||||||
|
using NUnit.Framework;
|
||||||
|
using Moq;
|
||||||
|
using FanucProgramManager.Cnc.Fanuc;
|
||||||
|
using FanucProgramManager.Cnc.Models;
|
||||||
|
|
||||||
|
namespace FanucProgramManager.Tests.Unit
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// FanucMachine calls real FOCAS DLL in ConnectCore and most methods guard
|
||||||
|
/// via EnsureConnected. Tests that require an active FOCAS handle are marked Ignore.
|
||||||
|
/// The tests here verify dialect call patterns by setting machine state via
|
||||||
|
/// reflection (bypass EnsureConnected) after injecting a mock dialect.
|
||||||
|
/// </summary>
|
||||||
|
[TestFixture]
|
||||||
|
public class FanucMachineTests
|
||||||
|
{
|
||||||
|
// Used in Returns() to modify the ref int len parameter — lambdas cannot have ref params.
|
||||||
|
private delegate short UploadDataDelegate(ushort hndl, ref int len, char[] data);
|
||||||
|
|
||||||
|
private FanucConnectionConfig EthernetConfig()
|
||||||
|
{
|
||||||
|
return new FanucConnectionConfig
|
||||||
|
{
|
||||||
|
Name = "TestFanuc",
|
||||||
|
Transport = FanucTransport.Ethernet,
|
||||||
|
IpAddress = "127.0.0.1",
|
||||||
|
Port = 8193,
|
||||||
|
ConnectTimeout = TimeSpan.FromSeconds(1)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static T GetRequiredField<T>(object target, string name)
|
||||||
|
{
|
||||||
|
var f = target.GetType().GetField(name,
|
||||||
|
System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance)
|
||||||
|
?? throw new InvalidOperationException(
|
||||||
|
string.Format("Field '{0}' not found on {1}. Field may have been renamed.", name, target.GetType().Name));
|
||||||
|
return (T)f.GetValue(target);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void SetRequiredField<T>(object target, string name, T value)
|
||||||
|
{
|
||||||
|
var f = target.GetType().GetField(name,
|
||||||
|
System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance)
|
||||||
|
?? throw new InvalidOperationException(
|
||||||
|
string.Format("Field '{0}' not found on {1}. Field may have been renamed.", name, target.GetType().Name));
|
||||||
|
f.SetValue(target, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
private FanucMachine CreateConnectedWithMockDialect(Mock<IFocasDialect> dialectMock)
|
||||||
|
{
|
||||||
|
var machine = new FanucMachine(EthernetConfig());
|
||||||
|
SetRequiredField(machine, "_state", ConnectionState.Connected);
|
||||||
|
SetRequiredField(machine, "_dialect", dialectMock.Object);
|
||||||
|
return machine;
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
[Ignore("Requires Fanuc FOCAS DLL — no mock injection path for ConnectCore")]
|
||||||
|
public void ConnectAsync_FocasReturnsOk_ResultSuccess()
|
||||||
|
{
|
||||||
|
// Would need real FOCAS DLL or a way to intercept Focas1.cnc_allclibhndl3
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
[Ignore("Requires Fanuc FOCAS DLL")]
|
||||||
|
public void ConnectAsync_FocasReturnsError_ResultFail()
|
||||||
|
{
|
||||||
|
// Same — Focas1 static calls cannot be intercepted without DLL
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
// Moq default: ref int len stays 0 after UploadData call, loop exits via else-branch (len == 0).
|
||||||
|
// This test verifies call sequence only, not data content.
|
||||||
|
// See ReadProgramAsync_UploadDataReturnsContent_ContentInResult for data-reading coverage.
|
||||||
|
public void ReadProgramAsync_CallsUploadStartThenDataThenEnd()
|
||||||
|
{
|
||||||
|
var dialect = new Mock<IFocasDialect>();
|
||||||
|
dialect.Setup(d => d.UploadStart(It.IsAny<ushort>(), It.IsAny<string>(), It.IsAny<long>(), It.IsAny<short>()))
|
||||||
|
.Returns(Focas1.EW_OK);
|
||||||
|
dialect.Setup(d => d.UploadData(It.IsAny<ushort>(), ref It.Ref<int>.IsAny, It.IsAny<char[]>()))
|
||||||
|
.Returns(Focas1.EW_OK);
|
||||||
|
dialect.Setup(d => d.UploadEnd(It.IsAny<ushort>())).Returns(Focas1.EW_OK);
|
||||||
|
|
||||||
|
using (var machine = CreateConnectedWithMockDialect(dialect))
|
||||||
|
{
|
||||||
|
machine.ReadProgramAsync("O1234").GetAwaiter().GetResult();
|
||||||
|
}
|
||||||
|
|
||||||
|
dialect.Verify(d => d.UploadStart(It.IsAny<ushort>(), "O1234", It.IsAny<long>(), 0), Times.Once);
|
||||||
|
dialect.Verify(d => d.UploadEnd(It.IsAny<ushort>()), Times.Once);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void ReadProgramAsync_UploadDataReturnsContent_ContentInResult()
|
||||||
|
{
|
||||||
|
var programContent = "O1234\nG0 X0\nM30\n%";
|
||||||
|
var chars = programContent.ToCharArray();
|
||||||
|
|
||||||
|
var dialect = new Mock<IFocasDialect>();
|
||||||
|
dialect.Setup(d => d.UploadStart(It.IsAny<ushort>(), It.IsAny<string>(), It.IsAny<long>(), It.IsAny<short>()))
|
||||||
|
.Returns(Focas1.EW_OK);
|
||||||
|
dialect.Setup(d => d.UploadData(It.IsAny<ushort>(), ref It.Ref<int>.IsAny, It.IsAny<char[]>()))
|
||||||
|
.Returns(new UploadDataDelegate(delegate(ushort h, ref int len, char[] buf)
|
||||||
|
{
|
||||||
|
Array.Copy(chars, buf, chars.Length);
|
||||||
|
len = chars.Length;
|
||||||
|
return Focas1.EW_OK;
|
||||||
|
}));
|
||||||
|
dialect.Setup(d => d.UploadEnd(It.IsAny<ushort>())).Returns(Focas1.EW_OK);
|
||||||
|
|
||||||
|
using (var machine = CreateConnectedWithMockDialect(dialect))
|
||||||
|
{
|
||||||
|
var result = machine.ReadProgramAsync("O1234").GetAwaiter().GetResult();
|
||||||
|
|
||||||
|
Assert.That(result.Success, Is.True);
|
||||||
|
Assert.That(result.Value, Is.EqualTo(programContent));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void ReadToolDataAsync_CallsUploadStartWithFileType1()
|
||||||
|
{
|
||||||
|
var dialect = new Mock<IFocasDialect>();
|
||||||
|
dialect.Setup(d => d.UploadStart(It.IsAny<ushort>(), It.IsAny<string>(), It.IsAny<long>(), It.IsAny<short>()))
|
||||||
|
.Returns(Focas1.EW_OK);
|
||||||
|
dialect.Setup(d => d.UploadData(It.IsAny<ushort>(), ref It.Ref<int>.IsAny, It.IsAny<char[]>()))
|
||||||
|
.Returns(Focas1.EW_OK);
|
||||||
|
dialect.Setup(d => d.UploadEnd(It.IsAny<ushort>())).Returns(Focas1.EW_OK);
|
||||||
|
|
||||||
|
using (var machine = CreateConnectedWithMockDialect(dialect))
|
||||||
|
{
|
||||||
|
machine.ReadToolDataAsync("O1234").GetAwaiter().GetResult();
|
||||||
|
}
|
||||||
|
|
||||||
|
dialect.Verify(d => d.UploadStart(It.IsAny<ushort>(), "O1234", It.IsAny<long>(), (short)1), Times.Once);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void ReadWorkZeroDataAsync_CallsUploadStartWithFileType5()
|
||||||
|
{
|
||||||
|
var dialect = new Mock<IFocasDialect>();
|
||||||
|
dialect.Setup(d => d.UploadStart(It.IsAny<ushort>(), It.IsAny<string>(), It.IsAny<long>(), It.IsAny<short>()))
|
||||||
|
.Returns(Focas1.EW_OK);
|
||||||
|
dialect.Setup(d => d.UploadData(It.IsAny<ushort>(), ref It.Ref<int>.IsAny, It.IsAny<char[]>()))
|
||||||
|
.Returns(Focas1.EW_OK);
|
||||||
|
dialect.Setup(d => d.UploadEnd(It.IsAny<ushort>())).Returns(Focas1.EW_OK);
|
||||||
|
|
||||||
|
using (var machine = CreateConnectedWithMockDialect(dialect))
|
||||||
|
{
|
||||||
|
machine.ReadWorkZeroDataAsync("O1234").GetAwaiter().GetResult();
|
||||||
|
}
|
||||||
|
|
||||||
|
dialect.Verify(d => d.UploadStart(It.IsAny<ushort>(), "O1234", It.IsAny<long>(), (short)5), Times.Once);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void ReadProgramAsync_UploadStartFails_ResultIsFail()
|
||||||
|
{
|
||||||
|
var dialect = new Mock<IFocasDialect>();
|
||||||
|
dialect.Setup(d => d.UploadStart(It.IsAny<ushort>(), It.IsAny<string>(), It.IsAny<long>(), It.IsAny<short>()))
|
||||||
|
.Returns(-3); // EW_SOCKET or any error
|
||||||
|
dialect.Setup(d => d.UploadEnd(It.IsAny<ushort>())).Returns(Focas1.EW_OK);
|
||||||
|
|
||||||
|
using (var machine = CreateConnectedWithMockDialect(dialect))
|
||||||
|
{
|
||||||
|
var result = machine.ReadProgramAsync("O1234").GetAwaiter().GetResult();
|
||||||
|
|
||||||
|
Assert.That(result.Success, Is.False);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void WriteProgramAsync_NullContent_ResultFail()
|
||||||
|
{
|
||||||
|
var dialect = new Mock<IFocasDialect>();
|
||||||
|
|
||||||
|
using (var machine = CreateConnectedWithMockDialect(dialect))
|
||||||
|
{
|
||||||
|
var prog = new CncProgram { Name = "O1234", Content = null };
|
||||||
|
var result = machine.WriteProgramAsync("//CNC_MEM/USER/PATH1/", prog).GetAwaiter().GetResult();
|
||||||
|
|
||||||
|
Assert.That(result.Success, Is.False);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void WriteProgramAsync_InvalidProgramName_ValidationFails()
|
||||||
|
{
|
||||||
|
var dialect = new Mock<IFocasDialect>();
|
||||||
|
|
||||||
|
using (var machine = CreateConnectedWithMockDialect(dialect))
|
||||||
|
{
|
||||||
|
// "PROG" does not match Fanuc O#### name rule — validator rejects before any FOCAS call
|
||||||
|
var prog = new CncProgram { Name = "PROG", Content = "O1234\nG00 X0\nM30" };
|
||||||
|
var result = machine.WriteProgramAsync("//CNC_MEM/USER/PATH1/", prog).GetAwaiter().GetResult();
|
||||||
|
|
||||||
|
Assert.That(result.Success, Is.False);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void State_InitialState_IsDisconnected()
|
||||||
|
{
|
||||||
|
using (var machine = new FanucMachine(EthernetConfig()))
|
||||||
|
{
|
||||||
|
Assert.That(machine.State, Is.EqualTo(ConnectionState.Disconnected));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void Name_ReturnsConfigName()
|
||||||
|
{
|
||||||
|
using (var machine = new FanucMachine(EthernetConfig()))
|
||||||
|
{
|
||||||
|
Assert.That(machine.Name, Is.EqualTo("TestFanuc"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
185
FanucProgramManager.Tests/Unit/FanucProgramParsingTests.cs
Normal file
185
FanucProgramManager.Tests/Unit/FanucProgramParsingTests.cs
Normal file
|
|
@ -0,0 +1,185 @@
|
||||||
|
using NUnit.Framework;
|
||||||
|
using FanucProgramManager;
|
||||||
|
|
||||||
|
namespace FanucProgramManager.Tests.Unit
|
||||||
|
{
|
||||||
|
[TestFixture]
|
||||||
|
public class FanucProgramParsingTests
|
||||||
|
{
|
||||||
|
private const string SimpleProgram =
|
||||||
|
"\nO1234(COMMENT)\n" +
|
||||||
|
"G0 X0 Y0\n" +
|
||||||
|
"M30\n" +
|
||||||
|
"%";
|
||||||
|
|
||||||
|
private const string ThreeFileInput =
|
||||||
|
"program:{\n" +
|
||||||
|
"\nO1234(COMMENT)\n" +
|
||||||
|
"G0 X0 Y0\n" +
|
||||||
|
"M30\n" +
|
||||||
|
"%\n" +
|
||||||
|
"}\n" +
|
||||||
|
"tooloffsetdata:{\n" +
|
||||||
|
"TOOL_DATA_LINE1\n" +
|
||||||
|
"}\n" +
|
||||||
|
"workzerooffset:{\n" +
|
||||||
|
"WORK_ZERO_LINE1\n" +
|
||||||
|
"}\n";
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void Constructor_OldFormat_ProgramFieldSet()
|
||||||
|
{
|
||||||
|
var p = new FanucProgram(SimpleProgram);
|
||||||
|
|
||||||
|
Assert.That(p.Program, Is.Not.Null);
|
||||||
|
Assert.That(p.ToolOffsetData, Is.Null);
|
||||||
|
Assert.That(p.WorkZeroOffsetData, Is.Null);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void Constructor_ThreeFileFormat_AllSectionsSet()
|
||||||
|
{
|
||||||
|
var p = new FanucProgram(ThreeFileInput);
|
||||||
|
|
||||||
|
Assert.That(p.Program, Is.Not.Null);
|
||||||
|
Assert.That(p.ToolOffsetData, Is.Not.Null);
|
||||||
|
Assert.That(p.WorkZeroOffsetData, Is.Not.Null);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void Constructor_ThreeFileFormat_ToolOffsetDataContent()
|
||||||
|
{
|
||||||
|
var p = new FanucProgram(ThreeFileInput);
|
||||||
|
|
||||||
|
Assert.That(p.ToolOffsetData, Does.Contain("TOOL_DATA_LINE1"));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void Constructor_ThreeFileFormat_WorkZeroOffsetDataContent()
|
||||||
|
{
|
||||||
|
var p = new FanucProgram(ThreeFileInput);
|
||||||
|
|
||||||
|
Assert.That(p.WorkZeroOffsetData, Does.Contain("WORK_ZERO_LINE1"));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void GetProgramTitle_StandardProgram_ReturnsO1234()
|
||||||
|
{
|
||||||
|
var p = new FanucProgram(SimpleProgram);
|
||||||
|
|
||||||
|
string title = p.GetProgramTitle();
|
||||||
|
|
||||||
|
Assert.That(title, Is.EqualTo("O1234"));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void GetProgramTitle_Static_ReturnsTitle()
|
||||||
|
{
|
||||||
|
string title = FanucProgram.GetProgramTitle("\nO5678\nG0\n%");
|
||||||
|
|
||||||
|
Assert.That(title, Is.EqualTo("O5678"));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void GetProgramComment_ProgramWithComment_ReturnsComment()
|
||||||
|
{
|
||||||
|
var p = new FanucProgram(SimpleProgram);
|
||||||
|
|
||||||
|
string comment = p.GetProgramComment();
|
||||||
|
|
||||||
|
Assert.That(comment, Is.EqualTo("COMMENT"));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void GetProgramComment_ProgramWithoutComment_ReturnsEmpty()
|
||||||
|
{
|
||||||
|
var p = new FanucProgram("\nO1234\nG0 X0\nM30\n%");
|
||||||
|
|
||||||
|
string comment = p.GetProgramComment();
|
||||||
|
|
||||||
|
Assert.That(comment, Is.EqualTo(""));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void ChangeComment_ReplaceExistingComment_CommentUpdated()
|
||||||
|
{
|
||||||
|
var p = new FanucProgram(SimpleProgram);
|
||||||
|
|
||||||
|
p.ChangeComment("NEW_COMMENT");
|
||||||
|
|
||||||
|
Assert.That(p.GetProgramComment(), Is.EqualTo("NEW_COMMENT"));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void ChangeComment_NoExistingComment_CommentInserted()
|
||||||
|
{
|
||||||
|
var p = new FanucProgram("\nO1234\nG0 X0\nM30\n%");
|
||||||
|
|
||||||
|
p.ChangeComment("INSERTED");
|
||||||
|
|
||||||
|
Assert.That(p.GetProgramComment(), Is.EqualTo("INSERTED"));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void Standardize_LeadingPercent_Removed()
|
||||||
|
{
|
||||||
|
var p = new FanucProgram("%\nO1234\nG0\n%");
|
||||||
|
|
||||||
|
Assert.That(p.Program, Does.Not.StartWith("%"));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void Standardize_AngleBracketTitle_ConvertedToPlain()
|
||||||
|
{
|
||||||
|
var p = new FanucProgram("\n<O1234>\nG0\n%");
|
||||||
|
|
||||||
|
Assert.That(p.Program, Does.Contain("O1234"));
|
||||||
|
Assert.That(p.Program, Does.Not.Contain("<O1234>"));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void ToString_ThreeFileFormat_ContainsAllSections()
|
||||||
|
{
|
||||||
|
var p = new FanucProgram(ThreeFileInput);
|
||||||
|
|
||||||
|
string output = p.ToString();
|
||||||
|
|
||||||
|
Assert.That(output, Does.Contain("program:{"));
|
||||||
|
Assert.That(output, Does.Contain("tooloffsetdata:{"));
|
||||||
|
Assert.That(output, Does.Contain("workzerooffset:{"));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void ToString_ProgramContent_RoundTrips()
|
||||||
|
{
|
||||||
|
var p = new FanucProgram(ThreeFileInput);
|
||||||
|
|
||||||
|
string output = p.ToString();
|
||||||
|
var p2 = new FanucProgram(output);
|
||||||
|
|
||||||
|
Assert.That(p2.GetProgramTitle(), Is.EqualTo(p.GetProgramTitle()));
|
||||||
|
Assert.That(p2.ToolOffsetData, Does.Contain("TOOL_DATA_LINE1"));
|
||||||
|
Assert.That(p2.WorkZeroOffsetData, Does.Contain("WORK_ZERO_LINE1"));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void SetValue_ToolOffsetData_ValueStored()
|
||||||
|
{
|
||||||
|
var p = new FanucProgram(SimpleProgram);
|
||||||
|
|
||||||
|
p.setValue("TOOL_DATA\n%", FanucProgram.FILE_TYPE.TOOL_OFFEST_DATA);
|
||||||
|
|
||||||
|
Assert.That(p.ToolOffsetData, Does.Contain("TOOL_DATA"));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void SetValue_WorkZeroOffset_ValueStored()
|
||||||
|
{
|
||||||
|
var p = new FanucProgram(SimpleProgram);
|
||||||
|
|
||||||
|
p.setValue("WORK_DATA\n%", FanucProgram.FILE_TYPE.WORK_ZERO_OFFSET);
|
||||||
|
|
||||||
|
Assert.That(p.WorkZeroOffsetData, Does.Contain("WORK_DATA"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
141
FanucProgramManager.Tests/Unit/HeidenhainMachineTests.cs
Normal file
141
FanucProgramManager.Tests/Unit/HeidenhainMachineTests.cs
Normal file
|
|
@ -0,0 +1,141 @@
|
||||||
|
using System;
|
||||||
|
using NUnit.Framework;
|
||||||
|
using Moq;
|
||||||
|
using FanucProgramManager.Cnc;
|
||||||
|
using FanucProgramManager.Cnc.Models;
|
||||||
|
using FanucProgramManager.Cnc.Heidenhain;
|
||||||
|
|
||||||
|
namespace FanucProgramManager.Tests.Unit
|
||||||
|
{
|
||||||
|
[TestFixture]
|
||||||
|
public class HeidenhainMachineTests
|
||||||
|
{
|
||||||
|
private HeidenhainConnectionConfig _config;
|
||||||
|
private Mock<ILsv2Client> _clientMock;
|
||||||
|
private Mock<INcProgramValidator> _validatorMock;
|
||||||
|
private HeidenhainMachine _machine;
|
||||||
|
|
||||||
|
[SetUp]
|
||||||
|
public void SetUp()
|
||||||
|
{
|
||||||
|
_config = new HeidenhainConnectionConfig
|
||||||
|
{
|
||||||
|
Name = "TestHeidenhain",
|
||||||
|
IpAddress = "192.168.1.4",
|
||||||
|
Port = 19000
|
||||||
|
};
|
||||||
|
_clientMock = new Mock<ILsv2Client>(MockBehavior.Strict);
|
||||||
|
// Always-ok validator so existing write tests are not affected by validation rules
|
||||||
|
_validatorMock = new Mock<INcProgramValidator>();
|
||||||
|
_validatorMock.Setup(v => v.Validate(It.IsAny<string>(), It.IsAny<string>()))
|
||||||
|
.Returns(ValidationResult.Ok());
|
||||||
|
// Internal constructor sets state = Connected immediately
|
||||||
|
_machine = new HeidenhainMachine(_config, _clientMock.Object, _validatorMock.Object);
|
||||||
|
}
|
||||||
|
|
||||||
|
[TearDown]
|
||||||
|
public void TearDown()
|
||||||
|
{
|
||||||
|
_clientMock.Setup(c => c.Disconnect());
|
||||||
|
_machine?.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
// State on construction
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void Constructor_WithInjectedClient_StateIsConnected()
|
||||||
|
{
|
||||||
|
Assert.That(_machine.State, Is.EqualTo(ConnectionState.Connected));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReadProgramAsync
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void ReadProgramAsync_ReceiveFileReturnsContent_ResultOk()
|
||||||
|
{
|
||||||
|
_clientMock.Setup(c => c.ReceiveFile("TNC:/nc_prog/O1234.H")).Returns("NC_CONTENT");
|
||||||
|
|
||||||
|
var result = _machine.ReadProgramAsync("TNC:/nc_prog/O1234.H").GetAwaiter().GetResult();
|
||||||
|
|
||||||
|
Assert.That(result.Success, Is.True);
|
||||||
|
Assert.That(result.Value, Is.EqualTo("NC_CONTENT"));
|
||||||
|
_clientMock.Verify(c => c.ReceiveFile("TNC:/nc_prog/O1234.H"), Times.Once);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void ReadProgramAsync_ReceiveFileReturnsNull_ResultIsFail()
|
||||||
|
{
|
||||||
|
_clientMock.Setup(c => c.ReceiveFile(It.IsAny<string>())).Returns((string)null);
|
||||||
|
|
||||||
|
var result = _machine.ReadProgramAsync("TNC:/nc_prog/MISSING.H").GetAwaiter().GetResult();
|
||||||
|
|
||||||
|
Assert.That(result.Success, Is.False);
|
||||||
|
}
|
||||||
|
|
||||||
|
// WriteProgramAsync
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void WriteProgramAsync_WithName_AppendsDotHExtension()
|
||||||
|
{
|
||||||
|
_clientMock.Setup(c => c.SendFile("TNC:/nc_prog/O1234.H", "CONTENT")).Returns(true);
|
||||||
|
|
||||||
|
var prog = new CncProgram { Name = "O1234", Content = "CONTENT" };
|
||||||
|
var result = _machine.WriteProgramAsync("TNC:/nc_prog/", prog).GetAwaiter().GetResult();
|
||||||
|
|
||||||
|
Assert.That(result.Success, Is.True);
|
||||||
|
_clientMock.Verify(c => c.SendFile("TNC:/nc_prog/O1234.H", "CONTENT"), Times.Once);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void WriteProgramAsync_NameAlreadyHasExtension_NoDoubleExtension()
|
||||||
|
{
|
||||||
|
_clientMock.Setup(c => c.SendFile("TNC:/nc_prog/O1234.H", "CONTENT")).Returns(true);
|
||||||
|
|
||||||
|
var prog = new CncProgram { Name = "O1234.H", Content = "CONTENT" };
|
||||||
|
_machine.WriteProgramAsync("TNC:/nc_prog/", prog).GetAwaiter().GetResult();
|
||||||
|
|
||||||
|
_clientMock.Verify(c => c.SendFile("TNC:/nc_prog/O1234.H", "CONTENT"), Times.Once);
|
||||||
|
_clientMock.Verify(c => c.SendFile(It.Is<string>(s => s.EndsWith(".H.H")), It.IsAny<string>()), Times.Never);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void WriteProgramAsync_EmptyContent_FailsWithoutCallingSendFile()
|
||||||
|
{
|
||||||
|
var prog = new CncProgram { Name = "O1234", Content = "" };
|
||||||
|
var result = _machine.WriteProgramAsync("TNC:/nc_prog/", prog).GetAwaiter().GetResult();
|
||||||
|
|
||||||
|
Assert.That(result.Success, Is.False);
|
||||||
|
_clientMock.Verify(c => c.SendFile(It.IsAny<string>(), It.IsAny<string>()), Times.Never);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void WriteProgramAsync_NullProgram_FailsWithoutCallingSendFile()
|
||||||
|
{
|
||||||
|
var result = _machine.WriteProgramAsync("TNC:/nc_prog/", null).GetAwaiter().GetResult();
|
||||||
|
|
||||||
|
Assert.That(result.Success, Is.False);
|
||||||
|
_clientMock.Verify(c => c.SendFile(It.IsAny<string>(), It.IsAny<string>()), Times.Never);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void WriteProgramAsync_SendFileFails_ResultIsFail()
|
||||||
|
{
|
||||||
|
_clientMock.Setup(c => c.SendFile(It.IsAny<string>(), It.IsAny<string>())).Returns(false);
|
||||||
|
|
||||||
|
var prog = new CncProgram { Name = "O1234", Content = "DATA" };
|
||||||
|
var result = _machine.WriteProgramAsync("TNC:/nc_prog/", prog).GetAwaiter().GetResult();
|
||||||
|
|
||||||
|
Assert.That(result.Success, Is.False);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReadActiveProgramAsync
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void ReadActiveProgramAsync_ReturnsFailNotSupported()
|
||||||
|
{
|
||||||
|
var result = _machine.ReadActiveProgramAsync().GetAwaiter().GetResult();
|
||||||
|
|
||||||
|
Assert.That(result.Success, Is.False);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
105
FanucProgramManager.Tests/Unit/IsoProgramFormatterTests.cs
Normal file
105
FanucProgramManager.Tests/Unit/IsoProgramFormatterTests.cs
Normal file
|
|
@ -0,0 +1,105 @@
|
||||||
|
using NUnit.Framework;
|
||||||
|
using FanucProgramManager.Cnc.Fanuc;
|
||||||
|
|
||||||
|
namespace FanucProgramManager.Tests.Unit
|
||||||
|
{
|
||||||
|
[TestFixture]
|
||||||
|
public class IsoProgramFormatterTests
|
||||||
|
{
|
||||||
|
// EnsureTitleBrackets
|
||||||
|
// The formatter works on programs with a leading blank line then a title line.
|
||||||
|
// e.g. "%\nO1234(COMMENT)\nG0...\n"
|
||||||
|
// SplitFirstNonEmptyLine splits after the SECOND newline, so we need at least 2 newlines.
|
||||||
|
|
||||||
|
private const string ProgramWithBrackets =
|
||||||
|
"%\n<O1234>(COMMENT)\nG0 X0\nM30\n";
|
||||||
|
|
||||||
|
private const string ProgramNoBrackets =
|
||||||
|
"%\nO1234(COMMENT)\nG0 X0\nM30\n";
|
||||||
|
|
||||||
|
private const string ProgramNoComment =
|
||||||
|
"%\nO1234\nG0 X0\nM30\n";
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void EnsureTitleBrackets_NoBrackets_AddsBrackets()
|
||||||
|
{
|
||||||
|
string result = IsoProgramFormatter.EnsureTitleBrackets(ProgramNoBrackets);
|
||||||
|
|
||||||
|
Assert.That(result, Does.Contain("<O1234>"));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void EnsureTitleBrackets_AlreadyHasBrackets_NoChange()
|
||||||
|
{
|
||||||
|
string result = IsoProgramFormatter.EnsureTitleBrackets(ProgramWithBrackets);
|
||||||
|
|
||||||
|
Assert.That(result, Is.EqualTo(ProgramWithBrackets));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void EnsureTitleBrackets_NullOrEmpty_ReturnsInput()
|
||||||
|
{
|
||||||
|
Assert.That(IsoProgramFormatter.EnsureTitleBrackets(null), Is.Null);
|
||||||
|
Assert.That(IsoProgramFormatter.EnsureTitleBrackets(""), Is.EqualTo(""));
|
||||||
|
}
|
||||||
|
|
||||||
|
// InsertOrReplaceComment
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void InsertOrReplaceComment_NoComment_InsertsComment()
|
||||||
|
{
|
||||||
|
string result = IsoProgramFormatter.InsertOrReplaceComment(ProgramNoComment, "TEST");
|
||||||
|
|
||||||
|
Assert.That(result, Does.Contain("(TEST)"));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void InsertOrReplaceComment_ExistingComment_ReplacesComment()
|
||||||
|
{
|
||||||
|
string result = IsoProgramFormatter.InsertOrReplaceComment(ProgramNoBrackets, "NEW");
|
||||||
|
|
||||||
|
Assert.That(result, Does.Contain("(NEW)"));
|
||||||
|
Assert.That(result, Does.Not.Contain("(COMMENT)"));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void InsertOrReplaceComment_EmptyComment_InsertsEmptyParens()
|
||||||
|
{
|
||||||
|
string result = IsoProgramFormatter.InsertOrReplaceComment(ProgramNoComment, "");
|
||||||
|
|
||||||
|
Assert.That(result, Does.Contain("()"));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void InsertOrReplaceComment_NullOrEmpty_ReturnsInput()
|
||||||
|
{
|
||||||
|
Assert.That(IsoProgramFormatter.InsertOrReplaceComment(null, "X"), Is.Null);
|
||||||
|
Assert.That(IsoProgramFormatter.InsertOrReplaceComment("", "X"), Is.EqualTo(""));
|
||||||
|
}
|
||||||
|
|
||||||
|
// RenameAndComment
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void RenameAndComment_ProducesCorrectHeader()
|
||||||
|
{
|
||||||
|
string result = IsoProgramFormatter.RenameAndComment(ProgramNoBrackets, "O9999", "DESC");
|
||||||
|
|
||||||
|
Assert.That(result, Does.StartWith("%\n<O9999>(DESC)"));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void RenameAndComment_BodyAppendedAfterHeader()
|
||||||
|
{
|
||||||
|
string result = IsoProgramFormatter.RenameAndComment(ProgramNoBrackets, "O9999", "DESC");
|
||||||
|
|
||||||
|
Assert.That(result, Does.Contain("G0 X0"));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void RenameAndComment_NullOrEmpty_ReturnsInput()
|
||||||
|
{
|
||||||
|
Assert.That(IsoProgramFormatter.RenameAndComment(null, "X", "Y"), Is.Null);
|
||||||
|
Assert.That(IsoProgramFormatter.RenameAndComment("", "X", "Y"), Is.EqualTo(""));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
163
FanucProgramManager.Tests/Unit/MitsubishiMachineTests.cs
Normal file
163
FanucProgramManager.Tests/Unit/MitsubishiMachineTests.cs
Normal file
|
|
@ -0,0 +1,163 @@
|
||||||
|
using System;
|
||||||
|
using NUnit.Framework;
|
||||||
|
using Moq;
|
||||||
|
using FanucProgramManager.Cnc;
|
||||||
|
using FanucProgramManager.Cnc.Models;
|
||||||
|
using FanucProgramManager.Cnc.Mitsubishi;
|
||||||
|
|
||||||
|
namespace FanucProgramManager.Tests.Unit
|
||||||
|
{
|
||||||
|
[TestFixture]
|
||||||
|
public class MitsubishiMachineTests
|
||||||
|
{
|
||||||
|
private MitsubishiConnectionConfig _config;
|
||||||
|
private Mock<IMitsubishiFtpClient> _ftpMock;
|
||||||
|
private Mock<INcProgramValidator> _validatorMock;
|
||||||
|
private MitsubishiMachine _machine;
|
||||||
|
|
||||||
|
[SetUp]
|
||||||
|
public void SetUp()
|
||||||
|
{
|
||||||
|
_config = new MitsubishiConnectionConfig
|
||||||
|
{
|
||||||
|
Name = "TestMitsubishi",
|
||||||
|
IpAddress = "192.168.1.3",
|
||||||
|
Port = 21,
|
||||||
|
ProgramDirectory = "/PRG/"
|
||||||
|
};
|
||||||
|
_ftpMock = new Mock<IMitsubishiFtpClient>(MockBehavior.Strict);
|
||||||
|
// Always-ok validator so existing write tests are not affected by validation rules
|
||||||
|
_validatorMock = new Mock<INcProgramValidator>();
|
||||||
|
_validatorMock.Setup(v => v.Validate(It.IsAny<string>(), It.IsAny<string>()))
|
||||||
|
.Returns(ValidationResult.Ok());
|
||||||
|
_machine = new MitsubishiMachine(_config, _ftpMock.Object, _validatorMock.Object);
|
||||||
|
}
|
||||||
|
|
||||||
|
[TearDown]
|
||||||
|
public void TearDown()
|
||||||
|
{
|
||||||
|
_machine?.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ConnectAsync
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void ConnectAsync_PingTrue_StateConnectedAndResultOk()
|
||||||
|
{
|
||||||
|
_ftpMock.Setup(f => f.Ping()).Returns(true);
|
||||||
|
|
||||||
|
var result = _machine.ConnectAsync().GetAwaiter().GetResult();
|
||||||
|
|
||||||
|
Assert.That(result.Success, Is.True);
|
||||||
|
Assert.That(_machine.State, Is.EqualTo(ConnectionState.Connected));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void ConnectAsync_PingFalse_StateFaultedAndResultFail()
|
||||||
|
{
|
||||||
|
_ftpMock.Setup(f => f.Ping()).Returns(false);
|
||||||
|
|
||||||
|
var result = _machine.ConnectAsync().GetAwaiter().GetResult();
|
||||||
|
|
||||||
|
Assert.That(result.Success, Is.False);
|
||||||
|
Assert.That(_machine.State, Is.EqualTo(ConnectionState.Faulted));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReadProgramAsync — no .MPF extension for Mitsubishi
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void ReadProgramAsync_FullPath_CallsDownloadFileNoExtension()
|
||||||
|
{
|
||||||
|
_ftpMock.Setup(f => f.Ping()).Returns(true);
|
||||||
|
_ftpMock.Setup(f => f.DownloadFile("/PRG/O1234")).Returns("NC_DATA");
|
||||||
|
_machine.ConnectAsync().GetAwaiter().GetResult();
|
||||||
|
|
||||||
|
var result = _machine.ReadProgramAsync("/PRG/O1234").GetAwaiter().GetResult();
|
||||||
|
|
||||||
|
Assert.That(result.Success, Is.True);
|
||||||
|
Assert.That(result.Value, Is.EqualTo("NC_DATA"));
|
||||||
|
_ftpMock.Verify(f => f.DownloadFile("/PRG/O1234"), Times.Once);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void ReadProgramAsync_ShortName_ResolvesWithDirectoryNoExtension()
|
||||||
|
{
|
||||||
|
_ftpMock.Setup(f => f.Ping()).Returns(true);
|
||||||
|
_ftpMock.Setup(f => f.DownloadFile("/PRG/O1234")).Returns("DATA");
|
||||||
|
_machine.ConnectAsync().GetAwaiter().GetResult();
|
||||||
|
|
||||||
|
_machine.ReadProgramAsync("O1234").GetAwaiter().GetResult();
|
||||||
|
|
||||||
|
// Must NOT append .MPF
|
||||||
|
_ftpMock.Verify(f => f.DownloadFile("/PRG/O1234"), Times.Once);
|
||||||
|
_ftpMock.Verify(f => f.DownloadFile(It.Is<string>(s => s.EndsWith(".MPF"))), Times.Never);
|
||||||
|
}
|
||||||
|
|
||||||
|
// WriteProgramAsync
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void WriteProgramAsync_ValidContent_CallsUploadFileNoExtension()
|
||||||
|
{
|
||||||
|
_ftpMock.Setup(f => f.Ping()).Returns(true);
|
||||||
|
_ftpMock.Setup(f => f.UploadFile("/PRG/O1234", "CONTENT"));
|
||||||
|
_machine.ConnectAsync().GetAwaiter().GetResult();
|
||||||
|
|
||||||
|
var prog = new CncProgram { Name = "O1234", Content = "CONTENT" };
|
||||||
|
var result = _machine.WriteProgramAsync("/PRG/", prog).GetAwaiter().GetResult();
|
||||||
|
|
||||||
|
Assert.That(result.Success, Is.True);
|
||||||
|
_ftpMock.Verify(f => f.UploadFile("/PRG/O1234", "CONTENT"), Times.Once);
|
||||||
|
_ftpMock.Verify(f => f.UploadFile(It.Is<string>(s => s.EndsWith(".MPF")), It.IsAny<string>()), Times.Never);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void WriteProgramAsync_EmptyContent_FailsWithoutCallingUploadFile()
|
||||||
|
{
|
||||||
|
_ftpMock.Setup(f => f.Ping()).Returns(true);
|
||||||
|
_machine.ConnectAsync().GetAwaiter().GetResult();
|
||||||
|
|
||||||
|
var prog = new CncProgram { Name = "O1234", Content = "" };
|
||||||
|
var result = _machine.WriteProgramAsync("/PRG/", prog).GetAwaiter().GetResult();
|
||||||
|
|
||||||
|
Assert.That(result.Success, Is.False);
|
||||||
|
_ftpMock.Verify(f => f.UploadFile(It.IsAny<string>(), It.IsAny<string>()), Times.Never);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void WriteProgramAsync_NullProgram_FailsWithoutCallingUploadFile()
|
||||||
|
{
|
||||||
|
_ftpMock.Setup(f => f.Ping()).Returns(true);
|
||||||
|
_machine.ConnectAsync().GetAwaiter().GetResult();
|
||||||
|
|
||||||
|
var result = _machine.WriteProgramAsync("/PRG/", null).GetAwaiter().GetResult();
|
||||||
|
|
||||||
|
Assert.That(result.Success, Is.False);
|
||||||
|
_ftpMock.Verify(f => f.UploadFile(It.IsAny<string>(), It.IsAny<string>()), Times.Never);
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteProgramAsync
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void DeleteProgramAsync_ValidPath_CallsDeleteFile()
|
||||||
|
{
|
||||||
|
_ftpMock.Setup(f => f.Ping()).Returns(true);
|
||||||
|
_ftpMock.Setup(f => f.DeleteFile("/PRG/O1234"));
|
||||||
|
_machine.ConnectAsync().GetAwaiter().GetResult();
|
||||||
|
|
||||||
|
var result = _machine.DeleteProgramAsync("/PRG/O1234").GetAwaiter().GetResult();
|
||||||
|
|
||||||
|
Assert.That(result.Success, Is.True);
|
||||||
|
_ftpMock.Verify(f => f.DeleteFile("/PRG/O1234"), Times.Once);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReadActiveProgramAsync
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void ReadActiveProgramAsync_ReturnsFailNotSupported()
|
||||||
|
{
|
||||||
|
var result = _machine.ReadActiveProgramAsync().GetAwaiter().GetResult();
|
||||||
|
|
||||||
|
Assert.That(result.Success, Is.False);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
280
FanucProgramManager.Tests/Unit/NcProgramValidatorTests.cs
Normal file
280
FanucProgramManager.Tests/Unit/NcProgramValidatorTests.cs
Normal file
|
|
@ -0,0 +1,280 @@
|
||||||
|
using NUnit.Framework;
|
||||||
|
using FanucProgramManager.Cnc;
|
||||||
|
using FanucProgramManager.Cnc.Models;
|
||||||
|
|
||||||
|
namespace FanucProgramManager.Tests.Unit
|
||||||
|
{
|
||||||
|
[TestFixture]
|
||||||
|
public class NcProgramValidatorTests
|
||||||
|
{
|
||||||
|
// ── Universal rules ───────────────────────────────────────────────────
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void Validate_BlockTooLong_ReturnsError()
|
||||||
|
{
|
||||||
|
var v = new NcProgramValidator(CncManufacturer.Fanuc);
|
||||||
|
string longLine = "O1234\n" + new string('X', 81);
|
||||||
|
var r = v.Validate("O1234", longLine);
|
||||||
|
Assert.That(r.Success, Is.False);
|
||||||
|
Assert.That(HasErrorContaining(r, "80"), Is.True);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void Validate_BlockExactly80_NoLengthError()
|
||||||
|
{
|
||||||
|
var v = new NcProgramValidator(CncManufacturer.Fanuc);
|
||||||
|
string line80 = new string('G', 80);
|
||||||
|
var r = v.Validate("O1234", "O1234\n" + line80);
|
||||||
|
// Should not have a block-length error (may still have other errors)
|
||||||
|
Assert.That(HasErrorContaining(r, "Block length"), Is.False);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void Validate_LowercaseLetter_ReturnsError()
|
||||||
|
{
|
||||||
|
var v = new NcProgramValidator(CncManufacturer.Siemens);
|
||||||
|
var r = v.Validate("PROG.MPF", "PROG.MPF\ng00 X10");
|
||||||
|
Assert.That(r.Success, Is.False);
|
||||||
|
Assert.That(HasErrorContaining(r, "Lowercase"), Is.True);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void Validate_NoLowercase_NoLowercaseError()
|
||||||
|
{
|
||||||
|
var v = new NcProgramValidator(CncManufacturer.Siemens);
|
||||||
|
var r = v.Validate("PROG.MPF", "G00 X10 Y20");
|
||||||
|
Assert.That(HasErrorContaining(r, "Lowercase"), Is.False);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void Validate_TabCharacter_ReturnsError()
|
||||||
|
{
|
||||||
|
var v = new NcProgramValidator(CncManufacturer.Siemens);
|
||||||
|
var r = v.Validate("PROG.MPF", "G00\tX10");
|
||||||
|
Assert.That(r.Success, Is.False);
|
||||||
|
Assert.That(HasErrorContaining(r, "Tab"), Is.True);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void Validate_NestedParentheses_ReturnsError()
|
||||||
|
{
|
||||||
|
var v = new NcProgramValidator(CncManufacturer.Fanuc);
|
||||||
|
var r = v.Validate("O1234", "O1234\nG00 ((NESTED))");
|
||||||
|
Assert.That(r.Success, Is.False);
|
||||||
|
Assert.That(HasErrorContaining(r, "Nested"), Is.True);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void Validate_SingleParenComment_NoNestedError()
|
||||||
|
{
|
||||||
|
var v = new NcProgramValidator(CncManufacturer.Fanuc);
|
||||||
|
var r = v.Validate("O1234", "O1234\nG00 (COMMENT) X10");
|
||||||
|
Assert.That(HasErrorContaining(r, "Nested"), Is.False);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void Validate_CommentExceeds32Chars_ReturnsError()
|
||||||
|
{
|
||||||
|
var v = new NcProgramValidator(CncManufacturer.Fanuc);
|
||||||
|
string longComment = "(" + new string('A', 33) + ")";
|
||||||
|
var r = v.Validate("O1234", "O1234\n" + longComment);
|
||||||
|
Assert.That(r.Success, Is.False);
|
||||||
|
Assert.That(HasErrorContaining(r, "Comment length"), Is.True);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void Validate_CommentExactly32Chars_NoCommentLengthError()
|
||||||
|
{
|
||||||
|
var v = new NcProgramValidator(CncManufacturer.Fanuc);
|
||||||
|
string comment = "(" + new string('A', 32) + ")";
|
||||||
|
var r = v.Validate("O1234", "O1234\n" + comment);
|
||||||
|
Assert.That(HasErrorContaining(r, "Comment length"), Is.False);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Fanuc rules ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void Validate_Fanuc_ValidNameAndContent_Succeeds()
|
||||||
|
{
|
||||||
|
var v = new NcProgramValidator(CncManufacturer.Fanuc);
|
||||||
|
var r = v.Validate("O1234", "O1234\nG00 X10 Y20\nM30");
|
||||||
|
Assert.That(r.Success, Is.True);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void Validate_Fanuc_NameNotOFourDigits_ReturnsError()
|
||||||
|
{
|
||||||
|
var v = new NcProgramValidator(CncManufacturer.Fanuc);
|
||||||
|
var r = v.Validate("PROG", "O1234\nG00");
|
||||||
|
Assert.That(r.Success, Is.False);
|
||||||
|
Assert.That(HasErrorContaining(r, "O####"), Is.True);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void Validate_Fanuc_NameO12345_ReturnsError()
|
||||||
|
{
|
||||||
|
var v = new NcProgramValidator(CncManufacturer.Fanuc);
|
||||||
|
// 5 digits — invalid for Fanuc
|
||||||
|
var r = v.Validate("O12345", "O1234\nG00");
|
||||||
|
Assert.That(r.Success, Is.False);
|
||||||
|
Assert.That(HasErrorContaining(r, "O####"), Is.True);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void Validate_Fanuc_FirstLineNotONumber_ReturnsError()
|
||||||
|
{
|
||||||
|
var v = new NcProgramValidator(CncManufacturer.Fanuc);
|
||||||
|
var r = v.Validate("O1234", "G00 X10\nM30");
|
||||||
|
Assert.That(r.Success, Is.False);
|
||||||
|
Assert.That(HasErrorContaining(r, "First line"), Is.True);
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestCase(CncManufacturer.Fanuc, "O1234", "O1234\nG00 X10;")]
|
||||||
|
[TestCase(CncManufacturer.Mitsubishi, "O1234", "O1234\nG00 X10;")]
|
||||||
|
[TestCase(CncManufacturer.Siemens, "PROG.MPF", "G00 X10;")]
|
||||||
|
[TestCase(CncManufacturer.Heidenhain, "PROG.H", "L X+10;")]
|
||||||
|
public void Validate_SemicolonForbidden_ReturnsError(CncManufacturer m, string name, string content)
|
||||||
|
{
|
||||||
|
var r = new NcProgramValidator(m).Validate(name, content);
|
||||||
|
Assert.That(r.Success, Is.False);
|
||||||
|
Assert.That(HasErrorContaining(r, ";"), Is.True);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void Validate_Fanuc_BracketForbidden_ReturnsError()
|
||||||
|
{
|
||||||
|
var v = new NcProgramValidator(CncManufacturer.Fanuc);
|
||||||
|
var r = v.Validate("O1234", "O1234\nG00 [X10]");
|
||||||
|
Assert.That(r.Success, Is.False);
|
||||||
|
Assert.That(HasErrorContaining(r, "["), Is.True);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void Validate_Fanuc_MacroHash_ReturnsWarningNotError()
|
||||||
|
{
|
||||||
|
var v = new NcProgramValidator(CncManufacturer.Fanuc);
|
||||||
|
var r = v.Validate("O1234", "O1234\n#100=10");
|
||||||
|
// Warnings alone do not cause failure
|
||||||
|
Assert.That(r.Success, Is.True);
|
||||||
|
Assert.That(HasWarningContaining(r, "#"), Is.True);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void Validate_Fanuc_NameFromPathStripped()
|
||||||
|
{
|
||||||
|
var v = new NcProgramValidator(CncManufacturer.Fanuc);
|
||||||
|
// Full path — name part is O1234
|
||||||
|
var r = v.Validate("//CNC_MEM/USER/PATH1/O1234", "O1234\nG00 X10");
|
||||||
|
Assert.That(HasErrorContaining(r, "O####"), Is.False);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Mitsubishi rules ──────────────────────────────────────────────────
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void Validate_Mitsubishi_ValidNameAndContent_Succeeds()
|
||||||
|
{
|
||||||
|
var v = new NcProgramValidator(CncManufacturer.Mitsubishi);
|
||||||
|
var r = v.Validate("O1234", "O1234\nG00 X10\nM30");
|
||||||
|
Assert.That(r.Success, Is.True);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void Validate_Mitsubishi_BadName_ReturnsError()
|
||||||
|
{
|
||||||
|
var v = new NcProgramValidator(CncManufacturer.Mitsubishi);
|
||||||
|
var r = v.Validate("PROG", "O1234\nG00");
|
||||||
|
Assert.That(r.Success, Is.False);
|
||||||
|
Assert.That(HasErrorContaining(r, "O####"), Is.True);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Siemens rules ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void Validate_Siemens_ValidNameAndContent_Succeeds()
|
||||||
|
{
|
||||||
|
var v = new NcProgramValidator(CncManufacturer.Siemens);
|
||||||
|
var r = v.Validate("PROG.MPF", "G00 X10 Y20\nM30");
|
||||||
|
Assert.That(r.Success, Is.True);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void Validate_Siemens_NameNoDotMpf_ReturnsError()
|
||||||
|
{
|
||||||
|
var v = new NcProgramValidator(CncManufacturer.Siemens);
|
||||||
|
var r = v.Validate("PROG", "G00 X10");
|
||||||
|
Assert.That(r.Success, Is.False);
|
||||||
|
Assert.That(HasErrorContaining(r, ".MPF"), Is.True);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void Validate_Siemens_LowercaseMpf_ReturnsNameError()
|
||||||
|
{
|
||||||
|
var v = new NcProgramValidator(CncManufacturer.Siemens);
|
||||||
|
var r = v.Validate("prog.mpf", "G00 X10");
|
||||||
|
Assert.That(r.Success, Is.False);
|
||||||
|
Assert.That(HasErrorContaining(r, ".MPF"), Is.True);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void Validate_Siemens_NoFirstLineRequired()
|
||||||
|
{
|
||||||
|
var v = new NcProgramValidator(CncManufacturer.Siemens);
|
||||||
|
var r = v.Validate("PROG.MPF", "G00 X10\nM30");
|
||||||
|
Assert.That(HasErrorContaining(r, "First line"), Is.False);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Heidenhain rules ──────────────────────────────────────────────────
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void Validate_Heidenhain_ValidNameAndContent_Succeeds()
|
||||||
|
{
|
||||||
|
var v = new NcProgramValidator(CncManufacturer.Heidenhain);
|
||||||
|
var r = v.Validate("PROG.H", "L X+10 Y+20 R0 FMAX\nL Z-5 F500");
|
||||||
|
Assert.That(r.Success, Is.True);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void Validate_Heidenhain_NameNoDotH_ReturnsError()
|
||||||
|
{
|
||||||
|
var v = new NcProgramValidator(CncManufacturer.Heidenhain);
|
||||||
|
var r = v.Validate("PROG", "L X+10");
|
||||||
|
Assert.That(r.Success, Is.False);
|
||||||
|
Assert.That(HasErrorContaining(r, ".H"), Is.True);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void Validate_Heidenhain_PercentForbidden_ReturnsError()
|
||||||
|
{
|
||||||
|
var v = new NcProgramValidator(CncManufacturer.Heidenhain);
|
||||||
|
var r = v.Validate("PROG.H", "L X+10 % COMMENT");
|
||||||
|
Assert.That(r.Success, Is.False);
|
||||||
|
Assert.That(HasErrorContaining(r, "%"), Is.True);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void Validate_Heidenhain_NoFirstLineRequired()
|
||||||
|
{
|
||||||
|
var v = new NcProgramValidator(CncManufacturer.Heidenhain);
|
||||||
|
var r = v.Validate("PROG.H", "L X+10\nM30");
|
||||||
|
Assert.That(HasErrorContaining(r, "First line"), Is.False);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Helpers ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private static bool HasErrorContaining(ValidationResult r, string fragment)
|
||||||
|
{
|
||||||
|
foreach (var e in r.Errors)
|
||||||
|
if (e.Severity == ValidationSeverity.Error && e.Message.Contains(fragment))
|
||||||
|
return true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool HasWarningContaining(ValidationResult r, string fragment)
|
||||||
|
{
|
||||||
|
foreach (var e in r.Errors)
|
||||||
|
if (e.Severity == ValidationSeverity.Warning && e.Message.Contains(fragment))
|
||||||
|
return true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
161
FanucProgramManager.Tests/Unit/SiemensMachineTests.cs
Normal file
161
FanucProgramManager.Tests/Unit/SiemensMachineTests.cs
Normal file
|
|
@ -0,0 +1,161 @@
|
||||||
|
using System;
|
||||||
|
using NUnit.Framework;
|
||||||
|
using Moq;
|
||||||
|
using FanucProgramManager.Cnc;
|
||||||
|
using FanucProgramManager.Cnc.Models;
|
||||||
|
using FanucProgramManager.Cnc.Siemens;
|
||||||
|
|
||||||
|
namespace FanucProgramManager.Tests.Unit
|
||||||
|
{
|
||||||
|
[TestFixture]
|
||||||
|
public class SiemensMachineTests
|
||||||
|
{
|
||||||
|
private SiemensConnectionConfig _config;
|
||||||
|
private Mock<ISiemensFtpClient> _ftpMock;
|
||||||
|
private Mock<INcProgramValidator> _validatorMock;
|
||||||
|
private SiemensMachine _machine;
|
||||||
|
|
||||||
|
[SetUp]
|
||||||
|
public void SetUp()
|
||||||
|
{
|
||||||
|
_config = new SiemensConnectionConfig
|
||||||
|
{
|
||||||
|
Name = "TestSiemens",
|
||||||
|
IpAddress = "192.168.1.2",
|
||||||
|
Port = 21,
|
||||||
|
ProgramDirectory = "/_N_MPF_DIR/"
|
||||||
|
};
|
||||||
|
_ftpMock = new Mock<ISiemensFtpClient>(MockBehavior.Strict);
|
||||||
|
// Always-ok validator so existing write tests are not affected by validation rules
|
||||||
|
_validatorMock = new Mock<INcProgramValidator>();
|
||||||
|
_validatorMock.Setup(v => v.Validate(It.IsAny<string>(), It.IsAny<string>()))
|
||||||
|
.Returns(ValidationResult.Ok());
|
||||||
|
_machine = new SiemensMachine(_config, _ftpMock.Object, _validatorMock.Object);
|
||||||
|
}
|
||||||
|
|
||||||
|
[TearDown]
|
||||||
|
public void TearDown()
|
||||||
|
{
|
||||||
|
_machine?.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ConnectAsync
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void ConnectAsync_PingTrue_StateConnectedAndResultOk()
|
||||||
|
{
|
||||||
|
_ftpMock.Setup(f => f.Ping()).Returns(true);
|
||||||
|
|
||||||
|
var result = _machine.ConnectAsync().GetAwaiter().GetResult();
|
||||||
|
|
||||||
|
Assert.That(result.Success, Is.True);
|
||||||
|
Assert.That(result.Value, Is.True);
|
||||||
|
Assert.That(_machine.State, Is.EqualTo(ConnectionState.Connected));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void ConnectAsync_PingFalse_StateFaultedAndResultFail()
|
||||||
|
{
|
||||||
|
_ftpMock.Setup(f => f.Ping()).Returns(false);
|
||||||
|
|
||||||
|
var result = _machine.ConnectAsync().GetAwaiter().GetResult();
|
||||||
|
|
||||||
|
Assert.That(result.Success, Is.False);
|
||||||
|
Assert.That(_machine.State, Is.EqualTo(ConnectionState.Faulted));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReadProgramAsync
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void ReadProgramAsync_FullPath_CallsDownloadFileWithPath()
|
||||||
|
{
|
||||||
|
_ftpMock.Setup(f => f.Ping()).Returns(true);
|
||||||
|
_ftpMock.Setup(f => f.DownloadFile("/_N_MPF_DIR/O1234.MPF")).Returns("PROGRAM_CONTENT");
|
||||||
|
_machine.ConnectAsync().GetAwaiter().GetResult();
|
||||||
|
|
||||||
|
var result = _machine.ReadProgramAsync("/_N_MPF_DIR/O1234.MPF").GetAwaiter().GetResult();
|
||||||
|
|
||||||
|
Assert.That(result.Success, Is.True);
|
||||||
|
Assert.That(result.Value, Is.EqualTo("PROGRAM_CONTENT"));
|
||||||
|
_ftpMock.Verify(f => f.DownloadFile("/_N_MPF_DIR/O1234.MPF"), Times.Once);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void ReadProgramAsync_ShortName_ResolvesWithDirectoryAndMpfExtension()
|
||||||
|
{
|
||||||
|
_ftpMock.Setup(f => f.Ping()).Returns(true);
|
||||||
|
_ftpMock.Setup(f => f.DownloadFile("/_N_MPF_DIR/O1234.MPF")).Returns("DATA");
|
||||||
|
_machine.ConnectAsync().GetAwaiter().GetResult();
|
||||||
|
|
||||||
|
_machine.ReadProgramAsync("O1234").GetAwaiter().GetResult();
|
||||||
|
|
||||||
|
_ftpMock.Verify(f => f.DownloadFile("/_N_MPF_DIR/O1234.MPF"), Times.Once);
|
||||||
|
}
|
||||||
|
|
||||||
|
// WriteProgramAsync
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void WriteProgramAsync_ValidContent_CallsUploadFile()
|
||||||
|
{
|
||||||
|
_ftpMock.Setup(f => f.Ping()).Returns(true);
|
||||||
|
_ftpMock.Setup(f => f.UploadFile(It.IsAny<string>(), "CONTENT"));
|
||||||
|
_machine.ConnectAsync().GetAwaiter().GetResult();
|
||||||
|
|
||||||
|
var prog = new CncProgram { Name = "O1234", Content = "CONTENT" };
|
||||||
|
var result = _machine.WriteProgramAsync("/_N_MPF_DIR/", prog).GetAwaiter().GetResult();
|
||||||
|
|
||||||
|
Assert.That(result.Success, Is.True);
|
||||||
|
_ftpMock.Verify(f => f.UploadFile(It.IsAny<string>(), "CONTENT"), Times.Once);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void WriteProgramAsync_EmptyContent_FailsWithoutCallingUploadFile()
|
||||||
|
{
|
||||||
|
_ftpMock.Setup(f => f.Ping()).Returns(true);
|
||||||
|
_machine.ConnectAsync().GetAwaiter().GetResult();
|
||||||
|
|
||||||
|
var prog = new CncProgram { Name = "O1234", Content = "" };
|
||||||
|
var result = _machine.WriteProgramAsync("/_N_MPF_DIR/", prog).GetAwaiter().GetResult();
|
||||||
|
|
||||||
|
Assert.That(result.Success, Is.False);
|
||||||
|
_ftpMock.Verify(f => f.UploadFile(It.IsAny<string>(), It.IsAny<string>()), Times.Never);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void WriteProgramAsync_NullProgram_FailsWithoutCallingUploadFile()
|
||||||
|
{
|
||||||
|
_ftpMock.Setup(f => f.Ping()).Returns(true);
|
||||||
|
_machine.ConnectAsync().GetAwaiter().GetResult();
|
||||||
|
|
||||||
|
var result = _machine.WriteProgramAsync("/_N_MPF_DIR/", null).GetAwaiter().GetResult();
|
||||||
|
|
||||||
|
Assert.That(result.Success, Is.False);
|
||||||
|
_ftpMock.Verify(f => f.UploadFile(It.IsAny<string>(), It.IsAny<string>()), Times.Never);
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteProgramAsync
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void DeleteProgramAsync_ValidPath_CallsDeleteFile()
|
||||||
|
{
|
||||||
|
_ftpMock.Setup(f => f.Ping()).Returns(true);
|
||||||
|
_ftpMock.Setup(f => f.DeleteFile("/_N_MPF_DIR/O1234.MPF"));
|
||||||
|
_machine.ConnectAsync().GetAwaiter().GetResult();
|
||||||
|
|
||||||
|
var result = _machine.DeleteProgramAsync("/_N_MPF_DIR/O1234.MPF").GetAwaiter().GetResult();
|
||||||
|
|
||||||
|
Assert.That(result.Success, Is.True);
|
||||||
|
_ftpMock.Verify(f => f.DeleteFile("/_N_MPF_DIR/O1234.MPF"), Times.Once);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReadActiveProgramAsync
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void ReadActiveProgramAsync_ReturnsFailNotSupported()
|
||||||
|
{
|
||||||
|
var result = _machine.ReadActiveProgramAsync().GetAwaiter().GetResult();
|
||||||
|
|
||||||
|
Assert.That(result.Success, Is.False);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
7
FanucProgramManager.Tests/packages.config
Normal file
7
FanucProgramManager.Tests/packages.config
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<packages>
|
||||||
|
<package id="NUnit" version="3.13.3" targetFramework="net472" />
|
||||||
|
<package id="NUnit3TestAdapter" version="4.2.1" targetFramework="net472" />
|
||||||
|
<package id="Moq" version="4.18.4" targetFramework="net472" />
|
||||||
|
<package id="Castle.Core" version="5.1.1" targetFramework="net472" />
|
||||||
|
</packages>
|
||||||
151
FanucProgramManager.csproj
Executable file
151
FanucProgramManager.csproj
Executable file
|
|
@ -0,0 +1,151 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||||
|
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
|
||||||
|
<PropertyGroup>
|
||||||
|
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
|
||||||
|
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
|
||||||
|
<ProjectGuid>{1032188B-3321-42B2-87ED-343BF718CAF5}</ProjectGuid>
|
||||||
|
<OutputType>Exe</OutputType>
|
||||||
|
<RootNamespace>FanucProgramManager</RootNamespace>
|
||||||
|
<AssemblyName>FanucProgramManager</AssemblyName>
|
||||||
|
<TargetFrameworkVersion>v4.7.2</TargetFrameworkVersion>
|
||||||
|
<FileAlignment>512</FileAlignment>
|
||||||
|
<AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects>
|
||||||
|
<Deterministic>true</Deterministic>
|
||||||
|
<IsWebBootstrapper>false</IsWebBootstrapper>
|
||||||
|
<PublishUrl>publish\</PublishUrl>
|
||||||
|
<Install>true</Install>
|
||||||
|
<InstallFrom>Disk</InstallFrom>
|
||||||
|
<UpdateEnabled>false</UpdateEnabled>
|
||||||
|
<UpdateMode>Foreground</UpdateMode>
|
||||||
|
<UpdateInterval>7</UpdateInterval>
|
||||||
|
<UpdateIntervalUnits>Days</UpdateIntervalUnits>
|
||||||
|
<UpdatePeriodically>false</UpdatePeriodically>
|
||||||
|
<UpdateRequired>false</UpdateRequired>
|
||||||
|
<MapFileExtensions>true</MapFileExtensions>
|
||||||
|
<ApplicationRevision>0</ApplicationRevision>
|
||||||
|
<ApplicationVersion>1.0.0.%2a</ApplicationVersion>
|
||||||
|
<UseApplicationTrust>false</UseApplicationTrust>
|
||||||
|
<BootstrapperEnabled>true</BootstrapperEnabled>
|
||||||
|
</PropertyGroup>
|
||||||
|
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
|
||||||
|
<PlatformTarget>AnyCPU</PlatformTarget>
|
||||||
|
<DebugSymbols>true</DebugSymbols>
|
||||||
|
<DebugType>full</DebugType>
|
||||||
|
<Optimize>false</Optimize>
|
||||||
|
<OutputPath>bin\Debug\</OutputPath>
|
||||||
|
<DefineConstants>DEBUG;TRACE</DefineConstants>
|
||||||
|
<ErrorReport>prompt</ErrorReport>
|
||||||
|
<WarningLevel>4</WarningLevel>
|
||||||
|
</PropertyGroup>
|
||||||
|
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
|
||||||
|
<PlatformTarget>AnyCPU</PlatformTarget>
|
||||||
|
<DebugType>pdbonly</DebugType>
|
||||||
|
<Optimize>true</Optimize>
|
||||||
|
<OutputPath>bin\Release\</OutputPath>
|
||||||
|
<DefineConstants>TRACE</DefineConstants>
|
||||||
|
<ErrorReport>prompt</ErrorReport>
|
||||||
|
<WarningLevel>4</WarningLevel>
|
||||||
|
</PropertyGroup>
|
||||||
|
<PropertyGroup>
|
||||||
|
<SignManifests>false</SignManifests>
|
||||||
|
</PropertyGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<Reference Include="Microsoft.Bcl.AsyncInterfaces, Version=7.0.0.0, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51, processorArchitecture=MSIL">
|
||||||
|
<HintPath>packages\Microsoft.Bcl.AsyncInterfaces.7.0.0\lib\net462\Microsoft.Bcl.AsyncInterfaces.dll</HintPath>
|
||||||
|
<Private>False</Private>
|
||||||
|
</Reference>
|
||||||
|
<Reference Include="System" />
|
||||||
|
<Reference Include="System.Core" />
|
||||||
|
<Reference Include="System.Management" />
|
||||||
|
<Reference Include="System.Memory, Version=4.0.1.2, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51, processorArchitecture=MSIL">
|
||||||
|
<HintPath>packages\System.Memory.4.5.5\lib\net461\System.Memory.dll</HintPath>
|
||||||
|
<Private>False</Private>
|
||||||
|
</Reference>
|
||||||
|
<Reference Include="System.Runtime.CompilerServices.Unsafe, Version=6.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
|
||||||
|
<HintPath>packages\System.Runtime.CompilerServices.Unsafe.6.0.0\lib\net461\System.Runtime.CompilerServices.Unsafe.dll</HintPath>
|
||||||
|
<Private>False</Private>
|
||||||
|
</Reference>
|
||||||
|
<Reference Include="System.Data.DataSetExtensions" />
|
||||||
|
<Reference Include="Microsoft.CSharp" />
|
||||||
|
<Reference Include="System.Data" />
|
||||||
|
<Reference Include="System.Net.Http" />
|
||||||
|
</ItemGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<Compile Include="AesCrypt.cs" />
|
||||||
|
<Compile Include="FanucProgram.cs" />
|
||||||
|
<Compile Include="InputArgs.cs" />
|
||||||
|
<Compile Include="FANUCMachine.cs" />
|
||||||
|
<Compile Include="fwlib32.cs" />
|
||||||
|
<Compile Include="Program.cs" />
|
||||||
|
<Compile Include="Properties\AssemblyInfo.cs" />
|
||||||
|
<Compile Include="Cnc\ICncMachine.cs" />
|
||||||
|
<Compile Include="Cnc\IFanucMachine.cs" />
|
||||||
|
<Compile Include="Cnc\Fanuc\FanucCapabilities.cs" />
|
||||||
|
<Compile Include="Cnc\Fanuc\FanucConnectionConfig.cs" />
|
||||||
|
<Compile Include="Cnc\Fanuc\FanucDialect.cs" />
|
||||||
|
<Compile Include="Cnc\Fanuc\FanucDialectDetector.cs" />
|
||||||
|
<Compile Include="Cnc\Fanuc\FanucErrorTranslator.cs" />
|
||||||
|
<Compile Include="Cnc\Fanuc\FanucMachine.cs" />
|
||||||
|
<Compile Include="Cnc\Fanuc\FocasDialectFactory.cs" />
|
||||||
|
<Compile Include="Cnc\Fanuc\IFocasDialect.cs" />
|
||||||
|
<Compile Include="Cnc\Fanuc\IsoProgramFormatter.cs" />
|
||||||
|
<Compile Include="Cnc\Fanuc\LegacyFocasDialect.cs" />
|
||||||
|
<Compile Include="Cnc\Fanuc\ModernFocasDialect.cs" />
|
||||||
|
<Compile Include="Cnc\Models\CncError.cs" />
|
||||||
|
<Compile Include="Cnc\Models\CncManufacturer.cs" />
|
||||||
|
<Compile Include="Cnc\Models\CncProgram.cs" />
|
||||||
|
<Compile Include="Cnc\Models\CncResult.cs" />
|
||||||
|
<Compile Include="Cnc\Models\ConnectionState.cs" />
|
||||||
|
<Compile Include="Cnc\Models\MachineMode.cs" />
|
||||||
|
<Compile Include="Cnc\Models\MachineSnapshot.cs" />
|
||||||
|
<Compile Include="Cnc\Models\MotionState.cs" />
|
||||||
|
<Compile Include="Cnc\Models\RunState.cs" />
|
||||||
|
<Compile Include="Cnc\Models\ValidationSeverity.cs" />
|
||||||
|
<Compile Include="Cnc\Models\ValidationError.cs" />
|
||||||
|
<Compile Include="Cnc\Models\ValidationResult.cs" />
|
||||||
|
<Compile Include="Cnc\INcProgramValidator.cs" />
|
||||||
|
<Compile Include="Cnc\NcProgramValidator.cs" />
|
||||||
|
<Compile Include="Cnc\CncMachineFactory.cs" />
|
||||||
|
<Compile Include="Cnc\Heidenhain\HeidenhainConnectionConfig.cs" />
|
||||||
|
<Compile Include="Cnc\Heidenhain\ILsv2Client.cs" />
|
||||||
|
<Compile Include="Cnc\Heidenhain\Lsv2Client.cs" />
|
||||||
|
<Compile Include="Cnc\Heidenhain\HeidenhainMachine.cs" />
|
||||||
|
<Compile Include="Cnc\Siemens\SiemensConnectionConfig.cs" />
|
||||||
|
<Compile Include="Cnc\Siemens\ISiemensFtpClient.cs" />
|
||||||
|
<Compile Include="Cnc\Siemens\SiemensFtpClient.cs" />
|
||||||
|
<Compile Include="Cnc\Siemens\SiemensMachine.cs" />
|
||||||
|
<Compile Include="Cnc\Mitsubishi\MitsubishiConnectionConfig.cs" />
|
||||||
|
<Compile Include="Cnc\Mitsubishi\IMitsubishiFtpClient.cs" />
|
||||||
|
<Compile Include="Cnc\Mitsubishi\MitsubishiFtpClient.cs" />
|
||||||
|
<Compile Include="Cnc\Mitsubishi\MitsubishiMachine.cs" />
|
||||||
|
</ItemGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<None Include="App.config" />
|
||||||
|
<None Include="packages.config" />
|
||||||
|
</ItemGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<Content Include="Cnc\Fanuc\libs\Fwlib32.dll">
|
||||||
|
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||||
|
</Content>
|
||||||
|
<Content Include="Cnc\Fanuc\libs\fwlibe1.dll">
|
||||||
|
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||||
|
</Content>
|
||||||
|
<Content Include="Cnc\Fanuc\libs\fwlibNCG.dll">
|
||||||
|
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||||
|
</Content>
|
||||||
|
</ItemGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<BootstrapperPackage Include=".NETFramework,Version=v4.7.2">
|
||||||
|
<Visible>False</Visible>
|
||||||
|
<ProductName>Microsoft .NET Framework 4.7.2 %28x86 e x64%29</ProductName>
|
||||||
|
<Install>true</Install>
|
||||||
|
</BootstrapperPackage>
|
||||||
|
<BootstrapperPackage Include="Microsoft.Net.Framework.3.5.SP1">
|
||||||
|
<Visible>False</Visible>
|
||||||
|
<ProductName>.NET Framework 3.5 SP1</ProductName>
|
||||||
|
<Install>false</Install>
|
||||||
|
</BootstrapperPackage>
|
||||||
|
</ItemGroup>
|
||||||
|
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
|
||||||
|
</Project>
|
||||||
20
FanucProgramManager.csproj.user
Executable file
20
FanucProgramManager.csproj.user
Executable file
|
|
@ -0,0 +1,20 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<Project ToolsVersion="Current" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||||
|
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Debug|AnyCPU'">
|
||||||
|
<StartArguments>-chiave=YQXnxVoz6MjuH18d9/XDHzfTyxd/prRciP3C8bwMYb1vv3oUDjR250JKBh1X84f9 -azione=invia -pathprogramma=c:\temp\FanucProgrammiSalvati\M0108.txt -nodo=9 -tipo=2 -tooloffset -workzero -debug</StartArguments>
|
||||||
|
</PropertyGroup>
|
||||||
|
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Release|AnyCPU'">
|
||||||
|
<StartArguments>-chiave=YQXnxVoz6MjuH18d9/XDHzfTyxd/prRciP3C8bwMYb1vv3oUDjR250JKBh1X84f9 -azione=invia -tipo=2 -nodo=9 -porta=8193 -path=2 -pathprogramma\temp\FanucProgrammiSalvati\programma.txt -debug</StartArguments>
|
||||||
|
</PropertyGroup>
|
||||||
|
<PropertyGroup>
|
||||||
|
<PublishUrlHistory>publish\</PublishUrlHistory>
|
||||||
|
<InstallUrlHistory />
|
||||||
|
<SupportUrlHistory />
|
||||||
|
<UpdateUrlHistory />
|
||||||
|
<BootstrapperUrlHistory />
|
||||||
|
<ErrorReportUrlHistory />
|
||||||
|
<FallbackCulture>it-IT</FallbackCulture>
|
||||||
|
<VerifyUploadedFiles>false</VerifyUploadedFiles>
|
||||||
|
<ProjectView>ShowAllFiles</ProjectView>
|
||||||
|
</PropertyGroup>
|
||||||
|
</Project>
|
||||||
31
FanucProgramManager.sln
Executable file
31
FanucProgramManager.sln
Executable file
|
|
@ -0,0 +1,31 @@
|
||||||
|
|
||||||
|
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||||
|
# Visual Studio Version 17
|
||||||
|
VisualStudioVersion = 17.5.33414.496
|
||||||
|
MinimumVisualStudioVersion = 10.0.40219.1
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FanucProgramManager", "FanucProgramManager.csproj", "{1032188B-3321-42B2-87ED-343BF718CAF5}"
|
||||||
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FanucProgramManager.Tests", "FanucProgramManager.Tests\FanucProgramManager.Tests.csproj", "{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}"
|
||||||
|
EndProject
|
||||||
|
Global
|
||||||
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
|
Debug|Any CPU = Debug|Any CPU
|
||||||
|
Release|Any CPU = Release|Any CPU
|
||||||
|
EndGlobalSection
|
||||||
|
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||||
|
{1032188B-3321-42B2-87ED-343BF718CAF5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{1032188B-3321-42B2-87ED-343BF718CAF5}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{1032188B-3321-42B2-87ED-343BF718CAF5}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{1032188B-3321-42B2-87ED-343BF718CAF5}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|Any CPU.ActiveCfg = Debug|AnyCPU
|
||||||
|
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|Any CPU.Build.0 = Debug|AnyCPU
|
||||||
|
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|Any CPU.ActiveCfg = Release|AnyCPU
|
||||||
|
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|Any CPU.Build.0 = Release|AnyCPU
|
||||||
|
EndGlobalSection
|
||||||
|
GlobalSection(SolutionProperties) = preSolution
|
||||||
|
HideSolutionNode = FALSE
|
||||||
|
EndGlobalSection
|
||||||
|
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||||
|
SolutionGuid = {79CE0E5A-A8EA-4EAD-B78D-EE8E0E9BADAB}
|
||||||
|
EndGlobalSection
|
||||||
|
EndGlobal
|
||||||
25
InputArgs.cs
Executable file
25
InputArgs.cs
Executable file
|
|
@ -0,0 +1,25 @@
|
||||||
|
using FanucProgramManager.Cnc.Models;
|
||||||
|
|
||||||
|
namespace FanucProgramManager
|
||||||
|
{
|
||||||
|
internal class InputArgs
|
||||||
|
{
|
||||||
|
public CncManufacturer manufacturer = CncManufacturer.Fanuc;
|
||||||
|
public int tipo = 0; // kept for legacy parsing only
|
||||||
|
public string azione = "";
|
||||||
|
public string ip = "";
|
||||||
|
public string porta = "";
|
||||||
|
public short pathCNC = 1;
|
||||||
|
public int nodoHssb = -1;
|
||||||
|
public string chiave = "";
|
||||||
|
public string username = "";
|
||||||
|
public string password = "";
|
||||||
|
public string commentoProgramma = "";
|
||||||
|
public string pathLocaleProgramma = "";
|
||||||
|
public bool compatibilityMode = false;
|
||||||
|
public bool hasToolOffsetData = false;
|
||||||
|
public bool hasWorkZeroOffsetData = false;
|
||||||
|
public bool mostraHelp = false;
|
||||||
|
public bool debugMode = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
447
Program.cs
Executable file
447
Program.cs
Executable file
|
|
@ -0,0 +1,447 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Management;
|
||||||
|
using System.Security.Cryptography;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
using FanucProgramManager.Cnc;
|
||||||
|
using FanucProgramManager.Cnc.Models;
|
||||||
|
|
||||||
|
namespace FanucProgramManager
|
||||||
|
{
|
||||||
|
internal class Program
|
||||||
|
{
|
||||||
|
private static InputArgs inputArgs = new InputArgs();
|
||||||
|
|
||||||
|
public static int Main(string[] args)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
parseArgomenti(args);
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
Console.Write("Gestore Programmi FANUC: ");
|
||||||
|
Console.WriteLine(e.Message);
|
||||||
|
Console.WriteLine("Errore parametri input. Consulta `-help' per maggiori informazioni.");
|
||||||
|
vediErrore(-101);
|
||||||
|
return -101;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (inputArgs.mostraHelp)
|
||||||
|
{
|
||||||
|
MostraAiuto();
|
||||||
|
vediErrore(0);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!checkLicense(inputArgs.chiave))
|
||||||
|
{
|
||||||
|
Console.WriteLine("Licenza ERRATA");
|
||||||
|
vediErrore(-100);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate required params
|
||||||
|
bool needsIp = inputArgs.manufacturer != CncManufacturer.Fanuc || inputArgs.nodoHssb == -1;
|
||||||
|
if (inputArgs.azione == "" || (needsIp && string.IsNullOrEmpty(inputArgs.ip)) || inputArgs.pathLocaleProgramma == "")
|
||||||
|
{
|
||||||
|
vediErrore(-102);
|
||||||
|
return -102;
|
||||||
|
}
|
||||||
|
|
||||||
|
Console.WriteLine(
|
||||||
|
"manufacturer: " + inputArgs.manufacturer + "\n" +
|
||||||
|
"IndirizzoIP: " + inputArgs.ip + "\n" +
|
||||||
|
"Porta: " + inputArgs.porta + "\n" +
|
||||||
|
"Compatibility: " + inputArgs.compatibilityMode + "\n" +
|
||||||
|
"commentoProgramma: " + inputArgs.commentoProgramma + "\n" +
|
||||||
|
"pathCNC: " + inputArgs.pathCNC + "\n" +
|
||||||
|
"pathLocaleProgramma: " + inputArgs.pathLocaleProgramma + "\n"
|
||||||
|
);
|
||||||
|
|
||||||
|
ICncMachine machine = null;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
machine = CncMachineFactory.Create(inputArgs);
|
||||||
|
|
||||||
|
switch (inputArgs.azione.ToUpperInvariant())
|
||||||
|
{
|
||||||
|
case "SCARICA": return Scarica(machine);
|
||||||
|
case "INVIA": return Invia(machine);
|
||||||
|
default:
|
||||||
|
vediErrore(-103);
|
||||||
|
return -103;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (machine != null)
|
||||||
|
machine.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static int Scarica(ICncMachine macchina)
|
||||||
|
{
|
||||||
|
Console.WriteLine("RICEVI DA CNC-->");
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var connResult = macchina.ConnectAsync().GetAwaiter().GetResult();
|
||||||
|
if (!connResult.Success)
|
||||||
|
{
|
||||||
|
Console.WriteLine("Macchina non connessa");
|
||||||
|
vediErrore(-201);
|
||||||
|
return -201;
|
||||||
|
}
|
||||||
|
|
||||||
|
Console.WriteLine("Inizio RICEZIONE NCProgram");
|
||||||
|
var progResult = macchina.ReadProgramAsync(inputArgs.pathCNC.ToString()).GetAwaiter().GetResult();
|
||||||
|
Console.WriteLine("Fine RICEZIONE NCProgram");
|
||||||
|
if (!progResult.Success || string.IsNullOrEmpty(progResult.Value))
|
||||||
|
{
|
||||||
|
macchina.DisconnectAsync().GetAwaiter().GetResult();
|
||||||
|
vediErrore(-204);
|
||||||
|
return -204;
|
||||||
|
}
|
||||||
|
|
||||||
|
FanucProgram programma = new FanucProgram(progResult.Value);
|
||||||
|
|
||||||
|
IFanucMachine fanuc = macchina as IFanucMachine;
|
||||||
|
|
||||||
|
if (inputArgs.hasToolOffsetData)
|
||||||
|
{
|
||||||
|
if (fanuc == null)
|
||||||
|
{
|
||||||
|
Console.WriteLine("ToolOffset non supportato per questo produttore");
|
||||||
|
macchina.DisconnectAsync().GetAwaiter().GetResult();
|
||||||
|
vediErrore(-205);
|
||||||
|
return -205;
|
||||||
|
}
|
||||||
|
Console.WriteLine("Inizio RICEZIONE ToolOffsetData");
|
||||||
|
var toolResult = fanuc.ReadToolDataAsync(inputArgs.pathCNC.ToString()).GetAwaiter().GetResult();
|
||||||
|
Console.WriteLine("Fine RICEZIONE ToolOffsetData");
|
||||||
|
if (!toolResult.Success || string.IsNullOrEmpty(toolResult.Value))
|
||||||
|
{
|
||||||
|
macchina.DisconnectAsync().GetAwaiter().GetResult();
|
||||||
|
vediErrore(-205);
|
||||||
|
return -205;
|
||||||
|
}
|
||||||
|
programma.setValue(toolResult.Value, FanucProgram.FILE_TYPE.TOOL_OFFEST_DATA);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (inputArgs.hasWorkZeroOffsetData)
|
||||||
|
{
|
||||||
|
if (fanuc == null)
|
||||||
|
{
|
||||||
|
Console.WriteLine("WorkZeroOffset non supportato per questo produttore");
|
||||||
|
macchina.DisconnectAsync().GetAwaiter().GetResult();
|
||||||
|
vediErrore(-206);
|
||||||
|
return -206;
|
||||||
|
}
|
||||||
|
Console.WriteLine("Inizio RICEZIONE WorkZeroOffsetData");
|
||||||
|
var workResult = fanuc.ReadWorkZeroDataAsync(inputArgs.pathCNC.ToString()).GetAwaiter().GetResult();
|
||||||
|
Console.WriteLine("Fine RICEZIONE WorkZeroOffsetData");
|
||||||
|
if (!workResult.Success || string.IsNullOrEmpty(workResult.Value))
|
||||||
|
{
|
||||||
|
macchina.DisconnectAsync().GetAwaiter().GetResult();
|
||||||
|
vediErrore(-206);
|
||||||
|
return -206;
|
||||||
|
}
|
||||||
|
programma.setValue(workResult.Value, FanucProgram.FILE_TYPE.WORK_ZERO_OFFSET);
|
||||||
|
}
|
||||||
|
|
||||||
|
macchina.DisconnectAsync().GetAwaiter().GetResult();
|
||||||
|
File.WriteAllText(inputArgs.pathLocaleProgramma, programma.ToString());
|
||||||
|
vediErrore(0);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Console.WriteLine(ex.Message);
|
||||||
|
vediErrore(-202);
|
||||||
|
return -202;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static int Invia(ICncMachine macchina)
|
||||||
|
{
|
||||||
|
Console.WriteLine("INVIA A CNC-->");
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var connResult = macchina.ConnectAsync().GetAwaiter().GetResult();
|
||||||
|
if (!connResult.Success)
|
||||||
|
{
|
||||||
|
Console.WriteLine("Macchina non connessa");
|
||||||
|
vediErrore(-201);
|
||||||
|
return -201;
|
||||||
|
}
|
||||||
|
|
||||||
|
FanucProgram programma = new FanucProgram(File.ReadAllText(inputArgs.pathLocaleProgramma));
|
||||||
|
if (!string.IsNullOrEmpty(inputArgs.commentoProgramma))
|
||||||
|
programma.ChangeComment(inputArgs.commentoProgramma);
|
||||||
|
|
||||||
|
if (programma.Program != null)
|
||||||
|
{
|
||||||
|
Console.WriteLine("Inizio INVIO NCProgram");
|
||||||
|
var cncProg = new CncProgram
|
||||||
|
{
|
||||||
|
Name = programma.GetProgramTitle(),
|
||||||
|
Comment = programma.GetProgramComment(),
|
||||||
|
Content = programma.Program,
|
||||||
|
Path = inputArgs.pathCNC.ToString()
|
||||||
|
};
|
||||||
|
var writeResult = macchina.WriteProgramAsync(inputArgs.pathCNC.ToString(), cncProg).GetAwaiter().GetResult();
|
||||||
|
Console.WriteLine("Fine INVIO NCProgram");
|
||||||
|
if (!writeResult.Success)
|
||||||
|
{
|
||||||
|
macchina.DisconnectAsync().GetAwaiter().GetResult();
|
||||||
|
vediErrore(-204);
|
||||||
|
return -204;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
IFanucMachine fanuc = macchina as IFanucMachine;
|
||||||
|
|
||||||
|
if (programma.ToolOffsetData != null && inputArgs.hasToolOffsetData)
|
||||||
|
{
|
||||||
|
if (fanuc == null)
|
||||||
|
{
|
||||||
|
Console.WriteLine("ToolOffset non supportato per questo produttore");
|
||||||
|
macchina.DisconnectAsync().GetAwaiter().GetResult();
|
||||||
|
vediErrore(-205);
|
||||||
|
return -205;
|
||||||
|
}
|
||||||
|
Console.WriteLine("Inizio INVIO ToolOffsetData");
|
||||||
|
var toolResult = fanuc.WriteToolDataAsync(inputArgs.pathCNC.ToString(), programma.ToolOffsetData).GetAwaiter().GetResult();
|
||||||
|
Console.WriteLine("Fine INVIO ToolOffsetData");
|
||||||
|
if (!toolResult.Success)
|
||||||
|
{
|
||||||
|
macchina.DisconnectAsync().GetAwaiter().GetResult();
|
||||||
|
vediErrore(-205);
|
||||||
|
return -205;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (programma.WorkZeroOffsetData != null && inputArgs.hasWorkZeroOffsetData)
|
||||||
|
{
|
||||||
|
if (fanuc == null)
|
||||||
|
{
|
||||||
|
Console.WriteLine("WorkZeroOffset non supportato per questo produttore");
|
||||||
|
macchina.DisconnectAsync().GetAwaiter().GetResult();
|
||||||
|
vediErrore(-206);
|
||||||
|
return -206;
|
||||||
|
}
|
||||||
|
Console.WriteLine("Inizio INVIO WorkZeroOffsetData");
|
||||||
|
var workResult = fanuc.WriteWorkZeroDataAsync(inputArgs.pathCNC.ToString(), programma.WorkZeroOffsetData).GetAwaiter().GetResult();
|
||||||
|
Console.WriteLine("Fine INVIO WorkZeroOffsetData");
|
||||||
|
if (!workResult.Success)
|
||||||
|
{
|
||||||
|
macchina.DisconnectAsync().GetAwaiter().GetResult();
|
||||||
|
vediErrore(-206);
|
||||||
|
return -206;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
macchina.DisconnectAsync().GetAwaiter().GetResult();
|
||||||
|
vediErrore(0);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Console.WriteLine(ex.Message);
|
||||||
|
vediErrore(-202);
|
||||||
|
return -202;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void vediErrore(int errore)
|
||||||
|
{
|
||||||
|
Console.WriteLine("Errore:" + errore);
|
||||||
|
Console.WriteLine();
|
||||||
|
Console.WriteLine("Codici Errori:");
|
||||||
|
Console.WriteLine("-100 : Licenza Errata/non valida.");
|
||||||
|
Console.WriteLine("-101 : Impossibile parsare i parametri.");
|
||||||
|
Console.WriteLine("-102 : Parametri mancanti. ");
|
||||||
|
Console.WriteLine("-103 : Codice connessione errato: 3->Ethernet 2->HSSB. ");
|
||||||
|
Console.WriteLine("-201 : Macchina non connessa. ");
|
||||||
|
Console.WriteLine("-202 : Eccezione nell'esecuzione dell'AZIONE. ");
|
||||||
|
Console.WriteLine("-203 : L'azione è fallita. ");
|
||||||
|
Console.WriteLine("-204 : Errore in fase invio/ricezione Programma CN. ");
|
||||||
|
Console.WriteLine("-205 : Errore in fase invio/ricezione tool offset data ");
|
||||||
|
Console.WriteLine("-206 : Errore in fase invio/ricezione work zero offest data");
|
||||||
|
Console.WriteLine("-207 : Posizionare il CN in MDI o EDIT per eseguire l'invio di un programma");
|
||||||
|
Console.WriteLine("-208 : Programma CN Caricato ma non settato a main.");
|
||||||
|
if (inputArgs.debugMode)
|
||||||
|
Console.ReadLine();
|
||||||
|
}
|
||||||
|
|
||||||
|
static Boolean checkLicense(string chiave)
|
||||||
|
{
|
||||||
|
string pcInfo = string.Empty;
|
||||||
|
ManagementClass mc = new ManagementClass("win32_processor");
|
||||||
|
ManagementObjectCollection moc = mc.GetInstances();
|
||||||
|
|
||||||
|
foreach (ManagementObject mo in moc)
|
||||||
|
{
|
||||||
|
//Get only the first CPU's ID
|
||||||
|
pcInfo = mo.Properties["processorID"].Value.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
ManagementObjectSearcher baseboardSearcher = new ManagementObjectSearcher("root\\CIMV2", "SELECT * FROM Win32_BaseBoard");
|
||||||
|
foreach (ManagementObject queryObj in baseboardSearcher.Get())
|
||||||
|
{
|
||||||
|
//Get motherboardSerialNumber
|
||||||
|
pcInfo += queryObj["SerialNumber"].ToString();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
using (Aes myAes = Aes.Create())
|
||||||
|
{
|
||||||
|
if (chiave == "")
|
||||||
|
return false;
|
||||||
|
myAes.IV = Convert.FromBase64String("qqxNM8Li99Cwx61VX/QoWA==");
|
||||||
|
myAes.Key = Convert.FromBase64String("7/12mP3dFLGqPGjujlwBCVblURxBaBcGGQ2c2FatfP4=");
|
||||||
|
byte[] encrypted = AesCrypt.EncryptStringToBytes_Aes(pcInfo, myAes.Key, myAes.IV);
|
||||||
|
string roundtrip = AesCrypt.DecryptStringFromBytes_Aes(encrypted, myAes.Key, myAes.IV);
|
||||||
|
string decoded = "";
|
||||||
|
try
|
||||||
|
{
|
||||||
|
decoded = AesCrypt.DecryptStringFromBytes_Aes(Convert.FromBase64String(chiave), myAes.Key, myAes.IV);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Console.WriteLine(ex.ToString());
|
||||||
|
}
|
||||||
|
return pcInfo == decoded;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void MostraAiuto()
|
||||||
|
{
|
||||||
|
Console.WriteLine("Utilizzo del Software gestore Programmi FANUC");
|
||||||
|
Console.WriteLine("Le funzioni pricipali sono due.");
|
||||||
|
Console.WriteLine("INVIA: caricamento di programma su CNC.");
|
||||||
|
Console.WriteLine("SCARICA: scaricamento di programma su CNC.");
|
||||||
|
Console.WriteLine();
|
||||||
|
Console.WriteLine();
|
||||||
|
Console.WriteLine("Lista dei parametri da fornire all'eseguibile\nDevono essere separati da uno spazio tra l'uno e l'altro,\nnon andare a capo e non lasciare spazi dopo l'= ");
|
||||||
|
Console.WriteLine();
|
||||||
|
Console.WriteLine("-chiave=");
|
||||||
|
Console.WriteLine(" la stringa chiave per eseguire il software.");
|
||||||
|
Console.WriteLine("-azione=");
|
||||||
|
Console.WriteLine(" l'{AZIONE} da eseguire.");
|
||||||
|
Console.WriteLine("-manufacturer=");
|
||||||
|
Console.WriteLine(" Il produttore del CNC: fanuc (default), heidenhain, siemens, mitsubishi");
|
||||||
|
Console.WriteLine("-tipo=");
|
||||||
|
Console.WriteLine(" Il tipo di connessione utilizzato dalla macchina: 3 per ethernet\n 2 per HSSB (solo Fanuc, legacy)");
|
||||||
|
Console.WriteLine("-ip=");
|
||||||
|
Console.WriteLine(" Dato per Ethernet indirizzo IP della macchina");
|
||||||
|
Console.WriteLine("-porta=");
|
||||||
|
Console.WriteLine(" Dato per Ethernet PORTA di connessione");
|
||||||
|
Console.WriteLine("-nodo=");
|
||||||
|
Console.WriteLine(" Dato per HSSB Nodo di connesione (solo Fanuc)");
|
||||||
|
Console.WriteLine("-username=");
|
||||||
|
Console.WriteLine(" Username per autenticazione (Heidenhain/Siemens)");
|
||||||
|
Console.WriteLine("-password=");
|
||||||
|
Console.WriteLine(" Password per autenticazione (Heidenhain/Siemens)");
|
||||||
|
Console.WriteLine("-compatibility");
|
||||||
|
Console.WriteLine(" Attivazione modalità compatibilità");
|
||||||
|
Console.WriteLine("-commento=");
|
||||||
|
Console.WriteLine(" Commento da applicare al programma scelto");
|
||||||
|
Console.WriteLine("-path=");
|
||||||
|
Console.WriteLine(" Path dei programmi del CNC dove vengono salvati i programmi, se non definito è impostato a 1 cioè //CNC_MEM/USER/PATH1/");
|
||||||
|
Console.WriteLine("-pathprogramma=");
|
||||||
|
Console.WriteLine(" Necessario per INVIO: Path del programma locale da caricare sul CNC");
|
||||||
|
Console.WriteLine("-workzero=");
|
||||||
|
Console.WriteLine(" Invia/Scarica anche i dati relativi al workzero offset");
|
||||||
|
Console.WriteLine("-tooloffset=");
|
||||||
|
Console.WriteLine(" Invia/Scarica anche i dati relativi al tool offset");
|
||||||
|
Console.WriteLine("-debug=");
|
||||||
|
Console.WriteLine(" Mantiene il terminale aperto alla fine delle operazioni");
|
||||||
|
Console.WriteLine("-help");
|
||||||
|
Console.WriteLine(" Mostra questo messaggio e termina");
|
||||||
|
Console.WriteLine();
|
||||||
|
Console.WriteLine("Codici Errori:");
|
||||||
|
Console.WriteLine("-100 : Licenza Errata/non valida.");
|
||||||
|
Console.WriteLine("-101 : Impossibile parsare i parametri.");
|
||||||
|
Console.WriteLine("-102 : Parametri mancanti. ");
|
||||||
|
Console.WriteLine("-103 : Codice connessione errato: 3->Ethernet 2->HSSB. ");
|
||||||
|
Console.WriteLine("-201 : Macchina non connessa. ");
|
||||||
|
Console.WriteLine("-202 : Eccezione nell'esecuzione dell'AZIONE. ");
|
||||||
|
Console.WriteLine("-203 : L'azione è fallita. ");
|
||||||
|
Console.WriteLine("-204 : Errore in fase invio/ricezione Programma CN. ");
|
||||||
|
Console.WriteLine("-205 : Errore in fase invio/ricezione tool offset data ");
|
||||||
|
Console.WriteLine("-206 : Errore in fase invio/ricezione work zero offest data");
|
||||||
|
Console.WriteLine("-207 : Posizionare il CN in MDI o EDIT per eseguire l'invio di un programma");
|
||||||
|
Console.WriteLine("-208 : Programma CN Caricato ma non settato a main.");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void parseArgomenti(string[] args)
|
||||||
|
{
|
||||||
|
List<string> arguments = new List<string>(args);
|
||||||
|
var help = arguments.Find(it => new Regex("-help.*", RegexOptions.IgnoreCase).IsMatch(it));
|
||||||
|
if (!string.IsNullOrEmpty(help))
|
||||||
|
{
|
||||||
|
inputArgs.mostraHelp = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var chiave = arguments.Find(it => new Regex("-chiave=.*", RegexOptions.IgnoreCase).IsMatch(it));
|
||||||
|
inputArgs.chiave = chiave.Substring(8);
|
||||||
|
var azione = arguments.Find(it => new Regex("-azione=.*", RegexOptions.IgnoreCase).IsMatch(it));
|
||||||
|
inputArgs.azione = azione.Substring(8);
|
||||||
|
var manufacturer = arguments.Find(it => new Regex("-manufacturer=.*", RegexOptions.IgnoreCase).IsMatch(it));
|
||||||
|
if (!string.IsNullOrEmpty(manufacturer))
|
||||||
|
{
|
||||||
|
string mfgVal = manufacturer.Substring(14).Trim().ToLowerInvariant();
|
||||||
|
switch (mfgVal)
|
||||||
|
{
|
||||||
|
case "heidenhain": inputArgs.manufacturer = CncManufacturer.Heidenhain; break;
|
||||||
|
case "siemens": inputArgs.manufacturer = CncManufacturer.Siemens; break;
|
||||||
|
case "mitsubishi": inputArgs.manufacturer = CncManufacturer.Mitsubishi; break;
|
||||||
|
default: inputArgs.manufacturer = CncManufacturer.Fanuc; break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
string tipo = arguments.Find(it => new Regex("-tipo=.*", RegexOptions.IgnoreCase).IsMatch(it));
|
||||||
|
if (!string.IsNullOrEmpty(tipo))
|
||||||
|
inputArgs.tipo = Int32.Parse(tipo.Substring(6));
|
||||||
|
else inputArgs.tipo = 3;
|
||||||
|
var ip = arguments.Find(it => new Regex("-ip=.*", RegexOptions.IgnoreCase).IsMatch(it));
|
||||||
|
if (!string.IsNullOrEmpty(ip))
|
||||||
|
inputArgs.ip = ip.Substring(4);
|
||||||
|
var porta = arguments.Find(it => new Regex("-porta=.*", RegexOptions.IgnoreCase).IsMatch(it));
|
||||||
|
if (!string.IsNullOrEmpty(porta))
|
||||||
|
inputArgs.porta = porta.Substring(7);
|
||||||
|
var nodo = arguments.Find(it => new Regex("-nodo=.*", RegexOptions.IgnoreCase).IsMatch(it));
|
||||||
|
if (!string.IsNullOrEmpty(nodo))
|
||||||
|
inputArgs.nodoHssb = Int32.Parse(nodo.Substring(6));
|
||||||
|
else inputArgs.nodoHssb = -1;
|
||||||
|
var username = arguments.Find(it => new Regex("-username=.*", RegexOptions.IgnoreCase).IsMatch(it));
|
||||||
|
if (!string.IsNullOrEmpty(username))
|
||||||
|
inputArgs.username = username.Substring(10);
|
||||||
|
var password = arguments.Find(it => new Regex("-password=.*", RegexOptions.IgnoreCase).IsMatch(it));
|
||||||
|
if (!string.IsNullOrEmpty(password))
|
||||||
|
inputArgs.password = password.Substring(10);
|
||||||
|
var compatibility = arguments.Find(it => new Regex("-compatibility.*", RegexOptions.IgnoreCase).IsMatch(it));
|
||||||
|
if (!string.IsNullOrEmpty(compatibility))
|
||||||
|
inputArgs.compatibilityMode = true;
|
||||||
|
var commento = arguments.Find(it => new Regex("-commento=.*", RegexOptions.IgnoreCase).IsMatch(it));
|
||||||
|
if (!string.IsNullOrEmpty(commento))
|
||||||
|
inputArgs.commentoProgramma = commento.Substring(10);
|
||||||
|
var path = arguments.Find(it => new Regex("-path=.*", RegexOptions.IgnoreCase).IsMatch(it));
|
||||||
|
if (!string.IsNullOrEmpty(path))
|
||||||
|
inputArgs.pathCNC = Int16.Parse(path.Substring(6));
|
||||||
|
var pathprogramma = arguments.Find(it => new Regex("-pathprogramma=.*", RegexOptions.IgnoreCase).IsMatch(it));
|
||||||
|
if (!string.IsNullOrEmpty(pathprogramma))
|
||||||
|
inputArgs.pathLocaleProgramma = pathprogramma.Substring(15);
|
||||||
|
var hasToolOffestData = arguments.Find(it => new Regex("-tooloffset.*", RegexOptions.IgnoreCase).IsMatch(it));
|
||||||
|
if (!string.IsNullOrEmpty(hasToolOffestData))
|
||||||
|
inputArgs.hasToolOffsetData = true;
|
||||||
|
var hasWorkZeroOffsetData = arguments.Find(it => new Regex("-workzero.*", RegexOptions.IgnoreCase).IsMatch(it));
|
||||||
|
if (!string.IsNullOrEmpty(hasWorkZeroOffsetData))
|
||||||
|
inputArgs.hasWorkZeroOffsetData = true;
|
||||||
|
var debugMode = arguments.Find(it => new Regex("-debug.*", RegexOptions.IgnoreCase).IsMatch(it));
|
||||||
|
if (!string.IsNullOrEmpty(debugMode))
|
||||||
|
inputArgs.debugMode = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
39
Properties/AssemblyInfo.cs
Executable file
39
Properties/AssemblyInfo.cs
Executable file
|
|
@ -0,0 +1,39 @@
|
||||||
|
using System.Reflection;
|
||||||
|
using System.Runtime.CompilerServices;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
|
||||||
|
[assembly: InternalsVisibleTo("FanucProgramManager.Tests")]
|
||||||
|
[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")]
|
||||||
|
|
||||||
|
// Le informazioni generali relative a un assembly sono controllate dal seguente
|
||||||
|
// set di attributi. Modificare i valori di questi attributi per modificare le informazioni
|
||||||
|
// associate a un assembly.
|
||||||
|
[assembly: AssemblyTitle("FanucProgramManager")]
|
||||||
|
[assembly: AssemblyDescription("")]
|
||||||
|
[assembly: AssemblyConfiguration("")]
|
||||||
|
[assembly: AssemblyCompany("")]
|
||||||
|
[assembly: AssemblyProduct("FanucProgramManager")]
|
||||||
|
[assembly: AssemblyCopyright("Copyright © 2023")]
|
||||||
|
[assembly: AssemblyTrademark("")]
|
||||||
|
[assembly: AssemblyCulture("")]
|
||||||
|
|
||||||
|
// Se si imposta ComVisible su false, i tipi in questo assembly non saranno visibili
|
||||||
|
// ai componenti COM. Se è necessario accedere a un tipo in questo assembly da
|
||||||
|
// COM, impostare su true l'attributo ComVisible per tale tipo.
|
||||||
|
[assembly: ComVisible(false)]
|
||||||
|
|
||||||
|
// Se il progetto viene esposto a COM, il GUID seguente verrà utilizzato come ID della libreria dei tipi
|
||||||
|
[assembly: Guid("1032188b-3321-42b2-87ed-343bf718caf5")]
|
||||||
|
|
||||||
|
// Le informazioni sulla versione di un assembly sono costituite dai seguenti quattro valori:
|
||||||
|
//
|
||||||
|
// Versione principale
|
||||||
|
// Versione secondaria
|
||||||
|
// Numero di build
|
||||||
|
// Revisione
|
||||||
|
//
|
||||||
|
// È possibile specificare tutti i valori oppure impostare valori predefiniti per i numeri relativi alla revisione e alla build
|
||||||
|
// usando l'asterisco '*' come illustrato di seguito:
|
||||||
|
// [assembly: AssemblyVersion("1.0.*")]
|
||||||
|
[assembly: AssemblyVersion("1.1.0.0")]
|
||||||
|
[assembly: AssemblyFileVersion("1.1.0.0")]
|
||||||
11569
fwlib32.cs
Executable file
11569
fwlib32.cs
Executable file
File diff suppressed because it is too large
Load diff
12
nunit.runsettings
Normal file
12
nunit.runsettings
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<RunSettings>
|
||||||
|
<NUnit>
|
||||||
|
<DefaultTestNamePattern>{m}</DefaultTestNamePattern>
|
||||||
|
</NUnit>
|
||||||
|
<!--
|
||||||
|
Filter examples:
|
||||||
|
Unit only: dotnet test FanucProgramManager.Tests --filter "Category!=Integration"
|
||||||
|
Integration only: dotnet test FanucProgramManager.Tests --filter "Category=Integration"
|
||||||
|
All: dotnet test FanucProgramManager.Tests
|
||||||
|
-->
|
||||||
|
</RunSettings>
|
||||||
61
packages.config
Executable file
61
packages.config
Executable file
|
|
@ -0,0 +1,61 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<packages>
|
||||||
|
<package id="Costura.Fody" version="5.7.0" targetFramework="net472" developmentDependency="true" />
|
||||||
|
<package id="Fody" version="6.6.4" targetFramework="net472" developmentDependency="true" />
|
||||||
|
<package id="Microsoft.Bcl.AsyncInterfaces" version="7.0.0" targetFramework="net472" />
|
||||||
|
<package id="Microsoft.NETCore.Platforms" version="7.0.2" targetFramework="net472" />
|
||||||
|
<package id="Microsoft.Win32.Primitives" version="4.3.0" targetFramework="net472" />
|
||||||
|
<package id="NETStandard.Library" version="2.0.3" targetFramework="net472" />
|
||||||
|
<package id="System.AppContext" version="4.3.0" targetFramework="net472" />
|
||||||
|
<package id="System.Buffers" version="4.5.1" targetFramework="net472" />
|
||||||
|
<package id="System.CodeDom" version="7.0.0" targetFramework="net472" />
|
||||||
|
<package id="System.Collections" version="4.3.0" targetFramework="net472" />
|
||||||
|
<package id="System.Collections.Concurrent" version="4.3.0" targetFramework="net472" />
|
||||||
|
<package id="System.Console" version="4.3.1" targetFramework="net472" />
|
||||||
|
<package id="System.Diagnostics.Debug" version="4.3.0" targetFramework="net472" />
|
||||||
|
<package id="System.Diagnostics.DiagnosticSource" version="7.0.2" targetFramework="net472" />
|
||||||
|
<package id="System.Diagnostics.Tools" version="4.3.0" targetFramework="net472" />
|
||||||
|
<package id="System.Diagnostics.Tracing" version="4.3.0" targetFramework="net472" />
|
||||||
|
<package id="System.Globalization" version="4.3.0" targetFramework="net472" />
|
||||||
|
<package id="System.Globalization.Calendars" version="4.3.0" targetFramework="net472" />
|
||||||
|
<package id="System.IO" version="4.3.0" targetFramework="net472" />
|
||||||
|
<package id="System.IO.Compression" version="4.3.0" targetFramework="net472" />
|
||||||
|
<package id="System.IO.Compression.ZipFile" version="4.3.0" targetFramework="net472" />
|
||||||
|
<package id="System.IO.FileSystem" version="4.3.0" targetFramework="net472" />
|
||||||
|
<package id="System.IO.FileSystem.Primitives" version="4.3.0" targetFramework="net472" />
|
||||||
|
<package id="System.Linq" version="4.3.0" targetFramework="net472" />
|
||||||
|
<package id="System.Linq.Expressions" version="4.3.0" targetFramework="net472" />
|
||||||
|
<package id="System.Management" version="7.0.1" targetFramework="net472" />
|
||||||
|
<package id="System.Memory" version="4.5.5" targetFramework="net472" />
|
||||||
|
<package id="System.Net.Http" version="4.3.4" targetFramework="net472" />
|
||||||
|
<package id="System.Net.Primitives" version="4.3.1" targetFramework="net472" />
|
||||||
|
<package id="System.Net.Sockets" version="4.3.0" targetFramework="net472" />
|
||||||
|
<package id="System.Numerics.Vectors" version="4.5.0" targetFramework="net472" />
|
||||||
|
<package id="System.ObjectModel" version="4.3.0" targetFramework="net472" />
|
||||||
|
<package id="System.Reflection" version="4.3.0" targetFramework="net472" />
|
||||||
|
<package id="System.Reflection.Extensions" version="4.3.0" targetFramework="net472" />
|
||||||
|
<package id="System.Reflection.Primitives" version="4.3.0" targetFramework="net472" />
|
||||||
|
<package id="System.Resources.ResourceManager" version="4.3.0" targetFramework="net472" />
|
||||||
|
<package id="System.Runtime" version="4.3.1" targetFramework="net472" />
|
||||||
|
<package id="System.Runtime.CompilerServices.Unsafe" version="6.0.0" targetFramework="net472" />
|
||||||
|
<package id="System.Runtime.Extensions" version="4.3.1" targetFramework="net472" />
|
||||||
|
<package id="System.Runtime.Handles" version="4.3.0" targetFramework="net472" />
|
||||||
|
<package id="System.Runtime.InteropServices" version="4.3.0" targetFramework="net472" />
|
||||||
|
<package id="System.Runtime.InteropServices.RuntimeInformation" version="4.3.0" targetFramework="net472" />
|
||||||
|
<package id="System.Runtime.Numerics" version="4.3.0" targetFramework="net472" />
|
||||||
|
<package id="System.Security.Cryptography.Algorithms" version="4.3.1" targetFramework="net472" />
|
||||||
|
<package id="System.Security.Cryptography.Encoding" version="4.3.0" targetFramework="net472" />
|
||||||
|
<package id="System.Security.Cryptography.Primitives" version="4.3.0" targetFramework="net472" />
|
||||||
|
<package id="System.Security.Cryptography.X509Certificates" version="4.3.2" targetFramework="net472" />
|
||||||
|
<package id="System.Text.Encoding" version="4.3.0" targetFramework="net472" />
|
||||||
|
<package id="System.Text.Encoding.Extensions" version="4.3.0" targetFramework="net472" />
|
||||||
|
<package id="System.Text.Encodings.Web" version="7.0.0" targetFramework="net472" />
|
||||||
|
<package id="System.Text.RegularExpressions" version="4.3.1" targetFramework="net472" />
|
||||||
|
<package id="System.Threading" version="4.3.0" targetFramework="net472" />
|
||||||
|
<package id="System.Threading.Tasks" version="4.3.0" targetFramework="net472" />
|
||||||
|
<package id="System.Threading.Tasks.Extensions" version="4.5.4" targetFramework="net472" />
|
||||||
|
<package id="System.Threading.Timer" version="4.3.0" targetFramework="net472" />
|
||||||
|
<package id="System.ValueTuple" version="4.5.0" targetFramework="net472" />
|
||||||
|
<package id="System.Xml.ReaderWriter" version="4.3.1" targetFramework="net472" />
|
||||||
|
<package id="System.Xml.XDocument" version="4.3.0" targetFramework="net472" />
|
||||||
|
</packages>
|
||||||
Loading…
Add table
Reference in a new issue