Porównaj commity

...

25 Commity

Autor SHA1 Wiadomość Data
Edouard Griffiths 29a8d21bac
Merge pull request #2000 from srcejon/freq_scanner
A variety of updates
2024-03-01 14:24:17 +01:00
srcejon cf3aeae55f
Merge branch 'f4exb:master' into freq_scanner 2024-02-28 16:17:16 +00:00
Edouard Griffiths 2a5c04f372
Merge pull request #2001 from mxi-box/device_claim_bugfix
Fix renumberate deviceset without modifying claimed of SamplingDevice
2024-02-28 15:23:15 +01:00
srcejon 7238b48e22 ADS-B: Add QT 6 support for map. 2024-02-28 12:34:57 +00:00
srcejon 0d333a910e Fix URLs on QT 6 map. 2024-02-28 12:34:03 +00:00
srcejon 3522feb355 Fix 32-bit float support for Spy Server. 2024-02-28 09:57:52 +00:00
mxi-box 2de9f16be4 Fix renumberate deviceset without modifying claimed of SamplingDevice 2024-02-28 06:46:35 +08:00
srcejon fafeb822d9 Fix Kiwi location. 2024-02-27 18:09:28 +00:00
srcejon d00e54452d Update map docs. 2024-02-27 16:59:19 +00:00
srcejon a6e8acf4ce Update map docs. 2024-02-27 16:55:47 +00:00
srcejon 5001ed751b Update map docs. 2024-02-27 16:41:37 +00:00
srcejon bb7ea39d28 Fix gcc warnings. 2024-02-27 16:28:23 +00:00
srcejon cacb740566 Update Map docs. 2024-02-27 16:25:34 +00:00
srcejon 5d96a09520 Update map QML/HTML. 2024-02-27 16:00:18 +00:00
srcejon 9506c93c11 Add libqt5svg5-dev dependency. 2024-02-27 15:53:40 +00:00
srcejon 580bfa7059 Add SVG and libacars. 2024-02-27 15:44:31 +00:00
srcejon ca8a537a50 HTTPDownloadManager: Support latest updates to Google Drive. 2024-02-27 15:41:39 +00:00
srcejon 36a25b5450 Add utils. 2024-02-27 15:41:14 +00:00
srcejon c9c605f448 SkyMap: Fix CORS and enabling caching. 2024-02-27 15:40:48 +00:00
srcejon e0839fce82 Map: Add Spy Server and Kiwi SDR. Add weather and satellite overlays. 2024-02-27 15:40:06 +00:00
srcejon c9b7ffa3c2 Fix SDRA protocol and improve spectrum refresh rate. 2024-02-27 15:06:12 +00:00
srcejon 6d0500ccbf Fix gcc warnings. 2024-02-27 14:24:27 +00:00
srcejon 8ad555972e Generate swagger files 2024-02-27 14:07:53 +00:00
srcejon 32c663c983 RemoteTCPInput: Add Spy Server support. 2024-02-27 14:03:34 +00:00
srcejon 9098f26679 SDRplay: Prevent nullpointer dereference for #1825 2024-02-27 13:59:15 +00:00
92 zmienionych plików z 5896 dodań i 1170 usunięć

Wyświetl plik

@ -84,7 +84,7 @@ for:
qml-module-qtlocation qml-module-qtpositioning qml-module-qtquick-window2 qml-module-qtquick-dialogs \
qml-module-qtquick-controls qml-module-qtquick-controls2 qml-module-qtquick-layouts qml-module-qtgraphicaleffects \
libqt5serialport5-dev qtdeclarative5-dev qtpositioning5-dev qtlocation5-dev \
libqt5charts5-dev libqt5texttospeech5-dev libqt5gamepad5-dev libfaad-dev zlib1g-dev \
libqt5charts5-dev libqt5texttospeech5-dev libqt5gamepad5-dev libqt5svg5-dev libfaad-dev zlib1g-dev \
libusb-1.0-0-dev libhidapi-dev libboost-all-dev libasound2-dev libopencv-dev libopencv-imgcodecs-dev \
libxml2-dev bison flex ffmpeg libpostproc-dev libavcodec-dev libavformat-dev \
libopus-dev libcodec2-dev libairspy-dev libhackrf-dev \

Wyświetl plik

@ -396,6 +396,10 @@ if(VS2019)
set(LIBSIGMF_LIBRARIES_DEBUG "${EXTERNAL_LIBRARY_FOLDER}/libsigmf/lib/libsigmfd.lib" DbgHelp CACHE INTERNAL "")
set(LIBSIGMF_DLL_DIR "${EXTERNAL_LIBRARY_FOLDER}/libsigmf/lib/" CACHE INTERNAL "")
set(LIBACARS_FOUND ON CACHE INTERNAL "")
set(LIBACARS_INCLUDE_DIR "${EXTERNAL_LIBRARY_FOLDER}/libacars/include/libacars-2" CACHE INTERNAL "")
set(LIBACARS_LIBRARIES "${EXTERNAL_LIBRARY_FOLDER}/libacars/lib/acars-2.lib" CACHE INTERNAL "")
# used on fixup_bundle phase
set(WINDOWS_FIXUP_BUNDLE_LIB_DIRS
"${EXTERNAL_LIBRARY_FOLDER}/fftw-3"
@ -654,7 +658,8 @@ if (BUILD_GUI)
Quick
QuickWidgets
TextToSpeech
Svg)
Svg
SvgWidgets)
else()
find_package(Qt5
REQUIRED COMPONENTS
@ -677,6 +682,8 @@ if (BUILD_GUI)
OpenGLWidgets
Quick
QuickWidgets
Svg
SvgWidgets
OPTIONAL_COMPONENTS
WebEngineQuick
WebEngineCore
@ -691,6 +698,7 @@ if (BUILD_GUI)
QuickWidgets
Location
TextToSpeech
Svg
OPTIONAL_COMPONENTS
WebEngine
WebEngineCore

2
debian/control vendored
Wyświetl plik

@ -17,6 +17,7 @@ Build-Depends: debhelper (>= 9),
libqt5quick5,
libqt5texttospeech5-dev,
libqt5gamepad5-dev,
libqt5svg5-dev,
qml-module-qtlocation,
qml-module-qtpositioning,
qml-module-qtquick-window2,
@ -64,6 +65,7 @@ Depends: ${shlibs:Depends},
libgl1-mesa-glx,
libqt5multimedia5-plugins,
libqt5gamepad5,
libqt5svg5,
qtspeech5-speechd-plugin,
pulseaudio,
ffmpeg,

Plik binarny nie jest wyświetlany.

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 547 KiB

Plik binarny nie jest wyświetlany.

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 1.0 MiB

Plik binarny nie jest wyświetlany.

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 872 KiB

Plik binarny nie jest wyświetlany.

Przed

Szerokość:  |  Wysokość:  |  Rozmiar: 53 KiB

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 36 KiB

Plik binarny nie jest wyświetlany.

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 24 KiB

Plik binarny nie jest wyświetlany.

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 51 KiB

Plik binarny nie jest wyświetlany.

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 23 KiB

Plik binarny nie jest wyświetlany.

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 530 KiB

Plik binarny nie jest wyświetlany.

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 470 KiB

Plik binarny nie jest wyświetlany.

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 60 KiB

Plik binarny nie jest wyświetlany.

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 722 KiB

Wyświetl plik

@ -4875,8 +4875,11 @@ ADSBDemodGUI::ADSBDemodGUI(PluginAPI* pluginAPI, DeviceUISet *deviceUISet, Baseb
ui->map->rootContext()->setContextProperty("airportModel", &m_airportModel);
ui->map->rootContext()->setContextProperty("airspaceModel", &m_airspaceModel);
ui->map->rootContext()->setContextProperty("navAidModel", &m_navAidModel);
#if (QT_VERSION < QT_VERSION_CHECK(6, 0, 0))
ui->map->setSource(QUrl(QStringLiteral("qrc:/map/map.qml")));
#else
ui->map->setSource(QUrl(QStringLiteral("qrc:/map/map_6.qml")));
#endif
connect(this, SIGNAL(customContextMenuRequested(const QPoint &)), this, SLOT(onMenuDialogCalled(const QPoint &)));
m_adsbDemod = reinterpret_cast<ADSBDemod*>(rxChannel); //new ADSBDemod(m_deviceUISet->m_deviceSourceAPI);

Wyświetl plik

@ -1,6 +1,8 @@
<RCC>
<qresource prefix="/">
<file>map/map.qml</file>
<file>map/map_6.qml</file>
<file>map/ModifiedMapView.qml</file>
<file>map/MapStation.qml</file>
<file>map/aircraft_2engine.png</file>
<file>map/aircraft_2enginesmall.png</file>

Wyświetl plik

@ -0,0 +1,178 @@
// Copyright (C) 2023 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
import QtQuick
import QtLocation as QL
import QtPositioning as QP
import Qt.labs.animation
/*!
\qmltype MapView
\inqmlmodule QtLocation
\brief An interactive map viewer component.
MapView wraps a Map and adds the typical interactive features:
changing the zoom level, panning and tilting the map.
The implementation is a QML assembly of smaller building blocks that are
available separately. In case you want to make changes in your own version
of this component, you can copy the QML, which is installed into the
\c qml/QtLocation module directory, and modify it as needed.
\sa Map
*/
Item {
/*!
\qmlproperty Map MapView::map
This property provides access to the underlying Map instance.
*/
property alias map: map
/*!
\qmlproperty real minimumZoomLevel
The minimum zoom level according to the size of the view.
\sa Map::minimumZoomLevel
*/
property real minimumZoomLevel: map.minimumZoomLevel
/*!
\qmlproperty real maximumZoomLevel
The maximum valid zoom level for the map.
\sa Map::maximumZoomLevel
*/
property real maximumZoomLevel: map.maximumZoomLevel
// --------------------------------
// implementation
id: root
Component.onCompleted: map.resetPinchMinMax()
QL.Map {
id: map
width: parent.width
height: parent.height
tilt: tiltHandler.persistentTranslation.y / -5
property bool pinchAdjustingZoom: false
BoundaryRule on zoomLevel {
id: br
minimum: map.minimumZoomLevel
maximum: map.maximumZoomLevel
}
onZoomLevelChanged: {
br.returnToBounds();
if (!pinchAdjustingZoom) resetPinchMinMax()
}
function resetPinchMinMax() {
pinch.persistentScale = 1
pinch.scaleAxis.minimum = Math.pow(2, root.minimumZoomLevel - map.zoomLevel + 1)
pinch.scaleAxis.maximum = Math.pow(2, root.maximumZoomLevel - map.zoomLevel - 1)
}
PinchHandler {
id: pinch
target: null
property real rawBearing: 0
property QP.geoCoordinate startCentroid
onActiveChanged: if (active) {
flickAnimation.stop()
pinch.startCentroid = map.toCoordinate(pinch.centroid.position, false)
} else {
flickAnimation.restart(centroid.velocity)
map.resetPinchMinMax()
}
onScaleChanged: (delta) => {
map.pinchAdjustingZoom = true
map.zoomLevel += Math.log2(delta)
map.alignCoordinateToPoint(pinch.startCentroid, pinch.centroid.position)
map.pinchAdjustingZoom = false
}
onRotationChanged: (delta) => {
pinch.rawBearing -= delta
// snap to 0° if we're close enough
map.bearing = (Math.abs(pinch.rawBearing) < 5) ? 0 : pinch.rawBearing
map.alignCoordinateToPoint(pinch.startCentroid, pinch.centroid.position)
}
grabPermissions: PointerHandler.TakeOverForbidden
}
WheelHandler {
id: wheel
// workaround for QTBUG-87646 / QTBUG-112394 / QTBUG-112432:
// Magic Mouse pretends to be a trackpad but doesn't work with PinchHandler
// and we don't yet distinguish mice and trackpads on Wayland either
acceptedDevices: Qt.platform.pluginName === "cocoa" || Qt.platform.pluginName === "wayland"
? PointerDevice.Mouse | PointerDevice.TouchPad
: PointerDevice.Mouse
onWheel: (event) => {
const loc = map.toCoordinate(wheel.point.position)
switch (event.modifiers) {
case Qt.NoModifier:
// jonb - Changed to make more like Qt5
//map.zoomLevel += event.angleDelta.y / 120
map.zoomLevel += event.angleDelta.y / 1000
break
case Qt.ShiftModifier:
map.bearing += event.angleDelta.y / 15
break
case Qt.ControlModifier:
map.tilt += event.angleDelta.y / 15
break
}
map.alignCoordinateToPoint(loc, wheel.point.position)
}
}
DragHandler {
id: drag
signal flickStarted // for autotests only
signal flickEnded
target: null
onTranslationChanged: (delta) => map.pan(-delta.x, -delta.y)
onActiveChanged: if (active) {
flickAnimation.stop()
} else {
flickAnimation.restart(centroid.velocity)
}
}
property vector3d animDest
onAnimDestChanged: if (flickAnimation.running) {
const delta = Qt.vector2d(animDest.x - flickAnimation.animDestLast.x, animDest.y - flickAnimation.animDestLast.y)
map.pan(-delta.x, -delta.y)
flickAnimation.animDestLast = animDest
}
Vector3dAnimation on animDest {
id: flickAnimation
property vector3d animDestLast
from: Qt.vector3d(0, 0, 0)
duration: 500
easing.type: Easing.OutQuad
onStarted: drag.flickStarted()
onStopped: drag.flickEnded()
function restart(vel) {
stop()
map.animDest = Qt.vector3d(0, 0, 0)
animDestLast = Qt.vector3d(0, 0, 0)
to = Qt.vector3d(vel.x / duration * 100, vel.y / duration * 100, 0)
start()
}
}
DragHandler {
id: tiltHandler
minimumPointCount: 2
maximumPointCount: 2
target: null
xAxis.enabled: false
grabPermissions: PointerHandler.TakeOverForbidden
onActiveChanged: if (active) flickAnimation.stop()
}
}
}

Wyświetl plik

@ -0,0 +1,537 @@
import QtQuick 2.14
import QtQuick.Window 2.14
import QtQuick.Controls 2.14
import QtPositioning 6.5
import QtLocation 6.5
import Qt5Compat.GraphicalEffects
Item {
id: qmlMap
property int aircraftZoomLevel: 11
property int aircraftMinZoomLevel: 11
property int airportZoomLevel: 11
property string mapProvider: "osm"
property variant mapPtr
property string requestedMapType
property bool lightIcons
property variant guiPtr
property bool smoothing
function createMap(pluginParameters, requestedMap, gui) {
requestedMapType = requestedMap
guiPtr = gui
var paramString = ""
for (var prop in pluginParameters) {
var parameter = 'PluginParameter { name: "' + prop + '"; value: "' + pluginParameters[prop] + '"}'
paramString = paramString + parameter
}
var pluginString = 'import QtLocation 6.5; Plugin{ name:"' + mapProvider + '"; ' + paramString + '}'
var plugin = Qt.createQmlObject (pluginString, qmlMap)
if (mapPtr) {
// Objects aren't destroyed immediately, so don't call findChild("map")
mapPtr.destroy()
mapPtr = null
}
mapPtr = actualMapComponent.createObject(page)
mapPtr.map.plugin = plugin
mapPtr.map.forceActiveFocus()
return mapPtr
}
Item {
id: page
anchors.fill: parent
}
Component {
id: actualMapComponent
ModifiedMapView {
id: mapView
objectName: "mapView"
anchors.fill: parent
map.center: QtPositioning.coordinate(51.5, 0.125) // London
map.zoomLevel: 10
map.objectName: "map"
// not in 6
//gesture.enabled: true
//gesture.acceptedGestures: MapGestureArea.PinchGesture | MapGestureArea.PanGesture
MouseArea {
anchors.fill: parent
propagateComposedEvents: true
onClicked: {
// Unhighlight current aircraft
guiPtr.clearHighlighted()
mouse.accepted = false
}
}
MapStation {
id: station
objectName: "station"
stationName: "Home"
}
MapItemView {
model: airspaceModel
delegate: airspaceComponent
parent: mapView.map
}
MapItemView {
model: navAidModel
delegate: navAidComponent
parent: mapView.map
}
MapItemView {
model: airspaceModel
delegate: airspaceNameComponent
parent: mapView.map
}
MapItemView {
model: airportModel
delegate: airportComponent
parent: mapView.map
}
// This needs to be before aircraftComponent MapItemView, so it's drawn underneath
MapItemView {
model: aircraftModel
delegate: aircraftPathComponent
parent: mapView.map
}
MapItemView {
model: aircraftModel
delegate: aircraftComponent
parent: mapView.map
}
map.onZoomLevelChanged: {
if (map.zoomLevel > aircraftMinZoomLevel) {
aircraftZoomLevel = map.zoomLevel
} else {
aircraftZoomLevel = aircraftMinZoomLevel
}
if (map.zoomLevel > 11) {
station.zoomLevel = map.zoomLevel
airportZoomLevel = map.zoomLevel
} else {
station.zoomLevel = 11
airportZoomLevel = 11
}
}
map.onSupportedMapTypesChanged : {
for (var i = 0; i < map.supportedMapTypes.length; i++) {
if (requestedMapType == map.supportedMapTypes[i].name) {
map.activeMapType = map.supportedMapTypes[i]
}
}
lightIcons = (requestedMapType == "Night Transit Map") || (requestedMapType == "mapbox://styles/mapbox/dark-v9")
}
}
}
Component {
id: navAidComponent
MapQuickItem {
id: navAid
anchorPoint.x: image.width/2
anchorPoint.y: image.height/2
coordinate: position
zoomLevel: airportZoomLevel
sourceItem: Grid {
columns: 1
Grid {
horizontalItemAlignment: Grid.AlignHCenter
columnSpacing: 5
layer.enabled: smoothing
layer.smooth: smoothing
Image {
id: image
source: navAidImage
visible: !lightIcons
MouseArea {
anchors.fill: parent
onClicked: (mouse) => {
selected = !selected
}
}
}
ColorOverlay {
cached: true
width: image.width
height: image.height
source: image
color: "#c0ffffff"
visible: lightIcons
}
Rectangle {
id: bubble
color: bubbleColour
border.width: 1
width: text.width + 5
height: text.height + 5
radius: 5
Text {
id: text
anchors.centerIn: parent
text: navAidData
}
MouseArea {
anchors.fill: parent
hoverEnabled: true
onClicked: (mouse) => {
selected = !selected
}
}
}
}
}
}
}
Component {
id: airspaceComponent
MapPolygon {
border.width: 1
border.color: airspaceBorderColor
color: airspaceFillColor
path: airspacePolygon
}
}
Component {
id: airspaceNameComponent
MapQuickItem {
coordinate: position
anchorPoint.x: airspaceText.width/2
anchorPoint.y: airspaceText.height/2
zoomLevel: airportZoomLevel
sourceItem: Grid {
columns: 1
Grid {
layer.enabled: smoothing
layer.smooth: smoothing
horizontalItemAlignment: Grid.AlignHCenter
Text {
id: airspaceText
text: details
}
}
}
}
}
Component {
id: aircraftPathComponent
MapPolyline {
line.width: 2
line.color: 'gray'
path: aircraftPath
}
}
Component {
id: aircraftComponent
MapQuickItem {
id: aircraft
anchorPoint.x: image.width/2
anchorPoint.y: image.height/2
coordinate: position
zoomLevel: aircraftZoomLevel
sourceItem: Grid {
columns: 1
Grid {
layer.enabled: smoothing
layer.smooth: smoothing
horizontalItemAlignment: Grid.AlignHCenter
Image {
id: image
rotation: heading
source: aircraftImage
visible: !lightIcons
MouseArea {
anchors.fill: parent
acceptedButtons: Qt.LeftButton | Qt.RightButton
onClicked: {
if (mouse.button === Qt.LeftButton) {
highlighted = true
console.log("z=" + aircraft.sourceItem.z)
aircraft.sourceItem.z = aircraft.sourceItem.z + 1
} else if (mouse.button === Qt.RightButton) {
contextMenu.popup()
}
}
onDoubleClicked: {
target = true
}
}
}
ColorOverlay {
cached: true
width: image.width
height: image.height
rotation: heading
source: image
color: "#c0ffffff"
visible: lightIcons
MouseArea {
anchors.fill: parent
onClicked: {
highlighted = true
}
onDoubleClicked: {
target = true
}
}
}
Rectangle {
id: bubble
color: bubbleColour
border.width: 1
width: text.width * 1.1
height: text.height * 1.1
radius: 5
Text {
id: text
anchors.centerIn: parent
text: adsbData
textFormat: TextEdit.RichText
}
MouseArea {
anchors.fill: parent
acceptedButtons: Qt.LeftButton | Qt.RightButton
onClicked: {
if (mouse.button === Qt.LeftButton) {
showAll = !showAll
} else if (mouse.button === Qt.RightButton) {
contextMenu.popup()
}
}
Menu {
id: contextMenu
MenuItem {
text: "Set as target"
onTriggered: target = true
}
MenuItem {
text: "Find on feature map"
onTriggered: aircraftModel.findOnMap(index)
}
}
}
}
}
}
}
}
Component {
id: airportComponent
MapItemGroup {
MapItemGroup {
property var groupVisible: false
id: rangeGroup
MapCircle {
id: circle5nm
center: position
color: "transparent"
border.color: "gray"
radius: 9260 // 5nm in metres
visible: rangeGroup.groupVisible
}
MapCircle {
id: circle10nm
center: position
color: "transparent"
border.color: "gray"
radius: 18520
visible: rangeGroup.groupVisible
}
MapCircle {
id: circle15nm
center: airport.coordinate
color: "transparent"
border.color: "gray"
radius: 27780
visible: rangeGroup.groupVisible
}
MapQuickItem {
id: text5nm
coordinate {
latitude: position.latitude
longitude: position.longitude + (5/60)/Math.cos(Math.abs(position.latitude)*Math.PI/180)
}
anchorPoint.x: 0
anchorPoint.y: height/2
sourceItem: Text {
color: "grey"
text: "5nm"
}
visible: rangeGroup.groupVisible
}
MapQuickItem {
id: text10nm
coordinate {
latitude: position.latitude
longitude: position.longitude + (10/60)/Math.cos(Math.abs(position.latitude)*Math.PI/180)
}
anchorPoint.x: 0
anchorPoint.y: height/2
sourceItem: Text {
color: "grey"
text: "10nm"
}
visible: rangeGroup.groupVisible
}
MapQuickItem {
id: text15nm
coordinate {
latitude: position.latitude
longitude: position.longitude + (15/60)/Math.cos(Math.abs(position.latitude)*Math.PI/180)
}
anchorPoint.x: 0
anchorPoint.y: height/2
sourceItem: Text {
color: "grey"
text: "15nm"
}
visible: rangeGroup.groupVisible
}
}
MapQuickItem {
id: airport
anchorPoint.x: image.width/2
anchorPoint.y: image.height/2
coordinate: position
zoomLevel: airportZoomLevel
sourceItem: Grid {
columns: 1
Grid {
horizontalItemAlignment: Grid.AlignHCenter
layer.enabled: smoothing
layer.smooth: smoothing
Image {
id: image
source: airportImage
visible: !lightIcons
MouseArea {
anchors.fill: parent
acceptedButtons: Qt.LeftButton | Qt.RightButton
onClicked: (mouse) => {
console.log("AIRPORT CLICKED ************************* ");
if (mouse.button === Qt.RightButton) {
showRangeItem.visible = !rangeGroup.groupVisible
hideRangeItem.visible = rangeGroup.groupVisible
menuItems.clear()
var scanners = airportModel.getFreqScanners()
for (var i = 0; i < scanners.length; i++) {
menuItems.append({
text: "Send to Frequency Scanner " + scanners[i],
airport: index,
scanner: scanners[i]
})
}
contextMenu.popup()
}
}
onDoubleClicked: (mouse) => {
rangeGroup.groupVisible = !rangeGroup.groupVisible
}
ListModel {
id: menuItems
}
Menu {
id: contextMenu
MenuItem {
id: showRangeItem
text: "Show range rings"
onTriggered: rangeGroup.groupVisible = true
height: visible ? implicitHeight : 0
}
MenuItem {
id: hideRangeItem
text: "Hide range rings"
onTriggered: rangeGroup.groupVisible = false
height: visible ? implicitHeight : 0
}
Instantiator {
model: menuItems
MenuItem {
text: model.text
onTriggered: airportModel.sendToFreqScanner(model.airport, model.scanner)
}
onObjectAdded: function(index, object) {
contextMenu.insertItem(index, object)
}
onObjectRemoved: function(index, object) {
contextMenu.removeItem(object)
}
}
}
}
}
ColorOverlay {
cached: true
width: image.width
height: image.height
source: image
color: "#c0ffffff"
visible: lightIcons
}
Rectangle {
id: bubble
color: bubbleColour
border.width: 1
width: text.width + 5
height: text.height + 5
radius: 5
Text {
id: text
anchors.centerIn: parent
text: airportData
}
MouseArea {
anchors.fill: parent
onClicked: (mouse) => {
console.log("AIRPORT 2 CLICKED ************************* ");
if (showFreq) {
var freqIdx = Math.floor((mouse.y-5)/((height-10)/airportDataRows))
if (freqIdx == 0) {
showFreq = false
}
} else {
showFreq = true
}
}
onDoubleClicked: (mouse) => {
if (showFreq) {
var freqIdx = Math.floor((mouse.y-5)/((height-10)/airportDataRows))
if (freqIdx != 0) {
selectedFreq = freqIdx - 1
}
}
}
}
}
}
}
}
}
}
}

Wyświetl plik

@ -58,6 +58,7 @@ if(NOT SERVER_MODE)
mapmodel.cpp
mapitem.cpp
mapwebsocketserver.cpp
maptileserver.cpp
cesiuminterface.cpp
czml.cpp
map.qrc
@ -78,6 +79,7 @@ if(NOT SERVER_MODE)
mapmodel.h
mapitem.h
mapwebsocketserver.h
maptileserver.h
cesiuminterface.h
czml.h
)
@ -86,12 +88,12 @@ if(NOT SERVER_MODE)
set(TARGET_LIB_GUI "sdrgui")
set(INSTALL_FOLDER ${INSTALL_PLUGINS_DIR})
if(Qt${QT_DEFAULT_MAJOR_VERSION}WebEngineCore_FOUND)
set(TARGET_LIB "Qt::Widgets" Qt::Quick Qt::QuickWidgets Qt::Positioning Qt::Location Qt::WebEngineCore Qt::WebEngineWidgets)
elseif(Qt${QT_DEFAULT_MAJOR_VERSION}WebEngine_FOUND)
set(TARGET_LIB "Qt::Widgets" Qt::Quick Qt::QuickWidgets Qt::Positioning Qt::Location Qt::WebEngine Qt::WebEngineCore Qt::WebEngineWidgets)
if(Qt${QT_DEFAULT_MAJOR_VERSION}WebEngine_FOUND)
set(TARGET_LIB "Qt::Widgets" Qt::Quick Qt::QuickWidgets Qt::Svg Qt::Positioning Qt::Location Qt::WebEngine Qt::WebEngineCore Qt::WebEngineWidgets)
elseif(Qt${QT_DEFAULT_MAJOR_VERSION}WebEngineCore_FOUND)
set(TARGET_LIB "Qt::Widgets" Qt::Quick Qt::QuickWidgets Qt::Svg Qt::SvgWidgets Qt::Positioning Qt::Location Qt::WebEngineCore Qt::WebEngineWidgets)
else()
set(TARGET_LIB "Qt::Widgets" Qt::Quick Qt::QuickWidgets Qt::Positioning Qt::Location)
set(TARGET_LIB "Qt::Widgets" Qt::Quick Qt::QuickWidgets Qt::Svg Qt::Positioning Qt::Location)
endif()
else()
set(TARGET_NAME mapsrv)

Wyświetl plik

@ -177,6 +177,28 @@ void CesiumInterface::showfoF2(bool show)
send(obj);
}
void CesiumInterface::showLayer(const QString& layer, bool show)
{
QJsonObject obj {
{"command", "showLayer"},
{"layer", layer},
{"show", show}
};
send(obj);
}
void CesiumInterface::setLayerSettings(const QString& layer, const QStringList& settings, const QList<QVariant>& values)
{
QJsonObject obj {
{"command", "setLayerSettings"},
{"layer", layer},
};
for (int i = 0; i < settings.size(); i++) {
obj.insert(settings[i], QJsonValue::fromVariant(values[i]));
}
send(obj);
}
void CesiumInterface::updateImage(const QString &name, float east, float west, float north, float south, float altitude, const QString &data)
{
QJsonObject obj {

Wyświetl plik

@ -68,6 +68,8 @@ public:
void setAntiAliasing(const QString &antiAliasing);
void showMUF(bool show);
void showfoF2(bool show);
void showLayer(const QString& layer, bool show);
void setLayerSettings(const QString& layer, const QStringList& settings, const QList<QVariant>& values);
void updateImage(const QString &name, float east, float west, float north, float south, float altitude, const QString &data);
void removeImage(const QString &name);
void removeAllImages();

Wyświetl plik

@ -475,6 +475,7 @@ QJsonObject CZML::update(ObjectMapItem *mapItem, bool isTarget, bool isSelected)
if ((mapItem->m_group == "Beacons")
|| (mapItem->m_group == "AM") || (mapItem->m_group == "FM") || (mapItem->m_group == "DAB")
|| (mapItem->m_group == "NavAid")
|| (mapItem->m_group == "Waypoints")
) {
displayDistanceMax = 1000000;
} else if ((mapItem->m_group == "Station") || (mapItem->m_group == "Radar") || (mapItem->m_group == "Radio Time Transmitters")) {

Wyświetl plik

@ -1,11 +1,18 @@
<RCC>
<qresource prefix="/map/">
<file>icons/groundtracks.png</file>
<file>icons/clock.png</file>
<file>icons/ibp.png</file>
<file>icons/muf.png</file>
<file>icons/fof2.png</file>
<file>icons/controltower.png</file>
<file>icons/vor.png</file>
</qresource>
<qresource prefix="/map/">
<file>icons/groundtracks.png</file>
<file>icons/clock.png</file>
<file>icons/ibp.png</file>
<file>icons/muf.png</file>
<file>icons/fof2.png</file>
<file>icons/controltower.png</file>
<file>icons/vor.png</file>
<file>icons/precipitation.png</file>
<file>icons/anchor.png</file>
<file>icons/cloud.png</file>
<file>icons/layers.png</file>
<file>icons/railway.png</file>
<file>icons/waypoints.png</file>
<file>icons/earthsat.png</file>
</qresource>
</RCC>

Plik binarny nie jest wyświetlany.

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 1.7 KiB

Plik binarny nie jest wyświetlany.

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 389 B

Plik binarny nie jest wyświetlany.

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 1.9 KiB

Plik binarny nie jest wyświetlany.

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 2.1 KiB

Plik binarny nie jest wyświetlany.

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 420 B

Plik binarny nie jest wyświetlany.

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 1.7 KiB

Plik binarny nie jest wyświetlany.

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 1.7 KiB

Wyświetl plik

@ -1,5 +1,5 @@
///////////////////////////////////////////////////////////////////////////////////
// Copyright (C) 2021-2023 Jon Beniston, M7RCE <jon@beniston.com> //
// Copyright (C) 2021-2024 Jon Beniston, M7RCE <jon@beniston.com> //
// Copyright (C) 2021-2023 Edouard Griffiths, F4EXB <f4exb06@gmail.com> //
// Copyright (C) 2022 Jiří Pinkava <jiri.pinkava@rossum.ai> //
// //
@ -47,6 +47,7 @@ const char* const Map::m_featureId = "Map";
Map::Map(WebAPIAdapterInterface *webAPIAdapterInterface) :
Feature(m_featureIdURI, webAPIAdapterInterface),
m_availableChannelOrFeatureHandler(MapSettings::m_pipeURIs, QStringList{"mapitems"}),
m_multiplier(0.0)
{
qDebug("Map::Map: webAPIAdapterInterface: %p", webAPIAdapterInterface);
@ -61,33 +62,33 @@ Map::Map(WebAPIAdapterInterface *webAPIAdapterInterface) :
&Map::networkManagerFinished
);
QObject::connect(
MainCore::instance(),
&MainCore::featureAdded,
&m_availableChannelOrFeatureHandler,
&AvailableChannelOrFeatureHandler::channelsOrFeaturesChanged,
this,
&Map::handleFeatureAdded
&Map::channelsOrFeaturesChanged
);
QObject::connect(
MainCore::instance(),
&MainCore::channelAdded,
&m_availableChannelOrFeatureHandler,
&AvailableChannelOrFeatureHandler::messageEnqueued,
this,
&Map::handleChannelAdded
&Map::handlePipeMessageQueue
);
QTimer::singleShot(2000, this, SLOT(scanAvailableChannelsAndFeatures()));
m_availableChannelOrFeatureHandler.scanAvailableChannelsAndFeatures();
}
Map::~Map()
{
QObject::disconnect(
MainCore::instance(),
&MainCore::featureAdded,
&m_availableChannelOrFeatureHandler,
&AvailableChannelOrFeatureHandler::channelsOrFeaturesChanged,
this,
&Map::handleFeatureAdded
&Map::channelsOrFeaturesChanged
);
QObject::disconnect(
MainCore::instance(),
&MainCore::channelAdded,
&m_availableChannelOrFeatureHandler,
&AvailableChannelOrFeatureHandler::messageEnqueued,
this,
&Map::handleChannelAdded
&Map::handlePipeMessageQueue
);
QObject::disconnect(
m_networkManager,
@ -435,156 +436,22 @@ QDateTime Map::getMapDateTime()
}
}
void Map::scanAvailableChannelsAndFeatures()
void Map::channelsOrFeaturesChanged(const QStringList& renameFrom, const QStringList& renameTo)
{
qDebug("Map::scanAvailableChannelsAndFeatures");
std::vector<FeatureSet*>& featureSets = MainCore::instance()->getFeatureeSets();
m_availableChannelOrFeatures.clear();
for (const auto& featureSet : featureSets)
{
for (int fei = 0; fei < featureSet->getNumberOfFeatures(); fei++)
{
Feature *feature = featureSet->getFeatureAt(fei);
if (MapSettings::m_pipeURIs.contains(feature->getURI()) && !m_availableChannelOrFeatures.contains(feature))
{
qDebug("Map::scanAvailableChannelsAndFeatures: store feature %d:%d %s (%p)",
featureSet->getIndex(), fei, qPrintable(feature->getURI()), feature);
registerPipe(feature);
MapSettings::AvailableChannelOrFeature availableItem =
MapSettings::AvailableChannelOrFeature{
"F",
featureSet->getIndex(),
fei,
feature->getIdentifier(),
feature
};
m_availableChannelOrFeatures[feature] = availableItem;
}
}
}
std::vector<DeviceSet*>& deviceSets = MainCore::instance()->getDeviceSets();
for (const auto& deviceSet : deviceSets)
{
DSPDeviceSourceEngine *deviceSourceEngine = deviceSet->m_deviceSourceEngine;
DSPDeviceMIMOEngine *deviceMimoEngine = deviceSet->m_deviceMIMOEngine;
if ((deviceSourceEngine) || (deviceMimoEngine))
{
for (int chi = 0; chi < deviceSet->getNumberOfChannels(); chi++)
{
ChannelAPI *channel = deviceSet->getChannelAt(chi);
if (MapSettings::m_pipeURIs.contains(channel->getURI()) && !m_availableChannelOrFeatures.contains(channel))
{
qDebug("Map::scanAvailableChannelsAndFeatures: store channel %d:%d %s (%p)",
deviceSet->getIndex(), chi, qPrintable(channel->getURI()), channel);
registerPipe(channel);
MapSettings::AvailableChannelOrFeature availableItem =
MapSettings::AvailableChannelOrFeature{
"R",
deviceSet->getIndex(),
chi,
channel->getIdentifier(),
channel};
m_availableChannelOrFeatures[channel] = availableItem;
}
}
}
}
notifyUpdate();
m_availableChannelOrFeatures = m_availableChannelOrFeatureHandler.getAvailableChannelOrFeatureList();
notifyUpdate(renameFrom, renameTo);
}
void Map::handleFeatureAdded(int featureSetIndex, Feature *feature)
{
FeatureSet *featureSet = MainCore::instance()->getFeatureeSets()[featureSetIndex];
if (MapSettings::m_pipeURIs.contains(feature->getURI()))
{
qDebug("Map::handleFeatureAdded: featureSetIndex: %d:%d feature: %s (%p)",
featureSetIndex, feature->getIndexInFeatureSet(), qPrintable(feature->getURI()), feature);
registerPipe(feature);
MapSettings::AvailableChannelOrFeature availableItem =
MapSettings::AvailableChannelOrFeature{
"F",
featureSet->getIndex(),
feature->getIndexInFeatureSet(),
feature->getIdentifier(),
feature
};
m_availableChannelOrFeatures[feature] = availableItem;
notifyUpdate();
}
}
void Map::handleChannelAdded(int deviceSetIndex, ChannelAPI *channel)
{
DeviceSet *deviceSet = MainCore::instance()->getDeviceSets()[deviceSetIndex];
DSPDeviceSourceEngine *deviceSourceEngine = deviceSet->m_deviceSourceEngine;
if (deviceSourceEngine && MapSettings::m_pipeURIs.contains(channel->getURI()))
{
qDebug("Map::handleChannelAdded: deviceSetIndex: %d:%d channel: %s (%p)",
deviceSetIndex, channel->getIndexInDeviceSet(), qPrintable(channel->getURI()), channel);
registerPipe(channel);
MapSettings::AvailableChannelOrFeature availableItem =
MapSettings::AvailableChannelOrFeature{
"R",
deviceSet->getIndex(),
channel->getIndexInDeviceSet(),
channel->getIdentifier(),
channel
};
m_availableChannelOrFeatures[channel] = availableItem;
notifyUpdate();
}
}
void Map::registerPipe(QObject *object)
{
qDebug("Map::registerPipe: register %s (%p)", qPrintable(object->objectName()), object);
MessagePipes& messagePipes = MainCore::instance()->getMessagePipes();
ObjectPipe *pipe = messagePipes.registerProducerToConsumer(object, this, "mapitems");
MessageQueue *messageQueue = qobject_cast<MessageQueue*>(pipe->m_element);
QObject::connect(
messageQueue,
&MessageQueue::messageEnqueued,
this,
[=](){ this->handlePipeMessageQueue(messageQueue); },
Qt::QueuedConnection
);
QObject::connect(
pipe,
&ObjectPipe::toBeDeleted,
this,
&Map::handleMessagePipeToBeDeleted
);
}
void Map::notifyUpdate()
void Map::notifyUpdate(const QStringList& renameFrom, const QStringList& renameTo)
{
if (getMessageQueueToGUI())
{
MsgReportAvailableChannelOrFeatures *msg = MsgReportAvailableChannelOrFeatures::create();
msg->getItems() = m_availableChannelOrFeatures.values();
MsgReportAvailableChannelOrFeatures *msg = MsgReportAvailableChannelOrFeatures::create(renameFrom, renameTo);
msg->getItems() = m_availableChannelOrFeatures;
getMessageQueueToGUI()->push(msg);
}
}
void Map::handleMessagePipeToBeDeleted(int reason, QObject* object)
{
if ((reason == 0) && m_availableChannelOrFeatures.contains(object)) // producer
{
qDebug("Map::handleMessagePipeToBeDeleted: removing channel or feature at (%p)", object);
m_availableChannelOrFeatures.remove(object);
notifyUpdate();
}
}
void Map::handlePipeMessageQueue(MessageQueue* messageQueue)
{
Message* message;

Wyświetl plik

@ -1,7 +1,7 @@
///////////////////////////////////////////////////////////////////////////////////
// Copyright (C) 2020, 2022 Edouard Griffiths, F4EXB <f4exb06@gmail.com> //
// Copyright (C) 2020 Kacper Michajłow <kasper93@gmail.com> //
// Copyright (C) 2021-2022 Jon Beniston, M7RCE <jon@beniston.com> //
// Copyright (C) 2021-2024 Jon Beniston, M7RCE <jon@beniston.com> //
// Copyright (C) 2022 Jiří Pinkava <jiri.pinkava@rossum.ai> //
// //
// This program is free software; you can redistribute it and/or modify //
@ -28,6 +28,8 @@
#include "feature/feature.h"
#include "util/message.h"
#include "availablechannelorfeaturehandler.h"
#include "maincore.h"
#include "mapsettings.h"
@ -110,17 +112,23 @@ public:
MESSAGE_CLASS_DECLARATION
public:
QList<MapSettings::AvailableChannelOrFeature>& getItems() { return m_availableChannelOrFeatures; }
AvailableChannelOrFeatureList& getItems() { return m_availableChannelOrFeatures; }
const QStringList& getRenameFrom() const { return m_renameFrom; }
const QStringList& getRenameTo() const { return m_renameTo; }
static MsgReportAvailableChannelOrFeatures* create() {
return new MsgReportAvailableChannelOrFeatures();
static MsgReportAvailableChannelOrFeatures* create(const QStringList& renameFrom, const QStringList& renameTo) {
return new MsgReportAvailableChannelOrFeatures(renameFrom, renameTo);
}
private:
QList<MapSettings::AvailableChannelOrFeature> m_availableChannelOrFeatures;
AvailableChannelOrFeatureList m_availableChannelOrFeatures;
QStringList m_renameFrom;
QStringList m_renameTo;
MsgReportAvailableChannelOrFeatures() :
Message()
MsgReportAvailableChannelOrFeatures(const QStringList& renameFrom, const QStringList& renameTo) :
Message(),
m_renameFrom(renameFrom),
m_renameTo(renameTo)
{}
};
@ -176,7 +184,8 @@ public:
private:
MapSettings m_settings;
QHash<QObject*, MapSettings::AvailableChannelOrFeature> m_availableChannelOrFeatures;
AvailableChannelOrFeatureList m_availableChannelOrFeatures;
AvailableChannelOrFeatureHandler m_availableChannelOrFeatureHandler;
QNetworkAccessManager *m_networkManager;
QNetworkRequest m_networkRequest;
@ -184,8 +193,7 @@ private:
void applySettings(const MapSettings& settings, const QList<QString>& settingsKeys, bool force = false);
void webapiFormatFeatureReport(SWGSDRangel::SWGFeatureReport& response);
void webapiReverseSendSettings(const QList<QString>& featureSettingsKeys, const MapSettings& settings, bool force);
void registerPipe(QObject *object);
void notifyUpdate();
void notifyUpdate(const QStringList& renameFrom, const QStringList& renameTo);
QDateTime m_mapDateTime;
QDateTime m_systemDateTime;
@ -194,10 +202,7 @@ private:
private slots:
void networkManagerFinished(QNetworkReply *reply);
void scanAvailableChannelsAndFeatures();
void handleFeatureAdded(int featureSetIndex, Feature *feature);
void handleChannelAdded(int deviceSetIndex, ChannelAPI *channel);
void handleMessagePipeToBeDeleted(int reason, QObject* object);
void channelsOrFeaturesChanged(const QStringList& renameFrom, const QStringList& renameTo);
void handlePipeMessageQueue(MessageQueue* messageQueue);
};

Wyświetl plik

@ -8,6 +8,8 @@
<file>map/antennadab.png</file>
<file>map/antennafm.png</file>
<file>map/antennaam.png</file>
<file>map/antennakiwi.png</file>
<file>map/antennaspyserver.png</file>
<file>map/ionosonde.png</file>
<file>map/VOR.png</file>
<file>map/VOR-DME.png</file>
@ -21,6 +23,7 @@
<file>map/airport_medium.png</file>
<file>map/airport_small.png</file>
<file>map/heliport.png</file>
<file>map/waypoint.png</file>
<file>map/map3d.html</file>
</qresource>
<qresource prefix="/">

Plik binarny nie jest wyświetlany.

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 3.6 KiB

Plik binarny nie jest wyświetlany.

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 3.6 KiB

Wyświetl plik

@ -303,12 +303,6 @@ Item {
height: text.height + 5
radius: 5
visible: mapTextVisible
Text {
id: text
anchors.centerIn: parent
text: mapText
textFormat: TextEdit.RichText
}
MouseArea {
anchors.fill: parent
hoverEnabled: true
@ -406,6 +400,17 @@ Item {
}
}
}
// Have Text after MouseArea, so links can be clicked
Text {
id: text
anchors.centerIn: parent
text: mapText
textFormat: TextEdit.RichText
onLinkActivated: {
console.log("Link", link);
mapModel.link(link);
}
}
}
}
}

Wyświetl plik

@ -316,12 +316,6 @@ Item {
height: text.height + 5
radius: 5
visible: mapTextVisible
Text {
id: text
anchors.centerIn: parent
text: mapText
textFormat: TextEdit.RichText
}
MouseArea {
anchors.fill: parent
hoverEnabled: true
@ -417,6 +411,17 @@ Item {
object.menu.removeItem(object)
}
}
}
}
// Have Text after MouseArea, so links can be clicked
Text {
id: text
anchors.centerIn: parent
text: mapText
textFormat: TextEdit.RichText
onLinkActivated: {
console.log("Link", link);
mapModel.link(link);
}
}
}

Plik binarny nie jest wyświetlany.

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 1.6 KiB

Wyświetl plik

@ -1,5 +1,5 @@
///////////////////////////////////////////////////////////////////////////////////
// Copyright (C) 2021-2023 Jon Beniston, M7RCE <jon@beniston.com> //
// Copyright (C) 2021-2024 Jon Beniston, M7RCE <jon@beniston.com> //
// Copyright (C) 2022 Edouard Griffiths, F4EXB <f4exb06@gmail.com> //
// //
// This program is free software; you can redistribute it and/or modify //
@ -22,6 +22,7 @@
#include <QTimer>
#include <QQuickItem>
#include <QQuickWidget>
#include <QTextEdit>
#include <QJsonObject>
#ifdef QT_WEBENGINE_FOUND
#include <QWebEngineFullScreenRequest>
@ -40,7 +41,13 @@
#include "util/azel.h"
#include "util/openaip.h"
#include "util/ourairportsdb.h"
#include "util/waypoints.h"
#include "util/rainviewer.h"
#include "util/nasaglobalimagery.h"
#include "util/kiwisdrlist.h"
#include "util/spyserverlist.h"
#include "settings/rollupstate.h"
#include "availablechannelorfeaturehandler.h"
#include "SWGMapItem.h"
@ -50,6 +57,7 @@
#include "mapradiotimedialog.h"
#include "cesiuminterface.h"
#include "osmtemplateserver.h"
#include "maptileserver.h"
#include "webserver.h"
#include "mapmodel.h"
@ -63,6 +71,7 @@ namespace Ui {
class MapGUI;
struct Beacon;
class QSvgWidget;
struct RadioTimeTransmitter {
QString m_callsign;
@ -165,8 +174,11 @@ public:
void addAirspace(const Airspace *airspace, const QString& group, int cnt);
void addAirspace();
void addAirports();
void addWaypoints();
void addNavtex();
void addVLF();
void addKiwiSDR();
void addSpyServer();
void find(const QString& target);
void track3D(const QString& target);
Q_INVOKABLE void supportedMapsChanged();
@ -181,7 +193,7 @@ private:
QList<QString> m_settingsKeys;
RollupState m_rollupState;
bool m_doApplySettings;
QList<MapSettings::AvailableChannelOrFeature> m_availableChannelOrFeatures;
AvailableChannelOrFeatureList m_availableChannelOrFeatures;
Map* m_map;
MessageQueue m_inputMessageQueue;
@ -201,17 +213,43 @@ private:
MapRadioTimeDialog m_radioTimeDialog;
quint16 m_osmPort;
OSMTemplateServer *m_templateServer;
quint16 m_mapTileServerPort;
MapTileServer *m_mapTileServer;
QTimer m_redrawMapTimer;
GIRO *m_giro;
QHash<QString, IonosondeStation *> m_ionosondeStations;
QSharedPointer<const QList<NavAid *>> m_navAids;
QSharedPointer<const QList<Airspace *>> m_airspaces;
QSharedPointer<const QHash<int, AirportInformation *>> m_airportInfo;
QSharedPointer<const QHash<QString, Waypoint *>> m_waypoints;
QGeoCoordinate m_lastFullUpdatePosition;
KiwiSDRList m_kiwiSDRList;
SpyServerList m_spyServerList;
CesiumInterface *m_cesium;
WebServer *m_webServer;
quint16 m_webPort;
RainViewer *m_rainViewer;
NASAGlobalImagery m_nasaGlobalImagery;
QList<NASAGlobalImagery::DataSet> m_nasaDataSets;
QHash<QString, NASAGlobalImagery::DataSet> m_nasaDataSetsHash;
NASAGlobalImagery::MetaData m_nasaMetaData;
QAction *m_displaySeaMarks;
QAction *m_displayRailways;
QAction *m_displayRain;
QAction *m_displayClouds;
QAction *m_displayNASAGlobalImagery;
QAction *m_displayMUF;
QAction *m_displayfoF2;
QString m_radarPath;
QString m_satellitePath;
NASAGlobalImagery::Legend *m_legend;
QWidget *m_nasaWidget;
QSvgWidget *m_legendWidget;
QTableWidget *m_overviewWidget;
QTextEdit *m_descriptionWidget;
explicit MapGUI(PluginAPI* pluginAPI, FeatureUISet *featureUISet, Feature *feature, QWidget* parent = nullptr);
virtual ~MapGUI();
@ -232,11 +270,31 @@ private:
QString cesiumIonAPIKey() const;
void redrawMap();
void makeUIConnections();
void createLayersMenu();
void displayToolbar();
void setEnableOverlay();
void applyNASAGlobalImagerySettings();
void createNASAGlobalImageryView();
void displayNASAMetaData();
void openKiwiSDR(const QString& url);
void openSpyServer(const QString& url);
QString formatFrequency(qint64 frequency) const;
static QString getDataDir();
static const QList<RadioTimeTransmitter> m_radioTimeTransmitters;
static const QList<RadioTimeTransmitter> m_vlfTransmitters;
enum NASARow {
NASA_TITLE,
NASA_SUBTITLE,
NASA_DEFAULT_DATE,
NASA_START_DATE,
NASA_END_DATE,
NASA_PERIOD,
NASA_LAYER_GROUP,
NASA_ROWS
};
private slots:
void init3DMap();
void onMenuDialogCalled(const QPoint &p);
@ -245,8 +303,16 @@ private slots:
void on_displayNames_clicked(bool checked=false);
void on_displayAllGroundTracks_clicked(bool checked=false);
void on_displaySelectedGroundTracks_clicked(bool checked=false);
void on_displayRain_clicked(bool checked=false);
void on_displayClouds_clicked(bool checked=false);
void on_displaySeaMarks_clicked(bool checked=false);
void on_displayRailways_clicked(bool checked=false);
void on_displayMUF_clicked(bool checked=false);
void on_displayfoF2_clicked(bool checked=false);
void on_displayNASAGlobalImagery_clicked(bool checked=false);
void on_nasaGlobalImageryIdentifier_currentIndexChanged(int index);
void on_nasaGlobalImageryOpacity_valueChanged(int index);
void on_layersMenu_clicked();
void on_find_returnPressed();
void on_maidenhead_clicked();
void on_deleteAll_clicked();
@ -258,6 +324,7 @@ private slots:
void receivedCesiumEvent(const QJsonObject &obj);
virtual void showEvent(QShowEvent *event) override;
virtual bool eventFilter(QObject *obj, QEvent *event) override;
void orientationChanged(Qt::ScreenOrientation orientation);
#ifdef QT_WEBENGINE_FOUND
void fullScreenRequested(QWebEngineFullScreenRequest fullScreenRequest);
void renderProcessTerminated(QWebEnginePage::RenderProcessTerminationStatus terminationStatus, int exitCode);
@ -270,9 +337,18 @@ private slots:
void giroDataUpdated(const GIRO::GIROStationData& data);
void mufUpdated(const QJsonDocument& document);
void foF2Updated(const QJsonDocument& document);
void pathUpdated(const QString& radarPath, const QString& satellitePath);
void nasaGlobalImageryDataUpdated(const QList<NASAGlobalImagery::DataSet>& dataSets);
void nasaGlobalImageryMetaDataUpdated(const NASAGlobalImagery::MetaData& metaData);
void nasaGlobalImageryLegendAvailable(const QString& url, const QByteArray& data);
void nasaGlobalImageryHTMLAvailable(const QString& url, const QByteArray& data);
void navAidsUpdated();
void airspacesUpdated();
void airportsUpdated();
void waypointsUpdated();
void kiwiSDRUpdated(const QList<KiwiSDRList::KiwiSDR>& sdrs);
void spyServerUpdated(const QList<SpyServerList::SpyServer>& sdrs);
void linkClicked(const QString& url);
};

Wyświetl plik

@ -171,6 +171,86 @@
</property>
</widget>
</item>
<item>
<widget class="ButtonSwitch" name="displayRain">
<property name="toolTip">
<string>Display weather radar (rain/snow)</string>
</property>
<property name="text">
<string>^</string>
</property>
<property name="icon">
<iconset resource="icons.qrc">
<normaloff>:/map/icons/precipitation.png</normaloff>:/map/icons/precipitation.png</iconset>
</property>
<property name="checkable">
<bool>true</bool>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="ButtonSwitch" name="displaySeaMarks">
<property name="toolTip">
<string>Display sea marks</string>
</property>
<property name="text">
<string>^</string>
</property>
<property name="icon">
<iconset resource="icons.qrc">
<normaloff>:/map/icons/anchor.png</normaloff>:/map/icons/anchor.png</iconset>
</property>
<property name="checkable">
<bool>true</bool>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="ButtonSwitch" name="displayClouds">
<property name="toolTip">
<string>Display satellite infra-red (clouds)</string>
</property>
<property name="text">
<string>^</string>
</property>
<property name="icon">
<iconset resource="icons.qrc">
<normaloff>:/map/icons/cloud.png</normaloff>:/map/icons/cloud.png</iconset>
</property>
<property name="checkable">
<bool>true</bool>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="ButtonSwitch" name="displayRailways">
<property name="toolTip">
<string>Display railways</string>
</property>
<property name="text">
<string>^</string>
</property>
<property name="icon">
<iconset resource="icons.qrc">
<normaloff>:/map/icons/railway.png</normaloff>:/map/icons/railway.png</iconset>
</property>
<property name="checkable">
<bool>true</bool>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="ButtonSwitch" name="displayMUF">
<property name="toolTip">

Wyświetl plik

@ -6,8 +6,8 @@
<rect>
<x>0</x>
<y>0</y>
<width>481</width>
<height>507</height>
<width>1282</width>
<height>293</height>
</rect>
</property>
<property name="sizePolicy">
@ -18,7 +18,7 @@
</property>
<property name="minimumSize">
<size>
<width>462</width>
<width>0</width>
<height>0</height>
</size>
</property>
@ -39,13 +39,13 @@
<rect>
<x>0</x>
<y>0</y>
<width>480</width>
<width>1221</width>
<height>41</height>
</rect>
</property>
<property name="minimumSize">
<size>
<width>480</width>
<width>300</width>
<height>0</height>
</size>
</property>
@ -165,10 +165,104 @@
</property>
</widget>
</item>
<item>
<widget class="QToolButton" name="layersMenu">
<property name="text">
<string/>
</property>
<property name="icon">
<iconset resource="icons.qrc">
<normaloff>:/map/icons/layers.png</normaloff>:/map/icons/layers.png</iconset>
</property>
<property name="popupMode">
<enum>QToolButton::InstantPopup</enum>
</property>
</widget>
</item>
<item>
<widget class="ButtonSwitch" name="displayClouds">
<property name="toolTip">
<string>Display satellite infra-red (clouds)</string>
</property>
<property name="text">
<string>^</string>
</property>
<property name="icon">
<iconset resource="icons.qrc">
<normaloff>:/map/icons/cloud.png</normaloff>:/map/icons/cloud.png</iconset>
</property>
<property name="checkable">
<bool>true</bool>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="ButtonSwitch" name="displayRain">
<property name="toolTip">
<string>Display weather radar (rain/snow)</string>
</property>
<property name="text">
<string>^</string>
</property>
<property name="icon">
<iconset resource="icons.qrc">
<normaloff>:/map/icons/precipitation.png</normaloff>:/map/icons/precipitation.png</iconset>
</property>
<property name="checkable">
<bool>true</bool>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="ButtonSwitch" name="displaySeaMarks">
<property name="toolTip">
<string>Display sea marks</string>
</property>
<property name="text">
<string>^</string>
</property>
<property name="icon">
<iconset resource="icons.qrc">
<normaloff>:/map/icons/anchor.png</normaloff>:/map/icons/anchor.png</iconset>
</property>
<property name="checkable">
<bool>true</bool>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="ButtonSwitch" name="displayRailways">
<property name="toolTip">
<string>Display railways</string>
</property>
<property name="text">
<string>^</string>
</property>
<property name="icon">
<iconset resource="icons.qrc">
<normaloff>:/map/icons/railway.png</normaloff>:/map/icons/railway.png</iconset>
</property>
<property name="checkable">
<bool>true</bool>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="ButtonSwitch" name="displayMUF">
<property name="toolTip">
<string>Display MUF contours</string>
<string>Display MUF (Maximum Usable Frequency) contours (3D only)</string>
</property>
<property name="text">
<string>^</string>
@ -188,7 +282,7 @@
<item>
<widget class="ButtonSwitch" name="displayfoF2">
<property name="toolTip">
<string>Display foF2 contours</string>
<string>Display foF2 (F2 layer critical frequency) contours (3D only)</string>
</property>
<property name="text">
<string>^</string>
@ -205,6 +299,71 @@
</property>
</widget>
</item>
<item>
<widget class="ButtonSwitch" name="displayNASAGlobalImagery">
<property name="toolTip">
<string>Display NASA GIBS data</string>
</property>
<property name="text">
<string>^</string>
</property>
<property name="icon">
<iconset resource="icons.qrc">
<normaloff>:/map/icons/earthsat.png</normaloff>:/map/icons/earthsat.png</iconset>
</property>
<property name="checkable">
<bool>true</bool>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QComboBox" name="nasaGlobalImageryIdentifier">
<property name="minimumSize">
<size>
<width>200</width>
<height>0</height>
</size>
</property>
<property name="toolTip">
<string>NASA GIBS data</string>
</property>
</widget>
</item>
<item>
<widget class="QDial" name="nasaGlobalImageryOpacity">
<property name="maximumSize">
<size>
<width>24</width>
<height>24</height>
</size>
</property>
<property name="toolTip">
<string>NASA GIBS image opacity (3D only)</string>
</property>
<property name="maximum">
<number>100</number>
</property>
<property name="value">
<number>100</number>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="nasaGlobalImageryOpacityText">
<property name="minimumSize">
<size>
<width>34</width>
<height>0</height>
</size>
</property>
<property name="text">
<string>100%</string>
</property>
</widget>
</item>
<item>
<widget class="ButtonSwitch" name="displayNames">
<property name="toolTip">
@ -392,12 +551,6 @@
<extends>QWidget</extends>
<header location="global">QtQuickWidgets/QQuickWidget</header>
</customwidget>
<customwidget>
<class>QWebEngineView</class>
<extends>QWidget</extends>
<header location="global">QtWebEngineWidgets/QWebEngineView</header>
<container>1</container>
</customwidget>
<customwidget>
<class>ButtonSwitch</class>
<extends>QToolButton</extends>
@ -409,6 +562,12 @@
<header>gui/rollupcontents.h</header>
<container>1</container>
</customwidget>
<customwidget>
<class>QWebEngineView</class>
<extends>QWidget</extends>
<header location="global">QtWebEngineWidgets/QWebEngineView</header>
<container>1</container>
</customwidget>
</customwidgets>
<tabstops>
<tabstop>find</tabstop>

Wyświetl plik

@ -619,6 +619,11 @@ Q_INVOKABLE void ObjectMapModel::moveToBack(int oldRow)
}
}
Q_INVOKABLE void ObjectMapModel::link(const QString& url)
{
emit linkClicked(url);
}
QVariant ObjectMapModel::data(const QModelIndex &index, int role) const
{
int row = index.row();

Wyświetl plik

@ -238,6 +238,8 @@ public:
Q_INVOKABLE void moveToFront(int oldRow);
Q_INVOKABLE void moveToBack(int oldRow);
Q_INVOKABLE void link(const QString& link);
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
bool setData(const QModelIndex &index, const QVariant& value, int role = Qt::EditRole) override;
@ -248,7 +250,6 @@ public:
Q_INVOKABLE QStringList getDeviceSets() const;
Q_INVOKABLE void setFrequency(qint64 frequency, const QString& deviceSet);
Q_INVOKABLE void viewChanged(double bottomLeftLongitude, double bottomRightLongitude);
bool isSelected3D(const ObjectMapItem *item) const
@ -264,6 +265,9 @@ public:
//public slots:
// void update3DMap(const QModelIndex &topLeft, const QModelIndex &bottomRight, const QVector<int> &roles = QVector<int>()) override;
signals:
void linkClicked(const QString& url);
protected:
void playAnimations(ObjectMapItem *item);
MapItem *newMapItem(const QObject *sourcePipe, const QString &group, MapSettings::MapItemSettings *itemSettings, SWGSDRangel::SWGMapItem *mapItem) override;

Wyświetl plik

@ -60,15 +60,21 @@ const QStringList MapSettings::m_pipeURIs = {
QStringLiteral("sdrangel.feature.vorlocalizer")
};
// GUI combo box should match ordering in this list
const QStringList MapSettings::m_mapProviders = {
QStringLiteral("osm"),
QStringLiteral("esri"),
QStringLiteral("mapbox"),
QStringLiteral("mapboxgl"),
QStringLiteral("maplibregl")
};
// Names as used in combo box in settings dialog
const QStringList MapSettings::m_mapProviderNames = {
QStringLiteral("OpenStreetMap"),
QStringLiteral("ESRI"),
QStringLiteral("MapboxGL"),
QStringLiteral("MapLibreGL")
};
MapSettings::MapSettings() :
m_rollupState(nullptr)
{
@ -144,6 +150,13 @@ MapSettings::MapSettings() :
m_itemSettings.insert("Airspace (Wave)", new MapItemSettings("Airspace (Wave)", false, QColor(255, 0, 0, 0x20), false, false, 11));
m_itemSettings.insert("Airspace (Airports)", new MapItemSettings("Airspace (Airports)", false, QColor(0, 0, 255, 0x20), false, false, 11));
MapItemSettings *waypointsSettings = new MapItemSettings("Waypoints", false, QColor(255, 0, 255), false, true, 11);
waypointsSettings->m_filterDistance = 500000;
m_itemSettings.insert("Waypoints", waypointsSettings);
m_itemSettings.insert("KiwiSDR", new MapItemSettings("KiwiSDR", true, QColor(0, 255, 0), false, true, 8));
m_itemSettings.insert("SpyServer", new MapItemSettings("SpyServer", true, QColor(0, 0, 255), false, true, 8));
resetToDefaults();
}
@ -184,6 +197,12 @@ void MapSettings::resetToDefaults()
m_antiAliasing = "None";
m_displayMUF = false;
m_displayfoF2 = false;
m_displayRain = false;
m_displayClouds = false;
m_displaySeaMarks = false;
m_displayNASAGlobalImagery = false;
m_nasaGlobalImageryIdentifier = "";
m_nasaGlobalImageryOpacity = 50;
m_workspaceIndex = 0;
m_checkWXAPIKey = "";
}
@ -229,6 +248,13 @@ QByteArray MapSettings::serialize() const
s.writeBool(35, m_displayMUF);
s.writeBool(36, m_displayfoF2);
s.writeBool(37, m_displayRain);
s.writeBool(38, m_displayClouds);
s.writeBool(39, m_displaySeaMarks);
s.writeBool(40, m_displayRailways);
s.writeBool(41, m_displayNASAGlobalImagery);
s.writeString(42, m_nasaGlobalImageryIdentifier);
s.writeS32(43, m_nasaGlobalImageryOpacity);
s.writeString(46, m_checkWXAPIKey);
@ -308,6 +334,13 @@ bool MapSettings::deserialize(const QByteArray& data)
d.readBool(35, &m_displayMUF, false);
d.readBool(36, &m_displayfoF2, false);
d.readBool(37, &m_displayRain, false);
d.readBool(38, &m_displayClouds, false);
d.readBool(39, &m_displaySeaMarks, false);
d.readBool(40, &m_displayRailways, false);
d.readBool(41, &m_displayNASAGlobalImagery, false);
d.readString(42, &m_nasaGlobalImageryIdentifier, "");
d.readS32(43, &m_nasaGlobalImageryOpacity, 50);
d.readString(46, &m_checkWXAPIKey, "");
@ -556,9 +589,30 @@ void MapSettings::applySettings(const QStringList& settingsKeys, const MapSettin
if (settingsKeys.contains("displayMUF")) {
m_displayMUF = settings.m_displayMUF;
}
if (settingsKeys.contains("misplayfoF2")) {
if (settingsKeys.contains("displayfoF2")) {
m_displayfoF2 = settings.m_displayfoF2;
}
if (settingsKeys.contains("displayRain")) {
m_displayRain = settings.m_displayRain;
}
if (settingsKeys.contains("displayClouds")) {
m_displayClouds = settings.m_displayClouds;
}
if (settingsKeys.contains("displaySeaMarks")) {
m_displaySeaMarks = settings.m_displaySeaMarks;
}
if (settingsKeys.contains("displayRailways")) {
m_displayRailways = settings.m_displayRailways;
}
if (settingsKeys.contains("displayNASAGlobalImagery")) {
m_displayNASAGlobalImagery = settings.m_displayNASAGlobalImagery;
}
if (settingsKeys.contains("nasaGlobalImageryIdentifier")) {
m_nasaGlobalImageryIdentifier = settings.m_nasaGlobalImageryIdentifier;
}
if (settingsKeys.contains("nasaGlobalImageryOpacity")) {
m_nasaGlobalImageryOpacity = settings.m_nasaGlobalImageryOpacity;
}
if (settingsKeys.contains("workspaceIndex")) {
m_workspaceIndex = settings.m_workspaceIndex;
}
@ -646,10 +700,30 @@ QString MapSettings::getDebugString(const QStringList& settingsKeys, bool force)
if (settingsKeys.contains("displayfoF2") || force) {
ostr << " m_displayfoF2: " << m_displayfoF2;
}
if (settingsKeys.contains("displayRain") || force) {
ostr << " m_displayRain: " << m_displayRain;
}
if (settingsKeys.contains("displayClouds") || force) {
ostr << " m_displayClouds: " << m_displayClouds;
}
if (settingsKeys.contains("displaySeaMarks") || force) {
ostr << " m_displaySeaMarks: " << m_displaySeaMarks;
}
if (settingsKeys.contains("displayRailways") || force) {
ostr << " m_displayRailways: " << m_displayRailways;
}
if (settingsKeys.contains("displayNASAGlobalImagery") || force) {
ostr << " m_displayNASAGlobalImagery: " << m_displayNASAGlobalImagery;
}
if (settingsKeys.contains("nasaGlobalImageryIdentifier") || force) {
ostr << " m_nasaGlobalImageryIdentifier: " << m_nasaGlobalImageryIdentifier.toStdString();
}
if (settingsKeys.contains("nasaGlobalImageryOpacity") || force) {
ostr << " m_nasaGlobalImageryOpacity: " << m_nasaGlobalImageryOpacity;
}
if (settingsKeys.contains("workspaceIndex") || force) {
ostr << " m_workspaceIndex: " << m_workspaceIndex;
}
return QString(ostr.str().c_str());
}

Wyświetl plik

@ -2,7 +2,7 @@
// Copyright (C) 2012 maintech GmbH, Otto-Hahn-Str. 15, 97204 Hoechberg, Germany //
// written by Christian Daniel //
// Copyright (C) 2015-2020, 2022 Edouard Griffiths, F4EXB <f4exb06@gmail.com> //
// Copyright (C) 2020-2023 Jon Beniston, M7RCE <jon@beniston.com> //
// Copyright (C) 2020-2024 Jon Beniston, M7RCE <jon@beniston.com> //
// //
// This program is free software; you can redistribute it and/or modify //
// it under the terms of the GNU General Public License as published by //
@ -66,26 +66,6 @@ struct MapSettings
quint32 m_3DColor;
};
struct AvailableChannelOrFeature
{
QString m_kind; //!< "R" for channel, "F" for feature
int m_superIndex;
int m_index;
QString m_type;
QObject *m_source;
AvailableChannelOrFeature() = default;
AvailableChannelOrFeature(const AvailableChannelOrFeature&) = default;
AvailableChannelOrFeature& operator=(const AvailableChannelOrFeature&) = default;
bool operator==(const AvailableChannelOrFeature& a) const {
return (m_kind == a.m_kind)
&& (m_superIndex == a.m_superIndex)
&& (m_index == a.m_index)
&& (m_type == a.m_type)
&& (m_source == a.m_source);
}
};
bool m_displayNames;
QString m_mapProvider;
QString m_thunderforestAPIKey;
@ -121,6 +101,13 @@ struct MapSettings
bool m_displayMUF; // Plot MUF contours
bool m_displayfoF2; // Plot foF2 contours
bool m_displayRain;
bool m_displayClouds;
bool m_displaySeaMarks;
bool m_displayRailways;
bool m_displayNASAGlobalImagery;
QString m_nasaGlobalImageryIdentifier;
int m_nasaGlobalImageryOpacity;
QString m_checkWXAPIKey; //!< checkwxapi.com API key
@ -142,6 +129,7 @@ struct MapSettings
static const QStringList m_pipeURIs;
static const QStringList m_mapProviders;
static const QStringList m_mapProviderNames;
};
Q_DECLARE_METATYPE(MapSettings::MapItemSettings *);

Wyświetl plik

@ -120,8 +120,14 @@ MapSettingsDialog::MapSettingsDialog(MapSettings *settings, QWidget* parent) :
#if (QT_VERSION >= QT_VERSION_CHECK(6, 0, 0))
ui->mapProvider->clear();
ui->mapProvider->addItem("OpenStreetMap");
#else
#ifdef WIN32
ui->mapProvider->removeItem(ui->mapProvider->findText("MapboxGL")); // Not supported on Windows
#endif
ui->mapProvider->setCurrentIndex(MapSettings::m_mapProviders.indexOf(settings->m_mapProvider));
ui->mapProvider->removeItem(ui->mapProvider->findText("ESRI")); // Currently broken https://bugreports.qt.io/browse/QTBUG-121228
#endif
const QString mapProviderName = MapSettings::m_mapProviderNames[MapSettings::m_mapProviders.indexOf(settings->m_mapProvider)];
ui->mapProvider->setCurrentIndex(ui->mapProvider->findText(mapProviderName));
ui->thunderforestAPIKey->setText(settings->m_thunderforestAPIKey);
ui->maptilerAPIKey->setText(settings->m_maptilerAPIKey);
ui->mapBoxAPIKey->setText(settings->m_mapBoxAPIKey);
@ -198,6 +204,7 @@ MapSettingsDialog::MapSettingsDialog(MapSettings *settings, QWidget* parent) :
connect(&m_ourAirportsDB, &OurAirportsDB::downloadProgress, this, &MapSettingsDialog::downloadProgress);
connect(&m_ourAirportsDB, &OurAirportsDB::downloadError, this, &MapSettingsDialog::downloadError);
connect(&m_ourAirportsDB, &OurAirportsDB::downloadAirportInformationFinished, this, &MapSettingsDialog::downloadAirportInformationFinished);
connect(&m_waypoints, &Waypoints::downloadWaypointsFinished, this, &MapSettingsDialog::downloadWaypointsFinished);
#ifndef QT_WEBENGINE_FOUND
ui->map3DSettings->setVisible(false);
@ -219,7 +226,7 @@ MapSettingsDialog::~MapSettingsDialog()
void MapSettingsDialog::accept()
{
QString mapProvider = MapSettings::m_mapProviders[ui->mapProvider->currentIndex()];
QString mapProvider = MapSettings::m_mapProviders[MapSettings::m_mapProviderNames.indexOf(ui->mapProvider->currentText())];
QString osmURL = ui->osmURL->text();
QString mapBoxStyles = ui->mapBoxStyles->text();
QString mapBoxAPIKey = ui->mapBoxAPIKey->text();
@ -515,6 +522,19 @@ void MapSettingsDialog::on_getAirspacesDB_clicked()
}
}
void MapSettingsDialog::on_getWaypoints_clicked()
{
// Don't try to download while already in progress
if (m_progressDialog == nullptr)
{
m_progressDialog = new QProgressDialog(this);
m_progressDialog->setMaximum(1);
m_progressDialog->setCancelButton(nullptr);
m_progressDialog->setWindowFlag(Qt::WindowCloseButtonHint, false);
m_waypoints.downloadWaypoints();
}
}
void MapSettingsDialog::downloadingURL(const QString& url)
{
if (m_progressDialog)
@ -581,3 +601,17 @@ void MapSettingsDialog::downloadAirportInformationFinished()
}
}
void MapSettingsDialog::downloadWaypointsFinished()
{
if (m_progressDialog) {
m_progressDialog->setLabelText("Reading waypoints.");
}
emit waypointsUpdated();
if (m_progressDialog)
{
m_progressDialog->close();
delete m_progressDialog;
m_progressDialog = nullptr;
}
}

Wyświetl plik

@ -29,6 +29,7 @@
#include "gui/httpdownloadmanagergui.h"
#include "util/openaip.h"
#include "util/ourairportsdb.h"
#include "util/waypoints.h"
#include "ui_mapsettingsdialog.h"
#include "mapsettings.h"
@ -104,6 +105,7 @@ private:
QProgressDialog *m_progressDialog;
OpenAIP m_openAIP;
OurAirportsDB m_ourAirportsDB;
Waypoints m_waypoints;
void unzip(const QString &filename);
@ -114,6 +116,7 @@ private slots:
void on_downloadModels_clicked();
void on_getAirportDB_clicked();
void on_getAirspacesDB_clicked();
void on_getWaypoints_clicked();
void downloadComplete(const QString &filename, bool success, const QString &url, const QString &errorMessage);
void downloadingURL(const QString& url);
void downloadProgress(qint64 bytesRead, qint64 totalBytes);
@ -121,11 +124,13 @@ private slots:
void downloadAirspaceFinished();
void downloadNavAidsFinished();
void downloadAirportInformationFinished();
void downloadWaypointsFinished();
signals:
void navAidsUpdated();
void airspacesUpdated();
void airportsUpdated();
void waypointsUpdated();
private:
Ui::MapSettingsDialog* ui;

Wyświetl plik

@ -83,11 +83,6 @@
<string>ESRI</string>
</property>
</item>
<item>
<property name="text">
<string>Mapbox</string>
</property>
</item>
<item>
<property name="text">
<string>MapboxGL</string>
@ -337,6 +332,20 @@
</property>
</widget>
</item>
<item>
<widget class="QToolButton" name="getWaypoints">
<property name="toolTip">
<string>Download aviation waypoints (3MB)</string>
</property>
<property name="text">
<string/>
</property>
<property name="icon">
<iconset resource="icons.qrc">
<normaloff>:/map/icons/waypoints.png</normaloff>:/map/icons/waypoints.png</iconset>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer">
<property name="orientation">

Wyświetl plik

@ -0,0 +1,18 @@
///////////////////////////////////////////////////////////////////////////////////
// Copyright (C) 2024 Jon Beniston, M7RCE <jon@beniston.com> //
// //
// This program is free software; you can redistribute it and/or modify //
// it under the terms of the GNU General Public License as published by //
// the Free Software Foundation as version 3 of the License, or //
// (at your option) any later version. //
// //
// This program is distributed in the hope that it will be useful, //
// but WITHOUT ANY WARRANTY; without even the implied warranty of //
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the //
// GNU General Public License V3 for more details. //
// //
// You should have received a copy of the GNU General Public License //
// along with this program. If not, see <http://www.gnu.org/licenses/>. //
///////////////////////////////////////////////////////////////////////////////////
#include "maptileserver.h"

Wyświetl plik

@ -0,0 +1,431 @@
///////////////////////////////////////////////////////////////////////////////////
// Copyright (C) 2021, 2023 Jon Beniston, M7RCE <jon@beniston.com> //
// //
// This program is free software; you can redistribute it and/or modify //
// it under the terms of the GNU General Public License as published by //
// the Free Software Foundation as version 3 of the License, or //
// (at your option) any later version. //
// //
// This program is distributed in the hope that it will be useful, //
// but WITHOUT ANY WARRANTY; without even the implied warranty of //
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the //
// GNU General Public License V3 for more details. //
// //
// You should have received a copy of the GNU General Public License //
// along with this program. If not, see <http://www.gnu.org/licenses/>. //
///////////////////////////////////////////////////////////////////////////////////
#ifndef INCLUDE_MAPTILESERVER_H_
#define INCLUDE_MAPTILESERVER_H_
#include <QTcpServer>
#include <QTcpSocket>
#include <QRegularExpression>
#include <QDebug>
#include <QtNetwork>
#include <QImage>
#include <QPainter>
#include <QHash>
#include <QMutex>
#include <QMutexLocker>
#include <QNetworkDiskCache>
class MapTileServer : public QTcpServer
{
Q_OBJECT
private:
QString m_thunderforestAPIKey;
QString m_maptilerAPIKey;
QNetworkAccessManager m_manager;
QMutex m_mutex;
struct TileJob {
QTcpSocket* m_socket;
QList<QString> m_urls;
QHash<QString, QImage> m_images;
QString m_format;
};
QList<TileJob *> m_tileJobs;
QHash<QNetworkReply *, TileJob *> m_replies;
QNetworkDiskCache *m_cache;
QString m_radarPath;
QString m_satellitePath;
QString m_nasaGlobalImageryPath;
QString m_nasaGlobalImageryFormat;
bool m_displayRain;
bool m_displayClouds;
bool m_displaySeaMarks;
bool m_displayRailways;
bool m_displayNASAGlobalImagery;
public:
// port - port to listen on / is listening on. Use 0 for any free port.
MapTileServer(quint16 &port, QObject* parent = 0) :
QTcpServer(parent),
m_thunderforestAPIKey(""),
m_maptilerAPIKey(""),
m_radarPath(""),
m_satellitePath(""),
m_nasaGlobalImageryPath(""),
m_nasaGlobalImageryFormat(""),
m_displayRain(false),
m_displayClouds(false),
m_displaySeaMarks(false),
m_displayRailways(false),
m_displayNASAGlobalImagery(false)
{
connect(&m_manager, &QNetworkAccessManager::finished, this, &MapTileServer::downloadFinished);
listen(QHostAddress::Any, port);
port = serverPort();
QStringList locations = QStandardPaths::standardLocations(QStandardPaths::AppDataLocation);
QDir writeableDir(locations[0]);
if (!writeableDir.mkpath(QStringLiteral("cache") + QDir::separator() + QStringLiteral("maptiles"))) {
qDebug() << "Failed to create cache/maptiles";
}
m_cache = new QNetworkDiskCache();
m_cache->setCacheDirectory(locations[0] + QDir::separator() + QStringLiteral("cache") + QDir::separator() + QStringLiteral("maptiles"));
m_cache->setMaximumCacheSize(1000000000);
m_manager.setCache(m_cache);
}
~MapTileServer()
{
disconnect(&m_manager, &QNetworkAccessManager::finished, this, &MapTileServer::downloadFinished);
delete m_cache;
}
void setThunderforestAPIKey(const QString& thunderforestAPIKey)
{
m_thunderforestAPIKey = thunderforestAPIKey;
}
void setMaptilerAPIKey(const QString& maptilerAPIKey)
{
m_maptilerAPIKey = maptilerAPIKey;
}
void setRadarPath(const QString& radarPath)
{
m_radarPath = radarPath;
}
void setSatellitePath(const QString& satellitePath)
{
m_satellitePath = satellitePath;
}
void setNASAGlobalImageryPath(const QString& nasaGlobalImageryPath)
{
m_nasaGlobalImageryPath = nasaGlobalImageryPath;
}
void setNASAGlobalImageryFormat(const QString& nasaGlobalImageryFormat)
{
m_nasaGlobalImageryFormat = nasaGlobalImageryFormat;
}
void setDisplaySeaMarks(bool displaySeaMarks)
{
m_displaySeaMarks = displaySeaMarks;
}
void setDisplayRailways(bool displayRailways)
{
m_displayRailways = displayRailways;
}
void setDisplayRain(bool displayRain)
{
m_displayRain = displayRain;
}
void setDisplayClouds(bool displayClouds)
{
m_displayClouds = displayClouds;
}
void setDisplayNASAGlobalImagery(bool displayNASAGlobalImagery)
{
m_displayNASAGlobalImagery = displayNASAGlobalImagery;
}
void incomingConnection(qintptr socket) override
{
QTcpSocket* s = new QTcpSocket(this);
connect(s, SIGNAL(readyRead()), this, SLOT(readClient()));
connect(s, SIGNAL(disconnected()), this, SLOT(discardClient()));
s->setSocketDescriptor(socket);
//addPendingConnection(socket);
}
bool isHttpRedirect(QNetworkReply *reply)
{
int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
// 304 is file not changed, but maybe we did
return (status >= 301 && status <= 308);
}
QNetworkReply *download(const QUrl &url)
{
QNetworkRequest request(url);
request.setAttribute(QNetworkRequest::RedirectPolicyAttribute, QNetworkRequest::NoLessSafeRedirectPolicy);
request.setRawHeader("User-Agent", "SDRangel"); // Required by a.tile.openstreetmap.org
// Don't cache rainviwer data as it's dynamic
if (!url.toString().contains("tilecache.rainviewer")) {
request.setAttribute(QNetworkRequest::CacheLoadControlAttribute, QNetworkRequest::PreferCache);
}
QNetworkReply *reply = m_manager.get(request);
connect(reply, &QNetworkReply::sslErrors, this, &MapTileServer::sslErrors);
//qDebug() << "MapTileServer: Downloading from " << url;
return reply;
}
QImage combine(const TileJob *job)
{
// Don't use job->m_images[job->m_urls[0]].size() as not always valid (E.g. map tiler can return http 204 - no content)
// Do we need to support 512x512?
QImage image(QSize(256, 256), QImage::Format_ARGB32_Premultiplied);
image.fill(qPremultiply(QColor(0, 0, 0, 0).rgba()));
QPainter painter(&image);
for (int i = 0; i < job->m_images.size(); i++) {
const QImage &img = job->m_images[job->m_urls[i]];
//qDebug() << "Image format " << i << " is " << img.format() << img.size();
}
for (int i = 0; i < job->m_images.size(); i++) {
const QImage &img = job->m_images[job->m_urls[i]];
//img.save(QString("in%1.png").arg(i), "PNG");
if (img.format() != QImage::Format_Invalid) {
painter.drawImage(image.rect(), img);
}
}
return image;
}
void replyImage(QTcpSocket* socket, const QImage& image, const QString& format)
{
QByteArray ba;
QBuffer buffer(&ba);
buffer.open(QIODevice::WriteOnly);
image.save(&buffer, qPrintable(format));
//qDebug() << "socket: " << socket << "thread:" << QThread::currentThread();
socket->write("HTTP/1.0 200 Ok\r\n"
"Content-Type: image/png\r\n"
"\r\n");
socket->write(buffer.buffer());
socket->close();
if (socket->state() == QTcpSocket::UnconnectedState) {
delete socket;
}
}
void replyError(QTcpSocket* socket)
{
QTextStream os(socket);
os.setAutoDetectUnicode(true);
os << "HTTP/1.0 404 Not Found\r\n"
"Content-Type: text/html\r\n"
"\r\n"
"<html>Not found</html>\r\n";
socket->close();
if (socket->state() == QTcpSocket::UnconnectedState) {
delete socket;
}
}
private slots:
void readClient()
{
QMutexLocker locker(&m_mutex);
QTcpSocket* socket = (QTcpSocket*)sender();
if (socket->canReadLine())
{
QString line = socket->readLine();
qDebug() << "HTTP Request: " << line;
QStringList tokens = QString(line).split(QRegularExpression("[ \r\n][ \r\n]*"));
if (tokens[0] == "GET")
{
QString xml = "";
// Create multiple requests for each image
// https://wiki.openstreetmap.org/wiki/Raster_tile_providers
// rain radar: https://tilecache.rainviewer.com/v2/radar/{timestamp=1705359600}/{size=256}/z/x/y/{color=0}/{options=1_1}.png
// "GET /street/1/2/3.png HTTP/1.1\r\n"
const QRegularExpression re("\\/([A-Za-z0-9\\-_]+)\\/([0-9]+)\\/([0-9]+)\\/([0-9]+).(png|jpg)");
QRegularExpressionMatch match = re.match(tokens[1]);
if (match.hasMatch())
{
QString map, x, y, z, format;
map = match.captured(1);
z = match.captured(2);
x = match.captured(3);
y = match.captured(4);
format = match.captured(5);
TileJob *job = new TileJob;
//qDebug() << "Created job" << job << "socket:" << socket << "thread:" << QThread::currentThread() ;
job->m_socket = socket;
if (format == "png") {
job->m_format = "PNG";
} else {
job->m_format = "JPG";
}
// This should match code in OSMTemplateServer::readClient
QString baseMapURL;
if (map == "street") {
baseMapURL = QString("https://tile.openstreetmap.org/%3/%1/%2.png").arg(x).arg(y).arg(z);
} else if (map == "satellite") {
baseMapURL = QString("https://api.maptiler.com/tiles/satellite-v2/%3/%1/%2.jpg?key=%4").arg(x).arg(y).arg(z).arg(m_maptilerAPIKey);
} else if ((map == "dark_nolabels") || (map == "light_nolabels")) {
baseMapURL = QString("http://1.basemaps.cartocdn.com/%4/%3/%1/%2.png").arg(x).arg(y).arg(z).arg(map);
} else {
baseMapURL = QString("http://a.tile.thunderforest.com/%4/%3/%1/%2.png?apikey=%5").arg(x).arg(y).arg(z).arg(map).arg(m_thunderforestAPIKey);
}
job->m_urls.append(baseMapURL);
if (m_displaySeaMarks) {
job->m_urls.append(QString("https://tiles.openseamap.org/seamark/%3/%1/%2.png").arg(x).arg(y).arg(z));
}
if (m_displayRailways) {
job->m_urls.append(QString("https://a.tiles.openrailwaymap.org/standard/%3/%1/%2.png").arg(x).arg(y).arg(z));
}
if (m_displayNASAGlobalImagery && !m_nasaGlobalImageryPath.isEmpty()) {
job->m_urls.append(QString("https://gibs.earthdata.nasa.gov/wmts/epsg3857/best/%4/%3/%2/%1.%5").arg(x).arg(y).arg(z).arg(m_nasaGlobalImageryPath).arg(m_nasaGlobalImageryFormat)); // x,y reversed compared to others
}
if (m_displayClouds && !m_satellitePath.isEmpty()) {
job->m_urls.append(QString("https://tilecache.rainviewer.com%4/256/%3/%1/%2/0/0_0.png").arg(x).arg(y).arg(z).arg(m_satellitePath));
}
if (m_displayRain && !m_radarPath.isEmpty()) {
job->m_urls.append(QString("https://tilecache.rainviewer.com%4/256/%3/%1/%2/4/1_1.png").arg(x).arg(y).arg(z).arg(m_radarPath));
}
m_tileJobs.append(job);
for (const auto& url : job->m_urls)
{
QNetworkReply *reply = download(QUrl(url));
m_replies.insert(reply, job);
}
}
else
{
replyError(socket);
}
}
}
}
void discardClient()
{
QTcpSocket* socket = (QTcpSocket*)sender();
//qDebug() << "discardClient socket:" << socket;
socket->deleteLater();
for (auto job : m_tileJobs) {
if (job->m_socket == socket) {
//qDebug() << "Socket closed on active job. job: " << job << "socket" << socket;
job->m_socket = nullptr;
}
}
}
void downloadFinished(QNetworkReply *reply)
{
QMutexLocker locker(&m_mutex);
//QString url = reply->url().toEncoded().constData();
QString url = reply->request().url().toEncoded().constData(); // reply->url() may differ if redirection occured, so use requested
if (!isHttpRedirect(reply))
{
QByteArray data = reply->readAll();
QImage image;
if (!reply->error())
{
if (!image.loadFromData(data))
{
qDebug() << "MapTileServer::downloadFinished: Failed to load image: " << url;
}
}
else
{
qDebug() << "MapTileServer::downloadFinished: Error: " << reply->error() << "for" << url;
}
bool found = false;
TileJob *job = m_replies[reply];
if (!m_tileJobs.contains(job)) {
qDebug() << "job has been deleted!";
}
for (const auto& jobURL : job->m_urls)
{
if (jobURL == url)
{
job->m_images.insert(url, image);
if (job->m_urls.size() == job->m_images.size())
{
// All images available
QImage combinedImage = combine(job);
if (job->m_socket)
{
replyImage(job->m_socket, combinedImage, job->m_format);
job->m_socket = nullptr;
m_tileJobs.removeAll(job);
delete job;
//qDebug() << "Delete job" << job;
}
else
{
qDebug() << "Socket was null. URL: " << url << "job:" << job;
}
}
found = true;
break;
}
}
if (!found) {
qDebug() << "MapTileServer::downloadFinished: Failed to match URL: " << url;
}
}
else
{
qDebug() << "MapTileServer::downloadFinished: Redirect";
}
reply->deleteLater();
m_replies.remove(reply);
}
void sslErrors(const QList<QSslError> &sslErrors)
{
for (const QSslError &error : sslErrors)
{
qCritical() << "MapTileServer: SSL error" << (int)error.error() << ": " << error.errorString();
#ifdef ANDROID
// On Android 6 (but not on 12), we always seem to get: "The issuer certificate of a locally looked up certificate could not be found"
// which causes downloads to fail, so ignore
if (error.error() == QSslError::UnableToGetLocalIssuerCertificate)
{
QNetworkReply *reply = qobject_cast<QNetworkReply *>(sender());
QList<QSslError> errorsThatCanBeIgnored;
errorsThatCanBeIgnored << QSslError(QSslError::UnableToGetLocalIssuerCertificate, error.certificate());
reply->ignoreSslErrors(errorsThatCanBeIgnored);
}
#endif
}
}
};
#endif

Wyświetl plik

@ -29,13 +29,17 @@ class OSMTemplateServer : public QTcpServer
private:
QString m_thunderforestAPIKey;
QString m_maptilerAPIKey;
quint16 m_tileServerPort;
bool m_overlay;
public:
// port - port to listen on / is listening on. Use 0 for any free port.
OSMTemplateServer(const QString &thunderforestAPIKey, const QString &maptilerAPIKey, quint16 &port, QObject* parent = 0) :
OSMTemplateServer(const QString &thunderforestAPIKey, const QString &maptilerAPIKey, quint16 tileServerPort, quint16 &port, QObject* parent = 0) :
QTcpServer(parent),
m_thunderforestAPIKey(thunderforestAPIKey),
m_maptilerAPIKey(maptilerAPIKey)
m_maptilerAPIKey(maptilerAPIKey),
m_tileServerPort(tileServerPort),
m_overlay(false)
{
listen(QHostAddress::Any, port);
port = serverPort();
@ -50,13 +54,24 @@ public:
//addPendingConnection(socket);
}
void setThunderforestAPIKey(const QString& thunderforestAPIKey)
{
m_thunderforestAPIKey = thunderforestAPIKey;
}
void setMaptilerAPIKey(const QString& maptilerAPIKey)
{
m_maptilerAPIKey = maptilerAPIKey;
}
void setEnableOverlay(bool enableOverlay)
{
m_overlay = enableOverlay;
}
private slots:
void readClient()
{
QStringList map({"/cycle", "/cycle-hires", "/hiking", "/hiking-hires", "/night-transit", "/night-transit-hires", "/terrain", "/terrain-hires", "/transit", "/transit-hires"});
QStringList mapId({"thf-cycle", "thf-cycle-hires", "thf-hike", "thf-hike-hires", "thf-nighttransit", "thf-nighttransit-hires", "thf-landsc", "thf-landsc-hires", "thf-transit", "thf-transit-hires"});
QStringList mapUrl({"cycle", "cycle", "outdoors", "outdoors", "transport-dark", "transport-dark", "landscape", "landscape", "transport", "transport"});
QTcpSocket* socket = (QTcpSocket*)sender();
if (socket->canReadLine())
{
@ -67,33 +82,43 @@ private slots:
{
bool hires = tokens[1].contains("hires");
QString hiresURL = hires ? "@2x" : "";
QString xml;
QString xml, url;
if ((tokens[1] == "/street") || (tokens[1] == "/street-hires"))
{
if (m_overlay) {
url = QString("http://127.0.0.1:%1/street/%z/%x/%y.png").arg(m_tileServerPort);
} else {
url = "https://tile.openstreetmap.org/%z/%x/%y.png";
}
xml = QString("\
{\
\"UrlTemplate\" : \"https://tile.openstreetmap.org/%z/%x/%y.png\",\
\"ImageFormat\" : \"png\",\
\"QImageFormat\" : \"Indexed8\",\
\"ID\" : \"wmf-intl-1x\",\
\"MaximumZoomLevel\" : 19,\
\"MapCopyRight\" : \"<a href='http://www.openstreetmap.org/copyright'>OpenStreetMap</a>\",\
\"DataCopyRight\" : \"\"\
}");
{\
\"UrlTemplate\" : \"%1\",\
\"ImageFormat\" : \"png\",\
\"QImageFormat\" : \"Indexed8\",\
\"ID\" : \"wmf-intl-1x\",\
\"MaximumZoomLevel\" : 19,\
\"MapCopyRight\" : \"<a href='http://www.openstreetmap.org/copyright'>OpenStreetMap</a>\",\
\"DataCopyRight\" : \"\"\
}").arg(url);
}
else if (tokens[1] == "/satellite")
{
if (m_overlay) {
url = QString("http://127.0.0.1:%1/satellite/%z/%x/%y.jpg").arg(m_tileServerPort);
} else {
url = QString("https://api.maptiler.com/tiles/satellite-v2/%z/%x/%y%1.jpg?key=%2").arg(hiresURL).arg(m_maptilerAPIKey);
}
xml = QString("\
{\
\"Enabled\" : true,\
\"UrlTemplate\" : \"https://api.maptiler.com/tiles/satellite/%z/%x/%y%1.jpg?key=%2\",\
\"UrlTemplate\" : \"%1\",\
\"ImageFormat\" : \"jpg\",\
\"QImageFormat\" : \"RGB888\",\
\"ID\" : \"usgs-l7\",\
\"MaximumZoomLevel\" : 20,\
\"MaximumZoomLevel\" : 22,\
\"MapCopyRight\" : \"<a href='http://maptiler.com/'>Maptiler</a>\",\
\"DataCopyRight\" : \"\"\
}").arg(hiresURL).arg(m_maptilerAPIKey);
}").arg(url);
}
else if (tokens[1].contains("transit"))
{
@ -103,32 +128,46 @@ private slots:
// Use CartoDB maps without labels for aviation maps
int idx = map.indexOf(tokens[1]);
if (m_overlay) {
url = QString("http://127.0.0.1:%1/%2/%z/%x/%y.png").arg(m_tileServerPort).arg(mapUrl[idx]);
} else {
url = QString("http://1.basemaps.cartocdn.com/%2/%z/%x/%y.png%1").arg(hiresURL).arg(mapUrl[idx]);
}
xml = QString("\
{\
\"UrlTemplate\" : \"http://1.basemaps.cartocdn.com/%2/%z/%x/%y.png%1\",\
\"UrlTemplate\" : \"%1\",\
\"ImageFormat\" : \"png\",\
\"QImageFormat\" : \"Indexed8\",\
\"ID\" : \"%3\",\
\"ID\" : \"%2\",\
\"MaximumZoomLevel\" : 20,\
\"MapCopyRight\" : \"<a href='https://carto.com'>CartoDB</a>\",\
\"DataCopyRight\" : \"\"\
}").arg(hiresURL).arg(mapUrl[idx]).arg(mapId[idx]);
}").arg(url).arg(mapId[idx]);
}
else
{
QStringList map({"/cycle", "/cycle-hires", "/hiking", "/hiking-hires", "/night-transit", "/night-transit-hires", "/terrain", "/terrain-hires", "/transit", "/transit-hires"});
QStringList mapId({"thf-cycle", "thf-cycle-hires", "thf-hike", "thf-hike-hires", "thf-nighttransit", "thf-nighttransit-hires", "thf-landsc", "thf-landsc-hires", "thf-transit", "thf-transit-hires"});
QStringList mapUrl({"cycle", "cycle", "outdoors", "outdoors", "transport-dark", "transport-dark", "landscape", "landscape", "transport", "transport"});
int idx = map.indexOf(tokens[1]);
if (idx != -1)
{
if (m_overlay) {
url = QString("http://127.0.0.1:%1/%2/%z/%x/%y.png").arg(m_tileServerPort).arg(mapUrl[idx]);
} else {
url = QString("http://a.tile.thunderforest.com/%1/%z/%x/%y%3.png?apikey=%2").arg(mapUrl[idx]).arg(m_thunderforestAPIKey).arg(hiresURL);
}
xml = QString("\
{\
\"UrlTemplate\" : \"http://a.tile.thunderforest.com/%1/%z/%x/%y%4.png?apikey=%2\",\
\"UrlTemplate\" : \"%1\",\
\"ImageFormat\" : \"png\",\
\"QImageFormat\" : \"Indexed8\",\
\"ID\" : \"%3\",\
\"ID\" : \"%2\",\
\"MaximumZoomLevel\" : 20,\
\"MapCopyRight\" : \"<a href='http://www.thunderforest.com/'>Thunderforest</a>\",\
\"DataCopyRight\" : \"<a href='http://www.openstreetmap.org/copyright'>OpenStreetMap</a> contributors\"\
}").arg(mapUrl[idx]).arg(m_thunderforestAPIKey).arg(mapId[idx]).arg(hiresURL);
}").arg(url).arg(mapId[idx]);
}
}
QTextStream os(socket);

Wyświetl plik

@ -9,9 +9,9 @@ On top of this, it can plot data from other plugins, such as:
* Aircraft from the ADS-B Demodulator,
* Ships from the AIS Demodulator,
* Satellites from the Satellite Tracker,
* Weather imagery from APT Demodulator,
* Satellite imagery from APT Demodulator,
* The Sun, Moon and Stars from the Star Tracker,
* Weather balloons from the RadioSonde feature,
* Weather balloons from the Radiosonde feature,
* RF Heat Maps from the Heap Map channel,
* Radials and estimated position from the VOR localizer feature,
* ILS course line and glide path from the ILS Demodulator.
@ -25,16 +25,22 @@ As well as internet data sources:
* Radio time transmitters,
* GRAVES radar,
* Ionosonde station data,
* Navtex transmitters.
* VLF transmitters.
* Navtex transmitters,
* VLF transmitters,
* KiwiSDRs,
* Spy Servers,
* Weather radar,
* Satellite infra-red data (clouds),
* Sea marks,
* Satellite imagery from NASA GIBS (Global Imagery Browse Services).
It can also create tracks showing the path aircraft, ships and APRS objects have taken, as well as predicted paths for satellites.
It can also create tracks showing the path aircraft, ships, radiosondes and APRS objects have taken, as well as predicted paths for satellites.
![2D Map feature](../../../doc/img/Map_plugin_beacons.png)
![3D Map feature](../../../doc/img/Map_plugin_apt.png)
3D Models are not included with SDRangel. They must be downloaded by pressing the Download 3D Models button in the Display Settings dialog (13).
3D Models are not included with SDRangel. They must be downloaded by pressing the Download 3D Models button in the Display Settings dialog (20).
<h2>Interface</h2>
@ -90,45 +96,104 @@ When clicked, opens the Radio Time Transmitters dialog.
![Radio Time transmitters dialog](../../../doc/img/Map_plugin_radiotime_dialog.png)
<h3>7: Display MUF Contours</h3>
<h3>7: Display Satellite Infrared</h3>
When checked, satellite infrared measurements (10.3um) are downloaded from the internet and are overlaid on the maps.
This essentially shows cloud cover. The images are updated every 10 minutes.
![Satellite IR](../../../doc/img/Map_plugin_clouds.png)
The data is similar to that which can be received using the [APT Demodulator](../../channelrx/demodapt/readme.md) in the Thermal-infrared (10.3-11.3 um) channel.
This is only supported on 2D raster maps and the 3D map.
<h3>8: Display Weather Radar</h3>
When checked, weather radar measurements are downloaded from the internet and are overlaid on the maps.
This shows rain and other forms of precipitation.
The images are updated every 10 minutes.
Green, yellow and red are rain, with red being the most intense.
Light blue through dark blue is snow, with dark blue being the most intense.
![Weather Radar](../../../doc/img/Map_plugin_weather_radar.png)
This is only supported on 2D raster maps and the 3D map.
<h3>9: Display Sea Marks</h3>
When checked, sea marks are overlaid on the maps.
![Sea Marks](../../../doc/img/Map_plugin_seamarks.png)
![Sea Marks Legend](../../../doc/img/Map_plugin_seamarks_legend.png)
This is only supported on 2D raster maps and the 3D map.
<h3>10: Display Railways</h3>
When checked, railway routes are overlaid on the maps.
![Railways](../../../doc/img/Map_plugin_railways.png)
![Railway Legend](../../../doc/img/Map_plugin_railway_legend.png)
This is only supported on 2D raster maps and the 3D map.
<h3>11: Display MUF Contours</h3>
When checked, contours will be downloaded and displayed on the 3D map, showing the MUF (Maximum Usable Frequency) for a 3000km path that reflects off the ionosphere.
The contours will be updated every 15 minutes. The latest contour data will always be displayed, irrespective of the time set on the 3D Map.
<h3>8: Display coF2 Contours</h3>
![MUF contours](../../../doc/img/Map_plugin_muf.png)
<h3>12: Display coF2 Contours</h3>
When checked, contours will be downloaded and displayed on the 3D map, showing coF2 (F2 layer critical frequency), the maximum frequency at which radio waves will be reflected vertically from the F2 region of the ionosphere.
The contours will be updated every 15 minutes. The latest contour data will always be displayed, irrespective of the time set on the 3D Map.
<h3>8: Display Names</h3>
<h3>13: Display NASA GIBS Data</h3>
When checked, enables overlay of data from NASA GIBS (Global Imagery Browse Services). This includes a vast array of Earth observation satellite data,
such as land and sea temperatures, atmospheric conditions, flux measurements and the like.
Details of available data products can be found [here](https://nasa-gibs.github.io/gibs-api-docs/available-visualizations/#visualization-product-catalog).
For some data sets, GIBS has data spanning many decades. The data period may be hours, days or months. The 3D map will attemp to show data from the closest time set in the 3D map's timescale.
The 2D map will only show data from the default date (which is displayed in the table at the bottom).
![NASA GIBS](../../../doc/img/Map_plugin_GIBS.png)
This is only supported on 2D raster maps and the 3D map.
<h3>14: NASA GIBS Data</h3>
Selects which data from NASA GIBS to overlay on the maps.
<h3>15: NASA GIBS Opacity</h3>
Sets the opacity used for the NASA GIBS overlay image overlay on the 3D map. Lower values make the image more transparent.
<h3>16: Display Names</h3>
When checked, names of objects are displayed in a bubble next to each object.
<h3>9: Display tracks for selected object</h3>
<h3>17: Display tracks for selected object</h3>
When checked, displays the track (taken or predicted) for the selected object.
<h3>10: Display tracks for all objects</h3>
<h3>18: Display tracks for all objects</h3>
When checked, displays the track (taken or predicted) for the all objects.
<h3>11: Delete</h3>
<h3>19: Delete</h3>
When clicked, all items will be deleted from the map.
<h3>12: Display settings</h3>
<h3>20: Display settings</h3>
When clicked, opens the Map Display Settings dialog:
![Map Display Settings Dialog](../../../doc/img/Map_plugin_display_settings.png)
The top half of the dialog allows customization of how objects from different SDRangel
plugins are displayed on the 2D and 3D maps. This includes:
* Whether images are displayed on the 2D map and whether 3D models are displayed on the 2D map.
* Whether labels are displayed giving the name of the object.
* Whether taken and predicted tracks are displayed and in which colour.
* How the image or 3D model is scaled as the zoom level changes.
![Map Display Settings Dialog Maps Tab](../../../doc/img/Map_plugin_display_settings.png)
For the 2D map, the settings include:
@ -139,13 +204,28 @@ For the 2D map, the settings include:
For the 3D map, the settings include:
* Whether the 3D map is displayed.
* The terrain provider, which provides elevation data. For a "flat" globe, terrain can be set to Ellipsoid for the WGS-84 ellipsoid.
* The buildings provider, which provides 3D building models. This can be set to None if no buildings are desired.
* Whether the globe and models are lit from the direction of the Sun or the camera.
* The camera reference frame. For ECEF (Earth Centered Earth Fixed), the camera rotates with the globe.
For ECI (Earth Centred Inertial) the camera is fixed in space and the globe will rotate under it.
* API keys, required to access maps from different providers.
The "Download 3D Models" button will download the 3D models of aircraft, ships and satellites that are required for the 3D map.
These are not included with the SDRangel distribution, so must be downloaded. It is recommeded to restart SDRangel after downloading the models.
![Map Display Settings Dialog Items Tab](../../../doc/img/Map_plugin_display_settings_items.png)
The Map Items tab customization of how objects from different SDRangel plugins and the Internet are displayed on the 2D and 3D maps. This includes:
* Whether images are displayed on the 2D map and whether 3D models are displayed on the 3D map.
* Whether labels are displayed giving the name of the object.
* Whether taken and predicted tracks are displayed and in which colour.
* How the image or 3D model is scaled as the zoom level changes.
![Map Display Settings Dialog API Keys Tab](../../../doc/img/Map_plugin_display_settings_apikeys.png)
API keys are required to access maps from different providers.
Free API keys are available by signing up for an accounts with:
@ -156,9 +236,6 @@ Free API keys are available by signing up for an accounts with:
If API keys are not specified, a default key will be used, but this may not work if too many users use it.
The "Download 3D Models" button will download the 3D models of aircraft, ships and satellites that are required for the 3D map.
These are not included with the SDRangel distribution, so must be downloaded.
<h3>Map</h3>
The map feature displays a 2D and a 3D map overlaid with objects reported by other SDRangel channels and features, as well as beacon locations.
@ -177,6 +254,14 @@ The 2D map will only display the last reported positions for objects.
The 3D map, however, has a timeline that allows replaying how objects have moved over time.
To the right of the timeline is the fullscreen toggle button, which allows the 3D map to be displayed fullscreen.
<h4>SDRs</h4>
The map can display KiwiSDRs and Spy Servers that are publically accessible via the internet. A URL is displayed in the info box.
Clicking on the URL will open a new KiwiSDR or RemoteTCPInput device which will connect to the corresponding SDR.
Before connecting, you should check the whether the number of users is below the maximum. Server data is updated every 2 minutes.
![SDRs](../../../doc/img/Map_plugin_SDRs.png)
<h4>Ionosonde Stations</h4>
When Ionosonde Stations are displayed, data is downloaded and displayed every 2 minutes. The data includes:
@ -205,8 +290,17 @@ Mapbox: https://www.mapbox.com/ Cesium: https://www.cesium.com Bing: https://www
Ionosonde data and MUF/coF2 contours from [KC2G](https://prop.kc2g.com/) with source data from [GIRO](https://giro.uml.edu/) and [NOAA NCEI](https://www.ngdc.noaa.gov/stp/iono/ionohome.html).
Icons made by Google from Flaticon https://www.flaticon.com
World icons created by turkkub from Flaticon https://www.flaticon.com
Sea Marks are from OpenSeaMap: https://www.openseamap.org/
Railways are from OpenRailwayMap: https://www.openrailwaymap.org/
Weather radar and satellite data is from RainViewer: https://www.rainviewer
Icons made by Google from Flaticon: https://www.flaticon.com
World icons created by turkkub from Flaticon: https://www.flaticon.com
Layers and Boat icons created by Freepik from Flaticon: https://www.flaticon.com
Railway icons created by Prosymbols Premium from Flaticon: https://www.flaticon.com
Satellite icons created by SyafriStudio from Flaticon: https://www.flaticon.com
3D models are by various artists under a variety of licenses. See: https://github.com/srcejon/sdrangel-3d-models
@ -218,8 +312,8 @@ If you wish to contribute a 3D model, see the https://github.com/srcejon/sdrange
Full details of the API can be found in the Swagger documentation. Here is a quick example of how to centre the map on an object from the command line:
curl -X POST "http://127.0.0.1:8091/sdrangel/featureset/0/feature/0/actions" -d '{"featureType": "Map", "MapActions": { "find": "M7RCE" }}'
curl -X POST "http://127.0.0.1:8091/sdrangel/featureset/feature/0/actions" -d '{"featureType": "Map", "MapActions": { "find": "M7RCE" }}'
And to centre the map at a particular latitude and longitude:
curl -X POST "http://127.0.0.1:8091/sdrangel/featureset/0/feature/0/actions" -d '{"featureType": "Map", "MapActions": { "find": "51.2 0.0" }}'
curl -X POST "http://127.0.0.1:8091/sdrangel/featureset/feature/0/actions" -d '{"featureType": "Map", "MapActions": { "find": "51.2 0.0" }}'

Wyświetl plik

@ -97,7 +97,7 @@ void WebServer::addFile(const QString &path, const QByteArray &data)
void WebServer::sendFile(QTcpSocket* socket, const QByteArray &data, MimeType *mimeType, const QString &path)
{
QString header = QString("HTTP/1.0 200 Ok\r\nContent-Type: %1\r\n\r\n").arg(mimeType->m_type);
QString header = QString("HTTP/1.0 200 Ok\r\nContent-Type: %1\r\nAccess-Control-Allow-Headers: *\r\nAccess-Control-Allow-Methods: *\r\nAccess-Control-Allow-Origin: *\r\n\r\n").arg(mimeType->m_type);
if (mimeType->m_binary)
{
// Send file as binary
@ -125,7 +125,7 @@ void WebServer::readClient()
if (socket->canReadLine())
{
QString line = socket->readLine();
//qDebug() << "WebServer HTTP Request: " << line;
qDebug() << "WebServer HTTP Request: " << line;
QStringList tokens = QString(line).split(QRegularExpression("[ \r\n][ \r\n]*"));
if (tokens[0] == "GET")

Wyświetl plik

@ -80,12 +80,6 @@ target_link_libraries(${TARGET_NAME}
install(TARGETS ${TARGET_NAME} DESTINATION ${INSTALL_FOLDER})
if(WIN32)
# Run deployqt for QtQuick etc
include(DeployQt)
windeployqt(${TARGET_NAME} ${SDRANGEL_BINARY_BIN_DIR} ${PROJECT_SOURCE_DIR}/skymap)
endif()
# Install debug symbols
if (WIN32)
install(FILES $<TARGET_PDB_FILE:${TARGET_NAME}> CONFIGURATIONS Debug RelWithDebInfo DESTINATION ${INSTALL_FOLDER} )

Wyświetl plik

@ -97,7 +97,7 @@ void WebServer::addFile(const QString &path, const QByteArray &data)
void WebServer::sendFile(QTcpSocket* socket, const QByteArray &data, MimeType *mimeType, const QString &path)
{
QString header = QString("HTTP/1.0 200 Ok\r\nContent-Type: %1\r\nAccess-Control-Allow-Origin \"*\"\r\n\r\n").arg(mimeType->m_type);
QString header = QString("HTTP/1.0 200 Ok\r\nContent-Type: %1\r\nAccess-Control-Allow-Headers: *\r\nAccess-Control-Allow-Methods: *\r\nAccess-Control-Allow-Origin: *\r\n\r\n").arg(mimeType->m_type);
if (mimeType->m_binary)
{
// Send file as binary

Wyświetl plik

@ -21,11 +21,23 @@
#include <QNetworkReply>
#include <QXmlStreamReader>
#include <QDebug>
#include <QNetworkDiskCache>
WTML::WTML()
{
m_networkManager = new QNetworkAccessManager();
QObject::connect(m_networkManager, &QNetworkAccessManager::finished, this, &WTML::handleReply);
QStringList locations = QStandardPaths::standardLocations(QStandardPaths::AppDataLocation);
QDir writeableDir(locations[0]);
if (!writeableDir.mkpath(QStringLiteral("cache") + QDir::separator() + QStringLiteral("wtml"))) {
qDebug() << "Failed to create cache/wtml";
}
m_cache = new QNetworkDiskCache();
m_cache->setCacheDirectory(locations[0] + QDir::separator() + QStringLiteral("cache") + QDir::separator() + QStringLiteral("wtml"));
m_cache->setMaximumCacheSize(100000000);
m_networkManager->setCache(m_cache);
}
WTML::~WTML()

Wyświetl plik

@ -22,6 +22,7 @@
class QNetworkAccessManager;
class QNetworkReply;
class QNetworkDiskCache;
// World Wide Telescope WTML files containing imageset catalogs
class WTML : public QObject
@ -48,6 +49,7 @@ signals:
private:
QNetworkAccessManager *m_networkManager;
QNetworkDiskCache *m_cache;
};

Wyświetl plik

@ -14,6 +14,7 @@ set(remotetcpinput_HEADERS
remotetcpinputsettings.h
remotetcpinputwebapiadapter.h
remotetcpinputplugin.h
spyserver.h
)
include_directories(

Wyświetl plik

@ -2,7 +2,8 @@
<h2>Introduction</h2>
This input sample source plugin gets its I/Q samples over the network via a TCP/IP connection from a server such as rtl_tcp, rsp_tcp or SDRangel's [Remote TCP Channel Sink](../../channelrx/remotetcpsink/readme.md) plugin.
This input sample source plugin gets its I/Q samples over the network via a TCP/IP connection from a server such as
rtl_tcp, rsp_tcp, Spy Server or SDRangel's [Remote TCP Channel Sink](../../channelrx/remotetcpsink/readme.md) plugin.
<h2>Interface</h2>
@ -92,7 +93,7 @@ When unchecked, the channel sample rate can be set to any value.
Specifies number of bits per I/Q sample transmitted via TCP/IP.
When the protocol is RTL0, only 8-bits are supported. SDRA protocol supports 8, 16, 24 and 32-bit samples.
When the protocol is RTL0, only 8-bits are supported. SDRA and Spy Server protocol supports 8, 16, 24 and 32-bit samples.
<h3>19: Server IP address</h3>
@ -102,7 +103,11 @@ IP address or hostname of the server that is running SDRangel's Remote TCP Sink
TCP port on the server to connect to.
<h3>21: Connection settings</h3>
<h3>21: Protocol</h3>
Selects protocol to use. Set to SDRangel for rtl_tcp, rsp_tcp or SDRangel's own protocol. Alternative, Spy Server can be selected to connect to Spy Servers.
<h3>23: Connection settings</h3>
Determines which settings are used when connecting.
@ -110,31 +115,32 @@ When checked, settings in the RemoteTCPInput GUI are written to the remote devic
When unchecked, if the remote server is using the SDRA protocol, the RemoteTCPInput GUI will be updated with the current settings from the remote device.
If the remote server is using the RTL0 protocol, the GUI will not be updated, which may mean the two are inconsistent.
<h3>22: Pre-fill</h3>
<h3>24: Pre-fill</h3>
Determines how many seconds of I/Q samples are buffered locally from the remote device, before being processed in SDRangel.
More buffering can handle more network congestion and other network problems, without gaps in the output, but increases the latency in changes to remote device settings.
<h3>23: Input buffer gauge</h3>
<h3>25: Input buffer gauge</h3>
Shows how much data is in the input buffer. Typically this will be just under the pre-fill setting.
If it becomes empty, the plugin will pause outputting of data until the buffer is refilled to the pre-fill level.
If the buffer repeatedly runs empty, this suggests you do not have enough network bandwidth for the current combination
of channel sample rate and sample bit depth. Reducing these to lower values may be required for uninterrupted data.
<h3>24: Output buffer gauge</h3>
<h3>26: Output buffer gauge</h3>
Shows how much data is in the output buffer. This should typically be empty. If not empty, this suggests your CPU can't keep up with the amount of data being received.
<h3>25: Device status</h3>
<h3>27: Device status</h3>
Shows the type of remote device that has been connected to.
<h3>26: Protocol status</h3>
<h3>28: Protocol status</h3>
Shows the protocol being used by the remote server. This will be RTL0 or SDRA.
Shows the protocol being used by the remote server. This will be RTL0, SDRA or Spy Server.
rtl_tcp and rsp_tcp always use the RTL0 protocol.
SDRangel's Remote TCP Sink plugin can use RTL0 or SDRA.
RTL0 is limited to sending 8-bit data, doesn't support decimation and does not send the current device settings on connection.
Spy Server supports decimation and gain, but no other settings.

Wyświetl plik

@ -394,6 +394,9 @@ void RemoteTCPInput::webapiUpdateDeviceSettings(
if (deviceSettingsKeys.contains("preFill")) {
settings.m_preFill = response.getRemoteTcpInputSettings()->getPreFill() != 0;
}
if (deviceSettingsKeys.contains("protocol")) {
settings.m_protocol = *response.getRemoteTcpInputSettings()->getProtocol();
}
if (deviceSettingsKeys.contains("useReverseAPI")) {
settings.m_useReverseAPI = response.getRemoteTcpInputSettings()->getUseReverseApi() != 0;
}
@ -430,6 +433,7 @@ void RemoteTCPInput::webapiFormatDeviceSettings(SWGSDRangel::SWGDeviceSettings&
response.getRemoteTcpInputSettings()->setDataPort(settings.m_dataPort);
response.getRemoteTcpInputSettings()->setOverrideRemoteSettings(settings.m_overrideRemoteSettings ? 1 : 0);
response.getRemoteTcpInputSettings()->setPreFill(settings.m_preFill ? 1 : 0);
response.getRemoteTcpInputSettings()->setProtocol(new QString(settings.m_protocol));
response.getRemoteTcpInputSettings()->setUseReverseApi(settings.m_useReverseAPI ? 1 : 0);

Wyświetl plik

@ -1,5 +1,5 @@
///////////////////////////////////////////////////////////////////////////////////
// Copyright (C) 2022-2023 Jon Beniston, M7RCE <jon@beniston.com> //
// Copyright (C) 2022-2024 Jon Beniston, M7RCE <jon@beniston.com> //
// Copyright (C) 2022 Edouard Griffiths, F4EXB <f4exb06@gmail.com> //
// //
// This program is free software; you can redistribute it and/or modify //
@ -49,7 +49,9 @@ RemoteTCPInputGui::RemoteTCPInputGui(DeviceUISet *deviceUISet, QWidget* parent)
m_forceSettings(true),
m_deviceGains(nullptr),
m_remoteDevice(RemoteTCPProtocol::RTLSDR_R820T),
m_connectionError(false)
m_connectionError(false),
m_spyServerGainRange("Gain", 0, 41, 1, ""),
m_spyServerGains({m_spyServerGainRange}, false, false)
{
m_deviceUISet = deviceUISet;
setAttribute(Qt::WA_DeleteOnClose, true);
@ -223,23 +225,27 @@ bool RemoteTCPInputGui::handleMessage(const Message& message)
device = devices.value(m_remoteDevice);
}
ui->device->setText(QString("Device: %1").arg(device));
ui->protocol->setText(QString("Protocol: %1").arg(report.getProtocol()));
ui->detectedProtocol->setText(QString("Protocol: %1").arg(report.getProtocol()));
// Update GUI so we only show widgets available for the protocol in use
bool sdra = report.getProtocol() == "SDRA";
if (sdra && (ui->sampleBits->count() != 4))
bool spyServer = report.getProtocol() == "Spy Server";
if (spyServer) {
m_spyServerGains.m_gains[0].m_max = report.getMaxGain();
}
if ((sdra || spyServer) && (ui->sampleBits->count() < 4))
{
ui->sampleBits->addItem("16");
ui->sampleBits->addItem("24");
ui->sampleBits->addItem("32");
}
else if (!sdra && (ui->sampleBits->count() != 1))
else if (!(sdra || spyServer) && (ui->sampleBits->count() != 1))
{
while (ui->sampleBits->count() > 1) {
ui->sampleBits->removeItem(ui->sampleBits->count() - 1);
}
}
if (sdra && (ui->decim->count() != 7))
if ((sdra || spyServer) && (ui->decim->count() != 7))
{
ui->decim->addItem("2");
ui->decim->addItem("4");
@ -248,19 +254,24 @@ bool RemoteTCPInputGui::handleMessage(const Message& message)
ui->decim->addItem("32");
ui->decim->addItem("64");
}
else if (!sdra && (ui->decim->count() != 1))
else if (!(sdra || spyServer) && (ui->decim->count() != 1))
{
while (ui->decim->count() > 1) {
ui->decim->removeItem(ui->decim->count() - 1);
}
}
if (!sdra) {
if (!sdra)
{
ui->deltaFrequency->setValue(0);
ui->channelGain->setValue(0);
ui->decimation->setChecked(true);
}
ui->deltaFrequencyLabel->setEnabled(sdra);
ui->deltaFrequency->setEnabled(sdra);
ui->deltaUnits->setEnabled(sdra);
ui->channelGainLabel->setEnabled(sdra);
ui->channelGain->setEnabled(sdra);
ui->channelGainText->setEnabled(sdra);
ui->decimation->setEnabled(sdra);
if (sdra) {
ui->centerFrequency->setValueRange(9, 0, 999999999); // Should add transverter control to protocol in the future
@ -271,7 +282,7 @@ bool RemoteTCPInputGui::handleMessage(const Message& message)
// Set sample rate range
if (m_sampleRateRanges.contains(m_remoteDevice))
{
const SampleRateRange *range = m_sampleRateRanges.value(m_remoteDevice);
const SampleRateRange *range = m_sampleRateRanges.value(m_remoteDevice);
ui->devSampleRate->setValueRange(8, range->m_min, range->m_max);
}
else if (m_sampleRateLists.contains(m_remoteDevice))
@ -284,6 +295,18 @@ bool RemoteTCPInputGui::handleMessage(const Message& message)
{
ui->devSampleRate->setValueRange(8, 0, 99999999);
}
ui->devSampleRateLabel->setEnabled(!spyServer);
ui->devSampleRate->setEnabled(!spyServer);
ui->devSampleRateUnits->setEnabled(!spyServer);
ui->agc->setEnabled(!spyServer);
ui->rfBWLabel->setEnabled(!spyServer);
ui->rfBW->setEnabled(!spyServer);
ui->rfBWUnits->setEnabled(!spyServer);
ui->dcOffset->setEnabled(!spyServer);
ui->iqImbalance->setEnabled(!spyServer);
ui->ppm->setEnabled(!spyServer);
ui->ppmLabel->setEnabled(!spyServer);
ui->ppmText->setEnabled(!spyServer);
displayGains();
return true;
@ -368,7 +391,9 @@ void RemoteTCPInputGui::displaySettings()
ui->deviceRateText->setText(tr("%1k").arg(m_settings.m_channelSampleRate / 1000.0));
ui->decimation->setChecked(!m_settings.m_channelDecimation);
ui->channelSampleRate->setEnabled(m_settings.m_channelDecimation);
ui->sampleBits->setCurrentIndex(m_settings.m_sampleBits/8-1);
ui->channelSampleRateLabel->setEnabled(m_settings.m_channelDecimation);
ui->channelSampleRateUnit->setEnabled(m_settings.m_channelDecimation);
ui->sampleBits->setCurrentText(QString::number(m_settings.m_sampleBits));
ui->dataPort->setText(tr("%1").arg(m_settings.m_dataPort));
ui->dataAddress->blockSignals(true);
@ -385,6 +410,10 @@ void RemoteTCPInputGui::displaySettings()
ui->preFill->setValue((int)(m_settings.m_preFill * 10.0));
ui->preFillText->setText(QString("%1s").arg(m_settings.m_preFill, 0, 'f', 2));
int idx = ui->protocol->findText(m_settings.m_protocol);
if (idx > 0) {
ui->protocol->setCurrentIndex(idx);
}
displayGains();
blockApplySettings(false);
@ -526,7 +555,11 @@ void RemoteTCPInputGui::displayGains()
QLabel *gainTexts[3] = {ui->gain1Text, ui->gain2Text, ui->gain3Text};
QWidget *gainLine[2] = {ui->gainLine1, ui->gainLine2};
m_deviceGains = m_gains.value(m_remoteDevice);
if (m_settings.m_protocol == "Spy Server") {
m_deviceGains = &m_spyServerGains;
} else {
m_deviceGains = m_gains.value(m_remoteDevice);
}
if (m_deviceGains)
{
ui->agc->setVisible(m_deviceGains->m_agc);
@ -746,12 +779,16 @@ void RemoteTCPInputGui::on_decimation_toggled(bool checked)
ui->channelSampleRate->setValue(m_settings.m_channelSampleRate);
}
ui->channelSampleRate->setEnabled(!checked);
ui->channelSampleRateLabel->setEnabled(!checked);
ui->channelSampleRateUnit->setEnabled(!checked);
sendSettings();
}
void RemoteTCPInputGui::on_sampleBits_currentIndexChanged(int index)
{
m_settings.m_sampleBits = 8 * (index + 1);
(void) index;
m_settings.m_sampleBits = ui->sampleBits->currentText().toInt();
m_settingsKeys.append("sampleBits");
sendSettings();
}
@ -808,6 +845,16 @@ void RemoteTCPInputGui::on_preFill_valueChanged(int value)
sendSettings();
}
void RemoteTCPInputGui::on_protocol_currentIndexChanged(int index)
{
(void) index;
m_settings.m_protocol = ui->protocol->currentText();
m_settingsKeys.append("protocol");
sendSettings();
displayGains();
}
void RemoteTCPInputGui::updateHardware()
{
if (m_doApplySettings)
@ -902,4 +949,5 @@ void RemoteTCPInputGui::makeUIConnections()
QObject::connect(ui->dataPort, &QLineEdit::editingFinished, this, &RemoteTCPInputGui::on_dataPort_editingFinished);
QObject::connect(ui->overrideRemoteSettings, &ButtonSwitch::toggled, this, &RemoteTCPInputGui::on_overrideRemoteSettings_toggled);
QObject::connect(ui->preFill, &QDial::valueChanged, this, &RemoteTCPInputGui::on_preFill_valueChanged);
QObject::connect(ui->protocol, QOverload<int>::of(&QComboBox::currentIndexChanged), this, &RemoteTCPInputGui::on_protocol_currentIndexChanged);
}

Wyświetl plik

@ -1,5 +1,5 @@
///////////////////////////////////////////////////////////////////////////////////
// Copyright (C) 2022-2023 Jon Beniston, M7RCE <jon@beniston.com> //
// Copyright (C) 2022-2024 Jon Beniston, M7RCE <jon@beniston.com> //
// Copyright (C) 2022 Edouard Griffiths, F4EXB <f4exb06@gmail.com> //
// //
// This program is free software; you can redistribute it and/or modify //
@ -122,6 +122,9 @@ private:
RemoteTCPProtocol::Device m_remoteDevice; // Remote device reported when connecting
bool m_connectionError;
DeviceGains::GainRange m_spyServerGainRange;
DeviceGains m_spyServerGains;
static const DeviceGains::GainRange m_rtlSDR34kGainRange;
static const DeviceGains m_rtlSDRe4kGains;
static const DeviceGains::GainRange m_rtlSDRR820GainRange;
@ -208,6 +211,7 @@ private slots:
void on_dataPort_editingFinished();
void on_overrideRemoteSettings_toggled(bool checked);
void on_preFill_valueChanged(int value);
void on_protocol_currentIndexChanged(int index);
void updateHardware();
void updateStatus();
void openDeviceSettingsDialog(const QPoint& p);

Wyświetl plik

@ -742,7 +742,7 @@ Use to ensure full dynamic range of 8-bit data is used.</string>
<item>
<layout class="QHBoxLayout" name="rfBWLayout">
<item>
<widget class="QLabel" name="sampleRateLabel_2">
<widget class="QLabel" name="channelSampleRateLabel">
<property name="text">
<string>Ch SR</string>
</property>
@ -777,7 +777,7 @@ Use to ensure full dynamic range of 8-bit data is used.</string>
</widget>
</item>
<item>
<widget class="QLabel" name="sampleRateUnit">
<widget class="QLabel" name="channelSampleRateUnit">
<property name="text">
<string>S/s</string>
</property>
@ -957,6 +957,29 @@ Use to ensure full dynamic range of 8-bit data is used.</string>
</property>
</spacer>
</item>
<item>
<widget class="QComboBox" name="protocol">
<property name="minimumSize">
<size>
<width>75</width>
<height>0</height>
</size>
</property>
<property name="toolTip">
<string>Protocol to use. SDRangel (inc. rtl_tcp) or Spy Server</string>
</property>
<item>
<property name="text">
<string>SDRangel</string>
</property>
</item>
<item>
<property name="text">
<string>Spy Server</string>
</property>
</item>
</widget>
</item>
<item>
<widget class="ButtonSwitch" name="overrideRemoteSettings">
<property name="toolTip">
@ -1163,7 +1186,7 @@ This should typically be empty. If full, your CPU cannot keep up and data will b
</widget>
</item>
<item>
<widget class="QLabel" name="protocol">
<widget class="QLabel" name="detectedProtocol">
<property name="text">
<string/>
</property>
@ -1174,6 +1197,11 @@ This should typically be empty. If full, your CPU cannot keep up and data will b
</layout>
</widget>
<customwidgets>
<customwidget>
<class>ButtonSwitch</class>
<extends>QToolButton</extends>
<header>gui/buttonswitch.h</header>
</customwidget>
<customwidget>
<class>ValueDialZ</class>
<extends>QWidget</extends>
@ -1186,11 +1214,6 @@ This should typically be empty. If full, your CPU cannot keep up and data will b
<header>gui/valuedial.h</header>
<container>1</container>
</customwidget>
<customwidget>
<class>ButtonSwitch</class>
<extends>QToolButton</extends>
<header>gui/buttonswitch.h</header>
</customwidget>
</customwidgets>
<tabstops>
<tabstop>startStop</tabstop>

Wyświetl plik

@ -1,5 +1,5 @@
///////////////////////////////////////////////////////////////////////////////////
// Copyright (C) 2022-2023 Jon Beniston, M7RCE <jon@beniston.com> //
// Copyright (C) 2022-2024 Jon Beniston, M7RCE <jon@beniston.com> //
// Copyright (C) 2022 Edouard Griffiths, F4EXB <f4exb06@gmail.com> //
// //
// This program is free software; you can redistribute it and/or modify //
@ -48,6 +48,7 @@ void RemoteTCPInputSettings::resetToDefaults()
m_dataPort = 1234;
m_overrideRemoteSettings = true;
m_preFill = 1.0f;
m_protocol = "SDRangel";
m_useReverseAPI = false;
m_reverseAPIAddress = "127.0.0.1";
m_reverseAPIPort = 8888;
@ -81,6 +82,7 @@ QByteArray RemoteTCPInputSettings::serialize() const
s.writeU32(22, m_reverseAPIPort);
s.writeU32(23, m_reverseAPIDeviceIndex);
s.writeList(24, m_addressList);
s.writeString(25, m_protocol);
for (int i = 0; i < m_maxGains; i++) {
s.writeS32(30+i, m_gain[i]);
@ -136,6 +138,7 @@ bool RemoteTCPInputSettings::deserialize(const QByteArray& data)
m_reverseAPIDeviceIndex = uintval > 99 ? 99 : uintval;
d.readList(24, &m_addressList);
d.readString(25, &m_protocol, "SDRangel");
for (int i = 0; i < m_maxGains; i++) {
d.readS32(30+i, &m_gain[i], 0);
@ -224,6 +227,9 @@ void RemoteTCPInputSettings::applySettings(const QStringList& settingsKeys, cons
if (settingsKeys.contains("addressList")) {
m_addressList = settings.m_addressList;
}
if (settingsKeys.contains("protocol")) {
m_protocol = settings.m_protocol;
}
for (int i = 0; i < m_maxGains; i++)
{
@ -309,6 +315,9 @@ QString RemoteTCPInputSettings::getDebugString(const QStringList& settingsKeys,
if (settingsKeys.contains("addressList") || force) {
ostr << " m_addressList: " << m_addressList.join(",").toStdString();
}
if (settingsKeys.contains("protocol") || force) {
ostr << " m_protocol: " << m_protocol.toStdString();
}
for (int i = 0; i < m_maxGains; i++)
{

Wyświetl plik

@ -2,7 +2,7 @@
// Copyright (C) 2012 maintech GmbH, Otto-Hahn-Str. 15, 97204 Hoechberg, Germany //
// written by Christian Daniel //
// Copyright (C) 2015-2020, 2022 Edouard Griffiths, F4EXB <f4exb06@gmail.com> //
// Copyright (C) 2020, 2022-2023 Jon Beniston, M7RCE <jon@beniston.com> //
// Copyright (C) 2020, 2022-2024 Jon Beniston, M7RCE <jon@beniston.com> //
// //
// This program is free software; you can redistribute it and/or modify //
// it under the terms of the GNU General Public License as published by //
@ -53,6 +53,7 @@ struct RemoteTCPInputSettings
uint16_t m_reverseAPIPort;
uint16_t m_reverseAPIDeviceIndex;
QStringList m_addressList; // List of dataAddresses that have been used in the past
QString m_protocol; // "SDRangel" or "Spy Server"
RemoteTCPInputSettings();
void resetToDefaults();

Wyświetl plik

@ -1,5 +1,5 @@
///////////////////////////////////////////////////////////////////////////////////
// Copyright (C) 2022-2023 Jon Beniston, M7RCE <jon@beniston.com> //
// Copyright (C) 2022-2024 Jon Beniston, M7RCE <jon@beniston.com> //
// Copyright (C) 2022 Edouard Griffiths, F4EXB <f4exb06@gmail.com> //
// Copyright (C) 2022 Jiří Pinkava <jiri.pinkava@rossum.ai> //
// //
@ -49,7 +49,7 @@ RemoteTCPInputTCPHandler::RemoteTCPInputTCPHandler(SampleSinkFifo *sampleFifo, D
{
m_sampleFifo->setSize(5000000); // Start with large FIFO, to avoid having to resize
m_tcpBuf = new char[m_sampleFifo->size()*2*4];
m_timer.setInterval(125);
m_timer.setInterval(50); // Previously 125, but this results in an obviously slow spectrum refresh rate
connect(&m_reconnectTimer, SIGNAL(timeout()), this, SLOT(reconnect()));
m_reconnectTimer.setSingleShot(true);
}
@ -165,11 +165,21 @@ void RemoteTCPInputTCPHandler::cleanup()
// E.g. sample rate or bit depth
void RemoteTCPInputTCPHandler::clearBuffer()
{
if (m_dataSocket)
if (m_dataSocket && m_readMetaData)
{
m_dataSocket->flush();
m_dataSocket->readAll();
m_fillBuffer = true;
if (m_spyServer)
{
// Can't just flush buffer, otherwise we'll lose header sync
// Read and throw away any available data
processSpyServerData(m_dataSocket->bytesAvailable(), true);
m_fillBuffer = true;
}
else
{
m_dataSocket->flush();
m_dataSocket->readAll();
m_fillBuffer = true;
}
}
}
@ -389,6 +399,60 @@ void RemoteTCPInputTCPHandler::setSampleBitDepth(int sampleBits)
}
}
void RemoteTCPInputTCPHandler::spyServerConnect()
{
QMutexLocker mutexLocker(&m_mutex);
quint8 request[8+4+9];
SpyServerProtocol::encodeUInt32(&request[0], 0);
SpyServerProtocol::encodeUInt32(&request[4], 4+9);
SpyServerProtocol::encodeUInt32(&request[8], SpyServerProtocol::ProtocolID);
memcpy(&request[8+4], "SDRangel", 9);
if (m_dataSocket) {
m_dataSocket->write((char*)request, sizeof(request));
}
}
void RemoteTCPInputTCPHandler::spyServerSet(int setting, int value)
{
QMutexLocker mutexLocker(&m_mutex);
quint8 request[8+8];
SpyServerProtocol::encodeUInt32(&request[0], 2);
SpyServerProtocol::encodeUInt32(&request[4], 8);
SpyServerProtocol::encodeUInt32(&request[8], setting);
SpyServerProtocol::encodeUInt32(&request[12], value);
if (m_dataSocket) {
m_dataSocket->write((char*)request, sizeof(request));
}
}
void RemoteTCPInputTCPHandler::spyServerSetIQFormat(int sampleBits)
{
quint32 format;
if (sampleBits == 8) {
format = 1;
} else if (sampleBits == 16) {
format = 2;
} else if (sampleBits == 24) {
format = 3;
} else if (sampleBits == 32) {
format = 4; // This is float
} else {
qDebug() << "RemoteTCPInputTCPHandler::spyServerSetIQFormat: Unsupported value" << sampleBits;
format = 1;
}
spyServerSet(SpyServerProtocol::setIQFormat, format);
}
void RemoteTCPInputTCPHandler::spyServerSetStreamIQ()
{
spyServerSetIQFormat(m_settings.m_sampleBits);
spyServerSet(SpyServerProtocol::setStreamingMode, 1); // Stream IQ only
spyServerSet(SpyServerProtocol::setStreamingEnabled, 1); // Enable streaming
}
void RemoteTCPInputTCPHandler::applySettings(const RemoteTCPInputSettings& settings, const QList<QString>& settingsKeys, bool force)
{
qDebug() << "RemoteTCPInputTCPHandler::applySettings: "
@ -396,86 +460,124 @@ void RemoteTCPInputTCPHandler::applySettings(const RemoteTCPInputSettings& setti
<< settings.getDebugString(settingsKeys, force);
QMutexLocker mutexLocker(&m_mutex);
if (settingsKeys.contains("centerFrequency") || force) {
setCenterFrequency(settings.m_centerFrequency);
}
if (settingsKeys.contains("loPpmCorrection") || force) {
setFreqCorrection(settings.m_loPpmCorrection);
}
if (settingsKeys.contains("dcBlock") || force) {
if (m_sdra) {
setDCOffsetRemoval(settings.m_dcBlock);
}
}
if (settingsKeys.contains("iqCorrection") || force) {
if (m_sdra) {
setIQCorrection(settings.m_iqCorrection);
}
}
if (settingsKeys.contains("biasTee") || force) {
setBiasTee(settings.m_biasTee);
}
if (settingsKeys.contains("directSampling") || force) {
setDirectSampling(settings.m_directSampling);
}
if (settingsKeys.contains("log2Decim") || force) {
if (m_sdra) {
setDecimation(settings.m_log2Decim);
}
}
if (settingsKeys.contains("devSampleRate") || force) {
setSampleRate(settings.m_devSampleRate);
}
if (settingsKeys.contains("agc") || force) {
setAGC(settings.m_agc);
}
if (force) {
setTunerAGC(1); // The SDRangel RTLSDR driver always has tuner gain as manual
}
if (settingsKeys.contains("gain[0]") || force) {
setTunerGain(settings.m_gain[0]);
}
for (int i = 1; i < 3; i++)
if (m_spyServer)
{
if (settingsKeys.contains(QString("gain[%1]").arg(i)) || force) {
setIFGain(i, settings.m_gain[i]);
if (settingsKeys.contains("centerFrequency") || force) {
spyServerSet(SpyServerProtocol::setCenterFrequency, settings.m_centerFrequency);
}
}
if (settingsKeys.contains("rfBW") || force) {
setBandwidth(settings.m_rfBW);
}
if (settingsKeys.contains("inputFrequencyOffset") || force) {
if (m_sdra) {
setChannelFreqOffset(settings.m_inputFrequencyOffset);
}
}
if (settingsKeys.contains("channelGain") || force) {
if (m_sdra) {
setChannelGain(settings.m_channelGain);
}
}
if ((settings.m_channelSampleRate != m_settings.m_channelSampleRate) || force)
{
// Resize FIFO to give us 1 second
if ((settingsKeys.contains("channelSampleRate") || force) && (settings.m_channelSampleRate > (qint32)m_sampleFifo->size()))
if ((settings.m_channelSampleRate != m_settings.m_channelSampleRate) || force)
{
qDebug() << "RemoteTCPInputTCPHandler::applySettings: Resizing sample FIFO from " << m_sampleFifo->size() << "to" << settings.m_channelSampleRate;
m_sampleFifo->setSize(settings.m_channelSampleRate);
delete[] m_tcpBuf;
m_tcpBuf = new char[m_sampleFifo->size()*2*4];
m_fillBuffer = true; // So we reprime FIFO
// Resize FIFO to give us 1 second
if ((settingsKeys.contains("channelSampleRate") || force) && (settings.m_channelSampleRate > (qint32)m_sampleFifo->size()))
{
qDebug() << "RemoteTCPInputTCPHandler::applySettings: Resizing sample FIFO from " << m_sampleFifo->size() << "to" << settings.m_channelSampleRate;
m_sampleFifo->setSize(settings.m_channelSampleRate);
delete[] m_tcpBuf;
m_tcpBuf = new char[m_sampleFifo->size()*2*4];
m_fillBuffer = true; // So we reprime FIFO
}
// Protocol only seems to allow changing decimation
//spyServerSet(SpyServerProtocol::???, settings.m_channelSampleRate);
clearBuffer();
}
if (m_sdra) {
setChannelSampleRate(settings.m_channelSampleRate);
if (settingsKeys.contains("sampleBits") || force)
{
spyServerSetIQFormat(settings.m_sampleBits);
clearBuffer();
}
if (settingsKeys.contains("log2Decim") || force)
{
spyServerSet(SpyServerProtocol::setIQDecimation, settings.m_log2Decim);
clearBuffer();
}
if (settingsKeys.contains("gain[0]") || force)
{
spyServerSet(SpyServerProtocol::setGain, settings.m_gain[0] / 10); // Convert 10ths dB to index
}
clearBuffer();
}
if (settingsKeys.contains("sampleBits") || force)
else
{
if (m_sdra) {
setSampleBitDepth(settings.m_sampleBits);
if (settingsKeys.contains("centerFrequency") || force) {
setCenterFrequency(settings.m_centerFrequency);
}
if (settingsKeys.contains("loPpmCorrection") || force) {
setFreqCorrection(settings.m_loPpmCorrection);
}
if (settingsKeys.contains("dcBlock") || force) {
if (m_sdra) {
setDCOffsetRemoval(settings.m_dcBlock);
}
}
if (settingsKeys.contains("iqCorrection") || force) {
if (m_sdra) {
setIQCorrection(settings.m_iqCorrection);
}
}
if (settingsKeys.contains("biasTee") || force) {
setBiasTee(settings.m_biasTee);
}
if (settingsKeys.contains("directSampling") || force) {
setDirectSampling(settings.m_directSampling);
}
if (settingsKeys.contains("log2Decim") || force) {
if (m_sdra) {
setDecimation(settings.m_log2Decim);
}
}
if (settingsKeys.contains("devSampleRate") || force) {
setSampleRate(settings.m_devSampleRate);
}
if (settingsKeys.contains("agc") || force) {
setAGC(settings.m_agc);
}
if (force) {
setTunerAGC(1); // The SDRangel RTLSDR driver always has tuner gain as manual
}
if (settingsKeys.contains("gain[0]") || force) {
setTunerGain(settings.m_gain[0]);
}
for (int i = 1; i < 3; i++)
{
if (settingsKeys.contains(QString("gain[%1]").arg(i)) || force) {
setIFGain(i, settings.m_gain[i]);
}
}
if (settingsKeys.contains("rfBW") || force) {
setBandwidth(settings.m_rfBW);
}
if (settingsKeys.contains("inputFrequencyOffset") || force) {
if (m_sdra) {
setChannelFreqOffset(settings.m_inputFrequencyOffset);
}
}
if (settingsKeys.contains("channelGain") || force) {
if (m_sdra) {
setChannelGain(settings.m_channelGain);
}
}
if ((settings.m_channelSampleRate != m_settings.m_channelSampleRate) || force)
{
// Resize FIFO to give us 1 second
if ((settingsKeys.contains("channelSampleRate") || force) && (settings.m_channelSampleRate > (qint32)m_sampleFifo->size()))
{
qDebug() << "RemoteTCPInputTCPHandler::applySettings: Resizing sample FIFO from " << m_sampleFifo->size() << "to" << settings.m_channelSampleRate;
m_sampleFifo->setSize(settings.m_channelSampleRate);
delete[] m_tcpBuf;
m_tcpBuf = new char[m_sampleFifo->size()*2*4];
m_fillBuffer = true; // So we reprime FIFO
}
if (m_sdra) {
setChannelSampleRate(settings.m_channelSampleRate);
}
clearBuffer();
}
if (settingsKeys.contains("sampleBits") || force)
{
if (m_sdra) {
setSampleBitDepth(settings.m_sampleBits);
}
clearBuffer();
}
clearBuffer();
}
// Don't use force, as disconnect can cause rtl_tcp to quit
@ -501,6 +603,12 @@ void RemoteTCPInputTCPHandler::connected()
MsgReportConnection *msg = MsgReportConnection::create(true);
m_messageQueueToGUI->push(msg);
}
m_spyServer = m_settings.m_protocol == "Spy Server";
m_state = HEADER;
m_sdra = false;
if (m_spyServer) {
spyServerConnect();
}
}
void RemoteTCPInputTCPHandler::reconnect()
@ -542,116 +650,366 @@ void RemoteTCPInputTCPHandler::dataReadyRead()
{
QMutexLocker mutexLocker(&m_mutex);
if (!m_readMetaData)
if (!m_readMetaData && !m_spyServer)
{
quint8 metaData[RemoteTCPProtocol::m_sdraMetaDataSize];
if (m_dataSocket->bytesAvailable() >= (qint64)sizeof(metaData))
processMetaData();
}
else if (!m_readMetaData && m_spyServer)
{
processSpyServerMetaData();
}
}
void RemoteTCPInputTCPHandler::processMetaData()
{
quint8 metaData[RemoteTCPProtocol::m_sdraMetaDataSize];
if (m_dataSocket->bytesAvailable() >= (qint64)sizeof(metaData))
{
qint64 bytesRead = m_dataSocket->read((char *)&metaData[0], 4);
if (bytesRead == 4)
{
qint64 bytesRead = m_dataSocket->read((char *)&metaData[0], 4);
if (bytesRead == 4)
// Read first 4 bytes which indicate which protocol is in use
// RTL0 or SDRA
char protochars[5];
memcpy(protochars, metaData, 4);
protochars[4] = '\0';
QString protocol(protochars);
if (protocol == "RTL0")
{
// Read first 4 bytes which indicate which protocol is in use
// RTL0 or SDRA
char protochars[5];
memcpy(protochars, metaData, 4);
protochars[4] = '\0';
QString protocol(protochars);
m_sdra = false;
m_spyServer = false;
bytesRead = m_dataSocket->read((char *)&metaData[4], RemoteTCPProtocol::m_rtl0MetaDataSize-4);
if (protocol == "RTL0")
m_device = (RemoteTCPProtocol::Device)RemoteTCPProtocol::extractUInt32(&metaData[4]);
if (m_messageQueueToGUI) {
m_messageQueueToGUI->push(MsgReportRemoteDevice::create(m_device, protocol));
}
if (m_settings.m_sampleBits != 8)
{
m_sdra = false;
bytesRead = m_dataSocket->read((char *)&metaData[4], RemoteTCPProtocol::m_rtl0MetaDataSize-4);
RemoteTCPProtocol::Device tuner = (RemoteTCPProtocol::Device)RemoteTCPProtocol::extractUInt32(&metaData[4]);
if (m_messageQueueToGUI) {
m_messageQueueToGUI->push(MsgReportRemoteDevice::create(tuner, protocol));
RemoteTCPInputSettings& settings = m_settings;
settings.m_sampleBits = 8;
QList<QString> settingsKeys{"sampleBits"};
if (m_messageQueueToInput) {
m_messageQueueToInput->push(RemoteTCPInput::MsgConfigureRemoteTCPInput::create(settings, settingsKeys));
}
if (m_settings.m_sampleBits != 8)
{
RemoteTCPInputSettings& settings = m_settings;
settings.m_sampleBits = 8;
QList<QString> settingsKeys{"sampleBits"};
if (m_messageQueueToInput) {
m_messageQueueToInput->push(RemoteTCPInput::MsgConfigureRemoteTCPInput::create(settings, settingsKeys));
}
if (m_messageQueueToGUI) {
m_messageQueueToGUI->push(RemoteTCPInput::MsgConfigureRemoteTCPInput::create(settings, settingsKeys));
}
if (m_messageQueueToGUI) {
m_messageQueueToGUI->push(RemoteTCPInput::MsgConfigureRemoteTCPInput::create(settings, settingsKeys));
}
}
else if (protocol == "SDRA")
{
m_sdra = true;
bytesRead = m_dataSocket->read((char *)&metaData[4], RemoteTCPProtocol::m_sdraMetaDataSize-4);
}
else if (protocol == "SDRA")
{
m_sdra = true;
m_spyServer = false;
bytesRead = m_dataSocket->read((char *)&metaData[4], RemoteTCPProtocol::m_sdraMetaDataSize-4);
RemoteTCPProtocol::Device device = (RemoteTCPProtocol::Device)RemoteTCPProtocol::extractUInt32(&metaData[4]);
if (m_messageQueueToGUI) {
m_messageQueueToGUI->push(MsgReportRemoteDevice::create(device, protocol));
}
if (!m_settings.m_overrideRemoteSettings)
m_device = (RemoteTCPProtocol::Device)RemoteTCPProtocol::extractUInt32(&metaData[4]);
if (m_messageQueueToGUI) {
m_messageQueueToGUI->push(MsgReportRemoteDevice::create(m_device, protocol));
}
if (!m_settings.m_overrideRemoteSettings)
{
// Update local settings to match remote
RemoteTCPInputSettings& settings = m_settings;
QList<QString> settingsKeys;
settings.m_centerFrequency = RemoteTCPProtocol::extractUInt64(&metaData[8]);
settingsKeys.append("centerFrequency");
settings.m_loPpmCorrection = RemoteTCPProtocol::extractUInt32(&metaData[16]);
settingsKeys.append("loPpmCorrection");
quint32 flags = RemoteTCPProtocol::extractUInt32(&metaData[20]);
settings.m_biasTee = flags & 1;
settingsKeys.append("biasTee");
settings.m_directSampling = (flags >> 1) & 1;
settingsKeys.append("directSampling");
settings.m_agc = (flags >> 2) & 1;
settingsKeys.append("agc");
settings.m_dcBlock = (flags >> 3) & 1;
settingsKeys.append("dcBlock");
settings.m_iqCorrection = (flags >> 4) & 1;
settingsKeys.append("iqCorrection");
settings.m_devSampleRate = RemoteTCPProtocol::extractUInt32(&metaData[24]);
settingsKeys.append("devSampleRate");
settings.m_log2Decim = RemoteTCPProtocol::extractUInt32(&metaData[28]);
settingsKeys.append("log2Decim");
settings.m_gain[0] = RemoteTCPProtocol::extractInt16(&metaData[32]);
settings.m_gain[1] = RemoteTCPProtocol::extractInt16(&metaData[34]);
settings.m_gain[2] = RemoteTCPProtocol::extractInt16(&metaData[36]);
settingsKeys.append("gain[0]");
settingsKeys.append("gain[1]");
settingsKeys.append("gain[2]");
settings.m_rfBW = RemoteTCPProtocol::extractUInt32(&metaData[40]);
settingsKeys.append("rfBW");
settings.m_inputFrequencyOffset = RemoteTCPProtocol::extractUInt32(&metaData[44]);
settingsKeys.append("inputFrequencyOffset");
settings.m_channelGain = RemoteTCPProtocol::extractUInt32(&metaData[48]);
settingsKeys.append("channelGain");
settings.m_channelSampleRate = RemoteTCPProtocol::extractUInt32(&metaData[52]);
settingsKeys.append("channelSampleRate");
settings.m_sampleBits = RemoteTCPProtocol::extractUInt32(&metaData[56]);
settingsKeys.append("sampleBits");
if (settings.m_channelSampleRate != (settings.m_devSampleRate >> settings.m_log2Decim))
{
// Update local settings to match remote
RemoteTCPInputSettings& settings = m_settings;
QList<QString> settingsKeys;
settings.m_centerFrequency = RemoteTCPProtocol::extractUInt64(&metaData[8]);
settingsKeys.append("centerFrequency");
settings.m_loPpmCorrection = RemoteTCPProtocol::extractUInt32(&metaData[16]);
settingsKeys.append("loPpmCorrection");
quint32 flags = RemoteTCPProtocol::extractUInt32(&metaData[20]);
settings.m_biasTee = flags & 1;
settingsKeys.append("biasTee");
settings.m_directSampling = (flags >> 1) & 1;
settingsKeys.append("directSampling");
settings.m_agc = (flags >> 2) & 1;
settingsKeys.append("agc");
settings.m_dcBlock = (flags >> 3) & 1;
settingsKeys.append("dcBlock");
settings.m_iqCorrection = (flags >> 4) & 1;
settingsKeys.append("iqCorrection");
settings.m_devSampleRate = RemoteTCPProtocol::extractUInt32(&metaData[24]);
settingsKeys.append("devSampleRate");
settings.m_log2Decim = RemoteTCPProtocol::extractUInt32(&metaData[28]);
settingsKeys.append("log2Decim");
settings.m_gain[0] = RemoteTCPProtocol::extractInt16(&metaData[32]);
settings.m_gain[1] = RemoteTCPProtocol::extractInt16(&metaData[34]);
settings.m_gain[2] = RemoteTCPProtocol::extractInt16(&metaData[36]);
settingsKeys.append("gain[0]");
settingsKeys.append("gain[1]");
settingsKeys.append("gain[2]");
settings.m_rfBW = RemoteTCPProtocol::extractUInt32(&metaData[40]);
settingsKeys.append("rfBW");
settings.m_inputFrequencyOffset = RemoteTCPProtocol::extractUInt32(&metaData[44]);
settingsKeys.append("inputFrequencyOffset");
settings.m_channelGain = RemoteTCPProtocol::extractUInt32(&metaData[48]);
settingsKeys.append("channelGain");
settings.m_channelSampleRate = RemoteTCPProtocol::extractUInt32(&metaData[52]);
settingsKeys.append("channelSampleRate");
settings.m_sampleBits = RemoteTCPProtocol::extractUInt32(&metaData[56]);
settingsKeys.append("sampleBits");
if (settings.m_channelSampleRate != (settings.m_devSampleRate >> settings.m_log2Decim))
{
settings.m_channelDecimation = true;
settingsKeys.append("channelDecimation");
}
if (m_messageQueueToInput) {
m_messageQueueToInput->push(RemoteTCPInput::MsgConfigureRemoteTCPInput::create(settings, settingsKeys));
}
if (m_messageQueueToGUI) {
m_messageQueueToGUI->push(RemoteTCPInput::MsgConfigureRemoteTCPInput::create(settings, settingsKeys));
}
settings.m_channelDecimation = true;
settingsKeys.append("channelDecimation");
}
if (m_messageQueueToInput) {
m_messageQueueToInput->push(RemoteTCPInput::MsgConfigureRemoteTCPInput::create(settings, settingsKeys));
}
if (m_messageQueueToGUI) {
m_messageQueueToGUI->push(RemoteTCPInput::MsgConfigureRemoteTCPInput::create(settings, settingsKeys));
}
}
}
else
{
qDebug() << "RemoteTCPInputTCPHandler::dataReadyRead: Unknown protocol: " << protocol;
}
if (m_settings.m_overrideRemoteSettings)
{
// Force settings to be sent to remote device (this needs to be after m_sdra is determined above)
applySettings(m_settings, QList<QString>(), true);
}
}
m_readMetaData = true;
}
}
void RemoteTCPInputTCPHandler::processSpyServerMetaData()
{
bool done = false;
while (!done)
{
if (m_state == HEADER)
{
if (m_dataSocket->bytesAvailable() >= (qint64)sizeof(SpyServerProtocol::Header))
{
qint64 bytesRead = m_dataSocket->read((char *)&m_spyServerHeader, sizeof(SpyServerProtocol::Header));
if (bytesRead == sizeof(SpyServerProtocol::Header)) {
m_state = DATA;
} else {
qDebug() << "RemoteTCPInputTCPHandler::processSpyServerMetaData: Failed to read:" << bytesRead << "/" << sizeof(SpyServerProtocol::Header);
}
}
else
{
done = true;
}
}
else if (m_state == DATA)
{
if (m_dataSocket->bytesAvailable() >= m_spyServerHeader.m_size)
{
qint64 bytesRead = m_dataSocket->read(&m_tcpBuf[0], m_spyServerHeader.m_size);
if (bytesRead == m_spyServerHeader.m_size)
{
if (m_spyServerHeader.m_message == SpyServerProtocol::DeviceMessage)
{
processSpyServerDevice((SpyServerProtocol::Device *) &m_tcpBuf[0]);
m_state = HEADER;
}
else if (m_spyServerHeader.m_message == SpyServerProtocol::StateMessage)
{
// This call can result in applySettings() calling clearBuffer() then processSpyServerData()
processSpyServerState((SpyServerProtocol::State *) &m_tcpBuf[0], true);
spyServerSetStreamIQ();
m_state = HEADER;
m_readMetaData = true;
done = true;
}
else
{
qDebug() << "RemoteTCPInputTCPHandler::processSpyServerMetaData: Unexpected message type" << m_spyServerHeader.m_message;
m_state = HEADER;
}
}
else
{
qDebug() << "RemoteTCPInputTCPHandler::dataReadyRead: Unknown protocol: " << protocol;
}
if (m_settings.m_overrideRemoteSettings)
{
// Force settings to be sent to remote device (this needs to be after m_sdra is determined above)
applySettings(m_settings, QList<QString>(), true);
qDebug() << "RemoteTCPInputTCPHandler::processSpyServerMetaData: Failed to read:" << bytesRead << "/" << m_spyServerHeader.m_size;
}
}
m_readMetaData = true;
else
{
done = true;
}
}
}
}
void RemoteTCPInputTCPHandler::processSpyServerDevice(const SpyServerProtocol::Device* ssDevice)
{
qDebug() << "RemoteTCPInputTCPHandler::processSpyServerDevice:"
<< "device:" << ssDevice->m_device
<< "serial:" << ssDevice->m_serial
<< "sampleRate:" << ssDevice->m_sampleRate
<< "decimationStages:" << ssDevice->m_decimationStages
<< "maxGainIndex:" << ssDevice->m_maxGainIndex
<< "minFrequency:" << ssDevice->m_minFrequency
<< "maxFrequency:" << ssDevice->m_maxFrequency
<< "sampleBits:" << ssDevice->m_sampleBits
<< "minDecimation:" << ssDevice->m_minDecimation;
switch (ssDevice->m_device)
{
case 1:
m_device = RemoteTCPProtocol::AIRSPY;
break;
case 2:
m_device = RemoteTCPProtocol::AIRSPY_HF;
break;
case 3:
m_device = ssDevice->m_maxGainIndex == 14
? RemoteTCPProtocol::RTLSDR_E4000
: RemoteTCPProtocol::RTLSDR_R820T;
break;
default:
m_device = RemoteTCPProtocol::UNKNOWN;
break;
}
if (m_messageQueueToGUI) {
m_messageQueueToGUI->push(MsgReportRemoteDevice::create(m_device, "Spy Server", ssDevice->m_maxGainIndex));
}
RemoteTCPInputSettings& settings = m_settings;
QList<QString> settingsKeys{};
// We can't change sample rate, so always have to update local setting to match
m_settings.m_devSampleRate = settings.m_devSampleRate = ssDevice->m_sampleRate;
settingsKeys.append("devSampleRate");
// Make sure decimation setting is at least the minimum
if (!m_settings.m_overrideRemoteSettings || (settings.m_log2Decim < (int) ssDevice->m_minDecimation))
{
m_settings.m_log2Decim = settings.m_log2Decim = ssDevice->m_minDecimation;
settingsKeys.append("log2Decim");
}
if (m_messageQueueToInput) {
m_messageQueueToInput->push(RemoteTCPInput::MsgConfigureRemoteTCPInput::create(settings, settingsKeys));
}
if (m_messageQueueToGUI) {
m_messageQueueToGUI->push(RemoteTCPInput::MsgConfigureRemoteTCPInput::create(settings, settingsKeys));
}
}
void RemoteTCPInputTCPHandler::processSpyServerState(const SpyServerProtocol::State* ssState, bool initial)
{
qDebug() << "RemoteTCPInputTCPHandler::processSpyServerState: "
<< "initial:" << initial
<< "controllable:" << ssState->m_controllable
<< "gain:" << ssState->m_gain
<< "deviceCenterFrequency:" << ssState->m_deviceCenterFrequency
<< "iqCenterFrequency:" << ssState->m_iqCenterFrequency;
if (initial && ssState->m_controllable && m_settings.m_overrideRemoteSettings)
{
// Force client settings to be sent to server
applySettings(m_settings, QList<QString>(), true);
}
else
{
// Update client settings with that from server
RemoteTCPInputSettings& settings = m_settings;
QList<QString> settingsKeys;
if (m_settings.m_centerFrequency != ssState->m_iqCenterFrequency)
{
settings.m_centerFrequency = ssState->m_iqCenterFrequency;
settingsKeys.append("centerFrequency");
}
if (m_settings.m_gain[0] != (qint32) ssState->m_gain)
{
settings.m_gain[0] = ssState->m_gain;
settingsKeys.append("gain[0]");
}
if (settingsKeys.size() > 0)
{
if (m_messageQueueToInput) {
m_messageQueueToInput->push(RemoteTCPInput::MsgConfigureRemoteTCPInput::create(settings, settingsKeys));
}
if (m_messageQueueToGUI) {
m_messageQueueToGUI->push(RemoteTCPInput::MsgConfigureRemoteTCPInput::create(settings, settingsKeys));
}
}
}
}
void RemoteTCPInputTCPHandler::processSpyServerData(int requiredBytes, bool clear)
{
if (!m_readMetaData) {
return;
}
bool done = false;
while (!done)
{
if (m_state == HEADER)
{
if (m_dataSocket->bytesAvailable() >= (qint64) sizeof(SpyServerProtocol::Header))
{
qint64 bytesRead = m_dataSocket->read((char *) &m_spyServerHeader, sizeof(SpyServerProtocol::Header));
if (bytesRead == sizeof(SpyServerProtocol::Header)) {
m_state = DATA;
} else {
qDebug() << "RemoteTCPInputTCPHandler::processSpyServerData: Failed to read:" << bytesRead << "/" << sizeof(SpyServerProtocol::Header);
}
}
else
{
done = true;
}
}
else if (m_state == DATA)
{
int bytes;
if ((m_spyServerHeader.m_message >= SpyServerProtocol::IQ8MMessage) && (m_spyServerHeader.m_message <= SpyServerProtocol::IQ32Message)) {
bytes = std::min(requiredBytes, (int) m_spyServerHeader.m_size);
} else {
bytes = m_spyServerHeader.m_size;
}
if (m_dataSocket->bytesAvailable() >= bytes)
{
qint64 bytesRead = m_dataSocket->read(&m_tcpBuf[0], bytes);
if (bytesRead == bytes)
{
if ((m_spyServerHeader.m_message >= SpyServerProtocol::IQ8MMessage) && (m_spyServerHeader.m_message <= SpyServerProtocol::IQ32Message))
{
if (!clear)
{
const int bytesPerIQPair = 2 * m_settings.m_sampleBits / 8;
convert(bytesRead / bytesPerIQPair);
}
m_spyServerHeader.m_size -= bytesRead;
requiredBytes -= bytesRead;
if (m_spyServerHeader.m_size == 0) {
m_state = HEADER;
}
if (requiredBytes <= 0) {
done = true;
}
}
else if (m_spyServerHeader.m_message == SpyServerProtocol::StateMessage)
{
processSpyServerState((SpyServerProtocol::State *) &m_tcpBuf[0], false);
m_state = HEADER;
}
else
{
qDebug() << "RemoteTCPInputTCPHandler::processSpyServerData: Skipping unsupported message";
m_state = HEADER;
}
}
else
{
qDebug() << "RemoteTCPInputTCPHandler::processSpyServerData: Failed to read:" << bytesRead << "/" << bytes;
}
}
else
{
done = true;
}
}
}
}
@ -663,12 +1021,12 @@ void RemoteTCPInputTCPHandler::processData()
if (m_dataSocket && (m_dataSocket->state() == QAbstractSocket::ConnectedState))
{
int sampleRate = m_settings.m_channelSampleRate;
int bytesPerSample = m_settings.m_sampleBits / 8;
int bytesPerSecond = sampleRate * 2 * bytesPerSample;
int bytesPerIQPair = 2 * m_settings.m_sampleBits / 8;
int bytesPerSecond = sampleRate * bytesPerIQPair;
if (m_dataSocket->bytesAvailable() < (0.1f * m_settings.m_preFill * bytesPerSecond))
{
qDebug() << "RemoteTCPInputTCPHandler::processData: Buffering!";
qDebug() << "RemoteTCPInputTCPHandler::processData: Buffering - bytesAvailable:" << m_dataSocket->bytesAvailable();
m_fillBuffer = true;
}
@ -690,7 +1048,7 @@ void RemoteTCPInputTCPHandler::processData()
{
if (m_dataSocket->bytesAvailable() >= m_settings.m_preFill * bytesPerSecond)
{
qDebug() << "Buffer primed bytesAvailable:" << m_dataSocket->bytesAvailable();
qDebug() << "RemoteTCPInputTCPHandler::processData: Buffer primed - bytesAvailable:" << m_dataSocket->bytesAvailable();
m_fillBuffer = false;
m_prevDateTime = QDateTime::currentDateTime();
factor = 1.0f / 4.0f; // If this is too high, samples can just be dropped downstream
@ -708,10 +1066,20 @@ void RemoteTCPInputTCPHandler::processData()
if (!m_fillBuffer)
{
if (m_dataSocket->bytesAvailable() >= requiredSamples*2*bytesPerSample)
if (!m_spyServer)
{
m_dataSocket->read(&m_tcpBuf[0], requiredSamples*2*bytesPerSample);
convert(requiredSamples);
// rtl_tcp/SDRA stream is just IQ samples
if (m_dataSocket->bytesAvailable() >= requiredSamples*bytesPerIQPair)
{
m_dataSocket->read(&m_tcpBuf[0], requiredSamples*bytesPerIQPair);
convert(requiredSamples);
}
}
else
{
// SpyServer stream is packetized, into a header and body, with multiple packet types
int requiredBytes = requiredSamples*bytesPerIQPair;
processSpyServerData(requiredBytes, false);
}
}
}
@ -728,10 +1096,32 @@ void RemoteTCPInputTCPHandler::convert(int nbSamples)
m_converterBuffer = new int32_t[nbSamples*2];
}
if ((m_settings.m_sampleBits == 32) && (SDR_RX_SAMP_SZ == 24))
if ((m_settings.m_sampleBits == 32) && (SDR_RX_SAMP_SZ == 24) && !m_spyServer)
{
m_sampleFifo->write(reinterpret_cast<quint8*>(m_tcpBuf), nbSamples*sizeof(Sample));
}
else if ((m_settings.m_sampleBits == 32) && (SDR_RX_SAMP_SZ == 24) && m_spyServer)
{
float *in = (float *)m_tcpBuf;
qint32 *out = (qint32 *)m_converterBuffer;
for (int is = 0; is < nbSamples*2; is++) {
out[is] = (qint32)(in[is] * SDR_RX_SCALEF);
}
m_sampleFifo->write(reinterpret_cast<quint8*>(out), nbSamples*sizeof(Sample));
}
else if ((m_settings.m_sampleBits == 32) && (SDR_RX_SAMP_SZ == 16) && m_spyServer)
{
float *in = (float *)m_tcpBuf;
qint16 *out = (qint16 *)m_converterBuffer;
for (int is = 0; is < nbSamples*2; is++) {
out[is] = (qint16)(in[is] * SDR_RX_SCALEF);
}
m_sampleFifo->write(reinterpret_cast<quint8*>(out), nbSamples*sizeof(Sample));
}
else if ((m_settings.m_sampleBits == 8) && (SDR_RX_SAMP_SZ == 16))
{
quint8 *in = (quint8 *)m_tcpBuf;

Wyświetl plik

@ -1,5 +1,5 @@
///////////////////////////////////////////////////////////////////////////////////
// Copyright (C) 2022 Jon Beniston, M7RCE <jon@beniston.com> //
// Copyright (C) 2022-2024 Jon Beniston, M7RCE <jon@beniston.com> //
// Copyright (C) 2022 Edouard Griffiths, F4EXB <f4exb06@gmail.com> //
// Copyright (C) 2022 Jiří Pinkava <jiri.pinkava@rossum.ai> //
// //
@ -29,6 +29,7 @@
#include "util/messagequeue.h"
#include "remotetcpinputsettings.h"
#include "../../channelrx/remotetcpsink/remotetcpprotocol.h"
#include "spyserver.h"
class SampleSinkFifo;
class MessageQueue;
@ -70,20 +71,23 @@ public:
public:
RemoteTCPProtocol::Device getDevice() const { return m_device; }
QString getProtocol() const { return m_protocol; }
int getMaxGain() const { return m_maxGain; }
static MsgReportRemoteDevice* create(RemoteTCPProtocol::Device device, const QString& protocol)
static MsgReportRemoteDevice* create(RemoteTCPProtocol::Device device, const QString& protocol, int maxGain = 0)
{
return new MsgReportRemoteDevice(device, protocol);
return new MsgReportRemoteDevice(device, protocol, maxGain);
}
protected:
RemoteTCPProtocol::Device m_device;
QString m_protocol;
int m_maxGain;
MsgReportRemoteDevice(RemoteTCPProtocol::Device device, const QString& protocol) :
MsgReportRemoteDevice(RemoteTCPProtocol::Device device, const QString& protocol, int maxGain) :
Message(),
m_device(device),
m_protocol(protocol)
m_protocol(protocol),
m_maxGain(maxGain)
{ }
};
@ -139,6 +143,11 @@ private:
QTimer m_reconnectTimer;
QDateTime m_prevDateTime;
bool m_sdra;
bool m_spyServer;
RemoteTCPProtocol::Device m_device;
SpyServerProtocol::Header m_spyServerHeader;
enum {HEADER, DATA} m_state; //!< FSM for reading Spy Server packets
int32_t *m_converterBuffer;
uint32_t m_converterBufferNbSamples;
@ -172,6 +181,15 @@ private:
void setChannelGain(int gain);
void setSampleBitDepth(int sampleBits);
void applySettings(const RemoteTCPInputSettings& settings, const QList<QString>& settingsKeys, bool force = false);
void processMetaData();
void spyServerConnect();
void spyServerSet(int setting, int value);
void spyServerSetIQFormat(int sampleBits);
void spyServerSetStreamIQ();
void processSpyServerMetaData();
void processSpyServerDevice(const SpyServerProtocol::Device* ssDevice);
void processSpyServerState(const SpyServerProtocol::State* ssState, bool initial);
void processSpyServerData(int requiredBytes, bool clear);
private slots:
void started();

Wyświetl plik

@ -0,0 +1,85 @@
#ifndef SPY_SERVER_H
#define SPY_SERVER_H
#include <QtCore>
class SpyServerProtocol {
public:
static constexpr int ProtocolID = (2<<24) | 1700;
enum Command {
setStreamingMode = 0,
setStreamingEnabled = 1,
setGain = 2,
setIQFormat = 100,
setCenterFrequency = 101,
setIQDecimation = 102,
};
enum Message {
DeviceMessage = 0,
StateMessage = 1,
IQ8MMessage = 100,
IQ16Message = 101,
IQ24Message = 102,
IQ32Message = 103
};
struct Header {
quint32 m_id;
quint32 m_message;
quint32 m_unused1;
quint32 m_unused2;
quint32 m_size;
};
struct Device {
quint32 m_device;
quint32 m_serial;
quint32 m_sampleRate;
quint32 m_unused1;
quint32 m_decimationStages; // 8 for Airspy HF, 11 for Airspy, 9 for E4000/R828D/R820
quint32 m_unused2;
quint32 m_maxGainIndex; // 8 for Airspy HF, 21 for Airspy, 14 for E4000, 29 for R828D/R820
quint32 m_minFrequency;
quint32 m_maxFrequency;
quint32 m_sampleBits;
quint32 m_minDecimation; // Set when maximum_bandwidth is set in spyserver.config
quint32 m_unused3;
};
struct State {
quint32 m_controllable;
quint32 m_gain;
quint32 m_deviceCenterFrequency;
quint32 m_iqCenterFrequency;
quint32 m_unused1;
quint32 m_unused2;
quint32 m_unused3;
quint32 m_unused4;
quint32 m_unused5;
};
static void encodeUInt32(quint8 *p, quint32 data)
{
p[3] = (data >> 24) & 0xff;
p[2] = (data >> 16) & 0xff;
p[1] = (data >> 8) & 0xff;
p[0] = data & 0xff;
}
static quint32 extractUInt32(quint8 *p)
{
quint32 data;
data = (p[0] & 0xff)
| ((p[1] & 0xff) << 8)
| ((p[2] & 0xff) << 16)
| ((p[3] & 0xff) << 24);
return data;
}
};
#endif /* SPY_SERVER_H */

Wyświetl plik

@ -320,23 +320,30 @@ void SDRPlayV3Gui::updateLNAValues()
bool found = false;
const int *attenuations = SDRPlayV3LNA::getAttenuations(m_sdrPlayV3Input->getDeviceId(), m_settings.m_centerFrequency);
int len = attenuations[0];
ui->gainLNA->blockSignals(true);
ui->gainLNA->clear();
for (int i = 1; i <= len; i++)
if (attenuations)
{
if (attenuations[i] == 0)
ui->gainLNA->addItem("0");
else
ui->gainLNA->addItem(QString("-%1").arg(attenuations[i]));
// Find closest match
if ((attenuations[i] == -currentValue) || (!found && (attenuations[i] > -currentValue)))
int len = attenuations[0];
for (int i = 1; i <= len; i++)
{
ui->gainLNA->setCurrentIndex(i - 1);
found = true;
if (attenuations[i] == 0)
ui->gainLNA->addItem("0");
else
ui->gainLNA->addItem(QString("-%1").arg(attenuations[i]));
// Find closest match
if ((attenuations[i] == -currentValue) || (!found && (attenuations[i] > -currentValue)))
{
ui->gainLNA->setCurrentIndex(i - 1);
found = true;
}
}
}
else
{
qDebug() << "SDRPlayV3Gui::updateLNAValues: No attenuations for deviceID: " << m_sdrPlayV3Input->getDeviceId();
}
ui->gainLNA->blockSignals(false);
}

Wyświetl plik

@ -239,12 +239,14 @@ set(sdrbase_SOURCES
util/golay2312.cpp
util/httpdownloadmanager.cpp
util/interpolation.cpp
util/kiwisdrlist.cpp
util/lfsr.cpp
util/maidenhead.cpp
util/message.cpp
util/messagequeue.cpp
util/mmsi.cpp
util/morse.cpp
util/nasaglobalimagery.cpp
util/navtex.cpp
util/openaip.cpp
util/osndb.cpp
@ -256,17 +258,20 @@ set(sdrbase_SOURCES
util/profiler.cpp
util/psk31.cpp
util/radiosonde.cpp
util/rainviewer.cpp
util/rtpsink.cpp
util/syncmessenger.cpp
util/samplesourceserializer.cpp
util/simpleserializer.cpp
util/serialutil.cpp
#util/spinlock.cpp
util/spyserverlist.cpp
util/rtty.cpp
util/uid.cpp
util/units.cpp
util/timeutil.cpp
util/visa.cpp
util/waypoints.cpp
util/weather.cpp
util/iot/device.cpp
util/iot/homeassistant.cpp
@ -482,6 +487,7 @@ set(sdrbase_HEADERS
util/incrementalarray.h
util/incrementalvector.h
util/interpolation.h
util/kiwisdrlist.h
util/lfsr.h
util/maidenhead.h
util/message.h
@ -490,6 +496,7 @@ set(sdrbase_HEADERS
util/morse.h
util/movingaverage.h
util/movingmaximum.h
util/nasaglobalimagery.h
util/navtex.h
util/openaip.h
util/osndb.h
@ -501,6 +508,7 @@ set(sdrbase_HEADERS
util/profiler.h
util/psk31.h
util/radiosonde.h
util/rainviewer.h
util/rtpsink.h
util/rtty.h
util/syncmessenger.h
@ -508,10 +516,12 @@ set(sdrbase_HEADERS
util/simpleserializer.h
util/serialutil.h
#util/spinlock.h
util/spyserverlist.h
util/uid.h
util/units.h
util/timeutil.h
util/visa.h
util/waypoints.h
util/weather.h
util/iot/device.h
util/iot/homeassistant.h

Wyświetl plik

@ -17,6 +17,7 @@
///////////////////////////////////////////////////////////////////////////////////
#include <QGlobalStatic>
#include <functional>
#include "deviceenumerator.h"
@ -432,6 +433,20 @@ void DeviceEnumerator::removeMIMOSelection(int tabIndex)
}
}
void DeviceEnumerator::renumeratetabIndex(int skippedTabIndex)
{
std::reference_wrapper<DevicesEnumeration> denums[] = {m_rxEnumeration, m_txEnumeration, m_mimoEnumeration};
for (DevicesEnumeration &denum : denums)
{
for (DevicesEnumeration::iterator it = denum.begin(); it != denum.end(); ++it)
{
if (it->m_samplingDevice.claimed > skippedTabIndex) {
it->m_samplingDevice.claimed--;
}
}
}
}
int DeviceEnumerator::getFileInputDeviceIndex() const
{
for (DevicesEnumeration::const_iterator it = m_rxEnumeration.begin(); it != m_rxEnumeration.end(); ++it)

Wyświetl plik

@ -52,6 +52,7 @@ public:
void removeRxSelection(int tabIndex);
void removeTxSelection(int tabIndex);
void removeMIMOSelection(int tabIndex);
void renumeratetabIndex(int skippedTabIndex);
int getNbRxSamplingDevices() const { return m_rxEnumeration.size(); }
int getNbTxSamplingDevices() const { return m_txEnumeration.size(); }
int getNbMIMOSamplingDevices() const { return m_mimoEnumeration.size(); }

Wyświetl plik

@ -13104,6 +13104,10 @@ margin-bottom: 20px;
"preFill" : {
"type" : "integer"
},
"protocol" : {
"type" : "string",
"description" : "(SDRangel or Spy Server)"
},
"useReverseAPI" : {
"type" : "integer",
"description" : "Synchronize with reverse API (1 for yes, 0 for no)"
@ -58668,7 +58672,7 @@ except ApiException as e:
</div>
<div id="generator">
<div class="content">
Generated 2024-02-12T10:33:45.606+01:00
Generated 2024-02-27T15:07:08.970+01:00
</div>
</div>
</div>

Wyświetl plik

@ -43,6 +43,9 @@ RemoteTCPInputSettings:
type: integer
preFill:
type: integer
protocol:
description: (SDRangel or Spy Server)
type: string
useReverseAPI:
description: Synchronize with reverse API (1 for yes, 0 for no)
type: integer

Wyświetl plik

@ -149,7 +149,7 @@ void HttpDownloadManager::downloadFinished(QNetworkReply *reply)
QString action = match.captured(1);
action = action.replace("&amp;", "&");
qDebug() << "HttpDownloadManager: Skipping Go ogle drive warning - downloading " << action;
qDebug() << "HttpDownloadManager: Skipping Google drive warning - downloading " << action;
QUrl newUrl(action);
QNetworkReply *newReply = download(newUrl, filename);
@ -160,7 +160,51 @@ void HttpDownloadManager::downloadFinished(QNetworkReply *reply)
}
else
{
qDebug() << "HttpDownloadManager: Can't find action URL in Google Drive page " << data;
qDebug() << "HttpDownloadManager: Can't find action URL in Google Drive page\nURL: " << url << "\nData:\n" << data;
}
}
else if (url.startsWith("https://drive.usercontent.google.com/download")
&& data.startsWith("<!DOCTYPE html>")
&& !filename.endsWith(".html")
)
{
QRegularExpression regexpAction("action=\\\"(.*?)\\\"");
QRegularExpressionMatch matchAction = regexpAction.match(data);
QRegularExpression regexpId("name=\"id\" value=\"([\\w-]+)\"");
QRegularExpressionMatch matchId = regexpId.match(data);
QRegularExpression regexpUuid("name=\"uuid\" value=\"([\\w-]+)\"");
QRegularExpressionMatch matchUuid = regexpUuid.match(data);
QRegularExpression regexpAt("name=\"at\" value=\"([\\w-]+\\:)\"");
QRegularExpressionMatch matchAt = regexpAt.match(data);
if (matchAction.hasMatch() && matchId.hasMatch() && matchUuid.hasMatch())
{
m_downloads.removeAll(reply);
m_filenames.remove(idx);
QString newURLString = matchAction.captured(1)
+ "?id=" + matchId.captured(1)
+ "&export=download"
+ "&authuser=0"
+ "&confirm=t"
+ "&uuid=" + matchUuid.captured(1)
;
if (matchAt.hasMatch()) {
newURLString = newURLString + "at=" + matchAt.captured(1);
}
qDebug() << "HttpDownloadManager: Skipping Google drive warning - downloading " << newURLString;
QUrl newUrl(newURLString);
QNetworkReply *newReply = download(newUrl, filename);
// Indicate that we are retrying, so progress dialogs can be updated
emit retryDownload(filename, reply, newReply);
retry = true;
}
else
{
qDebug() << "HttpDownloadManager: Can't find action URL in Google Drive page\nURL: " << url << "\nData:\n" << data;
}
}
else if (writeToFile(filename, data))

Wyświetl plik

@ -0,0 +1,198 @@
///////////////////////////////////////////////////////////////////////////////////
// Copyright (C) 2024 Jon Beniston, M7RCE <jon@beniston.com> //
// //
// This program is free software; you can redistribute it and/or modify //
// it under the terms of the GNU General Public License as published by //
// the Free Software Foundation as version 3 of the License, or //
// (at your option) any later version. //
// //
// This program is distributed in the hope that it will be useful, //
// but WITHOUT ANY WARRANTY; without even the implied warranty of //
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the //
// GNU General Public License V3 for more details. //
// //
// You should have received a copy of the GNU General Public License //
// along with this program. If not, see <http://www.gnu.org/licenses/>. //
///////////////////////////////////////////////////////////////////////////////////
#include "kiwisdrlist.h"
#include <QDebug>
#include <QUrl>
#include <QUrlQuery>
#include <QNetworkReply>
#include <QXmlStreamReader>
#include <QNetworkDiskCache>
#include <QRegularExpression>
KiwiSDRList::KiwiSDRList()
{
m_networkManager = new QNetworkAccessManager();
QObject::connect(m_networkManager, &QNetworkAccessManager::finished, this, &KiwiSDRList::handleReply);
QStringList locations = QStandardPaths::standardLocations(QStandardPaths::AppDataLocation);
QDir writeableDir(locations[0]);
if (!writeableDir.mkpath(QStringLiteral("cache") + QDir::separator() + QStringLiteral("kiwisdr"))) {
qDebug() << "Failed to create cache/kiwisdr";
}
m_cache = new QNetworkDiskCache();
m_cache->setCacheDirectory(locations[0] + QDir::separator() + QStringLiteral("cache") + QDir::separator() + QStringLiteral("kiwisdr"));
m_cache->setMaximumCacheSize(100000000);
m_networkManager->setCache(m_cache);
connect(&m_timer, &QTimer::timeout, this, &KiwiSDRList::update);
}
KiwiSDRList::~KiwiSDRList()
{
QObject::disconnect(m_networkManager, &QNetworkAccessManager::finished, this, &KiwiSDRList::handleReply);
delete m_networkManager;
}
void KiwiSDRList::getData()
{
QUrl url(QString("http://kiwisdr.com/public/"));
m_networkManager->get(QNetworkRequest(url));
}
void KiwiSDRList::getDataPeriodically(int periodInMins)
{
m_timer.setInterval(periodInMins*60*1000);
m_timer.start();
update();
}
void KiwiSDRList::update()
{
getData();
}
void KiwiSDRList::handleReply(QNetworkReply* reply)
{
if (reply)
{
if (!reply->error())
{
QString url = reply->url().toEncoded().constData();
QByteArray bytes = reply->readAll();
handleHTML(url, bytes);
}
else
{
qDebug() << "KiwiSDRList::handleReply: error: " << reply->error();
}
reply->deleteLater();
}
else
{
qDebug() << "KiwiSDRList::handleReply: reply is null";
}
}
void KiwiSDRList::handleHTML(const QString& url, const QByteArray& bytes)
{
QList<KiwiSDR> sdrs;
QString html(bytes);
QRegularExpression div("<div class='cl-info'>(.*?)<\\/div>", QRegularExpression::DotMatchesEverythingOption);
QRegularExpressionMatchIterator divItr = div.globalMatch(html);
if (divItr.hasNext())
{
while (divItr.hasNext())
{
QRegularExpressionMatch divMatch = divItr.next();
QString divText = divMatch.captured(1);
QRegularExpression urlRe("a href='(.*?)'");
QRegularExpressionMatch urlMatch = urlRe.match(divText);
if (urlMatch.hasMatch())
{
KiwiSDR sdr;
sdr.m_url = urlMatch.captured(1);
QRegularExpression element("<!-- (\\w+)=(.*?) -->");
QRegularExpressionMatchIterator elementItr = element.globalMatch(divText);
while(elementItr.hasNext())
{
QRegularExpressionMatch elementMatch = elementItr.next();
QString key = elementMatch.captured(1);
QString value = elementMatch.captured(2);
if (key == "name")
{
sdr.m_name = value;
}
else if (key == "sdr_hw")
{
sdr.m_sdrHW = value;
}
else if (key == "bands")
{
QRegularExpression freqRe("([\\d]+)-([\\d]+)");
QRegularExpressionMatch freqMatch = freqRe.match(value);
if (freqMatch.hasMatch())
{
sdr.m_lowFrequency = freqMatch.captured(1).toInt();
sdr.m_highFrequency = freqMatch.captured(2).toInt();
}
}
else if (key == "users")
{
sdr.m_users = value.toInt();
}
else if (key == "users_max")
{
sdr.m_usersMax = value.toInt();
}
else if (key == "gps")
{
QRegularExpression gpsRe("([\\d.+-]+), ([\\d.+-]+)");
QRegularExpressionMatch gpsMatch = gpsRe.match(value);
if (gpsMatch.hasMatch())
{
sdr.m_latitude = gpsMatch.captured(1).toFloat();
sdr.m_longitude = gpsMatch.captured(2).toFloat();
}
}
else if (key == "asl")
{
sdr.m_altitude = value.toInt();
}
else if (key == "loc")
{
sdr.m_location = value;
}
else if (key == "antenna")
{
sdr.m_antenna = value;
}
else if (key == "ant_connected")
{
sdr.m_antennaConnected = value == "1";
}
else if (key == "snr")
{
sdr.m_snr = value;
}
}
sdrs.append(sdr);
}
else
{
qDebug() << "KiwiSDRPublic::handleHTML: No URL found in:\n" << divText;
}
}
}
else
{
qDebug() << "KiwiSDRPublic::handleHTML: No cl-info found in:\n" << html;
}
emit dataUpdated(sdrs);
}

Wyświetl plik

@ -0,0 +1,78 @@
///////////////////////////////////////////////////////////////////////////////////
// Copyright (C) 2024 Jon Beniston, M7RCE <jon@beniston.com> //
// //
// This program is free software; you can redistribute it and/or modify //
// it under the terms of the GNU General Public License as published by //
// the Free Software Foundation as version 3 of the License, or //
// (at your option) any later version. //
// //
// This program is distributed in the hope that it will be useful, //
// but WITHOUT ANY WARRANTY; without even the implied warranty of //
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the //
// GNU General Public License V3 for more details. //
// //
// You should have received a copy of the GNU General Public License //
// along with this program. If not, see <http://www.gnu.org/licenses/>. //
///////////////////////////////////////////////////////////////////////////////////
#ifndef INCLUDE_KIWISDRLIST_H
#define INCLUDE_KIWISDRLIST_H
#include <QtCore>
#include <QTimer>
#include "export.h"
class QNetworkAccessManager;
class QNetworkReply;
class QNetworkDiskCache;
// Gets a list of public Kiwi SDRs from http://kiwisdr.com/public/
class SDRBASE_API KiwiSDRList : public QObject
{
Q_OBJECT
public:
struct KiwiSDR {
QString m_url;
QString m_status; // Only seems to be 'active'
QString m_offline; // Only seems to be 'no'
QString m_name;
QString m_sdrHW;
qint64 m_lowFrequency;
qint64 m_highFrequency;
int m_users;
int m_usersMax;
float m_latitude;
float m_longitude;
int m_altitude; // Above sea level (Not sure if ft or m)
QString m_location;
QString m_antenna;
bool m_antennaConnected;
QString m_snr;
};
KiwiSDRList();
~KiwiSDRList();
void getData();
void getDataPeriodically(int periodInMins=4);
public slots:
void handleReply(QNetworkReply* reply);
void update();
signals:
void dataUpdated(const QList<KiwiSDR>& sdrs); // Emitted when data are available.
private:
QNetworkAccessManager *m_networkManager;
QNetworkDiskCache *m_cache;
QTimer m_timer; // Timer for periodic updates
void handleHTML(const QString& url, const QByteArray& bytes);
};
#endif /* INCLUDE_KIWISDRLIST_H */

Wyświetl plik

@ -0,0 +1,326 @@
///////////////////////////////////////////////////////////////////////////////////
// Copyright (C) 2024 Jon Beniston, M7RCE <jon@beniston.com> //
// //
// This program is free software; you can redistribute it and/or modify //
// it under the terms of the GNU General Public License as published by //
// the Free Software Foundation as version 3 of the License, or //
// (at your option) any later version. //
// //
// This program is distributed in the hope that it will be useful, //
// but WITHOUT ANY WARRANTY; without even the implied warranty of //
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the //
// GNU General Public License V3 for more details. //
// //
// You should have received a copy of the GNU General Public License //
// along with this program. If not, see <http://www.gnu.org/licenses/>. //
///////////////////////////////////////////////////////////////////////////////////
#include "nasaglobalimagery.h"
#include <QDebug>
#include <QUrl>
#include <QUrlQuery>
#include <QNetworkReply>
#include <QXmlStreamReader>
#include <QNetworkDiskCache>
#include <QJsonDocument>
#include <QJsonObject>
NASAGlobalImagery::NASAGlobalImagery()
{
m_networkManager = new QNetworkAccessManager();
QObject::connect(m_networkManager, &QNetworkAccessManager::finished, this, &NASAGlobalImagery::handleReply);
QStringList locations = QStandardPaths::standardLocations(QStandardPaths::AppDataLocation);
QDir writeableDir(locations[0]);
if (!writeableDir.mkpath(QStringLiteral("cache") + QDir::separator() + QStringLiteral("nasaglobalimagery"))) {
qDebug() << "Failed to create cache/nasaglobalimagery";
}
m_cache = new QNetworkDiskCache();
m_cache->setCacheDirectory(locations[0] + QDir::separator() + QStringLiteral("cache") + QDir::separator() + QStringLiteral("nasaglobalimagery"));
m_cache->setMaximumCacheSize(100000000);
m_networkManager->setCache(m_cache);
}
NASAGlobalImagery::~NASAGlobalImagery()
{
QObject::disconnect(m_networkManager, &QNetworkAccessManager::finished, this, &NASAGlobalImagery::handleReply);
delete m_networkManager;
}
void NASAGlobalImagery::getData()
{
QUrl url(QString("https://gibs.earthdata.nasa.gov/wmts/epsg3857/best/1.0.0/WMTSCapabilities.xml"));
m_networkManager->get(QNetworkRequest(url));
}
void NASAGlobalImagery::getMetaData()
{
QUrl url(QString("https://worldview.earthdata.nasa.gov/config/wv.json"));
m_networkManager->get(QNetworkRequest(url));
}
void NASAGlobalImagery::handleReply(QNetworkReply* reply)
{
if (reply)
{
if (!reply->error())
{
QString url = reply->url().toEncoded().constData();
QByteArray bytes = reply->readAll();
if (url.endsWith(".xml")) {
handleXML(bytes);
} else if (url.endsWith(".svg")) {
handleSVG(url, bytes);
} else if (url.endsWith(".json")) {
handleJSON(bytes);
} else if (url.endsWith(".html")) {
handleHTML(url, bytes);
} else {
qDebug() << "NASAGlobalImagery::handleReply: unexpected URL: " << url;
}
}
else
{
qDebug() << "NASAGlobalImagery::handleReply: error: " << reply->error();
}
reply->deleteLater();
}
else
{
qDebug() << "NASAGlobalImagery::handleReply: reply is null";
}
}
void NASAGlobalImagery::handleXML(const QByteArray& bytes)
{
QXmlStreamReader xmlReader(bytes);
QList<DataSet> dataSets;
while (!xmlReader.atEnd() && !xmlReader.hasError())
{
while (xmlReader.readNextStartElement())
{
if (xmlReader.name() == QLatin1String("Capabilities"))
{
while(xmlReader.readNextStartElement())
{
if (xmlReader.name() == QLatin1String("Contents"))
{
while(xmlReader.readNextStartElement())
{
if (xmlReader.name() == QLatin1String("Layer"))
{
QString identifier;
QString colorMap;
QList<Legend> legends;
QString tileMatrixSet;
QStringList urls;
QString format;
QString defaultDateTime;
QStringList dates;
while(xmlReader.readNextStartElement())
{
if (xmlReader.name() == QLatin1String("Identifier"))
{
identifier = xmlReader.readElementText();
}
else if (xmlReader.name() == QLatin1String("TileMatrixSetLink"))
{
while(xmlReader.readNextStartElement())
{
if (xmlReader.name() == QLatin1String("TileMatrixSet"))
{
tileMatrixSet = xmlReader.readElementText();
}
else
{
xmlReader.skipCurrentElement();
}
}
}
else if (xmlReader.name() == QLatin1String("Style"))
{
while(xmlReader.readNextStartElement())
{
if (xmlReader.name() == QLatin1String("LegendURL"))
{
Legend legend;
legend.m_url = xmlReader.attributes().value("xlink:href").toString();
legend.m_width = (int)xmlReader.attributes().value("width").toFloat();
legend.m_height = (int)xmlReader.attributes().value("height").toFloat();
//qDebug() << legend.m_url << legend.m_width << legend.m_height;
legends.append(legend);
xmlReader.skipCurrentElement();
}
else
{
xmlReader.skipCurrentElement();
}
}
}
else if (xmlReader.name() == QLatin1String("Metadata"))
{
colorMap = xmlReader.attributes().value("xlink:href").toString();
xmlReader.skipCurrentElement();
}
else if (xmlReader.name() == QLatin1String("Format"))
{
format = xmlReader.readElementText();
}
else if (xmlReader.name() == QLatin1String("ResourceURL"))
{
QString url = xmlReader.attributes().value("template").toString();
if (!url.isEmpty()) {
urls.append(url);
}
xmlReader.skipCurrentElement();
}
else if (xmlReader.name() == QLatin1String("Dimension"))
{
while(xmlReader.readNextStartElement())
{
if (xmlReader.name() == QLatin1String("Default"))
{
defaultDateTime = xmlReader.readElementText();
}
else if (xmlReader.name() == QLatin1String("Value"))
{
dates.append(xmlReader.readElementText());
}
else
{
xmlReader.skipCurrentElement();
}
}
}
else
{
xmlReader.skipCurrentElement();
}
}
// Some layers are <Format>application/vnd.mapbox-vector-tile</Format>
if (!identifier.isEmpty() && !tileMatrixSet.isEmpty() && ((format == "image/png") || (format == "image/jpeg")))
{
DataSet dataSet;
dataSet.m_identifier = identifier;
dataSet.m_legends = legends;
dataSet.m_tileMatrixSet = tileMatrixSet;
dataSet.m_format = format;
dataSet.m_defaultDateTime = defaultDateTime;
dataSet.m_dates = dates;
dataSets.append(dataSet);
//qDebug() << "Adding layer" << identifier << colorMap << legends << tileMatrixSet;
}
}
else
{
xmlReader.skipCurrentElement();
}
}
}
else if (xmlReader.name() == QLatin1String("ServiceMetadataURL"))
{
// Empty?
}
else
{
xmlReader.skipCurrentElement();
}
}
}
else
{
xmlReader.skipCurrentElement();
}
}
}
// Ignore "Premature end of document." here even if ok
if (!xmlReader.atEnd() && xmlReader.hasError())
{
qDebug() << "NASAGlobalImagery::handleReply: Error parsing XML: " << xmlReader.errorString();
}
emit dataUpdated(dataSets);
}
void NASAGlobalImagery::downloadLegend(const Legend& legend)
{
QUrl url(legend.m_url);
m_networkManager->get(QNetworkRequest(url));
}
void NASAGlobalImagery::downloadHTML(const QString& urlString)
{
QUrl url(urlString);
m_networkManager->get(QNetworkRequest(url));
}
void NASAGlobalImagery::handleSVG(const QString& url, const QByteArray& bytes)
{
emit legendAvailable(url, bytes);
}
void NASAGlobalImagery::handleJSON(const QByteArray& bytes)
{
MetaData metaData;
QJsonDocument document = QJsonDocument::fromJson(bytes);
if (document.isObject())
{
QJsonObject obj = document.object();
if (obj.contains(QStringLiteral("layers")))
{
for (const auto& oRef : obj.value(QStringLiteral("layers")).toObject())
{
Layer layer;
QJsonObject o = oRef.toObject();
if (o.contains(QStringLiteral("id"))) {
layer.m_identifier = o.value(QStringLiteral("id")).toString();
}
if (o.contains(QStringLiteral("title"))) {
layer.m_title = o.value(QStringLiteral("title")).toString();
}
if (o.contains(QStringLiteral("subtitle"))) {
layer.m_subtitle = o.value(QStringLiteral("subtitle")).toString();
}
if (o.contains(QStringLiteral("description"))) {
layer.m_descriptionURL = "https://worldview.earthdata.nasa.gov/config/metadata/layers/" + o.value(QStringLiteral("description")).toString() + ".html";
}
if (o.contains(QStringLiteral("startDate"))) {
layer.m_startDate = QDateTime::fromString(o.value(QStringLiteral("startDate")).toString(), Qt::ISODate);
}
if (o.contains(QStringLiteral("endDate"))) {
layer.m_endDate = QDateTime::fromString(o.value(QStringLiteral("endDate")).toString(), Qt::ISODate);
}
if (o.contains(QStringLiteral("ongoing"))) {
layer.m_ongoing = o.value(QStringLiteral("ongoing")).toBool();
}
if (o.contains(QStringLiteral("layerPeriod"))) {
layer.m_layerPeriod = o.value(QStringLiteral("layerPeriod")).toString();
}
if (o.contains(QStringLiteral("layergroup"))) {
layer.m_layerGroup = o.value(QStringLiteral("layergroup")).toString();
}
if (!layer.m_identifier.isEmpty()) {
metaData.m_layers.insert(layer.m_identifier, layer);
}
}
}
}
emit metaDataUpdated(metaData);
}
void NASAGlobalImagery::handleHTML(const QString& url, const QByteArray& bytes)
{
emit htmlAvailable(url, bytes);
}

Wyświetl plik

@ -0,0 +1,98 @@
///////////////////////////////////////////////////////////////////////////////////
// Copyright (C) 2024 Jon Beniston, M7RCE <jon@beniston.com> //
// //
// This program is free software; you can redistribute it and/or modify //
// it under the terms of the GNU General Public License as published by //
// the Free Software Foundation as version 3 of the License, or //
// (at your option) any later version. //
// //
// This program is distributed in the hope that it will be useful, //
// but WITHOUT ANY WARRANTY; without even the implied warranty of //
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the //
// GNU General Public License V3 for more details. //
// //
// You should have received a copy of the GNU General Public License //
// along with this program. If not, see <http://www.gnu.org/licenses/>. //
///////////////////////////////////////////////////////////////////////////////////
#ifndef INCLUDE_NASAGLOBALIMAGERY_H
#define INCLUDE_NASAGLOBALIMAGERY_H
#include <QtCore>
#include <QTimer>
#include "export.h"
class QNetworkAccessManager;
class QNetworkReply;
class QNetworkDiskCache;
// NASA GIBS (Global Imagery Browse Server) API (https://nasa-gibs.github.io/gibs-api-docs/)
// Gets details of available data sets for use on maps
// Also supports cached download of .svg legends
class SDRBASE_API NASAGlobalImagery : public QObject
{
Q_OBJECT
public:
struct Legend {
QString m_url; // Typically to .svg file
int m_width;
int m_height;
};
struct DataSet {
QString m_identifier;
QList<Legend> m_legends;
QString m_tileMatrixSet;
QString m_format;
QString m_defaultDateTime;
QStringList m_dates;
};
struct Layer {
QString m_identifier;
QString m_title;
QString m_subtitle;
QString m_descriptionURL;
QDateTime m_startDate;
QDateTime m_endDate;
bool m_ongoing;
QString m_layerPeriod;
QString m_layerGroup;
};
struct MetaData {
QHash<QString, Layer> m_layers;
};
NASAGlobalImagery();
~NASAGlobalImagery();
void getData();
void getMetaData();
void downloadLegend(const Legend& legend);
void downloadHTML(const QString& url);
public slots:
void handleReply(QNetworkReply* reply);
signals:
void dataUpdated(const QList<DataSet>& dataSets); // Emitted when paths to new data are available.
void metaDataUpdated(const MetaData& metaData);
void legendAvailable(const QString& url, const QByteArray data);
void htmlAvailable(const QString& url, const QByteArray data);
private:
QNetworkAccessManager *m_networkManager;
QNetworkDiskCache *m_cache;
void handleXML(const QByteArray& bytes);
void handleSVG(const QString& url, const QByteArray& bytes);
void handleJSON(const QByteArray& bytes);
void handleHTML(const QString& url, const QByteArray& bytes);
};
#endif /* INCLUDE_NASAGLOBALIMAGERY_H */

Wyświetl plik

@ -0,0 +1,130 @@
///////////////////////////////////////////////////////////////////////////////////
// Copyright (C) 2024 Jon Beniston, M7RCE <jon@beniston.com> //
// //
// This program is free software; you can redistribute it and/or modify //
// it under the terms of the GNU General Public License as published by //
// the Free Software Foundation as version 3 of the License, or //
// (at your option) any later version. //
// //
// This program is distributed in the hope that it will be useful, //
// but WITHOUT ANY WARRANTY; without even the implied warranty of //
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the //
// GNU General Public License V3 for more details. //
// //
// You should have received a copy of the GNU General Public License //
// along with this program. If not, see <http://www.gnu.org/licenses/>. //
///////////////////////////////////////////////////////////////////////////////////
#include "rainviewer.h"
#include <QDebug>
#include <QUrl>
#include <QUrlQuery>
#include <QNetworkReply>
#include <QJsonDocument>
#include <QJsonObject>
RainViewer::RainViewer()
{
connect(&m_timer, &QTimer::timeout, this, &RainViewer::update);
m_networkManager = new QNetworkAccessManager();
QObject::connect(m_networkManager, &QNetworkAccessManager::finished, this, &RainViewer::handleReply);
}
RainViewer::~RainViewer()
{
m_timer.stop();
QObject::disconnect(m_networkManager, &QNetworkAccessManager::finished, this, &RainViewer::handleReply);
delete m_networkManager;
}
void RainViewer::getPathPeriodically(int periodInMins)
{
// Rain maps updated every 10mins
m_timer.setInterval(periodInMins*60*1000);
m_timer.start();
update();
}
void RainViewer::update()
{
getPath();
}
void RainViewer::getPath()
{
QUrl url(QString("https://api.rainviewer.com/public/weather-maps.json"));
m_networkManager->get(QNetworkRequest(url));
}
void RainViewer::handleReply(QNetworkReply* reply)
{
if (reply)
{
if (!reply->error())
{
QJsonDocument document = QJsonDocument::fromJson(reply->readAll());
if (document.isObject())
{
QJsonObject obj = document.object();
QString radarPath = "";
QString satellitePath = "";
if (obj.contains(QStringLiteral("radar")))
{
QJsonValue val = obj.value(QStringLiteral("radar"));
QJsonObject mainObj = val.toObject();
if (mainObj.contains(QStringLiteral("past")))
{
QJsonArray past = mainObj.value(QStringLiteral("past")).toArray();
if (past.size() > 0)
{
QJsonObject mostRecent = past.last().toObject();
if (mostRecent.contains(QStringLiteral("path"))) {
radarPath = mostRecent.value(QStringLiteral("path")).toString();
}
}
}
}
else
{
qDebug() << "RainViewer::handleReply: Object doesn't contain a radar: " << obj;
}
if (obj.contains(QStringLiteral("satellite")))
{
QJsonValue val = obj.value(QStringLiteral("satellite"));
QJsonObject mainObj = val.toObject();
if (mainObj.contains(QStringLiteral("infrared")))
{
QJsonArray ir = mainObj.value(QStringLiteral("infrared")).toArray();
if (ir.size() > 0)
{
QJsonObject mostRecent = ir.last().toObject();
if (mostRecent.contains(QStringLiteral("path"))) {
satellitePath = mostRecent.value(QStringLiteral("path")).toString();
}
}
}
}
else
{
qDebug() << "RainViewer::handleReply: Object doesn't contain a satellite: " << obj;
}
emit pathUpdated(radarPath, satellitePath);
}
else
{
qDebug() << "RainViewer::handleReply: Document is not an object: " << document;
}
}
else
{
qDebug() << "RainViewer::handleReply: error: " << reply->error();
}
reply->deleteLater();
}
else
{
qDebug() << "RainViewer::handleReply: reply is null";
}
}

Wyświetl plik

@ -0,0 +1,55 @@
///////////////////////////////////////////////////////////////////////////////////
// Copyright (C) 2024 Jon Beniston, M7RCE <jon@beniston.com> //
// //
// This program is free software; you can redistribute it and/or modify //
// it under the terms of the GNU General Public License as published by //
// the Free Software Foundation as version 3 of the License, or //
// (at your option) any later version. //
// //
// This program is distributed in the hope that it will be useful, //
// but WITHOUT ANY WARRANTY; without even the implied warranty of //
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the //
// GNU General Public License V3 for more details. //
// //
// You should have received a copy of the GNU General Public License //
// along with this program. If not, see <http://www.gnu.org/licenses/>. //
///////////////////////////////////////////////////////////////////////////////////
#ifndef INCLUDE_RAINVIEWER_H
#define INCLUDE_RAINVIEWER_H
#include <QtCore>
#include <QTimer>
#include "export.h"
class QNetworkAccessManager;
class QNetworkReply;
// RainViewer API wrapper (https://www.rainviewer.com/)
// Gets details of currently available weather radar and satellite IR data
class SDRBASE_API RainViewer : public QObject
{
Q_OBJECT
public:
RainViewer();
~RainViewer();
void getPath();
void getPathPeriodically(int periodInMins=15);
public slots:
void update();
void handleReply(QNetworkReply* reply);
signals:
void pathUpdated(const QString& radarPath, const QString& satellitePath); // Emitted when paths to new data are available.
private:
QTimer m_timer; // Timer for periodic updates
QNetworkAccessManager *m_networkManager;
};
#endif /* INCLUDE_RAINVIEWER_H */

Wyświetl plik

@ -0,0 +1,164 @@
///////////////////////////////////////////////////////////////////////////////////
// Copyright (C) 2024 Jon Beniston, M7RCE <jon@beniston.com> //
// //
// This program is free software; you can redistribute it and/or modify //
// it under the terms of the GNU General Public License as published by //
// the Free Software Foundation as version 3 of the License, or //
// (at your option) any later version. //
// //
// This program is distributed in the hope that it will be useful, //
// but WITHOUT ANY WARRANTY; without even the implied warranty of //
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the //
// GNU General Public License V3 for more details. //
// //
// You should have received a copy of the GNU General Public License //
// along with this program. If not, see <http://www.gnu.org/licenses/>. //
///////////////////////////////////////////////////////////////////////////////////
#include "spyserverlist.h"
#include <QDebug>
#include <QUrl>
#include <QUrlQuery>
#include <QNetworkReply>
#include <QXmlStreamReader>
#include <QNetworkDiskCache>
#include <QRegularExpression>
SpyServerList::SpyServerList()
{
m_networkManager = new QNetworkAccessManager();
QObject::connect(m_networkManager, &QNetworkAccessManager::finished, this, &SpyServerList::handleReply);
QStringList locations = QStandardPaths::standardLocations(QStandardPaths::AppDataLocation);
QDir writeableDir(locations[0]);
if (!writeableDir.mkpath(QStringLiteral("cache") + QDir::separator() + QStringLiteral("spyserver"))) {
qDebug() << "Failed to create cache/spyserver";
}
m_cache = new QNetworkDiskCache();
m_cache->setCacheDirectory(locations[0] + QDir::separator() + QStringLiteral("cache") + QDir::separator() + QStringLiteral("spyserver"));
m_cache->setMaximumCacheSize(100000000);
m_networkManager->setCache(m_cache);
connect(&m_timer, &QTimer::timeout, this, &SpyServerList::update);
}
SpyServerList::~SpyServerList()
{
QObject::disconnect(m_networkManager, &QNetworkAccessManager::finished, this, &SpyServerList::handleReply);
delete m_networkManager;
}
void SpyServerList::getData()
{
QUrl url(QString("https://airspy.com/directory/status.json"));
m_networkManager->get(QNetworkRequest(url));
}
void SpyServerList::getDataPeriodically(int periodInMins)
{
m_timer.setInterval(periodInMins*60*1000);
m_timer.start();
update();
}
void SpyServerList::update()
{
getData();
}
void SpyServerList::handleReply(QNetworkReply* reply)
{
if (reply)
{
if (!reply->error())
{
QString url = reply->url().toEncoded().constData();
QByteArray bytes = reply->readAll();
handleJSON(url, bytes);
}
else
{
qDebug() << "SpyServerList::handleReply: error: " << reply->error();
}
reply->deleteLater();
}
else
{
qDebug() << "SpyServerList::handleReply: reply is null";
}
}
void SpyServerList::handleJSON(const QString& url, const QByteArray& bytes)
{
QList<SpyServer> sdrs;
QJsonDocument document = QJsonDocument::fromJson(bytes);
if (document.isObject())
{
QJsonObject obj = document.object();
if (obj.contains(QStringLiteral("servers")))
{
QJsonArray servers = obj.value(QStringLiteral("servers")).toArray();
for (auto valRef : servers)
{
if (valRef.isObject())
{
QJsonObject serverObj = valRef.toObject();
SpyServer sdr;
if (serverObj.contains(QStringLiteral("generalDescription"))) {
sdr.m_generalDescription = serverObj.value(QStringLiteral("generalDescription")).toString();
}
if (serverObj.contains(QStringLiteral("deviceType"))) {
sdr.m_deviceType = serverObj.value(QStringLiteral("deviceType")).toString();
}
if (serverObj.contains(QStringLiteral("streamingHost"))) {
sdr.m_streamingHost = serverObj.value(QStringLiteral("streamingHost")).toString();
}
if (serverObj.contains(QStringLiteral("streamingPort"))) {
sdr.m_streamingPort = serverObj.value(QStringLiteral("streamingPort")).toInt();
}
if (serverObj.contains(QStringLiteral("currentClientCount"))) {
sdr.m_currentClientCount = serverObj.value(QStringLiteral("currentClientCount")).toInt();
}
if (serverObj.contains(QStringLiteral("maxClients"))) {
sdr.m_maxClients = serverObj.value(QStringLiteral("maxClients")).toInt();
}
if (serverObj.contains(QStringLiteral("antennaType"))) {
sdr.m_antennaType = serverObj.value(QStringLiteral("antennaType")).toString();
}
if (serverObj.contains(QStringLiteral("antennaLocation")))
{
QJsonObject location = serverObj.value(QStringLiteral("antennaLocation")).toObject();
sdr.m_latitude = location.value(QStringLiteral("lat")).toDouble();
sdr.m_longitude = location.value(QStringLiteral("long")).toDouble();
}
if (serverObj.contains(QStringLiteral("minimumFrequency"))) {
sdr.m_minimumFrequency = serverObj.value(QStringLiteral("minimumFrequency")).toInt();
}
if (serverObj.contains(QStringLiteral("maximumFrequency"))) {
sdr.m_maximumFrequency = serverObj.value(QStringLiteral("maximumFrequency")).toInt();
}
if (serverObj.contains(QStringLiteral("fullControlAllowed"))) {
sdr.m_fullControlAllowed = serverObj.value(QStringLiteral("fullControlAllowed")).toBool();
}
if (serverObj.contains(QStringLiteral("online"))) {
sdr.m_online = serverObj.value(QStringLiteral("online")).toBool();
}
sdrs.append(sdr);
}
}
}
}
else
{
qDebug() << "SpyServerList::handleJSON: Doc doesn't contain an object:\n" << document;
}
emit dataUpdated(sdrs);
}

Wyświetl plik

@ -0,0 +1,75 @@
///////////////////////////////////////////////////////////////////////////////////
// Copyright (C) 2024 Jon Beniston, M7RCE <jon@beniston.com> //
// //
// This program is free software; you can redistribute it and/or modify //
// it under the terms of the GNU General Public License as published by //
// the Free Software Foundation as version 3 of the License, or //
// (at your option) any later version. //
// //
// This program is distributed in the hope that it will be useful, //
// but WITHOUT ANY WARRANTY; without even the implied warranty of //
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the //
// GNU General Public License V3 for more details. //
// //
// You should have received a copy of the GNU General Public License //
// along with this program. If not, see <http://www.gnu.org/licenses/>. //
///////////////////////////////////////////////////////////////////////////////////
#ifndef INCLUDE_SPYSERVERLIST_H
#define INCLUDE_SPYSERVERLIST_H
#include <QtCore>
#include <QTimer>
#include "export.h"
class QNetworkAccessManager;
class QNetworkReply;
class QNetworkDiskCache;
// Gets a list of public SpyServers from https://airspy.com/directory/status.json
class SDRBASE_API SpyServerList : public QObject
{
Q_OBJECT
public:
struct SpyServer {
QString m_generalDescription;
QString m_deviceType;
QString m_streamingHost; // IP addrss
int m_streamingPort; // IP port
int m_currentClientCount;
int m_maxClients;
QString m_antennaType;
float m_latitude;
float m_longitude;
qint64 m_minimumFrequency;
qint64 m_maximumFrequency;
bool m_fullControlAllowed;
bool m_online;
};
SpyServerList();
~SpyServerList();
void getData();
void getDataPeriodically(int periodInMins=2);
public slots:
void handleReply(QNetworkReply* reply);
void update();
signals:
void dataUpdated(const QList<SpyServer>& sdrs); // Emitted when data are available.
private:
QNetworkAccessManager *m_networkManager;
QNetworkDiskCache *m_cache;
QTimer m_timer; // Timer for periodic updates
void handleJSON(const QString& url, const QByteArray& bytes);
};
#endif /* INCLUDE_SPYSERVERLIST_H */

Wyświetl plik

@ -0,0 +1,141 @@
///////////////////////////////////////////////////////////////////////////////////
// Copyright (C) 2024 Jon Beniston, M7RCE <jon@beniston.com> //
// //
// This program is free software; you can redistribute it and/or modify //
// it under the terms of the GNU General Public License as published by //
// the Free Software Foundation as version 3 of the License, or //
// (at your option) any later version. //
// //
// This program is distributed in the hope that it will be useful, //
// but WITHOUT ANY WARRANTY; without even the implied warranty of //
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the //
// GNU General Public License V3 for more details. //
// //
// You should have received a copy of the GNU General Public License //
// along with this program. If not, see <http://www.gnu.org/licenses/>. //
///////////////////////////////////////////////////////////////////////////////////
#include <QDebug>
#include "waypoints.h"
#include "csv.h"
QHash<QString, Waypoint *> *Waypoint::readCSV(const QString &filename)
{
QHash<QString, Waypoint *> *waypoints = new QHash<QString, Waypoint *>();
QFile file(filename);
if (file.open(QIODevice::ReadOnly | QIODevice::Text))
{
QTextStream in(&file);
QString error;
QStringList cols;
while(CSV::readRow(in, &cols))
{
Waypoint *waypoint = new Waypoint();
waypoint->m_name = cols[0];
waypoint->m_latitude = cols[1].toFloat();
waypoint->m_longitude = cols[2].toFloat();
waypoints->insert(waypoint->m_name, waypoint);
}
file.close();
}
else
{
qDebug() << "Waypoint::readCSV: Could not open " << filename << " for reading.";
}
return waypoints;
}
QSharedPointer<QHash<QString, Waypoint *>> Waypoints::m_waypoints;
QDateTime Waypoints::m_waypointsModifiedDateTime;
Waypoints::Waypoints(QObject *parent) :
QObject(parent)
{
connect(&m_dlm, &HttpDownloadManager::downloadComplete, this, &Waypoints::downloadFinished);
}
Waypoints::~Waypoints()
{
disconnect(&m_dlm, &HttpDownloadManager::downloadComplete, this, &Waypoints::downloadFinished);
}
QString Waypoints::getDataDir()
{
// Get directory to store app data in
QStringList locations = QStandardPaths::standardLocations(QStandardPaths::AppDataLocation);
// First dir is writable
return locations[0];
}
QString Waypoints::getWaypointsFilename()
{
return getDataDir() + "/" + "waypoints.csv";
}
void Waypoints::downloadWaypoints()
{
QString filename = getWaypointsFilename();
QString urlString = WAYPOINTS_URL;
QUrl dbURL(urlString);
qDebug() << "Waypoints::downloadWaypoints: Downloading " << urlString;
emit downloadingURL(urlString);
m_dlm.download(dbURL, filename);
}
void Waypoints::downloadFinished(const QString& filename, bool success)
{
if (!success) {
qDebug() << "Waypoints::downloadFinished: Failed: " << filename;
}
if (filename == getWaypointsFilename())
{
emit downloadWaypointsFinished();
}
else
{
qDebug() << "Waypoints::downloadFinished: Unexpected filename: " << filename;
emit downloadError(QString("Unexpected filename: %1").arg(filename));
}
}
// Read waypoints
QHash<QString, Waypoint *> *Waypoints::readWaypoints()
{
return Waypoint::readCSV(getWaypointsFilename());
}
QSharedPointer<const QHash<QString, Waypoint *>> Waypoints::getWaypoints()
{
QDateTime filesDateTime = getWaypointsModifiedDateTime();
if (!m_waypoints || (filesDateTime > m_waypointsModifiedDateTime))
{
// Using shared pointer, so old object, if it exists, will be deleted, when no longer used
m_waypoints = QSharedPointer<QHash<QString, Waypoint *>>(readWaypoints());
m_waypointsModifiedDateTime = filesDateTime;
}
return m_waypoints;
}
// Gets the date and time the waypoint file was most recently modified
QDateTime Waypoints::getWaypointsModifiedDateTime()
{
QFileInfo fileInfo(getWaypointsFilename());
return fileInfo.lastModified();
}
// Find a waypoint by name
const Waypoint *Waypoints::findWayPoint(const QString& name)
{
if (m_waypoints->contains(name)) {
return m_waypoints->value(name);
} else {
return nullptr;
}
}

Wyświetl plik

@ -0,0 +1,80 @@
///////////////////////////////////////////////////////////////////////////////////
// Copyright (C) 2021-2024 Jon Beniston, M7RCE <jon@beniston.com> //
// //
// This program is free software; you can redistribute it and/or modify //
// it under the terms of the GNU General Public License as published by //
// the Free Software Foundation as version 3 of the License, or //
// (at your option) any later version. //
// //
// This program is distributed in the hope that it will be useful, //
// but WITHOUT ANY WARRANTY; without even the implied warranty of //
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the //
// GNU General Public License V3 for more details. //
// //
// You should have received a copy of the GNU General Public License //
// along with this program. If not, see <http://www.gnu.org/licenses/>. //
///////////////////////////////////////////////////////////////////////////////////
#ifndef INCLUDE_WAYPOINTS_H
#define INCLUDE_WAYPOINTS_H
#include <QString>
#include <QFile>
#include <QHash>
#include <stdio.h>
#include <string.h>
#include "export.h"
#include "util/units.h"
#include "util/httpdownloadmanager.h"
#define WAYPOINTS_URL "https://github.com/srcejon/aviationwaypoints/waypoints.csv"
// Aviation waypoints
struct SDRBASE_API Waypoint {
QString m_name; // Typically 5 characters
float m_latitude;
float m_longitude;
static QHash<QString, Waypoint *> *readCSV(const QString &filename);
};
class SDRBASE_API Waypoints : public QObject {
Q_OBJECT
public:
Waypoints(QObject* parent = nullptr);
~Waypoints();
void downloadWaypoints();
static const Waypoint* findWayPoint(const QString& name);
static QSharedPointer<const QHash<QString, Waypoint *>> getWaypoints();
private:
HttpDownloadManager m_dlm;
static QSharedPointer<QHash<QString, Waypoint *>> m_waypoints;
static QDateTime m_waypointsModifiedDateTime;
static QHash<QString, Waypoint *> *readWaypoints();
static QString getDataDir();
static QString getWaypointsFilename();
static QDateTime getWaypointsModifiedDateTime();
public slots:
void downloadFinished(const QString& filename, bool success);
signals:
void downloadingURL(const QString& url);
void downloadError(const QString& error);
void downloadWaypointsFinished();
};
#endif // INCLUDE_WAYPOINTS_H

Wyświetl plik

@ -1072,6 +1072,7 @@ void MainWindow::removeDeviceSet(int deviceSetIndex)
m_deviceUIs.erase(m_deviceUIs.begin() + deviceSetIndex);
m_mainCore->removeDeviceSet(deviceSetIndex);
DeviceEnumerator::instance()->renumeratetabIndex(deviceSetIndex);
// Renumerate
for (int i = 0; i < (int) m_deviceUIs.size(); i++)

Wyświetl plik

@ -43,6 +43,9 @@ RemoteTCPInputSettings:
type: integer
preFill:
type: integer
protocol:
description: (SDRangel or Spy Server)
type: string
useReverseAPI:
description: Synchronize with reverse API (1 for yes, 0 for no)
type: integer

Wyświetl plik

@ -13104,6 +13104,10 @@ margin-bottom: 20px;
"preFill" : {
"type" : "integer"
},
"protocol" : {
"type" : "string",
"description" : "(SDRangel or Spy Server)"
},
"useReverseAPI" : {
"type" : "integer",
"description" : "Synchronize with reverse API (1 for yes, 0 for no)"
@ -58668,7 +58672,7 @@ except ApiException as e:
</div>
<div id="generator">
<div class="content">
Generated 2024-02-12T10:33:45.606+01:00
Generated 2024-02-27T15:07:08.970+01:00
</div>
</div>
</div>

Wyświetl plik

@ -68,6 +68,8 @@ SWGRemoteTCPInputSettings::SWGRemoteTCPInputSettings() {
m_override_remote_settings_isSet = false;
pre_fill = 0;
m_pre_fill_isSet = false;
protocol = nullptr;
m_protocol_isSet = false;
use_reverse_api = 0;
m_use_reverse_api_isSet = false;
reverse_api_address = nullptr;
@ -124,6 +126,8 @@ SWGRemoteTCPInputSettings::init() {
m_override_remote_settings_isSet = false;
pre_fill = 0;
m_pre_fill_isSet = false;
protocol = new QString("");
m_protocol_isSet = false;
use_reverse_api = 0;
m_use_reverse_api_isSet = false;
reverse_api_address = new QString("");
@ -158,6 +162,9 @@ SWGRemoteTCPInputSettings::cleanup() {
if(protocol != nullptr) {
delete protocol;
}
if(reverse_api_address != nullptr) {
delete reverse_api_address;
@ -217,6 +224,8 @@ SWGRemoteTCPInputSettings::fromJsonObject(QJsonObject &pJson) {
::SWGSDRangel::setValue(&pre_fill, pJson["preFill"], "qint32", "");
::SWGSDRangel::setValue(&protocol, pJson["protocol"], "QString", "QString");
::SWGSDRangel::setValue(&use_reverse_api, pJson["useReverseAPI"], "qint32", "");
::SWGSDRangel::setValue(&reverse_api_address, pJson["reverseAPIAddress"], "QString", "QString");
@ -301,6 +310,9 @@ SWGRemoteTCPInputSettings::asJsonObject() {
if(m_pre_fill_isSet){
obj->insert("preFill", QJsonValue(pre_fill));
}
if(protocol != nullptr && *protocol != QString("")){
toJsonValue(QString("protocol"), protocol, obj, QString("QString"));
}
if(m_use_reverse_api_isSet){
obj->insert("useReverseAPI", QJsonValue(use_reverse_api));
}
@ -517,6 +529,16 @@ SWGRemoteTCPInputSettings::setPreFill(qint32 pre_fill) {
this->m_pre_fill_isSet = true;
}
QString*
SWGRemoteTCPInputSettings::getProtocol() {
return protocol;
}
void
SWGRemoteTCPInputSettings::setProtocol(QString* protocol) {
this->protocol = protocol;
this->m_protocol_isSet = true;
}
qint32
SWGRemoteTCPInputSettings::getUseReverseApi() {
return use_reverse_api;
@ -622,6 +644,9 @@ SWGRemoteTCPInputSettings::isSet(){
if(m_pre_fill_isSet){
isObjectUpdated = true; break;
}
if(protocol && *protocol != QString("")){
isObjectUpdated = true; break;
}
if(m_use_reverse_api_isSet){
isObjectUpdated = true; break;
}

Wyświetl plik

@ -102,6 +102,9 @@ public:
qint32 getPreFill();
void setPreFill(qint32 pre_fill);
QString* getProtocol();
void setProtocol(QString* protocol);
qint32 getUseReverseApi();
void setUseReverseApi(qint32 use_reverse_api);
@ -178,6 +181,9 @@ private:
qint32 pre_fill;
bool m_pre_fill_isSet;
QString* protocol;
bool m_protocol_isSet;
qint32 use_reverse_api;
bool m_use_reverse_api_isSet;