The EveryHam Amateur Radio Contest Website is Online!

The homepage for the EveryHam Amateur Radio Contest is now live at EveryHam.org!

A trial contest was run on 18 April, thanks to the help of KD3BTG, M0CUV, N3VEM, N3CAN, and K3QB. I would consider the trial contest to be a success; it helped expose some bugs my ADIF parsing script and taught me some important lessons.

In addition to that announcement, I also wanted to share my thoughts and learnings on both designing the website itself, and on the code behind the scenes.

Website Design

Just how the contest itself was designed to be friendly towards the average ham, I wanted to take a similar approach to the web design.

  • The main page immediately shows the most recent contest, how long submissions are accepted for, and when the next contest will be held.
  • Rules have their own page on the site, no need to download a PDF.
  • Contest logs, in the standard ADIF format, are uploaded right on the website. No special forms or emails.
  • A dedicated page on the site provides instructions on how to export an ADIF file from several popular logging programs (currently QRZ and Wavelog) for operators who are new to the process.
  • When a station submits their log, they immediately see their score and their callsign appears in the leaderboards. No need to wonder if they submitted it correctly or to the right email address.
  • If a station accidentally submits a few too many QSOs the site still happily accepts the log and just drops the extra QSOs1. If they accidentally submit too few, they can just upload a new log and their score will be updated.

This was done in an effort to make the contest easy and accessible, even to hams who have never participated in a contest before.

Behind the Scenes

I initiated intended to create a static site using Hugo (like this blog) which would get updated after each contest period with the results. My initial idea for a workflow would have gone something like this:

  1. Station emails their log to me
  2. I save the adif to a directory
  3. At the end of the contest submission window, I run a script to ingest all the logs and update static site files with the scores.

While this approach is very efficient from a server resource standpoint, it requires manual action (ew) and lacks any interactivity.

Instead, I decided to take a more dynamic and user-friendly approach, and scripted out the entire website in perl using Dancer2. Static assets are cached and served by my reverse proxy, but everything else is generated on-the-fly. While I was already familiar with perl and html, this was my first time using Dancer2 and my first time creating a dynamic website from scratch. All of my past websites have either been static (like this blog) or used an existing publishing application.

ADIF Files (Nerd Alert!)

My ADIF parser was initially based on a script I found online. I figured I could re-use a lot of its logic to save myself some time. I eventually learned that this parser made a couple interestingly choices that would end up biting me in the end…

First, the parsing script I found started by skipping the ADIF header by seeking the <eoh> tag. Second, it discarded the ADIF value length and took whatever followed a field’s closing > as the value.

These both seemed like reasonable decisions at the time, so I copied this logic into my script. I did not bother to read the ADIF specs.

Everything worked fine in my private testing, but the very first log uploaded during the public test immediately failed to score correctly. Distance was calculated correctly, but no multipliers were applied. I had not written my script to save a copy of the uploaded log, however, so I had to review the DB entries and try to work backwards to determine what had failed. It soon became apparent what caused the issue: each of the fields had additional whitespace at the end2. The whitespace prevented the scoring logic from correctly assigning a scoring multiplier, causing it to fallback on the 1x default.

My initial reaction was to just wrap the values in a trim() function to account for the obviously “wrong” whitespace in this ADIF file, but what if I was wrong and the whitespace was ADIF compliant? Might there be other possibilities that I would need to account for as well?
Time to actually read the specs, I guess…

Oh my.

It is important for the programmer importing ADIF data to note that any number of characters of any value may follow the actual data in a field[…] There is nothing in the specifications to prevent an exporter from placing a comment after the actual data.

Yikes. Not only was that extra whitespace completely in compliance with the ADIF spec, but there could be almost ANYTHING following the data3!

So, while <CALL:5>CH3AP is most typical of an ADIF field,
<CALL:5:c>CH3AP This ham owes me $5! is still technically valid.

Instead of the simple my ($key, $val) = split('>', $_) from the original script, I realized that I needed something a bit more clever.
I eventually settled on this:

my ($key, $length, $val) = $_ =~ /(\w+):(\d+)(?::\w+)?>(.+)/;
trim(substr($val, 0, $length)).

Instead of discarding the length data, this uses that data as intended to identify the significant portion of the field’s value. I kept the trim() for good measure, though.

In practice, it first breaks up the entry like this:

Key Length Type Value
CALL 5 c CH3AP This ham owes me $5!

Then uses the Length to determine the important part of the value.

1 2 3 4 5 Ignore everything else
C H 3 A P This ham owes me $5!

As far as I can tell, this should accurately account for any unusual ADIF entries, and is presumably exactly why the length data exists in the first place. After applying that fix, the log in question parsed correctly and everything was good.

…Until two submissions later when another log failed to parse at all. Luckily, I now had my script saving a copy of the uploaded file, at least, so I was able to review exactly what was submitted. This log was a hand-typed file and contained only the minimum fields required by the contest (calls, grids, band, mode).
It was a fully ADIF-compliant file, yet my script failed to parse a single entry. Ugh.

This was also a result of me referencing a script I found instead of fully reading the ADIF spec. You may recall that the script I was using for reference skipped the header by seeking out the “end of header” tag.
Well, according to the ADIF spec, the header itself is entirely optional. Since this log did not contain a header, it didn’t include the <eoh> tag, and since it didn’t have an <eoh> tag, my script just kept skipping lines until it hit the end of the file, recording 0 QSOs.
After figuring out another fix (and actually reading the entire ADIF spec), I resubmitted the log and it was parsed and scored correctly.

Summary

I’m not sure if the website’s style is finalized, but I think I have some solid design principles to guide it. I addition to the design principles of being a friendly introduction into Contesting, I’m also trying to remain conscious of traditional web accessibility standards by using semantic HTML and higher-contrast colors.

Building the site has been a fun, but challenging experience. I enjoy coding, but don’t often find excuses to do so. Creating the EveryHam site has provided a great opportunity to work my brain, play with some perl, and learn new things.

The trial contest uncovered bugs in my code, which was one of the key purposes of the trial in the first place. In that sense, I would consider this a success. In retrospect, those bugs could have been caught beforehand had I actually read the specs of what I was trying to parse instead of just following someone else’s process4.

The next EveryHam contest is scheduled for 09 May 2026, and for the second Saturday of each month going forwards.
If you’re a ham who is interested in contesting but wants a more friendly and casual contesting experience, give it a try!

Until next time, Netizens.


  1. Within reason. Excessively large logs will be rejected by the server due to bandwidth limitations. ↩︎

  2. When comparing text in very a literal context, "SSB" is not the same as "SSB ", nor " SSB"↩︎

  3. I also learned that there is an optional character that can define the type of data contained in the value, too. ↩︎

  4. I think this goes to show that the “blind-leading-the-blind” problem, heavily associated with LLMs these days, really isn’t unique or new. ↩︎