Timezones

Date:2017-08-25

Intro

Timezones are a much needed feature around planet Earth to have noon (highest elevation of the sun) near 12 o’clock local time. This little feature alone is a surprisingly rich source of politics, skewed geographical timescapes, and other oddities. Programmers, astronomers and travellers may come across funny timezones with offsets not being full hours. The concept of daylight savings time is like moving one zone east during summer and coming back west during winter. Anyway, if programming a clock, you might want to display local time, no matter how odd its definition.

I decided to use Unix Epoch seconds as a second software clock on the controller, and use it to create local time. The definition of any timezone is reduced to a signed offset ranging from -43200 to 43200 (12*3600) at most. This value can be represented as a 32bit (2 cells) signed variable.

I also decided to ignore the concept of daylight savings time. If you need that concept, then define 2 timezones, and switch the timezone appropriately — not the clock itself.

This code is small and specific to a given hardware and use case. So imho it belongs into the main program text and not into a separate file.

Design Decisions

  • A timezone is represented by an offset in seconds applied to UTC, and a string label to identify it.
  • The offset fits into a 32bit memory location as a 32 bit signed variable.
  • There is no concept of daylight savings time.

Code Details

MasterClock drives Epoch Seconds, too

When setting the software master clock on startup, the corresponding Unix Epoch Seconds are calculated and stored in 2variable ESec

2variable Esec
: ++Esec  ( -- )  Esec 2@  1. d+  Esec 2! ;
: .Esec   ( -- )  Esec 2@ ud. ;

: init
  ...
  +sw
  0. Esec    2!

  +i2c
  i2c_addr_rtc i2c.ping? if             \ if RTC available
    hwclock>clock                       \     read it, set clock
    clock.get ut>s.short Esec 2!        \     set epoch seconds
  else
    #1970 1 1 0 0 0 clock.set
    0. Esec 2!
  then
;

The counter Esec is incremented in job.sec and should follow the counters of the MasterClock.

: job.sec ...
  ++Esec
;

Pins to select timezone

On my clock the timezone is selected by reading 2 pins. Depending on their values EsecOffset is set accordingly. The two timezones are defined such that switching to daylight savings time is a matter of selecting one of the two. I use a coding bridge or a switch for that. Any other time zone definitions and more pins to select them would be placed in this code fragment.

PORTA $03 bitmask: _tz

: +sw ( -- )          _tz pin_input ;

2variable EsecOffset
: UTC  ( -- )      0. EsecOffset 2! ;
: MEZ  ( -- )  #3600. EsecOffset 2! ;
: MESZ ( -- )  #7200. EsecOffset 2! ;


: _tz.set
  _tz pin@
  dup 0 = if
    UTC
    led_utc on  led_mez off led_mesz off
  then
  dup 2 = if
    MESZ
    led_utc off led_mez off led_mesz on
  then
  dup 3 = if
    UTC
    led_utc on  led_mez off led_mesz off
  then
  drop
;

In order to display local time read the value of ESec, apply the offset of the selected time zone and convert the result into the usual hour, minute etc. counters. The result is sent to the display.

: local.dt ( -- S M H d m Y )
  Esec 2@  EsecOffset 2@  d+  s>dt.short
;
: cd.localtime
  local.dt                              \ -- S M H d m Y
  drop drop drop                        \ -- S M H
  rot drop swap                         \ -- H M
  >r #10 /mod swap                      \ -- H.10 H.1
  r> #10 /mod swap                      \ -- H.10 H.1 M.10 M.1
  #4 type.7seg                          \ --
;

This function displays hours and minutes, it is specific to the available display (number and write order of digits), of course. The function shall be called perodically.

: job.min
  _tz.set cd.localtime
;

If you switch the time zone by adjusting the selection bridge, the new time is displayed at the next call of cd.localtime, i.e. at the next minute in this case.