YOGYUI

힐스테이트 광교산::엘리베이터 호출 제어 RS-485 패킷 분석 본문

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

힐스테이트 광교산::엘리베이터 호출 제어 RS-485 패킷 분석

요겨 2022. 6. 23. 21:50
반응형

한동안 1일 1디바이스씩 조지다가(?) 21일~23일 거제도 출장 일정으로 잠깐 휴식을 가졌다 ㅎㅎ

다시 열심히 달려보자..

 

거실 월패드랑 복도 쪽 제어패드에는 엘리베이터 호출 기능이 있다

월패드에서는 상향, 하향 양방향으로 호출이 가능한데 반해, 복도 제어패드에서는 하향으로만 호출이 된다는 차이가 있다

(Hi-oT 앱에서도 하향으로만 호출된다는 점은 약간 특이하다)

월패드로 엘리베이터를 호출하면서 패킷 변화를 캡쳐해보자

1. 엘리베이터 호출 시 패킷 분석

class ParserVarious(SerialParser):    
    def interpretPacket(self, packet: bytearray):
        try:
            if packet[3] == 0x18:  # 난방
                self.handleThermostat(packet)
            elif packet[3] == 0x1B:  # 가스차단기
                self.handleGasValve(packet)
            elif packet[3] == 0x1C:  # 시스템에어컨
                self.handleAirconditioner(packet)
            elif packet[3] == 0x2B:  # 환기 (전열교환기)
                self.handleVentilator(packet)
            else:
                if packet[4] == 0x02:
                    print(self.prettifyPacket(packet))
        except Exception as e:
            writeLog('interpretPacket::Exception::{} ({})'.format(e, packet), self)

다른 기기들과 마찬가지로, 패킷의 다섯번째 바이트가 0x02가 되는 패킷에 한해 캡쳐해보려고 스크립트를 수정했다

그런데... 아무리 호출해도 0x02가 되는 패킷을 캡쳐할 수가 없었다 ㅠㅠ

그래서 그냥 무작정 데이터가 변하는 패킷을 살펴봤더니...


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 34 01 41 10 00 00 00 00 9F EE
F7 0B 01 34 04 41 10 00 06 9A EE
F7 0D 01 34 01 41 10 00 B6 01 06 2E EE
F7 0B 01 34 04 41 10 00 06 9A EE
F7 0D 01 34 01 41 10 00 A6 02 07 3C EE

...

F7 0D 01 34 01 41 10 00 B6 -- -- 38 EE (개인정보 보호를 위해 마스크처리)
F7 0B 01 34 04 41 10 00 06 9A EE
F7 0D 01 34 01 41 10 00 01 -- -- 8F EE (개인정보 보호를 위해 마스크처리)
F7 0B 01 34 04 41 10 00 00 9C 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


내부 데이터가 분명하게 변하는 패킷을 발견했다!

바로 네번째 바이트가 0x34인 패킷이 엘리베이터 호출 기능과 관련된 패킷으로 99% 확실하게 결론지었다 ㅎㅎ

엘리베이터 호출 후 월패드가 복도 제어패드로 보내는 패킷 (5번째 바이트가 0x01)의 9~11번째 바이트 값이 바뀌게 되며, 복도 제어패드가 거실 월패드로 응답하는 패킷(5번째 바이트가 0x04)의 9번째 바이트값이 0x06으로 바뀌게 된다

특기할만한 점은, 엘리베이터가 도착하면 쿼리 패킷의 9번째 바이트값이 0x01로 바뀌고, 10번째 혹은 11번째 바이트값 중 하나가 16진수로 표현되는 나의 층수가 된다는 점이다

※ 예를 들어 3층에서 호출했으면 01 03 XX 혹은 01 XX 03 

결국 엘리베이터 호출 후 쿼리 패킷의 10~11번째 바이트값은 현재 엘리베이터 층수를 가리키는 것을 월패드 디스플레이와 비교하면서 알게 되었다!

거실 월패드 엘리베이터 현재 층수 표시 (개인정보보호를 위해 엘리베이터 호기 마스크 처리)

확실하게 하기 위해 거실 월패드에서 상행으로 호출해봤다


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 34 01 41 10 00 B5 B6 06 9A EE : 엘리베이터 현재 위치 = 지하6층(B6)
F7 0B 01 34 04 41 10 00 00 9C EE
F7 0D 01 34 01 41 10 00 A5 B5 06 89 EE : 엘리베이터 현재 위치 = 지하5층(B5)
F7 0B 01 34 04 41 10 00 00 9C EE
F7 0D 01 34 01 41 10 00 A5 B4 07 89 EE : 엘리베이터 현재 위치 = 지하4층(B4)
F7 0B 01 34 04 41 10 00 00 9C EE
F7 0D 01 34 01 41 10 00 A5 00 06 3C EE : ??? 
F7 0B 01 34 04 41 10 00 00 9C EE
F7 0D 01 34 01 41 10 00 A5 B3 07 8E EE : 엘리베이터 현재 위치 = 지하3층(B3)
F7 0B 01 34 04 41 10 00 00 9C EE
F7 0D 01 34 01 41 10 00 A5 01 06 3D EE : 엘리베이터 현재 위치 = 지상1층(01)
F7 0B 01 34 04 41 10 00 00 9C EE
F7 0D 01 34 01 41 10 00 A5 02 06 3E EE : 엘리베이터 현재 위치 = 지상2층(02)
F7 0B 01 34 04 41 10 00 00 9C EE
...
F7 0D 01 34 01 41 10 00 01 -- --  8E EE : 엘리베이터 도착 (층수 마스크 처리)
F7 0B 01 34 04 41 10 00 00 9C EE

 


하행 호출때와는 달리 쿼리 패킷의 9번째 바이트의 하위 4비트값이 5가 되는 것을 알 수 있다

(상행 호출 시 하위 4비트값은 6)

9번째 바이트의 상위 4비트값은 A혹은 B인데, 이게 뭘 의미하는건지는 정확히는 모르겠다 (A, B 각각 다른 엘리베이터를 가리키는 것 같긴 한데.. 이건 나중에 파서 만들면서 확인해볼 예정)

상행, 하행 호출 모두 쿼리 패킷9번째 바이트가 0x01이 되면 엘리베이터가 도착했다는 의미가 되는 것을 알 수 있다

가장 특이한 점은, 상행 호출 시 복도 제어패드의 응답패킷의 9번째 바이트값이 0x00으로 불변한다는 점이다

 

실제로 거실 월패드에서 상행 호출 시 복도 제어패드의 엘리베이터 버튼의 LED가 불변(꺼지지 않음)한다는 것을 알 수 있다

다른 기기들과 달리 엘리베이터는 제어패드의 기능이 '현재 버튼이 사용자에 의해 눌렸는지 여부'를 패킷의 9번째 바이트로 반환하는 것으로 보인다 - 즉, 제어 패드에는 엘리베이터 상행 호출 기능이 없으므로 응답 패킷에서 값이 변하지 않는 것으로 추측됨 (뇌피셜 ㅎㅎ...)

 

내가 홈네트워크와 연동하고 싶은 기능은 호출 - 도착알림 두 개가 전부이기 때문에, 엘리베이터 현재 층수와 관련해서는 크게 신경쓰지 않기로 한다 (엘리베이터가 호출되었는지, 도착했는지 여부는 쿼리 패킷 9번째 바이트값만 확인하면 된다!)

내 추측은... 9번째 바이트의 상위 4비트는 엘리베이터 호기 구분, 10번째 바이트는 엘리베이터의 층수를 가리키고, 11번째 바이트는 엘리베이터가 움직이는지, 멈췄는지 여부를 가리키는게 아닐까 싶은데.. 파서 구현하면서 간단하게 확인해보기만 하자 ㅎㅎ

2. 엘리베이터 호출 명령 패킷 찾기

제일 중요한 기능은 외부에서 RS-485 통신선에 임의의 패킷을 쑤셔넣어(?) 호출이 되어야된다는 점인데, 월패드나 제어패드, 혹은 앱으로는 명령 관련 패킷을 캡쳐할 수가 없었다

혹시나 싶어 엘리베이터 호출 후 복도 제어패드의 응답 패킷인 <F7 0B 01 34 04 41 10 00 06 9A EE> 를 송신해봤는데, 아무런 변화가 없었다 ㅠ_ㅠ

 

이제껏 해온 짬바가 있지, 포기할 순 없어서 다른 디바이스들의 명령 패킷을 참고해서 호출 명령 패킷을 만들어봐야겠다!

(과연 성공할지 여부에 대한 확신이 전혀 없는 상태에서 맨땅에 헤딩!)

다른 기기들의 제어 패킷 명령과 그에 대한 응답 패킷을 다시 한번 상기해보자


F7 0B 01 19 02 40 41 01 00 E6 EE : 컴퓨터방(방3)의 조명을 켜는 명령

F7 0B 01 19 04 40 40 00 01 E1 EE : 컴퓨터방(방3)의 조명이 켜져있을 때 응답

F7 0B 01 1B 02 43 11 03 00 B5 EE : 가스밸브 잠금 명령

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

F7 0B 01 2B 02 42 11 07 00 80 EE : 전열교환기 풍량 '강' 변경 명령

F7 0C 01 2B 04 40 11 00 01 07 82 EE : 전열교환기 응답 (환기 가동중, 풍량 = '강')


명령 패킷(5번째 바이트가 0x02)의 8번째 바이트에 변경하고자 하는 상태값 (On/Off 여부, 잠금 여부, 풍량, 온도 등등)을 통해 기기의 상태가 변경되며, 해당값은 변경 이후 응답 패킷에 적용되는 것을 이제껏 확인해왔다

 

엘리베이터의 응답 패킷 (복도 제어패드 → 거실 월패드)는 하행 호출 시 다음과 같다

F7 0B 01 34 04 41 10 00 06 9A EE

 

이를 토대로 상태값을 0x06으로 설정하는 명령패킷의 템플릿은 다음과 같을 것이라 추측할 수 있다(조명 명령 패킷 참고)

F7 0B 01 34 02 41 10 06 00 XX EE

여기서 XX는 체크섬(XOR SUM)이다

 

이 템플릿을 토대로 패킷을 한번 만들어보자

from functools import reduce

def calcXORChecksum(data: Union[bytearray, bytes, List[int]]) -> int:
    return reduce(lambda x, y: x ^ y, data, 0)

def makePacketCallDownside() -> bytearray:
    # F7 0B 01 34 02 41 10 06 00 XX EE
    packet = bytearray([0xF7, 0x0B, 0x01, 0x34])
    packet.append(0x02)
    packet.extend([0x41, 0x10, 0x06, 0x00])
    packet.append(calcXORChecksum(packet))
    packet.append(0xEE)
    return packet

함수를 통해 만들어낸 패킷은 바로 

F7 0B 01 34 02 41 10 06 00 9C EE 

놀랍게도(?) 이 패킷을 RS-485 통신선으로 송신했더니 하행 엘리베이터 호출이 됐다!


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 34 01 41 10 00 00 00 00 9F EE
F7 0B 01 34 04 41 10 00 00 9C EE
[ParserVarious (0xB3415250)] Send >> F7 0B 01 34 02 41 10 06 00 9C EE
F7 0D 01 34 01 41 10 00 00 00 00 9F EE
F7 0B 01 34 04 41 10 00 06 9A EE
F7 0D 01 34 01 41 10 00 A6 B6 06 89 EE : 엘리베이터가 호출됨!
F7 0B 01 34 04 41 10 00 06 9A EE
F7 0D 01 34 01 41 10 00 A6 B5 06 8A EE


하지만... 안타깝게도 9번째 바이트를 0x05로 만든 패킷을 보내면 상행 엘리베이터 호출이 되지 않을까 했던 기대는 박살났다 ㅠㅠ


[ParserVarious (0xB33D92B0)] Send >> F7 0B 01 34 02 41 10 05 00 9F EE
F7 0B 01 34 04 41 10 05 00 99 EE
[ParserVarious (0xB33D92B0)] Send >> F7 0B 01 34 02 41 10 05 00 9F EE
F7 0B 01 34 04 41 10 05 00 99 EE
[ParserVarious (0xB33D92B0)] Send >> F7 0B 01 34 02 41 10 05 00 9F EE
F7 0B 01 34 04 41 10 05 00 99 EE
[ParserVarious (0xB33D92B0)] Send >> F7 0B 01 34 02 41 10 05 00 9F EE
F7 0B 01 34 04 41 10 05 00 99 EE
[ParserVarious (0xB33D92B0)] Send >> F7 0B 01 34 02 41 10 05 00 9F EE
F7 0B 01 34 04 41 10 05 00 99 EE
[ParserVarious (0xB33D92B0)] Send >> F7 0B 01 34 02 41 10 05 00 9F EE
F7 0B 01 34 04 41 10 05 00 99 EE
[ParserVarious (0xB33D92B0)] Send >> F7 0B 01 34 02 41 10 05 00 9F EE
F7 0B 01 34 04 41 10 05 00 99 EE
[ParserVarious (0xB33D92B0)] Send >> F7 0B 01 34 02 41 10 05 00 9F EE
F7 0D 01 34 04 41 10 05 00 00 00 9F EE
[ParserVarious (0xB33D92B0)] Send >> F7 0B 01 34 02 41 10 05 00 9F EE
F7 0E 01 34 04 41 10 05 00 00 00 00 9C EE
[ParserVarious (0xB33D92B0)] Send >> F7 0B 01 34 02 41 10 05 00 9F EE
F7 0E 01 34 04 41 10 05 00 00 00 00 9C EE


시리얼 포트가 유휴시간일 때마다 보내서 10번을 반복해봤는데도 별다른 응답이 없었다..

응답 패킷을 봐도 하행 명령 패킷 송신 후에 9번째 바이트가 0x06으로 바뀌었었는데, 상행 명령 패킷 송신후에는 8번째 바이트가 0x05로 바뀔 뿐 9번째 바이트는 0x00으로 변화가 없다 ㅠ

아무래도 명령 패킷은 복도 제어패드의 '엘리베이터 호출 버튼이 사용자에 의해 눌린 상태'로 바꿔주는 명령이라, 상행 호출 버튼이 없는 제어패드가 이에 대한 응답은 하지 못하는게 아닌가하는 뇌피셜을 발휘해본다 ^^;;

 

그래도 하행 호출 명령 패킷 찾은게 어디야! 나름 뿌듯하다 ㅎㅎ

3. 엘리베이터 현재 층수 parsing

패킷 명세는 다음과 같다

  • 4번째 바이트가 0x34이면 엘리베이터 관련 패킷
  • 5번째 바이트가 0x01일 때 (쿼리)
    8번째 바이트의 하위 4비트는 엘리베이터 상태값 (0=IDLE, 1=도착, 5=상행호출 후 이동 중, 6=하행호출 후 이동 중)
    8번째 바이트의 상위 4비트는 0xA 혹은 0xB - 두 개 엘리베이터의 인덱스를 가리키는게 아닐까?
    9번째 바이트는 엘리베이터의 현재 층수
    10번째 바이트는 해당 엘리베이터가 움직이는지 여부를 가리키는게 아닐까?
  • 5번째 바이트가 0x04일 때 (응답)
    8번째 바이트의 하위 4비트는 엘리베이터의 상태값 (0=IDLE, 6=하행호출 후 이동 중)

뇌피셜에 해당하는 내용은 취소선으로 가려놨다 ㅎㅎ

이제 뇌피셜을 직접 눈으로 검증해볼 차례!

우선 시리얼 패킷 파서를 만들어주자

class ParserVarious(SerialParser):    
    def interpretPacket(self, packet: bytearray):
        try:
            if packet[3] == 0x18:  # 난방
                self.handleThermostat(packet)
            elif packet[3] == 0x1B:  # 가스차단기
                self.handleGasValve(packet)
            elif packet[3] == 0x1C:  # 시스템에어컨
                self.handleAirconditioner(packet)
            elif packet[3] == 0x2B:  # 환기 (전열교환기)
                self.handleVentilator(packet)
            elif packet[3] == 0x34:  # 엘리베이터
                self.handleElevator(packet)
        except Exception as e:
            writeLog('interpretPacket::Exception::{} ({})'.format(e, packet), self)
            
    def handleElevator(self, packet: bytearray):
        if packet[4] == 0x01:  # 상태 쿼리 (월패드 -> 복도 미니패드)
            # F7 0D 01 34 01 41 10 00 XX YY ZZ ** EE
            # XX: 00=Idle, 01=Arrived, 하위4비트가 6이면 하행 호출중, 5이면 상행 호출 중, 상위4비트는 엘리베이터 구분
            # YY: 엘리베이터 층수
            # ZZ: 엘리베이터가 움직이는지 여부
            # **: Checksum (XOR SUM)
            state = packet[8] & 0x0F  # 0 = idle, 1 = arrived, 5 = moving(up), 6 = moving(down)
            elevator_index = (packet[8] & 0xF0) >> 4  # 0x0A or 0x0B
            floor = ['??', '??']
            if elevator_index == 0x0A:
                floor[0] = '{:02X}'.format(packet[9])
            elif elevator_index == 0x0B:
                floor[1] = '{:02X}'.format(packet[9])
            result = {
                'device': 'elevator',
                'state': state,
                'floor': floor
            }
            self.sig_parse_result.emit(result)
        elif packet[4] == 0x02:
            pass
        elif packet[4] == 0x04:  # 상태 응답 (복도 미니패드 -> 월패드)
            state = packet[8] & 0x0F  # 0 = idle, 1 = arrived, 5 = moving(up), 6 = moving(down)
            result = {
                'device': 'elevator',
                'state': state
            }
            self.sig_parse_result.emit(result)

그리고 Flask로 간단하게 웹페이지를 만들어서 호출 후 월패드 디스플레이와 현재 층수를 비교해봤다

크흠... 디스플레이되는 층수랑 파서를 통해 해석한 층수가 서로 매칭이 안된다 ㅋㅋㅋ

역시 뇌피셜은 한계가 있는건가...

어쨌든 중요한건 '엘리베이터 호출' 그리고 '도착 알림' 기능이 핵심인데, 이들은 모두 정상적으로 파싱이 가능하니 현재 층수 해석은 나중에 시간을 내서 따로 해봐야겠다 (그러고는 안하겠지...)

 

이제 홈네트워크 플랫폼 (Homebridge, Home Assistant)에 액세서리를 추가할 일만 남았다!

반응형
Comments