Table of Contents

ChangeLog

15 of June 2024

  • initial release

DFRobot / Gravity Analog Sound Meter 1.0

This template is for the Gravity Analog Sound Meter released by DFRobot. The code has some boilerplate functionality where the template is able to connect to a designated Wifi network, as well as listen to over the wire OTP updates, whilst continuously polling the Gravity Analog Sound Meter and publishing the measured value to an MQTT bus.

Usage

At the time of writing, there is no runtime functionality and the template should just be modified in order to set the Wifi and/or OTP credentials after which it can be wired up and left to run on its own.

Theory

The theory behind the template revolves around the linear design of the Analog Sound Meter in that the output voltage is calibrated out of production to correspond to some amount of decibels. As per the specifications, the noise level that the Analog Sound Meter is capable of registering ranges from $30dBA$ to $130dBA$ with those two values corresponding to $0.6V$, respectively the maximum voltage that the analog pin is capable of reading (for most micro-arduinos, such as the WeMoS, that would be $3.3V$). Given the linear progression, the only thing left to do is just measure the voltage of the analog pin connected to the meter's signal pin and then linearly map the voltage range $0.6..3.3V$ into the decibels range $30...130dB$.

Here is the code responsible for measuring noise levels using DFRobot's Gravity Analog Sound Level Meter 1.0 within the template provided in the code section:

// https://grimore.org/fuss/mathematics/algebra#linearly_map_a_value_in_a_range_into_another_range
float mapValueToRange(float value, float xMin, float xMax, float yMin, float yMax) {
  return yMin + ((yMax - yMin) * (value - xMin)/(xMax - xMin));
}
 
  // measure noise level (dbA)
  float ai, vc, noise;
  ai = analogRead(DECIBELMETER_PIN_ANALOG);
  vc = mapValueToRange(ai, 0, 4095, 0, MAX_ANALOG_VOLTAGE);
  noise = vc * 50.0;

where:

As can be observed, after the value from the analog pin is measured, the value is mapped to the voltage range of the pin. The resulting scaled voltage is then multiplied by $50.0$, which is a cheap operation compared to mapValueToRange that is meant to scale the measured voltage to the decibel range, based on the observation that the lowest voltage $0.6V$ corresponding to the lowest amount of decibels that the board can measure, when multiplied by $50.0$ yield a nice lowest decibel value of $30dbA$.

Aside the boilerplate Arduino code, the former short snippet is the whole nucleus that is responsible of measuring the noise level that is characteristic to the provided template in the code section.

Code

/*************************************************************************/
/*    Copyright (C) 2024 Wizardry and Steamworks - License: GNU GPLv3    */
/*************************************************************************/
 
// Removing comment for debugging over the first serial port.
#define DEBUG 1
// The AP to connect to via Wifi.
#define STA_SSID ""
// The AP Wifi password.
#define STA_PSK ""
// The MQTT broker to connect to.
#define MQTT_HOST ""
// The MQTT broker username.
#define MQTT_USERNAME ""
// The MQTT broker password.
#define MQTT_PASSWORD ""
// The MQTT broker port.
#define MQTT_PORT 1883
// The default MQTT client ID is "esp-CHIPID" where CHIPID is the ESP8266
// or ESP32 chip identifier.
#define MQTT_CLIENT_ID() String("esp-" + String(GET_CHIP_ID(), HEX))
// The authentication password to use for OTA updates.
#define OTA_PASSWORD ""
// The OTA port on which updates take place.
#define OTA_PORT 8266
// The default topic that the sketch subscribes to is "esp/CHIPID" where
// CHIPID is the ESP8266 or ESP32 chip identifier.
#define MQTT_TOPIC() String("esp/" + String(GET_CHIP_ID(), HEX))
 
// Platform specific defines.
#if defined(ARDUINO_ARCH_ESP8266)
#define GET_CHIP_ID() (ESP.getChipId())
#elif defined(ARDUINO_ARCH_ESP32)
#define GET_CHIP_ID() ((uint16_t)(ESP.getEfuseMac() >> 32))
#endif
 
// Miscellaneous defines.
//#define CHIP_ID_HEX (String(GET_CHIP_ID()).c_str())
#define HOSTNAME() String("esp-" + String(GET_CHIP_ID(), HEX))
 
// Platform specific libraries.
#if defined(ARDUINO_ARCH_ESP8266)
#include <ESP8266WiFi.h>
#include <ESP8266mDNS.h>
#elif defined(ARDUINO_ARCH_ESP32)
#include <WiFi.h>
#include <ESPmDNS.h>
#endif
// General libraries.
#include <WiFiUdp.h>
#include <ArduinoOTA.h>
#include <PubSubClient.h>
#include <ArduinoJson.h>
#if defined(ARDUINO_ARCH_ESP32)
#include <FS.h>
#include <SPIFFS.h>
#endif
 
WiFiClient espClient;
PubSubClient mqttClient(espClient);
 
#define DECIBELMETER_PIN_ANALOG 32
#define MAX_ANALOG_VOLTAGE  3.3
 
// https://grimore.org/fuss/mathematics/algebra#linearly_map_a_value_in_a_range_into_another_range
float mapValueToRange(float value, float xMin, float xMax, float yMin, float yMax) {
  return yMin + (
           (
             yMax - yMin
           )
           *
           (
             value - xMin
           )
           /
           (
             xMax - xMin
           )
         );
}
 
void mqttCallback(char *topic, byte *payload, unsigned int length) {
  String msgTopic = String(topic);
  // payload is not null terminated and casting will not work
  char msgPayload[length + 1];
  snprintf(msgPayload, length + 1, "%s", payload);
#ifdef DEBUG
  Serial.println("Message received on topic: " + String(topic) + " with payload: " + String(msgPayload));
#endif
 
  // Parse the payload sent to the MQTT topic as a JSON document.
  StaticJsonDocument<256> doc;
#ifdef DEBUG
  Serial.println("Deserializing message....");
#endif
  DeserializationError error = deserializeJson(doc, msgPayload);
  if (error) {
#ifdef DEBUG
    Serial.println("Failed to parse MQTT payload as JSON: " + String(error.c_str()));
#endif
    return;
  }
}
 
bool mqttConnect() {
#ifdef DEBUG
  Serial.println("Attempting to connect to MQTT broker: " + String(MQTT_HOST));
#endif
  mqttClient.setServer(MQTT_HOST, MQTT_PORT);
 
  StaticJsonDocument<256> msg;
  if (mqttClient.connect(MQTT_CLIENT_ID().c_str(), MQTT_USERNAME, MQTT_PASSWORD)) {
#ifdef DEBUG
    Serial.println("Established connection with MQTT broker using client ID: " + MQTT_CLIENT_ID());
#endif
    // Uncomment this in order to receive messages from MQTT.
    //mqttClient.setCallback(mqttCallback);
    msg["action"] = "connected";
    char output[256];
    serializeJson(msg, output, 256);
    mqttClient.publish(MQTT_TOPIC().c_str(), output);
#ifdef DEBUG
    Serial.println("Attempting to subscribe to MQTT topic: " + MQTT_TOPIC());
#endif
    if (!mqttClient.subscribe(MQTT_TOPIC().c_str())) {
#ifdef DEBUG
      Serial.println("Failed to subscribe to MQTT topic: " + MQTT_TOPIC());
#endif
      return false;
    }
#ifdef DEBUG
    Serial.println("Subscribed to MQTT topic: " + MQTT_TOPIC());
#endif
    msg.clear();
    msg["action"] = "subscribed";
    serializeJson(msg, output, 256);
    mqttClient.publish(MQTT_TOPIC().c_str(), output);
    return true;
  }
#ifdef DEBUG
  Serial.println("Connection to MQTT broker failed with MQTT client state: " + String(mqttClient.state()));
#endif
  return false;
}
 
bool satifsyMessagePumps() {
  // Process OTA loop first since emergency OTA updates might be needed.
  ArduinoOTA.handle();
 
  // Process MQTT client loop.
  if (!mqttClient.connected()) {
    // If the connection to the MQTT broker has failed then sleep before carrying on.
    if (!mqttConnect()) {
      return false;
    }
  }
  mqttClient.loop();
 
  return true;
}
 
// the setup function runs once when you press reset or power the board
void setup() {
  // initialize the GPIO pins taht will be used
  pinMode (DECIBELMETER_PIN_ANALOG, INPUT);
 
  Serial.begin(19200);
 
#ifdef DEBUG
  Serial.println("Booted, setting up Wifi in 10s...");
#endif
  delay(10000);
 
  WiFi.mode(WIFI_STA);
#if defined(ARDUINO_ARCH_ESP8266)
  WiFi.hostname(HOSTNAME().c_str());
#elif defined(ARDUINO_ARCH_ESP32)
  WiFi.setHostname(HOSTNAME().c_str());
#endif
  WiFi.begin(STA_SSID, STA_PSK);
  while (WiFi.waitForConnectResult() != WL_CONNECTED) {
#ifdef DEBUG
    Serial.println("Failed to connect to Wifi, rebooting in 5s...");
#endif
    delay(5000);
    ESP.restart();
  }
 
#ifdef DEBUG
  Serial.print("Connected to Wifi: ");
  Serial.println(WiFi.localIP());
  Serial.println("Setting up OTA in 10s...");
#endif
  delay(10000);
 
  // Port defaults to 8266
  ArduinoOTA.setPort(OTA_PORT);
 
  // Hostname defaults to esp-[ChipID]
  ArduinoOTA.setHostname(HOSTNAME().c_str());
 
  // Set the OTA password
  ArduinoOTA.setPassword(OTA_PASSWORD);
 
  ArduinoOTA.onStart([]() {
    switch (ArduinoOTA.getCommand()) {
      case U_FLASH:  // Sketch
#ifdef DEBUG
        Serial.println("OTA start updating sketch.");
#endif
        break;
#if defined(ARDUINO_ARCH_ESP8266)
      case U_FS:
#elif defined(ARDUINO_ARCH_ESP32)
      case U_SPIFFS:
#endif
#ifdef DEBUG
        Serial.println("OTA start updating filesystem.");
#endif
        SPIFFS.end();
        break;
      default:
#ifdef DEBUG
        Serial.println("Unknown OTA update type.");
#endif
        break;
    }
  });
  ArduinoOTA.onEnd([]() {
#ifdef DEBUG
    Serial.println("OTA update complete.");
#endif
    SPIFFS.begin();
#if defined(ARDUINO_ARCH_ESP8266)
    // For what it's worth, check the filesystem on ESP8266.
    SPIFFS.check();
#endif
    ESP.restart();
  });
  ArduinoOTA.onProgress([](unsigned int progress, unsigned int total) {
#ifdef DEBUG
    Serial.printf("OTA update progress: %u%%\r", (progress / (total / 100)));
#endif
  });
  ArduinoOTA.onError([](ota_error_t error) {
#ifdef DEBUG
    Serial.printf("OTA update error [%u]: ", error);
#endif
    switch (error) {
      case OTA_AUTH_ERROR:
#ifdef DEBUG
        Serial.println("OTA authentication failed");
#endif
        break;
      case OTA_BEGIN_ERROR:
#ifdef DEBUG
        Serial.println("OTA begin failed");
#endif
        break;
      case OTA_CONNECT_ERROR:
#ifdef DEBUG
        Serial.println("OTA connect failed");
#endif
        break;
      case OTA_RECEIVE_ERROR:
#ifdef DEBUG
        Serial.println("OTA receive failed");
#endif
        break;
      case OTA_END_ERROR:
#ifdef DEBUG
        Serial.println("OTA end failed");
#endif
        break;
      default:
#ifdef DEBUG
        Serial.println("Unknown OTA failure");
#endif
        break;
    }
    ESP.restart();
  });
  ArduinoOTA.begin();
 
  // Set up MQTT client.
  mqttClient.setServer(MQTT_HOST, MQTT_PORT);
 
  // Touchdown.
#ifdef DEBUG
  Serial.println("Setup complete.");
#endif
}
 
// the loop function runs over and over again forever
void loop() {
  // Check the Wifi connection status.
  int wifiStatus = WiFi.status();
  switch (wifiStatus) {
    case WL_CONNECTED:
      if (!satifsyMessagePumps()) {
        delay(1000);
        break;
      }
      delay(1);
      break;
    case WL_NO_SHIELD:
#ifdef DEBUG
      Serial.println("No Wifi shield present.");
#endif
      goto DEFAULT_CASE;
      break;
    case WL_NO_SSID_AVAIL:
#ifdef DEBUG
      Serial.println("Configured SSID not found.");
#endif
      goto DEFAULT_CASE;
      break;
    // Temporary statuses indicating transitional states.
    case WL_IDLE_STATUS:
    case WL_SCAN_COMPLETED:
      delay(1000);
      break;
    // Fatal Wifi statuses trigger a delayed ESP restart.
    case WL_CONNECT_FAILED:
    case WL_CONNECTION_LOST:
    case WL_DISCONNECTED:
    default:
#ifdef DEBUG
      Serial.println("Wifi connection failed with status: " + String(wifiStatus));
#endif
DEFAULT_CASE:
      delay(10000);
      ESP.restart();
      break;
  }
 
  // measure noise level (dBA)
  float ai, vc, noise;
  ai = analogRead(DECIBELMETER_PIN_ANALOG);`
  vc = mapValueToRange(ai, 0, 4095, 0, MAX_ANALOG_VOLTAGE);
  noise = vc * 50.0;
#ifdef DEBUG
  Serial.println("Noise (dBA): " + String(noise));
#endif
  StaticJsonDocument<256> msg;
  msg["noise"] = noise;
  char output[256];
  serializeJson(msg, output, 256);
  mqttClient.publish(MQTT_TOPIC().c_str(), output);
 
  delay(1000);
}