Node & Firmata PTZ Camera

This article describes the implementation of a WiFi-connected PTZ camera system built around a Raspberry Pi Zero 2W. The Raspberry Pi hosts a web interface using Node.js and communicates with an Arduino Nano over USB serial using the Firmata protocol. A remote client browser captures input from its connected game controller and sends these control inputs over Wi-Fi to the camera system. Node.js receives and processes the inputs, then translates them into commands that are sent to the Arduino Nano. The Arduino, in turn, controls the stepper motors responsible for the camera’s pan, tilt and zoom/ focus movements.

Introduction

The author’s particular area of interest lies in assembling WiFi-connected devices, typically FPV systems incorporating a camera together with some form of device feedback and control. Such devices are generally operated remotely through a browser running on a distantly connected client machine. The device itself typically consists of a small Linux-based single-board computer, responsible for WiFi communication with the remote client, coupled to a microcontroller that performs the real-time hardware control tasks.

In previous projects, I have relied on the Apache web server, running on the SBC, to serve the web interface, with C code, PHP and JavaScript scripting providing the application logic. I have also used I²C communication to interface with a slave microcontroller, together with dedicated firmware to perform the real-time control functions. While this architecture is workable, it has proven less than ideal in practice. It introduces unnecessary complexity, divides the application logic between disparate software environments, and requires the development and maintenance of dedicated microcontroller firmware.

Although PHP running under Apache is sufficiently performant for the application, the memory footprint of the web server is a significant concern, particularly on a Linux SBC with only 0.5 GiB of RAM, of which 256 MB must be reserved for GPU-based image processing. There is also a more practical difficulty with the use of the optimized Arduino I²C Slave Peripheral routines developed for earlier projects. Although these routines perform well, their complexity makes them difficult for other users to understand or adapt. Consequently, despite their technical merits, they have come to be regarded as having relatively little practical value beyond the projects for which they were originally developed.

Accordingly, in developing this PTZ camera system, which serves as a pilot for a more sophisticated remotely operated telescope project, I decided to investigate Node.js as an alternative platform for both serving the web interface and implementing the application logic. Node.js offers a particularly attractive combination of execution speed and modest memory requirements. At the same time, I investigated the Firmata serial protocol, together with Node.js’ firmata.js library, as a replacement for I²C communication.

This approach eliminates the need to develop dedicated firmware for the microcontroller, since the logic governing real-time operation can instead be implemented in JavaScript on the SBC. The microcontroller effectively becomes a runtime-configured peripheral, with its behavior established through the Firmata USB serial connection between the Linux host and the 8-bit slave processor.

Firmata?

Firmata is, fundamentally, a communication protocol that provides an interface between a host system and a microcontroller running a suitable implementation of Firmata firmware. Rather than developing application-specific firmware for the slave microcontroller, the developer installs the appropriate Firmata firmware and then configures and controls the microcontroller at runtime. Hardware configuration and manipulation are performed by calling generic functions on the host system through a Firmata client library.

There is, however, no single, universally applicable implementation of Firmata. Multiple versions of the protocol exist, along with several Firmata firmware implementations, with support for a range of microcontrollers. There are also a variety of client libraries, each intended for use with a particular host programming language. Identifying a combination that worked reliably proved to be something of a challenge. The following is the configuration with which I ultimately had success:

  • Firmata Protocol 2.5
  • ConfigurableFirmata Server Arduino Library version 2.10.1
  • Arduino Nano 5V 16MHz ATmega328p
  • firmata.js Node.js JavaScript Client Library version 2.3.0
  • Node.js JavaScript runtime Engine version 12.22.12

Firmata Server Setup

The initial setup is straightforward. Download ConfigurableFirmata Server Arduino Library version 2.10.1, then follow these installation instructions to install and compile the library for an Arduino Nano fitted with an ATmega328P. Although visually and electrically similar, a Nano fitted with an ATmega328PB is not compatible with ConfigurableFirmata.

As its name suggests, ConfigurableFirmata is designed to be configured according to the hardware functions required by the application. In this project, its optional accelStepper module is used to control the stepper motors. Consequently, the corresponding header include in the master sketch, ConfigurableFirmata.ino, must be uncommented and enabled. This file is located in the examples/ConfigurableFirmata sub-directory of the library.

Early testing on an Arduino Pro Mini.

ConfigurableFirmata Issue

The introduction of the AccelStepper Arduino library was something of a watershed moment for hobbyist stepper-motor users. It provides both position and speed-based control, with smooth acceleration and deceleration and non-blocking operation. Its ease of use, reliability, support for multiple motors, and ability to perform other tasks while motors are moving have made it extremely popular.

Unfortunately, even the most recent ConfigurableFirmata interface to AccelStepper is incomplete. It supports position-based stepper control, but omits the library’s direct speed-control functionality. In particular, it uses the setMaxSpeed(speed)/run() pairing for position-oriented operations such as runToPosition(), while omitting the setSpeed(speed)/runSpeed() pairing intended for speed-based control.

For our PTZ control application, we need reliable access to both position and speed-based stepper control. To achieve joystick-based speed control, we have therefore had to work around ConfigurableFirmata’s limitations by using its position-control functionality in an unconventional way.

Firmata Client Javascript

The complete listing of the pathfinder project’s JavaScript code is linked below. Before examining it in detail, however, there are two aspects that warrant discussion: first, how the Firmata server is configured for stepper motor control, and second, how the project’s implementation makes unconventional use of its AccelStepper module.

The slave-control.js file contains a class built around the firmata.js client library, providing the interface through which the Firmata server is configured and controlled. The file begins by importing the Firmata module:

// slave-control.js
const Board = require("firmata");

Further into the class, a configuration method establishes the type, pin assignment, and operating mode for each stepper motor connected to the slave micro-controller. For brevity, only the configuration for the first motor is shown below:


  configureSlave() {
    this.board.accelStepperConfig({
      deviceNum: Device.PAN,
      type: this.board.STEPPER.TYPE.FOUR_WIRE,
      motorPin1: 2,    // IN2 for 28byj-48-5v unipolar stepper motor drive board.
      motorPin2: 3,    // IN4                        ''
      motorPin3: 4,    // IN1                        ''
      motorPin4: 5,    // IN3                        ''
      stepSize: this.board.STEPPER.STEP_SIZE.WHOLE,
    });
    console.log("x device 0 configured");
    .
    .
    .
  }

Once initialisation of the server board is complete, including the configuration described above, the class provides a number of functions for controlling the individual stepper motors. Again, ConfigurableFirmata’s access to the underlying AccelStepper module, provides a rather different control model from that required by this application. It expects the host to specify a target position relative to the motor’s current position, together with a maximum speed at which that position should be reached. The module then determines the direction of travel, accelerates the motor towards the target, and subsequently decelerates it as the target is approached. This behavior is useful for positional control, but is not what we require for a responsive PTZ system.

Our PTZ application instead requires an interface through which the step rate and direction of each motor can be changed rapidly and repeatedly. To achieve this using ConfigurableFirmata’s AccelStepper calls, we effectively co-opt its position-only-oriented interface. We repeatedly supply it with a deliberately distant and otherwise irrelevant target position, using that target solely to establish the desired direction of travel, together with the speed at which the motor should run.

setStepperValue(deviceNum, value) {

    // Request the current position first (this is asynchronous)
    this.board.accelStepperReportPosition(deviceNum, (currentPosition) => {
      // Store the reported position
      this.posnValue[deviceNum] = currentPosition;
      .
      .
      . 
        // Calculate new target position and speed
        const newPosition = 
                 currentPosition + Math.round(value * this.positionIncrement);
        const newSpeed = Math.round((value - DEAD_BAND) * this.maxStepsPerSec);
        .
        .
        .
          // Set the speed (this command returns immediately)
          this.board.accelStepperSpeed(deviceNum, newSpeed);
          // Set arbitrary, large target to indicate direction
          this.board.accelStepperTo(deviceNum, newPosition); 
        .
        .
        .
    });
  }

We therefore have direct control over the speed and direction of all three stepper motors, with each motor governed by the deflection of the corresponding joystick axis on the client’s game controller. This is a simplification of the actual implementation, but it should make the otherwise non-intuitive portion of the JavaScript code easier to understand.

The Node.js runtime executes an index.js application script, which instantiates the class described above and serves the index.html interface to the client browser. Once the page has loaded, JavaScript running in the browser communicates with the corresponding back-end JavaScript functions running on the Node.js server. Data is exchanged in both directions, with the client transmitting joystick inputs to control the PTZ system, and the server returning information describing the current state of the device.

Hardware & Wiring

As this was primarily a path-finding exercise, the hardware implementation was deliberately modest. I simply gutted a $5 WiFi camera with its original, rather inadequate pan and tilt mechanism, retaining the small stepper motors, and replaced its original PCB with an inexpensive OV5647-based 5MP camera module. The Arduino Nano and stepper motor control circuitry are housed within the original enclosure, while the camera module and Nano are connected to a Raspberry Pi Zero 2W mounted externally.

The camera lens used was an inexpensive 25 mm, 5MP telephoto lens with an M12 × 0.5 mounting thread. More capable lenses are readily available, although their prices tend to increase commensurately with their optical performance.

Only the pan and tilt functions were implemented in the interim prototype, although each axis was fully tested and is supported by the source code. The zoom/ focus function, though implemented in software, was not connected to the prototype hardware.

OS Setup

Because the the Node.js package Firmata.js used in the project is quite dated, and depends on several other similarly outdated Node.js packages which can be difficult to install and compile on more recent operating systems, I have chosen the relatively dated and minimal Buster release of the Raspbian OS. This provides the necessary compilation support for Firmata.js’ dependent modules, while also keeping the operating system’s memory footprint to a minimum.

The Firmata.js client library for Node.js, which supports the Firmata protocol version 2.5 used by this project, requires Node.js 12.22.x. We will therefore install the early 12.22.12 release of Node.js.

We will also install a version of Motion, the motion detection and video surveillance application, that remains compatible with both the legacy camera stack and the older operating system. Motion will provide the video stream for the application, while MotionEye, its web-based front-end, will provide a more convenient means of configuring and managing Motion. The two work remarkably well together on the Raspberry Pi Zero 2W, despite the platform’s limited memory.

The Raspberry Pi Zero 2W can be prepared for first use by following this extensive installation and configuration guide.

The github repository from which the project files are made available is at https://github.com/Green-Bug-Eyed-Monster/PTZ-Camera/.

Setting up MotionEye

Once MotionEye is running, its web interface is available on your local WLAN at http://camera.local:8765/. The default username is admin, while the default password is blank.

The first step is to select Add a Camera. In my case, the camera is of the MMAL type, although the appropriate selection will depend on the camera hardware being used. Under the Video Device tab, set a frame rate and resolution appropriate to the intended application.

If, like me, you make hasty purchasing decisions and have fitted a telephoto lens without a daylight IR filter, you may find that the resulting image has a distinctly pink cast. This can be corrected by adding the following line to Video Device > Extra Motion Options:

mmalcam_control_params -awb off -awbg 0.9,1.1 -sa 90 -co 0 -br 35

These parameters disable automatic white balance and apply a manual correction to the red and blue gain, while also adjusting saturation, contrast, and brightness to produce a more natural image.

To provide the Node.js-served PTZ application with a video feed, first enable Video Streaming in MotionEye. The available frame-rate and video-quality settings can then be adjusted to suit the application, with the resulting stream available at http://camera.local:8081/.

MotionEye also provides direct access to a JPEG snapshot of the current camera output. This can be viewed in a browser at http://camera.local:8765/picture/1/current/.

If you wish to experiment with video capture triggered by motion detection, there is a slightly non-obvious prerequisite: the Movies tab must first be enabled. Once enabled, Motion Detection can be activated, allowing its various detection and recording parameters to be configured and tested.

PTZ Camera UI

Node.js, running on the Pi Zero 2W, serves the index.html application at http://camera.local:3000/. The interface includes a reference to Motion’s MJPEG stream at http://camera.local:8081/, which is sufficient when the application is being used within the local WLAN. If the interface is to be operated from a remote location, however, this reference must be changed to whatever globally accessible URL you have configured for the camera’s video stream.

As this was intended only as a pathfinder project, little effort was invested in producing an aesthetically refined user interface. Once a game controller is connected and operational however, the interface provides the essential information required to operate and monitor the system. This includes the camera image, the various locally and remotely reported joystick axes, the absolute positions maintained for the stepper motor axes, and other relevant device-state information.

The right-hand joystick’s X and Y axes control pan and tilt speed respectively, with the motor speed varying proportionally according to joystick deflection. The left-hand joystick’s Y axis is assigned to the zoom/ focus function, although this was not connected to the prototype hardware.

I encountered an unexpected difficulty when connecting a recent Microsoft Xbox game controller to Linux Mint. The controller’s joystick and button mappings were presented in a rather unhelpful arrangement, requiring the installation of the rather intimidating xpadneo kernel module to restore the expected axis and button assignments.

Conclusions

The project was an experiment, seeking to determine the merits of replacing the usual Apache webserver, C code, JavaScript and PHP scripting, with the Node.js JavaScript engine and JavaScript. This part of the investigation was a success, and I will be using Node.js for all such WiFi-connected FPV gadgets in future. Node.js v12.22.12 saved a lot of memory space, and is known to be superior in performance in every respect.

The second part of the investigation was less conclusive. Rather than using the conventional I²C link, stepper motor control was implemented using ConfigurableFirmata and its integrated AccelStepper module, communicating with the real-time microcontroller over USB serial. This proved entirely adequate for manual camera positioning, with motor speed varying proportionally with joystick deflection. It did not, however, provide sufficiently smooth, fine-grained positional control for applications such as OpenCV-based motion tracking.

I remain convinced that the Firmata protocol itself is a viable means of communicating with a real-time microcontroller. The limitation was more likely a consequence of the particular way in which I employed ConfigurableFirmata’s AccelStepper class. Its interface is fundamentally intended for a different style of motor control from that required by this application, and the rather unconventional solution described earlier was necessarily a compromise.

However, the Node.js package Firmata.js depends on other outdated Node.js packages, which are difficult to compile on more recent operating systems than Buster, particularly on 64-bit ARM OS’. For that reason, I’m avoiding the future use of Firmata altogether.

For future projects, I expect to return to I²C communication, coupled with dedicated real-time slave peripheral firmware. The previous I²C communication routines may well be revised as a more accessible library implementation, with greater emphasis on comprehensibility, portability, and ease of adoption.

I hope that this project has proved useful to those considering similar technologies for their own work, whether as a practical guide or simply as a source of ideas for further experimentation. At the very least, I hope it has provided a small and worthwhile distraction from the rather troubled state of the world in which we currently find ourselves.

If you are able and so inclined, please consider helping to buy dinner for one of my many beloved street kitties:
paypal.me/RoxbyLaneDumpsterCat 🐾

Foscam WiFi IP Camera Upgrade.

Let’s take an old 640×480 WiFi IP camera, gut it, upgrade the camera to a 5MP USB, and re-animate the PT mechanism with an Arduino Pro Mini, sequencing ULN2003A stepper drivers.

The result will be a tracking mechanism for up to 2 variously telescopic cameras, suitable for image capture, processing and motion tracking experimentation.

Why Fossie had to die.

IP Cameras are usually accompanied by a CGI mechanism with which you can control them using a web browser. There are all manner of setting related to camera tuning, and PT(Z) motion control. As an example:

http://192.168.0.115/decoder_control.cgi?command=0&onestep=1

This was Fossie’s command to raise the PT mechanism one degree. The interface gave full PT stepper motor control, and had a particular setting for x or y motor speeds. Unfortunately, an attempt to change any of these axis traversal rates, has to be followed by an immediate CGI initiated system reset, to be successful. That made effective control, initially via joystick, unviable.

23/10/24

No dissection just yet. From this slightly later model Foscam camera teardown video, I discovered that the two stepper motors are both 28byj-48-5v unipolar, 64:1 reduction gear, 5-wire with XH-5P socket type. They may be driven to between 10RPM and 15RPM, with 4096 half steps or 2048 full steps per revolution.

Time to review the state of the art in off-the-shelf software for stepper motor drive under client system control.

25/10/24

Okay, it looks like the single best option for controlling a stepper motor equipped Arduino remotely, from python (urgh!) or javascript running on a client PC, is ConfigurableFirmata. Paint yourself into a corner with a thing called Telemetrix if you must, but you’ll find the former option your best bet. The latter is only supported by one client library (python), it isn’t at all extensible, and the developer responsible appears to be quite self-important.

1/11/24

Here are some important facts about the two versions of the ConfigurableFirmata Arduino library that you will need to know:

  • Version 2.x – No support for ESP32, but wide support under client libraries (firmata.js, Johnny-Five, pyfirmata, etc.)
  • Version 3.x – Support for ESP32, but little support under client libraries, save for the .Net library iot, and possibly some others.

Some ConfigurableFirmata client libraries, their language, and the version of the Firmata protocol they currently support:

  • firmata.js – JavaScript – Firmata protocol version 2.5.
  • Johny-Five – JavaScript – Firmata protocol version 2.5 and some 2.6.
  • pyfirmata – Python – Firmata protocol version 2.1 and some 2.2.

ConfigurableFirmata version numbers mirror the Firmata protocol version they implement. I spent a lot of time learning this the hard way, that protocol support of the chosen client library, must be matched with the appropriate ConfigurableFirmata library version. Also, that the ConfigurableFirmata library must be matched to code derived from it’s version’s ConfigurableFirmata.ino example.

Here’s the list of versions and artefacts I was able to run successfully together on the AT328PB I will use in this project:

  • Arduino IDE – arduino-ide_2.2.1_Linux_64bit.
  • ConfigurableFirmata Arduino library version 2.10.1.
  • Server sketches based on the ConfigurableFirmata.ino example from version 2.10.1.
  • Node JavaScript client library – Firmata.js current version (1/11/24).
  • Client javascript implementations based on examples from the current Firmata.js, running in node 12.22.9 on client PC.

You may find the ConfigurableFirmata server sketches produced by the site at firmatabuilder.com useful, be be aware that they should only be built with the 2.10.1 version of the ConfigurableFirmata Arduino library. Get this wrong and you’ll be in a very dark place.

2/11/24

Yep, ConfigurableFirmata and firmata.js do everything they will need to for this project. I’m now intending to use an AT328P microcontroller, rather than the AT328PB that required changes to ConfigurableFirmata’s boards.h file that I couldn’t quite muster.

Time to brush-up on my server-side JavaScript, as we will be initially controlling the camera via a web browser interface, served by a webserver running node. Got my nose buried in a pdf of ‘Node.js for Beginners -A comprehensive guide to building efficient, full-featured web applications with Node.js’ ~Ulises Gascón.

12/11/24

Trying to get my head around asynchronous programming, the model that javascript on Node.js employs. The book I previously referenced isn’t that comprehensive, so I’m augmenting my erudition with some carefully procured lessons from ChatGPT. What a wonder!

12/12/24

Okay, to control the speed and direction camera’s stepper motors, using a gamepad connected to a remote browser, the chain of events begins at the client. In the index page served by the remote camera, we establish periodic calls to a controlInputsLoop(). This function employs the Gamepad API to access the gamepad, and read it’s inputs. The data is then sent to node.js endpoints, asynchronously, using AJAX (Asynchronous JavaScript and XML) calls. The cycle repeats.

<!DOCTYPE html>

<html lang="en">

  <head>

    <meta charset="utf-8">
      
    <title>Remote Camera</title>

  </head>

<body>
 
  // Embedable frame sequence URL of MotionEye on Raspbian GNU/Linux 11 (bullseye)
  <img src="http://192.168.0.114:8765/picture/1/frame/">

</body>

<script>

var controlIntervalID;

window.addEventListener("gamepadconnected", function() {
  // Call gamepad control handler every 100ms.
  controlIntervalID = setInterval('controlInputsLoop()', 100);
});

window.addEventListener("gamepaddisconnected", function() {
  clearInterval(controlIntervalID);
});

function sendCommand(request) 
{
  const xhttp = new XMLHttpRequest();
  
  // What to do when the response is ready.
  xhttp.onload = function() {
    // nothing
  }

  // Initialise the GET request with the URL in `request`
  xhttp.open("GET", request, true);

  // Send the request
  xhttp.send();
}

function buttonPressed(b) {
  if (typeof(b) == "object") {
    // Access true or false pressed property of b.
    return b.pressed;
  }
  return b == 1.0; // Return true if b numeric 1.0, false otherwise.
}

controlInputsLoop()
{
  // Returns all connected gamepads.
  var gamepads = navigator.getGamepads();
    
  if (!gamepads)
    return;

  // Get the first gamepad.
  var gp = gamepads[0];
  
  if (buttonPressed(gp.buttons[8])) {
    // System shutdown command.
    sendCommand("/control/shutdown=1");
  }

  // Acquire control positions between -1.0 to 1.0.
  var altitude = gp.axes[3].toFixed(4);  // Right Y axis
  var azimuth  = gp.axes[2].toFixed(4);  // Right X axis

  sendCommand("/altitude/" + altitude);
  sendCommand("/azimuth/" + azimuth);

  return;
}

</script>

</html>

Update:

For anyone returning to see this page progress, the completed project has been moved to a new page at:

https://green.bug-eyed.monster/node-firmata-ptz-camera/


Program and Debug the ESP32-C3

Setup programming and inbuilt JTAG debugging for an ESP32-C3 on Platformio.

  • This is a ‘your results may vary’ solution, due to unknown variation.

Connection

Check first that your ESP32-C3 is recognised when connected via USB (Linux).

user@machine:~$ lsusb
...
Bus 008 Device 033: ID 303a:1001 Espressif USB JTAG/serial debug unit
...

platformio.ini

Some build flags are required if you wish to enable serial output over USB, at the bitrate set by monitor_speed.

The on-chip CMSIS-DAP device handles uploading for debugging, JTAG debugging, and can also flash code for non-debugger uploads if required.

For non-debugger builds, we can choose to use the USB CDC Serial firmware upload tool, which affords us some extra project statistics.

; PlatformIO Project Configuration File
;
;   Build options: build flags, source filter
;   Upload options: custom upload port, speed and extra flags
;   Library options: dependencies, extra library storages
;   Advanced options: extra scripting
;
; Please visit documentation for the other options and examples
; https://docs.platformio.org/page/projectconf.html

[env:esp32-c3-devkitm-1]
platform = espressif32
board = esp32-c3-devkitm-1
framework = arduino

monitor_speed=115200

build_flags = 
  -D ARDUINO_USB_MODE=1          ; Enable USB hardware (if used).
  -D ARDUINO_USB_CDC_ON_BOOT=1   ; Enable CDC (Communications Device
                                 ; Class) functionality on boot, 
                                 ; enabling USB serial communication.

debug_tool = cmsis-dap           ; on-chip JTAG debug
; upload_protocol = esp-builtin    ; on-chip JTAG upload (less feedback)
upload_protocol = esptool        ; USB serial upload
debug_init_break = tbreak setup
debug_server =
  $PLATFORMIO_CORE_DIR/packages/tool-openocd-esp32/bin/openocd
  -f $PLATFORMIO_CORE_DIR/packages/tool-openocd-esp32/share/openocd/scripts/board/esp32c3-builtin.cfg
  

build and upload via USB serial

Remember that you can always enter program upload mode manually by RESETting the device with the BOOT switch depressed, in cases where previously programmed serial configuration prevents automatic CDC uploads.

monitor serial

build for debugging then flash by JTAG

Full rebuild and upload produces lengthy wait, then blue ‘working’ indicator stops and the debugger control pallet appears.

set breakpoint and debug via JTAG


If you’ve found this compilation useful, then your assistance in helping others find it will be both benevolent and appreciated.


Bluetooth BLE scanner iBeacon decoder

A Bluetooth BLE iBeacon observer, that scans for a list of available beacons, then for each iBeacon found, it reads and processes it’s gathered data.

The example presented builds using Platformio with the espressif arduino-esp32 core, and leverages the NimBLE-Arduino Bluetooth library.

Project source code

; PlatformIO Project Configuration File
;
;   Build options: build flags, source filter
;   Upload options: custom upload port, speed and extra flags
;   Library options: dependencies, extra library storages
;   Advanced options: extra scripting
;
; Please visit documentation for the other options and examples
; https://docs.platformio.org/page/projectconf.html

[env:esp32dev]
platform = espressif32
board = esp32dev
framework = arduino


monitor_speed = 115200


lib_deps = h2zero/NimBLE-Arduino@^1.4.0
// File: main.cpp 
// ESP32_NimBLE_scanner_iBeacon_decoder.
// <[email protected]> MIT License.
//
// An iBeacon observer.
// Scan for servers and log each unique result. 
// Read each logged BLE beacon in turn, filter for
// iBeacons, then process their manufacturer_data.
// Repeat.

#include <Arduino.h>

#include "NimBLEBeacon.h"
#include "NimBLEDevice.h"


#define ENDIAN_CHANGE_U16(x) ((((x)&0xff00) >> 8) + (((x)&0xff) << 8))


void setup() { 

    Serial.begin(115200);

    // Initialise the NimBLE library, witholding any advertisable name.
    NimBLEDevice::init("");
    
    // Create a Scan(ner):
    // Get a pointer to a newly created Scan instance.
    NimBLEScan *pScan = NimBLEDevice::getScan();

    // Set active scanning, this will get more data from the advertiser.
    // Comment-out if not interested in iBeacon name.
    pScan->setActiveScan(true);

    // Block whilst scanning for advertising servers, storing
    // a list of all results, over a period given in seconds.
    Serial.printf("Scanning for BLE advertisers\n");
    NimBLEScanResults results = pScan->start(10);
    Serial.printf("Found %d BLE advertisers\n", results.getCount());
    
    // Iterate through the list of NimBLEAdvertisedDevice's stored
    // in the NimBLEScanResults scan results list.
    for(int i = 0; i < results.getCount(); i++) {

        // Get a pointer to the iterated Device instance.
        NimBLEAdvertisedDevice advertisedDevice = results.getDevice(i);

        // Retrieve the BLE beacon's manufacturer_data.
        std::string strManufacturerData = advertisedDevice.getManufacturerData();

        // Did we find any manufacturer_data?
        if (strManufacturerData != "")
        {
            // Look for Apple ID and iBeacon length
            if (strManufacturerData.length() == 25 &&
                strManufacturerData[0] == 0x4c &&
                strManufacturerData[1] == 0x00 &&
                strManufacturerData[2] == 0x02 &&
                strManufacturerData[3] == 0x15) 
            {
                // Leverage the NimBLEBeacon library 
                // to help process the iBeacon data.
                NimBLEBeacon iBeacon = NimBLEBeacon();

                // Load our NimBLEBeacon object with manufacturer_data.
                iBeacon.setData(strManufacturerData);

                // Process this found iBeacon's data
                Serial.printf("Name     : %s\n",
                        advertisedDevice.getName().c_str());
                Serial.printf("Address  : %s\n",
                        advertisedDevice.getAddress().toString().c_str());
                Serial.printf("UUID     : %s\n",
                        iBeacon.getProximityUUID().toString().c_str());
                Serial.printf("Major    : %d\n",
                        ENDIAN_CHANGE_U16(iBeacon.getMajor()));
                Serial.printf("Minor    : %d\n",
                        ENDIAN_CHANGE_U16(iBeacon.getMinor()));
                Serial.printf("TX power : %d dBm\n",
                        iBeacon.getSignalPower());
                Serial.printf("RSSI     : %d dBm\n",
                        advertisedDevice.getRSSI());
                Serial.println("-----------------------------------------------");
            }
        }
    }
}

void loop() {

    setup();
}

Serial Port Output

Output obtained when run in proximity to one BLE iBeacon device flashed with the companion Bluetooth BLE iBeacon on ESP32.

Scanning for BLE advertisers
Found 2 BLE advertisers
Name     : ESP32-iBeacon
Address  : 08:b6:1f:37:f3:92
UUID     : c6dd05d0-b428-4bd5-8831-4b62651e2b41
Major    : 1
Minor    : 1
TX power : -60 dBm
RSSI     : -64 dBm
-----------------------------------------------
Scanning for BLE advertisers

Suggested reading: https://www.elektor.com/products/develop-your-own-bluetooth-low-energy-applications (no affiliation).


Simple Bluetooth BLE Scanner Client on ESP32

Exploration of Creating a Client: a simple Bluetooth BLE scanner that compiles a list of proximate servers, then attempts connection to each in turn. Those devices with a specific connectable service, having a specific read/write characteristic, are read from.

The example presented builds using Platformio with the espressif arduino-esp32 core, and leverages the NimBLE-Arduino Bluetooth library.

Project source code

; File: platformio.ini
; PlatformIO Project Configuration File
;
;   Build options: build flags, source filter
;   Upload options: custom upload port, speed and extra flags
;   Library options: dependencies, extra library storages
;   Advanced options: extra scripting
;
; Please visit documentation for the other options and examples
; https://docs.platformio.org/page/projectconf.html

[env:esp32dev]
platform = espressif32
board = esp32dev
framework = arduino

monitor_speed=115200

lib_deps = h2zero/NimBLE-Arduino@^1.4.0
// File: main.cpp 
// ESP32_NimBLE_simple_client.
// Scan for servers, connect to any advertising a service with uuid "ABCD",
// then read from (or write to) that service's characteristic with uuid "1234".

#include <Arduino.h>

#include "NimBLEDevice.h"

void setup() { 

    Serial.begin(115200);

    // Initialise the NimBLE library, witholding any advertisable name.
    NimBLEDevice::init("");
    
    // Create a Scan(ner):
    // Get a pointer to a newly created Scan instance.
    NimBLEScan *pScan = NimBLEDevice::getScan();

    // Block whilst scanning for advertising servers, storing
    // a list of all results, over a period given in seconds,
    // or 0 (continuously).
    Serial.printf("Scanning for BLE advertisers\n");
    NimBLEScanResults results = pScan->start(10);
    Serial.printf("Found %d BLE advertisers\n", results.getCount());
    
    // Model a BLE UUID, setting the service uuid for which we'll look.
    NimBLEUUID serviceUuid("ABCD");
    
    // Iterate through the list of NimBLEAdvertisedDevice's stored
    // in the NimBLEScanResults scan results list.
    for(int i = 0; i < results.getCount(); i++) {

        // Get a pointer to the iterated Device instance.
        NimBLEAdvertisedDevice device = results.getDevice(i);
        
        // Does this iterated device advertise a service with UUID: "ABCD"?
        if (device.isAdvertisingService(serviceUuid)) {
            Serial.printf("Found a device advertising Service with UUID: ABCD\n");

            // Get a pointer to a newly created Client instance.
            NimBLEClient *pClient = NimBLEDevice::createClient();
            
            // Connect Client to iterated device.   
            if (pClient->connect(&device)) {

                // Get a pointer to the Service instance previously found.
                NimBLERemoteService *pService = pClient->getService(serviceUuid);
                
                // Did we get a pointer to the Service instance?
                if (pService != nullptr) {
                    Serial.printf("Found Service with UUID: ABCD\n");
                    
                    // Get a pointer to the Characteristic instance with UUID: "1234".
                    NimBLERemoteCharacteristic *pCharacteristic = pService->getCharacteristic("1234");
                    
                    // Did we get a pointer to the Characteristic instance we want?
                    if (pCharacteristic != nullptr) {
                        Serial.printf("Found Characteristic with UUID: 1234\n");

                        if (pCharacteristic->canRead()) {
                            Serial.printf("Characteristic is readable\n");

                            // Read the value of the Characteristic.
                            std::string value = pCharacteristic->readValue();

                            // Print the value.
                            Serial.printf("Characteristic value: %s\n", String(value.c_str()));
                        }
                    }
                }
            } else {
                // No device scanned advertises the service with characteristic we sought.
                printf("Failed to connect\n");
            }
            
            // Because simultaneous connections to Clients are possible, we abandon
            // the connection to this iterated client and reclaim it's resources.
            NimBLEDevice::deleteClient(pClient);
        }
    }
}

void loop() {

}

Serial Port Output

Output obtained when run in proximity to one BLE device flashed with the companion Simple BLE Server on ESP32.

Scanning for BLE advertisers
Found 3 BLE advertisers
Found a device advertising Service with UUID: ABCD
Found Service with UUID: ABCD
Found Characteristic with UUID: 1234
Characteristic is readable
Characteristic value: Hello BLE

Prototypical reference: https://github.com/h2zero/NimBLE-Arduino/blob/master/docs/New_user_guide.md#creating-a-client

Suggested reading: https://www.elektor.com/products/develop-your-own-bluetooth-low-energy-applications (no affiliation).


Getting Arduino AVR Disassembly with Platformio

You can get a cpp-source-interleaved disassembly listing, when compiling for Arduino AVR under Platformio.

You add these to platformio.ini:

build_flags =
    -save-temps=obj
    -fverbose-asm

Build_type = debug

Then get the c source interleaved disassembly with:

avr-objdump -S firmware.elf > disassembly.txt

A sample of the disassembly output:

...

void loop() 
{
  // Read adc and save result in read-only slave register 1
  i2c_registers[I2C_REG_1] = myadc_read();
     cc2:	0e 94 46 06 	call	0xc8c	; 0xc8c <_Z10myadc_readv>
     cc6:	90 93 21 01 	sts	0x0121, r25	; 0x800121 <i2c_registers+0x3>
     cca:	80 93 20 01 	sts	0x0120, r24	; 0x800120 <i2c_registers+0x2>

  // Spin for 100ms
  delay(100);
     cce:	64 e6       	ldi	r22, 0x64	; 100
     cd0:	70 e0       	ldi	r23, 0x00	; 0
     cd2:	80 e0       	ldi	r24, 0x00	; 0
     cd4:	90 e0       	ldi	r25, 0x00	; 0
     cd6:	0e 94 b5 03 	call	0x76a	; 0x76a <delay>
     cda:	08 95       	ret

...

Arduino I2C Slave Peripheral

With a recognition of the existing gap, I present this contribution aimed at enhancing the publicly accessible guidelines for laying out and coding Arduino I2C slave devices. This proposal introduces an organizational framework for coding I2C slave peripherals on Arduino, illustrated through a Platformio-based example.

Structure

Using an Atmel328PB microcontroller, we simulate a series of 16-bit peripheral registers, each with an 8-bit address. These registers are programmed akin to standard silicon I2C devices, allowing individual configuration for read-only, write-only, or read-write operations as perceived by the I2C master.

/*****************************************************************************
 *
 *                              Global Variables
 *
 *****************************************************************************/
// An array to store register values
int i2c_registers[I2C_NUM_REGS] = {0};

// I2C session persistent register address
uint8_t registerAddr = 0xFF;

In the development of an I2C slave application, peripheral functionality must be implemented through data source and data sink operations, skillfully integrated with the aforementioned emulated device registers.

In scenarios demanding highly efficient application code execution, integration occurs within the I2C interrupt service context. These application operations are executed dynamically during active I2C communication. However, integrating time-consuming application operations here can potentially delay I2C timing to a degree that compromises standards compliance, resulting in communication failures.

To mitigate this risk, in cases where application code runs slower, linkage between data source/sink operations and emulated peripheral registers occurs outside the I2C service interrupt context, typically within or under the task loop scope. Despite this approach, simultaneous access to emulated registers can occur, due to I2C ISR service routine triggering, leading to data integrity issues.

To ensure robustness, a pair of interrupt-suspending memory access macros is provided. These macros guarantee atomicity during peripheral register data manipulation within the task loop, thereby preventing potential corruption

I2C Call-back Functions

During program setup, the I2C bus under configuration, is assigned a device address, and call-back routines for the service of wired I2C bus transactions. When an I2C-inbound write occurs, data is consumed and/ or is stored in a register, with code inside the receiveEvent() callback. When an I2C-inbound read occurs, data is application generated, or is retrieved from a register, inside the requestEvent() ISR.

  Wire.begin(I2C_ADDRESS);      // Initialize I2C communication as a slave
  Wire.onReceive(receiveEvent); // Register the receive event handler
  Wire.onRequest(requestEvent); // Register the request event handler

The unpopulated receiveEvent() call-back

An unpopulated receiveEvent() ISR services wired I2C register storage instructions. Not yet linked to any application functionality, it merely provides write access to peripheral register memory.

/*****************************************************************************
 *
 *                               receiveEvent()
 *
 *****************************************************************************/
// Event handler for receiving data from the master, to write to a register
void receiveEvent(int numBytes) 
{ 
  // Read the requested register address
  registerAddr = Wire.read(); 
  
  if (numBytes == 3) 
  {
    // Read the data to write
    uint16_t value = Wire.read() ;        // value LSB
    value |= Wire.read() << 8;            // value MSB

    switch (registerAddr)
    {
      // Comment-out any read-only registers to prevent master write
      case I2C_REG_0:
      case I2C_REG_1: 
      case I2C_REG_2:
      case I2C_REG_3:
      case I2C_REG_4:
      case I2C_REG_5:
      case I2C_REG_6:
      case I2C_REG_7:
      {
        i2c_registers[registerAddr] = value;
        break;
      }
      default: return;
    }

    I2C_WRITE_DEBUG(registerAddr, value)
  }
}

The unpopulated requestEvent() call-back

An unpopulated requestEvent() ISR services wired I2C register retrieval instructions. Not yet linked to any application functionality, it merely provides read access to peripheral register memory.

/*****************************************************************************
 *
 *                               requestEvent()
 *
 *****************************************************************************/
// Event handler for responding to requests for register contents, from the master
void requestEvent() 
{
  uint16_t value = 0;

  switch (registerAddr)
  {
    // Comment out any write-only registers to prevent master read
    case I2C_REG_0:
    case I2C_REG_1:
    case I2C_REG_2:
    case I2C_REG_3:
    case I2C_REG_4:
    case I2C_REG_5:
    case I2C_REG_6:
    case I2C_REG_7:
    {
      value = i2c_registers[registerAddr];
      break;
    }
    default:
      return;
  }

  // Send the data read from
  Wire.write(value & 0xFF); // send LSB
  Wire.write(value >> 8);   // send MSB

  I2C_READ_DEBUG(registerAddr, value);

  return;
}

Read-ability and write-ability

So, we may see from the above, in the unpopulated ISR pair, an I2C master has the ability to write a value to one of a number of memory-only registers, and then read it back from there verbatim. We’ll see later, when we come to adding applications, such simultaneous read-ability and write-ability may not be desirable.

To make a peripheral register read-only, we remove the receiveEvent() ISRs ability to service peripheral register write requests. We do this by commenting-out service routine switch cases that involve the register.

void receiveEvent(int numBytes) 
{

...

    switch (registerAddr)
    {
      // Comment out any read-only registers to prevent master write
      case I2C_REG_0:
      //case I2C_REG_1:  // Make register 1 read-only
      case I2C_REG_2:
      case I2C_REG_3:
      case I2C_REG_4:
      case I2C_REG_5:
      case I2C_REG_6:
      case I2C_REG_7:
      {
        i2c_registers[registerAddr] = value;
        break;
      }
      default: return;
    }

...

To make a peripheral register write-only, we remove the requestEvent() ISRs ability to service peripheral register read requests. Again, we do this by commenting-out service routine switch cases that involve the register.

void requestEvent() 
{

...

  switch (registerAddr)
  {
    // Comment out any write-only registers to prevent master read
    case I2C_REG_0:
    case I2C_REG_1:
    // case I2C_REG_2:  // Make register 2 write-only
    case I2C_REG_3:
    case I2C_REG_4:
    case I2C_REG_5:
    case I2C_REG_6:
    case I2C_REG_7:
    {
      value = i2c_registers[registerAddr];
      break;
    }
    default:
      return;
  }

...

Application linkage inside loop()

Now that we have our peripheral registers configured, variously as read and write, read-only, or write-only, we come to linking these to application code. This involves the data sink and data source routines, that you will develop for your peripheral.

Remember, all access to peripheral registers outside the I2C interrupt service context, must employ the atomic register access macros, I2C_ATOMIC_REG_RD() and I2C_ATOMIC_REG_WR(). Again, this is vital to prevent the data corruption that occurs, when I2C ISRs are invoked, during peripheral register access. The macros momentarily ‘lock-out’ the ISRs.

/************************************************************************
 *
 *                                loop()
 *
 ************************************************************************/
void loop() 
{
  {
    uint16_t value = 0;

    // Read adc, and atomically save val in read-only slave register 1
    value = myadc_read();
    // i2c_registers[I2C_REG_1] = value; <- not atomic, instead...
    I2C_ATOMIC_REG_WR(i2c_registers[I2C_REG_1], value);

    // Retrieve contents of slave register 0 atomically, and consume
    // value = i2c_registers[I2C_REG_0]; <- not atomic, instead...
    I2C_ATOMIC_REG_RD(value, i2c_registers[I2C_REG_0]);
    myservo_set_pos(value);
  }

  // Other application tasks.

}

In the above, we see that a logically read-only peripheral register, I2C_REG_1, is fed samples from an onboard ADC, using an application call to myadc_read(). A remotely wired, I2C master may read the stored register data, with calls such as wiringPi’s wiringPiI2CReadReg16().

As well, above, we have an either read and write, or write-only configured peripheral register, I2C_REG_0, feeding position data to an attached servo. It does this via an application call to myservo_set_pos(). A remotely wired, I2C master may write the servo position data, with calls such as wiringPi’s wiringPiI2CWriteReg16().

On-demand, vs polled register access

The above mentioned approach for linking application data sink and source functionality, to underlying peripheral register memory, from outside I2C ISR service context, can be wasteful. Significant cpu cycles are consumed, in the application either constantly polling peripheral registers for incoming data, or frequently updating peripheral registers for fresh outgoing data.

To increase application efficiency, an alternate user code linkage solution exists, for on-demand exchange of data with peripheral registers. This involves application data source and data sink function calls, from within I2C ISR service context.

As mentioned previously, application calls made inside either of the I2C ISR call-backs, must be very brief, so as not to disrupt wired I2C communication. Between the servo and ADC application calls dealt with previously, only the myservo_set_pos() call is suitable. The myadc_read() function involves lengthy over-sampling, and any attempt to link it ‘on-demand’, from within an ISR, will break associated I2C transactions.

Application linkage inside the I2C ISRs

Here we examine the alternative approach to linking an I2C slave peripheral’s servo position. Data is exchanged only when requested to be, by a wired I2C bus transaction. The I2C receiveEvent() ISR handles incoming data, and we merely write that ‘value’ data to the servo, using the necessarily fast myservo_set_pos() application call.

void receiveEvent(int numBytes) 
{

...

    switch (registerAddr)
    {
      // Comment out any read-only registers to prevent master write
      case I2C_REG_0:
      {
        myservo_set_pos(value);
        i2c_registers[registerAddr] = value;
        break;
      }
      //case I2C_REG_1:  // Read-only
      case I2C_REG_2:
      case I2C_REG_3:
      case I2C_REG_4:
      case I2C_REG_5:
      case I2C_REG_6:
      case I2C_REG_7:
      {
        i2c_registers[registerAddr] = value;
        break;
      }
      default: return;
    }

    I2C_WRITE_DEBUG(registerAddr, value)
  }
}

...

In this case, we have chosen to make the servo peripheral register both readable, and writeable. To accomplish this, we must also store the incoming data in the underlying peripheral register, as shown above.

In the case where we wanted to make the application’s servo peripheral register write only, we would not store the incoming position data in any underlying peripheral register, or, at the very least, make the underlying register write only. This is again accomplished, by commenting-out the appropriate switch statement case, inside the requestEvent() ISR.

void requestEvent() 
{

...

  switch (registerAddr)
  {
    // Comment out any write-only registers to prevent master read
    //case I2C_REG_0:  // Make servo register 0 write-only
    case I2C_REG_1:
    case I2C_REG_2:
    case I2C_REG_3:
    case I2C_REG_4:
    case I2C_REG_5:
    case I2C_REG_6:
    case I2C_REG_7:
    {
      value = i2c_registers[registerAddr];
      break;
    }
    default:
      return;
  }

...

For the final case, we examine how outgoing application data may be linked to peripheral register requests, on-demand, from within I2C ISR context. For this, we will read an attached switch, with an imaginary application call, myswitch_get_posn(). We know that the call will be fast enough not to break wired I2C communication, and that logically the operation must be read-only.

The I2C requestEvent() ISR handles outgoing data, and we merely service a request for the switch position, with data from the myswitch_get_posn() application call.

void requestEvent() 
{

...

  switch (registerAddr)
  {
    // Comment out any write-only registers to prevent master read
    case I2C_REG_2:
    {
      value = myswitch_get_posn();
      break;
    }
    case I2C_REG_0:
    case I2C_REG_1:
    case I2C_REG_3:
    case I2C_REG_4:
    case I2C_REG_5:
    case I2C_REG_6:
    case I2C_REG_7:
    {
      value = i2c_registers[registerAddr];
      break;
    }
    default:
      return;
  }

...

As seen above, the switch peripheral register is logically read only, so stored peripheral register memory is not involved. To disable any attempt to write a peripheral register, making it read only, we comment-out it’s associated entry in the receiveEvent() I2C ISR.

void requestEvent() 
{

...

  switch (registerAddr)
  {
    // Comment out any write-only registers to prevent master read
    case I2C_REG_0:
    case I2C_REG_1:
    // case I2C_REG_2:  // Make switch register 2 write-only
    case I2C_REG_3:
    case I2C_REG_4:
    case I2C_REG_5:
    case I2C_REG_6:
    case I2C_REG_7:
    {
      value = i2c_registers[registerAddr];
      break;
    }
    default:
      return;
  }

...

Improving polled access efficiency

/************************************************************************
 *
 *                                loop()
 *
 ************************************************************************/
void loop() 
{
  {
    uint16_t value = 0;

    if (myservo_changed())
    {
      // Retrieve contents of slave register 0 atomically, and consume
      I2C_ATOMIC_REG_RD(value, i2c_registers[I2C_REG_0]);
      myservo_set_changed(0);

      // From here on, receiveEvent() ISR can take new register data,
      // and the snapshot 'value' can be processed, even if exhaustively
      myservo_set_pos(value);
    }
  }

  // Other application tasks.

}
void receiveEvent(int numBytes) 
{

...

    switch (registerAddr)
    {
      // Comment out any read-only registers to prevent master write
      case I2C_REG_0:
      {
        i2c_registers[registerAddr] = value;
        myservo_set_changed(1);
        break;
      }
      case I2C_REG_1:
      case I2C_REG_2:
      case I2C_REG_3:
      case I2C_REG_4:
      case I2C_REG_5:
      case I2C_REG_6:
      case I2C_REG_7:
      {
        i2c_registers[registerAddr] = value;
        break;
      }
      default: return;
    }

    I2C_WRITE_DEBUG(registerAddr, value)
  }
}

...

Well, that’s how the I2C slave peripheral model works, how to constrain access to it’s underlying peripheral registers, and how to link-in application code. Application linkage methods were shown, both by polling inside loop(), and by on-demand calls inside ISR context.

Just remember to keep application calls short inside the ISRs, and to use atomic register access macros outside them.

Associated Files:

The attached Arduino I2C slave demonstration code for this example, runs on an AT328PB. It reads the position of an ADC-connected potentiometer, and stores this data in a 16-bit I2C register numbered 1. It also reads data in a 16-bit I2C register numbered 0, and sets the corresponding position of an attached servo.

Example C code for a wiringPi-installed, Raspberry Pi master is also provided, which remotely reads the potentiometer value, and writes a proportionate value back to the I2C slave peripheral’s servo control register.

Demonstration code, of the suggested organisational paradigm, for an Arduino I2C slave peripheral. I2C_slave_model.zip

LiquidCrystal_I2C – I2C LCD Driver for 8052

The LiquidCrystal_I2C library is a modified version of the standard LiquidCrystal library as found on the Arduino website.

HD44780 controlled LCD, fitted with Chinese PCF8574 I2C backpack.
HD44780 controlled LCD, fitted with Chinese PCF8574 I2C backpack.

This library is intended to be used when a parallel HD44780 compatible LCD is controlled over I2C using a Chinese PCF8574 extender, as sold on eBay.

Chinese PCF8574 I2C to parallel LCD backpack
Chinese PCF8574 I2C to parallel LCD backpack.

Be aware that the Chinese PCF8574 extender is available in two versions, the PCF8574 and the PCF8574A, the only difference between the two is the I2C base address. See the documentation for details.

Library Download.

Arduino code library: LiquidCrystal_I2C.zip

C8_CO2_5K – CO2 Sensor

The C8 CO2 5K is a digital NDIR CO2 sensor, manufactured by Shenzhen Shenchen Technology Co., Ltd.

C8 NDIR CO2 Sensor and Output.

The C8_CO2_5K is a high-precision, low-cost NDIR CO2 sensor that is perfect for a variety of applications, including:

  • Indoor air quality monitoring
  • Greenhouse monitoring
  • Research and development
  • And more!

Associated Files:

English Language Datasheet – C8-CO2-5KV1-4-data-sheets.pdf
Chinese Language Datasheet – C8二氧化碳传感器产品规格书.pdf
Arduino Code Library – C8_CO2_5K.zip

Buy Factory Direct (no affiliation).

Visit – Shenzhen Shenchen Technology Co., Ltd.

or – http://www.iot-sc.com/