1635 lines
65 KiB
Plaintext
1635 lines
65 KiB
Plaintext
{
|
|
============================================================
|
|
UIrrigation_Impl.inc - 스마트팜 HMI 관수 제어(순차 스케쥴)
|
|
============================================================
|
|
이 파일은 UMain.pas의 implementation 부에 include 됩니다. ($I UIrrigation_Impl.inc)
|
|
}
|
|
|
|
// ─── 헬퍼 함수 ────────────────────────────────────────────────────────────────
|
|
function EvalIrriConditions(const AConds: array of TAutoCondItem; AMode: TAutoCondMode; AEmptyResult: Boolean): Boolean;
|
|
var i: Integer;
|
|
begin
|
|
if Length(AConds) = 0 then Exit(AEmptyResult);
|
|
if AMode = acmOR then
|
|
begin
|
|
for i := 0 to High(AConds) do
|
|
if frmMain.EvalOneCondition(AConds[i]) then Exit(True);
|
|
Result := False; // 하나도 만족 안함
|
|
end
|
|
else
|
|
begin
|
|
for i := 0 to High(AConds) do
|
|
if not frmMain.EvalOneCondition(AConds[i]) then Exit(False);
|
|
Result := True; // 모두 만족
|
|
end;
|
|
end;
|
|
|
|
function EvalIrriConditionsTotalFlow(const AConds: array of TAutoCondItem; AMode: TAutoCondMode; AEmptyResult: Boolean): Boolean;
|
|
var i: Integer;
|
|
begin
|
|
if Length(AConds) = 0 then Exit(AEmptyResult);
|
|
if AMode = acmOR then
|
|
begin
|
|
for i := 0 to High(AConds) do
|
|
if frmMain.EvalOneConditionTotalFlow(AConds[i]) then Exit(True);
|
|
Result := False; // 하나도 만족 안함
|
|
end
|
|
else
|
|
begin
|
|
for i := 0 to High(AConds) do
|
|
if not frmMain.EvalOneConditionTotalFlow(AConds[i]) then Exit(False);
|
|
Result := True; // 모두 만족
|
|
end;
|
|
end;
|
|
|
|
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;
|
|
|
|
// ─── 유량계(1회당 유량) 초기화 함수 ────────────────────────────────────────────────────────────────
|
|
procedure TfrmMain.EvalIrriFlowInit(const AConds: array of TAutoCondItem);
|
|
var i,n: Integer; strN:string;
|
|
begin
|
|
if Length(AConds) = 0 then Exit;
|
|
for i := 0 to High(AConds) do begin
|
|
if AConds[i].NodeID.StartsWith('DI_') then begin
|
|
n := GetNodeIndex(AConds[i].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
|
|
frmMain.FSensorValues[n] := 0;
|
|
end;
|
|
end;
|
|
end;
|
|
end;
|
|
|
|
// ─── 유량계(구역 최대 유량) 초기화 함수 ────────────────────────────────────────────────────────────────
|
|
procedure TfrmMain.EvalIrriFlowInitTotalFlow(const AConds: array of TAutoCondItem);
|
|
var i,n: Integer; strN:string;
|
|
begin
|
|
if Length(AConds) = 0 then Exit;
|
|
for i := 0 to High(AConds) do begin
|
|
if AConds[i].NodeID.StartsWith('DI_') then begin
|
|
n := GetNodeIndex(AConds[i].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
|
|
frmMain.FSensorValuesTotalFlow[n] := 0;
|
|
end;
|
|
end;
|
|
end;
|
|
end;
|
|
|
|
// ─── 스케쥴 로드 / 세이브 ────────────────────────────────────────────────────────
|
|
procedure InitActionBaseValues(var AAct: TIrriAction);
|
|
procedure ProcessArray(var arr: TArray<TAutoCondItem>);
|
|
var i, n: Integer; strN: string;
|
|
begin
|
|
for i := 0 to High(arr) do
|
|
begin
|
|
arr[i].BaseValue := 0.0;
|
|
if arr[i].NodeID.StartsWith('DI_') then
|
|
begin
|
|
n := GetNodeIndex(arr[i].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 begin
|
|
frmMain.FSensorValues[n] := 0;
|
|
end;
|
|
end;
|
|
end;
|
|
end;
|
|
end;
|
|
begin
|
|
ProcessArray(AAct.StartConds);
|
|
ProcessArray(AAct.StopConds);
|
|
ProcessArray(AAct.EndConds);
|
|
end;
|
|
|
|
procedure ParseCondArray(JArr: TJSONArray; var AConds: TArray<TAutoCondItem>);
|
|
var k: Integer; JC: TJSONObject; OpInt: Integer;
|
|
begin
|
|
if not Assigned(JArr) then Exit;
|
|
SetLength(AConds, JArr.Count);
|
|
for k := 0 to JArr.Count - 1 do
|
|
begin
|
|
JC := JArr.Items[k] as TJSONObject;
|
|
JC.TryGetValue<string>('NodeID', AConds[k].NodeID);
|
|
if JC.TryGetValue<Integer>('Op', OpInt) then
|
|
AConds[k].Op := TAutoCondOp(OpInt);
|
|
JC.TryGetValue<Double>('Value', AConds[k].Value);
|
|
{
|
|
// 강제 보정 로직 추가: 과거 잘못 저장된 Op 값 교정
|
|
if (AConds[k].NodeID.StartsWith('DI_')) or (AConds[k].NodeID.StartsWith('DO_')) then
|
|
begin
|
|
if Ord(AConds[k].Op) < Ord(acoON) then
|
|
AConds[k].Op := acoON;
|
|
end
|
|
else
|
|
begin
|
|
if Ord(AConds[k].Op) >= Ord(acoON) then
|
|
AConds[k].Op := acoGT;
|
|
end;
|
|
}
|
|
end;
|
|
end;
|
|
|
|
function CondArrayToJSON(const AConds: TArray<TAutoCondItem>): TJSONArray;
|
|
var k: Integer; JC: TJSONObject;
|
|
begin
|
|
Result := TJSONArray.Create;
|
|
for k := 0 to High(AConds) do
|
|
begin
|
|
JC := TJSONObject.Create;
|
|
JC.AddPair('NodeID', AConds[k].NodeID);
|
|
JC.AddPair('Op', TJSONNumber.Create(Integer(AConds[k].Op)));
|
|
JC.AddPair('Value', TJSONNumber.Create(AConds[k].Value));
|
|
JC.AddPair('FOp', TJSONNumber.Create(Integer(AConds[k].FOp)));
|
|
JC.AddPair('PerPulse',TJSONNumber.Create(AConds[k].PerPulse));
|
|
Result.AddElement(JC);
|
|
end;
|
|
end;
|
|
|
|
procedure TfrmMain.LoadIrriSchedules;
|
|
var
|
|
JSONPath, Content: string;
|
|
JArr, JActArr, JCondArr: TJSONArray;
|
|
JSched, JAct: TJSONObject;
|
|
i, j: Integer;
|
|
StartDateStr, EndDateStr: string;
|
|
begin
|
|
SetLength(FIrriSchedules, 0);
|
|
JSONPath := System.IOUtils.TPath.Combine(System.IOUtils.TPath.GetHomePath, 'SmartFarmHMI_Irrigation.json');
|
|
if not FileExists(JSONPath) then Exit;
|
|
Content := System.IOUtils.TFile.ReadAllText(JSONPath, TEncoding.UTF8);
|
|
JArr := TJSONObject.ParseJSONValue(Content) as TJSONArray;
|
|
if not Assigned(JArr) then Exit;
|
|
try
|
|
SetLength(FIrriSchedules, JArr.Count);
|
|
for i := 0 to JArr.Count - 1 do
|
|
begin
|
|
JSched := JArr.Items[i] as TJSONObject;
|
|
FIrriSchedules[i].Enabled := False;
|
|
FIrriSchedules[i].IsRunning := False;
|
|
FIrriSchedules[i].WaitingManual := False;
|
|
FIrriSchedules[i].RepeatDays := 1;
|
|
FIrriSchedules[i].StartDate := Date;
|
|
FIrriSchedules[i].EndDate := Date + 365;
|
|
JSched.TryGetValue<Boolean>('Enabled', FIrriSchedules[i].Enabled);
|
|
JSched.TryGetValue<string> ('Name', FIrriSchedules[i].SchedName);
|
|
JSched.TryGetValue<Integer>('IsAutoStart', FIrriSchedules[i].IsAutoStart);
|
|
JSched.TryGetValue<Word> ('StartHH', FIrriSchedules[i].StartHH);
|
|
JSched.TryGetValue<Word> ('StartMM', FIrriSchedules[i].StartMM);
|
|
JSched.TryGetValue<Integer>('RepeatDays', FIrriSchedules[i].RepeatDays);
|
|
JSched.TryGetValue<Integer>('LimitOnesTimeMin', FIrriSchedules[i].LimitOnesTimeMin);
|
|
if JSched.TryGetValue<string>('StartDate', StartDateStr) then
|
|
FIrriSchedules[i].StartDate := StrToDateDef(StartDateStr, Date);
|
|
if JSched.TryGetValue<string>('EndDate', EndDateStr) then
|
|
FIrriSchedules[i].EndDate := StrToDateDef(EndDateStr, Date + 365);
|
|
|
|
JActArr := JSched.GetValue('Actions') as TJSONArray;
|
|
if Assigned(JActArr) then
|
|
begin
|
|
SetLength(FIrriSchedules[i].Actions, JActArr.Count);
|
|
for j := 0 to JActArr.Count - 1 do
|
|
begin
|
|
JAct := JActArr.Items[j] as TJSONObject;
|
|
JAct.TryGetValue<string> ('TargetNodeID', FIrriSchedules[i].Actions[j].TargetNodeID);
|
|
JAct.TryGetValue<Integer>('DelayMins', FIrriSchedules[i].Actions[j].DelayMins);
|
|
JAct.TryGetValue<Integer>('TimeoutMin', FIrriSchedules[i].Actions[j].TimeoutMin);
|
|
|
|
var ModeInt: Integer;
|
|
if JAct.TryGetValue<Integer>('StartCondMode', ModeInt) then FIrriSchedules[i].Actions[j].StartCondMode := TAutoCondMode(ModeInt) else FIrriSchedules[i].Actions[j].StartCondMode := acmAND;
|
|
if JAct.TryGetValue<Integer>('StopCondMode', ModeInt) then FIrriSchedules[i].Actions[j].StopCondMode := TAutoCondMode(ModeInt) else FIrriSchedules[i].Actions[j].StopCondMode := acmAND;
|
|
if JAct.TryGetValue<Integer>('EndCondMode', ModeInt) then FIrriSchedules[i].Actions[j].EndCondMode := TAutoCondMode(ModeInt) else FIrriSchedules[i].Actions[j].EndCondMode := acmAND;
|
|
JCondArr := JAct.GetValue('StartConds') as TJSONArray;
|
|
if Assigned(JCondArr) then ParseCondArray(JCondArr, FIrriSchedules[i].Actions[j].StartConds);
|
|
JCondArr := JAct.GetValue('StopConds') as TJSONArray;
|
|
if Assigned(JCondArr) then ParseCondArray(JCondArr, FIrriSchedules[i].Actions[j].StopConds);
|
|
JCondArr := JAct.GetValue('EndConds') as TJSONArray;
|
|
if Assigned(JCondArr) then ParseCondArray(JCondArr, FIrriSchedules[i].Actions[j].EndConds);
|
|
end;
|
|
end;
|
|
end;
|
|
finally
|
|
JArr.Free;
|
|
end;
|
|
end;
|
|
|
|
procedure TfrmMain.SaveIrriSchedules;
|
|
var
|
|
JSONPath: string;
|
|
JArr, JActArr: TJSONArray;
|
|
JSched, JAct: TJSONObject;
|
|
i, j: Integer;
|
|
begin
|
|
JArr := TJSONArray.Create;
|
|
try
|
|
for i := 0 to High(FIrriSchedules) do
|
|
begin
|
|
JSched := TJSONObject.Create;
|
|
JSched.AddPair('Enabled', TJSONBool.Create(FIrriSchedules[i].Enabled));
|
|
JSched.AddPair('Name', FIrriSchedules[i].SchedName);
|
|
JSched.AddPair('IsAutoStart', TJSONNumber.Create(FIrriSchedules[i].IsAutoStart));
|
|
JSched.AddPair('StartHH', TJSONNumber.Create(FIrriSchedules[i].StartHH));
|
|
JSched.AddPair('StartMM', TJSONNumber.Create(FIrriSchedules[i].StartMM));
|
|
JSched.AddPair('RepeatDays', TJSONNumber.Create(FIrriSchedules[i].RepeatDays));
|
|
JSched.AddPair('LimitOnesTimeMin', TJSONNumber.Create(FIrriSchedules[i].LimitOnesTimeMin));
|
|
JSched.AddPair('StartDate', DateToStr(FIrriSchedules[i].StartDate));
|
|
JSched.AddPair('EndDate', DateToStr(FIrriSchedules[i].EndDate));
|
|
JActArr := TJSONArray.Create;
|
|
for j := 0 to High(FIrriSchedules[i].Actions) do
|
|
begin
|
|
JAct := TJSONObject.Create;
|
|
JAct.AddPair('TargetNodeID', FIrriSchedules[i].Actions[j].TargetNodeID);
|
|
JAct.AddPair('DelayMins', TJSONNumber.Create(FIrriSchedules[i].Actions[j].DelayMins));
|
|
JAct.AddPair('TimeoutMin', TJSONNumber.Create(FIrriSchedules[i].Actions[j].TimeoutMin));
|
|
JAct.AddPair('StartCondMode', TJSONNumber.Create(Integer(FIrriSchedules[i].Actions[j].StartCondMode)));
|
|
JAct.AddPair('StopCondMode', TJSONNumber.Create(Integer(FIrriSchedules[i].Actions[j].StopCondMode)));
|
|
JAct.AddPair('EndCondMode', TJSONNumber.Create(Integer(FIrriSchedules[i].Actions[j].EndCondMode)));
|
|
JAct.AddPair('StartConds', CondArrayToJSON(FIrriSchedules[i].Actions[j].StartConds));
|
|
JAct.AddPair('StopConds', CondArrayToJSON(FIrriSchedules[i].Actions[j].StopConds));
|
|
JAct.AddPair('EndConds', CondArrayToJSON(FIrriSchedules[i].Actions[j].EndConds));
|
|
JActArr.AddElement(JAct);
|
|
end;
|
|
JSched.AddPair('Actions', JActArr);
|
|
JArr.AddElement(JSched);
|
|
end;
|
|
JSONPath := System.IOUtils.TPath.Combine(System.IOUtils.TPath.GetHomePath, 'SmartFarmHMI_Irrigation.json');
|
|
System.IOUtils.TFile.WriteAllText(JSONPath, JArr.Format(2), TEncoding.UTF8);
|
|
finally
|
|
JArr.Free;
|
|
end;
|
|
end;
|
|
|
|
// ─── 백엔드 상태 머신 (타이머 호출용) ──────────────────────────────────────────────
|
|
procedure TfrmMain.CheckIrrigationSchedules;
|
|
var
|
|
i: Integer;
|
|
Sched: ^TIrrigationSchedule;
|
|
CurAct: ^TIrriAction;
|
|
h, m, s, ms: Word;
|
|
NowTime: TDateTime;
|
|
ChIdx: Integer;
|
|
begin
|
|
NowTime := Now;
|
|
DecodeTime(NowTime, h, m, s, ms);
|
|
|
|
for i := 0 to High(FIrriSchedules) do
|
|
begin
|
|
Sched := @FIrriSchedules[i];
|
|
|
|
if not Sched.Enabled then
|
|
begin
|
|
Sched.IsRunning := False;
|
|
Sched.WaitingManual := False;
|
|
Continue;
|
|
end;
|
|
|
|
// ─── [1단계] 대기 중 → 트리거 시간 도달 확인 ────────────────────────────
|
|
if (not Sched.IsRunning) and (not Sched.WaitingManual) then
|
|
begin
|
|
if (Date >= Sched.StartDate) and (Date <= Sched.EndDate) and
|
|
((Sched.LastRunDate = 0) or (DaysBetween(Date, Sched.LastRunDate) >= Sched.RepeatDays)) and
|
|
(Sched.StartHH = h) and (Sched.StartMM = m) and (s < 5) then
|
|
begin
|
|
if Sched.IsAutoStart = 0 then
|
|
begin
|
|
Sched.IsRunning := True;
|
|
Sched.CurrentActionIdx := 0;
|
|
Sched.IsCoolingDown := False;
|
|
Sched.IsDelaying := True;
|
|
Sched.ActionStartTick := NowTime;
|
|
Sched.SchedStartTick := NowTime;
|
|
Sched.LastRunDate := Date;
|
|
SaveIrriSchedules;
|
|
AddSystemLog('관수 스케쥴 자동 시작: ' + Sched.SchedName);
|
|
TThread.Queue(nil, UpdateIrriDashboardUI);
|
|
end
|
|
else
|
|
begin
|
|
Sched.WaitingManual := True;
|
|
Sched.LastRunDate := Date;
|
|
AddSystemLog('관수 스케쥴 수동 대기: ' + Sched.SchedName);
|
|
TThread.Queue(nil, UpdateIrriDashboardUI);
|
|
ShowCustomAlert('관수 시작 대기', 'Irrigation Ready',
|
|
'[' + Sched.SchedName + '] 관수 시작 시간입니다. 현황 화면의 [시작] 버튼을 눌러주세요.',
|
|
'[' + Sched.SchedName + '] Irrigation time reached. Press [Start].');
|
|
end;
|
|
end;
|
|
Continue;
|
|
end;
|
|
|
|
if Sched.WaitingManual then Continue;
|
|
|
|
// ─── [4단계] 전체 완료 ───────────────────────────────────────────────────
|
|
if Sched.CurrentActionIdx >= Length(Sched.Actions) then
|
|
begin
|
|
Sched.IsRunning := False;
|
|
Sched.IsCoolingDown := False;
|
|
AddSystemLog('관수 스케쥴 완료: ' + Sched.SchedName);
|
|
TThread.Queue(nil, UpdateIrriDashboardUI);
|
|
Continue;
|
|
end;
|
|
|
|
CurAct := @Sched.Actions[Sched.CurrentActionIdx];
|
|
ChIdx := GetNodeIndex(CurAct.TargetNodeID);
|
|
|
|
// ─── 쿨다운 진행 중 ─────────────────────────────────────────────────────
|
|
if Sched.IsCoolingDown then
|
|
begin
|
|
if (NowTime >= Sched.CooldownEndTick) and (not EvalIrriConditions(CurAct.StopConds, CurAct.StopCondMode, False)) then
|
|
begin
|
|
Sched.IsCoolingDown := False;
|
|
if EvalIrriConditionsTotalFlow(CurAct.EndConds, CurAct.EndCondMode, False) then
|
|
begin
|
|
// 종료조건 충족 → 다음 구역
|
|
AddSystemLog('쿨다운 후 종료조건: ' + Sched.SchedName + ' -> ' + CurAct.TargetNodeID);
|
|
Inc(Sched.CurrentActionIdx);
|
|
Sched.IsDelaying := True;
|
|
Sched.ActionStartTick := NowTime;
|
|
end
|
|
else
|
|
begin
|
|
// 종료조건 미달 → 시작조건 재검사
|
|
Sched.IsDelaying := True;
|
|
AddSystemLog('쿨다운 완료, 재시작 대기: ' + Sched.SchedName + ' -> ' + CurAct.TargetNodeID);
|
|
end;
|
|
TThread.Queue(nil, UpdateIrriDashboardUI);
|
|
end;
|
|
Continue;
|
|
end;
|
|
|
|
// ─── [2단계] 시작조건 검사 대기 ─────────────────────────────────────────
|
|
if Sched.IsDelaying then
|
|
begin
|
|
if EvalIrriConditions(CurAct.StartConds, CurAct.StartCondMode, True) then
|
|
begin
|
|
Sched.IsDelaying := False;
|
|
Sched.ActionStartTick := NowTime;
|
|
InitActionBaseValues(CurAct^);
|
|
if ChIdx >= 0 then PublishControl(ChIdx, True);
|
|
AddSystemLog('동작 시작: ' + Sched.SchedName + ' -> ' + CurAct.TargetNodeID);
|
|
TThread.Queue(nil, UpdateIrriDashboardUI);
|
|
end;
|
|
Continue;
|
|
end;
|
|
|
|
// ─── [3단계] 동작 중 모니터링 ───────────────────────────────────────────
|
|
// 상황 A: 타임아웃 → 강제 다음 구역 스킵
|
|
var effectiveTimeout := CurAct.TimeoutMin;
|
|
if effectiveTimeout <= 0 then effectiveTimeout := Sched.LimitOnesTimeMin;
|
|
if (effectiveTimeout > 0) and
|
|
(MinutesBetween(NowTime, Sched.ActionStartTick) >= effectiveTimeout) then
|
|
begin
|
|
if ChIdx >= 0 then PublishControl(ChIdx, False);
|
|
AddSystemLog('[경고] 타임아웃 스킵: ' + Sched.SchedName + ' -> ' + CurAct.TargetNodeID);
|
|
Inc(Sched.CurrentActionIdx);
|
|
Sched.IsDelaying := True;
|
|
Sched.ActionStartTick := NowTime;
|
|
TThread.Queue(nil, UpdateIrriDashboardUI);
|
|
Continue;
|
|
end;
|
|
|
|
// 상황 B: 종료조건 → 정상 완료, 다음 구역
|
|
if Sched.IsRunning and EvalIrriConditionsTotalFlow(CurAct.EndConds, CurAct.EndCondMode, False) then
|
|
begin
|
|
if ChIdx >= 0 then PublishControl(ChIdx, False);
|
|
AddSystemLog('종료조건 달성: ' + Sched.SchedName + ' -> ' + CurAct.TargetNodeID);
|
|
EvalIrriFlowInitTotalFlow(CurAct.EndConds);
|
|
Inc(Sched.CurrentActionIdx);
|
|
Sched.IsDelaying := True;
|
|
Sched.ActionStartTick := NowTime;
|
|
TThread.Queue(nil, UpdateIrriDashboardUI);
|
|
Continue;
|
|
end;
|
|
|
|
// 상황 C: 정지조건 → 임시 정지 + 쿨다운
|
|
if Sched.IsRunning and EvalIrriConditions(CurAct.StopConds, CurAct.StopCondMode, False) then
|
|
begin
|
|
EvalIrriFlowInit(CurAct.StopConds);
|
|
if ChIdx >= 0 then PublishControl(ChIdx, False);
|
|
Sched.IsCoolingDown := True;
|
|
Sched.CooldownEndTick := NowTime + CurAct.DelayMins / MinsPerDay;
|
|
AddSystemLog('정지조건 달성, 쿨다운(' + IntToStr(CurAct.DelayMins) + '분): '
|
|
+ Sched.SchedName + ' -> ' + CurAct.TargetNodeID);
|
|
TThread.Queue(nil, UpdateIrriDashboardUI);
|
|
Continue;
|
|
end;
|
|
end;
|
|
end;
|
|
|
|
// ─── UI 생성 / 갱신 ─────────────────────────────────────────────────────────
|
|
procedure TfrmMain.BuildIrriUI;
|
|
begin
|
|
// 전체 스크린 레이아웃
|
|
lytScreenIrri := TLayout.Create(Self);
|
|
lytScreenIrri.Parent := lytMainContent;
|
|
lytScreenIrri.Align := TAlignLayout.Client;
|
|
lytScreenIrri.Visible := False;
|
|
|
|
// 상단 스케쥴 대시보드
|
|
lytIrriDashboard := TLayout.Create(Self);
|
|
lytIrriDashboard.Parent := lytScreenIrri;
|
|
lytIrriDashboard.Align := TAlignLayout.Top;
|
|
lytIrriDashboard.Height := 250;
|
|
lytIrriDashboard.Margins.Rect := RectF(20, 20, 20, 20);
|
|
|
|
var DashboardTitle := BuildLabel(lytIrriDashboard, '진행 상황 (Status)', 20, TAlignLayout.Top, claLightgray, True);
|
|
DashboardTitle.Height := 30;
|
|
|
|
// 스케쥴 등록 목록용 스크롤박스
|
|
lytIrriSettings := TLayout.Create(Self); // wrapper for settings
|
|
lytIrriSettings.Parent := lytScreenIrri;
|
|
lytIrriSettings.Align := TAlignLayout.Client;
|
|
|
|
var SettingsTitle := BuildLabel(lytIrriSettings, '스케쥴 목록 (Schedules)', 20, TAlignLayout.Top, claLightgray, True);
|
|
SettingsTitle.Height := 30;
|
|
SettingsTitle.Margins.Rect := RectF(20, 0, 20, 0);
|
|
|
|
scrlIrriSettings := TScrollBox.Create(Self);
|
|
scrlIrriSettings.Parent := lytIrriSettings;
|
|
scrlIrriSettings.Align := TAlignLayout.Client;
|
|
scrlIrriSettings.Margins.Rect := RectF(20, 10, 20, 20);
|
|
|
|
// 하단 버튼 라인 - 추가 + 선택삭제
|
|
var btnRowLyt := TLayout.Create(Self);
|
|
btnRowLyt.Parent := lytIrriSettings;
|
|
btnRowLyt.Align := TAlignLayout.Bottom;
|
|
btnRowLyt.Height := 57;
|
|
btnRowLyt.Margins.Rect := RectF(0, 0, 16, 0);
|
|
|
|
var btnAddSched := CreateCustomButton(btnRowLyt, '+ 새 스케쥴 추가', TAlignLayout.Client, 0, OnIrriScheduleAdd);
|
|
TLabel(btnAddSched.Controls[0]).TextSettings.Font.Size := 20;
|
|
btnAddSched.Fill.Color := $FF226622;
|
|
btnAddSched.Margins.Rect := RectF(16, 0, 16, 0);
|
|
|
|
var btnDelSel := CreateCustomButton(btnRowLyt, '🗑 ' + '선택 삭제', TAlignLayout.Right, 600, OnIrriScheduleDelSelected);
|
|
TLabel(btnDelSel.Controls[0]).TextSettings.Font.Size := 20;
|
|
btnDelSel.Fill.Color := $FFFF5252;
|
|
btnDelSel.Margins.Rect := RectF(0, 0, 0, 0);
|
|
end;
|
|
|
|
procedure TfrmMain.BuildIrriActionPopupUI;
|
|
var
|
|
Bg, Dlg: TRectangle;
|
|
BottomLyt: TLayout;
|
|
BtnAdd, BtnClose: TRectangle;
|
|
TopDelBtn: TRectangle;
|
|
begin
|
|
lytIrriActionPopup := TLayout.Create(Self);
|
|
lytIrriActionPopup.Parent := lytMainContent;
|
|
lytIrriActionPopup.Align := TAlignLayout.Client;
|
|
lytIrriActionPopup.Visible := False;
|
|
|
|
Bg := TRectangle.Create(Self);
|
|
Bg.Parent := lytIrriActionPopup;
|
|
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 := lytIrriActionPopup;
|
|
Dlg.Align := TAlignLayout.Center;
|
|
Dlg.Width := 1000;
|
|
Dlg.Height := 800;
|
|
Dlg.Fill.Color := $FF222222;
|
|
Dlg.Stroke.Color := $FF444444;
|
|
Dlg.XRadius := 10; Dlg.YRadius := 10;
|
|
|
|
lblIrriActionTitle := BuildLabel(Dlg, '최상위 스케쥴 상세 동작 관리', 24, TAlignLayout.Top, claWhite, True);
|
|
lblIrriActionTitle.Height := 65;
|
|
lblIrriActionTitle.Margins.Rect := RectF(16, 16, 16, 0);
|
|
|
|
TopDelBtn := CreateCustomButton(lblIrriActionTitle, '🗑 ' + '선택 삭제', TAlignLayout.Right, 200, OnIrriActionDelSelected);
|
|
TLabel(TopDelBtn.Controls[0]).TextSettings.Font.Size := 20;
|
|
TopDelBtn.Fill.Color := $FFFF5252;
|
|
|
|
scrlIrriAction := TScrollBox.Create(Self);
|
|
scrlIrriAction.Parent := Dlg;
|
|
scrlIrriAction.Align := TAlignLayout.Client;
|
|
scrlIrriAction.Margins.Rect := RectF(16, 16, 16, 16);
|
|
|
|
BottomLyt := TLayout.Create(Self);
|
|
BottomLyt.Parent := Dlg;
|
|
BottomLyt.Align := TAlignLayout.Bottom;
|
|
BottomLyt.Height := 60;
|
|
|
|
BtnAdd := CreateCustomButton(BottomLyt, '+ 동작(구역) 추가', TAlignLayout.Left, 350, OnIrriActionAdd);
|
|
TLabel(BtnAdd.Controls[0]).TextSettings.Font.Size := 20;
|
|
BtnAdd.Margins.Rect := RectF(16, 0, 16, 16);
|
|
|
|
BtnClose := CreateCustomButton(BottomLyt, '닫기 (저장)', TAlignLayout.Right, 350, OnIrriActionPopupClose);
|
|
TLabel(BtnClose.Controls[0]).TextSettings.Font.Size := 20;
|
|
BtnClose.Margins.Rect := RectF(16, 0, 16, 16);
|
|
end;
|
|
|
|
procedure TfrmMain.UpdateIrriDashboardUI;
|
|
var
|
|
i: Integer;
|
|
StatStr: string;
|
|
Lbl: TLabel;
|
|
BtnStart: TRectangle;
|
|
DashLyt: TLayout;
|
|
begin
|
|
if lytIrriDashboard = nil then Exit;
|
|
|
|
// 기존 동적 컨트롤 제거
|
|
for i := lytIrriDashboard.ControlsCount - 1 downto 1 do
|
|
lytIrriDashboard.Controls[i].Parent := nil;
|
|
|
|
StatStr := '';
|
|
for i := 0 to High(FIrriSchedules) do
|
|
begin
|
|
if FIrriSchedules[i].WaitingManual then
|
|
StatStr := StatStr + Format('[%s] 🔔 수동 시작 대기 중', [FIrriSchedules[i].SchedName]) + sLineBreak
|
|
else if FIrriSchedules[i].IsRunning then
|
|
begin
|
|
var ActIdx := FIrriSchedules[i].CurrentActionIdx;
|
|
var ActTotal := Length(FIrriSchedules[i].Actions);
|
|
var ActName := '';
|
|
if (ActIdx >= 0) and (ActIdx < ActTotal) then
|
|
ActName := FIrriSchedules[i].Actions[ActIdx].TargetNodeID;
|
|
var StateStr: string;
|
|
if FIrriSchedules[i].IsCoolingDown then
|
|
StateStr := '쿨다운 대기 중'
|
|
else if FIrriSchedules[i].IsDelaying then
|
|
StateStr := '시작 조건 대기'
|
|
else
|
|
StateStr := '동작 중 ▶';
|
|
StatStr := StatStr + Format('[%s] %s 구역: %s (%d/%d)',
|
|
[FIrriSchedules[i].SchedName, StateStr, ActName, ActIdx + 1, ActTotal]) + sLineBreak;
|
|
end;
|
|
end;
|
|
|
|
if StatStr = '' then
|
|
StatStr := '현재 진행 중인 관수 스케쥴이 없습니다.';
|
|
|
|
Lbl := BuildLabel(lytIrriDashboard, StatStr, 18, TAlignLayout.Client, claWhite, False);
|
|
Lbl.Margins.Rect := RectF(10, 10, 10, 10);
|
|
Lbl.WordWrap := True;
|
|
|
|
// 수동 대기 중인 스케쥴이 있으면 [시작] 버튼 생성
|
|
for i := 0 to High(FIrriSchedules) do
|
|
begin
|
|
if FIrriSchedules[i].WaitingManual then
|
|
begin
|
|
BtnStart := CreateCustomButton(lytIrriDashboard,
|
|
'▶ [' + FIrriSchedules[i].SchedName + '] 시작',
|
|
TAlignLayout.Bottom, 0, OnIrriManualStartStop);
|
|
BtnStart.Fill.Color := $FF226622;
|
|
BtnStart.Height := 50;
|
|
BtnStart.Tag := i;
|
|
BtnStart.Margins.Rect := RectF(10, 0, 10, 5);
|
|
end;
|
|
end;
|
|
end;
|
|
|
|
procedure TfrmMain.RefreshIrriUI;
|
|
var
|
|
i: Integer;
|
|
CardH: Single;
|
|
Card: TRectangle;
|
|
LHeader: TLabel;
|
|
SW_En: TSwitch;
|
|
EdName, EdStartHH, EdStartMM, EdRepeatDays, EdLimitOnesTime: TEdit;
|
|
ChkDel: TCheckBox;
|
|
DtpSd, DtpEd: TDateEdit;
|
|
cbIsAutoStart: TCombobox;
|
|
begin
|
|
if FRefreshingAutoUI then Exit;
|
|
FRefreshingAutoUI := True;
|
|
try
|
|
scrlIrriSettings.BeginUpdate;
|
|
if Self.Focused <> nil then Self.Focused := nil;
|
|
|
|
for i := scrlIrriSettings.Content.ControlsCount - 1 downto 0 do
|
|
begin
|
|
var C := scrlIrriSettings.Content.Controls[i];
|
|
if C is TEdit then (C as TEdit).OnExit := nil;
|
|
if C is TSwitch then (C as TSwitch).OnSwitch := nil;
|
|
if C is TCheckBox then (C as TCheckBox).OnChange := nil;
|
|
if C is TDateEdit then (C as TDateEdit).OnChange := nil;
|
|
if C is TCombobox then (C as TCombobox).OnChange := nil;
|
|
C.Parent := nil; // macOS 안정
|
|
end;
|
|
|
|
if Length(FIrriSchedules) = 0 then
|
|
begin
|
|
var LEmpty := BuildLabel(scrlIrriSettings, '등록된 스케쥴이 없습니다.', 18, TAlignLayout.Top, claLightgray, False);
|
|
LEmpty.Margins.Top := 20;
|
|
LEmpty.TextSettings.HorzAlign := TTextAlign.Center;
|
|
end;
|
|
|
|
for i := 0 to High(FIrriSchedules) do
|
|
begin
|
|
CardH := 100;
|
|
Card := TRectangle.Create(Self);
|
|
Card.Parent := scrlIrriSettings;
|
|
Card.Align := TAlignLayout.Top;
|
|
Card.Height := CardH;
|
|
Card.Margins.Rect := RectF(0, 0, 0, 10);
|
|
Card.Fill.Color := IfThen(FIrriSchedules[i].IsSelected, $FF3A2020, $FF2D2D2D);
|
|
Card.Stroke.Color := $FF555555;
|
|
Card.XRadius := 8; Card.YRadius := 8;
|
|
|
|
// 체크박스 (선택용)
|
|
{$IFDEF ANDROID}
|
|
var ChkBg := TRectangle.Create(Self);
|
|
ChkBg.Parent := Card;
|
|
ChkBg.Position.X := 12; ChkBg.Position.Y := 17;
|
|
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 := 10; ChkDel.Position.Y := 14;
|
|
ChkDel.Width := 24; ChkDel.Height := 24;
|
|
ChkDel.IsChecked := FIrriSchedules[i].IsSelected;
|
|
ChkDel.Tag := i;
|
|
ChkDel.Scale.X := 1.5; ChkDel.Scale.Y := 1.5;
|
|
ChkDel.OnChange := OnIrriScheduleCheck;
|
|
|
|
LHeader := BuildLabel(Card, '스케쥴명 : ', 20, TAlignLayout.None, claLightgray, True);
|
|
LHeader.SetBounds(60, 16, 100, 30);
|
|
|
|
EdName := TEdit.Create(Self);
|
|
EdName.Parent := Card;
|
|
EdName.SetBounds(170, 16, 160, 30);
|
|
EdName.Text := FIrriSchedules[i].SchedName;
|
|
EdName.Tag := i;
|
|
EdName.TagFloat := 1;
|
|
EdName.StyledSettings := EdName.StyledSettings - [TStyledSetting.Size, TStyledSetting.FontColor];
|
|
EdName.TextSettings.Font.Size := 18;
|
|
{$IFDEF ANDROID}
|
|
EdName.TextSettings.FontColor := claWhite;
|
|
{$ENDIF}
|
|
EdName.OnExit := OnIrriScheduleEdit;
|
|
|
|
BuildLabel(Card, '활성화 :', 20, TAlignLayout.None, claLightgray, False).SetBounds(920, 16, 80, 30);
|
|
SW_En := TSwitch.Create(Self);
|
|
SW_En.Parent := Card;
|
|
SW_En.SetBounds(1080, 16, 120, 30);
|
|
SW_En.IsChecked := FIrriSchedules[i].Enabled;
|
|
SW_En.Tag := i; SW_En.TagFloat := 0;
|
|
SW_En.OnSwitch := OnIrriScheduleEdit;
|
|
|
|
// ── 시작일자 / 종료일자 ──────────────────────────────────────────────
|
|
BuildLabel(Card, '시작일자 :', 20, TAlignLayout.None, claLightgray, False).SetBounds(60, 60, 100, 30);
|
|
DtpSd := BuildDateEdit(Card, FIrriSchedules[i].StartDate, 18, TAlignLayout.None, claBlack, False);
|
|
DtpSd.SetBounds(170, 60, 160, 30);
|
|
DtpSd.Tag := i;
|
|
DtpSd.TagFloat := 2; // StartDate 필드
|
|
{$IFDEF ANDROID}
|
|
DtpSd.TextSettings.FontColor := claWhite;
|
|
{$ENDIF}
|
|
DtpSd.OnChange := OnIrriScheduleEdit;
|
|
|
|
BuildLabel(Card, '종료일자 :', 20, TAlignLayout.None, claLightgray, False).SetBounds(350, 60, 100, 30);
|
|
DtpEd := BuildDateEdit(Card, FIrriSchedules[i].EndDate, 18, TAlignLayout.None, claBlack, False);
|
|
DtpEd.SetBounds(450, 60, 160, 30);
|
|
DtpEd.Tag := i;
|
|
DtpEd.TagFloat := 3; // EndDate 필드
|
|
{$IFDEF ANDROID}
|
|
DtpEd.TextSettings.FontColor := claWhite;
|
|
{$ENDIF}
|
|
DtpEd.OnChange := OnIrriScheduleEdit;
|
|
|
|
// ── 시작시간 ──────────────────────────────────────────────
|
|
BuildLabel(Card, '시작시간 :', 20, TAlignLayout.None, claLightgray, False).SetBounds(630, 60, 130, 30);
|
|
EdStartHH := TEdit.Create(Self); EdStartHH.Parent := Card;
|
|
EdStartHH.SetBounds(775, 60, 50, 30);
|
|
EdStartHH.TextSettings.Font.Size := 18; EdStartHH.MaxLength := 2;
|
|
EdStartHH.Text := Format('%.2d', [FIrriSchedules[i].StartHH]);
|
|
EdStartHH.StyledSettings := EdStartHH.StyledSettings - [TStyledSetting.Size, TStyledSetting.FontColor];
|
|
EdStartHH.FilterChar := '0123456789';
|
|
{$IFDEF ANDROID}
|
|
EdStartHH.TextSettings.FontColor := claWhite;
|
|
{$ENDIF}
|
|
EdStartHH.TagFloat := 4; // StartHH 필드
|
|
EdStartHH.Tag := i; EdStartHH.OnExit := OnIrriScheduleEdit;
|
|
|
|
BuildLabel(Card, ':', 18, TAlignLayout.None, claWhite, True).SetBounds(830, 60, 10, 30);
|
|
EdStartMM := TEdit.Create(Self); EdStartMM.Parent := Card;
|
|
EdStartMM.SetBounds(840, 60, 50, 30);
|
|
EdStartMM.TextSettings.Font.Size := 18; EdStartMM.MaxLength := 2;
|
|
EdStartMM.Text := Format('%.2d', [FIrriSchedules[i].StartMM]);
|
|
EdStartMM.StyledSettings := EdStartMM.StyledSettings - [TStyledSetting.Size, TStyledSetting.FontColor];
|
|
EdStartMM.FilterChar := '0123456789';
|
|
{$IFDEF ANDROID}
|
|
EdStartMM.TextSettings.FontColor := claWhite;
|
|
{$ENDIF}
|
|
EdStartMM.TagFloat := 5; // StartHH 필드
|
|
EdStartMM.Tag := i; EdStartMM.OnExit := OnIrriScheduleEdit;
|
|
|
|
// ── 반복주기(일) ──────────────────────────────────────────────
|
|
BuildLabel(Card, '반복주기(일) :', 20, TAlignLayout.None, claLightgray, False).SetBounds(350, 16, 130, 30);
|
|
EdRepeatDays := TEdit.Create(Self); EdRepeatDays.Parent := Card;
|
|
EdRepeatDays.SetBounds(560, 16, 50, 30);
|
|
EdRepeatDays.TextSettings.Font.Size := 18; EdRepeatDays.MaxLength := 2;
|
|
EdRepeatDays.Text := Format('%.d', [FIrriSchedules[i].RepeatDays]);
|
|
EdRepeatDays.StyledSettings := EdRepeatDays.StyledSettings - [TStyledSetting.Size, TStyledSetting.FontColor];
|
|
EdRepeatDays.FilterChar := '0123456789';
|
|
{$IFDEF ANDROID}
|
|
EdRepeatDays.TextSettings.FontColor := claWhite;
|
|
{$ENDIF}
|
|
EdRepeatDays.TagFloat := 6; // RepeatDays 필드
|
|
EdRepeatDays.Tag := i; EdRepeatDays.OnExit := OnIrriScheduleEdit;
|
|
|
|
// ── 1회당 제한시간(분) ──────────────────────────────────────────────
|
|
BuildLabel(Card, '1회당 제한시간(분) :', 20, TAlignLayout.None, claLightgray, False).SetBounds(630, 16, 200, 30);
|
|
EdLimitOnesTime := TEdit.Create(Self); EdLimitOnesTime.Parent := Card;
|
|
EdLimitOnesTime.SetBounds(840, 16, 50, 30);
|
|
EdLimitOnesTime.TextSettings.Font.Size := 18; EdLimitOnesTime.MaxLength := 2;
|
|
EdLimitOnesTime.Text := Format('%.d', [FIrriSchedules[i].LimitOnesTimeMin]);
|
|
EdLimitOnesTime.StyledSettings := EdLimitOnesTime.StyledSettings - [TStyledSetting.Size, TStyledSetting.FontColor];
|
|
EdLimitOnesTime.FilterChar := '0123456789';
|
|
{$IFDEF ANDROID}
|
|
EdLimitOnesTime.TextSettings.FontColor := claWhite;
|
|
{$ENDIF}
|
|
EdLimitOnesTime.TagFloat := 7;
|
|
EdLimitOnesTime.Tag := i; EdLimitOnesTime.OnExit := OnIrriScheduleEdit;
|
|
|
|
// ── 스케쥴 자동 실행 여부 ──────────────────────────────────────────────
|
|
BuildLabel(Card, '자동 실행 여부:', 20, TAlignLayout.None, claLightgray, False).SetBounds(920, 60, 200, 30);
|
|
cbIsAutoStart := TComboBox.Create(Self); cbIsAutoStart.Parent := Card;
|
|
cbIsAutoStart.SetBounds(1080, 60, 120, 30);
|
|
var x: Integer;
|
|
for x := 0 to 1 do
|
|
begin
|
|
var LItms := TListBoxItem.Create(cbIsAutoStart);
|
|
LItms.Parent := cbIsAutoStart;
|
|
LItms.Height := 24 * 1.8;
|
|
LItms.StyledSettings := LItms.StyledSettings - [TStyledSetting.Size, TStyledSetting.FontColor];
|
|
LItms.TextSettings.Font.Size := 18;
|
|
if x = 0 then LItms.Text := '자동(즉시)' else LItms.Text := '수동(알림)';
|
|
{$IFDEF ANDROID}
|
|
LItms.TextSettings.FontColor := claWhite;
|
|
{$ENDIF}
|
|
cbIsAutoStart.AddObject(LItms);
|
|
end;
|
|
cbIsAutoStart.ItemIndex := FIrriSchedules[i].IsAutoStart;
|
|
cbIsAutoStart.Tag := i; cbIsAutoStart.TagFloat := 8;
|
|
cbIsAutoStart.OnChange := OnIrriScheduleEdit;
|
|
|
|
var btnDetail := CreateCustomButton(Card, '동작 관리', TAlignLayout.None, 160, OnIrriActionManageClick);
|
|
btnDetail.SetBounds(1240, 8, 160, 84);
|
|
btnDetail.Tag := i;
|
|
end;
|
|
finally
|
|
scrlIrriSettings.EndUpdate;
|
|
FRefreshingAutoUI := False;
|
|
end;
|
|
end;
|
|
|
|
// ─── UI 이벤트 핸들러 ────────────────────────────────────────────────────────
|
|
|
|
procedure TfrmMain.OnIrriNavClick(Sender: TObject);
|
|
begin
|
|
if Sender is TRectangle then
|
|
SwitchScreen(TRectangle(Sender).Tag);
|
|
end;
|
|
|
|
procedure TfrmMain.OnIrriScheduleAdd(Sender: TObject);
|
|
var S: TIrrigationSchedule;
|
|
begin
|
|
FillChar(S, SizeOf(S), 0);
|
|
S.Enabled := False;
|
|
S.SchedName := '새 스케쥴 ' + IntToStr(Length(FIrriSchedules)+1);
|
|
S.IsAutoStart := 0; // 0 : 즉시 , 1 : 수동
|
|
S.StartHH := 6; S.StartMM := 0;
|
|
S.RepeatDays := 1;
|
|
S.StartDate := Date;
|
|
S.EndDate := Date + 365;
|
|
S.IsSelected := False;
|
|
S.IsRunning := False;
|
|
S.WaitingManual := False;
|
|
S.IsCoolingDown := False;
|
|
SetLength(S.Actions, 0);
|
|
|
|
SetLength(FIrriSchedules, Length(FIrriSchedules)+1);
|
|
FIrriSchedules[High(FIrriSchedules)] := S;
|
|
SaveIrriSchedules;
|
|
AddSystemLog('시스템 설정 변경: 새 관수 스케쥴 추가됨');
|
|
TThread.Queue(nil, RefreshIrriUI);
|
|
end;
|
|
|
|
procedure TfrmMain.OnIrriScheduleCheck(Sender: TObject);
|
|
var Idx: Integer;
|
|
begin
|
|
if FRefreshingAutoUI then Exit; // UI 재뱌드 중 이벤트 무시
|
|
if Sender is TCheckBox then
|
|
begin
|
|
Idx := TCheckBox(Sender).Tag;
|
|
if (Idx >= 0) and (Idx <= High(FIrriSchedules)) then
|
|
FIrriSchedules[Idx].IsSelected := TCheckBox(Sender).IsChecked;
|
|
end;
|
|
end;
|
|
|
|
procedure TfrmMain.OnIrriScheduleDel(Sender: TObject);
|
|
begin
|
|
// Not used in checkbox mode
|
|
end;
|
|
|
|
procedure TfrmMain.OnIrriScheduleDelSelected(Sender: TObject);
|
|
var i, Cnt: Integer;
|
|
begin
|
|
Cnt := 0;
|
|
for i := High(FIrriSchedules) downto 0 do
|
|
if FIrriSchedules[i].IsSelected then
|
|
begin
|
|
Delete(FIrriSchedules, i, 1);
|
|
Inc(Cnt);
|
|
end;
|
|
if Cnt = 0 then Exit;
|
|
SaveIrriSchedules;
|
|
AddSystemLog('시스템 설정 변경: 선택 스케쥴 ' + IntToStr(Cnt) + '개 삭제');
|
|
TThread.Queue(nil, RefreshIrriUI);
|
|
end;
|
|
|
|
procedure TfrmMain.OnIrriScheduleEdit(Sender: TObject);
|
|
var Idx, FieldID: Integer;
|
|
begin
|
|
if FRefreshingAutoUI then Exit;
|
|
Idx := -1; FieldID := -1;
|
|
|
|
if Sender is TSwitch then
|
|
begin
|
|
Idx := (Sender as TSwitch).Tag;
|
|
FieldID := Trunc((Sender as TSwitch).TagFloat);
|
|
end
|
|
else if Sender is TEdit then
|
|
begin
|
|
Idx := (Sender as TEdit).Tag;
|
|
FieldID := Trunc((Sender as TEdit).TagFloat);
|
|
end
|
|
else if Sender is TDateEdit then
|
|
begin
|
|
Idx := (Sender as TDateEdit).Tag;
|
|
FieldID := Trunc((Sender as TDateEdit).TagFloat);
|
|
end
|
|
else if Sender is TComboBox then
|
|
begin
|
|
Idx := (Sender as TComboBox).Tag;
|
|
FieldID := Trunc((Sender as TComboBox).TagFloat);
|
|
end;
|
|
|
|
if (Idx < 0) or (Idx > High(FIrriSchedules)) then Exit;
|
|
|
|
case FieldID of
|
|
0: if Sender is TSwitch then FIrriSchedules[Idx].Enabled := (Sender as TSwitch).IsChecked;
|
|
1: if Sender is TEdit then FIrriSchedules[Idx].SchedName := (Sender as TEdit).Text;
|
|
2: if Sender is TDateEdit then FIrriSchedules[Idx].StartDate := (Sender as TDateEdit).Date;
|
|
3: if Sender is TDateEdit then FIrriSchedules[Idx].EndDate := (Sender as TDateEdit).Date;
|
|
4: if Sender is TEdit then FIrriSchedules[Idx].StartHH := StrToIntDef((Sender as TEdit).Text, 0) mod 24;
|
|
5: if Sender is TEdit then FIrriSchedules[Idx].StartMM := StrToIntDef((Sender as TEdit).Text, 0) mod 60;
|
|
6: if Sender is TEdit then FIrriSchedules[Idx].RepeatDays := StrToIntDef((Sender as TEdit).Text, 0);
|
|
7: if Sender is TEdit then FIrriSchedules[Idx].LimitOnesTimeMin := StrToIntDef((Sender as TEdit).Text, 0);
|
|
8: if Sender is TComboBox then FIrriSchedules[Idx].IsAutoStart:= (Sender as TComboBox).ItemIndex;
|
|
end;
|
|
|
|
SaveIrriSchedules;
|
|
AddSystemLog('시스템 설정 변경: 관수 스케쥴 [' + FIrriSchedules[Idx].SchedName + '] 수정됨');
|
|
end;
|
|
|
|
procedure TfrmMain.OnIrriCondEdit(Sender: TObject);
|
|
var
|
|
ActIdx, CondType, CondIdx, FieldID: Integer;
|
|
ValPart: Integer;
|
|
Act: ^TIrriAction;
|
|
ValStr: string;
|
|
begin
|
|
if FRefreshingAutoUI then Exit;
|
|
|
|
if Sender is TComboBox then
|
|
begin
|
|
ActIdx := (Sender as TComboBox).Tag;
|
|
ValPart := Trunc((Sender as TComboBox).TagFloat);
|
|
end
|
|
else if Sender is TEdit then
|
|
begin
|
|
ActIdx := (Sender as TEdit).Tag;
|
|
ValPart := Trunc((Sender as TEdit).TagFloat);
|
|
end
|
|
else Exit;
|
|
|
|
if (ActIdx < 0) or (ActIdx > High(FIrriSchedules[FCurrentEditIrriSchedIdx].Actions)) then Exit;
|
|
Act := @FIrriSchedules[FCurrentEditIrriSchedIdx].Actions[ActIdx];
|
|
|
|
CondType := ValPart div 1000;
|
|
CondIdx := (ValPart div 10) mod 100;
|
|
FieldID := ValPart mod 10;
|
|
|
|
var PCondArray: ^TArray<TAutoCondItem>;
|
|
var PCondMode: ^TAutoCondMode;
|
|
case CondType of
|
|
0: begin PCondArray := @Act^.StartConds; PCondMode := @Act^.StartCondMode; end;
|
|
1: begin PCondArray := @Act^.StopConds; PCondMode := @Act^.StopCondMode; end;
|
|
2: begin PCondArray := @Act^.EndConds; PCondMode := @Act^.EndCondMode; end;
|
|
else Exit;
|
|
end;
|
|
|
|
if FieldID = 3 then // AND/OR Mode
|
|
begin
|
|
if (Sender as TComboBox).ItemIndex = 0 then PCondMode^ := acmAND
|
|
else PCondMode^ := acmOR;
|
|
end
|
|
else if (CondIdx >= 0) and (CondIdx < Length(PCondArray^)) then
|
|
begin
|
|
case FieldID of
|
|
0: // NodeID
|
|
begin
|
|
ValStr := (Sender as TComboBox).Items[(Sender as TComboBox).ItemIndex];
|
|
var ss := ValStr.Split([' ']); // ID만 추출
|
|
if Length(ss) > 0 then
|
|
begin
|
|
var strId := ss[0];
|
|
|
|
PCondArray^[CondIdx].Op := TAutoCondOp(0);
|
|
|
|
PCondArray^[CondIdx].NodeID := strId;
|
|
end;
|
|
// 상태에 맞게 Op 콤보박스 아이템 및 Value Enabled 처리를 위해 UI 재갱신
|
|
TThread.Queue(nil, procedure begin RefreshIrriActionPopupUI(FCurrentEditIrriSchedIdx); end);
|
|
end;
|
|
1: // Op
|
|
PCondArray^[CondIdx].Op := TAutoCondOp((Sender as TComboBox).ItemIndex);
|
|
2: // Value
|
|
PCondArray^[CondIdx].Value := StrToFloatDef((Sender as TEdit).Text, 0.0);
|
|
4: // FOp
|
|
begin
|
|
PCondArray^[CondIdx].FOp := TAutoCondFOp((Sender as TComboBox).ItemIndex);
|
|
|
|
TThread.Queue(nil, procedure begin RefreshIrriActionPopupUI(FCurrentEditIrriSchedIdx); end);
|
|
end;
|
|
5: // PerPulse
|
|
begin
|
|
PCondArray^[CondIdx].PerPulse := StrToFloatDef((Sender as TEdit).Text, 0.0);
|
|
end;
|
|
end;
|
|
end;
|
|
|
|
SaveIrriSchedules;
|
|
end;
|
|
|
|
procedure TfrmMain.OnIrriCondAdd(Sender: TObject);
|
|
var
|
|
ActIdx, CondType: Integer;
|
|
Act: ^TIrriAction;
|
|
Item: TAutoCondItem;
|
|
begin
|
|
if FRefreshingAutoUI then Exit;
|
|
if not (Sender is TRectangle) then Exit;
|
|
|
|
ActIdx := (Sender as TRectangle).Tag;
|
|
CondType := Trunc((Sender as TRectangle).TagFloat) div 1000;
|
|
|
|
if (ActIdx < 0) or (ActIdx > High(FIrriSchedules[FCurrentEditIrriSchedIdx].Actions)) then Exit;
|
|
Act := @FIrriSchedules[FCurrentEditIrriSchedIdx].Actions[ActIdx];
|
|
|
|
Item.NodeID := '';
|
|
Item.Op := acoGT;
|
|
Item.Value := 0.0;
|
|
Item.FOp := acfoPulse;
|
|
Item.PerPulse := 0.0;
|
|
|
|
case CondType of
|
|
0: begin SetLength(Act^.StartConds, Length(Act^.StartConds)+1); Act^.StartConds[High(Act^.StartConds)] := Item; end;
|
|
1: begin SetLength(Act^.StopConds, Length(Act^.StopConds)+1); Act^.StopConds[High(Act^.StopConds)] := Item; end;
|
|
2: begin SetLength(Act^.EndConds, Length(Act^.EndConds)+1); Act^.EndConds[High(Act^.EndConds)] := Item; end;
|
|
end;
|
|
|
|
SaveIrriSchedules;
|
|
RefreshIrriActionPopupUI(FCurrentEditIrriSchedIdx);
|
|
end;
|
|
|
|
procedure TfrmMain.OnIrriCondDel(Sender: TObject);
|
|
var
|
|
ActIdx, CondType, CondIdx: Integer;
|
|
ValPart: Integer;
|
|
Act: ^TIrriAction;
|
|
begin
|
|
if FRefreshingAutoUI then Exit;
|
|
if not (Sender is TRectangle) then Exit;
|
|
|
|
ActIdx := (Sender as TRectangle).Tag;
|
|
ValPart := Trunc((Sender as TRectangle).TagFloat);
|
|
CondType := ValPart div 1000;
|
|
CondIdx := (ValPart div 10) mod 100;
|
|
|
|
if (ActIdx < 0) or (ActIdx > High(FIrriSchedules[FCurrentEditIrriSchedIdx].Actions)) then Exit;
|
|
Act := @FIrriSchedules[FCurrentEditIrriSchedIdx].Actions[ActIdx];
|
|
|
|
case CondType of
|
|
0: Delete(Act^.StartConds, CondIdx, 1);
|
|
1: Delete(Act^.StopConds, CondIdx, 1);
|
|
2: Delete(Act^.EndConds, CondIdx, 1);
|
|
end;
|
|
|
|
SaveIrriSchedules;
|
|
RefreshIrriActionPopupUI(FCurrentEditIrriSchedIdx);
|
|
end;
|
|
|
|
procedure TfrmMain.RefreshIrriActionPopupUI(ASchedIdx: Integer);
|
|
type
|
|
PIrriAction = ^TIrriAction;
|
|
var
|
|
i: Integer;
|
|
Card: TRectangle;
|
|
LblHeader: TLabel;
|
|
EdDelay: TEdit;
|
|
CbNode: TComboBox;
|
|
ChkDel: TCheckBox;
|
|
Act: PIrriAction;
|
|
TopLyt: TRectangle;
|
|
BaseY: Single;
|
|
begin
|
|
if (ASchedIdx < 0) or (ASchedIdx > High(FIrriSchedules)) then Exit;
|
|
lblIrriActionTitle.Text := '동작 순서 관리 : ' + FIrriSchedules[ASchedIdx].SchedName;
|
|
|
|
scrlIrriAction.BeginUpdate;
|
|
if Self.Focused <> nil then Self.Focused := nil;
|
|
try
|
|
for i := scrlIrriAction.Content.ControlsCount - 1 downto 0 do
|
|
begin
|
|
var C := scrlIrriAction.Content.Controls[i];
|
|
if C is TComboBox then (C as TComboBox).OnChange := nil;
|
|
if C is TEdit then (C as TEdit).OnExit := nil;
|
|
if C is TCheckBox then (C as TCheckBox).OnChange := nil;
|
|
C.Parent := nil; // macOS 안정
|
|
end;
|
|
|
|
for i := 0 to High(FIrriSchedules[ASchedIdx].Actions) do
|
|
begin
|
|
Act := @FIrriSchedules[ASchedIdx].Actions[i];
|
|
|
|
Card := TRectangle.Create(Self);
|
|
Card.Parent := scrlIrriAction;
|
|
Card.Align := TAlignLayout.Top;
|
|
Card.Margins.Rect := RectF(0, 0, 0, 12);
|
|
Card.Fill.Color := IfThen(Act^.IsSelected, $FF3A2020, $FF333333);
|
|
Card.Stroke.Color := $FF555555;
|
|
Card.XRadius := 5; Card.YRadius := 5;
|
|
|
|
{$IFDEF ANDROID}
|
|
var ChkBg := TRectangle.Create(Self);
|
|
ChkBg.Parent := Card;
|
|
ChkBg.Position.X := 10; ChkBg.Position.Y := 13;
|
|
ChkBg.Width := 29; ChkBg.Height := 29;
|
|
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 := 8; ChkDel.Position.Y := 10;
|
|
ChkDel.Width := 24; ChkDel.Height := 24;
|
|
ChkDel.IsChecked := Act^.IsSelected;
|
|
ChkDel.Tag := i;
|
|
ChkDel.Scale.X := 1.5; ChkDel.Scale.Y := 1.5;
|
|
ChkDel.OnChange := OnIrriActionCheck;
|
|
|
|
LblHeader := BuildLabel(Card, Format('동작 순번 %d', [i+1]), 18, TAlignLayout.None, claWhite, True);
|
|
LblHeader.SetBounds(42, 10, 150, 30);
|
|
|
|
var LblNode := BuildLabel(Card, '작동 구역 (DO):', 16, TAlignLayout.None, claLightgray, False);
|
|
LblNode.SetBounds(10, 50, 120, 30);
|
|
|
|
CbNode := TComboBox.Create(Self);
|
|
CbNode.Parent := Card;
|
|
CbNode.SetBounds(130, 50, 205, 30);
|
|
var kDO: Integer;
|
|
for kDO := 0 to High(FNodeConfigs) do
|
|
begin
|
|
if FNodeConfigs[kDO].ID.StartsWith('DO_') then
|
|
begin
|
|
var LItm := TListBoxItem.Create(CbNode);
|
|
LItm.Parent := CbNode;
|
|
LItm.Height := 24 * 1.8;
|
|
LItm.Text := FNodeConfigs[kDO].ID + ' [' + (
|
|
if FNodeConfigs[kDO].CustomName <> '' then FNodeConfigs[kDO].CustomName
|
|
else if FIsKorean then FNodeConfigs[kDO].TitleKO
|
|
else FNodeConfigs[kDO].TitleEN) + ']';
|
|
LItm.StyledSettings := LItm.StyledSettings - [TStyledSetting.Size, TStyledSetting.FontColor];
|
|
LItm.TextSettings.Font.Size := 16;
|
|
{$IFDEF ANDROID}
|
|
LItm.TextSettings.FontColor := claWhite;
|
|
{$ENDIF}
|
|
CbNode.AddObject(LItm);
|
|
end;
|
|
end;
|
|
var kSelDO: Integer;
|
|
CbNode.ItemIndex := -1;
|
|
for kSelDO := 0 to CbNode.Items.Count - 1 do
|
|
if CbNode.Items[kSelDO].StartsWith(Act^.TargetNodeID + ' ') or
|
|
(CbNode.Items[kSelDO] = Act^.TargetNodeID) then
|
|
begin CbNode.ItemIndex := kSelDO; Break; end;
|
|
CbNode.Tag := i;
|
|
CbNode.TagFloat := 10;
|
|
CbNode.OnChange := OnIrriActionEdit;
|
|
|
|
var LblDelay := BuildLabel(Card, '쿨다운(분):', 16, TAlignLayout.None, claLightgray, False);
|
|
LblDelay.SetBounds(345, 50, 90, 30);
|
|
EdDelay := TEdit.Create(Self);
|
|
EdDelay.Parent := Card;
|
|
EdDelay.SetBounds(443, 50, 55, 30);
|
|
EdDelay.Text := IntToStr(Act^.DelayMins);
|
|
EdDelay.Tag := i; EdDelay.TagFloat := 11;
|
|
EdDelay.FilterChar := '0123456789';
|
|
EdDelay.StyledSettings := EdDelay.StyledSettings - [TStyledSetting.Size, TStyledSetting.FontColor];
|
|
EdDelay.TextSettings.Font.Size := 16;
|
|
{$IFDEF ANDROID}
|
|
EdDelay.TextSettings.FontColor := claWhite;
|
|
{$ENDIF}
|
|
EdDelay.OnExit := OnIrriActionEdit;
|
|
|
|
var LblTimeout := BuildLabel(Card, '제한(분):', 16, TAlignLayout.None, claLightgray, False);
|
|
LblTimeout.SetBounds(510, 50, 80, 30);
|
|
var EdTimeout := TEdit.Create(Self);
|
|
EdTimeout.Parent := Card;
|
|
EdTimeout.SetBounds(593, 50, 55, 30);
|
|
EdTimeout.Text := IntToStr(Act^.TimeoutMin);
|
|
EdTimeout.Tag := i; EdTimeout.TagFloat := 12;
|
|
EdTimeout.FilterChar := '0123456789';
|
|
EdTimeout.StyledSettings := EdTimeout.StyledSettings - [TStyledSetting.Size, TStyledSetting.FontColor];
|
|
EdTimeout.TextSettings.Font.Size := 16;
|
|
{$IFDEF ANDROID}
|
|
EdTimeout.TextSettings.FontColor := claWhite;
|
|
{$ENDIF}
|
|
EdTimeout.OnExit := OnIrriActionEdit;
|
|
|
|
BaseY := 100;
|
|
|
|
var DrawCondGroup := procedure(ATitle: string; AConds: TArray<TAutoCondItem>; AMode: TAutoCondMode; CondType: Integer)
|
|
var
|
|
k,m: Integer;
|
|
cSen, cOp, cMode, cFOp: TComboBox;
|
|
eVal, EdPerPulse: TEdit;
|
|
btnDel, btnAdd: TRectangle;
|
|
bIsDIDO, bIsFlow: Boolean;
|
|
LItem: TListBoxItem;
|
|
begin
|
|
var lTitle := BuildLabel(Card, ATitle, 14, TAlignLayout.None, claLightgray, True);
|
|
lTitle.SetBounds(10, BaseY, 80, 30);
|
|
|
|
cMode := TComboBox.Create(Self); cMode.Parent := Card;
|
|
cMode.SetBounds(95, BaseY, 150, 30);
|
|
for m := 0 to 1 do begin
|
|
LItem := TListBoxItem.Create(cMode);
|
|
LItem.Parent := cMode;
|
|
LItem.Height := 24 * 1.8;
|
|
LItem.StyledSettings := LItem.StyledSettings - [TStyledSetting.Size, TStyledSetting.FontColor];
|
|
LItem.TextSettings.Font.Size := 16;
|
|
{$IFDEF ANDROID}
|
|
LItem.TextSettings.FontColor := claWhite;
|
|
{$ENDIF}
|
|
if m=0 then
|
|
LItem.Text := if FIsKorean then 'AND (모두 만족)' else 'AND (all match)'
|
|
else
|
|
LItem.Text := if FIsKorean then 'OR (하나 만족)' else 'OR (any match)';
|
|
cMode.AddObject(LItem);
|
|
end;
|
|
cMode.ItemIndex := Integer(AMode);
|
|
cMode.Tag := i; cMode.TagFloat := CondType * 1000 + 3;
|
|
cMode.OnChange := OnIrriCondEdit;
|
|
|
|
btnAdd := CreateCustomButton(Card, '+ 추가', TAlignLayout.None, 80, OnIrriCondAdd);
|
|
TLabel(btnAdd.Controls[0]).TextSettings.Font.Size := 16;
|
|
btnAdd.SetBounds(255, BaseY, 80, 30);
|
|
btnAdd.Fill.Color := $FF226622;
|
|
btnAdd.Tag := i; btnAdd.TagFloat := CondType * 1000 + 4;
|
|
|
|
if (CondType = 0) and (Length(AConds) = 0) then
|
|
begin
|
|
BuildLabel(Card, ' [조건 없음 - 무조건 동작]', 16, TAlignLayout.None, claGray, False).SetBounds(350, BaseY, 200, 30);
|
|
end;
|
|
|
|
BaseY := BaseY + 40;
|
|
|
|
for k := 0 to High(AConds) do
|
|
begin
|
|
bIsFlow := False;
|
|
cSen := TComboBox.Create(Self); cSen.Parent := Card;
|
|
cSen.SetBounds(95, BaseY, 240, 30);
|
|
|
|
var kS: Integer;
|
|
for kS := 0 to High(FNodeConfigs) do
|
|
begin
|
|
var strId := FNodeConfigs[kS].ID;
|
|
// 유량계 힌트 표시 (노드 ID가 DI 이고 유량이름을 포함하거나 DI_F 등 규칙이 있다면)
|
|
var strName := FNodeConfigs[kS].CustomName;
|
|
if strName = '' then begin if FIsKorean then strName := FNodeConfigs[kS].TitleKO else strName := FNodeConfigs[kS].TitleEN; end;
|
|
if (strId.StartsWith('DI_')) and ((strName.Contains('유량')) or (strName.Contains('Flow'))) then begin
|
|
strName := strName + ' (리터/펄스)';
|
|
if strId = AConds[k].NodeID then bIsFlow := True;
|
|
end;
|
|
|
|
var LItmS := TListBoxItem.Create(cSen);
|
|
LItmS.Parent := cSen;
|
|
LItmS.Height := 24 * 1.8;
|
|
LItmS.Text := strId + ' [' + strName + ']';
|
|
LItmS.StyledSettings := LItmS.StyledSettings - [TStyledSetting.Size, TStyledSetting.FontColor];
|
|
LItmS.TextSettings.Font.Size := 16;
|
|
{$IFDEF ANDROID}
|
|
LItmS.TextSettings.FontColor := claWhite;
|
|
{$ENDIF}
|
|
cSen.AddObject(LItmS);
|
|
end;
|
|
|
|
var kSel: Integer;
|
|
cSen.ItemIndex := -1;
|
|
for kSel := 0 to cSen.Items.Count - 1 do
|
|
if cSen.Items[kSel].StartsWith(AConds[k].NodeID + ' ') or
|
|
(cSen.Items[kSel] = AConds[k].NodeID) then
|
|
begin cSen.ItemIndex := kSel; Break; end;
|
|
|
|
cSen.Tag := i; cSen.TagFloat := CondType * 1000 + k * 10 + 0;
|
|
cSen.OnChange := OnIrriCondEdit;
|
|
|
|
bIsDIDO := (AConds[k].NodeID.StartsWith('DI_')) or (AConds[k].NodeID.StartsWith('DO_'));
|
|
cOp := TComboBox.Create(Self); cOp.Parent := Card;
|
|
cOp.SetBounds(345, BaseY, 150, 30);
|
|
if bIsDIDO and (not bIsFlow) then
|
|
begin
|
|
for m := 0 to 1 do begin
|
|
LItem := TListBoxItem.Create(cOp);
|
|
LItem.Parent := cOp;
|
|
LItem.Height := 24 * 1.8;
|
|
LItem.StyledSettings := LItem.StyledSettings - [TStyledSetting.Size, TStyledSetting.FontColor];
|
|
LItem.TextSettings.Font.Size := 16;
|
|
if m=0 then
|
|
LItem.Text := if FIsKorean then 'ON (접점닫힘)' else 'ON (Closed)'
|
|
else
|
|
LItem.Text := if FIsKorean then 'OFF (접점열림)' else 'OFF (Open)';
|
|
{$IFDEF ANDROID}
|
|
LItem.TextSettings.FontColor := claWhite;
|
|
{$ENDIF}
|
|
|
|
cOp.AddObject(LItem);
|
|
end;
|
|
if AConds[k].Op >= acoON then
|
|
cOp.ItemIndex := Ord(AConds[k].Op) - Ord(acoON)
|
|
else
|
|
cOp.ItemIndex := 0;
|
|
end else begin
|
|
for m := 0 to 2 do begin
|
|
LItem := TListBoxItem.Create(cOp);
|
|
LItem.Parent := cOp;
|
|
LItem.Height := 24 * 1.8;
|
|
LItem.StyledSettings := LItem.StyledSettings - [TStyledSetting.Size, TStyledSetting.FontColor];
|
|
LItem.TextSettings.Font.Size := 16;
|
|
if m=0 then
|
|
LItem.Text := '> (' + (if FIsKorean then '초과' else 'Greater') + ')'
|
|
else if m=1 then
|
|
LItem.Text := '< (' + (if FIsKorean then '미만' else 'Less') + ')'
|
|
else
|
|
LItem.Text := '= (' + (if FIsKorean then '같음' else 'Equal') + ')';
|
|
{$IFDEF ANDROID}
|
|
LItem.TextSettings.FontColor := claWhite;
|
|
{$ENDIF}
|
|
|
|
cOp.AddObject(LItem);
|
|
end;
|
|
if AConds[k].Op <= acoEQ then
|
|
cOp.ItemIndex := Ord(AConds[k].Op)
|
|
else
|
|
cOp.ItemIndex := 0;
|
|
end;
|
|
cOp.Tag := i; cOp.TagFloat := CondType * 1000 + k * 10 + 1;
|
|
cOp.OnChange := OnIrriCondEdit;
|
|
|
|
eVal := TEdit.Create(Self); eVal.Parent := Card;
|
|
eVal.SetBounds(785, BaseY, 70, 30);
|
|
eVal.Enabled := not (bIsDIDO and (not bIsFlow));
|
|
eVal.Text := if (bIsDIDO and (not bIsFlow)) then '--' else FloatToStr(AConds[k].Value);
|
|
eVal.Tag := i; eVal.TagFloat := CondType * 1000 + k * 10 + 2;
|
|
eVal.FilterChar := '0123456789.';
|
|
eVal.StyledSettings := eVal.StyledSettings - [TStyledSetting.Size, TStyledSetting.FontColor];
|
|
eVal.TextSettings.Font.Size := 16;
|
|
{$IFDEF ANDROID}
|
|
eVal.TextSettings.FontColor := claWhite;
|
|
{$ENDIF}
|
|
eVal.OnExit := OnIrriCondEdit;
|
|
|
|
cFOp := TComboBox.Create(Self); cFOp.Parent := Card;
|
|
cFOp.Enabled := bIsDIDO and bIsFlow;
|
|
cFOp.SetBounds(505, BaseY, 80, 30);
|
|
if bIsDIDO and bIsFlow then
|
|
begin
|
|
for m := 0 to 2 do begin
|
|
LItem := TListBoxItem.Create(cFOp);
|
|
LItem.Parent := cFOp;
|
|
LItem.Height := 24 * 1.8;
|
|
LItem.StyledSettings := LItem.StyledSettings - [TStyledSetting.Size, TStyledSetting.FontColor];
|
|
LItem.TextSettings.Font.Size := 16;
|
|
if m=0 then
|
|
LItem.Text := if FIsKorean then '펄스' else 'Pulse'
|
|
else if m=1 then
|
|
LItem.Text := if FIsKorean then '리터' else 'Liter'
|
|
else
|
|
LItem.Text := if FIsKorean then 'Kg' else 'Kg';
|
|
{$IFDEF ANDROID}
|
|
LItem.TextSettings.FontColor := claWhite;
|
|
{$ENDIF}
|
|
|
|
cFOp.AddObject(LItem);
|
|
end;
|
|
if AConds[k].FOp <= acfoKg then
|
|
cFOp.ItemIndex := Ord(AConds[k].FOp)
|
|
else
|
|
cFOp.ItemIndex := 0;
|
|
end;
|
|
cFOp.Tag := i; cFOp.TagFloat := CondType * 1000 + k * 10 + 4;
|
|
cFOp.OnChange := OnIrriCondEdit;
|
|
|
|
if bIsDIDO and bIsFlow then begin
|
|
case AConds[k].FOp of
|
|
acfoLiter :
|
|
BuildLabel(Card, if FIsKorean then '[ 펄스당 리터:' else '[ Liter per Pulse:', 16, TAlignLayout.None, claLightgray, False).SetBounds(595, BaseY, 100, 30);
|
|
acfoKg :
|
|
BuildLabel(Card, if FIsKorean then '[ 펄스당 Kg:' else '[ Kg per Pulse:', 16, TAlignLayout.None, claLightgray, False).SetBounds(595, BaseY, 100, 30);
|
|
else
|
|
BuildLabel(Card, '[ --', 16, TAlignLayout.None, claLightgray, False).SetBounds(595, BaseY, 100, 30);
|
|
end;
|
|
end else
|
|
BuildLabel(Card, '[ --', 16, TAlignLayout.None, claLightgray, False).SetBounds(595, BaseY, 100, 30);
|
|
|
|
EdPerPulse := TEdit.Create(Self);
|
|
EdPerPulse.Parent := Card;
|
|
EdPerPulse.Enabled := bIsDIDO and bIsFlow and (AConds[k].FOp <> acfoPulse) ;
|
|
EdPerPulse.SetBounds(705, BaseY, 50, 30);
|
|
EdPerPulse.Text := if (bIsDIDO and (not bIsFlow)) then FloatToStr(AConds[k].PerPulse) else '--';
|
|
EdPerPulse.Tag := i; EdPerPulse.TagFloat := CondType * 1000 + k * 10 + 5;
|
|
EdPerPulse.FilterChar := '0123456789.';
|
|
EdPerPulse.StyledSettings := EdPerPulse.StyledSettings - [TStyledSetting.Size, TStyledSetting.FontColor];
|
|
EdPerPulse.TextSettings.Font.Size := 16;
|
|
{$IFDEF ANDROID}
|
|
EdPerPulse.TextSettings.FontColor := claWhite;
|
|
{$ENDIF}
|
|
EdPerPulse.OnExit := OnIrriCondEdit;
|
|
|
|
BuildLabel(Card, ' ]', 16, TAlignLayout.None, claLightgray, False).SetBounds(755, BaseY, 20, 30);
|
|
|
|
btnDel := CreateCustomButton(Card, '✕', TAlignLayout.None, 50, OnIrriCondDel);
|
|
TLabel(btnDel.Controls[0]).TextSettings.Font.Size := 22;
|
|
btnDel.SetBounds(865, BaseY, 80, 30);
|
|
btnDel.Fill.Color := $FFFF5252;
|
|
btnDel.Tag := i; btnDel.TagFloat := CondType * 1000 + k * 10 + 6;
|
|
|
|
BaseY := BaseY + 36;
|
|
end;
|
|
BaseY := BaseY + 8; // 여백 추가
|
|
end;
|
|
|
|
DrawCondGroup('시작 조건:', Act^.StartConds, Act^.StartCondMode, 0);
|
|
DrawCondGroup('정지 조건:', Act^.StopConds, Act^.StopCondMode, 1);
|
|
DrawCondGroup('종료 조건:', Act^.EndConds, Act^.EndCondMode, 2);
|
|
|
|
Card.Height := BaseY + 10;
|
|
end;
|
|
finally
|
|
scrlIrriAction.EndUpdate;
|
|
end;
|
|
end;
|
|
|
|
procedure TfrmMain.OnIrriActionPopupClose(Sender: TObject);
|
|
begin
|
|
lytIrriActionPopup.Visible := False;
|
|
end;
|
|
|
|
procedure TfrmMain.OnIrriActionManageClick(Sender: TObject);
|
|
begin
|
|
if Sender is TRectangle then
|
|
begin
|
|
FCurrentEditIrriSchedIdx := TRectangle(Sender).Tag;
|
|
RefreshIrriActionPopupUI(FCurrentEditIrriSchedIdx);
|
|
lytIrriActionPopup.Visible := True;
|
|
lytIrriActionPopup.BringToFront;
|
|
end;
|
|
end;
|
|
|
|
procedure TfrmMain.OnIrriActionCheck(Sender: TObject);
|
|
var Idx: Integer;
|
|
begin
|
|
if (FCurrentEditIrriSchedIdx < 0) or (FCurrentEditIrriSchedIdx > High(FIrriSchedules)) then Exit;
|
|
if Sender is TCheckBox then
|
|
begin
|
|
Idx := TCheckBox(Sender).Tag;
|
|
if (Idx >= 0) and (Idx <= High(FIrriSchedules[FCurrentEditIrriSchedIdx].Actions)) then
|
|
FIrriSchedules[FCurrentEditIrriSchedIdx].Actions[Idx].IsSelected := TCheckBox(Sender).IsChecked;
|
|
end;
|
|
end;
|
|
|
|
procedure TfrmMain.OnIrriActionAdd(Sender: TObject);
|
|
var Act: TIrriAction;
|
|
begin
|
|
if (FCurrentEditIrriSchedIdx < 0) or (FCurrentEditIrriSchedIdx > High(FIrriSchedules)) then Exit;
|
|
|
|
Act.TargetNodeID := '';
|
|
Act.DelayMins := 0;
|
|
Act.TimeoutMin := 0;
|
|
Act.IsSelected := False;
|
|
Act.StartCondMode := acmAND;
|
|
Act.StopCondMode := acmAND;
|
|
Act.EndCondMode := acmAND;
|
|
SetLength(Act.StartConds, 0);
|
|
SetLength(Act.StopConds, 0);
|
|
SetLength(Act.EndConds, 0);
|
|
|
|
var Len := Length(FIrriSchedules[FCurrentEditIrriSchedIdx].Actions);
|
|
SetLength(FIrriSchedules[FCurrentEditIrriSchedIdx].Actions, Len + 1);
|
|
FIrriSchedules[FCurrentEditIrriSchedIdx].Actions[Len] := Act;
|
|
|
|
SaveIrriSchedules;
|
|
RefreshIrriActionPopupUI(FCurrentEditIrriSchedIdx);
|
|
end;
|
|
|
|
procedure TfrmMain.OnIrriActionDel(Sender: TObject);
|
|
begin
|
|
// Not used in checkbox mode
|
|
end;
|
|
|
|
procedure TfrmMain.OnIrriActionEdit(Sender: TObject);
|
|
var
|
|
SchedIdx, ActIdx, CondType: Integer;
|
|
FieldType: Integer;
|
|
NodeIDRaw, ItemText: string;
|
|
Cond: TAutoCondItem;
|
|
begin
|
|
if (FCurrentEditIrriSchedIdx < 0) or (FCurrentEditIrriSchedIdx > High(FIrriSchedules)) then Exit;
|
|
SchedIdx := FCurrentEditIrriSchedIdx;
|
|
FieldType := 0;
|
|
ActIdx := 0;
|
|
CondType := 0;
|
|
|
|
if Sender is TComboBox then
|
|
FieldType := Trunc((Sender as TComboBox).TagFloat)
|
|
else if Sender is TEdit then
|
|
FieldType := Trunc((Sender as TEdit).TagFloat);
|
|
|
|
case FieldType of
|
|
10: // \uc791\ub3d9 \uad6c\uc5ed (DO \ub178\ub4dc)
|
|
begin
|
|
ActIdx := (Sender as TComboBox).Tag;
|
|
if (ActIdx < 0) or (ActIdx > High(FIrriSchedules[SchedIdx].Actions)) then Exit;
|
|
ItemText := (Sender as TComboBox).Selected.Text;
|
|
// 'DO_00 [...]' \ud615\uc2dd\uc5d0\uc11c ID\ub9cc \ucd94\ucd9c
|
|
var SpPos := Pos(' [', ItemText);
|
|
if SpPos > 0 then
|
|
FIrriSchedules[SchedIdx].Actions[ActIdx].TargetNodeID := Copy(ItemText, 1, SpPos - 1)
|
|
else
|
|
FIrriSchedules[SchedIdx].Actions[ActIdx].TargetNodeID := ItemText;
|
|
end;
|
|
11: // 쿨다운 지연시간(분)
|
|
begin
|
|
ActIdx := (Sender as TEdit).Tag;
|
|
if (ActIdx < 0) or (ActIdx > High(FIrriSchedules[SchedIdx].Actions)) then Exit;
|
|
FIrriSchedules[SchedIdx].Actions[ActIdx].DelayMins :=
|
|
StrToIntDef((Sender as TEdit).Text, 0);
|
|
end;
|
|
12: // 1회 제한시간(분)
|
|
begin
|
|
ActIdx := (Sender as TEdit).Tag;
|
|
if (ActIdx < 0) or (ActIdx > High(FIrriSchedules[SchedIdx].Actions)) then Exit;
|
|
FIrriSchedules[SchedIdx].Actions[ActIdx].TimeoutMin :=
|
|
StrToIntDef((Sender as TEdit).Text, 0);
|
|
end;
|
|
20: // 조건 노드 선택
|
|
begin
|
|
ActIdx := (Sender as TComboBox).Tag div 10;
|
|
CondType := (Sender as TComboBox).Tag mod 10;
|
|
if (ActIdx < 0) or (ActIdx > High(FIrriSchedules[SchedIdx].Actions)) then Exit;
|
|
ItemText := (Sender as TComboBox).Selected.Text;
|
|
var SpPos := Pos(' [', ItemText);
|
|
NodeIDRaw := if SpPos > 0 then Copy(ItemText, 1, SpPos - 1) else ItemText;
|
|
if NodeIDRaw.StartsWith('--') then NodeIDRaw := '';
|
|
case CondType of
|
|
0: begin
|
|
if Length(FIrriSchedules[SchedIdx].Actions[ActIdx].StartConds) = 0 then
|
|
SetLength(FIrriSchedules[SchedIdx].Actions[ActIdx].StartConds, 1);
|
|
FIrriSchedules[SchedIdx].Actions[ActIdx].StartConds[0].NodeID := NodeIDRaw;
|
|
end;
|
|
1: begin
|
|
if Length(FIrriSchedules[SchedIdx].Actions[ActIdx].StopConds) = 0 then
|
|
SetLength(FIrriSchedules[SchedIdx].Actions[ActIdx].StopConds, 1);
|
|
FIrriSchedules[SchedIdx].Actions[ActIdx].StopConds[0].NodeID := NodeIDRaw;
|
|
end;
|
|
2: begin
|
|
if Length(FIrriSchedules[SchedIdx].Actions[ActIdx].EndConds) = 0 then
|
|
SetLength(FIrriSchedules[SchedIdx].Actions[ActIdx].EndConds, 1);
|
|
FIrriSchedules[SchedIdx].Actions[ActIdx].EndConds[0].NodeID := NodeIDRaw;
|
|
end;
|
|
end;
|
|
end;
|
|
21: // \uc870\uac74 \uc5f0\uc0b0\uc790
|
|
begin
|
|
ActIdx := (Sender as TComboBox).Tag div 10;
|
|
CondType := (Sender as TComboBox).Tag mod 10;
|
|
if (ActIdx < 0) or (ActIdx > High(FIrriSchedules[SchedIdx].Actions)) then Exit;
|
|
var Op := TAutoCondOp((Sender as TComboBox).ItemIndex);
|
|
case CondType of
|
|
0: begin
|
|
if Length(FIrriSchedules[SchedIdx].Actions[ActIdx].StartConds) = 0 then
|
|
SetLength(FIrriSchedules[SchedIdx].Actions[ActIdx].StartConds, 1);
|
|
FIrriSchedules[SchedIdx].Actions[ActIdx].StartConds[0].Op := Op;
|
|
end;
|
|
1: begin
|
|
if Length(FIrriSchedules[SchedIdx].Actions[ActIdx].StopConds) = 0 then
|
|
SetLength(FIrriSchedules[SchedIdx].Actions[ActIdx].StopConds, 1);
|
|
FIrriSchedules[SchedIdx].Actions[ActIdx].StopConds[0].Op := Op;
|
|
end;
|
|
2: begin
|
|
if Length(FIrriSchedules[SchedIdx].Actions[ActIdx].EndConds) = 0 then
|
|
SetLength(FIrriSchedules[SchedIdx].Actions[ActIdx].EndConds, 1);
|
|
FIrriSchedules[SchedIdx].Actions[ActIdx].EndConds[0].Op := Op;
|
|
end;
|
|
end;
|
|
end;
|
|
22: // \uc870\uac74 \uc218\uce58\uac12
|
|
begin
|
|
ActIdx := (Sender as TEdit).Tag div 10;
|
|
CondType := (Sender as TEdit).Tag mod 10;
|
|
if (ActIdx < 0) or (ActIdx > High(FIrriSchedules[SchedIdx].Actions)) then Exit;
|
|
var V := StrToFloatDef((Sender as TEdit).Text, 0);
|
|
case CondType of
|
|
0: begin
|
|
if Length(FIrriSchedules[SchedIdx].Actions[ActIdx].StartConds) = 0 then
|
|
SetLength(FIrriSchedules[SchedIdx].Actions[ActIdx].StartConds, 1);
|
|
FIrriSchedules[SchedIdx].Actions[ActIdx].StartConds[0].Value := V;
|
|
end;
|
|
1: begin
|
|
if Length(FIrriSchedules[SchedIdx].Actions[ActIdx].StopConds) = 0 then
|
|
SetLength(FIrriSchedules[SchedIdx].Actions[ActIdx].StopConds, 1);
|
|
FIrriSchedules[SchedIdx].Actions[ActIdx].StopConds[0].Value := V;
|
|
end;
|
|
2: begin
|
|
if Length(FIrriSchedules[SchedIdx].Actions[ActIdx].EndConds) = 0 then
|
|
SetLength(FIrriSchedules[SchedIdx].Actions[ActIdx].EndConds, 1);
|
|
FIrriSchedules[SchedIdx].Actions[ActIdx].EndConds[0].Value := V;
|
|
end;
|
|
end;
|
|
end;
|
|
end;
|
|
|
|
SaveIrriSchedules;
|
|
end;
|
|
|
|
procedure TfrmMain.OnIrriActionDelSelected(Sender: TObject);
|
|
var i, Cnt: Integer;
|
|
Acts: TArray<TIrriAction>;
|
|
begin
|
|
if (FCurrentEditIrriSchedIdx < 0) or (FCurrentEditIrriSchedIdx > High(FIrriSchedules)) then Exit;
|
|
Cnt := 0;
|
|
for i := High(FIrriSchedules[FCurrentEditIrriSchedIdx].Actions) downto 0 do
|
|
if FIrriSchedules[FCurrentEditIrriSchedIdx].Actions[i].IsSelected then
|
|
begin
|
|
Delete(FIrriSchedules[FCurrentEditIrriSchedIdx].Actions, i, 1);
|
|
Inc(Cnt);
|
|
end;
|
|
if Cnt = 0 then Exit;
|
|
SaveIrriSchedules;
|
|
RefreshIrriActionPopupUI(FCurrentEditIrriSchedIdx);
|
|
end;
|
|
|
|
procedure TfrmMain.OnIrriManualStartStop(Sender: TObject);
|
|
var Idx: Integer;
|
|
begin
|
|
if not (Sender is TRectangle) then Exit;
|
|
Idx := TRectangle(Sender).Tag;
|
|
if (Idx < 0) or (Idx > High(FIrriSchedules)) then Exit;
|
|
if not FIrriSchedules[Idx].WaitingManual then Exit;
|
|
|
|
// 수동 승인: 스케쥴 시작
|
|
FIrriSchedules[Idx].WaitingManual := False;
|
|
FIrriSchedules[Idx].IsRunning := True;
|
|
FIrriSchedules[Idx].CurrentActionIdx := 0;
|
|
FIrriSchedules[Idx].IsCoolingDown := False;
|
|
FIrriSchedules[Idx].IsDelaying := True;
|
|
FIrriSchedules[Idx].ActionStartTick := Now;
|
|
SaveIrriSchedules;
|
|
AddSystemLog('관수 스케쥴 수동 시작 승인: ' + FIrriSchedules[Idx].SchedName);
|
|
TThread.Queue(nil, UpdateIrriDashboardUI);
|
|
end;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|