1055 lines
45 KiB
Plaintext
1055 lines
45 KiB
Plaintext
{
|
||
============================================================
|
||
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 := $FF379637; AddCondBtn.Stroke.Kind := TBrushKind.None; //#FF379637
|
||
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'), 13, 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;
|