mirror of
https://github.com/meshtastic/python.git
synced 2026-06-02 12:45:00 -04:00
144 lines
4.5 KiB
Python
144 lines
4.5 KiB
Python
"""TCPInterface class for interfacing with http endpoint
|
|
"""
|
|
# pylint: disable=R0917
|
|
import contextlib
|
|
import logging
|
|
import socket
|
|
import threading
|
|
import time
|
|
from typing import Optional
|
|
|
|
from meshtastic.stream_interface import StreamInterface
|
|
|
|
DEFAULT_TCP_PORT = 4403
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class TCPInterface(StreamInterface):
|
|
"""Interface class for meshtastic devices over a TCP link"""
|
|
|
|
def __init__(
|
|
self,
|
|
hostname: str,
|
|
debugOut=None,
|
|
noProto: bool = False,
|
|
connectNow: bool = True,
|
|
portNumber: int = DEFAULT_TCP_PORT,
|
|
noNodes: bool = False,
|
|
timeout: int = 300,
|
|
):
|
|
"""Constructor, opens a connection to a specified IP address/hostname
|
|
|
|
Keyword Arguments:
|
|
hostname {string} -- Hostname/IP address of the device to connect to
|
|
timeout -- How long to wait for replies (default: 300 seconds)
|
|
"""
|
|
|
|
self.stream = None
|
|
|
|
self.hostname: str = hostname
|
|
self.portNumber: int = portNumber
|
|
|
|
self.socket: Optional[socket.socket] = None
|
|
self.reconnectLock = threading.Lock()
|
|
|
|
if connectNow:
|
|
self.myConnect()
|
|
else:
|
|
self.socket = None
|
|
|
|
super().__init__(
|
|
debugOut=debugOut,
|
|
noProto=noProto,
|
|
connectNow=connectNow,
|
|
noNodes=noNodes,
|
|
timeout=timeout,
|
|
)
|
|
|
|
def __repr__(self):
|
|
rep = f"TCPInterface({self.hostname!r}"
|
|
if self.debugOut is not None:
|
|
rep += f", debugOut={self.debugOut!r}"
|
|
if self.noProto:
|
|
rep += ", noProto=True"
|
|
if self.socket is None:
|
|
rep += ", connectNow=False"
|
|
if self.portNumber != DEFAULT_TCP_PORT:
|
|
rep += f", portNumber={self.portNumber!r}"
|
|
if self.noNodes:
|
|
rep += ", noNodes=True"
|
|
rep += ")"
|
|
return rep
|
|
|
|
def _socket_shutdown(self) -> None:
|
|
"""Shutdown the socket.
|
|
Note: Broke out this line so the exception could be unit tested.
|
|
"""
|
|
if self.socket is not None:
|
|
self.socket.shutdown(socket.SHUT_RDWR)
|
|
|
|
def myConnect(self) -> None:
|
|
"""Connect to socket."""
|
|
logger.debug(f"Connecting to {self.hostname}") # type: ignore[str-bytes-safe]
|
|
server_address = (self.hostname, self.portNumber)
|
|
self.socket = socket.create_connection(server_address)
|
|
|
|
def close(self) -> None:
|
|
"""Close a connection to the device."""
|
|
logger.debug("Closing TCP stream")
|
|
# Sometimes the socket read might be blocked in the reader thread.
|
|
# Therefore force a shutdown first to unblock reader thread reads.
|
|
self._wantExit = True
|
|
if self.socket is not None:
|
|
with contextlib.suppress(
|
|
Exception
|
|
): # Ignore errors in shutdown, because we might have a race with the server
|
|
self._socket_shutdown()
|
|
with contextlib.suppress(Exception):
|
|
self.socket.close()
|
|
|
|
self.socket = None
|
|
super().close()
|
|
|
|
def _writeBytes(self, b: bytes) -> None:
|
|
"""Write an array of bytes to our stream"""
|
|
if self.socket is not None:
|
|
try:
|
|
self.socket.sendall(b)
|
|
except OSError as e:
|
|
logger.error(f"Socket send error, reconnecting: {e}")
|
|
self._reconnect()
|
|
|
|
def _readBytes(self, length) -> Optional[bytes]:
|
|
"""Read an array of bytes from our stream"""
|
|
if self.socket is not None:
|
|
data = self.socket.recv(length)
|
|
# empty byte indicates a disconnected socket,
|
|
# we need to handle it to avoid an infinite loop reading from null socket
|
|
if data == b"":
|
|
logger.debug("Closed socket, re-connecting")
|
|
self._reconnect()
|
|
return data
|
|
|
|
# no socket, break reader thread
|
|
self._wantExit = True
|
|
return None
|
|
|
|
def _reconnect(self) -> None:
|
|
"""Reconnect to the socket"""
|
|
# Save the socket reference before attempting to acquire the lock.
|
|
sock = self.socket
|
|
with self.reconnectLock:
|
|
# Don't reconnect: someone else already did it.
|
|
if sock is not self.socket:
|
|
return
|
|
|
|
with contextlib.suppress(Exception):
|
|
self._socket_shutdown()
|
|
if self.socket is not None:
|
|
self.socket.close()
|
|
self.socket = None
|
|
time.sleep(1)
|
|
self.myConnect()
|
|
self._startConfig()
|