# time-service



ESP-IDF component that gives your ESP32 accurate timestamps without a dedicated RTC module.
On boot it sets the clock from the build timestamp so time is always running from something reasonable — no network required, no configuration needed. From there you can improve accuracy in whatever way suits your application: sync over SNTP once WiFi is up, set it manually from an external RTC or stored value, or both.
When time is corrected, your application is notified with the exact delta — no silent jumps, no surprises.
---
## Features
- **Explicit synchronization only** — no hidden background updates
- **Async SNTP sync** with single-flight coalescing — multiple callers share one network request
- **Jump callbacks** — every time change delivers old time, new time, delta, and origin
- **Graceful offline operation** — falls back to build timestamp, works without network stack
- **Manual time set** — accept time from RTC module, user input, or any external source
- **Human-readable conversion** — fixed-format timestamp strings for logs
- **Thread-safe reads** — safe to call `time_service_now()` from any task
- **Configurable SNTP servers** via `menuconfig`
---
## Installation
### Using ESP-IDF Component Manager (Recommended)
```bash
idf.py add-dependency "embedblocks/time-service^1.0.1"
```
Or in your project's `idf_component.yml`:
```yaml
dependencies:
embedblocks/time-service: "^1.0.1"
```
---
## How It Works
The component acts as a **time authority controller** — a policy layer over `settimeofday()`. The ESP32 RTC and ESP-IDF handle continuous time progression. This component governs who sets the time, when it changes, and how changes are communicated.
```
┌─────────────────────────────────────┐
│ Time Inputs │
│ │
│ SNTP sync │ User set │ Build │
└──────┬──────┴─────┬──────┴────┬─────┘
│ │ │
└────────────┼───────────┘
↓
time-service
(policy layer)
↓
settimeofday()
↓
ESP-IDF + RTC layer
(continuous progression)
↓
time_service_now()
```
At init, the component sets the system clock from the build timestamp — no network required. When the application has network connectivity, it can request an explicit SNTP sync via `time_service_sync_async()`.
---
## Time Sources
| Source | When Used | Origin Tag |
|---|---|---|
| SNTP | Explicit `sync_async()` call | `TIME_ORIGIN_SNTP` |
| Build timestamp | Init fallback, no network required | `TIME_ORIGIN_BUILD` |
| User provided | `set_time()` or `set_time_from_human()` | `TIME_ORIGIN_USER` |
---
## Offline Behavior
The component is designed to work without a network stack. If no network interfaces are registered when sync is requested:
- SNTP is skipped immediately with a warning log
- Callback fires with `success = false`
- System time is unchanged — continues on build time or last user-set time
- No crash, no assert, no attempt to initialize the network stack
---
## API
### Initialization
```c
esp_err_t time_service_init(time_init_result_t *result);
```
Safe to call before network initialization. Always succeeds.
---
### Core
```c
time_t time_service_now(void);
esp_err_t time_service_set_time(time_t t);
esp_err_t time_service_set_source(time_sync_source_t source);
void time_service_get_status(time_status_t *status);
```
---
### Async Sync
```c
esp_err_t time_service_sync_async(time_sync_cb_t cb);
```
Non-blocking. If a sync is already in progress, the callback is coalesced — one network request, all callbacks fire on completion.
---
### Human-Readable Time
```c
bool time_service_to_human(time_t epoch, int32_t offset_sec, time_human_t *out);
bool time_service_now_human(int32_t offset_sec, time_human_t *out);
size_t time_service_format(const time_human_t *ht, char *buf, size_t len);
size_t time_service_now_str(int32_t offset_sec, char *buf, size_t len);
esp_err_t time_service_set_time_from_human(const time_human_t *ht);
```
Fixed output format: `YYYY-MM-DD HH:MM:SS ±HHMM`
GMT offset is specified in **seconds** (e.g. `18000` for UTC+5 / Pakistan Standard Time).
---
## Usage Example
### Offline — Manual Time Set
```c
#include "time_service.h"
void app_main(void)
{
time_init_result_t result;
time_service_init(&result);
// Set time manually (from RTC module, user input, etc.)
time_service_set_time(1705314600);
char buf[TIME_SERVICE_STR_BUF_SIZE];
while (1) {
time_service_now_str(18000, buf, sizeof(buf));
ESP_LOGI(TAG, "[%s] running", buf);
vTaskDelay(pdMS_TO_TICKS(2000));
}
}
```
---
### Online — SNTP Sync After WiFi Connect
```c
#include "time_service.h"
static void on_sync_done(const time_sync_result_t *res)
{
if (res->success) {
ESP_LOGI(TAG, "Synced — jumped %ld sec via %d",
(long)res->jump.delta_sec, res->jump.origin);
} else {
ESP_LOGW(TAG, "Sync failed");
}
}
void app_main(void)
{
time_init_result_t result;
time_service_init(&result);
// ... connect WiFi ...
// Request sync once network is ready
vTaskDelay(pdMS_TO_TICKS(2000)); // allow stack to settle
time_service_sync_async(on_sync_done);
char buf[TIME_SERVICE_STR_BUF_SIZE];
while (1) {
time_service_now_str(18000, buf, sizeof(buf));
ESP_LOGI(TAG, "[%s]", buf);
vTaskDelay(pdMS_TO_TICKS(2000));
}
}
```
---
## Configuration
Via `idf.py menuconfig` → **Time Service Configuration**:
| Key | Default | Description |
|---|---|---|
| `TIME_SERVICE_SNTP_SERVER_0` | `pool.ntp.org` | Primary SNTP server |
| `TIME_SERVICE_SNTP_SERVER_1` | `time.google.com` | Secondary SNTP server |
| `TIME_SERVICE_SNTP_SERVER_2` | `time.windows.com` | Tertiary SNTP server |
| `TIME_SERVICE_SNTP_TIMEOUT_SEC` | `10` | Sync timeout in seconds |
| `TIME_SERVICE_MAX_SYNC_CALLBACKS` | `8` | Max coalesced callbacks per sync |
| `TIME_SERVICE_TASK_STACK_SIZE` | `4096` | Internal task stack (bytes) |
| `TIME_SERVICE_TASK_PRIORITY` | `5` | Internal task FreeRTOS priority |
---
## Network Requirements
The component does **not** manage the network stack. The application is responsible for:
- `esp_netif_init()`
- `esp_event_loop_create_default()`
- WiFi or Ethernet initialization and connection
`time_service_sync_async()` should only be called after IP connectivity is established. If called without a network, it fails gracefully via the callback.
---
## Chip Support
| Chip | Status |
|---|---|
| ESP32 | ✅ Tested |
| ESP32-S2 | ⚠️ Expected to work |
| ESP32-S3 | ⚠️ Expected to work |
| ESP32-C3 | ⚠️ Expected to work |
---
## Examples
| Example | Description |
|---|---|
| `offline` | Sets time manually via `set_time()` and logs human-readable timestamps. No network required. |
| `online` | Connects to WiFi using `example_connect`, performs SNTP sync, and logs timestamps with jump notifications. |
### Running an Example
```bash
cd examples/offline
idf.py build flash monitor
```
```bash
cd examples/online
idf.py build flash monitor
```
For the online example, configure your WiFi credentials first:
```bash
idf.py menuconfig # → Example Connection Configuration
```
---
## Limitations
- No automatic drift correction — sync is always explicit
- No DST handling — UTC offset is fixed, specified by the caller
- No custom timestamp format strings — one fixed, sortable format
- No deinit — component runs for the lifetime of the application
- Single internal sync task — concurrent sync requests are coalesced, not parallelized
---
## License
MIT License
idf.py add-dependency "embedblocks/time-service^1.0.2"