Posts

NTP with GPS and PPS working without network clock

Is it possible to have a PPS disciplined NTP server without a network reference clock or network connection?

While doing some camera synchronisation and timestamp testing, I setup a Raspberry Pi NTP server with PPS using an Adafruit GPS hat so that I could sync my cameras’ time to NTP, trigger them using the PPS signal, generate UTC timestamps and check the timestamps against the expected UTC second boundaries.

As part of this the question once again arose – Can I configure an NTP server with GPS time and PPS without having to configure a network reference clock along with the GPS and PPS sources?  This was important as my test setup didn’t have access to an external network, and yet I wanted typical NTP offsets of < 100us.

Well with quite a bit of messing I finally got it working, at this stage there are lots of pages that detail how to setup a Pi as an NTP server with PPS, some mention that you must have a network reference and some mention that you don’t – I had to do a lot of digging and a lot of messing to get to the bottom of things.

Amusingly, NTP configuration is something that ChatGPT is simply terrible at – don’t use it for this, it will just waste your time!

I had lots of problems, but the 2 most persistent was:

  1. The PPS source being marked as a false ticker (with an x to its left in the ntpq -p output)
  2. The PPS source not being referenced at all, “when” would remain at “-“

For ages, I lurched between problems 1 and 2…

Eventually I got it working for my setup.  The trick to get it working for me is that:

  • I needed 3 reference clocks in my setup for the PPS source to be used: PPS time, GPS time and a 3rd reference, normally this is a network NTP server but I found that configuring the local clock (127.127.1.0) worked instead.
  • I needed to be careful with my stratum settings, what worked for me was to put the local clock at stratum 15, GPS at stratum 10 and PPS at stratum 0.
  • Marked the PPS source as prefer (and not the GPS source as is often recommended?)

My working configuration:

tos mindist 0.500
# Local Clock
server 127.127.1.0
fudge 127.127.1.0 stratum 15
# GPS/NMEA Source from Hat
server 127.127.28.0 minpoll 4 maxpoll 4 prefer iburst
fudge 127.127.28.0 time1 0.000 refid GPS stratum 10
# PPS Source, stratum 0 and prefer
server 127.127.22.0 minpoll 4 maxpoll 4
fudge 127.127.22.0 refid PPS stratum 0 flag3 1 flag4 1 prefer

I deliberately left the time offset for the GPS source at 0.0 rather than trying to chase down the time packet offset so that I could see an “honest” offset value in the ntpq output:

 remote refid st t when poll reach delay offset jitter
==============================================================================
LOCAL(0) .LOCL. 15 l 58m 64 0 0.000 0.000 0.000
*SHM(0) .GPS. 10 l 16 16 377 0.000 -106.45 1.145
oPPS(0) .PPS. 0 l 15 16 377 0.000 -0.001 0.001

Python script to query NTP status and return it as JSON

Setting up and automatically keeping an eagle eye on NTP from software can be a bit of a (difficult) black art. A common way to query NTP status is to run:

ntpq -p

This returns some NTP status information. It’s output can be a bit difficult to work with from your software, so I have included a small python 3 script which runs runs “ntpq -p” parses, and prints the output in Json format. So instead of shelling out to ntpq you can shell out to this script and deal with the resulting json string instead… This can be handy if you don’t have access to a Python NTP library.

#!/usr/bin/python
#   Copyright 2018 Kevin Godden
#
#   Licensed under the Apache License, Version 2.0 (the "License");
#   you may not use this file except in compliance with the License.
#   You may obtain a copy of the License at
#
#       http://www.apache.org/licenses/LICENSE-2.0
#
#   Unless required by applicable law or agreed to in writing, software
#   distributed under the License is distributed on an "AS IS" BASIS,
#   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
#   See the License for the specific language governing permissions and
#   limitations under the License.
import subprocess
import json
import re
import sys
# Shell out to 'ntpq -p'
proc = subprocess.Popen(['ntpq', '-p'], stdout=subprocess.PIPE)
# Get the output
stdout_value = proc.communicate()[0].decode("utf-8")
#remove the header lines
start = stdout_value.find("===\n")
if start == -1:
    # We may be running on windows (\r\n), try \r...
    start = stdout_value.find("===\r")
    if start == -1:
        # No, go, exit with error
        result = {'query_result': 'failed', 'data': {}}
        print(json.dumps(result))
        sys.exit(1)
# Get the data part of the string
#pay_dirt = stdout_value[start+4:]
pay_dirt = stdout_value[start:]
# search for NTP line starting with * (primary server)
exp = ("\*((?P\S+)\s+)"
       "((?P\S+)\s+)"
       "((?P\S+)\s+)"
       "((?P\S+)\s+)"
       "((?P\S+)\s+)"
       "((?P\S+)\s+)"
       "((?P\S+)\s+)"
       "((?P\S+)\s+)"
       "((?P\S+)\s+)"
       "((?P\S+)\s+)")
regex = re.compile(exp, re.MULTILINE)
r = regex.search(pay_dirt)
# Did we get anything?
if not r:
    # No, try again without the * at the beginning, get
    # the first entry instead
    exp = (" ((?P\S+)\s+)"
           "((?P\S+)\s+)"
           "((?P\S+)\s+)"
           "((?P\S+)\s+)"
           "((?P\S+)\s+)"
           "((?P\S+)\s+)"
           "((?P\S+)\s+)"
           "((?P\S+)\s+)"
           "((?P\S+)\s+)"
           "((?P\S+)\s+)")
    regex = re.compile(exp, re.MULTILINE)
    r = regex.search(pay_dirt)
data = {}
if r:
    data = r.groupdict()
# Output Result
result = {'query_result': 'ok' if r else 'failed', 'data': data}
print(json.dumps(result))

GitHub repo is here.