22import socket
33from collections .abc import AsyncIterator
44from contextlib import asynccontextmanager
5- from typing import Literal , assert_never
5+ from dataclasses import dataclass
6+ from typing import Literal
67
78
89class AdamConnectionError (RuntimeError ):
@@ -13,90 +14,110 @@ class AdamConnectionError(RuntimeError):
1314ADAM_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