A WinUI 3 desktop application for monitoring and controlling HP/Symmetricom SmartClock GPS-disciplined oscillators over RS-232.
| Receivers | the HP/Symmetricom Z3805A and its SmartClock siblings — Z3801A, 58503A/B, 59551A, Z3816A; and, for monitoring only, any NMEA 0183 GNSS talker |
| Platform | Windows 10 (22H2) and Windows 11, x64 — see Supported platforms |
| Stack | WinUI 3 (Windows App SDK) on .NET 10, packaged as MSIX |
| Extensible | every receiver-specific fact sits behind one interface, IReceiverDriver; another GPS-disciplined oscillator is a driver plus one registration line, not a fork — see Adding a receiver |
The SmartClock family — the Z3805A and its siblings — is widely used in home and
small labs as a 10 MHz frequency and 1 PPS time reference. The receivers expose a
rich SCPI command set over a serial port, but the tools built to drive them are
Windows-9x-era applications that need serial-port shims, look badly out of place
on modern Windows, and in several cases put destructive firmware commands
directly next to harmless queries. The alternative is screen-scraping
:SYST:STAT? in a terminal emulator.
Two ideas shape this replacement:
- A glanceable primary window. The receiver's state should be readable at a glance on a second monitor that has been left running for weeks — not a reproduction of the device's 80×24 terminal screen, but a native Fluent surface.
- Destructive commands are unreachable, not merely warned about. The command catalog is an allowlist. Commands that can damage a receiver's calibration or firmware are absent from it entirely — they are not entries carrying a warning flag, and there is no dialog that leads to them.
Feature-complete against the specification's P0 set and in daily use against a
bench Z3805A; it is installed from the release zips
(latest release). The transport, parser, command
model, design system and every view are implemented, with the test suite and
every CI gate green. The
issue list — whose §
references resolve against the specification — says what is open. Three
receiver families are driven, and at least one member of each has now answered
on the bench — though for the UCCM that is a single module of one variant, and
the caveat below still stands.
Where it stands against Lady Heather, the tool most people run on these
receivers today, is in
docs/lady-heather-comparison.md.
| Receiver | Serial default |
|---|---|
| HP/Symmetricom Z3805A (reference device) | 9600-8-N-1 |
| Symmetricom Z3801A | 19200-7-O-1 from the factory; some units in the field 19200-7-E-1 |
| HP/Symmetricom 58503A/B | — |
| Symmetricom 59551A | — |
| Symmetricom Z3816A | — |
Any NMEA 0183 GNSS talker — a u-blox module, a marine receiver; proven against the simulator under tools/ and, since 7 Sep 2026, against two real pucks — ten captures replayed in CI |
4800-8-N-1 (the standard), commonly 9600 |
| Symmetricom and Trimble UCCM / UCCM-P — telecom GPSDO modules; mostly written from a third-party implementation, with one Trimble UCCM-P on the bench over 10–13 Sep 2026 | 57600-8-N-1 on that unit; other rates unverified |
The SmartClock units share the 58503A/B command set. The NMEA family is the
second driver (docs/tutorial-nmea-driver.md): it
gets the monitoring core — fix state, satellites, position and time — and a
talker has no disciplined oscillator, so the pages that are only about one are
dimmed in the navigation pane rather than filled with dashes. It sends exactly
one sentence, and only to a u-blox module (#508): $PUBX,04, a poll that
configures nothing, reads back GPS − UTC and the module's clock bias and drift,
and is asked only of hardware whose own banner said whose it was. Every other
talker is overheard and never written to.
The UCCM family is the third, and it comes with a caveat the other two do not:
most of it was written by reading another program's source rather than a vendor
document or a capture from real hardware, so treat what it reports as
provisional. One Trimble UCCM-P has now been on the bench across seven sittings
between 10 and 13 September 2026
(the captures), which settled the
line settings, all three protocol hypotheses — the module does not echo, its
C5 time codes are binary packets rather than the text the capture harness
had been looking for, and Command complete does terminate a reply — and, on the
last day, the state bytes for a module that is not locked, which no earlier
sitting could reach. That is still one module of one variant: a plain UCCM has
never been seen, no Symmetricom module has, and much of the driver remains a
citation. It is read-only —
the several writes those modules accept are deliberately absent from its catalog,
because deciding their safety tier without watching one execute would be guessing
at consequences.
Because the defaults
differ between siblings, every serial parameter is user-settable — baud, data
bits, parity, and stop bits — and the connection dialog offers an auto-detect
that walks the union of every registered driver's likely combinations — eleven
today — listening at each for a receiver that talks unprompted and sending
*IDN? only when nothing has claimed what it heard, until one is recognised. The
settings that worked on a given port last time are tried first (#502), so only a
first connection pays for the walk.
Handshaking is always off; DTR and RTS are asserted on open. §7.1 of the
specification gives the full parameter ranges.
The application is a Windows App SDK (WinUI 3) desktop app, so what it runs on is what the Windows App SDK runs on.
| Minimum | Windows 10, version 22H2 (build 10.0.19045) |
| Also supported | Windows 11, 21H2 through 25H2 |
| Windows Server | Server 2022 (20348) |
| Architecture | x64 only — see §6.1. Windows on ARM is not a supported configuration |
| Runtime | .NET 10 (LTS), installed by Microsoft's installer (the offline zip carries it); the Windows App SDK runtime, carried in both zips |
The floor is set in WinZ3805A.csproj as
TargetPlatformMinVersion and in Package.appxmanifest
as MinVersion, both 10.0.19045.0, while the project builds against the 10.0.26100 SDK
(TargetFramework net10.0-windows10.0.26100.0). Those are different jobs: the first is the
oldest Windows the app will install and run on, the second is the API surface it compiles
against. Install.cmd says so in words on anything older, before changing anything.
The floor is what is tested (#705, 5 Oct 2026). It was Windows 10 1809 - the Windows App SDK's own floor - but nothing ever ran the application there, while every release's QA pass runs on Windows 10 22H2 and Windows 11 (
build/qa). So the declared minimum is the oldest Windows the pass covers. Server 2019 (build 17763) went with 1809. Anyone choosing a machine for this application should be on Windows 11.
Not this application, and it cannot be — WinUI 3 is Windows-only by definition, which is why cross-platform support is a non-goal in §3 rather than a backlog item. A separate project, smartclock-monitor, reimplements the same thing in Python and Qt for the same receivers. It shares no code with this one, because none of it can be shared; what it does share is the specification, the captured parser fixtures and the colour derivation, all carried across verbatim.
It is early — at the time of writing it is scaffolding, not an application. What it is building from is docs/porting-to-python-qt.md, which is also the honest account of what such a port costs and what it cannot carry across.
A note on Windows App SDK 2.x. From 2.0 the minimum is no longer one number for the whole SDK;
it varies by component, and the highest in the restored 2.3.1 tree is
Microsoft.Windows.AI.MachineLearning at Windows 10 1903 (18362). The 22H2 floor is above every
one of them, so no component constrains it. The csproj still references the refactored
Microsoft.Windows.AI.MachineLearning directly only to exclude its runtime and native assets, which
this application does not use; its comment explains why Microsoft.WindowsAppSDK.ML itself cannot be
excluded - a framework-dependent build refuses to restore without it.
It is confirmed by running the application on Windows 10 22H2: QA-Win10, a VM provisioned from Microsoft's 22H2 media, runs every scenario of every release's QA pass.
Sources: Windows App SDK and supported Windows releases, Windows App SDK 2.0 release notes.
Download the latest release,
unblock the zip, extract it, and double-click Install.cmd. Each release has
two zips:
WinZ3805A-<version>-x64.zip, for a machine with an internet connection. If .NET 10 is not installed,Install.cmdopens Microsoft's download page for it.WinZ3805A-<version>-x64-offline.zip, for a machine without one. It adds Microsoft's own .NET 10 Runtime installer, whichInstall.cmdruns when .NET 10 is missing.
Unblock before extracting — right-click the zip → Properties → Unblock. Windows marks anything downloaded from the internet and the mark survives extraction. Skip it and Windows 11 asks whether to run
Install.cmd, saying the publisher could not be verified; choose Run and it carries on.
Both zips carry the signed package, its certificate and the x64 Windows App
Runtime. The package carries no .NET runtime of its own (§6.3). .NET 10 is
installed by Microsoft's installer and kept patched by Microsoft Update, which
it could not do for a runtime inside the package (#586, #588). Windows applies
those patches only when Receive updates for other Microsoft products is on,
and Install.cmd says whether it is. So a bench machine with neither Visual
Studio nor an internet connection installs and runs from the offline zip.
The package is signed with a self-signed certificate (build/devcert.pfx,
generated from the manifest on first use so its subject cannot drift from the
declared publisher), so the person installing trusts it once: Install.cmd
asks for administrator permission for exactly that step — adding the
certificate to Trusted People and nothing wider — and the
README in the zip explains what that does and does
not grant before asking. In the offline zip, the same single prompt also
installs .NET. Every release publishes the certificate thumbprint and each zip's
SHA-256, so both can be checked against what Windows shows you.
Coming from 1.3.0 or earlier? Those releases were signed under a misspelling
of the company's name, corrected to The Schnauzer Group LLC in 1.3.1. The
publisher is part of the package's identity, so 1.3.1 cannot upgrade the old copy
in place. Install.cmd handles that (#590). It saves the old copy's data to
Documents, moves the data into the new copy, then removes the old copy and the
certificate earlier releases were signed with. On Windows 10, which will not
install the two side by side, it saves the data and removes the old copy
before installing, then moves the data in (#617). That certificate is removed in the
same single administrator prompt, even if the old app was already uninstalled by
hand, since removing an app never removes its certificate.
Installed, but it does not start? If Install.cmd reports that WinZ3805A did not
stay open, docs/diagnose-start.md gives you a script to
download. It starts the app once and records how it ended, which the installer
cannot see (#614).
Uninstall from Settings › Apps. To remove everything any release has put on
a machine, including the certificates, the installer's logs and the copies it saved
to Documents, follow docs/remove-winz3805a.md. It
gives you a script to download and walks through each step (#618). It saves the
history first, and it never removes the shared Windows App Runtime or .NET.
build/Uninstall-Sideload.ps1 is the developer's
version: it runs from a clone and puts a test machine back to clean.
To build the installer yourself instead,
build/New-SideloadPackage.ps1 produces the
same dist\WinZ3805A-<version>-x64.zip from a clone.
Releases are built by .github/workflows/release.yml
on a v* tag — it runs the gates and the tests, builds and signs, then attaches
the zip with its thumbprint and checksum to a draft release.
The tag does not set the version. Package.appxmanifest does, and the
workflow refuses to build when the two disagree rather than restamping either:
§6.3 makes package identity effectively permanent, and a tag that could silently
change it would move that decision into a git command nobody reviews. Bump the
manifest in a pull request, merge, then tag the merge:
# after the version bump is on main
git tag v1.0.1 && git push origin v1.0.1The tag makes a draft release, which nobody but the repository's owner can see. The QA pass
and the soak run on that draft's own zips, and the draft is published only on the go, so what
anyone downloads is exactly what passed (#743). The whole sequence - merge and tag, the QA pass,
the soak, the QA-run issue, then on the go the publish and a check that the public zips are the
tested ones - is build\qa\Invoke-Release.ps1; see
build/qa/README.md.
Signing on the runner needs SIGNING_PFX_BASE64 and SIGNING_PFX_PASSWORD as
repository secrets; build/New-SigningSecrets.ps1
sets both from the local PFX and explains what rotating the key costs. Use the
workflow's Run workflow button for a dry run — it builds and signs and
attaches the zip as an artifact without publishing anything.
- .NET 10 SDK (LTS) — the exact version is pinned in
global.json;
rollForward: latestFeatureaccepts a newer patch or feature band. - Visual Studio 2026 with the .NET desktop development workload and the Windows App SDK extension. Windows App SDK 2.3.1 itself is restored from NuGet rather than installed separately.
- A machine meeting the floors in Supported platforms above. For building specifically, the app project needs the 10.0.26100 Windows SDK.
- The Windows App Development CLI (
winapp), to run the packaged app, sign the sideload package and photograph the guide:winget install Microsoft.WinAppCli(0.7.0 on the bench machine, 29 Sep 2026). - Developer Mode on (Windows Settings → System → For developers), which registering a development package needs.
This section owns the build, run and test commands; the copy in CLAUDE.md is
for agents and follows this one. MSBuild is not on PATH by default, and where it
lives depends on which Visual Studio edition is installed — Enterprise,
Professional, Community and Build Tools each put it somewhere different, and
Build Tools sits under Program Files (x86). So ask vswhere rather than
writing a path down:
$vswhere = "${env:ProgramFiles(x86)}\Microsoft Visual Studio\Installer\vswhere.exe"
$msb = & $vswhere -latest -products "*" -requires Microsoft.Component.MSBuild `
-find "MSBuild\**\Bin\MSBuild.exe" | Select-Object -First 1
& $msb WinZ3805A.sln -t:Restore -p:Configuration=Debug -p:Platform=x64
& $msb WinZ3805A.sln -t:Build -p:Configuration=Debug -p:Platform=x64Prefer MSBuild over dotnet build locally. Both work, but dotnet build
does not surface XAML compiler diagnostics: a malformed .xaml fails with no
indication of which file or what is wrong, while MSBuild reports it correctly.
This repository is XAML-heavy by design, so the difference matters in practice.
(CI uses dotnet build regardless — the hosted runner's Visual Studio MSBuild is
too old to load net10.0 projects. A XAML failure in CI may therefore not name
the file; reproduce it locally with MSBuild.)
Restore is per-platform, because the runtime identifier — and so the assets file
— differs between them. The valid combinations are Debug and Release
configurations against the x64 platform, which is the only one: there is no
AnyCPU (the Windows App SDK is native), no x86 and no ARM64. See §6.1.
TreatWarningsAsErrors and EnforceCodeStyleInBuild are both on, so a clean
build produces zero warnings and a code-style violation is a build error rather
than an editor suggestion.
build/Invoke-Wack.ps1 builds the Release MSIX and
runs the Windows App Certification Kit over it. It must run elevated, and it
takes over the desktop for ten to twenty minutes while the kit installs,
drives, and uninstalls the package.
The app is a single-project MSIX and needs package identity to launch, so run it
through winapp rather than by starting the .exe directly. Point it at the
project, not at a build-output folder:
winapp run src\WinZ3805A\WinZ3805A.csproj --arch x64 --detachGiven the project, winapp builds it and registers a complete package layout
under bin\Debug\…\win-x64\AppX, assets and fonts included, then launches it.
The application's data is kept between runs (--clean removes it). Given a
build-output folder instead, which this section said until 29 Sep 2026 (#556),
it builds the layout from that folder alone, and the build output carries no
Assets. The package then registers without its icons or its monospaced font,
and the taskbar shows a grey placeholder. Exit a running copy first.
The transport, parser, and command catalog live in a library with no UI references, so the tests run headlessly and the plain SDK is enough here:
dotnet test tests\WinZ3805A.Tests\WinZ3805A.Tests.csprojFixtures are status screens captured from real hardware.
tests/WinZ3805A.Tests/Fixtures/README.md
records their provenance, which tests assert against each, and which receiver
state is still missing; captures from
build/Capture-Fixtures.ps1 land in
Fixtures/captured/, where
the tests read them in place.
What neither the tests nor the gates can reach — anything that needs a person, a receiver, or a machine setting — is in docs/manual-qa.md, the release checklist.
Fifteen acceptance criteria — design-system, accessibility, object lifetime
and safety — are enforced by script over the source rather than by review.
Three more gates check things the source is not: two read the documents
(that every link and section reference resolves, and that every option in the
interface is named in the user's guide) and one reads the pull request (that
no closing keyword closes an issue nobody meant to close). All are
dependency-free and answer in seconds, which makes them the fastest local check
available; .github/workflows/ci.yml runs them in
dependency-free jobs of their own alongside the build, so a regression fails in
seconds instead of after a full build. Five further scripts there self-test the
tooling — the fixture, talker, UCCM and UCCM-transitions capture harnesses
and the soak's arithmetic — rather than gating the source. The full list, with what each guards
and why it exists, is in CLAUDE.md; the two below are the ones to
know first:
pwsh build/Test-NoHexLiterals.ps1 # no hex colour literals outside Themes/Colors.xaml
pwsh build/Test-NoBlockedCommands.ps1 # §8.4's exclusions appear nowhere but their one fileRun them all before pushing.
docs/requirements.md the specification
docs/ the other project documents — listed under Documentation below
src/WinZ3805A/ WinUI 3 app, single-project MSIX
src/WinZ3805A.Device/ class library — no UI references
tests/WinZ3805A.Tests/ xUnit, with Fixtures/ for captured SmartClock status screens, Nmea/Captures/ for captured talker cycles and Uccm/Captures/ for captured UCCM sittings
tools/NmeaSimulator/ the NMEA 0183 talker the tests and the tutorial run against
tools/UccmSimulator/ a UCCM module that is not one — the shapes the driver was written against, before hardware
tools/SmartClockSimulator/ a Z3805A that is not one — its screens match the captures byte for byte, and its states and faults come on demand
build/ the CI gate scripts and their inputs (palette/, fluent-stock-colours.txt), the three capture harnesses, the UCCM transitions watcher, the soak watcher, the two guide-image capture scripts, the asset generator, and the sideload, signing and WACK packaging scripts
.github/workflows/ci.yml the gates in their own jobs, alongside the Debug and Release x64 builds and the tests
The Device library has zero dependency on Microsoft.UI.*. All parsing,
command classification, and transport lives there, which is what makes the
highest-risk logic testable without a UI.
WinZ3805A talks to the HP/Symmetricom SmartClock family, but every piece of
device-specific knowledge sits behind one interface — IReceiverDriver — so
supporting another GPS-disciplined oscillator means writing a driver, not
modifying the application. What the receiver says — its *IDN? answer, or for a
talker the sentences it sends unprompted — chooses the driver at every connect,
the poller sweeps whatever the driver's plan says to sweep,
and a registered driver is one line in the composition root.
The complete walkthrough is
docs/adding-a-receiver.md: the architecture, the
contract member by member, the §8 safety obligations that bind third-party
drivers, the development process in order, the testing story (a fictional
second family in the test project runs the real connect and poll paths), and an
honest map of what a driver gets you today versus what is still written in the
SmartClock dialect. docs/tutorial-nmea-driver.md
is the walkthrough followed to the end for the second family that ships, an
NMEA 0183 talker — the opposite shape to the SmartClock, which is why it was
chosen — with a simulator under tools/ so it can be followed with nothing on
the desk. The third family, the UCCM, is the walkthrough followed with the
hardest part missing: it was written before any such module existed to test
against, and what that cost — and what a capture then settled — is recorded in
the captures' README.
- docs/requirements.md is the specification. It is
the authority on behaviour, and the
§references in issues, commit messages, and code comments resolve against it. Where anything else disagrees with it, the document wins. - CLAUDE.md carries the working conventions for the repository, including every CI gate with what it guards and why it exists.
One fact, one owner. The table says which document owns which kind of fact. Every other document links to the owner, or — where a reader will not follow a link, as in the user's guide or the sideload README — restates it in words without the number. Where this README restates something (the build commands, the platform floor, the x64 decision), the owner is named beside it.
| Kind of fact | Owner |
|---|---|
| What the application does and every figure in it — timeouts, cadences, baud rates, sizes, thresholds, the status model, the safety tiers | docs/requirements.md, by § |
| Platform, repository layout, naming and packaging decisions | §6 of the specification |
| Design tokens, components, motion and the accessibility criteria | §9 of the specification |
| How to build, run and test on this machine | this README, Building from source |
| The CI gates — what each guards and why it exists — and the agent conventions | CLAUDE.md |
| The receiver contract and how to add a driver | docs/adding-a-receiver.md; the tutorial is that guide followed once, and links rather than restates |
| What the user sees and presses | docs/how-to-use.md, in the user's words; the behaviour behind it is the specification's |
| Which checks need a person, a receiver or a machine setting | docs/manual-qa.md |
| Which fixtures exist and which test reads each | tests/WinZ3805A.Tests/Fixtures/README.md; their provenance is captured/capture-log.md |
| What ships in the package, and under which licence | THIRD-PARTY-NOTICES.md, regenerated from the project files |
| The privacy position | docs/privacy.md |
| Where this stands against the incumbent tool | docs/lady-heather-comparison.md |
The rest of docs/, and the other documents worth knowing about:
- docs/how-to-use.md — the user's guide: every window and control, every keyboard shortcut, and how to get the window back from the notification area, with screenshots from the running application.
- docs/adding-a-receiver.md — the driver author's guide, summarised in Adding a receiver above.
- docs/tutorial-nmea-driver.md — that guide followed to the end for a real family, an NMEA 0183 talker, with the simulator that stands in for one and every finding along the way.
- docs/manual-qa.md — the manual QA checklist: the checks that need a person, a receiver, or a machine setting.
- docs/lady-heather-comparison.md — where the application stands against Lady Heather, the incumbent tool for this family.
- docs/nmea-against-lady-heather.md — the narrower comparison of the two NMEA implementations, sentence by sentence, which reaches a different kind of conclusion from the one above.
- docs/privacy.md — the privacy policy: the application collects nothing and transmits nothing.
- docs/index.md — the front page of a GitHub Pages site that would publish the policy and the user's guide, and nothing else. Pages is not enabled.
- docs/porting-to-python-qt.md — a work plan for porting this application to Python and Qt, so it runs on Linux. Written to be handed to someone who has not seen this repository; it is being followed in smartclock-monitor.
- tests/WinZ3805A.Tests/Fixtures/README.md — provenance of the captured status screens the parser is tested against, with Nmea/Captures/ and Uccm/Captures/ doing the same for the other two families.
- THIRD-PARTY-NOTICES.md — the third-party components the application ships with, and the trademark position.
WinZ3805A is the repository, solution, root namespace, and MSIX package
identity name. The display name is deliberately a separate thing that can
change at any time, so it is read from the package manifest at runtime and never
hard-coded (§6.3).
The name contains a model designation rather than a company mark. This project is not affiliated with, endorsed by, or sponsored by HP, Hewlett-Packard, Agilent, Keysight, or Symmetricom; those names appear here only to describe the hardware the application talks to. §6.3 owns that position and its two hedges; THIRD-PARTY-NOTICES.md is the notice that ships.
MIT. Third-party components and trademarks are listed in THIRD-PARTY-NOTICES.md.