Analyse big data in excel with rows and columns

I have a simple program that saves a data set (temp, humidity, pressure), to an SD card every 5 minutes.
Its been running for 26 days now, so rather a lot of data.
Example
image
I'd like to be able to copy it to EXCEL and structure it like this:
time day1 day 2 day3
00:00 temp temp temp
00:05
with separate sheets for temp, humidity, pressure
but to keep it (relatively) simple lets just record and analyse the temperature readings.
Can anyone suggest a way to do this?

/*********
  Rui Santos & Sara Santos - Random Nerd Tutorials
  Complete project details at https://RandomNerdTutorials.com/esp32-datalogger-download-data-file/

  Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files.
  The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
  rev1  Modified to show local time
  all "success" messages commented out to make errors easier to fololow
*********/

#include <Arduino.h>
#include <WiFi.h>
#include <AsyncTCP.h>
#include <ESPAsyncWebServer.h>
#include "FS.h"
#include "SD.h"
#include "SPI.h"
#include <Adafruit_BME280.h>
#include <Adafruit_Sensor.h>
#include "time.h"
#include <WiFiUdp.h>
#include "creds.h"  //my wifi credentials

// NTP server to request epoch time
const char* ntpServer = "pool.ntp.org";

/*********
Connections as per tutorial: 
BME280: Vin 3v3; GND GND; SCL GPIO 22; SDA GPIO 21;
SD module: Vcc 3V3; CS GPIO 5; MOSI GPIO23; CLK GPIO 18; MISO GPIO 19; GND GND;
SD Card module requires Vcc to ESP32 Vin 
*********/

//D2 LED to GND to indicate reading in progress
const int ledPin = 2;
int ledState = LOW;   // ledState used to set the LED

// Variables to hold sensor readings
float temp;
float hum;
float pres;
String dataMessage;
String timeBuffer; // for local time

// File name where readings will be saved
const char* dataPath = "/data.txt";

// Timer variables
unsigned long lastTime = 0;
unsigned long timerDelay = 300;  //300 seconds = 5 min between readings; converted to msec in setup

// Create AsyncWebServer object on port 80
AsyncWebServer server(80);

// BME280 connect to ESP32 I2C (GPIO 21 = SDA, GPIO 22 = SCL)
Adafruit_BME280 bme;

// Init BME280
void initBME(){
  if (!bme.begin(0x76)) {
    Serial.println("Could not find a valid BME280 sensor, check wiring!");
    while (1);
  }
}

// Init microSD card
void initSDCard(){
  if(!SD.begin()){
    Serial.println("Card Mount Failed");
    return;
  }
  uint8_t cardType = SD.cardType();

  if(cardType == CARD_NONE){
    Serial.println("No SD card attached");
    return;
  }

  Serial.print("SD Card Type: ");
  if(cardType == CARD_MMC){
    Serial.println("MMC");
  } else if(cardType == CARD_SD){
    Serial.println("SDSC");
  } else if(cardType == CARD_SDHC){
    Serial.println("SDHC");
  } else {
    Serial.println("UNKNOWN");
  }
  uint64_t cardSize = SD.cardSize() / (1024 * 1024);
  Serial.printf("SD Card Size: %lluMB\n", cardSize);
}  //end of card initialisation

// Reconnect microSD card unless already connected
void connSDCard(){
  if(!SD.exists(dataPath)){
    Serial.print("File not accessible:  ");
    SD.begin();
    Serial.println("Reconnected SD card ");
    }
    return;
 }

// Write to the SD card
void writeFile(fs::FS &fs, const char * path, const char * message) {
  //Serial.printf("Writing file: %s\n", path);
    connSDCard();

  File file = fs.open(path, FILE_WRITE);
  if(!file) {
    Serial.println("Failed to open file for writing");
    return;
  }
  if(file.print(message)) {
    //Serial.println("File written");
  } else {
    Serial.println("Write failed");
  }
  file.close();
}

// Append data to the SD card
void appendFile(fs::FS &fs, const char * path, const char * message) {
  //Serial.printf("Appending to file: %s\n", path);
  connSDCard();
  File file = fs.open(path, FILE_APPEND);
  if(!file) {
    Serial.println("Failed to open file for appending");
    return;
  }
  if(file.print(message)) {
    //Serial.println("Message appended");
  } else {
    Serial.println("Append failed");
  }
  file.close();
}

// Delete file
void deleteFile(fs::FS &fs, const char * path){
  Serial.printf("Deleting file: %s\r\n", path);
  connSDCard();
  if(fs.remove(path)){
    Serial.println("- file deleted");
  } else {
    Serial.println("- delete failed");
  }
}

// Function that gets current epoch time  and converts to local time - from dataloggerLittleFSHighcharts.zip  https://drive.google.com/drive/folders/1pCMxZDTGQYR0_KZi7bPpzTpGjn2A6r3M
String getTime(){
  struct tm timeinfo;
  if(!getLocalTime(&timeinfo)){
    Serial.println("No time available (yet)");
  }
  char timeStringBuff[25];

  strftime( timeStringBuff, 
             sizeof(timeStringBuff), 
             //"%d/%m/%Y %H:%M:%S %Z", 
             "%a %d/%m/%Y %H:%M:%S",
             &timeinfo);
  String timeBuffer(timeStringBuff);
  return timeBuffer.c_str();
}

// Function that initializes wi-fi
void initWiFi() {
  WiFi.mode(WIFI_STA);
  WiFi.begin(ssid, password);
  Serial.print("Connecting to WiFi ..");
  while (WiFi.status() != WL_CONNECTED) {
    Serial.print('.');
    delay(1000);
  }
  Serial.println("connected");  //so IP is shown at start of line
  Serial.println(WiFi.localIP());
}

void setup() {
  Serial.begin(115200);
  Serial.println(__FILE__);  //print name of sketch for easy identification
  timerDelay *=1000; //convert from seconds to msec
  pinMode (ledPin, OUTPUT);
  

  initWiFi();
  initBME();
  initSDCard();
  configTime(0, 0, ntpServer);

  // If the data.txt file doesn't exist
  // Create a file on the SD card and write the data labels
  File file = SD.open("/data.txt");
  if(!file) {
    Serial.println("File doesn't exist");
    Serial.println("Creating file...");
    writeFile(SD, "/data.txt", "Day, Date, Time, Temperature, Humidity, Pressure \r\n");
  }
  else {
    Serial.println("File already exists");  
  }
  file.close();

  // Handle the root URL
  server.on("/", HTTP_GET, [](AsyncWebServerRequest *request){
    request->send(SD, "/index.html", "text/html");
  });

  // Handle the download button
  server.on("/download", HTTP_GET, [](AsyncWebServerRequest *request){
    Serial.println("HTTP: Downloading data ");
    request->send(SD, "/data.txt", String(), true);
  });

  // Handle the View Data button
  server.on("/view-data", HTTP_GET, [](AsyncWebServerRequest *request){
   // request->send(SD, "/data.txt", "text/txt");  doesnt work in firefox
  Serial.println("HTTP: Showing data on page ");
    request->send(SD, "/data.txt", "text/plain", false);
  });

  // Handle the delete button
  server.on("/delete", HTTP_GET, [](AsyncWebServerRequest *request){
    Serial.print("HTTP: Deleting data file ");
    deleteFile(SD, dataPath);
    request->send(200, "text/plain", "data.txt was deleted.");
  });

  // Uncomment the following line if you need to serve more static files like CSS and javascript or favicon
  //server.serveStatic("/", SD, "/");

  server.begin();
}

void loop() {
  if ((millis() - lastTime) > timerDelay) {
     //Get time
      timeBuffer = getTime();  //modified to now show gmt time
        
    //Get sensor readings
    temp = bme.readTemperature();
    //temp = 1.8*bme.readTemperature() + 32;
    hum = bme.readHumidity();
    pres = bme.readPressure()/100.0F;

    //Concatenate all info separated by commas
    dataMessage = timeBuffer + ", " + String(temp, 1) + ", " + String(hum, 1) + ", " + String(pres, 1)+ "\r\n";  //modified to show real time; /r/n is cr lf so no println needed
   
    Serial.print("Saving data: ");
    Serial.print(dataMessage);  //dont make extra line after printing the data line

    //Append the data to file
    appendFile(SD, "/data.txt", dataMessage.c_str());
     // if the LED is off turn it on and vice-versa:
    ledState = (ledState == LOW) ? HIGH : LOW;
    digitalWrite(ledPin, ledState);

    lastTime = millis();
  }
}

If not mistaken, it's called a pivot table in Excel. Even if I'm wrong, do the organising work in Excel.

As @sterretje says, use pivot table in excel.

You will need to add 2 columns to the data in excel to extract the date part and time part of the timestamp.

Then create your pivot table. Use the time column as the row axis and the date part as the column axis and the temp or humidity value as the pivot value.

Although it would involve more writing to the SD card, it might be advantageous to write one sensor value per row, like

2025-01-26 00:05, TEMP, 21.1
2025-01-26 00:05, HUM, 45.3
2025-01-26 00:10, TEMP, 21.2
2025-01-26 00:10, HUM, 45.2

You can do that in Excel

That's what I meant :rofl:

But you could also do it in the Arduino code

2025-01-26, 00:05, TEMP, 21.1
2025-01-26, 00:05, HUM, 45.3
2025-01-26, 00:10, TEMP, 21.2
2025-01-26, 00:10, HUM, 45.2

Which is easier :wink: Saves you a step in Excel (if you don't automate it).

Note:
Thanks for confirming that it indeed is a pivot tabel, I hardly use spreadsheet programs.

As @johnerrington already has 26 days of data then splitting the data in Excel is a more immediate solution

Fair point :wink:

Consider using a Data Base rather than misusing a spreadsheet. You can use Views to slice and dice the data different ways. SQLite would more than do the job.

SQLite Home Page

1 Like

Thanks everyone, I'll try it and report back. Never knew what a pivot table is!

If you use the data that you have already collected then you can use the Text to columns functionality to split the date and time into individual columns

Oh, wait 'till you discover pivot charts!

Thanks to all for your help. It was a bit trying but here is one result

This topic was automatically closed 180 days after the last reply. New replies are no longer allowed.