Skip to content

Commit

Permalink
Siren updates (#20)
Browse files Browse the repository at this point in the history
* add PROVIDER debug messages

* siren: make sure hil_device scaling always returns value in range

* siren: add labjack-ljm dependency to pybennu

* siren: implement configurable deadband

* siren: add labjack T4 device

* Update pybennu deb
- use new postinst script
- add labjack-ljm dependency
- add libboost dependency for helics build
  • Loading branch information
cmulk authored Mar 11, 2024
1 parent 27ae0b4 commit 4bc5d99
Show file tree
Hide file tree
Showing 7 changed files with 238 additions and 9 deletions.
1 change: 1 addition & 0 deletions src/pybennu/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,7 @@ deb: packagetools
-d python3-pip \
-d python3-setuptools \
-d wget \
-d libboost-python-dev \
-p '$(subst __colon__,:,$(DIST_DIR)/$(PACKAGE_FILENAME))' \
--name '$(PACKAGE_NAME)' \
$(call iter,$(PACKAGE_VENDOR),--vendor) \
Expand Down
4 changes: 2 additions & 2 deletions src/pybennu/postinst
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ fi
# TODO: we should statically include our own version of install.sh
# instead of downloading it every build and running sed on it
printf "\n\tINSTALLING ZMQ WITH DRAFT SUPPORT...\n\n"
wget --no-check-certificate https://raw.githubusercontent.com/zeromq/pyzmq/main/examples/draft/install.sh
wget --no-check-certificate -O install.sh https://raw.githubusercontent.com/zeromq/pyzmq/main/examples/draft/install.sh
sed -i -e "s/wget/wget --no-check-certificate/g" install.sh
sed -i -e "s/pip install/pip3 install --trusted-host pypi.org --trusted-host files.pythonhosted.org -I/g" install.sh
chmod +x install.sh
Expand All @@ -29,6 +29,6 @@ sed -i "s/helics-apps/helics-apps~=2.7.1/" /tmp/pyhelics/helics-2.7.1/setup.py
pip3 install --trusted-host pypi.org --trusted-host files.pythonhosted.org /tmp/pyhelics/helics-2.7.1/

printf "\n\tINSTALLING PYBENNU PIP DEPENDENCIES...\n\n"
pip3 install --trusted-host pypi.org --trusted-host files.pythonhosted.org bitarray 'elasticsearch>=5.3.0' 'helics~=2.7.1' 'matplotlib>=1.5.3' 'networkx>=1.11' 'numpy>=1.11.2' 'opendssdirect.py~=0.6.1' 'py-expression-eval~=0.3.14' 'PYPOWER>=5.0.1' 'pyserial>=3.4' 'pyyaml>=3.12' 'requests>=2.20' 'scipy>=0.18.1' sysv_ipc
pip3 install --trusted-host pypi.org --trusted-host files.pythonhosted.org bitarray 'elasticsearch>=5.3.0' 'helics~=2.7.1' 'matplotlib>=1.5.3' 'networkx>=1.11' 'numpy>=1.11.2' 'opendssdirect.py~=0.6.1' 'py-expression-eval~=0.3.14' 'PYPOWER>=5.0.1' 'pyserial>=3.4' 'pyyaml>=3.12' 'requests>=2.20' 'scipy>=0.18.1' sysv_ipc labjack-ljm~=1.23.0

printf "\nDONE!!\n\n"
12 changes: 8 additions & 4 deletions src/pybennu/pybennu/siren/hil_device/configs/labjack_t7.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,28 +4,32 @@
"min_data":0,
"max_data":60,
"min_volt":1,
"max_volt":5
"max_volt":5,
"deadband":0.5
},
"analog_input_2":{
"pin":"AIN1",
"min_data":0,
"max_data":60,
"min_volt":1,
"max_volt":5
"max_volt":5,
"deadband":0.5
},
"analog_input_3":{
"pin":"AIN2",
"min_data":0,
"max_data":60,
"min_volt":1,
"max_volt":5
"max_volt":5,
"deadband":0.5
},
"analog_input_4":{
"pin":"AIN3",
"min_data":0,
"max_data":60,
"min_volt":1,
"max_volt":5
"max_volt":5,
"deadband":0.5
},
"analog_output_1":{
"pin":"DAC0",
Expand Down
10 changes: 9 additions & 1 deletion src/pybennu/pybennu/siren/hil_device/hil_device.py
Original file line number Diff line number Diff line change
Expand Up @@ -107,11 +107,19 @@ def voltage_to_data(self, io_point, voltage):

def linear_scale(self, m, r_min, r_max, t_min, t_max):
'''Linearly scales m in [r_min, r_max] to a value in [t_min, t_max].
Ensures value is always within [t_min, t_max]
For an explanation, see:
https://stats.stackexchange.com/questions/281162/scale-a-number-between-a-range
'''
return min((m - r_min) / (r_max - r_min) * (t_max - t_min) + t_min, t_max)
scaled = (m - r_min) / (r_max - r_min) * (t_max - t_min) + t_min

if scaled <= t_min:
return t_min
if scaled >= t_max:
return t_max

return scaled


class MsgType(enum.Enum):
Expand Down
197 changes: 197 additions & 0 deletions src/pybennu/pybennu/siren/hil_device/labjack_t4.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
#!/usr/bin/python3

import json
import math
import sys

from labjack import ljm
from pybennu.siren.hil_device.hil_device import HIL_Device
from pybennu.siren.hil_device.hil_device import MsgType


class LabJackT4(HIL_Device):
def __init__(self, device_config, device_name, level_name, device_id):
self.device_name = device_name
self.device_id = device_id
self.flip_flop_state = {}

with open(device_config) as f:
self.label_mapping = json.load(f)
self.validate_label_mapping()

self.setup_logger(level_name)

# Open the first LabJack T4 device found.
try:
id = 'Any'
if self.device_id is not None:
id = self.device_id
print("Opening labjack connection")
self.handle = ljm.openS("T4", "ANY", id)
except ljm.ljm.LJMError as e:
self.logger.error(e)
sys.exit()

# Streaming configurations.
self.freq = 0
self.streaming = False
try:
ljm.eStreamStop(self.handle)
except Exception as e:
pass

self.logger.info(ljm.getHandleInfo(self.handle))

@property
def config_to_analog(self, *args):
# LabJack T7 uses the LJM library so this function is not needed.
raise AttributeError(
"'LabJackT7' object has no attribute 'config_to_analog'")

@property
def config_to_digital(self, *args):
# LabJack T7 uses the LJM library so this function is not needed.
raise AttributeError(
"'LabJackT7' object has no attribute 'config_to_digital'")

def read_analog(self, label):
try:
pin = self.label_mapping[label]["pin"]
voltage = ljm.eReadName(self.handle, pin)
return self.voltage_to_data(label, voltage)
except Exception as e:
self.logger.error(e)

def write_analog(self, label, data):
try:
pin = self.label_mapping[label]["pin"]
voltage = self.data_to_voltage(label, data)
ljm.eWriteName(self.handle, pin, voltage)
except Exception as e:
self.logger.error(e)

def read_digital(self, label):
try:
pin = self.label_mapping[label]
# Check Pin type. Analog Inputs can be used like Digital Inputs
if pin.startswith("AIN"):
return 1 if (ljm.eReadName(self.handle, pin) >= 2.5) else 0
else:
return ljm.eReadName(self.handle, pin)
except Exception as e:
self.logger.error(e)

def convert_volts_to_digital(self, volts):
digital = 0
if volts > 3.5:
digital = 1
return digital

def read_flip_flop(self, label):
try:
pins = self.label_mapping[label]
if isinstance(pins, list):
close_pin = 0
trip_pin = 0
for idx in range(0, len(pins), 2):
close_val = ljm.eReadName(self.handle, pins[idx])
close_digital = self.convert_volts_to_digital(close_val)
close_pin = close_pin | close_digital
trip_pin = trip_pin | self.convert_volts_to_digital(ljm.eReadName(self.handle, pins[idx + 1]))
if pins[0] not in self.flip_flop_state:
self.flip_flop_state[pins[0]] = 0
flip_flop_value = self.flippity_floppity(close_pin, trip_pin, self.flip_flop_state[pins[0]])
self.flip_flop_state[pins[0]] = flip_flop_value
return flip_flop_value
else:
self.logger.error('Flip flop configuration types should be a list of pairs, i.e. [close1, trip1, close2, trip2')
except Exception as e:
self.logger.error(e)
return 0

def flippity_floppity(self, close, trip, ff_state):
x = 0
if close == 1 or ff_state == 1:
x = 1
if x == 1 and trip == 1:
x = 0

return x

def write_digital(self, label, status):
try:
pin = self.label_mapping[label]
voltage = 1 if status else 0
# Currently have not used TDAC tag, but should work fine
if pin.startswith("TDAC"):
voltage = 10 if status else 0
ljm.eWriteName(self.handle, pin, voltage)
except Exception as e:
self.logger.error(e)

def write_waveform(self, label, freq):
try:
# No change in frequency.
if self.freq == freq:
return

# Stop the stream, then restart it with the new frequency.
if self.streaming:
ljm.eStreamStop(self.handle)

scale = 1
if 'scale' in self.label_mapping[label]:
scale = self.label_mapping[label]['scale']

# How many points from a single wave cycle to calculate.
shape = self.label_mapping[label]['shape']
if shape == 'sine':
resolution = 100
elif shape == 'square':
resolution = 2
else:
raise NotImplementedError()

# Raise centerline since LJ only does [0, 5] volts.
centerline = 2.5

# The amount of times the output pin will change state per second.
scan_rate = resolution * (freq * scale)

# Calculate the samples of the wave.
if shape == 'sine':
amplitude = 2
samples = [((math.sin(2 * math.pi * x / resolution) * amplitude) + centerline) for x in range(resolution)]
elif shape == 'square':
samples = [(x * 4.5) for x in range(resolution)]
else:
raise NotImplementedError

# Setup stream.
pin = self.label_mapping[label]['pin']
ljm.eWriteName(self.handle, 'STREAM_OUT0_TARGET', ljm.nameToAddress(pin)[0])
ljm.eWriteName(self.handle, 'STREAM_OUT0_BUFFER_SIZE', 512) # 256 values
ljm.eWriteName(self.handle, 'STREAM_OUT0_ENABLE', 1)
ljm.eWriteNameArray(self.handle, 'STREAM_OUT0_BUFFER_F32', resolution, samples)
ljm.eWriteName(self.handle, 'STREAM_OUT0_SET_LOOP', 1)
ljm.eReadName(self.handle, 'STREAM_OUT0_BUFFER_STATUS')
ljm.eWriteName(self.handle, 'STREAM_OUT0_LOOP_SIZE', resolution)

# Start stream.
ljm.eStreamStart(self.handle, 1, 1, [4800], scan_rate)

# Update member variables.
self.freq = freq
self.streaming = True
except Exception as e:
self.logger.error(e)

def cleanup(self):
self.logger.info('Closing connection.')
ljm.close(self.handle)

def __del__(self):
pass


HIL_Device.register(LabJackT4)
22 changes: 20 additions & 2 deletions src/pybennu/pybennu/siren/siren.py
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,16 @@ def create_device(self):
self.device_config.id)
break

def _is_outside_deadband(self, old_value, new_value, deadband):
"""Check if a new value is outside of a deadband threshold from and old value
Returns true if the difference between new_value and old_value are
greater than the deadband, or if the old_value is None.
"""
if old_value is None:
return True

return abs(new_value - old_value) > deadband

def _subscription_handler(self, message):
"""Receive Subscription message
This method gets called by the Subscriber base class in its run()
Expand Down Expand Up @@ -261,29 +271,37 @@ def _to_provider(self):
label = self.device_config.to_endpoint[_filter]
if 'analog' in label:
data = self.device.read_analog(label)
# set deadband if given in label mapping
deadband = self.device.label_mapping[label].get("deadband", 0.0)
if data is not None:
if _filter in self.to_endpoint_states and self.to_endpoint_states[_filter] != data:
if (
_filter in self.to_endpoint_states
and self._is_outside_deadband(self.to_endpoint_states[_filter], data, deadband)
):
self.to_endpoint_states[_filter] = data
self.device.logger.log(LEVELS['debug'], f'TO PROVIDER {label}:{data}')
with contextlib.redirect_stdout(None):
self.write_analog_point(_filter, data)
elif 'digital' in label:
data = self.device.read_digital(label)
if data is not None:
if _filter in self.to_endpoint_states and self.to_endpoint_states[_filter] != data:
self.to_endpoint_states[_filter] = data
self.device.logger.log(LEVELS['debug'], f'TO PROVIDER {label}:{data}')
with contextlib.redirect_stdout(None):
self.write_digital_point(_filter, data)
elif 'flip_flop' in label:
data = self.device.read_flip_flop(label)
if data is not None:
if _filter in self.to_endpoint_states and self.to_endpoint_states[_filter] != data:
self.to_endpoint_states[_filter] = data
self.device.logger.log(LEVELS['debug'], f'TO PROVIDER {label}:{data}')
with contextlib.redirect_stdout(None):
self.write_digital_point(_filter, data)
else:
raise NotImplementedError

self.device.logger.log(LEVELS['debug'], f'TO PROVIDER {label}:{data}')
sleep(self.device_config.polling_time)


Expand Down
1 change: 1 addition & 0 deletions src/pybennu/setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@ def run(self):
'PyYAML>=3.12', # pyyaml>=3.12 ==5.4.1
'requests>=2.20', # ~=2.26.0
'scipy>=0.18.1', # ~=1.7.1
'labjack-ljm~=1.23.0',
]

if 'linux' in sys.platform:
Expand Down

0 comments on commit 4bc5d99

Please sign in to comment.