Compare commits

...

187 Commits

Author SHA1 Message Date
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
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 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
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
85 changed files with 1242 additions and 2825 deletions

View File

@ -14,6 +14,15 @@ jobs:
with:
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
run: |
python -m pip install --upgrade pip
@ -36,9 +45,9 @@ jobs:
run: ls -R
- name: Create installer
uses: joncloud/makensis-action@v4
uses: joncloud/makensis-action@v4.1
with:
script-file: "freedata-server-nsis-config.nsi"
script-file: "freedata-nsis-config.nsi"
arguments: '/V3'
- name: LIST ALL FILES
@ -47,12 +56,14 @@ jobs:
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: 'FreeData-Server-Installer'
path: ./FreeData-Server-Installer.exe
name: 'FreeDATA-Installer'
path: ./FreeDATA-Installer.exe
- name: Upload Installer to Release
uses: softprops/action-gh-release@v1
if: startsWith(github.ref, 'refs/tags/v')
with:
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
python -m pip install --upgrade pip
pip3 install pyaudio
export PYTHONPATH=/Library/Frameworks/Python.framework/Versions/3.11/lib/:$PYTHONPATH
- name: Install Python dependencies
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

@ -45,18 +45,12 @@
"icon": "build/icon.png",
"target": [
{
"target": "nsis",
"target": "portable",
"arch": ["arm64", "x64"]
}
],
"artifactName": "${productName}-GUI-Windows-${version}.${ext}"
},
"nsis": {
"oneClick": false,
"perMachine": false,
"allowToChangeInstallationDirectory": true,
"deleteAppDataOnUninstall": true
},
"linux": {
"category": "Development",
"target": [

View File

@ -1,7 +1,6 @@
import { app, BrowserWindow, shell, ipcMain } from "electron";
import { release, platform } from "node:os";
import { join } from "node:path";
import { autoUpdater } from "electron-updater";
import { release, platform } from "os";
import { join, dirname } from "path";
import { existsSync } from "fs";
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
? join(process.env.DIST_ELECTRON, "../public")
: process.env.DIST;
process.env.FDUpdateAvail = "0";
// Disable GPU Acceleration for Windows 7
if (release().startsWith("6.1")) app.disableHardwareAcceleration();
@ -40,7 +38,7 @@ if (!app.requestSingleInstanceLock()) {
// process.env['ELECTRON_DISABLE_SECURITY_WARNINGS'] = 'true'
// set daemon process var
var daemonProcess = null;
var serverProcess = null;
let win: BrowserWindow | null = null;
// Here, you can also use other preload
const preload = join(__dirname, "../preload/index.js");
@ -75,9 +73,9 @@ async function createWindow() {
}
// Test actively push message to the Electron-Renderer
win.webContents.on("did-finish-load", () => {
win?.webContents.send("main-process-message", new Date().toLocaleString());
});
//win.webContents.on("did-finish-load", () => {
// win?.webContents.send("main-process-message", new Date().toLocaleString());
//});
// Make all links open with the browser, not with the application
win.webContents.setWindowOpenHandler(({ url }) => {
@ -87,12 +85,7 @@ async function createWindow() {
// win.webContents.on('will-navigate', (event, url) => { }) #344
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());
//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()) {
case "darwin":
daemonPath = join(process.resourcesPath, "modem", "freedata-server");
case "linux":
daemonPath = join(process.resourcesPath, "modem", "freedata-server");
break;
//case "darwin":
//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 "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":
daemonPath = join(process.resourcesPath, "modem", "freedata-server.exe");
break;
case "win64":
daemonPath = join(process.resourcesPath, "modem", "freedata-server.exe");
serverPath = join(basePath, "freedata-server", "freedata-server.exe");
console.log(`Starting server with path: ${serverPath}`);
serverProcess = spawn(
"cmd.exe",
["/c", "start", "cmd.exe", "/c", serverPath],
{ shell: true },
);
console.log(`Started server | PID: ${serverProcess.pid}`);
break;
default:
console.log("Unhandled OS Platform: ", platform());
serverProcess = null;
serverPath = null;
break;
}
//Start daemon binary if it exists
if (existsSync(daemonPath)) {
console.log("Starting freedata-server binary");
console.log("daemonPath:", daemonPath);
console.log("CWD:", join(daemonPath, ".."));
/*
var daemonProcess = spawn("freedata-server", [], {
cwd: join(process.env.DIST, "modem"),
shell: true
});
*/
/*
daemonProcess = spawn(daemonPath, [], {
shell: true
});
console.log(daemonProcess)
*/
daemonProcess = spawn(daemonPath, [], {});
serverProcess.on("error", (err) => {
console.error("Failed to start server process:", err);
serverProcess = null;
serverPath = null;
});
serverProcess.stdout.on("data", (data) => {
//console.log(`stdout: ${data}`);
});
// return process messages
daemonProcess.on("error", (err) => {
//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.");
}
serverProcess.stderr.on("data", (data) => {
console.error(`stderr: ${data}`);
});
});
//)
app.on("before-quit", () => {
close_sub_processes();
});
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() {
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
try {
if (daemonProcess != null) {
daemonProcess.kill();
}
} catch (e) {
console.log(e);
}
if (serverProcess != null) {
try {
console.log(`Killing server process with PID: ${serverProcess.pid}`);
console.log("closing modem and daemon");
try {
if (platform() == "win32") {
spawn("Taskkill", ["/IM", "freedata-modem.exe", "/F"]);
spawn("Taskkill", ["/IM", "freedata-server.exe", "/F"]);
}
switch (platform().toLowerCase()) {
//case "darwin":
// process.kill(serverProcess.pid);
// break;
//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") {
spawn("pkill", ["-9", "freedata-modem"]);
spawn("pkill", ["-9", "freedata-server"]);
default:
console.log("Unhandled OS Platform: ", platform());
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();
};
setTimeout(removeLoading, 4999);
// 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";
}
});
setTimeout(removeLoading, 3999);

View File

@ -2,7 +2,7 @@
"name": "FreeDATA",
"description": "FreeDATA Client application for connecting to FreeDATA server",
"private": true,
"version": "0.13.4-alpha",
"version": "0.14.3-alpha",
"main": "dist-electron/main/index.js",
"scripts": {
"start": "vite",
@ -13,7 +13,8 @@
"release": "vue-tsc --noEmit && vite build && electron-builder -p onTag",
"preview": "vite preview",
"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": {
"type": "git",
@ -39,12 +40,10 @@
"blob-util": "2.0.2",
"bootstrap": "5.3.2",
"bootstrap-icons": "1.11.3",
"bootswatch": "5.3.2",
"browser-image-compression": "2.0.2",
"chart.js": "4.4.1",
"chartjs-plugin-annotation": "3.0.1",
"electron-log": "5.1.1",
"electron-updater": "6.1.7",
"emoji-picker-element": "1.21.0",
"emoji-picker-element-data": "1.6.0",
"file-saver": "2.0.5",
@ -54,32 +53,32 @@
"noto-color-emoji": "^1.0.1",
"pinia": "2.1.7",
"qth-locator": "2.1.0",
"socket.io": "4.7.2",
"socket.io": "4.7.4",
"uuid": "^9.0.1",
"vue": "3.4.15",
"vue": "3.4.21",
"vue-chartjs": "5.3.0",
"vuemoji-picker": "0.2.0"
},
"devDependencies": {
"@types/nconf": "^0.10.6",
"@typescript-eslint/eslint-plugin": "6.21.0",
"@vitejs/plugin-vue": "5.0.3",
"electron": "28.2.2",
"@vitejs/plugin-vue": "5.0.4",
"electron": "28.2.6",
"electron-builder": "24.9.1",
"eslint": "8.56.0",
"eslint-config-prettier": "9.1.0",
"eslint-config-standard-with-typescript": "43.0.1",
"eslint-plugin-import": "2.29.1",
"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-vue": "9.20.1",
"eslint-plugin-vue": "9.22.0",
"typescript": "5.3.3",
"vite": "5.0.12",
"vite-plugin-electron": "0.28.0",
"vite": "5.1.3",
"vite-plugin-electron": "0.28.2",
"vite-plugin-electron-renderer": "0.14.5",
"vitest": "1.2.2",
"vue": "3.4.15",
"vue": "3.4.21",
"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";
import { Bar } from "vue-chartjs";
import { ref, computed } from "vue";
import { watch, nextTick, ref, computed } from "vue";
import annotationPlugin from "chartjs-plugin-annotation";
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>
<template>
@ -123,7 +137,7 @@ const beaconHistogramData = computed(() => ({
<div class="col-9 border-start vh-100 p-0">
<div class="d-flex flex-column vh-100">
<!-- 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">
<button type="button" class="btn btn-outline-secondary" disabled>
Beacons
@ -143,7 +157,7 @@ const beaconHistogramData = computed(() => ({
</nav>
<!-- Chat Messages Area -->
<div class="flex-grow-1 overflow-auto">
<div class="flex-grow-1 overflow-auto" ref="messagesContainer">
<chat_messages />
</div>

View File

@ -13,11 +13,7 @@ const chat = useChatStore(pinia);
function chatSelected(callsign) {
chat.selectedCallsign = callsign.toUpperCase();
// scroll message container to bottom
var messageBody = document.getElementById("message-container");
if (messageBody != null) {
// needs sensible defaults
messageBody.scrollTop = messageBody.scrollHeight - messageBody.clientHeight;
}
chat.triggerScrollToBottom();
processBeaconData(callsign);
}

View File

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

View File

@ -8,7 +8,7 @@ import "../../node_modules/gridstack/dist/gridstack.min.css";
import { GridStack } from "gridstack";
import { useStateStore } from "../store/stateStore.js";
const state = useStateStore(pinia);
import { setRadioParameters } from "../js/api";
import { setRadioParametersFrequency, setRadioParametersMode, setRadioParametersRFLevel } from "../js/api";
import { saveLocalSettingsToConfig, settingsStore } from "../store/settingsStore";
import active_heard_stations from "./grid/grid_active_heard_stations.vue";
@ -251,14 +251,22 @@ new gridWidget(
//New new widget ID should be 20
];
function updateFrequencyAndApply(frequency) {
state.new_frequency = frequency;
set_radio_parameters();
set_radio_parameter_frequency();
}
function set_radio_parameters(){
setRadioParameters(state.new_frequency, state.mode, state.rf_level);
function set_radio_parameter_frequency(){
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();
});
function onChange(event, changeItems) {
// update item position
changeItems.forEach((item) => {
var widget = items.value.find((w) => w.id == item.id);
if (!widget) {
console.error("Widget not found: " + item.id);
return;
if (typeof changeItems !== "undefined"){
// update item position
changeItems.forEach((item) => {
var widget = items.value.find((w) => w.id == item.id);
if (!widget) {
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(){
//Try to load grid from saved config
@ -475,8 +485,8 @@ function quickfill() {
<i class="bi bi-grip-vertical h5"></i>
</button>
<div class="grid-container" style="height: calc(100vh - 51px);">
<div class="grid-stack">
<div class="grid-container z-0" style="height: calc(100vh - 51px);">
<div class="grid-stack z-0">
<div
v-for="(w, indexs) in items"
class="grid-stack-item"

View File

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

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_top_navbar from "./main_top_navbar.vue";
import main_rig_control from "./main_rig_control.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_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 infoScreen from "./infoScreen.vue";
import main_modem_healthcheck from "./main_modem_healthcheck.vue";
import Dynamic_components from "./dynamic_components.vue";
import { getFreedataMessages } from "../js/api";
import { getRemote } from "../store/settingsStore.js";
import { loadAllData } from "../js/eventHandler";
</script>
<template>
<!-------------------------------- INFO TOASTS ---------------->
<div
aria-live="polite"
aria-atomic="true"
class="position-relative"
style="z-index: 500"
>
<div aria-live="polite" aria-atomic="true" class="position-relative z-3">
<div
class="toast-container position-absolute top-0 end-0 p-3"
id="mainToastContainer"
@ -48,6 +37,7 @@ import { getFreedataMessages } from "../js/api";
id="main-list-tab"
role="tablist"
style="margin-top: 100px"
@click="loadAllData"
>
<main_modem_healthcheck />
@ -84,17 +74,6 @@ import { getFreedataMessages } from "../js/api";
><i class="bi bi-rocket h3"></i
></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
class="list-group-item list-group-item-dark list-group-item-action d-none border-0 rounded-3 mb-2"
id="list-logger-list"
@ -112,6 +91,7 @@ import { getFreedataMessages } from "../js/api";
role="tab"
aria-controls="list-settings"
title="Settings"
@click="loadAllData"
><i class="bi bi-gear-wide-connected h3"></i
></a>
</div>
@ -303,14 +283,7 @@ import { getFreedataMessages } from "../js/api";
</div>
</div>
</div>
<div
class="tab-pane fade"
id="list-info"
role="tabpanel"
aria-labelledby="list-info-list"
>
<infoScreen />
</div>
<div
class="tab-pane fade show active"
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,235 +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 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-rigctld-list":
settings.remote.RADIO.control = "rigctld_bundle";
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>
<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,8 +2,6 @@
import { Modal } from "bootstrap";
import { onMounted } from "vue";
import infoScreen_updater from "./infoScreen_updater.vue";
import { setActivePinia } from "pinia";
import pinia from "../store/index";
setActivePinia(pinia);
@ -14,6 +12,11 @@ import { sendModemCQ } from "../js/api.js";
import { useStateStore } from "../store/stateStore.js";
const state = useStateStore(pinia);
import { useAudioStore } from "../store/audioStore";
const audioStore = useAudioStore();
import { useSerialStore } from "../store/serialStore";
const serialStore = useSerialStore();
import {
getVersion,
setConfig,
@ -21,11 +24,8 @@ import {
stopModem,
getModemState,
} from "../js/api";
import { audioInputOptions, audioOutputOptions } from "../js/deviceFormHelper";
import { serialDeviceOptions } from "../js/deviceFormHelper";
const version = import.meta.env.PACKAGE_VERSION;
var updateAvailable = process.env.FDUpdateAvail;
// start modemCheck modal once on startup
onMounted(() => {
@ -126,6 +126,7 @@ function testHamlib() {
max="65534"
min="1025"
v-model="settings.local.port"
@change="onChange"
/>
</div>
@ -137,6 +138,7 @@ function testHamlib() {
placeholder="modem host (default 127.0.0.1)"
id="modem_port"
v-model="settings.local.host"
@change="onChange"
/>
</div>
</div>
@ -203,6 +205,7 @@ function testHamlib() {
</button>
</label>
</div>
<!-- Audio Input Device -->
<div class="input-group input-group-sm mb-1">
<label class="input-group-text w-50"
@ -215,10 +218,10 @@ function testHamlib() {
v-model="settings.remote.AUDIO.input_device"
>
<option
v-for="option in audioInputOptions()"
v-bind:value="option.id"
v-for="device in audioStore.audioInputs"
:value="device.id"
>
{{ option.name }} [{{ option.api }}]
{{ device.name }} [{{ device.api }}]
</option>
</select>
</div>
@ -235,10 +238,10 @@ function testHamlib() {
v-model="settings.remote.AUDIO.output_device"
>
<option
v-for="option in audioOutputOptions()"
v-bind:value="option.id"
v-for="device in audioStore.audioOutputs"
:value="device.id"
>
{{ option.name }} [{{ option.api }}]
{{ device.name }} [{{ device.api }}]
</option>
</select>
</div>
@ -310,18 +313,16 @@ function testHamlib() {
>
<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"
class="form-select form-select-sm"
>
<option
v-for="option in serialDeviceOptions()"
v-bind:value="option.port"
v-for="device in serialStore.serialDevices"
:value="device.port"
:key="device.port"
>
{{ option.description }}
{{ device.description }}
</option>
</select>
</div>
@ -392,18 +393,6 @@ function testHamlib() {
data-bs-toggle="collapse"
>
Version
<span
class="badge ms-2"
:class="
updateAvailable === '1' ? 'bg-warning' : 'bg-success'
"
>
{{
updateAvailable === "1"
? "Update available ! ! ! !"
: "Current"
}}</span
>
</button>
</h2>
<div
@ -426,9 +415,6 @@ function testHamlib() {
>
Modem version | {{ state.modem_version }}
</button>
<div :class="updateAvailable === '1' ? '' : 'd-none'">
<infoScreen_updater />
</div>
</div>
</div>
</div>

View File

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

View File

@ -5,9 +5,24 @@ import { setActivePinia } from "pinia";
import pinia from "../store/index";
setActivePinia(pinia);
import { settingsStore as settings } from "../store/settingsStore.js";
import { settingsStore as settings, onChange } from "../store/settingsStore.js";
</script>
<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>

View File

@ -1,6 +1,7 @@
<script setup lang="ts">
import { settingsStore as settings, onChange } from "../store/settingsStore.js";
import { serialDeviceOptions } from "../js/deviceFormHelper";
import { useSerialStore } from "../store/serialStore";
const serialStore = useSerialStore();
</script>
<template>
@ -21,13 +22,13 @@ import { serialDeviceOptions } from "../js/deviceFormHelper";
<div class="input-group input-group-sm mb-1">
<span class="input-group-text" style="width: 180px">Rigctld port</span>
<input
type="text"
type="number"
class="form-control"
placeholder="rigctld port"
id="hamlib_rigctld_port"
aria-label="Device Port"
@change="onChange"
v-model="settings.remote.RIGCTLD.port"
v-model.number="settings.remote.RIGCTLD.port"
/>
</div>
@ -334,18 +335,16 @@ import { serialDeviceOptions } from "../js/deviceFormHelper";
<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"
class="form-select form-select-sm"
>
<option
v-for="option in serialDeviceOptions()"
v-bind:value="option.port"
v-for="device in serialStore.serialDevices"
:value="device.port"
:key="device.port"
>
{{ option.description }}
{{ device.description }}
</option>
</select>
</div>
@ -422,18 +421,18 @@ import { serialDeviceOptions } from "../js/deviceFormHelper";
<div class="input-group input-group-sm mb-1">
<span class="input-group-text" style="width: 180px">PTT device port</span>
<select
class="form-select form-select-sm"
aria-label=".form-select-sm"
id="hamlib_ptt_port"
@change="onChange"
v-model="settings.remote.RADIO.ptt_port"
class="form-select form-select-sm"
>
<option
v-for="option in serialDeviceOptions()"
v-bind:value="option.port"
v-for="device in serialStore.serialDevices"
:value="device.port"
:key="device.port"
>
{{ option.description }}
{{ device.description }}
</option>
</select>
</div>

View File

@ -6,7 +6,9 @@ import { useStateStore } from "../store/stateStore.js";
const state = useStateStore(pinia);
import { startModem, stopModem } from "../js/api.js";
import { audioInputOptions, audioOutputOptions } from "../js/deviceFormHelper";
import { useAudioStore } from "../store/audioStore";
const audioStore = useAudioStore();
</script>
<template>
@ -74,8 +76,8 @@ import { audioInputOptions, audioOutputOptions } from "../js/deviceFormHelper";
@change="onChange"
v-model="settings.remote.AUDIO.input_device"
>
<option v-for="option in audioInputOptions()" v-bind:value="option.id">
{{ option.name }} [{{ option.api }}]
<option v-for="device in audioStore.audioInputs" :value="device.id">
{{ device.name }} [{{ device.api }}]
</option>
</select>
</div>
@ -89,11 +91,12 @@ import { audioInputOptions, audioOutputOptions } from "../js/deviceFormHelper";
@change="onChange"
v-model="settings.remote.AUDIO.output_device"
>
<option v-for="option in audioOutputOptions()" v-bind:value="option.id">
{{ option.name }} [{{ option.api }}]
<option v-for="device in audioStore.audioOutputs" :value="device.id">
{{ device.name }} [{{ device.api }}]
</option>
</select>
</div>
<!-- Audio rx level-->
<div class="input-group input-group-sm mb-1">
<span class="input-group-text w-25">RX Audio Level</span>
@ -161,56 +164,6 @@ import { audioInputOptions, audioOutputOptions } from "../js/deviceFormHelper";
</select>
</div>
<div class="input-group input-group-sm mb-1">
<label class="input-group-text w-25">Tuning range</label>
<label class="input-group-text">fmin</label>
<select
class="form-select form-select-sm"
id="tuning_range_fmin"
@change="onChange"
v-model.number="settings.remote.MODEM.tuning_range_fmin"
>
<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>
<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>
</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">
@ -241,27 +194,4 @@ import { audioInputOptions, audioOutputOptions } from "../js/deviceFormHelper";
</div>
</label>
</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>

View File

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

View File

@ -142,10 +142,18 @@ export async function getModemState() {
return await apiGet("/modem/state");
}
export async function setRadioParameters(frequency, mode, rf_level) {
export async function setRadioParametersFrequency(frequency) {
return await apiPost("/radio", {
radio_frequency: frequency,
});
}
export async function setRadioParametersMode(mode) {
return await apiPost("/radio", {
radio_mode: mode,
});
}
export async function setRadioParametersRFLevel(rf_level) {
return await apiPost("/radio", {
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";
*/
import { displayToast } from "./popupHandler";
import {
getFreedataMessages,
getConfig,
getAudioDevices,
getSerialDevices,
getModemState,
} from "./api";
import { getFreedataMessages, getModemState, getAudioDevices } from "./api";
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 -------------
import { setActivePinia } from "pinia";
@ -29,6 +29,17 @@ import {
getRemote,
} from "../store/settingsStore.js";
export function loadAllData() {
getModemState();
getRemote();
getOverallHealth();
audioStore.loadAudioDevices();
serialStore.loadSerialDevices();
getFreedataMessages();
processFreedataMessages();
processRadioStatus();
}
export function connectionFailed(endpoint, event) {
stateStore.modem_connection = "disconnected";
}
@ -94,11 +105,7 @@ export function eventDispatcher(data) {
switch (data["modem"]) {
case "started":
displayToast("success", "bi-arrow-left-right", "Modem started", 5000);
getModemState();
getConfig();
getAudioDevices();
getSerialDevices();
getFreedataMessages();
loadAllData();
return;
case "stopped":
@ -107,11 +114,7 @@ export function eventDispatcher(data) {
case "restarted":
displayToast("secondary", "bi-bootstrap-reboot", "Modem restarted", 5000);
getModemState();
getConfig();
getAudioDevices();
getSerialDevices();
getFreedataMessages();
loadAllData();
return;
case "failed":
@ -132,18 +135,7 @@ export function eventDispatcher(data) {
displayToast("success", "bi-ethernet", message, 5000);
stateStore.modem_connection = "connected";
getRemote().then(() => {
//initConnections();
getModemState();
});
//getConfig();
getModemState();
getOverallHealth();
getAudioDevices();
getSerialDevices();
getFreedataMessages();
processFreedataMessages();
loadAllData();
return;
@ -186,6 +178,12 @@ export function eventDispatcher(data) {
100;
stateStore.arq_total_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;
case "ABORTING":
@ -228,6 +226,13 @@ export function eventDispatcher(data) {
stateStore.dxcallsign = data["arq-transfer-inbound"].dxcall;
stateStore.arq_transmission_percent = 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;
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
*/
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;
if (
callsign === undefined ||
@ -85,7 +85,7 @@ export function validateCallsignWithSSID(callsign: string) {
* @returns true or false if callsign appears to be valid without an SSID
*/
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 (
callsign === undefined ||

View File

@ -82,8 +82,8 @@ function createSortedMessagesList(data: {
}
export function newMessage(dxcall, body, attachments) {
console.log(attachments);
sendFreedataMessage(dxcall, body, attachments);
chatStore.triggerScrollToBottom();
}
/* ------ 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 newChatMessage = ref();
/* ------------------------------------------------ */
// Scroll to bottom functions
const scrollTrigger = ref(0);
function triggerScrollToBottom() {
scrollTrigger.value++;
}
/* ------------------------------------------------ */
var chat_filter = ref([
@ -92,5 +100,7 @@ export const useChatStore = defineStore("chatStore", () => {
arq_speed_list_bpm,
arq_speed_list_snr,
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

@ -54,15 +54,9 @@ const defaultConfig = {
enable_protocol: false,
},
MODEM: {
enable_fft: false,
enable_fsk: false,
enable_low_bandwidth_mode: false,
respond_to_cq: false,
rx_buffer_size: 0,
tuning_range_fmax: 0,
tuning_range_fmin: 0,
tx_delay: 0,
beacon_interval: 0,
enable_hamc: false,
enable_morse_identifier: false,
},
@ -98,6 +92,9 @@ const defaultConfig = {
tci_ip: "127.0.0.1",
tci_port: 0,
},
MESSAGES: {
enable_auto_repeat: false,
},
},
};

View File

@ -53,6 +53,7 @@ export const useStateStore = defineStore("stateStore", () => {
var arq_speed_list_bpm = 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_timeout = ref();
var arq_seconds_until_timeout_percent = ref();

View File

@ -1,5 +1,16 @@
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):
#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

View File

@ -52,17 +52,23 @@ class ARQDataTypeHandler:
return session_type
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)
self.state_manager.setARQ(False)
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:
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)
self.state_manager.setARQ(False)
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:
self.log(f"Unknown handling endpoint: {session_type}", isWarning=True)
@ -72,10 +78,13 @@ class ARQDataTypeHandler:
else:
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)
self.state_manager.setARQ(False)
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:
self.log(f"Unknown handling endpoint: {session_type}", isWarning=True)
@ -88,14 +97,14 @@ class ARQDataTypeHandler:
self.log(f"Preparing uncompressed data: {len(data)} Bytes")
return data
def handle_raw(self, data):
def handle_raw(self, data, statistics):
self.log(f"Handling uncompressed data: {len(data)} Bytes")
return data
def failed_raw(self, data):
def failed_raw(self, data, statistics):
return
def transmitted_raw(self, data):
def transmitted_raw(self, data, statistics):
return data
def prepare_raw_lzma(self, data):
@ -103,15 +112,15 @@ class ARQDataTypeHandler:
self.log(f"Preparing LZMA compressed data: {len(data)} Bytes >>> {len(compressed_data)} Bytes")
return compressed_data
def handle_raw_lzma(self, data):
def handle_raw_lzma(self, data, statistics):
decompressed_data = lzma.decompress(data)
self.log(f"Handling LZMA compressed data: {len(decompressed_data)} Bytes from {len(data)} Bytes")
return decompressed_data
def failed_raw_lzma(self, data):
def failed_raw_lzma(self, data, statistics):
return
def transmitted_raw_lzma(self, data):
def transmitted_raw_lzma(self, data, statistics):
decompressed_data = lzma.decompress(data)
return decompressed_data
@ -120,15 +129,15 @@ class ARQDataTypeHandler:
self.log(f"Preparing GZIP compressed data: {len(data)} Bytes >>> {len(compressed_data)} Bytes")
return compressed_data
def handle_raw_gzip(self, data):
def handle_raw_gzip(self, data, statistics):
decompressed_data = gzip.decompress(data)
self.log(f"Handling GZIP compressed data: {len(decompressed_data)} Bytes from {len(data)} Bytes")
return decompressed_data
def failed_raw_gzip(self, data):
def failed_raw_gzip(self, data, statistics):
return
def transmitted_raw_gzip(self, data):
def transmitted_raw_gzip(self, data, statistics):
decompressed_data = gzip.decompress(data)
return decompressed_data
@ -137,19 +146,19 @@ class ARQDataTypeHandler:
self.log(f"Preparing LZMA compressed P2PMSG data: {len(data)} Bytes >>> {len(compressed_data)} Bytes")
return compressed_data
def handle_p2pmsg_lzma(self, data):
def handle_p2pmsg_lzma(self, data, statistics):
decompressed_data = lzma.decompress(data)
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
def failed_p2pmsg_lzma(self, data):
def failed_p2pmsg_lzma(self, data, statistics):
decompressed_data = lzma.decompress(data)
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
def transmitted_p2pmsg_lzma(self, data):
def transmitted_p2pmsg_lzma(self, data, statistics):
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

View File

@ -1,3 +1,4 @@
import datetime
import queue, threading
import codec2
import data_frame_factory
@ -44,6 +45,8 @@ class ARQSession():
self.modem = modem
self.speed_level = 0
self.previous_speed_level = 0
self.frames_per_burst = 1
self.frame_factory = data_frame_factory.DataFrameFactory(self.config)
@ -55,6 +58,11 @@ class ARQSession():
self.session_ended = 0
self.session_max_age = 500
# 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}"
logger = self.logger.warn if isWarning else self.logger.info
@ -84,7 +92,7 @@ class ARQSession():
)
def set_details(self, snr, frequency_offset):
self.snr.append(snr)
self.snr = snr
self.frequency_offset = frequency_offset
def on_frame_received(self, frame):
@ -96,9 +104,7 @@ class ARQSession():
action_name = self.STATE_TRANSITION[self.state][frame_type]
received_data, type_byte = getattr(self, action_name)(frame)
if isinstance(received_data, bytearray) and isinstance(type_byte, int):
self.arq_data_type_handler.dispatch(type_byte, received_data)
self.states.setARQ(False)
self.arq_data_type_handler.dispatch(type_byte, received_data, self.update_histograms(len(received_data), len(received_data)))
return
self.log(f"Ignoring unknown transition from state {self.state.name} with frame {frame['frame_type']}")
@ -110,22 +116,49 @@ class ARQSession():
return False
def calculate_session_duration(self):
if self.session_ended == 0:
return time.time() - 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()
total_bytes = self.total_length
#total_bytes = self.total_length
# self.total_length
duration_in_minutes = duration / 60 # Convert duration from seconds to minutes
# Calculate bytes per minute
if duration_in_minutes > 0:
bytes_per_minute = int(total_bytes / duration_in_minutes)
bytes_per_minute = int(confirmed_bytes / duration_in_minutes)
else:
bytes_per_minute = 0
# Convert histograms lists to dictionaries
time_histogram_dict = {i: timestamp for i, timestamp in enumerate(self.time_histogram)}
snr_histogram_dict = {i: snr for i, snr in enumerate(self.snr_histogram)}
bpm_histogram_dict = {i: bpm for i, bpm in enumerate(self.bpm_histogram)}
return {
'total_bytes': total_bytes,
'duration': duration,
'bytes_per_minute': bytes_per_minute
}
'total_bytes': total_bytes,
'duration': duration,
'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())
return stats
def get_appropriate_speed_level(self, snr):
# Start with the lowest speed level as default
# In case of a not fitting SNR, we return the lowest speed level
appropriate_speed_level = min(self.SPEED_LEVEL_DICT.keys())
for level, details in self.SPEED_LEVEL_DICT.items():
if snr >= details['min_snr'] and level > appropriate_speed_level:
appropriate_speed_level = level
return appropriate_speed_level

View File

@ -76,13 +76,8 @@ class ARQSessionIRS(arq_session.ARQSession):
self.received_bytes = 0
self.received_crc = None
self.transmitted_acks = 0
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):
return self.total_length == self.received_bytes
@ -96,9 +91,7 @@ class ARQSessionIRS(arq_session.ARQSession):
self.log(f"Waiting {timeout} seconds...")
if not self.event_frame_received.wait(timeout):
self.log("Timeout waiting for ISS. Session failed.")
self.session_ended = time.time()
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())
self.transmission_failed()
def launch_transmit_and_wait(self, frame, timeout, mode):
thread_wait = threading.Thread(target = self.transmit_and_wait,
@ -112,7 +105,7 @@ class ARQSessionIRS(arq_session.ARQSession):
self.id,
self.dxcall,
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)
if not self.abort:
self.set_state(IRS_State.OPEN_ACK_SENT)
@ -126,14 +119,13 @@ class ARQSessionIRS(arq_session.ARQSession):
self.dx_snr.append(info_frame['snr'])
self.type_byte = info_frame['type']
self.calibrate_speed_settings()
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.calibrate_speed_settings()
self.set_decode_mode()
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.launch_transmit_and_wait(info_ack, self.TIMEOUT_CONNECT, mode=FREEDV_MODE.signalling)
if not self.abort:
@ -159,23 +151,26 @@ class ARQSessionIRS(arq_session.ARQSession):
self.received_bytes += len(data_part)
self.log(f"Received {self.received_bytes}/{self.total_length} bytes")
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
def receive_data(self, 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():
self.calibrate_speed_settings(burst_frame=burst_frame)
ack = self.frame_factory.build_arq_burst_ack(
self.id, self.received_bytes,
self.speed_level, self.frames_per_burst, self.snr[0], flag_abort=self.abort)
self.id,
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.launch_transmit_and_wait(ack, self.TIMEOUT_DATA, mode=FREEDV_MODE.signalling)
return None, None
@ -186,7 +181,7 @@ class ARQSessionIRS(arq_session.ARQSession):
self.received_bytes,
self.speed_level,
self.frames_per_burst,
self.snr[0],
self.snr,
flag_final=True,
flag_checksum=True)
self.transmit_frame(ack, mode=FREEDV_MODE.signalling)
@ -194,7 +189,7 @@ class ARQSessionIRS(arq_session.ARQSession):
self.session_ended = time.time()
self.set_state(IRS_State.ENDED)
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
else:
@ -203,28 +198,53 @@ class ARQSessionIRS(arq_session.ARQSession):
self.received_bytes,
self.speed_level,
self.frames_per_burst,
self.snr[0],
self.snr,
flag_final=True,
flag_checksum=False)
self.transmit_frame(ack, mode=FREEDV_MODE.signalling)
self.log("CRC fail at the end of transmission!")
self.session_ended = time.time()
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
return self.transmission_failed()
def calibrate_speed_settings(self):
self.speed_level = 0 # for now stay at lowest speed level
return
# if we have two ACKS, then consider increasing speed level
if self.transmitted_acks >= 2:
self.transmitted_acks = 0
new_speed_level = min(self.speed_level + 1, len(self.SPEED_LEVEL_DICT) - 1)
def calibrate_speed_settings(self, burst_frame=None):
if burst_frame:
received_speed_level = burst_frame['speed_level']
else:
received_speed_level = 0
# check first if the next mode supports the actual snr
if self.snr[0] >= self.SPEED_LEVEL_DICT[new_speed_level]["min_snr"]:
self.speed_level = new_speed_level
latest_snr = self.snr if self.snr else -10
appropriate_speed_level = self.get_appropriate_speed_level(latest_snr)
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):
self.log(f"Aborting transmission... setting abort flag")
@ -234,6 +254,16 @@ class ARQSessionIRS(arq_session.ARQSession):
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.set_state(IRS_State.ABORTED)
self.states.setARQ(False)
self.event_manager.send_arq_session_finished(
False, self.id, self.dxcall, False, self.state.name, statistics=self.calculate_session_statistics())
return None, None
False, self.id, self.dxcall, False, self.state.name, statistics=self.calculate_session_statistics(self.received_bytes, self.total_length))
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(f"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

@ -76,8 +76,7 @@ class ARQSessionISS(arq_session.ARQSession):
if len(self.state_manager.arq_iss_sessions) >= 255:
return False
def transmit_wait_and_retry(self, frame_or_burst, timeout, retries, mode):
def transmit_wait_and_retry(self, frame_or_burst, timeout, retries, mode, isARQBurst=False, ):
while retries > 0:
self.event_frame_received = threading.Event()
if isinstance(frame_or_burst, list): burst = frame_or_burst
@ -90,12 +89,19 @@ class ARQSessionISS(arq_session.ARQSession):
return
self.log("Timeout!")
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.transmission_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)
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, isARQBurst], daemon=True)
twr.start()
def start(self):
@ -105,11 +111,27 @@ class ARQSessionISS(arq_session.ARQSession):
self.launch_twr(session_open_frame, self.TIMEOUT_CONNECT_ACK, self.RETRIES_CONNECT, mode=FREEDV_MODE.signalling)
self.set_state(ISS_State.OPEN_SENT)
def set_speed_and_frames_per_burst(self, frame):
self.speed_level = frame['speed_level']
self.log(f"Speed level set to {self.speed_level}")
self.frames_per_burst = frame['frames_per_burst']
self.log(f"Frames per burst set to {self.frames_per_burst}")
def update_speed_level(self, frame):
self.log("---------------------------------------------------------", isWarning=True)
# Log the received frame for debugging
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):
# 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,
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.set_state(ISS_State.INFO_SENT)
@ -127,14 +149,15 @@ class ARQSessionISS(arq_session.ARQSession):
return None, None
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:
self.confirmed_bytes = irs_frame['offset']
self.log(f"IRS confirmed {self.confirmed_bytes}/{self.total_length} bytes")
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
if irs_frame["flag"]["ABORT"]:
@ -156,9 +179,9 @@ class ARQSessionISS(arq_session.ARQSession):
payload = self.data[offset : offset + payload_size]
data_frame = self.frame_factory.build_arq_burst_frame(
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)
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)
return None, None
@ -167,10 +190,10 @@ class ARQSessionISS(arq_session.ARQSession):
self.session_ended = time.time()
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.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))
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.states.setARQ(False)
self.arq_data_type_handler.transmitted(self.type_byte, self.data)
return None, None
def transmission_failed(self, irs_frame=None):
@ -178,11 +201,10 @@ class ARQSessionISS(arq_session.ARQSession):
self.session_ended = time.time()
self.set_state(ISS_State.FAILED)
self.log(f"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.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
def abort_transmission(self, irs_frame=None):
@ -191,7 +213,7 @@ class ARQSessionISS(arq_session.ARQSession):
self.set_state(ISS_State.ABORTING)
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
self.event_frame_received.set()
@ -211,7 +233,7 @@ class ARQSessionISS(arq_session.ARQSession):
self.event_frame_received.set()
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.states.setARQ(False)
return None, None

View File

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

View File

@ -22,14 +22,18 @@ class ARQRawCommand(TxCommand):
self.data = base64.b64decode(apiParams['data'])
def run(self, event_queue: Queue, modem):
self.emit_event(event_queue)
self.logger.info(self.log_message())
try:
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

View File

@ -15,7 +15,7 @@ class SendMessageCommand(TxCommand):
def set_params_from_api(self, apiParams):
origin = f"{self.config['STATION']['mycall']}-{self.config['STATION']['myssid']}"
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):
@ -23,28 +23,35 @@ class SendMessageCommand(TxCommand):
self.log("Modem busy, waiting until ready...")
return
if not modem:
self.log("Modem not running...", isWarning=True)
return
first_queued_message = DatabaseManagerMessages(self.event_manager).get_first_queued_message()
if not first_queued_message:
self.log("No queued message in database.")
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']}")
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)
# Convert JSON string to bytes (using UTF-8 encoding)
payload = message.to_payload().encode('utf-8')
json_bytearray = bytearray(payload)
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)
payload = message.to_payload().encode('utf-8')
json_bytearray = bytearray(payload)
data, data_type = self.arq_data_type_handler.prepare(json_bytearray, ARQ_SESSION_TYPES.p2pmsg_lzma)
iss = ARQSessionISS(self.config,
modem,
self.message.destination,
self.state_manager,
data,
data_type
)
iss = ARQSessionISS(self.config,
modem,
self.message.destination,
self.state_manager,
data,
data_type
)
self.state_manager.register_arq_iss_session(iss)
iss.start()
self.state_manager.register_arq_iss_session(iss)
iss.start()
except Exception as e:
self.log(f"Error starting ARQ session: {e}", isWarning=True)

View File

@ -1,10 +1,11 @@
[NETWORK]
modemport = 3050
modemaddress = 127.0.0.1
modemport = 5000
[STATION]
mycall = XX1XXX
mygrid = AA12aa
myssid = 6
mycall = AA1AAA
mygrid = JN48ea
myssid = 1
ssid_list = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
enable_explorer = True
enable_stats = True
@ -45,13 +46,11 @@ enable_protocol = False
[MODEM]
enable_hmac = False
tuning_range_fmax = 50
tuning_range_fmin = -50
enable_fsk = False
enable_low_bandwidth_mode = False
enable_morse_identifier = False
respond_to_cq = True
rx_buffer_size = 64
tx_delay = 200
beacon_interval = 300
[MESSAGES]
enable_auto_repeat = False

View File

@ -10,6 +10,7 @@ class CONFIG:
config_types = {
'NETWORK': {
'modemaddress': str,
'modemport': int,
},
'STATION': {
@ -31,7 +32,6 @@ class CONFIG:
'control': str,
'serial_port': str,
'model_id': int,
'serial_port': str,
'serial_speed': int,
'data_bits': int,
'stop_bits': int,
@ -56,24 +56,28 @@ class CONFIG:
'enable_protocol': bool,
},
'MODEM': {
'enable_fft': bool,
'tuning_range_fmax': int,
'tuning_range_fmin': int,
'enable_fsk': bool,
'enable_hmac': bool,
'enable_morse_identifier': bool,
'enable_low_bandwidth_mode': bool,
'respond_to_cq': bool,
'rx_buffer_size': int,
'tx_delay': int,
'beacon_interval': int,
'tx_delay': int
},
'MESSAGES': {
'enable_auto_repeat': bool,
}
}
default_values = {
list: '[]',
bool: 'False',
int: '0',
str: '',
}
def __init__(self, configfile: str):
# set up logger
self.log = structlog.get_logger("CONFIG")
self.log = structlog.get_logger(type(self).__name__)
# init configparser
self.parser = configparser.ConfigParser(inline_comment_prefixes="#", allow_no_value=True)
@ -88,6 +92,9 @@ class CONFIG:
# check if config file exists
self.config_exists()
# validate config structure
self.validate_config()
def config_exists(self):
"""
check if config file exists
@ -99,7 +106,7 @@ class CONFIG:
return False
# Validates config data
def validate(self, data):
def validate_data(self, data):
for section in data:
for setting in data[section]:
if not isinstance(data[section][setting], self.config_types[section][setting]):
@ -107,6 +114,39 @@ class CONFIG:
f" '{data[section][setting]}' {type(data[section][setting])} given.")
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
# is_writing means data from a dict being writen to the config file
# if False, it means the opposite direction
@ -132,8 +172,7 @@ class CONFIG:
# Sets and writes config data from a dict containing data settings
def write(self, data):
# Validate config data before writing
self.validate(data)
self.validate_data(data)
for section in data:
# init section if it doesn't exist yet
if not section.upper() in self.parser.keys():
@ -142,8 +181,13 @@ class CONFIG:
for setting in data[section]:
new_value = self.handle_setting(
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
try:
with open(self.config_name, 'w') as configfile:

View File

@ -144,6 +144,7 @@ class DataFrameFactory:
self.template_list[FR_TYPE.ARQ_BURST_FRAME.value] = {
"frame_length": None,
"session_id": 1,
"speed_level": 1,
"offset": 4,
"data": "dynamic",
}
@ -394,9 +395,10 @@ class DataFrameFactory:
}
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 = {
"session_id": session_id.to_bytes(1, 'big'),
"speed_level": speed_level.to_bytes(1, 'big'),
"offset": offset.to_bytes(4, 'big'),
"data": data,
}
@ -415,7 +417,6 @@ class DataFrameFactory:
if flag_abort:
flag = helpers.set_flag(flag, 'ABORT', True, self.ARQ_FLAGS)
payload = {
"session_id": session_id.to_bytes(1, 'big'),
"offset": offset.to_bytes(4, 'big'),

View File

@ -4,8 +4,6 @@ import ctypes
import structlog
import threading
import audio
import os
from modem_frametypes import FRAME_TYPE
import itertools
TESTMODE = False
@ -27,20 +25,17 @@ class Demodulator():
'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.tuning_range_fmin = config['MODEM']['tuning_range_fmin']
self.tuning_range_fmax = config['MODEM']['tuning_range_fmax']
self.rx_audio_level = config['AUDIO']['rx_audio_level']
self.service_queue = service_queue
self.AUDIO_FRAMES_PER_BUFFER_RX = 4800
self.buffer_overflow_counter = [0, 0, 0, 0, 0, 0, 0, 0]
self.is_codec2_traffic_counter = 0
self.is_codec2_traffic_cooldown = 5
self.audio_received_queue = audio_rx_q
self.modem_received_queue = modem_rx_q
self.data_queue_received = data_q_rx
self.states = states
@ -79,13 +74,6 @@ class Demodulator():
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
bytes_per_frame = int(
codec2.api.freedv_get_bits_per_modem_frame(c2instance) / 8
@ -136,48 +124,37 @@ class Demodulator():
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)
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)
audio.calculate_fft(audio_8k_level_adjusted, self.fft_queue, self.states)
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)
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)
except Exception as e:
self.log.warning("[AUDIO EXCEPTION]", status=status, time=time, frames=frames, e=e)
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:
"""
@ -247,7 +224,16 @@ class Demodulator():
snr = self.calculate_snr(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 = []
except Exception as e:
error_message = str(e)
@ -257,6 +243,7 @@ class Demodulator():
self.log.debug(
"[MDM] [demod_audio] demod loop ended", mode=mode_name, e=e
)
def tci_rx_callback(self) -> None:
"""
Callback for TCI RX
@ -297,6 +284,7 @@ class Demodulator():
frames_per_burst = min(frames_per_burst, 1)
frames_per_burst = max(frames_per_burst, 5)
# FIXME
frames_per_burst = 1
codec2.api.freedv_set_frames_per_burst(self.dat0_datac1_freedv, frames_per_burst)
@ -382,15 +370,18 @@ class Demodulator():
for mode in self.MODE_DICT:
codec2.api.freedv_set_sync(self.MODE_DICT[mode]["instance"], 0)
def set_decode_mode(self, mode):
for m in self.MODE_DICT: self.MODE_DICT[m]["decode"] = False
def set_decode_mode(self, modes_to_decode):
# Reset all modes to not decode
for m in self.MODE_DICT:
self.MODE_DICT[m]["decode"] = False
# signalling is always true
self.MODE_DICT[codec2.FREEDV_MODE.signalling.value]["decode"] = True
# Enable mode based on speed_level
self.MODE_DICT[mode.value]["decode"] = True
self.log.info(f"[MDM] [demod_audio] set data mode: {mode.name}")
# lowest speed level is alwys true
self.MODE_DICT[codec2.FREEDV_MODE.datac4.value]["decode"] = True
return
# Enable specified modes
for mode, decode in modes_to_decode.items():
if mode in self.MODE_DICT:
self.MODE_DICT[mode]["decode"] = decode

View File

@ -42,7 +42,10 @@ class EventManager:
}
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'
event = {
"type": "arq",
@ -52,6 +55,7 @@ class EventManager:
'received_bytes': received_bytes,
'total_bytes': total_bytes,
'state': state,
'statistics': statistics,
}
}
self.broadcast(event)

View File

@ -9,7 +9,7 @@ Created on 05.11.23
import requests
import threading
import ujson as json
import json
import structlog
import sched
import time

View File

@ -91,7 +91,6 @@ class FrameHandler():
def add_to_heard_stations(self):
frame = self.details['frame']
print(frame)
if 'origin' not in frame:
return

View File

@ -4,6 +4,7 @@ import data_frame_factory
import frame_handler
import datetime
from message_system_db_beacon import DatabaseManagerBeacon
from message_system_db_messages import DatabaseManagerMessages
from message_system_db_manager import DatabaseManager
@ -15,3 +16,7 @@ class BeaconFrameHandler(frame_handler.FrameHandler):
self.details["snr"],
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,6 +2,9 @@ import frame_handler_ping
import helpers
import data_frame_factory
import frame_handler
from message_system_db_messages import DatabaseManagerMessages
class CQFrameHandler(frame_handler_ping.PingFrameHandler):
def should_respond(self):
@ -14,3 +17,7 @@ class CQFrameHandler(frame_handler_ping.PingFrameHandler):
self.details['snr']
)
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

@ -713,15 +713,16 @@ def get_flag(byte, flag_name, flag_dict):
return get_bit(byte, position)
def find_binary_path(binary_name="rigctld", search_system_wide=False):
def find_binary_paths(binary_name="rigctld", search_system_wide=False):
"""
Search for a binary within the current working directory and its subdirectories.
Optionally, check system-wide locations and PATH environment variable if not found.
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: The full path to the binary if found, otherwise None.
: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"
@ -730,7 +731,7 @@ def find_binary_path(binary_name="rigctld", search_system_wide=False):
root_path = os.getcwd()
for dirpath, dirnames, filenames in os.walk(root_path):
if binary_name in filenames:
return os.path.join(dirpath, binary_name)
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:
@ -739,13 +740,16 @@ def find_binary_path(binary_name="rigctld", search_system_wide=False):
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"))
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):
return potential_path
binary_paths.append(potential_path)
return binary_paths
def kill_and_execute(binary_path, additional_args=None):

View File

@ -7,23 +7,28 @@ from message_system_db_messages import DatabaseManagerMessages
#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')
received_message_obj = MessageP2P.from_payload(decompressed_json_string)
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')
payload_message_obj = MessageP2P.from_payload(decompressed_json_string)
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={'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')
payload_message_obj = MessageP2P.from_payload(decompressed_json_string)
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={'statistics': statistics})
class MessageP2P:
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
import json
import hashlib
import os
class DatabaseManagerAttachments(DatabaseManager):
def __init__(self, uri='sqlite:///freedata-messages.db'):
super().__init__(uri)
def __init__(self, db_file=None):
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):

View File

@ -4,14 +4,16 @@ from sqlalchemy.orm import scoped_session, sessionmaker
from threading import local
from message_system_db_model import Base, Beacon, Station, Status, Attachment, P2PMessage
from datetime import timezone, timedelta, datetime
import json
import structlog
import helpers
import os
class DatabaseManagerBeacon(DatabaseManager):
def __init__(self, uri):
super().__init__(uri)
def __init__(self, db_file=None):
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):
session = None

View File

@ -7,12 +7,17 @@ from threading import local
from message_system_db_model import Base, Station, Status
import structlog
import helpers
import os
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
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.session_factory = sessionmaker(bind=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 datetime import datetime
import json
import os
class DatabaseManagerMessages(DatabaseManager):
def __init__(self, uri='sqlite:///freedata-messages.db'):
super().__init__(uri)
self.attachments_manager = DatabaseManagerAttachments(uri)
def __init__(self, db_file=None):
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
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()
try:
# Create and add the origin and destination Stations
@ -34,7 +40,8 @@ class DatabaseManagerMessages(DatabaseManager):
direction=direction,
status_id=status.id if status else None,
is_read=is_read,
attempt=0
attempt=0,
statistics=statistics
)
session.add(new_message)
@ -130,6 +137,8 @@ class DatabaseManagerMessages(DatabaseManager):
message.body = update_data['body']
if 'status' in update_data:
message.status = self.get_or_create_status(session, update_data['status'])
if 'statistics' in update_data:
message.statistics = update_data['statistics']
session.commit()
self.log(f"Updated: {message_id}")
@ -171,21 +180,27 @@ class DatabaseManagerMessages(DatabaseManager):
finally:
session.remove()
def increment_message_attempts(self, message_id):
session = self.get_thread_scoped_session()
def increment_message_attempts(self, message_id, session=None):
own_session = False
if not session:
session = self.get_thread_scoped_session()
own_session = True
try:
message = session.query(P2PMessage).filter_by(id=message_id).first()
if message:
message.attempt += 1
session.commit()
if own_session:
session.commit()
self.log(f"Incremented attempt count for message {message_id}")
else:
self.log(f"Message with ID {message_id} not found")
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}")
finally:
session.remove()
if own_session:
session.remove()
def mark_message_as_read(self, message_id):
session = self.get_thread_scoped_session()
@ -201,4 +216,42 @@ class DatabaseManagerMessages(DatabaseManager):
session.rollback()
self.log(f"An error occurred while marking message {message_id} as read: {e}")
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

@ -65,7 +65,7 @@ class RF:
self.MODEM_SAMPLE_RATE = codec2.api.FREEDV_FS_8000
# 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
self.AUDIO_CHANNELS = 1
self.MODE = 0
@ -74,17 +74,16 @@ class RF:
# 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.data_queue_received = queue.Queue()
self.fft_queue = fft_queue
self.demodulator = demodulator.Demodulator(self.config,
self.audio_received_queue,
self.modem_received_queue,
self.data_queue_received,
self.states,
self.event_manager,
self.service_queue,
self.fft_queue
)
@ -109,7 +108,6 @@ class RF:
# Initialize codec2, rig control, and data threads
self.init_codec2()
self.init_data_threads()
return True
@ -405,11 +403,6 @@ class RF:
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):
worker_received = threading.Thread(
target=self.demodulator.worker_received, name="WORKER_THREAD", daemon=True
)
worker_received.start()
# Low level modem audio transmit
def transmit_audio(self, audio_48k) -> None:

View File

@ -76,7 +76,11 @@ class radio:
self.connection.sendall(command.encode('utf-8') + b"\n")
response = self.connection.recv(1024)
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:
self.log.warning(f"[RIGCTLD] Error sending command [{command}] to rigctld: {err}")
self.connected = False
@ -189,36 +193,93 @@ class radio:
self.connect()
if self.connected:
self.parameters['frequency'] = self.send_command('f')
response = self.send_command(
'm').strip() # Get the mode/bandwidth response and remove leading/trailing spaces
try:
mode, bandwidth = response.split('\n', 1) # Split the response into mode and bandwidth
except ValueError:
self.get_frequency()
self.get_mode_bandwidth()
self.get_alc()
self.get_strength()
self.get_rf()
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'
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['bandwidth'] = bandwidth
self.parameters['alc'] = self.send_command('l ALC')
self.parameters['strength'] = self.send_command('l STRENGTH')
self.parameters['rf'] = self.send_command('l RFPOWER') # RF, RFPOWER
def get_alc(self):
try:
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'
"""Return the latest fetched parameters."""
return self.parameters
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_path = helpers.find_binary_path(binary_name, search_system_wide=True)
binary_paths = helpers.find_binary_paths(binary_name, search_system_wide=True)
additional_args = self.format_rigctld_args()
if binary_path:
self.log.info(f"Rigctld binary found at: {binary_path}")
helpers.kill_and_execute(binary_path, additional_args)
self.log.info(f"Executed rigctld...")
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
@ -229,29 +290,36 @@ class radio:
return value in ['ignore', 0]
# Model ID, Serial Port, and Speed
if not should_ignore(config.get('model_id', "0")):
if not should_ignore(config.get('model_id')):
args += ['-m', str(config['model_id'])]
if not should_ignore(config.get('serial_port', "0")):
if not should_ignore(config.get('serial_port')):
args += ['-r', config['serial_port']]
if not should_ignore(config.get('serial_speed', "0")):
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', "0")):
if not should_ignore(config.get('ptt_port')):
args += ['--ptt-port', config['ptt_port']]
if not should_ignore(config.get('ptt_type', "0")):
if not should_ignore(config.get('ptt_type')):
args += ['--ptt-type', config['ptt_type']]
# Serial DCD and DTR
if not should_ignore(config.get('serial_dcd', "0")):
args += ['--set-dcd', config['serial_dcd']]
if not should_ignore(config.get('serial_dtr', "0")):
args += ['--set-dtr', config['serial_dtr']]
if not should_ignore(config.get('serial_dcd')):
args += ['--dcd-type', config['serial_dcd']]
# Handling Stop Bits with the corrected --set-conf syntax
if not should_ignore(config.get('stop_bits', "0")):
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(" ")

View File

@ -16,13 +16,12 @@ class ScheduleManager:
self.state_manager = state_manger
self.event_manager = event_manager
self.config = self.config_manager.read()
self.beacon_interval = self.config['MODEM']['beacon_interval']
self.scheduler = sched.scheduler(time.time, time.sleep)
self.events = {
'check_for_queued_messages': {'function': self.check_for_queued_messages, 'interval': 10},
'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},
}
self.running = False # Flag to control the running state
@ -65,24 +64,36 @@ class ScheduleManager:
self.scheduler_thread.join()
def transmit_beacon(self):
if not self.state_manager.getARQ() and self.state_manager.is_beacon_running:
cmd = command_beacon.BeaconCommand(self.config, self.state_manager, self.event_manager)
cmd.run(self.event_manager, self.modem)
try:
if not self.state_manager.getARQ() and self.state_manager.is_beacon_running:
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):
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):
self.config = self.config_manager.read()
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):
if not self.state_manager.getARQ():
if DatabaseManagerMessages(self.event_manager).get_first_queued_message():
params = DatabaseManagerMessages(self.event_manager).get_first_queued_message()
command = command_message_send.SendMessageCommand(self.config_manager.read(), self.state_manager, self.event_manager, params)
command.transmit(self.modem)
try:
if first_queued_message := DatabaseManagerMessages(
self.event_manager
).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

View File

@ -1,3 +1,5 @@
import time
from flask import Flask, request, jsonify, make_response, abort, Response
from flask_sock import Sock
from flask_cors import CORS
@ -9,16 +11,19 @@ import audio
import queue
import service_manager
import state_manager
import ujson as json
import json
import websocket_manager as wsm
import api_validations as validations
import command_cq
import command_beacon
import command_ping
import command_feq
import command_test
import command_arq_raw
import command_message_send
import event_manager
import atexit
from message_system_db_manager import DatabaseManager
from message_system_db_messages import DatabaseManagerMessages
from message_system_db_attachments import DatabaseManagerAttachments
@ -26,17 +31,17 @@ from message_system_db_beacon import DatabaseManagerBeacon
from schedule_manager import ScheduleManager
app = Flask(__name__)
CORS(app)
CORS(app, resources={r"/*": {"origins": "*"}})
sock = Sock(app)
MODEM_VERSION = "0.13.4-alpha"
MODEM_VERSION = "0.14.3-alpha"
# set config file to use
def set_config():
if 'FREEDATA_CONFIG' in os.environ:
config_file = os.environ['FREEDATA_CONFIG']
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):
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
def enqueue_tx_command(cmd_class, params = {}):
command = cmd_class(app.config_manager.read(), app.state_manager, app.event_manager, params)
app.logger.info(f"Command {command.get_name()} running...")
if command.run(app.modem_events, app.service_manager.modem): # TODO remove the app.modem_event custom queue
return True
return False
try:
command = cmd_class(app.config_manager.read(), app.state_manager, app.event_manager, params)
app.logger.info(f"Command {command.get_name()} running...")
if command.run(app.modem_events, app.service_manager.modem): # TODO remove the app.modem_event custom queue
return True
except Exception as e:
app.logger.warning(f"Command {command.get_name()} failed...: {e}")
return False
## REST API
@app.route('/', methods=['GET'])
@ -93,11 +101,18 @@ def index():
@app.route('/config', methods=['GET', 'POST'])
def config():
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)
app.modem_service.put("restart")
if not set_config:
response = api_response(None, 'error writing config')
else:
app.modem_service.put("restart")
response = api_response(set_config)
return response
elif request.method == 'GET':
@ -139,6 +154,8 @@ def post_beacon():
if not app.state_manager.is_beacon_running:
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:
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:
api_abort('Modem not running', 503)
for id in app.state_manager.arq_irs_sessions:
app.state_manager.arq_irs_sessions[id].abort_transmission()
for id in app.state_manager.arq_iss_sessions:
app.state_manager.arq_iss_sessions[id].abort_transmission()
if app.state_manager.getARQ():
for id in app.state_manager.arq_irs_sessions:
app.state_manager.arq_irs_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)
@app.route('/radio', methods=['GET', 'POST'])
def get_post_radio():
if request.method in ['POST']:
app.radio_manager.set_frequency(request.json['radio_frequency'])
app.radio_manager.set_mode(request.json['radio_mode'])
app.radio_manager.set_rf_level(int(request.json['radio_rf_level']))
if "radio_frequency" in request.json:
app.radio_manager.set_frequency(request.json['radio_frequency'])
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)
elif request.method == 'GET':
@ -244,11 +265,12 @@ def get_post_radio():
def get_post_freedata_message():
if request.method in ['GET']:
result = DatabaseManagerMessages(app.event_manager).get_all_messages_json()
return api_response(result, 200)
if enqueue_tx_command(command_message_send.SendMessageCommand, request.json):
return api_response(request.json, 200)
else:
api_abort('Error executing command...', 500)
return api_response(result)
if request.method in ['POST']:
enqueue_tx_command(command_message_send.SendMessageCommand, request.json)
return api_response(request.json)
api_abort('Error executing command...', 500)
@app.route('/freedata/messages/<string:message_id>', methods=['GET', 'POST', 'PATCH', 'DELETE'])
def handle_freedata_message(message_id):
@ -302,6 +324,11 @@ def sock_fft(sock):
def sock_states(sock):
wsm.handle_connection(sock, wsm.states_client_list, app.state_queue)
@atexit.register
def stop_server():
app.service_manager.stop_modem()
time.sleep(1)
print('Server shutdown...')
if __name__ == "__main__":
@ -329,4 +356,13 @@ if __name__ == "__main__":
# initialize database default values
DatabaseManager(app.event_manager).initialize_default_values()
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

@ -1,5 +1,4 @@
import time
import ujson as json
import threading
import numpy as np
class StateManager:

View File

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

View File

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

View File

@ -103,7 +103,7 @@ class TestARQSession(unittest.TestCase):
def waitForSession(self, q, outbound = False):
key = 'arq-transfer-outbound' if outbound else 'arq-transfer-inbound'
while True:
while True and self.channels_running:
ev = q.get()
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.")
@ -125,16 +125,17 @@ class TestARQSession(unittest.TestCase):
def waitAndCloseChannels(self):
self.waitForSession(self.iss_event_queue, True)
self.channels_running = False
self.waitForSession(self.irs_event_queue, False)
self.channels_running = False
def testARQSessionSmallPayload(self):
# set Packet Error Rate (PER) / frame loss probability
self.loss_probability = 0
self.loss_probability = 30
self.establishChannels()
params = {
'dxcall': "XX1XXX-1",
'dxcall': "AA1AAA-1",
'data': base64.b64encode(bytes("Hello world!", encoding="utf-8")),
'type': "raw_lzma"
}
@ -143,13 +144,13 @@ class TestARQSession(unittest.TestCase):
self.waitAndCloseChannels()
del cmd
def DisabledtestARQSessionLargePayload(self):
def testARQSessionLargePayload(self):
# set Packet Error Rate (PER) / frame loss probability
self.loss_probability = 0
self.establishChannels()
params = {
'dxcall': "XX1XXX-1",
'dxcall': "AA1AAA-1",
'data': base64.b64encode(np.random.bytes(1000)),
'type': "raw_lzma"
}
@ -165,7 +166,7 @@ class TestARQSession(unittest.TestCase):
self.establishChannels()
params = {
'dxcall': "XX1XXX-1",
'dxcall': "AA1AAA-1",
'data': base64.b64encode(np.random.bytes(100)),
}
cmd = ARQRawCommand(self.config, self.iss_state_manager, self.iss_event_queue, params)
@ -184,7 +185,7 @@ class TestARQSession(unittest.TestCase):
self.establishChannels()
params = {
'dxcall': "XX1XXX-1",
'dxcall': "AA1AAA-1",
'data': base64.b64encode(np.random.bytes(100)),
}
cmd = ARQRawCommand(self.config, self.iss_state_manager, self.iss_event_queue, params)
@ -200,7 +201,7 @@ class TestARQSession(unittest.TestCase):
def testSessionCleanupISS(self):
params = {
'dxcall': "XX1XXX-1",
'dxcall': "AA1AAA-1",
'data': base64.b64encode(np.random.bytes(100)),
}
cmd = ARQRawCommand(self.config, self.iss_state_manager, self.iss_event_queue, params)

View File

@ -1,3 +1,5 @@
import sys
sys.path.append('modem')
import unittest
import config
@ -11,8 +13,8 @@ class TestConfigMethods(unittest.TestCase):
c = config.CONFIG('modem/config.ini.example')
self.assertTrue(c.config_exists())
c = config.CONFIG('modem/nonexistant.ini')
self.assertFalse(c.config_exists())
#c = config.CONFIG('modem/nonexistant')
#self.assertFalse(c.config_exists())
def test_read(self):
data = self.config.read()
@ -42,10 +44,10 @@ class TestConfigMethods(unittest.TestCase):
def test_validate_data(self):
data = {'STATION': {'ssid_list': "abc"}}
with self.assertRaises(ValueError):
self.config.validate(data)
self.config.validate_data(data)
data = {'STATION': {'ssid_list': [1, 2, 3]}}
self.assertIsNone(self.config.validate(data))
self.assertIsNone(self.config.validate_data(data))
if __name__ == '__main__':

View File

@ -49,7 +49,7 @@ class TestDataFrameFactory(unittest.TestCase):
offset = 40
payload = b'Hello World!'
frame = self.factory.build_arq_burst_frame(FREEDV_MODE.datac3,
session_id, offset, payload)
session_id, offset, payload, 0)
frame_data = self.factory.deconstruct(frame)
self.assertEqual(frame_data['session_id'], session_id)
self.assertEqual(frame_data['offset'], offset)
@ -58,11 +58,11 @@ class TestDataFrameFactory(unittest.TestCase):
payload = payload * 1000
self.assertRaises(OverflowError, self.factory.build_arq_burst_frame,
FREEDV_MODE.datac3, session_id, offset, payload)
FREEDV_MODE.datac3, session_id, offset, payload, 0)
def testAvailablePayload(self):
avail = self.factory.get_available_data_payload_for_mode(FRAME_TYPE.ARQ_BURST_FRAME, FREEDV_MODE.datac3)
self.assertEqual(avail, 120) # 128 bytes datac3 frame payload - BURST frame overhead
self.assertEqual(avail, 119) # 128 bytes datac3 frame payload - BURST frame overhead
if __name__ == '__main__':
unittest.main()

View File

@ -22,21 +22,21 @@ class TestDispatcher(unittest.TestCase):
# Example usage
example_data = b"Hello FreeDATA!"
formatted_data, type_byte = self.arq_data_type_handler.prepare(example_data, ARQ_SESSION_TYPES.raw)
dispatched_data = self.arq_data_type_handler.dispatch(type_byte, formatted_data)
dispatched_data = self.arq_data_type_handler.dispatch(type_byte, formatted_data, statistics={})
self.assertEqual(example_data, dispatched_data)
def testDataTypeHandlerLZMA(self):
# Example usage
example_data = b"Hello FreeDATA!"
formatted_data, type_byte = self.arq_data_type_handler.prepare(example_data, ARQ_SESSION_TYPES.raw_lzma)
dispatched_data = self.arq_data_type_handler.dispatch(type_byte, formatted_data)
dispatched_data = self.arq_data_type_handler.dispatch(type_byte, formatted_data, statistics={})
self.assertEqual(example_data, dispatched_data)
def testDataTypeHandlerGZIP(self):
# Example usage
example_data = b"Hello FreeDATA!"
formatted_data, type_byte = self.arq_data_type_handler.prepare(example_data, ARQ_SESSION_TYPES.raw_gzip)
dispatched_data = self.arq_data_type_handler.dispatch(type_byte, formatted_data)
dispatched_data = self.arq_data_type_handler.dispatch(type_byte, formatted_data, statistics={})
self.assertEqual(example_data, dispatched_data)

View File

@ -37,7 +37,7 @@ class TestDataFrameFactory(unittest.TestCase):
payload = message.to_payload()
received_message = MessageP2P.from_payload(payload)
received_message_dict = MessageP2P.to_dict(received_message)
self.database_manager.add_message(received_message_dict)
self.database_manager.add_message(received_message_dict, statistics={})
result = self.database_manager.get_message_by_id(message.id)
self.assertEqual(result["destination"], message.destination)
@ -53,7 +53,7 @@ class TestDataFrameFactory(unittest.TestCase):
payload = message.to_payload()
received_message = MessageP2P.from_payload(payload)
received_message_dict = MessageP2P.to_dict(received_message)
self.database_manager.add_message(received_message_dict)
self.database_manager.add_message(received_message_dict, statistics={})
result = self.database_manager.get_all_messages()
message_id = result[0]["id"]
@ -75,7 +75,7 @@ class TestDataFrameFactory(unittest.TestCase):
received_message = MessageP2P.from_payload(payload)
received_message_dict = MessageP2P.to_dict(received_message)
print(received_message_dict)
message_id = self.database_manager.add_message(received_message_dict, direction='receive')
message_id = self.database_manager.add_message(received_message_dict, statistics={}, direction='receive')
print(message_id)
self.database_manager.update_message(message_id, {'body' : 'hello123'})
@ -103,7 +103,7 @@ class TestDataFrameFactory(unittest.TestCase):
payload = message.to_payload()
received_message = MessageP2P.from_payload(payload)
received_message_dict = MessageP2P.to_dict(received_message)
message_id = self.database_manager.add_message(received_message_dict)
message_id = self.database_manager.add_message(received_message_dict, statistics={})
result = self.database_manager_attachments.get_attachments_by_message_id(message_id)
attachment_names = [attachment['name'] for attachment in result]
self.assertIn('test1.gif', attachment_names)
@ -116,7 +116,7 @@ class TestDataFrameFactory(unittest.TestCase):
payload = message.to_payload()
received_message = MessageP2P.from_payload(payload)
received_message_dict = MessageP2P.to_dict(received_message)
message_id = self.database_manager.add_message(received_message_dict)
message_id = self.database_manager.add_message(received_message_dict,statistics={},)
self.database_manager.increment_message_attempts(message_id)
@ -129,7 +129,7 @@ class TestDataFrameFactory(unittest.TestCase):
payload = message.to_payload()
received_message = MessageP2P.from_payload(payload)
received_message_dict = MessageP2P.to_dict(received_message)
message_id = self.database_manager.add_message(received_message_dict, is_read=False)
message_id = self.database_manager.add_message(received_message_dict, statistics={},is_read=False)
self.database_manager.mark_message_as_read(message_id)
result = self.database_manager.get_message_by_id(message_id)

View File

@ -60,7 +60,7 @@ class TestDataFrameFactory(unittest.TestCase):
payload = message.to_payload()
received_message = MessageP2P.from_payload(payload)
received_message_dict = MessageP2P.to_dict(received_message)
self.database_manager.add_message(received_message_dict)
self.database_manager.add_message(received_message_dict, statistics={})
self.assertEqual(message.origin, received_message.origin)
self.assertEqual(message.destination, received_message.destination)

View File

@ -108,7 +108,7 @@ class TestMessageProtocol(unittest.TestCase):
def waitForSession(self, q, outbound=False):
key = 'arq-transfer-outbound' if outbound else 'arq-transfer-inbound'
while True:
while True and self.channels_running:
ev = q.get()
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.")
@ -130,6 +130,7 @@ class TestMessageProtocol(unittest.TestCase):
def waitAndCloseChannels(self):
self.waitForSession(self.iss_event_queue, True)
self.channels_running = False
self.waitForSession(self.irs_event_queue, False)
self.channels_running = False
@ -139,7 +140,7 @@ class TestMessageProtocol(unittest.TestCase):
self.establishChannels()
params = {
'destination': "XX1XXX-1",
'destination': "AA1AAA-1",
'body': 'Hello World',
}

View File

@ -51,7 +51,7 @@ class TestProtocols(unittest.TestCase):
def testPingWithAck(self):
# Run ping command
api_params = { "dxcall": "XX1XXX-6"}
api_params = { "dxcall": "AA1AAA-1"}
ping_cmd = PingCommand(self.config, self.state_manager, self.event_manager, api_params)
#ping_cmd.run(self.event_queue, self.modem)
frame = ping_cmd.test(self.event_queue)

View File

@ -48,7 +48,7 @@ class TestIntegration(unittest.TestCase):
self.assertIn('RADIO', config)
def test_config_post(self):
config = {'NETWORK': {'modemport' : 3050}}
config = {'STATION': {'mygrid' : 'JN48ea'}}
r = requests.post(self.url + '/config',
headers={'Content-type': 'application/json'},
data = json.dumps(config))
@ -57,7 +57,7 @@ class TestIntegration(unittest.TestCase):
r = requests.get(self.url + '/config')
self.assertEqual(r.status_code, 200)
config = r.json()
self.assertEqual(config['NETWORK']['modemport'], 3050)
self.assertEqual(config['NETWORK']['modemport'], 5000)
if __name__ == '__main__':
unittest.main()