Unsupported Sensors? Try Arduino + Lua!

Some of you already know my enthusiasm for ArduPilot Lua scripting, and if you’re reading this, you’ll probably pick up on it pretty quickly. When I discovered the I2C, UART, and CAN bindings, the gears started turning, wondering how well we could leverage those comms protocols to expand the ArduPilot peripheral ecosystem without requiring firmware updates and dedicated, baked-in drivers.

Today, I uploaded an Arduino library to speed that endeavor along. It allows you to use an Arduino (or compatible) microcontroller as an autopilot connected I2C device, acting as a bridge between unsupported sensors and the autopilot.

To install the I2C_Slave library, download the repository as a zip file and follow the instructions for importing a zip file. Or, if you prefer PlatformIO, unzip the library to your project’s lib/ directory.

The library includes an example sketch and Lua script that supports multiple DS18B20 OneWire temperature sensors connected to the Arduino. Temperature data gets passed across the I2C bus and is made available to the user as MAVLink named float values, as shown here in the Mission Planner “Quick” tab:


Beyond just providing temperature values for display, such a script could include logic to warn of impending overtemperature or fire, RTL in an out of limits scenario, reduce power to a non-critical script controlled component to reduce battery consumption, etc. Other sensor types could easily be connected and added to the sketch, allowing for a broad fusion of data within the scripting environment that would otherwise be impossible using the autopilot alone.

The project’s README file should be laid out well enough for you to begin using the library on your own, but please fire up the discussion here and ask questions or provide feedback!

To date, I’ve tested the library with an Arduino Pro Mini and a MatekH743 autopilot running Rover 4.3. I did successfully compile the code for an ESP32 but did not hardware test it. All development was done under PlatfomIO, but it is Arduino IDE compatible and compliant with the Arduino library specification. If this gains some traction and is tested more widely in the community, I will explore uploading it as a community/3rd party library available within the Arduino Library Registry (Library Manager). Pull requests welcome!

Many sincere thanks to @iampete, @hendjosh, @geofrancis, @ktrussell, @swebre, and others who’ve joined in the discussions regarding scripting and use cases. If this library proves useful, it’s not much of a stretch to use it as a template for a similar CAN communication library or craft a means to poll/control many of the I/O functions on a microcontroller via Lua.


Very cool, il have a test rig set up tonight.

1 Like

Awesome! :partying_face:

I also had the Idea som years ago that you can hook an Arduino up to Ardupilot and push data there so that they will be logged.
My Idea was that you could somehow specify a category and sensor name under which this show up in the log. Like BARO/Baro.alt
I wanted to use this for logging AOA/SSA data fom a DIY vane as well as temperatures.

Is this possible with your library?

Yes, see this example:


All working after figuring out to set the script to use the correct i2c port on a pixhawk and converting to celcius.

Glad to see it!

I pushed a commit to add the following to the beginning of the example script:

-- Cube and Matek autopilots typically make I2C bus 0 available
-- some Pixhawk (and other) autopilots only expose I2C bus 1
-- set the I2C_BUS variable as appropriate for your board
local I2C_BUS           =    0


local arduino_i2c = i2c.get_device(I2C_BUS, SLAVE_ADDR)
1 Like

At the request of @geofrancis, the repo now includes another example for sampling all of the Arduino’s analog pins and viewing their values as named floats.

analogRead() returns a signed int. It would’ve been easier to write the example by casting the value to an unsigned integer (or ignoring the sign, since the value should never be negative), but I wanted to show another example of data unpacking, so I kept the signed integer type intact.

The Lua unpack_int() function should work for any signed integer type, regardless of size, so long as endianness remains the same.

There is also now a very basic example included - this library’s equivalent of blink.ino. Make sure you’re using the latest 0.2.0 version or higher (now rebased as the only commit, since things are so fresh, and there wasn’t much to lose by squashing revision history to this point).


Hey Yuri!
Very kind for you to mention me, but I really haven’t contributed besides questions. However, I think you are chasing something that will really be useful for us and others. Onward my Friend! And sincere Thanks.


1 Like

The initial commit would’ve handled character buffers poorly. That’s now fixed, and there’s a “Hello world” example demonstrating it.

I am electing, at least for now, to forgo handling Arduino String types, since they are implemented as very heavyweight objects, and I expect that this library will often be used on small boards with limited memory. I’d rather leave that memory available for your use. Your sketch will compile if you attempt to store String variables in the virtual registers, but the results will be unpredictable and probably not desirable.

Compiler optimization would probably leave such an implementation out of any sketch that did not otherwise use String variables, so if there is a strong desire for their inclusion, I can revisit the topic.

If you didn’t want to worry about data/object unpacking in Lua, you could copy numeric values into character buffers in your sketches, using sprintf() for integer types and dtostrf() for floating point values. Store those character representations of your sensor values into the virtual registers. In your Lua script, use the tonumber() function to convert a retrieved string back to a numeric value for use in your script logic or named float display.

Awesome Job, thank you!!

Yesterday, I tried to connect Adafruit Precision NXP 9-DOF Breakout Board - FXOS8700 + FXAS21002 for its magnetometer sensor to have a compass on my copter with no luck.

I will try with your library now and lets hope that everything will be OK.

Thank you again!

I’m afraid there are some limitations within scripting, and I don’t see a way to make compass data available to the EKF through the scripting engine.

You’ll be able to get raw magnetometer data using my library and an accompanying script on your autopilot, but I don’t think there is a way to calibrate it and use it for native position and navigation.

As an alternative, Matek makes an inexpensive, firmware supported GPS/compass module.

If you were very motivated, you could have a look at ardupilot/AP_Compass_Backend.h and write a C++ class to extend it for direct support of your module or a scripting driver for native use of scripted compass modules. However, this will require a custom build of the firmware, and I don’t anticipate much motivation from the dev team to accept a pull request for either of these options, since supported GPS/compass modules are cheap and readily available (coupled with this concern for flash size).

Thank you for your response.

Although motivated I am not in a position of writing this part of the firmware on my own yet. I have to finish first my Stroustrup books…! This sensor was just in my drawer (for another project) so i thought i could give it a try. So at the moment, the Matek GPS module seems an easier solution

Thank you again!

1 Like

@pketiki, it hadn’t quite occurred to me until I had a separate conversation about other unsupported hardware, but this library could potentially be used to emulate a supported device on the I2C bus. You’d have to know the address and data protocol for the supported hardware, and then write some code to translate data from the unsupported device into the format of the supported one, bypassing Lua scripting entirely. It’s probably not worth the time that it would take vs the cost of a supported magnetometer, but it can be done!

A little project update:

This morning I tested the library with an ESP32 and a Seeed XIAO (SAMD21) board. Things compiled and uploaded fine, but behavior was not quite what I expected. I did get data transfer over the I2C bus, but it was error prone.

I definitely want to support as many hardware variants as possible, and, in particular, the two boards I just mentioned. I know the ESP32 is very popular, and the XIAO boards are really cool little devices with tiny footprints that I feel are ideal for the application.

However, for the time being (v0.2.x), recommend sticking with an Atmega-based board until I can work through some of the oddities.

1 Like

I pushed a commit this morning that should make the library more widely compatible. The XIAO board is now working quite nicely.

However, ESP32 compatibility remains broken. The I2C slave implementation in the ESP32 Arduino framework has apparently been problematic for some time, and a somewhat recent PR to fix it does not appear to have provided a complete solution. I’m afraid I don’t have the depth of knowledge required to get it working properly.

1 Like

Hi @geofrancis, what are those nice multi pin sockets in the background of your last photo?

@pauljeff The big ones are 15 pin gx20 connectors. They come in 3 sizes, gx12, gx16 and gx20.

What if we don’t understand Lua ? :frowning:

You learn it, using examples and on-line videos.

haha nice suggestion

In a separate/private conversation, I was asked whether Lua provides a built-in map() function like the one provided by the Arduino libraries. I thought the answer might be of use to some of you:

To my knowledge, there is no map() function provided natively in Lua.

Here is the Arduino map() function from WMath.cpp:

long map(long x, long in_min, long in_max, long out_min, long out_max)
  return (x - in_min) * (out_max - out_min) / (in_max - in_min) + out_min;

In Lua, it looks like this:

local function map(x, in_min, in_max, out_min, out_max)
    return (x - in_min) * (out_max - out_min) / (in_max - in_min) + out_min

But the Arduino function does integer math, so if you want (nearly) identical output:

local function map(x, in_min, in_max, out_min, out_max)
    return math.floor((x - in_min) * (out_max - out_min) / (in_max - in_min) + out_min)
1 Like