Skip to content

TIME

Time management is an important research area in wireless sensor networks. It involves how to effectively coordinate and schedule nodes in the network to achieve efficient data collection, transmission, and processing. In this section, we will introduce some basic time management concepts, and time synchronization will be discussed in detail in other chapters.

Before we delve into detailed discussion, let's first investigate the related codes:

time.hpp

#pragma once
#include <Arduino.h>

/*
 * MCUTime - Unified time structure for embedded systems.
 * Provides both UNIX timestamp and human-readable calendar format.
 */
class MCUTime
{
public:
    /* === UNIX Time Fields === */
    uint64_t unix_epoch; // Seconds since 1970-01-01 00:00:00 UTC
    uint64_t unix_ms;    // Milliseconds since 1970-01-01 00:00:00 UTC
    uint64_t estimated_unix_ms; // Estimated milliseconds since 1970-01-01 00:00:00 UTC
    uint64_t estimated_unix_epoch; // Estimated seconds since 1970-01-01 00:00:00 UTC

    /* === Calendar Fields === */
    uint16_t year;  // Year (e.g., 2025)
    uint8_t month;  // Month [1-12]
    uint8_t day;    // Day [1-31]
    uint8_t hour;   // Hour [0-23]
    uint8_t minute; // Minute [0-59]
    uint8_t second; // Second [0-59]
    int32_t ms;     // Milliseconds [0-999]

    /* === Time Tracking === */
    uint64_t last_update_ms; // Last update time in milliseconds by syncing
    uint64_t last_update_epoch; // Last update time in seconds since epoch by syncing
    uint64_t mcu_time_ms; // Current time in milliseconds since MCU start
    uint64_t mcu_base_ms; // Base time in milliseconds when synced
    uint64_t delta_ms; // Time since last update in milliseconds

public:
    /* === Constructors === */
    MCUTime(); // Default constructor initializes to 0

    /* === Setters === */
    void set_calendar();  // Set calendar fields based on current Unix time
    void set_time_ms(uint64_t ms); // Set Unix time by passing milliseconds since Unix epoch
    void set_time_epoch(uint64_t epoch); // Set Unix time by passing seconds since Unix epoch

    /* === Time Update === */
    uint64_t estimate_time_ms(); // estimate time ms based on MCU millis and last update
    uint64_t estimate_time_epoch(); // estimate time epoch based on MCU millis and last update

    /* === Getters === */
    uint64_t get_unix_ms() const;
    uint64_t get_unix_epoch() const;
    uint64_t get_now_time_ms() const; // Get current time in ms in Unix ms format
    uint64_t compute_ms_from_calendar() const; // Compute Unix ms from calendar fields
    String to_string() const; // Return "YYYY-MM-DD HH:MM:SS.mmm"
    void print() const;       // Print formatted string to Serial

    /* === Comparison === */
    int8_t compare_to(const MCUTime &other) const;
};

// Add at the bottom of the header
extern MCUTime Time;  // Global time variable
extern MCUTime SensingSchedule; // Time synchronization variable

time.cpp

#include "time.hpp"

/* Definition of the operator functions in the MCUTime class */
MCUTime::MCUTime()
{
    unix_epoch = 0;
    unix_ms = 0;
    estimated_unix_ms = 0;
    estimated_unix_epoch = 0;
    year = 1970;
    month = 1;
    day = 1;
    hour = 0;
    minute = 0;
    second = 0;
    ms = 0;
    last_update_ms = 0;
    last_update_epoch = 0;
    mcu_time_ms = 0;
    delta_ms = 0;
}

// Helper: check leap year
static bool is_leap_year(uint16_t year)
{
    return ((year % 4 == 0) && (year % 100 != 0)) || (year % 400 == 0);
}

// Helper: days in each month (non-leap year)
static const uint8_t days_in_month[] = {
    31, 28, 31, 30, 31, 30,
    31, 31, 30, 31, 30, 31};

void MCUTime::set_calendar()
{
    // Extract milliseconds
    ms = unix_ms % 1000;

    // Get total seconds
    uint64_t seconds = unix_ms / 1000;

    // Keep a backup in the object
    unix_epoch = seconds;

    // Calculate time-of-day
    second = seconds % 60;
    seconds /= 60;
    minute = seconds % 60;
    seconds /= 60;
    hour = seconds % 24;
    seconds /= 24;

    // Now calculate date from days since 1970-01-01
    uint32_t days = (uint32_t)seconds;
    year = 1970;

    while (true)
    {
        uint16_t days_in_year = is_leap_year(year) ? 366 : 365;
        if (days >= days_in_year)
        {
            days -= days_in_year;
            year++;
        }
        else
        {
            break;
        }
    }

    // Now determine the month and day
    uint8_t month_index = 0;
    while (true)
    {
        uint8_t dim = days_in_month[month_index];
        if (month_index == 1 && is_leap_year(year))
            dim++; // February in leap year
        if (days >= dim)
        {
            days -= dim;
            month_index++;
        }
        else
        {
            break;
        }
    }

    month = month_index + 1;
    day = days + 1;
}

void MCUTime::set_time_ms(uint64_t ms)
{
    unix_ms = ms;
    unix_epoch = ms / 1000;

    // Update calendar fields
    set_calendar();
}

// be very careful to use this function
void MCUTime::set_time_epoch(uint64_t epoch)
{
    unix_epoch = epoch;
    unix_ms = epoch * 1000 + (millis() % 1000); // Convert to ms, keeping current millis for ms part

    // Update calendar fields
    set_calendar();
}

uint64_t MCUTime::estimate_time_ms()
{
    // Calculate delta since last update
    mcu_time_ms = millis();
    delta_ms = mcu_time_ms - mcu_base_ms;

    estimated_unix_ms = last_update_ms + delta_ms;
    estimated_unix_epoch = last_update_epoch + (delta_ms / 1000);

    return estimated_unix_ms;
}

uint64_t MCUTime::estimate_time_epoch()
{
    // Calculate delta since last update
    mcu_time_ms = millis();
    delta_ms = mcu_time_ms - mcu_base_ms;

    estimated_unix_ms = last_update_ms + delta_ms;
    estimated_unix_epoch = last_update_epoch + (delta_ms / 1000);

    return estimated_unix_epoch;
}

uint64_t MCUTime::get_unix_ms() const
{
    return unix_ms;
}

uint64_t MCUTime::get_unix_epoch() const
{
    return unix_epoch;
}

uint64_t MCUTime::get_now_time_ms() const
{
    return millis();
}

uint64_t MCUTime::compute_ms_from_calendar() const
{
    uint64_t days = 0;

    for (uint16_t y = 1970; y < year; y++)
    {
        days += is_leap_year(y) ? 366 : 365;
    }

    for (uint8_t m = 1; m < month; m++)
    {
        days += days_in_month[m - 1];
        if (m == 2 && is_leap_year(year))
            days++;
    }

    days += (day - 1);

    uint64_t ret_ms = (days * 86400ULL + hour * 3600 + minute * 60 + second) * 1000;
    ret_ms += ms;  // Include milliseconds

    return ret_ms;
}


String MCUTime::to_string() const
{
    char buffer[30];
    snprintf(buffer, sizeof(buffer), "%04u-%02u-%02u %02u:%02u:%02u.%03d",
             year, month, day, hour, minute, second, ms);
    return String(buffer);
}

void MCUTime::print() const
{
    Serial.println(to_string());
}

int8_t MCUTime::compare_to(const MCUTime &other) const
{
    if (unix_ms < other.unix_ms)
        return -1; // this is earlier
    else if (unix_ms > other.unix_ms)
        return 1; // this is later
    else
        return 0; // they are equal
}

/* Global time variable */
MCUTime Time;

/* Sensing Config */
MCUTime SensingSchedule;

Time Representation

Natural Timekeeping

Natural timekeeping refers to using calendar and clock formats familiar to humans to represent time. This usually includes fields such as year, month, day, hour, minute, and second. While this format is easy for humans to understand and use, it may not be efficient for computer systems to process.

Unix Time

Unix time refers to the number of seconds that have elapsed since January 1, 1970, 00:00:00 UTC. It is a standard time representation widely used in computer systems. The advantage of Unix time is that it is easy to compute and compare, although it is not easily readable by humans. Network time synchronization typically uses Unix time to align system clocks.

Runtime

Runtime refers to the duration from system startup to the current moment, usually measured in milliseconds. It is particularly important in real-time systems and embedded devices, as it helps monitor system status and performance. Most hardware platforms provide a timer to track runtime.

Note

In practical applications, natural timekeeping and Unix time are often converted between each other. Natural time is used when human-readable output is needed, while Unix time is preferred for efficient computation. In this project, while high precision is not strictly required, we adopt millisecond-level time representation to ensure reliable functionality.

Summary

From the perspective of time representation, all three methods have their strengths and weaknesses. Natural time is human-friendly but inefficient for machines; Unix time is computation-friendly but hard to read; runtime offers high precision but is relative to system startup time. For wireless sensor networks, synchronization between nodes is often needed.

To meet these requirements, we define a unified time structure called MCUTime in the time module. It includes fields for Unix time, natural time, and runtime, along with helper functions to support time conversion and calculation.

To facilitate time management, we define two key time variables:

  • Time: A global time variable that represents the current system time.

  • SensingSchedule: A configured sampling time variable used to record the scheduled sensing start time.