feat: 06-17 신규 작업본 반영 (개발사양서/기능검토/승인원/Source 등 추가)
.claude/ 제외(.gitignore 추가). 기존 초기커밋(5a96a69) 위에 신규·수정·이동분 커밋.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -15,6 +15,7 @@ namespace ErvDashboard.Api
|
||||
state.FanMode = s.FanMode;
|
||||
state.SubModeBitmap = s.SubMode;
|
||||
state.Hood = s.HoodEnable; // byte5 bit0 = 연동 Enable
|
||||
state.HoodRunning = s.HoodRunning; // byte5 bit1 = 연동운전중(메이크업 동작중)
|
||||
state.HoodConnected = s.HoodConnected; // byte5 bit2 = 후드 통신연결
|
||||
state.HystPreset = (HystPreset)s.HystPreset;
|
||||
state.HystPm25 = s.HystPm25;
|
||||
|
||||
@@ -81,7 +81,12 @@
|
||||
</Grid>
|
||||
|
||||
<StackPanel Orientation="Horizontal" Margin="0,14,0,0">
|
||||
<Button Content="변경" Width="90" Style="{StaticResource FlatButton}" Click="Apply_Click"/>
|
||||
<Button Content="읽어오기" Width="90" Style="{StaticResource FlatButton}" Click="Read_Click"
|
||||
ToolTip="ERV에서 통신으로 읽은 현재 값을 표에 불러옵니다(편집 내용은 버려짐)."/>
|
||||
<Button Content="프리셋" Width="90" Style="{StaticResource FlatButton}" Click="PresetDefault_Click"
|
||||
ToolTip="사양서 기본값을 표에 불러옵니다. ERV로 보내려면 '변경'을 누르세요."/>
|
||||
<Button Content="변경" Width="90" Style="{StaticResource FlatButton}" Click="Apply_Click"
|
||||
ToolTip="표의 값을 ERV로 전송합니다."/>
|
||||
<Button Content="닫기" Width="90" Style="{StaticResource FlatButton}" Click="Close_Click"/>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
|
||||
@@ -65,6 +65,25 @@ namespace ErvDashboard
|
||||
MPm10.Text = $"{_state.Pm10Thr[p][3] + 1}~"; MVoc.Text = $"{_state.VocThr[p][3] + 1}~";
|
||||
}
|
||||
|
||||
// 사양서 기본값으로 표 채우기 (전송은 '변경'에서) — 개발사양서 p.10
|
||||
void FillDefaults(int p)
|
||||
{
|
||||
TCo2_1.Text = DashboardState.DefCo2Thr[p][0].ToString(); TCo2_2.Text = DashboardState.DefCo2Thr[p][1].ToString(); TCo2_3.Text = DashboardState.DefCo2Thr[p][2].ToString(); TCo2_4.Text = DashboardState.DefCo2Thr[p][3].ToString();
|
||||
TPm25_1.Text = DashboardState.DefPm25Thr[p][0].ToString(); TPm25_2.Text = DashboardState.DefPm25Thr[p][1].ToString(); TPm25_3.Text = DashboardState.DefPm25Thr[p][2].ToString(); TPm25_4.Text = DashboardState.DefPm25Thr[p][3].ToString();
|
||||
TPm10_1.Text = DashboardState.DefPm10Thr[p][0].ToString(); TPm10_2.Text = DashboardState.DefPm10Thr[p][1].ToString(); TPm10_3.Text = DashboardState.DefPm10Thr[p][2].ToString(); TPm10_4.Text = DashboardState.DefPm10Thr[p][3].ToString();
|
||||
TVoc_1.Text = DashboardState.DefVocThr[p][0].ToString(); TVoc_2.Text = DashboardState.DefVocThr[p][1].ToString(); TVoc_3.Text = DashboardState.DefVocThr[p][2].ToString(); TVoc_4.Text = DashboardState.DefVocThr[p][3].ToString();
|
||||
var d = DashboardState.DefDeadband[p];
|
||||
DCo2.Text = d.Co2.ToString(); DPm25.Text = d.Pm25.ToString(); DPm10.Text = d.Pm10.ToString(); DVoc.Text = d.Voc.ToString();
|
||||
MCo2.Text = $"{DashboardState.DefCo2Thr[p][3] + 1}~"; MPm25.Text = $"{DashboardState.DefPm25Thr[p][3] + 1}~";
|
||||
MPm10.Text = $"{DashboardState.DefPm10Thr[p][3] + 1}~"; MVoc.Text = $"{DashboardState.DefVocThr[p][3] + 1}~";
|
||||
}
|
||||
|
||||
// '읽어오기' : ERV 통신값(=_state, STATUS로 갱신)으로 표 재채움 (편집 내용 버림)
|
||||
void Read_Click(object sender, RoutedEventArgs e) => FillGrid((int)_state.HystPreset);
|
||||
|
||||
// '프리셋' : 사양서 기본값으로 표 채움 (전송은 '변경')
|
||||
void PresetDefault_Click(object sender, RoutedEventArgs e) => FillDefaults((int)_state.HystPreset);
|
||||
|
||||
static int P(TextBox tb) { int.TryParse(tb.Text, out int v); return v < 0 ? 0 : v > 65535 ? 65535 : v; }
|
||||
|
||||
void Preset_Click(object sender, RoutedEventArgs e)
|
||||
|
||||
@@ -565,7 +565,9 @@ namespace ErvDashboard
|
||||
// - 바이패스 : 최대 1단(2~4단 비활성)
|
||||
// - 환기/공청 : 0~4단
|
||||
// 시나리오모드 활성 시: 운전모드·풍량·선택 안 된 시나리오모드 비활성화
|
||||
bool subActive = _state.SmartSleep || _state.ComfortCook || _state.ReliefRecover;
|
||||
// 쾌적조리는 '연동운전중(HoodRunning=후드 가동중)' 기준으로 시나리오 활성 판단.
|
||||
// 후드 OFF(대기 상태)면 ERV는 본래 운전모드로 복귀하므로 운전모드를 다시 활성화해야 함(사양 3.1).
|
||||
bool subActive = _state.SmartSleep || _state.HoodRunning || _state.ReliefRecover;
|
||||
int fanMax = _state.RunMode == RunMode.Bypass ? 1 : 4;
|
||||
foreach (var fb in _fanButtons)
|
||||
{
|
||||
@@ -576,7 +578,7 @@ namespace ErvDashboard
|
||||
|
||||
// 시나리오모드
|
||||
SetActive(SmartSleepBtn, _state.SmartSleep);
|
||||
SetActive(ComfortCookBtn, _state.ComfortCook);
|
||||
SetActive(ComfortCookBtn, _state.HoodRunning); // 메이크업 실제 동작중(후드 가동)일 때만 강조 — 후드 OFF면 해제
|
||||
SetActive(ReliefRecoverBtn, _state.ReliefRecover);
|
||||
// (활성 모드 버튼은 OFF 토글 가능해야 하므로 자기 자신은 유지)
|
||||
SmartSleepBtn.IsEnabled = !subActive || _state.SmartSleep;
|
||||
|
||||
@@ -12,7 +12,7 @@ namespace ErvDashboard.Model
|
||||
bool _powerOn;
|
||||
RunMode _runMode = RunMode.Off;
|
||||
byte _fanMode;
|
||||
bool _hood, _hoodConnected;
|
||||
bool _hood, _hoodConnected, _hoodRunning;
|
||||
bool _smartSleep, _comfortCook, _reliefRecover;
|
||||
|
||||
public bool PowerOn { get => _powerOn; set { if (_powerOn != value) { _powerOn = value; OnChanged(); } } }
|
||||
@@ -24,6 +24,8 @@ namespace ErvDashboard.Model
|
||||
public bool Hood { get => _hood; set { if (_hood != value) { _hood = value; OnChanged(); } } }
|
||||
// 후드 485 통신연결 여부 (STATUS byte5 bit2). 후드연동 ON일 때 연결/미연결 텍스트 표시용
|
||||
public bool HoodConnected { get => _hoodConnected; set { if (_hoodConnected != value) { _hoodConnected = value; OnChanged(); } } }
|
||||
// 후드연동에 의한 운전중(메이크업 에어 실제 동작중, STATUS byte5 bit1). 후드 OFF면 false → 쾌적조리 표시 해제·운전모드 활성.
|
||||
public bool HoodRunning { get => _hoodRunning; set { if (_hoodRunning != value) { _hoodRunning = value; OnChanged(); } } }
|
||||
|
||||
// ---- (꺼짐)예약 : 잔여초(STATUS 수신) ----
|
||||
int _reserveRemainSec;
|
||||
@@ -115,6 +117,20 @@ namespace ErvDashboard.Model
|
||||
public int[][] Pm10Thr { get; } = { new int[4], new int[4], new int[4] };
|
||||
public int[][] VocThr { get; } = { new int[4], new int[4], new int[4] };
|
||||
|
||||
// ===== 사양서 기본값 (히스테리시스/VSP 창의 '프리셋' 버튼용 — 불변) =====
|
||||
// 오염단계 임계 [preset 0 ECO/1 NORMAL/2 TURBO][L0~L3 상한] — 개발사양서 p.10
|
||||
public static readonly int[][] DefCo2Thr = { new[]{1000,1300,1600,2000}, new[]{800,1100,1400,1700}, new[]{700,1000,1300,1600} };
|
||||
public static readonly int[][] DefPm25Thr = { new[]{20,38,60,86}, new[]{14,29,49,69}, new[]{12,23,38,52} };
|
||||
public static readonly int[][] DefPm10Thr = { new[]{40,86,126,173}, new[]{28,66,102,138}, new[]{24,53,78,104} };
|
||||
public static readonly int[][] DefVocThr = { new[]{171,195,308,438}, new[]{120,150,250,350}, new[]{103,120,192,263} };
|
||||
// 데드밴드(하강) [preset] (Pm25,Pm10,Voc,Co2) — 개발사양서 p.10
|
||||
public static readonly (int Pm25,int Pm10,int Voc,int Co2)[] DefDeadband =
|
||||
{ (2,5,5,50), (2,5,5,50), (2,5,3,30) };
|
||||
// 풍량 VSP 기본값 [9엔트리: 환기1~4/바이패스/공청1~4] (Sa,Ea)
|
||||
// 개발사양서 p.12 첫 표(휴벤 ECO2/좌타입 HRD1-150EPI) = 펌웨어 MyControl.c 기본값. 환기4(터보)=자동250CMH 행.
|
||||
public static readonly (int Sa,int Ea)[] DefVsp =
|
||||
{ (56,57), (63,63), (70,70), (86,85), (67,75), (65,0), (72,0), (78,0), (80,0) };
|
||||
|
||||
// ---- 각실 ----
|
||||
public ObservableCollection<RoomState> Rooms { get; }
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
<Border Style="{StaticResource Card}" Margin="10">
|
||||
<StackPanel>
|
||||
<TextBlock Text="풍량 VSP 제어 · 상태 (SA 급기 / EA 배기)" Style="{StaticResource CardTitle}"/>
|
||||
<ItemsControl ItemsSource="{Binding Vsp}" Width="990">
|
||||
<ItemsControl x:Name="VspList" Width="990">
|
||||
<ItemsControl.ItemsPanel>
|
||||
<ItemsPanelTemplate><UniformGrid Columns="3"/></ItemsPanelTemplate>
|
||||
</ItemsControl.ItemsPanel>
|
||||
@@ -26,7 +26,12 @@
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
<StackPanel Orientation="Horizontal" Margin="0,12,0,0">
|
||||
<Button Content="VSP 적용" Width="100" Style="{StaticResource FlatButton}" Click="Apply_Click"/>
|
||||
<Button Content="읽어오기" Width="100" Style="{StaticResource FlatButton}" Click="Read_Click"
|
||||
ToolTip="ERV에서 통신으로 읽은 현재 VSP 값을 불러옵니다(편집 내용은 버려짐)."/>
|
||||
<Button Content="프리셋" Width="100" Style="{StaticResource FlatButton}" Click="PresetDefault_Click"
|
||||
ToolTip="사양서 기본 VSP 값을 불러옵니다. ERV로 보내려면 'VSP 적용'을 누르세요."/>
|
||||
<Button Content="VSP 적용" Width="100" Style="{StaticResource FlatButton}" Click="Apply_Click"
|
||||
ToolTip="현재 VSP 값을 ERV로 전송합니다."/>
|
||||
<Button Content="닫기" Width="90" Style="{StaticResource FlatButton}" Click="Close_Click"/>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
|
||||
@@ -1,21 +1,56 @@
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Windows;
|
||||
using ErvDashboard.Model;
|
||||
using ErvProtocol;
|
||||
|
||||
namespace ErvDashboard
|
||||
{
|
||||
// 풍량 VSP 팝업 (환기1~4 / 바이패스 / 공청1~4 SA·EA 편집)
|
||||
// - 편집 필드는 로컬 작업본(_work). STATUS(1초)가 덮어쓰지 않아 편집이 유지됨.
|
||||
// - 읽어오기 : ERV 통신값(_state) → 작업본 / 프리셋 : 사양 기본값 → 작업본 / VSP 적용 : 작업본 → ERV
|
||||
public partial class VspWindow : Window
|
||||
{
|
||||
readonly MainWindow _owner;
|
||||
readonly DashboardState _state;
|
||||
readonly ObservableCollection<VspRow> _work = new();
|
||||
|
||||
public VspWindow(MainWindow owner, DashboardState state)
|
||||
{
|
||||
InitializeComponent();
|
||||
_owner = owner;
|
||||
DataContext = state;
|
||||
_owner = owner; _state = state;
|
||||
|
||||
for (int i = 0; i < VspInfo.Count; i++)
|
||||
_work.Add(new VspRow(VspInfo.Labels[i], VspInfo.Group[i], VspInfo.Index[i]));
|
||||
VspList.ItemsSource = _work;
|
||||
|
||||
ReadFromErv(); // 열 때 ERV 현재값으로 시작
|
||||
}
|
||||
|
||||
// ERV 통신값(_state, STATUS 갱신) → 작업본
|
||||
void ReadFromErv()
|
||||
{
|
||||
for (int i = 0; i < _work.Count && i < _state.Vsp.Count; i++)
|
||||
{ _work[i].Sa = _state.Vsp[i].Sa; _work[i].Ea = _state.Vsp[i].Ea; }
|
||||
}
|
||||
|
||||
void Read_Click(object sender, RoutedEventArgs e) => ReadFromErv();
|
||||
|
||||
// 사양서 기본 VSP → 작업본 (전송은 'VSP 적용')
|
||||
void PresetDefault_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
var d = DashboardState.DefVsp;
|
||||
for (int i = 0; i < _work.Count && i < d.Length; i++)
|
||||
{ _work[i].Sa = d[i].Sa; _work[i].Ea = d[i].Ea; }
|
||||
}
|
||||
|
||||
// 작업본 → _state → ERV 전송
|
||||
void Apply_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
for (int i = 0; i < _work.Count && i < _state.Vsp.Count; i++)
|
||||
{ _state.Vsp[i].Sa = _work[i].Sa; _state.Vsp[i].Ea = _work[i].Ea; }
|
||||
_owner.ApplyVsp();
|
||||
}
|
||||
|
||||
void Apply_Click(object sender, RoutedEventArgs e) => _owner.ApplyVsp();
|
||||
void Close_Click(object sender, RoutedEventArgs e) => Close();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -171,6 +171,10 @@
|
||||
<div class="modal-h"><span>공기질 히스테리시스 (ECO / NORMAL / TURBO)</span>
|
||||
<button class="btn" onclick="closeModal()">닫기</button></div>
|
||||
<div class="ctrl-row"><span class="lbl">프리셋</span><span id="cPresets"></span></div>
|
||||
<div class="ctrl-row" style="gap:8px;margin-top:2px">
|
||||
<button class="btn" onclick="hystReadErv()">읽어오기</button>
|
||||
<button class="btn" onclick="hystPreset()">프리셋(기본값)</button>
|
||||
</div>
|
||||
<div style="font-weight:700;font-size:13px;margin:6px 0 2px">데드밴드(하강)</div>
|
||||
<div id="hystGrid"></div>
|
||||
<button class="btn" style="margin-top:10px" onclick="applyHyst()">데드밴드 변경</button>
|
||||
@@ -182,6 +186,11 @@
|
||||
<div class="modal-h"><span>풍량 VSP 제어 · 상태 (SA 급기 / EA 배기)</span>
|
||||
<button class="btn" onclick="closeModal()">닫기</button></div>
|
||||
<div class="vsp-grid" id="vspGrid"></div>
|
||||
<div style="margin-top:10px;display:flex;gap:8px">
|
||||
<button class="btn" onclick="vspReadErv()">읽어오기</button>
|
||||
<button class="btn" onclick="vspPreset()">프리셋(기본값)</button>
|
||||
<button class="btn" onclick="vspApply()">변경</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal modal-wide" id="modalGraph" style="display:none">
|
||||
<div class="modal-h"><span>로그 그래프 (가로 시간 · 세로 댐퍼/센서/모드)</span>
|
||||
@@ -232,6 +241,10 @@ const THR_DEMO = [
|
||||
{co2:[1000,1300,1600,2000],pm25:[20,38,60,86],pm10:[40,86,126,173],voc:[171,195,308,438]},
|
||||
{co2:[800,1100,1400,1700],pm25:[14,29,49,69],pm10:[28,66,102,138],voc:[120,150,250,350]},
|
||||
{co2:[700,1000,1300,1600],pm25:[12,23,38,52],pm10:[24,53,78,104],voc:[103,120,192,263]}];
|
||||
// ===== 사양 기본값 (모달 '프리셋' 버튼) : VSP=개발사양서 p.12(휴벤 ECO2/좌타입=펌웨어) / 데드밴드·임계=p.10 =====
|
||||
const VSP_DEFAULT = [[56,57],[63,63],[70,70],[86,85],[67,75],[65,0],[72,0],[78,0],[80,0]];
|
||||
const DB_DEFAULT = [{pm25:2,pm10:5,voc:5,co2:50},{pm25:2,pm10:5,voc:5,co2:50},{pm25:2,pm10:5,voc:3,co2:30}];
|
||||
const THR_DEFAULT = THR_DEMO.map(t=>({co2:[...t.co2],pm25:[...t.pm25],pm10:[...t.pm10],voc:[...t.voc]}));
|
||||
const HIST=60;
|
||||
|
||||
let current="site01", demoOn=true, tick=0;
|
||||
@@ -245,6 +258,12 @@ SITES.forEach((s,si)=>{ state[s]={
|
||||
thr:THR_DEMO.map(t=>({co2:[...t.co2],pm25:[...t.pm25],pm10:[...t.pm10],voc:[...t.voc]})),
|
||||
seed:si*100 }; });
|
||||
|
||||
// 모달 편집 버퍼 (폴링이 덮어쓰지 않음). null=ERV(state)값 표시. 읽어오기/프리셋/편집 시 채움, 변경/닫기 시 비움.
|
||||
let bufVsp=null, bufHyst=null, bufThr=null;
|
||||
function copyVsp(v){ return (v||[]).map(x=>({sa:x.sa,ea:x.ea})); }
|
||||
function copyHyst(h){ return (h||[]).map(x=>({pm25:x.pm25,pm10:x.pm10,voc:x.voc,co2:x.co2})); }
|
||||
function copyThr(t){ return (t||[]).map(x=>({co2:[...x.co2],pm25:[...x.pm25],pm10:[...x.pm10],voc:[...x.voc]})); }
|
||||
|
||||
// ===== 데모 생성기 =====
|
||||
function genSensors(s,t){
|
||||
const st=state[s]; st.online=true;
|
||||
@@ -289,6 +308,7 @@ async function ctl(action,a={}){
|
||||
}
|
||||
}
|
||||
function flash(msg){ const f=document.getElementById("footNote"); f.textContent=msg; f.style.color="var(--bad)"; setTimeout(setFoot,2500); }
|
||||
function okMsg(msg){ const f=document.getElementById("footNote"); f.textContent=msg; f.style.color="var(--good)"; setTimeout(setFoot,2200); }
|
||||
|
||||
// ===== 제어 핸들러 =====
|
||||
function togglePower(){ ctl("power",{value:state[current].g.power?0:1}); }
|
||||
@@ -310,13 +330,18 @@ function setDamperEa(room){ if(state[current].g.run_mode===2)return; const cur=s
|
||||
function setLed(room,val){ ctl("led",{room,value:parseInt(val)}); }
|
||||
function setReserve(h){ ctl("reserve",{value:parseInt(h)}); }
|
||||
function toggleReset(){ ctl("reset",{value:state[current].g.reset?0:1}); }
|
||||
function setHyst(pi,field,val){ state[current].hyst[pi][field]=parseInt(val)||0; }
|
||||
function applyHyst(){ const st=state[current];
|
||||
for(let pi=0;pi<3;pi++){ const v=st.hyst[pi]; ctl("hyst",{preset:pi,pm25:v.pm25,pm10:v.pm10,voc:v.voc,co2:v.co2}); } }
|
||||
function setThr(pi,poll,li,val){ state[current].thr[pi][poll][li]=parseInt(val)||0; }
|
||||
function applyThr(){ const st=state[current];
|
||||
for(let pi=0;pi<3;pi++) POLL.forEach((poll,pp)=>{ const v=st.thr[pi][poll];
|
||||
ctl("hystthr",{preset:pi,pollutant:pp,l1:v[0],l2:v[1],l3:v[2],l4:v[3],_poll:poll}); }); }
|
||||
function setHyst(pi,field,val){ if(!bufHyst)bufHyst=copyHyst(state[current].hyst); bufHyst[pi][field]=parseInt(val)||0; }
|
||||
function applyHyst(){ const hb=bufHyst||state[current].hyst;
|
||||
for(let pi=0;pi<3;pi++){ const v=hb[pi]; ctl("hyst",{preset:pi,pm25:v.pm25,pm10:v.pm10,voc:v.voc,co2:v.co2}); }
|
||||
bufHyst=null; okMsg("데드밴드 전송 완료"); }
|
||||
function setThr(pi,poll,li,val){ if(!bufThr)bufThr=copyThr(state[current].thr); bufThr[pi][poll][li]=parseInt(val)||0; }
|
||||
function applyThr(){ const tb=bufThr||state[current].thr;
|
||||
for(let pi=0;pi<3;pi++) POLL.forEach((poll,pp)=>{ const v=tb[pi][poll];
|
||||
ctl("hystthr",{preset:pi,pollutant:pp,l1:v[0],l2:v[1],l3:v[2],l4:v[3],_poll:poll}); });
|
||||
bufThr=null; okMsg("임계 전송 완료"); }
|
||||
// 히스테리시스 읽어오기/프리셋 (편집은 '데드밴드 변경'/'임계 변경'으로 전송)
|
||||
function hystReadErv(){ bufHyst=copyHyst(state[current].hyst); bufThr=copyThr(state[current].thr); renderHyst(); renderThr(); okMsg("ERV값 불러옴"); }
|
||||
function hystPreset(){ bufHyst=DB_DEFAULT.map(h=>({...h})); bufThr=copyThr(THR_DEFAULT); renderHyst(); renderThr(); okMsg("사양 기본값 불러옴 — '변경'으로 전송"); }
|
||||
|
||||
// ===== 팝업 =====
|
||||
function openModal(which){
|
||||
@@ -324,18 +349,24 @@ function openModal(which){
|
||||
document.getElementById("modalVsp").style.display = which==="vsp"?"block":"none";
|
||||
document.getElementById("modalGraph").style.display = which==="graph"?"block":"none";
|
||||
document.getElementById("modalBg").classList.add("show");
|
||||
if(which==="vsp") bufVsp=copyVsp(state[current].vsp); // 열 때 ERV 현재값
|
||||
if(which==="hyst"){ bufHyst=copyHyst(state[current].hyst); bufThr=copyThr(state[current].thr); }
|
||||
renderControls(); renderHyst(); renderThr(); renderVsp();
|
||||
if(which==="graph"){ renderSide(); loadGraph(); } // 열 때 현재 로드날짜로 갱신
|
||||
}
|
||||
function graphOpen(){ return document.getElementById("modalBg").classList.contains("show")
|
||||
&& document.getElementById("modalGraph").style.display==="block"; }
|
||||
function closeModal(){ document.getElementById("modalBg").classList.remove("show"); }
|
||||
function closeModal(){ document.getElementById("modalBg").classList.remove("show"); bufVsp=bufHyst=bufThr=null; }
|
||||
function closeModalBg(e){ if(e.target.id==="modalBg") closeModal(); }
|
||||
function setVsp(i,field,val){
|
||||
const v=state[current].vsp[i];
|
||||
const sa=field==="sa"?(parseInt(val)||0):v.sa, ea=field==="ea"?(parseInt(val)||0):v.ea;
|
||||
ctl("vsp",{group:VSP_GROUP[i],index:VSP_INDEX[i],sa,ea,_idx:i});
|
||||
if(!bufVsp) bufVsp=copyVsp(state[current].vsp);
|
||||
bufVsp[i][field]=parseInt(val)||0;
|
||||
}
|
||||
function vspReadErv(){ bufVsp=copyVsp(state[current].vsp); renderVsp(); okMsg("ERV값 불러옴"); }
|
||||
function vspPreset(){ bufVsp=VSP_DEFAULT.map(([sa,ea])=>({sa,ea})); renderVsp(); okMsg("사양 기본값 불러옴 — '변경'으로 전송"); }
|
||||
function vspApply(){ const b=bufVsp||state[current].vsp;
|
||||
for(let i=0;i<9;i++) ctl("vsp",{group:VSP_GROUP[i],index:VSP_INDEX[i],sa:b[i].sa,ea:b[i].ea,_idx:i});
|
||||
bufVsp=null; renderVsp(); okMsg("VSP 전송 완료"); }
|
||||
|
||||
// ===== 스마트수면 시간설정 (브라우저 스케줄, 현재 현장에 적용) =====
|
||||
let _slLast=-1;
|
||||
@@ -443,19 +474,19 @@ function renderRooms(){
|
||||
function renderHyst(){
|
||||
const grid=document.getElementById("hystGrid");
|
||||
if(document.activeElement && grid.contains(document.activeElement)) return;
|
||||
const st=state[current];
|
||||
const hb=bufHyst||state[current].hyst;
|
||||
let h='<div class="hyst-grid"><span class="hh"></span><span class="hh">PM2.5</span><span class="hh">PM10</span><span class="hh">VOC</span><span class="hh">CO2</span>';
|
||||
HYST_PRESETS.forEach((name,pi)=>{ const v=(st.hyst&&st.hyst[pi])||{pm25:0,pm10:0,voc:0,co2:0};
|
||||
HYST_PRESETS.forEach((name,pi)=>{ const v=(hb&&hb[pi])||{pm25:0,pm10:0,voc:0,co2:0};
|
||||
h+=`<span class="pl">${name}</span>`+HYST_FIELDS.map(f=>`<input type="number" min="0" value="${v[f]}" onchange="setHyst(${pi},'${f}',this.value)">`).join(""); });
|
||||
grid.innerHTML=h+"</div>";
|
||||
}
|
||||
function renderThr(){
|
||||
const grid=document.getElementById("thrGrid");
|
||||
if(document.activeElement && grid.contains(document.activeElement)) return;
|
||||
const st=state[current];
|
||||
const tb=bufThr||state[current].thr;
|
||||
let h='<div class="thr-grid"><span class="hh"></span><span class="hh">L1</span><span class="hh">L2</span><span class="hh">L3</span><span class="hh">L4</span>';
|
||||
HYST_PRESETS.forEach((name,pi)=>{
|
||||
POLL.forEach(poll=>{ const v=(st.thr&&st.thr[pi]&&st.thr[pi][poll])||[0,0,0,0];
|
||||
POLL.forEach(poll=>{ const v=(tb&&tb[pi]&&tb[pi][poll])||[0,0,0,0];
|
||||
h+=`<span class="pl">${name}·${poll.toUpperCase()}</span>`+
|
||||
[0,1,2,3].map(li=>`<input type="number" min="0" value="${v[li]}" onchange="setThr(${pi},'${poll}',${li},this.value)">`).join("");
|
||||
});
|
||||
@@ -465,8 +496,8 @@ function renderThr(){
|
||||
function renderVsp(){
|
||||
const grid=document.getElementById("vspGrid");
|
||||
if(document.activeElement && grid.contains(document.activeElement)) return;
|
||||
const st=state[current];
|
||||
grid.innerHTML = VSP_LABELS.map((lab,i)=>{ const v=(st.vsp&&st.vsp[i])||{sa:0,ea:0};
|
||||
const arr=bufVsp||state[current].vsp;
|
||||
grid.innerHTML = VSP_LABELS.map((lab,i)=>{ const v=(arr&&arr[i])||{sa:0,ea:0};
|
||||
return `<div class="vsp-row"><span class="vl">${lab}</span>`+
|
||||
`<span class="u">SA</span><input type="number" min="0" value="${v.sa}" onchange="setVsp(${i},'sa',this.value)">`+
|
||||
`<span class="u">EA</span><input type="number" min="0" value="${v.ea}" onchange="setVsp(${i},'ea',this.value)"></div>`;
|
||||
|
||||
@@ -171,6 +171,10 @@
|
||||
<div class="modal-h"><span>공기질 히스테리시스 (ECO / NORMAL / TURBO)</span>
|
||||
<button class="btn" onclick="closeModal()">닫기</button></div>
|
||||
<div class="ctrl-row"><span class="lbl">프리셋</span><span id="cPresets"></span></div>
|
||||
<div class="ctrl-row" style="gap:8px;margin-top:2px">
|
||||
<button class="btn" onclick="hystReadErv()">읽어오기</button>
|
||||
<button class="btn" onclick="hystPreset()">프리셋(기본값)</button>
|
||||
</div>
|
||||
<div style="font-weight:700;font-size:13px;margin:6px 0 2px">데드밴드(하강)</div>
|
||||
<div id="hystGrid"></div>
|
||||
<button class="btn" style="margin-top:10px" onclick="applyHyst()">데드밴드 변경</button>
|
||||
@@ -182,6 +186,11 @@
|
||||
<div class="modal-h"><span>풍량 VSP 제어 · 상태 (SA 급기 / EA 배기)</span>
|
||||
<button class="btn" onclick="closeModal()">닫기</button></div>
|
||||
<div class="vsp-grid" id="vspGrid"></div>
|
||||
<div style="margin-top:10px;display:flex;gap:8px">
|
||||
<button class="btn" onclick="vspReadErv()">읽어오기</button>
|
||||
<button class="btn" onclick="vspPreset()">프리셋(기본값)</button>
|
||||
<button class="btn" onclick="vspApply()">변경</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal modal-wide" id="modalGraph" style="display:none">
|
||||
<div class="modal-h"><span>로그 그래프 (가로 시간 · 세로 댐퍼/센서/모드)</span>
|
||||
@@ -223,7 +232,7 @@ const AQ = {1:{t:"매우나쁨",c:"#EF4444"},2:{t:"나쁨",c:"#F59E0B"},3:{t:"
|
||||
const VSP_LABELS = ["환기1","환기2","환기3","환기4","바이패스","공청1","공청2","공청3","공청4"];
|
||||
const VSP_GROUP = [0,0,0,0,1,2,2,2,2];
|
||||
const VSP_INDEX = [1,2,3,4,1,1,2,3,4];
|
||||
const VSP_DEMO = [[20,18],[40,38],[60,58],[80,78],[70,0],[25,0],[45,0],[65,0],[85,0]];
|
||||
const VSP_DEMO = [[56,57],[63,63],[70,70],[86,85],[67,75],[65,0],[72,0],[78,0],[80,0]];
|
||||
const HYST_PRESETS = ["ECO","NORMAL","TURBO"];
|
||||
const HYST_FIELDS = ["pm25","pm10","voc","co2"];
|
||||
const POLL = ["co2","pm25","pm10","voc"]; // 임계 오염원 순서(서버 thr 키)
|
||||
@@ -232,6 +241,10 @@ const THR_DEMO = [
|
||||
{co2:[1000,1300,1600,2000],pm25:[20,38,60,86],pm10:[40,86,126,173],voc:[171,195,308,438]},
|
||||
{co2:[800,1100,1400,1700],pm25:[14,29,49,69],pm10:[28,66,102,138],voc:[120,150,250,350]},
|
||||
{co2:[700,1000,1300,1600],pm25:[12,23,38,52],pm10:[24,53,78,104],voc:[103,120,192,263]}];
|
||||
// ===== 사양 기본값 (모달 '프리셋' 버튼) : VSP=개발사양서 p.12(휴벤 ECO2/좌타입=펌웨어) / 데드밴드·임계=p.10 =====
|
||||
const VSP_DEFAULT = [[56,57],[63,63],[70,70],[86,85],[67,75],[65,0],[72,0],[78,0],[80,0]];
|
||||
const DB_DEFAULT = [{pm25:2,pm10:5,voc:5,co2:50},{pm25:2,pm10:5,voc:5,co2:50},{pm25:2,pm10:5,voc:3,co2:30}];
|
||||
const THR_DEFAULT = THR_DEMO.map(t=>({co2:[...t.co2],pm25:[...t.pm25],pm10:[...t.pm10],voc:[...t.voc]}));
|
||||
const HIST=60;
|
||||
|
||||
let current="site01", demoOn=true, tick=0;
|
||||
@@ -245,6 +258,12 @@ SITES.forEach((s,si)=>{ state[s]={
|
||||
thr:THR_DEMO.map(t=>({co2:[...t.co2],pm25:[...t.pm25],pm10:[...t.pm10],voc:[...t.voc]})),
|
||||
seed:si*100 }; });
|
||||
|
||||
// 모달 편집 버퍼 (폴링이 덮어쓰지 않음). null=ERV(state)값 표시. 읽어오기/프리셋/편집 시 채움, 변경/닫기 시 비움.
|
||||
let bufVsp=null, bufHyst=null, bufThr=null;
|
||||
function copyVsp(v){ return (v||[]).map(x=>({sa:x.sa,ea:x.ea})); }
|
||||
function copyHyst(h){ return (h||[]).map(x=>({pm25:x.pm25,pm10:x.pm10,voc:x.voc,co2:x.co2})); }
|
||||
function copyThr(t){ return (t||[]).map(x=>({co2:[...x.co2],pm25:[...x.pm25],pm10:[...x.pm10],voc:[...x.voc]})); }
|
||||
|
||||
// ===== 데모 생성기 =====
|
||||
function genSensors(s,t){
|
||||
const st=state[s]; st.online=true;
|
||||
@@ -289,6 +308,7 @@ async function ctl(action,a={}){
|
||||
}
|
||||
}
|
||||
function flash(msg){ const f=document.getElementById("footNote"); f.textContent=msg; f.style.color="var(--bad)"; setTimeout(setFoot,2500); }
|
||||
function okMsg(msg){ const f=document.getElementById("footNote"); f.textContent=msg; f.style.color="var(--good)"; setTimeout(setFoot,2200); }
|
||||
|
||||
// ===== 제어 핸들러 =====
|
||||
function togglePower(){ ctl("power",{value:state[current].g.power?0:1}); }
|
||||
@@ -310,13 +330,18 @@ function setDamperEa(room){ if(state[current].g.run_mode===2)return; const cur=s
|
||||
function setLed(room,val){ ctl("led",{room,value:parseInt(val)}); }
|
||||
function setReserve(h){ ctl("reserve",{value:parseInt(h)}); }
|
||||
function toggleReset(){ ctl("reset",{value:state[current].g.reset?0:1}); }
|
||||
function setHyst(pi,field,val){ state[current].hyst[pi][field]=parseInt(val)||0; }
|
||||
function applyHyst(){ const st=state[current];
|
||||
for(let pi=0;pi<3;pi++){ const v=st.hyst[pi]; ctl("hyst",{preset:pi,pm25:v.pm25,pm10:v.pm10,voc:v.voc,co2:v.co2}); } }
|
||||
function setThr(pi,poll,li,val){ state[current].thr[pi][poll][li]=parseInt(val)||0; }
|
||||
function applyThr(){ const st=state[current];
|
||||
for(let pi=0;pi<3;pi++) POLL.forEach((poll,pp)=>{ const v=st.thr[pi][poll];
|
||||
ctl("hystthr",{preset:pi,pollutant:pp,l1:v[0],l2:v[1],l3:v[2],l4:v[3],_poll:poll}); }); }
|
||||
function setHyst(pi,field,val){ if(!bufHyst)bufHyst=copyHyst(state[current].hyst); bufHyst[pi][field]=parseInt(val)||0; }
|
||||
function applyHyst(){ const hb=bufHyst||state[current].hyst;
|
||||
for(let pi=0;pi<3;pi++){ const v=hb[pi]; ctl("hyst",{preset:pi,pm25:v.pm25,pm10:v.pm10,voc:v.voc,co2:v.co2}); }
|
||||
bufHyst=null; okMsg("데드밴드 전송 완료"); }
|
||||
function setThr(pi,poll,li,val){ if(!bufThr)bufThr=copyThr(state[current].thr); bufThr[pi][poll][li]=parseInt(val)||0; }
|
||||
function applyThr(){ const tb=bufThr||state[current].thr;
|
||||
for(let pi=0;pi<3;pi++) POLL.forEach((poll,pp)=>{ const v=tb[pi][poll];
|
||||
ctl("hystthr",{preset:pi,pollutant:pp,l1:v[0],l2:v[1],l3:v[2],l4:v[3],_poll:poll}); });
|
||||
bufThr=null; okMsg("임계 전송 완료"); }
|
||||
// 히스테리시스 읽어오기/프리셋 (편집은 '데드밴드 변경'/'임계 변경'으로 전송)
|
||||
function hystReadErv(){ bufHyst=copyHyst(state[current].hyst); bufThr=copyThr(state[current].thr); renderHyst(); renderThr(); okMsg("ERV값 불러옴"); }
|
||||
function hystPreset(){ bufHyst=DB_DEFAULT.map(h=>({...h})); bufThr=copyThr(THR_DEFAULT); renderHyst(); renderThr(); okMsg("사양 기본값 불러옴 — '변경'으로 전송"); }
|
||||
|
||||
// ===== 팝업 =====
|
||||
function openModal(which){
|
||||
@@ -324,18 +349,24 @@ function openModal(which){
|
||||
document.getElementById("modalVsp").style.display = which==="vsp"?"block":"none";
|
||||
document.getElementById("modalGraph").style.display = which==="graph"?"block":"none";
|
||||
document.getElementById("modalBg").classList.add("show");
|
||||
if(which==="vsp") bufVsp=copyVsp(state[current].vsp); // 열 때 ERV 현재값
|
||||
if(which==="hyst"){ bufHyst=copyHyst(state[current].hyst); bufThr=copyThr(state[current].thr); }
|
||||
renderControls(); renderHyst(); renderThr(); renderVsp();
|
||||
if(which==="graph"){ renderSide(); loadGraph(); } // 열 때 현재 로드날짜로 갱신
|
||||
}
|
||||
function graphOpen(){ return document.getElementById("modalBg").classList.contains("show")
|
||||
&& document.getElementById("modalGraph").style.display==="block"; }
|
||||
function closeModal(){ document.getElementById("modalBg").classList.remove("show"); }
|
||||
function closeModal(){ document.getElementById("modalBg").classList.remove("show"); bufVsp=bufHyst=bufThr=null; }
|
||||
function closeModalBg(e){ if(e.target.id==="modalBg") closeModal(); }
|
||||
function setVsp(i,field,val){
|
||||
const v=state[current].vsp[i];
|
||||
const sa=field==="sa"?(parseInt(val)||0):v.sa, ea=field==="ea"?(parseInt(val)||0):v.ea;
|
||||
ctl("vsp",{group:VSP_GROUP[i],index:VSP_INDEX[i],sa,ea,_idx:i});
|
||||
if(!bufVsp) bufVsp=copyVsp(state[current].vsp);
|
||||
bufVsp[i][field]=parseInt(val)||0;
|
||||
}
|
||||
function vspReadErv(){ bufVsp=copyVsp(state[current].vsp); renderVsp(); okMsg("ERV값 불러옴"); }
|
||||
function vspPreset(){ bufVsp=VSP_DEFAULT.map(([sa,ea])=>({sa,ea})); renderVsp(); okMsg("사양 기본값 불러옴 — '변경'으로 전송"); }
|
||||
function vspApply(){ const b=bufVsp||state[current].vsp;
|
||||
for(let i=0;i<9;i++) ctl("vsp",{group:VSP_GROUP[i],index:VSP_INDEX[i],sa:b[i].sa,ea:b[i].ea,_idx:i});
|
||||
bufVsp=null; renderVsp(); okMsg("VSP 전송 완료"); }
|
||||
|
||||
// ===== 스마트수면 시간설정 (브라우저 스케줄, 현재 현장에 적용) =====
|
||||
let _slLast=-1;
|
||||
@@ -443,19 +474,19 @@ function renderRooms(){
|
||||
function renderHyst(){
|
||||
const grid=document.getElementById("hystGrid");
|
||||
if(document.activeElement && grid.contains(document.activeElement)) return;
|
||||
const st=state[current];
|
||||
const hb=bufHyst||state[current].hyst;
|
||||
let h='<div class="hyst-grid"><span class="hh"></span><span class="hh">PM2.5</span><span class="hh">PM10</span><span class="hh">VOC</span><span class="hh">CO2</span>';
|
||||
HYST_PRESETS.forEach((name,pi)=>{ const v=(st.hyst&&st.hyst[pi])||{pm25:0,pm10:0,voc:0,co2:0};
|
||||
HYST_PRESETS.forEach((name,pi)=>{ const v=(hb&&hb[pi])||{pm25:0,pm10:0,voc:0,co2:0};
|
||||
h+=`<span class="pl">${name}</span>`+HYST_FIELDS.map(f=>`<input type="number" min="0" value="${v[f]}" onchange="setHyst(${pi},'${f}',this.value)">`).join(""); });
|
||||
grid.innerHTML=h+"</div>";
|
||||
}
|
||||
function renderThr(){
|
||||
const grid=document.getElementById("thrGrid");
|
||||
if(document.activeElement && grid.contains(document.activeElement)) return;
|
||||
const st=state[current];
|
||||
const tb=bufThr||state[current].thr;
|
||||
let h='<div class="thr-grid"><span class="hh"></span><span class="hh">L1</span><span class="hh">L2</span><span class="hh">L3</span><span class="hh">L4</span>';
|
||||
HYST_PRESETS.forEach((name,pi)=>{
|
||||
POLL.forEach(poll=>{ const v=(st.thr&&st.thr[pi]&&st.thr[pi][poll])||[0,0,0,0];
|
||||
POLL.forEach(poll=>{ const v=(tb&&tb[pi]&&tb[pi][poll])||[0,0,0,0];
|
||||
h+=`<span class="pl">${name}·${poll.toUpperCase()}</span>`+
|
||||
[0,1,2,3].map(li=>`<input type="number" min="0" value="${v[li]}" onchange="setThr(${pi},'${poll}',${li},this.value)">`).join("");
|
||||
});
|
||||
@@ -465,8 +496,8 @@ function renderThr(){
|
||||
function renderVsp(){
|
||||
const grid=document.getElementById("vspGrid");
|
||||
if(document.activeElement && grid.contains(document.activeElement)) return;
|
||||
const st=state[current];
|
||||
grid.innerHTML = VSP_LABELS.map((lab,i)=>{ const v=(st.vsp&&st.vsp[i])||{sa:0,ea:0};
|
||||
const arr=bufVsp||state[current].vsp;
|
||||
grid.innerHTML = VSP_LABELS.map((lab,i)=>{ const v=(arr&&arr[i])||{sa:0,ea:0};
|
||||
return `<div class="vsp-row"><span class="vl">${lab}</span>`+
|
||||
`<span class="u">SA</span><input type="number" min="0" value="${v.sa}" onchange="setVsp(${i},'sa',this.value)">`+
|
||||
`<span class="u">EA</span><input type="number" min="0" value="${v.ea}" onchange="setVsp(${i},'ea',this.value)"></div>`;
|
||||
|
||||
Reference in New Issue
Block a user