いろいろな人がM5StickCでJJY Simulatorを作っていましたので私も作ってみました。

 

まずは、その動画です。

 

ネットでいろいろ検索したところ、

(1) スイッチサイエンス M5StickC用JJYアンテナ基板

    → やはり外付け回路が必要なのかな…?大変そうだな。

(2) Qiita 標準電波 JJY もどきを M5StickC の Ticker で生成する

    → 適当な配線と抵抗1個でいいのか!これなら簡単そう。

(3) ESP32 (M5Stick-C) で電波時計を合わせよう

    → なんと!外付け全くなしでいいのか!びっくり!

という順番で見つかった感じです。

 

ソースコードはこちらです。

今までArduino IDEで作っていたのですがビルドが遅すぎてストレスだったので、

Visual Studio Code + PlatformIOにしたら快適になりました。

ソースコードの途中にある"<ssid>"と"<password>"は

実際に接続するWLANアクセスポイントのものに書き換えが必要です。

Aボタンを押すとNTPで時刻を再取得し、Bボタンを押すと画面が回転します。


※ 2022/06/29 こちらの関連記事もあります。

 

※ 2022/02/12 時刻取得に失敗することがあるため、

  WiFi.disconnect()を getLocalTime()の後に移動しました。

//------------------------------------------------------------------------------
// JJY Simulator for M5StickC
//
// * PlatformIO Project Configuration (platformio.ini)
// [env:m5stick-c]
// platform = espressif32
// board = m5stick-c
// framework = arduino
// lib_deps = m5stack/M5StickC@^0.2.0
// monitor_speed = 115200
//
// * M5StickC Port Configuration
// GPIO10: Internal LED
// GPIO26: Output for JJY
//
//------------------------------------------------------------------------------
// NICT JJY
//   http://jjy.nict.go.jp/jjy/trans/index-e.html
//
// (1) Time code system
//   :00 :01 :02 :03 :04 :05 :06 :07 :08 :09 :10 :11 :12 :13 :14
//    M  40m 20m 10m  0   8m  4m  2m  1m  P1  0   0  20h 10h  0
//   :15 :16 :17 :18 :19 :20 :21 :22 :23 :24 :25 :26 :27 :28 :29
//    8h  4h  2h  1h  P2  0   0 200d 100d 0  80d 40d 20d 10d  P3
//   :30 :31 :32 :33 :34 :35 :36 :37 :38 :39 :40 :41 :42 :43 :44
//    8d  4d  2d  1d  0   0  PA1 PA2 SU1  P4 SU2 80y 40y 20y 10y
//   :45 :46 :47 :48 :49 :50 :51 :52 :53 :54 :55 :56 :57 :58 :59
//    8y  4y  2y  1y  P5  4w  2w  1w LS1 LS2  0   0   0   0   P0
//
// (2) Pulse Width
//   Marker(M) and position markers(P0~P5): Pulse width 0.2s +-5ms
//   Binary 0: Pulse width 0.8s +-5ms
//   Binary 1: Pulse width 0.5s +-5ms
//
// (3) Marker (M) Position
//   The marker (M) corresponds to the exact minute
//   (the zero second of each minute).
//
// (4) Positions of the Position Markers (P0-P5)
//   The position marker P0 normally corresponds to the start of
//   the 59th second (for non-leap seconds).
//   However, for a positive leap second (insertion of a second),
//   P0 corresponds to the start of the 60th second (in this case,
//   the 59th second is represented by a binary 0).
//   For a negative leap second (removal of a second),
//   P0 corresponds to the start of the 58th second.
//   Position markers P1-P5 correspond to the start of the 9th,
//   19th, 29th, 39th, and 49th seconds, respectively.
//
// (5) Representation of Information
//   (a) Hour (6 bits: 20h,10h,8h,4h,2h,1h)
//     The hour in Japan Standard Time (JST) in 24-hour representation
//     20h,10: The value in BCD at 10 o'clock
//     8h,4h,2h,1h: The value in BCD at 1 o'clock
//   (b) Minute (7 bits: 40m,20m,10m,8m,4m,2m,1m)
//     The JST minute
//     40m,20m,10m: The value in BCD of 10 minutes
//     8m,4m,2m,1m: The value in BCD of 1 minute
//   (c) Annual date (10 bits: 200d,100d,80d,40d,20d,10d,8d,4d,2d,1d)
//     The annual date, counting January 1 as day 1.
//     Thus, Dec. 31 is day 365 in a non-leap year and day 366 in a leap year.
//     200d,100d: The value in BCD of 100 days
//     80d,40d,20d,10d: The value in BCD of 10 days
//     8d,4d,2d,1d: The value in BCD of 1 day
//   (d) Year (8 bits: 80y,40y,20y,10y,8y,4y,2y,1y)
//     The last 2 digits of the dominical year.
//     80y,40y,20y,10y: The value in BCD of 10 years
//     8y,4y,2y,1y: The value in BCD of 1 year
//   (e) Day of the week (3 bits: 4w,2w,1w)
//     The values 0-6 are allocated to Sunday-Saturday.
//   (f) Leap second information (2 bits: LS1,LS2)
//   (g) Parity (2 bits: PA1,PA2)
//     Parity bits are signals to determine whether the hour and minute signals
//     were correctly read. PA1 and PA2 correspond to the hour and minute,
//     respectively, and each is represented by an even parity of 1 bit.
//     PA1 = (20h+10h+8h+4h+2h+1h) mod 2
//     PA2 = (40m+20m+10m+8m+4m+2m+1m) mod 2
//     (mod 2 represents the remainder after division by 2)
//   (h) Spare bits (2 bits: SU1,SU2)
//
//------------------------------------------------------------------------------

#include <Arduino.h>
#include <M5StickC.h>
#include <WiFi.h>

//------------------------------------------------------------------------------
// Display Function

const char *m_name[] = {
    "M", "40m", "20m", "10m", "0", "8m", "4m", "2m", "1m", "P1",
    "0", "0", "20h", "10h", "0", "8h", "4h", "2h", "1h", "P2",
    "0", "0", "200d", "100d", "0", "80d", "40d", "20d", "10d", "P3",
    "8d", "4d", "2d", "1d", "0", "0", "PA1", "PA2", "SU1", "P4",
    "SU2", "80y", "40y", "20y", "10y", "8y", "4y", "2y", "1y", "P5",
    "4w", "2w", "1w", "LS1", "LS2", "0", "0", "0", "0", "P0"
};

int m_rotation = 1;     // 0,2: Portrait, 1,3: Landscape

void drawTitle() {
  const int px[2] = { 1, 4 };
  Serial.println("* JJY Simulator");
  M5.Lcd.setRotation(m_rotation);
  M5.Lcd.fillScreen(BLACK);
  M5.Lcd.setTextSize(1);
  M5.Lcd.setTextColor(WHITE, BLACK);
  M5.Lcd.drawString("JJY Simulator", px[m_rotation & 1], 6);
}

void drawSsid(const char *ssid) {
  const int px[2] = { 5, 10 };
  Serial.printf("SSID: %s\n", ssid);
  M5.Lcd.setCursor(px[m_rotation & 1], 18);
  M5.Lcd.printf("SSID: %s\n", ssid);
}

void drawNtps(const char *ntps) {
  const int px[2] = { 5, 10 };
  Serial.printf("\nNTP Server: %s\n", ntps);
  M5.Lcd.printf("\n");
  M5.Lcd.setCursor(px[m_rotation & 1], M5.Lcd.getCursorY() + 2);
  M5.Lcd.printf("NTP Server: %s\n", ntps);
}

void drawTime(struct tm timeInfo) {
  char buf[32];
  strftime(buf, sizeof(buf), "%Y/%m/%d(%a) %H:%M:%S", &timeInfo);
  Serial.println(buf);
  if (m_rotation & 1) { // Landscape
    M5.Lcd.setCursor(10, 18);
    M5.Lcd.printf(buf);
    M5.Lcd.setCursor(130, 6);
    M5.Lcd.printf("%4s", m_name[timeInfo.tm_sec]);
  } else {              // Portrait
    strftime(buf, sizeof(buf), "%y/%m/%d(%a", &timeInfo);
    M5.Lcd.setCursor(5, 18);
    M5.Lcd.printf(buf);
    strftime(buf, sizeof(buf), "%H:%M:%S", &timeInfo);
    M5.Lcd.setCursor(5, 28);
    M5.Lcd.printf(buf);
    M5.Lcd.setCursor(53, 38);
    M5.Lcd.printf("%4s", m_name[timeInfo.tm_sec]);
  }
}

void drawCode(int second, int bit, int spot = false) {
  const int px[2] = { 7, 12 };
  const int py[2] = { 54, 30 };
  const int md[2] = { 10, 20 };
  int dx = 7;
  int dy = 16;
  int sx = px[m_rotation & 1] + (second % md[m_rotation & 1]) * dx;
  int sy = py[m_rotation & 1] + (second / md[m_rotation & 1]) * dy;
  int wx[] = { 5, 3, 2 };
  int wy = 14;
  int cl[][2] = { { DARKGREEN, GREEN}, { OLIVE, YELLOW }, { MAROON, RED } };
  M5.Lcd.fillRect(sx, sy, wx[bit], wy, cl[bit][spot]);
  M5.Lcd.fillRect(sx + wx[bit], sy, dx - wx[bit], wy, BLACK);
}

//------------------------------------------------------------------------------
// Clock Function

int getDays(int y, int m, int d) {
  if (m <= 2) {
    y -= 1;
    m += 12;
  }
  int dy = 365 * (y - 1);
  int dl = (y / 4) - (y / 100) + (y / 400);
  int dm = 306 * (m + 1) / 10 - 122;
  return (59 + dy + dl + dm + d);
}

void setupTime() {
  const char *ssid = "<ssid>";
  const char *password = "<password>";
  const char *ntpServer =  "ntp.nict.jp";

  drawSsid(ssid);
  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED) {
    Serial.print(".");
    M5.Lcd.print(".");
    delay(500);
  }
  drawNtps(ntpServer);
  configTime(9 * 3600, 0, ntpServer);
  //WiFi.disconnect(true);
  //WiFi.mode(WIFI_OFF);
  Serial.print(".\n");
  M5.Lcd.print(".\n");

  struct tm timeInfo;
  if (getLocalTime(&timeInfo)) {
    RTC_DateTypeDef t_date;
    RTC_TimeTypeDef t_time;
    t_time.Seconds = timeInfo.tm_sec;
    t_time.Minutes = timeInfo.tm_min;
    t_time.Hours = timeInfo.tm_hour;
    t_date.Date = timeInfo.tm_mday;
    t_date.Month = timeInfo.tm_mon + 1;
    t_date.Year = timeInfo.tm_year + 1900;
    t_date.WeekDay = timeInfo.tm_wday;
    M5.Rtc.SetData(&t_date);
    M5.Rtc.SetTime(&t_time);
  }
  WiFi.disconnect(true);
  WiFi.mode(WIFI_OFF); 
}

void getTime(struct tm &timeInfo) {
  if (false) {
    getLocalTime(&timeInfo);
  } else {
    RTC_DateTypeDef t_date;
    RTC_TimeTypeDef t_time;
    M5.Rtc.GetData(&t_date);
    M5.Rtc.GetTime(&t_time);
    timeInfo.tm_sec = t_time.Seconds;
    timeInfo.tm_min = t_time.Minutes;
    timeInfo.tm_hour = t_time.Hours;
    timeInfo.tm_mday = t_date.Date;
    timeInfo.tm_mon = t_date.Month - 1;
    timeInfo.tm_year = t_date.Year - 1900;
    timeInfo.tm_wday = t_date.WeekDay;
    timeInfo.tm_yday = getDays(t_date.Year, t_date.Month, t_date.Date)
      - getDays(t_date.Year, 1, 1);
    timeInfo.tm_isdst = 0;
  }
}

//------------------------------------------------------------------------------
// Main Routine

#define PWM_PIN_LED M5_LED  // GPIO10
#define PWM_PIN_GPO 26      // GPIO26
#define PWM_CH_LED  0
#define PWM_CH_GPO  1
#define PWM_BITS    4
#define PWM_FREQ    40000   // 40 kHz, MAX: 80MHz / (1 << BITS)
#define PWM_DMAX (1 << PWM_BITS)

const int m_wait[] = { 800, 500, 200 };

int m_code[60];             // 0: Binary 0, 1: Binary 1, 2: Marker

void setup() {
  // put your setup code here, to run once:

  M5.begin();
  drawTitle();

  ledcSetup(PWM_CH_LED, PWM_FREQ, PWM_BITS);
  ledcAttachPin(PWM_PIN_LED, PWM_CH_LED);
  ledcWrite(PWM_CH_LED, PWM_DMAX);      // LED OFF

  ledcSetup(PWM_CH_GPO, PWM_FREQ, PWM_BITS);
  ledcAttachPin(PWM_PIN_GPO, PWM_CH_GPO);
  ledcWrite(PWM_CH_GPO, 0);             // GPO OFF

  setupTime();
  drawTitle();
}

void loop() {
  // put your main code here, to run repeatedly:

  static int s_tm_sec = 0;
  static int s_force = true;

  M5.update();
  if (M5.BtnA.wasPressed()) {
    drawTitle();
    setupTime();
    drawTitle();
    s_force = true;
  }
  if (M5.BtnB.wasPressed()) {
    m_rotation = (m_rotation + 1) % 4;
    drawTitle();
    s_force = true;
  }

  // Get Time
  struct tm timeInfo;
  getTime(timeInfo);
  if ((timeInfo.tm_sec == s_tm_sec) && !s_force) {
    delay(1);
    return;
  }
  drawTime(timeInfo);

  // Update Time Code
  if ((timeInfo.tm_sec == 0) || s_force) {
    s_force = false;

    // Convert to BCD
    struct tm ti = timeInfo;
    ti.tm_yday++;
    int bcd_year = (ti.tm_year % 10) + (((ti.tm_year / 10) % 10) << 4);
    int bcd_yday = (ti.tm_yday % 10) + (((ti.tm_yday / 10) % 10) << 5)
      + ((ti.tm_yday / 100) << 10);
    int bcd_wday = ti.tm_wday;
    int bcd_hour = (ti.tm_hour % 10) + ((ti.tm_hour / 10) << 5);
    int bcd_minute = (ti.tm_min % 10) + ((ti.tm_min / 10) << 5);

    for (int i = 0; i < 60; i++) {
      if ((i == 0) || (i % 10 == 9)) {
        // Marker or Position Marker
        m_code[i] = 2;
      } else if ((i >= 1) && (i <= 8)) {
        // Minute
        m_code[i] = (bcd_minute >> (8 - i)) & 1;
      } else if ((i >= 12) && (i <= 18)) {
        // Hour
        m_code[i] = (bcd_hour >> (18 - i)) & 1;
      } else if ((i >= 22) && (i <= 33)) {
        // Annual date
        m_code[i] = (bcd_yday >> (33 - i)) & 1;
      } else if (i == 36) {
        // Parity 1
        int parity = bcd_hour;
        parity ^= parity >> 4;
        parity ^= parity >> 2;
        parity ^= parity >> 1;
        m_code[i] = parity & 1;
      } else if (i == 37) {
        // Parity 2
        int parity = bcd_minute;
        parity ^= parity >> 4;
        parity ^= parity >> 2;
        parity ^= parity >> 1;
        m_code[i] = parity & 1;
      } else if ((i >= 41) && (i <= 48)) {
        // Year
        m_code[i] = (bcd_year >> (48 - i)) & 1;
      } else if ((i >= 50) && (i <= 52)) {
        // Day of the Week
        m_code[i] = (bcd_wday >> (52 - i)) & 1;
      }
      drawCode(i, m_code[i], i == timeInfo.tm_sec);
      Serial.printf("%d", m_code[i]);
    }
    Serial.println("");
  } else {
    drawCode(s_tm_sec, m_code[s_tm_sec], false);
    drawCode(timeInfo.tm_sec, m_code[timeInfo.tm_sec], true);
  }

  // Output Time Code
  ledcWrite(PWM_CH_LED, PWM_DMAX / 2);  // LED ON
  ledcWrite(PWM_CH_GPO, PWM_DMAX / 2);  // GPO ON
  delay(m_wait[m_code[timeInfo.tm_sec]]);
  ledcWrite(PWM_CH_LED, PWM_DMAX);      // LED OFF
  ledcWrite(PWM_CH_GPO, 0);             // GPO OFF

  s_tm_sec = timeInfo.tm_sec;
}