Plancia configurabile su Modbus RTU
Due programmi che condividono uModbusRTU e uGauge: - Console/PlanciaConsole: plancia nautica descritta da file XML, con modalita' plancia e modalita' configurazione. Comandi, spie, selettori, strumenti e allarmi sonori; il bus gira in un thread suo perche' la finestra non si fermi mai. - ProjectPlancia: il programma di prova piu' vecchio, usato per collaudare i canali. Le plance sono in Console/*.xml, la documentazione in Console/LEGGIMI.md. Esclusi dal versionamento i compilati (.exe, .dcu), i file dell'IDE e un audio da 17 MB. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
No files matched your search
@@ -0,0 +1,11 @@
|
|||||||
|
# Progetto Delphi per Windows: i sorgenti stanno a CRLF.
|
||||||
|
* text=auto eol=crlf
|
||||||
|
|
||||||
|
*.png binary
|
||||||
|
*.jpg binary
|
||||||
|
*.bmp binary
|
||||||
|
*.ico binary
|
||||||
|
*.wav binary
|
||||||
|
*.mp3 binary
|
||||||
|
*.res binary
|
||||||
|
*.exe binary
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
# Compilati e roba dell'IDE: si rifanno dal sorgente, non vanno versionati.
|
||||||
|
*.exe
|
||||||
|
*.dcu
|
||||||
|
*.dsk
|
||||||
|
*.local
|
||||||
|
*.identcache
|
||||||
|
*.tvsconfig
|
||||||
|
__history/
|
||||||
|
__recovery/
|
||||||
|
Win32/
|
||||||
|
Win64/
|
||||||
|
*.~*
|
||||||
|
|
||||||
|
# Copie di sicurezza e file di appoggio
|
||||||
|
*.bak
|
||||||
|
*.tmp
|
||||||
|
|
||||||
|
# Stato del programma, non configurazione: dice solo quale plancia era aperta
|
||||||
|
# l'ultima volta su questa macchina.
|
||||||
|
plancia-ultima.txt
|
||||||
|
|
||||||
|
# Impostazioni locali dell'ambiente, personali di questa macchina.
|
||||||
|
.claude/settings.local.json
|
||||||
|
|
||||||
|
# Audio troppo pesante per stare in git: resta solo sul disco.
|
||||||
|
Console/suoni/u_md6l89el2t-alarm-520411.mp3
|
||||||
@@ -0,0 +1,740 @@
|
|||||||
|
# PlanciaConsole
|
||||||
|
|
||||||
|
Plancia configurabile su Modbus RTU / RS485. Due modalità, decise dalla riga di
|
||||||
|
comando; la configurazione vive in un file XML letto all'avvio.
|
||||||
|
|
||||||
|
Riusa `..\uModbusRTU.pas` e `..\uGauge.pas` del programma di test
|
||||||
|
(`ProjectPlancia`): i due progetti condividono quei due file, non ne esistono
|
||||||
|
copie. Il bus gira in un thread a parte (`uModbusWorker.pas`), così la finestra
|
||||||
|
non si ferma mai; vedi "Il bus in un thread a parte".
|
||||||
|
|
||||||
|
## Avvio
|
||||||
|
|
||||||
|
PlanciaConsole.exe aboard, legge plancia.xml
|
||||||
|
PlanciaConsole.exe aboard
|
||||||
|
PlanciaConsole.exe config configurazione
|
||||||
|
PlanciaConsole.exe config -f rotta.xml configurazione su un altro file
|
||||||
|
PlanciaConsole.exe rotta.xml aboard su un altro file
|
||||||
|
|
||||||
|
Senza indicazioni viene riaperta l'ultima plancia usata, e in mancanza di
|
||||||
|
quella `plancia.xml` accanto all'eseguibile. `-f <percorso>`,
|
||||||
|
`-file=<percorso>` o un parametro libero indicano un file diverso.
|
||||||
|
|
||||||
|
### Modalità aboard
|
||||||
|
|
||||||
|
Legge l'XML, apre la porta seriale indicata nel file, si connette da sola e
|
||||||
|
opera. A vista c'è solo la plancia più una barra di stato con porta, baud, LED
|
||||||
|
verde/rosso e ultimo messaggio. Nessun comando di configurazione.
|
||||||
|
|
||||||
|
La finestra si dimensiona sulla plancia. Se il pannello è più grande dello
|
||||||
|
schermo lo zoom si riduce automaticamente per farcelo stare: a bordo è meglio
|
||||||
|
una plancia rimpicciolita che una da raggiungere scorrendo.
|
||||||
|
|
||||||
|
### Cambiare plancia a bordo
|
||||||
|
|
||||||
|
In fondo a destra, nella barra di stato, una casella elenca le plance della
|
||||||
|
stessa cartella del file caricato: tutti gli XML che sono configurazioni di
|
||||||
|
plancia, mostrati con il loro titolo. Scegliendone una:
|
||||||
|
|
||||||
|
- la porta seriale viene chiusa e riaperta con porta e baud del nuovo file;
|
||||||
|
- la finestra si ridimensiona sul nuovo pannello;
|
||||||
|
- **le bobine non vengono toccate**: i relè restano come sono e la nuova plancia
|
||||||
|
ne rilegge lo stato al primo ciclo di polling.
|
||||||
|
|
||||||
|
Il file viene letto per prova prima di lasciare quello in servizio: se è rotto
|
||||||
|
resta la plancia di prima e l'errore compare nella barra di stato.
|
||||||
|
|
||||||
|
La casella compare solo in aboard e solo se nella cartella ci sono almeno due
|
||||||
|
plance. In configurazione si usa "Apri", che chiede anche delle modifiche non
|
||||||
|
salvate. Dopo la scelta il fuoco viene tolto alla casella, così le frecce della
|
||||||
|
tastiera non cambiano plancia per sbaglio.
|
||||||
|
|
||||||
|
### Modalità configuration
|
||||||
|
|
||||||
|
Palette a sinistra, pannello a destra con le proprietà dell'elemento
|
||||||
|
selezionato, barra in alto con Nuovo / Apri / Salva / Salva con nome, griglia e
|
||||||
|
zoom.
|
||||||
|
|
||||||
|
- **Inserire**: trascina una voce della palette sul pannello. L'elemento nasce
|
||||||
|
centrato sul punto di rilascio; se lì c'è già qualcosa scala in diagonale
|
||||||
|
finché trova posto libero, così non si impilano tutti nello stesso punto.
|
||||||
|
- **Spostare**: trascina l'elemento. Un click senza trascinare non lo sposta:
|
||||||
|
il movimento parte solo oltre la soglia di trascinamento di Windows (pochi
|
||||||
|
pixel), e un asse su cui il mouse non si è mosso non viene riagganciato alla
|
||||||
|
griglia.
|
||||||
|
- **Ridimensionare**: trascina una delle otto maniglie dell'elemento
|
||||||
|
selezionato, oppure scrivi larghezza e altezza nel pannello proprietà.
|
||||||
|
- **Ritocchi da tastiera**, sull'elemento selezionato: **Ctrl+freccia** lo
|
||||||
|
sposta di un pixel, **Maiusc+freccia** cambia le misure di un pixel (destra
|
||||||
|
e giù ingrandiscono, sinistra e su rimpiccioliscono). Un pixel esatto anche
|
||||||
|
con la griglia attiva. Non funzionano mentre si scrive in una casella, dove
|
||||||
|
le frecce restano del testo: cliccare l'elemento riporta i tasti sul pannello.
|
||||||
|
- **Annulla / Ripeti**: pulsanti nella barra, oppure **Ctrl+Z** e **Ctrl+Y**
|
||||||
|
(o Ctrl+Maiusc+Z). Coprono spostamenti, misure, proprietà, tipo, elementi
|
||||||
|
aggiunti e cancellati e le impostazioni generali, fino a 100 passi. Le
|
||||||
|
modifiche in fila sulla stessa cosa, entro un secondo e mezzo l'una
|
||||||
|
dall'altra, contano come un passo solo: le cifre di una larghezza digitata,
|
||||||
|
o Ctrl+freccia tenuto premuto. Lo storico si azzera con Nuovo e Apri.
|
||||||
|
Un'immagine rimossa dalla libreria non torna con Annulla: tornano i
|
||||||
|
riferimenti degli elementi, ma il file va riaggiunto.
|
||||||
|
- **Griglia**: con "Griglia 10 px" attiva posizioni e bordi si agganciano a
|
||||||
|
multipli di 10.
|
||||||
|
- **Zoom**: da 50% a 200%. È solo una lente di lavoro: le coordinate salvate
|
||||||
|
nell'XML restano sempre quelle reali al 100%.
|
||||||
|
- **Configurare**: seleziona l'elemento e compila i campi. Le modifiche si
|
||||||
|
vedono subito.
|
||||||
|
- **Eliminare**: pulsante "Elimina elemento" o tasto Canc.
|
||||||
|
- **Chiusura**: chiudendo la finestra la plancia viene **salvata da sola** nel
|
||||||
|
file su cui si stava lavorando, senza chiedere niente. Solo se il
|
||||||
|
salvataggio non riesce (disco pieno, file di sola lettura) viene chiesto se
|
||||||
|
chiudere comunque. Nuovo, Apri e il passaggio in plancia invece continuano a
|
||||||
|
chiedere cosa fare delle modifiche: lì si può volerle buttare.
|
||||||
|
|
||||||
|
**Come viene scritto il file.** Il salvataggio non scrive mai sopra il file
|
||||||
|
buono: scrive un `.tmp` accanto e poi scambia i nomi, tenendo la versione
|
||||||
|
precedente come `.bak`. Così un programma chiuso male, un disco pieno o due
|
||||||
|
copie del programma che salvano insieme non possono lasciare un XML troncato,
|
||||||
|
che alla riapertura non si leggerebbe più. Se un file non si apre, accanto c'è
|
||||||
|
il `.bak` della volta prima: si rinomina e si riparte da lì.
|
||||||
|
|
||||||
|
**Se il file non si è aperto**, chiudendo non ci si salva sopra: il programma
|
||||||
|
avverte e propone "Salva con nome", perché salvare il pannello vuoto
|
||||||
|
cancellerebbe quello che il file contiene.
|
||||||
|
|
||||||
|
All'avvio senza indicazioni viene riaperta **l'ultima plancia usata**: il
|
||||||
|
percorso è ricordato in `plancia-ultima.txt` accanto all'eseguibile, e si
|
||||||
|
aggiorna a ogni apertura, salvataggio e cambio di plancia a bordo. Se quel file
|
||||||
|
non c'è, o la plancia è stata spostata, si riparte da `plancia.xml`. Un file
|
||||||
|
indicato sulla riga di comando ha comunque la precedenza.
|
||||||
|
|
||||||
|
In configurazione la porta seriale **non** viene aperta: si disegna la plancia
|
||||||
|
senza rischiare di comandare relè per sbaglio. Per provare i canali si usa il
|
||||||
|
programma di test `ProjectPlancia`.
|
||||||
|
|
||||||
|
### Passare da una modalità all'altra
|
||||||
|
|
||||||
|
Chi è partito con `config` trova in fondo a destra, nella barra di stato, un
|
||||||
|
pulsante **"Vai in plancia"**: la palette e le proprietà spariscono, la finestra
|
||||||
|
si veste sulla plancia e la porta seriale si apre. Da lì il pulsante diventa
|
||||||
|
**"Torna a configurare"** e riporta indietro, chiudendo la porta.
|
||||||
|
|
||||||
|
Serve a provare quello che si è appena disegnato senza chiudere e riaprire il
|
||||||
|
programma con un'altra riga di comando.
|
||||||
|
|
||||||
|
Tre cose da sapere:
|
||||||
|
|
||||||
|
- Se ci sono modifiche non salvate viene chiesto cosa farne **prima** di passare
|
||||||
|
in plancia, altrimenti si perderebbero in silenzio.
|
||||||
|
- Tornando a configurare la porta seriale viene **chiusa**, e le spie tornano a
|
||||||
|
"dato non disponibile": una plancia che non sta leggendo non deve sembrare in
|
||||||
|
servizio.
|
||||||
|
- Posizione della finestra e zoom di lavoro vengono ritrovati come li si era
|
||||||
|
lasciati, perché in plancia la finestra si ridimensiona sul pannello.
|
||||||
|
|
||||||
|
**Chi è partito in `aboard` non vede il pulsante.** Una plancia avviata in
|
||||||
|
servizio non deve offrire la strada per essere modificata per sbaglio; per
|
||||||
|
configurarla si riavvia con `config`.
|
||||||
|
|
||||||
|
### Modalità notturna
|
||||||
|
|
||||||
|
Accanto c'è il pulsante **"Notte"**, questo disponibile in entrambe le modalità.
|
||||||
|
Scurisce l'intera plancia: fondo quasi nero, immagine di sfondo ricalcolata
|
||||||
|
scura, e ogni colore abbassato alla stessa frazione, così i rapporti fra i
|
||||||
|
colori restano quelli del giorno e il quadro resta leggibile. Serve in
|
||||||
|
navigazione notturna, dove un pannello chiaro a tutto schermo brucia
|
||||||
|
l'adattamento al buio e per qualche minuto non si vede più fuori.
|
||||||
|
|
||||||
|
Le scritte non vengono abbassate ma **sostituite con un ambra**: una serigrafia
|
||||||
|
nera abbassata resterebbe nera, cioè invisibile sul fondo scuro. L'ambra è anche
|
||||||
|
il colore che disturba meno la visione notturna. Vale per etichette, legende dei
|
||||||
|
selettori, marchi e scale dei gauge.
|
||||||
|
|
||||||
|
Si torna al giorno con lo stesso pulsante, che nel frattempo dice "Giorno". La
|
||||||
|
scelta **non** viene salvata nell'XML: è una condizione del momento, non una
|
||||||
|
proprietà della plancia.
|
||||||
|
|
||||||
|
## Tipi di elemento
|
||||||
|
|
||||||
|
| Tipo | Funzione Modbus | Comportamento |
|
||||||
|
|---|---|---|
|
||||||
|
| Pulsante | `WriteSingleCoil` (05) | Chiude alla pressione, riapre al rilascio (es. horn) |
|
||||||
|
| Interruttore | `WriteSingleCoil` (05) + `ReadCoils` (01) | Commuta alla pressione del mouse e resta premuto |
|
||||||
|
| Selettore | `WriteSingleCoil` (05) + `ReadCoils` (01) | Manopola rotativa a 2 o 3 posizioni, anche a ritorno di molla |
|
||||||
|
| Spia | `ReadDiscreteInputs` (02) | Sola lettura, LED di colore configurabile |
|
||||||
|
| Gauge | `ReadHoldingRegisters` (03) | Colonna con scala, soglie e valore |
|
||||||
|
| Display | `ReadHoldingRegisters` (03) | Numero a sette segmenti rossi su fondo nero |
|
||||||
|
| Immagine | nessuna | Grafica decorativa: loghi, sagome, cornici |
|
||||||
|
| Testo | nessuna | Scritta serigrafata sul pannello |
|
||||||
|
|
||||||
|
Il campo `channel` è l'indirizzo Modbus della bobina o del registro, `slave`
|
||||||
|
l'indirizzo del nodo sul bus. Immagine e Testo non hanno né slave né canale.
|
||||||
|
|
||||||
|
Gli interruttori e i selettori rileggono le bobine ad ogni ciclo: se un relè
|
||||||
|
cambia stato per altra via, o una scrittura non va a segno, il comando a video
|
||||||
|
si riallinea. I pulsanti momentanei non vengono riallineati, perché il loro
|
||||||
|
stato dipende dal mouse.
|
||||||
|
|
||||||
|
### All'ingresso in plancia la plancia si allinea al campo
|
||||||
|
|
||||||
|
Entrando in modalità plancia (all'avvio, tornando dalla configurazione o
|
||||||
|
cambiando plancia) la prima lettura serve a mettere tutto nella posizione in
|
||||||
|
cui è il campo, non solo a controllare:
|
||||||
|
|
||||||
|
- **interruttori e selettori** dalle bobine (funzione 01);
|
||||||
|
- **pulsanti** dalle bobine, ma **solo in questa prima lettura**: se un canale
|
||||||
|
momentaneo è rimasto eccitato va mostrato chiuso, mentre dopo lo stato del
|
||||||
|
pulsante lo decide il mouse e rileggerlo lo farebbe lampeggiare;
|
||||||
|
- **spie** dagli ingressi digitali (funzione 02), come sempre.
|
||||||
|
|
||||||
|
**In questa prima lettura le bobine si chiedono una per una**, non a blocco. Un
|
||||||
|
blocco che arriva oltre l'ultimo canale del modulo fallisce tutto, e con lui
|
||||||
|
fallirebbe l'allineamento anche dei comandi su canali che esistono: chiedendo un
|
||||||
|
canale per volta fallisce solo la richiesta del canale che non c'è. Costa un
|
||||||
|
giro più lento, una volta sola. Finito l'allineamento si torna alle richieste
|
||||||
|
raggruppate, che sono quelle che tengono leggero il polling.
|
||||||
|
|
||||||
|
Chiudendo il programma non viene scritto niente sul bus: i relè restano come
|
||||||
|
sono, ed è la plancia che alla ripartenza si adatta a loro. Quando l'allineamento
|
||||||
|
è fatto la barra di stato dice quanti canali sono stati letti, quanti comandi
|
||||||
|
risultano chiusi e quanti canali non hanno risposto; se non risponde nessuna
|
||||||
|
bobina si riprova al ciclo dopo, invece di dare per aperto quello che non si sa.
|
||||||
|
Una spia già in allarme all'avvio fa suonare il suo cicalino.
|
||||||
|
|
||||||
|
### Più comandi sulla stessa bobina
|
||||||
|
|
||||||
|
Lo stesso `slave` e `channel` si possono mettere su più elementi: lo stesso relè
|
||||||
|
comandato da due punti della plancia, o un pulsante e un interruttore sullo
|
||||||
|
stesso circuito. Premendone uno **gli altri si muovono subito**, senza aspettare
|
||||||
|
la rilettura: mezzo secondo di disaccordo fra due comandi che sono la stessa cosa
|
||||||
|
si nota. Vale in tutte le combinazioni, compresi i selettori (per quelli a 3
|
||||||
|
posizioni conta la bobina del lato).
|
||||||
|
|
||||||
|
Le spie no: leggono gli ingressi digitali, che sono un altro spazio di
|
||||||
|
indirizzi, quindi una spia sullo stesso numero di canale di una bobina non è la
|
||||||
|
stessa cosa e non viene toccata.
|
||||||
|
|
||||||
|
Una risposta **più vecchia del comando appena dato** viene scartata. Le letture
|
||||||
|
partono a giri regolari e la risposta può arrivare dopo un click: crederle
|
||||||
|
farebbe tornare indietro il comando per un giro, e si vedrebbe il comando
|
||||||
|
spegnersi, riaccendersi e rispegnersi. Ogni comando segna l'istante in cui ha
|
||||||
|
scritto la sua bobina, e le risposte partite prima di quel momento non vengono
|
||||||
|
applicate a quella bobina.
|
||||||
|
|
||||||
|
**Attenzione**: questo riallineamento è anche il motivo per cui un interruttore
|
||||||
|
può *sembrare* un pulsante. Se la rilettura della bobina risponde 0 — relè
|
||||||
|
assente, indirizzo sbagliato, scrittura non andata a segno — l'interruttore
|
||||||
|
torna su da solo entro un ciclo di polling, e a occhio sembra che non resti
|
||||||
|
premuto. Il tipo dell'elemento non c'entra: si guarda la barra di stato.
|
||||||
|
|
||||||
|
### Cambiare tipo dopo il disegno
|
||||||
|
|
||||||
|
Pulsante, interruttore e spia si scambiano fra loro in qualsiasi momento, dalla
|
||||||
|
casella **Tipo** in cima al pannello proprietà, come si fa con la forma o il
|
||||||
|
canale. Condividono tutti i campi — canale, colori, forma, immagini, etichetta —
|
||||||
|
quindi il passaggio non perde niente. Cambia solo il funzionamento: il pulsante
|
||||||
|
chiude il contatto finché lo tieni premuto, l'interruttore commuta e resta, la
|
||||||
|
spia non si preme e si accende quando l'ingresso è chiuso.
|
||||||
|
|
||||||
|
Serve soprattutto per le lenti colorate che sulla foto di un quadro sembrano
|
||||||
|
pulsanti ma sono allarmi, come FIRE ALARM e OIL STEERING di ZEBRA.
|
||||||
|
|
||||||
|
**Attenzione al canale** passando da comando a spia o viceversa: il numero
|
||||||
|
resta lo stesso ma cambia significato. Per pulsanti e interruttori è una bobina
|
||||||
|
da comandare, per la spia un ingresso digitale da leggere (funzione 02), che di
|
||||||
|
solito sta su un altro modulo. La barra di stato lo ricorda al momento del
|
||||||
|
cambio.
|
||||||
|
|
||||||
|
Cambiando tipo l'elemento riparte da spento, perché un pulsante non viene più
|
||||||
|
riletto dal campo e resterebbe illuminato per sempre. Se l'etichetta era
|
||||||
|
ancora quella predefinita segue il nuovo tipo, altrimenti resta la tua.
|
||||||
|
|
||||||
|
### Selettore rotativo
|
||||||
|
|
||||||
|
La leva **ruota**: passando da una posizione all'altra si vede girare, sempre
|
||||||
|
passando per l'alto, in 130 millisecondi con partenza e arrivo morbidi. È solo
|
||||||
|
quello che si vede: il comando sul bus parte subito, all'inizio del movimento.
|
||||||
|
In configurazione la leva sta ferma dove dice la definizione.
|
||||||
|
|
||||||
|
Si "gira" premendo dal lato verso cui lo si vuole portare: pressione a sinistra
|
||||||
|
della manopola per scendere di una posizione, a destra per salire. La legenda
|
||||||
|
sopra la manopola è testo libero (`OFF ◄ 0 ► ON`, `STOP ◄ 0 ► START`, …).
|
||||||
|
|
||||||
|
Il cablaggio dipende dal numero di posizioni:
|
||||||
|
|
||||||
|
- **2 posizioni**: una bobina, quella di `channel`. Chiusa = posizione destra.
|
||||||
|
- **3 posizioni**: **due bobine**, `channel` e (salvo indicazione) `channel+1`.
|
||||||
|
La prima chiusa = posizione sinistra, la seconda chiusa = destra, entrambe
|
||||||
|
aperte = centro (0).
|
||||||
|
|
||||||
|
Quindi un selettore a 3 posizioni **occupa due canali**: nel numerare i canali
|
||||||
|
va lasciato il buco.
|
||||||
|
|
||||||
|
Per configurarlo: tipo Selettore, **Posizioni = 3**, e in **Canale** la bobina
|
||||||
|
del lato sinistro. La bobina di destra si imposta nel riquadro "Selettore", in
|
||||||
|
**Canale lato destro**: lasciandolo vuoto è il canale successivo (l'etichetta
|
||||||
|
ricorda quale sarebbe), e si compila solo quando sul modulo le due bobine non
|
||||||
|
sono contigue. Nell'XML è l'attributo `channel2`, scritto solo se indicato.
|
||||||
|
|
||||||
|
Il suggerimento a comparsa dell'elemento mostra entrambe le bobine.
|
||||||
|
|
||||||
|
#### Ritorno a molla
|
||||||
|
|
||||||
|
Con **Ritorno a molla** spuntato (nell'XML `momentary="true"`) il selettore non
|
||||||
|
resta dove lo porti: tiene la posizione finché lo tieni premuto e torna al
|
||||||
|
centro appena lasci il mouse, aprendo entrambe le bobine. È il comando di
|
||||||
|
avviamento dei quadri veri, `STOP ◄ 0 ► START`: si tiene su START finché il
|
||||||
|
motore parte, e si molla.
|
||||||
|
|
||||||
|
- Si preme direttamente dal lato voluto, senza passare per lo zero.
|
||||||
|
- Al rilascio torna al centro **sempre**, anche se il mouse è finito fuori
|
||||||
|
dall'elemento e anche se la scrittura di andata era fallita: un comando di
|
||||||
|
avviamento non deve mai restare eccitato.
|
||||||
|
- Finché è premuto il polling non lo sposta, altrimenti una lettura arrivata in
|
||||||
|
quell'istante lo farebbe scattare al centro sotto il dito.
|
||||||
|
- Vale anche a 2 posizioni: premuto = chiusa, lasciato = aperta.
|
||||||
|
|
||||||
|
In `antago.xml` e in `plancia-mfd.xml` è così il selettore **GENERATOR**.
|
||||||
|
|
||||||
|
Le due bobine **non vengono mai chiuse insieme**: a ogni cambio di posizione si
|
||||||
|
apre prima quella del lato opposto e solo dopo si chiude l'altra. Se la seconda
|
||||||
|
scrittura fallisce il selettore resta al centro, con tutte e due aperte. Questa
|
||||||
|
però è una protezione del programma: se i due lati comandano qualcosa che non
|
||||||
|
deve mai partire insieme (due sensi di marcia, due alimentazioni), serve
|
||||||
|
comunque l'interblocco elettrico fra i due relè.
|
||||||
|
|
||||||
|
### Gauge a quadrante
|
||||||
|
|
||||||
|
Con `style="dial"` il gauge non è una colonna ma uno strumento a lancetta, come
|
||||||
|
quelli dei display di plancia: scala su 270 gradi con tacche e numeri, lancetta,
|
||||||
|
e sotto il perno la lettura in cifre con le unità (`decimals` ne fissa i
|
||||||
|
decimali). Se l'elemento è più alto che largo il titolo va sotto al quadrante,
|
||||||
|
altrimenti dentro.
|
||||||
|
|
||||||
|
La fascia della scala è grigia. Con `warnBelow` / `warnAbove` impostati diventa
|
||||||
|
verde nell'intervallo buono e rossa fuori: senza soglie resta grigia, perché un
|
||||||
|
quadrante tutto verde direbbe "tutto a posto" senza saperlo. Senza dato valido
|
||||||
|
la lancetta non c'è e la lettura mostra `---`.
|
||||||
|
|
||||||
|
Stile e decimali del quadrante per ora si impostano solo nell'XML; il pannello
|
||||||
|
proprietà non li mostra ma il salvataggio li conserva.
|
||||||
|
|
||||||
|
### Display
|
||||||
|
|
||||||
|
Mostra il registro scalato in unità reali, allineato a destra sul numero di
|
||||||
|
cifre richiesto, con zeri davanti come i display veri. `digits` è il numero di
|
||||||
|
cifre, `decimals` quante dopo la virgola. Senza dato valido i segmenti restano
|
||||||
|
tutti spenti.
|
||||||
|
|
||||||
|
### Spia
|
||||||
|
|
||||||
|
Il colore da accesa si imposta con l'attributo `color` (`#RRGGBB`): rosso per
|
||||||
|
gli allarmi, verde per i consensi, giallo e arancio per i livelli. Se
|
||||||
|
l'etichetta è vuota il LED — o l'immagine PNG che lo sostituisce — occupa tutto
|
||||||
|
l'elemento, così si possono appoggiare spie piccole sopra una grafica. Vale per
|
||||||
|
qualunque elemento con l'etichetta fuori dal disegno: senza etichetta non viene
|
||||||
|
riservata nessuna fascia.
|
||||||
|
|
||||||
|
#### Allarme sonoro
|
||||||
|
|
||||||
|
Ogni spia può far suonare qualcosa quando si accende. Nel pannello proprietà,
|
||||||
|
riquadro **Allarme sonoro**:
|
||||||
|
|
||||||
|
- **Suona quando si accende**: la casella che accende o spegne l'allarme.
|
||||||
|
- **File**: si sceglie fra i suoni presenti nella cartella **`suoni\` accanto
|
||||||
|
al file XML della plancia** (MP3 e WAV). Come per le immagini, i suoni
|
||||||
|
viaggiano con la plancia quando si copia la cartella.
|
||||||
|
- **Aggiorna**: rilegge la cartella. Serve quando si aggiunge un file mentre il
|
||||||
|
programma è già aperto, senza doverlo riavviare.
|
||||||
|
|
||||||
|
Nell'XML sono gli attributi `alarm="true"` e `sound="cicalino.mp3"` sulla spia.
|
||||||
|
Il nome del file resta salvato anche con l'allarme spento, così si può
|
||||||
|
riaccendere senza riscegliere il suono.
|
||||||
|
|
||||||
|
Come suona:
|
||||||
|
|
||||||
|
- **Parte sul fronte**, quando la spia passa da spenta ad accesa, e poi **va in
|
||||||
|
ciclo**: finito il file ricomincia, e continua finché l'allarme c'è. Un
|
||||||
|
cicalino che suona una volta sola lo si perde se in quel momento si sta
|
||||||
|
guardando altrove. Si ferma quando l'allarme rientra, quando lo si zittisce
|
||||||
|
premendo la spia, o lasciando la plancia.
|
||||||
|
- **Se l'allarme è già attivo all'avvio** suona appena arriva la prima lettura:
|
||||||
|
una plancia che parte con una sentina piena deve dirlo.
|
||||||
|
- **Non blocca niente**: il suono va per conto suo, la plancia resta comandabile.
|
||||||
|
- Due allarmi diversi si sovrappongono; lo stesso allarme che si ripete riparte
|
||||||
|
da capo.
|
||||||
|
- **Quando l'allarme rientra il suono si ferma subito**, senza aspettare la
|
||||||
|
fine del file: serve per i suoni lunghi, una sirena che continua a suonare
|
||||||
|
su un allarme già passato è peggio del silenzio. Se lo stesso file è
|
||||||
|
assegnato a più spie, tace solo quando si è spenta l'ultima ancora accesa.
|
||||||
|
- **Se il file manca o non si può suonare**, la barra di stato lo dice: un
|
||||||
|
cicalino muto senza avviso è peggio di nessun cicalino.
|
||||||
|
- Uscendo dalla plancia (torna a configurare, chiusura) i suoni si fermano.
|
||||||
|
|
||||||
|
#### Zittire un allarme
|
||||||
|
|
||||||
|
**Si preme la spia che sta suonando**: è il gesto che viene naturale, si preme
|
||||||
|
quello che dà fastidio. Ogni pressione allunga il silenzio:
|
||||||
|
|
||||||
|
| Pressioni | Silenzio |
|
||||||
|
|---|---|
|
||||||
|
| 1 | un minuto |
|
||||||
|
| 2 | dieci minuti |
|
||||||
|
| 3 | un'ora |
|
||||||
|
| 4 | allarme di nuovo attivo |
|
||||||
|
|
||||||
|
Le pressioni contano finché il silenzio dura: dopo che è scaduto si ricomincia
|
||||||
|
da un minuto. Il suono in corso si ferma subito alla prima pressione.
|
||||||
|
|
||||||
|
**Il silenzio non spegne la spia**: il LED resta acceso, con una sbarra sopra,
|
||||||
|
perché guardando il quadro si deve capire che quell'allarme c'è ancora e sta
|
||||||
|
suonando a vuoto. Premere una spia spenta, o una senza allarme, non fa niente.
|
||||||
|
|
||||||
|
**Quando il silenzio scade, se la condizione è ancora presente l'allarme torna
|
||||||
|
a suonare**: è il senso di zittire "per un minuto" invece che per sempre.
|
||||||
|
|
||||||
|
In basso a destra, nella barra di stato, compare il tasto **"N allarmi zittiti
|
||||||
|
(tempo) — riattiva"**, che dice quanti sono e quanto manca al primo che torna a
|
||||||
|
suonare. Premendolo si riattivano tutti subito, e quelli ancora presenti
|
||||||
|
suonano di nuovo. Il tasto si vede solo finché c'è almeno un allarme zittito.
|
||||||
|
|
||||||
|
Se l'allarme rientra da solo, il silenzio si azzera con lui: la volta dopo si
|
||||||
|
riparte da un minuto. Tornando a configurare, o cambiando plancia, i silenzi si
|
||||||
|
azzerano tutti.
|
||||||
|
|
||||||
|
In `suoni\` c'è `cicalino.wav`, due bip generati per provare subito; va
|
||||||
|
sostituito con il suono vero.
|
||||||
|
|
||||||
|
La spia accetta anche la forma (`shape`) dei pulsanti, per somigliare alle
|
||||||
|
lenti di allarme dei quadri:
|
||||||
|
|
||||||
|
- **`round`** — ghiera metallica e lente tonda. Spenta la lente resta del suo
|
||||||
|
colore ma scura (`colorOff`, o `color` scurito se manca), accesa prende
|
||||||
|
`color` pieno con un riflesso. La ghiera non cambia: è una spia, non un
|
||||||
|
comando premuto.
|
||||||
|
- **`screen`** — tasto a video che si riempie di `color` quando l'ingresso è
|
||||||
|
chiuso.
|
||||||
|
- le altre forme — il LED tondo di sempre.
|
||||||
|
|
||||||
|
In ogni forma cliccare una spia non fa nulla e non manda niente sul bus.
|
||||||
|
|
||||||
|
### Aspetto di pulsanti, interruttori e spie
|
||||||
|
|
||||||
|
**La lente di un pulsante o di un interruttore non cambia mai colore.** Lo stato
|
||||||
|
si legge solo dal bordo, come su un quadro vero, dove il vetro è sempre dello
|
||||||
|
stesso colore e quello che cambia è la luce dietro.
|
||||||
|
|
||||||
|
- Forma **tonda**: la ghiera attorno alla lente è grigia a riposo e diventa
|
||||||
|
**azzurra luminosa** quando il comando è acceso, o mentre un pulsante è tenuto
|
||||||
|
premuto — più chiara contro la lente e più carica all'orlo, come se la luce
|
||||||
|
venisse da dentro.
|
||||||
|
- Forma **squadrata o a pillola**: non c'è ghiera, quindi è il bordo stesso che
|
||||||
|
si ingrossa e prende lo stesso azzurro.
|
||||||
|
|
||||||
|
Il colore della lente è **`colorOff`**. Su pulsanti e interruttori `color` non
|
||||||
|
tocca più il vetro: resta usato dalle spie, dove il cambio di colore è tutto
|
||||||
|
quello che c'è da vedere. Anche l'etichetta scritta *dentro* al comando resta
|
||||||
|
uguale: prima diventava bianca e in grassetto da acceso, ed era un secondo modo
|
||||||
|
di dire la stessa cosa che ora dice il bordo.
|
||||||
|
|
||||||
|
Tre attributi permettono di far somigliare i comandi a quelli del quadro vero,
|
||||||
|
e si impostano anche dal riquadro "Aspetto" del pannello proprietà:
|
||||||
|
|
||||||
|
- **`shape`** — `auto` (pillola per i pulsanti, squadrato per gli
|
||||||
|
interruttori), `round`, `rect`, `pill`, `screen`. I quadri di bordo hanno
|
||||||
|
quasi sempre pulsanti tondi: con `round` l'elemento viene disegnato con
|
||||||
|
ghiera metallica e lente, come un pulsante illuminato da incasso.
|
||||||
|
`screen` ("a video") è il tasto di un display multifunzione ed è l'eccezione
|
||||||
|
alla regola della lente: spento è un riquadro scuro con il filo chiaro
|
||||||
|
(`colorOff` per tingerlo), acceso **si riempie** del colore `color`. La
|
||||||
|
scritta sopra passa da chiara a nera quando il fondo diventa chiaro.
|
||||||
|
- **`captionPos`** — `center` (etichetta scritta sul comando), `below`,
|
||||||
|
`above`. Sui quadri veri l'etichetta è serigrafata **sotto** al pulsante:
|
||||||
|
con `below` il disegno si restringe per farle posto e il testo resta nero
|
||||||
|
sulla lamiera invece di stare sopra la lente. Le etichette lunghe vanno a
|
||||||
|
capo e la fascia si allarga da sola.
|
||||||
|
- **`color`** e **`colorOff`** — colore della lente accesa e a riposo. Serve
|
||||||
|
perché un pulsante STOP è rosso anche da spento e si limita a illuminarsi:
|
||||||
|
senza `colorOff` la lente a riposo è grigio-azzurra neutra.
|
||||||
|
|
||||||
|
### Testo e marchi
|
||||||
|
|
||||||
|
L'elemento Testo non serve solo alle scritte: con una cornice diventa un
|
||||||
|
marchio serigrafato, disegnato **con il font e non con un'immagine**, quindi
|
||||||
|
nitido a qualsiasi zoom invece di sgranare come farebbe un PNG ingrandito.
|
||||||
|
|
||||||
|
- **`fontName`** — nome del font, per esempio `Times New Roman` o
|
||||||
|
`Arial Black`. Vuoto = quello del pannello. Funziona su qualsiasi elemento,
|
||||||
|
non solo sul Testo.
|
||||||
|
- **`spacing`** — pixel in più fra una lettera e l'altra. I marchi hanno quasi
|
||||||
|
sempre le lettere larghe e senza questo non somigliano.
|
||||||
|
- **`frame`** — `none`, `oval`, `rect`, `round` (rettangolo stondato, cioè a
|
||||||
|
pastiglia). Disegnata con GDI+ in antialiasing, altrimenti un ovale grande
|
||||||
|
verrebbe scalettato.
|
||||||
|
- **`frameWidth`** — spessore del tratto della cornice.
|
||||||
|
- **`color`** — colore di inchiostro e cornice.
|
||||||
|
|
||||||
|
Un marchio su più righe si compone sovrapponendo più elementi: uno con la sola
|
||||||
|
cornice (etichetta vuota), uno con il nome in grande, uno con il sottotitolo
|
||||||
|
piccolo. È così che sono fatti i loghi ZEBRA e ANTAGO negli esempi.
|
||||||
|
|
||||||
|
Per andare a capo dentro un'etichetta si usa ` `, per esempio
|
||||||
|
`caption="PORT ENGINE"`.
|
||||||
|
|
||||||
|
## Immagini
|
||||||
|
|
||||||
|
La libreria immagini è nel pannello di sinistra: "Aggiungi..." accetta PNG, JPG
|
||||||
|
e BMP (selezione multipla), "Rimuovi" toglie l'immagine e la sgancia dagli
|
||||||
|
elementi che la usavano, chiedendo conferma.
|
||||||
|
|
||||||
|
Non è una `TImageList`: quella imporrebbe a tutte le immagini la stessa
|
||||||
|
dimensione, mentre qui un LED da 32 px e un logo da 600 px devono convivere.
|
||||||
|
|
||||||
|
**Nell'XML si salvano nome e percorso, non i pixel.** I file restano sul disco e
|
||||||
|
si possono sostituire senza toccare la configurazione. I percorsi sono relativi
|
||||||
|
alla cartella del file XML quando possibile, così l'intera plancia si trasporta
|
||||||
|
copiando una cartella; "Salva con nome" in un'altra cartella ricalcola i
|
||||||
|
percorsi da solo.
|
||||||
|
|
||||||
|
Dove si usano:
|
||||||
|
|
||||||
|
- **Pulsante, interruttore, spia**: due immagini, a riposo e attiva. Se il campo
|
||||||
|
"immagine a riposo" è vuoto si torna al disegno vettoriale predefinito. Se è
|
||||||
|
valorizzata solo quella a riposo, l'elemento non cambia aspetto quando si
|
||||||
|
attiva.
|
||||||
|
- **Immagine**: una sola, decorativa.
|
||||||
|
- **Sfondo del pannello**: si sceglie in "Generale → Immagine di sfondo", ed è
|
||||||
|
stirata su tutto il pannello.
|
||||||
|
|
||||||
|
Le PNG con trasparenza funzionano: gli elementi non riempiono il proprio sfondo,
|
||||||
|
quindi un logo con alpha si fonde con lo sfondo della plancia.
|
||||||
|
|
||||||
|
L'etichetta continua a essere scritta sopra l'immagine, in bianco con contorno
|
||||||
|
nero perché resti leggibile su fondo chiaro e scuro; sulle spie va in basso per
|
||||||
|
non coprire il LED. Se l'immagine contiene già la scritta, basta svuotare il
|
||||||
|
campo Etichetta.
|
||||||
|
|
||||||
|
Se un file manca o non si apre, nella libreria il nome compare seguito da `[!]`
|
||||||
|
e l'elemento mostra un riquadro tratteggiato in configurazione.
|
||||||
|
|
||||||
|
## Il file XML
|
||||||
|
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<plancia version="1">
|
||||||
|
<connection port="COM1" baud="9600" timeoutMs="500" pollMs="500"/>
|
||||||
|
<panel title="Plancia ZEBRA" width="980" height="560" background="sfondo"/>
|
||||||
|
<images>
|
||||||
|
<image name="sfondo" file="immagini\sfondo.png"/>
|
||||||
|
<image name="btn-off" file="immagini\btn-off.png"/>
|
||||||
|
<image name="btn-on" file="immagini\btn-on.png"/>
|
||||||
|
</images>
|
||||||
|
<elements>
|
||||||
|
<element kind="button" caption="HORN" slave="1" channel="5"
|
||||||
|
left="40" top="60" width="180" height="70"
|
||||||
|
imageOff="btn-off" imageOn="btn-on"/>
|
||||||
|
<element kind="image" caption="" image="logo-zebra"
|
||||||
|
left="620" top="440" width="320" height="90"/>
|
||||||
|
<element kind="gauge" caption="Serbatoio" slave="5" channel="1"
|
||||||
|
left="640" top="200" width="130" height="200"
|
||||||
|
rawMin="0" rawMax="4095" engMin="0" engMax="100"
|
||||||
|
units="%" warnBelow="15" warnAbove="1E30"/>
|
||||||
|
</elements>
|
||||||
|
</plancia>
|
||||||
|
|
||||||
|
`kind` vale `button`, `switch`, `rotary`, `lamp`, `gauge`, `display`, `image`
|
||||||
|
o `label`.
|
||||||
|
|
||||||
|
- `fontSize`: altezza del testo in pixel logici; assente = font del pannello.
|
||||||
|
- `imageOff` / `imageOn`: nomi presi da `<images>`; solo per pulsanti,
|
||||||
|
interruttori e spie.
|
||||||
|
- `image`: l'immagine dell'elemento decorativo.
|
||||||
|
- `background` su `<panel>`: immagine di sfondo.
|
||||||
|
- `ink` su `<panel>`: colore della serigrafia, `#RRGGBB`, nero se assente.
|
||||||
|
Vale per etichette sotto/sopra i comandi, legende e titoli di selettori,
|
||||||
|
display e quadranti, e per i `label` senza `color`. Le plance a fondo scuro
|
||||||
|
lo mettono chiaro. Si imposta solo nell'XML.
|
||||||
|
- `style` (`bar` o `dial`) e `decimals`: aspetto del `gauge`, vedi sopra.
|
||||||
|
- `color` / `colorOff`: colore acceso e a riposo di spie, pulsanti e
|
||||||
|
interruttori, `#RRGGBB`. Su `label` è il colore di inchiostro e cornice.
|
||||||
|
- `shape` e `captionPos`: forma del comando e posizione dell'etichetta.
|
||||||
|
- `fontName` e `spacing`: font e spaziatura fra le lettere.
|
||||||
|
- `frame` e `frameWidth`: cornice attorno a un `label`.
|
||||||
|
- `alarm` e `sound`: solo per `lamp`, allarme sonoro all'accensione; il file
|
||||||
|
sta nella cartella `suoni\` accanto all'XML.
|
||||||
|
- `positions` (2 o 3), `legend`, `channel2` e `momentary`: solo per `rotary`.
|
||||||
|
`channel2` è la bobina del lato destro di un selettore a 3 posizioni;
|
||||||
|
assente = `channel+1`. `momentary="true"` è il ritorno a molla.
|
||||||
|
- `digits` e `decimals`: solo per `display`.
|
||||||
|
- `raw*`, `eng*` e `units` valgono per `gauge` e `display`:
|
||||||
|
`rawMin`/`rawMax` sono il fondo scala del modulo (4095 = ADC a 12 bit),
|
||||||
|
`engMin`/`engMax` i valori reali corrispondenti.
|
||||||
|
- `warnBelow` / `warnAbove`: solo per `gauge`, sono le soglie oltre le quali la
|
||||||
|
colonna diventa rossa (`-1E30` e `1E30` significano "soglia non impostata").
|
||||||
|
|
||||||
|
I numeri decimali si scrivono con il punto. Il file si può modificare a mano:
|
||||||
|
un attributo mancante prende il valore di default. Per i caratteri speciali
|
||||||
|
nelle legende si usano le entità XML, per esempio `◀` e `▶` per le
|
||||||
|
frecce ◄ ►.
|
||||||
|
|
||||||
|
## File di esempio
|
||||||
|
|
||||||
|
- `esempio-plancia.xml` — solo elementi vettoriali, nessuna immagine.
|
||||||
|
- `esempio-immagini.xml` — usa la cartella `immagini\`: sfondo, logo con
|
||||||
|
trasparenza, pulsante e LED con grafica. Le immagini sono segnaposto generate
|
||||||
|
per la prova, da sostituire con quelle vere.
|
||||||
|
- `antago.xml` — riproduzione del quadro elettrico ANTAGO, ricavata dalla foto
|
||||||
|
del quadro reale. Il pannello è 1270×950 come la foto, così ogni elemento sta
|
||||||
|
dove sta sul quadro vero.
|
||||||
|
- `zebra.xml` — riproduzione della pulsantiera ZEBRA: 7 colonne × 6 righe di
|
||||||
|
pulsanti tondi illuminati, i due selettori delle ventole, i comandi motore
|
||||||
|
con gli ovali PORT/STBD ENGINE e il selettore PARALLEL a tre posizioni.
|
||||||
|
La foto è in prospettiva, quindi la griglia è stata ridisegnata dritta
|
||||||
|
(passo 120 in orizzontale, 145 in verticale); etichette, forme e colori delle
|
||||||
|
lenti vengono dalla foto.
|
||||||
|
|
||||||
|
- `plancia-mfd.xml` — tutti i comandi, le spie e le misure di ZEBRA e ANTAGO
|
||||||
|
su una sola plancia, nello stile della foto `immagini\Esempio plancia.jpg`:
|
||||||
|
cornice scura con tasti a video ai lati, schermo nero con quattro quadranti,
|
||||||
|
i due display dei caricabatterie, il sinottico della barca con le spie e i
|
||||||
|
tasti illuminati di motori e servizi; sotto, due file di selettori. Pannello
|
||||||
|
1900×1080. Gli indirizzi di ZEBRA sono quelli di `zebra.xml`; **le bobine
|
||||||
|
di ANTAGO sono spostate da slave 1 a slave 4**, perché sullo slave 1 si
|
||||||
|
sovrapponevano a quelle di ZEBRA, e i due comandi di prova "Luce" e "Horn"
|
||||||
|
di `antago.xml` (che stavano sul canale 0, lo stesso del selettore
|
||||||
|
VOLTMETER) sono sui canali 26 e 27 dello slave 4.
|
||||||
|
|
||||||
|
In tutti **slave e canali sono inventati** e vanno rimappati sui moduli
|
||||||
|
reali prima di collegare il bus.
|
||||||
|
|
||||||
|
## Strumenti
|
||||||
|
|
||||||
|
`strumenti\` contiene gli script PowerShell usati per preparare la grafica dei
|
||||||
|
due quadri:
|
||||||
|
|
||||||
|
| Script | Cosa fa |
|
||||||
|
|---|---|
|
||||||
|
| `ruota.ps1` | Raddrizza una foto scattata in verticale |
|
||||||
|
| `ritaglia-grafica.ps1` | Ritaglia un pezzo di serigrafia dalla foto e rende trasparente il fondo chiaro, lasciando solo il tratto |
|
||||||
|
| `genera-sfondo.ps1` | Disegna la lamiera del pannello con cornice e viti, di qualsiasi misura |
|
||||||
|
| `genera-mfd.ps1` | Disegna cornice e schermo di `plancia-mfd.xml` e la sagoma della barca ANTAGO in chiaro per il fondo nero |
|
||||||
|
|
||||||
|
Il ritaglio dalla foto conviene solo per i disegni che non si possono
|
||||||
|
ricostruire, come la sagoma della barca del quadro ANTAGO. **Le scritte no**:
|
||||||
|
i marchi ZEBRA e ANTAGO e gli ovali PORT/STBD ENGINE sono elementi Testo con
|
||||||
|
font e cornice, non immagini, così restano nitidi a ogni ingrandimento.
|
||||||
|
|
||||||
|
### Diagnostica del bus
|
||||||
|
|
||||||
|
Due programmi a riga di comando servono quando un modulo non si legge. Vanno
|
||||||
|
lanciati **con PlanciaConsole chiusa**, perché la porta si apre una volta sola.
|
||||||
|
Si ricompilano con i `build-*.bat` accanto ai sorgenti.
|
||||||
|
|
||||||
|
| Programma | Cosa fa |
|
||||||
|
|---|---|
|
||||||
|
| `scanbus.exe [porta] [baud] [ultimoIndirizzo]` | Interroga gli indirizzi uno per uno e dice chi risponde, con quali funzioni e che dati |
|
||||||
|
| `dumpframe.exe [porta] [baud] [slave] [funzione] [start] [quantità]` | Manda una sola richiesta e stampa i byte grezzi della risposta con la verifica della CRC |
|
||||||
|
| `monitorio.exe [porta] [baud] [slave] [canali] [cicli]` | Mostra gli ingressi digitali in tempo reale, una riga ad ogni cambiamento |
|
||||||
|
| `ringtest.exe [file.bmp]` | Disegna i comandi tondi spenti e accesi a tre misure e salva l'immagine: serve a giudicare una modifica ai colori guardandola |
|
||||||
|
|
||||||
|
`scanbus` serve a trovare l'indirizzo di un modulo appena montato. `dumpframe`
|
||||||
|
serve quando `scanbus` dà risultati incoerenti: mostrando il frame byte per byte
|
||||||
|
distingue un modulo che non risponde da due moduli che rispondono insieme.
|
||||||
|
|
||||||
|
**Due nodi con lo stesso indirizzo** è il guasto più frequente quando si
|
||||||
|
aggiunge un modulo: i moduli escono quasi tutti con indirizzo 1, e sul bus le
|
||||||
|
due risposte si sovrappongono. Il sintomo è una risposta di lunghezza variabile,
|
||||||
|
spesso con byte a zero in testa, e la CRC che non torna.
|
||||||
|
|
||||||
|
## Il bus in un thread a parte
|
||||||
|
|
||||||
|
Il thread principale non parla mai con la porta seriale. Tutto il Modbus vive
|
||||||
|
in un thread suo (`uModbusWorker.pas`), che apre la COM, fa le letture e le
|
||||||
|
scritture, e lascia i risultati in una cassetta condivisa. La finestra li
|
||||||
|
ritira con il suo timer. Così un timeout — mezzo secondo per richiesta — costa
|
||||||
|
tempo al thread del bus e non alla plancia, che resta trascinabile, ridisegnabile
|
||||||
|
e cliccabile anche con tutti i moduli staccati.
|
||||||
|
|
||||||
|
**Un thread solo, non uno per le letture e uno per le scritture.** Su RS485 il
|
||||||
|
filo è uno: due richieste insieme si sovrappongono e le risposte arrivano
|
||||||
|
mescolate. Chi comanda deve comunque aspettare la fine della richiesta in
|
||||||
|
corso, quindi un secondo thread aggiungerebbe solo un lucchetto attorno alla
|
||||||
|
porta. La reattività dei comandi si ottiene con la **precedenza**: la coda
|
||||||
|
delle scritture viene svuotata prima di ogni lettura, non solo a ogni giro,
|
||||||
|
quindi un pulsante premuto parte al massimo dopo la richiesta in corso.
|
||||||
|
|
||||||
|
Come si comporta:
|
||||||
|
|
||||||
|
- **Cliccare un comando torna subito.** La scrittura va in coda; l'elemento si
|
||||||
|
accende fidandosi. Se la scrittura non va a segno lo dice la barra di stato,
|
||||||
|
e la rilettura delle bobine rimette l'elemento a posto entro un ciclo.
|
||||||
|
- **L'ordine è garantito**: le due bobine di un selettore a 3 posizioni, o la
|
||||||
|
pressione e il rilascio di un pulsante, partono nella sequenza in cui sono
|
||||||
|
state messe in coda.
|
||||||
|
- **La porta si riapre da sola.** Se non c'è, o se cade (adattatore USB
|
||||||
|
staccato), il thread riprova ogni due secondi: non serve più riavviare il
|
||||||
|
programma.
|
||||||
|
- **Chiudendo**, l'attesa del thread dura al massimo quanto la richiesta in
|
||||||
|
corso.
|
||||||
|
|
||||||
|
## Aggiungere slave e canali
|
||||||
|
|
||||||
|
Niente nel programma è legato ai moduli montati adesso: una plancia si può
|
||||||
|
disegnare prima, e i moduli si aggiungono quando arrivano.
|
||||||
|
|
||||||
|
- **Slave**: qualsiasi indirizzo da 1 a 247, quanti se ne vuole sullo stesso
|
||||||
|
bus. Ogni slave nuovo aggiunge solo le sue richieste al giro di polling.
|
||||||
|
- **Canali**: qualsiasi numero da 0 a 65535, senza doverli tenere contigui.
|
||||||
|
Canali molto distanti sullo stesso slave allargano però l'intervallo letto
|
||||||
|
(vedi Polling), quindi conviene raggrupparli.
|
||||||
|
- **Limiti di una singola richiesta**, imposti dal protocollo: 2000 bit per
|
||||||
|
bobine e ingressi, 125 registri. Oltre, la richiesta viene saltata e la barra
|
||||||
|
di stato lo dice: si spezza l'intervallo o si sposta l'elemento su un altro
|
||||||
|
slave.
|
||||||
|
- **Nuove porte**: la porta è una per plancia (`<connection port=...>`). Due
|
||||||
|
bus separati si fanno con due plance, e si passa dall'una all'altra con la
|
||||||
|
casella in basso a destra.
|
||||||
|
|
||||||
|
**Una richiesta che non riesce si divide da sola.** Se la lettura di un blocco
|
||||||
|
di bobine fallisce, il programma la spezza a metà e riprova con le due metà, e
|
||||||
|
così via: un modulo da 32 canali interrogato fino al 38 finisce per farsi
|
||||||
|
leggere i suoi 32 in poche richieste, invece di non dare più niente. Ogni
|
||||||
|
divisione compare nella barra di stato. Le divisioni imparate valgono finché
|
||||||
|
non si cambia plancia o non si riapre la porta.
|
||||||
|
|
||||||
|
**Le richieste che non rispondono non danno fastidio.** Dopo tre errori di
|
||||||
|
fila una richiesta muta viene messa in pausa e riprovata ogni cinque secondi,
|
||||||
|
invece di costarne il timeout a ogni giro: così una plancia disegnata in
|
||||||
|
anticipo resta scorrevole e i moduli che rispondono vengono letti alla velocità
|
||||||
|
giusta. Appena il modulo viene collegato riparte da solo, entro quei cinque
|
||||||
|
secondi. La barra di stato distingue i due casi: `Timeout: nessuna risposta` è
|
||||||
|
una richiesta che si sta ancora provando, `non risponde, riprovo fra 4 s` è una
|
||||||
|
messa in pausa. La pausa si azzera da sola quando si cambia plancia, quando si
|
||||||
|
modificano i canali in configurazione e quando la porta viene riaperta.
|
||||||
|
|
||||||
|
La pausa vale **per singola richiesta**, non per modulo: sullo stesso nodo le
|
||||||
|
bobine possono rispondere benissimo mentre gli ingressi, che quel modulo non ha,
|
||||||
|
danno errore. Mettere in pausa tutto il nodo per colpa degli ingressi
|
||||||
|
lascerebbe la plancia cieca proprio su quello che si legge.
|
||||||
|
|
||||||
|
## Polling
|
||||||
|
|
||||||
|
Ad ogni ciclo (`pollMs`) gli elementi vengono raggruppati per slave e tipo, e
|
||||||
|
per ogni gruppo parte **una sola** richiesta che copre i canali da quello più
|
||||||
|
basso a quello più alto. Aggiungere elementi contigui non aumenta il traffico
|
||||||
|
sul bus; canali molto distanti sullo stesso slave sì, perché allargano
|
||||||
|
l'intervallo letto.
|
||||||
|
|
||||||
|
Ogni richiesta va per conto suo: se un modulo non risponde, gli altri gruppi
|
||||||
|
vengono letti comunque. Solo le spie e i gauge di **quella** richiesta passano a
|
||||||
|
"dato non disponibile"; il resto della plancia continua a leggere. Interruttori
|
||||||
|
e selettori invece non si azzerano, perché la loro posizione è un comando dato,
|
||||||
|
non una misura.
|
||||||
|
|
||||||
|
L'errore finisce nella barra di stato dicendo **cosa** non si è potuto leggere:
|
||||||
|
slave, funzione e intervallo di canali, per esempio
|
||||||
|
|
||||||
|
Lettura fallita su 4 richieste, la prima: slave 3, ingressi 0-14 (FC02): timeout
|
||||||
|
|
||||||
|
Così si capisce subito se manca un modulo, se l'indirizzo è sbagliato o se si
|
||||||
|
sta leggendo oltre i canali che il modulo ha (tipico: un modulo da 8 bobine
|
||||||
|
interrogato per 37, perché un elemento ha un canale alto). Il polling continua
|
||||||
|
e si riallinea da solo appena il bus risponde.
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
program PlanciaConsole;
|
||||||
|
|
||||||
|
uses
|
||||||
|
Vcl.Forms,
|
||||||
|
uConsoleMain in 'uConsoleMain.pas' {ConsoleForm},
|
||||||
|
uPlanciaElements in 'uPlanciaElements.pas',
|
||||||
|
uPlanciaConfig in 'uPlanciaConfig.pas',
|
||||||
|
uImageLib in 'uImageLib.pas',
|
||||||
|
uModbusRTU in '..\uModbusRTU.pas',
|
||||||
|
uModbusWorker in 'uModbusWorker.pas',
|
||||||
|
uAlarmSound in 'uAlarmSound.pas',
|
||||||
|
uGauge in '..\uGauge.pas';
|
||||||
|
|
||||||
|
{$R *.res}
|
||||||
|
|
||||||
|
begin
|
||||||
|
Application.Initialize;
|
||||||
|
Application.MainFormOnTaskbar := True;
|
||||||
|
Application.CreateForm(TConsoleForm, ConsoleForm);
|
||||||
|
Application.Run;
|
||||||
|
end.
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||||
|
<PropertyGroup>
|
||||||
|
<ProjectGuid>{B7C4D1E2-3F5A-4B6C-8D9E-0A1B2C3D4E5F}</ProjectGuid>
|
||||||
|
<ProjectVersion>19.6</ProjectVersion>
|
||||||
|
<FrameworkType>VCL</FrameworkType>
|
||||||
|
<MainSource>PlanciaConsole.dpr</MainSource>
|
||||||
|
<Base>True</Base>
|
||||||
|
<Config Condition="'$(Config)'==''">Debug</Config>
|
||||||
|
<Platform Condition="'$(Platform)'==''">Win32</Platform>
|
||||||
|
<TargetedPlatforms>1</TargetedPlatforms>
|
||||||
|
<AppType>Application</AppType>
|
||||||
|
<ProjectName Condition="'$(ProjectName)'==''">PlanciaConsole</ProjectName>
|
||||||
|
</PropertyGroup>
|
||||||
|
<PropertyGroup Condition="'$(Config)'=='Base' or '$(Base)'!=''">
|
||||||
|
<Base>true</Base>
|
||||||
|
</PropertyGroup>
|
||||||
|
<PropertyGroup Condition="'$(Config)'=='Debug' or '$(Cfg_1)'!=''">
|
||||||
|
<Cfg_1>true</Cfg_1>
|
||||||
|
<CfgParent>Base</CfgParent>
|
||||||
|
<Base>true</Base>
|
||||||
|
</PropertyGroup>
|
||||||
|
<PropertyGroup Condition="('$(Platform)'=='Win32' and '$(Cfg_1)'=='true') or '$(Cfg_1_Win32)'!=''">
|
||||||
|
<Cfg_1_Win32>true</Cfg_1_Win32>
|
||||||
|
<CfgParent>Cfg_1</CfgParent>
|
||||||
|
<Cfg_1>true</Cfg_1>
|
||||||
|
<Base>true</Base>
|
||||||
|
</PropertyGroup>
|
||||||
|
<PropertyGroup Condition="'$(Config)'=='Release' or '$(Cfg_2)'!=''">
|
||||||
|
<Cfg_2>true</Cfg_2>
|
||||||
|
<CfgParent>Base</CfgParent>
|
||||||
|
<Base>true</Base>
|
||||||
|
</PropertyGroup>
|
||||||
|
<PropertyGroup Condition="'$(Base)'!=''"/>
|
||||||
|
<PropertyGroup Condition="'$(Cfg_1)'!=''">
|
||||||
|
<DCC_Define>DEBUG;$(DCC_Define)</DCC_Define>
|
||||||
|
<DCC_DebugInformation>1</DCC_DebugInformation>
|
||||||
|
<DCC_Optimize>false</DCC_Optimize>
|
||||||
|
</PropertyGroup>
|
||||||
|
<PropertyGroup Condition="'$(Cfg_1_Win32)'!=''">
|
||||||
|
<BT_BuildType>Debug</BT_BuildType>
|
||||||
|
</PropertyGroup>
|
||||||
|
<PropertyGroup Condition="'$(Cfg_2)'!=''">
|
||||||
|
<DCC_DebugInformation>0</DCC_DebugInformation>
|
||||||
|
</PropertyGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<DelphiCompile Include="$(MainSource)">
|
||||||
|
<MainSource>MainSource</MainSource>
|
||||||
|
</DelphiCompile>
|
||||||
|
<DCCReference Include="uConsoleMain.pas">
|
||||||
|
<Form>ConsoleForm</Form>
|
||||||
|
<FormType>dfm</FormType>
|
||||||
|
</DCCReference>
|
||||||
|
<DCCReference Include="uPlanciaElements.pas"/>
|
||||||
|
<DCCReference Include="uPlanciaConfig.pas"/>
|
||||||
|
<DCCReference Include="uImageLib.pas"/>
|
||||||
|
<DCCReference Include="..\uModbusRTU.pas"/>
|
||||||
|
<DCCReference Include="uModbusWorker.pas"/>
|
||||||
|
<DCCReference Include="uAlarmSound.pas"/>
|
||||||
|
<DCCReference Include="..\uGauge.pas"/>
|
||||||
|
<BuildConfiguration Include="Base">
|
||||||
|
<Key>Base</Key>
|
||||||
|
</BuildConfiguration>
|
||||||
|
<BuildConfiguration Include="Debug">
|
||||||
|
<Key>Cfg_1</Key>
|
||||||
|
<CfgParent>Base</CfgParent>
|
||||||
|
</BuildConfiguration>
|
||||||
|
<BuildConfiguration Include="Release">
|
||||||
|
<Key>Cfg_2</Key>
|
||||||
|
<CfgParent>Base</CfgParent>
|
||||||
|
</BuildConfiguration>
|
||||||
|
</ItemGroup>
|
||||||
|
<Import Project="$(BDS)\Bin\CodeGear.Delphi.Targets" Condition="Exists('$(BDS)\Bin\CodeGear.Delphi.Targets')"/>
|
||||||
|
<Import Project="$(APPDATA)\Embarcadero\$(BDSAPPDATABASEDIR)\$(PRODUCTVERSION)\UserTools.proj" Condition="Exists('$(APPDATA)\Embarcadero\$(BDSAPPDATABASEDIR)\$(PRODUCTVERSION)\UserTools.proj')"/>
|
||||||
|
<ProjectExtensions>
|
||||||
|
<Borland.Personality>Delphi.Personality.12</Borland.Personality>
|
||||||
|
<Borland.ProjectType>VCLApplication</Borland.ProjectType>
|
||||||
|
<BorlandProject>
|
||||||
|
<Delphi.Personality>
|
||||||
|
<Source>
|
||||||
|
<Source Name="MainSource">PlanciaConsole.dpr</Source>
|
||||||
|
</Source>
|
||||||
|
</Delphi.Personality>
|
||||||
|
<Platforms>
|
||||||
|
<Platform value="Win32">True</Platform>
|
||||||
|
</Platforms>
|
||||||
|
</BorlandProject>
|
||||||
|
<ProjectFileVersion>12</ProjectFileVersion>
|
||||||
|
</ProjectExtensions>
|
||||||
|
</Project>
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<plancia version="1">
|
||||||
|
<connection port="COM1" baud="9600" timeoutMs="500" pollMs="500"/>
|
||||||
|
<panel title="ANTAGO - Quadro elettrico" width="1270" height="950" background="antago-sfondo"/>
|
||||||
|
<images>
|
||||||
|
<image name="antago-sfondo" file="immagini\antago-sfondo.png"/>
|
||||||
|
<image name="antago-barca" file="immagini\antago-barca.png"/>
|
||||||
|
</images>
|
||||||
|
<elements>
|
||||||
|
<element kind="display" caption="230V AC VOLTMETER" slave="0" channel="0" left="50" top="52" width="240" height="138" fontSize="17" digits="3" decimals="0" rawMin="0" rawMax="4095" engMin="0" engMax="300" units="V" warnBelow="-1E30" warnAbove="1E30"/>
|
||||||
|
<element kind="display" caption="230V AC AMMETER" slave="0" channel="0" left="345" top="52" width="240" height="138" fontSize="17" digits="4" decimals="1" rawMin="0" rawMax="4095" engMin="0" engMax="100" units="A" warnBelow="-1E30" warnAbove="1E30"/>
|
||||||
|
<element kind="display" caption="SERVICES BATT. AMMETER" slave="0" channel="0" left="645" top="52" width="240" height="138" fontSize="17" digits="4" decimals="1" rawMin="0" rawMax="4095" engMin="-100" engMax="100" units="A" warnBelow="-1E30" warnAbove="1E30"/>
|
||||||
|
<element kind="display" caption="24V DC VOLTMETER" slave="0" channel="0" left="940" top="50" width="245" height="138" fontSize="17" digits="4" decimals="1" rawMin="0" rawMax="4095" engMin="0" engMax="40" units="V" warnBelow="-1E30" warnAbove="1E30"/>
|
||||||
|
<element kind="display" caption="SERVICES BATT. CHARGER" slave="0" channel="0" left="345" top="222" width="240" height="123" fontSize="17" digits="4" decimals="1" rawMin="0" rawMax="4095" engMin="0" engMax="100" units="A" warnBelow="-1E30" warnAbove="1E30"/>
|
||||||
|
<element kind="display" caption="START BATT. CHARGER" slave="0" channel="0" left="645" top="222" width="240" height="123" fontSize="17" digits="4" decimals="1" rawMin="0" rawMax="4095" engMin="0" engMax="100" units="A" warnBelow="-1E30" warnAbove="1E30"/>
|
||||||
|
<element kind="rotary" caption="VOLTMETER" slave="1" channel="0" left="40" top="219" width="250" height="156" fontSize="15" positions="3" legend="S.SUPPLY ◀ 0 ▶ GENERAT."/>
|
||||||
|
<element kind="rotary" caption="VOLTMETER" slave="1" channel="2" left="938" top="219" width="250" height="156" fontSize="15" positions="3" legend="SERV.BATT. ◀ 0 ▶ START BATT."/>
|
||||||
|
<element kind="rotary" caption="GENERATOR" slave="1" channel="4" left="0" top="392" width="170" height="156" fontSize="15" positions="3" legend="STOP ◀ 0 ▶ START" momentary="true"/>
|
||||||
|
<element kind="rotary" caption="AIR CONDITIONER" slave="1" channel="6" left="173" top="392" width="170" height="156" fontSize="15" positions="2" legend="OFF ◀ ▶ ON"/>
|
||||||
|
<element kind="rotary" caption="MAIN POWER" slave="1" channel="8" left="355" top="392" width="170" height="156" fontSize="15" positions="3" legend="S.SUPPLY ◀ 0 ▶ GEN."/>
|
||||||
|
<element kind="rotary" caption="ALARMS" slave="1" channel="10" left="522" top="392" width="170" height="156" fontSize="15" positions="3" legend="TACIT ◀ 0 ▶ TEST" momentary="true"/>
|
||||||
|
<element kind="rotary" caption="SEWAGE PUMP" slave="1" channel="12" left="704" top="392" width="170" height="156" fontSize="15" positions="2" legend="OFF ◀ ▶ ON"/>
|
||||||
|
<element kind="rotary" caption="SERV. BATT." slave="1" channel="14" left="890" top="392" width="170" height="156" fontSize="15" positions="2" legend="OFF ◀ ▶ ON"/>
|
||||||
|
<element kind="rotary" caption="START BATT." slave="1" channel="16" left="1075" top="392" width="170" height="156" fontSize="15" positions="2" legend="OFF ◀ ▶ ON"/>
|
||||||
|
<element kind="rotary" caption="ENG.ROOM PUMP" slave="1" channel="18" left="0" top="570" width="170" height="156" fontSize="15" positions="2" legend="OFF ◀ ▶ ON"/>
|
||||||
|
<element kind="rotary" caption="AFT AUT. P." slave="1" channel="20" left="0" top="737" width="170" height="156" fontSize="15" positions="3" legend="PORT ◀ 0 ▶ STBD"/>
|
||||||
|
<element kind="rotary" caption="FORE AUT. PUMP" slave="1" channel="22" left="1065" top="577" width="170" height="156" fontSize="15" positions="2" legend="OFF ◀ ▶ ON"/>
|
||||||
|
<element kind="rotary" caption="CNT AUT. PUMP" slave="1" channel="24" left="1065" top="767" width="170" height="156" fontSize="15" positions="2" legend="OFF ◀ ▶ ON"/>
|
||||||
|
<element kind="image" caption="" left="200" top="580" width="815" height="206" image="antago-barca"/>
|
||||||
|
<element kind="label" caption="" left="452" top="824" width="335" height="112" frame="round" frameWidth="5"/>
|
||||||
|
<element kind="label" caption="ANTAGO" left="452" top="830" width="335" height="74" fontSize="52" fontName="Arial Black" spacing="2"/>
|
||||||
|
<element kind="label" caption="CANTIERE NAVALE" left="452" top="898" width="335" height="26" fontSize="14" fontName="Arial" spacing="4"/>
|
||||||
|
<element kind="lamp" caption="" slave="2" channel="0" left="420" top="570" width="22" height="22" color="#E0C020"/>
|
||||||
|
<element kind="lamp" caption="" slave="2" channel="1" left="480" top="590" width="22" height="22" color="#E0C020"/>
|
||||||
|
<element kind="lamp" caption="" slave="2" channel="2" left="390" top="600" width="22" height="22" color="#E0C020"/>
|
||||||
|
<element kind="lamp" caption="" slave="2" channel="3" left="457" top="615" width="23" height="25" color="#E04030"/>
|
||||||
|
<element kind="lamp" caption="" slave="2" channel="4" left="230" top="680" width="20" height="20" color="#E04030"/>
|
||||||
|
<element kind="lamp" caption="" slave="2" channel="5" left="198" top="723" width="22" height="22" color="#E04030"/>
|
||||||
|
<element kind="lamp" caption="" slave="2" channel="6" left="285" top="747" width="22" height="22" color="#E04030"/>
|
||||||
|
<element kind="lamp" caption="" slave="2" channel="7" left="445" top="730" width="22" height="22" color="#E04030"/>
|
||||||
|
<element kind="lamp" caption="" slave="0" channel="0" left="452" top="755" width="22" height="22" color="#E04030"/>
|
||||||
|
<element kind="lamp" caption="" slave="0" channel="0" left="785" top="772" width="22" height="22" color="#E08020"/>
|
||||||
|
<element kind="lamp" caption="" slave="0" channel="0" left="374" top="764" width="24" height="24" color="#E04868"/>
|
||||||
|
<element kind="label" caption="BILGES" left="340" top="792" width="92" height="24" fontSize="15"/>
|
||||||
|
<element kind="lamp" caption="" slave="0" channel="0" left="602" top="754" width="24" height="24" color="#40C040"/>
|
||||||
|
<element kind="lamp" caption="" slave="0" channel="0" left="638" top="754" width="24" height="24" color="#E0C020"/>
|
||||||
|
<element kind="lamp" caption="" slave="0" channel="0" left="668" top="754" width="24" height="24" color="#E0C020"/>
|
||||||
|
<element kind="label" caption="GREY WATER TANKS" left="505" top="792" width="215" height="24" fontSize="15"/>
|
||||||
|
<element kind="lamp" caption="" slave="0" channel="0" left="820" top="772" width="24" height="24" color="#E08020"/>
|
||||||
|
<element kind="label" caption="W.C. TANKS" left="770" top="796" width="140" height="24" fontSize="15"/>
|
||||||
|
<element kind="switch" caption="Luce" slave="1" channel="0" left="830" top="850" width="90" height="70"/>
|
||||||
|
<element kind="button" caption="Horn" slave="1" channel="0" left="940" top="860" width="50" height="48"/>
|
||||||
|
</elements>
|
||||||
|
</plancia>
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<plancia version="1">
|
||||||
|
<connection port="COM1" baud="9600" timeoutMs="500" pollMs="500"/>
|
||||||
|
<panel title="Plancia ZEBRA" width="980" height="560" background="sfondo"/>
|
||||||
|
<images>
|
||||||
|
<image name="sfondo" file="immagini\sfondo.png"/>
|
||||||
|
<image name="logo-zebra" file="immagini\logo-zebra.png"/>
|
||||||
|
<image name="led-off" file="immagini\led-off.png"/>
|
||||||
|
<image name="led-on" file="immagini\led-on.png"/>
|
||||||
|
<image name="btn-off" file="immagini\btn-off.png"/>
|
||||||
|
<image name="btn-on" file="immagini\btn-on.png"/>
|
||||||
|
</images>
|
||||||
|
<elements>
|
||||||
|
<element kind="image" caption="" image="logo-zebra" left="620" top="440" width="320" height="90"/>
|
||||||
|
<element kind="button" caption="HORN" slave="1" channel="5" left="40" top="60" width="180" height="70"
|
||||||
|
imageOff="btn-off" imageOn="btn-on"/>
|
||||||
|
<element kind="button" caption="Avvisatore" slave="1" channel="6" left="240" top="60" width="180" height="70"/>
|
||||||
|
<element kind="switch" caption="Luci coperta" slave="1" channel="0" left="40" top="160" width="180" height="70"/>
|
||||||
|
<element kind="switch" caption="Luci ponte" slave="1" channel="1" left="240" top="160" width="180" height="70"/>
|
||||||
|
<element kind="lamp" caption="Allarme" slave="3" channel="0" left="480" top="60" width="90" height="110"
|
||||||
|
imageOff="led-off" imageOn="led-on"/>
|
||||||
|
<element kind="lamp" caption="Ormeggio" slave="3" channel="1" left="590" top="60" width="90" height="110"/>
|
||||||
|
<element kind="gauge" caption="Temperatura" slave="5" channel="0" left="480" top="200" width="130" height="200"
|
||||||
|
rawMin="0" rawMax="4095" engMin="0" engMax="120" units="C" warnBelow="-1E30" warnAbove="100"/>
|
||||||
|
<element kind="gauge" caption="Serbatoio" slave="5" channel="1" left="640" top="200" width="130" height="200"
|
||||||
|
rawMin="0" rawMax="4095" engMin="0" engMax="100" units="%" warnBelow="15" warnAbove="1E30"/>
|
||||||
|
</elements>
|
||||||
|
</plancia>
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<plancia version="1">
|
||||||
|
<connection port="COM1" baud="9600" timeoutMs="500" pollMs="500"/>
|
||||||
|
<panel title="Plancia ZEBRA" width="980" height="560"/>
|
||||||
|
<elements>
|
||||||
|
<element kind="button" caption="Horn" slave="1" channel="5" left="30" top="30" width="120" height="48"/>
|
||||||
|
<element kind="button" caption="Avvisatore" slave="1" channel="6" left="170" top="30" width="120" height="48"/>
|
||||||
|
<element kind="switch" caption="Luci coperta" slave="1" channel="0" left="30" top="100" width="120" height="48"/>
|
||||||
|
<element kind="switch" caption="Luci ponte" slave="1" channel="1" left="170" top="100" width="120" height="48"/>
|
||||||
|
<element kind="switch" caption="Pompa sentina" slave="1" channel="2" left="310" top="100" width="120" height="48"/>
|
||||||
|
<element kind="lamp" caption="Allarme" slave="3" channel="0" left="470" top="30" width="90" height="64"/>
|
||||||
|
<element kind="lamp" caption="Ormeggio" slave="3" channel="1" left="570" top="30" width="90" height="64"/>
|
||||||
|
<element kind="lamp" caption="Ancora" slave="3" channel="2" left="670" top="30" width="90" height="64"/>
|
||||||
|
<element kind="gauge" caption="Temperatura" slave="5" channel="0" left="470" top="150" width="130" height="200"
|
||||||
|
rawMin="0" rawMax="4095" engMin="0" engMax="120" units="C" warnBelow="-1E30" warnAbove="100"/>
|
||||||
|
<element kind="gauge" caption="Serbatoio" slave="5" channel="1" left="620" top="150" width="130" height="200"
|
||||||
|
rawMin="0" rawMax="4095" engMin="0" engMax="100" units="%" warnBelow="15" warnAbove="1E30"/>
|
||||||
|
</elements>
|
||||||
|
</plancia>
|
||||||
|
After Width: | Height: | Size: 34 KiB |
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 9.4 KiB |
|
After Width: | Height: | Size: 388 B |
|
After Width: | Height: | Size: 392 B |
|
After Width: | Height: | Size: 1.3 KiB |
|
After Width: | Height: | Size: 1.4 KiB |
|
After Width: | Height: | Size: 2.0 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 97 KiB |
|
After Width: | Height: | Size: 8.0 KiB |
@@ -0,0 +1,92 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<plancia version="1">
|
||||||
|
<connection port="COM7" baud="9600" timeoutMs="500" pollMs="500"/>
|
||||||
|
<panel title="ZEBRA + ANTAGO - Plancia multifunzione" width="1900" height="1080" background="mfd-sfondo" ink="#E6E8EA"/>
|
||||||
|
<images>
|
||||||
|
<image name="mfd-sfondo" file="immagini\mfd-sfondo.png"/>
|
||||||
|
<image name="antago-barca-chiara" file="immagini\antago-barca-chiara.png"/>
|
||||||
|
</images>
|
||||||
|
<elements>
|
||||||
|
<element kind="switch" caption="WHITE LT" slave="1" channel="4" left="36" top="42" width="160" height="64" fontSize="13" color="#40D850" shape="screen"/>
|
||||||
|
<element kind="switch" caption="NAVIGATION LTS" slave="1" channel="5" left="36" top="126" width="160" height="64" fontSize="13" color="#40D850" shape="screen"/>
|
||||||
|
<element kind="switch" caption="STERN LT" slave="1" channel="6" left="36" top="210" width="160" height="64" fontSize="13" color="#40D850" shape="screen"/>
|
||||||
|
<element kind="switch" caption="ANCHOR LT" slave="1" channel="8" left="36" top="294" width="160" height="64" fontSize="13" color="#40D850" shape="screen"/>
|
||||||
|
<element kind="switch" caption="COMPASS LT" slave="1" channel="9" left="36" top="378" width="160" height="64" fontSize="13" color="#40D850" shape="screen"/>
|
||||||
|
<element kind="switch" caption="SUB LIGHT" slave="1" channel="11" left="36" top="462" width="160" height="64" fontSize="13" color="#40D850" shape="screen"/>
|
||||||
|
<element kind="switch" caption="INSTRUM.LTS" slave="1" channel="22" left="36" top="546" width="160" height="64" fontSize="13" color="#40D850" shape="screen"/>
|
||||||
|
<element kind="switch" caption="INSTRUMENTS" slave="1" channel="12" left="36" top="630" width="160" height="64" fontSize="13" color="#4090FF" shape="screen"/>
|
||||||
|
<element kind="switch" caption="PORT WIPER" slave="1" channel="1" left="1704" top="42" width="160" height="64" fontSize="13" color="#40D850" shape="screen"/>
|
||||||
|
<element kind="switch" caption="STBD WIPER" slave="1" channel="1" left="1704" top="126" width="160" height="64" fontSize="13" color="#40D850" shape="screen"/>
|
||||||
|
<element kind="button" caption="WATER WIPERS" slave="1" channel="2" left="1704" top="210" width="160" height="64" fontSize="13" color="#40D850" shape="screen"/>
|
||||||
|
<element kind="switch" caption="WASH PUMP" slave="1" channel="16" left="1704" top="294" width="160" height="64" fontSize="13" color="#40D850" shape="screen"/>
|
||||||
|
<element kind="switch" caption="WINCH ON" slave="1" channel="17" left="1704" top="378" width="160" height="64" fontSize="13" color="#40D850" shape="screen"/>
|
||||||
|
<element kind="button" caption="WINCH UP" slave="1" channel="10" left="1704" top="462" width="160" height="64" fontSize="13" color="#40D850" shape="screen"/>
|
||||||
|
<element kind="button" caption="WINCH DOWN" slave="1" channel="3" left="1704" top="546" width="160" height="64" fontSize="13" color="#40D850" shape="screen"/>
|
||||||
|
<element kind="switch" caption="GSM" slave="1" channel="20" left="1704" top="630" width="160" height="64" fontSize="13" color="#40D850" shape="screen"/>
|
||||||
|
<element kind="gauge" caption="230V AC VOLTMETER" slave="0" channel="0" left="245" top="48" width="240" height="268" fontSize="14" rawMin="0" rawMax="4095" engMin="0" engMax="300" units="V" warnBelow="-1E30" warnAbove="1E30" style="dial" decimals="0"/>
|
||||||
|
<element kind="gauge" caption="230V AC AMMETER" slave="0" channel="0" left="525" top="48" width="240" height="268" fontSize="14" rawMin="0" rawMax="4095" engMin="0" engMax="100" units="A" warnBelow="-1E30" warnAbove="1E30" style="dial" decimals="1"/>
|
||||||
|
<element kind="gauge" caption="SERVICES BATT. AMMETER" slave="0" channel="0" left="805" top="48" width="240" height="268" fontSize="14" rawMin="0" rawMax="4095" engMin="-100" engMax="100" units="A" warnBelow="-1E30" warnAbove="1E30" style="dial" decimals="1"/>
|
||||||
|
<element kind="gauge" caption="24V DC VOLTMETER" slave="0" channel="0" left="1085" top="48" width="240" height="268" fontSize="14" rawMin="0" rawMax="4095" engMin="0" engMax="40" units="V" warnBelow="-1E30" warnAbove="1E30" style="dial" decimals="1"/>
|
||||||
|
<element kind="display" caption="SERVICES BATT. CHARGER" slave="0" channel="0" left="1370" top="50" width="296" height="122" fontSize="15" digits="4" decimals="1" rawMin="0" rawMax="4095" engMin="0" engMax="100" units="A" warnBelow="-1E30" warnAbove="1E30"/>
|
||||||
|
<element kind="display" caption="START BATT. CHARGER" slave="0" channel="0" left="1370" top="182" width="296" height="122" fontSize="15" digits="4" decimals="1" rawMin="0" rawMax="4095" engMin="0" engMax="100" units="A" warnBelow="-1E30" warnAbove="1E30"/>
|
||||||
|
<element kind="label" caption="" left="230" top="318" width="850" height="286" color="#3A3F45" frame="rect" frameWidth="1"/>
|
||||||
|
<element kind="image" caption="" left="248" top="370" width="815" height="191" image="antago-barca-chiara"/>
|
||||||
|
<element kind="lamp" caption="" slave="2" channel="0" left="450" top="360" width="22" height="22" color="#E0C020"/>
|
||||||
|
<element kind="lamp" caption="" slave="2" channel="1" left="528" top="370" width="22" height="22" color="#E0C020"/>
|
||||||
|
<element kind="lamp" caption="" slave="2" channel="2" left="475" top="365" width="22" height="22" alarm="true" sound="engyclick-fire-alarm-beep-sound-effect-419969.mp3" color="#E0C020"/>
|
||||||
|
<element kind="lamp" caption="" slave="2" channel="3" left="505" top="390" width="22" height="22" color="#E04030"/>
|
||||||
|
<element kind="lamp" caption="" slave="2" channel="4" left="278" top="455" width="22" height="22" color="#E04030"/>
|
||||||
|
<element kind="lamp" caption="" slave="2" channel="5" left="246" top="498" width="22" height="22" color="#E04030"/>
|
||||||
|
<element kind="lamp" caption="" slave="2" channel="6" left="333" top="522" width="22" height="22" color="#E04030"/>
|
||||||
|
<element kind="lamp" caption="" slave="2" channel="7" left="493" top="505" width="22" height="22" color="#E04030"/>
|
||||||
|
<element kind="lamp" caption="" slave="2" channel="0" left="500" top="530" width="22" height="22" color="#E04030"/>
|
||||||
|
<element kind="lamp" caption="" slave="2" channel="1" left="833" top="547" width="22" height="22" color="#E08020"/>
|
||||||
|
<element kind="lamp" caption="" slave="2" channel="2" left="422" top="539" width="24" height="24" color="#E04868"/>
|
||||||
|
<element kind="label" caption="BILGES" left="388" top="567" width="92" height="24" fontSize="15"/>
|
||||||
|
<element kind="lamp" caption="" slave="2" channel="3" left="650" top="529" width="24" height="24" color="#40C040"/>
|
||||||
|
<element kind="lamp" caption="" slave="2" channel="4" left="686" top="529" width="24" height="24" alarm="true" sound="cicalino.wav" color="#E0C020"/>
|
||||||
|
<element kind="lamp" caption="" slave="2" channel="5" left="716" top="529" width="24" height="24" alarm="true" sound="cicalino.wav" color="#E0C020"/>
|
||||||
|
<element kind="label" caption="GREY WATER TANKS" left="553" top="567" width="215" height="24" fontSize="15"/>
|
||||||
|
<element kind="lamp" caption="" slave="2" channel="6" left="868" top="547" width="24" height="24" color="#E08020"/>
|
||||||
|
<element kind="label" caption="W.C. TANKS" left="818" top="571" width="140" height="24" fontSize="15"/>
|
||||||
|
<element kind="label" caption="" left="1100" top="318" width="566" height="286" color="#3A3F45" frame="rect" frameWidth="1"/>
|
||||||
|
<element kind="label" caption="PORT ENGINE" left="1116" top="330" width="250" height="30" fontSize="15" color="#4A9BFF" fontName="Arial" spacing="2" frame="round" frameWidth="2"/>
|
||||||
|
<element kind="label" caption="STBD ENGINE" left="1400" top="330" width="250" height="30" fontSize="15" color="#4A9BFF" fontName="Arial" spacing="2" frame="round" frameWidth="2"/>
|
||||||
|
<element kind="button" caption="SCROLL" slave="1" channel="25" left="1116" top="376" width="120" height="62" fontSize="13" color="#40D850" shape="screen"/>
|
||||||
|
<element kind="button" caption="RESET" slave="1" channel="26" left="1246" top="376" width="120" height="62" fontSize="13" color="#40D850" shape="screen"/>
|
||||||
|
<element kind="button" caption="STOP" slave="1" channel="30" left="1116" top="448" width="120" height="62" fontSize="13" color="#FF5038" colorOff="#3C1410" shape="screen"/>
|
||||||
|
<element kind="button" caption="START" slave="1" channel="31" left="1246" top="448" width="120" height="62" fontSize="13" color="#40D060" colorOff="#123A1C" shape="screen"/>
|
||||||
|
<element kind="button" caption="RESET" slave="1" channel="28" left="1400" top="376" width="120" height="62" fontSize="13" color="#40D850" shape="screen"/>
|
||||||
|
<element kind="button" caption="SCROLL" slave="1" channel="29" left="1530" top="376" width="120" height="62" fontSize="13" color="#40D850" shape="screen"/>
|
||||||
|
<element kind="button" caption="START" slave="1" channel="14" left="1400" top="448" width="120" height="62" fontSize="13" color="#40D060" colorOff="#123A1C" shape="screen"/>
|
||||||
|
<element kind="button" caption="STOP" slave="0" channel="0" left="1530" top="448" width="120" height="62" fontSize="13" color="#FF5038" colorOff="#3C1410" shape="screen"/>
|
||||||
|
<element kind="switch" caption="PARALLEL" slave="1" channel="27" left="1270" top="530" width="92" height="62" fontSize="13" color="#40D850" shape="screen"/>
|
||||||
|
<element kind="lamp" caption="FIRE ALARM" slave="2" channel="7" left="227" top="634" width="150" height="64" fontSize="13" color="#FF4030" colorOff="#3A1210" captionPos="center" shape="screen"/>
|
||||||
|
<element kind="lamp" caption="OIL STEERING" slave="2" channel="0" left="389" top="634" width="150" height="64" fontSize="13" color="#FF4030" colorOff="#3A1210" captionPos="center" shape="screen"/>
|
||||||
|
<element kind="switch" caption="STEERING" slave="1" channel="15" left="551" top="634" width="150" height="64" fontSize="13" color="#40D850" shape="screen"/>
|
||||||
|
<element kind="switch" caption="AUTOPILOT" slave="1" channel="13" left="713" top="634" width="150" height="64" fontSize="13" color="#40D850" shape="screen"/>
|
||||||
|
<element kind="switch" caption="TVCC" slave="1" channel="19" left="875" top="634" width="150" height="64" fontSize="13" color="#40D850" shape="screen"/>
|
||||||
|
<element kind="button" caption="HORN" slave="1" channel="21" left="1037" top="634" width="150" height="64" fontSize="13" color="#40D850" shape="screen"/>
|
||||||
|
<element kind="button" caption="TACIT" slave="1" channel="23" left="1199" top="634" width="150" height="64" fontSize="14" color="#40D850" shape="screen"/>
|
||||||
|
<element kind="switch" caption="Luce" slave="1" channel="26" left="1361" top="634" width="150" height="64" fontSize="13" color="#40D850" shape="screen"/>
|
||||||
|
<element kind="button" caption="Horn" slave="1" channel="27" left="1523" top="634" width="150" height="64" fontSize="15" color="#40D850" shape="screen"/>
|
||||||
|
<element kind="rotary" caption="24V DC FAN" slave="1" channel="18" left="39" top="740" width="190" height="150" fontSize="13" positions="2" legend="OFF ◀ ▶ ON"/>
|
||||||
|
<element kind="rotary" caption="CONTACT PORT" slave="0" channel="0" left="1120" top="520" width="110" height="80" fontSize="13" positions="2" legend=""/>
|
||||||
|
<element kind="rotary" caption="" slave="0" channel="0" left="1370" top="510" width="120" height="90" fontSize="13" positions="3" legend="PORT ◀ 0 ▶ STBD"/>
|
||||||
|
<element kind="rotary" caption="CONTACT STBD" slave="0" channel="0" left="1530" top="520" width="120" height="80" fontSize="13" positions="2" legend=""/>
|
||||||
|
<element kind="rotary" caption="230V AC FAN" slave="1" channel="24" left="240" top="740" width="190" height="150" fontSize="13" positions="3" legend=" FAN ◀ 0 ▶ BLOWER"/>
|
||||||
|
<element kind="rotary" caption="230V AC VOLTMETER" slave="1" channel="15" left="1480" top="730" width="190" height="150" fontSize="13" positions="3" legend="S.SUPPLY ◀ 0 ▶ GENERAT."/>
|
||||||
|
<element kind="rotary" caption="24V DC VOLTMETER" slave="1" channel="2" left="1680" top="730" width="190" height="150" fontSize="13" positions="3" legend="SERV.BTT. ◀ 0 ▶ START BTT."/>
|
||||||
|
<element kind="rotary" caption="GENERATOR" slave="1" channel="4" left="830" top="730" width="190" height="150" fontSize="13" positions="3" legend="STOP ◀ 0 ▶ START" momentary="true"/>
|
||||||
|
<element kind="rotary" caption="MAIN POWER" slave="1" channel="8" left="640" top="730" width="190" height="150" fontSize="13" positions="3" legend="SHORE ◀ 0 ▶ GEN."/>
|
||||||
|
<element kind="rotary" caption="AIR CONDITIONER" slave="1" channel="6" left="39" top="896" width="190" height="150" fontSize="13" positions="2" legend="OFF ◀ ▶ ON"/>
|
||||||
|
<element kind="rotary" caption="ALARMS" slave="1" channel="10" left="243" top="896" width="190" height="150" fontSize="13" positions="3" legend="TACIT ◀ 0 ▶ TEST" momentary="true"/>
|
||||||
|
<element kind="rotary" caption="SEWAGE PUMP" slave="1" channel="12" left="440" top="900" width="190" height="150" fontSize="13" positions="2" legend="OFF ◀ ▶ ON"/>
|
||||||
|
<element kind="rotary" caption="SERV. BATT." slave="1" channel="14" left="1210" top="730" width="190" height="150" fontSize="13" positions="2" legend="OFF ◀ ▶ ON"/>
|
||||||
|
<element kind="rotary" caption="START BATT." slave="1" channel="16" left="1020" top="730" width="190" height="150" fontSize="13" positions="2" legend="OFF ◀ ▶ ON"/>
|
||||||
|
<element kind="rotary" caption="ENG.ROOM PUMP" slave="1" channel="18" left="640" top="900" width="190" height="150" fontSize="13" positions="2" legend="OFF ◀ ▶ ON"/>
|
||||||
|
<element kind="rotary" caption="AFT AUT. P." slave="1" channel="20" left="830" top="900" width="190" height="150" fontSize="13" positions="3" legend="PORT ◀ 0 ▶ STBD"/>
|
||||||
|
<element kind="rotary" caption="FORE AUT. PUMP" slave="1" channel="22" left="1020" top="900" width="190" height="150" fontSize="13" positions="2" legend="OFF ◀ ▶ ON"/>
|
||||||
|
<element kind="rotary" caption="CNT AUT. PUMP" slave="1" channel="24" left="1210" top="900" width="190" height="150" fontSize="13" positions="2" legend="OFF ◀ ▶ ON"/>
|
||||||
|
</elements>
|
||||||
|
</plancia>
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<plancia version="1">
|
||||||
|
<connection port="COM7" baud="9600" timeoutMs="500" pollMs="500"/>
|
||||||
|
<panel title="Plancia" width="1000" height="640"/>
|
||||||
|
<elements>
|
||||||
|
<element kind="button" caption="Pulsante 0" slave="1" channel="0" left="20" top="50" width="120" height="48"/>
|
||||||
|
<element kind="switch" caption="Interruttore 0" slave="1" channel="0" left="160" top="50" width="120" height="48"/>
|
||||||
|
<element kind="lamp" caption="Spia 0" slave="1" channel="0" left="290" top="60" width="90" height="64" captionPos="below"/>
|
||||||
|
<element kind="gauge" caption="Gauge 0" slave="1" channel="0" left="390" top="30" width="130" height="200" rawMin="0" rawMax="4095" engMin="0" engMax="100" units="%" warnBelow="-1E30" warnAbove="1E30"/>
|
||||||
|
<element kind="button" caption="Horn" slave="1" channel="0" left="30" top="200" width="120" height="48"/>
|
||||||
|
<element kind="button" caption="Anchor Light" slave="1" channel="0" left="200" top="240" width="120" height="48"/>
|
||||||
|
<element kind="switch" caption="Luce poppa" slave="1" channel="0" left="100" top="340" width="120" height="48"/>
|
||||||
|
<element kind="lamp" caption="IN 1" slave="2" channel="0" left="30" top="440" width="90" height="64" captionPos="below" color="#00FF00"/>
|
||||||
|
<element kind="lamp" caption="IN 2" slave="2" channel="1" left="130" top="440" width="90" height="64" captionPos="below" color="#00FF00"/>
|
||||||
|
<element kind="lamp" caption="IN 3" slave="2" channel="2" left="230" top="440" width="90" height="64" captionPos="below" color="#00FF00"/>
|
||||||
|
<element kind="lamp" caption="IN 4" slave="2" channel="3" left="330" top="440" width="90" height="64" captionPos="below" color="#00FF00"/>
|
||||||
|
<element kind="lamp" caption="IN 5" slave="2" channel="4" left="430" top="440" width="90" height="64" captionPos="below" color="#00FF00"/>
|
||||||
|
<element kind="lamp" caption="IN 6" slave="2" channel="5" left="530" top="440" width="90" height="64" captionPos="below" color="#00FF00"/>
|
||||||
|
<element kind="lamp" caption="IN 7" slave="2" channel="6" left="630" top="440" width="90" height="64" captionPos="below" color="#00FF00"/>
|
||||||
|
<element kind="lamp" caption="IN 8" slave="2" channel="7" left="730" top="440" width="90" height="64" captionPos="below" color="#00FF00"/>
|
||||||
|
</elements>
|
||||||
|
</plancia>
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
@echo off
|
||||||
|
call "F:\Program Files (x86)\Embarcadero\Studio\37.0\bin\rsvars.bat" >nul
|
||||||
|
cd /d D:\dev\plancia\Console\strumenti
|
||||||
|
dcc32.exe -B "-NSSystem;System.Win;Winapi;Vcl;Data;Xml" -E. -N. -U"D:\dev\plancia;F:\Program Files (x86)\Embarcadero\Studio\37.0\lib\Win32\release" dumpframe.dpr
|
||||||
|
echo ==== EXIT: %ERRORLEVEL% ====
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
@echo off
|
||||||
|
call "F:\Program Files (x86)\Embarcadero\Studio\37.0\bin\rsvars.bat" >nul
|
||||||
|
cd /d D:\dev\plancia\Console\strumenti
|
||||||
|
dcc32.exe -B "-NSSystem;System.Win;Winapi;Vcl;Data;Xml" -E. -N. -U"D:\dev\plancia;F:\Program Files (x86)\Embarcadero\Studio\37.0\lib\Win32\release" monitorio.dpr
|
||||||
|
echo ==== EXIT: %ERRORLEVEL% ====
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
@echo off
|
||||||
|
call "F:\Program Files (x86)\Embarcadero\Studio\37.0\bin\rsvars.bat" >nul
|
||||||
|
cd /d D:\dev\plancia\Console\strumenti
|
||||||
|
dcc32.exe -B "-NSSystem;System.Win;Winapi;Vcl;Vcl.Imaging;Data;Xml" -E. -N. -U"D:\dev\plancia\Console;D:\dev\plancia;F:\Program Files (x86)\Embarcadero\Studio\37.0\lib\Win32\release" ringtest.dpr
|
||||||
|
echo ==== EXIT: %ERRORLEVEL% ====
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
@echo off
|
||||||
|
call "F:\Program Files (x86)\Embarcadero\Studio\37.0\bin\rsvars.bat" >nul
|
||||||
|
cd /d D:\dev\plancia\Console\strumenti
|
||||||
|
dcc32.exe -B "-NSSystem;System.Win;Winapi;Vcl;Data;Xml" -E. -N. -U"D:\dev\plancia;F:\Program Files (x86)\Embarcadero\Studio\37.0\lib\Win32\release" scanbus.dpr
|
||||||
|
echo ==== EXIT: %ERRORLEVEL% ====
|
||||||
@@ -0,0 +1,128 @@
|
|||||||
|
program dumpframe;
|
||||||
|
|
||||||
|
{
|
||||||
|
Manda una singola richiesta Modbus e stampa i byte grezzi della risposta,
|
||||||
|
senza interpretarli. Serve quando una lettura fallisce e non si sa se il
|
||||||
|
problema sia nel modulo, nel cablaggio o nel codice che decodifica.
|
||||||
|
|
||||||
|
Uso: dumpframe [porta] [baud] [slave] [funzione] [start] [quantita]
|
||||||
|
Es.: dumpframe COM7 9600 1 2 0 8
|
||||||
|
}
|
||||||
|
|
||||||
|
{$APPTYPE CONSOLE}
|
||||||
|
|
||||||
|
uses
|
||||||
|
Winapi.Windows, System.SysUtils;
|
||||||
|
|
||||||
|
function CRC16(const ABuf: array of Byte; ALen: Integer): Word;
|
||||||
|
var
|
||||||
|
I, J: Integer;
|
||||||
|
begin
|
||||||
|
Result := $FFFF;
|
||||||
|
for I := 0 to ALen - 1 do
|
||||||
|
begin
|
||||||
|
Result := Result xor ABuf[I];
|
||||||
|
for J := 0 to 7 do
|
||||||
|
if (Result and 1) <> 0 then
|
||||||
|
Result := (Result shr 1) xor $A001
|
||||||
|
else
|
||||||
|
Result := Result shr 1;
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
|
||||||
|
function Hex(const ABuf: TBytes; ALen: Integer): string;
|
||||||
|
var
|
||||||
|
I: Integer;
|
||||||
|
begin
|
||||||
|
Result := '';
|
||||||
|
for I := 0 to ALen - 1 do
|
||||||
|
Result := Result + IntToHex(ABuf[I], 2) + ' ';
|
||||||
|
end;
|
||||||
|
|
||||||
|
var
|
||||||
|
Port: string;
|
||||||
|
Baud, Slave, Func, Start, Qty: Integer;
|
||||||
|
H: THandle;
|
||||||
|
DCB: TDCB;
|
||||||
|
TO_: TCommTimeouts;
|
||||||
|
Req: array[0..7] of Byte;
|
||||||
|
C: Word;
|
||||||
|
Buf: TBytes;
|
||||||
|
Written, Got: DWORD;
|
||||||
|
Expected, I: Integer;
|
||||||
|
|
||||||
|
begin
|
||||||
|
Port := 'COM7'; Baud := 9600; Slave := 1; Func := 2; Start := 0; Qty := 8;
|
||||||
|
if ParamCount >= 1 then Port := ParamStr(1);
|
||||||
|
if ParamCount >= 2 then Baud := StrToIntDef(ParamStr(2), 9600);
|
||||||
|
if ParamCount >= 3 then Slave := StrToIntDef(ParamStr(3), 1);
|
||||||
|
if ParamCount >= 4 then Func := StrToIntDef(ParamStr(4), 2);
|
||||||
|
if ParamCount >= 5 then Start := StrToIntDef(ParamStr(5), 0);
|
||||||
|
if ParamCount >= 6 then Qty := StrToIntDef(ParamStr(6), 8);
|
||||||
|
|
||||||
|
H := CreateFile(PChar('\\.\' + Port), GENERIC_READ or GENERIC_WRITE, 0, nil,
|
||||||
|
OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, 0);
|
||||||
|
if H = THandle(-1) then
|
||||||
|
begin
|
||||||
|
Writeln(Format('Impossibile aprire %s (errore %d)', [Port, GetLastError]));
|
||||||
|
Readln; Halt(1);
|
||||||
|
end;
|
||||||
|
|
||||||
|
try
|
||||||
|
FillChar(DCB, SizeOf(DCB), 0);
|
||||||
|
DCB.DCBlength := SizeOf(DCB);
|
||||||
|
GetCommState(H, DCB);
|
||||||
|
DCB.BaudRate := Baud;
|
||||||
|
DCB.ByteSize := 8;
|
||||||
|
DCB.Parity := NOPARITY;
|
||||||
|
DCB.StopBits := ONESTOPBIT;
|
||||||
|
DCB.Flags := 0;
|
||||||
|
SetCommState(H, DCB);
|
||||||
|
|
||||||
|
TO_.ReadIntervalTimeout := 50;
|
||||||
|
TO_.ReadTotalTimeoutMultiplier := 10;
|
||||||
|
TO_.ReadTotalTimeoutConstant := 500;
|
||||||
|
TO_.WriteTotalTimeoutMultiplier := 10;
|
||||||
|
TO_.WriteTotalTimeoutConstant := 500;
|
||||||
|
SetCommTimeouts(H, TO_);
|
||||||
|
PurgeComm(H, PURGE_RXCLEAR or PURGE_TXCLEAR);
|
||||||
|
|
||||||
|
Req[0] := Slave;
|
||||||
|
Req[1] := Func;
|
||||||
|
Req[2] := Hi(Word(Start)); Req[3] := Lo(Word(Start));
|
||||||
|
Req[4] := Hi(Word(Qty)); Req[5] := Lo(Word(Qty));
|
||||||
|
C := CRC16(Req, 6);
|
||||||
|
Req[6] := Lo(C); Req[7] := Hi(C);
|
||||||
|
|
||||||
|
Write ('RICHIESTA (8 byte): ');
|
||||||
|
for I := 0 to 7 do Write(IntToHex(Req[I], 2), ' ');
|
||||||
|
Writeln;
|
||||||
|
|
||||||
|
WriteFile(H, Req[0], 8, Written, nil);
|
||||||
|
|
||||||
|
// Legge molto piu' del necessario: se arriva un eco della richiesta o
|
||||||
|
// byte di troppo si devono vedere, non nascondere troncando.
|
||||||
|
Expected := 64;
|
||||||
|
SetLength(Buf, Expected);
|
||||||
|
Sleep(150); // lascia il tempo allo slave di rispondere per intero
|
||||||
|
ReadFile(H, Buf[0], Expected, Got, nil);
|
||||||
|
|
||||||
|
Writeln(Format('RISPOSTA (%d byte): ', [Got]) + Hex(Buf, Got));
|
||||||
|
|
||||||
|
if Got >= 4 then
|
||||||
|
begin
|
||||||
|
C := CRC16(Buf, Got - 2);
|
||||||
|
Writeln(Format('CRC calcolata sui primi %d byte: %s %s - nel frame: %s %s -> %s',
|
||||||
|
[Got - 2, IntToHex(Lo(C), 2), IntToHex(Hi(C), 2),
|
||||||
|
IntToHex(Buf[Got - 2], 2), IntToHex(Buf[Got - 1], 2),
|
||||||
|
BoolToStr((Lo(C) = Buf[Got - 2]) and (Hi(C) = Buf[Got - 1]), True)]));
|
||||||
|
end;
|
||||||
|
|
||||||
|
finally
|
||||||
|
CloseHandle(H);
|
||||||
|
end;
|
||||||
|
|
||||||
|
Writeln;
|
||||||
|
Write('Premi INVIO per chiudere.');
|
||||||
|
Readln;
|
||||||
|
end.
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
# Grafica della plancia multifunzione (plancia-mfd.xml), sul modello della
|
||||||
|
# foto "Esempio plancia": cornice scura con il filo chiaro, schermo nero al
|
||||||
|
# centro. Produce anche la sagoma della barca ANTAGO in chiaro, perche' quella
|
||||||
|
# originale e' disegnata in nero e sullo schermo nero sparirebbe.
|
||||||
|
#
|
||||||
|
# powershell -File genera-mfd.ps1
|
||||||
|
|
||||||
|
param(
|
||||||
|
[string]$Dir = 'D:\dev\Plancia\Console\immagini',
|
||||||
|
[int]$W = 1900,
|
||||||
|
[int]$H = 1080,
|
||||||
|
# Schermo: deve coincidere con la disposizione degli elementi nell'XML.
|
||||||
|
[int]$ScreenLeft = 214,
|
||||||
|
[int]$ScreenTop = 34,
|
||||||
|
[int]$ScreenRight = 1686,
|
||||||
|
[int]$ScreenBottom = 716
|
||||||
|
)
|
||||||
|
|
||||||
|
Add-Type -AssemblyName System.Drawing
|
||||||
|
|
||||||
|
function RoundRect([int]$x, [int]$y, [int]$w, [int]$h, [int]$r) {
|
||||||
|
$p = New-Object System.Drawing.Drawing2D.GraphicsPath
|
||||||
|
$d = 2 * $r
|
||||||
|
$p.AddArc($x, $y, $d, $d, 180, 90)
|
||||||
|
$p.AddArc(($x + $w - $d), $y, $d, $d, 270, 90)
|
||||||
|
$p.AddArc(($x + $w - $d), ($y + $h - $d), $d, $d, 0, 90)
|
||||||
|
$p.AddArc($x, ($y + $h - $d), $d, $d, 90, 90)
|
||||||
|
$p.CloseFigure()
|
||||||
|
return $p
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- sfondo --------------------------------------------------------------
|
||||||
|
$bmp = New-Object System.Drawing.Bitmap($W, $H, [System.Drawing.Imaging.PixelFormat]::Format32bppArgb)
|
||||||
|
$g = [System.Drawing.Graphics]::FromImage($bmp)
|
||||||
|
$g.SmoothingMode = 'AntiAlias'
|
||||||
|
|
||||||
|
# Cornice: grafite opaca, appena piu' chiara in alto.
|
||||||
|
$rect = New-Object System.Drawing.Rectangle(0, 0, $W, $H)
|
||||||
|
$c1 = [System.Drawing.Color]::FromArgb(255, 44, 46, 50)
|
||||||
|
$c2 = [System.Drawing.Color]::FromArgb(255, 22, 23, 26)
|
||||||
|
$br = New-Object System.Drawing.Drawing2D.LinearGradientBrush($rect, $c1, $c2, 90.0)
|
||||||
|
$g.FillRectangle($br, $rect)
|
||||||
|
|
||||||
|
$edge = New-Object System.Drawing.Pen ([System.Drawing.Color]::FromArgb(255, 10, 10, 12)), 4
|
||||||
|
$path = RoundRect 2 2 ($W - 5) ($H - 5) 26
|
||||||
|
$g.DrawPath($edge, $path)
|
||||||
|
|
||||||
|
# Il filo chiaro che corre tutto attorno, come nella foto.
|
||||||
|
$line = New-Object System.Drawing.Pen ([System.Drawing.Color]::FromArgb(255, 200, 204, 209)), 2
|
||||||
|
$path = RoundRect 16 16 ($W - 33) ($H - 33) 20
|
||||||
|
$g.DrawPath($line, $path)
|
||||||
|
|
||||||
|
# Schermo: incassato (ombra scura attorno), nero, con un filo grigio.
|
||||||
|
$sw = $ScreenRight - $ScreenLeft
|
||||||
|
$sh = $ScreenBottom - $ScreenTop
|
||||||
|
$shadow = New-Object System.Drawing.Pen ([System.Drawing.Color]::FromArgb(255, 8, 8, 9)), 8
|
||||||
|
$g.DrawRectangle($shadow, ($ScreenLeft - 4), ($ScreenTop - 4), ($sw + 8), ($sh + 8))
|
||||||
|
$screen = New-Object System.Drawing.SolidBrush ([System.Drawing.Color]::FromArgb(255, 4, 5, 6))
|
||||||
|
$g.FillRectangle($screen, $ScreenLeft, $ScreenTop, $sw, $sh)
|
||||||
|
$glass = New-Object System.Drawing.Pen ([System.Drawing.Color]::FromArgb(255, 92, 97, 104)), 2
|
||||||
|
$g.DrawRectangle($glass, $ScreenLeft, $ScreenTop, $sw, $sh)
|
||||||
|
|
||||||
|
# Separazione fra lo schermo e la fila dei selettori.
|
||||||
|
$sep = New-Object System.Drawing.Pen ([System.Drawing.Color]::FromArgb(255, 70, 74, 80)), 1
|
||||||
|
$g.DrawLine($sep, 36, ($ScreenBottom + 16), ($W - 36), ($ScreenBottom + 16))
|
||||||
|
|
||||||
|
$g.Dispose()
|
||||||
|
$out = Join-Path $Dir 'mfd-sfondo.png'
|
||||||
|
$bmp.Save($out, [System.Drawing.Imaging.ImageFormat]::Png)
|
||||||
|
$bmp.Dispose()
|
||||||
|
Write-Output ("{0} {1}x{2}" -f $out, $W, $H)
|
||||||
|
|
||||||
|
# --- sagoma della barca in chiaro ---------------------------------------
|
||||||
|
# Stessa trasparenza dell'originale, colore invertito: il tratto nero diventa
|
||||||
|
# grigio chiaro, le sfumature di bordo restano sfumature.
|
||||||
|
$src = New-Object System.Drawing.Bitmap (Join-Path $Dir 'antago-barca.png')
|
||||||
|
$dst = New-Object System.Drawing.Bitmap($src.Width, $src.Height, [System.Drawing.Imaging.PixelFormat]::Format32bppArgb)
|
||||||
|
for ($y = 0; $y -lt $src.Height; $y++) {
|
||||||
|
for ($x = 0; $x -lt $src.Width; $x++) {
|
||||||
|
$p = $src.GetPixel($x, $y)
|
||||||
|
if ($p.A -eq 0) { continue }
|
||||||
|
$v = [int](220 - ($p.R + $p.G + $p.B) / 3 * 0.85)
|
||||||
|
if ($v -lt 0) { $v = 0 }
|
||||||
|
$dst.SetPixel($x, $y, [System.Drawing.Color]::FromArgb($p.A, $v, $v, [Math]::Min(255, $v + 6)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$out = Join-Path $Dir 'antago-barca-chiara.png'
|
||||||
|
$dst.Save($out, [System.Drawing.Imaging.ImageFormat]::Png)
|
||||||
|
$src.Dispose()
|
||||||
|
$dst.Dispose()
|
||||||
|
Write-Output ("{0}" -f $out)
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
# Disegna la lamiera di un pannello: fondo chiaro sfumato, cornice e viti
|
||||||
|
# agli angoli. Usato per gli sfondi delle plance di esempio.
|
||||||
|
#
|
||||||
|
# powershell -File genera-sfondo.ps1 -Out ..\immagini\sfondo.png -W 1270 -H 950
|
||||||
|
|
||||||
|
param(
|
||||||
|
[string]$Out = 'D:\dev\Plancia\Console\immagini\antago-sfondo.png',
|
||||||
|
[int]$W = 1270,
|
||||||
|
[int]$H = 950
|
||||||
|
)
|
||||||
|
|
||||||
|
Add-Type -AssemblyName System.Drawing
|
||||||
|
|
||||||
|
$bmp = New-Object System.Drawing.Bitmap($W, $H, [System.Drawing.Imaging.PixelFormat]::Format32bppArgb)
|
||||||
|
$g = [System.Drawing.Graphics]::FromImage($bmp)
|
||||||
|
$g.SmoothingMode = 'AntiAlias'
|
||||||
|
|
||||||
|
# Lamiera verniciata: bianco sporco con una leggerissima ombreggiatura verticale.
|
||||||
|
$rect = New-Object System.Drawing.Rectangle(0, 0, $W, $H)
|
||||||
|
$c1 = [System.Drawing.Color]::FromArgb(255, 246, 246, 243)
|
||||||
|
$c2 = [System.Drawing.Color]::FromArgb(255, 226, 227, 224)
|
||||||
|
$br = New-Object System.Drawing.Drawing2D.LinearGradientBrush($rect, $c1, $c2, 90.0)
|
||||||
|
$g.FillRectangle($br, $rect)
|
||||||
|
|
||||||
|
# Cornice del pannello
|
||||||
|
$pen = New-Object System.Drawing.Pen ([System.Drawing.Color]::FromArgb(255, 120, 122, 120)), 3
|
||||||
|
$g.DrawRectangle($pen, 6, 6, ($W - 13), ($H - 13))
|
||||||
|
$pen2 = New-Object System.Drawing.Pen ([System.Drawing.Color]::FromArgb(90, 255, 255, 255)), 1
|
||||||
|
$g.DrawRectangle($pen2, 10, 10, ($W - 21), ($H - 21))
|
||||||
|
|
||||||
|
# Viti agli angoli
|
||||||
|
$screwFill = New-Object System.Drawing.SolidBrush ([System.Drawing.Color]::FromArgb(255, 176, 178, 176))
|
||||||
|
$screwEdge = New-Object System.Drawing.Pen ([System.Drawing.Color]::FromArgb(255, 110, 112, 110)), 2
|
||||||
|
$slot = New-Object System.Drawing.Pen ([System.Drawing.Color]::FromArgb(255, 90, 92, 90)), 3
|
||||||
|
foreach ($p in @(@(26, 26), @(($W - 46), 26), @(26, ($H - 46)), @(($W - 46), ($H - 46)))) {
|
||||||
|
$x = $p[0]
|
||||||
|
$y = $p[1]
|
||||||
|
$g.FillEllipse($screwFill, $x, $y, 20, 20)
|
||||||
|
$g.DrawEllipse($screwEdge, $x, $y, 20, 20)
|
||||||
|
$g.DrawLine($slot, ($x + 4), ($y + 14), ($x + 16), ($y + 6))
|
||||||
|
}
|
||||||
|
|
||||||
|
$g.Dispose()
|
||||||
|
$bmp.Save($Out, [System.Drawing.Imaging.ImageFormat]::Png)
|
||||||
|
$bmp.Dispose()
|
||||||
|
Write-Output ("{0} {1}x{2}" -f $Out, $W, $H)
|
||||||
@@ -0,0 +1,129 @@
|
|||||||
|
program monitorio;
|
||||||
|
|
||||||
|
{
|
||||||
|
Mostra in tempo reale gli ingressi digitali di un modulo IO.
|
||||||
|
|
||||||
|
Serve al banco, quando si collega un morsetto e si vuole vedere subito quale
|
||||||
|
canale si muove. Stampa una riga solo quando qualcosa cambia, cosi' la
|
||||||
|
finestra non scorre da sola e la cronologia resta leggibile.
|
||||||
|
|
||||||
|
Uso: monitorio [porta] [baud] [slave] [canali] [cicli]
|
||||||
|
cicli = quante letture fare prima di uscire; 0 o assente = per sempre
|
||||||
|
Es.: monitorio COM7 9600 2 8
|
||||||
|
|
||||||
|
Si chiude con Ctrl+C.
|
||||||
|
}
|
||||||
|
|
||||||
|
{$APPTYPE CONSOLE}
|
||||||
|
|
||||||
|
uses
|
||||||
|
System.SysUtils,
|
||||||
|
uModbusRTU in '..\..\uModbusRTU.pas';
|
||||||
|
|
||||||
|
/// "1 0 0 1 . . . ." con i canali spenti resi come punto: gli accesi saltano all'occhio.
|
||||||
|
function Render(const ABits: TArray<Boolean>): string;
|
||||||
|
var
|
||||||
|
I: Integer;
|
||||||
|
begin
|
||||||
|
Result := '';
|
||||||
|
for I := 0 to High(ABits) do
|
||||||
|
if ABits[I] then
|
||||||
|
Result := Result + '[1]'
|
||||||
|
else
|
||||||
|
Result := Result + ' . ';
|
||||||
|
end;
|
||||||
|
|
||||||
|
function Header(ACount: Integer): string;
|
||||||
|
var
|
||||||
|
I: Integer;
|
||||||
|
begin
|
||||||
|
Result := '';
|
||||||
|
for I := 0 to ACount - 1 do
|
||||||
|
Result := Result + Format(' %d ', [I]);
|
||||||
|
end;
|
||||||
|
|
||||||
|
var
|
||||||
|
Port: string;
|
||||||
|
Baud, Slave, Count, Cycles, Done: Integer;
|
||||||
|
Bus: TModbusRTU;
|
||||||
|
Bits, Prev: TArray<Boolean>;
|
||||||
|
First: Boolean;
|
||||||
|
Changed: Boolean;
|
||||||
|
I, Errors: Integer;
|
||||||
|
LastErr: string;
|
||||||
|
|
||||||
|
begin
|
||||||
|
Port := 'COM7'; Baud := 9600; Slave := 2; Count := 8; Cycles := 0; Done := 0;
|
||||||
|
if ParamCount >= 1 then Port := ParamStr(1);
|
||||||
|
if ParamCount >= 2 then Baud := StrToIntDef(ParamStr(2), 9600);
|
||||||
|
if ParamCount >= 3 then Slave := StrToIntDef(ParamStr(3), 2);
|
||||||
|
if ParamCount >= 4 then Count := StrToIntDef(ParamStr(4), 8);
|
||||||
|
if ParamCount >= 5 then Cycles := StrToIntDef(ParamStr(5), 0);
|
||||||
|
|
||||||
|
Writeln(Format('Ingressi dello slave %d su %s a %d baud. Ctrl+C per uscire.',
|
||||||
|
[Slave, Port, Baud]));
|
||||||
|
Writeln;
|
||||||
|
Writeln(' canale: ' + Header(Count));
|
||||||
|
Writeln;
|
||||||
|
|
||||||
|
Bus := TModbusRTU.Create(Port, Baud, 500);
|
||||||
|
try
|
||||||
|
Bus.Connect;
|
||||||
|
First := True;
|
||||||
|
Errors := 0;
|
||||||
|
LastErr := '';
|
||||||
|
SetLength(Prev, 0);
|
||||||
|
|
||||||
|
while True do
|
||||||
|
begin
|
||||||
|
try
|
||||||
|
Bits := Bus.ReadDiscreteInputs(Slave, 0, Count);
|
||||||
|
|
||||||
|
if Errors > 0 then
|
||||||
|
begin
|
||||||
|
Writeln(Format('%s ... ripristinato dopo %d letture fallite',
|
||||||
|
[FormatDateTime('hh:nn:ss', Now), Errors]));
|
||||||
|
Errors := 0;
|
||||||
|
end;
|
||||||
|
|
||||||
|
Changed := First or (Length(Prev) <> Length(Bits));
|
||||||
|
if not Changed then
|
||||||
|
for I := 0 to High(Bits) do
|
||||||
|
if Bits[I] <> Prev[I] then
|
||||||
|
begin
|
||||||
|
Changed := True;
|
||||||
|
Break;
|
||||||
|
end;
|
||||||
|
|
||||||
|
if Changed then
|
||||||
|
begin
|
||||||
|
Writeln(FormatDateTime('hh:nn:ss', Now) + ' ' + Render(Bits));
|
||||||
|
Prev := Copy(Bits);
|
||||||
|
First := False;
|
||||||
|
end;
|
||||||
|
|
||||||
|
except
|
||||||
|
on E: Exception do
|
||||||
|
begin
|
||||||
|
// Un errore ripetuto non deve riempire lo schermo: lo si stampa
|
||||||
|
// la prima volta e poi si tace finche' non cambia.
|
||||||
|
Inc(Errors);
|
||||||
|
if E.Message <> LastErr then
|
||||||
|
begin
|
||||||
|
Writeln(FormatDateTime('hh:nn:ss', Now) + ' ERRORE: ' + E.Message);
|
||||||
|
LastErr := E.Message;
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
|
||||||
|
Sleep(200);
|
||||||
|
|
||||||
|
Inc(Done);
|
||||||
|
if (Cycles > 0) and (Done >= Cycles) then
|
||||||
|
Break;
|
||||||
|
end;
|
||||||
|
|
||||||
|
finally
|
||||||
|
Bus.Free;
|
||||||
|
end;
|
||||||
|
end.
|
||||||
@@ -0,0 +1,122 @@
|
|||||||
|
program ringtest;
|
||||||
|
|
||||||
|
{
|
||||||
|
Banco di prova per l'aspetto degli elementi: disegna la stessa scena di giorno
|
||||||
|
e di notte, spenta e accesa, e salva l'immagine. Serve a giudicare una
|
||||||
|
modifica ai colori guardandola invece che immaginandola.
|
||||||
|
|
||||||
|
Uso: ringtest [file.bmp]
|
||||||
|
}
|
||||||
|
|
||||||
|
{$APPTYPE CONSOLE}
|
||||||
|
|
||||||
|
uses
|
||||||
|
Winapi.Windows, System.SysUtils, System.Classes,
|
||||||
|
Vcl.Forms, Vcl.Graphics, Vcl.Controls, Vcl.ExtCtrls,
|
||||||
|
uPlanciaElements in '..\uPlanciaElements.pas',
|
||||||
|
uImageLib in '..\uImageLib.pas',
|
||||||
|
uModbusRTU in '..\..\uModbusRTU.pas';
|
||||||
|
|
||||||
|
const
|
||||||
|
PANE_W = 430;
|
||||||
|
PANE_H = 330;
|
||||||
|
|
||||||
|
var
|
||||||
|
Host: TForm;
|
||||||
|
Lib: TImageLibrary;
|
||||||
|
Bmp: TBitmap;
|
||||||
|
OutFile: string;
|
||||||
|
|
||||||
|
/// Un elemento nello stato voluto, dentro il riquadro indicato.
|
||||||
|
procedure Place(APane: TWinControl; AKind: TElementKind; AShape: TKeyShape;
|
||||||
|
ALeft, ATop, AW, AH: Integer; AOn, ANight: Boolean; const ACaption: string;
|
||||||
|
AOffColor: TColor);
|
||||||
|
var
|
||||||
|
Def: TElementDef;
|
||||||
|
El: TPlanciaElement;
|
||||||
|
begin
|
||||||
|
Def := TElementDef.Create(AKind);
|
||||||
|
Def.Shape := AShape;
|
||||||
|
Def.Caption := ACaption;
|
||||||
|
Def.CaptionPos := cpBelow;
|
||||||
|
Def.OffColor := AOffColor;
|
||||||
|
Def.Left := ALeft;
|
||||||
|
Def.Top := ATop;
|
||||||
|
Def.Width := AW;
|
||||||
|
Def.Height := AH;
|
||||||
|
El := TPlanciaElement.CreateElement(Host, Def, Lib);
|
||||||
|
El.Parent := APane;
|
||||||
|
El.Color := APane.Brush.Color;
|
||||||
|
El.NightMode := ANight;
|
||||||
|
El.SyncState(AOn);
|
||||||
|
end;
|
||||||
|
|
||||||
|
/// La stessa scena, cambia solo se e' notte.
|
||||||
|
procedure Scene(APane: TPanel; ANight: Boolean);
|
||||||
|
begin
|
||||||
|
// Tondi: spento e acceso, con lente neutra e lente rossa.
|
||||||
|
Place(APane, ekSwitch, ksRound, 20, 20, 96, 96, False, ANight, 'SPENTO', clNone);
|
||||||
|
Place(APane, ekSwitch, ksRound, 130, 20, 96, 96, True, ANight, 'ACCESO', clNone);
|
||||||
|
Place(APane, ekSwitch, ksRound, 240, 20, 96, 96, False, ANight, 'FIRE', TColor($001818B0));
|
||||||
|
Place(APane, ekSwitch, ksRound, 340, 20, 96, 96, True, ANight, 'FIRE ON', TColor($001818B0));
|
||||||
|
// Squadrati: qui lo stato sta tutto nel bordo.
|
||||||
|
Place(APane, ekButton, ksRect, 20, 150, 110, 44, False, ANight, 'RECT', clNone);
|
||||||
|
Place(APane, ekButton, ksRect, 150, 150, 110, 44, True, ANight, 'RECT ON', clNone);
|
||||||
|
// Spia: questa il colore lo cambia eccome.
|
||||||
|
Place(APane, ekLamp, ksAuto, 290, 150, 70, 60, False, ANight, 'SPIA', clNone);
|
||||||
|
Place(APane, ekLamp, ksAuto, 365, 150, 70, 60, True, ANight, 'SPIA ON', clNone);
|
||||||
|
// Scritta serigrafata: di notte deve passare all'ambra, non sparire.
|
||||||
|
Place(APane, ekLabel, ksAuto, 20, 250, 200, 50, False, ANight, 'ZEBRA', clNone);
|
||||||
|
Place(APane, ekGauge, ksAuto, 250, 225, 100, 95, False, ANight, 'LIVELLO', clNone);
|
||||||
|
end;
|
||||||
|
|
||||||
|
var
|
||||||
|
PaneDay, PaneNight: TPanel;
|
||||||
|
|
||||||
|
begin
|
||||||
|
OutFile := 'ringtest.bmp';
|
||||||
|
if ParamCount >= 1 then
|
||||||
|
OutFile := ParamStr(1);
|
||||||
|
|
||||||
|
Application.Initialize;
|
||||||
|
Lib := TImageLibrary.Create;
|
||||||
|
Host := TForm.Create(nil);
|
||||||
|
try
|
||||||
|
Host.BorderStyle := bsNone;
|
||||||
|
Host.ClientWidth := PANE_W * 2;
|
||||||
|
Host.ClientHeight := PANE_H;
|
||||||
|
|
||||||
|
PaneDay := TPanel.Create(Host);
|
||||||
|
PaneDay.Parent := Host;
|
||||||
|
PaneDay.SetBounds(0, 0, PANE_W, PANE_H);
|
||||||
|
PaneDay.BevelOuter := bvNone;
|
||||||
|
PaneDay.Color := clBtnFace;
|
||||||
|
PaneDay.ParentBackground := False;
|
||||||
|
|
||||||
|
PaneNight := TPanel.Create(Host);
|
||||||
|
PaneNight.Parent := Host;
|
||||||
|
PaneNight.SetBounds(PANE_W, 0, PANE_W, PANE_H);
|
||||||
|
PaneNight.BevelOuter := bvNone;
|
||||||
|
PaneNight.Color := CLR_NIGHT_BG;
|
||||||
|
PaneNight.ParentBackground := False;
|
||||||
|
|
||||||
|
Scene(PaneDay, False);
|
||||||
|
Scene(PaneNight, True);
|
||||||
|
|
||||||
|
Host.Show;
|
||||||
|
Application.ProcessMessages;
|
||||||
|
|
||||||
|
Bmp := TBitmap.Create;
|
||||||
|
try
|
||||||
|
Bmp.SetSize(Host.ClientWidth, Host.ClientHeight);
|
||||||
|
Host.PaintTo(Bmp.Canvas.Handle, 0, 0);
|
||||||
|
Bmp.SaveToFile(OutFile);
|
||||||
|
Writeln(Format('scritto %s (%dx%d)', [OutFile, Bmp.Width, Bmp.Height]));
|
||||||
|
finally
|
||||||
|
Bmp.Free;
|
||||||
|
end;
|
||||||
|
finally
|
||||||
|
Host.Free;
|
||||||
|
Lib.Free;
|
||||||
|
end;
|
||||||
|
end.
|
||||||
|
After Width: | Height: | Size: 16 KiB |
@@ -0,0 +1,41 @@
|
|||||||
|
# Estrae un pezzo di serigrafia dalla foto di un quadro: ritaglia la regione
|
||||||
|
# indicata e rende trasparenti i pixel piu' chiari della soglia, cosi' resta
|
||||||
|
# solo il tratto scuro e si appoggia su qualunque sfondo.
|
||||||
|
#
|
||||||
|
# powershell -File ritaglia-grafica.ps1 -Src foto.png -Out logo.png -X 452 -Y 824 -W 335 -H 112 -Soglia 128
|
||||||
|
|
||||||
|
param(
|
||||||
|
[string]$Src,
|
||||||
|
[string]$Out,
|
||||||
|
[int]$X,
|
||||||
|
[int]$Y,
|
||||||
|
[int]$W,
|
||||||
|
[int]$H,
|
||||||
|
[double]$Soglia = 128.0
|
||||||
|
)
|
||||||
|
|
||||||
|
Add-Type -AssemblyName System.Drawing
|
||||||
|
|
||||||
|
$img = [System.Drawing.Bitmap]::FromFile($Src)
|
||||||
|
$bmp = New-Object System.Drawing.Bitmap($W, $H, [System.Drawing.Imaging.PixelFormat]::Format32bppArgb)
|
||||||
|
$clear = [System.Drawing.Color]::FromArgb(0, 255, 255, 255)
|
||||||
|
|
||||||
|
for ($j = 0; $j -lt $H; $j++) {
|
||||||
|
for ($i = 0; $i -lt $W; $i++) {
|
||||||
|
$c = $img.GetPixel(($X + $i), ($Y + $j))
|
||||||
|
$lum = ([double]$c.R * 0.299) + ([double]$c.G * 0.587) + ([double]$c.B * 0.114)
|
||||||
|
if ($lum -gt $Soglia) {
|
||||||
|
$bmp.SetPixel($i, $j, $clear)
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
# Normalizza il tratto verso il nero, togliendo il colore della luce
|
||||||
|
$v = [int]([Math]::Max(0.0, [Math]::Min(255.0, ($lum * 0.7))))
|
||||||
|
$bmp.SetPixel($i, $j, [System.Drawing.Color]::FromArgb(255, $v, $v, $v))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$bmp.Save($Out, [System.Drawing.Imaging.ImageFormat]::Png)
|
||||||
|
$bmp.Dispose()
|
||||||
|
$img.Dispose()
|
||||||
|
Write-Output ("{0} {1}x{2}" -f $Out, $W, $H)
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
# Ruota un'immagine di 90 gradi. Serve per raddrizzare le foto dei quadri
|
||||||
|
# scattate in verticale.
|
||||||
|
#
|
||||||
|
# powershell -File ruota.ps1 -In foto.png -Out dritta.png -Verso ccw
|
||||||
|
|
||||||
|
param(
|
||||||
|
[string]$In,
|
||||||
|
[string]$Out,
|
||||||
|
[ValidateSet('cw', 'ccw')][string]$Verso = 'ccw'
|
||||||
|
)
|
||||||
|
|
||||||
|
Add-Type -AssemblyName System.Drawing
|
||||||
|
|
||||||
|
$img = [System.Drawing.Bitmap]::FromFile($In)
|
||||||
|
if ($Verso -eq 'cw') {
|
||||||
|
$img.RotateFlip([System.Drawing.RotateFlipType]::Rotate90FlipNone)
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$img.RotateFlip([System.Drawing.RotateFlipType]::Rotate270FlipNone)
|
||||||
|
}
|
||||||
|
$img.Save($Out, [System.Drawing.Imaging.ImageFormat]::Png)
|
||||||
|
Write-Output ("{0} {1}x{2}" -f $Out, $img.Width, $img.Height)
|
||||||
|
$img.Dispose()
|
||||||
@@ -0,0 +1,141 @@
|
|||||||
|
program scanbus;
|
||||||
|
|
||||||
|
{
|
||||||
|
Scanner del bus Modbus RTU.
|
||||||
|
|
||||||
|
Interroga gli indirizzi slave uno per uno e riporta chi risponde, con quali
|
||||||
|
funzioni e che dati restituisce. Serve a scoprire l'indirizzo di un modulo
|
||||||
|
appena inserito e a capire da che canale partono i suoi ingressi.
|
||||||
|
|
||||||
|
Uso: scanbus [porta] [baud] [ultimoIndirizzo]
|
||||||
|
Es.: scanbus COM7 9600 16
|
||||||
|
}
|
||||||
|
|
||||||
|
{$APPTYPE CONSOLE}
|
||||||
|
|
||||||
|
uses
|
||||||
|
System.SysUtils,
|
||||||
|
uModbusRTU in '..\..\uModbusRTU.pas';
|
||||||
|
|
||||||
|
const
|
||||||
|
PROBE_TIMEOUT_MS = 200; // basso: un indirizzo morto non deve costare mezzo secondo
|
||||||
|
PROBE_COUNT = 8; // 8 canali: la taglia tipica di questi moduli
|
||||||
|
|
||||||
|
/// Rende leggibili gli 8 bit come "1 0 0 1 ...", con i numeri di canale sopra.
|
||||||
|
function BitsToStr(const ABits: TArray<Boolean>): string;
|
||||||
|
var
|
||||||
|
I: Integer;
|
||||||
|
begin
|
||||||
|
Result := '';
|
||||||
|
for I := 0 to High(ABits) do
|
||||||
|
if ABits[I] then Result := Result + '1 ' else Result := Result + '0 ';
|
||||||
|
end;
|
||||||
|
|
||||||
|
function RegsToStr(const ARegs: TArray<Word>): string;
|
||||||
|
var
|
||||||
|
I: Integer;
|
||||||
|
begin
|
||||||
|
Result := '';
|
||||||
|
for I := 0 to High(ARegs) do
|
||||||
|
Result := Result + IntToStr(ARegs[I]) + ' ';
|
||||||
|
end;
|
||||||
|
|
||||||
|
var
|
||||||
|
Port: string;
|
||||||
|
Baud: Cardinal;
|
||||||
|
LastAddr: Integer;
|
||||||
|
Bus: TModbusRTU;
|
||||||
|
Addr: Integer;
|
||||||
|
Alive: Boolean;
|
||||||
|
Found: Integer;
|
||||||
|
Bits: TArray<Boolean>;
|
||||||
|
Regs: TArray<Word>;
|
||||||
|
|
||||||
|
begin
|
||||||
|
try
|
||||||
|
Port := 'COM7';
|
||||||
|
Baud := 9600;
|
||||||
|
LastAddr := 16;
|
||||||
|
if ParamCount >= 1 then Port := ParamStr(1);
|
||||||
|
if ParamCount >= 2 then Baud := StrToUIntDef(ParamStr(2), 9600);
|
||||||
|
if ParamCount >= 3 then LastAddr := StrToIntDef(ParamStr(3), 16);
|
||||||
|
|
||||||
|
Writeln(Format('Scansione %s a %d baud, indirizzi da 1 a %d.', [Port, Baud, LastAddr]));
|
||||||
|
Writeln('Chiudi PlanciaConsole prima di lanciarlo: la porta si apre una volta sola.');
|
||||||
|
Writeln;
|
||||||
|
|
||||||
|
Bus := TModbusRTU.Create(Port, Baud, PROBE_TIMEOUT_MS);
|
||||||
|
try
|
||||||
|
Bus.Connect;
|
||||||
|
Found := 0;
|
||||||
|
|
||||||
|
for Addr := 1 to LastAddr do
|
||||||
|
begin
|
||||||
|
Write(Format(#13' provo indirizzo %d... ', [Addr]));
|
||||||
|
Alive := False;
|
||||||
|
|
||||||
|
// FC 02 - ingressi digitali: e' cio' che interessa a un modulo IO
|
||||||
|
try
|
||||||
|
Bits := Bus.ReadDiscreteInputs(Addr, 0, PROBE_COUNT);
|
||||||
|
if not Alive then Writeln(#13 + Format('SLAVE %d trovato', [Addr]) + StringOfChar(' ', 20));
|
||||||
|
Alive := True;
|
||||||
|
Writeln(' FC02 ingressi (canali 0..7): ' + BitsToStr(Bits));
|
||||||
|
except
|
||||||
|
on E: Exception do ;
|
||||||
|
end;
|
||||||
|
|
||||||
|
// FC 01 - bobine: le uscite rele'
|
||||||
|
try
|
||||||
|
Bits := Bus.ReadCoils(Addr, 0, PROBE_COUNT);
|
||||||
|
if not Alive then Writeln(#13 + Format('SLAVE %d trovato', [Addr]) + StringOfChar(' ', 20));
|
||||||
|
Alive := True;
|
||||||
|
Writeln(' FC01 bobine (canali 0..7): ' + BitsToStr(Bits));
|
||||||
|
except
|
||||||
|
on E: Exception do ;
|
||||||
|
end;
|
||||||
|
|
||||||
|
if not Alive then
|
||||||
|
Continue; // indirizzo morto: inutile insistere con i registri
|
||||||
|
|
||||||
|
// Solo per chi ha gia' risposto: vale la pena chiedere anche i registri
|
||||||
|
try
|
||||||
|
Regs := Bus.ReadHoldingRegisters(Addr, 0, 4);
|
||||||
|
Writeln(' FC03 holding (reg 0..3): ' + RegsToStr(Regs));
|
||||||
|
except
|
||||||
|
on E: Exception do ;
|
||||||
|
end;
|
||||||
|
|
||||||
|
try
|
||||||
|
Regs := Bus.ReadInputRegisters(Addr, 0, 4);
|
||||||
|
Writeln(' FC04 input reg (reg 0..3): ' + RegsToStr(Regs));
|
||||||
|
except
|
||||||
|
on E: Exception do ;
|
||||||
|
end;
|
||||||
|
|
||||||
|
Writeln;
|
||||||
|
Inc(Found);
|
||||||
|
end;
|
||||||
|
|
||||||
|
Writeln(#13 + StringOfChar(' ', 40));
|
||||||
|
if Found = 0 then
|
||||||
|
begin
|
||||||
|
Writeln('Nessuno slave ha risposto.');
|
||||||
|
Writeln('Controlla: baud del modulo, scambio A/B del RS485, alimentazione,');
|
||||||
|
Writeln('e che PlanciaConsole non tenga gia'' aperta la porta.');
|
||||||
|
end
|
||||||
|
else
|
||||||
|
Writeln(Format('Trovati %d slave.', [Found]));
|
||||||
|
|
||||||
|
finally
|
||||||
|
Bus.Free;
|
||||||
|
end;
|
||||||
|
|
||||||
|
except
|
||||||
|
on E: Exception do
|
||||||
|
Writeln('ERRORE: ' + E.Message);
|
||||||
|
end;
|
||||||
|
|
||||||
|
Writeln;
|
||||||
|
Write('Premi INVIO per chiudere.');
|
||||||
|
Readln;
|
||||||
|
end.
|
||||||
@@ -0,0 +1,195 @@
|
|||||||
|
unit uAlarmSound;
|
||||||
|
|
||||||
|
{
|
||||||
|
Suoni di allarme delle spie.
|
||||||
|
|
||||||
|
Usa MCI (mciSendString), che su Windows suona MP3 e WAV senza librerie
|
||||||
|
esterne e senza mettere un componente sul form. La riproduzione non blocca:
|
||||||
|
"play" torna subito e il suono continua per conto suo, quindi una plancia
|
||||||
|
che suona resta comandabile.
|
||||||
|
|
||||||
|
Ogni file suona su un proprio alias, aperto la prima volta e tenuto aperto:
|
||||||
|
riaprirlo a ogni allarme costerebbe qualche decimo di secondo, e un cicalino
|
||||||
|
deve partire nell'istante in cui la spia si accende. Due allarmi diversi si
|
||||||
|
sovrappongono; lo stesso allarme che si ripete riparte da capo.
|
||||||
|
}
|
||||||
|
|
||||||
|
interface
|
||||||
|
|
||||||
|
uses
|
||||||
|
Winapi.Windows, Winapi.MMSystem, System.SysUtils, System.Classes,
|
||||||
|
System.Generics.Collections;
|
||||||
|
|
||||||
|
type
|
||||||
|
TAlarmPlayer = class
|
||||||
|
private
|
||||||
|
/// Percorso del file -> alias MCI aperto per quel file.
|
||||||
|
FOpen: TDictionary<string, string>;
|
||||||
|
FNextAlias: Integer;
|
||||||
|
FLastError: string;
|
||||||
|
function Command(const ACommand: string): Boolean;
|
||||||
|
/// Comando MCI che torna una risposta ('status ... mode' e simili).
|
||||||
|
function Query(const ACommand: string): string;
|
||||||
|
function AliasFor(const AFileName: string): string;
|
||||||
|
public
|
||||||
|
constructor Create;
|
||||||
|
destructor Destroy; override;
|
||||||
|
/// Suona il file, ripartendo da capo se sta gia' suonando. False se non
|
||||||
|
/// si e' potuto suonare; il motivo sta in LastError.
|
||||||
|
/// Con ALoop il suono riparte da solo quando finisce, finche' non lo si
|
||||||
|
/// ferma: un allarme deve continuare a chiamare, non suonare una volta.
|
||||||
|
function Play(const AFileName: string; ALoop: Boolean = False): Boolean;
|
||||||
|
/// Vero se quel file sta suonando in questo momento. Serve a far
|
||||||
|
/// ricominciare l'allarme sui dispositivi che non sanno ripetere da soli.
|
||||||
|
function IsPlaying(const AFileName: string): Boolean;
|
||||||
|
/// Ferma un suono solo, lasciando suonare gli altri: serve quando una
|
||||||
|
/// spia si spegne mentre altri allarmi sono ancora attivi.
|
||||||
|
procedure Stop(const AFileName: string);
|
||||||
|
/// Zittisce tutto: si usa lasciando la plancia o chiudendo.
|
||||||
|
procedure StopAll;
|
||||||
|
property LastError: string read FLastError;
|
||||||
|
end;
|
||||||
|
|
||||||
|
implementation
|
||||||
|
|
||||||
|
const
|
||||||
|
/// Oltre questo numero di suoni diversi aperti, il piu' vecchio si chiude:
|
||||||
|
/// una plancia con decine di allarmi non deve tenere aperti decine di
|
||||||
|
/// dispositivi MCI.
|
||||||
|
MAX_OPEN = 8;
|
||||||
|
|
||||||
|
{ TAlarmPlayer }
|
||||||
|
|
||||||
|
constructor TAlarmPlayer.Create;
|
||||||
|
begin
|
||||||
|
inherited Create;
|
||||||
|
FOpen := TDictionary<string, string>.Create;
|
||||||
|
end;
|
||||||
|
|
||||||
|
destructor TAlarmPlayer.Destroy;
|
||||||
|
begin
|
||||||
|
StopAll;
|
||||||
|
FOpen.Free;
|
||||||
|
inherited;
|
||||||
|
end;
|
||||||
|
|
||||||
|
function TAlarmPlayer.Command(const ACommand: string): Boolean;
|
||||||
|
var
|
||||||
|
Err: MCIERROR;
|
||||||
|
Buf: array[0..255] of Char;
|
||||||
|
begin
|
||||||
|
Err := mciSendString(PChar(ACommand), nil, 0, 0);
|
||||||
|
Result := Err = 0;
|
||||||
|
if Result then
|
||||||
|
Exit;
|
||||||
|
if mciGetErrorString(Err, Buf, Length(Buf)) then
|
||||||
|
FLastError := Buf
|
||||||
|
else
|
||||||
|
FLastError := Format('errore MCI %d', [Err]);
|
||||||
|
end;
|
||||||
|
|
||||||
|
function TAlarmPlayer.AliasFor(const AFileName: string): string;
|
||||||
|
var
|
||||||
|
Key: string;
|
||||||
|
Old: TPair<string, string>;
|
||||||
|
begin
|
||||||
|
Key := LowerCase(AFileName);
|
||||||
|
if FOpen.TryGetValue(Key, Result) then
|
||||||
|
Exit;
|
||||||
|
|
||||||
|
if FOpen.Count >= MAX_OPEN then
|
||||||
|
for Old in FOpen do
|
||||||
|
begin
|
||||||
|
Command('close ' + Old.Value);
|
||||||
|
FOpen.Remove(Old.Key);
|
||||||
|
Break;
|
||||||
|
end;
|
||||||
|
|
||||||
|
Inc(FNextAlias);
|
||||||
|
Result := Format('plancia_all%d', [FNextAlias]);
|
||||||
|
// Il tipo va detto: senza, MCI sceglie dall'estensione e sui .mp3 di certe
|
||||||
|
// installazioni sbaglia dispositivo.
|
||||||
|
if not Command(Format('open "%s" type mpegvideo alias %s',
|
||||||
|
[AFileName, Result])) then
|
||||||
|
if not Command(Format('open "%s" alias %s', [AFileName, Result])) then
|
||||||
|
Exit('');
|
||||||
|
FOpen.Add(Key, Result);
|
||||||
|
end;
|
||||||
|
|
||||||
|
function TAlarmPlayer.Query(const ACommand: string): string;
|
||||||
|
var
|
||||||
|
Buf: array[0..127] of Char;
|
||||||
|
begin
|
||||||
|
if mciSendString(PChar(ACommand), Buf, Length(Buf), 0) = 0 then
|
||||||
|
Result := Buf
|
||||||
|
else
|
||||||
|
Result := '';
|
||||||
|
end;
|
||||||
|
|
||||||
|
function TAlarmPlayer.IsPlaying(const AFileName: string): Boolean;
|
||||||
|
var
|
||||||
|
Alias: string;
|
||||||
|
begin
|
||||||
|
Result := FOpen.TryGetValue(LowerCase(AFileName), Alias) and
|
||||||
|
SameText(Query('status ' + Alias + ' mode'), 'playing');
|
||||||
|
end;
|
||||||
|
|
||||||
|
function TAlarmPlayer.Play(const AFileName: string; ALoop: Boolean): Boolean;
|
||||||
|
var
|
||||||
|
Alias: string;
|
||||||
|
begin
|
||||||
|
FLastError := '';
|
||||||
|
if AFileName = '' then
|
||||||
|
Exit(False);
|
||||||
|
if not FileExists(AFileName) then
|
||||||
|
begin
|
||||||
|
FLastError := 'file non trovato';
|
||||||
|
Exit(False);
|
||||||
|
end;
|
||||||
|
|
||||||
|
Alias := AliasFor(AFileName);
|
||||||
|
if Alias = '' then
|
||||||
|
Exit(False);
|
||||||
|
// "from 0" fa ripartire da capo un allarme che sta gia' suonando: meglio
|
||||||
|
// risentirlo dall'inizio che non sentire niente. Con "repeat" il
|
||||||
|
// dispositivo ripete da solo; non tutti lo accettano, e a quelli si rimedia
|
||||||
|
// facendolo ripartire da fuori quando risulta fermo (vedi IsPlaying).
|
||||||
|
Result := False;
|
||||||
|
if ALoop then
|
||||||
|
Result := Command(Format('play %s from 0 repeat', [Alias]));
|
||||||
|
if not Result then
|
||||||
|
Result := Command(Format('play %s from 0', [Alias]));
|
||||||
|
if not Result then
|
||||||
|
begin
|
||||||
|
// Il dispositivo puo' essere morto (cuffie staccate): si riapre da capo.
|
||||||
|
Command('close ' + Alias);
|
||||||
|
FOpen.Remove(LowerCase(AFileName));
|
||||||
|
Alias := AliasFor(AFileName);
|
||||||
|
if Alias <> '' then
|
||||||
|
Result := Command(Format('play %s from 0', [Alias]));
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
|
||||||
|
procedure TAlarmPlayer.Stop(const AFileName: string);
|
||||||
|
var
|
||||||
|
Alias: string;
|
||||||
|
begin
|
||||||
|
// Il dispositivo resta aperto: se l'allarme torna, riparte senza il ritardo
|
||||||
|
// dell'apertura.
|
||||||
|
if FOpen.TryGetValue(LowerCase(AFileName), Alias) then
|
||||||
|
Command('stop ' + Alias);
|
||||||
|
end;
|
||||||
|
|
||||||
|
procedure TAlarmPlayer.StopAll;
|
||||||
|
var
|
||||||
|
Alias: string;
|
||||||
|
begin
|
||||||
|
for Alias in FOpen.Values do
|
||||||
|
begin
|
||||||
|
Command('stop ' + Alias);
|
||||||
|
Command('close ' + Alias);
|
||||||
|
end;
|
||||||
|
FOpen.Clear;
|
||||||
|
end;
|
||||||
|
|
||||||
|
end.
|
||||||
@@ -0,0 +1,972 @@
|
|||||||
|
object ConsoleForm: TConsoleForm
|
||||||
|
Left = 0
|
||||||
|
Top = 0
|
||||||
|
Caption = 'Plancia'
|
||||||
|
ClientHeight = 760
|
||||||
|
ClientWidth = 1200
|
||||||
|
Color = clBtnFace
|
||||||
|
Font.Charset = DEFAULT_CHARSET
|
||||||
|
Font.Color = clWindowText
|
||||||
|
Font.Height = -12
|
||||||
|
Font.Name = 'Segoe UI'
|
||||||
|
Font.Style = []
|
||||||
|
Position = poScreenCenter
|
||||||
|
OnCloseQuery = FormCloseQuery
|
||||||
|
OnCreate = FormCreate
|
||||||
|
OnDestroy = FormDestroy
|
||||||
|
OnKeyDown = FormKeyDown
|
||||||
|
OnKeyPress = FormKeyPress
|
||||||
|
TextHeight = 15
|
||||||
|
object pnlToolbar: TPanel
|
||||||
|
Left = 0
|
||||||
|
Top = 0
|
||||||
|
Width = 1200
|
||||||
|
Height = 44
|
||||||
|
Align = alTop
|
||||||
|
BevelOuter = bvNone
|
||||||
|
TabOrder = 0
|
||||||
|
object lblZoom: TLabel
|
||||||
|
Left = 702
|
||||||
|
Top = 15
|
||||||
|
Width = 30
|
||||||
|
Height = 15
|
||||||
|
Caption = 'Zoom'
|
||||||
|
end
|
||||||
|
object lblFile: TLabel
|
||||||
|
Left = 852
|
||||||
|
Top = 15
|
||||||
|
Width = 3
|
||||||
|
Height = 15
|
||||||
|
end
|
||||||
|
object btnNuovo: TButton
|
||||||
|
Left = 10
|
||||||
|
Top = 8
|
||||||
|
Width = 80
|
||||||
|
Height = 28
|
||||||
|
Caption = 'Nuovo'
|
||||||
|
TabOrder = 0
|
||||||
|
OnClick = btnNuovoClick
|
||||||
|
end
|
||||||
|
object btnApri: TButton
|
||||||
|
Left = 96
|
||||||
|
Top = 8
|
||||||
|
Width = 80
|
||||||
|
Height = 28
|
||||||
|
Caption = 'Apri...'
|
||||||
|
TabOrder = 1
|
||||||
|
OnClick = btnApriClick
|
||||||
|
end
|
||||||
|
object btnSalva: TButton
|
||||||
|
Left = 182
|
||||||
|
Top = 8
|
||||||
|
Width = 80
|
||||||
|
Height = 28
|
||||||
|
Caption = 'Salva'
|
||||||
|
TabOrder = 2
|
||||||
|
OnClick = btnSalvaClick
|
||||||
|
end
|
||||||
|
object btnSalvaCome: TButton
|
||||||
|
Left = 268
|
||||||
|
Top = 8
|
||||||
|
Width = 110
|
||||||
|
Height = 28
|
||||||
|
Caption = 'Salva con nome...'
|
||||||
|
TabOrder = 3
|
||||||
|
OnClick = btnSalvaComeClick
|
||||||
|
end
|
||||||
|
object btnAnnulla: TButton
|
||||||
|
Left = 390
|
||||||
|
Top = 8
|
||||||
|
Width = 80
|
||||||
|
Height = 28
|
||||||
|
Hint = 'Annulla l'#39'ultima modifica (Ctrl+Z)'
|
||||||
|
Caption = 'Annulla'
|
||||||
|
Enabled = False
|
||||||
|
ParentShowHint = False
|
||||||
|
ShowHint = True
|
||||||
|
TabOrder = 6
|
||||||
|
OnClick = btnAnnullaClick
|
||||||
|
end
|
||||||
|
object btnRipeti: TButton
|
||||||
|
Left = 476
|
||||||
|
Top = 8
|
||||||
|
Width = 80
|
||||||
|
Height = 28
|
||||||
|
Hint = 'Ripete la modifica annullata (Ctrl+Y)'
|
||||||
|
Caption = 'Ripeti'
|
||||||
|
Enabled = False
|
||||||
|
ParentShowHint = False
|
||||||
|
ShowHint = True
|
||||||
|
TabOrder = 7
|
||||||
|
OnClick = btnRipetiClick
|
||||||
|
end
|
||||||
|
object chkGriglia: TCheckBox
|
||||||
|
Left = 568
|
||||||
|
Top = 14
|
||||||
|
Width = 120
|
||||||
|
Height = 17
|
||||||
|
Caption = 'Griglia 10 px'
|
||||||
|
Checked = True
|
||||||
|
State = cbChecked
|
||||||
|
TabOrder = 4
|
||||||
|
OnClick = chkGrigliaClick
|
||||||
|
end
|
||||||
|
object cboZoom: TComboBox
|
||||||
|
Left = 742
|
||||||
|
Top = 11
|
||||||
|
Width = 84
|
||||||
|
Height = 23
|
||||||
|
Style = csDropDownList
|
||||||
|
TabOrder = 5
|
||||||
|
OnChange = ZoomChanged
|
||||||
|
end
|
||||||
|
end
|
||||||
|
object pnlStatus: TPanel
|
||||||
|
Left = 0
|
||||||
|
Top = 730
|
||||||
|
Width = 1200
|
||||||
|
Height = 30
|
||||||
|
Align = alBottom
|
||||||
|
BevelOuter = bvNone
|
||||||
|
TabOrder = 1
|
||||||
|
object shpLed: TShape
|
||||||
|
Left = 12
|
||||||
|
Top = 8
|
||||||
|
Width = 14
|
||||||
|
Height = 14
|
||||||
|
Brush.Color = clGray
|
||||||
|
Shape = stCircle
|
||||||
|
end
|
||||||
|
object lblStatus: TLabel
|
||||||
|
Left = 36
|
||||||
|
Top = 8
|
||||||
|
Width = 3
|
||||||
|
Height = 15
|
||||||
|
end
|
||||||
|
object btnModo: TButton
|
||||||
|
Left = 1040
|
||||||
|
Top = 3
|
||||||
|
Width = 150
|
||||||
|
Height = 24
|
||||||
|
Anchors = [akTop, akRight]
|
||||||
|
Caption = 'Vai in plancia'
|
||||||
|
TabOrder = 0
|
||||||
|
OnClick = btnModoClick
|
||||||
|
end
|
||||||
|
object btnNotte: TButton
|
||||||
|
Left = 934
|
||||||
|
Top = 3
|
||||||
|
Width = 100
|
||||||
|
Height = 24
|
||||||
|
Anchors = [akTop, akRight]
|
||||||
|
Caption = 'Notte'
|
||||||
|
TabOrder = 1
|
||||||
|
OnClick = btnNotteClick
|
||||||
|
end
|
||||||
|
object btnAllarmi: TButton
|
||||||
|
Left = 470
|
||||||
|
Top = 3
|
||||||
|
Width = 228
|
||||||
|
Height = 24
|
||||||
|
Hint = 'Rimette in funzione gli allarmi sonori zittiti'
|
||||||
|
Anchors = [akTop, akRight]
|
||||||
|
Caption = 'Allarmi zittiti'
|
||||||
|
ParentShowHint = False
|
||||||
|
ShowHint = True
|
||||||
|
TabOrder = 3
|
||||||
|
TabStop = False
|
||||||
|
Visible = False
|
||||||
|
OnClick = btnAllarmiClick
|
||||||
|
end
|
||||||
|
object cboPlancia: TComboBox
|
||||||
|
Left = 704
|
||||||
|
Top = 3
|
||||||
|
Width = 224
|
||||||
|
Height = 23
|
||||||
|
Hint = 'Passa a un'#39'altra plancia della stessa cartella'
|
||||||
|
Style = csDropDownList
|
||||||
|
Anchors = [akTop, akRight]
|
||||||
|
ParentShowHint = False
|
||||||
|
ShowHint = True
|
||||||
|
TabOrder = 2
|
||||||
|
TabStop = False
|
||||||
|
Visible = False
|
||||||
|
OnSelect = cboPlanciaSelect
|
||||||
|
end
|
||||||
|
end
|
||||||
|
object pnlPalette: TPanel
|
||||||
|
Left = 0
|
||||||
|
Top = 44
|
||||||
|
Width = 200
|
||||||
|
Height = 686
|
||||||
|
Align = alLeft
|
||||||
|
BevelOuter = bvNone
|
||||||
|
TabOrder = 2
|
||||||
|
object lblPaletteTitle: TLabel
|
||||||
|
Left = 10
|
||||||
|
Top = 6
|
||||||
|
Width = 44
|
||||||
|
Height = 15
|
||||||
|
Caption = 'Palette'
|
||||||
|
Font.Charset = DEFAULT_CHARSET
|
||||||
|
Font.Color = clWindowText
|
||||||
|
Font.Height = -12
|
||||||
|
Font.Name = 'Segoe UI'
|
||||||
|
Font.Style = [fsBold]
|
||||||
|
ParentFont = False
|
||||||
|
end
|
||||||
|
object gbImmagini: TGroupBox
|
||||||
|
Left = 6
|
||||||
|
Top = 306
|
||||||
|
Width = 188
|
||||||
|
Height = 160
|
||||||
|
Caption = 'Libreria immagini'
|
||||||
|
TabOrder = 0
|
||||||
|
object lstImmagini: TListBox
|
||||||
|
Left = 10
|
||||||
|
Top = 20
|
||||||
|
Width = 168
|
||||||
|
Height = 100
|
||||||
|
ItemHeight = 15
|
||||||
|
TabOrder = 0
|
||||||
|
end
|
||||||
|
object btnImgAdd: TButton
|
||||||
|
Left = 10
|
||||||
|
Top = 126
|
||||||
|
Width = 80
|
||||||
|
Height = 26
|
||||||
|
Caption = 'Aggiungi...'
|
||||||
|
TabOrder = 1
|
||||||
|
OnClick = btnImgAddClick
|
||||||
|
end
|
||||||
|
object btnImgDel: TButton
|
||||||
|
Left = 96
|
||||||
|
Top = 126
|
||||||
|
Width = 82
|
||||||
|
Height = 26
|
||||||
|
Caption = 'Rimuovi'
|
||||||
|
TabOrder = 2
|
||||||
|
OnClick = btnImgDelClick
|
||||||
|
end
|
||||||
|
end
|
||||||
|
object gbGenerale: TGroupBox
|
||||||
|
Left = 0
|
||||||
|
Top = 486
|
||||||
|
Width = 200
|
||||||
|
Height = 200
|
||||||
|
Align = alBottom
|
||||||
|
Caption = 'Generale'
|
||||||
|
TabOrder = 1
|
||||||
|
object lblPort: TLabel
|
||||||
|
Left = 10
|
||||||
|
Top = 18
|
||||||
|
Width = 55
|
||||||
|
Height = 15
|
||||||
|
Caption = 'Porta COM'
|
||||||
|
end
|
||||||
|
object lblBaud: TLabel
|
||||||
|
Left = 105
|
||||||
|
Top = 18
|
||||||
|
Width = 27
|
||||||
|
Height = 15
|
||||||
|
Caption = 'Baud'
|
||||||
|
end
|
||||||
|
object lblPoll: TLabel
|
||||||
|
Left = 10
|
||||||
|
Top = 62
|
||||||
|
Width = 65
|
||||||
|
Height = 15
|
||||||
|
Caption = 'Polling (ms)'
|
||||||
|
end
|
||||||
|
object lblPanelSize: TLabel
|
||||||
|
Left = 105
|
||||||
|
Top = 62
|
||||||
|
Width = 88
|
||||||
|
Height = 15
|
||||||
|
Caption = 'Pannello L x A'
|
||||||
|
end
|
||||||
|
object lblTitolo: TLabel
|
||||||
|
Left = 10
|
||||||
|
Top = 106
|
||||||
|
Width = 76
|
||||||
|
Height = 15
|
||||||
|
Caption = 'Titolo plancia'
|
||||||
|
end
|
||||||
|
object lblSfondo: TLabel
|
||||||
|
Left = 10
|
||||||
|
Top = 150
|
||||||
|
Width = 105
|
||||||
|
Height = 15
|
||||||
|
Caption = 'Immagine di sfondo'
|
||||||
|
end
|
||||||
|
object cboPort: TComboBox
|
||||||
|
Left = 10
|
||||||
|
Top = 36
|
||||||
|
Width = 88
|
||||||
|
Height = 23
|
||||||
|
TabOrder = 0
|
||||||
|
Text = 'COM1'
|
||||||
|
OnChange = GeneralChanged
|
||||||
|
end
|
||||||
|
object cboBaud: TComboBox
|
||||||
|
Left = 105
|
||||||
|
Top = 36
|
||||||
|
Width = 85
|
||||||
|
Height = 23
|
||||||
|
TabOrder = 1
|
||||||
|
Text = '9600'
|
||||||
|
OnChange = GeneralChanged
|
||||||
|
end
|
||||||
|
object edtPoll: TEdit
|
||||||
|
Left = 10
|
||||||
|
Top = 80
|
||||||
|
Width = 60
|
||||||
|
Height = 23
|
||||||
|
TabOrder = 2
|
||||||
|
Text = '500'
|
||||||
|
OnChange = GeneralChanged
|
||||||
|
end
|
||||||
|
object edtPanelW: TEdit
|
||||||
|
Left = 105
|
||||||
|
Top = 80
|
||||||
|
Width = 40
|
||||||
|
Height = 23
|
||||||
|
TabOrder = 3
|
||||||
|
Text = '1000'
|
||||||
|
OnChange = GeneralChanged
|
||||||
|
end
|
||||||
|
object edtPanelH: TEdit
|
||||||
|
Left = 150
|
||||||
|
Top = 80
|
||||||
|
Width = 40
|
||||||
|
Height = 23
|
||||||
|
TabOrder = 4
|
||||||
|
Text = '640'
|
||||||
|
OnChange = GeneralChanged
|
||||||
|
end
|
||||||
|
object edtTitolo: TEdit
|
||||||
|
Left = 10
|
||||||
|
Top = 124
|
||||||
|
Width = 180
|
||||||
|
Height = 23
|
||||||
|
TabOrder = 5
|
||||||
|
Text = 'Plancia'
|
||||||
|
OnChange = GeneralChanged
|
||||||
|
end
|
||||||
|
object cboSfondo: TComboBox
|
||||||
|
Left = 10
|
||||||
|
Top = 168
|
||||||
|
Width = 180
|
||||||
|
Height = 23
|
||||||
|
Style = csDropDownList
|
||||||
|
TabOrder = 6
|
||||||
|
OnChange = BackgroundChanged
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
object pnlProps: TPanel
|
||||||
|
Left = 956
|
||||||
|
Top = 44
|
||||||
|
Width = 244
|
||||||
|
Height = 686
|
||||||
|
Align = alRight
|
||||||
|
BevelOuter = bvNone
|
||||||
|
TabOrder = 3
|
||||||
|
object lblPropTitle: TLabel
|
||||||
|
Left = 12
|
||||||
|
Top = 10
|
||||||
|
Width = 158
|
||||||
|
Height = 15
|
||||||
|
Caption = 'Nessun elemento selezionato'
|
||||||
|
Font.Charset = DEFAULT_CHARSET
|
||||||
|
Font.Color = clWindowText
|
||||||
|
Font.Height = -12
|
||||||
|
Font.Name = 'Segoe UI'
|
||||||
|
Font.Style = [fsBold]
|
||||||
|
ParentFont = False
|
||||||
|
end
|
||||||
|
object lblTipo: TLabel
|
||||||
|
Left = 12
|
||||||
|
Top = 32
|
||||||
|
Width = 25
|
||||||
|
Height = 15
|
||||||
|
Caption = 'Tipo'
|
||||||
|
end
|
||||||
|
object cboTipo: TComboBox
|
||||||
|
Left = 12
|
||||||
|
Top = 50
|
||||||
|
Width = 210
|
||||||
|
Height = 23
|
||||||
|
Style = csDropDownList
|
||||||
|
TabOrder = 16
|
||||||
|
OnChange = KindChanged
|
||||||
|
end
|
||||||
|
object lblCap: TLabel
|
||||||
|
Left = 12
|
||||||
|
Top = 82
|
||||||
|
Width = 48
|
||||||
|
Height = 15
|
||||||
|
Caption = 'Etichetta'
|
||||||
|
end
|
||||||
|
object lblFontSize: TLabel
|
||||||
|
Left = 180
|
||||||
|
Top = 82
|
||||||
|
Width = 42
|
||||||
|
Height = 15
|
||||||
|
Caption = 'Testo px'
|
||||||
|
end
|
||||||
|
object lblSlave: TLabel
|
||||||
|
Left = 12
|
||||||
|
Top = 130
|
||||||
|
Width = 30
|
||||||
|
Height = 15
|
||||||
|
Caption = 'Slave'
|
||||||
|
end
|
||||||
|
object lblCanale: TLabel
|
||||||
|
Left = 122
|
||||||
|
Top = 130
|
||||||
|
Width = 39
|
||||||
|
Height = 15
|
||||||
|
Caption = 'Canale'
|
||||||
|
end
|
||||||
|
object lblPos: TLabel
|
||||||
|
Left = 12
|
||||||
|
Top = 178
|
||||||
|
Width = 78
|
||||||
|
Height = 15
|
||||||
|
Caption = 'Posizione X / Y'
|
||||||
|
end
|
||||||
|
object lblSize: TLabel
|
||||||
|
Left = 12
|
||||||
|
Top = 226
|
||||||
|
Width = 105
|
||||||
|
Height = 15
|
||||||
|
Caption = 'Larghezza / altezza'
|
||||||
|
end
|
||||||
|
object edtCaption: TEdit
|
||||||
|
Left = 12
|
||||||
|
Top = 100
|
||||||
|
Width = 162
|
||||||
|
Height = 23
|
||||||
|
TabOrder = 0
|
||||||
|
OnChange = PropChanged
|
||||||
|
end
|
||||||
|
object edtFontSize: TEdit
|
||||||
|
Left = 180
|
||||||
|
Top = 100
|
||||||
|
Width = 42
|
||||||
|
Height = 23
|
||||||
|
TabOrder = 10
|
||||||
|
OnChange = PropChanged
|
||||||
|
end
|
||||||
|
object edtSlave: TEdit
|
||||||
|
Left = 12
|
||||||
|
Top = 148
|
||||||
|
Width = 96
|
||||||
|
Height = 23
|
||||||
|
TabOrder = 1
|
||||||
|
OnChange = PropChanged
|
||||||
|
end
|
||||||
|
object edtCanale: TEdit
|
||||||
|
Left = 122
|
||||||
|
Top = 148
|
||||||
|
Width = 100
|
||||||
|
Height = 23
|
||||||
|
TabOrder = 2
|
||||||
|
OnChange = PropChanged
|
||||||
|
end
|
||||||
|
object edtLeft: TEdit
|
||||||
|
Left = 12
|
||||||
|
Top = 196
|
||||||
|
Width = 96
|
||||||
|
Height = 23
|
||||||
|
TabOrder = 3
|
||||||
|
OnChange = PropChanged
|
||||||
|
end
|
||||||
|
object edtTop: TEdit
|
||||||
|
Left = 122
|
||||||
|
Top = 196
|
||||||
|
Width = 100
|
||||||
|
Height = 23
|
||||||
|
TabOrder = 4
|
||||||
|
OnChange = PropChanged
|
||||||
|
end
|
||||||
|
object edtWidth: TEdit
|
||||||
|
Left = 12
|
||||||
|
Top = 244
|
||||||
|
Width = 96
|
||||||
|
Height = 23
|
||||||
|
TabOrder = 5
|
||||||
|
OnChange = PropChanged
|
||||||
|
end
|
||||||
|
object edtHeight: TEdit
|
||||||
|
Left = 122
|
||||||
|
Top = 244
|
||||||
|
Width = 100
|
||||||
|
Height = 23
|
||||||
|
TabOrder = 6
|
||||||
|
OnChange = PropChanged
|
||||||
|
end
|
||||||
|
object gbImgElem: TGroupBox
|
||||||
|
Left = 8
|
||||||
|
Top = 276
|
||||||
|
Width = 228
|
||||||
|
Height = 212
|
||||||
|
Caption = 'Aspetto'
|
||||||
|
TabOrder = 7
|
||||||
|
object lblShape: TLabel
|
||||||
|
Left = 12
|
||||||
|
Top = 20
|
||||||
|
Width = 35
|
||||||
|
Height = 15
|
||||||
|
Caption = 'Forma'
|
||||||
|
end
|
||||||
|
object lblCapPos: TLabel
|
||||||
|
Left = 117
|
||||||
|
Top = 20
|
||||||
|
Width = 51
|
||||||
|
Height = 15
|
||||||
|
Caption = 'Etichetta'
|
||||||
|
end
|
||||||
|
object lblOnColor: TLabel
|
||||||
|
Left = 12
|
||||||
|
Top = 66
|
||||||
|
Width = 148
|
||||||
|
Height = 15
|
||||||
|
Caption = 'Colore acceso / a riposo'
|
||||||
|
end
|
||||||
|
object lblImgOff: TLabel
|
||||||
|
Left = 12
|
||||||
|
Top = 112
|
||||||
|
Width = 178
|
||||||
|
Height = 15
|
||||||
|
Caption = 'Immagine a riposo (vuoto = disegno)'
|
||||||
|
end
|
||||||
|
object lblImgOn: TLabel
|
||||||
|
Left = 12
|
||||||
|
Top = 158
|
||||||
|
Width = 90
|
||||||
|
Height = 15
|
||||||
|
Caption = 'Immagine attiva'
|
||||||
|
end
|
||||||
|
object cboShape: TComboBox
|
||||||
|
Left = 12
|
||||||
|
Top = 38
|
||||||
|
Width = 95
|
||||||
|
Height = 23
|
||||||
|
Style = csDropDownList
|
||||||
|
TabOrder = 0
|
||||||
|
OnChange = PropChanged
|
||||||
|
end
|
||||||
|
object cboCapPos: TComboBox
|
||||||
|
Left = 117
|
||||||
|
Top = 38
|
||||||
|
Width = 95
|
||||||
|
Height = 23
|
||||||
|
Style = csDropDownList
|
||||||
|
TabOrder = 1
|
||||||
|
OnChange = PropChanged
|
||||||
|
end
|
||||||
|
object edtOnColor: TEdit
|
||||||
|
Left = 12
|
||||||
|
Top = 84
|
||||||
|
Width = 95
|
||||||
|
Height = 23
|
||||||
|
TabOrder = 2
|
||||||
|
OnChange = PropChanged
|
||||||
|
end
|
||||||
|
object edtOffColor: TEdit
|
||||||
|
Left = 117
|
||||||
|
Top = 84
|
||||||
|
Width = 95
|
||||||
|
Height = 23
|
||||||
|
TabOrder = 5
|
||||||
|
OnChange = PropChanged
|
||||||
|
end
|
||||||
|
object cboImgOff: TComboBox
|
||||||
|
Left = 12
|
||||||
|
Top = 130
|
||||||
|
Width = 200
|
||||||
|
Height = 23
|
||||||
|
Style = csDropDownList
|
||||||
|
TabOrder = 3
|
||||||
|
OnChange = PropChanged
|
||||||
|
end
|
||||||
|
object cboImgOn: TComboBox
|
||||||
|
Left = 12
|
||||||
|
Top = 176
|
||||||
|
Width = 200
|
||||||
|
Height = 23
|
||||||
|
Style = csDropDownList
|
||||||
|
TabOrder = 4
|
||||||
|
OnChange = PropChanged
|
||||||
|
end
|
||||||
|
end
|
||||||
|
object gbGauge: TGroupBox
|
||||||
|
Left = 8
|
||||||
|
Top = 276
|
||||||
|
Width = 228
|
||||||
|
Height = 272
|
||||||
|
Caption = 'Scala del gauge'
|
||||||
|
TabOrder = 8
|
||||||
|
object lblRaw: TLabel
|
||||||
|
Left = 12
|
||||||
|
Top = 20
|
||||||
|
Width = 133
|
||||||
|
Height = 15
|
||||||
|
Caption = 'Registro: minimo / massimo'
|
||||||
|
end
|
||||||
|
object lblEng: TLabel
|
||||||
|
Left = 12
|
||||||
|
Top = 68
|
||||||
|
Width = 128
|
||||||
|
Height = 15
|
||||||
|
Caption = 'Valore reale: min / max'
|
||||||
|
end
|
||||||
|
object lblUnits: TLabel
|
||||||
|
Left = 12
|
||||||
|
Top = 116
|
||||||
|
Width = 80
|
||||||
|
Height = 15
|
||||||
|
Caption = 'Unit'#224' di misura'
|
||||||
|
end
|
||||||
|
object lblWarn: TLabel
|
||||||
|
Left = 12
|
||||||
|
Top = 164
|
||||||
|
Width = 116
|
||||||
|
Height = 15
|
||||||
|
Caption = 'Allarme sotto / sopra'
|
||||||
|
end
|
||||||
|
object edtRawMin: TEdit
|
||||||
|
Left = 12
|
||||||
|
Top = 38
|
||||||
|
Width = 95
|
||||||
|
Height = 23
|
||||||
|
TabOrder = 0
|
||||||
|
OnChange = PropChanged
|
||||||
|
end
|
||||||
|
object edtRawMax: TEdit
|
||||||
|
Left = 117
|
||||||
|
Top = 38
|
||||||
|
Width = 95
|
||||||
|
Height = 23
|
||||||
|
TabOrder = 1
|
||||||
|
OnChange = PropChanged
|
||||||
|
end
|
||||||
|
object edtEngMin: TEdit
|
||||||
|
Left = 12
|
||||||
|
Top = 86
|
||||||
|
Width = 95
|
||||||
|
Height = 23
|
||||||
|
TabOrder = 2
|
||||||
|
OnChange = PropChanged
|
||||||
|
end
|
||||||
|
object edtEngMax: TEdit
|
||||||
|
Left = 117
|
||||||
|
Top = 86
|
||||||
|
Width = 95
|
||||||
|
Height = 23
|
||||||
|
TabOrder = 3
|
||||||
|
OnChange = PropChanged
|
||||||
|
end
|
||||||
|
object edtUnits: TEdit
|
||||||
|
Left = 12
|
||||||
|
Top = 134
|
||||||
|
Width = 95
|
||||||
|
Height = 23
|
||||||
|
TabOrder = 4
|
||||||
|
OnChange = PropChanged
|
||||||
|
end
|
||||||
|
object edtWarnLo: TEdit
|
||||||
|
Left = 12
|
||||||
|
Top = 182
|
||||||
|
Width = 95
|
||||||
|
Height = 23
|
||||||
|
TabOrder = 5
|
||||||
|
OnChange = PropChanged
|
||||||
|
end
|
||||||
|
object edtWarnHi: TEdit
|
||||||
|
Left = 117
|
||||||
|
Top = 182
|
||||||
|
Width = 95
|
||||||
|
Height = 23
|
||||||
|
TabOrder = 6
|
||||||
|
OnChange = PropChanged
|
||||||
|
end
|
||||||
|
object lblDigits: TLabel
|
||||||
|
Left = 12
|
||||||
|
Top = 212
|
||||||
|
Width = 34
|
||||||
|
Height = 15
|
||||||
|
Caption = 'Cifre'
|
||||||
|
end
|
||||||
|
object lblDecimals: TLabel
|
||||||
|
Left = 117
|
||||||
|
Top = 212
|
||||||
|
Width = 51
|
||||||
|
Height = 15
|
||||||
|
Caption = 'Decimali'
|
||||||
|
end
|
||||||
|
object edtDigits: TEdit
|
||||||
|
Left = 12
|
||||||
|
Top = 230
|
||||||
|
Width = 95
|
||||||
|
Height = 23
|
||||||
|
TabOrder = 7
|
||||||
|
OnChange = PropChanged
|
||||||
|
end
|
||||||
|
object edtDecimals: TEdit
|
||||||
|
Left = 117
|
||||||
|
Top = 230
|
||||||
|
Width = 95
|
||||||
|
Height = 23
|
||||||
|
TabOrder = 8
|
||||||
|
OnChange = PropChanged
|
||||||
|
end
|
||||||
|
end
|
||||||
|
object gbTesto: TGroupBox
|
||||||
|
Left = 8
|
||||||
|
Top = 276
|
||||||
|
Width = 228
|
||||||
|
Height = 170
|
||||||
|
Caption = 'Testo'
|
||||||
|
TabOrder = 12
|
||||||
|
object lblFontName: TLabel
|
||||||
|
Left = 12
|
||||||
|
Top = 20
|
||||||
|
Width = 148
|
||||||
|
Height = 15
|
||||||
|
Caption = 'Font (vuoto = quello del pannello)'
|
||||||
|
end
|
||||||
|
object lblSpacing: TLabel
|
||||||
|
Left = 12
|
||||||
|
Top = 66
|
||||||
|
Width = 128
|
||||||
|
Height = 15
|
||||||
|
Caption = 'Spaziatura fra le lettere'
|
||||||
|
end
|
||||||
|
object lblFrame: TLabel
|
||||||
|
Left = 12
|
||||||
|
Top = 112
|
||||||
|
Width = 128
|
||||||
|
Height = 15
|
||||||
|
Caption = 'Cornice e spessore'
|
||||||
|
end
|
||||||
|
object edtFontName: TEdit
|
||||||
|
Left = 12
|
||||||
|
Top = 38
|
||||||
|
Width = 200
|
||||||
|
Height = 23
|
||||||
|
TabOrder = 0
|
||||||
|
OnChange = PropChanged
|
||||||
|
end
|
||||||
|
object edtSpacing: TEdit
|
||||||
|
Left = 12
|
||||||
|
Top = 84
|
||||||
|
Width = 95
|
||||||
|
Height = 23
|
||||||
|
TabOrder = 1
|
||||||
|
OnChange = PropChanged
|
||||||
|
end
|
||||||
|
object cboFrame: TComboBox
|
||||||
|
Left = 12
|
||||||
|
Top = 130
|
||||||
|
Width = 140
|
||||||
|
Height = 23
|
||||||
|
Style = csDropDownList
|
||||||
|
TabOrder = 2
|
||||||
|
OnChange = PropChanged
|
||||||
|
end
|
||||||
|
object edtFrameWidth: TEdit
|
||||||
|
Left = 160
|
||||||
|
Top = 130
|
||||||
|
Width = 52
|
||||||
|
Height = 23
|
||||||
|
TabOrder = 3
|
||||||
|
OnChange = PropChanged
|
||||||
|
end
|
||||||
|
end
|
||||||
|
object gbRotary: TGroupBox
|
||||||
|
Left = 8
|
||||||
|
Top = 276
|
||||||
|
Width = 228
|
||||||
|
Height = 205
|
||||||
|
Caption = 'Selettore'
|
||||||
|
TabOrder = 11
|
||||||
|
object lblPositions: TLabel
|
||||||
|
Left = 12
|
||||||
|
Top = 20
|
||||||
|
Width = 55
|
||||||
|
Height = 15
|
||||||
|
Caption = 'Posizioni'
|
||||||
|
end
|
||||||
|
object lblLegend: TLabel
|
||||||
|
Left = 12
|
||||||
|
Top = 68
|
||||||
|
Width = 145
|
||||||
|
Height = 15
|
||||||
|
Caption = 'Legenda sopra la manopola'
|
||||||
|
end
|
||||||
|
object cboPositions: TComboBox
|
||||||
|
Left = 12
|
||||||
|
Top = 38
|
||||||
|
Width = 95
|
||||||
|
Height = 23
|
||||||
|
Style = csDropDownList
|
||||||
|
TabOrder = 0
|
||||||
|
OnChange = PropChanged
|
||||||
|
end
|
||||||
|
object edtLegend: TEdit
|
||||||
|
Left = 12
|
||||||
|
Top = 86
|
||||||
|
Width = 200
|
||||||
|
Height = 23
|
||||||
|
TabOrder = 1
|
||||||
|
OnChange = PropChanged
|
||||||
|
end
|
||||||
|
object lblCanale2: TLabel
|
||||||
|
Left = 12
|
||||||
|
Top = 116
|
||||||
|
Width = 200
|
||||||
|
Height = 15
|
||||||
|
Caption = 'Canale lato destro'
|
||||||
|
end
|
||||||
|
object edtCanale2: TEdit
|
||||||
|
Left = 12
|
||||||
|
Top = 134
|
||||||
|
Width = 60
|
||||||
|
Height = 23
|
||||||
|
Hint = 'Bobina del lato destro; vuoto = canale + 1'
|
||||||
|
ParentShowHint = False
|
||||||
|
ShowHint = True
|
||||||
|
TabOrder = 2
|
||||||
|
OnChange = PropChanged
|
||||||
|
end
|
||||||
|
object chkMolla: TCheckBox
|
||||||
|
Left = 12
|
||||||
|
Top = 166
|
||||||
|
Width = 205
|
||||||
|
Height = 32
|
||||||
|
Hint =
|
||||||
|
'Tiene la posizione finch'#233' lo tieni premuto e torna al centro al ' +
|
||||||
|
'rilascio, come il comando di avviamento'
|
||||||
|
Caption = 'Ritorno a molla (torna al centro)'
|
||||||
|
ParentShowHint = False
|
||||||
|
ShowHint = True
|
||||||
|
TabOrder = 3
|
||||||
|
WordWrap = True
|
||||||
|
OnClick = PropChanged
|
||||||
|
end
|
||||||
|
end
|
||||||
|
object gbSuono: TGroupBox
|
||||||
|
Left = 8
|
||||||
|
Top = 492
|
||||||
|
Width = 228
|
||||||
|
Height = 74
|
||||||
|
Caption = 'Allarme sonoro'
|
||||||
|
TabOrder = 12
|
||||||
|
object chkAllarme: TCheckBox
|
||||||
|
Left = 12
|
||||||
|
Top = 18
|
||||||
|
Width = 205
|
||||||
|
Height = 17
|
||||||
|
Hint = 'Suona quando la spia si accende'
|
||||||
|
Caption = 'Suona quando si accende'
|
||||||
|
ParentShowHint = False
|
||||||
|
ShowHint = True
|
||||||
|
TabOrder = 0
|
||||||
|
OnClick = PropChanged
|
||||||
|
end
|
||||||
|
object cboSuono: TComboBox
|
||||||
|
Left = 12
|
||||||
|
Top = 40
|
||||||
|
Width = 134
|
||||||
|
Height = 23
|
||||||
|
Hint = 'File nella cartella "suoni" accanto alla plancia'
|
||||||
|
Style = csDropDownList
|
||||||
|
ParentShowHint = False
|
||||||
|
ShowHint = True
|
||||||
|
TabOrder = 1
|
||||||
|
OnChange = PropChanged
|
||||||
|
end
|
||||||
|
object btnSuoniRileggi: TButton
|
||||||
|
Left = 152
|
||||||
|
Top = 40
|
||||||
|
Width = 66
|
||||||
|
Height = 23
|
||||||
|
Hint = 'Rilegge la cartella dei suoni'
|
||||||
|
Caption = 'Aggiorna'
|
||||||
|
ParentShowHint = False
|
||||||
|
ShowHint = True
|
||||||
|
TabOrder = 2
|
||||||
|
OnClick = btnSuoniRileggiClick
|
||||||
|
end
|
||||||
|
end
|
||||||
|
object btnElimina: TButton
|
||||||
|
Left = 12
|
||||||
|
Top = 566
|
||||||
|
Width = 210
|
||||||
|
Height = 30
|
||||||
|
Caption = 'Elimina elemento'
|
||||||
|
TabOrder = 9
|
||||||
|
OnClick = btnEliminaClick
|
||||||
|
end
|
||||||
|
end
|
||||||
|
object scrCanvas: TScrollBox
|
||||||
|
Left = 200
|
||||||
|
Top = 44
|
||||||
|
Width = 756
|
||||||
|
Height = 686
|
||||||
|
Align = alClient
|
||||||
|
BevelInner = bvNone
|
||||||
|
BevelOuter = bvNone
|
||||||
|
BorderStyle = bsNone
|
||||||
|
Color = clAppWorkSpace
|
||||||
|
ParentColor = False
|
||||||
|
TabOrder = 4
|
||||||
|
object pnlCanvas: TPanel
|
||||||
|
Left = 0
|
||||||
|
Top = 0
|
||||||
|
Width = 1000
|
||||||
|
Height = 640
|
||||||
|
BevelOuter = bvNone
|
||||||
|
Color = clBtnFace
|
||||||
|
ParentBackground = False
|
||||||
|
TabOrder = 0
|
||||||
|
OnDragDrop = CanvasDragDrop
|
||||||
|
OnDragOver = CanvasDragOver
|
||||||
|
OnMouseDown = CanvasMouseDown
|
||||||
|
end
|
||||||
|
end
|
||||||
|
object tmrPoll: TTimer
|
||||||
|
Enabled = False
|
||||||
|
Interval = 500
|
||||||
|
OnTimer = tmrPollTimer
|
||||||
|
Left = 640
|
||||||
|
Top = 96
|
||||||
|
end
|
||||||
|
object dlgApri: TOpenDialog
|
||||||
|
DefaultExt = 'xml'
|
||||||
|
Filter =
|
||||||
|
'Configurazione plancia (*.xml)|*.xml|Tutti i file (*.*)|*.*'
|
||||||
|
Options = [ofHideReadOnly, ofPathMustExist, ofFileMustExist, ofEnableSizing]
|
||||||
|
Title = 'Apri configurazione'
|
||||||
|
Left = 704
|
||||||
|
Top = 96
|
||||||
|
end
|
||||||
|
object dlgSalva: TSaveDialog
|
||||||
|
DefaultExt = 'xml'
|
||||||
|
Filter =
|
||||||
|
'Configurazione plancia (*.xml)|*.xml|Tutti i file (*.*)|*.*'
|
||||||
|
Options = [ofOverwritePrompt, ofHideReadOnly, ofPathMustExist, ofEnableSizing]
|
||||||
|
Title = 'Salva configurazione'
|
||||||
|
Left = 768
|
||||||
|
Top = 96
|
||||||
|
end
|
||||||
|
object dlgImmagine: TOpenPictureDialog
|
||||||
|
Options = [ofHideReadOnly, ofAllowMultiSelect, ofPathMustExist, ofFileMustExist, ofEnableSizing]
|
||||||
|
Title = 'Aggiungi immagini alla libreria'
|
||||||
|
Left = 832
|
||||||
|
Top = 96
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,259 @@
|
|||||||
|
unit uImageLib;
|
||||||
|
|
||||||
|
{
|
||||||
|
Libreria delle immagini usabili sulla plancia.
|
||||||
|
|
||||||
|
Non e' una TImageList: quella impone a tutte le immagini la stessa
|
||||||
|
dimensione e serve per icone di toolbar. Qui invece le immagini hanno misure
|
||||||
|
qualsiasi (un LED da 32 px e un logo da 600 px stanno nella stessa libreria),
|
||||||
|
quindi si tiene una lista di TPicture indicizzata per nome.
|
||||||
|
|
||||||
|
Nell'XML si salvano nome e percorso, non i pixel: le immagini restano file
|
||||||
|
sul disco, sostituibili senza toccare la configurazione. I percorsi sono
|
||||||
|
relativi alla cartella del file di configurazione quando possibile, cosi'
|
||||||
|
l'intera plancia si sposta copiando una cartella.
|
||||||
|
}
|
||||||
|
|
||||||
|
interface
|
||||||
|
|
||||||
|
uses
|
||||||
|
System.SysUtils, System.Classes, System.IOUtils,
|
||||||
|
System.Generics.Collections,
|
||||||
|
Vcl.Graphics, Vcl.Imaging.pngimage, Vcl.Imaging.jpeg;
|
||||||
|
|
||||||
|
type
|
||||||
|
TImageEntry = class
|
||||||
|
public
|
||||||
|
Name: string;
|
||||||
|
/// Percorso come scritto nell'XML (di norma relativo alla sua cartella).
|
||||||
|
FileName: string;
|
||||||
|
Picture: TPicture;
|
||||||
|
/// Messaggio dell'eventuale errore di caricamento; '' se tutto bene.
|
||||||
|
Error: string;
|
||||||
|
destructor Destroy; override;
|
||||||
|
function Loaded: Boolean;
|
||||||
|
end;
|
||||||
|
|
||||||
|
TImageLibrary = class
|
||||||
|
private
|
||||||
|
FItems: TObjectList<TImageEntry>;
|
||||||
|
FBaseDir: string;
|
||||||
|
procedure LoadEntry(AEntry: TImageEntry);
|
||||||
|
public
|
||||||
|
constructor Create;
|
||||||
|
destructor Destroy; override;
|
||||||
|
procedure Clear;
|
||||||
|
/// Cartella di riferimento per i percorsi relativi (quella dell'XML).
|
||||||
|
procedure SetBaseDir(const ADir: string);
|
||||||
|
function Find(const AName: string): TImageEntry;
|
||||||
|
function Picture(const AName: string): TPicture;
|
||||||
|
/// Aggiunge un file alla libreria; il nome e' reso univoco.
|
||||||
|
function AddFile(const AFileName: string): TImageEntry;
|
||||||
|
/// Usata dal caricamento XML: nome e percorso arrivano dal file.
|
||||||
|
function AddEntry(const AName, AFileName: string): TImageEntry;
|
||||||
|
procedure Delete(const AName: string);
|
||||||
|
/// Ricalcola i percorsi rispetto a una nuova cartella di riferimento,
|
||||||
|
/// senza spostare i file: serve al "Salva con nome" in un'altra cartella.
|
||||||
|
procedure Rebase(const ANewDir: string);
|
||||||
|
procedure ReloadAll;
|
||||||
|
procedure FillNames(AStrings: TStrings; AIncludeEmpty: Boolean);
|
||||||
|
function AbsolutePath(const AFileName: string): string;
|
||||||
|
function Count: Integer;
|
||||||
|
function Item(AIndex: Integer): TImageEntry;
|
||||||
|
property BaseDir: string read FBaseDir;
|
||||||
|
end;
|
||||||
|
|
||||||
|
implementation
|
||||||
|
|
||||||
|
{ TImageEntry }
|
||||||
|
|
||||||
|
destructor TImageEntry.Destroy;
|
||||||
|
begin
|
||||||
|
Picture.Free;
|
||||||
|
inherited;
|
||||||
|
end;
|
||||||
|
|
||||||
|
function TImageEntry.Loaded: Boolean;
|
||||||
|
begin
|
||||||
|
Result := (Picture <> nil) and (Picture.Graphic <> nil) and
|
||||||
|
not Picture.Graphic.Empty;
|
||||||
|
end;
|
||||||
|
|
||||||
|
{ TImageLibrary }
|
||||||
|
|
||||||
|
constructor TImageLibrary.Create;
|
||||||
|
begin
|
||||||
|
inherited Create;
|
||||||
|
FItems := TObjectList<TImageEntry>.Create(True);
|
||||||
|
end;
|
||||||
|
|
||||||
|
destructor TImageLibrary.Destroy;
|
||||||
|
begin
|
||||||
|
FItems.Free;
|
||||||
|
inherited;
|
||||||
|
end;
|
||||||
|
|
||||||
|
procedure TImageLibrary.Clear;
|
||||||
|
begin
|
||||||
|
FItems.Clear;
|
||||||
|
end;
|
||||||
|
|
||||||
|
function TImageLibrary.Count: Integer;
|
||||||
|
begin
|
||||||
|
Result := FItems.Count;
|
||||||
|
end;
|
||||||
|
|
||||||
|
function TImageLibrary.Item(AIndex: Integer): TImageEntry;
|
||||||
|
begin
|
||||||
|
Result := FItems[AIndex];
|
||||||
|
end;
|
||||||
|
|
||||||
|
procedure TImageLibrary.SetBaseDir(const ADir: string);
|
||||||
|
begin
|
||||||
|
FBaseDir := IncludeTrailingPathDelimiter(ADir);
|
||||||
|
ReloadAll;
|
||||||
|
end;
|
||||||
|
|
||||||
|
function TImageLibrary.AbsolutePath(const AFileName: string): string;
|
||||||
|
begin
|
||||||
|
if AFileName = '' then
|
||||||
|
Exit('');
|
||||||
|
if TPath.IsPathRooted(AFileName) then
|
||||||
|
Exit(AFileName);
|
||||||
|
Result := FBaseDir + AFileName;
|
||||||
|
end;
|
||||||
|
|
||||||
|
procedure TImageLibrary.LoadEntry(AEntry: TImageEntry);
|
||||||
|
var
|
||||||
|
Full: string;
|
||||||
|
begin
|
||||||
|
FreeAndNil(AEntry.Picture);
|
||||||
|
AEntry.Error := '';
|
||||||
|
Full := AbsolutePath(AEntry.FileName);
|
||||||
|
if not FileExists(Full) then
|
||||||
|
begin
|
||||||
|
AEntry.Error := 'file non trovato: ' + Full;
|
||||||
|
Exit;
|
||||||
|
end;
|
||||||
|
AEntry.Picture := TPicture.Create;
|
||||||
|
try
|
||||||
|
AEntry.Picture.LoadFromFile(Full);
|
||||||
|
except
|
||||||
|
on E: Exception do
|
||||||
|
begin
|
||||||
|
FreeAndNil(AEntry.Picture);
|
||||||
|
AEntry.Error := E.Message;
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
|
||||||
|
procedure TImageLibrary.ReloadAll;
|
||||||
|
var
|
||||||
|
E: TImageEntry;
|
||||||
|
begin
|
||||||
|
for E in FItems do
|
||||||
|
LoadEntry(E);
|
||||||
|
end;
|
||||||
|
|
||||||
|
function TImageLibrary.Find(const AName: string): TImageEntry;
|
||||||
|
var
|
||||||
|
E: TImageEntry;
|
||||||
|
begin
|
||||||
|
if AName <> '' then
|
||||||
|
for E in FItems do
|
||||||
|
if SameText(E.Name, AName) then
|
||||||
|
Exit(E);
|
||||||
|
Result := nil;
|
||||||
|
end;
|
||||||
|
|
||||||
|
function TImageLibrary.Picture(const AName: string): TPicture;
|
||||||
|
var
|
||||||
|
E: TImageEntry;
|
||||||
|
begin
|
||||||
|
E := Find(AName);
|
||||||
|
if (E <> nil) and E.Loaded then
|
||||||
|
Result := E.Picture
|
||||||
|
else
|
||||||
|
Result := nil;
|
||||||
|
end;
|
||||||
|
|
||||||
|
function TImageLibrary.AddEntry(const AName, AFileName: string): TImageEntry;
|
||||||
|
begin
|
||||||
|
Result := TImageEntry.Create;
|
||||||
|
Result.Name := AName;
|
||||||
|
Result.FileName := AFileName;
|
||||||
|
FItems.Add(Result);
|
||||||
|
LoadEntry(Result);
|
||||||
|
end;
|
||||||
|
|
||||||
|
function TImageLibrary.AddFile(const AFileName: string): TImageEntry;
|
||||||
|
var
|
||||||
|
Base, Nome, Rel: string;
|
||||||
|
N: Integer;
|
||||||
|
begin
|
||||||
|
Base := ChangeFileExt(ExtractFileName(AFileName), '');
|
||||||
|
Nome := Base;
|
||||||
|
N := 1;
|
||||||
|
while Find(Nome) <> nil do
|
||||||
|
begin
|
||||||
|
Inc(N);
|
||||||
|
Nome := Format('%s %d', [Base, N]);
|
||||||
|
end;
|
||||||
|
|
||||||
|
// Percorso relativo se il file sta sotto la cartella della configurazione:
|
||||||
|
// cosi' la plancia resta trasportabile copiando la cartella.
|
||||||
|
Rel := AFileName;
|
||||||
|
if (FBaseDir <> '') and AFileName.StartsWith(FBaseDir, True) then
|
||||||
|
Rel := Copy(AFileName, Length(FBaseDir) + 1, MaxInt);
|
||||||
|
|
||||||
|
Result := AddEntry(Nome, Rel);
|
||||||
|
end;
|
||||||
|
|
||||||
|
procedure TImageLibrary.Delete(const AName: string);
|
||||||
|
var
|
||||||
|
E: TImageEntry;
|
||||||
|
begin
|
||||||
|
E := Find(AName);
|
||||||
|
if E <> nil then
|
||||||
|
FItems.Remove(E);
|
||||||
|
end;
|
||||||
|
|
||||||
|
procedure TImageLibrary.Rebase(const ANewDir: string);
|
||||||
|
var
|
||||||
|
E: TImageEntry;
|
||||||
|
Full, NewBase: string;
|
||||||
|
begin
|
||||||
|
NewBase := IncludeTrailingPathDelimiter(ANewDir);
|
||||||
|
if SameText(NewBase, FBaseDir) then
|
||||||
|
Exit;
|
||||||
|
for E in FItems do
|
||||||
|
begin
|
||||||
|
// AbsolutePath usa ancora la vecchia base: e' il punto della manovra.
|
||||||
|
Full := AbsolutePath(E.FileName);
|
||||||
|
if Full = '' then
|
||||||
|
Continue;
|
||||||
|
if Full.StartsWith(NewBase, True) then
|
||||||
|
E.FileName := Copy(Full, Length(NewBase) + 1, MaxInt)
|
||||||
|
else
|
||||||
|
E.FileName := Full;
|
||||||
|
end;
|
||||||
|
FBaseDir := NewBase;
|
||||||
|
end;
|
||||||
|
|
||||||
|
procedure TImageLibrary.FillNames(AStrings: TStrings; AIncludeEmpty: Boolean);
|
||||||
|
var
|
||||||
|
E: TImageEntry;
|
||||||
|
begin
|
||||||
|
AStrings.BeginUpdate;
|
||||||
|
try
|
||||||
|
AStrings.Clear;
|
||||||
|
if AIncludeEmpty then
|
||||||
|
AStrings.Add('');
|
||||||
|
for E in FItems do
|
||||||
|
AStrings.Add(E.Name);
|
||||||
|
finally
|
||||||
|
AStrings.EndUpdate;
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
|
||||||
|
end.
|
||||||
@@ -0,0 +1,606 @@
|
|||||||
|
unit uModbusWorker;
|
||||||
|
|
||||||
|
{
|
||||||
|
Il bus Modbus in un thread suo, cosi' il thread principale non si ferma mai:
|
||||||
|
una richiesta che va in timeout costa mezzo secondo al thread del bus, non
|
||||||
|
alla finestra, che resta trascinabile e ridisegnabile.
|
||||||
|
|
||||||
|
Un thread solo, non uno per le letture e uno per le scritture. Su RS485 il
|
||||||
|
filo e' uno: due richieste insieme si sovrappongono, e le risposte arrivano
|
||||||
|
mescolate. Chi comanda deve comunque aspettare che la richiesta in corso
|
||||||
|
finisca, quindi un secondo thread aggiungerebbe un lucchetto attorno alla
|
||||||
|
porta senza far guadagnare niente. La reattivita' dei comandi si ottiene
|
||||||
|
invece dando loro la precedenza: la coda delle scritture viene svuotata
|
||||||
|
prima di ogni lettura, cosi' un pulsante premuto parte al massimo dopo la
|
||||||
|
richiesta in corso.
|
||||||
|
|
||||||
|
Chi usa questa classe non chiama mai il bus: mette le scritture in coda e
|
||||||
|
ritira le letture quando gli fa comodo (TakeReadings). Nessuna callback
|
||||||
|
cross-thread: il form legge lo stato con il suo timer, e il thread non
|
||||||
|
tocca ne' controlli ne' elementi.
|
||||||
|
}
|
||||||
|
|
||||||
|
interface
|
||||||
|
|
||||||
|
uses
|
||||||
|
Winapi.Windows, System.SysUtils, System.Classes, System.SyncObjs,
|
||||||
|
System.Generics.Collections,
|
||||||
|
uModbusRTU;
|
||||||
|
|
||||||
|
type
|
||||||
|
/// Che funzione Modbus usa una richiesta del piano.
|
||||||
|
TRequestKind = (rqCoils, rqInputs, rqRegisters);
|
||||||
|
|
||||||
|
/// Una richiesta del piano di polling: copre canali contigui di uno slave.
|
||||||
|
TPollRequest = record
|
||||||
|
Kind: TRequestKind;
|
||||||
|
Slave: Integer;
|
||||||
|
First: Integer;
|
||||||
|
Count: Integer;
|
||||||
|
end;
|
||||||
|
|
||||||
|
/// Esito di una richiesta, con i dati se e' andata bene.
|
||||||
|
TPollReading = record
|
||||||
|
Request: TPollRequest;
|
||||||
|
/// Istante in cui la richiesta e' partita sul filo. Serve a capire se una
|
||||||
|
/// risposta e' piu' vecchia di un comando appena dato.
|
||||||
|
Sent: UInt64;
|
||||||
|
Ok: Boolean;
|
||||||
|
Error: string;
|
||||||
|
Bits: TArray<Boolean>;
|
||||||
|
Regs: TArray<Word>;
|
||||||
|
end;
|
||||||
|
|
||||||
|
TWriteCommand = record
|
||||||
|
Slave: Integer;
|
||||||
|
Channel: Integer;
|
||||||
|
Value: Boolean;
|
||||||
|
/// Etichetta dell'elemento, per i messaggi in barra di stato.
|
||||||
|
Caption: string;
|
||||||
|
end;
|
||||||
|
|
||||||
|
TWriteOutcome = record
|
||||||
|
Caption: string;
|
||||||
|
Value: Boolean;
|
||||||
|
Ok: Boolean;
|
||||||
|
Error: string;
|
||||||
|
end;
|
||||||
|
|
||||||
|
/// Quanto uno slave sta rispondendo male, per non inseguirlo a ogni giro.
|
||||||
|
TSlaveState = record
|
||||||
|
Failures: Integer;
|
||||||
|
NextTry: UInt64;
|
||||||
|
end;
|
||||||
|
|
||||||
|
TModbusWorker = class(TThread)
|
||||||
|
private
|
||||||
|
FPort: string;
|
||||||
|
FBaud: Integer;
|
||||||
|
FTimeoutMs: Integer;
|
||||||
|
FPollMs: Integer;
|
||||||
|
FLock: TCriticalSection;
|
||||||
|
/// Sveglia il thread: nuova scrittura, piano cambiato, o chiusura.
|
||||||
|
FWake: TEvent;
|
||||||
|
// Tutto quello che segue si tocca solo con FLock preso.
|
||||||
|
FPlan: TArray<TPollRequest>;
|
||||||
|
/// Piano appena cambiato: gli slave vanno riprovati subito, perche' fra i
|
||||||
|
/// canali nuovi puo' esserci un modulo appena montato.
|
||||||
|
FPlanFresh: Boolean;
|
||||||
|
FWrites: TQueue<TWriteCommand>;
|
||||||
|
FReadings: TArray<TPollReading>;
|
||||||
|
FReadingsFresh: Boolean;
|
||||||
|
FOutcomes: TList<TWriteOutcome>;
|
||||||
|
FConnected: Boolean;
|
||||||
|
FStatus: string;
|
||||||
|
FStatusOk: Boolean;
|
||||||
|
/// Stato di ogni RICHIESTA del piano: quante volte di fila e' fallita e
|
||||||
|
/// quando riprovarla. Per richiesta e non per slave: sullo stesso modulo
|
||||||
|
/// le bobine possono rispondere benissimo mentre gli ingressi, che non
|
||||||
|
/// ha, danno errore, e mettere in pausa tutto il modulo per colpa loro
|
||||||
|
/// lascerebbe la plancia cieca su quello che invece si legge.
|
||||||
|
FBackoff: TDictionary<string, TSlaveState>;
|
||||||
|
/// A chi tocca, fra le richieste rimandate, il tentativo di questo giro.
|
||||||
|
FRetryTurn: Integer;
|
||||||
|
procedure SetState(AConnected: Boolean; const AMsg: string; AOk: Boolean);
|
||||||
|
function CurrentPlan: TArray<TPollRequest>;
|
||||||
|
function TakeWrite(out ACmd: TWriteCommand): Boolean;
|
||||||
|
procedure AddOutcome(const ACmd: TWriteCommand; AOk: Boolean;
|
||||||
|
const AError: string);
|
||||||
|
procedure PublishReadings(const AReadings: TArray<TPollReading>);
|
||||||
|
/// Svuota la coda dei comandi. False se la porta e' caduta.
|
||||||
|
function FlushWrites(AModbus: TModbusRTU): Boolean;
|
||||||
|
function ReadOne(AModbus: TModbusRTU;
|
||||||
|
const ARequest: TPollRequest): TPollReading;
|
||||||
|
/// True se lo slave va interrogato adesso; ASecondsLeft dice quanto manca
|
||||||
|
/// al prossimo tentativo quando e' in pausa.
|
||||||
|
function RequestDue(const ARequest: TPollRequest;
|
||||||
|
out ASecondsLeft: Integer): Boolean;
|
||||||
|
/// Vero se la richiesta non ha ancora sbagliato: quelle che sbagliano si
|
||||||
|
/// rimandano in fondo al giro.
|
||||||
|
function FirstTry(const ARequest: TPollRequest): Boolean;
|
||||||
|
procedure NoteRequest(const ARequest: TPollRequest; AOk: Boolean);
|
||||||
|
protected
|
||||||
|
procedure Execute; override;
|
||||||
|
public
|
||||||
|
constructor Create(const APort: string; ABaud, ATimeoutMs,
|
||||||
|
APollMs: Integer);
|
||||||
|
destructor Destroy; override;
|
||||||
|
/// Sostituisce il piano di polling; il giro dopo usa questo.
|
||||||
|
procedure SetPlan(const APlan: TArray<TPollRequest>);
|
||||||
|
/// Mette un comando in coda e torna subito.
|
||||||
|
procedure EnqueueWrite(ASlave, AChannel: Integer; AValue: Boolean;
|
||||||
|
const ACaption: string);
|
||||||
|
/// Letture dell'ultimo giro completo, una volta sola: False se non ce ne
|
||||||
|
/// sono di nuove da quando sono state ritirate.
|
||||||
|
function TakeReadings(out AReadings: TArray<TPollReading>): Boolean;
|
||||||
|
/// Esiti delle scritture eseguite da quando sono stati ritirati.
|
||||||
|
function TakeOutcomes: TArray<TWriteOutcome>;
|
||||||
|
function IsBusConnected: Boolean;
|
||||||
|
procedure CurrentStatus(out AMsg: string; out AOk: Boolean);
|
||||||
|
/// Chiede la chiusura e sveglia il thread; non attende.
|
||||||
|
procedure Stop;
|
||||||
|
end;
|
||||||
|
|
||||||
|
implementation
|
||||||
|
|
||||||
|
const
|
||||||
|
/// Porta assente o occupata: si riprova, senza martellare.
|
||||||
|
RETRY_MS = 2000;
|
||||||
|
/// Errori di fila dopo i quali uno slave e' considerato assente. Tre, non
|
||||||
|
/// uno: un disturbo sulla linea non deve mettere in pausa un modulo che c'e'.
|
||||||
|
SLAVE_TOLERANCE = 3;
|
||||||
|
/// Prima pausa di una richiesta che non risponde. Una plancia puo' essere disegnata
|
||||||
|
/// prima che i moduli siano montati: le sue richieste non devono rubare mezzo
|
||||||
|
/// secondo di timeout a ogni giro a quelle dei moduli che ci sono. Appena il
|
||||||
|
/// modulo viene collegato riparte da solo, entro questa pausa.
|
||||||
|
SLAVE_BACKOFF_MS = 5000;
|
||||||
|
/// La pausa si allunga a ogni tentativo andato a vuoto, fino a questo
|
||||||
|
/// massimo: un modulo che non c'e' costa un timeout al minuto invece di uno
|
||||||
|
/// ogni cinque secondi, e il giro resta veloce per i moduli che ci sono.
|
||||||
|
MAX_BACKOFF_MS = 60000;
|
||||||
|
|
||||||
|
{ TModbusWorker }
|
||||||
|
|
||||||
|
constructor TModbusWorker.Create(const APort: string; ABaud, ATimeoutMs,
|
||||||
|
APollMs: Integer);
|
||||||
|
begin
|
||||||
|
FPort := APort;
|
||||||
|
FBaud := ABaud;
|
||||||
|
FTimeoutMs := ATimeoutMs;
|
||||||
|
FPollMs := APollMs;
|
||||||
|
if FPollMs < 20 then
|
||||||
|
FPollMs := 20;
|
||||||
|
FLock := TCriticalSection.Create;
|
||||||
|
FWake := TEvent.Create(nil, False, False, '');
|
||||||
|
FWrites := TQueue<TWriteCommand>.Create;
|
||||||
|
FOutcomes := TList<TWriteOutcome>.Create;
|
||||||
|
FBackoff := TDictionary<string, TSlaveState>.Create;
|
||||||
|
FStatus := 'Apertura porta...';
|
||||||
|
FStatusOk := True;
|
||||||
|
// Il thread parte solo quando tutto e' pronto.
|
||||||
|
inherited Create(False);
|
||||||
|
end;
|
||||||
|
|
||||||
|
destructor TModbusWorker.Destroy;
|
||||||
|
begin
|
||||||
|
// Prima si sveglia, poi si aspetta: l'attesa dura al massimo quanto la
|
||||||
|
// richiesta in corso.
|
||||||
|
Stop;
|
||||||
|
inherited;
|
||||||
|
FBackoff.Free;
|
||||||
|
FOutcomes.Free;
|
||||||
|
FWrites.Free;
|
||||||
|
FWake.Free;
|
||||||
|
FLock.Free;
|
||||||
|
end;
|
||||||
|
|
||||||
|
procedure TModbusWorker.Stop;
|
||||||
|
begin
|
||||||
|
Terminate;
|
||||||
|
FWake.SetEvent;
|
||||||
|
end;
|
||||||
|
|
||||||
|
procedure TModbusWorker.SetState(AConnected: Boolean; const AMsg: string;
|
||||||
|
AOk: Boolean);
|
||||||
|
begin
|
||||||
|
FLock.Enter;
|
||||||
|
try
|
||||||
|
FConnected := AConnected;
|
||||||
|
FStatus := AMsg;
|
||||||
|
FStatusOk := AOk;
|
||||||
|
finally
|
||||||
|
FLock.Leave;
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
|
||||||
|
function TModbusWorker.IsBusConnected: Boolean;
|
||||||
|
begin
|
||||||
|
FLock.Enter;
|
||||||
|
try
|
||||||
|
Result := FConnected;
|
||||||
|
finally
|
||||||
|
FLock.Leave;
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
|
||||||
|
procedure TModbusWorker.CurrentStatus(out AMsg: string; out AOk: Boolean);
|
||||||
|
begin
|
||||||
|
FLock.Enter;
|
||||||
|
try
|
||||||
|
AMsg := FStatus;
|
||||||
|
AOk := FStatusOk;
|
||||||
|
finally
|
||||||
|
FLock.Leave;
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
|
||||||
|
procedure TModbusWorker.SetPlan(const APlan: TArray<TPollRequest>);
|
||||||
|
begin
|
||||||
|
FLock.Enter;
|
||||||
|
try
|
||||||
|
FPlan := Copy(APlan);
|
||||||
|
FPlanFresh := True;
|
||||||
|
finally
|
||||||
|
FLock.Leave;
|
||||||
|
end;
|
||||||
|
FWake.SetEvent;
|
||||||
|
end;
|
||||||
|
|
||||||
|
function TModbusWorker.CurrentPlan: TArray<TPollRequest>;
|
||||||
|
begin
|
||||||
|
FLock.Enter;
|
||||||
|
try
|
||||||
|
Result := Copy(FPlan);
|
||||||
|
if FPlanFresh then
|
||||||
|
begin
|
||||||
|
FPlanFresh := False;
|
||||||
|
// Piano nuovo: nessuno slave resta in pausa per colpa del piano vecchio.
|
||||||
|
FBackoff.Clear;
|
||||||
|
end;
|
||||||
|
finally
|
||||||
|
FLock.Leave;
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
|
||||||
|
procedure TModbusWorker.EnqueueWrite(ASlave, AChannel: Integer;
|
||||||
|
AValue: Boolean; const ACaption: string);
|
||||||
|
var
|
||||||
|
Cmd: TWriteCommand;
|
||||||
|
begin
|
||||||
|
Cmd.Slave := ASlave;
|
||||||
|
Cmd.Channel := AChannel;
|
||||||
|
Cmd.Value := AValue;
|
||||||
|
Cmd.Caption := ACaption;
|
||||||
|
FLock.Enter;
|
||||||
|
try
|
||||||
|
FWrites.Enqueue(Cmd);
|
||||||
|
finally
|
||||||
|
FLock.Leave;
|
||||||
|
end;
|
||||||
|
// Non aspetta il prossimo giro di polling: il comando parte appena la
|
||||||
|
// richiesta in corso e' finita.
|
||||||
|
FWake.SetEvent;
|
||||||
|
end;
|
||||||
|
|
||||||
|
function TModbusWorker.TakeWrite(out ACmd: TWriteCommand): Boolean;
|
||||||
|
begin
|
||||||
|
FLock.Enter;
|
||||||
|
try
|
||||||
|
Result := FWrites.Count > 0;
|
||||||
|
if Result then
|
||||||
|
ACmd := FWrites.Dequeue;
|
||||||
|
finally
|
||||||
|
FLock.Leave;
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
|
||||||
|
procedure TModbusWorker.AddOutcome(const ACmd: TWriteCommand; AOk: Boolean;
|
||||||
|
const AError: string);
|
||||||
|
var
|
||||||
|
O: TWriteOutcome;
|
||||||
|
begin
|
||||||
|
O.Caption := ACmd.Caption;
|
||||||
|
O.Value := ACmd.Value;
|
||||||
|
O.Ok := AOk;
|
||||||
|
O.Error := AError;
|
||||||
|
FLock.Enter;
|
||||||
|
try
|
||||||
|
// Se nessuno li ritira (finestra occupata) non devono crescere all'infinito.
|
||||||
|
while FOutcomes.Count > 200 do
|
||||||
|
FOutcomes.Delete(0);
|
||||||
|
FOutcomes.Add(O);
|
||||||
|
finally
|
||||||
|
FLock.Leave;
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
|
||||||
|
function TModbusWorker.TakeOutcomes: TArray<TWriteOutcome>;
|
||||||
|
begin
|
||||||
|
FLock.Enter;
|
||||||
|
try
|
||||||
|
Result := FOutcomes.ToArray;
|
||||||
|
FOutcomes.Clear;
|
||||||
|
finally
|
||||||
|
FLock.Leave;
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
|
||||||
|
procedure TModbusWorker.PublishReadings(const AReadings: TArray<TPollReading>);
|
||||||
|
begin
|
||||||
|
FLock.Enter;
|
||||||
|
try
|
||||||
|
FReadings := AReadings;
|
||||||
|
FReadingsFresh := True;
|
||||||
|
finally
|
||||||
|
FLock.Leave;
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
|
||||||
|
function TModbusWorker.TakeReadings(out AReadings: TArray<TPollReading>): Boolean;
|
||||||
|
begin
|
||||||
|
FLock.Enter;
|
||||||
|
try
|
||||||
|
Result := FReadingsFresh;
|
||||||
|
if Result then
|
||||||
|
begin
|
||||||
|
AReadings := FReadings;
|
||||||
|
FReadingsFresh := False;
|
||||||
|
end;
|
||||||
|
finally
|
||||||
|
FLock.Leave;
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
|
||||||
|
function TModbusWorker.FlushWrites(AModbus: TModbusRTU): Boolean;
|
||||||
|
var
|
||||||
|
Cmd: TWriteCommand;
|
||||||
|
begin
|
||||||
|
Result := True;
|
||||||
|
while not Terminated and TakeWrite(Cmd) do
|
||||||
|
begin
|
||||||
|
try
|
||||||
|
AModbus.WriteSingleCoil(Cmd.Slave, Cmd.Channel, Cmd.Value);
|
||||||
|
AddOutcome(Cmd, True, '');
|
||||||
|
except
|
||||||
|
on E: Exception do
|
||||||
|
begin
|
||||||
|
AddOutcome(Cmd, False, E.Message);
|
||||||
|
// La porta non c'e' piu' (chiavetta staccata): si riapre da capo.
|
||||||
|
if not AModbus.IsConnected then
|
||||||
|
Exit(False);
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
|
||||||
|
/// Chiave di una richiesta nel registro delle pause.
|
||||||
|
function RequestKey(const ARequest: TPollRequest): string;
|
||||||
|
begin
|
||||||
|
Result := Format('%d:%d:%d:%d', [ARequest.Slave, Ord(ARequest.Kind),
|
||||||
|
ARequest.First, ARequest.Count]);
|
||||||
|
end;
|
||||||
|
|
||||||
|
function TModbusWorker.RequestDue(const ARequest: TPollRequest;
|
||||||
|
out ASecondsLeft: Integer): Boolean;
|
||||||
|
var
|
||||||
|
S: TSlaveState;
|
||||||
|
Now: UInt64;
|
||||||
|
begin
|
||||||
|
ASecondsLeft := 0;
|
||||||
|
if not FBackoff.TryGetValue(RequestKey(ARequest), S) then
|
||||||
|
Exit(True);
|
||||||
|
if S.Failures < SLAVE_TOLERANCE then
|
||||||
|
Exit(True);
|
||||||
|
Now := GetTickCount64;
|
||||||
|
Result := Now >= S.NextTry;
|
||||||
|
if not Result then
|
||||||
|
ASecondsLeft := Integer((S.NextTry - Now + 999) div 1000);
|
||||||
|
end;
|
||||||
|
|
||||||
|
function TModbusWorker.FirstTry(const ARequest: TPollRequest): Boolean;
|
||||||
|
var
|
||||||
|
S: TSlaveState;
|
||||||
|
begin
|
||||||
|
Result := not FBackoff.TryGetValue(RequestKey(ARequest), S) or
|
||||||
|
(S.Failures = 0);
|
||||||
|
end;
|
||||||
|
|
||||||
|
procedure TModbusWorker.NoteRequest(const ARequest: TPollRequest;
|
||||||
|
AOk: Boolean);
|
||||||
|
var
|
||||||
|
S: TSlaveState;
|
||||||
|
Attesa: Integer;
|
||||||
|
begin
|
||||||
|
if not FBackoff.TryGetValue(RequestKey(ARequest), S) then
|
||||||
|
begin
|
||||||
|
S.Failures := 0;
|
||||||
|
S.NextTry := 0;
|
||||||
|
end;
|
||||||
|
if AOk then
|
||||||
|
begin
|
||||||
|
// Ha risposto: torna una richiesta normale, fatta a ogni giro.
|
||||||
|
S.Failures := 0;
|
||||||
|
S.NextTry := 0;
|
||||||
|
end
|
||||||
|
else
|
||||||
|
begin
|
||||||
|
Inc(S.Failures);
|
||||||
|
if S.Failures >= SLAVE_TOLERANCE then
|
||||||
|
begin
|
||||||
|
Attesa := SLAVE_BACKOFF_MS * (S.Failures - SLAVE_TOLERANCE + 1);
|
||||||
|
if Attesa > MAX_BACKOFF_MS then
|
||||||
|
Attesa := MAX_BACKOFF_MS;
|
||||||
|
S.NextTry := GetTickCount64 + UInt64(Attesa);
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
FBackoff.AddOrSetValue(RequestKey(ARequest), S);
|
||||||
|
end;
|
||||||
|
|
||||||
|
function TModbusWorker.ReadOne(AModbus: TModbusRTU;
|
||||||
|
const ARequest: TPollRequest): TPollReading;
|
||||||
|
begin
|
||||||
|
Result := Default(TPollReading);
|
||||||
|
Result.Request := ARequest;
|
||||||
|
Result.Sent := GetTickCount64;
|
||||||
|
try
|
||||||
|
case ARequest.Kind of
|
||||||
|
rqCoils:
|
||||||
|
Result.Bits := AModbus.ReadCoils(ARequest.Slave, ARequest.First,
|
||||||
|
ARequest.Count);
|
||||||
|
rqInputs:
|
||||||
|
Result.Bits := AModbus.ReadDiscreteInputs(ARequest.Slave,
|
||||||
|
ARequest.First, ARequest.Count);
|
||||||
|
rqRegisters:
|
||||||
|
Result.Regs := AModbus.ReadHoldingRegisters(ARequest.Slave,
|
||||||
|
ARequest.First, ARequest.Count);
|
||||||
|
end;
|
||||||
|
Result.Ok := True;
|
||||||
|
except
|
||||||
|
on E: Exception do
|
||||||
|
begin
|
||||||
|
Result.Ok := False;
|
||||||
|
Result.Error := E.Message;
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
|
||||||
|
procedure TModbusWorker.Execute;
|
||||||
|
var
|
||||||
|
Modbus: TModbusRTU;
|
||||||
|
Plan: TArray<TPollRequest>;
|
||||||
|
Rimandate: TArray<TPollRequest>;
|
||||||
|
Readings: TArray<TPollReading>;
|
||||||
|
Reading: TPollReading;
|
||||||
|
I, J, Wait, Left: Integer;
|
||||||
|
Started: UInt64;
|
||||||
|
begin
|
||||||
|
NameThreadForDebugging('ModbusBus');
|
||||||
|
Modbus := nil;
|
||||||
|
try
|
||||||
|
while not Terminated do
|
||||||
|
begin
|
||||||
|
// 1. porta aperta? altrimenti si riprova ogni RETRY_MS, senza rumore
|
||||||
|
// sul bus e senza bloccare nessuno.
|
||||||
|
if Modbus = nil then
|
||||||
|
begin
|
||||||
|
try
|
||||||
|
Modbus := TModbusRTU.Create(FPort, FBaud, FTimeoutMs);
|
||||||
|
Modbus.Connect;
|
||||||
|
// Porta riaperta: si riparte interrogando tutti, anche quelli che
|
||||||
|
// prima erano muti.
|
||||||
|
FBackoff.Clear;
|
||||||
|
SetState(True, 'Connesso.', True);
|
||||||
|
except
|
||||||
|
on E: Exception do
|
||||||
|
begin
|
||||||
|
FreeAndNil(Modbus);
|
||||||
|
SetState(False, 'Connessione fallita: ' + E.Message, False);
|
||||||
|
FWake.WaitFor(RETRY_MS);
|
||||||
|
Continue;
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
|
||||||
|
Started := GetTickCount64;
|
||||||
|
|
||||||
|
// 2. i comandi passano avanti alle letture.
|
||||||
|
if not FlushWrites(Modbus) then
|
||||||
|
begin
|
||||||
|
FreeAndNil(Modbus);
|
||||||
|
SetState(False, 'Porta caduta, riapertura...', False);
|
||||||
|
Continue;
|
||||||
|
end;
|
||||||
|
|
||||||
|
// 3. un giro di letture, una richiesta per gruppo. Fra una richiesta e
|
||||||
|
// l'altra si guarda di nuovo la coda dei comandi: un pulsante premuto
|
||||||
|
// non deve aspettare la fine del giro.
|
||||||
|
Plan := CurrentPlan;
|
||||||
|
SetLength(Readings, 0);
|
||||||
|
SetLength(Rimandate, 0);
|
||||||
|
for I := 0 to High(Plan) do
|
||||||
|
begin
|
||||||
|
if Terminated then
|
||||||
|
Break;
|
||||||
|
if not FlushWrites(Modbus) then
|
||||||
|
Break;
|
||||||
|
if Modbus = nil then
|
||||||
|
Break;
|
||||||
|
// Una richiesta che non risponde da un po' si interroga di rado: una
|
||||||
|
// plancia disegnata prima dei moduli resta comunque scorrevole, e
|
||||||
|
// quello che c'e' viene letto alla velocita' giusta.
|
||||||
|
if not RequestDue(Plan[I], Left) then
|
||||||
|
begin
|
||||||
|
Reading := Default(TPollReading);
|
||||||
|
Reading.Request := Plan[I];
|
||||||
|
Reading.Sent := GetTickCount64;
|
||||||
|
Reading.Ok := False;
|
||||||
|
Reading.Error := Format('non risponde, riprovo fra %d s', [Left]);
|
||||||
|
Readings := Readings + [Reading];
|
||||||
|
Continue;
|
||||||
|
end;
|
||||||
|
// Le richieste gia' in difficolta' si rimandano in fondo al giro, e
|
||||||
|
// se ne riprova UNA SOLA per giro: ogni tentativo a vuoto costa un
|
||||||
|
// timeout, e otto timeout davanti alle spie voleva dire vedere un
|
||||||
|
// allarme quattro secondi dopo che era arrivato.
|
||||||
|
if not FirstTry(Plan[I]) then
|
||||||
|
begin
|
||||||
|
Rimandate := Rimandate + [Plan[I]];
|
||||||
|
Continue;
|
||||||
|
end;
|
||||||
|
Reading := ReadOne(Modbus, Plan[I]);
|
||||||
|
NoteRequest(Plan[I], Reading.Ok);
|
||||||
|
Readings := Readings + [Reading];
|
||||||
|
end;
|
||||||
|
|
||||||
|
// Le rimandate: una per giro, a turno, cosi' un modulo che torna viene
|
||||||
|
// ritrovato senza rallentare gli altri.
|
||||||
|
if (Length(Rimandate) > 0) and not Terminated and (Modbus <> nil) then
|
||||||
|
begin
|
||||||
|
if FRetryTurn >= Length(Rimandate) then
|
||||||
|
FRetryTurn := 0;
|
||||||
|
Reading := ReadOne(Modbus, Rimandate[FRetryTurn]);
|
||||||
|
NoteRequest(Rimandate[FRetryTurn], Reading.Ok);
|
||||||
|
Readings := Readings + [Reading];
|
||||||
|
Inc(FRetryTurn);
|
||||||
|
// Le altre rimandate: si dice che non sono state fatte adesso, senza
|
||||||
|
// spacciarle per fallite.
|
||||||
|
for J := 0 to High(Rimandate) do
|
||||||
|
if J <> FRetryTurn - 1 then
|
||||||
|
begin
|
||||||
|
Reading := Default(TPollReading);
|
||||||
|
Reading.Request := Rimandate[J];
|
||||||
|
Reading.Sent := GetTickCount64;
|
||||||
|
Reading.Ok := False;
|
||||||
|
Reading.Error := 'non risponde, riprovata a turno';
|
||||||
|
Readings := Readings + [Reading];
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
if Length(Readings) > 0 then
|
||||||
|
PublishReadings(Readings);
|
||||||
|
if Terminated then
|
||||||
|
Break;
|
||||||
|
if (Modbus <> nil) and not Modbus.IsConnected then
|
||||||
|
begin
|
||||||
|
FreeAndNil(Modbus);
|
||||||
|
SetState(False, 'Porta caduta, riapertura...', False);
|
||||||
|
Continue;
|
||||||
|
end;
|
||||||
|
|
||||||
|
// 4. riposo fino al prossimo giro, interrompibile da un comando o
|
||||||
|
// dalla chiusura.
|
||||||
|
Wait := FPollMs - Integer(GetTickCount64 - Started);
|
||||||
|
if Wait < 1 then
|
||||||
|
Wait := 1;
|
||||||
|
FWake.WaitFor(Wait);
|
||||||
|
end;
|
||||||
|
finally
|
||||||
|
if Modbus <> nil then
|
||||||
|
begin
|
||||||
|
Modbus.Disconnect;
|
||||||
|
Modbus.Free;
|
||||||
|
end;
|
||||||
|
SetState(False, 'Bus chiuso.', True);
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
|
||||||
|
end.
|
||||||
@@ -0,0 +1,687 @@
|
|||||||
|
unit uPlanciaConfig;
|
||||||
|
|
||||||
|
{
|
||||||
|
Configurazione della plancia e sua persistenza su XML.
|
||||||
|
|
||||||
|
Il DOM usato e' OmniXML (Pascal puro) e non MSXML: evita la dipendenza da
|
||||||
|
COM, che su un PC di bordo puo' essere un problema in meno.
|
||||||
|
|
||||||
|
I numeri con virgola sono letti e scritti sempre con il punto decimale
|
||||||
|
(FormatSettings invariante), altrimenti un file salvato su una macchina
|
||||||
|
italiana non si riaprirebbe su una inglese e viceversa.
|
||||||
|
}
|
||||||
|
|
||||||
|
interface
|
||||||
|
|
||||||
|
uses
|
||||||
|
Winapi.Windows,
|
||||||
|
System.SysUtils, System.Classes, System.Variants,
|
||||||
|
System.Generics.Collections,
|
||||||
|
Vcl.Graphics,
|
||||||
|
Xml.XMLIntf, Xml.XMLDoc, Xml.xmldom, Xml.omnixmldom,
|
||||||
|
uGauge, uImageLib, uPlanciaElements;
|
||||||
|
|
||||||
|
type
|
||||||
|
EPlanciaConfig = class(Exception);
|
||||||
|
|
||||||
|
/// Fotografia di quello che si cambia disegnando: elementi e impostazioni
|
||||||
|
/// del pannello. Serve ad annulla/ripeti. La libreria immagini non c'e':
|
||||||
|
/// le immagini sono file sul disco, e una rimossa non torna con un annulla
|
||||||
|
/// (gli elementi che la usavano ritrovano il nome, non il file).
|
||||||
|
TPlanciaSnapshot = class
|
||||||
|
private
|
||||||
|
FElements: TObjectList<TElementDef>;
|
||||||
|
public
|
||||||
|
Port: string;
|
||||||
|
Baud: Integer;
|
||||||
|
TimeoutMs: Integer;
|
||||||
|
PollMs: Integer;
|
||||||
|
Title: string;
|
||||||
|
PanelWidth: Integer;
|
||||||
|
PanelHeight: Integer;
|
||||||
|
Background: string;
|
||||||
|
Ink: TColor;
|
||||||
|
/// Indice dell'elemento selezionato al momento della foto; -1 nessuno.
|
||||||
|
Selected: Integer;
|
||||||
|
constructor Create;
|
||||||
|
destructor Destroy; override;
|
||||||
|
end;
|
||||||
|
|
||||||
|
TPlanciaConfig = class
|
||||||
|
private
|
||||||
|
FElements: TObjectList<TElementDef>;
|
||||||
|
FImages: TImageLibrary;
|
||||||
|
public
|
||||||
|
Port: string;
|
||||||
|
Baud: Integer;
|
||||||
|
TimeoutMs: Integer;
|
||||||
|
PollMs: Integer;
|
||||||
|
Title: string;
|
||||||
|
PanelWidth: Integer;
|
||||||
|
PanelHeight: Integer;
|
||||||
|
/// Nome dell'immagine di sfondo nella libreria; '' = nessuno sfondo.
|
||||||
|
Background: string;
|
||||||
|
/// Colore delle scritte serigrafate; clNone = nero. Le plance a fondo
|
||||||
|
/// scuro lo mettono chiaro, altrimenti le etichette sparirebbero.
|
||||||
|
Ink: TColor;
|
||||||
|
/// Cartella del file XML: i suoi sottoalberi (immagini, suoni) viaggiano
|
||||||
|
/// con la plancia quando si copia la cartella.
|
||||||
|
BaseDir: string;
|
||||||
|
/// Cartella dei suoni di allarme, dentro quella dell'XML.
|
||||||
|
function SoundsDir: string;
|
||||||
|
constructor Create;
|
||||||
|
destructor Destroy; override;
|
||||||
|
procedure Clear;
|
||||||
|
procedure LoadFromFile(const AFileName: string);
|
||||||
|
procedure SaveToFile(const AFileName: string);
|
||||||
|
/// Copia profonda dello stato attuale; la libera il chiamante.
|
||||||
|
function TakeSnapshot: TPlanciaSnapshot;
|
||||||
|
/// Riporta elementi e impostazioni a quelli della foto. Le definizioni
|
||||||
|
/// vengono ricreate: i controlli che puntavano alle vecchie vanno rifatti.
|
||||||
|
procedure RestoreSnapshot(ASnapshot: TPlanciaSnapshot);
|
||||||
|
property Elements: TObjectList<TElementDef> read FElements;
|
||||||
|
property Images: TImageLibrary read FImages;
|
||||||
|
end;
|
||||||
|
|
||||||
|
/// Percorso del file di configurazione predefinito, accanto all'eseguibile.
|
||||||
|
function DefaultConfigFile: string;
|
||||||
|
/// Percorso dell'ultima plancia usata, ricordato accanto all'eseguibile, ''
|
||||||
|
/// se non c'e' o il file non esiste piu'. Serve a riaprire da sola la plancia
|
||||||
|
/// su cui si stava lavorando.
|
||||||
|
function LoadLastFile: string;
|
||||||
|
procedure SaveLastFile(const AFileName: string);
|
||||||
|
/// Legge solo il titolo di una plancia, senza caricare immagini ed elementi.
|
||||||
|
/// False se il file non si apre o non e' una configurazione di plancia: serve
|
||||||
|
/// a elencare le plance di una cartella scartando gli altri XML.
|
||||||
|
function ReadPanelTitle(const AFileName: string; out ATitle: string): Boolean;
|
||||||
|
/// Colori in formato #RRGGBB, come sul web: e' quello che ci si aspetta
|
||||||
|
/// scrivendo a mano l'XML, non il $00BBGGRR di Windows.
|
||||||
|
function HtmlToColor(const AValue: string; ADefault: TColor): TColor;
|
||||||
|
function ColorToHtml(AColor: TColor): string;
|
||||||
|
|
||||||
|
implementation
|
||||||
|
|
||||||
|
var
|
||||||
|
FS: TFormatSettings;
|
||||||
|
|
||||||
|
function DefaultConfigFile: string;
|
||||||
|
begin
|
||||||
|
Result := IncludeTrailingPathDelimiter(ExtractFilePath(ParamStr(0))) +
|
||||||
|
'plancia.xml';
|
||||||
|
end;
|
||||||
|
|
||||||
|
/// Il promemoria sta accanto all'eseguibile, non nel registro: la plancia si
|
||||||
|
/// trasporta copiando una cartella, e con lei quello che stava aperto.
|
||||||
|
function LastFileStore: string;
|
||||||
|
begin
|
||||||
|
Result := IncludeTrailingPathDelimiter(ExtractFilePath(ParamStr(0))) +
|
||||||
|
'plancia-ultima.txt';
|
||||||
|
end;
|
||||||
|
|
||||||
|
function LoadLastFile: string;
|
||||||
|
var
|
||||||
|
Lines: TStringList;
|
||||||
|
begin
|
||||||
|
Result := '';
|
||||||
|
if not FileExists(LastFileStore) then
|
||||||
|
Exit;
|
||||||
|
Lines := TStringList.Create;
|
||||||
|
try
|
||||||
|
try
|
||||||
|
Lines.LoadFromFile(LastFileStore, TEncoding.UTF8);
|
||||||
|
if Lines.Count > 0 then
|
||||||
|
Result := Trim(Lines[0]);
|
||||||
|
except
|
||||||
|
// Promemoria illeggibile: si riparte dal file predefinito.
|
||||||
|
Result := '';
|
||||||
|
end;
|
||||||
|
finally
|
||||||
|
Lines.Free;
|
||||||
|
end;
|
||||||
|
// Un file cancellato o su una chiavetta scollegata non vale piu'.
|
||||||
|
if (Result <> '') and not FileExists(Result) then
|
||||||
|
Result := '';
|
||||||
|
end;
|
||||||
|
|
||||||
|
procedure SaveLastFile(const AFileName: string);
|
||||||
|
var
|
||||||
|
Lines: TStringList;
|
||||||
|
begin
|
||||||
|
Lines := TStringList.Create;
|
||||||
|
try
|
||||||
|
Lines.Add(ExpandFileName(AFileName));
|
||||||
|
try
|
||||||
|
Lines.SaveToFile(LastFileStore, TEncoding.UTF8);
|
||||||
|
except
|
||||||
|
// Cartella di sola lettura: si perde solo la comodita' di riaprire.
|
||||||
|
end;
|
||||||
|
finally
|
||||||
|
Lines.Free;
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
|
||||||
|
function AttrStr(ANode: IXMLNode; const AName, ADefault: string): string;
|
||||||
|
begin
|
||||||
|
if (ANode <> nil) and ANode.HasAttribute(AName) then
|
||||||
|
Result := VarToStr(ANode.Attributes[AName])
|
||||||
|
else
|
||||||
|
Result := ADefault;
|
||||||
|
end;
|
||||||
|
|
||||||
|
/// Togli la sola lettura, se c'e'.
|
||||||
|
procedure ClearReadOnly(const AFileName: string);
|
||||||
|
begin
|
||||||
|
if FileExists(AFileName) then
|
||||||
|
SetFileAttributes(PChar(AFileName), FILE_ATTRIBUTE_NORMAL);
|
||||||
|
end;
|
||||||
|
|
||||||
|
/// Cancella davvero, anche un file in sola lettura.
|
||||||
|
procedure ForceDelete(const AFileName: string);
|
||||||
|
begin
|
||||||
|
if not FileExists(AFileName) then
|
||||||
|
Exit;
|
||||||
|
ClearReadOnly(AFileName);
|
||||||
|
DeleteFile(AFileName);
|
||||||
|
end;
|
||||||
|
|
||||||
|
function ReadPanelTitle(const AFileName: string; out ATitle: string): Boolean;
|
||||||
|
var
|
||||||
|
Doc: IXMLDocument;
|
||||||
|
Root: IXMLNode;
|
||||||
|
begin
|
||||||
|
ATitle := '';
|
||||||
|
try
|
||||||
|
Doc := TXMLDocument.Create(nil);
|
||||||
|
Doc.LoadFromFile(AFileName);
|
||||||
|
Doc.Active := True;
|
||||||
|
Root := Doc.DocumentElement;
|
||||||
|
if (Root = nil) or not SameText(Root.NodeName, 'plancia') then
|
||||||
|
Exit(False);
|
||||||
|
ATitle := AttrStr(Root.ChildNodes.FindNode('panel'), 'title', '');
|
||||||
|
Result := True;
|
||||||
|
except
|
||||||
|
Result := False;
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
|
||||||
|
function AttrInt(ANode: IXMLNode; const AName: string;
|
||||||
|
ADefault: Integer): Integer;
|
||||||
|
begin
|
||||||
|
Result := StrToIntDef(AttrStr(ANode, AName, ''), ADefault);
|
||||||
|
end;
|
||||||
|
|
||||||
|
/// Tollera le forme che viene naturale scrivere a mano in un XML.
|
||||||
|
function AttrBool(ANode: IXMLNode; const AName: string;
|
||||||
|
ADefault: Boolean): Boolean;
|
||||||
|
var
|
||||||
|
S: string;
|
||||||
|
begin
|
||||||
|
S := LowerCase(Trim(AttrStr(ANode, AName, '')));
|
||||||
|
if S = '' then
|
||||||
|
Exit(ADefault);
|
||||||
|
Result := (S = 'true') or (S = '1') or (S = 'yes') or (S = 'si');
|
||||||
|
end;
|
||||||
|
|
||||||
|
function AttrFloat(ANode: IXMLNode; const AName: string;
|
||||||
|
const ADefault: Double): Double;
|
||||||
|
var
|
||||||
|
S: string;
|
||||||
|
begin
|
||||||
|
S := AttrStr(ANode, AName, '');
|
||||||
|
if S = '' then
|
||||||
|
Exit(ADefault);
|
||||||
|
// Tollera anche i file scritti a mano con la virgola decimale.
|
||||||
|
S := StringReplace(S, ',', '.', [rfReplaceAll]);
|
||||||
|
Result := StrToFloatDef(S, ADefault, FS);
|
||||||
|
end;
|
||||||
|
|
||||||
|
function FloatAttr(const AValue: Double): string;
|
||||||
|
begin
|
||||||
|
Result := FloatToStr(AValue, FS);
|
||||||
|
end;
|
||||||
|
|
||||||
|
function HtmlToColor(const AValue: string; ADefault: TColor): TColor;
|
||||||
|
var
|
||||||
|
S: string;
|
||||||
|
V: Integer;
|
||||||
|
begin
|
||||||
|
S := Trim(AValue);
|
||||||
|
if S.StartsWith('#') then
|
||||||
|
Delete(S, 1, 1);
|
||||||
|
if (Length(S) <> 6) or not TryStrToInt('$' + S, V) then
|
||||||
|
Exit(ADefault);
|
||||||
|
// Da RRGGBB a $00BBGGRR.
|
||||||
|
Result := TColor(((V and $FF) shl 16) or (V and $FF00) or
|
||||||
|
((V shr 16) and $FF));
|
||||||
|
end;
|
||||||
|
|
||||||
|
function ColorToHtml(AColor: TColor): string;
|
||||||
|
var
|
||||||
|
V: Integer;
|
||||||
|
begin
|
||||||
|
V := ColorToRGB(AColor);
|
||||||
|
Result := Format('#%.2x%.2x%.2x',
|
||||||
|
[V and $FF, (V shr 8) and $FF, (V shr 16) and $FF]);
|
||||||
|
end;
|
||||||
|
|
||||||
|
{ TPlanciaSnapshot }
|
||||||
|
|
||||||
|
constructor TPlanciaSnapshot.Create;
|
||||||
|
begin
|
||||||
|
inherited Create;
|
||||||
|
FElements := TObjectList<TElementDef>.Create(True);
|
||||||
|
Selected := -1;
|
||||||
|
end;
|
||||||
|
|
||||||
|
destructor TPlanciaSnapshot.Destroy;
|
||||||
|
begin
|
||||||
|
FElements.Free;
|
||||||
|
inherited;
|
||||||
|
end;
|
||||||
|
|
||||||
|
function CloneDef(ASource: TElementDef): TElementDef;
|
||||||
|
begin
|
||||||
|
Result := TElementDef.Create(ASource.Kind);
|
||||||
|
Result.AssignFrom(ASource);
|
||||||
|
end;
|
||||||
|
|
||||||
|
{ TPlanciaConfig }
|
||||||
|
|
||||||
|
function TPlanciaConfig.SoundsDir: string;
|
||||||
|
begin
|
||||||
|
Result := IncludeTrailingPathDelimiter(BaseDir) + 'suoni';
|
||||||
|
end;
|
||||||
|
|
||||||
|
function TPlanciaConfig.TakeSnapshot: TPlanciaSnapshot;
|
||||||
|
var
|
||||||
|
D: TElementDef;
|
||||||
|
begin
|
||||||
|
Result := TPlanciaSnapshot.Create;
|
||||||
|
Result.Port := Port;
|
||||||
|
Result.Baud := Baud;
|
||||||
|
Result.TimeoutMs := TimeoutMs;
|
||||||
|
Result.PollMs := PollMs;
|
||||||
|
Result.Title := Title;
|
||||||
|
Result.PanelWidth := PanelWidth;
|
||||||
|
Result.PanelHeight := PanelHeight;
|
||||||
|
Result.Background := Background;
|
||||||
|
Result.Ink := Ink;
|
||||||
|
for D in FElements do
|
||||||
|
Result.FElements.Add(CloneDef(D));
|
||||||
|
end;
|
||||||
|
|
||||||
|
procedure TPlanciaConfig.RestoreSnapshot(ASnapshot: TPlanciaSnapshot);
|
||||||
|
var
|
||||||
|
D: TElementDef;
|
||||||
|
begin
|
||||||
|
Port := ASnapshot.Port;
|
||||||
|
Baud := ASnapshot.Baud;
|
||||||
|
TimeoutMs := ASnapshot.TimeoutMs;
|
||||||
|
PollMs := ASnapshot.PollMs;
|
||||||
|
Title := ASnapshot.Title;
|
||||||
|
PanelWidth := ASnapshot.PanelWidth;
|
||||||
|
PanelHeight := ASnapshot.PanelHeight;
|
||||||
|
Background := ASnapshot.Background;
|
||||||
|
Ink := ASnapshot.Ink;
|
||||||
|
FElements.Clear;
|
||||||
|
for D in ASnapshot.FElements do
|
||||||
|
FElements.Add(CloneDef(D));
|
||||||
|
end;
|
||||||
|
|
||||||
|
constructor TPlanciaConfig.Create;
|
||||||
|
begin
|
||||||
|
inherited Create;
|
||||||
|
FElements := TObjectList<TElementDef>.Create(True);
|
||||||
|
FImages := TImageLibrary.Create;
|
||||||
|
Clear;
|
||||||
|
end;
|
||||||
|
|
||||||
|
destructor TPlanciaConfig.Destroy;
|
||||||
|
begin
|
||||||
|
FImages.Free;
|
||||||
|
FElements.Free;
|
||||||
|
inherited;
|
||||||
|
end;
|
||||||
|
|
||||||
|
procedure TPlanciaConfig.Clear;
|
||||||
|
begin
|
||||||
|
FElements.Clear;
|
||||||
|
FImages.Clear;
|
||||||
|
Background := '';
|
||||||
|
BaseDir := ExtractFilePath(ParamStr(0));
|
||||||
|
Ink := clNone;
|
||||||
|
Port := 'COM1';
|
||||||
|
Baud := 9600;
|
||||||
|
TimeoutMs := 500;
|
||||||
|
PollMs := 500;
|
||||||
|
Title := 'Plancia';
|
||||||
|
PanelWidth := 1000;
|
||||||
|
PanelHeight := 640;
|
||||||
|
end;
|
||||||
|
|
||||||
|
procedure TPlanciaConfig.LoadFromFile(const AFileName: string);
|
||||||
|
var
|
||||||
|
Doc: IXMLDocument;
|
||||||
|
Root, Node, Els, El: IXMLNode;
|
||||||
|
I: Integer;
|
||||||
|
Kind: TElementKind;
|
||||||
|
Def: TElementDef;
|
||||||
|
begin
|
||||||
|
if not FileExists(AFileName) then
|
||||||
|
raise EPlanciaConfig.CreateFmt('File di configurazione non trovato: %s',
|
||||||
|
[AFileName]);
|
||||||
|
|
||||||
|
Clear;
|
||||||
|
|
||||||
|
Doc := TXMLDocument.Create(nil);
|
||||||
|
Doc.LoadFromFile(AFileName);
|
||||||
|
Doc.Active := True;
|
||||||
|
|
||||||
|
Root := Doc.DocumentElement;
|
||||||
|
if (Root = nil) or not SameText(Root.NodeName, 'plancia') then
|
||||||
|
raise EPlanciaConfig.CreateFmt(
|
||||||
|
'%s non e'' una configurazione di plancia (manca il nodo <plancia>).',
|
||||||
|
[ExtractFileName(AFileName)]);
|
||||||
|
|
||||||
|
Node := Root.ChildNodes.FindNode('connection');
|
||||||
|
if Node <> nil then
|
||||||
|
begin
|
||||||
|
Port := AttrStr(Node, 'port', Port);
|
||||||
|
Baud := AttrInt(Node, 'baud', Baud);
|
||||||
|
TimeoutMs := AttrInt(Node, 'timeoutMs', TimeoutMs);
|
||||||
|
PollMs := AttrInt(Node, 'pollMs', PollMs);
|
||||||
|
end;
|
||||||
|
|
||||||
|
Node := Root.ChildNodes.FindNode('panel');
|
||||||
|
if Node <> nil then
|
||||||
|
begin
|
||||||
|
Title := AttrStr(Node, 'title', Title);
|
||||||
|
PanelWidth := AttrInt(Node, 'width', PanelWidth);
|
||||||
|
PanelHeight := AttrInt(Node, 'height', PanelHeight);
|
||||||
|
Background := AttrStr(Node, 'background', '');
|
||||||
|
Ink := HtmlToColor(AttrStr(Node, 'ink', ''), clNone);
|
||||||
|
end;
|
||||||
|
|
||||||
|
// I percorsi delle immagini sono relativi alla cartella dell'XML.
|
||||||
|
BaseDir := ExtractFilePath(ExpandFileName(AFileName));
|
||||||
|
FImages.SetBaseDir(BaseDir);
|
||||||
|
Els := Root.ChildNodes.FindNode('images');
|
||||||
|
if Els <> nil then
|
||||||
|
for I := 0 to Els.ChildNodes.Count - 1 do
|
||||||
|
begin
|
||||||
|
El := Els.ChildNodes[I];
|
||||||
|
if SameText(El.NodeName, 'image') then
|
||||||
|
FImages.AddEntry(AttrStr(El, 'name', ''), AttrStr(El, 'file', ''));
|
||||||
|
end;
|
||||||
|
|
||||||
|
Els := Root.ChildNodes.FindNode('elements');
|
||||||
|
if Els = nil then
|
||||||
|
Exit;
|
||||||
|
|
||||||
|
for I := 0 to Els.ChildNodes.Count - 1 do
|
||||||
|
begin
|
||||||
|
El := Els.ChildNodes[I];
|
||||||
|
if not SameText(El.NodeName, 'element') then
|
||||||
|
Continue;
|
||||||
|
if not ElementKindFromId(AttrStr(El, 'kind', ''), Kind) then
|
||||||
|
raise EPlanciaConfig.CreateFmt(
|
||||||
|
'Tipo di elemento sconosciuto "%s" nel file %s.',
|
||||||
|
[AttrStr(El, 'kind', ''), ExtractFileName(AFileName)]);
|
||||||
|
|
||||||
|
Def := TElementDef.Create(Kind);
|
||||||
|
FElements.Add(Def);
|
||||||
|
Def.Caption := AttrStr(El, 'caption', Def.Caption);
|
||||||
|
Def.Slave := AttrInt(El, 'slave', Def.Slave);
|
||||||
|
Def.Channel := AttrInt(El, 'channel', Def.Channel);
|
||||||
|
Def.Left := AttrInt(El, 'left', Def.Left);
|
||||||
|
Def.Top := AttrInt(El, 'top', Def.Top);
|
||||||
|
Def.Width := AttrInt(El, 'width', Def.Width);
|
||||||
|
Def.Height := AttrInt(El, 'height', Def.Height);
|
||||||
|
Def.FontSize := AttrInt(El, 'fontSize', Def.FontSize);
|
||||||
|
if Kind = ekDisplay then
|
||||||
|
begin
|
||||||
|
Def.Digits := AttrInt(El, 'digits', Def.Digits);
|
||||||
|
Def.Decimals := AttrInt(El, 'decimals', Def.Decimals);
|
||||||
|
end;
|
||||||
|
if Kind = ekRotary then
|
||||||
|
begin
|
||||||
|
Def.Positions := AttrInt(El, 'positions', Def.Positions);
|
||||||
|
Def.Legend := AttrStr(El, 'legend', Def.Legend);
|
||||||
|
// Assente = la bobina dopo `channel`, che e' il caso normale.
|
||||||
|
Def.Channel2 := AttrInt(El, 'channel2', Def.Channel2);
|
||||||
|
Def.Momentary := AttrBool(El, 'momentary', Def.Momentary);
|
||||||
|
end;
|
||||||
|
if Kind = ekLamp then
|
||||||
|
begin
|
||||||
|
Def.Alarm := AttrBool(El, 'alarm', Def.Alarm);
|
||||||
|
Def.Sound := AttrStr(El, 'sound', Def.Sound);
|
||||||
|
end;
|
||||||
|
if Kind in [ekLamp, ekButton, ekSwitch, ekLabel] then
|
||||||
|
begin
|
||||||
|
Def.OnColor := HtmlToColor(AttrStr(El, 'color', ''), Def.OnColor);
|
||||||
|
Def.OffColor := HtmlToColor(AttrStr(El, 'colorOff', ''), Def.OffColor);
|
||||||
|
end;
|
||||||
|
Def.CaptionPos := CaptionPosFromId(AttrStr(El, 'captionPos', ''),
|
||||||
|
Def.CaptionPos);
|
||||||
|
Def.Shape := ShapeFromId(AttrStr(El, 'shape', ''), Def.Shape);
|
||||||
|
Def.FontName := AttrStr(El, 'fontName', Def.FontName);
|
||||||
|
Def.Spacing := AttrInt(El, 'spacing', Def.Spacing);
|
||||||
|
if Kind = ekLabel then
|
||||||
|
begin
|
||||||
|
Def.Frame := FrameFromId(AttrStr(El, 'frame', ''), Def.Frame);
|
||||||
|
Def.FrameWidth := AttrInt(El, 'frameWidth', Def.FrameWidth);
|
||||||
|
end;
|
||||||
|
if Kind = ekImage then
|
||||||
|
Def.ImageOff := AttrStr(El, 'image', '')
|
||||||
|
else if ElementUsesImages(Kind) then
|
||||||
|
begin
|
||||||
|
Def.ImageOff := AttrStr(El, 'imageOff', '');
|
||||||
|
Def.ImageOn := AttrStr(El, 'imageOn', '');
|
||||||
|
end;
|
||||||
|
if ElementIsAnalog(Kind) then
|
||||||
|
begin
|
||||||
|
Def.RawMin := AttrInt(El, 'rawMin', Def.RawMin);
|
||||||
|
Def.RawMax := AttrInt(El, 'rawMax', Def.RawMax);
|
||||||
|
Def.EngMin := AttrFloat(El, 'engMin', Def.EngMin);
|
||||||
|
Def.EngMax := AttrFloat(El, 'engMax', Def.EngMax);
|
||||||
|
Def.Units := AttrStr(El, 'units', Def.Units);
|
||||||
|
Def.WarnBelow := AttrFloat(El, 'warnBelow', Def.WarnBelow);
|
||||||
|
Def.WarnAbove := AttrFloat(El, 'warnAbove', Def.WarnAbove);
|
||||||
|
end;
|
||||||
|
if Kind = ekGauge then
|
||||||
|
begin
|
||||||
|
Def.GaugeStyle := GaugeStyleFromId(AttrStr(El, 'style', ''),
|
||||||
|
Def.GaugeStyle);
|
||||||
|
// Il quadrante ha una lettura in cifre, e i decimali contano come sul
|
||||||
|
// display.
|
||||||
|
Def.Decimals := AttrInt(El, 'decimals', Def.Decimals);
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
|
||||||
|
procedure TPlanciaConfig.SaveToFile(const AFileName: string);
|
||||||
|
var
|
||||||
|
Doc: IXMLDocument;
|
||||||
|
Root, Node, Els, El: IXMLNode;
|
||||||
|
Def: TElementDef;
|
||||||
|
I: Integer;
|
||||||
|
Temp, Backup: string;
|
||||||
|
begin
|
||||||
|
// Salvando in un'altra cartella i percorsi relativi vanno ricalcolati,
|
||||||
|
// altrimenti punterebbero al vuoto.
|
||||||
|
BaseDir := ExtractFilePath(ExpandFileName(AFileName));
|
||||||
|
FImages.Rebase(BaseDir);
|
||||||
|
|
||||||
|
Doc := TXMLDocument.Create(nil);
|
||||||
|
Doc.Active := True;
|
||||||
|
Doc.Version := '1.0';
|
||||||
|
Doc.Encoding := 'UTF-8';
|
||||||
|
Doc.Options := Doc.Options + [doNodeAutoIndent];
|
||||||
|
|
||||||
|
Root := Doc.AddChild('plancia');
|
||||||
|
Root.Attributes['version'] := '1';
|
||||||
|
|
||||||
|
Node := Root.AddChild('connection');
|
||||||
|
Node.Attributes['port'] := Port;
|
||||||
|
Node.Attributes['baud'] := Baud;
|
||||||
|
Node.Attributes['timeoutMs'] := TimeoutMs;
|
||||||
|
Node.Attributes['pollMs'] := PollMs;
|
||||||
|
|
||||||
|
Node := Root.AddChild('panel');
|
||||||
|
Node.Attributes['title'] := Title;
|
||||||
|
Node.Attributes['width'] := PanelWidth;
|
||||||
|
Node.Attributes['height'] := PanelHeight;
|
||||||
|
if Background <> '' then
|
||||||
|
Node.Attributes['background'] := Background;
|
||||||
|
if Ink <> clNone then
|
||||||
|
Node.Attributes['ink'] := ColorToHtml(Ink);
|
||||||
|
|
||||||
|
if FImages.Count > 0 then
|
||||||
|
begin
|
||||||
|
Els := Root.AddChild('images');
|
||||||
|
for I := 0 to FImages.Count - 1 do
|
||||||
|
begin
|
||||||
|
El := Els.AddChild('image');
|
||||||
|
El.Attributes['name'] := FImages.Item(I).Name;
|
||||||
|
El.Attributes['file'] := FImages.Item(I).FileName;
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
|
||||||
|
Els := Root.AddChild('elements');
|
||||||
|
for Def in FElements do
|
||||||
|
begin
|
||||||
|
El := Els.AddChild('element');
|
||||||
|
El.Attributes['kind'] := ELEMENT_IDS[Def.Kind];
|
||||||
|
El.Attributes['caption'] := Def.Caption;
|
||||||
|
if ElementHasChannel(Def.Kind) then
|
||||||
|
begin
|
||||||
|
El.Attributes['slave'] := Def.Slave;
|
||||||
|
El.Attributes['channel'] := Def.Channel;
|
||||||
|
end;
|
||||||
|
El.Attributes['left'] := Def.Left;
|
||||||
|
El.Attributes['top'] := Def.Top;
|
||||||
|
El.Attributes['width'] := Def.Width;
|
||||||
|
El.Attributes['height'] := Def.Height;
|
||||||
|
if Def.FontSize > 0 then
|
||||||
|
El.Attributes['fontSize'] := Def.FontSize;
|
||||||
|
if Def.Kind = ekDisplay then
|
||||||
|
begin
|
||||||
|
El.Attributes['digits'] := Def.Digits;
|
||||||
|
El.Attributes['decimals'] := Def.Decimals;
|
||||||
|
end;
|
||||||
|
if Def.Kind = ekRotary then
|
||||||
|
begin
|
||||||
|
El.Attributes['positions'] := Def.Positions;
|
||||||
|
El.Attributes['legend'] := Def.Legend;
|
||||||
|
if (Def.Positions >= 3) and (Def.Channel2 >= 0) then
|
||||||
|
El.Attributes['channel2'] := Def.Channel2;
|
||||||
|
if Def.Momentary then
|
||||||
|
El.Attributes['momentary'] := 'true';
|
||||||
|
end;
|
||||||
|
if Def.Kind = ekLamp then
|
||||||
|
begin
|
||||||
|
// Il nome del suono si salva anche con l'allarme spento: si spunta e si
|
||||||
|
// rispunta la casella senza dover riscegliere il file.
|
||||||
|
if Def.Alarm then
|
||||||
|
El.Attributes['alarm'] := 'true';
|
||||||
|
if Def.Sound <> '' then
|
||||||
|
El.Attributes['sound'] := Def.Sound;
|
||||||
|
end;
|
||||||
|
if Def.Kind in [ekLamp, ekButton, ekSwitch, ekLabel] then
|
||||||
|
begin
|
||||||
|
if Def.OnColor <> TElementDef.DefaultOnColor(Def.Kind) then
|
||||||
|
El.Attributes['color'] := ColorToHtml(Def.OnColor);
|
||||||
|
if Def.OffColor <> clNone then
|
||||||
|
El.Attributes['colorOff'] := ColorToHtml(Def.OffColor);
|
||||||
|
end;
|
||||||
|
// Confronto con il predefinito del tipo, non con "center": una spia con
|
||||||
|
// l'etichetta al centro, salvata senza attributo, si riaprirebbe sotto.
|
||||||
|
if Def.CaptionPos <> TElementDef.DefaultCaptionPos(Def.Kind) then
|
||||||
|
El.Attributes['captionPos'] := CAPTION_POS_IDS[Def.CaptionPos];
|
||||||
|
if Def.Shape <> ksAuto then
|
||||||
|
El.Attributes['shape'] := SHAPE_IDS[Def.Shape];
|
||||||
|
if Def.FontName <> '' then
|
||||||
|
El.Attributes['fontName'] := Def.FontName;
|
||||||
|
if Def.Spacing <> 0 then
|
||||||
|
El.Attributes['spacing'] := Def.Spacing;
|
||||||
|
if Def.Kind = ekLabel then
|
||||||
|
begin
|
||||||
|
if Def.Frame <> fkNone then
|
||||||
|
begin
|
||||||
|
El.Attributes['frame'] := FRAME_IDS[Def.Frame];
|
||||||
|
El.Attributes['frameWidth'] := Def.FrameWidth;
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
if Def.Kind = ekImage then
|
||||||
|
begin
|
||||||
|
if Def.ImageOff <> '' then
|
||||||
|
El.Attributes['image'] := Def.ImageOff;
|
||||||
|
end
|
||||||
|
else if ElementUsesImages(Def.Kind) then
|
||||||
|
begin
|
||||||
|
if Def.ImageOff <> '' then
|
||||||
|
El.Attributes['imageOff'] := Def.ImageOff;
|
||||||
|
if Def.ImageOn <> '' then
|
||||||
|
El.Attributes['imageOn'] := Def.ImageOn;
|
||||||
|
end;
|
||||||
|
if ElementIsAnalog(Def.Kind) then
|
||||||
|
begin
|
||||||
|
El.Attributes['rawMin'] := Def.RawMin;
|
||||||
|
El.Attributes['rawMax'] := Def.RawMax;
|
||||||
|
El.Attributes['engMin'] := FloatAttr(Def.EngMin);
|
||||||
|
El.Attributes['engMax'] := FloatAttr(Def.EngMax);
|
||||||
|
El.Attributes['units'] := Def.Units;
|
||||||
|
El.Attributes['warnBelow'] := FloatAttr(Def.WarnBelow);
|
||||||
|
El.Attributes['warnAbove'] := FloatAttr(Def.WarnAbove);
|
||||||
|
end;
|
||||||
|
if (Def.Kind = ekGauge) and (Def.GaugeStyle <> gsBar) then
|
||||||
|
begin
|
||||||
|
El.Attributes['style'] := GAUGE_STYLE_IDS[Def.GaugeStyle];
|
||||||
|
El.Attributes['decimals'] := Def.Decimals;
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
|
||||||
|
// Non si scrive mai direttamente sopra il file buono: si scrive accanto e
|
||||||
|
// poi si scambia. Un programma chiuso male, un disco pieno o due copie del
|
||||||
|
// programma che salvano insieme lascerebbero altrimenti un XML troncato,
|
||||||
|
// che alla riapertura non si legge piu' e porta via la plancia. La copia
|
||||||
|
// precedente resta come .bak, che e' la via di scampo se succede comunque.
|
||||||
|
Temp := AFileName + '.tmp';
|
||||||
|
Backup := '';
|
||||||
|
ForceDelete(Temp);
|
||||||
|
Doc.SaveToFile(Temp);
|
||||||
|
if FileExists(AFileName) then
|
||||||
|
begin
|
||||||
|
Backup := AFileName + '.bak';
|
||||||
|
ForceDelete(Backup);
|
||||||
|
// La sola lettura su un file di configurazione e' quasi sempre un residuo
|
||||||
|
// di una copia (da uno zip, da una chiavetta): chi salva vuole salvare, e
|
||||||
|
// la versione di prima resta comunque nel .bak.
|
||||||
|
ClearReadOnly(AFileName);
|
||||||
|
// Se il rinomina non riesce (file aperto da un altro programma) si
|
||||||
|
// procede comunque: meglio salvare senza copia di sicurezza che non
|
||||||
|
// salvare.
|
||||||
|
if not RenameFile(AFileName, Backup) then
|
||||||
|
begin
|
||||||
|
Backup := '';
|
||||||
|
ForceDelete(AFileName);
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
if not RenameFile(Temp, AFileName) then
|
||||||
|
begin
|
||||||
|
// Non si e' riusciti a mettere il nuovo file al suo posto: si rimette
|
||||||
|
// dov'era quello di prima, invece di lasciare la plancia senza file.
|
||||||
|
if (Backup <> '') and FileExists(Backup) then
|
||||||
|
RenameFile(Backup, AFileName);
|
||||||
|
DeleteFile(Temp);
|
||||||
|
raise EPlanciaConfig.CreateFmt(
|
||||||
|
'Non riesco a scrivere %s: la versione precedente e'' stata rimessa ' +
|
||||||
|
'al suo posto.', [ExtractFileName(AFileName)]);
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
|
||||||
|
initialization
|
||||||
|
FS := TFormatSettings.Invariant;
|
||||||
|
DefaultDOMVendor := sOmniXmlVendor;
|
||||||
|
|
||||||
|
end.
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<plancia version="1">
|
||||||
|
<connection port="COM7" baud="9600" timeoutMs="500" pollMs="500"/>
|
||||||
|
<panel title="ZEBRA - Pulsantiera" width="950" height="940" background="zebra-sfondo"/>
|
||||||
|
<images>
|
||||||
|
<image name="zebra-sfondo" file="immagini\zebra-sfondo.png"/>
|
||||||
|
</images>
|
||||||
|
<elements>
|
||||||
|
<element kind="switch" caption="PORT WIPER" slave="1" channel="0" left="56" top="54" width="112" height="116" fontSize="12" captionPos="below" shape="round"/>
|
||||||
|
<element kind="switch" caption="STBD WIPER" slave="1" channel="1" left="180" top="50" width="112" height="116" fontSize="12" captionPos="below" shape="round"/>
|
||||||
|
<element kind="label" caption="ZEBRA" left="296" top="66" width="352" height="90" fontSize="62" fontName="Times New Roman" spacing="10"/>
|
||||||
|
<element kind="button" caption="WATER WIPERS" slave="1" channel="2" left="650" top="60" width="112" height="116" fontSize="12" captionPos="below" shape="round"/>
|
||||||
|
<element kind="button" caption="WINCH DOWN" slave="1" channel="3" left="776" top="54" width="112" height="116" fontSize="12" captionPos="below" shape="round"/>
|
||||||
|
<element kind="switch" caption="WHITE LT" slave="1" channel="4" left="56" top="199" width="112" height="116" fontSize="12" captionPos="below" shape="round"/>
|
||||||
|
<element kind="switch" caption="NAVIGATION LTS" slave="1" channel="5" left="176" top="199" width="112" height="116" fontSize="12" captionPos="below" shape="round"/>
|
||||||
|
<element kind="switch" caption="STERN LT" slave="1" channel="6" left="296" top="199" width="112" height="116" fontSize="12" captionPos="below" shape="round"/>
|
||||||
|
<element kind="lamp" caption="FIRE ALARM" slave="0" channel="0" left="416" top="199" width="112" height="116" fontSize="12" color="#FF4030" colorOff="#5A1410" captionPos="below" shape="round"/>
|
||||||
|
<element kind="switch" caption="ANCHOR LT" slave="1" channel="8" left="536" top="199" width="112" height="116" fontSize="12" captionPos="below" shape="round"/>
|
||||||
|
<element kind="switch" caption="COMPASS LT" slave="1" channel="9" left="660" top="200" width="112" height="116" fontSize="12" captionPos="below" shape="round"/>
|
||||||
|
<element kind="button" caption="WINCH UP" slave="1" channel="10" left="776" top="199" width="112" height="116" fontSize="12" captionPos="below" shape="round"/>
|
||||||
|
<element kind="switch" caption="SUB LIGHT" slave="1" channel="11" left="56" top="344" width="112" height="116" fontSize="12" captionPos="below" shape="round"/>
|
||||||
|
<element kind="switch" caption="INSTRUMENTS" slave="1" channel="12" left="176" top="344" width="112" height="116" fontSize="12" color="#4090FF" colorOff="#C8D0D8" captionPos="below" shape="round"/>
|
||||||
|
<element kind="switch" caption="AUTOPILOT" slave="1" channel="13" left="296" top="344" width="112" height="116" fontSize="12" captionPos="below" shape="round"/>
|
||||||
|
<element kind="lamp" caption="OIL STEERING" slave="0" channel="0" left="416" top="344" width="112" height="116" fontSize="12" color="#FF4030" colorOff="#5A1410" captionPos="below" shape="round"/>
|
||||||
|
<element kind="switch" caption="STEERING" slave="1" channel="15" left="536" top="344" width="112" height="116" fontSize="12" captionPos="below" shape="round"/>
|
||||||
|
<element kind="switch" caption="WASH PUMP" slave="1" channel="16" left="656" top="344" width="112" height="116" fontSize="12" captionPos="below" shape="round"/>
|
||||||
|
<element kind="switch" caption="WINCH ON" slave="1" channel="17" left="776" top="344" width="112" height="116" fontSize="12" captionPos="below" shape="round"/>
|
||||||
|
<element kind="rotary" caption="24V.DC FAN" slave="1" channel="18" left="56" top="465" width="112" height="140" fontSize="12" positions="2" legend="OFF ◀ ▶ ON"/>
|
||||||
|
<element kind="switch" caption="TVCC" slave="1" channel="19" left="176" top="489" width="112" height="116" fontSize="12" captionPos="below" shape="round"/>
|
||||||
|
<element kind="switch" caption="GSM" slave="1" channel="20" left="300" top="490" width="112" height="116" fontSize="12" captionPos="below" shape="round"/>
|
||||||
|
<element kind="button" caption="HORN" slave="1" channel="21" left="420" top="490" width="112" height="116" fontSize="12" captionPos="below" shape="round"/>
|
||||||
|
<element kind="switch" caption="INSTRUM.LTS" slave="1" channel="22" left="536" top="489" width="112" height="116" fontSize="12" captionPos="below" shape="round"/>
|
||||||
|
<element kind="button" caption="TACIT" slave="1" channel="23" left="656" top="489" width="112" height="116" fontSize="12" captionPos="below" shape="round"/>
|
||||||
|
<element kind="rotary" caption="230V.AC FAN" slave="1" channel="24" left="776" top="465" width="112" height="140" fontSize="12" positions="2" legend="FAN ◀ ▶ BLOWER"/>
|
||||||
|
<element kind="button" caption="SCROLL" slave="1" channel="25" left="56" top="634" width="112" height="116" fontSize="12" captionPos="below" shape="round"/>
|
||||||
|
<element kind="button" caption="RESET" slave="1" channel="26" left="176" top="634" width="112" height="116" fontSize="12" captionPos="below" shape="round"/>
|
||||||
|
<element kind="label" caption="PORT
|
||||||
|
ENGINE" left="300" top="628" width="104" height="104" fontSize="15" fontName="Arial" frame="oval" frameWidth="3"/>
|
||||||
|
<element kind="switch" caption="PARALLEL" slave="1" channel="27" left="416" top="634" width="112" height="116" fontSize="12" captionPos="below" shape="round"/>
|
||||||
|
<element kind="label" caption="STBD
|
||||||
|
ENGINE" left="540" top="630" width="104" height="104" fontSize="15" fontName="Arial" frame="oval" frameWidth="3"/>
|
||||||
|
<element kind="button" caption="RESET" slave="1" channel="28" left="660" top="630" width="112" height="116" fontSize="12" captionPos="below" shape="round"/>
|
||||||
|
<element kind="button" caption="SCROLL" slave="1" channel="29" left="776" top="634" width="112" height="116" fontSize="12" captionPos="below" shape="round"/>
|
||||||
|
<element kind="button" caption="STOP" slave="1" channel="30" left="56" top="779" width="112" height="116" fontSize="12" color="#FF5038" colorOff="#C82818" captionPos="below" shape="round"/>
|
||||||
|
<element kind="button" caption="START" slave="1" channel="31" left="176" top="779" width="112" height="116" fontSize="12" color="#40D060" colorOff="#187038" captionPos="below" shape="round"/>
|
||||||
|
<element kind="rotary" caption="CONTACT" slave="0" channel="0" left="296" top="779" width="112" height="116" fontSize="12" positions="2" legend=""/>
|
||||||
|
<element kind="rotary" caption="" slave="0" channel="0" left="416" top="775" width="112" height="120" fontSize="12" positions="3" legend="PORT ◀ 0 ▶ STBD"/>
|
||||||
|
<element kind="rotary" caption="CONTACT" slave="0" channel="0" left="536" top="779" width="112" height="116" fontSize="12" positions="2" legend=""/>
|
||||||
|
<element kind="button" caption="START" slave="0" channel="0" left="656" top="779" width="112" height="116" fontSize="12" color="#40D060" colorOff="#187038" captionPos="below" shape="round"/>
|
||||||
|
<element kind="button" caption="STOP" slave="0" channel="0" left="776" top="779" width="112" height="116" fontSize="12" color="#FF5038" colorOff="#C82818" captionPos="below" shape="round"/>
|
||||||
|
</elements>
|
||||||
|
</plancia>
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||||
|
<PropertyGroup>
|
||||||
|
<ProjectGuid>{78922105-9D91-4F64-B3AD-D910779B6FAD}</ProjectGuid>
|
||||||
|
</PropertyGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<Projects Include="Console\PlanciaConsole.dproj">
|
||||||
|
<Dependencies/>
|
||||||
|
</Projects>
|
||||||
|
<Projects Include="ProjectPlancia.dproj">
|
||||||
|
<Dependencies/>
|
||||||
|
</Projects>
|
||||||
|
</ItemGroup>
|
||||||
|
<ProjectExtensions>
|
||||||
|
<Borland.Personality>Default.Personality.12</Borland.Personality>
|
||||||
|
<Borland.ProjectType/>
|
||||||
|
<BorlandProject>
|
||||||
|
<Default.Personality/>
|
||||||
|
</BorlandProject>
|
||||||
|
</ProjectExtensions>
|
||||||
|
<Target Name="PlanciaConsole">
|
||||||
|
<MSBuild Projects="Console\PlanciaConsole.dproj"/>
|
||||||
|
</Target>
|
||||||
|
<Target Name="PlanciaConsole:Clean">
|
||||||
|
<MSBuild Projects="Console\PlanciaConsole.dproj" Targets="Clean"/>
|
||||||
|
</Target>
|
||||||
|
<Target Name="PlanciaConsole:Make">
|
||||||
|
<MSBuild Projects="Console\PlanciaConsole.dproj" Targets="Make"/>
|
||||||
|
</Target>
|
||||||
|
<Target Name="ProjectPlancia">
|
||||||
|
<MSBuild Projects="ProjectPlancia.dproj"/>
|
||||||
|
</Target>
|
||||||
|
<Target Name="ProjectPlancia:Clean">
|
||||||
|
<MSBuild Projects="ProjectPlancia.dproj" Targets="Clean"/>
|
||||||
|
</Target>
|
||||||
|
<Target Name="ProjectPlancia:Make">
|
||||||
|
<MSBuild Projects="ProjectPlancia.dproj" Targets="Make"/>
|
||||||
|
</Target>
|
||||||
|
<Target Name="Build">
|
||||||
|
<CallTarget Targets="PlanciaConsole;ProjectPlancia"/>
|
||||||
|
</Target>
|
||||||
|
<Target Name="Clean">
|
||||||
|
<CallTarget Targets="PlanciaConsole:Clean;ProjectPlancia:Clean"/>
|
||||||
|
</Target>
|
||||||
|
<Target Name="Make">
|
||||||
|
<CallTarget Targets="PlanciaConsole:Make;ProjectPlancia:Make"/>
|
||||||
|
</Target>
|
||||||
|
<Import Project="$(BDS)\Bin\CodeGear.Group.Targets" Condition="Exists('$(BDS)\Bin\CodeGear.Group.Targets')"/>
|
||||||
|
</Project>
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
program ProjectPlancia;
|
||||||
|
|
||||||
|
uses
|
||||||
|
Vcl.Forms,
|
||||||
|
uMain in 'uMain.pas' {MainForm},
|
||||||
|
uModbusRTU in 'uModbusRTU.pas',
|
||||||
|
uGauge in 'uGauge.pas';
|
||||||
|
|
||||||
|
{$R *.res}
|
||||||
|
|
||||||
|
begin
|
||||||
|
Application.Initialize;
|
||||||
|
Application.MainFormOnTaskbar := True;
|
||||||
|
Application.CreateForm(TMainForm, MainForm);
|
||||||
|
Application.Run;
|
||||||
|
end.
|
||||||
@@ -0,0 +1,121 @@
|
|||||||
|
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||||
|
<PropertyGroup>
|
||||||
|
<ProjectGuid>{A1B2C3D4-E5F6-4A5B-9C8D-1234567890AB}</ProjectGuid>
|
||||||
|
<ProjectVersion>20.3</ProjectVersion>
|
||||||
|
<FrameworkType>VCL</FrameworkType>
|
||||||
|
<MainSource>ProjectPlancia.dpr</MainSource>
|
||||||
|
<Base>True</Base>
|
||||||
|
<Config Condition="'$(Config)'==''">Debug</Config>
|
||||||
|
<Platform Condition="'$(Platform)'==''">Win32</Platform>
|
||||||
|
<TargetedPlatforms>1</TargetedPlatforms>
|
||||||
|
<AppType>Application</AppType>
|
||||||
|
<ProjectName Condition="'$(ProjectName)'==''">ProjectPlancia</ProjectName>
|
||||||
|
</PropertyGroup>
|
||||||
|
<PropertyGroup Condition="'$(Config)'=='Base' or '$(Base)'!=''">
|
||||||
|
<Base>true</Base>
|
||||||
|
</PropertyGroup>
|
||||||
|
<PropertyGroup Condition="('$(Platform)'=='Win64' and '$(Base)'=='true') or '$(Base_Win64)'!=''">
|
||||||
|
<Base_Win64>true</Base_Win64>
|
||||||
|
<CfgParent>Base</CfgParent>
|
||||||
|
<Base>true</Base>
|
||||||
|
</PropertyGroup>
|
||||||
|
<PropertyGroup Condition="'$(Config)'=='Debug' or '$(Cfg_1)'!=''">
|
||||||
|
<Cfg_1>true</Cfg_1>
|
||||||
|
<CfgParent>Base</CfgParent>
|
||||||
|
<Base>true</Base>
|
||||||
|
</PropertyGroup>
|
||||||
|
<PropertyGroup Condition="('$(Platform)'=='Win32' and '$(Cfg_1)'=='true') or '$(Cfg_1_Win32)'!=''">
|
||||||
|
<Cfg_1_Win32>true</Cfg_1_Win32>
|
||||||
|
<CfgParent>Cfg_1</CfgParent>
|
||||||
|
<Cfg_1>true</Cfg_1>
|
||||||
|
<Base>true</Base>
|
||||||
|
</PropertyGroup>
|
||||||
|
<PropertyGroup Condition="('$(Platform)'=='Win64' and '$(Cfg_1)'=='true') or '$(Cfg_1_Win64)'!=''">
|
||||||
|
<Cfg_1_Win64>true</Cfg_1_Win64>
|
||||||
|
<CfgParent>Cfg_1</CfgParent>
|
||||||
|
<Cfg_1>true</Cfg_1>
|
||||||
|
<Base>true</Base>
|
||||||
|
</PropertyGroup>
|
||||||
|
<PropertyGroup Condition="'$(Config)'=='Release' or '$(Cfg_2)'!=''">
|
||||||
|
<Cfg_2>true</Cfg_2>
|
||||||
|
<CfgParent>Base</CfgParent>
|
||||||
|
<Base>true</Base>
|
||||||
|
</PropertyGroup>
|
||||||
|
<PropertyGroup Condition="('$(Platform)'=='Win64' and '$(Cfg_2)'=='true') or '$(Cfg_2_Win64)'!=''">
|
||||||
|
<Cfg_2_Win64>true</Cfg_2_Win64>
|
||||||
|
<CfgParent>Cfg_2</CfgParent>
|
||||||
|
<Cfg_2>true</Cfg_2>
|
||||||
|
<Base>true</Base>
|
||||||
|
</PropertyGroup>
|
||||||
|
<PropertyGroup Condition="'$(Base)'!=''">
|
||||||
|
<SanitizedProjectName>ProjectPlancia</SanitizedProjectName>
|
||||||
|
<Icon_MainIcon>$(BDS)\bin\delphi_PROJECTICON.ico</Icon_MainIcon>
|
||||||
|
<UWP_DelphiLogo44>$(BDS)\bin\Artwork\Windows\UWP\delphi_UwpDefault_44.png</UWP_DelphiLogo44>
|
||||||
|
<UWP_DelphiLogo150>$(BDS)\bin\Artwork\Windows\UWP\delphi_UwpDefault_150.png</UWP_DelphiLogo150>
|
||||||
|
</PropertyGroup>
|
||||||
|
<PropertyGroup Condition="'$(Base_Win64)'!=''">
|
||||||
|
<DCC_Namespace>Winapi;System.Win;Data.Win;Datasnap.Win;Web.Win;Soap.Win;Xml.Win;$(DCC_Namespace)</DCC_Namespace>
|
||||||
|
<BT_BuildType>Debug</BT_BuildType>
|
||||||
|
<VerInfo_IncludeVerInfo>true</VerInfo_IncludeVerInfo>
|
||||||
|
<VerInfo_Keys>CompanyName=;FileDescription=$(MSBuildProjectName);FileVersion=1.0.0.0;InternalName=;LegalCopyright=;LegalTrademarks=;OriginalFilename=;ProgramID=com.embarcadero.$(MSBuildProjectName);ProductName=$(MSBuildProjectName);ProductVersion=1.0.0.0;Comments=</VerInfo_Keys>
|
||||||
|
<VerInfo_Locale>1033</VerInfo_Locale>
|
||||||
|
<Manifest_File>$(BDS)\bin\default_app.manifest</Manifest_File>
|
||||||
|
</PropertyGroup>
|
||||||
|
<PropertyGroup Condition="'$(Cfg_1)'!=''">
|
||||||
|
<DCC_Define>DEBUG;$(DCC_Define)</DCC_Define>
|
||||||
|
<DCC_DebugInformation>1</DCC_DebugInformation>
|
||||||
|
<DCC_Optimize>false</DCC_Optimize>
|
||||||
|
</PropertyGroup>
|
||||||
|
<PropertyGroup Condition="'$(Cfg_1_Win32)'!=''">
|
||||||
|
<BT_BuildType>Debug</BT_BuildType>
|
||||||
|
</PropertyGroup>
|
||||||
|
<PropertyGroup Condition="'$(Cfg_1_Win64)'!=''">
|
||||||
|
<AppDPIAwarenessMode>PerMonitorV2</AppDPIAwarenessMode>
|
||||||
|
</PropertyGroup>
|
||||||
|
<PropertyGroup Condition="'$(Cfg_2)'!=''">
|
||||||
|
<DCC_DebugInformation>0</DCC_DebugInformation>
|
||||||
|
</PropertyGroup>
|
||||||
|
<PropertyGroup Condition="'$(Cfg_2_Win64)'!=''">
|
||||||
|
<AppDPIAwarenessMode>PerMonitorV2</AppDPIAwarenessMode>
|
||||||
|
</PropertyGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<DelphiCompile Include="$(MainSource)">
|
||||||
|
<MainSource>MainSource</MainSource>
|
||||||
|
</DelphiCompile>
|
||||||
|
<DCCReference Include="uMain.pas">
|
||||||
|
<Form>MainForm</Form>
|
||||||
|
<FormType>dfm</FormType>
|
||||||
|
</DCCReference>
|
||||||
|
<DCCReference Include="uModbusRTU.pas"/>
|
||||||
|
<DCCReference Include="uGauge.pas"/>
|
||||||
|
<BuildConfiguration Include="Base">
|
||||||
|
<Key>Base</Key>
|
||||||
|
</BuildConfiguration>
|
||||||
|
<BuildConfiguration Include="Debug">
|
||||||
|
<Key>Cfg_1</Key>
|
||||||
|
<CfgParent>Base</CfgParent>
|
||||||
|
</BuildConfiguration>
|
||||||
|
<BuildConfiguration Include="Release">
|
||||||
|
<Key>Cfg_2</Key>
|
||||||
|
<CfgParent>Base</CfgParent>
|
||||||
|
</BuildConfiguration>
|
||||||
|
</ItemGroup>
|
||||||
|
<Import Project="$(BDS)\Bin\CodeGear.Delphi.Targets" Condition="Exists('$(BDS)\Bin\CodeGear.Delphi.Targets')"/>
|
||||||
|
<Import Project="$(APPDATA)\Embarcadero\$(BDSAPPDATABASEDIR)\$(PRODUCTVERSION)\UserTools.proj" Condition="Exists('$(APPDATA)\Embarcadero\$(BDSAPPDATABASEDIR)\$(PRODUCTVERSION)\UserTools.proj')"/>
|
||||||
|
<ProjectExtensions>
|
||||||
|
<Borland.Personality>Delphi.Personality.12</Borland.Personality>
|
||||||
|
<Borland.ProjectType>VCLApplication</Borland.ProjectType>
|
||||||
|
<BorlandProject>
|
||||||
|
<Delphi.Personality>
|
||||||
|
<Source>
|
||||||
|
<Source Name="MainSource">ProjectPlancia.dpr</Source>
|
||||||
|
</Source>
|
||||||
|
</Delphi.Personality>
|
||||||
|
<Platforms>
|
||||||
|
<Platform value="Win32">True</Platform>
|
||||||
|
<Platform value="Win64">False</Platform>
|
||||||
|
</Platforms>
|
||||||
|
</BorlandProject>
|
||||||
|
<ProjectFileVersion>12</ProjectFileVersion>
|
||||||
|
</ProjectExtensions>
|
||||||
|
</Project>
|
||||||
|
After Width: | Height: | Size: 107 KiB |
@@ -0,0 +1,304 @@
|
|||||||
|
unit uGauge;
|
||||||
|
|
||||||
|
interface
|
||||||
|
|
||||||
|
uses
|
||||||
|
Winapi.Windows, System.SysUtils, System.Classes, System.Types,
|
||||||
|
Vcl.Controls, Vcl.Graphics;
|
||||||
|
|
||||||
|
const
|
||||||
|
// Valori sentinella: soglia di allarme non impostata.
|
||||||
|
GAUGE_NO_WARN_LO = -1E30;
|
||||||
|
GAUGE_NO_WARN_HI = 1E30;
|
||||||
|
|
||||||
|
type
|
||||||
|
/// Dati necessari a disegnare una colonna: raccolti in un record cosi' lo
|
||||||
|
/// stesso disegno e' riusabile da qualunque controllo, non solo da
|
||||||
|
/// TTankGauge (lo usa anche l'elemento gauge della plancia).
|
||||||
|
TGaugeInfo = record
|
||||||
|
Title: string;
|
||||||
|
Units: string;
|
||||||
|
MinValue: Double;
|
||||||
|
MaxValue: Double;
|
||||||
|
Value: Double;
|
||||||
|
WarnBelow: Double;
|
||||||
|
WarnAbove: Double;
|
||||||
|
TickCount: Integer;
|
||||||
|
Valid: Boolean;
|
||||||
|
BackColor: TColor;
|
||||||
|
/// Colonna e scritte abbassate, per non abbagliare in navigazione notturna.
|
||||||
|
NightMode: Boolean;
|
||||||
|
/// Fattore di zoom: scala le misure fisse del disegno (titolo, colonna,
|
||||||
|
/// tacche). 0 o assente equivale a 1.
|
||||||
|
Scale: Double;
|
||||||
|
end;
|
||||||
|
|
||||||
|
/// Disegna la colonna dentro ABounds, in coordinate del canvas ricevuto.
|
||||||
|
procedure PaintTankGauge(ACanvas: TCanvas; const ABounds: TRect;
|
||||||
|
const AInfo: TGaugeInfo);
|
||||||
|
|
||||||
|
type
|
||||||
|
/// <summary>
|
||||||
|
/// Indicatore verticale a colonna, tipo termometro o livello serbatoio.
|
||||||
|
/// Disegnato a mano su TGraphicControl: nessuna dipendenza da package
|
||||||
|
/// esterni, quindi non serve installare nulla nell'IDE.
|
||||||
|
/// </summary>
|
||||||
|
TTankGauge = class(TGraphicControl)
|
||||||
|
private
|
||||||
|
FMinValue: Double;
|
||||||
|
FMaxValue: Double;
|
||||||
|
FValue: Double;
|
||||||
|
FUnits: string;
|
||||||
|
FTitle: string;
|
||||||
|
FWarnBelow: Double;
|
||||||
|
FWarnAbove: Double;
|
||||||
|
FTickCount: Integer;
|
||||||
|
FValid: Boolean;
|
||||||
|
procedure SetValue(const AValue: Double);
|
||||||
|
procedure SetValid(const AValue: Boolean);
|
||||||
|
procedure SetTitle(const AValue: string);
|
||||||
|
procedure SetUnits(const AValue: string);
|
||||||
|
function GaugeInfo: TGaugeInfo;
|
||||||
|
protected
|
||||||
|
procedure Paint; override;
|
||||||
|
public
|
||||||
|
constructor Create(AOwner: TComponent); override;
|
||||||
|
/// Imposta il fondo scala in unita' ingegneristiche e il numero di tacche.
|
||||||
|
procedure SetRange(const AMin, AMax: Double; ATickCount: Integer = 5);
|
||||||
|
/// Sotto ABelow o sopra AAbove la colonna diventa rossa.
|
||||||
|
procedure SetWarnings(const ABelow, AAbove: Double);
|
||||||
|
/// Assegnare Value marca automaticamente il dato come valido.
|
||||||
|
property Value: Double read FValue write SetValue;
|
||||||
|
/// A False la colonna resta vuota e il valore mostra '--'.
|
||||||
|
property Valid: Boolean read FValid write SetValid;
|
||||||
|
property Title: string read FTitle write SetTitle;
|
||||||
|
property Units: string read FUnits write SetUnits;
|
||||||
|
property MinValue: Double read FMinValue;
|
||||||
|
property MaxValue: Double read FMaxValue;
|
||||||
|
published
|
||||||
|
property Color;
|
||||||
|
property Font;
|
||||||
|
property ParentFont;
|
||||||
|
property Visible;
|
||||||
|
end;
|
||||||
|
|
||||||
|
implementation
|
||||||
|
|
||||||
|
const
|
||||||
|
TITLE_H = 18;
|
||||||
|
VALUE_H = 20;
|
||||||
|
BODY_W = 28;
|
||||||
|
TICK_LEN = 4;
|
||||||
|
CLR_OK = TColor($0050AF4C);
|
||||||
|
CLR_ALARM = TColor($002F2FD3);
|
||||||
|
// Modalita' notturna: stessa frazione e stesso ambra usati dalla plancia,
|
||||||
|
// altrimenti un gauge stonerebbe in mezzo ai comandi.
|
||||||
|
GAUGE_NIGHT_DIM = 0.34;
|
||||||
|
GAUGE_NIGHT_INK = TColor($003796EB);
|
||||||
|
|
||||||
|
constructor TTankGauge.Create(AOwner: TComponent);
|
||||||
|
begin
|
||||||
|
inherited Create(AOwner);
|
||||||
|
Width := 130;
|
||||||
|
Height := 200;
|
||||||
|
Color := clBtnFace;
|
||||||
|
FMinValue := 0;
|
||||||
|
FMaxValue := 100;
|
||||||
|
FValue := 0;
|
||||||
|
FTickCount := 5;
|
||||||
|
FValid := False;
|
||||||
|
FWarnBelow := GAUGE_NO_WARN_LO;
|
||||||
|
FWarnAbove := GAUGE_NO_WARN_HI;
|
||||||
|
end;
|
||||||
|
|
||||||
|
procedure TTankGauge.SetRange(const AMin, AMax: Double; ATickCount: Integer);
|
||||||
|
begin
|
||||||
|
FMinValue := AMin;
|
||||||
|
FMaxValue := AMax;
|
||||||
|
if ATickCount < 2 then
|
||||||
|
FTickCount := 2
|
||||||
|
else
|
||||||
|
FTickCount := ATickCount;
|
||||||
|
Invalidate;
|
||||||
|
end;
|
||||||
|
|
||||||
|
procedure TTankGauge.SetWarnings(const ABelow, AAbove: Double);
|
||||||
|
begin
|
||||||
|
FWarnBelow := ABelow;
|
||||||
|
FWarnAbove := AAbove;
|
||||||
|
Invalidate;
|
||||||
|
end;
|
||||||
|
|
||||||
|
procedure TTankGauge.SetValue(const AValue: Double);
|
||||||
|
begin
|
||||||
|
if (FValue = AValue) and FValid then
|
||||||
|
Exit;
|
||||||
|
FValue := AValue;
|
||||||
|
FValid := True;
|
||||||
|
Invalidate;
|
||||||
|
end;
|
||||||
|
|
||||||
|
procedure TTankGauge.SetValid(const AValue: Boolean);
|
||||||
|
begin
|
||||||
|
if FValid = AValue then
|
||||||
|
Exit;
|
||||||
|
FValid := AValue;
|
||||||
|
Invalidate;
|
||||||
|
end;
|
||||||
|
|
||||||
|
procedure TTankGauge.SetTitle(const AValue: string);
|
||||||
|
begin
|
||||||
|
if FTitle = AValue then
|
||||||
|
Exit;
|
||||||
|
FTitle := AValue;
|
||||||
|
Invalidate;
|
||||||
|
end;
|
||||||
|
|
||||||
|
procedure TTankGauge.SetUnits(const AValue: string);
|
||||||
|
begin
|
||||||
|
if FUnits = AValue then
|
||||||
|
Exit;
|
||||||
|
FUnits := AValue;
|
||||||
|
Invalidate;
|
||||||
|
end;
|
||||||
|
|
||||||
|
function GaugeValueText(const AInfo: TGaugeInfo): string;
|
||||||
|
begin
|
||||||
|
if not AInfo.Valid then
|
||||||
|
Exit('--');
|
||||||
|
Result := FormatFloat('0.#', AInfo.Value);
|
||||||
|
if AInfo.Units <> '' then
|
||||||
|
Result := Result + ' ' + AInfo.Units;
|
||||||
|
end;
|
||||||
|
|
||||||
|
procedure PaintTankGauge(ACanvas: TCanvas; const ABounds: TRect;
|
||||||
|
const AInfo: TGaugeInfo);
|
||||||
|
var
|
||||||
|
Body: TRect;
|
||||||
|
FillTop, I, Y, TxtY, TxtH, Ticks: Integer;
|
||||||
|
TitleH, ValueH, BodyW, TickLen: Integer;
|
||||||
|
Frac, TickVal, Z: Double;
|
||||||
|
S: string;
|
||||||
|
|
||||||
|
/// Il colore come va steso: tale e quale di giorno, abbassato di notte.
|
||||||
|
function Dusk(AColor: TColor): TColor;
|
||||||
|
var
|
||||||
|
C: Longint;
|
||||||
|
begin
|
||||||
|
if not AInfo.NightMode then
|
||||||
|
Exit(AColor);
|
||||||
|
C := ColorToRGB(AColor);
|
||||||
|
Result := TColor(RGB(
|
||||||
|
Round(GetRValue(C) * GAUGE_NIGHT_DIM),
|
||||||
|
Round(GetGValue(C) * GAUGE_NIGHT_DIM),
|
||||||
|
Round(GetBValue(C) * GAUGE_NIGHT_DIM)));
|
||||||
|
end;
|
||||||
|
|
||||||
|
begin
|
||||||
|
Ticks := AInfo.TickCount;
|
||||||
|
if Ticks < 2 then
|
||||||
|
Ticks := 2;
|
||||||
|
Z := AInfo.Scale;
|
||||||
|
if Z <= 0 then
|
||||||
|
Z := 1;
|
||||||
|
TitleH := Round(TITLE_H * Z);
|
||||||
|
ValueH := Round(VALUE_H * Z);
|
||||||
|
BodyW := Round(BODY_W * Z);
|
||||||
|
TickLen := Round(TICK_LEN * Z);
|
||||||
|
|
||||||
|
ACanvas.Brush.Style := bsSolid;
|
||||||
|
ACanvas.Brush.Color := AInfo.BackColor;
|
||||||
|
ACanvas.FillRect(ABounds);
|
||||||
|
|
||||||
|
Body := Rect(ABounds.Left + 1, ABounds.Top + TitleH,
|
||||||
|
ABounds.Left + 1 + BodyW, ABounds.Bottom - ValueH);
|
||||||
|
if Body.Bottom <= Body.Top then
|
||||||
|
Exit;
|
||||||
|
|
||||||
|
// colonna vuota
|
||||||
|
ACanvas.Brush.Color := Dusk(clWhite);
|
||||||
|
ACanvas.Pen.Color := Dusk(clGray);
|
||||||
|
ACanvas.Pen.Width := 1;
|
||||||
|
ACanvas.Rectangle(Body);
|
||||||
|
|
||||||
|
// riempimento proporzionale, dal basso verso l'alto
|
||||||
|
if AInfo.Valid and (AInfo.MaxValue > AInfo.MinValue) then
|
||||||
|
begin
|
||||||
|
Frac := (AInfo.Value - AInfo.MinValue) / (AInfo.MaxValue - AInfo.MinValue);
|
||||||
|
if Frac < 0 then
|
||||||
|
Frac := 0;
|
||||||
|
if Frac > 1 then
|
||||||
|
Frac := 1;
|
||||||
|
FillTop := (Body.Bottom - 1) - Round((Body.Bottom - Body.Top - 2) * Frac);
|
||||||
|
if FillTop < Body.Bottom - 1 then
|
||||||
|
begin
|
||||||
|
if (AInfo.Value < AInfo.WarnBelow) or (AInfo.Value > AInfo.WarnAbove) then
|
||||||
|
ACanvas.Brush.Color := Dusk(CLR_ALARM)
|
||||||
|
else
|
||||||
|
ACanvas.Brush.Color := Dusk(CLR_OK);
|
||||||
|
ACanvas.FillRect(Rect(Body.Left + 1, FillTop, Body.Right - 1,
|
||||||
|
Body.Bottom - 1));
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
|
||||||
|
ACanvas.Brush.Style := bsClear;
|
||||||
|
// Di notte le scritte non si abbassano, si sostituiscono: un titolo nero
|
||||||
|
// abbassato resterebbe nero, cioe' invisibile sul fondo scuro.
|
||||||
|
if AInfo.NightMode then
|
||||||
|
ACanvas.Font.Color := GAUGE_NIGHT_INK
|
||||||
|
else
|
||||||
|
ACanvas.Font.Color := clWindowText;
|
||||||
|
|
||||||
|
// titolo
|
||||||
|
ACanvas.Font.Style := [];
|
||||||
|
ACanvas.TextOut(ABounds.Left, ABounds.Top, AInfo.Title);
|
||||||
|
|
||||||
|
// tacche della scala, dal massimo in alto al minimo in basso
|
||||||
|
ACanvas.Pen.Color := Dusk(clGray);
|
||||||
|
for I := 0 to Ticks - 1 do
|
||||||
|
begin
|
||||||
|
Y := Body.Top + Round((Body.Bottom - Body.Top) * I / (Ticks - 1));
|
||||||
|
if Y >= Body.Bottom then
|
||||||
|
Y := Body.Bottom - 1;
|
||||||
|
ACanvas.MoveTo(Body.Right, Y);
|
||||||
|
ACanvas.LineTo(Body.Right + TickLen, Y);
|
||||||
|
|
||||||
|
TickVal := AInfo.MaxValue - (AInfo.MaxValue - AInfo.MinValue) * I / (Ticks - 1);
|
||||||
|
S := FormatFloat('0.#', TickVal);
|
||||||
|
TxtH := ACanvas.TextHeight(S);
|
||||||
|
TxtY := Y - TxtH div 2;
|
||||||
|
if TxtY < Body.Top then
|
||||||
|
TxtY := Body.Top;
|
||||||
|
if TxtY + TxtH > Body.Bottom then
|
||||||
|
TxtY := Body.Bottom - TxtH;
|
||||||
|
ACanvas.TextOut(Body.Right + TickLen + 3, TxtY, S);
|
||||||
|
end;
|
||||||
|
|
||||||
|
// valore corrente
|
||||||
|
ACanvas.Font.Style := [fsBold];
|
||||||
|
ACanvas.TextOut(ABounds.Left, ABounds.Bottom - ValueH + 2,
|
||||||
|
GaugeValueText(AInfo));
|
||||||
|
end;
|
||||||
|
|
||||||
|
function TTankGauge.GaugeInfo: TGaugeInfo;
|
||||||
|
begin
|
||||||
|
Result.Title := FTitle;
|
||||||
|
Result.Units := FUnits;
|
||||||
|
Result.MinValue := FMinValue;
|
||||||
|
Result.MaxValue := FMaxValue;
|
||||||
|
Result.Value := FValue;
|
||||||
|
Result.WarnBelow := FWarnBelow;
|
||||||
|
Result.WarnAbove := FWarnAbove;
|
||||||
|
Result.TickCount := FTickCount;
|
||||||
|
Result.Valid := FValid;
|
||||||
|
Result.BackColor := Color;
|
||||||
|
Result.Scale := 1;
|
||||||
|
end;
|
||||||
|
|
||||||
|
procedure TTankGauge.Paint;
|
||||||
|
begin
|
||||||
|
Canvas.Font := Font;
|
||||||
|
PaintTankGauge(Canvas, ClientRect, GaugeInfo);
|
||||||
|
end;
|
||||||
|
|
||||||
|
end.
|
||||||
@@ -0,0 +1,224 @@
|
|||||||
|
object MainForm: TMainForm
|
||||||
|
Left = 0
|
||||||
|
Top = 0
|
||||||
|
Caption = 'Plancia ZEBRA - Controllo RS485'
|
||||||
|
ClientHeight = 620
|
||||||
|
ClientWidth = 820
|
||||||
|
Color = clBtnFace
|
||||||
|
Font.Charset = DEFAULT_CHARSET
|
||||||
|
Font.Color = clWindowText
|
||||||
|
Font.Height = -12
|
||||||
|
Font.Name = 'Segoe UI'
|
||||||
|
Font.Style = []
|
||||||
|
Position = poScreenCenter
|
||||||
|
OnCreate = FormCreate
|
||||||
|
OnDestroy = FormDestroy
|
||||||
|
TextHeight = 15
|
||||||
|
object pnlTop: TPanel
|
||||||
|
Left = 0
|
||||||
|
Top = 0
|
||||||
|
Width = 820
|
||||||
|
Height = 110
|
||||||
|
Align = alTop
|
||||||
|
TabOrder = 0
|
||||||
|
ExplicitWidth = 814
|
||||||
|
object lblPort: TLabel
|
||||||
|
Left = 16
|
||||||
|
Top = 16
|
||||||
|
Width = 62
|
||||||
|
Height = 15
|
||||||
|
Caption = 'Porta COM:'
|
||||||
|
end
|
||||||
|
object lblBaud: TLabel
|
||||||
|
Left = 160
|
||||||
|
Top = 16
|
||||||
|
Width = 30
|
||||||
|
Height = 15
|
||||||
|
Caption = 'Baud:'
|
||||||
|
end
|
||||||
|
object lblStatus: TLabel
|
||||||
|
Left = 448
|
||||||
|
Top = 15
|
||||||
|
Width = 104
|
||||||
|
Height = 15
|
||||||
|
Caption = 'Stato: Disconnesso'
|
||||||
|
Font.Charset = DEFAULT_CHARSET
|
||||||
|
Font.Color = clWindowText
|
||||||
|
Font.Height = -12
|
||||||
|
Font.Name = 'Segoe UI'
|
||||||
|
Font.Style = [fsBold]
|
||||||
|
ParentFont = False
|
||||||
|
end
|
||||||
|
object lblSlaveRele: TLabel
|
||||||
|
Left = 16
|
||||||
|
Top = 60
|
||||||
|
Width = 52
|
||||||
|
Height = 15
|
||||||
|
Caption = 'Slave rel'#232':'
|
||||||
|
end
|
||||||
|
object lblSlaveIO: TLabel
|
||||||
|
Left = 160
|
||||||
|
Top = 60
|
||||||
|
Width = 50
|
||||||
|
Height = 15
|
||||||
|
Caption = 'Slave I/O:'
|
||||||
|
end
|
||||||
|
object lblSlaveAnalog: TLabel
|
||||||
|
Left = 300
|
||||||
|
Top = 60
|
||||||
|
Width = 72
|
||||||
|
Height = 15
|
||||||
|
Caption = 'Slave analog.:'
|
||||||
|
end
|
||||||
|
object cboPort: TComboBox
|
||||||
|
Left = 16
|
||||||
|
Top = 34
|
||||||
|
Width = 121
|
||||||
|
Height = 23
|
||||||
|
Style = csDropDownList
|
||||||
|
TabOrder = 0
|
||||||
|
end
|
||||||
|
object cboBaud: TComboBox
|
||||||
|
Left = 160
|
||||||
|
Top = 34
|
||||||
|
Width = 121
|
||||||
|
Height = 23
|
||||||
|
Style = csDropDownList
|
||||||
|
TabOrder = 1
|
||||||
|
end
|
||||||
|
object btnConnect: TButton
|
||||||
|
Left = 448
|
||||||
|
Top = 41
|
||||||
|
Width = 100
|
||||||
|
Height = 30
|
||||||
|
Caption = 'Connetti'
|
||||||
|
TabOrder = 2
|
||||||
|
OnClick = btnConnectClick
|
||||||
|
end
|
||||||
|
object edtSlaveRele: TEdit
|
||||||
|
Left = 92
|
||||||
|
Top = 57
|
||||||
|
Width = 40
|
||||||
|
Height = 23
|
||||||
|
TabOrder = 3
|
||||||
|
Text = '1'
|
||||||
|
end
|
||||||
|
object edtSlaveIO: TEdit
|
||||||
|
Left = 226
|
||||||
|
Top = 57
|
||||||
|
Width = 40
|
||||||
|
Height = 23
|
||||||
|
TabOrder = 4
|
||||||
|
Text = '2'
|
||||||
|
end
|
||||||
|
object edtSlaveAnalog: TEdit
|
||||||
|
Left = 386
|
||||||
|
Top = 57
|
||||||
|
Width = 40
|
||||||
|
Height = 23
|
||||||
|
TabOrder = 5
|
||||||
|
Text = '5'
|
||||||
|
end
|
||||||
|
object CBReadInput: TCheckBox
|
||||||
|
Left = 451
|
||||||
|
Top = 77
|
||||||
|
Width = 97
|
||||||
|
Height = 17
|
||||||
|
Caption = 'Leggi Ingressi'
|
||||||
|
Checked = True
|
||||||
|
State = cbChecked
|
||||||
|
TabOrder = 6
|
||||||
|
end
|
||||||
|
object CBReadAnalog: TCheckBox
|
||||||
|
Left = 560
|
||||||
|
Top = 77
|
||||||
|
Width = 113
|
||||||
|
Height = 17
|
||||||
|
Caption = 'Leggi Analogici'
|
||||||
|
TabOrder = 7
|
||||||
|
end
|
||||||
|
end
|
||||||
|
object pgcMain: TPageControl
|
||||||
|
Left = 0
|
||||||
|
Top = 110
|
||||||
|
Width = 820
|
||||||
|
Height = 380
|
||||||
|
ActivePage = tsInterruttori
|
||||||
|
Align = alClient
|
||||||
|
TabOrder = 1
|
||||||
|
ExplicitWidth = 814
|
||||||
|
ExplicitHeight = 363
|
||||||
|
object tsInterruttori: TTabSheet
|
||||||
|
Caption = 'Interruttori'
|
||||||
|
object pnlInterruttori: TPanel
|
||||||
|
Left = 0
|
||||||
|
Top = 0
|
||||||
|
Width = 812
|
||||||
|
Height = 350
|
||||||
|
Align = alClient
|
||||||
|
BevelOuter = bvNone
|
||||||
|
TabOrder = 0
|
||||||
|
ExplicitWidth = 806
|
||||||
|
ExplicitHeight = 333
|
||||||
|
end
|
||||||
|
end
|
||||||
|
object tsPulsanti: TTabSheet
|
||||||
|
Caption = 'Pulsanti'
|
||||||
|
ImageIndex = 3
|
||||||
|
object pnlPulsanti: TPanel
|
||||||
|
Left = 0
|
||||||
|
Top = 0
|
||||||
|
Width = 812
|
||||||
|
Height = 350
|
||||||
|
Align = alClient
|
||||||
|
BevelOuter = bvNone
|
||||||
|
TabOrder = 0
|
||||||
|
end
|
||||||
|
end
|
||||||
|
object tsInputs: TTabSheet
|
||||||
|
Caption = 'Ingressi Binari'
|
||||||
|
ImageIndex = 1
|
||||||
|
object pnlInputs: TPanel
|
||||||
|
Left = 0
|
||||||
|
Top = 0
|
||||||
|
Width = 812
|
||||||
|
Height = 350
|
||||||
|
Align = alClient
|
||||||
|
BevelOuter = bvNone
|
||||||
|
TabOrder = 0
|
||||||
|
end
|
||||||
|
end
|
||||||
|
object tsAnalog: TTabSheet
|
||||||
|
Caption = 'Analogici'
|
||||||
|
ImageIndex = 2
|
||||||
|
object pnlAnalog: TPanel
|
||||||
|
Left = 0
|
||||||
|
Top = 0
|
||||||
|
Width = 812
|
||||||
|
Height = 350
|
||||||
|
Align = alClient
|
||||||
|
BevelOuter = bvNone
|
||||||
|
TabOrder = 0
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
object mmoLog: TMemo
|
||||||
|
Left = 0
|
||||||
|
Top = 490
|
||||||
|
Width = 820
|
||||||
|
Height = 130
|
||||||
|
Align = alBottom
|
||||||
|
ReadOnly = True
|
||||||
|
ScrollBars = ssVertical
|
||||||
|
TabOrder = 2
|
||||||
|
ExplicitTop = 473
|
||||||
|
ExplicitWidth = 814
|
||||||
|
end
|
||||||
|
object tmrPoll: TTimer
|
||||||
|
Enabled = False
|
||||||
|
Interval = 500
|
||||||
|
OnTimer = tmrPollTimer
|
||||||
|
Left = 760
|
||||||
|
Top = 16
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,631 @@
|
|||||||
|
unit uMain;
|
||||||
|
|
||||||
|
interface
|
||||||
|
|
||||||
|
uses
|
||||||
|
Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants,
|
||||||
|
System.Classes, Vcl.Graphics, Vcl.Controls, Vcl.Forms, Vcl.Dialogs,
|
||||||
|
Vcl.StdCtrls, Vcl.ExtCtrls, Vcl.ComCtrls, Vcl.Buttons,
|
||||||
|
System.Generics.Collections, System.Win.Registry, System.StrUtils,
|
||||||
|
uModbusRTU, uGauge;
|
||||||
|
|
||||||
|
type
|
||||||
|
TMainForm = class(TForm)
|
||||||
|
pnlTop: TPanel;
|
||||||
|
lblPort: TLabel;
|
||||||
|
cboPort: TComboBox;
|
||||||
|
lblBaud: TLabel;
|
||||||
|
cboBaud: TComboBox;
|
||||||
|
btnConnect: TButton;
|
||||||
|
lblStatus: TLabel;
|
||||||
|
lblSlaveRele: TLabel;
|
||||||
|
edtSlaveRele: TEdit;
|
||||||
|
lblSlaveIO: TLabel;
|
||||||
|
edtSlaveIO: TEdit;
|
||||||
|
lblSlaveAnalog: TLabel;
|
||||||
|
edtSlaveAnalog: TEdit;
|
||||||
|
pgcMain: TPageControl;
|
||||||
|
tsInterruttori: TTabSheet;
|
||||||
|
tsPulsanti: TTabSheet;
|
||||||
|
tsInputs: TTabSheet;
|
||||||
|
tsAnalog: TTabSheet;
|
||||||
|
pnlInterruttori: TPanel;
|
||||||
|
pnlPulsanti: TPanel;
|
||||||
|
pnlInputs: TPanel;
|
||||||
|
pnlAnalog: TPanel;
|
||||||
|
mmoLog: TMemo;
|
||||||
|
tmrPoll: TTimer;
|
||||||
|
CBReadInput: TCheckBox;
|
||||||
|
CBReadAnalog: TCheckBox;
|
||||||
|
procedure FormCreate(Sender: TObject);
|
||||||
|
procedure FormDestroy(Sender: TObject);
|
||||||
|
procedure btnConnectClick(Sender: TObject);
|
||||||
|
procedure tmrPollTimer(Sender: TObject);
|
||||||
|
private
|
||||||
|
FModbus: TModbusRTU;
|
||||||
|
FSwitchButtons: array[0..15] of TSpeedButton;
|
||||||
|
FPulseButtons: array[0..15] of TSpeedButton;
|
||||||
|
FInputShapes: array[0..7] of TShape;
|
||||||
|
FInputLabels: array[0..7] of TLabel;
|
||||||
|
FAnalogLabels: array[0..3] of TLabel;
|
||||||
|
FAnalogGauges: array[0..3] of TTankGauge;
|
||||||
|
/// Ultimo stato mostrato per ogni ingresso, per registrare a log solo le
|
||||||
|
/// transizioni: un contatto che si chiude e' un evento, non un livello.
|
||||||
|
FInputState: array[0..7] of Boolean;
|
||||||
|
FInputValid: array[0..7] of Boolean;
|
||||||
|
/// Ultimo errore gia' registrato, per non riempire il log con lo stesso
|
||||||
|
/// messaggio due volte al secondo finche' il bus resta muto.
|
||||||
|
FInputErr: string;
|
||||||
|
FAnalogErr: string;
|
||||||
|
procedure Log(const AMsg: string);
|
||||||
|
procedure SetInputVisual(AIndex: Integer; AValid, AClosed: Boolean);
|
||||||
|
procedure InvalidateInputs;
|
||||||
|
procedure BuildSwitchControls;
|
||||||
|
procedure BuildPulseControls;
|
||||||
|
procedure BuildInputControls;
|
||||||
|
procedure BuildAnalogControls;
|
||||||
|
procedure SwitchButtonClick(Sender: TObject);
|
||||||
|
procedure PulseButtonMouseDown(Sender: TObject; Button: TMouseButton;
|
||||||
|
Shift: TShiftState; X, Y: Integer);
|
||||||
|
procedure PulseButtonMouseUp(Sender: TObject; Button: TMouseButton;
|
||||||
|
Shift: TShiftState; X, Y: Integer);
|
||||||
|
procedure SetPulseState(ABtn: TSpeedButton; AOn: Boolean);
|
||||||
|
procedure UpdateButtonVisual(ABtn: TSpeedButton; AOn: Boolean);
|
||||||
|
procedure SetConnectedState(AConnected: Boolean);
|
||||||
|
public
|
||||||
|
end;
|
||||||
|
|
||||||
|
var
|
||||||
|
MainForm: TMainForm;
|
||||||
|
|
||||||
|
implementation
|
||||||
|
|
||||||
|
{$R *.dfm}
|
||||||
|
|
||||||
|
type
|
||||||
|
/// Scalatura di un canale analogico: dal valore grezzo del registro Modbus
|
||||||
|
/// al valore ingegneristico mostrato dal gauge.
|
||||||
|
TAnalogCfg = record
|
||||||
|
Title: string;
|
||||||
|
RawMin: Integer;
|
||||||
|
RawMax: Integer;
|
||||||
|
EngMin: Double;
|
||||||
|
EngMax: Double;
|
||||||
|
Units: string;
|
||||||
|
WarnBelow: Double;
|
||||||
|
WarnAbove: Double;
|
||||||
|
end;
|
||||||
|
|
||||||
|
const
|
||||||
|
GRID_COLS = 4;
|
||||||
|
CTRL_W = 90;
|
||||||
|
CTRL_H = 30;
|
||||||
|
MARGIN = 12;
|
||||||
|
HDR_H = 24;
|
||||||
|
GAUGE_W = 130;
|
||||||
|
GAUGE_H = 200;
|
||||||
|
// Passo fra una spia d'ingresso e l'altra: deve stare larga la didascalia
|
||||||
|
// su due righe, altrimenti le scritte si accavallano.
|
||||||
|
INPUT_SPACING = 84;
|
||||||
|
|
||||||
|
// Configurazione dei 4 canali analogici. RawMin/RawMax vanno allineati al
|
||||||
|
// fondo scala del modulo (4095 = ADC a 12 bit); EngMin/EngMax sono i valori
|
||||||
|
// reali corrispondenti. WarnBelow/WarnAbove colorano di rosso la colonna.
|
||||||
|
ANALOG_CFG: array[0..3] of TAnalogCfg = (
|
||||||
|
(Title: 'Temperatura'; RawMin: 0; RawMax: 4095;
|
||||||
|
EngMin: 0; EngMax: 120; Units: '°C';
|
||||||
|
WarnBelow: GAUGE_NO_WARN_LO; WarnAbove: 100),
|
||||||
|
(Title: 'Livello serbatoio'; RawMin: 0; RawMax: 4095;
|
||||||
|
EngMin: 0; EngMax: 100; Units: '%';
|
||||||
|
WarnBelow: 15; WarnAbove: GAUGE_NO_WARN_HI),
|
||||||
|
(Title: 'Canale 2'; RawMin: 0; RawMax: 4095;
|
||||||
|
EngMin: 0; EngMax: 100; Units: '%';
|
||||||
|
WarnBelow: GAUGE_NO_WARN_LO; WarnAbove: GAUGE_NO_WARN_HI),
|
||||||
|
(Title: 'Canale 3'; RawMin: 0; RawMax: 4095;
|
||||||
|
EngMin: 0; EngMax: 100; Units: '%';
|
||||||
|
WarnBelow: GAUGE_NO_WARN_LO; WarnAbove: GAUGE_NO_WARN_HI)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Etichette dei pulsanti momentanei: stringa vuota = 'Canale N'.
|
||||||
|
// Es.: impostare PULSE_NAMES[0] := 'Horn' per il canale 0.
|
||||||
|
PULSE_NAMES: array[0..15] of string = (
|
||||||
|
'', '', '', '', '', '', '', '',
|
||||||
|
'', '', '', '', '', '', '', ''
|
||||||
|
);
|
||||||
|
|
||||||
|
/// Le porte realmente presenti, lette dal registro di Windows. Elencarle a
|
||||||
|
/// mano significa non vedere l'adattatore USB quando si presenta con un numero
|
||||||
|
/// diverso da quelli previsti.
|
||||||
|
procedure EnumSerialPorts(AList: TStrings);
|
||||||
|
var
|
||||||
|
Reg: TRegistry;
|
||||||
|
Names: TStringList;
|
||||||
|
Name: string;
|
||||||
|
begin
|
||||||
|
AList.Clear;
|
||||||
|
Reg := TRegistry.Create(KEY_READ);
|
||||||
|
Names := TStringList.Create;
|
||||||
|
try
|
||||||
|
Reg.RootKey := HKEY_LOCAL_MACHINE;
|
||||||
|
if Reg.OpenKeyReadOnly('HARDWARE\DEVICEMAP\SERIALCOMM') then
|
||||||
|
begin
|
||||||
|
Reg.GetValueNames(Names);
|
||||||
|
for Name in Names do
|
||||||
|
AList.Add(Reg.ReadString(Name));
|
||||||
|
Reg.CloseKey;
|
||||||
|
end;
|
||||||
|
finally
|
||||||
|
Names.Free;
|
||||||
|
Reg.Free;
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
|
||||||
|
procedure TMainForm.FormCreate(Sender: TObject);
|
||||||
|
var
|
||||||
|
I: Integer;
|
||||||
|
begin
|
||||||
|
EnumSerialPorts(cboPort.Items);
|
||||||
|
if cboPort.Items.Count = 0 then
|
||||||
|
// Nessuna porta trovata: meglio una lista di ripiego che una casella vuota.
|
||||||
|
for I := 1 to 10 do
|
||||||
|
cboPort.Items.Add('COM' + IntToStr(I));
|
||||||
|
I := cboPort.Items.IndexOf('COM7');
|
||||||
|
if I < 0 then
|
||||||
|
I := 0;
|
||||||
|
cboPort.ItemIndex := I;
|
||||||
|
|
||||||
|
cboBaud.Items.CommaText := '9600,19200,38400,57600,115200';
|
||||||
|
cboBaud.ItemIndex := 0;
|
||||||
|
|
||||||
|
edtSlaveRele.Text := '1';
|
||||||
|
edtSlaveIO.Text := '2';
|
||||||
|
edtSlaveAnalog.Text := '5';
|
||||||
|
|
||||||
|
BuildSwitchControls;
|
||||||
|
BuildPulseControls;
|
||||||
|
BuildInputControls;
|
||||||
|
BuildAnalogControls;
|
||||||
|
|
||||||
|
SetConnectedState(False);
|
||||||
|
end;
|
||||||
|
|
||||||
|
procedure TMainForm.FormDestroy(Sender: TObject);
|
||||||
|
begin
|
||||||
|
tmrPoll.Enabled := False;
|
||||||
|
if Assigned(FModbus) then
|
||||||
|
begin
|
||||||
|
FModbus.Disconnect;
|
||||||
|
FModbus.Free;
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
|
||||||
|
procedure TMainForm.Log(const AMsg: string);
|
||||||
|
begin
|
||||||
|
mmoLog.Lines.Add(FormatDateTime('hh:nn:ss', Now) + ' ' + AMsg);
|
||||||
|
end;
|
||||||
|
|
||||||
|
function ChannelCaption(AIndex: Integer): string;
|
||||||
|
begin
|
||||||
|
if PULSE_NAMES[AIndex] <> '' then
|
||||||
|
Result := PULSE_NAMES[AIndex]
|
||||||
|
else
|
||||||
|
Result := 'Canale ' + IntToStr(AIndex);
|
||||||
|
end;
|
||||||
|
|
||||||
|
procedure TMainForm.BuildSwitchControls;
|
||||||
|
var
|
||||||
|
I, Row, Col: Integer;
|
||||||
|
Hdr: TLabel;
|
||||||
|
Btn: TSpeedButton;
|
||||||
|
begin
|
||||||
|
Hdr := TLabel.Create(Self);
|
||||||
|
Hdr.Parent := pnlInterruttori;
|
||||||
|
Hdr.Caption := 'Il canale resta chiuso finché il pulsante è premuto giù.';
|
||||||
|
Hdr.Left := MARGIN;
|
||||||
|
Hdr.Top := MARGIN;
|
||||||
|
|
||||||
|
for I := 0 to 15 do
|
||||||
|
begin
|
||||||
|
Row := I div GRID_COLS;
|
||||||
|
Col := I mod GRID_COLS;
|
||||||
|
Btn := TSpeedButton.Create(Self);
|
||||||
|
Btn.Parent := pnlInterruttori;
|
||||||
|
Btn.Caption := 'Canale ' + IntToStr(I);
|
||||||
|
Btn.Left := MARGIN + Col * (CTRL_W + MARGIN);
|
||||||
|
Btn.Top := HDR_H + MARGIN + Row * (CTRL_H + 6);
|
||||||
|
Btn.Width := CTRL_W;
|
||||||
|
Btn.Height := CTRL_H;
|
||||||
|
Btn.Tag := I;
|
||||||
|
// GroupIndex univoco + AllowAllUp: il pulsante resta premuto e si sgancia
|
||||||
|
// al click successivo, senza influenzare gli altri canali.
|
||||||
|
Btn.GroupIndex := I + 1;
|
||||||
|
Btn.AllowAllUp := True;
|
||||||
|
Btn.Down := False;
|
||||||
|
Btn.OnClick := SwitchButtonClick;
|
||||||
|
FSwitchButtons[I] := Btn;
|
||||||
|
UpdateButtonVisual(Btn, False);
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
|
||||||
|
procedure TMainForm.BuildPulseControls;
|
||||||
|
var
|
||||||
|
I, Row, Col: Integer;
|
||||||
|
Hdr: TLabel;
|
||||||
|
Btn: TSpeedButton;
|
||||||
|
begin
|
||||||
|
Hdr := TLabel.Create(Self);
|
||||||
|
Hdr.Parent := pnlPulsanti;
|
||||||
|
Hdr.Caption := 'Il canale resta chiuso solo mentre il pulsante è premuto ' +
|
||||||
|
'(contatto momentaneo, es. horn).';
|
||||||
|
Hdr.Left := MARGIN;
|
||||||
|
Hdr.Top := MARGIN;
|
||||||
|
|
||||||
|
for I := 0 to 15 do
|
||||||
|
begin
|
||||||
|
Row := I div GRID_COLS;
|
||||||
|
Col := I mod GRID_COLS;
|
||||||
|
Btn := TSpeedButton.Create(Self);
|
||||||
|
Btn.Parent := pnlPulsanti;
|
||||||
|
Btn.Caption := ChannelCaption(I);
|
||||||
|
Btn.Left := MARGIN + Col * (CTRL_W + MARGIN);
|
||||||
|
Btn.Top := HDR_H + MARGIN + Row * (CTRL_H + 6);
|
||||||
|
Btn.Width := CTRL_W;
|
||||||
|
Btn.Height := CTRL_H;
|
||||||
|
Btn.Tag := I;
|
||||||
|
// Nessun GroupIndex: il pulsante non resta giù. Il comando è legato a
|
||||||
|
// MouseDown/MouseUp, non a OnClick, per chiudere e riaprire il contatto.
|
||||||
|
Btn.OnMouseDown := PulseButtonMouseDown;
|
||||||
|
Btn.OnMouseUp := PulseButtonMouseUp;
|
||||||
|
FPulseButtons[I] := Btn;
|
||||||
|
UpdateButtonVisual(Btn, False);
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
|
||||||
|
procedure TMainForm.UpdateButtonVisual(ABtn: TSpeedButton; AOn: Boolean);
|
||||||
|
begin
|
||||||
|
ABtn.ParentFont := False;
|
||||||
|
if AOn then
|
||||||
|
begin
|
||||||
|
ABtn.Font.Style := [fsBold];
|
||||||
|
ABtn.Font.Color := clGreen;
|
||||||
|
end
|
||||||
|
else
|
||||||
|
begin
|
||||||
|
ABtn.Font.Style := [];
|
||||||
|
ABtn.Font.Color := clWindowText;
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
|
||||||
|
procedure TMainForm.BuildInputControls;
|
||||||
|
var
|
||||||
|
I: Integer;
|
||||||
|
Shp: TShape;
|
||||||
|
Lbl: TLabel;
|
||||||
|
Hdr: TLabel;
|
||||||
|
begin
|
||||||
|
Hdr := TLabel.Create(Self);
|
||||||
|
Hdr.Parent := pnlInputs;
|
||||||
|
Hdr.Caption := 'Contatto chiuso = spia verde. Grigio = aperto. ' +
|
||||||
|
'Bordo rosso = dato non disponibile, non fidarsi di quello che mostra.';
|
||||||
|
Hdr.Left := MARGIN;
|
||||||
|
Hdr.Top := MARGIN;
|
||||||
|
|
||||||
|
for I := 0 to 7 do
|
||||||
|
begin
|
||||||
|
Shp := TShape.Create(Self);
|
||||||
|
Shp.Parent := pnlInputs;
|
||||||
|
Shp.Shape := stCircle;
|
||||||
|
Shp.Width := 24;
|
||||||
|
Shp.Height := 24;
|
||||||
|
Shp.Left := MARGIN + I * INPUT_SPACING;
|
||||||
|
Shp.Top := HDR_H + MARGIN;
|
||||||
|
FInputShapes[I] := Shp;
|
||||||
|
|
||||||
|
// La serigrafia del modulo parte da DI1, i canali Modbus da 0: tenere
|
||||||
|
// tutti e due sotto gli occhi evita di sbagliare morsetto in barca.
|
||||||
|
Lbl := TLabel.Create(Self);
|
||||||
|
Lbl.Parent := pnlInputs;
|
||||||
|
Lbl.Left := Shp.Left;
|
||||||
|
Lbl.Top := Shp.Top + 30;
|
||||||
|
FInputLabels[I] := Lbl;
|
||||||
|
|
||||||
|
FInputValid[I] := False;
|
||||||
|
FInputState[I] := False;
|
||||||
|
SetInputVisual(I, False, False);
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
|
||||||
|
/// Distingue tre casi, non due: chiuso, aperto, e "non lo so". Un bus caduto
|
||||||
|
/// non deve somigliare a un contatto aperto.
|
||||||
|
procedure TMainForm.SetInputVisual(AIndex: Integer; AValid, AClosed: Boolean);
|
||||||
|
var
|
||||||
|
Shp: TShape;
|
||||||
|
begin
|
||||||
|
Shp := FInputShapes[AIndex];
|
||||||
|
if not AValid then
|
||||||
|
begin
|
||||||
|
Shp.Brush.Color := clSilver;
|
||||||
|
Shp.Pen.Color := clRed;
|
||||||
|
Shp.Pen.Width := 2;
|
||||||
|
FInputLabels[AIndex].Caption := Format('DI%d (ch%d)'#13'n/d', [AIndex + 1, AIndex]);
|
||||||
|
end
|
||||||
|
else if AClosed then
|
||||||
|
begin
|
||||||
|
Shp.Brush.Color := clLime;
|
||||||
|
Shp.Pen.Color := clGreen;
|
||||||
|
Shp.Pen.Width := 1;
|
||||||
|
FInputLabels[AIndex].Caption := Format('DI%d (ch%d)'#13'CHIUSO', [AIndex + 1, AIndex]);
|
||||||
|
end
|
||||||
|
else
|
||||||
|
begin
|
||||||
|
Shp.Brush.Color := clGray;
|
||||||
|
Shp.Pen.Color := clBlack;
|
||||||
|
Shp.Pen.Width := 1;
|
||||||
|
FInputLabels[AIndex].Caption := Format('DI%d (ch%d)'#13'aperto', [AIndex + 1, AIndex]);
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
|
||||||
|
procedure TMainForm.InvalidateInputs;
|
||||||
|
var
|
||||||
|
I: Integer;
|
||||||
|
begin
|
||||||
|
for I := 0 to 7 do
|
||||||
|
begin
|
||||||
|
FInputValid[I] := False;
|
||||||
|
SetInputVisual(I, False, False);
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
|
||||||
|
function RawToEng(AIndex: Integer; ARaw: Word): Double;
|
||||||
|
var
|
||||||
|
Cfg: TAnalogCfg;
|
||||||
|
begin
|
||||||
|
Cfg := ANALOG_CFG[AIndex];
|
||||||
|
if Cfg.RawMax = Cfg.RawMin then
|
||||||
|
Exit(Cfg.EngMin);
|
||||||
|
Result := Cfg.EngMin +
|
||||||
|
(Integer(ARaw) - Cfg.RawMin) * (Cfg.EngMax - Cfg.EngMin) /
|
||||||
|
(Cfg.RawMax - Cfg.RawMin);
|
||||||
|
end;
|
||||||
|
|
||||||
|
procedure TMainForm.BuildAnalogControls;
|
||||||
|
var
|
||||||
|
I: Integer;
|
||||||
|
Lbl: TLabel;
|
||||||
|
Gauge: TTankGauge;
|
||||||
|
begin
|
||||||
|
// Il gauge è un TGraphicControl: senza doppio buffering la colonna
|
||||||
|
// sfarfalla ad ogni ciclo di polling.
|
||||||
|
pnlAnalog.DoubleBuffered := True;
|
||||||
|
|
||||||
|
for I := 0 to 3 do
|
||||||
|
begin
|
||||||
|
Gauge := TTankGauge.Create(Self);
|
||||||
|
Gauge.Parent := pnlAnalog;
|
||||||
|
Gauge.Left := MARGIN + I * (GAUGE_W + MARGIN);
|
||||||
|
Gauge.Top := MARGIN;
|
||||||
|
Gauge.Width := GAUGE_W;
|
||||||
|
Gauge.Height := GAUGE_H;
|
||||||
|
Gauge.Title := ANALOG_CFG[I].Title;
|
||||||
|
Gauge.Units := ANALOG_CFG[I].Units;
|
||||||
|
Gauge.SetRange(ANALOG_CFG[I].EngMin, ANALOG_CFG[I].EngMax);
|
||||||
|
Gauge.SetWarnings(ANALOG_CFG[I].WarnBelow, ANALOG_CFG[I].WarnAbove);
|
||||||
|
FAnalogGauges[I] := Gauge;
|
||||||
|
|
||||||
|
// Sotto al gauge resta il valore grezzo letto dal registro.
|
||||||
|
Lbl := TLabel.Create(Self);
|
||||||
|
Lbl.Parent := pnlAnalog;
|
||||||
|
Lbl.Caption := 'Registro: --';
|
||||||
|
Lbl.Left := Gauge.Left;
|
||||||
|
Lbl.Top := Gauge.Top + GAUGE_H + 4;
|
||||||
|
FAnalogLabels[I] := Lbl;
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
|
||||||
|
procedure TMainForm.SetConnectedState(AConnected: Boolean);
|
||||||
|
var
|
||||||
|
I: Integer;
|
||||||
|
begin
|
||||||
|
if not AConnected then
|
||||||
|
begin
|
||||||
|
// Senza polling i valori a video sarebbero fermi all'ultima lettura.
|
||||||
|
for I := 0 to 3 do
|
||||||
|
begin
|
||||||
|
FAnalogGauges[I].Valid := False;
|
||||||
|
FAnalogLabels[I].Caption := 'Registro: --';
|
||||||
|
end;
|
||||||
|
InvalidateInputs;
|
||||||
|
end;
|
||||||
|
|
||||||
|
if AConnected then
|
||||||
|
begin
|
||||||
|
btnConnect.Caption := 'Disconnetti';
|
||||||
|
lblStatus.Caption := 'Stato: Connesso';
|
||||||
|
lblStatus.Font.Color := clGreen;
|
||||||
|
end
|
||||||
|
else
|
||||||
|
begin
|
||||||
|
btnConnect.Caption := 'Connetti';
|
||||||
|
lblStatus.Caption := 'Stato: Disconnesso';
|
||||||
|
lblStatus.Font.Color := clRed;
|
||||||
|
end;
|
||||||
|
cboPort.Enabled := not AConnected;
|
||||||
|
cboBaud.Enabled := not AConnected;
|
||||||
|
tmrPoll.Enabled := AConnected;
|
||||||
|
end;
|
||||||
|
|
||||||
|
procedure TMainForm.btnConnectClick(Sender: TObject);
|
||||||
|
begin
|
||||||
|
if Assigned(FModbus) and FModbus.IsConnected then
|
||||||
|
begin
|
||||||
|
FModbus.Disconnect;
|
||||||
|
FreeAndNil(FModbus);
|
||||||
|
SetConnectedState(False);
|
||||||
|
Log('Disconnesso.');
|
||||||
|
Exit;
|
||||||
|
end;
|
||||||
|
|
||||||
|
try
|
||||||
|
FModbus := TModbusRTU.Create(cboPort.Text, StrToInt(cboBaud.Text), 500);
|
||||||
|
FModbus.Connect;
|
||||||
|
SetConnectedState(True);
|
||||||
|
Log('Connesso a ' + cboPort.Text + ' @ ' + cboBaud.Text + ' baud.');
|
||||||
|
except
|
||||||
|
on E: Exception do
|
||||||
|
begin
|
||||||
|
Log('ERRORE connessione: ' + E.Message);
|
||||||
|
FreeAndNil(FModbus);
|
||||||
|
SetConnectedState(False);
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
|
||||||
|
procedure TMainForm.SwitchButtonClick(Sender: TObject);
|
||||||
|
var
|
||||||
|
Btn: TSpeedButton;
|
||||||
|
Idx: Integer;
|
||||||
|
SlaveAddr: Integer;
|
||||||
|
begin
|
||||||
|
Btn := TSpeedButton(Sender);
|
||||||
|
UpdateButtonVisual(Btn, Btn.Down);
|
||||||
|
|
||||||
|
if not (Assigned(FModbus) and FModbus.IsConnected) then
|
||||||
|
begin
|
||||||
|
Log('Impossibile comandare il relè: non connesso.');
|
||||||
|
//Btn.Down := not Btn.Down;
|
||||||
|
//UpdateButtonVisual(Btn, Btn.Down);
|
||||||
|
Exit;
|
||||||
|
end;
|
||||||
|
|
||||||
|
Idx := Btn.Tag;
|
||||||
|
SlaveAddr := StrToIntDef(edtSlaveRele.Text, 1);
|
||||||
|
try
|
||||||
|
FModbus.WriteSingleCoil(SlaveAddr, Idx, Btn.Down);
|
||||||
|
Log(Format('Interruttore canale %d -> %s', [Idx, BoolToStr(Btn.Down, True)]));
|
||||||
|
except
|
||||||
|
on E: Exception do
|
||||||
|
begin
|
||||||
|
Log('ERRORE scrittura relè: ' + E.Message);
|
||||||
|
Btn.Down := not Btn.Down;
|
||||||
|
UpdateButtonVisual(Btn, Btn.Down);
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
|
||||||
|
procedure TMainForm.PulseButtonMouseDown(Sender: TObject; Button: TMouseButton;
|
||||||
|
Shift: TShiftState; X, Y: Integer);
|
||||||
|
begin
|
||||||
|
if Button = mbLeft then
|
||||||
|
SetPulseState(TSpeedButton(Sender), True);
|
||||||
|
end;
|
||||||
|
|
||||||
|
procedure TMainForm.PulseButtonMouseUp(Sender: TObject; Button: TMouseButton;
|
||||||
|
Shift: TShiftState; X, Y: Integer);
|
||||||
|
begin
|
||||||
|
// Il mouse è catturato dal pulsante: il MouseUp arriva anche se il cursore
|
||||||
|
// è stato trascinato fuori, quindi il contatto viene sempre riaperto.
|
||||||
|
if Button = mbLeft then
|
||||||
|
SetPulseState(TSpeedButton(Sender), False);
|
||||||
|
end;
|
||||||
|
|
||||||
|
procedure TMainForm.SetPulseState(ABtn: TSpeedButton; AOn: Boolean);
|
||||||
|
var
|
||||||
|
Idx: Integer;
|
||||||
|
SlaveAddr: Integer;
|
||||||
|
begin
|
||||||
|
UpdateButtonVisual(ABtn, AOn);
|
||||||
|
|
||||||
|
if not (Assigned(FModbus) and FModbus.IsConnected) then
|
||||||
|
begin
|
||||||
|
if AOn then
|
||||||
|
Log('Impossibile comandare il relè: non connesso.');
|
||||||
|
Exit;
|
||||||
|
end;
|
||||||
|
|
||||||
|
Idx := ABtn.Tag;
|
||||||
|
SlaveAddr := StrToIntDef(edtSlaveRele.Text, 1);
|
||||||
|
try
|
||||||
|
FModbus.WriteSingleCoil(SlaveAddr, Idx, AOn);
|
||||||
|
Log(Format('Pulsante %s -> %s', [ChannelCaption(Idx), BoolToStr(AOn, True)]));
|
||||||
|
except
|
||||||
|
on E: Exception do
|
||||||
|
// Nessun rollback: se fallisce la chiusura si tenta comunque l'apertura
|
||||||
|
// al rilascio, così il canale non resta eccitato per un errore.
|
||||||
|
Log('ERRORE scrittura relè: ' + E.Message);
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
|
||||||
|
procedure TMainForm.tmrPollTimer(Sender: TObject);
|
||||||
|
var
|
||||||
|
Inputs: TArray<Boolean>;
|
||||||
|
Analog: TArray<Word>;
|
||||||
|
I: Integer;
|
||||||
|
SlaveIO, SlaveAnalog: Integer;
|
||||||
|
begin
|
||||||
|
if not (Assigned(FModbus) and FModbus.IsConnected) then Exit;
|
||||||
|
|
||||||
|
SlaveIO := StrToIntDef(edtSlaveIO.Text, 2);
|
||||||
|
SlaveAnalog := StrToIntDef(edtSlaveAnalog.Text, 5);
|
||||||
|
|
||||||
|
if CBReadInput.Checked then
|
||||||
|
begin
|
||||||
|
try
|
||||||
|
Inputs := FModbus.ReadDiscreteInputs(SlaveIO, 0, 8);
|
||||||
|
for I := 0 to 7 do
|
||||||
|
begin
|
||||||
|
// Il log registra le transizioni, non i livelli: su una barca interessa
|
||||||
|
// sapere *quando* un contatto si e' chiuso, non che e' chiuso da un'ora.
|
||||||
|
if (not FInputValid[I]) or (Inputs[I] <> FInputState[I]) then
|
||||||
|
Log(Format('DI%d (ch%d) -> %s', [I + 1, I,
|
||||||
|
IfThen(Inputs[I], 'CHIUSO', 'aperto')]));
|
||||||
|
FInputState[I] := Inputs[I];
|
||||||
|
FInputValid[I] := True;
|
||||||
|
SetInputVisual(I, True, Inputs[I]);
|
||||||
|
end;
|
||||||
|
FInputErr := '';
|
||||||
|
except
|
||||||
|
on E: Exception do
|
||||||
|
begin
|
||||||
|
// Le spie vanno spente a "non so": lasciarle com'erano farebbe passare
|
||||||
|
// un bus caduto per un quadro tutto a posto.
|
||||||
|
if E.Message <> FInputErr then
|
||||||
|
begin
|
||||||
|
Log(Format('ERRORE lettura ingressi slave %d: %s', [SlaveIO, E.Message]));
|
||||||
|
FInputErr := E.Message;
|
||||||
|
end;
|
||||||
|
InvalidateInputs;
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
end
|
||||||
|
else
|
||||||
|
InvalidateInputs;
|
||||||
|
|
||||||
|
if CBReadAnalog.Checked then
|
||||||
|
begin
|
||||||
|
try
|
||||||
|
Analog := FModbus.ReadHoldingRegisters(SlaveAnalog, 0, 4);
|
||||||
|
for I := 0 to 3 do
|
||||||
|
begin
|
||||||
|
FAnalogLabels[I].Caption := 'Registro: ' + IntToStr(Analog[I]);
|
||||||
|
FAnalogGauges[I].Value := RawToEng(I, Analog[I]);
|
||||||
|
end;
|
||||||
|
FAnalogErr := '';
|
||||||
|
except
|
||||||
|
on E: Exception do
|
||||||
|
begin
|
||||||
|
if E.Message <> FAnalogErr then
|
||||||
|
begin
|
||||||
|
Log(Format('ERRORE lettura analogici slave %d: %s', [SlaveAnalog, E.Message]));
|
||||||
|
FAnalogErr := E.Message;
|
||||||
|
end;
|
||||||
|
for I := 0 to 3 do
|
||||||
|
begin
|
||||||
|
FAnalogGauges[I].Valid := False;
|
||||||
|
FAnalogLabels[I].Caption := 'Registro: --';
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
|
||||||
|
end.
|
||||||
@@ -0,0 +1,378 @@
|
|||||||
|
unit uModbusRTU;
|
||||||
|
|
||||||
|
{
|
||||||
|
Libreria Modbus RTU su RS485 per Delphi
|
||||||
|
Compatibile con: moduli relè Waveshare Modbus RTU 32-Ch,
|
||||||
|
moduli I/O digitali 8-Ch, moduli acquisizione analogica 8-Ch
|
||||||
|
Usa API Win32 pura (nessuna dipendenza esterna)
|
||||||
|
}
|
||||||
|
|
||||||
|
interface
|
||||||
|
|
||||||
|
uses
|
||||||
|
Winapi.Windows, system.SysUtils, system.Classes;
|
||||||
|
|
||||||
|
type
|
||||||
|
TModbusException = class(Exception);
|
||||||
|
|
||||||
|
TModbusRTU = class
|
||||||
|
private
|
||||||
|
FHandle: THandle;
|
||||||
|
FPortName: string;
|
||||||
|
FBaudRate: Cardinal;
|
||||||
|
FTimeoutMs: Cardinal;
|
||||||
|
/// Quando e' finito l'ultimo scambio: fra un frame e l'altro il Modbus RTU
|
||||||
|
/// vuole almeno 3,5 caratteri di silenzio.
|
||||||
|
FLastFrame: UInt64;
|
||||||
|
procedure WaitInterFrame;
|
||||||
|
function CalcCRC16(const ABuf: array of Byte; ALen: Integer): Word;
|
||||||
|
function SendReceive(const ARequest: array of Byte; AExpectedLen: Integer): TBytes;
|
||||||
|
public
|
||||||
|
constructor Create(const APortName: string; ABaudRate: Cardinal = 9600; ATimeoutMs: Cardinal = 500);
|
||||||
|
destructor Destroy; override;
|
||||||
|
|
||||||
|
procedure Connect;
|
||||||
|
procedure Disconnect;
|
||||||
|
function IsConnected: Boolean;
|
||||||
|
|
||||||
|
// Function 01 - Read Coils (uscite relè)
|
||||||
|
function ReadCoils(ASlaveAddr: Byte; AStartAddr, AQuantity: Word): TArray<Boolean>;
|
||||||
|
// Function 02 - Read Discrete Inputs (ingressi digitali)
|
||||||
|
function ReadDiscreteInputs(ASlaveAddr: Byte; AStartAddr, AQuantity: Word): TArray<Boolean>;
|
||||||
|
// Function 03 - Read Holding Registers (es. ingressi analogici)
|
||||||
|
function ReadHoldingRegisters(ASlaveAddr: Byte; AStartAddr, AQuantity: Word): TArray<Word>;
|
||||||
|
// Function 04 - Read Input Registers
|
||||||
|
function ReadInputRegisters(ASlaveAddr: Byte; AStartAddr, AQuantity: Word): TArray<Word>;
|
||||||
|
// Function 05 - Write Single Coil (accende/spegne un relè)
|
||||||
|
procedure WriteSingleCoil(ASlaveAddr: Byte; AAddr: Word; AValue: Boolean);
|
||||||
|
// Function 06 - Write Single Register
|
||||||
|
procedure WriteSingleRegister(ASlaveAddr: Byte; AAddr, AValue: Word);
|
||||||
|
// Function 15 - Write Multiple Coils
|
||||||
|
procedure WriteMultipleCoils(ASlaveAddr: Byte; AStartAddr: Word; const AValues: array of Boolean);
|
||||||
|
|
||||||
|
property PortName: string read FPortName;
|
||||||
|
property BaudRate: Cardinal read FBaudRate;
|
||||||
|
property TimeoutMs: Cardinal read FTimeoutMs write FTimeoutMs;
|
||||||
|
end;
|
||||||
|
|
||||||
|
implementation
|
||||||
|
|
||||||
|
const
|
||||||
|
INVALID_HANDLE = THandle(-1);
|
||||||
|
|
||||||
|
{ TModbusRTU }
|
||||||
|
|
||||||
|
constructor TModbusRTU.Create(const APortName: string; ABaudRate: Cardinal; ATimeoutMs: Cardinal);
|
||||||
|
begin
|
||||||
|
inherited Create;
|
||||||
|
FPortName := APortName;
|
||||||
|
FBaudRate := ABaudRate;
|
||||||
|
FTimeoutMs := ATimeoutMs;
|
||||||
|
FHandle := INVALID_HANDLE;
|
||||||
|
end;
|
||||||
|
|
||||||
|
destructor TModbusRTU.Destroy;
|
||||||
|
begin
|
||||||
|
Disconnect;
|
||||||
|
inherited;
|
||||||
|
end;
|
||||||
|
|
||||||
|
function TModbusRTU.IsConnected: Boolean;
|
||||||
|
begin
|
||||||
|
Result := FHandle <> INVALID_HANDLE;
|
||||||
|
end;
|
||||||
|
|
||||||
|
procedure TModbusRTU.Connect;
|
||||||
|
var
|
||||||
|
DCB: TDCB;
|
||||||
|
Timeouts: TCommTimeouts;
|
||||||
|
FullName: string;
|
||||||
|
begin
|
||||||
|
if IsConnected then Exit;
|
||||||
|
|
||||||
|
// Prefisso \\.\ necessario per COM10+
|
||||||
|
FullName := '\\.\' + FPortName;
|
||||||
|
|
||||||
|
FHandle := CreateFile(PChar(FullName), GENERIC_READ or GENERIC_WRITE,
|
||||||
|
0, nil, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, 0);
|
||||||
|
|
||||||
|
if FHandle = INVALID_HANDLE then
|
||||||
|
raise TModbusException.CreateFmt('Impossibile aprire %s (errore %d)', [FPortName, GetLastError]);
|
||||||
|
|
||||||
|
FillChar(DCB, SizeOf(DCB), 0);
|
||||||
|
DCB.DCBlength := SizeOf(DCB);
|
||||||
|
if not GetCommState(FHandle, DCB) then
|
||||||
|
raise TModbusException.Create('GetCommState fallita');
|
||||||
|
|
||||||
|
DCB.BaudRate := FBaudRate;
|
||||||
|
DCB.ByteSize := 8;
|
||||||
|
DCB.Parity := NOPARITY;
|
||||||
|
DCB.StopBits := ONESTOPBIT;
|
||||||
|
DCB.Flags := 0; // no flow control
|
||||||
|
|
||||||
|
if not SetCommState(FHandle, DCB) then
|
||||||
|
raise TModbusException.Create('SetCommState fallita: parametri seriali non validi');
|
||||||
|
|
||||||
|
Timeouts.ReadIntervalTimeout := 50;
|
||||||
|
Timeouts.ReadTotalTimeoutMultiplier := 10;
|
||||||
|
Timeouts.ReadTotalTimeoutConstant := FTimeoutMs;
|
||||||
|
Timeouts.WriteTotalTimeoutMultiplier := 10;
|
||||||
|
Timeouts.WriteTotalTimeoutConstant := FTimeoutMs;
|
||||||
|
SetCommTimeouts(FHandle, Timeouts);
|
||||||
|
|
||||||
|
PurgeComm(FHandle, PURGE_RXCLEAR or PURGE_TXCLEAR);
|
||||||
|
end;
|
||||||
|
|
||||||
|
procedure TModbusRTU.Disconnect;
|
||||||
|
begin
|
||||||
|
if IsConnected then
|
||||||
|
begin
|
||||||
|
CloseHandle(FHandle);
|
||||||
|
FHandle := INVALID_HANDLE;
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
|
||||||
|
function TModbusRTU.CalcCRC16(const ABuf: array of Byte; ALen: Integer): Word;
|
||||||
|
var
|
||||||
|
I, J: Integer;
|
||||||
|
CRC: Word;
|
||||||
|
begin
|
||||||
|
CRC := $FFFF;
|
||||||
|
for I := 0 to ALen - 1 do
|
||||||
|
begin
|
||||||
|
CRC := CRC xor ABuf[I];
|
||||||
|
for J := 0 to 7 do
|
||||||
|
begin
|
||||||
|
if (CRC and $0001) <> 0 then
|
||||||
|
CRC := (CRC shr 1) xor $A001
|
||||||
|
else
|
||||||
|
CRC := CRC shr 1;
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
Result := CRC;
|
||||||
|
end;
|
||||||
|
|
||||||
|
procedure TModbusRTU.WaitInterFrame;
|
||||||
|
var
|
||||||
|
Silenzio, Passato: UInt64;
|
||||||
|
begin
|
||||||
|
// 3,5 caratteri da 11 bit al baud in uso, mai meno di 2 ms: a 9600 sono
|
||||||
|
// circa 4 ms. Senza questa pausa le richieste partono attaccate alla
|
||||||
|
// risposta precedente, e il modulo ogni tanto non le sente: la lettura
|
||||||
|
// salta, la spia si spegne e l'allarme riparte. Costa pochi millisecondi
|
||||||
|
// per richiesta.
|
||||||
|
if FBaudRate = 0 then
|
||||||
|
Exit;
|
||||||
|
Silenzio := (35 * 11 * 1000) div (FBaudRate * 10);
|
||||||
|
if Silenzio < 2 then
|
||||||
|
Silenzio := 2;
|
||||||
|
if FLastFrame = 0 then
|
||||||
|
Exit;
|
||||||
|
Passato := GetTickCount64 - FLastFrame;
|
||||||
|
if Passato < Silenzio then
|
||||||
|
Sleep(Silenzio - Passato);
|
||||||
|
end;
|
||||||
|
|
||||||
|
function TModbusRTU.SendReceive(const ARequest: array of Byte; AExpectedLen: Integer): TBytes;
|
||||||
|
var
|
||||||
|
BytesWritten, BytesRead: DWORD;
|
||||||
|
RecvCRC: Word;
|
||||||
|
RecvBuf: TBytes;
|
||||||
|
begin
|
||||||
|
if not IsConnected then
|
||||||
|
raise TModbusException.Create('Porta non connessa: chiamare Connect prima');
|
||||||
|
|
||||||
|
WaitInterFrame;
|
||||||
|
PurgeComm(FHandle, PURGE_RXCLEAR or PURGE_TXCLEAR);
|
||||||
|
|
||||||
|
if not WriteFile(FHandle, ARequest[0], Length(ARequest), BytesWritten, nil) then
|
||||||
|
raise TModbusException.CreateFmt('Errore scrittura seriale (%d)', [GetLastError]);
|
||||||
|
if Integer(BytesWritten) <> Length(ARequest) then
|
||||||
|
raise TModbusException.Create('Scrittura seriale incompleta');
|
||||||
|
|
||||||
|
SetLength(RecvBuf, AExpectedLen);
|
||||||
|
if not ReadFile(FHandle, RecvBuf[0], AExpectedLen, BytesRead, nil) then
|
||||||
|
raise TModbusException.CreateFmt('Errore lettura seriale (%d)', [GetLastError]);
|
||||||
|
|
||||||
|
FLastFrame := GetTickCount64;
|
||||||
|
|
||||||
|
if BytesRead = 0 then
|
||||||
|
raise TModbusException.Create('Timeout: nessuna risposta dallo slave (verificare indirizzo/cablaggio)');
|
||||||
|
|
||||||
|
SetLength(RecvBuf, BytesRead);
|
||||||
|
|
||||||
|
// Il frame piu' corto possibile e' l'eccezione: indirizzo, funzione, codice, CRC.
|
||||||
|
if BytesRead < 5 then
|
||||||
|
raise TModbusException.CreateFmt('Risposta troncata: %d byte', [BytesRead]);
|
||||||
|
|
||||||
|
// La CRC ricevuta va confrontata, non solo calcolata in trasmissione. Se due
|
||||||
|
// nodi hanno lo stesso indirizzo rispondono insieme e i frame si sovrappongono:
|
||||||
|
// senza questo controllo il risultato corrotto passerebbe per un dato buono.
|
||||||
|
RecvCRC := CalcCRC16(RecvBuf, BytesRead - 2);
|
||||||
|
if (Lo(RecvCRC) <> RecvBuf[BytesRead - 2]) or (Hi(RecvCRC) <> RecvBuf[BytesRead - 1]) then
|
||||||
|
raise TModbusException.Create('CRC errata: risposta corrotta. Possibile collisione ' +
|
||||||
|
'fra due slave con lo stesso indirizzo, o disturbi sulla linea');
|
||||||
|
|
||||||
|
// Deve rispondere lo slave interrogato, non un altro.
|
||||||
|
if RecvBuf[0] <> ARequest[0] then
|
||||||
|
raise TModbusException.CreateFmt('Ha risposto lo slave %d invece del %d',
|
||||||
|
[RecvBuf[0], ARequest[0]]);
|
||||||
|
|
||||||
|
// Eccezione Modbus: function code con bit 0x80 settato.
|
||||||
|
if (RecvBuf[1] and $80) <> 0 then
|
||||||
|
raise TModbusException.CreateFmt('Eccezione Modbus dallo slave %d, codice %d',
|
||||||
|
[RecvBuf[0], RecvBuf[2]]);
|
||||||
|
|
||||||
|
Result := RecvBuf;
|
||||||
|
end;
|
||||||
|
|
||||||
|
function TModbusRTU.ReadCoils(ASlaveAddr: Byte; AStartAddr, AQuantity: Word): TArray<Boolean>;
|
||||||
|
var
|
||||||
|
Req: array[0..7] of Byte;
|
||||||
|
CRC: Word;
|
||||||
|
Resp: TBytes;
|
||||||
|
I: Integer;
|
||||||
|
ByteCount: Integer;
|
||||||
|
begin
|
||||||
|
Req[0] := ASlaveAddr;
|
||||||
|
Req[1] := $01;
|
||||||
|
Req[2] := Hi(AStartAddr); Req[3] := Lo(AStartAddr);
|
||||||
|
Req[4] := Hi(AQuantity); Req[5] := Lo(AQuantity);
|
||||||
|
CRC := CalcCRC16(Req, 6);
|
||||||
|
Req[6] := Lo(CRC); Req[7] := Hi(CRC);
|
||||||
|
|
||||||
|
ByteCount := (AQuantity + 7) div 8;
|
||||||
|
Resp := SendReceive(Req, 5 + ByteCount);
|
||||||
|
|
||||||
|
SetLength(Result, AQuantity);
|
||||||
|
for I := 0 to AQuantity - 1 do
|
||||||
|
Result[I] := (Resp[3 + (I div 8)] and (1 shl (I mod 8))) <> 0;
|
||||||
|
end;
|
||||||
|
|
||||||
|
function TModbusRTU.ReadDiscreteInputs(ASlaveAddr: Byte; AStartAddr, AQuantity: Word): TArray<Boolean>;
|
||||||
|
var
|
||||||
|
Req: array[0..7] of Byte;
|
||||||
|
CRC: Word;
|
||||||
|
Resp: TBytes;
|
||||||
|
I: Integer;
|
||||||
|
ByteCount: Integer;
|
||||||
|
begin
|
||||||
|
Req[0] := ASlaveAddr;
|
||||||
|
Req[1] := $02;
|
||||||
|
Req[2] := Hi(AStartAddr); Req[3] := Lo(AStartAddr);
|
||||||
|
Req[4] := Hi(AQuantity); Req[5] := Lo(AQuantity);
|
||||||
|
CRC := CalcCRC16(Req, 6);
|
||||||
|
Req[6] := Lo(CRC); Req[7] := Hi(CRC);
|
||||||
|
|
||||||
|
ByteCount := (AQuantity + 7) div 8;
|
||||||
|
Resp := SendReceive(Req, 5 + ByteCount);
|
||||||
|
|
||||||
|
SetLength(Result, AQuantity);
|
||||||
|
for I := 0 to AQuantity - 1 do
|
||||||
|
Result[I] := (Resp[3 + (I div 8)] and (1 shl (I mod 8))) <> 0;
|
||||||
|
end;
|
||||||
|
|
||||||
|
function TModbusRTU.ReadHoldingRegisters(ASlaveAddr: Byte; AStartAddr, AQuantity: Word): TArray<Word>;
|
||||||
|
var
|
||||||
|
Req: array[0..7] of Byte;
|
||||||
|
CRC: Word;
|
||||||
|
Resp: TBytes;
|
||||||
|
I: Integer;
|
||||||
|
begin
|
||||||
|
Req[0] := ASlaveAddr;
|
||||||
|
Req[1] := $03;
|
||||||
|
Req[2] := Hi(AStartAddr); Req[3] := Lo(AStartAddr);
|
||||||
|
Req[4] := Hi(AQuantity); Req[5] := Lo(AQuantity);
|
||||||
|
CRC := CalcCRC16(Req, 6);
|
||||||
|
Req[6] := Lo(CRC); Req[7] := Hi(CRC);
|
||||||
|
|
||||||
|
Resp := SendReceive(Req, 5 + AQuantity * 2);
|
||||||
|
|
||||||
|
SetLength(Result, AQuantity);
|
||||||
|
for I := 0 to AQuantity - 1 do
|
||||||
|
Result[I] := (Resp[3 + I * 2] shl 8) or Resp[4 + I * 2];
|
||||||
|
end;
|
||||||
|
|
||||||
|
function TModbusRTU.ReadInputRegisters(ASlaveAddr: Byte; AStartAddr, AQuantity: Word): TArray<Word>;
|
||||||
|
var
|
||||||
|
Req: array[0..7] of Byte;
|
||||||
|
CRC: Word;
|
||||||
|
Resp: TBytes;
|
||||||
|
I: Integer;
|
||||||
|
begin
|
||||||
|
Req[0] := ASlaveAddr;
|
||||||
|
Req[1] := $04;
|
||||||
|
Req[2] := Hi(AStartAddr); Req[3] := Lo(AStartAddr);
|
||||||
|
Req[4] := Hi(AQuantity); Req[5] := Lo(AQuantity);
|
||||||
|
CRC := CalcCRC16(Req, 6);
|
||||||
|
Req[6] := Lo(CRC); Req[7] := Hi(CRC);
|
||||||
|
|
||||||
|
Resp := SendReceive(Req, 5 + AQuantity * 2);
|
||||||
|
|
||||||
|
SetLength(Result, AQuantity);
|
||||||
|
for I := 0 to AQuantity - 1 do
|
||||||
|
Result[I] := (Resp[3 + I * 2] shl 8) or Resp[4 + I * 2];
|
||||||
|
end;
|
||||||
|
|
||||||
|
procedure TModbusRTU.WriteSingleCoil(ASlaveAddr: Byte; AAddr: Word; AValue: Boolean);
|
||||||
|
var
|
||||||
|
Req: array[0..7] of Byte;
|
||||||
|
CRC: Word;
|
||||||
|
ValWord: Word;
|
||||||
|
begin
|
||||||
|
if AValue then ValWord := $FF00 else ValWord := $0000;
|
||||||
|
|
||||||
|
Req[0] := ASlaveAddr;
|
||||||
|
Req[1] := $05;
|
||||||
|
Req[2] := Hi(AAddr); Req[3] := Lo(AAddr);
|
||||||
|
Req[4] := Hi(ValWord); Req[5] := Lo(ValWord);
|
||||||
|
CRC := CalcCRC16(Req, 6);
|
||||||
|
Req[6] := Lo(CRC); Req[7] := Hi(CRC);
|
||||||
|
|
||||||
|
SendReceive(Req, 8); // risposta = eco della richiesta
|
||||||
|
end;
|
||||||
|
|
||||||
|
procedure TModbusRTU.WriteSingleRegister(ASlaveAddr: Byte; AAddr, AValue: Word);
|
||||||
|
var
|
||||||
|
Req: array[0..7] of Byte;
|
||||||
|
CRC: Word;
|
||||||
|
begin
|
||||||
|
Req[0] := ASlaveAddr;
|
||||||
|
Req[1] := $06;
|
||||||
|
Req[2] := Hi(AAddr); Req[3] := Lo(AAddr);
|
||||||
|
Req[4] := Hi(AValue); Req[5] := Lo(AValue);
|
||||||
|
CRC := CalcCRC16(Req, 6);
|
||||||
|
Req[6] := Lo(CRC); Req[7] := Hi(CRC);
|
||||||
|
|
||||||
|
SendReceive(Req, 8);
|
||||||
|
end;
|
||||||
|
|
||||||
|
procedure TModbusRTU.WriteMultipleCoils(ASlaveAddr: Byte; AStartAddr: Word; const AValues: array of Boolean);
|
||||||
|
var
|
||||||
|
Req: TBytes;
|
||||||
|
ByteCount, Quantity, I: Integer;
|
||||||
|
CRC: Word;
|
||||||
|
begin
|
||||||
|
Quantity := Length(AValues);
|
||||||
|
ByteCount := (Quantity + 7) div 8;
|
||||||
|
SetLength(Req, 7 + ByteCount + 2);
|
||||||
|
|
||||||
|
Req[0] := ASlaveAddr;
|
||||||
|
Req[1] := $0F;
|
||||||
|
Req[2] := Hi(AStartAddr); Req[3] := Lo(AStartAddr);
|
||||||
|
Req[4] := Hi(Word(Quantity)); Req[5] := Lo(Word(Quantity));
|
||||||
|
Req[6] := ByteCount;
|
||||||
|
|
||||||
|
FillChar(Req[7], ByteCount, 0);
|
||||||
|
for I := 0 to Quantity - 1 do
|
||||||
|
if AValues[I] then
|
||||||
|
Req[7 + (I div 8)] := Req[7 + (I div 8)] or (1 shl (I mod 8));
|
||||||
|
|
||||||
|
CRC := CalcCRC16(Req, 7 + ByteCount);
|
||||||
|
Req[7 + ByteCount] := Lo(CRC);
|
||||||
|
Req[8 + ByteCount] := Hi(CRC);
|
||||||
|
|
||||||
|
SendReceive(Req, 8); // risposta fissa a 8 byte
|
||||||
|
end;
|
||||||
|
|
||||||
|
end.
|
||||||