SmartFarm/SOURCE/SmartFarmHMI/SmartFarmHMI_FMX/__history/UMain.pas.~193~
2026-09-04 10:53:44 +09:00

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.