GPSタイムサーバーの構築

最終更新日:2023年3月1日

※2023年3月1日 NTPdのコードにバグが存在することが判りました。現在調査中です。
GPSから取得している時間は合っていますが、NTPクライアントからの問合せに対し1日進んだ時間を返してしまいます。
このバグがいつから潜在していたのかもあわせて調査します。
⇒原因は経過日数計算において閏年の扱いに問題がありました。修正しました。
多分最初のスケッチからバグが潜在化していたと思います。

※このプロジェクトではBluePillの内部時計を使用しています。BluePillのVBT端子を使えば、内部時計のバッテリーバックアップも可能です。その場合、充電可能な電池を接続することをお勧めします。 LIRxxxx等です。CRxxxxを接続するとCRxxxx側に電流が流れ、爆発の恐れがあります。逆止回路もご検討ください。

※amazonジャパンで販売されていた『Crosstour GPSアクティブアンテナ空中車ダッシュカム』というGPSモジュールを購入しました。端子は3.5mmステレオプラグです。確認したところ、これは使えます。9600bpsです。流れてくるセンテンスは$GPGGA/$GPRMC、、でした。モジュールによってどのGPSを捕らえるように設定されているかまちまちであることが判りました。
問題はこのモジュールから$GPZDAのセンテンスが流れてこないことです。
タイムサーバとして使うなら$GxRMCを解析して日時情報を取得するのが正解なのかと思います。

※このtimeserverの精度ですが、文字列情報をもとにしてしまっているため最大1秒の遅れが出るかなと思っています。
1PPSの信号が取得出来るGPSに変更して接点による補正をすれば1msecオーダに出来るはずですが。。

※コードの修正をしました。->
主な変更点は


    GPSタイムサーバー

    工場に設置するPC装置はインターネットに接続することなく運用されることが多いです。そんなときに問題になるのが各PCの時計がずれることです。検査結果等の突き合わせが出来ません。
    市場ではTSV-400GPというGPSタイムサーバーが存在することは判っています。他にもたくさん有ると思います。

    そこで、Localのタイムサーバを検討しました。シリアルで接続するGPSレシーバは電波受信可能な箇所に貼り付けておくだけで、BluePill内部時計を校正し、PCからは起動タイミングや正時タイミングでSNTPでタイムサーバにアクセスする事で時刻統一が可能となります。

    GPSのchipはいくつかあるようです。ライブラリが存在するモノを選ぶ必要があります。
    スイスのublox社のChipを使った商品が流通しています。カーナビのGPSなんかはここのGPSを使ったモノが多々ありますし、Arduino用のモジュールも出ています。メーカからライブラリも出ているようなので、これをターゲットにして置きたいです。
    https://github.com/bolderflight/UBLOX
    安価に提供されているGPSモジュールに『UBX-M8030』というchipが搭載されているモノが見受けられます。このchipもこのドライバの対象のようです。
    目的が単に時刻取得で、GPSのごく一部の機能しか利用しないこともあり、安価に越したことはありません。
    ライブラリもとても単純化されており、これが使えるモノであれば何でも良さそうです。
    ※U-BLOX・GPSモジュールを購入した直後ですと内部がどのように設定されているか定かではありません。また、ボーレートも9600bpsのままかもしれません。U-BLOXのサイトから設定ツール『u-center_vxx.xx.exe』をダウンロードし、一旦クリアしてから必要な点のみ設定する必要があります。この設定内容について、ちゃんと把握できていません。https://github.com/bolderflight/UBLOXの説明だと、UBX-NAV-PVTが設定されていればうまくゆくような、そんな気がします。

    タイムサーバのソフトウエアですが、WIZNETのExampleにはNTPクライアントのコードしか提供されていません。しかしフォーラムには先人がトライしているモノが多々紹介されており、不可能ではないと考えました。
    https://forum.arduino.cc/index.php?topic=197870.0
    このリンク先のコードを参照してレタッチしてみました。GPSはU-BLOXのchipが搭載されているモジュールを前提にライブラリを読み替えています。
    ※このスケッチには大きな問題があることが判りました。それは秒以下の値を0に固定しているのです。これによる同期ミスが発生します。これを解決する対策を講じる必要があります

    検証方法自体はWindowsPCのインターネット時刻設定で確認出来ます。実際にビルドしてみました。内部時計を初期化してアクセスしてみたところ、とても反応の早い結果が得られました。期待した通りです。

    NTPサーバとしての情報を追加しておきます。
    NTP、SNTP のフォーマットについてはRFC-1305、RFC-2030を確認してください。

    クライアントから要求されたNTPパケットに対して、サーバ側は情報を追加して返すことになります。
    NTP/SNTPでは1900年1月1日0時(UTP)が基準で、64ビットの符号無し固定小数点で扱います。
    上位32bitが整数部で1900年1月1日0時からの相対時間を秒単位で表現します。
    下位32bitは秒以下の少数位を表しています。

    このプログラムではUTP123の受信パケットの先頭48バイトを解析して必要な情報を付加して返信しています。

    0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7

     

    0

      LI VN MODE 階層 ポーリング間隔 精度

    1

     

    ルート遅延32bitFIX

    2

     

    ルート分散32bitFIX

    3

     

    参照識別子

    4

     

    参照タイムスタンプ 64bitFIX

    5

     

    6

     

    開始タイムスタンプ 64bitFIX

    7

     

    8

     

    受信タイムスタンプ 64bitFIX

    9

     

    10

     

    送信タイムスタンプ 64bitFIX

    11

     

    12

     

    鍵識別子(任意)

    13

     

    メッセージダイジェスト(任意)
    14  
    15  

    16

    SNTP メッセージのセット内容(サーバ・クライアント・モード)

      クライアント リクエスト サーバ 応答 クライアント 処理
    閏秒指示子 0 0  
    バージョン番号 1〜4 4  
    モード 3 4  
    階層 0 1  
    ポーリング間隔 0 6 無視
    精度 0 −9 無視
    ルート遅延 0 0x0080 無視
    ルート分散 0 0x00C0 無視
    参照識別子 0 0x71808300 無視
    参照タイムスタンプ 0 TimeStamp 無視
    開始タイムスタンプ 0 クライアントの送信タイムスタンプコピー クライアントからのリクエスト送信時間がセットされている。
    受信タイムスタンプ 0 TimeStamp サーバの受信時間がセットされている。
    送信タイムスタンプ TimeStamp TimeStamp サーバの送信時間がセットされている。


    ソースを掲載します。追加が必要なライブラリは、

    /* 
     *  2020/2/28 T.Wanibe
     *  起動時のLED&KEYの表示を変更しました。GPSの受信が曖昧な内は『HELLO』表示とし、受信後に時刻表示するように
     *  変更しました。
     *  新たにGPSモジュールを購入しました。Crosstour CR900向けの3.5mmステレオジャックタイプです。
     *  こちらからは$GPのセンテンスが流れてきます。ところが、$GPZDAが流れてこないのです。多分設定で限定した
     *  センテンスしか流さないように流さないようにしているものと考えました。
     *  と云う事は、$GxRMCを解析して日時情報を求めるのが正解なのだと痛感しました。修正します。
     *  LED2が点灯したら内部時計設定完了です。
     *  設定画面がほしいので再度挑戦
     *  ver2のコードで問題を確認したため修正します。タイムゾンーンを単に+9してしまうとAM9に日付が変わります。
     *  これはダメです。正しいLocalタイム対応に修正します。※内部時計はLocalTimeに変更
     *  内部時計はGMTのまま扱い、LED&KEYに表示するときに対策します。そうしないとタイムサーバとしては問題ありです。
     *  このコードはGPSタイムサーバを構築したくて検証中のコードです
     *  NMEA0183のフォーマットを極力単純化して時刻取得するようにした。
     *  まずはNTPサーバを構築して内部時間を返すものを検討
     *  当初ヘッダが$GPGGAに対応するようにしていたが、購入したGPSが$GNGGAのコードしか出さなかったので修正した。
     *  $GNxxxはロシア版GPSであるGLONASSのセンテンスとのようです。
     *  $ GNGGA、185824.00,2605.0247881、N、08014.3097082、W、4,14,0.7、-24.343、M ,,, 0.59,0402 * 36
     *  185824.00           協定世界時(UTC)での時刻。 hhmmss.uu
     *  2605.0247881        緯度。dddmm.mmmm
     *  N                   N = 北緯、South = 南緯
     *  08014.3097082       経度。dddmm.mmmm
     *  W                   E = 東経、W = 西経
     *  4                   位置特定品質
     *  14                  使用衛星数
     *  0.7                 水平精度低下率
     *  -24.343             アンテナの海抜高さ
     *  M                   [m]
     *  0.59
     *  0402                差動基準地点ID
     *  * 36                チェックサム
     *  GNGGAには日付情報が無いためGNRMCを使うべきとあった。
     *  $GNRMC,001903.000,A,3430.61495,N,13316.67709,E,0.00,100.54,120220,,,A,V*00
     *  RMC(時刻、位置、日付)$__RMC,hhmmss.ss,a, ddmm.mm ,a, ddmm.mm ,a,x.x,x.x,mmddyy,a,a,*hh<CR><LF>
     *  $    センテンスの開始
     *  __RMC   アドレスフィールド( 5 文字)、 __ は「 GN 」「 GP 」など
     *  hhmmss.ss       時刻( UTC )  hh 時 mm 分 ss.ss 秒
     *  a       ステータス
     *  A=data valid
     *  V=navigation receiver warming
     *  ddmm.mm 経度  dd 度 mm.mm 分
     *  a       北緯( N )または南緯( S )
     *  ddmm.mm 経度  dd 度 mm.mm 分
     *  a       東経( E )または西経( W )
     *  x.x     対地速度( knot )
     *  x.x     対地コース(進路)(度)
     *  mmddyy  日付  yy 年 mm 月 dd 日
     *  x.x     偏差
     *  a       偏差の方向  W または E
     *  a       モード
     *  A :単独
     *  D :ディファレンシャルモード
     *  E : Estimated (dead reckoning),
     *  M :マニュアル入力
     *  S :シミュレーター入力
     *  N :無効
     *  *hh     チェックサム( $ から * の間)
     *  <CR><LF>        センテンスの終了
     *  センテンス長が固定長ではないようなので一寸パスします。
     *  結局GGA起動時のみ$GNZDAもチェックして日付情報を登録します。
     *  $GNZDA
     *  ZDA(時刻、日付)$__ZDA,x.x,x.x,x.x,*hh<CR><LF>
     *  $    センテンスの開始
     *  __ZDA   アドレスフィールド( 5 文字)、 __ は「 GP 」など
     *  hhmmss.ss       時刻( UTC )  hh 時 mm 分 ss.ss 秒
     *  dd      日
     *  mm      月
     *  yyyy    西暦
     *  hh      時(ローカルタイム)
     *  mm      分(ローカルタイム)
     *  *hh     チェックサム( $ から * の間)
     *  <CR><LF>        センテンスの終了
     * 最大131072バイトのフラッシュメモリのうち、スケッチが57048バイト(418)を使っています。
     * 最大20480バイトのRAMのうち、グローバル変数が4896バイト(218)を使っていて、ローカル変数で15584バイト使うことができます。
     */
    #include <SPI.h>                                        // needed for Arduino versions later than 0018
    #include <Ethernet3.h>
    #include <EthernetUdp3.h>                               // UDP library from: bjoern@cs.stanford.edu 12/30/2008
    //#include <UBLOX.h>
    #include <TM1638.h>
    #include <RTClock.h>
    #include <EEPROM.h>
    #include <avr/pgmspace.h>
    #include <TextFinder.h>                                 //WebSetting
    // Time Server Port
    #define NTP_PORT        123
    #define vers            "NTP GPS STM32miniShield"
    #define debug           true
    //PIN
    #define W550io_Rst      PA8                             //NIC_Reset
    #define SPI1_NSS_PIN    PA4                             //NIC_ChipSelec
    #define UpdateInterval  86400                           //更新間隔
    #define dataPin         PB4                             //LED&KEY
    #define clockPin        PB3                             //LED&KEY
    #define strobePin       PA15                            //LED&KEY
    #define BUILTIN_LED     PC13
    #define LED1            PB8
    #define LED2            PB9
    #define SERIAL_RX_BUFFER_SIZE 256
    #define HttpPort        80                              //HTTP
    #define DELIMITER       ","
    // Time Server MAC address
    byte mac[]              = {0x00,0x08,0xDC,0x54,0x4D,0xD1};      //Wiznet
    byte ip[]               = {192, 168, 0, 197};
    byte dns_server[]       = {192, 168, 0, 1};
    byte gateway[]          = {0, 0, 0, 0};
    byte subnet[]           = {255, 255, 255, 0};
    static const int NTP_PACKET_SIZE = 48;
    byte packetBuffer[NTP_PACKET_SIZE];                     //データを送受信するためのバッファ
    int             year    = 2020;
    byte            month   = 2;
    byte            day     = 11;
    byte            hour    = 13;
    byte            minute  = 10;
    byte            second  = 00;
    byte            hundredths      = 00;
    unsigned long   gDate, gTime, age;
    uint32_t        timestamp, tempval;
    // LM: GPS message parsing
    const char      EOL     = 10;                           // End-of-line
    const int       MSGLEN  = 67;                           // GNRMCメッセージの長さ、66文字の印刷可能文字+ 改行コード
    String          tmpMsg  = "";
    String          gnrmcMsg        = "";
    char            chkHader[]      = "RMC";                //このバージョンから3文字に変更
    // LM: Date/time handling
    String          sUTD    = "";                           // UT Date
    String          sUTC    = "";                           // UT Time
    String          HTTP_req= "";                           // stores the HTTP request
    const int       CENTURY = 2000;
    // LM: Alternative debug
    const boolean   MYDEBUG = true;
    char            STRBUF[50];
    int             timezone        = 9;                    // change to your timezone
    const uint8_t   daysInMonth [] PROGMEM = { 31,28,31,30,31,30,31,31,30,31,30,31 };//const or compiler complains
    const unsigned long seventyYears = 2208988800UL;        // to convert unix time to epoch
    long            gLoopCount = 3540;                      //1時間毎に更新予定 最初は1分後に更新したくて初期値を調整
    //オブジェクトコンストラクタ
    EthernetServer  webServer(HttpPort);
    EthernetClient  client;
    TM1638          LedAndKey(dataPin, clockPin, strobePin);//データピンPB4、クロックピンPB3、およびストローブピンPA15でモジュールを定義する
    RTClock         rtclock (RTCSEL_LSE);                   //RTC初期化
    tm_t            tmm;
    EthernetUDP     Udp;                                    //イーサネットUDPインスタンス
    //UBLOX           gps(Serial1,9600);                      //115200bpsでUBLOX・GPSは通信できるのか要確認
    const int       TD = 9;                                 //TimeZone 日本は+9です。
                                                            // これは、HTMLコードを「流す」ためのバッファーです。
                                                            // ””を含む最長の文字チェーン+1と同じ大きさでなければなりません。
    char    buffer[100];
                                                            // これはすべて切り詰められたHTMLコードです。 エディターでHTMLコードを作成し、バッファに収まるように切り分けます。
                                                            // HTMLをすべての場所で切り取ることができますが、\ "部分ではできません。HTML内でsimple"の代わりに\ "を
                                                            // 使用する必要があります。そうしないと機能しません。
    const byte      ID = 0x92;
    //----------------------------------
    void LANSetup(){
            int idcheck = EEPROM.read(0);
            Serial.print(F("LocalID = 0x"));Serial.println(idcheck,HEX);
            if (idcheck == ID){
                    //idがIDと同じ値の場合、
                    //これは、このスケッチがシールドを設定するために使用されたことを意味します。
                    //EERPOMの値を読み取ってシールドを設定します。
                    for (int i = 0; i < 6; i++){
                            mac[i] = EEPROM.read(i+1);
                    }
                    for (int i = 0; i < 4; i++){
                            ip[i] = EEPROM.read(i+7);
                    }
                    for (int i = 0; i < 4; i++){
                            subnet[i] = EEPROM.read(i+11);
                    }
                    for (int i = 0; i < 4; i++){
                            gateway[i] = EEPROM.read(i+15);
                    }
            }else{
                    //idが一致しない場合、初期値を書き込む事にします。
                    for (int i = 0 ; i < 6; i++){
                            EEPROM.write(i + 1,mac[i]);
                    }
                    for (int i = 0 ; i < 4; i++){
                            EEPROM.write(i + 7, ip[i]);
                    }
                    for (int i = 0 ; i < 4; i++){
                            EEPROM.write(i + 11, subnet[i]);
                    }
                    for (int i = 0 ; i < 4; i++){
                            EEPROM.write(i + 15, gateway[i]);
                    }
                    // IDを既知のビットに設定します。したがって、Arduinoをリセットすると、EEPROM値が使用されます。
                    EEPROM.write(0, ID); 
            }
            Ethernet.begin(mac, ip);
    //      Ethernet.begin(mac);                            //DHCPの場合
    //      Ethernet.begin(mac, ip, subnet);                //SubnetMaskを意識した場合。
    //      Ethernet.begin(mac, ip, subnet, gateway);       //gatewayを意識した場合。
    }
    //--------------------------
    void SetWebPage( EthernetClient client){
            client.print(F("<!DOCTYPE HTML PUBLIC \"\"><html><HEAD><META http-equiv=\"Content-Type\" charset=UTF-8\">"));
            client.print(F("<META http-equiv=\"Content-Style-Type\"><TITLE>"));
            client.print(vers);
            client.print(F(" Setup Page</TITLE></HEAD>"));
            client.print(F("<BODY marginwidth=\"0\" marginheight=\"0\" leftmargin=\"0\" style=\"margin: 0; padding: 0;\"><BLOCKQUOTE><BLOCKQUOTE>"));
            client.print(F("<table bgcolor=\"#17A1A5\" border=\"0\" width=\"100%\" cellpadding=\"1\" style=\"font-family:Verdana;color:#ffffff;font-size:12px;\">"));
            client.print(F("<tr><td>&nbsp;"));
            client.print(vers);
            client.print(F(" Setup Page</td></tr></table><br>"));
            //
            client.print(F("<script>function hex2num (s_hex) {eval(\"var n_num=0X\" + s_hex);return n_num;}</script>"));
            client.print(F("<FORM><input type=\"hidden\" name=\"SBM\" value=\"1\"><table><tr><td>MAC:</td><td>"));
            client.print(F("<input id=\"T1\" type=\"text\" size=\"3\" maxlength=\"2\" name=\"DT1\" value=\""));
            client.print(mac[0],HEX);
            client.print(F("\">.<input id=\"T3\" type=\"text\" size=\"3\" maxlength=\"2\" name=\"DT2\" value=\""));
            client.print(mac[1],HEX);
            client.print(F("\">.<input id=\"T5\" type=\"text\" size=\"3\" maxlength=\"2\" name=\"DT3\" value=\""));
            client.print(mac[2],HEX);
            client.print(F("\">.<input id=\"T7\" type=\"text\" size=\"3\" maxlength=\"2\" name=\"DT4\" value=\""));
            client.print(mac[3],HEX);
            client.print(F("\">.<input id=\"T9\" type=\"text\" size=\"3\" maxlength=\"2\" name=\"DT5\" value=\""));
            client.print(mac[4],HEX);
            client.print(F("\">.<input id=\"T11\" type=\"text\" size=\"3\" maxlength=\"2\" name=\"DT6\" value=\""));
            client.print(mac[5],HEX);
            //
            client.print(F("\"><input id=\"T2\" type=\"hidden\" name=\"DT1\"><input id=\"T4\" type=\"hidden\" name=\"DT2"));
            client.print(F("\"><input id=\"T6\" type=\"hidden\" name=\"DT3\"><input id=\"T8\" type=\"hidden\" name=\"DT4"));
            client.print(F("\"><input id=\"T10\" type=\"hidden\" name=\"DT5\"><input id=\"T12\" type=\"hidden\" name=\"DT6"));
            client.print(F("\"></td></tr><tr><td>IP:</td><td><input type=\"text\" size=\"3\" maxlength=\"3\" name=\"DT7\" value=\""));
            client.print(ip[0],DEC);
            client.print(F("\">.<input type=\"text\" size=\"3\" maxlength=\"3\" name=\"DT8\" value=\""));
            client.print(ip[1],DEC);
            client.print(F("\">.<input type=\"text\" size=\"3\" maxlength=\"3\" name=\"DT9\" value=\""));
            client.print(ip[2],DEC);
            client.print(F("\">.<input type=\"text\" size=\"3\" maxlength=\"3\" name=\"DT10\" value=\""));
            client.print(ip[3],DEC);
            //
            client.print(F("\"></td></tr><tr><td>MASK: </td><td><input type= \"text\" size=\"3\" maxlength=\"3\" name=\"DT11\" value=\""));
            client.print(subnet[0],DEC);
            client.print(F("\">.<input type=\"text\" size=\"3\" maxlength=\"3\" name=\"DT12\" value=\""));
            client.print(subnet[1],DEC);
            client.print(F("\">.<input type=\"text\" size=\"3\" maxlength=\"3\" name=\"DT13\" value=\""));
            client.print(subnet[2],DEC);
            client.print(F("\">.<input type=\"text\" size=\"3\" maxlength=\"3\" name=\"DT14\" value=\""));
            client.print(subnet[3],DEC);
            //
            client.print(F("\"></td></tr><tr><td>GW: </td><td><input type=\"text\" size=\"3\" maxlength=\"3\" name=\"DT15\" value=\""));
            client.print(gateway[0],DEC);
            client.print(F("\">.<input type=\"text\" size=\"3\" maxlength=\"3\" name=\"DT16\" value=\""));
            client.print(gateway[1],DEC);
            client.print(F("\">.<input type=\"text\" size=\"3\" maxlength=\"3\" name=\"DT17\" value=\""));
            client.print(gateway[2],DEC);
            client.print(F("\">.<input type=\"text\" size=\"3\" maxlength=\"3\" name=\"DT18\" value=\""));
            client.print(gateway[3],DEC);
            client.print(F("\"></td></tr><tr><td><br></td></tr><tr><td><input id=\"button1\"type=\"submit\" value=\"SUBMIT\" "));
            //
            client.print(F("Onclick=\"document.getElementById('T2').value "));
            client.print(F("= hex2num(document.getElementById('T1').value);"));
            client.print(F("document.getElementById('T4').value = hex2num(document.getElementById('T3').value);"));
            client.print(F("document.getElementById('T6').value = hex2num(document.getElementById('T5').value);"));
            client.print(F("document.getElementById('T8').value = hex2num(document.getElementById('T7').value);"));
            client.print(F("document.getElementById('T10').value = hex2num(document.getElementById('T9').value);"));
            client.print(F("document.getElementById('T12').value = hex2num(document.getElementById('T11').value);\""));
            //
            client.print(F("></td><td></td></tr></form></table></BLOCKQUOTE></BLOCKQUOTE></BODY></html>"));
    }
    //--------------------------
    void checkWebPage( EthernetClient client)
    {
            Serial.println(F("new webClient"));
            if (client) {
                    TextFinder  finder(client );
                    while (client.connected()) {
                            //digitalWrite(LED, HIGH);
                            if (client.available()) {
                                    //この部分はすべてのテキスト検索を行います。
                                    if( finder.find("GET /") ) {
                                            // 「setup」とい語が見つかった場合は、さらに探してください。
                                            // その単語が見つからない場合は、検索を停止して先に進みます。
                                            // これにより、後でスケッチに独自のWebページを配置できます。
                                            if (finder.findUntil("setup", "\n\r")){
                                                    // 「SBM」という単語が見つかった場合は、さらに探し続けます。
                                                    // その言葉が見つからない場合は、探して停止します。
                                                    // SUBMITボタンが押されていない、何も押されていないことを意味します
                                                    // セットアップページが構築されている場所に移動し、クライアントのブラウザに表示します。
                                                    if (finder.findUntil("SBM", "\n\r")){
                                                            byte SET = finder.getValue();
                                                            // これで、「DT」という文字を探している間に、「DT」の後ろにあるすべての数字を覚えて、
                                                            // 値を一致させて、mac、ip、subnet、およびgatewayに入れる必要があります。
                                                            while(finder.findUntil("DT", "\n\r")){
                                                                    int val = finder.getValue();
                                                                    // 「DT」のvalが1?6の場合、対応する値はMAC値でなければなりません。
                                                                    if(val >= 1 && val <= 6) {
                                                                            mac[val - 1] = finder.getValue();
                                                                    }
                                                                    // 「DT」のvalが7?10の場合、対応する値はIP値でなければなりません。
                                                                    if(val >= 7 && val <= 10) {
                                                                            ip[val - 7] = finder.getValue();
                                                                    }
                                                                    // 「DT」のvalが11?14の場合、対応する値はMASK値でなければなりません。
                                                                    if(val >= 11 && val <= 14) {
                                                                            subnet[val - 11] = finder.getValue();
                                                                    }
                                                                    // 「DT」のvalが15?18の場合、対応する値はGW値でなければなりません。
                                                                    if(val >= 15 && val <= 18) {
                                                                            gateway[val - 15] = finder.getValue();
                                                                    }
                                                            }
                                                            // すべてのデータを取得したので、EEPROMに保存できます
                                                            for (int i = 0 ; i < 6; i++){
                                                                    EEPROM.write(i + 1,mac[i]);
                                                            }
                                                            for (int i = 0 ; i < 4; i++){
                                                                    EEPROM.write(i + 7, ip[i]);
                                                            }
                                                            for (int i = 0 ; i < 4; i++){
                                                                    EEPROM.write(i + 11, subnet[i]);
                                                            }
                                                            for (int i = 0 ; i < 4; i++){
                                                                    EEPROM.write(i + 15, gateway[i]);
                                                            }
                                                            // IDを既知のビットに設定します。したがって、Arduinoをリセットすると、EEPROM値が使用されます。
                                                            EEPROM.write(0, ID); 
                                                            // すべてのデータがEEPROMに書き込まれている場合、arduinoをリセットする必要があります。
                                                            //ハードウェアリセットボタンを使用する必要があります。
                                                    }
                                                    // この時点から、セットアップページの構築を開始し、クライアントのブラウザーに表示できます。
                                                    client.println("HTTP/1.1 200 OK");
                                                    client.println("Content-Type: text/html");
                                                    client.println();
                                                    //
                                                    SetWebPage(client);
                                                    
                                                    break;
                                            }
                                    }
                                    client.println("HTTP/1.1 200 OK");
                                    client.println("Content-Type: text/html");
                                    client.println();
                                    // put your own html from here on
                                    client.print("IT WORKS: go to ");
                                    client.print(ip[0],DEC);
                                    for (int i= 1; i < 4; i++){
                                            client.print(".");
                                            client.print(ip[i],DEC);
                                    }
                                    client.print("/setup");
                                    // put your own html until here 
                                    break;  
                            }
                    }
                    //digitalWrite(LED, LOW);
                    delay(1);
                    client.stop();
            }     
    }
    //---------------------
    void processNTP() {
            //利用可能なデータがある場合は、パケットを読み取ります
            int packetSize  =       Udp.parsePacket();
            if(packetSize){
                    Serial.println(packetSize);
                    Udp.read(packetBuffer,NTP_PACKET_SIZE);
                    IPAddress       Remote  = Udp.remoteIP();
                    int             PortNum = Udp.remotePort();
                    //if (!getGPSdata())  return;
                    packetBuffer[0]         = 0b00100100;           // LI, Version, Mode
                    packetBuffer[1]         = 1 ;                   // stratum
                    packetBuffer[2]         = 6 ;                   // ポーリング最小
                    packetBuffer[3]         = 0xFA;                 // 精度
                    packetBuffer[7]         = 0;                    // ルート遅延
                    packetBuffer[8]         = 0;
                    packetBuffer[9]         = 8;
                    packetBuffer[10]        = 0;
                    packetBuffer[11]        = 0;                    // ルート分散
                    packetBuffer[12]        = 0;
                    packetBuffer[13]        = 0xC;
                    packetBuffer[14]        = 0;
                    //gps.crack_datetime(&year, &month, &day, &hour, &minute, &second, &hundredths, &age);
                    //crack(sUTD, sUTC);
                    timestamp = numberOfSecondsSince1900Epoch(year,month,day,hour,minute,second);
                    //timestamp = numberOfSecondsSince1900Epoch(tmm.year,tmm.month,tmm.day-1,tmm.hour,tmm.minute,tmm.second);
    #if debug
                    Serial.print(timestamp);Serial.print(F(":Time synchronization Request received\n"));
                    //print_date(gps);
    #endif
                    tempval                 = timestamp;
                    packetBuffer[12]        = 71;                  //"G";
                    packetBuffer[13]        = 80;                  //"P";
                    packetBuffer[14]        = 83;                  //"S";
                    packetBuffer[15]        = 0;                   //"0";
                    // reference timestamp
                    packetBuffer[16]        = (tempval >> 24) & 0XFF;
                    tempval                 = timestamp;
                    packetBuffer[17]        = (tempval >> 16) & 0xFF;
                    tempval                 = timestamp;
                    packetBuffer[18]        = (tempval >> 8) & 0xFF;
                    tempval                 = timestamp;
                    packetBuffer[19]        = (tempval) & 0xFF;
                    packetBuffer[20]        = 0;
                    packetBuffer[21]        = 0;
                    packetBuffer[22]        = 0;
                    packetBuffer[23]        = 0;
                    //copy originate timestamp from incoming UDP transmit timestamp
                    packetBuffer[24]        = packetBuffer[40];
                    packetBuffer[25]        = packetBuffer[41];
                    packetBuffer[26]        = packetBuffer[42];
                    packetBuffer[27]        = packetBuffer[43];
                    packetBuffer[28]        = packetBuffer[44];
                    packetBuffer[29]        = packetBuffer[45];
                    packetBuffer[30]        = packetBuffer[46];
                    packetBuffer[31]        = packetBuffer[47];
                    //receive timestamp
                    packetBuffer[32]        = (tempval >> 24) & 0XFF;
                    tempval                 = timestamp;
                    packetBuffer[33]        = (tempval >> 16) & 0xFF;
                    tempval                 = timestamp;
                    packetBuffer[34]        = (tempval >> 8) & 0xFF;
                    tempval                 = timestamp;
                    packetBuffer[35]        = (tempval) & 0xFF;
                    //
                    packetBuffer[36]        = 0;
                    packetBuffer[37]        = 0;
                    packetBuffer[38]        = 0;
                    packetBuffer[39]        = 0;
                    //transmitt timestamp
                    packetBuffer[40]        = (tempval >> 24) & 0XFF;
                    tempval                 = timestamp;
                    packetBuffer[41]        = (tempval >> 16) & 0xFF;
                    tempval                 = timestamp;
                    packetBuffer[42]        = (tempval >> 8) & 0xFF;
                    tempval                 = timestamp;
                    packetBuffer[43]        = (tempval) & 0xFF;
                    packetBuffer[44]        = 0;
                    packetBuffer[45]        = 0;
                    packetBuffer[46]        = 0;
                    packetBuffer[47]        = 0;
                    // Reply to the IP address and port that sent the NTP request
                    Udp.beginPacket(Remote, PortNum);
                    Udp.write(packetBuffer,NTP_PACKET_SIZE);
                    Udp.endPacket();
            }
    }
    //---------------NTP since 1900/01/01
    static unsigned long int numberOfSecondsSince1900Epoch(uint16_t y, uint8_t m, uint8_t d, uint8_t h, uint8_t mm, uint8_t s) {
            int leapAdjustment = 0;
            if (y >= 1970) {
                    y -= 1970;
                    leapAdjustment  = 2;                            // LM: 1970 was NOT a leap year!
            }
            uint16_t days = d;
            for (uint8_t i = 1; i < m; ++i)         days += pgm_read_byte(daysInMonth + i - 1);
    /*
            if (m > 2 && y % 4 == 0)                ++days;
    */
            if (m > 2 && (y + leapAdjustment) % 4 == 0)     ++days; // LM: Weak but okay for the present
            days += 365 * y + (y + 3) / 4 - 1;
            return days*24L*3600L + h*3600L + mm*60L + s + seventyYears;
    }
    //------------------
    bool readSensor(void){
            if (Serial1.available()) {
                    Serial1.readStringUntil('\n');                  //読み捨てて先頭から読めるようにする
                    String nmea = Serial1.readStringUntil('\n');
    #if debug
                    Serial.println(nmea);
    #endif
                    String  nmeames = nmea.substring(3,6);           // $GP***
                    int     count   = 0;
                    while(!nmeames.equals(chkHader)){
                           nmea     = Serial1.readStringUntil('\n');
                           nmeames  = nmea.substring(3,6);          // $GP***
                           digitalWrite(LED1, count++%2);
    #if debug
                           Serial.print(nmea);Serial.print(F("\t"));Serial.println(nmeames);
    #endif
                    }
                    int     nextPos = 0;
                    if ( nmeames.equals(chkHader)){                  // get UTC
                            nextPos         = nmea.indexOf(DELIMITER);//int indexOf(char ch, unsigned int fromIndex) const;
                            String shh      = nmea.substring(nextPos+1,nextPos+3);
                            String smm      = nmea.substring(nextPos+3,nextPos+5);
                            String sss      = nmea.substring(nextPos+5,nextPos+7);
                            nextPos         = nmea.indexOf(DELIMITER,nextPos+1);    //skip UTC
                            nextPos         = nmea.indexOf(DELIMITER,nextPos+1);    //skip Status
                            nextPos         = nmea.indexOf(DELIMITER,nextPos+1);    //skip 緯度
                            nextPos         = nmea.indexOf(DELIMITER,nextPos+1);    //skip 北緯か南緯か
                            nextPos         = nmea.indexOf(DELIMITER,nextPos+1);    //skip 経度
                            nextPos         = nmea.indexOf(DELIMITER,nextPos+1);    //skip 東経か西経か
                            nextPos         = nmea.indexOf(DELIMITER,nextPos+1);    //skip 地表における移動の速度[knot]
                            nextPos         = nmea.indexOf(DELIMITER,nextPos+1);    //skip 地表における移動の真方位[deg]
                            String sdd      = nmea.substring(nextPos+1,nextPos+3);
                            String smn      = nmea.substring(nextPos+3,nextPos+5);
                            String syy      = nmea.substring(nextPos+5,nextPos+7);
                            Serial.println("20" + syy + smn + sdd + shh + smm + sss);
                            tmm.hour        = shh.toInt();
                            hour            = tmm.hour;
                            tmm.minute      = smm.toInt();
                            minute          = tmm.minute;
                            tmm.second      = sss.toInt();
                            second          = tmm.second;
                            tmm.day         = sdd.toInt();
                            day             = tmm.day -1;
                            tmm.month       = smn.toInt();
                            month           = tmm.month;
                            tmm.year        = syy.toInt() + 2000 - 1970;
                            year            = syy.toInt() + 2000;
                            digitalWrite(LED1, LOW);
                            return true;
                    }else{
                            digitalWrite(LED1, HIGH);
                            return false;
                    }
            }
    }
    //------------------GPSにアクセスし、取得に成功したら内部時間を更新
    bool ReadGPS(){
            if(readSensor()) {
                    if(gLoopCount>3600){
                            rtclock.setTime(tmm);                   // 時刻の設定 一時間毎に更新
                            time_t Now = rtclock.getTime() + TD * 3600;
                            rtclock.setTime(Now);
                            gLoopCount = 0;
                            digitalWrite(LED2, HIGH);
                    }
                    hundredths = 0;                                 // LM: GPS time is always acquired on the second (not used)
                    age = 0;                                        //     Not used in this adaptation
            }else{
                    if(gLoopCount%100)      Serial.print(F("."));
                    else{
                            Serial.println(F("."));
                            digitalWrite(LED2, LOW);
                    }
            }
    }
    //------------------
    void setup() {
            Serial.begin(115200);                           //シリアル通信を開ます。
            delay(1000);
            Serial.println(F("Serial interface started"));
            pinMode(BUILTIN_LED,OUTPUT);
            pinMode(LED1,OUTPUT);digitalWrite(LED1, LOW);
            pinMode(LED2,OUTPUT);digitalWrite(LED2, LOW);
            // start Ethernet and UDP:(Ethernet3API)
            Ethernet.setCsPin(SPI1_NSS_PIN);
            Ethernet.setRstPin(W550io_Rst);
            //
            pinMode(W550io_Rst, OUTPUT);
            digitalWrite(W550io_Rst, LOW);
            delay(10);
            digitalWrite(W550io_Rst, HIGH);
            Serial.print(F("NIC_Reset\n"));
            LANSetup();                                             //Ethernet.begin(mac,ip);
            Serial.println(F("Ethernet interface started"));
            Serial.print(F("server is at "));Serial.println(Ethernet.localIP());
            webServer.begin();
            Udp.begin(NTP_PORT);
            //
    #if debug
            Serial.print("Version:");
            Serial.println(vers);
    #endif
            //GPS受信機との通信を開始する
            Serial1.begin(9600);                    //9600bps固定の場合
            ReadGPS();                              //GPS取得に成功したら内部時計を更新しています。
            rtclock.getTime(tmm);
            //sprintf(STRBUF,"%02u%02u%02u",tmm.hour,tmm.minute,tmm.second);
            //LedAndKey.setDisplayToString(STRBUF,0,2);
            LedAndKey.setDisplayToString(" HELLO  ",0,0);
            Serial.println(F("EndSetUp"));
    }
    //-------------------- 
    void loop() {
            //参考にしたコードはNTPdは常に動作させ、クライアントから時刻同期要求が有ったら
            //GPSにアクセスするというモノでした。
            //この修正版は、NTPdは常に動作させ、要求があったらBluePillの内部クロックを
            //返します。
            //また、NTPdの周回待ちにGPSにアクセスし、値を取得したら内部時間を更新します。
            delay(500);
            ReadGPS();
            delay(500);
            client = webServer.available();
            if(client)      checkWebPage(client);
            client.stop();                          // コネクションを閉じる。
            processNTP();
            gLoopCount++;
            digitalWrite(BUILTIN_LED,gLoopCount%2);  //毎秒LEDの点滅処理を実施
            rtclock.getTime(tmm);
            byte keys = LedAndKey.getButtons();
            switch(keys){
                    case 0x01:
                            sprintf(STRBUF,"%04u%02u%02u",tmm.year + 1970,tmm.month,tmm.day);
                            LedAndKey.setDisplayToString(STRBUF,0,0);
                            delay(1000);
                            break;
                    case 0x02:
                            sprintf(STRBUF,"  %02u%02u%02u",tmm.hour,tmm.minute,tmm.second);
                            LedAndKey.setDisplayToString(STRBUF,0,0);
                            break;                        
                   default:
                            if(digitalRead(LED2)){
                                    sprintf(STRBUF,"  %02u%02u%02u",tmm.hour,tmm.minute,tmm.second);
                                    LedAndKey.setDisplayToString(STRBUF,0,0);
                            }
            }
    }


    GPSモジュールメーカがいくつかあります。一寸調査し記録に残しておきます。

      メーカ コメント
    @ U-BLOX スイスに本社があるとのこと。APMフライトコントローラの設計上のGPSに採用されている
    A JRCモビリティ どの程度普及しているのか判らない
    B アルプス 車載用GNSS
    C STマイクロ Teseo-LIV3Fと云う商品があるとのことだが、、、
    D 古野電気 高いので対象外
    E Garmin 結構流通しているようです。
    https://www8.garmin.com/manuals/webhelp/gpsmap_touch/JA-JP/
    GUID-16CE378B-2135-4CEF-B6B3-651C6E810CD3.html
    にNMEA-0183の設定方法が記載されています。
    F SkyTraq 不明
    G MYK 横浜にある創業10年程度の会社
    H 太陽誘電 秋月で提供されているモジュールが太陽誘電製です。GYSFDMAXB

    タイムサーバで使う分にはGPSモジュールがどのメーカのモノであっても NMEA-0183 のフォーマットで出力してくれるモノであればU-BLOXで無くてもそのまま使えるはずです。
    U-BLOX以外のメーカが NMEA-0183 に見合う出力を出すためにどのような設定をすればいいのか、、この点は敷居が高いです。

    そこで考えました。既設定のまま使えるモジュールがいい!

    カーナビでGPSアンテナをオプションにしている商品があります。例えばAKEEYOのドライブレコーダが該当します。
    https://akeeyo.com/
    amazonでも扱っています。
    このカーナビにはオプションでGPSアンテナが用意されていますが、これが利用できます。

    RXの端子がないため設定は出来ません。また、u-centerに接続しても自動でu-bloxG7なんて認識もしません。しかし、通電して暫くすると電波を捕らえて、NMEA-0183データが流れてきます。
    ちなみにVCCは3.3Vで行いましたが大丈夫でした。接続方法ですが、『オーディオジャック+ピッチ変換基板のセット』を介した接続になります。

    実際に取得したデータを調べたところ、『$GPGGA』が有りません。代わりに『$GNGGA』です。また、この『$GNGGA』には日付情報が無いことを確認しました。TimeServerとしては大問題です。そこで『$GNZDA』を使う事にしました。
    また、ライブラリを使うよりも自分で String関数で文字処理した方がリーズナブルだと判断しました。
    GPSが電波を捕らえるまで時間が掛かることが有ります。この点はシリアルモニタを使って判断することになります。


    新たに購入した Crosstour GPSモジュールは 1000円以下で購入でき安価でした。また、pinアサインもakeeyoと同じ、9600bpsで受信できました。ただ、流れてくるセンテンスは『$GPGGA』と『$GPRMC』のみでした。このことから、

      本プロジェクトでは 『$GxRMC』から日時取得する としました。

    このコードを実行するに当たり追加が必要なライブラリは、

※2021/1/25:v3.7.4 バグ修正のためスケッチをUpdateしました。
※2020/4/17:3/19版にタイミングの問題があり修正 同期を掛けたタイミングによってTimeZone分ずれる問題があったのを修正。
※2020/12/23 NTPプロトコルでは時刻を1900年1月1日0時0分0秒からの経過秒数を64bit符号なし固定小数点で表現します。
上位32bitが整数部、下位32bitが小数点以下となります。 この下位32bitをこれまで0で扱っていました。RMCの値も少数位は.000だったのでいいかなと思っていましたが、クライアントからの要求の場合は下位32bitの時差が気になってしまいます。また、偶にエラーになることが判りました。修正しました。

/* 
 *  2021/1/25 T.Wanibe
 *  v3.7.4の修正点:
 *      ・ntpパケット内の経過時刻表記を修正
 *  v3.7.3の修正点:
 *      ・isnan()を使用するコードに変更
 *  v3.7.2の修正点:
 *      ・WDTを組み込みました。
 *      ソースコードなのかライブラリなのか、数日間動いた時点でMainLoopが止まってしまう様です。しかし、時計はちゃんと進んでいるため割込は動いています。
 *      これを打開するにはWDTをMainLoopに組み込むべきだと解釈しました
 *      ・WebServerへのアクセスで不用意なURLの場合にきちんと404を出すように見直した。
 *  v3.7.1の修正点:
 *      setup2画面(ユーティリティ)の充実
 *      少数位の時刻の精度見直し
 *      TimeZone追加
 *  v3.7.0の修正点:
 *      NTPtoolを使って調査したところ、STM32miniGPSNTPサーバは1秒以上時刻ズレが発生することが判った。
 *      何度かNTPtoolを実行して判ったこととして少数位の時刻を無視していることが原因と判ったので修正した。
 *  v3.6.3の修正点:
 *      setupのSubmit実行でマイコンの再起動を実行する様に変更
 *  v3.6.2の修正点:
 *       :点滅の間隔を1秒に変更
 *  v3.6.1の修正点:
 *       /setup頁への移行リンク追加
 *  v3.6の修正点:
 *       @正常運転中アンテナを外したところ時計表示が更新されなかった。バグと思われるので内部時計で更新されるように修正
 *       AアンテナかGPSの受信が出来ないときのLED挙動を変更
 *        LED1:アンテナ受信 未受信点灯 受信消灯
 *        LED2:
 *   BUILTIN_LED:実行中点滅
 *  NTP Time Serverの時刻表示をTM1637経由で4桁7セグLED版に変更しました。
 *  アンテナが外れた時にLED2が消灯するようにした。TimeServerとしては内部時計の値を返します。
 *  起動時のLED&KEYの表示を変更しました。GPSの受信が曖昧な内は『HELLO』表示とし、受信後に時刻表示するように
 *  変更しました。
 *  新たにGPSモジュールを購入しました。Crosstour CR900向けの3.5mmステレオジャックタイプです。
 *  こちらからは$GPのセンテンスが流れてきます。ところが、$GPZDAが流れてこないのです。多分設定で限定した
 *  センテンスしか流さないように流さないようにしているものと考えました。
 *  と云う事は、$GxRMCを解析して日時情報を求めるのが正解なのだと痛感しました。修正します。
 *  LED2が点灯したら内部時計設定完了です。
 *  設定画面がほしいので再度挑戦
 *  ver2のコードで問題を確認したため修正します。タイムゾンーンを単に+9してしまうとAM9に日付が変わります。
 *  これはダメです。正しいLocalタイム対応に修正します。※内部時計はLocalTimeに変更
 *  内部時計はGMTのまま扱い、LED&KEYに表示するときに対策します。そうしないとタイムサーバとしては問題ありです。
 *  このコードはGPSタイムサーバを構築したくて検証中のコードです
 *  NMEA0183のフォーマットを極力単純化して時刻取得するようにした。
 *  まずはNTPサーバを構築して内部時間を返すものを検討
 *  当初ヘッダが$GPGGAに対応するようにしていたが、購入したGPSが$GNGGAのコードしか出さなかったので修正した。
 *  $GNxxxはロシア版GPSであるGLONASSのセンテンスとのようです。
 *  $ GNGGA、185824.00,2605.0247881、N、08014.3097082、W、4,14,0.7、-24.343、M ,,, 0.59,0402 * 36
 *  185824.00           協定世界時(UTC)での時刻。 hhmmss.uu
 *  2605.0247881        緯度。dddmm.mmmm
 *  N                   N = 北緯、South = 南緯
 *  08014.3097082       経度。dddmm.mmmm
 *  W                   E = 東経、W = 西経
 *  4                   位置特定品質
 *  14                  使用衛星数
 *  0.7                 水平精度低下率
 *  -24.343             アンテナの海抜高さ
 *  M                   [m]
 *  0.59
 *  0402                差動基準地点ID
 *  * 36                チェックサム
 *  GNGGAには日付情報が無いためGNRMCを使うべきとあった。
 *  $GNRMC,001903.000,A,3430.61495,N,13316.67709,E,0.00,100.54,120220,,,A,V*00
 *  RMC(時刻、位置、日付)$__RMC,hhmmss.ss,a, ddmm.mm ,a, ddmm.mm ,a,x.x,x.x,mmddyy,a,a,*hh<CR><LF>
 *  $    センテンスの開始
 *  __RMC   アドレスフィールド( 5 文字)、 __ は「 GN 」「 GP 」など
 *  hhmmss.ss       時刻( UTC )  hh 時 mm 分 ss.ss 秒
 *  a       ステータス
 *  A=data valid
 *  V=navigation receiver warming
 *  ddmm.mm 経度  dd 度 mm.mm 分
 *  a       北緯( N )または南緯( S )
 *  ddmm.mm 経度  dd 度 mm.mm 分
 *  a       東経( E )または西経( W )
 *  x.x     対地速度( knot )
 *  x.x     対地コース(進路)(度)
 *  mmddyy  日付  yy 年 mm 月 dd 日
 *  x.x     偏差
 *  a       偏差の方向  W または E
 *  a       モード
 *  A :単独
 *  D :ディファレンシャルモード
 *  E : Estimated (dead reckoning),
 *  M :マニュアル入力
 *  S :シミュレーター入力
 *  N :無効
 *  *hh     チェックサム( $ から * の間)
 *  <CR><LF>        センテンスの終了
 *  センテンス長が固定長ではないようなので一寸パスします。
 *  結局GGA起動時のみ$GNZDAもチェックして日付情報を登録します。
 *  $GNZDA
 *  ZDA(時刻、日付)$__ZDA,x.x,x.x,x.x,*hh<CR><LF>
 *  $    センテンスの開始
 *  __ZDA   アドレスフィールド( 5 文字)、 __ は「 GP 」など
 *  hhmmss.ss       時刻( UTC )  hh 時 mm 分 ss.ss 秒
 *  dd      日
 *  mm      月
 *  yyyy    西暦
 *  hh      時(ローカルタイム)
 *  mm      分(ローカルタイム)
 *  *hh     チェックサム( $ から * の間)
 *  <CR><LF>        センテンスの終了
 * 最大131072バイトのフラッシュメモリのうち、スケッチが79192バイト(60%)を使っています。
 * 最大20480バイトのRAMのうち、グローバル変数が5656バイト(27%)を使っていて、ローカル変数で14824バイト使うことができます。
 */
#include <libmaple/iwdg.h>
#include <SPI.h>                                                // needed for Arduino versions later than 0018
#include <Ethernet3.h>
#include <EthernetUdp3.h>                                       // UDP library from: bjoern@cs.stanford.edu 12/30/2008
//#include <UBLOX.h>
//#include <TM1638.h>
#include <TM1637.h>
#include <RTClock.h>
#include <EEPROM.h>
#include <avr/pgmspace.h>
#include <TextFinder.h>                                         //WebSetting
// Time Server Port
#define ON              1
#define OFF             0
#define NTP_PORT        123
#define vers            "GPS NTP STM32miniShield 3.7.3"
#define debug           true
//PIN
#define W550io_Rst      PA8                                     //NIC_Reset
#define SPI1_NSS_PIN    PA4                                     //NIC_ChipSelec
#define UpdateInterval  86400                                   //更新間隔
//#define dataPin       PB4                                     //LED&KEY
//#define clockPin      PB3                                     //LED&KEY
//#define strobePin     PA15                                    //LED&KEY
#define CLK             PB3                                     //pins definitions for TM1637 and can be changed to other ports    
#define DIO             PB4
#define BUILTIN_LED     PC13
#define LED1            PB8
#define LED2            PB9
#define SERIAL_RX_BUFFER_SIZE 256
#define HttpPort        80                                      //HTTP
#define DELIMITER       ","
#define debug           1
#define iwdg_init_ms(N) iwdg_init(IWDG_PRE_256,((N)/6))         //設定時間をmsecとするためにマクロを用意する。
// Time Server MAC address
byte mac[]              = {0x00,0x08,0xDC,0x54,0x4D,0xD6};      //Wiznet
byte ip[]               = {192, 168, 0, 199};
byte dns_server[]       = {192, 168, 0, 1};
byte gateway[]          = {0, 0, 0, 0};
byte subnet[]           = {255, 255, 255, 0};
static const int NTP_PACKET_SIZE = 48;
byte packetBuffer[NTP_PACKET_SIZE];                             //データを送受信するためのバッファ
int8_t          TimeDisp[]      = {0x00,0x00,0x00,0x00};
unsigned char   ClockPoint      = 1;
unsigned char   Update;
unsigned long   halfsecond      = 0;
int             year            = 2020;
byte            month           = 2;
byte            day             = 11;
byte            hour            = 13;
byte            minute          = 10;
byte            second          = 00;
byte            hundredths      = 00;
unsigned long   gDate, gTime, age;
uint32_t        timestamp, tempval,microVal,fractions;
// LM: GPS message parsing
const char      EOL             = 10;                           // End-of-line
const int       MSGLEN          = 67;                           // GNRMCメッセージの長さ、66文字の印刷可能文字+ 改行コード
String          tmpMsg          = "";
String          gnrmcMsg        = "";
char            chkHader[]      = "RMC";                        //このバージョンから3文字に変更
// LM: Date/time handling
String          sUTD            = "";                           // UT Date
String          sUTC            = "";                           // UT Time
String          HTTP_req        = "";                           // stores the HTTP request
const int       CENTURY         = 2000;
// LM: Alternative debug
const boolean   MYDEBUG         = true;
char            STRBUF[50];
float           timezone        = 9.0;                          //TimeZone 日本は+9です。
const uint8_t   daysInMonth [] PROGMEM = { 31,28,31,30,31,30,31,31,30,31,30,31 };//const or compiler complains
const unsigned long seventyYears = 2208988800UL;                // to convert unix time to epoch
long            gLoopCount = 3;                                 //1時間毎に更新予定 最初は1分後に更新したくて初期値を調整
//オブジェクトコンストラクタ
EthernetServer  webServer(HttpPort);
EthernetClient  client;
//TM1638        LedAndKey(dataPin, clockPin, strobePin);        //データピンPB4、クロックピンPB3、およびストローブピンPA15でモジュールを定義する
TM1637          tm1637(CLK,DIO);
RTClock         rtclock (RTCSEL_LSE);                           //RTC初期化
tm_t            tmm;
EthernetUDP     Udp;                                            //イーサネットUDPインスタンス
char            buffer[100];
                                                                // これはすべて切り詰められたHTMLコードです。 エディターでHTMLコードを作成し、バッファに収まるように切り分けます。
                                                                // HTMLをすべての場所で切り取ることができますが、\ "部分ではできません。HTML内でsimple"の代わりに\ "を
                                                                // 使用する必要があります。そうしないと機能しません。
const byte      ID = 0x96;
bool            GpsEnable       = false;
char            buf1[32];
const char      FVirsion[]      = "3.7.3";
char            MacAddressStr[20];
char            IPAddressStr[20];
char            SubnetMaskStr[20];
char            GatewayStr[20];
char            NTPserverStr[20];
char            DateTimeStr[20];
char            Status          = 'V';
float           unit            = 2.33E-7;
String          nmea;
unsigned long   baseMilis       = millis();
unsigned long   miliTime;
String          setenceStr      = "$GPRMC";
String          StatusStr       = "V = 警告";
String          DateTime2Str    = "2020年12月24日 8時25分59秒.000";
String          DateStr;
String          TimeStr;
String          latitudeStr     = "北緯 35度41.1493分";
String          latitudeNSStr;
String          longitudeStr    = "東経 139度45.3994分";
String          longitudeEWStr;
String          VeloStr         = "00.0";
String          DirStr          = "0.0度";
String          ModeStr         = "A = 自律方式";
String          MagDegStr;
String          MagDevStr;
String          NavStr;
double          latitude;
double          longitude;
float           gDayBuf[24][4];                                         //24時間分4要素データ
typedef struct{
        uint32_t        UTC;
        long            LAT;                                            //10000倍した緯度
        long            LON;                                            //10000倍した経度
        int             DIR;                                            //100倍した値0〜3600
        int             SPD;                                            //速度(ノット単位)を100倍したもの
}RMC;
RMC             gRmc;
//----------------------------------
void LANSetup(){
        int idcheck = EEPROM.read(0);
        Serial.print(F("LocalID = 0x"));Serial.println(idcheck,HEX);
        if (idcheck == ID){
                //idがIDと同じ値の場合、
                //これは、このスケッチがシールドを設定するために使用されたことを意味します。
                //EERPOMの値を読み取ってシールドを設定します。
                for (int i = 0; i < 6; i++){
                        mac[i] = EEPROM.read(i+1);
                }
                for (int i = 0; i < 4; i++){
                        ip[i] = EEPROM.read(i+7);
                }
                for (int i = 0; i < 4; i++){
                        subnet[i] = EEPROM.read(i+11);
                }
                for (int i = 0; i < 4; i++){
                        gateway[i] = EEPROM.read(i+15);
                }
                timezone = float(EEPROM.read(19)) / 10.0;
        }else{
                //idが一致しない場合、初期値を書き込む事にします。
                for (int i = 0 ; i < 6; i++){
                        EEPROM.write(i + 1,mac[i]);
                }
                for (int i = 0 ; i < 4; i++){
                        EEPROM.write(i + 7, ip[i]);
                }
                for (int i = 0 ; i < 4; i++){
                        EEPROM.write(i + 11, subnet[i]);
                }
                for (int i = 0 ; i < 4; i++){
                        EEPROM.write(i + 15, gateway[i]);
                }
                EEPROM.write(19,char(timezone * 10));
                // IDを既知のビットに設定します。したがって、Arduinoをリセットすると、EEPROM値が使用されます。
                EEPROM.write(0, ID); 
        }
        sprintf(MacAddressStr,"%02x.%02x.%02x.%02x.%02x.%02x",mac[0],mac[1],mac[2],mac[3],mac[4],mac[5]);
        sprintf(IPAddressStr,"%d.%d.%d.%d",ip[0],ip[1],ip[2],ip[3]);
        sprintf(SubnetMaskStr,"%d.%d.%d.%d",subnet[0],subnet[1],subnet[2],subnet[3]);
        sprintf(GatewayStr,"%d.%d.%d.%d",gateway[0],gateway[1],gateway[2],gateway[3]);
        Ethernet.begin(mac, ip);
//      Ethernet.begin(mac);                                            //DHCPの場合
//      Ethernet.begin(mac, ip, subnet);                                //SubnetMaskを意識した場合。
//      Ethernet.begin(mac, ip, subnet, gateway);                       //gatewayを意識した場合。
}
//--------------------------
void SetWebPage( EthernetClient client){
        client.println("HTTP/1.1 200 OK");
        client.println("Content-Type: text/html");
        client.println();
        //
        client.print(F("<!DOCTYPE HTML PUBLIC \"\">\n<html>\n<HEAD>\n\t<META http-equiv=\"Content-Type\" charset=Shift_JIS\">\n"));
        client.print(F("\t<META http-equiv=\"Content-Style-Type\">\n\t<TITLE>"));
        client.print(vers);
        client.print(F(" Setup Page</TITLE>\n</HEAD>\n"));
        client.print(F("<BODY marginwidth=\"0\" marginheight=\"0\" leftmargin=\"0\" style=\"margin: 0; padding: 0;\">\n<BLOCKQUOTE><BLOCKQUOTE>\n"));
        client.print(F("<table bgcolor=\"#17A1A5\" border=\"0\" width=\"100%\" cellpadding=\"1\" style=\"font-family:Verdana;color:#ffffff;font-size:12px;\">\n"));
        client.print(F("<tr><td>"));
        client.print(vers);
        client.print(F(" Setup Page</td></tr></table><br>\n"));
        //
        client.print(F("<script>\n\tfunction hex2num (s_hex) {\n\t\teval(\"var n_num=0X\" + s_hex);\n\t\treturn n_num;\n\t}\n</script>\n"));
        client.print(F("<tbody>\n<FORM><input type=\"hidden\" name=\"SBM\" value=\"1\">\n<table>\n<tr><td>MAC:</td><td>"));
        client.print(F("<input id=\"T1\" type=\"text\" size=\"3\" maxlength=\"2\" name=\"DT1\" value=\""));
        client.print(mac[0],HEX);
        client.print(F("\">.<input id=\"T3\" type=\"text\" size=\"3\" maxlength=\"2\" name=\"DT2\" value=\""));
        client.print(mac[1],HEX);
        client.print(F("\">.<input id=\"T5\" type=\"text\" size=\"3\" maxlength=\"2\" name=\"DT3\" value=\""));
        client.print(mac[2],HEX);
        client.print(F("\">.<input id=\"T7\" type=\"text\" size=\"3\" maxlength=\"2\" name=\"DT4\" value=\""));
        client.print(mac[3],HEX);
        client.print(F("\">.<input id=\"T9\" type=\"text\" size=\"3\" maxlength=\"2\" name=\"DT5\" value=\""));
        client.print(mac[4],HEX);
        client.print(F("\">.<input id=\"T11\" type=\"text\" size=\"3\" maxlength=\"2\" name=\"DT6\" value=\""));
        client.print(mac[5],HEX);
        //
        client.print(F("\"><input id=\"T2\" type=\"hidden\" name=\"DT1\"><input id=\"T4\" type=\"hidden\" name=\"DT2"));
        client.print(F("\"><input id=\"T6\" type=\"hidden\" name=\"DT3\"><input id=\"T8\" type=\"hidden\" name=\"DT4"));
        client.print(F("\"><input id=\"T10\" type=\"hidden\" name=\"DT5\"><input id=\"T12\" type=\"hidden\" name=\"DT6"));
        client.print(F("\"></td></tr>\n<tr><td>IP:</td><td><input type=\"text\" size=\"3\" maxlength=\"3\" name=\"DT7\" value=\""));
        client.print(ip[0],DEC);
        client.print(F("\">.<input type=\"text\" size=\"3\" maxlength=\"3\" name=\"DT8\" value=\""));
        client.print(ip[1],DEC);
        client.print(F("\">.<input type=\"text\" size=\"3\" maxlength=\"3\" name=\"DT9\" value=\""));
        client.print(ip[2],DEC);
        client.print(F("\">.<input type=\"text\" size=\"3\" maxlength=\"3\" name=\"DT10\" value=\""));
        client.print(ip[3],DEC);
        //
        client.print(F("\"></td></tr>\n<tr><td>MASK: </td><td><input type= \"text\" size=\"3\" maxlength=\"3\" name=\"DT11\" value=\""));
        client.print(subnet[0],DEC);
        client.print(F("\">.<input type=\"text\" size=\"3\" maxlength=\"3\" name=\"DT12\" value=\""));
        client.print(subnet[1],DEC);
        client.print(F("\">.<input type=\"text\" size=\"3\" maxlength=\"3\" name=\"DT13\" value=\""));
        client.print(subnet[2],DEC);
        client.print(F("\">.<input type=\"text\" size=\"3\" maxlength=\"3\" name=\"DT14\" value=\""));
        client.print(subnet[3],DEC);
        //
        client.print(F("\"></td></tr>\n<tr><td>GW: </td><td><input type=\"text\" size=\"3\" maxlength=\"3\" name=\"DT15\" value=\""));
        client.print(gateway[0],DEC);
        client.print(F("\">.<input type=\"text\" size=\"3\" maxlength=\"3\" name=\"DT16\" value=\""));
        client.print(gateway[1],DEC);
        client.print(F("\">.<input type=\"text\" size=\"3\" maxlength=\"3\" name=\"DT17\" value=\""));
        client.print(gateway[2],DEC);
        client.print(F("\">.<input type=\"text\" size=\"3\" maxlength=\"3\" name=\"DT18\" value=\""));
        client.print(gateway[3],DEC);
        //
        client.print(F("\"></td></tr><tr><td COLSPAN='2'>\n<HR ALIGN=CENTER></td></tr>"));      //1行空けたい
        //
        client.print(F("<tr><td>TimeZone:</td><td><input type=\"text\" size=\"3\" maxlength=\"5\" name=\"DT19\" value=\""));
        client.print(timezone,2);
        client.print(F("\"></td></tr>\n<tr><td><br></td></tr><tr><td COLSPAN='2'><P ALIGN=RIGHT><input id=\"button1\"type=\"submit\" value=\"SUBMIT\" "));
        //
        client.print(F("Onclick=\""));
        client.print(F("document.getElementById('T2').value = hex2num(document.getElementById('T1').value);\n"));
        client.print(F("\t\tdocument.getElementById('T4').value = hex2num(document.getElementById('T3').value);\n"));
        client.print(F("\t\tdocument.getElementById('T6').value = hex2num(document.getElementById('T5').value);\n"));
        client.print(F("\t\tdocument.getElementById('T8').value = hex2num(document.getElementById('T7').value);\n"));
        client.print(F("\t\tdocument.getElementById('T10').value = hex2num(document.getElementById('T9').value);\n"));
        client.print(F("\t\tdocument.getElementById('T12').value = hex2num(document.getElementById('T11').value);\""));
        //
        client.print(F("></td></tr></tbody>\n</table>\n</BLOCKQUOTE></BLOCKQUOTE>\n</form>\n</BODY>\n</html>\n"));
}
//-------------------------- 2021/1/5
void SetUtilPage( EthernetClient client){
        client.println("HTTP/1.1 200 OK");
        client.println("Content-Type: text/html");
        client.println();
        //
        client.print(F("<!DOCTYPE HTML PUBLIC \"\">\n<html>\n<HEAD>\n\t<META http-equiv=\"Content-Type\" charset=\"UTF-8\">\n"));
        client.print(F("\t<META http-equiv=\"Content-Style-Type\">\n\t<TITLE>"));
        client.print(vers);
        client.print(F(" Utility Page</TITLE>\n"));
        client.print(F("\t<script type='application/javascript' SRC='https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.3.0/Chart.bundle.js'></script>\n"));
        client.print(F("\t<script type='application/javascript' SRC='https://cdn.rawgit.com/chartjs/Chart.js/master/samples/utils.js'></script>\n"));
        client.print(F("\t<style>\n\t\tcanvas{\n"));
        client.print(F("\t\t\t-moz-user-select: none;\n"));
        client.print(F("\t\t\t-webkit-user-select: none;\n"));
        client.print(F("\t\t\t-ms-user-select: none;\n\t\t}\n"));
        client.print(F("\t\thtml { font-family: Helvetica; display: inline-block; margin: 0px auto;}\n"));
        client.print(F("\t\tbody{margin-top: 50px;} h1 {color: #444444;margin: 50px auto 30px;}\n"));
        client.print(F("\t\tp {font-size: 24px;color: #444444;margin-bottom: 10px;}\n"));
        client.print(F("\t</style>\n</HEAD>\n"));
        client.print(F("<BODY marginwidth=\"0\" marginheight=\"0\" leftmargin=\"0\" style=\"margin: 0; padding: 0;\" BGCOLOR=\"#ffffff\">\n"));
        client.print(F("<FORM>\n<BLOCKQUOTE><BLOCKQUOTE>\n"));
        client.print(F("<P><table bgcolor=\"#17A1A5\" border=\"0\" width=\"100%\" cellpadding=\"1\" style=\"font-family:Verdana;color:#ffffff;font-size:12px;\" CELLSPACING=\"2\">\n"));
        client.print(F("<tr><td>"));
        client.print(vers);
        client.print(F(" Utility Page</td></tr></table><br>\n"));
        //
        client.print(F("<TABLE WIDTH=\"100%\" BORDER=\"0\" CELLSPACING=\"0\" CELLPADDING=\"0\">"));
        client.print(F("<TR>\n\t<TD WIDTH=\"25%\">FirmwareVirsion:</TD>\n\t<TD WIDTH=\"20%\"></TD>\n\t<TD WIDTH=\"55%\">"));
        client.print(FVirsion);
        client.print(F("</TD>\n</TR>"));
        client.print(F("<TR>\n\t<TD WIDTH=\"25%\">MacAddress:</TD>\n\t<TD WIDTH=\"20%\"></TD>\n\t<TD WIDTH=\"55%\">"));
        client.print(MacAddressStr);
        client.print(F("</TD>\n</TR>"));
        client.print(F("<TR>\n\t<TD WIDTH=\"25%\">IPAddress:</TD>\n\t<TD WIDTH=\"20%\"></TD>\n\t<TD WIDTH=\"55%\">"));
        client.print(IPAddressStr);
        client.print(F("</TD>\n</TR>"));
        client.print(F("<TR>\n\t<TD WIDTH=\"25%\">SubnetMask:</TD>\n\t<TD WIDTH=\"20%\"></TD>\n\t<TD WIDTH=\"55%\">"));
        client.print(SubnetMaskStr);
        client.print(F("</TD>\n</TR>"));
        client.print(F("<TR>\n\t<TD WIDTH=\"25%\" NOWRAP>Data acquisition time(Local):</TD>\n\t<TD WIDTH=\"20%\"></TD>\n\t<TD WIDTH=\"55%\">"));
        client.print(DateTimeStr);
        client.print(F("</TD>\n</TR></TABLE></P>\n"));
        client.print(F("<P><TABLE WIDTH=\"750\" BORDER=\"1\" CELLSPACING=\"0\" CELLPADDING=\"0\" HEIGHT=\"101\">\n"));
        client.print(F("\t<CAPTION ALIGN=\"TOP\"><P ALIGN=LEFT>GPS センテンス</CAPTION>"));
        client.print(F("\t<TR>\n\t\t<TD COLSPAN=\"2\" BGCOLOR=\"#000000\">\n\t\t<BLOCKQUOTE>\n"));
        client.print(F("\t\t\t<PRE><FONT COLOR=\"#ffffff\" SIZE=\"+1\">"));
        client.print(nmea);
        Serial.println(nmea);
        //$GPRMC,021227.000,A,3459.9060,N,13652.6560,E,0.09,344.80,241220,,,A*6D
        //$GNRMC,072815.000,V,         , ,          , ,    ,      ,241220,,,N,V*27
        //RMCは要素数が固定されていないようなので注意が必要
        analsysRMC();
        client.print(F("</FONT></PRE>\n\t\t</BLOCKQUOTE>"));
        client.print(F("</TD>\n\t</TR>\n\t<TR>\n\t\t<TD WIDTH=\"150\" HEIGHT=\"20\">ステータス:</TD>\n\t\t<TD WIDTH=\"600\">"));
        client.print(StatusStr);
        client.print(F("</TD>\n\t</TR>\n\t<TR>\n\t\t<TD WIDTH=\"150\" HEIGHT=\"20\">日時(UTC):</TD>\n\t\t<TD WIDTH=\"600\">"));
        client.print(DateTime2Str);
        client.print(F("</TD>\n\t</TR>\n\t<TR>\n\t\t<TD WIDTH=\"150\" HEIGHT=\"20\">位置情報:緯度</TD>\n\t\t<TD WIDTH=\"600\">"));
        client.print(latitudeNSStr);
        client.print(latitudeStr);
        client.print(F("</TD>\n\t</TR>\n\t<TR>\n\t\t<TD WIDTH=\"150\" HEIGHT=\"20\">位置情報:経度</TD>\n\t\t<TD WIDTH=\"600\">"));
        client.print(longitudeEWStr);
        client.print(longitudeStr);
        client.print(F("</TD>\n\t</TR>\n\t<TR>\n\t\t<TD WIDTH=\"150\" HEIGHT=\"20\">移動の速度:</TD>\n\t\t<TD WIDTH=\"600\">"));
        client.print(VeloStr);
        client.print(F("[knot]</TD>\n\t</TR>\n\t<TR>\n\t\t<TD WIDTH=\"150\" HEIGHT=\"20\">移動の真方位:</TD>\n\t\t<TD WIDTH=\"600\">"));
        client.print(DirStr);
        client.print(F("[度]</TD>\n\t</TR>\n\t<TR>\n\t\t<TD WIDTH=\"150\" HEIGHT=\"20\">モード:</TD>\n\t\t<TD WIDTH=\"600\">"));
        client.print(ModeStr);
        client.print(F("</TD>\n\t</TR>\n</TABLE>\n<CENTER>\n"));
        client.print(F("\t<div style='width:75%;'><canvas id='canvas' width='750' height='400' style='display: block; width: 750px; height: 400px;'></canvas></div>\n"));
        client.print(F("</CENTER></BLOCKQUOTE></BLOCKQUOTE>\n</form>\n"));
        client.print(F("<script>\n"));
        client.print(F("\t\tvar randomScalingFactor = function() {\n\t\t\treturn 18 + Math.random() * 10;\n\t\t};\n"));
        client.print(F("\t\tvar data_00 = ["));
        for(int i = 0; i<24 ;i++){
                client.print(isnan(gDayBuf[i][0]) ? String('\0') : String(gDayBuf[i][0],6));
                client.print(F(","));
        }
        client.print(F("];\n"));
        client.print(F("\t\tvar data_01 = ["));
        for(int i = 0; i<24 ;i++){
                client.print(isnan(gDayBuf[i][1]) ? String('\0') : String(gDayBuf[i][1],6));
                client.print(F(","));
        }
        client.print(F("];\n"));
        client.print(F("\t\tvar data_02 = ["));
        for(int i = 0; i<24 ;i++){
                client.print(isnan(gDayBuf[i][2]) ? String('\0') : String(gDayBuf[i][2],2));
                client.print(F(","));
        }
        client.print(F("];\n"));
        client.print(F("\t\tvar data_03 = ["));
        for(int i = 0; i<24 ;i++){
                client.print(isnan(gDayBuf[i][3]) ? String('\0') : String(gDayBuf[i][3],2));
                client.print(F(","));
        }
        client.print(F("];\n"));        
        client.print(F("\t\tvar config = {"));
        client.print(F("\t\t\ttype: 'line',\n\t\t\tdata: {\n"));
        client.print(F("\t\t\t\tlabels: ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12' , '13', '14', '15', '16', '17', '18', '19', '20', '21', '22', '23'],\n"));
        client.print(F("\t\t\t\tdatasets: [{\n"));
        client.print(F("\t\t\t\t\tlabel: '緯度[度]',\n"));
        client.print(F("\t\t\t\t\tdata: data_00,\n"));
        client.print(F("\t\t\t\t\tborderColor: window.chartColors.red,\n"));
        client.print(F("\t\t\t\t\tbackgroundColor: 'rgba(0, 0, 0, 0)',\n"));
        client.print(F("\t\t\t\t\tfill: false,\n"));
        client.print(F("\t\t\t\t\tcubicInterpolationMode: 'monotone',\n"));
        client.print(F("\t\t\t\t\tyAxisID: 'y1'\n"));
        client.print(F("\t\t\t\t}, {\n"));
        client.print(F("\t\t\t\t\tlabel: '経度[度]',\n"));
        client.print(F("\t\t\t\t\tdata: data_01,\n"));
        client.print(F("\t\t\t\t\tborderColor: window.chartColors.blue,\n"));
        client.print(F("\t\t\t\t\tbackgroundColor: 'rgba(0, 0, 0, 0)',\n"));
        client.print(F("\t\t\t\t\tfill: false,\n"));
        client.print(F("\t\t\t\t\tyAxisID: 'y2'\n"));
        client.print(F("\t\t\t\t}, {\n"));
        client.print(F("\t\t\t\t\tlabel: '速度[knot]',\n"));
        client.print(F("\t\t\t\t\tdata: data_02,\n"));
        client.print(F("\t\t\t\t\tborderColor: window.chartColors.orange,\n"));
        client.print(F("\t\t\t\t\tbackgroundColor: 'rgba(0, 0, 0, 0)',\n"));
        client.print(F("\t\t\t\t\tfill: false,\n"));
        client.print(F("\t\t\t\t\tyAxisID: 'y3'\n"));
        client.print(F("\t\t\t\t}, {\n"));
        client.print(F("\t\t\t\t\tlabel: '真方位[度]',\n"));
        client.print(F("\t\t\t\t\tdata: data_03,\n"));
        client.print(F("\t\t\t\t\tborderColor: window.chartColors.yellow,\n"));
        client.print(F("\t\t\t\t\tbackgroundColor: 'rgba(0, 0, 0, 0)',\n"));
        client.print(F("\t\t\t\t\tfill: false,\n"));
        client.print(F("\t\t\t\t\tyAxisID: 'y4'\n"));
        client.print(F("\t\t\t\t}]\n\t\t\t},\n"));
        client.print(F("\t\t\toptions: {\n\t\t\t\tresponsive: true,\n"));
        client.print(F("\t\t\t\ttitle: {\n\t\t\t\t\tdisplay: true,\n\t\t\t\t\ttext: '今日のGPS位置情報'\n\t\t\t\t},\n"));
        client.print(F("\t\t\t\tlegend: {\n\t\t\t\t\tdisplay: true\n\t\t\t\t},\n"));
        client.print(F("\t\t\t\ttooltips: {\n\t\t\t\t\tenabled: true,\n\t\t\t\t\tmode: 'index'\n\t\t\t\t},\n"));
        client.print(F("\t\t\t\tscales: {\n\t\t\t\t\txAxes: [{\n\t\t\t\t\t\tdisplay: true,\n"));
        client.print(F("\t\t\t\t\t\tscaleLabel: {\n\t\t\t\t\t\t\tdisplay: true,\n\t\t\t\t\t\t\tlabelString: '時刻 [時]'\n\t\t\t\t\t\t}\n\t\t\t\t\t}],\n"));
        client.print(F("\t\t\t\t\tyAxes: [{\n\t\t\t\t\t\tid:'y1',\n\t\t\t\t\t\tdisplay: true,\n"));
        client.print(F("\t\t\t\t\t\tscaleLabel: {\n\t\t\t\t\t\t\tfontColor: window.chartColors.red,\n"));
        client.print(F("\t\t\t\t\t\t\tdisplay: true,\n\t\t\t\t\t\t\tlabelString: '緯度[度]'\n\t\t\t\t\t\t},\n"));
        client.print(F("\t\t\t\t\t\tticks: {\n\t\t\t\t\t\t\tfontColor: window.chartColors.red,\n"));
        client.print(F("\t\t\t\t\t\t\tsuggestedMin: -120,\n\t\t\t\t\t\t\tsuggestedMax:  120\n\t\t\t\t\t\t}\n"));
        client.print(F("\t\t\t\t\t},{\n\t\t\t\t\t\tid:'y2',\n\t\t\t\t\t\tdisplay: true,\n"));
        client.print(F("\t\t\t\t\t\tscaleLabel: {\n\t\t\t\t\t\t\tfontColor: window.chartColors.blue,\n"));
        client.print(F("\t\t\t\t\t\t\tdisplay: true,\n\t\t\t\t\t\t\tlabelString: '経度[度]'\n\t\t\t\t\t\t},\n"));
        client.print(F("\t\t\t\t\t\tticks: {\n\t\t\t\t\t\t\tfontColor: window.chartColors.blue,\n"));
        client.print(F("\t\t\t\t\t\t\tsuggestedMin: -200,\n\t\t\t\t\t\t\tsuggestedMax:  200\n\t\t\t\t\t\t}\n"));
        client.print(F("\t\t\t\t\t},{\n\t\t\t\t\t\tid:'y3',\n\t\t\t\t\t\tposition: 'right',\n\t\t\t\t\t\tdisplay: true,\n"));
        client.print(F("\t\t\t\t\t\tscaleLabel: {\n\t\t\t\t\t\t\tfontColor: window.chartColors.orange,\n"));
        client.print(F("\t\t\t\t\t\t\tdisplay: true,\n\t\t\t\t\t\t\tlabelString: '速度[knot]'\n\t\t\t\t\t\t},\n"));
        client.print(F("\t\t\t\t\t\tticks: {\n\t\t\t\t\t\t\tfontColor: window.chartColors.orange,\n"));
        client.print(F("\t\t\t\t\t\t\tsuggestedMin: 0,\n\t\t\t\t\t\t\tsuggestedMax:  200\n\t\t\t\t\t\t\t}\n"));
        client.print(F("\t\t\t\t\t},{\n\t\t\t\t\t\tid:'y4',\n\t\t\t\t\t\tposition: 'right',\n\t\t\t\t\t\tdisplay: true,\n"));
        client.print(F("\t\t\t\t\t\tscaleLabel: {\n\t\t\t\t\t\t\tfontColor: window.chartColors.yellow,\n"));
        client.print(F("\t\t\t\t\t\t\tdisplay: true,\n\t\t\t\t\t\t\tlabelString: '真方位[度]'\n\t\t\t\t\t\t},\n"));
        client.print(F("\t\t\t\t\t\tticks: {\n\t\t\t\t\t\t\tfontColor: window.chartColors.yellow,\n"));
        client.print(F("\t\t\t\t\t\t\tsuggestedMin: 0,\n\t\t\t\t\t\t\tsuggestedMax:  400\n\t\t\t\t\t\t}\n\t\t\t\t\t}]\n"));    
        client.print(F("\t\t\t\t}\n\t\t\t}\n\t\t};\n")); 
        client.print(F("\t\twindow.onload = function() {\n\t\t\tvar ctx = document.getElementById('canvas').getContext('2d');\n"));
        client.print(F("\t\t\twindow.myLine = new Chart(ctx, config);\n\t\t};\n"));
        client.print(F("</script>\n"));
        client.print(F("</BODY></html>"));
}
//--------------------------
void checkWebPage( EthernetClient client)
{
        Serial.println(F("new webClient"));
        boolean restart = false;
        if (client) {
                TextFinder  finder(client );
                while (client.connected()) {
                        //digitalWrite(LED, HIGH);
                        if (client.available()) {
                                //この部分はすべてのテキスト検索を行います。
                                if( finder.find("GET /") ) {
                                        // 「setup」とい語が見つかった場合は、さらに探してください。
                                        // その単語が見つからない場合は、検索を停止して先に進みます。
                                        // これにより、後でスケッチに独自のWebページを配置できます。
                                        if (finder.findUntil("setup", "\n\r")){
                                                long cord = finder.getValue();
                                                sprintf(buf1,"setupIn=%d",cord);
                                                Serial.println(buf1);
                                                switch(cord){
                                                        case 1:
                                                                // 「SBM」という単語が見つかった場合は、さらに探し続けます。
                                                                // その言葉が見つからない場合は、探して停止します。
                                                                // SUBMITボタンが押されていない、何も押されていないことを意味します
                                                                // セットアップページが構築されている場所に移動し、クライアントのブラウザに表示します。
                                                                if (finder.findUntil("SBM", "\n\r")){
                                                                        byte SET = finder.getValue();
                                                                        // これで、「DT」という文字を探している間に、「DT」の後ろにあるすべての数字を覚えて、
                                                                        // 値を一致させて、mac、ip、subnet、およびgatewayに入れる必要があります。
                                                                        while(finder.findUntil("DT", "\n\r")){
                                                                                int val = finder.getValue();
                                                                                // 「DT」のvalが1?6の場合、対応する値はMAC値でなければなりません。
                                                                                if(val >= 1 && val <= 6) {
                                                                                        mac[val - 1] = finder.getValue();
                                                                                }
                                                                                // 「DT」のvalが7?10の場合、対応する値はIP値でなければなりません。
                                                                                if(val >= 7 && val <= 10) {
                                                                                        ip[val - 7] = finder.getValue();
                                                                                }
                                                                                // 「DT」のvalが11?14の場合、対応する値はMASK値でなければなりません。
                                                                                if(val >= 11 && val <= 14) {
                                                                                        subnet[val - 11] = finder.getValue();
                                                                                }
                                                                                // 「DT」のvalが15?18の場合、対応する値はGW値でなければなりません。
                                                                                if(val >= 15 && val <= 18) {
                                                                                        gateway[val - 15] = finder.getValue();
                                                                                }  
                                                                                //「DT」のvalが19の場合、対応する値はtimezoneです。
                                                                                timezone        = finder.getValue();
                                                                        }
                                                                        // すべてのデータを取得したので、EEPROMに保存できます
                                                                        for (int i = 0 ; i < 6; i++){
                                                                                EEPROM.write(i + 1,mac[i]);
                                                                        }
                                                                        for (int i = 0 ; i < 4; i++){
                                                                                EEPROM.write(i + 7, ip[i]);
                                                                        }
                                                                        for (int i = 0 ; i < 4; i++){
                                                                                EEPROM.write(i + 11, subnet[i]);
                                                                        }
                                                                        for (int i = 0 ; i < 4; i++){
                                                                                EEPROM.write(i + 15, gateway[i]);
                                                                        }
                                                                        EEPROM.write(19, char(timezone * 10));
                                                                        // IDを既知のビットに設定します。したがって、Arduinoをリセットすると、EEPROM値が使用されます。
                                                                        EEPROM.write(0, ID);
                                                                        sprintf(MacAddressStr,"%02x.%02x.%02x.%02x.%02x.%02x",mac[0],mac[1],mac[2],mac[3],mac[4],mac[5]);
                                                                        sprintf(IPAddressStr,"%d.%d.%d.%d",ip[0],ip[1],ip[2],ip[3]);
                                                                        sprintf(SubnetMaskStr,"%d.%d.%d.%d",subnet[0],subnet[1],subnet[2],subnet[3]);
                                                                        sprintf(GatewayStr,"%d.%d.%d.%d",gateway[0],gateway[1],gateway[2],gateway[3]);
                                                                        // すべてのデータがEEPROMに書き込まれている場合、arduinoをリセットする必要があります。
                                                                        //ハードウェアリセットボタンを使用する必要があります。
                                                                        restart = true;
                                                                }else{
                                                                        
                                                                }
                                                                // この時点から、セットアップページの構築を開始し、クライアントのブラウザーに表示できます。
                                                                SetWebPage(client);
                                                                break;
                                                        case 2:
                                                               // 新規にユーティリティ頁を作りました。
                                                                Serial.println(F("utilIn"));
                                                                rtclock.getTime(tmm);
                                                                sprintf(DateTimeStr,"%04u/%02u/%02u %02u:%02u:%02u",tmm.year+1970, tmm.month, tmm.day,tmm.hour, tmm.minute, tmm.second);
                                                                SetUtilPage(client);
                                                                break;
                                                        default:
                                                                PrintResponse404(client );
                                                                break;   
                                                }
                                        }else{
                                              pageIntroduction(client);  
                                        }
                                }
                                break;
                        }
                        PrintResponse404(client );
                }
                delay(1);
                client.stop();
                if(restart)     nvic_sys_reset();
        }     
}
//----------------------------
void pageIntroduction( EthernetClient client){
        client.println("HTTP/1.1 200 OK");
        client.println("Content-Type: text/html");
        client.println();
        // put your own html from here on
        sprintf(buf1,"%d.%d.%d.%d",ip[0],ip[1],ip[2],ip[3]);
        client.print("<HTML>\n<HEAD></HEAD>\n<BODY>\n<P>IT WORKS: go to <B><A HREF=\"https://");
        client.print(buf1);client.print("/setup1\">");
        client.print(buf1);client.print("/setup1</A></B></P>\n");
        client.print("<BLOCKQUOTE>\n\t<BLOCKQUOTE>\n\t\t<P>or</P>\n\t</BLOCKQUOTE>\n</BLOCKQUOTE>\n");
        client.print("<P>IT WORKS: go to <B><A HREF=\"https://");
        client.print(buf1);client.print("/setup2\">");
        client.print(buf1);client.print("/setup2</A></B></P>\n</BODY>\n</HTML>\n");
        // put your own html until here
        client.stop();
}
//--------------------------
void PrintResponse404( EthernetClient client )
{
        client.println(F("HTTP/1.1 404 Not Found"));
        client.println(F("Content-Type: text/html"));
        client.println(F("Connnection: close")); 
        client.println();
        client.stop();
}
//---------------------
void processNTP() {
        //利用可能なデータがある場合は、パケットを読み取ります
        //https://blog.goo.ne.jp/hiro239415/e/c426a545863921a13a9d6a70b7ae4484
        //NTP_Packet (48Byte)
        int packetSize  =       Udp.parsePacket();
        if(packetSize){
                digitalWrite(BUILTIN_LED,HIGH);                 //要求があったときにBuiltInLEDを点灯
                miliTime                = millis() - baseMilis;
                fractions               = uint32_t(float(miliTime % 1000) / unit);
                sprintf(buf1,"■%d\t%d\n",packetSize,fractions);
                Serial.print(buf1);
                Udp.read(packetBuffer,NTP_PACKET_SIZE);
                IPAddress       Remote  = Udp.remoteIP();
                int             PortNum = Udp.remotePort();
                //if (!getGPSdata())  return;
                packetBuffer[0]         = 0b00100100;           // 閏秒警告なしLI, 4:SNTPサーバ(Version), 4:サーバ(Mode)
                packetBuffer[1]         = 1 ;                   // 1:一次基準源(GPS等)(stratum
                packetBuffer[2]         = 5 ;                   // ポーリング64秒デフォルト値 6(64秒)->5(32秒)
                packetBuffer[3]         = 0xF6;                 // 精度 -3(0xFD)->-10(0xF6)に変更 msec
                packetBuffer[4]         = 0;                    // ルート遅延
                packetBuffer[5]         = 0;
                packetBuffer[6]         = 8;
                packetBuffer[7]         = 0;
                packetBuffer[8]        = 0;                    // ルート分散
                packetBuffer[9]        = 0;
                packetBuffer[10]        = 0xC;
                packetBuffer[11]        = 0;
                //gps.crack_datetime(&year, &month, &day, &hour, &minute, &second, &hundredths, &age);
                //crack(sUTD, sUTC);
                //timestamp = numberOfSecondsSince1900Epoch(year,month,day,hour,minute,second);
                rtclock.getTime(tmm);
                timestamp = numberOfSecondsSince1900Epoch(tmm.year+1970,tmm.month,tmm.day,tmm.hour,tmm.minute,tmm.second,timezone);
#if debug
                Serial.print(timestamp);Serial.print(F(":Time synchronization Request received\n"));
                //print_date(gps);
#endif
                tempval                 = timestamp;
                microVal                = fractions;
                packetBuffer[12]        = 71;                  //"G";   ReferenceID
                packetBuffer[13]        = 80;                  //"P";   ReferenceID
                packetBuffer[14]        = 83;                  //"S";   ReferenceID
                packetBuffer[15]        = 0;                   //"0";   ReferenceID
                // reference timestamp
                packetBuffer[16]        = (tempval >> 24) & 0XFF;
                tempval                 = timestamp;
                packetBuffer[17]        = (tempval >> 16) & 0xFF;
                tempval                 = timestamp;
                packetBuffer[18]        = (tempval >> 8) & 0xFF;
                tempval                 = timestamp;
                packetBuffer[19]        = (tempval) & 0xFF;
                packetBuffer[20]        = (microVal >> 24) & 0xFF;
                microVal                = fractions;
                packetBuffer[21]        = (microVal >> 16) & 0xFF;
                microVal                = fractions;
                packetBuffer[22]        = (microVal >> 8) & 0xFF;
                microVal                = fractions;
                packetBuffer[23]        = microVal & 0xFF;
                //Receive Timestamp 受信タイムスタンプ = サーバに要求が届いた時間
                packetBuffer[24]        = packetBuffer[40];
                packetBuffer[25]        = packetBuffer[41];
                packetBuffer[26]        = packetBuffer[42];
                packetBuffer[27]        = packetBuffer[43];
                packetBuffer[28]        = packetBuffer[44];
                packetBuffer[29]        = packetBuffer[45];
                packetBuffer[30]        = packetBuffer[46];
                packetBuffer[31]        = packetBuffer[47];
                //Transmit Timestamp 送信タイムスタンプ = サーバからクライアントに送られた時間
                rtclock.getTime(tmm);
                timestamp = numberOfSecondsSince1900Epoch(tmm.year+1970,tmm.month,tmm.day,tmm.hour,tmm.minute,tmm.second,timezone);
                miliTime                = millis() - baseMilis;
                fractions               = uint32_t(float(miliTime % 1000) / unit);
                tempval                 = timestamp;
                packetBuffer[32]        = (tempval >> 24) & 0XFF;
                tempval                 = timestamp;
                packetBuffer[33]        = (tempval >> 16) & 0xFF;
                tempval                 = timestamp;
                packetBuffer[34]        = (tempval >> 8) & 0xFF;
                tempval                 = timestamp;
                packetBuffer[35]        = (tempval) & 0xFF;
                //
                microVal                = fractions;
                packetBuffer[36]        = (microVal >> 24) & 0xFF;
                microVal                = fractions;
                packetBuffer[37]        = (microVal >> 16) & 0xFF;
                microVal                = fractions;
                packetBuffer[38]        = (microVal >> 8) & 0xFF;
                microVal                = fractions;
                packetBuffer[39]        = microVal & 0xFF;
                //transmit_timestamp_seconds
                rtclock.getTime(tmm);
                timestamp = numberOfSecondsSince1900Epoch(tmm.year+1970,tmm.month,tmm.day,tmm.hour,tmm.minute,tmm.second,timezone);
                miliTime                = millis() - baseMilis;
                fractions               = uint32_t(float(miliTime % 1000) / unit);
                tempval                 = timestamp;
                packetBuffer[40]        = (tempval >> 24) & 0XFF;
                tempval                 = timestamp;
                packetBuffer[41]        = (tempval >> 16) & 0xFF;
                tempval                 = timestamp;
                packetBuffer[42]        = (tempval >> 8) & 0xFF;
                tempval                 = timestamp;
                packetBuffer[43]        = (tempval) & 0xFF;
                //transmit_timestamp_fractions
                microVal                = fractions; 
                packetBuffer[44]        = (microVal >> 24) & 0xFF;
                microVal                = fractions;
                packetBuffer[45]        = (microVal >> 16) & 0xFF;
                microVal                = fractions;
                packetBuffer[46]        = (microVal >> 8) & 0xFF;
                microVal                = fractions;
                packetBuffer[47]        = microVal & 0xFF;
                // Reply to the IP address and port that sent the NTP request
                Udp.beginPacket(Remote, PortNum);
                Udp.write(packetBuffer,NTP_PACKET_SIZE);
                Udp.endPacket();
                digitalWrite(BUILTIN_LED,LOW);                         //処理完了で消灯
                sprintf(buf1,"●%d\t%d\t%d\t%d\n",packetBuffer[20],packetBuffer[21],packetBuffer[22],packetBuffer[23]);
                Serial.print(buf1);
        }
}
//---------------NTP since 1900/01/01 retouch2020/12/25
static unsigned long int numberOfSecondsSince1900Epoch(uint16_t y, uint8_t m, uint8_t d, uint8_t h, uint8_t mm, uint8_t s,float timezone) {
        //2036年問題が存在する。unsigned long intの桁溢れが発生
        //桁溢れが発生したら受信側が対応する必要がある。サーバ側では64bit化するべきだが動かなくなる。
        //timezoneをfloat扱いにした
        int leapAdjustment = 0;
        if (y >= 1970) {
                y -= 1970;
                leapAdjustment  = 2;                            // LM: 1970 was NOT a leap year!
        }
        uint16_t days = d;
        for (uint8_t i = 1; i < m; ++i)         days += pgm_read_byte(daysInMonth + i - 1);
/*
        if (m > 2 && y % 4 == 0)                ++days;
*/
        if (m > 2 && (y + leapAdjustment) % 4 == 0)     ++days; // LM: Weak but okay for the present
        days += 365 * y + (y + 3) / 4 - 1;
        long offsetTime = long((float(h)- timezone) * 3600.0);
        return days*24L*3600L + offsetTime + mm*60L + s + seventyYears;
}
//------------------
bool readSensor(void){
        if (Serial1.available()) {
                Serial1.readStringUntil('\n');                  //読み捨てて先頭から読めるようにする
                nmea = Serial1.readStringUntil('\n');
#if debug
                Serial.println(nmea);
#endif
                //$GNRMC,022757.000,A,3430.62587,N,13316.66175,E,0.63,144.24,231220,,,A,V*02
                String  nmeames = nmea.substring(3,6);           // $GP***
                int     count   = 0;
                while(!nmeames.equals(chkHader)){
                       nmea     = Serial1.readStringUntil('\n');
                       nmeames  = nmea.substring(3,6);          // $GP***
                       digitalWrite(LED2, count++%2);
#if debug
                       Serial.print(nmea);Serial.print(F("\t"));Serial.println(nmeames);
#endif
                        if(count>30){
                                digitalWrite(LED1, HIGH);
                                break;
                        }
                }
                int     nextPos = 0;
                if ( nmeames.equals(chkHader)){                  // get UTC
                        nextPos         = nmea.indexOf(DELIMITER);//int indexOf(char ch, unsigned int fromIndex) const;
                        String shh      = nmea.substring(nextPos+1,nextPos+3);
                        String smm      = nmea.substring(nextPos+3,nextPos+5);
                        String sss      = nmea.substring(nextPos+5,nextPos+7);
                        nextPos         = nmea.indexOf(DELIMITER,nextPos+1);    //skip UTC
                        nextPos         = nmea.indexOf(DELIMITER,nextPos+1);    //skip Status
                        nextPos         = nmea.indexOf(DELIMITER,nextPos+1);    //skip 緯度
                        nextPos         = nmea.indexOf(DELIMITER,nextPos+1);    //skip 北緯か南緯か
                        nextPos         = nmea.indexOf(DELIMITER,nextPos+1);    //skip 経度
                        nextPos         = nmea.indexOf(DELIMITER,nextPos+1);    //skip 東経か西経か
                        nextPos         = nmea.indexOf(DELIMITER,nextPos+1);    //skip 地表における移動の速度[knot]
                        nextPos         = nmea.indexOf(DELIMITER,nextPos+1);    //skip 地表における移動の真方位[deg]
                        String sdd      = nmea.substring(nextPos+1,nextPos+3);
                        String smn      = nmea.substring(nextPos+3,nextPos+5);
                        String syy      = nmea.substring(nextPos+5,nextPos+7);
                        Serial.println("20" + syy + smn + sdd + shh + smm + sss);
                        tmm.year        = syy.toInt() + 2000 - 1970;
                        //Serial.println(tmm.year);
                        if ((tmm.year < 31) || (tmm.year > 100)){               //2001~2079をサポート
                                return false;
                        }else{
                                tmm.hour        = shh.toInt();
                                hour            = tmm.hour;
                                tmm.minute      = smm.toInt();
                                minute          = tmm.minute;
                                tmm.second      = sss.toInt();
                                second          = tmm.second;
                                tmm.day         = sdd.toInt();
                                day             = tmm.day -1;
                                tmm.month       = smn.toInt();
                                month           = tmm.month;
                                year            = syy.toInt() + 2000;
                                digitalWrite(LED1, LOW);
                                digitalWrite(LED2, HIGH);
                                return true;
                        }
                }else{
                        digitalWrite(LED1, HIGH);
                        return false;
                }
        }
}
//------------------GPSにアクセスし、取得に成功したら内部時間を更新
bool ReadGPS(){
        if(readSensor()) {
                if(gLoopCount>60 | !GpsEnable){
                        rtclock.setTime(tmm);                   // 時刻の設定 一時間毎に更新
                        time_t Now = rtclock.getTime() + int(timezone * 3600.0);
                        rtclock.setTime(Now);
                        baseMilis  = millis();                  //20201224追加
                        gLoopCount = 1;
                        digitalWrite(LED2, HIGH);
                        GpsEnable      = true;
                }
                hundredths = 0;                                 // LM: GPS time is always acquired on the second (not used)
                age = 0;                                        //     Not used in this adaptation
        }else{
                delay(100); 
                if(gLoopCount%60)      Serial.print(F("."));
                else{
                        Serial.println(F("."));
                        GpsEnable      = false; 
                }
        }
        if(gLoopCount > 0x7FFFFFF0)     gLoopCount = 1;
        
}
//-----------------     割込CallBack
void TimeUpdate(void){
        rtclock.getTime(tmm);
        if(ClockPoint)  tm1637.point(POINT_ON);
        else            tm1637.point(POINT_OFF); 
        TimeDisp[0]     = tmm.hour      / 10;
        TimeDisp[1]     = tmm.hour      % 10;
        TimeDisp[2]     = tmm.minute    / 10;
        TimeDisp[3]     = tmm.minute    % 10;
        ClockPoint      = (~ClockPoint) & 0x01;
        tm1637.display(TimeDisp);
}
//-----------------
int     getHourTime(void){
        rtclock.getTime(tmm);
        return tmm.hour;
}
//----------------- RMC文字列を解析してクラスタRMCにセットする
void    analsysRMC(void){
        //$GPRMC,021227.000,A,3459.9060,N,13652.6560,E,0.09,344.80,241220,,,A*6D
        //$GNRMC,072815.000,V,         , ,          , ,    ,      ,241220,,,N,V*27
        //RMCは要素数が固定されていないようなので注意が必要
        int     stPosi = 0;
        int     endPosi;
        char    State;
        //センテンス
        endPosi =       nmea.indexOf(',',stPosi);
        if(!(endPosi<0)){
                setenceStr = nmea.substring(stPosi,endPosi);
                Serial.print(F("setenceStr="));Serial.println(setenceStr);
        }
        stPosi  =       endPosi+1;
        //測位時刻(UTC時分秒)        UTC時刻:09時24分03秒307
        endPosi =       nmea.indexOf(',',stPosi);
        Serial.print(F("endPosi="));Serial.println(endPosi);
        if(!(endPosi<0)){
                TimeStr = nmea.substring(stPosi,endPosi);
                Serial.print(F("TimeStr="));Serial.println(TimeStr);
        }
        stPosi  =       endPosi+1;
        //測位状態 A:単独測位中またはDGPS測位中 V:未測位    単独測位中/DGPS測位中
        endPosi =       nmea.indexOf(',',stPosi);
        if(!(endPosi<0)){
                StatusStr = nmea.substring(stPosi,endPosi);
                Serial.print(F("StatusStr="));Serial.println(StatusStr);
        }
        stPosi  =       endPosi+1;
        //緯度。dddmm.mmmm (10進)     緯度 43.07328
        endPosi =       nmea.indexOf(',',stPosi);
        if(!(endPosi<0)){
                latitudeStr = nmea.substring(stPosi,endPosi);
                Serial.print(F("latitudeStr="));Serial.println(latitudeStr);
        }
        stPosi  =       endPosi+1;
        //N:北緯、S:南緯       北緯
        endPosi =       nmea.indexOf(',',stPosi);
        if(!(endPosi<0)){
                latitudeNSStr = nmea.substring(stPosi,endPosi);
                Serial.print(F("latitudeNSStr="));Serial.println(latitudeNSStr);
        }
        stPosi  =       endPosi+1;
        //経度。dddmm.mmmm (10進)     経度 141.270980
        endPosi =       nmea.indexOf(',',stPosi);
        if(!(endPosi<0)){
                longitudeStr = nmea.substring(stPosi,endPosi);
                Serial.print(F("longitudeStr="));Serial.println(longitudeStr);
        }
        stPosi  =       endPosi+1;
        //E:東経、W:西経       東経
        endPosi =       nmea.indexOf(',',stPosi);
        if(!(endPosi<0)){
                longitudeEWStr = nmea.substring(stPosi,endPosi);
                Serial.print(F("longitudeEWStr="));Serial.println(longitudeEWStr);
        }
        stPosi  =       endPosi+1;
        //速度(000.0〜270.0、単位:ノット)  速度 0 Knot
        endPosi =       nmea.indexOf(',',stPosi);
        if(!(endPosi<0)){
                VeloStr = nmea.substring(stPosi,endPosi);
                Serial.print(F("VeloStr="));Serial.println(VeloStr);
        }
        stPosi  =       endPosi+1;
        //真方位による進行方向(000.0〜359.9、単位:度)    240.3度
        endPosi =       nmea.indexOf(',',stPosi);
        if(!(endPosi<0)){
                DirStr = nmea.substring(stPosi,endPosi);
                Serial.print(F("DirStr="));Serial.println(DirStr);
        }
        stPosi  =       endPosi+1;
        //ddmmyy 日付(日、月、年)        日付 2017年7月15日
        endPosi =       nmea.indexOf(',',stPosi);
        if(!(endPosi<0)){
                DateStr = nmea.substring(stPosi,endPosi);
                Serial.print(F("DateStr="));Serial.println(DateStr);
        }
        stPosi  =       endPosi+1;
        //磁気偏差(000.0〜180.0、単位:度)  0(なし)
        endPosi =       nmea.indexOf(',',stPosi);
        if(!(endPosi<0)){
                MagDegStr = nmea.substring(stPosi,endPosi);
                Serial.print(F("MagDegStr="));Serial.println(MagDegStr);
        }
        stPosi  =       endPosi+1;
        //磁気偏差 E:磁気偏差が東より W:磁気偏差が西より      なし
        endPosi =       nmea.indexOf(',',stPosi);
        if(!(endPosi<0)){
                MagDevStr = nmea.substring(stPosi,endPosi);
                Serial.print(F("MagDevStr="));Serial.println(MagDevStr);
        }
        stPosi  =       endPosi+1;
        //測位mode N:未測位 A:単独測位 D:DGPS測位
        endPosi =       nmea.indexOf(',',stPosi);
        if(!(endPosi<0)){
                ModeStr = nmea.substring(stPosi,endPosi);
                Serial.print(F("ModeStr="));Serial.println(ModeStr);
        }else{
                ModeStr = nmea.substring(stPosi,stPosi+1);
                Serial.print(F("ModeStr="));Serial.println(ModeStr);
        }
        stPosi  =       endPosi+1;
        //ナビゲーション状態 S:Safe C:Caution U:Unsafe V:無効(RAIM 機能 OFF)
        endPosi =       nmea.indexOf(',',stPosi);
        if(!(endPosi<0)){
                NavStr = nmea.substring(stPosi,endPosi);
                Serial.print(F("NavStr="));Serial.println(NavStr);
        }else{
                NavStr = nmea.substring(stPosi,stPosi+1);
                Serial.print(F("NavStr="));Serial.println(NavStr);
        }
        Status = StatusStr.charAt(0);
        switch(Status){
                case 'A':
                        StatusStr = "A = 有効";
                        digitalWrite(LED1, LOW);
                        break;
                case 'V':
                        StatusStr = "V = 警告";
                        digitalWrite(LED1, HIGH);
                        break;
                default:
                        StatusStr = "";
                        digitalWrite(LED1, HIGH);
                        break;        
        }
        DateTime2Str = "20" + DateStr.substring(4,6) + "年" + DateStr.substring(2,4) + "月" + DateStr.substring(0,2) + "日 " + TimeStr.substring(0,2) + "時" + TimeStr.substring(2,4) + "分" + TimeStr.substring(4,6) + "秒" + TimeStr.substring(7,10);
        State = latitudeNSStr.charAt(0);
        switch(State){
                case 'N':
                        latitudeNSStr = "北緯 ";
                        break;
                case 'S':
                        latitudeNSStr = "南緯 ";
                        break;
                default:
                        latitudeNSStr = "";
                        break;        
        }
        stPosi  = 0;
        endPosi = latitudeStr.indexOf('.',stPosi);
        if(endPosi>0){
                String latitudeStr1     = latitudeStr.substring(stPosi,endPosi-2);
                String latitudeStr2     = latitudeStr.substring(endPosi-2,endPosi);
                String latitudeStr3     = "." + latitudeStr.substring(endPosi+1);
                latitude        = latitudeStr1.toFloat() + (latitudeStr2.toFloat()/60.0) + (latitudeStr3.toFloat()/60.0);
                if(State == 'S')        latitude = -latitude;
                latitudeStr     = latitudeStr1 + "度" + latitudeStr2 + "分" + latitudeStr3;
        }
        State = longitudeEWStr.charAt(0);
        switch(State){
                case 'E':
                        longitudeEWStr = "東経 ";
                        break;
                case 'W':
                        longitudeEWStr = "西経 ";
                        break;
                default:
                        longitudeEWStr = "";
                        break;        
        }
        stPosi  = 0;
        endPosi = longitudeStr.indexOf('.',stPosi);
        if(endPosi>0){
                String longitudeStr1     = longitudeStr.substring(stPosi,endPosi-2);
                String longitudeStr2     = longitudeStr.substring(endPosi-2,endPosi);
                String longitudeStr3     = "." + longitudeStr.substring(endPosi+1);
                longitude        = longitudeStr1.toFloat() + (longitudeStr2.toFloat()/60.0) + (longitudeStr3.toFloat()/60.0);
                if(State == 'W')        longitude = -longitude;
                longitudeStr     = longitudeStr1 + "度" + longitudeStr2 + "分" + longitudeStr3;
        }        
        State = ModeStr.charAt(0);
        switch(State){
                case 'N':
                        ModeStr = "N = データなし";
                        break;
                case 'A':
                        ModeStr = "A = Autonomous(自律方式)";
                        break;
                case 'D':
                        ModeStr = "D = Differential(干渉測位方式)";
                        break;
                case 'E':
                        ModeStr = "E = Estimated(推定)";
                        break;
                case 'M':
                        ModeStr = "M = マニュアル入力";
                        break;
                case 'S':
                        ModeStr = "S = シミュレーター入力";
                        break;
                default:
                        ModeStr = State;
                        break;        
        }
        gRmc.UTC        =       timestamp;
        gRmc.LAT        =       long(latitude         * 10000.0);
        gRmc.LON        =       long(longitude        * 10000.0);
        gRmc.SPD        =       int(VeloStr.toFloat() * 100.0);
        gRmc.DIR        =       int(DirStr.toFloat()  * 100.0);
}
//------------------
void setup() {
        Serial.begin(115200);                                           //シリアル通信を開ます。
        delay(1000);
        Serial.println(F("Serial interface started"));
        pinMode(BUILTIN_LED,OUTPUT);
        pinMode(LED1,OUTPUT);digitalWrite(LED1, HIGH);
        pinMode(LED2,OUTPUT);digitalWrite(LED2, LOW);
        digitalWrite(BUILTIN_LED,LOW);                                  //LED消灯
        //TM1637 init Timmer Setup
        tm1637.set();
        tm1637.init();
        Timer1.pause();                                                 //
        Timer1.setPrescaleFactor(7200);                                 // 72MHz100uSEC
        Timer1.setOverflow(5000);                                       // 500mSEC
        Timer1.attachInterrupt(TIMER_UPDATE_INTERRUPT,TimeUpdate);      // 割り込みサーブルーチンの宣言:TimeUpdate
        Timer1.setCount(0);                                             // 0
        Timer1.refresh();
        Timer1.resume(); 
        // start Ethernet and UDP:(Ethernet3API)
        Ethernet.setCsPin(SPI1_NSS_PIN);
        Ethernet.setRstPin(W550io_Rst);
        //
        pinMode(W550io_Rst, OUTPUT);
        digitalWrite(W550io_Rst, LOW);
        delay(10);
        digitalWrite(W550io_Rst, HIGH);
        Serial.print(F("NIC_Reset\n"));
        LANSetup();                                                     //Ethernet.begin(mac,ip);
        Serial.println(F("Ethernet interface started"));
        Serial.print(F("server is at "));Serial.println(Ethernet.localIP());
        webServer.begin();
        Udp.begin(NTP_PORT);
        //
#if debug
        Serial.print("Version:");Serial.println(vers);
#endif
        //GPS受信機との通信を開始する
        Serial1.begin(9600);                                            //9600bps固定の場合
        ReadGPS();                                                      //GPS取得に成功したら内部時計を更新しています。
        rtclock.getTime(tmm);
        for(int i = 0;i<24;i++){
                for(int j = 0;j<4;j++){
                        gDayBuf[i][j] = NAN;
                }
        }
        //sprintf(STRBUF,"%02u%02u%02u",tmm.hour,tmm.minute,tmm.second);
        //LedAndKey.setDisplayToString(STRBUF,0,2);
        //LedAndKey.setDisplayToString(" HELLO  ",0,0);
        iwdg_init_ms(5000);                                             //ウォッチドッグをオンにして、オーバーフロー時間を設定します。
        Serial.println(F("EndSetUp"));
}
//-------------------- 
int count       = 0;
int preSet      = -1;
void loop() {
        //このLoopはscantime=1秒程度です。そのため5秒経っても戻らなかったらリセットすることにします。
        iwdg_feed();                                    //ウォッチドッグタイマーをリセットするには、ドッグ操作をフィードします
        //参考にしたコードはNTPdは常に動作させ、クライアントから時刻同期要求が有ったら
        //GPSにアクセスするというモノでした。
        //この修正版は、NTPdは常に動作させ、要求があったらBluePillの内部クロックを
        //返します。
        //また、NTPdの周回待ちにGPSにアクセスし、値を取得したら内部時間を更新します。
        //delay(500);
        ReadGPS();
        //delay(500);
        client  =       webServer.available();
        if(client)      checkWebPage(client);
        client.stop();                                                  // コネクションを閉じる。
        processNTP();
        int h = getHourTime();                                          //返値0-23
        if((Status == 'A') && (preSet != h)){
                //1時間おきに時刻を確認し、データをメモリに確保
                analsysRMC();
                preSet  = h;
                gDayBuf[h][0]   =       float(gRmc.LAT) / 10000.0;
                gDayBuf[h][1]   =       float(gRmc.LON) / 10000.0;
                gDayBuf[h][2]   =       float(gRmc.SPD) / 100.0;
                gDayBuf[h][3]   =       float(gRmc.DIR) / 100.0;
                //次回分のデータをNULL化
                h = (h+1) % 24;
                gDayBuf[h][0]   =       NAN;
                gDayBuf[h][1]   =       NAN;
                gDayBuf[h][2]   =       NAN;
                gDayBuf[h][3]   =       NAN;
        }       
        gLoopCount++;
        //digitalWrite(BUILTIN_LED,gLoopCount%2);  //毎秒LEDの点滅処理を実施
        //rtclock.getTime(tmm);
}

このコードを実行すると、Setup()が完了すると LED&KEY に 『HELLO』と表示されます。
GPSアンテナを捕らえると、LED1が点滅します。安定して電波を捕らえ、時刻をBluePill内部時計に登録したら、LED2が点灯します。また、このタイミングで LED&KEY に時刻表示をします。また、表示更新するタイミングでビルトインLED(PC13)を点滅させます。
LED2が点灯しているタイミングで、ネットワーク上のPCから時刻同期を求められたらBluePill内部時計値を返します。
GPSを確保しているかどうかを7セグLEDの一桁目にG/Cの違いで表示するように変更しました。、また、GPSを捕らえたらすぐに『HELLO』を消すように修正しました。
確認しているPCは、

です。
肝心のPLC側の時刻同期検証は出来ておりません。ただ、想定だけはしており、今後機会があれば検証してゆきます。
※仕事柄OMRONのPLC、SYSMACしか検証できないと思っています。


箱に入れて見ました。GPSはAKEEYOのものを接続しています。

ヒマラヤ化学工業所のCUBEという
PP製のケースに入れてみました。
もっとコンパクトな入れ物も有るのでしょうが、この程度でもいいのかな?

100×82×67.5 蓋付きケースです

側面にLAN、USB(マイクロB)、
φ3.5ステレオジャックを装備しています。PPなので加工は楽と云えば楽なのですが、フライス盤を持っていないのでそれなりに大変です。

内部へのアクセスは容易です。

気になっている点として、PPが
静電気に対してどの程度の物か
という事です。
壊れないことを祈りながら運用を
続けます。

部品表

@ ケース \110 ヒマラヤ化学工業所のCUBEセリアで\110です。
加工をどうするかですね。カッタナイフとハンドドリルでなんとかなります。
A STM32miniShield \1650  
B NIC \2442 USR-ES1が入手出来れば安価に出来ます。
AliExpressから入手すれば600円程度でも入手可能です。
C GPS \998 AmazonJapanでCrosstour GPSモジュールを入手しました。
AliExpressから入手すれば500円程度でも入手可能です。
D LED&KEY \850 AmazonJapanだと2個で850円というのがありました。
AliExpressから入手すれば200円程度でも入手可能です。
E φ3.5ステレオジャック \200 秋月で扱っている丸信のジャックの方がお勧めです。
https://akizukidenshi.com/catalog/g/gC-09630/
F JST-XH5Pハウジング \10×2 秋月から購入するのが一番リーズナブルだと思います。
コンタクトは100個入りの場合です。10個入りもあるのですが、結構失敗します。
工具はIZOKEE 圧着ペンチをお勧めします。
G JST-XH4Pハウジング \5×1
H JST-XHコンタクト \200
I ACアダプタ \1100 秋月 AD-B50P250
https://akizukidenshi.com/catalog/g/gM-10507/
J USBケーブル \500 TOPK https://ja.aliexpress.com/item/32912994331.html
合計 \8100  


オブジェクト自体はベクター殿のストレージをお借りしています。

https://www.vector.co.jp/vpack/browse/person/an051501.html


製作物に戻る


問い合わせ頁の表示


免責事項

本ソフトウエアは、あなたに対して何も保証しません。本ソフトウエアの関係者(他の利用者も含む)は、あなたに対して一切責任を負いません。
あなたが、本ソフトウエアを利用(コンパイル後の再利用など全てを含む)する場合は、自己責任で行う必要があります。

本ソフトウエアの著作権はToolsBoxに帰属します。
本ソフトウエアをご利用の結果生じた損害について、ToolsBoxは一切責任を負いません。
ToolsBoxはコンテンツとして提供する全ての文章、画像等について、内容の合法性・正確性・安全性等、において最善の注意をし、作成していますが、保証するものではありません。
ToolsBoxはリンクをしている外部サイトについては、何ら保証しません。
ToolsBoxは事前の予告無く、本ソフトウエアの開発・提供を中止する可能性があります。

商標・登録商標

Microsoft、Windows、WindowsNTは米国Microsoft Corporationの米国およびその他の国における登録商標です。
Windows Vista、Windows XPは、米国Microsoft Corporation.の商品名称です。
LabVIEW、National Instruments、NI、ni.comはNational Instrumentsの登録商標です。
I2Cは、NXP Semiconductors社の登録商標です。
その他の企業名ならびに製品名は、それぞれの会社の商標もしくは登録商標です。
すべての商標および登録商標は、それぞれの所有者に帰属します。