Adding a sensor to mavlink stream

I don’t think mavproxy is forwarding it to the topside computer, experiment sending that straight to the topside computer (

Hi @williangalvani, thanks for the advice - I’ll give it a try on Monday when I’m back with the hardware.

I’m assuming that’s using mavutil.mavlink_connection(udpin:, since I want that address and port to accept messages, but if I’ve misunderstood that directionality I’ll try with udpout (ie if udpin doesn’t work) :slight_smile:

Couldn’t make a connection (raised an exception) when I tried using udpin: udpout: connected but still didn’t come up with any value on QGC.

Having looked over the pymavlink examples page again, udpout seems like it’s the correct way to send data from the calling computer (the companion in this case) to the receiving computer (the top computer here), given

udpout : Outputs data to a specified address:port (client).

and the ‘Run pyMavlink on the companion computer’ example shows that it should be followed by wait_conn, so reasonably sure that that part of the process is set up as it’s supposed to be now. wait_conn completes successfully, but the measurement values (via named_value_float_send) still don’t seem to be getting through to QGC unfortunately. Not sure where else issues could be coming in - might have to try running pymavlink on the top computer and seeing if the messages are getting that far.

Finally got back to working on this a bit more. Got pymavlink today and found that my sensor readings are coming through correctly as named value floats, but their ‘time since boot’ was much larger than the normal named value float messages (6.1x10^6 vs 6.5x10^5 ms). Will have to check next week if it’s a scaling issue, or an offset issue from my sensor reading script starting earlier than the normal telemetry, or the normal telemetry script restarting (or at least restarting its boot count) at some point while the sensor script continues.

Also not sure if that’d be causing no displaying, so it’s still possible I’m checking the wrong value in my custom QGC build or something. At least now I know the messages are getting to the top, and can debug a bit more with pymavlink :slight_smile:

Hi @EliotBR,

Thank you for reporting your progress!

You could use mavlogdump to check where your messages don’t match the ones sent by the ROV. I’d take a look at the sysID and CompIP fields as QGC could be using those.

No worries - forums work best when later viewers of posts can refer to useful work and solutions that have already been gone through, including the mistakes that were made along the way. I’m hoping this will be a useful reference for others looking to do something similar in future, and am planning to make a summary once it’s successfully working :slight_smile:

Thanks! I wasn’t aware there were extra fields that don’t come through to pymavlink’s recv_match. I’ll make sure to check those if it’s still not working once I’ve sorted out the timing issue and double-checked the parameters I’ve put in QGC.


Timing difference is now just the small difference between when the two scripts start, which may be problematic for accurate timing later but should at least still allow the readings to display because they’re quite close together. That didn’t solve the issue unfortunately, but one less major thing to be concerned about for now.

@williangalvani I’m not sure how I’d be able to use mavlogdump in this case. I got one of the .bin log files off the pixhawk, but when analysing it I couldn’t find any named_value_float messages at all (there were a bunch of supposedly invalid headers, and then a bunch of parameters with 3 or 4 letter names) - maybe I need to use verbose, although I think I tried that and it didn’t help. I also realised that the bin files won’t help me to tell the difference between pixhawk and companion mavlink messages anyway because my sensor script sends directly to the top computer, so my custom messages shouldn’t even appear in the log file.

The tlogs have both my custom and the normal named_value_float messages, but they seem to just have the same information as a live pymavlink connection (no extra sysID or compIP fields, or anything else, just the usual name, time_boot_ms, and value). I can’t remember if I tried reading the tlogs with verbose, so will do that tomorrow, but it seems like that’s unlikely to be the missing piece given the other things I’ve tried.

Hi @EliotBR

That is actually a Dataflash Log. It doesn’t have mavlink communication, so it is not very relevant to your use case.

This seems to work here:

from pymavlink import mavutil
master = mavutil.mavlink_connection('udpout:localhost:14550', source_system=1)
master.mav.statustext_send(mavutil.mavlink.MAV_SEVERITY_NOTICE, "Qgc will read this".encode())

change localhost to the topside IP, if your qgc read this message, the communication is working and you can try the named_value_float message next.

That’d make sense, thanks!

Absolute legend!
I needed source_system=1 it seems. Thanks for sticking through with me until this got sorted out! :smiley:
I was pretty excited when I heard QGC talking to me :stuck_out_tongue:

1 Like

Here’s the summary

Companion computer

Read & Send Script

Make a script in the ~/companion/tools/ directory that:

  1. Connects to the top computer (requires QGC to be open)
  2. Gets sensor data (float)
  3. Sends the data in a named_value_float message


import time
from pymavlink import mavutil

def wait_conn(device):
    """ Sends a ping to develop UDP communication and waits for a response. """
    msg = None
    global boot_time
    while not msg:
        # boot_time = time.time()
            int((time.time()-boot_time) * 1e6), # Unix time since boot (microseconds)
            0, # Ping number
            0, # Request ping of all systems
            0  # Request ping of all components
        msg = device.recv_match()

if __name__ == '__main__':
    boot_time = time.time()
    # establish connection to top-side computer
    direction = 'udpout'
    device_ip = ''
    port      = '14550'
    params    = [direction, device_ip, port]
    print('connecting to {1} via {0} on port {2}'.format(*params))
    computer  = mavutil.mavlink_connection(':'.join(params), source_system=1)
    print('waiting for confirmation...')
    print('connection success!')
    # connect to sensor and set up output
    # TODO: connect to sensor
    sensor_name = 'MySensor' # MUST be 9 characters or fewer
    while 'reading data':
        value = sensor.get_value() # TODO: implement something like this
            int((time.time() - boot_time) * 1e3), # Unix time since boot (milliseconds)

For my ultrasonic thickness gauge case, I was reading in data with a serial connection and wanted a command-line option to only read and print the sensor data, without connecting to mavlink, so I could use ssh to check if the sensor was working. Code for that can be found here.

Run on startup

Modify ~/companion/.companion.rc, adding a new screen session with an appropriate name, to run right after If the read and send script doesn’t end (likely the case), have an & to run in its own thread

	sudo -H -u pi screen -dm -S mavproxy $COMPANION_DIR/tools/
	sudo -H -u pi screen -dm -S sensor $COMPANION_DIR/tools/ &
	sudo -H -u pi screen -dm -S video $COMPANION_DIR/tools/ 

Restart on telemetry restart

Modify ~/companion/scripts/ to also quit and restart the custom screen session.


screen -X -S sensor quit
screen -X -S mavproxy quit

sudo -H -u pi screen -dm -S mavproxy $COMPANION_DIR/tools/
sudo -H -u pi screen -dm -S sensor $COMPANION_DIR/tools/ &


Follow the instructions to get the code, but before building make the following modifications:

  1. In main ArduSub code file (src/FirmwarePlugin/APM/
// in void ArduSubFirmwarePlugin::_handleNamedValueFloat(mavlink_message_t* message)
    } else if (name == "RollPitch") {
    } else if (name == "MySensor") { // should be the same name as in your companion script
      _infoFactGroup.getFact("mySensor")->setRawValue(value.value); //name for finding in QGC

// ...
const char* APMSubmarineFactGroup::_rollPitchToggleFactName     = "rollPitchToggle";
const char* APMSubmarineFactGroup::_mySensorFactName            = "mySensor";
const char* APMSubmarineFactGroup::_rangefinderDistanceFactName = "rangefinderDistance";

// ...
// in APMSubmarineFactGroup::APMSubmarineFactGroup(QObject* parent)
    , _rollPitchToggleFact     (0, _rollPitchToggleFactName,     FactMetaData::valueTypeDouble)
    , _mySensorFact            (0, _mySensorFactName,            FactMetaData::valueTypeDouble)
    , _rangefinderDistanceFact (0, _rangefinderDistanceFactName, FactMetaData::valueTypeDouble)
// ...
    _addFact(&_rollPitchToggleFact    , _rollPitchToggleFactName);
    _addFact(&_mySensorFact           , _mySensorFactName);
    _addFact(&_rangefinderDistanceFact, _rangefinderDistanceFactName);

// ...
    _rollPitchToggleFact.setRawValue     (2); // 2 shows "Unavailable" in older firmwares
    _mySensorFact.setRawValue            (std::numeric_limits<float>::quiet_NaN()); // display as --.-- when not connected
    _rangefinderDistanceFact.setRawValue (std::numeric_limits<float>::quiet_NaN());
  1. In ArduSub header file (src/FirmwarePlugin/APM/ArduSubFirmwarePlugin.h)
// in class APMSubmarineFactGroup : public FactGroup
    Q_PROPERTY(Fact* inputHold           READ inputHold           CONSTANT)
    Q_PROPERTY(Fact* mySensor            READ mySensor            CONSTANT)
    Q_PROPERTY(Fact* rangefinderDistance READ rangefinderDistance CONSTANT)

// ...
    Fact* inputHold           (void) { return &_inputHoldFact; }
    Fact* mySensor            (void) { return &_mySensorFact; }
    Fact* rangefinderDistance (void) { return &_rangefinderDistanceFact; }

// ...
    static const char* _rollPitchToggleFactName;
    static const char* _mySensorFactName;
    static const char* _rangefinderDistanceFactName;

// ...
    Fact            _rollPitchToggleFact;
    Fact            _mySensorFact;
    Fact            _rangefinderDistanceFact;
  1. In QGC name descriptions file (src/Vehicle/SubmarineFact.json)
    "name":             "rollPitchToggle",
    "shortDesc": "Roll/Pitch Toggle",
    "type":             "int16",
    "enumStrings":      "Disabled,Enabled,Unavailable",
    "enumValues":       "0,1,2"
    "name":             "mySensor",
    "shortDesc": "DisplayName",
    "type":             "float",
    "decimalPlaces":    3,
    "units":            "mm"

then complete the QGC build through QtCreator and run.

Your new sensor should now be available through the QGC sensor display. If it appears but with --.-- as the value then it’s not currently sending any readings, so check that the screen session exists (through ssh, or the list of services at the top of


There will be some time offset between the messages sent by the custom script and those sent by the pixhawk. If doing time-based alignment it’s best to ignore the time for the custom script messages and instead calculate a new time based on the message received before and after in the top-side mavlink stream. Alternatively it may be possible to change the custom script to first make a mavlink connection to the pixhawk, and replace boot_time by the difference between time.time() and the time of the received message, although I haven’t yet tried this and can’t guarantee it would work correctly.


Very nice! Thank you for this!

Do you mind if we put this on our docs at (with proper credits)?

Not at all, I want it to be as useful as possible :slight_smile:
Thanks again for all the help getting it to work in the first place :slight_smile:


I have tried your @EliotBR code and got this error

connecting to localhost via udpin on port 14540
waiting for confirmation...
connection success!
Traceback (most recent call last):
  File "", line 40, in <module>
  File "/home/***/.local/lib/python3.8/site-packages/pymavlink/dialects/v10/", line 20329, in named_value_float_send
    return self.send(self.named_value_float_encode(time_boot_ms, name, value), force_mavlink1=force_mavlink1)
  File "/home/***/.local/lib/python3.8/site-packages/pymavlink/dialects/v10/", line 13784, in send
    buf = mavmsg.pack(self, force_mavlink1=force_mavlink1)
  File "/home/***/.local/lib/python3.8/site-packages/pymavlink/dialects/v10/", line 13336, in pack
    return MAVLink_message.pack(self, mav, 170, struct.pack('<If10s', self.time_boot_ms, self.value,, force_mavlink1=force_mavlink1)
struct.error: argument for 's' must be a bytes object

I couldnt find anything helpfull yet. I would be very happy if you can help me.

Hi @mhcekic, welcome to the forum :slight_smile:

I wrote and posted this code while working for a different company, but it did definitely work at the time.

What kind of setup are you using?

This specifies you’re connecting to something on the same device, on port 14540, so you’ve clearly set up your own endpoints/ports. The code was written for sending messages from the Companion computer to QGroundControl on the topside computer.

Here your traceback specifies you’re using Python 3.8, which isn’t available in the normal Companion software image. The code was written using Python 2.7, which has some differences from Python 3 with respect to bytes and strings. Given it’s complaining that it wants a bytes object you may need to change the sensor_name from the current string (e.g. try sensor_name = b'MySensor').

Note that since your setup is seemingly quite different to what the code was designed for, it’s very possible there will be other required changes for it to work properly.

Hi, I followed @EliotBR 's guide for my sensor and have it working nicely, so thank you! However, we are transitioning our systems to a Navigator/Blue OS setup, and I was wondering if it is possible to reuse the script and settings I used on my Companion computer on my BlueOS computer. If not, do you have any suggestions to how I can achieve similar functionality on BlueOS?



Hi @krisaue,

I’m glad this thread has been helpful for you :slight_smile:

BlueOS (1.1 beta) has an extensions system, which is the recommended approach for making device integrations. The creation process is a bit more involved than just putting a script in a folder but as a result makes extensions much easier to share across vehicles, and to integrate a visual interface where that’s desirable.

In case it’s useful, @rjehangir has been working on an extension for the Cygnus UT gauge, which follows on somewhat from the integration I originally made (above) for Companion, but also includes a web interface that allows logging and labelling measurements.

That extension is not currently registered in the store, so if you want to try it out you’ll need to add it manually via the “+” button in the Installed tab of the Extensions manager:

Field Value
Extension Identifier rjehangir.cygnus
Extension Name Cygnus UT
Docker image rjehangir/blueos-cygnus-ut
Docker tag latest

The “Custom settings” should be set to

 "ExposedPorts": {
   "8000/tcp": {}
  "HostConfig": {
    "PortBindings": {
      "8000/tcp": [
          "HostPort": ""

Hello my friend. Sorry for the translation. First of all, thank you for the above topic. I wrote a script similar to this and was able to send the sensor yield. Now, I want to send the battery data I want to do to pixhawk as a mavlink message. So instead of reading the voltage with a ready module, I want to send the voltage I read to Pixhawk. Any ideas on this?

Hey, how can I use this approach to use Bar02 pressure sensor for depth hold and related purpose?

Currently, I am trying to configure ( run & build ) QGroundControl Source Code ( from github ), to costumize it but failed to rebuild it , as it showing " missing kit error .

I am using :-
Windows 11
Qt creator 5.15

how to configure MS Visual Studio 2019 with Qt Creator