Skip to content

Commit f0de1c2

Browse files
chore: refactor to object inerface
1 parent 0244fb4 commit f0de1c2

File tree

1 file changed

+85
-64
lines changed

1 file changed

+85
-64
lines changed

adam_modbus/interface.py

Lines changed: 85 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@
22
import socket
33
from collections.abc import AsyncIterator
44
from contextlib import asynccontextmanager
5-
from typing import Literal, assert_never
5+
from dataclasses import dataclass
6+
from typing import Literal
67

78

89
class AdamConnectionError(RuntimeError):
@@ -13,90 +14,110 @@ class AdamConnectionError(RuntimeError):
1314
ADAM_CONNECTION_TIMEOUT = 0.1
1415

1516

16-
@asynccontextmanager
17-
async def adam_socket_context(
18-
ip: str,
19-
port: int = DEFAULT_ADAM_PORT,
20-
) -> AsyncIterator[socket.socket]:
21-
adam_sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
17+
@dataclass
18+
class AdamConnection:
19+
socket: socket.socket
20+
ip: str
21+
port: int
22+
timeout: float = ADAM_CONNECTION_TIMEOUT
23+
model: str | None = None
2224

23-
loop = asyncio.get_running_loop()
25+
async def _send_and_receive(self, message: str) -> str:
26+
loop = asyncio.get_running_loop()
2427

25-
# according to chatgpt, asyncio does not support timeouts on UDP sockets
26-
# so we manually do this with asyncio.wait_for, otherwise it will hang forever
27-
adam_sock.setblocking(False)
28+
try:
29+
await asyncio.wait_for(
30+
loop.sock_sendall(self.socket, message.encode("ascii")),
31+
self.timeout,
32+
)
33+
adam_out = await asyncio.wait_for(
34+
loop.sock_recv(self.socket, 100), self.timeout
35+
)
36+
except asyncio.TimeoutError:
37+
raise AdamConnectionError("ADAM connection timed out")
2838

29-
try:
30-
await loop.sock_connect(adam_sock, (ip, port))
31-
yield adam_sock
39+
response = adam_out.decode().strip()
40+
return response
3241

33-
except OSError:
34-
raise AdamConnectionError(f"Could not connect to ADAM at {ip}")
42+
async def set_digital_out(
43+
self,
44+
pin: int,
45+
value: bool,
46+
model: None | Literal["6052"] | Literal["6317"] = None,
47+
) -> None:
48+
if model is not None:
49+
self.model = model
3550

36-
finally:
37-
adam_sock.close()
51+
assert self.model is not None, "Model must be set before setting digital out"
3852

53+
if self.model == "6052":
54+
command = f"#011{pin:x}0{int(value)}\r"
55+
elif self.model == "6317":
56+
command = f"#01D0{pin:x}{int(value)}\r"
57+
else:
58+
raise NotImplementedError(
59+
f"Digital out not implemented for Adam-{self.model}"
60+
)
3961

40-
async def _adam_send_and_receive(message: str, adam_socket: socket.socket) -> str:
41-
loop = asyncio.get_running_loop()
62+
response = await self._send_and_receive(command)
63+
assert response[:3] == ">01", response[:3]
4264

43-
try:
44-
await asyncio.wait_for(
45-
loop.sock_sendall(adam_socket, message.encode("ascii")),
46-
ADAM_CONNECTION_TIMEOUT,
47-
)
48-
adam_out = await asyncio.wait_for(
49-
loop.sock_recv(adam_socket, 100), ADAM_CONNECTION_TIMEOUT
50-
)
51-
except asyncio.TimeoutError:
52-
raise AdamConnectionError("ADAM connection timed out")
65+
async def get_adam_digital_inputs(self) -> list[bool]:
66+
response = await self._send_and_receive("$016\r")
67+
assert response[:3] == "!01", f"Unexpected response: {response}"
5368

54-
response = adam_out.decode().strip()
55-
return response
69+
binary_string = "".join(f"{int(char, 16):0>4b}" for char in response[3:])
70+
return [char == "1" for char in binary_string][::-1]
5671

72+
async def get_adam_analog_inputs(self) -> list[float]:
73+
response = await self._send_and_receive("#01\r")
5774

58-
async def set_adam_digital_out(
59-
socket: socket.socket,
60-
model: Literal["6052"] | Literal["6317"],
61-
pin: int,
62-
value: bool,
63-
) -> None:
64-
if model == "6052":
65-
command = f"#011{pin:x}0{int(value)}\r"
66-
elif model == "6317":
67-
command = f"#01D0{pin:x}{int(value)}\r"
68-
else:
69-
assert_never(model)
75+
assert response[:3] == ">01", response
76+
response_data = response[3:]
7077

71-
response = await _adam_send_and_receive(command, socket)
72-
assert response[:3] == ">01", response[:3]
78+
# 7 characters per channel: +00.011
79+
assert len(response_data) % 7 == 0, response_data
7380

81+
return [
82+
float(response_data[i * 7 : i * 7 + 7])
83+
for i in range(len(response_data) // 7)
84+
]
7485

75-
async def get_adam_digital_inputs(socket: socket.socket) -> list[bool]:
76-
response = await _adam_send_and_receive("$016\r", socket)
77-
assert response[:3] == "!01", f"Unexpected response: {response}"
86+
async def get_adam_model(self) -> str:
87+
response = await self._send_and_receive("$01M\r")
7888

79-
binary_string = "".join(f"{int(char, 16):0>4b}" for char in response[3:])
80-
return [char == "1" for char in binary_string][::-1]
89+
assert response[:3] == "!01", f"Unexpected response: {response}"
8190

91+
self.model = response[3:]
8292

83-
async def get_adam_analog_inputs(socket: socket.socket) -> list[float]:
84-
response = await _adam_send_and_receive("#01\r", socket)
93+
return self.model
8594

86-
assert response[:3] == ">01", response
87-
response_data = response[3:]
8895

89-
# 7 characters per channel: +00.011
90-
assert len(response_data) % 7 == 0, response_data
96+
@asynccontextmanager
97+
async def adam_connection_context(
98+
ip: str,
99+
port: int = DEFAULT_ADAM_PORT,
100+
timeout: float = ADAM_CONNECTION_TIMEOUT,
101+
) -> AsyncIterator[AdamConnection]:
102+
adam_sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
91103

92-
return [
93-
float(response_data[i * 7 : i * 7 + 7]) for i in range(len(response_data) // 7)
94-
]
104+
loop = asyncio.get_running_loop()
95105

106+
# according to chatgpt, asyncio does not support timeouts on UDP sockets
107+
# so we manually do this with asyncio.wait_for, otherwise it will hang forever
108+
adam_sock.setblocking(False)
96109

97-
async def get_adam_model(socket: socket.socket) -> str:
98-
response = await _adam_send_and_receive("$01M\r", socket)
110+
try:
111+
await loop.sock_connect(adam_sock, (ip, port))
112+
yield AdamConnection(
113+
adam_sock,
114+
ip,
115+
port,
116+
timeout,
117+
)
99118

100-
assert response[:3] == "!01", f"Unexpected response: {response}"
119+
except OSError:
120+
raise AdamConnectionError(f"Could not connect to ADAM at {ip}")
101121

102-
return response[3:]
122+
finally:
123+
adam_sock.close()

0 commit comments

Comments
 (0)