Conductivity Sensor Integration into BlueROV2

Tony, I thought about the actual post to MavLink a bit. The instructions say to use: http://blueos.local:6040/v1/mavlink. blueos.local won’t resolve, so I’ve used the below instead: Typically, MavLink posts to 127.0.0.1:14450 or 127.0.0.1:5790. What should I be using?

Hi @rsheffield99 -

That’s strange - the node-red iframe seems to show that temp and conductivity were undefined? DO and pH don’t seem present?

What version of BlueOS are you using?

It’s a bit strange that blueos.local doesn’t resolve - how did you determine this?

Can you export your flow so I can directly compare it to the functional examples shared in the NodeRed guide? Sorry this has been such a hassle for you! I honestly think writing an extension with a LLM is a faster and more polished solution than wrestling with NodeRed - I moved on from it quickly after publishing that guide! I’m working on documenting just how easy it has become in the age of AI to create software like this, especially with examples to draw from.

Blue OS version 1.4.2.

I tried using http://blueos.local:6040/v1/mavlink in the http node first and got an error message that it was not a good address.

I’m convinced that it is simply an IP address thing at this point. What should I use to replace blueos.local?

Here is the flow. Without the sensors connected, all the sensors will show up as undefined.

The ones in the screen snapshot are showing undefined as I’m still having to reconnect the sensors multiple times to get them all to initialize properly.

flows (3).json (27.9 KB)

Hi @rsheffield99 -

I set the AI on it (opus 4.7), here’s the result which seems reasonable to me:
"
I read both flows carefully and walked the wiring on each path. Here’s the comparison.

The primary bug: paytoqs is wrong on every HTTP request

This is almost certainly why no data reaches mavlink2rest.

In the working flow, every http request node that posts to mavlink2rest is configured with:

"paytoqs": "body"

In the broken flow, every http request node is configured with:

"paytoqs": "ignore"

With paytoqs: "ignore", Node-RED throws away msg.payload and POSTs an empty body. mavlink2rest receives an empty request and rejects it (or silently does nothing). The Mavlink2Rest function nodes are correctly building the NAMED_VALUE_FLOAT JSON, but the http request node is then discarding it.

Fix: open every http request node in the broken flow (the four f3c530276dc7bb59, 333a6e45062d90e5, 0a97ed0176f73252, acfefffcbf81af9d) and set “Use” / “payload” → “a body of the request” (which writes paytoqs: "body" in JSON).

Likely secondary bug: Conductivity returns multi-field strings, breaking Number()

The broken flow has inject nodes O,EC,1, O,TDS,1, O,S,1, O,SG,1 that turn on multiple output fields on the Atlas EZO conductivity sensor. When that’s done, an R command returns something like 1413,701,1.5,1.013 (CSV), not a single number.

The mavlink2rest function does:

msg.payload = Number(msg.payload)

Number("1413,701,1.5,1.013")NaN. mavlink2rest will reject NaN even after the paytoqs fix.

Compounding this: those O,EC,1 / O,TDS,0 / O,S,0 / O,SG,0 inject nodes are not wired to anything, so they don’t actually configure the sensor — they’re orphaned. You either need to wire them to the conductivity serial-out (9426969d53379fec) to put the EZO into single-field mode, or split the CSV in the function before calling Number().

Wiring/structure differences worth knowing

The working flow’s pattern for each value is:

serial in → JSON parser → change(payload=payload.V) → Mavlink2Rest fn → http POST(body)

The broken flow uses two patterns inconsistently:

  • Conductivity & Temp use a serial requestmavlink2rest fn → http POST

  • DO & pH use serial requestMavlink2RestHandEntered fn → http POST, while a parallel mavlink2rest fn for the same sensor sits on the canvas with no output wired (e27bb938f438efda for DO at 1090,600 and 1020a160f3c41b5e for PH at 670,720). Those are dead nodes.

Other dead/orphaned bits in the broken flow:

  • OverVoltageFilter (242706ddfd761e2c) and Change to Value (3daf71b4e3817787) feed a UI text widget but have no upstream wiring — they never receive input.

  • serial out 9426969d53379fec has no inputs (so the O,* config injects do nothing).

  • serial in c448fc110d1f4bca has empty wires (output goes nowhere).

These don’t break the mavlink path but suggest the flow was mid-edit.

Minor note

URLs differ — working uses blueos.local:6040, broken uses 192.168.2.2:6040. Both should reach mavlink2rest from a BlueOS Node-RED extension; not an issue if the IP is correct on the target system.

TL;DR

Fix paytoqs: "ignore"paytoqs: "body" on the four http request nodes. That alone should get data flowing. Then for the conductivity path specifically, either wire the O,EC,1/O,TDS,0/O,S,0/O,SG,0 injects into the conductivity serial-out so the sensor returns a single number, or change the conductivity function to parse the first CSV field before Number().

Want me to produce a patched version of the broken JSON with these fixes applied?
"

:star_struck: :hushed_face: right?
I naturally said yes - try out this patched flow:

Patched_NVF_flow.json (28.1 KB)

No guarantee since I can’t test with your hardware, but it may get you on the right path!

Tony, thanks.

The nodes that set values for the conductivity sensor only run once and set parameters in firmware on the sensor itself. They never need to be run again and is why they show up as orphans. The Conductivity sensor then only puts out one “string” value, which I convert to a number.

I similarly turn on DO percentage values and turn off mgl once; then it returns a single percentage value that is converted to a number. This leaves orphan nodes in my flow; just in case I need to reset the sensor in the future.

I’m looking at the difference of the patched flow for the http nodes and see the paytog settings correction. I don’t immediately see how to set that in the dialog. Got any hints/directions on how to do that?

Hi @rsheffield99 -

Apparently the newer version of NodeRED BlueOS has hides the dropdown for that paytog setting, which is annoying! The fix has some different solutions -

  1. Load the fixed flow where it is adjusted

  2. Delete and re-drag the node. A freshly-created http request node on a current Node-RED version writes paytoqs: "" (empty) instead of "ignore", and the runtime treats empty as “send body” for POST” . So: Note the URL and any wires going in/out
    Delete the http request node
    Drag a new “http request” from the palette
    Set Method to POST, URL to http://192.168.2.2:6040/v1/mavlink
    Re-connect the input wire from the mavlink2rest function node
    Deploy

  3. Method-toggle trick. Open the http request node, change Method to GET — the Payload dropdown should appear. Change it from “Ignore” to “Append to query-string parameters”. Then change Method back to POST. Click Done, deploy. (This forces paytoqs to a non-“ignore” value, after which POST will send the body.)

You could verify 2 or 3 by performing on your existing node and then exporting, and checking the json for the change.

Tony, I used method 3. Looking at the flow now with BBEdit, it says “query” rather than “ignore”. Not “body” like in the patch, but hopefully it will work. If not, I’ll go into BBEdit and simply change paytogs to “body”. I will make the change on the unit in the Boat as well and test soon.

Tony,

Tested on the Boat with paytogs set to both query and body. Got all 4 sensors going to Mavlink that way. Still no NAMED_VALUE_FLOAT values in the tflog or csv files created by QGC and I still get this strange entry when trying to monitor Mavlink with the Cockpit tool. Clearly, I am not getting NAMED_VALUE_FLOAT

Tony, I found a LUA script for sending NAMED_VALUE_FLOAT to QGC and it uses 251 for the ID. Confirmed that ID in the “normal” Mavlink ID list. Will change to that and try it out.

Last time I was trying to send custom named values to QGC it was necessary to make a custom version of QGC that knows about those different names for it to be able to use them. It seems it still has a predefined list of them, so I’d assume that’s still required.

That image looks like a normal NAMED_VALUE_FLOAT to me, just not the specific one you’re trying to send. That one is apparently from ArduPilot’s wind vane library.

MAVLink has various messages that are used for multiple devices / data sources, and a live viewer of MAVLink messages is not great for differentiating those. Granted if it’s not changing at all then your messages aren’t being received by Cockpit, but that will be much easier to see if you check in Cockpit’s Tools / Data-lake, and search for “NAMED_”, which should show you any and all named values being received, differentiated by their name.

The Mavlink ID for NAMED_VALUE_FLOAT is definitely 251 (shows up in the QGC Mavlink inspection tool). I’ve changed to that, but still see nothing posted with the names/values I set in NAMED_VALUE_FLOAT. Still get the same AppWinDir though and no sensor data overriding it.

I also looked in Cockpit’s Data-Lake tool for NAMED_ and got more of the same wind data.

I checked out the Mavlink Endpoint selection in the BlueOS page; it shows Mavlink2Rest being at 127.0.0.1:14000. Tried putting that into the http request node (replacing 192.168.2.2:6040) and left the /v1/mavlink part of the address. Got a connection error.

What is happening behind the curtain at the 6040 udp port and the /v1/mavlink directory? Does this somehow map to the Mavlink2Rest IP/port? Is this also why the ID is set to 255 rather than 251 for NAMED_VALUE_FLOAT?

Not sure how that’s relevant? MAVLink2REST sends messages by their name, not their message ID.

That tells you BlueOS core’s MAVLink routing service has a MAVLink endpoint that communicates with MAVLink2REST on the local network (via the 127.0.0.1 loopback IP address), at port 14000.

BlueOS’s internal service connections are not intended to be used or spoofed by external programs/integrations, especially while they’re already in use. MAVLink2REST provides its public REST API at port 6040, so that is where it should be communicated with.

Node-RED is a BlueOS Extension, so it runs in its own (isolated) Docker container, which makes use of a network bridge to access the core BlueOS network. In the default permissions of the Node-RED Extension, that is seemingly implemented via the blueos.local domain. For general Extension development we generally recommend a different domain, to avoid confusion with the normal BlueOS network (especially since some people change their base domain, in which case blueos.local potentially comes across as weird/wrong to use).

Servers host to a port, and 6040 is the one BlueOS uses for MAVLink2REST. Visiting the bare port route shows the primary interface of the service, and you can generally find out more about a BlueOS service API via its /docs route. MAVLink2REST offers a /v1/mavlink route that in the context of a GET request is a raw page with all the latest MAVLink message states in it (which you can refresh / fetch to “get” the latest data), and in the context of a POST request allows "post"ing MAVLing messages to some other MAVLink system or component.

The service README has some extra context.

There are several different types of ID involved here:

  1. MAVLink system IDs, which generally correspond to a particular vehicle/system
  2. MAVLink component IDs, which correspond to a single component within a given system
  3. MAVLink message IDs, which link a message to its interpretation (e.g. parameter and name details)

Eliot,
Thank you.

The links to the ReadMe were helpful and I understand the IDs used in the exchange. I am still testing the Post to see if my messages are at least getting to the “local” port for export to the Cockpit standalone version I have on my Windows laptop. Is it correct to assume that the Mavlink Inspector in the BlueOS display shows the “local” data within the BlueOS Docker and that the Data Lake tool in Cockpit should show the Mavlink’d data on the laptop’s side? Does the Data Lake tool pull that data from the .BIN file Tony describes in his example (excerpt below)? I’ve not yet been able to find that .BIN file or set the logging parameter to collect the data when the Boat is not armed. Can you help me with that task? Also, how do the NAMED_VALUE_FLOAT parameters end up with the NVAL labels shown in the example?

Mavlink2Rest - Adding Data to the Overall Vehicle Log

Mavlink is a very lightweight messaging protocol for communicating between various devices, particularly the ArduSub and ArduRover autopilot processes. The Mavlink2Rest system makes the autopilot telemetry stream available for both input and output of messages, which if properly formatted get saved to the mission log. These ArduPilot .BIN logs are incredibly useful when analyzing a mission. They contain time-synced values for all sensors and other inputs and outputs of the autopilot system, and can easily be reviewed with this handy web tool. By sending data to the Mavlink2Rest interface as a NamedValue Float, it will automatically be logged to the .BIN file when the vehicle is armed (or always if the logging parameter is configured this way.) In the future, displaying these values in real time in the form of a widget in the Cockpit interface will be possible.

Hi @rsheffield99 -

To log at all times, change the LOG_DISARMED parameter to 1 (true.)

Can you share the NodeRed extensions permissions? On the extensions manager, click installed extensions, and pick edit on the node-red entry. Copy and paste the text in the permissions section here for us to review.

The data in Mavlink Inspector should mirror the Cockpit Data Lake, I believe?

It may be worth updating to the latest stable version of BlueOS, 1.4.3, as we continue to troubleshoot your issue…

As for the address to target, I notice my vibe-coded extensions seem to prefer to use the following, which as Eliot mentioned may be more correct and reliable than the blueos.local:

http://host.docker.internal/mavlink2rest/mavlink

Tony,

Looks like I will have to go to the Boat to change the ARM parameter as the settings wheel for that mini widget is greyed out in my development unit, which isn’t connected to the Boat.

Below is what I got from the Node-Red extensions on my development unit. If this is what you’re looking for, I’ll confirm the Boat extension is the same.

For clarification: I should use http://host.docker.internal/mavlink2rest/mavlink without using the udp port ID :6040 anywhere in the address?

{
  "ExposedPorts": {
    "80/tcp": {}
  },
  "HostConfig": {
    "Privileged": true,
    "Binds": [
      "/usr/blueos/extensions/node-red:/data:rw",
      "/etc/hostname:/etc/hostname:ro",
      "/dev:/dev:rw",
      "/:/home/workspace/host:rw"
    ],
    "PortBindings": {
      "80/tcp": [
        {
          "HostPort": ""
        }
      ]
    }
  }
}

Hi @rsheffield99 -

Changing the parameter requires a connection to the Boat, yes. I’m not sure what mini widget you’re referencing?

Keep using the port 6040, just update that base URL.

Everything looks correct in those permissions to me, just making sure there were no “gotchas”!

Hi @rsheffield99
Apologies, the permissions would need to change if that address I shared this AM would work. Best to stick with what is shared in the original NodeRed guide!

To move forward, we need to verify that NodeRed is sending data. To get it showing up in Cockpit from there, you’ll need to setup a lua script to forward the messages onward, changing ID values in the process that allow it to properly be consumed into the Data Lake. Did you catch that in the guide?

It looks like there’s a missing network bridge in the settings you posted. Relatedly, it seems there may have been some issues with deployment of the latest version of the Extension, which we’re working on resolving now.

In the meantime, could you try setting custom settings for the Extension as follows (noting the new “ExtraHosts” field):

{
  "ExposedPorts": {
    "80/tcp": {}
  },
  "HostConfig": {
    "ExtraHosts": [
      "host.docker.internal:host-gateway"
    ],
    "Privileged": true,
    "Binds": [
      "/usr/blueos/extensions/node-red:/data:rw",
      "/etc/hostname:/etc/hostname:ro",
      "/dev:/dev:rw",
      "/:/home/workspace/host:rw"
    ],
    "PortBindings": {
      "80/tcp": [
        {
          "HostPort": ""
        }
      ]
    }
  }
}

and then change your Node-RED flow to use the URL http://host.docker.internal:6040/v1/mavlink.

BlueOS provides some nice name → port remapping functionality for its services, and MAVLink2REST does not require the v1 as part of its mavlink route, so I believe it should also work to use http://host.docker.internal/mavlink2rest/mavlink instead, if you prefer.

Exactly.

There are different kinds of logs for different parts of the system.

  1. The autopilot records its sensor data and internal state in DataFlash (.bin) logs, which use a predefined set of messages to track everything that it did and experienced during operation. You can think of this like the autopilot firmware’s personal diary - it includes high frequency data and information that may not be needed or accessed by any other parts of the system, and has limited space to store things so it packs the data into a binary file format.
  2. Some information is relevant to external components and services though, and a common way of communicating that is using the MAVLink protocol, which has its own set of messages. As an example, a control station software like Cockpit may need to send movement commands, and receive status updates about things like the current vehicle orientation and the battery’s state of charge. Components making use of MAVLink to communicate may record those messages in MAVLink telemetry log (.tlog) files, but not all parts of the system have access to all the communication that’s happening.

Cockpit has different sources of data, and it collects them into a “lake” for use throughout the application (including by widgets, custom Actions, etc). MAVLink message data is one of the things that gets included in that, with effort taken to split out tracking of different message instances (so if you alternate between different names in NAMED_VALUE_FLOAT messages, it tracks the latest value for each name, instead of just whichever one was last).

This is the intersection of “the autopilot has received a message (that happened to be via MAVLink), and it records it in its personal diary”. NVAL is the autopilot’s efficiently packed version of a named value, for its internal logging purposes.[1]

As @tony-white has just pointed out, if you want to also forward those messages to Cockpit (e.g. for live previewing) you’ll need to set up that functionality in the autopilot, which requires

  1. adding the provided Lua script to the relevant location, and then
  2. enabling it to be run by setting the autopilot’s SCR_ENABLE parameter to 1.

  1. As something of a side note, that is apparently not being found/included by the auto-generator for the .bin log documentation I linked you to, so I’ll chase that up in ArduPilot. ↩︎

Eliot/Tony,

Thanks for the input. It was very helpful in figuring out what is going on.

I made all the changes and I appear to be putting out the proper name/value pairs to http://host.docker.internal/mavlink2rest/mavlink. It took me a couple of tries to get the privileges to update and I got error messages until I got the ExtraHosts specification to save. The http request nodes started to run w/o errors then. I am not seeing updates to my NAMED_VALUE_FLOAT (s) in the Data Lake or with the Mavlink inspectors (either on the BlueOS or Cockpit side).

Yes, I picked up on the LUA script so I can do a display on the Cockpit screen once I get the sensor values into the Data Lake. I also checked and I’m running v4.6.1 of the autopilot. I used the name in the link NV_forward.lua for the file name when I inserted it. I restarted the autopilot with the dialog, but didn’t set the SCR_ENABLE to 1.

Nice!

I think the MAVLink Inspector in BlueOS might filter for only values from the autopilot. Can you try checking the interface at http://192.168.2.2:6040/mavlink? Hopefully there’ll be a “System ID: 255” section down the bottom that includes the NAMED_VALUE_FLOATs you’re sending.

The autopilot doesn’t forward named value messages by default, and Cockpit filters for MAVLink messages from the autopilot by default, so if the script isn’t forwarding the messages (because it’s not enabled) then Cockpit won’t be receiving them.