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:
commit
4a01a2ec88
69 files changed
+11658
No files matched your search
@@ -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.
|
||||
Reference in new issue
Block a user