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

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