Writing a Plugin
This page walks through adding dashboard data for a new instrument: a parser that reads its files, and a plugin that tells OpenVDM which files to give it. Read Plugin Overview first.
The quickest start is to copy the sample plugin and parser closest to your data. The
.dist templates in server/plugins/ and server/plugins/parsers/ are complete,
working examples.
1. Write a Parser
Most instruments log text lines with a timestamp, and OpenVDMCSVParser does most of
the work for those. This parser, based on paro_parser.py.dist, reads lines like
2026-09-28T12:00:00.000Z,$PARO,1523.402 and draws depth against time:
"""Parser for Paro Scientific depth sensor data.
Parses comma-separated log files containing Paro depth measurements and returns
the JSON-formatted plugin data used by OpenVDM's Data Dashboard.
"""
import logging
import sys
from os.path import dirname, realpath
import pandas as pd
sys.path.append(dirname(dirname(dirname(dirname(realpath(__file__))))))
from server.lib.openvdm_plugin import OpenVDMCSVParser
FIELDS = ['hdr', 'depth']
PROC_COLS = ['date_time', 'depth']
ROUNDING = {'depth': 3}
class ParoParser(OpenVDMCSVParser):
"""Parser for Paro Scientific depth sensor log files."""
def __init__(self, start_dt=None, stop_dt=None, time_format=None,
skip_header=False, use_openvdm_api=False):
super().__init__(None, PROC_COLS, start_dt=start_dt, stop_dt=stop_dt,
time_format=time_format, skip_header=skip_header,
use_openvdm_api=use_openvdm_api)
def parse(self, filepath):
"""Parse the Paro depth sensor file and return plugin data dict."""
raw_data = {col: [] for col in self.proc_cols}
errors = []
for lineno, timestamp_str, _, fields in self.read_lines_with_timestamps(filepath):
try:
raw_data['depth'].append(float(dict(zip(FIELDS, fields))['depth']))
raw_data['date_time'].append(timestamp_str)
except (KeyError, ValueError):
errors.append(lineno)
df = pd.DataFrame(raw_data)
df['date_time'] = pd.to_datetime(df['date_time'], errors='coerce')
df = self.crop_data(df.dropna(subset=['date_time']))
if df.empty:
logging.warning("No valid data in file: %s", filepath)
return None
# Stats and quality tests, shown on the Data Quality tab
self.add_row_validity_stat([len(df), len(errors)])
self.add_time_bounds_stat([df['date_time'].min().to_pydatetime(),
df['date_time'].max().to_pydatetime()])
self.add_bounds_stat([df['depth'].min(), df['depth'].max()], 'Depth Bounds', 'm')
if len(errors) > 0.25 * (len(df) + len(errors)):
self.add_quality_test_failed('Rows')
else:
self.add_quality_test_passed('Rows')
# One chart series: [ms since epoch, value] pairs
df = self.round_data(self.resample_data(df.set_index('date_time')), ROUNDING)
df = df.set_index('date_time')
self.add_visualization_data({
'label': 'Depth',
'unit': 'm',
'data': [[int(pd.Timestamp(ts).timestamp() * 1000), row['depth']]
for ts, row in df.iterrows() if pd.notna(row['depth'])],
})
self.send_error_msg(errors, filepath)
self.plugin_data = self._sanitize_for_json(self.plugin_data)
return self.plugin_data
if __name__ == "__main__":
ParoParser.run_cli()
Save it as server/plugins/parsers/paro_parser.py. The helpers it uses:
| Method | Does |
|---|---|
read_lines_with_timestamps(filepath) |
Yields each line’s number, timestamp, remainder and comma-separated fields; skips lines without a timestamp |
crop_data(df) |
Keeps the rows between start_dt and stop_dt, when given (e.g. a lowering’s start and end) |
resample_data(df) |
Averages to one row per minute, so long files stay small enough to chart |
round_data(df, precision) |
Rounds the columns given in precision |
add_visualization_data(), add_*_stat(), add_quality_test_*() |
Add to the parser’s output (see Return Types) |
send_error_msg(errors, filepath) |
Logs the lines that couldn’t be parsed |
run_cli() |
Lets the parser be run from the command line for testing |
To fail a whole file (e.g. a required calibration file is missing), raise an exception. The plugin logs it, and the file gets no dashboard data.
For map data, add GeoJSON FeatureCollections instead of series: a LineString for a
track, or Points for positions, with ISO 8601 times (format_iso8601()) in their
properties. gga_parser.py.dist and the CTD and XBT position parsers are examples.
2. Test the Parser
Run it on a sample file from the OpenVDM root with the venv’s Python:
cd /opt/openvdm
./venv/bin/python server/plugins/parsers/paro_parser.py --help
./venv/bin/python server/plugins/parsers/paro_parser.py /path/to/sample.txt
It prints the plugin data as JSON: check the series, stats and quality tests.
3. Write the Plugin
The plugin is named after the collection system transfer: for a transfer named ROV,
it’s server/plugins/rov_plugin.py. Copy openrvdas_plugin.py.dist, which runs every
parser whose pattern matches, and change its filters and parsers:
from server.plugins.parsers.paro_parser import ParoParser
fileTypeFilters = [
{"data_type": "paro", "regex": "*/paro/*.txt", "parser": "Paro", "parser_options": {}},
]
class ROVPlugin(OpenVDMPlugin):
PARSER_MAP = {
"Paro": ParoParser,
}
...
Each filter has:
| Key | Description |
|---|---|
data_type |
The name the dashboard uses for this data, in datadashboard.yaml. Lower case, no spaces |
regex |
A glob pattern matched against the file’s full path |
parser |
The parser’s key in PARSER_MAP |
parser_options |
Options passed to the parser’s constructor, e.g. {"time_format": "%Y-%m-%dT%H:%M:%S.%fZ"} |
Several filters can match the same file, each producing its own data type. The module
must define process_file(filepath), and can define get_source_files(filepath) (see
Module-Level Functions).
Test the plugin the same way as the parser:
./venv/bin/python server/plugins/rov_plugin.py /path/to/cruise/ROV/paro/sample.txt
4. Show the Data
Add a panel for the new data type to a tab in
datadashboard.yaml. For a chart panel, the panel’s
id is the data type:
- plotType: chart
id: paro
heading: ROV Depth
dataArray:
- dataType: paro
visType: json-reversedY
5. Run It
Restart the data dashboard worker so it loads the new plugin, then rebuild the dashboard to process the files already transferred:
sudo supervisorctl restart openvdm:data_dashboard
Then run Rebuild Data Dashboard under Maintenance Tasks on the Configuration
page. New files are processed after each transfer from then on. Problems are logged in
/var/log/openvdm/data_dashboard.log.
Libraries
If a parser needs a Python package that isn’t in requirements.txt, install it into
OpenVDM’s venv (/opt/openvdm/venv/bin/pip install <package>). The installer
rebuilds the venv when it changes Python versions, so note what you’ve added and
install it again after an upgrade.