fix: 260618 룸컨 동기·쾌적조리·시나리오·그래프DB 등 수정
펌웨어(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>
This commit is contained in:
@@ -1,88 +0,0 @@
|
||||
namespace ErvProtocol
|
||||
{
|
||||
// 펌웨어 없이 UI/파이프라인 검증용 합성 STATUS payload(73B) 생성.
|
||||
public static class DemoStatus
|
||||
{
|
||||
public static byte[] BuildPayload(int tick)
|
||||
{
|
||||
var p = new byte[StatusDecoder.STATUS_LEN];
|
||||
p[0] = 1; // power
|
||||
p[1] = (byte)RunMode.Auto; // runMode
|
||||
p[2] = (byte)((tick / 5) % 2); // autoState 분산/집중
|
||||
p[3] = (byte)(2 + (tick % 3)); // fanMode 2~4
|
||||
p[4] = SubModeBits.SmartSleep; // subMode
|
||||
p[5] = (byte)(tick % 2); // hood
|
||||
p[6] = (byte)HystPreset.Normal; // preset
|
||||
WriteU16(p, 7, 30); WriteU16(p, 9, 50); WriteU16(p, 11, 300); WriteU16(p, 13, 700);
|
||||
WriteU16(p, 15, 0x0000); // errorCode
|
||||
|
||||
for (int r = 0; r < 4; r++)
|
||||
{
|
||||
int o = 17 + r * 14;
|
||||
int seed = tick + r * 13;
|
||||
p[o + 0] = (byte)((seed % 2) | (((seed % 3) == 0) ? 0x02 : 0)); // bit0 급기 / bit1 배기
|
||||
WriteU16(p, o + 1, 10 + (seed * 3) % 60);
|
||||
WriteU16(p, o + 3, 15 + (seed * 5) % 90);
|
||||
WriteU16(p, o + 5, 100 + (seed * 7) % 400);
|
||||
WriteU16(p, o + 7, 450 + (seed * 11) % 700);
|
||||
p[o + 9] = (byte)(1 + (seed % 4));
|
||||
p[o + 10] = (byte)(seed % 10);
|
||||
WriteU16(p, o + 11, (seed * 17) % 100);
|
||||
p[o + 13] = (byte)(seed % 5);
|
||||
}
|
||||
|
||||
p[73] = 0; // reset (토글 off)
|
||||
|
||||
// 풍량 VSP 설정값 (1바이트, 사양서 DL H-ERV VSP 실측표) : 환기1~4, 바이패스, 공청1~4 의 SA/EA
|
||||
int[] sa = { 56, 63, 70, 86, 67, 65, 72, 78, 80 };
|
||||
int[] ea = { 57, 63, 70, 85, 75, 0, 0, 0, 0 };
|
||||
for (int i = 0; i < 9; i++)
|
||||
{
|
||||
int o = 74 + i * 4;
|
||||
WriteU16(p, o, sa[i]);
|
||||
WriteU16(p, o + 2, ea[i]);
|
||||
}
|
||||
|
||||
// 히스테리시스 데드밴드(하강) (ECO/NORMAL/TURBO 의 PM2.5/PM10/VOC/CO2) - 사양서
|
||||
int[,] hyst = { { 2, 5, 5, 50 }, { 2, 5, 5, 50 }, { 2, 5, 3, 30 } };
|
||||
for (int i = 0; i < 3; i++)
|
||||
{
|
||||
int o = 110 + i * 8;
|
||||
WriteU16(p, o, hyst[i, 0]);
|
||||
WriteU16(p, o + 2, hyst[i, 1]);
|
||||
WriteU16(p, o + 4, hyst[i, 2]);
|
||||
WriteU16(p, o + 6, hyst[i, 3]);
|
||||
}
|
||||
|
||||
// 모드별 오염단계 임계표 (3프리셋 × [CO2,PM2.5,PM10,VOC] × L1~L4 상한) - 사양서
|
||||
int[][,] thr =
|
||||
{
|
||||
new int[,] { {1000,1300,1600,2000}, {20,38,60,86}, {40,86,126,173}, {171,195,308,438} }, // ECO
|
||||
new int[,] { {800,1100,1400,1700}, {14,29,49,69}, {28,66,102,138}, {120,150,250,350} }, // NORMAL
|
||||
new int[,] { {700,1000,1300,1600}, {12,23,38,52}, {24,53,78,104}, {103,120,192,263} }, // TURBO
|
||||
};
|
||||
for (int i = 0; i < 3; i++)
|
||||
{
|
||||
int o = StatusDecoder.THR_OFF + i * 32;
|
||||
for (int g = 0; g < 4; g++) // g: 0 CO2,1 PM2.5,2 PM10,3 VOC
|
||||
for (int k = 0; k < 4; k++)
|
||||
WriteU16(p, o + g * 8 + k * 2, thr[i][g, k]);
|
||||
}
|
||||
|
||||
// 각실 온도/습도 (offset 230~, 4실 × [Temp, Humi])
|
||||
for (int r = 0; r < 4; r++)
|
||||
{
|
||||
int o = StatusDecoder.TEMPHUMI_OFF + r * 2;
|
||||
p[o + 0] = (byte)(22 + (tick + r) % 6); // 22~27℃
|
||||
p[o + 1] = (byte)(40 + (tick + r * 7) % 30); // 40~69%
|
||||
}
|
||||
return p;
|
||||
}
|
||||
|
||||
static void WriteU16(byte[] p, int off, int v)
|
||||
{
|
||||
p[off] = (byte)((v >> 8) & 0xFF);
|
||||
p[off + 1] = (byte)(v & 0xFF);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -41,8 +41,5 @@ namespace ErvDashboard.Api
|
||||
void SetHystPreset(HystPreset preset); // 0x06
|
||||
void SetHystDeadband(int preset, int pm25, int pm10, int voc, int co2); // 0x07
|
||||
void SetHystThreshold(int preset, int pollutant, int l1, int l2, int l3, int l4); // 0x0D
|
||||
|
||||
// ---- 데모/테스트 (합성 STATUS를 수신 경로로 주입) ----
|
||||
void InjectDemoStatus(int tick);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -100,14 +100,6 @@ namespace ErvDashboard.Api
|
||||
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 InjectDemoStatus(int tick)
|
||||
{
|
||||
var frame = CtrlFrame.Build(StatusDecoder.STATUS, DemoStatus.BuildPayload(tick));
|
||||
_parser.Reset();
|
||||
_parser.Feed(frame);
|
||||
}
|
||||
|
||||
public void Dispose() => _ch.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,8 +22,6 @@ namespace ErvDashboard
|
||||
readonly DashboardState _state = new();
|
||||
readonly IErvApi _api = new SerialErvApi();
|
||||
|
||||
readonly DispatcherTimer _demoTimer;
|
||||
int _demoTick;
|
||||
bool _commActive;
|
||||
bool _ledDragging; // LED 슬라이더 thumb 드래그 중 (드래그 중엔 전송 보류 → 완료 시 1회)
|
||||
bool _suppressLed; // STATUS 동기 적용 중 LedDim→슬라이더 갱신으로 인한 ValueChanged 전송 차단
|
||||
@@ -61,7 +59,11 @@ namespace ErvDashboard
|
||||
readonly DispatcherTimer _clockTimer = new() { Interval = TimeSpan.FromSeconds(1) };
|
||||
|
||||
// ---- 그래프용 시계열 샘플링 (5초 간격) → SQLite 실시간 저장(무제한 누적) ----
|
||||
readonly Storage.LogDb _logDb = new(System.IO.Path.Combine(AppContext.BaseDirectory, "HERV_Log.db"));
|
||||
// 단일 exe(IncludeAllContentForSelfExtract=true)는 임시폴더로 추출 실행 → AppContext.BaseDirectory 가 임시폴더를 가리킴.
|
||||
// DB 는 실제 exe 위치(Environment.ProcessPath)에 저장해야 publish 폴더에서 보이고 재실행 간 누적 유지됨.
|
||||
static string LogDbPath => System.IO.Path.Combine(
|
||||
System.IO.Path.GetDirectoryName(Environment.ProcessPath) ?? AppContext.BaseDirectory, "HERV_Log.db");
|
||||
readonly Storage.LogDb _logDb = new(LogDbPath);
|
||||
readonly DispatcherTimer _sampleTimer = new() { Interval = TimeSpan.FromSeconds(5) };
|
||||
GraphWindow? _graphWin;
|
||||
|
||||
@@ -94,9 +96,6 @@ namespace ErvDashboard
|
||||
BuildModeButtons();
|
||||
BuildPresetButtons();
|
||||
|
||||
_demoTimer = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(700) };
|
||||
_demoTimer.Tick += (_, _) => DemoTick();
|
||||
|
||||
_clockTimer.Tick += (_, _) => CheckSleepSchedule();
|
||||
_clockTimer.Start();
|
||||
|
||||
@@ -163,7 +162,7 @@ namespace ErvDashboard
|
||||
{
|
||||
if (!_state.SmartSleep || _sleepEndAt is not { } endAt || DateTime.Now < endAt) return;
|
||||
_sleepEndAt = null;
|
||||
if (!_demoTimer.IsEnabled && CanSend()) _api.SetSubMode(SubModeType.SmartSleep, false);
|
||||
if (CanSend()) _api.SetSubMode(SubModeType.SmartSleep, false);
|
||||
ApplySubModeLocal("SmartSleep", false);
|
||||
Log("[스마트수면] 종료 시각 도달 → 자동 해제");
|
||||
if (NoScenarioActive()) RestorePreviousMode();
|
||||
@@ -219,7 +218,6 @@ namespace ErvDashboard
|
||||
// 팝업에서 호출 (제어 송신은 메인이 담당)
|
||||
public void SelectPreset(HystPreset preset)
|
||||
{
|
||||
if (_demoTimer.IsEnabled) { _state.HystPreset = preset; return; }
|
||||
if (!CanSend()) return;
|
||||
_api.SetHystPreset(preset);
|
||||
_state.HystPreset = preset;
|
||||
@@ -237,7 +235,6 @@ namespace ErvDashboard
|
||||
// 활성 프리셋의 오염단계 임계 + 데드밴드 송신 (HystWindow '변경')
|
||||
public void ApplyHystPreset(int preset)
|
||||
{
|
||||
if (_demoTimer.IsEnabled) return; // 데모: 상태만 갱신됨
|
||||
if (!CanSend()) return;
|
||||
_api.SetHystThreshold(preset, 0, _state.Co2Thr[preset][0], _state.Co2Thr[preset][1], _state.Co2Thr[preset][2], _state.Co2Thr[preset][3]);
|
||||
_api.SetHystThreshold(preset, 1, _state.Pm25Thr[preset][0], _state.Pm25Thr[preset][1], _state.Pm25Thr[preset][2], _state.Pm25Thr[preset][3]);
|
||||
@@ -321,7 +318,6 @@ namespace ErvDashboard
|
||||
|
||||
bool CanSend()
|
||||
{
|
||||
if (_demoTimer.IsEnabled) return false; // 데모 모드에선 송신 안 함
|
||||
if (!_api.IsConnected)
|
||||
{
|
||||
Log("연결 후 제어 가능합니다.");
|
||||
@@ -334,7 +330,6 @@ namespace ErvDashboard
|
||||
void Power_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
bool next = !_state.PowerOn;
|
||||
if (_demoTimer.IsEnabled) { _state.PowerOn = next; return; }
|
||||
if (!CanSend()) return;
|
||||
_api.SetPower(next);
|
||||
_state.PowerOn = next;
|
||||
@@ -346,7 +341,6 @@ namespace ErvDashboard
|
||||
if (sender is not Button b || b.Tag is not string tag) return;
|
||||
var def = Array.Find(ModeDefs, d => d.tag == tag);
|
||||
// 운전모드 전환 시 풍량 1단 (자동 제외). 실연결 시 ERV STATUS 로 최종 확정.
|
||||
if (_demoTimer.IsEnabled) { _state.RunMode = def.mode; if (def.mode != RunMode.Auto) _state.FanMode = 1; return; }
|
||||
if (!CanSend()) return;
|
||||
_api.SetRunMode(def.mode);
|
||||
_state.RunMode = def.mode;
|
||||
@@ -369,7 +363,6 @@ namespace ErvDashboard
|
||||
if (sender is not Button b || b.Tag is not int speed) return;
|
||||
if (_state.IsAuto) { Log("자동모드에서는 풍량 조절 불가"); return; }
|
||||
if (_state.RunMode == RunMode.Bypass && speed > 1) { Log("바이패스는 1단 고정"); return; }
|
||||
if (_demoTimer.IsEnabled) { _state.FanMode = (byte)speed; return; }
|
||||
if (!CanSend()) return;
|
||||
_api.SetFan(speed);
|
||||
_state.FanMode = (byte)speed;
|
||||
@@ -389,21 +382,7 @@ namespace ErvDashboard
|
||||
// 시나리오 첫 진입 → 직전 운전모드/풍량 기억(해제 시 복귀용)
|
||||
if (next && !anyBefore) { _modeBeforeScenario = _state.RunMode; _fanBeforeScenario = _state.FanMode; }
|
||||
if (tag == "SmartSleep" && !next) _sleepEndAt = null; // 스마트수면 수동 해제 → 자동해제 예약 취소
|
||||
if (_demoTimer.IsEnabled)
|
||||
{
|
||||
ApplySubModeLocal(tag, next);
|
||||
if (NoScenarioActive()) RestorePreviousMode();
|
||||
return;
|
||||
}
|
||||
if (!CanSend()) return;
|
||||
// 상호배타: 새 모드를 켤 때 기존 활성 모드는 장치에도 OFF 전송
|
||||
// (펌웨어는 시나리오모드를 독립 변수로 유지 → status 재수신 시 부활 방지)
|
||||
if (next)
|
||||
{
|
||||
if (tag != "SmartSleep" && _state.SmartSleep) _api.SetSubMode(SubModeType.SmartSleep, false);
|
||||
if (tag != "ComfortCook" && _state.ComfortCook) _api.SetSubMode(SubModeType.ComfortCook, false);
|
||||
if (tag != "ReliefRecover" && _state.ReliefRecover) _api.SetSubMode(SubModeType.ReliefRecover, false);
|
||||
}
|
||||
_api.SetSubMode(type, next);
|
||||
ApplySubModeLocal(tag, next);
|
||||
Log($"[제어] 시나리오모드 {b.Content} → {(next ? "ON" : "OFF")}");
|
||||
@@ -423,28 +402,18 @@ namespace ErvDashboard
|
||||
|
||||
void ApplySubModeLocal(string tag, bool on)
|
||||
{
|
||||
// 시나리오모드는 상호배타: 하나를 켜면 나머지는 해제
|
||||
if (on)
|
||||
// 시나리오모드 상호배타 없음 — 클릭한 모드만 on/off (사용자가 하나만 선택).
|
||||
switch (tag)
|
||||
{
|
||||
_state.SmartSleep = tag == "SmartSleep";
|
||||
_state.ComfortCook = tag == "ComfortCook";
|
||||
_state.ReliefRecover = tag == "ReliefRecover";
|
||||
}
|
||||
else
|
||||
{
|
||||
switch (tag)
|
||||
{
|
||||
case "SmartSleep": _state.SmartSleep = false; break;
|
||||
case "ComfortCook": _state.ComfortCook = false; break;
|
||||
case "ReliefRecover": _state.ReliefRecover = false; break;
|
||||
}
|
||||
case "SmartSleep": _state.SmartSleep = on; break;
|
||||
case "ComfortCook": _state.ComfortCook = on; break;
|
||||
case "ReliefRecover": _state.ReliefRecover = on; break;
|
||||
}
|
||||
}
|
||||
|
||||
void Hood_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
bool next = !_state.Hood;
|
||||
if (_demoTimer.IsEnabled) { _state.Hood = next; return; }
|
||||
if (!CanSend()) return;
|
||||
_api.SetHood(next);
|
||||
_state.Hood = next;
|
||||
@@ -454,7 +423,6 @@ namespace ErvDashboard
|
||||
void Reset_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
bool next = !_state.Reset;
|
||||
if (_demoTimer.IsEnabled) { _state.Reset = next; return; }
|
||||
if (!CanSend()) return;
|
||||
_api.SetReset(next);
|
||||
_state.Reset = next;
|
||||
@@ -474,11 +442,6 @@ namespace ErvDashboard
|
||||
var room = _state.Room(roomId);
|
||||
bool cur = type == 0 ? room.DamperSaOpen : room.DamperEaOpen;
|
||||
bool next = !cur;
|
||||
if (_demoTimer.IsEnabled)
|
||||
{
|
||||
if (type == 0) room.DamperSaOpen = next; else room.DamperEaOpen = next;
|
||||
return;
|
||||
}
|
||||
if (!CanSend()) return;
|
||||
_api.SetDiffuserDamper(roomId, type, next);
|
||||
if (type == 0) room.DamperSaOpen = next; else room.DamperEaOpen = next;
|
||||
@@ -511,7 +474,6 @@ namespace ErvDashboard
|
||||
// 직전 송신/수신값과 같으면 전송 안 함 — STATUS 역갱신이 유발한 ValueChanged(에코) 차단.
|
||||
// 사용자가 값을 실제로 바꾸면 dim 이 달라지므로 정상 전송됨.
|
||||
if (_lastLed.TryGetValue(roomId, out var last) && last == dim) return;
|
||||
if (_demoTimer.IsEnabled) return;
|
||||
if (!CanSend()) return;
|
||||
_api.SetDiffuserLed(roomId, dim);
|
||||
_lastLed[roomId] = dim;
|
||||
@@ -525,18 +487,11 @@ namespace ErvDashboard
|
||||
if (!IsLoaded || _suppressReserve) return;
|
||||
if (ReserveCombo.SelectedIndex < 0) return;
|
||||
int hours = ReserveCombo.SelectedIndex; // 0=해제, 1~8시간
|
||||
if (_demoTimer.IsEnabled) { _state.ReserveRemainSec = hours * 3600; return; }
|
||||
if (!CanSend()) return;
|
||||
_api.SetReserve(hours);
|
||||
Log(hours == 0 ? "[제어] 예약 해제" : $"[제어] {hours}시간 후 꺼짐 예약");
|
||||
}
|
||||
|
||||
// ================= 데모 모드 (버튼 제거됨 — 내부 합성 STATUS 경로는 비활성 상태로 유지) =================
|
||||
void DemoTick()
|
||||
{
|
||||
_api.InjectDemoStatus(_demoTick++);
|
||||
}
|
||||
|
||||
// ================= UI 갱신 =================
|
||||
void RefreshControls()
|
||||
{
|
||||
@@ -564,15 +519,19 @@ namespace ErvDashboard
|
||||
// - 자동 : 수동 조절 불가(전 단 비활성)
|
||||
// - 바이패스 : 최대 1단(2~4단 비활성)
|
||||
// - 환기/공청 : 0~4단
|
||||
// 시나리오모드 활성 시: 운전모드·풍량·선택 안 된 시나리오모드 비활성화
|
||||
// 쾌적조리는 '연동운전중(HoodRunning=후드 가동중)' 기준으로 시나리오 활성 판단.
|
||||
// 후드 OFF(대기 상태)면 ERV는 본래 운전모드로 복귀하므로 운전모드를 다시 활성화해야 함(사양 3.1).
|
||||
bool subActive = _state.SmartSleep || _state.HoodRunning || _state.ReliefRecover;
|
||||
// 시나리오모드 활성 시: 운전모드·풍량·선택 안 된 시나리오모드 비활성화(=클릭 불가)
|
||||
// 쾌적조리 잠금은 '메이크업 실제 연동중'(쾌적조리 토글 ON + 후드 가동) 일 때만.
|
||||
// - 펌웨어 Hood_process() 는 Hood_YeunDong_Enable==1 일 때만 모드/풍량을 건드림.
|
||||
// 쾌적조리 OFF면 후드만 켜져도 ERV는 그대로 → 다른 시나리오모드/운전모드 선택 가능해야 함.
|
||||
// - 후드 OFF(대기)면 메이크업 비연동 → 본래 운전모드로 복귀하므로 잠금 해제(사양 3.1).
|
||||
bool makeupActive = _state.ComfortCook && _state.HoodRunning;
|
||||
bool subActive = _state.SmartSleep || makeupActive || _state.ReliefRecover;
|
||||
int fanMax = _state.RunMode == RunMode.Bypass ? 1 : 4;
|
||||
foreach (var fb in _fanButtons)
|
||||
{
|
||||
int sp = (int)fb.Tag!;
|
||||
fb.IsEnabled = !subActive && !_state.IsAuto && sp <= fanMax;
|
||||
// 전원 OFF면 풍량 조절 불가 → 0 포함 전 단 비활성
|
||||
fb.IsEnabled = _state.PowerOn && !subActive && !_state.IsAuto && sp <= fanMax;
|
||||
SetActive(fb, sp == _state.FanMode);
|
||||
}
|
||||
|
||||
@@ -582,11 +541,12 @@ namespace ErvDashboard
|
||||
// (HoodRunning 으로 강조하면 대기/Roll-back 상태에서 버튼이 꺼져 보여 재선택 시 토글이 반대로 먹던 문제 수정)
|
||||
SetActive(ComfortCookBtn, _state.ComfortCook);
|
||||
SetActive(ReliefRecoverBtn, _state.ReliefRecover);
|
||||
// (활성 모드 버튼은 OFF 토글 가능해야 하므로 자기 자신은 유지)
|
||||
SmartSleepBtn.IsEnabled = !subActive || _state.SmartSleep;
|
||||
// 쾌적조리는 사양 9p 3.1 의 '독립 토글(연동 스위치)' — 후드 연결/가동·다른 시나리오와 무관하게 항상 토글 가능.
|
||||
// 시나리오모드 버튼은 항상 선택 가능(클릭 가능). 다른 시나리오가 켜져 있어도 비활성화하지 않음.
|
||||
// - 스마트수면 ↔ 안심회복은 펌웨어상 단일 Ext_Run_Mode 라 동시 불가 → 클릭 시 SubMode_Click 이 서로 전환.
|
||||
// - 쾌적조리는 별도 변수(Hood_YeunDong_Enable)라 독립 토글.
|
||||
SmartSleepBtn.IsEnabled = true;
|
||||
ComfortCookBtn.IsEnabled = true;
|
||||
ReliefRecoverBtn.IsEnabled = !subActive || _state.ReliefRecover;
|
||||
ReliefRecoverBtn.IsEnabled = true;
|
||||
// 스마트수면 시간설정 버튼 : 스마트수면 ON 일 때만 활성
|
||||
SmartSleepSetBtn.IsEnabled = _state.SmartSleep;
|
||||
foreach (var mb in _modeButtons) mb.IsEnabled = !subActive;
|
||||
@@ -676,7 +636,6 @@ namespace ErvDashboard
|
||||
|
||||
protected override void OnClosed(EventArgs e)
|
||||
{
|
||||
_demoTimer.Stop();
|
||||
_api.Dispose();
|
||||
_logDb.Dispose();
|
||||
base.OnClosed(e);
|
||||
|
||||
Reference in New Issue
Block a user