Skip to content

Commit

Permalink
Merge pull request #991 from dkriegner/inficon_sqm160
Browse files Browse the repository at this point in the history
Add new instrument: Inficon SQM160 SQM-160 multi-film rate/thickness monitor
  • Loading branch information
BenediktBurger committed Dec 21, 2023
2 parents b4441d3 + 4f309f2 commit dcb1cd4
Show file tree
Hide file tree
Showing 7 changed files with 365 additions and 0 deletions.
1 change: 1 addition & 0 deletions docs/api/instruments/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ Instruments by manufacturer:
heidenhain/index
hcp/index
hp/index
inficon/index
ipgphotonics/index
keithley/index
keysight/index
Expand Down
12 changes: 12 additions & 0 deletions docs/api/instruments/inficon/index.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
.. module:: pymeasure.instruments.inficon

#######
Inficon
#######

This section contains specific documentation on the Inficon instruments that are implemented. If you are interested in an instrument not included, please consider :doc:`adding the instrument </dev/adding_instruments/index>`.

.. toctree::
:maxdepth: 2

sqm160
11 changes: 11 additions & 0 deletions docs/api/instruments/inficon/sqm160.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
#################################################
Inficon SQM-160 multi-film rate/thickness monitor
#################################################

.. autoclass:: pymeasure.instruments.inficon.sqm160.SQM160
:members:
:show-inheritance:

.. autoclass:: pymeasure.instruments.inficon.sqm160.SensorChannel
:members:
:show-inheritance:
1 change: 1 addition & 0 deletions pymeasure/instruments/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@
from . import hcp
from . import heidenhain
from . import hp
from . import inficon
from . import ipgphotonics
from . import keithley
from . import keysight
Expand Down
25 changes: 25 additions & 0 deletions pymeasure/instruments/inficon/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
#
# This file is part of the PyMeasure package.
#
# Copyright (c) 2013-2023 PyMeasure Developers
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
#

from .sqm160 import SQM160
227 changes: 227 additions & 0 deletions pymeasure/instruments/inficon/sqm160.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,227 @@
#
# This file is part of the PyMeasure package.
#
# Copyright (c) 2013-2023 PyMeasure Developers
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
#

from pymeasure.instruments import Channel, Instrument


def calculate_checksum(msg):
"""calculate a two byte Cyclic Redundancy Check based on 14 bits
Parameters
----------
msg: bytes
Message of the device without the sync character
"""
# check if message contains data
if not msg:
return chr(0) + chr(0)
# initialize CRC
crc = 0x3fff
# loop over characters in message
for char in msg:
crc ^= char
for i in range(8):
tmpcrc = crc
crc = crc >> 1
if tmpcrc & 1 == 1:
crc ^= 0x2001
crc &= 0x3fff
# separate 14 significant bits in two byte checksum
return bytes(((crc & 0x7f) + 34, ((crc >> 7) & 0x7f) + 34))


class SensorChannel(Channel):
"""Sensor channel for individual rate measurements."""

rate = Channel.measurement(
"L{ch}?", """Get the current rate for a sensor in Angstrom per second""",
cast=float,
)

thickness = Channel.measurement(
"N{ch}", """Get the current thickness for a sensor in Angstrom""",
cast=float,
)

frequency = Channel.measurement(
"P{ch}", """Get the current frequency for a sensor in Hz""",
cast=float,
)

crystal_life = Channel.measurement(
"R{ch}", """Get the crystal life value in percent""",
cast=float,
)


class SQM160(Instrument):
"""Inficon SQM-160 multi-film rate/thickness monitor.
Uses a quartz crystal sensor to measure rate and thickness in a thin
film deposition process. Connection to the device is commonly made through
a serial connection (RS232) or optionally via USB or Ethernet.
A command packet always consists of the following:
- 1 Byte: Sync character ('!' appears only at the start of a message).
- 1 Byte: length character obtained from the message length without CRC.
A value of 34 is added so that no '!' can occur.
- Command message with variable length.
- 2 Byte: Cyclic Redundancy Check (CRC) checksum.
A response packet always consists of:
- 1 Byte: Sync character ('!' appears only at the start of a message).
- 1 Byte: length character obtained from the message length without CRC.
A value of 35 is added.
- 1 Byte: Response status character indicating the status of the command.
- Response message with variable length.
- 2 Byte: Cyclic Redundancy Check (CRC) checksum.
:param adapter: pyvisa resource name of the instrument or adapter instance
:param string name: Name of the instrument.
:param string baud_rate: Baud rate used by the serial connection.
:param kwargs: Any valid key-word argument for Instrument
"""
sensor_1 = Instrument.ChannelCreator(SensorChannel, 1)
sensor_2 = Instrument.ChannelCreator(SensorChannel, 2)
sensor_3 = Instrument.ChannelCreator(SensorChannel, 3)
sensor_4 = Instrument.ChannelCreator(SensorChannel, 4)
sensor_5 = Instrument.ChannelCreator(SensorChannel, 5)
sensor_6 = Instrument.ChannelCreator(SensorChannel, 6)

def __init__(self, adapter, name="Inficon SQM-160 thickness monitor",
baud_rate=19200, **kwargs):
super().__init__(adapter,
name,
includeSCPI=False,
write_termination="",
read_termination="",
asrl=dict(baud_rate=baud_rate),
timeout=3000,
**kwargs)

def read(self):
"""Reads a response message from the instrument.
This method also checks for a correct checksum.
:returns: the response packet
:rtype: string
:raises ConnectionError: if a checksum error is detected or a wrong
response status is detected.
"""
header = self.read_bytes(2)
# check valid header
if header[0] != 33: # b"!"
raise ConnectionError(f"invalid header start byte '{header[0]}' received")
length = header[1] - 35
if length <= 0:
raise ConnectionError(f"invalid message length '{header[1]}' -> length {length}")

response_status = self.read_bytes(1)
if response_status == b"C":
raise ConnectionError("invalid command response received")
elif response_status == b"D":
raise ConnectionError("Problem with data in command")
elif response_status != b"A":
raise ConnectionError(f"unknown response status character '{response_status}'")

if length - 1 > 0:
data = self.read_bytes(length - 1)
else:
data = b""
chksum = self.read_bytes(2)
calculated_checksum = calculate_checksum(
header[1].to_bytes(length=1, byteorder='big') + response_status + data)
if chksum == calculated_checksum:
return data.decode()
else:
raise ConnectionError(
f"checksum error in received message '{header + response_status + data}' "
f"with checksum '{calculated_checksum}' but received '{chksum}'")

def write(self, command):
"""Write a command to the device."""
length = chr(len(command) + 34)
message = f"{length}{command}".encode()
self.write_bytes(b"!" + message + calculate_checksum(message))

def check_set_errors(self):
"""Check the errors after setting a property."""
self.read()
return [] # no error happened

firmware_version = Instrument.measurement(
"@", """Get the firmware version.""",
cast=str,
)

number_of_channels = Instrument.measurement(
"J", """Get the number of installed channels""",
cast=int,
)

average_rate = Instrument.measurement(
"M", """Get the current average rate in Angstrom per second""",
cast=float,
)

average_thickness = Instrument.measurement(
"O", """Get the current average thickness in Angstrom""",
cast=float,
)

all_values = Instrument.measurement(
"W", """Get the current rate (Angstrom/s), Thickness (Angstrom), and frequency (Hz)
for each sensor""",
cast=float,
preprocess_reply=lambda msg: msg[5:], # ingore first '00.00'
)

reset_flag = Instrument.measurement(
"Y", """Get the power-up reset flag.
It is True only when read first after a power cycle.""",
cast=int,
values={True: 1, False: 0},
map_values=True,
)

def reset_system_parameters(self):
"""Reset all film and system parameters."""
self.write("Z")
self.read() # read obligatory response message

def reset_thickness_rate(self):
"""Reset the average thickness and rate.
This also sets all active Sensor Rates and Thicknesses to zero
"""
self.write("S")
self.read() # read obligatory response message

def reset_time(self):
"""Reset the time of the monitor to zero.
"""
self.write("T")
self.read() # read obligatory response message
88 changes: 88 additions & 0 deletions tests/instruments/inficon/test_sqm160.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
#
# This file is part of the PyMeasure package.
#
# Copyright (c) 2013-2023 PyMeasure Developers
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
#
import pytest

from pymeasure.test import expected_protocol
from pymeasure.instruments.inficon.sqm160 import calculate_checksum, SQM160


def test_checksum():
"""Verify the calculate_checksum function."""
# test against values documented in the manual
assert calculate_checksum(b'#@') == b'O7'
assert calculate_checksum(b'$C?') == b'g/'


def test_firmware_version():
"""Verify the communication of the firmware version."""
with expected_protocol(
SQM160,
[(b"!#@O7", b"!0AMON Ver 4.13Uw"),],
) as inst:
assert inst.firmware_version == "MON Ver 4.13"


def test_number_of_channels():
"""Verify the communication of the number of channels."""
with expected_protocol(
SQM160,
[(b"!#JO8", b"!%A6v\x86"),],
) as inst:
assert inst.number_of_channels == 6


def test_average_rate():
"""Verify reading of the average rate."""
with expected_protocol(
SQM160,
[(b"!#M\x8e\x8a", b"!*A 0.01 i?"),],
) as inst:
assert inst.average_rate == pytest.approx(0.01)


def test_average_thickness():
"""Verify reading of the average rate."""
with expected_protocol(
SQM160,
[(b"!#O\x8f9", b"!+A 0.000 Jo"),],
) as inst:
assert inst.average_thickness == pytest.approx(0.0)


def test_channel_rate():
"""Verify reading of the rate of a channel."""
with expected_protocol(
SQM160,
[(b"!%L1?\x85{", b"!*A 0.00 [d"),],
) as inst:
assert inst.sensor_1.rate == pytest.approx(0.0)


def test_channel_frequency():
"""Verify reading of the frequency of a channel."""
with expected_protocol(
SQM160,
[(b"!$P1Z\x91", b"!/A5875830.230:X"),],
) as inst:
assert inst.sensor_1.frequency == pytest.approx(5875830.23)

0 comments on commit dcb1cd4

Please sign in to comment.