Skip to content

Commit

Permalink
feat: Implemented response to heartbeat packets
Browse files Browse the repository at this point in the history
  • Loading branch information
davidrapan committed Dec 22, 2024
1 parent 0a02d1f commit 42d0d90
Show file tree
Hide file tree
Showing 2 changed files with 73 additions and 5 deletions.
35 changes: 31 additions & 4 deletions pysolarmanv5/pysolarmanv5.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
"""pysolarmanv5.py"""

import time
import errno
import queue
import struct
import socket
import logging
import selectors
import platform
import selectors

from threading import Thread, Event
from multiprocessing import Queue
Expand Down Expand Up @@ -299,18 +300,42 @@ def _send_receive_v5_frame(self, data_logging_stick_frame):
return v5_response

def _received_frame_is_valid(self, frame):
"""Check that the frame is valid and that the serial number of the received
"""
Check that the frame is valid and that the serial number of the received
frame matches with the last sent one.
Ignore also any frames with control code 0x4710 (counter frame).
"""
if not frame.startswith(self.v5_start):
self.log.debug("[%s] V5_MISMATCH: %s", self.serial, frame.hex(" "))
return False
if frame[5] != self.sequence_number:
self.log.debug("[%s] V5_SEQ_NO_MISMATCH: %s", self.serial, frame.hex(" "))
return False
return True

def _handle_protocol_frame(self, frame):
"""
Handles any protocol frame with control code 0x4710 (heartbeat frame).
"""
if frame.startswith(self.v5_start + b"\x01\x00\x10\x47"):
self.log.debug("[%s] COUNTER: %s", self.serial, frame.hex(" "))
self.log.debug("[%s] V5_HEARTBEAT: %s", self.serial, frame.hex(" "))
response_frame = bytearray(
self.v5_start
+ struct.pack("<H", 10)
+ frame[3:5]
+ frame[5:7]
+ self.v5_loggerserial
+ struct.pack("<H", 0x0100)
+ struct.pack("<I", int(time.time()))
+ struct.pack("<I", 0)
+ self.v5_checksum
+ self.v5_end
)
response_frame[4] = response_frame[4] - 0x30
response_frame[5] = (response_frame[5] + 1) & 0xFF
response_frame[-2] = self._calculate_v5_frame_checksum(response_frame)
self.log.debug("[%s] V5_HEARTBEAT RESP: %s", self.serial, response_frame.hex(" "))
if self._reader_thr.is_alive():
self.sock.sendall(response_frame)
return False
return True

Expand Down Expand Up @@ -350,6 +375,8 @@ def _data_receiver(self):
return
if not self._received_frame_is_valid(data):
continue
if not self._handle_protocol_frame(data):
continue
if self._data_wanted.is_set():
self._data_queue.put(data, timeout=self.socket_timeout)
else:
Expand Down
43 changes: 42 additions & 1 deletion pysolarmanv5/pysolarmanv5_async.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
"""pysolarmanv5_async.py"""

import time
import errno
import asyncio
import struct
import asyncio

from multiprocessing import Event
from umodbus.client.serial import rtu
Expand Down Expand Up @@ -160,6 +161,44 @@ def _send_data(self, data: bytes):
self.data_queue.put_nowait(data)
self.data_wanted_ev.clear()

async def _handle_protocol_frame(self, frame):
"""
Handles any protocol frame with control code 0x4710 (heartbeat frame).
"""
if frame.startswith(self.v5_start + b"\x01\x00\x10\x47"):
self.log.debug("[%s] V5_HEARTBEAT: %s", self.serial, frame.hex(" "))
response_frame = bytearray(
self.v5_start
+ struct.pack("<H", 10)
+ frame[3:5]
+ frame[5:7]
+ self.v5_loggerserial
+ struct.pack("<H", 0x0100)
+ struct.pack("<I", int(time.time()))
+ struct.pack("<I", 0)
+ self.v5_checksum
+ self.v5_end
)
response_frame[4] = response_frame[4] - 0x30
response_frame[5] = (response_frame[5] + 1) & 0xFF
response_frame[-2] = self._calculate_v5_frame_checksum(response_frame)
self.log.debug("[%s] V5_HEARTBEAT RESP: %s", self.serial, response_frame.hex(" "))
try:
self.writer.write(response_frame)
await self.writer.drain()
except (AttributeError, NoSocketAvailableError, TimeoutError, OSError) as e:
if isinstance(e, AttributeError):
e = NoSocketAvailableError("Connection already closed")
if isinstance(e, OSError) and e.errno == errno.EHOSTUNREACH:
e = TimeoutError
self.log.debug( # pylint: disable=logging-fstring-interpolation
f"[{self.serial}] V5_HEARTBEAT error: {type(e).__name__}{f': {e}' if f'{e}' else ''}"
)
except Exception as e:
self.log.exception("[%s] Send/Receive error: %s", self.serial, e)
return False
return True

async def _conn_keeper(self) -> None:
"""
Socket reader loop with extra logic when auto-reconnect is enabled
Expand All @@ -185,6 +224,8 @@ async def _conn_keeper(self) -> None:
break
if not self._received_frame_is_valid(data):
continue
if not await self._handle_protocol_frame(data):
continue
if self.data_wanted_ev.is_set():
self._send_data(data)
else:
Expand Down

0 comments on commit 42d0d90

Please sign in to comment.