commit dee8e930ea0a6fa841d5ff9a5272aa0a1008249b Author: Livio Merola Date: Fri Jul 24 09:11:00 2026 +0200 Initial LMEMoxaTransfer application diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c37fc44 --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ +bin/ +obj/ +.vs/ +build-check/ +build-check-latest/ +*.user +*.suo +*.userosscache +*.sln.docstates +RicezioniTemp/ diff --git a/README.md b/README.md new file mode 100644 index 0000000..a718ef7 --- /dev/null +++ b/README.md @@ -0,0 +1,104 @@ +# LMEMoxaTransfer + +Solution VB.NET WinForms per inviare e ricevere programmi CNC tra PC e macchinario Okuma tramite dispositivo Moxa in modalita TCP Server. + +Prima della riorganizzazione e' stata creata una copia completa in: + +`C:\Users\livio\Documents\TrasferimentoProgrammiOkuma_backup_20260724_azienda_macchine` + +## Configurazione vista nella schermata Moxa + +- Operating mode: `TCP Server Mode` +- Local TCP port: `10001` +- Packing length: `0` +- Delimiter 1/2: `0 (Disable)` +- Force transmit: `0` +- Serial baud rate: `4800` +- Data bits: `7` +- Stop bits: `1` +- Parity: `Even` +- FIFO: `Disable` +- Flow ctrl: `None` +- Interface: `RS-232` + +Con questa configurazione il PC si collega come client TCP all'indirizzo IP del Moxa sulla porta `10001`. + +## Configurazione aziende e macchine + +All'avvio l'app legge `moxa-okuma-settings.json` dalla cartella dell'eseguibile. Il file contiene: + +- elenco aziende e macchine Okuma con IP Moxa; +- impostazioni predefinite di invio/ricezione; +- hash e salt della password amministratore. + +Le macchine SECUREX mantengono il numero originale dell'immagine, quindi sono `M4`, `M5`, `M8`, `M11`, `M12`. + +La password iniziale e' `moxa`. Se nel JSON si svuotano `AdminPasswordHash` e `AdminPasswordSalt`, al primo sblocco l'app chiede di impostare una nuova password senza richiedere la vecchia. + +## Uso + +1. Aprire `TrasferimentoProgrammiOkuma.sln` con Visual Studio. +2. Avviare il progetto `TrasferimentoProgrammiOkuma.App`. +3. Selezionare azienda e macchina dalle due tendine. +4. Per ricevere da Okuma: + - scegliere la cartella di destinazione sul PC; + - premere `Ricevi da Okuma`; + - dal controllo Okuma avviare l'uscita/invio programma. + - la ricezione viene salvata prima in una cartella temporanea dell'app; + - se nel file ricevuto e' presente un nome preceduto da `$`, l'app lo propone come nome finale nella cartella scelta; il nome e' modificabile prima del salvataggio definitivo. +5. Per inviare a Okuma: + - mettere il controllo Okuma in ricezione; + - scegliere il file programma dal PC; + - premere `Invia a Okuma`. + +I controlli di configurazione sono bloccati di default. Rimangono disponibili i bottoni operativi e le caselle dei file. Usare `Sblocca controlli` per modificarli. + +## Stato Moxa + +La scheda `Stato Moxa` mostra tutte le macchine Okuma divise per azienda. Il pallino verde indica che l'IP Moxa risponde sulla porta TCP configurata, il rosso indica che non e' raggiungibile, il grigio indica che non e' ancora stato controllato. + +Il pulsante `Aggiorna` e' sempre disponibile. Il controllo automatico usa l'intervallo `Intervallo s`, modificabile solo dopo lo sblocco con password. + +## Menu e informazioni + +I comandi generali sono nel menu `Strumenti`; la voce `? > Informazioni su` mostra azienda `Compomac SPA`, autore `Livio Merola` e versione dell'app. + +All'avvio viene mostrato uno splash screen con l'icona dell'app. + +## ClickOnce + +Profilo di pubblicazione: + +```powershell +& "C:\Program Files\Microsoft Visual Studio\2022\Community\MSBuild\Current\Bin\MSBuild.exe" .\TrasferimentoProgrammiOkuma.App\TrasferimentoProgrammiOkuma.App.vbproj /target:Publish /p:Configuration=Release /p:PublishProfile=GiteaClickOnce /p:PublishDir=..\clickonce\ +``` + +La cartella `clickonce` contiene `LMEMoxaTransfer.application` e i file necessari. L'URL di aggiornamento configurato e': + +`https://gitea.lmcloud.it/livio/LMEMoxaTransfer/raw/branch/main/clickonce/LMEMoxaTransfer.application` + +## Note pratiche + +- Se la ricezione termina troppo presto, aumentare `Fine ricez. s`. +- Se la ricezione non finisce mai, ridurre `Fine ricez. s`. +- Se Okuma richiede un fine programma esplicito, provare `Terminatore invio` con `%` oppure `% + CRLF`. +- Per la procedura Okuma READ da `CN0:`, attivare `Invio a blocchi`, lasciare `Dim. blocco byte` a `32` e `Pausa blocco ms` a `100` come punto di partenza, avviare prima READ sulla macchina, attendere che sia in ricezione, poi premere `Invia a Okuma`. Questa modalita aggiunge automaticamente `% + CRLF` e mantiene il socket aperto almeno 1 secondo dopo l'invio. Se appare ancora `5239`, provare `16` byte con `250` ms, poi `8` byte con `300` ms. +- Il trasferimento e' byte-per-byte: il programma non modifica il contenuto del file selezionato, salvo il terminatore opzionale in invio. +- Se l'invio verso Okuma genera `5238 RS232C device not ready ERROR FF`, controllare prima il lato seriale Okuma-Moxa: baud rate, data bit, parita, stop bit, flow control e segnali DSR/CTS/DTR/RTS. L'app TCP puo collegarsi al Moxa, ma non puo forzare la porta RS232C della macchina a diventare pronta. +- Per controlli Okuma che non accettano un flusso troppo rapido, lasciare attivo `Invio riga per riga`, provare `Pausa riga ms` tra `200` e `500`, attivare `Invio carattere per carattere`, provare `Pausa carattere ms` tra `10` e `30`, e tenere `Attesa dopo invio s` almeno a `2`. +- Dalla schermata Moxa attuale il flow control e' `None`. Se sul parametro Okuma e' impostato `DC CODE`, provare `XON/XOFF` sul Moxa. Se invece Okuma usa handshake hardware, provare `RTS/CTS` e controllare il cablaggio dei segnali ready. +- Se l'errore corretto e' `5239 memoria lettura per canale RS232C troppo piena`, il flusso in ingresso e' troppo rapido per il buffer Okuma. Ridurre la velocita applicativa con `Invio carattere per carattere`, aumentare le pause, oppure far funzionare correttamente il flow control `XON/XOFF` o hardware. +- Se `5239` appare subito appena si preme invio, usare `Test connessione`: apre TCP verso il Moxa senza inviare dati. Se l'allarme non compare, il problema nasce al primo byte seriale. Se compare comunque, riavviare il Moxa con `Save/Restart`, azzerare l'allarme Okuma e controllare stato/cablaggio RS232. +- Dopo modifiche Moxa premere sempre `Submit` e poi `Save/Restart`; in caso di prove fallite, spegnere/riaccendere o riavviare il Moxa per svuotare eventuali dati rimasti nel buffer. + +## Build da terminale + +```powershell +dotnet build .\TrasferimentoProgrammiOkuma.sln +``` + +## Avvio da terminale + +```powershell +dotnet run --project .\TrasferimentoProgrammiOkuma.App\TrasferimentoProgrammiOkuma.App.vbproj +``` diff --git a/TrasferimentoProgrammiOkuma.App/Assets/app-icon.ico b/TrasferimentoProgrammiOkuma.App/Assets/app-icon.ico new file mode 100644 index 0000000..82808fb Binary files /dev/null and b/TrasferimentoProgrammiOkuma.App/Assets/app-icon.ico differ diff --git a/TrasferimentoProgrammiOkuma.App/Assets/app-icon.png b/TrasferimentoProgrammiOkuma.App/Assets/app-icon.png new file mode 100644 index 0000000..2d249d7 Binary files /dev/null and b/TrasferimentoProgrammiOkuma.App/Assets/app-icon.png differ diff --git a/TrasferimentoProgrammiOkuma.App/Form1.Designer.vb b/TrasferimentoProgrammiOkuma.App/Form1.Designer.vb new file mode 100644 index 0000000..b7ef7be --- /dev/null +++ b/TrasferimentoProgrammiOkuma.App/Form1.Designer.vb @@ -0,0 +1,31 @@ + +Partial Class Form1 + Inherits System.Windows.Forms.Form + + 'Form overrides dispose to clean up the component list. + + Protected Overrides Sub Dispose(disposing As Boolean) + Try + If disposing AndAlso components IsNot Nothing Then + components.Dispose() + End If + Finally + MyBase.Dispose(disposing) + End Try + End Sub + + 'Required by the Windows Form Designer + Private components As System.ComponentModel.IContainer + + 'NOTE: The following procedure is required by the Windows Form Designer + 'It can be modified using the Windows Form Designer. + 'Do not modify it using the code editor. + + Private Sub InitializeComponent() + components = New System.ComponentModel.Container() + Me.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font + Me.ClientSize = New System.Drawing.Size(800, 450) + Me.Text = "Form1" + End Sub + +End Class diff --git a/TrasferimentoProgrammiOkuma.App/Form1.resx b/TrasferimentoProgrammiOkuma.App/Form1.resx new file mode 100644 index 0000000..1af7de1 --- /dev/null +++ b/TrasferimentoProgrammiOkuma.App/Form1.resx @@ -0,0 +1,120 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + \ No newline at end of file diff --git a/TrasferimentoProgrammiOkuma.App/Form1.vb b/TrasferimentoProgrammiOkuma.App/Form1.vb new file mode 100644 index 0000000..1085728 --- /dev/null +++ b/TrasferimentoProgrammiOkuma.App/Form1.vb @@ -0,0 +1,1220 @@ +Imports System.IO +Imports System.Net.Sockets +Imports System.Reflection +Imports System.Security.Cryptography +Imports System.Text +Imports System.Text.Json +Imports System.Threading + +Public Class Form1 + Private Const ConfigFileName As String = "moxa-okuma-settings.json" + + Private ReadOnly cboAzienda As New ComboBox() + Private ReadOnly cboMacchina As New ComboBox() + Private ReadOnly txtIpMoxa As New TextBox() + Private ReadOnly nudPorta As New NumericUpDown() + Private ReadOnly nudTimeoutConnessione As New NumericUpDown() + Private ReadOnly nudTimeoutInattivita As New NumericUpDown() + Private ReadOnly nudPausaRiga As New NumericUpDown() + Private ReadOnly nudPausaCarattere As New NumericUpDown() + Private ReadOnly nudDimensioneBlocco As New NumericUpDown() + Private ReadOnly nudPausaBlocco As New NumericUpDown() + Private ReadOnly nudAttesaPrimaInvio As New NumericUpDown() + Private ReadOnly nudAttesaDopoInvio As New NumericUpDown() + Private ReadOnly chkInvioRigaPerRiga As New CheckBox() + Private ReadOnly chkInvioCaratterePerCarattere As New CheckBox() + Private ReadOnly chkInvioBlocchi As New CheckBox() + Private ReadOnly cboTerminatoreInvio As New ComboBox() + Private ReadOnly txtFileInvio As New TextBox() + Private ReadOnly txtFileRicezione As New TextBox() + Private ReadOnly btnSfogliaInvio As New Button() + Private ReadOnly btnSfogliaRicezione As New Button() + Private ReadOnly btnTestConnessione As New Button() + Private ReadOnly btnInvia As New Button() + Private ReadOnly btnRicevi As New Button() + Private ReadOnly btnAnnulla As New Button() + Private ReadOnly mnuSblocca As New ToolStripMenuItem("Sblocca controlli") + Private ReadOnly mnuCambiaPassword As New ToolStripMenuItem("Cambia password") + Private ReadOnly mnuPulisciLog As New ToolStripMenuItem("Pulisci log") + Private ReadOnly mnuInformazioni As New ToolStripMenuItem("Informazioni su") + Private ReadOnly btnAggiornaStatoMoxa As New Button() + Private ReadOnly chkMonitorStatoMoxa As New CheckBox() + Private ReadOnly nudIntervalloStatoMoxa As New NumericUpDown() + Private ReadOnly lstStatoMoxa As New ListView() + Private ReadOnly imgStatoMoxa As New ImageList() + Private ReadOnly statusTimer As New System.Windows.Forms.Timer() + Private ReadOnly txtLog As New TextBox() + Private ReadOnly progress As New ProgressBar() + Private ReadOnly lockedControls As New List(Of Control)() + + Private appConfig As AppConfig + Private cts As CancellationTokenSource + Private controlsUnlocked As Boolean + Private loadingSelection As Boolean + Private checkingMoxaStatus As Boolean + + Public Sub New() + InitializeComponent() + appConfig = LoadOrCreateConfig() + CleanupOldTemporaryReceives() + BuildInterface() + LoadCompanies() + SetControlsUnlocked(False) + End Sub + + Private Sub BuildInterface() + Text = "LMEMoxaTransfer" + Icon = New Icon(Path.Combine(Application.StartupPath, "Assets", "app-icon.ico")) + MinimumSize = New Size(1030, 690) + Size = New Size(1120, 760) + StartPosition = FormStartPosition.CenterScreen + + Dim mainMenu = BuildMainMenu() + MainMenuStrip = mainMenu + Controls.Add(mainMenu) + + Dim tabs As New TabControl With {.Dock = DockStyle.Fill} + Dim tabTrasferimento As New TabPage("Trasferimento programmi") + Dim tabStatoMoxa As New TabPage("Stato Moxa") + tabs.TabPages.Add(tabTrasferimento) + tabs.TabPages.Add(tabStatoMoxa) + Controls.Add(tabs) + + Dim root As New TableLayoutPanel With { + .Dock = DockStyle.Fill, + .ColumnCount = 1, + .RowCount = 4, + .Padding = New Padding(14) + } + root.RowStyles.Add(New RowStyle(SizeType.AutoSize)) + root.RowStyles.Add(New RowStyle(SizeType.AutoSize)) + root.RowStyles.Add(New RowStyle(SizeType.Percent, 100)) + root.RowStyles.Add(New RowStyle(SizeType.AutoSize)) + tabTrasferimento.Controls.Add(root) + + Dim grpConnessione As New GroupBox With { + .Text = "Connessione Moxa", + .Dock = DockStyle.Top, + .AutoSize = True, + .Padding = New Padding(12) + } + root.Controls.Add(grpConnessione, 0, 0) + + Dim conn As New TableLayoutPanel With {.Dock = DockStyle.Fill, .ColumnCount = 10, .AutoSize = True} + conn.ColumnStyles.Add(New ColumnStyle(SizeType.AutoSize)) + conn.ColumnStyles.Add(New ColumnStyle(SizeType.Absolute, 150)) + conn.ColumnStyles.Add(New ColumnStyle(SizeType.AutoSize)) + conn.ColumnStyles.Add(New ColumnStyle(SizeType.Absolute, 230)) + conn.ColumnStyles.Add(New ColumnStyle(SizeType.AutoSize)) + conn.ColumnStyles.Add(New ColumnStyle(SizeType.Absolute, 135)) + conn.ColumnStyles.Add(New ColumnStyle(SizeType.AutoSize)) + conn.ColumnStyles.Add(New ColumnStyle(SizeType.Absolute, 90)) + conn.ColumnStyles.Add(New ColumnStyle(SizeType.AutoSize)) + conn.ColumnStyles.Add(New ColumnStyle(SizeType.Absolute, 90)) + grpConnessione.Controls.Add(conn) + + cboAzienda.DropDownStyle = ComboBoxStyle.DropDownList + cboAzienda.Dock = DockStyle.Fill + cboMacchina.DropDownStyle = ComboBoxStyle.DropDownList + cboMacchina.Dock = DockStyle.Fill + txtIpMoxa.Dock = DockStyle.Fill + txtIpMoxa.ReadOnly = True + nudPorta.Minimum = 1 + nudPorta.Maximum = 65535 + nudTimeoutConnessione.Minimum = 1 + nudTimeoutConnessione.Maximum = 120 + nudTimeoutInattivita.Minimum = 1 + nudTimeoutInattivita.Maximum = 120 + + conn.Controls.Add(New Label With {.Text = "Azienda", .AutoSize = True, .Anchor = AnchorStyles.Left}, 0, 0) + conn.Controls.Add(cboAzienda, 1, 0) + conn.Controls.Add(New Label With {.Text = "Macchina", .AutoSize = True, .Anchor = AnchorStyles.Left, .Margin = New Padding(14, 0, 3, 0)}, 2, 0) + conn.Controls.Add(cboMacchina, 3, 0) + conn.Controls.Add(New Label With {.Text = "IP Moxa", .AutoSize = True, .Anchor = AnchorStyles.Left, .Margin = New Padding(14, 0, 3, 0)}, 4, 0) + conn.Controls.Add(txtIpMoxa, 5, 0) + conn.Controls.Add(New Label With {.Text = "Porta", .AutoSize = True, .Anchor = AnchorStyles.Left, .Margin = New Padding(14, 0, 3, 0)}, 6, 0) + conn.Controls.Add(nudPorta, 7, 0) + conn.Controls.Add(New Label With {.Text = "Timeout s", .AutoSize = True, .Anchor = AnchorStyles.Left, .Margin = New Padding(14, 0, 3, 0)}, 8, 0) + conn.Controls.Add(nudTimeoutConnessione, 9, 0) + + Dim grpTrasferimento As New GroupBox With { + .Text = "Invio e ricezione programmi", + .Dock = DockStyle.Top, + .AutoSize = True, + .Padding = New Padding(12), + .Margin = New Padding(0, 12, 0, 12) + } + root.Controls.Add(grpTrasferimento, 0, 1) + + Dim transfer As New TableLayoutPanel With {.Dock = DockStyle.Fill, .ColumnCount = 4, .RowCount = 10, .AutoSize = True} + transfer.ColumnStyles.Add(New ColumnStyle(SizeType.AutoSize)) + transfer.ColumnStyles.Add(New ColumnStyle(SizeType.Percent, 100)) + transfer.ColumnStyles.Add(New ColumnStyle(SizeType.AutoSize)) + transfer.ColumnStyles.Add(New ColumnStyle(SizeType.Absolute, 120)) + grpTrasferimento.Controls.Add(transfer) + + ConfigureButton(btnSfogliaInvio, "Sfoglia") + ConfigureButton(btnSfogliaRicezione, "Sfoglia") + ConfigureButton(btnTestConnessione, "Test connessione") + ConfigureButton(btnInvia, "Invia a Okuma") + ConfigureButton(btnRicevi, "Ricevi da Okuma") + ConfigureButton(btnAnnulla, "Annulla") + btnAnnulla.Enabled = False + + txtFileInvio.Dock = DockStyle.Fill + txtFileRicezione.Dock = DockStyle.Fill + cboTerminatoreInvio.DropDownStyle = ComboBoxStyle.DropDownList + cboTerminatoreInvio.Items.AddRange(New Object() {"Nessuno", "CR", "LF", "CRLF", "%", "% + CRLF"}) + chkInvioRigaPerRiga.Text = "Invio riga per riga" + chkInvioRigaPerRiga.AutoSize = True + chkInvioCaratterePerCarattere.Text = "Invio carattere per carattere" + chkInvioCaratterePerCarattere.AutoSize = True + chkInvioBlocchi.Text = "Invio a blocchi (% + CRLF)" + chkInvioBlocchi.AutoSize = True + ConfigureNumericDefaults() + + transfer.Controls.Add(New Label With {.Text = "File da inviare", .AutoSize = True, .Anchor = AnchorStyles.Left}, 0, 0) + transfer.Controls.Add(txtFileInvio, 1, 0) + transfer.Controls.Add(btnSfogliaInvio, 2, 0) + transfer.Controls.Add(btnInvia, 3, 0) + transfer.Controls.Add(New Label With {.Text = "Cartella ricezione", .AutoSize = True, .Anchor = AnchorStyles.Left}, 0, 1) + transfer.Controls.Add(txtFileRicezione, 1, 1) + transfer.Controls.Add(btnSfogliaRicezione, 2, 1) + transfer.Controls.Add(btnRicevi, 3, 1) + transfer.Controls.Add(New Label With {.Text = "Terminatore invio", .AutoSize = True, .Anchor = AnchorStyles.Left}, 0, 2) + transfer.Controls.Add(cboTerminatoreInvio, 1, 2) + transfer.Controls.Add(btnAnnulla, 3, 2) + transfer.Controls.Add(New Label With {.Text = "Pausa riga ms", .AutoSize = True, .Anchor = AnchorStyles.Left}, 0, 3) + transfer.Controls.Add(nudPausaRiga, 1, 3) + transfer.Controls.Add(chkInvioRigaPerRiga, 2, 3) + transfer.Controls.Add(New Label With {.Text = "Pausa carattere ms", .AutoSize = True, .Anchor = AnchorStyles.Left}, 0, 4) + transfer.Controls.Add(nudPausaCarattere, 1, 4) + transfer.Controls.Add(chkInvioCaratterePerCarattere, 2, 4) + transfer.Controls.Add(New Label With {.Text = "Modo Okuma READ", .AutoSize = True, .Anchor = AnchorStyles.Left}, 0, 5) + transfer.Controls.Add(chkInvioBlocchi, 1, 5) + transfer.Controls.Add(New Label With {.Text = "Dim. blocco byte", .AutoSize = True, .Anchor = AnchorStyles.Left}, 2, 5) + transfer.Controls.Add(nudDimensioneBlocco, 3, 5) + transfer.Controls.Add(New Label With {.Text = "Pausa blocco ms", .AutoSize = True, .Anchor = AnchorStyles.Left}, 0, 6) + transfer.Controls.Add(nudPausaBlocco, 1, 6) + transfer.Controls.Add(New Label With {.Text = "Fine ricez. s", .AutoSize = True, .Anchor = AnchorStyles.Left}, 2, 6) + transfer.Controls.Add(nudTimeoutInattivita, 3, 6) + transfer.Controls.Add(New Label With {.Text = "Attesa prima invio s", .AutoSize = True, .Anchor = AnchorStyles.Left}, 0, 7) + transfer.Controls.Add(nudAttesaPrimaInvio, 1, 7) + transfer.Controls.Add(btnTestConnessione, 3, 7) + transfer.Controls.Add(New Label With {.Text = "Attesa dopo invio s", .AutoSize = True, .Anchor = AnchorStyles.Left}, 0, 8) + transfer.Controls.Add(nudAttesaDopoInvio, 1, 8) + + Dim hint As New Label With { + .Text = "Prima di ricevere o inviare, selezionare azienda e macchina. I parametri Moxa vengono compilati dal file JSON nello start path.", + .AutoSize = True, + .Dock = DockStyle.Fill, + .ForeColor = Color.FromArgb(70, 70, 70), + .Margin = New Padding(0, 10, 0, 0) + } + transfer.SetColumnSpan(hint, 4) + transfer.Controls.Add(hint, 0, 9) + + txtLog.Dock = DockStyle.Fill + txtLog.Multiline = True + txtLog.ScrollBars = ScrollBars.Vertical + txtLog.ReadOnly = True + txtLog.Font = New Font("Consolas", 10.0F) + root.Controls.Add(txtLog, 0, 2) + + progress.Dock = DockStyle.Fill + progress.Style = ProgressBarStyle.Blocks + root.Controls.Add(progress, 0, 3) + + lockedControls.AddRange({nudPorta, nudTimeoutConnessione, nudTimeoutInattivita, cboTerminatoreInvio, nudPausaRiga, nudPausaCarattere, nudDimensioneBlocco, nudPausaBlocco, nudAttesaPrimaInvio, nudAttesaDopoInvio, chkInvioRigaPerRiga, chkInvioCaratterePerCarattere, chkInvioBlocchi}) + + AddHandler cboAzienda.SelectedIndexChanged, AddressOf CompanyChanged + AddHandler cboMacchina.SelectedIndexChanged, AddressOf MachineChanged + AddHandler mnuSblocca.Click, AddressOf UnlockClicked + AddHandler mnuCambiaPassword.Click, AddressOf ChangePasswordClicked + AddHandler mnuPulisciLog.Click, AddressOf ClearLogClicked + AddHandler mnuInformazioni.Click, AddressOf AboutClicked + AddHandler btnSfogliaInvio.Click, AddressOf SelectInputFile + AddHandler btnSfogliaRicezione.Click, AddressOf SelectOutputFile + AddHandler btnTestConnessione.Click, AddressOf TestConnectionClicked + AddHandler btnInvia.Click, AddressOf SendClicked + AddHandler btnRicevi.Click, AddressOf ReceiveClicked + AddHandler btnAnnulla.Click, Sub() cts?.Cancel() + + txtFileRicezione.Text = GetInitialReceiveDirectory() + BuildStatusTab(tabStatoMoxa) + Log($"Pronto. Configurazione: {ConfigPath()}") + End Sub + + Private Function BuildMainMenu() As MenuStrip + Dim menu As New MenuStrip() + Dim strumenti As New ToolStripMenuItem("Strumenti") + strumenti.DropDownItems.Add(mnuSblocca) + strumenti.DropDownItems.Add(mnuCambiaPassword) + strumenti.DropDownItems.Add(New ToolStripSeparator()) + strumenti.DropDownItems.Add(mnuPulisciLog) + + Dim aiuto As New ToolStripMenuItem("?") + aiuto.DropDownItems.Add(mnuInformazioni) + + menu.Items.Add(strumenti) + menu.Items.Add(aiuto) + Return menu + End Function + + Private Function GetInitialReceiveDirectory() As String + If Not String.IsNullOrWhiteSpace(appConfig.LastReceiveDirectory) AndAlso Directory.Exists(appConfig.LastReceiveDirectory) Then + Return appConfig.LastReceiveDirectory + End If + + Return Environment.GetFolderPath(Environment.SpecialFolder.DesktopDirectory) + End Function + + Private Sub BuildStatusTab(tab As TabPage) + Dim root As New TableLayoutPanel With { + .Dock = DockStyle.Fill, + .ColumnCount = 1, + .RowCount = 2, + .Padding = New Padding(14) + } + root.RowStyles.Add(New RowStyle(SizeType.AutoSize)) + root.RowStyles.Add(New RowStyle(SizeType.Percent, 100)) + tab.Controls.Add(root) + + Dim actions As New FlowLayoutPanel With {.Dock = DockStyle.Fill, .AutoSize = True, .FlowDirection = FlowDirection.LeftToRight} + ConfigureButton(btnAggiornaStatoMoxa, "Aggiorna") + chkMonitorStatoMoxa.Text = "Controllo automatico" + chkMonitorStatoMoxa.AutoSize = True + chkMonitorStatoMoxa.Checked = True + chkMonitorStatoMoxa.Margin = New Padding(16, 8, 0, 3) + nudIntervalloStatoMoxa.Minimum = 5 + nudIntervalloStatoMoxa.Maximum = 3600 + nudIntervalloStatoMoxa.Value = Math.Max(nudIntervalloStatoMoxa.Minimum, Math.Min(nudIntervalloStatoMoxa.Maximum, appConfig.MonitorRefreshSeconds)) + nudIntervalloStatoMoxa.Width = 80 + + actions.Controls.Add(btnAggiornaStatoMoxa) + actions.Controls.Add(chkMonitorStatoMoxa) + actions.Controls.Add(New Label With {.Text = "Intervallo s", .AutoSize = True, .Margin = New Padding(16, 9, 3, 3)}) + actions.Controls.Add(nudIntervalloStatoMoxa) + root.Controls.Add(actions, 0, 0) + + imgStatoMoxa.ImageSize = New Size(16, 16) + imgStatoMoxa.ColorDepth = ColorDepth.Depth32Bit + imgStatoMoxa.Images.Add("unknown", CreateStatusIcon(Color.Gray)) + imgStatoMoxa.Images.Add("online", CreateStatusIcon(Color.FromArgb(0, 150, 70))) + imgStatoMoxa.Images.Add("offline", CreateStatusIcon(Color.FromArgb(210, 40, 40))) + + lstStatoMoxa.Dock = DockStyle.Fill + lstStatoMoxa.View = View.Details + lstStatoMoxa.FullRowSelect = True + lstStatoMoxa.GridLines = True + lstStatoMoxa.SmallImageList = imgStatoMoxa + lstStatoMoxa.Columns.Add("Macchina", 280) + lstStatoMoxa.Columns.Add("IP Moxa", 140) + lstStatoMoxa.Columns.Add("Porta", 70) + lstStatoMoxa.Columns.Add("Stato", 130) + lstStatoMoxa.Columns.Add("Ultimo controllo", 160) + lstStatoMoxa.Columns.Add("Note", 220) + root.Controls.Add(lstStatoMoxa, 0, 1) + + lockedControls.Add(chkMonitorStatoMoxa) + lockedControls.Add(nudIntervalloStatoMoxa) + + statusTimer.Interval = CInt(nudIntervalloStatoMoxa.Value) * 1000 + statusTimer.Enabled = chkMonitorStatoMoxa.Checked + AddHandler btnAggiornaStatoMoxa.Click, AddressOf RefreshMoxaStatusClicked + AddHandler statusTimer.Tick, AddressOf StatusTimerTick + AddHandler nudIntervalloStatoMoxa.ValueChanged, AddressOf StatusIntervalChanged + AddHandler chkMonitorStatoMoxa.CheckedChanged, AddressOf StatusMonitorChanged + + PopulateMoxaStatusList() + End Sub + + Private Shared Function CreateStatusIcon(color As Color) As Bitmap + Dim bitmap As New Bitmap(16, 16) + Using g = Graphics.FromImage(bitmap) + g.Clear(Color.Transparent) + Using brush As New SolidBrush(color) + g.FillEllipse(brush, 2, 2, 12, 12) + End Using + Using pen As New Pen(Color.FromArgb(80, 0, 0, 0)) + g.DrawEllipse(pen, 2, 2, 12, 12) + End Using + End Using + Return bitmap + End Function + + Private Sub ConfigureNumericDefaults() + nudPorta.Minimum = 1 + nudPorta.Maximum = 65535 + nudTimeoutConnessione.Minimum = 1 + nudTimeoutConnessione.Maximum = 120 + nudTimeoutInattivita.Minimum = 1 + nudTimeoutInattivita.Maximum = 120 + nudPausaRiga.Minimum = 0 + nudPausaRiga.Maximum = 2000 + nudPausaCarattere.Minimum = 0 + nudPausaCarattere.Maximum = 200 + nudDimensioneBlocco.Minimum = 1 + nudDimensioneBlocco.Maximum = 1024 + nudPausaBlocco.Minimum = 0 + nudPausaBlocco.Maximum = 5000 + nudAttesaPrimaInvio.Minimum = 0 + nudAttesaPrimaInvio.Maximum = 60 + nudAttesaDopoInvio.Minimum = 0 + nudAttesaDopoInvio.Maximum = 30 + End Sub + + Private Sub LoadCompanies() + loadingSelection = True + cboAzienda.Items.Clear() + For Each company In appConfig.Companies + cboAzienda.Items.Add(company.Name) + Next + loadingSelection = False + + If cboAzienda.Items.Count > 0 Then + Dim index = Math.Max(0, appConfig.Companies.FindIndex(Function(c) c.Name = appConfig.DefaultCompany)) + cboAzienda.SelectedIndex = index + End If + + PopulateMoxaStatusList() + End Sub + + Private Sub PopulateMoxaStatusList() + If lstStatoMoxa.Columns.Count = 0 Then + Return + End If + + lstStatoMoxa.BeginUpdate() + lstStatoMoxa.Items.Clear() + lstStatoMoxa.Groups.Clear() + + For Each company In appConfig.Companies + Dim group As New ListViewGroup(company.Name, HorizontalAlignment.Left) + lstStatoMoxa.Groups.Add(group) + + For Each machine In company.Machines + Dim item As New ListViewItem(machine.Name, "unknown") With { + .Group = group, + .Tag = New MoxaStatusTarget With {.Company = company.Name, .Machine = machine} + } + item.SubItems.Add(machine.IpMoxa) + item.SubItems.Add(appConfig.DefaultSettings.Port.ToString()) + item.SubItems.Add("Non controllato") + item.SubItems.Add("") + item.SubItems.Add(machine.Note) + lstStatoMoxa.Items.Add(item) + Next + Next + + lstStatoMoxa.EndUpdate() + End Sub + + Private Sub CompanyChanged(sender As Object, e As EventArgs) + If loadingSelection OrElse cboAzienda.SelectedIndex < 0 Then + Return + End If + + Dim company = CurrentCompany() + loadingSelection = True + cboMacchina.Items.Clear() + For Each machine In company.Machines + cboMacchina.Items.Add(machine.Name) + Next + loadingSelection = False + + If cboMacchina.Items.Count > 0 Then + cboMacchina.SelectedIndex = 0 + End If + End Sub + + Private Sub MachineChanged(sender As Object, e As EventArgs) + If loadingSelection Then + Return + End If + + ApplyCurrentMachineSettings() + End Sub + + Private Sub ApplyCurrentMachineSettings() + Dim machine = CurrentMachine() + If machine Is Nothing Then + Return + End If + + Dim defaults = appConfig.DefaultSettings + txtIpMoxa.Text = machine.IpMoxa + nudPorta.Value = defaults.Port + nudTimeoutConnessione.Value = defaults.ConnectionTimeoutSeconds + nudTimeoutInattivita.Value = defaults.ReceiveIdleTimeoutSeconds + cboTerminatoreInvio.SelectedItem = defaults.SendTerminator + nudPausaRiga.Value = defaults.LineDelayMs + nudPausaCarattere.Value = defaults.CharDelayMs + chkInvioRigaPerRiga.Checked = defaults.LinePaced + chkInvioCaratterePerCarattere.Checked = defaults.CharPaced + chkInvioBlocchi.Checked = defaults.BlockPaced + nudDimensioneBlocco.Value = defaults.BlockSizeBytes + nudPausaBlocco.Value = defaults.BlockDelayMs + nudAttesaPrimaInvio.Value = defaults.WaitBeforeSendSeconds + nudAttesaDopoInvio.Value = defaults.WaitAfterSendSeconds + Log($"Macchina selezionata: {CurrentCompany().Name} / {machine.Name} - IP Moxa {machine.IpMoxa}.") + End Sub + + Private Function CurrentCompany() As CompanyConfig + If cboAzienda.SelectedIndex < 0 OrElse cboAzienda.SelectedIndex >= appConfig.Companies.Count Then + Return Nothing + End If + Return appConfig.Companies(cboAzienda.SelectedIndex) + End Function + + Private Function CurrentMachine() As MachineConfig + Dim company = CurrentCompany() + If company Is Nothing OrElse cboMacchina.SelectedIndex < 0 OrElse cboMacchina.SelectedIndex >= company.Machines.Count Then + Return Nothing + End If + Return company.Machines(cboMacchina.SelectedIndex) + End Function + + Private Sub UnlockClicked(sender As Object, e As EventArgs) + If controlsUnlocked Then + SetControlsUnlocked(False) + Return + End If + + If String.IsNullOrWhiteSpace(appConfig.AdminPasswordHash) OrElse String.IsNullOrWhiteSpace(appConfig.AdminPasswordSalt) Then + Dim newPassword = PromptPassword("Password non configurata", "Inserire una nuova password amministratore:") + If String.IsNullOrEmpty(newPassword) Then + Return + End If + SetPassword(newPassword) + SaveConfig() + SetControlsUnlocked(True) + Return + End If + + Dim password = PromptPassword("Sblocca controlli", "Inserire la password:") + If password Is Nothing Then + Return + End If + + If VerifyPassword(password) Then + SetControlsUnlocked(True) + Else + MessageBox.Show(Me, "Password non corretta.", "Accesso negato", MessageBoxButtons.OK, MessageBoxIcon.Warning) + End If + End Sub + + Private Sub ChangePasswordClicked(sender As Object, e As EventArgs) + If Not String.IsNullOrWhiteSpace(appConfig.AdminPasswordHash) AndAlso Not String.IsNullOrWhiteSpace(appConfig.AdminPasswordSalt) Then + Dim oldPassword = PromptPassword("Cambia password", "Inserire la vecchia password:") + If oldPassword Is Nothing Then + Return + End If + If Not VerifyPassword(oldPassword) Then + MessageBox.Show(Me, "Vecchia password non corretta.", "Password", MessageBoxButtons.OK, MessageBoxIcon.Warning) + Return + End If + End If + + Dim newPassword = PromptPassword("Cambia password", "Inserire la nuova password:") + If String.IsNullOrEmpty(newPassword) Then + Return + End If + + SetPassword(newPassword) + SaveConfig() + MessageBox.Show(Me, "Password aggiornata.", "Password", MessageBoxButtons.OK, MessageBoxIcon.Information) + End Sub + + Private Sub ClearLogClicked(sender As Object, e As EventArgs) + txtLog.Clear() + End Sub + + Private Sub AboutClicked(sender As Object, e As EventArgs) + Dim version = Assembly.GetExecutingAssembly().GetName().Version + MessageBox.Show(Me, + $"LMEMoxaTransfer{Environment.NewLine}{Environment.NewLine}Azienda: Compomac SPA{Environment.NewLine}Autore: Livio Merola{Environment.NewLine}Versione: {version}", + "Informazioni su", + MessageBoxButtons.OK, + MessageBoxIcon.Information) + End Sub + + Private Async Sub RefreshMoxaStatusClicked(sender As Object, e As EventArgs) + Await RefreshMoxaStatusAsync() + End Sub + + Private Async Sub StatusTimerTick(sender As Object, e As EventArgs) + If chkMonitorStatoMoxa.Checked Then + Await RefreshMoxaStatusAsync() + End If + End Sub + + Private Sub StatusIntervalChanged(sender As Object, e As EventArgs) + statusTimer.Interval = CInt(nudIntervalloStatoMoxa.Value) * 1000 + appConfig.MonitorRefreshSeconds = CInt(nudIntervalloStatoMoxa.Value) + SaveConfig() + End Sub + + Private Sub StatusMonitorChanged(sender As Object, e As EventArgs) + statusTimer.Enabled = chkMonitorStatoMoxa.Checked + End Sub + + Private Async Function RefreshMoxaStatusAsync() As Task + If checkingMoxaStatus Then + Return + End If + + checkingMoxaStatus = True + btnAggiornaStatoMoxa.Enabled = False + Try + For Each item As ListViewItem In lstStatoMoxa.Items + Dim target = TryCast(item.Tag, MoxaStatusTarget) + If target Is Nothing Then + Continue For + End If + + item.ImageKey = "unknown" + item.SubItems(3).Text = "Controllo..." + item.SubItems(4).Text = DateTime.Now.ToString("HH:mm:ss") + Dim online = Await IsMoxaReachableAsync(target.Machine.IpMoxa, appConfig.DefaultSettings.Port, TimeSpan.FromMilliseconds(1200)) + item.ImageKey = If(online, "online", "offline") + item.SubItems(3).Text = If(online, "Raggiungibile", "Non raggiungibile") + item.SubItems(4).Text = DateTime.Now.ToString("HH:mm:ss") + Next + Finally + btnAggiornaStatoMoxa.Enabled = True + checkingMoxaStatus = False + End Try + End Function + + Private Shared Async Function IsMoxaReachableAsync(ip As String, port As Integer, timeout As TimeSpan) As Task(Of Boolean) + If String.IsNullOrWhiteSpace(ip) Then + Return False + End If + + Using client As New TcpClient() + Try + Dim connectTask = client.ConnectAsync(ip.Trim(), port) + Dim completed = Await Task.WhenAny(connectTask, Task.Delay(timeout)) + If completed IsNot connectTask Then + Return False + End If + Await connectTask + Return True + Catch + Return False + End Try + End Using + End Function + + Private Sub SetControlsUnlocked(unlocked As Boolean) + controlsUnlocked = unlocked + For Each control In lockedControls + control.Enabled = unlocked + Next + mnuSblocca.Text = If(unlocked, "Blocca controlli", "Sblocca controlli") + End Sub + + Private Async Sub TestConnectionClicked(sender As Object, e As EventArgs) + Await RunTransferAsync(Async Function(token) + Using client = Await ConnectAsync(token) + Log("Test OK: connessione TCP aperta, nessun dato inviato a Okuma.") + End Using + End Function) + End Sub + + Private Shared Sub ConfigureButton(button As Button, text As String) + button.Text = text + button.AutoSize = True + button.MinimumSize = New Size(110, 32) + button.Margin = New Padding(8, 3, 0, 3) + End Sub + + Private Sub SelectInputFile(sender As Object, e As EventArgs) + Using dlg As New OpenFileDialog() + dlg.Title = "Seleziona programma da inviare a Okuma" + dlg.Filter = "Programmi CNC|*.min;*.eia;*.iso;*.txt;*.nc|Tutti i file|*.*" + If dlg.ShowDialog(Me) = DialogResult.OK Then + txtFileInvio.Text = dlg.FileName + End If + End Using + End Sub + + Private Sub SelectOutputFile(sender As Object, e As EventArgs) + Using dlg As New FolderBrowserDialog() + dlg.Description = "Scegli la cartella dove salvare il programma ricevuto" + dlg.UseDescriptionForTitle = True + If Directory.Exists(txtFileRicezione.Text) Then + dlg.SelectedPath = txtFileRicezione.Text + End If + If dlg.ShowDialog(Me) = DialogResult.OK Then + txtFileRicezione.Text = dlg.SelectedPath + appConfig.LastReceiveDirectory = dlg.SelectedPath + SaveConfig() + End If + End Using + End Sub + + Private Async Sub SendClicked(sender As Object, e As EventArgs) + If CurrentMachine() Is Nothing Then + MessageBox.Show(Me, "Selezionare azienda e macchina.", "Macchina mancante", MessageBoxButtons.OK, MessageBoxIcon.Warning) + Return + End If + If Not File.Exists(txtFileInvio.Text) Then + MessageBox.Show(Me, "Selezionare un file esistente da inviare.", "File mancante", MessageBoxButtons.OK, MessageBoxIcon.Warning) + Return + End If + + Await RunTransferAsync(Async Function(token) + Dim fileBytes = Await File.ReadAllBytesAsync(txtFileInvio.Text, token) + fileBytes = RemoveUtf8BomIfPresent(fileBytes) + LogFirstBytes(fileBytes) + ValidateCncTextBytes(fileBytes) + Dim blockPaced = chkInvioBlocchi.Checked + Dim terminator = If(blockPaced, Encoding.ASCII.GetBytes("%" & vbCrLf), GetTerminatorBytes()) + + Using client = Await ConnectAsync(token) + Using stream = client.GetStream() + Dim waitBeforeSend = CInt(nudAttesaPrimaInvio.Value) + If waitBeforeSend > 0 Then + Log($"Attesa prima del primo byte: {waitBeforeSend} s.") + Await Task.Delay(TimeSpan.FromSeconds(waitBeforeSend), token) + End If + + If blockPaced Then + Dim blockSize = CInt(nudDimensioneBlocco.Value) + Dim blockDelayMs = CInt(nudPausaBlocco.Value) + Log($"Invio Okuma READ: blocchi da {blockSize} byte con pausa {blockDelayMs} ms.") + Await SendBlockPacedAsync(stream, fileBytes, blockSize, blockDelayMs, token) + ElseIf chkInvioRigaPerRiga.Checked Then + Await SendLinePacedAsync(stream, fileBytes, CInt(nudPausaRiga.Value), CInt(nudPausaCarattere.Value), chkInvioCaratterePerCarattere.Checked, token) + Else + Await stream.WriteAsync(fileBytes, token) + End If + + If terminator.Length > 0 Then + Await stream.WriteAsync(terminator, token) + End If + Await stream.FlushAsync(token) + + Dim waitAfterSend = CInt(nudAttesaDopoInvio.Value) + If blockPaced Then + waitAfterSend = Math.Max(waitAfterSend, 1) + End If + If waitAfterSend > 0 Then + Await Task.Delay(TimeSpan.FromSeconds(waitAfterSend), token) + End If + End Using + End Using + Log($"Inviati {fileBytes.Length:N0} byte a Okuma.") + End Function) + End Sub + + Private Async Sub ReceiveClicked(sender As Object, e As EventArgs) + If CurrentMachine() Is Nothing Then + MessageBox.Show(Me, "Selezionare azienda e macchina.", "Macchina mancante", MessageBoxButtons.OK, MessageBoxIcon.Warning) + Return + End If + If String.IsNullOrWhiteSpace(txtFileRicezione.Text) Then + MessageBox.Show(Me, "Scegliere una cartella di destinazione per la ricezione.", "Cartella mancante", MessageBoxButtons.OK, MessageBoxIcon.Warning) + Return + End If + + Await RunTransferAsync(Async Function(token) + Dim destinationDirectory = Path.GetFullPath(txtFileRicezione.Text) + Directory.CreateDirectory(destinationDirectory) + Dim temporaryPath = CreateTemporaryReceivePath() + Using client = Await ConnectAsync(token) + Using stream = client.GetStream() + Dim received = Await ReceiveUntilIdleAsync(stream, temporaryPath, token) + Log($"Ricevuti {received:N0} byte da Okuma.") + End Using + End Using + Dim finalPath = PromptAndSaveReceivedFile(temporaryPath, destinationDirectory) + Log($"File ricevuto salvato in: {finalPath}") + End Function) + End Sub + + Private Async Function RunTransferAsync(action As Func(Of CancellationToken, Task)) As Task + SetBusy(True) + cts = New CancellationTokenSource() + + Try + Await action(cts.Token) + Log("Operazione completata.") + Catch ex As OperationCanceledException + Log("Operazione annullata.") + Catch ex As Exception + Log("Errore: " & ex.Message) + MessageBox.Show(Me, ex.Message, "Errore trasferimento", MessageBoxButtons.OK, MessageBoxIcon.Error) + Finally + cts.Dispose() + cts = Nothing + SetBusy(False) + End Try + End Function + + Private Shared Async Function SendLinePacedAsync(stream As NetworkStream, data As Byte(), lineDelayMs As Integer, charDelayMs As Integer, charPaced As Boolean, token As CancellationToken) As Task + Dim start = 0 + For i = 0 To data.Length - 1 + If data(i) = 10 Then + Await WriteChunkAsync(stream, data, start, i - start + 1, charDelayMs, charPaced, token) + Await stream.FlushAsync(token) + start = i + 1 + If lineDelayMs > 0 Then + Await Task.Delay(lineDelayMs, token) + End If + End If + Next + If start < data.Length Then + Await WriteChunkAsync(stream, data, start, data.Length - start, charDelayMs, charPaced, token) + End If + End Function + + Private Shared Async Function WriteChunkAsync(stream As NetworkStream, data As Byte(), offset As Integer, count As Integer, charDelayMs As Integer, charPaced As Boolean, token As CancellationToken) As Task + If Not charPaced Then + Await stream.WriteAsync(data.AsMemory(offset, count), token) + Return + End If + + For i = offset To offset + count - 1 + Await stream.WriteAsync(data.AsMemory(i, 1), token) + If charDelayMs > 0 Then + Await Task.Delay(charDelayMs, token) + End If + Next + End Function + + Private Shared Async Function SendBlockPacedAsync(stream As NetworkStream, data As Byte(), blockSize As Integer, blockDelayMs As Integer, token As CancellationToken) As Task + Dim offset = 0 + While offset < data.Length + Dim count = Math.Min(blockSize, data.Length - offset) + Await stream.WriteAsync(data.AsMemory(offset, count), token) + Await stream.FlushAsync(token) + offset += count + If offset < data.Length AndAlso blockDelayMs > 0 Then + Await Task.Delay(blockDelayMs, token) + End If + End While + End Function + + Private Async Function ConnectAsync(token As CancellationToken) As Task(Of TcpClient) + Dim machine = CurrentMachine() + If machine Is Nothing Then + Throw New InvalidOperationException("Macchina non selezionata.") + End If + + Dim ip = machine.IpMoxa.Trim() + Dim port = CInt(nudPorta.Value) + Dim timeout = TimeSpan.FromSeconds(CInt(nudTimeoutConnessione.Value)) + Dim client As New TcpClient With {.NoDelay = True} + + Log($"Connessione a {ip}:{port}...") + Dim connectTask = client.ConnectAsync(ip, port, token).AsTask() + Dim completed = Await Task.WhenAny(connectTask, Task.Delay(timeout, token)) + If completed IsNot connectTask Then + client.Dispose() + Throw New TimeoutException($"Connessione non riuscita entro {timeout.TotalSeconds:N0} secondi.") + End If + + Await connectTask + Log("Connessione aperta.") + Return client + End Function + + Private Async Function ReceiveUntilIdleAsync(stream As NetworkStream, destinationPath As String, token As CancellationToken) As Task(Of Long) + Dim buffer(8191) As Byte + Dim total As Long = 0 + Dim idleTimeout = TimeSpan.FromSeconds(CInt(nudTimeoutInattivita.Value)) + + Log("In attesa dati. Avviare l'invio programma dal controllo Okuma.") + Using output As New FileStream(destinationPath, FileMode.Create, FileAccess.Write, FileShare.Read, 8192, useAsync:=True) + Do + Dim readTask = stream.ReadAsync(buffer.AsMemory(0, buffer.Length), token).AsTask() + Dim completed = Await Task.WhenAny(readTask, Task.Delay(idleTimeout, token)) + + If completed IsNot readTask Then + If total = 0 Then + Throw New TimeoutException("Nessun dato ricevuto prima del timeout di inattivita.") + End If + Exit Do + End If + + Dim count = Await readTask + If count = 0 Then + Exit Do + End If + + Await output.WriteAsync(buffer.AsMemory(0, count), token) + total += count + progress.Value = CInt(Math.Min(progress.Maximum, total Mod progress.Maximum)) + Log($"Ricevuti {total:N0} byte...") + Loop + End Using + + Return total + End Function + + Private Shared Function CreateTemporaryReceivePath() As String + Dim tempDirectory = Path.Combine(Application.StartupPath, "RicezioniTemp") + Directory.CreateDirectory(tempDirectory) + Return Path.Combine(tempDirectory, $"ricezione_okuma_{DateTime.Now:yyyyMMdd_HHmmss}_{Guid.NewGuid():N}.tmp") + End Function + + Private Shared Sub CleanupOldTemporaryReceives() + Dim tempDirectory = Path.Combine(Application.StartupPath, "RicezioniTemp") + If Not Directory.Exists(tempDirectory) Then + Return + End If + + For Each filePath In Directory.EnumerateFiles(tempDirectory, "*.tmp") + Try + If File.GetLastWriteTime(filePath) < DateTime.Now.AddDays(-7) Then + File.Delete(filePath) + End If + Catch + End Try + Next + End Sub + + Private Function PromptAndSaveReceivedFile(temporaryPath As String, destinationDirectory As String) As String + If Not File.Exists(temporaryPath) Then + Return temporaryPath + End If + + Dim suggestedName = ExtractProgramFileName(File.ReadAllBytes(temporaryPath)) + If String.IsNullOrWhiteSpace(suggestedName) Then + suggestedName = "programma_okuma.min" + Log("Nessun nome programma preceduto da $ trovato nel file ricevuto: propongo un nome predefinito.") + End If + + Dim chosenName As String = Nothing + Do + chosenName = PromptReceivedFileName(suggestedName) + If chosenName Is Nothing Then + Log($"Nome finale annullato: il file temporaneo resta in {temporaryPath}") + Return temporaryPath + End If + + chosenName = Path.GetFileName(chosenName.Trim()) + If Not String.IsNullOrWhiteSpace(chosenName) AndAlso chosenName.IndexOfAny(Path.GetInvalidFileNameChars()) < 0 Then + Exit Do + End If + + MessageBox.Show(Me, "Nome file non valido.", "Nome file", MessageBoxButtons.OK, MessageBoxIcon.Warning) + Loop + + Dim finalPath = Path.Combine(destinationDirectory, chosenName) + + If File.Exists(finalPath) Then + Dim overwrite = MessageBox.Show(Me, $"Il file {chosenName} esiste gia'. Sovrascriverlo?", "File esistente", MessageBoxButtons.YesNo, MessageBoxIcon.Question) + If overwrite <> DialogResult.Yes Then + Return PromptAndSaveReceivedFile(temporaryPath, destinationDirectory) + End If + End If + + File.Move(temporaryPath, finalPath, True) + Return finalPath + End Function + + Private Shared Function ExtractProgramFileName(data As Byte()) As String + Dim invalidChars = Path.GetInvalidFileNameChars() + + For i = 0 To data.Length - 1 + If data(i) <> AscW("$"c) Then + Continue For + End If + + Dim builder As New StringBuilder() + For j = i + 1 To data.Length - 1 + Dim value = data(j) + If value = 0 OrElse value = &HD OrElse value = &HA OrElse value = &H9 OrElse value = AscW("$"c) Then + Exit For + End If + If value < &H20 OrElse value > &H7E Then + Exit For + End If + + Dim ch = ChrW(value) + If Array.IndexOf(invalidChars, ch) >= 0 Then + Exit For + End If + + builder.Append(ch) + Next + + Dim candidate = builder.ToString().Trim() + If candidate.Length > 0 AndAlso candidate.Contains("."c) Then + Return candidate + End If + Next + + Return "" + End Function + + Private Function PromptReceivedFileName(suggestedName As String) As String + Using form As New Form With {.Text = "Nome file ricevuto", .StartPosition = FormStartPosition.CenterParent, .FormBorderStyle = FormBorderStyle.FixedDialog, .MinimizeBox = False, .MaximizeBox = False, .ClientSize = New Size(440, 140)} + Dim label As New Label With {.Text = "Conferma o modifica il nome file da salvare:", .Left = 12, .Top = 12, .Width = 410, .AutoSize = False} + Dim textBox As New TextBox With {.Left = 12, .Top = 42, .Width = 410, .Text = suggestedName} + Dim ok As New Button With {.Text = "Salva", .Left = 256, .Top = 88, .Width = 78, .DialogResult = DialogResult.OK} + Dim cancel As New Button With {.Text = "Annulla", .Left = 344, .Top = 88, .Width = 78, .DialogResult = DialogResult.Cancel} + form.Controls.AddRange({label, textBox, ok, cancel}) + form.AcceptButton = ok + form.CancelButton = cancel + textBox.SelectAll() + + If form.ShowDialog(Me) = DialogResult.OK Then + Return textBox.Text + End If + End Using + + Return Nothing + End Function + + Private Function GetTerminatorBytes() As Byte() + Select Case CStr(cboTerminatoreInvio.SelectedItem) + Case "CR" + Return Encoding.ASCII.GetBytes(vbCr) + Case "LF" + Return Encoding.ASCII.GetBytes(vbLf) + Case "CRLF" + Return Encoding.ASCII.GetBytes(vbCrLf) + Case "%" + Return Encoding.ASCII.GetBytes("%") + Case "% + CRLF" + Return Encoding.ASCII.GetBytes("%" & vbCrLf) + Case Else + Return Array.Empty(Of Byte)() + End Select + End Function + + Private Shared Function RemoveUtf8BomIfPresent(data As Byte()) As Byte() + If data.Length < 3 OrElse data(0) <> &HEF OrElse data(1) <> &HBB OrElse data(2) <> &HBF Then + Return data + End If + + Dim cleanLength = data.Length - 3 + If cleanLength = 0 Then + Return Array.Empty(Of Byte)() + End If + + Dim clean(cleanLength - 1) As Byte + Buffer.BlockCopy(data, 3, clean, 0, clean.Length) + Return clean + End Function + + Private Sub LogFirstBytes(data As Byte()) + If data.Length = 0 Then + Log("File vuoto: nessun byte da inviare prima del terminatore.") + Return + End If + + Dim previewLength = Math.Min(16, data.Length) + Dim preview As New StringBuilder() + For i = 0 To previewLength - 1 + If i > 0 Then + preview.Append(" ") + End If + preview.Append(data(i).ToString("X2")) + Next + + Log($"Primi byte inviati: {preview}") + If data(0) > &H7F Then + Log($"Attenzione: il primo byte e' > 7 bit ({data(0):X2}). Per Okuma 7E1 puo' essere un carattere non valido.") + End If + End Sub + + Private Shared Sub ValidateCncTextBytes(data As Byte()) + For i = 0 To data.Length - 1 + Dim value = data(i) + Dim isAllowedControl = value = &H9 OrElse value = &HA OrElse value = &HD + Dim isPrintableAscii = value >= &H20 AndAlso value <= &H7E + If Not isAllowedControl AndAlso Not isPrintableAscii Then + Throw New InvalidDataException($"Il file non sembra testo CNC ASCII/7 bit: byte non valido 0x{value:X2} alla posizione {i}.") + End If + Next + End Sub + + Private Sub SetBusy(isBusy As Boolean) + btnInvia.Enabled = Not isBusy + btnRicevi.Enabled = Not isBusy + btnTestConnessione.Enabled = Not isBusy + btnSfogliaInvio.Enabled = Not isBusy + btnSfogliaRicezione.Enabled = Not isBusy + mnuSblocca.Enabled = Not isBusy + mnuCambiaPassword.Enabled = Not isBusy + mnuPulisciLog.Enabled = Not isBusy + btnAnnulla.Enabled = isBusy + If Not isBusy Then + For Each control In lockedControls + control.Enabled = controlsUnlocked + Next + Else + For Each control In lockedControls + control.Enabled = False + Next + End If + progress.Style = If(isBusy, ProgressBarStyle.Marquee, ProgressBarStyle.Blocks) + If Not isBusy Then + progress.Value = 0 + End If + End Sub + + Private Sub Log(message As String) + If InvokeRequired Then + BeginInvoke(Sub() Log(message)) + Return + End If + txtLog.AppendText($"{DateTime.Now:HH:mm:ss} {message}{Environment.NewLine}") + End Sub + + Private Shared Function ConfigPath() As String + Return Path.Combine(Application.StartupPath, ConfigFileName) + End Function + + Private Function LoadOrCreateConfig() As AppConfig + Dim path = ConfigPath() + If File.Exists(path) Then + Dim loaded = JsonSerializer.Deserialize(Of AppConfig)(File.ReadAllText(path), JsonOptions()) + If loaded IsNot Nothing Then + Return loaded + End If + End If + + Dim created = CreateDefaultConfig() + appConfig = created + SetPassword("moxa") + SaveConfig() + Return appConfig + End Function + + Private Sub SaveConfig() + File.WriteAllText(ConfigPath(), JsonSerializer.Serialize(appConfig, JsonOptions())) + End Sub + + Private Shared Function JsonOptions() As JsonSerializerOptions + Return New JsonSerializerOptions With {.WriteIndented = True, .PropertyNameCaseInsensitive = True} + End Function + + Private Function VerifyPassword(password As String) As Boolean + Dim salt = Convert.FromBase64String(appConfig.AdminPasswordSalt) + Dim expected = Convert.FromBase64String(appConfig.AdminPasswordHash) + Using pbkdf2 As New Rfc2898DeriveBytes(password, salt, 100000, HashAlgorithmName.SHA256) + Dim actual = pbkdf2.GetBytes(expected.Length) + Return CryptographicOperations.FixedTimeEquals(actual, expected) + End Using + End Function + + Private Sub SetPassword(password As String) + Dim salt(15) As Byte + RandomNumberGenerator.Fill(salt) + Using pbkdf2 As New Rfc2898DeriveBytes(password, salt, 100000, HashAlgorithmName.SHA256) + appConfig.AdminPasswordSalt = Convert.ToBase64String(salt) + appConfig.AdminPasswordHash = Convert.ToBase64String(pbkdf2.GetBytes(32)) + End Using + End Sub + + Private Function PromptPassword(title As String, prompt As String) As String + Using form As New Form With {.Text = title, .StartPosition = FormStartPosition.CenterParent, .FormBorderStyle = FormBorderStyle.FixedDialog, .MinimizeBox = False, .MaximizeBox = False, .ClientSize = New Size(360, 130)} + Dim label As New Label With {.Text = prompt, .Left = 12, .Top = 12, .Width = 330, .AutoSize = False} + Dim textBox As New TextBox With {.Left = 12, .Top = 42, .Width = 330, .UseSystemPasswordChar = True} + Dim ok As New Button With {.Text = "OK", .Left = 176, .Top = 84, .Width = 78, .DialogResult = DialogResult.OK} + Dim cancel As New Button With {.Text = "Annulla", .Left = 264, .Top = 84, .Width = 78, .DialogResult = DialogResult.Cancel} + form.Controls.AddRange({label, textBox, ok, cancel}) + form.AcceptButton = ok + form.CancelButton = cancel + If form.ShowDialog(Me) = DialogResult.OK Then + Return textBox.Text + End If + End Using + Return Nothing + End Function + + Private Shared Function CreateDefaultConfig() As AppConfig + Return New AppConfig With { + .DefaultCompany = "SECUREX", + .MonitorRefreshSeconds = 30, + .DefaultSettings = New TransferSettings With { + .Port = 10001, + .ConnectionTimeoutSeconds = 10, + .ReceiveIdleTimeoutSeconds = 15, + .SendTerminator = "%", + .LineDelayMs = 100, + .CharDelayMs = 10, + .LinePaced = True, + .CharPaced = False, + .BlockPaced = False, + .BlockSizeBytes = 32, + .BlockDelayMs = 100, + .WaitBeforeSendSeconds = 2, + .WaitAfterSendSeconds = 2 + }, + .Companies = New List(Of CompanyConfig) From { + New CompanyConfig With {.Name = "SECUREX", .Machines = New List(Of MachineConfig) From { + New MachineConfig With {.Name = "M4 - N2LB 300", .IpMoxa = "192.168.0.101", .IpMachineWise = "192.168.0.214", .Note = "OSP-U100L"}, + New MachineConfig With {.Name = "M5 - N3LB 300", .IpMoxa = "192.168.0.146", .IpMachineWise = "192.168.0.47", .Note = "OSP-E100L"}, + New MachineConfig With {.Name = "M8 - CENTRO OKUMA", .IpMoxa = "192.168.0.113", .IpMachineWise = "192.168.0.68", .Note = "OSP-P200M"}, + New MachineConfig With {.Name = "M11 - TORNIO OKUMA LB25I-N", .IpMoxa = "192.168.0.88", .IpMachineWise = "192.168.0.223"}, + New MachineConfig With {.Name = "M12 - TORNIO OKUMA LB 400", .IpMoxa = "192.168.0.99", .IpMachineWise = "192.168.0.66"} + }}, + New CompanyConfig With {.Name = "EMILIA", .Machines = New List(Of MachineConfig) From { + New MachineConfig With {.Name = "M1", .IpMoxa = "192.168.30.8", .IpMachineWise = "192.168.30.221"}, + New MachineConfig With {.Name = "M2", .IpMoxa = "192.168.30.9", .IpMachineWise = "192.168.30.222"}, + New MachineConfig With {.Name = "M3", .IpMoxa = "192.168.30.10", .IpMachineWise = "192.168.30.223"}, + New MachineConfig With {.Name = "M4 - CENTRO LAVORO OKUMA", .IpMoxa = "192.168.30.11", .IpMachineWise = "192.168.30.224"}, + New MachineConfig With {.Name = "M5", .IpMoxa = "192.168.30.12", .IpMachineWise = "192.168.30.225"}, + New MachineConfig With {.Name = "M6", .IpMoxa = "192.168.30.13", .IpMachineWise = "192.168.30.226"} + }}, + New CompanyConfig With {.Name = "SMART", .Machines = New List(Of MachineConfig) From { + New MachineConfig With {.Name = "M1", .IpMoxa = "192.168.35.10", .IpMachineWise = "192.168.35.212"}, + New MachineConfig With {.Name = "M2", .IpMoxa = "192.168.35.11", .IpMachineWise = "192.168.35.211"}, + New MachineConfig With {.Name = "M3", .IpMoxa = "192.168.35.12", .IpMachineWise = "192.168.35.213"}, + New MachineConfig With {.Name = "M4 - CENTRO LAVORO OKUMA", .IpMoxa = "192.168.35.13", .IpMachineWise = "192.168.35.214"} + }} + } + } + End Function +End Class + +Public Class AppConfig + Public Property AdminPasswordHash As String = "" + Public Property AdminPasswordSalt As String = "" + Public Property DefaultCompany As String = "" + Public Property MonitorRefreshSeconds As Integer = 30 + Public Property LastReceiveDirectory As String = "" + Public Property DefaultSettings As New TransferSettings() + Public Property Companies As New List(Of CompanyConfig)() +End Class + +Public Class CompanyConfig + Public Property Name As String = "" + Public Property Machines As New List(Of MachineConfig)() +End Class + +Public Class MachineConfig + Public Property Name As String = "" + Public Property IpMoxa As String = "" + Public Property IpMachineWise As String = "" + Public Property Note As String = "" +End Class + +Friend Class MoxaStatusTarget + Public Property Company As String = "" + Public Property Machine As MachineConfig +End Class + +Public Class TransferSettings + Public Property Port As Integer = 10001 + Public Property ConnectionTimeoutSeconds As Integer = 10 + Public Property ReceiveIdleTimeoutSeconds As Integer = 15 + Public Property SendTerminator As String = "%" + Public Property LineDelayMs As Integer = 100 + Public Property CharDelayMs As Integer = 10 + Public Property LinePaced As Boolean = True + Public Property CharPaced As Boolean = False + Public Property BlockPaced As Boolean = False + Public Property BlockSizeBytes As Integer = 32 + Public Property BlockDelayMs As Integer = 100 + Public Property WaitBeforeSendSeconds As Integer = 2 + Public Property WaitAfterSendSeconds As Integer = 2 +End Class diff --git a/TrasferimentoProgrammiOkuma.App/Program.vb b/TrasferimentoProgrammiOkuma.App/Program.vb new file mode 100644 index 0000000..2076df5 --- /dev/null +++ b/TrasferimentoProgrammiOkuma.App/Program.vb @@ -0,0 +1,14 @@ +Friend Module Program + + + Friend Sub Main(args As String()) + Application.SetHighDpiMode(HighDpiMode.SystemAware) + Application.EnableVisualStyles() + Application.SetCompatibleTextRenderingDefault(False) + Using splash As New SplashForm() + splash.ShowDialog() + End Using + Application.Run(New Form1) + End Sub + +End Module diff --git a/TrasferimentoProgrammiOkuma.App/Properties/PublishProfiles/GiteaClickOnce.pubxml b/TrasferimentoProgrammiOkuma.App/Properties/PublishProfiles/GiteaClickOnce.pubxml new file mode 100644 index 0000000..c8aec92 --- /dev/null +++ b/TrasferimentoProgrammiOkuma.App/Properties/PublishProfiles/GiteaClickOnce.pubxml @@ -0,0 +1,26 @@ + + + + Release + Any CPU + ..\clickonce\ + ..\clickonce\ + true + Web + true + Foreground + 1 + Days + https://gitea.lmcloud.it/livio/LMEMoxaTransfer/raw/branch/main/clickonce/ + https://gitea.lmcloud.it/livio/LMEMoxaTransfer/raw/branch/main/clickonce/ + LMEMoxaTransfer + Compomac SPA + Compomac SPA + 1.0.0.%2a + true + false + false + true + true + + diff --git a/TrasferimentoProgrammiOkuma.App/SplashForm.vb b/TrasferimentoProgrammiOkuma.App/SplashForm.vb new file mode 100644 index 0000000..4d63065 --- /dev/null +++ b/TrasferimentoProgrammiOkuma.App/SplashForm.vb @@ -0,0 +1,72 @@ +Imports System.Reflection + +Public Class SplashForm + Inherits Form + + Private ReadOnly closeTimer As New System.Windows.Forms.Timer() + + Public Sub New() + FormBorderStyle = FormBorderStyle.None + StartPosition = FormStartPosition.CenterScreen + ClientSize = New Size(520, 260) + BackColor = Color.FromArgb(12, 18, 24) + ShowInTaskbar = False + + Dim iconPath = IO.Path.Combine(Application.StartupPath, "Assets", "app-icon.png") + Dim picture As New PictureBox With { + .SizeMode = PictureBoxSizeMode.Zoom, + .Left = 34, + .Top = 42, + .Width = 150, + .Height = 150 + } + If IO.File.Exists(iconPath) Then + picture.Image = Image.FromFile(iconPath) + End If + Controls.Add(picture) + + Dim title As New Label With { + .Text = "LMEMoxaTransfer", + .ForeColor = Color.White, + .Font = New Font("Segoe UI", 24.0F, FontStyle.Bold), + .Left = 205, + .Top = 62, + .Width = 290, + .Height = 44 + } + Controls.Add(title) + + Dim subtitle As New Label With { + .Text = "Trasferimento programmi Okuma via Moxa", + .ForeColor = Color.FromArgb(170, 225, 220), + .Font = New Font("Segoe UI", 10.5F), + .Left = 208, + .Top = 112, + .Width = 290, + .Height = 28 + } + Controls.Add(subtitle) + + Dim versionLabel As New Label With { + .Text = $"Compomac SPA - Livio Merola - v{Assembly.GetExecutingAssembly().GetName().Version}", + .ForeColor = Color.FromArgb(180, 190, 200), + .Font = New Font("Segoe UI", 9.0F), + .Left = 208, + .Top = 154, + .Width = 290, + .Height = 28 + } + Controls.Add(versionLabel) + + closeTimer.Interval = 1400 + AddHandler closeTimer.Tick, Sub() + closeTimer.Stop() + Close() + End Sub + End Sub + + Protected Overrides Sub OnShown(e As EventArgs) + MyBase.OnShown(e) + closeTimer.Start() + End Sub +End Class diff --git a/TrasferimentoProgrammiOkuma.App/TrasferimentoProgrammiOkuma.App.vbproj b/TrasferimentoProgrammiOkuma.App/TrasferimentoProgrammiOkuma.App.vbproj new file mode 100644 index 0000000..5523a0c --- /dev/null +++ b/TrasferimentoProgrammiOkuma.App/TrasferimentoProgrammiOkuma.App.vbproj @@ -0,0 +1,44 @@ + + + + WinExe + net8.0-windows + TrasferimentoProgrammiOkuma.App + LMEMoxaTransfer + LMEMoxaTransfer + LMEMoxaTransfer + Compomac SPA + Livio Merola + 1.0.0 + Assets\app-icon.ico + Sub Main + true + + + + + + + + + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + + + + + + + + + + diff --git a/TrasferimentoProgrammiOkuma.App/moxa-okuma-settings.json b/TrasferimentoProgrammiOkuma.App/moxa-okuma-settings.json new file mode 100644 index 0000000..8a2a882 --- /dev/null +++ b/TrasferimentoProgrammiOkuma.App/moxa-okuma-settings.json @@ -0,0 +1,129 @@ +{ + "AdminPasswordHash": "NZHINCSfx8Y4PePhYcXhbf/psJfiBfrDWRO0AgGnfE0=", + "AdminPasswordSalt": "QY9k9R4AmZvvj13geQ9N8A==", + "DefaultCompany": "SECUREX", + "MonitorRefreshSeconds": 30, + "LastReceiveDirectory": "", + "DefaultSettings": { + "Port": 10001, + "ConnectionTimeoutSeconds": 10, + "ReceiveIdleTimeoutSeconds": 15, + "SendTerminator": "%", + "LineDelayMs": 100, + "CharDelayMs": 10, + "LinePaced": true, + "CharPaced": false, + "BlockPaced": false, + "BlockSizeBytes": 32, + "BlockDelayMs": 100, + "WaitBeforeSendSeconds": 2, + "WaitAfterSendSeconds": 2 + }, + "Companies": [ + { + "Name": "SECUREX", + "Machines": [ + { + "Name": "M4 - N2LB 300", + "IpMoxa": "192.168.0.101", + "IpMachineWise": "192.168.0.214", + "Note": "OSP-U100L" + }, + { + "Name": "M5 - N3LB 300", + "IpMoxa": "192.168.0.146", + "IpMachineWise": "192.168.0.47", + "Note": "OSP-E100L" + }, + { + "Name": "M8 - CENTRO OKUMA", + "IpMoxa": "192.168.0.113", + "IpMachineWise": "192.168.0.68", + "Note": "OSP-P200M" + }, + { + "Name": "M11 - TORNIO OKUMA LB25I-N", + "IpMoxa": "192.168.0.88", + "IpMachineWise": "192.168.0.223", + "Note": "" + }, + { + "Name": "M12 - TORNIO OKUMA LB 400", + "IpMoxa": "192.168.0.99", + "IpMachineWise": "192.168.0.66", + "Note": "" + } + ] + }, + { + "Name": "EMILIA", + "Machines": [ + { + "Name": "M1", + "IpMoxa": "192.168.30.8", + "IpMachineWise": "192.168.30.221", + "Note": "" + }, + { + "Name": "M2", + "IpMoxa": "192.168.30.9", + "IpMachineWise": "192.168.30.222", + "Note": "" + }, + { + "Name": "M3", + "IpMoxa": "192.168.30.10", + "IpMachineWise": "192.168.30.223", + "Note": "" + }, + { + "Name": "M4 - CENTRO LAVORO OKUMA", + "IpMoxa": "192.168.30.11", + "IpMachineWise": "192.168.30.224", + "Note": "" + }, + { + "Name": "M5", + "IpMoxa": "192.168.30.12", + "IpMachineWise": "192.168.30.225", + "Note": "" + }, + { + "Name": "M6", + "IpMoxa": "192.168.30.13", + "IpMachineWise": "192.168.30.226", + "Note": "" + } + ] + }, + { + "Name": "SMART", + "Machines": [ + { + "Name": "M1", + "IpMoxa": "192.168.35.10", + "IpMachineWise": "192.168.35.212", + "Note": "" + }, + { + "Name": "M2", + "IpMoxa": "192.168.35.11", + "IpMachineWise": "192.168.35.211", + "Note": "" + }, + { + "Name": "M3", + "IpMoxa": "192.168.35.12", + "IpMachineWise": "192.168.35.213", + "Note": "" + }, + { + "Name": "M4 - CENTRO LAVORO OKUMA", + "IpMoxa": "192.168.35.13", + "IpMachineWise": "192.168.35.214", + "Note": "" + } + ] + } + ] +} diff --git a/TrasferimentoProgrammiOkuma.sln b/TrasferimentoProgrammiOkuma.sln new file mode 100644 index 0000000..496a181 --- /dev/null +++ b/TrasferimentoProgrammiOkuma.sln @@ -0,0 +1,34 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.0.31903.59 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{F184B08F-C81C-45F6-A57F-5ABD9991F28F}") = "TrasferimentoProgrammiOkuma.App", "TrasferimentoProgrammiOkuma.App\TrasferimentoProgrammiOkuma.App.vbproj", "{C8229DCA-6227-47F6-BE2B-8698EF104AB7}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 + Release|Any CPU = Release|Any CPU + Release|x64 = Release|x64 + Release|x86 = Release|x86 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {C8229DCA-6227-47F6-BE2B-8698EF104AB7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C8229DCA-6227-47F6-BE2B-8698EF104AB7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C8229DCA-6227-47F6-BE2B-8698EF104AB7}.Debug|x64.ActiveCfg = Debug|Any CPU + {C8229DCA-6227-47F6-BE2B-8698EF104AB7}.Debug|x64.Build.0 = Debug|Any CPU + {C8229DCA-6227-47F6-BE2B-8698EF104AB7}.Debug|x86.ActiveCfg = Debug|Any CPU + {C8229DCA-6227-47F6-BE2B-8698EF104AB7}.Debug|x86.Build.0 = Debug|Any CPU + {C8229DCA-6227-47F6-BE2B-8698EF104AB7}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C8229DCA-6227-47F6-BE2B-8698EF104AB7}.Release|Any CPU.Build.0 = Release|Any CPU + {C8229DCA-6227-47F6-BE2B-8698EF104AB7}.Release|x64.ActiveCfg = Release|Any CPU + {C8229DCA-6227-47F6-BE2B-8698EF104AB7}.Release|x64.Build.0 = Release|Any CPU + {C8229DCA-6227-47F6-BE2B-8698EF104AB7}.Release|x86.ActiveCfg = Release|Any CPU + {C8229DCA-6227-47F6-BE2B-8698EF104AB7}.Release|x86.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection +EndGlobal diff --git a/clickonce/Assets/app-icon.ico b/clickonce/Assets/app-icon.ico new file mode 100644 index 0000000..82808fb Binary files /dev/null and b/clickonce/Assets/app-icon.ico differ diff --git a/clickonce/Assets/app-icon.png b/clickonce/Assets/app-icon.png new file mode 100644 index 0000000..2d249d7 Binary files /dev/null and b/clickonce/Assets/app-icon.png differ diff --git a/clickonce/LMEMoxaTransfer.application b/clickonce/LMEMoxaTransfer.application new file mode 100644 index 0000000..cde01f3 --- /dev/null +++ b/clickonce/LMEMoxaTransfer.application @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + oi5lS/pRUXMeVYsdA+pak8lcLEA7jh6YsTIU0y/JugE= + + + + \ No newline at end of file diff --git a/clickonce/LMEMoxaTransfer.deps.json b/clickonce/LMEMoxaTransfer.deps.json new file mode 100644 index 0000000..66ccf38 --- /dev/null +++ b/clickonce/LMEMoxaTransfer.deps.json @@ -0,0 +1,23 @@ +{ + "runtimeTarget": { + "name": ".NETCoreApp,Version=v8.0", + "signature": "" + }, + "compilationOptions": {}, + "targets": { + ".NETCoreApp,Version=v8.0": { + "LMEMoxaTransfer/1.0.0": { + "runtime": { + "LMEMoxaTransfer.dll": {} + } + } + } + }, + "libraries": { + "LMEMoxaTransfer/1.0.0": { + "type": "project", + "serviceable": false, + "sha512": "" + } + } +} \ No newline at end of file diff --git a/clickonce/LMEMoxaTransfer.dll b/clickonce/LMEMoxaTransfer.dll new file mode 100644 index 0000000..14e363d Binary files /dev/null and b/clickonce/LMEMoxaTransfer.dll differ diff --git a/clickonce/LMEMoxaTransfer.dll.manifest b/clickonce/LMEMoxaTransfer.dll.manifest new file mode 100644 index 0000000..d39ae86 --- /dev/null +++ b/clickonce/LMEMoxaTransfer.dll.manifest @@ -0,0 +1,93 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 3v/2JePabKRfZ0iw+t9nxGWMYs3Sn5Xy0OtlTNlsovs= + + + + + + + + + + NM9t8/K8+c6z60ZCFceDhUGaGAJUWggXZzYw0ap3wbk= + + + + + + + + + vrKGEd9S1kba0lj1Dse7EraF35cGx7P3QeERgj0t9sQ= + + + + + + + + + jNXxFh3PRnxb5zDA++I58M/QVn1Fk7FQXBvcrYmB17w= + + + + + + + + + XHvEaaUXpCqo4cYw+kX/5yHnVAqbvb/DMqmvXo45vr8= + + + \ No newline at end of file diff --git a/clickonce/LMEMoxaTransfer.exe b/clickonce/LMEMoxaTransfer.exe new file mode 100644 index 0000000..70e94e8 Binary files /dev/null and b/clickonce/LMEMoxaTransfer.exe differ diff --git a/clickonce/LMEMoxaTransfer.pdb b/clickonce/LMEMoxaTransfer.pdb new file mode 100644 index 0000000..2714e93 Binary files /dev/null and b/clickonce/LMEMoxaTransfer.pdb differ diff --git a/clickonce/LMEMoxaTransfer.runtimeconfig.json b/clickonce/LMEMoxaTransfer.runtimeconfig.json new file mode 100644 index 0000000..ad83f36 --- /dev/null +++ b/clickonce/LMEMoxaTransfer.runtimeconfig.json @@ -0,0 +1,20 @@ +{ + "runtimeOptions": { + "tfm": "net8.0", + "frameworks": [ + { + "name": "Microsoft.NETCore.App", + "version": "8.0.0" + }, + { + "name": "Microsoft.WindowsDesktop.App", + "version": "8.0.0" + } + ], + "configProperties": { + "System.Reflection.Metadata.MetadataUpdater.IsSupported": false, + "System.Runtime.Serialization.EnableUnsafeBinaryFormatterSerialization": true, + "CSWINRT_USE_WINDOWS_UI_XAML_PROJECTIONS": false + } + } +} \ No newline at end of file diff --git a/clickonce/Launcher.exe b/clickonce/Launcher.exe new file mode 100644 index 0000000..5ffcb19 Binary files /dev/null and b/clickonce/Launcher.exe differ diff --git a/clickonce/moxa-okuma-settings.json b/clickonce/moxa-okuma-settings.json new file mode 100644 index 0000000..8a2a882 --- /dev/null +++ b/clickonce/moxa-okuma-settings.json @@ -0,0 +1,129 @@ +{ + "AdminPasswordHash": "NZHINCSfx8Y4PePhYcXhbf/psJfiBfrDWRO0AgGnfE0=", + "AdminPasswordSalt": "QY9k9R4AmZvvj13geQ9N8A==", + "DefaultCompany": "SECUREX", + "MonitorRefreshSeconds": 30, + "LastReceiveDirectory": "", + "DefaultSettings": { + "Port": 10001, + "ConnectionTimeoutSeconds": 10, + "ReceiveIdleTimeoutSeconds": 15, + "SendTerminator": "%", + "LineDelayMs": 100, + "CharDelayMs": 10, + "LinePaced": true, + "CharPaced": false, + "BlockPaced": false, + "BlockSizeBytes": 32, + "BlockDelayMs": 100, + "WaitBeforeSendSeconds": 2, + "WaitAfterSendSeconds": 2 + }, + "Companies": [ + { + "Name": "SECUREX", + "Machines": [ + { + "Name": "M4 - N2LB 300", + "IpMoxa": "192.168.0.101", + "IpMachineWise": "192.168.0.214", + "Note": "OSP-U100L" + }, + { + "Name": "M5 - N3LB 300", + "IpMoxa": "192.168.0.146", + "IpMachineWise": "192.168.0.47", + "Note": "OSP-E100L" + }, + { + "Name": "M8 - CENTRO OKUMA", + "IpMoxa": "192.168.0.113", + "IpMachineWise": "192.168.0.68", + "Note": "OSP-P200M" + }, + { + "Name": "M11 - TORNIO OKUMA LB25I-N", + "IpMoxa": "192.168.0.88", + "IpMachineWise": "192.168.0.223", + "Note": "" + }, + { + "Name": "M12 - TORNIO OKUMA LB 400", + "IpMoxa": "192.168.0.99", + "IpMachineWise": "192.168.0.66", + "Note": "" + } + ] + }, + { + "Name": "EMILIA", + "Machines": [ + { + "Name": "M1", + "IpMoxa": "192.168.30.8", + "IpMachineWise": "192.168.30.221", + "Note": "" + }, + { + "Name": "M2", + "IpMoxa": "192.168.30.9", + "IpMachineWise": "192.168.30.222", + "Note": "" + }, + { + "Name": "M3", + "IpMoxa": "192.168.30.10", + "IpMachineWise": "192.168.30.223", + "Note": "" + }, + { + "Name": "M4 - CENTRO LAVORO OKUMA", + "IpMoxa": "192.168.30.11", + "IpMachineWise": "192.168.30.224", + "Note": "" + }, + { + "Name": "M5", + "IpMoxa": "192.168.30.12", + "IpMachineWise": "192.168.30.225", + "Note": "" + }, + { + "Name": "M6", + "IpMoxa": "192.168.30.13", + "IpMachineWise": "192.168.30.226", + "Note": "" + } + ] + }, + { + "Name": "SMART", + "Machines": [ + { + "Name": "M1", + "IpMoxa": "192.168.35.10", + "IpMachineWise": "192.168.35.212", + "Note": "" + }, + { + "Name": "M2", + "IpMoxa": "192.168.35.11", + "IpMachineWise": "192.168.35.211", + "Note": "" + }, + { + "Name": "M3", + "IpMoxa": "192.168.35.12", + "IpMachineWise": "192.168.35.213", + "Note": "" + }, + { + "Name": "M4 - CENTRO LAVORO OKUMA", + "IpMoxa": "192.168.35.13", + "IpMachineWise": "192.168.35.214", + "Note": "" + } + ] + } + ] +} diff --git a/tools/Compare-CncFiles.ps1 b/tools/Compare-CncFiles.ps1 new file mode 100644 index 0000000..540558a --- /dev/null +++ b/tools/Compare-CncFiles.ps1 @@ -0,0 +1,120 @@ +param( + [Parameter(Mandatory = $true)] + [string]$GoodFile, + + [Parameter(Mandatory = $true)] + [string]$BadFile, + + [int]$ContextBytes = 32, + + [int]$MaxDifferences = 200 +) + +Set-StrictMode -Version Latest +$ErrorActionPreference = "Stop" + +function Format-Byte { + param([byte]$Value) + + if ($Value -eq 0x0D) { return "CR" } + if ($Value -eq 0x0A) { return "LF" } + if ($Value -eq 0x09) { return "TAB" } + if ($Value -eq 0x12) { return "DC2" } + if ($Value -eq 0x14) { return "DC4" } + if ($Value -eq 0x20) { return "SPACE" } + if ($Value -ge 32 -and $Value -le 126) { return [char]$Value } + return "." +} + +function Format-Bytes { + param( + [byte[]]$Bytes, + [int]$Start, + [int]$Count + ) + + if ($Bytes.Length -eq 0) { return "" } + $safeStart = [Math]::Max(0, $Start) + $safeEnd = [Math]::Min($Bytes.Length - 1, $safeStart + $Count - 1) + if ($safeStart -gt $safeEnd) { return "" } + + $hex = New-Object System.Collections.Generic.List[string] + $txt = New-Object System.Collections.Generic.List[string] + + for ($i = $safeStart; $i -le $safeEnd; $i++) { + $hex.Add($Bytes[$i].ToString("X2")) + $txt.Add((Format-Byte $Bytes[$i])) + } + + return ("HEX: " + ($hex -join " ") + [Environment]::NewLine + "TXT: " + ($txt -join " ")) +} + +$goodPath = (Resolve-Path -LiteralPath $GoodFile).Path +$badPath = (Resolve-Path -LiteralPath $BadFile).Path + +$good = [System.IO.File]::ReadAllBytes($goodPath) +$bad = [System.IO.File]::ReadAllBytes($badPath) + +Write-Host "GOOD: $goodPath" +Write-Host "BAD : $badPath" +Write-Host "GOOD length: $($good.Length) bytes" +Write-Host "BAD length: $($bad.Length) bytes" +Write-Host "" + +$limit = [Math]::Min($good.Length, $bad.Length) +$diffCount = 0 + +for ($i = 0; $i -lt $limit; $i++) { + if ($good[$i] -ne $bad[$i]) { + $diffCount++ + if ($diffCount -le $MaxDifferences) { + Write-Host ("Difference #{0} at byte {1} / 0x{1:X}" -f $diffCount, $i) + Write-Host (" GOOD: 0x{0} [{1}]" -f $good[$i].ToString("X2"), (Format-Byte $good[$i])) + Write-Host (" BAD : 0x{0} [{1}]" -f $bad[$i].ToString("X2"), (Format-Byte $bad[$i])) + Write-Host " GOOD context:" + Write-Host (Format-Bytes $good ($i - $ContextBytes) (($ContextBytes * 2) + 1)) + Write-Host " BAD context:" + Write-Host (Format-Bytes $bad ($i - $ContextBytes) (($ContextBytes * 2) + 1)) + Write-Host "" + } + } +} + +if ($good.Length -ne $bad.Length) { + Write-Host "Files have different length." + if ($good.Length -gt $bad.Length) { + Write-Host "GOOD has extra bytes from offset $($bad.Length) / 0x$($bad.Length.ToString("X")):" + Write-Host (Format-Bytes $good $bad.Length ([Math]::Min(256, $good.Length - $bad.Length))) + } else { + Write-Host "BAD has extra bytes from offset $($good.Length) / 0x$($good.Length.ToString("X")):" + Write-Host (Format-Bytes $bad $good.Length ([Math]::Min(256, $bad.Length - $good.Length))) + } + Write-Host "" +} + +Write-Host "Total differing byte positions in common length: $diffCount" + +$badControls = @{} +for ($i = 0; $i -lt $bad.Length; $i++) { + $b = $bad[$i] + $allowed = $b -eq 0x09 -or $b -eq 0x0A -or $b -eq 0x0D -or ($b -ge 0x20 -and $b -le 0x7E) + if (-not $allowed) { + $key = "0x" + $b.ToString("X2") + " " + (Format-Byte $b) + if (-not $badControls.ContainsKey($key)) { + $badControls[$key] = New-Object System.Collections.Generic.List[int] + } + if ($badControls[$key].Count -lt 20) { + $badControls[$key].Add($i) + } + } +} + +Write-Host "" +Write-Host "Suspicious bytes in BAD file, excluding TAB/CR/LF/ASCII printable:" +if ($badControls.Count -eq 0) { + Write-Host " none" +} else { + foreach ($entry in $badControls.GetEnumerator() | Sort-Object Name) { + Write-Host (" {0}: offsets {1}" -f $entry.Key, (($entry.Value | ForEach-Object { "$_ / 0x$($_.ToString("X"))" }) -join ", ")) + } +}