Skocz do zawartości

Zegar ze stoperem synchronizowany GPS (lub NTP) do zastosowań astronomicznych


rziomber

Pomocna odpowiedź

Służba czasu ma istotne znaczenie przy różnego rodzaju obserwacjach astronomicznych. M.in do rejestracji jasności gwiazd zmiennych (szczególnie krótkookresowe zmienne zaćmieniowe wymagają dokładnego zanotowania momentu obserwacji), czasu wystąpienia zjawisk zakryciowych czy też w oczekiwaniu na tranzyt np ISS na tle Słońca czy Księżyca.

Zainspirowany projektem opisanym w niedawnym artykule przedstawiam swój stary pomysł, o nieco innych założeniach i chyba bardziej przydatny do faktycznych obserwacji nieba. Do budowy możemy wykorzystać moduł oparty na mikrokontrolerze ATmega328P, np Arduino UNO, Nano czy nawet Pro Mini (co zapewni nam energooszczędność i dłuższy czas pracy na baterii). Bardzo istotne, by wybrać moduł GPS z wyprowadzonym sygnałem PPS.

Zrealizowałem go w dwóch wersjach: z wyraźnym wyświetlaczem siedmiosegmentowym LED z kontrolerem MAX7219 i alarmem oraz mniej wyraźnym OLED, za to z opcją zapamiętywania momentów ostatnich naciśnięć przycisku.

655958926_GPSClock.thumb.jpg.4566819b24813c419c6b3d5e588004dd.jpg979267080_GPSClock7-seg.thumb.png.67ea1318058e31bb79a5e3b736b3b7ff.png811151600_gpsclock.thumb.png.80e11968b8c6775f71d62ecc636cc1ed.png

/*
  DIN - 12
  CLK - 11
  LOAD - 10
*/
#include "LedControl.h" //http://github.com/wayoda/LedControl
#include <Time.h>  // http://www.pjrc.com/teensy/td_libs_Time.html
#include <TinyGPS++.h> // http://arduiniana.org/libraries/tinygpsplus/
#include <SoftwareSerial.h>
#define UsePPS 1
#define PPSPin 2
#define UTCcorrection -2
#define TimeZone 0
#define BuzzerPin 13

static const int RXPin = 3, TXPin = 4;
unsigned long syncTime = 0, cycle = 0;
bool sync = 0;
tmElements_t tmbegin;
unsigned long beginTime = 0;

TinyGPSPlus gps;
LedControl lc = LedControl(12, 11, 10, 1);
SoftwareSerial ss(RXPin, TXPin);
void setup() {
  Serial.begin(9600);
  tmbegin.Hour = 20;
  tmbegin.Minute = 42;
  tmbegin.Second = 36;
  tmbegin.Day = 17;
  tmbegin.Month = 1;
  tmbegin.Year = 2019 - 1970;
  beginTime = makeTime(tmbegin);
  Serial.println("Begin time: " + String(beginTime));

  ss.begin(9600);
  if (UsePPS)
  {
    pinMode(PPSPin, INPUT_PULLUP);
    attachInterrupt(digitalPinToInterrupt(PPSPin), PPS, RISING);
  }

  pinMode(BuzzerPin, OUTPUT);
  digitalWrite(BuzzerPin, LOW);
  /*
    The MAX72XX is in power-saving mode on startup,
    we have to do a wakeup call
  */
  lc.shutdown(0, false);
  /* Set the brightness to a medium values */
  lc.setIntensity(0, 1);
  /* and clear the display */
  lc.clearDisplay(0);
}

void loop() {
  while (!sync && ss.available() > 0)
  {
    gps.encode(ss.read());
  }
  if (millis() - syncTime - cycle * 1000 >= 1000) {
    String digit = String(second());
    if (digit.length() == 1) {
      lc.setChar(0, 0, digit[0], false);
      lc.setChar(0, 1, '0', false);
    } else
    {
      lc.setChar(0, 0, digit[1], false);
      lc.setChar(0, 1, digit[0], false);
    }

    digit = String(minute());
    if (digit.length() == 1) {
      lc.setChar(0, 2, digit[0], true);
      lc.setChar(0, 3, '0', false);
    } else
    {
      lc.setChar(0, 2, digit[1], true);
      lc.setChar(0, 3, digit[0], false);
    }

    digit = String(hour());
    if (digit.length() == 1) {
      lc.setChar(0, 4, digit[0], true);
      lc.setChar(0, 5, '0', false);
    } else
    {
      lc.setChar(0, 4, digit[1], true);
      lc.setChar(0, 5, digit[0], false);
    }
    if (now() - (TimeZone * 60 * 60) >= beginTime) {
      digitalWrite(BuzzerPin, HIGH);
    }
    cycle++;

    Serial.println(String(year()) + "." + String(month()) + "." + String(day()) + " " + String(hour()) + ":" + String(minute()) + ":" + String(second()) + " UNIX time: " + String(now()));
  }
}

void PPS()
{
  if (gps.time.isUpdated() && gps.date.year() > 2015 && !timeStatus())
  {
    setTime(gps.time.hour(), gps.time.minute(), gps.time.second(), gps.date.day(), gps.date.month(), gps.date.year());
    adjustTime(UTCcorrection + (TimeZone * 60 * 60) + 1);
    syncTime = millis();
    cycle = 0;
    detachInterrupt(digitalPinToInterrupt(PPSPin));
    sync = 1;
  }
  Serial.println("PPS");
}

Arduino "czyta" UART ze strony modułu GPS dopóki zegar nie jest zsynchronizowany.

while (!sync && ss.available() > 0) { gps.encode(ss.read()); }

Jeżeli GPS odczyta aktualny czas, Arduino czeka do najbliższego przerwania dokonanego przez wzrastające zbocze sygnału PPS. Wtedy ustawiany jest "zegarek" w bibliotece Time. Po tym deaktywowane jest przerwanie pinu PPS.

Po uruchomieniu urządzenia sprawdźmy, czy działa ono poprawnie. Porównajmy wskazania np ze stroną https://time.is. Być może konieczna będzie korekcja sekund przestępnych, gdyż GPS prezentuje czas nieskoordynowany. Zrobimy to poprawiając #define UTCcorrection -2

Alarm możemy ustawić edytując tmbegin.Hour = 20; itd. Brzęczek podłączony do pinu 13 ostrzeże nas, że za chwilę nastąpi przejście Międzynarodowej Stacji Kosmicznej na tle Słońca lub Księżyca, a my musimy szczególnie się skupić. Możemy nawet podłączyć w to miejsce transoptor, który "podkablowany" do lustrzanki cyfrowej automatycznie zacznie wykonywać serię zdjęć (wcześniej ustawmy aparat w "continuous shooting mode")!

173452030_GPSClock7-segDSLR.thumb.png.4d90f18ec50e5abe1502ca7dc8701fbf.png

Takie zjawisko trwa około sekundy i szczególnie w przypadku Słońca jest zupełnie niespodziewane (wizualnie dla obserwatora, wcześniej możemy jednak wyliczyć stosowny moment) i łatwo je przegapić.

383946021_ISSSun.thumb.jpg.d38e20545fda7316992322f0ac578622.jpg

Tranzyt ISS na tle Słońca sfotografowany automatycznie 8 kwietnia 2017. Zjawisko wystąpiło bardzo nisko nad horyzontem. Stąd... komin sąsiada (czarny pasek) widoczny po lewej. Po prawej plama słoneczna. ISS to ta prostokątna plamka na środku wysokości, po prawej.

Wersja ze stoperem wykorzystuje wyświetlacz OLED z kontrolerem SPI SH1106. W pierwszej linijce prezentowany jest aktualny czas. W kolejnych - historia zatrzymań stopera.

GPS_occultations_timer.thumb.jpg.d7d71b0bab909b33f61f717d876e13fe.jpg

/*  OLED          Arduino
     D0-----------10
     D1-----------9
     RST----------13
     DC-----------11
     VCC----------5V
     GND----------GND*/

#include <Time.h>  // http://www.pjrc.com/teensy/td_libs_Time.html
#include <TinyGPS++.h> // http://arduiniana.org/libraries/tinygpsplus/
#include <SPI.h>
#include <Adafruit_GFX.h> // http://github.com/adafruit/Adafruit-GFX-Library
#include <Adafruit_SSD1306.h> // http://github.com/adafruit/Adafruit_SSD1306
#include <SoftwareSerial.h>
#define PPSPin 2
#define ButtonPin 3
#define UTCcorrection -1 // difference between time received from TinyGPS++ library and UTC (leap seconds!) http://time.is/UTC
#define remeberValues 3

#define OLED_MOSI   9
#define OLED_CLK   10
#define OLED_DC    11
#define OLED_CS    12
#define OLED_RESET 13

#define NUMFLAKES 10
#define XPOS 0
#define YPOS 1
#define DELTAY 2

unsigned long pushsFIFO[remeberValues];
unsigned long lastButtonchange = 0, syncUNIXtime = 0, syncMillis = 0, pushTime = 0, pushTimeUNIX = 0;
unsigned char LCDrefreshSecond = 0;
static const int RXPin = 4, TXPin = 5;
static const uint32_t GPSBaud = 9600;
unsigned long cutnumber();
String timeString();
String millisToTimeString();
void addToFIFO();

TinyGPSPlus gps;
tmElements_t tm;
SoftwareSerial ss(RXPin, TXPin);
Adafruit_SSD1306 display(OLED_MOSI, OLED_CLK, OLED_DC, OLED_RESET, OLED_CS);
/* Uncomment this block to use hardware SPI
  #define OLED_DC     6
  #define OLED_CS     7
  #define OLED_RESET  8
  Adafruit_SSD1306 display(OLED_DC, OLED_RESET, OLED_CS);
*/

void setup() {
  Serial.begin(9600);
  ss.begin(GPSBaud);
  pinMode(PPSPin, INPUT_PULLUP);
  attachInterrupt(digitalPinToInterrupt(PPSPin), PPS, RISING);
  pinMode(ButtonPin, INPUT_PULLUP);
  attachInterrupt(digitalPinToInterrupt(ButtonPin), button, FALLING);
  display.begin(SSD1306_SWITCHCAPVCC);
  display.clearDisplay();
  for (int i = 0; i < remeberValues; i++)
  {
    pushsFIFO[remeberValues] = 0;
  }
}

void loop() {
  while (ss.available() > 0)
  {
    gps.encode(ss.read());
  }

  if (pushTime)
  {
    Serial.print("Button push time: ");
    Serial.print(pushTime);
    Serial.print(" sync time: ");
    Serial.print(syncMillis);
    Serial.print(" sync UNIX time: ");
    Serial.println(syncUNIXtime);
    Serial.println(millisToTimeString(pushTime));
    Serial.println(second());
    pushTime = 0;
  }

  if (LCDrefreshSecond != cutnumber(millis() - syncMillis, 4, 3)) {
    breakTime(now(), tm);
    display.clearDisplay();
    display.setTextSize(1);
    display.setTextColor(WHITE);
    display.setCursor(0, 0);
    display.print(tm.Month);
    display.print("/");
    display.print(tm.Day);
    display.print("/");
    display.print((tm.Year > 0) ? tm.Year - 30 : 0);
    display.print(" ");
    display.print(tm.Hour);
    display.print(":");
    display.print(tm.Minute);
    display.print(":");
    display.print(tm.Second);
    display.println("UTC");

    for (int i = 0; i < remeberValues; i++)
    {
      if (pushsFIFO[i] == 0)
      {
        break;
      }
      display.print(i + 1);
      display.print(": ");
      display.println(millisToTimeString(pushsFIFO[i]));
    }
    display.display();

    LCDrefreshSecond = cutnumber(millis() - syncMillis, 4, 3);
  }
}

void PPS()
{
  if (gps.time.isUpdated() && gps.date.year() > 2015 && !timeStatus())
  {
    syncMillis = millis();
    setTime(gps.time.hour(), gps.time.minute(), gps.time.second(), gps.date.day(), gps.date.month(), gps.date.year());
    adjustTime(UTCcorrection + 1);
    detachInterrupt(digitalPinToInterrupt(PPSPin));
    syncUNIXtime = now();
  }
  Serial.println("PPS");
}

void button()
{
  if (millis() - lastButtonchange > 500)
  {
    pushTime = millis();
    addToFIFO(pushTime);
    LCDrefreshSecond = LCDrefreshSecond + 5;
  }
  lastButtonchange = millis();
}

unsigned long cutnumber(unsigned long number, unsigned int cutbeg, unsigned int cutend)
{
  number = number - pow(10, cutbeg) * (unsigned long) (number / pow(10, cutbeg));
  if (cutend != 0)
  {
    number = (unsigned long) (number / pow(10, cutend));
  }
  return number;
}

String timeString(unsigned long timeUNIX)
{
  breakTime(timeUNIX, tm);
  String timeWord = String(tm.Hour) + ":" + String(tm.Minute) + ":" + String(tm.Second);
  return timeWord;
}

String millisToTimeString(unsigned long inputMillis)
{
  pushTimeUNIX = syncUNIXtime + (inputMillis - syncMillis) / 1000;
  String timeWord = timeString(pushTimeUNIX) + "." + cutnumber(inputMillis - syncMillis, 3, 0);
  return timeWord;
}

void addToFIFO(unsigned long value) {
  for (int i = remeberValues - 1; i > 0; i--)
  {
    pushsFIFO[i] = pushsFIFO[i - 1];
  }
  pushsFIFO[0] = value;
}

Stoper dedykowany jest obserwatorom zjawisk zakryciowych.

772px-Before_4_Sgr_Occultation_by_the_Mo

Tuż przed zakryciem gwiazdy 4 Sgr przez Księżyc.

Do fotografowania dłuższych zjawisk astronomicznych (np zaćmień Słońca czy Księżyca) możemy wykorzystać szkic GPS_DSLR_timer.ino. Ustawimy tam moment początku i końca oraz interwał czasu, z jakim chcemy automatycznie fotografować zjawisko.

Możemy też wykonać mniej dokładny zegarek synchronizowany serwerem NTP. Użyjemy w tym celu ESP8266 oraz wyświetlacza LCD 2x16 z konwerterem I2C LCM1602.

746552085_NTPClock.thumb.jpg.03cc9e4552298a56c20778570f5d32e0.jpg

Jeżeli napotkamy problem z uruchomieniem tego "monitora", pamiętajmy że występuje on pod różnymi adresami I2C (pierwsza zmienna przy wywołaniu konstruktora biblioteki LCD LiquidCrystal_I2C lcd(0x27, 16, 2);). Czasem też konieczna jest regulacja kontrastu kręcąc potencjometrem znajdującym się pod spodem. Co ciekawe niewielka zmiana napięcia zasilającego powoduje zmianę prezentowanego kontrastu.

/*
  I2C
  D4 - SCL
  D3 - SDA
*/

#include <ESP8266HTTPClient.h>
#include <ESP8266WiFi.h>
#include <WiFiClientSecure.h>
#include <WiFiUdp.h>
#include <Wire.h>
#include <LiquidCrystal_I2C.h> //https://github.com/fdebrabander/Arduino-LiquidCrystal-I2C-library
#include <Time.h>  // http://www.pjrc.com/teensy/td_libs_Time.html

IPAddress timeServerIP; // time.nist.gov NTP server address
const char* ntpServerName = "time.google.com";
const int NTP_PACKET_SIZE = 48; // NTP time stamp is in the first 48 bytes of the message
byte packetBuffer[ NTP_PACKET_SIZE]; //buffer to hold incoming and outgoing packets
unsigned long last_time_sync = 0, last_NTP_check = 0, last_time_LCD = 0;
unsigned int localPort = 2390;      // local port to listen for UDP packets

// A UDP instance to let us send and receive packets over UDP
WiFiUDP udp;

LiquidCrystal_I2C lcd(0x27, 16, 2);
tmElements_t tm;
time_t getNtpTime();

void setup() {
  Serial.begin(9600);                                  //Serial connection
  Wire.begin(D3, D4);
  Wire.setClock(100000);
  lcd.begin();
  lcd.backlight();
  WiFi.softAPdisconnect();
  WiFi.disconnect();
  WiFi.mode(WIFI_STA);
  WiFi.begin("Siec WiFi", "i haslo do niej");   //WiFi connection

  while (WiFi.status() != WL_CONNECTED) {  //Wait for the WiFI connection completion
    delay(500);
    Serial.println("Waiting for connection");
  }
  udp.begin(localPort);
}

void loop() {
  if (millis() - last_time_sync > 10 * 24 * 60 * 60 * 1000 || last_time_sync == 0) {
    unsigned long unixt = unix_time();
    Serial.println(unixt);
    if (unixt > 1512876158) {
      Serial.println("SYNC OK");
      setTime(unixt);
      last_time_sync = millis();
    }
  }
  unsigned long nowTime = now();
  if (nowTime > last_time_LCD) {
    last_time_LCD = nowTime;
    lcd.clear();
    lcd.setCursor(0, 0);
    lcd.print(String(year()-2000) + "." + String(month()) + "." + String(day()) + " " + String(hour()) + ":" + String(minute()) + ":" + String(second()));
    lcd.setCursor(0, 1);
    lcd.print("UNIX " + String(now()));
    Serial.println(String(year()) + "." + String(month()) + "." + String(day()) + " " + String(hour()) + ":" + String(minute()) + ":" + String(second()) + " UNIX time: " + String(now()));
  }
}

// send an NTP request to the time server at the given address
//https://github.com/esp8266/Arduino/blob/master/libraries/ESP8266WiFi/examples/NTPClient/NTPClient.ino
unsigned long sendNTPpacket(IPAddress& address)
{
  //Serial.println("sending NTP packet...");
  // set all bytes in the buffer to 0
  memset(packetBuffer, 0, NTP_PACKET_SIZE);
  // Initialize values needed to form NTP request
  // (see URL above for details on the packets)
  packetBuffer[0] = 0b11100011;   // LI, Version, Mode
  packetBuffer[1] = 0;     // Stratum, or type of clock
  packetBuffer[2] = 6;     // Polling Interval
  packetBuffer[3] = 0xEC;  // Peer Clock Precision
  // 8 bytes of zero for Root Delay & Root Dispersion
  packetBuffer[12]  = 49;
  packetBuffer[13]  = 0x4E;
  packetBuffer[14]  = 49;
  packetBuffer[15]  = 52;

  // all NTP fields have been given values, now
  // you can send a packet requesting a timestamp:
  udp.beginPacket(address, 123); //NTP requests are to port 123
  udp.write(packetBuffer, NTP_PACKET_SIZE);
  udp.endPacket();
}

time_t unix_time() {
  if (WiFi.status() == WL_CONNECTED && ((unsigned long)(millis() - last_NTP_check) > 15000 || last_time_sync == 0)) {
    last_NTP_check = millis();
    //get a random server from the pool
    WiFi.hostByName(ntpServerName, timeServerIP);

    sendNTPpacket(timeServerIP); // send an NTP packet to a time server
    // wait to see if a reply is available
    delay(1000);

    int cb = udp.parsePacket();
    if (!cb) {
      Serial.println("no packet yet");
    }
    else {
      //Serial.print("packet received, length=");
      //Serial.println(cb);
      // We've received a packet, read the data from it
      udp.read(packetBuffer, NTP_PACKET_SIZE); // read the packet into the buffer

      //the timestamp starts at byte 40 of the received packet and is four bytes,
      // or two words, long. First, esxtract the two words:

      unsigned long highWord = word(packetBuffer[40], packetBuffer[41]);
      unsigned long lowWord = word(packetBuffer[42], packetBuffer[43]);
      // combine the four bytes (two words) into a long integer
      // this is NTP time (seconds since Jan 1 1900):
      unsigned long secsSince1900 = highWord << 16 | lowWord;
      Serial.print("Seconds since Jan 1 1900 = " );
      Serial.println(secsSince1900);

      // now convert NTP time into everyday time:
      Serial.println("Unix time sync ");
      // Unix time starts on Jan 1 1970. In seconds, that's 2208988800:
      const unsigned long seventyYears = 2208988800UL;
      // subtract seventy years:
      unsigned long epoch = secsSince1900 - seventyYears;
      // print Unix time:
      if (epoch > 1512876158) {
        return epoch + 2;
      } else
      {
        return 0;
      }
    }
  }
}

Do komunikacji z serwerem NTP wykorzystałem poradnik Arduino Time Sync from NTP Server using ESP8266 WiFi module.

Edytowano przez Treker
Poprawiłem formatowanie.
  • Lubię! 1
Link do komentarza
Share on other sites

Podoba Ci się ten projekt? Zostaw pozytywny komentarz i daj znać autorowi, że zbudował coś fajnego!

Masz uwagi? Napisz kulturalnie co warto zmienić. Doceń pracę autora nad konstrukcją oraz opisem.

Właśnie zaakceptowałem Twój opis, możesz go teraz zgłosić do akcji rabatowej umieszczając link w temacie zbiorczym. Dziękuję za przedstawienie ciekawego projektu, zachęcam do prezentowania kolejnych DIY oraz aktywności na naszym forum 🙂

Link do komentarza
Share on other sites

Bądź aktywny - zaloguj się lub utwórz konto!

Tylko zarejestrowani użytkownicy mogą komentować zawartość tej strony

Utwórz konto w ~20 sekund!

Zarejestruj nowe konto, to proste!

Zarejestruj się »

Zaloguj się

Posiadasz własne konto? Użyj go!

Zaloguj się »
×
×
  • Utwórz nowe...

Ważne informacje

Ta strona używa ciasteczek (cookies), dzięki którym może działać lepiej. Więcej na ten temat znajdziesz w Polityce Prywatności.