<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="3.10.0">Jekyll</generator><link href="https://satpulse.net/feed.xml" rel="self" type="application/atom+xml" /><link href="https://satpulse.net/" rel="alternate" type="text/html" /><updated>2026-05-16T00:51:48+00:00</updated><id>https://satpulse.net/feed.xml</id><title type="html">SatPulse</title><subtitle>SatPulse makes it easy to use a GPS receiver as a source of time for a PTP and NTP server</subtitle><author><name>James Clark</name></author><entry><title type="html">SatPulse 0.2 released</title><link href="https://satpulse.net/2026/05/08/satpulse-0.2-released.html" rel="alternate" type="text/html" title="SatPulse 0.2 released" /><published>2026-05-08T02:30:00+00:00</published><updated>2026-05-08T02:30:00+00:00</updated><id>https://satpulse.net/2026/05/08/satpulse-0.2-released</id><content type="html" xml:base="https://satpulse.net/2026/05/08/satpulse-0.2-released.html"><![CDATA[<p>I have released <a href="https://github.com/jclark/satpulse/releases/tag/v0.2">SatPulse 0.2</a>. This is a major new release with a lot of new functionality (more than 60% of the code is new since 0.1).</p>

<p>Highlights of this release:</p>

<ul>
  <li>there is a <a href="/2026/05/05/evolving-a-new-phc-synchronization-architecture-for-satpulse-0.2.html">new PHC synchronization architecture</a>, including a GNSS/PHC simulator (which has a <a href="/man/satpulsetool-syncsim.1.html">CLI</a>)</li>
  <li>it supports <a href="/2026/04/01/a-tour-of-the-gps-modules-supported-by-satpulse.html">seven vendor protocols in addition to UBX</a></li>
  <li>it is evolving beyond the original PTP use case to become an <a href="/2026/04/11/design-of-satpulse-compared-with-gpsd.html">alternative to gpsd</a> for some server-side use cases, in particular it can now be used as a <a href="/2026/04/06/building-an-ntp-server-on-a-raspberry-pi-with-chrony-or-ntpd-rs.html">source of time for an NTP server</a> (either <a href="https://chrony-project.org/">chrony</a> or <a href="https://github.com/pendulum-project/ntpd-rs">ntpd-rs</a>) <a href="/2026/04/01/using-satpulse-for-timing-without-a-phc.html">without needing a PHC</a></li>
  <li>it has had <a href="/2026/05/03/satpulse-0.2-hardware-test-matrix.html">extensive testing on a wide range of hardware</a></li>
</ul>

<p>I created a <a href="https://github.com/jclark/satpulse/discussions/268">discussion thread</a> where you can ask questions, share experiences and suggest new features.</p>]]></content><author><name>James Clark</name></author><summary type="html"><![CDATA[I have released SatPulse 0.2. This is a major new release with a lot of new functionality (more than 60% of the code is new since 0.1).]]></summary></entry><entry><title type="html">Evolving a new PHC synchronization architecture for SatPulse 0.2</title><link href="https://satpulse.net/2026/05/05/evolving-a-new-phc-synchronization-architecture-for-satpulse-0.2.html" rel="alternate" type="text/html" title="Evolving a new PHC synchronization architecture for SatPulse 0.2" /><published>2026-05-05T04:30:00+00:00</published><updated>2026-05-05T04:30:00+00:00</updated><id>https://satpulse.net/2026/05/05/evolving-a-new-phc-synchronization-architecture-for-satpulse-0.2</id><content type="html" xml:base="https://satpulse.net/2026/05/05/evolving-a-new-phc-synchronization-architecture-for-satpulse-0.2.html"><![CDATA[<p>One of the major changes in SatPulse 0.2 is a new architecture for the PHC synchronization subsystem.
The PHC synchronization subsystem has two inputs: a stream of timestamps from the PHC and a stream of messages from the GPS receiver.
Its primary function is to synchronize the time of the PHC with the GPS receiver’s time.
This involves generating a stream of <em>samples</em> from the two streams of timestamps and messages.
A sample says what the offset is between PHC time and GNSS time.
These samples are then used to synchronize the PHC and to update the PTP grandmaster with the synchronization status.</p>

<p>Generating samples includes the following tasks:</p>
<ul>
  <li>pulse edge filtering: some Intel NICs generate timestamps for both edges of a pulse; in this case, we have to identify which edges are leading edges</li>
  <li>sample completion: the timestamp for a leading edge marks the top of a second; completing the sample means determining which second that is</li>
  <li>sawtooth correction: a GPS receiver can only generate a pulse on an edge of its internal clock, but there can be an offset between the edge of its internal clock and the top of the second; a timing-grade GPS receiver will output a message for each pulse giving the size of this offset; the sample then needs to be adjusted for this offset</li>
</ul>

<h2 id="01-phc-synchronization-architecture">0.1 PHC synchronization architecture</h2>

<p>My initial implementation of PHC synchronization followed the approach of the <code class="language-plaintext highlighter-rouge">ts2phc</code> program,
included in LinuxPTP.
The approach consists of a 2-stage pipeline.
The initial stage generates samples by combining timestamps from the PHC with time-of-day information from GPS messages.
The samples are fed into a second stage, which uses a PI servo to adjust the phase and frequency of the PHC.</p>

<p>This approach evolved to add a monitoring stage to the pipeline between the sample-generation stage and the servo.
This monitoring stage had a variety of responsibilities.
It determined whether the PHC was in sync with GNSS time, and used this to dynamically update
the PTP grandmaster’s clock quality.
It also performed outlier detection using a MAD algorithm.</p>

<p>I found two major problems with this pipeline approach.
The first problem was that each stage in the pipeline ended up
maintaining its own state, but these states were not coordinated.</p>

<ul>
  <li>the sample-generation stage had an initialization state for analyzing the intervals between edges;
this was used with Intel NICs that timestamp both edges of a pulse to ensure that trailing edges were ignored</li>
  <li>the monitoring stage maintained state of whether the PHC was synchronized to GNSS time</li>
  <li>the servo stage maintained state related to deciding whether to step the PHC</li>
</ul>

<p>This became particularly problematic for the sample-generation stage.
It’s important for PHC synchronization to be as reliable as possible.
I found that GPS messages were not completely reliable for determining time-of-day information.
Perhaps the most common problem is that the GPS emits too many messages for the available serial bandwidth,
which causes messages to be delayed or dropped.
When in a synchronized state, a more reliable way is to use the PHC, since the PHC will be accurate to within a microsecond or so,
but this doesn’t work at all when the PHC is not synchronized.
However, the sample-generation stage doesn’t have access to the synchronization state of downstream stages.
The sample-generation stage became increasingly complex over time, using ad hoc heuristics to decide whether to prefer
information from the PHC or from messages.</p>

<p>This ties into the second main problem. I had very limited ability to test the pipeline as a whole.
My main approach was to save the inputs and outputs of the sample-generation stage;
I could then replay the inputs to make sure they produced the same outputs.
But if the sample-generation stage was affected by the monitoring stage, this would no longer be possible.</p>

<p>The first problem meant that a rewrite was needed: I hadn’t decomposed the problem in the best way.
And if I was going to do a rewrite, then I should solve the second problem once and for all,
and that meant I needed a simulator.</p>

<p>The most significant open source project in the timing simulation space that I know of is Miroslav Lichvar’s <a href="https://gitlab.com/chrony/clknetsim">clknetsim</a>.
In fact, this project is what made me realise that serious testing needed a simulator.
However, clknetsim would not work for SatPulse, because it relies on being able to redirect system calls using LD_PRELOAD,
but SatPulse is written in Go and on Linux Go usually produces statically linked executables; system calls are raw kernel syscalls, which do not
go through a dynamic library. So that meant I needed to develop my own simulator.
Also clknetsim does not handle the GPS side of things.</p>

<h2 id="simulator">Simulator</h2>

<p>The goal of the simulator is not to be perfect, but to be realistic enough to enable closed-loop testing of synchronization algorithms.</p>

<p>The simulator is initialized with a configuration and performs a simulation for some period of time.
The simulator is driven by the progress of simulated time, which represents true time.
As simulated time progresses, the simulator emits timestamps and GPS messages.
It also implements a PHC interface that can be used to adjust the phase and frequency of the simulated PHC.
There is a crucial feedback loop:
each timestamp is measured with respect to the PHC and has to take account of any phase and frequency adjustments made through the PHC interface.
Another complicating factor is that GPS messages can include sawtooth corrections for the PPS signal
and these corrections have to match the timestamps being generated.</p>

<p>When run under a simulator, the code under test produces its normal output,
but the simulator can observe the offsets between the simulated true time and the simulated PHC.
It can produce a log of these offsets and also generate statistics such as the maximum offset and the Allan deviation.
These statistics could only be produced in real-world testing by using a reference clock that tracks UTC with much greater accuracy than a GPS PPS signal. This would require expensive hardware such as a caesium clock or better still, a hydrogen maser; a rubidium clock would not be sufficient.</p>

<p>The configuration includes error models for the PHC oscillator and the GPS PPS signal.
Each error model consists of a number of components that describe different sources of error, which are combined additively.
For example, the PHC error model has components for white, flicker and random walk FM noise.
The GPS PPS error model includes a component for sawtooth error,
which is used in generating both the timestamp for the PPS edge and the sawtooth correction in the corresponding GPS message.</p>

<p>In Go, the error models are represented by <code class="language-plaintext highlighter-rouge">func(t float64) float64</code>:
the return value gives the instantaneous error at simulated time t.
The return value for the PHC error model is a frequency error,
whereas the return value for the GPS PPS error model is a phase error.
This reflects the underlying physical reality that an oscillator is a continuous process whose state at any instant is a rate, whereas a PPS signal is a discrete process whose state for each pulse is a position in time.</p>

<p>The error models can be derived from physical measurements made of the PHC oscillator and the GPS PPS signal.
(A PHC oscillator can be measured by making the PHC output a PPS signal while free-running.)
In a future post, I will go into more detail about how I made measurements and used them to derive error models.</p>

<h2 id="02-phc-synchronization-architecture">0.2 PHC synchronization architecture</h2>

<p>The approach in 0.2 is modal. There are three modes: reset, converging and tracking.
At a high level, these modes work as follows.
Reset is the initial mode: its job is to generate a single, reliable sample; it does this by collecting a batch of timestamps and GPS messages.
After generating the sample, reset mode will perform a step of the PHC so as to guarantee that the PHC is close to the GNSS time.
At that point, it transitions to converging mode. Its job is to aggressively adjust the frequency so as to bring the PHC into as precise as possible alignment with GNSS time.
When the offsets between the PHC and timestamps are no longer decreasing, it transitions to tracking mode.
Its job is to continually tweak the PHC frequency so as to keep the offsets as small as possible.
It remains in tracking mode so long as the offsets indicate that the PHC is still synchronized to GNSS time.
If synchronization is lost, it transitions to reset mode.</p>

<p>Each mode is associated with a clock quality notified to the PTP grandmaster:
tracking mode is associated with a clock quality representing a synchronized state;
reset and converging mode are associated with a clock quality representing an unsynchronized state.</p>

<p>The following table summarizes the operation of the modes.</p>

<table>
  <thead>
    <tr>
      <th>Task</th>
      <th>Reset</th>
      <th>Converging</th>
      <th>Tracking</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Pulse edge filtering</td>
      <td>analysis of batch of pulse edges</td>
      <td>parity from leading edge learned in reset</td>
      <td>alignment of edge to top of second</td>
    </tr>
    <tr>
      <td>Sample completion</td>
      <td>alignment of batch of timestamps and messages</td>
      <td>round timestamp to nearest second</td>
      <td>round timestamp to nearest second</td>
    </tr>
    <tr>
      <td>Sawtooth correction</td>
      <td>not applied</td>
      <td>not applied</td>
      <td>applied</td>
    </tr>
    <tr>
      <td>Outlier detection</td>
      <td>validation of batch of timestamps and messages</td>
      <td>none</td>
      <td>MAD-based</td>
    </tr>
    <tr>
      <td>PHC control</td>
      <td>step when leaving mode</td>
      <td>aggressive PI servo</td>
      <td>gentle PI servo</td>
    </tr>
    <tr>
      <td>Successful exit</td>
      <td>valid sample</td>
      <td>offsets stabilize</td>
      <td>none</td>
    </tr>
    <tr>
      <td>Failure exit</td>
      <td>none</td>
      <td>too many missing samples</td>
      <td>too many bad samples</td>
    </tr>
  </tbody>
</table>

<p>The key point to notice in the table is that each mode performs its tasks very differently.
I want to focus particularly on sample completion, which is the most fundamental part of sample generation.
In production, SatPulse should be spending 99.9999% of its time in tracking mode,
and in tracking mode sample completion is utterly trivial:
the PHC clock will be accurate to a microsecond, so you can just round the timestamp to know which second the timestamp is for.</p>

<p>In contrast, sample completion in reset mode is quite elaborate.
If reset mode makes the wrong choice of second, then that will persist throughout the operation of the daemon,
so the implementation takes a lot of care to ensure that it is right.
It collects time messages and timestamps for several seconds,
and then performs multiple consistency and quality checks.</p>

<p>Reset mode has to correlate time messages with timestamps.
For time messages, we record the monotonic time at which we read the first character of the message.
But these monotonic times cannot be compared directly with the timestamps, which are in the PHC time domain.
The natural way to handle this is to record the monotonic time immediately after the timestamp is read.
But the Raspberry Pi CM4/CM5 ethernet PHY driver has a quirk which makes this insufficient by itself:
the driver can deliver the timestamp to user space up to 0.25s after the pulse occurred.
To handle this, we also record the PHC time immediately after reading the timestamp,
and then adjust the post-read monotonic time by the difference between the post-read PHC time and the timestamp.
We also have to account for the possibility that the PHC is fast or slow.
The average interval in PHC time between successive pulses tells us how much PHC time corresponds to one second,
and we use this to scale the PHC difference before using it to adjust the monotonic time.</p>

<p>The decomposition of responsibilities is as follows.
The main implementation package is <code class="language-plaintext highlighter-rouge">phcsync</code>.
It has a controller, which is responsible for orchestrating the modes.
For each mode, there is a sample-generator and a sample-processor.
The sample-generator is responsible for pulse edge filtering, sample completion and sawtooth correction;
in tracking mode it uses the pulse width discovered in reset mode.
The sample-processor is responsible for outlier detection,
and for determining when and how to adjust the PHC and change mode;
these PHC adjustments and mode changes are then performed by the controller.
The sample-processors for converging and tracking mode share a PI servo implementation;
in tracking mode, the servo is initialized using the PHC frequency error discovered in reset mode.
The controller feeds samples from the sample-generator to the sample-processor.
The controller also synthesizes missing samples and feeds them to the sample-processor.
The controller notifies the PTP grandmaster for mode changes that imply a change in clock quality.
Thus, as in 0.1, there is a 3-stage pipeline: sample-generator then sample-processor then controller.
But the pipeline is driven by the controller, and the sample-generator and sample-processor
are mode-specific.</p>

<p>The other implementation package is <code class="language-plaintext highlighter-rouge">timemsg</code>,
which serves as a bridge between <code class="language-plaintext highlighter-rouge">phcsync</code> and the GPS subsystem.
<code class="language-plaintext highlighter-rouge">timemsg</code> maintains a buffer of recent time-related messages from the GPS receiver.
<code class="language-plaintext highlighter-rouge">phcsync</code> defines an interface which captures what it needs to know about time messages,
and <code class="language-plaintext highlighter-rouge">timemsg</code> implements this.
Reset mode obviously depends on this interface,
but tracking mode also uses it for sawtooth corrections.
This separation between <code class="language-plaintext highlighter-rouge">phcsync</code> and <code class="language-plaintext highlighter-rouge">timemsg</code> was also designed to enable <code class="language-plaintext highlighter-rouge">timemsg</code>
to be reused for a new feature in 0.2: samples can be provided to an NTP server
based on serial timing, without needing a PHC.</p>

<p>The benefits in terms of user-visible features of the 0.2 implementation are relatively modest.
Reset mode can disambiguate leading and trailing edges even with a 50% duty cycle.
PHC synchronization parameters are now fully configurable using a new <code class="language-plaintext highlighter-rouge">[sync]</code> section of the config file.
The simulator has a CLI that can be used to tune these parameters, in particular the Kp/Ki constants for the tracking servo.</p>

<p>But the major wins from the new architecture are in terms of improving reliability and providing a foundation for future development.
The most important missing feature at the moment is holdover:
the modal architecture can accommodate this in a natural way.
Sample generation has a clean and principled architecture that solves the problems this had in 0.1;
in tracking mode, it does not depend on time messages and so should be more reliable.
The most important aspect of the architecture is, I believe, the simulator.
This solves the testability problem we had in 0.1 and improves the reliability of 0.2.
But it is also crucial for future development: without a simulator,
it would be very difficult to develop a reliable implementation of complex features like holdover.</p>]]></content><author><name>James Clark</name></author><summary type="html"><![CDATA[One of the major changes in SatPulse 0.2 is a new architecture for the PHC synchronization subsystem. The PHC synchronization subsystem has two inputs: a stream of timestamps from the PHC and a stream of messages from the GPS receiver. Its primary function is to synchronize the time of the PHC with the GPS receiver’s time. This involves generating a stream of samples from the two streams of timestamps and messages. A sample says what the offset is between PHC time and GNSS time. These samples are then used to synchronize the PHC and to update the PTP grandmaster with the synchronization status.]]></summary></entry><entry><title type="html">SatPulse 0.2 hardware test matrix</title><link href="https://satpulse.net/2026/05/03/satpulse-0.2-hardware-test-matrix.html" rel="alternate" type="text/html" title="SatPulse 0.2 hardware test matrix" /><published>2026-05-03T07:10:00+00:00</published><updated>2026-05-03T07:10:00+00:00</updated><id>https://satpulse.net/2026/05/03/satpulse-0.2-hardware-test-matrix</id><content type="html" xml:base="https://satpulse.net/2026/05/03/satpulse-0.2-hardware-test-matrix.html"><![CDATA[<p>I have set up 12 different machines for automated testing of SatPulse.
I have chosen the hardware to provide coverage along multiple dimensions:
CPU architecture, OS, PHC, GPS protocol and serial connection type.</p>

<table>
  <thead>
    <tr>
      <th>Hostname</th>
      <th>Machine</th>
      <th>OS</th>
      <th>Ethernet</th>
      <th>GPS</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td rowspan="2">abondance</td>
      <td rowspan="2">ASUS S500SD</td>
      <td rowspan="2">Debian 13</td>
      <td>I225-T1</td>
      <td>ZED-F9P</td>
    </tr>
    <tr>
      <td>I225-T1</td>
      <td>BG7TBL CM55</td>
    </tr>
    <tr>
      <td>brie</td>
      <td>Intel 12th Gen PC</td>
      <td>Ubuntu 24.04</td>
      <td>E810-XXVDA4T</td>
      <td>NEO-F10T</td>
    </tr>
    <tr>
      <td>gubbeen</td>
      <td>Raspberry Pi 5</td>
      <td>Debian 13</td>
      <td>I210-T1</td>
      <td>M10050-KB</td>
    </tr>
    <tr>
      <td>maasdam</td>
      <td>Minisforum MS-01</td>
      <td>Fedora 43</td>
      <td>I226-T1</td>
      <td>ZED-F9T-20B</td>
    </tr>
    <tr>
      <td>morbier</td>
      <td>Raspberry Pi CM4</td>
      <td>Debian 11</td>
      <td>onboard</td>
      <td>LEA-M8F</td>
    </tr>
    <tr>
      <td>pecorino</td>
      <td>Raspberry Pi CM4</td>
      <td>Fedora 42</td>
      <td>onboard</td>
      <td>LEA-6T</td>
    </tr>
    <tr>
      <td>roquefort</td>
      <td>Raspberry Pi CM4</td>
      <td>Debian 12</td>
      <td>onboard</td>
      <td>ZED-X20P</td>
    </tr>
    <tr>
      <td>serpa</td>
      <td>Raspberry Pi CM5</td>
      <td>Debian 13</td>
      <td>onboard</td>
      <td>UM980</td>
    </tr>
    <tr>
      <td>taleggio</td>
      <td>Raspberry Pi 5</td>
      <td>Debian 12</td>
      <td>TimeHAT</td>
      <td>NEO-M9N</td>
    </tr>
    <tr>
      <td>valencay</td>
      <td>Raspberry Pi CM5</td>
      <td>Debian 12</td>
      <td>onboard</td>
      <td>LEA-M8T</td>
    </tr>
    <tr>
      <td>valtellina</td>
      <td>Raspberry Pi CM5</td>
      <td>Debian 13</td>
      <td>onboard</td>
      <td>NEO-F10N</td>
    </tr>
    <tr>
      <td>wensleydale</td>
      <td>Raspberry Pi CM4</td>
      <td>Debian 12</td>
      <td>onboard</td>
      <td>UM960</td>
    </tr>
  </tbody>
</table>

<p>More details:</p>

<dl>
  <dt>abondance</dt>
  <dd>ASUS S500SD desktop (Intel B660 chipset) with an Intel i5-12400 and 64 GB RAM. It has two Intel I225-T1 PCIe cards: one is connected to an internal ArduSimple simpleRTK2B M.2, which has a u-blox ZED-F9P, and the other is connected over RS232 to a BG7TBL CM55 GPSDO, which has a LEA-M8T. This supports cross-timestamping with <a href="/hardware/ptm.html">PTM</a>.</dd>
  <dt>brie</dt>
  <dd>MSI Z690I motherboard with an Intel i9-12900K and 32 GB RAM. It has an Intel E810-XXVDA4T PCIe card. GPS is a u-blox EVK-F10T connected by USB.</dd>
  <dt>gubbeen</dt>
  <dd>Raspberry Pi 5 with 16 GB RAM in a Geekworm X1010 expansion board with PCIe slot, which contains an Intel I210-T1. GPS is a u-blox M10050-KB (u-blox M10 standard-precision) connected to UART0.</dd>
  <dt>maasdam</dt>
  <dd>Minisforum MS-01 with an Intel i5-12600H and 32 GB RAM. It has an I226-T1 PCIe card. GPS is a u-blox ZED-F9T-20B in a gnss.store USB dongle. This supports cross-timestamping with <a href="/hardware/ptm.html">PTM</a>.</dd>
  <dt>morbier</dt>
  <dd>Raspberry Pi CM4 with 8 GB RAM and 32 GB eMMC, on the official IO board. GPS is a u-blox LEA-M8F on a Timebeat sandwich board.</dd>
  <dt>pecorino</dt>
  <dd>Raspberry Pi CM4 with 8 GB RAM and 32 GB eMMC, on the official IO board. GPS is a u-blox LEA-6T connected to UART3.</dd>
  <dt>roquefort</dt>
  <dd>Raspberry Pi CM4 with 4 GB RAM and 8 GB eMMC, on a Waveshare PoE board. GPS is a u-blox EVK-X20P connected by USB.</dd>
  <dt>serpa</dt>
  <dd>Raspberry Pi CM5 with 2 GB RAM and NVMe SSD, on a Geekworm X1500 carrier board. GPS is a Unicore UM980 connected to UART0.</dd>
  <dt>taleggio</dt>
  <dd>Raspberry Pi 5 with 4 GB RAM with a Timebeat TimeHAT, which uses an Intel I226-LM. GPS is a u-blox NEO-M9N in the TimeHAT’s M.2 slot.</dd>
  <dt>valencay</dt>
  <dd>Raspberry Pi CM5 with 2 GB RAM and 16 GB eMMC, on the official IO board. GPS is a u-blox LEA-M8T connected to UART0.</dd>
  <dt>valtellina</dt>
  <dd>Raspberry Pi CM5 Lite with 8 GB RAM and NVMe SSD, on the official IO board. GPS is a u-blox NEO-F10N connected to UART0.</dd>
  <dt>wensleydale</dt>
  <dd>Raspberry Pi CM4 with 8 GB RAM and 8 GB eMMC, on the official IO board. GPS is a Unicore UM960 connected to UART0.</dd>
</dl>

<p>The testing uses the Ansible playbooks in the <a href="https://github.com/jclark/satpulse/tree/master/systest">systest</a> directory.
It runs both satpulsed and chrony on the target machines, with satpulsed supplying samples for chrony.
The main checks are that</p>
<ul>
  <li>satpulsed logs show that synchronization has been established and not lost; and</li>
  <li>chrony has selected the satpulsed refclock; chrony is also configured to use highly-accurate NTP servers on the LAN, and won’t select the satpulsed refclock if it diverges too much from those NTP servers.</li>
</ul>

<p>It also uses the <code class="language-plaintext highlighter-rouge">clocklog.py</code> script to produce some statistics from the clock.log about the quality of synchronization.</p>

<p>I also have a <a href="/setup/monitor.html">Grafana dashboard</a> which shows metrics from each test machine.</p>

<p><img src="/assets/images/test-matrix-grafana-dashboard.png" alt="Grafana dashboard showing metrics from each test machine" /></p>]]></content><author><name>James Clark</name></author><summary type="html"><![CDATA[I have set up 12 different machines for automated testing of SatPulse. I have chosen the hardware to provide coverage along multiple dimensions: CPU architecture, OS, PHC, GPS protocol and serial connection type.]]></summary></entry><entry><title type="html">Desktop GUI preview</title><link href="https://satpulse.net/2026/04/26/desktop-gui-preview.html" rel="alternate" type="text/html" title="Desktop GUI preview" /><published>2026-04-26T00:00:00+00:00</published><updated>2026-04-26T00:00:00+00:00</updated><id>https://satpulse.net/2026/04/26/desktop-gui-preview</id><content type="html" xml:base="https://satpulse.net/2026/04/26/desktop-gui-preview.html"><![CDATA[<p>I have been working on a desktop GUI for SatPulse. Here are a couple of demo videos.</p>

<!-- Courtesy of embedresponsively.com -->

<div class="responsive-video-container">
    <iframe src="https://www.youtube-nocookie.com/embed/lBE5Ls4ly0M" frameborder="0" webkitallowfullscreen="" mozallowfullscreen="" allowfullscreen=""></iframe>
  </div>

<!-- Courtesy of embedresponsively.com -->

<div class="responsive-video-container">
    <iframe src="https://www.youtube-nocookie.com/embed/aDrLsjJSugU" frameborder="0" webkitallowfullscreen="" mozallowfullscreen="" allowfullscreen=""></iframe>
  </div>

<p>This is a native app running on macOS. It uses the same
<a href="/2026/04/11/design-of-satpulse-compared-with-gpsd.html">GPS library</a>
that <code class="language-plaintext highlighter-rouge">satpulsed</code> and <code class="language-plaintext highlighter-rouge">satpulsetool</code> use.
The code is on the <code class="language-plaintext highlighter-rouge">desktop-gui</code> branch of the <a href="https://github.com/jclark/satpulse">repo</a>.</p>

<p>It uses a tabbed layout, with tabs for:</p>

<ul>
  <li>monitoring - this is conceptually quite similar to what the Web monitoring app does, but has a richer set of components</li>
  <li>log-level monitoring - this is similar to what you get with the packet log in <code class="language-plaintext highlighter-rouge">satpulsetool</code> or with the <code class="language-plaintext highlighter-rouge">--packet-log</code> option in <code class="language-plaintext highlighter-rouge">satpulsed</code>; but instead of giving a stream of packets, it groups packets by protocol and message ID; it also allows decoding of packets similar to what is provided by <code class="language-plaintext highlighter-rouge">satpulsetool decode</code></li>
  <li>high-level configuration - this allows you to configure the GPS receiver in a high-level, device-independent way, without needing any knowledge of the receiver protocol, similar to <code class="language-plaintext highlighter-rouge">satpulsetool gps</code></li>
  <li>low-level configuration - this uses message files to provide a lower-level device-dependent way, similar to <code class="language-plaintext highlighter-rouge">-m</code> and <code class="language-plaintext highlighter-rouge">-t</code> options of <code class="language-plaintext highlighter-rouge">satpulsetool gps</code></li>
  <li>corrections - this allows you to pull RTCM corrections from an NTRIP caster or TCP server and feed them to the receiver, with a live view of the corrections stream; I plan to provide this functionality in a future release of <code class="language-plaintext highlighter-rouge">satpulsed</code> using a <code class="language-plaintext highlighter-rouge">[stream.pull]</code> section in the configuration file</li>
</ul>

<p>This is built using <a href="https://wails.io/">Wails</a>, which supports Linux and Windows, as well as macOS.
But my initial testing has focused on macOS, partly because that’s what I use day-to-day for desktop work,
and partly because there’s a gap on macOS, since GNSS vendors provide their proprietary apps only for Windows.</p>

<p>This is still very much in the preview phase: some aspects of the UI are rough, particularly the monitoring tab.
With Wails, the UI is written using standard Web technologies (HTML, CSS and JavaScript),
which run in the platform’s native webview component.
The desktop GUI and the <code class="language-plaintext highlighter-rouge">satpulsed</code> Web monitoring app are both written using Preact and Tailwind CSS;
this should make it possible in the future for the monitoring tab to share Preact components with satpulsed.</p>

<p>Why bother with writing a GUI at all? I developed the device-independent configuration approach to give the best possible
user experience for <code class="language-plaintext highlighter-rouge">satpulsed</code>. But it turned out to be a lot more engineering effort than I anticipated.
And to be honest, from the point of view of <code class="language-plaintext highlighter-rouge">satpulsed</code>, it doesn’t really earn its keep:
the added benefit to users is too small to justify the effort that went into it.
I added <code class="language-plaintext highlighter-rouge">satpulsetool</code> to help unlock the value of device-independent configuration to a wider audience.
But <code class="language-plaintext highlighter-rouge">satpulsetool</code> can’t fully do this.
Most users only have one GNSS receiver, so the value to users of this configuration model
isn’t so much that it’s device-independent,
but that it avoids requiring the user to know anything about the receiver’s native protocol;
and the kind of user that benefits most from this is a non-technical user, who will want a GUI not a CLI.</p>]]></content><author><name>James Clark</name></author><summary type="html"><![CDATA[I have been working on a desktop GUI for SatPulse. Here are a couple of demo videos.]]></summary></entry><entry><title type="html">Design of SatPulse compared with GPSd</title><link href="https://satpulse.net/2026/04/11/design-of-satpulse-compared-with-gpsd.html" rel="alternate" type="text/html" title="Design of SatPulse compared with GPSd" /><published>2026-04-11T00:00:00+00:00</published><updated>2026-04-11T00:00:00+00:00</updated><id>https://satpulse.net/2026/04/11/design-of-satpulse-compared-with-gpsd</id><content type="html" xml:base="https://satpulse.net/2026/04/11/design-of-satpulse-compared-with-gpsd.html"><![CDATA[<p>In version 0.1 SatPulse focused on a specialized use case:
transferring time from a GPS to a PTP hardware clock (PHC).
In <a href="/2026/04/01/first-pre-release-of-satpulse-0.2.html">version 0.2</a>,
SatPulse’s scope is much broader.
It now includes a rich, general-purpose GPS subsystem,
which supports a <a href="/2026/04/01/a-tour-of-the-gps-modules-supported-by-satpulse.html">wide range of vendor protocols</a>.
I believe 0.2 already has everything needed to provide full support for
<a href="/2026/04/06/building-an-ntp-server-on-a-raspberry-pi-with-chrony-or-ntpd-rs.html">timing-oriented use</a>
of a GPS receiver on Linux.
It also includes most of the pieces that are needed to support
precision positioning using RTK and NTRIP; this will be rounded out in upcoming releases.</p>

<p>At the moment, support for GPS receivers in Linux is almost universally based on GPSd.
In this post I want to explain some key design choices I made for SatPulse
that are different from those made by GPSd.
My goal is to help people understand when it might be worth considering SatPulse
as an alternative to GPSd.
I want to emphasize that SatPulse is not attempting to be a replacement for GPSd.
GPSd does what it sets out to do very well, as evidenced by its popularity.</p>

<p>TL;DR: Consider SatPulse for server-side use of a GPS receiver,
or when GPS receiver configuration is needed.</p>

<p>Let me start by giving a brief overview of how GPSd works.
For a fuller description, see the <a href="https://aosabook.org/en/v2/gpsd.html">GPSd chapter</a> from the Architecture of Open Source Applications book.
GPSd is both a daemon, written in C, and a suite of related tools.
It is centered around a device-independent data model of the information
provided by periodic messages emitted by GPS receivers.
A single instance of the daemon acts as a multiplexer.
Streams of messages are read from multiple sources such as serial devices,
converted to the device-independent data model and provided to multiple clients.
GPSd’s primary API is a service API: a client runs as a separate process and
interacts with GPSd over a TCP socket using a JSON protocol;
it also provides a client library API that wraps the service API.
In the GPSd architecture, the daemon does not perform application-level work on the data;
its role is restricted to multiplexing and conversion into the device-independent data model.
GPSd has a zero-configuration philosophy:
when a user plugs in a GPS receiver, a GPSd client should work without requiring
the user to perform any configuration.
GPSd provides only a very limited device-independent abstraction for GPS
receiver configuration; most importantly it has the concept of enabling a <em>binary</em> mode,
which configures the receiver to output messages using its vendor-specific binary protocol instead of NMEA.
Instead it provides a separate Python program, ubxtool, for configuring u-blox receivers using the UBX protocol.</p>

<p>Like GPSd, SatPulse is also both a daemon and a suite of related tools,
and it is also centered around a similar device-independent data model.
SatPulse is written in Go.
Its codebase is comparable in size to GPSd’s.
SatPulse compiles into two executables:
<code class="language-plaintext highlighter-rouge">satpulsed</code>, which is a daemon, and <code class="language-plaintext highlighter-rouge">satpulsetool</code>, which is a command line tool.
All the application-level functionality that SatPulse provides
is included in <code class="language-plaintext highlighter-rouge">satpulsed</code> or <code class="language-plaintext highlighter-rouge">satpulsetool</code> depending on whether that functionality
needs to work as a daemon or not.
The daemon is configured using a file in TOML syntax.
One instance of the daemon runs for each device to which a receiver is attached.
This allows each instance to have its own configuration file,
and for systemd to manage the daemon lifecycle.
SatPulse does not attempt to discover GPS receivers:
it opens a serial device only when explicitly configured to do so.
The configuration file has a modular structure, with separate TOML tables to configure
each aspect of application-level functionality.
The second executable, <code class="language-plaintext highlighter-rouge">satpulsetool</code>, is a suite of command-line tools.
It uses a subcommand syntax, so for example <code class="language-plaintext highlighter-rouge">satpulsetool gps</code> runs the <code class="language-plaintext highlighter-rouge">gps</code> tool.
These separate tools are bundled into a single executable because the Go language has a runtime that makes executables quite large.
The GPS subsystem of SatPulse is structured as a reusable library of Go packages.
Both <code class="language-plaintext highlighter-rouge">satpulsed</code> and <code class="language-plaintext highlighter-rouge">satpulsetool</code> use this library for their GPS-related functionality:
<code class="language-plaintext highlighter-rouge">satpulsetool</code> does not need <code class="language-plaintext highlighter-rouge">satpulsed</code> to be running.</p>

<p>The most significant GPS functionality in SatPulse that goes beyond what GPSd provides
is support for GPS receiver configuration.
For basic GPS usage, the factory default configuration is often sufficient.
But modern GPS receivers even at modest price points have started to offer
more advanced features, such as RTK, PPP-HAS or OSNMA,
which typically require some configuration to be used.
SatPulse provides a device-independent abstraction for GPS receiver configuration.
This allows you to choose which specific aspects of the configuration should be changed.
So, for example, you can specify that a time pulse should be enabled with a specific pulse width,
that the time pulse should be enabled only when the receiver has a lock,
and that it should be aligned to the system time of a particular GNSS;
or you can specify that the receiver should operate in time mode with specific fixed ECEF coordinates.
The changes to be made are given to the configuration engine which figures out how to apply
them to a specific receiver. This often involves reading the existing configuration of the receiver,
so that only the specified aspects of the configuration are changed.
An important part of configuring a GPS receiver is enabling the right set of periodic messages.
Each receiver divides up information in different ways.
So instead of specifying particular named device-dependent messages (e.g. UBX-NAV-PVT), you specify the needed information in data-model terms (e.g. the time in UTC and position in geodetic coordinates).
The engine then enables the best set of messages that provide this information.
The configuration engine is part of the library and is used both by <code class="language-plaintext highlighter-rouge">satpulsed</code> and <code class="language-plaintext highlighter-rouge">satpulsetool</code>.
Having <code class="language-plaintext highlighter-rouge">satpulsed</code> perform receiver configuration works well because it can
infer some aspects of receiver configuration from the application-level configuration.
For example, if the configuration file specifies a PHC to be disciplined,
then the daemon will ensure a 1PPS time pulse is enabled.
But configuration changes that affect the receiver’s non-volatile
memory or interrupt receiver operation (such as changing the enabled GNSS constellations) are only
done when specifically requested by the user with <code class="language-plaintext highlighter-rouge">satpulsetool</code>.
Implementing device-independent configuration is significantly more complex than implementing
the device-independent data model for periodic messages.
In the Go package that implements the device-independent abstractions for the u-blox UBX protocol,
about 70% of the code is devoted to configuration.
SatPulse also provides an alternative lower-level approach to configuration,
which is less challenging to implement:
see <a href="/2026/01/29/improving-gps-configuration.html">this blog post</a> for more detail.</p>

<p>The device-independent data model provided by the GPS subsystem can be serialized as JSON.
The daemon uses this for its Web dashboard feature.
It includes an HTTP server with an endpoint that exposes this data model as JSON-encoded server-sent events (SSE).
Another endpoint serves an HTML page which uses JavaScript to connect to the SSE endpoint and display a dashboard.
The daemon does not yet expose this data model over a network socket for consumption by third-party applications.
This will be straightforward to do, but I want to stabilize the data model first.</p>

<p>However, SatPulse emphasizes a different approach to allowing multiple independent applications to share access to
a single receiver, based on providing network access to packet streams.
The daemon can be configured to make TCP ports or Unix domain sockets
proxy the packets emitted by the receiver,
optionally filtering packets by protocol,
and optionally also allowing writing to the receiver, with a configurable locking strategy to prevent conflicts between writers.
This provides functionality similar to ser2net, but is protocol-aware.
Streams of native-protocol packets are already a well-defined wire-format.
Exposing this directly on a network endpoint allows receiver sharing without the need to define
a new daemon-dependent wire-format.
Many applications already exist that can work with such packet streams.
Here are three examples.</p>
<ol>
  <li>Every vendor that I know of provides an application for configuring their receivers, typically Windows-only.
Many of these applications, notably u-center, allow the receiver to be accessed over a TCP socket.
These can be used with SatPulse by taking advantage of the read-write feature.</li>
  <li>IANA has <a href="https://www.iana.org/assignments/service-names-port-numbers/service-names-port-numbers.xhtml?search=nmea">registered</a> the service name nmea-0183 and port 10110 for NMEA over TCP or UDP. <a href="https://gitlab.freedesktop.org/geoclue/geoclue/-/wikis/home">GeoClue</a> is a D-Bus service that provides location information. It includes an NMEA backend that uses DNS-based service discovery (RFC 6763 as implemented by Avahi) to discover nmea-0183 services on the network. SatPulse can be configured to expose the NMEA service on port 10110, Avahi can be configured to advertise it and then the GeoClue NMEA backend can discover it and expose it to desktop apps over D-Bus.</li>
  <li>RTK works by providing the rover’s receiver with a stream of RTCM packets emitted by a base station’s receiver. In a production environment, NTRIP is typically used to move packets from a base to a rover. An RTK base station uses a NTRIP server to provide RTCM packets to an NTRIP caster; an RTK rover uses an NTRIP client to get RTCM packets from the caster. NTRIP support is planned for a future release of SatPulse. But one popular open-source caster, the <a href="https://igs.bkg.bund.de/ntrip/bkgcaster">BKG NtripCaster</a> can <a href="https://igs.bkg.bund.de/root_ftp/NTRIP/software/caster/ntripcaster_manual.html#c5">pull</a> RTCM data from a TCP port without using NTRIP. This allows SatPulse to be used today as the GPS component of a combined RTK and time server.</li>
</ol>

<p>We can summarize the key design choices for SatPulse that are different from GPSd as follows:</p>

<ol>
  <li>it is written in Go</li>
  <li>the primary GPS API is a library API</li>
  <li>the daemon does application-level work</li>
  <li>the daemon has a configuration file</li>
  <li>the daemon has a separately configured instance per receiver</li>
  <li>it provides a device-independent model for GPS configuration</li>
  <li>it emphasizes packet streams as a foundational layer</li>
</ol>

<p>So why did I make these choices? They were not independent.
The initial PTP use case requires a daemon to do substantial application-level work:
it uses timestamp events from the Linux PHC subsystem in combination
with messages from the GPS receiver to discipline the PHC and send metadata updates to the PTP daemon.
I didn’t want this use case to require two independent daemons.
Once the daemon is doing application-level work, then there needs to be a way to configure it.
Having one daemon instance per serial device makes things straightforward:
each instance can have its own configuration file
and systemd can ensure that the daemon is not started until the serial device is ready.</p>

<p>Even with the initial PTP use case, the daemon has significant internal concurrency:
reading from the serial device, reading timestamp events, and updating the PTP daemon
are naturally concurrent with the main PHC-disciplining loop.
Other features like packet proxying or HTTP monitoring involve additional concurrency.
I would not attempt implementing a daemon with this level of concurrency
except in a modern language like Go, which is memory safe and has language support for concurrency.
The implementation of <code class="language-plaintext highlighter-rouge">satpulsetool</code> also uses concurrency, although to a lesser extent than <code class="language-plaintext highlighter-rouge">satpulsed</code>.
Go makes it relatively straightforward to have a library that supports concurrency in a flexible way.</p>

<p>I hope this post has made it clear that GPSd and SatPulse are very different programs,
which have made different design choices for good reasons.
SatPulse would not be a suitable replacement for many uses of GPSd.
But I think there are several use-cases, particularly on the server side, where
SatPulse’s more integrated approach and support for GPS configuration offers significant advantages.</p>]]></content><author><name>James Clark</name></author><summary type="html"><![CDATA[In version 0.1 SatPulse focused on a specialized use case: transferring time from a GPS to a PTP hardware clock (PHC). In version 0.2, SatPulse’s scope is much broader. It now includes a rich, general-purpose GPS subsystem, which supports a wide range of vendor protocols. I believe 0.2 already has everything needed to provide full support for timing-oriented use of a GPS receiver on Linux. It also includes most of the pieces that are needed to support precision positioning using RTK and NTRIP; this will be rounded out in upcoming releases.]]></summary></entry><entry><title type="html">Building an NTP server on a Raspberry Pi with chrony or ntpd-rs</title><link href="https://satpulse.net/2026/04/06/building-an-ntp-server-on-a-raspberry-pi-with-chrony-or-ntpd-rs.html" rel="alternate" type="text/html" title="Building an NTP server on a Raspberry Pi with chrony or ntpd-rs" /><published>2026-04-06T00:00:00+00:00</published><updated>2026-04-06T00:00:00+00:00</updated><id>https://satpulse.net/2026/04/06/building-an-ntp-server-on-a-raspberry-pi-with-chrony-or-ntpd-rs</id><content type="html" xml:base="https://satpulse.net/2026/04/06/building-an-ntp-server-on-a-raspberry-pi-with-chrony-or-ntpd-rs.html"><![CDATA[<p>In an <a href="https://satpulse.net/2026/04/01/using-satpulse-for-timing-without-a-phc.html">earlier post</a>, I described how SatPulse can now work without the specialised PHC hardware it previously required. This means, in particular, that it is possible to use SatPulse to build an NTP server on a regular Raspberry Pi, rather than requiring a Raspberry Pi CM4 or CM5.</p>

<p>In this post, I will explain how to do this, using two different NTP servers: chrony and ntpd-rs.</p>

<h2 id="hardware">Hardware</h2>

<p>Before we get into the details of configuration, let me say something about GPS hardware.
When using a Raspberry Pi, the PPS signal is connected to a GPIO pin on the Pi.
This means that the time of the PPS edge is measured in software by the kernel,
rather than in hardware.
The accuracy you will get is in the microsecond range.
Basic GPS receivers can achieve an accuracy of 50ns in normal conditions.
So my main hardware advice is that in this context a fancy, expensive GPS receiver will not deliver any measurable benefit.</p>

<p>GPS receivers are available in a Pi HAT form factor, which plugs into the 40-pin GPIO header.
These HATs typically wire the GPS PPS signal to pin 12 (GPIO 18).
I do not recommend these when using PHC hardware, because the GPS PPS signal needs to be wired to
the SYNC_OUT pin on a different header.
But when using a normal Raspberry Pi, they are very convenient,
although you typically pay a premium for this convenience.</p>

<h2 id="setup">Setup</h2>

<p>The following steps can be done exactly as described in <a href="https://satpulse.net/setup/index.html">SatPulse Setup Guide</a>.</p>

<ol>
  <li><a href="https://satpulse.net/setup/rpi-os.html">Install Raspberry Pi OS</a></li>
  <li><a href="https://satpulse.net/setup/rpi-uart.html">Configure the UART</a></li>
  <li><a href="https://satpulse.net/setup/satpulse-install.html">Install SatPulse</a> but choose the 0.2 pre-release</li>
  <li><a href="https://satpulse.net/setup/gps-serial.html">Verify the GPS serial connection</a>; the device will be <code class="language-plaintext highlighter-rouge">/dev/ttyAMA0</code></li>
</ol>

<p>Configuration of kernel PPS is different.
First, configure pin 12 (GPIO 18) as a PPS pin, by adding the following at the bottom of <code class="language-plaintext highlighter-rouge">/boot/firmware/config.txt</code></p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>dtoverlay=pps-gpio,gpiopin=18
</code></pre></div></div>

<p>Reboot after this. To verify the PPS signal is working, install pps-tools using</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>sudo apt install pps-tools
</code></pre></div></div>

<p>Then do:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>sudo ppstest /dev/pps0
</code></pre></div></div>

<p>It should show PPS events once per second:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>trying PPS source "/dev/pps0"
found PPS source "/dev/pps0"
ok, found 1 source(s), now start fetching data...
source 0 - assert 1775392263.000000336, sequence: 170178 - clear  0.000000000, sequence: 0
source 0 - assert 1775392264.000000215, sequence: 170179 - clear  0.000000000, sequence: 0
source 0 - assert 1775392264.999998780, sequence: 170180 - clear  0.000000000, sequence: 0
source 0 - assert 1775392265.999999085, sequence: 170181 - clear  0.000000000, sequence: 0
</code></pre></div></div>

<p>Exit with Ctrl-C.</p>

<p>Now you need to edit <code class="language-plaintext highlighter-rouge">/etc/satpulse.toml</code>.  Most important point is to remove the <code class="language-plaintext highlighter-rouge">[phc]</code> section.
All you need is this:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>[serial]
device = "/dev/ttyAMA0"
# fix to match your serial device speed
speed = 9600

[gps]
config = true
vendor = "u-blox"

# Enable HTTP monitoring on port 2000
[[http]]
listen = ":2000"

[ntp]
# Use this for chrony
sock.path = "/var/run/chrony.satpulse.sock"
# Use this for ntpd-rs
# sock.path = "/run/ntpd-rs/satpulse.ttyAMA0.sock"
</code></pre></div></div>

<p>Uncomment the sock.path line for whichever of chrony or ntpd-rs you use.</p>

<p>You can now start SatPulse using</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>sudo systemctl start satpulse@ttyAMA0
</code></pre></div></div>

<p>You can point a browser at port 2000 and you should get a page showing information
from the GPS receiver.</p>

<h3 id="chrony">Chrony</h3>

<p>To configure chrony, first install with</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>sudo apt install chrony
</code></pre></div></div>

<p>Then create a file <code class="language-plaintext highlighter-rouge">/etc/chrony/conf.d/satpulse.conf</code> with the following:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>refclock PPS /dev/pps0 poll 2 lock GPS refid PPS
refclock SOCK /var/run/chrony.satpulse.sock offset 0.1 delay 0.2 refid GPS noselect
</code></pre></div></div>

<p>The refclock PPS line makes chrony use the kernel PPS API to read PPS samples from /dev/pps0.
These are accurate but lack time-of-day information.
The refclock SOCK line makes chrony read samples generated by satpulsed.
These are inaccurate but include time-of-day.
The <code class="language-plaintext highlighter-rouge">offset 0.1</code> corrects for serial messages coming 0.1 second after the top of the second.</p>

<p>Chrony can use the samples from satpulsed to complete the PPS samples.
Chrony can also use samples from other NTP servers to complete the samples,
so if you want to check that this is really working,
you should at least temporarily comment out the <code class="language-plaintext highlighter-rouge">pool</code> line from <code class="language-plaintext highlighter-rouge">/etc/chrony/chrony.conf</code>.</p>

<h3 id="ntpd-rs">ntpd-rs</h3>

<p>Ubuntu has <a href="https://discourse.ubuntu.com/t/ntpd-rs-its-about-time/79154">announced</a> plans to adopt <a href="https://github.com/pendulum-project/ntpd-rs">ntpd-rs</a> as its default NTP server, replacing chrony, primarily because of memory safety.
Since SatPulse is written in Go, the combination of ntpd-rs and SatPulse provides a fully memory-safe timing stack.
Like SatPulse, ntpd-rs uses TOML for its configuration files and supports Prometheus,
so the combination makes for a pleasantly harmonious configuration and observability experience.</p>

<p>To install, download a deb file from the <a href="https://github.com/pendulum-project/ntpd-rs/releases">Releases page</a>.
You need at least version 1.7.2.
(The version in Raspberry Pi OS will not work.)</p>

<p>Next, you will need to remove your existing NTP daemon (e.g. systemd-timesyncd or chrony):</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>sudo apt remove systemd-timesyncd chrony
</code></pre></div></div>

<p>Then install ntpd-rs</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>sudo dpkg -i ./ntpd-rs_1.7.2-1_arm64.deb
</code></pre></div></div>

<p>Now you need to set things up so that ntpd-rs can access <code class="language-plaintext highlighter-rouge">/dev/pps0</code>.
Create a file <code class="language-plaintext highlighter-rouge">/etc/udev/rules.d/99-ntpd-rs-pps.rules</code> containing the line</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>KERNEL=="pps0", GROUP="ntpd-rs", MODE="0640"
</code></pre></div></div>

<p>This ensures that when /dev/pps0 is created at boot time it will have a group and permissions
that enables ntpd-rs to access it. To make this take effect without booting, do</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>sudo udevadm control --reload
sudo udevadm trigger /dev/pps0
</code></pre></div></div>

<p>Now we need to configure ntpd-rs.
Put the following in <code class="language-plaintext highlighter-rouge">/etc/ntpd-rs/ntp.toml</code>:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>[observability]
log-level = "debug"
observation-path = "/var/run/ntpd-rs/observe"

[[source]]
mode = "pps"
path = "/dev/pps0"
precision = 1e-7
accuracy = 1e-6

[[source]]
mode = "sock"
precision = 1e-2
path = "/run/ntpd-rs/satpulse.ttyAMA0.sock"
accuracy = 0.2

[synchronization]
minimum-agreeing-sources = 1

[[server]]
listen = "0.0.0.0:123"
</code></pre></div></div>

<p>When I tried initially to get this working with version 1.7.1 of ntpd-rs,
it didn’t work. I submitted an <a href="https://github.com/pendulum-project/ntpd-rs/issues/2169">issue</a>,
and within a day one of the developers, David Venhoek, had added a <a href="https://github.com/pendulum-project/ntpd-rs/pull/2171">feature</a> to enable this configuration.
This was included in version 1.7.2, released a few days ago.
It adds the ability to specify the accuracy of sources as distinct from their precision.
Accuracy means how close measurements are to true time.
Precision means how much measurements vary from each other.
The serial timing measurements from satpulsed are quite precise
but are extremely inaccurate.
Specifying a low accuracy for samples from satpulsed
ensures that ntpd-rs uses the PPS samples as the primary source,
and uses the satpulsed samples just to complete the PPS samples.
But note that if you specify an accuracy of 0.25 or worse,
you need to increase <code class="language-plaintext highlighter-rouge">maximum-source-uncertainty</code> from its default of 0.25.</p>

<p>Now you can start ntpd-rs using</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>sudo systemctl start ntpd-rs
</code></pre></div></div>

<p>You can verify it’s working by using</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ntp-ctl status
</code></pre></div></div>]]></content><author><name>James Clark</name></author><summary type="html"><![CDATA[In an earlier post, I described how SatPulse can now work without the specialised PHC hardware it previously required. This means, in particular, that it is possible to use SatPulse to build an NTP server on a regular Raspberry Pi, rather than requiring a Raspberry Pi CM4 or CM5.]]></summary></entry><entry><title type="html">First pre-release of SatPulse 0.2</title><link href="https://satpulse.net/2026/04/01/first-pre-release-of-satpulse-0.2.html" rel="alternate" type="text/html" title="First pre-release of SatPulse 0.2" /><published>2026-04-01T11:15:00+00:00</published><updated>2026-04-01T11:15:00+00:00</updated><id>https://satpulse.net/2026/04/01/first-pre-release-of-satpulse-0.2</id><content type="html" xml:base="https://satpulse.net/2026/04/01/first-pre-release-of-satpulse-0.2.html"><![CDATA[<p>I just made the first pre-release of SatPulse 0.2.</p>

<p>The <a href="https://github.com/jclark/satpulse/releases/tag/v0.2-pre-20260401">release notes</a> have more information.</p>

<p>I would be grateful if you could try it out and let me know how you get on.</p>

<p>I created a <a href="https://github.com/jclark/satpulse/discussions/248">discussion thread</a> where you can ask questions and share experiences.</p>]]></content><author><name>James Clark</name></author><summary type="html"><![CDATA[I just made the first pre-release of SatPulse 0.2.]]></summary></entry><entry><title type="html">A tour of the GPS modules supported by SatPulse 0.2</title><link href="https://satpulse.net/2026/04/01/a-tour-of-the-gps-modules-supported-by-satpulse.html" rel="alternate" type="text/html" title="A tour of the GPS modules supported by SatPulse 0.2" /><published>2026-04-01T10:15:00+00:00</published><updated>2026-04-01T10:15:00+00:00</updated><id>https://satpulse.net/2026/04/01/a-tour-of-the-gps-modules-supported-by-satpulse</id><content type="html" xml:base="https://satpulse.net/2026/04/01/a-tour-of-the-gps-modules-supported-by-satpulse.html"><![CDATA[<p>For the last 8 months, I have been working on broadening the range of GNSS hardware supported by SatPulse. In 0.1, there was support only for u-blox modules. In 0.2, I have added support for a broad range of modules from other vendors, all of which are Chinese.</p>

<p>Before getting into the details of the supported modules, I want to talk about what supporting a GPS module means.
All GPS modules support NMEA which provides information about navigation solutions as they are computed by the module. GPS modules also support RTCM, which is used both to consume corrections provided to the module and to provide corrections for use by other modules. Apart from that, every GPS module supports one or more vendor-specific protocols. The messages used by these protocols can be divided into two groups. The first group performs a similar function to NMEA: they are messages periodically emitted by the module to provide information about the navigation solutions computed and other aspects of the ongoing operation of the module. The second group performs configuration: these include both requests that are input to the module to query and alter configuration, and messages that are output by the module as responses to these requests. Supporting a module involves enabling SatPulse to make use of both kinds of message.</p>

<p>SatPulse supports vendor-specific periodic messages in the obvious way, by converting vendor-specific messages into a uniform abstraction. In the source code, this abstraction is defined by the <code class="language-plaintext highlighter-rouge">gpsprot</code> (GPS protocol) package. In 0.1, there were abstract messages that provide information about</p>

<ul>
  <li>the current time in either UTC or TAI; also includes sawtooth error/quantization error and time accuracy</li>
  <li>the progress of a survey-in operation</li>
  <li>the next scheduled leap second</li>
  <li>satellite positions and signal strengths</li>
</ul>

<p>In 0.2, there are additional messages that provide information about</p>

<ul>
  <li>the characteristics of a navigation solution; this includes
    <ul>
      <li>the technique used to compute the solution (e.g. ranging code or carrier phase)</li>
      <li>dimensionality of the fix i.e. 3D, 2D or time only</li>
      <li>what sensors beyond GNSS contributed to the solution (e.g. INS)</li>
      <li>kinds of corrections used (e.g. SBAS, RTK, PPP)</li>
      <li>accuracy (position, velocity)</li>
      <li>DOP in all its flavours</li>
      <li>age of differential corrections</li>
      <li>GNSS constellations and bands used in computing the solution</li>
    </ul>
  </li>
  <li>position, in ECEF or geodetic coordinates</li>
  <li>velocity, in ECEF or geodetic coordinates (i.e speed and course-over-ground, or NED)</li>
</ul>

<p>These abstract messages can be serialized as JSON, and can be seen in the event log, which also includes information about pulse edges.
The messages also feed the observability subsystem, which exposes them as Prometheus metrics.</p>

<p>For configuration, there are now two kinds of configuration support as I described in more detail in my <a href="https://satpulse.net/2026/01/29/improving-gps-configuration.html">earlier blog</a>.</p>

<ul>
  <li>high-level configuration, intent-based configuration, where you describe what you want, and SatPulse turns that into the appropriate messages for the specific module</li>
  <li>low-level configuration, which is based on message files</li>
</ul>

<p>High-level configuration in 0.2 provides a similar set of features as in 0.1. There are a couple of new properties that can be set:</p>

<ul>
  <li>minimum elevation angle for satellites to be used in the navigation solution</li>
  <li>RTCM base ID - the reference station ID for the RTCM messages that the module generates</li>
</ul>

<p>I have added a few features to the message file implementation since the last blog:</p>

<ul>
  <li>the message file implementation now understands the request/response patterns of various protocols, which allows it to identify which messages from the module are responses to messages sent</li>
  <li>there is a file inclusion capability, which is useful for allowing model-specific message files to share messages that work across multiple vendor models</li>
</ul>

<p>The high/low-level configuration split leads naturally to two tiers of support. In both tiers, there is</p>

<ul>
  <li>support for generating the full range of abstract messages from the vendor-specific messages</li>
  <li>support for the protocol in the message file implementation</li>
</ul>

<p>The difference is that Tier 1 provides configuration support primarily via the high-level mechanism; message files are used to supplement this to support vendor-specific features that are not exposed via high-level configuration, whereas Tier 2 uses message files to provide configuration support.
For Tier 2, the provided message files use a common set of conventions for tags, which provides a degree of vendor-independence in the user interface.</p>

<p>SatPulse 0.1 had support only for u-blox. In 0.2, there is support for the following vendors</p>

<ol>
  <li>u-blox - tier 1</li>
  <li>Unicore - tier 1</li>
  <li>Quectel - tier 2</li>
  <li>Zhongke/CASIC - tier 2</li>
  <li>Allystar - tier 2</li>
  <li>Techtotop/Taidou - tier 2</li>
  <li>ByNav - tier 2</li>
  <li>ComNav/SinoGNSS - tier 2</li>
</ol>

<p>Apart from u-blox, these vendors are all based in China. There are other Western vendors, but they are all either much more expensive than u-blox (Septentrio, Trimble, NovAtel) or the chips are designed to be integrated into mobile phones and are not available as modules that you can buy separately (e.g. Broadcom). Septentrio has recently come down in price a bit and they also have an excellent reputation for timing, so I hope to add support for Septentrio in the future. I already have some support for NovAtel, because some Chinese vendors have adopted the NovAtel OEM 6/7 series protocol.</p>

<p>I have chosen which Chinese modules to support based on a number of factors:</p>

<ul>
  <li>Are they at least dual-band? Given the low cost of dual-band modules, there are relatively few cases where single-band modules remain interesting.</li>
  <li>Do they have any features that are particularly interesting for precision timing and/or positioning?</li>
  <li>How common and popular are they on Taobao?</li>
  <li>Are there any Western vendors that resell them?</li>
</ul>

<p>Taobao recently started supporting direct shipping to my country of residence,
which makes using Taobao much more convenient.
I have found Taobao vendors to be considerably more knowledgeable about what they are selling than AliExpress vendors.
If you can buy from Taobao, I recommend it.</p>

<h2 id="u-blox">u-blox</h2>

<p>SatPulse supports all u-blox modules starting from LEA-6T all the way through to ZED-X20P. I have tested it on all timing modules:</p>

<ul>
  <li>LEA-6T</li>
  <li>LEA-M8T</li>
  <li>LEA-M8F</li>
  <li>ZED-F9T (both L1/L2 and L1/L5 variants)</li>
  <li>NEO-F10T</li>
</ul>

<p>and on both current high-precision modules:</p>

<ul>
  <li>ZED-F9P</li>
  <li>ZED-X20P</li>
</ul>

<p>and on many standard precision modules, including:</p>

<ul>
  <li>NEO-M9N</li>
  <li>M10050-KB</li>
  <li>NEO-F10N (which is L1/L5)</li>
</ul>

<p>In 0.2, the improvements in u-blox support are:</p>

<ul>
  <li>support for richer information (navigation solution characteristics, position, velocity)</li>
  <li>support for additional configuration properties (minimum elevation and RTCM base id)</li>
  <li>support for ZED-X20P</li>
</ul>

<p>I have done much more testing on u-blox modules than on any other brand.</p>

<h2 id="unicore">Unicore</h2>

<p>There is tier 1 support for the UM980 family, which consists of</p>

<ul>
  <li>UM980, base model, which includes support for Galileo HAS (which is promised but not yet available for u-blox ZED-X20P)</li>
  <li>UM981, adds INS</li>
  <li>UM982, adds dual antennas</li>
  <li>UM960, lower end version, without PPP support</li>
</ul>

<p>These use a protocol which is similar to that used by NovAtel OEM6/7 messages. Periodic data messages have dual binary/ASCII formats;
the packet formats are similar, but the binary packet format has different sync bytes.
Configuration messages are ASCII lines, similar but different from NovAtel.
These modules also have some undocumented support for some periodic data messages that use the same packet format as NovAtel.
This includes the RANGECMPB raw message which can be used by RTKLIB to generate RINEX observation files.</p>

<p>In the West, these modules are distributed by <a href="https://www.ardusimple.com/product/simplertk3b-budget/">ArduSimple</a>, <a href="https://gnss.store/collections/unicore-gnss-modules">gnss.store</a> and <a href="https://www.sparkfun.com/sparkfun-triband-gnss-rtk-breakout-um980.html">SparkFun</a>.</p>

<p>SparkFun have a <a href="https://github.com/sparkfun/SparkFun_RTK_Torch/tree/main/UM980_Firmware">GitHub repo with the latest UM980 firmware</a>.</p>

<p>Unicore have another range of modules UM6XX, which uses a completely different protocol, which is not yet supported.</p>

<h2 id="quectel">Quectel</h2>

<p>Quectel have a bewildering array of GNSS modules. There are two families that I find interesting and that SatPulse supports:</p>

<ul>
  <li>LG290P family - this is Quectel’s high-end module which includes support for Galileo HAS in the most recent firmware
    <ul>
      <li>LG290P is the base model</li>
      <li>LG580P is the dual antenna variant</li>
      <li>LG680P is the LG290P in the 22x17mm u-blox ZED form factor</li>
    </ul>
  </li>
  <li>LC29H family - this is a relatively inexpensive L1/L5 model based on the Airoha AG3335 chipset; there are several variants, of which the most interesting are:
    <ul>
      <li>LC29H(AA) is the best fit for base-station case; it can produce RTCM corrections but does not have the RTK engine that can consume them; it is supposed to get OSNMA support at some point</li>
      <li>LC29H(DA) is the one that is suitable for use as an RTK rover</li>
    </ul>
  </li>
</ul>

<p>Quectel’s vendor specific protocol uses the NMEA proprietary extension mechanism. The NMEA sentences start with <code class="language-plaintext highlighter-rouge">$PQTM</code> (The <code class="language-plaintext highlighter-rouge">P</code> is the NMEA proprietary extension mechanism, and <code class="language-plaintext highlighter-rouge">QTM</code> is Quectel’s 3-letter identifier). The LG290P supports a richer set of PQTM messages than the LC29H.
The LC29H also supports some NMEA proprietary sentences defined by Airoha; these start with <code class="language-plaintext highlighter-rouge">$PAIR</code>.</p>

<p>In the West, SparkFun sell an <a href="https://www.sparkfun.com/sparkfun-quadband-gnss-rtk-breakout-lg290p-qwiic.html">LG290P board</a>.
They also have a <a href="https://github.com/sparkfun/SparkFun_RTK_Postcard/tree/main/Firmware">GitHub repo with the latest LG290P firmware</a>.</p>

<p>On Taobao, a good shop to buy from is <a href="https://mzhtek.taobao.com/">Mozi Technology/Mozihao</a>.</p>

<h2 id="zhongkecasic">Zhongke/CASIC</h2>

<p><a href="https://icofchina.com/">Zhongke Microelectronics</a> is best known for the ATGM332D and ATGM336H series of modules.
The difference between these is the form-factor: ATGM332D uses the u-blox NEO form factor,
whereas the ATGM336H uses the MAX form factor.
The notable feature of these modules is that they are extraordinarily cheap.
The bare module (without a board) is about $1.25 on Taobao.</p>

<p>The vendor-specific protocol is CASIC. CAS stands for Chinese Academy of Sciences,
and Zhongke in Chinese also refers to the Chinese Academy of Sciences. The CASIC protocol is UBX-like.</p>

<p>There are literally dozens of different versions of these modules, and there are some very significant differences between them.</p>

<p>The most common version is ATGM332D-5N31. These module names follow a common pattern.
The ‘5’ indicates the generation of the chip used (in this case, the AT6558).
‘N’ likely stands for navigation. The ‘3’ designates which constellations are supported:
1 means GPS, 2 means BeiDou, 4 means GLONASS, and these are added together to indicate the set of constellations supported. So 3 means GPS+BeiDou and 7 means GPS+BeiDou+GLONASS.
I haven’t figured out how the final digit works.</p>

<p>There is a more recent and less common 6 series (e.g. the ATGM332D-6N74) which adds support for Galileo. This uses the AT6668 chip. More interestingly, there is also an F8 series (e.g. <a href="https://www.icofchina.com/daohang/duopin/2531.html">ATGM332D-F8N76</a>), which is dual-band.
This uses the AT9880 chip.
It is not common yet but I expect it will become common:
like the rest of the ATGM332D series this is extraordinarily cheap, about $2.15 on Taobao;
it seems to be the cheapest dual-band module in the world.</p>

<p>The 5 series use a default baud rate of 9600, whereas the 6 and F8 series use 115200.
More significantly, although they all use the same packet format,
the 6 and F8 series support a different set of messages.
The 5 series uses NAV class messages, whereas the 6 and F8 series uses NAV2 class messages.</p>

<p>There is some English language documentation available for the version of CASIC that uses NAV messages;
there is nothing available on the NAV2 messages.
Fortunately, one of my Taobao sellers was able to supply me with a Chinese language protocol manual which describes this version of CASIC.
SatPulse has support for both the NAV messages and the NAV2 messages.</p>

<p>As well as the low-end ATGM332D/ATGM336H modules, Zhongke also make higher-end modules designed explicitly for timing.
I have the AT632-6T-30. This is similar to the 6 series of the ATGM332D, and is L1 only, but has a full set of timing features, which makes it interesting.
These use a TIM2 class of message. In particular it has a TIM2-TPX message including quantization error.
It is interesting to have a modern L1 timing module; u-blox have not done an L1 timing module since the LEA-M8T.
One noticeable difference is that the peak-to-peak amplitude of the sawtooth error is about 6ns on the AT632 compared to 21ns on the M8T,
presumably reflecting the higher clock speed of the more modern module.</p>

<p>Zhongke provide a GnssToolkit3 application for Windows for configuring their modules.</p>

<h2 id="allystar">Allystar</h2>

<p>I first came across <a href="https://www.allystar.com/en">Allystar</a> because of their TAu1201 module, which has been one of the cheapest L1/L5 modules available.
The <a href="https://xinghewei.tmall.com/">StarRiver store</a> on Taobao sell a variant of their SR1723 board with the TAU1201.
This costs about $7.50. With various fees, shipping and tax to Thailand, the all-in cost is about $10.50.
In the West, it will be a bit more.
The TAU1201 uses their Cynosure III chip. Their latest chip is Cynosure IV, which is available in the TAU951M-P200.
This is a more expensive module that also does RTK.</p>

<p>Allystar provide a Windows app called <a href="https://docs.datagnss.com/rtk-board/files/Satrack_client_V1.31.007.zip">Satrack</a> for working with Allystar modules.</p>

<p>Allystar’s binary protocol is UBX-like. They haven’t done a particularly thorough job of it.
For example, the message that provides information about satellite positions and signals provides
less information than is available via NMEA, and Satrack doesn’t make use of it.</p>

<h2 id="bynav">Bynav</h2>

<p><a href="https://www.bynav.com/en/">Bynav</a> makes M20/M10 series of modules based on their own Alice 22nm SoC.
They are strong in the automotive industry.
I have been mainly testing with the M20, but have also tried the M10.
The M21 adds an IMU on top of the M20; the M22 adds a slightly better grade of IMU.
The M20D adds dual antennas.
The M10 is a smaller-footprint, cheaper, lower-end version of the M20 (update rate of 10Hz vs 50Hz on the M20).
They seem pretty decent for RTK.</p>

<p>Bynav’s vendor-specific protocol is NovAtel-style. Their binary and ASCII packet formats for periodic messages (called logs in NovAtel parlance)
are exactly the same as documented by NovAtel for the OEM6/7 range.
Bynav support some messages defined by NovAtel and add some messages of their own.
The configuration commands are NovAtel-style, but not compatible.</p>

<p>In the West, they are sold by <a href="https://gnss.store/collections/bynav-gnss-modules">gnss.store</a>.</p>

<h2 id="comnavsinognss">ComNav/SinoGNSS</h2>

<p>This company brands itself as <a href="https://www.comnavtech.com/">ComNav Technology</a> for Western audiences,
but uses <a href="https://www.sinognss.com/">SinoGNSS</a> for Chinese audiences.
Their latest modules are the K9xx series.
Base module is <a href="https://www.comnavtech.com/product/oem/k901.html">K901</a>. <a href="https://www.comnavtech.com/product/oem/k902.html">K902</a> adds an IMU. <a href="https://www.comnavtech.com/product/oem/k922.html">K922</a> adds dual antennas.
They all have support for Galileo HAS.
These are widely available on Taobao.</p>

<p>The protocol documentation on the ComNav site is rather out of date.
My Taobao seller gave me the current Chinese-language protocol documentation,
which was from another company called <a href="https://www.qinnav.com/product/module/">Qeetek</a>, which also advertises these modules.
So I suspect these modules are actually made by Qeetek.</p>

<p>The situation with the protocol is very similar to Bynav.
Their binary and ASCII packet formats for periodic messages are exactly the same as NovAtel.
They support some messages defined by NovAtel and some of their own.
The configuration commands are NovAtel-style, but not compatible.
But the configuration system is not in my view well-designed.
One major deficiency is that it does not allow the current configuration to be queried.
Another problem is that responses to configuration commands are not designed to be machine readable:
they are not wrapped in a protocol packet that allows them to be distinguished from other traffic from the module.</p>

<p>Apart from the weakness in the configuration protocol, I found convergence of both Galileo HAS 
and its BeiDou equivalent (B2b-PPP) to be flaky.</p>

<h2 id="techtotoptaidou">Techtotop/Taidou</h2>

<p>I had not heard of <a href="https://www.techtotop.com/enindex.aspx">Techtotop</a>, known in Chinese as Taidou, until a month or so ago.
They make their own GNSS silicon, but they seem to be almost completely unknown outside China.</p>

<p>I find them interesting because they have relatively inexpensive modules specifically designed for timing.
The one I have is the T303-5D, which is an L1/L5 module. This isn’t on their web site, but I believe it is just a smaller footprint
version of the <a href="https://www.techtotop.com/detail.aspx?cid=1088">T302-5D</a>.
I got this module from <a href="https://shop471758324.taobao.com/">this shop on Taobao</a>.</p>

<p>It uses a UBX-style protocol called SDBP, about which there appears to be exactly zero information available in English.
My Taobao seller was able to provide a Chinese language protocol manual with full details.
The quality of documentation and protocol design seem good to me:
it has everything I expect of a timing module, including quantization error reporting.</p>

<p>Techtotop also provide the <a href="https://www.techtotop.com/category.aspx?NodeID=49">TDMonitor</a> application for Windows,
which is similar to u-center (although the UI is all Chinese).</p>

<h2 id="where-satpulse-is-heading">Where SatPulse is heading</h2>

<p>My vision for SatPulse 0.1 was quite narrow: to transfer time from a GPS module to a PTP hardware clock. But it turns out doing a really good job of that requires a
complex and sophisticated GPS subsystem.
The subsystem should work for GPS modules from many vendors not just from u-blox.
When monitoring a timing GPS, you want rich information about the current state of GPS,
including the characteristics of the navigation solution.
And it turns out that getting position information from the GPS is also useful:
for example, you may want to use HAS to establish the fixed position to be used in timing.
If you have done the work to create a GPS subsystem that works well for timing,
you have done at least 80% of the work to create a GPS subsystem that works well for a wide variety of other applications.
It makes sense to do that extra work, because the market for timing is relatively small:
many more people care about precision positioning than care about precision timing.
So this is where I am heading with SatPulse:
precision timing is still a core part of the mission;
but I want to add the few extra features that are needed to make SatPulse useful for
a broader range of applications, in particular precision positioning.</p>]]></content><author><name>James Clark</name></author><summary type="html"><![CDATA[For the last 8 months, I have been working on broadening the range of GNSS hardware supported by SatPulse. In 0.1, there was support only for u-blox modules. In 0.2, I have added support for a broad range of modules from other vendors, all of which are Chinese.]]></summary></entry><entry><title type="html">Using SatPulse for timing without a PHC</title><link href="https://satpulse.net/2026/04/01/using-satpulse-for-timing-without-a-phc.html" rel="alternate" type="text/html" title="Using SatPulse for timing without a PHC" /><published>2026-04-01T09:45:00+00:00</published><updated>2026-04-01T09:45:00+00:00</updated><id>https://satpulse.net/2026/04/01/using-satpulse-for-timing-without-a-phc</id><content type="html" xml:base="https://satpulse.net/2026/04/01/using-satpulse-for-timing-without-a-phc.html"><![CDATA[<p>Up to now, using SatPulse for timing has required some <a href="/hardware/">very specialized hardware</a>.
Over the last couple of days, I have implemented a feature that removes this requirement.
It works very simply. SatPulse already has the ability to talk to an NTP server using chrony’s refclock SOCK protocol;
this is configured by the <code class="language-plaintext highlighter-rouge">[ntp]</code> section of <code class="language-plaintext highlighter-rouge">satpulse.toml</code>.
SatPulse also has the ability to work without a PTP hardware clock;
this is configured simply by leaving out the <code class="language-plaintext highlighter-rouge">[phc]</code> section of <code class="language-plaintext highlighter-rouge">satpulse.toml</code>.
In this mode, you can use SatPulse for monitoring and for GNSS packet distribution.
What I’ve implemented is to make the <code class="language-plaintext highlighter-rouge">[ntp]</code> section work without a <code class="language-plaintext highlighter-rouge">[phc]</code> section.
In this situation, SatPulse will use the timing of serial messages from the receiver
to generate samples to send to the NTP server.
On its own, the accuracy from serial timing will be very poor: worse than you would typically get from NTP over a network.
But this becomes useful when combined with an NTP server that has the ability to read a PPS signal.
For example chrony has a PPS refclock that uses the kernel PPS subsystem.
Chrony can also read a PPS signal from a PHC using the PHC refclock with the extpps option.
In this case the NTP server uses the much more accurate PPS signal to determine when a second starts,
and will use the information from SatPulse to determine which second it is.</p>

<p>I have hooked this up to SatPulse’s GPS auto-configuration system (enabled with <code class="language-plaintext highlighter-rouge">config=true</code> in the <code class="language-plaintext highlighter-rouge">[gps]</code> section).
So when you have an <code class="language-plaintext highlighter-rouge">[ntp]</code> and no <code class="language-plaintext highlighter-rouge">[phc]</code> section, it will configure the GPS appropriately (e.g enable a PPS,
enable time mode, enable messages reporting UTC time).
I think this will provide quite a nice way of running an NTP server: as well as auto-configuration, you get the
other SatPulse conveniences like a Web dashboard and Prometheus metrics.</p>

<p>I have only tested this very lightly. I used the chrony PHC extpps option to test,
since all my machines are currently set up with the PPS connected to a PHC.
I used the following as the satpulsed configuration:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>[serial]
device = "/dev/ttyACM0"
speed = 38400
[gps]
config = true
vendor = "u-blox"
[ntp]
sock.path = "/var/run/chrony.satpulse.clk.sock"
</code></pre></div></div>

<p>and then used this as my chrony configuration:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>refclock PHC /dev/ptp0:extpps:pin=1 width 0.1 poll 2 lock NMEA refid PPS
refclock SOCK /var/run/chrony.satpulse.clk.sock offset 0.1 delay 0.2 refid NMEA noselect
</code></pre></div></div>

<p>The <code class="language-plaintext highlighter-rouge">lock NMEA</code> option tells chrony to use the samples from SatPulse to complete the PHC samples;
the <code class="language-plaintext highlighter-rouge">noselect</code> option tells chrony not to use the SatPulse samples as an independent time source.</p>

<p>Using chrony to read the PHC like this has one advantage over using SatPulse with a <code class="language-plaintext highlighter-rouge">[phc]</code> section:
SatPulse will adjust the phase and frequency of the PHC, which is what you want for PTP,
whereas chrony will leave it free-running.
This makes it easy to use the hardware-timestamping feature of chrony, which requires
a free-running PHC.
With SatPulse, you would need to use a virtual PHC, which I have not yet implemented support for.
So using chrony like this is right now the best approach if you want NTP hardware timestamping
and are not interested in PTP.</p>]]></content><author><name>James Clark</name></author><summary type="html"><![CDATA[Up to now, using SatPulse for timing has required some very specialized hardware. Over the last couple of days, I have implemented a feature that removes this requirement. It works very simply. SatPulse already has the ability to talk to an NTP server using chrony’s refclock SOCK protocol; this is configured by the [ntp] section of satpulse.toml. SatPulse also has the ability to work without a PTP hardware clock; this is configured simply by leaving out the [phc] section of satpulse.toml. In this mode, you can use SatPulse for monitoring and for GNSS packet distribution. What I’ve implemented is to make the [ntp] section work without a [phc] section. In this situation, SatPulse will use the timing of serial messages from the receiver to generate samples to send to the NTP server. On its own, the accuracy from serial timing will be very poor: worse than you would typically get from NTP over a network. But this becomes useful when combined with an NTP server that has the ability to read a PPS signal. For example chrony has a PPS refclock that uses the kernel PPS subsystem. Chrony can also read a PPS signal from a PHC using the PHC refclock with the extpps option. In this case the NTP server uses the much more accurate PPS signal to determine when a second starts, and will use the information from SatPulse to determine which second it is.]]></summary></entry><entry><title type="html">Improving SatPulse’s support for GPS configuration</title><link href="https://satpulse.net/2026/01/29/improving-gps-configuration.html" rel="alternate" type="text/html" title="Improving SatPulse’s support for GPS configuration" /><published>2026-01-29T00:00:00+00:00</published><updated>2026-01-29T00:00:00+00:00</updated><id>https://satpulse.net/2026/01/29/improving-gps-configuration</id><content type="html" xml:base="https://satpulse.net/2026/01/29/improving-gps-configuration.html"><![CDATA[<p>In this post, I want to describe some recent improvements in how SatPulse supports GPS configuration.</p>

<h2 id="how-gps-receivers-communicate">How GPS receivers communicate</h2>

<p>To understand why this feature is useful, it helps to understand how GPS receivers work at the protocol level. There are three layers: packet format, periodic messages, and configuration.</p>

<p>GPS receivers communicate over a serial connection by sending and receiving a byte stream consisting of a sequence of packets. Each packet has a specific format, and the stream can mix packets in different formats. A packet format defines how to represent a message type and a type-specific structured payload as a sequence of bytes with a checksum.  NMEA 0183 is the most important standard packet format. A NMEA packet is called a sentence: it starts with <code class="language-plaintext highlighter-rouge">$</code> and ends with a two-character hex checksum and CR/LF. The payload is represented as comma-delimited ASCII fields. RTCM 3 is another standard packet format, used for GPS correction data such as RTK base station observations. It uses a binary encoding with a 24-bit checksum. Many vendors define their own proprietary binary packet formats. For example, u-blox defines the UBX format.</p>

<p>GPS receivers compute position-velocity-time (PVT) solutions periodically, typically once per second. For each solution, they output messages with the results of the solution, and other information about how the solution was computed (such as the satellites used). These messages will be represented as message types in a packet format. NMEA defines standard sentence types, such as RMC, GGA, GSV for this and all GPS receivers support them. Almost all GPS receivers can also emit other periodic messages that provide information beyond what can be expressed by standard NMEA sentences. Some GPS receivers make use of the extensibility mechanism provided by NMEA (“proprietary sentences” beginning with <code class="language-plaintext highlighter-rouge">$P</code>), but many others use messages in proprietary binary packet formats.</p>

<p>All GPS receivers provide a configuration mechanism. At a minimum they provide a way to control the speed of the serial connection and which messages are emitted. But most GPS receivers allow for many aspects of their operation to be configured, for example, which constellations they should use or the width of the PPS pulse that they should generate. For GPS configuration, it is completely vendor-dependent: there is no standard. Configuration requires communication in both directions: from host to GPS receiver and well as from GPS receiver to host. As with periodic messages, configuration uses packets. RTCM 3 messages go both from host to receiver as well as from receiver to host, so GPS receivers need to be able to deal with a mix of packet formats in the input they receive.</p>

<p>I have seen three different approaches to configuration.</p>

<ul>
  <li>Configuration uses the same binary packet format used for periodic messages. UBX is the most common example of this. A few other vendors use UBX-like protocols: CASIC (Zhongke Microsystems) and Allystar</li>
  <li>Configuration requests (from host to receiver) use NMEA proprietary sentences; responses may be either standard NMEA TXT sentences or proprietary sentences. Quectel is one vendor that uses this approach.</li>
  <li>Configuration requests use plain ASCII lines (usually CRLF terminated); responses are typically some sort of ASCII packet. Septentrio and NovAtel both use this approach. Their products are both very high-end, but several Chinese vendors have adopted protocols based on NovAtel, notably Unicore, SinoGNSS (ComNav) and ByNAV, in more affordable products.</li>
</ul>

<h2 id="satpulse-01">SatPulse 0.1</h2>

<p>SatPulse 0.1 internally uses protocol-independent interfaces for all three layers, but they are implemented only for UBX.</p>

<p>It might seem unnecessary to have such abstraction for only one protocol, but in fact UBX has many versions which have major differences.
Only the packet layer of UBX remains constant between versions. The periodic message layer has significant differences: different versions of receivers support different messages. With the configuration layer, u-blox receivers from generation 9 onwards use a completely different approach from receivers of generation 8 and earlier. The earlier generations have separate message types for each aspect of configuration. The later generation uses a single message that wraps a completely separate key-value configuration system: this allows multiple configuration changes to be performed atomically by a single message.</p>

<p>The fundamental idea in 0.1 is to create protocol independent representation of all the configuration that needs to be done. This representation has two parts:</p>
<ul>
  <li>it specifies the desired values for various properties; for example, it might say that the time pulse should have a pulse width of 0.1s,</li>
  <li>it specifies various operations that should be performed; for example, it might say that the configuration of the receiver should be saved to non-volatile memory
This representation is handed over to a protocol-specific subsystem, which does as much of what was requested as the receiver supports.
It then returns the actual properties that the receiver was able to implement. For example, there is a property for the set of GNSS signals that should be enabled. If you request that L1 and L5 signals should be enabled, but the receiver only supports L1, then it will enable L1 and return a property with only L1 being enabled.</li>
</ul>

<p>You might ask: why bother with all this? SatPulse works best if the receiver is configured in very specific ways. My goal is to provide a “just works” experience: so I want SatPulse to be able to detect what kind of receiver it is and automatically configure it so that it works optimally. SatPulse 0.1 does achieve this for a broad range of UBX receivers.</p>

<h2 id="multi-protocol-support">Multi-protocol support</h2>

<p>One of my main goals for SatPulse after version 0.1 is to make it truly multi-protocol. It is one thing to design an interface to be protocol-independent. It is another thing for it to actually work well for a range of protocols. In choosing the second protocol to support, I wanted a protocol that was rich and sophisticated, completely different from UBX and has decent documentation.</p>

<p>I decided on the protocol implemented by the UM98x series of GPS receivers from Unicore, which is based on the NovAtel OEM 6 and 7 protocols. This is primarily designed for the RTK market, but it works well for timing also. Periodic messages (which are called logs in the NovAtel world) have dual binary/ASCII representations. Configuration requests use plain ASCII lines; responses are NMEA-like.</p>

<p>In implementing the configuration support for the UM98x, I substantially reworked the configuration interface.
The objective is to make the protocol-specific code as simple and testable as possible.
The protocol-specific code is completely deterministic and does no IO.
An intermediate protocol-independent orchestration layer does IO and manages retries and timeouts.</p>

<p>The documented UM98x packet formats and logs are similar but distinct from the NovAtel OEM6/7 ones. But it turns out that UM98x can also be configured to generate packet formats and logs that are exactly as defined in the NovAtel documentation. These packet formats and logs are also implemented by a couple of other recent Chinese GNSS receivers (Bynav M20 and SinoGNSS K901), so I also implemented support for these.</p>

<h2 id="gps-message-files">GPS message files</h2>

<p>The approach to configuration in 0.1 has some fundamental limitations:</p>

<ul>
  <li>it’s a lot of work to implement configuration support</li>
  <li>if this work has not been done for a receiver, then SatPulse provides no help at all in configuration</li>
  <li>if you want to configure a receiver feature that SatPulse does not support, then again SatPulse provides no help</li>
</ul>

<p>Most vendors provide a proprietary, but free-to-use Windows GUI app that can be used to configure their receivers.
For example, u-blox provides u-center.
In practice, for many configuration tasks with SatPulse 0.1, users would have to rely on these Windows apps.
For users living on Linux or macOS, this is not a good situation.</p>

<p>The multi-protocol support is a nice addition but does not address these limitations.</p>

<p>In the last week, I have implemented an alternative, more pragmatic approach to configuration that tries to address these limitations.
The approach is a simple one: have a file that defines messages that can be sent to the GPS receiver.
There is a new <code class="language-plaintext highlighter-rouge">-m</code>/<code class="language-plaintext highlighter-rouge">--msg-file</code> option for <code class="language-plaintext highlighter-rouge">satpulsetool gps</code> that specifies the message definition file.
This does not use the configuration abstraction at all and allows arbitrary messages to be sent to the receiver.
Since the main SatPulse configuration file is TOML, I also chose TOML for the format of this definition file.</p>

<p>Here’s a simple example. The Unicore UM980 supports Galileo HAS, but SatPulse configuration abstraction does not yet have anything for this.
To enable Galileo HAS, create a file <code class="language-plaintext highlighter-rouge">um980-has.toml</code>.</p>

<div class="language-toml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nn">[[line]]</span>
<span class="py">text</span> <span class="p">=</span> <span class="s">"CONFIG PPP ENABLE E6-HAS"</span>
<span class="nn">[[line]]</span>
<span class="py">text</span> <span class="p">=</span> <span class="s">"CONFIG PPP CONVERGE 10 20"</span>
</code></pre></div></div>

<p>Each <code class="language-plaintext highlighter-rouge">[[line]]</code> block defines a text command. Run it with:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>satpulsetool gps -d /dev/ttyUSB0 -s 115200 -m um980-has.toml
</code></pre></div></div>

<p>This will send each line, terminated with CR/LF by default.
When used in this simple way, the main problem that this solves is enabling the user to see how the GPS receiver responded to the message.
Since SatPulse knows about packet formats, it can intelligently identify which packets might be responses to the message and display only those.
If you use <code class="language-plaintext highlighter-rouge">cat</code> and <code class="language-plaintext highlighter-rouge">stty</code>, you have no idea how the receiver responded.
If you try to use a terminal emulator, the periodic messages being continually output by the receiver (which may include binary) makes it difficult to see the responses.
There is also a <code class="language-plaintext highlighter-rouge">--packet-log</code> option that allows you to capture all packets sent and received.</p>

<h3 id="nmea-message-type">NMEA message type</h3>

<p>The <code class="language-plaintext highlighter-rouge">nmea</code> message type is similar to <code class="language-plaintext highlighter-rouge">line</code>, but it knows about NMEA checksums.
For example, this is how to configure PPS on a Quectel LG290P.</p>

<div class="language-toml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nn">[[nmea]]</span>
<span class="py">text</span> <span class="p">=</span> <span class="s">"PQTMCFGPPS,W,1,1,100,2,1,0"</span>
</code></pre></div></div>

<p>The tool prepends <code class="language-plaintext highlighter-rouge">$</code> if missing and appends the checksum automatically.</p>

<h3 id="message-libraries">Message libraries</h3>

<p>The message file can also be used to create a library of messages.
Each message can be given a tag and a description.</p>

<div class="language-toml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nn">[[nmea]]</span>
<span class="py">text</span> <span class="p">=</span> <span class="s">"PQTMCFGMSGRATE,W,RMC,1"</span>
<span class="py">tag</span> <span class="p">=</span> <span class="s">"nmea-daemon"</span>
<span class="py">description</span> <span class="p">=</span> <span class="s">"Enable NMEA messages understood by satpulse daemon"</span>
<span class="nn">[[nmea]]</span>
<span class="py">text</span> <span class="p">=</span> <span class="s">"PQTMCFGMSGRATE,W,GGA,1"</span>
<span class="py">tag</span> <span class="p">=</span> <span class="s">"nmea-daemon"</span>
<span class="nn">[[nmea]]</span>
<span class="py">text</span> <span class="p">=</span> <span class="s">"PQTMCFGMSGRATE,W,GSA,1"</span>
<span class="py">tag</span> <span class="p">=</span> <span class="s">"nmea-daemon"</span>
<span class="nn">[[nmea]]</span>
<span class="py">text</span> <span class="p">=</span> <span class="s">"PQTMCFGMSGRATE,W,GSV,1"</span>
<span class="py">tag</span> <span class="p">=</span> <span class="s">"nmea-daemon"</span>
<span class="nn">[[nmea]]</span>
<span class="py">text</span> <span class="p">=</span> <span class="s">"PQTMSAVEPAR"</span>
<span class="py">tag</span> <span class="p">=</span> <span class="s">"save"</span>
<span class="py">description</span> <span class="p">=</span> <span class="s">"Save configuration to NVM"</span>
</code></pre></div></div>

<p>Run specific tags with <code class="language-plaintext highlighter-rouge">-t</code>:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>satpulsetool gps -d /dev/ttyUSB0 -s 460800 -m quectel.toml -t nmea-daemon,save
</code></pre></div></div>

<p>The <code class="language-plaintext highlighter-rouge">--show-tags</code> flag lists available tags with descriptions.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>satpulsetool gps -m quectel.toml --show-tags
</code></pre></div></div>

<h3 id="binary-messages">Binary messages</h3>

<p>Sending binary messages is a less straightforward.</p>

<p>You can specify the exact bytes to send. For example, with u-blox L5 receivers,
there is a special command you need to send get GPS L5 signal to work.
The u-blox docs give it as a hex string.</p>

<div class="language-toml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nn">[[binary]]</span>
<span class="py">hex</span> <span class="p">=</span> <span class="s">"B562068A0900000100000100321001DEED"</span>
<span class="py">tag</span> <span class="p">=</span> <span class="s">"gps-l5-health"</span>
<span class="py">description</span> <span class="p">=</span> <span class="s">"Use GPS L5 signal regardless of health status"</span>
</code></pre></div></div>

<p>But usually it is not convenient to specify the full byte sequence directly.</p>

<p>When SatPulse has support for a binary packet format, you can use this to specify things in a more readable way.
For example, let’s take the CASIC binary protocol, which is similar to UBX.
SatPulse has support for the packet format layer, but does not have configuration support.
There’s a CFG-TP message for controlling the time pulse.
The CASIC specification describes it like this.</p>

<p><strong>CFG-TP (0x06 0x03)</strong></p>

<p><strong>Payload Content</strong></p>

<table>
  <thead>
    <tr>
      <th style="text-align: left">Offset</th>
      <th style="text-align: left">Type</th>
      <th style="text-align: left">Name</th>
      <th style="text-align: left">Unit</th>
      <th style="text-align: left">Description</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td style="text-align: left">0</td>
      <td style="text-align: left">U4</td>
      <td style="text-align: left">interval</td>
      <td style="text-align: left">us</td>
      <td style="text-align: left">Pulse Interval</td>
    </tr>
    <tr>
      <td style="text-align: left">4</td>
      <td style="text-align: left">U4</td>
      <td style="text-align: left">width</td>
      <td style="text-align: left">us</td>
      <td style="text-align: left">Pulse Width</td>
    </tr>
    <tr>
      <td style="text-align: left">8</td>
      <td style="text-align: left">U1</td>
      <td style="text-align: left">enable</td>
      <td style="text-align: left"> </td>
      <td style="text-align: left">Enable Flag (0=Off, 1=On, 2=Auto Maintain, 3=Fix Only)</td>
    </tr>
    <tr>
      <td style="text-align: left">9</td>
      <td style="text-align: left">I1</td>
      <td style="text-align: left">polar</td>
      <td style="text-align: left"> </td>
      <td style="text-align: left">Polarity (0=Rising, 1=Falling)</td>
    </tr>
    <tr>
      <td style="text-align: left">10</td>
      <td style="text-align: left">U1</td>
      <td style="text-align: left">timeRef</td>
      <td style="text-align: left"> </td>
      <td style="text-align: left">0=UTC, 1=Satellite Time</td>
    </tr>
    <tr>
      <td style="text-align: left">11</td>
      <td style="text-align: left">U1</td>
      <td style="text-align: left">timeSource</td>
      <td style="text-align: left"> </td>
      <td style="text-align: left">0=GPS, 1=BDS, 2=GLN, 4=BDS(Main), 5=GPS(Main), 6=GLN(Main)</td>
    </tr>
    <tr>
      <td style="text-align: left">12</td>
      <td style="text-align: left">R4</td>
      <td style="text-align: left">userDelay</td>
      <td style="text-align: left">s</td>
      <td style="text-align: left">User Delay</td>
    </tr>
  </tbody>
</table>

<p>We can use this to define a message as follows:</p>

<div class="language-toml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nn">[[casbin]]</span>
<span class="py">tag</span> <span class="p">=</span> <span class="s">"pps-gps"</span>
<span class="py">description</span> <span class="p">=</span> <span class="s">"Enable PPS aligned to GPS time"</span>
<span class="py">class</span> <span class="p">=</span> <span class="mh">0x06</span>
<span class="py">id</span> <span class="p">=</span> <span class="mh">0x03</span>
<span class="py">payload.types</span> <span class="p">=</span> <span class="s">"U4U4U1I1U1U1R4"</span>
<span class="py">payload.values</span> <span class="p">=</span> <span class="p">[</span><span class="mi">1000000</span><span class="p">,</span> <span class="mi">100000</span><span class="p">,</span> <span class="mi">3</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">1</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mf">0.0</span><span class="p">]</span>
</code></pre></div></div>

<p>Each type descriptor in the <code class="language-plaintext highlighter-rouge">payload.types</code> string specifies how to encode corresponding entry in <code class="language-plaintext highlighter-rouge">payload.values</code>.
SatPulse doesn’t know about the CFG-TP message but it does know how CASIC binary packets work, and it can use this
to produce the right packet from this higher-level description (the packet has two sync bytes, the payload length, the class, the message id, the payload with values in little-endian byte-order and then a checksum).</p>

<h3 id="using-ai-to-create-message-libraries">Using AI to create message libraries</h3>

<p>I have had good success using AI to create message libraries. The workflow is:</p>

<ol>
  <li>convert the protocol spec from PDF into a more AI-friendly format such as Markdown</li>
  <li>prompt a coding agent (e.g., Claude Code) to generate a message library, giving it:
    <ul>
      <li>the protocol spec</li>
      <li>description of the message file format</li>
      <li>a few examples of a message library</li>
    </ul>
  </li>
  <li>allow the agent to use satpulsetool to access the GPS receiver:
    <ul>
      <li>try a message in the message library and see whether the receiver ACKs it</li>
      <li>capture output from the receiver before and after to see whether the configuration message has had the expected effect on the output (use <code class="language-plaintext highlighter-rouge">--capture N</code> with <code class="language-plaintext highlighter-rouge">--packet-log</code> to capture packets for N seconds)</li>
    </ul>
  </li>
</ol>

<h3 id="examples">Examples</h3>

<p>The <a href="https://github.com/jclark/satpulse/tree/master/configs/gpsmsg">configs/gpsmsg</a> directory in the repository has some example message libraries.</p>]]></content><author><name>James Clark</name></author><summary type="html"><![CDATA[In this post, I want to describe some recent improvements in how SatPulse supports GPS configuration.]]></summary></entry></feed>