chore: HERV 통합 저장소 초기 커밋
- 펌웨어(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>
This commit is contained in:
@@ -0,0 +1,201 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Microsoft.Data.Sqlite;
|
||||
using ErvDashboard.Model;
|
||||
|
||||
namespace ErvDashboard.Storage
|
||||
{
|
||||
// 로그 시계열 실시간 저장 (SQLite 단일파일, 무제한 누적).
|
||||
// - sample : 5초 1행 (시간·전원·운전/풍량·시나리오)
|
||||
// - room_sample : 실별 댐퍼/센서 (sample 당 N행)
|
||||
// 그래프는 LoadRecent() 로 최근 구간을 읽어 표시, 이후 실시간 append.
|
||||
public sealed class LogDb : IDisposable
|
||||
{
|
||||
readonly SqliteConnection _conn;
|
||||
|
||||
public LogDb(string path)
|
||||
{
|
||||
_conn = new SqliteConnection($"Data Source={path}");
|
||||
_conn.Open();
|
||||
Exec("PRAGMA journal_mode=WAL;"); // 동시 읽기/쓰기 성능
|
||||
Exec("PRAGMA synchronous=NORMAL;");
|
||||
Exec(@"CREATE TABLE IF NOT EXISTS sample(
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
time INTEGER NOT NULL, -- DateTime.Ticks
|
||||
power INTEGER, runmode INTEGER, automode INTEGER, fanmode INTEGER,
|
||||
sleep INTEGER, cook INTEGER, recover INTEGER, hystpreset INTEGER);");
|
||||
// 구버전 DB 마이그레이션 — 이미 있으면 무시
|
||||
try { Exec("ALTER TABLE sample ADD COLUMN automode INTEGER DEFAULT 0;"); } catch { }
|
||||
try { Exec("ALTER TABLE sample ADD COLUMN hystpreset INTEGER DEFAULT 0;"); } catch { }
|
||||
Exec(@"CREATE TABLE IF NOT EXISTS room_sample(
|
||||
sample_id INTEGER NOT NULL,
|
||||
room_idx INTEGER NOT NULL,
|
||||
damper_sa INTEGER, damper_ra INTEGER,
|
||||
co2 INTEGER, pm25 INTEGER, pm10 INTEGER, voc INTEGER,
|
||||
temp INTEGER, humi INTEGER, led INTEGER, level INTEGER,
|
||||
PRIMARY KEY(sample_id, room_idx));");
|
||||
}
|
||||
|
||||
void Exec(string sql)
|
||||
{
|
||||
using var cmd = _conn.CreateCommand();
|
||||
cmd.CommandText = sql;
|
||||
cmd.ExecuteNonQuery();
|
||||
}
|
||||
|
||||
// 1샘플 실시간 저장 (sample + room_sample 트랜잭션 1회).
|
||||
public void Insert(LogSample s)
|
||||
{
|
||||
using var tx = _conn.BeginTransaction();
|
||||
|
||||
long id;
|
||||
using (var cmd = _conn.CreateCommand())
|
||||
{
|
||||
cmd.Transaction = tx;
|
||||
cmd.CommandText =
|
||||
"INSERT INTO sample(time,power,runmode,automode,fanmode,sleep,cook,recover,hystpreset) " +
|
||||
"VALUES($t,$p,$rm,$am,$fm,$sl,$ck,$rc,$hp); SELECT last_insert_rowid();";
|
||||
cmd.Parameters.AddWithValue("$t", s.Time.Ticks);
|
||||
cmd.Parameters.AddWithValue("$p", s.Power ? 1 : 0);
|
||||
cmd.Parameters.AddWithValue("$rm", (int)s.RunMode);
|
||||
cmd.Parameters.AddWithValue("$am", (int)s.AutoMode);
|
||||
cmd.Parameters.AddWithValue("$hp", (int)s.HystPreset);
|
||||
cmd.Parameters.AddWithValue("$fm", (int)s.FanMode);
|
||||
cmd.Parameters.AddWithValue("$sl", s.SmartSleep ? 1 : 0);
|
||||
cmd.Parameters.AddWithValue("$ck", s.ComfortCook ? 1 : 0);
|
||||
cmd.Parameters.AddWithValue("$rc", s.ReliefRecover ? 1 : 0);
|
||||
id = (long)(cmd.ExecuteScalar() ?? 0L);
|
||||
}
|
||||
|
||||
using (var cmd = _conn.CreateCommand())
|
||||
{
|
||||
cmd.Transaction = tx;
|
||||
cmd.CommandText =
|
||||
"INSERT INTO room_sample(sample_id,room_idx,damper_sa,damper_ra,co2,pm25,pm10,voc,temp,humi,led,level) " +
|
||||
"VALUES($sid,$ri,$dsa,$dra,$co2,$pm25,$pm10,$voc,$tp,$hm,$led,$lv);";
|
||||
var pSid = cmd.Parameters.Add("$sid", SqliteType.Integer);
|
||||
var pRi = cmd.Parameters.Add("$ri", SqliteType.Integer);
|
||||
var pDsa = cmd.Parameters.Add("$dsa", SqliteType.Integer);
|
||||
var pDra = cmd.Parameters.Add("$dra", SqliteType.Integer);
|
||||
var pCo2 = cmd.Parameters.Add("$co2", SqliteType.Integer);
|
||||
var pP25 = cmd.Parameters.Add("$pm25", SqliteType.Integer);
|
||||
var pP10 = cmd.Parameters.Add("$pm10", SqliteType.Integer);
|
||||
var pVoc = cmd.Parameters.Add("$voc", SqliteType.Integer);
|
||||
var pTp = cmd.Parameters.Add("$tp", SqliteType.Integer);
|
||||
var pHm = cmd.Parameters.Add("$hm", SqliteType.Integer);
|
||||
var pLed = cmd.Parameters.Add("$led", SqliteType.Integer);
|
||||
var pLv = cmd.Parameters.Add("$lv", SqliteType.Integer);
|
||||
pSid.Value = id;
|
||||
for (int i = 0; i < s.Rooms.Length; i++)
|
||||
{
|
||||
var rm = s.Rooms[i];
|
||||
pRi.Value = i;
|
||||
pDsa.Value = rm.DamperSa ? 1 : 0;
|
||||
pDra.Value = rm.DamperRa ? 1 : 0;
|
||||
pCo2.Value = rm.Co2; pP25.Value = rm.Pm25; pP10.Value = rm.Pm10; pVoc.Value = rm.Voc;
|
||||
pTp.Value = rm.Temp; pHm.Value = rm.Humi; pLed.Value = rm.Led; pLv.Value = rm.Level;
|
||||
cmd.ExecuteNonQuery();
|
||||
}
|
||||
}
|
||||
|
||||
tx.Commit();
|
||||
}
|
||||
|
||||
// 최근 max개 샘플을 시간 오름차순으로 로드 (그래프 표시용).
|
||||
public List<LogSample> LoadRecent(int max)
|
||||
{
|
||||
using var cmd = _conn.CreateCommand();
|
||||
cmd.CommandText =
|
||||
"SELECT id,time,power,runmode,automode,fanmode,sleep,cook,recover,hystpreset FROM " +
|
||||
"(SELECT * FROM sample ORDER BY id DESC LIMIT $n) ORDER BY id ASC;";
|
||||
cmd.Parameters.AddWithValue("$n", max);
|
||||
return Materialize(cmd);
|
||||
}
|
||||
|
||||
// 지정 날짜(그날 00:00 ~ 다음날 00:00, 로컬)의 샘플을 시간 오름차순으로 로드.
|
||||
public List<LogSample> LoadByDate(DateTime day)
|
||||
{
|
||||
using var cmd = _conn.CreateCommand();
|
||||
cmd.CommandText =
|
||||
"SELECT id,time,power,runmode,automode,fanmode,sleep,cook,recover,hystpreset " +
|
||||
"FROM sample WHERE time >= $s AND time < $e ORDER BY id ASC;";
|
||||
cmd.Parameters.AddWithValue("$s", day.Date.Ticks);
|
||||
cmd.Parameters.AddWithValue("$e", day.Date.AddDays(1).Ticks);
|
||||
return Materialize(cmd);
|
||||
}
|
||||
|
||||
// 데이터가 존재하는 날짜 목록(오름차순) — 상단 날짜 선택 UI용.
|
||||
public List<DateTime> LoadDates()
|
||||
{
|
||||
var list = new List<DateTime>();
|
||||
using var cmd = _conn.CreateCommand();
|
||||
// Ticks/일(864e9)로 버킷팅 → 날짜(자정) 환산
|
||||
cmd.CommandText = "SELECT DISTINCT time/864000000000 FROM sample ORDER BY 1;";
|
||||
using var rd = cmd.ExecuteReader();
|
||||
while (rd.Read()) list.Add(new DateTime(rd.GetInt64(0) * TimeSpan.TicksPerDay));
|
||||
return list;
|
||||
}
|
||||
|
||||
// sample 행을 읽고 room_sample 을 붙여 LogSample 리스트로 구성 (시간 오름차순).
|
||||
List<LogSample> Materialize(SqliteCommand sampleCmd)
|
||||
{
|
||||
var byId = new Dictionary<long, LogSample>();
|
||||
var order = new List<long>();
|
||||
using (var rd = sampleCmd.ExecuteReader())
|
||||
{
|
||||
while (rd.Read())
|
||||
{
|
||||
long id = rd.GetInt64(0);
|
||||
byId[id] = new LogSample
|
||||
{
|
||||
Time = new DateTime(rd.GetInt64(1)),
|
||||
Power = rd.GetInt32(2) != 0,
|
||||
RunMode = (byte)rd.GetInt32(3),
|
||||
AutoMode = (byte)(rd.IsDBNull(4) ? 0 : rd.GetInt32(4)),
|
||||
FanMode = (byte)rd.GetInt32(5),
|
||||
SmartSleep = rd.GetInt32(6) != 0,
|
||||
ComfortCook = rd.GetInt32(7) != 0,
|
||||
ReliefRecover = rd.GetInt32(8) != 0,
|
||||
HystPreset = (byte)(rd.IsDBNull(9) ? 0 : rd.GetInt32(9)),
|
||||
};
|
||||
order.Add(id);
|
||||
}
|
||||
}
|
||||
if (order.Count == 0) return new List<LogSample>();
|
||||
AttachRooms(byId, order);
|
||||
var result = new List<LogSample>(order.Count);
|
||||
foreach (var id in order) result.Add(byId[id]);
|
||||
return result;
|
||||
}
|
||||
|
||||
// order(오름차순 id) 범위의 room_sample 을 읽어 각 LogSample.Rooms 채움.
|
||||
void AttachRooms(Dictionary<long, LogSample> byId, List<long> order)
|
||||
{
|
||||
var rooms = new Dictionary<long, List<RoomSample>>();
|
||||
using var cmd = _conn.CreateCommand();
|
||||
cmd.CommandText =
|
||||
"SELECT sample_id,room_idx,damper_sa,damper_ra,co2,pm25,pm10,voc,temp,humi,led,level " +
|
||||
"FROM room_sample WHERE sample_id BETWEEN $min AND $max ORDER BY sample_id, room_idx;";
|
||||
cmd.Parameters.AddWithValue("$min", order[0]);
|
||||
cmd.Parameters.AddWithValue("$max", order[order.Count - 1]);
|
||||
using var rd = cmd.ExecuteReader();
|
||||
while (rd.Read())
|
||||
{
|
||||
long sid = rd.GetInt64(0);
|
||||
if (!byId.ContainsKey(sid)) continue;
|
||||
if (!rooms.TryGetValue(sid, out var lst)) { lst = new List<RoomSample>(); rooms[sid] = lst; }
|
||||
lst.Add(new RoomSample
|
||||
{
|
||||
DamperSa = rd.GetInt32(2) != 0,
|
||||
DamperRa = rd.GetInt32(3) != 0,
|
||||
Co2 = rd.GetInt32(4), Pm25 = rd.GetInt32(5), Pm10 = rd.GetInt32(6), Voc = rd.GetInt32(7),
|
||||
Temp = rd.GetInt32(8), Humi = rd.GetInt32(9), Led = rd.GetInt32(10), Level = rd.GetInt32(11),
|
||||
});
|
||||
}
|
||||
foreach (var id in order)
|
||||
if (rooms.TryGetValue(id, out var lst)) byId[id].Rooms = lst.ToArray();
|
||||
}
|
||||
|
||||
public void Dispose() => _conn.Dispose();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user