b18d9c84bf
펌웨어(SOURCE/HECO2/User): - My_Hood.c: 후드 전원 OFF 시 쾌적조리 토글 자동 해제 (6) - My_RJ2.c/My_Homenet.c/My_define.h: 룸컨 전용 pending(RoomCtrl_Push) 도입 → 전원 ON 간헐 미동작·전원 OFF 후 룸컨 옛모드(공청) 표시 해소 (11)(12) (공유 Command_request_type 레이스 + 716 equalize 조기클리어 + 분배기 wipe 회피) 대시보드(TestProgram/PCDashBoard): - 쾌적조리 미선택+후드 ON 시 다른 시나리오 버튼 선택 가능 (7) - 시나리오 버튼 항상 선택 가능 + 상호배타 로직 삭제 (8)(9) - 데모 루틴 전체 삭제 (DemoStatus.cs 포함) (9) - 전원 OFF 시 풍량 버튼(0 포함) 비활성 (13) - 그래프 DB(HERV_Log.db)를 임시폴더가 아닌 exe 폴더에 저장 (14) 문서/메모리: doc/260618_*.md 정리, command-request-type-shared-race 메모리 추가 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
106 lines
4.9 KiB
C#
106 lines
4.9 KiB
C#
using ErvDashboard.Protocol; // SerialChannel, HexFormat
|
|
using ErvProtocol;
|
|
|
|
namespace ErvDashboard.Api
|
|
{
|
|
// IErvApi 의 RS485(시리얼) 구현. 시리얼 채널 + 공용 프레임 파서/빌더(CtrlFrame, StatusDecoder)를 내장한다.
|
|
// UI 비종속 : STATUS 는 StatusRecord 이벤트로만 알리고, DashboardState 매핑은 호출부(MainWindow)가 담당.
|
|
public sealed class SerialErvApi : IErvApi
|
|
{
|
|
readonly SerialChannel _ch = new();
|
|
readonly FrameParser _parser = new();
|
|
|
|
DateTime _lastByte = DateTime.MinValue;
|
|
static readonly TimeSpan FrameGap = TimeSpan.FromMilliseconds(60);
|
|
|
|
StatusRecord? _lastStatus; // 최근 수신 STATUS (GetRoomStatus 조회용)
|
|
|
|
public event Action<bool>? ConnectionChanged;
|
|
public event Action<StatusRecord>? StatusReceived;
|
|
public event Action<string>? Log;
|
|
|
|
public bool IsConnected => _ch.IsConnected;
|
|
|
|
public static string[] GetAvailablePorts() => SerialChannel.GetAvailablePorts();
|
|
|
|
public SerialErvApi()
|
|
{
|
|
_ch.ByteReceived += OnByte;
|
|
_ch.Log += s => Log?.Invoke(s);
|
|
_ch.ConnectionChanged += b => ConnectionChanged?.Invoke(b);
|
|
_parser.OnFrame += HandleFrame;
|
|
_parser.OnError += m => Log?.Invoke("RX " + m);
|
|
}
|
|
|
|
public bool Connect(string port, int baud) => _ch.Connect(port, baud);
|
|
public void Disconnect() => _ch.Disconnect();
|
|
|
|
// 최근 STATUS 기준 각실(room 1~4) 상태 조회
|
|
public bool GetRoomStatus(int room, out bool damperSa, out bool damperEa, out AirQuality airQuality, out int led)
|
|
{
|
|
damperSa = false; damperEa = false; airQuality = AirQuality.Normal; led = 0;
|
|
var s = _lastStatus;
|
|
if (s == null || room < 1 || room > 4) return false;
|
|
var r = s.Rooms[room - 1];
|
|
damperSa = r.DamperSa;
|
|
damperEa = r.DamperEa;
|
|
airQuality = (AirQuality)r.AirQuality;
|
|
led = r.LedDim;
|
|
return true;
|
|
}
|
|
|
|
// 시리얼 바이트 → 공용 파서 (프레임 갭 시 동기 리셋)
|
|
void OnByte(byte b)
|
|
{
|
|
var now = DateTime.UtcNow;
|
|
if (now - _lastByte > FrameGap) _parser.Reset();
|
|
_lastByte = now;
|
|
_parser.FeedByte(b);
|
|
}
|
|
|
|
void HandleFrame(byte cmd, byte[] p)
|
|
{
|
|
switch (cmd)
|
|
{
|
|
case StatusDecoder.STATUS:
|
|
Log?.Invoke($"RX STATUS ({p.Length}B)");
|
|
var rec = StatusDecoder.Decode(p);
|
|
if (rec != null) { _lastStatus = rec; StatusReceived?.Invoke(rec); }
|
|
else Log?.Invoke($" STATUS too short ({p.Length}<{StatusDecoder.STATUS_LEN})");
|
|
break;
|
|
case StatusDecoder.ACK:
|
|
if (p.Length >= 2)
|
|
Log?.Invoke($"RX ACK echo=0x{p[0]:X2} result={(p[1] == 0 ? "OK" : "ERR")}");
|
|
break;
|
|
default:
|
|
Log?.Invoke($"RX unknown cmd=0x{cmd:X2}");
|
|
break;
|
|
}
|
|
}
|
|
|
|
// ================= 송신 (공용 CtrlFrame) =================
|
|
void SendFrame(byte[] f)
|
|
{
|
|
if (_ch.Send(f, f.Length))
|
|
Log?.Invoke($"TX {HexFormat.Bytes(f, f.Length)}");
|
|
}
|
|
|
|
public void RequestStatus() => SendFrame(CtrlFrame.RequestStatus());
|
|
public void SetPower(bool on) => SendFrame(CtrlFrame.Power(on ? 1 : 0));
|
|
public void SetRunMode(RunMode mode) => SendFrame(CtrlFrame.RunModeCmd((int)mode));
|
|
public void SetFan(int speed) => SendFrame(CtrlFrame.Fan(speed));
|
|
public void SetSubMode(SubModeType type, bool on) => SendFrame(CtrlFrame.SubMode((int)type, on ? 1 : 0));
|
|
public void SetHood(bool on) => SendFrame(CtrlFrame.Hood(on ? 1 : 0));
|
|
public void SetReserve(int hours) => SendFrame(CtrlFrame.Reserve(hours));
|
|
public void SetReset(bool on) => SendFrame(CtrlFrame.Reset(on ? 1 : 0));
|
|
public void SetDiffuserDamper(int room, int type, bool open) => SendFrame(CtrlFrame.Damper(room, type, open ? 1 : 0));
|
|
public void SetDiffuserLed(int room, int dim) => SendFrame(CtrlFrame.Led(room, dim));
|
|
public void SetVsp(int group, int index, int sa, int ea) => SendFrame(CtrlFrame.Vsp(group, index, sa, ea));
|
|
public void SetHystPreset(HystPreset preset) => SendFrame(CtrlFrame.Preset((int)preset));
|
|
public void SetHystDeadband(int preset, int pm25, int pm10, int voc, int co2) => SendFrame(CtrlFrame.HystValue(preset, pm25, pm10, voc, co2));
|
|
public void SetHystThreshold(int preset, int pollutant, int l1, int l2, int l3, int l4) => SendFrame(CtrlFrame.HystThr(preset, pollutant, l1, l2, l3, l4));
|
|
|
|
public void Dispose() => _ch.Dispose();
|
|
}
|
|
}
|