YOGYUI

힐스테이트 광교산::도시가스차단기(밸브) 제어 RS-485 패킷 분석 본문

홈네트워크(IoT)/힐스테이트 광교산

힐스테이트 광교산::도시가스차단기(밸브) 제어 RS-485 패킷 분석

요겨 2022. 6. 14. 22:44
반응형

지난번 월패드의 조명 관련 RS-485 포트를 후킹하여 집안의 조명 제어아울렛(콘센트) 제어를 Homebridge 및 Home Assistant와 연동하여 애플 및 안드로이드 모바일 기기를 통해 제어할 수 있게 만들었다

이제 조명 말고 다른 RS-485 포트도 후킹해볼 차례~

1. RS-485포트 연결

터미널 블록의 조명 RS-485 포트 왼쪽에 '가스', '일괄', 그리고 라벨이 붙여지지 않은 2개의 케이블의 선이 다발로 묶여서 연결되어 있는데, 선의 색(청색과 흰색+청색)이 조명과 동일하길래 RS-485 결선을 똑같이 해서 USB to RS485 컨버터 하나를 더 달아봤다

※ 가스 외에 다양한 디바이스를 이 포트로 제어할 수 있는듯?

 

갈수록 보기 싫어지고 있지만 뭐... 아직은 개발단계니 ㅎㅎ

결국 소형 PCB 보드 하나 만들긴 만들어야겠다는 생각이 점점 들고있다 (ESP32 기반으로다가...)

 

2. 시리얼 패킷 후킹

조명때와 마찬가지로 보레이드 9600으로 연결했더니 다음과 같은 바이트스트림이 흘러들어왔다


F7 0D
01 34 01 41 10 00 00 00 00 9F EE
F7 0B
01 34 04 41 10 00 00 9C EE
F7 0D 01 48 01
40 10 00 71 11 02 80 EE
F7 0D 01 48 04
40 10 00 71 11 02 85 EE
F7 0E 01 2A 01 40
10 00 19 00 1B 03 82 EE
F7 0E 01 2A 04
40 10 00 19 02 1B 03 85 EE
F7 0B 01 2B 01
40 11 00 00 86 EE
F7 0C 01 2B
04 40 11 00 02 00 86 EE
F7 0B 01 18 01 46 10 00 00 B2 EE
F7 22 01 18 04 46 10 00 04 19 15 04 19 17
04 19 16 04 19 16 00 00 00 00 00 00 00 00 00
00 00 00 9C EE


조명과 마찬가지로 0xF7과 0xEE가 반복되어 나타나니 동일하게 패킷 파싱 구문을 만들 수 있다

class SerialParser:
    __metaclass__ = ABCMeta

    buffer: bytearray
    enable_console_log: bool = False
    chunk_cnt: int = 0
    max_chunk_cnt: int = 1e6
    max_buffer_size: int = 200

    def __init__(self, ser: SerialComm):
        self.buffer = bytearray()
        self.sig_parse_result = Callback(dict)
        ser.sig_send_data.connect(self.onSendData)
        ser.sig_recv_data.connect(self.onRecvData)
        self.serial = ser

    def release(self):
        self.buffer.clear()

    def sendPacket(self, packet: Union[bytes, bytearray]):
        self.serial.sendData(packet)

    def sendString(self, packet_str: str):
        self.serial.sendData(bytearray([int(x, 16) for x in packet_str.split(' ')]))

    def onSendData(self, data: bytes):
        msg = ' '.join(['%02X' % x for x in data])
        writeLog("Send >> {}".format(msg), self)

    def onRecvData(self, data: bytes):
        if len(self.buffer) > self.max_buffer_size:
            self.buffer.clear()
        self.buffer.extend(data)
        self.handlePacket()

    def handlePacket(self):
        idx = self.buffer.find(0xF7)
        if idx > 0:
            self.buffer = self.buffer[idx:]
        if len(self.buffer) >= 3:
            packet_length = self.buffer[1]
            if len(self.buffer) >= packet_length:
                if self.buffer[0] == 0xF7 and self.buffer[packet_length - 1] == 0xEE:
                    packet = self.buffer[:packet_length]
                    try:
                        checksum_calc = self.calcXORChecksum(packet[:-2])
                        checksum_recv = packet[-2]
                        if checksum_calc == checksum_recv:
                            self.interpretPacket(packet)
                        else:
                            writeLog('Checksum Error (calc={}, recv={}) ({})'.format(
                                checksum_calc, checksum_recv, self.prettifyPacket(packet)), self)
                        self.buffer = self.buffer[packet_length:]
                    except IndexError:
                        writeLog('Index Error (buffer={}, packet_len={}, packet={})'.format(
                            self.prettifyPacket(self.buffer), packet_length, self.prettifyPacket(packet)), self)

조명과 가스(+기타) 시리얼 바이트스트림의 파싱 구문은 동일하게 하고, 패킷 해석 구문만 SerialParser를 상속받는 자식 객체에서 구현하는 방향으로 코드 구조를 변경했다

 

이제 0xF7, 0xEE 기반으로 구분한 패킷을 한줄한줄 기록해보자

class ParserGas(SerialParser):    
    def interpretPacket(self, packet: bytearray):
        print(self.prettifyPacket(packet))

패킷 기록


F7 0D 01 34 01 41 10 00 00 00 00 9F EE
F7 0B 01 34 04 41 10 00 00 9C EE
F7 0D 01 48 01 40 10 00 71 11 02 80 EE
F7 0D 01 48 04 40 10 00 71 11 02 85 EE
F7 0E 01 2A 01 40 10 00 19 00 1B 03 82 EE
F7 0E 01 2A 04 40 10 00 19 02 1B 03 85 EE
F7 0B 01 18 01 46 10 00 00 B2 EE
F7 22 01 18 04 46 10 00 04 18 15 04 19 17 04 19 16 04 19 16 00 00 00 00 00 00 00 00 00 00 00 00 9D EE
F7 0B 01 18 01 46 10 00 00 B2 EE
F7 22 01 18 04 46 10 00 04 18 15 04 19 17 04 19 16 04 19 16 00 00 00 00 00 00 00 00 00 00 00 00 9D EE
F7 0B 01 18 01 46 10 00 00 B2 EE
F7 22 01 18 04 46 10 00 04 18 15 04 19 17 04 19 16 04 19 16 00 00 00 00 00 00 00 00 00 00 00 00 9D EE
F7 0B 01 18 01 46 10 00 00 B2 EE
F7 22 01 18 04 46 10 00 04 18 15 04 19 17 04 19 16 04 19 16 00 00 00 00 00 00 00 00 00 00 00 00 9D EE
F7 0B 01 1C 01 40 11 00 00 B1 EE
F7 0F 01 1C 04 40 11 00 02 18 17 01 03 BF EE
F7 0B 01 1C 01 40 21 00 00 81 EE
F7 0F 01 1C 04 40 21 00 02 19 17 01 01 8C EE
F7 0B 01 1C 01 40 31 00 00 91 EE
F7 0F 01 1C 04 40 31 00 02 19 17 01 02 9F EE
F7 0B 01 1C 01 40 41 00 00 E1 EE
F7 0F 01 1C 04 40 41 00 02 19 12 01 04 EC EE
F7 0B 01 1B 01 43 11 00 00 B5 EE
F7 0D 01 1B 04 43 11 00 03 00 00 B5 EE
F7 0B 01 18 01 46 10 00 00 B2 EE
F7 22 01 18 04 46 10 00 04 18 15 04 19 17 04 19 16 04 19 16 00 00 00 00 00 00 00 00 00 00 00 00 9D EE
F7 0D 01 34 01 41 10 00 00 00 00 9F EE
F7 0B 01 34 04 41 10 00 00 9C EE
F7 0D 01 48 01 40 10 00 71 11 02 80 EE
F7 0D 01 48 04 40 10 00 71 11 02 85 EE
F7 0E 01 2A 01 40 10 00 19 00 1B 03 82 EE
F7 0E 01 2A 04 40 10 00 19 02 1B 03 85 EE
F7 0B 01 2B 01 40 11 00 00 86 EE
F7 0C 01 2B 04 40 11 00 02 00 86 EE
F7 0B 01 18 01 46 10 00 00 B2 EE
F7 22 01 18 04 46 10 00 04 18 15 04 19 17 04 19 16 04 19 16 00 00 00 00 00 00 00 00 00 00 00 00 9D EE
F7 0B 01 1C 01 40 11 00 00 B1 EE
F7 0F 01 1C 04 40 11 00 02 18 17 01 03 BF EE
F7 0B 01 1C 01 40 21 00 00 81 EE
F7 0F 01 1C 04 40 21 00 02 19 17 01 01 8C EE
F7 0B 01 1C 01 40 31 00 00 91 EE
F7 0F 01 1C 04 40 31 00 02 19 17 01 02 9F EE
F7 0B 01 1C 01 40 41 00 00 E1 EE
F7 0F 01 1C 04 40 41 00 02 19 12 01 04 EC EE
F7 0B 01 1B 01 43 11 00 00 B5 EE
F7 0D 01 1B 04 43 11 00 03 00 00 B5 EE
F7 0B 01 18 01 46 10 00 00 B2 EE
F7 22 01 18 04 46 10 00 04 18 15 04 19 17 04 19 16 04 19 16 00 00 00 00 00 00 00 00 00 00 00 00 9D EE
F7 0D 01 34 01 41 10 00 00 00 00 9F EE
F7 0B 01 34 04 41 10 00 00 9C EE
F7 0D 01 48 01 40 10 00 71 11 02 80 EE
F7 0D 01 48 04 40 10 00 71 11 02 85 EE
F7 0E 01 2A 01 40 10 00 19 00 1B 03 82 EE
F7 0E 01 2A 04 40 10 00 19 02 1B 03 85 EE
F7 0B 01 18 01 46 10 00 00 B2 EE
F7 22 01 18 04 46 10 00 04 18 15 04 19 17 04 19 16 04 19 16 00 00 00 00 00 00 00 00 00 00 00 00 9D EE
F7 0B 01 1C 01 40 11 00 00 B1 EE
F7 0F 01 1C 04 40 11 00 02 18 17 01 03 BF EE
F7 0B 01 1C 01 40 21 00 00 81 EE
F7 0F 01 1C 04 40 21 00 02 19 17 01 01 8C EE
F7 0B 01 1C 01 40 31 00 00 91 EE
F7 0F 01 1C 04 40 31 00 02 19 17 01 02 9F EE
F7 0B 01 1C 01 40 41 00 00 E1 EE
F7 0F 01 1C 04 40 41 00 02 19 12 01 04 EC EE
F7 0B 01 2B 01 40 11 00 00 86 EE
F7 0C 01 2B 04 40 11 00 02 00 86 EE
F7 0B 01 1B 01 43 11 00 00 B5 EE
F7 0D 01 1B 04 43 11 00 03 00 00 B5 EE
F7 0B 01 18 01 46 10 00 00 B2 EE
F7 22 01 18 04 46 10 00 04 18 15 04 19 17 04 19 16 04 19 16 00 00 00 00 00 00 00 00 00 00 00 00 9D EE
F7 0D 01 34 01 41 10 00 00 00 00 9F EE
F7 0B 01 34 04 41 10 00 00 9C EE
F7 0D 01 48 01 40 10 00 71 11 02 80 EE
F7 0D 01 48 04 40 10 00 71 11 02 85 EE


패킷 구조는 조명 포트와 동일한 것으로 보인다 (두번째 바이트가 패킷의 길이, 다섯번째 바이트가 0x01이면 쿼리, 0x04이면 응답 패킷인 것 등등)

제어하고자 하는 디바이스의 아이디를 가리키는 네번째 바이트는 조명의 경우 0x19, 아울렛의 경우 0x1F였다

 

가스가 속해있는 이 RS-485 포트에는 네번째 바이트가

0x18, 0x1B, 0x1C, 0x2A, 0x2B, 0x34, 0x48 

총 7개 종류가 반복되어 조회-응답이 반복되는 것을 알 수 있다

 

이제 앱에서 하나하나 제어하면서 위 값들이 어떤 기기들과 관련된 값인지 알아나가야 한다

3. 가스밸브 외부(앱) 제어 및 패킷 해석

Hi-oT 어플리케이션에서 가스 밸브 잠금 기능을 활용해 5번째 바이트가 0x05가 되는 패킷을 캡쳐해봤다

F7 0B 01 1B 02 43 11 03 00 B5 EE

와우~

네번째 바이트0x1B이면 가스밸브와 관련된 패킷인 것 같다

※ 아이파크때와 마찬가지로 닫혀있는 밸브를 외부 명령을 통해 열 수는 없다

 

이제 네번째 바이트가 0x1B인 패킷만 캡쳐해보자

class ParserGas(SerialParser):    
    def interpretPacket(self, packet: bytearray):
        if packet[2:4] == bytearray([0x01, 0x1B]):
            print(self.prettifyPacket(packet))

F7 0B 01 1B 01 43 11 00 00 B5 EE : 쿼리

F7 0D 01 1B 04 43 11 00 04 00 00 B2 EE : 밸브가 열려있을 때 응답

F7 0B 01 1B 02 43 11 03 00 B5 EE : 밸브 잠금 명령 (앱)

F7 0B 01 1B 04 43 11 03 03 B0 EE : 잠금 명령 직후 응답

F7 0B 01 1B 01 43 11 00 00 B5 EE : 쿼리

F7 0D 01 1B 04 43 11 00 03 00 00 B5 EE : 밸브가 닫혀있을 때 응답


조명, 아울렛과 달리 가스밸브는 집안 전체를 통틀어 딱 1개만 있으니 6~7번째 바이트 [0x43, 0x11]의 의미는 굳이 해석하려고 하지 말자.. ㅋㅋ (다른 디바이스들도 해석하다보면 어찌어찌 답이 나오겠지 뭐~)
조명, 아울렛은 ON 상태값을 가리키는 값이 0x01, OFF 상태값을 가리키는 값이 0x02였는데, 가스밸브는 0x03이 '잠김', 0x04가 '열림'인 것으로 해석되었다

 

이제 가스밸브와 관련된 조회, 명령, 응답 패킷명세를 얻었으니 홈네트워크 플랫폼과 연동할 준비는 모두 끝났다!

반응형