| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327 |
- #include <arpa/inet.h>
- #include <errno.h>
- #include <string.h>
- #include <sys/socket.h>
- #include <unistd.h>
- #include "esp_log.h"
- #include "esp_timer.h"
- #include "freertos/FreeRTOS.h"
- #include "freertos/task.h"
- #include "ntp_clock.h"
- static const char *TAG = "ntp_clock";
- // Timing packet types (from shairport-sync)
- #define TIMING_REQUEST 0xD2
- #define TIMING_RESPONSE 0xD3
- // Timing request packet structure (32 bytes)
- typedef struct __attribute__((packed)) {
- uint8_t leader; // 0x80
- uint8_t type; // 0xD2 for request
- uint16_t seqno; // sequence number (network byte order)
- uint32_t filler; // padding
- uint64_t origin; // our transmit time (will be echoed back)
- uint64_t receive; // zeroed in request
- uint64_t transmit; // zeroed in request
- } timing_packet_t;
- // Number of measurements to keep for stability
- #define TIMING_HISTORY_SIZE 8
- #define MIN_MEASUREMENTS 3
- #define TIMING_INTERVAL_MS 3000 // Send request every 3 seconds
- #define NTP_STACK_SIZE 3072
- // Timing state
- static struct {
- bool running;
- TaskHandle_t task_handle;
- int socket;
- struct sockaddr_in remote_addr;
- // Clock offset tracking
- bool locked;
- int64_t offset_ns; // Current best offset
- int64_t measurements[TIMING_HISTORY_SIZE];
- int64_t dispersions[TIMING_HISTORY_SIZE];
- int measurement_count;
- int measurement_index;
- } ntp = {0};
- // Convert local time (microseconds) to NTP timestamp format (for packet)
- static uint64_t local_us_to_ntp(int64_t local_us) {
- // NTP timestamp: upper 32 bits = seconds, lower 32 bits = fraction
- uint32_t secs = (uint32_t)(local_us / 1000000);
- uint64_t frac_us = local_us % 1000000;
- uint32_t frac = (uint32_t)((frac_us << 32) / 1000000);
- return ((uint64_t)secs << 32) | frac;
- }
- // Convert NTP timestamp from packet to nanoseconds
- static int64_t ntp_to_ns(uint32_t secs, uint32_t frac) {
- int64_t ns = (int64_t)secs * 1000000000LL;
- ns += ((int64_t)frac * 1000000000LL) >> 32;
- return ns;
- }
- // Process timing response and calculate offset
- static void process_timing_response(const uint8_t *packet, size_t len,
- int64_t arrival_ns) {
- if (len < sizeof(timing_packet_t)) {
- return;
- }
- // Extract timestamps from response
- // Origin: our original transmit time (echoed back)
- // Receive: when remote received our request
- // Transmit: when remote sent response
- // Offsets in packet (after 8-byte header)
- // Bytes 8-15: origin timestamp (our transmit time, echoed)
- // Bytes 16-23: receive timestamp (remote's receive time)
- // Bytes 24-31: transmit timestamp (remote's transmit time)
- uint32_t origin_secs = ntohl(*(uint32_t *)(packet + 8));
- uint32_t origin_frac = ntohl(*(uint32_t *)(packet + 12));
- uint32_t receive_secs = ntohl(*(uint32_t *)(packet + 16));
- uint32_t receive_frac = ntohl(*(uint32_t *)(packet + 20));
- uint32_t transmit_secs = ntohl(*(uint32_t *)(packet + 24));
- uint32_t transmit_frac = ntohl(*(uint32_t *)(packet + 28));
- int64_t departure_ns = ntp_to_ns(origin_secs, origin_frac);
- int64_t remote_receive_ns = ntp_to_ns(receive_secs, receive_frac);
- int64_t remote_transmit_ns = ntp_to_ns(transmit_secs, transmit_frac);
- // Calculate offset using NTP formula:
- // Round-trip time = (arrival - departure) - (transmit - receive)
- // Offset = ((receive - departure) + (transmit - arrival)) / 2
- // = remote_transmit + (return_time - remote_processing) / 2 - arrival
- int64_t round_trip_ns = arrival_ns - departure_ns;
- int64_t remote_processing_ns = remote_transmit_ns - remote_receive_ns;
- int64_t network_delay_ns = round_trip_ns - remote_processing_ns;
- // Offset = remote_transmit + network_delay/2 - arrival
- // This gives: remote_time = local_time + offset
- int64_t offset_ns = remote_transmit_ns + (network_delay_ns / 2) - arrival_ns;
- // Dispersion is the uncertainty (half round-trip time)
- int64_t dispersion_ns = network_delay_ns / 2;
- // Sanity check: reject if round-trip is negative or too large (>1 second)
- if (round_trip_ns < 0 || round_trip_ns > 1000000000LL) {
- ESP_LOGW(TAG, "Rejecting measurement: RTT=%lld ms",
- (long long)(round_trip_ns / 1000000));
- return;
- }
- // Store measurement
- int idx = ntp.measurement_index;
- ntp.measurements[idx] = offset_ns;
- ntp.dispersions[idx] = dispersion_ns;
- ntp.measurement_index = (idx + 1) % TIMING_HISTORY_SIZE;
- if (ntp.measurement_count < TIMING_HISTORY_SIZE) {
- ntp.measurement_count++;
- }
- // Find best measurement (lowest dispersion)
- int64_t best_offset = offset_ns;
- int64_t best_dispersion = dispersion_ns;
- for (int i = 0; i < ntp.measurement_count; i++) {
- if (ntp.dispersions[i] < best_dispersion) {
- best_dispersion = ntp.dispersions[i];
- best_offset = ntp.measurements[i];
- }
- }
- ntp.offset_ns = best_offset;
- // Consider locked after enough measurements
- if (!ntp.locked && ntp.measurement_count >= MIN_MEASUREMENTS) {
- ntp.locked = true;
- ESP_LOGI(TAG, "NTP timing locked: offset=%lld ms, dispersion=%lld us",
- (long long)(ntp.offset_ns / 1000000),
- (long long)(best_dispersion / 1000));
- }
- ESP_LOGD(TAG, "Timing: RTT=%lld us, offset=%lld ms",
- (long long)(round_trip_ns / 1000), (long long)(offset_ns / 1000000));
- }
- // Send timing request
- static void send_timing_request(void) {
- timing_packet_t req;
- memset(&req, 0, sizeof(req));
- req.leader = 0x80;
- req.type = TIMING_REQUEST;
- req.seqno = htons(7); // Fixed sequence number (like shairport-sync)
- // Set transmit field to our current time — the AirPlay source echoes
- // request bytes 24-31 (transmit) into response bytes 8-15 (origin).
- int64_t now_us = esp_timer_get_time();
- uint64_t ntp_time = local_us_to_ntp(now_us);
- // Split into network byte order (avoid unaligned access)
- uint8_t *tx_bytes = (uint8_t *)&req.transmit;
- uint32_t secs = htonl((uint32_t)(ntp_time >> 32));
- uint32_t frac = htonl((uint32_t)(ntp_time & 0xFFFFFFFF));
- memcpy(tx_bytes, &secs, 4);
- memcpy(tx_bytes + 4, &frac, 4);
- sendto(ntp.socket, &req, sizeof(req), 0, (struct sockaddr *)&ntp.remote_addr,
- sizeof(ntp.remote_addr));
- }
- // Timing task: sends requests and processes responses
- static void ntp_task(void *pvParameters) {
- uint8_t packet[64];
- struct sockaddr_in src_addr;
- socklen_t addr_len;
- // Initial delay before first request
- vTaskDelay(pdMS_TO_TICKS(300));
- TickType_t last_request = 0;
- while (ntp.running) {
- // Send timing request periodically
- TickType_t now = xTaskGetTickCount();
- if (now - last_request >= pdMS_TO_TICKS(TIMING_INTERVAL_MS)) {
- send_timing_request();
- last_request = now;
- }
- // Check for response (with short timeout)
- addr_len = sizeof(src_addr);
- ssize_t len = recvfrom(ntp.socket, packet, sizeof(packet), 0,
- (struct sockaddr *)&src_addr, &addr_len);
- if (len < 0) {
- if (errno == EAGAIN || errno == EWOULDBLOCK) {
- continue;
- }
- if (ntp.running) {
- ESP_LOGE(TAG, "recvfrom error: %d", errno);
- }
- break;
- }
- if (len >= 2) {
- int64_t arrival_ns = esp_timer_get_time() * 1000LL;
- uint8_t pkt_type = packet[1];
- if (pkt_type == TIMING_RESPONSE) {
- process_timing_response(packet, len, arrival_ns);
- }
- }
- }
- ntp.task_handle = NULL;
- vTaskDelete(NULL);
- }
- static bool ntp_wait_for_task_stopped(int timeout_ticks) {
- while (ntp.task_handle != NULL && timeout_ticks-- > 0) {
- vTaskDelay(pdMS_TO_TICKS(50));
- }
- return ntp.task_handle == NULL;
- }
- esp_err_t ntp_clock_start_client(uint32_t remote_ip, uint16_t remote_port) {
- if (ntp.running) {
- // If already running to same target, keep going
- if (ntp.remote_addr.sin_addr.s_addr == remote_ip &&
- ntohs(ntp.remote_addr.sin_port) == remote_port) {
- return ESP_OK;
- }
- // Stop existing and restart with new target
- ntp_clock_stop();
- if (!ntp_wait_for_task_stopped(20)) {
- ESP_LOGE(TAG, "Previous NTP task did not stop");
- return ESP_ERR_INVALID_STATE;
- }
- }
- if (ntp.task_handle != NULL) {
- ESP_LOGW(TAG, "NTP task still stopping, waiting");
- if (!ntp_wait_for_task_stopped(20)) {
- ESP_LOGE(TAG, "NTP task still active");
- return ESP_ERR_INVALID_STATE;
- }
- }
- ntp.socket = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
- if (ntp.socket < 0) {
- ESP_LOGE(TAG, "Failed to create socket: %d", errno);
- return ESP_FAIL;
- }
- // Set receive timeout (100ms for responsive checking)
- struct timeval tv = {.tv_sec = 0, .tv_usec = 100000};
- setsockopt(ntp.socket, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
- // Setup remote address
- memset(&ntp.remote_addr, 0, sizeof(ntp.remote_addr));
- ntp.remote_addr.sin_family = AF_INET;
- ntp.remote_addr.sin_addr.s_addr = remote_ip;
- ntp.remote_addr.sin_port = htons(remote_port);
- // Reset state
- ntp.locked = false;
- ntp.offset_ns = 0;
- ntp.measurement_count = 0;
- ntp.measurement_index = 0;
- memset(ntp.measurements, 0, sizeof(ntp.measurements));
- memset(ntp.dispersions, 0, sizeof(ntp.dispersions));
- ntp.running = true;
- ntp.task_handle = NULL;
- BaseType_t task_ret = xTaskCreate(ntp_task, "ntp_clock", NTP_STACK_SIZE, NULL,
- 5, &ntp.task_handle);
- if (task_ret != pdPASS || ntp.task_handle == NULL) {
- ESP_LOGE(TAG, "Failed to create NTP task");
- close(ntp.socket);
- ntp.socket = -1;
- ntp.running = false;
- return ESP_FAIL;
- }
- uint8_t *ip = (uint8_t *)&remote_ip;
- ESP_LOGI(TAG, "NTP timing client started -> %d.%d.%d.%d:%d", ip[0], ip[1],
- ip[2], ip[3], remote_port);
- return ESP_OK;
- }
- void ntp_clock_stop(void) {
- if (!ntp.running) {
- return;
- }
- ntp.running = false;
- if (ntp.socket >= 0) {
- close(ntp.socket);
- ntp.socket = -1;
- }
- if (ntp.task_handle) {
- if (!ntp_wait_for_task_stopped(20)) {
- ESP_LOGW(TAG, "NTP task did not exit within timeout");
- }
- }
- ntp.locked = false;
- ntp.measurement_count = 0;
- ESP_LOGI(TAG, "NTP timing stopped");
- }
- bool ntp_clock_is_locked(void) {
- return ntp.locked;
- }
- int64_t ntp_clock_get_offset_ns(void) {
- return ntp.offset_ns;
- }
|