Introduction: Going Beyond StandardFirmata - Revisited
A short while ago, I was contacted by Dr. Martyn Wheeler, a pymata4 user, for guidance in adding support for the DHT22 Humidity/Temperature sensor to the pymata4 library. The pymata4 library, in conjunction with its Arduino counterpart, FirmataExpress, allows users to control and monitor their Arduino devices remotely. Within a few rounds of email exchanges, Dr. Wheeler was successful in modifying both pymata4 and FirmataExpress. As a result, support for the DHT22 and DHT11 sensors is now a standard part of pymata4 and FirmataExpress.
In May of 2014, I wrote an article on adding support to Firmata for additional devices. Reflecting on that I article, I realized how much has changed since I took pen to paper for that article. In addition to this article, Dr. Wheeler documented his efforts, and you might wish to check that out as well.
FirmataExpress is based on StandardFirmata, and the StandardFirmata directory structure has evolved. In addition, the pymata4 API is also quite a bit different from the original PyMata API of 2014. I thought this would be the perfect time to revisit and update that article. Using Dr. Wheeler’s work as a basis, let's explore how to extend pymata4/FirmataExpress functionality.
Before We Begin - Some Background Information About Arduino/Firmata
So what is Firmata? Quoting from the Firmata web page, "Firmata is a generic protocol for communicating with microcontrollers from software on a host computer."
Arduino Firmata uses a serial interface to transport both command and report information between an Arduino microcontroller and a PC, typically using a serial/USB link set to 57600 bps. The data transferred across this link is binary, and the protocol is implemented in a client/server model.
The server side is uploaded to an Arduino microcontroller in the form of an Arduino sketch. The StandardFirmata sketch, included with the Arduino IDE, controls the Arduino I/O pins, as commanded by the client. It also reports input pin changes and other report information back to the client. FirmataExpress is an extended version of StandardFirmata. It runs at a serial link speed of 115200 bps.
The Arduino client used for this article is pymata4. It is a Python application that is executed on a PC. It both sends commands to and receives reports from the Arduino server. Because pymata4 is implemented in Python, it runs on Windows, Linux (including Raspberry Pi), and macOS computers.
Why Use Firmata?
Arduino microcontrollers are wonderful little devices, but processor and memory resources are somewhat limited. For applications that are processor or memory intensive, there is often little choice but to offload the resource demand onto a PC for the application to be successful.
But that's not the only reason for using StandardFirmata. When developing lighter weight Arduino applications, a PC can provide tools and debugging capabilities not directly available on an Arduino microcontroller. Using a "fixed" client and server helps confine the application complexity to a PC, which is more easily managed. Once the application is perfected, it can be translated into a custom, standalone Arduino sketch.
Why Use pymata4?
Being its author, of course, I am biased. That being said, it is the only Python-based Firmata client that has been continuously maintained over the last several years. It provides an intuitive and easy to use API. In addition to StandardFirmata based sketches, It supports Firmata over WiFi for devices like the ESP-8266 when using the StandardFirmataWifI sketch.
Also, pymata4 was designed to be easily extended by a user to support additional sensors and actuators not currently supported by StandardFirmata.
Step 1: Understanding the Firmata Protocol
The Arduino Firmata communications protocol is derived from the MIDI protocol, which uses one or more 7-bit bytes to represent data.
Firmata was designed to be user-extensible. The mechanism that provides this extensibility is the System Exclusive (SysEx) messaging protocol.
The format of a SysEx message, as defined by the Firmata Protocol, is shown in the illustration above. It begins with a START_SYSEX byte with a fixed value of hexadecimal 0xF0, and is followed by a unique SysEx command byte. The value of the command byte must be in the range of hexadecimal 0x00-0x7F. The command byte is then followed by an unspecified number of 7-bit data bytes. Finally, the message is terminated with an END_SYSEX byte, with a fixed value of hexadecimal 0xF7.
Firmata Data Encoding/Decoding
Since the user data portion of a SysEx message consists of a series of 7-bit bytes, you may wonder how one represents a value greater than 128 (0x7f)? Firmata encodes those values by disassembling them into multiple 7-bit byte chunks before the data is marshaled across the data link. The least significant byte (LSB) of a data item is sent first, followed by increasingly significant components of the data item by convention. The most significant byte (MSB) of the data item is the last data item sent.
How Does This Work?
Let’s say we wish to incorporate a value 525 into the data portion of a SysEx message. Since a value of 525 is clearly greater than a value of 128, we need to split or disassemble it into 7-bit byte “chunks.”
Here is how that is done.
The value of 525 in decimal is equivalent to the hexadecimal value of 0x20D, a 2-byte value. To get the LSB, we mask the value by AND'ing it with 0x7F. Both "C" and Python implementations are shown below:
// "C" implementation to isolate LSB int max_distance_LSB = max_distance & 0x7f ; // mask the lower byte # Python implementation to isolate LSB max_distance_LSB = max_distance & 0x7F # mask the lower byte
After masking, max_distance_LSB will contain 0x0d. 0x20D & 0x7F = 0x0D.
Next, we need to isolate the MSB for this 2-byte value. To do this, we will shift the value of 0x20D to the right, 7 places.
// "C" implementation to isolate MSB of 2 byte value int max_distance_MSB = max_distance >> 7 ; // shift the high order byte # Python implementation to isoloate MSB of 2 byte value max_distance_MSB = max_distance >> 7 # shift to get the upper byte After shifting, max_distance_MSB will contain a value of 0x04.
When the "chunkified" marshaled data is received, it needs to be reassembled into a single value. Here is how the data is reassembled in both "C" and Python
// "C" implementation to reassemble the 2 byte, // 7 bit values into a single value int max_distance = argv[0] + (argv[1] << 7) ; # Python implementation to reassemble the 2 byte, # 7 bit values into a single value max_distance = data[0] + (data[1] << 7)
After reassembly, the value once again equals 525 decimal or 0x20D hexadecimal.
This disassembly/reassembly process may be performed by either the client or server.
Step 2: Let's Get Started
Supporting a new device requires changes to both the Arduino resident server and the PC resident Python client. Dr. Wheeler's work will be used to illustrate the necessary modifications.
Perhaps the most important step is to decide whether you wish to integrate an existing supporting device library into the Arduino side of the equation or write your own. It is recommended that if you can find an existing library, it is far simpler to use it than writing your own from scratch.
For DHT device support, Dr. Wheeler based his extension code on the DHTNew library. Very cleverly, Dr. Wheeler split the functionality of the DHTNew library across the Arduino and pymata4 sides of the equation to provide minimal blocking on the Arduino side.
If we look at DHTNew, it performs all of the following:
- Sets the selected pin digital output mode.
- Clocks out an encoded signal to retrieve the latest humidity and temperature values.
- Checks for and reports any errors.
- Calculates human-readable temperature and humidity values from the raw data retrieved.
To keep things as efficient as possible on the FirmataExpress side, Dr. Wheeler offloaded the data conversion routines from the Arduino to pymata4.
Step 3: Modifying FirmataExpress for DHT Support
The FirmataExpress Directory Tree
Below are all the files that comprise the FirmataExpress repository. This tree is identical to that of StandardFiramata, just that some of the filenames reflect the repository name.
The files that need modification are the ones that have an asterisk(*) next to them.
├── * Boards.h
├── examples
│ └── FirmataExpress
│ ├── boardx
│ ├── * FirmataExpress.ino
│ ├── LICENSE.txt
│ └── Makefile
├── * FirmataConstants.h
├── * FirmataDefines.h
├── FirmataExpress.cpp
├── FirmataExpress.h
├── FirmataMarshaller.cpp
├── FirmataMarshaller.h
├── FirmataParser.cpp
└── FirmataParser.h
Let’s look at each of the files and the changes that were made.
This file contains pin-type macro definitions for each of the supported board types. It defines the maximum number of devices supported when more than one device needs to be supported.
For the DHT device, up to 6 devices may be connected at a time and this value is defined as:
#ifndef MAX_DHTS #define MAX_DHTS 6 #endif
Also, pin-type macros may be optionally defined for the new device, either for all board types or just the ones that are of interest to you. These macros are used mostly for reporting purposes and are not used for controlling the devices. These macros define both the pins that support the device:
#define IS_PIN_DHT(p) (IS_PIN_DIGITAL(p) && (p) - 2 < MAX_DHTS)
As well as a macro to define a pin-number conversion.
#define PIN_TO_DHT(p) PIN_TO_DIGITAL(p)
This file contains the firmware version number, which you may wish to modify to keep track of which version you have loaded onto your Arduino. It also contains the Firmata message values, including the Firmata SysEx messages.
You will need to assign a new message or set of messages for your device in this file. For the DHT, two messages were added. One configures a pin as a “DHT” pin, and the other, as a reporter message, when sending the latest DHT data back to the client.
static const int DHT_CONFIG = 0x64; static const int DHT_DATA = 0x65;
Pin modes are also specified in this file. For the DHT, a new pin mode was created:
static const int PIN_MODE_DHT = 0x0F; // pin configured for DHT
When adding a new pin mode, the TOTAL_PIN_MODES must be adjusted:
static const int TOTAL_PIN_MODES = 17;
This file must be updated to reflect the new messages added to FirmataConstants.h:
#ifdef DHT_CONFIG<br>#undef DHT_CONFIG #endif #define DHT_CONFIG firmata::DHT_CONFIG // DHT request #ifdef DHT_DATA #undef DHT_DATA #endif #define DHT_DATA firmata::DHT_DATA // DHT reply #ifdef PIN_MODE_DHT #undef PIN_MODE_DHT #endif #define PIN_MODE_DHT firmata::PIN_MODE_DHT
In this discussion, we will cover the “high-points” of the changes made to this Arduino sketch.
In order for FirmataExpress to support up to six DHT devices simultaneously, 3 arrays were created to keep track of each of the device’s associated pin number, its WakeUpDelay value, and the device type, that is DHT22 or DHT11:
// DHT sensors int numActiveDHTs = 0 ; // number of DHTs attached uint8_t DHT_PinNumbers[MAX_DHTS] ; uint8_t DHT_WakeUpDelay[MAX_DHTS] ; uint8_t DHT_TYPE[MAX_DHTS] ;
Because both device types require approximately 2 seconds between reads, we need to make sure that we read each DHT only once in the 2-second time frame. Some devices, such as the DHT devices and HC-SR04 distance sensors, are only accessed periodically. This allows them the time to interact with their environments.
uint8_t nextDHT = 0 ; // index into dht[] for next device to be read uint8_t currentDHT = 0; // Keeps track of which sensor is active. int dhtNumLoops = 0; // Target number of times through loop b4 accessing a DHT int dhtLoopCounter = 0; // Loop counter
Configuring And Reading The DHT Device
When FirmataExpress receives a SysEx command to configure a pin for DHT operation, it verifies that the maximum number of DHT devices has not been exceeded. If the new DHT can be supported, the DHT arrays are updated. If the DHT type is unknown, a SysEx string message is created and transmitted back to pymata4
case DHT_CONFIG:<br> int DHT_Pin = argv[0] ; int DHT_type = argv[1]; if ( numActiveDHTs < MAX_DHTS) { if (DHT_type == 22) { DHT_WakeUpDelay[numActiveDHTs] = 1; } else if (DHT_type == 11) { DHT_WakeUpDelay[numActiveDHTs] = 18; } else { Firmata.sendString("ERROR: UNKNOWN SENSOR TYPE, VALID SENSORS ARE 11, 22"); break; } // test the sensor DHT_PinNumbers[numActiveDHTs] = DHT_Pin ; DHT_TYPE[numActiveDHTs] = DHT_type; setPinModeCallback(DHT_Pin, PIN_MODE_DHT);
FirmataExpress then attempts to communicate with the DHT device. If there any errors, it forms a SysEx message with the error data and sends the SysEx message back to pymat4. The _bits variable holds the data returned by the DHT device for additional processing by pymata4 if desired.
Firmata.write(START_SYSEX); Firmata.write(DHT_DATA) ; Firmata.write(DHT_Pin) ; Firmata.write(DHT_type) ; for (uint8_t i = 0; i < sizeof(_bits) - 1; ++i) { Firmata.write(_bits[i] & 0x7f); Firmata.write(_bits[i] >> 7 & 0x7f); } Firmata.write(abs(rv)); Firmata.write(1); Firmata.write(END_SYSEX);
If valid data is returned, the number of active DHTs is incremented. A variable that keeps track of how many loop iterations to complete before checking the next DHT for data is also adjusted. This variable assures that no matter how many DHTs are added to the system, they will all be read within a 2 second period.
int rv = readDhtSensor(numActiveDHTs); if (rv == DHTLIB_OK) { numActiveDHTs++ ; dhtNumLoops = dhtNumLoops / numActiveDHTs ; // all okay }
If one or more DHT devices have been configured in the loop function of the sketch, then the next DHT device is read. Either the valid data or its error status is returned to pymata4 in the form of a SysEx message:
if ( dhtLoopCounter++ > dhtNumLoops)<br> { if (numActiveDHTs) { int rv = readDhtSensor(nextDHT); uint8_t current_pin = DHT_PinNumbers[nextDHT] ; uint8_t current_type = DHT_TYPE[nextDHT] ; dhtLoopCounter = 0 ; currentDHT = nextDHT ; if ( nextDHT++ >= numActiveDHTs - 1) { nextDHT = 0 ; } if (rv == DHTLIB_OK) { // TEST CHECKSUM uint8_t sum = _bits[0] + _bits[1] + _bits[2] + _bits[3]; if (_bits[4] != sum) { rv = -1; } } // send the message back with an error status Firmata.write(START_SYSEX); Firmata.write(DHT_DATA) ; Firmata.write(current_pin) ; Firmata.write(current_type) ; for (uint8_t i = 0; i < sizeof(_bits) - 1; ++i) { Firmata.write(_bits[i] ); // Firmata.write(_bits[i] ; } Firmata.write(abs(rv)); Firmata.write(0); Firmata.write(END_SYSEX); } }
The code used to communicate with the DHT device is derived directly from the DHTNew library:
int readDhtSensor(int index){ // INIT BUFFERVAR TO RECEIVE DATA uint8_t mask = 128; uint8_t idx = 0; // EMPTY BUFFER // memset(_bits, 0, sizeof(_bits)); for (uint8_t i = 0; i < 5; i++) _bits[i] = 0; uint8_t pin = DHT_PinNumbers[index] ; uint8_t wakeupDelay = DHT_WakeUpDelay[index] ; // REQUEST SAMPLE pinMode(pin, OUTPUT); digitalWrite(pin, LOW); delay(wakeupDelay); pinMode(pin, INPUT); delayMicroseconds(40); // GET ACKNOWLEDGE or TIMEOUT uint16_t loopCnt = DHTLIB_TIMEOUT; while (digitalRead(pin) == LOW) { if (--loopCnt == 0) return DHTLIB_ERROR_TIMEOUT; } loopCnt = DHTLIB_TIMEOUT; while (digitalRead(pin) == HIGH) { if (--loopCnt == 0) return DHTLIB_ERROR_TIMEOUT; } // READ THE OUTPUT - 40 BITS => 5 BYTES for (uint8_t i = 40; i != 0; i--) { loopCnt = DHTLIB_TIMEOUT; while (digitalRead(pin) == LOW) { if (--loopCnt == 0) return DHTLIB_ERROR_TIMEOUT; } uint32_t t = micros(); loopCnt = DHTLIB_TIMEOUT; while (digitalRead(pin) == HIGH) { if (--loopCnt == 0) return DHTLIB_ERROR_TIMEOUT; } if ((micros() - t) > 40) { _bits[idx] |= mask; } mask >>= 1; if (mask == 0) // next byte? { mask = 128; idx++; } } return DHTLIB_OK; }
Step 4: Modifying Pymata4 for DHT Support
To support the DHT, we need to add both the new pin-type and SysEx messages to this file:
# pin modes<br> INPUT = 0x00 # pin set as input OUTPUT = 0x01 # pin set as output ANALOG = 0x02 # analog pin in analogInput mode PWM = 0x03 # digital pin in PWM output mode SERVO = 0x04 # digital pin in Servo output mode I2C = 0x06 # pin included in I2C setup STEPPER = 0x08 # any pin in stepper mode SERIAL = 0x0a PULLUP = 0x0b # Any pin in pullup mode SONAR = 0x0c # Any pin in SONAR mode TONE = 0x0d # Any pin in tone mode PIXY = 0x0e # reserved for pixy camera mode DHT = 0x0f # DHT sensor IGNORE = 0x7f # DHT SysEx command messages<br> DHT_CONFIG = 0x64 # dht config command<br> DHT_DATA = 0x65 # dht sensor reply
The added pin type and SysEx commands must match the values in FirmataConstants.h added to FirmataExpress.
Pymata4 uses a Python dictionary to quickly associate an incoming Firmata message with a message handler. The name of this dictionary is report_dispatch.
The format for a dictionary entry is:
{MessageID: [message_handler, number of data bytes to be processed ]}
An entry was added to the dictionary to handle incoming DHT messages:
{PrivateConstants.DHT_DATA: [self._dht_read_response, 7]}
The 7 bytes of data in the message are the Arduino digital pin number, the type of DHT device (22 or 11), and the 5 bytes of raw data.
The _dht_read_response method checks for any reported errors. If there are no reported errors, the humidity and temperature are calculated using the algorithm ported from the Arduino DHTNew library.
The calculated values are reported via a user-supplied callback method. They are also stored in the internal pin_data data structure. The last value reported may be recalled by polling pin_data using the dht_read method.
Configuring A New DHT Device
When adding a new DHT device, the set_pin_mode_dht method is called. This method updates the pin_data for digital pins. It also creates and sends a DHT_CONFIG SysEx message to FirmataExpress.
Step 5: Wrapping Up
As we have seen, adding Firmata support for a new device requires you to modify the Arduino FirmataExpress server code and the Python-based pymata4 client code. FirmataExpress code can be challenging to debug. A method called printData was added to FirmataExpress to aid in debugging. This method allows you to send data values from FirmataExpress and will print them on the pymata4 console.
This function requires both a pointer to a character string and the value you wish to view. If the data value is contained in a variable called argc, you might call printData with the following parameters.
printData((char*)"argc= ", argc) ;
If you have any questions, just leave a comment, and I will be happy to answer.
Happy coding!