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

+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.