The ptp4l program supports up to four time zones via the ALTERNATE_TIME_OFFSET_INDICATOR TLV. Introduce a helper program that leverages the local time zone database to monitor for changes in daylight savings time and publishing them.
Signed-off-by: Richard Cochran <richardcoch...@gmail.com> --- makefile | 7 +- tz2alt.8 | 126 ++++++++++++++++++ tz2alt.c | 379 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 510 insertions(+), 2 deletions(-) create mode 100644 tz2alt.8 create mode 100644 tz2alt.c diff --git a/makefile b/makefile index 0f8f185..3e3b8b3 100644 --- a/makefile +++ b/makefile @@ -22,7 +22,7 @@ CC = $(CROSS_COMPILE)gcc VER = -DVER=$(version) CFLAGS = -Wall $(VER) $(incdefs) $(DEBUG) $(EXTRA_CFLAGS) LDLIBS = -lm -lrt -pthread $(EXTRA_LDFLAGS) -PRG = ptp4l hwstamp_ctl nsm phc2sys phc_ctl pmc timemaster ts2phc +PRG = ptp4l hwstamp_ctl nsm phc2sys phc_ctl pmc timemaster ts2phc tz2alt FILTERS = filter.o mave.o mmedian.o SERVOS = linreg.o ntpshm.o nullf.o pi.o refclock_sock.o servo.o TRANSP = raw.o transport.o udp.o udp6.o uds.o @@ -35,7 +35,7 @@ OBJ = bmc.o clock.o clockadj.o clockcheck.o config.o designated_fsm.o \ unicast_fsm.o unicast_service.o util.o version.o OBJECTS = $(OBJ) hwstamp_ctl.o nsm.o phc2sys.o phc_ctl.o pmc.o pmc_agent.o \ - pmc_common.o sysoff.o timemaster.o $(TS2PHC) + pmc_common.o sysoff.o timemaster.o $(TS2PHC) tz2alt.o SRC = $(OBJECTS:.o=.c) DEPEND = $(OBJECTS:.o=.d) srcdir := $(dir $(lastword $(MAKEFILE_LIST))) @@ -72,6 +72,9 @@ ts2phc: config.o clockadj.o hash.o interface.o msg.o phc.o pmc_agent.o \ pmc_common.o print.o $(SERVOS) sk.o $(TS2PHC) tlv.o transport.o raw.o \ udp.o udp6.o uds.o util.o version.o +tz2alt: config.o hash.o interface.o lstab.o msg.o phc.o pmc_common.o print.o \ + sk.o tlv.o $(TRANSP) tz2alt.o util.o version.o + version.o: .version version.sh $(filter-out version.d,$(DEPEND)) .version: force diff --git a/tz2alt.8 b/tz2alt.8 new file mode 100644 index 0000000..66a6605 --- /dev/null +++ b/tz2alt.8 @@ -0,0 +1,126 @@ +.TH TS2ALT 8 "February 2023" "linuxptp" +.SH NAME +tz2alt - Monitors daylight savings time changes and publishes them to PTP stack. + +.SH SYNOPSIS +.B ts2alt +[ +.B \-hmqv +] [ +.BI \-f " conf" +] [ +.BI \-k " key" +] [ +.BI \-p " period" +] [ +.BI \-w " window" +] [ +.BI \-z " timezone" +] [ +.I long-options +] +.I .\|.\|. + +.SH DESCRIPTION +.B tz2alt +leverages the local time zone database to monitor for changes in +daylight savings time and publishes the pending changes to the PTP +stack. + +.SH OPTIONS +.TP +.BI \-f " config" +Read configuration from the specified file. +No configuration file is read by default. +.TP +.BI \-h +Displays the command line help summary. +.TP +.BI \-l " print-level" +Sets the maximum syslog level of messages which should be printed or +sent to the system logger. The default is 6 (LOG_INFO). +.TP +.B \-m +Prints log messages to the standard output. +.TP +.B \-q +Prevents sending log messages to the system logger. +.TP +.B \-v +Prints the software version and exits. + +.SH LONG OPTIONS + +Each and every configuration file option (see below) may also appear +as a "long" style command line argument. For example, the use_syslog +option may be set using either of these two forms. + +.RS +\f(CW\-\-use_syslog 1 \-\-use_syslog=1\fP +.RE + +Option values given on the command line override values in the global +section of the configuration file. + +.SH CONFIGURATION FILE + +The configuration file is divided into sections. Each section starts with a +line containing its name enclosed in brackets and it follows with settings. +Each setting is placed on a separate line, it contains the name of the +option and the value separated by whitespace characters. Empty lines and lines +starting with # are ignored. + +.SH GLOBAL OPTIONS + +.TP +.B domainNumber +The domain attribute of the local clock. +The default is 0. + +.TP +.B leapfile +The path to the current leap seconds definition file. In a Debian +system this file is provided by the tzdata package and can be found at +/usr/share/zoneinfo/leap-seconds.list. If a leapfile is configured it +will be reloaded if modified. The default is an empty string, which +causes the program to use a hard coded table that reflects the known +leap seconds on the date of the software's release. + +.TP +.B logging_level +The maximum logging level of messages which should be printed. +The default is 6 (LOG_INFO). + +.TP +.B message_tag +The tag which is added to all messages printed to the standard output +or system log. The default is an empty string (which cannot be set in +the configuration file as the option requires an argument). + +.TP +.B transportSpecific +The transport specific field. Must be in the range 0 to 255. +The default is 0. + +.TP +.B use_syslog +Print messages to the system log if enabled. The default is 1 (enabled). + +.TP +.B verbose +Print messages to the standard output if enabled. The default is 0 (disabled). + +.SH WARNING + +Be cautious when sharing the same configuration file between ptp4l, +phc2sys, and tz2alt. Keep in mind that values specified in the +configuration file take precedence over the default values. If an +option which is common to the other programs is set in the +configuration file, then the value will be applied to all the programs +using the file, and this might not be what is expected. + +It is recommended to use separate configuration files for ptp4l, +phc2sys, and tz2alt in order to avoid any unexpected behavior. + +.SH SEE ALSO +.BR ptp4l (8) diff --git a/tz2alt.c b/tz2alt.c new file mode 100644 index 0000000..ed06420 --- /dev/null +++ b/tz2alt.c @@ -0,0 +1,379 @@ +/** + * @file tztool.c + * @note Copyright (C) 2021 Richard Cochran <richardcoch...@gmail.com> + * @note SPDX-License-Identifier: GPL-2.0+ + */ +#include <errno.h> +#include <stdbool.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <time.h> +#include <unistd.h> + +#include "config.h" +#include "lstab.h" +#include "pmc_common.h" +#include "print.h" +#include "version.h" +#include "tz.h" + +#define DEFAULT_TZ "PST8PDT" +#define DEFAULT_PERIOD 3600 +#define DEFAULT_WINDOW (3600 * 24 * 30 * 3) + +static int key_field, period = DEFAULT_PERIOD, window = DEFAULT_WINDOW; +static char uds_local[MAX_IFNAME_SIZE + 1]; +static struct lstab *lstab; +static struct config *cfg; + +struct tzinfo { + const char *name; + char display_name[MAX_TZ_DISPLAY_NAME + 1]; + time_t timestamp; + + /* Following fields populated by get_offsets. */ + + time_t local_utc_offset; + time_t local_tai_offset; + int tai_utc_offset; + enum lstab_result tai_result; +}; + +static int get_offsets(struct tzinfo *tz); + +static bool offsets_equal(struct tzinfo *a, struct tzinfo *b) +{ + return a->local_utc_offset == b->local_utc_offset; +} + +static bool find_next_discontinuity(struct tzinfo *tz, struct tzinfo *next) +{ + time_t i, j, n; + bool gt; + + next->timestamp = tz->timestamp + window; + get_offsets(next); + if (offsets_equal(tz, next)) { + return false; + } + + i = 0; + j = window; + + while (1) { + next->timestamp = tz->timestamp + i; + get_offsets(next); + gt = offsets_equal(tz, next); + if (gt) { + n = j - i - 1; + } else { + j = i; + i = 0; + n = j - i - 1; + } + if (!n) { + if (gt) { + next->timestamp++; + get_offsets(next); + } + break; + } + i += (n + 1) / 2; + } + + return true; +} + +static int get_offsets(struct tzinfo *tz) +{ + struct tm tm = {0}; + time_t t2; + + tz->tai_result = lstab_utc2tai(lstab, tz->timestamp, + &tz->tai_utc_offset); + if (tz->tai_result == LSTAB_UNKNOWN) { + pr_err("leap second table is stale"); + return -1; + } + + setenv("TZ", tz->name, 1); + tzset(); + if (!localtime_r(&tz->timestamp, &tm)) { + return -1; + } + + setenv("TZ", "UTC", 1); + tzset(); + t2 = mktime(&tm); + tz->local_utc_offset = t2 - tz->timestamp; + tz->local_tai_offset = tz->local_utc_offset - tz->tai_utc_offset; + + return 0; +} + +static int get_unambiguous_time(struct tzinfo *tz) +{ + int err; + + do { + tz->timestamp = time(NULL); + err = get_offsets(tz); + } while (tz->tai_result == LSTAB_AMBIGUOUS); + + return err; +} + +static void show_timezone_info(const char *label, struct tzinfo *tz) +{ + pr_debug("%s %s ts %ld local-utc %ld tai-utc %d local-tai %ld %s", + label, + tz->name, + tz->timestamp, + tz->local_utc_offset, + tz->tai_utc_offset, + tz->local_tai_offset, + tz->tai_result == LSTAB_OK ? "valid" : "invalid"); +} + +/* Returns true if display name was truncated. */ +static bool tz_set_name(struct tzinfo *tz, const char *name) +{ + const char *suffix; + int len; + + memset(tz->display_name, 0, sizeof(tz->display_name)); + tz->name = name; + + len = strlen(name); + if (len <= MAX_TZ_DISPLAY_NAME) { + strncpy(tz->display_name, name, sizeof(tz->display_name) - 1); + return false; + } + + /* + * The displayName field is limited to 10 characters, but + * there are many valid time zone names like "Europe/Vienna". + * Use the suffix if present. + */ + suffix = strchr(name, '/'); + if (suffix) { + suffix++; + len = strlen(suffix); + if (len > 0 && len <= MAX_TZ_DISPLAY_NAME) { + strncpy(tz->display_name, suffix, + sizeof(tz->display_name) - 1); + return true; + } + } + + /* No nice suffix to be found, so just truncate. */ + strncpy(tz->display_name, name, sizeof(tz->display_name) - 1); + + return true; +} + +static int update_ptp_serivce(struct tzinfo *tz, struct tzinfo *next) +{ + struct alternate_time_offset_properties atop; + struct management_tlv_datum mtd; + uint64_t time_of_next_jump; + struct pmc *pmc; + int err; + + pmc = pmc_create(cfg, TRANS_UDS, uds_local, 0, + config_get_int(cfg, NULL, "domainNumber"), + config_get_int(cfg, NULL, "transportSpecific") << 4, 1); + if (!pmc) { + return -1; + } + err = pmc_send_set_aton(pmc, MID_ALTERNATE_TIME_OFFSET_NAME, + key_field, tz->display_name); + if (err) { + return err; + } + memset(&atop, 0, sizeof(atop)); + atop.keyField = key_field; + atop.currentOffset = tz->local_tai_offset; + if (next) { + atop.jumpSeconds = next->local_tai_offset - tz->local_tai_offset; + time_of_next_jump = (uint64_t) next->timestamp; + atop.timeOfNextJump.seconds_lsb = time_of_next_jump & 0xffffffff; + atop.timeOfNextJump.seconds_msb = time_of_next_jump >> 32; + } + err = pmc_send_set_action(pmc, MID_ALTERNATE_TIME_OFFSET_PROPERTIES, + &atop, sizeof(atop)); + if (err) { + return err; + } + mtd.val = key_field; + mtd.reserved = 1; /*enable field*/ + err = pmc_send_set_action(pmc, MID_ALTERNATE_TIME_OFFSET_ENABLE, + &mtd, sizeof(mtd)); + if (err) { + return err; + } + + pmc_destroy(pmc); + return 0; +} + +static int do_tztool(const char *timezone) +{ + struct tzinfo nx, tz; + const char *leapfile; + bool pending; + char buf[64]; + int err; + + if (key_field > MAX_TIME_ZONES - 1) { + pr_err("key field %d exceeds maximum of %d", key_field, + MAX_TIME_ZONES - 1); + return -1; + } + + tz_set_name(&nx, timezone); + if (tz_set_name(&tz, timezone)) { + pr_info("truncating time zone display name from %s to %s", + tz.name, tz.display_name); + } + + leapfile = config_get_string(cfg, NULL, "leapfile"); + if (!leapfile) { + pr_err("please specify leap second table with --leapfile"); + return -1; + } + + while (is_running()) { + + /* Read the leap seconds file again as it may have changed. */ + lstab = lstab_create(leapfile); + if (!lstab) { + pr_err("failed to create leap second table"); + return -1; + } + + err = get_unambiguous_time(&tz); + if (err) { + return err; + } + show_timezone_info("current time = ", &tz); + + pending = find_next_discontinuity(&tz, &nx); + if (pending) { + setenv("TZ", nx.name, 1); + tzset(); + if (ctime_r(&nx.timestamp, buf)) { + buf[strlen(buf) - 1] = 0; + } + show_timezone_info("discontinuity = ", &nx); + pr_info("next discontinuity %s %s", buf, nx.name); + } else { + pr_info("no discontinuity within %d second window", window); + } + + lstab_destroy(lstab); + lstab = NULL; + + err = update_ptp_serivce(&tz, pending ? &nx : NULL); + if (err) { + pr_err("failed to update PTP service"); + return err; + } + sleep(period); + } + return 0; +} + +static void usage(char *progname) +{ + fprintf(stderr, + "\nusage: %s [options]\n\n" + " -f [file] read configuration from 'file'\n" + " -h prints this message and exits\n" + " -k [num] key field for the ALTERNATE_TIME_OFFSET_INDICATOR TLV\n" + " -p [num] period between updates in seconds, default %d\n" + " -v prints the software version and exits\n" + " -w [num] look ahead time window in seconds, default %d\n" + " -z zone Time zone string, default '%s'\n" + " See /usr/share/zoneinfo for valid strings\n" + "\n", + progname, DEFAULT_PERIOD, DEFAULT_WINDOW, DEFAULT_TZ); +} + +int main(int argc, char *argv[]) +{ + char *config = NULL, *progname, *timezone = DEFAULT_TZ; + int c, err = 0, index; + struct option *opts; + + if (handle_term_signals()) { + return -1; + } + cfg = config_create(); + if (!cfg) { + return -1; + } + opts = config_long_options(cfg); + print_set_verbose(1); + print_set_syslog(0); + + /* Process the command line arguments. */ + progname = strrchr(argv[0], '/'); + progname = progname ? 1+progname : argv[0]; + while (EOF != (c = getopt_long(argc, argv, "f:hk:p:vw:z:", opts, &index))) { + switch (c) { + case 0: + if (config_parse_option(cfg, opts[index].name, optarg)) { + config_destroy(cfg); + return -1; + } + break; + case 'f': + config = optarg; + break; + case 'k': + key_field = atoi(optarg); + break; + case 'p': + period = atoi(optarg); + break; + case 'v': + version_show(stdout); + config_destroy(cfg); + return 0; + case 'w': + window = atoi(optarg); + break; + case 'z': + timezone = optarg; + break; + case 'h': + usage(progname); + config_destroy(cfg); + return 0; + case '?': + default: + usage(progname); + config_destroy(cfg); + return -1; + } + } + + print_set_syslog(0); + print_set_verbose(1); + + if (config && (err = config_read(config, cfg))) { + goto out; + } + + print_set_progname(progname); + print_set_tag(config_get_string(cfg, NULL, "message_tag")); + print_set_level(config_get_int(cfg, NULL, "logging_level")); + snprintf(uds_local, sizeof(uds_local), "/var/run/tztool.%d", getpid()); + + err = do_tztool(timezone); +out: + config_destroy(cfg); + return err; +} -- 2.30.2 _______________________________________________ Linuxptp-devel mailing list Linuxptp-devel@lists.sourceforge.net https://lists.sourceforge.net/lists/listinfo/linuxptp-devel