2485 lines
86 KiB
Plaintext
2485 lines
86 KiB
Plaintext
unit UMain;
|
|
|
|
interface
|
|
|
|
uses
|
|
System.SysUtils, System.Types, System.UITypes, System.UIConsts, System.Classes, System.Variants, System.Math, System.IniFiles, System.IOUtils,
|
|
System.JSON, System.DateUtils,
|
|
FMX.Types, FMX.Controls, FMX.Forms, FMX.Graphics, FMX.Dialogs, FMX.Ani,
|
|
FMX.Objects, FMX.Layouts, FMX.Controls.Presentation, FMX.StdCtrls, FMX.Effects,
|
|
FMX.Edit, FMX.ListBox, FMX.Media, FMX.Pickers,
|
|
{$IFDEF MSWINDOWS}
|
|
Winapi.Windows, Winapi.ShellAPI,
|
|
{$ENDIF}
|
|
{$IFDEF ANDROID}
|
|
Androidapi.JNI.GraphicsContentViewText, Androidapi.JNI.Net, Androidapi.JNI.JavaTypes, Androidapi.Helpers, Androidapi.JNIBridge,
|
|
{$ENDIF}
|
|
{$IFDEF MACOS}
|
|
Macapi.AppKit, Macapi.Foundation, Macapi.ObjectiveC, Macapi.CocoaTypes,
|
|
Posix.Stdlib,
|
|
{$ENDIF}
|
|
{$IFDEF LINUX}
|
|
Posix.Stdlib,
|
|
{$ENDIF}
|
|
UMQTTClient, UAutoControl, FMX.ExtCtrls, System.ImageList, FMX.ImgList,
|
|
FMX.Memo.Types, FMX.ScrollBox, FMX.Memo, FMX.DateTimeCtrls, FMX.EditBox;
|
|
|
|
type
|
|
TNodeConfig = record
|
|
ID: string; // e.g. 'TM_00'
|
|
NodeType: string; // e.g. 'TM'
|
|
Index: Integer; // e.g. 0
|
|
TitleKO: string;
|
|
TitleEN: string;
|
|
CustomName: string;
|
|
InUse: Boolean;
|
|
SettingRect: TRectangle;
|
|
SettingSwitch: TSwitch;
|
|
SettingEdit: TEdit;
|
|
CustomIcon: string;
|
|
SettingIconCombo: TComboBox;
|
|
end;
|
|
|
|
TSensorWidget = class
|
|
Background: TRectangle;
|
|
IconLabel: TLabel;
|
|
TitleLabel: TLabel;
|
|
ValueLabel: TLabel;
|
|
UnitLabel: TLabel;
|
|
ProgressBarBg: TRectangle;
|
|
ProgressBarFill: TRectangle;
|
|
TitleKO, TitleEN: string;
|
|
CustomName: string;
|
|
NodeID: string;
|
|
MaxValue: Double;
|
|
procedure UpdateValue(Value: Double);
|
|
end;
|
|
|
|
TControlWidget = class
|
|
Background: TRectangle;
|
|
IconLabel: TLabel;
|
|
TitleLabel: TLabel;
|
|
StatusLabel: TLabel;
|
|
ToggleSwitch: TSwitch;
|
|
TitleKO, TitleEN: string;
|
|
CustomName: string;
|
|
NodeID: string;
|
|
procedure UpdateStatus(IsOn: Boolean);
|
|
end;
|
|
|
|
TfrmMain = class(TForm)
|
|
TimerTime: TTimer;
|
|
MediaPlayer: TMediaPlayer;
|
|
bgRect: TRectangle;
|
|
lytSidebar: TRectangle;
|
|
lytSidebarTop: TLayout;
|
|
SidebarIcon: TLabel;
|
|
lblAppTitle: TLabel;
|
|
lytNavMenu: TLayout;
|
|
btnNavDashboard: TRectangle;
|
|
lblNavDashboardIcon: TLabel;
|
|
lblNavDashboardText: TLabel;
|
|
btnNavNodeSettings: TRectangle;
|
|
lblNavNodeSettingsIcon: TLabel;
|
|
lblNavNodeSettingsText: TLabel;
|
|
btnNavCCTV: TRectangle;
|
|
lblNavCCTVIcon: TLabel;
|
|
lblNavCCTVText: TLabel;
|
|
btnNavAUTO: TRectangle;
|
|
lblNavAUTOIcon: TLabel;
|
|
lblNavAUTOText: TLabel;
|
|
pnlAutoModeBox: TRectangle;
|
|
lblAutoModeTitle: TLabel;
|
|
pnlAutoModeBtn: TRectangle;
|
|
lblAutoModeStatus: TLabel;
|
|
bgStatus: TRectangle;
|
|
lblStatusTitle: TLabel;
|
|
lblConnectionStatus: TLabel;
|
|
lytMainContent: TLayout;
|
|
lytHeader: TLayout;
|
|
lblHeaderTitle: TLabel;
|
|
LControls: TLayout;
|
|
lblDateTime: TLabel;
|
|
btnFullscreen: TRectangle;
|
|
lblFullscreenText: TLabel;
|
|
btnLang: TRectangle;
|
|
lblLangText: TLabel;
|
|
lytScreenDashboard: TLayout;
|
|
scrollWidgets: TVertScrollBox;
|
|
lytWidgets: TFlowLayout;
|
|
lytScreenNodeSettings: TLayout;
|
|
scrollNodeSettings: TVertScrollBox;
|
|
lytNodesMain: TFlowLayout;
|
|
lytScreenSettings: TLayout;
|
|
lblSettingsTitle: TLabel;
|
|
edtRTSPUrl: TEdit;
|
|
btnCameraAdd: TRectangle;
|
|
lblCameraAddText: TLabel;
|
|
lstCameras: TListBox;
|
|
btnCameraDel: TRectangle;
|
|
lblCameraDelText: TLabel;
|
|
lytScreenCCTV: TLayout;
|
|
lytScreenAuto: TLayout;
|
|
scrollAuto: TVertScrollBox;
|
|
btnAutoRuleBar: TLayout;
|
|
MediaControl: TMediaPlayerControl;
|
|
lytCCTVContainer: TFlowLayout;
|
|
rectModalOverlay: TRectangle;
|
|
rectModalContent: TRectangle;
|
|
ShadowEffect1: TShadowEffect;
|
|
lblModalIcon: TLabel;
|
|
lblModalTitle: TLabel;
|
|
lblModalMessage: TLabel;
|
|
btnModalClose: TRectangle;
|
|
lblModalCloseText: TLabel;
|
|
TimerAuto: TTimer;
|
|
ScaledLayout1: TScaledLayout;
|
|
TimerStartAutoCK: TTimer;
|
|
ImageList1: TImageList;
|
|
Glyph1: TGlyph;
|
|
btnNavIrri: TRectangle;
|
|
lblNavIrriIcon: TLabel;
|
|
lblNavIrriText: TLabel;
|
|
btnNavLogs: TRectangle;
|
|
lblNavLogsIcon: TLabel;
|
|
lblNavLogsText: TLabel;
|
|
btnNavSettings: TRectangle;
|
|
lblNavSettingsIcon: TLabel;
|
|
lblNavSettingsText: TLabel;
|
|
Label1: TLabel;
|
|
Label2: TLabel;
|
|
Label3: TLabel;
|
|
procedure FormCreate(Sender: TObject);
|
|
procedure FormDestroy(Sender: TObject);
|
|
procedure FormResize(Sender: TObject);
|
|
procedure TimerTimeTimer(Sender: TObject);
|
|
procedure OnLangClick(Sender: TObject);
|
|
procedure OnFullscreenClick(Sender: TObject);
|
|
procedure OnModalCloseClick(Sender: TObject);
|
|
procedure OnToggleChange(Sender: TObject);
|
|
procedure OnNodeNameExit(Sender: TObject);
|
|
procedure OnNavClick(Sender: TObject);
|
|
procedure OnAddCameraClick(Sender: TObject);
|
|
procedure OnDelCameraClick(Sender: TObject);
|
|
procedure OnPlayCameraClick(Sender: TObject);
|
|
procedure OnShowPathClick(Sender: TObject);
|
|
// Auto Control (UAutoControl_Impl.inc) mapped events MUST be published (before private)
|
|
procedure OnAutoModeToggle(Sender: TObject);
|
|
procedure TimerAutoTimer(Sender: TObject);
|
|
procedure OnAutoRuleAdd(Sender: TObject);
|
|
procedure OnAutoRuleDel(Sender: TObject);
|
|
procedure OnAutoCondAdd(Sender: TObject);
|
|
procedure OnAutoCondDel(Sender: TObject);
|
|
procedure OnAutoRuleEdit(Sender: TObject);
|
|
procedure OnAutoRuleDelSelected(Sender: TObject);
|
|
procedure OnAutoRuleCheck(Sender: TObject);
|
|
procedure TimerStartAutoCKTimer(Sender: TObject);
|
|
procedure bgStatusDblClick(Sender: TObject);
|
|
|
|
private
|
|
FMQTTGateWayID, FMQTTBrokerIP, FMQTTUserID, FMQTTUserPASS : string;
|
|
FMQTTBrokerPORT : Integer;
|
|
FMQTTTopicHEAD, FMQTTTopicTAIL : string;
|
|
|
|
FIsKorean: Boolean;
|
|
FNodeConfigs: TArray<TNodeConfig>;
|
|
Sensors: TArray<TSensorWidget>;
|
|
Controls: TArray<TControlWidget>;
|
|
FRTSPApps: TStringList; // Store RTSP app package names
|
|
FMQTTClient: TMQTTClient;
|
|
FControlPending: array[0..20] of Boolean; // Increased for DO_00..DO_15
|
|
FControlDesired: array[0..20] of Boolean;
|
|
FUpdatingControls: Boolean; // MQTT 업데이트 시 OnToggleChange 재귀 방지
|
|
FMQTTMessageRE: Boolean; // 초기 실행시 자동실행을 위한 MQTT(실시간 게이트웨이 현황) 적용 체크
|
|
|
|
FAutoRules: TArray<TAutoRule>;
|
|
FAutoMode: Boolean;
|
|
FRefreshingAutoUI: Boolean;
|
|
|
|
FIrriSchedules: TArray<TIrrigationSchedule>;
|
|
FSensorValues: array[0..99] of Double;
|
|
FSensorValuesTotalFlow: array[0..99] of Double;
|
|
btnShowPath: TRectangle;
|
|
|
|
lytScreenLogs: TLayout;
|
|
memoLogs: TMemo;
|
|
dateLog: TDateEdit;
|
|
lblLogDate: TLabel;
|
|
btnRefreshLogs: TRectangle;
|
|
|
|
lytScreenIrri: TLayout;
|
|
lytIrriDashboard: TLayout;
|
|
lytIrriSettings: TLayout;
|
|
scrlIrriSettings: TScrollBox;
|
|
|
|
lytIrriActionPopup: TLayout;
|
|
scrlIrriAction: TScrollBox;
|
|
lblIrriActionTitle: TLabel;
|
|
FCurrentEditIrriSchedIdx: Integer;
|
|
|
|
lytMQTTSetupPopup: TLayout;
|
|
scrMQTTSetup: TScrollBox;
|
|
lblMQTTSetupTitle: TLabel;
|
|
|
|
procedure BuildLogUI;
|
|
procedure OnRefreshLogsClick(Sender: TObject);
|
|
procedure AddSystemLog(const AMessage: string);
|
|
procedure BuildIrriUI;
|
|
procedure BuildIrriActionPopupUI;
|
|
procedure OnIrriNavClick(Sender: TObject);
|
|
|
|
procedure BuildMQTTSetupPopupUI;
|
|
procedure OnMQTTSetupEdit(Sender: TObject);
|
|
procedure OnMQTTSetupSaveClose(Sender: TObject);
|
|
|
|
procedure InitNodeConfigs;
|
|
procedure BuildNodeSettingsUI;
|
|
procedure RebuildDashboardUI;
|
|
procedure OnNodeSettingToggle(Sender: TObject);
|
|
procedure OnNodeIconChange(Sender: TObject);
|
|
|
|
function BuildLabel(AParent: TFmxObject; AText: string; ASize: Single; AAlign: TAlignLayout; AColor: TAlphaColor; IsBold: Boolean = False): TLabel;
|
|
function BuildDateEdit(AParent: TFmxObject; ADate: TDate; ASize: Single; AAlign: TAlignLayout; AColor: TAlphaColor; IsBold: Boolean = False): TDateEdit;
|
|
function BuildWidgetBg(AParent: TFmxObject): TRectangle;
|
|
function CreateCustomButton(AParent: TFmxObject; AText: string; AAlign: TAlignLayout; AWidth: Single; Event: TNotifyEvent): TRectangle;
|
|
|
|
function AddSensor(const AConfig: TNodeConfig; AIcon, AUnit: string; AMax: Double): TSensorWidget;
|
|
function AddControl(const AConfig: TNodeConfig; AIcon: string): TControlWidget;
|
|
|
|
procedure ApplyLanguage;
|
|
procedure LoadSettings;
|
|
procedure BuildCCTVGrid;
|
|
procedure SaveSettings;
|
|
procedure SaveSettings_MQTT;
|
|
procedure SaveSetting_each(ck,_section, _ident, _Value: string);
|
|
|
|
procedure SwitchScreen(Index: Integer);
|
|
procedure ShowCustomAlert(const ATitleKO, ATitleEN, AMessageKO, AMessageEN: string);
|
|
function GetGroupName(const AType: string; IsKorean: Boolean): string;
|
|
|
|
procedure UpdateAutoModeBtnUI;
|
|
procedure BuildAutoUI;
|
|
procedure RefreshAutoUI;
|
|
procedure LoadAutoRules;
|
|
procedure SaveAutoRules;
|
|
function GetCurrentSensorValue(const ANodeID: string): Double;
|
|
function GetCurrentSensorValueTotalFlow(const ANodeID: string): Double;
|
|
procedure EvalIrriFlowInit(const AConds: array of TAutoCondItem);
|
|
procedure EvalIrriFlowInitTotalFlow(const AConds: array of TAutoCondItem);
|
|
function EvalOneCondition(const ACond: TAutoCondItem): Boolean;
|
|
function EvalOneConditionTotalFlow(const ACond: TAutoCondItem): Boolean;
|
|
function EvalAutoRule(var ARule: TAutoRule): Boolean;
|
|
procedure CheckAutoRules;
|
|
|
|
// Irrigation Schedule (UIrrigation_Impl.inc)
|
|
procedure RefreshIrriUI;
|
|
procedure LoadIrriSchedules;
|
|
procedure SaveIrriSchedules;
|
|
procedure CheckIrrigationSchedules;
|
|
procedure UpdateIrriDashboardUI;
|
|
|
|
procedure OnIrriScheduleAdd(Sender: TObject);
|
|
procedure OnIrriScheduleDel(Sender: TObject);
|
|
procedure OnIrriScheduleDelSelected(Sender: TObject);
|
|
procedure OnIrriScheduleCheck(Sender: TObject);
|
|
procedure OnIrriScheduleEdit(Sender: TObject);
|
|
|
|
procedure RefreshIrriActionPopupUI(ASchedIdx: Integer);
|
|
procedure OnIrriActionPopupClose(Sender: TObject);
|
|
procedure OnIrriActionManageClick(Sender: TObject);
|
|
procedure OnIrriActionCheck(Sender: TObject);
|
|
procedure OnIrriActionDelSelected(Sender: TObject);
|
|
procedure OnIrriActionEdit(Sender: TObject);
|
|
|
|
procedure OnIrriActionAdd(Sender: TObject);
|
|
procedure OnIrriActionDel(Sender: TObject);
|
|
procedure OnIrriCondAdd(Sender: TObject);
|
|
procedure OnIrriCondDel(Sender: TObject);
|
|
procedure OnIrriCondEdit(Sender: TObject);
|
|
|
|
procedure OnIrriManualStartStop(Sender: TObject);
|
|
|
|
procedure OnMQTTMessage(const ATopic, APayload: string);
|
|
procedure OnMQTTStatus(AConnected: Boolean);
|
|
function ParsePipeValue(const AData: string; AIndex: Integer): Double;
|
|
procedure PublishControl(AIndex: Integer; AOn: Boolean);
|
|
function GetIconByIndex(AIndex: Integer): string;
|
|
function GetIconIndex(const AIcon: string): Integer;
|
|
public
|
|
end;
|
|
|
|
var
|
|
frmMain: TfrmMain;
|
|
|
|
implementation
|
|
|
|
uses System.Generics.Collections;
|
|
|
|
{$R *.fmx}
|
|
|
|
{$I UAutoControl_Impl.inc}
|
|
{$I UIrrigation_Impl.inc}
|
|
|
|
{ TSensorWidget }
|
|
|
|
procedure TSensorWidget.UpdateValue(Value: Double);
|
|
var
|
|
TargetWidth: Single;
|
|
begin
|
|
if ValueLabel <> nil then
|
|
ValueLabel.Text := Format('%.1f', [Value]);
|
|
|
|
if (ProgressBarBg <> nil) and (ProgressBarFill <> nil) then
|
|
begin
|
|
TargetWidth := (Value / MaxValue) * ProgressBarBg.Width;
|
|
if TargetWidth > ProgressBarBg.Width then TargetWidth := ProgressBarBg.Width;
|
|
TAnimator.AnimateFloat(ProgressBarFill, 'Width', TargetWidth, 0.3);
|
|
end;
|
|
end;
|
|
|
|
{ TControlWidget }
|
|
|
|
procedure TControlWidget.UpdateStatus(IsOn: Boolean);
|
|
begin
|
|
if ToggleSwitch <> nil then
|
|
ToggleSwitch.IsChecked := IsOn;
|
|
|
|
if StatusLabel <> nil then
|
|
begin
|
|
if IsOn then
|
|
begin
|
|
if frmMain.FIsKorean then StatusLabel.Text := '켜짐' else StatusLabel.Text := 'ON';
|
|
StatusLabel.TextSettings.FontColor := $FF4CAF50;
|
|
end
|
|
else
|
|
begin
|
|
if frmMain.FIsKorean then StatusLabel.Text := '꺼짐' else StatusLabel.Text := 'OFF';
|
|
StatusLabel.TextSettings.FontColor := $FFFF5252;
|
|
end;
|
|
end;
|
|
end;
|
|
|
|
{ TfrmMain }
|
|
|
|
procedure TfrmMain.FormCreate(Sender: TObject);
|
|
var
|
|
rectEditBg: TRectangle;
|
|
begin
|
|
FMQTTMessageRE := False;
|
|
FIsKorean := True;
|
|
// Self.FullScreen := True;
|
|
TimerTime.Interval := 1000;
|
|
|
|
FRTSPApps := TStringList.Create;
|
|
|
|
InitNodeConfigs;
|
|
LoadSettings;
|
|
LoadAutoRules; // AUTO LOAD
|
|
LoadIrriSchedules; // IRRI LOAD
|
|
BuildNodeSettingsUI;
|
|
RebuildDashboardUI;
|
|
BuildAutoUI; // AUTO BUILD
|
|
BuildLogUI; // LOG UI
|
|
BuildIrriUI; // IRRI UI
|
|
BuildIrriActionPopupUI;
|
|
ApplyLanguage;
|
|
BuildMQTTSetupPopupUI;
|
|
|
|
// edtRTSPUrl 에디트 박스 배경을 흰색으로 변경하고 폰트를 검은색으로 설정
|
|
rectEditBg := TRectangle.Create(Self);
|
|
rectEditBg.Parent := edtRTSPUrl.Parent;
|
|
rectEditBg.Align := edtRTSPUrl.Align;
|
|
rectEditBg.Margins := edtRTSPUrl.Margins;
|
|
rectEditBg.Position := edtRTSPUrl.Position;
|
|
rectEditBg.Size := edtRTSPUrl.Size;
|
|
rectEditBg.Fill.Color := claWhite;
|
|
rectEditBg.Stroke.Kind := TBrushKind.None;
|
|
rectEditBg.XRadius := 8;
|
|
rectEditBg.YRadius := 8;
|
|
|
|
edtRTSPUrl.Align := TAlignLayout.Client;
|
|
edtRTSPUrl.Margins.Rect := RectF(15, 0, 15, 0); // 좌우 여백 추가
|
|
edtRTSPUrl.Parent := rectEditBg;
|
|
{$IFNDEF ANDROID}
|
|
edtRTSPUrl.StyleLookup := 'transparentedit';
|
|
{$ENDIF}
|
|
edtRTSPUrl.TextSettings.FontColor := claBlack;
|
|
edtRTSPUrl.StyledSettings := edtRTSPUrl.StyledSettings - [TStyledSetting.FontColor];
|
|
|
|
{$IFDEF MACOS}
|
|
// macOS: Force the application to accept first mouse clicks immediately.
|
|
TNSApplication.Wrap(TNSApplication.OCClass.sharedApplication).activateIgnoringOtherApps(True);
|
|
{$ENDIF}
|
|
|
|
// Timer enable
|
|
TimerTime.Enabled := True;
|
|
|
|
// MQTT 클라이언트 초기화 및 연결
|
|
FMQTTClient := TMQTTClient.Create(FMQTTBrokerIP, FMQTTBrokerPORT, 'SmartFarmHMI');
|
|
FMQTTClient.OnMessage := OnMQTTMessage;
|
|
FMQTTClient.OnStatus := OnMQTTStatus;
|
|
FMQTTClient.Subscribe(FMQTTTopicHEAD+'PUB/'+FMQTTTopicTAIL+FMQTTGateWayID);
|
|
FMQTTClient.Connect;
|
|
|
|
// 파일 경로 표시 버튼 생성
|
|
btnShowPath := CreateCustomButton(lytScreenSettings, '저장 경로 확인', TAlignLayout.Bottom, 250, OnShowPathClick);
|
|
btnShowPath.Height := 60;
|
|
btnShowPath.Margins.Rect := RectF(40, 0, 20, 10);
|
|
|
|
Self.FullScreen := True;
|
|
end;
|
|
|
|
procedure TfrmMain.FormDestroy(Sender: TObject);
|
|
begin
|
|
if FMQTTClient <> nil then
|
|
begin
|
|
FMQTTClient.Disconnect;
|
|
FreeAndNil(FMQTTClient);
|
|
end;
|
|
if Assigned(FRTSPApps) then
|
|
FreeAndNil(FRTSPApps);
|
|
end;
|
|
|
|
procedure TfrmMain.FormResize(Sender: TObject);
|
|
var
|
|
ItemW, AvailW: Single;
|
|
SidebarW: Single;
|
|
i: Integer;
|
|
Cols, Rows, TotalItems: Integer;
|
|
begin
|
|
SidebarW := 350; // default
|
|
// Apply sidebar width first so scrollWidgets.Width reflects the correct available area
|
|
if lytSidebar <> nil then
|
|
begin
|
|
if ClientWidth < 1000 then
|
|
SidebarW := 250
|
|
else
|
|
SidebarW := 350;
|
|
lytSidebar.Width := SidebarW;
|
|
end;
|
|
|
|
if (lytWidgets <> nil) and (scrollWidgets <> nil) then
|
|
begin
|
|
// Calculate available width from ClientWidth directly (avoids stale layout values)
|
|
AvailW := ClientWidth - SidebarW - 80;
|
|
if AvailW < 1 then AvailW := 1;
|
|
// Each item slot = widget width (440) + FlowLayout HorizontalGap (40)
|
|
ItemW := 440 + 40;
|
|
Cols := Trunc(AvailW / ItemW);
|
|
if Cols < 1 then Cols := 1;
|
|
TotalItems := Length(Sensors) + Length(Controls);
|
|
if TotalItems = 0 then TotalItems := 9; // Fallback during initial render
|
|
Rows := Ceil(TotalItems / Cols);
|
|
// Row height = widget height (270) + top margin (10) + bottom margin (10) + VerticalGap (20) = 310px
|
|
// Add extra 80px bottom padding so the last row is never cut off by the scroll box
|
|
lytWidgets.Height := Rows * 220 + 200;
|
|
end;
|
|
|
|
if (lytNodesMain <> nil) and (scrollNodeSettings <> nil) then
|
|
begin
|
|
{$IFNDEF ANDROID}
|
|
AvailW := ClientWidth - SidebarW - 40; // Approx 40px for scrollbar/margins
|
|
if AvailW < 1 then AvailW := 1;
|
|
|
|
ItemW := 700; // Base card width
|
|
if AvailW < ItemW then
|
|
ItemW := AvailW - 20;
|
|
|
|
Cols := Trunc(AvailW / (ItemW + 20)); // +20 for FlowLayout HorizontalGap
|
|
if Cols < 1 then Cols := 1;
|
|
|
|
Rows := 0;
|
|
TotalItems := 0;
|
|
|
|
for i := 0 to lytNodesMain.ControlsCount - 1 do
|
|
begin
|
|
if lytNodesMain.Controls[i].Tag = -2 then
|
|
begin
|
|
// TFlowLayoutBreak: It means the current line ended.
|
|
if TotalItems > 0 then
|
|
begin
|
|
Inc(Rows);
|
|
TotalItems := 0;
|
|
end;
|
|
end
|
|
else if lytNodesMain.Controls[i].Tag = -1 then
|
|
begin
|
|
// Header
|
|
lytNodesMain.Controls[i].Width := lytNodesMain.Width; // Span entire FlowLayout width
|
|
if TotalItems > 0 then
|
|
begin
|
|
Inc(Rows);
|
|
TotalItems := 0;
|
|
end;
|
|
Inc(Rows); // The header itself counts as one row
|
|
end
|
|
else
|
|
begin
|
|
// Normal card item
|
|
lytNodesMain.Controls[i].Width := ItemW; // Ensure card dynamically fits!
|
|
|
|
Inc(TotalItems);
|
|
if TotalItems >= Cols then
|
|
begin
|
|
Inc(Rows);
|
|
TotalItems := 0;
|
|
end;
|
|
end;
|
|
end;
|
|
if TotalItems > 0 then Inc(Rows);
|
|
|
|
lytNodesMain.Height := Rows * 100 + 200;
|
|
{$ENDIF}
|
|
end;
|
|
|
|
// Wait, if Android was skipped, how does lytNodesMain get its height for scrolling?
|
|
// We MUST calculate height for Android too, or else the ScrollBox has no content height to scroll!
|
|
if (lytNodesMain <> nil) and (scrollNodeSettings <> nil) then
|
|
begin
|
|
{$IFDEF ANDROID}
|
|
// Calculate how many rows we have assuming 2 items per row
|
|
TotalItems := 0;
|
|
Rows := 0;
|
|
for i := 0 to lytNodesMain.ControlsCount - 1 do
|
|
begin
|
|
if lytNodesMain.Controls[i].Tag = -1 then // Header
|
|
begin
|
|
if TotalItems > 0 then
|
|
begin
|
|
Inc(Rows);
|
|
TotalItems := 0;
|
|
end;
|
|
Inc(Rows);
|
|
end
|
|
else if lytNodesMain.Controls[i].Tag <> -2 then // Normal item
|
|
begin
|
|
Inc(TotalItems);
|
|
if TotalItems >= 2 then
|
|
begin
|
|
Inc(Rows);
|
|
TotalItems := 0;
|
|
end;
|
|
end;
|
|
end;
|
|
if TotalItems > 0 then Inc(Rows);
|
|
|
|
// Each row is ItemLyt.Height (100) + VerticalGap (maybe 20).
|
|
lytNodesMain.Height := Rows * 120 + 80;
|
|
{$ENDIF}
|
|
end;
|
|
end;
|
|
|
|
function TfrmMain.BuildLabel(AParent: TFmxObject; AText: string; ASize: Single; AAlign: TAlignLayout; AColor: TAlphaColor; IsBold: Boolean): TLabel;
|
|
begin
|
|
Result := TLabel.Create(Self);
|
|
Result.Parent := AParent;
|
|
Result.Align := AAlign;
|
|
Result.Text := AText;
|
|
|
|
// Disable default styling to apply custom font size and color
|
|
Result.StyledSettings := Result.StyledSettings - [TStyledSetting.Size, TStyledSetting.FontColor, TStyledSetting.Style];
|
|
|
|
Result.TextSettings.Font.Size := ASize;
|
|
if IsBold then
|
|
Result.TextSettings.Font.Style := [TFontStyle.fsBold]
|
|
else
|
|
Result.TextSettings.Font.Style := [];
|
|
|
|
Result.TextSettings.FontColor := AColor;
|
|
Result.WordWrap := False;
|
|
end;
|
|
|
|
function TfrmMain.BuildDateEdit(AParent: TFmxObject; ADate: TDate; ASize: Single; AAlign: TAlignLayout; AColor: TAlphaColor; IsBold: Boolean): TDateEdit;
|
|
begin
|
|
Result := TDateEdit.Create(Self);
|
|
Result.Parent := AParent;
|
|
Result.Align := AAlign;
|
|
Result.Date := ADate;
|
|
|
|
// Disable default styling to apply custom font size and color
|
|
Result.StyledSettings := Result.StyledSettings - [TStyledSetting.Size, TStyledSetting.FontColor, TStyledSetting.Style];
|
|
|
|
Result.TextSettings.Font.Size := ASize;
|
|
if IsBold then
|
|
Result.TextSettings.Font.Style := [TFontStyle.fsBold]
|
|
else
|
|
Result.TextSettings.Font.Style := [];
|
|
|
|
Result.TextSettings.FontColor := AColor;
|
|
Result.WordWrap := False;
|
|
end;
|
|
|
|
function TfrmMain.BuildWidgetBg(AParent: TFmxObject): TRectangle;
|
|
begin
|
|
Result := TRectangle.Create(Self);
|
|
Result.Parent := AParent;
|
|
Result.Fill.Color := $20FFFFFF;
|
|
Result.Stroke.Color := $30FFFFFF;
|
|
Result.XRadius := 16;
|
|
Result.YRadius := 16;
|
|
Result.Margins.Rect := RectF(10, 10, 10, 10);
|
|
end;
|
|
|
|
function TfrmMain.CreateCustomButton(AParent: TFmxObject; AText: string; AAlign: TAlignLayout; AWidth: Single; Event: TNotifyEvent): TRectangle;
|
|
var
|
|
L: TLabel;
|
|
begin
|
|
Result := TRectangle.Create(Self);
|
|
Result.Parent := AParent;
|
|
Result.Align := AAlign;
|
|
Result.Width := AWidth;
|
|
Result.Fill.Color := $40FFFFFF;
|
|
Result.Stroke.Color := $40FFFFFF;
|
|
Result.XRadius := 8;
|
|
Result.YRadius := 8;
|
|
Result.Cursor := crHandPoint;
|
|
Result.HitTest := True;
|
|
Result.OnClick := Event;
|
|
Result.Margins.Rect := RectF(10, 10, 10, 10);
|
|
|
|
L := TLabel.Create(Self);
|
|
L.Parent := Result;
|
|
L.Align := TAlignLayout.Client;
|
|
|
|
L.StyledSettings := L.StyledSettings - [TStyledSetting.Size, TStyledSetting.FontColor, TStyledSetting.Style];
|
|
|
|
L.TextSettings.HorzAlign := TTextAlign.Center;
|
|
L.TextSettings.VertAlign := TTextAlign.Center;
|
|
L.TextSettings.Font.Size := 24;
|
|
L.TextSettings.Font.Style := [TFontStyle.fsBold];
|
|
L.TextSettings.FontColor := claWhite;
|
|
L.HitTest := False;
|
|
L.Text := AText;
|
|
end;
|
|
|
|
function TfrmMain.GetGroupName(const AType: string; IsKorean: Boolean): string;
|
|
begin
|
|
if AType = 'DI' then
|
|
begin
|
|
if IsKorean then Result := '디지털 입력' else Result := 'Digital In';
|
|
end
|
|
else if AType = 'DO' then
|
|
begin
|
|
if IsKorean then Result := '디지털 출력' else Result := 'Digital Out';
|
|
end
|
|
else if AType = 'AI' then
|
|
begin
|
|
if IsKorean then Result := '아날로그 입력' else Result := 'Analog In';
|
|
end
|
|
else if AType = 'HM' then
|
|
begin
|
|
if IsKorean then Result := '습도 센서' else Result := 'Humidity Sensor';
|
|
end
|
|
else if AType = 'TM' then
|
|
begin
|
|
if IsKorean then Result := '온도 센서' else Result := 'Temp Sensor';
|
|
end
|
|
else if AType = 'CD' then
|
|
begin
|
|
if IsKorean then Result := 'CO2 센서' else Result := 'CO2 Sensor';
|
|
end
|
|
else if AType = 'NT' then
|
|
begin
|
|
if IsKorean then Result := 'EC 센서' else Result := 'EC Sensor';
|
|
end
|
|
else if AType = 'PH' then
|
|
begin
|
|
if IsKorean then Result := 'pH 센서' else Result := 'pH Sensor';
|
|
end
|
|
else if AType = 'GT' then
|
|
begin
|
|
if IsKorean then Result := '토양 온도 1' else Result := 'Soil Temp 1';
|
|
end
|
|
else if AType = 'GM' then
|
|
begin
|
|
if IsKorean then Result := '토양 습도' else Result := 'Soil Moisture';
|
|
end
|
|
else if AType = 'GNT' then
|
|
begin
|
|
if IsKorean then Result := '토양 EC' else Result := 'Soil EC';
|
|
end
|
|
else if AType = 'WP' then
|
|
begin
|
|
if IsKorean then Result := '토양 pH' else Result := 'Soil pH';
|
|
end
|
|
else if AType = 'GT2' then
|
|
begin
|
|
if IsKorean then Result := '토양 온도 2' else Result := 'Soil Temp 2';
|
|
end
|
|
else
|
|
begin
|
|
if IsKorean then Result := '기타' else Result := 'Other';
|
|
end;
|
|
end;
|
|
|
|
|
|
|
|
function TfrmMain.AddSensor(const AConfig: TNodeConfig; AIcon, AUnit: string; AMax: Double): TSensorWidget;
|
|
var
|
|
W: TSensorWidget;
|
|
TopLyt, ValLyt: TLayout;
|
|
begin
|
|
W := TSensorWidget.Create;
|
|
W.TitleKO := AConfig.TitleKO;
|
|
W.TitleEN := AConfig.TitleEN;
|
|
W.CustomName := AConfig.CustomName; // Assign custom name
|
|
W.NodeID := AConfig.ID;
|
|
W.MaxValue := AMax;
|
|
|
|
W.Background := BuildWidgetBg(lytWidgets);
|
|
W.Background.Width := 440;
|
|
// W.Background.Height := 270;
|
|
W.Background.Height := 200;
|
|
|
|
TopLyt := TLayout.Create(W.Background);
|
|
TopLyt.Parent := W.Background;
|
|
TopLyt.Align := TAlignLayout.Top;
|
|
TopLyt.Height := 75;
|
|
TopLyt.Margins.Rect := RectF(15, 15, 15, 0);
|
|
|
|
W.IconLabel := BuildLabel(TopLyt, AIcon, 60, TAlignLayout.Left, claWhite, False);
|
|
W.IconLabel.Width := 90;
|
|
|
|
W.TitleLabel := BuildLabel(TopLyt, '', 36, TAlignLayout.Client, claWhite, True);
|
|
W.TitleLabel.TextSettings.HorzAlign := TTextAlign.Leading;
|
|
|
|
ValLyt := TLayout.Create(W.Background);
|
|
ValLyt.Parent := W.Background;
|
|
ValLyt.Align := TAlignLayout.Client;
|
|
ValLyt.Margins.Rect := RectF(15, 15, 15, 15);
|
|
|
|
// W.ValueLabel := BuildLabel(ValLyt, '--', 72, TAlignLayout.Client, claWhite, True);
|
|
W.ValueLabel := BuildLabel(ValLyt, '--', 56, TAlignLayout.Client, claWhite, True);
|
|
W.ValueLabel.TextSettings.HorzAlign := TTextAlign.Leading;
|
|
|
|
W.UnitLabel := BuildLabel(ValLyt, AUnit, 36, TAlignLayout.Right, claLightgray, False);
|
|
W.UnitLabel.Width := 90;
|
|
|
|
W.ProgressBarBg := TRectangle.Create(W.Background);
|
|
W.ProgressBarBg.Parent := W.Background;
|
|
W.ProgressBarBg.Align := TAlignLayout.Bottom;
|
|
W.ProgressBarBg.Height := 15;
|
|
W.ProgressBarBg.Margins.Rect := RectF(15, 0, 15, 15);
|
|
W.ProgressBarBg.Fill.Color := $40FFFFFF;
|
|
W.ProgressBarBg.Stroke.Kind := TBrushKind.None;
|
|
W.ProgressBarBg.XRadius := 7.5;
|
|
W.ProgressBarBg.YRadius := 7.5;
|
|
|
|
W.ProgressBarFill := TRectangle.Create(W.ProgressBarBg);
|
|
W.ProgressBarFill.Parent := W.ProgressBarBg;
|
|
W.ProgressBarFill.Align := TAlignLayout.Left;
|
|
W.ProgressBarFill.Width := 0;
|
|
W.ProgressBarFill.Fill.Color := $FF4CAF50;
|
|
W.ProgressBarFill.Stroke.Kind := TBrushKind.None;
|
|
W.ProgressBarFill.XRadius := 7.5;
|
|
W.ProgressBarFill.YRadius := 7.5;
|
|
|
|
SetLength(Sensors, Length(Sensors) + 1);
|
|
Sensors[High(Sensors)] := W;
|
|
Result := W;
|
|
end;
|
|
|
|
function TfrmMain.AddControl(const AConfig: TNodeConfig; AIcon: string): TControlWidget;
|
|
var
|
|
W: TControlWidget;
|
|
TopLyt, ValLyt: TLayout;
|
|
begin
|
|
W := TControlWidget.Create;
|
|
W.TitleKO := AConfig.TitleKO;
|
|
W.TitleEN := AConfig.TitleEN;
|
|
W.CustomName := AConfig.CustomName; // Assign custom name
|
|
W.NodeID := AConfig.ID;
|
|
|
|
W.Background := BuildWidgetBg(lytWidgets);
|
|
W.Background.Width := 440;
|
|
// W.Background.Height := 270;
|
|
W.Background.Height := 200;
|
|
|
|
TopLyt := TLayout.Create(W.Background);
|
|
TopLyt.Parent := W.Background;
|
|
TopLyt.Align := TAlignLayout.Top;
|
|
TopLyt.Height := 75;
|
|
TopLyt.Margins.Rect := RectF(15, 15, 15, 0);
|
|
|
|
W.IconLabel := BuildLabel(TopLyt, AIcon, 60, TAlignLayout.Left, claWhite, False);
|
|
W.IconLabel.Width := 90;
|
|
|
|
W.TitleLabel := BuildLabel(TopLyt, '', 36, TAlignLayout.Client, claWhite, True);
|
|
W.TitleLabel.TextSettings.HorzAlign := TTextAlign.Leading;
|
|
|
|
ValLyt := TLayout.Create(W.Background);
|
|
ValLyt.Parent := W.Background;
|
|
ValLyt.Align := TAlignLayout.Client;
|
|
ValLyt.Margins.Rect := RectF(15, 15, 15, 15);
|
|
|
|
// W.StatusLabel := BuildLabel(ValLyt, 'OFF', 54, TAlignLayout.Client, $FFFF5252, True);
|
|
W.StatusLabel := BuildLabel(ValLyt, 'OFF', 50, TAlignLayout.Client, $FFFF5252, True);
|
|
W.StatusLabel.TextSettings.HorzAlign := TTextAlign.Leading;
|
|
|
|
if AConfig.NodeType <> 'DI' then
|
|
begin
|
|
W.ToggleSwitch := TSwitch.Create(W.Background);
|
|
W.ToggleSwitch.Parent := ValLyt;
|
|
W.ToggleSwitch.Align := TAlignLayout.Right;
|
|
W.ToggleSwitch.Width := 120;
|
|
{$IFDEF ANDROID}
|
|
W.ToggleSwitch.Scale.X := 3;
|
|
W.ToggleSwitch.Scale.Y := 3;
|
|
{$ENDIF}
|
|
W.ToggleSwitch.OnSwitch := OnToggleChange;
|
|
end
|
|
else
|
|
begin
|
|
W.ToggleSwitch := nil;
|
|
end;
|
|
|
|
SetLength(Controls, Length(Controls) + 1);
|
|
Controls[High(Controls)] := W;
|
|
Result := W;
|
|
end;
|
|
|
|
procedure TfrmMain.ApplyLanguage;
|
|
var
|
|
i: Integer;
|
|
begin
|
|
if FIsKorean then
|
|
begin
|
|
lblAppTitle.Text := '스마트팜'#13#10'HMI 센터';
|
|
lblStatusTitle.Text := '연결 상태';
|
|
lblNavDashboardText.Text := '모니터링';
|
|
lblNavNodeSettingsText.Text := '노드 설정';
|
|
lblNavSettingsText.Text := 'CCTV설정';
|
|
lblNavCCTVText.Text := 'CCTV';
|
|
lblNavAUTOText.Text := '자동 설정';
|
|
if lblNavLogsText <> nil then lblNavLogsText.Text := '시스템 로그';
|
|
if lblNavIrriText <> nil then lblNavIrriText.Text := '관수 스케쥴';
|
|
lblAutoModeTitle.Text := '수동 / 자동 모드';
|
|
|
|
if FAutoMode then lblAutoModeStatus.Text := '🤖 자동 모드 ON'
|
|
else lblAutoModeStatus.Text := '🖐 수동 모드';
|
|
|
|
lblSettingsTitle.Text := 'CCTV설정 (CCTV 주소 관리)';
|
|
|
|
if lblConnectionStatus.Tag = 0 then lblConnectionStatus.Text := '연결 중...';
|
|
if lblConnectionStatus.Tag = 1 then lblConnectionStatus.Text := '연결됨';
|
|
if lblConnectionStatus.Tag = 2 then lblConnectionStatus.Text := '연결 끊김';
|
|
|
|
if lytScreenDashboard.Visible then lblHeaderTitle.Text := '환경 모니터링'
|
|
else if lytScreenNodeSettings.Visible then lblHeaderTitle.Text := '노드 설정'
|
|
else if lytScreenSettings.Visible then lblHeaderTitle.Text := 'CCTV설정'
|
|
else if lytScreenCCTV.Visible then lblHeaderTitle.Text := 'CCTV 보기'
|
|
else if lytScreenAuto.Visible then lblHeaderTitle.Text := '자동 설정'
|
|
else if (lytScreenLogs <> nil) and lytScreenLogs.Visible then lblHeaderTitle.Text := '시스템 로그'
|
|
else if (lytScreenIrri <> nil) and lytScreenIrri.Visible then lblHeaderTitle.Text := '관수 스케쥴'
|
|
else lblHeaderTitle.Text := '환경 모니터링';
|
|
|
|
lblLangText.Text := 'EN';
|
|
lblModalCloseText.Text := '확인';
|
|
lblModalTitle.Text := '연결 오류';
|
|
lblModalMessage.Text := '서버와의 연결이 끊어졌습니다.';
|
|
|
|
lblCameraAddText.Text := '주소 추가';
|
|
lblCameraDelText.Text := '선택한 주소 삭제';
|
|
edtRTSPUrl.TextPrompt := 'RTSP 스트리밍 주소를 입력하세요 (예: rtsp://192.168.0.10:554/stream1)';
|
|
if btnShowPath <> nil then
|
|
TLabel(btnShowPath.Controls[0]).Text := '저장 경로 확인';
|
|
|
|
if lblLogDate <> nil then lblLogDate.Text := '로그 날짜 선택:';
|
|
if btnRefreshLogs <> nil then TLabel(btnRefreshLogs.Controls[0]).Text := '로그 불러오기 (Load)';
|
|
end
|
|
else
|
|
begin
|
|
lblAppTitle.Text := 'SmartFarm'#13#10'HMI Center';
|
|
lblStatusTitle.Text := 'Connection Status';
|
|
lblNavDashboardText.Text := 'Dashboard';
|
|
lblNavNodeSettingsText.Text := 'Node Settings';
|
|
lblNavSettingsText.Text := 'Settings';
|
|
lblNavCCTVText.Text := 'CCTV';
|
|
lblNavAUTOText.Text := 'Auto Settings';
|
|
if lblNavLogsText <> nil then lblNavLogsText.Text := 'System Logs';
|
|
if lblNavIrriText <> nil then lblNavIrriText.Text := 'Irrigation';
|
|
lblAutoModeTitle.Text := 'Auto / Manual Mode';
|
|
|
|
if FAutoMode then lblAutoModeStatus.Text := '🤖 AUTO MODE ON'
|
|
else lblAutoModeStatus.Text := '🖐 MANUAL MODE';
|
|
|
|
lblSettingsTitle.Text := 'CCTV Settings (Manage URLs)';
|
|
|
|
if lblConnectionStatus.Tag = 0 then lblConnectionStatus.Text := 'Connecting...';
|
|
if lblConnectionStatus.Tag = 1 then lblConnectionStatus.Text := 'Connected';
|
|
if lblConnectionStatus.Tag = 2 then lblConnectionStatus.Text := 'Disconnected';
|
|
|
|
if lytScreenDashboard.Visible then lblHeaderTitle.Text := 'Environment Overview'
|
|
else if lytScreenNodeSettings.Visible then lblHeaderTitle.Text := 'Node Settings'
|
|
else if lytScreenSettings.Visible then lblHeaderTitle.Text := 'CCTV Settings'
|
|
else if lytScreenCCTV.Visible then lblHeaderTitle.Text := 'CCTV View'
|
|
else if lytScreenAuto.Visible then lblHeaderTitle.Text := 'Auto Settings'
|
|
else if (lytScreenLogs <> nil) and lytScreenLogs.Visible then lblHeaderTitle.Text := 'System Logs'
|
|
else if (lytScreenIrri <> nil) and lytScreenIrri.Visible then lblHeaderTitle.Text := 'Irrigation Schedule'
|
|
else lblHeaderTitle.Text := 'Environment Overview';
|
|
|
|
lblLangText.Text := 'KO';
|
|
lblModalCloseText.Text := 'OK';
|
|
lblModalTitle.Text := 'Connection Error';
|
|
lblModalMessage.Text := 'Connection to server was lost.';
|
|
|
|
lblCameraAddText.Text := 'Add Address';
|
|
lblCameraDelText.Text := 'Delete Selected';
|
|
edtRTSPUrl.TextPrompt := 'Enter RTSP Stream URL (e.g., rtsp://192.168.0.10:554/stream1)';
|
|
if btnShowPath <> nil then
|
|
TLabel(btnShowPath.Controls[0]).Text := 'Show INI Path';
|
|
|
|
if lblLogDate <> nil then lblLogDate.Text := 'Select Log Date:';
|
|
if btnRefreshLogs <> nil then TLabel(btnRefreshLogs.Controls[0]).Text := 'Load Logs';
|
|
end;
|
|
|
|
BuildCCTVGrid; // Reload CCTV button text based on new language
|
|
|
|
// 관수 스케쥴 화면 텍스트 갱신
|
|
if (scrlIrriSettings <> nil) and (Length(FIrriSchedules) >= 0) then
|
|
TThread.Queue(nil, RefreshIrriUI);
|
|
|
|
// 자동설정 하단 버튼 텍스트 갱신
|
|
if btnAutoRuleBar <> nil then
|
|
BuildAutoUI;
|
|
|
|
for i := 0 to High(FNodeConfigs) do
|
|
begin
|
|
if (FNodeConfigs[i].SettingRect <> nil) and (FNodeConfigs[i].SettingRect.ControlsCount > 0) then
|
|
begin
|
|
if FNodeConfigs[i].SettingRect.Controls[0] is TLabel then
|
|
begin
|
|
if FIsKorean then
|
|
TLabel(FNodeConfigs[i].SettingRect.Controls[0]).Text := FNodeConfigs[i].TitleKO + ' [' + FNodeConfigs[i].ID + ']'
|
|
else
|
|
TLabel(FNodeConfigs[i].SettingRect.Controls[0]).Text := FNodeConfigs[i].TitleEN + ' [' + FNodeConfigs[i].ID + ']';
|
|
end;
|
|
end;
|
|
end;
|
|
|
|
for i := 0 to High(Sensors) do
|
|
begin
|
|
if Sensors[i].CustomName <> '' then
|
|
Sensors[i].TitleLabel.Text := Sensors[i].CustomName
|
|
else if FIsKorean then
|
|
Sensors[i].TitleLabel.Text := Sensors[i].TitleKO
|
|
else
|
|
Sensors[i].TitleLabel.Text := Sensors[i].TitleEN;
|
|
end;
|
|
|
|
for i := 0 to High(Controls) do
|
|
begin
|
|
if Controls[i].CustomName <> '' then
|
|
Controls[i].TitleLabel.Text := Controls[i].CustomName
|
|
else if FIsKorean then
|
|
Controls[i].TitleLabel.Text := Controls[i].TitleKO
|
|
else
|
|
Controls[i].TitleLabel.Text := Controls[i].TitleEN;
|
|
if Controls[i].ToggleSwitch <> nil then
|
|
Controls[i].UpdateStatus(Controls[i].ToggleSwitch.IsChecked)
|
|
else if Controls[i].StatusLabel <> nil then
|
|
Controls[i].UpdateStatus((Controls[i].StatusLabel.Text = '켜짐') or (Controls[i].StatusLabel.Text = 'ON'));
|
|
end;
|
|
|
|
// Translate Node Settings group headers
|
|
if lytNodesMain <> nil then
|
|
begin
|
|
for i := 0 to lytNodesMain.ControlsCount - 1 do
|
|
begin
|
|
if lytNodesMain.Controls[i].Tag = -1 then // Is Header Layout
|
|
begin
|
|
if (lytNodesMain.Controls[i] as TLayout).ControlsCount > 0 then
|
|
begin
|
|
if (lytNodesMain.Controls[i] as TLayout).Controls[0] is TLabel then
|
|
begin
|
|
with TLabel((lytNodesMain.Controls[i] as TLayout).Controls[0]) do
|
|
begin
|
|
Text := GetGroupName(Hint, FIsKorean);
|
|
end;
|
|
end;
|
|
end;
|
|
end;
|
|
end;
|
|
end;
|
|
end;
|
|
|
|
procedure TfrmMain.bgStatusDblClick(Sender: TObject);
|
|
begin
|
|
if Sender is TRectangle then
|
|
begin
|
|
lytMQTTSetupPopup.Visible := True;
|
|
lytMQTTSetupPopup.BringToFront;
|
|
end;
|
|
end;
|
|
|
|
procedure TfrmMain.OnLangClick(Sender: TObject);
|
|
begin
|
|
FIsKorean := not FIsKorean;
|
|
ApplyLanguage;
|
|
SaveSetting_each('bool', 'SetupLang', 'IsKorean', BoolToStr(FIsKorean));
|
|
end;
|
|
|
|
procedure TfrmMain.OnFullscreenClick(Sender: TObject);
|
|
begin
|
|
Self.FullScreen := not Self.FullScreen;
|
|
end;
|
|
|
|
procedure TfrmMain.OnModalCloseClick(Sender: TObject);
|
|
begin
|
|
rectModalOverlay.Visible := False;
|
|
end;
|
|
|
|
procedure TfrmMain.OnToggleChange(Sender: TObject);
|
|
var
|
|
SW: TSwitch;
|
|
W: TControlWidget;
|
|
i, j, NodeIdx: Integer;
|
|
DesiredState: Boolean;
|
|
begin
|
|
// MQTT에서 업데이트 중이면 무시 (재귀 방지)
|
|
if FUpdatingControls then Exit;
|
|
|
|
SW := Sender as TSwitch;
|
|
for i := 0 to High(Controls) do
|
|
begin
|
|
W := Controls[i];
|
|
if W.ToggleSwitch = SW then
|
|
begin
|
|
// 사용자가 원하는 상태 캐치
|
|
DesiredState := SW.IsChecked;
|
|
|
|
// 토글을 원래 상태로 되돌림 (MQTT 확인 후에만 UI 변경)
|
|
FUpdatingControls := True;
|
|
try
|
|
SW.IsChecked := not DesiredState;
|
|
finally
|
|
FUpdatingControls := False;
|
|
end;
|
|
|
|
// 설정된 노드 인덱스 찾기
|
|
NodeIdx := -1;
|
|
for j := 0 to High(FNodeConfigs) do
|
|
begin
|
|
if FNodeConfigs[j].ID = W.NodeID then
|
|
begin
|
|
NodeIdx := FNodeConfigs[j].Index;
|
|
Break;
|
|
end;
|
|
end;
|
|
|
|
// MQTT Publish 및 Pending 설정
|
|
if (NodeIdx >= 0) and (NodeIdx <= High(FControlPending)) then
|
|
begin
|
|
FControlPending[NodeIdx] := True;
|
|
FControlDesired[NodeIdx] := DesiredState;
|
|
PublishControl(NodeIdx, DesiredState);
|
|
end;
|
|
|
|
Break;
|
|
end;
|
|
end;
|
|
end;
|
|
|
|
procedure TfrmMain.OnNavClick(Sender: TObject);
|
|
var
|
|
Btn: TRectangle;
|
|
begin
|
|
if Sender is TRectangle then
|
|
begin
|
|
Btn := TRectangle(Sender);
|
|
SwitchScreen(Btn.Tag);
|
|
end;
|
|
end;
|
|
|
|
procedure TfrmMain.SwitchScreen(Index: Integer);
|
|
var
|
|
PrevIndex: Integer;
|
|
begin
|
|
{$IFDEF ANDROID}
|
|
if Index = 2 then begin
|
|
ShowCustomAlert('CCTV 설정 정보', 'CCTV SETTINGS', '안드로이드 앱에서 설정가능합니다.', '');
|
|
Exit;
|
|
end;
|
|
{$ENDIF}
|
|
|
|
// 현재 활성화된 화면 인덱스 저장
|
|
PrevIndex := 0;
|
|
if lytScreenDashboard.Visible then PrevIndex := 0
|
|
else if lytScreenNodeSettings.Visible then PrevIndex := 1
|
|
else if lytScreenSettings.Visible then PrevIndex := 2
|
|
else if lytScreenCCTV.Visible then PrevIndex := 3
|
|
else if lytScreenAuto.Visible then PrevIndex := 4
|
|
else if (lytScreenLogs <> nil) and lytScreenLogs.Visible then PrevIndex := 5
|
|
else if (lytScreenIrri <> nil) and lytScreenIrri.Visible then PrevIndex := 6;
|
|
|
|
// Stop media player if moving away from CCTV or just generally resetting state
|
|
MediaPlayer.Stop;
|
|
MediaControl.Visible := False;
|
|
|
|
// Reset all button backgrounds
|
|
btnNavDashboard.Fill.Kind := TBrushKind.None;
|
|
btnNavSettings.Fill.Kind := TBrushKind.None;
|
|
btnNavCCTV.Fill.Kind := TBrushKind.None;
|
|
btnNavAUTO.Fill.Kind := TBrushKind.None;
|
|
btnNavNodeSettings.Fill.Kind := TBrushKind.None;
|
|
if btnNavLogs <> nil then btnNavLogs.Fill.Kind := TBrushKind.None;
|
|
if btnNavIrri <> nil then btnNavIrri.Fill.Kind := TBrushKind.None;
|
|
|
|
// 만약 CCTV(3)를 누른 경우, 화면 전환을 하지 않고 이전 화면 상태를 복구하기 위해서 임시 보관
|
|
// 하지만 먼저 모든 화면을 숨김
|
|
lytScreenDashboard.Visible := False;
|
|
lytScreenSettings.Visible := False;
|
|
lytScreenCCTV.Visible := False;
|
|
lytScreenAuto.Visible := False;
|
|
lytScreenNodeSettings.Visible := False;
|
|
if lytScreenLogs <> nil then lytScreenLogs.Visible := False;
|
|
if lytScreenIrri <> nil then lytScreenIrri.Visible := False;
|
|
|
|
// Show active screen and set active button style
|
|
case Index of
|
|
0:
|
|
begin
|
|
btnNavDashboard.Fill.Kind := TBrushKind.Solid;
|
|
btnNavDashboard.Fill.Color := $404CAF50;
|
|
lytScreenDashboard.Visible := True;
|
|
if FIsKorean then lblHeaderTitle.Text := '환경 모니터링' else lblHeaderTitle.Text := 'Environment Overview';
|
|
end;
|
|
1:
|
|
begin
|
|
btnNavNodeSettings.Fill.Kind := TBrushKind.Solid;
|
|
btnNavNodeSettings.Fill.Color := $404CAF50;
|
|
lytScreenNodeSettings.Visible := True;
|
|
if FIsKorean then lblHeaderTitle.Text := '노드 설정' else lblHeaderTitle.Text := 'Node Settings';
|
|
end;
|
|
2:
|
|
begin
|
|
btnNavSettings.Fill.Kind := TBrushKind.Solid;
|
|
btnNavSettings.Fill.Color := $404CAF50;
|
|
lytScreenSettings.Visible := True;
|
|
if FIsKorean then lblHeaderTitle.Text := 'CCTV설정' else lblHeaderTitle.Text := 'CCTV Settings';
|
|
end;
|
|
3:
|
|
begin
|
|
// Windows와 Android: 화면 전환 없이 바로 첫 번째 카메라 외부 실행
|
|
// MacOS와 Linux: CCTV 화면으로 전환
|
|
|
|
{$IF DEFINED(MSWINDOWS) OR DEFINED(ANDROID)}
|
|
if FRTSPApps.Count > 0 then
|
|
begin
|
|
var LParts := FRTSPApps[0].Split(['|']);
|
|
var LAppPkg := '';
|
|
if Length(LParts) > 0 then LAppPkg := LParts[0];
|
|
|
|
{$IFDEF ANDROID}
|
|
try
|
|
var Intent := TAndroidHelper.Context.getPackageManager().getLaunchIntentForPackage(StringToJString(LAppPkg));
|
|
if Intent <> nil then
|
|
TAndroidHelper.Activity.startActivity(Intent)
|
|
else
|
|
begin
|
|
if FIsKorean then
|
|
ShowCustomAlert('앱 실행 불가', 'Error', '패키지명(' + LAppPkg + ')을 가진 앱을 타겟 기기에서 찾을 수 없습니다.'#13#10'정확한 패키지명을 확인하거나 앱이 설치되었는지 확인해주세요.', '')
|
|
else
|
|
ShowCustomAlert('Launch Error', 'Error', '', 'Cannot find package (' + LAppPkg + ').');
|
|
end;
|
|
except
|
|
on E: Exception do
|
|
begin
|
|
if FIsKorean then
|
|
ShowCustomAlert('앱 실행 예외', 'Launch Error', '앱 실행 중 오류가 발생했습니다.'#13#10 + E.Message, '')
|
|
else
|
|
ShowCustomAlert('Launch Error', 'Launch Error', '', 'Cannot launch RtspStreaming app.'#13#10'Error: ' + E.Message);
|
|
end;
|
|
end;
|
|
{$ENDIF}
|
|
|
|
{$IFDEF MSWINDOWS}
|
|
if lstCameras.Count > 0 then
|
|
begin
|
|
var URL := lstCameras.Items[0];
|
|
if URL <> '' then
|
|
begin
|
|
var ExePath := ExtractFilePath(ParamStr(0)) + 'SmartFarmHMI_VCL_RTSP.exe';
|
|
ShellExecute(0, 'open', PChar(ExePath), PChar(URL), nil, SW_SHOWNORMAL);
|
|
end;
|
|
end
|
|
else
|
|
begin
|
|
if FIsKorean then
|
|
ShowCustomAlert('안내', 'Info', '등록된 CCTV 주소가 없습니다. CCTV 설정에서 추가해주세요.', '')
|
|
else
|
|
ShowCustomAlert('Info', 'Info', '', 'No CCTV URL is registered. Please add one in CCTV Settings.');
|
|
end;
|
|
{$ENDIF}
|
|
end
|
|
else
|
|
begin
|
|
if FIsKorean then
|
|
ShowCustomAlert('안내', 'Info', '등록된 CCTV 앱이 설정에 없습니다.', '')
|
|
else
|
|
ShowCustomAlert('Info', 'Info', '', 'No CCTV App is registered in settings.');
|
|
end;
|
|
|
|
// 화면 이동 복원
|
|
if PrevIndex <> 3 then
|
|
SwitchScreen(PrevIndex)
|
|
else
|
|
SwitchScreen(0);
|
|
{$ELSE}
|
|
|
|
// MacOS / Linux: 기존의 CCTV 화면 전환 동작 수행
|
|
btnNavCCTV.Fill.Kind := TBrushKind.Solid;
|
|
btnNavCCTV.Fill.Color := $404CAF50;
|
|
lytScreenCCTV.Visible := True;
|
|
if FIsKorean then lblHeaderTitle.Text := 'CCTV 보기' else lblHeaderTitle.Text := 'CCTV View';
|
|
BuildCCTVGrid; // Rebuild camera buttons when entering CCTV tab
|
|
|
|
{$ENDIF}
|
|
end;
|
|
4:
|
|
begin
|
|
btnNavAUTO.Fill.Kind := TBrushKind.Solid;
|
|
btnNavAUTO.Fill.Color := $404CAF50;
|
|
lytScreenAuto.Visible := True;
|
|
if FIsKorean then lblHeaderTitle.Text := '자동 설정' else lblHeaderTitle.Text := 'Auto Settings';
|
|
end;
|
|
5:
|
|
begin
|
|
if btnNavLogs <> nil then
|
|
begin
|
|
btnNavLogs.Fill.Kind := TBrushKind.Solid;
|
|
btnNavLogs.Fill.Color := $404CAF50;
|
|
end;
|
|
if lytScreenLogs <> nil then lytScreenLogs.Visible := True;
|
|
if FIsKorean then lblHeaderTitle.Text := '시스템 로그' else lblHeaderTitle.Text := 'System Logs';
|
|
OnRefreshLogsClick(nil);
|
|
end;
|
|
6:
|
|
begin
|
|
if btnNavIrri <> nil then
|
|
begin
|
|
btnNavIrri.Fill.Kind := TBrushKind.Solid;
|
|
btnNavIrri.Fill.Color := $404CAF50;
|
|
end;
|
|
if lytScreenIrri <> nil then lytScreenIrri.Visible := True;
|
|
if FIsKorean then lblHeaderTitle.Text := '관수 스케쥴' else lblHeaderTitle.Text := 'Irrigation Schedule';
|
|
RefreshIrriUI;
|
|
end;
|
|
end;
|
|
end;
|
|
|
|
procedure TfrmMain.OnAddCameraClick(Sender: TObject);
|
|
var
|
|
LItem: TListBoxItem;
|
|
begin
|
|
if Trim(edtRTSPUrl.Text) <> '' then
|
|
begin
|
|
LItem := TListBoxItem.Create(lstCameras);
|
|
LItem.Parent := lstCameras;
|
|
LItem.Height := 80;
|
|
LItem.Text := Trim(edtRTSPUrl.Text);
|
|
|
|
// Apply custom font size on the individual item level
|
|
LItem.StyledSettings := LItem.StyledSettings - [TStyledSetting.Size];
|
|
LItem.TextSettings.Font.Size := 24;
|
|
|
|
lstCameras.AddObject(LItem);
|
|
edtRTSPUrl.Text := '';
|
|
SaveSettings;
|
|
end;
|
|
end;
|
|
|
|
procedure TfrmMain.OnDelCameraClick(Sender: TObject);
|
|
begin
|
|
if lstCameras.ItemIndex >= 0 then
|
|
begin
|
|
lstCameras.Items.Delete(lstCameras.ItemIndex);
|
|
SaveSettings;
|
|
end;
|
|
end;
|
|
|
|
procedure TfrmMain.LoadSettings;
|
|
var
|
|
Ini: TIniFile;
|
|
i, Count: Integer;
|
|
S, SettingsPath: string;
|
|
LItem: TListBoxItem;
|
|
begin
|
|
lstCameras.Clear;
|
|
FRTSPApps.Clear;
|
|
SettingsPath := System.IOUtils.TPath.Combine(System.IOUtils.TPath.GetHomePath, 'SmartFarmHMI_settings.ini');
|
|
Ini := TIniFile.Create(SettingsPath);
|
|
try
|
|
FMQTTGateWayID := Ini.ReadString( 'MQTT', 'GateWayID', '1000');
|
|
FMQTTBrokerIP := Ini.ReadString( 'MQTT', 'BrokerIP', '192.168.200.240');
|
|
FMQTTUserID := Ini.ReadString( 'MQTT', 'UserID', 'qsentech');
|
|
FMQTTUserPASS := Ini.ReadString( 'MQTT', 'UserPASS', 'qsentech!1233');
|
|
FMQTTBrokerPORT := Ini.ReadInteger('MQTT', 'BrokerPORT', 1883);
|
|
FMQTTTopicHEAD := Ini.ReadString( 'MQTT', 'TopicHEAD', 'SMART-FARM/2/3/2/');
|
|
FMQTTTopicTAIL := Ini.ReadString( 'MQTT', 'TopicTAIL', '1/1/0/GTW/');
|
|
|
|
FIsKorean := Ini.ReadBool('SetupLang', 'IsKorean', True);
|
|
Count := Ini.ReadInteger('CCTV', 'Count', 0);
|
|
for i := 0 to Count - 1 do
|
|
begin
|
|
S := Ini.ReadString('CCTV', 'Cam' + IntToStr(i), '');
|
|
if S <> '' then
|
|
begin
|
|
LItem := TListBoxItem.Create(lstCameras);
|
|
LItem.Parent := lstCameras;
|
|
LItem.Height := 80;
|
|
LItem.Text := S;
|
|
LItem.StyledSettings := LItem.StyledSettings - [TStyledSetting.Size];
|
|
LItem.TextSettings.Font.Size := 24;
|
|
lstCameras.AddObject(LItem);
|
|
end;
|
|
end;
|
|
|
|
// Load RTSP Apps
|
|
Count := Ini.ReadInteger('RTSPApps', 'Count', 0);
|
|
if Count > 0 then
|
|
begin
|
|
for i := 0 to Count - 1 do
|
|
begin
|
|
S := Ini.ReadString('RTSPApps', 'App' + IntToStr(i), '');
|
|
if S <> '' then
|
|
FRTSPApps.Add(S); // Format: package_name|AppName (e.g. com.example.rtspstreaming|RtspStreaming)
|
|
end;
|
|
end
|
|
else
|
|
begin
|
|
// Fallback default app if no apps are configured
|
|
FRTSPApps.Add('com.example.rtspstreaming|RtspStreaming');
|
|
end;
|
|
|
|
for i := 0 to High(FNodeConfigs) do
|
|
begin
|
|
FNodeConfigs[i].InUse := Ini.ReadBool('Nodes', FNodeConfigs[i].ID, False);
|
|
FNodeConfigs[i].CustomName := Ini.ReadString('NodeNames', FNodeConfigs[i].ID, '');
|
|
// Read icon index (integer) and convert to emoji to avoid encoding issues
|
|
FNodeConfigs[i].CustomIcon := GetIconByIndex(Ini.ReadInteger('NodeIcons', FNodeConfigs[i].ID, 0));
|
|
end;
|
|
finally
|
|
Ini.Free;
|
|
end;
|
|
end;
|
|
|
|
procedure TfrmMain.SaveSettings;
|
|
var
|
|
Ini: TIniFile;
|
|
i: Integer;
|
|
SettingsPath: string;
|
|
begin
|
|
SettingsPath := System.IOUtils.TPath.Combine(System.IOUtils.TPath.GetHomePath, 'SmartFarmHMI_settings.ini');
|
|
Ini := TIniFile.Create(SettingsPath);
|
|
try
|
|
Ini.EraseSection('CCTV');
|
|
Ini.WriteInteger('CCTV', 'Count', lstCameras.Count);
|
|
for i := 0 to lstCameras.Count - 1 do
|
|
Ini.WriteString('CCTV', 'Cam' + IntToStr(i), lstCameras.Items[i]);
|
|
|
|
Ini.EraseSection('RTSPApps');
|
|
Ini.WriteInteger('RTSPApps', 'Count', FRTSPApps.Count);
|
|
for i := 0 to FRTSPApps.Count - 1 do
|
|
Ini.WriteString('RTSPApps', 'App' + IntToStr(i), FRTSPApps[i]);
|
|
|
|
for i := 0 to High(FNodeConfigs) do
|
|
begin
|
|
Ini.WriteBool('Nodes', FNodeConfigs[i].ID, FNodeConfigs[i].InUse);
|
|
Ini.WriteString('NodeNames', FNodeConfigs[i].ID, FNodeConfigs[i].CustomName);
|
|
// Save icon as integer index instead of emoji to avoid encoding errors
|
|
Ini.WriteInteger('NodeIcons', FNodeConfigs[i].ID, GetIconIndex(FNodeConfigs[i].CustomIcon));
|
|
end;
|
|
finally
|
|
Ini.Free;
|
|
end;
|
|
AddSystemLog('앱 공통 설정 및 CCTV 목록이 저장되었습니다.');
|
|
end;
|
|
|
|
procedure TfrmMain.SaveSettings_MQTT;
|
|
var
|
|
Ini: TIniFile;
|
|
i: Integer;
|
|
SettingsPath: string;
|
|
begin
|
|
SettingsPath := System.IOUtils.TPath.Combine(System.IOUtils.TPath.GetHomePath, 'SmartFarmHMI_settings.ini');
|
|
Ini := TIniFile.Create(SettingsPath);
|
|
try
|
|
Ini.EraseSection('MQTT');
|
|
Ini.WriteString( 'MQTT', 'GateWayID' , FMQTTGateWayID);
|
|
Ini.WriteString( 'MQTT', 'BrokerIP' , FMQTTBrokerIP);
|
|
Ini.WriteString( 'MQTT', 'UserID' , FMQTTUserID);
|
|
Ini.WriteString( 'MQTT', 'UserPASS' , FMQTTUserPASS);
|
|
Ini.WriteInteger('MQTT', 'BrokerPORT' , FMQTTBrokerPORT);
|
|
Ini.WriteString( 'MQTT', 'TopicHEAD' , FMQTTTopicHEAD);
|
|
Ini.WriteString( 'MQTT', 'TopicTAIL' , FMQTTTopicTAIL);
|
|
finally
|
|
Ini.Free;
|
|
end;
|
|
AddSystemLog('MQTT 접속정보 설정이 저장되었습니다.');
|
|
end;
|
|
|
|
procedure TfrmMain.SaveSetting_each(ck,_section,_ident,_Value : string);
|
|
var Ini: TIniFile; SP: string;
|
|
begin
|
|
SP := System.IOUtils.TPath.Combine(System.IOUtils.TPath.GetHomePath, 'SmartFarmHMI_settings.ini');
|
|
Ini := TIniFile.Create(SP);
|
|
try
|
|
if ck = 'bool' then begin
|
|
Ini.WriteBool(_section, _ident, StrToBool(_Value));
|
|
end else
|
|
if ck = 'string' then begin
|
|
Ini.WriteString(_section, _ident, _Value);
|
|
end else
|
|
if ck = 'integer' then begin
|
|
Ini.WriteInteger(_section, _ident, StrToInt(_Value));
|
|
end;
|
|
finally
|
|
Ini.Free;
|
|
end;
|
|
end;
|
|
|
|
function TfrmMain.GetIconByIndex(AIndex: Integer): string;
|
|
begin
|
|
case AIndex of
|
|
1: Result := '🎛️';
|
|
2: Result := '🌡️';
|
|
3: Result := '💧';
|
|
4: Result := '💨';
|
|
5: Result := '☀️';
|
|
6: Result := '🌱';
|
|
7: Result := '⚡';
|
|
8: Result := '🧪';
|
|
9: Result := '📊';
|
|
10: Result := '📡';
|
|
11: Result := '💡';
|
|
12: Result := '🔔';
|
|
else
|
|
Result := ''; // 0 = 기본 (default)
|
|
end;
|
|
end;
|
|
|
|
function TfrmMain.GetIconIndex(const AIcon: string): Integer;
|
|
begin
|
|
if AIcon = '🎛️' then Result := 1
|
|
else if AIcon = '🌡️' then Result := 2
|
|
else if AIcon = '💧' then Result := 3
|
|
else if AIcon = '💨' then Result := 4
|
|
else if AIcon = '☀️' then Result := 5
|
|
else if AIcon = '🌱' then Result := 6
|
|
else if AIcon = '⚡' then Result := 7
|
|
else if AIcon = '🧪' then Result := 8
|
|
else if AIcon = '📊' then Result := 9
|
|
else if AIcon = '📡' then Result := 10
|
|
else if AIcon = '💡' then Result := 11
|
|
else if AIcon = '🔔' then Result := 12
|
|
else Result := 0; // 기본
|
|
end;
|
|
|
|
procedure TfrmMain.BuildCCTVGrid;
|
|
var
|
|
i: Integer;
|
|
Btn: TRectangle;
|
|
LText, LAppName, LPackageName: string;
|
|
Parts: TArray<string>;
|
|
begin
|
|
// Clear previous buttons
|
|
while lytCCTVContainer.ControlsCount > 0 do
|
|
lytCCTVContainer.Controls[0].Free;
|
|
|
|
{$IF DEFINED(MSWINDOWS) OR DEFINED(ANDROID)}
|
|
// MSWINDOWS, ANDROID: Use registered RTSP Apps
|
|
if FRTSPApps.Count = 0 then Exit;
|
|
|
|
for i := 0 to FRTSPApps.Count - 1 do
|
|
begin
|
|
Parts := FRTSPApps[i].Split(['|']);
|
|
if Length(Parts) > 0 then
|
|
begin
|
|
LPackageName := Parts[0];
|
|
if Length(Parts) > 1 then
|
|
LAppName := Parts[1]
|
|
else
|
|
LAppName := LPackageName;
|
|
|
|
Btn := CreateCustomButton(lytCCTVContainer, LAppName, TAlignLayout.Left, 250, OnPlayCameraClick);
|
|
Btn.Hint := LPackageName; // Store App Package Name in Hint property
|
|
end;
|
|
end;
|
|
{$ELSE}
|
|
// MACOS, LINUX: Use registered camera URLs directly
|
|
if lstCameras.Count = 0 then Exit;
|
|
|
|
for i := 0 to lstCameras.Count - 1 do
|
|
begin
|
|
LText := 'Camera ' + IntToStr(i + 1);
|
|
Btn := CreateCustomButton(lytCCTVContainer, LText, TAlignLayout.Left, 250, OnPlayCameraClick);
|
|
Btn.Hint := lstCameras.Items[i]; // Store Camera URL in Hint property
|
|
end;
|
|
{$ENDIF}
|
|
end;
|
|
|
|
procedure TfrmMain.OnPlayCameraClick(Sender: TObject);
|
|
var
|
|
AppPackageName: string;
|
|
i: Integer;
|
|
{$IFDEF MSWINDOWS}
|
|
ExePath: string;
|
|
{$ENDIF}
|
|
{$IFDEF ANDROID}
|
|
Intent: JIntent;
|
|
{$ENDIF}
|
|
{$IFDEF MACOS}
|
|
Cmd: string;
|
|
{$ENDIF}
|
|
{$IFDEF LINUX}
|
|
Cmd: string;
|
|
{$ENDIF}
|
|
begin
|
|
if Sender is TRectangle then
|
|
begin
|
|
AppPackageName := TRectangle(Sender).Hint;
|
|
if AppPackageName <> '' then
|
|
begin
|
|
{$IF DEFINED(MSWINDOWS) OR DEFINED(ANDROID)}
|
|
|
|
{$IFDEF MSWINDOWS}
|
|
// 윈도우에서는 임시로 첫번째 CCTV 주소를 파라미터로 실행 유지
|
|
if lstCameras.Count > 0 then
|
|
begin
|
|
ExePath := ExtractFilePath(ParamStr(0)) + 'SmartFarmHMI_VCL_RTSP.exe';
|
|
ShellExecute(0, 'open', PChar(ExePath), PChar(lstCameras.Items[0]), nil, SW_SHOWNORMAL);
|
|
end
|
|
else
|
|
ShowCustomAlert('안내', 'Info', '등록된 CCTV 주소가 없습니다.', 'No CCTV URL is registered.');
|
|
{$ENDIF}
|
|
|
|
{$IFDEF ANDROID}
|
|
try
|
|
Intent := TAndroidHelper.Context.getPackageManager().getLaunchIntentForPackage(StringToJString(AppPackageName));
|
|
if Intent <> nil then
|
|
begin
|
|
TAndroidHelper.Activity.startActivity(Intent);
|
|
end
|
|
else
|
|
begin
|
|
if FIsKorean then
|
|
ShowCustomAlert('앱 실행 불가', 'Error', '패키지명(' + AppPackageName + ')을 가진 앱을 스마트폰에서 찾을 수 없습니다.'#13#10'정확한 패키지명 확인 및 설치상태를 점검해주세요.', '')
|
|
else
|
|
ShowCustomAlert('Launch Error', 'Error', '', 'Cannot find package (' + AppPackageName + ').');
|
|
end;
|
|
except
|
|
on E: Exception do
|
|
begin
|
|
if FIsKorean then
|
|
ShowCustomAlert('앱 실행 실패', 'Launch Error', '앱 실행 중 오류가 발생했습니다.'#13#10'오류: ' + E.Message, '')
|
|
else
|
|
ShowCustomAlert('Launch Error', 'Launch Error', '', 'Cannot launch app.'#13#10'Error: ' + E.Message);
|
|
end;
|
|
end;
|
|
{$ENDIF}
|
|
|
|
{$ELSE}
|
|
|
|
// Mac/Linux는 버튼의 Hint 속성(AppPackageName 변수)에 저장된 URL을 VLC로 실행
|
|
{$IFDEF MACOS}
|
|
Cmd := 'open -a VLC "' + AppPackageName + '"';
|
|
_system(PAnsiChar(UTF8Encode(Cmd)));
|
|
{$ENDIF}
|
|
|
|
{$IFDEF LINUX}
|
|
Cmd := 'vlc "' + AppPackageName + '" &';
|
|
_system(PAnsiChar(UTF8Encode(Cmd)));
|
|
{$ENDIF}
|
|
{$ENDIF}
|
|
end
|
|
else
|
|
begin
|
|
if FIsKorean then
|
|
ShowCustomAlert('오류', 'Error', '선택된 앱 정보가 유효하지 않습니다.', '')
|
|
else
|
|
ShowCustomAlert('오류', 'Error', '', 'Selected app info is invalid.');
|
|
end;
|
|
end;
|
|
end;
|
|
|
|
procedure TfrmMain.ShowCustomAlert(const ATitleKO, ATitleEN, AMessageKO, AMessageEN: string);
|
|
begin
|
|
if FIsKorean then
|
|
begin
|
|
lblModalTitle.Text := ATitleKO;
|
|
lblModalMessage.Text := AMessageKO;
|
|
end
|
|
else
|
|
begin
|
|
lblModalTitle.Text := ATitleEN;
|
|
lblModalMessage.Text := AMessageEN;
|
|
end;
|
|
|
|
// 안드로이드에서 텍스트가 짤리거나 겹치는 현상을 방지하기 위해 명시적으로 설정
|
|
lblModalMessage.WordWrap := True;
|
|
lblModalMessage.AutoSize := False;
|
|
lblModalMessage.TextSettings.VertAlign := TTextAlign.Center;
|
|
|
|
// 팝업창 크기 동적 조절 (글자 수에 비례하여 크기 증가)
|
|
if lblModalMessage.Parent is TControl then
|
|
begin
|
|
var Panel := TControl(lblModalMessage.Parent);
|
|
// 기본 크기 설정
|
|
Panel.Height := 250;
|
|
lblModalMessage.Height := 120;
|
|
|
|
// 긴 메세지일 경우 창 세로 길이를 더 늘려줌
|
|
if Length(lblModalMessage.Text) > 30 then
|
|
begin
|
|
Panel.Height := 350;
|
|
lblModalMessage.Height := 200;
|
|
end;
|
|
|
|
{$IFDEF ANDROID}
|
|
// 안드로이드에서는 텍스트 크기 스케일링으로 인해 잘릴 수 있으므로 높이를 크게 늘림
|
|
// 추가적인 여백 확보
|
|
Panel.Height := Panel.Height + 150;
|
|
lblModalMessage.Height := lblModalMessage.Height + 150;
|
|
{$ENDIF}
|
|
end;
|
|
|
|
rectModalOverlay.Visible := True;
|
|
rectModalOverlay.BringToFront;
|
|
end;
|
|
|
|
procedure TfrmMain.OnShowPathClick(Sender: TObject);
|
|
var
|
|
S: string;
|
|
begin
|
|
S := System.IOUtils.TPath.Combine(System.IOUtils.TPath.GetHomePath, 'SmartFarmHMI_settings.ini');
|
|
ShowCustomAlert('저장 경로 안내', 'INI File Path', 'INI 파일 저장 경로:'#13#10 + S, 'INI Settings Path:'#13#10 + S);
|
|
end;
|
|
|
|
procedure TfrmMain.AddSystemLog(const AMessage: string);
|
|
var
|
|
LogDir, LogFile: string;
|
|
LogLine: string;
|
|
begin
|
|
LogDir := System.IOUtils.TPath.Combine(System.IOUtils.TPath.GetHomePath, 'Logs');
|
|
if not DirectoryExists(LogDir) then
|
|
ForceDirectories(LogDir);
|
|
|
|
LogFile := System.IOUtils.TPath.Combine(LogDir, 'SmartFarmHMI_' + FormatDateTime('yyyymmdd', Now) + '.log');
|
|
|
|
LogLine := '[' + FormatDateTime('yyyy-mm-dd hh:nn:ss', Now) + '] ' + AMessage;
|
|
|
|
try
|
|
System.IOUtils.TFile.AppendAllText(LogFile, LogLine + sLineBreak, TEncoding.UTF8);
|
|
except
|
|
// 로그 파일 접근 예외 무시
|
|
end;
|
|
end;
|
|
|
|
procedure TfrmMain.OnRefreshLogsClick(Sender: TObject);
|
|
var
|
|
LogFile: string;
|
|
LogDir: string;
|
|
TempList: TStringList;
|
|
i: Integer;
|
|
TargetDate: TDateTime;
|
|
begin
|
|
if (memoLogs = nil) or (dateLog = nil) then Exit;
|
|
memoLogs.Lines.Clear;
|
|
|
|
TargetDate := dateLog.Date;
|
|
|
|
LogDir := System.IOUtils.TPath.Combine(System.IOUtils.TPath.GetHomePath, 'Logs');
|
|
LogFile := System.IOUtils.TPath.Combine(LogDir, 'SmartFarmHMI_' + FormatDateTime('yyyymmdd', TargetDate) + '.log');
|
|
|
|
if FileExists(LogFile) then
|
|
begin
|
|
TempList := TStringList.Create;
|
|
try
|
|
try
|
|
TempList.LoadFromFile(LogFile, TEncoding.UTF8);
|
|
memoLogs.Lines.BeginUpdate;
|
|
try
|
|
for i := TempList.Count - 1 downto 0 do
|
|
begin
|
|
if Trim(TempList[i]) <> '' then
|
|
memoLogs.Lines.Add(TempList[i]);
|
|
end;
|
|
finally
|
|
memoLogs.Lines.EndUpdate;
|
|
end;
|
|
except
|
|
memoLogs.Lines.Add('Failed to load log file.');
|
|
end;
|
|
finally
|
|
TempList.Free;
|
|
end;
|
|
end
|
|
else
|
|
begin
|
|
if FIsKorean then
|
|
memoLogs.Lines.Add(FormatDateTime('yyyy-mm-dd', TargetDate) + ' 날짜의 로그가 없습니다.')
|
|
else
|
|
memoLogs.Lines.Add('No logs available for ' + FormatDateTime('yyyy-mm-dd', TargetDate));
|
|
end;
|
|
end;
|
|
|
|
procedure TfrmMain.BuildLogUI;
|
|
var
|
|
lytLogHeader: TLayout;
|
|
begin
|
|
// 화면 레이아웃
|
|
lytScreenLogs := TLayout.Create(Self);
|
|
lytScreenLogs.Parent := lytMainContent;
|
|
lytScreenLogs.Align := TAlignLayout.Client;
|
|
lytScreenLogs.Visible := False;
|
|
|
|
// 상단 헤더 (날짜 선택 및 새로고침)
|
|
lytLogHeader := TLayout.Create(Self);
|
|
lytLogHeader.Parent := lytScreenLogs;
|
|
lytLogHeader.Align := TAlignLayout.Top;
|
|
lytLogHeader.Height := 70;
|
|
lytLogHeader.Margins.Rect := RectF(0, 10, 0, 10);
|
|
|
|
lblLogDate := BuildLabel(lytLogHeader, '로그 날짜 선택:', 26, TAlignLayout.None, claWhite, True);
|
|
lblLogDate.SetBounds(0, 0, 200, 70);
|
|
|
|
dateLog := BuildDateEdit(lytLogHeader, Now, 32, TAlignLayout.None, claBlack, False);
|
|
dateLog.SetBounds(240, 10, 250, 50);
|
|
{$IFDEF ANDROID}
|
|
dateLog.TextSettings.FontColor := claWhite;
|
|
{$ENDIF}
|
|
dateLog.OnChange := OnRefreshLogsClick; // 날짜 변경 시 자동 새로고침
|
|
|
|
btnRefreshLogs := CreateCustomButton(lytLogHeader, '로그 불러오기 (Load)', TAlignLayout.None, 930, OnRefreshLogsClick);
|
|
btnRefreshLogs.SetBounds(540, 10, 930, 50);
|
|
TLabel(btnRefreshLogs.Controls[0]).TextSettings.Font.Size := 26;
|
|
|
|
memoLogs := TMemo.Create(Self);
|
|
memoLogs.Parent := lytScreenLogs;
|
|
memoLogs.Align := TAlignLayout.Client;
|
|
memoLogs.TextSettings.Font.Size := 16;
|
|
memoLogs.ReadOnly := True;
|
|
|
|
memoLogs.StyledSettings := memoLogs.StyledSettings - [TStyledSetting.FontColor, TStyledSetting.Size];
|
|
|
|
// memoLogs.WordWrap := True; // TMemo WordWrap depends on Delphi version, it's safe not to enforce it heavily if not needed for plain text
|
|
end;
|
|
|
|
|
|
procedure TfrmMain.TimerTimeTimer(Sender: TObject);
|
|
begin
|
|
lblDateTime.Text := FormatDateTime('yyyy-mm-dd hh:nn:ss', Now);
|
|
end;
|
|
|
|
|
|
procedure TfrmMain.TimerStartAutoCKTimer(Sender: TObject);
|
|
begin
|
|
if FAutoMode then begin
|
|
TimerStartAutoCK.Enabled := false;
|
|
SaveAutoRules;
|
|
LoadAutoRules;
|
|
UpdateAutoModeBtnUI;
|
|
TimerAuto.Enabled := FAutoMode;
|
|
end
|
|
else
|
|
TimerStartAutoCK.Enabled := false;
|
|
end;
|
|
|
|
procedure TfrmMain.OnMQTTStatus(AConnected: Boolean);
|
|
begin
|
|
if AConnected then
|
|
begin
|
|
lblConnectionStatus.Tag := 1;
|
|
lblConnectionStatus.TextSettings.FontColor := $FF4CAF50;
|
|
if FIsKorean then
|
|
lblConnectionStatus.Text := '연결됨'
|
|
else
|
|
lblConnectionStatus.Text := 'Connected';
|
|
|
|
AddSystemLog('시스템 설정 변경: MQTT Broker 접속정보 [ 연결됨 ] 수정됨');
|
|
|
|
TimerStartAutoCK.Enabled := True;
|
|
end
|
|
else
|
|
begin
|
|
FMQTTMessageRE := False;
|
|
lblConnectionStatus.Tag := 2;
|
|
lblConnectionStatus.TextSettings.FontColor := $FFFF5252;
|
|
if FIsKorean then
|
|
lblConnectionStatus.Text := '연결 끊김'
|
|
else
|
|
lblConnectionStatus.Text := 'Disconnected';
|
|
|
|
AddSystemLog('시스템 설정 변경: MQTT Broker 접속정보 [ 연결 끊김 ] 수정됨');
|
|
end;
|
|
end;
|
|
|
|
function TfrmMain.ParsePipeValue(const AData: string; AIndex: Integer): Double;
|
|
var
|
|
Parts: TArray<string>;
|
|
begin
|
|
Result := 0;
|
|
Parts := AData.Split(['|']);
|
|
if (AIndex >= 0) and (AIndex < Length(Parts)) then
|
|
begin
|
|
if not TryStrToFloat(Parts[AIndex], Result) then
|
|
Result := 0;
|
|
end;
|
|
Result := Result / 10.0;
|
|
end;
|
|
|
|
procedure TfrmMain.PublishControl(AIndex: Integer; AOn: Boolean);
|
|
var
|
|
DOKey, DOVal, MsgPayload: string;
|
|
begin
|
|
DOKey := Format('DO_%2.2d', [AIndex]);
|
|
if AOn then DOVal := '1' else DOVal := '0';
|
|
MsgPayload := '{"' + DOKey + '":"' + DOVal + '"}';
|
|
|
|
if (FMQTTClient <> nil) and FMQTTClient.Connected then
|
|
begin
|
|
FMQTTClient.Publish(FMQTTTopicHEAD+'SUB/'+FMQTTTopicTAIL+FMQTTGateWayID, MsgPayload);
|
|
AddSystemLog('제어 명령 전송 - ' + DOKey + ': ' + (if AOn then 'ON' else 'OFF'));
|
|
end;
|
|
end;
|
|
|
|
procedure TfrmMain.OnMQTTMessage(const ATopic, APayload: string);
|
|
var
|
|
JObj: TJSONObject;
|
|
Val, strN: string;
|
|
i, j: Integer;
|
|
begin
|
|
try
|
|
JObj := TJSONObject.ParseJSONValue(APayload) as TJSONObject;
|
|
if JObj = nil then Exit;
|
|
try
|
|
// Apply updates matching node configurations dynamically
|
|
for i := 0 to High(FNodeConfigs) do
|
|
begin
|
|
if not FNodeConfigs[i].InUse then Continue;
|
|
|
|
if (FNodeConfigs[i].NodeType = 'DO') or (FNodeConfigs[i].NodeType = 'DI') then
|
|
begin
|
|
if JObj.TryGetValue<string>(FNodeConfigs[i].NodeType, Val) then
|
|
begin
|
|
FUpdatingControls := True;
|
|
try
|
|
for j := 0 to High(Controls) do
|
|
begin
|
|
if Controls[j].NodeID = FNodeConfigs[i].ID then
|
|
begin
|
|
Controls[j].UpdateStatus((ParsePipeValue(Val, FNodeConfigs[i].Index) * 10) > 0);
|
|
|
|
if FNodeConfigs[i].NodeType = 'DI' then begin
|
|
strN := FNodeConfigs[i].CustomName + FNodeConfigs[i].TitleKO + FNodeConfigs[i].TitleEN;
|
|
if (strN.Contains('유량')) or (strN.Contains('Flow')) then begin
|
|
if (ParsePipeValue(Val, FNodeConfigs[i].Index) * 10) > 0 then begin
|
|
FSensorValues[i] := FSensorValues[i] + (ParsePipeValue(Val, FNodeConfigs[i].Index) * 10);
|
|
|
|
FSensorValuesTotalFlow[i] := FSensorValuesTotalFlow[i] + (ParsePipeValue(Val, FNodeConfigs[i].Index) * 10);
|
|
|
|
AddSystemLog('유량계 (펄스입력) [' + FNodeConfigs[i].ID +'/'+FloatToStr(FSensorValues[i])+'/'+FloatToStr(FSensorValuesTotalFlow[i])+ '] -> ' + FloatToStr(ParsePipeValue(Val, FNodeConfigs[i].Index) * 10));
|
|
|
|
// 추후 삭제 (펄스값 확인용)
|
|
if Label1.Visible then begin
|
|
Label1.Text := FloatToStr(ParsePipeValue(Val, FNodeConfigs[i].Index) * 10);
|
|
Label2.Text := FloatToStr(FSensorValues[i]);
|
|
Label3.Text := FloatToStr(FSensorValuesTotalFlow[i]);
|
|
end;
|
|
end
|
|
else begin
|
|
// 추후 삭제 (펄스값 확인용)
|
|
if Label1.Visible then begin
|
|
Label1.Text := FloatToStr(ParsePipeValue(Val, FNodeConfigs[i].Index) * 10);
|
|
Label2.Text := FloatToStr(FSensorValues[i]);
|
|
Label3.Text := FloatToStr(FSensorValuesTotalFlow[i]);
|
|
end;
|
|
end;
|
|
end;
|
|
end;
|
|
|
|
if FloatToStr(FSensorValues[i]) <> FloatToStr((ParsePipeValue(Val, FNodeConfigs[i].Index) * 10)) then
|
|
begin
|
|
if FNodeConfigs[i].NodeType = 'DO' then begin
|
|
AddSystemLog('장비 상태 변경 [' + FNodeConfigs[i].ID +'/'+FloatToStr(FSensorValues[i])+'/'+FloatToStr(ParsePipeValue(Val, FNodeConfigs[i].Index) * 10)+ '] -> ' + (if (ParsePipeValue(Val, FNodeConfigs[i].Index) * 10) > 0 then 'ON' else 'OFF'));
|
|
FSensorValues[i] := (ParsePipeValue(Val, FNodeConfigs[i].Index) * 10);
|
|
end
|
|
else if FNodeConfigs[i].NodeType = 'DI' then begin
|
|
strN := FNodeConfigs[i].CustomName + FNodeConfigs[i].TitleKO + FNodeConfigs[i].TitleEN;
|
|
if not ((strN.Contains('유량')) or (strN.Contains('Flow'))) then begin
|
|
AddSystemLog('입력 상태 변경 [' + FNodeConfigs[i].ID +'/'+FloatToStr(FSensorValues[i])+'/'+FloatToStr(ParsePipeValue(Val, FNodeConfigs[i].Index) * 10)+ '] -> ' + (if (ParsePipeValue(Val, FNodeConfigs[i].Index) * 10) > 0 then 'ON' else 'OFF'));
|
|
FSensorValues[i] := (ParsePipeValue(Val, FNodeConfigs[i].Index) * 10);
|
|
end;
|
|
end;
|
|
end;
|
|
|
|
// Update pending status check
|
|
if FControlPending[FNodeConfigs[i].Index] then
|
|
begin
|
|
if ((ParsePipeValue(Val, FNodeConfigs[i].Index) * 10) > 0) = FControlDesired[FNodeConfigs[i].Index] then
|
|
FControlPending[FNodeConfigs[i].Index] := False;
|
|
end;
|
|
Break;
|
|
end;
|
|
end;
|
|
finally
|
|
FUpdatingControls := False;
|
|
end;
|
|
end;
|
|
end
|
|
else
|
|
begin
|
|
if JObj.TryGetValue<string>(FNodeConfigs[i].NodeType, Val) then
|
|
begin
|
|
for j := 0 to High(Sensors) do
|
|
begin
|
|
if Sensors[j].NodeID = FNodeConfigs[i].ID then
|
|
begin
|
|
if FSensorValues[i] <> ParsePipeValue(Val, FNodeConfigs[i].Index) then
|
|
FSensorValues[i] := ParsePipeValue(Val, FNodeConfigs[i].Index);
|
|
|
|
Sensors[j].UpdateValue(ParsePipeValue(Val, FNodeConfigs[i].Index));
|
|
Break;
|
|
end;
|
|
end;
|
|
end;
|
|
end;
|
|
end;
|
|
|
|
FMQTTMessageRE := True;
|
|
finally
|
|
JObj.Free;
|
|
end;
|
|
except
|
|
end;
|
|
end;
|
|
|
|
procedure TfrmMain.InitNodeConfigs;
|
|
var
|
|
curIdx: Integer;
|
|
procedure AddNodes(const AType, AKo, AEn: string; ACount: Integer; IsZeroBased: Boolean = True);
|
|
var
|
|
j, idxNum: Integer;
|
|
begin
|
|
for j := 0 to ACount - 1 do
|
|
begin
|
|
if IsZeroBased then idxNum := j else idxNum := j + 1;
|
|
FNodeConfigs[curIdx].ID := Format('%s_%2.2d', [AType, idxNum]);
|
|
FNodeConfigs[curIdx].NodeType := AType;
|
|
FNodeConfigs[curIdx].Index := idxNum;
|
|
FNodeConfigs[curIdx].TitleKO := Format('%s %d', [AKo, j + 1]);
|
|
FNodeConfigs[curIdx].TitleEN := Format('%s %d', [AEn, j + 1]);
|
|
FNodeConfigs[curIdx].CustomName := '';
|
|
FNodeConfigs[curIdx].InUse := False;
|
|
Inc(curIdx);
|
|
end;
|
|
end;
|
|
begin
|
|
SetLength(FNodeConfigs, 86);
|
|
curIdx := 0;
|
|
AddNodes('DI', '입력', 'Digital In', 16, True);
|
|
AddNodes('DO', '출력', 'Digital Out', 16, True);
|
|
AddNodes('AI', '아날로그', 'Analog In', 4, True);
|
|
AddNodes('HM', '습도', 'Humidity', 5, True);
|
|
AddNodes('TM', '온도', 'Temperature', 5, True);
|
|
AddNodes('CD', 'CO2', 'CO2', 5, True);
|
|
AddNodes('NT', 'EC', 'EC', 5, True);
|
|
AddNodes('PH', 'pH', 'pH', 5, True);
|
|
AddNodes('GT', '토양 온도 1', 'Soil Temp 1', 5, True);
|
|
AddNodes('GM', '토양 습도', 'Soil Moisture', 5, True);
|
|
AddNodes('GNT', '토양 EC', 'Soil EC', 5, True);
|
|
AddNodes('WP', '토양 pH', 'Soil pH', 5, True);
|
|
AddNodes('GT2', '토양 온도 2', 'Soil Temp 2', 5, True);
|
|
end;
|
|
|
|
procedure TfrmMain.BuildNodeSettingsUI;
|
|
var
|
|
i, j: Integer;
|
|
ItemLyt, HeaderLyt: TLayout;
|
|
Lbl, HeaderLbl: TLabel;
|
|
CurrentType: string;
|
|
SelTxt: string;
|
|
LItem: TListBoxItem;
|
|
begin
|
|
if lytNodesMain = nil then Exit;
|
|
while lytNodesMain.ControlsCount > 0 do lytNodesMain.Controls[0].Free;
|
|
|
|
CurrentType := '';
|
|
|
|
for i := 0 to High(FNodeConfigs) do
|
|
begin
|
|
if CurrentType <> FNodeConfigs[i].NodeType then
|
|
begin
|
|
CurrentType := FNodeConfigs[i].NodeType;
|
|
|
|
// FMX FlowLayout Break before group
|
|
var LBreakBefore := TFlowLayoutBreak.Create(lytNodesMain);
|
|
LBreakBefore.Parent := lytNodesMain;
|
|
LBreakBefore.Tag := -2; // Identifier for layour breaks
|
|
|
|
// Inject Group Header
|
|
HeaderLyt := TLayout.Create(lytNodesMain);
|
|
HeaderLyt.Parent := lytNodesMain;
|
|
HeaderLyt.Tag := -1; // Identifier for header layout
|
|
HeaderLyt.Height := 50;
|
|
HeaderLyt.Width := 1000;
|
|
HeaderLyt.Margins.Rect := RectF(5, 20, 5, 20); // Spacing before and after new group
|
|
|
|
|
|
HeaderLbl := TLabel.Create(HeaderLyt);
|
|
HeaderLbl.Parent := HeaderLyt;
|
|
HeaderLbl.Align := TAlignLayout.Client;
|
|
HeaderLbl.TextSettings.Font.Size := 28;
|
|
HeaderLbl.TextSettings.Font.Style := [TFontStyle.fsBold];
|
|
HeaderLbl.TextSettings.FontColor := $FF4CAF50; // Green emphasis
|
|
HeaderLbl.StyledSettings := HeaderLbl.StyledSettings - [TStyledSetting.Size, TStyledSetting.FontColor, TStyledSetting.Style];
|
|
HeaderLbl.TextSettings.HorzAlign := TTextAlign.Leading;
|
|
HeaderLbl.TextSettings.VertAlign := TTextAlign.Trailing; // Align text to bottom of header
|
|
HeaderLbl.Text := GetGroupName(CurrentType, FIsKorean);
|
|
HeaderLbl.Hint := CurrentType; // Store node type for later translation
|
|
|
|
var LBreakAfter := TFlowLayoutBreak.Create(lytNodesMain);
|
|
LBreakAfter.Parent := lytNodesMain;
|
|
LBreakAfter.Tag := -2; // identifier for layout break
|
|
end;
|
|
|
|
ItemLyt := TLayout.Create(lytNodesMain);
|
|
ItemLyt.Parent := lytNodesMain;
|
|
|
|
ItemLyt.Width := 700;
|
|
ItemLyt.Height := 80;
|
|
|
|
FNodeConfigs[i].SettingRect := BuildWidgetBg(ItemLyt);
|
|
FNodeConfigs[i].SettingRect.Parent := ItemLyt;
|
|
FNodeConfigs[i].SettingRect.Align := TAlignLayout.Client;
|
|
FNodeConfigs[i].SettingRect.Margins.Rect := RectF(5, 5, 5, 5);
|
|
|
|
Lbl := TLabel.Create(FNodeConfigs[i].SettingRect);
|
|
Lbl.Parent := FNodeConfigs[i].SettingRect;
|
|
Lbl.Align := TAlignLayout.Client;
|
|
Lbl.Margins.Rect := RectF(15, 0, 10, 0);
|
|
Lbl.StyledSettings := Lbl.StyledSettings - [TStyledSetting.Size, TStyledSetting.FontColor, TStyledSetting.Style];
|
|
Lbl.TextSettings.Font.Size := 24;
|
|
Lbl.TextSettings.FontColor := claWhite;
|
|
Lbl.TextSettings.HorzAlign := TTextAlign.Leading;
|
|
if FIsKorean then
|
|
Lbl.Text := FNodeConfigs[i].TitleKO + ' [' + FNodeConfigs[i].ID + ']'
|
|
else
|
|
Lbl.Text := FNodeConfigs[i].TitleEN + ' [' + FNodeConfigs[i].ID + ']';
|
|
|
|
FNodeConfigs[i].SettingEdit := TEdit.Create(FNodeConfigs[i].SettingRect);
|
|
FNodeConfigs[i].SettingEdit.Parent := FNodeConfigs[i].SettingRect;
|
|
FNodeConfigs[i].SettingEdit.Align := TAlignLayout.Right;
|
|
FNodeConfigs[i].SettingEdit.Margins.Rect := RectF(0, 15, 15, 15);
|
|
FNodeConfigs[i].SettingEdit.Width := 170;
|
|
FNodeConfigs[i].SettingEdit.Tag := i;
|
|
FNodeConfigs[i].SettingEdit.Text := FNodeConfigs[i].CustomName;
|
|
FNodeConfigs[i].SettingEdit.TextPrompt := '위젯명 (선택)';
|
|
FNodeConfigs[i].SettingEdit.StyledSettings := FNodeConfigs[i].SettingEdit.StyledSettings - [TStyledSetting.Size, TStyledSetting.FontColor];
|
|
FNodeConfigs[i].SettingEdit.TextSettings.Font.Size := 24;
|
|
{$IFDEF ANDROID}
|
|
FNodeConfigs[i].SettingEdit.TextSettings.FontColor := claWhite;
|
|
{$ENDIF}
|
|
FNodeConfigs[i].SettingEdit.OnExit := OnNodeNameExit;
|
|
|
|
FNodeConfigs[i].SettingIconCombo := TComboBox.Create(FNodeConfigs[i].SettingRect);
|
|
FNodeConfigs[i].SettingIconCombo.Parent := FNodeConfigs[i].SettingRect;
|
|
FNodeConfigs[i].SettingIconCombo.Align := TAlignLayout.Right;
|
|
FNodeConfigs[i].SettingIconCombo.Margins.Rect := RectF(0, 10, 10, 10);
|
|
FNodeConfigs[i].SettingIconCombo.Width := 110;
|
|
FNodeConfigs[i].SettingIconCombo.Tag := i;
|
|
FNodeConfigs[i].SettingIconCombo.ItemHeight := 24 * 1.8;
|
|
{$IFNDEF ANDROID}
|
|
FNodeConfigs[i].SettingIconCombo.DropDownKind := TDropDownKind.Custom;
|
|
{$ENDIF}
|
|
|
|
// Create Custom Styled Dropdown Items
|
|
for j := 0 to 12 do
|
|
begin
|
|
case j of
|
|
0: SelTxt := '기본';
|
|
1: SelTxt := '🎛️';
|
|
2: SelTxt := '🌡️';
|
|
3: SelTxt := '💧';
|
|
4: SelTxt := '💨';
|
|
5: SelTxt := '☀️';
|
|
6: SelTxt := '🌱';
|
|
7: SelTxt := '⚡';
|
|
8: SelTxt := '🧪';
|
|
9: SelTxt := '📊';
|
|
10: SelTxt := '📡';
|
|
11: SelTxt := '💡';
|
|
12: SelTxt := '🔔';
|
|
end;
|
|
LItem := TListBoxItem.Create(FNodeConfigs[i].SettingIconCombo);
|
|
LItem.Parent := FNodeConfigs[i].SettingIconCombo;
|
|
LItem.Text := SelTxt;
|
|
LItem.Height := 24 * 1.8;
|
|
LItem.StyledSettings := LItem.StyledSettings - [TStyledSetting.Size, TStyledSetting.FontColor];
|
|
LItem.TextSettings.Font.Size := 24;
|
|
{$IFDEF ANDROID}
|
|
LItem.TextSettings.FontColor := claWhite;
|
|
{$ENDIF}
|
|
FNodeConfigs[i].SettingIconCombo.AddObject(LItem);
|
|
end;
|
|
|
|
if FNodeConfigs[i].CustomIcon = '' then
|
|
FNodeConfigs[i].SettingIconCombo.ItemIndex := 0
|
|
else
|
|
FNodeConfigs[i].SettingIconCombo.ItemIndex := FNodeConfigs[i].SettingIconCombo.Items.IndexOf(FNodeConfigs[i].CustomIcon);
|
|
|
|
FNodeConfigs[i].SettingIconCombo.OnChange := OnNodeIconChange;
|
|
|
|
FNodeConfigs[i].SettingSwitch := TSwitch.Create(FNodeConfigs[i].SettingRect);
|
|
FNodeConfigs[i].SettingSwitch.Parent := FNodeConfigs[i].SettingRect;
|
|
FNodeConfigs[i].SettingSwitch.Align := TAlignLayout.Right;
|
|
FNodeConfigs[i].SettingSwitch.Margins.Rect := RectF(0, 10, 15, 10);
|
|
FNodeConfigs[i].SettingSwitch.Width := 100;
|
|
{$IFDEF ANDROID}
|
|
FNodeConfigs[i].SettingSwitch.Scale.X := 1.5;
|
|
FNodeConfigs[i].SettingSwitch.Scale.Y := 1.5;
|
|
{$ENDIF}
|
|
FNodeConfigs[i].SettingSwitch.Tag := i;
|
|
FNodeConfigs[i].SettingSwitch.IsChecked := FNodeConfigs[i].InUse;
|
|
FNodeConfigs[i].SettingSwitch.OnSwitch := OnNodeSettingToggle;
|
|
end;
|
|
end;
|
|
|
|
procedure TfrmMain.RebuildDashboardUI;
|
|
var
|
|
i: Integer;
|
|
DefIcon: string;
|
|
begin
|
|
if lytWidgets = nil then Exit;
|
|
while lytWidgets.ControlsCount > 0 do lytWidgets.Controls[0].Free;
|
|
|
|
SetLength(Sensors, 0);
|
|
SetLength(Controls, 0);
|
|
|
|
for i := 0 to High(FNodeConfigs) do
|
|
begin
|
|
if FNodeConfigs[i].InUse then
|
|
begin
|
|
DefIcon := '📡'; // Base fallback
|
|
|
|
if (FNodeConfigs[i].NodeType = 'DO') or (FNodeConfigs[i].NodeType = 'DI') then
|
|
DefIcon := '🎛️'
|
|
else if FNodeConfigs[i].NodeType = 'TM' then DefIcon := '🌡️'
|
|
else if FNodeConfigs[i].NodeType = 'HM' then DefIcon := '💧'
|
|
else if FNodeConfigs[i].NodeType = 'CD' then DefIcon := '💨'
|
|
else if (FNodeConfigs[i].NodeType = 'GT') or (FNodeConfigs[i].NodeType = 'GT2') then DefIcon := '🌡️'
|
|
else if FNodeConfigs[i].NodeType = 'GM' then DefIcon := '🌱'
|
|
else if (FNodeConfigs[i].NodeType = 'NT') or (FNodeConfigs[i].NodeType = 'GNT') then DefIcon := '⚡'
|
|
else if (FNodeConfigs[i].NodeType = 'PH') or (FNodeConfigs[i].NodeType = 'WP') then DefIcon := '🧪'
|
|
else if FNodeConfigs[i].NodeType = 'AI' then DefIcon := '📊';
|
|
|
|
// Override with custom icon if provided
|
|
if FNodeConfigs[i].CustomIcon <> '' then
|
|
DefIcon := FNodeConfigs[i].CustomIcon;
|
|
|
|
if (FNodeConfigs[i].NodeType = 'DO') or (FNodeConfigs[i].NodeType = 'DI') then
|
|
AddControl(FNodeConfigs[i], DefIcon)
|
|
else
|
|
begin
|
|
if FNodeConfigs[i].NodeType = 'TM' then AddSensor(FNodeConfigs[i], DefIcon, '°C', 50.0)
|
|
else if FNodeConfigs[i].NodeType = 'HM' then AddSensor(FNodeConfigs[i], DefIcon, '%', 100.0)
|
|
else if FNodeConfigs[i].NodeType = 'CD' then AddSensor(FNodeConfigs[i], DefIcon, 'ppm', 2000.0)
|
|
else if FNodeConfigs[i].NodeType = 'GT' then AddSensor(FNodeConfigs[i], DefIcon, '°C', 50.0) //°C
|
|
else if FNodeConfigs[i].NodeType = 'GM' then AddSensor(FNodeConfigs[i], DefIcon, '%', 100.0)
|
|
else if (FNodeConfigs[i].NodeType = 'NT') or (FNodeConfigs[i].NodeType = 'GNT') then AddSensor(FNodeConfigs[i], DefIcon, 'dS/m', 10.0)
|
|
else if (FNodeConfigs[i].NodeType = 'PH') or (FNodeConfigs[i].NodeType = 'WP') then AddSensor(FNodeConfigs[i], DefIcon, 'pH', 14.0)
|
|
else if FNodeConfigs[i].NodeType = 'AI' then AddSensor(FNodeConfigs[i], DefIcon, 'V', 10.0)
|
|
else if FNodeConfigs[i].NodeType = 'GT2' then AddSensor(FNodeConfigs[i], DefIcon, '°C', 50.0)
|
|
else AddSensor(FNodeConfigs[i], DefIcon, '', 100.0);
|
|
end;
|
|
end;
|
|
end;
|
|
FormResize(Self);
|
|
ApplyLanguage; // Always update labels after rebuilding widgets
|
|
end;
|
|
|
|
procedure TfrmMain.OnNodeSettingToggle(Sender: TObject);
|
|
var
|
|
Idx: Integer;
|
|
begin
|
|
if (Sender is TSwitch) and not FUpdatingControls then
|
|
begin
|
|
Idx := TSwitch(Sender).Tag;
|
|
if (Idx >= 0) and (Idx <= High(FNodeConfigs)) then
|
|
begin
|
|
FNodeConfigs[Idx].InUse := TSwitch(Sender).IsChecked;
|
|
SaveSetting_each('bool', 'Nodes', FNodeConfigs[Idx].ID, BoolToStr(FNodeConfigs[Idx].InUse));
|
|
RebuildDashboardUI; // ApplyLanguage is called within RebuildDashboardUI
|
|
AddSystemLog('시스템 설정 변경: 노드 사용설정 변경 [' + FNodeConfigs[Idx].ID +' / '+BoolToStr(FNodeConfigs[Idx].InUse)+ '] 사용 속성 수정됨');
|
|
end;
|
|
end;
|
|
end;
|
|
|
|
procedure TfrmMain.OnNodeNameExit(Sender: TObject);
|
|
var
|
|
Idx: Integer;
|
|
begin
|
|
if Sender is TEdit then
|
|
begin
|
|
Idx := TEdit(Sender).Tag;
|
|
if (Idx >= 0) and (Idx <= High(FNodeConfigs)) then
|
|
begin
|
|
if FNodeConfigs[Idx].CustomName <> Trim(TEdit(Sender).Text) then
|
|
begin
|
|
FNodeConfigs[Idx].CustomName := Trim(TEdit(Sender).Text);
|
|
SaveSetting_each('string', 'NodeNames', FNodeConfigs[Idx].ID, FNodeConfigs[Idx].CustomName);
|
|
RebuildDashboardUI;
|
|
ApplyLanguage; // Update immediate UI texts
|
|
AddSystemLog('시스템 설정 변경: 노드 위젯명 변경 [' + FNodeConfigs[Idx].ID +' / '+FNodeConfigs[Idx].CustomName+ '] 위젯명 수정됨');
|
|
end;
|
|
end;
|
|
end;
|
|
end;
|
|
|
|
procedure TfrmMain.OnNodeIconChange(Sender: TObject);
|
|
var
|
|
Idx: Integer;
|
|
Cb: TComboBox;
|
|
SelTxt: string;
|
|
begin
|
|
if Sender is TComboBox then
|
|
begin
|
|
Cb := TComboBox(Sender);
|
|
Idx := Cb.Tag;
|
|
if (Idx >= 0) and (Idx <= High(FNodeConfigs)) then
|
|
begin
|
|
if Cb.ItemIndex > 0 then
|
|
SelTxt := Cb.Items[Cb.ItemIndex]
|
|
else
|
|
SelTxt := ''; // Default
|
|
|
|
if FNodeConfigs[Idx].CustomIcon <> SelTxt then
|
|
begin
|
|
FNodeConfigs[Idx].CustomIcon := SelTxt;
|
|
SaveSetting_each('integer', 'NodeIcons', FNodeConfigs[Idx].ID, IntToStr(GetIconIndex(FNodeConfigs[Idx].CustomIcon)));
|
|
RebuildDashboardUI; // ApplyLanguage is called within RebuildDashboardUI
|
|
AddSystemLog('시스템 설정 변경: 노드 아이콘 변경 [' + FNodeConfigs[Idx].ID +' / '+SelTxt+ '] 아이콘 변경됨');
|
|
end;
|
|
end;
|
|
end;
|
|
end;
|
|
|
|
procedure TfrmMain.BuildMQTTSetupPopupUI;
|
|
var
|
|
Bg, Dlg: TRectangle;
|
|
BottomLyt: TLayout;
|
|
BtnAddClose: TRectangle;
|
|
EdGateWayID, EdBrokerIP, EdBrokerPORT, EdUserID, EdUserPASS, EdTopicHEAD, EdTopicTAIL: TEdit;
|
|
begin
|
|
lytMQTTSetupPopup := TLayout.Create(Self);
|
|
lytMQTTSetupPopup.Parent := lytMainContent;
|
|
lytMQTTSetupPopup.Align := TAlignLayout.Client;
|
|
lytMQTTSetupPopup.Visible := False;
|
|
|
|
Bg := TRectangle.Create(Self);
|
|
Bg.Parent := lytMQTTSetupPopup;
|
|
Bg.Align := TAlignLayout.Client;
|
|
Bg.Fill.Color := $B0000000; // Semi-transparent black
|
|
Bg.Stroke.Kind := TBrushKind.None;
|
|
Bg.HitTest := True;
|
|
|
|
Dlg := TRectangle.Create(Self);
|
|
Dlg.Parent := lytMQTTSetupPopup;
|
|
Dlg.Align := TAlignLayout.Center;
|
|
Dlg.Width := 750;
|
|
Dlg.Height := 800;
|
|
Dlg.Fill.Color := $FF222222;
|
|
Dlg.Stroke.Color := $FF444444;
|
|
Dlg.XRadius := 10; Dlg.YRadius := 10;
|
|
|
|
lblMQTTSetupTitle := BuildLabel(Dlg, '연결 설정 [ MQTTBroker Setting ]', 24, TAlignLayout.Top, claWhite, True);
|
|
lblMQTTSetupTitle.Height := 65;
|
|
lblMQTTSetupTitle.Margins.Rect := RectF(16, 16, 16, 0);
|
|
|
|
scrMQTTSetup := TScrollBox.Create(Self);
|
|
scrMQTTSetup.Parent := Dlg;
|
|
scrMQTTSetup.Align := TAlignLayout.Client;
|
|
scrMQTTSetup.Margins.Rect := RectF(16, 16, 16, 16);
|
|
|
|
// ── MQTTBroker Setting - GateWayID ──────────────────────────────────────────────
|
|
BuildLabel(Dlg, 'GateWayID :', 22, TAlignLayout.None, claLightgray, False).SetBounds(50, 120, 150, 30);
|
|
EdGateWayID := TEdit.Create(Self);
|
|
EdGateWayID.Parent := Dlg;
|
|
EdGateWayID.SetBounds(250, 120, 400, 30);
|
|
EdGateWayID.Text := FMQTTGateWayID;
|
|
EdGateWayID.Tag := 1;
|
|
EdGateWayID.TagFloat := 1;
|
|
EdGateWayID.StyledSettings := EdGateWayID.StyledSettings - [TStyledSetting.Size, TStyledSetting.FontColor];
|
|
EdGateWayID.TextSettings.Font.Size := 22;
|
|
{$IFDEF ANDROID}
|
|
EdGateWayID.TextSettings.FontColor := claWhite;
|
|
{$ENDIF}
|
|
EdGateWayID.OnExit := OnMQTTSetupEdit;
|
|
|
|
// ── MQTTBroker Setting - IP ──────────────────────────────────────────────
|
|
BuildLabel(Dlg, 'IP :', 22, TAlignLayout.None, claLightgray, False).SetBounds(50, 190, 150, 30);
|
|
EdBrokerIP := TEdit.Create(Self);
|
|
EdBrokerIP.Parent := Dlg;
|
|
EdBrokerIP.SetBounds(250, 190, 400, 30);
|
|
EdBrokerIP.Text := FMQTTBrokerIP;
|
|
EdBrokerIP.Tag := 2;
|
|
EdBrokerIP.TagFloat := 2;
|
|
EdBrokerIP.StyledSettings := EdBrokerIP.StyledSettings - [TStyledSetting.Size, TStyledSetting.FontColor];
|
|
EdBrokerIP.TextSettings.Font.Size := 22;
|
|
{$IFDEF ANDROID}
|
|
EdBrokerIP.TextSettings.FontColor := claWhite;
|
|
{$ENDIF}
|
|
EdBrokerIP.OnExit := OnMQTTSetupEdit;
|
|
|
|
// ── MQTTBroker Setting - PORT ──────────────────────────────────────────────
|
|
BuildLabel(Dlg, 'PORT :', 22, TAlignLayout.None, claLightgray, False).SetBounds(50, 260, 150, 30);
|
|
EdBrokerPORT := TEdit.Create(Self);
|
|
EdBrokerPORT.Parent := Dlg;
|
|
EdBrokerPORT.SetBounds(250, 260, 400, 30);
|
|
EdBrokerPORT.Text := IntToStr(FMQTTBrokerPORT);
|
|
EdBrokerPORT.Tag := 3;
|
|
EdBrokerPORT.TagFloat := 3;
|
|
EdBrokerPORT.StyledSettings := EdBrokerPORT.StyledSettings - [TStyledSetting.Size, TStyledSetting.FontColor];
|
|
EdBrokerPORT.TextSettings.Font.Size := 22;
|
|
{$IFDEF ANDROID}
|
|
EdBrokerPORT.TextSettings.FontColor := claWhite;
|
|
{$ENDIF}
|
|
EdBrokerPORT.OnExit := OnMQTTSetupEdit;
|
|
|
|
// ── MQTTBroker Setting - USERID ──────────────────────────────────────────────
|
|
BuildLabel(Dlg, 'USERID :', 22, TAlignLayout.None, claLightgray, False).SetBounds(50, 330, 150, 30);
|
|
EdUserID := TEdit.Create(Self);
|
|
EdUserID.Parent := Dlg;
|
|
EdUserID.SetBounds(250, 330, 400, 30);
|
|
EdUserID.Text := FMQTTUserID;
|
|
EdUserID.Tag := 4;
|
|
EdUserID.TagFloat := 4;
|
|
EdUserID.StyledSettings := EdUserID.StyledSettings - [TStyledSetting.Size, TStyledSetting.FontColor];
|
|
EdUserID.TextSettings.Font.Size := 22;
|
|
{$IFDEF ANDROID}
|
|
EdUserID.TextSettings.FontColor := claWhite;
|
|
{$ENDIF}
|
|
EdUserID.OnExit := OnMQTTSetupEdit;
|
|
|
|
// ── MQTTBroker Setting - USERPASS ──────────────────────────────────────────────
|
|
BuildLabel(Dlg, 'USERPASS :', 22, TAlignLayout.None, claLightgray, False).SetBounds(50, 400, 150, 30);
|
|
EdUserPASS := TEdit.Create(Self);
|
|
EdUserPASS.Parent := Dlg;
|
|
EdUserPASS.SetBounds(250, 400, 400, 30);
|
|
EdUserPASS.Text := FMQTTUserPASS;
|
|
EdUserPASS.PasswordChar := '*';
|
|
EdUserPASS.Password := True;
|
|
EdUserPASS.Tag := 5;
|
|
EdUserPASS.TagFloat := 5;
|
|
EdUserPASS.StyledSettings := EdUserPASS.StyledSettings - [TStyledSetting.Size, TStyledSetting.FontColor];
|
|
EdUserPASS.TextSettings.Font.Size := 22;
|
|
{$IFDEF ANDROID}
|
|
EdUserPASS.TextSettings.FontColor := claWhite;
|
|
{$ENDIF}
|
|
EdUserPASS.OnExit := OnMQTTSetupEdit;
|
|
|
|
// ── MQTTBroker Setting - TopicHEAD ──────────────────────────────────────────────
|
|
BuildLabel(Dlg, 'TopicHEAD :', 22, TAlignLayout.None, claLightgray, False).SetBounds(50, 470, 150, 30);
|
|
EdTopicHEAD := TEdit.Create(Self);
|
|
EdTopicHEAD.Parent := Dlg;
|
|
EdTopicHEAD.SetBounds(250, 470, 400, 30);
|
|
EdTopicHEAD.Text := FMQTTTopicHEAD;
|
|
EdTopicHEAD.Tag := 6;
|
|
EdTopicHEAD.TagFloat := 6;
|
|
EdTopicHEAD.StyledSettings := EdTopicHEAD.StyledSettings - [TStyledSetting.Size, TStyledSetting.FontColor];
|
|
EdTopicHEAD.TextSettings.Font.Size := 22;
|
|
{$IFDEF ANDROID}
|
|
EdTopicHEAD.TextSettings.FontColor := claWhite;
|
|
{$ENDIF}
|
|
EdTopicHEAD.OnExit := OnMQTTSetupEdit;
|
|
|
|
// ── MQTTBroker Setting - TopicTAIL ──────────────────────────────────────────────
|
|
BuildLabel(Dlg, 'TopicTAIL :', 22, TAlignLayout.None, claLightgray, False).SetBounds(50, 540, 150, 30);
|
|
EdTopicTAIL := TEdit.Create(Self);
|
|
EdTopicTAIL.Parent := Dlg;
|
|
EdTopicTAIL.SetBounds(250, 540, 400, 30);
|
|
EdTopicTAIL.Text := FMQTTTopicTAIL;
|
|
EdTopicTAIL.Tag := 7;
|
|
EdTopicTAIL.TagFloat := 7;
|
|
EdTopicTAIL.StyledSettings := EdTopicTAIL.StyledSettings - [TStyledSetting.Size, TStyledSetting.FontColor];
|
|
EdTopicTAIL.TextSettings.Font.Size := 22;
|
|
{$IFDEF ANDROID}
|
|
EdTopicTAIL.TextSettings.FontColor := claWhite;
|
|
{$ENDIF}
|
|
EdTopicTAIL.OnExit := OnMQTTSetupEdit;
|
|
|
|
BottomLyt := TLayout.Create(Self);
|
|
BottomLyt.Parent := Dlg;
|
|
BottomLyt.Align := TAlignLayout.Bottom;
|
|
BottomLyt.Height := 80;
|
|
|
|
BtnAddClose := CreateCustomButton(BottomLyt, '저장/닫기 [ Save/Close ]', TAlignLayout.Client, 664, OnMQTTSetupSaveClose);
|
|
TLabel(BtnAddClose.Controls[0]).TextSettings.Font.Size := 20;
|
|
BtnAddClose.Margins.Rect := RectF(16, 0, 16, 16);
|
|
end;
|
|
|
|
procedure TfrmMain.OnMQTTSetupEdit(Sender: TObject);
|
|
var FieldID: Integer;
|
|
begin
|
|
// if FRefreshingAutoUI then Exit;
|
|
FieldID := -1;
|
|
|
|
if Sender is TEdit then
|
|
begin
|
|
FieldID := Trunc((Sender as TEdit).TagFloat);
|
|
end;
|
|
|
|
case FieldID of
|
|
1: if Sender is TEdit then FMQTTGateWayID := (Sender as TEdit).Text;
|
|
2: if Sender is TEdit then FMQTTBrokerIP := (Sender as TEdit).Text;
|
|
3: if Sender is TEdit then FMQTTBrokerPORT := StrToIntDef((Sender as TEdit).Text, 1883);
|
|
4: if Sender is TEdit then FMQTTUserID := (Sender as TEdit).Text;
|
|
5: if Sender is TEdit then FMQTTUserPASS := (Sender as TEdit).Text;
|
|
6: if Sender is TEdit then FMQTTTopicHEAD := (Sender as TEdit).Text;
|
|
7: if Sender is TEdit then FMQTTTopicTAIL := (Sender as TEdit).Text;
|
|
end;
|
|
|
|
SaveSettings_MQTT;
|
|
AddSystemLog('시스템 설정 변경: MQTT Broker 정보 ['+(Sender as TEdit).Text+'] 수정됨');
|
|
end;
|
|
|
|
procedure TfrmMain.OnMQTTSetupSaveClose(Sender: TObject);
|
|
var Act: TIrriAction;
|
|
begin
|
|
// SaveSettings_MQTT;
|
|
lytMQTTSetupPopup.Visible := False;
|
|
|
|
if FMQTTClient <> nil then
|
|
begin
|
|
FMQTTClient.OnMessage := nil;
|
|
try
|
|
FMQTTClient.Disconnect;
|
|
except
|
|
end;
|
|
Sleep(100);
|
|
FMQTTClient.OnStatus := nil;
|
|
FreeAndNil(FMQTTClient);
|
|
|
|
// MQTT 클라이언트 초기화 및 연결
|
|
FMQTTClient := TMQTTClient.Create(FMQTTBrokerIP, FMQTTBrokerPORT, 'SmartFarmHMI');
|
|
FMQTTClient.OnMessage := OnMQTTMessage;
|
|
FMQTTClient.OnStatus := OnMQTTStatus;
|
|
FMQTTClient.Subscribe(FMQTTTopicHEAD+'PUB/'+FMQTTTopicTAIL+FMQTTGateWayID);
|
|
|
|
if FIsKorean then lblConnectionStatus.Text := '연결중...' else lblConnectionStatus.Text := 'Connecting...';
|
|
|
|
FMQTTClient.Connect;
|
|
|
|
AddSystemLog('시스템 MQTT 재연결 : MQTT Broker 정보 ['+FMQTTGateWayID+' / '+
|
|
FMQTTBrokerIP+' / '+
|
|
IntToStr(FMQTTBrokerPORT)+' / '+
|
|
FMQTTUserID+' / '+
|
|
FMQTTUserPASS+' / '+
|
|
FMQTTTopicHEAD+' / '+
|
|
FMQTTTopicTAIL+']');
|
|
end;
|
|
end;
|
|
|
|
end.
|