IoTプロトコル

NTP

ネットワーク経由で時刻を合わせる仕組み。

概要

NTP(Network Time Protocol)は、ネットワークに接続されたコンピュータやデバイスの時刻をUTC(協定世界時)に同期するためのプロトコルです。1985年にDavid L. Millsが設計し、現在はRFC 5905(NTPv4)として標準化されています。

IoT・組み込み機器にとって正確な時刻は以下の理由から不可欠です。

  • タイムスタンプの一貫性:異なるデバイスから送られたセンサーデータを正しい時系列で結合する
  • 証明書の有効期限検証:TLS/HTTPSによる安全通信には現在時刻が必要
  • ログの正確性:障害解析にはミリ秒単位の正確なログ時刻が必要
  • スケジューリング:定時実行タスク(毎日0時にデータ集計など)
  • JWTトークンの有効期限:認証トークンの期限切れ検知

マイコン(ESP32、STM32等)は電源断すると内蔵RTC(リアルタイムクロック)の時刻がリセットされる製品が多く、起動時にNTPで時刻を取得するのが標準的なパターンです。


歴史・背景

NTPの歴史はインターネットの黎明期にさかのぼります。1985年、David L. Millsによって最初のバージョン(NTPv0)が実装され、実験的なインターネット上で使われ始めました。

バージョンRFC主な変更
NTPv1RFC 10591988最初の公式RFC
NTPv2RFC 11191989認証機能追加
NTPv3RFC 13051992分散タイムサーバー対応
NTPv4RFC 59052010IPv6対応、アルゴリズム改善
NTSRFC 89152020TLSによる認証付き時刻同期

IoT分野での重要性が高まった背景として、2016年のMirai等のIoTマルウェア感染事例で「証明書期限切れで通信できない → 時刻が合っていない」という問題が多発し、適切な時刻同期の重要性が再認識されました。

SNTP(Simple NTP)はNTPv4の簡易サブセットで、精度要件が緩い組み込み機器に向いています。ESP-IDFやLwIPが実装しているのは実質的にSNTPです。


技術仕様

階層構造(Stratum)

NTPサーバーはStratumという階層で管理されます。

Stratum 0: 基準信号源(原子時計、GNSS、GPS)

Stratum 1: 基準時刻サーバー(Stratum 0に直接接続)

Stratum 2: Stratum 1と同期するサーバー

Stratum 3: Stratum 2と同期するサーバー
    ↓   ...
Stratum 15: 最下層(Stratum 16は未同期/無効)

一般的なIoTデバイスはStratum 2〜3のサーバー(pool.ntp.org等)と同期します。

パケットフォーマット

NTPパケットは48バイト固定長(基本部分)です。

 0                   1                   2                   3
 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|LI | VN  |Mode |    Stratum     |     Poll      |  Precision   |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                         Root Delay                            |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                         Root Dispersion                       |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                          Reference ID                         |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                                                               |
+                     Reference Timestamp (64)                  +
|                                                               |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                     Originate Timestamp (64)                  |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                      Receive Timestamp (64)                   |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                      Transmit Timestamp (64)                  |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
フィールド説明
LIうるう秒警告(Leap Indicator)
VNバージョン番号(現在4)
Mode3=クライアント、4=サーバー
Stratum階層レベル
Pollポーリング間隔(2^n秒)
Precisionクロック精度(2^n秒)
Root Delay基準時刻源までの往復遅延
Transmit Timestamp送信時刻(64ビット、1/2^32秒精度)

NTPタイムスタンプ形式

NTPタイムスタンプは1900年1月1日00:00:00 UTCを起点とする64ビット固定小数点数です。

上位32ビット: 秒(整数部)
下位32ビット: 秒未満(小数部、1/2^32秒 ≈ 0.23ナノ秒精度)

Unixタイムへの変換:
NTP epoch (1900-01-01) から Unix epoch (1970-01-01) は 70年 = 2208988800秒

unix_time = ntp_seconds - 2208988800
import struct
import socket

def get_ntp_time(server="pool.ntp.org"):
    """NTPサーバーから時刻を取得"""
    NTP_PORT = 123
    REFERENCE_TIME_1900 = 2208988800  # NTP epochからUnix epochへのオフセット
    
    # NTPリクエストパケット(48バイト)
    packet = b'\x1b' + b'\x00' * 47
    
    sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
    sock.settimeout(5)
    
    sock.sendto(packet, (server, NTP_PORT))
    data, _ = sock.recvfrom(1024)
    sock.close()
    
    if data:
        # Transmit Timestamp(40バイト目から8バイト)
        ntp_time = struct.unpack('!I', data[40:44])[0]
        unix_time = ntp_time - REFERENCE_TIME_1900
        return unix_time
    
    return None

import datetime
t = get_ntp_time()
print(datetime.datetime.utcfromtimestamp(t))

時刻オフセット計算

NTPクライアントはネットワーク遅延を考慮して時刻オフセットを計算します。

T1: クライアントがリクエストを送信した時刻
T2: サーバーがリクエストを受信した時刻
T3: サーバーがレスポンスを送信した時刻
T4: クライアントがレスポンスを受信した時刻

往復遅延(RTT): δ = (T4 - T1) - (T3 - T2)
時刻オフセット:  θ = ((T2 - T1) + (T3 - T4)) / 2

クライアントの正確な時刻 = クライアント現在時刻 + θ

動作原理

ESP32でのSNTP実装

#include "esp_sntp.h"
#include "esp_log.h"
#include <time.h>

static const char *TAG = "sntp";

// 時刻同期完了コールバック
void time_sync_notification_cb(struct timeval *tv) {
    ESP_LOGI(TAG, "時刻同期完了: %s", ctime(&tv->tv_sec));
}

void sntp_init_and_sync(void) {
    ESP_LOGI(TAG, "SNTPクライアント初期化");
    
    // NTPサーバー設定(日本向けにJP NTPプールを使用)
    sntp_setoperatingmode(SNTP_OPMODE_POLL);
    sntp_setservername(0, "ntp.nict.jp");           // 日本標準時研究機構
    sntp_setservername(1, "0.jp.pool.ntp.org");      // 日本プール
    sntp_setservername(2, "pool.ntp.org");            // グローバルフォールバック
    
    sntp_set_time_sync_notification_cb(time_sync_notification_cb);
    
    // 滑らか補正(slewモード)- 急な時刻ジャンプを避ける
    sntp_set_sync_mode(SNTP_SYNC_MODE_SMOOTH);
    
    sntp_init();
    
    // 同期完了を待つ(最大10秒)
    int retry = 0;
    const int max_retries = 20;
    
    while (sntp_get_sync_status() == SNTP_SYNC_STATUS_RESET && 
           ++retry < max_retries) {
        ESP_LOGI(TAG, "時刻同期待ち... (%d/%d)", retry, max_retries);
        vTaskDelay(pdMS_TO_TICKS(500));
    }
    
    if (retry == max_retries) {
        ESP_LOGW(TAG, "NTP同期タイムアウト");
        return;
    }
    
    // タイムゾーン設定(日本標準時 UTC+9)
    setenv("TZ", "JST-9", 1);
    tzset();
    
    time_t now;
    struct tm timeinfo;
    time(&now);
    localtime_r(&now, &timeinfo);
    
    char strtime[64];
    strftime(strtime, sizeof(strtime), "%Y-%m-%d %H:%M:%S", &timeinfo);
    ESP_LOGI(TAG, "現在時刻(JST): %s", strtime);
}

Raspberry PiでのNTP(systemd-timesyncd)

# systemd-timesyncd の設定確認
timedatectl status

# NTPサーバーをJP NTPプールに設定
cat > /etc/systemd/timesyncd.conf << EOF
[Time]
NTP=ntp.nict.jp 0.jp.pool.ntp.org 1.jp.pool.ntp.org
FallbackNTP=pool.ntp.org
EOF

systemctl restart systemd-timesyncd

# 同期状態を確認
timedatectl show-timesync --all

# 詳細ログ
journalctl -u systemd-timesyncd -f
# ntpd(従来型)の設定
cat >> /etc/ntp.conf << EOF
server ntp.nict.jp iburst
server 0.jp.pool.ntp.org iburst
server 1.jp.pool.ntp.org iburst
EOF

systemctl restart ntp

# 同期状態確認
ntpq -pn

NTSによるセキュア時刻同期

NTS(Network Time Security、RFC 8915)はTLSによりNTP通信を保護します。

# chrony(NTSサポート)の設定
cat > /etc/chrony.conf << EOF
# NTSサーバー(Cloudflare)
server time.cloudflare.com iburst nts

# 証明書検証
ntsdumpdir /var/lib/chrony/nts-cookies

# 許容誤差
makestep 1.0 3
rtcsync
EOF

systemctl restart chronyd

# NTS接続確認
chronyc sources -v
# * time.cloudflare.com ... nts

用途・ユースケース

センサーデータのタイムスタンプ付与

// NTP同期済みの時刻でセンサーデータを記録
#include <time.h>
#include "cJSON.h"

char* create_timestamped_data(float temperature, float humidity) {
    time_t now;
    struct tm *tm_info;
    char iso8601[30];
    
    time(&now);
    tm_info = gmtime(&now);
    
    // ISO 8601形式(UTC)
    strftime(iso8601, sizeof(iso8601), 
             "%Y-%m-%dT%H:%M:%SZ", tm_info);
    
    cJSON *root = cJSON_CreateObject();
    cJSON_AddStringToObject(root, "timestamp", iso8601);
    cJSON_AddNumberToObject(root, "temperature", temperature);
    cJSON_AddNumberToObject(root, "humidity", humidity);
    
    char *json = cJSON_PrintUnformatted(root);
    cJSON_Delete(root);
    return json;
}

TLS証明書検証への活用

HTTPSで通信する前に時刻が正確であることを確認します。証明書の有効期限チェックに現在時刻が使われるため、時刻が大幅にずれているとすべてのHTTPS通信が失敗します。

// TLS接続前の時刻チェック
bool is_time_synchronized(void) {
    time_t now;
    time(&now);
    
    // 2020年以降なら同期済みとみなす(未同期だと1970年になる)
    struct tm *tm_info = gmtime(&now);
    return (tm_info->tm_year + 1900) >= 2020;
}

void connect_to_api(void) {
    if (!is_time_synchronized()) {
        ESP_LOGE(TAG, "時刻未同期:TLS接続を中止");
        sync_time_from_ntp();
        return;
    }
    
    // HTTPS接続を実行
    http_connect_to_api();
}

定時タスクのスケジューリング

# Raspberry Pi: 毎日0時にデータ集計
import schedule
import time
from datetime import datetime

def daily_aggregation():
    print(f"日次集計開始: {datetime.now().isoformat()}")
    # データ集計処理
    aggregate_daily_data()
    print(f"日次集計完了: {datetime.now().isoformat()}")

# スケジュール登録(ntpで時刻が正確であることが前提)
schedule.every().day.at("00:00").do(daily_aggregation)
schedule.every(5).minutes.do(send_heartbeat)

while True:
    schedule.run_pending()
    time.sleep(1)

LoRaWAN との時刻同期

LoRaWANのClass Bモードでは、ゲートウェイのビーコン信号とデバイスの時刻同期が必要です。ゲートウェイはGPS/NTPで時刻を同期し、ビーコンに含めます。

GPS(Stratum 0)

LoRaWANゲートウェイ(NTP/GPS時刻同期済み)
    ↓ ビーコン信号(128秒周期)
LoRaWANデバイス(Class B)
    ビーコン時刻に合わせてRTCを校正
    → 決まった時間スロットで受信待ち

実装・開発のポイント

NTPサーバーの選択

サーバー対象精度備考
ntp.nict.jp日本国内<1ms国立研究開発法人情報通信研究機構
pool.ntp.orgグローバル数ms自動的に近いサーバーを選択
time.cloudflare.comグローバル数msNTS対応
time.google.comグローバル<1msGoogleのタイムサーバー
169.254.169.123AWS EC2<1msAWSインスタンスローカル

ポーリング間隔の設定

// ESP-IDFでのポーリング間隔設定(sntp_component.h)
// デフォルトは15分(900秒)
// 最小値は15秒

// menuconfig 設定(sdkconfig.defaults)
// CONFIG_LWIP_SNTP_UPDATE_DELAY=15000  // 15秒(最小値)
// CONFIG_LWIP_SNTP_UPDATE_DELAY=3600000 // 1時間(推奨)

時刻同期失敗時の対処

// RTCに時刻をバックアップして、NTP失敗時に使う(ESP32の例)
#include "driver/rtc_io.h"

// RTC メモリに最後の同期時刻を保存(ディープスリープをまたいで保持)
RTC_DATA_ATTR static time_t last_synced_time = 0;
RTC_DATA_ATTR static uint32_t last_synced_uptime = 0;

void backup_time_to_rtc(void) {
    time(&last_synced_time);
    last_synced_uptime = esp_log_timestamp() / 1000;
}

void restore_time_from_rtc(void) {
    if (last_synced_time > 0) {
        // ディープスリープ中の経過時間を加算
        uint32_t sleep_duration_sec = 
            (esp_log_timestamp() / 1000) - last_synced_uptime;
        
        time_t estimated_time = last_synced_time + sleep_duration_sec;
        struct timeval tv = {.tv_sec = estimated_time};
        settimeofday(&tv, NULL);
        
        ESP_LOGI(TAG, "RTC時刻を復元: %s", ctime(&estimated_time));
    }
}

うるう秒の考慮

NTPはうるう秒を「LIフラグ」で予告します。サーバーがうるう秒調整を行う前日の23:59から翌日0:01のセンサーデータのタイムスタンプには注意が必要です。

# うるう秒を持たないUNIX時間を使う場合の注意
# MQTTやREST APIでは ISO 8601 (UTC) 文字列が推奨
# "2015-06-30T23:59:59Z" → 最後のうるう秒の例

from datetime import datetime, timezone
# UTC時刻で記録(うるう秒は記録されない点に注意)
ts = datetime.now(timezone.utc).isoformat()

他技術との比較

項目NTPGNSS/GPS時刻PTP(IEEE 1588)RTC(内蔵)
精度ミリ秒〜マイクロ秒ナノ秒ナノ秒〜マイクロ秒秒〜分/月
ネットワーク要件インターネット接続GPSアンテナLAN内専用不要
コスト無料GPSモジュール代専用HW内蔵
IoT適合○(屋外・農業)◎(産業)△(長期ずれ)
セキュリティ△(NTSで改善)

GPS/GNSSと組み合わせることで、インターネット接続がないオフライン環境でも高精度な時刻同期が可能です。産業IoT分野ではPTP(IEEE 1588)がマイクロ秒精度を実現し、モーション制御など同期精度が重要な用途に使われます。ESP32Raspberry PiではNTPが時刻同期の標準手段であり、Wi-Fi接続後の初期化シーケンスに組み込むことが推奨されます。

関連用語

参考リンク