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): Add Mavlink input plugin #16221

Draft
wants to merge 31 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

@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
Comment on lines 51 to 55
endpointConfig, err := ParseMavlinkEndpointConfig(s)
if err != nil {
s.Log.Debugf("%s", err.Error())
return
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Config validation should be done in func (s *Mavlink) Init() error method.

Copy link
Author

Choose a reason for hiding this comment

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

Fixed, move this code into Init.

}

func (s *Mavlink) Start(acc telegraf.Accumulator) error {
s.acc = acc
Copy link
Contributor

Choose a reason for hiding this comment

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

Not really necessary to copy the var I think,

Copy link
Author

Choose a reason for hiding this comment

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

Fixed.

# REQUEST_DATA_STREAM or MAV_CMD_SET_MESSAGE_INTERVAL
# (See https://mavlink.io/en/mavgen_python/howto_requestmessages.html#how-to-request--stream-messages)
stream_request_enable = true
stream_request_frequency = 4
Copy link
Contributor

Choose a reason for hiding this comment

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

There is no EOL at EOF

Copy link
Author

Choose a reason for hiding this comment

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

Fixed.

@@ -0,0 +1,37 @@
# Read metrics from a Mavlink connection to a flight controller.
[[inputs.mavlink]]
# Flight controller URL. Must be a valid Mavlink connection string in one
Copy link
Contributor

Choose a reason for hiding this comment

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

Comments need to have double escaped: ##

Copy link
Author

Choose a reason for hiding this comment

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

Fixed.

Comment on lines 65 to 71
### Note: Mavlink Dialects

This plugin currently only uses the ArduPilot-specific dialect, which also
includes messages from the common Mavlink dialect.

See the [Mavlink docs](https://mavlink.io/en/messages/ardupilotmega.html) for
more info on dialects.
Copy link
Contributor

Choose a reason for hiding this comment

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

Is this intended to be a sub-section of Configuration?

Copy link
Author

Choose a reason for hiding this comment

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

I've just removed this and merged it into the copy in the plugin summary at the top, doesn't need its own section.

Comment on lines 39 to 44
// Container for a parsed Mavlink frame
type metricFrameData struct {
name string
tags map[string]string
fields map[string]any
}
Copy link
Contributor

Choose a reason for hiding this comment

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

At this point, you can just use telegraf.Metric as that will be just the same?

Copy link
Author

Choose a reason for hiding this comment

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

Removed this type entirely in favor of passing around telegraf.Metric

for i := 0; i < t.NumField(); i++ {
field := t.Field(i)
value := v.Field(i)
out.fields[ConvertToSnakeCase(field.Name)] = value.Interface()
Copy link
Contributor

Choose a reason for hiding this comment

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

I don't think it is needed to do this conversion, Telegraf is perfectly fine with CamelCase field and metric names.

Copy link
Author

Choose a reason for hiding this comment

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

I'd like to keep this around, there's a lot of different conventions in mavlink message naming and not much consistency. The Mavlink docs use capitalized snake case like MESSAGE_GLOBAL_POSITION_INT, gomavlib converts that to MessageGlobalPositionInt, and some other client libraries use normal snake case like message_global_position_int.

My argument for using lowercase snake case is that, with a lack of one definite standard, I suspect by convention most people will want the outputs to be snake case in their database (Influx, Postgres, etc).

Copy link
Contributor

Choose a reason for hiding this comment

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

Okay, fine for me 👍

Comment on lines 22 to 30
// Check if a string is in a slice
func Contains(slice []string, str string) bool {
for _, item := range slice {
if item == str {
return true
}
}
return false
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Use internal choice.Contains instead.

Copy link
Author

Choose a reason for hiding this comment

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

Thanks, updated

Comment on lines 69 to 73
if err != nil {
s.Log.Debugf("Mavlink failed to connect (%s), will try again in 5s...", err.Error())
time.Sleep(5 * time.Second)
continue
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Simply return a internal.StartupError and let Telegraf handle the reconnect.

Copy link
Author

Choose a reason for hiding this comment

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

Updated so the Mavlink connection happens outside the loop and throws StartupError. One caveat I do see about doing it this way is that this causes Telegraf to shutdown if Mavlink client initialization fails, so we could lose the metrics from other plugins if we're rebooting waiting for the USB device which may not turn on at the same time as the companion computer.

That said, this condition actually only happens in serial port mode, where the plugin is looking for a serial device. In TCP or UDP mode the client internally handles reconnection so this won't cause the client to fail.

Comment on lines 60 to 63
if strings.HasPrefix(s.FcuURL, "serial://") {
tmpStr := strings.TrimPrefix(s.FcuURL, "serial://")
tmpStrParts := strings.Split(tmpStr, ":")
deviceName := tmpStrParts[0]
Copy link
Contributor

Choose a reason for hiding this comment

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

Go builtin net/url has useful Parse method to facilitate this easier..

Copy link
Author

@chrisdalke chrisdalke Nov 29, 2024

Choose a reason for hiding this comment

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

Updated to remove strings.HasPrefix(s.FcuURL, "serial://") and use url.Parse instead. There's still a little string find-and-replace here, because the serial port paths have forward slashes in them, which seems to mess with the URL parsing.

@telegraf-tiger
Copy link
Contributor

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!

// Parse out the Mavlink endpoint.
endpointConfig, err := ParseMavlinkEndpointConfig(s)
if err != nil {
return fmt.Errorf("%s", err.Error())
Copy link
Contributor

Choose a reason for hiding this comment

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

Simply return the error? Why wrapping the message in a new string to be added as error?

func ParseMavlinkEndpointConfig(s *Mavlink) ([]gomavlib.EndpointConf, error) {
u, err := url.Parse(s.FcuURL)
if err != nil {
return nil, errors.New("invalid fcu_url")
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
return nil, errors.New("invalid fcu_url")
return nil, fmt.Errorf("invalid fcu_url: %w", err)

if len(tmpStrParts) == 2 {
newBaudRate, err := strconv.Atoi(tmpStrParts[1])
if err != nil {
return nil, errors.New("serial baud rate not valid")
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
return nil, errors.New("serial baud rate not valid")
return nil, fmt.Errorf("serial baud rate not valid: %w", err)

And similar for all following error returns..

Comment on lines +87 to +89
tmpStr := strings.TrimPrefix(s.FcuURL, "tcp://")
tmpStrParts := strings.Split(tmpStr, ":")
if len(tmpStrParts) != 2 {
Copy link
Contributor

Choose a reason for hiding this comment

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

Please use net/url utilities

Suggested change
tmpStr := strings.TrimPrefix(s.FcuURL, "tcp://")
tmpStrParts := strings.Split(tmpStr, ":")
if len(tmpStrParts) != 2 {
if u.Port() == "" {

Comment on lines +93 to +97
hostname := tmpStrParts[0]
port, err := strconv.Atoi(tmpStrParts[1])
if err != nil {
return nil, errors.New("tcp port is invalid")
}
Copy link
Contributor

Choose a reason for hiding this comment

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

No need to convert port to int to convert it to string again later..

Suggested change
hostname := tmpStrParts[0]
port, err := strconv.Atoi(tmpStrParts[1])
if err != nil {
return nil, errors.New("tcp port is invalid")
}
hostname := u.Hostname()
port := u.Port()

(validity of port number already done in url.Parse.)

if len(s.MessageFilter) > 0 && !choice.Contains(result.Name(), s.MessageFilter) {
continue
}
result.AddTag("fcu_url", s.FcuURL)
Copy link
Contributor

Choose a reason for hiding this comment

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

We normally use source as tag name for indicating from where a metric arrived.

})
if err != nil {
return &internal.StartupError{
Err: fmt.Errorf("mavlink client failed (%w)", err),
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
Err: fmt.Errorf("mavlink client failed (%w)", err),
Err: fmt.Errorf("mavlink client failed: %w", err),


## Filter to specific messages. Only the messages in this list will be parsed.
## If blank or unset, all messages will be accepted.
# message_filter = ["global_position_int", "attitude"]
Copy link
Contributor

Choose a reason for hiding this comment

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

Commented out paramaters should contain the default value. This seems not to be the case here?

if len(hostname) > 0 {
return []gomavlib.EndpointConf{
gomavlib.EndpointTCPClient{
Address: fmt.Sprintf("%s:%d", hostname, port),
Copy link
Contributor

Choose a reason for hiding this comment

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

This could simply be

Suggested change
Address: fmt.Sprintf("%s:%d", hostname, port),
Address: u.Hostname,

As you checked before it has port number..

}

// Parse the FcuURL config to setup a mavlib endpoint config
func ParseMavlinkEndpointConfig(s *Mavlink) ([]gomavlib.EndpointConf, error) {
Copy link
Contributor

Choose a reason for hiding this comment

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

If this is the only value that is used in this function, then maybe just only pass that.

Suggested change
func ParseMavlinkEndpointConfig(s *Mavlink) ([]gomavlib.EndpointConf, error) {
func ParseMavlinkEndpointConfig(fcuURL string) ([]gomavlib.EndpointConf, error) {

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
Projects
None yet
Development

Successfully merging this pull request may close these issues.

feat(inputs): Add Mavlink input plugin
3 participants