Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
59 commits
Select commit Hold shift + click to select a range
8734523
Initial version of ModbusTCP service and entities
Oct 4, 2024
4792a55
Fixing python errors in ethernet modbus file
Robbie-1208 Oct 10, 2024
4322174
better modbus log information
Oct 17, 2024
b83642d
added a FormatEntity that supports asteval, in addition define an com…
renereimann May 27, 2025
5611674
merge develop into feature/asteval
renereimann Jul 17, 2025
d8b8a41
extending documentation of functions and classes, reusing code that i…
renereimann Jul 20, 2025
8d1f7b2
Fixing the dripline version in the docker compose file
renereimann Jul 28, 2025
6ecb50a
fixing typo in name of super class
renereimann Jul 28, 2025
e2e23c1
we have to format the raw value, since its reported as a dict, we hav…
renereimann Jul 28, 2025
656d2c5
separating cmd and asteval endpoints into different files, changing F…
renereimann Jul 29, 2025
1bf48ce
Defining Pfeiffer endpoints that use the pfeiffer telegram protocol. …
renereimann Jul 29, 2025
71c4d77
example config file to show how Pfeiffer Endpoints are used
renereimann Jul 29, 2025
fcecc09
improve doc strings
renereimann Jul 29, 2025
9affd8c
using develop-dev as base container
renereimann Jul 30, 2025
e18c304
adding new extension service and entity to talk with Huber devices
renereimann Sep 2, 2025
cccff87
adding Huber service and entity
renereimann Sep 2, 2025
23ec55a
adding the extraction of the reply. This is partly entity dependent a…
renereimann Sep 2, 2025
0b7b74d
Expanded to include other code types
Robbie-1208 Sep 18, 2025
bb0a5bb
Fixed Dockerfile slack_sdk error
Robbie-1208 Sep 30, 2025
e3f94a5
Merge tag 'v2.1.0' into develop
nsoblath Oct 1, 2025
428268f
update to recent dripline version
renereimann Oct 6, 2025
5e90e9f
separate conversion function to float in its own function
renereimann Oct 6, 2025
ced66d1
update to recent dripline version
renereimann Oct 6, 2025
de6ab64
replacing depricated BinaryPayloadDecoder by convert_from_register, b…
renereimann Dec 19, 2025
3d168ee
return register(s) not the modbus object itself. Differ between array…
renereimann Dec 19, 2025
1ec9dc8
do a second try after reconnect if query fails
renereimann Dec 19, 2025
6926617
implement type conversion for any datatype provided by pymodbus. Also…
renereimann Dec 19, 2025
cae9183
add doc string for parameters
renereimann Dec 19, 2025
b677db6
adding only get and set entities
renereimann Dec 19, 2025
7a578b1
changing __init__
renereimann Dec 19, 2025
7213b23
resolve merge conflicts with develop
renereimann Dec 19, 2025
d98f9d0
updating dripline-python version to v5.1.2 to get ride of quill error
renereimann Jan 22, 2026
7b699e8
Merge branch 'origin/develop' into feature/asteval
wcpettus Jan 22, 2026
ae59856
Cleaning imports
wcpettus Jan 22, 2026
36045e8
Merge pull request #221 from project8/feature/asteval
nsoblath Jan 22, 2026
1848063
updating dripline version
renereimann Jan 23, 2026
c7e6af3
Merge branch 'develop' of github.com:project8/dragonfly into develop
renereimann Jan 23, 2026
252664a
Merge <develop> to <feature/huber_chiller>
renereimann Jan 23, 2026
984684b
Merge branch 'develop' into feature/modbus
renereimann Jan 23, 2026
6b1d0ab
updating dripline-python version
renereimann Jan 23, 2026
3d6e5de
Merge <develop> to <feature/pfeiffer>
renereimann Jan 23, 2026
58cb1a0
remove unused import and factor out doublicate read and write code fo…
renereimann Jan 23, 2026
afe73bc
making the wordorder again a service property and not a per endpoint …
renereimann Jan 23, 2026
3c76b77
return unformated value if no datatype is matching. Comment out unuse…
renereimann Jan 23, 2026
1790251
Merge pull request #224 from project8/feature/pfeiffer
nsoblath Jan 23, 2026
58d8bf0
merge <develop> to <feature/huber_chiller>
renereimann Jan 26, 2026
a2f0a20
Merge branch 'develop' into feature/huber_chiller
renereimann Jan 26, 2026
a39beff
merge <develop> to <feature/modbus>
renereimann Jan 26, 2026
b8cfea5
installing python packages in one go
renereimann Feb 5, 2026
d271e02
Merge pull request #227 from project8/feature/modbus
renereimann Feb 5, 2026
4c3cc79
Merge tag 'v2.1.1' into develop
nsoblath Feb 5, 2026
914aa63
update documentation comments, nicer fromating and better variable name
renereimann Feb 5, 2026
2564939
merging <develop> into <feature/huber_chiller>
renereimann Feb 5, 2026
8fc2943
Merge branch 'develop' into feature/huber_chiller
renereimann Feb 5, 2026
d302928
Changing entity class name to HuberGetEntity since it just supports g…
renereimann Feb 18, 2026
f9c10e4
Merge pull request #229 from project8/feature/huber_chiller
renereimann Mar 5, 2026
2b3744f
Bumped version to 2.2.0
nsoblath-pnnl Jun 4, 2026
0d67a75
Added stub info in changelog
nsoblath-pnnl Jun 4, 2026
c173580
adding description of new classes
renereimann Jun 5, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
ARG img_user=ghcr.io/driplineorg
ARG img_repo=dripline-python
#ARG img_tag=develop-dev
ARG img_tag=v5.1.5

FROM ${img_user}/${img_repo}:${img_tag}

COPY . /usr/local/src_dragonfly

WORKDIR /usr/local/src_dragonfly
RUN pip install docker
RUN pip install docker pymodbus
RUN pip install .

WORKDIR /
16 changes: 16 additions & 0 deletions changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,22 @@ Types of changes: Added, Changed, Deprecated, Removed, Fixed, Security

## [Unreleased]

## [2.2.0] -- ...

### Added

- AstevalFormatEntity FormatEntity which supports asteval evaluation at formating
- CmdEntity Class providing a CMD entity, this was implemented in dripline-cpp but not in dripline-python, can be used to e.g. call a calibrate CMD
- EthernetHuberService Service implementing the communication protocol used by Huber company
- HuberGetEntity A get entity to implement communication protocol used by Huber company
- EthernetModbusService Service to implement modbus communication
- ModbusEntity Entiy for modbus communication
- ModbusGetEntity GetEntity for modbus communication
- ModbusSetEntity SetEntity for modbus communication
- PfeifferEntity Entity supporting formatting used by devices from Pfeiffer company
- PfeifferGetEntity GetEntity supporting formatting used by devices from Pfeiffer company
- PfeifferSetEntity SetEnitty supporting formatting used by devices from Pfeiffer company

## [2.1.1] -- 2026-02-05

### Changed
Expand Down
5 changes: 5 additions & 0 deletions dripline/extensions/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,10 @@
# Modules in this directory

from .add_auth_spec import *
from .cmd_endpoint import *
from .asteval_endpoint import *
from .thermo_fisher_endpoint import *
from .ethernet_thermo_fisher_service import *
from .ethernet_huber_service import *
from .ethernet_modbus_service import *
from .pfeiffer_endpoint import *
36 changes: 36 additions & 0 deletions dripline/extensions/asteval_endpoint.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
from dripline.core import calibrate
from dripline.implementations import FormatEntity

import logging
logger = logging.getLogger(__name__)

__all__ = []



__all__.append('AstevalFormatEntity')
class AstevalFormatEntity(FormatEntity):
'''
Utility Entity allowing arbitrary set and query syntax and formatting for more complicated usage cases
No assumption about SCPI communication syntax.
'''

def __init__(self,
asteval_format_response_string="def f(response): return response",
**kwargs):
'''
Args:
asteval_format_response_string (str): function definition to format response. Default: "def f(response): return response"
'''
FormatEntity.__init__(self, **kwargs)
self.asteval_format_response_string = asteval_format_response_string # has to contain a definition "def f(response): ... return value"
logger.debug(f'asteval_format_response_string: {repr(self.asteval_format_response_string)}')
self.evaluator(asteval_format_response_string)

@calibrate()
def on_get(self):
result = FormatEntity.on_get(self)
raw = result["value_raw"]
processed_result = self.evaluator(f"f('{raw}')")
logger.debug(f"processed_result: {repr(processed_result)}")
return processed_result
28 changes: 28 additions & 0 deletions dripline/extensions/cmd_endpoint.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
from dripline.core import Entity

import logging
logger = logging.getLogger(__name__)

__all__ = []

__all__.append('CmdEntity')
class CmdEntity(Entity):
'''
SCPI Entity to execute a command, instead of a get or set.
The command is given via "cmd_str" and takes no additional arguments.
This can e.g. be used to auto-calibrate, set to zero or similar device commands.
'''
def __init__(self, cmd_str=None, **kwargs):
'''
Args:
cmd_str (str): sent verbatim in the event of cmd().
'''
Entity.__init__(self, **kwargs)
logger.debug(f"I get cmd_str: {cmd_str}, which is of type {type(cmd_str)}.")
if cmd_str is None:
raise ValueError("cmd_str is required for CmdEntity")
self.cmd_str = cmd_str

def cmd(self):
logger.debug("in cmd function")
return self.service.send_to_device([self.cmd_str])
157 changes: 157 additions & 0 deletions dripline/extensions/ethernet_huber_service.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
import time

from dripline.core import ThrowReply, Entity, calibrate
from dripline.implementations import EthernetSCPIService

import logging
logger = logging.getLogger(__name__)

__all__ = []

__all__.append('EthernetHuberService')

def int_to_hexstr(value):
return hex(value)[2:].zfill(2)

def hexstr_to_bytes(value):
return bytes.fromhex(value)

def bytes_to_hexstr(value):
return value.hex()

def hexstr_to_int(value):
return int(value, 16)

def bytes_to_ints(value):
return [byte for byte in value]

class EthernetHuberService(EthernetSCPIService):
'''
A fairly specific subclass of Service for connecting to ethernet-capable huber devices.
In particular, devices must support a half-duplex serial communication with header information, variable length data-payload and a checksum.
'''
def __init__(self, **kwargs):
'''
Args:
socket_timeout (int): number of seconds to wait for a reply from the device before timeout.
socket_info (tuple or string): either socket.socket.connect argument tuple, or string that
parses into one.
'''
EthernetSCPIService.__init__(self, **kwargs)

def calculate_checksum(self, input_string):
"""
Calculates the 1-byte checksum of the input string.
Returns the checksum as a 2-character uppercase hex string.

:param input_string: The string to compute the checksum for
:return: Checksum as a hex string (e.g., 'C6')
"""
total = sum(ord(char) for char in input_string)
checksum = total % 256
return f"{checksum:02X}"

def check_checksum(self, cmd):
# calculate checksum of response except checksum and check if match checksum
return self.calculate_checksum( cmd[:-2] ) == cmd[-2:]


def _assemble_cmd(self, cmd_in):
cmd_raw = cmd_in.split(" ")[0]
data = " ".join(cmd_in.split(" ")[1:])
cmd = "[M01" + cmd_raw
length = len(cmd) + len(data) + 2
cmd = cmd + f"{length:02X}" + data
cs = self.calculate_checksum(cmd)
cmd = cmd + cs + self.command_terminator
return cmd

def _extract_reply(self, response, cmd):
if not self.calculate_checksum(response[:-2]) == response[-2:]:
logger.warning("Checksum not matching")
if not response[:4] == "[S01":
logger.warning("Header not matching")
if not response[4] == cmd:
logger.warning("cmd is not matching")
if not int(response[5:7], 16) == len(response)-2:
logger.warning("length not matching")
return response[7:-2]

def _send_commands(self, commands):
'''
Take a list of commands, send to instrument and receive responses, do any necessary formatting.

commands (list||None): list of command(s) to send to the instrument following (re)connection to the instrument, still must return a reply!
: if impossible, set as None to skip
'''
all_data=[]

for cmd in commands:
command = self._assemble_cmd(cmd)
logger.debug(f"sending: {command.encode()}")
self.socket.send(command.encode())
if command == self.command_terminator:
blanck_command = True
else:
blanck_command = False

data = self._listen(blanck_command)

if self.reply_echo_cmd:
if data.startswith(command):
data = data[len(command):]
elif not blank_command:
raise ThrowReply('device_error_connection', f'Bad ethernet query return: {data}')
logger.info(f"sync: {repr(command)} -> {repr(data)}")
data = self._extract_reply(data, cmd.split(" ")[0])
all_data.append(data)
return all_data


__all__.append("HuberGetEntity")
class HuberGetEntity(Entity):
'''
A endpoint of a Huber device that returns the request result
'''

def __init__(self,
get_str=None,
offset=0,
nbytes=-1,
numeric=False,
**kwargs):
'''
Args:
get_str: hexstring of the command, e.g. 20
'''
if get_str is None:
raise ValueError('<get_str is required to __init__ HuberGetEntity instance')
else:
self.get_str = get_str
self.offset = offset
self.nbytes = nbytes
self.numeric = numeric
Entity.__init__(self, **kwargs)

def convert_to_float(self, hex_str):
val = int(hex_str, 16)
if val > int("7FFF", 16):
val = val - int("FFFF", 16) - 1
return val/100.

@calibrate()
def on_get(self):
# setup cmd here
to_send = [self.get_str]
logger.debug(f'Send cmd in hexstr: {to_send[0]}')
result = self.service.send_to_device(to_send)
logger.debug(f'raw result is: {result}')
result = result[self.offset: self.offset+self.nbytes]
if self.numeric:
logger.debug("is numeric")
result = self.convert_to_float(result)
logger.debug(f'extracted result is: {result}')
return result

def on_set(self, value):
raise ThrowReply('message_error_invalid_method', f"endpoint '{self.name}' does not support set")
Loading
Loading