Since we wrote Receiving QSOs over the network from N1MM one year ago, the system has been surprisingly stable in reception of new QSOs from N1MM. Our IRC channel has enjoyed access to fresh QSO statistics during busy contests.
LB3YH Arne has this fall put up a Raspberry Pi-based system for displaying various kinds of information on screens we have mounted at our club locales. It was tempting to put up some live QSO statistics on these screens, but the local file-based nature of the database put too strict limitations to what we were able to do without being forced to do terrible workarounds with dire consequences.
It was therefore time to improve on the reception system, and move the database from the file-based sqlite database to a more wholesome postgresql database on a central Samfundet server. We’ve put the full project up at GitHub.
Database
While migrating, we also wanted to do some improvements to the database setup itself. We wanted to base it on two main principles.
1. The full edit history in the database should be retained.
(Heavily inspired by/copied/straight up stolen from an SQL database setup for electronic logging made by LA6YKA somewhere in 2008-2011.)
First, the unique key of a record is specified to be the fields (qsoid, modified)
, where qsoid
is a unique ID and modified
is the timestamp for when the QSO last was modified. Instead of deleting or updating existing records, we let updates and deletes insert new records with the same qsoid
and more recent modified
timestamp. Only new QSOs use new, unique qsoid
s. This retains the full history. A “valid” QSO is then the entry among multiple, identical qsoid
s that has the most recent modified time. Implementation of such complicated rules is tempting to do in the client, which brings us to the second principle:
2. The database should by itself specify a user-friendly API.
(After ARK-monday discussions with LA2TSA.)
A database user, or a client using the database, should not have to do any special tricks to insert, update or delete QSOs in the database, or even know how the underlying table works or that it in reality can only handle INSERTs. Everything should be handled automatically when doing SELECT, INSERT, UPDATE and DELETE.
To achieve this, a view qsos
was created, which functions as a metatable:
- SELECT from
qsos
: Groups the entries inqsos_raw
by theirqsoid
s, and selects only the entry with the most recentmodified
field, with blank entries removed. - INSERT into
qsos
: Triggers an INSERT intoqsos_raw
, with a new uniqueqsoid
and the current time asmodified
. - UPDATE of
qsos
: Triggers an INSERT intoqsos_raw
, with the sameqsoid
and the current time asmodified
. - DELETE from
qsos
: Triggers an INSERT intoqsos_raw
, with the sameqsoid
, the current time asmodified
and all fields otherwise blank.
The view qsos
has the expected behavior when deleting, inserting and updating QSOs, while qsos_raw
shows the full edit history. This is probably better illustrated by showing an example:
Example: INSERT, DELETE and UPDATE on qsos
To the left is the underlying table qsos_raw
shown, into which entries are inserted on the various operations, while the view qsos
is shown to the right.
Run an INSERT statement on qsos
:
INSERT INTO qsos (timestamp, call, operator) VALUES ('2019-08-07 13:30', 'LA3WUA', 'LA9SSA');
qsos_raw
then contain the inserted QSO along with the previous QSO. qsos
shows the same information.
Run an update of an existing QSO:
UPDATE qsos SET call='LB7RH' WHERE qsoid = 2;
qsos_raw
then still contains the previous version of the QSO, but also a new version with a more recent modified timestamp. qsos
shows only the updated version, emulating a real UPDATE.
Run a delete on an existing QSO:
DELETE FROM qsos WHERE qsoid=2;
All previous versions are retained, but a new, blank record is inserted. qsos
does not show the deleted QSO, emulating a real DELETE.
A client implementation don’t have to care about the underlying behavior of the database, since operations on qsos
trigger the correct behavior on qsos_raw
. All history is retained, automatically. Finally, a user can thus have access only to INSERT operations on the table qsos_raw
, and can’t by mistake eradicate qsos_raw
by a wrongly implemented UPDATE or DELETE, since only INSERT is allowed on this table.
The sql schema can be found here.
The qso_raw
table otherwise contains more or less the same fields as in N1MM’s sqlite database, and we have not put much consideration into the design here. Further development and cleaning here might make the setup suitable for more than just reception of N1MM contacts in the future, for example by making use of more inspiration from the previously mentioned SQL database setup developed by LA6YKA.
N1MM receiver
We’ve mostly outlined the receiver in the previous blog post, but in short, N1MM can be set up to send XML messages containing log changes to a UDP port whenever new contacts are logged, edited or deleted. The documentation apparently can be interpreted as follows:
- New contact: N1MM sends a
contactinfo
message, containing new QSO information. - Delete contact: N1MM sends a
contactdelete
message, containing enough information to identify the QSO to be deleted. - Edit contact: N1MM sends a
contactdelete
message to identify the QSO, and then acontactreplace
message to replace with new information.
This works very well for new contacts and contacts to be deleted, but we’ve found that this is not necessarily the behavior for editing of contacts. When contacts are edited, there are actually three cases.
First case: Timestamp and/or call is edited. N1MM uses (timestamp, call)
as key, so that these are unique identificators. The QSO has to be looked up based on information in the contactdelete
message, which is what the contactdelete
message is for.
Second case: The other fields are edited. contactreplace
and contactdelete
contain the same call
and timestamp
information. For consistency, we can look up the QSO to be updated from the contactdelete
message.
Third case: Everything breaks down! contactdelete
contains nonsense information, and the QSO has to be looked up based on information from contactreplace
alone. This appears to happen randomly.
In addition, we always have to delete the QSO on contactdelete
, since any contactdelete
might be an actual contactdelete
and not just a message preceding contactreplace
. Admittedly, to avoid unnecessary deletion one could also have used some kind of queuing behavior, and check two subsequent messages after some delay. However, this is asking for a race condition if contactdelete
is not immediately followed by contactreplace
within the assumed time. To be on the safe side, we always delete the QSO regardless. Luckily, we have a QSO database which retains all edit history, making a deletion undo not necessarily effortless and elegant, but at least possible.
With contactinfo
and contactdelete
(without subsequent contactreplace
) messages as in the list above, the contactreplace
case changes to the following:
- Edit contact: N1MM sends a
contactdelete
message. Attempt to delete the QSO. Acontactreplace
message arrives. Undo deletion of the previously deleted QSO. Then attempt to look up theqsoid
of the contact to edit: Any combination might be valid: (timestamp
fromcontactdelete
,call
fromcontactdelete
), (timestamp
fromcontactdelete
,call
fromcontactreplace
), (timestamp
fromcontactreplace
,call
fromcontactdelete
), (timestamp
fromcontactreplace
,call
fromcontactreplace
). Then edit the corresponding contact.
Except for the last shenanigan, the N1MM receiver is mostly straight forward. We’ve put the Python script for our N1MM receiver here.
0 Comments
1 Pingback