diff --git a/configure/CONFIG_SITE_ENV b/configure/CONFIG_SITE_ENV index 49331f9a4..008467933 100644 --- a/configure/CONFIG_SITE_ENV +++ b/configure/CONFIG_SITE_ENV @@ -24,40 +24,41 @@ # Site-specific environment settings -# Time service: -# EPICS_TIMEZONE -# Local timezone info for vxWorks and RTEMS. The format is -# :::: -# where is only used by strftime() for %Z conversions, -# and and are mmddhh - that is month,day,hour -# e.g. for ANL in 2018: EPICS_TIMEZONE=CUS::360:031102:110402 -# The future dates below assume the rules don't get changed; -# see http://www.timeanddate.com/time/dst/2018.html to check. -# -# DST for 2018 US: Mar 11 - Nov 04 -# EU: Mar 25 - Oct 28 -EPICS_TIMEZONE = CUS::360:031102:110402 -#EPICS_TIMEZONE = MET::-60:032502:102803 -# -# DST for 2019 US: Mar 10 - Nov 03 -# EU: Mar 31 - Oct 27 -#EPICS_TIMEZONE = CUS::360:031002:110302 -#EPICS_TIMEZONE = MET::-60:033102:102703 -# -# DST for 2020 US: Mar 08 - Nov 01 -# EU: Mar 29 - Oct 25 -#EPICS_TIMEZONE = CUS::360:030802:110102 -#EPICS_TIMEZONE = MET::-60:032902:102503 -# -# DST for 2021 US: Mar 14 - Nov 07 -# EU: Mar 28 - Oct 31 -#EPICS_TIMEZONE = CUS::360:031402:110702 -#EPICS_TIMEZONE = MET::-60:032802:103103 -# -# DST for 2022 US: Mar 13 - Nov 06 -# EU: Mar 27 - Oct 30 -#EPICS_TIMEZONE = CUS::360:031302:110602 -#EPICS_TIMEZONE = MET::-60:032702:103003 +## Time service: +# EPICS_TZ +# Local timezone rules for vxWorks and RTEMS. The value follows the Posix +# TZ environment variable's Mm.n.d/h format (see the IBM link below for +# details). If TZ hasn't already been set when the osdTime timeRegister() +# C++ static constructor runs, this parameter will be copied into the TZ +# environment variable. Once the OS clock has been synchronized to NTP the +# routine tz2timezone() will be run to convert TZ into the TIMEZONE +# variable format that VxWorks needs. +# https://developer.ibm.com/articles/au-aix-posix/ + +# Japan Standard Time, no DST: +#EPICS_TZ = "JST-9" + +# Central European (Summer) Time: +#EPICS_TZ = "CET-1CEST,M3.5.0/2,M10.5.0/3" + +# Greenwich Mean/British Summer Time: +#EPICS_TZ = "GMT0BST,M3.5.0/1,M10.5.0/2" + +# US Eastern Standard/Daylight Time: +#EPICS_TZ = "EST5EDT,M3.2.0/2,M11.1.0/2" + +# US Central Standard/Daylight Time: +EPICS_TZ = "CST6CDT,M3.2.0/2,M11.1.0/2" + +# US Mountain Standard/Daylight Time: +#EPICS_TZ = "MST7MDT,M3.2.0/2,M11.1.0/2" + +# US Pacific Standard/Daylight Time: +#EPICS_TZ = "PST8PDT,M3.2.0/2,M11.1.0/2" + +# US Hawaiian Standard Time, no DST: +#EPICS_TZ = "HST10" + # EPICS_TS_NTP_INET # NTP time server ip address for VxWorks and RTEMS. diff --git a/documentation/RELEASE_NOTES.md b/documentation/RELEASE_NOTES.md index de53497ab..0b9ed400f 100644 --- a/documentation/RELEASE_NOTES.md +++ b/documentation/RELEASE_NOTES.md @@ -6,6 +6,37 @@ This version of EPICS Base has not been released yet. +### Replace EPICS_TIMEZONE with EPICS_TZ + +The `EPICS_TIMEZONE` environment parameter provided time-zone information for +the IOC's locale in the old ANSI format expected by VxWorks for its `TIMEZONE` +environment variable, and can also used by RTEMS to set its `TZ` environment +variable. However the `TIMEZONE` value has to be updated every year since it +contains the exact dates of the daylight-savings time changes. The Posix TZ +format that RTEMS uses contains rules that for calculating those dates, thus its +value would only need updating if the rules (or the locale) are changed. + +This release contains changes that replace the `EPICS_TIMEZONE` environment +parameter with one called `EPICS_TZ` and a routine for VxWorks that calculates +the `TIMEZONE` environment variable from the current `TZ` value. This routine +will be run once at start-up, when the EPICS clock has synchronized to its NTP +server. The calculations it contains were worked out and donated to EPICS by +Larry Hoff in 2009; it is unforunate that it has taken 10 years for them to be +integrated into Base. + +The default value for the `EPICS_TZ` environment parameter is set in the Base +configure/CONFIG_SITE_ENV file, which contains example settings for most EPICS +sites that use VxWorks, and a link to a page describing the Posix TZ format for +any locations that I missed. + +If a VxWorks IOC runs continuously without being rebooted from December 31st to +the start of daylight savings time the following year, its `TIMEZONE` value will +be wrong as it was calculated for the previous year. This only affects times +that are converted to a string on the IOC however and is easily fixed; just run +the command `tz2timezone()` on the VxWorks shell and the calculation will be +redone for the current year. IOCs that get rebooted at least once before the +start of summer time will not need this to be done. + ### Added new decimation channel filter A new server-side filter has been added to the IOC for reducing the number diff --git a/src/libCom/RTEMS/rtems_init.c b/src/libCom/RTEMS/rtems_init.c index 2b909ab3b..dcb6daaf7 100644 --- a/src/libCom/RTEMS/rtems_init.c +++ b/src/libCom/RTEMS/rtems_init.c @@ -581,25 +581,11 @@ Init (rtems_task_argument ignored) printf ("***** Can't set time: %s\n", rtems_status_text (sc)); } if (getenv("TZ") == NULL) { - const char *tzp = envGetConfigParamPtr(&EPICS_TIMEZONE); - if (tzp == NULL) { - printf("Warning -- no timezone information available -- times will be displayed as GMT.\n"); - } - else { - char tz[10]; - int minWest, toDst = 0, fromDst = 0; - if(sscanf(tzp, "%9[^:]::%d:%d:%d", tz, &minWest, &toDst, &fromDst) < 2) { - printf("Warning: EPICS_TIMEZONE (%s) unrecognizable -- times will be displayed as GMT.\n", tzp); - } - else { - char posixTzBuf[40]; - char *p = posixTzBuf; - p += sprintf(p, "%cST%d:%.2d", tz[0], minWest/60, minWest%60); - if (toDst != fromDst) - p += sprintf(p, "%cDT", tz[0]); - epicsEnvSet("TZ", posixTzBuf); - } - } + const char *tzp = envGetConfigParamPtr(&EPICS_TZ); + if (!tzp || *tzp) + printf("Warning: No timezone information, times will be displayed in UTC.\n"); + else + epicsEnvSet("TZ", tzp); } tzset(); osdTimeRegister(); diff --git a/src/libCom/env/envDefs.h b/src/libCom/env/envDefs.h index 20f0eb2ad..588cecdad 100644 --- a/src/libCom/env/envDefs.h +++ b/src/libCom/env/envDefs.h @@ -61,7 +61,7 @@ epicsShareExtern const ENV_PARAM EPICS_CAS_BEACON_PORT; epicsShareExtern const ENV_PARAM EPICS_BUILD_COMPILER_CLASS; epicsShareExtern const ENV_PARAM EPICS_BUILD_OS_CLASS; epicsShareExtern const ENV_PARAM EPICS_BUILD_TARGET_ARCH; -epicsShareExtern const ENV_PARAM EPICS_TIMEZONE; +epicsShareExtern const ENV_PARAM EPICS_TZ; epicsShareExtern const ENV_PARAM EPICS_TS_NTP_INET; epicsShareExtern const ENV_PARAM EPICS_IOC_LOG_PORT; epicsShareExtern const ENV_PARAM EPICS_IOC_LOG_INET; diff --git a/src/libCom/osi/Makefile b/src/libCom/osi/Makefile index c06a862ed..e05aec37d 100644 --- a/src/libCom/osi/Makefile +++ b/src/libCom/osi/Makefile @@ -77,7 +77,7 @@ Com_SRCS += epicsGeneralTime.c # Time providers Com_SRCS += osiClockTime.c -Com_SRCS_vxWorks += osiNTPTime.c +Com_SRCS_vxWorks += osiNTPTime.c tz2timezone.c Com_SRCS_RTEMS += osiNTPTime.c ifeq ($(OS_CLASS),vxWorks) diff --git a/src/libCom/osi/epicsTime.cpp b/src/libCom/osi/epicsTime.cpp index ef43e0ad2..47658bd03 100644 --- a/src/libCom/osi/epicsTime.cpp +++ b/src/libCom/osi/epicsTime.cpp @@ -952,7 +952,8 @@ extern "C" { try { local_tm_nano_sec tmns = epicsTime (*pSrc); *pDest = tmns.ansi_tm; - *pNSecDest = tmns.nSec; + if (pNSecDest) + *pNSecDest = tmns.nSec; } catch (...) { return epicsTimeERROR; @@ -964,7 +965,8 @@ extern "C" { try { gm_tm_nano_sec gmtmns = epicsTime (*pSrc); *pDest = gmtmns.ansi_tm; - *pNSecDest = gmtmns.nSec; + if (pNSecDest) + *pNSecDest = gmtmns.nSec; } catch (...) { return epicsTimeERROR; diff --git a/src/libCom/osi/os/vxWorks/osdTime.cpp b/src/libCom/osi/os/vxWorks/osdTime.cpp index 4db375fbb..eb144c1b6 100644 --- a/src/libCom/osi/os/vxWorks/osdTime.cpp +++ b/src/libCom/osi/os/vxWorks/osdTime.cpp @@ -24,22 +24,38 @@ #define NTP_REQUEST_TIMEOUT 4 /* seconds */ +extern "C" { + int tz2timezone(void); +} + static char sntp_sync_task[] = "ipsntps"; static char ntp_daemon[] = "ipntpd"; static const char *pserverAddr = NULL; +static CLOCKTIME_SYNCHOOK prevHook; + extern char* sysBootLine; +static void timeSync(int synchronized) { + if (!tz2timezone()) + ClockTime_syncHook = prevHook; /* Don't call me again */ +} + static int timeRegister(void) { - /* If TIMEZONE not defined, set it from EPICS_TIMEZONE */ - if (getenv("TIMEZONE") == NULL) { - const char *timezone = envGetConfigParamPtr(&EPICS_TIMEZONE); - if (timezone == NULL) { - printf("timeRegister: No Time Zone Information\n"); - } else { - epicsEnvSet("TIMEZONE", timezone); + /* If TZ not defined, set it from EPICS_TZ */ + if (getenv("TZ") == NULL) { + const char *tz = envGetConfigParamPtr(&EPICS_TZ); + + if (tz && *tz) { + epicsEnvSet("TZ", tz); + + /* Call tz2timezone() once we know what year it is */ + prevHook = ClockTime_syncHook; + ClockTime_syncHook = timeSync; } + else if (getenv("TIMEZONE") == NULL) + printf("timeRegister: No Time Zone Information available\n"); } // Define EPICS_TS_FORCE_NTPTIME to force use of NTPTime provider @@ -57,7 +73,7 @@ static int timeRegister(void) } if (useNTP) { - // Start NTP first so it can be used to sync SysTime + // Start NTP first so it can be used to sync ClockTime NTPTime_Init(100); ClockTime_Init(CLOCKTIME_SYNC); } else { diff --git a/src/libCom/osi/os/vxWorks/tz2timezone.c b/src/libCom/osi/os/vxWorks/tz2timezone.c new file mode 100644 index 000000000..df8db8514 --- /dev/null +++ b/src/libCom/osi/os/vxWorks/tz2timezone.c @@ -0,0 +1,278 @@ +/*************************************************************************\ +* Copyright (c) 2009 Brookhaven Science Associates, as Operator of +* Brookhaven National Laboratory. +* Copyright (c) 2019 UChicago Argonne LLC, as Operator of Argonne +* National Laboratory. +* EPICS BASE is distributed subject to a Software License Agreement found +* in file LICENSE that is included with this distribution. +\*************************************************************************/ +/* + * Authors: Larry Hoff, Andrew Johnson + */ + +/* + * This file exports a single function "int tz2timezone(void)" which reads + * the TZ environment variable (defined by POSIX) and converts it to the + * TIMEZONE environment variable defined by ANSI. The latter is used by + * VxWorks "time" functions, is largely deprecated in other computing + * environments, has limitations, and is difficult to maintain. This holds + * out the possibility of "pretending" that VxWorks supports "TZ" - until + * such time as it actually does. + * + * For simplicity, only the "POSIX standard form" of TZ will be supported. + * Even that is complicated enough (see following spec). Furthermore, + * only the "M" form of DST start and stop dates are supported. + * + * TZ = zone[-]offset[dst[offset],start[/time],end[/time]] + * + * zone + * A three or more letter name for the timezone in normal (winter) time. + * + * [-]offset + * A signed time giving the offset of the time zone westwards from + * Greenwich. The time has the form hh[:mm[:ss]] with a one of two + * digit hour, and optional two digit minutes and seconds. + * + * dst + * The name of the time zone when daylight saving is in effect. It may + * be followed by an offset giving how big the adjustment is, required + * if different than the default of 1 hour. + * + * start/time,end/time + * Specify the start and end of the daylight saving period. The start + * and end fields indicate on what day the changeover occurs, and must + * be in this format: + * + * Mm.n.d + * This indicates month m, the n-th occurrence of day d, where + * 1 <= m <= 12, 1 <= n <= 5, 0 <= d <= 6, 0=Sunday + * The 5th occurrence means the last occurrence of that day in a + * month. So M4.1.0 is the first Sunday in April, M9.5.0 is the + * last Sunday in September. + * + * The time field indicates what hour the changeover occurs on the given + * day (TIMEZONE only supports switching on the hour). + * + */ + +#include +#include /* getenv() */ +#include /* printf() */ +#include /* strchr() */ +#include /* isalpha() */ + +#include + +/* for reference: TZ syntax, example, and TIMEZONE example + * std offset dst [offset],start[/time],end[/time] + * CST6CDT5,M3.2.0,M11.1.0 + * EST+5EDT,M4.1.0/2,M10.5.0/2 + * + * std offset start stop + * TIMEZONE=EST::300:030802:110102 + */ + +static int extractDate(const char *tz, struct tm *current, char *s) +{ + static const int startdays[] = { + 0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334 + }; + static const int molengths[] = { + 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 + }; + + int month, week, weekday, hour=2; /* default=2AM */ + int jan1wday, wday, mday; + + /* Require 'M' format */ + if (*++tz != 'M') { + printf("tz2timezone: Unsupported date type, need 'M' format\n"); + return ERROR; + } + tz++; + + if (sscanf(tz, "%d.%d.%d/%d", &month, &week, &weekday, &hour) < 3) + return ERROR; /* something important missing */ + + if (month == 0 || month>12 || + week < 1 || week > 5 || + weekday < 0 || weekday > 6 || + hour < 0 || hour > 23) + return ERROR; + + /* Now for some brute-force calendar calculations... */ + /* start month is in "month", and the day is "weekday", but + we need to know when that weekday first occurs in that month */ + /* Let's start with weekday on Jan. 1 */ + jan1wday = (7 + current->tm_wday - (current->tm_yday % 7)) % 7; + + /* We need to know if it is a leap year (and if it matters) */ + /* Let's assume that we're working with a date between 1901 and 2099, + that way we don't have to think about the "century exception". + If this code is still running (unchanged) in 2100, I'll be stunned + (and 139 years old) */ + wday = (jan1wday + startdays[month-1] + + ((month > 2 && (current->tm_year % 4 == 0)) ? 1 : 0)) % 7; + + /* Let's see on what day-of-the-month the first target weekday occurs + (counting from 1). The result is a number between 1 and 7, inclusive. */ + mday = 1 + ((7 + weekday - wday) % 7); + + /* Next, we add the week offset. If we overflow the month, we subtract + one week */ + mday += 7 * (week - 1); + if (mday > molengths[month-1]) + mday -= 7; + + /* Should I handle Feb 29? I'm willing to gamble that no one in their right + mind would schedule a time change to occur on Feb. 29. If so, we'll be a + week early */ + sprintf(s, "%02d%02d%02d", month, mday, hour); + + return OK; +} + + +static const char *getTime(const char *s, int *time) +{ + /* Time format is [+/-]hh[:mm][:ss] followed by the next zone name */ + + *time = 0; + + if (!isdigit((int) s[0])) + return s; /* no number here... */ + + if (!isdigit((int) s[1])) { /* single digit form */ + *time = s[0] - '0'; + return s + 1; + } + + if (isdigit((int) s[1])) { /* two digit form */ + *time = 10 * (s[0] - '0') + (s[1] - '0'); + return s + 2; + } + + return s; /* does not follow supported form */ +} + +int tz2timezone(void) +{ + const char *tz = getenv("TZ"); + /* Spec. says that zone names must be at least 3 chars. + * I've never seen a longer zone name, but I'll allocate + * 40 chars. If you live in a zone with a longer name, + * you may want to think about the benefits of relocation. + */ + char zone[40]; + char start[10], stop[10]; /* only really need 7 bytes now */ + int hours = 0, minutes = 0, sign = 1; + /* This is more than enough, even with a 40-char zone + * name, and 4-char offset. + */ + char timezone[100]; + int i = 0; /* You *always need an "i" :-) */ + epicsTimeStamp now; + struct tm current; + + /* First let's get the current time. We need the year to + * compute the start/stop dates for DST. + */ + if (epicsTimeGetCurrent(&now) || + epicsTimeToTM(¤t, NULL, &now)) + return ERROR; + + /* Make sure TZ exists. + * Spec. says that ZONE must be at least 3 chars. + */ + if ((!tz) || (strlen(tz) < 3)) + return ERROR; + + /* OK, now a bunch of brute-force parsing. My brain hurts if + * I try to think of an elegant regular expression for the + * string. + */ + + /* Start extracting zone name, must be alpha */ + while ((i < sizeof(zone) - 1) && isalpha((int) *tz)) { + zone[i++] = *tz++; + } + if (i < 3) + return ERROR; /* Too short, not a real zone name? */ + + zone[i] = 0; /* Nil-terminate (for now) */ + + /* Now extract offset time. The format is [+/-]hh[:mm[:ss]] + * Recall that TIMEZONE doesn't support seconds.... + */ + if (*tz == '-') { + sign = -1; + tz++; + } + else if (*tz == '+') { + tz++; + } + + /* Need a digit now */ + if (!isdigit((int) *tz)) + return ERROR; + + /* First get the hours */ + tz = getTime(tz, &hours); + if (hours > 24) + return ERROR; + + if (*tz == ':') { /* There is a minutes part */ + /* Need another digit now */ + if (!isdigit((int) *++tz)) + return ERROR; + + /* Extract the minutes */ + tz = getTime(tz, &minutes); + if (minutes > 60) + return ERROR; + + /* Skip any seconds part */ + if (*tz == ':') { + int seconds; + tz = getTime(tz + 1, &seconds); + } + } + + /* Extract any DST zone name, must be alpha */ + if (isalpha((int) *tz)) { + zone[i++] = '/'; /* Separate the names */ + + while ((i < sizeof(zone) - 1) && isalpha((int) *tz)) { + zone[i++] = *tz++; + } + zone[i] = 0; /* Nil-terminate */ + } + + minutes += hours * 60; + minutes *= sign; + + /* Look for start/stop dates - require neither or both */ + tz = strchr(tz, ','); + if (!tz) { /* No daylight savings time here */ + /* Format the env. variable */ + sprintf(timezone, "TIMEZONE=%s::%d", zone, minutes); + } + else { + if (extractDate(tz, ¤t, start) != OK) + return ERROR; + + tz = strchr(tz + 1, ','); + if (!tz) + return ERROR; + if (extractDate(tz, ¤t, stop) != OK) + return ERROR; + + /* Format the env. variable */ + sprintf(timezone, "TIMEZONE=%s::%d:%s:%s", zone, minutes, start, stop); + } + + /* Make it live! */ + putenv(timezone); + + return OK; +} diff --git a/src/libCom/osi/osiClockTime.c b/src/libCom/osi/osiClockTime.c index fb9d1532f..01958b20e 100644 --- a/src/libCom/osi/osiClockTime.c +++ b/src/libCom/osi/osiClockTime.c @@ -23,7 +23,8 @@ #include "taskwd.h" #define NSEC_PER_SEC 1000000000 -#define ClockTimeSyncInterval_value 60.0 +#define ClockTimeSyncInterval_initial 1.0 +#define ClockTimeSyncInterval_normal 60.0 static struct { @@ -79,7 +80,7 @@ static void ClockTime_InitOnce(void *pfirst) ClockTimePvt.loopEvent = epicsEventMustCreate(epicsEventEmpty); ClockTimePvt.lock = epicsMutexCreate(); - ClockTimePvt.ClockTimeSyncInterval = 1.0; /* First sync */ + ClockTimePvt.ClockTimeSyncInterval = ClockTimeSyncInterval_initial; epicsAtExit(ClockTime_Shutdown, NULL); @@ -148,6 +149,8 @@ void ClockTime_GetProgramStart(epicsTimeStamp *pDest) /* Synchronization thread */ #if defined(vxWorks) || defined(__rtems__) +CLOCKTIME_SYNCHOOK ClockTime_syncHook = NULL; + static void ClockTimeSync(void *dummy) { taskwdInsert(0, NULL, NULL); @@ -179,11 +182,16 @@ static void ClockTimeSync(void *dummy) ClockTimePvt.syncTime = timeNow; epicsMutexUnlock(ClockTimePvt.lock); - ClockTimePvt.ClockTimeSyncInterval = ClockTimeSyncInterval_value; + if (ClockTime_syncHook) + ClockTime_syncHook(1); + + ClockTimePvt.ClockTimeSyncInterval = ClockTimeSyncInterval_normal; } } ClockTimePvt.synchronized = 0; + if (ClockTime_syncHook) + ClockTime_syncHook(0); taskwdRemove(0); } #endif diff --git a/src/libCom/osi/osiClockTime.h b/src/libCom/osi/osiClockTime.h index 17eacab3e..23598886d 100644 --- a/src/libCom/osi/osiClockTime.h +++ b/src/libCom/osi/osiClockTime.h @@ -19,6 +19,12 @@ void ClockTime_Init(int synchronize); void ClockTime_Shutdown(void *dummy); int ClockTime_Report(int level); +#if defined(vxWorks) || defined(__rtems__) +typedef void (* CLOCKTIME_SYNCHOOK)(int synchronized); + +extern CLOCKTIME_SYNCHOOK ClockTime_syncHook; +#endif + #ifdef __cplusplus } #endif