not1mm 24.8.27__tar.gz → 24.9.5__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (163) hide show
  1. {not1mm-24.8.27 → not1mm-24.9.5}/PKG-INFO +13 -10
  2. {not1mm-24.8.27 → not1mm-24.9.5}/README.md +12 -9
  3. {not1mm-24.8.27 → not1mm-24.9.5}/not1mm/__main__.py +108 -54
  4. {not1mm-24.8.27 → not1mm-24.9.5}/not1mm/lib/database.py +2 -16
  5. not1mm-24.9.5/not1mm/lib/fldigi_watcher.py +32 -0
  6. {not1mm-24.8.27 → not1mm-24.9.5}/not1mm/lib/ft8_watcher.py +5 -1
  7. {not1mm-24.8.27 → not1mm-24.9.5}/not1mm/lib/version.py +1 -1
  8. {not1mm-24.8.27 → not1mm-24.9.5}/not1mm/plugins/arrl_field_day.py +43 -8
  9. {not1mm-24.8.27 → not1mm-24.9.5}/not1mm/plugins/cq_160_cw.py +2 -2
  10. {not1mm-24.8.27 → not1mm-24.9.5}/not1mm/plugins/cq_160_ssb.py +2 -2
  11. {not1mm-24.8.27 → not1mm-24.9.5}/not1mm/plugins/helvetia.py +2 -2
  12. not1mm-24.9.5/not1mm/plugins/ref_cw.py +502 -0
  13. not1mm-24.9.5/not1mm/test.py +6 -0
  14. {not1mm-24.8.27 → not1mm-24.9.5}/not1mm.egg-info/PKG-INFO +13 -10
  15. {not1mm-24.8.27 → not1mm-24.9.5}/not1mm.egg-info/SOURCES.txt +2 -0
  16. {not1mm-24.8.27 → not1mm-24.9.5}/pyproject.toml +1 -1
  17. not1mm-24.8.27/not1mm/test.py +0 -13
  18. {not1mm-24.8.27 → not1mm-24.9.5}/LICENSE +0 -0
  19. {not1mm-24.8.27 → not1mm-24.9.5}/not1mm/__init__.py +0 -0
  20. {not1mm-24.8.27 → not1mm-24.9.5}/not1mm/bandmap.py +0 -0
  21. {not1mm-24.8.27 → not1mm-24.9.5}/not1mm/checkwindow.py +0 -0
  22. {not1mm-24.8.27 → not1mm-24.9.5}/not1mm/data/JetBrainsMono-Regular.ttf +0 -0
  23. {not1mm-24.8.27 → not1mm-24.9.5}/not1mm/data/MASTER.SCP +0 -0
  24. {not1mm-24.8.27 → not1mm-24.9.5}/not1mm/data/about.ui +0 -0
  25. {not1mm-24.8.27 → not1mm-24.9.5}/not1mm/data/alpha bravo charlie delta.txt +0 -0
  26. {not1mm-24.8.27 → not1mm-24.9.5}/not1mm/data/bandmap.ui +0 -0
  27. {not1mm-24.8.27 → not1mm-24.9.5}/not1mm/data/check.png +0 -0
  28. {not1mm-24.8.27 → not1mm-24.9.5}/not1mm/data/checkwindow.ui +0 -0
  29. {not1mm-24.8.27 → not1mm-24.9.5}/not1mm/data/configuration.ui +0 -0
  30. {not1mm-24.8.27 → not1mm-24.9.5}/not1mm/data/contests.sql +0 -0
  31. {not1mm-24.8.27 → not1mm-24.9.5}/not1mm/data/cty.json +0 -0
  32. {not1mm-24.8.27 → not1mm-24.9.5}/not1mm/data/cwmacros.txt +0 -0
  33. {not1mm-24.8.27 → not1mm-24.9.5}/not1mm/data/donors.html +0 -0
  34. {not1mm-24.8.27 → not1mm-24.9.5}/not1mm/data/editcontact.ui +0 -0
  35. {not1mm-24.8.27 → not1mm-24.9.5}/not1mm/data/editmacro.ui +0 -0
  36. {not1mm-24.8.27 → not1mm-24.9.5}/not1mm/data/greendot.png +0 -0
  37. {not1mm-24.8.27 → not1mm-24.9.5}/not1mm/data/k6gte-not1mm.desktop +0 -0
  38. {not1mm-24.8.27 → not1mm-24.9.5}/not1mm/data/k6gte.not1mm-128.png +0 -0
  39. {not1mm-24.8.27 → not1mm-24.9.5}/not1mm/data/k6gte.not1mm-32.png +0 -0
  40. {not1mm-24.8.27 → not1mm-24.9.5}/not1mm/data/k6gte.not1mm-64.png +0 -0
  41. {not1mm-24.8.27 → not1mm-24.9.5}/not1mm/data/logwindow.ui +0 -0
  42. {not1mm-24.8.27 → not1mm-24.9.5}/not1mm/data/logwindowx.ui +0 -0
  43. {not1mm-24.8.27 → not1mm-24.9.5}/not1mm/data/main.ui +0 -0
  44. {not1mm-24.8.27 → not1mm-24.9.5}/not1mm/data/new_contest.ui +0 -0
  45. {not1mm-24.8.27 → not1mm-24.9.5}/not1mm/data/not1mm.html +0 -0
  46. {not1mm-24.8.27 → not1mm-24.9.5}/not1mm/data/opon.ui +0 -0
  47. {not1mm-24.8.27 → not1mm-24.9.5}/not1mm/data/phonetics/0.wav +0 -0
  48. {not1mm-24.8.27 → not1mm-24.9.5}/not1mm/data/phonetics/1.wav +0 -0
  49. {not1mm-24.8.27 → not1mm-24.9.5}/not1mm/data/phonetics/2.wav +0 -0
  50. {not1mm-24.8.27 → not1mm-24.9.5}/not1mm/data/phonetics/3.wav +0 -0
  51. {not1mm-24.8.27 → not1mm-24.9.5}/not1mm/data/phonetics/4.wav +0 -0
  52. {not1mm-24.8.27 → not1mm-24.9.5}/not1mm/data/phonetics/5.wav +0 -0
  53. {not1mm-24.8.27 → not1mm-24.9.5}/not1mm/data/phonetics/6.wav +0 -0
  54. {not1mm-24.8.27 → not1mm-24.9.5}/not1mm/data/phonetics/7.wav +0 -0
  55. {not1mm-24.8.27 → not1mm-24.9.5}/not1mm/data/phonetics/73.wav +0 -0
  56. {not1mm-24.8.27 → not1mm-24.9.5}/not1mm/data/phonetics/8.wav +0 -0
  57. {not1mm-24.8.27 → not1mm-24.9.5}/not1mm/data/phonetics/9.wav +0 -0
  58. {not1mm-24.8.27 → not1mm-24.9.5}/not1mm/data/phonetics/a.wav +0 -0
  59. {not1mm-24.8.27 → not1mm-24.9.5}/not1mm/data/phonetics/again.wav +0 -0
  60. {not1mm-24.8.27 → not1mm-24.9.5}/not1mm/data/phonetics/b.wav +0 -0
  61. {not1mm-24.8.27 → not1mm-24.9.5}/not1mm/data/phonetics/c.wav +0 -0
  62. {not1mm-24.8.27 → not1mm-24.9.5}/not1mm/data/phonetics/contest.wav +0 -0
  63. {not1mm-24.8.27 → not1mm-24.9.5}/not1mm/data/phonetics/cq.wav +0 -0
  64. {not1mm-24.8.27 → not1mm-24.9.5}/not1mm/data/phonetics/d.wav +0 -0
  65. {not1mm-24.8.27 → not1mm-24.9.5}/not1mm/data/phonetics/e.wav +0 -0
  66. {not1mm-24.8.27 → not1mm-24.9.5}/not1mm/data/phonetics/f.wav +0 -0
  67. {not1mm-24.8.27 → not1mm-24.9.5}/not1mm/data/phonetics/g.wav +0 -0
  68. {not1mm-24.8.27 → not1mm-24.9.5}/not1mm/data/phonetics/h.wav +0 -0
  69. {not1mm-24.8.27 → not1mm-24.9.5}/not1mm/data/phonetics/i.wav +0 -0
  70. {not1mm-24.8.27 → not1mm-24.9.5}/not1mm/data/phonetics/j.wav +0 -0
  71. {not1mm-24.8.27 → not1mm-24.9.5}/not1mm/data/phonetics/k.wav +0 -0
  72. {not1mm-24.8.27 → not1mm-24.9.5}/not1mm/data/phonetics/k6gte.wav +0 -0
  73. {not1mm-24.8.27 → not1mm-24.9.5}/not1mm/data/phonetics/l.wav +0 -0
  74. {not1mm-24.8.27 → not1mm-24.9.5}/not1mm/data/phonetics/m.wav +0 -0
  75. {not1mm-24.8.27 → not1mm-24.9.5}/not1mm/data/phonetics/mynumber.wav +0 -0
  76. {not1mm-24.8.27 → not1mm-24.9.5}/not1mm/data/phonetics/n.wav +0 -0
  77. {not1mm-24.8.27 → not1mm-24.9.5}/not1mm/data/phonetics/nil.wav +0 -0
  78. {not1mm-24.8.27 → not1mm-24.9.5}/not1mm/data/phonetics/o.wav +0 -0
  79. {not1mm-24.8.27 → not1mm-24.9.5}/not1mm/data/phonetics/p.wav +0 -0
  80. {not1mm-24.8.27 → not1mm-24.9.5}/not1mm/data/phonetics/q.wav +0 -0
  81. {not1mm-24.8.27 → not1mm-24.9.5}/not1mm/data/phonetics/r.wav +0 -0
  82. {not1mm-24.8.27 → not1mm-24.9.5}/not1mm/data/phonetics/roger.wav +0 -0
  83. {not1mm-24.8.27 → not1mm-24.9.5}/not1mm/data/phonetics/s.wav +0 -0
  84. {not1mm-24.8.27 → not1mm-24.9.5}/not1mm/data/phonetics/space.wav +0 -0
  85. {not1mm-24.8.27 → not1mm-24.9.5}/not1mm/data/phonetics/t.wav +0 -0
  86. {not1mm-24.8.27 → not1mm-24.9.5}/not1mm/data/phonetics/thankyou.wav +0 -0
  87. {not1mm-24.8.27 → not1mm-24.9.5}/not1mm/data/phonetics/thankyouqrz.wav +0 -0
  88. {not1mm-24.8.27 → not1mm-24.9.5}/not1mm/data/phonetics/u.wav +0 -0
  89. {not1mm-24.8.27 → not1mm-24.9.5}/not1mm/data/phonetics/v.wav +0 -0
  90. {not1mm-24.8.27 → not1mm-24.9.5}/not1mm/data/phonetics/w.wav +0 -0
  91. {not1mm-24.8.27 → not1mm-24.9.5}/not1mm/data/phonetics/x.wav +0 -0
  92. {not1mm-24.8.27 → not1mm-24.9.5}/not1mm/data/phonetics/y.wav +0 -0
  93. {not1mm-24.8.27 → not1mm-24.9.5}/not1mm/data/phonetics/yourcall.wav +0 -0
  94. {not1mm-24.8.27 → not1mm-24.9.5}/not1mm/data/phonetics/z.wav +0 -0
  95. {not1mm-24.8.27 → not1mm-24.9.5}/not1mm/data/pickcontest.ui +0 -0
  96. {not1mm-24.8.27 → not1mm-24.9.5}/not1mm/data/radio_green.png +0 -0
  97. {not1mm-24.8.27 → not1mm-24.9.5}/not1mm/data/radio_grey.png +0 -0
  98. {not1mm-24.8.27 → not1mm-24.9.5}/not1mm/data/radio_red.png +0 -0
  99. {not1mm-24.8.27 → not1mm-24.9.5}/not1mm/data/reddot.png +0 -0
  100. {not1mm-24.8.27 → not1mm-24.9.5}/not1mm/data/settings.ui +0 -0
  101. {not1mm-24.8.27 → not1mm-24.9.5}/not1mm/data/splash.png +0 -0
  102. {not1mm-24.8.27 → not1mm-24.9.5}/not1mm/data/ssbmacros.txt +0 -0
  103. {not1mm-24.8.27 → not1mm-24.9.5}/not1mm/data/vfo.ui +0 -0
  104. {not1mm-24.8.27 → not1mm-24.9.5}/not1mm/fsutils.py +0 -0
  105. {not1mm-24.8.27 → not1mm-24.9.5}/not1mm/lib/__init__.py +0 -0
  106. {not1mm-24.8.27 → not1mm-24.9.5}/not1mm/lib/about.py +0 -0
  107. {not1mm-24.8.27 → not1mm-24.9.5}/not1mm/lib/cat_interface.py +0 -0
  108. {not1mm-24.8.27 → not1mm-24.9.5}/not1mm/lib/cwinterface.py +0 -0
  109. {not1mm-24.8.27 → not1mm-24.9.5}/not1mm/lib/edit_contact.py +0 -0
  110. {not1mm-24.8.27 → not1mm-24.9.5}/not1mm/lib/edit_macro.py +0 -0
  111. {not1mm-24.8.27 → not1mm-24.9.5}/not1mm/lib/edit_opon.py +0 -0
  112. {not1mm-24.8.27 → not1mm-24.9.5}/not1mm/lib/edit_station.py +0 -0
  113. {not1mm-24.8.27 → not1mm-24.9.5}/not1mm/lib/ham_utility.py +0 -0
  114. {not1mm-24.8.27 → not1mm-24.9.5}/not1mm/lib/lookup.py +0 -0
  115. {not1mm-24.8.27 → not1mm-24.9.5}/not1mm/lib/multicast.py +0 -0
  116. {not1mm-24.8.27 → not1mm-24.9.5}/not1mm/lib/n1mm.py +0 -0
  117. {not1mm-24.8.27 → not1mm-24.9.5}/not1mm/lib/new_contest.py +0 -0
  118. {not1mm-24.8.27 → not1mm-24.9.5}/not1mm/lib/playsound.py +0 -0
  119. {not1mm-24.8.27 → not1mm-24.9.5}/not1mm/lib/plugin_common.py +0 -0
  120. {not1mm-24.8.27 → not1mm-24.9.5}/not1mm/lib/select_contest.py +0 -0
  121. {not1mm-24.8.27 → not1mm-24.9.5}/not1mm/lib/settings.py +0 -0
  122. {not1mm-24.8.27 → not1mm-24.9.5}/not1mm/lib/super_check_partial.py +0 -0
  123. {not1mm-24.8.27 → not1mm-24.9.5}/not1mm/lib/versiontest.py +0 -0
  124. {not1mm-24.8.27 → not1mm-24.9.5}/not1mm/logwindow.py +0 -0
  125. {not1mm-24.8.27 → not1mm-24.9.5}/not1mm/plugins/10_10_fall_cw.py +0 -0
  126. {not1mm-24.8.27 → not1mm-24.9.5}/not1mm/plugins/10_10_spring_cw.py +0 -0
  127. {not1mm-24.8.27 → not1mm-24.9.5}/not1mm/plugins/10_10_summer_phone.py +0 -0
  128. {not1mm-24.8.27 → not1mm-24.9.5}/not1mm/plugins/10_10_winter_phone.py +0 -0
  129. {not1mm-24.8.27 → not1mm-24.9.5}/not1mm/plugins/__init__.py +0 -0
  130. {not1mm-24.8.27 → not1mm-24.9.5}/not1mm/plugins/arrl_10m.py +0 -0
  131. {not1mm-24.8.27 → not1mm-24.9.5}/not1mm/plugins/arrl_dx_cw.py +0 -0
  132. {not1mm-24.8.27 → not1mm-24.9.5}/not1mm/plugins/arrl_dx_ssb.py +0 -0
  133. {not1mm-24.8.27 → not1mm-24.9.5}/not1mm/plugins/arrl_rtty_ru.py +0 -0
  134. {not1mm-24.8.27 → not1mm-24.9.5}/not1mm/plugins/arrl_ss_cw.py +0 -0
  135. {not1mm-24.8.27 → not1mm-24.9.5}/not1mm/plugins/arrl_ss_phone.py +0 -0
  136. {not1mm-24.8.27 → not1mm-24.9.5}/not1mm/plugins/arrl_vhf_jan.py +0 -0
  137. {not1mm-24.8.27 → not1mm-24.9.5}/not1mm/plugins/arrl_vhf_jun.py +0 -0
  138. {not1mm-24.8.27 → not1mm-24.9.5}/not1mm/plugins/arrl_vhf_sep.py +0 -0
  139. {not1mm-24.8.27 → not1mm-24.9.5}/not1mm/plugins/canada_day.py +0 -0
  140. {not1mm-24.8.27 → not1mm-24.9.5}/not1mm/plugins/cq_wpx_cw.py +0 -0
  141. {not1mm-24.8.27 → not1mm-24.9.5}/not1mm/plugins/cq_wpx_ssb.py +0 -0
  142. {not1mm-24.8.27 → not1mm-24.9.5}/not1mm/plugins/cq_ww_cw.py +0 -0
  143. {not1mm-24.8.27 → not1mm-24.9.5}/not1mm/plugins/cq_ww_ssb.py +0 -0
  144. {not1mm-24.8.27 → not1mm-24.9.5}/not1mm/plugins/cwt.py +0 -0
  145. {not1mm-24.8.27 → not1mm-24.9.5}/not1mm/plugins/general_logging.py +0 -0
  146. {not1mm-24.8.27 → not1mm-24.9.5}/not1mm/plugins/iaru_hf.py +0 -0
  147. {not1mm-24.8.27 → not1mm-24.9.5}/not1mm/plugins/icwc_mst.py +0 -0
  148. {not1mm-24.8.27 → not1mm-24.9.5}/not1mm/plugins/jidx_cw.py +0 -0
  149. {not1mm-24.8.27 → not1mm-24.9.5}/not1mm/plugins/jidx_ph.py +0 -0
  150. {not1mm-24.8.27 → not1mm-24.9.5}/not1mm/plugins/k1usn_sst.py +0 -0
  151. {not1mm-24.8.27 → not1mm-24.9.5}/not1mm/plugins/naqp_cw.py +0 -0
  152. {not1mm-24.8.27 → not1mm-24.9.5}/not1mm/plugins/naqp_ssb.py +0 -0
  153. {not1mm-24.8.27 → not1mm-24.9.5}/not1mm/plugins/phone_weekly_test.py +0 -0
  154. {not1mm-24.8.27 → not1mm-24.9.5}/not1mm/plugins/stew_perry_topband.py +0 -0
  155. {not1mm-24.8.27 → not1mm-24.9.5}/not1mm/plugins/winter_field_day.py +0 -0
  156. {not1mm-24.8.27 → not1mm-24.9.5}/not1mm/radio.py +0 -0
  157. {not1mm-24.8.27 → not1mm-24.9.5}/not1mm/vfo.py +0 -0
  158. {not1mm-24.8.27 → not1mm-24.9.5}/not1mm/voice_keying.py +0 -0
  159. {not1mm-24.8.27 → not1mm-24.9.5}/not1mm.egg-info/dependency_links.txt +0 -0
  160. {not1mm-24.8.27 → not1mm-24.9.5}/not1mm.egg-info/entry_points.txt +0 -0
  161. {not1mm-24.8.27 → not1mm-24.9.5}/not1mm.egg-info/requires.txt +0 -0
  162. {not1mm-24.8.27 → not1mm-24.9.5}/not1mm.egg-info/top_level.txt +0 -0
  163. {not1mm-24.8.27 → not1mm-24.9.5}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: not1mm
3
- Version: 24.8.27
3
+ Version: 24.9.5
4
4
  Summary: NOT1MM Logger
5
5
  Author-email: Michael Bridak <michael.bridak@gmail.com>
6
6
  Project-URL: Homepage, https://github.com/mbridak/not1mm
@@ -100,6 +100,7 @@ Requires-Dist: Levenshtein
100
100
  - [Cluster](#cluster)
101
101
  - [N1MM Packets](#n1mm-packets)
102
102
  - [Bands](#bands)
103
+ - [Logging WSJT-X FT8/FT4 contacts](#logging-wsjt-x-ft8ft4-contacts)
103
104
  - [Sending CW](#sending-cw)
104
105
  - [Editing macro keys](#editing-macro-keys)
105
106
  - [Macro substitutions](#macro-substitutions)
@@ -225,11 +226,8 @@ generated, 'cause I'm lazy, list of those who've submitted PR's.
225
226
 
226
227
  ## Recent Changes
227
228
 
228
- - [24-8-27] Added Helvetia contest.
229
- - [24-8-22] Add loading splash screen.
230
- - [24-8-20] Added K1USN Slow Speed Test
231
- - [24-8-17-1] Did an oops. Fixed the oops.
232
- - [24-8-17] Removed some cruft. Made dockable widgets not floatable since Wayland breaks this.
229
+ - [24-9-5] Added FlDigi support for Field Day.
230
+ - [24-9-3] Added WSJT-X FT8 mode contacts to ARRL Field Day.
233
231
 
234
232
  See [CHANGELOG.md](CHANGELOG.md) for prior changes.
235
233
 
@@ -250,7 +248,7 @@ not1mm requires:
250
248
  - PyQt6
251
249
  - libportaudio2
252
250
  - libxcb-cursor0 (maybe... Depends on the distro)
253
-
251
+
254
252
  You should install these through your distribution's package manager before continuing.
255
253
 
256
254
  ### Common installation recipes for Ubuntu and Fedora
@@ -337,9 +335,7 @@ pamac build not1mm-git
337
335
  #### Mint
338
336
 
339
337
  ```bash
340
- sudo apt install python3-pip
341
- sudo apt install pipx
342
- sudo apt install libxcb-cursor0
338
+ sudo apt install python3-pip pipx libxcb-cursor0
343
339
  pipx install not1mm
344
340
  pipx ensurepath
345
341
  ```
@@ -608,6 +604,13 @@ appear. Those without will not.
608
604
 
609
605
  ![Bands Configuration Screen](https://github.com/mbridak/not1mm/raw/master/pic/configure_bands.png)
610
606
 
607
+ ## Logging WSJT-X FT8/FT4 contacts
608
+
609
+ **Currently only working for ARRL Field Day.**
610
+
611
+ not1mm listens for WSJT-X UDP traffic on the default localhost:2237. No setup is
612
+ needed to be done on not1mm's side.
613
+
611
614
  ## Sending CW
612
615
 
613
616
  Other than sending CW by hand, you can also send predefined CW text messages by
@@ -69,6 +69,7 @@
69
69
  - [Cluster](#cluster)
70
70
  - [N1MM Packets](#n1mm-packets)
71
71
  - [Bands](#bands)
72
+ - [Logging WSJT-X FT8/FT4 contacts](#logging-wsjt-x-ft8ft4-contacts)
72
73
  - [Sending CW](#sending-cw)
73
74
  - [Editing macro keys](#editing-macro-keys)
74
75
  - [Macro substitutions](#macro-substitutions)
@@ -194,11 +195,8 @@ generated, 'cause I'm lazy, list of those who've submitted PR's.
194
195
 
195
196
  ## Recent Changes
196
197
 
197
- - [24-8-27] Added Helvetia contest.
198
- - [24-8-22] Add loading splash screen.
199
- - [24-8-20] Added K1USN Slow Speed Test
200
- - [24-8-17-1] Did an oops. Fixed the oops.
201
- - [24-8-17] Removed some cruft. Made dockable widgets not floatable since Wayland breaks this.
198
+ - [24-9-5] Added FlDigi support for Field Day.
199
+ - [24-9-3] Added WSJT-X FT8 mode contacts to ARRL Field Day.
202
200
 
203
201
  See [CHANGELOG.md](CHANGELOG.md) for prior changes.
204
202
 
@@ -219,7 +217,7 @@ not1mm requires:
219
217
  - PyQt6
220
218
  - libportaudio2
221
219
  - libxcb-cursor0 (maybe... Depends on the distro)
222
-
220
+
223
221
  You should install these through your distribution's package manager before continuing.
224
222
 
225
223
  ### Common installation recipes for Ubuntu and Fedora
@@ -306,9 +304,7 @@ pamac build not1mm-git
306
304
  #### Mint
307
305
 
308
306
  ```bash
309
- sudo apt install python3-pip
310
- sudo apt install pipx
311
- sudo apt install libxcb-cursor0
307
+ sudo apt install python3-pip pipx libxcb-cursor0
312
308
  pipx install not1mm
313
309
  pipx ensurepath
314
310
  ```
@@ -577,6 +573,13 @@ appear. Those without will not.
577
573
 
578
574
  ![Bands Configuration Screen](https://github.com/mbridak/not1mm/raw/master/pic/configure_bands.png)
579
575
 
576
+ ## Logging WSJT-X FT8/FT4 contacts
577
+
578
+ **Currently only working for ARRL Field Day.**
579
+
580
+ not1mm listens for WSJT-X UDP traffic on the default localhost:2237. No setup is
581
+ needed to be done on not1mm's side.
582
+
580
583
  ## Sending CW
581
584
 
582
585
  Other than sending CW by hand, you can also send predefined CW text messages by
@@ -23,6 +23,7 @@ import threading
23
23
  import uuid
24
24
 
25
25
  from json import dumps, loads
26
+ from json.decoder import JSONDecodeError
26
27
  from pathlib import Path
27
28
  from shutil import copyfile
28
29
 
@@ -66,6 +67,7 @@ from not1mm.lib.settings import Settings
66
67
  from not1mm.lib.version import __version__
67
68
  from not1mm.lib.versiontest import VersionTest
68
69
  from not1mm.lib.ft8_watcher import FT8Watcher
70
+ from not1mm.lib.fldigi_watcher import FlDigiWatcher
69
71
 
70
72
  import not1mm.fsutils as fsutils
71
73
  from not1mm.logwindow import LogWindow
@@ -162,7 +164,9 @@ class MainWindow(QtWidgets.QMainWindow):
162
164
 
163
165
  radio_thread = QThread()
164
166
  voice_thread = QThread()
167
+ fldigi_thread = QThread()
165
168
 
169
+ fldigi_watcher = None
166
170
  rig_control = None
167
171
  log_window = None
168
172
  check_window = None
@@ -186,14 +190,13 @@ class MainWindow(QtWidgets.QMainWindow):
186
190
  )
187
191
  self.setCorner(Qt.Corner.TopLeftCorner, Qt.DockWidgetArea.LeftDockWidgetArea)
188
192
  self.setCorner(Qt.Corner.BottomLeftCorner, Qt.DockWidgetArea.LeftDockWidgetArea)
189
- data_path = fsutils.APP_DATA_PATH / "main.ui"
190
- uic.loadUi(data_path, self)
193
+ uic.loadUi(fsutils.APP_DATA_PATH / "main.ui", self)
191
194
  self.cw_entry.hide()
192
195
  self.leftdot.hide()
193
196
  self.rightdot.hide()
194
197
  self.n1mm = N1MM()
195
198
  self.ft8 = FT8Watcher()
196
- self.ft8.set_callback(self.ft8_result)
199
+ self.ft8.set_callback(None)
197
200
  self.mscp = SCP(fsutils.APP_DATA_PATH)
198
201
  self.next_field = self.other_2
199
202
  self.dupe_indicator.hide()
@@ -459,8 +462,15 @@ class MainWindow(QtWidgets.QMainWindow):
459
462
  self.setWindowIcon(
460
463
  QtGui.QIcon(str(fsutils.APP_DATA_PATH / "k6gte.not1mm-32.png"))
461
464
  )
462
- with open(fsutils.APP_DATA_PATH / "cty.json", "rt", encoding="utf-8") as c_file:
463
- self.ctyfile = loads(c_file.read())
465
+
466
+ try:
467
+ with open(
468
+ fsutils.APP_DATA_PATH / "cty.json", "rt", encoding="utf-8"
469
+ ) as c_file:
470
+ self.ctyfile = loads(c_file.read())
471
+ except (IOError, JSONDecodeError, TypeError):
472
+ logging.CRITICAL("There was an error parsing the BigCity file.")
473
+
464
474
  self.readpreferences()
465
475
 
466
476
  self.voice_process = Voice()
@@ -491,9 +501,7 @@ class MainWindow(QtWidgets.QMainWindow):
491
501
  self.make_op_dir()
492
502
 
493
503
  self.clearinputs()
494
-
495
- if self.pref.get("contest"):
496
- self.load_contest()
504
+ self.load_contest()
497
505
  self.read_cw_macros()
498
506
 
499
507
  # Featureset for wayland
@@ -502,6 +510,13 @@ class MainWindow(QtWidgets.QMainWindow):
502
510
  | QtWidgets.QDockWidget.DockWidgetFeature.DockWidgetMovable
503
511
  )
504
512
 
513
+ self.fldigi_watcher = FlDigiWatcher()
514
+ self.fldigi_watcher.moveToThread(self.fldigi_thread)
515
+ self.fldigi_thread.started.connect(self.fldigi_watcher.run)
516
+ self.fldigi_thread.finished.connect(self.fldigi_watcher.deleteLater)
517
+ self.fldigi_watcher.poll_callback.connect(self.fldigi_qso)
518
+ self.fldigi_thread.start()
519
+
505
520
  self.log_window = LogWindow()
506
521
  self.log_window.setObjectName("log-window")
507
522
  if os.environ.get("WAYLAND_DISPLAY"):
@@ -561,32 +576,50 @@ class MainWindow(QtWidgets.QMainWindow):
561
576
  "You can udate to the current version by using:\npip install -U not1mm"
562
577
  )
563
578
 
564
- def ft8_result(self, result: dict):
579
+ def fldigi_qso(self, result: str):
565
580
  """
566
- Callback for ft8 watcher
581
+ gets called when there is a new fldigi qso logged.
567
582
 
568
583
  {
569
- 'CALL': 'KE0OG',
570
- 'GRIDSQUARE': 'DM10AT',
571
- 'MODE': 'FT8',
572
- 'RST_SENT': '',
573
- 'RST_RCVD': '',
574
- 'QSO_DATE': '20210329',
575
- 'TIME_ON': '183213',
576
- 'QSO_DATE_OFF': '20210329',
577
- 'TIME_OFF': '183213',
578
- 'BAND': '20M',
579
- 'FREQ': '14.074754',
584
+ 'FREQ': '7.029500',
585
+ 'CALL': 'DL2DSL',
586
+ 'MODE': 'RTTY',
587
+ 'NAME': 'BOB',
588
+ 'QSO_DATE': '20240904',
589
+ 'QSO_DATE_OFF': '20240904',
590
+ 'TIME_OFF': '212825',
591
+ 'TIME_ON': '212800',
592
+ 'RST_RCVD': '599',
593
+ 'RST_SENT': '599',
594
+ 'BAND': '40M',
595
+ 'COUNTRY': 'FED. REP. OF GERMANY',
596
+ 'CQZ': '14',
597
+ 'STX': '000',
598
+ 'STX_STRING': '1D ORG',
599
+ 'CLASS': '1D',
600
+ 'ARRL_SECT': 'DX',
601
+ 'TX_PWR': '0',
602
+ 'OPERATOR': 'K6GTE',
580
603
  'STATION_CALLSIGN': 'K6GTE',
581
604
  'MY_GRIDSQUARE': 'DM13AT',
582
- 'CONTEST_ID': 'ARRL-FIELD-DAY',
583
- 'SRX_STRING': '1D UT',
584
- 'CLASS': '1D',
585
- 'ARRL_SECT': 'UT'
605
+ 'MY_CITY': 'ANAHEIM, CA',
606
+ 'MY_STATE': 'CA'
586
607
  }
587
608
 
588
609
  """
589
- print(f"{result=}")
610
+
611
+ datadict = {}
612
+ splitdata = result.upper().strip().split("<")
613
+ for data in splitdata:
614
+ if data:
615
+ tag = data.split(":")
616
+ if tag == ["EOR>"]:
617
+ break
618
+ datadict[tag[0]] = tag[1].split(">")[1].strip()
619
+ logger.debug(f"{datadict=}")
620
+ if hasattr(self.contest, "ft8_handler"):
621
+ self.contest.set_self(self)
622
+ self.contest.ft8_handler(datadict)
590
623
 
591
624
  def setDarkMode(self, setdarkmode=False) -> None:
592
625
  """Forces a darkmode palette."""
@@ -1287,10 +1320,15 @@ class MainWindow(QtWidgets.QMainWindow):
1287
1320
  if updated:
1288
1321
  cty.dump(fsutils.APP_DATA_PATH / "cty.json")
1289
1322
  self.show_message_box("cty file updated.")
1290
- with open(
1291
- fsutils.APP_DATA_PATH / "cty.json", "rt", encoding="utf-8"
1292
- ) as ctyfile:
1293
- self.ctyfile = loads(ctyfile.read())
1323
+ try:
1324
+ with open(
1325
+ fsutils.APP_DATA_PATH / "cty.json", "rt", encoding="utf-8"
1326
+ ) as ctyfile:
1327
+ self.ctyfile = loads(ctyfile.read())
1328
+ except (IOError, JSONDecodeError, TypeError) as err:
1329
+ logging.CRITICAL(
1330
+ f"There was an error {err} parsing the BigCity file."
1331
+ )
1294
1332
  else:
1295
1333
  self.show_message_box("An Error occured updating file.")
1296
1334
  else:
@@ -1898,11 +1936,14 @@ class MainWindow(QtWidgets.QMainWindow):
1898
1936
  " "
1899
1937
  )[:19]
1900
1938
  self.contact["Call"] = self.callsign.text()
1901
- self.contact["Freq"] = round(float(self.radio_state.get("vfoa", 0.0)) / 1000, 2)
1902
- self.contact["QSXFreq"] = round(
1903
- float(self.radio_state.get("vfoa", 0.0)) / 1000, 2
1904
- )
1905
- self.contact["Mode"] = self.radio_state.get("mode", "")
1939
+ if self.contact.get("Mode") not in ("FT8", "FT4"):
1940
+ self.contact["Freq"] = round(
1941
+ float(self.radio_state.get("vfoa", 0.0)) / 1000, 2
1942
+ )
1943
+ self.contact["QSXFreq"] = round(
1944
+ float(self.radio_state.get("vfoa", 0.0)) / 1000, 2
1945
+ )
1946
+ self.contact["Mode"] = self.radio_state.get("mode", "")
1906
1947
  self.contact["ContestName"] = self.contest.cabrillo_name
1907
1948
  self.contact["ContestNR"] = self.pref.get("contest", "0")
1908
1949
  self.contact["StationPrefix"] = self.station.get("Call", "")
@@ -2093,7 +2134,7 @@ class MainWindow(QtWidgets.QMainWindow):
2093
2134
 
2094
2135
  def save_settings(self) -> None:
2095
2136
  """
2096
- Save settings to database.
2137
+ Save Station settings to database.
2097
2138
 
2098
2139
  Parameters
2099
2140
  ----------
@@ -2322,7 +2363,7 @@ class MainWindow(QtWidgets.QMainWindow):
2322
2363
  with open(fsutils.CONFIG_FILE, "wt", encoding="utf-8") as file_descriptor:
2323
2364
  file_descriptor.write(dumps(self.pref, indent=4))
2324
2365
  # logger.info("writing: %s", self.pref)
2325
- except IOError as exception:
2366
+ except (IOError, TypeError, ValueError) as exception:
2326
2367
  logger.critical("writepreferences: %s", exception)
2327
2368
 
2328
2369
  def readpreferences(self) -> None:
@@ -2344,7 +2385,12 @@ class MainWindow(QtWidgets.QMainWindow):
2344
2385
  with open(
2345
2386
  fsutils.CONFIG_FILE, "rt", encoding="utf-8"
2346
2387
  ) as file_descriptor:
2347
- self.pref = loads(file_descriptor.read())
2388
+ try:
2389
+ self.pref = loads(file_descriptor.read())
2390
+ except (JSONDecodeError, TypeError):
2391
+ logging.CRITICAL(
2392
+ "There was an error parsing the preference file."
2393
+ )
2348
2394
  logger.info("%s", self.pref)
2349
2395
  else:
2350
2396
  logger.info("No preference file. Writing preference.")
@@ -2354,7 +2400,7 @@ class MainWindow(QtWidgets.QMainWindow):
2354
2400
  self.pref = self.pref_ref.copy()
2355
2401
  file_descriptor.write(dumps(self.pref, indent=4))
2356
2402
  logger.info("%s", self.pref)
2357
- except IOError as exception:
2403
+ except (IOError, TypeError, ValueError) as exception:
2358
2404
  logger.critical("Error: %s", exception)
2359
2405
 
2360
2406
  self.look_up = None
@@ -3197,10 +3243,14 @@ class MainWindow(QtWidgets.QMainWindow):
3197
3243
  else:
3198
3244
  macro_file = "ssbmacros.txt"
3199
3245
  if not (fsutils.USER_DATA_PATH / macro_file).exists():
3200
- logger.debug("read_cw_macros: copying default macro file.")
3201
- copyfile(
3202
- fsutils.APP_DATA_PATH / macro_file, fsutils.USER_DATA_PATH / macro_file
3203
- )
3246
+ logger.debug("copying default macro file.")
3247
+ try:
3248
+ copyfile(
3249
+ fsutils.APP_DATA_PATH / macro_file,
3250
+ fsutils.USER_DATA_PATH / macro_file,
3251
+ )
3252
+ except IOError as err:
3253
+ logger.critical(f"Error {err} copying macro file.")
3204
3254
  try:
3205
3255
  fsutils.openFileWithOS(fsutils.USER_DATA_PATH / macro_file)
3206
3256
  except FileNotFoundError | PermissionError | OSError as err:
@@ -3221,22 +3271,26 @@ class MainWindow(QtWidgets.QMainWindow):
3221
3271
  macro_file = "ssbmacros.txt"
3222
3272
 
3223
3273
  if not (fsutils.USER_DATA_PATH / macro_file).exists():
3224
- logger.debug("read_cw_macros: copying default macro file.")
3225
- copyfile(
3226
- fsutils.APP_DATA_PATH / macro_file, fsutils.USER_DATA_PATH / macro_file
3227
- )
3228
- with open(
3229
- fsutils.USER_DATA_PATH / macro_file, "r", encoding="utf-8"
3230
- ) as file_descriptor:
3231
- for line in file_descriptor:
3232
- try:
3274
+ logger.debug("copying default macro file.")
3275
+ try:
3276
+ copyfile(
3277
+ fsutils.APP_DATA_PATH / macro_file,
3278
+ fsutils.USER_DATA_PATH / macro_file,
3279
+ )
3280
+ except IOError as err:
3281
+ logger.critical(f"Error {err} copying macro file.")
3282
+ try:
3283
+ with open(
3284
+ fsutils.USER_DATA_PATH / macro_file, "r", encoding="utf-8"
3285
+ ) as file_descriptor:
3286
+ for line in file_descriptor:
3233
3287
  mode, fkey, buttonname, cwtext = line.split("|")
3234
3288
  if mode.strip().upper() == "R" and self.pref.get("run_state"):
3235
3289
  self.fkeys[fkey.strip()] = (buttonname.strip(), cwtext.strip())
3236
3290
  if mode.strip().upper() != "R" and not self.pref.get("run_state"):
3237
3291
  self.fkeys[fkey.strip()] = (buttonname.strip(), cwtext.strip())
3238
- except ValueError as err:
3239
- logger.info("read_cw_macros: %s", err)
3292
+ except (IOError, ValueError) as err:
3293
+ logger.info("read_cw_macros: %s", err)
3240
3294
  keys = self.fkeys.keys()
3241
3295
  if "F1" in keys:
3242
3296
  self.F1.setText(f"F1: {self.fkeys['F1'][0]}")
@@ -886,28 +886,14 @@ class DataBase:
886
886
  logger.debug("%s", exception)
887
887
  return {}
888
888
 
889
- def fetch_mult1_count(self) -> dict:
889
+ def fetch_mult_count(self, mult: int) -> dict:
890
890
  """return QSO count"""
891
891
  try:
892
892
  with sqlite3.connect(self.database) as conn:
893
893
  conn.row_factory = self.row_factory
894
894
  cursor = conn.cursor()
895
895
  cursor.execute(
896
- f"select count(*) as count from dxlog where IsMultiplier1 = 1 and ContestNR = {self.current_contest};"
897
- )
898
- return cursor.fetchone()
899
- except sqlite3.OperationalError as exception:
900
- logger.debug("%s", exception)
901
- return {}
902
-
903
- def fetch_mult2_count(self) -> dict:
904
- """return QSO count"""
905
- try:
906
- with sqlite3.connect(self.database) as conn:
907
- conn.row_factory = self.row_factory
908
- cursor = conn.cursor()
909
- cursor.execute(
910
- f"select count(*) as count from dxlog where IsMultiplier2 = 1 and ContestNR = {self.current_contest};"
896
+ f"select count(*) as count from dxlog where IsMultiplier{mult} = 1 and ContestNR = {self.current_contest};"
911
897
  )
912
898
  return cursor.fetchone()
913
899
  except sqlite3.OperationalError as exception:
@@ -0,0 +1,32 @@
1
+ import xmlrpc.client
2
+ from PyQt6.QtCore import QObject, pyqtSignal, QThread, QEventLoop
3
+
4
+
5
+ class FlDigiWatcher(QObject):
6
+ """fldigi watcher"""
7
+
8
+ poll_callback = pyqtSignal(str)
9
+ time_to_quit = False
10
+
11
+ def __init__(self):
12
+ super().__init__()
13
+ ...
14
+
15
+ self.target = "http://127.0.0.1:7362"
16
+ self.payload = ""
17
+ self.response = ""
18
+
19
+ def run(self):
20
+ while not self.time_to_quit:
21
+ try:
22
+ server = xmlrpc.client.ServerProxy(self.target)
23
+ self.response = server.logbook.last_record()
24
+ except OSError:
25
+ continue
26
+ if self.payload != self.response:
27
+ self.payload = self.response
28
+ try:
29
+ self.poll_callback.emit(self.payload)
30
+ except QEventLoop:
31
+ ...
32
+ QThread.msleep(100)
@@ -6,10 +6,14 @@ https://github.com/mbridak/not1mm
6
6
  GPL V3
7
7
  """
8
8
 
9
+ import logging
10
+
9
11
  from PyQt6 import QtNetwork
10
12
 
11
13
  import struct
12
14
 
15
+ logger = logging.getLogger(__name__)
16
+
13
17
 
14
18
  class FT8Watcher:
15
19
  """Main Window"""
@@ -64,7 +68,7 @@ class FT8Watcher:
64
68
  datagram, sender_host, sender_port_number = self.udp_socket.readDatagram(
65
69
  self.udp_socket.pendingDatagramSize()
66
70
  )
67
- # print("%s %s %s", sender_host, sender_port_number, datagram)
71
+ logger.debug(f"{datagram=}")
68
72
 
69
73
  if datagram[0:4] != b"\xad\xbc\xcb\xda":
70
74
  return # bail if no wsjt-x magic number
@@ -1,3 +1,3 @@
1
1
  """It's the version"""
2
2
 
3
- __version__ = "24.8.27"
3
+ __version__ = "24.9.5"
@@ -8,6 +8,7 @@ import logging
8
8
  from pathlib import Path
9
9
  from PyQt6 import QtWidgets
10
10
 
11
+ from not1mm.lib.ham_utility import get_logged_band
11
12
  from not1mm.lib.plugin_common import gen_adif, get_points
12
13
  from not1mm.lib.version import __version__
13
14
 
@@ -114,7 +115,7 @@ def points(self):
114
115
  _mode = self.contact.get("Mode", "")
115
116
  if _mode in "SSB, USB, LSB, FM, AM":
116
117
  return 1
117
- if _mode in "CW, RTTY":
118
+ if _mode in "CW, RTTY, FT8":
118
119
  return 2
119
120
  return 0
120
121
 
@@ -295,8 +296,10 @@ def cabrillo(self):
295
296
  for contact in log:
296
297
  the_date_and_time = contact.get("TS", "")
297
298
  themode = contact.get("Mode", "")
298
- if themode == "LSB" or themode == "USB":
299
+ if themode in ("LSB", "USB", "FM"):
299
300
  themode = "PH"
301
+ if themode in ("FT8", "FT4", "RTTY"):
302
+ themode = "DG"
300
303
  frequency = str(int(contact.get("Freq", "0"))).rjust(5)
301
304
 
302
305
  loggeddate = the_date_and_time[:10]
@@ -330,7 +333,7 @@ def set_self(the_outie):
330
333
 
331
334
  def ft8_handler(the_packet: dict):
332
335
  """Process FT8 QSO packets
333
-
336
+ FT8
334
337
  {
335
338
  'CALL': 'KE0OG',
336
339
  'GRIDSQUARE': 'DM10AT',
@@ -350,19 +353,51 @@ def ft8_handler(the_packet: dict):
350
353
  'CLASS': '1D',
351
354
  'ARRL_SECT': 'UT'
352
355
  }
356
+ FlDigi
357
+ {
358
+ 'FREQ': '7.029500',
359
+ 'CALL': 'DL2DSL',
360
+ 'MODE': 'RTTY',
361
+ 'NAME': 'BOB',
362
+ 'QSO_DATE': '20240904',
363
+ 'QSO_DATE_OFF': '20240904',
364
+ 'TIME_OFF': '212825',
365
+ 'TIME_ON': '212800',
366
+ 'RST_RCVD': '599',
367
+ 'RST_SENT': '599',
368
+ 'BAND': '40M',
369
+ 'COUNTRY': 'FED. REP. OF GERMANY',
370
+ 'CQZ': '14',
371
+ 'STX': '000',
372
+ 'STX_STRING': '1D ORG',
373
+ 'CLASS': '1D',
374
+ 'ARRL_SECT': 'DX',
375
+ 'TX_PWR': '0',
376
+ 'OPERATOR': 'K6GTE',
377
+ 'STATION_CALLSIGN': 'K6GTE',
378
+ 'MY_GRIDSQUARE': 'DM13AT',
379
+ 'MY_CITY': 'ANAHEIM, CA',
380
+ 'MY_STATE': 'CA'
381
+ }
353
382
 
354
383
  """
355
- print(f"{the_packet=}")
356
-
384
+ logger.debug(f"{the_packet=}")
357
385
  if ALTEREGO is not None:
358
-
359
386
  ALTEREGO.callsign.setText(the_packet.get("CALL"))
360
387
  ALTEREGO.contact["Call"] = the_packet.get("CALL", "")
361
388
  ALTEREGO.contact["SNT"] = ALTEREGO.sent.text()
362
389
  ALTEREGO.contact["RCV"] = ALTEREGO.receive.text()
363
390
  ALTEREGO.contact["Exchange1"] = the_packet.get("CLASS", "ERR")
364
391
  ALTEREGO.contact["Sect"] = the_packet.get("ARRL_SECT", "ERR")
365
- ALTEREGO.contact["Mode"] = "FT8"
392
+ ALTEREGO.contact["Mode"] = the_packet.get("MODE", "ERR")
393
+ ALTEREGO.contact["Freq"] = round(float(the_packet.get("FREQ", "0.0")) * 1000, 2)
394
+ ALTEREGO.contact["QSXFreq"] = round(
395
+ float(the_packet.get("FREQ", "0.0")) * 1000, 2
396
+ )
397
+ ALTEREGO.contact["Band"] = get_logged_band(
398
+ str(int(float(the_packet.get("FREQ", "0.0")) * 1000000))
399
+ )
400
+ logger.debug(f"{ALTEREGO.contact=}")
366
401
  ALTEREGO.other_1.setText(the_packet.get("CLASS", "ERR"))
367
402
  ALTEREGO.other_2.setText(the_packet.get("ARRL_SECT", "ERR"))
368
- print(f"\n{ALTEREGO.contact=}\n")
403
+ ALTEREGO.save_contact()
@@ -177,7 +177,7 @@ def points(self):
177
177
 
178
178
  def show_mults(self):
179
179
  """Return display string for mults"""
180
- result = self.database.fetch_mult1_count()
180
+ result = self.database.fetch_mult_count(1)
181
181
  count = result.get("count", 0)
182
182
  return count
183
183
 
@@ -198,7 +198,7 @@ def calc_score(self):
198
198
  if score is None:
199
199
  score = "0"
200
200
  contest_points = int(score)
201
- result = self.database.fetch_mult1_count()
201
+ result = self.database.fetch_mult_count(1)
202
202
  mults = int(result.get("count", 0))
203
203
  return contest_points * mults
204
204
  return 0
@@ -177,7 +177,7 @@ def points(self):
177
177
 
178
178
  def show_mults(self):
179
179
  """Return display string for mults"""
180
- result = self.database.fetch_mult1_count()
180
+ result = self.database.fetch_mult_count(1)
181
181
  count = result.get("count", 0)
182
182
  return count
183
183
 
@@ -198,7 +198,7 @@ def calc_score(self):
198
198
  if score is None:
199
199
  score = "0"
200
200
  contest_points = int(score)
201
- result = self.database.fetch_mult1_count()
201
+ result = self.database.fetch_mult_count(1)
202
202
  mults = int(result.get("count", 0))
203
203
  return contest_points * mults
204
204
  return 0
@@ -250,8 +250,8 @@ def points(self):
250
250
 
251
251
  def show_mults(self):
252
252
  """Return display string for mults"""
253
- return int(self.database.fetch_mult1_count().get("count", 0)) + int(
254
- self.database.fetch_mult2_count().get("count", 0)
253
+ return int(self.database.fetch_mult_count(1).get("count", 0)) + int(
254
+ self.database.fetch_mult_count(2).get("count", 0)
255
255
  )
256
256
 
257
257