Files
HECO2/TestProgram/WebDashBoard/ErvCollector/Program.cs
T
jeon 5a96a696b1 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>
2026-06-15 21:44:23 +09:00

156 lines
6.5 KiB
C#

using System.Net;
using System.Net.Sockets;
using System.Text.Json;
using ErvCollector.Http;
using ErvCollector.Storage;
using ErvProtocol;
namespace ErvCollector
{
// 미니PC 수집/제어 서버:
// 3개 현장 EW11(TCP Client) → 포트별(6001/6002/6003) 수신 → 0xAA STATUS 파싱
// → 샘플러(10초 + 변화시) → InfluxDB 적재
// 동시에 HTTP(대시보드 + /api/latest + /api/control) 로 원격 모니터링/제어 제공.
// 제어는 현장 EW11 이 열어둔 동일 TCP 소켓으로 CTRL_* 프레임을 역방향 송신.
internal static class Program
{
static Config _cfg = null!;
static InfluxLineWriter _influx = null!;
static SiteHub _hub = null!;
static HistoryDb _history = null!;
static async Task Main()
{
_cfg = Config.Load("appsettings.json");
_influx = new InfluxLineWriter(_cfg.Influx.Url, _cfg.Influx.Org, _cfg.Influx.Bucket, _cfg.Influx.Token);
_influx.OnError += m => LogErr(m);
_hub = new SiteHub();
_hub.Log += LogErr;
// 그래프 이력 — exe 옆 SQLite (현장별 5초 누적)
_history = new HistoryDb(Path.Combine(AppContext.BaseDirectory, "erv_history.db"));
Log($"ErvCollector 시작. Influx={_cfg.Influx.Url} bucket={_cfg.Influx.Bucket} 샘플주기={_cfg.SampleIntervalSeconds}s");
// HTTP 대시보드/제어 서버
var http = new ControlServer(_cfg.Http.Prefix, _cfg.Http.Token, _cfg.Sites.Select(s => s.Name), _hub, _history);
http.Log += Log;
try { http.Start(); Log($" HTTP 대시보드/제어 ← {_cfg.Http.Prefix}"); }
catch (Exception ex) { LogErr($"HTTP 서버 시작 실패: {ex.Message}"); }
var tasks = new List<Task>();
foreach (var site in _cfg.Sites)
{
Log($" 현장 '{site.Name}' ← TCP 포트 {site.Port} 대기");
tasks.Add(ListenSiteAsync(site));
}
await Task.WhenAll(tasks);
}
static async Task ListenSiteAsync(SiteConfig site)
{
var listener = new TcpListener(IPAddress.Any, site.Port);
listener.Start();
while (true)
{
TcpClient client;
try { client = await listener.AcceptTcpClientAsync(); }
catch (Exception ex) { LogErr($"[{site.Name}] accept 오류: {ex.Message}"); await Task.Delay(500); continue; }
_ = HandleClientAsync(site, client);
}
}
static async Task HandleClientAsync(SiteConfig site, TcpClient client)
{
var remote = client.Client.RemoteEndPoint?.ToString() ?? "?";
Log($"[{site.Name}] 연결됨 ({remote})");
var parser = new FrameParser();
var sampler = new Sampler(_cfg.SampleIntervalSeconds);
var lastHist = DateTime.MinValue; // 그래프 이력 5초 throttle (현장별)
parser.OnError += m => LogErr($"[{site.Name}] {m}");
parser.OnFrame += (cmd, payload) =>
{
if (cmd != StatusDecoder.STATUS) return; // 저장 대상은 STATUS만
var rec = StatusDecoder.Decode(payload);
if (rec == null) { LogErr($"[{site.Name}] STATUS 길이부족 ({payload.Length})"); return; }
_hub.SetStatus(site.Name, rec); // 최신 상태(원격 조회/대시보드용)
var now = DateTime.UtcNow;
// 그래프 이력 : 5초마다 SQLite 기록
if ((now - lastHist).TotalSeconds >= 5)
{
lastHist = now;
try { _history.Insert(site.Name, rec); } catch (Exception ex) { LogErr($"[{site.Name}] 이력저장: {ex.Message}"); }
}
if (sampler.ShouldWrite(rec, now, out var reason))
_ = _influx.WriteAsync(site.Name, rec, now);
// reason=="skip" 이면 적재 생략 (주기/무변화)
if (reason == "change") Log($"[{site.Name}] 상태변화 기록 (mode={rec.RunMode} fan={rec.FanMode} err=0x{rec.ErrorCode:X4})");
};
NetworkStream? stream = null;
try
{
using (client)
using (stream = client.GetStream())
{
_hub.SetSocket(site.Name, stream); // 제어 송신용 소켓 등록
var buf = new byte[1024];
int n;
while ((n = await stream.ReadAsync(buf)) > 0)
parser.Feed(buf.AsSpan(0, n));
}
}
catch (Exception ex) { LogErr($"[{site.Name}] 수신 오류: {ex.Message}"); }
finally
{
if (stream != null) _hub.RemoveSocket(site.Name, stream);
Log($"[{site.Name}] 연결 종료 ({remote})");
}
}
static void Log(string m) => Console.WriteLine($"[{DateTime.Now:yyyy-MM-dd HH:mm:ss}] {m}");
static void LogErr(string m) => Console.WriteLine($"[{DateTime.Now:yyyy-MM-dd HH:mm:ss}] ERR {m}");
}
// ---- 설정 ----
sealed class Config
{
public InfluxConfig Influx { get; set; } = new();
public HttpConfig Http { get; set; } = new();
public int SampleIntervalSeconds { get; set; } = 10;
public List<SiteConfig> Sites { get; set; } = new();
public static Config Load(string path)
{
var json = File.ReadAllText(path);
var opt = new JsonSerializerOptions { PropertyNameCaseInsensitive = true };
return JsonSerializer.Deserialize<Config>(json, opt)
?? throw new InvalidOperationException("appsettings.json 파싱 실패");
}
}
sealed class InfluxConfig
{
public string Url { get; set; } = "http://127.0.0.1:8086";
public string Org { get; set; } = "herv";
public string Bucket { get; set; } = "erv";
public string Token { get; set; } = "";
}
sealed class HttpConfig
{
public string Prefix { get; set; } = "http://+:8080/"; // Linux: http://*:8080/, Windows 비관리자: http://localhost:8080/
public string Token { get; set; } = ""; // 제어 인증 토큰(비우면 인증 없음, 개발용)
}
sealed class SiteConfig
{
public int Port { get; set; }
public string Name { get; set; } = "";
}
}