No Code AI(肉寇)AI自動化兩日精通|實體6小時+線上6小時
|

AMEBA透過網路校時RTC時鐘模組

   

作者:曹永忠

前文介紹 RTC 時鐘模組具備時間功能,並且為了斷電時仍然可以保留時間,補足Ameba開發板並沒有內置時鐘(Internal Clock)的問題,可以見到 「Tiny RTC I2C 時鐘模組」的外觀圖,模組採用DS1307晶片,若讀者需要更詳盡的資料,請參考「Arduino投幣計時器(網路篇)」,內容關於RTC 時鐘模組。

RTC時鐘模組的介紹與描述

Tiny RTC I2C 時鐘模組

Tiny RTC I2C 時鐘模組

如下圖所示,我們可以參考時鐘模組之電路連接圖,先將電路連接完善後,撰寫與測試下列「Tiny RTC I2C 時鐘模組」測試程式。

時鐘模組電路連接方式

時鐘模組電路連接方式

在完成上圖所示之時鐘模組之電路連接之後,如下圖所示,完成時鐘模組電路實際組裝的工作。

時鐘模組電路實際組裝圖

時鐘模組電路實際組裝圖

RTC模組的校時問題

我們發現,若裝設新的開發板或不同時間點設置新裝置,必須透過使用者重新設定時間,或是修改程式;因為「RTC 時鐘模組」的初始化時間是寫在Ameba開發板的程式內,必須重新編譯程式並上傳,這樣是非常不方便的。

若我們具有自動校時的功能,完全不需使用者手動設定或重新更新系統軟體,就可以達到正確時間校時的機制,這樣才能更簡單的產業化應用,由於目前網路校時非常的方便,所以我們可以透過網路校時的方式來實踐這個機制,或許是一個相當完善的解決方案。

Ameba的WiFi功能

Ameba開發板是一塊「IOT Wi-Fi微型化模組」(RTL8711AF and RTL8195AM),內建 ARM Cortex-M3 CPU、記憶體,同時還配置了完整的無線網路協議,包含:SSL硬體加速電路、UART、 I2C、 SPI、PWM ,以及高速的 SDIO 接口等各式序列介面(如下圖所示)。

Ameba開發板核心晶片「RTL8195AM」規格

Ameba開發板使用「RTL8195AM」為開發板核心晶片,功能強大,其下為晶片的基本規格:

  • 32-bit 166MHz ARM Cortex-M3 CPU
  • 內建 低功耗 802.11 b/g/n 2.4G 無線 Wi-Fi
  • 內建 NFC
  • 介面支援 : GPIO / PWM / SPI / I2C / ADC / DAC / UART
  • Crypto HW engine : 可做硬體加解密, 支援 MD5/ SHA-1 / SHA2-256 / DES / 3DES / AES
  • IC 本身有 512K RAM, 另外模組包含 2M SDRAM / 16M bit flash

Ameba開發板的規格

Ameba開發板具有強大的功能,並內含WiFi上網功能,其下為基本規格:

  • 與Arduino UNO開發板相容,可支援大多數 Arduino 擴充板(Shield),如DfRobot的 LCD Keypad shield…等等
  • 含一個 NXP LPC11U35 cortex-M0 IC,具備下列功能:
  1. 不須使用 JLINK 可直接透過 USB傳入程式image檔
  2. 不須使用USB序列傳輸線,UART即可使用將訊息傳給開發用的電腦

取得網路校時時間資料

將Ameba開發板的驅動程式安裝好之後,我們打開開發工具「Sketch IDE」整合開發軟體撰寫一段程式,如下表所示之網路校時測試程式,就可以透過Ameba WiFi模組取得網路校時時間。

網路校時測試程式(UdpNtpClient)


#include "PMType.h"
#include
#include
#include
uint8_t MacData[6];

char ssid[] = "TSAO";      // your network SSID (name)
char pass[] = "TSAO1234";     // your network password
char server[] = "gpssensor.ddns.net"; // the MQTT server of LASS

#define MAX_CLIENT_ID_LEN 10
#define MAX_TOPIC_LEN     50
char clientId[MAX_CLIENT_ID_LEN];
char outTopic[MAX_TOPIC_LEN];

IPAddress  Meip ,Megateway ,Mesubnet ;
String MacAddress ;
int status = WL_IDLE_STATUS;
WiFiUDP Udp;
const char ntpServer[] = "pool.ntp.org";
const long timeZoneOffset = 28800L;
const int NTP_PACKET_SIZE = 48; // NTP time stamp is in the first 48 bytes of the message
const byte nptSendPacket[ NTP_PACKET_SIZE] = {
  0xE3, 0x00, 0x06, 0xEC, 0x00, 0x00, 0x00, 0x00,  0x00, 0x00, 0x00, 0x00, 0x31, 0x4E, 0x31, 0x34,
  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
};
byte ntpRecvBuffer[ NTP_PACKET_SIZE ];

#define LEAP_YEAR(Y)     ( ((1970+Y)>0) && !((1970+Y)%4) && ( ((1970+Y)%100) || !((1970+Y)%400) ) )
static  const uint8_t monthDays[]={31,28,31,30,31,30,31,31,30,31,30,31}; // API starts months from 1, this array starts from 0
uint32_t epochSystem = 0; // timestamp of system boot up
   unsigned long epoch  ;
  int NDPyear, NDPmonth, NDPday, NDPhour, NDPminute, NDPsecond;


void setup() {
  MacAddress = GetWifiMac() ;
    ShowMac() ;
    initializeWiFi();
     delay(1500);
}

void loop() { // run over and over
    ShowNTPDateTime() ;
   delay(6000); // delay 1 minute for next measurement
 

}

void ShowNTPDateTime()
{
      retrieveNtpTime() ;
      getCurrentTime(epoch+timeZoneOffset, &NDPyear, &NDPmonth, &NDPday, &NDPhour, &NDPminute, &NDPsecond);
    //ttt->year = NDPyear ;
    Serial.print("NDP Date is :");
    Serial.print(StringDate(NDPyear,NDPmonth,NDPday));
    Serial.print("and ");
    Serial.print("NDP Time is :");
    Serial.print(StringTime(NDPhour,NDPminute,NDPsecond));
    Serial.print("\n");

}
void ShowMac()
{
 
     Serial.print("MAC:");
     Serial.print(MacAddress);
     Serial.print("\n");

}

void ShowInternetStatus()
{
   
        if (WiFi.status())
          {
               Meip = WiFi.localIP();
               Serial.print("Get IP is:");
               Serial.print(Meip);
               Serial.print("\n");
             
          }
          else
          {
                       Serial.print("DisConnected:");
                       Serial.print("\n");
          }

}


String  StringDate(int yyy,int mmm,int ddd) {
  String ttt ;
//nowT  = now;
 ttt = print4digits(yyy) + "-" + print2digits(mmm) + "-" + print2digits(ddd) ;
  return ttt ;
}


String  StringTime(int hhh,int mmm,int sss) {
  String ttt ;
  ttt = print2digits(hhh) + ":" + print2digits(mmm) + ":" + print2digits(sss) ;
return ttt ;
}

String  print2digits(int number) {
  String ttt ;
  if (number >= 0 && number < 10) { ttt = String("0") + String(number); } else { ttt = String(number); } return ttt ; } String print4digits(int number) { String ttt ; ttt = String(number); return ttt ; } String GetWifiMac() { String tt ; String t1,t2,t3,t4,t5,t6 ; WiFi.status(); //this method must be used for get MAC WiFi.macAddress(MacData); Serial.print("Mac:"); Serial.print(MacData[0],HEX) ; Serial.print("/"); Serial.print(MacData[1],HEX) ; Serial.print("/"); Serial.print(MacData[2],HEX) ; Serial.print("/"); Serial.print(MacData[3],HEX) ; Serial.print("/"); Serial.print(MacData[4],HEX) ; Serial.print("/"); Serial.print(MacData[5],HEX) ; Serial.print("~"); t1 = print2HEX((int)MacData[0]); t2 = print2HEX((int)MacData[1]); t3 = print2HEX((int)MacData[2]); t4 = print2HEX((int)MacData[3]); t5 = print2HEX((int)MacData[4]); t6 = print2HEX((int)MacData[5]); tt = (t1+t2+t3+t4+t5+t6) ; Serial.print(tt); Serial.print("\n"); return tt ; } String print2HEX(int number) { String ttt ; if (number >= 0 && number < 16)
  {
    ttt = String("0") + String(number,HEX);
  }
  else
  {
      ttt = String(number,HEX);
  }
  return ttt ;
}



// send an NTP request to the time server at the given address
void retrieveNtpTime() {
  Serial.println("Send NTP packet");

  Udp.beginPacket(ntpServer, 123); //NTP requests are to port 123
  Udp.write(nptSendPacket, NTP_PACKET_SIZE);
  Udp.endPacket();

  if(Udp.parsePacket()) {
    Serial.println("NTP packet received");
    Udp.read(ntpRecvBuffer, NTP_PACKET_SIZE); // read the packet into the buffer
   
    unsigned long highWord = word(ntpRecvBuffer[40], ntpRecvBuffer[41]);
    unsigned long lowWord = word(ntpRecvBuffer[42], ntpRecvBuffer[43]);
    unsigned long secsSince1900 = highWord << 16 | lowWord; const unsigned long seventyYears = 2208988800UL; // epoch = secsSince1900 - seventyYears + timeZoneOffset ; epoch = secsSince1900 - seventyYears ; epochSystem = epoch - millis() / 1000; } } void getCurrentTime(unsigned long epoch, int *year, int *month, int *day, int *hour, int *minute, int *second) { int tempDay = 0; *hour = (epoch % 86400L) / 3600; *minute = (epoch % 3600) / 60; *second = epoch % 60; *year = 1970; *month = 0; *day = epoch / 86400; for (*year = 1970; ; (*year)++) { if (tempDay + (LEAP_YEAR(*year) ? 366 : 365) > *day) {
      break;
    } else {
      tempDay += (LEAP_YEAR(*year) ? 366 : 365);
    }
  }
  tempDay = *day - tempDay; // the days left in a year
  for ((*month) = 0; (*month) < 12; (*month)++) {
    if ((*month) == 1) {
      if (LEAP_YEAR(*year)) {
        if (tempDay - 29 < 0) {
          break;
        } else {
          tempDay -= 29;
        }
      } else {
        if (tempDay - 28 < 0) {
          break;
        } else {
          tempDay -= 28;
        }
      }
    } else {
      if (tempDay - monthDays[(*month)] < 0) {
        break;
      } else {
        tempDay -= monthDays[(*month)];
      }
    }
  }
  (*month)++;
  *day = tempDay+2; // one for base 1, one for current day
}

void initializeWiFi() {
  while (status != WL_CONNECTED) {
    Serial.print("Attempting to connect to SSID: ");
    Serial.println(ssid);
    // Connect to WPA/WPA2 network. Change this line if using open or WEP network:
    status = WiFi.begin(ssid, pass);
  //   status = WiFi.begin(ssid);

    // wait 10 seconds for connection:
    delay(10000);
  }
  Serial.print("Success to connect AP:") ;
  Serial.print(ssid) ;
  Serial.print("\n") ;
 

  // local port to listen for UDP packets
      Udp.begin(2390);
}

void printWifiData()
{
  // print your WiFi shield's IP address:
  Meip = WiFi.localIP();
  Serial.print("IP Address: ");
  Serial.println(Meip);
  Serial.print("\n");

  // print your MAC address:
  byte mac[6];
  WiFi.macAddress(mac);
  Serial.print("MAC address: ");
  Serial.print(mac[5], HEX);
  Serial.print(":");
  Serial.print(mac[4], HEX);
  Serial.print(":");
  Serial.print(mac[3], HEX);
  Serial.print(":");
  Serial.print(mac[2], HEX);
  Serial.print(":");
  Serial.print(mac[1], HEX);
  Serial.print(":");
  Serial.println(mac[0], HEX);

  // print your subnet mask:
  Mesubnet = WiFi.subnetMask();
  Serial.print("NetMask: ");
  Serial.println(Mesubnet);

  // print your gateway address:
  Megateway = WiFi.gatewayIP();
  Serial.print("Gateway: ");
  Serial.println(Megateway);
}

如下圖所示,讀者可以看到本次實驗-網路校時測試程式結果畫面。

透過網路校時設計RTC時鐘模組

整合上述程式,我們可以將RTC時鐘模組的網路校時功能給予實作出來。打開Ameba開發工具「Sketch IDE」整合開發軟體撰寫一段程式,如下表所示之網路校時RTC時鐘模組測試程式,就可以透過Ameba WiFi模組取得網路時間並校正時鐘模組。

網路校時RTC 時鐘模組測試程式(SetTime_fromNet)


#include
#include "RTClib.h"
RTC_DS1307 RTC;
// above is used for RTC
#include "PMType.h"
#include
#include
#include
//    Aboev is used for WIFI
uint8_t MacData[6];

char ssid[] = "linkitone";      // your network SSID (name)
char pass[] = "";     // your network password
char server[] = "gpssensor.ddns.net"; // the MQTT server of LASS

#define MAX_CLIENT_ID_LEN 10
#define MAX_TOPIC_LEN     50
char clientId[MAX_CLIENT_ID_LEN];
char outTopic[MAX_TOPIC_LEN];

IPAddress  Meip , Megateway , Mesubnet ;
String MacAddress ;
int status = WL_IDLE_STATUS;
WiFiUDP Udp;
const char ntpServer[] = "pool.ntp.org";
const long timeZoneOffset = 28800L;
const int NTP_PACKET_SIZE = 48; // NTP time stamp is in the first 48 bytes of the message
const byte nptSendPacket[ NTP_PACKET_SIZE] = {
  0xE3, 0x00, 0x06, 0xEC, 0x00, 0x00, 0x00, 0x00,  0x00, 0x00, 0x00, 0x00, 0x31, 0x4E, 0x31, 0x34,
  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
};
byte ntpRecvBuffer[ NTP_PACKET_SIZE ];

#define LEAP_YEAR(Y)     ( ((1970+Y)>0) && !((1970+Y)%4) && ( ((1970+Y)%100) || !((1970+Y)%400) ) )
static  const uint8_t monthDays[] = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}; // API starts months from 1, this array starts from 0
uint32_t epochSystem = 0; // timestamp of system boot up
unsigned long epoch  ;
int NDPyear, NDPmonth, NDPday, NDPhour, NDPminute, NDPsecond;
// this is used for WIFI and NTP


void setup() {
  Serial.begin(9600);
  initRTC() ;
  //  init RTC Modules
  MacAddress = GetWifiMac() ;
  ShowMac() ;
  initializeWiFi();
  ShowNTPDateTime() ;
  SetRTCTime(NDPyear, NDPmonth, NDPday, NDPhour, NDPminute, NDPsecond);
  delay(1500);
}

void loop() { // run over and over
  delay(1000); // delay 1 minute for next measurement
  Serial.print("Now RTC Data and Time is :") ;
  Serial.print(ShowDateTime()) ;
  Serial.print("\n") ;
  delay(1000) ;

}

void ShowNTPDateTime()
{
  retrieveNtpTime() ;
  getCurrentTime(epoch + timeZoneOffset, &NDPyear, &NDPmonth, &NDPday, &NDPhour, &NDPminute, &NDPsecond);
  //ttt->year = NDPyear ;
  Serial.print("NDP Date is :");
  Serial.print(StringDate(NDPyear, NDPmonth, NDPday));
  Serial.print("and ");
  Serial.print("NDP Time is :");
  Serial.print(StringTime(NDPhour, NDPminute, NDPsecond));
  Serial.print("\n");

}
void ShowMac()
{

  Serial.print("MAC:");
  Serial.print(MacAddress);
  Serial.print("\n");

}

void ShowInternetStatus()
{

  if (WiFi.status())
  {
    Meip = WiFi.localIP();
    Serial.print("Get IP is:");
    Serial.print(Meip);
    Serial.print("\n");

  }
  else
  {
    Serial.print("DisConnected:");
    Serial.print("\n");
  }

}



void initRTC()
{
  Wire.begin();
  RTC.begin();
  if (! RTC.isrunning()) {
    Serial.println("RTC is NOT running!");

  }
}
String ShowDateTime()
{

  return StrDate() + "  " + StrTime() ;
}

String  StrDate() {
  String ttt ;
  //nowT  = now;
  DateTime now = RTC.now();
  ttt = print4digits(now.year()) + "-" + print2digits(now.month()) + "-" + print2digits(now.day()) ;
  //ttt = print4digits(NDPyear) + "/" + print2digits(NDPmonth) + "/" + print2digits(NDPday) ;
  return ttt ;
}

String  StringDate(int yyy, int mmm, int ddd) {
  String ttt ;
  //nowT  = now;
  ttt = print4digits(yyy) + "-" + print2digits(mmm) + "-" + print2digits(ddd) ;
  return ttt ;
}


String  StrTime() {
  String ttt ;
  // nowT  = RTC.now();
  DateTime now = RTC.now();
  ttt = print2digits(now.hour()) + ":" + print2digits(now.minute()) + ":" + print2digits(now.second()) ;
  //  ttt = print2digits(NDPhour) + ":" + print2digits(NDPminute) + ":" + print2digits(NDPsecond) ;
  return ttt ;
}

String  StringTime(int hhh, int mmm, int sss) {
  String ttt ;
  ttt = print2digits(hhh) + ":" + print2digits(mmm) + ":" + print2digits(sss) ;
  return ttt ;
}

String  print2digits(int number) {
  String ttt ;
  if (number >= 0 && number < 10) { ttt = String("0") + String(number); } else { ttt = String(number); } return ttt ; } String print4digits(int number) { String ttt ; ttt = String(number); return ttt ; } String GetWifiMac() { String tt ; String t1, t2, t3, t4, t5, t6 ; WiFi.status(); //this method must be used for get MAC WiFi.macAddress(MacData); Serial.print("Mac:"); Serial.print(MacData[0], HEX) ; Serial.print("/"); Serial.print(MacData[1], HEX) ; Serial.print("/"); Serial.print(MacData[2], HEX) ; Serial.print("/"); Serial.print(MacData[3], HEX) ; Serial.print("/"); Serial.print(MacData[4], HEX) ; Serial.print("/"); Serial.print(MacData[5], HEX) ; Serial.print("~"); t1 = print2HEX((int)MacData[0]); t2 = print2HEX((int)MacData[1]); t3 = print2HEX((int)MacData[2]); t4 = print2HEX((int)MacData[3]); t5 = print2HEX((int)MacData[4]); t6 = print2HEX((int)MacData[5]); tt = (t1 + t2 + t3 + t4 + t5 + t6) ; Serial.print(tt); Serial.print("\n"); return tt ; } String print2HEX(int number) { String ttt ; if (number >= 0 && number < 16)
  {
    ttt = String("0") + String(number, HEX);
  }
  else
  {
    ttt = String(number, HEX);
  }
  return ttt ;
}


// send an NTP request to the time server at the given address
void retrieveNtpTime() {
  Serial.println("Send NTP packet");

  Udp.beginPacket(ntpServer, 123); //NTP requests are to port 123
  Udp.write(nptSendPacket, NTP_PACKET_SIZE);
  Udp.endPacket();

  if (Udp.parsePacket()) {
    Serial.println("NTP packet received");
    Udp.read(ntpRecvBuffer, NTP_PACKET_SIZE); // read the packet into the buffer

    unsigned long highWord = word(ntpRecvBuffer[40], ntpRecvBuffer[41]);
    unsigned long lowWord = word(ntpRecvBuffer[42], ntpRecvBuffer[43]);
    unsigned long secsSince1900 = highWord << 16 | lowWord; const unsigned long seventyYears = 2208988800UL; // epoch = secsSince1900 - seventyYears + timeZoneOffset ; epoch = secsSince1900 - seventyYears ; epochSystem = epoch - millis() / 1000; } } void SetRTCTime( int yr, int mon, int dd, int hr, int mins, int secs) { RTC.adjust(DateTime(yr, mon, dd, hr, mins, secs)); } void getCurrentTime(unsigned long epoch, int *year, int *month, int *day, int *hour, int *minute, int *second) { int tempDay = 0; *hour = (epoch % 86400L) / 3600; *minute = (epoch % 3600) / 60; *second = epoch % 60; *year = 1970; *month = 0; *day = epoch / 86400; for (*year = 1970; ; (*year)++) { if (tempDay + (LEAP_YEAR(*year) ? 366 : 365) > *day) {
      break;
    } else {
      tempDay += (LEAP_YEAR(*year) ? 366 : 365);
    }
  }
  tempDay = *day - tempDay; // the days left in a year
  for ((*month) = 0; (*month) < 12; (*month)++) {
    if ((*month) == 1) {
      if (LEAP_YEAR(*year)) {
        if (tempDay - 29 < 0) {
          break;
        } else {
          tempDay -= 29;
        }
      } else {
        if (tempDay - 28 < 0) {
          break;
        } else {
          tempDay -= 28;
        }
      }
    } else {
      if (tempDay - monthDays[(*month)] < 0) {
        break;
      } else {
        tempDay -= monthDays[(*month)];
      }
    }
  }
  (*month)++;
  *day = tempDay + 2; // one for base 1, one for current day
}

void initializeWiFi() {
  while (status != WL_CONNECTED) {
    Serial.print("Attempting to connect to SSID: ");
    Serial.println(ssid);
    // Connect to WPA/WPA2 network. Change this line if using open or WEP network:
    // status = WiFi.begin(ssid, pass);
    status = WiFi.begin(ssid);

    // wait 10 seconds for connection:
    delay(10000);
  }
  Serial.print("Success to connect AP:") ;
  Serial.print(ssid) ;
  Serial.print("\n") ;


  // local port to listen for UDP packets
  Udp.begin(2390);
}

void printWifiData()
{
  // print your WiFi shield's IP address:
  Meip = WiFi.localIP();
  Serial.print("IP Address: ");
  Serial.println(Meip);
  Serial.print("\n");

  // print your MAC address:
  byte mac[6];
  WiFi.macAddress(mac);
  Serial.print("MAC address: ");
  Serial.print(mac[5], HEX);
  Serial.print(":");
  Serial.print(mac[4], HEX);
  Serial.print(":");
  Serial.print(mac[3], HEX);
  Serial.print(":");
  Serial.print(mac[2], HEX);
  Serial.print(":");
  Serial.print(mac[1], HEX);
  Serial.print(":");
  Serial.println(mac[0], HEX);

  // print your subnet mask:
  Mesubnet = WiFi.subnetMask();
  Serial.print("NetMask: ");
  Serial.println(Mesubnet);

  // print your gateway address:
  Megateway = WiFi.gatewayIP();
  Serial.print("Gateway: ");
  Serial.println(Megateway);
}

如下圖所示,可以看到本次實驗「網路校時RTC 時鐘模組」測試程式結果畫面,在下圖第一個紅框中,取得網路校時的時間;在第二個紅框中,已將網路校時的時間寫入RTC時鐘模組,之後就由模組讀出時間,不必浪費網路頻寬,為了取得時間資料,不斷向網路校時伺服器要求時間,不僅加快了系統的速度,也省去了網路不必要的頻寬浪費。

小結

本文主要介紹Ameba開發板如何透過WiFi模組連接上網際網路,去取得網路校正時間,並且成功校正RTC時鐘模組。這樣的功能,可以說是網路化、實務化的核心功能,透過解說,相信讀者更可以應用RTC時鐘模組到商業化產品,進而將本技術應用在物聯網的核心技術。

曹永忠

訂閱MakerPRO知識充電報

與40000位開發者一同掌握科技創新的技術資訊!

Author: 曹永忠

國立中央大學資訊管理學系博士,目前在國立暨南國際大學電機工程學系兼任助理教授、國立高雄科技大學商務資訊應用系兼任助理教授自由作家,專注於軟體工程、軟體開發與設計、物件導向程式設計、物聯網系統開發、Arduino開發、嵌入式系統開發。長期投入資訊系統設計與開發、企業應用系統開發、軟體工程、物聯網系統開發、軟硬體技術整合等領域,並持續發表作品及相關專業著作,並通過台灣圖霸的專家認證。

Share This Post On
468 ad

Submit a Comment

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *