いろいろな人が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;
}