Ver Fonte

first commit

Satoshi Yoneda há 2 semanas atrás
commit
d3ade15244

+ 1 - 0
PseudoJJY/.gitignore

@@ -0,0 +1 @@
+config.py

+ 3 - 0
PseudoJJY/.micropico

@@ -0,0 +1,3 @@
+{
+    "info": "This file is just used to identify a project folder."
+}

+ 8 - 0
PseudoJJY/.vscode/extensions.json

@@ -0,0 +1,8 @@
+{
+    "recommendations": [
+        "ms-python.python",
+        "visualstudioexptteam.vscodeintellicode",
+        "ms-python.vscode-pylance",
+        "paulober.pico-w-go"
+    ]
+}

+ 15 - 0
PseudoJJY/.vscode/settings.json

@@ -0,0 +1,15 @@
+{
+    "python.languageServer": "Pylance",
+    "python.analysis.typeCheckingMode": "basic",
+    "python.analysis.diagnosticSeverityOverrides": {
+        "reportMissingModuleSource": "none"
+    },
+    "python.terminal.activateEnvironment": false,
+    "micropico.openOnStart": true,
+    "python.analysis.typeshedPaths": [
+        "~/.micropico-stubs/included"
+    ],
+    "python.analysis.extraPaths": [
+        "~/.micropico-stubs/included"
+    ]
+}

+ 192 - 0
PseudoJJY/pseudo_jjy.py

@@ -0,0 +1,192 @@
+from machine import RTC
+from machine import Pin
+import rp2
+import utime as time
+import ntptime
+
+import network
+from config import WIFI_CONFIG
+
+
+# 40kHzキャリア発振ステートマシン
+@rp2.asm_pio(
+    set_init=(rp2.PIO.OUT_LOW),
+    autopull=False,
+)
+def _40kHz_osc():
+    wrap_target()
+    wait(0, irq, 4)     # IRQ4 = on/off: キャリア発振/停止
+    set(pins, 1)        # High
+    nop()               # 1Clock
+    set(pins, 0)        # Low
+    wrap()
+
+# JJY変調ステートマシン
+@rp2.asm_pio(
+    autopull=False,
+    sideset_init=(rp2.PIO.OUT_LOW),
+    fifo_join=rp2.PIO.JOIN_TX
+)
+def _JJY_Pulse():
+    irq(4).side(0)              # IRQ4 Set / Carrier stop
+    wrap_target()
+    pull(block)         # 1
+    mov(x, osr)         # 2
+    pull(block)         # 3
+    mov(y, osr)         # 4
+    irq(clear,4).side(1)        # IRQ4 clear/ Carrier start
+    label("send_loop")
+    jmp(x_dec, "send_loop")
+    irq(4).side(0)              # IRQ4 Set / Carrier stop
+    label("wait_loop")
+    jmp(y_dec, "wait_loop")
+    wrap()
+
+# JJY変調コード
+JJY_PULSE_CODE = ((800*1000-5,200*1000-1),    # 0 = 800ms carrier + 200ms blank
+                  (500*1000-5,500*1000-1),    # 1 = 500ms carrier + 500ms blank
+                  (200*1000-5,800*1000-1))    # Position Marker = 200ms carrier + 800ms blank
+
+JJY_MARKER = 2
+
+# JJYタイムコードエンコーダー
+def jjy_encode(minute, hour, yearday, year, weekday):
+    code = [0] * 60
+    pa1 = 0     # parity
+    pa2 = 0
+
+    # marker
+    for i in (0, 9, 19, 29, 39, 49, 59):
+        code[i] = JJY_MARKER
+
+    # minute
+    m10, m1 = minute // 10, minute % 10
+    code[1], code[2], code[3] = (m10 >> 2) & 1, (m10 >> 1) & 1, m10 & 1
+    code[5], code[6], code[7], code[8] = (m1 >> 3) & 1, (m1 >> 2) & 1, (m1 >> 1) & 1, m1 & 1
+    pa2 = (code[1]+code[2]+code[3]+code[5]+code[6]+code[7]+code[8]) % 2
+
+    # hour
+    h10, h1 = hour // 10, hour % 10
+    code[12], code[13] = (h10 >> 1) & 1, h10 & 1
+    code[15], code[16], code[17], code[18] = (h1 >> 3) & 1, (h1 >> 2) & 1, (h1 >> 1) & 1, h1 & 1
+    pa1 = (code[12]+code[13]+code[15]+code[16]+code[17]+code[18]) % 2
+
+    # yearday
+    yd100, yd10, yd1 = yearday // 100, (yearday % 100) // 10, yearday % 10
+    code[22], code[23] = (yd100 >> 1) & 1, yd100 & 1
+    code[25], code[26], code[27], code[28] = (yd10 >> 3) & 1, (yd10 >> 2) & 1, (yd10 >> 1) & 1, yd10 & 1
+    code[30], code[31], code[32], code[33] = (yd1 >> 3) & 1, (yd1 >> 2) & 1, (yd1 >> 1) & 1, yd1 & 1
+
+    # parity
+    code[36] = pa1
+    code[37] = pa2
+
+    # year下2桁
+    year %= 100
+    y10, y1 = year // 10, year % 10
+    code[41], code[42], code[43], code[44] = (y10 >> 3) & 1, (y10 >> 2) & 1, (y10 >> 1) & 1, y10 & 1
+    code[45], code[46], code[47], code[48] = (y1 >> 3) & 1, (y1 >> 2) & 1, (y1 >> 1) & 1, y1 & 1
+
+    # weekday
+    jjy_weekday = (weekday + 1) % 7 
+    code[50], code[51], code[52] = (jjy_weekday >> 2) & 1, (jjy_weekday >> 1) & 1, jjy_weekday & 1
+
+    return code
+
+# Wi-Fi接続
+def wifi_connect(ssid, passkey, timeout=20):
+    conn = network.WLAN(network.STA_IF)
+    if conn.isconnected():
+        return conn
+    conn.active(True)
+    conn.connect(ssid, passkey)
+    while not conn.isconnected() and timeout > 0:
+        time.sleep(1)
+        timeout -= 1
+    if conn.isconnected():
+        return conn
+    else:
+        return None
+
+# RTCをNTPで設定
+def setup_rtc_from_ntp():
+    conn = wifi_connect(WIFI_CONFIG["ssid"], WIFI_CONFIG["pass"])
+    if conn is not None:
+        conn.ifconfig()
+        time.sleep(1)
+        rtc = RTC()
+        ntptime.host = 'ntp.nict.jp'    # 日本用
+        now = time.localtime(ntptime.time() + 9 * 60 * 60)
+        rtc.datetime((now[0], now[1], now[2], now[6], now[3], now[4], now[5], 0))
+        print("Set RTC to %d/%d/%d, %d:%d:%d" % (now[0], now[1], now[2], now[3], now[4], now[5]))
+        conn.active(False)
+        conn = None
+    else:
+        print("Cant connect to %s" % WIFI_CONFIG["ssid"])
+
+
+# RTCをあわせる間隔(分)
+EXPIRE = 60 * 6  # 6時間に1回RTCを補正する
+# 疑似JJY出力ポート
+OSC_OUT = 16
+INDICATOR_PIN = 15
+
+if __name__ == "__main__":
+    # 疑似JJY送信機の準備
+    osc = Pin(OSC_OUT, Pin.OUT)
+    osc.value(0)
+    # 160kHz / 4 = 40kHz
+    sm_osc  = rp2.StateMachine(0, _40kHz_osc, freq=160000, set_base=osc)
+    # 1MHz = 1us
+    sm_jjy = rp2.StateMachine(1, _JJY_Pulse, freq=1000000,sideset_base=Pin(INDICATOR_PIN))
+    # 先にsm_jjyを起動してIRQ4を立てておく
+    sm_jjy.active(1)
+    sm_osc.active(1)
+    
+    # 時間計測カウンタ(分)
+    counter = EXPIRE + 1
+    try:
+        while(True):
+            counter += 1
+            # 初回及びEXPIRE分おきにRTCを更新して時間を調整
+            if counter > EXPIRE:
+                counter = 0
+                # FIFOが空になるまで待つ
+                while sm_jjy.tx_fifo() != 0:
+                    time.sleep(1)
+                # 更に待つ
+                time.sleep(2)
+                # これで疑似JJYの送信は停止しているはず
+                # sm.active(0)からのリスタートが何故か機能しないのでこの方法で止めるしかない
+                # NTPでRTCを更新
+                try:
+                    setup_rtc_from_ntp()
+                except Exception:
+                    # なにか問題が起きても無視する
+                    pass
+            # 次の0秒を待つ
+            while True:
+                now = time.localtime()
+                if now[5] == 0:
+                    break
+            
+            jjy_code = jjy_encode(now[4], now[3], now[7], now[0], now[6])
+            if sm_jjy.tx_fifo() != 0:
+                # 送信し終わってない場合、RTCに調整が入ってズレた可能性があるので1分飛ばす
+                time.sleep(1)
+                continue
+            # JJYタイムコード送信
+            for code in jjy_code:
+                # ブロックしても構わない
+                sm_jjy.put(JJY_PULSE_CODE[code][0])
+                sm_jjy.put(JJY_PULSE_CODE[code][1])
+
+    except KeyboardInterrupt:
+        print("keyboard interrupt")
+    finally:
+        sm_osc.active(0)
+        sm_jjy.active(0)
+        # PIO0からすべてのPIOコードを削除して終了する
+        pio0 = rp2.PIO(0)
+        pio0.remove_program()
+        Pin(OSC_OUT, Pin.OUT).low()

+ 0 - 0
README.md


+ 3 - 0
WaveClock/.micropico

@@ -0,0 +1,3 @@
+{
+    "info": "This file is just used to identify a project folder."
+}

+ 8 - 0
WaveClock/.vscode/extensions.json

@@ -0,0 +1,8 @@
+{
+    "recommendations": [
+        "ms-python.python",
+        "visualstudioexptteam.vscodeintellicode",
+        "ms-python.vscode-pylance",
+        "paulober.pico-w-go"
+    ]
+}

+ 15 - 0
WaveClock/.vscode/settings.json

@@ -0,0 +1,15 @@
+{
+    "python.languageServer": "Pylance",
+    "python.analysis.typeCheckingMode": "basic",
+    "python.analysis.diagnosticSeverityOverrides": {
+        "reportMissingModuleSource": "none"
+    },
+    "python.terminal.activateEnvironment": false,
+    "micropico.openOnStart": true,
+    "python.analysis.typeshedPaths": [
+        "~/.micropico-stubs/included"
+    ],
+    "python.analysis.extraPaths": [
+        "~/.micropico-stubs/included"
+    ]
+}

+ 15 - 0
WaveClock/Debug.py

@@ -0,0 +1,15 @@
+class Debug():
+    """
+    デバッグメッセージ用のクラス
+    """
+    DEBUG_ENABLED = True
+
+    def dprint(self, *args):
+        """
+        [クラス名] メッセージ の形式でデバッグ出力
+
+        Args:
+            *args: 出力したいメッセージや変数。
+        """
+        if self.DEBUG_ENABLED:
+            print(f"[{self.__class__.__name__}]", *args)

+ 247 - 0
WaveClock/JJYDecoder.py

@@ -0,0 +1,247 @@
+from machine import Pin
+import rp2
+from micropython import schedule
+import utime as time
+from Debug import Debug
+from TimeSource import TimeSource
+
+# インジケーターLEDのGPIO
+INDICATOR_PIN = 25
+
+@rp2.asm_pio(
+    sideset_init=(rp2.PIO.OUT_LOW),
+)
+def jjy_capture_p():
+    wrap_target()
+    wait(1, pin, 0)          # 立ち上がりを待つ
+    set(x, 0).side(1)        # カウンタのリセット
+    
+    label("loop")
+    # --- 1ms 待機ブロック (10kHz動作時、10クロック消費) ---
+    nop() [7]                # 8クロック
+    nop()                    # 1クロック
+    jmp(x_dec, "next")       # 1クロック (xをカウントダウン)
+    
+    label("next")
+    jmp(pin, "loop")         # まだHighならループ継続(+1クロック)
+    
+    mov(isr, x).side(0)      # カウント結果をISRへ
+    push()                   # Pythonへ送信
+    irq(rel(0))
+    wrap()
+
+@rp2.asm_pio(
+    sideset_init=(rp2.PIO.OUT_LOW),
+)
+def jjy_capture_n():
+    wrap_target()
+    wait(0, pin, 0)          # 立ち下がりを待つ
+    set(x, 0).side(1)        # カウンタのリセット
+    
+    label("loop")
+    # --- 1ms 待機ブロック (10kHz動作時、10クロック消費) ---
+    nop() [7]                # 8クロック
+    jmp(x_dec, "next")       # 1クロック (xをカウントダウン)
+    
+    label("next")
+    jmp(pin, "success")      # Highならループ脱出(+1クロック)
+    jmp("loop")              # Lowなのでループ継続(+1クロック)
+
+    label("success")
+    mov(isr, x).side(0)      # カウント結果をISRへ
+    push()                   # Pythonへ送信
+    irq(rel(0))
+    wrap()
+
+# 定数定義
+JJY_ERROR  = 3      # エラー
+JJY_P_MARK = 2      # ポジションマーカー
+MARK_POSITIONS = (0, 9, 19, 29, 39, 49, 59)    # ポジションマーカーの位置
+
+class JJYDecoder(Debug,TimeSource):
+    """
+    JJYデコーダークラス
+    """
+
+    def __init__(self, callback=None, smid = 0, input_port=15, input_pol=1, indicator_port=INDICATOR_PIN):
+        """
+        コンストラクタ
+
+        Args:
+            callback: コールバック関数 callback((jjy_time, received_ticks_ms)):
+            jjy_time: 受信した日本標準時(UNIXエポックタイム)
+            received_ticks_ms: jjy_timeを受信したtime.ticks_ms()
+            smid: このクラスで使用するステートマシン番号
+            input_port: JJY受信ユニットの出力が接続されているGPIO番号
+            input_pol: JJY受信ユニットの出力極性、1=正論理、0=負論理
+            idicator_port: インジケーターLEDのGPIO番号
+        """
+        # 60秒分のビットを格納するバッファ
+        self.bit_buffer = [0] * 60
+        # 現在の秒位置
+        self.pos = 0
+        # 同期が始まったならTrue
+        self.synced = False
+        # マーカーを識別したらTrue
+        self.last_was_marker = False
+        # 同期に成功した回数
+        self.sync_counter = 0
+        # 受信ステートマシンの割り込みが発生したticks_ms
+        self.ticks_ms = 0
+        # コールバック関数
+        self.callback_funcs = []
+        # ステートマシン番号
+        self.sm_id = smid
+        # JJY受信ステートマシンのロードと起動
+        in_pin = Pin(input_port, Pin.IN, pull=-1)
+        self.sm: rp2.StateMachine
+        if input_pol == 1:  # 正論理
+            self.sm = rp2.StateMachine(self.sm_id, jjy_capture_p, freq=10000, in_base=in_pin, jmp_pin=in_pin,sideset_base=Pin(indicator_port))
+        else:   # 負論理
+            self.sm = rp2.StateMachine(self.sm_id, jjy_capture_n, freq=10000, in_base=in_pin, jmp_pin=in_pin,sideset_base=Pin(indicator_port))
+        self.sm.irq(self._pio_handler,1)
+        self.add_callback(callback)
+        # self.sm.active(1)
+
+    @micropython.native
+    def _pio_handler(self, p):
+        """
+        PIO割り込みハンドラ
+        """
+        schedule(self._jjy_interrupt, (p, time.ticks_ms()))
+
+    @micropython.native
+    def _jjy_interrupt(self, args):
+        """
+        JJY受信割り込みハンドラ
+        """
+        p, ticks = (args)
+        pulse_width = 0x100000000 - int(self.sm.get())
+        self.ticks_ms = ticks - pulse_width    # この信号の推定立ち上がり時間
+ 
+        bit = JJY_ERROR                 # 3 はエラー
+        if pulse_width < 100: pass      # おそらくはノイズ、無視する
+        elif pulse_width < 350: bit = JJY_P_MARK    # ポジションマーカー
+        elif pulse_width < 600: bit = 1
+        elif pulse_width < 950: bit = 0
+        else: pass                      # なんらかの異常
+
+        self.dprint("pulse width=%d, bit=%d" % (pulse_width, bit))
+
+        if bit == JJY_ERROR:            # エラーが起きたらいったんご破算にする
+            self.synced = False
+            self.pos = 0
+        
+        if bit == JJY_P_MARK:           # ポジションマーカー
+            if self.last_was_marker:    # ポジションマーカーが2つ続いたら1フレーム開始
+                self.dprint("--- Frame Sync ---")
+                self.pos = 0
+                self.synced = True      # 同期開始
+            self.last_was_marker = True
+            if self.synced:             # ポジションマーカーの位置をチェック
+                if self.pos not in MARK_POSITIONS:
+                    self.dprint("-- Error: Out of sync --")   # 同期が外れているのでご破算にする
+                    self.synced = False
+                    self.pos = 0
+        else:
+            self.last_was_marker = False
+        
+        if self.synced:
+            self.bit_buffer[self.pos] = bit
+            self.pos += 1
+            # 1フレーム受信完了
+            if self.pos >= 60:
+                self.__decode_frame()
+                self.pos = 0
+                self.synced = False     # 継続せず
+    
+    @micropython.native
+    def __decode_frame(self):
+        """
+        1フレーム60個分のデータを日本標準時にデコードするプライベート関数
+        """
+        # パリティチェック
+        pa2 = (self.bit_buffer[1]+self.bit_buffer[2]+self.bit_buffer[3]+self.bit_buffer[5]+self.bit_buffer[6]+self.bit_buffer[7]+self.bit_buffer[8]) % 2
+        pa1 = (self.bit_buffer[12]+self.bit_buffer[13]+self.bit_buffer[15]+self.bit_buffer[16]+self.bit_buffer[17]+self.bit_buffer[18]) % 2
+        if pa1 != self.bit_buffer[36] or pa2 != self.bit_buffer[37]:    # エラー、デコードを諦める
+            self.dprint("-- Parity Error --")
+            return
+        
+        # minute
+        m10 = self.bit_buffer[1] << 2 | self.bit_buffer[2] << 1 | self.bit_buffer[3]
+        m1  = self.bit_buffer[5] << 3 | self.bit_buffer[6] << 2 | self.bit_buffer[7] << 1 | self.bit_buffer[8]
+        minute = m10 * 10 + m1
+
+        # hour
+        h10 = self.bit_buffer[12] << 1 | self.bit_buffer[13]
+        h1  = self.bit_buffer[15] << 3 | self.bit_buffer[16] << 2 | self.bit_buffer[17] << 1 | self.bit_buffer[18]
+        hour = h10 * 10 + h1
+
+        # yearday
+        yd100 = self.bit_buffer[22] << 1 | self.bit_buffer[23]
+        yd10  = self.bit_buffer[25] << 3 | self.bit_buffer[26] << 2 | self.bit_buffer[27] << 1 | self.bit_buffer[28]
+        yd1   = self.bit_buffer[30] << 3 | self.bit_buffer[31] << 2 | self.bit_buffer[32] << 1 | self.bit_buffer[33]
+        yearday = yd100 * 100 + yd10 * 10 + yd1
+
+        # year
+        y10 = self.bit_buffer[41] << 3 | self.bit_buffer[42] << 2 | self.bit_buffer[43] << 1 | self.bit_buffer[44]
+        y1  = self.bit_buffer[45] << 3 | self.bit_buffer[46] << 2 | self.bit_buffer[47] << 1 | self.bit_buffer[48]
+        year = y10 * 10 + y1 + 2000     # 2000年代は決め打ち
+
+        # month, mday
+        starting_sec = time.mktime((year,1,1,0,0,0,0,0))        # 起点=本年1月1日0時0分
+        target_sec   = starting_sec + (yearday - 1) * 86400     # 経過秒を加算
+        current_dt   = time.localtime(target_sec)
+        mday = current_dt[2]
+        month = current_dt[1]
+
+        # weekday
+        jjy_weekday = self.bit_buffer[50] << 2 | self.bit_buffer[51] << 1 | self.bit_buffer[52]
+        weekday = (jjy_weekday + 6) % 7
+
+        # JJYから得た時刻を通知する
+        jjy_time = time.mktime((year,month,mday,hour,minute,59,weekday,yearday))
+        data = (jjy_time, self.ticks_ms)
+        try:
+            for callback in self.callback_funcs:
+                if callback is not None:
+                    schedule(callback, data)
+        except RuntimeError:
+            self.dprint("micropython.schedule queue full")
+
+    def add_callback(self, callback):
+        """
+        時刻を通知するコールバック関数を登録する
+
+        Args:
+        callback: コールバック関数 callback((jjy_time, received_ticks_ms)):
+            jjy_time: 受信した日本標準時(UNIXエポックタイム)
+            received_ticks_ms: jjy_timeを受信したtime.ticks_ms()
+        """
+        if callback is not None:
+            self.callback_funcs.append(callback)
+    
+    def stop(self):
+        """ステートマシンの実行を一時停止する"""
+        self.sm.active(0)
+    
+    def restart(self):
+        """ステートマシンをリセットして再起動する"""
+        self.sm.restart()
+        self.sm.active(1)
+
+    def release(self):
+        """
+        終了処理。
+        ステートマシンを停止し、割り込みを解除した上でPIOコードをメモリから除去する。
+        """
+        # SM停止
+        self.sm.active(0)
+        # 割り込みハンドラ解除
+        self.sm.irq(None, 1)
+        # PIOコード除去
+        pio_no = 0
+        if self.sm_id > 3:
+            pio_no = 1
+        pio = rp2.PIO(pio_no)
+        pio.remove_program()

+ 136 - 0
WaveClock/JJYReceiver.py

@@ -0,0 +1,136 @@
+from Debug import Debug
+from machine import Pin
+from machine import RTC
+import utime as time
+from JJYDecoder import JJYDecoder
+from micropython import schedule
+from machine import Timer
+from TimeSyncer import TimeSyncer
+
+# 定数定義
+STATE_IDLE  = 0
+STATE_TRY   = 1
+STATE_RETRY = 2
+
+class JJYReceiver(Debug,TimeSyncer):
+
+    def __init__(self, jjydec: JJYDecoder, pon_pin: int, pon_pol: int, band_sel_pin:int, preferred_band:int, retry_minute=10, sync_indicator_pin=17):
+        """
+        コンストラクタ
+        Args:
+            jjydec: JJYDecoderのインスタンス
+            pon_pin: 電源制御(PON)を接続しているGPIO番号
+            pon_pol: 電源制御の極性(電源オンになる値)
+            band_sel_pin: バンド選択を接続しているGPIO番号
+            preferred_band: 優先したいバンド(1か0)
+            retry_minute: JJYデコード試行のリトライ時間(分)
+            sync_indicator_pin: 同期インジケーターLEDのGPIO
+        """
+        # JJYデコーダーにコールバックを設定
+        self.jjy = jjydec
+        self.jjy.add_callback(self.decoded)
+        # 電源制御
+        self.pon = Pin(pon_pin, Pin.OUT)
+        # まず電源を切っておく
+        self.pon.value(pon_pol ^ 1)
+        self.pon_polality = pon_pol
+        # バンド選択
+        self.band_select = Pin(band_sel_pin, Pin.OUT)
+        self.estimated_band = preferred_band
+        self.band_select.value(self.estimated_band)
+
+        self.retry_count = retry_minute           # リトライ分
+
+        self.first_try = True               # 初回試行
+        self.try_counter = 0                # 試行カウンタ
+        self.idle_counter = 0               # アイドリングカウンタ
+
+        # 現在のステート
+        self.state = STATE_TRY
+        # 同期インジケーター
+        self.sync_indicator = Pin(sync_indicator_pin,mode=Pin.OUT)
+        self.sync_indicator.value(0)    # まず消灯
+        # タイマー
+        self.tm = Timer()
+        # タイマーハンドラ起動時呼び出し
+        self._timer_handler(self.tm)
+    
+    def sync_start(self):
+        """強制同期スタート"""
+        if self.state == STATE_IDLE:
+            self.idle_counter = 0   # カウンタリセット
+            self.state = STATE_TRY
+    
+    def sync_stop(self):
+        """同期停止"""
+        if self.state == STATE_TRY or self.state == STATE_RETRY:
+            self.state = STATE_IDLE
+            self.try_counter = 0                    # カウンタリセット
+            self.idle_counter = 0
+            self.jjy.stop()                         # 一旦停止
+            self.pon.value(self.pon_polality ^ 1)   # 一旦電源を切る
+
+    def _tick(self, arg):
+        """
+        タイマー割り込みからスケジュールされる状態遷移・制御ロジック
+        """
+        self.dprint("--- tick %d---" % (self.idle_counter))
+        if self.state == STATE_TRY or self.state == STATE_RETRY:
+            if self.try_counter == 0:     # 試行開始
+                self.dprint("--- Try start ---")
+                # JJY起動
+                self.band_select.value(self.estimated_band)     # バンド設定
+                self.pon.value(self.pon_polality)               # 電源オン
+                self.jjy.restart()
+                # インジケーターLED消灯
+                self.sync_indicator.value(0)
+
+            self.try_counter += 1
+            # 試行回数を超えている
+            if self.try_counter > self.retry_count:
+                self.try_counter = 0                    # カウンタリセット
+                self.jjy.stop()                         # 一旦停止
+                self.pon.value(self.pon_polality ^ 1)   # 一旦電源を切る
+                if self.state == STATE_TRY:
+                    self.state = STATE_RETRY
+                    self.dprint("--- Retry start ---")
+                    self.estimated_band ^= 1            # バンドを変えてみる
+                    time.sleep(5)   # 少し待つ
+                elif self.state == STATE_RETRY:
+                    self.estimated_band ^= 1            # バンドを戻す
+                    if self.first_try:                  # バンドを変えて試行を繰り返す
+                        self.state = STATE_TRY
+                        time.sleep(5)               # 少し待つ
+                    else:
+                        self.state = STATE_IDLE         # 諦める
+                        self.sync_indicator.value(0) # インジケータ消灯
+
+        elif self.state == STATE_IDLE:
+            self.idle_counter += 1
+            if self.idle_counter >= 60:                 # アイドル1時間経過
+                self.idle_counter = 0
+                self.try_counter = 0
+                self.state = STATE_TRY                  # 時間合わせの試行を開始
+
+    def _timer_handler(self, t):
+        """
+        1分周期のハードウェアタイマーハンドラ。
+        MicroPythonのGCストールを避けるため、実際の処理はschedule()に委譲する。
+        """
+        schedule(self._tick, 0)
+        self.tm.init(mode=Timer.ONE_SHOT, period=1000*60, callback=self._timer_handler)
+
+    def decoded(self, args):
+        """
+        JJY信号のデコード成功時に呼ばれるコールバック
+        """
+        self.first_try = False
+        self.state = STATE_IDLE         # 成功したのでアイドルに切り替える
+        self.try_counter = 0            # カウンタリセット
+        self.idle_counter = 0
+        self.dprint("--- Succeeded JJY signal decode ---")
+        # インジケーター点灯
+        self.sync_indicator.value(1)
+        # 電源を切る
+        self.jjy.stop()
+        self.pon.value(self.pon_polality ^ 1)

+ 12 - 0
WaveClock/JJY_CONFIG.py

@@ -0,0 +1,12 @@
+JJY_CONFIG={
+    "signal_out_pin": 15,       # 信号入力GPIO
+    "signal_pol": 1,            # 信号の極性
+    "pon_pin": 13,              # 電源制御(PON)GPIO
+    "pon_pol": 0,               # PON極性(0でオン)
+    "band_select_pin": 14,      # バンド切り替えGPIO
+    "default_band": 1,          # 優先するバンド(1か0)
+    "tm1637_sda_pin": 2,        # TM1637 SDAピン
+    "mode_select_pin": 16,      # モード選択スイッチ
+    "force_sync_pin": 17,       # 強制受信スイッチ
+    "sync_indicator_pin":18,    # 同期インジケーターLED
+}

+ 145 - 0
WaveClock/RTCClockApp.py

@@ -0,0 +1,145 @@
+from Debug import Debug
+from machine import Pin
+from machine import RTC
+import utime as time
+from tm1637 import TM1637
+from TimeSource import TimeSource
+from TimeSyncer import TimeSyncer
+from micropython import schedule
+from machine import Timer
+from machine import idle
+
+MODE_TIME = 0
+MODE_DATE = 1
+MODE_WDAY = 2
+MODE_SEC  = 3
+
+WEEKDAYS=("non","tuE","uEd","tHu","Fri","SAt","Sun")
+
+class RTCClockApp(Debug):
+    """RTCを用いた時計アプリケーションクラス"""
+
+    def __init__(self, display_dev: TM1637, time_source: TimeSource, time_sync: TimeSyncer, mode_select_pin: int, force_sync_pin: int):
+        """
+        コンストラクタ
+        Args:
+            display_dev: TM1637のインスタンス
+            time_source: TimeSourceのインスタンス
+            time_sync: TimeSyncerのインスタンス
+            mode_select_pin: 表示モード切替スイッチが接続されているGPIO番号
+            force_sync_pin: 強制受信スイッチが接続されているGPIO番号
+        """
+        self.disp = display_dev
+        self.timesrc = time_source
+        self.timesync = time_sync
+        # タイマー10ms
+        self.tm = Timer()
+        # 表示更新処理中フラグ(キュー溢れ防止用)
+        self._updating = False
+        # 1つ前の秒
+        self.prev_sec = -1
+        # 初期表示
+        self.disp.show_str("----")
+        # RTCに現在日時が設定されているか?
+        self.setup_rtc = False
+        # TimeSourceにコールバック関数を登録
+        self.timesrc.add_callback(self.adjust_rtc)
+        # 表示モード
+        self.mode = MODE_TIME
+        # 1つ前の表示モード
+        self.prev_mode = self.mode
+        # キースイッチ設定
+        self._last_keydown_times = {} # キー押下時間を格納する辞書
+        self.mode_select = Pin(mode_select_pin, Pin.IN, pull=Pin.PULL_UP)
+        self.force_sync = Pin(force_sync_pin, mode=Pin.IN, pull=Pin.PULL_UP)
+        self.mode_select.irq(self._key_event_handler,trigger=Pin.IRQ_FALLING)
+        self.force_sync.irq(self._key_event_handler,trigger=Pin.IRQ_FALLING)
+    
+    def _key_event_handler(self, p):
+        """スイッチ押下時の割り込みハンドラ"""
+        key_id = id(p)
+        now = time.ticks_ms()
+        last_time = self._last_keydown_times.get(key_id, 0)
+        if time.ticks_diff(now, last_time) > 500:    # チャタリング防止: 500ms以上経過していたら
+            self._last_keydown_times[key_id] = now
+            schedule(self._key_down, p)
+    
+    def _key_down(self, p):
+        """キーダウンイベント処理(非同期コンテキストで実行)"""
+        if p == self.mode_select:       # 表示モード選択
+            self.mode = (self.mode + 1) % 4
+        elif p == self.force_sync:      # 強制受信
+            self.timesync.sync_start()
+
+    def adjust_rtc(self, args):
+        """
+        RTCに現在日時を設定するコールバック関数
+        Args:
+            args: (jjy_time, ticks_ms)
+            jjy_time: RTCに書き込むUNIXエポックタイム
+            ticks_ms: この時間を受信したtime.ticks_ms()
+        """
+        jjy_time, ticks_ms = (args)
+
+        rtc = RTC()
+        elapsed_ms = time.ticks_diff(time.ticks_ms(), ticks_ms)
+        rtc_time = jjy_time + (elapsed_ms // 1000) + 1
+        now = time.localtime(rtc_time)
+        # 次の秒が来るまでスリープしてタイミングを合わせる
+        time.sleep_ms(1000 - (elapsed_ms % 1000))
+        rtc.datetime((now[0], now[1], now[2], now[6], now[3], now[4], now[5], 0))
+        self.dprint("Set RTC to %d/%d/%d, %d:%d:%d" % (now[0], now[1], now[2], now[3], now[4], now[5]))
+
+        self.setup_rtc = True
+    
+    @micropython.native
+    def _timer_handler(self,t):
+        """
+        10msタイマー割り込みハンドラ
+        実際の処理はschedule()に委譲
+        """
+        # 前回の処理が終わっていない、またはキューが一杯の場合はスキップする
+        if not self._updating:
+            try:
+                schedule(self._update_display, None)
+                self._updating = True
+            except RuntimeError:
+                self.dprint("--- schedule() queue full ---")
+
+    @micropython.native
+    def _update_display(self, _):
+        """
+        表示の更新を行う(メインスレッドで実行)
+        """
+        try:
+            now = time.localtime()
+            sec = now[5]
+            if self.prev_sec != sec or self.prev_mode != self.mode:
+                self.prev_sec = sec
+                self.prev_mode = self.mode
+                # 表示更新
+                disp_str = "----"
+                if self.setup_rtc:
+                    if self.mode == MODE_TIME:
+                        if now[5] % 2 == 0:
+                            disp_str = "%2d:%02d" % (now[3],now[4])
+                        else:
+                            disp_str = "%2d%02d" % (now[3],now[4])
+                    elif self.mode == MODE_DATE:
+                        disp_str = "%2d%2d" % (now[1], now[2])
+                    elif self.mode == MODE_WDAY:
+                        disp_str = " %s" % (WEEKDAYS[now[6]])
+                    elif self.mode == MODE_SEC:
+                        disp_str = "%2d%2d" % (now[4], now[5])
+                self.disp.show_str(disp_str)
+        finally:
+            # 処理完了(またはエラー発生)時にフラグを下ろす
+            self._updating = False
+    
+    def run(self):
+        """
+        アプリケーションメインループ
+        """
+        self.tm.init(mode=Timer.PERIODIC, period=10, callback=self._timer_handler)
+        while True:
+            idle()

+ 6 - 0
WaveClock/TimeSource.py

@@ -0,0 +1,6 @@
+
+class TimeSource:
+    """時刻ソースのインターフェースを定義する基底クラス"""
+    def add_callback(self, callback):
+        raise NotImplementedError("Subclasses must implement add_callback()")
+

+ 9 - 0
WaveClock/TimeSyncer.py

@@ -0,0 +1,9 @@
+class TimeSyncer:
+    """時刻ハードウェアインターフェースを定義する基底クラス"""
+    def sync_start(self):
+        """時刻同期開始要求"""
+        raise NotImplementedError("Subclasses must implement sync_start()")
+
+    def sync_stop(self):
+        """時刻同期停止要求"""
+        raise NotImplementedError("Subclasses must implement sync_stop()")

+ 38 - 0
WaveClock/WaveClock.py

@@ -0,0 +1,38 @@
+"""
+WaveClock.py
+
+Pico/Pico2シリーズを利用した電波時計のメインプログラム。
+JJYの受信、デコード、RTCの補正、およびTM1637ディスプレイへの表示を一括して管理する。
+"""
+from Debug import Debug
+from JJYDecoder import JJYDecoder
+from JJYReceiver import JJYReceiver
+from RTCClockApp import RTCClockApp
+from tm1637 import TM1637
+# 設定ファイル
+from JJY_CONFIG import JJY_CONFIG
+
+# 表示器
+disp = TM1637(sda_pin=JJY_CONFIG["tm1637_sda_pin"],contrast=4)
+# JJYデコーダー
+jjy = JJYDecoder(smid=disp._available_ids.pop(),
+                 input_port=JJY_CONFIG["signal_out_pin"],
+                 input_pol=JJY_CONFIG["signal_pol"],
+                 )
+
+# JJY受信ユニット制御クラス
+receiver = JJYReceiver(jjy,
+                       pon_pin=JJY_CONFIG["pon_pin"],
+                       pon_pol=JJY_CONFIG["pon_pol"],
+                       band_sel_pin=JJY_CONFIG["band_select_pin"],
+                       preferred_band=JJY_CONFIG["default_band"],
+                       sync_indicator_pin=JJY_CONFIG["sync_indicator_pin"]
+                       )
+
+# 時計アプリケーションクラス
+app = RTCClockApp(disp, jjy, receiver, 
+                  mode_select_pin=JJY_CONFIG["mode_select_pin"],
+                  force_sync_pin=JJY_CONFIG["force_sync_pin"])
+
+# 時計スタート
+app.run()

+ 8 - 0
WaveClock/boot.py

@@ -0,0 +1,8 @@
+"""
+SMPSをPWMモードに切り替える
+"""
+from machine import Pin
+from utime import sleep
+
+smps_mode = Pin(23, Pin.OUT)
+smps_mode.value(1)

+ 159 - 0
WaveClock/tm1637.py

@@ -0,0 +1,159 @@
+import rp2
+from machine import Pin
+import time
+
+class TM1637:
+    # 割り当て可能なステートマシンID管理
+    _available_ids = list(range(8))
+    
+    # セグメントデータ定義 (Cコードの digit_seg / letter_seg に対応)
+    _DIGIT_SEG = [0x3f, 0x06, 0x5b, 0x4f, 0x66, 0x6d, 0x7d, 0x07, 0x7f, 0x6f]
+    _LETTER_SEG = {
+        'A': 0x77, 'b': 0x7c, 'C': 0x39, 'd': 0x5e, 'E': 0x79, 'F': 0x71,
+        'G': 0x3d, 'H': 0x76, 'i': 0x04, 'J': 0x0e, 'L': 0x38, 'n': 0x54,
+        'o': 0x5c, 'P': 0x73, 'q': 0x67, 'r': 0x50, 'S': 0x6d, 't': 0x78,
+        'u': 0x1c, 'y': 0x6e, '-': 0x40, ' ': 0x00
+    }
+
+    # PIOプログラム定義
+    @rp2.asm_pio(
+        out_init=(rp2.PIO.IN_HIGH),
+        set_init=(rp2.PIO.IN_HIGH),
+        sideset_init=(rp2.PIO.OUT_HIGH),
+        out_shiftdir=rp2.PIO.SHIFT_RIGHT,
+        autopull=False,
+        fifo_join=rp2.PIO.JOIN_TX
+    )
+    def _tm1637_pio():
+        wrap_target()
+        # --- Start Condition ---
+        pull(block)
+        set(pindirs, 0b00)          # SDA/SCL input
+        nop()                   [7]
+        set(pindirs, 0b01)          # SDA output/SCL input
+        set(pins, 0)                # SDA is Low
+        nop()                   [7]
+        set(pindirs, 0b11)             # SDA/SCL output
+        set(x, 7)               .side(0)    # CLK is Low
+        jmp("output")
+        
+        label("byte_loop")
+        set(x, 7)       .side(0)    # CLK is Low
+        pull(block)
+        
+        label("output")
+        nop()                   [3]
+        out(pins, 1)            [4] # LSB output
+        nop()                   .side(1) # CLK is High
+        nop()                   [7]
+        nop()                   .side(0)    # CLK is Low
+        jmp(x_dec, "output")
+        
+        # --- ACK Check ---
+        set(pindirs, 0b10)      [7] # SDA input/SCL output
+        nop()                   .side(1) # SCL = High
+        jmp(pin, "nack")            # SDA High (NACK) なら分岐
+        jmp("ack")
+        
+        label("nack")
+        irq(rel(0))                 # NACK通知
+        
+        label("ack")
+        nop()                   [6]
+        nop()                   .side(0) # CLK is Low (ACK cycle end)
+        set(pindirs, 0b11)      [7] # SDA/SCL Output
+        out(x, 1)                   # 9bit目を読み取って判定
+        jmp(not_x, "byte_loop")     # 0なら次のバイトへ
+        
+        # --- Stop Condition ---
+        set(pins, 0)            [7] # SDA is Low
+        nop()                   .side(1) # SCL is High
+        nop()                   [7]
+        set(pindirs, 0b00)      [7] # SDA/SCL Input (STOP)
+        wrap()
+
+    def __new__(cls, *args, **kwargs):
+        if not cls._available_ids:
+            raise RuntimeError("Maximum TM1637 instances (8) reached")
+        instance = super().__new__(cls)
+        instance.sm_id = cls._available_ids.pop(0)
+        return instance
+
+    def __init__(self, sda_pin, columns=6, contrast=3):
+        # SDAの次のピンをSCLとする仕様を再現 (sda_base_pin + 1)
+        self.sda_pin = Pin(sda_pin, Pin.IN, pull=Pin.PULL_UP)
+        self.scl_pin = Pin(sda_pin + 1, Pin.IN, pull=Pin.PULL_UP)
+        self.columns = columns
+        
+        # PIO初期化 (2MHz)
+        self.sm = rp2.StateMachine(
+            self.sm_id,
+            self._tm1637_pio,
+            freq=2000000,
+            sideset_base=self.scl_pin,
+            set_base=self.sda_pin, # SDA(bit0) & SCL(bit1) をまとめて制御
+            out_base=self.sda_pin,
+            jmp_pin=self.sda_pin
+        )
+        self.sm.active(1)
+        
+        # 初期表示設定
+        self.clear()
+        self.set_contrast(contrast)
+    
+    @micropython.native
+    def _send_cmd(self, data, keep_going=False):
+        # 9ビット目に「終了(1)」か「継続(0)」のフラグを立てる
+        val = (data & 0xFF) | (0 if keep_going else 0x100)
+        self.sm.put(val)
+
+    def set_contrast(self, level):
+        # 0x88 (Display ON) | 0~7
+        cmd = 0x88 | (level & 0x07)
+        self._send_cmd(cmd)
+
+    def _chr_to_seg(self, char):
+        if '0' <= char <= '9':
+            return self._DIGIT_SEG[int(char)]
+        return self._LETTER_SEG.get(char, 0x00)
+
+    @micropython.native
+    def show_str(self, s):
+        # 文字列を解析してセグメントデータのリストを作成
+        segments = []
+        i = 0
+        s_len = len(s)
+        
+        # カラム数または文字列が終わるまでループ
+        while i < s_len and len(segments) < self.columns:
+            char = s[i]
+            base_seg = self._chr_to_seg(char)
+            
+            # 次の文字が存在し、かつドットかコロンの場合
+            if (i + 1) < s_len and s[i + 1] in ('.', ':'):
+                base_seg |= 0x80  # 8ビット目(ドット)を立てる
+                i += 1            # ドット文字分インデックスを進める(スキップ)
+            
+            segments.append(base_seg)
+            i += 1
+
+        # 2. 生成したデータを出力
+        # データセット(自動アドレスインクリメント)
+        self._send_cmd(0x40) # TM1637_AUTOINC
+        # アドレス設定 (0)
+        self._send_cmd(0xC0, keep_going=True) # TM1637_ADDRSET
+        
+        # リストの内容を送信
+        for j, seg in enumerate(segments):
+            # 最後かどうかを判定 (リストの最後 かつ カラム最大)
+            is_last = (j == len(segments) - 1)
+            self._send_cmd(seg, keep_going=not is_last)
+
+    def clear(self):
+        self.show_str(' ' * self.columns)
+
+    def release(self):
+        self.sm.active(0)
+        if self.sm_id not in self.__class__._available_ids:
+            self.__class__._available_ids.append(self.sm_id)
+            self.__class__._available_ids.sort()