320	void roomcon_parsing(void)
321	{
322		uint8_t i = 0;
323
324	    RJ_Vsp_Mode = Rx_roomcon232_buffer[1];
325		switch(Rx_roomcon232_buffer[1])
326		{
327			case RX_DATA_MODE_NORMAL:
328
329				Err_Code &= ~(ERROR_FILTER_CLEAN|ERROR_FILTER_CHANGE|ERROR_SOJA_CHANGE|ERROR_PROTECT|ERROR_SOMETIME);
330				Err_Code |= Rx_roomcon232_buffer[7]&(ERROR_FILTER_CLEAN|ERROR_FILTER_CHANGE|ERROR_SOJA_CHANGE|ERROR_PROTECT|ERROR_SOMETIME);
331
332				if(Command_request_type & (TYPE_MODE|TYPE_FAN_SPEED|TYPE_RESERVATION) )
333				{
334	                if((Hood_Yeundong_flag == 1)&&(Tx_Yeundong_Delay == 0))
335					{
336						Hood_Warming_up_Timer = 0;/////////////////
337						Hood_Yeundong_flag = 0;
338						Command_request_type |= (TYPE_HOOD_STATE);
339					}
340
341					Tx_roomcon232_buffer[0] = 0xAA;
342					Tx_roomcon232_buffer[1] = COMMAND_CONTROLL;
343
344					Tx_roomcon232_buffer[2] = Set_Run_Mode;
345					Tx_roomcon232_buffer[3] = Set_Fan_Mode;
346					Tx_roomcon232_buffer[4] = Auto_Mode;
347
348	                Tx_roomcon232_buffer[5] = (Heater_Enable|(UV_Enable<<4)|Kijer_Enable);
349					Tx_roomcon232_buffer[6] = 0;
350					Tx_roomcon232_buffer[7] = 0;
351					Tx_roomcon232_buffer[8] = 0;
352					Tx_roomcon232_buffer[9] = 0;
353					if(Command_request_type & TYPE_RESERVATION)
354					{
355						Tx_roomcon232_buffer[10] = 1;
356						Tx_roomcon232_buffer[11] = (uint8_t)(Set_Reserve_timer_sec/3600); //// extern setting timer
357						Tx_roomcon232_buffer[12] = (uint8_t)(Set_Reserve_timer_sec%3600/60); ////extern setting timer
358					}
359					else
360					{
361						Tx_roomcon232_buffer[10] = 0;
362						Tx_roomcon232_buffer[11] = 0;
363						Tx_roomcon232_buffer[12] = 0;
364					}
365
366
367					Tx_roomcon232_buffer[13] = 0;
368					for(i=0; i<13; i++)Tx_roomcon232_buffer[13] ^= Tx_roomcon232_buffer[i];
369					Tx_roomcon232_buffer[14] = 0xEE;
370
371	                Command_request_type |= TYPE_SEND_FLAG;
372
373				}
374	            else if(Command_request_type & (TYPE_HOOD_STATE))// add 2022.1.25
375				{
376					Hood_info_command();
377				}
378				else
379				{
380	                Set_Reserve_timer_sec = (uint32_t)Rx_roomcon232_buffer[11]*3600 + (uint32_t)Rx_roomcon232_buffer[12]*60 ;
381					Txbuff_init();
382				}
383				SCUART_Write(SC0,Tx_roomcon232_buffer,15);
384
385				break;
386
387			case RX_DATA_MODE_EVENT:
388
389				Run_Mode = Rx_roomcon232_buffer[2];
390	            if(Run_Mode != MODE_AUTO)
391	            {
392	                Fan_Mode = Rx_roomcon232_buffer[3];
393	            }
394
395				if(Fan_Mode != 0)Err_Code &= ~(ERROR_EA_FAN|ERROR_SA_FAN);
396
397				if(Command_request_type & TYPE_SEND_FLAG)
398	            {
399	                    Command_request_type = 0;
400	                    Command_request_type &= ~TYPE_SEND_FLAG;
401	                    Run_Mode = Rx_roomcon232_buffer[2];
402	                    if(Run_Mode != MODE_AUTO)
403	                    {
404	                        Fan_Mode = Rx_roomcon232_buffer[3];//DEMO
405	                    }
406	                    //if(Run_Mode != MODE_AUTO)Fan_Mode = Rx_roomcon232_buffer[3];//DEMO
407	                    //else Fan_Mode = Set_Fan_Mode;
408	            }
409				else

void roomcon_parsing(void)
{
	uint8_t i = 0;

    RJ_Vsp_Mode = Rx_roomcon232_buffer[1];
	switch(Rx_roomcon232_buffer[1])
	{
		case RX_DATA_MODE_NORMAL:

			Err_Code &= ~(ERROR_FILTER_CLEAN|ERROR_FILTER_CHANGE|ERROR_SOJA_CHANGE|ERROR_PROTECT|ERROR_SOMETIME);
			Err_Code |= Rx_roomcon232_buffer[7]&(ERROR_FILTER_CLEAN|ERROR_FILTER_CHANGE|ERROR_SOJA_CHANGE|ERROR_PROTECT|ERROR_SOMETIME);

			if(Command_request_type & (TYPE_MODE|TYPE_FAN_SPEED|TYPE_RESERVATION) )
			{
                if((Hood_Yeundong_flag == 1)&&(Tx_Yeundong_Delay == 0))
				{
					Hood_Warming_up_Timer = 0;/////////////////
					Hood_Yeundong_flag = 0;
					Command_request_type |= (TYPE_HOOD_STATE);
				}

				Tx_roomcon232_buffer[0] = 0xAA;
				Tx_roomcon232_buffer[1] = COMMAND_CONTROLL;

				Tx_roomcon232_buffer[2] = Set_Run_Mode;
				Tx_roomcon232_buffer[3] = Set_Fan_Mode;
				Tx_roomcon232_buffer[4] = Auto_Mode;

                Tx_roomcon232_buffer[5] = (Heater_Enable|(UV_Enable<<4)|Kijer_Enable);
				Tx_roomcon232_buffer[6] = 0;
				Tx_roomcon232_buffer[7] = 0;
				Tx_roomcon232_buffer[8] = 0;
				Tx_roomcon232_buffer[9] = 0;
				if(Command_request_type & TYPE_RESERVATION)
				{
					Tx_roomcon232_buffer[10] = 1;
					Tx_roomcon232_buffer[11] = (uint8_t)(Set_Reserve_timer_sec/3600); //// extern setting timer
					Tx_roomcon232_buffer[12] = (uint8_t)(Set_Reserve_timer_sec%3600/60); ////extern setting timer
				}
				else
				{
					Tx_roomcon232_buffer[10] = 0;
					Tx_roomcon232_buffer[11] = 0;
					Tx_roomcon232_buffer[12] = 0;
				}

				Tx_roomcon232_buffer[13] = 0;
				for(i=0; i<13; i++)Tx_roomcon232_buffer[13] ^= Tx_roomcon232_buffer[i];
				Tx_roomcon232_buffer[14] = 0xEE;

                Command_request_type |= TYPE_SEND_FLAG;

			}
            else if(Command_request_type & (TYPE_HOOD_STATE))// add 2022.1.25
			{
				Hood_info_command();
			}
			else
			{
                Set_Reserve_timer_sec = (uint32_t)Rx_roomcon232_buffer[11]*3600 + (uint32_t)Rx_roomcon232_buffer[12]*60 ;
				Txbuff_init();
			}
			SCUART_Write(SC0,Tx_roomcon232_buffer,15);

			break;

		case RX_DATA_MODE_EVENT:

			Run_Mode = Rx_roomcon232_buffer[2];
            if(Run_Mode != MODE_AUTO)
            {
                Fan_Mode = Rx_roomcon232_buffer[3];
            }

			if(Fan_Mode != 0)Err_Code &= ~(ERROR_EA_FAN|ERROR_SA_FAN);

			if(Command_request_type & TYPE_SEND_FLAG)
            {
                    Command_request_type = 0;
                    Command_request_type &= ~TYPE_SEND_FLAG;
                    Run_Mode = Rx_roomcon232_buffer[2];
                    if(Run_Mode != MODE_AUTO)
                    {
                        Fan_Mode = Rx_roomcon232_buffer[3];//DEMO
                    }
                    //if(Run_Mode != MODE_AUTO)Fan_Mode = Rx_roomcon232_buffer[3];//DEMO
                    //else Fan_Mode = Set_Fan_Mode;
            }
			else


핵심 단서를 찾았습니다. 전원 ON 핸들러는 `Set_Run_Mode = Run_Mode = VENTILATION; Set_Fan_Mode = Fan_Mode = 1;` 로 **Set_* 와 현재값을 같게** 설정합니다. 그런데 룸컨 푸시 조건(332)은 `TYPE_MODE|TYPE_FAN_SPEED|TYPE_RESERVATION`만 보고 **TYPE_POWER는 안 봅니다.** 그리고 715~716에서 `Set==현재`면 TYPE_MODE/FAN을 지웁니다. 이 클리어가 푸시보다 먼저 돌면 룸컨 명령이 안 나갑니다 — 전형적 간헐 레이스. 715~760을 확인하겠습니다.


700					Set_Run_Mode = Run_Mode = Rx_roomcon232_buffer[2];
701	                if(Run_Mode != MODE_AUTO)
702	                {
703	                    Set_Fan_Mode = Fan_Mode = Rx_roomcon232_buffer[3];
704	                }
705				}
706				//Auto_Mode = Rx_roomcon232_buffer[4];
707				Heater_OnOff = Rx_roomcon232_buffer[5]&0x01;
708				UV_OnOff = Rx_roomcon232_buffer[5]&0x10;
709
710				Filter_Reset_Flag |= Rx_roomcon232_buffer[7];
711
712				Reserve_timer_sec = (uint32_t)Rx_roomcon232_buffer[11]*3600 + (uint32_t)Rx_roomcon232_buffer[12]*60 ;
713
714
715				if(Set_Run_Mode == Run_Mode)Command_request_type &= ~TYPE_MODE;
716				if(Set_Fan_Mode == Fan_Mode)Command_request_type &= ~TYPE_FAN_SPEED;
717
718				if(Reserve_timer_sec == 0){if(Set_Reserve_timer_sec == 0)Command_request_type &= ~TYPE_RESERVATION;}
719				else
720				{
721					if(Set_Reserve_timer_sec != 0)
722					{
723						if(((Reserve_timer_sec-1)/3600 + 1) == ((Set_Reserve_timer_sec-1)/3600 + 1))Command_request_type &= ~TYPE_RESERVATION;
724					}
725				}
726				Txbuff_init();
727				SCUART_Write(SC0,Tx_roomcon232_buffer,15);
728				break;
729
730			case RX_DATA_CONTROLL_INFO: // 룸콘
731				Spec_info_reply();
732				SCUART_Write(SC0,Tx_roomcon232_buffer,15);
733				break;
734			case RX_DATA_RPM_INFO: //2021.5.31
735
736	          	if(Rx_roomcon232_buffer[2] == 1)Roomcon_Filter_Error |= ERROR_FILTER_CLEAN;
737				else Roomcon_Filter_Error &= ~ERROR_FILTER_CLEAN;
738
739				if(Rx_roomcon232_buffer[3] == 1)Roomcon_Filter_Error |= ERROR_FILTER_CHANGE;
740				else Roomcon_Filter_Error &= ~ERROR_FILTER_CHANGE;
741
742				if(Rx_roomcon232_buffer[4] == 1)Roomcon_Filter_Error |= ERROR_SOJA_CHANGE;
743				else Roomcon_Filter_Error &= ~ERROR_SOJA_CHANGE;
744
745	            Rpm_info_reply();
746				SCUART_Write(SC0,Tx_roomcon232_buffer,15);
747				break;
748			case RX_DATA_ERROR_CLEAR: //2021.5.31
749
750	            if(Rx_roomcon232_buffer[1] == 0x22)
751	            {
752	                Err_Code &= ~(ERROR_SA_FAN|ERROR_EA_FAN);
753	            }
754				Txbuff_init();
755				SCUART_Write(SC0,Tx_roomcon232_buffer,15);
756				break;
757	        case RX_DATA_HOOD_INFO: // add 2022.1.25
758				Command_request_type &= ~(TYPE_HOOD_STATE);
759				Txbuff_init();
760				SCUART_Write(SC0,Tx_roomcon232_buffer,15);
761				break;
762			default:
763	          break;
764

				Set_Run_Mode = Run_Mode = Rx_roomcon232_buffer[2];
                if(Run_Mode != MODE_AUTO)
                {
                    Set_Fan_Mode = Fan_Mode = Rx_roomcon232_buffer[3];
                }
			}
			//Auto_Mode = Rx_roomcon232_buffer[4];
			Heater_OnOff = Rx_roomcon232_buffer[5]&0x01;
			UV_OnOff = Rx_roomcon232_buffer[5]&0x10;

			Filter_Reset_Flag |= Rx_roomcon232_buffer[7];

			Reserve_timer_sec = (uint32_t)Rx_roomcon232_buffer[11]*3600 + (uint32_t)Rx_roomcon232_buffer[12]*60 ;

			if(Set_Run_Mode == Run_Mode)Command_request_type &= ~TYPE_MODE;
			if(Set_Fan_Mode == Fan_Mode)Command_request_type &= ~TYPE_FAN_SPEED;

			if(Reserve_timer_sec == 0){if(Set_Reserve_timer_sec == 0)Command_request_type &= ~TYPE_RESERVATION;}
			else
			{
				if(Set_Reserve_timer_sec != 0)
				{
					if(((Reserve_timer_sec-1)/3600 + 1) == ((Set_Reserve_timer_sec-1)/3600 + 1))Command_request_type &= ~TYPE_RESERVATION;
				}
			}
			Txbuff_init();
			SCUART_Write(SC0,Tx_roomcon232_buffer,15);
			break;

		case RX_DATA_CONTROLL_INFO: // 룸콘
			Spec_info_reply();
			SCUART_Write(SC0,Tx_roomcon232_buffer,15);
			break;
		case RX_DATA_RPM_INFO: //2021.5.31

          	if(Rx_roomcon232_buffer[2] == 1)Roomcon_Filter_Error |= ERROR_FILTER_CLEAN;
			else Roomcon_Filter_Error &= ~ERROR_FILTER_CLEAN;

			if(Rx_roomcon232_buffer[3] == 1)Roomcon_Filter_Error |= ERROR_FILTER_CHANGE;
			else Roomcon_Filter_Error &= ~ERROR_FILTER_CHANGE;

			if(Rx_roomcon232_buffer[4] == 1)Roomcon_Filter_Error |= ERROR_SOJA_CHANGE;
			else Roomcon_Filter_Error &= ~ERROR_SOJA_CHANGE;

            Rpm_info_reply();
			SCUART_Write(SC0,Tx_roomcon232_buffer,15);
			break;
		case RX_DATA_ERROR_CLEAR: //2021.5.31

            if(Rx_roomcon232_buffer[1] == 0x22)
            {
                Err_Code &= ~(ERROR_SA_FAN|ERROR_EA_FAN);
            }
			Txbuff_init();
			SCUART_Write(SC0,Tx_roomcon232_buffer,15);
			break;
        case RX_DATA_HOOD_INFO: // add 2022.1.25
			Command_request_type &= ~(TYPE_HOOD_STATE);
			Txbuff_init();
			SCUART_Write(SC0,Tx_roomcon232_buffer,15);
			break;
		default:
          break;


715~716 클리어가 어느 RX 케이스에 있는지, 그리고 NORMAL 푸시와의 순서를 확정하기 위해 660~705를 읽겠습니다.


408	            }
409				else
410				{
411
412	                    Set_Run_Mode = Run_Mode = Rx_roomcon232_buffer[2];
413	                    Set_Fan_Mode = Fan_Mode = Rx_roomcon232_buffer[3];
414
415	                    if((Fan_Mode == 0)&&(Run_Mode == MODE_VENTILATION)&&(RJ_Vsp_Mode != RX_DATA_MODE_VSP))
416	                    {
417	                        if((Memory_Hood_Status != Hood_Status)&&(Hood_YeunDong_Enable == 1))
418	                        {
419	                            if(Memory_Hood_Status == 0)
420	                            {
421	                                Hood_Power_On = 0;
422	                                Hood_Fan_Mode = 0;
423	                                Hood_Control = 0x80;
424	                            }
425	                            else
426	                            {
427	                                Hood_Power_On = 1;
428	                                Hood_Fan_Mode = Memory_Hood_Status;
429	                                Hood_Control = 0x80;
430	                            }
431
432	                            Hood_Status = Memory_Hood_Status;
433	                        }
434
435	                        Set_Run_Mode = My_Memory_Run_Mode = 0;
436	                        Set_Fan_Mode = My_Memory_Fan_Mode = 0;
437	                        Command_request_type |= (TYPE_MODE|TYPE_FAN_SPEED);
438
439	                        Force_Damper_Mode = 0;
440	                        Ext_Run_Mode = 0;
441	                        Ext_Run_Mode_Off_Delay = 0;
442	                        Pre_Ext_Run_Mode = Ext_Run_Mode ;
443	                        Pre_Ext_Select_Room = Ext_Select_Room;
444
445	                        HREV_Hood_Control = 0;
446	                    }
447
448	                    if(Hood_Yeundong_flag == 1)
449	                    {
450	                        Hood_Yeundong_flag = 0;
451	                        Command_request_type |= (TYPE_HOOD_STATE);
452	                    }
453				}
454
455
456				//Auto_Mode = Rx_roomcon232_buffer[4];
457				Heater_OnOff = Rx_roomcon232_buffer[5]&0x01;
458				UV_OnOff = Rx_roomcon232_buffer[5]&0x10;
459
460				Filter_Reset_Flag |= Rx_roomcon232_buffer[7]&0x01;
461
462				if(Rx_roomcon232_buffer[10] == 1)
463				{
464					Set_Reserve_timer_sec = (uint32_t)Rx_roomcon232_buffer[11]*3600 + (uint32_t)Rx_roomcon232_buffer[12]*60 ;
465				}
466
467	            if(Run_Mode == MODE_VENTILATION)

            }
			else
			{

                    Set_Run_Mode = Run_Mode = Rx_roomcon232_buffer[2];
                    Set_Fan_Mode = Fan_Mode = Rx_roomcon232_buffer[3];

                    if((Fan_Mode == 0)&&(Run_Mode == MODE_VENTILATION)&&(RJ_Vsp_Mode != RX_DATA_MODE_VSP))
                    {
                        if((Memory_Hood_Status != Hood_Status)&&(Hood_YeunDong_Enable == 1))
                        {
                            if(Memory_Hood_Status == 0)
                            {
                                Hood_Power_On = 0;
                                Hood_Fan_Mode = 0;
                                Hood_Control = 0x80;
                            }
                            else
                            {
                                Hood_Power_On = 1;
                                Hood_Fan_Mode = Memory_Hood_Status;
                                Hood_Control = 0x80;
                            }

                            Hood_Status = Memory_Hood_Status;
                        }

                        Set_Run_Mode = My_Memory_Run_Mode = 0;
                        Set_Fan_Mode = My_Memory_Fan_Mode = 0;
                        Command_request_type |= (TYPE_MODE|TYPE_FAN_SPEED);

                        Force_Damper_Mode = 0;
                        Ext_Run_Mode = 0;
                        Ext_Run_Mode_Off_Delay = 0;
                        Pre_Ext_Run_Mode = Ext_Run_Mode ;
                        Pre_Ext_Select_Room = Ext_Select_Room;

                        HREV_Hood_Control = 0;
                    }

                    if(Hood_Yeundong_flag == 1)
                    {
                        Hood_Yeundong_flag = 0;
                        Command_request_type |= (TYPE_HOOD_STATE);
                    }
			}

			//Auto_Mode = Rx_roomcon232_buffer[4];
			Heater_OnOff = Rx_roomcon232_buffer[5]&0x01;
			UV_OnOff = Rx_roomcon232_buffer[5]&0x10;

			Filter_Reset_Flag |= Rx_roomcon232_buffer[7]&0x01;

			if(Rx_roomcon232_buffer[10] == 1)
			{
				Set_Reserve_timer_sec = (uint32_t)Rx_roomcon232_buffer[11]*3600 + (uint32_t)Rx_roomcon232_buffer[12]*60 ;
			}

            if(Run_Mode == MODE_VENTILATION)


my_rj2.c 수정금지는 해지..필요하면 수정해.


대시보드에서 전원 on을 하면 erv에 연결된 룸컨이 간헐적으로 안켜져. erv 펌웨어 좀 살펴봐


수정금지 해지 확인했습니다 — 필요하면 My_RJ2.c도 고치겠습니다. 715~716이 속한 케이스를 정확히 파악하기 위해 660~700을 읽겠습니다.


655				Tx_roomcon232_buffer[0] = 0xAA;
656				Tx_roomcon232_buffer[1] = 0x04;
657				Tx_roomcon232_buffer[2] = 0;
658				Tx_roomcon232_buffer[3] = 0;
659				Tx_roomcon232_buffer[4] = 0;
660				Tx_roomcon232_buffer[5] = 0;
661				Tx_roomcon232_buffer[6] = 0;
662				Tx_roomcon232_buffer[7] = 0;
663				Tx_roomcon232_buffer[8] = 0;
664				Tx_roomcon232_buffer[9] = 0;
665				Tx_roomcon232_buffer[10] = 0;
666				Tx_roomcon232_buffer[11] = 0;
667				Tx_roomcon232_buffer[12] = 0;
668
669				Tx_roomcon232_buffer[13] = 0;
670				for(i=0; i<13; i++)Tx_roomcon232_buffer[13] ^= Tx_roomcon232_buffer[i];
671				Tx_roomcon232_buffer[14] = 0xEE;
672
673				SCUART_Write(SC0,Tx_roomcon232_buffer,15);
674
675				break;
676			case RX_DATA_SENSOR_INFO: //
677				Command_request_type &= ~(TYPE_SENSOR_INFO);
678				Txbuff_init();
679				SCUART_Write(SC0,Tx_roomcon232_buffer,15);
680				break;
681
682			case RX_DATA_FILTER_INFO: //룸콘
683				Command_request_type &= ~(TYPE_FILTER_INFO);
684				Filter_info_reply();
685				SCUART_Write(SC0,Tx_roomcon232_buffer,15);
686				break;
687
688			case RX_DATA_CONTROLL: //
689				if(Command_request_type & TYPE_SEND_FLAG)
690	            {
691	                    Command_request_type &= ~TYPE_SEND_FLAG;
692	                    Run_Mode = Rx_roomcon232_buffer[2];
693	                    if(Run_Mode != MODE_AUTO)
694	                    {
695	                        Fan_Mode = Rx_roomcon232_buffer[3];///////////////// DEMO
696	                    }
697	            }
698				else
699				{
700					Set_Run_Mode = Run_Mode = Rx_roomcon232_buffer[2];
701	                if(Run_Mode != MODE_AUTO)
702	                {

			Tx_roomcon232_buffer[0] = 0xAA;
			Tx_roomcon232_buffer[1] = 0x04;
			Tx_roomcon232_buffer[2] = 0;
			Tx_roomcon232_buffer[3] = 0;
			Tx_roomcon232_buffer[4] = 0;
			Tx_roomcon232_buffer[5] = 0;
			Tx_roomcon232_buffer[6] = 0;
			Tx_roomcon232_buffer[7] = 0;
			Tx_roomcon232_buffer[8] = 0;
			Tx_roomcon232_buffer[9] = 0;
			Tx_roomcon232_buffer[10] = 0;
			Tx_roomcon232_buffer[11] = 0;
			Tx_roomcon232_buffer[12] = 0;

			Tx_roomcon232_buffer[13] = 0;
			for(i=0; i<13; i++)Tx_roomcon232_buffer[13] ^= Tx_roomcon232_buffer[i];
			Tx_roomcon232_buffer[14] = 0xEE;

			SCUART_Write(SC0,Tx_roomcon232_buffer,15);

			break;
		case RX_DATA_SENSOR_INFO: //
			Command_request_type &= ~(TYPE_SENSOR_INFO);
			Txbuff_init();
			SCUART_Write(SC0,Tx_roomcon232_buffer,15);
			break;

		case RX_DATA_FILTER_INFO: //룸콘
			Command_request_type &= ~(TYPE_FILTER_INFO);
			Filter_info_reply();
			SCUART_Write(SC0,Tx_roomcon232_buffer,15);
			break;

		case RX_DATA_CONTROLL: //
			if(Command_request_type & TYPE_SEND_FLAG)
            {
                    Command_request_type &= ~TYPE_SEND_FLAG;
                    Run_Mode = Rx_roomcon232_buffer[2];
                    if(Run_Mode != MODE_AUTO)
                    {
                        Fan_Mode = Rx_roomcon232_buffer[3];///////////////// DEMO
                    }
            }
			else
			{
				Set_Run_Mode = Run_Mode = Rx_roomcon232_buffer[2];
                if(Run_Mode != MODE_AUTO)
                {


핵심 레이스를 특정했습니다. 정리하면:

**원인 (간헐적 전원 ON 실패):**
1. CTRL_POWER ON 핸들러가 `Set_Fan_Mode = Fan_Mode = 1`로 **Set과 현재값을 같게** 만듦.
2. 룸컨 푸시(COMMAND_CONTROLL)는 `RX_DATA_MODE_NORMAL`에서 `TYPE_MODE|TYPE_FAN_SPEED` 플래그를 보고 나감.
3. 그런데 그 전에 `RX_DATA_CONTROLL`(688)의 else(700)가 먼저 도착하면 `Set_Fan_Mode = Fan_Mode = Rx[3]`로 **룸컨의 현재(OFF=0) 상태로 Set을 덮어버리고**, 715~716이 `Set==현재`라 `TYPE_FAN_SPEED`를 **조기 클리어** → 푸시가 영영 안 나감 → 룸컨 안 켜짐. 도착 순서에 따라 간헐적.

확정 전에 같은 패턴이 더 있는지, 정확한 텍스트를 확인하겠습니다.


Grep

Set_Fan_Mode = Fan_Mode = Rx|Set_Run_Mode = Run_Mode = Rx


412:                    Set_Run_Mode = Run_Mode = Rx_roomcon232_buffer[2];
413:                    Set_Fan_Mode = Fan_Mode = Rx_roomcon232_buffer[3];
700:				Set_Run_Mode = Run_Mode = Rx_roomcon232_buffer[2];
703:                    Set_Fan_Mode = Fan_Mode = Rx_roomcon232_buffer[3];


두 곳(EVENT else 409, CONTROLL else 698)에서 같은 클로버가 발생합니다. 정확한 공백(탭/스페이스)으로 안전히 편집하기 위해 두 구간을 좁게 다시 읽겠습니다.


407	                    //else Fan_Mode = Set_Fan_Mode;
408	            }
409				else
410				{
411
412	                    Set_Run_Mode = Run_Mode = Rx_roomcon232_buffer[2];
413	                    Set_Fan_Mode = Fan_Mode = Rx_roomcon232_buffer[3];
414
415	                    if((Fan_Mode == 0)&&(Run_Mode == MODE_VENTILATION)&&(RJ_Vsp_Mode != RX_DATA_MODE_VSP))
416	                    {

                    //else Fan_Mode = Set_Fan_Mode;
            }
			else
			{

                    Set_Run_Mode = Run_Mode = Rx_roomcon232_buffer[2];
                    Set_Fan_Mode = Fan_Mode = Rx_roomcon232_buffer[3];

                    if((Fan_Mode == 0)&&(Run_Mode == MODE_VENTILATION)&&(RJ_Vsp_Mode != RX_DATA_MODE_VSP))
                    {


697	            }
698				else
699				{
700					Set_Run_Mode = Run_Mode = Rx_roomcon232_buffer[2];
701	                if(Run_Mode != MODE_AUTO)
702	                {
703	                    Set_Fan_Mode = Fan_Mode = Rx_roomcon232_buffer[3];
704	                }
705				}

            }
			else
			{
				Set_Run_Mode = Run_Mode = Rx_roomcon232_buffer[2];
                if(Run_Mode != MODE_AUTO)
                {
                    Set_Fan_Mode = Fan_Mode = Rx_roomcon232_buffer[3];
                }
			}


원인이 명확합니다. 정리:

**간헐적 전원 ON 실패 = 룸컨 echo와 푸시의 레이스**
- 핸들러가 `Set_Fan_Mode = Fan_Mode = 1`로 Set==현재로 만듦.
- 푸시(NORMAL) 전에 룸컨 상태 echo(`RX_DATA_CONTROLL` else)가 오면 `Set_Fan = Fan = Rx[3]=0`(룸컨 OFF값)으로 덮고, 715~716이 `Set==현재`라 `TYPE_FAN_SPEED`를 조기 클리어 → 푸시 유실 → 룸컨 안 켜짐.

**수정 (2곳):**
1. **My_Homenet.c** — 전원 ON 시 `Fan_Mode`를 즉시 1로 안 올림(`Set_Fan=1`, `Fan=0` 유지) → 플래그가 ack 전에 안 사라짐.
2. **My_RJ2.c** — 룸컨 echo의 else 분기(EVENT 409 / CONTROLL 698)를 "대시보드 명령 푸시 대기중이면 채택 보류"로 가드 → Set_* 클로버 방지.

수정합니다.


Edit

                    /* 전원 ON : 환기 모드 + 풍량 1단 (ERVSimulator HomeNetProtocol 와 동일) */
                    Set_Run_Mode = Run_Mode = MODE_VENTILATION;
                    Set_Fan_Mode = Fan_Mode = 1;

                    /* 전원 ON : 환기 모드 + 풍량 1단 (ERVSimulator HomeNetProtocol 와 동일) */
                    Set_Run_Mode = Run_Mode = MODE_VENTILATION;
                    /* [전원ON룸컨레이스] Fan_Mode 를 즉시 1 로 올리지 않음 → Set_Fan(1) != Fan(0) 유지.
                       룸컨 echo(RX_DATA_CONTROLL/EVENT)가 NORMAL 푸시보다 먼저 와도 Set_Fan 이 0 으로 덮이거나
                       My_RJ2 L716(Set==현재 → TYPE_FAN_SPEED 조기 클리어)로 룸컨 푸시가 유실되는 것을 방지.
                       팬은 룸컨이 명령을 수신·확인(echo)하면 Fan_Mode 가 1 로 갱신되며 켜짐. */
                    Set_Fan_Mode = 1;


The file d:\project\nuvoton\HECO2\SOURCE\HECO2\User\My_Homenet.c has been updated successfully. (file state is current in your context — no need to Read it back)

                    /* 전원 ON : 환기 모드 + 풍량 1단 (ERVSimulator HomeNetProtocol 와 동일) */
                    Set_Run_Mode = Run_Mode = MODE_VENTILATION;
                    Set_Fan_Mode = Fan_Mode = 1;

                    /* 전원 ON : 환기 모드 + 풍량 1단 (ERVSimulator HomeNetProtocol 와 동일) */
                    Set_Run_Mode = Run_Mode = MODE_VENTILATION;
                    /* [전원ON룸컨레이스] Fan_Mode 를 즉시 1 로 올리지 않음 → Set_Fan(1) != Fan(0) 유지.
                       룸컨 echo(RX_DATA_CONTROLL/EVENT)가 NORMAL 푸시보다 먼저 와도 Set_Fan 이 0 으로 덮이거나
                       My_RJ2 L716(Set==현재 → TYPE_FAN_SPEED 조기 클리어)로 룸컨 푸시가 유실되는 것을 방지.
                       팬은 룸컨이 명령을 수신·확인(echo)하면 Fan_Mode 가 1 로 갱신되며 켜짐. */
                    Set_Fan_Mode = 1;

-                    Set_Fan_Mode = Fan_Mode = 1;

+                    /* [전원ON룸컨레이스] Fan_Mode 를 즉시 1 로 올리지 않음 → Set_Fan(1) != Fan(0) 유지.

+                       룸컨 echo(RX_DATA_CONTROLL/EVENT)가 NORMAL 푸시보다 먼저 와도 Set_Fan 이 0 으로 덮이거나

+                       My_RJ2 L716(Set==현재 → TYPE_FAN_SPEED 조기 클리어)로 룸컨 푸시가 유실되는 것을 방지.

+                       팬은 룸컨이 명령을 수신·확인(echo)하면 Fan_Mode 가 1 로 갱신되며 켜짐. */

/* [모드변경댐퍼테스트] 마스터 정렬 : 모드변경 시 명령경로 즉시 팬설정 제거.

팬이 0까지 감속해야 Fan_Speed_process 가 Damper_Mode 를 호출(내부댐퍼 이동).

팬 복원은 Fan_Speed_process wait==1 에서 수행. */


			else
			{

                    Set_Run_Mode = Run_Mode = Rx_roomcon232_buffer[2];
                    Set_Fan_Mode = Fan_Mode = Rx_roomcon232_buffer[3];

			else if(!(Command_request_type & (TYPE_MODE|TYPE_FAN_SPEED)))   /* [전원ON룸컨레이스] 대시보드 명령(전원/모드/풍량) 푸시 대기중엔 룸컨 자기보고로 Set_* 를 덮지 않음 */
			{

                    Set_Run_Mode = Run_Mode = Rx_roomcon232_buffer[2];
                    Set_Fan_Mode = Fan_Mode = Rx_roomcon232_buffer[3];


The file d:\project\nuvoton\HECO2\SOURCE\HECO2\User\My_RJ2.c has been updated successfully. (file state is current in your context — no need to Read it back)

			else
			{

                    Set_Run_Mode = Run_Mode = Rx_roomcon232_buffer[2];
                    Set_Fan_Mode = Fan_Mode = Rx_roomcon232_buffer[3];

			else if(!(Command_request_type & (TYPE_MODE|TYPE_FAN_SPEED)))   /* [전원ON룸컨레이스] 대시보드 명령(전원/모드/풍량) 푸시 대기중엔 룸컨 자기보고로 Set_* 를 덮지 않음 */
			{

                    Set_Run_Mode = Run_Mode = Rx_roomcon232_buffer[2];
                    Set_Fan_Mode = Fan_Mode = Rx_roomcon232_buffer[3];

//if(Run_Mode != MODE_AUTO)Fan_Mode = Rx_roomcon232_buffer[3];//DEMO

+      else if(!(Command_request_type & (TYPE_MODE|TYPE_FAN_SPEED)))   /* [전원ON룸컨레이스] 대시보드 명령(전원/모드/풍량) 푸시 대기중엔 룸컨 자기보고로 Set_* 를 덮지 않음 */


			else
			{
				Set_Run_Mode = Run_Mode = Rx_roomcon232_buffer[2];
                if(Run_Mode != MODE_AUTO)
                {
                    Set_Fan_Mode = Fan_Mode = Rx_roomcon232_buffer[3];
                }
			}

			else if(!(Command_request_type & (TYPE_MODE|TYPE_FAN_SPEED)))   /* [전원ON룸컨레이스] 대시보드 명령(전원/모드/풍량) 푸시 대기중엔 룸컨 자기보고로 Set_* 를 덮지 않음 */
			{
				Set_Run_Mode = Run_Mode = Rx_roomcon232_buffer[2];
                if(Run_Mode != MODE_AUTO)
                {
                    Set_Fan_Mode = Fan_Mode = Rx_roomcon232_buffer[3];
                }
			}


			else
			{
				Set_Run_Mode = Run_Mode = Rx_roomcon232_buffer[2];
                if(Run_Mode != MODE_AUTO)
                {
                    Set_Fan_Mode = Fan_Mode = Rx_roomcon232_buffer[3];
                }
			}

			else if(!(Command_request_type & (TYPE_MODE|TYPE_FAN_SPEED)))   /* [전원ON룸컨레이스] 대시보드 명령(전원/모드/풍량) 푸시 대기중엔 룸컨 자기보고로 Set_* 를 덮지 않음 */
			{
				Set_Run_Mode = Run_Mode = Rx_roomcon232_buffer[2];
                if(Run_Mode != MODE_AUTO)
                {
                    Set_Fan_Mode = Fan_Mode = Rx_roomcon232_buffer[3];
                }
			}


All three edits applied. Building firmware:


Bash

cd "d:/project/nuvoton/HECO2/SOURCE/HECO2" && bash build.sh all 2>&1 | tail -8

Rebuild firmware after power-on race fix


[CC]  User/My_RJ2.c
[LD]  build/HERV.elf
[HEX] build/HERV.hex
[BIN] build/HERV.bin

   text	   data	    bss	    dec	    hex	filename
  43972	   1940	   3388	  49300	   c094	build/HERV.elf


빌드 성공 — 경고/오류 0 (text 43952 → 43972). 문서에 기록합니다.


Edit

- **검증 완료**: ERV 전원 OFF→ON(콜드부트) 후 대시보드 실행 시 안심회복 버튼 해제됨(사장님 확인 2026-06-18). 코드 수정 불필요.

- **검증 완료**: ERV 전원 OFF→ON(콜드부트) 후 대시보드 실행 시 안심회복 버튼 해제됨(사장님 확인 2026-06-18). 코드 수정 불필요.

---

# 260618 (11) 대시보드 전원 ON 시 룸컨 간헐적 미동작 (타이밍 레이스) 수정

## 증상
- 대시보드에서 전원 ON 하면 ERV에 연결된 룸컨이 **간헐적으로 안 켜짐**.

## 원인 (룸컨 echo ↔ 푸시 레이스)
- 룸컨(RJ2)은 마스터로 폴링, ERV는 응답(slave). 전원/모드/풍량 변경은 ERV가 `Command_request_type`의 `TYPE_MODE|TYPE_FAN_SPEED` 플래그를 보고 `RX_DATA_MODE_NORMAL` 응답에 `COMMAND_CONTROLL`(Set_Run/Set_Fan)을 실어 푸시.
- CTRL_POWER ON 핸들러(`My_Homenet.c`)가 `Set_Fan_Mode = Fan_Mode = 1` 로 **Set==현재**로 만듦.
- 푸시(NORMAL)보다 룸컨 상태 echo `RX_DATA_CONTROLL`(My_RJ2.c L698 else)/`RX_DATA_MODE_EVENT`(L409 else)가 먼저 도착하면:
  - else가 `Set_Fan_Mode = Fan_Mode = Rx[3]`로 **룸컨 현재(OFF=0) 상태를 Set_* 에 덮음**,
  - 이어 L715~716이 `Set==현재`라 `TYPE_FAN_SPEED`를 **조기 클리어** → 푸시가 영영 안 나감 → 룸컨 미동작.
- 도착 순서(NORMAL vs CONTROLL/EVENT)에 따라 간헐적.

## 수정 (2파일 3곳)
- `Source/HECO2/User/My_Homenet.c` CTRL_POWER ON: `Set_Fan_Mode = Fan_Mode = 1;` → **`Set_Fan_Mode = 1;`** (Fan_Mode 즉시 1로 안 올림). Set_Fan(1)!=Fan(0) 유지 → echo가 먼저 와도(아래 가드와 함께) TYPE_FAN_SPEED 가 살아 NORMAL 푸시 보장. 팬은 룸컨 echo가 Fan_Mode=1 로 갱신하면 켜짐. 마커 `[전원ON룸컨레이스]`.
- `Source/HECO2/User/My_RJ2.c` 룸컨 echo else 2곳을 가드 :
  - L409 `RX_DATA_MODE_EVENT` else → `else if(!(Command_request_type & (TYPE_MODE|TYPE_FAN_SPEED)))`
  - L698 `RX_DATA_CONTROLL` else → 동일 가드
  - 효과: 대시보드 명령(전원/모드/풍량)이 푸시 대기중이면 룸컨 자기보고로 Set_* 를 덮지 않음 → 명령 유실/조기 클리어 방지. ack(SEND_FLAG) 경로는 if-branch라 가드 영향 없음. 평상시(명령 없음) 룸컨 패널 조작 반영은 그대로(가드 통과).

## 빌드 결과
- `bash build.sh all` → 성공, 경고/오류 0. text 43952 → **43972**.

## 후속 / 참고
- 실장비: 대시보드 전원 ON 반복 시 룸컨이 매번 켜지는지 확인.
- 동일 패턴 잠재: `CTRL_RUNMODE`(L287/292)·`CTRL_FAN`(L308)도 `Set==현재`로 equalize → 같은 716 조기클리어 레이스 소지. else 가드(L409/698)로 클로버는 막히나, 모드/풍량 변경 시에도 룸컨 푸시 누락이 보이면 동일하게 de-equalize 적용 검토. (이번엔 보고된 전원 ON 만 수정.)


The file d:\project\nuvoton\HECO2\doc\260618_내부댐퍼_모드변경시_미동작_수정.md has been updated successfully. (file state is current in your context — no need to Read it back)

- **검증 완료**: ERV 전원 OFF→ON(콜드부트) 후 대시보드 실행 시 안심회복 버튼 해제됨(사장님 확인 2026-06-18). 코드 수정 불필요.

---

# 260618 (11) 대시보드 전원 ON 시 룸컨 간헐적 미동작 (타이밍 레이스) 수정

## 증상
- 대시보드에서 전원 ON 하면 ERV에 연결된 룸컨이 **간헐적으로 안 켜짐**.

## 원인 (룸컨 echo ↔ 푸시 레이스)
- 룸컨(RJ2)은 마스터로 폴링, ERV는 응답(slave). 전원/모드/풍량 변경은 ERV가 `Command_request_type`의 `TYPE_MODE|TYPE_FAN_SPEED` 플래그를 보고 `RX_DATA_MODE_NORMAL` 응답에 `COMMAND_CONTROLL`(Set_Run/Set_Fan)을 실어 푸시.
- CTRL_POWER ON 핸들러(`My_Homenet.c`)가 `Set_Fan_Mode = Fan_Mode = 1` 로 **Set==현재**로 만듦.
- 푸시(NORMAL)보다 룸컨 상태 echo `RX_DATA_CONTROLL`(My_RJ2.c L698 else)/`RX_DATA_MODE_EVENT`(L409 else)가 먼저 도착하면:
  - else가 `Set_Fan_Mode = Fan_Mode = Rx[3]`로 **룸컨 현재(OFF=0) 상태를 Set_* 에 덮음**,
  - 이어 L715~716이 `Set==현재`라 `TYPE_FAN_SPEED`를 **조기 클리어** → 푸시가 영영 안 나감 → 룸컨 미동작.
- 도착 순서(NORMAL vs CONTROLL/EVENT)에 따라 간헐적.

## 수정 (2파일 3곳)
- `Source/HECO2/User/My_Homenet.c` CTRL_POWER ON: `Set_Fan_Mode = Fan_Mode = 1;` → **`Set_Fan_Mode = 1;`** (Fan_Mode 즉시 1로 안 올림). Set_Fan(1)!=Fan(0) 유지 → echo가 먼저 와도(아래 가드와 함께) TYPE_FAN_SPEED 가 살아 NORMAL 푸시 보장. 팬은 룸컨 echo가 Fan_Mode=1 로 갱신하면 켜짐. 마커 `[전원ON룸컨레이스]`.
- `Source/HECO2/User/My_RJ2.c` 룸컨 echo else 2곳을 가드 :
  - L409 `RX_DATA_MODE_EVENT` else → `else if(!(Command_request_type & (TYPE_MODE|TYPE_FAN_SPEED)))`
  - L698 `RX_DATA_CONTROLL` else → 동일 가드
  - 효과: 대시보드 명령(전원/모드/풍량)이 푸시 대기중이면 룸컨 자기보고로 Set_* 를 덮지 않음 → 명령 유실/조기 클리어 방지. ack(SEND_FLAG) 경로는 if-branch라 가드 영향 없음. 평상시(명령 없음) 룸컨 패널 조작 반영은 그대로(가드 통과).

## 빌드 결과
- `bash build.sh all` → 성공, 경고/오류 0. text 43952 → **43972**.

## 후속 / 참고
- 실장비: 대시보드 전원 ON 반복 시 룸컨이 매번 켜지는지 확인.
- 동일 패턴 잠재: `CTRL_RUNMODE`(L287/292)·`CTRL_FAN`(L308)도 `Set==현재`로 equalize → 같은 716 조기클리어 레이스 소지. else 가드(L409/698)로 클로버는 막히나, 모드/풍량 변경 시에도 룸컨 푸시 누락이 보이면 동일하게 de-equalize 적용 검토. (이번엔 보고된 전원 ON 만 수정.)

- 펌웨어 `Ext_Run_Mode` 기본값 0(My_bunbaegi.c:984), 1로 설정되는 곳은 명령(My_Homenet.c:319, ReliefRecover ON)뿐 → 펌웨어 자가진입 없음.

- **원인 확정**: 연결된 ERV가 이전 테스트에서 안심회복 켠 상태로 RAM에 남아있었고(전원 미재투입), 대시보드만 재실행해 그 상태를 그대로 표시한 것.

+# 260618 (11) 대시보드 전원 ON 시 룸컨 간헐적 미동작 (타이밍 레이스) 수정

+- 대시보드에서 전원 ON 하면 ERV에 연결된 룸컨이 **간헐적으로 안 켜짐**.

+## 원인 (룸컨 echo ↔ 푸시 레이스)

+- 룸컨(RJ2)은 마스터로 폴링, ERV는 응답(slave). 전원/모드/풍량 변경은 ERV가 `Command_request_type`의 `TYPE_MODE|TYPE_FAN_SPEED` 플래그를 보고 `RX_DATA_MODE_NORMAL` 응답에 `COMMAND_CONTROLL`(Set_Run/Set_Fan)을 실어 푸시.

+- CTRL_POWER ON 핸들러(`My_Homenet.c`)가 `Set_Fan_Mode = Fan_Mode = 1` 로 **Set==현재**로 만듦.

+- 푸시(NORMAL)보다 룸컨 상태 echo `RX_DATA_CONTROLL`(My_RJ2.c L698 else)/`RX_DATA_MODE_EVENT`(L409 else)가 먼저 도착하면:

+  - else가 `Set_Fan_Mode = Fan_Mode = Rx[3]`로 **룸컨 현재(OFF=0) 상태를 Set_* 에 덮음**,

+  - 이어 L715~716이 `Set==현재`라 `TYPE_FAN_SPEED`를 **조기 클리어** → 푸시가 영영 안 나감 → 룸컨 미동작.

+- 도착 순서(NORMAL vs CONTROLL/EVENT)에 따라 간헐적.

+- `Source/HECO2/User/My_Homenet.c` CTRL_POWER ON: `Set_Fan_Mode = Fan_Mode = 1;` → **`Set_Fan_Mode = 1;`** (Fan_Mode 즉시 1로 안 올림). Set_Fan(1)!=Fan(0) 유지 → echo가 먼저 와도(아래 가드와 함께) TYPE_FAN_SPEED 가 살아 NORMAL 푸시 보장. 팬은 룸컨 echo가 Fan_Mode=1 로 갱신하면 켜짐. 마커 `[전원ON룸컨레이스]`.

+- `Source/HECO2/User/My_RJ2.c` 룸컨 echo else 2곳을 가드 :

+  - L409 `RX_DATA_MODE_EVENT` else → `else if(!(Command_request_type & (TYPE_MODE|TYPE_FAN_SPEED)))`

+  - L698 `RX_DATA_CONTROLL` else → 동일 가드

+  - 효과: 대시보드 명령(전원/모드/풍량)이 푸시 대기중이면 룸컨 자기보고로 Set_* 를 덮지 않음 → 명령 유실/조기 클리어 방지. ack(SEND_FLAG) 경로는 if-branch라 가드 영향 없음. 평상시(명령 없음) 룸컨 패널 조작 반영은 그대로(가드 통과).

+- `bash build.sh all` → 성공, 경고/오류 0. text 43952 → **43972**.

+- 실장비: 대시보드 전원 ON 반복 시 룸컨이 매번 켜지는지 확인.

+- 동일 패턴 잠재: `CTRL_RUNMODE`(L287/292)·`CTRL_FAN`(L308)도 `Set==현재`로 equalize → 같은 716 조기클리어 레이스 소지. else 가드(L409/698)로 클로버는 막히나, 모드/풍량 변경 시에도 룸컨 푸시 누락이 보이면 동일하게 de-equalize 적용 검토. (이번엔 보고된 전원 ON 만 수정.)


수정 완료했습니다.

## 원인 — 룸컨 echo ↔ 푸시 타이밍 레이스

룸컨이 마스터로 폴링하고 ERV가 응답하는 구조에서, 전원/모드/풍량 변경은 ERV가 `TYPE_MODE|TYPE_FAN_SPEED` 플래그를 보고 `RX_DATA_MODE_NORMAL` 응답에 명령을 실어 보냅니다. 그런데:

1. CTRL_POWER ON 핸들러가 `Set_Fan_Mode = Fan_Mode = 1`로 **Set==현재**로 만듦.
2. NORMAL 푸시보다 룸컨 상태 echo(`RX_DATA_CONTROLL` else)가 먼저 오면 `Set_Fan = Fan = Rx[3]=0`(룸컨 OFF값)으로 **덮어쓰고**, 715~716이 `Set==현재`라 `TYPE_FAN_SPEED`를 **조기 클리어** → 푸시 유실 → 룸컨 안 켜짐.
3. echo와 NORMAL의 도착 순서에 따라 **간헐적**.

## 수정 (2파일 3곳)

| 파일 | 변경 |
|------|------|
| [My_Homenet.c](SOURCE/HECO2/User/My_Homenet.c) | 전원 ON 시 `Set_Fan_Mode = Fan_Mode = 1` → **`Set_Fan_Mode = 1`** (Fan_Mode 즉시 안 올림 → Set≠현재 유지) |
| [My_RJ2.c:409](SOURCE/HECO2/User/My_RJ2.c#L409) | `RX_DATA_MODE_EVENT` else → `else if(명령 푸시 대기중 아님)` |
| [My_RJ2.c:698](SOURCE/HECO2/User/My_RJ2.c#L698) | `RX_DATA_CONTROLL` else → 동일 가드 |

**핵심:** 대시보드 명령이 푸시 대기중이면 룸컨의 자기보고(OFF 상태)로 `Set_*`을 덮지 않게 가드 → 명령 유실/조기 클리어 방지. ack(SEND_FLAG) 처리는 if-branch라 영향 없고, 평상시 룸컨 패널 조작 반영도 그대로(가드 통과)입니다.

**빌드:** 경고/오류 0 (text 43952 → 43972).

⚠ **참고:** `CTRL_RUNMODE`(모드변경)·`CTRL_FAN`(풍량변경)도 같은 equalize 패턴이라 잠재적으로 같은 레이스가 있습니다. else 가드로 클로버는 막혔지만, 모드/풍량 변경 시에도 룸컨 푸시 누락이 보이면 동일하게 de-equalize를 적용하면 됩니다. 이번엔 보고하신 **전원 ON만** 수정했습니다.

실장비에서 전원 ON 반복 시 룸컨이 매번 켜지는지 확인 부탁드립니다.


지금 공청에서 전원 off를 했는데..erv는 정지했는데 룸컨은 계속 공청을 표시해.


이건 반대 방향 — 전원 OFF가 룸컨에 전달이 안 되는 문제입니다. 그리고 전원 OFF는 모터 정지를 위해 `Set_Run=Run=VENT, Set_Fan=Fan=0`으로 **반드시 equalize**해야 해서(주석 250~253), 값 기반(Set≠현재) 푸시 생존 트릭을 못 씁니다. 그래서 ON 때와 달리 **TYPE_POWER 플래그로 푸시를 구동**하는 게 정답입니다.

`TYPE_*` 플래그 정의부터 확인하겠습니다(비트 플래그인지).


350:#define TYPE_POWER			0x40
351:#define TYPE_MODE			0x01
352:#define TYPE_FAN_SPEED		0x02
353:#define TYPE_RESERVATION	0x04
356:#define TYPE_HOOD_STATE		0x40 // add 2022.1.25
357:#define TYPE_SEND_FLAG		0x80


중요 — `TYPE_POWER = 0x40`인데 `TYPE_HOOD_STATE = 0x40`로 **같은 비트**입니다. 이 충돌을 파악해야 합니다. 정의 블록 전체를 봅니다.


346	#define COMMAND_REQUEST_FILTER_INFO	0x05
347	#define COMMAND_REQUEST_SENSOR_INFO	0x06
348	#define COMMAND_CONTROLL	0x07 // mode / speed / reservaion
349
350	#define TYPE_POWER			0x40
351	#define TYPE_MODE			0x01
352	#define TYPE_FAN_SPEED		0x02
353	#define TYPE_RESERVATION	0x04
354	#define TYPE_FILTER_INFO		0x10
355	#define TYPE_SENSOR_INFO		0x20
356	#define TYPE_HOOD_STATE		0x40 // add 2022.1.25
357	#define TYPE_SEND_FLAG		0x80
358
359

#define COMMAND_REQUEST_FILTER_INFO	0x05
#define COMMAND_REQUEST_SENSOR_INFO	0x06
#define COMMAND_CONTROLL	0x07 // mode / speed / reservaion

#define TYPE_POWER			0x40
#define TYPE_MODE			0x01
#define TYPE_FAN_SPEED		0x02
#define TYPE_RESERVATION	0x04
#define TYPE_FILTER_INFO		0x10
#define TYPE_SENSOR_INFO		0x20
#define TYPE_HOOD_STATE		0x40 // add 2022.1.25
#define TYPE_SEND_FLAG		0x80


`TYPE_POWER(0x40)`가 `TYPE_HOOD_STATE(0x40)`와 **같은 비트**라 충돌합니다. 전원 OFF는 모터 정지 위해 equalize가 강제되어 값 기반 푸시가 불가능하니, **TYPE_POWER 플래그로 푸시를 구동**해야 하는데 비트 충돌부터 풀어야 합니다. 전체 사용처를 확인하겠습니다.


SOURCE\HECO2\User\My_define.h:350:#define TYPE_POWER			0x40
SOURCE\HECO2\User\My_define.h:356:#define TYPE_HOOD_STATE		0x40 // add 2022.1.25
SOURCE\HECO2\User\My_bunbaegi.c:519:                    if(Command_request_type & TYPE_POWER)Tx_bunbaegi_buffer[5] |= 0x80;
SOURCE\HECO2\User\My_Hood.c:142://        if((Hood_Status == 1)&&(Hood_Yeundong_flag == 0))Command_request_type |= TYPE_HOOD_STATE;// add 2022.1.25
SOURCE\HECO2\User\My_Hood.c:143://        if((Hood_Status == 0)&&(Hood_Yeundong_flag == 1))Command_request_type |= TYPE_HOOD_STATE;// add 2022.1.25
SOURCE\HECO2\User\My_Hood.c:208:			Command_request_type |= (TYPE_MODE|TYPE_FAN_SPEED|TYPE_HOOD_STATE);
SOURCE\HECO2\User\My_Hood.c:243:            Command_request_type |= TYPE_HOOD_STATE;// add 2022.1.25
SOURCE\HECO2\User\My_Homenet.c:270:                /* 각실분배기 : 전원 비트(TYPE_POWER) + 모드/풍량. 룸컨(RJ2)도 Command_request_type 로 푸시 */
SOURCE\HECO2\User\My_Homenet.c:271:                Command_request_type |= (TYPE_POWER | TYPE_MODE | TYPE_FAN_SPEED);
SOURCE\HECO2\User\My_Hood_old.c:152://        if((Hood_Status == 1)&&(Hood_Yeundong_flag == 0))Command_request_type |= TYPE_HOOD_STATE;// add 2022.1.25
SOURCE\HECO2\User\My_Hood_old.c:153://        if((Hood_Status == 0)&&(Hood_Yeundong_flag == 1))Command_request_type |= TYPE_HOOD_STATE;// add 2022.1.25
SOURCE\HECO2\User\My_Hood_old.c:215:			Command_request_type |= (TYPE_MODE|TYPE_FAN_SPEED|TYPE_HOOD_STATE);
SOURCE\HECO2\User\My_Hood_old.c:241:            Command_request_type |= TYPE_HOOD_STATE;// add 2022.1.25
SOURCE\HECO2\User\My_RJ2.c:338:					Command_request_type |= (TYPE_HOOD_STATE);
SOURCE\HECO2\User\My_RJ2.c:374:            else if(Command_request_type & (TYPE_HOOD_STATE))// add 2022.1.25
SOURCE\HECO2\User\My_RJ2.c:451:                        Command_request_type |= (TYPE_HOOD_STATE);
SOURCE\HECO2\User\My_RJ2.c:758:			Command_request_type &= ~(TYPE_HOOD_STATE);


비트 충돌이 확인됐습니다: `TYPE_POWER(0x40)` == `TYPE_HOOD_STATE(0x40)` — 전원 명령이 룸컨엔 hood-state로, hood가 분배기엔 power로 오인되는 잠재 버그. 깨끗이 풀려면 TYPE_POWER에 빈 비트(0x08)를 주고, RJ2 푸시 조건에 TYPE_POWER를 넣어 OFF도 푸시되게 해야 합니다. 분배기 쪽 영향과 ack 경로를 확인하겠습니다.


505	                    Tx_bunbaegi_buffer[30] = (uint8_t)(SEN66_VOC_value[6]>>8);;
506	                    Tx_bunbaegi_buffer[31] = (uint8_t)(SEN66_VOC_value[6]);
507
508	                    Tx_bunbaegi_buffer[32] = 0;
509	                }
510	                else //normal
511	                {
512	                    id_2 = 1; //living room
513
514	                    Tx_bunbaegi_buffer[4] = 0x00; //VSP_MODE
515	                    Tx_bunbaegi_buffer[5] = Diffuser_Power[id_2];  //Power Mode
516	                    Tx_bunbaegi_buffer[6] = Diffuser_Run_Mode[id_2];  //Run Mode
517	                    Tx_bunbaegi_buffer[7] = Diffuser_Fan_Speed[id_2];  //Fan Speed
518
519	                    if(Command_request_type & TYPE_POWER)Tx_bunbaegi_buffer[5] |= 0x80;
520	                    if(Command_request_type & TYPE_MODE)Tx_bunbaegi_buffer[6] |= 0x80;
521	                    if(Command_request_type & TYPE_FAN_SPEED)Tx_bunbaegi_buffer[7] |= 0x80;
522	                    Command_request_type = 0;
523
524	                    Tx_bunbaegi_buffer[8] = 0;  //reservation time
525	                    Tx_bunbaegi_buffer[9] = UV_OnOff;  //Heater/UV
526	                    Tx_bunbaegi_buffer[10] = 0; //Filter Reset
527	                    Tx_bunbaegi_buffer[11] = 0; //RPM Start
528	                    Tx_bunbaegi_buffer[12] = 0; //RPM Stop
529

                    Tx_bunbaegi_buffer[30] = (uint8_t)(SEN66_VOC_value[6]>>8);;
                    Tx_bunbaegi_buffer[31] = (uint8_t)(SEN66_VOC_value[6]);

                    Tx_bunbaegi_buffer[32] = 0;
                }
                else //normal
                {
                    id_2 = 1; //living room

                    Tx_bunbaegi_buffer[4] = 0x00; //VSP_MODE
                    Tx_bunbaegi_buffer[5] = Diffuser_Power[id_2];  //Power Mode
                    Tx_bunbaegi_buffer[6] = Diffuser_Run_Mode[id_2];  //Run Mode
                    Tx_bunbaegi_buffer[7] = Diffuser_Fan_Speed[id_2];  //Fan Speed

                    if(Command_request_type & TYPE_POWER)Tx_bunbaegi_buffer[5] |= 0x80;
                    if(Command_request_type & TYPE_MODE)Tx_bunbaegi_buffer[6] |= 0x80;
                    if(Command_request_type & TYPE_FAN_SPEED)Tx_bunbaegi_buffer[7] |= 0x80;
                    Command_request_type = 0;

                    Tx_bunbaegi_buffer[8] = 0;  //reservation time
                    Tx_bunbaegi_buffer[9] = UV_OnOff;  //Heater/UV
                    Tx_bunbaegi_buffer[10] = 0; //Filter Reset
                    Tx_bunbaegi_buffer[11] = 0; //RPM Start
                    Tx_bunbaegi_buffer[12] = 0; //RPM Stop


결정적 발견입니다 — `My_bunbaegi.c:522`에서 분배기 통신이 `Command_request_type = 0;`으로 **플래그 전체를 지웁니다.** 즉 `Command_request_type`은 룸컨(RJ2)과 분배기 **두 소비자가 공유**하는데, 분배기가 먼저 처리하면 RJ2가 플래그를 못 봐서 룸컨 푸시가 유실됩니다. 게다가 `TYPE_POWER(0x40)`은 `TYPE_HOOD_STATE`와 비트 충돌, 전원 OFF는 모터정지 위해 equalize 강제(716 조기클리어).

**근본 원인 = 공유 플래그 레이스 + 716 equalize 클리어.** 가장 견고한 해법은 **룸컨 전용 pending 플래그**(분배기가 못 건드리고, RJ2 ack로만 클리어)를 두는 것입니다.

`Command_request_type`의 extern 선언 위치와 ack 라인을 확인하겠습니다.


373:extern uint8_t Command_request_type;


140	uint8_t EEP_Save_Flag = 0;
141
142	uint8_t Command_request_type = 0;
143	uint8_t Roomcon_Filter_Error = 0; // 2021.5.31

uint8_t EEP_Save_Flag = 0;

uint8_t Command_request_type = 0;
uint8_t Roomcon_Filter_Error = 0; // 2021.5.31


모든 위치와 ack 라인 확인 완료. 룸컨 전용 pending 플래그 `RoomCtrl_Push`를 도입해 견고하게 고치겠습니다 — 분배기(`Command_request_type=0`)·716 equalize 클리어와 무관하게 룸컨에 전원변경이 반드시 푸시되고, 룸컨 ack로만 해제됩니다. 동시에 ON 때 de-equalize는 원복합니다(이제 불필요).

8곳을 수정합니다.


extern uint8_t Command_request_type;
extern uint8_t RoomCtrl_Push;   /* [룸컨푸시] 대시보드 전원변경의 룸컨(RJ2) 전용 pending. 분배기와 공유 안 함, RJ2 ack(echo)로만 클리어 */


The file d:\project\nuvoton\HECO2\SOURCE\HECO2\User\My_define.h has been updated successfully. (file state is current in your context — no need to Read it back)

extern uint8_t Command_request_type;
extern uint8_t RoomCtrl_Push;   /* [룸컨푸시] 대시보드 전원변경의 룸컨(RJ2) 전용 pending. 분배기와 공유 안 함, RJ2 ack(echo)로만 클리어 */

+extern uint8_t RoomCtrl_Push;   /* [룸컨푸시] 대시보드 전원변경의 룸컨(RJ2) 전용 pending. 분배기와 공유 안 함, RJ2 ack(echo)로만 클리어 */


uint8_t Command_request_type = 0;
uint8_t Roomcon_Filter_Error = 0; // 2021.5.31

uint8_t Command_request_type = 0;
uint8_t RoomCtrl_Push = 0;   /* [룸컨푸시] 대시보드 전원변경의 룸컨 전용 pending. 분배기(My_bunbaegi L522 Command_request_type=0)·716 equalize 조기클리어와 무관하게 룸컨에 푸시 보장, RJ2 ack 로만 해제 */
uint8_t Roomcon_Filter_Error = 0; // 2021.5.31


uint8_t Command_request_type = 0;
uint8_t Roomcon_Filter_Error = 0; // 2021.5.31

uint8_t Command_request_type = 0;
uint8_t RoomCtrl_Push = 0;   /* [룸컨푸시] 대시보드 전원변경의 룸컨 전용 pending. 분배기(My_bunbaegi L522 Command_request_type=0)·716 equalize 조기클리어와 무관하게 룸컨에 푸시 보장, RJ2 ack 로만 해제 */
uint8_t Roomcon_Filter_Error = 0; // 2021.5.31

+uint8_t RoomCtrl_Push = 0;   /* [룸컨푸시] 대시보드 전원변경의 룸컨 전용 pending. 분배기(My_bunbaegi L522 Command_request_type=0)·716 equalize 조기클리어와 무관하게 룸컨에 푸시 보장, RJ2 ack 로만 해제 */


if(Command_request_type & (TYPE_MODE|TYPE_FAN_SPEED|TYPE_RESERVATION) )

if((Command_request_type & (TYPE_MODE|TYPE_FAN_SPEED|TYPE_RESERVATION)) || RoomCtrl_Push)   /* [룸컨푸시] 전용 pending 도 푸시 트리거 */


-      if(Command_request_type & (TYPE_MODE|TYPE_FAN_SPEED|TYPE_RESERVATION) )

+      if((Command_request_type & (TYPE_MODE|TYPE_FAN_SPEED|TYPE_RESERVATION)) || RoomCtrl_Push)   /* [룸컨푸시] 전용 pending 도 푸시 트리거 */

if((Hood_Yeundong_flag == 1)&&(Tx_Yeundong_Delay == 0))


else if(!(Command_request_type & (TYPE_MODE|TYPE_FAN_SPEED)))   /* [전원ON룸컨레이스] 대시보드 명령(전원/모드/풍량) 푸시 대기중엔 룸컨 자기보고로 Set_* 를 덮지 않음 */

else if(!RoomCtrl_Push && !(Command_request_type & (TYPE_MODE|TYPE_FAN_SPEED)))   /* [룸컨푸시] 명령 푸시 대기중엔 룸컨 자기보고로 Set_* 를 덮지 않음 */


The file d:\project\nuvoton\HECO2\SOURCE\HECO2\User\My_RJ2.c has been updated. All occurrences were successfully replaced. (file state is current in your context — no need to Read it back)

//if(Run_Mode != MODE_AUTO)Fan_Mode = Rx_roomcon232_buffer[3];//DEMO

-      else if(!(Command_request_type & (TYPE_MODE|TYPE_FAN_SPEED)))   /* [전원ON룸컨레이스] 대시보드 명령(전원/모드/풍량) 푸시 대기중엔 룸컨 자기보고로 Set_* 를 덮지 않음 */

+      else if(!RoomCtrl_Push && !(Command_request_type & (TYPE_MODE|TYPE_FAN_SPEED)))   /* [룸컨푸시] 명령 푸시 대기중엔 룸컨 자기보고로 Set_* 를 덮지 않음 */

Set_Run_Mode = Run_Mode = Rx_roomcon232_buffer[2];


                    Command_request_type = 0;
                    Command_request_type &= ~TYPE_SEND_FLAG;
                    Run_Mode = Rx_roomcon232_buffer[2];

                    Command_request_type = 0;
                    Command_request_type &= ~TYPE_SEND_FLAG;
                    RoomCtrl_Push = 0;   /* [룸컨푸시] 룸컨 ack(echo) → 전용 pending 해제 */
                    Run_Mode = Rx_roomcon232_buffer[2];


                    Command_request_type = 0;
                    Command_request_type &= ~TYPE_SEND_FLAG;
                    Run_Mode = Rx_roomcon232_buffer[2];

                    Command_request_type = 0;
                    Command_request_type &= ~TYPE_SEND_FLAG;
                    RoomCtrl_Push = 0;   /* [룸컨푸시] 룸컨 ack(echo) → 전용 pending 해제 */
                    Run_Mode = Rx_roomcon232_buffer[2];

+                    RoomCtrl_Push = 0;   /* [룸컨푸시] 룸컨 ack(echo) → 전용 pending 해제 */


                    Command_request_type &= ~TYPE_SEND_FLAG;
                    Run_Mode = Rx_roomcon232_buffer[2];
                    if(Run_Mode != MODE_AUTO)
                    {
                        Fan_Mode = Rx_roomcon232_buffer[3];///////////////// DEMO

                    Command_request_type &= ~TYPE_SEND_FLAG;
                    RoomCtrl_Push = 0;   /* [룸컨푸시] 룸컨 ack(echo) → 전용 pending 해제 */
                    Run_Mode = Rx_roomcon232_buffer[2];
                    if(Run_Mode != MODE_AUTO)
                    {
                        Fan_Mode = Rx_roomcon232_buffer[3];///////////////// DEMO


                    Command_request_type &= ~TYPE_SEND_FLAG;
                    Run_Mode = Rx_roomcon232_buffer[2];
                    if(Run_Mode != MODE_AUTO)
                    {
                        Fan_Mode = Rx_roomcon232_buffer[3];///////////////// DEMO

                    Command_request_type &= ~TYPE_SEND_FLAG;
                    RoomCtrl_Push = 0;   /* [룸컨푸시] 룸컨 ack(echo) → 전용 pending 해제 */
                    Run_Mode = Rx_roomcon232_buffer[2];
                    if(Run_Mode != MODE_AUTO)
                    {
                        Fan_Mode = Rx_roomcon232_buffer[3];///////////////// DEMO


                    /* 전원 ON : 환기 모드 + 풍량 1단 (ERVSimulator HomeNetProtocol 와 동일) */
                    Set_Run_Mode = Run_Mode = MODE_VENTILATION;
                    /* [전원ON룸컨레이스] Fan_Mode 를 즉시 1 로 올리지 않음 → Set_Fan(1) != Fan(0) 유지.
                       룸컨 echo(RX_DATA_CONTROLL/EVENT)가 NORMAL 푸시보다 먼저 와도 Set_Fan 이 0 으로 덮이거나
                       My_RJ2 L716(Set==현재 → TYPE_FAN_SPEED 조기 클리어)로 룸컨 푸시가 유실되는 것을 방지.
                       팬은 룸컨이 명령을 수신·확인(echo)하면 Fan_Mode 가 1 로 갱신되며 켜짐. */
                    Set_Fan_Mode = 1;

                    /* 전원 ON : 환기 모드 + 풍량 1단 (ERVSimulator HomeNetProtocol 와 동일) */
                    Set_Run_Mode = Run_Mode = MODE_VENTILATION;
                    Set_Fan_Mode = Fan_Mode = 1;


                    /* 전원 ON : 환기 모드 + 풍량 1단 (ERVSimulator HomeNetProtocol 와 동일) */
                    Set_Run_Mode = Run_Mode = MODE_VENTILATION;
                    /* [전원ON룸컨레이스] Fan_Mode 를 즉시 1 로 올리지 않음 → Set_Fan(1) != Fan(0) 유지.
                       룸컨 echo(RX_DATA_CONTROLL/EVENT)가 NORMAL 푸시보다 먼저 와도 Set_Fan 이 0 으로 덮이거나
                       My_RJ2 L716(Set==현재 → TYPE_FAN_SPEED 조기 클리어)로 룸컨 푸시가 유실되는 것을 방지.
                       팬은 룸컨이 명령을 수신·확인(echo)하면 Fan_Mode 가 1 로 갱신되며 켜짐. */
                    Set_Fan_Mode = 1;

                    /* 전원 ON : 환기 모드 + 풍량 1단 (ERVSimulator HomeNetProtocol 와 동일) */
                    Set_Run_Mode = Run_Mode = MODE_VENTILATION;
                    Set_Fan_Mode = Fan_Mode = 1;

-                    /* [전원ON룸컨레이스] Fan_Mode 를 즉시 1 로 올리지 않음 → Set_Fan(1) != Fan(0) 유지.

-                       룸컨 echo(RX_DATA_CONTROLL/EVENT)가 NORMAL 푸시보다 먼저 와도 Set_Fan 이 0 으로 덮이거나

-                       My_RJ2 L716(Set==현재 → TYPE_FAN_SPEED 조기 클리어)로 룸컨 푸시가 유실되는 것을 방지.

-                       팬은 룸컨이 명령을 수신·확인(echo)하면 Fan_Mode 가 1 로 갱신되며 켜짐. */

+                    Set_Fan_Mode = Fan_Mode = 1;

/* [모드변경댐퍼테스트] 마스터 정렬 : 모드변경 시 명령경로 즉시 팬설정 제거.

팬이 0까지 감속해야 Fan_Speed_process 가 Damper_Mode 를 호출(내부댐퍼 이동).

팬 복원은 Fan_Speed_process wait==1 에서 수행. */


                /* 각실분배기 : 전원 비트(TYPE_POWER) + 모드/풍량. 룸컨(RJ2)도 Command_request_type 로 푸시 */
                Command_request_type |= (TYPE_POWER | TYPE_MODE | TYPE_FAN_SPEED);

                /* 각실분배기 : 전원 비트(TYPE_POWER) + 모드/풍량. 룸컨(RJ2)도 Command_request_type 로 푸시 */
                Command_request_type |= (TYPE_POWER | TYPE_MODE | TYPE_FAN_SPEED);
                /* [룸컨푸시] 전원 ON/OFF 는 룸컨 전용 pending 으로도 표시 → 분배기(My_bunbaegi L522 Command_request_type=0)나
                   My_RJ2 L716(전원OFF는 모터정지 위해 Set==현재 equalize 강제 → TYPE_*_조기클리어)로 룸컨 푸시가 유실되어
                   룸컨이 옛 모드(예: 공청)를 계속 표시하던 문제 해소. RJ2 ack(echo) 시 자동 해제. */
                RoomCtrl_Push = 1;


                /* 각실분배기 : 전원 비트(TYPE_POWER) + 모드/풍량. 룸컨(RJ2)도 Command_request_type 로 푸시 */
                Command_request_type |= (TYPE_POWER | TYPE_MODE | TYPE_FAN_SPEED);
                /* [룸컨푸시] 전원 ON/OFF 는 룸컨 전용 pending 으로도 표시 → 분배기(My_bunbaegi L522 Command_request_type=0)나
                   My_RJ2 L716(전원OFF는 모터정지 위해 Set==현재 equalize 강제 → TYPE_*_조기클리어)로 룸컨 푸시가 유실되어
                   룸컨이 옛 모드(예: 공청)를 계속 표시하던 문제 해소. RJ2 ack(echo) 시 자동 해제. */
                RoomCtrl_Push = 1;

for(i = 1; i <= 4; i++) { Diffuser_Damper_Manual[i] = 0; Diffuser_Led_Manual[i] = 0; }

+                /* [룸컨푸시] 전원 ON/OFF 는 룸컨 전용 pending 으로도 표시 → 분배기(My_bunbaegi L522 Command_request_type=0)나

+                   My_RJ2 L716(전원OFF는 모터정지 위해 Set==현재 equalize 강제 → TYPE_*_조기클리어)로 룸컨 푸시가 유실되어

+                   룸컨이 옛 모드(예: 공청)를 계속 표시하던 문제 해소. RJ2 ack(echo) 시 자동 해제. */

case 0x02:  /* CTRL_RUNMODE [mode] 0off/1환기/2자동/3공청/4바이패스 */


All 8 edits applied. Building:


Bash

cd "d:/project/nuvoton/HECO2/SOURCE/HECO2" && bash build.sh all 2>&1 | tail -8

Rebuild firmware after room controller push fix


[CC]  User/main.c
[LD]  build/HERV.elf
[HEX] build/HERV.hex
[BIN] build/HERV.bin

   text	   data	    bss	    dec	    hex	filename
  44024	   1940	   3388	  49352	   c0c8	build/HERV.elf


빌드 성공 — 경고/오류 0 (text 43972 → 44024). 문서에 기록합니다.


Edit

- 동일 패턴 잠재: `CTRL_RUNMODE`(L287/292)·`CTRL_FAN`(L308)도 `Set==현재`로 equalize → 같은 716 조기클리어 레이스 소지. else 가드(L409/698)로 클로버는 막히나, 모드/풍량 변경 시에도 룸컨 푸시 누락이 보이면 동일하게 de-equalize 적용 검토. (이번엔 보고된 전원 ON 만 수정.)

- 동일 패턴 잠재: `CTRL_RUNMODE`(L287/292)·`CTRL_FAN`(L308)도 `Set==현재`로 equalize → 같은 716 조기클리어 레이스 소지. else 가드(L409/698)로 클로버는 막히나, 모드/풍량 변경 시에도 룸컨 푸시 누락이 보이면 동일하게 de-equalize 적용 검토. (이번엔 보고된 전원 ON 만 수정.)
  - → 260618 (12) 에서 룸컨 전용 pending(`RoomCtrl_Push`)으로 전원 ON/OFF 모두 견고화. (11)의 de-equalize 는 원복(불필요).

---

# 260618 (12) 전원 OFF 시 룸컨이 옛 모드(공청) 계속 표시 + (11) 통합 — 룸컨 전용 푸시 플래그

## 증상
- 공청에서 대시보드 전원 OFF → ERV(모터)는 정지하는데 **룸컨은 계속 "공청" 표시**. (전원 ON 간헐 미동작과 같은 계열)

## 근본 원인 (공유 플래그 레이스)
- `Command_request_type` 을 **룸컨(RJ2)·각실분배기(bunbagi) 두 소비자가 공유**.
  - `My_bunbaegi.c:522` 가 프레임 송신 후 `Command_request_type = 0;` 로 **전체를 wipe** → 분배기가 먼저 처리하면 RJ2가 플래그를 못 봐 룸컨 푸시 유실.
  - 전원 OFF는 모터 정지를 위해 `Set_Run=Run=VENT, Set_Fan=Fan=0` **equalize 강제**(주석 250~253) → `My_RJ2.c:715~716`(Set==현재 → TYPE_MODE/FAN 클리어)가 NORMAL 푸시 전에 플래그 제거 → 룸컨 푸시 유실.
  - 둘 다 도착순서 의존 → 간헐/상황적.
- (부수발견) `TYPE_POWER(0x40)` == `TYPE_HOOD_STATE(0x40)` **비트 충돌**(My_define.h). 전원명령↔후드상태가 서로 오인될 잠재 버그. 이번 수정은 별도 변수를 써서 이 충돌을 우회(미해결로 남김 — 후속 정리 권장).

## 수정 — 룸컨 전용 pending 플래그 `RoomCtrl_Push`
- `My_define.h` : `extern uint8_t RoomCtrl_Push;` 추가.
- `My_RJ2.c` :
  - 전역 `uint8_t RoomCtrl_Push = 0;` 정의.
  - NORMAL 푸시 조건(L332)에 `|| RoomCtrl_Push` 추가 → 분배기 wipe·716 클리어와 무관하게 룸컨에 푸시.
  - echo else 가드(L409/698) : `!RoomCtrl_Push && !(...)` → pending 중 룸컨 자기보고로 Set_* 안 덮음.
  - ack(SEND_FLAG) 경로(EVENT L399 / CONTROLL L691) : `RoomCtrl_Push = 0;` → 룸컨이 명령 수신·확인하면 해제.
- `My_Homenet.c` CTRL_POWER : `Command_request_type |= (TYPE_POWER|TYPE_MODE|TYPE_FAN_SPEED);` 직후 `RoomCtrl_Push = 1;`. (11)의 ON de-equalize 는 원복(`Set_Fan_Mode = Fan_Mode = 1;`) — RoomCtrl_Push 가 푸시를 보장하므로 불필요, 모터는 즉시 ON.

## 효과
- 전원 ON/OFF 시 분배기·716 타이밍과 무관하게 룸컨에 Set_Run/Set_Fan(전원 OFF=VENT/0) 이 반드시 푸시 → 룸컨이 즉시 OFF(또는 ON) 동기. ack 시 pending 해제로 평상시 룸컨 패널 조작 반영은 그대로.

## 빌드 결과
- `bash build.sh all` → 성공, 경고/오류 0. text 43972 → **44024**.

## 후속 / 주의
- **실장비 검증 필수** : 핵심 통신 상태머신(RJ2) 변경. 전원 ON/OFF 반복 시 룸컨이 매번 동기되는지, 룸컨 패널 직접 조작·후드연동·분배기 동작에 회귀 없는지 확인.
- `TYPE_POWER`/`TYPE_HOOD_STATE` 0x40 비트 충돌은 미해결(빈 비트 0x08 로 분리 권장). 이번 수정과 독립.
- 모드/풍량 변경(CTRL_RUNMODE/CTRL_FAN)도 룸컨 동기 누락이 보이면 동일하게 `RoomCtrl_Push = 1;` 적용 가능.


- 동일 패턴 잠재: `CTRL_RUNMODE`(L287/292)·`CTRL_FAN`(L308)도 `Set==현재`로 equalize → 같은 716 조기클리어 레이스 소지. else 가드(L409/698)로 클로버는 막히나, 모드/풍량 변경 시에도 룸컨 푸시 누락이 보이면 동일하게 de-equalize 적용 검토. (이번엔 보고된 전원 ON 만 수정.)
  - → 260618 (12) 에서 룸컨 전용 pending(`RoomCtrl_Push`)으로 전원 ON/OFF 모두 견고화. (11)의 de-equalize 는 원복(불필요).

---

# 260618 (12) 전원 OFF 시 룸컨이 옛 모드(공청) 계속 표시 + (11) 통합 — 룸컨 전용 푸시 플래그

## 증상
- 공청에서 대시보드 전원 OFF → ERV(모터)는 정지하는데 **룸컨은 계속 "공청" 표시**. (전원 ON 간헐 미동작과 같은 계열)

## 근본 원인 (공유 플래그 레이스)
- `Command_request_type` 을 **룸컨(RJ2)·각실분배기(bunbagi) 두 소비자가 공유**.
  - `My_bunbaegi.c:522` 가 프레임 송신 후 `Command_request_type = 0;` 로 **전체를 wipe** → 분배기가 먼저 처리하면 RJ2가 플래그를 못 봐 룸컨 푸시 유실.
  - 전원 OFF는 모터 정지를 위해 `Set_Run=Run=VENT, Set_Fan=Fan=0` **equalize 강제**(주석 250~253) → `My_RJ2.c:715~716`(Set==현재 → TYPE_MODE/FAN 클리어)가 NORMAL 푸시 전에 플래그 제거 → 룸컨 푸시 유실.
  - 둘 다 도착순서 의존 → 간헐/상황적.
- (부수발견) `TYPE_POWER(0x40)` == `TYPE_HOOD_STATE(0x40)` **비트 충돌**(My_define.h). 전원명령↔후드상태가 서로 오인될 잠재 버그. 이번 수정은 별도 변수를 써서 이 충돌을 우회(미해결로 남김 — 후속 정리 권장).

## 수정 — 룸컨 전용 pending 플래그 `RoomCtrl_Push`
- `My_define.h` : `extern uint8_t RoomCtrl_Push;` 추가.
- `My_RJ2.c` :
  - 전역 `uint8_t RoomCtrl_Push = 0;` 정의.
  - NORMAL 푸시 조건(L332)에 `|| RoomCtrl_Push` 추가 → 분배기 wipe·716 클리어와 무관하게 룸컨에 푸시.
  - echo else 가드(L409/698) : `!RoomCtrl_Push && !(...)` → pending 중 룸컨 자기보고로 Set_* 안 덮음.
  - ack(SEND_FLAG) 경로(EVENT L399 / CONTROLL L691) : `RoomCtrl_Push = 0;` → 룸컨이 명령 수신·확인하면 해제.
- `My_Homenet.c` CTRL_POWER : `Command_request_type |= (TYPE_POWER|TYPE_MODE|TYPE_FAN_SPEED);` 직후 `RoomCtrl_Push = 1;`. (11)의 ON de-equalize 는 원복(`Set_Fan_Mode = Fan_Mode = 1;`) — RoomCtrl_Push 가 푸시를 보장하므로 불필요, 모터는 즉시 ON.

## 효과
- 전원 ON/OFF 시 분배기·716 타이밍과 무관하게 룸컨에 Set_Run/Set_Fan(전원 OFF=VENT/0) 이 반드시 푸시 → 룸컨이 즉시 OFF(또는 ON) 동기. ack 시 pending 해제로 평상시 룸컨 패널 조작 반영은 그대로.

## 빌드 결과
- `bash build.sh all` → 성공, 경고/오류 0. text 43972 → **44024**.

## 후속 / 주의
- **실장비 검증 필수** : 핵심 통신 상태머신(RJ2) 변경. 전원 ON/OFF 반복 시 룸컨이 매번 동기되는지, 룸컨 패널 직접 조작·후드연동·분배기 동작에 회귀 없는지 확인.
- `TYPE_POWER`/`TYPE_HOOD_STATE` 0x40 비트 충돌은 미해결(빈 비트 0x08 로 분리 권장). 이번 수정과 독립.
- 모드/풍량 변경(CTRL_RUNMODE/CTRL_FAN)도 룸컨 동기 누락이 보이면 동일하게 `RoomCtrl_Push = 1;` 적용 가능.

- 실장비: 대시보드 전원 ON 반복 시 룸컨이 매번 켜지는지 확인.

+  - → 260618 (12) 에서 룸컨 전용 pending(`RoomCtrl_Push`)으로 전원 ON/OFF 모두 견고화. (11)의 de-equalize 는 원복(불필요).

+# 260618 (12) 전원 OFF 시 룸컨이 옛 모드(공청) 계속 표시 + (11) 통합 — 룸컨 전용 푸시 플래그

+- 공청에서 대시보드 전원 OFF → ERV(모터)는 정지하는데 **룸컨은 계속 "공청" 표시**. (전원 ON 간헐 미동작과 같은 계열)

+## 근본 원인 (공유 플래그 레이스)

+- `Command_request_type` 을 **룸컨(RJ2)·각실분배기(bunbagi) 두 소비자가 공유**.

+  - `My_bunbaegi.c:522` 가 프레임 송신 후 `Command_request_type = 0;` 로 **전체를 wipe** → 분배기가 먼저 처리하면 RJ2가 플래그를 못 봐 룸컨 푸시 유실.

+  - 전원 OFF는 모터 정지를 위해 `Set_Run=Run=VENT, Set_Fan=Fan=0` **equalize 강제**(주석 250~253) → `My_RJ2.c:715~716`(Set==현재 → TYPE_MODE/FAN 클리어)가 NORMAL 푸시 전에 플래그 제거 → 룸컨 푸시 유실.

+  - 둘 다 도착순서 의존 → 간헐/상황적.

+- (부수발견) `TYPE_POWER(0x40)` == `TYPE_HOOD_STATE(0x40)` **비트 충돌**(My_define.h). 전원명령↔후드상태가 서로 오인될 잠재 버그. 이번 수정은 별도 변수를 써서 이 충돌을 우회(미해결로 남김 — 후속 정리 권장).

+## 수정 — 룸컨 전용 pending 플래그 `RoomCtrl_Push`

+- `My_define.h` : `extern uint8_t RoomCtrl_Push;` 추가.

+  - 전역 `uint8_t RoomCtrl_Push = 0;` 정의.

+  - NORMAL 푸시 조건(L332)에 `|| RoomCtrl_Push` 추가 → 분배기 wipe·716 클리어와 무관하게 룸컨에 푸시.

+  - echo else 가드(L409/698) : `!RoomCtrl_Push && !(...)` → pending 중 룸컨 자기보고로 Set_* 안 덮음.

+  - ack(SEND_FLAG) 경로(EVENT L399 / CONTROLL L691) : `RoomCtrl_Push = 0;` → 룸컨이 명령 수신·확인하면 해제.

+- `My_Homenet.c` CTRL_POWER : `Command_request_type |= (TYPE_POWER|TYPE_MODE|TYPE_FAN_SPEED);` 직후 `RoomCtrl_Push = 1;`. (11)의 ON de-equalize 는 원복(`Set_Fan_Mode = Fan_Mode = 1;`) — RoomCtrl_Push 가 푸시를 보장하므로 불필요, 모터는 즉시 ON.

+- 전원 ON/OFF 시 분배기·716 타이밍과 무관하게 룸컨에 Set_Run/Set_Fan(전원 OFF=VENT/0) 이 반드시 푸시 → 룸컨이 즉시 OFF(또는 ON) 동기. ack 시 pending 해제로 평상시 룸컨 패널 조작 반영은 그대로.

+- `bash build.sh all` → 성공, 경고/오류 0. text 43972 → **44024**.

+- **실장비 검증 필수** : 핵심 통신 상태머신(RJ2) 변경. 전원 ON/OFF 반복 시 룸컨이 매번 동기되는지, 룸컨 패널 직접 조작·후드연동·분배기 동작에 회귀 없는지 확인.

+- `TYPE_POWER`/`TYPE_HOOD_STATE` 0x40 비트 충돌은 미해결(빈 비트 0x08 로 분리 권장). 이번 수정과 독립.

+- 모드/풍량 변경(CTRL_RUNMODE/CTRL_FAN)도 룸컨 동기 누락이 보이면 동일하게 `RoomCtrl_Push = 1;` 적용 가능.


수정 완료. 핵심 아키텍처 사실이라 메모리에도 남기겠습니다.


Write

---
name: command-request-type-shared-race
description: ERV의 Command_request_type은 룸컨(RJ2)·분배기(bunbagi) 공유 플래그라 대시보드 명령 푸시에 레이스가 있음
metadata:
  node_type: memory
  type: project
---

HECO2 ERV 펌웨어에서 `Command_request_type`(My_RJ2.c 전역, My_define.h TYPE_* 비트)은 **룸컨(RJ2, SC0)과 각실분배기(My_bunbaegi.c) 두 통신 소비자가 공유**한다. 대시보드 명령(전원/모드/풍량)이 이 플래그를 set 하면 두 버스가 각자 소비·클리어하는데:
- `My_bunbaegi.c:522` 가 프레임 송신 후 `Command_request_type = 0;` 로 **전체 wipe** → 분배기가 먼저 돌면 RJ2가 플래그를 못 봐 룸컨 푸시 유실.
- `My_RJ2.c:715~716` 이 `Set==현재` 면 TYPE_MODE/FAN 클리어. 전원 OFF는 모터정지 위해 `Set_Run=Run=VENT, Set_Fan=Fan=0` equalize 강제(My_Homenet.c CTRL_POWER 주석 250~253) → NORMAL 푸시 전에 플래그 제거 → 룸컨이 옛 모드(예: 공청) 계속 표시.
- 둘 다 도착순서 의존 → 간헐적.

**Why:** 전원 ON 간헐 미동작 / 전원 OFF 후 룸컨 옛모드 표시의 근본 원인. 2026-06-18 수정.

**How to apply:** 룸컨에 반드시 보내야 하는 대시보드 명령은 공유 `Command_request_type` 대신 **룸컨 전용 pending `RoomCtrl_Push`**(My_RJ2.c 전역)로 표시한다. 분배기가 못 건드리고 RJ2 ack(SEND_FLAG echo, EVENT L399/CONTROLL L691)로만 해제. RJ2 NORMAL 푸시조건(L332)에 `|| RoomCtrl_Push`, echo else 가드(L409/698)에 `!RoomCtrl_Push &&` 추가됨. 모드/풍량도 동기 누락 보이면 `CTRL_RUNMODE/CTRL_FAN`에 `RoomCtrl_Push=1` 적용. 별개 잠재버그: `TYPE_POWER==TYPE_HOOD_STATE==0x40` 비트 충돌(빈 비트 0x08 분리 권장). 관련 [[internal-damper-fan-gate]], [[herv-master-source]].


File created successfully at: d:\project\nuvoton\HECO2\doc\claude-memory\command-request-type-shared-race.md (file state is current in your context — no need to Read it back)

---
name: command-request-type-shared-race
description: ERV의 Command_request_type은 룸컨(RJ2)·분배기(bunbagi) 공유 플래그라 대시보드 명령 푸시에 레이스가 있음
metadata:
  node_type: memory
  type: project
---

HECO2 ERV 펌웨어에서 `Command_request_type`(My_RJ2.c 전역, My_define.h TYPE_* 비트)은 **룸컨(RJ2, SC0)과 각실분배기(My_bunbaegi.c) 두 통신 소비자가 공유**한다. 대시보드 명령(전원/모드/풍량)이 이 플래그를 set 하면 두 버스가 각자 소비·클리어하는데:
- `My_bunbaegi.c:522` 가 프레임 송신 후 `Command_request_type = 0;` 로 **전체 wipe** → 분배기가 먼저 돌면 RJ2가 플래그를 못 봐 룸컨 푸시 유실.
- `My_RJ2.c:715~716` 이 `Set==현재` 면 TYPE_MODE/FAN 클리어. 전원 OFF는 모터정지 위해 `Set_Run=Run=VENT, Set_Fan=Fan=0` equalize 강제(My_Homenet.c CTRL_POWER 주석 250~253) → NORMAL 푸시 전에 플래그 제거 → 룸컨이 옛 모드(예: 공청) 계속 표시.
- 둘 다 도착순서 의존 → 간헐적.

**Why:** 전원 ON 간헐 미동작 / 전원 OFF 후 룸컨 옛모드 표시의 근본 원인. 2026-06-18 수정.

**How to apply:** 룸컨에 반드시 보내야 하는 대시보드 명령은 공유 `Command_request_type` 대신 **룸컨 전용 pending `RoomCtrl_Push`**(My_RJ2.c 전역)로 표시한다. 분배기가 못 건드리고 RJ2 ack(SEND_FLAG echo, EVENT L399/CONTROLL L691)로만 해제. RJ2 NORMAL 푸시조건(L332)에 `|| RoomCtrl_Push`, echo else 가드(L409/698)에 `!RoomCtrl_Push &&` 추가됨. 모드/풍량도 동기 누락 보이면 `CTRL_RUNMODE/CTRL_FAN`에 `RoomCtrl_Push=1` 적용. 별개 잠재버그: `TYPE_POWER==TYPE_HOOD_STATE==0x40` 비트 충돌(빈 비트 0x08 분리 권장). 관련 [[internal-damper-fan-gate]], [[herv-master-source]].


Edit

- [내부댐퍼 팬 게이트](internal-damper-fan-gate.md) — 본체 댐퍼는 팬이 0까지 내려가야 모드별 이동, 명령경로 끼어들면 미동작

- [내부댐퍼 팬 게이트](internal-damper-fan-gate.md) — 본체 댐퍼는 팬이 0까지 내려가야 모드별 이동, 명령경로 끼어들면 미동작
- [Command_request_type 공유 레이스](command-request-type-shared-race.md) — 룸컨·분배기 공유 플래그 → 전원 푸시 유실, RoomCtrl_Push 전용 플래그로 해결


The file d:\project\nuvoton\HECO2\doc\claude-memory\MEMORY.md has been updated successfully. (file state is current in your context — no need to Read it back)

- [내부댐퍼 팬 게이트](internal-damper-fan-gate.md) — 본체 댐퍼는 팬이 0까지 내려가야 모드별 이동, 명령경로 끼어들면 미동작
- [Command_request_type 공유 레이스](command-request-type-shared-race.md) — 룸컨·분배기 공유 플래그 → 전원 푸시 유실, RoomCtrl_Push 전용 플래그로 해결

# Memory Index

- [HERV 마스터 소스](herv-master-source.md) — 검증된 펌웨어 = D:\Project\nuvoton\HERV_DL_MH_2nd\Program, 댐퍼/팬 회귀 비교 기준
- [내부댐퍼 팬 게이트](internal-damper-fan-gate.md) — 본체 댐퍼는 팬이 0까지 내려가야 모드별 이동, 명령경로 끼어들면 미동작

+- [Command_request_type 공유 레이스](command-request-type-shared-race.md) — 룸컨·분배기 공유 플래그 → 전원 푸시 유실, RoomCtrl_Push 전용 플래그로 해결


수정 완료했습니다.

## 근본 원인 — 공유 플래그 레이스

`Command_request_type`은 **룸컨(RJ2)과 각실분배기(bunbagi) 두 통신 소비자가 공유**하는 플래그였습니다:

1. **분배기 wipe** — [My_bunbaegi.c:522](SOURCE/HECO2/User/My_bunbaegi.c#L522)가 송신 후 `Command_request_type = 0;`으로 전체를 지움 → 분배기가 먼저 처리하면 RJ2가 플래그를 못 봐 룸컨 푸시 유실.
2. **716 equalize 클리어** — 전원 OFF는 모터 정지를 위해 `Set==현재`로 equalize 강제 → [My_RJ2.c:715~716](SOURCE/HECO2/User/My_RJ2.c#L715)이 NORMAL 푸시 전에 TYPE_MODE/FAN을 지움 → 룸컨이 옛 모드(공청) 계속 표시.

둘 다 도착 순서에 의존 → 간헐적. (부수발견: `TYPE_POWER == TYPE_HOOD_STATE == 0x40` 비트 충돌도 있음 — 이번엔 별도 변수로 우회.)

## 수정 — 룸컨 전용 pending 플래그 `RoomCtrl_Push`

| 파일 | 변경 |
|------|------|
| My_define.h | `extern uint8_t RoomCtrl_Push;` |
| My_RJ2.c | 전역 정의 + NORMAL 푸시조건에 `\|\| RoomCtrl_Push` + echo else 가드에 `!RoomCtrl_Push &&` + ack 2곳에서 `RoomCtrl_Push=0` |
| My_Homenet.c | CTRL_POWER에 `RoomCtrl_Push=1` (ON de-equalize는 원복) |

**효과:** 분배기 wipe·716 클리어와 무관하게 전원 ON/OFF가 룸컨에 반드시 푸시되고(전원 OFF=VENT/0 → 공청 해제), 룸컨이 명령을 확인(echo/ack)하면 자동 해제됩니다. 평상시 룸컨 패널 직접 조작 반영은 그대로입니다.

**빌드:** 경고/오류 0 (text 43972 → 44024).

⚠ **이번 수정은 핵심 통신 상태머신(RJ2)을 건드렸으니 실장비 검증이 꼭 필요합니다:**
- 전원 ON/OFF 반복 시 룸컨이 매번 동기되는지 (특히 공청→OFF)
- 룸컨 패널 직접 조작 / 후드연동 / 분배기 동작에 회귀 없는지

확인 부탁드립니다. 모드/풍량 변경에서도 같은 누락이 보이면 동일하게 `RoomCtrl_Push=1`을 적용할 수 있습니다.


잘되는 것 같아.
그리고, 전원 off를 하면 풍량 0도 비활성화 시켜 줘.


전원 ON/OFF 잘 된다니 다행입니다. 이제 전원 OFF 시 풍량 0 버튼도 비활성화하는 건 대시보드 수정입니다. 현재 풍량 버튼 활성 로직을 확인하겠습니다.


47-        };
48-
49:        readonly List<Button> _fanButtons = new();
50-        readonly List<Button> _modeButtons = new();
51-        readonly List<Button> _presetButtons = new();
52-
53-        HystWindow? _hystWin;
54-        VspWindow? _vspWin;
55-        SmartSleepWindow? _sleepWin;
--
107-        void BuildFanButtons()
108-        {
109:            _fanButtons.AddRange(new[] { Fan0, Fan1, Fan2, Fan3, Fan4 });
110-        }
111-
112-        void BuildModeButtons()
113-        {
114-            _modeButtons.AddRange(new[] { ModeVent, ModeAuto, ModeAir, ModeBypass });
115-        }
--
523-            bool makeupActive = _state.ComfortCook && _state.HoodRunning;
524-            bool subActive = _state.SmartSleep || makeupActive || _state.ReliefRecover;
525:            int fanMax = _state.RunMode == RunMode.Bypass ? 1 : 4;
526:            foreach (var fb in _fanButtons)
527-            {
528-                int sp = (int)fb.Tag!;
529:                fb.IsEnabled = !subActive && !_state.IsAuto && sp <= fanMax;
530-                SetActive(fb, sp == _state.FanMode);
531-            }
532-
533-            // 시나리오모드
534-            SetActive(SmartSleepBtn,    _state.SmartSleep);
535-            // 쾌적조리는 사양 9p 3.1 의 'UI 토글(연동 스위치)' — 후드 가동중이 아니라 토글 ON/OFF(=Hood_YeunDong_Enable)를 강조.


풍량 버튼 활성 조건에 전원 체크가 없어, 전원 OFF여도 풍량 0~4 버튼이 클릭 가능한 상태입니다. **전원 OFF면 모든 풍량 버튼(0 포함) 비활성**으로 바꾸겠습니다.


                int sp = (int)fb.Tag!;
                fb.IsEnabled = !subActive && !_state.IsAuto && sp <= fanMax;
                SetActive(fb, sp == _state.FanMode);

                int sp = (int)fb.Tag!;
                // 전원 OFF면 풍량 조절 불가 → 0 포함 전 단 비활성
                fb.IsEnabled = _state.PowerOn && !subActive && !_state.IsAuto && sp <= fanMax;
                SetActive(fb, sp == _state.FanMode);


The file d:\project\nuvoton\HECO2\TestProgram\PCDashBoard\MainWindow.xaml.cs has been updated successfully. (file state is current in your context — no need to Read it back)

                int sp = (int)fb.Tag!;
                fb.IsEnabled = !subActive && !_state.IsAuto && sp <= fanMax;
                SetActive(fb, sp == _state.FanMode);

                int sp = (int)fb.Tag!;
                // 전원 OFF면 풍량 조절 불가 → 0 포함 전 단 비활성
                fb.IsEnabled = _state.PowerOn && !subActive && !_state.IsAuto && sp <= fanMax;
                SetActive(fb, sp == _state.FanMode);

-                fb.IsEnabled = !subActive && !_state.IsAuto && sp <= fanMax;

+                // 전원 OFF면 풍량 조절 불가 → 0 포함 전 단 비활성

+                fb.IsEnabled = _state.PowerOn && !subActive && !_state.IsAuto && sp <= fanMax;


PowerShell

dotnet publish "d:\project\nuvoton\HECO2\TestProgram\PCDashBoard\ErvDashboard.csproj" -c Release 2>&1 | Select-String -Pattern "error|ErvDashboard ->" | Select-Object -Last 5

Publish dashboard after fan button disable on power off


Error: Exit code 1
C:\Program Files\dotnet\sdk\10.0.300\Sdks\Microsoft.NET.Sdk.WindowsDesktop\targets\Microsoft.WinFX.targets(707,5): erro
r BG1002: 'd:\project\nuvoton\HECO2\TestProgram\PCDashBoard\obj\Release\net10.0-windows\win-x64\SmartSleepWindow.baml'
파일을 찾을 수 없습니다. [d:\project\nuvoton\HECO2\TestProgram\PCDashBoard\ErvDashboard.csproj]
C:\Program Files\dotnet\sdk\10.0.300\Sdks\Microsoft.NET.Sdk.WindowsDesktop\targets\Microsoft.WinFX.targets(707,5): erro
r BG1002: 'd:\project\nuvoton\HECO2\TestProgram\PCDashBoard\obj\Release\net10.0-windows\win-x64\VspWindow.baml' 파일을 찾을
수 없습니다. [d:\project\nuvoton\HECO2\TestProgram\PCDashBoard\ErvDashboard.csproj]


잘되는 것 같아. 그리고, 전원 off를 하면 풍량 0도 비활성화 시켜 줘.


BAML 캐시 오류(stale obj)입니다. clean 후 다시 빌드하겠습니다.


Remove-Item -Recurse -Force "d:\project\nuvoton\HECO2\TestProgram\PCDashBoard\obj","d:\project\nuvoton\HECO2\TestProgram\PCDashBoard\bin" -ErrorAction SilentlyContinue; dotnet publish "d:\project\nuvoton\HECO2\TestProgram\PCDashBoard\ErvDashboard.csproj" -c Release 2>&1 | Select-String -Pattern "error|ErvDashboard ->" | Select-Object -Last 6

Clean obj/bin and re-publish dashboard


  ErvDashboard -> d:\project\nuvoton\HECO2\TestProgram\PCDashBoard\bin\Release\net10.0-windows\win-x64\ErvDashboard.dll
  ErvDashboard -> d:\project\nuvoton\HECO2\TestProgram\PCDashBoard\bin\Release\net10.0-windows\win-x64\publish\


clean 후 publish 성공 (오류 0). 문서에 기록합니다.


Edit

- 모드/풍량 변경(CTRL_RUNMODE/CTRL_FAN)도 룸컨 동기 누락이 보이면 동일하게 `RoomCtrl_Push = 1;` 적용 가능.

- 모드/풍량 변경(CTRL_RUNMODE/CTRL_FAN)도 룸컨 동기 누락이 보이면 동일하게 `RoomCtrl_Push = 1;` 적용 가능.
- **검증 완료**: 전원 ON/OFF 시 룸컨 동기 정상(사장님 확인 2026-06-18).

---

# 260618 (13) 전원 OFF 시 풍량 버튼(0 포함) 비활성화

## 요청
- 전원 OFF 하면 풍량 0 버튼도 비활성화.

## 변경
- `TestProgram/PCDashBoard/MainWindow.xaml.cs` 풍량 버튼 활성 조건:
  - `fb.IsEnabled = !subActive && !_state.IsAuto && sp <= fanMax;`
  - → `fb.IsEnabled = _state.PowerOn && !subActive && !_state.IsAuto && sp <= fanMax;`
  - 전원 OFF면 0~4 전 단 비활성(풍량 조절 불가). 운전모드 버튼은 그대로(전원 OFF에서 모드 누르면 전원 ON).

## 빌드/배포
- `dotnet publish ErvDashboard.csproj -c Release` → 오류 0. (BAML stale 캐시로 1회 실패 → obj/bin clean 후 성공.) publish 갱신.


The file d:\project\nuvoton\HECO2\doc\260618_내부댐퍼_모드변경시_미동작_수정.md has been updated successfully. (file state is current in your context — no need to Read it back)

- 모드/풍량 변경(CTRL_RUNMODE/CTRL_FAN)도 룸컨 동기 누락이 보이면 동일하게 `RoomCtrl_Push = 1;` 적용 가능.
- **검증 완료**: 전원 ON/OFF 시 룸컨 동기 정상(사장님 확인 2026-06-18).

---

# 260618 (13) 전원 OFF 시 풍량 버튼(0 포함) 비활성화

## 요청
- 전원 OFF 하면 풍량 0 버튼도 비활성화.

## 변경
- `TestProgram/PCDashBoard/MainWindow.xaml.cs` 풍량 버튼 활성 조건:
  - `fb.IsEnabled = !subActive && !_state.IsAuto && sp <= fanMax;`
  - → `fb.IsEnabled = _state.PowerOn && !subActive && !_state.IsAuto && sp <= fanMax;`
  - 전원 OFF면 0~4 전 단 비활성(풍량 조절 불가). 운전모드 버튼은 그대로(전원 OFF에서 모드 누르면 전원 ON).

## 빌드/배포
- `dotnet publish ErvDashboard.csproj -c Release` → 오류 0. (BAML stale 캐시로 1회 실패 → obj/bin clean 후 성공.) publish 갱신.

- **실장비 검증 필수** : 핵심 통신 상태머신(RJ2) 변경. 전원 ON/OFF 반복 시 룸컨이 매번 동기되는지, 룸컨 패널 직접 조작·후드연동·분배기 동작에 회귀 없는지 확인.

- `TYPE_POWER`/`TYPE_HOOD_STATE` 0x40 비트 충돌은 미해결(빈 비트 0x08 로 분리 권장). 이번 수정과 독립.

+- **검증 완료**: 전원 ON/OFF 시 룸컨 동기 정상(사장님 확인 2026-06-18).

+# 260618 (13) 전원 OFF 시 풍량 버튼(0 포함) 비활성화

+- 전원 OFF 하면 풍량 0 버튼도 비활성화.

+- `TestProgram/PCDashBoard/MainWindow.xaml.cs` 풍량 버튼 활성 조건:

+  - `fb.IsEnabled = !subActive && !_state.IsAuto && sp <= fanMax;`

+  - → `fb.IsEnabled = _state.PowerOn && !subActive && !_state.IsAuto && sp <= fanMax;`

+  - 전원 OFF면 0~4 전 단 비활성(풍량 조절 불가). 운전모드 버튼은 그대로(전원 OFF에서 모드 누르면 전원 ON).

+- `dotnet publish ErvDashboard.csproj -c Release` → 오류 0. (BAML stale 캐시로 1회 실패 → obj/bin clean 후 성공.) publish 갱신.


수정 완료했습니다.

## 변경 — 전원 OFF 시 풍량 버튼 비활성

[MainWindow.xaml.cs:529](TestProgram/PCDashBoard/MainWindow.xaml.cs#L529):

```csharp
// 변경 전
fb.IsEnabled = !subActive && !_state.IsAuto && sp <= fanMax;
// 변경 후 — 전원 OFF면 0 포함 전 단 비활성
fb.IsEnabled = _state.PowerOn && !subActive && !_state.IsAuto && sp <= fanMax;
```

- 전원 OFF → 풍량 0~4 **모두 비활성**(풍량 조절 불가).
- 운전모드 버튼은 그대로 둠 (전원 OFF에서 모드를 누르면 전원이 켜지므로).

**빌드:** dotnet publish 오류 0 (BAML 캐시 문제로 1회 실패 → obj/bin clean 후 성공), 단일 exe 갱신.

오늘 작업이 꽤 쌓였습니다 (펌웨어 (6)~(12), 대시보드 여러 건). 변경분을 **커밋**해 둘까요? 원하시면 오늘 작업을 정리한 커밋 메시지로 묶겠습니다.


그리고, 대시보드 그래프 db 파일은 어디에 저장돼?


그래프 DB 경로는 코드에 명시돼 있습니다. [MainWindow.xaml.cs:64](TestProgram/PCDashBoard/MainWindow.xaml.cs#L64):

```csharp
readonly Storage.LogDb _logDb = new(System.IO.Path.Combine(AppContext.BaseDirectory, "HERV_Log.db"));
```

즉 **`HERV_Log.db`** (SQLite) 파일이 **실행 파일(ErvDashboard.exe)이 있는 폴더**에 생성됩니다 (`AppContext.BaseDirectory` = exe 위치).

publish 빌드 기준 실제 경로:

```
TestProgram\PCDashBoard\bin\Release\net10.0-windows\win-x64\publish\HERV_Log.db
```

실제로 있는지 확인해 보겠습니다.


PowerShell

Get-ChildItem -Path "d:\project\nuvoton\HECO2\TestProgram\PCDashBoard\bin" -Recurse -Filter "HERV_Log.db*" -ErrorAction SilentlyContinue | Select-Object LastWriteTime, @{n='MB';e={[math]::Round($_.Length/1MB,2)}}, FullName | Format-Table -AutoSize -Wrap

Find HERV_Log.db files


(PowerShell completed with no output)


실행하면서 저장하지?


DB 파일이 아직 안 보이는 건 publish된 exe를 그 폴더에서 아직 실행하지 않았기 때문입니다. "실행하면서 저장하지?" — 네, 맞습니다. 정확히 확인하겠습니다.


63:        readonly DispatcherTimer _sampleTimer = new() { Interval = TimeSpan.FromSeconds(5) };
64-        GraphWindow? _graphWin;
65-
66-        public MainWindow()
67-        {
68-            InitializeComponent();
69-            DataContext = _state;
70-
71-            RoomItems.ItemsSource = _state.Rooms;
--
98:            _sampleTimer.Tick += (_, _) => TakeSample();
99:            _sampleTimer.Start();
100-
101-            RefreshPortsList();
102-            RefreshControls();
103-        }
104-
105-        // ================= 버튼 목록 등록 (UI 는 MainWindow.xaml 에 정의) =================
106-        // 강조/활성 갱신(RefreshControls)을 위해 XAML 버튼을 리스트로 모은다. 순서는 ModeDefs 와 동일.
107-        void BuildFanButtons()
--
175:        void TakeSample()
176-        {
177-            var rooms = _state.Rooms;
178-            var rs = new Model.RoomSample[rooms.Count];
179-            for (int i = 0; i < rooms.Count; i++)
180-            {
181-                var r = rooms[i];
182-                rs[i] = new Model.RoomSample
183-                {
--
201:            try { _logDb.Insert(sample); }
202-            catch (Exception ex) { Log($"[로그DB] 저장 실패: {ex.Message}"); }
203-            _graphWin?.OnSampleAdded(sample);
204-        }
205-
206-        void OpenGraph_Click(object sender, RoutedEventArgs e)
207-        {
208-            var names = new System.Collections.Generic.List<string>();
209-            foreach (var r in _state.Rooms) names.Add(r.Name);
--
636:            _logDb.Dispose();
637-            base.OnClosed(e);
638-        }
639-    }
640-
641-    // ================= 컨버터 =================
642-    public class AirQualityToBrushConverter : IValueConverter
643-    {
644-        static readonly Brush Red    = (Brush)new BrushConverter().ConvertFromString("#EF4444")!;


네, 맞습니다. **대시보드 실행 중 5초마다 자동 저장**됩니다.

흐름:
- 생성자에서 `_sampleTimer`(5초 주기) 시작 → [TakeSample()](TestProgram/PCDashBoard/MainWindow.xaml.cs#L175) 호출
- 매 틱마다 현재 상태를 모아 [_logDb.Insert(sample)](TestProgram/PCDashBoard/MainWindow.xaml.cs#L201)로 SQLite에 append (무제한 누적)

정리:
- **파일:** `HERV_Log.db` (SQLite), exe와 같은 폴더에 생성
- **저장 시점:** 앱 실행 직후부터 5초 간격으로 계속 (앱 종료 시 `_logDb.Dispose()`)
- **DB가 아직 안 보이는 이유:** publish된 exe를 그 publish 폴더에서 아직 실행 안 했기 때문. 처음 실행하면 첫 5초 틱에서 `...\publish\HERV_Log.db`가 생성됩니다.

참고로 한 가지 — `_sampleTimer`는 **연결 여부와 무관하게** 앱 시작 시 바로 돌기 때문에, 장비에 연결 안 된 상태에서도 (기본/빈 상태값이) 5초마다 계속 쌓입니다. 연결됐을 때만 저장하도록 바꾸길 원하시면 말씀해 주세요.


지금 publish 폴더에서 실행했는데 db파일이 없어.


publish 폴더에서 실행했는데 DB가 없다면, 단일 exe(PublishSingleFile)에서 `AppContext.BaseDirectory`가 다른 곳을 가리키거나 SQLite 저장이 조용히 실패하는 경우입니다. `LogDb` 구현을 보고, 디스크 전체에서 파일을 찾아보겠습니다.


Get-ChildItem -Path "C:\","D:\" -Recurse -Filter "HERV_Log.db" -ErrorAction SilentlyContinue -Force | Select-Object LastWriteTime, FullName | Format-Table -AutoSize -Wrap

Search entire disk for HERV_Log.db


Command running in background with ID: b7x7nf76w. Output is being written to: C:\Users\user\AppData\Local\Temp\claude\d--project-nuvoton-HECO2\d2979ea2-c54f-4168-a02b-739afadaece5\tasks\b7x7nf76w.output. You will be notified when it completes. To check interim output, use Read on that file path.


1	using System;
2	using System.Collections.Generic;
3	using Microsoft.Data.Sqlite;
4	using ErvDashboard.Model;
5
6	namespace ErvDashboard.Storage
7	{
8	    // 로그 시계열 실시간 저장 (SQLite 단일파일, 무제한 누적).
9	    //  - sample      : 5초 1행 (시간·전원·운전/풍량·시나리오)
10	    //  - room_sample : 실별 댐퍼/센서 (sample 당 N행)
11	    // 그래프는 LoadRecent() 로 최근 구간을 읽어 표시, 이후 실시간 append.
12	    public sealed class LogDb : IDisposable
13	    {
14	        readonly SqliteConnection _conn;
15
16	        public LogDb(string path)
17	        {
18	            _conn = new SqliteConnection($"Data Source={path}");
19	            _conn.Open();
20	            Exec("PRAGMA journal_mode=WAL;");      // 동시 읽기/쓰기 성능
21	            Exec("PRAGMA synchronous=NORMAL;");
22	            Exec(@"CREATE TABLE IF NOT EXISTS sample(
23	                       id      INTEGER PRIMARY KEY AUTOINCREMENT,
24	                       time    INTEGER NOT NULL,   -- DateTime.Ticks
25	                       power   INTEGER, runmode INTEGER, automode INTEGER, fanmode INTEGER,
26	                       sleep   INTEGER, cook    INTEGER, recover INTEGER, hystpreset INTEGER);");
27	            // 구버전 DB 마이그레이션 — 이미 있으면 무시
28	            try { Exec("ALTER TABLE sample ADD COLUMN automode INTEGER DEFAULT 0;"); } catch { }
29	            try { Exec("ALTER TABLE sample ADD COLUMN hystpreset INTEGER DEFAULT 0;"); } catch { }
30	            Exec(@"CREATE TABLE IF NOT EXISTS room_sample(
31	                       sample_id INTEGER NOT NULL,
32	                       room_idx  INTEGER NOT NULL,
33	                       damper_sa INTEGER, damper_ra INTEGER,
34	                       co2 INTEGER, pm25 INTEGER, pm10 INTEGER, voc INTEGER,
35	                       temp INTEGER, humi INTEGER, led INTEGER, level INTEGER,
36	                       PRIMARY KEY(sample_id, room_idx));");
37	        }
38
39	        void Exec(string sql)
40	        {
41	            using var cmd = _conn.CreateCommand();
42	            cmd.CommandText = sql;
43	            cmd.ExecuteNonQuery();
44	        }
45
46	        // 1샘플 실시간 저장 (sample + room_sample 트랜잭션 1회).
47	        public void Insert(LogSample s)
48	        {
49	            using var tx = _conn.BeginTransaction();
50
51	            long id;
52	            using (var cmd = _conn.CreateCommand())
53	            {
54	                cmd.Transaction = tx;
55	                cmd.CommandText =
56	                    "INSERT INTO sample(time,power,runmode,automode,fanmode,sleep,cook,recover,hystpreset) " +
57	                    "VALUES($t,$p,$rm,$am,$fm,$sl,$ck,$rc,$hp); SELECT last_insert_rowid();";
58	                cmd.Parameters.AddWithValue("$t", s.Time.Ticks);
59	                cmd.Parameters.AddWithValue("$p", s.Power ? 1 : 0);
60	                cmd.Parameters.AddWithValue("$rm", (int)s.RunMode);
61	                cmd.Parameters.AddWithValue("$am", (int)s.AutoMode);
62	                cmd.Parameters.AddWithValue("$hp", (int)s.HystPreset);
63	                cmd.Parameters.AddWithValue("$fm", (int)s.FanMode);
64	                cmd.Parameters.AddWithValue("$sl", s.SmartSleep ? 1 : 0);
65	                cmd.Parameters.AddWithValue("$ck", s.ComfortCook ? 1 : 0);
66	                cmd.Parameters.AddWithValue("$rc", s.ReliefRecover ? 1 : 0);
67	                id = (long)(cmd.ExecuteScalar() ?? 0L);
68	            }
69
70	            using (var cmd = _conn.CreateCommand())
71	            {
72	                cmd.Transaction = tx;
73	                cmd.CommandText =
74	                    "INSERT INTO room_sample(sample_id,room_idx,damper_sa,damper_ra,co2,pm25,pm10,voc,temp,humi,led,level) " +
75	                    "VALUES($sid,$ri,$dsa,$dra,$co2,$pm25,$pm10,$voc,$tp,$hm,$led,$lv);";
76	                var pSid = cmd.Parameters.Add("$sid", SqliteType.Integer);
77	                var pRi  = cmd.Parameters.Add("$ri",  SqliteType.Integer);
78	                var pDsa = cmd.Parameters.Add("$dsa", SqliteType.Integer);
79	                var pDra = cmd.Parameters.Add("$dra", SqliteType.Integer);
80	                var pCo2 = cmd.Parameters.Add("$co2", SqliteType.Integer);
81	                var pP25 = cmd.Parameters.Add("$pm25", SqliteType.Integer);
82	                var pP10 = cmd.Parameters.Add("$pm10", SqliteType.Integer);
83	                var pVoc = cmd.Parameters.Add("$voc", SqliteType.Integer);
84	                var pTp  = cmd.Parameters.Add("$tp",  SqliteType.Integer);
85	                var pHm  = cmd.Parameters.Add("$hm",  SqliteType.Integer);
86	                var pLed = cmd.Parameters.Add("$led", SqliteType.Integer);
87	                var pLv  = cmd.Parameters.Add("$lv",  SqliteType.Integer);
88	                pSid.Value = id;
89	                for (int i = 0; i < s.Rooms.Length; i++)
90	                {
91	                    var rm = s.Rooms[i];
92	                    pRi.Value = i;
93	                    pDsa.Value = rm.DamperSa ? 1 : 0;
94	                    pDra.Value = rm.DamperRa ? 1 : 0;
95	                    pCo2.Value = rm.Co2; pP25.Value = rm.Pm25; pP10.Value = rm.Pm10; pVoc.Value = rm.Voc;
96	                    pTp.Value = rm.Temp; pHm.Value = rm.Humi; pLed.Value = rm.Led; pLv.Value = rm.Level;
97	                    cmd.ExecuteNonQuery();
98	                }
99	            }
100
101	            tx.Commit();
102	        }
103
104	        // 최근 max개 샘플을 시간 오름차순으로 로드 (그래프 표시용).
105	        public List<LogSample> LoadRecent(int max)
106	        {
107	            using var cmd = _conn.CreateCommand();
108	            cmd.CommandText =
109	                "SELECT id,time,power,runmode,automode,fanmode,sleep,cook,recover,hystpreset FROM " +
110	                "(SELECT * FROM sample ORDER BY id DESC LIMIT $n) ORDER BY id ASC;";
111	            cmd.Parameters.AddWithValue("$n", max);
112	            return Materialize(cmd);
113	        }
114
115	        // 지정 날짜(그날 00:00 ~ 다음날 00:00, 로컬)의 샘플을 시간 오름차순으로 로드.
116	        public List<LogSample> LoadByDate(DateTime day)
117	        {
118	            using var cmd = _conn.CreateCommand();
119	            cmd.CommandText =
120	                "SELECT id,time,power,runmode,automode,fanmode,sleep,cook,recover,hystpreset " +
121	                "FROM sample WHERE time >= $s AND time < $e ORDER BY id ASC;";
122	            cmd.Parameters.AddWithValue("$s", day.Date.Ticks);
123	            cmd.Parameters.AddWithValue("$e", day.Date.AddDays(1).Ticks);
124	            return Materialize(cmd);
125	        }
126
127	        // 데이터가 존재하는 날짜 목록(오름차순) — 상단 날짜 선택 UI용.
128	        public List<DateTime> LoadDates()
129	        {
130	            var list = new List<DateTime>();
131	            using var cmd = _conn.CreateCommand();
132	            // Ticks/일(864e9)로 버킷팅 → 날짜(자정) 환산
133	            cmd.CommandText = "SELECT DISTINCT time/864000000000 FROM sample ORDER BY 1;";
134	            using var rd = cmd.ExecuteReader();
135	            while (rd.Read()) list.Add(new DateTime(rd.GetInt64(0) * TimeSpan.TicksPerDay));
136	            return list;
137	        }
138
139	        // sample 행을 읽고 room_sample 을 붙여 LogSample 리스트로 구성 (시간 오름차순).
140	        List<LogSample> Materialize(SqliteCommand sampleCmd)
141	        {
142	            var byId = new Dictionary<long, LogSample>();
143	            var order = new List<long>();
144	            using (var rd = sampleCmd.ExecuteReader())
145	            {
146	                while (rd.Read())
147	                {
148	                    long id = rd.GetInt64(0);
149	                    byId[id] = new LogSample
150	                    {
151	                        Time = new DateTime(rd.GetInt64(1)),
152	                        Power = rd.GetInt32(2) != 0,
153	                        RunMode = (byte)rd.GetInt32(3),
154	                        AutoMode = (byte)(rd.IsDBNull(4) ? 0 : rd.GetInt32(4)),
155	                        FanMode = (byte)rd.GetInt32(5),
156	                        SmartSleep = rd.GetInt32(6) != 0,
157	                        ComfortCook = rd.GetInt32(7) != 0,
158	                        ReliefRecover = rd.GetInt32(8) != 0,
159	                        HystPreset = (byte)(rd.IsDBNull(9) ? 0 : rd.GetInt32(9)),
160	                    };
161	                    order.Add(id);
162	                }
163	            }
164	            if (order.Count == 0) return new List<LogSample>();
165	            AttachRooms(byId, order);
166	            var result = new List<LogSample>(order.Count);
167	            foreach (var id in order) result.Add(byId[id]);
168	            return result;
169	        }
170
171	        // order(오름차순 id) 범위의 room_sample 을 읽어 각 LogSample.Rooms 채움.
172	        void AttachRooms(Dictionary<long, LogSample> byId, List<long> order)
173	        {
174	            var rooms = new Dictionary<long, List<RoomSample>>();
175	            using var cmd = _conn.CreateCommand();
176	            cmd.CommandText =
177	                "SELECT sample_id,room_idx,damper_sa,damper_ra,co2,pm25,pm10,voc,temp,humi,led,level " +
178	                "FROM room_sample WHERE sample_id BETWEEN $min AND $max ORDER BY sample_id, room_idx;";
179	            cmd.Parameters.AddWithValue("$min", order[0]);
180	            cmd.Parameters.AddWithValue("$max", order[order.Count - 1]);
181	            using var rd = cmd.ExecuteReader();
182	            while (rd.Read())
183	            {
184	                long sid = rd.GetInt64(0);
185	                if (!byId.ContainsKey(sid)) continue;
186	                if (!rooms.TryGetValue(sid, out var lst)) { lst = new List<RoomSample>(); rooms[sid] = lst; }
187	                lst.Add(new RoomSample
188	                {
189	                    DamperSa = rd.GetInt32(2) != 0,
190	                    DamperRa = rd.GetInt32(3) != 0,
191	                    Co2 = rd.GetInt32(4), Pm25 = rd.GetInt32(5), Pm10 = rd.GetInt32(6), Voc = rd.GetInt32(7),
192	                    Temp = rd.GetInt32(8), Humi = rd.GetInt32(9), Led = rd.GetInt32(10), Level = rd.GetInt32(11),
193	                });
194	            }
195	            foreach (var id in order)
196	                if (rooms.TryGetValue(id, out var lst)) byId[id].Rooms = lst.ToArray();
197	        }
198
199	        public void Dispose() => _conn.Dispose();
200	    }
201	}
202

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();
    }
}


`LogDb` 생성자가 `SqliteConnection.Open()` 시 파일을 자동 생성하므로(기본 ReadWriteCreate), 앱이 켜지기만 하면 `HERV_Log.db`가 생겨야 정상입니다. 단일 exe에서 `AppContext.BaseDirectory`가 임시 추출 폴더를 가리켰을 가능성이 있어, 디스크 전체 검색 결과를 확인하겠습니다.


1
2


검색이 진행 중입니다. 그동안 csproj의 publish 설정(단일 exe 추출 방식)을 확인하겠습니다.


1	<Project Sdk="Microsoft.NET.Sdk">
2
3	  <PropertyGroup>
4	    <OutputType>WinExe</OutputType>
5	    <TargetFramework>net10.0-windows</TargetFramework>
6	    <UseWPF>true</UseWPF>
7	    <Nullable>enable</Nullable>
8	    <ImplicitUsings>enable</ImplicitUsings>
9	    <RootNamespace>ErvDashboard</RootNamespace>
10	    <AssemblyName>ErvDashboard</AssemblyName>
11	    <StartupObject>ErvDashboard.App</StartupObject>
12	    <RuntimeIdentifier>win-x64</RuntimeIdentifier>
13	    <SelfContained>true</SelfContained>
14	    <PublishSingleFile>true</PublishSingleFile>
15	    <IncludeNativeLibrariesForSelfExtract>true</IncludeNativeLibrariesForSelfExtract>
16	    <IncludeAllContentForSelfExtract>true</IncludeAllContentForSelfExtract>
17	  </PropertyGroup>
18
19	  <ItemGroup>
20	    <PackageReference Include="Microsoft.Data.Sqlite" Version="10.0.0" />
21	    <PackageReference Include="LiveChartsCore.SkiaSharpView.WPF" Version="2.1.0-dev-798" />
22	    <PackageReference Include="System.IO.Ports" Version="10.0.5" />
23	  </ItemGroup>
24
25	  <ItemGroup>
26	    <!-- 공용 프로토콜 라이브러리 (단일 진실원본) -->
27	    <ProjectReference Include="..\ErvProtocol\ErvProtocol.csproj" />
28	  </ItemGroup>
29
30	</Project>
31

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>WinExe</OutputType>
    <TargetFramework>net10.0-windows</TargetFramework>
    <UseWPF>true</UseWPF>
    <Nullable>enable</Nullable>
    <ImplicitUsings>enable</ImplicitUsings>
    <RootNamespace>ErvDashboard</RootNamespace>
    <AssemblyName>ErvDashboard</AssemblyName>
    <StartupObject>ErvDashboard.App</StartupObject>
    <RuntimeIdentifier>win-x64</RuntimeIdentifier>
    <SelfContained>true</SelfContained>
    <PublishSingleFile>true</PublishSingleFile>
    <IncludeNativeLibrariesForSelfExtract>true</IncludeNativeLibrariesForSelfExtract>
    <IncludeAllContentForSelfExtract>true</IncludeAllContentForSelfExtract>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.Data.Sqlite" Version="10.0.0" />
    <PackageReference Include="LiveChartsCore.SkiaSharpView.WPF" Version="2.1.0-dev-798" />
    <PackageReference Include="System.IO.Ports" Version="10.0.5" />
  </ItemGroup>

  <ItemGroup>
    <!-- 공용 프로토콜 라이브러리 (단일 진실원본) -->
    <ProjectReference Include="..\ErvProtocol\ErvProtocol.csproj" />
  </ItemGroup>

</Project>


원인을 찾았습니다. csproj 16번 줄 **`<IncludeAllContentForSelfExtract>true</IncludeAllContentForSelfExtract>`** 때문입니다.

이 옵션이 켜지면 단일 exe가 실행 시 **모든 콘텐츠를 임시 폴더에 추출**해서 거기서 돌고, 그 결과 **`AppContext.BaseDirectory`가 publish 폴더가 아니라 임시 추출 폴더**를 가리킵니다. 그래서 `HERV_Log.db`가 `%TEMP%\.net\ErvDashboard\<해시>\` 같은 임시 폴더에 생기고 있어 publish 폴더엔 안 보이는 겁니다.

디스크 검색으로 확인하겠습니다.


Wasted call — file unchanged since your last Read. Refer to that earlier tool_result instead.


검색은 시간이 걸리니 기다리지 않고, 진단이 확실하므로 수정하겠습니다. DB를 **실제 exe 폴더**(`Environment.ProcessPath` 기준)에 저장하도록 바꾸면 추출 모드와 무관하게 publish 폴더에 생깁니다. 현재 라인을 확인합니다.


62:        readonly Storage.LogDb _logDb = new(System.IO.Path.Combine(AppContext.BaseDirectory, "HERV_Log.db"));


Edit

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);


        // 단일 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 _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) };


DB를 실제 exe 폴더에 저장하도록 수정했습니다. 빌드/publish 합니다 (BAML 캐시 회피 위해 clean 포함).


PowerShell

Remove-Item -Recurse -Force "d:\project\nuvoton\HECO2\TestProgram\PCDashBoard\obj","d:\project\nuvoton\HECO2\TestProgram\PCDashBoard\bin" -ErrorAction SilentlyContinue; dotnet publish "d:\project\nuvoton\HECO2\TestProgram\PCDashBoard\ErvDashboard.csproj" -c Release 2>&1 | Select-String -Pattern "error|ErvDashboard ->" | Select-Object -Last 6

Clean and publish dashboard after DB path fix


<task-notification>
<task-id>b7x7nf76w</task-id>
<tool-use-id>toolu_01WboPqVdHEqveNktYcQtQTR</tool-use-id>
<output-file>C:\Users\user\AppData\Local\Temp\claude\d--project-nuvoton-HECO2\d2979ea2-c54f-4168-a02b-739afadaece5\tasks\b7x7nf76w.output</output-file>
<status>failed</status>
<summary>Background command "Search entire disk for HERV_Log.db" failed with exit code 1</summary>
</task-notification>


Publish 성공. 디스크 검색 결과로 진단을 확인하겠습니다.


1
2	LastWriteTime          FullName
3	-------------          --------
4	2026-06-15 오전 1:23:44  C:\$Recycle.Bin\S-1-5-21-1552625420-3898252012-4045764816-1001\$R1QBBFG\PCDashBoard\bin\Debug\ne
5	                       t10.0-windows\win-x64\HERV_Log.db
6	2026-06-13 오전 8:58:50  C:\$Recycle.Bin\S-1-5-21-1552625420-3898252012-4045764816-1001\$R1QBBFG\PCDashBoard\bin\Release\
7	                       net10.0-windows\win-x64\HERV_Log.db
8	2026-06-14 오후 9:20:35  C:\Users\user\AppData\Local\Temp\.net\ErvDashboard\7bLVOpf0ilOs\HERV_Log.db
9	2026-06-14 오후 5:18:57  C:\Users\user\AppData\Local\Temp\.net\ErvDashboard\cOE2U0tbECFO\HERV_Log.db
10	2026-06-14 오후 5:07:30  C:\Users\user\AppData\Local\Temp\.net\ErvDashboard\JZZf-rSYhTFe\HERV_Log.db
11	2026-06-18 오후 11:29:01 C:\Users\user\AppData\Local\Temp\.net\ErvDashboard\ndOixJZ5tSMz\HERV_Log.db
12	2026-06-14 오후 8:59:53  C:\Users\user\AppData\Local\Temp\.net\ErvDashboard\x0b19r-biB5-\HERV_Log.db
13
14
15

LastWriteTime          FullName
-------------          --------
2026-06-15 오전 1:23:44  C:\$Recycle.Bin\S-1-5-21-1552625420-3898252012-4045764816-1001\$R1QBBFG\PCDashBoard\bin\Debug\ne
                       t10.0-windows\win-x64\HERV_Log.db
2026-06-13 오전 8:58:50  C:\$Recycle.Bin\S-1-5-21-1552625420-3898252012-4045764816-1001\$R1QBBFG\PCDashBoard\bin\Release\
                       net10.0-windows\win-x64\HERV_Log.db
2026-06-14 오후 9:20:35  C:\Users\user\AppData\Local\Temp\.net\ErvDashboard\7bLVOpf0ilOs\HERV_Log.db
2026-06-14 오후 5:18:57  C:\Users\user\AppData\Local\Temp\.net\ErvDashboard\cOE2U0tbECFO\HERV_Log.db
2026-06-14 오후 5:07:30  C:\Users\user\AppData\Local\Temp\.net\ErvDashboard\JZZf-rSYhTFe\HERV_Log.db
2026-06-18 오후 11:29:01 C:\Users\user\AppData\Local\Temp\.net\ErvDashboard\ndOixJZ5tSMz\HERV_Log.db
2026-06-14 오후 8:59:53  C:\Users\user\AppData\Local\Temp\.net\ErvDashboard\x0b19r-biB5-\HERV_Log.db