Monday, April 3, 2017

Temperature & Humidity Monitor with FlashAir + Arduino + Raspberry Pi (3): Sending Measurements from Arduino to Raspberry Pi

[This report is also available in English.]

Table of contents
  1. Setup and reading sensor values
  2. Using iSDIO while reading the SD card
  3. Sending measurements from Arduino to Raspberry Pi
  4. Visualizing on Raspberry Pi — completion

Check on Amazon In the previous installment we got the Arduino + FlashAir setup ready.
This time, we finally transmit data from FlashAir to the Raspberry Pi.
The source code for this project is on GitHub.

Connecting Arduino and FlashAir

Wire them up as shown:


SD card pin descriptions (chip select is on pin 4 in this project):
SilkscreenMeaning
CDUnused
CSChip select
CLKSync clock
SDOData output
SDIData input
GNDGround
3.3V3.3V power
5V5V power


Reading Config Values from FlashAir

Since we now have both SD access and iSDIO working, let's read a config value stored on the SD card.

I store the location ID on the FlashAir in a file called "id.txt" containing a single number.

On the Raspberry Pi side, measurements are stored with their location ID.
Writing the room ID to the FlashAir means a single device can be moved between rooms without reflashing.

This way, you can reassign the location by editing the file on the FlashAir — no need to open Arduino IDE and recompile.

Note: I originally wanted to store WiFi credentials in the SD card too, but implementing too much caused memory exhaustion and instability, so I dropped that.

// Include the patched SD library
#include <iSdio.h>
#include <SDExt.h>

// SD card chip select pin
#define CHIP_SELECT_PIN 4

// Config file path
#define ID_FILE "/id.txt"

int ID = 0;

// Read a file from the SD card
char* SD_read( char* path, char* buf, size_t s ) {

  // Delay to avoid instability from back-to-back FlashAir access
  delay(1000);

  // Open the file
  File file = SD.open(path,FILE_READ);
  if ( !file ) { return ""; }

  // Read contents
  String str = "";
  while (file.available()) { str += char(file.read()); }
  file.close();

  // Return as char array
  str.toCharArray(buf, s);

  return buf;
}

// SD card setup
void setup_SD() {
  Serial.println(F("Initializing SD card..."));

  // SS pin must be set to OUTPUT for the SD library to work
  pinMode(SS, OUTPUT);
  if (!SD.begin(CHIP_SELECT_PIN)) {
    Serial.println(F("Card failed, or not present"));
    abort();
  }

  // ... (abbreviated) ...

  // Convert SD card data to integer
  char buf[32];
  ID = atoi(SD_read(ID_FILE,buf,32));
  Serial.print(F("ID="));
  Serial.println(ID);
}

void setup() {
  Serial.begin(9600);
  while (!Serial) { ; }
  setup_SD();
}


Activating WiFi Only When Transmitting

Leaving WiFi on continuously drains power, so we connect only when needed.

#include <iSdio.h>
#include <SDExt.h>

// Measurement interval (ms)
#define LOG_INTERVAL 600000

char SSID[] = "{your WiFi SSID}";
char NETWORKKEY[] = "{your WiFi password}";
char API_HOST[] = "{Raspberry Pi IP address}";
char API_PATH[] = "/api.php";

uint8_t buffer[512];
uint32_t nextSequenceId = 0;

// Reused from FlashAir Developers tutorial
boolean iSDIO_waitResponse(uint32_t sequenceId) {
  // (body omitted)
}

boolean iSDIO_disconnect(uint32_t sequenceId) {
  // (body omitted)
}

boolean iSDIO_connect(uint32_t sequenceId, const char* ssid, const char* networkKey) {
  // (body omitted)
}

// Based on tutorial iSDIO_http, modified to take host/path as arguments
boolean iSDIO_http(uint32_t sequenceId, char* host, char* path, char* param) {
  Serial.println(F("http command: "));
  memset(buffer, 0, 512);
  uint8_t* p = buffer;
  p = put_command_header(p, 1, 0);

  // 0x21 = connect without SSL
  p = put_command_info_header(p, 0x21, sequenceId, 2);

  // Host = Raspberry Pi IP address
  p = put_str_arg(p, host);

  // Build HTTP GET request
  char getParam[128];
  sprintf(getParam,
    "GET %s?%s HTTP/1.1\r\n"
    "Host: %s\r\n"
    "User-Agent: FlashAir\r\n"
    "\r\n", path, param, host);
  Serial.println(getParam);
  p = put_str_arg(p, getParam);
  put_command_header(buffer, 1, (p - buffer));
  return SD.card.writeExtDataPort(1, 1, 0x000, buffer) ? true : false;
}

void setup_SD() {
  Serial.println(F("Initializing SD card..."));
  pinMode(SS, OUTPUT);
  if (!SD.begin(CHIP_SELECT_PIN)) {
    Serial.println(F("Card failed, or not present"));
    abort();
  }

  if (SD.card.readExtMemory(1, 1, 0x420, 0x34, buffer)) {
    if (buffer[0x20] == 0x01) {
      nextSequenceId = get_u32(buffer + 0x24);
      iSDIO_waitResponse(nextSequenceId);
      nextSequenceId++;
    } else {
      nextSequenceId = 0;
    }
  } else {
    Serial.println(F("Failed to read status."));
    nextSequenceId = 0;
  }

  char buf[32];
  ID = atoi(SD_read(ID_FILE,buf,32));
  Serial.print(F("ID="));
  Serial.println(ID);
}

void setup() {
  Serial.begin(9600);
  while (!Serial) { ; }
  setup_SD();
}

void loop() {
  unsigned long start = millis();
  digitalWrite(13,HIGH);

  // Read temperature and humidity
  int chk = DHT.read22(DHT_PIN);
  if ( chk != 0 ){
    Serial.println(F("Fail to read humidity."));
    delay(1000);
    return;
  }

  // Build HTTP parameters
  char humid[8];
  char temp[8];
  char param[32];
  sprintf(param,"ID=%d&H=%s&T=%s",
    ID,dtostrf(DHT.humidity,5,2,humid),dtostrf(DHT.temperature,5,2,temp));
  Serial.print(F("PARAM:"));
  Serial.println(param);

  // Connect to WiFi with retries
  for ( int retry = 3; retry > 0; retry -- ) {
    delay(1000);
    if( iSDIO_connect(nextSequenceId, SSID, NETWORKKEY) &&
      iSDIO_waitResponse(nextSequenceId) ) {
      Serial.println(F("Connected"));
      retry = 0;
    } else {
      Serial.print(F("Failed or waiting. errorCode="));
      Serial.println(SD.card.errorCode(), HEX);
    }
    nextSequenceId++;
  }

  // HTTP GET with retries
  for ( int retry = 3; retry > 0; retry -- ) {
    delay(1000);
    if ( iSDIO_http(nextSequenceId,API_HOST,API_PATH,param) &&
      iSDIO_waitResponse(nextSequenceId)) {
        Serial.println(F("OK"));
        retry = 0;
    } else {
      Serial.print(F("Failed or waiting. errorCode="));
      Serial.println(SD.card.errorCode(), HEX);
    }
    nextSequenceId++;
  }

  // Disconnect WiFi when done
  if (iSDIO_disconnect(nextSequenceId) &&
      iSDIO_waitResponse(nextSequenceId)) {
    Serial.println(F("Success."));
  } else {
    Serial.print(F("Failed or waiting. errorCode="));
    Serial.println(SD.card.errorCode(), HEX);
  }
  nextSequenceId++;

  digitalWrite(13,LOW);

  // Sleep until next measurement
  unsigned long span = millis() - start;
  if ( span > LOG_INTERVAL ) { return; }
  unsigned long sleep_time = LOG_INTERVAL - span;
  Serial.print(F("SLEEP => "));
  Serial.println(sleep_time,DEC);

  delaySleep(sleep_time);
}


Low-Power Sleep

Check on Amazon Measurement and transmission take only a few seconds — most of the time is idle waiting.

A proper low-power implementation would use a relay to cut power to the FlashAir and DHT sensor while the Arduino sleeps, combined with WDT (Watch Dog Timer) to wake up.

That's a bit involved, so for now I'm just using a sleep-modified delay as a rough power reduction — not really proper power saving.
It still consumes a lot of power, so long-term use needs an external battery pack.

For the sleep-delay technique, I referenced:
"Radio Pench: Reducing current consumption during Arduino delay()"

void delaySleep(unsigned long t) {
  //
  // Note: uses millis(), so continuous operation beyond ~50 days is not supported.
  //
  unsigned long t0;
  if( t <= 16 ) {                       // 16ms or less: use regular delay
    delay(t);
  }
  else{                                 // 17ms+: sleep-based delay
    t0 = millis();
    set_sleep_mode (SLEEP_MODE_IDLE);
    while( millis() - t0 < t ) {
      sleep_mode();                     // Enter sleep (auto-resumes, so loop)
    }
  }
}


The Arduino side is done.
Running it now will throw errors... of course — there's no server to receive the data yet.
Next time: building the Raspberry Pi server.

No comments:

Post a Comment