5a96a696b1
- 펌웨어(program), C# 대시보드(TestProgram), 시뮬레이터(Simulator), 프로토콜/문서(Protocol, doc) 전체를 단일 저장소로 통합 - program 폴더의 별도 git 저장소를 제거하고 통합 저장소에 흡수 - 빌드 산출물(program/build, bin/obj, *.o/.elf/.bin/.hex 등) .gitignore 처리 - 사내 Synology NAS Git 원격 연결 예정 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
219 lines
11 KiB
C#
219 lines
11 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Collections.ObjectModel;
|
|
using System.Linq;
|
|
using System.Windows;
|
|
using System.Windows.Controls;
|
|
using LiveChartsCore.Defaults;
|
|
using LiveChartsCore.SkiaSharpView;
|
|
using ErvDashboard.Model;
|
|
using ErvDashboard.Storage;
|
|
|
|
namespace ErvDashboard
|
|
{
|
|
// 로그 그래프 창 : 가로=시간, 세로=각실 댐퍼on/off·센서값·운전/시나리오모드 (LiveCharts2).
|
|
// 좌측 체크박스로 표시 시리즈 선택. 데이터는 SQLite(LogDb)에서 최근 구간 로드 + 실시간 append.
|
|
public partial class GraphWindow : Window
|
|
{
|
|
readonly LogDb _db;
|
|
readonly string[] _roomNames;
|
|
const int MaxSamples = 17280; // 차트 표시 상한(최근 24h @ 5초). DB에는 전체 보관.
|
|
|
|
sealed class SeriesDef
|
|
{
|
|
public string Group = "";
|
|
public string Label = "";
|
|
public Func<LogSample, double> Value = _ => 0;
|
|
public bool Default;
|
|
}
|
|
|
|
SeriesDef[] _defs = Array.Empty<SeriesDef>();
|
|
LineSeries<DateTimePoint>[] _series = Array.Empty<LineSeries<DateTimePoint>>();
|
|
int _builtCount;
|
|
bool[] _shown = Array.Empty<bool>(); // 시리즈별 표시(체크) 상태 — 체크된 것만 차트/범례에 노출
|
|
DateTime _selectedDate = DateTime.Today; // 현재 로드된 날짜 (기본 오늘)
|
|
|
|
public GraphWindow(string[] roomNames, LogDb db)
|
|
{
|
|
InitializeComponent();
|
|
_db = db;
|
|
_roomNames = roomNames;
|
|
BuildDefs();
|
|
BuildChart();
|
|
BuildCheckboxes();
|
|
DatePick.SelectedDate = DateTime.Today;
|
|
_selectedDate = DateTime.Today;
|
|
BuildForDate(); // 초기: 오늘 로드(실시간). 이후 날짜는 '불러오기' 버튼으로 명시적 로드.
|
|
}
|
|
|
|
void BuildDefs()
|
|
{
|
|
var list = new List<SeriesDef>
|
|
{
|
|
new() { Group = "운전", Label = "운전모드", Value = s => s.RunMode },
|
|
new() { Group = "운전", Label = "자동-집중", Value = s => s.AutoMode == 1 ? 1 : 0 },
|
|
new() { Group = "운전", Label = "자동-분산", Value = s => s.AutoMode == 2 ? 1 : 0 },
|
|
new() { Group = "운전", Label = "프리셋-ECO", Value = s => s.HystPreset == 0 ? 1 : 0 },
|
|
new() { Group = "운전", Label = "프리셋-NORMAL", Value = s => s.HystPreset == 1 ? 1 : 0 },
|
|
new() { Group = "운전", Label = "프리셋-TURBO", Value = s => s.HystPreset == 2 ? 1 : 0 },
|
|
new() { Group = "운전", Label = "풍량", Value = s => s.FanMode },
|
|
new() { Group = "운전", Label = "전원", Value = s => s.Power ? 1 : 0 },
|
|
new() { Group = "시나리오", Label = "스마트수면", Value = s => s.SmartSleep ? 1 : 0 },
|
|
new() { Group = "시나리오", Label = "쾌적조리", Value = s => s.ComfortCook ? 1 : 0 },
|
|
new() { Group = "시나리오", Label = "안심회복", Value = s => s.ReliefRecover ? 1 : 0 },
|
|
};
|
|
for (int r = 0; r < _roomNames.Length; r++)
|
|
{
|
|
int ri = r; string nm = _roomNames[r];
|
|
list.Add(new() { Group = nm, Label = $"{nm} CO2", Value = s => s.Rooms[ri].Co2, Default = true });
|
|
list.Add(new() { Group = nm, Label = $"{nm} PM2.5", Value = s => s.Rooms[ri].Pm25 });
|
|
list.Add(new() { Group = nm, Label = $"{nm} PM10", Value = s => s.Rooms[ri].Pm10 });
|
|
list.Add(new() { Group = nm, Label = $"{nm} VOC", Value = s => s.Rooms[ri].Voc });
|
|
list.Add(new() { Group = nm, Label = $"{nm} 온도", Value = s => s.Rooms[ri].Temp });
|
|
list.Add(new() { Group = nm, Label = $"{nm} 습도", Value = s => s.Rooms[ri].Humi });
|
|
list.Add(new() { Group = nm, Label = $"{nm} LED", Value = s => s.Rooms[ri].Led });
|
|
list.Add(new() { Group = nm, Label = $"{nm} 부하점수", Value = s => s.Rooms[ri].Level });
|
|
list.Add(new() { Group = nm, Label = $"{nm} 급기댐퍼", Value = s => s.Rooms[ri].DamperSa ? 1 : 0 });
|
|
list.Add(new() { Group = nm, Label = $"{nm} 배기댐퍼", Value = s => s.Rooms[ri].DamperRa ? 1 : 0 });
|
|
}
|
|
_defs = list.ToArray();
|
|
}
|
|
|
|
void BuildChart()
|
|
{
|
|
_series = _defs.Select(d => new LineSeries<DateTimePoint>
|
|
{
|
|
Name = d.Label,
|
|
Values = new ObservableCollection<DateTimePoint>(),
|
|
GeometrySize = 0,
|
|
LineSmoothness = 0,
|
|
}).ToArray();
|
|
_shown = new bool[_defs.Length];
|
|
Array.Fill(_shown, true); // 그래프 열면 전체 선택(좌측 체크박스도 전부 체크)
|
|
// 가로축: 날짜 제거, 시간(HH:mm:ss)만 표시 (날짜는 상단 DatePicker)
|
|
Chart.XAxes = new[] { new DateTimeAxis(TimeSpan.FromSeconds(5), dt => dt.ToString("HH:mm:ss")) };
|
|
Chart.ZoomMode = LiveChartsCore.Measure.ZoomAndPanMode.X;
|
|
Chart.LegendTextSize = 10; // 범례 글씨 작게
|
|
Chart.LegendPosition = LiveChartsCore.Measure.LegendPosition.Bottom; // 범례 하단
|
|
ApplyVisible();
|
|
}
|
|
|
|
// 체크된 시리즈만 차트(+범례)에 노출. 체크 해제하면 범례에서도 사라짐.
|
|
void ApplyVisible()
|
|
{
|
|
Chart.Series = _series.Where((s, i) => _shown[i]).ToArray();
|
|
}
|
|
|
|
void BuildCheckboxes()
|
|
{
|
|
string? group = null;
|
|
for (int i = 0; i < _defs.Length; i++)
|
|
{
|
|
if (_defs[i].Group != group)
|
|
{
|
|
group = _defs[i].Group;
|
|
CheckPanel.Children.Add(new TextBlock { Text = group, FontWeight = FontWeights.Bold, Margin = new Thickness(0, 8, 0, 2) });
|
|
}
|
|
int idx = i;
|
|
var label = _defs[i].Label.StartsWith(group + " ") ? _defs[i].Label.Substring(group!.Length + 1) : _defs[i].Label;
|
|
var cb = new CheckBox { Content = label, IsChecked = true, Margin = new Thickness(4, 1, 0, 1) };
|
|
cb.Checked += (_, _) => { _shown[idx] = true; ApplyVisible(); };
|
|
cb.Unchecked += (_, _) => { _shown[idx] = false; ApplyVisible(); };
|
|
CheckPanel.Children.Add(cb);
|
|
}
|
|
}
|
|
|
|
// 선택 날짜(_selectedDate)의 하루치 데이터를 DB에서 로드해 차트 구성.
|
|
void BuildForDate()
|
|
{
|
|
var samples = _db.LoadByDate(_selectedDate);
|
|
for (int k = 0; k < _series.Length; k++)
|
|
{
|
|
var col = (ObservableCollection<DateTimePoint>)_series[k].Values!;
|
|
col.Clear();
|
|
foreach (var s in samples) col.Add(new DateTimePoint(s.Time, _defs[k].Value(s)));
|
|
}
|
|
_builtCount = samples.Count;
|
|
bool today = _selectedDate == DateTime.Today;
|
|
CountText.Text = $"{_selectedDate:yyyy-MM-dd} · {samples.Count}개 (5초){(today ? " · 실시간" : "")}";
|
|
}
|
|
|
|
// "불러오기" : 선택한 날짜를 확정 로드. 오늘이면 실시간 갱신, 과거면 정적 유지.
|
|
void Load_Click(object sender, RoutedEventArgs e)
|
|
{
|
|
_selectedDate = DatePick.SelectedDate?.Date ?? DateTime.Today;
|
|
BuildForDate();
|
|
}
|
|
|
|
// 메인이 새 샘플을 저장할 때 호출 — 오늘 보기일 때만 차트에 증분 1건 추가(상한 초과분 트림).
|
|
public void OnSampleAdded(LogSample s)
|
|
{
|
|
if (_selectedDate != DateTime.Today) return; // 과거 날짜 보기 중엔 실시간 추가 안 함
|
|
for (int k = 0; k < _series.Length; k++)
|
|
{
|
|
var col = (ObservableCollection<DateTimePoint>)_series[k].Values!;
|
|
col.Add(new DateTimePoint(s.Time, _defs[k].Value(s)));
|
|
if (col.Count > MaxSamples) col.RemoveAt(0);
|
|
}
|
|
_builtCount++;
|
|
CountText.Text = $"{_selectedDate:yyyy-MM-dd} · {Math.Min(_builtCount, MaxSamples)}개 (5초) · 실시간";
|
|
}
|
|
|
|
void Refresh_Click(object sender, RoutedEventArgs e) => BuildForDate();
|
|
|
|
// 선택 날짜의 데이터를 CSV(Excel 호환, UTF-8 BOM)로 저장. 날짜·시간 컬럼 분리.
|
|
void Excel_Click(object sender, RoutedEventArgs e)
|
|
{
|
|
var samples = _db.LoadByDate(_selectedDate);
|
|
if (samples.Count == 0) { MessageBox.Show("저장할 데이터가 없습니다."); return; }
|
|
var dlg = new Microsoft.Win32.SaveFileDialog
|
|
{
|
|
Filter = "CSV (Excel) (*.csv)|*.csv",
|
|
FileName = $"HERV_Log_{_selectedDate:yyyyMMdd}.csv"
|
|
};
|
|
if (dlg.ShowDialog() != true) return;
|
|
try { ExportCsv(dlg.FileName, samples); MessageBox.Show($"저장 완료:\n{dlg.FileName}"); }
|
|
catch (Exception ex) { MessageBox.Show($"저장 실패: {ex.Message}"); }
|
|
}
|
|
|
|
void ExportCsv(string path, List<LogSample> samples)
|
|
{
|
|
string[] modeName = { "환기", "자동", "바이패스", "공청" };
|
|
string[] autoName = { "", "집중", "분산" };
|
|
|
|
var head = new List<string> { "날짜", "시간", "전원", "운전모드", "자동상태", "풍량", "스마트수면", "쾌적조리", "안심회복" };
|
|
foreach (var nm in _roomNames)
|
|
head.AddRange(new[] { $"{nm}_급기댐퍼", $"{nm}_배기댐퍼", $"{nm}_CO2", $"{nm}_PM2.5", $"{nm}_PM10", $"{nm}_VOC", $"{nm}_온도", $"{nm}_습도", $"{nm}_LED", $"{nm}_부하점수" });
|
|
|
|
var sb = new System.Text.StringBuilder();
|
|
sb.AppendLine(string.Join(",", head));
|
|
foreach (var s in samples)
|
|
{
|
|
var row = new List<string>
|
|
{
|
|
s.Time.ToString("yyyy-MM-dd"),
|
|
s.Time.ToString("HH:mm:ss"),
|
|
s.Power ? "1" : "0",
|
|
s.RunMode < modeName.Length ? modeName[s.RunMode] : s.RunMode.ToString(),
|
|
s.AutoMode < autoName.Length ? autoName[s.AutoMode] : "",
|
|
s.FanMode.ToString(),
|
|
s.SmartSleep ? "1" : "0",
|
|
s.ComfortCook ? "1" : "0",
|
|
s.ReliefRecover ? "1" : "0",
|
|
};
|
|
foreach (var rm in s.Rooms)
|
|
{
|
|
row.Add(rm.DamperSa ? "1" : "0");
|
|
row.Add(rm.DamperRa ? "1" : "0");
|
|
row.Add(rm.Co2.ToString()); row.Add(rm.Pm25.ToString()); row.Add(rm.Pm10.ToString()); row.Add(rm.Voc.ToString());
|
|
row.Add(rm.Temp.ToString()); row.Add(rm.Humi.ToString()); row.Add(rm.Led.ToString()); row.Add(rm.Level.ToString());
|
|
}
|
|
sb.AppendLine(string.Join(",", row));
|
|
}
|
|
System.IO.File.WriteAllText(path, sb.ToString(), new System.Text.UTF8Encoding(true));
|
|
}
|
|
|
|
void Close_Click(object sender, RoutedEventArgs e) => Close();
|
|
}
|
|
}
|