YOGYUI

광교아이파크::엘리베이터 Apple 홈킷 연동 (2) 본문

홈네트워크(IoT)/광교아이파크

광교아이파크::엘리베이터 Apple 홈킷 연동 (2)

요겨 2021. 1. 23. 22:30
반응형

2. Packet Analysis

UTP 커플러를 통해 분기하여 후킹하는 시스템 도식은 다음과 같다

Serial Packet Hooking System Schematic

게이트웨이와 소형 월패드간 통신은 RS-422 방식으로 Full-Duplex인데, Tx, Rx 각 라인에 각각 USB-RS485 컨버터를 장착해 바이트스트림 후킹 뿐만 아니라, 통신 라인에 사용자가 임의의 패킷을 송신할 수 있다

(이틀정도 삽질해서 알게됐다)

 

보통의 경우라면 보내는 쪽에 다이오드를 달아서 받는 쪽으로민 패킷이 전송되도록 해야하지만, 일반적인 RS-422, RS-485 핸들링 IC는 Tx단에 정보가 흘러들어오더라도 크게 문제되지 않는다 (경험적으로 알게 된 사실...)

그래서 신호 블로킹을 위한 다이오드는 추가로 장착하지 않기로 했다

 

게이트웨이에서 소형 월패드로 정보를 보내는 방향에 장착한 컨버터를 'SMART1', 반대 방향의 컨버터를 'SMART2'라고 임의로 이름붙인 뒤, 패킷을 파싱해보았다

(조명, 난방 등과 마찬가지로 baudrate는 9600일 때 의미있는 정보 추출 가능)

# SmartParser.py
from SerialComm import SerialComm

class SmartParser():
    buffer1: bytearray
    buffer2: bytearray
    enable_console_log: bool = False
    max_buffer_size: int = 200

    def __init__(self, ser1: SerialComm, ser2: SerialComm):
        super().__init__()
        self.buffer1 = bytearray()
        self.buffer2 = bytearray()
        ser1.sig_recv_data.connect(self.onRecvData1)
        ser2.sig_recv_data.connect(self.onRecvData2)

    def release(self):
        self.buffer1.clear()
        self.buffer2.clear()

    def onRecvData1(self, data: bytes):
        if len(self.buffer1) > self.max_buffer_size:
            self.buffer1.clear()
        self.buffer1.extend(data)
        self.handlePacket1()

    def onRecvData2(self, data: bytes):
        if len(self.buffer2) > self.max_buffer_size:
            self.buffer2.clear()
        self.buffer2.extend(data)
        self.handlePacket2()

    def handlePacket1(self):
        try:
            idx = self.buffer1.find(0x2)
            if idx > 0:
                self.buffer1 = self.buffer1[idx:]

            if len(self.buffer1) >= 3:
                packetLen = self.buffer1[2]
                if len(self.buffer1) >= packetLen:
                    chunk = self.buffer1[:packetLen]
                    if self.enable_console_log:
                        msg = ' '.join(['%02X' % x for x in chunk])
                        print('[SER 1] ' + msg)
                    self.buffer1 = self.buffer1[packetLen:]
        except Exception:
            pass
    
    def handlePacket2(self):
        try:
            idx = self.buffer2.find(0x2)
            if idx > 0:
                self.buffer2 = self.buffer2[idx:]

            if len(self.buffer2) >= 3:
                packetLen = self.buffer2[2]
                if len(self.buffer2) >= packetLen:
                    chunk = self.buffer2[:packetLen]
                    if self.enable_console_log:
                        msg = ' '.join(['%02X' % x for x in chunk])
                        print('[SER 2] ' + msg)
                    self.buffer2 = self.buffer2[packetLen:]
        except Exception:
            pass

if __name__ == '__main__':
    ser1 = SerialComm()
    ser2 = SerialComm()
    par = SmartParser(ser1, ser2)
    par.enable_console_log = True
    ser1.connect('/dev/ttyUSB2', 9600)
    ser2.connect('/dev/ttyUSB3', 9600)
    
    while True:
        pass
[SER 1] 02 C1 06 11 C3 11
[SER 2] 02 C1 0C 91 C3 00 01 00 02 01 02 A3
[SER 1] 02 C1 06 11 C4 18
[SER 2] 02 C1 0C 91 C4 00 01 00 02 01 02 A2
[SER 1] 02 C1 06 11 C5 17
[SER 2] 02 C1 0C 91 C5 00 01 00 02 01 02 A5
[SER 1] 02 C1 06 11 C6 16
[SER 2] 02 C1 0C 91 C6 00 01 00 02 01 02 A4
[SER 1] 02 C1 06 11 C7 15
[SER 2] 02 C1 0C 91 C7 00 01 00 02 01 02 A7
[SER 1] 02 C1 06 11 C8 1C
[SER 2] 02 C1 0C 91 C8 00 01 00 02 01 02 96
[SER 1] 02 C1 06 11 C9 1B
[SER 2] 02 C1 0C 91 C9 00 01 00 02 01 02 99
[SER 1] 02 C1 06 11 CA 1A
[SER 2] 02 C1 0C 91 CA 00 01 00 02 01 02 98
[SER 1] 02 C1 13 13 CB 15 01 18 00 23 19 00 FF 01 00 00 01 02 00
[SER 2] 02 C1 0C 93 CB 00 01 00 02 01 02 99
[SER 1] 02 C1 06 11 CC 20
[SER 2] 02 C1 0C 91 CC 00 01 00 02 01 02 9A
[SER 1] 02 C1 06 11 CD 1F
[SER 2] 02 C1 0C 91 CD 00 01 00 02 01 02 9D
[SER 1] 02 C1 06 11 CE 1E
[SER 2] 02 C1 0C 91 CE 00 01 00 02 01 02 9C
[SER 1] 02 C1 06 11 CF 1D
[SER 2] 02 C1 0C 91 CF 00 01 00 02 01 02 9F
[SER 1] 02 C1 06 11 D0 04
[SER 2] 02 C1 0C 91 D0 00 01 00 02 01 02 8E
[SER 1] 02 C1 06 11 D1 03
[SER 2] 02 C1 0C 91 D1 00 01 00 02 01 02 91
[SER 1] 02 C1 06 11 D2 02
[SER 2] 02 C1 0C 91 D2 00 01 00 02 01 02 90
[SER 1] 02 C1 06 11 D3 01
[SER 2] 02 C1 0C 91 D3 00 01 00 02 01 02 93
[SER 1] 02 C1 06 11 D4 08
[SER 2] 02 C1 0C 91 D4 00 01 00 02 01 02 92
[SER 1] 02 C1 06 11 D5 07
[SER 2] 02 C1 0C 91 D5 00 01 00 02 01 02 95
[SER 1] 02 C1 06 11 D6 06
[SER 2] 02 C1 0C 91 D6 00 01 00 02 01 02 94
[SER 1] 02 C1 06 11 D7 05
[SER 2] 02 C1 0C 91 D7 00 01 00 02 01 02 97
[SER 1] 02 C1 06 11 D8 0C
[SER 2] 02 C1 0C 91 D8 00 01 00 02 01 02 86
[SER 1] 02 C1 06 11 D9 0B
[SER 2] 02 C1 0C 91 D9 00 01 00 02 01 02 89
[SER 1] 02 C1 06 11 DA 0A
[SER 2] 02 C1 0C 91 DA 00 01 00 02 01 02 88
[SER 1] 02 C1 06 11 DB 09
[SER 2] 02 C1 0C 91 DB 00 01 00 02 01 02 8B
[SER 1] 02 C1 06 11 DC 10
[SER 2] 02 C1 0C 91 DC 00 01 00 02 01 02 8A
[SER 1] 02 C1 06 11 DD 0F
[SER 2] 02 C1 0C 91 DD 00 01 00 02 01 02 8D
[SER 1] 02 C1 06 11 DE 0E
[SER 2] 02 C1 0C 91 DE 00 01 00 02 01 02 8C
[SER 1] 02 C1 06 11 DF 0D
[SER 2] 02 C1 0C 91 DF 00 01 00 02 01 02 8F
[SER 1] 02 C1 06 11 E0 34
[SER 2] 02 C1 0C 91 E0 00 01 00 02 01 02 BE
[SER 1] 02 C1 06 11 E1 33
[SER 2] 02 C1 0C 91 E1 00 01 00 02 01 02 C1
[SER 1] 02 C1 06 11 E2 32
[SER 2] 02 C1 0C 91 E2 00 01 00 02 01 02 C0
[SER 1] 02 C1 06 11 E3 31
[SER 2] 02 C1 0C 91 E3 00 01 00 02 01 02 C3
[SER 1] 02 C1 06 11 E4 38
[SER 2] 02 C1 0C 91 E4 00 01 00 02 01 02 C2
[SER 1] 02 C1 06 11 E5 37
[SER 2] 02 C1 0C 91 E5 00 01 00 02 01 02 C5
[SER 1] 02 C1 06 11 E6 36
[SER 2] 02 C1 0C 91 E6 00 01 00 02 01 02 C4
[SER 1] 02 C1 06 11 E7 35
[SER 2] 02 C1 0C 91 E7 00 01 00 02 01 02 C7
[SER 1] 02 C1 06 11 E8 3C
[SER 2] 02 C1 0C 91 E8 00 01 00 02 01 02 B6
[SER 1] 02 C1 06 11 E9 3B
[SER 2] 02 C1 0C 91 E9 00 01 00 02 01 02 B9
[SER 1] 02 C1 06 11 EA 3A
[SER 2] 02 C1 0C 91 EA 00 01 00 02 01 02 B8
[SER 1] 02 C1 13 13 EB 15 01 18 00 23 23 00 FF 01 00 00 01 02 A6
[SER 2] 02 C1 0C 93 EB 00 01 00 02 01 02 B9
[SER 1] 02 C1 1B 23 EC 05 01 00 10 88 02 00 00 00 03 00 00 00 04 00 00 00 05 00 00 00 D0
[SER 2] 02 C1 0C A3 EC 00 01 00 02 01 02 8C
[SER 1] 02 C1 06 11 ED 3F
[SER 2] 02 C1 0C 91 ED 00 01 00 02 01 02 BD
[SER 1] 02 C1 06 11 EE 3E
[SER 2] 02 C1 0C 91 EE 00 01 00 02 01 02 BC
[SER 1] 02 C1 06 11 EF 3D
[SER 2] 02 C1 0C 91 EF 00 01 00 02 01 02 BF
[SER 1] 02 C1 06 11 F0 24
[SER 2] 02 C1 0C 91 F0 00 01 00 02 01 02 AE
[SER 1] 02 C1 06 11 F1 23
[SER 2] 02 C1 0C 91 F1 00 01 00 02 01 02 B1
[SER 1] 02 C1 06 11 F2 22
[SER 2] 02 C1 0C 91 F2 00 01 00 02 01 02 B0

조명, 난방 등과 유사하게 생겨먹은 패킷들이 SMART1, SMART2 각각 흐르는 것을 확인할 수 있다

예상했던 것과 같이, 게이트웨이에서 쿼리 패킷을 송신하면 (SMART1) 소형 월패드에서 그에 대한 응답을 회신 (SMART2)하는 구조로 보인다 (RS-422 통신 방식이 아닐까했던 추측이 맞아떨어짐!!)

쿼리 패킷 구조는 크게 3종류가 있는 것으로 보인다

  • 02 C1 06 11 ... : 패킷 길이 6, SMART2에서의 응답 패킷(길이=12) 4번째 바이트 0x91 = 0x80 + 0x11
  • 02 C1 13 13 ... : 패킷 길이 19, SMART2에서의 응답 패킷(길이=12) 4번째 바이트 0x93 = 0x80 + 0x13
  • 02 C1 1B 23 ... : 패킷 길이 27, SMART2에서의 응답 패킷(길이=12) 4번째 바이트 0xA3 = 0x80 + 0x23

재미있는 사실은, 소형 월패드에서 엘리베이터 호출 버튼을 클릭하면 별도의 패킷을 송신하는게 아니라 다음과 같이 회신 패킷에 정보를 변경한다는 점이다 (4번째 바이트가 쿼리=0x11, 응답=0x91)

 

[SER 1] 02 C1 06 11 E8 3C
[SER 2] 02 C1 0C 91 E8 00 01 00 02 01 02 B6
[SER 1] 02 C1 06 11 E9 3B
[SER 2] 02 C1 0C 91 E9 00 01 00 02 01 02 B9
[SER 1] 02 C1 06 11 EA 3A
[SER 2] 02 C1 0C 91 EA 00 01 00 02 01 02 B8
[SER 1] 02 C1 06 11 EB 39
[SER 2] 02 C1 0C 91 EB 00 01 00 02 01 02 BB
[SER 1] 02 C1 06 11 EC 40
[SER 2] 02 C1 0C 91 EC 00 01 00 02 01 02 BA
[SER 1] 02 C1 06 11 ED 3F << 엘리베이터 하행 클릭

[SER 2] 02 C1 0C 91 ED 10 01 00 02 01 02 AD << 6번째 바이트가 0x10, 상행 버튼 클릭 시 0x20
[SER 1] 02 C1 08 12 EE 01 80 90
[SER 2] 02 C1 0C 92 EE 00 01 00 02 01 02 BB
[SER 1] 02 C1 13 13 EF 15 01 18 02 18 1A 01 FF 01 00 00 01 02 D3
[SER 2] 02 C1 0C 93 EF 01 01 00 02 01 02 B8
[SER 1] 02 C1 06 11 F0 24
[SER 2] 02 C1 0C 91 F0 01 01 00 02 01 02 B3
[SER 1] 02 C1 06 11 F1 23
[SER 2] 02 C1 0C 91 F1 01 01 00 02 01 02 AC
[SER 1] 02 C1 06 11 F2 22
[SER 2] 02 C1 0C 91 F2 01 01 00 02 01 02 B5
[SER 1] 02 C1 06 11 F3 21
[SER 2] 02 C1 0C 91 F3 01 01 00 02 01 02 AE
[SER 1] 02 C1 13 13 F4 15 01 18 02 18 1B 01 01 01 00 00 01 02 43
[SER 2] 02 C1 0C 93 F4 01 01 00 02 01 02 B9
[SER 1] 02 C1 06 11 F5 27
[SER 2] 02 C1 0C 91 F5 01 01 00 02 01 02 B0

 

또한, 엘리베이터 호출 후 4번째 바이트가 0x13인 쿼리 패킷의 전송 빈도가 갑자기 증가하는데, 0x13인 패킷에서 엘리베이터 운행에 대한 정보를 얻을 수 있다

 

[SER 1] 02 C1 13 13 EF 15 01 18 02 18 1A 01 FF 01 00 00 01 02 D3 << 엘리베이터 운행 시작
[SER 2] 02 C1 0C 93 EF 01 01 00 02 01 02 B8

[SER 1] 02 C1 13 13 F4 15 01 18 02 18 1B 01 01 01 00 00 01 02 43 << 1층
[SER 2] 02 C1 0C 93 F4 01 01 00 02 01 02 B9

...

[SER 1] 02 C1 13 13 14 15 01 18 02 18 25 01 05 01 00 00 01 02 F1  << 5층
[SER 2] 02 C1 0C 93 14 01 01 00 02 01 02 59

[SER 1] 02 C1 13 13 1A 15 01 18 02 18 27 01 06 01 00 00 01 02 F6 << 6층
[SER 2] 02 C1 0C 93 1A 01 01 00 02 01 02 4B

...

[SER 1] 02 C1 13 13 27 15 01 18 02 18 2B 01 09 01 00 00 01 02 D8 << 9층
[SER 2] 02 C1 0C 93 27 01 01 00 02 01 02 80

[SER 1] 02 C1 13 13 2E 15 01 18 02 18 2D 01 0A 01 00 00 01 02 D4 << 10층
[SER 2] 02 C1 0C 93 2E 01 01 00 02 01 02 7F

...

[SER 1] 02 C1 13 13 47 15 01 18 02 18 35 04 -- 01 00 00 01 02 -- << 개인정보 보호를 위해 Masking

[SER 2] 02 C1 0C 91 46 01 01 00 02 01 02 29

 

 

정리하면, 4번째 바이트가 0x13인 (SMART1) 쿼리 패킷의 12번째 패킷은 엘리베이터의 운행 정보 (0 = 멈춰있음, 1 = 운행중, 4 = 도착)를 가리키고, 13번째 패킷은 현재 층을 가리킨다

간단하게 Flow Chart를 그려보면 다음과 같다

엘리베이터 관련 통신 Flow Chart

 

그런데, 내가 아무리 SMART2로 하행버튼이 눌린 상태를 가리키는 패킷을 보내도 엘리베이터는 호출이 되지 않았다

대략 3~4일정도 삽질한 끝에, Timestamp가 일치해야만 게이트웨이가 정상적으로 반응한다는 사실을 발견했다

주고받는 패킷의 5번째 바이트는 Timestamp로서 SMART1과 SMART2가 일치하고 한 쌍의 송수신이 끝나면 값이 1씩 증가하는데, 게이트웨이가 가장 최근에 보낸 Timestamp와 일치하는 값을 가진 응답 패킷을 보내야 임의로 엘리베이터 호출이 되는 것을 알 수 있었다

 

Timestamp는 0 ~ 255까지 총 256의 값을 가질 수 있으므로, 엘리베이터를 손으로 호출하면서 특정 Timestamp에 해당하는 패킷을 노가다로 모아야한다 (이 때, 전기 절약을 위해 게이트웨이의 '네트웍' 케이블을 연결 해제하면 월패드로 아무리 호출해도 실제 엘리베이터는 움직이지 않는다)

절약정신!

 

[시리즈 링크]

광교아이파크::엘리베이터 Apple 홈킷 연동 (1)

광교아이파크::엘리베이터 Apple 홈킷 연동 (2)

광교아이파크::엘리베이터 Apple 홈킷 연동 (3)

광교아이파크::엘리베이터 Apple 홈킷 연동 (4)

반응형