Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Implemented time response for cloudless time synchronization #82

Open
wants to merge 26 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
87abc88
feat: Implemented response to heartbeat packets
davidrapan Dec 22, 2024
22b64ca
refactor: Add _v5_heartbeat_response_frame
davidrapan Dec 22, 2024
257003c
refactor: V5_HEARTBEAT error message
davidrapan Dec 22, 2024
1afac93
refactor: Rename _v5_heartbeat_response_frame to _v5_time_response_frame
davidrapan Dec 22, 2024
8b23962
feat: Add V5_HANDSHAKE handling for server mode
davidrapan Dec 22, 2024
1424502
feat: Add V5_DATA and V5_WIFI handling for server mode
davidrapan Dec 22, 2024
f16ac48
refactor: _handle_protocol_frame through _received_frame_response
davidrapan Dec 22, 2024
9163dfd
refactor: Add variable continue to _received_frame_response
davidrapan Dec 22, 2024
ac76b6c
fix: _v5_time_response_frame argument name
davidrapan Dec 24, 2024
c43846c
refactor: _v5_time_response_frame and _received_frame_response method…
davidrapan Dec 25, 2024
e5d4d94
feat: Add control codes
davidrapan Dec 26, 2024
86398b5
refactor: Encoder & Decoder to use control codes
davidrapan Dec 26, 2024
4cceb1f
refactor: Extract _v5_header from _v5_frame_encoder and thus reuse he…
davidrapan Dec 26, 2024
4a36f3b
fix: _v5_frame_decoder control condition
davidrapan Dec 26, 2024
1889f9d
refactor: Add _get_response_code to get control code from response co…
davidrapan Dec 26, 2024
091bc96
fix: V5 frame encoder
davidrapan Dec 26, 2024
0a08116
fix: v5_magic as int
davidrapan Dec 26, 2024
cbd2e67
refactor: Extract _v5_trailer from _v5_frame_encoder to be reused
davidrapan Dec 26, 2024
4d120f7
refactor: Turn _get_response_code into staticmethod
davidrapan Dec 26, 2024
e2d1411
refactor: Generalize _received_frame_response
davidrapan Dec 26, 2024
5b19f62
refactor: Use of negative indexing
davidrapan Dec 26, 2024
5a0f55e
refactor: serial, v5_serial and v5_loggerserial were hella confusing!!!
davidrapan Dec 26, 2024
a5a0712
refactor: MockDatalogger to reuse _v5_header and _v5_trailer "constru…
davidrapan Dec 26, 2024
c4ce1f7
feat: Update protocol docs with known control codes
davidrapan Dec 26, 2024
ade1c3a
feat: Add argument & return types
davidrapan Dec 26, 2024
652fbf8
refactor: Use of PySolarmanV5._get_response_code in tests
davidrapan Dec 26, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 37 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 @@ -265,6 +266,26 @@ def _v5_frame_decoder(self, v5_frame):

return modbus_frame

def _v5_heartbeat_response_frame(self, heartbeat_frame):
"""
Creates response to 0x4710 (heartbeat frame)
"""
response_frame = bytearray(
self.v5_start
+ struct.pack("<H", 10)
+ heartbeat_frame[3: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)
return response_frame

def _send_receive_v5_frame(self, data_logging_stick_frame):
"""Send v5 frame to the data logger and receive response

Expand Down Expand Up @@ -299,18 +320,28 @@ 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):
davidrapan marked this conversation as resolved.
Show resolved Hide resolved
"""
Handles protocol frames 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 = self._v5_heartbeat_response_frame(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 +381,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
29 changes: 28 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,30 @@ 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 protocol frames 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 = self._v5_heartbeat_response_frame(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 +210,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