/* ============================================================================= * My_Homenet.c — HOMENET(UART1, 115200 N81) <-> ErvDashboard 바이너리 프로토콜 * 규격 : TestProgram/PC_ERV_Protocol.md * (My_Uart.c 에서 분리 — PC 대시보드 통신 전용. 공용 전역/CRC16은 My_bunbaeggi.c 정의, My_define.h extern) * ===========================================================================*/ #include #include #include #include #include "Nano100Series.h" #include "adc.h" #include "gpio.h" #include "pwm.h" #include "timer.h" #include "uart.h" #include "sys.h" #include "clk.h" #include "My_define.h" /* ============================================================================ * HOMENET (UART1, 115200 N81) <-> ErvDashboard 바이너리 프로토콜 (Rev2.0) * 규격 : TestProgram/PC_ERV_Protocol.md * 프레임 : STX(0xAA) | CMD | DATA[240] | CRC_L | CRC_H (항상 244B 고정) * CRC-16/MODBUS (CMD~DATA[240]=241B), 리틀엔디안. 멀티바이트 값은 빅엔디안. * PC->ERV : 명령 인자는 DATA 앞쪽, 나머지 0 패딩 * ERV->PC : STATUS(0x81) DATA=240B 상태 / ACK(0x82) DATA[0]=echoCmd DATA[1]=result * ==========================================================================*/ #define HN_STX 0xAA #define HN_STATUS 0x81 #define HN_ACK 0x82 #define HN_DATA_LEN 240 /* 고정 DATA 크기 */ #define HN_STATUS_LEN 240 /* 238 + 2((꺼짐)예약 잔여초 u16) */ /* ---- 수신 파서 상태 (ISR 컨텍스트) ---- Hn_rx[0]=CMD, [1..240]=DATA ---- */ static uint8_t Hn_step = 0, Hn_pos = 0, Hn_crc_lo = 0; static uint8_t Hn_rx[1 + HN_DATA_LEN]; /* ---- 처리 대기 명령 (메인 컨텍스트로 전달) ---- */ volatile uint8_t Hn_cmd_ready = 0; static uint8_t Hn_cmd = 0, Hn_cmd_pl[HN_DATA_LEN]; /* PC대시보드 제어 → 룸컨(RJ2) 상태 푸시 래치 (My_RJ2.c roomcon_parsing 에서 소비/클리어) */ volatile uint8_t Homenet_RJ_Request = 0; uint8_t Homenet_Reset_State = 0; void Homenet_Rx_Byte(uint8_t b) { switch(Hn_step) { case 0: if(b == HN_STX) Hn_step = 1; break; case 1: Hn_rx[0] = b; Hn_pos = 0; Hn_step = 2; break; /* CMD */ case 2: /* DATA[240] */ Hn_rx[1 + Hn_pos] = b; if(++Hn_pos >= HN_DATA_LEN) Hn_step = 3; break; case 3: Hn_crc_lo = b; Hn_step = 4; break; /* 첫 CRC 바이트(표준 하위) */ case 4: { /* 수신 바이트(하위먼저) -> CRC16() 반환형식(바이트스왑)으로 재구성하여 비교 */ uint16_t rxcrc = ((uint16_t)Hn_crc_lo << 8) | b; uint16_t i; /* CRC 대상 = CMD + DATA[240] = Hn_rx[0..240] (241B) */ if((CRC16(Hn_rx, (uint16_t)(1 + HN_DATA_LEN)) == rxcrc) && (Hn_cmd_ready == 0)) { Hn_cmd = Hn_rx[0]; for(i = 0; i < HN_DATA_LEN; i++) Hn_cmd_pl[i] = Hn_rx[1 + i]; Hn_cmd_ready = 1; } Hn_step = 0; break; } default: Hn_step = 0; break; } } /* 고정 244B 송신 : STX | CMD | DATA[240] | CRC_L | CRC_H. payload는 len만큼 채우고 나머지 0 패딩 */ static void Homenet_Send_Frame(uint8_t cmd, uint8_t *payload, uint16_t len) { static uint8_t buf[1 + 1 + HN_DATA_LEN + 2]; /* 244 */ uint16_t crc; uint16_t n = 0; uint16_t i; buf[n++] = HN_STX; buf[n++] = cmd; for(i = 0; i < HN_DATA_LEN; i++) buf[n++] = (i < len) ? payload[i] : 0; crc = CRC16(&buf[1], (uint16_t)(1 + HN_DATA_LEN)); /* CMD+DATA[240]=241B. CRC16()는 표준 MODBUS값의 바이트스왑 반환 */ buf[n++] = (uint8_t)(crc >> 8); /* CRC_L (표준 하위바이트) - bunbaegi 와 동일 순서 */ buf[n++] = (uint8_t)(crc & 0xFF); /* CRC_H (표준 상위바이트) */ HOMENET_485_DIR = 1; UART_Write(UART1, buf, n); while(!(UART1->FSR & UART_FSR_TX_EMPTY_F_Msk)); while(!(UART1->FSR & UART_FSR_TE_F_Msk)); HOMENET_485_DIR = 0; } static void hn_w16(uint8_t *p, uint16_t v){ p[0] = (uint8_t)(v >> 8); p[1] = (uint8_t)(v & 0xFF); } static uint8_t hn_runmode_code(void) { if(Power_On != 1) return 0x00; switch(Run_Mode) { case MODE_VENTILATION: return 0x01; case MODE_AUTO: return 0x02; case MODE_AIRCLEAN: return 0x03; case MODE_BYPASS: return 0x04; default: return 0x01; } } /* Room_Level(0좋음~4매우나쁨) -> 프로토콜 공기질코드(1매우나쁨~4좋음) */ static uint8_t hn_airq_code(uint8_t level) { switch(level){ case 0: return 4; case 1: return 3; case 2: return 2; default: return 1; } } void Homenet_Build_Status(uint8_t *p) { uint8_t r; uint16_t i; for(i = 0; i < HN_STATUS_LEN; i++) p[i] = 0; /* --- 글로벌 0~16 --- */ p[0] = Power_On; p[1] = hn_runmode_code(); p[2] = Auto_Concentrate; /* 0 분산 / 1 집중 */ p[3] = Fan_Mode; p[4] = (uint8_t)(((Ext_Run_Mode == 4) ? 0x01 : 0) /* 스마트수면 */ | (Hood_YeunDong_Enable ? 0x02 : 0) /* 쾌적조리 */ | ((Ext_Run_Mode == 1) ? 0x04 : 0)); /* 안심회복 */ /* bit0 연동Enable / bit1 연동운전중 / bit2 후드 통신연결(폴 응답 생존) */ p[5] = (uint8_t)((Hood_YeunDong_Enable ? 0x01 : 0) | ((Hood_Status != 0) ? 0x02 : 0) | ((Hood_Conn_Timeout != 0) ? 0x04 : 0)); p[6] = Hyst_Preset; hn_w16(&p[7], Pm25_Db[Hyst_Preset]); hn_w16(&p[9], Pm10_Db[Hyst_Preset]); hn_w16(&p[11], Voc_Db[Hyst_Preset]); hn_w16(&p[13], Co2_Db[Hyst_Preset]); hn_w16(&p[15], (uint16_t)Err_Code); /* --- 각실 17~ (14B x 4) : 거실=1, 침1=2, 침2=3, 침3=4 --- */ for(r = 0; r < 4; r++) { uint8_t room = (uint8_t)(r + 1); uint8_t o = (uint8_t)(17 + r * 14); /* damper 비트맵 : bit0=급기(SA) 열림 / bit1=배기(RA/EA) 열림 */ p[o+0] = (uint8_t)((Diffuser_Dmp_Ang_SA[room] != 0 ? 0x01 : 0) | (Diffuser_Dmp_Ang_RA[room] != 0 ? 0x02 : 0)); hn_w16(&p[o+1], SEN66_pm2p5[room]); hn_w16(&p[o+3], SEN66_pm10p0[room]); hn_w16(&p[o+5], (uint16_t)SEN66_VOC_value[room]); hn_w16(&p[o+7], SEN66_CO2_value[room]); p[o+9] = hn_airq_code(Room_Level[room]); p[o+10] = Light_Bright[room]; hn_w16(&p[o+11], (uint16_t)Room_Level[room]); /* 각실 부하점수 = 실 Level */ p[o+13] = Fan_Mode; /* 최종 풍량(전체 단수) */ } /* --- ERV 리셋 73 --- */ p[73] = Homenet_Reset_State; /* --- VSP 74~109 : 환기1~4, 바이패스, 공청1~4 (각 SA,EA u16 BE) --- */ hn_w16(&p[74], s_FAN1_VEN_1_DAN); hn_w16(&p[76], s_FAN2_VEN_1_DAN); hn_w16(&p[78], s_FAN1_VEN_2_DAN); hn_w16(&p[80], s_FAN2_VEN_2_DAN); hn_w16(&p[82], s_FAN1_VEN_3_DAN); hn_w16(&p[84], s_FAN2_VEN_3_DAN); hn_w16(&p[86], s_FAN1_VEN_4_DAN); hn_w16(&p[88], s_FAN2_VEN_4_DAN); hn_w16(&p[90], s_FAN1_BYPASS_1_DAN); hn_w16(&p[92], s_FAN2_BYPASS_1_DAN); hn_w16(&p[94], s_FAN1_AIR_1_DAN); hn_w16(&p[96], s_FAN2_AIR_1_DAN); hn_w16(&p[98], s_FAN1_AIR_2_DAN); hn_w16(&p[100], s_FAN2_AIR_2_DAN); hn_w16(&p[102], s_FAN1_AIR_3_DAN); hn_w16(&p[104], s_FAN2_AIR_3_DAN); hn_w16(&p[106], s_FAN1_AIR_4_DAN); hn_w16(&p[108], s_FAN2_AIR_4_DAN); /* --- 히스테리시스 데드밴드 테이블 110~133 : ECO/NORMAL/TURBO x (PM2.5,PM10,VOC,CO2) --- */ for(r = 0; r < 3; r++) { uint8_t o = (uint8_t)(110 + r * 8); hn_w16(&p[o+0], Pm25_Db[r]); hn_w16(&p[o+2], Pm10_Db[r]); hn_w16(&p[o+4], Voc_Db[r]); hn_w16(&p[o+6], Co2_Db[r]); } /* --- 모드별 오염단계 임계표 134~229 : 3프리셋 x [CO2,PM2.5,PM10,VOC] x L1~L4 --- */ for(r = 0; r < 3; r++) { int o = 134 + r * 32; uint8_t k; for(k = 0; k < 4; k++) hn_w16(&p[o + 0 + k*2], Co2_Thr[r][k]); for(k = 0; k < 4; k++) hn_w16(&p[o + 8 + k*2], Pm25_Thr[r][k]); for(k = 0; k < 4; k++) hn_w16(&p[o + 16 + k*2], Pm10_Thr[r][k]); for(k = 0; k < 4; k++) hn_w16(&p[o + 24 + k*2], Voc_Thr[r][k]); } /* --- 각실 온도/습도 230~237 : 4실 x [Temp, Humi] (디퓨저 SEN66 값, 0~255 클램프) --- */ for(r = 0; r < 4; r++) { uint8_t room = (uint8_t)(r + 1); uint8_t o = (uint8_t)(230 + r * 2); int16_t t = SEN66_temperature_value[room]; int16_t h = SEN66_humidity_value[room]; p[o+0] = (uint8_t)((t < 0) ? 0 : (t > 255) ? 255 : t); p[o+1] = (uint8_t)((h < 0) ? 0 : (h > 255) ? 255 : h); } /* --- (꺼짐)예약 잔여초 238~239 (u16 BE) --- */ hn_w16(&p[238], Reserve_Remain_Sec); } static uint8_t hn_set_vsp(uint8_t grp, uint8_t idx, uint8_t sa, uint8_t ea) { if(grp == 0) /* 환기 idx 1~4 */ { switch(idx){ case 1: s_FAN1_VEN_1_DAN=sa; s_FAN2_VEN_1_DAN=ea; break; case 2: s_FAN1_VEN_2_DAN=sa; s_FAN2_VEN_2_DAN=ea; break; case 3: s_FAN1_VEN_3_DAN=sa; s_FAN2_VEN_3_DAN=ea; break; case 4: s_FAN1_VEN_4_DAN=sa; s_FAN2_VEN_4_DAN=ea; break; default: return 1; } } else if(grp == 1) /* 바이패스 idx 1 */ { if(idx == 1){ s_FAN1_BYPASS_1_DAN=sa; s_FAN2_BYPASS_1_DAN=ea; } else return 1; } else if(grp == 2) /* 공청 idx 1~4 */ { switch(idx){ case 1: s_FAN1_AIR_1_DAN=sa; s_FAN2_AIR_1_DAN=ea; break; case 2: s_FAN1_AIR_2_DAN=sa; s_FAN2_AIR_2_DAN=ea; break; case 3: s_FAN1_AIR_3_DAN=sa; s_FAN2_AIR_3_DAN=ea; break; case 4: s_FAN1_AIR_4_DAN=sa; s_FAN2_AIR_4_DAN=ea; break; default: return 1; } } else return 1; return 0; } /* 수신 제어명령 적용 + ACK. result 0=OK / 1=ERR */ static void hn_apply_cmd(uint8_t cmd, uint8_t *pl, uint8_t len) { uint8_t result = 0, i; uint8_t ack[2]; switch(cmd) { case 0x01: /* CTRL_POWER [onoff] */ if(len >= 1) { Power_On = pl[0] ? 1 : 0; for(i = 1; i < 7; i++) Diffuser_Power[i] = Power_On; if(Power_On == 0){ Set_Run_Mode = 0; Set_Fan_Mode = 0; } else { /* 전원 ON : 환기 모드 + 풍량 1단 (ERVSimulator HomeNetProtocol 와 동일) */ Set_Run_Mode = Run_Mode = MODE_VENTILATION; Set_Fan_Mode = Fan_Mode = 1; } /* 전원 토글 시 수동 댐퍼/LED 해제 → 자동 추종 복귀 */ for(i = 1; i <= 4; i++) { Diffuser_Damper_Manual[i] = 0; Diffuser_Led_Manual[i] = 0; } /* 각실분배기 : 전원 비트(TYPE_POWER) + 모드/풍량. 룸컨(RJ2) : 별도 래치로 확실히 푸시 */ Command_request_type |= (TYPE_POWER | TYPE_MODE | TYPE_FAN_SPEED); Homenet_RJ_Request |= (TYPE_MODE | TYPE_FAN_SPEED); } else result = 1; break; case 0x02: /* CTRL_RUNMODE [mode] 0off/1환기/2자동/3공청/4바이패스 */ if(len >= 1) { if(pl[0] == 0) Power_On = 0; else { Power_On = 1; switch(pl[0]) { case 1: Set_Run_Mode = MODE_VENTILATION; break; case 2: Set_Run_Mode = MODE_AUTO; break; case 3: Set_Run_Mode = MODE_AIRCLEAN; break; case 4: Set_Run_Mode = MODE_BYPASS; break; default: result = 1; break; } if(result == 0) { Run_Mode = Set_Run_Mode; Command_request_type |= TYPE_MODE; Homenet_RJ_Request |= TYPE_MODE; /* 룸컨이 새 모드 덮어쓰지 않게 푸시 */ /* 운전모드 전환 시 풍량 1단 (자동 제외 - 자동은 부하점수로 결정) */ if(Set_Run_Mode != MODE_AUTO) { Set_Fan_Mode = Fan_Mode = 1; Command_request_type |= TYPE_FAN_SPEED; Homenet_RJ_Request |= TYPE_FAN_SPEED; } } } /* 모드 전환 시 수동 댐퍼 해제 → 새 모드는 기본(전실 개방)에서 시작 */ for(i = 1; i <= 4; i++) Diffuser_Damper_Manual[i] = 0; } else result = 1; break; case 0x03: /* CTRL_FAN [speed] */ if(len >= 1) { Set_Fan_Mode = Fan_Mode = pl[0]; Command_request_type |= TYPE_FAN_SPEED; Homenet_RJ_Request |= TYPE_FAN_SPEED; /* 룸컨이 새 풍량 덮어쓰지 않게 푸시 */ if(Run_Mode != MODE_AUTO) Fan_Speed_Setting(Run_Mode, Fan_Mode); /* 즉시 반영(자동은 부하점수로 결정) */ } else result = 1; break; case 0x04: /* CTRL_SUBMODE [type][onoff] 1수면/2조리/3회복 */ if(len >= 2) { if(pl[0] == 1) Ext_Run_Mode = pl[1] ? 4 : 0; else if(pl[0] == 2) Hood_YeunDong_Enable = pl[1] ? 1 : 0; else if(pl[0] == 3){ Ext_Run_Mode = pl[1] ? 1 : 0; Ext_Select_Room = 2; } else result = 1; /* 부가모드 변경 시 수동 댐퍼 해제 (수동 댐퍼는 환기/공청/바이패스에서만) */ if(result == 0) for(i = 1; i <= 4; i++) Diffuser_Damper_Manual[i] = 0; } else result = 1; break; case 0x05: /* CTRL_HOOD [onoff] */ if(len >= 1) Hood_YeunDong_Enable = pl[0] ? 1 : 0; else result = 1; break; case 0x06: /* CTRL_HYST_PRESET [preset] */ if(len >= 1 && pl[0] < 3) Hyst_Preset = pl[0]; else result = 1; break; case 0x07: /* CTRL_HYST_VALUE [preset][pm25][pm10][voc][co2] u16 BE */ if(len >= 9 && pl[0] < 3) { uint8_t ps = pl[0]; Pm25_Db[ps] = (uint16_t)(((uint16_t)pl[1] << 8) | pl[2]); Pm10_Db[ps] = (uint16_t)(((uint16_t)pl[3] << 8) | pl[4]); Voc_Db[ps] = (uint16_t)(((uint16_t)pl[5] << 8) | pl[6]); Co2_Db[ps] = (uint16_t)(((uint16_t)pl[7] << 8) | pl[8]); } else result = 1; break; case 0x08: /* CTRL_DAMPER [room][type 0급기SA/1배기EA][onoff] — 수동 댐퍼(비자동에서 위치 유지) */ if(len >= 3 && pl[0] >= 1 && pl[0] <= 4) { uint8_t ang = pl[2] ? 110 : 0; if(pl[1] == 0) Memory_Diffuser_Dmp_Ang_SA[pl[0]] = Diffuser_Dmp_Ang_SA[pl[0]] = ang; /* 급기 */ else if(pl[1] == 1) Memory_Diffuser_Dmp_Ang_RA[pl[0]] = Diffuser_Dmp_Ang_RA[pl[0]] = ang; /* 배기 */ else result = 1; if(result == 0) Diffuser_Damper_Manual[pl[0]] = 1; /* 자동로직 덮어쓰기 차단(자동/모드전환 시 해제) */ } else result = 1; break; case 0x09: /* CTRL_LED [room][dim] — 수동 LED : 자동 추종 해제하고 지정값 유지(모든 모드, 전원OFF만 해제) */ if(len >= 2 && pl[0] >= 1 && pl[0] <= 4) { Light_Bright[pl[0]] = pl[1]; Diffuser_Led_Manual[pl[0]] = 1; } else result = 1; break; case 0x0A: /* REQ_STATUS : 응답은 호출부에서 */ break; case 0x0B: /* CTRL_RESET [onoff] */ if(len >= 1) Homenet_Reset_State = pl[0] ? 1 : 0; else result = 1; break; case 0x0C: /* CTRL_VSP [group][index][sa(2)][ea(2)] */ if(len >= 6) result = hn_set_vsp(pl[0], pl[1], (uint8_t)(((uint16_t)pl[2] << 8) | pl[3]), (uint8_t)(((uint16_t)pl[4] << 8) | pl[5])); else result = 1; break; case 0x0D: /* CTRL_HYST_THR [preset][pollutant 0CO2/1PM2.5/2PM10/3VOC][L1~L4 u16] */ if(len >= 10 && pl[0] < 3 && pl[1] < 4) { uint8_t ps = pl[0], g = pl[1], k; uint16_t *arr = 0; if(g == 0) arr = Co2_Thr[ps]; else if(g == 1) arr = Pm25_Thr[ps]; else if(g == 2) arr = Pm10_Thr[ps]; else if(g == 3) arr = Voc_Thr[ps]; if(arr) { for(k = 0; k < 4; k++) arr[k] = (uint16_t)(((uint16_t)pl[2 + k*2] << 8) | pl[3 + k*2]); } else result = 1; } else result = 1; break; case 0x0E: /* CTRL_RESERVE [hours 0~8] : N시간 후 전원 OFF (0=해제) */ if(len >= 1 && pl[0] <= 8) Reserve_Remain_Sec = (uint16_t)(pl[0] * 3600); else result = 1; break; default: result = 1; break; } /* 히스테리시스 프리셋/데드밴드/임계, VSP 변경은 EEPROM 영속화 */ if(result == 0 && (cmd == 0x06 || cmd == 0x07 || cmd == 0x0C || cmd == 0x0D)) EEP_Save_Flag = 1; ack[0] = cmd; ack[1] = result; Homenet_Send_Frame(HN_ACK, ack, 2); } /* 메인 루프(빠른 주기)에서 호출 : 수신 명령 처리 + REQ_STATUS 즉시 응답 */ void Homenet_Process(void) { static uint8_t status_buf[HN_STATUS_LEN]; if(Hn_cmd_ready) { uint8_t cmd = Hn_cmd, pl[HN_DATA_LEN]; uint16_t i; for(i = 0; i < HN_DATA_LEN; i++) pl[i] = Hn_cmd_pl[i]; Hn_cmd_ready = 0; hn_apply_cmd(cmd, pl, HN_DATA_LEN); if(cmd == 0x0A) /* REQ_STATUS */ { Homenet_Build_Status(status_buf); Homenet_Send_Frame(HN_STATUS, status_buf, HN_STATUS_LEN); } } } /* 1초 주기 STATUS 자동 송신 */ void Homenet_Send_Status(void) { static uint8_t status_buf[HN_STATUS_LEN]; Homenet_Build_Status(status_buf); Homenet_Send_Frame(HN_STATUS, status_buf, HN_STATUS_LEN); } void UART1_HANDLE() { uint8_t u8InChar=0xFF; uint32_t u32IntSts= UART1->ISR; if(u32IntSts & UART_ISR_RDA_IS_Msk) { u8InChar = UART_READ(UART1); /* Rx trigger level is 1 byte*/ Homenet_Rx_Byte(u8InChar); // HOMENET(ErvDashboard) 바이너리 프레임 수신 } if(u32IntSts & UART_ISR_THRE_IS_Msk) { ; } } void UART1_IRQHandler(void) { UART1_HANDLE(); }