YOGYUI

홈네트워크 RS-485 수신 패킷 버퍼 해석 기능 강화 (GitHub 소스코드) 본문

홈네트워크(IoT)

홈네트워크 RS-485 수신 패킷 버퍼 해석 기능 강화 (GitHub 소스코드)

요겨 2024. 1. 11. 23:17
반응형

Improve RS-485 packet recv buffer parser

2023년 12월 무렵, 내가 힐스테이트의 현대통신 RS-485 홈네트워크 관련 작업을 하면서 깃허브에 올려둔 코드를 사용하고자 하는 유저가 종종 문의를 해 답변을 하고 원격 지원을 하는 와중에 짬을 내 코드를 리뷰하는 시간도 가졌다

이래저래 디버깅하는 와중에 ew11 무선 RS485 컨버터로부터 데이터를 수신할 때 버퍼링을 통해 여러개의 패킷을 한꺼번에 받는데, 내가 짠 코드는 여러개의 패킷 중 최초로 받은 단 1개의 패킷만 해석하는 크리티컬한 개선 필요 사항을 발견했다!

 

대충대충 동작한 어거지로 하게 짠 코드라 그런가.. 시간이 지나서 돌아보니 상당히 민망한 부분 ㅋㅋ

지금 실제로 사용하는데 큰 문제는 없지만, 이제 내 코드를 변경없이 가져다 사용하는 유저들이 조금씩 늘어나고 있기에, 확실하게 고쳐야 할 사항은 개선을 해야겠다는 생각이 들어 부랴부랴 수정 작업에 들어갔다

1. 코드 변경 내역

1.1. 기존 코드

class PacketParser:
    def onRecvData(self, data: bytes):
        self.line_busy = True
        if len(self.buffer) > self.max_buffer_size:
            self.buffer.clear()
            self.line_busy = False
        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) >= 2:
            packet_length = self.buffer[1]
            if len(self.buffer) >= packet_length:
                if self.buffer[0] == 0xF7 and self.buffer[packet_length - 1] == 0xEE:
                    self.line_busy = False
                    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:
                            pacstr = self.prettifyPacket(packet)
                            self.log(f'Checksum Error (calc={checksum_calc}, recv={checksum_recv}) ({pacstr})')
                        self.buffer = self.buffer[packet_length:]
                    except IndexError:
                        buffstr = self.prettifyPacket(self.buffer)
                        pacstr = self.prettifyPacket(packet)
                        self.log(f'Index Error (buffer={buffstr}, packet_len={packet_length}, packet={pacstr})')

기존 코드는 USB-to-RS485 컨버터를 장착한 환경에서 구현했는데, 그 때 당시만 해도 컨버터가 패킷 여러개를 뭉쳐서 보내는 경우가 거의 없었기 때문에 시리얼 객체 데이터 수신 시 즉각적으로 시작 바이트 0xF7 ~ 끝 바이트 0xEE 단일 패킷만 찾아서 처리할 수 있게 구현한 상태..

1.2. 변경 코드

class PacketParser:
    def handlePacket(self):
        count = 0
        while True:
            if count >= 10:
                self.buffer.clear()
                break
            count += 1
            idx_prefix = self.buffer.find(0xF7)
            if idx_prefix >= 0:
                self.buffer = self.buffer[idx_prefix:]
            else:
                # prefix 바이트가 버퍼에 없을 경우 루프 종료
                break
            if len(self.buffer) >= 2:
                packet_length = self.buffer[1]
                if len(self.buffer) >= packet_length:
                    if self.buffer[packet_length - 1] == 0xEE:
                        self.line_busy = False
                        packet = self.buffer[:packet_length]
                        self.buffer = self.buffer[packet_length:]
                        try:
                            checksum_calc = self.calcXORChecksum(packet[:-2])
                            checksum_recv = packet[-2]
                            self.interpretPacket(packet)
                            if checksum_calc != checksum_recv:
                                pacstr = self.prettifyPacket(packet)
                                self.log(f'Checksum Error (calc={checksum_calc}, recv={checksum_recv}) ({pacstr})')
                        except IndexError:
                            buffstr = self.prettifyPacket(self.buffer)
                            pacstr = self.prettifyPacket(packet)
                            self.log(f'Index Error (buffer={buffstr}, packet_len={packet_length}, packet={pacstr})')
                        count -= 1
                        continue
                    else:
                        # 패킷 끝 바이트가 suffix(0xEE)가 아닐 경우 잔여 버퍼 해석
                        if len(self.buffer) > 0:
                            self.buffer = self.buffer[1:]
                            idx_prefix = self.buffer.find(0xF7)
                            if idx_prefix >= 0:
                                # 잔여 버퍼만 잘라낸 뒤 루프 초기화
                                self.buffer = self.buffer[idx_prefix:]
                                continue
                            else:
                                # 잔여 버퍼 중 prefix(0xF7) 바이트가 없으면 버퍼 비우고 루프 종료
                                self.buffer.clear()
                                break
                else:
                    # 버퍼 길이가 패킷 길이보다 작을 경우 루프 종료 (다음 버퍼 수신 대기)
                    break
            else:
                # 버퍼 길이가 2보다 작을 경우 루프 종료 (다음 버퍼 수신 대기)
                break

while을 통해 무한루프를 구현한 후, 버퍼 내부에 유효 패킷을 반복해서 찾는 코드로 변경했다

특정 조건에서 루프를 빠져나가거나 루프 시작점으로 회귀하게 구현한 것 말고는 코드 구성은 거의 동일하다

현업에서 시리얼 통신 구현 작업을 하면서 익힌 노하우(?!)를 허접하게나마 적용해봤다 ㅎㅎ

※ 확실히 C 배열이나 C++ 벡터보다 파이썬 리스트 사용법이 간단하고 코드 가독성도 좋다

  • prefix(0xF7) 바이트를 찾으면 해당 바이트부터 유효 패킷인지 판단 (버퍼 슬라이싱)
    prefix 바이트가 없을 경우 루프 종료
  • 버퍼 길이가 2 미만일 경우 - 루프 종료
  • 버퍼 길이가 패킷 스펙상의 길이보다 작을 경우 - 루프 종료: 패킷이 잘려서 수신된 상태임을 가정, 다음 패킷 수신 대기
  • 패킷 스펙상 마지막 바이트가 suffix (0xEE) 바이트일 경우 유효 패킷으로 판단, 해석 시작 및 루프 초기화 (버퍼의 다음 유효 패킷 탐색 시작)
  • 패킷 스펙상 마지막 바이트가 suffix 바이트가 아닐 경우 유효하지 않은 패킷으로 판단
    또다른 prefix 바이트가 있으면 해당 바이트부터 탐색 시작하도록 루프 초기화
    prefix 바이트가 없으면 버퍼를 비우고 루프 종료
  • 루프가 10번 이상 회귀했다면 버퍼를 비우고 종료 (무한 루프 방지)

2. 코드 변경 후 패킷 해석 동작

간단하게 확인하기 위해 로그 함수를 handlePacket 메서드와 interpretPacket 메서드에 추가해봤다

class PacketParser:
    def handlePacket(self):
        self.log(f'buffer: {self.prettifyPacket(self.buffer)}')
        """
        뒤는 동일
        """
    
    def interpretPacket(self, packet: bytearray):
        self.log(f'packet: {self.prettifyPacket(packet)}')
        """
        뒤는 동일
        """

 

터미널에서 실행 후 로그 중 일부를 발췌해봤다

<01:23:14.045573> [PacketParser (0xB2CE5930)] <LIGHT> buffer: F7 0B 01 19 01 40 20 00 00 85 EE F7 0C 01 19 04 40 20 00 02 02 87 EE F7 0B 01 19 01 40 30 00 00 95 EE F7 0B 01 19 04 40 30 00 02 92 EE F7 0B 01 19 01 40 40 00 00 E5 EE F7 0B 01 19 04 40 40 00 02 E2 EE F7 0B 01 19 01 40 60 00 00 C5 EE F7 0C 01 19 04 40 60 00 02 02 C7 EE F7 0B 01 1F 01 40 10 00 00 B3 EE F7 1C 01 1F 04 40 10 00 11 01 00 16 00 00 00 00 02 12 01 00 00 00 00 00 00 02 B4 EE F7 0B 01 1F 01 40 20 00 00 83 EE F7 1C 01 1F 04 40 20 00 21 01 00 00 00 00 00 00 02 22 01 00 00 00 00 00 00 02 92 EE F7 0B 01 1F 01 40 30 00 00 93 EE F7 1C 01 1F 04 40 30 00 31 01 00 00 00 00 00 00 02 32 01 00 00 00 00 00 00 02 82 EE F7 0B 01 1F 01 40 40 00 00 E3 EE F7 1C 01 1F 04 40 40 00 41 01 00 00 00 00 00 00 02 42 01 00 00 00 00 00 00 02 F2 EE
<01:23:14.045969> [PacketParser (0xB2CE5930)] <LIGHT> packet: F7 0B 01 19 01 40 20 00 00 85 EE
<01:23:14.046079> [PacketParser (0xB2CE5930)] <LIGHT> packet: F7 0C 01 19 04 40 20 00 02 02 87 EE
<01:23:14.046306> [PacketParser (0xB2CE5930)] <LIGHT> packet: F7 0B 01 19 01 40 30 00 00 95 EE
<01:23:14.046736> [PacketParser (0xB2CE5930)] <LIGHT> packet: F7 0B 01 19 04 40 30 00 02 92 EE
<01:23:14.046913> [PacketParser (0xB2CE5930)] <LIGHT> packet: F7 0B 01 19 01 40 40 00 00 E5 EE
<01:23:14.047459> [PacketParser (0xB2CE5930)] <LIGHT> packet: F7 0B 01 19 04 40 40 00 02 E2 EE
<01:23:14.047656> [PacketParser (0xB2CE5930)] <LIGHT> packet: F7 0B 01 19 01 40 60 00 00 C5 EE
<01:23:14.047782> [PacketParser (0xB2CE5930)] <LIGHT> packet: F7 0C 01 19 04 40 60 00 02 02 C7 EE
<01:23:14.048050> [PacketParser (0xB2CE5930)] <LIGHT> packet: F7 0B 01 1F 01 40 10 00 00 B3 EE
<01:23:14.048194> [PacketParser (0xB2CE5930)] <LIGHT> packet: F7 1C 01 1F 04 40 10 00 11 01 00 16 00 00 00 00 02 12 01 00 00 00 00 00 00 02 B4 EE
<01:23:14.049816> [PacketParser (0xB2CE5930)] <LIGHT> packet: F7 0B 01 1F 01 40 20 00 00 83 EE
<01:23:14.057791> [PacketParser (0xB2CE5930)] <LIGHT> packet: F7 1C 01 1F 04 40 20 00 21 01 00 00 00 00 00 00 02 22 01 00 00 00 00 00 00 02 92 EE
<01:23:14.059094> [PacketParser (0xB2CE5930)] <LIGHT> packet: F7 0B 01 1F 01 40 30 00 00 93 EE
<01:23:14.064523> [PacketParser (0xB2CE5930)] <LIGHT> packet: F7 1C 01 1F 04 40 30 00 31 01 00 00 00 00 00 00 02 32 01 00 00 00 00 00 00 02 82 EE
<01:23:14.065556> [PacketParser (0xB2CE5930)] <LIGHT> packet: F7 0B 01 1F 01 40 40 00 00 E3 EE
<01:23:14.066995> [PacketParser (0xB2CE5930)] <LIGHT> packet: F7 1C 01 1F 04 40 40 00 41 01 00 00 00 00 00 00 02 42 01 00 00 00 00 00 00 02 F2 EE

EW11로부터 무려 246바이트의 데이터를 한꺼번에 수신받았는데, 16개의 유효 패킷을 버퍼 수신 후 210ms 시간동안 회귀를 돌며 모두 무사히(?) 해석해냈다

 

기존 코드였으면 최초 1개의 패킷만 사용하고 버퍼에 잔존한 15개의 소중한(?) 패킷을 아예 해석 시도조차 하지 않고 버리기 때문에 홈브릿지와 홈어시스턴트의 액세서리(디바이스) 상태값 업데이트가 늦어질 수 밖에 없었을 텐데, 이제 그런 걱정은 더이상 하지 않아도 된다! ^^

※ 당연히 명령 패킷 송신에 대한 응답 패킷 수신 해석도 버리는 패킷 없이 모두 할 수 있으니 스마트홈 플랫폼 - 코드간 연동 속도도 소폭 향상!

3. 깃허브(GitHub) 커밋

https://github.com/YOGYUI/HomeNetwork/commit/d17331e88bc7d6273c5abac8a80f59f1170fa3e6

 

패킷 여러개 뭉쳐서 수신될 경우 전체 버퍼 해석 시도하도록 기능 강화 · YOGYUI/HomeNetwork@d17331e

YOGYUI committed Jan 11, 2024

github.com

커밋 아이디: d17331e88bc7d6273c5abac8a80f59f1170fa3e6

기존 코드를 클론해 사용중인 유저라면 클론한 경로에서 깃 풀(git pull) 명령어만 입려하면 코드를 최신화할 수 있다

$ git pull

 

만약 git fork로 포크해가서 사용중인 유저라면 코드 최신화는 다음 글을 참고하기 바란다 

https://json.postype.com/post/210431

 

4. 참고사항

위 변경사항은 힐스테이트 광교산 코드(현대통신)에만 적용했다

Bestin 기반이었던 광교 아이파크 코드는 2년 넘게 방치중인 상태 ㅎㅎ... (실제 거주하고 있지 않다보니 관심도가 크게 떨어질 뿐더러 코드 변경에 대한 테스트도 불가능한 상황 ㅠ)

만약 광교 아이파크 코드도 동일한 수정이 필요한 분이 있다면 댓글이나 이메일(lee2002w@gmail.com)으로 문의를 바랍니다~

 


끝~!

 

반응형
Comments