Repository: influxdata/telegraf
Pull request #16780: feat(inputs.mavlink): Add plugin
Author: login: chrisdalke
Labels: name: feat
description: Improvement on an existing feature such as adding a new setting/mode to an existing plugin
name: new plugin
name: ready for final review
description: This pull request has been reviewed and/or tested by multiple users and is ready for a final review.
name: plugin/input
description: 1. Request for new input plugins 2. Issues/PRs that are related to input plugins
Created: 2025-04-14T01:26:30Z | Merged: 2025-05-09T21:18:57Z

## Summary

Add a Mavlink input plugin.

The `mavlink` plugin connects to a MavLink-compatible flight controller such as as [ArduPilot](https://ardupilot.org/) or [PX4](https://px4.io/). 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.

## Checklist
<!-- Mandatory
Please confirm the following by replacing the space with an "x" between the []:
-->

- [x] No AI generated code was used in this PR

## Related issues

resolves #16220
superseeds #16221

Linked issue #16220: feat(inputs): Add Mavlink input plugin
### Use Case

It would be useful to have a Mavlink input plugin which connects to a MavLink-compatible flight controller such as as [ArduPilot](https://ardupilot.org/) or [PX4](https://px4.io/). and translates all incoming messages into metrics.

This would allow Telegraf to be used to ingest live flight metrics from unmanned systems (drones, planes, boats, etc.) on a companion computer.

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.


### Expected behavior

The Mavlink plugin should:
- Take in a connection string and other relevant settings
- Start an sync connection to Mavlink
- Convert any received Mavlink messages into metrics
- Gracefully handle disconnection/reconnection without requiring a telegraf restart

### Actual behavior

N/A

### Additional info

I am creating a PR to add this plugin.

Diff:
diff --git a/docs/LICENSE_OF_DEPENDENCIES.md b/docs/LICENSE_OF_DEPENDENCIES.md
index 0670cc9670070..d1cc629a7c847 100644
--- a/docs/LICENSE_OF_DEPENDENCIES.md
+++ b/docs/LICENSE_OF_DEPENDENCIES.md
@@ -97,6 +97,7 @@ following works:
 - github.com/aws/smithy-go [Apache License 2.0](https://github.com/aws/smithy-go/blob/main/LICENSE)
 - github.com/benbjohnson/clock [MIT License](https://github.com/benbjohnson/clock/blob/master/LICENSE)
 - github.com/beorn7/perks [MIT License](https://github.com/beorn7/perks/blob/master/LICENSE)
+- github.com/bluenviron/gomavlib [MIT License](https://github.com/bluenviron/gomavlib/blob/main/LICENSE)
 - github.com/blues/jsonata-go [MIT License](https://github.com/blues/jsonata-go/blob/main/LICENSE)
 - github.com/bmatcuk/doublestar [MIT License](https://github.com/bmatcuk/doublestar/blob/master/LICENSE)
 - github.com/boschrexroth/ctrlx-datalayer-golang [MIT License](https://github.com/boschrexroth/ctrlx-datalayer-golang/blob/main/LICENSE)
@@ -121,6 +122,7 @@ following works:
 - github.com/couchbase/goutils [Apache License 2.0](https://github.com/couchbase/goutils/blob/master/LICENSE.md)
 - github.com/cpuguy83/dockercfg [MIT License](https://github.com/cpuguy83/dockercfg/blob/main/LICENSE)
 - github.com/cpuguy83/go-md2man [MIT License](https://github.com/cpuguy83/go-md2man/blob/master/LICENSE.md)
+- github.com/creack/goselect [MIT License](https://github.com/creack/goselect/blob/master/LICENSE)
 - github.com/danieljoos/wincred [MIT License](https://github.com/danieljoos/wincred/blob/master/LICENSE)
 - github.com/datadope-io/go-zabbix [MIT License](https://github.com/datadope-io/go-zabbix/blob/master/LICENSE)
 - github.com/davecgh/go-spew [ISC License](https://github.com/davecgh/go-spew/blob/master/LICENSE)
@@ -421,6 +423,7 @@ following works:
 - github.com/zeebo/errs [MIT License](https://github.com/zeebo/errs/blob/master/LICENSE)
 - github.com/zeebo/xxh3 [BSD 2-Clause "Simplified" License](https://github.com/zeebo/xxh3/blob/master/LICENSE)
 - github.com/zentures/cityhash [MIT License](https://github.com/zentures/cityhash/blob/master/LICENSE)
+- go.bug.st/serial [BSD 3-Clause License](https://github.com/bugst/go-serial/blob/master/LICENSE)
 - go.mongodb.org/mongo-driver [Apache License 2.0](https://github.com/mongodb/mongo-go-driver/blob/master/LICENSE)
 - go.opencensus.io [Apache License 2.0](https://github.com/census-instrumentation/opencensus-go/blob/master/LICENSE)
 - go.opentelemetry.io/auto/sdk [Apache License 2.0](https://github.com/open-telemetry/opentelemetry-go-instrumentation/blob/main/sdk/LICENSE)
diff --git a/go.mod b/go.mod
index 148d333252ce9..af09e88f87930 100644
--- a/go.mod
+++ b/go.mod
@@ -62,6 +62,7 @@ require (
 	github.com/aws/aws-sdk-go-v2/service/timestreamwrite v1.30.2
 	github.com/aws/smithy-go v1.22.3
 	github.com/benbjohnson/clock v1.3.5
+	github.com/bluenviron/gomavlib/v3 v3.1.0
 	github.com/blues/jsonata-go v1.5.4
 	github.com/bmatcuk/doublestar/v3 v3.0.0
 	github.com/boschrexroth/ctrlx-datalayer-golang v1.3.1
@@ -323,6 +324,7 @@ require (
 	github.com/couchbase/goutils v0.1.0 // indirect
 	github.com/cpuguy83/dockercfg v0.3.2 // indirect
 	github.com/cpuguy83/go-md2man/v2 v2.0.6 // indirect
+	github.com/creack/goselect v0.1.2 // indirect
 	github.com/danieljoos/wincred v1.2.2 // indirect
 	github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
 	github.com/devigned/tab v0.1.1 // indirect
@@ -464,7 +466,7 @@ require (
 	github.com/philhofer/fwd v1.1.3-0.20240916144458-20a13a1f6b7c // indirect
 	github.com/pierrec/lz4/v4 v4.1.22 // indirect
 	github.com/pion/logging v0.2.2 // indirect
-	github.com/pion/transport/v2 v2.2.4 // indirect
+	github.com/pion/transport/v2 v2.2.10 // indirect
 	github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect
 	github.com/pkg/errors v0.9.1 // indirect
 	github.com/pkg/sftp v1.13.9 // indirect
@@ -518,6 +520,7 @@ require (
 	github.com/zitadel/logging v0.6.2 // indirect
 	github.com/zitadel/oidc/v3 v3.37.0 // indirect
 	github.com/zitadel/schema v1.3.1 // indirect
+	go.bug.st/serial v1.6.2 // indirect
 	go.etcd.io/etcd/api/v3 v3.5.4 // indirect
 	go.opencensus.io v0.24.0 // indirect
 	go.opentelemetry.io/auto/sdk v1.1.0 // indirect
diff --git a/go.sum b/go.sum
index 5d70f4c09ef0a..1e518b43a7d6f 100644
--- a/go.sum
+++ b/go.sum
@@ -1,3 +1,5 @@
+bou.ke/monkey v1.0.2 h1:kWcnsrCNUatbxncxR/ThdYqbytgOIArtYWqcQLQzKLI=
+bou.ke/monkey v1.0.2/go.mod h1:OqickVX3tNx6t33n1xvtTtu85YN5s6cKwVug+oHMaIA=
 cel.dev/expr v0.23.1 h1:K4KOtPCJQjVggkARsjG9RWXP6O4R73aHeJMa/dmCQQg=
 cel.dev/expr v0.23.1/go.mod h1:hLPLo1W4QUmuYdA72RBX06QTs6MXw941piREPl3Yfiw=
 cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
@@ -956,6 +958,8 @@ github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kB
 github.com/bitly/go-hostpool v0.1.0 h1:XKmsF6k5el6xHG3WPJ8U0Ku/ye7njX7W81Ng7O2ioR0=
 github.com/bitly/go-hostpool v0.1.0/go.mod h1:4gOCgp6+NZnVqlKyZ/iBZFTAJKembaVENUpMkpg42fw=
 github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84=
+github.com/bluenviron/gomavlib/v3 v3.1.0 h1:+CDYAkn2FZQZijYwaHS2pmoVu5ZsfGwWSV56CjkxzfE=
+github.com/bluenviron/gomavlib/v3 v3.1.0/go.mod h1:6iGC6AvAFl7OcDQHhyNYq8uMrqcRYW0ksdc/a0/29UM=
 github.com/blues/jsonata-go v1.5.4 h1:XCsXaVVMrt4lcpKeJw6mNJHqQpWU751cnHdCFUq3xd8=
 github.com/blues/jsonata-go v1.5.4/go.mod h1:uns2jymDrnI7y+UFYCqsRTEiAH22GyHnNXrkupAVFWI=
 github.com/bmatcuk/doublestar v1.1.1 h1:YroD6BJCZBYx06yYFEWvUuKVWQn3vLLQAVmDmvTSaiQ=
@@ -1079,6 +1083,8 @@ github.com/cpuguy83/dockercfg v0.3.2/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHf
 github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
 github.com/cpuguy83/go-md2man/v2 v2.0.6 h1:XJtiaUW6dEEqVuZiMTn1ldk455QWwEIsMIJlo5vtkx0=
 github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
+github.com/creack/goselect v0.1.2 h1:2DNy14+JPjRBgPzAd1thbQp4BSIihxcBf0IXhQXDRa0=
+github.com/creack/goselect v0.1.2/go.mod h1:a/NhLweNvqIYMuxcMOuWY516Cimucms3DglDzQP3hKY=
 github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=
 github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
 github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY=
@@ -2092,8 +2098,11 @@ github.com/pion/dtls/v2 v2.2.12 h1:KP7H5/c1EiVAAKUmXyCzPiQe5+bCJrpOeKg/L05dunk=
 github.com/pion/dtls/v2 v2.2.12/go.mod h1:d9SYc9fch0CqK90mRk1dC7AkzzpwJj6u2GU3u+9pqFE=
 github.com/pion/logging v0.2.2 h1:M9+AIj/+pxNsDfAT64+MAVgJO0rsyLnoJKCqf//DoeY=
 github.com/pion/logging v0.2.2/go.mod h1:k0/tDVsRCX2Mb2ZEmTqNa7CWsQPc+YYCB7Q+5pahoms=
-github.com/pion/transport/v2 v2.2.4 h1:41JJK6DZQYSeVLxILA2+F4ZkKb4Xd/tFJZRFZQ9QAlo=
 github.com/pion/transport/v2 v2.2.4/go.mod h1:q2U/tf9FEfnSBGSW6w5Qp5PFWRLRj3NjLhCCgpRK4p0=
+github.com/pion/transport/v2 v2.2.10 h1:ucLBLE8nuxiHfvkFKnkDQRYWYfp8ejf4YBOPfaQpw6Q=
+github.com/pion/transport/v2 v2.2.10/go.mod h1:sq1kSLWs+cHW9E+2fJP95QudkzbK7wscs8yYgQToO5E=
+github.com/pion/transport/v3 v3.0.6 h1:k1mQU06bmmX143qSWgXFqSH1KUJceQvIUuVH/K5ELWw=
+github.com/pion/transport/v3 v3.0.6/go.mod h1:HvJr2N/JwNJAfipsRleqwFoR3t/pWyHeZUs89v3+t5s=
 github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
 github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
 github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
@@ -2385,6 +2394,7 @@ github.com/vmware/govmomi v0.50.0 h1:vFOnUCBCX3m3MgTKfBp68Pz5gsHvKkO07Y2wCGYYQOM
 github.com/vmware/govmomi v0.50.0/go.mod h1:Z5uo7z0kRhVV00E4gfbUGwUaXIKTgqngsT+t/mIDpcI=
 github.com/wavefronthq/wavefront-sdk-go v0.15.0 h1:po9E3vh/0y7kOx8D9EtFp7kbSLLLKbmu/w/s1xGJAQU=
 github.com/wavefronthq/wavefront-sdk-go v0.15.0/go.mod h1:V72c8e+bXuLK8HpA6ioW0ll5mK9IPD+4IHNNDY75ksA=
+github.com/wlynxg/anet v0.0.3/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguHxoA=
 github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
 github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
 github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM=
@@ -2444,6 +2454,8 @@ github.com/zitadel/oidc/v3 v3.37.0 h1:nYATWlnP7f18XiAbw6upUruBaqfB1kUrXrSTf1EYGO
 github.com/zitadel/oidc/v3 v3.37.0/go.mod h1:/xDan4OUQhguJ4Ur73OOJrtugvR164OMnidXP9xfVNw=
 github.com/zitadel/schema v1.3.1 h1:QT3kwiRIRXXLVAs6gCK/u044WmUVh6IlbLXUsn6yRQU=
 github.com/zitadel/schema v1.3.1/go.mod h1:071u7D2LQacy1HAN+YnMd/mx1qVE2isb0Mjeqg46xnU=
+go.bug.st/serial v1.6.2 h1:kn9LRX3sdm+WxWKufMlIRndwGfPWsH1/9lCWXQCasq8=
+go.bug.st/serial v1.6.2/go.mod h1:UABfsluHAiaNI+La2iESysd9Vetq7VRdpxvjx7CmmOE=
 go.einride.tech/aip v0.68.1 h1:16/AfSxcQISGN5z9C5lM+0mLYXihrHbQ1onvYTr93aQ=
 go.einride.tech/aip v0.68.1/go.mod h1:XaFtaj4HuA3Zwk9xoBtTWgNubZ0ZZXv9BZJCkuKuWbg=
 go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
diff --git a/plugins/inputs/all/mavlink.go b/plugins/inputs/all/mavlink.go
new file mode 100644
index 0000000000000..d0fe524fc7b4c
--- /dev/null
+++ b/plugins/inputs/all/mavlink.go
@@ -0,0 +1,5 @@
+//go:build !custom || inputs || inputs.mavlink
+
+package all
+
+import _ "github.com/influxdata/telegraf/plugins/inputs/mavlink" // register plugin
diff --git a/plugins/inputs/mavlink/README.md b/plugins/inputs/mavlink/README.md
new file mode 100644
index 0000000000000..599e7b18e5353
--- /dev/null
+++ b/plugins/inputs/mavlink/README.md
@@ -0,0 +1,95 @@
+# MavLink Input Plugin
+
+This plugin collects metrics from [MavLink][mavlink]-compatible flight
+controllers such as [ArduPilot][ardupilot] or [PX4][px4] to live ingest
+flight metrics from unmanned systems (drones, planes, boats, etc.)
+Currently the ArduPilot-specific Mavlink dialect is used, check the
+[Mavlink documentation][mavlink_docs] for more details and the various
+messages available.
+
+> [!WARNING]
+> This plugin potentially generates a large amount of data. If your output plugin cannot handle the rate of messages, use [Metric filters][metric_filters] to limit the metrics written to outputs, and/or the `filters` configuration parameter to limit which Mavlink messages this plugin parses.
+
+⭐ Telegraf v1.35.0
+🏷️ iot
+💻 all
+
+[ardupilot]: https://ardupilot.org/
+[mavlink]: https://mavlink.io/
+[mavlink_docs]: https://mavlink.io/en/messages/ardupilotmega.html
+[metric_filters]: ../../../docs/CONFIGURATION.md#metric-filtering
+[px4]: https://px4.io/
+
+## Global configuration options <!-- @/docs/includes/plugin_config.md -->
+
+In addition to the plugin-specific configuration settings, plugins support
+additional global and plugin configuration settings. These settings are used to
+modify metrics, tags, and field or create aliases and configure ordering, etc.
+See the [CONFIGURATION.md][CONFIGURATION.md] for more details.
+
+[CONFIGURATION.md]: ../../../docs/CONFIGURATION.md#plugins
+
+## Configuration
+
+```toml @sample.conf
+# Read metrics from a Mavlink flight controller.
+[[inputs.mavlink]]
+  ## Flight controller URL supporting serial port, UDP and TCP connections.
+  ## Options are documented at
+  ##   https://mavsdk.mavlink.io/v1.4/en/cpp/guide/connections.html.
+  ##
+  ## Examples:
+  ## - Serial port: serial:///dev/ttyACM0:57600
+  ## - TCP client:  tcp://192.168.1.12:5760
+  ## - UDP client:  udp://192.168.1.12:14550
+  ## - TCP server:  tcpserver://:5760
+  ## - UDP server:  udpserver://:14550
+  # url = "tcp://127.0.0.1:5760"
+
+  ## Filter to specific messages. Only the messages in this list will be parsed.
+  ## If blank or unset, all messages will be accepted. Glob syntax is accepted.
+  ## Each message in this list should be lowercase camel_case, with "message_"
+  ## prefix removed, eg: "global_position_int", "attitude"
+  # filter = []
+
+  ## Mavlink system ID for Telegraf. Only used if the mavlink plugin is sending 
+  ## messages, eg. when `stream_request_frequency` is 0 (see below.)
+  # system_id = 254
+
+  ## Determines whether the plugin sends requests to subscribe to data.
+  ## In mavlink, stream rates must be configured before data is received.
+  ## This config item sets the rate in Hz, with 0 disabling the request.
+  ## 
+  ## This frequency should be set to 0 if your software already controls the 
+  ## rates using REQUEST_DATA_STREAM or MAV_CMD_SET_MESSAGE_INTERVAL
+  ## (See https://mavlink.io/en/mavgen_python/howto_requestmessages.html)
+  # stream_request_frequency = 4
+```
+
+## Metrics
+
+Each supported Mavlink message translates to one metric group, and fields
+on the Mavlink message are converted to fields in telegraf.
+
+The name of the Mavlink message is translated into lowercase and any
+leading text `message_` is dropped.
+
+For example, the message [ATTITUDE][attitude] will become an `attitude` metric,
+with all fields copied from its Mavlink message definition.
+
+[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
+ekf_status_report,source=udp://:5760,sys_id=1 velocity_variance=0.006436665542423725,pos_horiz_variance=0.006062425673007965,pos_vert_variance=0.0029854460153728724,compass_variance=0.010930062271654606,terrain_alt_variance=0,airspeed_variance=0
+local_position_ned,source=udp://:5760,sys_id=1 time_boot_ms=1731552i,x=-0.010437906719744205,y=-0.02162001095712185,z=-0.0037050051614642143,vx=-0.011906237341463566,vy=-0.02467793971300125,vz=0.012739507481455803
+vibration,source=udp://:5760,sys_id=1 time_usec=1731552102i,vibration_x=0.0028534166049212217,vibration_y=0.002792230574414134,vibration_z=0.0028329004999250174,clipping_0=0i,clipping_1=0i,clipping_2=0i
+battery_status,source=udp://:5760,sys_id=1 id=0i,temperature=32767i,current_battery=0i,current_consumed=0i,energy_consumed=0i,battery_remaining=100i,time_remaining=0i
+ahrs,source=udp://:5760,sys_id=1 omegaix=-0.0012698185164481401,omegaiy=-0.0011798597406595945,omegaiz=-0.0017210562946274877,accel_weight=0,renorm_val=0,error_rp=0.002372326795011759,error_yaw=0.0014012008905410767
+ahrs2,source=udp://:5760,sys_id=1 roll=-0.0015893152449280024,pitch=-0.0018129277741536498,yaw=-1.2297048568725586,altitude=0.22999998927116394,lat=450469223i,lng=-834024728i
+attitude,source=udp://:5760,sys_id=1 time_boot_ms=1731811i,roll=-0.0011288427049294114,pitch=-0.0013485358795151114,yaw=-1.2430261373519897,rollspeed=-0.00023304438218474388,pitchspeed=-0.00023194786626845598,yawspeed=-0.0008081073756329715 0
+global_position_int,source=udp://:5760,sys_id=1 time_boot_ms=1731811i,lat=450469223i,lon=-834024730i,alt=0i,relative_alt=-115i,vx=-1i,vy=-2i,vz=1i,hdg=28878i
+gps_raw_int,source=udp://:5760,sys_id=1 time_usec=1731635000i,lat=450469223i,lon=-834024728i,alt=0i,eph=121i,epv=200i,vel=0i,cog=0i,satellites_visible=10i,alt_ellipsoid=0i,hacc=300i,vacc=300i,vel_acc=40i,hdg_acc=0i,yaw=0i
+```
diff --git a/plugins/inputs/mavlink/mavlink.go b/plugins/inputs/mavlink/mavlink.go
new file mode 100644
index 0000000000000..a3f9216fd6051
--- /dev/null
+++ b/plugins/inputs/mavlink/mavlink.go
@@ -0,0 +1,261 @@
+//go:generate ../../../tools/readme_config_includer/generator
+package mavlink
+
+import (
+	"context"
+	_ "embed"
+	"errors"
+	"fmt"
+	"net"
+	"net/url"
+	"reflect"
+	"strconv"
+	"strings"
+	"sync"
+
+	"github.com/bluenviron/gomavlib/v3"
+	"github.com/bluenviron/gomavlib/v3/pkg/dialects/ardupilotmega"
+	"github.com/bluenviron/gomavlib/v3/pkg/frame"
+
+	"github.com/influxdata/telegraf"
+	"github.com/influxdata/telegraf/filter"
+	"github.com/influxdata/telegraf/internal"
+	"github.com/influxdata/telegraf/plugins/inputs"
+)
+
+type Mavlink struct {
+	URL                    string          `toml:"url"`
+	SystemID               uint8           `toml:"system_id"`
+	FilterPattern          []string        `toml:"filter"`
+	StreamRequestFrequency uint16          `toml:"stream_request_frequency"`
+	Log                    telegraf.Logger `toml:"-"`
+
+	filter         filter.Filter
+	connection     *gomavlib.Node
+	endpointConfig gomavlib.EndpointConf
+	cancel         context.CancelFunc
+	wg             sync.WaitGroup
+}
+
+//go:embed sample.conf
+var sampleConfig string
+
+func (*Mavlink) SampleConfig() string {
+	return sampleConfig
+}
+
+func (m *Mavlink) Init() error {
+	// Set default values
+	if m.URL == "" {
+		m.URL = "tcp://127.0.0.1:5760"
+	}
+
+	// Handle the different Mavlink endpoint schemata
+	u, err := url.Parse(m.URL)
+	if err != nil {
+		return fmt.Errorf("invalid url: %w", err)
+	}
+
+	switch u.Scheme {
+	case "serial":
+		// Parse serial URL by hand, because it is not a compliant URL.
+		// Use the default baudrate if not specified
+		baudRate := 57600
+		device, rate, found := strings.Cut(strings.TrimPrefix(m.URL, "serial://"), ":")
+		if found {
+			r, err := strconv.Atoi(rate)
+			if err != nil {
+				return fmt.Errorf("serial baud rate not valid: %w", err)
+			}
+			baudRate = r
+		}
+
+		m.endpointConfig = gomavlib.EndpointSerial{
+			Device: device,
+			Baud:   baudRate,
+		}
+
+	case "tcp":
+		// Use default TCP port if it was not specified
+		port := u.Port()
+		if port == "" {
+			port = "5760"
+		}
+
+		if u.Hostname() == "" {
+			return errors.New("tcp client requires a hostname")
+		}
+
+		m.endpointConfig = gomavlib.EndpointTCPClient{
+			Address: net.JoinHostPort(u.Hostname(), port),
+		}
+
+	case "tcpserver":
+		// Use default TCP port if it was not specified
+		port := u.Port()
+		if port == "" {
+			port = "5760"
+		}
+
+		// Use default host 0.0.0.0 (bind on all interfaces)
+		// if host was not specified
+		hostname := u.Hostname()
+		if hostname == "" {
+			hostname = "0.0.0.0"
+		}
+
+		m.endpointConfig = gomavlib.EndpointTCPServer{
+			Address: net.JoinHostPort(hostname, port),
+		}
+
+	case "udp":
+		// Use default UDP port if it was not specified
+		port := u.Port()
+		if port == "" {
+			port = "14550"
+		}
+
+		if u.Hostname() == "" {
+			return errors.New("udp client requires a hostname")
+		}
+
+		m.endpointConfig = gomavlib.EndpointUDPClient{
+			Address: net.JoinHostPort(u.Hostname(), port),
+		}
+
+	case "udpserver":
+		// Use default UDP port if it was not specified
+		port := u.Port()
+		if port == "" {
+			port = "14550"
+		}
+
+		// Use default host 0.0.0.0 (bind on all interfaces)
+		// if host was not specified
+		hostname := u.Hostname()
+		if hostname == "" {
+			hostname = "0.0.0.0"
+		}
+
+		m.endpointConfig = gomavlib.EndpointUDPServer{
+			Address: net.JoinHostPort(hostname, port),
+		}
+
+	default:
+		return fmt.Errorf("unknown scheme %q", u.Scheme)
+	}
+
+	// Compile filter
+	m.filter, err = filter.Compile(m.FilterPattern)
+	if err != nil {
+		return fmt.Errorf("compiling filter failed: %w", err)
+	}
+
+	return nil
+}
+
+func (m *Mavlink) Start(acc telegraf.Accumulator) error {
+	// Start MAVLink endpoint
+	connection, err := gomavlib.NewNode(gomavlib.NodeConf{
+		Endpoints:              []gomavlib.EndpointConf{m.endpointConfig},
+		Dialect:                ardupilotmega.Dialect,
+		OutVersion:             gomavlib.V2,
+		OutSystemID:            m.SystemID,
+		StreamRequestEnable:    m.StreamRequestFrequency > 0,
+		StreamRequestFrequency: int(m.StreamRequestFrequency),
+	})
+	if err != nil {
+		return &internal.StartupError{
+			Err:   fmt.Errorf("connecting to mavlink endpoint %s failed: %w", m.URL, err),
+			Retry: true,
+		}
+	}
+	m.connection = connection
+	ctx, cancelFunc := context.WithCancel(context.Background())
+	m.cancel = cancelFunc
+
+	// Start routine to connect to Mavlink and stream out data async
+	m.wg.Add(1)
+	go func(ctx context.Context) {
+		defer m.connection.Close()
+		defer m.wg.Done()
+
+		for {
+			select {
+			case <-ctx.Done():
+				return
+			case evt := <-m.connection.Events():
+				switch evt := evt.(type) {
+				case *gomavlib.EventFrame:
+					m.handleFrame(acc, evt.Frame)
+				case *gomavlib.EventChannelOpen:
+					m.Log.Tracef("Mavlink channel opened")
+				case *gomavlib.EventChannelClose:
+					m.Log.Tracef("Mavlink channel closed")
+				case *gomavlib.EventParseError:
+					m.Log.Tracef("Mavlink parse error: %v", evt.Error)
+				case *gomavlib.EventStreamRequested:
+					m.Log.Tracef("Issued stream request to system %d, component %d", evt.SystemID, evt.ComponentID)
+				default:
+					m.Log.Tracef("Unhandled Mavlink event type: %T", evt)
+				}
+			}
+		}
+	}(ctx)
+
+	return nil
+}
+
+func (*Mavlink) Gather(telegraf.Accumulator) error {
+	return nil
+}
+
+func (m *Mavlink) Stop() {
+	m.cancel()
+	m.wg.Wait()
+}
+
+// Convert a Mavlink frame into a telegraf Metric.
+func (m *Mavlink) handleFrame(acc telegraf.Accumulator, frm frame.Frame) {
+	v := reflect.Indirect(reflect.ValueOf(frm.GetMessage()))
+	t := v.Type()
+
+	name := internal.SnakeCase(strings.TrimPrefix(t.Name(), "Message"))
+	if m.filter != nil && !m.filter.Match(name) {
+		return
+	}
+
+	tags := map[string]string{
+		"sys_id": strconv.FormatUint(uint64(frm.GetSystemID()), 10),
+		"source": m.URL,
+	}
+	fields := make(map[string]interface{}, t.NumField())
+
+	for i := 0; i < t.NumField(); i++ {
+		field := t.Field(i)
+		value := v.Field(i)
+		fieldName := internal.SnakeCase(field.Name)
+
+		if value.Kind() == reflect.Slice || value.Kind() == reflect.Array {
+			// Split array types into individual primitive values
+			// with _<n> appended to the key
+			for j := 0; j < value.Len(); j++ {
+				indexedFieldName := fmt.Sprintf("%s_%d", fieldName, j+1)
+				fields[indexedFieldName] = value.Index(j).Interface()
+			}
+		} else {
+			fields[fieldName] = value.Interface()
+		}
+	}
+
+	acc.AddFields(name, fields, tags)
+}
+
+func init() {
+	inputs.Add("mavlink", func() telegraf.Input {
+		return &Mavlink{
+			SystemID:               254,
+			StreamRequestFrequency: 4,
+		}
+	})
+}
diff --git a/plugins/inputs/mavlink/mavlink_test.go b/plugins/inputs/mavlink/mavlink_test.go
new file mode 100644
index 0000000000000..c759b1e2040e6
--- /dev/null
+++ b/plugins/inputs/mavlink/mavlink_test.go
@@ -0,0 +1,315 @@
+package mavlink
+
+import (
+	"testing"
+	"time"
+
+	"github.com/bluenviron/gomavlib/v3"
+	"github.com/bluenviron/gomavlib/v3/pkg/dialects/common"
+	"github.com/bluenviron/gomavlib/v3/pkg/frame"
+	"github.com/docker/go-connections/nat"
+	"github.com/stretchr/testify/require"
+	"github.com/testcontainers/testcontainers-go/wait"
+
+	"github.com/influxdata/telegraf"
+	"github.com/influxdata/telegraf/metric"
+	"github.com/influxdata/telegraf/testutil"
+)
+
+// Test that all URL types can be parsed successfully
+func TestParseURL(t *testing.T) {
+	tests := []struct {
+		name     string
+		url      string
+		expected gomavlib.EndpointConf
+	}{
+		{
+			name: "serial Linux",
+			url:  "serial:///dev/ttyACM0:115200",
+			expected: gomavlib.EndpointSerial{
+				Device: "/dev/ttyACM0",
+				Baud:   115200,
+			},
+		},
+		{
+			name: "serial Linux with default baudrate",
+			url:  "serial:///dev/ttyACM0",
+			expected: gomavlib.EndpointSerial{
+				Device: "/dev/ttyACM0",
+				Baud:   57600,
+			},
+		},
+		{
+			name: "serial Windows",
+			url:  "serial://COM1:115200",
+			expected: gomavlib.EndpointSerial{
+				Device: "COM1",
+				Baud:   115200,
+			},
+		},
+		{
+			name: "serial Windows with default baudrate",
+			url:  "serial://COM1",
+			expected: gomavlib.EndpointSerial{
+				Device: "COM1",
+				Baud:   57600,
+			},
+		},
+		{
+			name: "UDP client",
+			url:  "udp://192.168.1.12:14550",
+			expected: gomavlib.EndpointUDPClient{
+				Address: "192.168.1.12:14550",
+			},
+		},
+		{
+			name: "UDP client with default port",
+			url:  "udp://192.168.1.12",
+			expected: gomavlib.EndpointUDPClient{
+				Address: "192.168.1.12:14550",
+			},
+		},
+		{
+			name: "UDP server",
+			url:  "udpserver://:14550",
+			expected: gomavlib.EndpointUDPServer{
+				Address: "0.0.0.0:14550",
+			},
+		},
+		{
+			name: "UDP server with default port",
+			url:  "udpserver://",
+			expected: gomavlib.EndpointUDPServer{
+				Address: "0.0.0.0:14550",
+			},
+		},
+		{
+			name: "UDP server on localhost",
+			url:  "udpserver://127.0.0.1",
+			expected: gomavlib.EndpointUDPServer{
+				Address: "127.0.0.1:14550",
+			},
+		},
+		{
+			name: "TCP client",
+			url:  "tcp://192.168.1.12:5760",
+			expected: gomavlib.EndpointTCPClient{
+				Address: "192.168.1.12:5760",
+			},
+		},
+		{
+			name: "TCP client with default port",
+			url:  "tcp://192.168.1.12",
+			expected: gomavlib.EndpointTCPClient{
+				Address: "192.168.1.12:5760",
+			},
+		},
+		{
+			name: "TCP server",
+			url:  "tcpserver://:5761",
+			expected: gomavlib.EndpointTCPServer{
+				Address: "0.0.0.0:5761",
+			},
+		},
+		{
+			name: "TCP server with default port",
+			url:  "tcpserver://",
+			expected: gomavlib.EndpointTCPServer{
+				Address: "0.0.0.0:5760",
+			},
+		},
+		{
+			name: "TCP server on localhost",
+			url:  "tcpserver://127.0.0.1",
+			expected: gomavlib.EndpointTCPServer{
+				Address: "127.0.0.1:5760",
+			},
+		},
+		{
+			name: "Default connection",
+			url:  "",
+			expected: gomavlib.EndpointTCPClient{
+				Address: "127.0.0.1:5760",
+			},
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			// Setup the plugin
+			plugin := &Mavlink{URL: tt.url}
+			require.NoError(t, plugin.Init())
+
+			// Check the resulting endpoint configuration
+			require.EqualValues(t, tt.expected, plugin.endpointConfig)
+		})
+	}
+}
+
+// Test that some mavlink messages are correctly decoded into telegraf metrics.
+func TestMavlinkDecoding(t *testing.T) {
+	tests := []struct {
+		name     string
+		input    *frame.V2Frame
+		expected []telegraf.Metric
+	}{
+		{
+			name: "Heartbeat",
+			input: &frame.V2Frame{
+				SequenceNumber: 1,
+				SystemID:       1,
+				ComponentID:    1,
+				Message: &common.MessageHeartbeat{
+					Type:           0,
+					Autopilot:      1,
+					BaseMode:       2,
+					CustomMode:     3,
+					SystemStatus:   4,
+					MavlinkVersion: 5,
+				},
+				Checksum: 0,
+			},
+			expected: []telegraf.Metric{metric.New(
+				"heartbeat",
+				map[string]string{
+					"sys_id": "1",
+					"source": "udpserver://",
+				},
+				map[string]interface{}{
+					"custom_mode":     uint32(3),
+					"mavlink_version": uint8(5),
+				},
+				time.Unix(0, 0),
+			)},
+		},
+		{
+			name: "Attitude",
+			input: &frame.V2Frame{
+				SequenceNumber: 1,
+				SystemID:       1,
+				ComponentID:    1,
+				Message: &common.MessageAttitude{
+					TimeBootMs: uint32(123),
+					Roll:       float32(1.234),
+					Pitch:      float32(0.463),
+					Yaw:        float32(-0.112),
+					Rollspeed:  float32(0.001),
+					Pitchspeed: float32(0.002),
+					Yawspeed:   float32(0.003),
+				},
+				Checksum: 0,
+			},
+			expected: []telegraf.Metric{metric.New(
+				"attitude",
+				map[string]string{
+					"sys_id": "1",
+					"source": "udpserver://",
+				},
+				map[string]interface{}{
+					"pitch":        float32(0.463),
+					"roll":         float32(1.234),
+					"yaw":          float32(-0.112),
+					"pitchspeed":   float32(0.002),
+					"rollspeed":    float32(0.001),
+					"yawspeed":     float32(0.003),
+					"time_boot_ms": uint32(123),
+				},
+				time.Unix(0, 0),
+			)},
+		},
+		{
+			name: "ESC Status",
+			input: &frame.V2Frame{
+				SequenceNumber: 1,
+				SystemID:       1,
+				ComponentID:    1,
+				Message: &common.MessageEscStatus{
+					Index:    uint8(0),
+					TimeUsec: uint64(12345),
+					Rpm:      [4]int32{0, 1, 2, 3},
+					Voltage:  [4]float32{10.0, 11.0, 12.0, 13.0},
+					Current:  [4]float32{14.0, 15.0, 16.0, 17.0},
+				},
+				Checksum: 0,
+			},
+			expected: []telegraf.Metric{metric.New(
+				"esc_status",
+				map[string]string{
+					"sys_id": "1",
+					"source": "udpserver://",
+				},
+				map[string]interface{}{
+					"index":     uint8(0),
+					"time_usec": uint64(12345),
+					"current_1": float32(14.0),
+					"current_2": float32(15.0),
+					"current_3": float32(16.0),
+					"current_4": float32(17.0),
+					"rpm_1":     int32(0),
+					"rpm_2":     int32(1),
+					"rpm_3":     int32(2),
+					"rpm_4":     int32(3),
+					"voltage_1": float32(10.0),
+					"voltage_2": float32(11.0),
+					"voltage_3": float32(12.0),
+					"voltage_4": float32(13.0),
+				},
+				time.Unix(0, 0),
+			)},
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			// Setup the plugin
+			plugin := &Mavlink{URL: "udpserver://"}
+			require.NoError(t, plugin.Init())
+			acc := testutil.Accumulator{}
+
+			plugin.handleFrame(&acc, tt.input)
+
+			// Check that accumulator contains the metric
+			testutil.RequireMetricsStructureEqual(t, tt.expected, acc.GetTelegrafMetrics(), testutil.IgnoreTime())
+		})
+	}
+}
+
+// Test that the plugin can interface with a real ArduPilot container.
+func TestArduPilotIntegration(t *testing.T) {
+	if testing.Short() {
+		t.Skip("Skipping integration test in short mode")
+	}
+
+	// Start the docker container for ArduPilot
+	container := testutil.Container{
+		Image:        "radarku/ardupilot-sitl",
+		ExposedPorts: []string{"5760"},
+		WaitingFor: wait.ForAll(
+			wait.ForListeningPort(nat.Port("5760")),
+		),
+	}
+	require.NoError(t, container.Start(), "failed to start ardupilot container")
+	defer container.Terminate()
+
+	// Setup the plugin
+	plugin := Mavlink{
+		URL:                    "tcp://127.0.0.1:" + container.Ports["5760"],
+		SystemID:               254,
+		StreamRequestFrequency: 4,
+		Log:                    testutil.Logger{},
+	}
+	require.NoError(t, plugin.Init())
+
+	// Collect the metrics and compare
+	var acc testutil.Accumulator
+	require.NoError(t, plugin.Start(&acc))
+	defer plugin.Stop()
+
+	// Expect to have received more than 10 metrics. The exact metrics received
+	// is non-deterministic because ArduPilot's startup may vary between runs,
+	// but should be on the order of 100 metrics in 5 seconds.
+	// Content of metrics is not tested here as we don't know what to expect.
+	require.Eventually(t, func() bool {
+		return acc.NMetrics() >= 10
+	}, 10*time.Second, 100*time.Millisecond, "less than 10 metrics received")
+}
diff --git a/plugins/inputs/mavlink/sample.conf b/plugins/inputs/mavlink/sample.conf
new file mode 100644
index 0000000000000..f3a2849209279
--- /dev/null
+++ b/plugins/inputs/mavlink/sample.conf
@@ -0,0 +1,32 @@
+# Read metrics from a Mavlink flight controller.
+[[inputs.mavlink]]
+  ## Flight controller URL supporting serial port, UDP and TCP connections.
+  ## Options are documented at
+  ##   https://mavsdk.mavlink.io/v1.4/en/cpp/guide/connections.html.
+  ##
+  ## Examples:
+  ## - Serial port: serial:///dev/ttyACM0:57600
+  ## - TCP client:  tcp://192.168.1.12:5760
+  ## - UDP client:  udp://192.168.1.12:14550
+  ## - TCP server:  tcpserver://:5760
+  ## - UDP server:  udpserver://:14550
+  # url = "tcp://127.0.0.1:5760"
+
+  ## Filter to specific messages. Only the messages in this list will be parsed.
+  ## If blank or unset, all messages will be accepted. Glob syntax is accepted.
+  ## Each message in this list should be lowercase camel_case, with "message_"
+  ## prefix removed, eg: "global_position_int", "attitude"
+  # filter = []
+
+  ## Mavlink system ID for Telegraf. Only used if the mavlink plugin is sending 
+  ## messages, eg. when `stream_request_frequency` is 0 (see below.)
+  # system_id = 254
+
+  ## Determines whether the plugin sends requests to subscribe to data.
+  ## In mavlink, stream rates must be configured before data is received.
+  ## This config item sets the rate in Hz, with 0 disabling the request.
+  ## 
+  ## This frequency should be set to 0 if your software already controls the 
+  ## rates using REQUEST_DATA_STREAM or MAV_CMD_SET_MESSAGE_INTERVAL
+  ## (See https://mavlink.io/en/mavgen_python/howto_requestmessages.html)
+  # stream_request_frequency = 4


Reviews:
user: login: srebhan
body: Thanks for the update @chrisdalke! Some smaller comments and suggestions...
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-23T14:21:30Z
user: login: chrisdalke
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-23T20:54:58Z
user: login: chrisdalke
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-23T20:59:42Z
user: login: chrisdalke
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-23T21:00:31Z
user: login: chrisdalke
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-23T21:19:04Z
user: login: Hipska
state: CHANGES_REQUESTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-24T08:16:56Z
user: login: chrisdalke
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-24T13:48:17Z
user: login: chrisdalke
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-24T13:50:37Z
user: login: chrisdalke
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-24T13:52:28Z
user: login: chrisdalke
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-24T15:22:06Z
user: login: chrisdalke
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-24T15:27:41Z
user: login: chrisdalke
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-24T15:36:01Z
user: login: Hipska
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-24T15:46:21Z
user: login: Hipska
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-24T15:47:34Z
user: login: Hipska
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-24T15:54:18Z
user: login: chrisdalke
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-24T16:09:32Z
user: login: srebhan
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-05T11:43:29Z
user: login: srebhan
body: @chrisdalke thanks for the update! I have one suggestion to move the whole metric handling into `convertFrameToMetric` as this simplifies the code IMO. Other than that, please bring back the "large amout of metrics" warning and check the remaining comments... We are almost there. 
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-05T12:36:31Z
user: login: chrisdalke
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-05T14:08:20Z
user: login: srebhan
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-05T14:18:27Z
user: login: chrisdalke
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-05T15:45:11Z
user: login: srebhan
body: @chrisdalke awesome. Just some more formatting things and we are good from my side...
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-07T08:33:26Z
user: login: srebhan
body: Thanks a lot @chrisdalke for this nice plugin!
state: APPROVED
author_association: MEMBER
submitted_at: 2025-05-08T13:47:29Z
user: login: Hipska
state: CHANGES_REQUESTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-08T14:01:47Z
user: login: Hipska
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-08T17:26:27Z
user: login: skartikey
body: @chrisdalke Great work!
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-09T21:18:50Z

Review comments:
diff_hunk: @@ -0,0 +1,99 @@
+# MavLink Input Plugin
+
+This plugin collects metrics from [MavLink][mavlink]-compatible flight
+controllers such as [ArduPilot][ardupilot] or [PX4][px4] to live ingest
+flight metrics from unmanned systems (drones, planes, boats, etc.)
+Currently the ArduPilot-specific Mavlink dialect is used, check the
+[Mavlink documentation][mavlink_docs] for more details and the various
+messages available.
+
+> [!WARNING]
+> This plugin potentially generates a large amount of data! Please use metric
+> filters to limit the amount of metrics produced!
+
+⭐ Telegraf v1.34.0
path: plugins/inputs/mavlink/README.md
user: login: srebhan
body: ```suggestion
⭐ Telegraf v1.35.0
```
created_at: 2025-04-23T13:36:30Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,37 @@
+# Read metrics from a Mavlink flight controller.
+[[inputs.mavlink]]
+  ## Flight controller URL supporting serial port, UDP and TCP connections.
+  ## Options are documented at
+  ##   https://mavsdk.mavlink.io/v1.4/en/cpp/guide/connections.html.
+  ##
+  ## Examples:
+  ## - Serial port: serial:///dev/ttyACM0:57600
+  ## - TCP client:  tcp://192.168.1.12:14550
+  ## - UDP client:  udp://192.168.1.12:14550
+  ## - UDP server:  udp://:14540
+  ##
+  ## The default is `tcp://127.0.0.1:5760`.
+  # url = "tcp://127.0.0.1:5760"
path: plugins/inputs/mavlink/sample.conf
user: login: srebhan
body: Please don't mention defaults as that's what should be in the commented setting
```suggestion
  ##
  # url = "tcp://127.0.0.1:5760"
```

Same for all other options below.
created_at: 2025-04-23T13:37:49Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,251 @@
+//go:generate ../../../tools/readme_config_includer/generator
+package mavlink
+
+import (
+	"context"
+	_ "embed"
+	"fmt"
+	"net"
+	"net/url"
+	"reflect"
+	"strconv"
+	"strings"
+	"sync"
+	"time"
+
+	"github.com/bluenviron/gomavlib/v3"
+	"github.com/bluenviron/gomavlib/v3/pkg/dialects/ardupilotmega"
+	"github.com/bluenviron/gomavlib/v3/pkg/frame"
+
+	"github.com/influxdata/telegraf"
+	"github.com/influxdata/telegraf/filter"
+	"github.com/influxdata/telegraf/internal"
+	"github.com/influxdata/telegraf/metric"
+	"github.com/influxdata/telegraf/plugins/inputs"
+)
+
+type Mavlink struct {
+	URL                    string   `toml:"url"`
+	SystemID               uint8    `toml:"system_id"`
+	FilterPattern          []string `toml:"filter"`
+	StreamRequestDisable   bool     `toml:"stream_request_disable"`
+	StreamRequestFrequency uint16   `toml:"stream_request_frequency"`
path: plugins/inputs/mavlink/mavlink.go
user: login: srebhan
body: Would it be possible to infer the `stream_request_disable` setting from `stream_request_frequency` being positive for requests to be enabled and zero for requests being disabled?
created_at: 2025-04-23T13:40:40Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,251 @@
+//go:generate ../../../tools/readme_config_includer/generator
+package mavlink
+
+import (
+	"context"
+	_ "embed"
+	"fmt"
+	"net"
+	"net/url"
+	"reflect"
+	"strconv"
+	"strings"
+	"sync"
+	"time"
+
+	"github.com/bluenviron/gomavlib/v3"
+	"github.com/bluenviron/gomavlib/v3/pkg/dialects/ardupilotmega"
+	"github.com/bluenviron/gomavlib/v3/pkg/frame"
+
+	"github.com/influxdata/telegraf"
+	"github.com/influxdata/telegraf/filter"
+	"github.com/influxdata/telegraf/internal"
+	"github.com/influxdata/telegraf/metric"
+	"github.com/influxdata/telegraf/plugins/inputs"
+)
+
+type Mavlink struct {
+	URL                    string   `toml:"url"`
+	SystemID               uint8    `toml:"system_id"`
+	FilterPattern          []string `toml:"filter"`
+	StreamRequestDisable   bool     `toml:"stream_request_disable"`
+	StreamRequestFrequency uint16   `toml:"stream_request_frequency"`
+
+	Log telegraf.Logger `toml:"-"`
+
+	filter         filter.Filter
+	connection     *gomavlib.Node
+	endpointConfig []gomavlib.EndpointConf
+	cancel         context.CancelFunc
+	wg             sync.WaitGroup
+}
+
+//go:embed sample.conf
+var sampleConfig string
+
+func (*Mavlink) SampleConfig() string {
+	return sampleConfig
+}
+
+func (m *Mavlink) Init() error {
+	// Set default values
+	if len(m.URL) == 0 {
+		m.URL = "tcp://127.0.0.1:5760"
+	}
+
+	if !m.StreamRequestDisable && m.StreamRequestFrequency == 0 {
+		m.StreamRequestFrequency = 4
+	}
+
+	// Parse out the Mavlink endpoint.
+	// Try to parse the URL
+	u, err := url.Parse(m.URL)
+	if err != nil {
+		return fmt.Errorf("invalid url: %w", err)
+	}
+
+	switch u.Scheme {
+	case "serial":
+		// Serial client
+		// Parse serial URL by hand, because it is not a compliant URL.
+		baudRate := 57600
+		device, rate, found := strings.Cut(strings.TrimPrefix(m.URL, "serial://"), ":")
+		if found {
+			r, err := strconv.Atoi(rate)
+			if err != nil {
+				return fmt.Errorf("serial baud rate not valid: %w", err)
+			}
+			baudRate = r
+		}
+
+		m.endpointConfig = []gomavlib.EndpointConf{
+			gomavlib.EndpointSerial{
+				Device: device,
+				Baud:   baudRate,
+			},
+		}
+	case "tcp":
+		// 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 mavlink TCP port if a port was not provided or was invalid.
+			host = u.Host
+			port = "14540"
+		}
+
+		if len(host) > 0 {
+			m.endpointConfig = []gomavlib.EndpointConf{
+				gomavlib.EndpointTCPClient{
+					Address: host + ":" + port,
+				},
+			}
+		} else {
+			m.endpointConfig = []gomavlib.EndpointConf{
+				gomavlib.EndpointTCPServer{
+					Address: ":" + port,
+				},
+			}
+		}
+
+	case "udp":
+		// 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 mavlink TCP port if a port was not provided or was invalid.
+			host = u.Host
+			port = "5760"
+		}
+
+		if len(host) > 0 {
+			m.endpointConfig = []gomavlib.EndpointConf{
+				gomavlib.EndpointUDPClient{
+					Address: host + ":" + port,
+				},
+			}
+		} else {
+			m.endpointConfig = []gomavlib.EndpointConf{
+				gomavlib.EndpointUDPServer{
+					Address: ":" + port,
+				},
+			}
+		}
+
+	default:
+		return fmt.Errorf("unknown scheme %q", u.Scheme)
+	}
+
+	// Compile filter
+	m.filter, err = filter.Compile(m.FilterPattern)
+	if err != nil {
+		return fmt.Errorf("compiling filter failed: %w", err)
+	}
+
+	return nil
+}
+
+// Convert a Mavlink frame into a struct containing Metric data.
+func convertFrameToMetric(frm frame.Frame, msgFilter filter.Filter) telegraf.Metric {
+	m := frm.GetMessage()
+	t := reflect.TypeOf(m)
+	v := reflect.ValueOf(m)
+	if t.Kind() == reflect.Ptr {
+		t = t.Elem()
+		v = v.Elem()
+	}
+
+	name := internal.SnakeCase(strings.TrimPrefix(t.Name(), "Message"))
+
+	if msgFilter != nil && !msgFilter.Match(name) {
+		return nil
+	}
+
+	tags := map[string]string{
+		"sys_id": strconv.FormatUint(uint64(frm.GetSystemID()), 10),
+	}
+	fields := make(map[string]interface{}, t.NumField())
+
+	for i := 0; i < t.NumField(); i++ {
+		field := t.Field(i)
+		value := v.Field(i)
+		fieldName := internal.SnakeCase(field.Name)
+
+		if value.Kind() == reflect.Slice || value.Kind() == reflect.Array {
+			// Split array types into individual primitive values
+			// with _<n> appended to the key
+			for j := 0; j < value.Len(); j++ {
+				indexedFieldName := fmt.Sprintf("%s_%d", fieldName, j+1)
+				fields[indexedFieldName] = value.Index(j).Interface()
+			}
+		} else {
+			fields[fieldName] = value.Interface()
+		}
+	}
+
+	return metric.New(name, tags, fields, time.Now())
+}
path: plugins/inputs/mavlink/mavlink.go
user: login: srebhan
body: Please move this down below all exported (plugin interface) functions but before `init` (lowercase)!
created_at: 2025-04-23T13:44:42Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,251 @@
+//go:generate ../../../tools/readme_config_includer/generator
+package mavlink
+
+import (
+	"context"
+	_ "embed"
+	"fmt"
+	"net"
+	"net/url"
+	"reflect"
+	"strconv"
+	"strings"
+	"sync"
+	"time"
+
+	"github.com/bluenviron/gomavlib/v3"
+	"github.com/bluenviron/gomavlib/v3/pkg/dialects/ardupilotmega"
+	"github.com/bluenviron/gomavlib/v3/pkg/frame"
+
+	"github.com/influxdata/telegraf"
+	"github.com/influxdata/telegraf/filter"
+	"github.com/influxdata/telegraf/internal"
+	"github.com/influxdata/telegraf/metric"
+	"github.com/influxdata/telegraf/plugins/inputs"
+)
+
+type Mavlink struct {
+	URL                    string   `toml:"url"`
+	SystemID               uint8    `toml:"system_id"`
+	FilterPattern          []string `toml:"filter"`
+	StreamRequestDisable   bool     `toml:"stream_request_disable"`
+	StreamRequestFrequency uint16   `toml:"stream_request_frequency"`
+
+	Log telegraf.Logger `toml:"-"`
+
+	filter         filter.Filter
+	connection     *gomavlib.Node
+	endpointConfig []gomavlib.EndpointConf
+	cancel         context.CancelFunc
+	wg             sync.WaitGroup
+}
+
+//go:embed sample.conf
+var sampleConfig string
+
+func (*Mavlink) SampleConfig() string {
+	return sampleConfig
+}
+
+func (m *Mavlink) Init() error {
+	// Set default values
+	if len(m.URL) == 0 {
+		m.URL = "tcp://127.0.0.1:5760"
+	}
+
+	if !m.StreamRequestDisable && m.StreamRequestFrequency == 0 {
+		m.StreamRequestFrequency = 4
+	}
+
+	// Parse out the Mavlink endpoint.
+	// Try to parse the URL
+	u, err := url.Parse(m.URL)
+	if err != nil {
+		return fmt.Errorf("invalid url: %w", err)
+	}
+
+	switch u.Scheme {
+	case "serial":
+		// Serial client
+		// Parse serial URL by hand, because it is not a compliant URL.
+		baudRate := 57600
+		device, rate, found := strings.Cut(strings.TrimPrefix(m.URL, "serial://"), ":")
+		if found {
+			r, err := strconv.Atoi(rate)
+			if err != nil {
+				return fmt.Errorf("serial baud rate not valid: %w", err)
+			}
+			baudRate = r
+		}
+
+		m.endpointConfig = []gomavlib.EndpointConf{
+			gomavlib.EndpointSerial{
+				Device: device,
+				Baud:   baudRate,
+			},
+		}
+	case "tcp":
+		// 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 mavlink TCP port if a port was not provided or was invalid.
+			host = u.Host
+			port = "14540"
+		}
+
+		if len(host) > 0 {
+			m.endpointConfig = []gomavlib.EndpointConf{
+				gomavlib.EndpointTCPClient{
+					Address: host + ":" + port,
+				},
+			}
+		} else {
+			m.endpointConfig = []gomavlib.EndpointConf{
+				gomavlib.EndpointTCPServer{
+					Address: ":" + port,
+				},
+			}
+		}
+
+	case "udp":
+		// 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 mavlink TCP port if a port was not provided or was invalid.
+			host = u.Host
+			port = "5760"
+		}
+
+		if len(host) > 0 {
+			m.endpointConfig = []gomavlib.EndpointConf{
+				gomavlib.EndpointUDPClient{
+					Address: host + ":" + port,
+				},
+			}
+		} else {
+			m.endpointConfig = []gomavlib.EndpointConf{
+				gomavlib.EndpointUDPServer{
+					Address: ":" + port,
+				},
+			}
+		}
+
+	default:
+		return fmt.Errorf("unknown scheme %q", u.Scheme)
+	}
+
+	// Compile filter
+	m.filter, err = filter.Compile(m.FilterPattern)
+	if err != nil {
+		return fmt.Errorf("compiling filter failed: %w", err)
+	}
+
+	return nil
+}
+
+// Convert a Mavlink frame into a struct containing Metric data.
+func convertFrameToMetric(frm frame.Frame, msgFilter filter.Filter) telegraf.Metric {
+	m := frm.GetMessage()
+	t := reflect.TypeOf(m)
+	v := reflect.ValueOf(m)
+	if t.Kind() == reflect.Ptr {
+		t = t.Elem()
+		v = v.Elem()
+	}
+
+	name := internal.SnakeCase(strings.TrimPrefix(t.Name(), "Message"))
+
+	if msgFilter != nil && !msgFilter.Match(name) {
+		return nil
+	}
+
+	tags := map[string]string{
+		"sys_id": strconv.FormatUint(uint64(frm.GetSystemID()), 10),
+	}
+	fields := make(map[string]interface{}, t.NumField())
+
+	for i := 0; i < t.NumField(); i++ {
+		field := t.Field(i)
+		value := v.Field(i)
+		fieldName := internal.SnakeCase(field.Name)
+
+		if value.Kind() == reflect.Slice || value.Kind() == reflect.Array {
+			// Split array types into individual primitive values
+			// with _<n> appended to the key
+			for j := 0; j < value.Len(); j++ {
+				indexedFieldName := fmt.Sprintf("%s_%d", fieldName, j+1)
+				fields[indexedFieldName] = value.Index(j).Interface()
+			}
+		} else {
+			fields[fieldName] = value.Interface()
+		}
+	}
+
+	return metric.New(name, tags, fields, time.Now())
+}
+
+func (m *Mavlink) Start(acc telegraf.Accumulator) error {
+	// Start MAVLink endpoint
+	connection, err := gomavlib.NewNode(gomavlib.NodeConf{
+		Endpoints:              m.endpointConfig,
+		Dialect:                ardupilotmega.Dialect,
+		OutVersion:             gomavlib.V2,
+		OutSystemID:            m.SystemID,
+		StreamRequestEnable:    !m.StreamRequestDisable,
+		StreamRequestFrequency: int(m.StreamRequestFrequency),
+	})
+	if err != nil {
+		return &internal.StartupError{
+			Err:   fmt.Errorf("connecting to mavlink endpoint %s failed: %w", m.URL, err),
+			Retry: true,
+		}
+	}
+	m.connection = connection
+	ctx, cancelFunc := context.WithCancel(context.Background())
+	m.cancel = cancelFunc
+
+	// Start routine to connect to Mavlink and stream out data async
+	m.wg.Add(1)
+	go func(ctx context.Context) {
+		defer m.connection.Close()
+		defer m.wg.Done()
+
+		for {
+			select {
+			case <-ctx.Done():
+				return
+			case evt := <-m.connection.Events():
+				switch evt := evt.(type) {
+				case *gomavlib.EventFrame:
+					if result := convertFrameToMetric(evt.Frame, m.filter); result != nil {
+						result.AddTag("source", m.URL)
+						acc.AddMetric(result)
+					}
+				case *gomavlib.EventChannelOpen:
+					m.Log.Debugf("Mavlink channel opened")
+				case *gomavlib.EventChannelClose:
+					m.Log.Debugf("Mavlink channel closed")
path: plugins/inputs/mavlink/mavlink.go
user: login: srebhan
body: Please make these trace level messages!
```suggestion
					m.Log.Tracef("Mavlink channel opened")
				case *gomavlib.EventChannelClose:
					m.Log.Tracef("Mavlink channel closed")
```
created_at: 2025-04-23T13:47:57Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,251 @@
+//go:generate ../../../tools/readme_config_includer/generator
+package mavlink
+
+import (
+	"context"
+	_ "embed"
+	"fmt"
+	"net"
+	"net/url"
+	"reflect"
+	"strconv"
+	"strings"
+	"sync"
+	"time"
+
+	"github.com/bluenviron/gomavlib/v3"
+	"github.com/bluenviron/gomavlib/v3/pkg/dialects/ardupilotmega"
+	"github.com/bluenviron/gomavlib/v3/pkg/frame"
+
+	"github.com/influxdata/telegraf"
+	"github.com/influxdata/telegraf/filter"
+	"github.com/influxdata/telegraf/internal"
+	"github.com/influxdata/telegraf/metric"
+	"github.com/influxdata/telegraf/plugins/inputs"
+)
+
+type Mavlink struct {
+	URL                    string   `toml:"url"`
+	SystemID               uint8    `toml:"system_id"`
+	FilterPattern          []string `toml:"filter"`
+	StreamRequestDisable   bool     `toml:"stream_request_disable"`
+	StreamRequestFrequency uint16   `toml:"stream_request_frequency"`
+
+	Log telegraf.Logger `toml:"-"`
+
+	filter         filter.Filter
+	connection     *gomavlib.Node
+	endpointConfig []gomavlib.EndpointConf
+	cancel         context.CancelFunc
+	wg             sync.WaitGroup
+}
+
+//go:embed sample.conf
+var sampleConfig string
+
+func (*Mavlink) SampleConfig() string {
+	return sampleConfig
+}
+
+func (m *Mavlink) Init() error {
+	// Set default values
+	if len(m.URL) == 0 {
+		m.URL = "tcp://127.0.0.1:5760"
+	}
+
+	if !m.StreamRequestDisable && m.StreamRequestFrequency == 0 {
+		m.StreamRequestFrequency = 4
+	}
+
+	// Parse out the Mavlink endpoint.
+	// Try to parse the URL
+	u, err := url.Parse(m.URL)
+	if err != nil {
+		return fmt.Errorf("invalid url: %w", err)
+	}
+
+	switch u.Scheme {
+	case "serial":
+		// Serial client
+		// Parse serial URL by hand, because it is not a compliant URL.
+		baudRate := 57600
+		device, rate, found := strings.Cut(strings.TrimPrefix(m.URL, "serial://"), ":")
+		if found {
+			r, err := strconv.Atoi(rate)
+			if err != nil {
+				return fmt.Errorf("serial baud rate not valid: %w", err)
+			}
+			baudRate = r
+		}
+
+		m.endpointConfig = []gomavlib.EndpointConf{
+			gomavlib.EndpointSerial{
+				Device: device,
+				Baud:   baudRate,
+			},
+		}
+	case "tcp":
+		// 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 mavlink TCP port if a port was not provided or was invalid.
+			host = u.Host
+			port = "14540"
+		}
+
+		if len(host) > 0 {
+			m.endpointConfig = []gomavlib.EndpointConf{
+				gomavlib.EndpointTCPClient{
+					Address: host + ":" + port,
+				},
+			}
+		} else {
+			m.endpointConfig = []gomavlib.EndpointConf{
+				gomavlib.EndpointTCPServer{
+					Address: ":" + port,
+				},
+			}
+		}
+
+	case "udp":
+		// 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 mavlink TCP port if a port was not provided or was invalid.
+			host = u.Host
+			port = "5760"
+		}
+
+		if len(host) > 0 {
+			m.endpointConfig = []gomavlib.EndpointConf{
+				gomavlib.EndpointUDPClient{
+					Address: host + ":" + port,
+				},
+			}
+		} else {
+			m.endpointConfig = []gomavlib.EndpointConf{
+				gomavlib.EndpointUDPServer{
+					Address: ":" + port,
+				},
+			}
+		}
+
+	default:
+		return fmt.Errorf("unknown scheme %q", u.Scheme)
+	}
+
+	// Compile filter
+	m.filter, err = filter.Compile(m.FilterPattern)
+	if err != nil {
+		return fmt.Errorf("compiling filter failed: %w", err)
+	}
+
+	return nil
+}
+
+// Convert a Mavlink frame into a struct containing Metric data.
+func convertFrameToMetric(frm frame.Frame, msgFilter filter.Filter) telegraf.Metric {
+	m := frm.GetMessage()
+	t := reflect.TypeOf(m)
+	v := reflect.ValueOf(m)
+	if t.Kind() == reflect.Ptr {
+		t = t.Elem()
+		v = v.Elem()
+	}
+
+	name := internal.SnakeCase(strings.TrimPrefix(t.Name(), "Message"))
+
+	if msgFilter != nil && !msgFilter.Match(name) {
+		return nil
+	}
+
+	tags := map[string]string{
+		"sys_id": strconv.FormatUint(uint64(frm.GetSystemID()), 10),
+	}
+	fields := make(map[string]interface{}, t.NumField())
+
+	for i := 0; i < t.NumField(); i++ {
+		field := t.Field(i)
+		value := v.Field(i)
+		fieldName := internal.SnakeCase(field.Name)
+
+		if value.Kind() == reflect.Slice || value.Kind() == reflect.Array {
+			// Split array types into individual primitive values
+			// with _<n> appended to the key
+			for j := 0; j < value.Len(); j++ {
+				indexedFieldName := fmt.Sprintf("%s_%d", fieldName, j+1)
+				fields[indexedFieldName] = value.Index(j).Interface()
+			}
+		} else {
+			fields[fieldName] = value.Interface()
+		}
+	}
+
+	return metric.New(name, tags, fields, time.Now())
+}
+
+func (m *Mavlink) Start(acc telegraf.Accumulator) error {
+	// Start MAVLink endpoint
+	connection, err := gomavlib.NewNode(gomavlib.NodeConf{
+		Endpoints:              m.endpointConfig,
+		Dialect:                ardupilotmega.Dialect,
+		OutVersion:             gomavlib.V2,
+		OutSystemID:            m.SystemID,
+		StreamRequestEnable:    !m.StreamRequestDisable,
+		StreamRequestFrequency: int(m.StreamRequestFrequency),
+	})
+	if err != nil {
+		return &internal.StartupError{
+			Err:   fmt.Errorf("connecting to mavlink endpoint %s failed: %w", m.URL, err),
+			Retry: true,
+		}
+	}
+	m.connection = connection
+	ctx, cancelFunc := context.WithCancel(context.Background())
+	m.cancel = cancelFunc
+
+	// Start routine to connect to Mavlink and stream out data async
+	m.wg.Add(1)
+	go func(ctx context.Context) {
+		defer m.connection.Close()
+		defer m.wg.Done()
+
+		for {
+			select {
+			case <-ctx.Done():
+				return
+			case evt := <-m.connection.Events():
+				switch evt := evt.(type) {
path: plugins/inputs/mavlink/mavlink.go
user: login: srebhan
body: Should you also log other (unhandled) types using trace messages to detect new event type implementations that need to be handled?
created_at: 2025-04-23T13:48:55Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,268 @@
+package mavlink
+
+import (
+	"testing"
+	"time"
+
+	"github.com/bluenviron/gomavlib/v3"
+	"github.com/bluenviron/gomavlib/v3/pkg/dialects/common"
+	"github.com/bluenviron/gomavlib/v3/pkg/frame"
+	"github.com/docker/go-connections/nat"
+	"github.com/stretchr/testify/require"
+	"github.com/testcontainers/testcontainers-go/wait"
+
+	"github.com/influxdata/telegraf"
+	"github.com/influxdata/telegraf/filter"
+	"github.com/influxdata/telegraf/metric"
+	"github.com/influxdata/telegraf/testutil"
+)
+
+// Test that a serial port URL can be parsed (Linux)
+func TestParseSerialUrlLinux(t *testing.T) {
+	plugin := Mavlink{
+		URL: "serial:///dev/ttyACM0:115200",
+	}
+
+	err := plugin.Init()
+	require.NoError(t, err)
path: plugins/inputs/mavlink/mavlink_test.go
user: login: srebhan
body: Please be brief. Same for other instances below.
```suggestion
	require.NoError(t, plugin.Init())
```
created_at: 2025-04-23T13:49:30Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,268 @@
+package mavlink
+
+import (
+	"testing"
+	"time"
+
+	"github.com/bluenviron/gomavlib/v3"
+	"github.com/bluenviron/gomavlib/v3/pkg/dialects/common"
+	"github.com/bluenviron/gomavlib/v3/pkg/frame"
+	"github.com/docker/go-connections/nat"
+	"github.com/stretchr/testify/require"
+	"github.com/testcontainers/testcontainers-go/wait"
+
+	"github.com/influxdata/telegraf"
+	"github.com/influxdata/telegraf/filter"
+	"github.com/influxdata/telegraf/metric"
+	"github.com/influxdata/telegraf/testutil"
+)
+
+// Test that a serial port URL can be parsed (Linux)
+func TestParseSerialUrlLinux(t *testing.T) {
+	plugin := Mavlink{
+		URL: "serial:///dev/ttyACM0:115200",
+	}
+
+	err := plugin.Init()
+	require.NoError(t, err)
+
+	endpoint, ok := plugin.endpointConfig[0].(gomavlib.EndpointSerial)
+	require.True(t, ok)
path: plugins/inputs/mavlink/mavlink_test.go
user: login: srebhan
body: We need to make sure to not panic... Same for other instances below!
```suggestion
	require.Len(t, plugin.endpointConfig, 1)
	endpoint, ok := plugin.endpointConfig[0].(gomavlib.EndpointSerial)
	require.True(t, ok)
```
created_at: 2025-04-23T13:50:45Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,268 @@
+package mavlink
+
+import (
+	"testing"
+	"time"
+
+	"github.com/bluenviron/gomavlib/v3"
+	"github.com/bluenviron/gomavlib/v3/pkg/dialects/common"
+	"github.com/bluenviron/gomavlib/v3/pkg/frame"
+	"github.com/docker/go-connections/nat"
+	"github.com/stretchr/testify/require"
+	"github.com/testcontainers/testcontainers-go/wait"
+
+	"github.com/influxdata/telegraf"
+	"github.com/influxdata/telegraf/filter"
+	"github.com/influxdata/telegraf/metric"
+	"github.com/influxdata/telegraf/testutil"
+)
+
+// Test that a serial port URL can be parsed (Linux)
+func TestParseSerialUrlLinux(t *testing.T) {
+	plugin := Mavlink{
+		URL: "serial:///dev/ttyACM0:115200",
+	}
+
+	err := plugin.Init()
+	require.NoError(t, err)
+
+	endpoint, ok := plugin.endpointConfig[0].(gomavlib.EndpointSerial)
+	require.True(t, ok)
+	require.Equal(t, "/dev/ttyACM0", endpoint.Device)
+	require.Equal(t, 115200, endpoint.Baud)
+}
+
+// Test that a serial port URL can be parsed (Windows)
+func TestParseSerialUrlWindows(t *testing.T) {
+	plugin := Mavlink{
+		URL: "serial://COM1:115200",
+	}
+
+	err := plugin.Init()
+	require.NoError(t, err)
+
+	endpoint, ok := plugin.endpointConfig[0].(gomavlib.EndpointSerial)
+	require.True(t, ok)
+	require.Equal(t, "COM1", endpoint.Device)
+	require.Equal(t, 115200, endpoint.Baud)
+}
+
+// Test that a UDP client URL can be parsed.
+func TestParseUDPClientUrl(t *testing.T) {
+	plugin := Mavlink{
+		URL: "udp://192.168.1.12:14550",
+	}
+
+	err := plugin.Init()
+	require.NoError(t, err)
+
+	endpoint, ok := plugin.endpointConfig[0].(gomavlib.EndpointUDPClient)
+	require.True(t, ok)
+	require.Equal(t, "192.168.1.12:14550", endpoint.Address)
+}
+
+// Test that a UDP server URL can be parsed.
+func TestParseUDPServerUrl(t *testing.T) {
+	plugin := Mavlink{
+		URL: "udp://:14540",
+	}
+
+	err := plugin.Init()
+	require.NoError(t, err)
+
+	endpoint, ok := plugin.endpointConfig[0].(gomavlib.EndpointUDPServer)
+	require.True(t, ok)
+	require.Equal(t, ":14540", endpoint.Address)
+}
+
+// Test that a TCP client URL can be parsed.
+func TestParseTCPClientUrl(t *testing.T) {
+	plugin := Mavlink{
+		URL: "tcp://192.168.1.12:14550",
+	}
+
+	err := plugin.Init()
+	require.NoError(t, err)
+
+	endpoint, ok := plugin.endpointConfig[0].(gomavlib.EndpointTCPClient)
+	require.True(t, ok)
+	require.Equal(t, "192.168.1.12:14550", endpoint.Address)
+}
path: plugins/inputs/mavlink/mavlink_test.go
user: login: srebhan
body: All of those should be a table-based test:
```suggestion
func TestParseURL(t *testing.T) {
	tests := []struct {
		name     string
		url      string
		expected gomavlib.EndpointConf
	}{
		{
			name: "serial Linux",
			url:  "serial:///dev/ttyACM0:115200",
			expected: gomavlib.EndpointSerial{
				Device: "/dev/ttyACM0",
				Baud:   115200,
			},
		},
		{
			name: "serial Linux with default baudrate",
			url:  "serial:///dev/ttyACM0",
			expected: gomavlib.EndpointSerial{
				Device: "/dev/ttyACM0",
				Baud:   57600,
			},
		},
		{
			name: "serial Windows",
			url:  "serial://COM1:115200",
			expected: gomavlib.EndpointSerial{
				Device: "COM1",
				Baud:   115200,
			},
		},
		{
			name: "serial Windows with default baudrate",
			url:  "serial://COM1",
			expected: gomavlib.EndpointSerial{
				Device: "COM1",
				Baud:   57600,
			},
		},
		{
			name: "UDP client",
			url:  "udp://192.168.1.12:14550",
			expected: gomavlib.EndpointUDPClient{
				Address: "192.168.1.12:14550",
			},
		},
		{
			name: "UDP client with default port",
			url:  "udp://192.168.1.12",
			expected: gomavlib.EndpointUDPClient{
				Address: "192.168.1.12:5760",
			},
		},
		{
			name: "UDP server",
			url:  "udp://:14550",
			expected: gomavlib.EndpointUDPServer{
				Address: ":14550",
			},
		},
		{
			name: "UDP server with default port",
			url:  "udp://",
			expected: gomavlib.EndpointUDPServer{
				Address: ":5760",
			},
		},
		{
			name: "TCP client",
			url:  "tcp://192.168.1.12:14550",
			expected: gomavlib.EndpointTCPClient{
				Address: "192.168.1.12:14550",
			},
		},
		{
			name: "TCP client with default port",
			url:  "tcp://192.168.1.12",
			expected: gomavlib.EndpointTCPClient{
				Address: "192.168.1.12:14540",
			},
		},
		{
			name: "TCP server",
			url:  "tcp://:14550",
			expected: gomavlib.EndpointTCPServer{
				Address: ":14550",
			},
		},
		{
			name: "TCP server with default port",
			url:  "tcp://",
			expected: gomavlib.EndpointTCPServer{
				Address: ":14540",
			},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			// Setup the plugin
			plugin := &Mavlink{URL: tt.url}
			require.NoError(t, plugin.Init())

			// Check the resulting endpoint configuration
			require.Len(t, plugin.endpointConfig, 1)
			require.EqualValues(t, tt.expected, plugin.endpointConfig[0])
		})
	}
}

```
created_at: 2025-04-23T14:10:25Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,251 @@
+//go:generate ../../../tools/readme_config_includer/generator
+package mavlink
+
+import (
+	"context"
+	_ "embed"
+	"fmt"
+	"net"
+	"net/url"
+	"reflect"
+	"strconv"
+	"strings"
+	"sync"
+	"time"
+
+	"github.com/bluenviron/gomavlib/v3"
+	"github.com/bluenviron/gomavlib/v3/pkg/dialects/ardupilotmega"
+	"github.com/bluenviron/gomavlib/v3/pkg/frame"
+
+	"github.com/influxdata/telegraf"
+	"github.com/influxdata/telegraf/filter"
+	"github.com/influxdata/telegraf/internal"
+	"github.com/influxdata/telegraf/metric"
+	"github.com/influxdata/telegraf/plugins/inputs"
+)
+
+type Mavlink struct {
+	URL                    string   `toml:"url"`
+	SystemID               uint8    `toml:"system_id"`
+	FilterPattern          []string `toml:"filter"`
+	StreamRequestDisable   bool     `toml:"stream_request_disable"`
+	StreamRequestFrequency uint16   `toml:"stream_request_frequency"`
+
+	Log telegraf.Logger `toml:"-"`
+
+	filter         filter.Filter
+	connection     *gomavlib.Node
+	endpointConfig []gomavlib.EndpointConf
+	cancel         context.CancelFunc
+	wg             sync.WaitGroup
+}
+
+//go:embed sample.conf
+var sampleConfig string
+
+func (*Mavlink) SampleConfig() string {
+	return sampleConfig
+}
+
+func (m *Mavlink) Init() error {
+	// Set default values
+	if len(m.URL) == 0 {
+		m.URL = "tcp://127.0.0.1:5760"
+	}
+
+	if !m.StreamRequestDisable && m.StreamRequestFrequency == 0 {
+		m.StreamRequestFrequency = 4
+	}
+
+	// Parse out the Mavlink endpoint.
+	// Try to parse the URL
+	u, err := url.Parse(m.URL)
+	if err != nil {
+		return fmt.Errorf("invalid url: %w", err)
+	}
+
+	switch u.Scheme {
+	case "serial":
+		// Serial client
+		// Parse serial URL by hand, because it is not a compliant URL.
+		baudRate := 57600
+		device, rate, found := strings.Cut(strings.TrimPrefix(m.URL, "serial://"), ":")
+		if found {
+			r, err := strconv.Atoi(rate)
+			if err != nil {
+				return fmt.Errorf("serial baud rate not valid: %w", err)
+			}
+			baudRate = r
+		}
+
+		m.endpointConfig = []gomavlib.EndpointConf{
+			gomavlib.EndpointSerial{
+				Device: device,
+				Baud:   baudRate,
+			},
+		}
+	case "tcp":
+		// 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 mavlink TCP port if a port was not provided or was invalid.
+			host = u.Host
+			port = "14540"
+		}
+
+		if len(host) > 0 {
+			m.endpointConfig = []gomavlib.EndpointConf{
+				gomavlib.EndpointTCPClient{
+					Address: host + ":" + port,
+				},
+			}
+		} else {
+			m.endpointConfig = []gomavlib.EndpointConf{
+				gomavlib.EndpointTCPServer{
+					Address: ":" + port,
+				},
+			}
+		}
+
+	case "udp":
+		// 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 mavlink TCP port if a port was not provided or was invalid.
+			host = u.Host
+			port = "5760"
+		}
+
+		if len(host) > 0 {
path: plugins/inputs/mavlink/mavlink.go
user: login: srebhan
body: ```suggestion
		if host == "" {
```
created_at: 2025-04-23T14:11:01Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,251 @@
+//go:generate ../../../tools/readme_config_includer/generator
+package mavlink
+
+import (
+	"context"
+	_ "embed"
+	"fmt"
+	"net"
+	"net/url"
+	"reflect"
+	"strconv"
+	"strings"
+	"sync"
+	"time"
+
+	"github.com/bluenviron/gomavlib/v3"
+	"github.com/bluenviron/gomavlib/v3/pkg/dialects/ardupilotmega"
+	"github.com/bluenviron/gomavlib/v3/pkg/frame"
+
+	"github.com/influxdata/telegraf"
+	"github.com/influxdata/telegraf/filter"
+	"github.com/influxdata/telegraf/internal"
+	"github.com/influxdata/telegraf/metric"
+	"github.com/influxdata/telegraf/plugins/inputs"
+)
+
+type Mavlink struct {
+	URL                    string   `toml:"url"`
+	SystemID               uint8    `toml:"system_id"`
+	FilterPattern          []string `toml:"filter"`
+	StreamRequestDisable   bool     `toml:"stream_request_disable"`
+	StreamRequestFrequency uint16   `toml:"stream_request_frequency"`
+
+	Log telegraf.Logger `toml:"-"`
+
+	filter         filter.Filter
+	connection     *gomavlib.Node
+	endpointConfig []gomavlib.EndpointConf
+	cancel         context.CancelFunc
+	wg             sync.WaitGroup
+}
+
+//go:embed sample.conf
+var sampleConfig string
+
+func (*Mavlink) SampleConfig() string {
+	return sampleConfig
+}
+
+func (m *Mavlink) Init() error {
+	// Set default values
+	if len(m.URL) == 0 {
+		m.URL = "tcp://127.0.0.1:5760"
+	}
+
+	if !m.StreamRequestDisable && m.StreamRequestFrequency == 0 {
+		m.StreamRequestFrequency = 4
+	}
+
+	// Parse out the Mavlink endpoint.
+	// Try to parse the URL
+	u, err := url.Parse(m.URL)
+	if err != nil {
+		return fmt.Errorf("invalid url: %w", err)
+	}
+
+	switch u.Scheme {
+	case "serial":
+		// Serial client
+		// Parse serial URL by hand, because it is not a compliant URL.
+		baudRate := 57600
+		device, rate, found := strings.Cut(strings.TrimPrefix(m.URL, "serial://"), ":")
+		if found {
+			r, err := strconv.Atoi(rate)
+			if err != nil {
+				return fmt.Errorf("serial baud rate not valid: %w", err)
+			}
+			baudRate = r
+		}
+
+		m.endpointConfig = []gomavlib.EndpointConf{
+			gomavlib.EndpointSerial{
+				Device: device,
+				Baud:   baudRate,
+			},
+		}
+	case "tcp":
+		// 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 mavlink TCP port if a port was not provided or was invalid.
+			host = u.Host
+			port = "14540"
+		}
+
+		if len(host) > 0 {
path: plugins/inputs/mavlink/mavlink.go
user: login: srebhan
body: ```suggestion
		if host == "" {
```
created_at: 2025-04-23T14:11:13Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,251 @@
+//go:generate ../../../tools/readme_config_includer/generator
+package mavlink
+
+import (
+	"context"
+	_ "embed"
+	"fmt"
+	"net"
+	"net/url"
+	"reflect"
+	"strconv"
+	"strings"
+	"sync"
+	"time"
+
+	"github.com/bluenviron/gomavlib/v3"
+	"github.com/bluenviron/gomavlib/v3/pkg/dialects/ardupilotmega"
+	"github.com/bluenviron/gomavlib/v3/pkg/frame"
+
+	"github.com/influxdata/telegraf"
+	"github.com/influxdata/telegraf/filter"
+	"github.com/influxdata/telegraf/internal"
+	"github.com/influxdata/telegraf/metric"
+	"github.com/influxdata/telegraf/plugins/inputs"
+)
+
+type Mavlink struct {
+	URL                    string   `toml:"url"`
+	SystemID               uint8    `toml:"system_id"`
+	FilterPattern          []string `toml:"filter"`
+	StreamRequestDisable   bool     `toml:"stream_request_disable"`
+	StreamRequestFrequency uint16   `toml:"stream_request_frequency"`
+
+	Log telegraf.Logger `toml:"-"`
+
+	filter         filter.Filter
+	connection     *gomavlib.Node
+	endpointConfig []gomavlib.EndpointConf
+	cancel         context.CancelFunc
+	wg             sync.WaitGroup
+}
+
+//go:embed sample.conf
+var sampleConfig string
+
+func (*Mavlink) SampleConfig() string {
+	return sampleConfig
+}
+
+func (m *Mavlink) Init() error {
+	// Set default values
+	if len(m.URL) == 0 {
path: plugins/inputs/mavlink/mavlink.go
user: login: srebhan
body: Use type specific tests as this gives an indication that the URL is a string. Same for other string checks below.
```suggestion
	if m.URL == "" {
```
created_at: 2025-04-23T14:12:09Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,251 @@
+//go:generate ../../../tools/readme_config_includer/generator
+package mavlink
+
+import (
+	"context"
+	_ "embed"
+	"fmt"
+	"net"
+	"net/url"
+	"reflect"
+	"strconv"
+	"strings"
+	"sync"
+	"time"
+
+	"github.com/bluenviron/gomavlib/v3"
+	"github.com/bluenviron/gomavlib/v3/pkg/dialects/ardupilotmega"
+	"github.com/bluenviron/gomavlib/v3/pkg/frame"
+
+	"github.com/influxdata/telegraf"
+	"github.com/influxdata/telegraf/filter"
+	"github.com/influxdata/telegraf/internal"
+	"github.com/influxdata/telegraf/metric"
+	"github.com/influxdata/telegraf/plugins/inputs"
+)
+
+type Mavlink struct {
+	URL                    string   `toml:"url"`
+	SystemID               uint8    `toml:"system_id"`
+	FilterPattern          []string `toml:"filter"`
+	StreamRequestDisable   bool     `toml:"stream_request_disable"`
+	StreamRequestFrequency uint16   `toml:"stream_request_frequency"`
+
+	Log telegraf.Logger `toml:"-"`
+
+	filter         filter.Filter
+	connection     *gomavlib.Node
+	endpointConfig []gomavlib.EndpointConf
path: plugins/inputs/mavlink/mavlink.go
user: login: srebhan
body: Why do we need a slice here? Wouldn't a single endpoint config be sufficient?
created_at: 2025-04-23T14:12:34Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,268 @@
+package mavlink
+
+import (
+	"testing"
+	"time"
+
+	"github.com/bluenviron/gomavlib/v3"
+	"github.com/bluenviron/gomavlib/v3/pkg/dialects/common"
+	"github.com/bluenviron/gomavlib/v3/pkg/frame"
+	"github.com/docker/go-connections/nat"
+	"github.com/stretchr/testify/require"
+	"github.com/testcontainers/testcontainers-go/wait"
+
+	"github.com/influxdata/telegraf"
+	"github.com/influxdata/telegraf/filter"
+	"github.com/influxdata/telegraf/metric"
+	"github.com/influxdata/telegraf/testutil"
+)
+
+// Test that a serial port URL can be parsed (Linux)
+func TestParseSerialUrlLinux(t *testing.T) {
+	plugin := Mavlink{
+		URL: "serial:///dev/ttyACM0:115200",
+	}
+
+	err := plugin.Init()
+	require.NoError(t, err)
+
+	endpoint, ok := plugin.endpointConfig[0].(gomavlib.EndpointSerial)
+	require.True(t, ok)
+	require.Equal(t, "/dev/ttyACM0", endpoint.Device)
+	require.Equal(t, 115200, endpoint.Baud)
+}
+
+// Test that a serial port URL can be parsed (Windows)
+func TestParseSerialUrlWindows(t *testing.T) {
+	plugin := Mavlink{
+		URL: "serial://COM1:115200",
+	}
+
+	err := plugin.Init()
+	require.NoError(t, err)
+
+	endpoint, ok := plugin.endpointConfig[0].(gomavlib.EndpointSerial)
+	require.True(t, ok)
+	require.Equal(t, "COM1", endpoint.Device)
+	require.Equal(t, 115200, endpoint.Baud)
+}
+
+// Test that a UDP client URL can be parsed.
+func TestParseUDPClientUrl(t *testing.T) {
+	plugin := Mavlink{
+		URL: "udp://192.168.1.12:14550",
+	}
+
+	err := plugin.Init()
+	require.NoError(t, err)
+
+	endpoint, ok := plugin.endpointConfig[0].(gomavlib.EndpointUDPClient)
+	require.True(t, ok)
+	require.Equal(t, "192.168.1.12:14550", endpoint.Address)
+}
+
+// Test that a UDP server URL can be parsed.
+func TestParseUDPServerUrl(t *testing.T) {
+	plugin := Mavlink{
+		URL: "udp://:14540",
+	}
+
+	err := plugin.Init()
+	require.NoError(t, err)
+
+	endpoint, ok := plugin.endpointConfig[0].(gomavlib.EndpointUDPServer)
+	require.True(t, ok)
+	require.Equal(t, ":14540", endpoint.Address)
+}
+
+// Test that a TCP client URL can be parsed.
+func TestParseTCPClientUrl(t *testing.T) {
+	plugin := Mavlink{
+		URL: "tcp://192.168.1.12:14550",
+	}
+
+	err := plugin.Init()
+	require.NoError(t, err)
+
+	endpoint, ok := plugin.endpointConfig[0].(gomavlib.EndpointTCPClient)
+	require.True(t, ok)
+	require.Equal(t, "192.168.1.12:14550", endpoint.Address)
+}
+
+// Test that an invalid URL is caught.
+func TestParseInvalidUrl(t *testing.T) {
+	plugin := Mavlink{
+		URL: "ftp://not-a-valid-fcu-url",
+	}
+
+	err := plugin.Init()
+	require.ErrorContains(t, err, "unknown scheme \"ftp\"")
+}
+
+// Test that some mavlink messages are correctly decoded into telegraf metrics.
+func TestMavlinkDecoding(t *testing.T) {
+	tests := []struct {
+		name     string
+		input    *frame.V2Frame
+		expected []telegraf.Metric
+	}{
+		{
+			name: "Heartbeat",
+			input: &frame.V2Frame{
+				SequenceNumber: 1,
+				SystemID:       1,
+				ComponentID:    1,
+				Message: &common.MessageHeartbeat{
+					Type:           0,
+					Autopilot:      1,
+					BaseMode:       2,
+					CustomMode:     3,
+					SystemStatus:   4,
+					MavlinkVersion: 5,
+				},
+				Checksum: 0,
+			},
+			expected: []telegraf.Metric{metric.New(
+				"heartbeat",
+				map[string]string{
+					"sys_id": "1",
+				},
+				map[string]interface{}{
+					"custom_mode":     uint64(3),
+					"mavlink_version": uint64(5),
+				},
+				time.Unix(0, 0),
+			)},
+		},
+		{
+			name: "Attitude",
+			input: &frame.V2Frame{
+				SequenceNumber: 1,
+				SystemID:       1,
+				ComponentID:    1,
+				Message: &common.MessageAttitude{
+					TimeBootMs: 123,
+					Roll:       1.234,
+					Pitch:      0.463,
+					Yaw:        -0.112,
+					Rollspeed:  0.001,
+					Pitchspeed: 0.002,
+					Yawspeed:   0.003,
+				},
+				Checksum: 0,
+			},
+			expected: []telegraf.Metric{metric.New(
+				"attitude",
+				map[string]string{
+					"sys_id": "1",
+				},
+				map[string]interface{}{
+					"pitch":        float64(0.463),
+					"roll":         float64(1.234),
+					"yaw":          float64(-0.112),
+					"pitchspeed":   float64(0.002),
+					"rollspeed":    float64(0.001),
+					"yawspeed":     float64(0.003),
+					"time_boot_ms": uint64(123),
+				},
+				time.Unix(0, 0),
+			)},
+		},
+		{
+			name: "ESC Status",
+			input: &frame.V2Frame{
+				SequenceNumber: 1,
+				SystemID:       1,
+				ComponentID:    1,
+				Message: &common.MessageEscStatus{
+					Index:    0,
+					TimeUsec: 12345,
+					Rpm:      [4]int32{0, 1, 2, 3},
+					Voltage:  [4]float32{10.0, 11.0, 12.0, 13.0},
+					Current:  [4]float32{14.0, 15.0, 16.0, 17.0},
+				},
+				Checksum: 0,
+			},
+			expected: []telegraf.Metric{metric.New(
+				"esc_status",
+				map[string]string{
+					"sys_id": "1",
+				},
+				map[string]interface{}{
+					"index":     uint64(0),
+					"time_usec": uint64(12345),
+					"current_1": float64(14.0),
+					"current_2": float64(15.0),
+					"current_3": float64(16.0),
+					"current_4": float64(17.0),
+					"rpm_1":     int32(0),
+					"rpm_2":     int32(1),
+					"rpm_3":     int32(2),
+					"rpm_4":     int32(3),
+					"voltage_1": float64(10.0),
+					"voltage_2": float64(11.0),
+					"voltage_3": float64(12.0),
+					"voltage_4": float64(13.0),
+				},
+				time.Unix(0, 0),
+			)},
+		},
+	}
+
+	tmpFilter, err := filter.Compile(make([]string, 0))
+	require.NoError(t, err)
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			actual := convertFrameToMetric(tt.input, tmpFilter)
+			testutil.RequireMetricsStructureEqual(t, tt.expected, []telegraf.Metric{actual}, testutil.IgnoreTime())
+		})
+	}
+}
+
+// Test that the plugin can interface with a real ArduPilot container.
+func TestArduPilotIntegration(t *testing.T) {
+	if testing.Short() {
+		t.Skip("Skipping integration test in short mode")
+	}
+
+	// Start the docker container for ArduPilot
+	t.Log("Starting ArduPilot container")
path: plugins/inputs/mavlink/mavlink_test.go
user: login: srebhan
body: Please remove the debug logging! Same for all other logs below!
created_at: 2025-04-23T14:14:38Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,268 @@
+package mavlink
+
+import (
+	"testing"
+	"time"
+
+	"github.com/bluenviron/gomavlib/v3"
+	"github.com/bluenviron/gomavlib/v3/pkg/dialects/common"
+	"github.com/bluenviron/gomavlib/v3/pkg/frame"
+	"github.com/docker/go-connections/nat"
+	"github.com/stretchr/testify/require"
+	"github.com/testcontainers/testcontainers-go/wait"
+
+	"github.com/influxdata/telegraf"
+	"github.com/influxdata/telegraf/filter"
+	"github.com/influxdata/telegraf/metric"
+	"github.com/influxdata/telegraf/testutil"
+)
+
+// Test that a serial port URL can be parsed (Linux)
+func TestParseSerialUrlLinux(t *testing.T) {
+	plugin := Mavlink{
+		URL: "serial:///dev/ttyACM0:115200",
+	}
+
+	err := plugin.Init()
+	require.NoError(t, err)
+
+	endpoint, ok := plugin.endpointConfig[0].(gomavlib.EndpointSerial)
+	require.True(t, ok)
+	require.Equal(t, "/dev/ttyACM0", endpoint.Device)
+	require.Equal(t, 115200, endpoint.Baud)
+}
+
+// Test that a serial port URL can be parsed (Windows)
+func TestParseSerialUrlWindows(t *testing.T) {
+	plugin := Mavlink{
+		URL: "serial://COM1:115200",
+	}
+
+	err := plugin.Init()
+	require.NoError(t, err)
+
+	endpoint, ok := plugin.endpointConfig[0].(gomavlib.EndpointSerial)
+	require.True(t, ok)
+	require.Equal(t, "COM1", endpoint.Device)
+	require.Equal(t, 115200, endpoint.Baud)
+}
+
+// Test that a UDP client URL can be parsed.
+func TestParseUDPClientUrl(t *testing.T) {
+	plugin := Mavlink{
+		URL: "udp://192.168.1.12:14550",
+	}
+
+	err := plugin.Init()
+	require.NoError(t, err)
+
+	endpoint, ok := plugin.endpointConfig[0].(gomavlib.EndpointUDPClient)
+	require.True(t, ok)
+	require.Equal(t, "192.168.1.12:14550", endpoint.Address)
+}
+
+// Test that a UDP server URL can be parsed.
+func TestParseUDPServerUrl(t *testing.T) {
+	plugin := Mavlink{
+		URL: "udp://:14540",
+	}
+
+	err := plugin.Init()
+	require.NoError(t, err)
+
+	endpoint, ok := plugin.endpointConfig[0].(gomavlib.EndpointUDPServer)
+	require.True(t, ok)
+	require.Equal(t, ":14540", endpoint.Address)
+}
+
+// Test that a TCP client URL can be parsed.
+func TestParseTCPClientUrl(t *testing.T) {
+	plugin := Mavlink{
+		URL: "tcp://192.168.1.12:14550",
+	}
+
+	err := plugin.Init()
+	require.NoError(t, err)
+
+	endpoint, ok := plugin.endpointConfig[0].(gomavlib.EndpointTCPClient)
+	require.True(t, ok)
+	require.Equal(t, "192.168.1.12:14550", endpoint.Address)
+}
+
+// Test that an invalid URL is caught.
+func TestParseInvalidUrl(t *testing.T) {
+	plugin := Mavlink{
+		URL: "ftp://not-a-valid-fcu-url",
+	}
+
+	err := plugin.Init()
+	require.ErrorContains(t, err, "unknown scheme \"ftp\"")
+}
+
+// Test that some mavlink messages are correctly decoded into telegraf metrics.
+func TestMavlinkDecoding(t *testing.T) {
+	tests := []struct {
+		name     string
+		input    *frame.V2Frame
+		expected []telegraf.Metric
+	}{
+		{
+			name: "Heartbeat",
+			input: &frame.V2Frame{
+				SequenceNumber: 1,
+				SystemID:       1,
+				ComponentID:    1,
+				Message: &common.MessageHeartbeat{
+					Type:           0,
+					Autopilot:      1,
+					BaseMode:       2,
+					CustomMode:     3,
+					SystemStatus:   4,
+					MavlinkVersion: 5,
+				},
+				Checksum: 0,
+			},
+			expected: []telegraf.Metric{metric.New(
+				"heartbeat",
+				map[string]string{
+					"sys_id": "1",
+				},
+				map[string]interface{}{
+					"custom_mode":     uint64(3),
+					"mavlink_version": uint64(5),
+				},
+				time.Unix(0, 0),
+			)},
+		},
+		{
+			name: "Attitude",
+			input: &frame.V2Frame{
+				SequenceNumber: 1,
+				SystemID:       1,
+				ComponentID:    1,
+				Message: &common.MessageAttitude{
+					TimeBootMs: 123,
+					Roll:       1.234,
+					Pitch:      0.463,
+					Yaw:        -0.112,
+					Rollspeed:  0.001,
+					Pitchspeed: 0.002,
+					Yawspeed:   0.003,
+				},
+				Checksum: 0,
+			},
+			expected: []telegraf.Metric{metric.New(
+				"attitude",
+				map[string]string{
+					"sys_id": "1",
+				},
+				map[string]interface{}{
+					"pitch":        float64(0.463),
+					"roll":         float64(1.234),
+					"yaw":          float64(-0.112),
+					"pitchspeed":   float64(0.002),
+					"rollspeed":    float64(0.001),
+					"yawspeed":     float64(0.003),
+					"time_boot_ms": uint64(123),
+				},
+				time.Unix(0, 0),
+			)},
+		},
+		{
+			name: "ESC Status",
+			input: &frame.V2Frame{
+				SequenceNumber: 1,
+				SystemID:       1,
+				ComponentID:    1,
+				Message: &common.MessageEscStatus{
+					Index:    0,
+					TimeUsec: 12345,
+					Rpm:      [4]int32{0, 1, 2, 3},
+					Voltage:  [4]float32{10.0, 11.0, 12.0, 13.0},
+					Current:  [4]float32{14.0, 15.0, 16.0, 17.0},
+				},
+				Checksum: 0,
+			},
+			expected: []telegraf.Metric{metric.New(
+				"esc_status",
+				map[string]string{
+					"sys_id": "1",
+				},
+				map[string]interface{}{
+					"index":     uint64(0),
+					"time_usec": uint64(12345),
+					"current_1": float64(14.0),
+					"current_2": float64(15.0),
+					"current_3": float64(16.0),
+					"current_4": float64(17.0),
+					"rpm_1":     int32(0),
+					"rpm_2":     int32(1),
+					"rpm_3":     int32(2),
+					"rpm_4":     int32(3),
+					"voltage_1": float64(10.0),
+					"voltage_2": float64(11.0),
+					"voltage_3": float64(12.0),
+					"voltage_4": float64(13.0),
+				},
+				time.Unix(0, 0),
+			)},
+		},
+	}
+
+	tmpFilter, err := filter.Compile(make([]string, 0))
+	require.NoError(t, err)
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			actual := convertFrameToMetric(tt.input, tmpFilter)
+			testutil.RequireMetricsStructureEqual(t, tt.expected, []telegraf.Metric{actual}, testutil.IgnoreTime())
+		})
+	}
+}
+
+// Test that the plugin can interface with a real ArduPilot container.
+func TestArduPilotIntegration(t *testing.T) {
+	if testing.Short() {
+		t.Skip("Skipping integration test in short mode")
+	}
+
+	// Start the docker container for ArduPilot
+	t.Log("Starting ArduPilot container")
+	container := testutil.Container{
+		Image:        "radarku/ardupilot-sitl",
+		ExposedPorts: []string{"5760"},
+		WaitingFor: wait.ForAll(
+			wait.ForListeningPort(nat.Port("5760")),
+		),
+	}
+	require.NoError(t, container.Start(), "failed to start ardupilot container")
+	defer container.Terminate()
+
+	t.Logf("ArduPilot is listening on port: %s", container.Ports["5760"])
+
+	// Setup the plugin
+	plugin := Mavlink{
+		URL:      "tcp://127.0.0.1:" + container.Ports["5760"],
+		SystemID: 254,
+	}
+	plugin.Log = testutil.Logger{}
path: plugins/inputs/mavlink/mavlink_test.go
user: login: srebhan
body: ```suggestion
	plugin := Mavlink{
		URL:      "tcp://127.0.0.1:" + container.Ports["5760"],
		SystemID: 254,
		Log:      testutil.Logger{},
	}
```
created_at: 2025-04-23T14:15:15Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,268 @@
+package mavlink
+
+import (
+	"testing"
+	"time"
+
+	"github.com/bluenviron/gomavlib/v3"
+	"github.com/bluenviron/gomavlib/v3/pkg/dialects/common"
+	"github.com/bluenviron/gomavlib/v3/pkg/frame"
+	"github.com/docker/go-connections/nat"
+	"github.com/stretchr/testify/require"
+	"github.com/testcontainers/testcontainers-go/wait"
+
+	"github.com/influxdata/telegraf"
+	"github.com/influxdata/telegraf/filter"
+	"github.com/influxdata/telegraf/metric"
+	"github.com/influxdata/telegraf/testutil"
+)
+
+// Test that a serial port URL can be parsed (Linux)
+func TestParseSerialUrlLinux(t *testing.T) {
+	plugin := Mavlink{
+		URL: "serial:///dev/ttyACM0:115200",
+	}
+
+	err := plugin.Init()
+	require.NoError(t, err)
+
+	endpoint, ok := plugin.endpointConfig[0].(gomavlib.EndpointSerial)
+	require.True(t, ok)
+	require.Equal(t, "/dev/ttyACM0", endpoint.Device)
+	require.Equal(t, 115200, endpoint.Baud)
+}
+
+// Test that a serial port URL can be parsed (Windows)
+func TestParseSerialUrlWindows(t *testing.T) {
+	plugin := Mavlink{
+		URL: "serial://COM1:115200",
+	}
+
+	err := plugin.Init()
+	require.NoError(t, err)
+
+	endpoint, ok := plugin.endpointConfig[0].(gomavlib.EndpointSerial)
+	require.True(t, ok)
+	require.Equal(t, "COM1", endpoint.Device)
+	require.Equal(t, 115200, endpoint.Baud)
+}
+
+// Test that a UDP client URL can be parsed.
+func TestParseUDPClientUrl(t *testing.T) {
+	plugin := Mavlink{
+		URL: "udp://192.168.1.12:14550",
+	}
+
+	err := plugin.Init()
+	require.NoError(t, err)
+
+	endpoint, ok := plugin.endpointConfig[0].(gomavlib.EndpointUDPClient)
+	require.True(t, ok)
+	require.Equal(t, "192.168.1.12:14550", endpoint.Address)
+}
+
+// Test that a UDP server URL can be parsed.
+func TestParseUDPServerUrl(t *testing.T) {
+	plugin := Mavlink{
+		URL: "udp://:14540",
+	}
+
+	err := plugin.Init()
+	require.NoError(t, err)
+
+	endpoint, ok := plugin.endpointConfig[0].(gomavlib.EndpointUDPServer)
+	require.True(t, ok)
+	require.Equal(t, ":14540", endpoint.Address)
+}
+
+// Test that a TCP client URL can be parsed.
+func TestParseTCPClientUrl(t *testing.T) {
+	plugin := Mavlink{
+		URL: "tcp://192.168.1.12:14550",
+	}
+
+	err := plugin.Init()
+	require.NoError(t, err)
+
+	endpoint, ok := plugin.endpointConfig[0].(gomavlib.EndpointTCPClient)
+	require.True(t, ok)
+	require.Equal(t, "192.168.1.12:14550", endpoint.Address)
+}
+
+// Test that an invalid URL is caught.
+func TestParseInvalidUrl(t *testing.T) {
+	plugin := Mavlink{
+		URL: "ftp://not-a-valid-fcu-url",
+	}
+
+	err := plugin.Init()
+	require.ErrorContains(t, err, "unknown scheme \"ftp\"")
+}
+
+// Test that some mavlink messages are correctly decoded into telegraf metrics.
+func TestMavlinkDecoding(t *testing.T) {
+	tests := []struct {
+		name     string
+		input    *frame.V2Frame
+		expected []telegraf.Metric
+	}{
+		{
+			name: "Heartbeat",
+			input: &frame.V2Frame{
+				SequenceNumber: 1,
+				SystemID:       1,
+				ComponentID:    1,
+				Message: &common.MessageHeartbeat{
+					Type:           0,
+					Autopilot:      1,
+					BaseMode:       2,
+					CustomMode:     3,
+					SystemStatus:   4,
+					MavlinkVersion: 5,
+				},
+				Checksum: 0,
+			},
+			expected: []telegraf.Metric{metric.New(
+				"heartbeat",
+				map[string]string{
+					"sys_id": "1",
+				},
+				map[string]interface{}{
+					"custom_mode":     uint64(3),
+					"mavlink_version": uint64(5),
+				},
+				time.Unix(0, 0),
+			)},
+		},
+		{
+			name: "Attitude",
+			input: &frame.V2Frame{
+				SequenceNumber: 1,
+				SystemID:       1,
+				ComponentID:    1,
+				Message: &common.MessageAttitude{
+					TimeBootMs: 123,
+					Roll:       1.234,
+					Pitch:      0.463,
+					Yaw:        -0.112,
+					Rollspeed:  0.001,
+					Pitchspeed: 0.002,
+					Yawspeed:   0.003,
+				},
+				Checksum: 0,
+			},
+			expected: []telegraf.Metric{metric.New(
+				"attitude",
+				map[string]string{
+					"sys_id": "1",
+				},
+				map[string]interface{}{
+					"pitch":        float64(0.463),
+					"roll":         float64(1.234),
+					"yaw":          float64(-0.112),
+					"pitchspeed":   float64(0.002),
+					"rollspeed":    float64(0.001),
+					"yawspeed":     float64(0.003),
+					"time_boot_ms": uint64(123),
+				},
+				time.Unix(0, 0),
+			)},
+		},
+		{
+			name: "ESC Status",
+			input: &frame.V2Frame{
+				SequenceNumber: 1,
+				SystemID:       1,
+				ComponentID:    1,
+				Message: &common.MessageEscStatus{
+					Index:    0,
+					TimeUsec: 12345,
+					Rpm:      [4]int32{0, 1, 2, 3},
+					Voltage:  [4]float32{10.0, 11.0, 12.0, 13.0},
+					Current:  [4]float32{14.0, 15.0, 16.0, 17.0},
+				},
+				Checksum: 0,
+			},
+			expected: []telegraf.Metric{metric.New(
+				"esc_status",
+				map[string]string{
+					"sys_id": "1",
+				},
+				map[string]interface{}{
+					"index":     uint64(0),
+					"time_usec": uint64(12345),
+					"current_1": float64(14.0),
+					"current_2": float64(15.0),
+					"current_3": float64(16.0),
+					"current_4": float64(17.0),
+					"rpm_1":     int32(0),
+					"rpm_2":     int32(1),
+					"rpm_3":     int32(2),
+					"rpm_4":     int32(3),
+					"voltage_1": float64(10.0),
+					"voltage_2": float64(11.0),
+					"voltage_3": float64(12.0),
+					"voltage_4": float64(13.0),
+				},
+				time.Unix(0, 0),
+			)},
+		},
+	}
+
+	tmpFilter, err := filter.Compile(make([]string, 0))
+	require.NoError(t, err)
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			actual := convertFrameToMetric(tt.input, tmpFilter)
+			testutil.RequireMetricsStructureEqual(t, tt.expected, []telegraf.Metric{actual}, testutil.IgnoreTime())
+		})
+	}
+}
+
+// Test that the plugin can interface with a real ArduPilot container.
+func TestArduPilotIntegration(t *testing.T) {
+	if testing.Short() {
+		t.Skip("Skipping integration test in short mode")
+	}
+
+	// Start the docker container for ArduPilot
+	t.Log("Starting ArduPilot container")
+	container := testutil.Container{
+		Image:        "radarku/ardupilot-sitl",
+		ExposedPorts: []string{"5760"},
+		WaitingFor: wait.ForAll(
+			wait.ForListeningPort(nat.Port("5760")),
+		),
+	}
+	require.NoError(t, container.Start(), "failed to start ardupilot container")
+	defer container.Terminate()
+
+	t.Logf("ArduPilot is listening on port: %s", container.Ports["5760"])
+
+	// Setup the plugin
+	plugin := Mavlink{
+		URL:      "tcp://127.0.0.1:" + container.Ports["5760"],
+		SystemID: 254,
+	}
+	plugin.Log = testutil.Logger{}
+	require.NoError(t, plugin.Init())
+
+	// Collect the metrics and compare
+	var acc testutil.Accumulator
+	require.NoError(t, plugin.Start(&acc))
path: plugins/inputs/mavlink/mavlink_test.go
user: login: srebhan
body: We need to make sure to stop the plugin even if the tests fail
```suggestion
	require.NoError(t, plugin.Start(&acc))
	defer plugin.Stop()
```
created_at: 2025-04-23T14:18:38Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,268 @@
+package mavlink
+
+import (
+	"testing"
+	"time"
+
+	"github.com/bluenviron/gomavlib/v3"
+	"github.com/bluenviron/gomavlib/v3/pkg/dialects/common"
+	"github.com/bluenviron/gomavlib/v3/pkg/frame"
+	"github.com/docker/go-connections/nat"
+	"github.com/stretchr/testify/require"
+	"github.com/testcontainers/testcontainers-go/wait"
+
+	"github.com/influxdata/telegraf"
+	"github.com/influxdata/telegraf/filter"
+	"github.com/influxdata/telegraf/metric"
+	"github.com/influxdata/telegraf/testutil"
+)
+
+// Test that a serial port URL can be parsed (Linux)
+func TestParseSerialUrlLinux(t *testing.T) {
+	plugin := Mavlink{
+		URL: "serial:///dev/ttyACM0:115200",
+	}
+
+	err := plugin.Init()
+	require.NoError(t, err)
+
+	endpoint, ok := plugin.endpointConfig[0].(gomavlib.EndpointSerial)
+	require.True(t, ok)
+	require.Equal(t, "/dev/ttyACM0", endpoint.Device)
+	require.Equal(t, 115200, endpoint.Baud)
+}
+
+// Test that a serial port URL can be parsed (Windows)
+func TestParseSerialUrlWindows(t *testing.T) {
+	plugin := Mavlink{
+		URL: "serial://COM1:115200",
+	}
+
+	err := plugin.Init()
+	require.NoError(t, err)
+
+	endpoint, ok := plugin.endpointConfig[0].(gomavlib.EndpointSerial)
+	require.True(t, ok)
+	require.Equal(t, "COM1", endpoint.Device)
+	require.Equal(t, 115200, endpoint.Baud)
+}
+
+// Test that a UDP client URL can be parsed.
+func TestParseUDPClientUrl(t *testing.T) {
+	plugin := Mavlink{
+		URL: "udp://192.168.1.12:14550",
+	}
+
+	err := plugin.Init()
+	require.NoError(t, err)
+
+	endpoint, ok := plugin.endpointConfig[0].(gomavlib.EndpointUDPClient)
+	require.True(t, ok)
+	require.Equal(t, "192.168.1.12:14550", endpoint.Address)
+}
+
+// Test that a UDP server URL can be parsed.
+func TestParseUDPServerUrl(t *testing.T) {
+	plugin := Mavlink{
+		URL: "udp://:14540",
+	}
+
+	err := plugin.Init()
+	require.NoError(t, err)
+
+	endpoint, ok := plugin.endpointConfig[0].(gomavlib.EndpointUDPServer)
+	require.True(t, ok)
+	require.Equal(t, ":14540", endpoint.Address)
+}
+
+// Test that a TCP client URL can be parsed.
+func TestParseTCPClientUrl(t *testing.T) {
+	plugin := Mavlink{
+		URL: "tcp://192.168.1.12:14550",
+	}
+
+	err := plugin.Init()
+	require.NoError(t, err)
+
+	endpoint, ok := plugin.endpointConfig[0].(gomavlib.EndpointTCPClient)
+	require.True(t, ok)
+	require.Equal(t, "192.168.1.12:14550", endpoint.Address)
+}
+
+// Test that an invalid URL is caught.
+func TestParseInvalidUrl(t *testing.T) {
+	plugin := Mavlink{
+		URL: "ftp://not-a-valid-fcu-url",
+	}
+
+	err := plugin.Init()
+	require.ErrorContains(t, err, "unknown scheme \"ftp\"")
+}
+
+// Test that some mavlink messages are correctly decoded into telegraf metrics.
+func TestMavlinkDecoding(t *testing.T) {
+	tests := []struct {
+		name     string
+		input    *frame.V2Frame
+		expected []telegraf.Metric
+	}{
+		{
+			name: "Heartbeat",
+			input: &frame.V2Frame{
+				SequenceNumber: 1,
+				SystemID:       1,
+				ComponentID:    1,
+				Message: &common.MessageHeartbeat{
+					Type:           0,
+					Autopilot:      1,
+					BaseMode:       2,
+					CustomMode:     3,
+					SystemStatus:   4,
+					MavlinkVersion: 5,
+				},
+				Checksum: 0,
+			},
+			expected: []telegraf.Metric{metric.New(
+				"heartbeat",
+				map[string]string{
+					"sys_id": "1",
+				},
+				map[string]interface{}{
+					"custom_mode":     uint64(3),
+					"mavlink_version": uint64(5),
+				},
+				time.Unix(0, 0),
+			)},
+		},
+		{
+			name: "Attitude",
+			input: &frame.V2Frame{
+				SequenceNumber: 1,
+				SystemID:       1,
+				ComponentID:    1,
+				Message: &common.MessageAttitude{
+					TimeBootMs: 123,
+					Roll:       1.234,
+					Pitch:      0.463,
+					Yaw:        -0.112,
+					Rollspeed:  0.001,
+					Pitchspeed: 0.002,
+					Yawspeed:   0.003,
+				},
+				Checksum: 0,
+			},
+			expected: []telegraf.Metric{metric.New(
+				"attitude",
+				map[string]string{
+					"sys_id": "1",
+				},
+				map[string]interface{}{
+					"pitch":        float64(0.463),
+					"roll":         float64(1.234),
+					"yaw":          float64(-0.112),
+					"pitchspeed":   float64(0.002),
+					"rollspeed":    float64(0.001),
+					"yawspeed":     float64(0.003),
+					"time_boot_ms": uint64(123),
+				},
+				time.Unix(0, 0),
+			)},
+		},
+		{
+			name: "ESC Status",
+			input: &frame.V2Frame{
+				SequenceNumber: 1,
+				SystemID:       1,
+				ComponentID:    1,
+				Message: &common.MessageEscStatus{
+					Index:    0,
+					TimeUsec: 12345,
+					Rpm:      [4]int32{0, 1, 2, 3},
+					Voltage:  [4]float32{10.0, 11.0, 12.0, 13.0},
+					Current:  [4]float32{14.0, 15.0, 16.0, 17.0},
+				},
+				Checksum: 0,
+			},
+			expected: []telegraf.Metric{metric.New(
+				"esc_status",
+				map[string]string{
+					"sys_id": "1",
+				},
+				map[string]interface{}{
+					"index":     uint64(0),
+					"time_usec": uint64(12345),
+					"current_1": float64(14.0),
+					"current_2": float64(15.0),
+					"current_3": float64(16.0),
+					"current_4": float64(17.0),
+					"rpm_1":     int32(0),
+					"rpm_2":     int32(1),
+					"rpm_3":     int32(2),
+					"rpm_4":     int32(3),
+					"voltage_1": float64(10.0),
+					"voltage_2": float64(11.0),
+					"voltage_3": float64(12.0),
+					"voltage_4": float64(13.0),
+				},
+				time.Unix(0, 0),
+			)},
+		},
+	}
+
+	tmpFilter, err := filter.Compile(make([]string, 0))
+	require.NoError(t, err)
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			actual := convertFrameToMetric(tt.input, tmpFilter)
+			testutil.RequireMetricsStructureEqual(t, tt.expected, []telegraf.Metric{actual}, testutil.IgnoreTime())
+		})
+	}
+}
+
+// Test that the plugin can interface with a real ArduPilot container.
+func TestArduPilotIntegration(t *testing.T) {
+	if testing.Short() {
+		t.Skip("Skipping integration test in short mode")
+	}
+
+	// Start the docker container for ArduPilot
+	t.Log("Starting ArduPilot container")
+	container := testutil.Container{
+		Image:        "radarku/ardupilot-sitl",
+		ExposedPorts: []string{"5760"},
+		WaitingFor: wait.ForAll(
+			wait.ForListeningPort(nat.Port("5760")),
+		),
+	}
+	require.NoError(t, container.Start(), "failed to start ardupilot container")
+	defer container.Terminate()
+
+	t.Logf("ArduPilot is listening on port: %s", container.Ports["5760"])
+
+	// Setup the plugin
+	plugin := Mavlink{
+		URL:      "tcp://127.0.0.1:" + container.Ports["5760"],
+		SystemID: 254,
+	}
+	plugin.Log = testutil.Logger{}
+	require.NoError(t, plugin.Init())
+
+	// Collect the metrics and compare
+	var acc testutil.Accumulator
+	require.NoError(t, plugin.Start(&acc))
+
+	// Wait 5 seconds, then inspect metrics
+	time.Sleep(time.Second * 5)
+	require.NoError(t, plugin.Gather(&acc))
+	actual := acc.GetTelegrafMetrics()
+	plugin.Stop()
+
+	// Expect to have received more than 10 metrics
+	// The exact metrics received is non-deterministic because ArduPilot's
+	// startup may differ between runs, but should be on the order of 100
+	// metrics in 5 seconds. Not actually testing the content of metrics
+	// here; that is tested in TestMavlinkDecoding.
+	t.Logf("Received %d metrics", len(actual))
+	require.Greater(t, len(actual), 10)
path: plugins/inputs/mavlink/mavlink_test.go
user: login: srebhan
body: This is racy and calls for flaky and slow tests! You should wait for the metrics like this
```suggestion
	// Expect to have received more than 10 metrics. The exact metrics received 
	// is non-deterministic because ArduPilot's startup may vary between runs,
	// but should be on the order of 100 metrics in 5 seconds.
	// Content of metrics is not tested here as we don't know what to expect.
        require.Eventually(t, func() bool {
                return acc.NMetrics() >= 10
        }, 3*time.Second, 100*time.Millisecond, "less than 10 metrics received")
```

created_at: 2025-04-23T14:20:23Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,251 @@
+//go:generate ../../../tools/readme_config_includer/generator
+package mavlink
+
+import (
+	"context"
+	_ "embed"
+	"fmt"
+	"net"
+	"net/url"
+	"reflect"
+	"strconv"
+	"strings"
+	"sync"
+	"time"
+
+	"github.com/bluenviron/gomavlib/v3"
+	"github.com/bluenviron/gomavlib/v3/pkg/dialects/ardupilotmega"
+	"github.com/bluenviron/gomavlib/v3/pkg/frame"
+
+	"github.com/influxdata/telegraf"
+	"github.com/influxdata/telegraf/filter"
+	"github.com/influxdata/telegraf/internal"
+	"github.com/influxdata/telegraf/metric"
+	"github.com/influxdata/telegraf/plugins/inputs"
+)
+
+type Mavlink struct {
+	URL                    string   `toml:"url"`
+	SystemID               uint8    `toml:"system_id"`
+	FilterPattern          []string `toml:"filter"`
+	StreamRequestDisable   bool     `toml:"stream_request_disable"`
+	StreamRequestFrequency uint16   `toml:"stream_request_frequency"`
path: plugins/inputs/mavlink/mavlink.go
user: login: chrisdalke
body: Yes, agreed that makes sense. I've changed this to only have `stream_request_frequency` as a config option, and value `0` means "disabled". Added a comment to the config to explain that
created_at: 2025-04-23T20:54:58Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,268 @@
+package mavlink
+
+import (
+	"testing"
+	"time"
+
+	"github.com/bluenviron/gomavlib/v3"
+	"github.com/bluenviron/gomavlib/v3/pkg/dialects/common"
+	"github.com/bluenviron/gomavlib/v3/pkg/frame"
+	"github.com/docker/go-connections/nat"
+	"github.com/stretchr/testify/require"
+	"github.com/testcontainers/testcontainers-go/wait"
+
+	"github.com/influxdata/telegraf"
+	"github.com/influxdata/telegraf/filter"
+	"github.com/influxdata/telegraf/metric"
+	"github.com/influxdata/telegraf/testutil"
+)
+
+// Test that a serial port URL can be parsed (Linux)
+func TestParseSerialUrlLinux(t *testing.T) {
+	plugin := Mavlink{
+		URL: "serial:///dev/ttyACM0:115200",
+	}
+
+	err := plugin.Init()
+	require.NoError(t, err)
+
+	endpoint, ok := plugin.endpointConfig[0].(gomavlib.EndpointSerial)
+	require.True(t, ok)
+	require.Equal(t, "/dev/ttyACM0", endpoint.Device)
+	require.Equal(t, 115200, endpoint.Baud)
+}
+
+// Test that a serial port URL can be parsed (Windows)
+func TestParseSerialUrlWindows(t *testing.T) {
+	plugin := Mavlink{
+		URL: "serial://COM1:115200",
+	}
+
+	err := plugin.Init()
+	require.NoError(t, err)
+
+	endpoint, ok := plugin.endpointConfig[0].(gomavlib.EndpointSerial)
+	require.True(t, ok)
+	require.Equal(t, "COM1", endpoint.Device)
+	require.Equal(t, 115200, endpoint.Baud)
+}
+
+// Test that a UDP client URL can be parsed.
+func TestParseUDPClientUrl(t *testing.T) {
+	plugin := Mavlink{
+		URL: "udp://192.168.1.12:14550",
+	}
+
+	err := plugin.Init()
+	require.NoError(t, err)
+
+	endpoint, ok := plugin.endpointConfig[0].(gomavlib.EndpointUDPClient)
+	require.True(t, ok)
+	require.Equal(t, "192.168.1.12:14550", endpoint.Address)
+}
+
+// Test that a UDP server URL can be parsed.
+func TestParseUDPServerUrl(t *testing.T) {
+	plugin := Mavlink{
+		URL: "udp://:14540",
+	}
+
+	err := plugin.Init()
+	require.NoError(t, err)
+
+	endpoint, ok := plugin.endpointConfig[0].(gomavlib.EndpointUDPServer)
+	require.True(t, ok)
+	require.Equal(t, ":14540", endpoint.Address)
+}
+
+// Test that a TCP client URL can be parsed.
+func TestParseTCPClientUrl(t *testing.T) {
+	plugin := Mavlink{
+		URL: "tcp://192.168.1.12:14550",
+	}
+
+	err := plugin.Init()
+	require.NoError(t, err)
+
+	endpoint, ok := plugin.endpointConfig[0].(gomavlib.EndpointTCPClient)
+	require.True(t, ok)
+	require.Equal(t, "192.168.1.12:14550", endpoint.Address)
+}
+
+// Test that an invalid URL is caught.
+func TestParseInvalidUrl(t *testing.T) {
+	plugin := Mavlink{
+		URL: "ftp://not-a-valid-fcu-url",
+	}
+
+	err := plugin.Init()
+	require.ErrorContains(t, err, "unknown scheme \"ftp\"")
+}
+
+// Test that some mavlink messages are correctly decoded into telegraf metrics.
+func TestMavlinkDecoding(t *testing.T) {
+	tests := []struct {
+		name     string
+		input    *frame.V2Frame
+		expected []telegraf.Metric
+	}{
+		{
+			name: "Heartbeat",
+			input: &frame.V2Frame{
+				SequenceNumber: 1,
+				SystemID:       1,
+				ComponentID:    1,
+				Message: &common.MessageHeartbeat{
+					Type:           0,
+					Autopilot:      1,
+					BaseMode:       2,
+					CustomMode:     3,
+					SystemStatus:   4,
+					MavlinkVersion: 5,
+				},
+				Checksum: 0,
+			},
+			expected: []telegraf.Metric{metric.New(
+				"heartbeat",
+				map[string]string{
+					"sys_id": "1",
+				},
+				map[string]interface{}{
+					"custom_mode":     uint64(3),
+					"mavlink_version": uint64(5),
+				},
+				time.Unix(0, 0),
+			)},
+		},
+		{
+			name: "Attitude",
+			input: &frame.V2Frame{
+				SequenceNumber: 1,
+				SystemID:       1,
+				ComponentID:    1,
+				Message: &common.MessageAttitude{
+					TimeBootMs: 123,
+					Roll:       1.234,
+					Pitch:      0.463,
+					Yaw:        -0.112,
+					Rollspeed:  0.001,
+					Pitchspeed: 0.002,
+					Yawspeed:   0.003,
+				},
+				Checksum: 0,
+			},
+			expected: []telegraf.Metric{metric.New(
+				"attitude",
+				map[string]string{
+					"sys_id": "1",
+				},
+				map[string]interface{}{
+					"pitch":        float64(0.463),
+					"roll":         float64(1.234),
+					"yaw":          float64(-0.112),
+					"pitchspeed":   float64(0.002),
+					"rollspeed":    float64(0.001),
+					"yawspeed":     float64(0.003),
+					"time_boot_ms": uint64(123),
+				},
+				time.Unix(0, 0),
+			)},
+		},
+		{
+			name: "ESC Status",
+			input: &frame.V2Frame{
+				SequenceNumber: 1,
+				SystemID:       1,
+				ComponentID:    1,
+				Message: &common.MessageEscStatus{
+					Index:    0,
+					TimeUsec: 12345,
+					Rpm:      [4]int32{0, 1, 2, 3},
+					Voltage:  [4]float32{10.0, 11.0, 12.0, 13.0},
+					Current:  [4]float32{14.0, 15.0, 16.0, 17.0},
+				},
+				Checksum: 0,
+			},
+			expected: []telegraf.Metric{metric.New(
+				"esc_status",
+				map[string]string{
+					"sys_id": "1",
+				},
+				map[string]interface{}{
+					"index":     uint64(0),
+					"time_usec": uint64(12345),
+					"current_1": float64(14.0),
+					"current_2": float64(15.0),
+					"current_3": float64(16.0),
+					"current_4": float64(17.0),
+					"rpm_1":     int32(0),
+					"rpm_2":     int32(1),
+					"rpm_3":     int32(2),
+					"rpm_4":     int32(3),
+					"voltage_1": float64(10.0),
+					"voltage_2": float64(11.0),
+					"voltage_3": float64(12.0),
+					"voltage_4": float64(13.0),
+				},
+				time.Unix(0, 0),
+			)},
+		},
+	}
+
+	tmpFilter, err := filter.Compile(make([]string, 0))
+	require.NoError(t, err)
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			actual := convertFrameToMetric(tt.input, tmpFilter)
+			testutil.RequireMetricsStructureEqual(t, tt.expected, []telegraf.Metric{actual}, testutil.IgnoreTime())
+		})
+	}
+}
+
+// Test that the plugin can interface with a real ArduPilot container.
+func TestArduPilotIntegration(t *testing.T) {
+	if testing.Short() {
+		t.Skip("Skipping integration test in short mode")
+	}
+
+	// Start the docker container for ArduPilot
+	t.Log("Starting ArduPilot container")
+	container := testutil.Container{
+		Image:        "radarku/ardupilot-sitl",
+		ExposedPorts: []string{"5760"},
+		WaitingFor: wait.ForAll(
+			wait.ForListeningPort(nat.Port("5760")),
+		),
+	}
+	require.NoError(t, container.Start(), "failed to start ardupilot container")
+	defer container.Terminate()
+
+	t.Logf("ArduPilot is listening on port: %s", container.Ports["5760"])
+
+	// Setup the plugin
+	plugin := Mavlink{
+		URL:      "tcp://127.0.0.1:" + container.Ports["5760"],
+		SystemID: 254,
+	}
+	plugin.Log = testutil.Logger{}
+	require.NoError(t, plugin.Init())
+
+	// Collect the metrics and compare
+	var acc testutil.Accumulator
+	require.NoError(t, plugin.Start(&acc))
+
+	// Wait 5 seconds, then inspect metrics
+	time.Sleep(time.Second * 5)
+	require.NoError(t, plugin.Gather(&acc))
+	actual := acc.GetTelegrafMetrics()
+	plugin.Stop()
+
+	// Expect to have received more than 10 metrics
+	// The exact metrics received is non-deterministic because ArduPilot's
+	// startup may differ between runs, but should be on the order of 100
+	// metrics in 5 seconds. Not actually testing the content of metrics
+	// here; that is tested in TestMavlinkDecoding.
+	t.Logf("Received %d metrics", len(actual))
+	require.Greater(t, len(actual), 10)
path: plugins/inputs/mavlink/mavlink_test.go
user: login: chrisdalke
body: Perfect, thanks. that's a much better method of testing, updated.
created_at: 2025-04-23T20:59:42Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,268 @@
+package mavlink
+
+import (
+	"testing"
+	"time"
+
+	"github.com/bluenviron/gomavlib/v3"
+	"github.com/bluenviron/gomavlib/v3/pkg/dialects/common"
+	"github.com/bluenviron/gomavlib/v3/pkg/frame"
+	"github.com/docker/go-connections/nat"
+	"github.com/stretchr/testify/require"
+	"github.com/testcontainers/testcontainers-go/wait"
+
+	"github.com/influxdata/telegraf"
+	"github.com/influxdata/telegraf/filter"
+	"github.com/influxdata/telegraf/metric"
+	"github.com/influxdata/telegraf/testutil"
+)
+
+// Test that a serial port URL can be parsed (Linux)
+func TestParseSerialUrlLinux(t *testing.T) {
+	plugin := Mavlink{
+		URL: "serial:///dev/ttyACM0:115200",
+	}
+
+	err := plugin.Init()
+	require.NoError(t, err)
+
+	endpoint, ok := plugin.endpointConfig[0].(gomavlib.EndpointSerial)
+	require.True(t, ok)
+	require.Equal(t, "/dev/ttyACM0", endpoint.Device)
+	require.Equal(t, 115200, endpoint.Baud)
+}
+
+// Test that a serial port URL can be parsed (Windows)
+func TestParseSerialUrlWindows(t *testing.T) {
+	plugin := Mavlink{
+		URL: "serial://COM1:115200",
+	}
+
+	err := plugin.Init()
+	require.NoError(t, err)
+
+	endpoint, ok := plugin.endpointConfig[0].(gomavlib.EndpointSerial)
+	require.True(t, ok)
+	require.Equal(t, "COM1", endpoint.Device)
+	require.Equal(t, 115200, endpoint.Baud)
+}
+
+// Test that a UDP client URL can be parsed.
+func TestParseUDPClientUrl(t *testing.T) {
+	plugin := Mavlink{
+		URL: "udp://192.168.1.12:14550",
+	}
+
+	err := plugin.Init()
+	require.NoError(t, err)
+
+	endpoint, ok := plugin.endpointConfig[0].(gomavlib.EndpointUDPClient)
+	require.True(t, ok)
+	require.Equal(t, "192.168.1.12:14550", endpoint.Address)
+}
+
+// Test that a UDP server URL can be parsed.
+func TestParseUDPServerUrl(t *testing.T) {
+	plugin := Mavlink{
+		URL: "udp://:14540",
+	}
+
+	err := plugin.Init()
+	require.NoError(t, err)
+
+	endpoint, ok := plugin.endpointConfig[0].(gomavlib.EndpointUDPServer)
+	require.True(t, ok)
+	require.Equal(t, ":14540", endpoint.Address)
+}
+
+// Test that a TCP client URL can be parsed.
+func TestParseTCPClientUrl(t *testing.T) {
+	plugin := Mavlink{
+		URL: "tcp://192.168.1.12:14550",
+	}
+
+	err := plugin.Init()
+	require.NoError(t, err)
+
+	endpoint, ok := plugin.endpointConfig[0].(gomavlib.EndpointTCPClient)
+	require.True(t, ok)
+	require.Equal(t, "192.168.1.12:14550", endpoint.Address)
+}
path: plugins/inputs/mavlink/mavlink_test.go
user: login: chrisdalke
body: Updated!
created_at: 2025-04-23T21:00:30Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,251 @@
+//go:generate ../../../tools/readme_config_includer/generator
+package mavlink
+
+import (
+	"context"
+	_ "embed"
+	"fmt"
+	"net"
+	"net/url"
+	"reflect"
+	"strconv"
+	"strings"
+	"sync"
+	"time"
+
+	"github.com/bluenviron/gomavlib/v3"
+	"github.com/bluenviron/gomavlib/v3/pkg/dialects/ardupilotmega"
+	"github.com/bluenviron/gomavlib/v3/pkg/frame"
+
+	"github.com/influxdata/telegraf"
+	"github.com/influxdata/telegraf/filter"
+	"github.com/influxdata/telegraf/internal"
+	"github.com/influxdata/telegraf/metric"
+	"github.com/influxdata/telegraf/plugins/inputs"
+)
+
+type Mavlink struct {
+	URL                    string   `toml:"url"`
+	SystemID               uint8    `toml:"system_id"`
+	FilterPattern          []string `toml:"filter"`
+	StreamRequestDisable   bool     `toml:"stream_request_disable"`
+	StreamRequestFrequency uint16   `toml:"stream_request_frequency"`
+
+	Log telegraf.Logger `toml:"-"`
+
+	filter         filter.Filter
+	connection     *gomavlib.Node
+	endpointConfig []gomavlib.EndpointConf
+	cancel         context.CancelFunc
+	wg             sync.WaitGroup
+}
+
+//go:embed sample.conf
+var sampleConfig string
+
+func (*Mavlink) SampleConfig() string {
+	return sampleConfig
+}
+
+func (m *Mavlink) Init() error {
+	// Set default values
+	if len(m.URL) == 0 {
+		m.URL = "tcp://127.0.0.1:5760"
+	}
+
+	if !m.StreamRequestDisable && m.StreamRequestFrequency == 0 {
+		m.StreamRequestFrequency = 4
+	}
+
+	// Parse out the Mavlink endpoint.
+	// Try to parse the URL
+	u, err := url.Parse(m.URL)
+	if err != nil {
+		return fmt.Errorf("invalid url: %w", err)
+	}
+
+	switch u.Scheme {
+	case "serial":
+		// Serial client
+		// Parse serial URL by hand, because it is not a compliant URL.
+		baudRate := 57600
+		device, rate, found := strings.Cut(strings.TrimPrefix(m.URL, "serial://"), ":")
+		if found {
+			r, err := strconv.Atoi(rate)
+			if err != nil {
+				return fmt.Errorf("serial baud rate not valid: %w", err)
+			}
+			baudRate = r
+		}
+
+		m.endpointConfig = []gomavlib.EndpointConf{
+			gomavlib.EndpointSerial{
+				Device: device,
+				Baud:   baudRate,
+			},
+		}
+	case "tcp":
+		// 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 mavlink TCP port if a port was not provided or was invalid.
+			host = u.Host
+			port = "14540"
+		}
+
+		if len(host) > 0 {
+			m.endpointConfig = []gomavlib.EndpointConf{
+				gomavlib.EndpointTCPClient{
+					Address: host + ":" + port,
+				},
+			}
+		} else {
+			m.endpointConfig = []gomavlib.EndpointConf{
+				gomavlib.EndpointTCPServer{
+					Address: ":" + port,
+				},
+			}
+		}
+
+	case "udp":
+		// 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 mavlink TCP port if a port was not provided or was invalid.
+			host = u.Host
+			port = "5760"
+		}
+
+		if len(host) > 0 {
+			m.endpointConfig = []gomavlib.EndpointConf{
+				gomavlib.EndpointUDPClient{
+					Address: host + ":" + port,
+				},
+			}
+		} else {
+			m.endpointConfig = []gomavlib.EndpointConf{
+				gomavlib.EndpointUDPServer{
+					Address: ":" + port,
+				},
+			}
+		}
+
+	default:
+		return fmt.Errorf("unknown scheme %q", u.Scheme)
+	}
+
+	// Compile filter
+	m.filter, err = filter.Compile(m.FilterPattern)
+	if err != nil {
+		return fmt.Errorf("compiling filter failed: %w", err)
+	}
+
+	return nil
+}
+
+// Convert a Mavlink frame into a struct containing Metric data.
+func convertFrameToMetric(frm frame.Frame, msgFilter filter.Filter) telegraf.Metric {
+	m := frm.GetMessage()
+	t := reflect.TypeOf(m)
+	v := reflect.ValueOf(m)
+	if t.Kind() == reflect.Ptr {
+		t = t.Elem()
+		v = v.Elem()
+	}
+
+	name := internal.SnakeCase(strings.TrimPrefix(t.Name(), "Message"))
+
+	if msgFilter != nil && !msgFilter.Match(name) {
+		return nil
+	}
+
+	tags := map[string]string{
+		"sys_id": strconv.FormatUint(uint64(frm.GetSystemID()), 10),
+	}
+	fields := make(map[string]interface{}, t.NumField())
+
+	for i := 0; i < t.NumField(); i++ {
+		field := t.Field(i)
+		value := v.Field(i)
+		fieldName := internal.SnakeCase(field.Name)
+
+		if value.Kind() == reflect.Slice || value.Kind() == reflect.Array {
+			// Split array types into individual primitive values
+			// with _<n> appended to the key
+			for j := 0; j < value.Len(); j++ {
+				indexedFieldName := fmt.Sprintf("%s_%d", fieldName, j+1)
+				fields[indexedFieldName] = value.Index(j).Interface()
+			}
+		} else {
+			fields[fieldName] = value.Interface()
+		}
+	}
+
+	return metric.New(name, tags, fields, time.Now())
+}
+
+func (m *Mavlink) Start(acc telegraf.Accumulator) error {
+	// Start MAVLink endpoint
+	connection, err := gomavlib.NewNode(gomavlib.NodeConf{
+		Endpoints:              m.endpointConfig,
+		Dialect:                ardupilotmega.Dialect,
+		OutVersion:             gomavlib.V2,
+		OutSystemID:            m.SystemID,
+		StreamRequestEnable:    !m.StreamRequestDisable,
+		StreamRequestFrequency: int(m.StreamRequestFrequency),
+	})
+	if err != nil {
+		return &internal.StartupError{
+			Err:   fmt.Errorf("connecting to mavlink endpoint %s failed: %w", m.URL, err),
+			Retry: true,
+		}
+	}
+	m.connection = connection
+	ctx, cancelFunc := context.WithCancel(context.Background())
+	m.cancel = cancelFunc
+
+	// Start routine to connect to Mavlink and stream out data async
+	m.wg.Add(1)
+	go func(ctx context.Context) {
+		defer m.connection.Close()
+		defer m.wg.Done()
+
+		for {
+			select {
+			case <-ctx.Done():
+				return
+			case evt := <-m.connection.Events():
+				switch evt := evt.(type) {
path: plugins/inputs/mavlink/mavlink.go
user: login: chrisdalke
body: Agreed. I added a trace message for the other relevant types (eg `EventParseError`) and a default case which prints the unhandled event type at trace level.
created_at: 2025-04-23T21:19:04Z
author_association: CONTRIBUTOR
diff_hunk: @@ -2084,8 +2090,11 @@ github.com/pion/dtls/v2 v2.2.12 h1:KP7H5/c1EiVAAKUmXyCzPiQe5+bCJrpOeKg/L05dunk=
 github.com/pion/dtls/v2 v2.2.12/go.mod h1:d9SYc9fch0CqK90mRk1dC7AkzzpwJj6u2GU3u+9pqFE=
 github.com/pion/logging v0.2.2 h1:M9+AIj/+pxNsDfAT64+MAVgJO0rsyLnoJKCqf//DoeY=
 github.com/pion/logging v0.2.2/go.mod h1:k0/tDVsRCX2Mb2ZEmTqNa7CWsQPc+YYCB7Q+5pahoms=
-github.com/pion/transport/v2 v2.2.4 h1:41JJK6DZQYSeVLxILA2+F4ZkKb4Xd/tFJZRFZQ9QAlo=
 github.com/pion/transport/v2 v2.2.4/go.mod h1:q2U/tf9FEfnSBGSW6w5Qp5PFWRLRj3NjLhCCgpRK4p0=
+github.com/pion/transport/v2 v2.2.10 h1:ucLBLE8nuxiHfvkFKnkDQRYWYfp8ejf4YBOPfaQpw6Q=
+github.com/pion/transport/v2 v2.2.10/go.mod h1:sq1kSLWs+cHW9E+2fJP95QudkzbK7wscs8yYgQToO5E=
+github.com/pion/transport/v3 v3.0.6 h1:k1mQU06bmmX143qSWgXFqSH1KUJceQvIUuVH/K5ELWw=
+github.com/pion/transport/v3 v3.0.6/go.mod h1:HvJr2N/JwNJAfipsRleqwFoR3t/pWyHeZUs89v3+t5s=
path: go.sum
user: login: Hipska
body: Adding v2.2.10 and v3.0.6 at the same time? 🤔 
created_at: 2025-04-24T07:29:57Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,95 @@
+# MavLink Input Plugin
+
+This plugin collects metrics from [MavLink][mavlink]-compatible flight
+controllers such as [ArduPilot][ardupilot] or [PX4][px4] to live ingest
+flight metrics from unmanned systems (drones, planes, boats, etc.)
+Currently the ArduPilot-specific Mavlink dialect is used, check the
+[Mavlink documentation][mavlink_docs] for more details and the various
+messages available.
+
+> [!WARNING]
+> This plugin potentially generates a large amount of data! Please use metric
+> filters to limit the amount of metrics produced!
path: plugins/inputs/mavlink/README.md
user: login: Hipska
body: @chrisdalke "metric filters" could probably better be a link to `../../../docs/CONFIGURATION.md#metric-filtering`.

@srebhan Does this also exist on other inputs? Maybe convert it into a to be included template?
created_at: 2025-04-24T07:34:48Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,95 @@
+# MavLink Input Plugin
+
+This plugin collects metrics from [MavLink][mavlink]-compatible flight
+controllers such as [ArduPilot][ardupilot] or [PX4][px4] to live ingest
+flight metrics from unmanned systems (drones, planes, boats, etc.)
+Currently the ArduPilot-specific Mavlink dialect is used, check the
+[Mavlink documentation][mavlink_docs] for more details and the various
+messages available.
+
+> [!WARNING]
+> This plugin potentially generates a large amount of data! Please use metric
+> filters to limit the amount of metrics produced!
+
+⭐ Telegraf v1.35.0
+🏷️ iot
+💻 all
+
+[mavlink]: https://mavlink.io/
+[ardupilot]: https://ardupilot.org/
+[px4]: https://px4.io/
+[mavlink_docs]: https://mavlink.io/en/messages/ardupilotmega.html
+
+## Global configuration options <!-- @/docs/includes/plugin_config.md -->
+
+In addition to the plugin-specific configuration settings, plugins support
+additional global and plugin configuration settings. These settings are used to
+modify metrics, tags, and field or create aliases and configure ordering, etc.
+See the [CONFIGURATION.md][CONFIGURATION.md] for more details.
+
+[CONFIGURATION.md]: ../../../docs/CONFIGURATION.md#plugins
+
+## Configuration
+
+```toml @sample.conf
+# Read metrics from a Mavlink flight controller.
+[[inputs.mavlink]]
+  ## Flight controller URL supporting serial port, UDP and TCP connections.
+  ## Options are documented at
+  ##   https://mavsdk.mavlink.io/v1.4/en/cpp/guide/connections.html.
+  ##
+  ## Examples:
+  ## - Serial port: serial:///dev/ttyACM0:57600
+  ## - TCP client:  tcp://192.168.1.12:5760
+  ## - UDP client:  udp://192.168.1.12:14550
+  ## - TCP server:  udp://:5760
+  ## - UDP server:  udp://:14550
+  # url = "tcp://127.0.0.1:5760"
+
+  ## Filter to specific messages. Only the messages in this list will be parsed.
+  ## If blank or unset, all messages will be accepted. Glob syntax is accepted.
+  ## Each message in this list should be lowercase camel_case, with "message_"
+  ## prefix removed, eg: "global_position_int", "attitude"
+  # filter = []
+
+  ## Mavlink system ID for Telegraf
+  ## Only used if the mavlink plugin is sending messages, eg.
+  ## when `stream_request_disable` is false (see below.)
+  # system_id = 254
+
+  ## Determines whether the plugin sends requests to subscribe to data.
+  ## In mavlink, stream rates must be configured before data is received.
+  ## This config item sets the rate in Hz, with 0 disabling the request.
+  ## 
+  ## This frequency should be set to 0 if your software already controls the 
+  ## rates using REQUEST_DATA_STREAM or MAV_CMD_SET_MESSAGE_INTERVAL
+  ## (See https://mavlink.io/en/mavgen_python/howto_requestmessages.html)
+  # stream_request_frequency = 4
+```
+
+## Metrics
+
+Each supported Mavlink message translates to one metric group, and fields
+on the Mavlink message are converted to fields in telegraf.
+
+The name of the Mavlink message is translated into lowercase and any
+leading text `message_` is dropped.
+
+For example, the message [ATTITUDE](https://mavlink.io/en/messages/common.html)
+will become an `attitude` metric, with all fields copied from its Mavlink
+message definition.
path: plugins/inputs/mavlink/README.md
user: login: Hipska
body: ```suggestion
For example, the message [ATTITUDE](https://mavlink.io/en/messages/common.html#ATTITUDE)
will become an `attitude` metric, with all fields copied from its Mavlink
message definition.
```
created_at: 2025-04-24T07:37:55Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,33 @@
+# Read metrics from a Mavlink flight controller.
+[[inputs.mavlink]]
+  ## Flight controller URL supporting serial port, UDP and TCP connections.
+  ## Options are documented at
+  ##   https://mavsdk.mavlink.io/v1.4/en/cpp/guide/connections.html.
+  ##
+  ## Examples:
+  ## - Serial port: serial:///dev/ttyACM0:57600
+  ## - TCP client:  tcp://192.168.1.12:5760
+  ## - UDP client:  udp://192.168.1.12:14550
+  ## - TCP server:  udp://:5760
path: plugins/inputs/mavlink/sample.conf
user: login: Hipska
body: ```suggestion
  ## - TCP server:  tcp://:5760
```
created_at: 2025-04-24T07:38:53Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,33 @@
+# Read metrics from a Mavlink flight controller.
+[[inputs.mavlink]]
+  ## Flight controller URL supporting serial port, UDP and TCP connections.
+  ## Options are documented at
+  ##   https://mavsdk.mavlink.io/v1.4/en/cpp/guide/connections.html.
+  ##
+  ## Examples:
+  ## - Serial port: serial:///dev/ttyACM0:57600
+  ## - TCP client:  tcp://192.168.1.12:5760
+  ## - UDP client:  udp://192.168.1.12:14550
+  ## - TCP server:  udp://:5760
+  ## - UDP server:  udp://:14550
+  # url = "tcp://127.0.0.1:5760"
+
+  ## Filter to specific messages. Only the messages in this list will be parsed.
+  ## If blank or unset, all messages will be accepted. Glob syntax is accepted.
+  ## Each message in this list should be lowercase camel_case, with "message_"
+  ## prefix removed, eg: "global_position_int", "attitude"
+  # filter = []
+
+  ## Mavlink system ID for Telegraf
+  ## Only used if the mavlink plugin is sending messages, eg.
+  ## when `stream_request_disable` is false (see below.)
+  # system_id = 254
path: plugins/inputs/mavlink/sample.conf
user: login: Hipska
body: ```suggestion
  ## Mavlink system ID for Telegraf. Only used if the mavlink plugin is sending
  ## messages, eg. when `stream_request_frequency` is 0 (see below.)
  # system_id = 254
```
created_at: 2025-04-24T07:42:28Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,95 @@
+# MavLink Input Plugin
+
+This plugin collects metrics from [MavLink][mavlink]-compatible flight
+controllers such as [ArduPilot][ardupilot] or [PX4][px4] to live ingest
+flight metrics from unmanned systems (drones, planes, boats, etc.)
+Currently the ArduPilot-specific Mavlink dialect is used, check the
+[Mavlink documentation][mavlink_docs] for more details and the various
+messages available.
+
+> [!WARNING]
+> This plugin potentially generates a large amount of data! Please use metric
+> filters to limit the amount of metrics produced!
path: plugins/inputs/mavlink/README.md
user: login: Hipska
body: Or did you meant the `filter` plugin config option? Then the wording should be more clear, as it can be confused with the telegraf included metric filtering functionality.
created_at: 2025-04-24T07:47:47Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,251 @@
+//go:generate ../../../tools/readme_config_includer/generator
+package mavlink
+
+import (
+	"context"
+	_ "embed"
+	"fmt"
+	"net"
+	"net/url"
+	"reflect"
+	"strconv"
+	"strings"
+	"sync"
+	"time"
+
+	"github.com/bluenviron/gomavlib/v3"
+	"github.com/bluenviron/gomavlib/v3/pkg/dialects/ardupilotmega"
+	"github.com/bluenviron/gomavlib/v3/pkg/frame"
+
+	"github.com/influxdata/telegraf"
+	"github.com/influxdata/telegraf/filter"
+	"github.com/influxdata/telegraf/internal"
+	"github.com/influxdata/telegraf/metric"
+	"github.com/influxdata/telegraf/plugins/inputs"
+)
+
+type Mavlink struct {
+	URL                    string   `toml:"url"`
+	SystemID               uint8    `toml:"system_id"`
+	FilterPattern          []string `toml:"filter"`
+	StreamRequestDisable   bool     `toml:"stream_request_disable"`
+	StreamRequestFrequency uint16   `toml:"stream_request_frequency"`
+
+	Log telegraf.Logger `toml:"-"`
+
+	filter         filter.Filter
+	connection     *gomavlib.Node
+	endpointConfig []gomavlib.EndpointConf
path: plugins/inputs/mavlink/mavlink.go
user: login: Hipska
body: It indeed looks there's always only one item added. (Unless it would make sense to have `URL` to become a slice as well?)
created_at: 2025-04-24T07:49:08Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,253 @@
+//go:generate ../../../tools/readme_config_includer/generator
+package mavlink
+
+import (
+	"context"
+	_ "embed"
+	"fmt"
+	"net"
+	"net/url"
+	"reflect"
+	"strconv"
+	"strings"
+	"sync"
+	"time"
+
+	"github.com/bluenviron/gomavlib/v3"
+	"github.com/bluenviron/gomavlib/v3/pkg/dialects/ardupilotmega"
+	"github.com/bluenviron/gomavlib/v3/pkg/frame"
+
+	"github.com/influxdata/telegraf"
+	"github.com/influxdata/telegraf/filter"
+	"github.com/influxdata/telegraf/internal"
+	"github.com/influxdata/telegraf/metric"
+	"github.com/influxdata/telegraf/plugins/inputs"
+)
+
+type Mavlink struct {
+	URL                    string   `toml:"url"`
+	SystemID               uint8    `toml:"system_id"`
+	FilterPattern          []string `toml:"filter"`
+	StreamRequestFrequency uint16   `toml:"stream_request_frequency"`
+
+	Log telegraf.Logger `toml:"-"`
+
+	filter         filter.Filter
+	connection     *gomavlib.Node
+	endpointConfig []gomavlib.EndpointConf
+	cancel         context.CancelFunc
+	wg             sync.WaitGroup
+}
+
+//go:embed sample.conf
+var sampleConfig string
+
+func (*Mavlink) SampleConfig() string {
+	return sampleConfig
+}
+
+func (m *Mavlink) Init() error {
+	// Set default values
+	if m.URL == "" {
+		m.URL = "tcp://127.0.0.1:5760"
+	}
+
+	// Parse out the Mavlink endpoint.
+	// Try to parse the URL
+	u, err := url.Parse(m.URL)
+	if err != nil {
+		return fmt.Errorf("invalid url: %w", err)
+	}
+
+	switch u.Scheme {
+	case "serial":
+		// Serial client
+		// Parse serial URL by hand, because it is not a compliant URL.
+		baudRate := 57600
+		device, rate, found := strings.Cut(strings.TrimPrefix(m.URL, "serial://"), ":")
+		if found {
+			r, err := strconv.Atoi(rate)
+			if err != nil {
+				return fmt.Errorf("serial baud rate not valid: %w", err)
+			}
+			baudRate = r
+		}
+
+		m.endpointConfig = []gomavlib.EndpointConf{
+			gomavlib.EndpointSerial{
+				Device: device,
+				Baud:   baudRate,
+			},
+		}
+	case "tcp":
+		// 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 mavlink TCP port if a port was not provided or was invalid.
+			host = u.Host
+			port = "5760"
+		}
path: plugins/inputs/mavlink/mavlink.go
user: login: Hipska
body: Maybe use `u.Hostname()` and `u.Port()` instead?
created_at: 2025-04-24T08:12:09Z
author_association: CONTRIBUTOR
diff_hunk: @@ -2084,8 +2090,11 @@ github.com/pion/dtls/v2 v2.2.12 h1:KP7H5/c1EiVAAKUmXyCzPiQe5+bCJrpOeKg/L05dunk=
 github.com/pion/dtls/v2 v2.2.12/go.mod h1:d9SYc9fch0CqK90mRk1dC7AkzzpwJj6u2GU3u+9pqFE=
 github.com/pion/logging v0.2.2 h1:M9+AIj/+pxNsDfAT64+MAVgJO0rsyLnoJKCqf//DoeY=
 github.com/pion/logging v0.2.2/go.mod h1:k0/tDVsRCX2Mb2ZEmTqNa7CWsQPc+YYCB7Q+5pahoms=
-github.com/pion/transport/v2 v2.2.4 h1:41JJK6DZQYSeVLxILA2+F4ZkKb4Xd/tFJZRFZQ9QAlo=
 github.com/pion/transport/v2 v2.2.4/go.mod h1:q2U/tf9FEfnSBGSW6w5Qp5PFWRLRj3NjLhCCgpRK4p0=
+github.com/pion/transport/v2 v2.2.10 h1:ucLBLE8nuxiHfvkFKnkDQRYWYfp8ejf4YBOPfaQpw6Q=
+github.com/pion/transport/v2 v2.2.10/go.mod h1:sq1kSLWs+cHW9E+2fJP95QudkzbK7wscs8yYgQToO5E=
+github.com/pion/transport/v3 v3.0.6 h1:k1mQU06bmmX143qSWgXFqSH1KUJceQvIUuVH/K5ELWw=
+github.com/pion/transport/v3 v3.0.6/go.mod h1:HvJr2N/JwNJAfipsRleqwFoR3t/pWyHeZUs89v3+t5s=
path: go.sum
user: login: chrisdalke
body: I'm not sure how this happened. Possible that it's from running `make tidy` on both Linux and Mac and then merging the changes? Either way I'll fix this
created_at: 2025-04-24T13:48:17Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,95 @@
+# MavLink Input Plugin
+
+This plugin collects metrics from [MavLink][mavlink]-compatible flight
+controllers such as [ArduPilot][ardupilot] or [PX4][px4] to live ingest
+flight metrics from unmanned systems (drones, planes, boats, etc.)
+Currently the ArduPilot-specific Mavlink dialect is used, check the
+[Mavlink documentation][mavlink_docs] for more details and the various
+messages available.
+
+> [!WARNING]
+> This plugin potentially generates a large amount of data! Please use metric
+> filters to limit the amount of metrics produced!
path: plugins/inputs/mavlink/README.md
user: login: chrisdalke
body: I did mean telegraf metric filtering. What I was trying to suggest here is that because this generates potentially 100s-1000s of metrics per second, certain output plugins may struggle to keep up or fill your storage very fast.

Maybe this is obvious and should be removed as it's on the user to evaluate whether the output plugin is capable of handling the volume produced by input plugins?
created_at: 2025-04-24T13:50:37Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,251 @@
+//go:generate ../../../tools/readme_config_includer/generator
+package mavlink
+
+import (
+	"context"
+	_ "embed"
+	"fmt"
+	"net"
+	"net/url"
+	"reflect"
+	"strconv"
+	"strings"
+	"sync"
+	"time"
+
+	"github.com/bluenviron/gomavlib/v3"
+	"github.com/bluenviron/gomavlib/v3/pkg/dialects/ardupilotmega"
+	"github.com/bluenviron/gomavlib/v3/pkg/frame"
+
+	"github.com/influxdata/telegraf"
+	"github.com/influxdata/telegraf/filter"
+	"github.com/influxdata/telegraf/internal"
+	"github.com/influxdata/telegraf/metric"
+	"github.com/influxdata/telegraf/plugins/inputs"
+)
+
+type Mavlink struct {
+	URL                    string   `toml:"url"`
+	SystemID               uint8    `toml:"system_id"`
+	FilterPattern          []string `toml:"filter"`
+	StreamRequestDisable   bool     `toml:"stream_request_disable"`
+	StreamRequestFrequency uint16   `toml:"stream_request_frequency"`
+
+	Log telegraf.Logger `toml:"-"`
+
+	filter         filter.Filter
+	connection     *gomavlib.Node
+	endpointConfig []gomavlib.EndpointConf
path: plugins/inputs/mavlink/mavlink.go
user: login: chrisdalke
body: Honestly it's just because I was struggling with the Go syntax, I'll fix this now.

For what it's worth it is technically possible to use multiple endpoints. This is common in drone world where you may have a serial connection, TCP, and UDP connection all at once, networking together a system in multiple ways.
created_at: 2025-04-24T13:52:28Z
author_association: CONTRIBUTOR
diff_hunk: @@ -2084,8 +2090,11 @@ github.com/pion/dtls/v2 v2.2.12 h1:KP7H5/c1EiVAAKUmXyCzPiQe5+bCJrpOeKg/L05dunk=
 github.com/pion/dtls/v2 v2.2.12/go.mod h1:d9SYc9fch0CqK90mRk1dC7AkzzpwJj6u2GU3u+9pqFE=
 github.com/pion/logging v0.2.2 h1:M9+AIj/+pxNsDfAT64+MAVgJO0rsyLnoJKCqf//DoeY=
 github.com/pion/logging v0.2.2/go.mod h1:k0/tDVsRCX2Mb2ZEmTqNa7CWsQPc+YYCB7Q+5pahoms=
-github.com/pion/transport/v2 v2.2.4 h1:41JJK6DZQYSeVLxILA2+F4ZkKb4Xd/tFJZRFZQ9QAlo=
 github.com/pion/transport/v2 v2.2.4/go.mod h1:q2U/tf9FEfnSBGSW6w5Qp5PFWRLRj3NjLhCCgpRK4p0=
+github.com/pion/transport/v2 v2.2.10 h1:ucLBLE8nuxiHfvkFKnkDQRYWYfp8ejf4YBOPfaQpw6Q=
+github.com/pion/transport/v2 v2.2.10/go.mod h1:sq1kSLWs+cHW9E+2fJP95QudkzbK7wscs8yYgQToO5E=
+github.com/pion/transport/v3 v3.0.6 h1:k1mQU06bmmX143qSWgXFqSH1KUJceQvIUuVH/K5ELWw=
+github.com/pion/transport/v3 v3.0.6/go.mod h1:HvJr2N/JwNJAfipsRleqwFoR3t/pWyHeZUs89v3+t5s=
path: go.sum
user: login: chrisdalke
body: After investigation, this is correct. gomavlib uses `transport/v2` as a direct dependency, but also `transport/v3` as an indirect dependency: https://github.com/bluenviron/gomavlib/blob/a8d7c644e4dbe8832ff0e683deb3b4de44eaebe8/go.mod#L8
created_at: 2025-04-24T15:22:06Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,251 @@
+//go:generate ../../../tools/readme_config_includer/generator
+package mavlink
+
+import (
+	"context"
+	_ "embed"
+	"fmt"
+	"net"
+	"net/url"
+	"reflect"
+	"strconv"
+	"strings"
+	"sync"
+	"time"
+
+	"github.com/bluenviron/gomavlib/v3"
+	"github.com/bluenviron/gomavlib/v3/pkg/dialects/ardupilotmega"
+	"github.com/bluenviron/gomavlib/v3/pkg/frame"
+
+	"github.com/influxdata/telegraf"
+	"github.com/influxdata/telegraf/filter"
+	"github.com/influxdata/telegraf/internal"
+	"github.com/influxdata/telegraf/metric"
+	"github.com/influxdata/telegraf/plugins/inputs"
+)
+
+type Mavlink struct {
+	URL                    string   `toml:"url"`
+	SystemID               uint8    `toml:"system_id"`
+	FilterPattern          []string `toml:"filter"`
+	StreamRequestDisable   bool     `toml:"stream_request_disable"`
+	StreamRequestFrequency uint16   `toml:"stream_request_frequency"`
+
+	Log telegraf.Logger `toml:"-"`
+
+	filter         filter.Filter
+	connection     *gomavlib.Node
+	endpointConfig []gomavlib.EndpointConf
path: plugins/inputs/mavlink/mavlink.go
user: login: chrisdalke
body: Fix this so it's just one config instead of a slice. 
created_at: 2025-04-24T15:27:41Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,253 @@
+//go:generate ../../../tools/readme_config_includer/generator
+package mavlink
+
+import (
+	"context"
+	_ "embed"
+	"fmt"
+	"net"
+	"net/url"
+	"reflect"
+	"strconv"
+	"strings"
+	"sync"
+	"time"
+
+	"github.com/bluenviron/gomavlib/v3"
+	"github.com/bluenviron/gomavlib/v3/pkg/dialects/ardupilotmega"
+	"github.com/bluenviron/gomavlib/v3/pkg/frame"
+
+	"github.com/influxdata/telegraf"
+	"github.com/influxdata/telegraf/filter"
+	"github.com/influxdata/telegraf/internal"
+	"github.com/influxdata/telegraf/metric"
+	"github.com/influxdata/telegraf/plugins/inputs"
+)
+
+type Mavlink struct {
+	URL                    string   `toml:"url"`
+	SystemID               uint8    `toml:"system_id"`
+	FilterPattern          []string `toml:"filter"`
+	StreamRequestFrequency uint16   `toml:"stream_request_frequency"`
+
+	Log telegraf.Logger `toml:"-"`
+
+	filter         filter.Filter
+	connection     *gomavlib.Node
+	endpointConfig []gomavlib.EndpointConf
+	cancel         context.CancelFunc
+	wg             sync.WaitGroup
+}
+
+//go:embed sample.conf
+var sampleConfig string
+
+func (*Mavlink) SampleConfig() string {
+	return sampleConfig
+}
+
+func (m *Mavlink) Init() error {
+	// Set default values
+	if m.URL == "" {
+		m.URL = "tcp://127.0.0.1:5760"
+	}
+
+	// Parse out the Mavlink endpoint.
+	// Try to parse the URL
+	u, err := url.Parse(m.URL)
+	if err != nil {
+		return fmt.Errorf("invalid url: %w", err)
+	}
+
+	switch u.Scheme {
+	case "serial":
+		// Serial client
+		// Parse serial URL by hand, because it is not a compliant URL.
+		baudRate := 57600
+		device, rate, found := strings.Cut(strings.TrimPrefix(m.URL, "serial://"), ":")
+		if found {
+			r, err := strconv.Atoi(rate)
+			if err != nil {
+				return fmt.Errorf("serial baud rate not valid: %w", err)
+			}
+			baudRate = r
+		}
+
+		m.endpointConfig = []gomavlib.EndpointConf{
+			gomavlib.EndpointSerial{
+				Device: device,
+				Baud:   baudRate,
+			},
+		}
+	case "tcp":
+		// 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 mavlink TCP port if a port was not provided or was invalid.
+			host = u.Host
+			port = "5760"
+		}
path: plugins/inputs/mavlink/mavlink.go
user: login: chrisdalke
body: Good idea, that shortens this a little.
created_at: 2025-04-24T15:36:01Z
author_association: CONTRIBUTOR
diff_hunk: @@ -2084,8 +2090,11 @@ github.com/pion/dtls/v2 v2.2.12 h1:KP7H5/c1EiVAAKUmXyCzPiQe5+bCJrpOeKg/L05dunk=
 github.com/pion/dtls/v2 v2.2.12/go.mod h1:d9SYc9fch0CqK90mRk1dC7AkzzpwJj6u2GU3u+9pqFE=
 github.com/pion/logging v0.2.2 h1:M9+AIj/+pxNsDfAT64+MAVgJO0rsyLnoJKCqf//DoeY=
 github.com/pion/logging v0.2.2/go.mod h1:k0/tDVsRCX2Mb2ZEmTqNa7CWsQPc+YYCB7Q+5pahoms=
-github.com/pion/transport/v2 v2.2.4 h1:41JJK6DZQYSeVLxILA2+F4ZkKb4Xd/tFJZRFZQ9QAlo=
 github.com/pion/transport/v2 v2.2.4/go.mod h1:q2U/tf9FEfnSBGSW6w5Qp5PFWRLRj3NjLhCCgpRK4p0=
+github.com/pion/transport/v2 v2.2.10 h1:ucLBLE8nuxiHfvkFKnkDQRYWYfp8ejf4YBOPfaQpw6Q=
+github.com/pion/transport/v2 v2.2.10/go.mod h1:sq1kSLWs+cHW9E+2fJP95QudkzbK7wscs8yYgQToO5E=
+github.com/pion/transport/v3 v3.0.6 h1:k1mQU06bmmX143qSWgXFqSH1KUJceQvIUuVH/K5ELWw=
+github.com/pion/transport/v3 v3.0.6/go.mod h1:HvJr2N/JwNJAfipsRleqwFoR3t/pWyHeZUs89v3+t5s=
path: go.sum
user: login: Hipska
body: You're right, I missed the `v2` and `v3` suffix. Can be closed.
created_at: 2025-04-24T15:46:21Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,95 @@
+# MavLink Input Plugin
+
+This plugin collects metrics from [MavLink][mavlink]-compatible flight
+controllers such as [ArduPilot][ardupilot] or [PX4][px4] to live ingest
+flight metrics from unmanned systems (drones, planes, boats, etc.)
+Currently the ArduPilot-specific Mavlink dialect is used, check the
+[Mavlink documentation][mavlink_docs] for more details and the various
+messages available.
+
+> [!WARNING]
+> This plugin potentially generates a large amount of data! Please use metric
+> filters to limit the amount of metrics produced!
path: plugins/inputs/mavlink/README.md
user: login: Hipska
body: Yeah, I would leave that up to the users. There are also other plugins that can generate lots of metrics..
created_at: 2025-04-24T15:47:34Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,238 @@
+//go:generate ../../../tools/readme_config_includer/generator
+package mavlink
+
+import (
+	"context"
+	_ "embed"
+	"fmt"
+	"net/url"
+	"reflect"
+	"strconv"
+	"strings"
+	"sync"
+	"time"
+
+	"github.com/bluenviron/gomavlib/v3"
+	"github.com/bluenviron/gomavlib/v3/pkg/dialects/ardupilotmega"
+	"github.com/bluenviron/gomavlib/v3/pkg/frame"
+
+	"github.com/influxdata/telegraf"
+	"github.com/influxdata/telegraf/filter"
+	"github.com/influxdata/telegraf/internal"
+	"github.com/influxdata/telegraf/metric"
+	"github.com/influxdata/telegraf/plugins/inputs"
+)
+
+type Mavlink struct {
+	URL                    string   `toml:"url"`
+	SystemID               uint8    `toml:"system_id"`
+	FilterPattern          []string `toml:"filter"`
+	StreamRequestFrequency uint16   `toml:"stream_request_frequency"`
+
+	Log telegraf.Logger `toml:"-"`
+
+	filter         filter.Filter
+	connection     *gomavlib.Node
+	endpointConfig gomavlib.EndpointConf
+	cancel         context.CancelFunc
+	wg             sync.WaitGroup
+}
+
+//go:embed sample.conf
+var sampleConfig string
+
+func (*Mavlink) SampleConfig() string {
+	return sampleConfig
+}
+
+func (m *Mavlink) Init() error {
+	// Set default values
+	if m.URL == "" {
+		m.URL = "tcp://127.0.0.1:5760"
+	}
+
+	// Parse out the Mavlink endpoint.
+	// Try to parse the URL
+	u, err := url.Parse(m.URL)
+	if err != nil {
+		return fmt.Errorf("invalid url: %w", err)
+	}
+
+	switch u.Scheme {
+	case "serial":
+		// Serial client
+		// Parse serial URL by hand, because it is not a compliant URL.
+		baudRate := 57600
+		device, rate, found := strings.Cut(strings.TrimPrefix(m.URL, "serial://"), ":")
+		if found {
+			r, err := strconv.Atoi(rate)
+			if err != nil {
+				return fmt.Errorf("serial baud rate not valid: %w", err)
+			}
+			baudRate = r
+		}
+
+		m.endpointConfig = gomavlib.EndpointSerial{
+			Device: device,
+			Baud:   baudRate,
+		}
+	case "tcp":
+		// Use default TCP port if it was not specified
+		port := u.Port()
+		if port == "" {
+			port = "5760"
+		}
+
+		if u.Hostname() == "" {
+			m.endpointConfig = gomavlib.EndpointTCPServer{
+				Address: "0.0.0.0:" + port,
+			}
+		} else {
+			m.endpointConfig = gomavlib.EndpointTCPClient{
+				Address: u.Hostname() + ":" + port,
path: plugins/inputs/mavlink/mavlink.go
user: login: Hipska
body: Maybe future-proof for IPv6 addresses:
```suggestion
				Address: net.JoinHostPort(u.Hostname(), port),
```
created_at: 2025-04-24T15:53:40Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,238 @@
+//go:generate ../../../tools/readme_config_includer/generator
+package mavlink
+
+import (
+	"context"
+	_ "embed"
+	"fmt"
+	"net/url"
+	"reflect"
+	"strconv"
+	"strings"
+	"sync"
+	"time"
+
+	"github.com/bluenviron/gomavlib/v3"
+	"github.com/bluenviron/gomavlib/v3/pkg/dialects/ardupilotmega"
+	"github.com/bluenviron/gomavlib/v3/pkg/frame"
+
+	"github.com/influxdata/telegraf"
+	"github.com/influxdata/telegraf/filter"
+	"github.com/influxdata/telegraf/internal"
+	"github.com/influxdata/telegraf/metric"
+	"github.com/influxdata/telegraf/plugins/inputs"
+)
+
+type Mavlink struct {
+	URL                    string   `toml:"url"`
+	SystemID               uint8    `toml:"system_id"`
+	FilterPattern          []string `toml:"filter"`
+	StreamRequestFrequency uint16   `toml:"stream_request_frequency"`
+
+	Log telegraf.Logger `toml:"-"`
+
+	filter         filter.Filter
+	connection     *gomavlib.Node
+	endpointConfig gomavlib.EndpointConf
+	cancel         context.CancelFunc
+	wg             sync.WaitGroup
+}
+
+//go:embed sample.conf
+var sampleConfig string
+
+func (*Mavlink) SampleConfig() string {
+	return sampleConfig
+}
+
+func (m *Mavlink) Init() error {
+	// Set default values
+	if m.URL == "" {
+		m.URL = "tcp://127.0.0.1:5760"
+	}
+
+	// Parse out the Mavlink endpoint.
+	// Try to parse the URL
+	u, err := url.Parse(m.URL)
+	if err != nil {
+		return fmt.Errorf("invalid url: %w", err)
+	}
+
+	switch u.Scheme {
+	case "serial":
+		// Serial client
+		// Parse serial URL by hand, because it is not a compliant URL.
+		baudRate := 57600
+		device, rate, found := strings.Cut(strings.TrimPrefix(m.URL, "serial://"), ":")
+		if found {
+			r, err := strconv.Atoi(rate)
+			if err != nil {
+				return fmt.Errorf("serial baud rate not valid: %w", err)
+			}
+			baudRate = r
+		}
+
+		m.endpointConfig = gomavlib.EndpointSerial{
+			Device: device,
+			Baud:   baudRate,
+		}
+	case "tcp":
+		// Use default TCP port if it was not specified
+		port := u.Port()
+		if port == "" {
+			port = "5760"
+		}
+
+		if u.Hostname() == "" {
+			m.endpointConfig = gomavlib.EndpointTCPServer{
+				Address: "0.0.0.0:" + port,
+			}
+		} else {
+			m.endpointConfig = gomavlib.EndpointTCPClient{
+				Address: u.Hostname() + ":" + port,
+			}
+		}
+
+	case "udp":
+		// Use default UDP port if it was not specified
+		port := u.Port()
+		if port == "" {
+			port = "14550"
+		}
+
+		if u.Hostname() == "" {
+			m.endpointConfig = gomavlib.EndpointUDPServer{
+				Address: "0.0.0.0:" + port,
+			}
+		} else {
+			m.endpointConfig = gomavlib.EndpointUDPClient{
+				Address: u.Hostname() + ":" + port,
path: plugins/inputs/mavlink/mavlink.go
user: login: Hipska
body: ```suggestion
				Address: net.JoinHostPort(u.Hostname(), port),
```
created_at: 2025-04-24T15:54:11Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,95 @@
+# MavLink Input Plugin
+
+This plugin collects metrics from [MavLink][mavlink]-compatible flight
+controllers such as [ArduPilot][ardupilot] or [PX4][px4] to live ingest
+flight metrics from unmanned systems (drones, planes, boats, etc.)
+Currently the ArduPilot-specific Mavlink dialect is used, check the
+[Mavlink documentation][mavlink_docs] for more details and the various
+messages available.
+
+> [!WARNING]
+> This plugin potentially generates a large amount of data! Please use metric
+> filters to limit the amount of metrics produced!
path: plugins/inputs/mavlink/README.md
user: login: chrisdalke
body: Ok, I've just removed this warning section entirely then.
created_at: 2025-04-24T16:09:31Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,95 @@
+# MavLink Input Plugin
+
+This plugin collects metrics from [MavLink][mavlink]-compatible flight
+controllers such as [ArduPilot][ardupilot] or [PX4][px4] to live ingest
+flight metrics from unmanned systems (drones, planes, boats, etc.)
+Currently the ArduPilot-specific Mavlink dialect is used, check the
+[Mavlink documentation][mavlink_docs] for more details and the various
+messages available.
+
+> [!WARNING]
+> This plugin potentially generates a large amount of data! Please use metric
+> filters to limit the amount of metrics produced!
path: plugins/inputs/mavlink/README.md
user: login: srebhan
body: @Hipska and @chrisdalke I would really like to see this warning being put back into place especially if the plugin produces large amounts of data. Link to the "metric-filtering" section as @Hipska suggested in the warning and also reference the `filter` option of the plugin! It's better to put out an additional warning than to put out too few. ;-)
created_at: 2025-05-05T11:43:29Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,90 @@
+# MavLink Input Plugin
+
+This plugin collects metrics from [MavLink][mavlink]-compatible flight
+controllers such as [ArduPilot][ardupilot] or [PX4][px4] to live ingest
+flight metrics from unmanned systems (drones, planes, boats, etc.)
+Currently the ArduPilot-specific Mavlink dialect is used, check the
+[Mavlink documentation][mavlink_docs] for more details and the various
+messages available.
+
+⭐ Telegraf v1.35.0
+🏷️ iot
+💻 all
+
+[mavlink]: https://mavlink.io/
+[ardupilot]: https://ardupilot.org/
+[px4]: https://px4.io/
+[mavlink_docs]: https://mavlink.io/en/messages/ardupilotmega.html
+
+## Global configuration options <!-- @/docs/includes/plugin_config.md -->
+
+In addition to the plugin-specific configuration settings, plugins support
+additional global and plugin configuration settings. These settings are used to
+modify metrics, tags, and field or create aliases and configure ordering, etc.
+See the [CONFIGURATION.md][CONFIGURATION.md] for more details.
+
+[CONFIGURATION.md]: ../../../docs/CONFIGURATION.md#plugins
+
+## Configuration
+
+```toml @sample.conf
+# Read metrics from a Mavlink flight controller.
+[[inputs.mavlink]]
+  ## Flight controller URL supporting serial port, UDP and TCP connections.
+  ## Options are documented at
+  ##   https://mavsdk.mavlink.io/v1.4/en/cpp/guide/connections.html.
+  ##
+  ## Examples:
+  ## - Serial port: serial:///dev/ttyACM0:57600
+  ## - TCP client:  tcp://192.168.1.12:5760
+  ## - UDP client:  udp://192.168.1.12:14550
+  ## - TCP server:  tcp://:5760
+  ## - UDP server:  udp://:14550
+  # url = "tcp://127.0.0.1:5760"
+
+  ## Filter to specific messages. Only the messages in this list will be parsed.
+  ## If blank or unset, all messages will be accepted. Glob syntax is accepted.
+  ## Each message in this list should be lowercase camel_case, with "message_"
+  ## prefix removed, eg: "global_position_int", "attitude"
+  # filter = []
+
+  ## Mavlink system ID for Telegraf. Only used if the mavlink plugin is sending 
+  ## messages, eg. when `stream_request_frequency` is 0 (see below.)
+  # system_id = 254
+
+  ## Determines whether the plugin sends requests to subscribe to data.
+  ## In mavlink, stream rates must be configured before data is received.
+  ## This config item sets the rate in Hz, with 0 disabling the request.
+  ## 
+  ## This frequency should be set to 0 if your software already controls the 
+  ## rates using REQUEST_DATA_STREAM or MAV_CMD_SET_MESSAGE_INTERVAL
+  ## (See https://mavlink.io/en/mavgen_python/howto_requestmessages.html)
+  # stream_request_frequency = 4
+```
+
+## Metrics
+
+Each supported Mavlink message translates to one metric group, and fields
+on the Mavlink message are converted to fields in telegraf.
+
+The name of the Mavlink message is translated into lowercase and any
+leading text `message_` is dropped.
+
+For example, the message
+[ATTITUDE](https://mavlink.io/en/messages/common.html#ATTITUDE) will become an
+`attitude` metric, with all fields copied from its Mavlink message definition.
path: plugins/inputs/mavlink/README.md
user: login: srebhan
body: Please use indirect links!
```suggestion
For example, the message [ATTITUDE][attitude] will become an
`attitude` metric, with all fields copied from its Mavlink message definition.

[attitude]: https://mavlink.io/en/messages/common.html#ATTITUDE
```

created_at: 2025-05-05T11:44:54Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,239 @@
+//go:generate ../../../tools/readme_config_includer/generator
+package mavlink
+
+import (
+	"context"
+	_ "embed"
+	"fmt"
+	"net"
+	"net/url"
+	"reflect"
+	"strconv"
+	"strings"
+	"sync"
+	"time"
+
+	"github.com/bluenviron/gomavlib/v3"
+	"github.com/bluenviron/gomavlib/v3/pkg/dialects/ardupilotmega"
+	"github.com/bluenviron/gomavlib/v3/pkg/frame"
+
+	"github.com/influxdata/telegraf"
+	"github.com/influxdata/telegraf/filter"
+	"github.com/influxdata/telegraf/internal"
+	"github.com/influxdata/telegraf/metric"
+	"github.com/influxdata/telegraf/plugins/inputs"
+)
+
+type Mavlink struct {
+	URL                    string   `toml:"url"`
+	SystemID               uint8    `toml:"system_id"`
+	FilterPattern          []string `toml:"filter"`
+	StreamRequestFrequency uint16   `toml:"stream_request_frequency"`
+
+	Log telegraf.Logger `toml:"-"`
+
+	filter         filter.Filter
+	connection     *gomavlib.Node
+	endpointConfig gomavlib.EndpointConf
+	cancel         context.CancelFunc
+	wg             sync.WaitGroup
+}
+
+//go:embed sample.conf
+var sampleConfig string
+
+func (*Mavlink) SampleConfig() string {
+	return sampleConfig
+}
+
+func (m *Mavlink) Init() error {
+	// Set default values
+	if m.URL == "" {
+		m.URL = "tcp://127.0.0.1:5760"
+	}
+
+	// Parse out the Mavlink endpoint.
+	// Try to parse the URL
+	u, err := url.Parse(m.URL)
+	if err != nil {
+		return fmt.Errorf("invalid url: %w", err)
+	}
+
path: plugins/inputs/mavlink/mavlink.go
user: login: srebhan
body: How about
```suggestion
	// Handle the different Mavlink endpoint schemata
	u, err := url.Parse(m.URL)
	if err != nil {
		return fmt.Errorf("invalid url: %w", err)
	}
```
created_at: 2025-05-05T11:46:04Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,239 @@
+//go:generate ../../../tools/readme_config_includer/generator
+package mavlink
+
+import (
+	"context"
+	_ "embed"
+	"fmt"
+	"net"
+	"net/url"
+	"reflect"
+	"strconv"
+	"strings"
+	"sync"
+	"time"
+
+	"github.com/bluenviron/gomavlib/v3"
+	"github.com/bluenviron/gomavlib/v3/pkg/dialects/ardupilotmega"
+	"github.com/bluenviron/gomavlib/v3/pkg/frame"
+
+	"github.com/influxdata/telegraf"
+	"github.com/influxdata/telegraf/filter"
+	"github.com/influxdata/telegraf/internal"
+	"github.com/influxdata/telegraf/metric"
+	"github.com/influxdata/telegraf/plugins/inputs"
+)
+
+type Mavlink struct {
+	URL                    string   `toml:"url"`
+	SystemID               uint8    `toml:"system_id"`
+	FilterPattern          []string `toml:"filter"`
+	StreamRequestFrequency uint16   `toml:"stream_request_frequency"`
+
+	Log telegraf.Logger `toml:"-"`
+
+	filter         filter.Filter
+	connection     *gomavlib.Node
+	endpointConfig gomavlib.EndpointConf
+	cancel         context.CancelFunc
+	wg             sync.WaitGroup
+}
+
+//go:embed sample.conf
+var sampleConfig string
+
+func (*Mavlink) SampleConfig() string {
+	return sampleConfig
+}
+
+func (m *Mavlink) Init() error {
+	// Set default values
+	if m.URL == "" {
+		m.URL = "tcp://127.0.0.1:5760"
+	}
+
+	// Parse out the Mavlink endpoint.
+	// Try to parse the URL
+	u, err := url.Parse(m.URL)
+	if err != nil {
+		return fmt.Errorf("invalid url: %w", err)
+	}
+
+	switch u.Scheme {
+	case "serial":
+		// Serial client
+		// Parse serial URL by hand, because it is not a compliant URL.
path: plugins/inputs/mavlink/mavlink.go
user: login: srebhan
body: ```suggestion
		// Parse serial URL by hand, because it is not a compliant URL.
		// Use the default baudrate if not specified
```
created_at: 2025-05-05T11:47:32Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,239 @@
+//go:generate ../../../tools/readme_config_includer/generator
+package mavlink
+
+import (
+	"context"
+	_ "embed"
+	"fmt"
+	"net"
+	"net/url"
+	"reflect"
+	"strconv"
+	"strings"
+	"sync"
+	"time"
+
+	"github.com/bluenviron/gomavlib/v3"
+	"github.com/bluenviron/gomavlib/v3/pkg/dialects/ardupilotmega"
+	"github.com/bluenviron/gomavlib/v3/pkg/frame"
+
+	"github.com/influxdata/telegraf"
+	"github.com/influxdata/telegraf/filter"
+	"github.com/influxdata/telegraf/internal"
+	"github.com/influxdata/telegraf/metric"
+	"github.com/influxdata/telegraf/plugins/inputs"
+)
+
+type Mavlink struct {
+	URL                    string   `toml:"url"`
+	SystemID               uint8    `toml:"system_id"`
+	FilterPattern          []string `toml:"filter"`
+	StreamRequestFrequency uint16   `toml:"stream_request_frequency"`
+
+	Log telegraf.Logger `toml:"-"`
+
+	filter         filter.Filter
+	connection     *gomavlib.Node
+	endpointConfig gomavlib.EndpointConf
+	cancel         context.CancelFunc
+	wg             sync.WaitGroup
+}
+
+//go:embed sample.conf
+var sampleConfig string
+
+func (*Mavlink) SampleConfig() string {
+	return sampleConfig
+}
+
+func (m *Mavlink) Init() error {
+	// Set default values
+	if m.URL == "" {
+		m.URL = "tcp://127.0.0.1:5760"
+	}
+
+	// Parse out the Mavlink endpoint.
+	// Try to parse the URL
+	u, err := url.Parse(m.URL)
+	if err != nil {
+		return fmt.Errorf("invalid url: %w", err)
+	}
+
+	switch u.Scheme {
+	case "serial":
+		// Serial client
+		// Parse serial URL by hand, because it is not a compliant URL.
+		baudRate := 57600
+		device, rate, found := strings.Cut(strings.TrimPrefix(m.URL, "serial://"), ":")
+		if found {
+			r, err := strconv.Atoi(rate)
+			if err != nil {
+				return fmt.Errorf("serial baud rate not valid: %w", err)
+			}
+			baudRate = r
+		}
+
+		m.endpointConfig = gomavlib.EndpointSerial{
+			Device: device,
+			Baud:   baudRate,
+		}
+	case "tcp":
+		// Use default TCP port if it was not specified
+		port := u.Port()
+		if port == "" {
+			port = "5760"
+		}
+
+		if u.Hostname() == "" {
+			m.endpointConfig = gomavlib.EndpointTCPServer{
+				Address: "0.0.0.0:" + port,
+			}
+		} else {
+			m.endpointConfig = gomavlib.EndpointTCPClient{
+				Address: net.JoinHostPort(u.Hostname(), port),
+			}
+		}
path: plugins/inputs/mavlink/mavlink.go
user: login: srebhan
body: One question here: What if someone wants the server to listen on a specific device only? Shouldn't we have `tcp` for the client and `tcp-server` for the server to make the choice explicit? This way we could allow to specify a local listener address... Same for UDP.
created_at: 2025-05-05T11:49:33Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,239 @@
+//go:generate ../../../tools/readme_config_includer/generator
+package mavlink
+
+import (
+	"context"
+	_ "embed"
+	"fmt"
+	"net"
+	"net/url"
+	"reflect"
+	"strconv"
+	"strings"
+	"sync"
+	"time"
+
+	"github.com/bluenviron/gomavlib/v3"
+	"github.com/bluenviron/gomavlib/v3/pkg/dialects/ardupilotmega"
+	"github.com/bluenviron/gomavlib/v3/pkg/frame"
+
+	"github.com/influxdata/telegraf"
+	"github.com/influxdata/telegraf/filter"
+	"github.com/influxdata/telegraf/internal"
+	"github.com/influxdata/telegraf/metric"
+	"github.com/influxdata/telegraf/plugins/inputs"
+)
+
+type Mavlink struct {
+	URL                    string   `toml:"url"`
+	SystemID               uint8    `toml:"system_id"`
+	FilterPattern          []string `toml:"filter"`
+	StreamRequestFrequency uint16   `toml:"stream_request_frequency"`
+
+	Log telegraf.Logger `toml:"-"`
+
+	filter         filter.Filter
+	connection     *gomavlib.Node
+	endpointConfig gomavlib.EndpointConf
+	cancel         context.CancelFunc
+	wg             sync.WaitGroup
+}
+
+//go:embed sample.conf
+var sampleConfig string
+
+func (*Mavlink) SampleConfig() string {
+	return sampleConfig
+}
+
+func (m *Mavlink) Init() error {
+	// Set default values
+	if m.URL == "" {
+		m.URL = "tcp://127.0.0.1:5760"
+	}
+
+	// Parse out the Mavlink endpoint.
+	// Try to parse the URL
+	u, err := url.Parse(m.URL)
+	if err != nil {
+		return fmt.Errorf("invalid url: %w", err)
+	}
+
+	switch u.Scheme {
+	case "serial":
+		// Serial client
+		// Parse serial URL by hand, because it is not a compliant URL.
+		baudRate := 57600
+		device, rate, found := strings.Cut(strings.TrimPrefix(m.URL, "serial://"), ":")
+		if found {
+			r, err := strconv.Atoi(rate)
+			if err != nil {
+				return fmt.Errorf("serial baud rate not valid: %w", err)
+			}
+			baudRate = r
+		}
+
+		m.endpointConfig = gomavlib.EndpointSerial{
+			Device: device,
+			Baud:   baudRate,
+		}
+	case "tcp":
+		// Use default TCP port if it was not specified
+		port := u.Port()
+		if port == "" {
+			port = "5760"
+		}
+
+		if u.Hostname() == "" {
+			m.endpointConfig = gomavlib.EndpointTCPServer{
+				Address: "0.0.0.0:" + port,
+			}
+		} else {
+			m.endpointConfig = gomavlib.EndpointTCPClient{
+				Address: net.JoinHostPort(u.Hostname(), port),
+			}
+		}
+
+	case "udp":
+		// Use default UDP port if it was not specified
+		port := u.Port()
+		if port == "" {
+			port = "14550"
+		}
+
+		if u.Hostname() == "" {
+			m.endpointConfig = gomavlib.EndpointUDPServer{
+				Address: "0.0.0.0:" + port,
+			}
+		} else {
+			m.endpointConfig = gomavlib.EndpointUDPClient{
+				Address: net.JoinHostPort(u.Hostname(), port),
+			}
+		}
+
+	default:
+		return fmt.Errorf("unknown scheme %q", u.Scheme)
+	}
+
+	// Compile filter
+	m.filter, err = filter.Compile(m.FilterPattern)
+	if err != nil {
+		return fmt.Errorf("compiling filter failed: %w", err)
+	}
+
+	return nil
+}
+
+func (m *Mavlink) Start(acc telegraf.Accumulator) error {
+	// Start MAVLink endpoint
+	connection, err := gomavlib.NewNode(gomavlib.NodeConf{
+		Endpoints:              []gomavlib.EndpointConf{m.endpointConfig},
+		Dialect:                ardupilotmega.Dialect,
+		OutVersion:             gomavlib.V2,
+		OutSystemID:            m.SystemID,
+		StreamRequestEnable:    m.StreamRequestFrequency > 0,
+		StreamRequestFrequency: int(m.StreamRequestFrequency),
+	})
+	if err != nil {
+		return &internal.StartupError{
+			Err:   fmt.Errorf("connecting to mavlink endpoint %s failed: %w", m.URL, err),
+			Retry: true,
+		}
+	}
+	m.connection = connection
+	ctx, cancelFunc := context.WithCancel(context.Background())
+	m.cancel = cancelFunc
+
+	// Start routine to connect to Mavlink and stream out data async
+	m.wg.Add(1)
+	go func(ctx context.Context) {
+		defer m.connection.Close()
+		defer m.wg.Done()
+
+		for {
+			select {
+			case <-ctx.Done():
+				return
+			case evt := <-m.connection.Events():
+				switch evt := evt.(type) {
+				case *gomavlib.EventFrame:
+					if result := convertFrameToMetric(evt.Frame, m.filter); result != nil {
+						result.AddTag("source", m.URL)
+						acc.AddMetric(result)
+					}
+				case *gomavlib.EventChannelOpen:
+					m.Log.Tracef("Mavlink channel opened")
+				case *gomavlib.EventChannelClose:
+					m.Log.Tracef("Mavlink channel closed")
+				case *gomavlib.EventParseError:
+					m.Log.Tracef("Mavlink parse error: %s", evt.Error.Error())
+				case *gomavlib.EventStreamRequested:
+					m.Log.Tracef("Issued stream request to system %d, component %d", evt.SystemID, evt.ComponentID)
+				default:
+					m.Log.Tracef("Unhandled Mavlink event: %T", evt)
+				}
+			}
+		}
+	}(ctx)
+
+	return nil
+}
+
+func (*Mavlink) Gather(telegraf.Accumulator) error {
+	return nil
+}
+
+func (m *Mavlink) Stop() {
+	m.cancel()
+	m.wg.Wait()
+}
+
+// Convert a Mavlink frame into a struct containing Metric data.
+func convertFrameToMetric(frm frame.Frame, msgFilter filter.Filter) telegraf.Metric {
+	m := frm.GetMessage()
+	t := reflect.TypeOf(m)
+	v := reflect.ValueOf(m)
+	if t.Kind() == reflect.Ptr {
+		t = t.Elem()
+		v = v.Elem()
+	}
path: plugins/inputs/mavlink/mavlink.go
user: login: srebhan
body: How about
```suggestion
	v := reflect.Indirect(reflect.ValueOf(m))
	t := v.Type()
```
created_at: 2025-05-05T12:25:41Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,239 @@
+//go:generate ../../../tools/readme_config_includer/generator
+package mavlink
+
+import (
+	"context"
+	_ "embed"
+	"fmt"
+	"net"
+	"net/url"
+	"reflect"
+	"strconv"
+	"strings"
+	"sync"
+	"time"
+
+	"github.com/bluenviron/gomavlib/v3"
+	"github.com/bluenviron/gomavlib/v3/pkg/dialects/ardupilotmega"
+	"github.com/bluenviron/gomavlib/v3/pkg/frame"
+
+	"github.com/influxdata/telegraf"
+	"github.com/influxdata/telegraf/filter"
+	"github.com/influxdata/telegraf/internal"
+	"github.com/influxdata/telegraf/metric"
+	"github.com/influxdata/telegraf/plugins/inputs"
+)
+
+type Mavlink struct {
+	URL                    string   `toml:"url"`
+	SystemID               uint8    `toml:"system_id"`
+	FilterPattern          []string `toml:"filter"`
+	StreamRequestFrequency uint16   `toml:"stream_request_frequency"`
+
+	Log telegraf.Logger `toml:"-"`
+
+	filter         filter.Filter
+	connection     *gomavlib.Node
+	endpointConfig gomavlib.EndpointConf
+	cancel         context.CancelFunc
+	wg             sync.WaitGroup
+}
+
+//go:embed sample.conf
+var sampleConfig string
+
+func (*Mavlink) SampleConfig() string {
+	return sampleConfig
+}
+
+func (m *Mavlink) Init() error {
+	// Set default values
+	if m.URL == "" {
+		m.URL = "tcp://127.0.0.1:5760"
+	}
+
+	// Parse out the Mavlink endpoint.
+	// Try to parse the URL
+	u, err := url.Parse(m.URL)
+	if err != nil {
+		return fmt.Errorf("invalid url: %w", err)
+	}
+
+	switch u.Scheme {
+	case "serial":
+		// Serial client
+		// Parse serial URL by hand, because it is not a compliant URL.
+		baudRate := 57600
+		device, rate, found := strings.Cut(strings.TrimPrefix(m.URL, "serial://"), ":")
+		if found {
+			r, err := strconv.Atoi(rate)
+			if err != nil {
+				return fmt.Errorf("serial baud rate not valid: %w", err)
+			}
+			baudRate = r
+		}
+
+		m.endpointConfig = gomavlib.EndpointSerial{
+			Device: device,
+			Baud:   baudRate,
+		}
+	case "tcp":
+		// Use default TCP port if it was not specified
+		port := u.Port()
+		if port == "" {
+			port = "5760"
+		}
+
+		if u.Hostname() == "" {
+			m.endpointConfig = gomavlib.EndpointTCPServer{
+				Address: "0.0.0.0:" + port,
+			}
+		} else {
+			m.endpointConfig = gomavlib.EndpointTCPClient{
+				Address: net.JoinHostPort(u.Hostname(), port),
+			}
+		}
+
+	case "udp":
+		// Use default UDP port if it was not specified
+		port := u.Port()
+		if port == "" {
+			port = "14550"
+		}
+
+		if u.Hostname() == "" {
+			m.endpointConfig = gomavlib.EndpointUDPServer{
+				Address: "0.0.0.0:" + port,
+			}
+		} else {
+			m.endpointConfig = gomavlib.EndpointUDPClient{
+				Address: net.JoinHostPort(u.Hostname(), port),
+			}
+		}
+
+	default:
+		return fmt.Errorf("unknown scheme %q", u.Scheme)
+	}
+
+	// Compile filter
+	m.filter, err = filter.Compile(m.FilterPattern)
+	if err != nil {
+		return fmt.Errorf("compiling filter failed: %w", err)
+	}
+
+	return nil
+}
+
+func (m *Mavlink) Start(acc telegraf.Accumulator) error {
+	// Start MAVLink endpoint
+	connection, err := gomavlib.NewNode(gomavlib.NodeConf{
+		Endpoints:              []gomavlib.EndpointConf{m.endpointConfig},
+		Dialect:                ardupilotmega.Dialect,
+		OutVersion:             gomavlib.V2,
+		OutSystemID:            m.SystemID,
+		StreamRequestEnable:    m.StreamRequestFrequency > 0,
+		StreamRequestFrequency: int(m.StreamRequestFrequency),
+	})
+	if err != nil {
+		return &internal.StartupError{
+			Err:   fmt.Errorf("connecting to mavlink endpoint %s failed: %w", m.URL, err),
+			Retry: true,
+		}
+	}
+	m.connection = connection
+	ctx, cancelFunc := context.WithCancel(context.Background())
+	m.cancel = cancelFunc
+
+	// Start routine to connect to Mavlink and stream out data async
+	m.wg.Add(1)
+	go func(ctx context.Context) {
+		defer m.connection.Close()
+		defer m.wg.Done()
+
+		for {
+			select {
+			case <-ctx.Done():
+				return
+			case evt := <-m.connection.Events():
+				switch evt := evt.(type) {
+				case *gomavlib.EventFrame:
+					if result := convertFrameToMetric(evt.Frame, m.filter); result != nil {
+						result.AddTag("source", m.URL)
+						acc.AddMetric(result)
+					}
path: plugins/inputs/mavlink/mavlink.go
user: login: srebhan
body: How about making `convertFrameToMetric` a member of the plugin and pass in the accumulator? This way the handling can fully happen in that function
```suggestion
					m.handleFrame(acc, evt.Frame)
```
created_at: 2025-05-05T12:29:25Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,239 @@
+//go:generate ../../../tools/readme_config_includer/generator
+package mavlink
+
+import (
+	"context"
+	_ "embed"
+	"fmt"
+	"net"
+	"net/url"
+	"reflect"
+	"strconv"
+	"strings"
+	"sync"
+	"time"
+
+	"github.com/bluenviron/gomavlib/v3"
+	"github.com/bluenviron/gomavlib/v3/pkg/dialects/ardupilotmega"
+	"github.com/bluenviron/gomavlib/v3/pkg/frame"
+
+	"github.com/influxdata/telegraf"
+	"github.com/influxdata/telegraf/filter"
+	"github.com/influxdata/telegraf/internal"
+	"github.com/influxdata/telegraf/metric"
+	"github.com/influxdata/telegraf/plugins/inputs"
+)
+
+type Mavlink struct {
+	URL                    string   `toml:"url"`
+	SystemID               uint8    `toml:"system_id"`
+	FilterPattern          []string `toml:"filter"`
+	StreamRequestFrequency uint16   `toml:"stream_request_frequency"`
+
+	Log telegraf.Logger `toml:"-"`
+
+	filter         filter.Filter
+	connection     *gomavlib.Node
+	endpointConfig gomavlib.EndpointConf
+	cancel         context.CancelFunc
+	wg             sync.WaitGroup
+}
+
+//go:embed sample.conf
+var sampleConfig string
+
+func (*Mavlink) SampleConfig() string {
+	return sampleConfig
+}
+
+func (m *Mavlink) Init() error {
+	// Set default values
+	if m.URL == "" {
+		m.URL = "tcp://127.0.0.1:5760"
+	}
+
+	// Parse out the Mavlink endpoint.
+	// Try to parse the URL
+	u, err := url.Parse(m.URL)
+	if err != nil {
+		return fmt.Errorf("invalid url: %w", err)
+	}
+
+	switch u.Scheme {
+	case "serial":
+		// Serial client
+		// Parse serial URL by hand, because it is not a compliant URL.
+		baudRate := 57600
+		device, rate, found := strings.Cut(strings.TrimPrefix(m.URL, "serial://"), ":")
+		if found {
+			r, err := strconv.Atoi(rate)
+			if err != nil {
+				return fmt.Errorf("serial baud rate not valid: %w", err)
+			}
+			baudRate = r
+		}
+
+		m.endpointConfig = gomavlib.EndpointSerial{
+			Device: device,
+			Baud:   baudRate,
+		}
+	case "tcp":
+		// Use default TCP port if it was not specified
+		port := u.Port()
+		if port == "" {
+			port = "5760"
+		}
+
+		if u.Hostname() == "" {
+			m.endpointConfig = gomavlib.EndpointTCPServer{
+				Address: "0.0.0.0:" + port,
+			}
+		} else {
+			m.endpointConfig = gomavlib.EndpointTCPClient{
+				Address: net.JoinHostPort(u.Hostname(), port),
+			}
+		}
+
+	case "udp":
+		// Use default UDP port if it was not specified
+		port := u.Port()
+		if port == "" {
+			port = "14550"
+		}
+
+		if u.Hostname() == "" {
+			m.endpointConfig = gomavlib.EndpointUDPServer{
+				Address: "0.0.0.0:" + port,
+			}
+		} else {
+			m.endpointConfig = gomavlib.EndpointUDPClient{
+				Address: net.JoinHostPort(u.Hostname(), port),
+			}
+		}
+
+	default:
+		return fmt.Errorf("unknown scheme %q", u.Scheme)
+	}
+
+	// Compile filter
+	m.filter, err = filter.Compile(m.FilterPattern)
+	if err != nil {
+		return fmt.Errorf("compiling filter failed: %w", err)
+	}
+
+	return nil
+}
+
+func (m *Mavlink) Start(acc telegraf.Accumulator) error {
+	// Start MAVLink endpoint
+	connection, err := gomavlib.NewNode(gomavlib.NodeConf{
+		Endpoints:              []gomavlib.EndpointConf{m.endpointConfig},
+		Dialect:                ardupilotmega.Dialect,
+		OutVersion:             gomavlib.V2,
+		OutSystemID:            m.SystemID,
+		StreamRequestEnable:    m.StreamRequestFrequency > 0,
+		StreamRequestFrequency: int(m.StreamRequestFrequency),
+	})
+	if err != nil {
+		return &internal.StartupError{
+			Err:   fmt.Errorf("connecting to mavlink endpoint %s failed: %w", m.URL, err),
+			Retry: true,
+		}
+	}
+	m.connection = connection
+	ctx, cancelFunc := context.WithCancel(context.Background())
+	m.cancel = cancelFunc
+
+	// Start routine to connect to Mavlink and stream out data async
+	m.wg.Add(1)
+	go func(ctx context.Context) {
+		defer m.connection.Close()
+		defer m.wg.Done()
+
+		for {
+			select {
+			case <-ctx.Done():
+				return
+			case evt := <-m.connection.Events():
+				switch evt := evt.(type) {
+				case *gomavlib.EventFrame:
+					if result := convertFrameToMetric(evt.Frame, m.filter); result != nil {
+						result.AddTag("source", m.URL)
+						acc.AddMetric(result)
+					}
+				case *gomavlib.EventChannelOpen:
+					m.Log.Tracef("Mavlink channel opened")
+				case *gomavlib.EventChannelClose:
+					m.Log.Tracef("Mavlink channel closed")
+				case *gomavlib.EventParseError:
+					m.Log.Tracef("Mavlink parse error: %s", evt.Error.Error())
+				case *gomavlib.EventStreamRequested:
+					m.Log.Tracef("Issued stream request to system %d, component %d", evt.SystemID, evt.ComponentID)
+				default:
+					m.Log.Tracef("Unhandled Mavlink event: %T", evt)
+				}
+			}
+		}
+	}(ctx)
+
+	return nil
+}
+
+func (*Mavlink) Gather(telegraf.Accumulator) error {
+	return nil
+}
+
+func (m *Mavlink) Stop() {
+	m.cancel()
+	m.wg.Wait()
+}
+
+// Convert a Mavlink frame into a struct containing Metric data.
+func convertFrameToMetric(frm frame.Frame, msgFilter filter.Filter) telegraf.Metric {
path: plugins/inputs/mavlink/mavlink.go
user: login: srebhan
body: With the suggestion above I would convert to
```suggestion
// Convert a Mavlink frame into a struct containing Metric data.
func (m *Mavlink) handleFrame(acc telegraf.Accumulator, frm frame.Frame) {
```
created_at: 2025-05-05T12:30:35Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,239 @@
+//go:generate ../../../tools/readme_config_includer/generator
+package mavlink
+
+import (
+	"context"
+	_ "embed"
+	"fmt"
+	"net"
+	"net/url"
+	"reflect"
+	"strconv"
+	"strings"
+	"sync"
+	"time"
+
+	"github.com/bluenviron/gomavlib/v3"
+	"github.com/bluenviron/gomavlib/v3/pkg/dialects/ardupilotmega"
+	"github.com/bluenviron/gomavlib/v3/pkg/frame"
+
+	"github.com/influxdata/telegraf"
+	"github.com/influxdata/telegraf/filter"
+	"github.com/influxdata/telegraf/internal"
+	"github.com/influxdata/telegraf/metric"
+	"github.com/influxdata/telegraf/plugins/inputs"
+)
+
+type Mavlink struct {
+	URL                    string   `toml:"url"`
+	SystemID               uint8    `toml:"system_id"`
+	FilterPattern          []string `toml:"filter"`
+	StreamRequestFrequency uint16   `toml:"stream_request_frequency"`
+
+	Log telegraf.Logger `toml:"-"`
+
+	filter         filter.Filter
+	connection     *gomavlib.Node
+	endpointConfig gomavlib.EndpointConf
+	cancel         context.CancelFunc
+	wg             sync.WaitGroup
+}
+
+//go:embed sample.conf
+var sampleConfig string
+
+func (*Mavlink) SampleConfig() string {
+	return sampleConfig
+}
+
+func (m *Mavlink) Init() error {
+	// Set default values
+	if m.URL == "" {
+		m.URL = "tcp://127.0.0.1:5760"
+	}
+
+	// Parse out the Mavlink endpoint.
+	// Try to parse the URL
+	u, err := url.Parse(m.URL)
+	if err != nil {
+		return fmt.Errorf("invalid url: %w", err)
+	}
+
+	switch u.Scheme {
+	case "serial":
+		// Serial client
+		// Parse serial URL by hand, because it is not a compliant URL.
+		baudRate := 57600
+		device, rate, found := strings.Cut(strings.TrimPrefix(m.URL, "serial://"), ":")
+		if found {
+			r, err := strconv.Atoi(rate)
+			if err != nil {
+				return fmt.Errorf("serial baud rate not valid: %w", err)
+			}
+			baudRate = r
+		}
+
+		m.endpointConfig = gomavlib.EndpointSerial{
+			Device: device,
+			Baud:   baudRate,
+		}
+	case "tcp":
+		// Use default TCP port if it was not specified
+		port := u.Port()
+		if port == "" {
+			port = "5760"
+		}
+
+		if u.Hostname() == "" {
+			m.endpointConfig = gomavlib.EndpointTCPServer{
+				Address: "0.0.0.0:" + port,
+			}
+		} else {
+			m.endpointConfig = gomavlib.EndpointTCPClient{
+				Address: net.JoinHostPort(u.Hostname(), port),
+			}
+		}
+
+	case "udp":
+		// Use default UDP port if it was not specified
+		port := u.Port()
+		if port == "" {
+			port = "14550"
+		}
+
+		if u.Hostname() == "" {
+			m.endpointConfig = gomavlib.EndpointUDPServer{
+				Address: "0.0.0.0:" + port,
+			}
+		} else {
+			m.endpointConfig = gomavlib.EndpointUDPClient{
+				Address: net.JoinHostPort(u.Hostname(), port),
+			}
+		}
+
+	default:
+		return fmt.Errorf("unknown scheme %q", u.Scheme)
+	}
+
+	// Compile filter
+	m.filter, err = filter.Compile(m.FilterPattern)
+	if err != nil {
+		return fmt.Errorf("compiling filter failed: %w", err)
+	}
+
+	return nil
+}
+
+func (m *Mavlink) Start(acc telegraf.Accumulator) error {
+	// Start MAVLink endpoint
+	connection, err := gomavlib.NewNode(gomavlib.NodeConf{
+		Endpoints:              []gomavlib.EndpointConf{m.endpointConfig},
+		Dialect:                ardupilotmega.Dialect,
+		OutVersion:             gomavlib.V2,
+		OutSystemID:            m.SystemID,
+		StreamRequestEnable:    m.StreamRequestFrequency > 0,
+		StreamRequestFrequency: int(m.StreamRequestFrequency),
+	})
+	if err != nil {
+		return &internal.StartupError{
+			Err:   fmt.Errorf("connecting to mavlink endpoint %s failed: %w", m.URL, err),
+			Retry: true,
+		}
+	}
+	m.connection = connection
+	ctx, cancelFunc := context.WithCancel(context.Background())
+	m.cancel = cancelFunc
+
+	// Start routine to connect to Mavlink and stream out data async
+	m.wg.Add(1)
+	go func(ctx context.Context) {
+		defer m.connection.Close()
+		defer m.wg.Done()
+
+		for {
+			select {
+			case <-ctx.Done():
+				return
+			case evt := <-m.connection.Events():
+				switch evt := evt.(type) {
+				case *gomavlib.EventFrame:
+					if result := convertFrameToMetric(evt.Frame, m.filter); result != nil {
+						result.AddTag("source", m.URL)
+						acc.AddMetric(result)
+					}
+				case *gomavlib.EventChannelOpen:
+					m.Log.Tracef("Mavlink channel opened")
+				case *gomavlib.EventChannelClose:
+					m.Log.Tracef("Mavlink channel closed")
+				case *gomavlib.EventParseError:
+					m.Log.Tracef("Mavlink parse error: %s", evt.Error.Error())
+				case *gomavlib.EventStreamRequested:
+					m.Log.Tracef("Issued stream request to system %d, component %d", evt.SystemID, evt.ComponentID)
+				default:
+					m.Log.Tracef("Unhandled Mavlink event: %T", evt)
+				}
+			}
+		}
+	}(ctx)
+
+	return nil
+}
+
+func (*Mavlink) Gather(telegraf.Accumulator) error {
+	return nil
+}
+
+func (m *Mavlink) Stop() {
+	m.cancel()
+	m.wg.Wait()
+}
+
+// Convert a Mavlink frame into a struct containing Metric data.
+func convertFrameToMetric(frm frame.Frame, msgFilter filter.Filter) telegraf.Metric {
+	m := frm.GetMessage()
+	t := reflect.TypeOf(m)
+	v := reflect.ValueOf(m)
+	if t.Kind() == reflect.Ptr {
+		t = t.Elem()
+		v = v.Elem()
+	}
+
+	name := internal.SnakeCase(strings.TrimPrefix(t.Name(), "Message"))
+
+	if msgFilter != nil && !msgFilter.Match(name) {
+		return nil
+	}
path: plugins/inputs/mavlink/mavlink.go
user: login: srebhan
body: With the suggestion above this converts to
```suggestion
	if m.filter != nil && !m.filter.Match(name) {
		return
	}
```
created_at: 2025-05-05T12:31:21Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,239 @@
+//go:generate ../../../tools/readme_config_includer/generator
+package mavlink
+
+import (
+	"context"
+	_ "embed"
+	"fmt"
+	"net"
+	"net/url"
+	"reflect"
+	"strconv"
+	"strings"
+	"sync"
+	"time"
+
+	"github.com/bluenviron/gomavlib/v3"
+	"github.com/bluenviron/gomavlib/v3/pkg/dialects/ardupilotmega"
+	"github.com/bluenviron/gomavlib/v3/pkg/frame"
+
+	"github.com/influxdata/telegraf"
+	"github.com/influxdata/telegraf/filter"
+	"github.com/influxdata/telegraf/internal"
+	"github.com/influxdata/telegraf/metric"
+	"github.com/influxdata/telegraf/plugins/inputs"
+)
+
+type Mavlink struct {
+	URL                    string   `toml:"url"`
+	SystemID               uint8    `toml:"system_id"`
+	FilterPattern          []string `toml:"filter"`
+	StreamRequestFrequency uint16   `toml:"stream_request_frequency"`
+
+	Log telegraf.Logger `toml:"-"`
+
+	filter         filter.Filter
+	connection     *gomavlib.Node
+	endpointConfig gomavlib.EndpointConf
+	cancel         context.CancelFunc
+	wg             sync.WaitGroup
+}
+
+//go:embed sample.conf
+var sampleConfig string
+
+func (*Mavlink) SampleConfig() string {
+	return sampleConfig
+}
+
+func (m *Mavlink) Init() error {
+	// Set default values
+	if m.URL == "" {
+		m.URL = "tcp://127.0.0.1:5760"
+	}
+
+	// Parse out the Mavlink endpoint.
+	// Try to parse the URL
+	u, err := url.Parse(m.URL)
+	if err != nil {
+		return fmt.Errorf("invalid url: %w", err)
+	}
+
+	switch u.Scheme {
+	case "serial":
+		// Serial client
+		// Parse serial URL by hand, because it is not a compliant URL.
+		baudRate := 57600
+		device, rate, found := strings.Cut(strings.TrimPrefix(m.URL, "serial://"), ":")
+		if found {
+			r, err := strconv.Atoi(rate)
+			if err != nil {
+				return fmt.Errorf("serial baud rate not valid: %w", err)
+			}
+			baudRate = r
+		}
+
+		m.endpointConfig = gomavlib.EndpointSerial{
+			Device: device,
+			Baud:   baudRate,
+		}
+	case "tcp":
+		// Use default TCP port if it was not specified
+		port := u.Port()
+		if port == "" {
+			port = "5760"
+		}
+
+		if u.Hostname() == "" {
+			m.endpointConfig = gomavlib.EndpointTCPServer{
+				Address: "0.0.0.0:" + port,
+			}
+		} else {
+			m.endpointConfig = gomavlib.EndpointTCPClient{
+				Address: net.JoinHostPort(u.Hostname(), port),
+			}
+		}
+
+	case "udp":
+		// Use default UDP port if it was not specified
+		port := u.Port()
+		if port == "" {
+			port = "14550"
+		}
+
+		if u.Hostname() == "" {
+			m.endpointConfig = gomavlib.EndpointUDPServer{
+				Address: "0.0.0.0:" + port,
+			}
+		} else {
+			m.endpointConfig = gomavlib.EndpointUDPClient{
+				Address: net.JoinHostPort(u.Hostname(), port),
+			}
+		}
+
+	default:
+		return fmt.Errorf("unknown scheme %q", u.Scheme)
+	}
+
+	// Compile filter
+	m.filter, err = filter.Compile(m.FilterPattern)
+	if err != nil {
+		return fmt.Errorf("compiling filter failed: %w", err)
+	}
+
+	return nil
+}
+
+func (m *Mavlink) Start(acc telegraf.Accumulator) error {
+	// Start MAVLink endpoint
+	connection, err := gomavlib.NewNode(gomavlib.NodeConf{
+		Endpoints:              []gomavlib.EndpointConf{m.endpointConfig},
+		Dialect:                ardupilotmega.Dialect,
+		OutVersion:             gomavlib.V2,
+		OutSystemID:            m.SystemID,
+		StreamRequestEnable:    m.StreamRequestFrequency > 0,
+		StreamRequestFrequency: int(m.StreamRequestFrequency),
+	})
+	if err != nil {
+		return &internal.StartupError{
+			Err:   fmt.Errorf("connecting to mavlink endpoint %s failed: %w", m.URL, err),
+			Retry: true,
+		}
+	}
+	m.connection = connection
+	ctx, cancelFunc := context.WithCancel(context.Background())
+	m.cancel = cancelFunc
+
+	// Start routine to connect to Mavlink and stream out data async
+	m.wg.Add(1)
+	go func(ctx context.Context) {
+		defer m.connection.Close()
+		defer m.wg.Done()
+
+		for {
+			select {
+			case <-ctx.Done():
+				return
+			case evt := <-m.connection.Events():
+				switch evt := evt.(type) {
+				case *gomavlib.EventFrame:
+					if result := convertFrameToMetric(evt.Frame, m.filter); result != nil {
+						result.AddTag("source", m.URL)
+						acc.AddMetric(result)
+					}
+				case *gomavlib.EventChannelOpen:
+					m.Log.Tracef("Mavlink channel opened")
+				case *gomavlib.EventChannelClose:
+					m.Log.Tracef("Mavlink channel closed")
+				case *gomavlib.EventParseError:
+					m.Log.Tracef("Mavlink parse error: %s", evt.Error.Error())
+				case *gomavlib.EventStreamRequested:
+					m.Log.Tracef("Issued stream request to system %d, component %d", evt.SystemID, evt.ComponentID)
+				default:
+					m.Log.Tracef("Unhandled Mavlink event: %T", evt)
+				}
+			}
+		}
+	}(ctx)
+
+	return nil
+}
+
+func (*Mavlink) Gather(telegraf.Accumulator) error {
+	return nil
+}
+
+func (m *Mavlink) Stop() {
+	m.cancel()
+	m.wg.Wait()
+}
+
+// Convert a Mavlink frame into a struct containing Metric data.
+func convertFrameToMetric(frm frame.Frame, msgFilter filter.Filter) telegraf.Metric {
+	m := frm.GetMessage()
+	t := reflect.TypeOf(m)
+	v := reflect.ValueOf(m)
+	if t.Kind() == reflect.Ptr {
+		t = t.Elem()
+		v = v.Elem()
+	}
+
+	name := internal.SnakeCase(strings.TrimPrefix(t.Name(), "Message"))
+
+	if msgFilter != nil && !msgFilter.Match(name) {
+		return nil
+	}
+
+	tags := map[string]string{
+		"sys_id": strconv.FormatUint(uint64(frm.GetSystemID()), 10),
path: plugins/inputs/mavlink/mavlink.go
user: login: srebhan
body: ```suggestion
		"sys_id": strconv.FormatUint(uint64(frm.GetSystemID()), 10),
		"source": m.URL,
```
created_at: 2025-05-05T12:31:46Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,239 @@
+//go:generate ../../../tools/readme_config_includer/generator
+package mavlink
+
+import (
+	"context"
+	_ "embed"
+	"fmt"
+	"net"
+	"net/url"
+	"reflect"
+	"strconv"
+	"strings"
+	"sync"
+	"time"
+
+	"github.com/bluenviron/gomavlib/v3"
+	"github.com/bluenviron/gomavlib/v3/pkg/dialects/ardupilotmega"
+	"github.com/bluenviron/gomavlib/v3/pkg/frame"
+
+	"github.com/influxdata/telegraf"
+	"github.com/influxdata/telegraf/filter"
+	"github.com/influxdata/telegraf/internal"
+	"github.com/influxdata/telegraf/metric"
+	"github.com/influxdata/telegraf/plugins/inputs"
+)
+
+type Mavlink struct {
+	URL                    string   `toml:"url"`
+	SystemID               uint8    `toml:"system_id"`
+	FilterPattern          []string `toml:"filter"`
+	StreamRequestFrequency uint16   `toml:"stream_request_frequency"`
+
+	Log telegraf.Logger `toml:"-"`
+
+	filter         filter.Filter
+	connection     *gomavlib.Node
+	endpointConfig gomavlib.EndpointConf
+	cancel         context.CancelFunc
+	wg             sync.WaitGroup
+}
+
+//go:embed sample.conf
+var sampleConfig string
+
+func (*Mavlink) SampleConfig() string {
+	return sampleConfig
+}
+
+func (m *Mavlink) Init() error {
+	// Set default values
+	if m.URL == "" {
+		m.URL = "tcp://127.0.0.1:5760"
+	}
+
+	// Parse out the Mavlink endpoint.
+	// Try to parse the URL
+	u, err := url.Parse(m.URL)
+	if err != nil {
+		return fmt.Errorf("invalid url: %w", err)
+	}
+
+	switch u.Scheme {
+	case "serial":
+		// Serial client
+		// Parse serial URL by hand, because it is not a compliant URL.
+		baudRate := 57600
+		device, rate, found := strings.Cut(strings.TrimPrefix(m.URL, "serial://"), ":")
+		if found {
+			r, err := strconv.Atoi(rate)
+			if err != nil {
+				return fmt.Errorf("serial baud rate not valid: %w", err)
+			}
+			baudRate = r
+		}
+
+		m.endpointConfig = gomavlib.EndpointSerial{
+			Device: device,
+			Baud:   baudRate,
+		}
+	case "tcp":
+		// Use default TCP port if it was not specified
+		port := u.Port()
+		if port == "" {
+			port = "5760"
+		}
+
+		if u.Hostname() == "" {
+			m.endpointConfig = gomavlib.EndpointTCPServer{
+				Address: "0.0.0.0:" + port,
+			}
+		} else {
+			m.endpointConfig = gomavlib.EndpointTCPClient{
+				Address: net.JoinHostPort(u.Hostname(), port),
+			}
+		}
+
+	case "udp":
+		// Use default UDP port if it was not specified
+		port := u.Port()
+		if port == "" {
+			port = "14550"
+		}
+
+		if u.Hostname() == "" {
+			m.endpointConfig = gomavlib.EndpointUDPServer{
+				Address: "0.0.0.0:" + port,
+			}
+		} else {
+			m.endpointConfig = gomavlib.EndpointUDPClient{
+				Address: net.JoinHostPort(u.Hostname(), port),
+			}
+		}
+
+	default:
+		return fmt.Errorf("unknown scheme %q", u.Scheme)
+	}
+
+	// Compile filter
+	m.filter, err = filter.Compile(m.FilterPattern)
+	if err != nil {
+		return fmt.Errorf("compiling filter failed: %w", err)
+	}
+
+	return nil
+}
+
+func (m *Mavlink) Start(acc telegraf.Accumulator) error {
+	// Start MAVLink endpoint
+	connection, err := gomavlib.NewNode(gomavlib.NodeConf{
+		Endpoints:              []gomavlib.EndpointConf{m.endpointConfig},
+		Dialect:                ardupilotmega.Dialect,
+		OutVersion:             gomavlib.V2,
+		OutSystemID:            m.SystemID,
+		StreamRequestEnable:    m.StreamRequestFrequency > 0,
+		StreamRequestFrequency: int(m.StreamRequestFrequency),
+	})
+	if err != nil {
+		return &internal.StartupError{
+			Err:   fmt.Errorf("connecting to mavlink endpoint %s failed: %w", m.URL, err),
+			Retry: true,
+		}
+	}
+	m.connection = connection
+	ctx, cancelFunc := context.WithCancel(context.Background())
+	m.cancel = cancelFunc
+
+	// Start routine to connect to Mavlink and stream out data async
+	m.wg.Add(1)
+	go func(ctx context.Context) {
+		defer m.connection.Close()
+		defer m.wg.Done()
+
+		for {
+			select {
+			case <-ctx.Done():
+				return
+			case evt := <-m.connection.Events():
+				switch evt := evt.(type) {
+				case *gomavlib.EventFrame:
+					if result := convertFrameToMetric(evt.Frame, m.filter); result != nil {
+						result.AddTag("source", m.URL)
+						acc.AddMetric(result)
+					}
+				case *gomavlib.EventChannelOpen:
+					m.Log.Tracef("Mavlink channel opened")
+				case *gomavlib.EventChannelClose:
+					m.Log.Tracef("Mavlink channel closed")
+				case *gomavlib.EventParseError:
+					m.Log.Tracef("Mavlink parse error: %s", evt.Error.Error())
+				case *gomavlib.EventStreamRequested:
+					m.Log.Tracef("Issued stream request to system %d, component %d", evt.SystemID, evt.ComponentID)
+				default:
+					m.Log.Tracef("Unhandled Mavlink event: %T", evt)
+				}
+			}
+		}
+	}(ctx)
+
+	return nil
+}
+
+func (*Mavlink) Gather(telegraf.Accumulator) error {
+	return nil
+}
+
+func (m *Mavlink) Stop() {
+	m.cancel()
+	m.wg.Wait()
+}
+
+// Convert a Mavlink frame into a struct containing Metric data.
+func convertFrameToMetric(frm frame.Frame, msgFilter filter.Filter) telegraf.Metric {
+	m := frm.GetMessage()
+	t := reflect.TypeOf(m)
+	v := reflect.ValueOf(m)
+	if t.Kind() == reflect.Ptr {
+		t = t.Elem()
+		v = v.Elem()
+	}
+
+	name := internal.SnakeCase(strings.TrimPrefix(t.Name(), "Message"))
+
+	if msgFilter != nil && !msgFilter.Match(name) {
+		return nil
+	}
+
+	tags := map[string]string{
+		"sys_id": strconv.FormatUint(uint64(frm.GetSystemID()), 10),
+	}
+	fields := make(map[string]interface{}, t.NumField())
+
+	for i := 0; i < t.NumField(); i++ {
+		field := t.Field(i)
+		value := v.Field(i)
+		fieldName := internal.SnakeCase(field.Name)
+
+		if value.Kind() == reflect.Slice || value.Kind() == reflect.Array {
+			// Split array types into individual primitive values
+			// with _<n> appended to the key
+			for j := 0; j < value.Len(); j++ {
+				indexedFieldName := fmt.Sprintf("%s_%d", fieldName, j+1)
+				fields[indexedFieldName] = value.Index(j).Interface()
+			}
+		} else {
+			fields[fieldName] = value.Interface()
+		}
+	}
+
+	return metric.New(name, tags, fields, time.Now())
path: plugins/inputs/mavlink/mavlink.go
user: login: srebhan
body: ```suggestion
	acc.AddFields(name, fields, tags)
```
created_at: 2025-05-05T12:32:32Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,239 @@
+//go:generate ../../../tools/readme_config_includer/generator
+package mavlink
+
+import (
+	"context"
+	_ "embed"
+	"fmt"
+	"net"
+	"net/url"
+	"reflect"
+	"strconv"
+	"strings"
+	"sync"
+	"time"
+
+	"github.com/bluenviron/gomavlib/v3"
+	"github.com/bluenviron/gomavlib/v3/pkg/dialects/ardupilotmega"
+	"github.com/bluenviron/gomavlib/v3/pkg/frame"
+
+	"github.com/influxdata/telegraf"
+	"github.com/influxdata/telegraf/filter"
+	"github.com/influxdata/telegraf/internal"
+	"github.com/influxdata/telegraf/metric"
+	"github.com/influxdata/telegraf/plugins/inputs"
+)
+
+type Mavlink struct {
+	URL                    string   `toml:"url"`
+	SystemID               uint8    `toml:"system_id"`
+	FilterPattern          []string `toml:"filter"`
+	StreamRequestFrequency uint16   `toml:"stream_request_frequency"`
+
+	Log telegraf.Logger `toml:"-"`
+
+	filter         filter.Filter
+	connection     *gomavlib.Node
+	endpointConfig gomavlib.EndpointConf
+	cancel         context.CancelFunc
+	wg             sync.WaitGroup
+}
+
+//go:embed sample.conf
+var sampleConfig string
+
+func (*Mavlink) SampleConfig() string {
+	return sampleConfig
+}
+
+func (m *Mavlink) Init() error {
+	// Set default values
+	if m.URL == "" {
+		m.URL = "tcp://127.0.0.1:5760"
+	}
+
+	// Parse out the Mavlink endpoint.
+	// Try to parse the URL
+	u, err := url.Parse(m.URL)
+	if err != nil {
+		return fmt.Errorf("invalid url: %w", err)
+	}
+
+	switch u.Scheme {
+	case "serial":
+		// Serial client
+		// Parse serial URL by hand, because it is not a compliant URL.
+		baudRate := 57600
+		device, rate, found := strings.Cut(strings.TrimPrefix(m.URL, "serial://"), ":")
+		if found {
+			r, err := strconv.Atoi(rate)
+			if err != nil {
+				return fmt.Errorf("serial baud rate not valid: %w", err)
+			}
+			baudRate = r
+		}
+
+		m.endpointConfig = gomavlib.EndpointSerial{
+			Device: device,
+			Baud:   baudRate,
+		}
+	case "tcp":
+		// Use default TCP port if it was not specified
+		port := u.Port()
+		if port == "" {
+			port = "5760"
+		}
+
+		if u.Hostname() == "" {
+			m.endpointConfig = gomavlib.EndpointTCPServer{
+				Address: "0.0.0.0:" + port,
+			}
+		} else {
+			m.endpointConfig = gomavlib.EndpointTCPClient{
+				Address: net.JoinHostPort(u.Hostname(), port),
+			}
+		}
path: plugins/inputs/mavlink/mavlink.go
user: login: chrisdalke
body: Yes, this is a good point. I have seen some Mavlink client libraries solve this by separating into `tcp` (TCP client) and `tcps` (TCP server) modes, so I'll do that.

In server mode, the default hostname will be `0.0.0.0` (all interfaces) but the user can choose to bind to a specific interface.
created_at: 2025-05-05T14:08:20Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,239 @@
+//go:generate ../../../tools/readme_config_includer/generator
+package mavlink
+
+import (
+	"context"
+	_ "embed"
+	"fmt"
+	"net"
+	"net/url"
+	"reflect"
+	"strconv"
+	"strings"
+	"sync"
+	"time"
+
+	"github.com/bluenviron/gomavlib/v3"
+	"github.com/bluenviron/gomavlib/v3/pkg/dialects/ardupilotmega"
+	"github.com/bluenviron/gomavlib/v3/pkg/frame"
+
+	"github.com/influxdata/telegraf"
+	"github.com/influxdata/telegraf/filter"
+	"github.com/influxdata/telegraf/internal"
+	"github.com/influxdata/telegraf/metric"
+	"github.com/influxdata/telegraf/plugins/inputs"
+)
+
+type Mavlink struct {
+	URL                    string   `toml:"url"`
+	SystemID               uint8    `toml:"system_id"`
+	FilterPattern          []string `toml:"filter"`
+	StreamRequestFrequency uint16   `toml:"stream_request_frequency"`
+
+	Log telegraf.Logger `toml:"-"`
+
+	filter         filter.Filter
+	connection     *gomavlib.Node
+	endpointConfig gomavlib.EndpointConf
+	cancel         context.CancelFunc
+	wg             sync.WaitGroup
+}
+
+//go:embed sample.conf
+var sampleConfig string
+
+func (*Mavlink) SampleConfig() string {
+	return sampleConfig
+}
+
+func (m *Mavlink) Init() error {
+	// Set default values
+	if m.URL == "" {
+		m.URL = "tcp://127.0.0.1:5760"
+	}
+
+	// Parse out the Mavlink endpoint.
+	// Try to parse the URL
+	u, err := url.Parse(m.URL)
+	if err != nil {
+		return fmt.Errorf("invalid url: %w", err)
+	}
+
+	switch u.Scheme {
+	case "serial":
+		// Serial client
+		// Parse serial URL by hand, because it is not a compliant URL.
+		baudRate := 57600
+		device, rate, found := strings.Cut(strings.TrimPrefix(m.URL, "serial://"), ":")
+		if found {
+			r, err := strconv.Atoi(rate)
+			if err != nil {
+				return fmt.Errorf("serial baud rate not valid: %w", err)
+			}
+			baudRate = r
+		}
+
+		m.endpointConfig = gomavlib.EndpointSerial{
+			Device: device,
+			Baud:   baudRate,
+		}
+	case "tcp":
+		// Use default TCP port if it was not specified
+		port := u.Port()
+		if port == "" {
+			port = "5760"
+		}
+
+		if u.Hostname() == "" {
+			m.endpointConfig = gomavlib.EndpointTCPServer{
+				Address: "0.0.0.0:" + port,
+			}
+		} else {
+			m.endpointConfig = gomavlib.EndpointTCPClient{
+				Address: net.JoinHostPort(u.Hostname(), port),
+			}
+		}
path: plugins/inputs/mavlink/mavlink.go
user: login: srebhan
body: OK, just a small nitpick, `tcps` sometimes is referred to as "secure TCP" so maybe use something more explicit e.g. `tcpserv`...
created_at: 2025-05-05T14:18:27Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,239 @@
+//go:generate ../../../tools/readme_config_includer/generator
+package mavlink
+
+import (
+	"context"
+	_ "embed"
+	"fmt"
+	"net"
+	"net/url"
+	"reflect"
+	"strconv"
+	"strings"
+	"sync"
+	"time"
+
+	"github.com/bluenviron/gomavlib/v3"
+	"github.com/bluenviron/gomavlib/v3/pkg/dialects/ardupilotmega"
+	"github.com/bluenviron/gomavlib/v3/pkg/frame"
+
+	"github.com/influxdata/telegraf"
+	"github.com/influxdata/telegraf/filter"
+	"github.com/influxdata/telegraf/internal"
+	"github.com/influxdata/telegraf/metric"
+	"github.com/influxdata/telegraf/plugins/inputs"
+)
+
+type Mavlink struct {
+	URL                    string   `toml:"url"`
+	SystemID               uint8    `toml:"system_id"`
+	FilterPattern          []string `toml:"filter"`
+	StreamRequestFrequency uint16   `toml:"stream_request_frequency"`
+
+	Log telegraf.Logger `toml:"-"`
+
+	filter         filter.Filter
+	connection     *gomavlib.Node
+	endpointConfig gomavlib.EndpointConf
+	cancel         context.CancelFunc
+	wg             sync.WaitGroup
+}
+
+//go:embed sample.conf
+var sampleConfig string
+
+func (*Mavlink) SampleConfig() string {
+	return sampleConfig
+}
+
+func (m *Mavlink) Init() error {
+	// Set default values
+	if m.URL == "" {
+		m.URL = "tcp://127.0.0.1:5760"
+	}
+
+	// Parse out the Mavlink endpoint.
+	// Try to parse the URL
+	u, err := url.Parse(m.URL)
+	if err != nil {
+		return fmt.Errorf("invalid url: %w", err)
+	}
+
+	switch u.Scheme {
+	case "serial":
+		// Serial client
+		// Parse serial URL by hand, because it is not a compliant URL.
+		baudRate := 57600
+		device, rate, found := strings.Cut(strings.TrimPrefix(m.URL, "serial://"), ":")
+		if found {
+			r, err := strconv.Atoi(rate)
+			if err != nil {
+				return fmt.Errorf("serial baud rate not valid: %w", err)
+			}
+			baudRate = r
+		}
+
+		m.endpointConfig = gomavlib.EndpointSerial{
+			Device: device,
+			Baud:   baudRate,
+		}
+	case "tcp":
+		// Use default TCP port if it was not specified
+		port := u.Port()
+		if port == "" {
+			port = "5760"
+		}
+
+		if u.Hostname() == "" {
+			m.endpointConfig = gomavlib.EndpointTCPServer{
+				Address: "0.0.0.0:" + port,
+			}
+		} else {
+			m.endpointConfig = gomavlib.EndpointTCPClient{
+				Address: net.JoinHostPort(u.Hostname(), port),
+			}
+		}
+
+	case "udp":
+		// Use default UDP port if it was not specified
+		port := u.Port()
+		if port == "" {
+			port = "14550"
+		}
+
+		if u.Hostname() == "" {
+			m.endpointConfig = gomavlib.EndpointUDPServer{
+				Address: "0.0.0.0:" + port,
+			}
+		} else {
+			m.endpointConfig = gomavlib.EndpointUDPClient{
+				Address: net.JoinHostPort(u.Hostname(), port),
+			}
+		}
+
+	default:
+		return fmt.Errorf("unknown scheme %q", u.Scheme)
+	}
+
+	// Compile filter
+	m.filter, err = filter.Compile(m.FilterPattern)
+	if err != nil {
+		return fmt.Errorf("compiling filter failed: %w", err)
+	}
+
+	return nil
+}
+
+func (m *Mavlink) Start(acc telegraf.Accumulator) error {
+	// Start MAVLink endpoint
+	connection, err := gomavlib.NewNode(gomavlib.NodeConf{
+		Endpoints:              []gomavlib.EndpointConf{m.endpointConfig},
+		Dialect:                ardupilotmega.Dialect,
+		OutVersion:             gomavlib.V2,
+		OutSystemID:            m.SystemID,
+		StreamRequestEnable:    m.StreamRequestFrequency > 0,
+		StreamRequestFrequency: int(m.StreamRequestFrequency),
+	})
+	if err != nil {
+		return &internal.StartupError{
+			Err:   fmt.Errorf("connecting to mavlink endpoint %s failed: %w", m.URL, err),
+			Retry: true,
+		}
+	}
+	m.connection = connection
+	ctx, cancelFunc := context.WithCancel(context.Background())
+	m.cancel = cancelFunc
+
+	// Start routine to connect to Mavlink and stream out data async
+	m.wg.Add(1)
+	go func(ctx context.Context) {
+		defer m.connection.Close()
+		defer m.wg.Done()
+
+		for {
+			select {
+			case <-ctx.Done():
+				return
+			case evt := <-m.connection.Events():
+				switch evt := evt.(type) {
+				case *gomavlib.EventFrame:
+					if result := convertFrameToMetric(evt.Frame, m.filter); result != nil {
+						result.AddTag("source", m.URL)
+						acc.AddMetric(result)
+					}
path: plugins/inputs/mavlink/mavlink.go
user: login: chrisdalke
body: Makes sense, updated to restructure things this way.
created_at: 2025-05-05T15:45:11Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,263 @@
+//go:generate ../../../tools/readme_config_includer/generator
+package mavlink
+
+import (
+	"context"
+	_ "embed"
+	"errors"
+	"fmt"
+	"net"
+	"net/url"
+	"reflect"
+	"strconv"
+	"strings"
+	"sync"
+
+	"github.com/bluenviron/gomavlib/v3"
+	"github.com/bluenviron/gomavlib/v3/pkg/dialects/ardupilotmega"
+	"github.com/bluenviron/gomavlib/v3/pkg/frame"
+
+	"github.com/influxdata/telegraf"
+	"github.com/influxdata/telegraf/filter"
+	"github.com/influxdata/telegraf/internal"
+	"github.com/influxdata/telegraf/plugins/inputs"
+)
+
+type Mavlink struct {
+	URL                    string   `toml:"url"`
+	SystemID               uint8    `toml:"system_id"`
+	FilterPattern          []string `toml:"filter"`
+	StreamRequestFrequency uint16   `toml:"stream_request_frequency"`
+
+	Log telegraf.Logger `toml:"-"`
path: plugins/inputs/mavlink/mavlink.go
user: login: srebhan
body: ```suggestion
	URL                    string          `toml:"url"`
	SystemID               uint8           `toml:"system_id"`
	FilterPattern          []string        `toml:"filter"`
	StreamRequestFrequency uint16          `toml:"stream_request_frequency"`
	Log                    telegraf.Logger `toml:"-"`
```
created_at: 2025-05-07T08:29:00Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,263 @@
+//go:generate ../../../tools/readme_config_includer/generator
+package mavlink
+
+import (
+	"context"
+	_ "embed"
+	"errors"
+	"fmt"
+	"net"
+	"net/url"
+	"reflect"
+	"strconv"
+	"strings"
+	"sync"
+
+	"github.com/bluenviron/gomavlib/v3"
+	"github.com/bluenviron/gomavlib/v3/pkg/dialects/ardupilotmega"
+	"github.com/bluenviron/gomavlib/v3/pkg/frame"
+
+	"github.com/influxdata/telegraf"
+	"github.com/influxdata/telegraf/filter"
+	"github.com/influxdata/telegraf/internal"
+	"github.com/influxdata/telegraf/plugins/inputs"
+)
+
+type Mavlink struct {
+	URL                    string   `toml:"url"`
+	SystemID               uint8    `toml:"system_id"`
+	FilterPattern          []string `toml:"filter"`
+	StreamRequestFrequency uint16   `toml:"stream_request_frequency"`
+
+	Log telegraf.Logger `toml:"-"`
+
+	filter         filter.Filter
+	connection     *gomavlib.Node
+	endpointConfig gomavlib.EndpointConf
+	cancel         context.CancelFunc
+	wg             sync.WaitGroup
+}
+
+//go:embed sample.conf
+var sampleConfig string
+
+func (*Mavlink) SampleConfig() string {
+	return sampleConfig
+}
+
+func (m *Mavlink) Init() error {
+	// Set default values
+	if m.URL == "" {
+		m.URL = "tcp://127.0.0.1:5760"
+	}
+
+	// Handle the different Mavlink endpoint schemata
+	u, err := url.Parse(m.URL)
+	if err != nil {
+		return fmt.Errorf("invalid url: %w", err)
+	}
+
+	switch u.Scheme {
+	case "serial":
+		// Parse serial URL by hand, because it is not a compliant URL.
+		// Use the default baudrate if not specified
+		baudRate := 57600
+		device, rate, found := strings.Cut(strings.TrimPrefix(m.URL, "serial://"), ":")
+		if found {
+			r, err := strconv.Atoi(rate)
+			if err != nil {
+				return fmt.Errorf("serial baud rate not valid: %w", err)
+			}
+			baudRate = r
+		}
+
+		m.endpointConfig = gomavlib.EndpointSerial{
+			Device: device,
+			Baud:   baudRate,
+		}
+
+	case "tcp":
+		// Use default TCP port if it was not specified
+		port := u.Port()
+		if port == "" {
+			port = "5760"
+		}
+
+		if u.Hostname() == "" {
+			return errors.New("tcp client requires a hostname")
+		}
+
+		m.endpointConfig = gomavlib.EndpointTCPClient{
+			Address: net.JoinHostPort(u.Hostname(), port),
+		}
+
+	case "tcpserver":
+		// Use default TCP port if it was not specified
+		port := u.Port()
+		if port == "" {
+			port = "5760"
+		}
+
+		// Use default host 0.0.0.0 (bind on all interfaces)
+		// if host was not specified
+		hostname := u.Hostname()
+		if hostname == "" {
+			hostname = "0.0.0.0"
+		}
+
+		m.endpointConfig = gomavlib.EndpointTCPServer{
+			Address: net.JoinHostPort(hostname, port),
+		}
+
+	case "udp":
+		// Use default UDP port if it was not specified
+		port := u.Port()
+		if port == "" {
+			port = "14550"
+		}
+
+		if u.Hostname() == "" {
+			return errors.New("udp client requires a hostname")
+		}
+
+		m.endpointConfig = gomavlib.EndpointUDPClient{
+			Address: net.JoinHostPort(u.Hostname(), port),
+		}
+
+	case "udpserver":
+		// Use default UDP port if it was not specified
+		port := u.Port()
+		if port == "" {
+			port = "14550"
+		}
+
+		// Use default host 0.0.0.0 (bind on all interfaces)
+		// if host was not specified
+		hostname := u.Hostname()
+		if hostname == "" {
+			hostname = "0.0.0.0"
+		}
+
+		m.endpointConfig = gomavlib.EndpointUDPServer{
+			Address: net.JoinHostPort(hostname, port),
+		}
+
+	default:
+		return fmt.Errorf("unknown scheme %q", u.Scheme)
+	}
+
+	// Compile filter
+	m.filter, err = filter.Compile(m.FilterPattern)
+	if err != nil {
+		return fmt.Errorf("compiling filter failed: %w", err)
+	}
+
+	return nil
+}
+
+func (m *Mavlink) Start(acc telegraf.Accumulator) error {
+	// Start MAVLink endpoint
+	connection, err := gomavlib.NewNode(gomavlib.NodeConf{
+		Endpoints:              []gomavlib.EndpointConf{m.endpointConfig},
+		Dialect:                ardupilotmega.Dialect,
+		OutVersion:             gomavlib.V2,
+		OutSystemID:            m.SystemID,
+		StreamRequestEnable:    m.StreamRequestFrequency > 0,
+		StreamRequestFrequency: int(m.StreamRequestFrequency),
+	})
+	if err != nil {
+		return &internal.StartupError{
+			Err:   fmt.Errorf("connecting to mavlink endpoint %s failed: %w", m.URL, err),
+			Retry: true,
+		}
+	}
+	m.connection = connection
+	ctx, cancelFunc := context.WithCancel(context.Background())
+	m.cancel = cancelFunc
+
+	// Start routine to connect to Mavlink and stream out data async
+	m.wg.Add(1)
+	go func(ctx context.Context) {
+		defer m.connection.Close()
+		defer m.wg.Done()
+
+		for {
+			select {
+			case <-ctx.Done():
+				return
+			case evt := <-m.connection.Events():
+				switch evt := evt.(type) {
+				case *gomavlib.EventFrame:
+					m.handleFrame(acc, evt.Frame)
+				case *gomavlib.EventChannelOpen:
+					m.Log.Tracef("Mavlink channel opened")
+				case *gomavlib.EventChannelClose:
+					m.Log.Tracef("Mavlink channel closed")
+				case *gomavlib.EventParseError:
+					m.Log.Tracef("Mavlink parse error: %s", evt.Error.Error())
path: plugins/inputs/mavlink/mavlink.go
user: login: srebhan
body: ```suggestion
					m.Log.Tracef("Mavlink parse error: %v", evt.Error)
```
created_at: 2025-05-07T08:30:39Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,263 @@
+//go:generate ../../../tools/readme_config_includer/generator
+package mavlink
+
+import (
+	"context"
+	_ "embed"
+	"errors"
+	"fmt"
+	"net"
+	"net/url"
+	"reflect"
+	"strconv"
+	"strings"
+	"sync"
+
+	"github.com/bluenviron/gomavlib/v3"
+	"github.com/bluenviron/gomavlib/v3/pkg/dialects/ardupilotmega"
+	"github.com/bluenviron/gomavlib/v3/pkg/frame"
+
+	"github.com/influxdata/telegraf"
+	"github.com/influxdata/telegraf/filter"
+	"github.com/influxdata/telegraf/internal"
+	"github.com/influxdata/telegraf/plugins/inputs"
+)
+
+type Mavlink struct {
+	URL                    string   `toml:"url"`
+	SystemID               uint8    `toml:"system_id"`
+	FilterPattern          []string `toml:"filter"`
+	StreamRequestFrequency uint16   `toml:"stream_request_frequency"`
+
+	Log telegraf.Logger `toml:"-"`
+
+	filter         filter.Filter
+	connection     *gomavlib.Node
+	endpointConfig gomavlib.EndpointConf
+	cancel         context.CancelFunc
+	wg             sync.WaitGroup
+}
+
+//go:embed sample.conf
+var sampleConfig string
+
+func (*Mavlink) SampleConfig() string {
+	return sampleConfig
+}
+
+func (m *Mavlink) Init() error {
+	// Set default values
+	if m.URL == "" {
+		m.URL = "tcp://127.0.0.1:5760"
+	}
+
+	// Handle the different Mavlink endpoint schemata
+	u, err := url.Parse(m.URL)
+	if err != nil {
+		return fmt.Errorf("invalid url: %w", err)
+	}
+
+	switch u.Scheme {
+	case "serial":
+		// Parse serial URL by hand, because it is not a compliant URL.
+		// Use the default baudrate if not specified
+		baudRate := 57600
+		device, rate, found := strings.Cut(strings.TrimPrefix(m.URL, "serial://"), ":")
+		if found {
+			r, err := strconv.Atoi(rate)
+			if err != nil {
+				return fmt.Errorf("serial baud rate not valid: %w", err)
+			}
+			baudRate = r
+		}
+
+		m.endpointConfig = gomavlib.EndpointSerial{
+			Device: device,
+			Baud:   baudRate,
+		}
+
+	case "tcp":
+		// Use default TCP port if it was not specified
+		port := u.Port()
+		if port == "" {
+			port = "5760"
+		}
+
+		if u.Hostname() == "" {
+			return errors.New("tcp client requires a hostname")
+		}
+
+		m.endpointConfig = gomavlib.EndpointTCPClient{
+			Address: net.JoinHostPort(u.Hostname(), port),
+		}
+
+	case "tcpserver":
+		// Use default TCP port if it was not specified
+		port := u.Port()
+		if port == "" {
+			port = "5760"
+		}
+
+		// Use default host 0.0.0.0 (bind on all interfaces)
+		// if host was not specified
+		hostname := u.Hostname()
+		if hostname == "" {
+			hostname = "0.0.0.0"
+		}
+
+		m.endpointConfig = gomavlib.EndpointTCPServer{
+			Address: net.JoinHostPort(hostname, port),
+		}
+
+	case "udp":
+		// Use default UDP port if it was not specified
+		port := u.Port()
+		if port == "" {
+			port = "14550"
+		}
+
+		if u.Hostname() == "" {
+			return errors.New("udp client requires a hostname")
+		}
+
+		m.endpointConfig = gomavlib.EndpointUDPClient{
+			Address: net.JoinHostPort(u.Hostname(), port),
+		}
+
+	case "udpserver":
+		// Use default UDP port if it was not specified
+		port := u.Port()
+		if port == "" {
+			port = "14550"
+		}
+
+		// Use default host 0.0.0.0 (bind on all interfaces)
+		// if host was not specified
+		hostname := u.Hostname()
+		if hostname == "" {
+			hostname = "0.0.0.0"
+		}
+
+		m.endpointConfig = gomavlib.EndpointUDPServer{
+			Address: net.JoinHostPort(hostname, port),
+		}
+
+	default:
+		return fmt.Errorf("unknown scheme %q", u.Scheme)
+	}
+
+	// Compile filter
+	m.filter, err = filter.Compile(m.FilterPattern)
+	if err != nil {
+		return fmt.Errorf("compiling filter failed: %w", err)
+	}
+
+	return nil
+}
+
+func (m *Mavlink) Start(acc telegraf.Accumulator) error {
+	// Start MAVLink endpoint
+	connection, err := gomavlib.NewNode(gomavlib.NodeConf{
+		Endpoints:              []gomavlib.EndpointConf{m.endpointConfig},
+		Dialect:                ardupilotmega.Dialect,
+		OutVersion:             gomavlib.V2,
+		OutSystemID:            m.SystemID,
+		StreamRequestEnable:    m.StreamRequestFrequency > 0,
+		StreamRequestFrequency: int(m.StreamRequestFrequency),
+	})
+	if err != nil {
+		return &internal.StartupError{
+			Err:   fmt.Errorf("connecting to mavlink endpoint %s failed: %w", m.URL, err),
+			Retry: true,
+		}
+	}
+	m.connection = connection
+	ctx, cancelFunc := context.WithCancel(context.Background())
+	m.cancel = cancelFunc
+
+	// Start routine to connect to Mavlink and stream out data async
+	m.wg.Add(1)
+	go func(ctx context.Context) {
+		defer m.connection.Close()
+		defer m.wg.Done()
+
+		for {
+			select {
+			case <-ctx.Done():
+				return
+			case evt := <-m.connection.Events():
+				switch evt := evt.(type) {
+				case *gomavlib.EventFrame:
+					m.handleFrame(acc, evt.Frame)
+				case *gomavlib.EventChannelOpen:
+					m.Log.Tracef("Mavlink channel opened")
+				case *gomavlib.EventChannelClose:
+					m.Log.Tracef("Mavlink channel closed")
+				case *gomavlib.EventParseError:
+					m.Log.Tracef("Mavlink parse error: %s", evt.Error.Error())
+				case *gomavlib.EventStreamRequested:
+					m.Log.Tracef("Issued stream request to system %d, component %d", evt.SystemID, evt.ComponentID)
+				default:
+					m.Log.Tracef("Unhandled Mavlink event: %T", evt)
path: plugins/inputs/mavlink/mavlink.go
user: login: srebhan
body: ```suggestion
					m.Log.Tracef("Unhandled Mavlink event type: %T", evt)
```
created_at: 2025-05-07T08:31:02Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,263 @@
+//go:generate ../../../tools/readme_config_includer/generator
+package mavlink
+
+import (
+	"context"
+	_ "embed"
+	"errors"
+	"fmt"
+	"net"
+	"net/url"
+	"reflect"
+	"strconv"
+	"strings"
+	"sync"
+
+	"github.com/bluenviron/gomavlib/v3"
+	"github.com/bluenviron/gomavlib/v3/pkg/dialects/ardupilotmega"
+	"github.com/bluenviron/gomavlib/v3/pkg/frame"
+
+	"github.com/influxdata/telegraf"
+	"github.com/influxdata/telegraf/filter"
+	"github.com/influxdata/telegraf/internal"
+	"github.com/influxdata/telegraf/plugins/inputs"
+)
+
+type Mavlink struct {
+	URL                    string   `toml:"url"`
+	SystemID               uint8    `toml:"system_id"`
+	FilterPattern          []string `toml:"filter"`
+	StreamRequestFrequency uint16   `toml:"stream_request_frequency"`
+
+	Log telegraf.Logger `toml:"-"`
+
+	filter         filter.Filter
+	connection     *gomavlib.Node
+	endpointConfig gomavlib.EndpointConf
+	cancel         context.CancelFunc
+	wg             sync.WaitGroup
+}
+
+//go:embed sample.conf
+var sampleConfig string
+
+func (*Mavlink) SampleConfig() string {
+	return sampleConfig
+}
+
+func (m *Mavlink) Init() error {
+	// Set default values
+	if m.URL == "" {
+		m.URL = "tcp://127.0.0.1:5760"
+	}
+
+	// Handle the different Mavlink endpoint schemata
+	u, err := url.Parse(m.URL)
+	if err != nil {
+		return fmt.Errorf("invalid url: %w", err)
+	}
+
+	switch u.Scheme {
+	case "serial":
+		// Parse serial URL by hand, because it is not a compliant URL.
+		// Use the default baudrate if not specified
+		baudRate := 57600
+		device, rate, found := strings.Cut(strings.TrimPrefix(m.URL, "serial://"), ":")
+		if found {
+			r, err := strconv.Atoi(rate)
+			if err != nil {
+				return fmt.Errorf("serial baud rate not valid: %w", err)
+			}
+			baudRate = r
+		}
+
+		m.endpointConfig = gomavlib.EndpointSerial{
+			Device: device,
+			Baud:   baudRate,
+		}
+
+	case "tcp":
+		// Use default TCP port if it was not specified
+		port := u.Port()
+		if port == "" {
+			port = "5760"
+		}
+
+		if u.Hostname() == "" {
+			return errors.New("tcp client requires a hostname")
+		}
+
+		m.endpointConfig = gomavlib.EndpointTCPClient{
+			Address: net.JoinHostPort(u.Hostname(), port),
+		}
+
+	case "tcpserver":
+		// Use default TCP port if it was not specified
+		port := u.Port()
+		if port == "" {
+			port = "5760"
+		}
+
+		// Use default host 0.0.0.0 (bind on all interfaces)
+		// if host was not specified
+		hostname := u.Hostname()
+		if hostname == "" {
+			hostname = "0.0.0.0"
+		}
+
+		m.endpointConfig = gomavlib.EndpointTCPServer{
+			Address: net.JoinHostPort(hostname, port),
+		}
+
+	case "udp":
+		// Use default UDP port if it was not specified
+		port := u.Port()
+		if port == "" {
+			port = "14550"
+		}
+
+		if u.Hostname() == "" {
+			return errors.New("udp client requires a hostname")
+		}
+
+		m.endpointConfig = gomavlib.EndpointUDPClient{
+			Address: net.JoinHostPort(u.Hostname(), port),
+		}
+
+	case "udpserver":
+		// Use default UDP port if it was not specified
+		port := u.Port()
+		if port == "" {
+			port = "14550"
+		}
+
+		// Use default host 0.0.0.0 (bind on all interfaces)
+		// if host was not specified
+		hostname := u.Hostname()
+		if hostname == "" {
+			hostname = "0.0.0.0"
+		}
+
+		m.endpointConfig = gomavlib.EndpointUDPServer{
+			Address: net.JoinHostPort(hostname, port),
+		}
+
+	default:
+		return fmt.Errorf("unknown scheme %q", u.Scheme)
+	}
+
+	// Compile filter
+	m.filter, err = filter.Compile(m.FilterPattern)
+	if err != nil {
+		return fmt.Errorf("compiling filter failed: %w", err)
+	}
+
+	return nil
+}
+
+func (m *Mavlink) Start(acc telegraf.Accumulator) error {
+	// Start MAVLink endpoint
+	connection, err := gomavlib.NewNode(gomavlib.NodeConf{
+		Endpoints:              []gomavlib.EndpointConf{m.endpointConfig},
+		Dialect:                ardupilotmega.Dialect,
+		OutVersion:             gomavlib.V2,
+		OutSystemID:            m.SystemID,
+		StreamRequestEnable:    m.StreamRequestFrequency > 0,
+		StreamRequestFrequency: int(m.StreamRequestFrequency),
+	})
+	if err != nil {
+		return &internal.StartupError{
+			Err:   fmt.Errorf("connecting to mavlink endpoint %s failed: %w", m.URL, err),
+			Retry: true,
+		}
+	}
+	m.connection = connection
+	ctx, cancelFunc := context.WithCancel(context.Background())
+	m.cancel = cancelFunc
+
+	// Start routine to connect to Mavlink and stream out data async
+	m.wg.Add(1)
+	go func(ctx context.Context) {
+		defer m.connection.Close()
+		defer m.wg.Done()
+
+		for {
+			select {
+			case <-ctx.Done():
+				return
+			case evt := <-m.connection.Events():
+				switch evt := evt.(type) {
+				case *gomavlib.EventFrame:
+					m.handleFrame(acc, evt.Frame)
+				case *gomavlib.EventChannelOpen:
+					m.Log.Tracef("Mavlink channel opened")
+				case *gomavlib.EventChannelClose:
+					m.Log.Tracef("Mavlink channel closed")
+				case *gomavlib.EventParseError:
+					m.Log.Tracef("Mavlink parse error: %s", evt.Error.Error())
+				case *gomavlib.EventStreamRequested:
+					m.Log.Tracef("Issued stream request to system %d, component %d", evt.SystemID, evt.ComponentID)
+				default:
+					m.Log.Tracef("Unhandled Mavlink event: %T", evt)
+				}
+			}
+		}
+	}(ctx)
+
+	return nil
+}
+
+func (*Mavlink) Gather(telegraf.Accumulator) error {
+	return nil
+}
+
+func (m *Mavlink) Stop() {
+	m.cancel()
+	m.wg.Wait()
+}
+
+// Convert a Mavlink frame into a telegraf Metric.
+func (m *Mavlink) handleFrame(acc telegraf.Accumulator, frm frame.Frame) {
+	v := reflect.Indirect(reflect.ValueOf(frm.GetMessage()))
+	t := v.Type()
+
+	name := internal.SnakeCase(strings.TrimPrefix(t.Name(), "Message"))
+
+	if m.filter != nil && !m.filter.Match(name) {
+		return
+	}
path: plugins/inputs/mavlink/mavlink.go
user: login: srebhan
body: ```suggestion
	name := internal.SnakeCase(strings.TrimPrefix(t.Name(), "Message"))
	if m.filter != nil && !m.filter.Match(name) {
		return
	}
```
created_at: 2025-05-07T08:31:49Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,96 @@
+# MavLink Input Plugin
+
+This plugin collects metrics from [MavLink][mavlink]-compatible flight
+controllers such as [ArduPilot][ardupilot] or [PX4][px4] to live ingest
+flight metrics from unmanned systems (drones, planes, boats, etc.)
+Currently the ArduPilot-specific Mavlink dialect is used, check the
+[Mavlink documentation][mavlink_docs] for more details and the various
+messages available.
+
+> [!WARNING]
+> This plugin potentially generates a large amount of data. If your output plugin cannot handle the rate of messages, use [Metric filters][metric_filters] to limit the metrics written to outputs, and/or the `filters` configuration parameter to limit which Mavlink messages this plugin parses.
+
+⭐ Telegraf v1.35.0
+🏷️ iot
+💻 all
+
+[ardupilot]: https://ardupilot.org/
+[mavlink]: https://mavlink.io/
+[mavlink_docs]: https://mavlink.io/en/messages/ardupilotmega.html
+[metric_filters]: ../../../docs/CONFIGURATION.md#metric-filtering
+[px4]: https://px4.io/
+
+## Global configuration options <!-- @/docs/includes/plugin_config.md -->
+
+In addition to the plugin-specific configuration settings, plugins support
+additional global and plugin configuration settings. These settings are used to
+modify metrics, tags, and field or create aliases and configure ordering, etc.
+See the [CONFIGURATION.md][CONFIGURATION.md] for more details.
+
+[CONFIGURATION.md]: ../../../docs/CONFIGURATION.md#plugins
+
+## Configuration
+
+```toml @sample.conf
+# Read metrics from a Mavlink flight controller.
+[[inputs.mavlink]]
+  ## Flight controller URL supporting serial port, UDP and TCP connections.
+  ## Options are documented at
+  ##   https://mavsdk.mavlink.io/v1.4/en/cpp/guide/connections.html.
+  ##
+  ## Examples:
+  ## - Serial port: serial:///dev/ttyACM0:57600
+  ## - TCP client:  tcp://192.168.1.12:5760
+  ## - UDP client:  udp://192.168.1.12:14550
+  ## - TCP server:  tcpserver://:5760
+  ## - UDP server:  udpserver://:14550
+  # url = "tcp://127.0.0.1:5760"
+
+  ## Filter to specific messages. Only the messages in this list will be parsed.
+  ## If blank or unset, all messages will be accepted. Glob syntax is accepted.
+  ## Each message in this list should be lowercase camel_case, with "message_"
+  ## prefix removed, eg: "global_position_int", "attitude"
+  # filter = []
+
+  ## Mavlink system ID for Telegraf. Only used if the mavlink plugin is sending 
+  ## messages, eg. when `stream_request_frequency` is 0 (see below.)
+  # system_id = 254
+
+  ## Determines whether the plugin sends requests to subscribe to data.
+  ## In mavlink, stream rates must be configured before data is received.
+  ## This config item sets the rate in Hz, with 0 disabling the request.
+  ## 
+  ## This frequency should be set to 0 if your software already controls the 
+  ## rates using REQUEST_DATA_STREAM or MAV_CMD_SET_MESSAGE_INTERVAL
+  ## (See https://mavlink.io/en/mavgen_python/howto_requestmessages.html)
+  # stream_request_frequency = 4
+```
+
+## Metrics
+
+Each supported Mavlink message translates to one metric group, and fields
+on the Mavlink message are converted to fields in telegraf.
+
+The name of the Mavlink message is translated into lowercase and any
+leading text `message_` is dropped.
+
+For example, the message
+[ATTITUDE][attitude] will become an
+`attitude` metric, with all fields copied from its Mavlink message definition.
path: plugins/inputs/mavlink/README.md
user: login: Hipska
body: ```suggestion
For example, the message [ATTITUDE][attitude] will become an `attitude` metric,
with all fields copied from its Mavlink message definition.
```
created_at: 2025-05-08T13:54:02Z
author_association: CONTRIBUTOR

PR comments:
user: login: chrisdalke
created_at: 2025-05-05T15:46:15Z
body: @srebhan Updated to address all the comments above!
author_association: CONTRIBUTOR
user: login: telegraf-tiger[bot]
created_at: 2025-05-08T15:28:50Z
body: Download PR build artifacts for [linux_amd64.tar.gz](https://output.circle-artifacts.com/output/job/4962ddd1-bfc4-47fc-b047-26a2a14177dd/artifacts/0/build/dist/telegraf-1.35.0~52a1d5e9_linux_amd64.tar.gz), [darwin_arm64.tar.gz](https://output.circle-artifacts.com/output/job/b3735a82-6613-42f3-b650-c5ff13dabf39/artifacts/0/build/dist/telegraf-1.35.0~52a1d5e9_darwin_arm64.tar.gz), and [windows_amd64.zip](https://output.circle-artifacts.com/output/job/a8f86f6f-bcda-4f28-8db0-a37c26205e9a/artifacts/0/build/dist/telegraf-1.35.0~52a1d5e9_windows_amd64.zip).
Downloads for additional architectures and packages are available below.

:relaxed: This pull request doesn't significantly change the Telegraf binary size (less than 1%) 

<details>
<summary> :package: Click here to get additional PR build artifacts</summary>

## Artifact URLs

|                                                                               DEB                                                                                |                                                                               RPM                                                                                |                                                                                      TAR GZ                                                                                      |                                                                                    ZIP                                                                                     |
|------------------------------------------------------------------------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| [amd64.deb](https://output.circle-artifacts.com/output/job/4962ddd1-bfc4-47fc-b047-26a2a14177dd/artifacts/0/build/dist/telegraf_1.35.0~52a1d5e9-0_amd64.deb)     | [aarch64.rpm](https://output.circle-artifacts.com/output/job/f0fbbbee-9011-4b53-9cf0-96aade24f2b6/artifacts/0/build/dist/telegraf-1.35.0~52a1d5e9-0.aarch64.rpm) | [darwin_amd64.tar.gz](https://output.circle-artifacts.com/output/job/14fd7b8e-4a17-48a4-a9aa-d404509433a1/artifacts/0/build/dist/telegraf-1.35.0~52a1d5e9_darwin_amd64.tar.gz)   | [windows_amd64.zip](https://output.circle-artifacts.com/output/job/a8f86f6f-bcda-4f28-8db0-a37c26205e9a/artifacts/0/build/dist/telegraf-1.35.0~52a1d5e9_windows_amd64.zip) |
| [arm64.deb](https://output.circle-artifacts.com/output/job/f0fbbbee-9011-4b53-9cf0-96aade24f2b6/artifacts/0/build/dist/telegraf_1.35.0~52a1d5e9-0_arm64.deb)     | [armel.rpm](https://output.circle-artifacts.com/output/job/6fb9a1cb-67d7-4c54-93e8-ed22c0fae921/artifacts/0/build/dist/telegraf-1.35.0~52a1d5e9-0.armel.rpm)     | [darwin_arm64.tar.gz](https://output.circle-artifacts.com/output/job/b3735a82-6613-42f3-b650-c5ff13dabf39/artifacts/0/build/dist/telegraf-1.35.0~52a1d5e9_darwin_arm64.tar.gz)   | [windows_arm64.zip](https://output.circle-artifacts.com/output/job/a8f86f6f-bcda-4f28-8db0-a37c26205e9a/artifacts/0/build/dist/telegraf-1.35.0~52a1d5e9_windows_arm64.zip) |
| [armel.deb](https://output.circle-artifacts.com/output/job/6fb9a1cb-67d7-4c54-93e8-ed22c0fae921/artifacts/0/build/dist/telegraf_1.35.0~52a1d5e9-0_armel.deb)     | [armv6hl.rpm](https://output.circle-artifacts.com/output/job/c6c0f40e-7bda-4b23-9e09-cea7a8383f8f/artifacts/0/build/dist/telegraf-1.35.0~52a1d5e9-0.armv6hl.rpm) | [freebsd_amd64.tar.gz](https://output.circle-artifacts.com/output/job/4962ddd1-bfc4-47fc-b047-26a2a14177dd/artifacts/0/build/dist/telegraf-1.35.0~52a1d5e9_freebsd_amd64.tar.gz) | [windows_i386.zip](https://output.circle-artifacts.com/output/job/a8f86f6f-bcda-4f28-8db0-a37c26205e9a/artifacts/0/build/dist/telegraf-1.35.0~52a1d5e9_windows_i386.zip)   |
| [armhf.deb](https://output.circle-artifacts.com/output/job/c6c0f40e-7bda-4b23-9e09-cea7a8383f8f/artifacts/0/build/dist/telegraf_1.35.0~52a1d5e9-0_armhf.deb)     | [i386.rpm](https://output.circle-artifacts.com/output/job/3aa62ec6-b636-4150-888f-398302bc08c0/artifacts/0/build/dist/telegraf-1.35.0~52a1d5e9-0.i386.rpm)       | [freebsd_armv7.tar.gz](https://output.circle-artifacts.com/output/job/c6c0f40e-7bda-4b23-9e09-cea7a8383f8f/artifacts/0/build/dist/telegraf-1.35.0~52a1d5e9_freebsd_armv7.tar.gz) |                                                                                                                                                                            |
| [i386.deb](https://output.circle-artifacts.com/output/job/3aa62ec6-b636-4150-888f-398302bc08c0/artifacts/0/build/dist/telegraf_1.35.0~52a1d5e9-0_i386.deb)       | [ppc64le.rpm](https://output.circle-artifacts.com/output/job/89b1531e-c032-4867-a7f0-eef980ca3704/artifacts/0/build/dist/telegraf-1.35.0~52a1d5e9-0.ppc64le.rpm) | [freebsd_i386.tar.gz](https://output.circle-artifacts.com/output/job/3aa62ec6-b636-4150-888f-398302bc08c0/artifacts/0/build/dist/telegraf-1.35.0~52a1d5e9_freebsd_i386.tar.gz)   |                                                                                                                                                                            |
| [mips.deb](https://output.circle-artifacts.com/output/job/73800d4e-30b1-4eb4-9bf3-4b065f96bcf6/artifacts/0/build/dist/telegraf_1.35.0~52a1d5e9-0_mips.deb)       | [riscv64.rpm](https://output.circle-artifacts.com/output/job/76ce41be-a5d3-4c87-8351-5c968c5413b2/artifacts/0/build/dist/telegraf-1.35.0~52a1d5e9-0.riscv64.rpm) | [linux_amd64.tar.gz](https://output.circle-artifacts.com/output/job/4962ddd1-bfc4-47fc-b047-26a2a14177dd/artifacts/0/build/dist/telegraf-1.35.0~52a1d5e9_linux_amd64.tar.gz)     |                                                                                                                                                                            |
| [mipsel.deb](https://output.circle-artifacts.com/output/job/92bd56aa-1e6d-4c52-b201-b9e36b5ebae9/artifacts/0/build/dist/telegraf_1.35.0~52a1d5e9-0_mipsel.deb)   | [s390x.rpm](https://output.circle-artifacts.com/output/job/99a3d74c-c665-4223-9a39-498f08dc4007/artifacts/0/build/dist/telegraf-1.35.0~52a1d5e9-0.s390x.rpm)     | [linux_arm64.tar.gz](https://output.circle-artifacts.com/output/job/f0fbbbee-9011-4b53-9cf0-96aade24f2b6/artifacts/0/build/dist/telegraf-1.35.0~52a1d5e9_linux_arm64.tar.gz)     |                                                                                                                                                                            |
| [ppc64el.deb](https://output.circle-artifacts.com/output/job/89b1531e-c032-4867-a7f0-eef980ca3704/artifacts/0/build/dist/telegraf_1.35.0~52a1d5e9-0_ppc64el.deb) | [x86_64.rpm](https://output.circle-artifacts.com/output/job/4962ddd1-bfc4-47fc-b047-26a2a14177dd/artifacts/0/build/dist/telegraf-1.35.0~52a1d5e9-0.x86_64.rpm)   | [linux_armel.tar.gz](https://output.circle-artifacts.com/output/job/6fb9a1cb-67d7-4c54-93e8-ed22c0fae921/artifacts/0/build/dist/telegraf-1.35.0~52a1d5e9_linux_armel.tar.gz)     |                                                                                                                                                                            |
| [riscv64.deb](https://output.circle-artifacts.com/output/job/76ce41be-a5d3-4c87-8351-5c968c5413b2/artifacts/0/build/dist/telegraf_1.35.0~52a1d5e9-0_riscv64.deb) |                                                                                                                                                                  | [linux_armhf.tar.gz](https://output.circle-artifacts.com/output/job/c6c0f40e-7bda-4b23-9e09-cea7a8383f8f/artifacts/0/build/dist/telegraf-1.35.0~52a1d5e9_linux_armhf.tar.gz)     |                                                                                                                                                                            |
| [s390x.deb](https://output.circle-artifacts.com/output/job/99a3d74c-c665-4223-9a39-498f08dc4007/artifacts/0/build/dist/telegraf_1.35.0~52a1d5e9-0_s390x.deb)     |                                                                                                                                                                  | [linux_i386.tar.gz](https://output.circle-artifacts.com/output/job/3aa62ec6-b636-4150-888f-398302bc08c0/artifacts/0/build/dist/telegraf-1.35.0~52a1d5e9_linux_i386.tar.gz)       |                                                                                                                                                                            |
|                                                                                                                                                                  |                                                                                                                                                                  | [linux_mips.tar.gz](https://output.circle-artifacts.com/output/job/73800d4e-30b1-4eb4-9bf3-4b065f96bcf6/artifacts/0/build/dist/telegraf-1.35.0~52a1d5e9_linux_mips.tar.gz)       |                                                                                                                                                                            |
|                                                                                                                                                                  |                                                                                                                                                                  | [linux_mipsel.tar.gz](https://output.circle-artifacts.com/output/job/92bd56aa-1e6d-4c52-b201-b9e36b5ebae9/artifacts/0/build/dist/telegraf-1.35.0~52a1d5e9_linux_mipsel.tar.gz)   |                                                                                                                                                                            |
|                                                                                                                                                                  |                                                                                                                                                                  | [linux_ppc64le.tar.gz](https://output.circle-artifacts.com/output/job/89b1531e-c032-4867-a7f0-eef980ca3704/artifacts/0/build/dist/telegraf-1.35.0~52a1d5e9_linux_ppc64le.tar.gz) |                                                                                                                                                                            |
|                                                                                                                                                                  |                                                                                                                                                                  | [linux_riscv64.tar.gz](https://output.circle-artifacts.com/output/job/76ce41be-a5d3-4c87-8351-5c968c5413b2/artifacts/0/build/dist/telegraf-1.35.0~52a1d5e9_linux_riscv64.tar.gz) |                                                                                                                                                                            |
|                                                                                                                                                                  |                                                                                                                                                                  | [linux_s390x.tar.gz](https://output.circle-artifacts.com/output/job/99a3d74c-c665-4223-9a39-498f08dc4007/artifacts/0/build/dist/telegraf-1.35.0~52a1d5e9_linux_s390x.tar.gz)     |                                                                                                                                                                            |
</details>
author_association: CONTRIBUTOR
user: login: booo
created_at: 2025-05-18T08:59:47Z
body: Sorry for some "off topic noise" now.

Can I test this easily within the docker ecosystem?

I tried:

```
➜  telegraf sudo docker run --rm --volume $PWD/config.toml:/etc/telegraf/telegraf.conf quay.io/influxdb/telegraf-nightly:latest          
2025-05-18T08:54:35Z I! Loading config: /etc/telegraf/telegraf.conf
2025-05-18T08:54:35Z E! loading config file /etc/telegraf/telegraf.conf failed: error parsing mavlink, undefined but requested input: mavlink
➜  telegraf cat config.toml 
[[inputs.mavlink]]
url = "udpserver://:14550"
```

Would love to beta test this!
author_association: NONE
user: login: chrisdalke
created_at: 2025-06-09T15:09:07Z
body: @booo Thanks for trying this. At the time you had tried it out, the nightly build hadn't succeeded yet. However it looks like now the nightly build is updated and you should be able to try again! 

If that does not work, try building from source. 
author_association: CONTRIBUTOR
user: login: booo
created_at: 2025-06-13T17:54:54Z
body: > @booo Thanks for trying this. At the time you had tried it out, the nightly build hadn't succeeded yet. However it looks like now the nightly build is updated and you should be able to try again!
> 
> If that does not work, try building from source.

In the end I did build it from source. I created a small Dockerfile and Compose file: https://gitlab.com/searchwing/development/groundstation/antennagroundstation/mavlink-telegraf

So far it works quite well! Well done. Looking forward to create some fancy dashboards for our test flights :)
author_association: NONE
user: login: Hipska
created_at: 2025-06-13T19:31:26Z
body: Thanks for sharing, but by having only two plugins activated, you could build yourself a much smaller binary with only those included.
author_association: CONTRIBUTOR