-
Notifications
You must be signed in to change notification settings - Fork 13
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Added modbus agent #638
base: main
Are you sure you want to change the base?
Added modbus agent #638
Conversation
Modbus agent should work with most Modbus devices simply by supplying the correct configuration file. Specifically, this has been tested on the following devices on site: -VLT cooling loop pump controllers -ELNet power monitors -DSE 8610mkII generator controllers Sample configuration files for each device are included.
for more information, see https://pre-commit.ci
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the PR @DaveBoettger! A few comments and questions below. Biggest thing I'd say is the docs page. Some unit tests would be great, and I'm happy to help get that started, details in the comments.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think we want to package these into the published package on PyPI. For lack of a better place to put them at the moment, can we move them to /examples/configs/modbus/
at the root of the repository?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for being so thorough with the docstrings! In order to easily view them can you create a docs page for this agent? It should live in docs/agents/modbus_agent.rst
. There are many examples in that directory, and also documentation here for creating these pages.
To include the agent in the side bar, the new docs .rst
page must be added to the docs/index.rst
file, alphabetically, in the "Agent Reference" section.
""" | ||
|
||
config_dir = os.environ.get('OCS_CONFIG_DIR') | ||
path = os.path.join(config_dir, 'modbus_configs', dir_name) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would leave out 'modbus_configs'
, let the user decide what directory structure/naming they want. Otherwise this forces $OCS_CONFIG_DIR/modbus_configs/<user defined directory>
.
|
||
def load_configs(dir_name, config_extension='yaml'): | ||
""" | ||
Loads all register configuration files form the specified directory (path). |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
"file form" -> "files from"
|
||
|
||
def twos(val, num_bytes): | ||
"""Take an unsigned integer representation of a two's compilment integer and return the correctly signed integer |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please wrap lines to 80 characters. Lots of editors support automating this in some fashion, or have extensions to do so.
help="Starting action for the agent.") | ||
pgroup.add_argument("--configdir", type=str, help="Path to directory containing .yaml config files.") | ||
pgroup.add_argument("--sample-interval", type=float, default=10., help="Time between samples in seconds.") | ||
pgroup.add_argument("--unit_id", type=float, default=1, help="Modbus unit id for module.") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Convention here is to use hyphens instead of underscores, please make this --unit-id
, rather than --unit_id
.
{"fields": | ||
{'Oil_pressure': {'value': 100.0, 'units': 'Kpa'}, | ||
'Coolant_temperature': {'value': 20.0, 'units': 'Degrees C'}, | ||
'Oil_temperature': {'value': 20.0, 'units': 'Degrees C'}, | ||
'Fuel_level': {'value': 100.0, 'units': '%'}, | ||
'Charge_alternator_voltage': {'value': 10.0, 'units': 'V'}, | ||
'Engine_Battery_voltage': {'value': 10.0, 'units': 'V'}, | ||
'Engine_speed': {'value': 4000, 'units': 'RPM'}, | ||
'Generator_frequency': {'value': 1.0, 'units': 'Hz'}, | ||
... | ||
'connection': {'last_attempt': 1680812613.939653, 'connected': True}}, | ||
"address": 'localhost', | ||
"timestamp":1601925677.6914878} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is great to see! However I noticed that the documented fields here seem to differ from the register names in Generator_Engine_Data.yaml
, i.e. 'Engine_oil_pressure' vs 'Oil_pressure'. Is this just a case of one of these two locations being out of date?
This documentation is primarily for the OCS control program writer who needs to reference these keys and might not have access to a running instance of the agent to reverse engineer which keys are available. So these sorts of differences matter here, but I realize it's tedious to keep multiple locations up to date.
There's also a sort of unique issue here in that this agent could be monitoring a different device with different registers, and thus different "fields". I'd like to capture that info here too.
My suggestion would be to include a general structure for the session.data
, and describe how it's populated, while pointing to the example configuration files for the details of specific field names and possible value types, etc. Still include things that are always true in the general structure, like the timestamp. Sound good?
if self.error_out_of_range: | ||
try: | ||
if val < rconfig['min_val']: | ||
val = self.error_val | ||
except KeyError: | ||
pass | ||
|
||
try: | ||
if val > rconfig['max_val']: | ||
val = self.error_val | ||
except KeyError: | ||
pass |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Two questions here:
- How does
self.error_val
get set? It seems right now to always beNone
. This would run into issues on line 247 with thefloat()
cast, but maybe I'm missing something. - Would it make sense to distinguish between values below 'min_val' and values above 'max_val'?
try: | ||
self.read_start = config['read_start'] | ||
except KeyError: | ||
self.read_start = config['page'] * 256 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Where does config['page']
come in? I don't see a 'page' key in any of the example YAML files, or any additions of the key elsewhere in the code.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This would go above and beyond, but it would be useful to see some examples in the form of some unit tests for the code up to line 156, all the bit register code that doesn't involve actually communicating with a device. There are some docs for how to run the tests here.
The tests would live under socs/tests/agents/. There aren't a lot of examples in there, but you could check out the supersync tests.
Happy to talk more about this too.
Modbus agent should work with most Modbus devices simply by supplying the correct configuration file.
Specifically, this has been tested on the following devices on site: -VLT cooling loop pump controllers
-ELNet power monitors
-DSE 8610mkII generator controllers
Sample configuration files for each device are included.
Description
Motivation and Context
How Has This Been Tested?
Types of changes
Checklist: