SmartFarm/SOURCE/SmartFarmHMI/SmartFarmHMI_FMX/__history/UAutoControl_Impl.inc.~100~
2026-09-04 10:53:44 +09:00

1055 lines
45 KiB
Plaintext
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

{
============================================================
UAutoControl_Impl.inc - 스마트팜 HMI 자동운전 메서드 구현 (FMX 버전)
============================================================
이 파일은 UMain.pas의 implementation 섹션에
$I UAutoControl_Impl.inc 으로 포함됩니다.
TfrmMain의 자동운전 관련 메서드가 모두 여기 정의됩니다.
============================================================
}
// ─── 자동운전 모드 토글 ──────────────────────────────────────────────────────
procedure TfrmMain.OnAutoModeToggle(Sender: TObject);
begin
FAutoMode := not FAutoMode;
SaveAutoRules;
if FAutoMode then begin
LoadAutoRules;
end
else begin
Glyph1.ImageIndex := -1;
end;
UpdateAutoModeBtnUI;
if FAutoMode then
begin
TimerAuto.Interval := 1000;
AddSystemLog('시스템 설정 변경: 자동 제어 모드 ON 설정됨');
end
else
begin
AddSystemLog('시스템 설정 변경: 자동 제어 모드 OFF 설정됨');
end;
TimerAuto.Enabled := FAutoMode;
end;
procedure TfrmMain.UpdateAutoModeBtnUI;
begin
if (pnlAutoModeBtn = nil) or (lblAutoModeStatus = nil) then Exit;
if FAutoMode then
begin
pnlAutoModeBtn.Fill.Color := $FF4CAF50; // CLR_GREEN
if FIsKorean then lblAutoModeStatus.Text := '🤖 자동 모드 ON'
else lblAutoModeStatus.Text := '🤖 AUTO MODE ON';
lblAutoModeStatus.TextSettings.FontColor := claWhite;
end else begin
pnlAutoModeBtn.Fill.Color := $FF555555;
if FIsKorean then lblAutoModeStatus.Text := '🖐 수동 모드'
else lblAutoModeStatus.Text := '🖐 MANUAL MODE';
lblAutoModeStatus.TextSettings.FontColor := claLightgray; // CLR_GRAY
end;
end;
// ─── 자동운전 규칙 UI 이벤트 ─────────────────────────────────────────────────
procedure TfrmMain.OnAutoRuleAdd(Sender: TObject);
var R: TAutoRule;
begin
R.Enabled := True;
R.StartHH := 0; R.StartMM := 0;
R.EndHH := 23; R.EndMM := 59;
R.DOChannel := 1; R.DOState := True;
R.CondMode := acmAND;
SetLength(R.Conditions, 0); // 조건 없음으로 초기화
R.WasActive := False;
R.IsSelected := False;
R.UseSchedule := False;
R.WorkMinutes := 0;
R.RestMinutes := 0;
if FIsKorean then R.RuleName := '새 규칙 ' + IntToStr(Length(FAutoRules)+1)
else R.RuleName := 'Rule ' + IntToStr(Length(FAutoRules)+1);
SetLength(FAutoRules, Length(FAutoRules)+1);
FAutoRules[High(FAutoRules)] := R;
SaveAutoRules;
AddSystemLog('시스템 설정 변경: 새로운 자동 제어 규칙 추가됨 (' + R.RuleName + ')');
TThread.Queue(nil, procedure begin RefreshAutoUI; end);
end;
procedure TfrmMain.OnAutoRuleDel(Sender: TObject);
var Idx, i: Integer;
begin
if Self.Focused <> nil then Self.Focused := nil;
Idx := -1;
if Sender is TRectangle then Idx := (Sender as TRectangle).Tag
else if Sender is TLabel then Idx := (Sender as TLabel).Tag;
if (Idx < 0) or (Idx > High(FAutoRules)) then Exit;
for i := Idx to High(FAutoRules)-1 do
FAutoRules[i] := FAutoRules[i+1];
SetLength(FAutoRules, Length(FAutoRules)-1);
SaveAutoRules;
AddSystemLog('시스템 설정 변경: 자동 제어 규칙 다일 삭제됨 (인덱스 ' + IntToStr(Idx) + ')');
TThread.Queue(nil, procedure begin RefreshAutoUI; end);
end;
procedure TfrmMain.OnAutoRuleCheck(Sender: TObject);
var
Idx: Integer;
begin
if Sender is TCheckBox then
begin
Idx := (Sender as TCheckBox).Tag;
if (Idx >= 0) and (Idx <= High(FAutoRules)) then
FAutoRules[Idx].IsSelected := (Sender as TCheckBox).IsChecked;
end;
end;
procedure TfrmMain.OnAutoRuleDelSelected(Sender: TObject);
var
i, j: Integer;
Changed: Boolean;
begin
if Self.Focused <> nil then Self.Focused := nil;
Changed := False;
// 역순으로 순회하며 선택된 항목 삭제
for i := High(FAutoRules) downto 0 do
begin
if FAutoRules[i].IsSelected then
begin
for j := i to High(FAutoRules) - 1 do
FAutoRules[j] := FAutoRules[j+1];
SetLength(FAutoRules, Length(FAutoRules) - 1);
Changed := True;
end;
end;
if Changed then
begin
SaveAutoRules;
AddSystemLog('시스템 설정 변경: 선택된 자동 제어 규칙 일괄 삭제됨');
TThread.Queue(nil, procedure begin RefreshAutoUI; end);
end;
end;
procedure TfrmMain.OnAutoCondAdd(Sender: TObject);
var RuleIdx: Integer; C: TAutoCondItem;
begin
RuleIdx := -1;
if Sender is TRectangle then RuleIdx := (Sender as TRectangle).Tag
else if Sender is TLabel then RuleIdx := (Sender as TLabel).Tag;
if (RuleIdx < 0) or (RuleIdx > High(FAutoRules)) then Exit;
C.NodeID := '';
C.Op := acoGT;
C.Value := 0;
SetLength(FAutoRules[RuleIdx].Conditions, Length(FAutoRules[RuleIdx].Conditions)+1);
FAutoRules[RuleIdx].Conditions[High(FAutoRules[RuleIdx].Conditions)] := C;
SaveAutoRules;
TThread.Queue(nil, procedure begin RefreshAutoUI; end);
end;
procedure TfrmMain.OnAutoCondDel(Sender: TObject);
var TagVal, RuleIdx, CondIdx, i: Integer;
begin
if Self.Focused <> nil then Self.Focused := nil;
TagVal := -1;
if Sender is TRectangle then TagVal := (Sender as TRectangle).Tag
else if Sender is TLabel then TagVal := (Sender as TLabel).Tag;
if TagVal = -1 then Exit;
RuleIdx := TagVal and $FF;
CondIdx := (TagVal shr 8) and $FF;
if (RuleIdx < 0) or (RuleIdx > High(FAutoRules)) then Exit;
if (CondIdx < 0) or (CondIdx > High(FAutoRules[RuleIdx].Conditions)) then Exit;
for i := CondIdx to High(FAutoRules[RuleIdx].Conditions)-1 do
FAutoRules[RuleIdx].Conditions[i] := FAutoRules[RuleIdx].Conditions[i+1];
SetLength(FAutoRules[RuleIdx].Conditions, Length(FAutoRules[RuleIdx].Conditions)-1);
SaveAutoRules;
TThread.Queue(nil, procedure begin RefreshAutoUI; end);
end;
procedure TfrmMain.OnAutoRuleEdit(Sender: TObject);
var
TagVal, RuleIdx, FieldID, CondIdx: Integer;
S: string;
begin
if FRefreshingAutoUI then Exit;
TagVal := 0;
if Sender is TRectangle then TagVal := (Sender as TRectangle).Tag
else if Sender is TSwitch then TagVal := (Sender as TSwitch).Tag
else if Sender is TEdit then TagVal := (Sender as TEdit).Tag
else if Sender is TComboBox then TagVal := (Sender as TComboBox).Tag
else if Sender is TLabel then TagVal := (Sender as TLabel).Tag;
RuleIdx := TagVal and $FF;
FieldID := (TagVal shr 8) and $FF;
CondIdx := (TagVal shr 16) and $FF;
if (RuleIdx < 0) or (RuleIdx > High(FAutoRules)) then Exit;
case FieldID of
0: if Sender is TSwitch then begin // Enabled
FAutoRules[RuleIdx].Enabled := (Sender as TSwitch).IsChecked;
SaveAutoRules;
TThread.Queue(nil, procedure begin RefreshAutoUI; end);
end;
1: if Sender is TEdit then FAutoRules[RuleIdx].StartHH := StrToIntDef((Sender as TEdit).Text, 0) mod 24;
2: if Sender is TEdit then FAutoRules[RuleIdx].StartMM := StrToIntDef((Sender as TEdit).Text, 0) mod 60;
3: if Sender is TEdit then FAutoRules[RuleIdx].EndHH := StrToIntDef((Sender as TEdit).Text, 0) mod 24;
4: if Sender is TEdit then FAutoRules[RuleIdx].EndMM := StrToIntDef((Sender as TEdit).Text, 0) mod 60;
5: if Sender is TComboBox then // DOChannel
FAutoRules[RuleIdx].DOChannel := (Sender as TComboBox).ItemIndex;
6: if Sender is TSwitch then // DOState
begin
FAutoRules[RuleIdx].DOState := (Sender as TSwitch).IsChecked;
SaveAutoRules;
TThread.Queue(nil, procedure begin RefreshAutoUI; end);
end;
10: if Sender is TEdit then // RuleName
FAutoRules[RuleIdx].RuleName := (Sender as TEdit).Text;
11: if Sender is TComboBox then begin // CondMode
if (Sender as TComboBox).ItemIndex = 0 then
FAutoRules[RuleIdx].CondMode := acmAND
else
FAutoRules[RuleIdx].CondMode := acmOR;
end;
12: if Sender is TSwitch then // UseSchedule
begin
FAutoRules[RuleIdx].UseSchedule := (Sender as TSwitch).IsChecked;
SaveAutoRules;
TThread.Queue(nil, procedure begin RefreshAutoUI; end);
end;
13: if Sender is TEdit then // WorkMinutes
FAutoRules[RuleIdx].WorkMinutes := StrToIntDef((Sender as TEdit).Text, 0);
14: if Sender is TEdit then // RestMinutes
FAutoRules[RuleIdx].RestMinutes := StrToIntDef((Sender as TEdit).Text, 0);
// ── 조건별 NodeID ──
20: if Sender is TComboBox then
begin
if (CondIdx >= 0) and (CondIdx <= High(FAutoRules[RuleIdx].Conditions)) then
begin
var bWasDIDO: Boolean := FAutoRules[RuleIdx].Conditions[CondIdx].NodeID.StartsWith('DI_') or
FAutoRules[RuleIdx].Conditions[CondIdx].NodeID.StartsWith('DO_');
S := (Sender as TComboBox).Selected.Text;
if S.StartsWith('--') then FAutoRules[RuleIdx].Conditions[CondIdx].NodeID := ''
else FAutoRules[RuleIdx].Conditions[CondIdx].NodeID := S.Split([' '])[0];
var bNowDIDO: Boolean := FAutoRules[RuleIdx].Conditions[CondIdx].NodeID.StartsWith('DI_') or
FAutoRules[RuleIdx].Conditions[CondIdx].NodeID.StartsWith('DO_');
// 노드 타입이 변경된 경우 기본 연산자 자동 설정
if bNowDIDO and not bWasDIDO then
FAutoRules[RuleIdx].Conditions[CondIdx].Op := acoON // 수치→DI: ON으로
else if not bNowDIDO and bWasDIDO then
FAutoRules[RuleIdx].Conditions[CondIdx].Op := acoGT; // DI→수치: > 로
// 노드 타입이 달라졌으면 UI 전체 새로고침 (Op 콤보박스 항목이 바뀌어야 함)
if bWasDIDO <> bNowDIDO then
begin
SaveAutoRules;
AddSystemLog('시스템 설정 변경: 자동 제어 규칙 [' + FAutoRules[RuleIdx].RuleName + '] 조건 센서타입 수정됨');
TThread.Queue(nil, procedure begin RefreshAutoUI; end);
Exit;
end;
end;
end;
// ── 조건별 Op ──
21: if Sender is TComboBox then
begin
if (CondIdx >= 0) and (CondIdx <= High(FAutoRules[RuleIdx].Conditions)) then
begin
if FAutoRules[RuleIdx].Conditions[CondIdx].NodeID.StartsWith('DI_') then
FAutoRules[RuleIdx].Conditions[CondIdx].Op :=
TAutoCondOp((Sender as TComboBox).ItemIndex + Ord(acoON))
else
FAutoRules[RuleIdx].Conditions[CondIdx].Op :=
TAutoCondOp((Sender as TComboBox).ItemIndex);
end;
end;
// ── 조건별 Value ──
22: if Sender is TEdit then
begin
if (CondIdx >= 0) and (CondIdx <= High(FAutoRules[RuleIdx].Conditions)) then
TryStrToFloat((Sender as TEdit).Text, FAutoRules[RuleIdx].Conditions[CondIdx].Value);
end;
end;
SaveAutoRules;
AddSystemLog('시스템 설정 변경: 자동 제어 규칙 [' + FAutoRules[RuleIdx].RuleName + '] 속성 수정됨');
end;
// ─── 자동운전 UI 빌드 ────────────────────────────────────────────────────────
procedure TfrmMain.BuildAutoUI;
var
AddBtnBar, DelBtnBar: TRectangle;
begin
RefreshAutoUI;
// 하단 고정 버튼 바 구성
if btnAutoRuleBar <> nil then
begin
while btnAutoRuleBar.ControlsCount > 0 do
btnAutoRuleBar.Controls[0].Free;
AddBtnBar := TRectangle.Create(Self);
AddBtnBar.Parent := btnAutoRuleBar;
AddBtnBar.Align := TAlignLayout.Client;
// AddBtnBar.Margins.Rect := RectF(16, 8, 8, 8);
AddBtnBar.Margins.Rect := RectF(16, 8, 16, 0);
AddBtnBar.Fill.Color := $FF226622;
AddBtnBar.Stroke.Kind := TBrushKind.None;
AddBtnBar.XRadius := 8; AddBtnBar.YRadius := 8;
AddBtnBar.Cursor := crHandPoint;
AddBtnBar.HitTest := True;
AddBtnBar.OnClick := OnAutoRuleAdd;
var LAd := BuildLabel(AddBtnBar, ' ' + (if FIsKorean then '새 규칙 추가' else 'Add Rule'),
20, TAlignLayout.Client, claWhite, True);
LAd.TextSettings.HorzAlign := TTextAlign.Center;
LAd.HitTest := False;
DelBtnBar := TRectangle.Create(Self);
DelBtnBar.Parent := btnAutoRuleBar;
DelBtnBar.Align := TAlignLayout.Right;
DelBtnBar.Width := 600;
// DelBtnBar.Margins.Rect := RectF(8, 8, 16, 8);
DelBtnBar.Margins.Rect := RectF(0, 8, 16, 0);
DelBtnBar.Fill.Color := $FFFF5252;
DelBtnBar.Stroke.Kind := TBrushKind.None;
DelBtnBar.XRadius := 8; DelBtnBar.YRadius := 8;
DelBtnBar.Cursor := crHandPoint;
DelBtnBar.HitTest := True;
DelBtnBar.OnClick := OnAutoRuleDelSelected;
var LDl := BuildLabel(DelBtnBar, '🗑 ' + (if FIsKorean then '선택 삭제' else 'Del Selected'),
20, TAlignLayout.Client, claWhite, True);
LDl.TextSettings.HorzAlign := TTextAlign.Center;
LDl.HitTest := False;
end;
end;
procedure TfrmMain.RefreshAutoUI;
const
RULE_PAD = 16;
RULE_PAD_ = 16 + 300;
RULE_GAP = 16;
COND_ROW_H = 38;
BASE_H = 360; // 280 + 80
COND_BTN_H = 38;
ANDOR_H = 34;
var
i, j, CY, CardH: Integer;
CC: Integer;
Card, AddCondBtn, DelCondBtn: TRectangle;
LAdd, LAddCond, LDelCond: TLabel;
SW_En, SW_DO: TSwitch;
EdStartHH, EdStartMM, EdEndHH, EdEndMM, EdCondVal, EdName: TEdit;
CbDO, CbCondOp, CbCondNode, CbCondMode: TComboBox;
AddBtnPnl, DelSelBtn: TRectangle;
TopMenuLyt: TLayout;
ChkDel: TCheckBox;
{$IFDEF ANDROID}
ChkBg: TRectangle;
{$ENDIF}
bIsDIDO: Boolean;
CondY: Integer;
RowY: Integer;
CLR_CARD: TAlphaColor;
SelIdx: Integer;
LIcon, LEnLbl, LTimeLbl, LDoLbl, LDoState, LCondLbl, LEmpty: TLabel;
Line1, Line2: TRectangle;
LItem: TListBoxItem;
begin
if scrollAuto = nil then Exit;
FRefreshingAutoUI := True;
try
scrollAuto.BeginUpdate;
// 1) 폼 리프레시 시 발생할 수 있는 내부 포커싱 및 이벤트 체인 오류 방지
if Self.Focused <> nil then
Self.Focused := nil;
// 2) MacOS/Linux 환경의 Native controls 충돌 방지를 위해
// 이벤트 연결을 해제하고 시각적 트리에서 분리한 후 안전하게 메모리를 해제합니다.
for i := scrollAuto.Content.ControlsCount - 1 downto 0 do
begin
var C := scrollAuto.Content.Controls[i];
if C is TEdit then (C as TEdit).OnExit := nil;
if C is TComboBox then (C as TComboBox).OnChange := nil;
if C is TSwitch then (C as TSwitch).OnSwitch := nil;
if C is TCheckBox then (C as TCheckBox).OnChange := nil;
C.Parent := nil; // FMX ScrollBox 오작동 및 Native 객체 Crash 방지
// C.DisposeOf;
end;
CY := RULE_GAP;
// 규칙이 없을 때
if Length(FAutoRules) = 0 then
begin
LEmpty := BuildLabel(scrollAuto, if FIsKorean then '등록된 자동 제어 규칙이 없습니다.' else 'No auto control rules registered.', 18, TAlignLayout.Top, claLightgray, False);
LEmpty.Margins.Top := 20;
LEmpty.TextSettings.HorzAlign := TTextAlign.Center;
Exit;
end;
// 각 규칙 카드
for i := 0 to High(FAutoRules) do
begin
CC := Length(FAutoRules[i].Conditions);
CardH := BASE_H + CC * (COND_ROW_H + 4) + COND_BTN_H;
if CC >= 2 then Inc(CardH, ANDOR_H + 4);
if FAutoRules[i].Enabled then CLR_CARD := $FF1E293B else CLR_CARD := $FF333333;
Card := TRectangle.Create(Self);
Card.Parent := scrollAuto;
Card.Align := TAlignLayout.Top;
Card.Height := CardH;
Card.Margins.Rect := RectF(RULE_GAP, RULE_GAP, RULE_GAP, 0);
Card.Fill.Color := CLR_CARD;
Card.Stroke.Kind := TBrushKind.None;
Card.XRadius := 12; Card.YRadius := 12;
Card.Tag := i;
// ── 카드 헤더 줄: 체크박스 + 아이콘 + 이름 + 활성토글 ──
{$IFDEF ANDROID}
// 안드로이드에서 체크박스가 어두운 배경 위에서 잘 안보이므로
// 체크박스 뒤에 밝은 흰색 배경을 깔아줍니다.
ChkBg := TRectangle.Create(Self);
ChkBg.Parent := Card;
ChkBg.Position.X := RULE_PAD - 4;
ChkBg.Position.Y := 13;
ChkBg.Width := 30;
ChkBg.Height := 30;
ChkBg.Fill.Color := claWhite;
ChkBg.Stroke.Kind := TBrushKind.None;
ChkBg.XRadius := 4;
ChkBg.YRadius := 4;
ChkBg.HitTest := False;
{$ENDIF}
ChkDel := TCheckBox.Create(Self);
ChkDel.Parent := Card;
ChkDel.Position.X := RULE_PAD-6;
ChkDel.Position.Y := 10;
ChkDel.Width := 24;
ChkDel.Height := 24;
ChkDel.IsChecked := FAutoRules[i].IsSelected;
ChkDel.Tag := i;
ChkDel.Scale.X := 1.5;
ChkDel.Scale.Y := 1.5;
ChkDel.OnChange := OnAutoRuleCheck;
LIcon := BuildLabel(Card, '⚙️', 20, TAlignLayout.None, claWhite, False);
LIcon.SetBounds(RULE_PAD + 34, 10, 36, 36);
EdName := TEdit.Create(Self); EdName.Parent := Card;
EdName.Position.X := RULE_PAD + 70; EdName.Position.Y := 10;
EdName.Width := 300; EdName.Height := 36;
EdName.TextSettings.Font.Size := 18;
EdName.Text := FAutoRules[i].RuleName;
EdName.StyledSettings := EdName.StyledSettings - [TStyledSetting.Size, TStyledSetting.FontColor];
{$IFDEF ANDROID}
EdName.TextSettings.FontColor := claWhite;
{$ENDIF}
EdName.Tag := i or (10 shl 8); EdName.OnExit := OnAutoRuleEdit;
LEnLbl := BuildLabel(Card, if FIsKorean then '활성' else 'Enable', 16, TAlignLayout.None, claLightgray, False);
LEnLbl.Position.X := Card.Width - RULE_PAD - 130; LEnLbl.Position.Y := 16;
LEnLbl.Width := 50; LEnLbl.Height := 24;
LEnLbl.Anchors := [TAnchorKind.akTop, TAnchorKind.akRight];
SW_En := TSwitch.Create(Self); SW_En.Parent := Card;
SW_En.Position.X := Card.Width - RULE_PAD - 70; SW_En.Position.Y := 10;
SW_En.Width := 70;
{$IFDEF ANDROID}
SW_En.Scale.X := 1.5;
SW_En.Scale.Y := 1.5;
{$ENDIF}
SW_En.IsChecked := FAutoRules[i].Enabled;
SW_En.Tag := i or (0 shl 8); SW_En.OnSwitch := OnAutoRuleEdit;
SW_En.Anchors := [TAnchorKind.akTop, TAnchorKind.akRight];
// ── 구분선 ──
Line1 := TRectangle.Create(Self); Line1.Parent := Card;
Line1.Position.X := RULE_PAD; Line1.Position.Y := 56;
Line1.Width := Card.Width - RULE_PAD*2; Line1.Height := 1;
Line1.Fill.Color := $FF555555; Line1.Stroke.Kind := TBrushKind.None;
Line1.Anchors := [TAnchorKind.akTop, TAnchorKind.akLeft, TAnchorKind.akRight];
// ── 동작 시간 설정 ──
LTimeLbl := BuildLabel(Card, '⏰ ' + (if FIsKorean then '동작 시간' else 'Active Time'), 15, TAlignLayout.None, claLightgray, True);
LTimeLbl.SetBounds(RULE_PAD, 66, 160, 24);
BuildLabel(Card, if FIsKorean then '시작' else 'Start', 13, TAlignLayout.None, claLightgray, False).SetBounds(RULE_PAD_, 96, 50, 20);
EdStartHH := TEdit.Create(Self); EdStartHH.Parent := Card;
EdStartHH.Position.X := RULE_PAD_ + 50; EdStartHH.Position.Y := 92; EdStartHH.Width := 50; EdStartHH.Height := 32;
EdStartHH.TextSettings.Font.Size := 18; EdStartHH.MaxLength := 2;
EdStartHH.Text := Format('%.2d', [FAutoRules[i].StartHH]);
EdStartHH.StyledSettings := EdStartHH.StyledSettings - [TStyledSetting.Size, TStyledSetting.FontColor];
EdStartHH.FilterChar := '0123456789';
{$IFDEF ANDROID}
EdStartHH.TextSettings.FontColor := claWhite;
{$ENDIF}
EdStartHH.Tag := i or (1 shl 8); EdStartHH.OnExit := OnAutoRuleEdit;
BuildLabel(Card, ':', 18, TAlignLayout.None, claWhite, True).SetBounds(RULE_PAD_ + 104, 96, 12, 24);
EdStartMM := TEdit.Create(Self); EdStartMM.Parent := Card;
EdStartMM.Position.X := RULE_PAD_ + 118; EdStartMM.Position.Y := 92; EdStartMM.Width := 50; EdStartMM.Height := 32;
EdStartMM.TextSettings.Font.Size := 18; EdStartMM.MaxLength := 2;
EdStartMM.Text := Format('%.2d', [FAutoRules[i].StartMM]);
EdStartMM.StyledSettings := EdStartMM.StyledSettings - [TStyledSetting.Size, TStyledSetting.FontColor];
EdStartMM.FilterChar := '0123456789';
{$IFDEF ANDROID}
EdStartMM.TextSettings.FontColor := claWhite;
{$ENDIF}
EdStartMM.Tag := i or (2 shl 8); EdStartMM.OnExit := OnAutoRuleEdit;
BuildLabel(Card, if FIsKorean then '종료' else 'End', 13, TAlignLayout.None, claLightgray, False).SetBounds(RULE_PAD_ + 200, 96, 50, 20);
EdEndHH := TEdit.Create(Self); EdEndHH.Parent := Card;
EdEndHH.Position.X := RULE_PAD_ + 250; EdEndHH.Position.Y := 92; EdEndHH.Width := 50; EdEndHH.Height := 32;
EdEndHH.TextSettings.Font.Size := 18; EdEndHH.MaxLength := 2;
EdEndHH.Text := Format('%.2d', [FAutoRules[i].EndHH]);
EdEndHH.StyledSettings := EdEndHH.StyledSettings - [TStyledSetting.Size, TStyledSetting.FontColor];
EdEndHH.FilterChar := '0123456789';
{$IFDEF ANDROID}
EdEndHH.TextSettings.FontColor := claWhite;
{$ENDIF}
EdEndHH.Tag := i or (3 shl 8); EdEndHH.OnExit := OnAutoRuleEdit;
BuildLabel(Card, ':', 18, TAlignLayout.None, claWhite, True).SetBounds(RULE_PAD_ + 304, 96, 12, 24);
EdEndMM := TEdit.Create(Self); EdEndMM.Parent := Card;
EdEndMM.Position.X := RULE_PAD_ + 318; EdEndMM.Position.Y := 92; EdEndMM.Width := 50; EdEndMM.Height := 32;
EdEndMM.TextSettings.Font.Size := 18; EdEndMM.MaxLength := 2;
EdEndMM.Text := Format('%.2d', [FAutoRules[i].EndMM]);
EdEndMM.StyledSettings := EdEndMM.StyledSettings - [TStyledSetting.Size, TStyledSetting.FontColor];
EdEndMM.FilterChar := '0123456789';
{$IFDEF ANDROID}
EdEndMM.TextSettings.FontColor := claWhite;
{$ENDIF}
EdEndMM.Tag := i or (4 shl 8); EdEndMM.OnExit := OnAutoRuleEdit;
// ── 스케줄 반복 작동 ──
Line2 := TRectangle.Create(Self); Line2.Parent := Card;
Line2.Position.X := RULE_PAD; Line2.Position.Y := 134;
Line2.Width := Card.Width - RULE_PAD*2; Line2.Height := 1;
Line2.Fill.Color := $FF555555; Line2.Stroke.Kind := TBrushKind.None;
Line2.Anchors := [TAnchorKind.akTop, TAnchorKind.akLeft, TAnchorKind.akRight];
BuildLabel(Card, '🔄 ' + (if FIsKorean then '스케줄 (반복 작동)' else 'Schedule (Repeat)'), 15, TAlignLayout.None, claLightgray, True).SetBounds(RULE_PAD, 142, 220, 24);
BuildLabel(Card, if FIsKorean then '사용' else 'Use', 13, TAlignLayout.None, claLightgray, False).SetBounds(RULE_PAD_, 172, 40, 20);
var SW_Sched := TSwitch.Create(Self); SW_Sched.Parent := Card;
SW_Sched.Position.X := RULE_PAD_ + 40; SW_Sched.Position.Y := 166;
SW_Sched.Width := 70;
{$IFDEF ANDROID}
SW_Sched.Scale.X := 1.5;
SW_Sched.Scale.Y := 1.5;
{$ENDIF}
SW_Sched.IsChecked := FAutoRules[i].UseSchedule;
SW_Sched.Tag := i or (12 shl 8); SW_Sched.OnSwitch := OnAutoRuleEdit;
BuildLabel(Card, if FIsKorean then '작동(분)' else 'Work(m)', 13, TAlignLayout.None, claLightgray, False).SetBounds(RULE_PAD_ + 120, 172, 60, 20);
var EdWork := TEdit.Create(Self); EdWork.Parent := Card;
EdWork.Position.X := RULE_PAD_ + 180; EdWork.Position.Y := 168; EdWork.Width := 50; EdWork.Height := 30;
EdWork.TextSettings.Font.Size := 18;
EdWork.Text := IntToStr(FAutoRules[i].WorkMinutes);
EdWork.StyledSettings := EdWork.StyledSettings - [TStyledSetting.Size, TStyledSetting.FontColor];
EdWork.FilterChar := '0123456789';
{$IFDEF ANDROID} EdWork.TextSettings.FontColor := claWhite; {$ENDIF}
EdWork.Tag := i or (13 shl 8); EdWork.OnExit := OnAutoRuleEdit;
BuildLabel(Card, if FIsKorean then '휴식(분)' else 'Rest(m)', 13, TAlignLayout.None, claLightgray, False).SetBounds(RULE_PAD_ + 240, 172, 60, 20);
var EdRest := TEdit.Create(Self); EdRest.Parent := Card;
EdRest.Position.X := RULE_PAD_ + 300; EdRest.Position.Y := 168; EdRest.Width := 50; EdRest.Height := 30;
EdRest.TextSettings.Font.Size := 18;
EdRest.Text := IntToStr(FAutoRules[i].RestMinutes);
EdRest.StyledSettings := EdRest.StyledSettings - [TStyledSetting.Size, TStyledSetting.FontColor];
EdRest.FilterChar := '0123456789';
{$IFDEF ANDROID} EdRest.TextSettings.FontColor := claWhite; {$ENDIF}
EdRest.Tag := i or (14 shl 8); EdRest.OnExit := OnAutoRuleEdit;
// ── DO 출력 설정 ──
var Line3 := TRectangle.Create(Self); Line3.Parent := Card;
Line3.Position.X := RULE_PAD; Line3.Position.Y := 214;
Line3.Width := Card.Width - RULE_PAD*2; Line3.Height := 1;
Line3.Fill.Color := $FF555555; Line3.Stroke.Kind := TBrushKind.None;
Line3.Anchors := [TAnchorKind.akTop, TAnchorKind.akLeft, TAnchorKind.akRight];
LDoLbl := BuildLabel(Card, '🎛️ ' + (if FIsKorean then '출력 (DO) 설정' else 'Output (DO) Setting'), 15, TAlignLayout.None, claLightgray, True);
LDoLbl.SetBounds(RULE_PAD, 222, 220, 24);
BuildLabel(Card, if FIsKorean then '채널' else 'Channel', 13, TAlignLayout.None, claLightgray, False).SetBounds(RULE_PAD_, 252, 60, 20);
CbDO := TComboBox.Create(Self); CbDO.Parent := Card;
CbDO.Position.X := RULE_PAD_ + 64; CbDO.Position.Y := 248; CbDO.Width := 100; CbDO.Height := 30;
for j := 0 to 15 do begin
LItem := TListBoxItem.Create(CbDO);
LItem.Parent := CbDO;
LItem.Text := 'DO_' + Format('%.2d', [j]);
LItem.Height := 24 * 1.8;
LItem.StyledSettings := LItem.StyledSettings - [TStyledSetting.Size, TStyledSetting.FontColor];
LItem.TextSettings.Font.Size := 18;
{$IFDEF ANDROID}
LItem.TextSettings.FontColor := claWhite;
{$ENDIF}
CbDO.AddObject(LItem);
end;
CbDO.ItemIndex := FAutoRules[i].DOChannel;
CbDO.Tag := i or (5 shl 8); CbDO.OnChange := OnAutoRuleEdit;
BuildLabel(Card, if FIsKorean then '상태' else 'State', 13, TAlignLayout.None, claLightgray, False).SetBounds(RULE_PAD_ + 200, 252, 50, 20);
SW_DO := TSwitch.Create(Self); SW_DO.Parent := Card;
SW_DO.Position.X := RULE_PAD_ + 254; SW_DO.Position.Y := 246;
SW_DO.Width := 70;
{$IFDEF ANDROID}
SW_DO.Scale.X := 1.5;
SW_DO.Scale.Y := 1.5;
{$ENDIF}
SW_DO.IsChecked := FAutoRules[i].DOState;
SW_DO.Tag := i or (6 shl 8); SW_DO.OnSwitch := OnAutoRuleEdit;
LDoState := BuildLabel(Card, if FAutoRules[i].DOState then 'ON' else 'OFF', 14, TAlignLayout.None, if FAutoRules[i].DOState then $FF4CAF50 else $FFFF5252, True);
LDoState.SetBounds(RULE_PAD_ + 336, 252, 40, 24);
// ── 추가 조건 섹션 ──────────────────────────────────────────────────────────
var Line4 := TRectangle.Create(Self); Line4.Parent := Card;
Line4.Position.X := RULE_PAD; Line4.Position.Y := 290;
Line4.Width := Card.Width - RULE_PAD*2; Line4.Height := 1;
Line4.Fill.Color := $FF555555; Line4.Stroke.Kind := TBrushKind.None;
Line4.Anchors := [TAnchorKind.akTop, TAnchorKind.akLeft, TAnchorKind.akRight];
LCondLbl := BuildLabel(Card, '📊 ' + (if FIsKorean then '추가 조건 (선택사항)' else 'Additional Conditions (optional)'), 15, TAlignLayout.None, claLightgray, True);
LCondLbl.SetBounds(RULE_PAD, 298, 340, 24);
CondY := 328;
if CC >= 2 then
begin
BuildLabel(Card, if FIsKorean then '조건 결합' else 'Combine', 13, TAlignLayout.None, claLightgray, False).SetBounds(RULE_PAD_, CondY + 6, 80, 20);
CbCondMode := TComboBox.Create(Self); CbCondMode.Parent := Card;
CbCondMode.Position.X := RULE_PAD_ + 84; CbCondMode.Position.Y := CondY; CbCondMode.Width := 200; CbCondMode.Height := 30;
for j := 0 to 1 do begin
LItem := TListBoxItem.Create(CbCondMode);
LItem.Parent := CbCondMode;
LItem.Height := 24 * 1.8;
LItem.StyledSettings := LItem.StyledSettings - [TStyledSetting.Size, TStyledSetting.FontColor];
LItem.TextSettings.Font.Size := 18;
{$IFDEF ANDROID}
LItem.TextSettings.FontColor := claWhite;
{$ENDIF}
if j=0 then
LItem.Text := if FIsKorean then 'AND (모두 만족)' else 'AND (all match)'
else
LItem.Text := if FIsKorean then 'OR (하나 만족)' else 'OR (any match)';
CbCondMode.AddObject(LItem);
end;
CbCondMode.ItemIndex := Ord(FAutoRules[i].CondMode);
CbCondMode.Tag := i or (11 shl 8); CbCondMode.OnChange := OnAutoRuleEdit;
Inc(CondY, ANDOR_H + 4);
end;
for j := 0 to CC - 1 do
begin
RowY := CondY + j * (COND_ROW_H + 4);
BuildLabel(Card, IntToStr(j+1) + '.', 12, TAlignLayout.None, claLightgray, False).SetBounds(RULE_PAD_, RowY + 8, 22, 22);
CbCondNode := TComboBox.Create(Self); CbCondNode.Parent := Card;
CbCondNode.Position.X := RULE_PAD_ + 26; CbCondNode.Position.Y := RowY; CbCondNode.Width := 250; CbCondNode.Height := 30;
LItem := TListBoxItem.Create(CbCondNode);
LItem.Parent := CbCondNode;
LItem.Height := 24 * 1.8;
LItem.StyledSettings := LItem.StyledSettings - [TStyledSetting.Size, TStyledSetting.FontColor];
LItem.TextSettings.Font.Size := 18;
{$IFDEF ANDROID}
LItem.TextSettings.FontColor := claWhite;
{$ENDIF}
LItem.Text := '--(' + (if FIsKorean then '노드 선택' else 'Select Node') + ')';
CbCondNode.AddObject(LItem);
var k: Integer;
for k := 0 to High(FNodeConfigs) do begin
LItem := TListBoxItem.Create(CbCondNode);
LItem.Parent := CbCondNode;
LItem.Height := 24 * 1.8;
LItem.StyledSettings := LItem.StyledSettings - [TStyledSetting.Size, TStyledSetting.FontColor];
LItem.TextSettings.Font.Size := 18;
{$IFDEF ANDROID}
LItem.TextSettings.FontColor := claWhite;
{$ENDIF}
LItem.Text := FNodeConfigs[k].ID + ' [' + (
if FNodeConfigs[k].CustomName <> '' then FNodeConfigs[k].CustomName
else if FIsKorean then FNodeConfigs[k].TitleKO
else FNodeConfigs[k].TitleEN) + ']';
CbCondNode.AddObject(LItem);
end;
SelIdx := 0;
for k := 0 to CbCondNode.Items.Count-1 do
if CbCondNode.Items[k].StartsWith(FAutoRules[i].Conditions[j].NodeID + ' ') or
(CbCondNode.Items[k] = FAutoRules[i].Conditions[j].NodeID) then
begin SelIdx := k; Break; end;
CbCondNode.ItemIndex := SelIdx;
CbCondNode.Tag := i or (20 shl 8) or (j shl 16);
CbCondNode.OnChange := OnAutoRuleEdit;
bIsDIDO := FAutoRules[i].Conditions[j].NodeID.StartsWith('DI_') or FAutoRules[i].Conditions[j].NodeID.StartsWith('DO_');
CbCondOp := TComboBox.Create(Self); CbCondOp.Parent := Card;
CbCondOp.Position.X := RULE_PAD_ + 284; CbCondOp.Position.Y := RowY; CbCondOp.Width := 180; CbCondOp.Height := 30;
if bIsDIDO then
begin
for k := 0 to 1 do begin
LItem := TListBoxItem.Create(CbCondOp);
LItem.Parent := CbCondOp;
LItem.Height := 24 * 1.8;
LItem.StyledSettings := LItem.StyledSettings - [TStyledSetting.Size, TStyledSetting.FontColor];
LItem.TextSettings.Font.Size := 18;
{$IFDEF ANDROID}
LItem.TextSettings.FontColor := claWhite;
{$ENDIF}
if k=0 then
LItem.Text := if FIsKorean then 'ON (접점닫힘)' else 'ON (Closed)'
else
LItem.Text := if FIsKorean then 'OFF (접점열림)' else 'OFF (Open)';
CbCondOp.AddObject(LItem);
end;
if FAutoRules[i].Conditions[j].Op >= acoON then
CbCondOp.ItemIndex := Ord(FAutoRules[i].Conditions[j].Op) - Ord(acoON)
else
CbCondOp.ItemIndex := 0;
end else begin
for k := 0 to 2 do begin
LItem := TListBoxItem.Create(CbCondOp);
LItem.Parent := CbCondOp;
LItem.Height := 24 * 1.8;
LItem.StyledSettings := LItem.StyledSettings - [TStyledSetting.Size, TStyledSetting.FontColor];
LItem.TextSettings.Font.Size := 18;
{$IFDEF ANDROID}
LItem.TextSettings.FontColor := claWhite;
{$ENDIF}
if k=0 then
LItem.Text := '> (' + (if FIsKorean then '초과' else 'Greater') + ')'
else if k=1 then
LItem.Text := '< (' + (if FIsKorean then '미만' else 'Less') + ')'
else
LItem.Text := '= (' + (if FIsKorean then '같음' else 'Equal') + ')';
CbCondOp.AddObject(LItem);
end;
if FAutoRules[i].Conditions[j].Op <= acoEQ then
CbCondOp.ItemIndex := Ord(FAutoRules[i].Conditions[j].Op)
else
CbCondOp.ItemIndex := 0;
end;
CbCondOp.Tag := i or (21 shl 8) or (j shl 16);
CbCondOp.OnChange := OnAutoRuleEdit;
EdCondVal := TEdit.Create(Self); EdCondVal.Parent := Card;
EdCondVal.Position.X := RULE_PAD_ + 472; EdCondVal.Position.Y := RowY; EdCondVal.Width := 76; EdCondVal.Height := 30;
EdCondVal.TextSettings.Font.Size := 18;
EdCondVal.Enabled := not bIsDIDO;
EdCondVal.Text := if bIsDIDO then '--' else Format('%.1f', [FAutoRules[i].Conditions[j].Value]);
EdCondVal.StyledSettings := EdCondVal.StyledSettings - [TStyledSetting.Size, TStyledSetting.FontColor];
EdCondVal.FilterChar := '0123456789.';
{$IFDEF ANDROID}
EdCondVal.TextSettings.FontColor := claWhite;
{$ENDIF}
EdCondVal.Tag := i or (22 shl 8) or (j shl 16);
EdCondVal.OnExit := OnAutoRuleEdit;
DelCondBtn := TRectangle.Create(Self); DelCondBtn.Parent := Card;
DelCondBtn.Position.X := RULE_PAD_ + 554; DelCondBtn.Position.Y := RowY; DelCondBtn.Width := 80; DelCondBtn.Height := 30;
DelCondBtn.Fill.Color := $FFFF5252; DelCondBtn.Stroke.Kind := TBrushKind.None;
DelCondBtn.XRadius := 4; DelCondBtn.YRadius := 4;
DelCondBtn.Cursor := crHandPoint; DelCondBtn.Tag := i or (j shl 8); DelCondBtn.HitTest := True; DelCondBtn.OnClick := OnAutoCondDel;
LDelCond := BuildLabel(DelCondBtn, '✕', 13, TAlignLayout.Client, claWhite, True);
LDelCond.TextSettings.HorzAlign := TTextAlign.Center;
LDelCond.HitTest := False;
end;
var AddCondY: Integer := CondY + CC * (COND_ROW_H + 4) + 4;
AddCondBtn := TRectangle.Create(Self); AddCondBtn.Parent := Card;
AddCondBtn.Position.X := RULE_PAD; AddCondBtn.Position.Y := AddCondY;
AddCondBtn.Width := Card.Width - RULE_PAD * 2; AddCondBtn.Height := COND_BTN_H - 4 + 20;
AddCondBtn.Fill.Color := $FF2F5E2F; AddCondBtn.Stroke.Kind := TBrushKind.None;
AddCondBtn.XRadius := 4; AddCondBtn.YRadius := 4;
AddCondBtn.Cursor := crHandPoint; AddCondBtn.Tag := i; AddCondBtn.HitTest := True; AddCondBtn.OnClick := OnAutoCondAdd;
AddCondBtn.Anchors := [TAnchorKind.akTop, TAnchorKind.akLeft, TAnchorKind.akRight];
LAddCond := BuildLabel(AddCondBtn, ' ' + (if FIsKorean then '조건 추가' else 'Add Condition'), 16, TAlignLayout.Client, claWhite, True);
LAddCond.TextSettings.HorzAlign := TTextAlign.Center;
LAddCond.HitTest := False;
end;
finally
scrollAuto.EndUpdate;
FRefreshingAutoUI := False;
end;
end;
// ─── INI 저장/불러오기 ────────────────────────────────────────────────────────
procedure TfrmMain.LoadAutoRules;
var Ini: TIniFile; SP: string; N, CC, i, c: Integer;
begin
SP := System.IOUtils.TPath.Combine(System.IOUtils.TPath.GetHomePath, 'SmartFarmHMI_settings.ini');
Ini := TIniFile.Create(SP);
try
N := Ini.ReadInteger('AutoRules', 'Count', 0);
SetLength(FAutoRules, N);
FAutoMode := Ini.ReadBool('AutoRules', 'AutoMode', False);
for i := 0 to N-1 do
begin
FAutoRules[i].Enabled := Ini.ReadBool ('AutoRules', 'R'+IntToStr(i)+'_En', True);
FAutoRules[i].StartHH := Ini.ReadInteger ('AutoRules', 'R'+IntToStr(i)+'_SHH', 0);
FAutoRules[i].StartMM := Ini.ReadInteger ('AutoRules', 'R'+IntToStr(i)+'_SMM', 0);
FAutoRules[i].EndHH := Ini.ReadInteger ('AutoRules', 'R'+IntToStr(i)+'_EHH', 23);
FAutoRules[i].EndMM := Ini.ReadInteger ('AutoRules', 'R'+IntToStr(i)+'_EMM', 59);
FAutoRules[i].DOChannel := Ini.ReadInteger ('AutoRules', 'R'+IntToStr(i)+'_DOC', 0);
FAutoRules[i].DOState := Ini.ReadBool ('AutoRules', 'R'+IntToStr(i)+'_DOS', True);
FAutoRules[i].CondMode := TAutoCondMode(Ini.ReadInteger('AutoRules', 'R'+IntToStr(i)+'_CondMode', 0));
FAutoRules[i].RuleName := Ini.ReadString ('AutoRules', 'R'+IntToStr(i)+'_Name',
if FIsKorean then '규칙 '+IntToStr(i+1) else 'Rule '+IntToStr(i+1));
FAutoRules[i].UseSchedule:= Ini.ReadBool ('AutoRules', 'R'+IntToStr(i)+'_UseSched', False);
FAutoRules[i].WorkMinutes:= Ini.ReadInteger ('AutoRules', 'R'+IntToStr(i)+'_WorkM', 0);
FAutoRules[i].RestMinutes:= Ini.ReadInteger ('AutoRules', 'R'+IntToStr(i)+'_RestM', 0);
FAutoRules[i].WasActive := False;
CC := Ini.ReadInteger('AutoRules', 'R'+IntToStr(i)+'_CC', 0);
SetLength(FAutoRules[i].Conditions, CC);
for c := 0 to CC-1 do
begin
FAutoRules[i].Conditions[c].NodeID := Ini.ReadString ('AutoRules', 'R'+IntToStr(i)+'_C'+IntToStr(c)+'_NID', '');
FAutoRules[i].Conditions[c].Op := TAutoCondOp(Ini.ReadInteger('AutoRules', 'R'+IntToStr(i)+'_C'+IntToStr(c)+'_COP', 0));
FAutoRules[i].Conditions[c].Value := Ini.ReadFloat ('AutoRules', 'R'+IntToStr(i)+'_C'+IntToStr(c)+'_CV', 0);
end;
end;
finally
Ini.Free;
end;
end;
procedure TfrmMain.SaveAutoRules;
var Ini: TIniFile; SP: string; i, c: Integer;
begin
SP := System.IOUtils.TPath.Combine(System.IOUtils.TPath.GetHomePath, 'SmartFarmHMI_settings.ini');
Ini := TIniFile.Create(SP);
try
Ini.EraseSection('AutoRules');
Ini.WriteInteger('AutoRules', 'Count', Length(FAutoRules));
Ini.WriteBool ('AutoRules', 'AutoMode', FAutoMode);
for i := 0 to High(FAutoRules) do
begin
Ini.WriteBool ('AutoRules', 'R'+IntToStr(i)+'_En', FAutoRules[i].Enabled);
Ini.WriteInteger('AutoRules', 'R'+IntToStr(i)+'_SHH', FAutoRules[i].StartHH);
Ini.WriteInteger('AutoRules', 'R'+IntToStr(i)+'_SMM', FAutoRules[i].StartMM);
Ini.WriteInteger('AutoRules', 'R'+IntToStr(i)+'_EHH', FAutoRules[i].EndHH);
Ini.WriteInteger('AutoRules', 'R'+IntToStr(i)+'_EMM', FAutoRules[i].EndMM);
Ini.WriteInteger('AutoRules', 'R'+IntToStr(i)+'_DOC', FAutoRules[i].DOChannel);
Ini.WriteBool ('AutoRules', 'R'+IntToStr(i)+'_DOS', FAutoRules[i].DOState);
Ini.WriteInteger('AutoRules', 'R'+IntToStr(i)+'_CondMode', Ord(FAutoRules[i].CondMode));
Ini.WriteString ('AutoRules', 'R'+IntToStr(i)+'_Name', FAutoRules[i].RuleName);
Ini.WriteBool ('AutoRules', 'R'+IntToStr(i)+'_UseSched', FAutoRules[i].UseSchedule);
Ini.WriteInteger('AutoRules', 'R'+IntToStr(i)+'_WorkM', FAutoRules[i].WorkMinutes);
Ini.WriteInteger('AutoRules', 'R'+IntToStr(i)+'_RestM', FAutoRules[i].RestMinutes);
Ini.WriteInteger('AutoRules', 'R'+IntToStr(i)+'_CC', Length(FAutoRules[i].Conditions));
for c := 0 to High(FAutoRules[i].Conditions) do
begin
Ini.WriteString ('AutoRules', 'R'+IntToStr(i)+'_C'+IntToStr(c)+'_NID', FAutoRules[i].Conditions[c].NodeID);
Ini.WriteInteger('AutoRules', 'R'+IntToStr(i)+'_C'+IntToStr(c)+'_COP', Ord(FAutoRules[i].Conditions[c].Op));
Ini.WriteFloat ('AutoRules', 'R'+IntToStr(i)+'_C'+IntToStr(c)+'_CV', FAutoRules[i].Conditions[c].Value);
end;
end;
finally
Ini.Free;
end;
end;
// ─── 자동운전 규칙 평가 ───────────────────────────────────────────────────────
function TfrmMain.GetCurrentSensorValue(const ANodeID: string): Double;
var i: Integer;
begin
Result := 0;
for i := 0 to High(FNodeConfigs) do
if FNodeConfigs[i].ID = ANodeID then
begin
Result := FSensorValues[i];
Exit;
end;
end;
function TfrmMain.EvalOneCondition(const ACond: TAutoCondItem): Boolean;
var SVal: Double;
begin
if ACond.NodeID = '' then begin Result := True; Exit; end;
SVal := GetCurrentSensorValue(ACond.NodeID); // 실시간 노드 값 (펄스일 경우 1회당 누적 펄스값)
case ACond.Op of
acoGT: Result := SVal > ACond.Value;
acoLT: Result := SVal < ACond.Value;
acoEQ: Result := Abs(SVal - ACond.Value) < 0.001;
acoON: Result := SVal > 0; // 접점 닫힘 ( 1 (HIGH) 입력 )
acoOFF: Result := SVal <= 0; // 접점 열림 ( 0 (LOW) 입력 )
else
Result := True;
end;
end;
function TfrmMain.GetCurrentSensorValueTotalFlow(const ANodeID: string): Double;
var i: Integer;
begin
Result := 0;
for i := 0 to High(FNodeConfigs) do
if FNodeConfigs[i].ID = ANodeID then
begin
Result := FSensorValuesTotalFlow[i];
Exit;
end;
end;
function TfrmMain.EvalOneConditionTotalFlow(const ACond: TAutoCondItem): Boolean;
var SVal: Double; n: integer; strN: string;
function GetNodeIndex(const NodeID: string): Integer;
var i: Integer;
begin
Result := -1;
for i := 0 to High(frmMain.FNodeConfigs) do
if frmMain.FNodeConfigs[i].ID = NodeID then Exit(frmMain.FNodeConfigs[i].Index);
end;
begin
if ACond.NodeID = '' then begin Result := True; Exit; end;
SVal := GetCurrentSensorValue(ACond.NodeID);
if ACond.NodeID.StartsWith('DI_') then begin
n := GetNodeIndex(ACond.NodeID);
if n >= 0 then
begin
strN := frmMain.FNodeConfigs[n].CustomName + frmMain.FNodeConfigs[n].TitleKO + frmMain.FNodeConfigs[n].TitleEN;
if (strN.Contains('유량')) or (strN.Contains('Flow')) then
SVal := GetCurrentSensorValueTotalFlow(ACond.NodeID); // 실시간 노드 값 (펄스일 경우 구역 누적 펄스값)
end;
end;
case ACond.Op of
acoGT: Result := SVal > ACond.Value;
acoLT: Result := SVal < ACond.Value;
acoEQ: Result := Abs(SVal - ACond.Value) < 0.001;
acoON: Result := SVal > 0; // 접점 닫힘 ( 1 (HIGH) 입력 )
acoOFF: Result := SVal <= 0; // 접점 열림 ( 0 (LOW) 입력 )
else
Result := True;
end;
end;
function TfrmMain.EvalAutoRule(var ARule: TAutoRule): Boolean;
var
NowH, NowM: Word;
StartMin, EndMin, NowMin: Integer;
c: Integer;
CondResult: Boolean;
CycleTime, ElapsedMins: Integer;
begin
Result := False;
if not ARule.Enabled then Exit;
NowH := HourOf(Now);
NowM := MinuteOf(Now);
StartMin := ARule.StartHH * 60 + ARule.StartMM;
EndMin := ARule.EndHH * 60 + ARule.EndMM;
NowMin := NowH * 60 + NowM;
if EndMin >= StartMin then
Result := (NowMin >= StartMin) and (NowMin < EndMin)
else
Result := (NowMin >= StartMin) or (NowMin < EndMin);
if not Result then Exit;
if Length(ARule.Conditions) > 0 then
begin
if ARule.CondMode = acmAND then
begin
CondResult := True;
for c := 0 to High(ARule.Conditions) do
if not EvalOneCondition(ARule.Conditions[c]) then
begin CondResult := False; Break; end;
end else begin
CondResult := False;
for c := 0 to High(ARule.Conditions) do
if EvalOneCondition(ARule.Conditions[c]) then
begin CondResult := True; Break; end;
end;
Result := CondResult;
end;
if not Result then Exit;
// 스케줄 (반복 작동) 확인
if ARule.UseSchedule and (ARule.WorkMinutes > 0) and (ARule.RestMinutes >= 0) then
begin
CycleTime := ARule.WorkMinutes + ARule.RestMinutes;
if CycleTime > 0 then
begin
if NowMin >= StartMin then
ElapsedMins := NowMin - StartMin
else
ElapsedMins := (NowMin + 1440) - StartMin;
if (ElapsedMins mod CycleTime) >= ARule.WorkMinutes then
Result := False;
end;
end;
end;
procedure TfrmMain.CheckAutoRules;
var i: Integer; Active: Boolean;
begin
for i := 0 to High(FAutoRules) do
begin
Active := EvalAutoRule(FAutoRules[i]);
if Active <> FAutoRules[i].WasActive then
begin
FAutoRules[i].WasActive := Active;
if Active then
PublishControl(FAutoRules[i].DOChannel, FAutoRules[i].DOState)
else
PublishControl(FAutoRules[i].DOChannel, not FAutoRules[i].DOState);
// Update UI toggle switch manually after auto publish
if FControlPending[FAutoRules[i].DOChannel] = False then
begin
FControlPending[FAutoRules[i].DOChannel] := True;
if Active then FControlDesired[FAutoRules[i].DOChannel] := FAutoRules[i].DOState
else FControlDesired[FAutoRules[i].DOChannel] := not FAutoRules[i].DOState;
end;
end;
end;
end;
// ─── 자동운전 타이머 ─────────────────────────────────────────────────────────
procedure TfrmMain.TimerAutoTimer(Sender: TObject);
begin
TimerAuto.Enabled := False;
try
if FAutoMode then begin
Glyph1.ImageIndex := (Glyph1.ImageIndex + 1) mod 4;
CheckAutoRules;
CheckIrrigationSchedules;
end;
// if TimerAuto.Interval = 1000 then TimerAuto.Interval := 3000;
finally
TimerAuto.Enabled := True;
end;
end;