0
56 | var meq1 = "^(" + C + ")?" + V + C + "(" + V + ")?$"; // [C]VC[V] is m=1
57 | var mgr1 = "^(" + C + ")?" + V + C + V + C; // [C]VCVC... is m>1
58 | var s_v = "^(" + C + ")?" + v; // vowel in stem
59 |
60 | this.stemWord = function (w) {
61 | var stem;
62 | var suffix;
63 | var firstch;
64 | var origword = w;
65 |
66 | if (w.length < 3)
67 | return w;
68 |
69 | var re;
70 | var re2;
71 | var re3;
72 | var re4;
73 |
74 | firstch = w.substr(0,1);
75 | if (firstch == "y")
76 | w = firstch.toUpperCase() + w.substr(1);
77 |
78 | // Step 1a
79 | re = /^(.+?)(ss|i)es$/;
80 | re2 = /^(.+?)([^s])s$/;
81 |
82 | if (re.test(w))
83 | w = w.replace(re,"$1$2");
84 | else if (re2.test(w))
85 | w = w.replace(re2,"$1$2");
86 |
87 | // Step 1b
88 | re = /^(.+?)eed$/;
89 | re2 = /^(.+?)(ed|ing)$/;
90 | if (re.test(w)) {
91 | var fp = re.exec(w);
92 | re = new RegExp(mgr0);
93 | if (re.test(fp[1])) {
94 | re = /.$/;
95 | w = w.replace(re,"");
96 | }
97 | }
98 | else if (re2.test(w)) {
99 | var fp = re2.exec(w);
100 | stem = fp[1];
101 | re2 = new RegExp(s_v);
102 | if (re2.test(stem)) {
103 | w = stem;
104 | re2 = /(at|bl|iz)$/;
105 | re3 = new RegExp("([^aeiouylsz])\\1$");
106 | re4 = new RegExp("^" + C + v + "[^aeiouwxy]$");
107 | if (re2.test(w))
108 | w = w + "e";
109 | else if (re3.test(w)) {
110 | re = /.$/;
111 | w = w.replace(re,"");
112 | }
113 | else if (re4.test(w))
114 | w = w + "e";
115 | }
116 | }
117 |
118 | // Step 1c
119 | re = /^(.+?)y$/;
120 | if (re.test(w)) {
121 | var fp = re.exec(w);
122 | stem = fp[1];
123 | re = new RegExp(s_v);
124 | if (re.test(stem))
125 | w = stem + "i";
126 | }
127 |
128 | // Step 2
129 | re = /^(.+?)(ational|tional|enci|anci|izer|bli|alli|entli|eli|ousli|ization|ation|ator|alism|iveness|fulness|ousness|aliti|iviti|biliti|logi)$/;
130 | if (re.test(w)) {
131 | var fp = re.exec(w);
132 | stem = fp[1];
133 | suffix = fp[2];
134 | re = new RegExp(mgr0);
135 | if (re.test(stem))
136 | w = stem + step2list[suffix];
137 | }
138 |
139 | // Step 3
140 | re = /^(.+?)(icate|ative|alize|iciti|ical|ful|ness)$/;
141 | if (re.test(w)) {
142 | var fp = re.exec(w);
143 | stem = fp[1];
144 | suffix = fp[2];
145 | re = new RegExp(mgr0);
146 | if (re.test(stem))
147 | w = stem + step3list[suffix];
148 | }
149 |
150 | // Step 4
151 | re = /^(.+?)(al|ance|ence|er|ic|able|ible|ant|ement|ment|ent|ou|ism|ate|iti|ous|ive|ize)$/;
152 | re2 = /^(.+?)(s|t)(ion)$/;
153 | if (re.test(w)) {
154 | var fp = re.exec(w);
155 | stem = fp[1];
156 | re = new RegExp(mgr1);
157 | if (re.test(stem))
158 | w = stem;
159 | }
160 | else if (re2.test(w)) {
161 | var fp = re2.exec(w);
162 | stem = fp[1] + fp[2];
163 | re2 = new RegExp(mgr1);
164 | if (re2.test(stem))
165 | w = stem;
166 | }
167 |
168 | // Step 5
169 | re = /^(.+?)e$/;
170 | if (re.test(w)) {
171 | var fp = re.exec(w);
172 | stem = fp[1];
173 | re = new RegExp(mgr1);
174 | re2 = new RegExp(meq1);
175 | re3 = new RegExp("^" + C + v + "[^aeiouwxy]$");
176 | if (re.test(stem) || (re2.test(stem) && !(re3.test(stem))))
177 | w = stem;
178 | }
179 | re = /ll$/;
180 | re2 = new RegExp(mgr1);
181 | if (re.test(w) && re2.test(w)) {
182 | re = /.$/;
183 | w = w.replace(re,"");
184 | }
185 |
186 | // and turn initial Y back to y
187 | if (firstch == "y")
188 | w = firstch.toLowerCase() + w.substr(1);
189 | return w;
190 | }
191 | }
192 |
193 |
--------------------------------------------------------------------------------
/docs/build/html/_static/minus.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rrobinett/wsprdaemon/71d90d1563fcb2c9f07d2aa53f2150326229aa63/docs/build/html/_static/minus.png
--------------------------------------------------------------------------------
/docs/build/html/_static/plus.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rrobinett/wsprdaemon/71d90d1563fcb2c9f07d2aa53f2150326229aa63/docs/build/html/_static/plus.png
--------------------------------------------------------------------------------
/docs/build/html/_static/pygments.css:
--------------------------------------------------------------------------------
1 | pre { line-height: 125%; }
2 | td.linenos .normal { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; }
3 | span.linenos { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; }
4 | td.linenos .special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; }
5 | span.linenos.special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; }
6 | .highlight .hll { background-color: #ffffcc }
7 | .highlight { background: #f8f8f8; }
8 | .highlight .c { color: #3D7B7B; font-style: italic } /* Comment */
9 | .highlight .err { border: 1px solid #F00 } /* Error */
10 | .highlight .k { color: #008000; font-weight: bold } /* Keyword */
11 | .highlight .o { color: #666 } /* Operator */
12 | .highlight .ch { color: #3D7B7B; font-style: italic } /* Comment.Hashbang */
13 | .highlight .cm { color: #3D7B7B; font-style: italic } /* Comment.Multiline */
14 | .highlight .cp { color: #9C6500 } /* Comment.Preproc */
15 | .highlight .cpf { color: #3D7B7B; font-style: italic } /* Comment.PreprocFile */
16 | .highlight .c1 { color: #3D7B7B; font-style: italic } /* Comment.Single */
17 | .highlight .cs { color: #3D7B7B; font-style: italic } /* Comment.Special */
18 | .highlight .gd { color: #A00000 } /* Generic.Deleted */
19 | .highlight .ge { font-style: italic } /* Generic.Emph */
20 | .highlight .ges { font-weight: bold; font-style: italic } /* Generic.EmphStrong */
21 | .highlight .gr { color: #E40000 } /* Generic.Error */
22 | .highlight .gh { color: #000080; font-weight: bold } /* Generic.Heading */
23 | .highlight .gi { color: #008400 } /* Generic.Inserted */
24 | .highlight .go { color: #717171 } /* Generic.Output */
25 | .highlight .gp { color: #000080; font-weight: bold } /* Generic.Prompt */
26 | .highlight .gs { font-weight: bold } /* Generic.Strong */
27 | .highlight .gu { color: #800080; font-weight: bold } /* Generic.Subheading */
28 | .highlight .gt { color: #04D } /* Generic.Traceback */
29 | .highlight .kc { color: #008000; font-weight: bold } /* Keyword.Constant */
30 | .highlight .kd { color: #008000; font-weight: bold } /* Keyword.Declaration */
31 | .highlight .kn { color: #008000; font-weight: bold } /* Keyword.Namespace */
32 | .highlight .kp { color: #008000 } /* Keyword.Pseudo */
33 | .highlight .kr { color: #008000; font-weight: bold } /* Keyword.Reserved */
34 | .highlight .kt { color: #B00040 } /* Keyword.Type */
35 | .highlight .m { color: #666 } /* Literal.Number */
36 | .highlight .s { color: #BA2121 } /* Literal.String */
37 | .highlight .na { color: #687822 } /* Name.Attribute */
38 | .highlight .nb { color: #008000 } /* Name.Builtin */
39 | .highlight .nc { color: #00F; font-weight: bold } /* Name.Class */
40 | .highlight .no { color: #800 } /* Name.Constant */
41 | .highlight .nd { color: #A2F } /* Name.Decorator */
42 | .highlight .ni { color: #717171; font-weight: bold } /* Name.Entity */
43 | .highlight .ne { color: #CB3F38; font-weight: bold } /* Name.Exception */
44 | .highlight .nf { color: #00F } /* Name.Function */
45 | .highlight .nl { color: #767600 } /* Name.Label */
46 | .highlight .nn { color: #00F; font-weight: bold } /* Name.Namespace */
47 | .highlight .nt { color: #008000; font-weight: bold } /* Name.Tag */
48 | .highlight .nv { color: #19177C } /* Name.Variable */
49 | .highlight .ow { color: #A2F; font-weight: bold } /* Operator.Word */
50 | .highlight .w { color: #BBB } /* Text.Whitespace */
51 | .highlight .mb { color: #666 } /* Literal.Number.Bin */
52 | .highlight .mf { color: #666 } /* Literal.Number.Float */
53 | .highlight .mh { color: #666 } /* Literal.Number.Hex */
54 | .highlight .mi { color: #666 } /* Literal.Number.Integer */
55 | .highlight .mo { color: #666 } /* Literal.Number.Oct */
56 | .highlight .sa { color: #BA2121 } /* Literal.String.Affix */
57 | .highlight .sb { color: #BA2121 } /* Literal.String.Backtick */
58 | .highlight .sc { color: #BA2121 } /* Literal.String.Char */
59 | .highlight .dl { color: #BA2121 } /* Literal.String.Delimiter */
60 | .highlight .sd { color: #BA2121; font-style: italic } /* Literal.String.Doc */
61 | .highlight .s2 { color: #BA2121 } /* Literal.String.Double */
62 | .highlight .se { color: #AA5D1F; font-weight: bold } /* Literal.String.Escape */
63 | .highlight .sh { color: #BA2121 } /* Literal.String.Heredoc */
64 | .highlight .si { color: #A45A77; font-weight: bold } /* Literal.String.Interpol */
65 | .highlight .sx { color: #008000 } /* Literal.String.Other */
66 | .highlight .sr { color: #A45A77 } /* Literal.String.Regex */
67 | .highlight .s1 { color: #BA2121 } /* Literal.String.Single */
68 | .highlight .ss { color: #19177C } /* Literal.String.Symbol */
69 | .highlight .bp { color: #008000 } /* Name.Builtin.Pseudo */
70 | .highlight .fm { color: #00F } /* Name.Function.Magic */
71 | .highlight .vc { color: #19177C } /* Name.Variable.Class */
72 | .highlight .vg { color: #19177C } /* Name.Variable.Global */
73 | .highlight .vi { color: #19177C } /* Name.Variable.Instance */
74 | .highlight .vm { color: #19177C } /* Name.Variable.Magic */
75 | .highlight .il { color: #666 } /* Literal.Number.Integer.Long */
--------------------------------------------------------------------------------
/docs/build/html/_static/sphinx_highlight.js:
--------------------------------------------------------------------------------
1 | /* Highlighting utilities for Sphinx HTML documentation. */
2 | "use strict";
3 |
4 | const SPHINX_HIGHLIGHT_ENABLED = true
5 |
6 | /**
7 | * highlight a given string on a node by wrapping it in
8 | * span elements with the given class name.
9 | */
10 | const _highlight = (node, addItems, text, className) => {
11 | if (node.nodeType === Node.TEXT_NODE) {
12 | const val = node.nodeValue;
13 | const parent = node.parentNode;
14 | const pos = val.toLowerCase().indexOf(text);
15 | if (
16 | pos >= 0 &&
17 | !parent.classList.contains(className) &&
18 | !parent.classList.contains("nohighlight")
19 | ) {
20 | let span;
21 |
22 | const closestNode = parent.closest("body, svg, foreignObject");
23 | const isInSVG = closestNode && closestNode.matches("svg");
24 | if (isInSVG) {
25 | span = document.createElementNS("http://www.w3.org/2000/svg", "tspan");
26 | } else {
27 | span = document.createElement("span");
28 | span.classList.add(className);
29 | }
30 |
31 | span.appendChild(document.createTextNode(val.substr(pos, text.length)));
32 | const rest = document.createTextNode(val.substr(pos + text.length));
33 | parent.insertBefore(
34 | span,
35 | parent.insertBefore(
36 | rest,
37 | node.nextSibling
38 | )
39 | );
40 | node.nodeValue = val.substr(0, pos);
41 | /* There may be more occurrences of search term in this node. So call this
42 | * function recursively on the remaining fragment.
43 | */
44 | _highlight(rest, addItems, text, className);
45 |
46 | if (isInSVG) {
47 | const rect = document.createElementNS(
48 | "http://www.w3.org/2000/svg",
49 | "rect"
50 | );
51 | const bbox = parent.getBBox();
52 | rect.x.baseVal.value = bbox.x;
53 | rect.y.baseVal.value = bbox.y;
54 | rect.width.baseVal.value = bbox.width;
55 | rect.height.baseVal.value = bbox.height;
56 | rect.setAttribute("class", className);
57 | addItems.push({ parent: parent, target: rect });
58 | }
59 | }
60 | } else if (node.matches && !node.matches("button, select, textarea")) {
61 | node.childNodes.forEach((el) => _highlight(el, addItems, text, className));
62 | }
63 | };
64 | const _highlightText = (thisNode, text, className) => {
65 | let addItems = [];
66 | _highlight(thisNode, addItems, text, className);
67 | addItems.forEach((obj) =>
68 | obj.parent.insertAdjacentElement("beforebegin", obj.target)
69 | );
70 | };
71 |
72 | /**
73 | * Small JavaScript module for the documentation.
74 | */
75 | const SphinxHighlight = {
76 |
77 | /**
78 | * highlight the search words provided in localstorage in the text
79 | */
80 | highlightSearchWords: () => {
81 | if (!SPHINX_HIGHLIGHT_ENABLED) return; // bail if no highlight
82 |
83 | // get and clear terms from localstorage
84 | const url = new URL(window.location);
85 | const highlight =
86 | localStorage.getItem("sphinx_highlight_terms")
87 | || url.searchParams.get("highlight")
88 | || "";
89 | localStorage.removeItem("sphinx_highlight_terms")
90 | url.searchParams.delete("highlight");
91 | window.history.replaceState({}, "", url);
92 |
93 | // get individual terms from highlight string
94 | const terms = highlight.toLowerCase().split(/\s+/).filter(x => x);
95 | if (terms.length === 0) return; // nothing to do
96 |
97 | // There should never be more than one element matching "div.body"
98 | const divBody = document.querySelectorAll("div.body");
99 | const body = divBody.length ? divBody[0] : document.querySelector("body");
100 | window.setTimeout(() => {
101 | terms.forEach((term) => _highlightText(body, term, "highlighted"));
102 | }, 10);
103 |
104 | const searchBox = document.getElementById("searchbox");
105 | if (searchBox === null) return;
106 | searchBox.appendChild(
107 | document
108 | .createRange()
109 | .createContextualFragment(
110 | '' +
111 | '' +
112 | _("Hide Search Matches") +
113 | "
"
114 | )
115 | );
116 | },
117 |
118 | /**
119 | * helper function to hide the search marks again
120 | */
121 | hideSearchWords: () => {
122 | document
123 | .querySelectorAll("#searchbox .highlight-link")
124 | .forEach((el) => el.remove());
125 | document
126 | .querySelectorAll("span.highlighted")
127 | .forEach((el) => el.classList.remove("highlighted"));
128 | localStorage.removeItem("sphinx_highlight_terms")
129 | },
130 |
131 | initEscapeListener: () => {
132 | // only install a listener if it is really needed
133 | if (!DOCUMENTATION_OPTIONS.ENABLE_SEARCH_SHORTCUTS) return;
134 |
135 | document.addEventListener("keydown", (event) => {
136 | // bail for input elements
137 | if (BLACKLISTED_KEY_CONTROL_ELEMENTS.has(document.activeElement.tagName)) return;
138 | // bail with special keys
139 | if (event.shiftKey || event.altKey || event.ctrlKey || event.metaKey) return;
140 | if (DOCUMENTATION_OPTIONS.ENABLE_SEARCH_SHORTCUTS && (event.key === "Escape")) {
141 | SphinxHighlight.hideSearchWords();
142 | event.preventDefault();
143 | }
144 | });
145 | },
146 | };
147 |
148 | _ready(() => {
149 | /* Do not call highlightSearchWords() when we are on the search page.
150 | * It will highlight words from the *previous* search query.
151 | */
152 | if (typeof Search === "undefined") SphinxHighlight.highlightSearchWords();
153 | SphinxHighlight.initEscapeListener();
154 | });
155 |
--------------------------------------------------------------------------------
/docs/build/html/objects.inv:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rrobinett/wsprdaemon/71d90d1563fcb2c9f07d2aa53f2150326229aa63/docs/build/html/objects.inv
--------------------------------------------------------------------------------
/docs/make.bat:
--------------------------------------------------------------------------------
1 | @ECHO OFF
2 |
3 | pushd %~dp0
4 |
5 | REM Command file for Sphinx documentation
6 |
7 | if "%SPHINXBUILD%" == "" (
8 | set SPHINXBUILD=sphinx-build
9 | )
10 | set SOURCEDIR=source
11 | set BUILDDIR=build
12 |
13 | %SPHINXBUILD% >NUL 2>NUL
14 | if errorlevel 9009 (
15 | echo.
16 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx
17 | echo.installed, then set the SPHINXBUILD environment variable to point
18 | echo.to the full path of the 'sphinx-build' executable. Alternatively you
19 | echo.may add the Sphinx directory to PATH.
20 | echo.
21 | echo.If you don't have Sphinx installed, grab it from
22 | echo.https://www.sphinx-doc.org/
23 | exit /b 1
24 | )
25 |
26 | if "%1" == "" goto help
27 |
28 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
29 | goto end
30 |
31 | :help
32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
33 |
34 | :end
35 | popd
36 |
--------------------------------------------------------------------------------
/docs/requirements.txt:
--------------------------------------------------------------------------------
1 | sphinx
2 | sphinx-rtd-theme
3 | myst-nb
4 |
--------------------------------------------------------------------------------
/docs/source/.DS_Store:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rrobinett/wsprdaemon/71d90d1563fcb2c9f07d2aa53f2150326229aa63/docs/source/.DS_Store
--------------------------------------------------------------------------------
/docs/source/FAQ.md:
--------------------------------------------------------------------------------
1 | # Frequently Asked Questions
2 |
3 |
4 |
--------------------------------------------------------------------------------
/docs/source/_images/.DS_Store:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rrobinett/wsprdaemon/71d90d1563fcb2c9f07d2aa53f2150326229aa63/docs/source/_images/.DS_Store
--------------------------------------------------------------------------------
/docs/source/_images/IGMP_switch.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rrobinett/wsprdaemon/71d90d1563fcb2c9f07d2aa53f2150326229aa63/docs/source/_images/IGMP_switch.png
--------------------------------------------------------------------------------
/docs/source/_images/WD-data-architecture.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rrobinett/wsprdaemon/71d90d1563fcb2c9f07d2aa53f2150326229aa63/docs/source/_images/WD-data-architecture.png
--------------------------------------------------------------------------------
/docs/source/_images/btop.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rrobinett/wsprdaemon/71d90d1563fcb2c9f07d2aa53f2150326229aa63/docs/source/_images/btop.png
--------------------------------------------------------------------------------
/docs/source/_images/pskreporter.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rrobinett/wsprdaemon/71d90d1563fcb2c9f07d2aa53f2150326229aa63/docs/source/_images/pskreporter.png
--------------------------------------------------------------------------------
/docs/source/_images/psws.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rrobinett/wsprdaemon/71d90d1563fcb2c9f07d2aa53f2150326229aa63/docs/source/_images/psws.png
--------------------------------------------------------------------------------
/docs/source/_images/wd-file-structure.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rrobinett/wsprdaemon/71d90d1563fcb2c9f07d2aa53f2150326229aa63/docs/source/_images/wd-file-structure.png
--------------------------------------------------------------------------------
/docs/source/_images/wd-functions.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rrobinett/wsprdaemon/71d90d1563fcb2c9f07d2aa53f2150326229aa63/docs/source/_images/wd-functions.png
--------------------------------------------------------------------------------
/docs/source/_images/wwv_obs.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rrobinett/wsprdaemon/71d90d1563fcb2c9f07d2aa53f2150326229aa63/docs/source/_images/wwv_obs.png
--------------------------------------------------------------------------------
/docs/source/conf.py:
--------------------------------------------------------------------------------
1 | # Configuration file for the Sphinx documentation builder.
2 | #
3 | # For the full list of built-in configuration values, see the documentation:
4 | # https://www.sphinx-doc.org/en/master/usage/configuration.html
5 |
6 | # -- Project information -----------------------------------------------------
7 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information
8 |
9 | project = 'wsprdaemon'
10 | copyright = '2025, Rob Robinett'
11 | author = 'Rob Robinett'
12 | release = '3.3.1'
13 |
14 | # -- General configuration ---------------------------------------------------
15 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration
16 |
17 | extensions = [
18 | 'myst_nb',
19 | 'sphinx.ext.autodoc',
20 | 'sphinx.ext.viewcode',
21 | 'sphinx.ext.napoleon',
22 | 'sphinx.ext.mathjax'
23 | ]
24 |
25 | myst_enable_extentions = [
26 | "amsmath", # Enables AMS-style math environments like align
27 | "html_image"
28 | ]
29 |
30 | html_theme = 'sphinx_rtd_theme'
31 |
32 | templates_path = ['_templates']
33 | exclude_patterns = []
34 |
35 | html_static_path = ['_static']
36 |
--------------------------------------------------------------------------------
/docs/source/configuration/ka9q-web.md:
--------------------------------------------------------------------------------
1 | # ka9q-web
2 |
3 | This software builds in reference to ka9q-radio and requires no specific configuration.
4 | WD typically starts it automatically. You invoke it manually, if necessary, thus:
5 | ```
6 | ka9q-web -m your-radiod-status-stream.local -p 8081 -n "callsign grid antenna" &
7 | ```
8 | where "your-radiod-status-stream" is the name specified in the [GLOBAL] of your radiod@.conf (often hf.local or hf-status.local)
9 |
10 | ## Viewing the ka9q-web spectrum display
11 |
12 | Logged in an running everything locally, direct your browser to http://localhost:8081.
13 |
14 | If managing the computer remotely using ssh, you can set up an ssh tunnel from the remote computer to your local computer like this:
15 |
16 | From your local machine, run:
17 | ```
18 | ssh -L 8081:localhost:8081 wsprdaemon@aa.bb.cc.dd
19 | ```
20 | where aa.bb.cc.dd is the ip address or name of the remote computer running ka9q-web. (Substitute another username if not running WD eponymously.)
21 |
22 | Then direct your browser to http://localhost:8081 to view ka9q-web served from the aa.bb.cc.dd remote computer.
23 | If you happen to be using port 8081 on your local computer for another purpose, simply replace the port number after -L in the command above to an unused port YYYY. Then direct your browser to http://localhost:YYYY.
24 |
25 | The port will disappear when you close the ssh session.
26 |
27 | **John Melton G0ORX** started this with a proof-of-concept version in late 2023. This adjunct to ka9q-radio displays a spectrum, waterfall, and other data from radiod. **Scott Newell N5TNL** has since improved it dramatically in collaboration with **Rob Robinett AI6VN**, **Phil Karn KA9Q**, **Glenn Elmore N6GN**, **Jim Lill WA2ZKD**, and desultory kibbitzers.
28 |
29 | - Web Server by John Melton, G0ORX (https://github.com/g0orx/ka9q-radio)
30 | - ka9q-radio by Phil Karn, KA9Q (https://github.com/ka9q/ka9q-radio)
31 | - Onion Web Framework by David Moreno (https://github.com/davidmoreno/onion)
32 | - Spectrum/Waterfall Display by Jeppe Ledet-Pedersen (https://github.com/jledet/waterfall)
33 |
--------------------------------------------------------------------------------
/docs/source/configuration/radiod@.conf/radiod@rx888-wsprdaemon.conf.md:
--------------------------------------------------------------------------------
1 | # Example radiod@rx888-wsprdaemon.conf
2 |
3 | ## minimalist setup on a single computer for wspr, wwv, ft4 and ft8.
4 |
5 | The following directs radiod to use a RX888 to present simultaneous multicast streams of 16 wspr channels, streams of 7 wwv and 3 chu channels, and streams of 9 ft4 and 11 ft8 channels.
6 |
7 | You will find more detailed descriptions of these sections in:
8 | - [global](./global.md)
9 | - [hardware](./hardware.md)
10 | - [channels](./channels.md)
11 |
12 | ---
13 |
14 | ```
15 | [global]
16 | hardware = rx888
17 | status = bee1-hf-status.local
18 | samprate = 12000
19 | mode = usb
20 | ttl = 0
21 | fft-threads = 0
22 |
23 | [rx888]
24 | device = "rx888"
25 | description = "AC0G @EM38ww dipole" # good to put callsign, gridsquare, and antenna description in here
26 | samprate = 64800000 # or 129600000
27 |
28 | [WSPR]
29 | encoding = float
30 | disable = no
31 | data = bee1-wspr-pcm.local
32 | agc=0
33 | gain=0
34 | samprate = 12000
35 | mode = usb
36 | low=1300
37 | high=1700
38 | freq = "136k000 474k200 1m836600 3m568600 3m592600 5m287200 5m364700 7m038600 10m138700 13m553900 14m095600 18m104600 21m094600 24m924600 28m124600 50m293000""
39 |
40 | [WWV-IQ]
41 | disable = no
42 | encoding=float
43 | data = bee1-wwv-iq.local
44 | agc=0
45 | gain=0
46 | samprate = 16k
47 | mode = iq
48 | freq = "60000 2m500000 5m000000 10m000000 15m000000 20m000000 25m000000 3m330000 7m850000 14m670000" ### Added the three CHU frequencies
49 |
50 | [FT8]
51 | disable = no
52 | data = ft8-pcm.local
53 | mode = usb
54 | freq = "1m840000 3m573000 5m357000 7m074000 10m136000 14m074000 18m100000 21m074000 24m915000 28m074000 50m313000"
55 |
56 | [FT4]
57 | disable = no
58 | data = ft4-pcm.local
59 | mode = usb
60 | freq = "3m575000 7m047500 10m140000 14m080000 18m104000 21m140000 24m919000 28m180000 50m318000"
61 | ```
62 |
63 |
64 |
--------------------------------------------------------------------------------
/docs/source/configuration/radiod_conf.md:
--------------------------------------------------------------------------------
1 | # Configuring ka9q-radio: radiod@.conf
2 |
3 | Likewise, the radiod@.conf, located in /etc/radio/, has lots of options.
4 |
5 | However, it boils down to setting up the following:
6 | - [global settings](./radiod@.conf/global.md)
7 | - [hardware settings](./radiod@.conf/hardware.md)
8 | - [channel settings](./radiod@.conf/channels.md)
9 |
10 | ## [Example radiod@rx888-wsprdaemon.conf](./radiod@.conf/radiod@rx888-wsprdaemon.conf.md)
11 |
--------------------------------------------------------------------------------
/docs/source/configuration/wd_conf.md:
--------------------------------------------------------------------------------
1 | # Configuring wsprdaemon: wsprdaemon.conf
2 |
3 | The template for a wsprdaemon.conf file, located in /home/wsprdaemon/wsprdaemon/, includes copious information and looks dauntingly complicated.
4 |
5 | However, it boils down to setting up the following:
6 | 1. [computer-related parameters](wsprdaemon.conf.d/computer.md)
7 | 2. [ka9q-radio parameters](wsprdaemon.conf.d/ka9q-radio.md)
8 | 3. [reporting parameters](wsprdaemon.conf.d/reporting.md)
9 | 4. [receiver definitions](wsprdaemon.conf.d/receivers.md)
10 | 5. [schedule definitions](wsprdaemon.conf.d/schedule.md)
11 |
12 | ## [Example wsprdaemon.conf](wsprdaemon.conf.d/wsprdaemon.conf.md)
--------------------------------------------------------------------------------
/docs/source/configuration/wsprdaemon.conf.d/computer.md:
--------------------------------------------------------------------------------
1 |
2 | # Computer Issues
3 |
4 | ## CPUs
5 |
6 | ### Ryzen 5560, 5700, 5800, 5825, and above
7 | Known to work running both WD and radiod.
8 |
9 | ### Intel
10 |
11 | ### Raspberry Pi (4 or 5)
12 | Can work in constrained use -- but not supporting the full bandwidth of a RX888 plus WD.
13 | Known to work with RTL-SDR, funcube dongle, AirspyR2, etc.
14 |
15 | ### Orange Pi-5
16 | Known to work running both WD and radiod if configured correctly.
17 |
18 | ## Memory
19 |
20 | ## Disk storage
21 |
22 |
--------------------------------------------------------------------------------
/docs/source/configuration/wsprdaemon.conf.d/ka9q-radio.md:
--------------------------------------------------------------------------------
1 | # Configuration with ka9q-radio and ka9q-web
2 |
3 | ## Running radiod locally or remotely
4 |
5 | ## Dependence on ka9q-radio processes
6 |
7 | ## radiod configuration file
8 |
9 | ## ka9q-web title
10 |
11 | ## code commits
12 |
13 |
14 |
--------------------------------------------------------------------------------
/docs/source/configuration/wsprdaemon.conf.d/receivers.md:
--------------------------------------------------------------------------------
1 |
2 | # Receiver definitions
3 |
4 | ## KiwiSDR
5 |
6 | ## KA9Q
7 |
8 | ```
9 | ##############################################################
10 | ### The RECEIVER_LIST() array defines the physical (KIWI_xxx or KA9Q...) and logical (MERG...) receive devices available on this server
11 | ### Each element of RECEIVER_LIST is a string with 5 space-separated fields:
12 | ### " ID(no spaces) IP:PORT or RTL:n MyCall MyGrid KiwPassword Optional SIGNAL_LEVEL_ADJUSTMENTS
13 | ### [[DEFAULT:ADJUST,]BAND_0:ADJUST[,BAND_N:ADJUST_N]...]
14 | ### A comma-separated list of BAND:ADJUST pairs
15 | ### BAND is one of 2200..10, while ADJUST is in dBs TO BE ADDED to the raw data
16 | ### So If you have a +10 dB LNA, ADJUST '-10' will LOWER the reported level so that your reports reflect the level at the input of the LNA
17 | ### DEFAULT defaults to zero and is applied to all bands not specified with a BAND:ADJUST
18 |
19 | declare RECEIVER_LIST=(
20 | "KA9Q_0 wspr-pcm.local OE3GBB/Q JN87aq NULL" ### A receiver name which starts with 'KA9Q_...' will decode wav files supplied by the KA9Q-radio multicast RTP streams
21 | ### In WD 3.1.0 WD assumes all WSPR audio streams come from a local instance of KA9Q
22 | ### which by default outputs all the WSPR audio stream on the multicast DNS address wspr-pcm.local
23 | "KA9Q_1 wspr1-pcm.local AI6VN CM88mc NULL" ### Multicast streams from remote KA9Q receivers can be sources, and not just RX-888s
24 | "KA9Q_0_WSPR_IQ wspr-iq.local AI6VN CM88mc NULL" ### Multicast IQ streams from the local RX888 + KA9Q receiver
25 | "KA9Q_0_WWV_IQ wwv-iq.local AI6VN CM88mc NULL" ### Those streams are not enabled by default in the radiod.conf file. So if you configue an IQ rx job,
26 | ### you will need to set 'disabled = no' for one or both in radiod@rx888-wsprdaemon.conf and then restart radiod
27 |
28 | "KIWI_0 10.11.12.100:8073 AI6VN CM88mc NULL"
29 | "KIWI_1 10.11.12.101:8073 AI6VN CM88mc foobar DEFAULT:-10,80:-12,30:-8,20:2,15:6" ### You can optionally adjust noise levels for the antenna factor
30 | "KIWI_2 10.11.12.102:8073 AI6VN CM88mc foobar"
31 |
32 | "MERG_K01_Q01 KIWI_0,KIWI_1,KA9Q_0,KA9Q_1 AI6VN CM88mc foobar" ### For a receiver with a name starting with "MERG", the IP field is a list of two or more 'real' receivers a defined above. For a logical MERG receiver
33 | )
34 |
35 | ```
36 |
--------------------------------------------------------------------------------
/docs/source/configuration/wsprdaemon.conf.d/reporting.md:
--------------------------------------------------------------------------------
1 |
2 | # Reporting
3 |
4 | ## WSPR Reporting
5 |
6 | ```
7 | ################### The following variables are used in normally running installations ###################
8 | SIGNAL_LEVEL_UPLOAD="noise" ### Whether and how to upload extended spots to wsprdaemon.org. WD always attempts to upload spots to wsprnet.org
9 | ### SIGNAL_LEVEL_UPLOAD="no" => (Default) Only upload spots directly to wsprnet.org
10 | ### SIGNAL_LEVEL_UPLOAD_MODE="noise" => In addition, upload extended spots and noise data to wsprdaemon.org
11 | ### SIGNAL_LEVEL_UPLOAD_MODE="proxy" => Don't directly upload spots to wsprdaemon.org. Instead, after uploading extended spots and noise data to wsprdaemon.org have it regenerate and upload those spots to wsp
12 | ### This mode minimizes the use of Internet bandwidth, but makes getting spots to wsprnet.org dependent upon the wsprdameon.org services.
13 |
14 | # If SIGNAL_LEVEL_UPLOAD in NOT "no", then you must modify SIGNAL_LEVEL_UPLOAD_ID from "AI6VN" to your call sign. SIGNAL_LEVEL_UPLOAD_ID cannot include '/
15 | SIGNAL_LEVEL_UPLOAD_ID="OE3GBB_Q" ### The name put in upload log records, the title bar of the graph, and the name used to view spots and noise at that server.
16 | # SIGNAL_LEVEL_UPLOAD_GRAPHS="yes" ### If this variable is defined as "yes" AND SIGNAL_LEVEL_UPLOAD_ID is defined, then FTP graphs of the last 24 hours to http://wsprdaemon.org/graphs/SIGNAL_LEVEL_UPLOAD_ID
17 | # SIGNAL_LEVEL_LOCAL_GRAPHS="yes" ### If this variable is defined as "yes" AND SIGNAL_LEVEL_UPLOAD_ID is defined, then make graphs visible at http://localhost/
18 | #
19 | ### Graphs default to y-axis minimum of -175 dB to maximum of -105 dB. X pixels default to 40, Y pixels default to 30. If the graph of your system isn't pleasing, you can change the graph's appearance by
20 | ### uncommenting one or more of these variables and changing their values
21 | # NOISE_GRAPHS_Y_MIN=-175
22 | # NOISE_GRAPHS_Y_MAX=-105
23 | # NOISE_GRAPHS_X_PIXEL=40
24 | # NOISE_GRAPHS_Y_PIXEL=30
25 | ```
26 |
27 | ## HamSCI -- Grape Reporting
28 |
29 | WD includes configurable support for the [HamSCI GRAPE WWV Doppler shift project](https://hamsci.org/grape). From local or remote RX888 receivers, WD can record a continuous series of one minute long 16000 sps flac-compressed IQ wav files. Soon after 00:00 UTC, WD creates a single 3.8 MB 24hour-10hz-iq.wav file which is uploaded to WD's WD1 server at grape.wsprdaemon.org. WD software on WD1 then converts the one or more time station band recordings into [Digital RF (DRF)](https://github.com/MITHaystack/digital_rf) file format and uploads those DRF files to the HamSCI server.
30 |
31 | You must create an account at https://pswsnetwork.caps.ua.edu/. This account enables you to manage your site(s) and instrument(s).
32 |
33 | After establishing your account, you create a "site" having a SITE_ID and a TOKEN for each WD instance contributing to the GRAPE project. The SITE_ID takes the form "S000NNN". You typically name the site with your callsign and a useful discriminator if you have more than one site, for instance, "AC0G_B1", "AC0G_B2". For each site, you add an "instrument" of particular type, for instance, "magnetometer" or "rx888", with an INSTRUMENT_ID. You can create multiple sites but each site has only one instrument. The SITE_ID and TOKEN function as username and password for uploading data. The SITE_ID and INSTRUMENT_ID function to identify the data in DRF.
34 |
35 | On your WD server, you then
36 | - [configure KA9Q-radio](../radiod@.conf/channels.md) to output WWV-IQ channels.
37 | - [configure WD receivers](./receivers.md) listen to those channels.
38 | - [configure a WD schedule](./schedule.md) for listening on each channel.
39 | - [configure WD reporting](./reporting.md) with GRAPE_PSWS_ID="_"
40 |
41 | Finally, enable automatic uploads of HamSCI data. For example, with SITE_ID="S000987" run:
42 | ```
43 | ssh-copy-id S000987@pswsnetwork.caps.ua.edu
44 | ```
45 | The site will respond by asking for a password. Enter the TOKEN for that site and you should get a message of success. The most common cause of failure at this point is errant copy and paste with a character missing or an added space at the beginning or end of the token string.
46 |
47 | You can check if auto login works by executing the 'wdssp' alias.
48 |
49 | Source : https://groups.io/g/wsprdaemon/message/3319 Rob Robinett Source : https://github.com/rrobinett/wsprdaemon/blob/master/wd_template.conf Source : https://groups.io/g/wsprdaemon/message/3301 Rob Robinett
50 |
51 | ## PSKReporter
52 |
53 |
--------------------------------------------------------------------------------
/docs/source/configuration/wsprdaemon.conf.d/wsprdaemon.conf.md:
--------------------------------------------------------------------------------
1 | # Example of a working wsprdaemon.conf
2 |
3 | Minimalist configuration for a stand-alone machine that listens to wspr and WWV/CHU streams from radiod (ka9q-radio), then processes and uploads the results to wsprnet, wsprdaemon.org, pskreporter, and pswsnetwork.caps.ua.edu.
4 |
5 | You will find further details on these parameters and definitions in
6 | - [Computer-related parameters](./computer.md)
7 | - [ka9q-radio/web parameters](./ka9q-radio.md)
8 | - [reporting parameters](./reporting.md)
9 | - [receiver definitions](./receivers.md)
10 | - [schedule definitions](./schedule.md)
11 |
12 | ```
13 | # 1. Computer-related parameters:
14 | # RAC setup enables WD supporters to access your machine remotely. Get a RAC # from Rob Robinett.
15 | # WD will run without this
16 | REMOTE_ACCESS_CHANNEL=27
17 | REMOTE_ACCESS_ID="AC0G-BEE1"
18 |
19 | # CPU/CORE TUNING if neccessary
20 | # the following will restrict wd processes to particular cores if necessary (e.g., Ryzen 7 - 5700 series)
21 | # WD will run without this
22 | WD_CPU_CORES="2-15"
23 | RADIOD_CPU_CORES="0-1"
24 |
25 | # 2. ka9q-radio/web parameters
26 | KA9Q_RADIO_COMMIT="main"
27 | KA9Q_RUNS_ONLY_REMOTELY="no"
28 | KA9Q_CONF_NAME="ac0g-bee1-rx888"
29 | KA9Q_WEB_COMMIT_CHECK="main"
30 | # If you don't set the title here, it will default to the description in the radiod@config file
31 | KA9Q_WEB_TITLE="AC0G_@EM38ww_Longwire"
32 |
33 | # 3. Reporting parameters
34 | # for contributing to HamSCI monitoring of WWV/CHU
35 | GRAPE_PSWS_ID="S000171_172"
36 | # for reporting to wsprdaemon.org
37 | SIGNAL_LEVEL_UPLOAD="noise"
38 | SIGNAL_LEVEL_UPLOAD_ID="AC0G_BEE1"
39 | SIGNAL_LEVEL_UPLOAD_GRAPHS="yes"
40 |
41 | # 4. Receiver definitions -- REQUIRED
42 | # two radiod receivers -- one for wspr and one for wwv
43 | declare RECEIVER_LIST=(
44 | "KA9Q_0_WSPR wspr-pcm.local AI6VN CM88mc NULL"
45 | "KA9Q_0_WWV_IQ wwv-iq.local AI6VN CM88mc NULL"
46 | )
47 |
48 | # 5. Schedule definitions -- REQUIRED
49 | # SCHEDULE
50 | declare WSPR_SCHEDULE=(
51 | "00:00 KA9Q_0_WSPR,2200,W2:F2:F5:F15:F30 KA9Q_0_WSPR,630,W2:F2:F5 KA9Q_0_WSPR,160,W2:F2:F5
52 | KA9Q_0_WSPR,80,W2:F2:F5 KA9Q_0_WSPR,80eu,W2:F2:F5 KA9Q_0_WSPR,60,W2:F2:F5
53 | KA9Q_0_WSPR,60eu,W2:F2:F5 KA9Q_0_WSPR,40,W2:F2:F5 KA9Q_0_WSPR,30,W2:F2:F5
54 | KA9Q_0_WSPR,22,W2 KA9Q_0_WSPR,20,W2:F2:F5 KA9Q_0_WSPR,17,W2:F2:F5
55 | KA9Q_0_WSPR,15,W2:F2:F5 KA9Q_0_WSPR,12,W2:F2:F5 KA9Q_0_WSPR,10,W2:F2:F5
56 |
57 | KA9Q_0_WWV_IQ,WWV_2_5,I1 KA9Q_0_WWV_IQ,WWV_5,I1 KA9Q_0_WWV_IQ,WWV_10,I1
58 | KA9Q_0_WWV_IQ,WWV_15,I1 KA9Q_0_WWV_IQ,WWV_20,I1 KA9Q_0_WWV_IQ,WWV_25,I1
59 | KA9Q_0_WWV_IQ,CHU_3,I1 KA9Q_0_WWV_IQ,CHU_7,I1 KA9Q_0_WWV_IQ,CHU_14,I1"
60 | )
61 | ```
--------------------------------------------------------------------------------
/docs/source/contributors.md:
--------------------------------------------------------------------------------
1 | # Contributors
2 |
3 | ## Creator and Mastermind:
4 |
5 | - Rob Robinett AI6VN
6 |
7 | ## Others lending a hand:
8 |
9 | - Phil Karn KA9Q
10 | - Gwyn Griffiths
11 | - Christoph Mayer
12 | - Scott Newell N5TNL
13 | - Franco Venturi K4VZ
14 | - Philip Barnard
15 | - Michael Hauan AC0G
16 |
--------------------------------------------------------------------------------
/docs/source/description/collaborations.md:
--------------------------------------------------------------------------------
1 | # Collaborations
2 |
3 | ## wsprnet.org
4 |
5 | ## HamSCI
6 |
7 | ## SuperDARN
8 |
9 | ## WSPR.rocks
10 |
11 | ## WSPR.live
12 |
13 | ## PSKReporter
14 |
15 |
16 |
--------------------------------------------------------------------------------
/docs/source/description/history.md:
--------------------------------------------------------------------------------
1 | # History
2 |
3 | WD
4 |
--------------------------------------------------------------------------------
/docs/source/description/how_it_works.md:
--------------------------------------------------------------------------------
1 | # How wsprdaemon Works
2 |
3 | Wsprdaemon (WD) runs as a Linux service to decode WSPR and FST4W spots from one or more Kiwis and/or RX888 SDRs and reliably posts them to wsprnet.org. It includes many features not found in WSJT-x, including multiple band and/or multiple receiver support. WD also records additional information about spots like doppler shift and background noise level which permit much deeper understanding of propagation conditions. For systems like the KiwiSDR which have a limited number of receive channels, schedules can be configured to switch between bands at different hours of the day or at sunrise/sunset-relative times. Spots obtained from multiple receivers on the same band ( e.g a 40M vertical and 500' Beverage ) can be merged together with only the best SNR posted to wsprnet.org. WD can be configured to create graphs of the background noise level for display locally and/or at graphs.wsprdaemon.org.
4 |
5 | After configuration, WD runs like a home appliance: it recovers on its own from power and internet outages and caches all spots and other data it gathers until wsprnet.org and/or wsprdaemon.net confirm delivery. Most of the 20+ 'top spotting' sites at http://wspr.rocks/topspotters/ are running WD, and in aggregate they report about 33% of the 7+M spots recorded each day at wsprnet.org.
6 |
7 | WD runs on almost any Debian Linux system running Ubuntu 22.04 LTS on x86. Although WD on a Pi 4 can decode 10+ bands, most sites run WD on a x86 CPU.
8 |
9 | ## Basic components
10 |
11 | - receiver integration (KiwiSDR, ka9q-radio)
12 | - wspr decoding and reporting to wsprnet
13 | - grape recording, conversion to digital_rf, and reporting to HamSCI
14 | - pskreporter
15 |
16 | 
17 |
18 | ## A running instance of wsprdaemon performs several tasks:
19 |
20 | - configuration and installation checks
21 | - preparation of recording and posting directories
22 | - "listening" functions for defined KIWI and KA9Q receivers
23 | - decoding and posting wspr and fst4w spots
24 | - recording 16 kHz I/Q around WWV and CHU broadcasts for upload to HamSCI
25 | - monitoring and logging results and errors
26 |
27 | 
28 |
29 | ## What will make it not start or then stop working?
30 |
31 | - data stream not defined -- wd requires at least one receiver (KIWI or KA9Q)
32 | - schedule not defined -- wd requires a schedule definition
33 | - wd requires a working hardware radio (KiwiSDR or RX888) with proper configuration
34 | - [See Troubleshooting](../troubleshooting/overview.md)
35 |
36 | ## What happens to the data?
37 |
38 | WD sends the collected data to several upstream servers (depending on configuration):
39 | - 1. wsprdaemon.org (wspr spots and other information)
40 | - 2. wspr.net (wspr spots only)
41 | - 3. pskreporter.info (FT4 and FT8 spots)
42 | - 4. pswsnetwork.caps.ua.edu (WWV/H and CHU monitoring)
43 |
44 | A diagram of the WSPR reports:
45 |
46 | 
--------------------------------------------------------------------------------
/docs/source/description/validity.md:
--------------------------------------------------------------------------------
1 | # Validity of data path and results
2 |
3 | describe the data path and validity checks
4 |
--------------------------------------------------------------------------------
/docs/source/external_links.md:
--------------------------------------------------------------------------------
1 | # External Links
2 |
3 | ## [Technical Descriptions](https://wsprdaemon.org/technical.html)
4 | ## [wsprdaemon](https://github.com/rrobinett/wsprdaemon.git)
5 | ## [ka9q-radio](https://ka9q-radio.org)
6 | ## [Personal Space Weather Station Data](https://pswsnetwork.caps.ua.edu/home)
7 | ## [HamSCI](https://hamsci.org/grape)
8 | ## [WSPR Rocks!](https://wspr.rocks)
9 | ## [WSPR.net](https://wsprnet.org)
10 | ## [PSKReporter](https://pskreporter.info)
11 | ## [SuperDARN](https://superdarn.ca/)
--------------------------------------------------------------------------------
/docs/source/index.md:
--------------------------------------------------------------------------------
1 |
2 | # WSPRDAEMON Documentation
3 |
4 | ```{toctree}
5 | :maxdepth: 2
6 | :caption: Description
7 | description/how_it_works.md
8 | description/validity.md
9 | ```
10 |
11 | ```{toctree}
12 | :maxdepth: 2
13 | :caption: Requirements
14 |
15 | requirements/os.md
16 | requirements/radios.md
17 | requirements/network.md
18 | ```
19 |
20 | ```{toctree}
21 | :maxdepth: 2
22 | :caption: Software Installation
23 |
24 | installation/preparation.md
25 | installation/git.md
26 | ```
27 |
28 | ```{toctree}
29 | :maxdepth: 1
30 | :caption: Software Configuration
31 |
32 | configuration/wd_conf.md
33 | configuration/radiod_conf.md
34 | configuration/ka9q-web.md
35 | ```
36 |
37 | ```{toctree}
38 | :maxdepth: 2
39 | :caption: Working Results
40 |
41 | results/wspr.md
42 | results/grape.md
43 | results/psk.md
44 | ```
45 |
46 | ```{toctree}
47 | :maxdepth: 2
48 | :caption: Maintenance & Updates
49 |
50 | maintenance/operating.md
51 | maintenance/monitoring.md
52 | maintenance/aliases.md
53 | ```
54 |
55 | ```{toctree}
56 | :maxdepth: 2
57 | :caption: Troubleshooting
58 |
59 | troubleshooting/overview.md
60 | troubleshooting/typicals.md
61 | ```
62 |
63 | ```{toctree}
64 | :maxdepth: 1
65 | :caption: FAQ
66 |
67 | FAQ.md
68 | ```
69 |
70 | ```{toctree}
71 | :maxdepth: 1
72 | :caption: External Links
73 |
74 | external_links.md
75 | ```
76 |
77 | ```{toctree}
78 | :maxdepth: 1
79 | :caption: Contributors
80 |
81 | contributors.md
82 | ```
83 |
--------------------------------------------------------------------------------
/docs/source/installation/git.md:
--------------------------------------------------------------------------------
1 | # Download the software with GIT
2 |
3 | GitHub hosts the repository for all versions of WD. Presently, the current master provides version 3.2.3. The latest development version, 3.3.1, remains in a branch.
4 |
5 | ## Clone wsprdaemon from github.com
6 |
7 | From /home/wsprdaemon (or the installing user's home directory) [See Preparing the Installation](./preparation.md)
8 | ```
9 | git clone https://github.com/rrobinett/wsprdaemon.git
10 | cd wsprdaemon
11 | ```
12 | Execute all further git commands in the /home/wsprdaemon/wsprdaemon directory.
13 |
14 | Ensure you have the latest stable version:
15 | ```
16 | git checkout master
17 | git status
18 | git log
19 | ```
20 |
21 | Subsequently, to apply any updates of the latest version, use:
22 | ```
23 | git pull
24 | ```
25 |
26 | "master" generally refers to the latest stable version of the code. As development of the code proceeds,
27 | you may elect to switch to a development branch, e.g., 3.3.1. To do this, use:
28 | ```
29 | git checkout 3.3.1
30 | git pull
31 | ```
32 |
33 |
34 | WD provides lots of shell "aliases" to important and otherwise useful functions. To have immediate access to these, run:
35 | ```
36 | source bash-aliases ../.bash_aliases
37 | ```
38 |
39 | Having prepared and cloned the wsprdaemon software, now you can run it:
40 | ```
41 | wd
42 | ```
43 |
44 | This sets the stage and prompts you to configure your setup:
45 | - [wsprdaemon configuration](../configuration/wd_conf.md)
46 | - [radiod configuration](../configuration/radiod_conf.md)
47 | - KiwiSDR
48 |
49 | Once you have defined a new wsprdaemon.conf (or restored your previous one) then invoke a command like:
50 | ```
51 | wdv
52 | ```
53 | Nominally, this reports the current version of wsprdaemon but it will use wsprdaemon.conf to proceed with setting up wsprdaemon to run -- downloading and compiling any required software (e.g., ka9q-radio and ka9q-web), setting up the proper directories, etc.
54 |
55 | Finally, you can start wsprdaemon with:
56 | ```
57 | wd -A
58 | ```
59 | which will pipe all messages to stdout (the screen from which you invoked the command) which can help if something doesn't function properly.
60 |
61 | Or simply run:
62 | ```
63 | wda
64 | ```
65 | which starts wsprdaemon "quietly" piping all messages to log files.
66 |
67 | # To install ka9q-radio independently:
68 |
69 | ka9q-radio has many uses outside its integrated role with WD. You can install and run it without WD.
70 | Keep in mind that Rob checks out a particular version of ka9q-radio that he knows works with WD.
71 | So, if you use another version, you may find its interaction with WD problematic. YMMV.
72 |
73 | For details of ka9q-radio installation, consult the docs sub-directory in the ka9q-radio created after performing a `git clone`.
74 |
75 | - [KA9Q_RADIO_GIT_URL](https://github.com/ka9q/ka9q-radio.git)
76 | - [KA9Q_FT8_GIT_URL](https://github.com/ka9q/ft8_lib.git)
77 | - [PSK_UPLOADER_GIT_URL](https://github.com/pjsg/ftlib-pskreporter.git)
78 |
79 | ## To install ka9q-web:
80 |
81 | ka9q-web requires ka9q-radio of course, but also the web server package, onion, produced by David Moreno, so install it first.
82 |
83 | - [ONION_GIT_URL](https://github.com/davidmoreno/onion)
84 |
85 | Use the following bash scripts as scripts or just as a guide to installation:
86 |
87 | ```
88 | declare ONION_LIBS_NEEDED="libgnutls28-dev libgcrypt20-dev cmake"
89 | if [[ ${OS_RELEASE} =~ 24.04 ]]; then
90 | ONION_LIBS_NEEDED="${ONION_LIBS_NEEDED} libgnutls30t64 libgcrypt20"
91 | fi
92 |
93 | function build_onion() {
94 | local project_subdir=$1
95 | local project_logfile="${project_subdir}-build.log"
96 |
97 | wd_logger 2 "Building ${project_subdir}"
98 | (
99 | cd ${project_subdir}
100 | mkdir -p build
101 | cd build
102 | cmake -DONION_USE_PAM=false -DONION_USE_PNG=false -DONION_USE_JPEG=false -DONION_USE_XML2=false -DONION_USE_SYSTEMD=false -DONION_USE_SQLITE3=false -DONION_USE_REDIS=false -DONION_USE_GC=false -DONION_USE_TESTS=false -DONION_EXAMPLES=false -DONION_USE_BINDINGS_CPP=false ..
103 | make
104 | sudo make install
105 | sudo ldconfig
106 | ) >& ${project_logfile}
107 | rc=$?
108 | if [[ ${rc} -ne 0 ]]; then
109 | wd_logger 1 "ERROR: compile of '${project_subdir}' returned ${rc}:\n$( < ${project_logfile} )"
110 | exit 1
111 | fi
112 | wd_logger 2 "Done"
113 | return 0
114 | }
115 | ```
116 |
117 | - [KA9Q_WEB_GIT_URL](https://github.com/scottnewell/ka9q-web)
118 |
119 | ```
120 | function build_ka9q_web() {
121 | local project_subdir=$1
122 | local project_logfile="${project_subdir}_build.log"
123 |
124 | wd_logger 2 "Building ${project_subdir}"
125 | (
126 | cd ${project_subdir}
127 | make
128 | sudo make install
129 | ) >& ${project_logfile}
130 | rc=$? ; if (( rc )); then
131 | wd_logger 1 "ERROR: compile of 'ka9q-web' returned ${rc}:\n$(< ${project_logfile})"
132 | exit 1
133 | fi
134 | wd_logger 2 "Done"
135 | return 0
136 | }
137 | ```
--------------------------------------------------------------------------------
/docs/source/maintenance/monitoring.md:
--------------------------------------------------------------------------------
1 | # Monitoring WD operation
2 |
3 | ## recordings
4 |
5 | ## postings
6 |
7 | ## logs
8 |
9 | ## using BTOP
10 |
11 | 
12 |
13 | ## Useful wd aliases
14 |
15 | - *wd-query* -- usage: wd-query {-r | -t} ID [HOURS_TO_SEARCH] -r => search for ID of reporter -t => search for ID of transmit beacon"
16 | - *wd-wsprlog-check*
17 | - watch "ls -lt `find -name pcmrecord-errors.log`"
18 | - *wdrl* -- show the syslog entries for the radiod service. add -f to watch new log lines appear
19 | - *wdln* -- watch the upload to wsprnet log
20 | - *wdle* -- search all log files for ERROR lines
--------------------------------------------------------------------------------
/docs/source/maintenance/operating.md:
--------------------------------------------------------------------------------
1 | # Operating WD
2 |
3 | ## Start and Stop
4 |
5 | After installation, there are two different ways to run WD. Each of these is invoked from the command line.
6 |
7 | The first method is the usual one where WD is run as a 'systemctl' service. In this mode it automatically starts when Linux boots or reboots after a power cycle. The command line (aliased) commands associated with this mode are 'wd -a' and 'wd -z'. 'wd -a' starts this mode while 'wd -z' terminates it. However, this method does not post system errors back to the command line. For this reason, when first verifying at startup or for verifying operation after wsprdaemon.conf has been modified, it can be useful to temporarily use the second mode.
8 |
9 | This second mode is invoked with 'wd -A' and terminated with 'wd -Z'. Note the capitalization differences. 'wd -A' invokes WD from the command line rather than automatically from the systemctl environment. This means that it does post errors back to the command line where they can be viewed. This mode is exited by typing 'wd -Z'.
10 |
11 | Once an installation and wsprdaemon.conf changes are verified, 'wd -Z' followed by 'wd -a' will make operation automatic. Any future changes should be made by first stopping this automatic operation and then temporarily using 'wd -A' to re-verify them followed by 'wd -Z' and 'wd -a' once they are acceptable.
12 |
13 |
14 |
--------------------------------------------------------------------------------
/docs/source/network/basics.md:
--------------------------------------------------------------------------------
1 | # Basic Network Requirements
2 |
3 |
4 |
--------------------------------------------------------------------------------
/docs/source/network/multicast.md:
--------------------------------------------------------------------------------
1 | # Multicast Requirements
2 |
3 | 
4 |
5 |
--------------------------------------------------------------------------------
/docs/source/requirements/network.md:
--------------------------------------------------------------------------------
1 | # Networking
2 |
3 | WD using ka9q-radio typically runs on a stand-alone computer. In this scenario, a standard ethernet or WiFi connection to the computer will suffice for remote management and for reporting.
4 |
5 | However, ka9q-radio uses RTP (multicast) streams to manage interprocess communications. This means one can run ka9q-radio with a hardware radio on one computer and process the output on another. However, multicast streams produce significant network traffic -- enough to bring a WiFi network to a standstill.
6 |
7 | When running on a stand-alone computer, one should set the parameter ttl = 0 in radiod@.conf. This directs radiod to put no RTP streams on the LAN. When distributing the functions between computers, however, one sets ttl = 1 in radiod@.conf and, especially with a connected WiFi LAN, interposes an IGMP-aware ethernet switch (with IGMP snooping ON) between the computers using RTP and the rest of your network. This will confine the RTP streams to the connections between your radiod and WD computers so they don't flood the rest of your LAN.
8 |
9 | 
--------------------------------------------------------------------------------
/docs/source/requirements/os.md:
--------------------------------------------------------------------------------
1 | # Operating Sytems
2 |
3 | I currently do most of my installation and run-time testing on the recently released Ubuntu 24.04 LTS server OS. Desktop Ubuntu includes a lot of software auto-upgrade features which can disrupt the operation of WD and other applications, so I suggest you avoid using the desktop version. If you do use it, then at least follow [Clint KA7OEI's "de-snap" instructions](http://www.sdrutah.org/info/websdr_Ubuntu_2204_install_notes.html#snapd)
4 |
5 | [Download Ubuntu server](https://ubuntu.com/download/server) and run the 'Raspberry Pi Imager' program on your PC to copy the Ubuntu.iso file to a 8 GB or larger USB thumb drive.
6 |
7 | In Imager select:
8 |
9 | Rasperry Pi Device => "NO FILTERING" Operating System => Use custom => browse to the Ubuntu image file you have downloaded to your PC Storage => the thumb drive
10 |
11 | You will then insert the USB thumb drive into your host and boot from that drive. Frequently you will need to press the DEL key or a function key (e.g. F7) immediately after power-up in order to instruct the BIOS of the server to boot from the thumb drive.
12 |
13 | I suggest that you create a user 'wsprdaemon' with sudo privileges after installation is complete.
14 |
15 | While WD can run on many different x86 and ARM CPU's, the RX888 is best run on a i5-6500T-class or newer x86 server.
16 |
17 | For new installations I have found the Beelink brand SER 5 Ryzen 5800U offers excellent price, performance and low power consumption for a WD system. Today Sept 9, 2024 Amazon offers it for $270 (after $18 discount) at [Amazon](https://www.amazon.com/Beelink-SER5-Computer-Graphics-Support/dp/B0D6G965BC), but the same Beelink may be offered at several different prices on Amazon, so search for price including 'discount coupons'. The Beelink Ser 5 5560U is another excellent choice which until today I was able to purchase for $219. Also consider the Ryzen 5800 series chips, (5800U, 5800H, 5825H) but avoid the 5700 series as these have a divided L3 cache which may introduce gaps in the USB stream as processing switches from one set of cores to another.
18 |
19 | Whatever server you choose, WD runs a little better on 16 GB of ram.
20 |
21 | The Beelink comes with Windows installed but WD runs on Linux, so I usually first install Windows and associate the Beelink with my Microsoft account to be sure the server hardware and software are functional, and in case I want to restore Windows on that server.
22 |
23 | Maximizing CPU performance on the Beelink requires that the 'always slow' default fan setting be changed in the BIOS to 'fan on a 40C, fan max at 60C, speed ramp 8'. The 'btop' program which I run to monitor the CPU usage displays CPU temperature among many other things. If it shows the CPU at much more than 60C during the 100% busy periods which start every even 2 minutes, then your fan is not running fast enough.
24 |
25 | I also find the 'power always on' setting deeply buried in the ACH sub menu.
26 |
--------------------------------------------------------------------------------
/docs/source/requirements/radios.md:
--------------------------------------------------------------------------------
1 | # Compatible Radios
2 |
3 | Please note, for the present and in collaboration with HamSCI efforts, WD supports kiwiSDR and RX888. The documentation here also primarily addresses the configuration and use of WD with those two radios.
4 |
5 | ## KiwiSDR
6 |
7 | ## Radios that work with ka9q-radio
8 |
9 | ### RX888
10 |
11 | The RX888 connects to your computer via a USB 3 -- SuperSpeed connection. These typically have a BLUE tab on the computer socket as distinct from the white or black for USB 2.
12 |
13 | As delivered the RX888 has sub-optimal thermal protection and for radio science applications it needs an external GPSDO @ 27 MHz clock (although you can alter this).
14 |
15 | Paul Elliott WB6CXC created a screwdriver-only kit which enhances the thermal protection and adds a ground-isolated external clock SMA input port. Paul describes the installation and use of his kit on [his website](https://turnislandsystems.com/wp-content/uploads/2024/05/RX888-Kit-2.pdf)
16 |
17 | The kit is available at [the TAPR web store](https://tapr.org/product/rx888-clock-kit-and-thermal-pad/).
18 |
19 | ### Airspy variants
20 |
21 | - Airspy R2
22 | - Airspy HF+
23 |
24 | ### RTL-SDR
25 |
26 | ### SDRPLAY variants
27 |
28 | Not directly suppported by radiod.
29 | - RSPduo
30 | - RSPdx
31 |
32 | ### FobosSDR
33 |
34 | ### Others...
35 |
36 | - Funcube Dongle
37 | - OpenHPSDR variants
38 |
--------------------------------------------------------------------------------
/docs/source/results/grape.md:
--------------------------------------------------------------------------------
1 | # HamSCI WWV/H and CHU Monitoring
2 |
3 | WD software captures data in support of the [HamSCI Personal Space Weather Station project](https://hamsci.org/psws-overview).
4 | It makes simultaneous I/Q stream recordings of WWV, WWVH, and CHU broadcasts, converts these into digital_rf data repositories and uploads them to the [PSWS server at the University of Alabama](https://pswsnetwork.caps.ua.edu/).
5 |
6 | 
7 |
8 | 
9 |
--------------------------------------------------------------------------------
/docs/source/results/psk.md:
--------------------------------------------------------------------------------
1 | # PSKReporter
2 |
3 | Managed by Philip Gladstone, the [website](https://pskreporter.info) aggregates lots of information. A WD/radiod installation uploads spots to this site.
4 |
5 | 
--------------------------------------------------------------------------------
/docs/source/results/wspr.md:
--------------------------------------------------------------------------------
1 | # WSPR spots
2 |
3 | WSPR, which stands for Weak Signal Propagation Reporter, defines a protocol used in amateur radio for assessing radio signal propagation. It allows users to send and receive very low-power transmissions over long distances, that a receiving station then decodes and reports. Here are some key points about WSPR:
4 |
5 | **Purpose**: WSPR is primarily used for scientific and testing purposes, helping amateur radio operators analyze radio wave propagation conditions.
6 |
7 | **Operation**: WSPR operates on specific frequencies and uses a special encoding method for its transmissions. Unlike traditional radio communication, WSPR does not support two-way conversations; it focuses on sending short, beacon-like signals.
8 |
9 | **Implementation**: The WSPR protocol can be implemented in software, and it is compatible with various hardware setups, making it accessible for many amateur radio operators.
10 |
11 | **Community and Reporting**: WSPR signals are often received by automated software-defined radio (SDR) receivers, which can report back the signal's strength and other details via the Internet, contributing to a global database of propagation conditions.
12 |
13 | For more detailed information, you may refer to the [WSPR Wikipedia page](https://en.wikipedia.org/wiki/WSPR_(amateur_radio_software)).
14 |
15 | ## WSPR.Rocks
16 |
17 | Managed by Philip Barnard VK7JJ.
18 | Presents an interface to a Clickhouse DB courtesy of Arne at wspr.live and hosted at wsprdaemon.org.
19 |
20 | [wspr.rocks](https://wspr.rocks)
21 |
22 | ## WSPR.Live
23 |
24 | WSPR.live allows you to do analysis on the real-time wspr spot data. The database contains all spots ever reported to wsprnet.org and allows public access to the data.
25 |
26 | [wspr.live](https://wspr.live)
27 |
28 | ## WSPRnet
29 |
30 | Amateur radio operators using K1JT's MEPT_JT digital mode collaborate via the Weak Signal Propagation Reporter Network to probe radio frequency propagation conditions using very low power (QRP/QRPp) transmissions. The software is open source, and the data collected are available to the public through [this site](https://wsprnet.org/).
31 |
32 |
33 |
--------------------------------------------------------------------------------
/docs/source/troubleshooting/overview.md:
--------------------------------------------------------------------------------
1 | # Troubleshooting wsprdaemon
2 |
3 | Nothing ever goes wrong!
4 |
5 | ## Use tmux or screen
6 |
7 | Especially useful in managing a computer remotely. This will enable you to maintain a session between logins or should your network connection drop.
8 |
9 | ## Use btop!
10 |
11 | 
12 |
13 | ## Use wdln
14 |
15 | This displays the wsprnet upload log.
16 |
17 | ## Use wdle
18 |
19 | ## Check recordings
20 |
21 | Change to the temporary directory.
22 |
23 | ```
24 | cdt
25 | ```
26 | Then enter the sub-directory for a receiver and channel, for instance, the following moves to receiver KA9Q_0 and a 20m wspr channel:
27 | ```
28 | cd recording.d/KA9Q_0/20/
29 | ```
30 | Here, you can invoke the wdww alias that dynamically lists the wav files for that channel. A problem exists if you see no files or don't see regular incrementing of the latest file size.
31 |
32 | ```
33 | wdww
34 | ```
35 |
36 | ## Check the logs
37 |
38 | You can locate logs using something like the following command:
39 | ```
40 | find . -type f -name "*.log" ! -name "*sox.log" ! -name "*error.log"
41 | ```
42 | where:
43 | - "." indicates starting the search from the current directory
44 | - "-type" directs the search only for files
45 | - "-name" specifies the filename of interest
46 | - "! -name" specifies names to exclude
47 |
48 | ### logs in /var/log
49 |
50 | - /var/log/wspr.log
51 | - /var/log/ft8.log
52 | - /var/log/ft4.log
53 |
54 | ### logs in ~/wsprdaemon
55 |
56 | - ./wav-archive.d/grape_upload_daemon.log
57 | - ./ka9q-radio_build.log
58 | - ./grep.log
59 | - ./ps.log
60 | - ./diff.log
61 | - ./git.log
62 | - ./uploads.d/wsprdaemon.d/upload_to_wsprdaemon_daemon.log
63 | - ./uploads.d/wsprnet.d/spots.d/upload_to_wsprnet_daemon.log
64 | - ./ft8_lib_build.log
65 | - ./onion-build.log
66 | - ./noise_plot.log
67 | - ./ka9q_web_daemon.log
68 | - ./ka9q-web_build.log
69 | - ./watchdog_daemon.log
70 | - ./ka9q_web_service_8081.log
71 |
72 | ### logs in /dev/shm/wsprdaemon
73 |
74 | Note: the 2nd and 3rd subdirectories will vary according to your receivers and channels.
75 |
76 | - ./recording.d/KA9Q_LONGWIRE/80eu/posting_daemon.log
77 | - ./recording.d/KA9Q_LONGWIRE/80eu/decoding_daemon.log
78 | - ./recording.d/KA9Q_LONGWIRE/80eu/find.log
79 | - ./recording.d/KA9Q_LONGWIRE/80eu/metadump.log
80 | - ./recording.d/KA9Q_LONGWIRE/80eu/ka9q_status.log
81 | - ./recording.d/KA9Q_LONGWIRE/80eu/sox-stats.log
82 | - ./recording.d/KA9Q_LONGWIRE/80eu/adc_overloads.log
83 | - ./recording.d/KA9Q_LONGWIRE/80eu/sox.log
84 | - ./recording.d/KA9Q_LONGWIRE/80eu/get-peak-wav-sample.log
85 | - ./recording.d/KA9Q_LONGWIRE/80eu/W_120/decoding_daemon.log
86 | - ./recording.d/KA9Q_LONGWIRE/80eu/wav_status.log
87 | - ./recording.d/KA9Q_LONGWIRE/80eu/add_derived.log
88 | - ./recording.d/KA9Q_LONGWIRE/80eu/printf.log
89 |
90 | - ./recording.d/KA9Q_LONGWIRE_WWV/wav-record-daemon-all.log
91 | - ./recording.d/KA9Q_LONGWIRE_WWV/pcmrecord-errors.log
92 | - ./recording.d/KA9Q_LONGWIRE_WWV/pcmrecord-outs.log
93 |
94 | - ./uploads.d/wsprdaemon.d/grep.log
95 | - ./uploads.d/wsprnet.d/curl.log
96 |
--------------------------------------------------------------------------------
/docs/source/troubleshooting/typicals.md:
--------------------------------------------------------------------------------
1 | # Some typical problems
2 |
3 | ## Consider usb loading issues.
4 | - is the RX888 plugged into a USB3 superspeed socket?
5 | - has radiod successfully loading the firmware to the RX888?
6 |
7 | ## Consider system service issues
8 | - systemctl service enabled?
9 | - configured to always restart on boot?
10 | - does your computer or OS support what you want the RX888 or wsprdaemon to do?
11 | - for Beelink, set the fan to 'auto' with the fan turning on at 40C and getting to max speed at 60C with at ramp of '8'
12 | - have you configured the computer to resume function after loss/restoration of power?
13 |
14 | ## Consider avahi address translation issues
15 | - avahi installed (see pre-requisite libraries)
16 |
17 | ## Consider ethernet hubs/architecture
18 |
19 | - ttl = 0 by default (assuming a stand-alone setup) so as not to flood the LAN or WLAN with multicast packets
20 | - if using multicast between computers, ttl = 1 required on sending computer and on the right NIC
21 | - multicast will require an IGMP-capable switch with snooping ON to isolate the computers using radiod multicast from the rest of your LAN or WLAN, particularly if your LAN uses WiFi.
22 | - have you defined and enabled a device in radiod@rx888-XXX.conf?
23 |
24 | ## Brute force recovery
25 |
26 | One method of recovery involves, in effect, starting from (almost) scratch.
27 |
28 | The important "almost" refers to preserving your configuration. DON'T FORGET THIS!
29 |
30 | First, critically, copy your ~/wsprdaemon/wsprdaemon.conf file to somewhere safe, e.g., the home directory:
31 | ```
32 | cp ~/wsprdaemon/wsprdaemon.conf ~
33 | ```
34 | Then delete the wsprdaemon subdirectory:
35 | ```
36 | rm -rf ~/wsprdaemon/
37 | ```
38 |
39 | Clean out /shm/wsprdaemon:
40 | ```
41 | rm -rf /shm/wsprdaemon/*
42 | ```
43 |
44 | Clone the WD repository (while in /home/wsprdaemon/):
45 | ```
46 | git clone https://github.com/rrobinett/wsprdaemon.git
47 | ```
48 |
49 | Copy the wsprdaemon.conf back into the new ~/wsprdaemon:
50 | ```
51 | cp ~/wsprdaemon.conf ~/wsrdaemon
52 | ```
53 |
54 | Run wd to get everything built and installed:
55 | ```
56 | wd
57 | ```
58 |
59 | Restart WD:
60 | ```
61 | wda
62 | ```
63 |
64 | Check the usual places (see above) to ensure things are functioning as expected.
--------------------------------------------------------------------------------
/get-peak-wav-sample.py:
--------------------------------------------------------------------------------
1 | import soundfile as sf
2 | import numpy as np
3 | import argparse
4 |
5 | # Set up argument parser
6 | parser = argparse.ArgumentParser(description="Find the maximum sample value and its dBFS value in a list of audio files.")
7 | parser.add_argument("files", nargs='+', help="Paths to the input WAV files")
8 | args = parser.parse_args()
9 |
10 | max_sample_value = -np.inf # Initialize to the smallest possible value
11 |
12 | # Process each file
13 | for file_path in args.files:
14 | try:
15 | # Read the audio file
16 | data, samplerate = sf.read(file_path)
17 |
18 | # Flatten the data if it's multi-channel
19 | if data.ndim > 1:
20 | data = data.flatten()
21 |
22 | # Update the maximum sample value
23 | file_max = np.max(np.abs(data))
24 | max_sample_value = max(max_sample_value, file_max)
25 |
26 | # print(f"File: {file_path}, Max Sample: {file_max:.12f}")
27 |
28 | except Exception as e:
29 | print(f"Error processing {file_path}: {e}")
30 |
31 | # Compute dBFS for the overall maximum sample
32 | if max_sample_value > -np.inf:
33 | overall_dbfs = 20 * np.log10(max_sample_value) if max_sample_value > 0 else -float('inf')
34 | # print(f"\nOverall Max Sample Value (Linear): {max_sample_value:.12f}")
35 | # print(f"Overall Max Sample Value (dBFS): {overall_dbfs:.12f} dBFS")
36 | print(f"{max_sample_value:.12f}")
37 | print(f"{overall_dbfs:.12f}")
38 | else:
39 | print("\nNo valid files processed.")
40 |
41 |
--------------------------------------------------------------------------------
/ka9q-ft-cleanup.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | # This script is run by cron ever 10 minutes to cleanup old wav files
4 | # and truncate ftX.log files which have grown too large
5 |
6 | declare -r GET_FILE_SIZE_CMD="stat --format=%s"
7 | declare -r LOG_FILE_MAX_SIZE_BYTES=1000000
8 | function truncate_file() {
9 | local file_path=$1 ### Must be a text format file
10 | local file_max_size=$2 ### In bytes
11 | local file_size=$( ${GET_FILE_SIZE_CMD} ${file_path} )
12 |
13 | [[ $verbosity -ge 3 ]] && echo "$(date): truncate_file() '${file_path}' of size ${file_size} bytes to max size of ${file_max_size} bytes"
14 |
15 | if [[ ${file_size} -gt ${file_max_size} ]]; then
16 | local file_lines=$( cat ${file_path} | wc -l )
17 | local truncated_file_lines=$(( ${file_lines} / 2))
18 | local tmp_file_path="${file_path%.*}.tmp"
19 | tail -n ${truncated_file_lines} ${file_path} > ${tmp_file_path}
20 | mv ${tmp_file_path} ${file_path}
21 | local truncated_file_size=$( ${GET_FILE_SIZE_CMD} ${file_path} )
22 | [[ $verbosity -ge 1 ]] && echo "$(date): truncate_file() '${file_path}' of original size ${file_size} bytes / ${file_lines} lines now is ${truncated_file_size} bytes"
23 | fi
24 | }
25 |
26 | cd /dev/shm/ka9q-radio
27 |
28 | declare old_wav_file_name_list=( $(find . -type f -name '*wav' -mmin +30) )
29 | for old_wav_file_name in ${old_wav_file_name_list[@]}; do
30 | [[ $verbosity -ge 1 ]] && echo "$(date): deleting old_wav_file_name=${old_wav_file_name}"
31 | rm -f ${old_wav_file_name}
32 | done
33 |
34 | declare log_file_name_list=( $(find -type f -name '*.log') )
35 | for log_file_name in ${log_file_name_list[@]}; do
36 | truncate_file ${log_file_name} ${LOG_FILE_MAX_SIZE_BYTES}
37 | done
38 | cd - > /dev/null
39 |
40 |
41 |
--------------------------------------------------------------------------------
/noise_graphs_reporter_index_template.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | Noise Analysis
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/noise_graphs_root_index_template.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | Wsprdaemon Noise Graphs
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/one-minute-silent-float.wv:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rrobinett/wsprdaemon/71d90d1563fcb2c9f07d2aa53f2150326229aa63/one-minute-silent-float.wv
--------------------------------------------------------------------------------
/radiod@rx888-wsprdaemon-template.conf:
--------------------------------------------------------------------------------
1 | # Generic minimal RX888-wsprdaemon config
2 |
3 | [global]
4 | hardware = rx888 # use built-in rx888 driver, configured in [rx888]
5 | status = hf-status.local # DNS name for receiver status and commands
6 | data = hf-data.local
7 | samprate = 12000 # default output sample rate
8 | mode = usb # default receive mode
9 | # rest are defaults
10 | #ttl = 1
11 | ttl = 0 # Too many WD sites don't have IGMP aware ethernet swtiches, so don't send radiod multicast packets out the ethernet port.
12 | fft-threads = 1
13 | #iface = enp1s0
14 |
15 | [rx888]
16 | device = "rx888" # required so it won't be seen as a demod section
17 | description = "rx888 wsprdaemon" # good to put callsign and antenna description in here
18 | gain = 0 # dB
19 | # rest are defaults
20 | #samprate = 129600000 # Hz
21 | samprate = 64800000 # 128 Msps will eventual burn out the stock RX888 Mk II, and this 64 Msps frees much CPU on older CPUs
22 |
23 | [WSPR]
24 | # Bottom of 200 Hz WSPR segments on each band. Center is 1500 Hz higher
25 | # sample rate must be 12 kHz as required by wsprd
26 | disable = no
27 | encoding = float
28 | data = wspr-pcm.local
29 | agc = 0
30 | gain = 0
31 | samprate = 12000
32 | mode = usb
33 | low = 1300
34 | high = 1700
35 | freq = "136k000 474k200 1m836600 3m568600 3m592600 5m287200 5m364700 7m038600 10m138700 13m553900 14m095600 18m104600 21m094600 24m924600 28m124600 50m293000"
36 |
37 | [FT8]
38 | disable = no
39 | data = ft8-pcm.local
40 | mode = usb
41 | freq = "1m840000 3m573000 5m357000 7m074000 10m136000 14m074000 18m100000 21m074000 24m915000 28m074000 50m313000"
42 |
43 | [FT4]
44 | disable = no
45 | data = ft4-pcm.local
46 | mode = usb
47 | freq = "3m575000 7m047500 10m140000 14m080000 18m104000 21m140000 24m919000 28m180000 50m318000"
48 |
49 | [WWV-IQ]
50 | disable = no
51 | encoding = float
52 | data = wwv-iq.local
53 | agc = 0
54 | gain = 0
55 | samprate = 16k
56 | mode = iq
57 | freq = "60k000 2500000 5000000 10000000 15000000 20000000 25000000 3330000 7850000 14670000" ### Added the three CHU frequencies
58 |
59 | [HF MANUAL]
60 | data = hf-pcm.local
61 | freq = 0
62 |
--------------------------------------------------------------------------------
/show-memory-usage.sh:
--------------------------------------------------------------------------------
1 | declare pid_file_list=()
2 | declare pid_last_values_file="show-memory-usage.last"
3 | declare pid_new_values_file="show-memory-usage.new"
4 |
5 | touch ${pid_last_values_file}
6 |
7 | function get_daemon_pid_list()
8 | {
9 | pid_file_list=($(find ~/wsprdaemon /tmp/wsprdaemon -name '*.pid') )
10 | }
11 |
12 | function get_mem_usage()
13 | {
14 | local mem_total=0
15 | > ${pid_new_values_file}
16 | get_daemon_pid_list
17 | for pid_file in ${pid_file_list[@]} ; do
18 | local pid_val=$(< ${pid_file} )
19 | if ps ${pid_val} > /dev/null ; then
20 | local pid_rss_val=$(awk -v pid_file=${pid_file} '/VmRSS/{printf "%s\n", $2}' /proc/${pid_val}/status)
21 | # printf "PID %6s VmRSS %6s from pid file %s\n" ${pid_val} ${pid_rss_val} ${pid_file}
22 | local old_rss_val=$( grep "${pid_file}" ${pid_last_values_file} | awk '{print $1}')
23 | if [[ "${pid_rss_val}" -ne "${old_rss_val}" ]]; then
24 | printf "For PID %8s old=%8s new=%8s from file %s\n" "${pid_val}" "${old_rss_val}" "${pid_rss_val}" "${pid_file}"
25 | fi
26 | printf "%8d %s\n" ${pid_rss_val} ${pid_file} >> ${pid_new_values_file}
27 | mem_total=$(( mem_total + pid_rss_val))
28 | else
29 | echo "pid file ${pid_file} contains pid # ${pid_val} which isn't active"
30 | rm ${pid_file}
31 | fi
32 | done
33 | local output_str="$(date): Found ${#pid_file_list[@]} pid files with a VmRSS total of ${mem_total}"
34 | echo "${output_str}" >> show-memory-usage.txt
35 | echo "${output_str}"
36 | }
37 | while true; do
38 | get_mem_usage
39 | sleep 10
40 | done
41 |
--------------------------------------------------------------------------------
/silent_iq.flac:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rrobinett/wsprdaemon/71d90d1563fcb2c9f07d2aa53f2150326229aa63/silent_iq.flac
--------------------------------------------------------------------------------
/suntimes.py:
--------------------------------------------------------------------------------
1 | from math import cos,sin,acos,asin,tan, floor
2 | from math import degrees as deg, radians as rad , pi as pi
3 | from datetime import date,datetime,time,timezone,timedelta
4 | import calendar
5 | import sys
6 | from numpy import deg2rad
7 | import subprocess
8 |
9 | # Gwyn Griffiths G3ZIL 2 September 2023 V2
10 | # Basic principles sunrise and sunset calculator needs lat and lon as the two input arguments
11 | # Extracts timezone for the local computer using timedatectl as an operating system command
12 | # Equations from NOAA at https://gml.noaa.gov/grad/solcalc/solareqns.PDF
13 | # Watch out here - the trig functions have mix of degrees and radian inputs so explicit conversion used where needed
14 | # Error check for perpetual day or night and times are outputin the 'except' block
15 | # V2 has no timezone argument, calculates from call to operating system executable timedatectl
16 | # G3ZIL checking code 30 April 2025 for any numerical error
17 |
18 | lat=float(sys.argv[1])
19 | lon=float(sys.argv[2])
20 |
21 | date_offset=0
22 |
23 | # calculate day of year
24 | today=datetime.now(timezone.utc)+timedelta(days=date_offset) # get today's date UTC timezone
25 | day_of_year=int(today.strftime('%j')) # day of year as integer
26 | year=int(today.strftime('%Y')) # year as integer
27 |
28 | # get number of days in year
29 | if(calendar.isleap(year)):
30 | n_days=366
31 | else:
32 | n_days=365
33 |
34 | # get hour
35 | hour=int(today.strftime('%H')) # hour as zero padded integer
36 |
37 | # calculate fractional year gamma where whole year is two pi, so gamma is in radians, fine for trig functions below
38 | gamma=((2*pi)/n_days)*(day_of_year-1+(hour-12)/24)
39 | # calculate equation of time in minutes
40 | eqtime=229.18*(0.000075+0.001868*cos(gamma)-0.032077*sin(gamma)-0.014615*cos(2*gamma)-0.040849*sin(2*gamma))
41 |
42 | # calculate solar declination angle in radians
43 | decl=0.006918-0.399912*cos(gamma)+0.070257*sin(gamma)-0.006758*cos(2*gamma)+0.000907*sin(2*gamma)-0.002697*cos(3*gamma)+0.00148*sin(3*gamma)
44 |
45 | # calculate timezone offset in integer hours from longitude
46 | tz_offset = float(subprocess.check_output("timedatectl | awk '/Time/{print substr($5,1,3)}'", shell=True, universal_newlines=True))
47 |
48 | #print ("time zone offset from timedatectl ", tz_offset)
49 |
50 | # use the try feature as error trap for polar night and day
51 | try:
52 | # Sunrise/Sunset Calculations
53 | # For the special case of sunrise or sunset, the zenith is set to 90.833 (the approximate correction for
54 | # atmospheric refraction at sunrise and sunset, and the size of the solar disk), and the hour angle
55 | # becomes:
56 | deg2rad=360/(2*pi)
57 | ha_sunrise=acos((cos(90.833/deg2rad)/(cos(lat/deg2rad)*cos(decl)))-tan(lat/deg2rad)*tan(decl))*deg2rad
58 | ha_sunset=-acos((cos(90.833/deg2rad)/(cos(lat/deg2rad)*cos(decl)))-tan(lat/deg2rad)*tan(decl))*deg2rad
59 |
60 | #Then the UTC time of sunrise (or sunset) in minutes is:
61 | sunrise = 720-4*(lon+ha_sunrise)-eqtime
62 | sunset = 720-4*(lon+ha_sunset)-eqtime # was +, an error, corrected 30 April 2025, the error was about 2 minutes, but see major bug below
63 | hour_sunrise=int((floor(sunrise/60)+tz_offset) % 24)
64 | hour_sunset=int((floor(sunset/60)+tz_offset) % 24)
65 | min_sunrise=int(sunrise % 60)
66 | min_sunset=int(sunset % 60)
67 | print("{:02d}".format(hour_sunrise),":", "{:02d}".format(min_sunrise)," ", "{:02d}".format(hour_sunset),":", "{:02d}".format(min_sunset), sep='')
68 | # above print line had an error in that the print for min_sunset was printing the variable min_sunrise. Corrected 30 April 2025 G3ZIL
69 |
70 | except ValueError as e:
71 | if (('math domain error') in str (e)): # exception raised
72 | if (lat> 60) and (100 <= day_of_year <=260): # Northern hemisphere summer
73 | print('00:00 23:59') # it is light all day
74 | elif (lat >60) and (1 <= day_of_year <=80 or 280 <= day_of_year <366): # Northern hemisphere winter
75 | print('00:00 00:01') # it is dark all day
76 | elif (lat< -60) and (100 <= day_of_year <=260): # Southern hemisphere winter
77 | print('00:00 00:01') # it is dark all day
78 | elif (lat <-60) and (1 <= day_of_year <=80 or 280 <= day_of_year <366): # Southern hemisphere summer
79 | print('00:00 23:59') # it is light all day
80 | else:
81 | print ('Should never get here given latitude and day of year')
82 |
--------------------------------------------------------------------------------
/ts_batch_upload.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | #!/usr/bin/python
3 | # March-May 2020 Gwyn Griffiths
4 | # ts_batch_upload.py a program to read in a spots file scraped from wsprnet.org by scraper.sh and upload to a TimescaleDB
5 | # Version 1.2 May 2020 batch upload from a parsed file. Takes about 1.7s compared with 124s for line by line
6 | # that has been pre-formatted with an awk line to be in the right order and have single quotes around the time and character fields
7 | # Added additional diagnostics to identify which part of the upload fails (12 in 1936 times)
8 | import psycopg2 # This is the main connection tool, believed to be written in C
9 | import psycopg2.extras # This is needed for the batch upload functionality
10 | import csv # To import the csv file
11 | import sys # to get at command line argument with argv
12 | import argparse
13 | import logging
14 |
15 | # initially set the connection flag to be None
16 | conn=None
17 | connected="Not connected"
18 | cursor="No cursor"
19 | execute="Not executed"
20 | commit="Not committed"
21 | ret_code=0
22 |
23 | def ts_batch_upload(batch_file, sql, connect_info):
24 | global conn, connected, cursor, execute, commit, ret_code
25 | try:
26 | with batch_file as csv_file:
27 | csv_data = csv.reader(csv_file, delimiter=',')
28 | # connect to the PostgreSQL database
29 | logging.debug("Trying to connect")
30 | conn = psycopg2.connect(connect_info)
31 | connected = "Connected"
32 | logging.debug("Appear to have connected")
33 | # create a new cursor
34 | cur = conn.cursor()
35 | cursor = "Got cursor"
36 | # execute the INSERT statement
37 | psycopg2.extras.execute_batch(cur, sql, csv_data)
38 | execute = "Executed"
39 | logging.debug("After the execute")
40 | # commit the changes to the database
41 | conn.commit()
42 | commit = "Committed"
43 | # close communication with the database
44 | cur.close()
45 | logging.debug("%s %s %s %s" % (connected, cursor, execute, commit) )
46 | except:
47 | logging.error("Unable to record spot file to the database: %s %s %s %s" % (connected, cursor, execute, commit))
48 | ret_code=1
49 | finally:
50 | if conn is not None:
51 | conn.close()
52 | sys.exit(ret_code)
53 |
54 | if __name__ == "__main__":
55 | parser = argparse.ArgumentParser(description='Upload WSPRNET spots to Timescale DB')
56 | parser.add_argument("-i", "--input", dest="spotsFile", help="FILE is a CSV containing WSPRNET spots", metavar="FILE", required=True, nargs='?', type=argparse.FileType('r'), default=sys.stdin)
57 | parser.add_argument("-s", "--sql", dest="sqlFile", help="FILE is a SQL file containing an INSERT query", metavar="FILE", required=True, type=argparse.FileType('r'), default="insert-spots.sql")
58 | parser.add_argument("-a", "--address", dest="address", help="ADDRESS is the hostname of the Timescale DB", metavar="ADDRESS", required=False, default="localhost")
59 | parser.add_argument("-o", "--ip_port", dest="ip_port", help="The IP port of the Timescale DB", metavar="IPPORT", required=False, default="5432")
60 | parser.add_argument("-d", "--database", dest="database", help="DATABASE is the database name in Timescale DB", metavar="DATABASE", required=True, default="wsprnet")
61 | parser.add_argument("-u", "--username", dest="username", help="USERNAME is the username to use with Timescale DB", metavar="USERNAME", required=True, default="wsprnet")
62 | parser.add_argument("-p", "--password", dest="password", help="PASSWORD is the password to use with Timescale DB", metavar="PASSWORD", required=True, default="secret")
63 | parser.add_argument("--log", dest="log", help="The Python logging module's log level to use", type=lambda x: getattr(logging, x), required=False, default=logging.INFO)
64 | args = parser.parse_args()
65 |
66 | logging.basicConfig(level=args.log)
67 |
68 | with args.sqlFile as sql_file:
69 | sql = sql_file.read().strip()
70 |
71 | connect_info="dbname='%s' user='%s' host='%s' port='%s' password='%s'" % (args.database, args.username, args.address, args.ip_port, args.password)
72 | logging.debug(connect_info)
73 | ts_batch_upload(batch_file=args.spotsFile, sql=sql, connect_info=connect_info)
74 |
--------------------------------------------------------------------------------
/ts_insert_wd_noise.sql:
--------------------------------------------------------------------------------
1 | INSERT INTO wsprdaemon_noise_s (time, site, receiver, rx_grid, band, rms_level, c2_level, ov) VALUES (%s, %s, %s, %s, %s, %s, %s, %s)
2 |
--------------------------------------------------------------------------------
/ts_insert_wd_spots.sql:
--------------------------------------------------------------------------------
1 | INSERT INTO wsprdaemon_spots_s
2 | (time, sync_quality, "SNR", dt, freq, tx_call, tx_grid, "tx_dBm", drift, decode_cycles, jitter, blocksize, metric, osd_decode, ipass, nhardmin, mode, rms_noise, c2_noise, band, rx_grid, rx_id, km, rx_az, rx_lat, rx_lon, tx_az, tx_lat, tx_lon, v_lat, v_lon, ov_count, wsprnet_info, receiver )
3 | VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
4 |
5 |
--------------------------------------------------------------------------------
/ts_insert_wn_spots.sql:
--------------------------------------------------------------------------------
1 | INSERT INTO spots (wd_time, "Spotnum", "Date", "Reporter", "ReporterGrid", "dB", "MHz", "CallSign", "Grid", "Power", "Drift", distance, azimuth, "Band", version, code,
2 | wd_band, wd_c2_noise, wd_rms_noise, wd_rx_az, wd_rx_lat, wd_rx_lon, wd_tx_az, wd_tx_lat, wd_tx_lon, wd_v_lat, wd_v_lon )
3 | VALUES( %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s )
--------------------------------------------------------------------------------
/ts_noise.awk:
--------------------------------------------------------------------------------
1 | NF == 15 {
2 | no_head=FILENAME
3 |
4 | n = split (FILENAME, path_array, /\//)
5 | call_grid=path_array[n-3]
6 |
7 | split (call_grid, call_grid_array, "_")
8 | site=call_grid_array[1]
9 | gsub(/=/,"/",site)
10 | rx_grid=call_grid_array[2]
11 |
12 | receiver=path_array[4]
13 | band=path_array[5]
14 | time_freq=path_array[6]
15 |
16 | split (time_freq, time_freq_array, "_")
17 | date=time_freq_array[1]
18 | split (date,date_array,"")
19 | date_ts="20"date_array[1]date_array[2]"-"date_array[3]date_array[4]"-"date_array[5]date_array[6]
20 | time=time_freq_array[2]
21 | split (time, time_array,"")
22 | time_ts=time_array[1]time_array[2]":"time_array[3]time_array[4]
23 |
24 | rms_level=$13
25 | c2_level=$14
26 | ov=$15
27 | #printf "time='%s:%s' \nsite='%s' \nreceiver='%s' \nrx_grid='%s' \nband='%s' \nrms_level:'%s' \nc2_level:'%s' \nov='%s'\n", date_ts, time_ts, site, receiver, rx_grid, band, rms_level, c2_level, ov
28 | printf "%s:%s,%s,%s,%s,%s,%s,%s,%s\n", date_ts, time_ts, site, receiver, rx_grid, band, rms_level, c2_level, ov
29 | }
30 |
--------------------------------------------------------------------------------
/wav2grape.conf:
--------------------------------------------------------------------------------
1 | # config file for wav2grape
2 |
3 | [global]
4 | channel name = ch0
5 | subdir cadence secs = 86400
6 | file cadence millisecs = 3600000
7 | compression level = 9
8 |
9 | [subchannels]
10 | WWVB = 0.06
11 | WWV_2_5 = 2.5
12 | WWV_5 = 5
13 | WWV_10 = 10
14 | WWV_15 = 15
15 | WWV_20 = 20
16 | WWV_25 = 25
17 | CHU_3 = 3.33
18 | CHU_7 = 7.85
19 | CHU_14 = 14.67
20 | K_BEACON_5 = 5.1543
21 | K_BEACON_7_3 = 7.0393
22 | K_BEACON_7_4 = 7.0394
23 |
24 | # you can have additional metadata for a specific site and receiver
25 | #[AC0G_EM38ww]
26 | #site_property = abc
27 | #
28 | #[AC0G_EM38ww KA9Q_0_WWV_IQ]
29 | #receiver_property = def
30 |
31 | [KFS_SW]
32 | gpsdo = yes
33 |
34 |
--------------------------------------------------------------------------------
/wav2grape.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | # go from wav files to Grape:
3 | # - convert from wav to Digital RF dataset
4 | # - upload Digital RF dataset directory to PSWS network
5 | #
6 | # Franco Venturi - K4VZ - Mon 22 Jan 2024 01:20:14 PM UTC
7 | # Rob Robinett - AI6VN - Mon Jan 30 2024 modified to be part of a Wsprdaemon client
8 |
9 | set -euo pipefail
10 |
11 | WSPRDAEMON_ROOT_DIR=${WSPRDAEMONM_ROOT_DIR-~/wsprdaemon}
12 |
13 | declare -r PSWS_SERVER_URL='pswsnetwork.caps.ua.edu'
14 | declare -r UPLOAD_TO_PSWS_SERVER_COMPLETED_FILE_NAME='pswsnetwork_upload_completed'
15 | declare -r GRAPE_TMP_DIR="/run/user/$(id -u)/grape_drf_cache"
16 | declare -r WAV2GRAPE_PYTHON_CMD="${WSPRDAEMON_ROOT_DIR}/wav2grape.py"
17 |
18 |
19 | ### Given: the path to the .../wav-archive.d//_ directory under which there may be one or more receivers with 24 hour wav files which have not
20 | ### been converted to DRF and uploaded to the GRAPE server
21 | ### Returns: 0 on nothing to do or success on uploading
22 |
23 | declare WD_TEST_RX_DIR=~/wsprdaemon/wav-archive.d/20240128/KFS=Q_CM87tj/KA9Q_Omni_WWV_IQ@S000199_999
24 |
25 | function upload_24hour_wavs_to_grape_drf_server() {
26 | local reporter_wav_root_dir=$( realpath $1 )
27 | # reporter_wav_root_dir=${WD_TEST_RX_DIR}
28 |
29 | local reporter_upload_complete_file_name="${reporter_wav_root_dir}/${UPLOAD_TO_PSWS_SERVER_COMPLETED_FILE_NAME}"
30 |
31 | if [[ -f ${reporter_upload_complete_file_name} ]]; then
32 | echo "File ${reporter_upload_complete_file_name} exists, so upload of wav files has already been successful"
33 | return 0
34 | fi
35 | ### On the WD client the flac and 24hour.wav files are cached in the non-volitile file system which has the format:
36 | ### ...../wsprdaemon/wav-archive.d//_/@_/
37 | ### WSPR_REPORTER_ID, WSPR_REPORTER_GRID and WD_RECEIVER and WD_RECEIVER_NAME are assigned by the WD client and entered into the wsprdaemon.conf file
38 | ### Each WD client can support multiple WSPR_REPORTER_IDs, each of which can have the same or a unique WSPR_REPORTER_GRID
39 | ### Each WSPR_REPORTER_ID+WSPR_REPORTER_GRID is associated with one or more WSPR_RECIEVER_NAMEs, and each of those will support one or more BANDS
40 | ###
41 | local dir_path_list=( ${reporter_wav_root_dir//\// } )
42 | local wav_date=${dir_path_list[-2]}
43 | local reporter_info=${dir_path_list[-1]}
44 | local reporter_id=${reporter_info%_*} ### Chop off the _GRID to get the WSPR reporter id
45 | local reporter_grid=${reporter_info#*_} ### Chop off the REPROTER_ID to get the grid
46 |
47 | ### Search each receiver for wav files
48 | local receiver_dir
49 | local receiver_dir_list=( $(find "${reporter_wav_root_dir}" -mindepth 1 -maxdepth 1 -type d -not -name '*mutex.lock' | sort ) )
50 | if [[ ${#receiver_dir_list[@]} -eq 0 ]]; then
51 | echo "There are no receiver dirs under ${reporter_wav_root_dir}"
52 | return 1
53 | fi
54 | for receiver_dir in ${receiver_dir_list[@]} ; do
55 | local receiver_info="${receiver_dir##*/}"
56 | local receiver_name="${receiver_info%@*}"
57 | local pswsnetwork_info="${receiver_info#*@}"
58 | local psws_station_id="${pswsnetwork_info%_*}"
59 | local psws_instrument_id="${pswsnetwork_info#*_}"
60 | echo "Processing ${receiver_dir}:
61 | date: ${wav_date}- site: ${reporter_id} - receiver_name: $receiver_name - psws_station_id: $psws_station_id - psws_instrument_id: $psws_instrument_id" 1>&2
62 | rm -rf ${GRAPE_TMP_DIR}/*
63 | umask 022
64 | local receiver_tmp_dir="$("$WAV2GRAPE_PYTHON_CMD" -i "$receiver_dir" -o "$GRAPE_TMP_DIR")"
65 | echo "DRF files can be found in ${receiver_tmp_dir}. Now upload them"
66 | # upload to PSWS network
67 | (
68 | cd "$(dirname "$receiver_tmp_dir")"
69 | {
70 | echo "put -r .";
71 | echo "mkdir c$(basename "$receiver_tmp_dir")\#$psws_instrument_id\#$(date -u +%Y-%m-%dT%H-%M)";
72 | } | sftp -b - "$psws_station_id"@"$PSWS_SERVER_URL"
73 | )
74 | rm -r "$receiver_tmp_dir"
75 | done
76 | echo touch "${reporter_upload_complete_file_name}"
77 | }
78 |
79 | upload_24hour_wavs_to_grape_drf_server $1
80 |
--------------------------------------------------------------------------------
/wav_window.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding: utf-8 -*-
3 | # Filename: wav_window_v1.py
4 | # January 2020 Gwyn Griffiths
5 | # Program to apply a Hann window to a wsprdaemon wav file for subsequent processing by sox stat -freq (initially at least)
6 |
7 | from __future__ import print_function
8 | import math
9 | import scipy
10 | import scipy.io.wavfile as wavfile
11 | import numpy as np
12 | import wave
13 | import sys
14 |
15 | WAV_INPUT_FILENAME=sys.argv[1]
16 | WAV_OUTPUT_FILENAME=sys.argv[2]
17 |
18 | # Set up the audio file parameters for windowing
19 | # fs_rate is passed to the output file
20 | fs_rate, signal = wavfile.read(WAV_INPUT_FILENAME) # returns sample rate as int and data as numpy array
21 | # set some constants
22 | N_FFT=352 # this being the number expected
23 | N_FFT_POINTS=4096 # number of input samples in each sox stat -freq FFT (fixed)
24 | # so N_FFT * N_FFT_POINTS = 1441792 samples, which at 12000 samples per second is 120.15 seconds
25 | # while we have only 120 seconds, so for now operate with N_FFT-1 to have all filled
26 | # may decide all 352 are overkill anyway
27 | N=N_FFT*N_FFT_POINTS
28 | w=np.zeros(N_FFT_POINTS)
29 |
30 | output=np.zeros(N, dtype=np.int16) # declaring as dtype=np.int16 is critical as the wav file needs to be 16 bit integers
31 |
32 | # create a N_FFT_POINTS array with the Hann weighting function
33 | for i in range (0, N_FFT_POINTS):
34 | x=(math.pi*float(i))/float(N_FFT_POINTS)
35 | w[i]=np.sin(x)**2
36 |
37 | for j in range (0, N_FFT-1):
38 | offset=j*N_FFT_POINTS
39 | for i in range (0, N_FFT_POINTS):
40 | output[i+offset]=int(w[i]*signal[i+offset])
41 | wavfile.write(WAV_OUTPUT_FILENAME, fs_rate, output)
42 |
--------------------------------------------------------------------------------
/wd-grape-statistics.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | declare WD_PSWS_SITES=( S000042 S000108 S000109 S000110 S000111 S000113 S000114 S000115 S000116 S000117 S000119 S000120 S000121 S000123)
4 |
5 | function print-header() {
6 | if [[ "$1" == "header" ]]; then
7 | printf "Found ${#date_list[@]} dates in ${#WD_PSWS_SITES[@]} WD GRAPE Sites\n"
8 | fi
9 | printf "Date/Site#:"
10 | local site_id
11 | for site_id in ${WD_PSWS_SITES[@]}; do
12 | local site_num="${site_id: -3}"
13 | printf " %s" "${site_num}"
14 | done
15 | printf "\n"
16 | if [[ "$1" != "header" ]]; then
17 | printf "Found ${#date_list[@]} dates in ${#WD_PSWS_SITES[@]} WD GRAPE Sites\n"
18 | fi
19 |
20 | }
21 |
22 | function wd-statistics() {
23 | local site_home_list=( ${WD_PSWS_SITES[@]/#/..\/} )
24 | local date_list=( $(find ${site_home_list[@]} -mindepth 1 -maxdepth 1 -type d -name 'OBS*' -printf "%f\n" 2> /dev/null | sort -u ) )
25 |
26 | print-header "header"
27 | for obs_date in ${date_list[@]}; do
28 | printf "${obs_date:3:10}: "
29 | local site_home
30 | for site_home in ${site_home_list[@]}; do
31 | local dir_size=" "
32 | local obs_date_dir=${site_home}/${obs_date}
33 | if [[ -d ${obs_date_dir} ]]; then
34 | dir_size="$(du -sh ${obs_date_dir} | cut -f 1)"
35 | fi
36 | printf "%4s " "${dir_size}"
37 | #exit 1
38 | done
39 | printf "\n"
40 | done
41 | print-header "foot"
42 | }
43 |
44 | wd-statistics
45 |
--------------------------------------------------------------------------------
/wd_remote_access_daemon.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | #
3 | ### This function is executed by systemctl to start and stop a remote access connection in the ~/wsprdsaemon/bin directory
4 |
5 | declare FRPC_CMD=./frpc
6 | declare FRPC_INI_FILE=${FRPC_CMD}_wd.ini
7 | declare FRPC_ERROR_SLEEP_SECS=10 ### Wait this long after a connection fails before retrying the connection
8 |
9 | function wd_remote_access_daemon() {
10 | echo "Starting with args '$@'"
11 | if [[ "${1-}" == "-A" ]] ; then
12 | echo "Starting"
13 | local rc
14 | ${FRPC_CMD} -c ${FRPC_INI_FILE} &
15 | rc=$?
16 | if [[ ${rc} -ne 0 ]]; then
17 | echo "ERROR: '${FRPC_CMD} -c ${FRPC_INI_FILE} &' => ${rc}. Sleep ${FRPC_ERROR_SLEEP_SECS} and try to connect once again"
18 | fi
19 | else
20 | echo "Stopping"
21 | fi
22 | return 0
23 | }
24 |
25 | wd_remote_access_daemon $@
26 |
--------------------------------------------------------------------------------
/wd_spots_to_ts.awk:
--------------------------------------------------------------------------------
1 | #!/bin/awk
2 |
3 | BEGIN {
4 | ts_2_10_fields_count = split ( "date time sync_quality snr dt freq call grid pwr drift decode_cycles jitter blocksize metric osd_decode ipass nhardmin for_wsprnet rms_noise c2_noise band my_grid my_call_sign km rx_az rx_lat rx_lon tx_az tx_lat tx_lon v_lat v_lon", ts_2_10_field_names, " ")
5 | ts_3_0_fields_count = split ( "date time sync_quality snr dt freq call grid pwr drift decode_cycles jitter blocksize metric osd_decode ipass nhardmin for_wsprnet rms_noise c2_noise band my_grid my_call_sign km rx_az rx_lat rx_lon tx_az tx_lat tx_lon v_lat v_lon overload_counts pkt_mode", ts_3_0_field_names, " ")
6 | }
7 |
8 | function print_spot_line_fields (spot_line) {
9 | spot_line_count = split( spot_line, spot_line_array, " ")
10 | if ( spot_line_count == ts_2_10_fields_count ) {
11 | for ( i = 1; i <= ts_2_10_fields_count; ++ i ) {
12 | printf ( "#%2d: %15s: %s\n", i, ts_2_10_field_names[i], spot_line_array[i] )
13 | }
14 | return
15 | }
16 | if ( spot_line_count == ts_3_0_fields_count ) {
17 | for ( i = 1; i <= ts_3_0_fields_count; ++ i ) {
18 | printf ( "#%2d: %15s: %s\n", i, ts_3_0_field_names[i], spot_line_array[i] )
19 | }
20 | return
21 | }
22 | printf ( "ERROR: spot line has %d fields, not the expected %d fields: '%s'\n", spot_line_count, ts_2_10_fields_count, spot_line)
23 | return
24 | }
25 |
26 | ### Process 32 filed WD 2.x spot lines and 34 field WD 3.0 spot lines into the CSV line format expected by TS
27 |
28 | NF != 32 && NF != 34 {
29 | printf( "ERROR: file %s spot line has %d fields, not the expected 32 or 34 fields: '%s\n'", FILENAME, NF, $0)
30 | }
31 |
32 | NF == 32 || NF == 34 {
33 | field_count = split($0, fields, " ")
34 | if ( NF == 32 ) {
35 | if ( fields[18] == 0 ) {
36 | fields[18] = 2 ### In WD 2.x this field was a placeholder and always set to zero. Set is to the value for 'WSPR-2' pkt mode, since that is the only pkt mode decoded by WD 2.x
37 | } else {
38 | ### In 2.10 some or all of the type 2 spots were incorrectly formatted where tx 'grid' was 'none', the next field 'tpwr' held that grid, and the following fields were similarly off by one
39 | ### This code attempts to detect and clean up such malformed lines
40 | if ( verbosity > 1 ) print_spot_line_fields($0)
41 | if ( verbosity > 0 ) printf ("ERROR: found unexpected 'pkt_mode' value '%s' instead of the expected value '0' in spot file %s spot line:\n%s\n", fields[18], FILENAME, $0 )
42 | for ( i = 8; i <= 19; ++i ) {
43 | if ( verbosity > 1 ) printf ("Move field #%2d (%s) to field #%2d\n", i+1, fields[i+1], i)
44 | fields[i] = fields[i+1]
45 | }
46 | ### In 2.10 the RMS noise field is lost and C2_noise may contain RMS or C2 data. So make both noise levels the same
47 | fields[18] = 2 ### In 2.10 this field was 'for_wsprnet' but in 3.0 is is 'pkt_mode' and all 2.10 spots are WSPR-2
48 | }
49 | fields[++field_count] = 0 ### 'ov_count' : There is no overload counts information in WD 2.x spot lines
50 | fields[++field_count] = 0 ### 'wsprnet_info' : This field may be used in WD 3.0 to signal that the server should 'proxy upload' this spot to wsprnet.org
51 |
52 | if ( fields[9] != int(fields[9]) ) {
53 | ### Some older versions of WD produce corrupt lines. Don't record them
54 | printf ("ERROR: ")
55 | }
56 | }
57 | ### Create the spot time in the TS format: "20YY-MM-DD:HH:MM"
58 | wd_year = substr(fields[1], 1, 2)
59 | wd_month = substr(fields[1], 3, 2)
60 | wd_day = substr(fields[1], 5, 2)
61 | wd_hour = substr(fields[2], 1, 2)
62 | wd_min = substr(fields[2], 3, 2)
63 | ts_time = ( "20" wd_year "-" wd_month "-" wd_day ":" wd_hour ":" wd_min )
64 |
65 | fields[3] = int ( fields[3] * 100 ) ### ALL_WSPR.TXT reports sync_quality as a float (0.NN), but we have defined that sync field as a int in TS
66 |
67 | printf( "\"%s\"", ts_time )
68 |
69 | fields[7] = toupper(fields[7])
70 | fields[8] = ( toupper(substr(fields[8], 1, 2)) substr(fields[8], 3, 2) tolower(substr(fields[8], 5, 2)) )
71 | fields[23] = toupper(fields[23])
72 | fields[22] = ( toupper(substr(fields[22], 1, 2)) substr(fields[22], 3, 2) tolower(substr(fields[22], 5, 2)) )
73 |
74 | file_path_count = split ( FILENAME, file_path_array, "/" )
75 | rx_name = file_path_array[ file_path_count - 2]
76 | fields[++field_count] = rx_name ### Taken from path to the file which contains this spot line
77 |
78 | for ( i = 3; i <= field_count; ++ i) {
79 | printf ( ",%s", fields[i])
80 | }
81 | printf "\n"
82 | }
83 |
--------------------------------------------------------------------------------
/wd_version.txt:
--------------------------------------------------------------------------------
1 | 3.3.2
2 |
--------------------------------------------------------------------------------
/wn_from_wd_spot_file.awk:
--------------------------------------------------------------------------------
1 | #!/bin/awk
2 |
3 | ### This awk script takes a file of 34 field WD extended spot lines and output 11 field wsprnet batch upload spot lines
4 | ### Doing that requries moving the 'sync_quality' to field 3 and transforming the 'pkt_mode' field in field $18 of extended spots to a subset in field 11 of the WN spot line
5 |
6 | NF != 34 {
7 | printf ("ERROR: WD spot file %s has %d fields instead of the expected 34 fields\n", FILENAME, NF )
8 | }
9 | NF == 34 {
10 | if ( $8 == "none" ) {
11 | $8 = " "
12 | }
13 | wd_pkt_mode = $18
14 | if ( wd_pkt_mode == 2 ) ### produced by 'wsprd'
15 | wn_pkt_mode = 2 ### WSPR-2
16 | else if ( wd_pkt_mode == 15 ) ### produced by 'wsprd'
17 | wn_pkt_mode = 15 ### WSPR-15
18 | else if ( wd_pkt_mode == 3 ) ### added by WD to the lines produced by 'jt9'
19 | wn_pkt_mode = 3 ### FST4W-120
20 | else if ( wd_pkt_mode == 6 ) ### added by WD to the lines produced by 'jt9'
21 | wn_pkt_mode = 5 ### FST4W-300
22 | else if ( wd_pkt_mode == 16 ) ### added by WD to the lines produced by 'jt9'
23 | wn_pkt_mode = 15 ### FST4W-900
24 | else if ( wd_pkt_mode == 31 ) ### added by WD to the lines produced by 'jt9'
25 | wn_pkt_mode = 30 ### FST4W-1800
26 | else {
27 | wn_pkt_mod= 2
28 | printf ("ERROR: WD spot line has pkt_mode = '%s', not one of the expected 2/3/5/15/16/30 values: ", wd_pkt_mode)
29 | }
30 | printf ( "%6s %4s %5.2f %3d %5.2f %12.7f %-14s %-6s %2d %2d %4d\n", $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, wn_pkt_mode)
31 | }
32 |
--------------------------------------------------------------------------------
/wspr.rotate:
--------------------------------------------------------------------------------
1 | /var/log/wspr.log
2 | {
3 | rotate 10
4 | daily
5 | missingok
6 | notifempty
7 | compress
8 | delaycompress
9 | copytruncate
10 | }
11 |
--------------------------------------------------------------------------------
/wsprnet-scraper.awk:
--------------------------------------------------------------------------------
1 | #!/bin/awk
2 |
3 | ### Filter and convert spots repored by the wsprnet.org API into a csv file which will be recorded in the TS and CH databases
4 | ###
5 | ### The calling command line is expected to define the awk variable spot_epoch:
6 | ### awk -v spot_epoch=${spot_epoch} -f ${WSPRDAEMON_ROOT_DIR}/wsprnet-scraper.awk <<< "${sorted_lines}" > ${WSPRNET_SCRAPER_TMP_PATH}/filtered_spots.csv
7 |
8 | BEGIN {
9 | FS = ","
10 | OFS = ","
11 | spot_minute = ( ( spot_epoch % 3600 ) / 60 )
12 | spot_date = strftime( "%Y-%m-%d:%H:%M", spot_epoch )
13 |
14 | is_odd_minute = ( spot_minute % 2 )
15 | if ( is_odd_minute == 0 ) {
16 | fixed_spot_epoch = spot_epoch
17 | } else {
18 | fixed_spot_epoch = spot_epoch + 60
19 | }
20 | fixed_date = strftime( "%Y-%m-%d:%H:%M", fixed_spot_epoch )
21 |
22 | print_diags = 0
23 | if ( print_diags == 1 ) {
24 | if ( is_odd_minute == 0 ) {
25 | printf ( "Finding spots with epoch %d which is at even minute %d == '%s'\n", spot_epoch, spot_minute, fixed_date)
26 | } else {
27 | printf ( "Finding spots with epoch %d which is at odd minute %d and if mode is invalid change the spot to the next even minute epoch %d == '%s'\n", spot_epoch, spot_minute, fixed_spot_epoch, fixed_date)
28 | }
29 | }
30 |
31 | for ( i = 0; i < 60; i += 2 ) { valid_mode_1_minute[i] = 1 } ## WSPR-2 valid minutes
32 | for ( i = 0; i < 60; i += 2 ) { valid_mode_3_minute[i] = 1 } ## FST4W-120 valid minutes
33 | for ( i = 0; i < 60; i += 5 ) { valid_mode_4_minute[i] = 1 } ## FST4W-300 valid minutes
34 | for ( i = 0; i < 60; i += 15 ) { valid_mode_2_minute[i] = 1 } ## FST4W-900 valid minutes
35 | for ( i = 0; i < 60; i += 30 ) { valid_mode_8_minute[i] = 1 } ## FST4W-1800 valid minutes
36 |
37 | if ( valid_mode_1_minute[spot_minute] == 1 ) { is_valid_mode_1_minute = 1 }
38 | if ( valid_mode_3_minute[spot_minute] == 1 ) { is_valid_mode_3_minute = 1 }
39 | if ( valid_mode_4_minute[spot_minute] == 1 ) { is_valid_mode_4_minute = 1 }
40 | if ( valid_mode_2_minute[spot_minute] == 1 ) { is_valid_mode_2_minute = 1 }
41 | if ( valid_mode_8_minute[spot_minute] == 1 ) { is_valid_mode_8_minute = 1 }
42 | }
43 |
44 | $2 == spot_epoch {
45 | found_valid_mode = 1
46 | found_valid_minute = 1
47 | if ( $15 == 1 ) {
48 | spot_length = 2 ### Mode 1 spots (2 minute long WSPR) can happen only on even minutes
49 | if ( is_valid_mode_1_minute == 0 ) {
50 | found_valid_minute = 0
51 | }
52 | } else if ( $15 == 3 ) {
53 | spot_length = 2 ### Mode 1 spots (2 minute long FST4W-120) can happen only on even minutes
54 | if ( is_valid_mode_3_minute == 0 ) {
55 | found_valid_minute = 0
56 | }
57 | } else if ( $15 == 2 ) {
58 | spot_length = 15 ### Mode 2 spots (15 minute long WSPR and FST4W) can happen on both even and odd minutes
59 | if ( is_valid_mode_2_minute == 0 ) {
60 | found_valid_minute = 0
61 | }
62 | } else if ( $15 == 4 ) {
63 | spot_length = 5 ### Mode 4 spots (5 minute long FST4W) can happen on both even and odd minutes
64 | if ( is_valid_mode_4_minute == 0 ) {
65 | found_valid_minute = 0
66 | }
67 | } else if ( $15 == 8 ) {
68 | spot_length = 30 ### Mode 8 spots (30 minute long FST4W) can happen only on even minutess
69 | if ( is_valid_mode_8_minute == 0 ) {
70 | found_valid_minute = 0
71 | }
72 | } else {
73 | found_valid_mode = 0
74 | }
75 |
76 | if ( found_valid_mode == 1) {
77 | if ( found_valid_minute == 1) {
78 | printf ( "%s,%s\n", spot_date, $0 ) ### This should be the case for the vast majority of spots
79 | } else {
80 | ### Mode is valid, but minute is not valid
81 | if ( is_odd_minute == 0 ) {
82 | ### Leave unchanged time of valid modes at invalid even minutes
83 | printf ("Found valid mode %2d == %2d minute long spot at invalid even minute %2d: %s\n", $15, spot_length, spot_minute, $0 )
84 | printf ( "%s,%s\n", spot_date, $0 )
85 | } else {
86 | $2 = fixed_spot_epoch
87 | printf ("Found valid mode %2d == %2d minute long spot at invalid odd minute %2d and fixed it to %d '%s': %s\n", $15, spot_length, spot_minute, fixed_spot_epoch, fixed_date, $0 )
88 | printf ( "%s,%s\n", fixed_date, $0 )
89 | }
90 | }
91 | } else {
92 | ### Mode is invalid, so always force to even minute
93 | if ( is_odd_minute == 0 ) {
94 | printf ("Found invalid mode %2d == %2d minute long spot at even minute %2d: %s\n", $15, spot_length, spot_minute, $0 )
95 | printf ( "%s,%s\n", spot_date, $0 ) ### Leave unchanged even minute bad mode spots
96 | } else {
97 | $2 = fixed_spot_epoch
98 | printf ("Found invalid mode %2d spot at odd minute %2d and fixed it to %d '%s': %s\n", $15, spot_minute, fixed_spot_epoch, fixed_date, $0 )
99 | printf ( "%s,%s\n", fixed_date, $0 )
100 | }
101 | }
102 | }
103 |
--------------------------------------------------------------------------------
/wwv_start.py:
--------------------------------------------------------------------------------
1 | #!/home/wsprdaemon/wsprdaemon/venv/bin/python3
2 |
3 | import numpy as np
4 | import os
5 | import soundfile as sf
6 | import sys
7 | from pathlib import Path
8 | from scipy import signal
9 | import re
10 |
11 | def cross_file(filename):
12 | # look for 0.8 seconds of 1 kHz (eventually, also 1.5 kHz at top of hour)
13 |
14 | # determine tone burst frequency from filename, if possible
15 | # expects a filename such as 20250405T044300Z_5000000_iq.wav
16 | tone = 1000
17 | regex_pattern = r'(\d{8})T(\d{2})(\d{2})(\d{2})Z_(\d+)_([a-z]+).wav'
18 | match = re.search(regex_pattern, filename)
19 | if match:
20 | if int(match.group(3)) == 0:
21 | # top of hour, 1500 Hz instead of 1000 Hz
22 | tone = 1500
23 |
24 | # read first 3 seconds from wav file
25 | wav_sample_rate = sf.info(filename).samplerate
26 | samples, wav_sample_rate = sf.read(filename, frames = (3 * wav_sample_rate))
27 |
28 | # Convert to a 1d array of complex values
29 | samples_c = samples.view(dtype = np.complex128)
30 |
31 | # demod, remove DC offset, convert to 1d array
32 | wav_amp = np.abs(samples_c)
33 | wav_demod = wav_amp - np.mean(wav_amp);
34 | wav_demod = wav_demod.squeeze()
35 |
36 | # generate demod wav file for testing
37 | # sf.write('demod.wav', wav_demod, wav_sample_rate, subtype="FLOAT")
38 |
39 | # create 0.8 seconds of sine wav at 1 kHz
40 | x = np.linspace(0, 0.8, int(0.8 * wav_sample_rate))
41 | beep = 0.05 * np.sin(2 * x * np.pi * tone)
42 |
43 | # normalize amplitudes
44 | beep = (beep - np.mean(beep)) / np.std(beep)
45 | wav_demod = (wav_demod - np.mean(wav_demod)) / np.std(wav_demod)
46 |
47 | # cross corr
48 | corr = signal.correlate(wav_demod, beep, mode='full', method='fft')
49 | lags = signal.correlation_lags(len(wav_demod), len(beep), mode='full')
50 |
51 | peak = np.argmax(corr)
52 | wav_peak = lags[peak];
53 | peak_value = corr[peak]
54 |
55 | print(f'{1000.0 * (wav_peak / wav_sample_rate):.2f} ms')
56 | return
57 |
58 | def main():
59 | cross_file(sys.argv[1])
60 |
61 | if __name__ == '__main__':
62 | main()
63 |
--------------------------------------------------------------------------------