Compare commits

...

293 commits

Author SHA1 Message Date
DJ2LS 0e81bddedd
Merge pull request #702 from DJ2LS/dependabot/npm_and_yarn/gui/vite-5.1.7
Bump vite from 5.1.3 to 5.1.7 in /gui
2024-04-13 17:28:22 +02:00
dependabot[bot] e09b257936
Bump vite from 5.1.3 to 5.1.7 in /gui
Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 5.1.3 to 5.1.7.
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/v5.1.7/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v5.1.7/packages/vite)

---
updated-dependencies:
- dependency-name: vite
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-04-03 17:14:53 +00:00
Mashintime fbd625cf5b Merge branch 'develop' of github.com:DJ2LS/FreeDATA into develop 2024-03-30 00:09:39 -04:00
DJ2LS eccd0e12a7 some fixes... 2024-03-27 19:55:19 +01:00
DJ2LS c2ff18b111
Merge pull request #694 from DJ2LS/main-cf-autofix
Apply fixes from CodeFactor
2024-03-27 17:02:10 +01:00
DJ2LS 83d06259a7
Merge pull request #695 from DJ2LS/main
forgot changing branch...
2024-03-27 10:50:55 +01:00
codefactor-io bea6c086af
[CodeFactor] Apply fixes to commit b110735 2024-03-27 09:49:41 +00:00
DJ2LS b11073521b
Merge pull request #693 from DJ2LS/dev-calculate-grid-distance
calculate grid distance
2024-03-27 10:49:25 +01:00
DJ2LS 329ce9dc20 add distance information to heard stations and events 2024-03-27 10:16:14 +01:00
DJ2LS b0e46fb998
Merge pull request #692 from DJ2LS/dev-add-grid-to-event
add gridsquare to event
2024-03-27 09:45:44 +01:00
DJ2LS 33f0d2a160
Merge pull request #691 from DJ2LS/dev-interface-tests
Socket interface prototype
2024-03-27 09:45:25 +01:00
DJ2LS 7811aae324 add gridsquare to event if available 2024-03-27 09:34:06 +01:00
DJ2LS f5d0b90eda fixing a test 2024-03-27 09:20:24 +01:00
Mashintime 5f1597c27f Merge branch 'develop' of github.com:DJ2LS/FreeDATA into develop 2024-03-25 19:46:05 -04:00
DJ2LS bdd8888f1b enable/disable socket interface 2024-03-25 19:29:30 +01:00
DJ2LS e24a64ba25 Merge branch 'develop' into dev-interface-tests 2024-03-25 19:04:37 +01:00
DJ2LS ef4ea345fd fixed explorer 2024-03-25 08:28:02 +01:00
DJ2LS ea27044d91
Merge pull request #667 from DJ2LS/dependabot/npm_and_yarn/gui/develop/vitest-1.3.1 2024-03-24 15:33:33 -07:00
DJ2LS cb70ed8445
Merge pull request #690 from DJ2LS/dev-max-bandwidth
set maximum bandwidth
2024-03-24 14:17:30 -07:00
DJ2LS 9fe59ffaa1
Merge pull request #688 from DJ2LS/main-cf-autofix
Apply fixes from CodeFactor
2024-03-24 13:48:20 -07:00
DJ2LS 6fb092dd58 fixed test 2024-03-24 21:47:48 +01:00
DJ2LS 3cb329b379 check for max bandwidth on session open 2024-03-24 21:39:44 +01:00
DJ2LS 8c224ab7dc define maximum used bandwidth during transmission 2024-03-24 21:17:07 +01:00
DJ2LS 577da699a8
Merge pull request #689 from DJ2LS/dev-arq
adjusted ping and qrv
2024-03-24 12:58:09 -07:00
DJ2LS b28ee0aa85 adjusted cq and ping 2024-03-24 20:17:11 +01:00
DJ2LS cc6b3eb958 pep related improvements to ARQ 2024-03-24 20:07:18 +01:00
DJ2LS ff6ef4b130 increased ARQ data timeout 2024-03-24 19:30:56 +01:00
DJ2LS b0c0940e5d respond to CQ and PING if not in ARQ session 2024-03-24 19:29:28 +01:00
DJ2LS e57d3cf665 saved latest dev state.. 2024-03-24 19:16:19 +01:00
Mashintime 6ed283a5eb Merge branch 'develop' of github.com:DJ2LS/FreeDATA into develop 2024-03-22 16:32:19 -04:00
DJ2LS f308134276 added rx part for larger data 2024-03-19 22:16:27 +01:00
DJ2LS bb8da6f5d8 first attempt with arq session inside connection 2024-03-19 22:10:58 +01:00
DJ2LS aaa2084bfd adjusted tests 2024-03-19 13:19:13 +01:00
DJ2LS 9da3fb80f0 adjusted tests 2024-03-19 13:01:25 +01:00
DJ2LS 5ed11d771f added data and command queues 2024-03-19 11:19:13 +01:00
DJ2LS adb0c82332 Merge branch 'develop' into dev-interface-tests 2024-03-17 16:28:21 +01:00
DJ2LS 8d74566b6c rigctld hotfix 2024-03-17 10:14:56 -04:00
DJ2LS 363b90da07 rigctld hotfix 2024-03-17 12:04:58 +01:00
codefactor-io 34747356c9
[CodeFactor] Apply fixes to commit 03cc026 2024-03-16 17:19:30 +00:00
Mashintime 03cc026965 Remove references to unused components. 2024-03-16 13:17:00 -04:00
DJ2LS 6178454250
Merge pull request #686 from DJ2LS/develop 2024-03-16 10:07:28 -07:00
DJ2LS 40f7337b6c bump version 2024-03-16 16:24:24 +01:00
DJ2LS 0beb4aea23 small cleanup 2024-03-16 16:23:41 +01:00
DJ2LS 6562a44175 adjusted response part for commands 2024-03-16 16:11:38 +01:00
DJ2LS d2b3f3a36e adjusted response part for commands 2024-03-16 15:52:14 +01:00
DJ2LS 0f3611fb15 testing first response 2024-03-16 13:57:48 +01:00
DJ2LS 4d3a0832d5 bind p2p connection to interface 2024-03-16 13:44:58 +01:00
DJ2LS 24f41edb63 add state and event manager instaces to command and data handler 2024-03-16 11:14:48 +01:00
DJ2LS 7714c7aeb6 added socket interface to modem 2024-03-16 10:29:13 +01:00
DJ2LS 6b4bdb4d7d Merge branch 'develop' into dev-interface-tests 2024-03-16 09:34:03 +01:00
DJ2LS cbc928f117 added dummy functions for continuing work on... 2024-03-15 14:33:06 +01:00
DJ2LS 216799fe2b added disconnect 2024-03-15 14:25:46 +01:00
DJ2LS 01b1977630 avoid event queue overflow 2024-03-15 13:28:21 +01:00
DJ2LS 41d9642eb1 adjusted server shutdown and transmitting state 2024-03-15 09:45:51 +01:00
DJ2LS 787cb2d862 don't restart modem on input overflow for now for checking if callback mode solved problems 2024-03-13 22:06:59 +01:00
DJ2LS fbf4e68d9a
Merge branch 'develop' into dependabot/npm_and_yarn/gui/develop/vitest-1.3.1 2024-03-13 12:26:53 -07:00
DJ2LS 791e9ab9c6
Merge pull request #685 from DJ2LS/gui-fixes
Gui fixes
2024-03-13 02:15:36 -07:00
DJ2LS 98dada2af5
Merge pull request #684 from DJ2LS/dev-audio-callback 2024-03-13 00:30:45 -07:00
Mashintime 4f4c678eac Clicking a heard station populates ping textboxs 2024-03-12 21:37:44 -04:00
Mashintime f5de99a25b Fix 18m freq selection 2024-03-12 20:40:35 -04:00
DJ2LS c8fa826e60 fixed tx buffer delay 2024-03-12 19:54:07 +01:00
DJ2LS 7bed6041f3 restructured modem part 2024-03-12 19:48:50 +01:00
DJ2LS b58749a8a5 restructured modem part 2024-03-12 18:39:30 +01:00
DJ2LS dd703b4bbd using audio transmit callback 2024-03-12 16:33:40 +01:00
DJ2LS 6b7146e02c more clean server shutdown 2024-03-11 20:07:15 +01:00
DJ2LS 2b32fb740c restart server on input overflow 2024-03-11 19:52:03 +01:00
DJ2LS e4744a113f build fix 2024-03-11 19:27:09 +01:00
dependabot[bot] 9eb3e3f889
Bump vitest from 1.2.2 to 1.3.1 in /gui
Bumps [vitest](https://github.com/vitest-dev/vitest/tree/HEAD/packages/vitest) from 1.2.2 to 1.3.1.
- [Release notes](https://github.com/vitest-dev/vitest/releases)
- [Commits](https://github.com/vitest-dev/vitest/commits/v1.3.1/packages/vitest)

---
updated-dependencies:
- dependency-name: vitest
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-03-11 10:05:53 +00:00
DJ2LS 6dc09c4d1b
Merge pull request #683 from DJ2LS/develop
0.14.3
2024-03-11 03:05:25 -07:00
DJ2LS 919b9c6eeb
Merge pull request #669 from DJ2LS/dependabot/npm_and_yarn/gui/develop/vue-3.4.21
Bump vue from 3.4.15 to 3.4.21 in /gui
2024-03-11 03:03:48 -07:00
DJ2LS 71c62a2f2c
Merge pull request #670 from DJ2LS/dependabot/npm_and_yarn/gui/develop/eslint-plugin-vue-9.22.0
Bump eslint-plugin-vue from 9.20.1 to 9.22.0 in /gui
2024-03-11 03:03:43 -07:00
DJ2LS 6d8f81ea89
Merge pull request #681 from DJ2LS/dependabot/npm_and_yarn/gui/develop/electron-29.1.1
Bump electron from 28.2.2 to 29.1.1 in /gui
2024-03-11 03:03:29 -07:00
DJ2LS de25c95a05 bump version 2024-03-11 11:03:11 +01:00
dependabot[bot] 3a095a00ae
Bump electron from 28.2.2 to 29.1.1 in /gui
Bumps [electron](https://github.com/electron/electron) from 28.2.2 to 29.1.1.
- [Release notes](https://github.com/electron/electron/releases)
- [Changelog](https://github.com/electron/electron/blob/main/docs/breaking-changes.md)
- [Commits](https://github.com/electron/electron/compare/v28.2.2...v29.1.1)

---
updated-dependencies:
- dependency-name: electron
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-03-11 10:02:12 +00:00
DJ2LS 69672482b3
Merge pull request #682 from DJ2LS/dev-remove-info-screen
removing info screen from gui
2024-03-11 02:59:11 -07:00
DJ2LS c4ab8dfe4a small cleanup of dependencies 2024-03-11 10:58:54 +01:00
codefactor-io 09a87ab9d5
[CodeFactor] Apply fixes 2024-03-11 09:50:49 +00:00
DJ2LS 248bc596cc removed info screen 2024-03-11 10:46:59 +01:00
DJ2LS e5215a838e attempt fixing audio input overflow 2024-03-10 18:37:27 +01:00
DJ2LS d48f457fd5 attempt fixing audio input overflow 2024-03-09 18:11:00 +01:00
DJ2LS d5eea3c99c fixed bug and version bump 2024-03-09 14:31:32 +01:00
DJ2LS ec6b16b672
Merge pull request #680 from DJ2LS/dev-audio-debugging 2024-03-09 05:26:22 -08:00
DJ2LS 4de818f3a6
Merge branch 'develop' into dev-audio-debugging 2024-03-09 05:26:16 -08:00
DJ2LS dc67b5632d
Merge pull request #679 from DJ2LS/dev-cleanup
code cleanup
2024-03-09 02:44:04 -08:00
DJ2LS 6b7327df3d added some audio debugging and reduced a not needed thread in demodulator 2024-03-09 11:42:40 +01:00
DJ2LS 0bd6f6c9f9 code cleanup 2024-03-09 11:06:20 +01:00
DJ2LS 20b1fe7e2d WIP p2p 2024-03-09 10:47:27 +01:00
DJ2LS 1599eb1515
Merge pull request #678 from DJ2LS/dev-beacon-adjustments
adjusted beacon
2024-03-07 07:56:49 -08:00
DJ2LS 61e861ee06
Merge pull request #677 from DJ2LS/dev-gui-autoscroll
fix message auto scroll
2024-03-07 06:47:21 -08:00
DJ2LS 0e07697a98 removed beacon interval change, send beacon when enabling it 2024-03-07 15:33:42 +01:00
DJ2LS 11e51afe07 Merge remote-tracking branch 'origin/dev-gui-autoscroll' into dev-gui-autoscroll 2024-03-07 15:14:49 +01:00
DJ2LS f63c597e80 send messages only if modem running 2024-03-07 15:14:07 +01:00
codefactor-io 454c4758d0
[CodeFactor] Apply fixes 2024-03-07 13:50:47 +00:00
DJ2LS 184caf57ab adjusted message scrolling 2024-03-07 14:48:13 +01:00
DJ2LS e65e3a984c adjusted CORS for server 2024-03-07 13:40:24 +01:00
DJ2LS e9ee0e600d adjusted macos python lookup path 2024-03-07 13:30:17 +01:00
dependabot[bot] dd964474ee
Bump vue from 3.4.15 to 3.4.21 in /gui
Bumps [vue](https://github.com/vuejs/core) from 3.4.15 to 3.4.21.
- [Release notes](https://github.com/vuejs/core/releases)
- [Changelog](https://github.com/vuejs/core/blob/main/CHANGELOG.md)
- [Commits](https://github.com/vuejs/core/compare/v3.4.15...v3.4.21)

---
updated-dependencies:
- dependency-name: vue
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-03-04 15:15:52 +00:00
DJ2LS 6b693c6759 bump version info 2024-03-04 16:15:46 +01:00
DJ2LS d2ee01479a
Merge pull request #675 from DJ2LS/dev-fix-modem-port
support different server ports
2024-03-04 07:14:56 -08:00
DJ2LS 9520bb4689
Merge pull request #673 from DJ2LS/dev-nsis-installer
NSIS Installer for Windows
2024-03-04 07:14:36 -08:00
DJ2LS 90790bd8e0 support different server ports 2024-03-04 15:50:08 +01:00
DJ2LS de82f649b1 Merge remote-tracking branch 'origin/dev-nsis-installer' into dev-nsis-installer 2024-03-04 11:55:09 +01:00
DJ2LS d078394af4 fixes #672 2024-03-04 11:55:00 +01:00
codefactor-io a258f4b16f
[CodeFactor] Apply fixes 2024-03-04 10:42:55 +00:00
DJ2LS eeb72faf83 introduced store for serial and audio devices for fixing audio related problems 2024-03-04 11:36:44 +01:00
DJ2LS 0240c6cd1d possible test fix, possible fix of startmenu items 2024-03-03 11:51:43 +01:00
DJ2LS 7e1d6f6100 added version to Installer name, add start menu entries, refresh audio devices on request 2024-03-03 11:09:29 +01:00
DJ2LS 01f3cefe55 typescript fix 2024-03-03 10:08:32 +01:00
DJ2LS 659b1c0c56 adjusted uninstaller and fixed a icon 2024-03-03 09:53:55 +01:00
DJ2LS 1c6109a25a don't delete config on update, adjusted audio device fetching 2024-03-03 09:49:21 +01:00
DJ2LS f6170604a6 adjusted config fetching 2024-03-02 18:24:14 +01:00
DJ2LS ffb3db775f get all settings when clicking settings 2024-03-02 11:51:09 +01:00
DJ2LS d90ac7cec0 get config when clicking settings 2024-03-02 11:44:23 +01:00
DJ2LS 34dcdd5d8a fixed copy and paste error 2024-03-02 11:38:09 +01:00
DJ2LS bab8aad126 bug fix and added install-deps command 2024-03-02 10:41:07 +01:00
DJ2LS d4275642d7 adjusted server startup and stopping 2024-03-02 10:33:44 +01:00
DJ2LS 7ff95571c3 adjusted database and config paths 2024-03-01 21:29:11 +01:00
DJ2LS 297be826dd this one should work... 2024-03-01 17:55:42 +01:00
dependabot[bot] e529bbb395
Bump eslint-plugin-vue from 9.20.1 to 9.22.0 in /gui
Bumps [eslint-plugin-vue](https://github.com/vuejs/eslint-plugin-vue) from 9.20.1 to 9.22.0.
- [Release notes](https://github.com/vuejs/eslint-plugin-vue/releases)
- [Commits](https://github.com/vuejs/eslint-plugin-vue/compare/v9.20.1...v9.22.0)

---
updated-dependencies:
- dependency-name: eslint-plugin-vue
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-03-01 16:20:24 +00:00
DJ2LS 1b927ad183 adjusted lookup path and updated gh action 2024-03-01 17:04:15 +01:00
DJ2LS 8081a44e92 adjusted server lookup path 2024-03-01 15:37:39 +01:00
DJ2LS 470503eb5c adjusted server lookup path 2024-03-01 15:14:18 +01:00
DJ2LS e221637394 adjusted lookup path and updated gh action 2024-03-01 12:10:49 +01:00
DJ2LS 14cbd46a88 adjusted lookup path 2024-03-01 10:39:10 +01:00
DJ2LS 2ee4776693 adjusted lookup path 2024-03-01 09:44:57 +01:00
DJ2LS b2657bcbbd adjusted codec2 file search path 2024-02-29 20:34:27 +01:00
DJ2LS 515f895ed3 more error handling 2024-02-29 19:33:12 +01:00
DJ2LS 6c9439ac70 adjusted server lookup path 2024-02-29 17:46:34 +01:00
DJ2LS 2f21dc6abc adjusted server lookup path 2024-02-29 17:45:40 +01:00
DJ2LS 2ea6a4fe13 removed updater, auto start server 2024-02-29 16:46:56 +01:00
DJ2LS 84504f734f removed updater, auto start server 2024-02-29 16:36:56 +01:00
DJ2LS 100c871cd9 first attempt creating a full bundle 2024-02-29 15:27:39 +01:00
DJ2LS e1b5872e26 first attempt creating a full bundle 2024-02-29 15:05:54 +01:00
DJ2LS 744ed425c3
Merge pull request #666 from DJ2LS/develop
0.14.0
2024-02-28 21:19:47 +01:00
DJ2LS 2b21aab26b bump version 2024-02-28 21:18:43 +01:00
DJ2LS c369037deb
Merge pull request #665 from DJ2LS/dev-arq
mode gear shifting
2024-02-28 20:18:19 +01:00
DJ2LS c8e6e11d84 removed ujson dependency 2024-02-28 20:01:26 +01:00
DJ2LS 05a274edeb attempt fixing macos server build 2024-02-28 19:44:50 +01:00
DJ2LS bdaed0e873 fixed test 2024-02-28 19:41:06 +01:00
DJ2LS 6566412d64 Merge remote-tracking branch 'origin/dev-arq' into dev-arq 2024-02-28 09:56:33 +01:00
DJ2LS 25de300970 fixed test 2024-02-28 09:56:28 +01:00
codefactor-io 2f07441fc6
[CodeFactor] Apply fixes 2024-02-28 08:27:09 +00:00
DJ2LS fd9fb81fa2 adjusted stats 2024-02-27 22:39:45 +01:00
DJ2LS 9706260933 adjust speed level with info ack 2024-02-25 21:23:15 +01:00
DJ2LS 4a34386c26 small adjustments to api validation, hopefully catching mygrid 4/6 error 2024-02-25 21:08:45 +01:00
DJ2LS 419f7732df more try/except for schedule manager 2024-02-25 20:44:05 +01:00
DJ2LS 59778165bf fixed some tests 2024-02-24 22:27:01 +01:00
DJ2LS 0c322bacf8 WIP gui stats 2024-02-24 22:10:41 +01:00
DJ2LS e7ce198fa1 WIP gui stats 2024-02-24 22:01:15 +01:00
DJ2LS dd4ca1903b added session statistics 2024-02-24 21:49:53 +01:00
DJ2LS 956cede593 try except for schedule manager 2024-02-24 20:34:53 +01:00
DJ2LS 374f400f30 adding fallback speed level 2024-02-23 15:21:52 +01:00
DJ2LS 36890fe131 adding fallback speed level 2024-02-23 14:38:08 +01:00
DJ2LS d3d09d4019 attempt of sending in previous speed level 2024-02-23 13:59:47 +01:00
DJ2LS f307ed779f attempt of sending in previous speed level 2024-02-23 13:55:15 +01:00
DJ2LS 7ecccabcc0 fixed arq burst 2024-02-23 08:59:03 +01:00
DJ2LS 33ad50fbe2 moved from upshift downshift to speed level int 2024-02-22 21:49:57 +01:00
DJ2LS 3574f76a79 moved from upshift downshift to speed level int 2024-02-22 21:15:43 +01:00
DJ2LS 0100104afb fixed missing arq abort state 2024-02-22 16:03:28 +01:00
DJ2LS 89f61c15fd adjusted gear shifting 2024-02-22 15:46:15 +01:00
DJ2LS 7eb9fa1dc5 adjusted gear shifting 2024-02-22 15:39:18 +01:00
DJ2LS 451ec404e9 adjusted gear shifting 2024-02-22 15:30:21 +01:00
DJ2LS e64d71b135 set initial speed level 2024-02-22 15:18:25 +01:00
DJ2LS 22f0226600 gear shifting test 2024-02-22 15:05:54 +01:00
DJ2LS 836f4b99d8 fixed toasts z index 2024-02-22 13:02:23 +01:00
DJ2LS c64ea890d1
Merge pull request #660 from DJ2LS/develop
adjusted rigctld and config handling
2024-02-22 12:51:38 +01:00
DJ2LS 6c147106df bump version 2024-02-22 10:59:51 +01:00
DJ2LS 6f64b61ea5 adjusted config setting for avoiding restart 2024-02-22 10:58:28 +01:00
DJ2LS e284a58db9 iterate through all rigctld binaries 2024-02-22 10:49:30 +01:00
DJ2LS 8ccff438d0 hopefully fixed rigctld fetching 2024-02-22 10:26:18 +01:00
DJ2LS cf06bbffea Prettified Code! 2024-02-21 16:46:47 +00:00
DJ2LS a6eec88337
Merge pull request #656 from DJ2LS/develop
several arq related fixes
2024-02-21 17:46:32 +01:00
DJ2LS f33222794b
Merge pull request #657 from DJ2LS/dev-radio-control
adjusted radio control and some more fixes
2024-02-21 17:19:50 +01:00
DJ2LS 9db78d1031 arq adjustments 2024-02-21 17:05:28 +01:00
DJ2LS 0349cf1b7c callsign related adustments... 2024-02-21 16:55:45 +01:00
DJ2LS fbcc49019f fixed missing updater 2024-02-21 16:52:26 +01:00
DJ2LS 8b65b6240b added updater 2024-02-21 11:18:57 +01:00
DJ2LS 98d8812571 fixed modals in background 2024-02-21 11:10:24 +01:00
DJ2LS c42ac793b9 added error check to gui 2024-02-21 11:01:24 +01:00
DJ2LS 2156a8fa8f added updater placeholder... 2024-02-20 11:05:57 +01:00
DJ2LS ba5fbd3a71 introduced radioHandler.ts 2024-02-20 10:05:13 +01:00
DJ2LS 5c232a2165 forgot a 0 ... 2024-02-20 09:52:26 +01:00
DJ2LS 31a93b3183 adjusted state fetching and rf power setting 2024-02-20 09:39:47 +01:00
DJ2LS fea294b26f fixed radio api 2024-02-20 09:13:34 +01:00
DJ2LS f76dc5da14 first attempt fixing radio mode related problems by separating parameters 2024-02-20 08:09:05 +01:00
DJ2LS f1971cdf4f Merge remote-tracking branch 'origin/develop' into develop 2024-02-19 20:08:11 +01:00
DJ2LS 1337a4a0c8 fixed more tests 2024-02-19 20:08:05 +01:00
DJ2LS 347a916a34
Merge pull request #645 from DJ2LS/dependabot/npm_and_yarn/gui/develop/socket.io-4.7.4 2024-02-19 11:46:05 +01:00
DJ2LS b1a1a40e97
Merge branch 'develop' into dependabot/npm_and_yarn/gui/develop/socket.io-4.7.4 2024-02-19 11:46:00 +01:00
DJ2LS 4405293a90
Merge pull request #644 from DJ2LS/dependabot/npm_and_yarn/gui/develop/vite-plugin-electron-0.28.2 2024-02-19 11:45:50 +01:00
DJ2LS d95bea09a8
Merge branch 'develop' into dependabot/npm_and_yarn/gui/develop/vite-plugin-electron-0.28.2 2024-02-19 11:45:43 +01:00
DJ2LS b7563040ef only retry first result 2024-02-19 11:06:15 +01:00
DJ2LS 30de19f729 possibly fixed repeating message 2024-02-19 10:59:53 +01:00
DJ2LS 2c24545e68 possibly fixed repeating message 2024-02-19 10:53:44 +01:00
DJ2LS 35276b01ef improved message tests which went stuck 2024-02-19 08:37:16 +01:00
DJ2LS 10be8db7d0 improved arq tests which went stuck 2024-02-19 08:27:40 +01:00
DJ2LS 1cfae172bb
Merge branch 'develop' into dependabot/npm_and_yarn/gui/develop/socket.io-4.7.4 2024-02-19 07:48:03 +01:00
DJ2LS 4a3a0e4893
Merge branch 'develop' into dependabot/npm_and_yarn/gui/develop/vite-plugin-electron-0.28.2 2024-02-19 07:47:59 +01:00
DJ2LS fde3de12d6
Merge pull request #643 from DJ2LS/dependabot/npm_and_yarn/gui/develop/vitejs/plugin-vue-5.0.4 2024-02-19 07:44:57 +01:00
DJ2LS b002e7136e
Merge branch 'develop' into dependabot/npm_and_yarn/gui/develop/vitejs/plugin-vue-5.0.4 2024-02-19 07:44:10 +01:00
dependabot[bot] 25bd486f8e
Bump vite-plugin-electron from 0.28.0 to 0.28.2 in /gui
Bumps [vite-plugin-electron](https://github.com/electron-vite/vite-plugin-electron) from 0.28.0 to 0.28.2.
- [Release notes](https://github.com/electron-vite/vite-plugin-electron/releases)
- [Changelog](https://github.com/electron-vite/vite-plugin-electron/blob/main/CHANGELOG.md)
- [Commits](https://github.com/electron-vite/vite-plugin-electron/compare/v0.28.0...v0.28.2)

---
updated-dependencies:
- dependency-name: vite-plugin-electron
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-02-19 06:24:03 +00:00
DJ2LS 9969b214d9
Merge pull request #642 from DJ2LS/dependabot/npm_and_yarn/gui/develop/eslint-plugin-prettier-5.1.3 2024-02-19 07:23:31 +01:00
DJ2LS 084c1143ee
Merge branch 'develop' into dependabot/npm_and_yarn/gui/develop/eslint-plugin-prettier-5.1.3 2024-02-19 07:23:22 +01:00
DJ2LS 67a3ab31e7
Merge pull request #654 from DJ2LS/dependabot/npm_and_yarn/gui/develop/vite-5.1.3 2024-02-19 07:22:15 +01:00
DJ2LS 91941eec7b
Merge branch 'develop' into dependabot/npm_and_yarn/gui/develop/vite-5.1.3 2024-02-19 07:22:03 +01:00
Mashintime 6db6c486a3 Remove print (accidentally committed) 2024-02-18 15:46:40 -05:00
Mashintime d87579f9ac Merge branch 'develop' of github.com:DJ2LS/FreeDATA into develop 2024-02-18 15:39:08 -05:00
Mashintime ee6ca66602
Merge pull request #655 from DJ2LS/dev-message-auto-repeat
message auto repeat
2024-02-18 15:37:37 -05:00
Mashintime 0213e538fa
Merge pull request #653 from DJ2LS/dev-arq
arq adjustments
2024-02-18 15:37:04 -05:00
Mashintime 796d1c0566 Only restart modem if config is valid 2024-02-18 15:34:10 -05:00
Mashintime 47242fb33e Remove duplicate setting 2024-02-18 15:33:42 -05:00
Mashintime 70228054fd Remove unused setting 2024-02-18 15:33:29 -05:00
DJ2LS f8bff53eae updated example config and typo 2024-02-18 21:31:08 +01:00
DJ2LS 2bfc8c345a Merge remote-tracking branch 'origin/dev-arq' into dev-message-auto-repeat 2024-02-18 21:19:21 +01:00
DJ2LS 7d33c0aad9 Merge remote-tracking branch 'origin/dev-arq' into dev-arq 2024-02-18 21:18:37 +01:00
DJ2LS dbc959d06e added config related parts 2024-02-18 21:15:33 +01:00
DJ2LS 3a84ec0bbb Merge remote-tracking branch 'origin/dev-arq' into dev-message-auto-repeat 2024-02-18 21:11:06 +01:00
DJ2LS 7a09f94767 added config related parts 2024-02-18 21:08:14 +01:00
DJ2LS 25dedfde6c Merge remote-tracking branch 'origin/develop' into dev-message-auto-repeat 2024-02-18 20:38:55 +01:00
DJ2LS e90d1f7716 adjusted config 2024-02-18 20:31:01 +01:00
Mashintime 273914d714 Extra serial port in config.py 2024-02-18 13:57:53 -05:00
Mashintime 44d24123b1 Remove enable_fft from gui settingstore 2024-02-18 13:57:16 -05:00
Mashintime d7bd9c86a8 Merge branch 'develop' of github.com:DJ2LS/FreeDATA into develop 2024-02-18 12:43:09 -05:00
DJ2LS 303ac0d6ef
Merge branch 'develop' into dev-arq 2024-02-18 16:16:24 +01:00
dependabot[bot] b52d55285c
Bump vite from 5.0.12 to 5.1.3 in /gui
Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 5.0.12 to 5.1.3.
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/main/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v5.1.3/packages/vite)

---
updated-dependencies:
- dependency-name: vite
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-02-18 14:08:57 +00:00
DJ2LS 5ef97720b0
Merge pull request #652 from DJ2LS/dev-update-config
auto update server config
2024-02-18 15:07:33 +01:00
DJ2LS c329070606
Merge pull request #651 from DJ2LS/more-rigctl
Rigctl argument tweaks
2024-02-18 15:07:15 +01:00
DJ2LS 7f86ab2ece arq adjustments and attempt fixing tests 2024-02-18 15:06:13 +01:00
Mashintime 5bee82a17c Further adjustment 2024-02-17 16:57:54 -05:00
DJ2LS 657a6a8967 default values as list 2024-02-17 20:45:51 +01:00
DJ2LS 404585ebe0 auto update server config 2024-02-17 20:42:07 +01:00
Mashintime c26f9cb9ba Incr Next Version 2024-02-17 12:55:10 -05:00
Mashintime 7eaac1cc29 Fix typos/remove rts 2024-02-17 11:03:59 -05:00
Mashintime a07249213e
Merge branch 'develop' into more-rigctl 2024-02-17 10:53:04 -05:00
Mashintime b69e485f10 Merge branch 'develop' of github.com:DJ2LS/FreeDATA into develop 2024-02-17 10:48:44 -05:00
Mashintime fc055671cb Adjustments to rigctld arguments 2024-02-17 10:41:56 -05:00
DJ2LS 916c2a4a63 delete beacons older than 2 days 2024-02-17 10:41:33 -05:00
DJ2LS 8d47d4890e removed rigctld typo 2024-02-17 10:41:33 -05:00
DJ2LS b569cbc315 adjusted a rigctld command 2024-02-17 10:41:33 -05:00
DJ2LS 4f50b802ac adjusted state manager in data type handler 2024-02-17 10:41:33 -05:00
DJ2LS 0e1986b2da removed possibly obsolete arq state at wrong position 2024-02-17 10:41:33 -05:00
DJ2LS 390817caa7 repeat message when beacon received 2024-02-16 11:02:10 +01:00
DJ2LS 11a27bcbd7
Merge pull request #650 from DJ2LS/dev-arq 2024-02-14 22:57:32 +01:00
DJ2LS 877b517b72 delete beacons older than 2 days 2024-02-14 16:45:19 +01:00
DJ2LS 9a8ebfef77 removed rigctld typo 2024-02-14 16:29:18 +01:00
DJ2LS b017f39133 adjusted a rigctld command 2024-02-14 16:20:00 +01:00
DJ2LS 6dd78bf8fc adjusted state manager in data type handler 2024-02-14 09:27:20 +01:00
DJ2LS fffc59b0a6 removed possibly obsolete arq state at wrong position 2024-02-14 09:13:39 +01:00
Mashintime 12d1010da9 Bump version 2024-02-12 16:17:24 -05:00
Mashintime 3b505d24f2
Merge pull request #648 from DJ2LS/develop
Develop
2024-02-12 16:14:46 -05:00
Mashintime fc9e848f1f
Merge branch 'main' into develop 2024-02-12 16:14:20 -05:00
DJ2LS f437b2a01b
Merge pull request #647 from DJ2LS/rigctld-win
Internal rigctld tweaks for windows users
2024-02-11 21:38:24 +01:00
DJ2LS 0738fe1454 removed 32bit hamlib build 2024-02-11 20:59:29 +01:00
Mashintime 321dda3fd9 Include custom args when starting rigctld 2024-02-11 13:09:51 -05:00
Mashintime 37bc01e426 Check Program Files on Windows for Hamlib 2024-02-11 13:07:48 -05:00
Mashintime f11c61c8a6 Remove default settings from example config 2024-02-11 13:05:03 -05:00
dependabot[bot] acb2bb4e9b
Bump socket.io from 4.7.2 to 4.7.4 in /gui
Bumps [socket.io](https://github.com/socketio/socket.io) from 4.7.2 to 4.7.4.
- [Release notes](https://github.com/socketio/socket.io/releases)
- [Changelog](https://github.com/socketio/socket.io/blob/main/CHANGELOG.md)
- [Commits](https://github.com/socketio/socket.io/compare/4.7.2...4.7.4)

---
updated-dependencies:
- dependency-name: socket.io
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-02-11 10:20:01 +00:00
dependabot[bot] 910690178e
Bump @vitejs/plugin-vue from 5.0.3 to 5.0.4 in /gui
Bumps [@vitejs/plugin-vue](https://github.com/vitejs/vite-plugin-vue/tree/HEAD/packages/plugin-vue) from 5.0.3 to 5.0.4.
- [Release notes](https://github.com/vitejs/vite-plugin-vue/releases)
- [Changelog](https://github.com/vitejs/vite-plugin-vue/blob/main/packages/plugin-vue/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite-plugin-vue/commits/plugin-vue@5.0.4/packages/plugin-vue)

---
updated-dependencies:
- dependency-name: "@vitejs/plugin-vue"
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-02-11 10:19:32 +00:00
dependabot[bot] 950eab71fe
Bump eslint-plugin-prettier from 5.0.1 to 5.1.3 in /gui
Bumps [eslint-plugin-prettier](https://github.com/prettier/eslint-plugin-prettier) from 5.0.1 to 5.1.3.
- [Release notes](https://github.com/prettier/eslint-plugin-prettier/releases)
- [Changelog](https://github.com/prettier/eslint-plugin-prettier/blob/master/CHANGELOG.md)
- [Commits](https://github.com/prettier/eslint-plugin-prettier/compare/v5.0.1...v5.1.3)

---
updated-dependencies:
- dependency-name: eslint-plugin-prettier
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-02-11 10:19:21 +00:00
DJ2LS 38c54b575a
Merge pull request #640 from DJ2LS/develop
start rigctld automatically & fix for missing gui config
2024-02-11 11:18:31 +01:00
DJ2LS c15e489b1c
Merge pull request #630 from DJ2LS/dependabot/npm_and_yarn/gui/develop/eslint-plugin-n-16.6.2
Bump eslint-plugin-n from 16.1.0 to 16.6.2 in /gui
2024-02-11 10:07:35 +01:00
DJ2LS 923ab36668
Merge pull request #631 from DJ2LS/dependabot/npm_and_yarn/gui/develop/vitest-1.2.2
Bump vitest from 1.0.2 to 1.2.2 in /gui
2024-02-11 10:07:26 +01:00
DJ2LS 21ab92f873
Merge pull request #632 from DJ2LS/dependabot/npm_and_yarn/gui/develop/eslint-config-standard-with-typescript-43.0.1
Bump eslint-config-standard-with-typescript from 43.0.0 to 43.0.1 in /gui
2024-02-11 10:07:19 +01:00
DJ2LS 75c5227536
Merge pull request #634 from DJ2LS/dependabot/npm_and_yarn/gui/develop/typescript-eslint/eslint-plugin-6.21.0
Bump @typescript-eslint/eslint-plugin from 6.19.1 to 6.21.0 in /gui
2024-02-11 10:07:07 +01:00
DJ2LS fa9d684853
Merge pull request #635 from DJ2LS/dependabot/npm_and_yarn/gui/develop/electron-28.2.2
Bump electron from 28.1.3 to 28.2.2 in /gui
2024-02-11 10:07:00 +01:00
DJ2LS a0525ef01b adjusted dependabot 2024-02-11 10:06:47 +01:00
DJ2LS bb668a1ac9 fixed control check 2024-02-11 10:00:57 +01:00
dependabot[bot] 8332690c67
Bump electron from 28.1.3 to 28.2.2 in /gui
Bumps [electron](https://github.com/electron/electron) from 28.1.3 to 28.2.2.
- [Release notes](https://github.com/electron/electron/releases)
- [Changelog](https://github.com/electron/electron/blob/main/docs/breaking-changes.md)
- [Commits](https://github.com/electron/electron/compare/v28.1.3...v28.2.2)

---
updated-dependencies:
- dependency-name: electron
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-02-11 08:58:09 +00:00
DJ2LS 139bdc7699 version update 2024-02-11 09:57:07 +01:00
DJ2LS 2ccabc28bc
Merge pull request #639 from DJ2LS/dev-start-rigctld-process
start rigctld from local binary
2024-02-11 09:56:37 +01:00
codefactor-io 8525a7a321
[CodeFactor] Apply fixes 2024-02-11 08:55:56 +00:00
DJ2LS 045cb38a63 fixed healthcheck 2024-02-11 09:54:18 +01:00
DJ2LS 9734ce9a1c fixed healthcheck 2024-02-11 09:21:08 +01:00
DJ2LS 64e5e35b6f added internal/external hamlib 2024-02-11 09:17:52 +01:00
DJ2LS c3d558a07a
Merge pull request #638 from arodland/patch-1
fix loading default config
2024-02-11 09:02:29 +01:00
DJ2LS a955d45518 adjusted settings 2024-02-11 08:48:49 +01:00
Andrew Rodland 7099d2e379
fix loading default config
without this change, if the user doesn't have a valid config on disk, they just get a white screen on startup due to `nconf.required` throwing an exception.
2024-02-10 23:13:53 -05:00
DJ2LS 5093abc1dc use system wide path as well for lookup 2024-02-10 21:44:02 +01:00
DJ2LS 704186c3c2 added downloading windwos hamlib releases for nsis 2024-02-10 21:36:14 +01:00
DJ2LS f239a1a3be first attempt using rigctld... 2024-02-10 21:28:07 +01:00
DJ2LS fd63cc7fa7
Merge pull request #637 from DJ2LS/develop
Config related adjustments and build process update
2024-02-10 14:01:21 +01:00
codefactor-io 74d79204ec
[CodeFactor] Apply fixes to commit ec34a69 2024-02-10 12:57:33 +00:00
DJ2LS ec34a690e9 improved gui reconnecting 2024-02-10 13:57:18 +01:00
DJ2LS 17bce4b0db version update 2024-02-10 11:05:55 +01:00
DJ2LS d38b3bc672 reduced explorer interval 2024-02-10 11:05:10 +01:00
DJ2LS c89a809e6d adjusted some default config 2024-02-10 11:02:13 +01:00
DJ2LS d906d9ab5e adjusted artifact names 2024-02-10 07:06:16 +01:00
DJ2LS a6c671e113
Merge pull request #636 from DJ2LS/main-cf-autofix
Apply fixes from CodeFactor
2024-02-08 22:18:41 +01:00
codefactor-io 2f977e748b
[CodeFactor] Apply fixes to commit ba8a8bc 2024-02-08 21:05:57 +00:00
dependabot[bot] 8d7b961f4e
Bump @typescript-eslint/eslint-plugin from 6.19.1 to 6.21.0 in /gui
Bumps [@typescript-eslint/eslint-plugin](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/eslint-plugin) from 6.19.1 to 6.21.0.
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/eslint-plugin/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v6.21.0/packages/eslint-plugin)

---
updated-dependencies:
- dependency-name: "@typescript-eslint/eslint-plugin"
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-02-06 16:51:39 +00:00
dependabot[bot] 4f3b4b2762
Bump eslint-config-standard-with-typescript in /gui
Bumps [eslint-config-standard-with-typescript](https://github.com/mightyiam/eslint-config-standard-with-typescript) from 43.0.0 to 43.0.1.
- [Release notes](https://github.com/mightyiam/eslint-config-standard-with-typescript/releases)
- [Changelog](https://github.com/mightyiam/eslint-config-standard-with-typescript/blob/master/CHANGELOG.md)
- [Commits](https://github.com/mightyiam/eslint-config-standard-with-typescript/compare/v43.0.0...v43.0.1)

---
updated-dependencies:
- dependency-name: eslint-config-standard-with-typescript
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-02-05 16:34:47 +00:00
dependabot[bot] 243dc771fd
Bump vitest from 1.0.2 to 1.2.2 in /gui
Bumps [vitest](https://github.com/vitest-dev/vitest/tree/HEAD/packages/vitest) from 1.0.2 to 1.2.2.
- [Release notes](https://github.com/vitest-dev/vitest/releases)
- [Commits](https://github.com/vitest-dev/vitest/commits/v1.2.2/packages/vitest)

---
updated-dependencies:
- dependency-name: vitest
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-02-05 16:34:38 +00:00
dependabot[bot] a99bece107
Bump eslint-plugin-n from 16.1.0 to 16.6.2 in /gui
Bumps [eslint-plugin-n](https://github.com/eslint-community/eslint-plugin-n) from 16.1.0 to 16.6.2.
- [Release notes](https://github.com/eslint-community/eslint-plugin-n/releases)
- [Commits](https://github.com/eslint-community/eslint-plugin-n/compare/16.1.0...16.6.2)

---
updated-dependencies:
- dependency-name: eslint-plugin-n
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-02-05 16:34:23 +00:00
109 changed files with 3566 additions and 3788 deletions

View file

@ -6,19 +6,19 @@ updates:
- package-ecosystem: "github-actions" - package-ecosystem: "github-actions"
directory: "/" directory: "/"
schedule: schedule:
interval: "daily" interval: "monthly"
target-branch: "develop" target-branch: "develop"
# Maintain dependencies for npm # Maintain dependencies for npm
- package-ecosystem: "npm" - package-ecosystem: "npm"
directory: "/gui" directory: "/gui"
schedule: schedule:
interval: "daily" interval: "monthly"
target-branch: "develop" target-branch: "develop"
# Maintain dependencies for pip # Maintain dependencies for pip
- package-ecosystem: "pip" - package-ecosystem: "pip"
directory: "/" directory: "/"
schedule: schedule:
interval: "daily" interval: "monthly"
target-branch: "develop" target-branch: "develop"

View file

@ -14,11 +14,28 @@ jobs:
with: with:
python-version: "3.11" python-version: "3.11"
- name: Electron Builder
working-directory: gui
run: |
npm i
npm run build
- name: LIST ALL FILES
run: ls -R
- name: Install Python dependencies - name: Install Python dependencies
run: | run: |
python -m pip install --upgrade pip python -m pip install --upgrade pip
pip install -r requirements.txt pip install -r requirements.txt
- uses: robinraju/release-downloader@v1.9
with:
repository: "Hamlib/Hamlib"
fileName: " hamlib-w64-*.zip"
latest: true
extract: true
out-file-path: "modem/lib/hamlib/"
- name: Build binaries - name: Build binaries
working-directory: modem working-directory: modem
run: | run: |
@ -28,9 +45,9 @@ jobs:
run: ls -R run: ls -R
- name: Create installer - name: Create installer
uses: joncloud/makensis-action@v4 uses: joncloud/makensis-action@v4.1
with: with:
script-file: "freedata-server-nsis-config.nsi" script-file: "freedata-nsis-config.nsi"
arguments: '/V3' arguments: '/V3'
- name: LIST ALL FILES - name: LIST ALL FILES
@ -39,12 +56,14 @@ jobs:
- name: Upload artifact - name: Upload artifact
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4
with: with:
name: 'FreeData-Server-Installer' name: 'FreeDATA-Installer'
path: ./FreeData-Server-Installer.exe path: ./FreeDATA-Installer.exe
- name: Upload Installer to Release - name: Upload Installer to Release
uses: softprops/action-gh-release@v1 uses: softprops/action-gh-release@v1
if: startsWith(github.ref, 'refs/tags/v') if: startsWith(github.ref, 'refs/tags/v')
with: with:
draft: true draft: true
files: ./FreeData-Server-Installer.exe files: ./FreeDATA-Installer.exe
tag_name: ${{ github.ref_name }}
name: 'FreeDATA-Installer-${{ github.ref_name }}'

View file

@ -48,6 +48,7 @@ jobs:
brew install portaudio brew install portaudio
python -m pip install --upgrade pip python -m pip install --upgrade pip
pip3 install pyaudio pip3 install pyaudio
export PYTHONPATH=/Library/Frameworks/Python.framework/Versions/3.11/lib/:$PYTHONPATH
- name: Install Python dependencies - name: Install Python dependencies
run: | run: |

132
freedata-nsis-config.nsi Normal file
View file

@ -0,0 +1,132 @@
!include "MUI2.nsh"
; Request administrative rights
RequestExecutionLevel admin
; The name and file name of the installer
Name "FreeDATA Installer"
OutFile "FreeDATA-Installer.exe"
; Default installation directory for the server
InstallDir "$LOCALAPPDATA\FreeDATA"
; Registry key to store the installation directory
InstallDirRegKey HKCU "Software\FreeDATA" "Install_Dir"
; Modern UI settings
!define MUI_ABORTWARNING
; Installer interface settings
!define MUI_ICON "documentation\icon.ico"
!define MUI_UNICON "documentation\icon.ico" ; Icon for the uninstaller
; Define the welcome page text
!define MUI_WELCOMEPAGE_TEXT "Welcome to the FreeDATA Setup Wizard. This wizard will guide you through the installation process."
!define MUI_FINISHPAGE_TEXT "Folder: $INSTDIR"
!define MUI_DIRECTORYPAGE_TEXT_TOP "Please select the installation folder. It's recommended to use the suggested one to avoid permission problems."
; Pages
!insertmacro MUI_PAGE_WELCOME
!insertmacro MUI_PAGE_LICENSE "LICENSE"
!insertmacro MUI_PAGE_COMPONENTS
!insertmacro MUI_PAGE_DIRECTORY
!insertmacro MUI_PAGE_INSTFILES
!insertmacro MUI_PAGE_FINISH
; Uninstaller
!insertmacro MUI_UNPAGE_WELCOME
!insertmacro MUI_UNPAGE_CONFIRM
!insertmacro MUI_UNPAGE_INSTFILES
!insertmacro MUI_UNPAGE_FINISH
; Language (you can choose and configure the language(s) you want)
!insertmacro MUI_LANGUAGE "English"
; Installer Sections
Section "FreeData Server" SEC01
; Set output path to the installation directory
SetOutPath $INSTDIR\freedata-server
; Check if "config.ini" exists and back it up
IfFileExists $INSTDIR\freedata-server\config.ini backupConfig
doneBackup:
; Add your application files here
File /r "modem\server.dist\*"
; Restore the original "config.ini" if it was backed up
IfFileExists $INSTDIR\freedata-server\config.ini.bak restoreConfig
; Create a shortcut in the user's desktop
CreateShortCut "$DESKTOP\FreeDATA Server.lnk" "$INSTDIR\freedata-server\freedata-server.exe"
; Create Uninstaller
WriteUninstaller "$INSTDIR\Uninstall.exe"
; Create a Start Menu directory
CreateDirectory "$SMPROGRAMS\FreeDATA"
; Create shortcut in the Start Menu directory
CreateShortCut "$SMPROGRAMS\FreeDATA\FreeDATA Server.lnk" "$INSTDIR\freedata-server\freedata-server.exe"
; Create an Uninstall shortcut
CreateShortCut "$SMPROGRAMS\FreeDATA\Uninstall FreeDATA.lnk" "$INSTDIR\Uninstall.exe"
; Backup "config.ini" before overwriting files
backupConfig:
Rename $INSTDIR\freedata-server\config.ini $INSTDIR\freedata-server\config.ini.bak
Goto doneBackup
; Restore the original "config.ini"
restoreConfig:
Delete $INSTDIR\freedata-server\config.ini
Rename $INSTDIR\freedata-server\config.ini.bak $INSTDIR\freedata-server\config.ini
SectionEnd
Section "FreeData x64 GUI" SEC02
; Set output path to the GUI installation directory
SetOutPath $INSTDIR\freedata-gui
; Add GUI files here
File /r "gui\release\win-unpacked\*"
; Create a shortcut on the desktop for the GUI
CreateShortCut "$DESKTOP\FreeDATA GUI.lnk" "$INSTDIR\freedata-gui\freedata.exe"
; Create a start menu shortcut
CreateShortCut "$SMPROGRAMS\FreeDATA\FreeDATA GUI.lnk" "$INSTDIR\freedata-gui\freedata.exe"
; Create an Uninstall shortcut
CreateShortCut "$SMPROGRAMS\FreeDATA\Uninstall FreeDATA.lnk" "$INSTDIR\Uninstall.exe"
SectionEnd
; Uninstaller Section
Section "Uninstall"
; Delete files and directories for the server
Delete $INSTDIR\freedata-server\*.*
RMDir /r $INSTDIR\freedata-server
; Delete files and directories for the GUI
Delete $INSTDIR\freedata-gui\*.*
RMDir /r $INSTDIR\freedata-gui
; Remove the desktop shortcuts
Delete "$DESKTOP\FreeDATA Server.lnk"
Delete "$DESKTOP\FreeDATA GUI.lnk"
; Remove Start Menu shortcuts
Delete "$SMPROGRAMS\FreeDATA\*.*"
RMDir "$SMPROGRAMS\FreeDATA"
; Attempt to delete the uninstaller itself
Delete $EXEPATH
; Now remove the installation directory if it's empty
RMDir /r $INSTDIR
SectionEnd

View file

@ -1,102 +0,0 @@
!include "MUI2.nsh"
; Request administrative rights
RequestExecutionLevel admin
; The name and file name of the installer
Name "FreeData Server"
OutFile "FreeData-Server-Installer.exe"
; Default installation directory
; InstallDir "$PROGRAMFILES\FreeData\freedata-server"
InstallDir "$LOCALAPPDATA\FreeData\freedata-server"
; Registry key to store the installation directory
InstallDirRegKey HKCU "Software\FreeData\freedata-server" "Install_Dir"
; Modern UI settings
!define MUI_ABORTWARNING
; Installer interface settings
!define MUI_ICON "documentation\icon.ico"
!define MUI_UNICON "documentation\icon.ico" ; Icon for the uninstaller
; Define the welcome page text
!define MUI_WELCOMEPAGE_TEXT "Welcome to the FreeData Server Setup Wizard. This wizard will guide you through the installation process."
!define MUI_FINISHPAGE_TEXT "Folder: $INSTDIR"
!define MUI_DIRECTORYPAGE_TEXT_TOP "Please select the installation folder. Its recommended using the suggested one for avoiding permission problems."
; Pages
!insertmacro MUI_PAGE_WELCOME
!insertmacro MUI_PAGE_LICENSE "LICENSE"
;!insertmacro MUI_PAGE_COMPONENTS
!insertmacro MUI_PAGE_DIRECTORY
!insertmacro MUI_PAGE_INSTFILES
!insertmacro MUI_PAGE_FINISH
; Uninstaller
!insertmacro MUI_UNPAGE_WELCOME
!insertmacro MUI_UNPAGE_CONFIRM
!insertmacro MUI_UNPAGE_INSTFILES
!insertmacro MUI_UNPAGE_FINISH
; Language (you can choose and configure the language(s) you want)
!insertmacro MUI_LANGUAGE "English"
; Installer Sections
Section "FreeData Server" SEC01
; Set output path to the installation directory
SetOutPath $INSTDIR
; Check if "config.ini" exists and back it up
IfFileExists $INSTDIR\config.ini backupConfig
doneBackup:
; Add your application files here
File /r "modem\server.dist\*.*"
; Restore the original "config.ini" if it was backed up
IfFileExists $INSTDIR\config.ini.bak restoreConfig
; Create a shortcut in the user's desktop
CreateShortCut "$DESKTOP\FreeData Server.lnk" "$INSTDIR\freedata-server.exe"
; Create Uninstaller
WriteUninstaller "$INSTDIR\Uninstall.exe"
; Backup "config.ini" before overwriting files
backupConfig:
Rename $INSTDIR\config.ini $INSTDIR\config.ini.bak
Goto doneBackup
; Restore the original "config.ini"
restoreConfig:
Delete $INSTDIR\config.ini
Rename $INSTDIR\config.ini.bak $INSTDIR\config.ini
SectionEnd
; Uninstaller Section
Section "Uninstall"
; Delete files and directories
Delete $INSTDIR\freedata-server.exe
RMDir /r $INSTDIR
; Remove the shortcut
Delete "$DESKTOP\FreeData Server.lnk"
; Additional uninstallation commands here
SectionEnd

View file

@ -39,29 +39,23 @@
"gatekeeperAssess": false, "gatekeeperAssess": false,
"mergeASARs": true, "mergeASARs": true,
"x64ArchFiles": "**/*", "x64ArchFiles": "**/*",
"artifactName": "${productName}-Mac-${version}.${ext}" "artifactName": "${productName}-GUI-Mac-${version}.${ext}"
}, },
"win": { "win": {
"icon": "build/icon.png", "icon": "build/icon.png",
"target": [ "target": [
{ {
"target": "nsis", "target": "portable",
"arch": ["arm64", "x64"] "arch": ["arm64", "x64"]
} }
], ],
"artifactName": "${productName}-Windows-${version}.${ext}" "artifactName": "${productName}-GUI-Windows-${version}.${ext}"
},
"nsis": {
"oneClick": false,
"perMachine": false,
"allowToChangeInstallationDirectory": true,
"deleteAppDataOnUninstall": true
}, },
"linux": { "linux": {
"category": "Development", "category": "Development",
"target": [ "target": [
"AppImage" "AppImage"
], ],
"artifactName": "${productName}-${version}.${ext}" "artifactName": "${productName}-GUI-Linux-${version}.${ext}"
} }
} }

View file

@ -1,7 +1,6 @@
import { app, BrowserWindow, shell, ipcMain } from "electron"; import { app, BrowserWindow, shell, ipcMain } from "electron";
import { release, platform } from "node:os"; import { release, platform } from "os";
import { join } from "node:path"; import { join, dirname } from "path";
import { autoUpdater } from "electron-updater";
import { existsSync } from "fs"; import { existsSync } from "fs";
import { spawn } from "child_process"; import { spawn } from "child_process";
@ -20,7 +19,6 @@ process.env.DIST = join(process.env.DIST_ELECTRON, "../dist");
process.env.VITE_PUBLIC = process.env.VITE_DEV_SERVER_URL process.env.VITE_PUBLIC = process.env.VITE_DEV_SERVER_URL
? join(process.env.DIST_ELECTRON, "../public") ? join(process.env.DIST_ELECTRON, "../public")
: process.env.DIST; : process.env.DIST;
process.env.FDUpdateAvail = "0";
// Disable GPU Acceleration for Windows 7 // Disable GPU Acceleration for Windows 7
if (release().startsWith("6.1")) app.disableHardwareAcceleration(); if (release().startsWith("6.1")) app.disableHardwareAcceleration();
@ -40,7 +38,7 @@ if (!app.requestSingleInstanceLock()) {
// process.env['ELECTRON_DISABLE_SECURITY_WARNINGS'] = 'true' // process.env['ELECTRON_DISABLE_SECURITY_WARNINGS'] = 'true'
// set daemon process var // set daemon process var
var daemonProcess = null; var serverProcess = null;
let win: BrowserWindow | null = null; let win: BrowserWindow | null = null;
// Here, you can also use other preload // Here, you can also use other preload
const preload = join(__dirname, "../preload/index.js"); const preload = join(__dirname, "../preload/index.js");
@ -75,9 +73,9 @@ async function createWindow() {
} }
// Test actively push message to the Electron-Renderer // Test actively push message to the Electron-Renderer
win.webContents.on("did-finish-load", () => { //win.webContents.on("did-finish-load", () => {
win?.webContents.send("main-process-message", new Date().toLocaleString()); // win?.webContents.send("main-process-message", new Date().toLocaleString());
}); //});
// Make all links open with the browser, not with the application // Make all links open with the browser, not with the application
win.webContents.setWindowOpenHandler(({ url }) => { win.webContents.setWindowOpenHandler(({ url }) => {
@ -87,12 +85,7 @@ async function createWindow() {
// win.webContents.on('will-navigate', (event, url) => { }) #344 // win.webContents.on('will-navigate', (event, url) => { }) #344
win.once("ready-to-show", () => { win.once("ready-to-show", () => {
//autoUpdater.logger = log.scope("updater"); //
//autoUpdater.channel = config.update_channel;
autoUpdater.autoInstallOnAppQuit = false;
autoUpdater.autoDownload = true;
autoUpdater.checkForUpdatesAndNotify();
//autoUpdater.quitAndInstall();
}); });
} }
@ -102,68 +95,61 @@ app.whenReady().then(() => {
console.log(platform()); console.log(platform());
//Generate daemon binary path //Generate daemon binary path
var daemonPath = ""; var serverPath = "";
console.log(process.env);
// Attempt to find Installation Folder
console.log(app.getAppPath());
console.log(join(app.getAppPath(), "..", ".."));
console.log(join(app.getAppPath(), "..", "..", ".."));
//var basePath = join(app.getAppPath(), '..', '..', '..') || join(process.env.PWD, '..') || join(process.env.INIT_CWD, '..') || join(process.env.DIST, '..', '..', '..');
var basePath = join(app.getAppPath(), "..", "..", "..");
switch (platform().toLowerCase()) { switch (platform().toLowerCase()) {
case "darwin": //case "darwin":
daemonPath = join(process.resourcesPath, "modem", "freedata-server"); //serverPath = join(basePath, "freedata-server", "freedata-server.exe");
case "linux": //serverProcess = spawn(serverPath, [], { detached: true });
daemonPath = join(process.resourcesPath, "modem", "freedata-server"); //serverProcess.unref(); // Allow the server process to continue running independently of the parent process
break; // break;
//case "linux":
//serverPath = join(basePath, "freedata-server", "freedata-server.exe");
//serverProcess = spawn(serverPath, [], { detached: true });
//serverProcess.unref(); // Allow the server process to continue running independently of the parent process
// break;
case "win32": case "win32":
daemonPath = join(process.resourcesPath, "modem", "freedata-server.exe"); serverPath = join(basePath, "freedata-server", "freedata-server.exe");
break; console.log(`Starting server with path: ${serverPath}`);
case "win64": serverProcess = spawn(
daemonPath = join(process.resourcesPath, "modem", "freedata-server.exe"); "cmd.exe",
["/c", "start", "cmd.exe", "/c", serverPath],
{ shell: true },
);
console.log(`Started server | PID: ${serverProcess.pid}`);
break; break;
default: default:
console.log("Unhandled OS Platform: ", platform()); console.log("Unhandled OS Platform: ", platform());
serverProcess = null;
serverPath = null;
break; break;
} }
//Start daemon binary if it exists serverProcess.on("error", (err) => {
if (existsSync(daemonPath)) { console.error("Failed to start server process:", err);
console.log("Starting freedata-server binary"); serverProcess = null;
console.log("daemonPath:", daemonPath); serverPath = null;
console.log("CWD:", join(daemonPath, "..")); });
/* serverProcess.stdout.on("data", (data) => {
var daemonProcess = spawn("freedata-server", [], { //console.log(`stdout: ${data}`);
cwd: join(process.env.DIST, "modem"), });
shell: true
});
*/
/*
daemonProcess = spawn(daemonPath, [], {
shell: true
});
console.log(daemonProcess)
*/
daemonProcess = spawn(daemonPath, [], {});
// return process messages serverProcess.stderr.on("data", (data) => {
daemonProcess.on("error", (err) => { console.error(`stderr: ${data}`);
//daemonProcessLog.error(`error when starting daemon: ${err}`); });
console.log(err); });
});
daemonProcess.on("message", () => {
// daemonProcessLog.info(`${data}`);
});
daemonProcess.stdout.on("data", () => {
// daemonProcessLog.info(`${data}`);
});
daemonProcess.stderr.on("data", (data) => {
// daemonProcessLog.info(`${data}`);
console.log(data);
});
daemonProcess.on("close", (code) => {
// daemonProcessLog.warn(`daemonProcess exited with code ${code}`);
});
} else {
daemonProcess = null;
daemonPath = null;
console.log("Daemon binary doesn't exist--normal for dev environments.");
}
//) app.on("before-quit", () => {
close_sub_processes();
}); });
app.on("window-all-closed", () => { app.on("window-all-closed", () => {
@ -205,104 +191,33 @@ ipcMain.handle("open-win", (_, arg) => {
} }
}); });
//restart and install udpate
ipcMain.on("request-restart-and-install-update", (event, data) => {
close_sub_processes();
autoUpdater.quitAndInstall();
});
// LISTENER FOR UPDATER EVENTS
autoUpdater.on("update-available", (info) => {
process.env.FDUpdateAvail = "1";
console.log("update available");
let arg = {
status: "update-available",
info: info,
};
win.webContents.send("action-updater", arg);
});
autoUpdater.on("update-not-available", (info) => {
console.log("update not available");
let arg = {
status: "update-not-available",
info: info,
};
win.webContents.send("action-updater", arg);
});
autoUpdater.on("update-downloaded", (info) => {
process.env.FDUpdateAvail = "1";
console.log("update downloaded");
let arg = {
status: "update-downloaded",
info: info,
};
win.webContents.send("action-updater", arg);
// we need to call this at this point.
// if an update is available and we are force closing the app
// the entire screen crashes...
//console.log('quit application and install update');
//autoUpdater.quitAndInstall();
});
autoUpdater.on("checking-for-update", () => {
console.log("checking for update");
let arg = {
status: "checking-for-update",
version: app.getVersion(),
};
win.webContents.send("action-updater", arg);
});
autoUpdater.on("download-progress", (progress) => {
let arg = {
status: "download-progress",
progress: progress,
};
win.webContents.send("action-updater", arg);
});
autoUpdater.on("error", (error) => {
console.log("update error");
let arg = {
status: "error",
progress: error,
};
win.webContents.send("action-updater", arg);
console.log("AUTO UPDATER : " + error);
});
function close_sub_processes() { function close_sub_processes() {
console.log("closing sub processes"); console.log("Closing sub processes...");
// closing the modem binary if not closed when closing application and also our daemon which has been started by the gui if (serverProcess != null) {
try { try {
if (daemonProcess != null) { console.log(`Killing server process with PID: ${serverProcess.pid}`);
daemonProcess.kill();
}
} catch (e) {
console.log(e);
}
console.log("closing modem and daemon"); switch (platform().toLowerCase()) {
try { //case "darwin":
if (platform() == "win32") { // process.kill(serverProcess.pid);
spawn("Taskkill", ["/IM", "freedata-modem.exe", "/F"]); // break;
spawn("Taskkill", ["/IM", "freedata-server.exe", "/F"]); //case "linux":
} // process.kill(serverProcess.pid);
// break;
case "win32":
// For Windows, use taskkill to ensure all child processes are also terminated
spawn("taskkill", ["/pid", serverProcess.pid.toString(), "/f", "/t"]);
break;
if (platform() == "linux") { default:
spawn("pkill", ["-9", "freedata-modem"]); console.log("Unhandled OS Platform: ", platform());
spawn("pkill", ["-9", "freedata-server"]); serverProcess = null;
serverPath = null;
break;
}
} catch (error) {
console.error(`Error killing server process: ${error}`);
} }
if (platform() == "darwin") {
spawn("pkill", ["-9", "freedata-modem"]);
spawn("pkill", ["-9", "freedata-server"]);
}
} catch (e) {
console.log(e);
} }
} }

View file

@ -111,83 +111,4 @@ window.onmessage = (ev) => {
ev.data.payload === "removeLoading" && removeLoading(); ev.data.payload === "removeLoading" && removeLoading();
}; };
setTimeout(removeLoading, 4999); setTimeout(removeLoading, 3999);
// IPC ACTION FOR AUTO UPDATER
ipcRenderer.on("action-updater", (event, arg) => {
if (arg.status == "download-progress") {
var progressinfo =
"(" +
Math.round(arg.progress.transferred / 1024) +
"kB /" +
Math.round(arg.progress.total / 1024) +
"kB)" +
" @ " +
Math.round(arg.progress.bytesPerSecond / 1024) +
"kByte/s";
document.getElementById("UpdateProgressInfo").innerHTML = progressinfo;
document
.getElementById("UpdateProgressBar")
.setAttribute("aria-valuenow", arg.progress.percent);
document
.getElementById("UpdateProgressBar")
.setAttribute("style", "width:" + arg.progress.percent + "%;");
}
if (arg.status == "checking-for-update") {
//document.title = document.title + ' - v' + arg.version;
//updateTitle(
// config.myCall,
// config.tnc_host,
// config.tnc_port,
// " -v " + arg.version,
//);
document.getElementById("updater_status").innerHTML =
'<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>';
document.getElementById("updater_status").className =
"btn btn-secondary btn-sm";
document.getElementById("update_and_install").style.display = "none";
}
if (arg.status == "update-downloaded") {
document.getElementById("update_and_install").removeAttribute("style");
document.getElementById("updater_status").innerHTML =
'<i class="bi bi-cloud-download ms-1 me-1" style="color: white;"></i>';
document.getElementById("updater_status").className =
"btn btn-success btn-sm";
// HERE WE NEED TO RUN THIS SOMEHOW...
//mainLog.info('quit application and install update');
//autoUpdater.quitAndInstall();
}
if (arg.status == "update-not-available") {
document.getElementById("updater_last_version").innerHTML =
arg.info.releaseName;
document.getElementById("updater_last_update").innerHTML =
arg.info.releaseDate;
document.getElementById("updater_release_notes").innerHTML =
arg.info.releaseNotes;
document.getElementById("updater_status").innerHTML =
'<i class="bi bi-check2-square ms-1 me-1" style="color: white;"></i>';
document.getElementById("updater_status").className =
"btn btn-success btn-sm";
document.getElementById("update_and_install").style.display = "none";
}
if (arg.status == "update-available") {
document.getElementById("updater_status").innerHTML =
'<i class="bi bi-hourglass-split ms-1 me-1" style="color: white;"></i>';
document.getElementById("updater_status").className =
"btn btn-warning btn-sm";
document.getElementById("update_and_install").style.display = "none";
}
if (arg.status == "error") {
document.getElementById("updater_status").innerHTML =
'<i class="bi bi-exclamation-square ms-1 me-1" style="color: white;"></i>';
document.getElementById("updater_status").className =
"btn btn-danger btn-sm";
document.getElementById("update_and_install").style.display = "none";
}
});

View file

@ -2,7 +2,7 @@
"name": "FreeDATA", "name": "FreeDATA",
"description": "FreeDATA Client application for connecting to FreeDATA server", "description": "FreeDATA Client application for connecting to FreeDATA server",
"private": true, "private": true,
"version": "0.13.2-alpha", "version": "0.14.5-alpha",
"main": "dist-electron/main/index.js", "main": "dist-electron/main/index.js",
"scripts": { "scripts": {
"start": "vite", "start": "vite",
@ -13,7 +13,8 @@
"release": "vue-tsc --noEmit && vite build && electron-builder -p onTag", "release": "vue-tsc --noEmit && vite build && electron-builder -p onTag",
"preview": "vite preview", "preview": "vite preview",
"lint": "eslint --ext .js,.vue src", "lint": "eslint --ext .js,.vue src",
"lint-fix": "eslint --ext .js,.vue --fix src" "lint-fix": "eslint --ext .js,.vue --fix src",
"install-deps": "npm install && npm update"
}, },
"repository": { "repository": {
"type": "git", "type": "git",
@ -39,12 +40,10 @@
"blob-util": "2.0.2", "blob-util": "2.0.2",
"bootstrap": "5.3.2", "bootstrap": "5.3.2",
"bootstrap-icons": "1.11.3", "bootstrap-icons": "1.11.3",
"bootswatch": "5.3.2",
"browser-image-compression": "2.0.2", "browser-image-compression": "2.0.2",
"chart.js": "4.4.1", "chart.js": "4.4.1",
"chartjs-plugin-annotation": "3.0.1", "chartjs-plugin-annotation": "3.0.1",
"electron-log": "5.1.1", "electron-log": "5.1.1",
"electron-updater": "6.1.7",
"emoji-picker-element": "1.21.0", "emoji-picker-element": "1.21.0",
"emoji-picker-element-data": "1.6.0", "emoji-picker-element-data": "1.6.0",
"file-saver": "2.0.5", "file-saver": "2.0.5",
@ -54,32 +53,32 @@
"noto-color-emoji": "^1.0.1", "noto-color-emoji": "^1.0.1",
"pinia": "2.1.7", "pinia": "2.1.7",
"qth-locator": "2.1.0", "qth-locator": "2.1.0",
"socket.io": "4.7.2", "socket.io": "4.7.4",
"uuid": "^9.0.1", "uuid": "^9.0.1",
"vue": "3.4.15", "vue": "3.4.21",
"vue-chartjs": "5.3.0", "vue-chartjs": "5.3.0",
"vuemoji-picker": "0.2.0" "vuemoji-picker": "0.2.0"
}, },
"devDependencies": { "devDependencies": {
"@types/nconf": "^0.10.6", "@types/nconf": "^0.10.6",
"@typescript-eslint/eslint-plugin": "6.19.1", "@typescript-eslint/eslint-plugin": "6.21.0",
"@vitejs/plugin-vue": "5.0.3", "@vitejs/plugin-vue": "5.0.4",
"electron": "28.1.3", "electron": "28.2.6",
"electron-builder": "24.9.1", "electron-builder": "24.9.1",
"eslint": "8.56.0", "eslint": "8.56.0",
"eslint-config-prettier": "9.1.0", "eslint-config-prettier": "9.1.0",
"eslint-config-standard-with-typescript": "43.0.0", "eslint-config-standard-with-typescript": "43.0.1",
"eslint-plugin-import": "2.29.1", "eslint-plugin-import": "2.29.1",
"eslint-plugin-n": "16.1.0", "eslint-plugin-n": "16.6.2",
"eslint-plugin-prettier": "5.0.1", "eslint-plugin-prettier": "5.1.3",
"eslint-plugin-promise": "6.1.1", "eslint-plugin-promise": "6.1.1",
"eslint-plugin-vue": "9.20.1", "eslint-plugin-vue": "9.22.0",
"typescript": "5.3.3", "typescript": "5.3.3",
"vite": "5.0.12", "vite": "5.1.7",
"vite-plugin-electron": "0.28.0", "vite-plugin-electron": "0.28.2",
"vite-plugin-electron-renderer": "0.14.5", "vite-plugin-electron-renderer": "0.14.5",
"vitest": "1.0.2", "vitest": "1.3.1",
"vue": "3.4.15", "vue": "3.4.21",
"vue-tsc": "1.8.27" "vue-tsc": "1.8.27"
} }
} }

Binary file not shown.

Before

Width:  |  Height:  |  Size: 125 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 342 KiB

View file

@ -1,3 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-person-fill" viewBox="0 0 16 16">
<path d="M3 14s-1 0-1-1 1-4 6-4 6 3 6 4-1 1-1 1H3Zm5-6a3 3 0 1 0 0-6 3 3 0 0 0 0 6Z"/>
</svg>

Before

Width:  |  Height:  |  Size: 225 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 136 KiB

View file

@ -25,7 +25,7 @@ import {
} from "chart.js"; } from "chart.js";
import { Bar } from "vue-chartjs"; import { Bar } from "vue-chartjs";
import { ref, computed } from "vue"; import { watch, nextTick, ref, computed } from "vue";
import annotationPlugin from "chartjs-plugin-annotation"; import annotationPlugin from "chartjs-plugin-annotation";
ChartJS.register( ChartJS.register(
@ -101,6 +101,20 @@ const beaconHistogramData = computed(() => ({
}, },
], ],
})); }));
const messagesContainer = ref(null);
watch(
() => chat.scrollTrigger,
(newVal, oldVal) => {
//console.log("Trigger changed from", oldVal, "to", newVal); // Debugging line
nextTick(() => {
if (messagesContainer.value) {
messagesContainer.value.scrollTop =
messagesContainer.value.scrollHeight;
}
});
},
);
</script> </script>
<template> <template>
@ -123,7 +137,7 @@ const beaconHistogramData = computed(() => ({
<div class="col-9 border-start vh-100 p-0"> <div class="col-9 border-start vh-100 p-0">
<div class="d-flex flex-column vh-100"> <div class="d-flex flex-column vh-100">
<!-- Top Navbar --> <!-- Top Navbar -->
<nav class="navbar sticky-top bg-body-tertiary shadow"> <nav class="navbar sticky-top z-0 bg-body-tertiary shadow">
<div class="input-group mb-0 p-0 w-25"> <div class="input-group mb-0 p-0 w-25">
<button type="button" class="btn btn-outline-secondary" disabled> <button type="button" class="btn btn-outline-secondary" disabled>
Beacons Beacons
@ -143,7 +157,7 @@ const beaconHistogramData = computed(() => ({
</nav> </nav>
<!-- Chat Messages Area --> <!-- Chat Messages Area -->
<div class="flex-grow-1 overflow-auto"> <div class="flex-grow-1 overflow-auto" ref="messagesContainer">
<chat_messages /> <chat_messages />
</div> </div>

View file

@ -8,17 +8,12 @@ import { getBeaconDataByCallsign } from "../js/api.js";
import { ref, computed } from "vue"; import { ref, computed } from "vue";
const chat = useChatStore(pinia); const chat = useChatStore(pinia);
function chatSelected(callsign) { function chatSelected(callsign) {
chat.selectedCallsign = callsign.toUpperCase(); chat.selectedCallsign = callsign.toUpperCase();
// scroll message container to bottom // scroll message container to bottom
var messageBody = document.getElementById("message-container"); chat.triggerScrollToBottom();
if (messageBody != null) {
// needs sensible defaults
messageBody.scrollTop = messageBody.scrollHeight - messageBody.clientHeight;
}
processBeaconData(callsign); processBeaconData(callsign);
} }
@ -46,24 +41,17 @@ function newChat() {
if (callsign === "") return; if (callsign === "") return;
this.newChatCall.value = ""; this.newChatCall.value = "";
} }
</script> </script>
<template> <template>
<nav class="navbar sticky-top bg-body-tertiary shadow">
<nav class="navbar sticky-top bg-body-tertiary shadow"> <button
class="btn btn-outline-primary w-100"
<button data-bs-target="#newChatModal"
class="btn btn-outline-primary w-100" data-bs-toggle="modal"
data-bs-target="#newChatModal" >
data-bs-toggle="modal" <i class="bi bi-pencil-square"> Start a new chat</i>
> </button>
<i class="bi bi-pencil-square"> Start a new chat</i> </nav>
</button>
</nav>
<div <div
class="list-group bg-body-tertiary m-0 p-1" class="list-group bg-body-tertiary m-0 p-1"

View file

@ -21,13 +21,9 @@ function getDateTime(timestampRaw) {
let day = date.getDate().toString().padStart(2, "0"); let day = date.getDate().toString().padStart(2, "0");
return `${year}-${month}-${day}`; return `${year}-${month}-${day}`;
} }
</script> </script>
<template> <template>
<div class="tab-content p-3" id="nav-tabContent-chat-messages"> <div class="tab-content p-3" id="nav-tabContent-chat-messages">
<template <template
v-for="(details, callsign, key) in chat.callsign_list" v-for="(details, callsign, key) in chat.callsign_list"
@ -66,10 +62,6 @@ function getDateTime(timestampRaw) {
</div> </div>
</template> </template>
</div> </div>
</template> </template>
<style> <style>

View file

@ -30,7 +30,7 @@ import {
Legend Legend
} from 'chart.js' } from 'chart.js'
import { Line } from 'vue-chartjs' import { Line } from 'vue-chartjs'
import { ref, computed } from 'vue'; import { ref, computed, nextTick } from 'vue';
import { VuemojiPicker, EmojiClickEventDetail } from 'vuemoji-picker' import { VuemojiPicker, EmojiClickEventDetail } from 'vuemoji-picker'
@ -90,6 +90,8 @@ function transmitNewMessage() {
chat.selectedCallsign = Object.keys(chat.callsign_list)[0]; chat.selectedCallsign = Object.keys(chat.callsign_list)[0];
} }
chat.inputText = chat.inputText.trim(); chat.inputText = chat.inputText.trim();
// Proceed only if there is text or files selected // Proceed only if there is text or files selected
@ -101,6 +103,7 @@ function transmitNewMessage() {
type: file.type, type: file.type,
data: file.content data: file.content
}; };
}); });
if (chat.selectedCallsign.startsWith("BC-")) { if (chat.selectedCallsign.startsWith("BC-")) {
@ -120,6 +123,7 @@ function transmitNewMessage() {
chat.inputText = ''; chat.inputText = '';
chatModuleMessage.value = ""; chatModuleMessage.value = "";
resetFile() resetFile()
} }
function resetFile(event){ function resetFile(event){

View file

@ -8,7 +8,7 @@ import "../../node_modules/gridstack/dist/gridstack.min.css";
import { GridStack } from "gridstack"; import { GridStack } from "gridstack";
import { useStateStore } from "../store/stateStore.js"; import { useStateStore } from "../store/stateStore.js";
const state = useStateStore(pinia); const state = useStateStore(pinia);
import { setRadioParameters } from "../js/api"; import { setRadioParametersFrequency, setRadioParametersMode, setRadioParametersRFLevel } from "../js/api";
import { saveLocalSettingsToConfig, settingsStore } from "../store/settingsStore"; import { saveLocalSettingsToConfig, settingsStore } from "../store/settingsStore";
import active_heard_stations from "./grid/grid_active_heard_stations.vue"; import active_heard_stations from "./grid/grid_active_heard_stations.vue";
@ -251,14 +251,22 @@ new gridWidget(
//New new widget ID should be 20 //New new widget ID should be 20
]; ];
function updateFrequencyAndApply(frequency) { function updateFrequencyAndApply(frequency) {
state.new_frequency = frequency; state.new_frequency = frequency;
set_radio_parameters(); set_radio_parameter_frequency();
} }
function set_radio_parameters(){ function set_radio_parameter_frequency(){
setRadioParameters(state.new_frequency, state.mode, state.rf_level); setRadioParametersFrequency(state.new_frequency)
}
function set_radio_parameter_mode(){
setRadioParametersMode(state.mode)
}
function set_radio_parameter_rflevel(){
setRadioParametersRFLevel(state.rf_level)
} }
@ -358,19 +366,21 @@ onMounted(() => {
setGridEditState(); setGridEditState();
}); });
function onChange(event, changeItems) { function onChange(event, changeItems) {
// update item position if (typeof changeItems !== "undefined"){
changeItems.forEach((item) => { // update item position
var widget = items.value.find((w) => w.id == item.id); changeItems.forEach((item) => {
if (!widget) { var widget = items.value.find((w) => w.id == item.id);
console.error("Widget not found: " + item.id); if (!widget) {
return; console.error("Widget not found: " + item.id);
return;
}
widget.x = item.x;
widget.y = item.y;
widget.w = item.w;
widget.h = item.h;
});
saveGridLayout();
} }
widget.x = item.x;
widget.y = item.y;
widget.w = item.w;
widget.h = item.h;
});
saveGridLayout();
} }
function restoreGridLayoutFromConfig(){ function restoreGridLayoutFromConfig(){
//Try to load grid from saved config //Try to load grid from saved config
@ -475,8 +485,8 @@ function quickfill() {
<i class="bi bi-grip-vertical h5"></i> <i class="bi bi-grip-vertical h5"></i>
</button> </button>
<div class="grid-container" style="height: calc(100vh - 51px);"> <div class="grid-container z-0" style="height: calc(100vh - 51px);">
<div class="grid-stack"> <div class="grid-stack z-0">
<div <div
v-for="(w, indexs) in items" v-for="(w, indexs) in items"
class="grid-stack-item" class="grid-stack-item"
@ -822,7 +832,7 @@ function quickfill() {
<h6>15m</h6> <h6>15m</h6>
</div> </div>
</a> </a>
<a href="#" class="list-group-item list-group-item-action" @click="updateFrequencyAndApply(14093000)"> <a href="#" class="list-group-item list-group-item-action" @click="updateFrequencyAndApply(18106000)">
<div class="d-flex w-100 justify-content-between"> <div class="d-flex w-100 justify-content-between">
<h5 class="mb-1">18.106 MHz</h5> <h5 class="mb-1">18.106 MHz</h5>
<small>EU / US</small> <small>EU / US</small>

View file

@ -21,6 +21,14 @@ function startStopBeacon() {
} }
} }
var dxcallPing = ref(""); var dxcallPing = ref("");
window.addEventListener(
"stationSelected",
function (eventdata) {
let evt = <CustomEvent>eventdata;
dxcallPing.value = evt.detail;
},
false,
);
</script> </script>
<template> <template>
<div class="card h-100"> <div class="card h-100">

View file

@ -21,6 +21,14 @@ function startStopBeacon() {
} }
} }
var dxcallPing = ref(""); var dxcallPing = ref("");
window.addEventListener(
"stationSelected",
function (eventdata) {
let evt = <CustomEvent>eventdata;
dxcallPing.value = evt.detail;
},
false,
);
</script> </script>
<template> <template>
<div class="card h-100"> <div class="card h-100">

View file

@ -34,6 +34,10 @@ function getMaidenheadDistance(dxGrid) {
// //
} }
} }
function pushToPing(origin)
{
window.dispatchEvent(new CustomEvent("stationSelected", {bubbles:true, detail: origin }));
}
</script> </script>
<template> <template>
<div class="card h-100"> <div class="card h-100">
@ -61,7 +65,7 @@ function getMaidenheadDistance(dxGrid) {
</thead> </thead>
<tbody id="gridHeardStations"> <tbody id="gridHeardStations">
<!--https://vuejs.org/guide/essentials/list.html--> <!--https://vuejs.org/guide/essentials/list.html-->
<tr v-for="item in state.heard_stations" :key="item.origin"> <tr v-for="item in state.heard_stations" :key="item.origin" @click="pushToPing(item.origin)">
<td> <td>
{{ getDateTime(item.timestamp) }} {{ getDateTime(item.timestamp) }}
</td> </td>

View file

@ -34,6 +34,10 @@ function getMaidenheadDistance(dxGrid) {
// //
} }
} }
function pushToPing(origin)
{
window.dispatchEvent(new CustomEvent("stationSelected", {bubbles:true, detail: origin }));
}
</script> </script>
<template> <template>
<div class="card h-100"> <div class="card h-100">
@ -54,7 +58,7 @@ function getMaidenheadDistance(dxGrid) {
</thead> </thead>
<tbody id="miniHeardStations"> <tbody id="miniHeardStations">
<!--https://vuejs.org/guide/essentials/list.html--> <!--https://vuejs.org/guide/essentials/list.html-->
<tr v-for="item in state.heard_stations" :key="item.origin"> <tr v-for="item in state.heard_stations" :key="item.origin" @click="pushToPing(item.origin)">
<td> <td>
<span class="fs-6">{{ getDateTime(item.timestamp) }}</span> <span class="fs-6">{{ getDateTime(item.timestamp) }}</span>
</td> </td>

View file

@ -1,15 +1,26 @@
<script setup lang="ts"> <script setup lang="ts">
import { setActivePinia } from "pinia"; import { setActivePinia } from "pinia";
import pinia from "../../store/index"; import pinia from "../../store/index";
import { setRadioParameters } from "../../js/api"; import { setRadioParametersFrequency, setRadioParametersMode, setRadioParametersRFLevel } from "../../js/api";
setActivePinia(pinia); setActivePinia(pinia);
import { useStateStore } from "../../store/stateStore.js"; import { useStateStore } from "../../store/stateStore.js";
const state = useStateStore(pinia); const state = useStateStore(pinia);
function set_radio_parameters() { function set_radio_parameter_frequency(){
setRadioParameters(state.frequency, state.mode, state.rf_level); setRadioParametersFrequency(state.new_frequency)
} }
function set_radio_parameter_mode(){
setRadioParametersMode(state.mode)
}
function set_radio_parameter_rflevel(){
setRadioParametersRFLevel(state.rf_level)
}
</script> </script>
<template> <template>
@ -47,18 +58,15 @@ function set_radio_parameters() {
<select <select
class="form-control" class="form-control"
v-model="state.mode" v-model="state.mode"
@click="set_radio_parameters()" @click="set_radio_parameter_mode()"
v-bind:class="{ v-bind:class="{
disabled: state.hamlib_status === 'disconnected', disabled: state.hamlib_status === 'disconnected',
}" }"
> >
<option selected value="">---</option>
<option value="USB">USB</option> <option value="USB">USB</option>
<option value="LSB">LSB</option> <option value="USB-D">USB-D</option>
<option value="PKTUSB">PKT-U</option> <option value="PKTUSB">PKT-U</option>
<option value="PKTLSB">PKT-L</option>
<option value="AM">AM</option>
<option value="FM">FM</option>
<option value="PKTFM">PKTFM</option>
</select> </select>
</div> </div>
</div> </div>
@ -69,7 +77,7 @@ function set_radio_parameters() {
<select <select
class="form-control" class="form-control"
v-model="state.rf_level" v-model="state.rf_level"
@click="set_radio_parameters()" @click="set_radio_parameter_rflevel()"
v-bind:class="{ v-bind:class="{
disabled: state.hamlib_status === 'disconnected', disabled: state.hamlib_status === 'disconnected',
}" }"

View file

@ -12,6 +12,15 @@ function transmitPing() {
sendModemPing(dxcallPing.value.toUpperCase()); sendModemPing(dxcallPing.value.toUpperCase());
} }
var dxcallPing = ref(""); var dxcallPing = ref("");
window.addEventListener(
"stationSelected",
function (eventdata) {
let evt = <CustomEvent>eventdata;
dxcallPing.value = evt.detail;
},
false,
);
</script> </script>
<template> <template>
<div class="input-group" style="width: calc(100% - 24px)"> <div class="input-group" style="width: calc(100% - 24px)">

View file

@ -1,223 +0,0 @@
<script setup lang="ts">
import { ref, onMounted } from "vue";
import { setActivePinia } from "pinia";
import pinia from "../store/index";
setActivePinia(pinia);
import infoScreen_updater from "./infoScreen_updater.vue";
function openWebExternal(url) {
open(url);
}
const cards = ref([
{
titleName: "Simon",
titleCall: "DJ2LS",
role: "Founder & Core Developer",
imgSrc: "dj2ls.png",
},
{
titleName: "Alan",
titleCall: "N1QM",
role: "Developer",
imgSrc: "person-fill.svg",
},
{
titleName: "Stefan",
titleCall: "DK5SM",
role: "Tester",
imgSrc: "person-fill.svg",
},
{
titleName: "Wolfgang",
titleCall: "DL4IAZ",
role: "Supporter",
imgSrc: "person-fill.svg",
},
{
titleName: "David",
titleCall: "VK5DGR",
role: "Codec 2 Founder",
imgSrc: "vk5dgr.jpeg",
},
{
titleName: "John",
titleCall: "EI7IG",
role: "Tester",
imgSrc: "ei7ig.jpeg",
},
{
titleName: "Paul",
titleCall: "N2KIQ",
role: "Developer",
imgSrc: "person-fill.svg",
},
{
titleName: "Trip",
titleCall: "KT4WO",
role: "Tester",
imgSrc: "kt4wo.png",
},
{
titleName: "Manuel",
titleCall: "DF7MH",
role: "Tester",
imgSrc: "person-fill.svg",
},
{
titleName: "Darren",
titleCall: "G0HWW",
role: "Tester",
imgSrc: "person-fill.svg",
},
{
titleName: "Kai",
titleCall: "LA3QMA",
role: "Developer",
imgSrc: "person-fill.svg",
},
{
titleName: "Pedro",
titleCall: "F4JAW",
role: "Core Developer",
imgSrc: "person-fill.svg",
},
]);
// Shuffle cards
function shuffleCards() {
cards.value = cards.value.sort(() => Math.random() - 0.5);
}
onMounted(shuffleCards);
</script>
<template>
<!--<infoScreen_updater />-->
<div class="container-fluid">
<div class="row">
<h6>Important URLs</h6>
<div
class="btn-toolbar mx-auto"
role="toolbar"
aria-label="Toolbar with button groups"
>
<div class="btn-group">
<button
class="btn btn-sm bi bi-geo-alt btn-secondary me-2"
id="openExplorer"
type="button"
data-bs-placement="bottom"
@click="openWebExternal('https://explorer.freedata.app')"
>
Explorer map
</button>
</div>
<div class="btn-group">
<button
class="btn btn-sm btn-secondary me-2 bi bi-graph-up"
id="btnStats"
type="button"
data-bs-placement="bottom"
@click="openWebExternal('https://statistics.freedata.app/')"
>
Statistics
</button>
</div>
<div class="btn-group">
<button
class="btn btn-secondary bi bi-bookmarks me-2"
id="fdWww"
data-bs-toggle="tooltip"
data-bs-trigger="hover"
title="FreeDATA website"
role="button"
@click="openWebExternal('https://freedata.app')"
>
Website
</button>
</div>
<div class="btn-group">
<button
class="btn btn-secondary bi bi-github me-2"
id="ghUrl"
data-bs-toggle="tooltip"
data-bs-trigger="hover"
title="Github"
role="button"
@click="openWebExternal('https://github.com/dj2ls/freedata')"
>
Github
</button>
</div>
<div class="btn-group">
<button
class="btn btn-secondary bi bi-wikipedia me-2"
id="wikiUrl"
data-bs-toggle="tooltip"
data-bs-trigger="hover"
title="Wiki"
role="button"
@click="openWebExternal('https://wiki.freedata.app')"
>
Wiki
</button>
</div>
<div class="btn-group">
<button
class="btn btn-secondary bi bi-discord"
id="discordUrl"
data-bs-toggle="tooltip"
data-bs-trigger="hover"
title="Discord"
role="button"
@click="openWebExternal('https://discord.freedata.app')"
>
Discord
</button>
</div>
</div>
</div>
<hr />
<div class="row">
<h6>We would like to especially thank the following</h6>
</div>
<div
class="d-flex flex-nowrap overflow-y-auto w-100"
style="height: calc(100vh - 170px); overflow-x: hidden"
>
<div class="row row-cols-1 row-cols-sm-2 row-cols-md-4 row-cols-lg-6">
<div class="d-inline-block" v-for="card in cards" :key="card.titleName">
<div class="col">
<div class="card border-dark m-1" style="max-width: 10rem">
<img :src="card.imgSrc" class="card-img-top grayscale" />
<div class="card-body">
<p class="card-text text-center">{{ card.role }}</p>
</div>
<div class="card-footer text-body-secondary text-center">
<strong>{{ card.titleCall }}</strong>
</div>
<div class="card-footer text-body-secondary text-center">
<strong>{{ card.titleName }}</strong>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<style>
.grayscale {
filter: grayscale(100%);
transition: filter 0.3s ease-in-out;
}
.grayscale:hover {
filter: grayscale(0);
}
</style>

View file

@ -1,102 +0,0 @@
<script setup lang="ts">
import { setActivePinia } from "pinia";
import pinia from "../store/index";
setActivePinia(pinia);
import { useStateStore } from "../store/stateStore.js";
import { settingsStore } from "../store/settingsStore";
import { onMounted } from "vue";
import { ipcRenderer } from "electron";
const state = useStateStore(pinia);
onMounted(() => {
window.addEventListener("DOMContentLoaded", () => {
// we are using this area for implementing the electron runUpdater
// we need access to DOM for displaying updater results in GUI
// close app, update and restart
document
.getElementById("update_and_install")
.addEventListener("click", () => {
ipcRenderer.send("request-restart-and-install-update");
});
});
});
</script>
<template>
<div class="card m-2">
<div class="card-header p-1 d-flex">
<div class="container">
<div class="row">
<div class="col-1">
<i class="bi bi-cloud-download" style="font-size: 1.2rem"></i>
</div>
<div class="col-3">
<strong class="fs-5">Updater</strong>
</div>
<div class="col-7">
<div class="progress w-100 ms-1 m-1">
<div
class="progress-bar"
style="width: 0%"
role="progressbar"
id="UpdateProgressBar"
aria-valuenow="0"
aria-valuemin="0"
aria-valuemax="100"
>
<span id="UpdateProgressInfo"></span>
</div>
</div>
</div>
<div class="col-1 text-end">
<button
type="button"
id="openHelpModalUpdater"
data-bs-toggle="modal"
data-bs-target="#updaterHelpModal"
class="btn m-0 p-0 border-0"
>
<i class="bi bi-question-circle" style="font-size: 1rem"></i>
</button>
</div>
</div>
</div>
</div>
<div class="card-body p-2 mb-1">
<button
class="btn btn-secondary btn-sm ms-1 me-1"
id="updater_channel"
type="button"
disabled
>
Update channel:&nbsp; {{ settingsStore.local.update_channel }}
</button>
<button
class="btn btn-secondary btn-sm ms-1"
id="updater_status"
type="button"
disabled
>
...
</button>
<button
class="btn btn-secondary btn-sm ms-1"
id="updater_changelog"
type="button"
data-bs-toggle="modal"
data-bs-target="#updaterReleaseNotes"
>
Changelog
</button>
<button
class="btn btn-primary btn-sm ms-1"
id="update_and_install"
type="button"
style="display: none"
>
Install & Restart
</button>
</div>
</div>
</template>

View file

@ -5,32 +5,21 @@ setActivePinia(pinia);
import main_modals from "./main_modals.vue"; import main_modals from "./main_modals.vue";
import main_top_navbar from "./main_top_navbar.vue"; import main_top_navbar from "./main_top_navbar.vue";
import main_rig_control from "./main_rig_control.vue";
import settings_view from "./settings.vue"; import settings_view from "./settings.vue";
import main_active_rig_control from "./main_active_rig_control.vue";
import main_footer_navbar from "./main_footer_navbar.vue"; import main_footer_navbar from "./main_footer_navbar.vue";
import main_active_stats from "./main_active_stats.vue";
import main_active_broadcasts from "./main_active_broadcasts.vue";
import main_active_heard_stations from "./main_active_heard_stations.vue";
import main_active_audio_level from "./main_active_audio_level.vue";
import chat from "./chat.vue"; import chat from "./chat.vue";
import infoScreen from "./infoScreen.vue";
import main_modem_healthcheck from "./main_modem_healthcheck.vue"; import main_modem_healthcheck from "./main_modem_healthcheck.vue";
import Dynamic_components from "./dynamic_components.vue"; import Dynamic_components from "./dynamic_components.vue";
import { getFreedataMessages } from "../js/api"; import { getFreedataMessages } from "../js/api";
import { getRemote } from "../store/settingsStore.js";
import { loadAllData } from "../js/eventHandler";
</script> </script>
<template> <template>
<!-------------------------------- INFO TOASTS ----------------> <!-------------------------------- INFO TOASTS ---------------->
<div <div aria-live="polite" aria-atomic="true" class="position-relative z-3">
aria-live="polite"
aria-atomic="true"
class="position-relative"
style="z-index: 500"
>
<div <div
class="toast-container position-absolute top-0 end-0 p-3" class="toast-container position-absolute top-0 end-0 p-3"
id="mainToastContainer" id="mainToastContainer"
@ -48,6 +37,7 @@ import { getFreedataMessages } from "../js/api";
id="main-list-tab" id="main-list-tab"
role="tablist" role="tablist"
style="margin-top: 100px" style="margin-top: 100px"
@click="loadAllData"
> >
<main_modem_healthcheck /> <main_modem_healthcheck />
@ -84,17 +74,6 @@ import { getFreedataMessages } from "../js/api";
><i class="bi bi-rocket h3"></i ><i class="bi bi-rocket h3"></i
></a> ></a>
<a
class="list-group-item list-group-item-dark list-group-item-action border border-0 rounded-3 mb-2"
id="list-info-list"
data-bs-toggle="list"
href="#list-info"
role="tab"
aria-controls="list-info"
title="About"
><i class="bi bi-info h3"></i
></a>
<a <a
class="list-group-item list-group-item-dark list-group-item-action d-none border-0 rounded-3 mb-2" class="list-group-item list-group-item-dark list-group-item-action d-none border-0 rounded-3 mb-2"
id="list-logger-list" id="list-logger-list"
@ -112,6 +91,7 @@ import { getFreedataMessages } from "../js/api";
role="tab" role="tab"
aria-controls="list-settings" aria-controls="list-settings"
title="Settings" title="Settings"
@click="loadAllData"
><i class="bi bi-gear-wide-connected h3"></i ><i class="bi bi-gear-wide-connected h3"></i
></a> ></a>
</div> </div>
@ -144,27 +124,6 @@ import { getFreedataMessages } from "../js/api";
<!-------------------------------- MAIN AREA ----------------> <!-------------------------------- MAIN AREA ---------------->
<!------------------------------------------------------------------------------------------> <!------------------------------------------------------------------------------------------>
<div class="container">
<div class="row">
<div class="col-5">
<main_active_rig_control />
</div>
<div class="col-4">
<main_active_broadcasts />
</div>
<div class="col-3">
<main_active_audio_level />
</div>
</div>
<div class="row">
<div class="col-7">
<main_active_heard_stations />
</div>
<div class="col-5">
<main_active_stats />
</div>
</div>
</div>
</div> </div>
</div> </div>
@ -303,14 +262,7 @@ import { getFreedataMessages } from "../js/api";
</div> </div>
</div> </div>
</div> </div>
<div
class="tab-pane fade"
id="list-info"
role="tabpanel"
aria-labelledby="list-info-list"
>
<infoScreen />
</div>
<div <div
class="tab-pane fade show active" class="tab-pane fade show active"
id="list-grid" id="list-grid"

View file

@ -1,170 +0,0 @@
<script setup lang="ts">
import { setActivePinia } from "pinia";
import pinia from "../store/index";
setActivePinia(pinia);
import { useStateStore } from "../store/stateStore.js";
const state = useStateStore(pinia);
</script>
<template>
<div class="card mb-1">
<div class="card-header p-1">
<div class="container">
<div class="row">
<div class="col-1">
<i class="bi bi-volume-up" style="font-size: 1.2rem"></i>
</div>
<div class="col-3">
<strong>Audio</strong>
</div>
<div class="col-7">
<button
type="button"
id="audioModalButton"
data-bs-toggle="modal"
data-bs-target="#audioModal"
class="btn btn-sm btn-outline-secondary me-1"
>
Tune
</button>
</div>
<div class="col-1 text-end">
<button
type="button"
id="openHelpModalAudioLevel"
data-bs-toggle="modal"
data-bs-target="#audioLevelHelpModal"
class="btn m-0 p-0 border-0"
>
<i class="bi bi-question-circle" style="font-size: 1rem"></i>
</button>
</div>
</div>
</div>
</div>
<div class="card-body p-2">
<div class="container">
<div class="row">
<div class="col-sm">
<div
class="progress mb-0 rounded-0 rounded-top"
style="height: 22px"
>
<div
class="progress-bar progress-bar-striped bg-primary force-gpu"
id="noise_level"
role="progressbar"
:style="{ width: state.s_meter_strength_percent + '%' }"
aria-valuenow="{{state.s_meter_strength_percent}}"
aria-valuemin="0"
aria-valuemax="100"
></div>
<p
class="justify-content-center d-flex position-absolute w-100"
id="noise_level_value"
>
S-Meter(dB): {{ state.s_meter_strength_raw }}
</p>
</div>
<div
class="progress mb-0 rounded-0 rounded-bottom"
style="height: 8px"
>
<div
class="progress-bar progress-bar-striped bg-warning"
role="progressbar"
style="width: 1%"
aria-valuenow="1"
aria-valuemin="0"
aria-valuemax="100"
></div>
<div
class="progress-bar bg-success"
role="progressbar"
style="width: 89%"
aria-valuenow="50"
aria-valuemin="0"
aria-valuemax="100"
></div>
<div
class="progress-bar progress-bar-striped bg-warning"
role="progressbar"
style="width: 20%"
aria-valuenow="20"
aria-valuemin="0"
aria-valuemax="100"
></div>
<div
class="progress-bar progress-bar-striped bg-danger"
role="progressbar"
style="width: 29%"
aria-valuenow="29"
aria-valuemin="0"
aria-valuemax="100"
></div>
</div>
</div>
<div class="col-sm">
<div
class="progress mb-0 rounded-0 rounded-top"
style="height: 22px"
>
<div
class="progress-bar progress-bar-striped bg-primary force-gpu"
id="dbfs_level"
role="progressbar"
:style="{ width: state.dbfs_level_percent + '%' }"
aria-valuenow="0"
aria-valuemin="0"
aria-valuemax="100"
></div>
<p
class="justify-content-center d-flex position-absolute w-100"
id="dbfs_level_value"
>
{{ state.dbfs_level }} dBFS
</p>
</div>
<div
class="progress mb-0 rounded-0 rounded-bottom"
style="height: 8px"
>
<div
class="progress-bar progress-bar-striped bg-warning"
role="progressbar"
style="width: 1%"
aria-valuenow="1"
aria-valuemin="0"
aria-valuemax="100"
></div>
<div
class="progress-bar bg-success"
role="progressbar"
style="width: 89%"
aria-valuenow="50"
aria-valuemin="0"
aria-valuemax="100"
></div>
<div
class="progress-bar progress-bar-striped bg-warning"
role="progressbar"
style="width: 20%"
aria-valuenow="20"
aria-valuemin="0"
aria-valuemax="100"
></div>
<div
class="progress-bar progress-bar-striped bg-danger"
role="progressbar"
style="width: 29%"
aria-valuenow="29"
aria-valuemin="0"
aria-valuemax="100"
></div>
</div>
</div>
</div>
</div>
</div>
</div>
</template>

View file

@ -1,110 +0,0 @@
<script setup lang="ts">
import { setActivePinia } from "pinia";
import pinia from "../store/index";
setActivePinia(pinia);
import { settingsStore as settings} from "../store/settingsStore.js";
import { useStateStore } from "../store/stateStore.js";
const state = useStateStore(pinia);
import { sendModemCQ, sendModemPing, setModemBeacon } from "../js/api.js";
function transmitPing() {
sendModemPing((<HTMLInputElement>document.getElementById("dxCall")).value);
}
function startStopBeacon() {
if (state.beacon_state === true) {
setModemBeacon(false);
}
else {
setModemBeacon(true);
}
}
</script>
<template>
<div class="card mb-1">
<div class="card-header p-1">
<div class="container">
<div class="row">
<div class="col-1">
<i class="bi bi-broadcast" style="font-size: 1.2rem"></i>
</div>
<div class="col-10">
<strong class="fs-5">Broadcasts</strong>
</div>
<div class="col-1 text-end">
<button
type="button"
id="openHelpModalBroadcasts"
data-bs-toggle="modal"
data-bs-target="#broadcastsHelpModal"
class="btn m-0 p-0 border-0"
>
<i class="bi bi-question-circle" style="font-size: 1rem"></i>
</button>
</div>
</div>
</div>
</div>
<div class="card-body p-2">
<div class="row">
<div class="col-md-auto">
<div class="input-group input-group-sm mb-0">
<input
type="text"
class="form-control"
style="max-width: 6rem; text-transform: uppercase"
placeholder="DXcall"
pattern="[A-Z]*"
id="dxCall"
maxlength="11"
aria-label="Input group"
aria-describedby="btnGroupAddon"
/>
<button
class="btn btn-sm btn-outline-secondary ms-1"
id="sendPing"
type="button"
data-bs-placement="bottom"
data-bs-toggle="tooltip"
data-bs-trigger="hover"
data-bs-html="false"
title="Send a ping request to a remote station"
@click="transmitPing()"
>
Ping
</button>
<button
class="btn btn-sm btn-outline-secondary ms-1"
id="sendCQ"
type="button"
title="Send a CQ to the world"
@click="sendModemCQ()"
>
Call CQ
</button>
<button
type="button"
id="startBeacon"
class="btn btn-sm ms-1"
@click="startStopBeacon()"
v-bind:class="{
'btn-success': state.beacon_state === true,
'btn-outline-secondary': state.beacon_state === false,
}"
title="Toggle beacon mode. The interval can be set in settings. While sending a beacon, you can receive ping requests and open a datachannel. If a datachannel is opened, the beacon pauses."
>
<i class="bi bi-soundwave"></i> Toggle beacon
</button>
</div>
</div>
</div>
<!-- end of row-->
</div>
</div>
</template>

View file

@ -1,122 +0,0 @@
<script setup lang="ts">
// @ts-nocheck
const { distance } = require("qth-locator");
import { setActivePinia } from "pinia";
import pinia from "../store/index";
setActivePinia(pinia);
import { settingsStore as settings } from "../store/settingsStore.js";
import { useStateStore } from "../store/stateStore.js";
const state = useStateStore(pinia);
function getDateTime(timestampRaw) {
var datetime = new Date(timestampRaw * 1000).toLocaleString(
navigator.language,
{
hourCycle: "h23",
year: "numeric",
month: "2-digit",
day: "2-digit",
hour: "2-digit",
minute: "2-digit",
second: "2-digit",
},
);
return datetime;
}
function getMaidenheadDistance(dxGrid) {
if (typeof dxGrid != "undefined") {
try {
return parseInt(distance(settings.remote.STATION.mygrid, dxGrid));
} catch (e) {
console.warn(e);
}
}
}
</script>
<template>
<div class="card mb-1 h-100">
<!--325px-->
<div class="card-header p-1">
<div class="container">
<div class="row">
<div class="col-auto">
<i class="bi bi-list-columns-reverse" style="font-size: 1.2rem"></i>
</div>
<div class="col-10">
<strong class="fs-5">Heard stations</strong>
</div>
<div class="col-1 text-end">
<button
type="button"
id="openHelpModalHeardStations"
data-bs-toggle="modal"
data-bs-target="#heardStationsHelpModal"
class="btn m-0 p-0 border-0"
>
<i class="bi bi-question-circle" style="font-size: 1rem"></i>
</button>
</div>
</div>
</div>
</div>
<div class="card-body p-0" style="overflow-y: overlay">
<div class="table-responsive">
<!-- START OF TABLE FOR HEARD STATIONS -->
<table class="table table-sm" id="tblHeardStationList">
<thead>
<tr>
<th scope="col" id="thTime">
<i id="hslSort" class="bi bi-sort-up"></i>Time
</th>
<th scope="col" id="thFreq">Freq</th>
<th scope="col" id="thDxcall">DXCall</th>
<th scope="col" id="thDxgrid">Grid</th>
<th scope="col" id="thDist">Dist</th>
<th scope="col" id="thType">Type</th>
<th scope="col" id="thSnr">SNR</th>
<!--<th scope="col">Off</th>-->
</tr>
</thead>
<tbody id="heardstations">
<!--https://vuejs.org/guide/essentials/list.html-->
<tr v-for="item in state.heard_stations" :key="item.origin">
<td>
<span class="badge bg-secondary">{{
getDateTime(item.timestamp)
}}</span>
</td>
<td>
<span class="badge bg-secondary"
>{{ item.frequency / 1000 }} kHz</span
>
</td>
<td>
<span class="badge bg-secondary">{{ item.origin }}</span>
</td>
<td>
<span class="badge bg-secondary">{{ item.gridsquare }}</span>
</td>
<td>
<span class="badge bg-secondary"
>{{ getMaidenheadDistance(item.gridsquare) }} km</span
>
</td>
<td>
<span class="badge bg-secondary">{{ item.activity_type }}</span>
</td>
<td>
<span class="badge bg-secondary">{{ item.snr }}</span>
</td>
<!--<td>{{ item.offset }}</td>-->
</tr>
</tbody>
</table>
</div>
<!-- END OF HEARD STATIONS TABLE -->
</div>
</div>
</template>

View file

@ -1,256 +0,0 @@
<script setup lang="ts">
import { setActivePinia } from "pinia";
import pinia from "../store/index";
setActivePinia(pinia);
import { useStateStore } from "../store/stateStore.js";
const state = useStateStore(pinia);
import { setRadioParameters } from "../js/api";
function updateFrequencyAndApply(frequency) {
state.new_frequency = frequency;
set_radio_parameters();
}
function set_radio_parameters() {
setRadioParameters(state.new_frequency, state.mode, state.rf_level);
}
</script>
<template>
<div class="mb-3">
<div class="card mb-1">
<div class="card-header p-1">
<div class="container">
<div class="row">
<div class="col-1">
<i class="bi bi-house-door" style="font-size: 1.2rem"></i>
</div>
<div class="col-10">
<strong class="fs-5 me-2">Radio control</strong>
<span
class="badge"
v-bind:class="{
'text-bg-success': state.hamlib_status === 'connected',
'text-bg-danger disabled':
state.hamlib_status === 'disconnected',
}"
>{{ state.hamlib_status }}</span
>
</div>
<div class="col-1 text-end">
<button
type="button"
id="openHelpModalStation"
data-bs-toggle="modal"
data-bs-target="#stationHelpModal"
class="btn m-0 p-0 border-0"
disabled
>
<i class="bi bi-question-circle" style="font-size: 1rem"></i>
</button>
</div>
</div>
</div>
</div>
<div class="card-body p-2">
<div class="input-group input-group-sm bottom-0 m-0">
<div class="me-2">
<div class="input-group input-group-sm">
<span class="input-group-text">QRG</span>
<span class="input-group-text">{{ state.frequency }} Hz</span>
<!-- Dropdown Button -->
<button
v-bind:class="{
disabled: state.hamlib_status === 'disconnected',
}"
class="btn btn-secondary dropdown-toggle"
type="button"
id="dropdownMenuButton"
data-bs-toggle="dropdown"
aria-expanded="false"
></button>
<!-- Dropdown Menu -->
<ul class="dropdown-menu" aria-labelledby="dropdownMenuButton">
<li>
<div class="input-group p-1">
<span class="input-group-text">frequency</span>
<input
v-model="state.new_frequency"
style="max-width: 8rem"
pattern="[0-9]*"
type="text"
class="form-control form-control-sm"
v-bind:class="{
disabled: state.hamlib_status === 'disconnected',
}"
placeholder="Type frequency..."
aria-label="Frequency"
/>
<button
class="btn btn-sm btn-outline-success"
type="button"
@click="updateFrequencyAndApply(state.new_frequency)"
v-bind:class="{
disabled: state.hamlib_status === 'disconnected',
}"
>
<i class="bi bi-check-square"></i>
</button>
</div>
</li>
<!-- Dropdown Divider -->
<li><hr class="dropdown-divider" /></li>
<!-- Dropdown Items -->
<li>
<a
class="dropdown-item"
href="#"
@click="updateFrequencyAndApply(50616000)"
><strong>50616 kHz</strong>&nbsp;
<span class="badge bg-secondary">6m | USB</span>&nbsp;
<span class="badge bg-info">EU | US</span>
</a>
</li>
<li>
<a
class="dropdown-item"
href="#"
@click="updateFrequencyAndApply(50308000)"
><strong>50308 kHz</strong>&nbsp;
<span class="badge bg-secondary">6m | USB</span>&nbsp;
<span class="badge bg-info">US</span></a
>
</li>
<li>
<a
class="dropdown-item"
href="#"
@click="updateFrequencyAndApply(28093000)"
><strong>28093 kHz</strong>&nbsp;
<span class="badge bg-secondary">10m | USB</span>&nbsp;
<span class="badge bg-info">EU | US</span>
</a>
</li>
<li>
<a
class="dropdown-item"
href="#"
@click="updateFrequencyAndApply(27265000)"
><strong>27265 kHz</strong>&nbsp;
<span class="badge bg-secondary">11m | USB</span>&nbsp;
<span class="badge bg-dark">Ch 26</span></a
>
</li>
<li>
<a
class="dropdown-item"
href="#"
@click="updateFrequencyAndApply(27245000)"
><strong>27245 kHz</strong>&nbsp;
<span class="badge bg-secondary">11m | USB</span>&nbsp;
<span class="badge bg-dark">Ch 25</span></a
>
</li>
<li>
<a
class="dropdown-item"
href="#"
@click="updateFrequencyAndApply(24908000)"
><strong>24908 kHz</strong>&nbsp;
<span class="badge bg-secondary">12m | USB</span>&nbsp;
<span class="badge bg-info">EU | US</span>
</a>
</li>
<li>
<a
class="dropdown-item"
href="#"
@click="updateFrequencyAndApply(21093000)"
><strong>21093 kHz</strong>&nbsp;
<span class="badge bg-secondary">15m | USB</span>&nbsp;
<span class="badge bg-info">EU | US</span>
</a>
</li>
<li>
<a
class="dropdown-item"
href="#"
@click="updateFrequencyAndApply(14093000)"
><strong>14093 kHz</strong>&nbsp;
<span class="badge bg-secondary">20m | USB</span>&nbsp;
<span class="badge bg-info">EU | US</span>
</a>
</li>
<li>
<a
class="dropdown-item"
href="#"
@click="updateFrequencyAndApply(7053000)"
><strong>7053 kHz</strong>&nbsp;
<span class="badge bg-secondary">40m | USB</span>&nbsp;
<span class="badge bg-info">EU | US</span>
</a>
</li>
</ul>
</div>
</div>
<div class="me-2">
<div class="input-group input-group-sm">
<span class="input-group-text">Mode</span>
<select
class="form-control"
v-model="state.mode"
@click="set_radio_parameters()"
v-bind:class="{
disabled: state.hamlib_status === 'disconnected',
}"
>
<option value="USB">USB</option>
<option value="LSB">LSB</option>
<option value="PKTUSB">PKT-U</option>
<option value="PKTLSB">PKT-L</option>
<option value="AM">AM</option>
<option value="FM">FM</option>
<option value="PKTFM">PKTFM</option>
</select>
</div>
</div>
<div class="me-2">
<div class="input-group input-group-sm">
<span class="input-group-text">Power</span>
<select
class="form-control"
v-model="state.rf_level"
@click="set_radio_parameters()"
v-bind:class="{
disabled: state.hamlib_status === 'disconnected',
}"
>
<option value="0">-</option>
<option value="10">10</option>
<option value="20">20</option>
<option value="30">30</option>
<option value="40">40</option>
<option value="50">50</option>
<option value="60">60</option>
<option value="70">70</option>
<option value="80">80</option>
<option value="90">90</option>
<option value="100">100</option>
</select>
<span class="input-group-text">%</span>
</div>
</div>
</div>
</div>
</div>
</div>
</template>

View file

@ -1,402 +0,0 @@
<script setup lang="ts">
// @ts-nocheck
// reason for no check is, that we have some mixing of typescript and chart js which seems to be not to be fixed that easy
import { setActivePinia } from "pinia";
import pinia from "../store/index";
setActivePinia(pinia);
import { settingsStore as settings } from "../store/settingsStore.js";
import { useStateStore } from "../store/stateStore.js";
const state = useStateStore(pinia);
import {
Chart as ChartJS,
CategoryScale,
LinearScale,
PointElement,
LineElement,
Title,
Tooltip,
Legend,
} from "chart.js";
import { Line, Scatter } from "vue-chartjs";
import { computed } from "vue";
function selectStatsControl(obj) {
switch (obj.delegateTarget.id) {
case "list-waterfall-list":
settings.local.spectrum = "waterfall";
break;
case "list-scatter-list":
settings.local.spectrum = "scatter";
break;
case "list-chart-list":
settings.local.spectrum = "chart";
break;
default:
settings.local.spectrum = "waterfall";
}
//saveSettingsToFile();
}
ChartJS.register(
CategoryScale,
LinearScale,
PointElement,
LineElement,
Title,
Tooltip,
Legend,
);
// https://www.chartjs.org/docs/latest/samples/line/segments.html
const skipped = (speedCtx, value) =>
speedCtx.p0.skip || speedCtx.p1.skip ? value : undefined;
const down = (speedCtx, value) =>
speedCtx.p0.parsed.y > speedCtx.p1.parsed.y ? value : undefined;
var transmissionSpeedChartOptions = {
//type: "line",
responsive: true,
animations: true,
maintainAspectRatio: false,
cubicInterpolationMode: "monotone",
tension: 0.4,
scales: {
SNR: {
type: "linear",
ticks: { beginAtZero: false, color: "rgb(255, 99, 132)" },
position: "right",
},
SPEED: {
type: "linear",
ticks: { beginAtZero: false, color: "rgb(120, 100, 120)" },
position: "left",
grid: {
drawOnChartArea: false, // only want the grid lines for one axis to show up
},
},
x: { ticks: { beginAtZero: true } },
},
};
const transmissionSpeedChartData = computed(() => ({
labels: state.arq_speed_list_timestamp,
datasets: [
{
type: "line",
label: "SNR[dB]",
data: state.arq_speed_list_snr,
borderColor: "rgb(75, 192, 192, 1.0)",
pointRadius: 1,
segment: {
borderColor: (speedCtx) =>
skipped(speedCtx, "rgb(0,0,0,0.4)") ||
down(speedCtx, "rgb(192,75,75)"),
borderDash: (speedCtx) => skipped(speedCtx, [3, 3]),
},
spanGaps: true,
backgroundColor: "rgba(75, 192, 192, 0.2)",
order: 1,
yAxisID: "SNR",
},
{
type: "bar",
label: "Speed[bpm]",
data: state.arq_speed_list_bpm,
borderColor: "rgb(120, 100, 120, 1.0)",
backgroundColor: "rgba(120, 100, 120, 0.2)",
order: 0,
yAxisID: "SPEED",
},
],
}));
const scatterChartOptions = {
responsive: true,
maintainAspectRatio: false,
scales: {
x: {
type: "linear",
position: "bottom",
grid: {
display: true,
lineWidth: 1, // Set the line width for x-axis grid lines
},
ticks: {
display: false,
},
},
y: {
type: "linear",
position: "left",
grid: {
display: true,
lineWidth: 1, // Set the line width for y-axis grid lines
},
ticks: {
display: false,
},
},
},
plugins: {
legend: {
display: false,
},
tooltip: {
enabled: false,
},
},
};
// dummy data
//state.scatter = [{"x":"166","y":"46"},{"x":"-193","y":"-139"},{"x":"-165","y":"-291"},{"x":"311","y":"-367"},{"x":"389","y":"199"},{"x":"78","y":"372"},{"x":"242","y":"-431"},{"x":"-271","y":"-248"},{"x":"28","y":"-130"},{"x":"-20","y":"187"},{"x":"74","y":"362"},{"x":"-316","y":"-229"},{"x":"-180","y":"261"},{"x":"321","y":"360"},{"x":"438","y":"-288"},{"x":"378","y":"-94"},{"x":"462","y":"-163"},{"x":"-265","y":"248"},{"x":"210","y":"314"},{"x":"230","y":"-320"},{"x":"261","y":"-244"},{"x":"-283","y":"-373"}]
const scatterChartData = computed(() => ({
datasets: [
{
type: "scatter",
fill: true,
data: state.scatter,
label: "Scatter",
tension: 0.1,
borderColor: "rgb(0, 255, 0)",
},
],
}));
</script>
<script lang="ts">
import { initWaterfall, setColormap } from "../js/waterfallHandler.js";
var localSpectrum;
export default {
mounted() {
// This code will be executed after the component is mounted to the DOM
// You can access DOM elements or perform other initialization here
//const myElement = this.$refs.waterfall; // Access the DOM element with ref
// init waterfall
localSpectrum = initWaterfall("waterfall-main");
},
};
</script>
<template>
<div class="card mb-1" style="height: calc(var(--variable-height) - 20px)">
<div class="card-header p-1">
<div class="container">
<div class="row">
<div class="col-11 p-0">
<div class="btn-group h-100" role="group">
<div
class="list-group bg-body-tertiary list-group-horizontal"
id="list-tab"
role="tablist"
>
<a
class="py-0 list-group-item list-group-item-dark list-group-item-action"
id="list-waterfall-list"
data-bs-toggle="list"
href="#list-waterfall"
role="tab"
aria-controls="list-waterfall"
v-bind:class="{
active: settings.local.spectrum === 'waterfall',
}"
@click="selectStatsControl($event)"
><strong><i class="bi bi-water"></i></strong
></a>
<a
class="py-0 list-group-item list-group-item-dark list-group-item-action"
id="list-scatter-list"
data-bs-toggle="list"
href="#list-scatter"
role="tab"
aria-controls="list-scatter"
v-bind:class="{
active: settings.local.spectrum === 'scatter',
}"
@click="selectStatsControl($event)"
><strong><i class="bi bi-border-outer"></i></strong
></a>
<a
class="py-0 list-group-item list-group-item-dark list-group-item-action"
id="list-chart-list"
data-bs-toggle="list"
href="#list-chart"
role="tab"
aria-controls="list-chart"
v-bind:class="{ active: settings.local.spectrum === 'chart' }"
@click="selectStatsControl($event)"
><strong><i class="bi bi-graph-up-arrow"></i></strong
></a>
</div>
</div>
<div class="btn-group" role="group" aria-label="Busy indicators">
<button
class="btn btn-sm ms-1 p-1 disabled"
type="button"
data-bs-placement="top"
data-bs-toggle="tooltip"
data-bs-trigger="hover"
data-bs-html="true"
v-bind:class="{
'btn-warning': state.channel_busy_slot[0] === true,
'btn-outline-secondary': state.channel_busy_slot[0] === false,
}"
title="Channel busy state: <strong class='text-success'>not busy</strong> / <strong class='text-danger'>busy </strong>"
>
S1
</button>
<button
class="btn btn-sm p-1 disabled"
type="button"
data-bs-placement="top"
data-bs-toggle="tooltip"
data-bs-trigger="hover"
data-bs-html="true"
v-bind:class="{
'btn-warning': state.channel_busy_slot[1] === true,
'btn-outline-secondary': state.channel_busy_slot[1] === false,
}"
title="Channel busy state: <strong class='text-success'>not busy</strong> / <strong class='text-danger'>busy </strong>"
>
S2
</button>
<button
class="btn btn-sm p-1 disabled"
type="button"
data-bs-placement="top"
data-bs-toggle="tooltip"
data-bs-trigger="hover"
data-bs-html="true"
v-bind:class="{
'btn-warning': state.channel_busy_slot[2] === true,
'btn-outline-secondary': state.channel_busy_slot[2] === false,
}"
title="Channel busy state: <strong class='text-success'>not busy</strong> / <strong class='text-danger'>busy </strong>"
>
S3
</button>
<button
class="btn btn-sm p-1 disabled"
type="button"
data-bs-placement="top"
data-bs-toggle="tooltip"
data-bs-trigger="hover"
data-bs-html="true"
v-bind:class="{
'btn-warning': state.channel_busy_slot[3] === true,
'btn-outline-secondary': state.channel_busy_slot[3] === false,
}"
title="Channel busy state: <strong class='text-success'>not busy</strong> / <strong class='text-danger'>busy </strong>"
>
S4
</button>
<button
class="btn btn-sm p-1 disabled"
type="button"
data-bs-placement="top"
data-bs-toggle="tooltip"
data-bs-trigger="hover"
data-bs-html="true"
v-bind:class="{
'btn-warning': state.channel_busy_slot[4] === true,
'btn-outline-secondary': state.channel_busy_slot[4] === false,
}"
title="Channel busy state: <strong class='text-success'>not busy</strong> / <strong class='text-danger'>busy </strong>"
>
S5
</button>
<button
class="btn btn-sm p-1 disabled"
type="button"
data-bs-placement="top"
data-bs-toggle="tooltip"
data-bs-trigger="hover"
data-bs-html="true"
title="Recieving data: illuminates <strong class='text-success'>green</strong> if receiving codec2 data"
v-bind:class="{
'btn-success': state.is_codec2_traffic === true,
'btn-outline-secondary': state.is_codec2_traffic === false,
}"
>
data
</button>
</div>
</div>
<div class="col-1 text-end">
<button
type="button"
id="openHelpModalWaterfall"
data-bs-toggle="modal"
data-bs-target="#waterfallHelpModal"
class="btn m-0 p-0 border-0"
>
<i class="bi bi-question-circle" style="font-size: 1rem"></i>
</button>
</div>
</div>
</div>
</div>
<div class="card-body p-1">
<div class="tab-content" id="nav-stats-tabContent">
<div
class="tab-pane fade"
v-bind:class="{
'show active': settings.local.spectrum === 'waterfall',
}"
id="list-waterfall"
role="stats_tabpanel"
aria-labelledby="list-waterfall-list"
>
<canvas
ref="waterfall-main"
id="waterfall-main"
style="
position: relative;
z-index: 2;
aspect-ratio: unset;
width: 100%;
height: 200px;
"
class="force-gpu'"
></canvas>
</div>
<div
class="tab-pane fade"
v-bind:class="{
'show active': settings.local.spectrum === 'scatter',
}"
id="list-scatter"
role="tabpanel"
aria-labelledby="list-scatter-list"
>
<Scatter :data="scatterChartData" :options="scatterChartOptions" />
</div>
<div
class="tab-pane fade"
v-bind:class="{ 'show active': settings.local.spectrum === 'chart' }"
id="list-chart"
role="tabpanel"
aria-labelledby="list-chart-list"
>
<Line
:data="transmissionSpeedChartData"
:options="transmissionSpeedChartOptions"
/>
</div>
</div>
<!--278px-->
</div>
</div>
</template>

View file

@ -1,62 +0,0 @@
<script setup>
import { setActivePinia } from "pinia";
import pinia from "../store/index";
setActivePinia(pinia);
import { useAudioStore } from "../store/audioStore.js";
const audio = useAudioStore(pinia);
import { setConfig } from "../js/api";
</script>
<template>
<div class="card mb-0">
<div class="card-header p-1">
<div class="container">
<div class="row">
<div class="col-1">
<i class="bi bi-volume-up" style="font-size: 1.2rem"></i>
</div>
<div class="col-10">
<strong class="fs-5">Audio devices</strong>
</div>
<div class="col-1 text-end">
<button
type="button"
data-bs-toggle="modal"
data-bs-target="#audioHelpModal"
class="btn m-0 p-0 border-0"
>
<i class="bi bi-question-circle" style="font-size: 1rem"></i>
</button>
</div>
</div>
</div>
</div>
<div class="card-body p-2" style="height: 100px">
<div class="input-group input-group-sm mb-1">
<span class="input-group-text">
<i class="bi bi-mic-fill" style="font-size: 1rem"></i>
</span>
<select
class="form-select form-select-sm"
aria-label=".form-select-sm"
v-html="audio.getInputDevices()"
@change="setConfig"
></select>
</div>
<div class="input-group input-group-sm">
<span class="input-group-text">
<i class="bi bi-volume-up" style="font-size: 1rem"></i>
</span>
<select
class="form-select form-select-sm"
aria-label=".form-select-sm"
v-html="audio.getOutputDevices()"
@change="setConfig"
></select>
</div>
</div>
</div>
</template>

View file

@ -1,114 +0,0 @@
<script setup lang="ts">
import { setActivePinia } from "pinia";
import pinia from "../store/index";
setActivePinia(pinia);
import { settingsStore as settings } from "../store/settingsStore.js";
</script>
<template>
<div class="card mb-1">
<div class="card-header p-1">
<div class="container">
<div class="row">
<div class="col-1">
<i class="bi bi-house-door" style="font-size: 1.2rem"></i>
</div>
<div class="col-10">
<strong class="fs-5">My station</strong>
</div>
<div class="col-1 text-end">
<button
type="button"
id="openHelpModalStation"
data-bs-toggle="modal"
data-bs-target="#stationHelpModal"
class="btn m-0 p-0 border-0"
>
<i class="bi bi-question-circle" style="font-size: 1rem"></i>
</button>
</div>
</div>
</div>
</div>
<div class="card-body p-2">
<div class="row">
<div class="col-md-auto">
<div
class="input-group input-group-sm mb-0"
data-bs-placement="bottom"
data-bs-toggle="tooltip"
data-bs-trigger="hover"
data-bs-html="false"
title="Enter your callsign and save it"
>
<span class="input-group-text">
<i class="bi bi-person-bounding-box" style="font-size: 1rem"></i>
</span>
<input
type="text"
class="form-control"
style="width: 5rem; text-transform: uppercase"
placeholder="callsign"
pattern="[A-Z]*"
id="myCall"
maxlength="8"
aria-label="Input group"
aria-describedby="btnGroupAddon"
v-model="settings.remote.STATION.mycall"
/>
<select
class="form-select form-select-sm"
aria-label=".form-select-sm"
id="myCallSSID"
v-model="settings.remote.STATION.myssid"
>
<option selected value="0">0</option>
<option value="1">1</option>
<option value="2">2</option>
<option value="3">3</option>
<option value="4">4</option>
<option value="5">5</option>
<option value="6">6</option>
<option value="7">7</option>
<option value="8">8</option>
<option value="9">9</option>
<option value="10">10</option>
<option value="11">11</option>
<option value="12">12</option>
<option value="13">13</option>
<option value="14">14</option>
<option value="15">15</option>
</select>
</div>
</div>
<div class="col-md-auto">
<div
class="input-group input-group-sm mb-0"
data-bs-placement="bottom"
data-bs-toggle="tooltip"
data-bs-trigger="hover"
data-bs-html="false"
title="Enter your gridsquare and save it"
>
<span class="input-group-text">
<i class="bi bi-house-fill" style="font-size: 1rem"></i>
</span>
<input
type="text"
class="form-control mr-1"
style="max-width: 6rem"
placeholder="locator"
id="myGrid"
maxlength="6"
aria-label="Input group"
aria-describedby="btnGroupAddon"
v-model="settings.remote.STATION.mygrid"
/>
</div>
</div>
</div>
<!-- end of row-->
</div>
</div>
</template>

View file

@ -1,260 +0,0 @@
<script setup lang="ts">
import { setActivePinia } from "pinia";
import pinia from "../store/index";
setActivePinia(pinia);
import { settingsStore as settings} from "../store/settingsStore.js";
import { useStateStore } from "../store/stateStore.js";
const state = useStateStore(pinia);
function startStopRigctld() {
switch (state.rigctld_started) {
case "stopped":
settings.remote.RADIO.serial_port = (<HTMLInputElement>document.getElementById("hamlib_deviceport")).value;
//startRigctld();
break;
case "running":
//stopRigctld();
// dirty hack for calling this command twice, otherwise modem won't stop rigctld from time to time
//stopRigctld();
break;
default:
}
}
function selectRadioControl() {
// @ts-expect-error
switch (event.target.id) {
case "list-rig-control-none-list":
settings.remote.RADIO.control = "disabled";
break;
case "list-rig-control-rigctld-list":
settings.remote.RADIO.control = "rigctld";
break;
case "list-rig-control-tci-list":
settings.remote.RADIO.control = "tci";
break;
default:
console.log("default=!==");
settings.remote.RADIO.control = "disabled";
}
//saveSettingsToFile();
}
function testHamlib(){
console.log("not yet implemented")
alert("not yet implemented")
}
</script>
<template>
<div class="mb-3">
<div class="card mb-1">
<div class="card-header p-1">
<div class="container">
<div class="row">
<div class="col-1">
<i class="bi bi-projector" style="font-size: 1.2rem"></i>
</div>
<div class="col-4">
<strong class="fs-5">Rig control</strong>
</div>
<div class="col-6">
<div
class="list-group bg-body-tertiary list-group-horizontal w-75"
id="rig-control-list-tab"
role="rig-control-tablist"
>
<a
class="p-1 list-group-item list-group-item-dark list-group-item-action"
id="list-rig-control-none-list"
data-bs-toggle="list"
href="#list-rig-control-none"
role="tab"
aria-controls="list-rig-control-none"
v-bind:class="{ active: settings.remote.RADIO.control === 'disabled' }"
@click="selectRadioControl()"
>None</a
>
<a
class="p-1 list-group-item list-group-item-dark list-group-item-action"
id="list-rig-control-rigctld-list"
data-bs-toggle="list"
href="#list-rig-control-rigctld"
role="tab"
aria-controls="list-rig-control-rigctld"
v-bind:class="{ active: settings.remote.RADIO.control === 'rigctld' }"
@click="selectRadioControl()"
>Rigctld</a
>
<a
class="p-1 list-group-item list-group-item-dark list-group-item-action"
id="list-rig-control-tci-list"
data-bs-toggle="list"
href="#list-rig-control-tci"
role="tab"
aria-controls="list-rig-control-tci"
v-bind:class="{ active: settings.remote.RADIO.control === 'tci' }"
@click="selectRadioControl()"
>TCI</a
>
</div>
</div>
<div class="col-1 text-end">
<button
type="button"
id="openHelpModalRigControl"
data-bs-toggle="modal"
data-bs-target="#rigcontrolHelpModal"
class="btn m-0 p-0 border-0"
>
<i class="bi bi-question-circle" style="font-size: 1rem"></i>
</button>
</div>
</div>
</div>
</div>
<div class="card-body p-2" style="height: 100px">
<div class="tab-content" id="rig-control-nav-tabContent">
<div
class="tab-pane fade"
v-bind:class="{ 'show active': settings.remote.RADIO.control === 'disabled' }"
id="list-rig-control-none"
role="tabpanel"
aria-labelledby="list-rig-control-none-list"
>
<p class="small">
Modem will not utilize rig control and features will be limited. While
functional; it is recommended to configure hamlib. <br>
Use this setting also for <strong> VOX </strong>
</p>
</div>
<div
class="tab-pane fade"
id="list-rig-control-rigctld"
v-bind:class="{ 'show active': settings.remote.RADIO.control === 'rigctld' }"
role="tabpanel"
aria-labelledby="list-rig-control-rigctld-list"
>
<div class="input-group input-group-sm mb-1">
<div class="input-group input-group-sm mb-1">
<span class="input-group-text">Rigctld service</span>
<button
class="btn btn-outline-success"
type="button"
id="hamlib_rigctld_start"
@click="startStopRigctld"
>
Start
</button>
<button
class="btn btn-outline-danger"
type="button"
id="hamlib_rigctld_stop"
@click="startStopRigctld"
>
Stop
</button>
<input
type="text"
class="form-control"
placeholder="Status"
id="hamlib_rigctld_status"
aria-label="State"
aria-describedby="basic-addon1"
v-model="state.rigctld_started"
/>
<button
type="button"
id="testHamlib"
class="btn btn-sm btn-outline-secondary ms-1"
data-bs-placement="bottom"
data-bs-toggle="tooltip"
data-bs-trigger="hover"
data-bs-html="true"
@click="testHamlib"
title="Test your hamlib settings and toggle PTT once. Button will become <strong class='text-success'>green</strong> on success and <strong class='text-danger'>red</strong> if fails."
>
PTT Test
</button>
</div>
</div>
</div>
<div
class="tab-pane fade"
id="list-rig-control-tci"
v-bind:class="{ 'show active': settings.remote.RADIO.control === 'tci' }"
role="tabpanel"
aria-labelledby="list-rig-control-tci-list"
>
<div class="input-group input-group-sm mb-1">
<div class="input-group input-group-sm mb-1">
<span class="input-group-text">TCI</span>
<span class="input-group-text">Address</span>
<input
type="text"
class="form-control"
placeholder="tci IP"
id="tci_ip"
aria-label="Device IP"
v-model="settings.remote.TCI.tci_ip"
/>
</div>
<div class="input-group input-group-sm mb-1">
<span class="input-group-text">Port</span>
<input
type="text"
class="form-control"
placeholder="tci port"
id="tci_port"
aria-label="Device Port"
v-model="settings.remote.TCI.tci_port"
/>
</div>
</div>
</div>
</div>
<!-- RADIO CONTROL DISABLED -->
<div id="radio-control-disabled"></div>
<!-- RADIO CONTROL RIGCTLD -->
<div id="radio-control-rigctld"></div>
<!-- RADIO CONTROL TCI-->
<div id="radio-control-tci"></div>
<!-- RADIO CONTROL HELP -->
<div id="radio-control-help">
<!--
<strong>VOX:</strong> Use rig control mode 'none'
<br />
<strong>HAMLIB locally:</strong> configure in settings, then
start/stop service.
<br />
<strong>HAMLIB remotely:</strong> Enter IP/Port, connection
happens automatically.
-->
</div>
</div>
<!--<div class="card-footer text-muted small" id="hamlib_info_field">
Define Modem rig control mode (none/hamlib)
</div>
-->
</div>
</div>
</template>

View file

@ -2,17 +2,21 @@
import { Modal } from "bootstrap"; import { Modal } from "bootstrap";
import { onMounted } from "vue"; import { onMounted } from "vue";
import infoScreen_updater from "./infoScreen_updater.vue";
import { setActivePinia } from "pinia"; import { setActivePinia } from "pinia";
import pinia from "../store/index"; import pinia from "../store/index";
setActivePinia(pinia); setActivePinia(pinia);
import { settingsStore as settings, onChange } from "../store/settingsStore.js"; import { settingsStore as settings, onChange } from "../store/settingsStore.js";
import { sendModemCQ } from "../js/api.js";
import { useStateStore } from "../store/stateStore.js"; import { useStateStore } from "../store/stateStore.js";
const state = useStateStore(pinia); const state = useStateStore(pinia);
import { useAudioStore } from "../store/audioStore";
const audioStore = useAudioStore();
import { useSerialStore } from "../store/serialStore";
const serialStore = useSerialStore();
import { import {
getVersion, getVersion,
setConfig, setConfig,
@ -20,11 +24,8 @@ import {
stopModem, stopModem,
getModemState, getModemState,
} from "../js/api"; } from "../js/api";
import { audioInputOptions, audioOutputOptions } from "../js/deviceFormHelper";
import { serialDeviceOptions } from "../js/deviceFormHelper";
const version = import.meta.env.PACKAGE_VERSION; const version = import.meta.env.PACKAGE_VERSION;
var updateAvailable = process.env.FDUpdateAvail;
// start modemCheck modal once on startup // start modemCheck modal once on startup
onMounted(() => { onMounted(() => {
@ -50,6 +51,7 @@ function getRigControlStuff() {
case "disabled": case "disabled":
return true; return true;
case "rigctld": case "rigctld":
case "rigctld_bundle":
case "tci": case "tci":
return state.radio_status; return state.radio_status;
default: default:
@ -61,7 +63,7 @@ function getRigControlStuff() {
} }
function testHamlib() { function testHamlib() {
alert("Not yet implemented."); sendModemCQ();
} }
</script> </script>
@ -124,6 +126,7 @@ function testHamlib() {
max="65534" max="65534"
min="1025" min="1025"
v-model="settings.local.port" v-model="settings.local.port"
@change="onChange"
/> />
</div> </div>
@ -135,6 +138,7 @@ function testHamlib() {
placeholder="modem host (default 127.0.0.1)" placeholder="modem host (default 127.0.0.1)"
id="modem_port" id="modem_port"
v-model="settings.local.host" v-model="settings.local.host"
@change="onChange"
/> />
</div> </div>
</div> </div>
@ -201,6 +205,7 @@ function testHamlib() {
</button> </button>
</label> </label>
</div> </div>
<!-- Audio Input Device --> <!-- Audio Input Device -->
<div class="input-group input-group-sm mb-1"> <div class="input-group input-group-sm mb-1">
<label class="input-group-text w-50" <label class="input-group-text w-50"
@ -213,10 +218,10 @@ function testHamlib() {
v-model="settings.remote.AUDIO.input_device" v-model="settings.remote.AUDIO.input_device"
> >
<option <option
v-for="option in audioInputOptions()" v-for="device in audioStore.audioInputs"
v-bind:value="option.id" :value="device.id"
> >
{{ option.name }} [{{ option.api }}] {{ device.name }} [{{ device.api }}]
</option> </option>
</select> </select>
</div> </div>
@ -233,10 +238,10 @@ function testHamlib() {
v-model="settings.remote.AUDIO.output_device" v-model="settings.remote.AUDIO.output_device"
> >
<option <option
v-for="option in audioOutputOptions()" v-for="device in audioStore.audioOutputs"
v-bind:value="option.id" :value="device.id"
> >
{{ option.name }} [{{ option.api }}] {{ device.name }} [{{ device.api }}]
</option> </option>
</select> </select>
</div> </div>
@ -284,52 +289,47 @@ function testHamlib() {
<option selected value="disabled"> <option selected value="disabled">
Disabled (no rig control; use with VOX) Disabled (no rig control; use with VOX)
</option> </option>
<option selected value="rigctld">Rigctld (Hamlib)</option> <option selected value="rigctld">
Rigctld (external Hamlib)
</option>
<option selected value="rigctld_bundle">
Rigctld (internal Hamlib)
</option>
<option selected value="tci">TCI</option> <option selected value="tci">TCI</option>
</select> </select>
</div> </div>
<div <div
:class=" :class="
settings.remote.RADIO.control == 'rigctld' ? '' : 'd-none' settings.remote.RADIO.control == 'rigctld_bundle'
? ''
: 'd-none'
" "
> >
<!-- Shown when rigctld is selected--> <!-- Shown when rigctld is selected-->
<div class="input-group input-group-sm mb-1"> <div class="input-group input-group-sm mb-1">
<label class="input-group-text w-25" <span class="input-group-text" style="width: 180px"
>Rigctld control</label >Radio port</span
> >
<label class="input-group-text">
<button <select
type="button" @change="onChange"
class="btn btn-sm btn-outline-success" v-model="settings.remote.RADIO.serial_port"
data-bs-toggle="tooltip" class="form-select form-select-sm"
data-bs-trigger="hover" >
data-bs-html="false" <option
title="Start rigctld" v-for="device in serialStore.serialDevices"
v-bind:class="{ :value="device.port"
disabled: state.rigctld_started == 'true', :key="device.port"
}"
> >
<i class="bi bi-play-fill"></i> {{ device.description }}
</button> </label </option>
><label class="input-group-text"> </select>
<button </div>
type="button"
class="btn btn-sm btn-outline-danger" <div class="input-group input-group-sm mb-1">
data-bs-toggle="tooltip" <label class="input-group-text w-25">Rigctld Test</label>
data-bs-trigger="hover"
data-bs-html="false"
title="Stop rigctld"
v-bind:class="{
disabled:
state.rigctld_started == 'false' ||
state.rigctld_started === undefined,
}"
>
<i class="bi bi-stop-fill"></i>
</button>
</label>
<label class="input-group-text"> <label class="input-group-text">
<button <button
type="button" type="button"
@ -342,32 +342,10 @@ function testHamlib() {
@click="testHamlib" @click="testHamlib"
title="Test your hamlib settings and toggle PTT once. Button will become <strong class='text-success'>green</strong> on success and <strong class='text-danger'>red</strong> if fails." title="Test your hamlib settings and toggle PTT once. Button will become <strong class='text-success'>green</strong> on success and <strong class='text-danger'>red</strong> if fails."
> >
PTT test Send a CQ
</button> </button>
</label> </label>
</div> </div>
<div class="input-group input-group-sm mb-1">
<span class="input-group-text" style="width: 180px"
>Radio port</span
>
<select
class="form-select form-select-sm"
aria-label=".form-select-sm"
id="hamlib_deviceport"
style="width: 7rem"
@change="onChange"
v-model="settings.remote.RADIO.serial_port"
>
<option
v-for="option in serialDeviceOptions()"
v-bind:value="option.port"
>
{{ option.description }}
</option>
</select>
</div>
</div> </div>
<div <div
:class=" :class="
@ -415,18 +393,6 @@ function testHamlib() {
data-bs-toggle="collapse" data-bs-toggle="collapse"
> >
Version Version
<span
class="badge ms-2"
:class="
updateAvailable === '1' ? 'bg-warning' : 'bg-success'
"
>
{{
updateAvailable === "1"
? "Update available ! ! ! !"
: "Current"
}}</span
>
</button> </button>
</h2> </h2>
<div <div
@ -449,9 +415,6 @@ function testHamlib() {
> >
Modem version | {{ state.modem_version }} Modem version | {{ state.modem_version }}
</button> </button>
<div :class="updateAvailable === '1' ? '' : 'd-none'">
<infoScreen_updater />
</div>
</div> </div>
</div> </div>
</div> </div>

View file

@ -10,7 +10,7 @@ import { settingsStore as settings } from "../store/settingsStore.js";
</script> </script>
<template> <template>
<nav class="navbar bg-body-tertiary border-bottom"> <nav class="navbar bg-body-tertiary border-bottom z-0">
<div class="mx-auto"> <div class="mx-auto">
<span class="badge bg-secondary me-4"> <span class="badge bg-secondary me-4">
Modem Connection {{ state.modem_connection }} Modem Connection {{ state.modem_connection }}

View file

@ -5,9 +5,24 @@ import { setActivePinia } from "pinia";
import pinia from "../store/index"; import pinia from "../store/index";
setActivePinia(pinia); setActivePinia(pinia);
import { settingsStore as settings } from "../store/settingsStore.js"; import { settingsStore as settings, onChange } from "../store/settingsStore.js";
</script> </script>
<template> <template>
<h5>...soon...</h5> <div class="input-group input-group-sm mb-1">
<label class="input-group-text w-50">Enable message auto repeat</label>
<label class="input-group-text w-50">
<div class="form-check form-switch form-check-inline ms-2">
<input
class="form-check-input"
type="checkbox"
@change="onChange"
v-model="settings.remote.MESSAGES.enable_auto_repeat"
/>
<label class="form-check-label" for="enableMessagesAutoRepeatSwitch"
>Re-send message on beacon</label
>
</div>
</label>
</div>
</template> </template>

File diff suppressed because it is too large Load diff

View file

@ -6,7 +6,9 @@ import { useStateStore } from "../store/stateStore.js";
const state = useStateStore(pinia); const state = useStateStore(pinia);
import { startModem, stopModem } from "../js/api.js"; import { startModem, stopModem } from "../js/api.js";
import { audioInputOptions, audioOutputOptions } from "../js/deviceFormHelper";
import { useAudioStore } from "../store/audioStore";
const audioStore = useAudioStore();
</script> </script>
<template> <template>
@ -74,8 +76,8 @@ import { audioInputOptions, audioOutputOptions } from "../js/deviceFormHelper";
@change="onChange" @change="onChange"
v-model="settings.remote.AUDIO.input_device" v-model="settings.remote.AUDIO.input_device"
> >
<option v-for="option in audioInputOptions()" v-bind:value="option.id"> <option v-for="device in audioStore.audioInputs" :value="device.id">
{{ option.name }} [{{ option.api }}] {{ device.name }} [{{ device.api }}]
</option> </option>
</select> </select>
</div> </div>
@ -89,11 +91,12 @@ import { audioInputOptions, audioOutputOptions } from "../js/deviceFormHelper";
@change="onChange" @change="onChange"
v-model="settings.remote.AUDIO.output_device" v-model="settings.remote.AUDIO.output_device"
> >
<option v-for="option in audioOutputOptions()" v-bind:value="option.id"> <option v-for="device in audioStore.audioOutputs" :value="device.id">
{{ option.name }} [{{ option.api }}] {{ device.name }} [{{ device.api }}]
</option> </option>
</select> </select>
</div> </div>
<!-- Audio rx level--> <!-- Audio rx level-->
<div class="input-group input-group-sm mb-1"> <div class="input-group input-group-sm mb-1">
<span class="input-group-text w-25">RX Audio Level</span> <span class="input-group-text w-25">RX Audio Level</span>
@ -162,70 +165,19 @@ import { audioInputOptions, audioOutputOptions } from "../js/deviceFormHelper";
</div> </div>
<div class="input-group input-group-sm mb-1"> <div class="input-group input-group-sm mb-1">
<label class="input-group-text w-25">Tuning range</label> <label class="input-group-text w-50">Maximum used bandwidth</label>
<label class="input-group-text">fmin</label>
<select <select
class="form-select form-select-sm" class="form-select form-select-sm"
id="tuning_range_fmin" id="maximum_bandwidth"
@change="onChange" @change="onChange"
v-model.number="settings.remote.MODEM.tuning_range_fmin" v-model.number="settings.remote.MODEM.maximum_bandwidth"
> >
<option value="-50">-50</option> <option value="250">250 Hz</option>
<option value="-100">-100</option> <option value="563">563 Hz</option>
<option value="-150">-150</option> <option value="1700">1700 Hz</option>
<option value="-200">-200</option>
<option value="-250">-250</option>
</select>
<label class="input-group-text">fmax</label>
<select
class="form-select form-select-sm"
id="tuning_range_fmax"
@change="onChange"
v-model.number="settings.remote.MODEM.tuning_range_fmax"
>
<option value="50">50</option>
<option value="100">100</option>
<option value="150">150</option>
<option value="200">200</option>
<option value="250">250</option>
</select>
</div>
<div class="input-group input-group-sm mb-1">
<span class="input-group-text w-50">Beacon interval</span>
<select
class="form-select form-select-sm"
aria-label=".form-select-sm"
id="beaconInterval"
style="width: 6rem"
@change="onChange"
v-model.number="settings.remote.MODEM.beacon_interval"
>
<option value="60">60 secs</option>
<option value="90">90 secs</option>
<option value="120">2 mins</option>
<option selected value="300">5 mins</option>
<option value="600">10 mins</option>
<option value="900">15 mins</option>
<option value="1800">30 mins</option>
<option value="3600">60 mins</option>
</select> </select>
</div> </div>
<div class="input-group input-group-sm mb-1">
<label class="input-group-text w-50">Enable 250Hz bandwidth mode</label>
<label class="input-group-text w-50">
<div class="form-check form-switch form-check-inline">
<input
class="form-check-input"
type="checkbox"
id="250HzModeSwitch"
v-model="settings.remote.MODEM.enable_low_bandwidth_mode"
@change="onChange"
/>
<label class="form-check-label" for="250HzModeSwitch">250Hz</label>
</div>
</label>
</div>
<div class="input-group input-group-sm mb-1"> <div class="input-group input-group-sm mb-1">
<label class="input-group-text w-50">Respond to CQ</label> <label class="input-group-text w-50">Respond to CQ</label>
<label class="input-group-text w-50"> <label class="input-group-text w-50">
@ -241,27 +193,4 @@ import { audioInputOptions, audioOutputOptions } from "../js/deviceFormHelper";
</div> </div>
</label> </label>
</div> </div>
<div class="input-group input-group-sm mb-1">
<label class="input-group-text w-50">RX buffer size</label>
<label class="input-group-text w-50">
<select
class="form-select form-select-sm"
id="rx_buffer_size"
@change="onChange"
v-model.number="settings.remote.MODEM.rx_buffer_size"
>
<option value="1">1</option>
<option value="2">2</option>
<option value="4">4</option>
<option value="8">8</option>
<option value="16">16</option>
<option value="32">32</option>
<option value="64">64</option>
<option value="128">128</option>
<option value="256">256</option>
<option value="512">512</option>
<option value="1024">1024</option>
</select>
</label>
</div>
</template> </template>

View file

@ -18,7 +18,8 @@ import settings_tci from "./settings_tci.vue";
<option selected value="disabled"> <option selected value="disabled">
Disabled / VOX (no rig control - use with VOX) Disabled / VOX (no rig control - use with VOX)
</option> </option>
<option selected value="rigctld">Rigctld (Hamlib)</option> <option selected value="rigctld">Rigctld (external Hamlib)</option>
<option selected value="rigctld_bundle">Rigctld (internal Hamlib)</option>
<option selected value="tci">TCI</option> <option selected value="tci">TCI</option>
</select> </select>
</div> </div>

View file

@ -1,6 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
import { settingsStore as settings, onChange } from "../store/settingsStore.js"; import { settingsStore as settings, onChange } from "../store/settingsStore.js";
import { serialDeviceOptions } from "../js/deviceFormHelper";
</script> </script>
<template> <template>

View file

@ -69,8 +69,11 @@ export async function getVersion() {
let data = await apiGet("/version").then((res) => { let data = await apiGet("/version").then((res) => {
return res; return res;
}); });
return data.version;
//return data["version"]; if (typeof data !== "undefined" && typeof data.version !== "undefined") {
return data.version;
}
return 0;
} }
export async function getConfig() { export async function getConfig() {
@ -139,10 +142,18 @@ export async function getModemState() {
return await apiGet("/modem/state"); return await apiGet("/modem/state");
} }
export async function setRadioParameters(frequency, mode, rf_level) { export async function setRadioParametersFrequency(frequency) {
return await apiPost("/radio", { return await apiPost("/radio", {
radio_frequency: frequency, radio_frequency: frequency,
});
}
export async function setRadioParametersMode(mode) {
return await apiPost("/radio", {
radio_mode: mode, radio_mode: mode,
});
}
export async function setRadioParametersRFLevel(rf_level) {
return await apiPost("/radio", {
radio_rf_level: rf_level, radio_rf_level: rf_level,
}); });
} }

View file

@ -1,53 +0,0 @@
import { getAudioDevices, getSerialDevices } from "./api";
let audioDevices = await getAudioDevices();
let serialDevices = await getSerialDevices();
//Dummy device data sent if unable to get devices from modem to prevent GUI crash
const skel = JSON.parse(`
[{
"api": "MME",
"id": "0000",
"name": "No devices received from modem",
"native_index": 0
}]`);
export function loadAudioDevices() {
getAudioDevices().then((devices) => {
audioDevices = devices;
});
}
export function loadSerialDevices() {
getSerialDevices().then((devices) => {
serialDevices = devices;
});
}
export function audioInputOptions() {
if (audioDevices === undefined) {
return skel;
}
return audioDevices.in;
}
export function audioOutputOptions() {
if (audioDevices === undefined) {
return skel;
}
return audioDevices.out;
}
export function serialDeviceOptions() {
//Return ignore option if no serialDevices
if (serialDevices === undefined)
return [{ description: "-- ignore --", port: "ignore" }];
if (serialDevices.findIndex((device) => device.port == "ignore") == -1) {
//Add an ignore option for rig and ptt for transceivers that don't require them
serialDevices.push({ description: "-- ignore --", port: "ignore" });
}
return serialDevices;
}

View file

@ -8,14 +8,14 @@ import {
} from "./chatHandler"; } from "./chatHandler";
*/ */
import { displayToast } from "./popupHandler"; import { displayToast } from "./popupHandler";
import { import { getFreedataMessages, getModemState, getAudioDevices } from "./api";
getFreedataMessages,
getConfig,
getAudioDevices,
getSerialDevices,
getModemState,
} from "./api";
import { processFreedataMessages } from "./messagesHandler.ts"; import { processFreedataMessages } from "./messagesHandler.ts";
import { processRadioStatus } from "./radioHandler.ts";
import { useAudioStore } from "../store/audioStore";
const audioStore = useAudioStore();
import { useSerialStore } from "../store/serialStore";
const serialStore = useSerialStore();
// ----------------- init pinia stores ------------- // ----------------- init pinia stores -------------
import { setActivePinia } from "pinia"; import { setActivePinia } from "pinia";
@ -24,7 +24,21 @@ setActivePinia(pinia);
import { useStateStore } from "../store/stateStore.js"; import { useStateStore } from "../store/stateStore.js";
const stateStore = useStateStore(pinia); const stateStore = useStateStore(pinia);
import { settingsStore as settings } from "../store/settingsStore.js"; import {
settingsStore as settings,
getRemote,
} from "../store/settingsStore.js";
export function loadAllData() {
getModemState();
getRemote();
getOverallHealth();
audioStore.loadAudioDevices();
serialStore.loadSerialDevices();
getFreedataMessages();
processFreedataMessages();
processRadioStatus();
}
export function connectionFailed(endpoint, event) { export function connectionFailed(endpoint, event) {
stateStore.modem_connection = "disconnected"; stateStore.modem_connection = "disconnected";
@ -91,11 +105,7 @@ export function eventDispatcher(data) {
switch (data["modem"]) { switch (data["modem"]) {
case "started": case "started":
displayToast("success", "bi-arrow-left-right", "Modem started", 5000); displayToast("success", "bi-arrow-left-right", "Modem started", 5000);
getModemState(); loadAllData();
getConfig();
getAudioDevices();
getSerialDevices();
getFreedataMessages();
return; return;
case "stopped": case "stopped":
@ -104,11 +114,7 @@ export function eventDispatcher(data) {
case "restarted": case "restarted":
displayToast("secondary", "bi-bootstrap-reboot", "Modem restarted", 5000); displayToast("secondary", "bi-bootstrap-reboot", "Modem restarted", 5000);
getModemState(); loadAllData();
getConfig();
getAudioDevices();
getSerialDevices();
getFreedataMessages();
return; return;
case "failed": case "failed":
@ -128,13 +134,8 @@ export function eventDispatcher(data) {
message = "Connected to server"; message = "Connected to server";
displayToast("success", "bi-ethernet", message, 5000); displayToast("success", "bi-ethernet", message, 5000);
stateStore.modem_connection = "connected"; stateStore.modem_connection = "connected";
getModemState();
getOverallHealth(); loadAllData();
getConfig();
getAudioDevices();
getSerialDevices();
getFreedataMessages();
processFreedataMessages();
return; return;
@ -177,6 +178,12 @@ export function eventDispatcher(data) {
100; 100;
stateStore.arq_total_bytes = stateStore.arq_total_bytes =
data["arq-transfer-outbound"].received_bytes; data["arq-transfer-outbound"].received_bytes;
stateStore.arq_speed_list_timestamp =
data["arq-transfer-outbound"].statistics.time_histogram;
stateStore.arq_speed_list_bpm =
data["arq-transfer-outbound"].statistics.bpm_histogram;
stateStore.arq_speed_list_snr =
data["arq-transfer-outbound"].statistics.snr_histogram;
return; return;
case "ABORTING": case "ABORTING":
@ -219,6 +226,13 @@ export function eventDispatcher(data) {
stateStore.dxcallsign = data["arq-transfer-inbound"].dxcall; stateStore.dxcallsign = data["arq-transfer-inbound"].dxcall;
stateStore.arq_transmission_percent = 0; stateStore.arq_transmission_percent = 0;
stateStore.arq_total_bytes = 0; stateStore.arq_total_bytes = 0;
stateStore.arq_speed_list_timestamp =
data["arq-transfer-inbound"].statistics.time_histogram;
stateStore.arq_speed_list_bpm =
data["arq-transfer-inbound"].statistics.bpm_histogram;
stateStore.arq_speed_list_snr =
data["arq-transfer-inbound"].statistics.snr_histogram;
return; return;
case "OPEN_ACK_SENT": case "OPEN_ACK_SENT":

View file

@ -64,7 +64,7 @@ export function sortByPropertyDesc(property) {
* @returns true or false if callsign appears to be valid with an SSID * @returns true or false if callsign appears to be valid with an SSID
*/ */
export function validateCallsignWithSSID(callsign: string) { export function validateCallsignWithSSID(callsign: string) {
var patt = new RegExp("^[A-Z]+[0-9][A-Z]*-(1[0-5]|[0-9])$"); var patt = new RegExp("^[A-Za-z0-9]{1,7}-[0-9]{1,3}$");
callsign = callsign; callsign = callsign;
if ( if (
callsign === undefined || callsign === undefined ||
@ -85,7 +85,7 @@ export function validateCallsignWithSSID(callsign: string) {
* @returns true or false if callsign appears to be valid without an SSID * @returns true or false if callsign appears to be valid without an SSID
*/ */
export function validateCallsignWithoutSSID(callsign: string) { export function validateCallsignWithoutSSID(callsign: string) {
var patt = new RegExp("^[A-Z]+[0-9][A-Z]+$"); var patt = new RegExp("^[A-Za-z0-9]{1,7}$");
if ( if (
callsign === undefined || callsign === undefined ||

View file

@ -82,8 +82,8 @@ function createSortedMessagesList(data: {
} }
export function newMessage(dxcall, body, attachments) { export function newMessage(dxcall, body, attachments) {
console.log(attachments);
sendFreedataMessage(dxcall, body, attachments); sendFreedataMessage(dxcall, body, attachments);
chatStore.triggerScrollToBottom();
} }
/* ------ TEMPORARY DUMMY FUNCTIONS --- */ /* ------ TEMPORARY DUMMY FUNCTIONS --- */

View file

@ -0,0 +1,20 @@
// pinia store setup
import { setActivePinia } from "pinia";
import pinia from "../store/index";
setActivePinia(pinia);
import { settingsStore as settings, onChange } from "../store/settingsStore.js";
import { useStateStore } from "../store/stateStore";
const stateStore = useStateStore(pinia);
import {
getRadioStatus,
} from "./api";
export async function processRadioStatus(){
let result = await getRadioStatus()
stateStore.mode = result.radio_mode
stateStore.frequency = result.radio_frequency
stateStore.rf_level = result.radio_rf_level
}

View file

@ -0,0 +1,39 @@
import { defineStore } from "pinia";
import { getAudioDevices } from "../js/api";
import { ref } from "vue";
// Define skel fallback data
const skel = [
{
api: "ERR",
id: "0000",
name: "No devices received from modem",
native_index: 0,
},
];
export const useAudioStore = defineStore("audioStore", () => {
const audioInputs = ref([]);
const audioOutputs = ref([]);
const loadAudioDevices = async () => {
try {
const devices = await getAudioDevices();
// Check if devices are valid and have entries, otherwise use skel
audioInputs.value = devices && devices.in.length > 0 ? devices.in : skel;
audioOutputs.value =
devices && devices.out.length > 0 ? devices.out : skel;
} catch (error) {
console.error("Failed to load audio devices:", error);
// Use skel as fallback in case of error
audioInputs.value = skel;
audioOutputs.value = skel;
}
};
return {
audioInputs,
audioOutputs,
loadAudioDevices,
};
});

View file

@ -7,6 +7,14 @@ export const useChatStore = defineStore("chatStore", () => {
var newChatCallsign = ref(); var newChatCallsign = ref();
var newChatMessage = ref(); var newChatMessage = ref();
/* ------------------------------------------------ */
// Scroll to bottom functions
const scrollTrigger = ref(0);
function triggerScrollToBottom() {
scrollTrigger.value++;
}
/* ------------------------------------------------ */ /* ------------------------------------------------ */
var chat_filter = ref([ var chat_filter = ref([
@ -92,5 +100,7 @@ export const useChatStore = defineStore("chatStore", () => {
arq_speed_list_bpm, arq_speed_list_bpm,
arq_speed_list_snr, arq_speed_list_snr,
arq_speed_list_timestamp, arq_speed_list_timestamp,
scrollTrigger,
triggerScrollToBottom,
}; };
}); });

View file

@ -0,0 +1,38 @@
import { defineStore } from "pinia";
import { getSerialDevices } from "../js/api"; // Make sure this points to the correct file
import { ref } from "vue";
// Define "skel" fallback data for serial devices
const skelSerial = [
{
description: "No devices received from modem",
port: "ignore", // Using "ignore" as a placeholder value
},
];
export const useSerialStore = defineStore("serialStore", () => {
const serialDevices = ref([]);
const loadSerialDevices = async () => {
try {
const devices = await getSerialDevices();
// Check if devices are valid and have entries, otherwise use skelSerial
serialDevices.value =
devices && devices.length > 0 ? devices : skelSerial;
} catch (error) {
console.error("Failed to load serial devices:", error);
// Use skelSerial as fallback in case of error
serialDevices.value = skelSerial;
}
// Ensure the "-- ignore --" option is always available
if (!serialDevices.value.some((device) => device.port === "ignore")) {
serialDevices.value.push({ description: "-- ignore --", port: "ignore" });
}
};
return {
serialDevices,
loadSerialDevices,
};
});

View file

@ -29,23 +29,8 @@ nconf.file({ file: configPath });
//They should be an exact mirror (variable wise) of settingsStore.local //They should be an exact mirror (variable wise) of settingsStore.local
//Nothing else should be needed aslong as components are using v-bind //Nothing else should be needed aslong as components are using v-bind
// +++ // +++
nconf.defaults({
local: {
host: "127.0.0.1",
port: "5000",
spectrum: "waterfall",
wf_theme: 2,
update_channel: "alpha",
enable_sys_notification: false,
grid_layout: "[]",
grid_preset: "[]",
grid_enabled: true,
},
});
nconf.required(["local:host", "local:port"]); const defaultConfig = {
export const settingsStore = reactive({
local: { local: {
host: "127.0.0.1", host: "127.0.0.1",
port: "5000", port: "5000",
@ -69,17 +54,11 @@ export const settingsStore = reactive({
enable_protocol: false, enable_protocol: false,
}, },
MODEM: { MODEM: {
enable_fft: false,
enable_fsk: false,
enable_low_bandwidth_mode: false,
respond_to_cq: false, respond_to_cq: false,
rx_buffer_size: 0,
tuning_range_fmax: 0,
tuning_range_fmin: 0,
tx_delay: 0, tx_delay: 0,
beacon_interval: 0,
enable_hamc: false, enable_hamc: false,
enable_morse_identifier: false, enable_morse_identifier: false,
maximum_bandwidth: 3000,
}, },
RADIO: { RADIO: {
control: "disabled", control: "disabled",
@ -104,7 +83,7 @@ export const settingsStore = reactive({
STATION: { STATION: {
enable_explorer: false, enable_explorer: false,
enable_stats: false, enable_stats: false,
mycall: "", mycall: "DEFAULT",
myssid: 0, myssid: 0,
mygrid: "", mygrid: "",
ssid_list: [], ssid_list: [],
@ -113,8 +92,16 @@ export const settingsStore = reactive({
tci_ip: "127.0.0.1", tci_ip: "127.0.0.1",
tci_port: 0, tci_port: 0,
}, },
MESSAGES: {
enable_auto_repeat: false,
},
}, },
}); };
nconf.defaults(defaultConfig);
nconf.required(["local:host", "local:port"]);
export const settingsStore = reactive(defaultConfig);
//Save settings for GUI to config file //Save settings for GUI to config file
settingsStore.local = nconf.get("local"); settingsStore.local = nconf.get("local");
@ -128,7 +115,13 @@ export function onChange() {
export function getRemote() { export function getRemote() {
return getConfig().then((conf) => { return getConfig().then((conf) => {
settingsStore.remote = conf; if (typeof conf !== "undefined") {
settingsStore.remote = conf;
onChange();
} else {
console.warn("Received undefined configuration, using default!");
settingsStore.remote = defaultConfig.remote;
}
}); });
} }

View file

@ -53,6 +53,7 @@ export const useStateStore = defineStore("stateStore", () => {
var arq_speed_list_bpm = ref([]); var arq_speed_list_bpm = ref([]);
var arq_speed_list_snr = ref([]); var arq_speed_list_snr = ref([]);
/* TODO Those 3 can be removed I guess , DJ2LS*/
var arq_seconds_until_finish = ref(); var arq_seconds_until_finish = ref();
var arq_seconds_until_timeout = ref(); var arq_seconds_until_timeout = ref();
var arq_seconds_until_timeout_percent = ref(); var arq_seconds_until_timeout_percent = ref();

View file

@ -1,5 +1,16 @@
import re import re
def validate_remote_config(config):
if not config:
return
mygrid = config["STATION"]["mygrid"]
if len(mygrid) != 6:
raise ValueError(f"Gridsquare must be 6 characters!")
return True
def validate_freedata_callsign(callsign): def validate_freedata_callsign(callsign):
#regexp = "^[a-zA-Z]+\d+\w+-\d{1,2}$" #regexp = "^[a-zA-Z]+\d+\w+-\d{1,2}$"
regexp = "^[A-Za-z0-9]{1,7}-[0-9]{1,3}$" # still broken - we need to allow all ssids form 0 - 255 regexp = "^[A-Za-z0-9]{1,7}-[0-9]{1,3}$" # still broken - we need to allow all ssids form 0 - 255

View file

@ -11,6 +11,7 @@ class ARQ_SESSION_TYPES(Enum):
raw_lzma = 10 raw_lzma = 10
raw_gzip = 11 raw_gzip = 11
p2pmsg_lzma = 20 p2pmsg_lzma = 20
p2p_connection = 30
class ARQDataTypeHandler: class ARQDataTypeHandler:
def __init__(self, event_manager, state_manager): def __init__(self, event_manager, state_manager):
@ -43,6 +44,12 @@ class ARQDataTypeHandler:
'failed' : self.failed_p2pmsg_lzma, 'failed' : self.failed_p2pmsg_lzma,
'transmitted': self.transmitted_p2pmsg_lzma, 'transmitted': self.transmitted_p2pmsg_lzma,
}, },
ARQ_SESSION_TYPES.p2p_connection: {
'prepare': self.prepare_p2p_connection,
'handle': self.handle_p2p_connection,
'failed': self.failed_p2p_connection,
'transmitted': self.transmitted_p2p_connection,
},
} }
@staticmethod @staticmethod
@ -52,17 +59,23 @@ class ARQDataTypeHandler:
return session_type return session_type
return None return None
def dispatch(self, type_byte: int, data: bytearray): def dispatch(self, type_byte: int, data: bytearray, statistics: dict):
session_type = self.get_session_type_from_value(type_byte) session_type = self.get_session_type_from_value(type_byte)
self.state_manager.setARQ(False)
if session_type and session_type in self.handlers and 'handle' in self.handlers[session_type]: if session_type and session_type in self.handlers and 'handle' in self.handlers[session_type]:
return self.handlers[session_type]['handle'](data) return self.handlers[session_type]['handle'](data, statistics)
else: else:
self.log(f"Unknown handling endpoint for type: {type_byte}", isWarning=True) self.log(f"Unknown handling endpoint for type: {type_byte}", isWarning=True)
def failed(self, type_byte: int, data: bytearray): def failed(self, type_byte: int, data: bytearray, statistics: dict):
session_type = self.get_session_type_from_value(type_byte) session_type = self.get_session_type_from_value(type_byte)
self.state_manager.setARQ(False)
if session_type in self.handlers and 'failed' in self.handlers[session_type]: if session_type in self.handlers and 'failed' in self.handlers[session_type]:
return self.handlers[session_type]['failed'](data) return self.handlers[session_type]['failed'](data, statistics)
else: else:
self.log(f"Unknown handling endpoint: {session_type}", isWarning=True) self.log(f"Unknown handling endpoint: {session_type}", isWarning=True)
@ -72,10 +85,13 @@ class ARQDataTypeHandler:
else: else:
self.log(f"Unknown preparation endpoint: {session_type}", isWarning=True) self.log(f"Unknown preparation endpoint: {session_type}", isWarning=True)
def transmitted(self, type_byte: int, data: bytearray): def transmitted(self, type_byte: int, data: bytearray, statistics: dict):
session_type = self.get_session_type_from_value(type_byte) session_type = self.get_session_type_from_value(type_byte)
self.state_manager.setARQ(False)
if session_type in self.handlers and 'transmitted' in self.handlers[session_type]: if session_type in self.handlers and 'transmitted' in self.handlers[session_type]:
return self.handlers[session_type]['transmitted'](data) return self.handlers[session_type]['transmitted'](data, statistics)
else: else:
self.log(f"Unknown handling endpoint: {session_type}", isWarning=True) self.log(f"Unknown handling endpoint: {session_type}", isWarning=True)
@ -88,14 +104,14 @@ class ARQDataTypeHandler:
self.log(f"Preparing uncompressed data: {len(data)} Bytes") self.log(f"Preparing uncompressed data: {len(data)} Bytes")
return data return data
def handle_raw(self, data): def handle_raw(self, data, statistics):
self.log(f"Handling uncompressed data: {len(data)} Bytes") self.log(f"Handling uncompressed data: {len(data)} Bytes")
return data return data
def failed_raw(self, data): def failed_raw(self, data, statistics):
return return
def transmitted_raw(self, data): def transmitted_raw(self, data, statistics):
return data return data
def prepare_raw_lzma(self, data): def prepare_raw_lzma(self, data):
@ -103,15 +119,15 @@ class ARQDataTypeHandler:
self.log(f"Preparing LZMA compressed data: {len(data)} Bytes >>> {len(compressed_data)} Bytes") self.log(f"Preparing LZMA compressed data: {len(data)} Bytes >>> {len(compressed_data)} Bytes")
return compressed_data return compressed_data
def handle_raw_lzma(self, data): def handle_raw_lzma(self, data, statistics):
decompressed_data = lzma.decompress(data) decompressed_data = lzma.decompress(data)
self.log(f"Handling LZMA compressed data: {len(decompressed_data)} Bytes from {len(data)} Bytes") self.log(f"Handling LZMA compressed data: {len(decompressed_data)} Bytes from {len(data)} Bytes")
return decompressed_data return decompressed_data
def failed_raw_lzma(self, data): def failed_raw_lzma(self, data, statistics):
return return
def transmitted_raw_lzma(self, data): def transmitted_raw_lzma(self, data, statistics):
decompressed_data = lzma.decompress(data) decompressed_data = lzma.decompress(data)
return decompressed_data return decompressed_data
@ -120,15 +136,15 @@ class ARQDataTypeHandler:
self.log(f"Preparing GZIP compressed data: {len(data)} Bytes >>> {len(compressed_data)} Bytes") self.log(f"Preparing GZIP compressed data: {len(data)} Bytes >>> {len(compressed_data)} Bytes")
return compressed_data return compressed_data
def handle_raw_gzip(self, data): def handle_raw_gzip(self, data, statistics):
decompressed_data = gzip.decompress(data) decompressed_data = gzip.decompress(data)
self.log(f"Handling GZIP compressed data: {len(decompressed_data)} Bytes from {len(data)} Bytes") self.log(f"Handling GZIP compressed data: {len(decompressed_data)} Bytes from {len(data)} Bytes")
return decompressed_data return decompressed_data
def failed_raw_gzip(self, data): def failed_raw_gzip(self, data, statistics):
return return
def transmitted_raw_gzip(self, data): def transmitted_raw_gzip(self, data, statistics):
decompressed_data = gzip.decompress(data) decompressed_data = gzip.decompress(data)
return decompressed_data return decompressed_data
@ -137,19 +153,51 @@ class ARQDataTypeHandler:
self.log(f"Preparing LZMA compressed P2PMSG data: {len(data)} Bytes >>> {len(compressed_data)} Bytes") self.log(f"Preparing LZMA compressed P2PMSG data: {len(data)} Bytes >>> {len(compressed_data)} Bytes")
return compressed_data return compressed_data
def handle_p2pmsg_lzma(self, data): def handle_p2pmsg_lzma(self, data, statistics):
decompressed_data = lzma.decompress(data) decompressed_data = lzma.decompress(data)
self.log(f"Handling LZMA compressed P2PMSG data: {len(decompressed_data)} Bytes from {len(data)} Bytes") self.log(f"Handling LZMA compressed P2PMSG data: {len(decompressed_data)} Bytes from {len(data)} Bytes")
message_received(self.event_manager, self.state_manager, decompressed_data) message_received(self.event_manager, self.state_manager, decompressed_data, statistics)
return decompressed_data return decompressed_data
def failed_p2pmsg_lzma(self, data): def failed_p2pmsg_lzma(self, data, statistics):
decompressed_data = lzma.decompress(data) decompressed_data = lzma.decompress(data)
self.log(f"Handling failed LZMA compressed P2PMSG data: {len(decompressed_data)} Bytes from {len(data)} Bytes", isWarning=True) self.log(f"Handling failed LZMA compressed P2PMSG data: {len(decompressed_data)} Bytes from {len(data)} Bytes", isWarning=True)
message_failed(self.event_manager, self.state_manager, decompressed_data) message_failed(self.event_manager, self.state_manager, decompressed_data, statistics)
return decompressed_data return decompressed_data
def transmitted_p2pmsg_lzma(self, data): def transmitted_p2pmsg_lzma(self, data, statistics):
decompressed_data = lzma.decompress(data) decompressed_data = lzma.decompress(data)
message_transmitted(self.event_manager, self.state_manager, decompressed_data) message_transmitted(self.event_manager, self.state_manager, decompressed_data, statistics)
return decompressed_data return decompressed_data
def prepare_p2p_connection(self, data):
compressed_data = gzip.compress(data)
self.log(f"Preparing gzip compressed P2P_CONNECTION data: {len(data)} Bytes >>> {len(compressed_data)} Bytes")
print(self.state_manager.p2p_connection_sessions)
return compressed_data
def handle_p2p_connection(self, data, statistics):
decompressed_data = gzip.decompress(data)
self.log(f"Handling gzip compressed P2P_CONNECTION data: {len(decompressed_data)} Bytes from {len(data)} Bytes")
print(self.state_manager.p2p_connection_sessions)
print(decompressed_data)
print(self.state_manager.p2p_connection_sessions)
for session_id in self.state_manager.p2p_connection_sessions:
print(session_id)
self.state_manager.p2p_connection_sessions[session_id].received_arq(decompressed_data)
def failed_p2p_connection(self, data, statistics):
decompressed_data = gzip.decompress(data)
self.log(f"Handling failed gzip compressed P2P_CONNECTION data: {len(decompressed_data)} Bytes from {len(data)} Bytes", isWarning=True)
print(self.state_manager.p2p_connection_sessions)
return decompressed_data
def transmitted_p2p_connection(self, data, statistics):
decompressed_data = gzip.decompress(data)
print(decompressed_data)
print(self.state_manager.p2p_connection_sessions)
for session_id in self.state_manager.p2p_connection_sessions:
print(session_id)
self.state_manager.p2p_connection_sessions[session_id].transmitted_arq()

View file

@ -1,4 +1,5 @@
import queue, threading import datetime
import threading
import codec2 import codec2
import data_frame_factory import data_frame_factory
import structlog import structlog
@ -8,33 +9,36 @@ import time
from arq_data_type_handler import ARQDataTypeHandler from arq_data_type_handler import ARQDataTypeHandler
class ARQSession(): class ARQSession:
SPEED_LEVEL_DICT = { SPEED_LEVEL_DICT = {
0: { 0: {
'mode': codec2.FREEDV_MODE.datac4, 'mode': codec2.FREEDV_MODE.datac4,
'min_snr': -10, 'min_snr': -10,
'duration_per_frame': 5.17, 'duration_per_frame': 5.17,
'bandwidth': 250,
}, },
1: { 1: {
'mode': codec2.FREEDV_MODE.datac3, 'mode': codec2.FREEDV_MODE.datac3,
'min_snr': 0, 'min_snr': 0,
'duration_per_frame': 3.19, 'duration_per_frame': 3.19,
'bandwidth': 563,
}, },
2: { 2: {
'mode': codec2.FREEDV_MODE.datac1, 'mode': codec2.FREEDV_MODE.datac1,
'min_snr': 3, 'min_snr': 3,
'duration_per_frame': 4.18, 'duration_per_frame': 4.18,
'bandwidth': 1700,
}, },
} }
def __init__(self, config: dict, modem, dxcall: str): def __init__(self, config: dict, modem, dxcall: str, state_manager):
self.logger = structlog.get_logger(type(self).__name__) self.logger = structlog.get_logger(type(self).__name__)
self.config = config self.config = config
self.event_manager: EventManager = modem.event_manager self.event_manager: EventManager = modem.event_manager
self.states = modem.states #self.states = modem.states
self.states = state_manager
self.states.setARQ(True) self.states.setARQ(True)
self.snr = [] self.snr = []
@ -44,6 +48,8 @@ class ARQSession():
self.modem = modem self.modem = modem
self.speed_level = 0 self.speed_level = 0
self.previous_speed_level = 0
self.frames_per_burst = 1 self.frames_per_burst = 1
self.frame_factory = data_frame_factory.DataFrameFactory(self.config) self.frame_factory = data_frame_factory.DataFrameFactory(self.config)
@ -55,7 +61,12 @@ class ARQSession():
self.session_ended = 0 self.session_ended = 0
self.session_max_age = 500 self.session_max_age = 500
def log(self, message, isWarning = False): # histogram lists for storing statistics
self.snr_histogram = []
self.bpm_histogram = []
self.time_histogram = []
def log(self, message, isWarning=False):
msg = f"[{type(self).__name__}][id={self.id}][state={self.state}]: {message}" msg = f"[{type(self).__name__}][id={self.id}][state={self.state}]: {message}"
logger = self.logger.warn if isWarning else self.logger.info logger = self.logger.warn if isWarning else self.logger.info
logger(msg) logger(msg)
@ -84,48 +95,89 @@ class ARQSession():
) )
def set_details(self, snr, frequency_offset): def set_details(self, snr, frequency_offset):
self.snr.append(snr) self.snr = snr
self.frequency_offset = frequency_offset self.frequency_offset = frequency_offset
def on_frame_received(self, frame): def on_frame_received(self, frame):
self.event_frame_received.set() self.event_frame_received.set()
self.log(f"Received {frame['frame_type']}") self.log(f"Received {frame['frame_type']}")
frame_type = frame['frame_type_int'] frame_type = frame['frame_type_int']
if self.state in self.STATE_TRANSITION: if self.state in self.STATE_TRANSITION and frame_type in self.STATE_TRANSITION[self.state]:
if frame_type in self.STATE_TRANSITION[self.state]: action_name = self.STATE_TRANSITION[self.state][frame_type]
action_name = self.STATE_TRANSITION[self.state][frame_type] received_data, type_byte = getattr(self, action_name)(frame)
received_data, type_byte = getattr(self, action_name)(frame) if isinstance(received_data, bytearray) and isinstance(type_byte, int):
if isinstance(received_data, bytearray) and isinstance(type_byte, int): self.arq_data_type_handler.dispatch(type_byte, received_data, self.update_histograms(len(received_data), len(received_data)))
self.arq_data_type_handler.dispatch(type_byte, received_data) return
self.states.setARQ(False)
return
self.log(f"Ignoring unknown transition from state {self.state.name} with frame {frame['frame_type']}") self.log(f"Ignoring unknown transition from state {self.state.name} with frame {frame['frame_type']}")
def is_session_outdated(self): def is_session_outdated(self):
session_alivetime = time.time() - self.session_max_age session_alivetime = time.time() - self.session_max_age
if self.session_ended < session_alivetime and self.state.name in ['FAILED', 'ENDED', 'ABORTED']: return self.session_ended < session_alivetime and self.state.name in [
return True 'FAILED',
return False 'ENDED',
'ABORTED',
]
def calculate_session_duration(self): def calculate_session_duration(self):
if self.session_ended == 0:
return time.time() - self.session_started
return self.session_ended - self.session_started return self.session_ended - self.session_started
def calculate_session_statistics(self): def calculate_session_statistics(self, confirmed_bytes, total_bytes):
duration = self.calculate_session_duration() duration = self.calculate_session_duration()
total_bytes = self.total_length # total_bytes = self.total_length
# self.total_length # self.total_length
duration_in_minutes = duration / 60 # Convert duration from seconds to minutes duration_in_minutes = duration / 60 # Convert duration from seconds to minutes
# Calculate bytes per minute # Calculate bytes per minute
if duration_in_minutes > 0: if duration_in_minutes > 0:
bytes_per_minute = int(total_bytes / duration_in_minutes) bytes_per_minute = int(confirmed_bytes / duration_in_minutes)
else: else:
bytes_per_minute = 0 bytes_per_minute = 0
# Convert histograms lists to dictionaries
time_histogram_dict = dict(enumerate(self.time_histogram))
snr_histogram_dict = dict(enumerate(self.snr_histogram))
bpm_histogram_dict = dict(enumerate(self.bpm_histogram))
return { return {
'total_bytes': total_bytes, 'total_bytes': total_bytes,
'duration': duration, 'duration': duration,
'bytes_per_minute': bytes_per_minute 'bytes_per_minute': bytes_per_minute,
} 'time_histogram': time_histogram_dict,
'snr_histogram': snr_histogram_dict,
'bpm_histogram': bpm_histogram_dict,
}
def update_histograms(self, confirmed_bytes, total_bytes):
stats = self.calculate_session_statistics(confirmed_bytes, total_bytes)
self.snr_histogram.append(self.snr)
self.bpm_histogram.append(stats['bytes_per_minute'])
self.time_histogram.append(datetime.datetime.now().isoformat())
# Limit the size of each histogram to the last 20 entries
self.snr_histogram = self.snr_histogram[-20:]
self.bpm_histogram = self.bpm_histogram[-20:]
self.time_histogram = self.time_histogram[-20:]
return stats
def get_appropriate_speed_level(self, snr, maximum_bandwidth=None):
if maximum_bandwidth is None:
maximum_bandwidth = self.config['MODEM']['maximum_bandwidth']
# Adjust maximum_bandwidth based on special conditions or invalid configurations
if maximum_bandwidth == 0:
# Use the maximum available bandwidth from the speed level dictionary
maximum_bandwidth = max(details['bandwidth'] for details in self.SPEED_LEVEL_DICT.values())
# Initialize appropriate_speed_level to the lowest level that meets the minimum criteria
appropriate_speed_level = min(self.SPEED_LEVEL_DICT.keys())
for level, details in self.SPEED_LEVEL_DICT.items():
if snr >= details['min_snr'] and details['bandwidth'] <= maximum_bandwidth and level > appropriate_speed_level:
appropriate_speed_level = level
return appropriate_speed_level

View file

@ -18,7 +18,7 @@ class IRS_State(Enum):
class ARQSessionIRS(arq_session.ARQSession): class ARQSessionIRS(arq_session.ARQSession):
TIMEOUT_CONNECT = 55 #14.2 TIMEOUT_CONNECT = 55 #14.2
TIMEOUT_DATA = 60 TIMEOUT_DATA = 120
STATE_TRANSITION = { STATE_TRANSITION = {
IRS_State.NEW: { IRS_State.NEW: {
@ -59,8 +59,8 @@ class ARQSessionIRS(arq_session.ARQSession):
}, },
} }
def __init__(self, config: dict, modem, dxcall: str, session_id: int): def __init__(self, config: dict, modem, dxcall: str, session_id: int, state_manager):
super().__init__(config, modem, dxcall) super().__init__(config, modem, dxcall, state_manager)
self.id = session_id self.id = session_id
self.dxcall = dxcall self.dxcall = dxcall
@ -76,19 +76,15 @@ class ARQSessionIRS(arq_session.ARQSession):
self.received_bytes = 0 self.received_bytes = 0
self.received_crc = None self.received_crc = None
self.transmitted_acks = 0 self.maximum_bandwidth = 0
self.abort = False self.abort = False
def set_decode_mode(self):
self.modem.demodulator.set_decode_mode(self.get_mode_by_speed_level(self.speed_level))
def all_data_received(self): def all_data_received(self):
return self.total_length == self.received_bytes return self.total_length == self.received_bytes
def final_crc_matches(self) -> bool: def final_crc_matches(self) -> bool:
match = self.total_crc == helpers.get_crc_32(bytes(self.received_data)).hex() return self.total_crc == helpers.get_crc_32(bytes(self.received_data)).hex()
return match
def transmit_and_wait(self, frame, timeout, mode): def transmit_and_wait(self, frame, timeout, mode):
self.event_frame_received.clear() self.event_frame_received.clear()
@ -96,9 +92,7 @@ class ARQSessionIRS(arq_session.ARQSession):
self.log(f"Waiting {timeout} seconds...") self.log(f"Waiting {timeout} seconds...")
if not self.event_frame_received.wait(timeout): if not self.event_frame_received.wait(timeout):
self.log("Timeout waiting for ISS. Session failed.") self.log("Timeout waiting for ISS. Session failed.")
self.session_ended = time.time() self.transmission_failed()
self.set_state(IRS_State.FAILED)
self.event_manager.send_arq_session_finished(False, self.id, self.dxcall, False, self.state.name, statistics=self.calculate_session_statistics())
def launch_transmit_and_wait(self, frame, timeout, mode): def launch_transmit_and_wait(self, frame, timeout, mode):
thread_wait = threading.Thread(target = self.transmit_and_wait, thread_wait = threading.Thread(target = self.transmit_and_wait,
@ -106,13 +100,19 @@ class ARQSessionIRS(arq_session.ARQSession):
thread_wait.start() thread_wait.start()
def send_open_ack(self, open_frame): def send_open_ack(self, open_frame):
self.maximum_bandwidth = open_frame['maximum_bandwidth']
# check for maximum bandwidth. If ISS bandwidth is higher than own, then use own
if open_frame['maximum_bandwidth'] > self.config['MODEM']['maximum_bandwidth']:
self.maximum_bandwidth = self.config['MODEM']['maximum_bandwidth']
self.event_manager.send_arq_session_new( self.event_manager.send_arq_session_new(
False, self.id, self.dxcall, 0, self.state.name) False, self.id, self.dxcall, 0, self.state.name)
ack_frame = self.frame_factory.build_arq_session_open_ack( ack_frame = self.frame_factory.build_arq_session_open_ack(
self.id, self.id,
self.dxcall, self.dxcall,
self.version, self.version,
self.snr[0], flag_abort=self.abort) self.snr, flag_abort=self.abort)
self.launch_transmit_and_wait(ack_frame, self.TIMEOUT_CONNECT, mode=FREEDV_MODE.signalling) self.launch_transmit_and_wait(ack_frame, self.TIMEOUT_CONNECT, mode=FREEDV_MODE.signalling)
if not self.abort: if not self.abort:
self.set_state(IRS_State.OPEN_ACK_SENT) self.set_state(IRS_State.OPEN_ACK_SENT)
@ -126,14 +126,13 @@ class ARQSessionIRS(arq_session.ARQSession):
self.dx_snr.append(info_frame['snr']) self.dx_snr.append(info_frame['snr'])
self.type_byte = info_frame['type'] self.type_byte = info_frame['type']
self.calibrate_speed_settings()
self.log(f"New transfer of {self.total_length} bytes") self.log(f"New transfer of {self.total_length} bytes")
self.event_manager.send_arq_session_new(False, self.id, self.dxcall, self.total_length, self.state.name) self.event_manager.send_arq_session_new(False, self.id, self.dxcall, self.total_length, self.state.name)
self.calibrate_speed_settings()
self.set_decode_mode()
info_ack = self.frame_factory.build_arq_session_info_ack( info_ack = self.frame_factory.build_arq_session_info_ack(
self.id, self.total_crc, self.snr[0], self.id, self.total_crc, self.snr,
self.speed_level, self.frames_per_burst, flag_abort=self.abort) self.speed_level, self.frames_per_burst, flag_abort=self.abort)
self.launch_transmit_and_wait(info_ack, self.TIMEOUT_CONNECT, mode=FREEDV_MODE.signalling) self.launch_transmit_and_wait(info_ack, self.TIMEOUT_CONNECT, mode=FREEDV_MODE.signalling)
if not self.abort: if not self.abort:
@ -159,23 +158,26 @@ class ARQSessionIRS(arq_session.ARQSession):
self.received_bytes += len(data_part) self.received_bytes += len(data_part)
self.log(f"Received {self.received_bytes}/{self.total_length} bytes") self.log(f"Received {self.received_bytes}/{self.total_length} bytes")
self.event_manager.send_arq_session_progress( self.event_manager.send_arq_session_progress(
False, self.id, self.dxcall, self.received_bytes, self.total_length, self.state.name) False, self.id, self.dxcall, self.received_bytes, self.total_length, self.state.name, self.calculate_session_statistics(self.received_bytes, self.total_length))
return True return True
def receive_data(self, burst_frame): def receive_data(self, burst_frame):
self.process_incoming_data(burst_frame) self.process_incoming_data(burst_frame)
self.calibrate_speed_settings() # update statistics
self.update_histograms(self.received_bytes, self.total_length)
if not self.all_data_received(): if not self.all_data_received():
self.calibrate_speed_settings(burst_frame=burst_frame)
ack = self.frame_factory.build_arq_burst_ack( ack = self.frame_factory.build_arq_burst_ack(
self.id, self.received_bytes, self.id,
self.speed_level, self.frames_per_burst, self.snr[0], flag_abort=self.abort) self.received_bytes,
self.speed_level,
self.frames_per_burst,
self.snr,
flag_abort=self.abort
)
self.set_decode_mode()
# increase ack counter
# self.transmitted_acks += 1
self.set_state(IRS_State.BURST_REPLY_SENT) self.set_state(IRS_State.BURST_REPLY_SENT)
self.launch_transmit_and_wait(ack, self.TIMEOUT_DATA, mode=FREEDV_MODE.signalling) self.launch_transmit_and_wait(ack, self.TIMEOUT_DATA, mode=FREEDV_MODE.signalling)
return None, None return None, None
@ -186,7 +188,7 @@ class ARQSessionIRS(arq_session.ARQSession):
self.received_bytes, self.received_bytes,
self.speed_level, self.speed_level,
self.frames_per_burst, self.frames_per_burst,
self.snr[0], self.snr,
flag_final=True, flag_final=True,
flag_checksum=True) flag_checksum=True)
self.transmit_frame(ack, mode=FREEDV_MODE.signalling) self.transmit_frame(ack, mode=FREEDV_MODE.signalling)
@ -194,7 +196,7 @@ class ARQSessionIRS(arq_session.ARQSession):
self.session_ended = time.time() self.session_ended = time.time()
self.set_state(IRS_State.ENDED) self.set_state(IRS_State.ENDED)
self.event_manager.send_arq_session_finished( self.event_manager.send_arq_session_finished(
False, self.id, self.dxcall, True, self.state.name, data=self.received_data, statistics=self.calculate_session_statistics()) False, self.id, self.dxcall, True, self.state.name, data=self.received_data, statistics=self.calculate_session_statistics(self.received_bytes, self.total_length))
return self.received_data, self.type_byte return self.received_data, self.type_byte
else: else:
@ -203,37 +205,72 @@ class ARQSessionIRS(arq_session.ARQSession):
self.received_bytes, self.received_bytes,
self.speed_level, self.speed_level,
self.frames_per_burst, self.frames_per_burst,
self.snr[0], self.snr,
flag_final=True, flag_final=True,
flag_checksum=False) flag_checksum=False)
self.transmit_frame(ack, mode=FREEDV_MODE.signalling) self.transmit_frame(ack, mode=FREEDV_MODE.signalling)
self.log("CRC fail at the end of transmission!") self.log("CRC fail at the end of transmission!")
self.session_ended = time.time() return self.transmission_failed()
self.set_state(IRS_State.FAILED)
self.event_manager.send_arq_session_finished(
False, self.id, self.dxcall, False, self.state.name, statistics=self.calculate_session_statistics())
return False, False
def calibrate_speed_settings(self): def calibrate_speed_settings(self, burst_frame=None):
self.speed_level = 0 # for now stay at lowest speed level if burst_frame:
return received_speed_level = burst_frame['speed_level']
# if we have two ACKS, then consider increasing speed level else:
if self.transmitted_acks >= 2: received_speed_level = 0
self.transmitted_acks = 0
new_speed_level = min(self.speed_level + 1, len(self.SPEED_LEVEL_DICT) - 1)
# check first if the next mode supports the actual snr latest_snr = self.snr if self.snr else -10
if self.snr[0] >= self.SPEED_LEVEL_DICT[new_speed_level]["min_snr"]: appropriate_speed_level = self.get_appropriate_speed_level(latest_snr, self.maximum_bandwidth)
self.speed_level = new_speed_level modes_to_decode = {}
# Log the latest SNR, current, appropriate speed levels, and the previous speed level
self.log(
f"Latest SNR: {latest_snr}, Current Speed Level: {self.speed_level}, Appropriate Speed Level: {appropriate_speed_level}, Previous Speed Level: {self.previous_speed_level}",
isWarning=True)
# Adjust the speed level by one step towards the appropriate level, if needed
if appropriate_speed_level > self.speed_level and self.speed_level < len(self.SPEED_LEVEL_DICT) - 1:
# we need to ensure, the received data is equal to our speed level before changing it
if received_speed_level == self.speed_level:
self.speed_level += 1
elif appropriate_speed_level < self.speed_level and self.speed_level > 0:
# we need to ensure, the received data is equal to our speed level before changing it
if received_speed_level == self.speed_level:
self.speed_level -= 1
# Always decode the current mode
current_mode = self.get_mode_by_speed_level(self.speed_level).value
modes_to_decode[current_mode] = True
# Decode the previous speed level mode
if self.previous_speed_level != self.speed_level:
previous_mode = self.get_mode_by_speed_level(self.previous_speed_level).value
modes_to_decode[previous_mode] = True
self.previous_speed_level = self.speed_level # Update the previous speed level
self.log(f"Modes to Decode: {list(modes_to_decode.keys())}", isWarning=True)
# Apply the new decode mode based on the updated and previous speed levels
self.modem.demodulator.set_decode_mode(modes_to_decode)
return self.speed_level
def abort_transmission(self): def abort_transmission(self):
self.log(f"Aborting transmission... setting abort flag") self.log("Aborting transmission... setting abort flag")
self.abort = True self.abort = True
def send_stop_ack(self, stop_frame): def send_stop_ack(self, stop_frame):
stop_ack = self.frame_factory.build_arq_stop_ack(self.id) stop_ack = self.frame_factory.build_arq_stop_ack(self.id)
self.launch_transmit_and_wait(stop_ack, self.TIMEOUT_CONNECT, mode=FREEDV_MODE.signalling) self.launch_transmit_and_wait(stop_ack, self.TIMEOUT_CONNECT, mode=FREEDV_MODE.signalling)
self.set_state(IRS_State.ABORTED) self.set_state(IRS_State.ABORTED)
self.states.setARQ(False)
self.event_manager.send_arq_session_finished( self.event_manager.send_arq_session_finished(
False, self.id, self.dxcall, False, self.state.name, statistics=self.calculate_session_statistics()) False, self.id, self.dxcall, False, self.state.name, statistics=self.calculate_session_statistics(self.received_bytes, self.total_length))
return None, None return None, None
def transmission_failed(self, irs_frame=None):
# final function for failed transmissions
self.session_ended = time.time()
self.set_state(IRS_State.FAILED)
self.log("Transmission failed!")
self.event_manager.send_arq_session_finished(True, self.id, self.dxcall,False, self.state.name, statistics=self.calculate_session_statistics(self.received_bytes, self.total_length))
self.states.setARQ(False)
return None, None

View file

@ -1,6 +1,5 @@
import threading import threading
import data_frame_factory import data_frame_factory
import queue
import random import random
from codec2 import FREEDV_MODE from codec2 import FREEDV_MODE
from modem_frametypes import FRAME_TYPE from modem_frametypes import FRAME_TYPE
@ -54,7 +53,7 @@ class ARQSessionISS(arq_session.ARQSession):
} }
def __init__(self, config: dict, modem, dxcall: str, state_manager, data: bytearray, type_byte: bytes): def __init__(self, config: dict, modem, dxcall: str, state_manager, data: bytearray, type_byte: bytes):
super().__init__(config, modem, dxcall) super().__init__(config, modem, dxcall, state_manager)
self.state_manager = state_manager self.state_manager = state_manager
self.data = data self.data = data
self.total_length = len(data) self.total_length = len(data)
@ -76,8 +75,7 @@ class ARQSessionISS(arq_session.ARQSession):
if len(self.state_manager.arq_iss_sessions) >= 255: if len(self.state_manager.arq_iss_sessions) >= 255:
return False return False
def transmit_wait_and_retry(self, frame_or_burst, timeout, retries, mode, isARQBurst=False, ):
def transmit_wait_and_retry(self, frame_or_burst, timeout, retries, mode):
while retries > 0: while retries > 0:
self.event_frame_received = threading.Event() self.event_frame_received = threading.Event()
if isinstance(frame_or_burst, list): burst = frame_or_burst if isinstance(frame_or_burst, list): burst = frame_or_burst
@ -90,26 +88,50 @@ class ARQSessionISS(arq_session.ARQSession):
return return
self.log("Timeout!") self.log("Timeout!")
retries = retries - 1 retries = retries - 1
# TODO TEMPORARY TEST FOR SENDING IN LOWER SPEED LEVEL IF WE HAVE TWO FAILED TRANSMISSIONS!!!
if retries == 8 and isARQBurst and self.speed_level > 0:
self.log("SENDING IN FALLBACK SPEED LEVEL", isWarning=True)
self.speed_level = 0
self.send_data({'flag':{'ABORT': False, 'FINAL': False}, 'speed_level': self.speed_level})
return
self.set_state(ISS_State.FAILED) self.set_state(ISS_State.FAILED)
self.transmission_failed() self.transmission_failed()
def launch_twr(self, frame_or_burst, timeout, retries, mode): def launch_twr(self, frame_or_burst, timeout, retries, mode, isARQBurst=False):
twr = threading.Thread(target = self.transmit_wait_and_retry, args=[frame_or_burst, timeout, retries, mode], daemon=True) twr = threading.Thread(target = self.transmit_wait_and_retry, args=[frame_or_burst, timeout, retries, mode, isARQBurst], daemon=True)
twr.start() twr.start()
def start(self): def start(self):
maximum_bandwidth = self.config['MODEM']['maximum_bandwidth']
self.event_manager.send_arq_session_new( self.event_manager.send_arq_session_new(
True, self.id, self.dxcall, self.total_length, self.state.name) True, self.id, self.dxcall, self.total_length, self.state.name)
session_open_frame = self.frame_factory.build_arq_session_open(self.dxcall, self.id) session_open_frame = self.frame_factory.build_arq_session_open(self.dxcall, self.id, maximum_bandwidth)
self.launch_twr(session_open_frame, self.TIMEOUT_CONNECT_ACK, self.RETRIES_CONNECT, mode=FREEDV_MODE.signalling) self.launch_twr(session_open_frame, self.TIMEOUT_CONNECT_ACK, self.RETRIES_CONNECT, mode=FREEDV_MODE.signalling)
self.set_state(ISS_State.OPEN_SENT) self.set_state(ISS_State.OPEN_SENT)
def set_speed_and_frames_per_burst(self, frame): def update_speed_level(self, frame):
self.speed_level = frame['speed_level'] self.log("---------------------------------------------------------", isWarning=True)
self.log(f"Speed level set to {self.speed_level}")
self.frames_per_burst = frame['frames_per_burst'] # Log the received frame for debugging
self.log(f"Frames per burst set to {self.frames_per_burst}") self.log(f"Received frame: {frame}", isWarning=True)
# Extract the speed_level directly from the frame
if 'speed_level' in frame:
new_speed_level = frame['speed_level']
# Ensure the new speed level is within the allowable range
if 0 <= new_speed_level < len(self.SPEED_LEVEL_DICT):
# Log the speed level change if it's different from the current speed level
if new_speed_level != self.speed_level:
self.log(f"Changing speed level from {self.speed_level} to {new_speed_level}", isWarning=True)
self.speed_level = new_speed_level # Update the current speed level
else:
self.log("Received speed level is the same as the current speed level.", isWarning=True)
else:
self.log(f"Received speed level {new_speed_level} is out of allowable range.", isWarning=True)
else:
self.log("No speed level specified in the received frame.", isWarning=True)
def send_info(self, irs_frame): def send_info(self, irs_frame):
# check if we received an abort flag # check if we received an abort flag
@ -119,7 +141,7 @@ class ARQSessionISS(arq_session.ARQSession):
info_frame = self.frame_factory.build_arq_session_info(self.id, self.total_length, info_frame = self.frame_factory.build_arq_session_info(self.id, self.total_length,
helpers.get_crc_32(self.data), helpers.get_crc_32(self.data),
self.snr[0], self.type_byte) self.snr, self.type_byte)
self.launch_twr(info_frame, self.TIMEOUT_CONNECT_ACK, self.RETRIES_CONNECT, mode=FREEDV_MODE.signalling) self.launch_twr(info_frame, self.TIMEOUT_CONNECT_ACK, self.RETRIES_CONNECT, mode=FREEDV_MODE.signalling)
self.set_state(ISS_State.INFO_SENT) self.set_state(ISS_State.INFO_SENT)
@ -127,14 +149,15 @@ class ARQSessionISS(arq_session.ARQSession):
return None, None return None, None
def send_data(self, irs_frame): def send_data(self, irs_frame):
# update statistics
self.update_histograms(self.confirmed_bytes, self.total_length)
self.set_speed_and_frames_per_burst(irs_frame) self.update_speed_level(irs_frame)
if 'offset' in irs_frame: if 'offset' in irs_frame:
self.confirmed_bytes = irs_frame['offset'] self.confirmed_bytes = irs_frame['offset']
self.log(f"IRS confirmed {self.confirmed_bytes}/{self.total_length} bytes") self.log(f"IRS confirmed {self.confirmed_bytes}/{self.total_length} bytes")
self.event_manager.send_arq_session_progress( self.event_manager.send_arq_session_progress(
True, self.id, self.dxcall, self.confirmed_bytes, self.total_length, self.state.name) True, self.id, self.dxcall, self.confirmed_bytes, self.total_length, self.state.name, statistics=self.calculate_session_statistics(self.confirmed_bytes, self.total_length))
# check if we received an abort flag # check if we received an abort flag
if irs_frame["flag"]["ABORT"]: if irs_frame["flag"]["ABORT"]:
@ -151,14 +174,14 @@ class ARQSessionISS(arq_session.ARQSession):
payload_size = self.get_data_payload_size() payload_size = self.get_data_payload_size()
burst = [] burst = []
for f in range(0, self.frames_per_burst): for _ in range(0, self.frames_per_burst):
offset = self.confirmed_bytes offset = self.confirmed_bytes
payload = self.data[offset : offset + payload_size] payload = self.data[offset : offset + payload_size]
data_frame = self.frame_factory.build_arq_burst_frame( data_frame = self.frame_factory.build_arq_burst_frame(
self.SPEED_LEVEL_DICT[self.speed_level]["mode"], self.SPEED_LEVEL_DICT[self.speed_level]["mode"],
self.id, self.confirmed_bytes, payload) self.id, self.confirmed_bytes, payload, self.speed_level)
burst.append(data_frame) burst.append(data_frame)
self.launch_twr(burst, self.TIMEOUT_TRANSFER, self.RETRIES_CONNECT, mode='auto') self.launch_twr(burst, self.TIMEOUT_TRANSFER, self.RETRIES_CONNECT, mode='auto', isARQBurst=True)
self.set_state(ISS_State.BURST_SENT) self.set_state(ISS_State.BURST_SENT)
return None, None return None, None
@ -167,31 +190,34 @@ class ARQSessionISS(arq_session.ARQSession):
self.session_ended = time.time() self.session_ended = time.time()
self.set_state(ISS_State.ENDED) self.set_state(ISS_State.ENDED)
self.log(f"All data transfered! flag_final={irs_frame['flag']['FINAL']}, flag_checksum={irs_frame['flag']['CHECKSUM']}") self.log(f"All data transfered! flag_final={irs_frame['flag']['FINAL']}, flag_checksum={irs_frame['flag']['CHECKSUM']}")
self.event_manager.send_arq_session_finished(True, self.id, self.dxcall,True, self.state.name, statistics=self.calculate_session_statistics()) self.event_manager.send_arq_session_finished(True, self.id, self.dxcall,True, self.state.name, statistics=self.calculate_session_statistics(self.confirmed_bytes, self.total_length))
print(self.state_manager.p2p_connection_sessions)
print(self.arq_data_type_handler.state_manager.p2p_connection_sessions)
self.arq_data_type_handler.transmitted(self.type_byte, self.data, self.calculate_session_statistics(self.confirmed_bytes, self.total_length))
self.state_manager.remove_arq_iss_session(self.id) self.state_manager.remove_arq_iss_session(self.id)
self.states.setARQ(False) self.states.setARQ(False)
self.arq_data_type_handler.transmitted(self.type_byte, self.data)
return None, None return None, None
def transmission_failed(self, irs_frame=None): def transmission_failed(self, irs_frame=None):
# final function for failed transmissions # final function for failed transmissions
self.session_ended = time.time() self.session_ended = time.time()
self.set_state(ISS_State.FAILED) self.set_state(ISS_State.FAILED)
self.log(f"Transmission failed!") self.log("Transmission failed!")
self.event_manager.send_arq_session_finished(True, self.id, self.dxcall,False, self.state.name, statistics=self.calculate_session_statistics()) self.event_manager.send_arq_session_finished(True, self.id, self.dxcall,False, self.state.name, statistics=self.calculate_session_statistics(self.confirmed_bytes, self.total_length))
self.states.setARQ(False) self.states.setARQ(False)
self.arq_data_type_handler.failed(self.type_byte, self.data) self.arq_data_type_handler.failed(self.type_byte, self.data, self.calculate_session_statistics(self.confirmed_bytes, self.total_length))
return None, None return None, None
def abort_transmission(self, irs_frame=None): def abort_transmission(self, irs_frame=None):
# function for starting the abort sequence # function for starting the abort sequence
self.log(f"aborting transmission...") self.log("aborting transmission...")
self.set_state(ISS_State.ABORTING) self.set_state(ISS_State.ABORTING)
self.event_manager.send_arq_session_finished( self.event_manager.send_arq_session_finished(
True, self.id, self.dxcall, False, self.state.name, statistics=self.calculate_session_statistics()) True, self.id, self.dxcall, False, self.state.name, statistics=self.calculate_session_statistics(self.confirmed_bytes, self.total_length))
# break actual retries # break actual retries
self.event_frame_received.set() self.event_frame_received.set()
@ -211,7 +237,7 @@ class ARQSessionISS(arq_session.ARQSession):
self.event_frame_received.set() self.event_frame_received.set()
self.event_manager.send_arq_session_finished( self.event_manager.send_arq_session_finished(
True, self.id, self.dxcall, False, self.state.name, statistics=self.calculate_session_statistics()) True, self.id, self.dxcall, False, self.state.name, statistics=self.calculate_session_statistics(self.confirmed_bytes, self.total_length))
self.state_manager.remove_arq_iss_session(self.id) self.state_manager.remove_arq_iss_session(self.id)
self.states.setARQ(False) self.states.setARQ(False)
return None, None return None, None

View file

@ -1,16 +1,12 @@
""" """
Gather information about audio devices. Gather information about audio devices.
""" """
import atexit
import multiprocessing import multiprocessing
import crcengine import crcengine
import sounddevice as sd import sounddevice as sd
import structlog import structlog
import numpy as np import numpy as np
import queue import queue
import threading
atexit.register(sd._terminate)
log = structlog.get_logger("audio") log = structlog.get_logger("audio")

View file

@ -74,23 +74,20 @@ def freedv_get_mode_name_by_value(mode: int) -> str:
return FREEDV_MODE(mode).name return FREEDV_MODE(mode).name
# Check if we are running in a pyinstaller environment # Get the directory of the current script file
#if hasattr(sys, "_MEIPASS"): script_dir = os.path.dirname(os.path.abspath(__file__))
# sys.path.append(getattr(sys, "_MEIPASS")) sys.path.append(script_dir)
#else: # Use script_dir to construct the paths for file search
sys.path.append(os.path.abspath("."))
#log.info("[C2 ] Searching for libcodec2...")
if sys.platform == "linux": if sys.platform == "linux":
files = glob.glob(r"**/*libcodec2*", recursive=True) files = glob.glob(os.path.join(script_dir, "**/*libcodec2*"), recursive=True)
files.append("libcodec2.so") files.append(os.path.join(script_dir, "libcodec2.so"))
elif sys.platform == "darwin": elif sys.platform == "darwin":
if hasattr(sys, "_MEIPASS"): if hasattr(sys, "_MEIPASS"):
files = glob.glob(getattr(sys, "_MEIPASS") + '/**/*libcodec2*', recursive=True) files = glob.glob(os.path.join(getattr(sys, "_MEIPASS"), '**/*libcodec2*'), recursive=True)
else: else:
files = glob.glob(r"**/*libcodec2*.dylib", recursive=True) files = glob.glob(os.path.join(script_dir, "**/*libcodec2*.dylib"), recursive=True)
elif sys.platform in ["win32", "win64"]: elif sys.platform in ["win32", "win64"]:
files = glob.glob(r"**\*libcodec2*.dll", recursive=True) files = glob.glob(os.path.join(script_dir, "**\\*libcodec2*.dll"), recursive=True)
else: else:
files = [] files = []

View file

@ -8,7 +8,7 @@ from arq_data_type_handler import ARQDataTypeHandler
class TxCommand(): class TxCommand():
def __init__(self, config: dict, state_manager: StateManager, event_manager, apiParams:dict = {}): def __init__(self, config: dict, state_manager: StateManager, event_manager, apiParams:dict = {}, socket_command_handler=None):
self.config = config self.config = config
self.logger = structlog.get_logger(type(self).__name__) self.logger = structlog.get_logger(type(self).__name__)
self.state_manager = state_manager self.state_manager = state_manager
@ -16,6 +16,7 @@ class TxCommand():
self.set_params_from_api(apiParams) self.set_params_from_api(apiParams)
self.frame_factory = DataFrameFactory(config) self.frame_factory = DataFrameFactory(config)
self.arq_data_type_handler = ARQDataTypeHandler(event_manager, state_manager) self.arq_data_type_handler = ARQDataTypeHandler(event_manager, state_manager)
self.socket_command_handler = socket_command_handler
def log(self, message, isWarning = False): def log(self, message, isWarning = False):
msg = f"[{type(self).__name__}]: {message}" msg = f"[{type(self).__name__}]: {message}"

View file

@ -22,14 +22,18 @@ class ARQRawCommand(TxCommand):
self.data = base64.b64decode(apiParams['data']) self.data = base64.b64decode(apiParams['data'])
def run(self, event_queue: Queue, modem): def run(self, event_queue: Queue, modem):
self.emit_event(event_queue) try:
self.logger.info(self.log_message()) self.emit_event(event_queue)
self.logger.info(self.log_message())
prepared_data, type_byte = self.arq_data_type_handler.prepare(self.data, self.type) prepared_data, type_byte = self.arq_data_type_handler.prepare(self.data, self.type)
iss = ARQSessionISS(self.config, modem, self.dxcall, self.state_manager, prepared_data, type_byte)
if iss.id:
self.state_manager.register_arq_iss_session(iss)
iss.start()
return iss
except Exception as e:
self.log(f"Error starting ARQ session: {e}", isWarning=True)
iss = ARQSessionISS(self.config, modem, self.dxcall, self.state_manager, prepared_data, type_byte)
if iss.id:
self.state_manager.register_arq_iss_session(iss)
iss.start()
return iss
return False return False

View file

@ -15,7 +15,7 @@ class SendMessageCommand(TxCommand):
def set_params_from_api(self, apiParams): def set_params_from_api(self, apiParams):
origin = f"{self.config['STATION']['mycall']}-{self.config['STATION']['myssid']}" origin = f"{self.config['STATION']['mycall']}-{self.config['STATION']['myssid']}"
self.message = MessageP2P.from_api_params(origin, apiParams) self.message = MessageP2P.from_api_params(origin, apiParams)
DatabaseManagerMessages(self.event_manager).add_message(self.message.to_dict(), direction='transmit', status='queued') DatabaseManagerMessages(self.event_manager).add_message(self.message.to_dict(), statistics={}, direction='transmit', status='queued')
def transmit(self, modem): def transmit(self, modem):
@ -23,28 +23,35 @@ class SendMessageCommand(TxCommand):
self.log("Modem busy, waiting until ready...") self.log("Modem busy, waiting until ready...")
return return
if not modem:
self.log("Modem not running...", isWarning=True)
return
first_queued_message = DatabaseManagerMessages(self.event_manager).get_first_queued_message() first_queued_message = DatabaseManagerMessages(self.event_manager).get_first_queued_message()
if not first_queued_message: if not first_queued_message:
self.log("No queued message in database.") self.log("No queued message in database.")
return return
try:
self.log(f"Queued message found: {first_queued_message['id']}")
DatabaseManagerMessages(self.event_manager).update_message(first_queued_message["id"], update_data={'status': 'transmitting'})
message_dict = DatabaseManagerMessages(self.event_manager).get_message_by_id(first_queued_message["id"])
message = MessageP2P.from_api_params(message_dict['origin'], message_dict)
self.log(f"Queued message found: {first_queued_message['id']}") # Convert JSON string to bytes (using UTF-8 encoding)
DatabaseManagerMessages(self.event_manager).update_message(first_queued_message["id"], update_data={'status': 'transmitting'}) payload = message.to_payload().encode('utf-8')
message_dict = DatabaseManagerMessages(self.event_manager).get_message_by_id(first_queued_message["id"]) json_bytearray = bytearray(payload)
message = MessageP2P.from_api_params(message_dict['origin'], message_dict) data, data_type = self.arq_data_type_handler.prepare(json_bytearray, ARQ_SESSION_TYPES.p2pmsg_lzma)
# Convert JSON string to bytes (using UTF-8 encoding) iss = ARQSessionISS(self.config,
payload = message.to_payload().encode('utf-8') modem,
json_bytearray = bytearray(payload) self.message.destination,
data, data_type = self.arq_data_type_handler.prepare(json_bytearray, ARQ_SESSION_TYPES.p2pmsg_lzma) self.state_manager,
data,
data_type
)
iss = ARQSessionISS(self.config, self.state_manager.register_arq_iss_session(iss)
modem, iss.start()
self.message.destination, except Exception as e:
self.state_manager, self.log(f"Error starting ARQ session: {e}", isWarning=True)
data,
data_type
)
self.state_manager.register_arq_iss_session(iss)
iss.start()

View file

@ -0,0 +1,37 @@
import queue
from command import TxCommand
import api_validations
import base64
from queue import Queue
from p2p_connection import P2PConnection
class P2PConnectionCommand(TxCommand):
def set_params_from_api(self, apiParams):
self.origin = apiParams['origin']
if not api_validations.validate_freedata_callsign(self.origin):
self.origin = f"{self.origin}-0"
self.destination = apiParams['destination']
if not api_validations.validate_freedata_callsign(self.destination):
self.destination = f"{self.destination}-0"
def connect(self, event_queue: Queue, modem):
pass
def run(self, event_queue: Queue, modem):
try:
self.emit_event(event_queue)
session = P2PConnection(self.config, modem, self.origin, self.destination, self.state_manager, self.event_manager, self.socket_command_handler)
print(session)
if session.session_id:
self.state_manager.register_p2p_connection_session(session)
session.connect()
return session
return False
except Exception as e:
self.log(f"Error starting P2P Connection session: {e}", isWarning=True)
return False

View file

@ -1,10 +1,11 @@
[NETWORK] [NETWORK]
modemport = 3050 modemaddress = 127.0.0.1
modemport = 5000
[STATION] [STATION]
mycall = XX1XXX mycall = AA1AAA
mygrid = JN18cv mygrid = JN48ea
myssid = 6 myssid = 1
ssid_list = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] ssid_list = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
enable_explorer = True enable_explorer = True
enable_stats = True enable_stats = True
@ -14,14 +15,13 @@ input_device = 5a1c
output_device = bd6c output_device = bd6c
rx_audio_level = 0 rx_audio_level = 0
tx_audio_level = 0 tx_audio_level = 0
enable_auto_tune = False
[RIGCTLD] [RIGCTLD]
ip = 127.0.0.1 ip = 127.0.0.1
port = 4532 port = 4532
path = path =
command = command =
arguments = --cenas arguments =
[RADIO] [RADIO]
control = disabled control = disabled
@ -45,13 +45,18 @@ enable_protocol = False
[MODEM] [MODEM]
enable_hmac = False enable_hmac = False
tuning_range_fmax = 50
tuning_range_fmin = -50
enable_fsk = False
enable_low_bandwidth_mode = False
enable_morse_identifier = False enable_morse_identifier = False
respond_to_cq = False respond_to_cq = True
rx_buffer_size = 64 tx_delay = 50
tx_delay = 200 maximum_bandwidth = 1700
beacon_interval = 90 enable_socket_interface = False
[SOCKET_INTERFACE]
enable = False
host = 127.0.0.1
cmd_port = 8000
data_port = 8001
[MESSAGES]
enable_auto_repeat = False

View file

@ -10,6 +10,7 @@ class CONFIG:
config_types = { config_types = {
'NETWORK': { 'NETWORK': {
'modemaddress': str,
'modemport': int, 'modemport': int,
}, },
'STATION': { 'STATION': {
@ -25,13 +26,11 @@ class CONFIG:
'output_device': str, 'output_device': str,
'rx_audio_level': int, 'rx_audio_level': int,
'tx_audio_level': int, 'tx_audio_level': int,
'enable_auto_tune': bool,
}, },
'RADIO': { 'RADIO': {
'control': str, 'control': str,
'serial_port': str, 'serial_port': str,
'model_id': int, 'model_id': int,
'serial_port': str,
'serial_speed': int, 'serial_speed': int,
'data_bits': int, 'data_bits': int,
'stop_bits': int, 'stop_bits': int,
@ -56,24 +55,36 @@ class CONFIG:
'enable_protocol': bool, 'enable_protocol': bool,
}, },
'MODEM': { 'MODEM': {
'enable_fft': bool,
'tuning_range_fmax': int,
'tuning_range_fmin': int,
'enable_fsk': bool,
'enable_hmac': bool, 'enable_hmac': bool,
'enable_morse_identifier': bool, 'enable_morse_identifier': bool,
'enable_low_bandwidth_mode': bool, 'maximum_bandwidth': int,
'respond_to_cq': bool, 'respond_to_cq': bool,
'rx_buffer_size': int,
'tx_delay': int, 'tx_delay': int,
'beacon_interval': int, 'enable_socket_interface': bool,
}, },
'SOCKET_INTERFACE': {
'enable' : bool,
'host' : str,
'cmd_port' : int,
'data_port' : int,
},
'MESSAGES': {
'enable_auto_repeat': bool,
}
}
default_values = {
list: '[]',
bool: 'False',
int: '0',
str: '',
} }
def __init__(self, configfile: str): def __init__(self, configfile: str):
# set up logger # set up logger
self.log = structlog.get_logger("CONFIG") self.log = structlog.get_logger(type(self).__name__)
# init configparser # init configparser
self.parser = configparser.ConfigParser(inline_comment_prefixes="#", allow_no_value=True) self.parser = configparser.ConfigParser(inline_comment_prefixes="#", allow_no_value=True)
@ -88,6 +99,9 @@ class CONFIG:
# check if config file exists # check if config file exists
self.config_exists() self.config_exists()
# validate config structure
self.validate_config()
def config_exists(self): def config_exists(self):
""" """
check if config file exists check if config file exists
@ -99,7 +113,7 @@ class CONFIG:
return False return False
# Validates config data # Validates config data
def validate(self, data): def validate_data(self, data):
for section in data: for section in data:
for setting in data[section]: for setting in data[section]:
if not isinstance(data[section][setting], self.config_types[section][setting]): if not isinstance(data[section][setting], self.config_types[section][setting]):
@ -107,6 +121,39 @@ class CONFIG:
f" '{data[section][setting]}' {type(data[section][setting])} given.") f" '{data[section][setting]}' {type(data[section][setting])} given.")
raise ValueError(message) raise ValueError(message)
def validate_config(self):
"""
Updates the configuration file to match exactly what is defined in self.config_types.
It removes sections and settings not defined there and adds missing sections and settings.
"""
existing_sections = self.parser.sections()
# Remove sections and settings not defined in self.config_types
for section in existing_sections:
if section not in self.config_types:
self.parser.remove_section(section)
self.log.info(f"[CFG] Removing undefined section: {section}")
continue
existing_settings = self.parser.options(section)
for setting in existing_settings:
if setting not in self.config_types[section]:
self.parser.remove_option(section, setting)
self.log.info(f"[CFG] Removing undefined setting: {section}.{setting}")
# Add missing sections and settings from self.config_types
for section, settings in self.config_types.items():
if section not in existing_sections:
self.parser.add_section(section)
self.log.info(f"[CFG] Adding missing section: {section}")
for setting, value_type in settings.items():
if not self.parser.has_option(section, setting):
default_value = self.default_values.get(value_type, None)
self.parser.set(section, setting, str(default_value))
self.log.info(f"[CFG] Adding missing setting: {section}.{setting}")
return self.write_to_file()
# Handle special setting data type conversion # Handle special setting data type conversion
# is_writing means data from a dict being writen to the config file # is_writing means data from a dict being writen to the config file
# if False, it means the opposite direction # if False, it means the opposite direction
@ -132,8 +179,7 @@ class CONFIG:
# Sets and writes config data from a dict containing data settings # Sets and writes config data from a dict containing data settings
def write(self, data): def write(self, data):
# Validate config data before writing # Validate config data before writing
self.validate(data) self.validate_data(data)
for section in data: for section in data:
# init section if it doesn't exist yet # init section if it doesn't exist yet
if not section.upper() in self.parser.keys(): if not section.upper() in self.parser.keys():
@ -142,8 +188,13 @@ class CONFIG:
for setting in data[section]: for setting in data[section]:
new_value = self.handle_setting( new_value = self.handle_setting(
section, setting, data[section][setting], True) section, setting, data[section][setting], True)
self.parser[section][setting] = str(new_value) try:
self.parser[section][setting] = str(new_value)
except Exception as e:
self.log.error("[CFG] error setting config key", e=e)
return self.write_to_file()
def write_to_file(self):
# Write config data to file # Write config data to file
try: try:
with open(self.config_name, 'w') as configfile: with open(self.config_name, 'w') as configfile:
@ -157,7 +208,7 @@ class CONFIG:
""" """
read config file read config file
""" """
self.log.info("[CFG] reading...") #self.log.info("[CFG] reading...")
if not self.config_exists(): if not self.config_exists():
return False return False

View file

@ -18,6 +18,7 @@ class DataFrameFactory:
} }
def __init__(self, config): def __init__(self, config):
self.myfullcall = f"{config['STATION']['mycall']}-{config['STATION']['myssid']}" self.myfullcall = f"{config['STATION']['mycall']}-{config['STATION']['myssid']}"
self.mygrid = config['STATION']['mygrid'] self.mygrid = config['STATION']['mygrid']
@ -28,6 +29,7 @@ class DataFrameFactory:
self._load_ping_templates() self._load_ping_templates()
self._load_fec_templates() self._load_fec_templates()
self._load_arq_templates() self._load_arq_templates()
self._load_p2p_connection_templates()
def _load_broadcast_templates(self): def _load_broadcast_templates(self):
# cq frame # cq frame
@ -98,6 +100,7 @@ class DataFrameFactory:
"destination_crc": 3, "destination_crc": 3,
"origin": 6, "origin": 6,
"session_id": 1, "session_id": 1,
"maximum_bandwidth": 2,
} }
self.template_list[FR_TYPE.ARQ_SESSION_OPEN_ACK.value] = { self.template_list[FR_TYPE.ARQ_SESSION_OPEN_ACK.value] = {
@ -144,6 +147,7 @@ class DataFrameFactory:
self.template_list[FR_TYPE.ARQ_BURST_FRAME.value] = { self.template_list[FR_TYPE.ARQ_BURST_FRAME.value] = {
"frame_length": None, "frame_length": None,
"session_id": 1, "session_id": 1,
"speed_level": 1,
"offset": 4, "offset": 4,
"data": "dynamic", "data": "dynamic",
} }
@ -158,6 +162,63 @@ class DataFrameFactory:
"snr": 1, "snr": 1,
"flag": 1, "flag": 1,
} }
def _load_p2p_connection_templates(self):
# p2p connect request
self.template_list[FR_TYPE.P2P_CONNECTION_CONNECT.value] = {
"frame_length": self.LENGTH_SIG1_FRAME,
"destination_crc": 3,
"origin": 6,
"session_id": 1,
}
# connect ACK
self.template_list[FR_TYPE.P2P_CONNECTION_CONNECT_ACK.value] = {
"frame_length": self.LENGTH_SIG1_FRAME,
"destination_crc": 3,
"origin": 6,
"session_id": 1,
}
# heartbeat for "is alive"
self.template_list[FR_TYPE.P2P_CONNECTION_HEARTBEAT.value] = {
"frame_length": self.LENGTH_SIG1_FRAME,
"session_id": 1,
}
# ack heartbeat
self.template_list[FR_TYPE.P2P_CONNECTION_HEARTBEAT_ACK.value] = {
"frame_length": self.LENGTH_SIG1_FRAME,
"session_id": 1,
}
# p2p payload frames
self.template_list[FR_TYPE.P2P_CONNECTION_PAYLOAD.value] = {
"frame_length": None,
"session_id": 1,
"sequence_id": 1,
"data": "dynamic",
}
# p2p payload frame ack
self.template_list[FR_TYPE.P2P_CONNECTION_PAYLOAD_ACK.value] = {
"frame_length": self.LENGTH_SIG1_FRAME,
"session_id": 1,
"sequence_id": 1,
}
# heartbeat for "is alive"
self.template_list[FR_TYPE.P2P_CONNECTION_DISCONNECT.value] = {
"frame_length": self.LENGTH_SIG1_FRAME,
"session_id": 1,
}
# ack heartbeat
self.template_list[FR_TYPE.P2P_CONNECTION_DISCONNECT_ACK.value] = {
"frame_length": self.LENGTH_SIG1_FRAME,
"session_id": 1,
}
def construct(self, frametype, content, frame_length = LENGTH_SIG1_FRAME): def construct(self, frametype, content, frame_length = LENGTH_SIG1_FRAME):
@ -218,7 +279,7 @@ class DataFrameFactory:
elif key in ["session_id", "speed_level", elif key in ["session_id", "speed_level",
"frames_per_burst", "version", "frames_per_burst", "version",
"offset", "total_length", "state", "type"]: "offset", "total_length", "state", "type", "maximum_bandwidth"]:
extracted_data[key] = int.from_bytes(data, 'big') extracted_data[key] = int.from_bytes(data, 'big')
elif key in ["snr"]: elif key in ["snr"]:
@ -327,11 +388,12 @@ class DataFrameFactory:
test_frame[:1] = bytes([FR_TYPE.TEST_FRAME.value]) test_frame[:1] = bytes([FR_TYPE.TEST_FRAME.value])
return test_frame return test_frame
def build_arq_session_open(self, destination, session_id): def build_arq_session_open(self, destination, session_id, maximum_bandwidth):
payload = { payload = {
"destination_crc": helpers.get_crc_24(destination), "destination_crc": helpers.get_crc_24(destination),
"origin": helpers.callsign_to_bytes(self.myfullcall), "origin": helpers.callsign_to_bytes(self.myfullcall),
"session_id": session_id.to_bytes(1, 'big'), "session_id": session_id.to_bytes(1, 'big'),
"maximum_bandwidth": maximum_bandwidth.to_bytes(2, 'big'),
} }
return self.construct(FR_TYPE.ARQ_SESSION_OPEN, payload) return self.construct(FR_TYPE.ARQ_SESSION_OPEN, payload)
@ -394,14 +456,16 @@ class DataFrameFactory:
} }
return self.construct(FR_TYPE.ARQ_SESSION_INFO_ACK, payload) return self.construct(FR_TYPE.ARQ_SESSION_INFO_ACK, payload)
def build_arq_burst_frame(self, freedv_mode: codec2.FREEDV_MODE, session_id: int, offset: int, data: bytes): def build_arq_burst_frame(self, freedv_mode: codec2.FREEDV_MODE, session_id: int, offset: int, data: bytes, speed_level: int):
payload = { payload = {
"session_id": session_id.to_bytes(1, 'big'), "session_id": session_id.to_bytes(1, 'big'),
"speed_level": speed_level.to_bytes(1, 'big'),
"offset": offset.to_bytes(4, 'big'), "offset": offset.to_bytes(4, 'big'),
"data": data, "data": data,
} }
frame = self.construct(FR_TYPE.ARQ_BURST_FRAME, payload, self.get_bytes_per_frame(freedv_mode)) return self.construct(
return frame FR_TYPE.ARQ_BURST_FRAME, payload, self.get_bytes_per_frame(freedv_mode)
)
def build_arq_burst_ack(self, session_id: bytes, offset, speed_level: int, def build_arq_burst_ack(self, session_id: bytes, offset, speed_level: int,
frames_per_burst: int, snr: int, flag_final=False, flag_checksum=False, flag_abort=False): frames_per_burst: int, snr: int, flag_final=False, flag_checksum=False, flag_abort=False):
@ -415,7 +479,6 @@ class DataFrameFactory:
if flag_abort: if flag_abort:
flag = helpers.set_flag(flag, 'ABORT', True, self.ARQ_FLAGS) flag = helpers.set_flag(flag, 'ABORT', True, self.ARQ_FLAGS)
payload = { payload = {
"session_id": session_id.to_bytes(1, 'big'), "session_id": session_id.to_bytes(1, 'big'),
"offset": offset.to_bytes(4, 'big'), "offset": offset.to_bytes(4, 'big'),
@ -425,3 +488,62 @@ class DataFrameFactory:
"flag": flag.to_bytes(1, 'big'), "flag": flag.to_bytes(1, 'big'),
} }
return self.construct(FR_TYPE.ARQ_BURST_ACK, payload) return self.construct(FR_TYPE.ARQ_BURST_ACK, payload)
def build_p2p_connection_connect(self, destination, origin, session_id):
payload = {
"destination_crc": helpers.get_crc_24(destination),
"origin": helpers.callsign_to_bytes(origin),
"session_id": session_id.to_bytes(1, 'big'),
}
return self.construct(FR_TYPE.P2P_CONNECTION_CONNECT, payload)
def build_p2p_connection_connect_ack(self, destination, origin, session_id):
payload = {
"destination_crc": helpers.get_crc_24(destination),
"origin": helpers.callsign_to_bytes(origin),
"session_id": session_id.to_bytes(1, 'big'),
}
return self.construct(FR_TYPE.P2P_CONNECTION_CONNECT_ACK, payload)
def build_p2p_connection_heartbeat(self, session_id):
payload = {
"session_id": session_id.to_bytes(1, 'big'),
}
return self.construct(FR_TYPE.P2P_CONNECTION_HEARTBEAT, payload)
def build_p2p_connection_heartbeat_ack(self, session_id):
payload = {
"session_id": session_id.to_bytes(1, 'big'),
}
return self.construct(FR_TYPE.P2P_CONNECTION_HEARTBEAT_ACK, payload)
def build_p2p_connection_payload(self, freedv_mode: codec2.FREEDV_MODE, session_id: int, sequence_id: int, data: bytes):
payload = {
"session_id": session_id.to_bytes(1, 'big'),
"sequence_id": sequence_id.to_bytes(1, 'big'),
"data": data,
}
return self.construct(
FR_TYPE.P2P_CONNECTION_PAYLOAD,
payload,
self.get_bytes_per_frame(freedv_mode),
)
def build_p2p_connection_payload_ack(self, session_id, sequence_id):
payload = {
"session_id": session_id.to_bytes(1, 'big'),
"sequence_id": sequence_id.to_bytes(1, 'big'),
}
return self.construct(FR_TYPE.P2P_CONNECTION_PAYLOAD_ACK, payload)
def build_p2p_connection_disconnect(self, session_id):
payload = {
"session_id": session_id.to_bytes(1, 'big'),
}
return self.construct(FR_TYPE.P2P_CONNECTION_DISCONNECT, payload)
def build_p2p_connection_disconnect_ack(self, session_id):
payload = {
"session_id": session_id.to_bytes(1, 'big'),
}
return self.construct(FR_TYPE.P2P_CONNECTION_DISCONNECT_ACK, payload)

View file

@ -4,8 +4,6 @@ import ctypes
import structlog import structlog
import threading import threading
import audio import audio
import os
from modem_frametypes import FRAME_TYPE
import itertools import itertools
TESTMODE = False TESTMODE = False
@ -27,20 +25,16 @@ class Demodulator():
'decoding_thread': None 'decoding_thread': None
} }
def __init__(self, config, audio_rx_q, modem_rx_q, data_q_rx, states, event_manager, fft_queue): def __init__(self, config, audio_rx_q, data_q_rx, states, event_manager, service_queue, fft_queue):
self.log = structlog.get_logger("Demodulator") self.log = structlog.get_logger("Demodulator")
self.tuning_range_fmin = config['MODEM']['tuning_range_fmin'] self.service_queue = service_queue
self.tuning_range_fmax = config['MODEM']['tuning_range_fmax']
self.rx_audio_level = config['AUDIO']['rx_audio_level']
self.AUDIO_FRAMES_PER_BUFFER_RX = 4800 self.AUDIO_FRAMES_PER_BUFFER_RX = 4800
self.buffer_overflow_counter = [0, 0, 0, 0, 0, 0, 0, 0] self.buffer_overflow_counter = [0, 0, 0, 0, 0, 0, 0, 0]
self.is_codec2_traffic_counter = 0 self.is_codec2_traffic_counter = 0
self.is_codec2_traffic_cooldown = 5 self.is_codec2_traffic_cooldown = 5
self.audio_received_queue = audio_rx_q self.audio_received_queue = audio_rx_q
self.modem_received_queue = modem_rx_q
self.data_queue_received = data_q_rx self.data_queue_received = data_q_rx
self.states = states self.states = states
@ -79,13 +73,6 @@ class Demodulator():
codec2.api.freedv_open(mode), ctypes.c_void_p codec2.api.freedv_open(mode), ctypes.c_void_p
) )
# set tuning range
codec2.api.freedv_set_tuning_range(
c2instance,
ctypes.c_float(float(self.tuning_range_fmin)),
ctypes.c_float(float(self.tuning_range_fmax)),
)
# get bytes per frame # get bytes per frame
bytes_per_frame = int( bytes_per_frame = int(
codec2.api.freedv_get_bits_per_modem_frame(c2instance) / 8 codec2.api.freedv_get_bits_per_modem_frame(c2instance) / 8
@ -135,49 +122,6 @@ class Demodulator():
) )
self.MODE_DICT[mode]['decoding_thread'].start() self.MODE_DICT[mode]['decoding_thread'].start()
def sd_input_audio_callback(self, indata: np.ndarray, frames: int, time, status) -> None:
audio_48k = np.frombuffer(indata, dtype=np.int16)
audio_8k = self.resampler.resample48_to_8(audio_48k)
audio_8k_level_adjusted = audio.set_audio_volume(audio_8k, self.rx_audio_level)
audio.calculate_fft(audio_8k_level_adjusted, self.fft_queue, self.states)
length_audio_8k_level_adjusted = len(audio_8k_level_adjusted)
# Avoid buffer overflow by filling only if buffer for
# selected datachannel mode is not full
index = 0
for mode in self.MODE_DICT:
mode_data = self.MODE_DICT[mode]
audiobuffer = mode_data['audio_buffer']
decode = mode_data['decode']
index += 1
if audiobuffer:
if (audiobuffer.nbuffer + length_audio_8k_level_adjusted) > audiobuffer.size:
self.buffer_overflow_counter[index] += 1
self.event_manager.send_buffer_overflow(self.buffer_overflow_counter)
elif decode:
audiobuffer.push(audio_8k_level_adjusted)
def worker_received(self) -> None:
"""Worker for FIFO queue for processing received frames"""
while True:
data = self.modem_received_queue.get()
self.log.debug("[MDM] worker_received: received data!")
# data[0] = bytes_out
# data[1] = freedv session
# data[2] = bytes_per_frame
# data[3] = snr
item = {
'payload': data[0],
'freedv': data[1],
'bytes_per_frame': data[2],
'snr': data[3],
'frequency_offset': self.get_frequency_offset(data[1]),
}
self.data_queue_received.put(item)
self.modem_received_queue.task_done()
def get_frequency_offset(self, freedv: ctypes.c_void_p) -> float: def get_frequency_offset(self, freedv: ctypes.c_void_p) -> float:
""" """
@ -247,7 +191,16 @@ class Demodulator():
snr = self.calculate_snr(freedv) snr = self.calculate_snr(freedv)
self.get_scatter(freedv) self.get_scatter(freedv)
self.modem_received_queue.put([bytes_out, freedv, bytes_per_frame, snr]) item = {
'payload': bytes_out,
'freedv': freedv,
'bytes_per_frame': bytes_per_frame,
'snr': snr,
'frequency_offset': self.get_frequency_offset(freedv),
}
self.data_queue_received.put(item)
state_buffer = [] state_buffer = []
except Exception as e: except Exception as e:
error_message = str(e) error_message = str(e)
@ -257,6 +210,7 @@ class Demodulator():
self.log.debug( self.log.debug(
"[MDM] [demod_audio] demod loop ended", mode=mode_name, e=e "[MDM] [demod_audio] demod loop ended", mode=mode_name, e=e
) )
def tci_rx_callback(self) -> None: def tci_rx_callback(self) -> None:
""" """
Callback for TCI RX Callback for TCI RX
@ -297,6 +251,7 @@ class Demodulator():
frames_per_burst = min(frames_per_burst, 1) frames_per_burst = min(frames_per_burst, 1)
frames_per_burst = max(frames_per_burst, 5) frames_per_burst = max(frames_per_burst, 5)
# FIXME
frames_per_burst = 1 frames_per_burst = 1
codec2.api.freedv_set_frames_per_burst(self.dat0_datac1_freedv, frames_per_burst) codec2.api.freedv_set_frames_per_burst(self.dat0_datac1_freedv, frames_per_burst)
@ -382,15 +337,19 @@ class Demodulator():
for mode in self.MODE_DICT: for mode in self.MODE_DICT:
codec2.api.freedv_set_sync(self.MODE_DICT[mode]["instance"], 0) codec2.api.freedv_set_sync(self.MODE_DICT[mode]["instance"], 0)
def set_decode_mode(self, mode): def set_decode_mode(self, modes_to_decode=None):
# Reset all modes to not decode
for m in self.MODE_DICT: self.MODE_DICT[m]["decode"] = False for m in self.MODE_DICT:
self.MODE_DICT[m]["decode"] = False
# signalling is always true # signalling is always true
self.MODE_DICT[codec2.FREEDV_MODE.signalling.value]["decode"] = True self.MODE_DICT[codec2.FREEDV_MODE.signalling.value]["decode"] = True
# Enable mode based on speed_level # lowest speed level is alwys true
self.MODE_DICT[mode.value]["decode"] = True self.MODE_DICT[codec2.FREEDV_MODE.datac4.value]["decode"] = True
self.log.info(f"[MDM] [demod_audio] set data mode: {mode.name}")
return # Enable specified modes
if modes_to_decode:
for mode, decode in modes_to_decode.items():
if mode in self.MODE_DICT:
self.MODE_DICT[mode]["decode"] = decode

View file

@ -12,6 +12,8 @@ class EventManager:
def broadcast(self, data): def broadcast(self, data):
for q in self.queues: for q in self.queues:
self.logger.debug(f"Event: ", ev=data) self.logger.debug(f"Event: ", ev=data)
if q.qsize() > 10:
q.queue.clear()
q.put(data) q.put(data)
def send_ptt_change(self, on:bool = False): def send_ptt_change(self, on:bool = False):
@ -42,7 +44,10 @@ class EventManager:
} }
self.broadcast(event) self.broadcast(event)
def send_arq_session_progress(self, outbound: bool, session_id, dxcall, received_bytes, total_bytes, state): def send_arq_session_progress(self, outbound: bool, session_id, dxcall, received_bytes, total_bytes, state, statistics=None):
if statistics is None:
statistics = {}
direction = 'outbound' if outbound else 'inbound' direction = 'outbound' if outbound else 'inbound'
event = { event = {
"type": "arq", "type": "arq",
@ -52,6 +57,7 @@ class EventManager:
'received_bytes': received_bytes, 'received_bytes': received_bytes,
'total_bytes': total_bytes, 'total_bytes': total_bytes,
'state': state, 'state': state,
'statistics': statistics,
} }
} }
self.broadcast(event) self.broadcast(event)

View file

@ -9,7 +9,7 @@ Created on 05.11.23
import requests import requests
import threading import threading
import ujson as json import json
import structlog import structlog
import sched import sched
import time import time
@ -24,7 +24,6 @@ class explorer():
self.config = self.config_manager.read() self.config = self.config_manager.read()
self.states = states self.states = states
self.explorer_url = "https://api.freedata.app/explorer.php" self.explorer_url = "https://api.freedata.app/explorer.php"
self.publish_interval = 120
def push(self): def push(self):
self.config = self.config_manager.read() self.config = self.config_manager.read()
@ -34,17 +33,16 @@ class explorer():
callsign = str(self.config['STATION']['mycall']) + "-" + str(self.config["STATION"]['myssid']) callsign = str(self.config['STATION']['mycall']) + "-" + str(self.config["STATION"]['myssid'])
gridsquare = str(self.config['STATION']['mygrid']) gridsquare = str(self.config['STATION']['mygrid'])
version = str(self.modem_version) version = str(self.modem_version)
bandwidth = str(self.config['MODEM']['enable_low_bandwidth_mode']) bandwidth = str(self.config['MODEM']['maximum_bandwidth'])
beacon = str(self.states.is_beacon_running) beacon = str(self.states.is_beacon_running)
strength = str(self.states.s_meter_strength) strength = str(self.states.s_meter_strength)
# stop pushing if default callsign # stop pushing if default callsign
if callsign in ['XX1XXX-6']: if callsign in ['XX1XXX-6']:
# Reschedule the push method
self.scheduler.enter(self.publish_interval, 1, self.push)
return return
log.info("[EXPLORER] publish", frequency=frequency, band=band, callsign=callsign, gridsquare=gridsquare, version=version, bandwidth=bandwidth) # disabled this one
# log.info("[EXPLORER] publish", frequency=frequency, band=band, callsign=callsign, gridsquare=gridsquare, version=version, bandwidth=bandwidth)
headers = {"Content-Type": "application/json"} headers = {"Content-Type": "application/json"}
station_data = {'callsign': callsign, 'gridsquare': gridsquare, 'frequency': frequency, 'strength': strength, 'band': band, 'version': version, 'bandwidth': bandwidth, 'beacon': beacon, "lastheard": []} station_data = {'callsign': callsign, 'gridsquare': gridsquare, 'frequency': frequency, 'strength': strength, 'band': band, 'version': version, 'bandwidth': bandwidth, 'beacon': beacon, "lastheard": []}

View file

@ -13,8 +13,11 @@ from frame_handler import FrameHandler
from frame_handler_ping import PingFrameHandler from frame_handler_ping import PingFrameHandler
from frame_handler_cq import CQFrameHandler from frame_handler_cq import CQFrameHandler
from frame_handler_arq_session import ARQFrameHandler from frame_handler_arq_session import ARQFrameHandler
from frame_handler_p2p_connection import P2PConnectionFrameHandler
from frame_handler_beacon import BeaconFrameHandler from frame_handler_beacon import BeaconFrameHandler
class DISPATCHER(): class DISPATCHER():
FRAME_HANDLER = { FRAME_HANDLER = {
@ -22,9 +25,18 @@ class DISPATCHER():
FR_TYPE.ARQ_SESSION_OPEN.value: {"class": ARQFrameHandler, "name": "ARQ Data Channel Open"}, FR_TYPE.ARQ_SESSION_OPEN.value: {"class": ARQFrameHandler, "name": "ARQ Data Channel Open"},
FR_TYPE.ARQ_SESSION_INFO_ACK.value: {"class": ARQFrameHandler, "name": "ARQ INFO ACK"}, FR_TYPE.ARQ_SESSION_INFO_ACK.value: {"class": ARQFrameHandler, "name": "ARQ INFO ACK"},
FR_TYPE.ARQ_SESSION_INFO.value: {"class": ARQFrameHandler, "name": "ARQ Data Channel Info"}, FR_TYPE.ARQ_SESSION_INFO.value: {"class": ARQFrameHandler, "name": "ARQ Data Channel Info"},
FR_TYPE.ARQ_CONNECTION_CLOSE.value: {"class": ARQFrameHandler, "name": "ARQ CLOSE SESSION"}, FR_TYPE.P2P_CONNECTION_CONNECT.value: {"class": P2PConnectionFrameHandler, "name": "P2P Connection CONNECT"},
FR_TYPE.ARQ_CONNECTION_HB.value: {"class": ARQFrameHandler, "name": "ARQ HEARTBEAT"}, FR_TYPE.P2P_CONNECTION_CONNECT_ACK.value: {"class": P2PConnectionFrameHandler, "name": "P2P Connection CONNECT ACK"},
FR_TYPE.ARQ_CONNECTION_OPEN.value: {"class": ARQFrameHandler, "name": "ARQ OPEN SESSION"}, FR_TYPE.P2P_CONNECTION_DISCONNECT.value: {"class": P2PConnectionFrameHandler, "name": "P2P Connection DISCONNECT"},
FR_TYPE.P2P_CONNECTION_DISCONNECT_ACK.value: {"class": P2PConnectionFrameHandler,
"name": "P2P Connection DISCONNECT ACK"},
FR_TYPE.P2P_CONNECTION_PAYLOAD.value: {"class": P2PConnectionFrameHandler,
"name": "P2P Connection PAYLOAD"},
FR_TYPE.P2P_CONNECTION_PAYLOAD_ACK.value: {"class": P2PConnectionFrameHandler,
"name": "P2P Connection PAYLOAD ACK"},
#FR_TYPE.ARQ_CONNECTION_HB.value: {"class": ARQFrameHandler, "name": "ARQ HEARTBEAT"},
#FR_TYPE.ARQ_CONNECTION_OPEN.value: {"class": ARQFrameHandler, "name": "ARQ OPEN SESSION"},
FR_TYPE.ARQ_STOP.value: {"class": ARQFrameHandler, "name": "ARQ STOP"}, FR_TYPE.ARQ_STOP.value: {"class": ARQFrameHandler, "name": "ARQ STOP"},
FR_TYPE.ARQ_STOP_ACK.value: {"class": ARQFrameHandler, "name": "ARQ STOP ACK"}, FR_TYPE.ARQ_STOP_ACK.value: {"class": ARQFrameHandler, "name": "ARQ STOP ACK"},
FR_TYPE.BEACON.value: {"class": BeaconFrameHandler, "name": "BEACON"}, FR_TYPE.BEACON.value: {"class": BeaconFrameHandler, "name": "BEACON"},
@ -82,7 +94,7 @@ class DISPATCHER():
if frametype not in self.FRAME_HANDLER: if frametype not in self.FRAME_HANDLER:
self.log.warning( self.log.warning(
"[Modem] ARQ - other frame type", frametype=FR_TYPE(frametype).name) "[DISPATCHER] ARQ - other frame type", frametype=FR_TYPE(frametype).name)
return return
# instantiate handler # instantiate handler

View file

@ -6,9 +6,11 @@ import structlog
import time, uuid import time, uuid
from codec2 import FREEDV_MODE from codec2 import FREEDV_MODE
from message_system_db_manager import DatabaseManager from message_system_db_manager import DatabaseManager
import maidenhead
TESTMODE = False TESTMODE = False
class FrameHandler(): class FrameHandler():
def __init__(self, name: str, config, states: StateManager, event_manager: EventManager, def __init__(self, name: str, config, states: StateManager, event_manager: EventManager,
@ -34,7 +36,7 @@ class FrameHandler():
ft = self.details['frame']['frame_type'] ft = self.details['frame']['frame_type']
valid = False valid = False
# Check for callsign checksum # Check for callsign checksum
if ft in ['ARQ_SESSION_OPEN', 'ARQ_SESSION_OPEN_ACK', 'PING', 'PING_ACK']: if ft in ['ARQ_SESSION_OPEN', 'ARQ_SESSION_OPEN_ACK', 'PING', 'PING_ACK', 'P2P_CONNECTION_CONNECT']:
valid, mycallsign = helpers.check_callsign( valid, mycallsign = helpers.check_callsign(
call_with_ssid, call_with_ssid,
self.details["frame"]["destination_crc"], self.details["frame"]["destination_crc"],
@ -51,6 +53,20 @@ class FrameHandler():
session_id = self.details['frame']['session_id'] session_id = self.details['frame']['session_id']
if session_id in self.states.arq_iss_sessions: if session_id in self.states.arq_iss_sessions:
valid = True valid = True
# check for p2p connection
elif ft in ['P2P_CONNECTION_CONNECT']:
valid, mycallsign = helpers.check_callsign(
call_with_ssid,
self.details["frame"]["destination_crc"],
self.config['STATION']['ssid_list'])
#check for p2p connection
elif ft in ['P2P_CONNECTION_CONNECT_ACK', 'P2P_CONNECTION_PAYLOAD', 'P2P_CONNECTION_PAYLOAD_ACK', 'P2P_CONNECTION_DISCONNECT', 'P2P_CONNECTION_DISCONNECT_ACK']:
session_id = self.details['frame']['session_id']
if session_id in self.states.p2p_connection_sessions:
valid = True
else: else:
valid = False valid = False
@ -87,15 +103,21 @@ class FrameHandler():
self.states.add_activity(activity) self.states.add_activity(activity)
def add_to_heard_stations(self): def add_to_heard_stations(self):
frame = self.details['frame'] frame = self.details['frame']
print(frame)
if 'origin' not in frame: if 'origin' not in frame:
return return
dxgrid = frame['gridsquare'] if 'gridsquare' in frame else "------" dxgrid = frame.get('gridsquare', "------")
# Initialize distance values
distance_km = None
distance_miles = None
if dxgrid != "------" and frame.get('gridsquare'):
distance_dict = maidenhead.distance_between_locators(self.config['STATION']['mygrid'], frame['gridsquare'])
distance_km = distance_dict['kilometers']
distance_miles = distance_dict['miles']
helpers.add_to_heard_stations( helpers.add_to_heard_stations(
frame['origin'], frame['origin'],
dxgrid, dxgrid,
@ -104,8 +126,9 @@ class FrameHandler():
self.details['frequency_offset'], self.details['frequency_offset'],
self.states.radio_frequency, self.states.radio_frequency,
self.states.heard_stations, self.states.heard_stations,
distance_km=distance_km, # Pass the kilometer distance
distance_miles=distance_miles # Pass the miles distance
) )
def make_event(self): def make_event(self):
event = { event = {
@ -119,6 +142,13 @@ class FrameHandler():
if 'origin' in self.details['frame']: if 'origin' in self.details['frame']:
event['dxcallsign'] = self.details['frame']['origin'] event['dxcallsign'] = self.details['frame']['origin']
if 'gridsquare' in self.details['frame']:
event['gridsquare'] = self.details['frame']['gridsquare']
distance = maidenhead.distance_between_locators(self.config['STATION']['mygrid'], self.details['frame']['gridsquare'])
event['distance_kilometers'] = distance['kilometers']
event['distance_miles'] = distance['miles']
return event return event
def emit_event(self): def emit_event(self):

View file

@ -32,7 +32,8 @@ class ARQFrameHandler(frame_handler.FrameHandler):
session = ARQSessionIRS(self.config, session = ARQSessionIRS(self.config,
self.modem, self.modem,
frame['origin'], frame['origin'],
session_id) session_id,
self.states)
self.states.register_arq_irs_session(session) self.states.register_arq_irs_session(session)
elif frame['frame_type_int'] in [ elif frame['frame_type_int'] in [

View file

@ -4,6 +4,7 @@ import data_frame_factory
import frame_handler import frame_handler
import datetime import datetime
from message_system_db_beacon import DatabaseManagerBeacon from message_system_db_beacon import DatabaseManagerBeacon
from message_system_db_messages import DatabaseManagerMessages
from message_system_db_manager import DatabaseManager from message_system_db_manager import DatabaseManager
@ -15,3 +16,7 @@ class BeaconFrameHandler(frame_handler.FrameHandler):
self.details["snr"], self.details["snr"],
self.details['frame']["gridsquare"] self.details['frame']["gridsquare"]
) )
if self.config["MESSAGES"]["enable_auto_repeat"]:
# set message to queued if beacon received
DatabaseManagerMessages(self.event_manager).set_message_to_queued_for_callsign(self.details['frame']["origin"])

View file

@ -2,11 +2,25 @@ import frame_handler_ping
import helpers import helpers
import data_frame_factory import data_frame_factory
import frame_handler import frame_handler
class CQFrameHandler(frame_handler_ping.PingFrameHandler): from message_system_db_messages import DatabaseManagerMessages
def should_respond(self):
self.logger.debug(f"Respond to CQ: {self.config['MODEM']['respond_to_cq']}") class CQFrameHandler(frame_handler.FrameHandler):
return self.config['MODEM']['respond_to_cq']
#def should_respond(self):
# self.logger.debug(f"Respond to CQ: {self.config['MODEM']['respond_to_cq']}")
# return bool(self.config['MODEM']['respond_to_cq'] and not self.states.getARQ())
def follow_protocol(self):
if self.states.getARQ():
return
self.logger.debug(
f"[Modem] Responding to request from [{self.details['frame']['origin']}]",
snr=self.details['snr'],
)
self.send_ack()
def send_ack(self): def send_ack(self):
factory = data_frame_factory.DataFrameFactory(self.config) factory = data_frame_factory.DataFrameFactory(self.config)
@ -14,3 +28,7 @@ class CQFrameHandler(frame_handler_ping.PingFrameHandler):
self.details['snr'] self.details['snr']
) )
self.transmit(qrv_frame) self.transmit(qrv_frame)
if self.config["MESSAGES"]["enable_auto_repeat"]:
# set message to queued if CQ received
DatabaseManagerMessages(self.event_manager).set_message_to_queued_for_callsign(self.details['frame']["origin"])

View file

@ -0,0 +1,54 @@
from queue import Queue
import frame_handler
from event_manager import EventManager
from state_manager import StateManager
from modem_frametypes import FRAME_TYPE as FR
from p2p_connection import P2PConnection
class P2PConnectionFrameHandler(frame_handler.FrameHandler):
def follow_protocol(self):
if not self.should_respond():
return
frame = self.details['frame']
session_id = frame['session_id']
snr = self.details["snr"]
frequency_offset = self.details["frequency_offset"]
if frame['frame_type_int'] == FR.P2P_CONNECTION_CONNECT.value:
# Lost OPEN_ACK case .. ISS will retry opening a session
if session_id in self.states.arq_irs_sessions:
session = self.states.p2p_connection_sessions[session_id]
# Normal case when receiving a SESSION_OPEN for the first time
else:
# if self.states.check_if_running_arq_session():
# self.logger.warning("DISCARDING SESSION OPEN because of ongoing ARQ session ", frame=frame)
# return
print(frame)
session = P2PConnection(self.config,
self.modem,
frame['origin'],
frame['destination_crc'],
self.states, self.event_manager)
session.session_id = session_id
self.states.register_p2p_connection_session(session)
elif frame['frame_type_int'] in [
FR.P2P_CONNECTION_CONNECT_ACK.value,
FR.P2P_CONNECTION_DISCONNECT.value,
FR.P2P_CONNECTION_DISCONNECT_ACK.value,
FR.P2P_CONNECTION_PAYLOAD.value,
FR.P2P_CONNECTION_PAYLOAD_ACK.value,
]:
session = self.states.get_p2p_connection_session(session_id)
else:
self.logger.warning("DISCARDING FRAME", frame=frame)
return
session.set_details(snr, frequency_offset)
session.on_frame_received(frame)

View file

@ -15,15 +15,11 @@ class PingFrameHandler(frame_handler.FrameHandler):
# ft = self.details['frame']['frame_type'] # ft = self.details['frame']['frame_type']
# self.logger.info(f"[Modem] {ft} received but not for us.") # self.logger.info(f"[Modem] {ft} received but not for us.")
# return valid # return valid
#def should_respond(self):
# return self.is_frame_for_me()
def follow_protocol(self): def follow_protocol(self):
if not bool(self.is_frame_for_me() and not self.states.getARQ()):
if not self.should_respond():
return return
self.logger.debug( self.logger.debug(
f"[Modem] Responding to request from [{self.details['frame']['origin']}]", f"[Modem] Responding to request from [{self.details['frame']['origin']}]",
snr=self.details['snr'], snr=self.details['snr'],

View file

@ -15,7 +15,10 @@ import hmac
import os import os
import sys import sys
from pathlib import Path from pathlib import Path
import platform
import subprocess
import psutil
import glob
log = structlog.get_logger("helpers") log = structlog.get_logger("helpers")
@ -118,60 +121,44 @@ def get_crc_32(data: str) -> bytes:
return crc_algorithm(data).to_bytes(4, byteorder="big") return crc_algorithm(data).to_bytes(4, byteorder="big")
def add_to_heard_stations(dxcallsign, dxgrid, datatype, snr, offset, frequency, heard_stations_list): from datetime import datetime, timezone
""" import time
def add_to_heard_stations(dxcallsign, dxgrid, datatype, snr, offset, frequency, heard_stations_list, distance_km=None,
distance_miles=None):
"""
Args: Args:
dxcallsign: dxcallsign (str): The callsign of the DX station.
dxgrid: dxgrid (str): The Maidenhead grid square of the DX station.
datatype: datatype (str): The type of data received (e.g., FT8, CW).
snr: snr (int): Signal-to-noise ratio of the received signal.
offset: offset (float): Frequency offset.
frequency: frequency (float): Base frequency of the received signal.
heard_stations_list (list): List containing heard stations.
distance_km (float): Distance to the DX station in kilometers.
distance_miles (float): Distance to the DX station in miles.
Returns: Returns:
Nothing Nothing. The function updates the heard_stations_list in-place.
""" """
# check if buffer empty # Convert current timestamp to an integer
if len(heard_stations_list) == 0: current_timestamp = int(datetime.now(timezone.utc).timestamp())
heard_stations_list.append(
[dxcallsign, dxgrid, int(datetime.now(timezone.utc).timestamp()), datatype, snr, offset, frequency] # Initialize the new entry
) new_entry = [
# if not, we search and update dxcallsign, dxgrid, current_timestamp, datatype, snr, offset, frequency, distance_km, distance_miles
]
# Check if the buffer is empty or if the callsign is not already in the list
if not any(dxcallsign == station[0] for station in heard_stations_list):
heard_stations_list.append(new_entry)
else: else:
for i in range(len(heard_stations_list)): # Search for the existing entry and update
# Update callsign with new timestamp for i, entry in enumerate(heard_stations_list):
if heard_stations_list[i].count(dxcallsign) > 0: if entry[0] == dxcallsign:
heard_stations_list[i] = [ heard_stations_list[i] = new_entry
dxcallsign,
dxgrid,
int(time.time()),
datatype,
snr,
offset,
frequency,
]
break break
# Insert if nothing found
if i == len(heard_stations_list) - 1:
heard_stations_list.append(
[
dxcallsign,
dxgrid,
int(time.time()),
datatype,
snr,
offset,
frequency,
]
)
break
# for idx, item in enumerate(heard_stations_list):
# if dxcallsign in item:
# item = [dxcallsign, int(time.time())]
# heard_stations_list[idx] = item
def callsign_to_bytes(callsign: str) -> bytes: def callsign_to_bytes(callsign: str) -> bytes:
@ -701,9 +688,73 @@ def set_flag(byte, flag_name, value, flag_dict):
position = flag_dict[flag_name] position = flag_dict[flag_name]
return set_bit(byte, position, value) return set_bit(byte, position, value)
def get_flag(byte, flag_name, flag_dict): def get_flag(byte, flag_name, flag_dict):
"""Get the value of the flag from the byte according to the flag dictionary.""" """Get the value of the flag from the byte according to the flag dictionary."""
if flag_name not in flag_dict: if flag_name not in flag_dict:
raise ValueError(f"Unknown flag name: {flag_name}") raise ValueError(f"Unknown flag name: {flag_name}")
position = flag_dict[flag_name] position = flag_dict[flag_name]
return get_bit(byte, position) return get_bit(byte, position)
def find_binary_paths(binary_name="rigctld", search_system_wide=False):
"""
Search for a binary within the current working directory, its subdirectories, and optionally,
system-wide locations and the PATH environment variable.
:param binary_name: The base name of the binary to search for, without extension.
:param search_system_wide: Boolean flag to enable or disable system-wide search.
:return: A list of full paths to the binary if found, otherwise an empty list.
"""
binary_paths = [] # Initialize an empty list to store found paths
# Adjust binary name for Windows
if platform.system() == 'Windows':
binary_name += ".exe"
# Search in the current working directory and subdirectories
root_path = os.getcwd()
for dirpath, dirnames, filenames in os.walk(root_path):
if binary_name in filenames:
binary_paths.append(os.path.join(dirpath, binary_name))
# If system-wide search is enabled, look in system locations and PATH
if search_system_wide:
system_paths = os.environ.get('PATH', '').split(os.pathsep)
# Optionally add common binary locations for Unix-like and Windows systems
if platform.system() != 'Windows':
system_paths.extend(['/usr/bin', '/usr/local/bin', '/bin'])
else:
system_paths.extend(glob.glob("C:\\Program Files\\Hamlib*\\bin"))
system_paths.extend(glob.glob("C:\\Program Files (x86)\\Hamlib*\\bin"))
for path in system_paths:
potential_path = os.path.join(path, binary_name)
if os.path.isfile(potential_path):
binary_paths.append(potential_path)
return binary_paths
def kill_and_execute(binary_path, additional_args=None):
"""
Kills any running instances of the binary across Linux, macOS, and Windows, then starts a new one non-blocking.
:param binary_path: The full path to the binary to execute.
:param additional_args: A list of additional arguments to pass to the binary.
:return: subprocess.Popen object of the started process
"""
# Kill any existing instances of the binary
for proc in psutil.process_iter(attrs=['pid', 'name', 'cmdline']):
try:
cmdline = proc.info['cmdline']
# Ensure cmdline is iterable and not None
if cmdline and binary_path in ' '.join(cmdline):
proc.kill()
print(f"Killed running instance with PID: {proc.info['pid']}")
except (psutil.NoSuchProcess, psutil.AccessDenied, psutil.ZombieProcess):
pass # Process no longer exists or no permission to kill
# Execute the binary with additional arguments non-blocking
command = [binary_path] + (additional_args if additional_args else [])
return subprocess.Popen(command)

94
modem/maidenhead.py Normal file
View file

@ -0,0 +1,94 @@
import math
def haversine(lat1, lon1, lat2, lon2):
"""
Calculate the great circle distance in kilometers between two points
on the Earth (specified in decimal degrees).
Parameters:
lat1, lon1: Latitude and longitude of point 1.
lat2, lon2: Latitude and longitude of point 2.
Returns:
float: Distance between the two points in kilometers.
"""
# Radius of the Earth in kilometers. Use 3956 for miles
R = 6371.0
# Convert latitude and longitude from degrees to radians
lat1 = math.radians(lat1)
lon1 = math.radians(lon1)
lat2 = math.radians(lat2)
lon2 = math.radians(lon2)
# Difference in coordinates
dlon = lon2 - lon1
dlat = lat2 - lat1
# Haversine formula
a = math.sin(dlat / 2) ** 2 + math.cos(lat1) * math.cos(lat2) * math.sin(dlon / 2) ** 2
c = 2 * math.atan2(math.sqrt(a), math.sqrt(1 - a))
distance = R * c
return distance
def maidenhead_to_latlon(grid_square):
"""
Convert a Maidenhead locator to latitude and longitude coordinates.
The output coordinates represent the southwestern corner of the grid square.
Parameters:
grid_square (str): The Maidenhead locator.
Returns:
tuple: A tuple containing the latitude and longitude (in that order) of the grid square's center.
"""
if len(grid_square) < 4 or len(grid_square) % 2 != 0:
raise ValueError("Grid square must be at least 4 characters long and an even length.")
grid_square = grid_square.upper()
lon = -180 + (ord(grid_square[0]) - ord('A')) * 20
lat = -90 + (ord(grid_square[1]) - ord('A')) * 10
lon += (int(grid_square[2]) * 2)
lat += int(grid_square[3])
if len(grid_square) >= 6:
lon += (ord(grid_square[4]) - ord('A')) * (5 / 60)
lat += (ord(grid_square[5]) - ord('A')) * (2.5 / 60)
if len(grid_square) == 8:
lon += int(grid_square[6]) * (5 / 600)
lat += int(grid_square[7]) * (2.5 / 600)
# Adjust to the center of the grid square
if len(grid_square) <= 4:
lon += 1
lat += 0.5
elif len(grid_square) == 6:
lon += 2.5 / 60
lat += 1.25 / 60
else:
lon += 2.5 / 600
lat += 1.25 / 600
return lat, lon
def distance_between_locators(locator1, locator2):
"""
Calculate the distance between two Maidenhead locators and return the result as a dictionary.
Parameters:
locator1 (str): The first Maidenhead locator.
locator2 (str): The second Maidenhead locator.
Returns:
dict: A dictionary containing the distances in kilometers and miles.
"""
lat1, lon1 = maidenhead_to_latlon(locator1)
lat2, lon2 = maidenhead_to_latlon(locator2)
km = haversine(lat1, lon1, lat2, lon2)
miles = km * 0.621371
return {'kilometers': km, 'miles': miles}

View file

@ -7,23 +7,28 @@ from message_system_db_messages import DatabaseManagerMessages
#import command_message_send #import command_message_send
def message_received(event_manager, state_manager, data): def message_received(event_manager, state_manager, data, statistics):
decompressed_json_string = data.decode('utf-8') decompressed_json_string = data.decode('utf-8')
received_message_obj = MessageP2P.from_payload(decompressed_json_string) received_message_obj = MessageP2P.from_payload(decompressed_json_string)
received_message_dict = MessageP2P.to_dict(received_message_obj) received_message_dict = MessageP2P.to_dict(received_message_obj)
DatabaseManagerMessages(event_manager).add_message(received_message_dict, direction='receive', status='received', is_read=False) DatabaseManagerMessages(event_manager).add_message(received_message_dict, statistics, direction='receive', status='received', is_read=False)
def message_transmitted(event_manager, state_manager, data): def message_transmitted(event_manager, state_manager, data, statistics):
decompressed_json_string = data.decode('utf-8') decompressed_json_string = data.decode('utf-8')
payload_message_obj = MessageP2P.from_payload(decompressed_json_string) payload_message_obj = MessageP2P.from_payload(decompressed_json_string)
payload_message = MessageP2P.to_dict(payload_message_obj) payload_message = MessageP2P.to_dict(payload_message_obj)
# Todo we need to optimize this - WIP
DatabaseManagerMessages(event_manager).update_message(payload_message["id"], update_data={'status': 'transmitted'}) DatabaseManagerMessages(event_manager).update_message(payload_message["id"], update_data={'status': 'transmitted'})
DatabaseManagerMessages(event_manager).update_message(payload_message["id"], update_data={'statistics': statistics})
def message_failed(event_manager, state_manager, data):
def message_failed(event_manager, state_manager, data, statistics):
decompressed_json_string = data.decode('utf-8') decompressed_json_string = data.decode('utf-8')
payload_message_obj = MessageP2P.from_payload(decompressed_json_string) payload_message_obj = MessageP2P.from_payload(decompressed_json_string)
payload_message = MessageP2P.to_dict(payload_message_obj) payload_message = MessageP2P.to_dict(payload_message_obj)
# Todo we need to optimize this - WIP
DatabaseManagerMessages(event_manager).update_message(payload_message["id"], update_data={'status': 'failed'}) DatabaseManagerMessages(event_manager).update_message(payload_message["id"], update_data={'status': 'failed'})
DatabaseManagerMessages(event_manager).update_message(payload_message["id"], update_data={'statistics': statistics})
class MessageP2P: class MessageP2P:
def __init__(self, id: str, origin: str, destination: str, body: str, attachments: list) -> None: def __init__(self, id: str, origin: str, destination: str, body: str, attachments: list) -> None:

View file

@ -2,12 +2,16 @@ from message_system_db_manager import DatabaseManager
from message_system_db_model import MessageAttachment, Attachment, P2PMessage from message_system_db_model import MessageAttachment, Attachment, P2PMessage
import json import json
import hashlib import hashlib
import os
class DatabaseManagerAttachments(DatabaseManager): class DatabaseManagerAttachments(DatabaseManager):
def __init__(self, uri='sqlite:///freedata-messages.db'): def __init__(self, db_file=None):
super().__init__(uri) if not db_file:
script_dir = os.path.dirname(os.path.abspath(__file__))
db_path = os.path.join(script_dir, 'freedata-messages.db')
db_file = 'sqlite:///' + db_path
super().__init__(db_file)
def add_attachment(self, session, message, attachment_data): def add_attachment(self, session, message, attachment_data):

View file

@ -4,14 +4,16 @@ from sqlalchemy.orm import scoped_session, sessionmaker
from threading import local from threading import local
from message_system_db_model import Base, Beacon, Station, Status, Attachment, P2PMessage from message_system_db_model import Base, Beacon, Station, Status, Attachment, P2PMessage
from datetime import timezone, timedelta, datetime from datetime import timezone, timedelta, datetime
import json import os
import structlog
import helpers
class DatabaseManagerBeacon(DatabaseManager): class DatabaseManagerBeacon(DatabaseManager):
def __init__(self, uri): def __init__(self, db_file=None):
super().__init__(uri) if not db_file:
script_dir = os.path.dirname(os.path.abspath(__file__))
db_path = os.path.join(script_dir, 'freedata-messages.db')
db_file = 'sqlite:///' + db_path
super().__init__(db_file)
def add_beacon(self, timestamp, callsign, snr, gridsquare): def add_beacon(self, timestamp, callsign, snr, gridsquare):
session = None session = None

View file

@ -7,12 +7,17 @@ from threading import local
from message_system_db_model import Base, Station, Status from message_system_db_model import Base, Station, Status
import structlog import structlog
import helpers import helpers
import os
class DatabaseManager: class DatabaseManager:
def __init__(self, event_manger, uri='sqlite:///freedata-messages.db'): def __init__(self, event_manger, db_file=None):
self.event_manager = event_manger self.event_manager = event_manger
if not db_file:
script_dir = os.path.dirname(os.path.abspath(__file__))
db_path = os.path.join(script_dir, 'freedata-messages.db')
db_file = 'sqlite:///' + db_path
self.engine = create_engine(uri, echo=False) self.engine = create_engine(db_file, echo=False)
self.thread_local = local() self.thread_local = local()
self.session_factory = sessionmaker(bind=self.engine) self.session_factory = sessionmaker(bind=self.engine)
Base.metadata.create_all(self.engine) Base.metadata.create_all(self.engine)

View file

@ -4,14 +4,20 @@ from message_system_db_model import Status, P2PMessage
from sqlalchemy.exc import IntegrityError from sqlalchemy.exc import IntegrityError
from datetime import datetime from datetime import datetime
import json import json
import os
class DatabaseManagerMessages(DatabaseManager): class DatabaseManagerMessages(DatabaseManager):
def __init__(self, uri='sqlite:///freedata-messages.db'): def __init__(self, db_file=None):
super().__init__(uri) if not db_file:
self.attachments_manager = DatabaseManagerAttachments(uri) script_dir = os.path.dirname(os.path.abspath(__file__))
db_path = os.path.join(script_dir, 'freedata-messages.db')
db_file = 'sqlite:///' + db_path
def add_message(self, message_data, direction='receive', status=None, is_read=True): super().__init__(db_file)
self.attachments_manager = DatabaseManagerAttachments(db_file)
def add_message(self, message_data, statistics, direction='receive', status=None, is_read=True):
session = self.get_thread_scoped_session() session = self.get_thread_scoped_session()
try: try:
# Create and add the origin and destination Stations # Create and add the origin and destination Stations
@ -34,7 +40,8 @@ class DatabaseManagerMessages(DatabaseManager):
direction=direction, direction=direction,
status_id=status.id if status else None, status_id=status.id if status else None,
is_read=is_read, is_read=is_read,
attempt=0 attempt=0,
statistics=statistics
) )
session.add(new_message) session.add(new_message)
@ -130,6 +137,8 @@ class DatabaseManagerMessages(DatabaseManager):
message.body = update_data['body'] message.body = update_data['body']
if 'status' in update_data: if 'status' in update_data:
message.status = self.get_or_create_status(session, update_data['status']) message.status = self.get_or_create_status(session, update_data['status'])
if 'statistics' in update_data:
message.statistics = update_data['statistics']
session.commit() session.commit()
self.log(f"Updated: {message_id}") self.log(f"Updated: {message_id}")
@ -171,21 +180,27 @@ class DatabaseManagerMessages(DatabaseManager):
finally: finally:
session.remove() session.remove()
def increment_message_attempts(self, message_id): def increment_message_attempts(self, message_id, session=None):
session = self.get_thread_scoped_session() own_session = False
if not session:
session = self.get_thread_scoped_session()
own_session = True
try: try:
message = session.query(P2PMessage).filter_by(id=message_id).first() message = session.query(P2PMessage).filter_by(id=message_id).first()
if message: if message:
message.attempt += 1 message.attempt += 1
session.commit() if own_session:
session.commit()
self.log(f"Incremented attempt count for message {message_id}") self.log(f"Incremented attempt count for message {message_id}")
else: else:
self.log(f"Message with ID {message_id} not found") self.log(f"Message with ID {message_id} not found")
except Exception as e: except Exception as e:
session.rollback() if own_session:
session.rollback()
self.log(f"An error occurred while incrementing attempts for message {message_id}: {e}") self.log(f"An error occurred while incrementing attempts for message {message_id}: {e}")
finally: finally:
session.remove() if own_session:
session.remove()
def mark_message_as_read(self, message_id): def mark_message_as_read(self, message_id):
session = self.get_thread_scoped_session() session = self.get_thread_scoped_session()
@ -201,4 +216,42 @@ class DatabaseManagerMessages(DatabaseManager):
session.rollback() session.rollback()
self.log(f"An error occurred while marking message {message_id} as read: {e}") self.log(f"An error occurred while marking message {message_id} as read: {e}")
finally: finally:
session.remove() session.remove()
def set_message_to_queued_for_callsign(self, callsign):
session = self.get_thread_scoped_session()
try:
# Find the 'failed' status object
failed_status = session.query(Status).filter_by(name='failed').first()
# Find the 'queued' status object
queued_status = session.query(Status).filter_by(name='queued').first()
# Ensure both statuses are found
if not failed_status or not queued_status:
self.log("Failed or queued status not found", isWarning=True)
return
# Query for messages with the specified callsign, 'failed' status, and fewer than 10 attempts
message = session.query(P2PMessage) \
.filter(P2PMessage.destination_callsign == callsign) \
.filter(P2PMessage.status_id == failed_status.id) \
.filter(P2PMessage.attempt < 10) \
.first()
if message:
# Increment attempt count using the existing function
self.increment_message_attempts(message.id, session)
message.status_id = queued_status.id
self.log(f"Set message {message.id} to queued and incremented attempt")
session.commit()
return {'status': 'success', 'message': f'{len(message)} message(s) set to queued'}
else:
return {'status': 'failure', 'message': 'No eligible messages found'}
except Exception as e:
session.rollback()
self.log(f"An error occurred while setting messages to queued: {e}", isWarning=True)
return {'status': 'failure', 'message': str(e)}
finally:
session.remove()

View file

@ -9,10 +9,7 @@ Created on Wed Dec 23 07:04:24 2020
# pylint: disable=invalid-name, line-too-long, c-extension-no-member # pylint: disable=invalid-name, line-too-long, c-extension-no-member
# pylint: disable=import-outside-toplevel # pylint: disable=import-outside-toplevel
import atexit
import ctypes
import queue import queue
import threading
import time import time
import codec2 import codec2
import numpy as np import numpy as np
@ -20,9 +17,9 @@ import sounddevice as sd
import structlog import structlog
import tci import tci
import cw import cw
from queues import RIGCTLD_COMMAND_QUEUE
import audio import audio
import demodulator import demodulator
import modulator
TESTMODE = False TESTMODE = False
@ -44,50 +41,52 @@ class RF:
self.audio_input_device = config['AUDIO']['input_device'] self.audio_input_device = config['AUDIO']['input_device']
self.audio_output_device = config['AUDIO']['output_device'] self.audio_output_device = config['AUDIO']['output_device']
self.tx_audio_level = config['AUDIO']['tx_audio_level']
self.enable_audio_auto_tune = config['AUDIO']['enable_auto_tune']
self.tx_delay = config['MODEM']['tx_delay']
self.radiocontrol = config['RADIO']['control'] self.radiocontrol = config['RADIO']['control']
self.rigctld_ip = config['RIGCTLD']['ip'] self.rigctld_ip = config['RIGCTLD']['ip']
self.rigctld_port = config['RIGCTLD']['port'] self.rigctld_port = config['RIGCTLD']['port']
self.states.setTransmitting(False)
self.ptt_state = False
self.radio_alc = 0.0
self.tci_ip = config['TCI']['tci_ip'] self.tci_ip = config['TCI']['tci_ip']
self.tci_port = config['TCI']['tci_port'] self.tci_port = config['TCI']['tci_port']
self.tx_audio_level = config['AUDIO']['tx_audio_level']
self.rx_audio_level = config['AUDIO']['rx_audio_level']
self.ptt_state = False
self.enqueuing_audio = False # set to True, while we are processing audio
self.AUDIO_SAMPLE_RATE = 48000 self.AUDIO_SAMPLE_RATE = 48000
self.MODEM_SAMPLE_RATE = codec2.api.FREEDV_FS_8000 self.modem_sample_rate = codec2.api.FREEDV_FS_8000
# 8192 Let's do some tests with very small chunks for TX # 8192 Let's do some tests with very small chunks for TX
self.AUDIO_FRAMES_PER_BUFFER_TX = 1200 if self.radiocontrol in ["tci"] else 2400 * 2 #self.AUDIO_FRAMES_PER_BUFFER_TX = 1200 if self.radiocontrol in ["tci"] else 2400 * 2
# 8 * (self.AUDIO_SAMPLE_RATE/self.MODEM_SAMPLE_RATE) == 48 # 8 * (self.AUDIO_SAMPLE_RATE/self.modem_sample_rate) == 48
self.AUDIO_CHANNELS = 1 self.AUDIO_CHANNELS = 1
self.MODE = 0 self.MODE = 0
self.rms_counter = 0 self.rms_counter = 0
# Make sure our resampler will work self.audio_out_queue = queue.Queue()
assert (self.AUDIO_SAMPLE_RATE / self.MODEM_SAMPLE_RATE) == codec2.api.FDMDV_OS_48 # type: ignore
# Make sure our resampler will work
assert (self.AUDIO_SAMPLE_RATE / self.modem_sample_rate) == codec2.api.FDMDV_OS_48 # type: ignore
self.modem_received_queue = queue.Queue()
self.audio_received_queue = queue.Queue() self.audio_received_queue = queue.Queue()
self.data_queue_received = queue.Queue() self.data_queue_received = queue.Queue()
self.fft_queue = fft_queue self.fft_queue = fft_queue
self.demodulator = demodulator.Demodulator(self.config, self.demodulator = demodulator.Demodulator(self.config,
self.audio_received_queue, self.audio_received_queue,
self.modem_received_queue,
self.data_queue_received, self.data_queue_received,
self.states, self.states,
self.event_manager, self.event_manager,
self.service_queue,
self.fft_queue self.fft_queue
) )
self.modulator = modulator.Modulator(self.config)
def tci_tx_callback(self, audio_48k) -> None: def tci_tx_callback(self, audio_48k) -> None:
@ -105,11 +104,6 @@ class RF:
if not self.init_audio(): if not self.init_audio():
raise RuntimeError("Unable to init audio devices") raise RuntimeError("Unable to init audio devices")
self.demodulator.start(self.sd_input_stream) self.demodulator.start(self.sd_input_stream)
atexit.register(self.sd_input_stream.stop)
# Initialize codec2, rig control, and data threads
self.init_codec2()
self.init_data_threads()
return True return True
@ -155,17 +149,25 @@ class RF:
self.sd_input_stream = sd.InputStream( self.sd_input_stream = sd.InputStream(
channels=1, channels=1,
dtype="int16", dtype="int16",
callback=self.demodulator.sd_input_audio_callback, callback=self.sd_input_audio_callback,
device=in_dev_index, device=in_dev_index,
samplerate=self.AUDIO_SAMPLE_RATE, samplerate=self.AUDIO_SAMPLE_RATE,
blocksize=4800, blocksize=4800,
) )
self.sd_input_stream.start() self.sd_input_stream.start()
self.sd_output_stream = sd.OutputStream(
channels=1,
dtype="int16",
callback=self.sd_output_audio_callback,
device=out_dev_index,
samplerate=self.AUDIO_SAMPLE_RATE,
blocksize=4800,
)
self.sd_output_stream.start()
return True return True
except Exception as audioerr: except Exception as audioerr:
self.log.error("[MDM] init: starting pyaudio callback failed", e=audioerr) self.log.error("[MDM] init: starting pyaudio callback failed", e=audioerr)
self.stop_modem() self.stop_modem()
@ -188,191 +190,7 @@ class RF:
return True return True
def audio_auto_tune(self):
# enable / disable AUDIO TUNE Feature / ALC correction
if self.enable_audio_auto_tune:
if self.radio_alc == 0.0:
self.tx_audio_level = self.tx_audio_level + 20
elif 0.0 < self.radio_alc <= 0.1:
print("0.0 < self.radio_alc <= 0.1")
self.tx_audio_level = self.tx_audio_level + 2
self.log.debug("[MDM] AUDIO TUNE", audio_level=str(self.tx_audio_level),
alc_level=str(self.radio_alc))
elif 0.1 < self.radio_alc < 0.2:
print("0.1 < self.radio_alc < 0.2")
self.tx_audio_level = self.tx_audio_level
self.log.debug("[MDM] AUDIO TUNE", audio_level=str(self.tx_audio_level),
alc_level=str(self.radio_alc))
elif 0.2 < self.radio_alc < 0.99:
print("0.2 < self.radio_alc < 0.99")
self.tx_audio_level = self.tx_audio_level - 20
self.log.debug("[MDM] AUDIO TUNE", audio_level=str(self.tx_audio_level),
alc_level=str(self.radio_alc))
elif 1.0 >= self.radio_alc:
print("1.0 >= self.radio_alc")
self.tx_audio_level = self.tx_audio_level - 40
self.log.debug("[MDM] AUDIO TUNE", audio_level=str(self.tx_audio_level),
alc_level=str(self.radio_alc))
else:
self.log.debug("[MDM] AUDIO TUNE", audio_level=str(self.tx_audio_level),
alc_level=str(self.radio_alc))
def transmit(
self, mode, repeats: int, repeat_delay: int, frames: bytearray
) -> bool:
"""
Args:
mode:
repeats:
repeat_delay:
frames:
"""
if TESTMODE:
return
self.demodulator.reset_data_sync()
# get freedv instance by mode
mode_transition = {
codec2.FREEDV_MODE.signalling: self.freedv_datac13_tx,
codec2.FREEDV_MODE.datac0: self.freedv_datac0_tx,
codec2.FREEDV_MODE.datac1: self.freedv_datac1_tx,
codec2.FREEDV_MODE.datac3: self.freedv_datac3_tx,
codec2.FREEDV_MODE.datac4: self.freedv_datac4_tx,
codec2.FREEDV_MODE.datac13: self.freedv_datac13_tx,
}
if mode in mode_transition:
freedv = mode_transition[mode]
else:
print("wrong mode.................")
print(mode)
return False
# Wait for some other thread that might be transmitting
self.states.waitForTransmission()
self.states.setTransmitting(True)
#self.states.channel_busy_event.wait()
start_of_transmission = time.time()
# Open codec2 instance
self.MODE = mode
txbuffer = bytes()
# Add empty data to handle ptt toggle time
if self.tx_delay > 0:
self.transmit_add_silence(txbuffer, self.tx_delay)
self.log.debug(
"[MDM] TRANSMIT", mode=self.MODE.name, delay=self.tx_delay
)
if not isinstance(frames, list): frames = [frames]
for _ in range(repeats):
# Create modulation for all frames in the list
for frame in frames:
txbuffer = self.transmit_add_preamble(txbuffer, freedv)
txbuffer = self.transmit_create_frame(txbuffer, freedv, frame)
txbuffer = self.transmit_add_postamble(txbuffer, freedv)
# Add delay to end of frames
self.transmit_add_silence(txbuffer, repeat_delay)
# Re-sample back up to 48k (resampler works on np.int16)
x = np.frombuffer(txbuffer, dtype=np.int16)
self.audio_auto_tune()
x = audio.set_audio_volume(x, self.tx_audio_level)
if self.radiocontrol not in ["tci"]:
txbuffer_out = self.resampler.resample8_to_48(x)
else:
txbuffer_out = x
# transmit audio
self.transmit_audio(txbuffer_out)
self.radio.set_ptt(False)
self.event_manager.send_ptt_change(False)
self.states.setTransmitting(False)
end_of_transmission = time.time()
transmission_time = end_of_transmission - start_of_transmission
self.log.debug("[MDM] ON AIR TIME", time=transmission_time)
return True
def transmit_add_preamble(self, buffer, freedv):
# Init buffer for preample
n_tx_preamble_modem_samples = codec2.api.freedv_get_n_tx_preamble_modem_samples(
freedv
)
mod_out_preamble = ctypes.create_string_buffer(n_tx_preamble_modem_samples * 2)
# Write preamble to txbuffer
codec2.api.freedv_rawdatapreambletx(freedv, mod_out_preamble)
buffer += bytes(mod_out_preamble)
return buffer
def transmit_add_postamble(self, buffer, freedv):
# Init buffer for postamble
n_tx_postamble_modem_samples = (
codec2.api.freedv_get_n_tx_postamble_modem_samples(freedv)
)
mod_out_postamble = ctypes.create_string_buffer(
n_tx_postamble_modem_samples * 2
)
# Write postamble to txbuffer
codec2.api.freedv_rawdatapostambletx(freedv, mod_out_postamble)
# Append postamble to txbuffer
buffer += bytes(mod_out_postamble)
return buffer
def transmit_add_silence(self, buffer, duration):
data_delay = int(self.MODEM_SAMPLE_RATE * (duration / 1000)) # type: ignore
mod_out_silence = ctypes.create_string_buffer(data_delay * 2)
buffer += bytes(mod_out_silence)
return buffer
def transmit_create_frame(self, txbuffer, freedv, frame):
# Get number of bytes per frame for mode
bytes_per_frame = int(codec2.api.freedv_get_bits_per_modem_frame(freedv) / 8)
payload_bytes_per_frame = bytes_per_frame - 2
# Init buffer for data
n_tx_modem_samples = codec2.api.freedv_get_n_tx_modem_samples(freedv)
mod_out = ctypes.create_string_buffer(n_tx_modem_samples * 2)
# Create buffer for data
# Use this if CRC16 checksum is required (DATAc1-3)
buffer = bytearray(payload_bytes_per_frame)
# Set buffersize to length of data which will be send
buffer[: len(frame)] = frame # type: ignore
# Create crc for data frame -
# Use the crc function shipped with codec2
# to avoid CRC algorithm incompatibilities
# Generate CRC16
crc = ctypes.c_ushort(
codec2.api.freedv_gen_crc16(bytes(buffer), payload_bytes_per_frame)
)
# Convert crc to 2-byte (16-bit) hex string
crc = crc.value.to_bytes(2, byteorder="big")
# Append CRC to data buffer
buffer += crc
assert (bytes_per_frame == len(buffer))
data = (ctypes.c_ubyte * bytes_per_frame).from_buffer_copy(buffer)
# modulate DATA and save it into mod_out pointer
codec2.api.freedv_rawdatatx(freedv, mod_out, data)
txbuffer += bytes(mod_out)
return txbuffer
def transmit_morse(self, repeats, repeat_delay, frames): def transmit_morse(self, repeats, repeat_delay, frames):
self.states.waitForTransmission() self.states.waitForTransmission()
@ -383,36 +201,54 @@ class RF:
) )
start_of_transmission = time.time() start_of_transmission = time.time()
txbuffer_out = cw.MorseCodePlayer().text_to_signal("DJ2LS-1") txbuffer_out = cw.MorseCodePlayer().text_to_signal(self.config['STATION'].mycall)
self.transmit_audio(txbuffer_out) # transmit audio
self.radio.set_ptt(False) self.enqueue_audio_out(txbuffer_out)
self.event_manager.send_ptt_change(False)
self.states.setTransmitting(False)
end_of_transmission = time.time() end_of_transmission = time.time()
transmission_time = end_of_transmission - start_of_transmission transmission_time = end_of_transmission - start_of_transmission
self.log.debug("[MDM] ON AIR TIME", time=transmission_time) self.log.debug("[MDM] ON AIR TIME", time=transmission_time)
def init_codec2(self):
# Open codec2 instances
# INIT TX MODES - here we need all modes. def transmit(
self.freedv_datac0_tx = codec2.open_instance(codec2.FREEDV_MODE.datac0.value) self, mode, repeats: int, repeat_delay: int, frames: bytearray
self.freedv_datac1_tx = codec2.open_instance(codec2.FREEDV_MODE.datac1.value) ) -> bool:
self.freedv_datac3_tx = codec2.open_instance(codec2.FREEDV_MODE.datac3.value)
self.freedv_datac4_tx = codec2.open_instance(codec2.FREEDV_MODE.datac4.value)
self.freedv_datac13_tx = codec2.open_instance(codec2.FREEDV_MODE.datac13.value)
def init_data_threads(self): self.demodulator.reset_data_sync()
worker_received = threading.Thread(
target=self.demodulator.worker_received, name="WORKER_THREAD", daemon=True # Wait for some other thread that might be transmitting
) self.states.waitForTransmission()
worker_received.start() self.states.setTransmitting(True)
# self.states.channel_busy_event.wait()
start_of_transmission = time.time()
txbuffer = self.modulator.create_burst(mode, repeats, repeat_delay, frames)
# Re-sample back up to 48k (resampler works on np.int16)
x = np.frombuffer(txbuffer, dtype=np.int16)
x = audio.set_audio_volume(x, self.tx_audio_level)
if self.radiocontrol not in ["tci"]:
txbuffer_out = self.resampler.resample8_to_48(x)
else:
txbuffer_out = x
# transmit audio
self.enqueue_audio_out(txbuffer_out)
end_of_transmission = time.time()
transmission_time = end_of_transmission - start_of_transmission
self.log.debug("[MDM] ON AIR TIME", time=transmission_time)
def enqueue_audio_out(self, audio_48k) -> None:
self.enqueuing_audio = True
if not self.states.isTransmitting():
self.states.setTransmitting(True)
# Low level modem audio transmit
def transmit_audio(self, audio_48k) -> None:
self.radio.set_ptt(True) self.radio.set_ptt(True)
self.event_manager.send_ptt_change(True) self.event_manager.send_ptt_change(True)
@ -421,5 +257,72 @@ class RF:
# we need to wait manually for tci processing # we need to wait manually for tci processing
self.tci_module.wait_until_transmitted(audio_48k) self.tci_module.wait_until_transmitted(audio_48k)
else: else:
sd.play(audio_48k, blocksize=4096, blocking=True) # slice audio data to needed blocklength
block_size = 4800
pad_length = -len(audio_48k) % block_size
padded_data = np.pad(audio_48k, (0, pad_length), mode='constant')
sliced_audio_data = padded_data.reshape(-1, block_size)
# add each block to audio out queue
for block in sliced_audio_data:
self.audio_out_queue.put(block)
self.enqueuing_audio = False
self.states.transmitting_event.wait()
self.radio.set_ptt(False)
self.event_manager.send_ptt_change(False)
return return
def sd_output_audio_callback(self, outdata: np.ndarray, frames: int, time, status) -> None:
try:
if not self.audio_out_queue.empty():
chunk = self.audio_out_queue.get_nowait()
audio.calculate_fft(chunk, self.fft_queue, self.states)
outdata[:] = chunk.reshape(outdata.shape)
else:
# reset transmitting state only, if we are not actively processing audio
# for avoiding a ptt toggle state bug
if not self.enqueuing_audio:
self.states.setTransmitting(False)
# Fill with zeros if the queue is empty
outdata.fill(0)
except Exception as e:
self.log.warning("[AUDIO STATUS]", status=status, time=time, frames=frames, e=e)
outdata.fill(0)
def sd_input_audio_callback(self, indata: np.ndarray, frames: int, time, status) -> None:
if status:
self.log.warning("[AUDIO STATUS]", status=status, time=time, frames=frames)
# FIXME on windows input overflows crashing the rx audio stream. Lets restart the server then
#if status.input_overflow:
# self.service_queue.put("restart")
return
try:
audio_48k = np.frombuffer(indata, dtype=np.int16)
audio_8k = self.resampler.resample48_to_8(audio_48k)
audio_8k_level_adjusted = audio.set_audio_volume(audio_8k, self.rx_audio_level)
if not self.states.isTransmitting():
audio.calculate_fft(audio_8k_level_adjusted, self.fft_queue, self.states)
length_audio_8k_level_adjusted = len(audio_8k_level_adjusted)
# Avoid buffer overflow by filling only if buffer for
# selected datachannel mode is not full
index = 0
for mode in self.demodulator.MODE_DICT:
mode_data = self.demodulator.MODE_DICT[mode]
audiobuffer = mode_data['audio_buffer']
decode = mode_data['decode']
index += 1
if audiobuffer:
if (audiobuffer.nbuffer + length_audio_8k_level_adjusted) > audiobuffer.size:
self.demodulator.buffer_overflow_counter[index] += 1
self.event_manager.send_buffer_overflow(self.demodulator.buffer_overflow_counter)
elif decode:
audiobuffer.push(audio_8k_level_adjusted)
except Exception as e:
self.log.warning("[AUDIO EXCEPTION]", status=status, time=time, frames=frames, e=e)

View file

@ -6,9 +6,6 @@ from enum import Enum
class FRAME_TYPE(Enum): class FRAME_TYPE(Enum):
"""Lookup for frame types""" """Lookup for frame types"""
ARQ_CONNECTION_OPEN = 1
ARQ_CONNECTION_HB = 2
ARQ_CONNECTION_CLOSE = 3
ARQ_STOP = 10 ARQ_STOP = 10
ARQ_STOP_ACK = 11 ARQ_STOP_ACK = 11
ARQ_SESSION_OPEN = 12 ARQ_SESSION_OPEN = 12
@ -17,6 +14,14 @@ class FRAME_TYPE(Enum):
ARQ_SESSION_INFO_ACK = 15 ARQ_SESSION_INFO_ACK = 15
ARQ_BURST_FRAME = 20 ARQ_BURST_FRAME = 20
ARQ_BURST_ACK = 21 ARQ_BURST_ACK = 21
P2P_CONNECTION_CONNECT = 30
P2P_CONNECTION_CONNECT_ACK = 31
P2P_CONNECTION_HEARTBEAT = 32
P2P_CONNECTION_HEARTBEAT_ACK = 33
P2P_CONNECTION_PAYLOAD = 34
P2P_CONNECTION_PAYLOAD_ACK = 35
P2P_CONNECTION_DISCONNECT = 36
P2P_CONNECTION_DISCONNECT_ACK = 37
MESH_BROADCAST = 100 MESH_BROADCAST = 100
MESH_SIGNALLING_PING = 101 MESH_SIGNALLING_PING = 101
MESH_SIGNALLING_PING_ACK = 102 MESH_SIGNALLING_PING_ACK = 102

150
modem/modulator.py Normal file
View file

@ -0,0 +1,150 @@
import ctypes
import codec2
import structlog
class Modulator:
log = structlog.get_logger("RF")
def __init__(self, config):
self.config = config
self.tx_delay = config['MODEM']['tx_delay']
self.modem_sample_rate = codec2.api.FREEDV_FS_8000
# Initialize codec2, rig control, and data threads
self.init_codec2()
def init_codec2(self):
# Open codec2 instances
# INIT TX MODES - here we need all modes.
self.freedv_datac0_tx = codec2.open_instance(codec2.FREEDV_MODE.datac0.value)
self.freedv_datac1_tx = codec2.open_instance(codec2.FREEDV_MODE.datac1.value)
self.freedv_datac3_tx = codec2.open_instance(codec2.FREEDV_MODE.datac3.value)
self.freedv_datac4_tx = codec2.open_instance(codec2.FREEDV_MODE.datac4.value)
self.freedv_datac13_tx = codec2.open_instance(codec2.FREEDV_MODE.datac13.value)
def transmit_add_preamble(self, buffer, freedv):
# Init buffer for preample
n_tx_preamble_modem_samples = codec2.api.freedv_get_n_tx_preamble_modem_samples(
freedv
)
mod_out_preamble = ctypes.create_string_buffer(n_tx_preamble_modem_samples * 2)
# Write preamble to txbuffer
codec2.api.freedv_rawdatapreambletx(freedv, mod_out_preamble)
buffer += bytes(mod_out_preamble)
return buffer
def transmit_add_postamble(self, buffer, freedv):
# Init buffer for postamble
n_tx_postamble_modem_samples = (
codec2.api.freedv_get_n_tx_postamble_modem_samples(freedv)
)
mod_out_postamble = ctypes.create_string_buffer(
n_tx_postamble_modem_samples * 2
)
# Write postamble to txbuffer
codec2.api.freedv_rawdatapostambletx(freedv, mod_out_postamble)
# Append postamble to txbuffer
buffer += bytes(mod_out_postamble)
return buffer
def transmit_add_silence(self, buffer, duration):
data_delay = int(self.modem_sample_rate * (duration / 1000)) # type: ignore
mod_out_silence = ctypes.create_string_buffer(data_delay * 2)
buffer += bytes(mod_out_silence)
return buffer
def transmit_create_frame(self, txbuffer, freedv, frame):
# Get number of bytes per frame for mode
bytes_per_frame = int(codec2.api.freedv_get_bits_per_modem_frame(freedv) / 8)
payload_bytes_per_frame = bytes_per_frame - 2
# Init buffer for data
n_tx_modem_samples = codec2.api.freedv_get_n_tx_modem_samples(freedv)
mod_out = ctypes.create_string_buffer(n_tx_modem_samples * 2)
# Create buffer for data
# Use this if CRC16 checksum is required (DATAc1-3)
buffer = bytearray(payload_bytes_per_frame)
# Set buffersize to length of data which will be send
buffer[: len(frame)] = frame # type: ignore
# Create crc for data frame -
# Use the crc function shipped with codec2
# to avoid CRC algorithm incompatibilities
# Generate CRC16
crc = ctypes.c_ushort(
codec2.api.freedv_gen_crc16(bytes(buffer), payload_bytes_per_frame)
)
# Convert crc to 2-byte (16-bit) hex string
crc = crc.value.to_bytes(2, byteorder="big")
# Append CRC to data buffer
buffer += crc
assert (bytes_per_frame == len(buffer))
data = (ctypes.c_ubyte * bytes_per_frame).from_buffer_copy(buffer)
# modulate DATA and save it into mod_out pointer
codec2.api.freedv_rawdatatx(freedv, mod_out, data)
txbuffer += bytes(mod_out)
return txbuffer
def create_burst(
self, mode, repeats: int, repeat_delay: int, frames: bytearray
) -> bool:
"""
Args:
mode:
repeats:
repeat_delay:
frames:
"""
# get freedv instance by mode
mode_transition = {
codec2.FREEDV_MODE.signalling: self.freedv_datac13_tx,
codec2.FREEDV_MODE.datac0: self.freedv_datac0_tx,
codec2.FREEDV_MODE.datac1: self.freedv_datac1_tx,
codec2.FREEDV_MODE.datac3: self.freedv_datac3_tx,
codec2.FREEDV_MODE.datac4: self.freedv_datac4_tx,
codec2.FREEDV_MODE.datac13: self.freedv_datac13_tx,
}
if mode in mode_transition:
freedv = mode_transition[mode]
else:
print("wrong mode.................")
print(mode)
return False
# Open codec2 instance
self.MODE = mode
self.log.debug(
"[MDM] TRANSMIT", mode=self.MODE.name, delay=self.tx_delay
)
txbuffer = bytes()
# Add empty data to handle ptt toggle time
if self.tx_delay > 0:
txbuffer = self.transmit_add_silence(txbuffer, self.tx_delay)
if not isinstance(frames, list): frames = [frames]
for _ in range(repeats):
# Create modulation for all frames in the list
for frame in frames:
txbuffer = self.transmit_add_preamble(txbuffer, freedv)
txbuffer = self.transmit_create_frame(txbuffer, freedv, frame)
txbuffer = self.transmit_add_postamble(txbuffer, freedv)
# Add delay to end of frames
txbuffer = self.transmit_add_silence(txbuffer, repeat_delay)
return txbuffer

315
modem/p2p_connection.py Normal file
View file

@ -0,0 +1,315 @@
import threading
from enum import Enum
from modem_frametypes import FRAME_TYPE
from codec2 import FREEDV_MODE
import data_frame_factory
import structlog
import random
from queue import Queue
import time
from command_arq_raw import ARQRawCommand
import numpy as np
import base64
from arq_data_type_handler import ARQDataTypeHandler, ARQ_SESSION_TYPES
from arq_session_iss import ARQSessionISS
class States(Enum):
NEW = 0
CONNECTING = 1
CONNECT_SENT = 2
CONNECT_ACK_SENT = 3
CONNECTED = 4
#HEARTBEAT_SENT = 5
#HEARTBEAT_ACK_SENT = 6
PAYLOAD_SENT = 7
ARQ_SESSION = 8
DISCONNECTING = 9
DISCONNECTED = 10
FAILED = 11
class P2PConnection:
STATE_TRANSITION = {
States.NEW: {
FRAME_TYPE.P2P_CONNECTION_CONNECT.value: 'connected_irs',
},
States.CONNECTING: {
FRAME_TYPE.P2P_CONNECTION_CONNECT_ACK.value: 'connected_iss',
},
States.CONNECTED: {
FRAME_TYPE.P2P_CONNECTION_CONNECT.value: 'connected_irs',
FRAME_TYPE.P2P_CONNECTION_CONNECT_ACK.value: 'connected_iss',
FRAME_TYPE.P2P_CONNECTION_PAYLOAD.value: 'received_data',
FRAME_TYPE.P2P_CONNECTION_DISCONNECT.value: 'received_disconnect',
},
States.PAYLOAD_SENT: {
FRAME_TYPE.P2P_CONNECTION_PAYLOAD_ACK.value: 'transmitted_data',
},
States.DISCONNECTING: {
FRAME_TYPE.P2P_CONNECTION_DISCONNECT_ACK.value: 'received_disconnect_ack',
},
States.DISCONNECTED: {
FRAME_TYPE.P2P_CONNECTION_DISCONNECT.value: 'received_disconnect',
FRAME_TYPE.P2P_CONNECTION_DISCONNECT_ACK.value: 'received_disconnect_ack',
},
}
def __init__(self, config: dict, modem, origin: str, destination: str, state_manager, event_manager, socket_command_handler=None):
self.logger = structlog.get_logger(type(self).__name__)
self.config = config
self.frame_factory = data_frame_factory.DataFrameFactory(self.config)
self.socket_command_handler = socket_command_handler
self.destination = destination
self.origin = origin
self.bandwidth = 0
self.state_manager = state_manager
self.event_manager = event_manager
self.modem = modem
self.modem.demodulator.set_decode_mode()
self.p2p_data_rx_queue = Queue()
self.p2p_data_tx_queue = Queue()
self.arq_data_type_handler = ARQDataTypeHandler(self.event_manager, self.state_manager)
self.state = States.NEW
self.session_id = self.generate_id()
self.event_frame_received = threading.Event()
self.RETRIES_CONNECT = 5
self.TIMEOUT_CONNECT = 5
self.TIMEOUT_DATA = 5
self.RETRIES_DATA = 5
self.ENTIRE_CONNECTION_TIMEOUT = 100
self.is_ISS = False # Indicator, if we are ISS or IRS
self.last_data_timestamp= time.time()
self.start_data_processing_worker()
def start_data_processing_worker(self):
"""Starts a worker thread to monitor the transmit data queue and process data."""
def data_processing_worker():
while True:
if time.time() > self.last_data_timestamp + self.ENTIRE_CONNECTION_TIMEOUT and self.state is not States.ARQ_SESSION:
self.disconnect()
return
if not self.p2p_data_tx_queue.empty() and self.state == States.CONNECTED:
self.process_data_queue()
threading.Event().wait(0.1)
# Create and start the worker thread
worker_thread = threading.Thread(target=data_processing_worker, daemon=True)
worker_thread.start()
def generate_id(self):
while True:
random_int = random.randint(1,255)
if random_int not in self.state_manager.p2p_connection_sessions:
return random_int
if len(self.state_manager.p2p_connection_sessions) >= 255:
return False
def set_details(self, snr, frequency_offset):
self.snr = snr
self.frequency_offset = frequency_offset
def log(self, message, isWarning = False):
msg = f"[{type(self).__name__}][id={self.session_id}][state={self.state}][ISS={bool(self.is_ISS)}]: {message}"
logger = self.logger.warn if isWarning else self.logger.info
logger(msg)
def set_state(self, state):
if self.state == state:
self.log(f"{type(self).__name__} state {self.state.name} unchanged.")
else:
self.log(f"{type(self).__name__} state change from {self.state.name} to {state.name}")
self.state = state
def on_frame_received(self, frame):
self.last_data_timestamp = time.time()
self.event_frame_received.set()
self.log(f"Received {frame['frame_type']}")
frame_type = frame['frame_type_int']
if self.state in self.STATE_TRANSITION:
if frame_type in self.STATE_TRANSITION[self.state]:
action_name = self.STATE_TRANSITION[self.state][frame_type]
response = getattr(self, action_name)(frame)
return
self.log(f"Ignoring unknown transition from state {self.state.name} with frame {frame['frame_type']}")
def transmit_frame(self, frame: bytearray, mode='auto'):
self.log("Transmitting frame")
if mode in ['auto']:
mode = self.get_mode_by_speed_level(self.speed_level)
self.modem.transmit(mode, 1, 1, frame)
def transmit_wait_and_retry(self, frame_or_burst, timeout, retries, mode):
while retries > 0:
self.event_frame_received = threading.Event()
if isinstance(frame_or_burst, list): burst = frame_or_burst
else: burst = [frame_or_burst]
for f in burst:
self.transmit_frame(f, mode)
self.event_frame_received.clear()
self.log(f"Waiting {timeout} seconds...")
if self.event_frame_received.wait(timeout):
return
self.log("Timeout!")
retries = retries - 1
#self.connected_iss() # override connection state for simulation purposes
self.session_failed()
def launch_twr(self, frame_or_burst, timeout, retries, mode):
twr = threading.Thread(target = self.transmit_wait_and_retry, args=[frame_or_burst, timeout, retries, mode], daemon=True)
twr.start()
def transmit_and_wait_irs(self, frame, timeout, mode):
self.event_frame_received.clear()
self.transmit_frame(frame, mode)
self.log(f"Waiting {timeout} seconds...")
#if not self.event_frame_received.wait(timeout):
# self.log("Timeout waiting for ISS. Session failed.")
# self.transmission_failed()
def launch_twr_irs(self, frame, timeout, mode):
thread_wait = threading.Thread(target = self.transmit_and_wait_irs,
args = [frame, timeout, mode], daemon=True)
thread_wait.start()
def connect(self):
self.set_state(States.CONNECTING)
self.is_ISS = True
session_open_frame = self.frame_factory.build_p2p_connection_connect(self.origin, self.destination, self.session_id)
self.launch_twr(session_open_frame, self.TIMEOUT_CONNECT, self.RETRIES_CONNECT, mode=FREEDV_MODE.signalling)
return
def connected_iss(self, frame=None):
self.log("CONNECTED ISS...........................")
self.set_state(States.CONNECTED)
self.is_ISS = True
if self.socket_command_handler:
self.socket_command_handler.socket_respond_connected(self.origin, self.destination, self.bandwidth)
def connected_irs(self, frame):
self.log("CONNECTED IRS...........................")
self.state_manager.register_p2p_connection_session(self)
self.set_state(States.CONNECTED)
self.is_ISS = False
self.orign = frame["origin"]
self.destination = frame["destination_crc"]
if self.socket_command_handler:
self.socket_command_handler.socket_respond_connected(self.origin, self.destination, self.bandwidth)
session_open_frame = self.frame_factory.build_p2p_connection_connect_ack(self.destination, self.origin, self.session_id)
self.launch_twr_irs(session_open_frame, self.ENTIRE_CONNECTION_TIMEOUT, mode=FREEDV_MODE.signalling)
def session_failed(self):
self.set_state(States.FAILED)
if self.socket_command_handler:
self.socket_command_handler.socket_respond_disconnected()
def process_data_queue(self, frame=None):
if not self.p2p_data_tx_queue.empty():
print("processing data....")
self.set_state(States.PAYLOAD_SENT)
data = self.p2p_data_tx_queue.get()
sequence_id = random.randint(0,255)
data = data.encode('utf-8')
if len(data) <= 11:
mode = FREEDV_MODE.signalling
elif 11 < len(data) < 32:
mode = FREEDV_MODE.datac4
else:
self.transmit_arq(data)
return
payload = self.frame_factory.build_p2p_connection_payload(mode, self.session_id, sequence_id, data)
self.launch_twr(payload, self.TIMEOUT_DATA, self.RETRIES_DATA,mode=mode)
return
def prepare_data_chunk(self, data, mode):
return data
def received_data(self, frame):
print(frame)
self.p2p_data_rx_queue.put(frame['data'])
ack_data = self.frame_factory.build_p2p_connection_payload_ack(self.session_id, 0)
self.launch_twr_irs(ack_data, self.ENTIRE_CONNECTION_TIMEOUT, mode=FREEDV_MODE.signalling)
def transmit_data_ack(self, frame):
print(frame)
def transmitted_data(self, frame):
print("transmitted data...")
self.set_state(States.CONNECTED)
def disconnect(self):
if self.state not in [States.DISCONNECTING, States.DISCONNECTED]:
self.set_state(States.DISCONNECTING)
disconnect_frame = self.frame_factory.build_p2p_connection_disconnect(self.session_id)
self.launch_twr(disconnect_frame, self.TIMEOUT_CONNECT, self.RETRIES_CONNECT, mode=FREEDV_MODE.signalling)
return
def received_disconnect(self, frame):
self.log("DISCONNECTED...............")
self.set_state(States.DISCONNECTED)
if self.socket_command_handler:
self.socket_command_handler.socket_respond_disconnected()
self.is_ISS = False
disconnect_ack_frame = self.frame_factory.build_p2p_connection_disconnect_ack(self.session_id)
self.launch_twr_irs(disconnect_ack_frame, self.ENTIRE_CONNECTION_TIMEOUT, mode=FREEDV_MODE.signalling)
def received_disconnect_ack(self, frame):
self.log("DISCONNECTED...............")
self.set_state(States.DISCONNECTED)
if self.socket_command_handler:
self.socket_command_handler.socket_respond_disconnected()
def transmit_arq(self, data):
self.set_state(States.ARQ_SESSION)
print("----------------------------------------------------------------")
print(self.destination)
print(self.state_manager.p2p_connection_sessions)
prepared_data, type_byte = self.arq_data_type_handler.prepare(data, ARQ_SESSION_TYPES.p2p_connection)
iss = ARQSessionISS(self.config, self.modem, 'AA1AAA-1', self.state_manager, prepared_data, type_byte)
iss.id = self.session_id
if iss.id:
self.state_manager.register_arq_iss_session(iss)
iss.start()
return iss
def transmitted_arq(self):
self.last_data_timestamp = time.time()
self.set_state(States.CONNECTED)
def received_arq(self, data):
self.last_data_timestamp = time.time()
self.set_state(States.CONNECTED)
self.p2p_data_rx_queue.put(data)

View file

@ -20,8 +20,8 @@ class RadioManager:
def _init_rig_control(self): def _init_rig_control(self):
# Check how we want to control the radio # Check how we want to control the radio
if self.radiocontrol == "rigctld": if self.radiocontrol in ["rigctld", "rigctld_bundle"]:
self.radio = rigctld.radio(self.state_manager, hostname=self.rigctld_ip,port=self.rigctld_port) self.radio = rigctld.radio(self.config, self.state_manager, hostname=self.rigctld_ip,port=self.rigctld_port)
elif self.radiocontrol == "tci": elif self.radiocontrol == "tci":
raise NotImplementedError raise NotImplementedError
# self.radio = self.tci_module # self.radio = self.tci_module

View file

@ -1,6 +1,6 @@
import socket import socket
import structlog import structlog
import time import helpers
import threading import threading
class radio: class radio:
@ -8,11 +8,12 @@ class radio:
log = structlog.get_logger("radio (rigctld)") log = structlog.get_logger("radio (rigctld)")
def __init__(self, states, hostname="localhost", port=4532, timeout=5): def __init__(self, config, states, hostname="localhost", port=4532, timeout=5):
self.hostname = hostname self.hostname = hostname
self.port = port self.port = port
self.timeout = timeout self.timeout = timeout
self.states = states self.states = states
self.config = config
self.connection = None self.connection = None
self.connected = False self.connected = False
@ -29,6 +30,10 @@ class radio:
'ptt': False # Initial PTT state is set to False 'ptt': False # Initial PTT state is set to False
} }
# start rigctld...
if self.config["RADIO"]["control"] in ["rigctld_bundle"]:
self.start_service()
# connect to radio # connect to radio
self.connect() self.connect()
@ -45,7 +50,8 @@ class radio:
def disconnect(self): def disconnect(self):
self.connected = False self.connected = False
self.connection.close() if self.connection:
self.connection.close()
del self.connection del self.connection
self.connection = None self.connection = None
self.states.set("radio_status", False) self.states.set("radio_status", False)
@ -70,7 +76,11 @@ class radio:
self.connection.sendall(command.encode('utf-8') + b"\n") self.connection.sendall(command.encode('utf-8') + b"\n")
response = self.connection.recv(1024) response = self.connection.recv(1024)
self.await_response.set() self.await_response.set()
return response.decode('utf-8').strip() stripped_result = response.decode('utf-8').strip()
if 'RPRT' in stripped_result:
return None
return stripped_result
except Exception as err: except Exception as err:
self.log.warning(f"[RIGCTLD] Error sending command [{command}] to rigctld: {err}") self.log.warning(f"[RIGCTLD] Error sending command [{command}] to rigctld: {err}")
self.connected = False self.connected = False
@ -183,21 +193,137 @@ class radio:
self.connect() self.connect()
if self.connected: if self.connected:
self.parameters['frequency'] = self.send_command('f') self.get_frequency()
response = self.send_command( self.get_mode_bandwidth()
'm').strip() # Get the mode/bandwidth response and remove leading/trailing spaces self.get_alc()
try: self.get_strength()
mode, bandwidth = response.split('\n', 1) # Split the response into mode and bandwidth self.get_rf()
except ValueError:
return self.parameters
def get_frequency(self):
try:
frequency_response = self.send_command('f')
self.parameters['frequency'] = frequency_response if frequency_response is not None else 'err'
except Exception as e:
self.log.warning(f"Error getting frequency: {e}")
self.parameters['frequency'] = 'err'
def get_mode_bandwidth(self):
try:
response = self.send_command('m')
if response is not None:
response = response.strip()
mode, bandwidth = response.split('\n', 1)
else:
mode = 'err' mode = 'err'
bandwidth = 'err' bandwidth = 'err'
except ValueError:
mode = 'err'
bandwidth = 'err'
except Exception as e:
self.log.warning(f"Error getting mode and bandwidth: {e}")
mode = 'err'
bandwidth = 'err'
finally:
self.parameters['mode'] = mode self.parameters['mode'] = mode
self.parameters['bandwidth'] = bandwidth self.parameters['bandwidth'] = bandwidth
self.parameters['alc'] = self.send_command('l ALC') def get_alc(self):
self.parameters['strength'] = self.send_command('l STRENGTH') try:
self.parameters['rf'] = self.send_command('l RFPOWER') # RF, RFPOWER alc_response = self.send_command('l ALC')
self.parameters['alc'] = alc_response if alc_response is not None else 'err'
except Exception as e:
self.log.warning(f"Error getting ALC: {e}")
self.parameters['alc'] = 'err'
def get_strength(self):
try:
strength_response = self.send_command('l STRENGTH')
self.parameters['strength'] = strength_response if strength_response is not None else 'err'
except Exception as e:
self.log.warning(f"Error getting strength: {e}")
self.parameters['strength'] = 'err'
def get_rf(self):
try:
rf_response = self.send_command('l RFPOWER')
if rf_response is not None:
self.parameters['rf'] = int(float(rf_response) * 100)
else:
self.parameters['rf'] = 'err'
except ValueError:
self.parameters['rf'] = 'err'
except Exception as e:
self.log.warning(f"Error getting RF power: {e}")
self.parameters['rf'] = 'err'
def start_service(self):
binary_name = "rigctld"
binary_paths = helpers.find_binary_paths(binary_name, search_system_wide=True)
additional_args = self.format_rigctld_args()
if binary_paths:
for binary_path in binary_paths:
try:
self.log.info(f"Attempting to start rigctld using binary found at: {binary_path}")
helpers.kill_and_execute(binary_path, additional_args)
self.log.info("Successfully executed rigctld.")
break # Exit the loop after successful execution
except Exception as e:
pass
# let's keep this hidden for the user to avoid confusion
# self.log.warning(f"Failed to start rigctld with binary at {binary_path}: {e}")
else:
self.log.warning("Failed to start rigctld with all found binaries.", binaries=binary_paths)
else:
self.log.warning("Rigctld binary not found.")
def format_rigctld_args(self):
config = self.config['RADIO'] # Accessing the 'RADIO' section of the INI file
config_rigctld = self.config['RIGCTLD'] # Accessing the 'RIGCTLD' section of the INI file for custom args
args = []
# Helper function to check if the value should be ignored
def should_ignore(value):
return value in ['ignore', 0]
# Model ID, Serial Port, and Speed
if not should_ignore(config.get('model_id')):
args += ['-m', str(config['model_id'])]
if not should_ignore(config.get('serial_port')):
args += ['-r', config['serial_port']]
if not should_ignore(config.get('serial_speed')):
args += ['-s', str(config['serial_speed'])]
# PTT Port and Type
if not should_ignore(config.get('ptt_port')):
args += ['-p', config['ptt_port']]
if not should_ignore(config.get('ptt_type')):
args += ['-P', config['ptt_type']]
# Serial DCD and DTR
if not should_ignore(config.get('serial_dcd')):
args += ['-D', config['serial_dcd']]
if not should_ignore(config.get('serial_dtr')):
args += ['--set-conf', f'dtr_state={config["serial_dtr"]}']
# Handling Data Bits and Stop Bits
if not should_ignore(config.get('data_bits')):
args += ['--set-conf', f'data_bits={config["data_bits"]}']
if not should_ignore(config.get('stop_bits')):
args += ['--set-conf', f'stop_bits={config["stop_bits"]}']
# Fixme #rts_state
# if not should_ignore(config.get('rts_state')):
# args += ['--set-conf', f'stop_bits={config["rts_state"]}']
# Handle custom arguments for rigctld
# Custom args are split via ' ' so python doesn't add extranaeous quotes on windows
args += config_rigctld["arguments"].split(" ")
print("Hamlib args ==>" + str(args))
return args
"""Return the latest fetched parameters."""
return self.parameters

View file

@ -16,13 +16,12 @@ class ScheduleManager:
self.state_manager = state_manger self.state_manager = state_manger
self.event_manager = event_manager self.event_manager = event_manager
self.config = self.config_manager.read() self.config = self.config_manager.read()
self.beacon_interval = self.config['MODEM']['beacon_interval']
self.scheduler = sched.scheduler(time.time, time.sleep) self.scheduler = sched.scheduler(time.time, time.sleep)
self.events = { self.events = {
'check_for_queued_messages': {'function': self.check_for_queued_messages, 'interval': 10}, 'check_for_queued_messages': {'function': self.check_for_queued_messages, 'interval': 10},
'explorer_publishing': {'function': self.push_to_explorer, 'interval': 120}, 'explorer_publishing': {'function': self.push_to_explorer, 'interval': 60},
'transmitting_beacon': {'function': self.transmit_beacon, 'interval': self.beacon_interval}, 'transmitting_beacon': {'function': self.transmit_beacon, 'interval': 600},
'beacon_cleanup': {'function': self.delete_beacons, 'interval': 600}, 'beacon_cleanup': {'function': self.delete_beacons, 'interval': 600},
} }
self.running = False # Flag to control the running state self.running = False # Flag to control the running state
@ -65,24 +64,36 @@ class ScheduleManager:
self.scheduler_thread.join() self.scheduler_thread.join()
def transmit_beacon(self): def transmit_beacon(self):
if not self.state_manager.getARQ() and self.state_manager.is_beacon_running: try:
cmd = command_beacon.BeaconCommand(self.config, self.state_manager, self.event_manager) if not self.state_manager.getARQ() and self.state_manager.is_beacon_running:
cmd.run(self.event_manager, self.modem) cmd = command_beacon.BeaconCommand(self.config, self.state_manager, self.event_manager)
cmd.run(self.event_manager, self.modem)
except Exception as e:
print(e)
def delete_beacons(self): def delete_beacons(self):
DatabaseManagerBeacon(self.event_manager).beacon_cleanup_older_than_days(14) try:
DatabaseManagerBeacon(self.event_manager).beacon_cleanup_older_than_days(2)
except Exception as e:
print(e)
def push_to_explorer(self): def push_to_explorer(self):
self.config = self.config_manager.read() self.config = self.config_manager.read()
if self.config['STATION']['enable_explorer']: if self.config['STATION']['enable_explorer']:
explorer.explorer(self.modem_version, self.config_manager, self.state_manager).push() try:
explorer.explorer(self.modem_version, self.config_manager, self.state_manager).push()
except Exception as e:
print(e)
def check_for_queued_messages(self): def check_for_queued_messages(self):
if not self.state_manager.getARQ(): if not self.state_manager.getARQ():
if DatabaseManagerMessages(self.event_manager).get_first_queued_message(): try:
params = DatabaseManagerMessages(self.event_manager).get_first_queued_message() if first_queued_message := DatabaseManagerMessages(
command = command_message_send.SendMessageCommand(self.config_manager.read(), self.state_manager, self.event_manager, params) self.event_manager
command.transmit(self.modem) ).get_first_queued_message():
command = command_message_send.SendMessageCommand(self.config_manager.read(), self.state_manager, self.event_manager, first_queued_message)
command.transmit(self.modem)
except Exception as e:
print(e)
return return

View file

@ -1,3 +1,5 @@
import time
from flask import Flask, request, jsonify, make_response, abort, Response from flask import Flask, request, jsonify, make_response, abort, Response
from flask_sock import Sock from flask_sock import Sock
from flask_cors import CORS from flask_cors import CORS
@ -9,16 +11,19 @@ import audio
import queue import queue
import service_manager import service_manager
import state_manager import state_manager
import ujson as json import json
import websocket_manager as wsm import websocket_manager as wsm
import api_validations as validations import api_validations as validations
import command_cq import command_cq
import command_beacon
import command_ping import command_ping
import command_feq import command_feq
import command_test import command_test
import command_arq_raw import command_arq_raw
import command_message_send import command_message_send
import event_manager import event_manager
import atexit
from message_system_db_manager import DatabaseManager from message_system_db_manager import DatabaseManager
from message_system_db_messages import DatabaseManagerMessages from message_system_db_messages import DatabaseManagerMessages
from message_system_db_attachments import DatabaseManagerAttachments from message_system_db_attachments import DatabaseManagerAttachments
@ -26,17 +31,17 @@ from message_system_db_beacon import DatabaseManagerBeacon
from schedule_manager import ScheduleManager from schedule_manager import ScheduleManager
app = Flask(__name__) app = Flask(__name__)
CORS(app)
CORS(app, resources={r"/*": {"origins": "*"}}) CORS(app, resources={r"/*": {"origins": "*"}})
sock = Sock(app) sock = Sock(app)
MODEM_VERSION = "0.13.2-alpha" MODEM_VERSION = "0.14.5-alpha"
# set config file to use # set config file to use
def set_config(): def set_config():
if 'FREEDATA_CONFIG' in os.environ: if 'FREEDATA_CONFIG' in os.environ:
config_file = os.environ['FREEDATA_CONFIG'] config_file = os.environ['FREEDATA_CONFIG']
else: else:
config_file = 'config.ini' script_dir = os.path.dirname(os.path.abspath(__file__))
config_file = os.path.join(script_dir, 'config.ini')
if os.path.exists(config_file): if os.path.exists(config_file):
print(f"Using config from {config_file}") print(f"Using config from {config_file}")
@ -72,11 +77,14 @@ def validate(req, param, validator, isRequired = True):
# Takes a transmit command and puts it in the transmit command queue # Takes a transmit command and puts it in the transmit command queue
def enqueue_tx_command(cmd_class, params = {}): def enqueue_tx_command(cmd_class, params = {}):
command = cmd_class(app.config_manager.read(), app.state_manager, app.event_manager, params) try:
app.logger.info(f"Command {command.get_name()} running...") command = cmd_class(app.config_manager.read(), app.state_manager, app.event_manager, params)
if command.run(app.modem_events, app.service_manager.modem): # TODO remove the app.modem_event custom queue app.logger.info(f"Command {command.get_name()} running...")
return True if command.run(app.modem_events, app.service_manager.modem): # TODO remove the app.modem_event custom queue
return False return True
except Exception as e:
app.logger.warning(f"Command {command.get_name()} failed...: {e}")
return False
## REST API ## REST API
@app.route('/', methods=['GET']) @app.route('/', methods=['GET'])
@ -93,11 +101,18 @@ def index():
@app.route('/config', methods=['GET', 'POST']) @app.route('/config', methods=['GET', 'POST'])
def config(): def config():
if request.method in ['POST']: if request.method in ['POST']:
if not validations.validate_remote_config(request.json):
return api_abort("wrong config", 500)
# check if config already exists
if app.config_manager.read() == request.json:
return api_response(request.json)
set_config = app.config_manager.write(request.json) set_config = app.config_manager.write(request.json)
app.modem_service.put("restart")
if not set_config: if not set_config:
response = api_response(None, 'error writing config') response = api_response(None, 'error writing config')
else: else:
app.modem_service.put("restart")
response = api_response(set_config) response = api_response(set_config)
return response return response
elif request.method == 'GET': elif request.method == 'GET':
@ -139,6 +154,8 @@ def post_beacon():
if not app.state_manager.is_beacon_running: if not app.state_manager.is_beacon_running:
app.state_manager.set('is_beacon_running', request.json['enabled']) app.state_manager.set('is_beacon_running', request.json['enabled'])
if not app.state_manager.getARQ():
enqueue_tx_command(command_beacon.BeaconCommand, request.json)
else: else:
app.state_manager.set('is_beacon_running', request.json['enabled']) app.state_manager.set('is_beacon_running', request.json['enabled'])
@ -222,19 +239,23 @@ def post_modem_send_raw_stop():
if not app.state_manager.is_modem_running: if not app.state_manager.is_modem_running:
api_abort('Modem not running', 503) api_abort('Modem not running', 503)
for id in app.state_manager.arq_irs_sessions: if app.state_manager.getARQ():
app.state_manager.arq_irs_sessions[id].abort_transmission() for id in app.state_manager.arq_irs_sessions:
for id in app.state_manager.arq_iss_sessions: app.state_manager.arq_irs_sessions[id].abort_transmission()
app.state_manager.arq_iss_sessions[id].abort_transmission() for id in app.state_manager.arq_iss_sessions:
app.state_manager.arq_iss_sessions[id].abort_transmission()
return api_response(request.json) return api_response(request.json)
@app.route('/radio', methods=['GET', 'POST']) @app.route('/radio', methods=['GET', 'POST'])
def get_post_radio(): def get_post_radio():
if request.method in ['POST']: if request.method in ['POST']:
app.radio_manager.set_frequency(request.json['radio_frequency']) if "radio_frequency" in request.json:
app.radio_manager.set_mode(request.json['radio_mode']) app.radio_manager.set_frequency(request.json['radio_frequency'])
app.radio_manager.set_rf_level(int(request.json['radio_rf_level'])) if "radio_mode" in request.json:
app.radio_manager.set_mode(request.json['radio_mode'])
if "radio_rf_level" in request.json:
app.radio_manager.set_rf_level(int(request.json['radio_rf_level']))
return api_response(request.json) return api_response(request.json)
elif request.method == 'GET': elif request.method == 'GET':
@ -244,11 +265,12 @@ def get_post_radio():
def get_post_freedata_message(): def get_post_freedata_message():
if request.method in ['GET']: if request.method in ['GET']:
result = DatabaseManagerMessages(app.event_manager).get_all_messages_json() result = DatabaseManagerMessages(app.event_manager).get_all_messages_json()
return api_response(result, 200) return api_response(result)
if enqueue_tx_command(command_message_send.SendMessageCommand, request.json): if request.method in ['POST']:
return api_response(request.json, 200) enqueue_tx_command(command_message_send.SendMessageCommand, request.json)
else: return api_response(request.json)
api_abort('Error executing command...', 500)
api_abort('Error executing command...', 500)
@app.route('/freedata/messages/<string:message_id>', methods=['GET', 'POST', 'PATCH', 'DELETE']) @app.route('/freedata/messages/<string:message_id>', methods=['GET', 'POST', 'PATCH', 'DELETE'])
def handle_freedata_message(message_id): def handle_freedata_message(message_id):
@ -302,7 +324,20 @@ def sock_fft(sock):
def sock_states(sock): def sock_states(sock):
wsm.handle_connection(sock, wsm.states_client_list, app.state_queue) wsm.handle_connection(sock, wsm.states_client_list, app.state_queue)
@atexit.register
def stop_server():
try:
app.service_manager.modem_service.put("stop")
app.socket_interface_manager.stop_servers()
if app.service_manager.modem:
app.service_manager.modem.sd_input_stream.stop
audio.sd._terminate()
except Exception as e:
print("Error stopping modem")
time.sleep(1)
print("------------------------------------------")
print('Server shutdown...')
if __name__ == "__main__": if __name__ == "__main__":
app.config['SOCK_SERVER_OPTIONS'] = {'ping_interval': 10} app.config['SOCK_SERVER_OPTIONS'] = {'ping_interval': 10}
@ -313,6 +348,7 @@ if __name__ == "__main__":
app.config_manager = CONFIG(config_file) app.config_manager = CONFIG(config_file)
# start modem # start modem
app.p2p_data_queue = queue.Queue() # queue which holds processing data of p2p connections
app.state_queue = queue.Queue() # queue which holds latest states app.state_queue = queue.Queue() # queue which holds latest states
app.modem_events = queue.Queue() # queue which holds latest events app.modem_events = queue.Queue() # queue which holds latest events
app.modem_fft = queue.Queue() # queue which holds latest fft data app.modem_fft = queue.Queue() # queue which holds latest fft data
@ -324,9 +360,21 @@ if __name__ == "__main__":
app.schedule_manager = ScheduleManager(app.MODEM_VERSION, app.config_manager, app.state_manager, app.event_manager) app.schedule_manager = ScheduleManager(app.MODEM_VERSION, app.config_manager, app.state_manager, app.event_manager)
# start service manager # start service manager
app.service_manager = service_manager.SM(app) app.service_manager = service_manager.SM(app)
# start modem service # start modem service
app.modem_service.put("start") app.modem_service.put("start")
# initialize database default values # initialize database default values
DatabaseManager(app.event_manager).initialize_default_values() DatabaseManager(app.event_manager).initialize_default_values()
wsm.startThreads(app) wsm.startThreads(app)
app.run()
conf = app.config_manager.read()
modemaddress = conf['NETWORK']['modemaddress']
modemport = conf['NETWORK']['modemport']
if not modemaddress:
modemaddress = '127.0.0.1'
if not modemport:
modemport = 5000
app.run(modemaddress, modemport)

View file

@ -5,7 +5,7 @@ import structlog
import audio import audio
import radio_manager import radio_manager
from socket_interface import SocketInterfaceHandler
class SM: class SM:
def __init__(self, app): def __init__(self, app):
@ -19,7 +19,7 @@ class SM:
self.state_manager = app.state_manager self.state_manager = app.state_manager
self.event_manager = app.event_manager self.event_manager = app.event_manager
self.schedule_manager = app.schedule_manager self.schedule_manager = app.schedule_manager
self.socket_interface_manager = None
runner_thread = threading.Thread( runner_thread = threading.Thread(
target=self.runner, name="runner thread", daemon=True target=self.runner, name="runner thread", daemon=True
@ -34,15 +34,23 @@ class SM:
self.start_radio_manager() self.start_radio_manager()
self.start_modem() self.start_modem()
if self.config['SOCKET_INTERFACE']['enable']:
self.socket_interface_manager = SocketInterfaceHandler(self.modem, self.app.config_manager, self.state_manager, self.event_manager).start_servers()
elif cmd in ['stop'] and self.modem: elif cmd in ['stop'] and self.modem:
self.stop_modem() self.stop_modem()
self.stop_radio_manager() self.stop_radio_manager()
if self.config['SOCKET_INTERFACE']['enable']:
self.socket_interface_manager.stop_servers()
# we need to wait a bit for avoiding a portaudio crash # we need to wait a bit for avoiding a portaudio crash
threading.Event().wait(0.5) threading.Event().wait(0.5)
elif cmd in ['restart']: elif cmd in ['restart']:
self.stop_modem() self.stop_modem()
self.stop_radio_manager() self.stop_radio_manager()
if self.config['SOCKET_INTERFACE']['enable']:
self.socket_interface_manager.stop_servers()
# we need to wait a bit for avoiding a portaudio crash # we need to wait a bit for avoiding a portaudio crash
threading.Event().wait(0.5) threading.Event().wait(0.5)

193
modem/socket_interface.py Normal file
View file

@ -0,0 +1,193 @@
""" WORK IN PROGRESS by DJ2LS"""
import socketserver
import threading
import structlog
import select
from queue import Queue
from socket_interface_commands import SocketCommandHandler
class CommandSocket(socketserver.BaseRequestHandler):
#def __init__(self, request, client_address, server):
def __init__(self, request, client_address, server, modem=None, state_manager=None, event_manager=None, config_manager=None):
self.state_manager = state_manager
self.event_manager = event_manager
self.config_manager = config_manager
self.modem = modem
self.logger = structlog.get_logger(type(self).__name__)
self.command_handler = SocketCommandHandler(request, self.modem, self.config_manager, self.state_manager, self.event_manager)
self.handlers = {
'CONNECT': self.command_handler.handle_connect,
'DISCONNECT': self.command_handler.handle_disconnect,
'MYCALL': self.command_handler.handle_mycall,
'BW': self.command_handler.handle_bw,
'ABORT': self.command_handler.handle_abort,
'PUBLIC': self.command_handler.handle_public,
'CWID': self.command_handler.handle_cwid,
'LISTEN': self.command_handler.handle_listen,
'COMPRESSION': self.command_handler.handle_compression,
'WINLINK SESSION': self.command_handler.handle_winlink_session,
}
super().__init__(request, client_address, server)
def log(self, message, isWarning = False):
msg = f"[{type(self).__name__}]: {message}"
logger = self.logger.warn if isWarning else self.logger.info
logger(msg)
def handle(self):
self.log(f"Client connected: {self.client_address}")
try:
while True:
data = self.request.recv(1024).strip()
if not data:
break
decoded_data = data.decode()
self.log(f"Command received from {self.client_address}: {decoded_data}")
self.parse_command(decoded_data)
finally:
self.log(f"Command connection closed with {self.client_address}")
def parse_command(self, data):
for command in self.handlers:
if data.startswith(command):
# Extract command arguments after the command itself
args = data[len(command):].strip().split()
self.dispatch_command(command, args)
return
self.send_response("ERROR: Unknown command\r\n")
def dispatch_command(self, command, data):
if command in self.handlers:
handler = self.handlers[command]
handler(data)
else:
self.send_response(f"Unknown command: {command}")
class DataSocket(socketserver.BaseRequestHandler):
#def __init__(self, request, client_address, server):
def __init__(self, request, client_address, server, modem=None, state_manager=None, event_manager=None, config_manager=None):
self.state_manager = state_manager
self.event_manager = event_manager
self.config_manager = config_manager
self.modem = modem
self.logger = structlog.get_logger(type(self).__name__)
super().__init__(request, client_address, server)
def log(self, message, isWarning = False):
msg = f"[{type(self).__name__}]: {message}"
logger = self.logger.warn if isWarning else self.logger.info
logger(msg)
def handle(self):
self.log(f"Data connection established with {self.client_address}")
try:
while True:
ready_to_read, _, _ = select.select([self.request], [], [], 1) # 1-second timeout
if ready_to_read:
self.data = self.request.recv(1024).strip()
if not self.data:
break
try:
self.log(f"Data received from {self.client_address}: [{len(self.data)}] - {self.data.decode()}")
except Exception:
self.log(f"Data received from {self.client_address}: [{len(self.data)}] - {self.data}")
for session in self.state_manager.p2p_connection_sessions:
print(f"sessions: {session}")
session.p2p_data_tx_queue.put(self.data)
# Check if there's something to send from the queue, without blocking
for session_id in self.state_manager.p2p_connection_sessions:
session = self.state_manager.get_p2p_connection_session(session_id)
if not session.p2p_data_tx_queue.empty():
data_to_send = session.p2p_data_tx_queue.get_nowait() # Use get_nowait to avoid blocking
self.request.sendall(data_to_send)
self.log(f"Sent data to {self.client_address}")
finally:
self.log(f"Data connection closed with {self.client_address}")
#class ThreadedTCPServer(socketserver.ThreadingMixIn, socketserver.TCPServer):
# allow_reuse_address = True
class CustomThreadedTCPServer(socketserver.ThreadingMixIn, socketserver.TCPServer):
allow_reuse_address = True
def __init__(self, server_address, RequestHandlerClass, bind_and_activate=True, **kwargs):
self.extra_args = kwargs
super().__init__(server_address, RequestHandlerClass, bind_and_activate=bind_and_activate)
def finish_request(self, request, client_address):
self.RequestHandlerClass(request, client_address, self, **self.extra_args)
class SocketInterfaceHandler:
def __init__(self, modem, config_manager, state_manager, event_manager):
self.modem = modem
self.config_manager = config_manager
self.config = self.config_manager.read()
self.state_manager = state_manager
self.event_manager = event_manager
self.logger = structlog.get_logger(type(self).__name__)
self.command_port = self.config["SOCKET_INTERFACE"]["cmd_port"]
self.data_port = self.config["SOCKET_INTERFACE"]["data_port"]
self.command_server = None
self.data_server = None
self.command_server_thread = None
self.data_server_thread = None
def log(self, message, isWarning = False):
msg = f"[{type(self).__name__}]: {message}"
logger = self.logger.warn if isWarning else self.logger.info
logger(msg)
def start_servers(self):
if self.command_port == 0:
self.command_port = 8300
if self.data_port == 0:
self.data_port = 8301
# Method to start both command and data server threads
self.command_server_thread = threading.Thread(target=self.run_server, args=(self.command_port, CommandSocket))
self.data_server_thread = threading.Thread(target=self.run_server, args=(self.data_port, DataSocket))
self.command_server_thread.start()
self.data_server_thread.start()
self.log(f"Interfaces started")
def run_server(self, port, handler):
with CustomThreadedTCPServer(('127.0.0.1', port), handler, modem=self.modem, state_manager=self.state_manager, event_manager=self.event_manager, config_manager=self.config_manager) as server:
self.log(f"Server started on port {port}")
if port == self.command_port:
self.command_server = server
else:
self.data_server = server
server.serve_forever()
def stop_servers(self):
# Gracefully shutdown the server
if self.command_server:
self.command_server.shutdown()
if self.data_server:
self.data_server.shutdown()
self.log(f"Interfaces stopped")
def wait_for_server_threads(self):
# Wait for both server threads to finish
self.command_server_thread.join()
self.data_server_thread.join()

View file

@ -0,0 +1,76 @@
""" WORK IN PROGRESS by DJ2LS"""
from command_p2p_connection import P2PConnectionCommand
class SocketCommandHandler:
def __init__(self, cmd_request, modem, config_manager, state_manager, event_manager):
self.cmd_request = cmd_request
self.modem = modem
self.config_manager = config_manager
self.state_manager = state_manager
self.event_manager = event_manager
self.session = None
def send_response(self, message):
full_message = f"{message}\r\n"
self.cmd_request.sendall(full_message.encode())
def handle_connect(self, data):
params = {
'origin': data[0],
'destination': data[1],
}
cmd = P2PConnectionCommand(self.config_manager.read(), self.state_manager, self.event_manager, params, self)
self.session = cmd.run(self.event_manager.queues, self.modem)
#if self.session.session_id:
# self.state_manager.register_p2p_connection_session(self.session)
# self.send_response("OK")
# self.session.connect()
#else:
# self.send_response("ERROR")
def handle_disconnect(self, data):
# Your existing connect logic
self.send_response("OK")
def handle_mycall(self, data):
# Logic for handling MYCALL command
self.send_response("OK")
def handle_bw(self, data):
# Logic for handling BW command
self.send_response("OK")
def handle_abort(self, data):
# Logic for handling ABORT command
self.send_response("OK")
def handle_public(self, data):
# Logic for handling PUBLIC command
self.send_response("OK")
def handle_cwid(self, data):
# Logic for handling CWID command
self.send_response("OK")
def handle_listen(self, data):
# Logic for handling LISTEN command
self.send_response("OK")
def handle_compression(self, data):
# Logic for handling COMPRESSION command
self.send_response("OK")
def handle_winlink_session(self, data):
# Logic for handling WINLINK SESSION command
self.send_response("OK")
def socket_respond_disconnected(self):
self.send_response("DISCONNECTED")
def socket_respond_connected(self, mycall, dxcall, bandwidth):
message = f"CONNECTED {mycall} {dxcall} {bandwidth}"
self.send_response(message)

View file

@ -1,5 +1,4 @@
import time import time
import ujson as json
import threading import threading
import numpy as np import numpy as np
class StateManager: class StateManager:
@ -39,6 +38,8 @@ class StateManager:
self.arq_iss_sessions = {} self.arq_iss_sessions = {}
self.arq_irs_sessions = {} self.arq_irs_sessions = {}
self.p2p_connection_sessions = {}
#self.mesh_routing_table = [] #self.mesh_routing_table = []
self.radio_frequency = 0 self.radio_frequency = 0
@ -215,3 +216,15 @@ class StateManager:
"radio_rf_level": self.radio_rf_level, "radio_rf_level": self.radio_rf_level,
"s_meter_strength": self.s_meter_strength, "s_meter_strength": self.s_meter_strength,
} }
def register_p2p_connection_session(self, session):
if session.session_id in self.p2p_connection_sessions:
print("session already registered...")
return False
self.p2p_connection_sessions[session.session_id] = session
return True
def get_p2p_connection_session(self, id):
if id not in self.p2p_connection_sessions:
pass
return self.p2p_connection_sessions[id]

View file

@ -8,7 +8,7 @@ Created on 05.11.23
# pylint: disable=import-outside-toplevel, attribute-defined-outside-init # pylint: disable=import-outside-toplevel, attribute-defined-outside-init
import requests import requests
import ujson as json import json
import structlog import structlog
log = structlog.get_logger("stats") log = structlog.get_logger("stats")

View file

@ -5,7 +5,6 @@ PyAudio
pyserial pyserial
sounddevice sounddevice
structlog structlog
ujson
requests requests
chardet chardet
colorama colorama

View file

@ -103,7 +103,7 @@ class TestARQSession(unittest.TestCase):
def waitForSession(self, q, outbound = False): def waitForSession(self, q, outbound = False):
key = 'arq-transfer-outbound' if outbound else 'arq-transfer-inbound' key = 'arq-transfer-outbound' if outbound else 'arq-transfer-inbound'
while True: while True and self.channels_running:
ev = q.get() ev = q.get()
if key in ev and ('success' in ev[key] or 'ABORTED' in ev[key]): if key in ev and ('success' in ev[key] or 'ABORTED' in ev[key]):
self.logger.info(f"[{threading.current_thread().name}] {key} session ended.") self.logger.info(f"[{threading.current_thread().name}] {key} session ended.")
@ -125,16 +125,17 @@ class TestARQSession(unittest.TestCase):
def waitAndCloseChannels(self): def waitAndCloseChannels(self):
self.waitForSession(self.iss_event_queue, True) self.waitForSession(self.iss_event_queue, True)
self.channels_running = False
self.waitForSession(self.irs_event_queue, False) self.waitForSession(self.irs_event_queue, False)
self.channels_running = False self.channels_running = False
def testARQSessionSmallPayload(self): def testARQSessionSmallPayload(self):
# set Packet Error Rate (PER) / frame loss probability # set Packet Error Rate (PER) / frame loss probability
self.loss_probability = 0 self.loss_probability = 30
self.establishChannels() self.establishChannels()
params = { params = {
'dxcall': "XX1XXX-1", 'dxcall': "AA1AAA-1",
'data': base64.b64encode(bytes("Hello world!", encoding="utf-8")), 'data': base64.b64encode(bytes("Hello world!", encoding="utf-8")),
'type': "raw_lzma" 'type': "raw_lzma"
} }
@ -143,13 +144,13 @@ class TestARQSession(unittest.TestCase):
self.waitAndCloseChannels() self.waitAndCloseChannels()
del cmd del cmd
def DisabledtestARQSessionLargePayload(self): def testARQSessionLargePayload(self):
# set Packet Error Rate (PER) / frame loss probability # set Packet Error Rate (PER) / frame loss probability
self.loss_probability = 0 self.loss_probability = 0
self.establishChannels() self.establishChannels()
params = { params = {
'dxcall': "XX1XXX-1", 'dxcall': "AA1AAA-1",
'data': base64.b64encode(np.random.bytes(1000)), 'data': base64.b64encode(np.random.bytes(1000)),
'type': "raw_lzma" 'type': "raw_lzma"
} }
@ -165,7 +166,7 @@ class TestARQSession(unittest.TestCase):
self.establishChannels() self.establishChannels()
params = { params = {
'dxcall': "XX1XXX-1", 'dxcall': "AA1AAA-1",
'data': base64.b64encode(np.random.bytes(100)), 'data': base64.b64encode(np.random.bytes(100)),
} }
cmd = ARQRawCommand(self.config, self.iss_state_manager, self.iss_event_queue, params) cmd = ARQRawCommand(self.config, self.iss_state_manager, self.iss_event_queue, params)
@ -184,7 +185,7 @@ class TestARQSession(unittest.TestCase):
self.establishChannels() self.establishChannels()
params = { params = {
'dxcall': "XX1XXX-1", 'dxcall': "AA1AAA-1",
'data': base64.b64encode(np.random.bytes(100)), 'data': base64.b64encode(np.random.bytes(100)),
} }
cmd = ARQRawCommand(self.config, self.iss_state_manager, self.iss_event_queue, params) cmd = ARQRawCommand(self.config, self.iss_state_manager, self.iss_event_queue, params)
@ -200,7 +201,7 @@ class TestARQSession(unittest.TestCase):
def testSessionCleanupISS(self): def testSessionCleanupISS(self):
params = { params = {
'dxcall': "XX1XXX-1", 'dxcall': "AA1AAA-1",
'data': base64.b64encode(np.random.bytes(100)), 'data': base64.b64encode(np.random.bytes(100)),
} }
cmd = ARQRawCommand(self.config, self.iss_state_manager, self.iss_event_queue, params) cmd = ARQRawCommand(self.config, self.iss_state_manager, self.iss_event_queue, params)
@ -220,7 +221,9 @@ class TestARQSession(unittest.TestCase):
session = arq_session_irs.ARQSessionIRS(self.config, session = arq_session_irs.ARQSessionIRS(self.config,
self.irs_modem, self.irs_modem,
'AA1AAA-1', 'AA1AAA-1',
random.randint(0, 255)) random.randint(0, 255),
self.irs_state_manager
)
self.irs_state_manager.register_arq_irs_session(session) self.irs_state_manager.register_arq_irs_session(session)
for session_id in self.irs_state_manager.arq_irs_sessions: for session_id in self.irs_state_manager.arq_irs_sessions:
session = self.irs_state_manager.arq_irs_sessions[session_id] session = self.irs_state_manager.arq_irs_sessions[session_id]

Some files were not shown because too many files have changed in this diff Show more