Skip to content

Commit 12a1cbb

Browse files
authored
Merge pull request #12 from houlihaj/feature-011-implement-ftdi-interface
Add ability to connect using the FTDI drivers
2 parents a27edb1 + d885ea9 commit 12a1cbb

8 files changed

Lines changed: 210 additions & 20 deletions

File tree

examples/connect_ftdi.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import time
2+
import logging
3+
import ftd2xx
4+
from mecompyapi.tec1090series import MeerstetterTEC
5+
6+
7+
if __name__ == '__main__':
8+
# start logging
9+
logging.basicConfig(level=logging.DEBUG, format="%(asctime)s:%(module)s:%(levelname)s:%(message)s")
10+
11+
# initialize controller
12+
mc = MeerstetterTEC()
13+
14+
mc.connect_ftdi(id_str="DK0E1IDC")
15+
16+
identity = mc.get_id()
17+
print(f"identity: {identity}")
18+
print("\n", end="")
19+
20+
mc.tear()

examples/connect_serial_port.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import time
2+
import logging
3+
from mecompyapi.tec1090series import MeerstetterTEC
4+
5+
6+
if __name__ == '__main__':
7+
# start logging
8+
logging.basicConfig(level=logging.DEBUG, format="%(asctime)s:%(module)s:%(levelname)s:%(message)s")
9+
10+
# initialize controller
11+
mc = MeerstetterTEC()
12+
13+
mc.connect_serial_port(port="COM9")
14+
15+
identity = mc.get_id()
16+
print(f"identity: {identity}")
17+
print("\n", end="")
18+
19+
mc.tear()

examples/query_all_settings.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
# initialize controller
1111
mc = MeerstetterTEC()
1212

13-
mc.connect(port="COM13")
13+
mc.connect_serial_port(port="COM9")
1414

1515
identity = mc.get_id()
1616
print(f"identity: {identity}")

requirements.txt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
pyserial
2-
pythoncrc
2+
pythoncrc
3+
ftd2xx

setup.cfg

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,10 @@ classifiers =
1313
Programming Language :: Python :: 3
1414
Programming Language :: Python :: 3.9
1515
Programming Language :: Python :: 3.10
16+
Programming Language :: Python :: 3.11
1617
License :: OSI Approved :: MIT License
1718
Operating System :: Microsoft :: Windows
18-
19+
1920
[options]
2021
package_dir =
2122
= src
@@ -24,6 +25,7 @@ python_requires = >=3.9
2425
install_requires =
2526
pyserial>=3.5
2627
pythoncrc>=1.21
28+
ftd2xx>=1.3.3
2729
include_package_data=True
2830

2931
[options.packages.find]

src/mecompyapi/mecom_core/mecom_query_set.py

Lines changed: 9 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
import random
22

3+
from typing import Union
4+
35
from mecompyapi.mecom_core.mecom_frame import MeComFrame, MeComPacket, ERcvType
46
from mecompyapi.phy_wrapper.int_mecom_phy import IntMeComPhy, MeComPhyTimeoutException
57
from mecompyapi.phy_wrapper.mecom_phy_serial_port import MeComPhySerialPort
8+
from mecompyapi.phy_wrapper.mecom_phy_ftdi import MeComPhyFtdi
69

710

811
class SetServerErrorException(Exception):
@@ -55,10 +58,7 @@ class MeComQuerySet:
5558
otherwise it will throw an exception.
5659
"""
5760

58-
# sequence_number: int
59-
# statistics = Statistics()
60-
61-
def __init__(self, phy_com: MeComPhySerialPort):
61+
def __init__(self, phy_com: Union[MeComPhySerialPort, MeComPhyFtdi]):
6262
"""
6363
Initializes the communication interface.
6464
This object can then be passed to the Command objects like MeBasicCmd.
@@ -73,13 +73,7 @@ def __init__(self, phy_com: MeComPhySerialPort):
7373
self.is_ready = False
7474
self.version_is_okay = False
7575
self.default_device_address = 1
76-
77-
# def get(self):
78-
# raise NotImplementedError
79-
#
80-
# def set(self):
81-
# raise NotImplementedError
82-
76+
8377
def get_is_ready(self):
8478
"""
8579
true when the interface is ready to use; false if not.
@@ -302,7 +296,8 @@ def local_query(self, tx_frame: MeComPacket) -> MeComPacket:
302296
)
303297
if rx_frame.sequence_number != self.sequence_number:
304298
raise GeneralException(
305-
f"Query failed : Wrong Sequence Number received. Received {rx_frame.sequence_number} ; Expected {self.sequence_number}"
299+
f"Query failed : Wrong Sequence Number received. "
300+
f"Received {rx_frame.sequence_number} ; Expected {self.sequence_number}"
306301
)
307302
if rx_frame.address != tx_frame.address:
308303
raise GeneralException(
@@ -357,7 +352,8 @@ def local_set(self, tx_frame: MeComPacket) -> MeComPacket:
357352
# Communication failed, check last error
358353
if rx_frame.sequence_number != self.sequence_number:
359354
raise GeneralException(
360-
f"Set failed: Wrong Sequence Number received. Received {rx_frame.sequence_number} ; Expected {self.sequence_number}"
355+
f"Set failed: Wrong Sequence Number received. "
356+
f"Received {rx_frame.sequence_number} ; Expected {self.sequence_number}"
361357
)
362358

363359
if rx_frame.address != tx_frame.address:
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
import ftd2xx
2+
from ftd2xx import FTD2XX
3+
4+
from mecompyapi.phy_wrapper.int_mecom_phy import (
5+
IntMeComPhy, MeComPhyInterfaceException, MeComPhyTimeoutException
6+
)
7+
8+
9+
class MeComPhyFtdi(IntMeComPhy):
10+
"""
11+
Implements the IMeComPhy interface for the FTDI chip drivers.
12+
"""
13+
def __init__(self):
14+
"""
15+
Implements the IMeComPhy interface for the FTDI chip drivers.
16+
"""
17+
super().__init__()
18+
self.ftdi = None
19+
20+
def mecom_set_default_settings(self, baudrate: int):
21+
"""
22+
Initializes the FTDI default settings, so that is it usually running
23+
with Meerstetter products.
24+
25+
This function is not part of the IMeComPhy interface.
26+
27+
:param baudrate: Baud Rate for the Serial Interface.
28+
:type baudrate: int
29+
"""
30+
raise NotImplementedError
31+
32+
def connect(self, id_str: str, timeout: int = 1, baudrate: int = 57600) -> None:
33+
"""
34+
Open a handle to an usb device by serial number(default), description or
35+
location(Windows only) depending on value of flags and return an FTD2XX
36+
instance for it.
37+
38+
:param id_str: ID string from listDevices
39+
:type id_str: str
40+
:param timeout: Time in seconds for read timeout. If timeout happens, read returns empty string.
41+
:type timeout: int
42+
:param baudrate: The baud rate setting.
43+
:type baudrate: int
44+
:raises SerialException:
45+
:raises InstrumentConnectionError:
46+
:return: None
47+
"""
48+
id_str_bytes: bytes = id_str.encode()
49+
self.ftdi: FTD2XX = ftd2xx.openEx(id_str=id_str_bytes)
50+
self.ftdi.purge(mask=3) # purges receive and transmit buffer in the device
51+
self.ftdi.setTimeouts(read=timeout * 1000, write=timeout * 1000)
52+
53+
def tear(self):
54+
"""
55+
Tear should always be called when the instrument is being disconnected. It should
56+
also be called when the program is ending.
57+
"""
58+
self.ftdi.purge(mask=3) # purges receive and transmit buffer in the device
59+
self.ftdi.close()
60+
61+
def send_string(self, stream: str):
62+
"""
63+
Sends data to the physical interface.
64+
65+
:param stream: The whole content of the Stream is sent to the physical interface.
66+
:type stream: str
67+
"""
68+
self.ftdi.purge(mask=3) # purges receive and transmit buffer in the device
69+
70+
stream_bytes = stream.encode()
71+
72+
self.ftdi.write(data=stream_bytes)
73+
74+
def get_data_or_timeout(self):
75+
"""
76+
Tries to read data from the physical interface or throws a timeout exception.
77+
78+
Reads the available data in the physical interface buffer and returns immediately.
79+
If the receiving buffer is empty, it tries to read at least one byte.
80+
It will wait till the timeout occurs if nothing is received.
81+
Must probably be called several times to receive the whole frame.
82+
83+
:raises MeComPhyInterfaceException: Thrown when the underlying physical interface
84+
is not OK.
85+
:raises MeComPhyTimeoutException: Thrown when 0 bytes were received during the
86+
specified timeout time.
87+
"""
88+
try:
89+
# initialize response and carriage return
90+
cr = "\r".encode()
91+
response_frame = b''
92+
response_byte = self.ftdi.read(nchars=1) # read one byte at a time, timeout is set on instance level
93+
94+
# read until stop byte
95+
while response_byte != cr:
96+
response_frame += response_byte
97+
response_byte = self.ftdi.read(nchars=1)
98+
99+
return response_frame.decode()
100+
101+
except Exception as e:
102+
raise MeComPhyInterfaceException(f"Failure during receiving: {e}")
103+
104+
def change_speed(self, baudrate: int):
105+
"""
106+
Used to change the Serial Speed in case of serial communication interfaces.
107+
108+
:param baudrate: Baud Rate for the Serial Interface.
109+
:type baudrate: int
110+
"""
111+
raise NotImplementedError
112+
113+
def set_timeout(self, milliseconds: int):
114+
"""
115+
Used to modify the standard timeout of the physical interface.
116+
117+
:param milliseconds:
118+
:type milliseconds: int
119+
"""
120+
raise NotImplementedError

src/mecompyapi/tec1090series.py

Lines changed: 36 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,14 @@
44
import datetime
55
import statistics
66
from enum import Enum
7-
from typing import Tuple, List, Optional
7+
from typing import Tuple, List, Optional, Union
88

99
from mecompyapi.mecom_core.mecom_query_set import MeComQuerySet
1010
from mecompyapi.mecom_core.mecom_basic_cmd import MeComBasicCmd
1111
from mecompyapi.mecom_core.com_command_exception import ComCommandException
1212

13-
from mecompyapi.phy_wrapper.mecom_phy_serial_port import MeComPhySerialPort, InstrumentException, ResponseTimeout
13+
from mecompyapi.phy_wrapper.mecom_phy_serial_port import MeComPhySerialPort, InstrumentException
14+
from mecompyapi.phy_wrapper.mecom_phy_ftdi import MeComPhyFtdi
1415

1516
from mecompyapi.mecom_tec.lookup_table.lut_cmd import LutCmd
1617
from mecompyapi.mecom_tec.lookup_table.lut_status import LutStatus
@@ -118,14 +119,14 @@ class MeerstetterTEC(object):
118119
./meerstetter/pyMeCom/mecom/commands.py
119120
"""
120121
def __init__(self, *args, **kwargs) -> None:
121-
self.phy_com = MeComPhySerialPort()
122+
self.phy_com = None # type: Optional[Union[MeComPhySerialPort, MeComPhyFtdi]]
122123
self.mequery_set = None # type: Optional[MeComQuerySet]
123124
self.mecom_basic_cmd = None # type: Optional[MeComBasicCmd]
124125
self.mecom_lut_cmd = None # type: Optional[LutCmd]
125126
self.address = None # type: Optional[int]
126127
self.instance = None # type: Optional[int]
127128

128-
def connect(self, port: str = "COM9", instance: int = 1):
129+
def connect_serial_port(self, port: str = "COM9", instance: int = 1):
129130
"""
130131
Connects to a serial port. On Windows, these are typically 'COMX' where X
131132
is the number of the port. In Linux, they are often /dev/ttyXXXY where XXX
@@ -143,6 +144,8 @@ def connect(self, port: str = "COM9", instance: int = 1):
143144
:type instance: int
144145
:return: None
145146
"""
147+
self.phy_com = MeComPhySerialPort()
148+
146149
self.instance = instance
147150
self.phy_com.connect(port_name=port)
148151
mequery_set = MeComQuerySet(phy_com=self.phy_com)
@@ -159,6 +162,35 @@ def connect(self, port: str = "COM9", instance: int = 1):
159162
continue
160163
raise InstrumentException(f"Could not successfully query the controller address after {retries} retries...")
161164

165+
def connect_ftdi(self, id_str: str = "DK0E1IDC", instance: int = 1):
166+
"""
167+
Connect to the controller using the FTDI chip drivers.
168+
169+
:param id_str:
170+
:type id_str: str
171+
:param instance:
172+
:type instance: int
173+
:return: None
174+
"""
175+
self.phy_com = MeComPhyFtdi()
176+
177+
self.instance = instance
178+
self.phy_com.connect(id_str=id_str)
179+
mequery_set = MeComQuerySet(phy_com=self.phy_com)
180+
self.mecom_basic_cmd = MeComBasicCmd(mequery_set=mequery_set)
181+
self.mecom_lut_cmd = LutCmd(mecom_query_set=mequery_set)
182+
183+
retries = 3
184+
for _ in range(retries):
185+
try:
186+
self.address = self.get_device_address()
187+
logging.debug(f"connected to {self.address}")
188+
return
189+
except ComCommandException as e:
190+
logging.debug(f"[ComCommandException] : {e}")
191+
continue
192+
raise InstrumentException(f"Could not successfully query the controller address after {retries} retries...")
193+
162194
def tear(self):
163195
"""
164196
Tear should always be called when the instrument is being disconnected. It should

0 commit comments

Comments
 (0)