事前準備 / スケッチ

スケッチ(ソースコード)

$Date: 2023/02/14 00:33:43 $
$Revision: 1.20 $

スケッチ

M5Stack Core2でも動作しますが、ボタンまわりの処理(タッチスクリーンで擬似的にボタンを処理)を最適化していないため、Core2ではボタンまわりの操作で挙動不審な箇所があります。(R.1.34で修正しました)

この業界はマニュアルをよく読まない人が多いので、ここで注意喚起! 下記のスケッチだけではコンパイルできません!。 次のページで説明している個別の設定ファイルが必要です。

履歴

  • 2021-06-30 R.1.33 初版公開
  • 2021-07-01 R.1.34 M5Stack Core2のボタン制御を変更
  • 2021-07-01 R.1.35 Core2関係修正
  • 2021-07-03 R.1.39 修正
  • 2021-07-04 R.1.40 修正
  • 2021-07-05 R.1.41 修正
  • 2021-10-12 R.1.43 修正
  • 2021-12-19 R.1.47 Grove接続のGPSに対応しました
  • 2021-12-18 R.1.51 修正
  • 2023-02-14 R.20130214A 修正版

スケッチダウンロード

deserializeJson() + filterの使い方が何か間違っている気がする....メモリー関係をだましだまし使っているような....辛うじて動いている状態.. ...

255行目付近の DynamicJsonDocument mapData(52428); の値を少し小さめにすると安定するかもしれません。 あまり小さくすると、地図が表示されなくなります。この調整が微妙......

クリップボードにコピーしました
/**   -*- c -*-
  Copyright (C) 2021 by Takeki Yahiro. All Rights Reserved.
  $Id: ktm5stack-20230214A.ino,v 1.1 2023/02/14 00:28:12 yahiro Exp $

  https://ketaitracker.info/ktm5stack/
*/

/*
**   API Document
**     https://docs.m5stack.com/en/api/system

** Core2
**  https://docs.m5stack.com/en/arduino/arduino_home_page?id=m5core2_api
**  https://github.com/m5stack/M5Core2/tree/master/src
*/

/*
   設定ファイルを各自作成し,下記でインクルードしてください。

*/

#include "N0CALL.h"


//#define USE_M5STACK_CORE2
//#define USE_GPS_GROVE

/**
    system configuration
**/
const char * version = "20230214A";

#ifndef KTPOS_SERVER
#define KTPOS_SERVER "https://150.95.133.242/ktm5stack/"
#endif
#define KTPOS_PROTOCOL_VERSION 1

char ktpos_server[] = KTPOS_SERVER;



/*
    TODO (bugfix etc)
     それぞれ別々のタスクから画面出力を行っているので,文字の表示位置がずれる (その間,他のタスクを停止する?)
     メッセージ表示(これは簡単に実装できそう)
     接近警報(音を鳴らすか。。。)

     USE TFT_eSprite class
     https://lang-ship.com/blog/work/m5stickc-display2/
     メモリーが足りない・・・のでボツ・・
*/

//
// 動作・処理方法(experimental)

//#define USE_MULTI_TASK
//#define USE_MULTI_TASK_GPS
#define USE_MAPDATA_CLEAR

//#define USE_SPRITE




#ifdef USE_M5STACK_CORE2
#include <M5Core2.h>

#else
#include <M5Stack.h>
#endif

#include <M5Display.h>

/*
   https://github.com/mikalhart/TinyGPSPlus
*/
#include <TinyGPS++.h>
/*
   Arduino Json Lib
   https://github.com/bblanchon/ArduinoJson
   https://arduinojson.org/
*/
#include <ArduinoJson.h>
#include <Arduino.h>





#ifdef USE_SPRITE
#define VRAM vram
#else
#define VRAM M5.Lcd
#endif

/*
  // for indoor
  #define COLOR_COAST TFT_GREEN
  #define COLOR_ROAD M5.Lcd.color565( 30, 30, 30 )
  #define COLOR_RAIL M5.Lcd.color565( 60, 60, 60 )
*/

// for outdoor
#define COLOR_COAST TFT_GREEN
//#define COLOR_ROAD M5.Lcd.color565( 170, 170, 170 )
#define COLOR_ROAD 44373

//#define COLOR_RAIL M5.Lcd.color565( 130, 130, 230 )
#define COLOR_RAIL 33820

#define COLOR_OTHER_FOOTPRINT TFT_YELLOW
#define COLOR_MY_FOOTPRINT    TFT_GREEN
#define COLOR_MY_POSITION     TFT_DARKGREEN

#if 0
#define SEMA_GET getSema()
#define SEMA_REL releaseSema()
#else
#define SEMA_GET
#define SEMA_REL
#endif



int my_course = 0;
int my_speed = 0;
/*
  #ifndef ALERT_STATIONS
  #define ALERT_STATIONS ""
  #endif
*/

#ifdef ALERT_STATIONS

int beep = 1;

#include <map>
struct Alert {
  public:
    float dist ;            // 距離
    unsigned long lastshow; //
};

std::map<std::string, Alert> alert;
#endif

#include <WiFi.h>
#include <WiFiMulti.h>

#include <HTTPClient.h>

HTTPClient http;

#ifdef USE_OTA
#include <WiFiUdp.h>
#include <ArduinoOTA.h>
#endif


#define USE_M5STACK

#ifndef CX
#define CX 160
#endif

#ifndef CY
#define CY 120
#endif

#ifndef UPDATE_INTERVAL
#define UPDATE_INTERVAL 30
#endif

//
// 地図表示で常時 北が上
//
//#define NORTH_UP

//
// GPS画面 グラフ表示での速度,高度,コースのカラー
//
#define COLOR_SPEED       TFT_GREEN
#define COLOR_ALTITUDE    M5.Lcd.color565( 192, 192, 192 )
#define COLOR_ALTITUDE2   M5.Lcd.color565( 50, 50, 50 )
#define COLOR_COURSE      TFT_ORANGE


#define STATUS_AREA_HEIGHT 7

#define STATUS_BUSY  1
#define STATUS_IDLE -1
#define STATUS_NONE  0

unsigned long next_send_time;

WiFiMulti wifiMulti;

#define DISPLAY_MODE_GPS  1
#define DISPLAY_MODE_MAP 2
#define DISPLAY_MODE_RADAR 3
#define DISPLAY_MODE_LIST 4

#define DISPLAY_MODE_GOOGLEMAP 5

#define DISPLAY_MODE_N 4

#ifndef TARGET_STATIONS
#define TARGET_STATIONS "*"
#endif

#ifndef START_NORTH_UP
#define START_NORTH_UP true
#endif


#ifdef DEFAULT_DISPLAY_MODE
int displayMode = DEFAULT_DISPLAY_MODE;
#else
int displayMode = DISPLAY_MODE_MAP;
#endif



// DISPLAY_MODE_LIST
int display_list_target = 0;  // 0 :all 1:mobile only

// DISPLAY_MODE_MAP
int zoom = 4;

int mapHeading = 0;  //  North up
bool north_up = START_NORTH_UP;

// 送受信データ量
uint32_t txByte = 0;
uint32_t rxByte = 0;
uint16_t txRxCount = 0;

int statusBusy = 0;
/*
   This sample code demonstrates the normal use of a TinyGPS++ (TinyGPSPlus) object.
   It requires the use of SoftwareSerial, and assumes that you have a
   4800-baud serial GPS device hooked up on pins 4(rx) and 3(tx).
*/
static const uint32_t GPSBaud = 9600;

#ifdef USE_GPS
// The TinyGPS++ object
TinyGPSPlus gps;

// The serial connection to the GPS device

HardwareSerial ss(2);

#endif

TaskHandle_t taskHandle[4];

#ifdef USE_SPRITE
TFT_eSprite vram = TFT_eSprite( &M5.Lcd );
#endif


//DynamicJsonDocument mapData(65536);  // 64KB , *2 128KB
DynamicJsonDocument mapData(52428);  // 64KB , *2 128KB
//
//StaticJsonDocument<65536> mapData;
//StaticJsonDocument<55536> mapData;



void setup()
{
  M5.begin();
#ifdef USE_M5STACK_CORE2
  // M5.Axp.SetSpkEnable(true ); // 実際に使う直前にONにする?
#else
  M5.Power.begin();
  M5.Speaker.begin();
  M5.Speaker.mute();
#endif

#ifdef USE_GPS
#ifdef USE_GPS_GROVE
  ss.begin( GPSBaud, SERIAL_8N1, 33, 32 );
#else
  ss.begin(GPSBaud);
#endif
#endif

#if 0
  M5.Speaker.setVolume( 1 );
  M5.Speaker.tone( 800, 5);
  delay( 100 );
  M5.Speaker.mute();
#endif

  USE_SERIAL.begin(115200 );

  USE_SERIAL.println();
  USE_SERIAL.println();
  USE_SERIAL.println();
#if 0
  USE_SERIAL.println( M5.Lcd.color565( 130, 130, 230 ));
  USE_SERIAL.println( TFT_YELLOW );
#endif

#ifdef USE_SPRITE
  USE_SERIAL.println( F("Sprite Mode") );
  vram.setColorDepth( 8 );
  vram.createSprite( 320, 240);
  //vram.setSwapBytes( true );
#endif

  dispAppTitle();

  initWiFi();
  connectWiFi();
  for (uint8_t t = 4; t > 0; t--) {
    USE_SERIAL.printf("[SETUP] WAIT %d...\n", t);
    USE_SERIAL.flush();
    dispLcdColor( 300, 225, 2, TFT_DARKGREY, "%d", t );
    delay(1000);
  }

#ifdef ALERT_STATIONS
#ifdef ALERT_BEEP
  beep = 1;
#else
  beep = 0;
#endif
  initAlert();
#endif



  /*
    M5.Lcd.drawPngUrl( "http://192.168.24.10/~yahiro/ktpos/m5stack.png", 0, 0 );
  */
  smartDelay( 2000 );

  next_send_time = millis();  // ms

  USE_SERIAL.println( "start xTaskCreatePinnedToCore()" );
  /* Priolity 0:low 25:high */
  xTaskCreatePinnedToCore(taskWatchDog, "taskWatchDog", 1024, NULL, 1 /* Priolity */ , &taskHandle[3], 1 /* Core ID */);


#ifdef USE_GPS
#ifdef USE_MULTI_TASK_GPS
  xTaskCreatePinnedToCore(taskGPS, "taskGPS", 4096, NULL, 10 /* Priolity */ , &taskHandle[0], 1 /* Core ID */);
#endif
#endif

#ifdef USE_MULTI_TASK
  xTaskCreatePinnedToCore(taskDisplay, "taskDisplay", 8192 , NULL, 5 /* Priolity */ , &taskHandle[1], 1 /* Core ID */);

  USE_SERIAL.println( "use multitask fot updatePosition()" );
  xTaskCreatePinnedToCore(taskUpdatePosition, "taskUpdatePosition",  8192 /* 16384 *//* 32768 */, NULL, 20 /* Priolity */ , &taskHandle[2], 1 /* Core ID */);
#endif

  smartDelay( 2000 );
  M5.update();

  initDisplay();
#ifdef USE_M5STACK_CORE2


#endif

#ifdef USE_OTA
  initOta();
#endif

}

bool availableChange = true;

void clearHttp()
{
  if ( http.connected()) {
    USE_SERIAL.println( "call http.end()" );
    http.end();
  }
}
/*

   Button A
*/
void doBtnA()
{
  USE_SERIAL.println( "Button A" );

  clearHttp();

  displayMode++;
  if ( displayMode > DISPLAY_MODE_N ) {
#ifdef USE_GPS
    displayMode = 1;
#else
    displayMode = 2;
#endif
  }
  initDisplay();
}

/*
   Button B
*/
void doBtnB()
{
  USE_SERIAL.println( "Button B" );
  switch ( displayMode )
  {
    case DISPLAY_MODE_MAP:
    case DISPLAY_MODE_RADAR:
      zoom = zoom + 1;
      clearHttp();

      initDisplay();
      break;

    case DISPLAY_MODE_LIST:
      display_list_target = (display_list_target + 1) % 2;
      clearHttp();

      initDisplay();
      break;
  }
}

/*
   Button B 長押し
*/
void doBtnBLong()
{
  USE_SERIAL.println( "Btn B Long Pressed" );
  switch ( displayMode )
  {
    case DISPLAY_MODE_MAP:
    case DISPLAY_MODE_RADAR:
      clearHttp();

#ifndef NORTH_UP
      changeNorthup();
#endif
      break;
  }

}

/*
   Button C
*/
void doBtnC()
{
  USE_SERIAL.println( "Button C" );
  switch ( displayMode )
  {
    case DISPLAY_MODE_MAP:
    case DISPLAY_MODE_RADAR:
      clearHttp();

      zoom = zoom - 1;
      zoom = max( 1, zoom );
      initDisplay();
      break;

    case DISPLAY_MODE_LIST:
#ifdef ALERT_STATIONS
      beep = beep ? 0 : 1;
      if  ( beep ) {
        dispLcdColorBg(250, 220, 2, TFT_BLACK, TFT_GREEN,  "O N" );
      } else {

        dispLcdColorBg(250, 220, 2, TFT_BLACK, TFT_ORANGE, "OFF" );
      }
#endif
      break;
  }
  smartDelay( 100 );
}

void loop()
{
  //static bool btnALong = false;
  static bool btnBLong = false;
  //static bool btnCLong = false;
#ifdef USE_M5STACK_CORE2
  static bool btn_mapHeading = false;

  TouchPoint_t pos = M5.Touch.getPressPoint();

#endif

  M5.update();

#ifdef USE_M5STACK_CORE2
#if 1
  if ( 0 <= pos.x && pos.x <= 60 && 0 <= pos.y && pos.y <= 60 && btn_mapHeading == false ) {
    btn_mapHeading = true;
    //USE_SERIAL.println( "Btn Press mapHeading" );
    doBtnBLong();
  } else if ( pos.x == -1 ) {
    btn_mapHeading = false;
  }
#endif
#if 1
  static int  touchBtnCurrent = 0; // 最後に押されたボタン (チャタリング防止)

  if ( 0 <= pos.x && pos.x <= 106 && 240 <= pos.y && pos.y <= 280  ) {
    if ( touchBtnCurrent != 1 ) {
      touchBtnCurrent = 1;
      doBtnA();
    }
  } else if ( 107 <= pos.x && pos.x <= 213 && 240 <= pos.y && pos.y <= 280  ) {
    if (  touchBtnCurrent != 2 ) {
      touchBtnCurrent = 2;
      doBtnB();
    }
  } else if ( 214 <= pos.x && pos.x <= 320 && 240 <= pos.y && pos.y <= 280  ) {
    if ( touchBtnCurrent != 3 ) {
      touchBtnCurrent = 3;
      doBtnC();
    }
  } else {
    touchBtnCurrent = 0;
  }
#endif
#if 0
  if ( M5.BtnA.wasPressed()) {
    doBtnA();
  }
  if ( M5.BtnB.wasPressed()) {
    doBtnB();
  }
  if ( M5.BtnC.wasPressed()) {
    doBtnC();
  }

#endif


#else
  // normal M5STACK
  if ( M5.BtnA.wasReleased()) {
    doBtnA();
  }
  if ( M5.BtnB.pressedFor( 1000 )) {
    if ( !btnBLong ) {
      btnBLong = true;
      doBtnBLong();
    }
  }
  if ( M5.BtnB.wasReleased()) {
    if ( btnBLong ) {
      btnBLong = false;
    } else {
      doBtnB();
    }
  }
  if ( M5.BtnC.wasReleased()) {
    doBtnC();
  }

#endif

#ifndef USE_MULTI_TASK_GPS
#ifdef USE_GPS
  doGps();
#endif
#endif

#ifndef USE_MULTI_TASK
  updatePosition();

  doDisplay();

#endif
#if defined( USE_OTA )
  ArduinoOTA.handle();
#endif
  smartDelay( 1 );
}


#ifndef NORTH_UP
/*
   ノースアップ、ヘディングアップの切り替え処理
*/
int changeNorthup()
{
  north_up = (north_up ? false : true);
  if ( north_up ) {

    M5.Lcd.fillCircle( 160, 200, 15, TFT_BLACK );

    drawCompassRaw( 160, 200, 15,  0 );
    smartDelay( 360 );
    mapHeading = 0;
  } else {

    for ( int news = 0; news <= 360; news += 10 ) {
      M5.Lcd.fillCircle( 160, 200, 15, TFT_BLACK );
      drawCompassRaw( 160, 200, 15,  news );
      smartDelay( 10 );
    }

  }
  initDisplay();
  return 0;
}
#endif

void dispAppTitle()
{
  int news;

  dispLcdColorCenter( /*76,*/ 40, 2, TFT_GREEN, "%s", F("KetaiTracker") ); // 12 char,  x = 160 - strlen( xxxx )/2 * 14   ,  font-size 1=6px 2=12px 2=18px;...
  dispLcdColorCenter( /*83,*/ 70, 2, TFT_GREEN, "%s", F("for M5Stack") ); // 11 char
  dispLcdColorCenter( /*41,*/ 110, 2, TFT_WHITE, version );   //  17char 160 - 17/2* 14
#ifdef DEMO
  dispLcdColorCenter( /* 106, */ 127, 1, TFT_RED, "%s", F("Demonstration Mode") );
#endif
  dispLcdColorCenter( 200, 1, TFT_WHITE, "(C) JF6LZE" );
  dispLcdColorCenter( 212, 1, TFT_WHITE, "All Rights Reserved" );
  dispLcdColorCenter( /* 94,*/ 160, 2, TFT_ORANGE, CALLSIGN );

#ifdef USE_M5STACK_CORE2
  // dispLcdColorCenter( 0, 2, TFT_WHITE, "%fV %fW",  M5.Axp.GetBatVoltage(), M5.Axp.GetBatChargeCurrent());

#endif
  for ( news = 0; news <= 360; news += 30 ) {
    M5.Lcd.fillCircle( 20, 20, 15, TFT_BLACK );
    drawCompassRaw( 20, 20, 15, news );
    smartDelay( 30 );
  }
}

#ifdef  USE_OTA
// ref. sample sketch BasicOTA.ino
void initOta()
{

  ArduinoOTA
  .onStart([]() {
    String type;
    if (ArduinoOTA.getCommand() == U_FLASH)
      type = "sketch";
    else // U_SPIFFS
      type = "filesystem";

    // NOTE: if updating SPIFFS this would be the place to unmount SPIFFS using SPIFFS.end()
    USE_SERIAL.println("Start updating " + type);
  })
  .onEnd([]() {
    USE_SERIAL.println("\nEnd");
  })
  .onProgress([](unsigned int progress, unsigned int total) {
    USE_SERIAL.printf("Progress: %u%%\r", (progress / (total / 100)));
  })
  .onError([](ota_error_t error) {
    USE_SERIAL.printf("Error[%u]: ", error);
    if (error == OTA_AUTH_ERROR) USE_SERIAL.println("Auth Failed");
    else if (error == OTA_BEGIN_ERROR) USE_SERIAL.println("Begin Failed");
    else if (error == OTA_CONNECT_ERROR) USE_SERIAL.println("Connect Failed");
    else if (error == OTA_RECEIVE_ERROR) USE_SERIAL.println("Receive Failed");
    else if (error == OTA_END_ERROR) USE_SERIAL.println("End Failed");
  });

  ArduinoOTA.begin();
  USE_SERIAL.println( "OTA Enabled" );
}

#endif

#ifdef ALERT_STATIONS
int initAlert()
{
  char * buf;

  buf = (char *)malloc( strlen( ALERT_STATIONS ) + 1 );
  strcpy( buf, ALERT_STATIONS );
  USE_SERIAL.println( "initAlert()" );
  char * p;
  for ( p = strtok(  buf, "," ); p != NULL ; p = strtok( NULL, "," )) {
    USE_SERIAL.println( p );
    alert[p].dist = 100000;

  }
  return 0;
}

/*
   接近警報
*/
void checkAlert( const String payload )
{
  int n;
  const char * callsign;
  float dist;

  USE_SERIAL.println( F("checkAlert()") );
#ifndef USE_M5STACK_CORE2
  int tone_freq;
#endif
  DynamicJsonDocument filter(200);
  filter["near"] = true;
  filter["cnt"]  = true;

#ifdef USE_MAPDATA_CLEAR
  mapData.clear();
#endif
  DeserializationError err =
    deserializeJson( mapData, payload , DeserializationOption::Filter( filter) );
  // 2021.12.23
#if 1
  if ( err.code() != DeserializationError::Ok ) {
    USE_SERIAL.println( F(" error on drawMapLocalStations") );
    return;
  }
#endif
  n = mapData["cnt"];
  // USE_SERIAL.print( "checkAlert(); " );
  for (int i = 0; i < n ; i++ ) {

    callsign = mapData["near"][i]["callsign"];
    dist = mapData["near"][i]["dists"];
    //USE_SERIAL.print( callsign );

    auto itr = alert.find( callsign );
    if ( itr != alert.end()) {
      USE_SERIAL.print( " Alert Station = " );
      USE_SERIAL.println( callsign );
      // 経過時間のチェック 古いデータはreset
      if ( alert[callsign].lastshow + 1800000 < (millis() )) { // 30min 他のパラメータとの要調整
        alert[callsign].dist = 99999;
        USE_SERIAL.print( F("reset alert ") );
        USE_SERIAL.print( callsign );
        USE_SERIAL.print( F(" ") );
        USE_SERIAL.print( alert[callsign].lastshow );
        USE_SERIAL.print( F(" ") );
        USE_SERIAL.print( millis());
        USE_SERIAL.println( "" );
      }
      if ( dist < alert[callsign].dist ) { // km
        USE_SERIAL.print( "close to you! " );
        USE_SERIAL.println( dist );
        alert[callsign].dist = dist;
        alert[callsign].lastshow = millis();
        if ( beep ) {
#ifdef USE_M5STACK_CORE2

#else
          tone_freq = 440 + 440 *  ( 1 - dist / 20.0);
          tone_freq = max( tone_freq, 440 );

          M5.Speaker.tone( tone_freq, 80 );
          delay( 80 );
          M5.Speaker.mute();
          if ( dist < 1 ) {
            delay( 40 );
            M5.Speaker.tone( tone_freq, 80 );
            delay( 80 );
            M5.Speaker.mute();
          }
          if ( dist < 0.5 ) {
            delay( 40 );
            M5.Speaker.tone( tone_freq, 80 );
            delay( 80 );
            M5.Speaker.mute();
          }
#endif
        }
      }
    } else {

    }
  }
  USE_SERIAL.println( "..done" );

}
#endif
/*
    GPSの受信状態を表示
*/
void dispGpsStatus()
{
#ifdef USE_GPS
  uint16_t bcolor;
  uint16_t fcolor;
#endif

  if ( statusBusy )
    return;

#ifdef USE_GPS
  fcolor = TFT_BLACK;
  switch (  (int)gps.satellites.value()) {

    case 0:
      bcolor = TFT_RED;
      break;
    case 1:
    case 2:
    case 3:
      bcolor = TFT_ORANGE;
      break;
    default:
      bcolor = TFT_BLACK;
      fcolor = TFT_GREEN;
      break;
  }

  dispLcdColorBg( 272, 0, 1, fcolor, bcolor, "GPS" );
#else


#endif
}

void taskWatchDog( void * arg )
{
  static int check = 0;
  while ( 1 ) {
    if ( statusBusy > 0 ) {
      check++;
      if ( check > 15 ) {
        USE_SERIAL.println( "watch dog 15 sec!" );
      }
      if ( check > 20 ) {
        USE_SERIAL.println( "Reset by watchdog" );
#ifdef USE_M5STACK_CORE2
        M5.shutdown( 1 );
#else
        M5.Power.reset();
#endif
      }
    } else {
      check = 0;
    }
    smartDelay( 1000 );
  }
}

#ifdef USE_GPS
void doGps()
{

  if (gps.location.isValid()  ) {
    setGraphDataInterval( gps.speed.kmph(), gps.altitude.meters(), gps.course.deg() );
  }

  dispGpsStatus();

}
void taskGPS( void * arg)
{
  while ( 1 ) {
    doGps();
    smartDelay( 1000 );
  }
}
#endif

void doDisplay() {
  switch ( displayMode ) {

    case DISPLAY_MODE_GPS:
#ifdef USE_GPS
      dispGps(  );
      smartDelay( 500 );
#else
#endif
      break;

    case DISPLAY_MODE_MAP :
    case DISPLAY_MODE_RADAR:
      showNearGps();
      smartDelay( 500 );
      break;

#ifdef USE_GOOGLEMAP
    case DISPLAY_MODE_GOOGLEMAP:
      showGoogleMap( 0 );
      smartDelay( 5000 );
      break;
#endif

    default:
      smartDelay( 500 );
      break;

  }

}
void taskDisplay( void * arg)
{
  while ( 1 ) {
    doDisplay();

  }
  smartDelay( 500 );
}

void taskUpdatePosition( void * arg )
{
  while ( 1 ) {
    updatePosition();
    smartDelay( 1000 ); // 1000ms   GPS Freq?
  }
}

/*
   データ表示領域のクリア
*/
void clearDisplay()
{
  M5.Lcd.fillRect( 0, STATUS_AREA_HEIGHT, 320, 240, TFT_BLACK );
}

/*
** ステイタス領域のクリア
*/
void clearStatus()
{
  M5.Lcd.fillRect( 0, 0, 320, STATUS_AREA_HEIGHT, TFT_BLACK );
}

void initDisplay()
{
  USE_SERIAL.println( "initDisplay() start" );

  clearStatus();
  clearDisplay();

  dispLcdColorCenter( 120, 2, TFT_YELLOW, "WAIT" );
  USE_SERIAL.println( "WAIT" );

  switch ( displayMode ) {
    case DISPLAY_MODE_GPS:
      USE_SERIAL.println( "DISPLAY_MODE_GPS" );
      dispLcdColor( 0,  220, 2, TFT_GREEN, CALLSIGN );

      initGraph();
      smartDelay( 1 );
      break;

    case DISPLAY_MODE_MAP :
    case DISPLAY_MODE_RADAR:
      USE_SERIAL.println( "DISPLAY_MODE_MAP or RADAR" );
      showNearGps();
      smartDelay(1);
      break;

    case DISPLAY_MODE_LIST:
      USE_SERIAL.println( "DISPLAY_MODE_LIST" );
      switch ( display_list_target ) {
        case 0:
          //dispLcdColor( 0, 100, 2, TFT_YELLOW, "List " );
          dispLcdColorCenter( 80, 2, TFT_YELLOW, "List" );
          break;
        case 1:
          dispLcdColorCenter( 80, 2, TFT_YELLOW, "List (mobile)" );
          break;
      }
      smartDelay( 1 );
      break;
#ifdef USE_GOOGLEMAP
    case DISPLAY_MODE_GOOGLEMAP:
      dispLcdColor( 0, 100, 2, TFT_GREEN, "Google Map" );
      showGoogleMap( 1 );
      break;
#endif
    default:
      USE_SERIAL.println( "DISPLAY_MODE_DEFAULT may be error" );
      break;
  }
  updateDisplay();
  USE_SERIAL.println( "initDisplay() end" );
}


/*
   通信状態の表示
*/
#define COMM_BEGIN  1
#define COMM_END   -1
#define COMM_NONE   0

void dispTxRxByte()
{

  dispLcdColor( 295, 230, 1, TFT_GREEN, "%4d", txRxCount );
  /*
    dispLcdColor( 295, 194, 1, TFT_GREEN, "%4d", uxTaskGetStackHighWaterMark(taskHandle[0]));
    dispLcdColor( 295, 202, 1, TFT_GREEN, "%4d", uxTaskGetStackHighWaterMark(taskHandle[1]));
    dispLcdColor( 295, 210, 1, TFT_GREEN, "%4d", uxTaskGetStackHighWaterMark(taskHandle[2]));
  */
}

void dispCommStatus( int sw )
{
  static int busy = 0;
  uint32_t txRxByte ;
  const char * unit = "KB";

  busy += sw;

  txRxByte = (uint16_t)((txByte + rxByte) / 1024);
  if ( txRxByte >= 8192 ) {
    txRxByte = (uint16_t) (txRxByte / 1024);
    unit = "MB";
  }

  dispLcdColorBg( 231, 0, 1,
                  (busy ? TFT_BLACK : TFT_GREEN ),
                  (busy ? TFT_ORANGE : TFT_BLACK ),
                  "%4d%s", txRxByte, unit
                );
  dispTemperature();
}

/*
   System Status  (busy|idle)
*/
void setStatus( int sw )
{

  statusBusy += sw;
  dispLcdColorBg( 295, 0, 1,
                  ( statusBusy > 0 ? TFT_BLACK  : TFT_GREEN )  ,
                  ( statusBusy > 0 ?  TFT_ORANGE : TFT_BLACK ),
                  "%s",
                  ( statusBusy > 0 ?  F("Busy") : F("Idle") )
                );

}

/*
   内部温度の表示 (高くなるとCPUが止まるらしい)
*/
void dispTemperature()
{

#ifdef USE_DTOSTRF
  char buf[16];

  dtostrf( temperatureRead(), 4, 1, buf );
  dispLcdColor( 200, 0, 1, TFT_WHITE, "%s" , buf);
#else
  dispLcdColor( 200, 0, 1, TFT_WHITE, "%.1f" , temperatureRead() );
#endif
  //USE_SERIAL.println( temperatureRead() );
}

void dispStatus()
{
  /* common */
  clearStatus();

  setStatus( STATUS_NONE );
  dispGpsStatus();
  dispCommStatus( COMM_NONE );
  //dispTemperature();

  switch ( displayMode ) {
    case DISPLAY_MODE_GPS:

      break;
    case DISPLAY_MODE_MAP:
    case DISPLAY_MODE_RADAR:
      showNearGps();
      break;

    case DISPLAY_MODE_LIST:

      break;
  }
}

int updateDisplay()
{
  USE_SERIAL.println( F("updateDisplay() start") );
  updatePositionToServer( 880 );
  USE_SERIAL.println( F("updateDisplay() end") );

  return 0;
}

void initWiFi()
{
  int n;
  char * ssid, * password;
  char buf[256];
  int ret;

  n = sizeof( ap_list ) / sizeof( char * );
  USE_SERIAL.println( n );
  for ( int i = 0 ; i < n ; i++ ) {

    sprintf( buf, "%s", ap_list[i] );
    ssid  = strtok( buf, "," );
    password = strtok( NULL, "," );

    USE_SERIAL.println( ssid );
    USE_SERIAL.println( password );

    wifiMulti.addAP( ssid, password );

  }
  ret = wifiMulti.run();
  //USE_SERIAL.println( ret );
}

void connectWiFi()
{
  switch ( wifiMulti.run()) {
    case WL_CONNECTED:
      USE_SERIAL.println( F("Wifi Connected") );
      return;

    case WL_CONNECT_FAILED:
      USE_SERIAL.println( F("Wifi connect failed") );
      break;

    case WL_DISCONNECTED:
      USE_SERIAL.println( F("Wifi disconnected") );
      break;
  }
}
/*
   320 x 240 TFT
*/
#ifdef USE_GPS
void dispGps( )
{
#ifdef USE_DTOSTRF
  char buf[32];
#endif
  if ( statusBusy )
    return;

#ifdef FIX_MODE

  dispLcdColor(   0,  30, 3, TFT_WHITE, "%9.4f", HOME_LAT );
  dispLcdColor(   0,  60, 3, TFT_WHITE, "%9.4f", HOME_LNG );

#else

  M5.Lcd.setCursor( 0, 0 );
  M5.Lcd.setTextSize( 1 );

  printDateTime(gps.date, gps.time);
#ifdef USE_DTOSTRF
  dtostrf( gps.location.lat(), 9, 4, buf );
  dispLcdColor(   0,  30, 3, TFT_WHITE, "%s", buf );


  dtostrf( gps.location.lng(), 9, 4, buf );
  dispLcdColor(   0,  60, 3, TFT_WHITE, "%s", buf );
#else

  dispLcdColor(   0,  30, 3, TFT_WHITE, "%9.4f", (float)gps.location.lat() );
  dispLcdColor(   0,  60, 3, TFT_WHITE, "%9.4f", (float)gps.location.lng() );
#endif

  dispLcdColor(   0, 90, 3, TFT_WHITE, "%4d", (int)gps.satellites.value() );
  dispLcdColor( 120, 95, 2, TFT_WHITE, "Sat." );

  dispLcdColor( 200, 30, 3, TFT_WHITE, "%3d", (int)gps.speed.kmph());
  dispLcdColor( 255, 35, 2, COLOR_SPEED, "km/h" );

  dispLcdColor( 200, 60, 3, TFT_WHITE, "%3d", (int)gps.course.deg());
  dispLcdColor( 255, 65, 2, COLOR_COURSE, "deg" );

  dispLcdColor( 183,  90, 3, TFT_WHITE, "%4d", (int)gps.altitude.meters() );
  dispLcdColor( 255,  95, 2, COLOR_ALTITUDE, "mH" );

  /*
       Graph
  */
  drawGraphAll();
#endif
}
#endif

/*
   送信タイミング
*/
int updatePosition()
{
#ifdef USE_GPS
  static int lastHeading = -1;
  int heading = -1;
  int headingDiff = 0;
#endif
  USE_SERIAL.println( F( "updatePsotin() start" ));

  if ( millis() >= next_send_time ) {
    updatePositionToServer( 440  );
    next_send_time = millis() + UPDATE_INTERVAL * 1000;
  } else {
#ifdef USE_GPS
    if ( gps.location.isValid()) {
      if ( lastHeading == -1 ) {
        lastHeading = (int)gps.course.deg();
      } else {

        heading = (int)gps.course.deg();
        headingDiff = abs( lastHeading - heading ) ;
        if ( headingDiff > 180 ) {
          headingDiff = 360 - headingDiff;
        }

        // debug
        dispLcdColor( 30,  220, 3, TFT_WHITE, "%2d", headingDiff );

        if ( headingDiff > 10 ) {
          updatePositionToServer( 1600 );
          next_send_time = millis() + UPDATE_INTERVAL * 1000;
          lastHeading = heading;
        }
      }
    }
#endif
  }
  smartDelay( 100 );
  USE_SERIAL.println( F("updateDisplay() end") );

  return 0;
}

#define ACCESS_MODE_GET 1
#define ACCESS_MODE_SET 2



int updatePositionToServer( int freq )
{

  static int busy = 0;
  static int notConnect = 0;
  static int seq = 0;

  if ( statusBusy )
    return 1;

  if ( busy )
    return 1;

  busy = 1;

#ifdef BEEP_ON_SEND
  int tone_freq = freq;

  M5.Speaker.tone( tone_freq, 80 );
  delay( 40 );
  M5.Speaker.mute();
#endif

#ifdef DEMO
  displayMode++;
  if ( displayMode > DISPLAY_MODE_N ) {
    displayMode = 0;
    clearStatus();
    clearDisplay();
    dispAppTitle();

    smartDelay( 5000 );
    M5.Lcd.qrcode( "https://ketaitracker.info/ktm5stack/" );
    smartDelay( 5000);
    displayMode = 1;
    clearStatus();
    clearDisplay();
  }


#endif

  setStatus( STATUS_BUSY );
  if ((wifiMulti.run() == WL_CONNECTED)) {

    //    HTTPClient http;

    char params[80];
    char url[1024];
#ifndef USE_GPS
    char alt[10];
    char course[10];
    char spd[10];
#endif
    int mode = ACCESS_MODE_GET;
    float lat, lng;



    if ( notConnect ) {
      //dispLcdColor( 50, 210, 2, TFT_RED, "                     ", notConnect );
      dispLcdColor( 50, 210, 2, TFT_RED, "                     " );
      /*
        M5.Lcd.setCursor( 107, 210 );
        M5.Lcd.setTextColor( TFT_RED, TFT_BLACK );
        M5.Lcd.setTextSize( 2 );
        M5.Lcd.print( "           " );
      */
      notConnect = 0;
    }
    //  display current IP address
    if ( displayMode == DISPLAY_MODE_GPS ) {
      //noInterrupts();
      M5.Lcd.setTextColor( TFT_WHITE, TFT_BLACK );
      M5.Lcd.setTextSize( 2 );
      M5.Lcd.setCursor( 107, 220 );
      M5.Lcd.print( WiFi.localIP());
      //interrupts();
    }

#ifdef USE_GPS
    if (gps.location.isValid()  ) {
      mode = ACCESS_MODE_SET;
    }
#else
    mode = ACCESS_MODE_GET;
#endif

    txRxCount++;
#ifdef FIX_MODE
    lat = HOME_LAT;
    lng = HOME_LNG;
    mode = ACCESS_MODE_GET;
    mapHeading = 0;
#else
#ifdef USE_GPS
    if ( gps.location.isValid()) {
      lat = gps.location.lat();
      lng = gps.location.lng();
      if ( lat == 0.0 || lng == 0.0 ) {
        mode = ACCESS_MODE_GET;
      }
    } else {
      lat = 0.0;
      lng = 0.0;
      mode = ACCESS_MODE_GET;
    }
#else
    lat = 0.0;
    lng = 0.0;
#endif
#ifndef NORTH_UP
    if ( !north_up ) {
#ifdef USE_GPS
      if ( gps.location.isValid()) {
        mapHeading = (int)gps.course.deg();
      } else {
        mapHeading = 0;

      }
#else
      mapHeading = 0;
#endif
    }
#endif

#endif

#ifdef USE_GPS
    USE_SERIAL.println( gps.altitude.feet() );

#ifdef USE_DTOSTRF
    dtostrf( gps.altitude.feet(), 0, 0, alt );
    dtostrf( gps.course.deg(),    0, 0, course );
    dtostrf( gps.speed.knots(),   0, 0, spd );
    sprintf( params, "pos=%f,%f,%s,%s,%s",
             lat, lng,
             alt,
             course,
             spd

           );
#else
    sprintf( params, "pos=%.5f,%.5f,%d,%d,%d",
             lat, lng,
             (int)gps.altitude.feet(),
             (int)gps.course.deg(),
             (int)gps.speed.knots()
           );
#endif
#else
    strcpy( alt, "0" );
    strcpy( course, "0" );
    strcpy(  spd, "0" );
    sprintf( params, "pos=%f,%f,%s,%s,%s",
             lat, lng,
             alt,
             course,
             spd

           );
#endif
    seq++;

#ifdef DEMO
    sprintf( url, "%s?cmd=%s&%s&c=%s&p=%d&z=%d&d=%d&a=%d&t=%s&v=%d&cx=%d&cy=%d&nu=%d&demo=1&seq=%d",
             ktpos_server,
             "get",
             params,
             CALLSIGN,
             PASSCODE,
             zoom,
             displayMode,
             display_list_target,
             TARGET_STATIONS,
             KTPOS_PROTOCOL_VERSION,
             CX,
             CY,
             (north_up ? 1 : 0),
             seq
           );
#else
    sprintf( url, "%s?cmd=%s&%s&c=%s&p=%d&z=%d&d=%d&a=%d&t=%s&v=%d&cx=%d&cy=%d&nu=%d&st=%d&seq=%d",
             ktpos_server,
             ( mode == ACCESS_MODE_GET ? "get" : "set" ),
             params,
             CALLSIGN,
             PASSCODE,
             zoom,
             displayMode,
             display_list_target,
             TARGET_STATIONS,
             KTPOS_PROTOCOL_VERSION,
             CX,
             CY,
             (north_up ? 1 : 0),
#ifdef USE_GPS
             (int)gps.satellites.value(),
#else
             0,
#endif
             seq
           );
#endif
    //http.setTimeout( 10000 );
    smartDelay( 1 );

    txByte += strlen( url );
    // txByte += http.getSize();
    dispCommStatus( COMM_BEGIN );
    http.begin( url ); // "http://192.168.24.10/"); //HTTP
    USE_SERIAL.print( F("http.GET() start :") );
    USE_SERIAL.println( url );
    int httpCode = http.GET();
    USE_SERIAL.println( F("http.GET() end") );
    dispCommStatus( COMM_END );

    smartDelay(1 );

    if (httpCode > 0) {
      if (httpCode == HTTP_CODE_OK) {
        String payload = http.getString();
        //USE_SERIAL.println(payload);
        rxByte += payload.length();

        // 意味無し無駄コード -> 地図のコンパスを描画するのに必要!(出世した)
        if ( mode == ACCESS_MODE_GET ) {
          DynamicJsonDocument my(200);
          StaticJsonDocument<200>filter;
          filter["my"] = true;
          DeserializationError err = deserializeJson( my, payload, DeserializationOption::Filter(filter));
#if 1
          // 2021.12.23
          if ( err.code() != DeserializationError::Ok ) {
            http.end();
            return -1;
          }
#endif

#if 1
          switch (err.code()) {
            case DeserializationError::Ok:
              USE_SERIAL.println(F("Deserialization succeeded on ACCESS_MODE_GET"));
              break;

            case DeserializationError::InvalidInput:
              dispErrorMsg( "Invalid input!" );
              http.end();

              return -1;
              break;

            case DeserializationError::NoMemory:
              dispErrorMsg( "Not enough memory" );
              http.end();

              return -1;
              break;

            case DeserializationError::IncompleteInput:
              USE_SERIAL.println(F("Deserialization ImcomplementInput"));
              http.end();

              return -1;
              break;

            case DeserializationError::TooDeep:
              USE_SERIAL.println(F("Deserialization TooDeep"));
              http.end();

              return -1;
              break;

            default:
              USE_SERIAL.println(F("Deserialization failed on ACCESS_MODE_GET "));
              USE_SERIAL.println( err.c_str());
              USE_SERIAL.println( payload );
              http.end();

              return -1;
              break;
          }
#endif

          my_course = my["my"]["course"];
          my_speed  = my["my"]["spd"];

#ifndef NORTH_UP
          if ( !north_up ) {
            mapHeading = my["my"]["course"];
          }
#endif
        }

        switch ( displayMode ) {

          case DISPLAY_MODE_MAP:
          case DISPLAY_MODE_RADAR:
#ifdef DEMO
            initDisplay();
#endif
            showNear( payload );
            break;

          case DISPLAY_MODE_LIST:
#ifdef DEMO
            initDisplay();
#endif
            showList( payload );
        }
#ifdef ALERT_STATIONS
        checkAlert( payload );
#endif
      }


    } else {
      USE_SERIAL.printf("[HTTP] GET... failed, error: %s\n", http.errorToString(httpCode).c_str());
      char buf[512];
      sprintf( buf, "ERROR: %s\n", http.errorToString(httpCode).c_str());
      M5.Lcd.setCursor( 0, 230 );
      M5.Lcd.setTextColor( TFT_RED , TFT_BLACK);
      M5.Lcd.setTextSize( 1 );
      M5.Lcd.print( buf );

    }

    http.end();

    setStatus( STATUS_IDLE );

  } else {
    /*
      M5.Lcd.setCursor( 107, 210 );
      M5.Lcd.setTextColor( TFT_RED, TFT_BLACK );
      M5.Lcd.setTextSize( 2 );
      M5.Lcd.print( "Not Connect");
    */
    setStatus( STATUS_IDLE );

    notConnect++;
    dispLcdColor( 50, 210, 2, TFT_RED, "Wi-Fi NotConnect (%d)", notConnect );
    smartDelay( 100 );
  }


  dispCommStatus( COMM_NONE );

  busy = 0;

  return 0;

}

void showMap()
{
  //https://jetloger.com/m5stack-m5camera-capture-lcd/#anker12

  // https://github.com/m5stack/M5Stack/blob/master/src/M5Display.h
  // https://github.com/m5stack/M5Stack/blob/f10648586cc4324d844cfd9505cfa47d3d61b7ca/src/M5Display.cpp
  // M5.Lcd.drawPngUrl();
}

#ifdef USE_GOOGLEMAP
int showGoogleMap( int reset  )
{
  static int olat, olng ;
  int lat, lng;
  const char * url = "https://maps.googleapis.com/maps/api/staticmap";
  char buf[256];

  if ( reset ) {
    olat = 0;
    olng = 0;
  }

  USE_SERIAL.println( "showGoogelMap()" );
  if ( gps.location.isValid()) {
    drawCurrentPositionGps();
    lat = round( gps.location.lat() * 1000 );
    lng = round( gps.location.lng() * 1000 );
    if ( olat != lat && olng != lng ) {
      sprintf( buf, "%s?center=%f,%f&zoom=10&size=320x240&key=%s", url, (float)gps.location.lat(), (float)gps.location.lng(), GOOGLE_MAP_API_KEY );
      USE_SERIAL.println( buf );

      M5.Lcd.drawPngUrl( buf, 0, 0 );
      drawCurrentPositionGps();
    } else {
      USE_SERIAL.println( "not moved" );

    }
    olat = lat;
    olng = lng;


  } else {
    M5.Lcd.drawPngUrl( "http://192.168.24.10/~yahiro/ktpos/m5stack.png", 0, 0 );
  }
  return 0;
}

#endif

void showNearGps()
{
#ifdef USE_DTOSTRF
  char buf[64];
#endif
  uint16_t color;

#ifdef USE_GPS
#ifdef USE_DTOSTRF
  dtostrf( gps.location.lat(), 8, 4, buf );
  dispLcdColor(   0,  0, 1, TFT_WHITE, "%s", buf );

  dtostrf( gps.location.lng(), 9, 4, buf );
  dispLcdColor(   50,  0, 1, TFT_WHITE, "%s", buf );
#else
  dispLcdColor(   0,  0, 1, TFT_WHITE, "%8.4f", (float)gps.location.lat() );
  dispLcdColor(   50,  0, 1, TFT_WHITE, "%9.4f", (float)gps.location.lng() );
#endif
#endif

  switch ( zoom ) {
    case 1:
    case 2:
    case 3:
    case 4: // 高速 国道
      color = TFT_RED;
      break;
    case 5: // 地方国道
      color = TFT_ORANGE;
      break;
    case 6: // 主要県道 地方県道
      color = TFT_YELLOW;
      break;
    case 7: // 市町村道
      color = TFT_BLUE;
      break;
    default: // 路地
      color = TFT_WHITE;
      break;
  }

  float range = (float)(160.0 / pow( 2, zoom));
  /* MAP range */
  if ( range >= 2.0 ) {
    dispLcdColor( 130, 0, 1, color, "%2dkm", (int)range);
  } else {
    dispLcdColor( 130, 0, 1, color, "%3dm", (int)(range * 1000.0) );
  }
  //dispLcdColor( 160, 0, 1, TFT_WHITE, "%d", zoom );
}


#define DRAW_MAP_SHAPE_DOT 0
#define DRAW_MAP_SHAPE_LINE 1


/**
    描画用座標計算
   news  方位(0-360)
   dist  距離(km)

   return  dx, dy  座標(dx, dy)
*/
void calcXY( float news, float dist, int * dx, int * dy )
{
  int s, x, y;

#ifndef NORTH_UP
  if ( !north_up ) {
    news -= mapHeading;
  }
#endif

  s = (int)((360.0 - news) + 90.0) % 360;

  dist = dist * pow( 2, zoom );  // 倍率

  x = dist * cos( s * 0.01745 ); //  / 180 * PI
  y = dist * sin( s * 0.01745 );
  // 320 x 240
  * dx = CX + x;
  * dy = CY - y;

}

void calcXYforArrow(  int news, int len, int bx, int by, int * dx, int * dy )
{

  int s, x, y;
#ifndef NORTH_UP
  if ( !north_up ) {
    news -= mapHeading;
  }
#endif
  s = (int)((360.0 - news) + 90.0) % 360;

  x = len * cos( s * 0.01745 ); //  / 180 * PI
  y = len * sin( s * 0.01745 );
  // 320 x 240
  * dx = bx + x;
  * dy = by - y;
}

void calcXYforAbs(  int news, int len, int bx, int by, int * dx, int * dy )
{

  int s, x, y;

  s = (int)((360.0 - news) + 90.0) % 360;

  x = len * cos( s * 0.01745 ); //  / 180 * PI
  y = len * sin( s * 0.01745 );
  // 320 x 240
  * dx = bx + x;
  * dy = by - y;
}

//
// https://arduinojson.org/v6/api/dynamicjsondocument/
//



//#define DEBUG_DRAW_MAP


#if 0

void drawMapShapeZ( char [] target , const String json_str, uint16_t color, int mode = DRAW_MAP_SHAPE_LINE )
{
  int i = 0;
  int odx = -1;
  int ody = -1;
  int dx, dy;

  //int ldx, ldy;
  int pack = 0;
  char buf[30];

  //StaticJsonDocument<200> filter;
  DynamicJsonDocument filter(200);

  for ( pack = 0; pack < 100; pack++ ) {
    i  = 0;
    odx = -1;
    ody = -1;
    filter.clear();
    sprintf( buf, "p%d", pack );
    if ( pack > 0 ) {
      //filter["map"][target][pack - 1] = false;
    }
    filter["map"][target][buf] = true;
#ifdef USE_MAPDATA_CLEAR
    mapData.clear();
#endif
    //DeserializationError err =
    deserializeJson( mapData, json_str, DeserializationOption::Filter( filter) );
    switch (err.code()) {
      case DeserializationError::Ok:
        //USE_SERIAL.println(F("Deserialization succeeded"));
        break;
      case DeserializationError::InvalidInput:
        dispErrorMsg( "Invalid input!" );
        break;
      case DeserializationError::NoMemory:

        dispErrorMsg( "Not enough memory" );
        break;
      default:
        USE_SERIAL.println(F("Deserialization failed"));
        USE_SERIAL.println( json_str );
        break;
    }
    USE_SERIAL.print( target );
    USE_SERIAL.print( " ");
    USE_SERIAL.print( pack );

    USE_SERIAL.print( " = " );
    USE_SERIAL.println( mapData.memoryUsage());

    //serializeJsonPretty(mapData, Serial);

    while ( mapData["map"][target][buf][i][0] != -1 ) {
      dx = mapData["map"][target][buf][i][0];
      dy = mapData["map"][target][buf][i][1];
#ifdef DEBUG_DRAW_MAP
      USE_SERIAL.print( dx );
      USE_SERIAL.print( "," );
      USE_SERIAL.print( dy );
      USE_SERIAL.println( "" );
#endif
      // memory over の処理 :たぶん (0,0) (0,0)となっている
      if ( dx == 0 && dy == 0 ) {
        int nx, ny;
        nx = mapData["map"][target][buf][i + 1][0];
        ny = mapData["map"][target][buf][i + 1][1];
        if ( nx == 0 && ny ==  0 ) {
          USE_SERIAL.print( F("may be not enough memory : ") );
          USE_SERIAL.println( target );
          //continue;
          return;
        }


      }
      if ( dx == -2 && dy == -2 ) { // end of road
        odx = -1;
        ody = -1;
      } else {
        if ( odx == -1 && ody == -1)  {
#ifdef DEBUG_DRAW_MAP
          if ( ldx == dx && ldy == dy ) {
            USE_SERIAL.println( "dup draw pixel" );

          }
#endif
          VRAM.drawPixel( dx, dy, color );
#ifdef DEBUG_DRAW_MAP
          ldx = dx;
          ldy = dy;
#endif
        } else {
          if ( mode == DRAW_MAP_SHAPE_LINE ) {
            VRAM.drawLine( odx, ody, dx, dy, color );
#ifdef DEBUG_DRAW_MAP
            if ( ldx == dx && ldy == dy ) {
              USE_SERIAL.println( "dup draw Line" );
            }
            ldx = dx;
            ldy = dy;
#endif
          }
          else
            VRAM.drawPixel( dx, dy , color );
        }
        odx = dx;
        ody = dy;
      }
      i++;
      if ( i > 76800 ) {
        USE_SERIAL.println( F("Over map data self limit") );
        break;
      }
    }
  }
}
#endif

void drawMapShape( const char target[] , const String json_str, uint16_t color, int mode = DRAW_MAP_SHAPE_LINE )
{
  int i = 0;
  int odx = -1;
  int ody = -1;
  int dx, dy;
#ifdef DEBUG_DRAW_MAP
  int ldx, ldy;
#endif

  //USE_SERIAL.println( json_str );
  USE_SERIAL.print( target );
  USE_SERIAL.print( F(" = ") );
  StaticJsonDocument<200> filter;
  filter["map"][target] = true;
  filter["map"]["use_cache"] = true;
#ifdef USE_MAPDATA_CLEAR
  mapData.clear();
#endif
  DeserializationError err =  deserializeJson( mapData, json_str, DeserializationOption::Filter( filter) );

#if 0
  // 2021.12.23
  if ( err.code() != DeserializationError::Ok ) {
    USE_SERIAL.println( F(" error on DrawMapShape") );
    return;
  }
#endif
#if 1
  switch (err.code()) {
    case DeserializationError::Ok:
      //USE_SERIAL.println(F("Deserialization succeeded"));
      break;

    case DeserializationError::InvalidInput:
      dispErrorMsg( "Invalid input! on drawMapShape" );
      return;
      break;

    case DeserializationError::NoMemory:
      dispErrorMsg( "Not enough memory on drawMapShape" );
      return;
      break;

    case DeserializationError::IncompleteInput:
      USE_SERIAL.println(F("Deserialization ImcomplementInput on drawMapShape"));
      return;
      break;

    case DeserializationError::TooDeep:
      USE_SERIAL.println(F("Deserialization TooDeep on drawMapShape"));
      return;
      break;

    default:
      USE_SERIAL.println(F("Deserialization failed on drawMapShape"));
      USE_SERIAL.println( err.c_str());
      USE_SERIAL.println( json_str );
      return;
      break;
  }
#endif

  //#define DEBUG_DRAW_MAP

  // USE_SERIAL.print( F("= ") );
  USE_SERIAL.println( mapData.memoryUsage());


#if 1
  USE_SERIAL.print( F( "Map Data Use Cache is " ));
  USE_SERIAL.print((int) mapData["map"]["use_cache"] );
  USE_SERIAL.println( (mapData["map"]["use_cache"] > 0) ? "USE" : "none" );
  if ( mapData["map"]["use_cache"] > 0 ) {
    dispLcdColorBg( 0, 225, 1, TFT_WHITE, TFT_GREEN, "C" );
  } else {

    dispLcdColorBg( 0, 225, 1, TFT_WHITE, TFT_RED, "N" );
  }
#endif

  while ( mapData["map"][target][i][0] != -1 ) {
    dx = mapData["map"][target][i][0];
    dy = mapData["map"][target][i][1];
#ifdef DEBUG_DRAW_MAP
    USE_SERIAL.print( dx );
    USE_SERIAL.print( "," );
    USE_SERIAL.print( dy );
    USE_SERIAL.println( "" );
#endif
    // memory over の処理 :たぶん (0,0) (0,0)となっている
    if ( dx == 0 && dy == 0 ) {
      int nx, ny;
      nx = mapData["map"][target][i + 1][0];
      ny = mapData["map"][target][i + 1][1];
      if ( nx == 0 && ny ==  0 ) {
        USE_SERIAL.print( F("may be not enough memory : ") );
        USE_SERIAL.println( target );
        return;
      }


    }
    if ( dx == -2 && dy == -2 ) { // end of road
      odx = -1;
      ody = -1;
      yield(); // そこまで時間を要していないと思うが...
    } else {
      if ( odx == -1 && ody == -1)  {
#ifdef DEBUG_DRAW_MAP
        if ( ldx == dx && ldy == dy ) {
          USE_SERIAL.println( "dup draw pixel" );

        }
#endif
        VRAM.drawPixel( dx, dy, color );
#ifdef DEBUG_DRAW_MAP
        ldx = dx;
        ldy = dy;
#endif
      } else {
        if ( mode == DRAW_MAP_SHAPE_LINE ) {
          VRAM.drawLine( odx, ody, dx, dy, color );
#ifdef DEBUG_DRAW_MAP
          if ( ldx == dx && ldy == dy ) {
            USE_SERIAL.println( "dup draw Line" );
          }
          ldx = dx;
          ldy = dy;
#endif
        }
        else
          VRAM.drawPixel( dx, dy , color );
      }
      odx = dx;
      ody = dy;
    }
    i++;
    if ( i > 4000 ) {
      USE_SERIAL.println( F("Over map data self limit") );
      break;
    }
  }

}

int spdToLen( int spd )
{
  if ( spd < 1 )
    return 0;

  return (int) log10( spd ) * 25;  // 10km/h ->  25 100km/ -> 50

}

/*
   航空レーダーっぽく表示(笑)for Radar
*/
void dispCallsignWithInfo( const int x, const int y, const char * callsign, const int speed, const int alt , const int course,  const int aprstype )
{
  int dx, dy;
  int dx1, dy1, dx2, dy2;
  int offset_x = 0, offset_y = 0;

  int pr = 135;  // 情報を表示する方向

  if ( aprstype & 1 ) {
    M5.Lcd.fillCircle( x, y, 4, TFT_RED );
  } else {
    M5.Lcd.drawTriangle( x, y - 2, x - 2, y + 2, x + 2, y + 2, TFT_GREEN );
  }

  // 基本となる表示位置(方向)の設定
  if ( x > CX ) {
    if ( y < CY )
      pr = 45;
    else
      pr = 135;
  } else if ( y < CY )
    pr = 315;
  else
    pr = 215;

  // 進行方向にかぶる場合は修正
  if ( aprstype & 1 ) {
    if ( 90 <= course && course <= 200 ) {
      pr = 45;
    }
  }

  // 表示方向に合わせた位置のオフセット設定
  if ( pr < 90 ) {
    offset_y = -14;
  } else if ( pr < 180 ) {

  } else if ( pr < 270 ) {
    offset_x = -49;
  } else {
    offset_x = -49;
    offset_y = -14;
  }

  /*
     移動局の場合は,追加情報を表示
  */
  if ( aprstype & 1 ) {
    calcXYforArrow( pr, 10, x, y, &dx1, &dy1 );
    calcXYforArrow( pr, 20, x, y, &dx2, &dy2 );
    calcXYforArrow( pr, 23, x, y, &dx, &dy );

    M5.Lcd.drawLine( dx1, dy1, dx2, dy2, TFT_DARKGREEN );
  } else {
    dx = x + 8;
    dy = y + 8;
  }

  dy += offset_y;
  dx += offset_x;

  // future
#define RADAR_CALLSIGN_SIZE 1
#define RADAR_FIXSTATION_SIZE 1
#define RADAR_INFO_SIZE 1

  //dispLcdColorTransparent( dx, dy, (((aprstype & 1 ) == 1 )? RADAR_CALLSIGN_SIZE : RADAR_FIXSTATION_SIZE ), TFT_GREEN, "%s", callsign );
#if 0
  if ( aprstype & 1 ) {
    dispLcdColorTransparent( dx, dy,  RADAR_CALLSIGN_SIZE, TFT_GREEN, "%s", callsign );
  } else {
    dispLcdColorTransparent( dx, dy,  RADAR_FIXSTATION_SIZE, TFT_GREEN, "%s", callsign );
  }
#else
  dispLcdColorTransparent( dx, dy,  RADAR_CALLSIGN_SIZE, TFT_GREEN, "%s", callsign );
#endif
  if ( !(aprstype & 1) )
    return;

  dy = dy + 11;
  dispLcdColorTransparent( dx, dy, RADAR_INFO_SIZE, TFT_GREEN, "%03d %03d", alt, speed );


}
/*
   周辺局の描画
*/
void drawMapLocalStations( const String json_str )
{

  int i;
  float news;
  float dist;
  int course, spd, alt;
  //int x, y;
  int dx, dy;
  int odx, ody;
  int aprstype;
  int fn;
  StaticJsonDocument<200> filter;

  filter["near"] = true;
  filter["footprint"] = true;
  filter["cnt"] = true;
#ifdef USE_MAPDATA_CLEAR
  mapData.clear();
#endif
  DeserializationError err =
    deserializeJson( mapData, json_str, DeserializationOption::Filter( filter) );

  // 2021.12.23
#if 1
  if ( err.code() != DeserializationError::Ok ) {
    USE_SERIAL.println( F(" error on drawMapLocalStations") );
    USE_SERIAL.println( json_str );
    return;
  }
#endif

  int n = mapData["cnt"];
  USE_SERIAL.print( F("Local station N is "));
  USE_SERIAL.println( n );

  for ( i = 0 ; i < n ; i++ ) {
    const char * callsign =  mapData["near"][i]["callsign"];
    news = mapData["near"][i]["drcs"];
    dist = mapData["near"][i]["dists"];
    aprstype = mapData["near"][i]["aprstype"];
    course = mapData["near"][i]["course"];
    spd = mapData["near"][i]["spd"];
    alt = mapData["near"][i]["alt"];
    /*
      周辺局の現在位置をプロット
    */
    calcXY( news, dist, &dx, &dy );

    switch ( displayMode ) {
      case DISPLAY_MODE_MAP:
        if ( spd > 4 ) {
          M5.Lcd.fillCircle( dx, dy,  ( aprstype & 1 ) ? 5 : 3 /* size */ , TFT_RED );
        } else {
          M5.Lcd.drawCircle( dx, dy,  ( aprstype & 1 ) ? 5 : 3 /* size */ , TFT_RED );

        }
        dispCallsignMap( dx + 4, dy - 3, ( aprstype & 1 ) ? 2 : 1 , TFT_WHITE, "%s", callsign );

        break;

      case DISPLAY_MODE_RADAR:

        dispCallsignWithInfo( dx, dy, callsign, spd, alt, course, aprstype );

    }
    if ( aprstype & 1 ) {
      /* 進行ベクトル */
      int dx1, dy1;
      int dx2, dy2, dx3, dy3;
      int as = 150; // 180 - 30
      const int arrow_size = 10;

      calcXYforArrow( course, spdToLen( spd ), dx, dy, &dx1, &dy1 );
      M5.Lcd.drawLine( dx, dy, dx1, dy1, TFT_RED );
      if ( displayMode == DISPLAY_MODE_MAP ) {
        calcXYforArrow( course + as, arrow_size, dx1, dy1, &dx2, &dy2 );
        calcXYforArrow( course - as, arrow_size, dx1, dy1, &dx3, &dy3 );
        M5.Lcd.fillTriangle( dx1, dy1, dx2, dy2, dx3, dy3, TFT_RED );
      }
    }



    /* footprint */
    if ( displayMode == DISPLAY_MODE_MAP ) {
      fn = mapData["footprint"][i]["cnt"];
      if ( aprstype & 1 ) {
        for ( int j = 0 ; j < fn ; j++ ) {
          news = mapData["footprint"][i]["data"][j]["drcs"];
          dist = mapData["footprint"][i]["data"][j]["dists"];

          calcXY( news, dist, &dx, &dy );

          if ( j == 0 ) {
            M5.Lcd.drawPixel( dx, dy,  COLOR_OTHER_FOOTPRINT );
          } else {
            M5.Lcd.drawLine( odx, ody, dx, dy, COLOR_OTHER_FOOTPRINT );
          }
          odx = dx;
          ody = dy;
        }
      }
    }

  }
}


void dispErrorMsg( const char * msg )
{
  USE_SERIAL.println( msg );
  dispLcdColor( 0, 220, 2, TFT_RED, "%s", msg );
  delay( 1000 );
}

void drawArrow( int x, int y, int drc, int speed , uint16_t color )
{
  int dx, dy;

  calcXYforArrow( drc, speed, x, y,  &dx, &dy );
  M5.Lcd.drawLine( x, y, dx, dy, color );
}




/*
   現在位置をプロット 進行方向,速度
*/
void drawCurrentPositionNewsSpeed( int news, int spd )
{
  int r = 15;  // 自分の位置を示す円の半径
  int x1, y1, x2, y2;
  int x3, y3, x4, y4;
  int as = 150; // 180 - 30 //  矢印の角度
  const int arrow_size = 15;
  int  len = spdToLen( spd );
  /*
    news = 219;
    spd = 100;
  */
  M5.Lcd.drawCircle( CX, CY, r, COLOR_MY_POSITION );
  if ( r >= 10 ) {
    M5.Lcd.fillCircle( CX, CY, 2, COLOR_MY_POSITION );
  }
  // 速度が 2km/hを超える場合のみベクトルを表示する
  if ( spd > 2 ) {
    calcXYforArrow( news, r, CX, CY, &x1, &y1 );
    calcXYforArrow( news, r + len, CX, CY, &x2, &y2 );
    M5.Lcd.drawLine( x1, y1, x2, y2, COLOR_MY_POSITION );

    calcXYforArrow( news + as, arrow_size, x2, y2, &x3, &y3 );
    calcXYforArrow( news - as, arrow_size, x2, y2, &x4, &y4 );
    M5.Lcd.fillTriangle( x2, y2, x3, y3, x4, y4, COLOR_MY_POSITION );
  } else {
    M5.Lcd.fillCircle( CX, CY, (int)(r / 2), COLOR_MY_POSITION );

  }

  //dispLcdColor( CX + 4, CY + 4, 1, TFT_WHITE, "%d %d", news, spd );
}

/*

*/
void drawCurrentPositionGps()
{
#ifdef USE_GPS
  if ( gps.location.isValid()) {
    int news = gps.course.deg();
    int spd  = gps.speed.kmph();

    drawCurrentPositionNewsSpeed( news, spd );
  } else {
    drawCurrentPositionNewsSpeed( my_course, my_speed );

  }
#else
  drawCurrentPositionNewsSpeed( my_course, my_speed );

#endif
}

/*
  void drawCurrentPositionMyData()
  {
  drawCurrentPositionNewsSpeed( my_course, my_speed );
  }
*/

/*
  方位磁針を描画
*/
void drawCompassRaw( int cx, int cy, int r, int news )
{
  int x, y, x1, y1, x2, y2;

  M5.Lcd.drawCircle( cx, cy, r, TFT_LIGHTGREY );

  calcXYforAbs( news + 90, r / 3, cx, cy, &x1, &y1 );
  calcXYforAbs( news - 90, r / 3, cx, cy, &x2, &y2 );

  calcXYforAbs( news + 180     , r    , cx, cy, &x , &y  );
  M5.Lcd.fillTriangle( x, y, x1, y1, x2, y2, TFT_LIGHTGREY );

  calcXYforAbs( news     , r    , cx, cy, &x , &y  );
  M5.Lcd.fillTriangle( x, y, x1, y1, x2, y2, TFT_RED );

}

void drawCompass()
{
  int cx = 20;
  int cy = 27;
  int r = 15;     // 方位磁針の大きさ(半径)

#ifdef NORTH_UP
  int news = 0;
#else
  int news  = ( north_up ? 0 : (360 - mapHeading));
#endif
  drawCompassRaw( cx, cy, r, news );
}

/*
   地図モード、レーダモード 主ルーチン
*/
void showNear( const String str )
{
  // int i;
  float news;
  // int x, y;
  int dx, dy;
  int odx, ody;
  float dist;
  //float s;
  //int aprstype;
  int fn;


  M5.Lcd.startWrite();

  clearDisplay();
  dispStatus();

  // 画面中央の垂直/水平線
  M5.Lcd.drawLine( CX, 0, CX, 240, M5.Lcd.color565( 70, 70, 50 ));
  M5.Lcd.drawLine( 0, CY, 320, CY, M5.Lcd.color565( 70, 70, 50 ));

  // 方位磁針
  drawCompass();


  //   drawMap Shape
  if ( displayMode == DISPLAY_MODE_MAP ) {
#ifdef USE_SPRITE
    vram.fillSprite( TFT_BLACK );
#endif
    drawMapShape( "coast", str, COLOR_COAST, DRAW_MAP_SHAPE_DOT  );
    drawMapShape( "road",  str, COLOR_ROAD,  DRAW_MAP_SHAPE_LINE );
#ifdef USE_SPRITE
    //clearDisplay();
    vram.pushSprite( 0, 0 );

#endif
  }


  // 近隣局を描画
  drawMapLocalStations( str );

  // 自局を描画
  // 自分の足跡
  StaticJsonDocument<200> filter;
  filter["myfootprint"] = true;
#ifdef USE_MAPDATA_CLEAR
  mapData.clear();
#endif
  DeserializationError err = deserializeJson( mapData, str, DeserializationOption::Filter( filter) );
#if 1
  switch (err.code()) {
    case DeserializationError::Ok:
      USE_SERIAL.println(F("Deserialization succeeded on showNear" ));
      break;
    case DeserializationError::InvalidInput:
      dispErrorMsg( "Invalid input!" );
      break;
    case DeserializationError::NoMemory:

      dispErrorMsg( "Not enough memory" );
      break;
    default:
      USE_SERIAL.print(F("Deserialization failed"));
      break;
  }
#endif

  USE_SERIAL.print( "myfootprint = " );
  USE_SERIAL.println( mapData.memoryUsage());
  /* myfootprint */
  fn = mapData["myfootprint"]["cnt"];
  for ( int j = 0 ; j < fn ; j++ ) {

    news = mapData["myfootprint"]["data"][j]["drcs"];
    dist = mapData["myfootprint"]["data"][j]["dists"];

    calcXY( news, dist, &dx, &dy );


    if ( j == 0 ) {
      M5.Lcd.drawPixel( dx, dy,  COLOR_MY_FOOTPRINT );
    } else {
      M5.Lcd.drawLine( odx, ody, dx, dy, COLOR_MY_FOOTPRINT );
    }
    odx = dx;
    ody = dy;
  }

  // 自局の現在位置を描画
  drawCurrentPositionGps();


  dispStatus();


  M5.Lcd.endWrite();
  smartDelay( 1 );
}





// This custom version of delay() ensures that the gps object
// is being "fed".
static void smartDelay(unsigned long ms)
{
  unsigned long start = millis();
  do
  {
#ifdef USE_GPS
    while (ss.available())
      gps.encode(ss.read());
#endif
    delay( 1 ); // for task switch
  } while (millis() - start < ms);
}

#define BUFSIZE 1024
#define BUFLEN  1023

void dispLcd( uint16_t x, uint16_t y, uint16_t size, const char * fmt, ... )
{
  char buf[BUFSIZE];

  va_list arg;
  va_start(arg, fmt );
  vsnprintf( buf, BUFLEN, fmt, arg );
#if defined( USE_M5STACK )
  M5.Lcd.setCursor( x, y );
  M5.Lcd.setTextSize( size );
  M5.Lcd.print( buf );
#endif
#if defined( USE_LCD )
  display.setCursor( x, y );
  display.setTextSize( size );
  display.print( buf );
  display.display();
#endif

  va_end(arg);
}

void dispLcdColorTransparent( uint16_t x, uint16_t y, uint16_t size, uint16_t color, const char * fmt, ... )
{
  char buf[BUFSIZE];

  va_list arg;
  va_start(arg, fmt );
  vsnprintf( buf, BUFLEN, fmt, arg );
#if defined( USE_M5STACK )
  M5.Lcd.setTextColor( color );
  M5.Lcd.setCursor( x, y );
  M5.Lcd.setTextSize( size );
  M5.Lcd.setTextWrap( 0 );
  M5.Lcd.print( buf );
#endif

  va_end(arg);
}

void dispLcdColor( uint16_t x, uint16_t y, uint16_t size, uint16_t color, const char * fmt, ... )
{
  char buf[BUFSIZE];

  va_list arg;
  va_start(arg, fmt );
  vsnprintf( buf, BUFLEN, fmt, arg );
#if defined( USE_M5STACK )
  M5.Lcd.setTextColor( color, TFT_BLACK  );
  M5.Lcd.setCursor( x, y );
  M5.Lcd.setTextSize( size );
  M5.Lcd.print( buf );
#endif
#if defined( USE_LCD )
  display.setTextColor( color, TFFT_BLACK  );
  display.setCursor( x, y );
  display.setTextSize( size );
  display.print( buf );
  display.display();
#endif

  va_end(arg);
}

void dispLcdColorCenter( uint16_t y, uint16_t size, uint16_t color, const char * fmt, ... )
{
  int x;
  char buf[BUFSIZE];

  va_list arg;
  va_start(arg, fmt );
  vsnprintf( buf, BUFLEN, fmt, arg );

  x = 160 - strlen( buf ) / 2.0 * 6 * size;

  M5.Lcd.setTextColor( color, TFT_BLACK  );
  M5.Lcd.setCursor( x, y );
  M5.Lcd.setTextSize( size );
  M5.Lcd.print( buf );
  va_end(arg);
}

void dispLcdColorBg( uint16_t x, uint16_t y, uint16_t size, uint16_t color, uint16_t bgcolor, const char * fmt, ... )
{
  char buf[BUFSIZE];

  va_list arg;
  va_start(arg, fmt );
  vsnprintf( buf, BUFLEN, fmt, arg );
#if defined( USE_M5STACK )
  M5.Lcd.setTextColor( color, bgcolor );
  M5.Lcd.setCursor( x, y );
  M5.Lcd.setTextSize( size );
  M5.Lcd.print( buf );
#endif
#if defined( USE_LCD )
  display.setTextColor( color, bgcolor );
  display.setCursor( x, y );
  display.setTextSize( size );
  display.print( buf );
  display.display();
#endif

  va_end(arg);
}

void dispCallsignMap( uint16_t x, uint16_t y, uint16_t size, uint16_t color, const char * fmt, ... )
{
  char buf[BUFSIZE];

  va_list arg;
  va_start(arg, fmt );
  vsnprintf( buf, BUFLEN, fmt, arg );
#if defined( USE_M5STACK )
  M5.Lcd.setTextColor( color  );
  M5.Lcd.setCursor( x, y );
  M5.Lcd.setTextSize( size );
  M5.Lcd.setTextWrap( 0 );
  M5.Lcd.print( buf );
#endif
#if defined( USE_LCD )
  display.setTextColor( color, TFT_BLACK  );
  display.setCursor( x, y );
  display.setTextSize( size );
  display.print( buf );
  display.display();
#endif

  va_end(arg);
}

uint16_t calcColor( uint8_t r, uint8_t g, uint8_t b, int st )
{
  // TODO: シグモイド関数を使ったほうがなめらかに表現できるかも...
  float rate = 100.0;

  rate -= ( st / 600.0 ) * 100;
  rate = max( (int)rate, 30 );

  rate /= 100;

  r *= rate;
  g *= rate;
  b *= rate;

  return M5.Lcd.color565( r, g, b );
}

/*
   近隣局一覧
*/
void showList( const String str )
{
#ifdef USE_MAPDATA_CLEAR
  mapData.clear();
#endif
  DeserializationError err =
    deserializeJson( mapData, str );

  // 2021.12.23
#if 1
  if ( err.code() != DeserializationError::Ok ) {
    USE_SERIAL.println( F(" error on showList()") );
    return;
  }
#endif
#if defined( YOUNG_HAM )

  const int lineHeight = 20;
  const int callsignSize = 2;
  const int offsetDistBar = 16;
  const int widthCallsign = 108; // 2 x 6 x 9

#elif defined( OLD_HAM )

  const int lineHeight = 40;
  const int callsignSize = 4;
  const int offsetDistBar = 32;
  const int widthCallsign = 216; // 4 x 6 x 9
#else

  const int lineHeight = 30;
  const int callsignSize = 3;
  const int offsetDistBar = 24;
  const int widthCallsign = 162; // 3 x 6 x 9
#endif

  int n = mapData["cnt"];
  int i;
  float news;
  float dist = 0;
  int course, spd;
  int aprstype;
  int x, y;
  int w;
  int dx1, dy1, dx2, dy2, dx3, dy3;
  int st;
  int drc_diff;
  uint16_t distBarColor;
  const int news_size = 20;
  USE_SERIAL.println( n );

  y = 15;
  clearDisplay();

  for ( i = 0 ; i < n ; i++ ) {
    const char * callsign =  mapData["near"][i]["callsign"];
    aprstype = mapData["near"][i]["aprstype"];

    if ( display_list_target ) {
      if ( !(aprstype & 1))
        continue;
    }
    news = mapData["near"][i]["drcs"];
    dist = mapData["near"][i]["dists"];
    course = mapData["near"][i]["course"];
    spd = mapData["near"][i]["spd"];
    st = mapData["near"][i]["st"];



    // 接近 or 離反の判定
    drc_diff = ( ((int)news + 180 ) - course + 360 ) % 360;
    if ( drc_diff <= 60 || 300 <= drc_diff ) {
      // 接近
      distBarColor = TFT_GREEN;
    } else if ( drc_diff <= 120 || 240 <= drc_diff ) {
      // ??難しい判断
      distBarColor = TFT_YELLOW;
    } else {
      // 離反
      distBarColor = TFT_RED;
    }


    // コールサイン (SSID)
    //  font-width 6px   height 7px;
    x  = 0;
#ifdef LIST_MODE_COLOR
    dispLcdColor( x, y, callsignSize, (aprstype & 1 ) ? distBarColor : TFT_LIGHTGREY, "%-9s", callsign );
#else
    dispLcdColor( x, y, callsignSize, (aprstype & 1 ) ? TFT_WHITE : TFT_LIGHTGREY, "%-9s", callsign );
#endif
    x += widthCallsign;


    // 距離
    if ( dist < 10 ) {
      dispLcdColor( x, y, 2, TFT_YELLOW, "%4d  ", (int)(dist * 1000 ));
      x += 48;
    } else {
      dispLcdColor( x, y, 2, TFT_WHITE, "%4d", (int)dist );
      x += 48;
      dispLcdColor( x, y + 10, 1, TFT_DARKGREY,  "km" );
    }
    if ( dist > 0 ) {
      w = 180 - log10( dist ) * 138;
    } else {
      w = 320;
    }
    w = max( 10, w );
    // M5.Lcd.fillRect( w, y + 24, 320, 4,  TFT_BLACK ); // クリアー
    // 距離を示す Bar
#ifdef LIST_MODE_COLOR
    M5.Lcd.fillRect( 0, y + offsetDistBar, w, 4 , TFT_WHITE );
#else
    M5.Lcd.fillRect( 0, y + offsetDistBar, w, 4 , distBarColor );
#endif
    x += 25;

    calcXYforArrow( news,       news_size / 2, x, y + 10, &dx1, &dy1 );
    calcXYforArrow( news + 155, news_size / 2, x, y + 10, &dx2, &dy2 );
    calcXYforArrow( news - 155, news_size / 2, x, y + 10, &dx3, &dy3 );
    M5.Lcd.fillRect( x - news_size / 2, y , news_size, news_size, TFT_BLACK );
    M5.Lcd.fillTriangle( dx1, dy1, dx2, dy2, dx3, dy3, calcColor( 255, 255, 0, st ));
    //M5.Lcd.drawLine( x, y + 15, dx1, dy1, TFT_YELLOW );
    x += news_size;
    x -= 5;

    //
    // 速度を表示
    if ( aprstype & 1 ) { // 移動局のみ速度を表示
#if 0
      dispLcdColor( x, y, 2, TFT_GREEN, "%3d", spd );
#endif
#if 0
      {
        const int gh = 16;
        const int gw = 30;

        float spd_rate = (float)spd / 100.0;
        //M5.Lcd.fillRect( x, y,  gw,  gh, TFT_BLACK);          // クリアー
        M5.Lcd.drawLine( x, y + gh, x + gw , y + gh, TFT_DARKGREEN ); // 下線
        M5.Lcd.fillTriangle( x, y + gh,  x + gw * spd_rate, y + gh, x + gw * spd_rate, y + gh - gh * spd_rate ,  TFT_GREEN );
      }
#endif
#if 0
      // 独立して進行方向と速度をグラフィック表示
      {

        const int gh = 21;
        const int gw = 21;
        int cx, cy;

        cx = x + gw / 2;
        cy = y + gh / 2;

        //USE_SERIAL.println( x );
        //USE_SERIAL.println( x + gw );
        calcXYforArrow( course,       min( spd, gw / 2 ), cx, cy, &dx2, &dy2 );
        calcXYforArrow( course + 180, min( spd, gw / 2 ), cx, cy, &dx1, &dy1 ); // 基点
        M5.Lcd.drawLine( dx1, dy1, dx2, dy2, TFT_RED );
        M5.Lcd.fillCircle( dx1, dy1, 3, TFT_RED );

        //M5.Lcd.drawRect( x, y, gw,  gh , TFT_GREEN );
      }
#endif
#if 1
      //  相対的 方角の上進行方向,速度をグラフィックでオーバーレイで表示
      {
        x -= news_size;
        x -= 5;
        const int gh = 21;
        const int gw = 21;
        int cx, cy;

        cx = x + gw / 2;
        cy = y + gh / 2;

        //USE_SERIAL.println( x );
        //USE_SERIAL.println( x + gw );
        calcXYforArrow( course,       min( spd, gw ), cx, cy, &dx2, &dy2 );
        // calcXYforArrow( course + 180, min( spd, gw / 2 ), cx, cy, &dx1, &dy1 ); // 基点
        if ( spd >= 2 ) {
          M5.Lcd.drawLine( cx, cy, dx2, dy2, TFT_RED );

          M5.Lcd.fillCircle( cx, cy, 3, TFT_RED );
        } else {
          M5.Lcd.drawCircle( cx, cy, 3, TFT_RED );
        }
        //M5.Lcd.drawRect( x, y, gw,  gh , TFT_GREEN );
      }
#endif
#if 0
      // for DEBUG
      dispLcdColor( 260, y, 2, TFT_WHITE, "%3d", drc_diff );
#endif
      // 1局分終わり
    }
    y += lineHeight;

  }
}

/*
   Graph Functions

*/

///////////////////////
// グラフ
///////////////////////

#define GRAPH_X 0
#define GRAPH_Y 115
#define GRAPH_WIDTH 320
#define GRAPH_HEIGHT 100
#define GRAPH_SEED 3
#define GRAPH_DATA_MAX (GRAPH_WIDTH-2)

#define GRAPH_INTERVAL 5

#define SEED_TEMPERATURE 0
#define SEED_HUMIDITY    1
#define SEED_PRESSURE    2

#define COLOR_TEMPERATURE COLOR_SPEED
#define COLOR_HUMIDITY    COLOR_ALTITUDE
#define COLOR_PRESSURE    COLOR_COURSE

#define COLOR_INC         TFT_ORANGE
#define COLOR_DEC         TFT_CYAN

#define DATA_STACK 600

time_t now()
{
  time_t t;

  time( &t );
  return t;
}

void initGraph()
{
#if defined( USE_M5STACK )
  M5.Lcd.fillRect( GRAPH_X, GRAPH_Y, GRAPH_WIDTH, GRAPH_HEIGHT, TFT_BLACK );
  M5.Lcd.drawRect( GRAPH_X, GRAPH_Y, GRAPH_WIDTH, GRAPH_HEIGHT, TFT_WHITE );
#endif
}

void clearGraph()
{
#if defined( USE_M5STACK )
  M5.Lcd.fillRect( GRAPH_X + 1, GRAPH_Y + 1, GRAPH_WIDTH - 2, GRAPH_HEIGHT - 2, TFT_BLACK );
#endif
}

double graphData[GRAPH_DATA_MAX][GRAPH_SEED];
uint16_t graphDataN = 0;

/*
    一定間隔でデータを保存する
    保存した場合は true, 無視(時間内)の場合は falseを返す
*/
bool setGraphDataInterval(  double temperature, double humidity, double pressure )
{
  static time_t nextTime = 0L;

  // mills()は使えない(50日でロールバックするため
  if ( nextTime > now())
    return false;

  nextTime = now() + GRAPH_INTERVAL; // グラフの描画間隔
  setGraphData( temperature, humidity, pressure );

  return true;
}

void setGraphData( double temperature, double humidity, double pressure )
{
  if ( temperature <= 1.0 )
    temperature = 0.0;

  if ( graphDataN < GRAPH_DATA_MAX ) {

  } else {
    int i, j;
    for ( i = 0 ; i < (GRAPH_DATA_MAX - 1); i++ ) {
      for ( j = 0; j < GRAPH_SEED; j++ ) {
        graphData[i][j] = graphData[i + 1][j];
      }
    }
    graphDataN--;
  }
  //USE_SERIAL.println( graphDataN  );
  graphData[graphDataN][SEED_TEMPERATURE] = temperature;
  graphData[graphDataN][SEED_HUMIDITY]    = humidity;
  graphData[graphDataN][SEED_PRESSURE]    = pressure;
  graphDataN++;

}

#define GRAPH_DOT  1
#define GRAPH_BAR  2
#define GRAPH_LINE 3

void drawGraphAll()
{
  clearGraph();
  /*
     Barを先に描画する
  */
  drawGraph( SEED_HUMIDITY,    COLOR_HUMIDITY    , GRAPH_BAR);  // Altitude

  drawGraph( SEED_TEMPERATURE, COLOR_TEMPERATURE , GRAPH_DOT);  // Speed

  drawGraph( SEED_PRESSURE,    COLOR_PRESSURE    , GRAPH_DOT);  // Course
}

void drawGraph(uint16_t seed, uint16_t color, int graph_type )
{
  double max = -10000.0;
  double min = 10000.0;
  int x, y;
  int h;
  for ( x = 0; x < graphDataN; x++ ) {
    if ( max < graphData[x][seed] )
      max = graphData[x][seed];
    if ( min > graphData[x][seed] )
      min = graphData[x][seed];
  }

  if ( max == min ) {
    max += 0.5;
    min -= 0.5;
  } else {
    max += 0.1;
    min -= 0.1;

  }

  for ( x = 0 ; x < graphDataN; x++ ) {
    h = ((graphData[x][seed] - min) / (max - min ) * GRAPH_HEIGHT);
    y = GRAPH_Y + GRAPH_HEIGHT - h;
    //USE_SERIAL.print( y ); USE_SERIAL.print( "," );
#if defined( USE_M5STACK )
    switch ( graph_type ) {
      case GRAPH_BAR:
        if ( 1 ) {
          M5.Lcd.drawFastVLine( x + 1, y + 3, h - 3 , /*color*/ COLOR_ALTITUDE2);
        }
      case GRAPH_DOT:
        M5.Lcd.fillCircle( x + 1, y, 1 /* size */ , color );
        if ( 0 ) {
          M5.Lcd.drawPixel( x + 1, y    , color );
          M5.Lcd.drawPixel( x + 1, y + 1, color );
          M5.Lcd.drawPixel( x + 1, y + 2, color );
          M5.Lcd.drawPixel( x + 2, y    , color );
          M5.Lcd.drawPixel( x + 2, y + 1, color );
          M5.Lcd.drawPixel( x + 2, y + 2, color );
          M5.Lcd.drawPixel( x    , y    , color );
          M5.Lcd.drawPixel( x    , y + 1, color );
          M5.Lcd.drawPixel( x    , y + 2, color );
        }

        break;

      case GRAPH_LINE:

        break;
    }
#endif
  }
  //USE_SERIAL.println( "" );

}


#if 0

static void printFloat(float val, bool valid, int len, int prec)
{
  if (!valid)
  {
    while (len-- > 1)
      M5.Lcd.print('*');
    M5.Lcd.print(' ');
  }
  else
  {
    M5.Lcd.print(val, prec);
    int vi = abs((int)val);
    int flen = prec + (val < 0.0 ? 2 : 1); // . and -
    flen += vi >= 1000 ? 4 : vi >= 100 ? 3 : vi >= 10 ? 2 : 1;
    for (int i = flen; i < len; ++i)
      M5.Lcd.print(' ');
  }
  smartDelay(0);
}

static void printInt(unsigned long val, bool valid, int len)
{
  char sz[32] = "*****************";
  if (valid)
    sprintf(sz, "%ld", val);
  sz[len] = 0;
  for (int i = strlen(sz); i < len; ++i)
    sz[i] = ' ';
  if (len > 0)
    sz[len - 1] = ' ';
  M5.Lcd.print(sz);
  smartDelay(0);
}

#endif

static void printDateTime(TinyGPSDate & d, TinyGPSTime & t)
{
  M5.Lcd.setTextColor( TFT_WHITE, TFT_BLACK );
  if (!d.isValid())
  {
    M5.Lcd.print(F("********** "));
  }
  else
  {
    char sz[32];
    sprintf(sz, "%02d/%02d/%02d ", d.year(), d.month(), d.day());
    M5.Lcd.print(sz);
  }

  if (!t.isValid())
  {
    M5.Lcd.print(F("******** "));
  }
  else
  {
    char sz[32];
    sprintf(sz, "%02d:%02d:%02d ", t.hour(), t.minute(), t.second());
    M5.Lcd.print(sz);
  }

  //printInt(d.age(), d.isValid(), 5);
  // smartDelay(0);
}

#if 0
static void printStr(const char *str, int len)
{
  int slen = strlen(str);
  for (int i = 0; i < len; ++i)
    M5.Lcd.print(i < slen ? str[i] : ' ');
  smartDelay(0);
}
#endif
印刷日: 2024-05-20 15:33:27
User:
URL: https://ketaitracker.info/ktm5stack/index.php?page=sketch