summaryrefslogtreecommitdiff
blob: efbf5799a0913be3ad10b588621bf5161133957a (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
#! /usr/bin/perl -w

# KuroBox/Linkstation Clock Calibration Program
#
# Sets the internal system 'tickadj' variable to
# the optimum value, computed by comparing elapsed
# time on the local box versus that indicated by
# an external NTP server.
#
# The sample window is set to 5min by default, but
# empirical tests indicate an interval even as low
# as 1min gives pretty good results.
#
# Use as:  adjtime.pl -t -s 68.12.13.56 -i 60
#          adjtime.pl -v -s 68.12.13.56 -i 60
#
# 2005-02-13 v0.2 don north ak6dn@mindspring.com
# - initial version
# 2005-02-15 v0.3 don north
# - added error checking for missing servers, etc
# - range limited tickadj to +/-10% per iteration
# 2005-02-20 v0.4 don north
# - added retry count/delay for ntp server

# generic defaults
my $VERSION = 'v0.4'; # version of code
my $DEBUG = 0; # set to 1 for debug messages
my $VERBOSE = 0; # set to 1 for verbose messages

# specific defaults
my $NTPSERVER = ''; # ip address of NTP server
my $INTERVAL = 5*60; # seconds default sample interval
my $CHECKEND = 0; # set to 1 to check final tick calibration
my $CHECKONLY = 0; # set to 1 to only check clock, no tick update
my $RETRYCOUNT = 8; # retry count before giving up
my $RETRYDELAY = 5; # base retry delay, in seconds

my $ERROR = 0;
for (my $i = 0; $i <= $#ARGV; $i++) {
    if    ($ARGV[$i] eq '-h') { $ERROR = 1; }
    elsif ($ARGV[$i] eq '-d') { $VERBOSE = 1; $DEBUG = 1; }
    elsif ($ARGV[$i] eq '-v') { $VERBOSE = 1; }
    elsif ($ARGV[$i] eq '-c') { $CHECKEND = 1; }
    elsif ($ARGV[$i] eq '-t') { $VERBOSE = 1; $CHECKONLY = 1; }
    elsif ($ARGV[$i] eq '-s') { $NTPSERVER = $ARGV[++$i]; }
    elsif ($ARGV[$i] eq '-i') { $INTERVAL = $ARGV[++$i]; }
    elsif ($ARGV[$i] eq '-r') { $RETRYCOUNT = $ARGV[++$i]; }
    elsif ($ARGV[$i] eq '-R') { $RETRYDELAY = $ARGV[++$i]; }
    else                      { $ERROR = 1; }
}

# say hello
printf STDERR "adjtime.pl %s for Kurobox/Linkstation (perl %g)\n", $VERSION, $] if $VERBOSE;

# check for correct arguments present, print usage if errors
if ($ERROR || $INTERVAL <= 0) {
    print STDERR "Usage: $0 [options...]\n";
    print STDERR <<"EOF";
       -h            help; print this message
       -d            enable debug mode
       -v            verbose status reporting
       -t            test only; check clock calibration, no update
       -c            check clock calibration after setting tickadj
       -s IPADDR     ip address of NTP server [use /etc/ntp.conf]
       -i DELAY      sample interval, seconds [$INTERVAL]
       -r COUNT      NTP server retry count [$RETRYCOUNT]
       -R DELAY      NTP server query retry delay, seconds [$RETRYDELAY]
EOF
    # exit if errors...
    die "Aborted due to command line errors.\n";
}

# globals
my $DELTA = 0; # actual time slept, seconds
my $OFFSET = 0; # clock error, seconds

# --------------------------------------------------------------------------------

# get an ntp time server from the ntp config file in none specified

unless ($NTPSERVER) {
    if (open(INP, "< /etc/ntp.conf")) {
	while (my $line = scalar(<INP>)) {
	    if ($line =~ m/^\s*server\s+(\S+)/i) {
		$NTPSERVER = $1;
		last;
	    }
	}
	close(INP);
    }
}
printf STDERR "Using NTP server at '%s'\n", $NTPSERVER if $VERBOSE;

# --------------------------------------------------------------------------------

# get the calibration factors by syncing the clock an hour apart
#
#   ntpdate -b server; sleep 3600; ntpdate -b server
#   13 Sep 14:55:31 ntpdate[8507]: step time server 192.168.1.1 offset 0.190378 sec
#   13 Sep 15:55:28 ntpdate[8509]: step time server 192.168.1.1 offset -3.261289 sec

if (1) { 

    # set the clock from the ntp server, discard offset
    $OFFSET = get_offset();
    die "Error: no NTP server response from $NTPSERVER" if $OFFSET eq 'N/A';
    printf STDERR "Initial time offset is %f seconds\n", $OFFSET if $VERBOSE;
	   
    # wait a long time...
    printf STDERR "Going to sleep for %d seconds ... ", $INTERVAL if $VERBOSE;
    $DELTA = time();
    sleep($INTERVAL);
    $DELTA = time()-$DELTA;
    printf STDERR "slept for %d seconds\n", $DELTA if $VERBOSE;

    # set the clock from the ntp server, offset is our clock error
    $OFFSET = get_offset();
    die "Error: no NTP server response from $NTPSERVER" if $OFFSET eq 'N/A';
    printf STDERR "Final time offset is %f seconds\n", $OFFSET if $VERBOSE;

}

# --------------------------------------------------------------------------------

# Now compute the correct value of the tickadj number as:
#
#   corr tick = curr tick * (sample time [sec] + offset [sec]) / sample time [sec]
#             = 10000 * (3600 + (-3.261289)) / 3600
#             = 9991

if (1) {

    my ($TICKOLD,$TICKNEW,$TICKOPT,$TICKEND) = (10000,10000,10000,10000);

    # get current tickadj value
    $TICKOLD = get_tickadj();
    printf STDERR "Current tick adjustment is %d\n", $TICKOLD if $VERBOSE;

    # compute what the tickadj should be based on time measurement
    $TICKOPT = $TICKOLD*($DELTA+$OFFSET)/$DELTA;
    printf STDERR "Optimal tick adjustment is %d [%f]\n", int($TICKOPT+0.5),$TICKOPT if $VERBOSE;
    # only change the tickadj by at most +/-10% in one shot, convert to an int
    $TICKNEW = int(max(min($TICKOPT,1.1*$TICKOLD),0.9*$TICKOLD)+0.5);
    printf STDERR "Range limited tick adjustment is %d\n", $TICKNEW
	if $VERBOSE && $TICKNEW != int($TICKOPT+0.5);

    if ($TICKOLD == $TICKNEW) {
	# old/new values same, no adjustment required
	printf STDERR "No tick adjustment required\n" if $VERBOSE;
    } elsif (!$CHECKONLY) {
	# update the tickadj value, if different
    	$TICKEND = get_tickadj($TICKNEW);
    	printf STDERR "Updated tick adjustment is %d\n", $TICKEND if $VERBOSE;
	printf STDERR "Error: Failed tickadj; exp=%d, rcv=%d\n", $TICKNEW, $TICKEND
	    if $TICKNEW != $TICKEND;
    }

}

# --------------------------------------------------------------------------------

# After the tickadj update, now recheck the calibration of the clock:
#
#   ntpdate -b server; sleep 3600; ntpdate -b server
#   23 Nov 12:29:50 ntpdate[496]: step time server server offset 0.001564 sec
#   23 Nov 13:29:55 ntpdate[498]: step time server server offset 0.003010 sec

if ($CHECKEND) {

    # set the clock from the ntp server, discard offset
    $OFFSET = get_offset();
    die "Error: no NTP server response from $NTPSERVER" if $OFFSET eq 'N/A';
    printf STDERR "Initial time offset is %f seconds\n", $OFFSET if $VERBOSE;
	   
    # wait a long time...
    printf STDERR "Going to sleep for %d seconds ... ", $INTERVAL if $VERBOSE;
    $DELTA = time();
    sleep($INTERVAL);
    $DELTA = time()-$DELTA;
    printf STDERR "slept for %d seconds\n", $DELTA if $VERBOSE;

    # set the clock from the ntp server, offset is our clock error
    $OFFSET = get_offset();
    die "Error: no NTP server response from $NTPSERVER" if $OFFSET eq 'N/A';
    printf STDERR "Final time offset is %f seconds\n", $OFFSET if $VERBOSE;

}

# --------------------------------------------------------------------------------

exit;

##################################################################################

# return time offset from ntp server, or N/A if not available

sub get_offset { # ()

    foreach my $k (1..$RETRYCOUNT) {
    	# query the server, check response
    	foreach my $line (`/usr/sbin/ntpdate -b $NTPSERVER`) {
	    printf STDERR "DEBUG: [%s]\n", $line if $DEBUG;
	    # return offset if we get a valid number response
	    return $1 if $line =~ m/\s+offset\s+(-?\d+[.]\d+)\s+sec\s*$/i;
    	}
    	# no response, retry with exponential delay
    	printf STDERR "Error: no response from server %s, will retry in %d sec\n",
    		$NTPSERVER, $RETRYDELAY*2**($k-1) if $VERBOSE;
    	sleep($RETRYDELAY*2**($k-1));
    }
    # whoops, no response at all
    return 'N/A';

}

##################################################################################

# return system tick adj value, set it if non-blank argument supplied

sub get_tickadj { # ($)

    my ($tick) = @_;
    $tick = '' unless defined($tick);

    foreach my $line (`/usr/bin/tickadj $tick`) {
	printf STDERR "DEBUG: [%s]\n", $line if $DEBUG;
	return $1 if $line =~ m/^tick\s+=\s+(\d+)\s*$/i;
    }
    # whoops, tickadj failed
    return -1;

}

##################################################################################

sub min { my ($a,$b) = @_; return $a <= $b ? $a : $b; }

sub max { my ($a,$b) = @_; return $a >= $b ? $a : $b; }

##################################################################################

# the end