# time-service



ESP-IDF component for **accurate system time management** on ESP32 with SNTP synchronization and manual control.
Instead of letting the system time change silently in the background, this component enforces explicit, observable time control — every change is user-initiated, and every component that cares is notified.
---
## 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.0"
```
Or in your project's `idf_component.yml`:
```yaml
dependencies:
embedblocks/time_service: "^1.0.0"
```
---
## 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.0"