トップ ソフト 雑記 日記 リンク

2015年12月24日木曜日

ArduinoでJJY発信(おうちハック Advent Calendar 2015)

おうちハック Advent Calendar 201515日目に書いた内容について、
ツッコミがあったのでおうちハックアドベントカレンダーの15日目として。
内容的には、完全にArduino Advent Calendar 2015なんですが、続き物ということでおうちハックにぶら下げます。


電波時計に供給される日本標準時電波(JJY)は、国立研究開発法人情報通信研究機構の日本標準時グループによって、福島県と佐賀県から送信されています。電波時計は、これらの電波を受信して現在時刻を知るわけです。

自宅に電波時計があり、買ったときに住んでいた家では電波を受信していたのですが、何度か引越をするうちに電波を受信しなくなってしまったようです。
何度手動で合わせても少しずつずれてしまう。電車やバスの時間に合わせて家を出ようと思うと、これが意外と辛い。 また、窓際に置いて強制受信ボタンを押してみたりしたのですが、どうも受信しない。

ということで、ちょうど家にあったArduinoをネットに繋いでNTPから時刻情報を受信し、そこからJJYを発信してみました。

必要なもの
  • Arduino
  • ネットシールド
  • 電源用USBケーブル
  • 通信用LANケーブル
Arduinoとネットシールドを合体させ、電源とLANを繋いで以下のスケッチを流し込みます。
#include <SPI.h>
#include <Ethernet.h>
#include <EthernetUdp.h>
#include <Time.h>


// bit set / clear
#ifndef cbi
#define cbi(PORT, BIT) (_SFR_BYTE(PORT) &= ~_BV(BIT))
#endif
#ifndef sbi
#define sbi(PORT, BIT) (_SFR_BYTE(PORT) |= _BV(BIT))
#endif

// Circuit
// pin3 - LED -------- GND
// A UDP instance to let us send and receive packets over UDP
EthernetUDP Udp;


byte timeServer[] = { 192,168,0,4 }; // ntp.nict.jp NTP server

const unsigned int localPort = 8888;   // local UDP port
const int NTP_PACKET_SIZE= 48;
byte packetBuffer[NTP_PACKET_SIZE];
byte timecode[60];
unsigned long lastNTPTime = 0;

void setup()
{
  Serial.begin(9600);

  // Ethernet settings
  byte mac[] = { 0x90,0xA2,0xDA,0x0F,0xE6,0xEE };
  byte ip[] = { 192,168,0,20 };
  byte gateway[] = { 192,168,0,1 };
  byte subnet[] = { 255,255,255,0 };

  Ethernet.begin(mac, ip, gateway, subnet);
  delay(1000);
  Udp.begin(localPort);
  NTPSetTime();
  setupTimeCode();
}

void loop()
{
  int wait_start = second();
  while (wait_start == second()); // wait until time is corrected
  unsigned long startTime = millis();

  // generate 40khz from 3 pin using PWM
  pinMode(3, OUTPUT);
  digitalWrite(3, LOW);
  TCCR2A = _BV(WGM20);
  TCCR2B = _BV(WGM22) | _BV(CS20);
  OCR2A = F_CPU / 2 / 40000/*hz*/;
  OCR2B = OCR2A / 2; /* 50% duty */
  sbi(TCCR2A,COM2B1);

  // print out current time
  Serial.print(year());
  Serial.print('/');
  Serial.print(month());
  Serial.print('/');
  Serial.print(day());
  Serial.print(' ');
  Serial.print(hour());
  Serial.print(':');
  Serial.print(minute());
  Serial.print(':');
  Serial.print(second());
  Serial.print('(');
  Serial.print(weekday());
  Serial.print(')');
  Serial.println(dayOfYear());

  // calc signal duration (ms)
  int ms = calcTimeCodeDuration();

  // wait ms and stop PWM
  while (millis() - startTime < ms);
  cbi(TCCR2A,COM2B1);
  
  if (millis() - lastNTPTime > 10*60*1000L) {
    NTPSetTime();
    lastNTPTime = millis();
  }
}

//=========================== NTP ===========================
bool NTPSetTime()
{
  bool flag = false;
  sendNTPpacket(timeServer);
  Serial.println("Waiting NTP response ...");
  delay(100);  // wait 100 ms to ensure the packet is sent

  if (Udp.parsePacket()) {
    Udp.read(packetBuffer,NTP_PACKET_SIZE);
    unsigned long highWord = word(packetBuffer[40], packetBuffer[41]);
    unsigned long lowWord = word(packetBuffer[42], packetBuffer[43]);
    unsigned long secsSince1900 = highWord << 16 | lowWord;
  
    unsigned int fraction_hi = word(packetBuffer[44], packetBuffer[45]);
  
    // Unix time starts on Jan 1 1970, v.s. NTP ans is since Jan 1 1900.
    const unsigned long seventyYears = 2208988800UL;     
    unsigned long epoch = secsSince1900 - seventyYears;  
  
    // wait until next sencod
    delay(900 - fraction_hi / (65536/1000));
  
    // Set current time in JST (GMT+0900)
    setTime(epoch + 1 + 9*60*60);
  
    Serial.print("localtime = ");
    Serial.println(epoch);
    flag = true;
  }
  else {
    Serial.println("no data.");
  }
  return flag;
}

unsigned long sendNTPpacket(byte *address)
{
  memset(packetBuffer, 0, NTP_PACKET_SIZE); 
  // Initialize values needed to form NTP request
  packetBuffer[0] = 0b11100011;   // LI, Version, Mode
  packetBuffer[1] = 0;     // Stratum, or type of clock
  packetBuffer[2] = 6;     // Polling Interval
  packetBuffer[3] = 0xEC;  // Peer Clock Precision
  // 8 bytes of zero for Root Delay & Root Dispersion
  packetBuffer[12]  = 49; 
  packetBuffer[13]  = 0x4E;
  packetBuffer[14]  = 49;
  packetBuffer[15]  = 52;

  //send NTP request packet (port 123)
  Udp.beginPacket(address, 123);
  Udp.write( packetBuffer,NTP_PACKET_SIZE);
  Udp.endPacket();
}


//=========================== JJY ===========================

unsigned int calcTimeCodeDuration()
{
  int s = second();
  if (s == 0)
    setupTimeCode();
  return timecode[s] * 100;
}

void setupTimeCode()
{
  int i;
  memset(timecode, 8, sizeof(timecode));

  setupTimeCode100(minute(), 0);
  timecode[0] = 2;

  setupTimeCode100(hour(), 10);

  int d = dayOfYear();
  setupTimeCode100(d/10, 20);
  setupTimeCode100(d%10*10, 30);

  int parity1 = 0, parity2 = 0;
  for (i = 12; i < 20; i++) parity1 ^= timecode[i] == 5;
  for (i =  1; i < 10; i++) parity2 ^= timecode[i] == 5;
  timecode[36] = parity1 ? 5 : 8;
  timecode[37] = parity2 ? 5 : 8;

  setupTimeCode100(year()%100, 40);
  for (i = 44; i > 40; i--)
    timecode[i] = timecode[i-1];
  timecode[40] = 8;

  int w = weekday() - 1;
  timecode[50] = (w & 4) ? 5 : 8;
  timecode[51] = (w & 2) ? 5 : 8;
  timecode[52] = (w & 1) ? 5 : 8;
  timecode[59] = 2;
  
  /* dump */
  for (i = 0; i < 60; i++) {
    Serial.print(timecode[i], DEC);
    Serial.print(i % 10 == 9 ? "\r\n" : " ");
  }
}

void setupTimeCode100(int m, int i)
{
  timecode[i+0] = ((m/10) & 8) ? 5 : 8;
  timecode[i+1] = ((m/10) & 4) ? 5 : 8;
  timecode[i+2] = ((m/10) & 2) ? 5 : 8;
  timecode[i+3] = ((m/10) & 1) ? 5 : 8;
  timecode[i+4] = 8;
  timecode[i+5] = ((m%10) & 8) ? 5 : 8;
  timecode[i+6] = ((m%10) & 4) ? 5 : 8;
  timecode[i+7] = ((m%10) & 2) ? 5 : 8;
  timecode[i+8] = ((m%10) & 1) ? 5 : 8;
  timecode[i+9] = 2;
}

int dayOfYear()
{
  tmElements_t tm = {0, 0, 0, 0, 1, 1, CalendarYrToTm(year())};
  time_t t = makeTime(tm);
  return (now() - t) / SECS_PER_DAY + 1;
}
とりあえず動けば良いやということで、マジックナンバーを埋め込みまくりです。
スケッチの中で指定している「192,168,0,4」は自宅LAN内のNTPdです。自宅にNTPdが立っていない人は、対応しているルーターやプロバイダで指定されているものを使うと良いでしょう。
ArduinoのIPアドレスが192.168.0.20、ゲートウェイとサブネットマスクは環境に合わせて設定してください。
loop関数内でPWM変調を掛けて電波の送信をコントロールしています。
それ以外の関数はNTPdとおしゃべりする部分です。

発信する電波は、40khz、ArduinoのPWM送信に対応している3番ピンを使います。

わたしの場合は50cmのリード線を3周巻いています。
アンテナとして機能するために、何周かのループになっている必要があります。
時計のすぐそばに設置するなら、それほど電波強度も必要ないので、3周程度できっちりと巻いてなくても受信できました。ただし時計の真裏でないと伝わりませんでした。
時計側の性能にもよるので、もう少し長いリード線を使って、きっちりと巻いた方がいいかもしれません。

我が家では、ルーター(NEC AtermWR9500N)にUSBポートと、余っているLANポートがあったので、そこに繋いでいます。

3 件のコメント:

  1. JJYがどうしても届かない場所に電波時計を置きたかったので実際に作ってみての1週間程度運用してみての微妙な点を指摘させて頂きます。

    1.millis() で待ちを行っている箇所がありますが、millis() は50日程度で0クリアされるため、0クリアが起きた場合の回避を記述していないので、最悪50日待ちとなります。(滅多に起きないでしょうが。。。)

    2.NTPの取得をサーバー側からの送信時間に頼り1回で時刻設定していますが、私の場合ちょうど10分程度ずれた(原因が通信遅延なのかNTPのデータ異常なのかは不明)ので、2回以上取得して、誤差が許容範囲ならば適用とかにしないと危険です。

    3.delay(900 - fraction_hi / (65536/1000)); の箇所が無限待ちとなる可能性がありそうです。というのもfraction_hi は最大4294967296を想定しなければいけないのに、それを65.536で割った値を引くと負数もしくはとんでもなく大きな正の数となるからです。
    計算式の正しさが今ひとつわからないのですが、最低限回避のコードを記述しないとハングした様な動きとなります。

    返信削除
    返信
    1. 検証ありがとうございます。
      コードは、あちこちから引っ張り寄せた物を継ぎはぎしたので、NTPDの仕様をきちんと理解した上で書かれたものでは無いのです。
      なんとなく、正しい時間で設定されたので良いかーという感じで運用していました。

      削除
  2. 前回と同じものです
    もう一点指摘させてください。
    恐らくは前回の2の問題に関連しています。

    if (Udp.parsePacket()) {

    とありますが、parsePacketは受け取れたデータサイズを示す様です。
    つまりは、
    if (Udp.parsePacket() >= NTP_PACKET_SIZE) {
    としておかないと、受信データが途中で切れた上に、正常受信した場合に
    時計情報が出鱈目となります。

    恐らくは元ネタという場所を知っていますが、色々考慮すべき事がありますね。。。
    無料でここまで提供頂いて指摘もなにもあったものではありませんが。。。

    返信削除

広告