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>
This commit is contained in:
f.bittiandClaude Opus 5 committed 2026-09-22 16:56:34 +02:00
commit 4a01a2ec88
69 files changed
+11658

No files matched your search

+11
View File
@@ -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
+26
View File
@@ -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
+740
View File
@@ -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 `&#10;`, per esempio
`caption="PORT&#10;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 `&#9664;` e `&#9654;` 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.
+21
View File
@@ -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.
+89
View File
@@ -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>
Binary file not shown.
+54
View File
@@ -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>
+28
View File
@@ -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>
+19
View File
@@ -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>
Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 388 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 392 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 97 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.0 KiB

+92
View File
@@ -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>
+22
View File
@@ -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>
+5
View File
@@ -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% ====
+5
View File
@@ -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% ====
+5
View File
@@ -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% ====
+5
View File
@@ -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% ====
+128
View File
@@ -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.
+91
View File
@@ -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)
+46
View File
@@ -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)
+129
View File
@@ -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.
+122
View File
@@ -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.
Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

+41
View File
@@ -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)
+23
View File
@@ -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()
+141
View File
@@ -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.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+195
View File
@@ -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.
+972
View File
@@ -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
File diff suppressed because it is too large. Load diff
+259
View File
@@ -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.
+606
View File
@@ -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.
+687
View File
@@ -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.
File diff suppressed because it is too large. Load diff
+52
View File
@@ -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>
+48
View File
@@ -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>
+16
View File
@@ -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.
+121
View File
@@ -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>
Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 107 KiB

+304
View File
@@ -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.
+224
View File
@@ -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
+631
View File
@@ -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.
+378
View File
@@ -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.