Skip to content
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

feat(inputs.mavlink): Add plugin #16221

Open
wants to merge 43 commits into
base: master
Choose a base branch
from

Conversation

chrisdalke
Copy link

@chrisdalke chrisdalke commented Nov 22, 2024

Summary

Add a Mavlink input plugin.

The mavlink plugin connects to a MavLink-compatible flight controller such as as ArduPilot or PX4. and translates all incoming messages into metrics.

The purpose of this plugin is to allow Telegraf to be used to ingest live flight metrics from unmanned systems (drones, planes, boats, etc.)

Telegraf is already often used on flight computers (eg a Raspberry Pi) to collect system metrics for drones and it would be valuable to extend this to also provide a convenient way to record flight telemetry.

TODO

  • gomavlib is currently on a fork to support reading serial ports on i386 and darwin. Will need to upstream the fix for this and move back to the main repo after merge, or before merge if the forked gomavlib is not acceptable

Checklist

  • No AI generated code was used in this PR

Related issues

resolves #16220

@telegraf-tiger telegraf-tiger bot added the feat Improvement on an existing feature such as adding a new setting/mode to an existing plugin label Nov 22, 2024
plugins/inputs/mavlink/mavlink.go Outdated Show resolved Hide resolved
plugins/inputs/mavlink/mavlink.go Outdated Show resolved Hide resolved
plugins/inputs/mavlink/sample.conf Outdated Show resolved Hide resolved
plugins/inputs/mavlink/sample.conf Outdated Show resolved Hide resolved
plugins/inputs/mavlink/README.md Outdated Show resolved Hide resolved
plugins/inputs/mavlink/mavlink.go Outdated Show resolved Hide resolved
plugins/inputs/mavlink/mavlink_utils.go Outdated Show resolved Hide resolved
plugins/inputs/mavlink/mavlink_utils.go Outdated Show resolved Hide resolved
plugins/inputs/mavlink/mavlink.go Outdated Show resolved Hide resolved
plugins/inputs/mavlink/mavlink_utils.go Outdated Show resolved Hide resolved
Copy link
Contributor

@Hipska Hipska left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Going in good direction!

plugins/inputs/mavlink/mavlink.go Outdated Show resolved Hide resolved
plugins/inputs/mavlink/mavlink_utils.go Outdated Show resolved Hide resolved
plugins/inputs/mavlink/mavlink_utils.go Outdated Show resolved Hide resolved
plugins/inputs/mavlink/mavlink_utils.go Outdated Show resolved Hide resolved
plugins/inputs/mavlink/mavlink_utils.go Outdated Show resolved Hide resolved
plugins/inputs/mavlink/mavlink.go Outdated Show resolved Hide resolved
plugins/inputs/mavlink/mavlink.go Outdated Show resolved Hide resolved
plugins/inputs/mavlink/sample.conf Outdated Show resolved Hide resolved
plugins/inputs/mavlink/mavlink_utils.go Outdated Show resolved Hide resolved
plugins/inputs/mavlink/mavlink_utils.go Outdated Show resolved Hide resolved
@chrisdalke chrisdalke marked this pull request as ready for review December 6, 2024 03:50
@srebhan srebhan self-assigned this Dec 9, 2024
Copy link
Member

@srebhan srebhan left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@chrisdalke thanks for your contribution! Please check my comments in the code. Furthermore, I do have some general remarks

  1. I'm hesitant to merge plugins using forked one-man dependencies as experience shows that this is not maintainable on the long run. In your case I see two possibilities, the first being we hold-up the plugin until the upstream library merged your PR. This will likely take longer if possible at all.
    The second option is to base your plugin on the upstream library and limit the availablility of the plugin to the platforms supported (excluding i386 and darwin if I understand correctly). What do you think?
  2. Please keep the comments in the sample.conf file brief and intend the file with two spaces.
  3. Is there some way of mocking a Mavlink controller endpoint? E.g. using a minimal implementation mimicking some messages or a test-container/simulator?

Looking forward to your update!

docs/LICENSE_OF_DEPENDENCIES.md Outdated Show resolved Hide resolved
plugins/inputs/mavlink/README.md Outdated Show resolved Hide resolved
@@ -0,0 +1,39 @@
# Read metrics from a Mavlink connection to a flight controller.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
# Read metrics from a Mavlink connection to a flight controller.
# Read metrics from a Mavlink flight controller.

plugins/inputs/mavlink/sample.conf Outdated Show resolved Hide resolved
##
## The meaning of each of these modes is documented by
## https://mavsdk.mavlink.io/v1.4/en/cpp/guide/connections.html.
fcu_url = "udp://:14540"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
fcu_url = "udp://:14540"
url = "udp://:14540"

Comment on lines +92 to +97

case *gomavlib.EventChannelOpen:
s.Log.Debugf("Mavlink channel opened")

case *gomavlib.EventChannelClose:
s.Log.Debugf("Mavlink channel closed")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we need those? Should we do something about it if the channel is opened/closed?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These messages indicate that the Mavlink connection has started/stopped, which is separate from the Mavlink client initializing its background thread.

There's nothing Telegraf can do in this case but it helps the user debug the Mavlink setup which can be difficult. One example of where this could occur is if you had two TCP client connections with the same system_id one would get kicked off. Another example is a flaky USB port.

Without these messages it's hard to tell the difference between (1) Mavlink is not connected vs (2) Robot has a problem and is not broadcasting messages

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it help to reconnect if the channel is closed?

plugins/inputs/mavlink/mavlink.go Outdated Show resolved Hide resolved
plugins/inputs/mavlink/mavlink.go Outdated Show resolved Hide resolved
plugins/inputs/mavlink/mavlink.go Outdated Show resolved Hide resolved
plugins/inputs/mavlink/mavlink_test.go Outdated Show resolved Hide resolved
@srebhan srebhan added new plugin plugin/input 1. Request for new input plugins 2. Issues/PRs that are related to input plugins labels Dec 11, 2024
@srebhan srebhan changed the title feat(inputs): Add Mavlink input plugin feat(inputs.mavlink): Add plugin Dec 11, 2024
@telegraf-tiger
Copy link
Contributor

@chrisdalke
Copy link
Author

3. Is there some way of mocking a Mavlink controller endpoint? E.g. using a minimal implementation mimicking some messages or a test-container/simulator?

I can prepare an ArduPilot container which will output data over UDP -- Is there a reference to how you'd like this setup to match other plugins?

The name of the Mavlink message is translated into lowercase and any
leading text `message_` is dropped.

For example, [MESSAGE_ATTITUDE](https://mavlink.io/en/messages/common.html)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
For example, [MESSAGE_ATTITUDE](https://mavlink.io/en/messages/common.html)
For example, message [ATTITUDE](https://mavlink.io/en/messages/common.html#ATTITUDE)

## Example Output

```text
system_time,source=udp://:5760,sys_id=1 time_unix_usec=1732901334516981i,time_boot_ms=1731552i 0
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
system_time,source=udp://:5760,sys_id=1 time_unix_usec=1732901334516981i,time_boot_ms=1731552i 0
system_time,source=udp://:5760,sys_id=1 time_unix_usec=1732901334516981i,time_boot_ms=1731552i

Including no timestamp is better than including a wrong one.

Copy link
Member

@srebhan srebhan left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @chrisdalke for the update and the explanations! Some more comments in the code, nothing too big...

Regarding testing, I suggest to also add a unit-test for convertEventFrameToMetric to make sure we get sensible metrics from different event types... Please use table-based testing for this (see this example) to allow adding new types easily.

For the integration test you can take some inspirations from this plugin. Let me know if you need help!

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As you are setting url, system_id, stream_request_enable and stream_request_frequency to some default value in the code I expect these to be sensible defaults. If that's the case, please comment the option in the config to let the user know that those are sensible defaults and are optional to change.

Comment on lines +30 to +31
stream_request_enable = true
stream_request_frequency = 4
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about inverting the option so that false is the sensible default? Like

Suggested change
stream_request_enable = true
stream_request_frequency = 4
# disable_stream_requests = false
# stream_request_frequency = 4

Comment on lines +52 to +59
// Split host and port, and use default port if it was not specified
host, port, err := net.SplitHostPort(u.Host)
if err != nil {
// Use default port if we could not parse out the port.
host = u.Host
port = "14550"
}

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is only relevant for UDP and TCP but not for serial?!? If so, please move the code down to the respective sections.

Furthermore, there is also the possibility of malformed host names like 127.0.0.1:: so you should check if the error is "missing port in address"...

Comment on lines +81 to +93
if len(host) > 0 {
s.endpointConfig = []gomavlib.EndpointConf{
gomavlib.EndpointTCPClient{
Address: host + ":" + port,
},
}
} else {
s.endpointConfig = []gomavlib.EndpointConf{
gomavlib.EndpointTCPServer{
Address: ":" + port,
},
}
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This can be simplified to

Suggested change
if len(host) > 0 {
s.endpointConfig = []gomavlib.EndpointConf{
gomavlib.EndpointTCPClient{
Address: host + ":" + port,
},
}
} else {
s.endpointConfig = []gomavlib.EndpointConf{
gomavlib.EndpointTCPServer{
Address: ":" + port,
},
}
}
s.endpointConfig = []gomavlib.EndpointConf{
gomavlib.EndpointTCPClient{
Address: host + ":" + port,
},
}

same for UDP.

}

default:
return fmt.Errorf("could not parse url %s", s.URL)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually this should be

Suggested change
return fmt.Errorf("could not parse url %s", s.URL)
return fmt.Errorf("unknown scheme %q", u.Scheme)

Comment on lines +189 to +193
URL: "udp://:14540",
FilterPattern: make([]string, 0),
SystemID: 254,
StreamRequestEnable: true,
StreamRequestFrequency: 4,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Setting default values should be done in Init()! Here you only need to keep values where the nil value is a valid setting.

Suggested change
URL: "udp://:14540",
FilterPattern: make([]string, 0),
SystemID: 254,
StreamRequestEnable: true,
StreamRequestFrequency: 4,
SystemID: 254,
StreamRequestEnable: true,


filter filter.Filter
connection *gomavlib.Node
endpointConfig []gomavlib.EndpointConf
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this a list but you never set more than one config? Please change this to

Suggested change
endpointConfig []gomavlib.EndpointConf
endpointConfig gomavlib.EndpointConf

and then pass [] gomavlib.EndpointConf{s.endpointConfig} to the connection setup instead.

Comment on lines +12 to +21
testConfig := Mavlink{
URL: "serial:///dev/ttyACM0:115200",
}

err := testConfig.Init()
require.NoError(t, err)
endpoint, ok := testConfig.endpointConfig[0].(gomavlib.EndpointSerial)
require.True(t, ok)
require.Equal(t, "/dev/ttyACM0", endpoint.Device)
require.Equal(t, 115200, endpoint.Baud)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please be brief! It also helps readability to call the plugin instance plugin... ;-)

Suggested change
testConfig := Mavlink{
URL: "serial:///dev/ttyACM0:115200",
}
err := testConfig.Init()
require.NoError(t, err)
endpoint, ok := testConfig.endpointConfig[0].(gomavlib.EndpointSerial)
require.True(t, ok)
require.Equal(t, "/dev/ttyACM0", endpoint.Device)
require.Equal(t, 115200, endpoint.Baud)
plugin := &Mavlink{
URL: "serial:///dev/ttyACM0:115200",
}
require.NoError(t, plugin.Init())
endpoint, ok := plugin.endpointConfig.(gomavlib.EndpointSerial)
require.True(t, ok)
require.Equal(t, "/dev/ttyACM0", endpoint.Device)
require.Equal(t, 115200, endpoint.Baud)

Same for the tests below.

)

// Convert a Mavlink event into a struct containing Metric data.
func convertEventFrameToMetric(frm *gomavlib.EventFrame, msgFilter filter.Filter) telegraf.Metric {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suggest moving this function to mavlink.go and get rid of the extra file. What do you think?

return sampleConfig
}

func (s *Mavlink) Init() error {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is the variable named s? We usually use the first letter of the plugin type...

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
feat Improvement on an existing feature such as adding a new setting/mode to an existing plugin new plugin plugin/input 1. Request for new input plugins 2. Issues/PRs that are related to input plugins
Projects
None yet
Development

Successfully merging this pull request may close these issues.

feat(inputs): Add Mavlink input plugin
4 participants