From 3dfedcf8d1c99e4a9eabedb75115192ccd779995 Mon Sep 17 00:00:00 2001 From: "dennis.sheirer" Date: Sat, 26 Apr 2014 19:48:24 +0000 Subject: [PATCH] Initial code check in continued --- src/alias/Alias.java | 126 + src/alias/AliasDirectory.java | 82 + src/alias/AliasDirectoryNode.java | 91 + src/alias/AliasEditor.java | 205 ++ src/alias/AliasID.java | 41 + src/alias/AliasIDNode.java | 35 + src/alias/AliasIDType.java | 31 + src/alias/AliasList.java | 409 +++ src/alias/AliasListNode.java | 126 + src/alias/AliasNode.java | 373 +++ src/alias/Group.java | 73 + src/alias/GroupEditor.java | 110 + src/alias/GroupNode.java | 126 + src/alias/ListEditor.java | 107 + src/alias/esn/ESNEditor.java | 100 + src/alias/esn/ESNNode.java | 86 + src/alias/esn/Esn.java | 72 + src/alias/fleetsync/FleetsyncID.java | 73 + src/alias/fleetsync/FleetsyncIDEditor.java | 109 + src/alias/fleetsync/FleetsyncIDNode.java | 97 + src/alias/fleetsync/StatusID.java | 69 + src/alias/fleetsync/StatusIDEditor.java | 114 + src/alias/fleetsync/StatusIDNode.java | 86 + src/alias/mdc/MDC1200ID.java | 73 + src/alias/mdc/MDC1200IDEditor.java | 99 + src/alias/mdc/MDC1200IDNode.java | 91 + src/alias/mobileID/MINEditor.java | 100 + src/alias/mobileID/MINNode.java | 86 + src/alias/mobileID/Min.java | 75 + src/alias/mpt1327/MPT1327ID.java | 72 + src/alias/mpt1327/MPT1327IDEditor.java | 99 + src/alias/mpt1327/MPT1327IDNode.java | 91 + src/alias/siteID/SiteID.java | 69 + src/alias/siteID/SiteIDEditor.java | 114 + src/alias/siteID/SiteIDNode.java | 86 + src/alias/talkgroup/TalkgroupID.java | 102 + src/alias/talkgroup/TalkgroupIDEditor.java | 151 + src/alias/talkgroup/TalkgroupIDNode.java | 86 + src/alias/uniqueID/UniqueID.java | 69 + src/alias/uniqueID/UniqueIDEditor.java | 114 + src/alias/uniqueID/UniqueIDNode.java | 86 + src/audio/AudioAdapter.java | 97 + src/audio/AudioInverter.java | 82 + src/audio/AudioOutput.java | 116 + src/audio/AudioType.java | 87 + src/audio/AudioTypeListener.java | 23 + src/audio/InversionFrequency.java | 59 + src/audio/SquelchListener.java | 31 + src/bits/BitSetBuffer.java | 322 +++ src/bits/BitSetFullException.java | 28 + src/bits/MessageFramer.java | 232 ++ src/bits/SyncPattern.java | 118 + src/bits/SyncPatternMatcher.java | 74 + .../AveragingCircularBooleanBuffer.java | 107 + src/buffer/BufferOverflowException.java | 33 + src/buffer/BufferOverflowListener.java | 23 + src/buffer/ByteArrayDelayBuffer.java | 60 + src/buffer/ByteArrayListener.java | 23 + src/buffer/ByteBufferListener.java | 25 + src/buffer/ByteBufferProvider.java | 33 + src/buffer/ComplexByteBuffer.java | 91 + src/buffer/ComplexByteBufferListener.java | 23 + src/buffer/ComplexCircularBuffer.java | 90 + src/buffer/ComplexSampleBufferAssembler.java | 73 + src/buffer/ComplexTappedCircularBuffer.java | 87 + .../FloatArrayCircularAveragingBuffer.java | 97 + src/buffer/FloatAveragingBuffer.java | 57 + src/buffer/FloatBufferAssembler.java | 70 + src/buffer/FloatCircularAveragingBuffer.java | 110 + src/buffer/FloatCircularAveragingBuffer2.java | 68 + src/buffer/FloatCircularBuffer.java | 108 + src/buffer/FloatSampleBuffer.java | 66 + src/buffer/FloatSampleBufferAssembler.java | 72 + src/buffer/FloatTappedCircularBuffer.java | 86 + src/buffer/FloatToByteBufferAssembler.java | 69 + src/buffer/ShortComplexByteBuffer.java | 53 + src/buffer/StereoByteBuffer.java | 59 + src/buffer/StereoByteBufferListener.java | 24 + src/buffer/StereoByteBufferProvider.java | 32 + .../StereoToMonoByteBufferConverter.java | 58 + src/controller/BaseNode.java | 177 ++ src/controller/ConfigurableNode.java | 44 + .../ConfigurationControllerModel.java | 280 ++ src/controller/ConfigurationEditor.java | 75 + src/controller/ConfigurationTreePanel.java | 117 + src/controller/ControllerPanel.java | 134 + .../ControllerTreeCellRenderer.java | 106 + src/controller/Editor.java | 36 + src/controller/EmptyEditor.java | 59 + src/controller/ResourceManager.java | 159 ++ src/controller/ThreadPoolManager.java | 97 + .../activity/ActivitySummaryFrame.java | 69 + .../activity/ActivitySummaryProvider.java | 26 + src/controller/activity/CallEvent.java | 226 ++ .../activity/CallEventAliasCellRenderer.java | 72 + src/controller/activity/CallEventModel.java | 176 ++ src/controller/activity/CallEventPanel.java | 84 + .../activity/MessageActivityModel.java | 155 ++ .../activity/MessageActivityPanel.java | 82 + .../activity/MessageDetailsProvider.java | 82 + .../channel/AbstractChannelEditor.java | 42 + src/controller/channel/Channel.java | 902 ++++++ src/controller/channel/ChannelEditor.java | 235 ++ src/controller/channel/ChannelListener.java | 32 + src/controller/channel/ChannelManager.java | 127 + src/controller/channel/ChannelMap.java | 120 + src/controller/channel/ChannelMapEditor.java | 223 ++ src/controller/channel/ChannelMapList.java | 76 + .../channel/ChannelMapListNode.java | 87 + src/controller/channel/ChannelMapModel.java | 167 ++ src/controller/channel/ChannelMapNode.java | 102 + src/controller/channel/ChannelNode.java | 168 ++ src/controller/channel/ChannelRange.java | 122 + .../channel/ChannelSelectionListener.java | 23 + src/controller/channel/ProcessingChain.java | 721 +++++ src/controller/config/Configuration.java | 40 + src/controller/site/Site.java | 86 + src/controller/site/SiteEditor.java | 101 + src/controller/site/SiteNode.java | 167 ++ src/controller/state/AuxChannelState.java | 80 + src/controller/state/AuxStatePanel.java | 143 + src/controller/state/ChannelState.java | 664 +++++ src/controller/state/ChannelStateList.java | 442 +++ src/controller/state/ChannelStatePanel.java | 386 +++ .../state/ChannelStateSelectionListener.java | 24 + src/controller/system/System.java | 85 + src/controller/system/SystemEditor.java | 101 + src/controller/system/SystemList.java | 60 + src/controller/system/SystemListNode.java | 85 + src/controller/system/SystemNode.java | 165 ++ src/crc/CRC.java | 68 + src/crc/CRCFleetsync.java | 207 ++ src/crc/CRCLTR.java | 185 ++ src/crc/CRCPassport.java | 215 ++ src/crc/CRCUtil.java | 58 + src/crc/Checksum.java | 52 + src/crc/ChecksumTest.java | 96 + src/crc/ChecksumType.java | 223 ++ src/decode/AuxDecodeComponentEditor.java | 110 + src/decode/DecodeComponentEditor.java | 134 + src/decode/DecodeEditor.java | 52 + src/decode/DecodeEditorFactory.java | 59 + src/decode/Decoder.java | 148 + src/decode/DecoderFactory.java | 181 ++ src/decode/DecoderType.java | 110 + src/decode/config/AuxDecodeConfiguration.java | 60 + src/decode/config/DecodeConfigFactory.java | 58 + src/decode/config/DecodeConfigLTRNet.java | 44 + .../config/DecodeConfigLTRStandard.java | 44 + src/decode/config/DecodeConfigMPT1327.java | 43 + src/decode/config/DecodeConfigNBFM.java | 28 + src/decode/config/DecodeConfigPassport.java | 28 + src/decode/config/DecodeConfiguration.java | 50 + src/decode/fleetsync1/Fleetsync1Message.java | 175 ++ .../Fleetsync1MessageProcessor.java | 49 + src/decode/fleetsync2/Fleetsync2Decoder.java | 184 ++ .../Fleetsync2MessageProcessor.java | 59 + .../fleetsync2/FleetsyncActivitySummary.java | 171 ++ src/decode/fleetsync2/FleetsyncCallEvent.java | 188 ++ .../fleetsync2/FleetsyncChannelState.java | 195 ++ src/decode/fleetsync2/FleetsyncMessage.java | 863 ++++++ .../fleetsync2/FleetsyncMessageType.java | 42 + src/decode/fleetsync2/FleetsyncPanel.java | 167 ++ src/decode/ltrnet/LTRCallEvent.java | 184 ++ src/decode/ltrnet/LTRNetActivitySummary.java | 424 +++ src/decode/ltrnet/LTRNetChannelState.java | 331 +++ src/decode/ltrnet/LTRNetDecoder.java | 118 + src/decode/ltrnet/LTRNetEditor.java | 70 + src/decode/ltrnet/LTRNetISWMessage.java | 312 +++ src/decode/ltrnet/LTRNetMessage.java | 243 ++ src/decode/ltrnet/LTRNetMessageProcessor.java | 147 + src/decode/ltrnet/LTRNetOSWMessage.java | 579 ++++ src/decode/ltrnet/LTRNetPanel.java | 229 ++ src/decode/ltrstandard/LTRChannelState.java | 208 ++ src/decode/ltrstandard/LTRFSKDemodulator.java | 132 + .../ltrstandard/LTRFSKDemodulator2.java | 122 + src/decode/ltrstandard/LTRPanel.java | 219 ++ .../LTRStandardActivitySummary.java | 209 ++ .../ltrstandard/LTRStandardDecoder.java | 117 + src/decode/ltrstandard/LTRStandardEditor.java | 70 + .../ltrstandard/LTRStandardISWMessage.java | 212 ++ .../ltrstandard/LTRStandardMessage.java | 256 ++ .../LTRStandardMessageProcessor.java | 67 + .../ltrstandard/LTRStandardOSWMessage.java | 222 ++ src/decode/mdc1200/MDCActivitySummary.java | 131 + src/decode/mdc1200/MDCCallEvent.java | 159 ++ src/decode/mdc1200/MDCChannelState.java | 183 ++ src/decode/mdc1200/MDCDecoder.java | 202 ++ src/decode/mdc1200/MDCMessage.java | 319 +++ src/decode/mdc1200/MDCMessageProcessor.java | 155 ++ src/decode/mdc1200/MDCMessageType.java | 40 + src/decode/mdc1200/MDCPanel.java | 161 ++ .../mpt1327/MPT1327ActivitySummary.java | 171 ++ src/decode/mpt1327/MPT1327CallEvent.java | 154 ++ src/decode/mpt1327/MPT1327ChannelState.java | 346 +++ src/decode/mpt1327/MPT1327Decoder.java | 283 ++ src/decode/mpt1327/MPT1327Editor.java | 103 + src/decode/mpt1327/MPT1327Message.java | 986 +++++++ .../mpt1327/MPT1327MessageProcessor.java | 59 + src/decode/mpt1327/MPT1327Panel.java | 282 ++ src/decode/mpt1327/MPT1327TrafficPanel.java | 258 ++ src/decode/nbfm/NBFMChannelState.java | 69 + src/decode/nbfm/NBFMConventionalDecoder.java | 77 + src/decode/nbfm/NBFMEditor.java | 31 + src/decode/nbfm/NBFMPanel.java | 136 + .../passport/PassportActivitySummary.java | 233 ++ src/decode/passport/PassportBand.java | 71 + src/decode/passport/PassportCallEvent.java | 164 ++ src/decode/passport/PassportChannelState.java | 236 ++ src/decode/passport/PassportDecoder.java | 108 + src/decode/passport/PassportEditor.java | 31 + src/decode/passport/PassportMessage.java | 634 +++++ .../passport/PassportMessageProcessor.java | 109 + src/decode/passport/PassportPanel.java | 255 ++ src/dsp/BooleanZeroCrossingDetector.java | 117 + src/dsp/FloatToBinarySlicer.java | 69 + src/dsp/FloatToBinarySlicerLTR.java | 137 + src/dsp/NRZDecoder.java | 77 + src/dsp/SyncDetectListener.java | 27 + src/dsp/SyncDetectProvider.java | 23 + src/dsp/filter/BooleanAveragingFilter.java | 110 + src/dsp/filter/ComplexCICDecimate.java | 341 +++ src/dsp/filter/ComplexFIRFilter.java | 160 ++ src/dsp/filter/ComplexFilter.java | 54 + src/dsp/filter/ComplexHalfBandFilter.java | 186 ++ .../ComplexHalfBandNoDecimateFilter.java | 180 ++ .../ComplexPolyphaseDecimationFilter.java | 177 ++ src/dsp/filter/DCRemovalFilter.java | 65 + src/dsp/filter/DCRemovalFilter2.java | 56 + src/dsp/filter/FilterFactory.java | 729 +++++ src/dsp/filter/Filters.java | 1238 +++++++++ src/dsp/filter/FloatFIRFilter.java | 177 ++ src/dsp/filter/FloatHalfBandFilter.java | 203 ++ .../filter/FloatHalfBandNoDecimateFilter.java | 193 ++ src/dsp/filter/Gain.java | 45 + src/dsp/filter/GoertzelFilter.java | 161 ++ src/dsp/filter/IIRBiQuadraticFilter.java | 197 ++ src/dsp/filter/SquaringFilter.java | 51 + src/dsp/filter/Window.java | 239 ++ src/dsp/fsk/FSK2Decoder.java | 287 ++ src/dsp/fsk/SymbolEvent.java | 75 + src/dsp/nbfm/FMDiscriminator.java | 100 + src/dsp/nbfm/FilteringNBFMDemodulator.java | 92 + src/dsp/nbfm/NBFMDemodulator.java | 92 + src/eventlog/CallEventLogger.java | 43 + src/eventlog/EventLogComponentEditor.java | 98 + src/eventlog/EventLogManager.java | 60 + src/eventlog/EventLogType.java | 51 + src/eventlog/EventLogger.java | 111 + src/eventlog/MessageEventLogger.java | 65 + .../config/EventLogConfiguration.java | 57 + src/gui/SDRTrunk.java | 318 +++ src/gui/control/JFrequencyControl.java | 423 +++ src/instrument/Instrumentable.java | 53 + src/instrument/gui/BinaryTapViewPanel.java | 152 + src/instrument/gui/DecoderViewFrame.java | 232 ++ src/instrument/gui/FloatTapViewPanel.java | 119 + src/instrument/gui/SampleModel.java | 78 + .../gui/SymbolEventTapViewPanel.java | 186 ++ src/instrument/gui/TapViewPanel.java | 129 + src/instrument/gui/Viewer.java | 234 ++ src/instrument/gui/WaveSourceFrame.java | 242 ++ src/instrument/tap/Tap.java | 96 + src/instrument/tap/TapListener.java | 23 + src/instrument/tap/TapType.java | 27 + src/instrument/tap/event/SyncDetectTap.java | 60 + src/instrument/tap/stream/BinaryTap.java | 59 + src/instrument/tap/stream/ComplexTap.java | 60 + src/instrument/tap/stream/FloatTap.java | 58 + src/instrument/tap/stream/StreamTap.java | 41 + src/instrument/tap/stream/SymbolEventTap.java | 58 + src/log/Log.java | 233 ++ src/map/DefaultIcon.java | 36 + src/map/IconManager.java | 494 ++++ src/map/MapIcon.java | 188 ++ src/map/MapIconListCellRenderer.java | 57 + src/map/MapMouseListener.java | 183 ++ src/map/MapPanel.java | 148 + src/map/MapService.java | 183 ++ src/map/Plottable.java | 139 + src/map/PlottableEntity.java | 171 ++ src/map/PlottableEntityPainter.java | 71 + src/map/PlottableEntityRenderer.java | 169 ++ src/map/PlottableUpdateListener.java | 27 + src/map/SelectionAdapter.java | 103 + src/map/SelectionPainter.java | 58 + src/message/Message.java | 63 + src/message/MessageDirection.java | 24 + src/message/MessageType.java | 74 + src/org/jdesktop/beans/AbstractBean.java | 504 ++++ .../beans/AbstractSerializableBean.java | 104 + src/org/jdesktop/swingx/JXMapKit.form | 145 + src/org/jdesktop/swingx/JXMapKit.java | 724 +++++ src/org/jdesktop/swingx/JXMapViewer.java | 781 ++++++ .../jdesktop/swingx/OSMTileFactoryInfo.java | 59 + .../swingx/VirtualEarthTileFactoryInfo.java | 134 + .../swingx/input/CenterMapListener.java | 72 + .../jdesktop/swingx/input/PanKeyListener.java | 76 + .../swingx/input/PanMouseInputListener.java | 106 + .../input/ZoomMouseWheelListenerCenter.java | 46 + .../input/ZoomMouseWheelListenerCursor.java | 71 + .../swingx/mapviewer/AbstractTileFactory.java | 401 +++ .../swingx/mapviewer/DefaultTileFactory.java | 28 + .../swingx/mapviewer/DefaultWaypoint.java | 63 + .../mapviewer/DefaultWaypointRenderer.java | 58 + .../jdesktop/swingx/mapviewer/GeoBounds.java | 158 ++ .../swingx/mapviewer/GeoPosition.java | 113 + .../swingx/mapviewer/LocalResponseCache.java | 322 +++ src/org/jdesktop/swingx/mapviewer/Tile.java | 214 ++ .../jdesktop/swingx/mapviewer/TileCache.java | 141 + .../swingx/mapviewer/TileFactory.java | 144 + .../swingx/mapviewer/TileFactoryInfo.java | 325 +++ .../swingx/mapviewer/TileListener.java | 32 + .../jdesktop/swingx/mapviewer/Waypoint.java | 25 + .../swingx/mapviewer/WaypointPainter.java | 90 + .../swingx/mapviewer/WaypointRenderer.java | 32 + .../CylindricalProjectionTileFactory.java | 101 + .../mapviewer/bmng/SLMapServerInfo.java | 129 + .../mapviewer/empty/EmptyTileFactory.java | 103 + .../mapviewer/esri/ESRITileFactory.java | 98 + .../swingx/mapviewer/resources/loading.png | Bin 0 -> 3318 bytes .../swingx/mapviewer/resources/minus.png | Bin 0 -> 75 bytes .../swingx/mapviewer/resources/plus.png | Bin 0 -> 82 bytes .../mapviewer/resources/standard_waypoint.png | Bin 0 -> 1152 bytes .../swingx/mapviewer/util/GeoUtil.java | 167 ++ .../swingx/mapviewer/util/MercatorUtils.java | 78 + .../swingx/mapviewer/wms/WMSService.java | 113 + .../swingx/mapviewer/wms/WMSTileFactory.java | 42 + .../swingx/painter/AbstractPainter.java | 441 +++ .../swingx/painter/CompoundPainter.java | 497 ++++ src/org/jdesktop/swingx/painter/Painter.java | 104 + .../swingx/util/GraphicsUtilities.java | 871 ++++++ src/org/jdesktop/swingx/util/ShapeUtils.java | 159 ++ src/playlist/Playlist.java | 77 + src/playlist/PlaylistManager.java | 214 ++ src/playlist/PlaylistNode.java | 147 + src/properties/SystemProperties.java | 307 +++ src/record/RecordComponentEditor.java | 83 + src/record/RecordEditor.java | 52 + src/record/Recorder.java | 60 + src/record/RecorderManager.java | 74 + src/record/RecorderType.java | 42 + src/record/config/RecordConfiguration.java | 57 + src/record/wave/BooleanWaveRecorder.java | 173 ++ src/record/wave/ComplexWaveRecorder.java | 175 ++ src/record/wave/FloatWaveRecorder.java | 173 ++ src/record/wave/WaveRecorder.java | 134 + src/record/wave/WriteExample.java | 72 + src/sample/Broadcaster.java | 76 + src/sample/BufferOverflow.java | 36 + src/sample/ComplexToFloatSampleConverter.java | 56 + src/sample/Decimator.java | 70 + src/sample/FloatArrayToComplexAdapter.java | 60 + src/sample/Listener.java | 23 + src/sample/Provider.java | 25 + src/sample/complex/ComplexSample.java | 225 ++ .../complex/ComplexSampleAssembler.java | 72 + .../complex/ComplexSampleListListener.java | 25 + .../complex/ComplexSampleRateCounter.java | 107 + src/sample/complex/ComplexSampleUtils.java | 49 + src/sample/complex/RTLComplexSample.java | 57 + src/sample/complex/ThreadKiller.java | 33 + src/settings/ColorSetting.java | 190 ++ src/settings/ColorSettingMenuItem.java | 71 + .../ColorSettingResetAllMenuItem.java | 57 + src/settings/ColorSettingResetMenuItem.java | 57 + src/settings/FileSetting.java | 52 + src/settings/MapViewSetting.java | 79 + src/settings/Setting.java | 45 + src/settings/SettingChangeListener.java | 24 + src/settings/Settings.java | 295 ++ src/settings/SettingsManager.java | 818 ++++++ src/source/BufferReceiver.java | 27 + src/source/ComplexSource.java | 15 + src/source/EmptySourceEditor.java | 44 + src/source/FloatArraySource.java | 12 + src/source/FloatSource.java | 11 + src/source/SampleProvider.java | 28 + src/source/Source.java | 56 + src/source/SourceComponentEditor.java | 119 + src/source/SourceEditor.java | 38 + src/source/SourceEditorFactory.java | 46 + src/source/SourceException.java | 33 + src/source/SourceManager.java | 66 + src/source/SourceType.java | 44 + src/source/config/SourceConfigFactory.java | 50 + src/source/config/SourceConfigMixer.java | 73 + src/source/config/SourceConfigNone.java | 34 + src/source/config/SourceConfigTuner.java | 53 + src/source/config/SourceConfiguration.java | 59 + src/source/mixer/ChannelShortAdapter.java | 85 + src/source/mixer/MixerChannel.java | 23 + src/source/mixer/MixerEditor.java | 149 + src/source/mixer/MixerManager.java | 258 ++ src/source/mixer/MixerSource.java | 274 ++ src/source/mixer/SampleAdapter.java | 29 + src/source/mixer/ShortAdapter.java | 63 + src/source/mixer/SystemMixerDevices.java | 116 + src/source/tuner/DigitalDropChannel.java | 152 + .../tuner/FrequencyChangeBroadcaster.java | 25 + src/source/tuner/FrequencyChangeListener.java | 38 + src/source/tuner/FrequencyController.java | 220 ++ src/source/tuner/MixerTuner.java | 98 + src/source/tuner/MixerTunerDataLine.java | 42 + src/source/tuner/MixerTunerType.java | 117 + .../NumericallyControlledOscillator.java | 130 + src/source/tuner/Tuner.java | 190 ++ src/source/tuner/TunerChannel.java | 114 + src/source/tuner/TunerChannelSource.java | 273 ++ src/source/tuner/TunerClass.java | 260 ++ src/source/tuner/TunerConfiguration.java | 65 + .../tuner/TunerConfigurationAssignment.java | 64 + src/source/tuner/TunerController.java | 250 ++ src/source/tuner/TunerEditor.java | 57 + src/source/tuner/TunerGroupNode.java | 35 + src/source/tuner/TunerManager.java | 547 ++++ src/source/tuner/TunerNode.java | 127 + src/source/tuner/TunerSelectionListener.java | 23 + src/source/tuner/TunerType.java | 46 + src/source/tuner/ettus/B100Tuner.java | 106 + .../tuner/ettus/B100TunerEditorPanel.java | 50 + src/source/tuner/fcd/FCDCommand.java | 163 ++ src/source/tuner/fcd/FCDModel.java | 53 + src/source/tuner/fcd/FCDTuner.java | 125 + src/source/tuner/fcd/FCDTunerController.java | 497 ++++ .../tuner/fcd/FCDTunerDetailsPanel.java | 48 + .../fcd/proV1/FCD1TunerConfiguration.java | 145 + .../proV1/FCD1TunerConfigurationPanel.java | 667 +++++ .../tuner/fcd/proV1/FCD1TunerController.java | 698 +++++ .../tuner/fcd/proV1/FCD1TunerEditorPanel.java | 102 + .../fcd/proplusV2/FCD2TunerConfiguration.java | 82 + .../FCD2TunerConfigurationPanel.java | 377 +++ .../fcd/proplusV2/FCD2TunerController.java | 367 +++ .../fcd/proplusV2/FCD2TunerEditorPanel.java | 101 + src/source/tuner/rtl/RTL2832InfoPanel.java | 76 + .../tuner/rtl/RTL2832SampleAdapter.java | 59 + src/source/tuner/rtl/RTL2832Tuner.java | 168 ++ .../tuner/rtl/RTL2832TunerController.java | 1848 +++++++++++++ src/source/tuner/rtl/RTL2832TunerType.java | 28 + .../tuner/rtl/e4k/E4KTunerConfiguration.java | 124 + .../rtl/e4k/E4KTunerConfigurationPanel.java | 688 +++++ .../tuner/rtl/e4k/E4KTunerController.java | 2452 +++++++++++++++++ .../tuner/rtl/e4k/E4KTunerEditorPanel.java | 111 + .../rtl/r820t/R820TTunerConfiguration.java | 123 + .../r820t/R820TTunerConfigurationPanel.java | 619 +++++ .../tuner/rtl/r820t/R820TTunerController.java | 2408 ++++++++++++++++ .../rtl/r820t/R820TTunerEditorPanel.java | 93 + src/source/tuner/usb/USBTunerDevice.java | 44 + src/source/tuner/usb/USBUtils.java | 114 + src/source/wave/ComplexWaveSource.java | 300 ++ src/source/wave/FloatWaveSource.java | 290 ++ src/source/wave/WaveSource.java | 76 + src/spectrum/.directory | 4 + src/spectrum/DFTProcessor.java | 546 ++++ src/spectrum/DFTResultsListener.java | 26 + src/spectrum/DFTResultsProvider.java | 25 + src/spectrum/FFTWidth.java | 46 + src/spectrum/OverlayPanel.java | 590 ++++ src/spectrum/Pausable.java | 29 + src/spectrum/SpectralDisplayPanel.java | 785 ++++++ src/spectrum/SpectrumFrame.java | 84 + src/spectrum/SpectrumPanel.java | 257 ++ src/spectrum/WaterfallColorModel.java | 94 + src/spectrum/WaterfallPanel.java | 287 ++ src/test/TestCRC.java | 309 +++ src/util/Angle.java | 32 + src/util/ArcTangent.java | 262 ++ src/util/ColorIcon.java | 60 + src/util/Oscillator.java | 97 + src/util/TimeStamp.java | 84 + src/util/waveaudio/WavFile.java | 864 ++++++ src/util/waveaudio/WavFileException.java | 43 + src/util/waveaudio/WaveUtils.java | 149 + src/util/waveaudio/WaveWriter.java | 170 ++ 474 files changed, 76613 insertions(+) create mode 100644 src/alias/Alias.java create mode 100644 src/alias/AliasDirectory.java create mode 100644 src/alias/AliasDirectoryNode.java create mode 100644 src/alias/AliasEditor.java create mode 100644 src/alias/AliasID.java create mode 100644 src/alias/AliasIDNode.java create mode 100644 src/alias/AliasIDType.java create mode 100644 src/alias/AliasList.java create mode 100644 src/alias/AliasListNode.java create mode 100644 src/alias/AliasNode.java create mode 100644 src/alias/Group.java create mode 100644 src/alias/GroupEditor.java create mode 100644 src/alias/GroupNode.java create mode 100644 src/alias/ListEditor.java create mode 100644 src/alias/esn/ESNEditor.java create mode 100644 src/alias/esn/ESNNode.java create mode 100644 src/alias/esn/Esn.java create mode 100644 src/alias/fleetsync/FleetsyncID.java create mode 100644 src/alias/fleetsync/FleetsyncIDEditor.java create mode 100644 src/alias/fleetsync/FleetsyncIDNode.java create mode 100644 src/alias/fleetsync/StatusID.java create mode 100644 src/alias/fleetsync/StatusIDEditor.java create mode 100644 src/alias/fleetsync/StatusIDNode.java create mode 100644 src/alias/mdc/MDC1200ID.java create mode 100644 src/alias/mdc/MDC1200IDEditor.java create mode 100644 src/alias/mdc/MDC1200IDNode.java create mode 100644 src/alias/mobileID/MINEditor.java create mode 100644 src/alias/mobileID/MINNode.java create mode 100644 src/alias/mobileID/Min.java create mode 100644 src/alias/mpt1327/MPT1327ID.java create mode 100644 src/alias/mpt1327/MPT1327IDEditor.java create mode 100644 src/alias/mpt1327/MPT1327IDNode.java create mode 100644 src/alias/siteID/SiteID.java create mode 100644 src/alias/siteID/SiteIDEditor.java create mode 100644 src/alias/siteID/SiteIDNode.java create mode 100644 src/alias/talkgroup/TalkgroupID.java create mode 100644 src/alias/talkgroup/TalkgroupIDEditor.java create mode 100644 src/alias/talkgroup/TalkgroupIDNode.java create mode 100644 src/alias/uniqueID/UniqueID.java create mode 100644 src/alias/uniqueID/UniqueIDEditor.java create mode 100644 src/alias/uniqueID/UniqueIDNode.java create mode 100644 src/audio/AudioAdapter.java create mode 100644 src/audio/AudioInverter.java create mode 100644 src/audio/AudioOutput.java create mode 100644 src/audio/AudioType.java create mode 100644 src/audio/AudioTypeListener.java create mode 100644 src/audio/InversionFrequency.java create mode 100644 src/audio/SquelchListener.java create mode 100644 src/bits/BitSetBuffer.java create mode 100644 src/bits/BitSetFullException.java create mode 100644 src/bits/MessageFramer.java create mode 100644 src/bits/SyncPattern.java create mode 100644 src/bits/SyncPatternMatcher.java create mode 100644 src/buffer/AveragingCircularBooleanBuffer.java create mode 100644 src/buffer/BufferOverflowException.java create mode 100644 src/buffer/BufferOverflowListener.java create mode 100644 src/buffer/ByteArrayDelayBuffer.java create mode 100644 src/buffer/ByteArrayListener.java create mode 100644 src/buffer/ByteBufferListener.java create mode 100644 src/buffer/ByteBufferProvider.java create mode 100644 src/buffer/ComplexByteBuffer.java create mode 100644 src/buffer/ComplexByteBufferListener.java create mode 100644 src/buffer/ComplexCircularBuffer.java create mode 100644 src/buffer/ComplexSampleBufferAssembler.java create mode 100644 src/buffer/ComplexTappedCircularBuffer.java create mode 100644 src/buffer/FloatArrayCircularAveragingBuffer.java create mode 100644 src/buffer/FloatAveragingBuffer.java create mode 100644 src/buffer/FloatBufferAssembler.java create mode 100644 src/buffer/FloatCircularAveragingBuffer.java create mode 100644 src/buffer/FloatCircularAveragingBuffer2.java create mode 100644 src/buffer/FloatCircularBuffer.java create mode 100644 src/buffer/FloatSampleBuffer.java create mode 100644 src/buffer/FloatSampleBufferAssembler.java create mode 100644 src/buffer/FloatTappedCircularBuffer.java create mode 100644 src/buffer/FloatToByteBufferAssembler.java create mode 100644 src/buffer/ShortComplexByteBuffer.java create mode 100644 src/buffer/StereoByteBuffer.java create mode 100644 src/buffer/StereoByteBufferListener.java create mode 100644 src/buffer/StereoByteBufferProvider.java create mode 100644 src/buffer/StereoToMonoByteBufferConverter.java create mode 100644 src/controller/BaseNode.java create mode 100644 src/controller/ConfigurableNode.java create mode 100644 src/controller/ConfigurationControllerModel.java create mode 100644 src/controller/ConfigurationEditor.java create mode 100644 src/controller/ConfigurationTreePanel.java create mode 100644 src/controller/ControllerPanel.java create mode 100644 src/controller/ControllerTreeCellRenderer.java create mode 100644 src/controller/Editor.java create mode 100644 src/controller/EmptyEditor.java create mode 100644 src/controller/ResourceManager.java create mode 100644 src/controller/ThreadPoolManager.java create mode 100644 src/controller/activity/ActivitySummaryFrame.java create mode 100644 src/controller/activity/ActivitySummaryProvider.java create mode 100644 src/controller/activity/CallEvent.java create mode 100644 src/controller/activity/CallEventAliasCellRenderer.java create mode 100644 src/controller/activity/CallEventModel.java create mode 100644 src/controller/activity/CallEventPanel.java create mode 100644 src/controller/activity/MessageActivityModel.java create mode 100644 src/controller/activity/MessageActivityPanel.java create mode 100644 src/controller/activity/MessageDetailsProvider.java create mode 100644 src/controller/channel/AbstractChannelEditor.java create mode 100644 src/controller/channel/Channel.java create mode 100644 src/controller/channel/ChannelEditor.java create mode 100644 src/controller/channel/ChannelListener.java create mode 100644 src/controller/channel/ChannelManager.java create mode 100644 src/controller/channel/ChannelMap.java create mode 100644 src/controller/channel/ChannelMapEditor.java create mode 100644 src/controller/channel/ChannelMapList.java create mode 100644 src/controller/channel/ChannelMapListNode.java create mode 100644 src/controller/channel/ChannelMapModel.java create mode 100644 src/controller/channel/ChannelMapNode.java create mode 100644 src/controller/channel/ChannelNode.java create mode 100644 src/controller/channel/ChannelRange.java create mode 100644 src/controller/channel/ChannelSelectionListener.java create mode 100644 src/controller/channel/ProcessingChain.java create mode 100644 src/controller/config/Configuration.java create mode 100644 src/controller/site/Site.java create mode 100644 src/controller/site/SiteEditor.java create mode 100644 src/controller/site/SiteNode.java create mode 100644 src/controller/state/AuxChannelState.java create mode 100644 src/controller/state/AuxStatePanel.java create mode 100644 src/controller/state/ChannelState.java create mode 100644 src/controller/state/ChannelStateList.java create mode 100644 src/controller/state/ChannelStatePanel.java create mode 100644 src/controller/state/ChannelStateSelectionListener.java create mode 100644 src/controller/system/System.java create mode 100644 src/controller/system/SystemEditor.java create mode 100644 src/controller/system/SystemList.java create mode 100644 src/controller/system/SystemListNode.java create mode 100644 src/controller/system/SystemNode.java create mode 100644 src/crc/CRC.java create mode 100644 src/crc/CRCFleetsync.java create mode 100644 src/crc/CRCLTR.java create mode 100644 src/crc/CRCPassport.java create mode 100644 src/crc/CRCUtil.java create mode 100644 src/crc/Checksum.java create mode 100644 src/crc/ChecksumTest.java create mode 100644 src/crc/ChecksumType.java create mode 100644 src/decode/AuxDecodeComponentEditor.java create mode 100644 src/decode/DecodeComponentEditor.java create mode 100644 src/decode/DecodeEditor.java create mode 100644 src/decode/DecodeEditorFactory.java create mode 100644 src/decode/Decoder.java create mode 100644 src/decode/DecoderFactory.java create mode 100644 src/decode/DecoderType.java create mode 100644 src/decode/config/AuxDecodeConfiguration.java create mode 100644 src/decode/config/DecodeConfigFactory.java create mode 100644 src/decode/config/DecodeConfigLTRNet.java create mode 100644 src/decode/config/DecodeConfigLTRStandard.java create mode 100644 src/decode/config/DecodeConfigMPT1327.java create mode 100644 src/decode/config/DecodeConfigNBFM.java create mode 100644 src/decode/config/DecodeConfigPassport.java create mode 100644 src/decode/config/DecodeConfiguration.java create mode 100644 src/decode/fleetsync1/Fleetsync1Message.java create mode 100644 src/decode/fleetsync1/Fleetsync1MessageProcessor.java create mode 100644 src/decode/fleetsync2/Fleetsync2Decoder.java create mode 100644 src/decode/fleetsync2/Fleetsync2MessageProcessor.java create mode 100644 src/decode/fleetsync2/FleetsyncActivitySummary.java create mode 100644 src/decode/fleetsync2/FleetsyncCallEvent.java create mode 100644 src/decode/fleetsync2/FleetsyncChannelState.java create mode 100644 src/decode/fleetsync2/FleetsyncMessage.java create mode 100644 src/decode/fleetsync2/FleetsyncMessageType.java create mode 100644 src/decode/fleetsync2/FleetsyncPanel.java create mode 100644 src/decode/ltrnet/LTRCallEvent.java create mode 100644 src/decode/ltrnet/LTRNetActivitySummary.java create mode 100644 src/decode/ltrnet/LTRNetChannelState.java create mode 100644 src/decode/ltrnet/LTRNetDecoder.java create mode 100644 src/decode/ltrnet/LTRNetEditor.java create mode 100644 src/decode/ltrnet/LTRNetISWMessage.java create mode 100644 src/decode/ltrnet/LTRNetMessage.java create mode 100644 src/decode/ltrnet/LTRNetMessageProcessor.java create mode 100644 src/decode/ltrnet/LTRNetOSWMessage.java create mode 100644 src/decode/ltrnet/LTRNetPanel.java create mode 100644 src/decode/ltrstandard/LTRChannelState.java create mode 100644 src/decode/ltrstandard/LTRFSKDemodulator.java create mode 100644 src/decode/ltrstandard/LTRFSKDemodulator2.java create mode 100644 src/decode/ltrstandard/LTRPanel.java create mode 100644 src/decode/ltrstandard/LTRStandardActivitySummary.java create mode 100644 src/decode/ltrstandard/LTRStandardDecoder.java create mode 100644 src/decode/ltrstandard/LTRStandardEditor.java create mode 100644 src/decode/ltrstandard/LTRStandardISWMessage.java create mode 100644 src/decode/ltrstandard/LTRStandardMessage.java create mode 100644 src/decode/ltrstandard/LTRStandardMessageProcessor.java create mode 100644 src/decode/ltrstandard/LTRStandardOSWMessage.java create mode 100644 src/decode/mdc1200/MDCActivitySummary.java create mode 100644 src/decode/mdc1200/MDCCallEvent.java create mode 100644 src/decode/mdc1200/MDCChannelState.java create mode 100644 src/decode/mdc1200/MDCDecoder.java create mode 100644 src/decode/mdc1200/MDCMessage.java create mode 100644 src/decode/mdc1200/MDCMessageProcessor.java create mode 100644 src/decode/mdc1200/MDCMessageType.java create mode 100644 src/decode/mdc1200/MDCPanel.java create mode 100644 src/decode/mpt1327/MPT1327ActivitySummary.java create mode 100644 src/decode/mpt1327/MPT1327CallEvent.java create mode 100644 src/decode/mpt1327/MPT1327ChannelState.java create mode 100644 src/decode/mpt1327/MPT1327Decoder.java create mode 100644 src/decode/mpt1327/MPT1327Editor.java create mode 100644 src/decode/mpt1327/MPT1327Message.java create mode 100644 src/decode/mpt1327/MPT1327MessageProcessor.java create mode 100644 src/decode/mpt1327/MPT1327Panel.java create mode 100644 src/decode/mpt1327/MPT1327TrafficPanel.java create mode 100644 src/decode/nbfm/NBFMChannelState.java create mode 100644 src/decode/nbfm/NBFMConventionalDecoder.java create mode 100644 src/decode/nbfm/NBFMEditor.java create mode 100644 src/decode/nbfm/NBFMPanel.java create mode 100644 src/decode/passport/PassportActivitySummary.java create mode 100644 src/decode/passport/PassportBand.java create mode 100644 src/decode/passport/PassportCallEvent.java create mode 100644 src/decode/passport/PassportChannelState.java create mode 100644 src/decode/passport/PassportDecoder.java create mode 100644 src/decode/passport/PassportEditor.java create mode 100644 src/decode/passport/PassportMessage.java create mode 100644 src/decode/passport/PassportMessageProcessor.java create mode 100644 src/decode/passport/PassportPanel.java create mode 100644 src/dsp/BooleanZeroCrossingDetector.java create mode 100644 src/dsp/FloatToBinarySlicer.java create mode 100644 src/dsp/FloatToBinarySlicerLTR.java create mode 100644 src/dsp/NRZDecoder.java create mode 100644 src/dsp/SyncDetectListener.java create mode 100644 src/dsp/SyncDetectProvider.java create mode 100644 src/dsp/filter/BooleanAveragingFilter.java create mode 100644 src/dsp/filter/ComplexCICDecimate.java create mode 100644 src/dsp/filter/ComplexFIRFilter.java create mode 100644 src/dsp/filter/ComplexFilter.java create mode 100644 src/dsp/filter/ComplexHalfBandFilter.java create mode 100644 src/dsp/filter/ComplexHalfBandNoDecimateFilter.java create mode 100644 src/dsp/filter/ComplexPolyphaseDecimationFilter.java create mode 100644 src/dsp/filter/DCRemovalFilter.java create mode 100644 src/dsp/filter/DCRemovalFilter2.java create mode 100644 src/dsp/filter/FilterFactory.java create mode 100644 src/dsp/filter/Filters.java create mode 100644 src/dsp/filter/FloatFIRFilter.java create mode 100644 src/dsp/filter/FloatHalfBandFilter.java create mode 100644 src/dsp/filter/FloatHalfBandNoDecimateFilter.java create mode 100644 src/dsp/filter/Gain.java create mode 100644 src/dsp/filter/GoertzelFilter.java create mode 100644 src/dsp/filter/IIRBiQuadraticFilter.java create mode 100644 src/dsp/filter/SquaringFilter.java create mode 100644 src/dsp/filter/Window.java create mode 100644 src/dsp/fsk/FSK2Decoder.java create mode 100644 src/dsp/fsk/SymbolEvent.java create mode 100644 src/dsp/nbfm/FMDiscriminator.java create mode 100644 src/dsp/nbfm/FilteringNBFMDemodulator.java create mode 100644 src/dsp/nbfm/NBFMDemodulator.java create mode 100644 src/eventlog/CallEventLogger.java create mode 100644 src/eventlog/EventLogComponentEditor.java create mode 100644 src/eventlog/EventLogManager.java create mode 100644 src/eventlog/EventLogType.java create mode 100644 src/eventlog/EventLogger.java create mode 100644 src/eventlog/MessageEventLogger.java create mode 100644 src/eventlog/config/EventLogConfiguration.java create mode 100644 src/gui/SDRTrunk.java create mode 100644 src/gui/control/JFrequencyControl.java create mode 100644 src/instrument/Instrumentable.java create mode 100644 src/instrument/gui/BinaryTapViewPanel.java create mode 100644 src/instrument/gui/DecoderViewFrame.java create mode 100644 src/instrument/gui/FloatTapViewPanel.java create mode 100644 src/instrument/gui/SampleModel.java create mode 100644 src/instrument/gui/SymbolEventTapViewPanel.java create mode 100644 src/instrument/gui/TapViewPanel.java create mode 100644 src/instrument/gui/Viewer.java create mode 100644 src/instrument/gui/WaveSourceFrame.java create mode 100644 src/instrument/tap/Tap.java create mode 100644 src/instrument/tap/TapListener.java create mode 100644 src/instrument/tap/TapType.java create mode 100644 src/instrument/tap/event/SyncDetectTap.java create mode 100644 src/instrument/tap/stream/BinaryTap.java create mode 100644 src/instrument/tap/stream/ComplexTap.java create mode 100644 src/instrument/tap/stream/FloatTap.java create mode 100644 src/instrument/tap/stream/StreamTap.java create mode 100644 src/instrument/tap/stream/SymbolEventTap.java create mode 100644 src/log/Log.java create mode 100644 src/map/DefaultIcon.java create mode 100644 src/map/IconManager.java create mode 100644 src/map/MapIcon.java create mode 100644 src/map/MapIconListCellRenderer.java create mode 100644 src/map/MapMouseListener.java create mode 100644 src/map/MapPanel.java create mode 100644 src/map/MapService.java create mode 100644 src/map/Plottable.java create mode 100644 src/map/PlottableEntity.java create mode 100644 src/map/PlottableEntityPainter.java create mode 100644 src/map/PlottableEntityRenderer.java create mode 100644 src/map/PlottableUpdateListener.java create mode 100644 src/map/SelectionAdapter.java create mode 100644 src/map/SelectionPainter.java create mode 100644 src/message/Message.java create mode 100644 src/message/MessageDirection.java create mode 100644 src/message/MessageType.java create mode 100644 src/org/jdesktop/beans/AbstractBean.java create mode 100644 src/org/jdesktop/beans/AbstractSerializableBean.java create mode 100644 src/org/jdesktop/swingx/JXMapKit.form create mode 100644 src/org/jdesktop/swingx/JXMapKit.java create mode 100644 src/org/jdesktop/swingx/JXMapViewer.java create mode 100644 src/org/jdesktop/swingx/OSMTileFactoryInfo.java create mode 100644 src/org/jdesktop/swingx/VirtualEarthTileFactoryInfo.java create mode 100644 src/org/jdesktop/swingx/input/CenterMapListener.java create mode 100644 src/org/jdesktop/swingx/input/PanKeyListener.java create mode 100644 src/org/jdesktop/swingx/input/PanMouseInputListener.java create mode 100644 src/org/jdesktop/swingx/input/ZoomMouseWheelListenerCenter.java create mode 100644 src/org/jdesktop/swingx/input/ZoomMouseWheelListenerCursor.java create mode 100644 src/org/jdesktop/swingx/mapviewer/AbstractTileFactory.java create mode 100644 src/org/jdesktop/swingx/mapviewer/DefaultTileFactory.java create mode 100644 src/org/jdesktop/swingx/mapviewer/DefaultWaypoint.java create mode 100644 src/org/jdesktop/swingx/mapviewer/DefaultWaypointRenderer.java create mode 100644 src/org/jdesktop/swingx/mapviewer/GeoBounds.java create mode 100644 src/org/jdesktop/swingx/mapviewer/GeoPosition.java create mode 100644 src/org/jdesktop/swingx/mapviewer/LocalResponseCache.java create mode 100644 src/org/jdesktop/swingx/mapviewer/Tile.java create mode 100644 src/org/jdesktop/swingx/mapviewer/TileCache.java create mode 100644 src/org/jdesktop/swingx/mapviewer/TileFactory.java create mode 100644 src/org/jdesktop/swingx/mapviewer/TileFactoryInfo.java create mode 100644 src/org/jdesktop/swingx/mapviewer/TileListener.java create mode 100644 src/org/jdesktop/swingx/mapviewer/Waypoint.java create mode 100644 src/org/jdesktop/swingx/mapviewer/WaypointPainter.java create mode 100644 src/org/jdesktop/swingx/mapviewer/WaypointRenderer.java create mode 100644 src/org/jdesktop/swingx/mapviewer/bmng/CylindricalProjectionTileFactory.java create mode 100644 src/org/jdesktop/swingx/mapviewer/bmng/SLMapServerInfo.java create mode 100644 src/org/jdesktop/swingx/mapviewer/empty/EmptyTileFactory.java create mode 100644 src/org/jdesktop/swingx/mapviewer/esri/ESRITileFactory.java create mode 100644 src/org/jdesktop/swingx/mapviewer/resources/loading.png create mode 100644 src/org/jdesktop/swingx/mapviewer/resources/minus.png create mode 100644 src/org/jdesktop/swingx/mapviewer/resources/plus.png create mode 100644 src/org/jdesktop/swingx/mapviewer/resources/standard_waypoint.png create mode 100644 src/org/jdesktop/swingx/mapviewer/util/GeoUtil.java create mode 100644 src/org/jdesktop/swingx/mapviewer/util/MercatorUtils.java create mode 100644 src/org/jdesktop/swingx/mapviewer/wms/WMSService.java create mode 100644 src/org/jdesktop/swingx/mapviewer/wms/WMSTileFactory.java create mode 100644 src/org/jdesktop/swingx/painter/AbstractPainter.java create mode 100644 src/org/jdesktop/swingx/painter/CompoundPainter.java create mode 100644 src/org/jdesktop/swingx/painter/Painter.java create mode 100644 src/org/jdesktop/swingx/util/GraphicsUtilities.java create mode 100644 src/org/jdesktop/swingx/util/ShapeUtils.java create mode 100644 src/playlist/Playlist.java create mode 100644 src/playlist/PlaylistManager.java create mode 100644 src/playlist/PlaylistNode.java create mode 100644 src/properties/SystemProperties.java create mode 100644 src/record/RecordComponentEditor.java create mode 100644 src/record/RecordEditor.java create mode 100644 src/record/Recorder.java create mode 100644 src/record/RecorderManager.java create mode 100644 src/record/RecorderType.java create mode 100644 src/record/config/RecordConfiguration.java create mode 100644 src/record/wave/BooleanWaveRecorder.java create mode 100644 src/record/wave/ComplexWaveRecorder.java create mode 100644 src/record/wave/FloatWaveRecorder.java create mode 100644 src/record/wave/WaveRecorder.java create mode 100644 src/record/wave/WriteExample.java create mode 100644 src/sample/Broadcaster.java create mode 100644 src/sample/BufferOverflow.java create mode 100644 src/sample/ComplexToFloatSampleConverter.java create mode 100644 src/sample/Decimator.java create mode 100644 src/sample/FloatArrayToComplexAdapter.java create mode 100644 src/sample/Listener.java create mode 100644 src/sample/Provider.java create mode 100644 src/sample/complex/ComplexSample.java create mode 100644 src/sample/complex/ComplexSampleAssembler.java create mode 100644 src/sample/complex/ComplexSampleListListener.java create mode 100644 src/sample/complex/ComplexSampleRateCounter.java create mode 100644 src/sample/complex/ComplexSampleUtils.java create mode 100644 src/sample/complex/RTLComplexSample.java create mode 100644 src/sample/complex/ThreadKiller.java create mode 100644 src/settings/ColorSetting.java create mode 100644 src/settings/ColorSettingMenuItem.java create mode 100644 src/settings/ColorSettingResetAllMenuItem.java create mode 100644 src/settings/ColorSettingResetMenuItem.java create mode 100644 src/settings/FileSetting.java create mode 100644 src/settings/MapViewSetting.java create mode 100644 src/settings/Setting.java create mode 100644 src/settings/SettingChangeListener.java create mode 100644 src/settings/Settings.java create mode 100644 src/settings/SettingsManager.java create mode 100644 src/source/BufferReceiver.java create mode 100644 src/source/ComplexSource.java create mode 100644 src/source/EmptySourceEditor.java create mode 100644 src/source/FloatArraySource.java create mode 100644 src/source/FloatSource.java create mode 100644 src/source/SampleProvider.java create mode 100644 src/source/Source.java create mode 100644 src/source/SourceComponentEditor.java create mode 100644 src/source/SourceEditor.java create mode 100644 src/source/SourceEditorFactory.java create mode 100644 src/source/SourceException.java create mode 100644 src/source/SourceManager.java create mode 100644 src/source/SourceType.java create mode 100644 src/source/config/SourceConfigFactory.java create mode 100644 src/source/config/SourceConfigMixer.java create mode 100644 src/source/config/SourceConfigNone.java create mode 100644 src/source/config/SourceConfigTuner.java create mode 100644 src/source/config/SourceConfiguration.java create mode 100644 src/source/mixer/ChannelShortAdapter.java create mode 100644 src/source/mixer/MixerChannel.java create mode 100644 src/source/mixer/MixerEditor.java create mode 100644 src/source/mixer/MixerManager.java create mode 100644 src/source/mixer/MixerSource.java create mode 100644 src/source/mixer/SampleAdapter.java create mode 100644 src/source/mixer/ShortAdapter.java create mode 100644 src/source/mixer/SystemMixerDevices.java create mode 100644 src/source/tuner/DigitalDropChannel.java create mode 100644 src/source/tuner/FrequencyChangeBroadcaster.java create mode 100644 src/source/tuner/FrequencyChangeListener.java create mode 100644 src/source/tuner/FrequencyController.java create mode 100644 src/source/tuner/MixerTuner.java create mode 100644 src/source/tuner/MixerTunerDataLine.java create mode 100644 src/source/tuner/MixerTunerType.java create mode 100644 src/source/tuner/NumericallyControlledOscillator.java create mode 100644 src/source/tuner/Tuner.java create mode 100644 src/source/tuner/TunerChannel.java create mode 100644 src/source/tuner/TunerChannelSource.java create mode 100644 src/source/tuner/TunerClass.java create mode 100644 src/source/tuner/TunerConfiguration.java create mode 100644 src/source/tuner/TunerConfigurationAssignment.java create mode 100644 src/source/tuner/TunerController.java create mode 100644 src/source/tuner/TunerEditor.java create mode 100644 src/source/tuner/TunerGroupNode.java create mode 100644 src/source/tuner/TunerManager.java create mode 100644 src/source/tuner/TunerNode.java create mode 100644 src/source/tuner/TunerSelectionListener.java create mode 100644 src/source/tuner/TunerType.java create mode 100644 src/source/tuner/ettus/B100Tuner.java create mode 100644 src/source/tuner/ettus/B100TunerEditorPanel.java create mode 100644 src/source/tuner/fcd/FCDCommand.java create mode 100644 src/source/tuner/fcd/FCDModel.java create mode 100644 src/source/tuner/fcd/FCDTuner.java create mode 100644 src/source/tuner/fcd/FCDTunerController.java create mode 100644 src/source/tuner/fcd/FCDTunerDetailsPanel.java create mode 100644 src/source/tuner/fcd/proV1/FCD1TunerConfiguration.java create mode 100644 src/source/tuner/fcd/proV1/FCD1TunerConfigurationPanel.java create mode 100644 src/source/tuner/fcd/proV1/FCD1TunerController.java create mode 100644 src/source/tuner/fcd/proV1/FCD1TunerEditorPanel.java create mode 100644 src/source/tuner/fcd/proplusV2/FCD2TunerConfiguration.java create mode 100644 src/source/tuner/fcd/proplusV2/FCD2TunerConfigurationPanel.java create mode 100644 src/source/tuner/fcd/proplusV2/FCD2TunerController.java create mode 100644 src/source/tuner/fcd/proplusV2/FCD2TunerEditorPanel.java create mode 100644 src/source/tuner/rtl/RTL2832InfoPanel.java create mode 100644 src/source/tuner/rtl/RTL2832SampleAdapter.java create mode 100644 src/source/tuner/rtl/RTL2832Tuner.java create mode 100644 src/source/tuner/rtl/RTL2832TunerController.java create mode 100644 src/source/tuner/rtl/RTL2832TunerType.java create mode 100644 src/source/tuner/rtl/e4k/E4KTunerConfiguration.java create mode 100644 src/source/tuner/rtl/e4k/E4KTunerConfigurationPanel.java create mode 100644 src/source/tuner/rtl/e4k/E4KTunerController.java create mode 100644 src/source/tuner/rtl/e4k/E4KTunerEditorPanel.java create mode 100644 src/source/tuner/rtl/r820t/R820TTunerConfiguration.java create mode 100644 src/source/tuner/rtl/r820t/R820TTunerConfigurationPanel.java create mode 100644 src/source/tuner/rtl/r820t/R820TTunerController.java create mode 100644 src/source/tuner/rtl/r820t/R820TTunerEditorPanel.java create mode 100644 src/source/tuner/usb/USBTunerDevice.java create mode 100644 src/source/tuner/usb/USBUtils.java create mode 100644 src/source/wave/ComplexWaveSource.java create mode 100644 src/source/wave/FloatWaveSource.java create mode 100644 src/source/wave/WaveSource.java create mode 100644 src/spectrum/.directory create mode 100644 src/spectrum/DFTProcessor.java create mode 100644 src/spectrum/DFTResultsListener.java create mode 100644 src/spectrum/DFTResultsProvider.java create mode 100644 src/spectrum/FFTWidth.java create mode 100644 src/spectrum/OverlayPanel.java create mode 100644 src/spectrum/Pausable.java create mode 100644 src/spectrum/SpectralDisplayPanel.java create mode 100644 src/spectrum/SpectrumFrame.java create mode 100644 src/spectrum/SpectrumPanel.java create mode 100644 src/spectrum/WaterfallColorModel.java create mode 100644 src/spectrum/WaterfallPanel.java create mode 100644 src/test/TestCRC.java create mode 100644 src/util/Angle.java create mode 100644 src/util/ArcTangent.java create mode 100644 src/util/ColorIcon.java create mode 100644 src/util/Oscillator.java create mode 100644 src/util/TimeStamp.java create mode 100644 src/util/waveaudio/WavFile.java create mode 100644 src/util/waveaudio/WavFileException.java create mode 100644 src/util/waveaudio/WaveUtils.java create mode 100644 src/util/waveaudio/WaveWriter.java diff --git a/src/alias/Alias.java b/src/alias/Alias.java new file mode 100644 index 00000000..333a8495 --- /dev/null +++ b/src/alias/Alias.java @@ -0,0 +1,126 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package alias; + +import java.awt.Color; +import java.util.ArrayList; + +import javax.xml.bind.annotation.XmlAttribute; + +import audio.AudioType; + +public class Alias +{ + private String mName; + private int mColor; + private String mIconName; + private ArrayList mAliasIDs = new ArrayList(); + + public Alias() + { + } + + public String toString() + { + return "Alias: " + mName; + } + + @XmlAttribute + public String getName() + { + return mName; + } + + public void setName( String name ) + { + mName = name; + } + + @XmlAttribute + public int getColor() + { + return mColor; + } + + public void setColor( int color ) + { + mColor = color; + } + + public Color getMapColor() + { + return new Color( mColor ); + } + + @XmlAttribute + public String getIconName() + { + return mIconName; + } + + public void setIconName( String iconName ) + { + mIconName = iconName; + } + + public ArrayList getId() + { + return mAliasIDs; + } + + public void setId( ArrayList id ) + { + mAliasIDs = id; + } + + public void addAliasID( AliasID id ) + { + mAliasIDs.add( id ); + } + + public void removeAliasID( AliasID id ) + { + mAliasIDs.remove( id ); + } + + public boolean hasAudioType() + { + for( AliasID id: mAliasIDs ) + { + if( id.hasAudioType() ) + { + return true; + } + } + + return false; + } + + public AudioType getAudioType() + { + for( AliasID id: mAliasIDs ) + { + if( id.hasAudioType() ) + { + return id.getAudioType(); + } + } + + return AudioType.NORMAL; + } +} diff --git a/src/alias/AliasDirectory.java b/src/alias/AliasDirectory.java new file mode 100644 index 00000000..b1bf7e95 --- /dev/null +++ b/src/alias/AliasDirectory.java @@ -0,0 +1,82 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package alias; + +import java.util.ArrayList; + +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlRootElement; +import javax.xml.bind.annotation.XmlSeeAlso; + +@XmlSeeAlso( { AliasList.class} ) +@XmlRootElement( name = "alias_directory" ) +public class AliasDirectory +{ + private ArrayList mAliasList = new ArrayList(); + + public AliasDirectory() + { + } + + public void refresh() + { + for( AliasList list: mAliasList ) + { + list.update(); + } + } + + @XmlElement( name = "alias_list" ) + public ArrayList getAliasList() + { + return mAliasList; + } + + public void setAliasList( ArrayList lists ) + { + mAliasList = lists; + } + + public void addAliasList( AliasList list ) + { + mAliasList.add( list ); + } + + public void removeAliasList( AliasList list ) + { + mAliasList.remove( list ); + } + + /** + * Gets the named alias list, or returns an empty alias list + */ + public AliasList getAliasList( String name ) + { + AliasList retVal = new AliasList(); + + for( AliasList list: mAliasList ) + { + if( list.getName().equalsIgnoreCase( name ) ) + { + return list; + } + } + + return retVal; + } +} diff --git a/src/alias/AliasDirectoryNode.java b/src/alias/AliasDirectoryNode.java new file mode 100644 index 00000000..a616b755 --- /dev/null +++ b/src/alias/AliasDirectoryNode.java @@ -0,0 +1,91 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package alias; + +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; + +import javax.swing.JMenuItem; +import javax.swing.JPopupMenu; + +import controller.ConfigurableNode; + +public class AliasDirectoryNode extends ConfigurableNode +{ + private static final long serialVersionUID = 1L; + + public AliasDirectoryNode( AliasDirectory directory ) + { + super( directory ); + } + + public AliasDirectory getAliasDirectory() + { + return (AliasDirectory)getUserObject(); + } + + public void init() + { + for( AliasList list: getAliasDirectory().getAliasList() ) + { + AliasListNode node = new AliasListNode( list ); + + getModel().addNode( node, AliasDirectoryNode.this, getChildCount() ); + + node.init(); + } + + sort(); + } + + public String toString() + { + return "Alias Lists [" + this.getChildCount() + "]"; + } + + public JPopupMenu getContextMenu() + { + JPopupMenu retVal = new JPopupMenu(); + + JMenuItem addItem = new JMenuItem( "Add Alias List" ); + addItem.addActionListener( new ActionListener() + { + @Override + public void actionPerformed( ActionEvent e ) + { + AliasList list = new AliasList(); + + getAliasDirectory().addAliasList( list ); + + AliasListNode node = new AliasListNode( list ); + + getModel().addNode( node, + AliasDirectoryNode.this, + AliasDirectoryNode.this.getChildCount() ); + + sort(); + + getModel().showNode( node ); + } + } ); + + retVal.add( addItem ); + + return retVal; + } +} diff --git a/src/alias/AliasEditor.java b/src/alias/AliasEditor.java new file mode 100644 index 00000000..38c753de --- /dev/null +++ b/src/alias/AliasEditor.java @@ -0,0 +1,205 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package alias; + +import java.awt.Color; +import java.awt.Dimension; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; + +import javax.swing.BorderFactory; +import javax.swing.JButton; +import javax.swing.JColorChooser; +import javax.swing.JComboBox; +import javax.swing.JLabel; +import javax.swing.JOptionPane; +import javax.swing.JPanel; +import javax.swing.JTextField; +import javax.swing.tree.TreePath; + +import map.MapIcon; +import map.MapIconListCellRenderer; +import net.miginfocom.swing.MigLayout; +import settings.SettingsManager; +import controller.ConfigurableNode; +import controller.ResourceManager; + +public class AliasEditor extends JPanel implements ActionListener +{ + private static final long serialVersionUID = 1L; + + private AliasNode mAliasNode; + private ResourceManager mResourceManager; + + private JLabel mLabelName; + private JTextField mTextAlias; + + private JLabel mLabelMapColor; + private JButton mButtonColor; + private JComboBox mComboMapIcon; + private JButton mBtnIconManager; + + public AliasEditor( AliasNode aliasNode, ResourceManager resourceManager ) + { + mAliasNode = aliasNode; + mResourceManager = resourceManager; + + initGUI(); + } + + private void initGUI() + { + setLayout( new MigLayout() ); + + setBorder( BorderFactory.createTitledBorder( "Alias" ) ); + + mLabelName = new JLabel( "Name:" ); + add( mLabelName, "align right" ); + + mTextAlias = new JTextField( mAliasNode.getAlias().getName() ); + add( mTextAlias, "grow, wrap" ); + + mLabelMapColor = new JLabel( "Map Color:" ); + add( mLabelMapColor, "align right" ); + + mButtonColor = new JButton( "Map Color" ); + mButtonColor.setBackground( mAliasNode.getAlias().getMapColor() ); + mButtonColor.setForeground( mAliasNode.getAlias().getMapColor() ); + mButtonColor.addActionListener( new ActionListener() + { + @Override + public void actionPerformed( ActionEvent e ) + { + Color newColor = JColorChooser.showDialog( + AliasEditor.this, + "Choose color for map labels and routes", + mAliasNode.getAlias().getMapColor() ); + + mButtonColor.setForeground( newColor ); + mButtonColor.setBackground( newColor ); + } + } ); + add( mButtonColor, "wrap" ); + + add( new JLabel( "Map Icon:" ), "align right" ); + + mComboMapIcon = new JComboBox( + mResourceManager.getSettingsManager().getMapIcons() ); + + MapIconListCellRenderer renderer = new MapIconListCellRenderer(); + renderer.setPreferredSize( new Dimension( 200, 30 ) ); + mComboMapIcon.setRenderer( renderer ); + + String iconName = mAliasNode.getAlias().getIconName(); + + if( iconName == null ) + { + iconName = SettingsManager.DEFAULT_ICON; + } + + MapIcon savedIcon = mResourceManager.getSettingsManager() + .getMapIcon( iconName ); + + if( savedIcon != null ) + { + mComboMapIcon.setSelectedItem( savedIcon ); + } + + add( mComboMapIcon, "wrap" ); + + //Dummy place holder + add( new JLabel() ); + + mBtnIconManager = new JButton( "Icon Manager" ); + mBtnIconManager.addActionListener( new ActionListener() + { + @Override + public void actionPerformed( ActionEvent arg0 ) + { + mResourceManager.getController().showIconManager(); + } + } ); + add( mBtnIconManager, "span 2,wrap" ); + + JButton btnSave = new JButton( "Save" ); + btnSave.addActionListener( AliasEditor.this ); + add( btnSave ); + + JButton btnReset = new JButton( "Reset" ); + btnReset.addActionListener( AliasEditor.this ); + add( btnReset, "wrap" ); + } + + @Override + public void actionPerformed( ActionEvent e ) + { + String command = e.getActionCommand(); + + if( command.contentEquals( "Save" ) ) + { + String alias = mTextAlias.getText(); + + if( alias != null ) + { + boolean expanded = mAliasNode.getModel().getTree() + .isExpanded( new TreePath( mAliasNode ) ); + + mAliasNode.getAlias().setName( alias ); + ((ConfigurableNode)mAliasNode.getParent()).sort(); + + mAliasNode.getAlias().setColor( + mButtonColor.getBackground().getRGB() ); + + MapIcon selectedIcon = (MapIcon)mComboMapIcon.getSelectedItem(); + + if( selectedIcon != null ) + { + mAliasNode.getAlias().setIconName( selectedIcon.getName() ); + } + + mAliasNode.save(); + + mAliasNode.show(); + + if( expanded ) + { + mAliasNode.getModel().getTree() + .expandPath( new TreePath( mAliasNode ) ); + } + } + else + { + JOptionPane.showMessageDialog( AliasEditor.this, "Please enter an alias name" ); + } + } + else if( command.contentEquals( "Reset" ) ) + { + mTextAlias.setText( mAliasNode.getAlias().getName() ); + + mButtonColor.setBackground( mAliasNode.getAlias().getMapColor() ); + mButtonColor.setForeground( mAliasNode.getAlias().getMapColor() ); + + MapIcon savedIcon = mResourceManager.getSettingsManager() + .getMapIcon( mAliasNode.getAlias().getIconName() ); + + mComboMapIcon.setSelectedItem( savedIcon ); + } + + mAliasNode.refresh(); + } +} diff --git a/src/alias/AliasID.java b/src/alias/AliasID.java new file mode 100644 index 00000000..090d091e --- /dev/null +++ b/src/alias/AliasID.java @@ -0,0 +1,41 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package alias; + +import audio.AudioType; + +public abstract class AliasID +{ + public AliasID() + { + } + + public abstract AliasIDType getType(); + + public boolean hasAudioType() + { + return false; + } + + public AudioType getAudioType() + { + return AudioType.NORMAL; + } + + public abstract boolean matches( AliasID id ); +} diff --git a/src/alias/AliasIDNode.java b/src/alias/AliasIDNode.java new file mode 100644 index 00000000..c861c56a --- /dev/null +++ b/src/alias/AliasIDNode.java @@ -0,0 +1,35 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package alias; + +import controller.ConfigurableNode; + +public class AliasIDNode extends ConfigurableNode +{ + private static final long serialVersionUID = 1L; + + public AliasIDNode( AliasID id ) + { + super( id ); + } + + public void save() + { + getModel().getResourceManager().getPlaylistManager().save(); + } +} diff --git a/src/alias/AliasIDType.java b/src/alias/AliasIDType.java new file mode 100644 index 00000000..dea58b9c --- /dev/null +++ b/src/alias/AliasIDType.java @@ -0,0 +1,31 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package alias; + +public enum AliasIDType +{ + ESN, + Fleetsync, + LTRNetUID, + MDC1200, + MIN, + MPT1327, + Site, + Status, + Talkgroup; +} diff --git a/src/alias/AliasList.java b/src/alias/AliasList.java new file mode 100644 index 00000000..91ed9d03 --- /dev/null +++ b/src/alias/AliasList.java @@ -0,0 +1,409 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package alias; + +import java.util.ArrayList; +import java.util.HashMap; + +import javax.xml.bind.annotation.XmlAttribute; +import javax.xml.bind.annotation.XmlRootElement; +import javax.xml.bind.annotation.XmlSeeAlso; + +import alias.esn.Esn; +import alias.fleetsync.FleetsyncID; +import alias.fleetsync.StatusID; +import alias.mdc.MDC1200ID; +import alias.mobileID.Min; +import alias.mpt1327.MPT1327ID; +import alias.siteID.SiteID; +import alias.talkgroup.TalkgroupID; +import alias.uniqueID.UniqueID; + +@XmlSeeAlso( { Alias.class, + AliasID.class, + FleetsyncID.class, + Esn.class, + Group.class, + MDC1200ID.class, + Min.class, + MPT1327ID.class, + UniqueID.class, + SiteID.class, + StatusID.class, + TalkgroupID.class } ) +@XmlRootElement( name = "alias_list" ) +public class AliasList implements Comparable +{ + private String mName; + private ArrayList mGroups = new ArrayList(); + + private HashMap mESN = new HashMap(); + private HashMap mFleetsync = new HashMap(); + private HashMap mMDC1200 = new HashMap(); + private HashMap mMobileID = new HashMap(); + private HashMap mMPT1327 = new HashMap(); + private HashMap mSiteID = new HashMap(); + private HashMap mStatus = new HashMap(); + private HashMap mTalkgroup = new HashMap(); + private HashMap mUniqueID = new HashMap(); + + private boolean mESNWildcard = false; + private boolean mFleetsyncWildcard = false; + private boolean mMDC1200Wildcard = false; + private boolean mMPT1327Wildcard = false; + private boolean mTalkgroupWildcard = false; + + public AliasList() + { + this( null ); + } + + public AliasList( String name ) + { + mName = name; + update(); + } + + /** + * Load/Reload all lookup hashmaps + */ + public void update() + { + //Clear hashmaps + mESN.clear(); + mFleetsync.clear(); + mMDC1200.clear(); + mMobileID.clear(); + mMPT1327.clear(); + mSiteID.clear(); + mStatus.clear(); + mTalkgroup.clear(); + mUniqueID.clear(); + + for( Group group: mGroups ) + { + for( Alias alias: group.getAlias() ) + { + for( AliasID id: alias.getId() ) + { + switch( id.getType() ) + { + case ESN: + Esn esn = (Esn)id; + if( esn.getEsn().contains( "*" ) ) + { + mESNWildcard = true; + + //Replace (*) wildcard with regex wildcard (.) + mESN.put( esn.getEsn().replace( "*", "." ), alias ); + } + else + { + mESN.put( esn.getEsn(), alias ); + } + break; + case Fleetsync: + FleetsyncID fs = (FleetsyncID)id; + + if( fs.getIdent().contains( "*" ) ) + { + mFleetsyncWildcard = true; + + //Replace (*) wildcard with regex wildcard (.) + mFleetsync.put( fs.getIdent().replace( "*", "." ), alias ); + } + else + { + mFleetsync.put( fs.getIdent(), alias ); + } + break; + case MDC1200: + MDC1200ID mdc = (MDC1200ID)id; + + if( mdc.getIdent().contains( "*" ) ) + { + mMDC1200Wildcard = true; + + //Replace (*) wildcard with regex wildcard (.) + mMDC1200.put( mdc.getIdent().replace( "*", "." ), alias ); + } + else + { + mMDC1200.put( mdc.getIdent(), alias ); + } + break; + case MPT1327: + MPT1327ID mpt = (MPT1327ID)id; + + String ident = mpt.getIdent(); + + if( ident != null ) + { + if( ident.contains( "*" ) ) + { + mMPT1327Wildcard = true; + + //Replace (*) wildcard with regex wildcard (.) + mMPT1327.put( ident.replace( "*", "." ), alias ); + } + else + { + mMPT1327.put( ident, alias ); + } + } + break; + case MIN: + Min min = (Min)id; + mMobileID.put( min.getMin(), alias ); + break; + case LTRNetUID: + UniqueID uid = (UniqueID)id; + mUniqueID.put( uid.getUid(), alias ); + break; + case Site: + mSiteID.put( ((SiteID)id).getSite(), alias ); + break; + case Status: + mStatus.put( ((StatusID)id).getStatus(), alias ); + break; + case Talkgroup: + TalkgroupID tgid = (TalkgroupID)id; + + if( tgid.getTalkgroup().contains( "*" ) ) + { + mTalkgroupWildcard = true; + + //Replace (*) wildcard with regex wildcard (.) + mTalkgroup.put( tgid.getTalkgroup() + .replace( "*", "." ), alias ); + } + else + { + mTalkgroup.put( tgid.getTalkgroup(), alias ); + } + break; + } + } + } + } + } + + public Alias getSiteID( int siteID ) + { + return mSiteID.get( siteID ); + } + + public Alias getStatus( int status ) + { + return mStatus.get( status ); + } + + public Alias getUniqueID( int uniqueID ) + { + return mUniqueID.get( uniqueID ); + } + + public Alias getESNAlias( String esn ) + { + if( esn != null ) + { + Alias retVal = null; + + if( mESNWildcard ) + { + for( String key: mESN.keySet() ) + { + if( esn.matches( key ) ) + { + return mESN.get( key ); + } + } + } + else + { + return mESN.get( esn ); + } + } + + return null; + } + + + public Alias getFleetsyncAlias( String ident ) + { + if( ident != null ) + { + Alias retVal = null; + + if( mFleetsyncWildcard ) + { + for( String key: mFleetsync.keySet() ) + { + if( ident.matches( key ) ) + { + return mFleetsync.get( key ); + } + } + } + else + { + return mFleetsync.get( ident ); + } + } + + return null; + } + + public Alias getMDC1200Alias( String ident ) + { + if( ident != null ) + { + Alias retVal = null; + + if( mMDC1200Wildcard ) + { + for( String key: mMDC1200.keySet() ) + { + if( ident.matches( key ) ) + { + return mMDC1200.get( key ); + } + } + } + else + { + return mMDC1200.get( ident ); + } + } + + return null; + } + + public Alias getMPT1327Alias( String ident ) + { + if( ident != null ) + { + Alias retVal = null; + + if( mMPT1327Wildcard ) + { + for( String key: mMPT1327.keySet() ) + { + if( ident.matches( key ) ) + { + return mMPT1327.get( key ); + } + } + } + else + { + return mMPT1327.get( ident ); + } + } + + return null; + } + + public Group getGroup( Alias alias ) + { + if( alias != null ) + { + for( Group group: mGroups ) + { + if( group.contians( alias ) ) + { + return group; + } + } + } + + return null; + } + + public Alias getMobileIDNumberAlias( String ident ) + { + return mMobileID.get( ident ); + } + + public Alias getTalkgroupAlias( String tgid ) + { + if( tgid != null ) + { + Alias retVal = null; + + if( mTalkgroupWildcard ) + { + for( String key: mTalkgroup.keySet() ) + { + if( tgid.matches( key ) ) + { + return mTalkgroup.get( key ); + } + } + } + else + { + return mTalkgroup.get( tgid ); + } + } + + return null; + } + + public String toString() + { + return mName; + } + + @XmlAttribute + public String getName() + { + return mName; + } + + public void setName( String name ) + { + mName = name; + } + + public ArrayList getGroup() + { + return mGroups; + } + + public void setGroup( ArrayList groups ) + { + mGroups = groups; + } + + public void addGroup( Group group ) + { + mGroups.add( group ); + } + + public void removeGroup( Group group ) + { + mGroups.remove( group ); + } + + @Override + public int compareTo( AliasList otherAliasList ) + { + return getName().compareTo( otherAliasList.getName() ); + } +} diff --git a/src/alias/AliasListNode.java b/src/alias/AliasListNode.java new file mode 100644 index 00000000..65632971 --- /dev/null +++ b/src/alias/AliasListNode.java @@ -0,0 +1,126 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package alias; + +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; + +import javax.swing.JMenuItem; +import javax.swing.JOptionPane; +import javax.swing.JPanel; +import javax.swing.JPopupMenu; +import javax.swing.JSeparator; + +import controller.ConfigurableNode; + +public class AliasListNode extends ConfigurableNode +{ + private static final long serialVersionUID = 1L; + + public AliasListNode( AliasList list ) + { + super( list ); + } + + @Override + public JPanel getEditor() + { + return new ListEditor( this ); + } + + public AliasList getList() + { + return (AliasList)getUserObject(); + } + + public void init() + { + for( Group group: getList().getGroup() ) + { + GroupNode node = new GroupNode( group ); + + getModel().addNode( node, AliasListNode.this, getChildCount() ); + + node.init(); + } + + sort(); + } + + public String toString() + { + return getList().getName() + " [" + this.getChildCount() + "]"; + } + + public JPopupMenu getContextMenu() + { + JPopupMenu retVal = new JPopupMenu(); + + JMenuItem addItem = new JMenuItem( "Add Group" ); + addItem.addActionListener( new ActionListener() + { + @Override + public void actionPerformed( ActionEvent e ) + { + Group group = new Group(); + + getList().addGroup( group ); + + GroupNode node = new GroupNode( group ); + + getModel().addNode( node, + AliasListNode.this, + AliasListNode.this.getChildCount() ); + + sort(); + + getModel().showNode( node ); + } + } ); + + retVal.add( addItem ); + + retVal.add( new JSeparator() ); + + JMenuItem deleteItem = new JMenuItem( "Delete" ); + deleteItem.addActionListener( new ActionListener() + { + @Override + public void actionPerformed( ActionEvent e ) + { + int n = JOptionPane.showConfirmDialog( getModel().getTree(), + "Are you sure you want to permanently delete this node?" ); + + if( n == JOptionPane.YES_OPTION ) + { + AliasDirectoryNode parent = (AliasDirectoryNode)getParent(); + + parent.getAliasDirectory().removeAliasList( getList() ); + + save(); + + getModel().removeNodeFromParent( AliasListNode.this ); + } + } + } ); + + retVal.add( deleteItem ); + + return retVal; + } +} diff --git a/src/alias/AliasNode.java b/src/alias/AliasNode.java new file mode 100644 index 00000000..a2520303 --- /dev/null +++ b/src/alias/AliasNode.java @@ -0,0 +1,373 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package alias; + +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; + +import javax.swing.JMenu; +import javax.swing.JMenuItem; +import javax.swing.JOptionPane; +import javax.swing.JPanel; +import javax.swing.JPopupMenu; +import javax.swing.JSeparator; + +import map.MapIcon; +import alias.esn.ESNNode; +import alias.esn.Esn; +import alias.fleetsync.FleetsyncID; +import alias.fleetsync.FleetsyncIDNode; +import alias.fleetsync.StatusID; +import alias.fleetsync.StatusIDNode; +import alias.mdc.MDC1200ID; +import alias.mdc.MDC1200IDNode; +import alias.mobileID.MINNode; +import alias.mobileID.Min; +import alias.mpt1327.MPT1327ID; +import alias.mpt1327.MPT1327IDNode; +import alias.siteID.SiteID; +import alias.siteID.SiteIDNode; +import alias.talkgroup.TalkgroupID; +import alias.talkgroup.TalkgroupIDNode; +import alias.uniqueID.UniqueID; +import alias.uniqueID.UniqueIDNode; +import controller.ConfigurableNode; + +public class AliasNode extends ConfigurableNode +{ + private static final long serialVersionUID = 1L; + + public AliasNode( Alias alias ) + { + super( alias ); + } + + @Override + public JPanel getEditor() + { + return new AliasEditor( this, getModel().getResourceManager() ); + } + + public Alias getAlias() + { + return (Alias)getUserObject(); + } + + public String getIconPath() + { + String icon = getAlias().getIconName(); + + MapIcon mapIcon = getModel().getResourceManager() + .getSettingsManager().getMapIcon( icon ); + + if( mapIcon != null ) + { + return mapIcon.getPath(); + } + else + { + return null; + } + } + + public void init() + { + for( AliasID aliasID: getAlias().getId() ) + { + if( aliasID instanceof Esn ) + { + getModel().addNode( new ESNNode( (Esn)aliasID ), + AliasNode.this, getChildCount() ); + } + else if( aliasID instanceof FleetsyncID ) + { + getModel().addNode( new FleetsyncIDNode( (FleetsyncID)aliasID ), + AliasNode.this, getChildCount() ); + } + else if( aliasID instanceof MDC1200ID ) + { + getModel().addNode( new MDC1200IDNode( (MDC1200ID)aliasID ), + AliasNode.this, getChildCount() ); + } + else if( aliasID instanceof Min ) + { + getModel().addNode( new MINNode( (Min)aliasID ), + AliasNode.this, getChildCount() ); + } + else if( aliasID instanceof MPT1327ID ) + { + getModel().addNode( new MPT1327IDNode( (MPT1327ID)aliasID ), + AliasNode.this, getChildCount() ); + } + else if( aliasID instanceof SiteID ) + { + getModel().addNode( new SiteIDNode( (SiteID)aliasID ), + AliasNode.this, getChildCount() ); + } + else if( aliasID instanceof StatusID ) + { + getModel().addNode( new StatusIDNode( (StatusID)aliasID ), + AliasNode.this, getChildCount() ); + } + else if( aliasID instanceof TalkgroupID ) + { + getModel().addNode( new TalkgroupIDNode( (TalkgroupID)aliasID ), + AliasNode.this, getChildCount() ); + } + else if( aliasID instanceof UniqueID ) + { + getModel().addNode( new UniqueIDNode( (UniqueID)aliasID ), + AliasNode.this, getChildCount() ); + } + } + + sort(); + } + + public String toString() + { + return getAlias().getName() + " [" + this.getChildCount() + "]"; + } + + public JPopupMenu getContextMenu() + { + JPopupMenu retVal = new JPopupMenu(); + + JMenu addIDMenu = new JMenu( "Add ID" ); + + JMenuItem addESNItem = new JMenuItem( "ESN" ); + addESNItem.addActionListener( new ActionListener() + { + @Override + public void actionPerformed( ActionEvent e ) + { + Esn esn = new Esn(); + + getAlias().addAliasID( esn ); + + ESNNode node = new ESNNode( esn ); + + getModel().addNode( node, + AliasNode.this, + AliasNode.this.getChildCount() ); + + node.show(); + } + } ); + addIDMenu.add( addESNItem ); + + JMenuItem addFleetsyncItem = new JMenuItem( "Fleetsync" ); + addFleetsyncItem.addActionListener( new ActionListener() + { + @Override + public void actionPerformed( ActionEvent e ) + { + FleetsyncID fs = new FleetsyncID(); + + getAlias().addAliasID( fs ); + + FleetsyncIDNode node = new FleetsyncIDNode( fs ); + + getModel().addNode( node, + AliasNode.this, + AliasNode.this.getChildCount() ); + + node.show(); + } + } ); + addIDMenu.add( addFleetsyncItem ); + + JMenuItem addMDCItem = new JMenuItem( "MDC-1200" ); + addMDCItem.addActionListener( new ActionListener() + { + @Override + public void actionPerformed( ActionEvent e ) + { + MDC1200ID mdc = new MDC1200ID(); + + getAlias().addAliasID( mdc ); + + MDC1200IDNode node = new MDC1200IDNode( mdc ); + + getModel().addNode( node, + AliasNode.this, + AliasNode.this.getChildCount() ); + + node.show(); + } + } ); + addIDMenu.add( addMDCItem ); + + JMenuItem addMINItem = new JMenuItem( "MIN" ); + addMINItem.addActionListener( new ActionListener() + { + @Override + public void actionPerformed( ActionEvent e ) + { + Min min = new Min(); + + getAlias().addAliasID( min ); + + MINNode node = new MINNode( min ); + + getModel().addNode( node, + AliasNode.this, + AliasNode.this.getChildCount() ); + + node.show(); + } + } ); + addIDMenu.add( addMINItem ); + + JMenuItem addMPTItem = new JMenuItem( "MPT-1327" ); + addMPTItem.addActionListener( new ActionListener() + { + @Override + public void actionPerformed( ActionEvent e ) + { + MPT1327ID mpt = new MPT1327ID(); + + getAlias().addAliasID( mpt ); + + MPT1327IDNode node = new MPT1327IDNode( mpt ); + + getModel().addNode( node, + AliasNode.this, + AliasNode.this.getChildCount() ); + + node.show(); + } + } ); + addIDMenu.add( addMPTItem ); + + JMenuItem addSiteItem = new JMenuItem( "Site" ); + addSiteItem.addActionListener( new ActionListener() + { + @Override + public void actionPerformed( ActionEvent e ) + { + SiteID siteID = new SiteID(); + + getAlias().addAliasID( siteID ); + + SiteIDNode node = new SiteIDNode( siteID ); + + getModel().addNode( node, + AliasNode.this, + AliasNode.this.getChildCount() ); + + node.show(); + } + } ); + addIDMenu.add( addSiteItem ); + + JMenuItem addStatusItem = new JMenuItem( "Status" ); + addStatusItem.addActionListener( new ActionListener() + { + @Override + public void actionPerformed( ActionEvent e ) + { + StatusID statusID = new StatusID(); + + getAlias().addAliasID( statusID ); + + StatusIDNode node = new StatusIDNode( statusID ); + + getModel().addNode( node, + AliasNode.this, + AliasNode.this.getChildCount() ); + + node.show(); + } + } ); + + addIDMenu.add( addStatusItem ); + + JMenuItem addTalkgroupItem = new JMenuItem( "Talkgroup" ); + addTalkgroupItem.addActionListener( new ActionListener() + { + @Override + public void actionPerformed( ActionEvent e ) + { + TalkgroupID tg = new TalkgroupID(); + + getAlias().addAliasID( tg ); + + TalkgroupIDNode node = new TalkgroupIDNode( tg ); + + getModel().addNode( node, + AliasNode.this, + AliasNode.this.getChildCount() ); + + node.show(); + } + } ); + addIDMenu.add( addTalkgroupItem ); + + JMenuItem addUniqueIDItem = new JMenuItem( "Unique ID" ); + addUniqueIDItem.addActionListener( new ActionListener() + { + @Override + public void actionPerformed( ActionEvent e ) + { + UniqueID uid = new UniqueID(); + + getAlias().addAliasID( uid ); + + UniqueIDNode node = new UniqueIDNode( uid ); + + getModel().addNode( node, + AliasNode.this, + AliasNode.this.getChildCount() ); + + node.show(); + } + } ); + addIDMenu.add( addUniqueIDItem ); + + retVal.add( addIDMenu ); + + retVal.add( new JSeparator() ); + + JMenuItem deleteItem = new JMenuItem( "Delete" ); + deleteItem.addActionListener( new ActionListener() + { + @Override + public void actionPerformed( ActionEvent e ) + { + int n = JOptionPane.showConfirmDialog( getModel().getTree(), + "Are you sure you want to permanently delete this node?" ); + + if( n == JOptionPane.YES_OPTION ) + { + GroupNode parent = (GroupNode)getParent(); + + parent.getGroup().removeAlias( getAlias() ); + + save(); + + getModel().removeNodeFromParent( AliasNode.this ); + } + } + } ); + + retVal.add( deleteItem ); + + return retVal; + } +} diff --git a/src/alias/Group.java b/src/alias/Group.java new file mode 100644 index 00000000..6607605e --- /dev/null +++ b/src/alias/Group.java @@ -0,0 +1,73 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package alias; + +import java.util.ArrayList; + +import javax.xml.bind.annotation.XmlAttribute; + +public class Group +{ + private String mName; + private ArrayList mAliases = new ArrayList(); + + public Group() + { + } + + public String toString() + { + return "Group: " + mName; + } + + @XmlAttribute + public String getName() + { + return mName; + } + + public void setName( String name ) + { + mName = name; + } + + public ArrayList getAlias() + { + return mAliases; + } + + public void setAlias( ArrayList aliases ) + { + mAliases = aliases; + } + + public void addAlias( Alias alias ) + { + mAliases.add( alias ); + } + + public void removeAlias( Alias alias ) + { + mAliases.remove( alias ); + } + + public boolean contians( Alias alias ) + { + return mAliases.contains( alias ); + } +} diff --git a/src/alias/GroupEditor.java b/src/alias/GroupEditor.java new file mode 100644 index 00000000..c383aa92 --- /dev/null +++ b/src/alias/GroupEditor.java @@ -0,0 +1,110 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package alias; + +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; + +import javax.swing.BorderFactory; +import javax.swing.JButton; +import javax.swing.JLabel; +import javax.swing.JOptionPane; +import javax.swing.JPanel; +import javax.swing.JTextField; +import javax.swing.tree.TreePath; + +import net.miginfocom.swing.MigLayout; +import controller.ConfigurableNode; + +public class GroupEditor extends JPanel implements ActionListener +{ + private static final long serialVersionUID = 1L; + private GroupNode mGroupNode; + + private JLabel mLabelName; + private JTextField mTextGroup; + + public GroupEditor( GroupNode groupNode ) + { + mGroupNode = groupNode; + + initGUI(); + } + + private void initGUI() + { + setLayout( new MigLayout() ); + + setBorder( BorderFactory.createTitledBorder( "Group" ) ); + + mLabelName = new JLabel( "Name:" ); + add( mLabelName, "align right" ); + + mTextGroup = new JTextField( mGroupNode.getGroup().getName() ); + add( mTextGroup, "grow, wrap" ); + + JButton btnSave = new JButton( "Save" ); + btnSave.addActionListener( GroupEditor.this ); + add( btnSave ); + + JButton btnReset = new JButton( "Reset" ); + btnReset.addActionListener( GroupEditor.this ); + add( btnReset, "wrap" ); + } + + @Override + public void actionPerformed( ActionEvent e ) + { + String command = e.getActionCommand(); + + if( command.contentEquals( "Save" ) ) + { + String group = mTextGroup.getText(); + + if( group != null ) + { + boolean expanded = mGroupNode.getModel().getTree() + .isExpanded( new TreePath( mGroupNode ) ); + + mGroupNode.getGroup().setName( group ); + + ((ConfigurableNode)mGroupNode.getParent()).sort(); + + mGroupNode.save(); + + mGroupNode.show(); + + if( expanded ) + { + mGroupNode.getModel().getTree() + .expandPath( new TreePath( mGroupNode ) ); + } + } + else + { + JOptionPane.showMessageDialog( GroupEditor.this, "Please enter a group name" ); + } + } + else if( command.contentEquals( "Reset" ) ) + { + mTextGroup.setText( mGroupNode.getGroup().getName() ); + } + + mGroupNode.refresh(); + } +} diff --git a/src/alias/GroupNode.java b/src/alias/GroupNode.java new file mode 100644 index 00000000..ac0ca894 --- /dev/null +++ b/src/alias/GroupNode.java @@ -0,0 +1,126 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package alias; + +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; + +import javax.swing.JMenuItem; +import javax.swing.JOptionPane; +import javax.swing.JPanel; +import javax.swing.JPopupMenu; +import javax.swing.JSeparator; + +import controller.ConfigurableNode; + +public class GroupNode extends ConfigurableNode +{ + private static final long serialVersionUID = 1L; + + public GroupNode( Group group ) + { + super( group ); + } + + @Override + public JPanel getEditor() + { + return new GroupEditor( this ); + } + + public Group getGroup() + { + return (Group)getUserObject(); + } + + public void init() + { + for( Alias alias: getGroup().getAlias() ) + { + AliasNode node = new AliasNode( alias ); + + getModel().addNode( node, GroupNode.this, getChildCount() ); + + node.init(); + } + + sort(); + } + + public String toString() + { + return getGroup().getName() + " [" + this.getChildCount() + "]"; + } + + public JPopupMenu getContextMenu() + { + JPopupMenu retVal = new JPopupMenu(); + + JMenuItem addItem = new JMenuItem( "Add Alias" ); + addItem.addActionListener( new ActionListener() + { + @Override + public void actionPerformed( ActionEvent e ) + { + Alias alias = new Alias(); + + getGroup().addAlias( alias ); + + AliasNode node = new AliasNode( alias ); + + getModel().addNode( node, + GroupNode.this, + GroupNode.this.getChildCount() ); + + sort(); + + getModel().showNode( node ); + } + } ); + + retVal.add( addItem ); + + retVal.add( new JSeparator() ); + + JMenuItem deleteItem = new JMenuItem( "Delete" ); + deleteItem.addActionListener( new ActionListener() + { + @Override + public void actionPerformed( ActionEvent e ) + { + int n = JOptionPane.showConfirmDialog( getModel().getTree(), + "Are you sure you want to permanently delete this node?" ); + + if( n == JOptionPane.YES_OPTION ) + { + AliasListNode parent = (AliasListNode)getParent(); + + parent.getList().removeGroup( getGroup() ); + + save(); + + getModel().removeNodeFromParent( GroupNode.this ); + } + } + } ); + + retVal.add( deleteItem ); + + return retVal; + } +} diff --git a/src/alias/ListEditor.java b/src/alias/ListEditor.java new file mode 100644 index 00000000..84151e32 --- /dev/null +++ b/src/alias/ListEditor.java @@ -0,0 +1,107 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package alias; + +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; + +import javax.swing.BorderFactory; +import javax.swing.JButton; +import javax.swing.JLabel; +import javax.swing.JOptionPane; +import javax.swing.JPanel; +import javax.swing.JTextField; +import javax.swing.tree.TreePath; + +import net.miginfocom.swing.MigLayout; +import controller.ConfigurableNode; + +public class ListEditor extends JPanel implements ActionListener +{ + private static final long serialVersionUID = 1L; + private AliasListNode mListNode; + + private JLabel mLabelName; + private JTextField mTextGroup; + + public ListEditor( AliasListNode listNode ) + { + mListNode = listNode; + + initGUI(); + } + + private void initGUI() + { + setLayout( new MigLayout() ); + + setBorder( BorderFactory.createTitledBorder( "List" ) ); + + mLabelName = new JLabel( "Name:" ); + add( mLabelName, "align right" ); + + mTextGroup = new JTextField( mListNode.getList().getName() ); + add( mTextGroup, "grow, wrap" ); + + JButton btnSave = new JButton( "Save" ); + btnSave.addActionListener( ListEditor.this ); + add( btnSave ); + + JButton btnReset = new JButton( "Reset" ); + btnReset.addActionListener( ListEditor.this ); + add( btnReset, "wrap" ); + } + + @Override + public void actionPerformed( ActionEvent e ) + { + String command = e.getActionCommand(); + + if( command.contentEquals( "Save" ) ) + { + String group = mTextGroup.getText(); + + if( group != null ) + { + boolean expanded = mListNode.getModel().getTree() + .isExpanded( new TreePath( mListNode ) ); + + mListNode.getList().setName( group ); + ((ConfigurableNode)mListNode.getParent()).sort(); + mListNode.save(); + + mListNode.show(); + + if( expanded ) + { + mListNode.getModel().getTree().expandPath( new TreePath( mListNode ) ); + } + } + else + { + JOptionPane.showMessageDialog( ListEditor.this, "Please enter a list name" ); + } + } + else if( command.contentEquals( "Reset" ) ) + { + mTextGroup.setText( mListNode.getList().getName() ); + } + + mListNode.refresh(); + } +} diff --git a/src/alias/esn/ESNEditor.java b/src/alias/esn/ESNEditor.java new file mode 100644 index 00000000..4234ef53 --- /dev/null +++ b/src/alias/esn/ESNEditor.java @@ -0,0 +1,100 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package alias.esn; + +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; + +import javax.swing.BorderFactory; +import javax.swing.JButton; +import javax.swing.JLabel; +import javax.swing.JOptionPane; +import javax.swing.JPanel; +import javax.swing.JTextField; + +import net.miginfocom.swing.MigLayout; +import controller.ConfigurableNode; + +public class ESNEditor extends JPanel implements ActionListener +{ + private static final long serialVersionUID = 1L; + private ESNNode mESNNode; + + private JLabel mLabelName; + private JTextField mTextESN; + + public ESNEditor( ESNNode esnNode ) + { + mESNNode = esnNode; + + initGUI(); + } + + private void initGUI() + { + setLayout( new MigLayout() ); + + setBorder( BorderFactory.createTitledBorder( "ESN" ) ); + + mLabelName = new JLabel( "ESN:" ); + add( mLabelName, "align right" ); + + mTextESN = new JTextField( mESNNode.getESN().getEsn() ); + add( mTextESN, "grow, wrap" ); + + JButton btnSave = new JButton( "Save" ); + btnSave.addActionListener( ESNEditor.this ); + add( btnSave ); + + JButton btnReset = new JButton( "Reset" ); + btnReset.addActionListener( ESNEditor.this ); + add( btnReset, "wrap" ); + } + + @Override + public void actionPerformed( ActionEvent e ) + { + String command = e.getActionCommand(); + + if( command.contentEquals( "Save" ) ) + { + String esn = mTextESN.getText(); + + if( esn != null ) + { + mESNNode.getESN().setEsn( esn ); + + ((ConfigurableNode)mESNNode.getParent()).sort(); + + mESNNode.save(); + + mESNNode.show(); + } + else + { + JOptionPane.showMessageDialog( ESNEditor.this, "Please enter an ESN" ); + } + } + else if( command.contentEquals( "Reset" ) ) + { + mTextESN.setText( mESNNode.getESN().getEsn() ); + } + + mESNNode.refresh(); + } +} diff --git a/src/alias/esn/ESNNode.java b/src/alias/esn/ESNNode.java new file mode 100644 index 00000000..4b6d837f --- /dev/null +++ b/src/alias/esn/ESNNode.java @@ -0,0 +1,86 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package alias.esn; + +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; + +import javax.swing.JMenuItem; +import javax.swing.JOptionPane; +import javax.swing.JPanel; +import javax.swing.JPopupMenu; + +import alias.AliasIDNode; +import alias.AliasNode; + +public class ESNNode extends AliasIDNode +{ + private static final long serialVersionUID = 1L; + + public ESNNode( Esn esn ) + { + super( esn ); + } + + @Override + public JPanel getEditor() + { + return new ESNEditor( this ); + } + + public Esn getESN() + { + return (Esn)getUserObject(); + } + + public String toString() + { + return "ESN " + getESN().getEsn(); + } + + public JPopupMenu getContextMenu() + { + JPopupMenu retVal = new JPopupMenu(); + + JMenuItem addSystemItem = new JMenuItem( "Delete" ); + addSystemItem.addActionListener( new ActionListener() + { + @Override + public void actionPerformed( ActionEvent e ) + { + int n = JOptionPane.showConfirmDialog( getModel().getTree(), + "Are you sure you want to permanently delete this node?" ); + + if( n == JOptionPane.YES_OPTION ) + { + AliasNode parent = (AliasNode)getParent(); + + parent.getAlias().removeAliasID( getESN() ); + + save(); + + getModel().removeNodeFromParent( ESNNode.this ); + } + } + } ); + + retVal.add( addSystemItem ); + + return retVal; + } +} diff --git a/src/alias/esn/Esn.java b/src/alias/esn/Esn.java new file mode 100644 index 00000000..a2cfe786 --- /dev/null +++ b/src/alias/esn/Esn.java @@ -0,0 +1,72 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package alias.esn; + +import javax.xml.bind.annotation.XmlAttribute; + +import alias.AliasID; +import alias.AliasIDType; + +public class Esn extends AliasID +{ + private String mEsn; + + public Esn() + { + } + + @XmlAttribute + public String getEsn() + { + return mEsn; + } + + public void setEsn( String esn ) + { + mEsn = esn; + } + + public String toString() + { + return "ESN: " + mEsn; + } + + @Override + public boolean matches( AliasID id ) + { + boolean retVal = false; + + if( mEsn != null && id instanceof Esn ) + { + Esn esn = (Esn)id; + + //Create a pattern - replace * wildcards with regex single-char wildcard + String pattern = mEsn.replace( "*", ".?" ); + + retVal = esn.getEsn().matches( pattern ); + } + + return retVal; + } + + @Override + public AliasIDType getType() + { + return AliasIDType.ESN; + } +} diff --git a/src/alias/fleetsync/FleetsyncID.java b/src/alias/fleetsync/FleetsyncID.java new file mode 100644 index 00000000..43d0a979 --- /dev/null +++ b/src/alias/fleetsync/FleetsyncID.java @@ -0,0 +1,73 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package alias.fleetsync; + +import javax.xml.bind.annotation.XmlAttribute; + +import alias.AliasID; +import alias.AliasIDType; + + +public class FleetsyncID extends AliasID +{ + private String mID; + + public FleetsyncID() + { + } + + @XmlAttribute + public String getIdent() + { + return mID; + } + + public void setIdent( String ident ) + { + mID = ident; + } + + public String toString() + { + return "Fleetsync: " + mID; + } + + @Override + public boolean matches( AliasID id ) + { + boolean retVal = false; + + if( mID != null && id instanceof FleetsyncID ) + { + FleetsyncID tgid = (FleetsyncID)id; + + //Create a pattern - replace * wildcards with regex single-char wildcard + String pattern = mID.replace( "*", ".?" ); + + retVal = tgid.getIdent().matches( pattern ); + } + + return retVal; + } + + @Override + public AliasIDType getType() + { + return AliasIDType.Fleetsync; + } +} diff --git a/src/alias/fleetsync/FleetsyncIDEditor.java b/src/alias/fleetsync/FleetsyncIDEditor.java new file mode 100644 index 00000000..78ef6db6 --- /dev/null +++ b/src/alias/fleetsync/FleetsyncIDEditor.java @@ -0,0 +1,109 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package alias.fleetsync; + +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; + +import javax.swing.BorderFactory; +import javax.swing.JButton; +import javax.swing.JLabel; +import javax.swing.JOptionPane; +import javax.swing.JPanel; +import javax.swing.JTextArea; +import javax.swing.JTextField; + +import net.miginfocom.swing.MigLayout; +import controller.ConfigurableNode; + +public class FleetsyncIDEditor extends JPanel implements ActionListener +{ + private static final long serialVersionUID = 1L; + private FleetsyncIDNode mFleetsyncIDNode; + + private JLabel mLabelName; + private JTextField mTextIdent; + + private String mHelpText = "Enter a formatted fleetsync identifier.\n\n" + + "Format: PPP-IIII (P=Prefix, I=Identifier)\n\n" + + "Wildcard: use one or more asterisks (*) for any talkgroup " + + "digits.\n\n"; + + public FleetsyncIDEditor( FleetsyncIDNode fsNode ) + { + mFleetsyncIDNode = fsNode; + initGUI(); + } + + private void initGUI() + { + setLayout( new MigLayout() ); + + setBorder( BorderFactory.createTitledBorder( "Fleetsync" ) ); + + mLabelName = new JLabel( "Ident:" ); + add( mLabelName, "align right" ); + + mTextIdent = new JTextField( mFleetsyncIDNode.getFleetsyncID().getIdent() ); + add( mTextIdent, "grow, wrap" ); + + JButton btnSave = new JButton( "Save" ); + btnSave.addActionListener( FleetsyncIDEditor.this ); + add( btnSave ); + + JButton btnReset = new JButton( "Reset" ); + btnReset.addActionListener( FleetsyncIDEditor.this ); + add( btnReset, "wrap" ); + + JTextArea helpText = new JTextArea( mHelpText ); + helpText.setLineWrap( true ); + add( helpText, "span,grow" ); + } + + @Override + public void actionPerformed( ActionEvent e ) + { + String command = e.getActionCommand(); + + if( command.contentEquals( "Save" ) ) + { + String ident = mTextIdent.getText(); + + if( ident != null ) + { + mFleetsyncIDNode.getFleetsyncID().setIdent( ident ); + + ((ConfigurableNode)mFleetsyncIDNode.getParent()).sort(); + + mFleetsyncIDNode.save(); + + mFleetsyncIDNode.show(); + } + else + { + JOptionPane.showMessageDialog( FleetsyncIDEditor.this, "Please enter an ident" ); + } + } + else if( command.contentEquals( "Reset" ) ) + { + mTextIdent.setText( mFleetsyncIDNode.getFleetsyncID().getIdent() ); + } + + mFleetsyncIDNode.refresh(); + } +} diff --git a/src/alias/fleetsync/FleetsyncIDNode.java b/src/alias/fleetsync/FleetsyncIDNode.java new file mode 100644 index 00000000..1cea3feb --- /dev/null +++ b/src/alias/fleetsync/FleetsyncIDNode.java @@ -0,0 +1,97 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package alias.fleetsync; + +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; + +import javax.swing.JMenuItem; +import javax.swing.JOptionPane; +import javax.swing.JPanel; +import javax.swing.JPopupMenu; + +import alias.AliasIDNode; +import alias.AliasNode; + +public class FleetsyncIDNode extends AliasIDNode +{ + private static final long serialVersionUID = 1L; + + public FleetsyncIDNode( FleetsyncID id ) + { + super( id ); + } + + @Override + public JPanel getEditor() + { + return new FleetsyncIDEditor( this ); + } + + public FleetsyncID getFleetsyncID() + { + return (FleetsyncID)getUserObject(); + } + + public String toString() + { + StringBuilder sb = new StringBuilder(); + + sb.append( "Fleetsync " ); + + String ident = getFleetsyncID().getIdent(); + + if( ident != null ) + { + sb.append( ident ); + } + + return sb.toString(); + } + + public JPopupMenu getContextMenu() + { + JPopupMenu retVal = new JPopupMenu(); + + JMenuItem deleteItem = new JMenuItem( "Delete" ); + deleteItem.addActionListener( new ActionListener() + { + @Override + public void actionPerformed( ActionEvent e ) + { + int n = JOptionPane.showConfirmDialog( getModel().getTree(), + "Are you sure you want to permanently delete this node?" ); + + if( n == JOptionPane.YES_OPTION ) + { + AliasNode parent = (AliasNode)getParent(); + + parent.getAlias().removeAliasID( getFleetsyncID() ); + + save(); + + getModel().removeNodeFromParent( FleetsyncIDNode.this ); + } + } + } ); + + retVal.add( deleteItem ); + + return retVal; + } +} diff --git a/src/alias/fleetsync/StatusID.java b/src/alias/fleetsync/StatusID.java new file mode 100644 index 00000000..6f19dd2e --- /dev/null +++ b/src/alias/fleetsync/StatusID.java @@ -0,0 +1,69 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package alias.fleetsync; + +import javax.xml.bind.annotation.XmlAttribute; + +import alias.AliasID; +import alias.AliasIDType; + +public class StatusID extends AliasID +{ + private int mStatus; + + public StatusID() + { + } + + @XmlAttribute + public int getStatus() + { + return mStatus; + } + + public void setStatus( int status ) + { + this.mStatus = status; + } + + public String toString() + { + return "Status: " + mStatus; + } + + @Override + public boolean matches( AliasID id ) + { + boolean retVal = false; + + if( id instanceof StatusID ) + { + StatusID statusId = (StatusID)id; + + retVal = ( mStatus == statusId.getStatus() ); + } + + return retVal; + } + + @Override + public AliasIDType getType() + { + return AliasIDType.Status; + } +} diff --git a/src/alias/fleetsync/StatusIDEditor.java b/src/alias/fleetsync/StatusIDEditor.java new file mode 100644 index 00000000..81e1a52c --- /dev/null +++ b/src/alias/fleetsync/StatusIDEditor.java @@ -0,0 +1,114 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package alias.fleetsync; + +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; + +import javax.swing.BorderFactory; +import javax.swing.JButton; +import javax.swing.JLabel; +import javax.swing.JOptionPane; +import javax.swing.JPanel; +import javax.swing.JTextField; + +import log.Log; +import net.miginfocom.swing.MigLayout; +import controller.ConfigurableNode; + +public class StatusIDEditor extends JPanel implements ActionListener +{ + private static final long serialVersionUID = 1L; + private StatusIDNode mStatusIDNode; + + private JLabel mLabelName; + private JTextField mTextStatusID; + + public StatusIDEditor( StatusIDNode statusIDNode ) + { + mStatusIDNode = statusIDNode; + + initGUI(); + } + + private void initGUI() + { + setLayout( new MigLayout() ); + + setBorder( BorderFactory.createTitledBorder( "Status Code" ) ); + + mLabelName = new JLabel( "Status Code:" ); + add( mLabelName, "align right" ); + + mTextStatusID = new JTextField( + String.valueOf( mStatusIDNode.getStatusID().getStatus() ) ); + add( mTextStatusID, "grow, wrap" ); + + JButton btnSave = new JButton( "Save" ); + btnSave.addActionListener( StatusIDEditor.this ); + add( btnSave ); + + JButton btnReset = new JButton( "Reset" ); + btnReset.addActionListener( StatusIDEditor.this ); + add( btnReset, "wrap" ); + } + + @Override + public void actionPerformed( ActionEvent e ) + { + String command = e.getActionCommand(); + + if( command.contentEquals( "Save" ) ) + { + String statusIDString = mTextStatusID.getText(); + + if( statusIDString != null ) + { + try + { + int id = Integer.parseInt( statusIDString ); + + mStatusIDNode.getStatusID().setStatus( id ); + + ((ConfigurableNode)mStatusIDNode.getParent()).sort(); + + mStatusIDNode.save(); + + mStatusIDNode.show(); + } + catch( Exception ex ) + { + Log.error( "StatusIDEditor - error parsing int status " + + "id from [" + statusIDString + "]" ); + } + } + else + { + JOptionPane.showMessageDialog( StatusIDEditor.this, + "Please enter a status ID code" ); + } + } + else if( command.contentEquals( "Reset" ) ) + { + mTextStatusID.setText( + String.valueOf( mStatusIDNode.getStatusID().getStatus() ) ); + } + + mStatusIDNode.refresh(); + } +} diff --git a/src/alias/fleetsync/StatusIDNode.java b/src/alias/fleetsync/StatusIDNode.java new file mode 100644 index 00000000..40db6536 --- /dev/null +++ b/src/alias/fleetsync/StatusIDNode.java @@ -0,0 +1,86 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package alias.fleetsync; + +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; + +import javax.swing.JMenuItem; +import javax.swing.JOptionPane; +import javax.swing.JPanel; +import javax.swing.JPopupMenu; + +import alias.AliasIDNode; +import alias.AliasNode; + +public class StatusIDNode extends AliasIDNode +{ + private static final long serialVersionUID = 1L; + + public StatusIDNode( StatusID id ) + { + super( id ); + } + + @Override + public JPanel getEditor() + { + return new StatusIDEditor( this ); + } + + public StatusID getStatusID() + { + return (StatusID)getUserObject(); + } + + public String toString() + { + return "Status Code " + getStatusID().getStatus(); + } + + public JPopupMenu getContextMenu() + { + JPopupMenu retVal = new JPopupMenu(); + + JMenuItem addSystemItem = new JMenuItem( "Delete" ); + addSystemItem.addActionListener( new ActionListener() + { + @Override + public void actionPerformed( ActionEvent e ) + { + int n = JOptionPane.showConfirmDialog( getModel().getTree(), + "Are you sure you want to permanently delete this node?" ); + + if( n == JOptionPane.YES_OPTION ) + { + AliasNode parent = (AliasNode)getParent(); + + parent.getAlias().removeAliasID( getStatusID() ); + + save(); + + getModel().removeNodeFromParent( StatusIDNode.this ); + } + } + } ); + + retVal.add( addSystemItem ); + + return retVal; + } +} diff --git a/src/alias/mdc/MDC1200ID.java b/src/alias/mdc/MDC1200ID.java new file mode 100644 index 00000000..bcd29467 --- /dev/null +++ b/src/alias/mdc/MDC1200ID.java @@ -0,0 +1,73 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package alias.mdc; + +import javax.xml.bind.annotation.XmlAttribute; + +import alias.AliasID; +import alias.AliasIDType; + + +public class MDC1200ID extends AliasID +{ + private String mID; + + public MDC1200ID() + { + } + + @XmlAttribute + public String getIdent() + { + return mID; + } + + public void setIdent( String ident ) + { + mID = ident; + } + + public String toString() + { + return "MDC-1200: " + mID; + } + + @Override + public boolean matches( AliasID id ) + { + boolean retVal = false; + + if( mID != null && id instanceof MDC1200ID ) + { + MDC1200ID tgid = (MDC1200ID)id; + + //Create a pattern - replace * wildcards with regex single-char wildcard + String pattern = mID.replace( "*", ".?" ); + + retVal = tgid.getIdent().matches( pattern ); + } + + return retVal; + } + + @Override + public AliasIDType getType() + { + return AliasIDType.MDC1200; + } +} diff --git a/src/alias/mdc/MDC1200IDEditor.java b/src/alias/mdc/MDC1200IDEditor.java new file mode 100644 index 00000000..7f396983 --- /dev/null +++ b/src/alias/mdc/MDC1200IDEditor.java @@ -0,0 +1,99 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package alias.mdc; + +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; + +import javax.swing.BorderFactory; +import javax.swing.JButton; +import javax.swing.JLabel; +import javax.swing.JOptionPane; +import javax.swing.JPanel; +import javax.swing.JTextField; + +import net.miginfocom.swing.MigLayout; +import controller.ConfigurableNode; + +public class MDC1200IDEditor extends JPanel implements ActionListener +{ + private static final long serialVersionUID = 1L; + private MDC1200IDNode mMDC1200IDNode; + + private JLabel mLabelName; + private JTextField mTextIdent; + + public MDC1200IDEditor( MDC1200IDNode fsNode ) + { + mMDC1200IDNode = fsNode; + initGUI(); + } + + private void initGUI() + { + setLayout( new MigLayout() ); + + setBorder( BorderFactory.createTitledBorder( "MDC-1200" ) ); + + mLabelName = new JLabel( "Unit ID:" ); + add( mLabelName, "align right" ); + + mTextIdent = new JTextField( mMDC1200IDNode.getMDC1200ID().getIdent() ); + add( mTextIdent, "grow, wrap" ); + + JButton btnSave = new JButton( "Save" ); + btnSave.addActionListener( MDC1200IDEditor.this ); + add( btnSave ); + + JButton btnReset = new JButton( "Reset" ); + btnReset.addActionListener( MDC1200IDEditor.this ); + add( btnReset, "wrap" ); + } + + @Override + public void actionPerformed( ActionEvent e ) + { + String command = e.getActionCommand(); + + if( command.contentEquals( "Save" ) ) + { + String ident = mTextIdent.getText(); + + if( ident != null ) + { + mMDC1200IDNode.getMDC1200ID().setIdent( ident ); + + ((ConfigurableNode)mMDC1200IDNode.getParent()).sort(); + + mMDC1200IDNode.save(); + + mMDC1200IDNode.show(); + } + else + { + JOptionPane.showMessageDialog( MDC1200IDEditor.this, "Please enter a unit ID" ); + } + } + else if( command.contentEquals( "Reset" ) ) + { + mTextIdent.setText( mMDC1200IDNode.getMDC1200ID().getIdent() ); + } + + mMDC1200IDNode.refresh(); + } +} diff --git a/src/alias/mdc/MDC1200IDNode.java b/src/alias/mdc/MDC1200IDNode.java new file mode 100644 index 00000000..0af27b15 --- /dev/null +++ b/src/alias/mdc/MDC1200IDNode.java @@ -0,0 +1,91 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package alias.mdc; + +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; + +import javax.swing.JMenuItem; +import javax.swing.JOptionPane; +import javax.swing.JPanel; +import javax.swing.JPopupMenu; + +import alias.AliasIDNode; +import alias.AliasNode; + +public class MDC1200IDNode extends AliasIDNode +{ + private static final long serialVersionUID = 1L; + + public MDC1200IDNode( MDC1200ID id ) + { + super( id ); + } + + @Override + public JPanel getEditor() + { + return new MDC1200IDEditor( this ); + } + + public MDC1200ID getMDC1200ID() + { + return (MDC1200ID)getUserObject(); + } + + public String toString() + { + StringBuilder sb = new StringBuilder(); + + sb.append( "MDC-1200 " ); + sb.append( getMDC1200ID().getIdent() ); + + return sb.toString(); + } + + public JPopupMenu getContextMenu() + { + JPopupMenu retVal = new JPopupMenu(); + + JMenuItem deleteItem = new JMenuItem( "Delete" ); + deleteItem.addActionListener( new ActionListener() + { + @Override + public void actionPerformed( ActionEvent e ) + { + int n = JOptionPane.showConfirmDialog( getModel().getTree(), + "Are you sure you want to permanently delete this node?" ); + + if( n == JOptionPane.YES_OPTION ) + { + AliasNode parent = (AliasNode)getParent(); + + parent.getAlias().removeAliasID( getMDC1200ID() ); + + save(); + + getModel().removeNodeFromParent( MDC1200IDNode.this ); + } + } + } ); + + retVal.add( deleteItem ); + + return retVal; + } +} diff --git a/src/alias/mobileID/MINEditor.java b/src/alias/mobileID/MINEditor.java new file mode 100644 index 00000000..a5cf31b4 --- /dev/null +++ b/src/alias/mobileID/MINEditor.java @@ -0,0 +1,100 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package alias.mobileID; + +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; + +import javax.swing.BorderFactory; +import javax.swing.JButton; +import javax.swing.JLabel; +import javax.swing.JOptionPane; +import javax.swing.JPanel; +import javax.swing.JTextField; + +import net.miginfocom.swing.MigLayout; +import controller.ConfigurableNode; + +public class MINEditor extends JPanel implements ActionListener +{ + private static final long serialVersionUID = 1L; + private MINNode mMINNode; + + private JLabel mLabelName; + private JTextField mTextMIN; + + public MINEditor( MINNode minNode ) + { + mMINNode = minNode; + + initGUI(); + } + + private void initGUI() + { + setLayout( new MigLayout() ); + + setBorder( BorderFactory.createTitledBorder( "Mobile ID Number (MIN)" ) ); + + mLabelName = new JLabel( "MIN:" ); + add( mLabelName, "align right" ); + + mTextMIN = new JTextField( mMINNode.getMIN().getMin() ); + add( mTextMIN, "grow, wrap" ); + + JButton btnSave = new JButton( "Save" ); + btnSave.addActionListener( MINEditor.this ); + add( btnSave ); + + JButton btnReset = new JButton( "Reset" ); + btnReset.addActionListener( MINEditor.this ); + add( btnReset, "wrap" ); + } + + @Override + public void actionPerformed( ActionEvent e ) + { + String command = e.getActionCommand(); + + if( command.contentEquals( "Save" ) ) + { + String esn = mTextMIN.getText(); + + if( esn != null ) + { + mMINNode.getMIN().setMin( esn ); + + ((ConfigurableNode)mMINNode.getParent()).sort(); + + mMINNode.save(); + + mMINNode.show(); + } + else + { + JOptionPane.showMessageDialog( MINEditor.this, "Please enter an ESN" ); + } + } + else if( command.contentEquals( "Reset" ) ) + { + mTextMIN.setText( mMINNode.getMIN().getMin() ); + } + + mMINNode.refresh(); + } +} diff --git a/src/alias/mobileID/MINNode.java b/src/alias/mobileID/MINNode.java new file mode 100644 index 00000000..c8ea62ff --- /dev/null +++ b/src/alias/mobileID/MINNode.java @@ -0,0 +1,86 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package alias.mobileID; + +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; + +import javax.swing.JMenuItem; +import javax.swing.JOptionPane; +import javax.swing.JPanel; +import javax.swing.JPopupMenu; + +import alias.AliasIDNode; +import alias.AliasNode; + +public class MINNode extends AliasIDNode +{ + private static final long serialVersionUID = 1L; + + public MINNode( Min min ) + { + super( min ); + } + + @Override + public JPanel getEditor() + { + return new MINEditor( this ); + } + + public Min getMIN() + { + return (Min)getUserObject(); + } + + public String toString() + { + return "MIN " + getMIN().getMin(); + } + + public JPopupMenu getContextMenu() + { + JPopupMenu retVal = new JPopupMenu(); + + JMenuItem addSystemItem = new JMenuItem( "Delete" ); + addSystemItem.addActionListener( new ActionListener() + { + @Override + public void actionPerformed( ActionEvent e ) + { + int n = JOptionPane.showConfirmDialog( getModel().getTree(), + "Are you sure you want to permanently delete this node?" ); + + if( n == JOptionPane.YES_OPTION ) + { + AliasNode parent = (AliasNode)getParent(); + + parent.getAlias().removeAliasID( getMIN() ); + + save(); + + getModel().removeNodeFromParent( MINNode.this ); + } + } + } ); + + retVal.add( addSystemItem ); + + return retVal; + } +} diff --git a/src/alias/mobileID/Min.java b/src/alias/mobileID/Min.java new file mode 100644 index 00000000..649b9df0 --- /dev/null +++ b/src/alias/mobileID/Min.java @@ -0,0 +1,75 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package alias.mobileID; + +import javax.xml.bind.annotation.XmlAttribute; + +import alias.AliasID; +import alias.AliasIDType; + +/** + * Mobile ID Number + */ +public class Min extends AliasID +{ + private String mMin; + + public Min() + { + } + + @XmlAttribute + public String getMin() + { + return mMin; + } + + public void setMin( String min ) + { + mMin = min; + } + + public String toString() + { + return "MIN: " + mMin; + } + + @Override + public boolean matches( AliasID id ) + { + boolean retVal = false; + + if( mMin != null && id instanceof Min ) + { + Min min = (Min)id; + + //Create a pattern - replace * wildcards with regex single-char wildcard + String pattern = mMin.replace( "*", ".?" ); + + retVal = min.getMin().matches( pattern ); + } + + return retVal; + } + + @Override + public AliasIDType getType() + { + return AliasIDType.MIN; + } +} diff --git a/src/alias/mpt1327/MPT1327ID.java b/src/alias/mpt1327/MPT1327ID.java new file mode 100644 index 00000000..fc7c6a1f --- /dev/null +++ b/src/alias/mpt1327/MPT1327ID.java @@ -0,0 +1,72 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package alias.mpt1327; + +import javax.xml.bind.annotation.XmlAttribute; + +import alias.AliasID; +import alias.AliasIDType; + +public class MPT1327ID extends AliasID +{ + private String mID; + + public MPT1327ID() + { + } + + @XmlAttribute + public String getIdent() + { + return mID; + } + + public void setIdent( String ident ) + { + mID = ident; + } + + public String toString() + { + return "MPT-1327: " + mID; + } + + @Override + public boolean matches( AliasID id ) + { + boolean retVal = false; + + if( mID != null && id instanceof MPT1327ID ) + { + MPT1327ID tgid = (MPT1327ID)id; + + //Create a pattern - replace * wildcards with regex single-char wildcard + String pattern = mID.replace( "*", ".?" ); + + retVal = tgid.getIdent().matches( pattern ); + } + + return retVal; + } + + @Override + public AliasIDType getType() + { + return AliasIDType.MPT1327; + } +} diff --git a/src/alias/mpt1327/MPT1327IDEditor.java b/src/alias/mpt1327/MPT1327IDEditor.java new file mode 100644 index 00000000..e1b6fd46 --- /dev/null +++ b/src/alias/mpt1327/MPT1327IDEditor.java @@ -0,0 +1,99 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package alias.mpt1327; + +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; + +import javax.swing.BorderFactory; +import javax.swing.JButton; +import javax.swing.JLabel; +import javax.swing.JOptionPane; +import javax.swing.JPanel; +import javax.swing.JTextField; + +import net.miginfocom.swing.MigLayout; +import controller.ConfigurableNode; + +public class MPT1327IDEditor extends JPanel implements ActionListener +{ + private static final long serialVersionUID = 1L; + private MPT1327IDNode mMPT1327IDNode; + + private JLabel mLabelName; + private JTextField mTextIdent; + + public MPT1327IDEditor( MPT1327IDNode fsNode ) + { + mMPT1327IDNode = fsNode; + initGUI(); + } + + private void initGUI() + { + setLayout( new MigLayout() ); + + setBorder( BorderFactory.createTitledBorder( "MPT-1327" ) ); + + mLabelName = new JLabel( "Unit ID:" ); + add( mLabelName, "align right" ); + + mTextIdent = new JTextField( mMPT1327IDNode.getMPT1327ID().getIdent() ); + add( mTextIdent, "grow, wrap" ); + + JButton btnSave = new JButton( "Save" ); + btnSave.addActionListener( MPT1327IDEditor.this ); + add( btnSave ); + + JButton btnReset = new JButton( "Reset" ); + btnReset.addActionListener( MPT1327IDEditor.this ); + add( btnReset, "wrap" ); + } + + @Override + public void actionPerformed( ActionEvent e ) + { + String command = e.getActionCommand(); + + if( command.contentEquals( "Save" ) ) + { + String ident = mTextIdent.getText(); + + if( ident != null ) + { + mMPT1327IDNode.getMPT1327ID().setIdent( ident ); + + ((ConfigurableNode)mMPT1327IDNode.getParent()).sort(); + + mMPT1327IDNode.save(); + + mMPT1327IDNode.show(); + } + else + { + JOptionPane.showMessageDialog( MPT1327IDEditor.this, "Please enter a MPT1327 unit ID" ); + } + } + else if( command.contentEquals( "Reset" ) ) + { + mTextIdent.setText( mMPT1327IDNode.getMPT1327ID().getIdent() ); + } + + mMPT1327IDNode.refresh(); + } +} diff --git a/src/alias/mpt1327/MPT1327IDNode.java b/src/alias/mpt1327/MPT1327IDNode.java new file mode 100644 index 00000000..e6bb9039 --- /dev/null +++ b/src/alias/mpt1327/MPT1327IDNode.java @@ -0,0 +1,91 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package alias.mpt1327; + +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; + +import javax.swing.JMenuItem; +import javax.swing.JOptionPane; +import javax.swing.JPanel; +import javax.swing.JPopupMenu; + +import alias.AliasIDNode; +import alias.AliasNode; + +public class MPT1327IDNode extends AliasIDNode +{ + private static final long serialVersionUID = 1L; + + public MPT1327IDNode( MPT1327ID id ) + { + super( id ); + } + + @Override + public JPanel getEditor() + { + return new MPT1327IDEditor( this ); + } + + public MPT1327ID getMPT1327ID() + { + return (MPT1327ID)getUserObject(); + } + + public String toString() + { + StringBuilder sb = new StringBuilder(); + + sb.append( "MPT-1327 " ); + sb.append( getMPT1327ID().getIdent() ); + + return sb.toString(); + } + + public JPopupMenu getContextMenu() + { + JPopupMenu retVal = new JPopupMenu(); + + JMenuItem deleteItem = new JMenuItem( "Delete" ); + deleteItem.addActionListener( new ActionListener() + { + @Override + public void actionPerformed( ActionEvent e ) + { + int n = JOptionPane.showConfirmDialog( getModel().getTree(), + "Are you sure you want to permanently delete this node?" ); + + if( n == JOptionPane.YES_OPTION ) + { + AliasNode parent = (AliasNode)getParent(); + + parent.getAlias().removeAliasID( getMPT1327ID() ); + + save(); + + getModel().removeNodeFromParent( MPT1327IDNode.this ); + } + } + } ); + + retVal.add( deleteItem ); + + return retVal; + } +} diff --git a/src/alias/siteID/SiteID.java b/src/alias/siteID/SiteID.java new file mode 100644 index 00000000..65dfa416 --- /dev/null +++ b/src/alias/siteID/SiteID.java @@ -0,0 +1,69 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package alias.siteID; + +import javax.xml.bind.annotation.XmlAttribute; + +import alias.AliasID; +import alias.AliasIDType; + +public class SiteID extends AliasID +{ + private int mSite; + + public SiteID() + { + } + + @XmlAttribute + public int getSite() + { + return mSite; + } + + public void setSite( int site ) + { + this.mSite = site; + } + + public String toString() + { + return "Site: " + mSite; + } + + @Override + public boolean matches( AliasID id ) + { + boolean retVal = false; + + if( id instanceof SiteID ) + { + SiteID uid = (SiteID)id; + + retVal = ( mSite == uid.getSite() ); + } + + return retVal; + } + + @Override + public AliasIDType getType() + { + return AliasIDType.Site; + } +} diff --git a/src/alias/siteID/SiteIDEditor.java b/src/alias/siteID/SiteIDEditor.java new file mode 100644 index 00000000..d3eb8b14 --- /dev/null +++ b/src/alias/siteID/SiteIDEditor.java @@ -0,0 +1,114 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package alias.siteID; + +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; + +import javax.swing.BorderFactory; +import javax.swing.JButton; +import javax.swing.JLabel; +import javax.swing.JOptionPane; +import javax.swing.JPanel; +import javax.swing.JTextField; + +import log.Log; +import net.miginfocom.swing.MigLayout; +import controller.ConfigurableNode; + +public class SiteIDEditor extends JPanel implements ActionListener +{ + private static final long serialVersionUID = 1L; + private SiteIDNode mSiteIDNode; + + private JLabel mLabelName; + private JTextField mTextSiteID; + + public SiteIDEditor( SiteIDNode siteIDNode ) + { + mSiteIDNode = siteIDNode; + + initGUI(); + } + + private void initGUI() + { + setLayout( new MigLayout() ); + + setBorder( BorderFactory.createTitledBorder( "Site ID" ) ); + + mLabelName = new JLabel( "Site ID:" ); + add( mLabelName, "align right" ); + + mTextSiteID = new JTextField( + String.valueOf( mSiteIDNode.getSiteID().getSite() ) ); + add( mTextSiteID, "grow, wrap" ); + + JButton btnSave = new JButton( "Save" ); + btnSave.addActionListener( SiteIDEditor.this ); + add( btnSave ); + + JButton btnReset = new JButton( "Reset" ); + btnReset.addActionListener( SiteIDEditor.this ); + add( btnReset, "wrap" ); + } + + @Override + public void actionPerformed( ActionEvent e ) + { + String command = e.getActionCommand(); + + if( command.contentEquals( "Save" ) ) + { + String siteIDString = mTextSiteID.getText(); + + if( siteIDString != null ) + { + try + { + int id = Integer.parseInt( siteIDString ); + + mSiteIDNode.getSiteID().setSite( id ); + + ((ConfigurableNode)mSiteIDNode.getParent()).sort(); + + mSiteIDNode.save(); + + mSiteIDNode.show(); + } + catch( Exception ex ) + { + Log.error( "SiteIDEditor - error parsing int site " + + "id from [" + siteIDString + "]" ); + } + } + else + { + JOptionPane.showMessageDialog( SiteIDEditor.this, + "Please enter a site ID" ); + } + } + else if( command.contentEquals( "Reset" ) ) + { + mTextSiteID.setText( + String.valueOf( mSiteIDNode.getSiteID().getSite() ) ); + } + + mSiteIDNode.refresh(); + } +} diff --git a/src/alias/siteID/SiteIDNode.java b/src/alias/siteID/SiteIDNode.java new file mode 100644 index 00000000..09659149 --- /dev/null +++ b/src/alias/siteID/SiteIDNode.java @@ -0,0 +1,86 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package alias.siteID; + +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; + +import javax.swing.JMenuItem; +import javax.swing.JOptionPane; +import javax.swing.JPanel; +import javax.swing.JPopupMenu; + +import alias.AliasIDNode; +import alias.AliasNode; + +public class SiteIDNode extends AliasIDNode +{ + private static final long serialVersionUID = 1L; + + public SiteIDNode( SiteID id ) + { + super( id ); + } + + @Override + public JPanel getEditor() + { + return new SiteIDEditor( this ); + } + + public SiteID getSiteID() + { + return (SiteID)getUserObject(); + } + + public String toString() + { + return "Site " + getSiteID().getSite(); + } + + public JPopupMenu getContextMenu() + { + JPopupMenu retVal = new JPopupMenu(); + + JMenuItem addSystemItem = new JMenuItem( "Delete" ); + addSystemItem.addActionListener( new ActionListener() + { + @Override + public void actionPerformed( ActionEvent e ) + { + int n = JOptionPane.showConfirmDialog( getModel().getTree(), + "Are you sure you want to permanently delete this node?" ); + + if( n == JOptionPane.YES_OPTION ) + { + AliasNode parent = (AliasNode)getParent(); + + parent.getAlias().removeAliasID( getSiteID() ); + + save(); + + getModel().removeNodeFromParent( SiteIDNode.this ); + } + } + } ); + + retVal.add( addSystemItem ); + + return retVal; + } +} diff --git a/src/alias/talkgroup/TalkgroupID.java b/src/alias/talkgroup/TalkgroupID.java new file mode 100644 index 00000000..a6f79940 --- /dev/null +++ b/src/alias/talkgroup/TalkgroupID.java @@ -0,0 +1,102 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package alias.talkgroup; + +import javax.xml.bind.annotation.XmlAttribute; + +import alias.AliasID; +import alias.AliasIDType; +import audio.AudioType; + + +public class TalkgroupID extends AliasID +{ + private String mTalkgroup; + private AudioType mAudioType; + + public TalkgroupID() + { + } + + @XmlAttribute + public String getTalkgroup() + { + return mTalkgroup; + } + + public void setTalkgroup( String talkgroup ) + { + mTalkgroup = talkgroup; + } + + @XmlAttribute + public AudioType getAudioType() + { + return mAudioType; + } + + public void setAudioType( AudioType type ) + { + mAudioType = type; + } + + public boolean hasAudioType() + { + return mAudioType != null; + } + + public String toString() + { + StringBuilder sb = new StringBuilder(); + + sb.append( "Talkgroup: " + mTalkgroup ); + + if( mAudioType != null ) + { + sb.append( " [Audio " ); + sb.append( mAudioType.getShortDisplayString() ); + sb.append( "]" ); + } + + return sb.toString(); + } + + @Override + public boolean matches( AliasID id ) + { + boolean retVal = false; + + if( id instanceof TalkgroupID ) + { + TalkgroupID tgid = (TalkgroupID)id; + + //Create a pattern - replace * wildcards with regex single-char wildcard + String pattern = mTalkgroup.replace( "*", ".?" ); + + retVal = tgid.getTalkgroup().matches( pattern ); + } + + return retVal; + } + + @Override + public AliasIDType getType() + { + return AliasIDType.Talkgroup; + } +} diff --git a/src/alias/talkgroup/TalkgroupIDEditor.java b/src/alias/talkgroup/TalkgroupIDEditor.java new file mode 100644 index 00000000..097b1283 --- /dev/null +++ b/src/alias/talkgroup/TalkgroupIDEditor.java @@ -0,0 +1,151 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package alias.talkgroup; + +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; + +import javax.swing.BorderFactory; +import javax.swing.DefaultComboBoxModel; +import javax.swing.JButton; +import javax.swing.JComboBox; +import javax.swing.JLabel; +import javax.swing.JOptionPane; +import javax.swing.JPanel; +import javax.swing.JTextArea; +import javax.swing.JTextField; + +import net.miginfocom.swing.MigLayout; +import audio.AudioType; +import controller.ConfigurableNode; + +public class TalkgroupIDEditor extends JPanel implements ActionListener +{ + private static final long serialVersionUID = 1L; + private TalkgroupIDNode mTalkgroupIDNode; + + private JLabel mLabelName; + private JTextField mTextTalkgroupID; + private JLabel mLabelAudio; + private JComboBox mComboAudioTypes; + + private String mHelpText = "Enter a formatted talkgroup identifier.\n\n" + + "LTR Talkgroup Format: A-HH-TTT (A=Area, H=Home Repeater, " + + "T=Talkgroup)\n\n" + + "Passport Talkgroup Format: xxxxx (up to 5-digit number)\n\n" + + "Wildcard: use one or more asterisks (*) for any talkgroup digits.\n\n" + + "Audio: if the talkgroup uses simple frequency inversion, specify" + + " the inversion frequency from the dropdown list to automatically" + + " un-invert audio for this specific talkgroup"; + + public TalkgroupIDEditor( TalkgroupIDNode talkgroupIDNode ) + { + mTalkgroupIDNode = talkgroupIDNode; + + initGUI(); + } + + private void initGUI() + { + setLayout( new MigLayout() ); + + setBorder( BorderFactory.createTitledBorder( "Talkgroup ID" ) ); + + mLabelName = new JLabel( "TGID:" ); + add( mLabelName, "align right" ); + + mTextTalkgroupID = new JTextField(); + + mTextTalkgroupID.setText( mTalkgroupIDNode.getTalkgroupID().getTalkgroup() ); + + add( mTextTalkgroupID, "grow, wrap" ); + + mLabelAudio = new JLabel( "Audio:" ); + add( mLabelAudio, "align right" ); + + mComboAudioTypes = new JComboBox(); + + mComboAudioTypes.setModel( + new DefaultComboBoxModel( AudioType.values() ) ); + + AudioType audioType = mTalkgroupIDNode.getTalkgroupID().getAudioType(); + + if( audioType != null ) + { + mComboAudioTypes.setSelectedItem( audioType ); + } + + add( mComboAudioTypes, "wrap" ); + JButton btnSave = new JButton( "Save" ); + btnSave.addActionListener( TalkgroupIDEditor.this ); + add( btnSave ); + + JButton btnReset = new JButton( "Reset" ); + btnReset.addActionListener( TalkgroupIDEditor.this ); + add( btnReset, "wrap" ); + + JTextArea helpText = new JTextArea( mHelpText ); + helpText.setLineWrap( true ); + add( helpText, "span,grow" ); + } + + @Override + public void actionPerformed( ActionEvent e ) + { + String command = e.getActionCommand(); + + if( command.contentEquals( "Save" ) ) + { + String tgid = mTextTalkgroupID.getText(); + + if( tgid != null ) + { + mTalkgroupIDNode.getTalkgroupID().setTalkgroup( tgid ); + + ((ConfigurableNode)mTalkgroupIDNode.getParent()).sort(); + + AudioType selected = mComboAudioTypes + .getItemAt( mComboAudioTypes.getSelectedIndex() ); + + if( selected != null ) + { + mTalkgroupIDNode.getTalkgroupID().setAudioType( selected ); + } + + mTalkgroupIDNode.save(); + + mTalkgroupIDNode.show(); + } + else + { + JOptionPane.showMessageDialog( TalkgroupIDEditor.this, + "Please enter a talkgroup ID" ); + } + } + else if( command.contentEquals( "Reset" ) ) + { + mTextTalkgroupID.setText( + mTalkgroupIDNode.getTalkgroupID().getTalkgroup() ); + + mComboAudioTypes.setSelectedItem( + mTalkgroupIDNode.getTalkgroupID().getAudioType() ); + } + + mTalkgroupIDNode.refresh(); + } +} diff --git a/src/alias/talkgroup/TalkgroupIDNode.java b/src/alias/talkgroup/TalkgroupIDNode.java new file mode 100644 index 00000000..39cc1f64 --- /dev/null +++ b/src/alias/talkgroup/TalkgroupIDNode.java @@ -0,0 +1,86 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package alias.talkgroup; + +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; + +import javax.swing.JMenuItem; +import javax.swing.JOptionPane; +import javax.swing.JPanel; +import javax.swing.JPopupMenu; + +import alias.AliasIDNode; +import alias.AliasNode; + +public class TalkgroupIDNode extends AliasIDNode +{ + private static final long serialVersionUID = 1L; + + public TalkgroupIDNode( TalkgroupID id ) + { + super( id ); + } + + @Override + public JPanel getEditor() + { + return new TalkgroupIDEditor( this ); + } + + public TalkgroupID getTalkgroupID() + { + return (TalkgroupID)getUserObject(); + } + + public String toString() + { + return "Talkgroup " + getTalkgroupID().getTalkgroup(); + } + + public JPopupMenu getContextMenu() + { + JPopupMenu retVal = new JPopupMenu(); + + JMenuItem addSystemItem = new JMenuItem( "Delete" ); + addSystemItem.addActionListener( new ActionListener() + { + @Override + public void actionPerformed( ActionEvent e ) + { + int n = JOptionPane.showConfirmDialog( getModel().getTree(), + "Are you sure you want to permanently delete this node?" ); + + if( n == JOptionPane.YES_OPTION ) + { + AliasNode parent = (AliasNode)getParent(); + + parent.getAlias().removeAliasID( getTalkgroupID() ); + + save(); + + getModel().removeNodeFromParent( TalkgroupIDNode.this ); + } + } + } ); + + retVal.add( addSystemItem ); + + return retVal; + } +} diff --git a/src/alias/uniqueID/UniqueID.java b/src/alias/uniqueID/UniqueID.java new file mode 100644 index 00000000..6b2d1487 --- /dev/null +++ b/src/alias/uniqueID/UniqueID.java @@ -0,0 +1,69 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package alias.uniqueID; + +import javax.xml.bind.annotation.XmlAttribute; + +import alias.AliasID; +import alias.AliasIDType; + +public class UniqueID extends AliasID +{ + private int mUid; + + public UniqueID() + { + } + + @XmlAttribute + public int getUid() + { + return mUid; + } + + public void setUid( int uid ) + { + this.mUid = uid; + } + + public String toString() + { + return "UniqueID: " + mUid; + } + + @Override + public boolean matches( AliasID id ) + { + boolean retVal = false; + + if( id instanceof UniqueID ) + { + UniqueID uid = (UniqueID)id; + + retVal = mUid == uid.getUid(); + } + + return retVal; + } + + @Override + public AliasIDType getType() + { + return AliasIDType.LTRNetUID; + } +} diff --git a/src/alias/uniqueID/UniqueIDEditor.java b/src/alias/uniqueID/UniqueIDEditor.java new file mode 100644 index 00000000..717715e4 --- /dev/null +++ b/src/alias/uniqueID/UniqueIDEditor.java @@ -0,0 +1,114 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package alias.uniqueID; + +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; + +import javax.swing.BorderFactory; +import javax.swing.JButton; +import javax.swing.JLabel; +import javax.swing.JOptionPane; +import javax.swing.JPanel; +import javax.swing.JTextField; + +import log.Log; +import net.miginfocom.swing.MigLayout; +import controller.ConfigurableNode; + +public class UniqueIDEditor extends JPanel implements ActionListener +{ + private static final long serialVersionUID = 1L; + private UniqueIDNode mUniqueIDNode; + + private JLabel mLabelUniqueID; + private JTextField mTextUniqueID; + + public UniqueIDEditor( UniqueIDNode uniqueIDNode ) + { + mUniqueIDNode = uniqueIDNode; + + initGUI(); + } + + private void initGUI() + { + setLayout( new MigLayout() ); + + setBorder( BorderFactory.createTitledBorder( "Unique ID" ) ); + + mLabelUniqueID = new JLabel( "Unique ID:" ); + add( mLabelUniqueID, "align right" ); + + mTextUniqueID = new JTextField( + String.valueOf( mUniqueIDNode.getUniqueID().getUid() ) ); + add( mTextUniqueID, "grow, wrap" ); + + JButton btnSave = new JButton( "Save" ); + btnSave.addActionListener( UniqueIDEditor.this ); + add( btnSave ); + + JButton btnReset = new JButton( "Reset" ); + btnReset.addActionListener( UniqueIDEditor.this ); + add( btnReset, "wrap" ); + } + + @Override + public void actionPerformed( ActionEvent e ) + { + String command = e.getActionCommand(); + + if( command.contentEquals( "Save" ) ) + { + String uid = mTextUniqueID.getText(); + + if( uid != null ) + { + try + { + int uidInt = Integer.parseInt( uid ); + + mUniqueIDNode.getUniqueID().setUid( uidInt ); + + ((ConfigurableNode)mUniqueIDNode.getParent()).sort(); + + mUniqueIDNode.save(); + + mUniqueIDNode.show(); + } + catch( Exception ex ) + { + Log.error( "UniqueIDEditor - exception trying to parse " + + "int from uid [" + uid + "]" ); + } + } + else + { + JOptionPane.showMessageDialog( UniqueIDEditor.this, + "Please enter a site number and unique ID" ); + } + } + else if( command.contentEquals( "Reset" ) ) + { + mTextUniqueID.setText( + String.valueOf( mUniqueIDNode.getUniqueID().getUid() ) ); + } + + mUniqueIDNode.refresh(); + } +} diff --git a/src/alias/uniqueID/UniqueIDNode.java b/src/alias/uniqueID/UniqueIDNode.java new file mode 100644 index 00000000..30e32667 --- /dev/null +++ b/src/alias/uniqueID/UniqueIDNode.java @@ -0,0 +1,86 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package alias.uniqueID; + +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; + +import javax.swing.JMenuItem; +import javax.swing.JOptionPane; +import javax.swing.JPanel; +import javax.swing.JPopupMenu; + +import alias.AliasIDNode; +import alias.AliasNode; + +public class UniqueIDNode extends AliasIDNode +{ + private static final long serialVersionUID = 1L; + + public UniqueIDNode( UniqueID id ) + { + super( id ); + } + + @Override + public JPanel getEditor() + { + return new UniqueIDEditor( this ); + } + + public UniqueID getUniqueID() + { + return (UniqueID)getUserObject(); + } + + public String toString() + { + return "Unique ID " + getUniqueID().getUid(); + } + + public JPopupMenu getContextMenu() + { + JPopupMenu retVal = new JPopupMenu(); + + JMenuItem addSystemItem = new JMenuItem( "Delete" ); + addSystemItem.addActionListener( new ActionListener() + { + @Override + public void actionPerformed( ActionEvent e ) + { + int n = JOptionPane.showConfirmDialog( getModel().getTree(), + "Are you sure you want to permanently delete this node?" ); + + if( n == JOptionPane.YES_OPTION ) + { + AliasNode parent = (AliasNode)getParent(); + + parent.getAlias().removeAliasID( getUniqueID() ); + + save(); + + getModel().removeNodeFromParent( UniqueIDNode.this ); + } + } + } ); + + retVal.add( addSystemItem ); + + return retVal; + } +} diff --git a/src/audio/AudioAdapter.java b/src/audio/AudioAdapter.java new file mode 100644 index 00000000..1cfa9377 --- /dev/null +++ b/src/audio/AudioAdapter.java @@ -0,0 +1,97 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package audio; + +import util.Oscillator; +import dsp.filter.FilterFactory; +import dsp.filter.FloatFIRFilter; +import dsp.filter.Window.WindowType; + +/** + * Based on the AudioType applied, produces normal, muted, or audio inverted + * audio output. + */ +public class AudioAdapter +{ + private static int mGain = 5; + private Oscillator mSineWaveGenerator; + private FloatFIRFilter mPostInversionFilter; + private FloatFIRFilter mAudioHighPassFilter; + private AudioType mAudioType = AudioType.NORMAL; + private int mSampleRate; + + public AudioAdapter( int sampleRate ) + { + mSampleRate = sampleRate; + mSineWaveGenerator = new Oscillator( 3000, mSampleRate ); + mAudioHighPassFilter = new FloatFIRFilter( + FilterFactory.getHighPass( mSampleRate, 200, 1600, 48, + WindowType.HAMMING, true ), mGain ); + mPostInversionFilter = new FloatFIRFilter( + FilterFactory.getLowPass( mSampleRate, 3000, 5000, 48, + WindowType.HAMMING, true ), mGain ); + } + + public void setAudioType( AudioType type ) + { + if( type != null && type != mAudioType ) + { + mAudioType = type; + + int inversionFrequency = type.getAudioInversionFrequency(); + + if( inversionFrequency != 0 ) + { + mSineWaveGenerator.setFrequency( inversionFrequency );; + } + } + } + + public float get( float sample ) + { + float retVal; + + switch( mAudioType ) + { + case NORMAL: + retVal = mAudioHighPassFilter.get( sample ); + break; + case MUTE: + retVal = 0.0f; + break; + case INV2632: + case INV2718: + case INV2868: + case INV3023: + case INV3107: + case INV3196: + case INV3333: + case INV3339: + case INV3496: + case INV3729: + case INV4096: + default: + retVal = (float)( mPostInversionFilter.get( + mAudioHighPassFilter.get( sample ) * + mSineWaveGenerator.nextFloat() ) ); + break; + } + + return retVal; + } +} diff --git a/src/audio/AudioInverter.java b/src/audio/AudioInverter.java new file mode 100644 index 00000000..26855b5d --- /dev/null +++ b/src/audio/AudioInverter.java @@ -0,0 +1,82 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package audio; + +import sample.Listener; +import util.Oscillator; +import dsp.filter.Filters; +import dsp.filter.FloatFIRFilter; + +/** + * Applies audio inversion (or un-inversion) to a stream of float audio samples + * by multiplying each successive sample by either a 1 or a -1 value. + */ +public class AudioInverter implements Listener +{ + private Oscillator mSineWaveGenerator; + private FloatFIRFilter mPostInversionLowPassFilter; + private Listener mListener; + + public AudioInverter( int inversionFrequency, int sampleRate ) + { + mSineWaveGenerator = new Oscillator( inversionFrequency, sampleRate ); + + mPostInversionLowPassFilter = new FloatFIRFilter( + Filters.FIRLP_55TAP_48000FS_3000FC.getCoefficients(), 1.04 ); + + mPostInversionLowPassFilter.setListener( new FilteredSampleProcessor() ); + } + + public AudioInverter( InversionFrequency frequency, int sampleRate ) + { + this( frequency.getFrequency(), sampleRate ); + } + + public void setListener( Listener listener ) + { + mListener = listener; + } + + @Override + public void receive( Float sample ) + { + //Multiply the sample by the folding frequency and then send it to + //the low pass filter + if( mPostInversionLowPassFilter != null ) + { + mPostInversionLowPassFilter.receive( + sample * mSineWaveGenerator.nextFloat() ); + } + } + + /** + * Simple class to receive the output of the FIR low pass filter and then + * send it to the registered listener(s) + */ + class FilteredSampleProcessor implements Listener + { + @Override + public void receive( Float sample ) + { + if( mListener != null ) + { + mListener.receive( sample ); + } + } + } +} diff --git a/src/audio/AudioOutput.java b/src/audio/AudioOutput.java new file mode 100644 index 00000000..94a8d01e --- /dev/null +++ b/src/audio/AudioOutput.java @@ -0,0 +1,116 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package audio; + +import java.nio.ByteBuffer; + +import javax.sound.sampled.AudioFormat; +import javax.sound.sampled.AudioSystem; +import javax.sound.sampled.LineUnavailableException; +import javax.sound.sampled.SourceDataLine; + +import log.Log; +import sample.Listener; +import buffer.FloatSampleBufferAssembler; + +public class AudioOutput implements Listener, + SquelchListener, + AudioTypeListener +{ + private static final int sAUDIO_BLOCK_SIZE = 24000; + + private static final AudioFormat sAUDIO_FORMAT = + new AudioFormat( 48000, //SampleRate + 16, //Sample Size + 1, //Channels + true, //Signed + false ); //Little Endian + + private AudioAdapter mAdapter = new AudioAdapter( 48000 ); + + private FloatSampleBufferAssembler mBuffer = + new FloatSampleBufferAssembler( sAUDIO_BLOCK_SIZE ); + private SourceDataLine mOutput; + + private boolean mEnabled = false; + private SquelchState mSquelchState = SquelchState.SQUELCH; + + public AudioOutput( String channelName ) + { + try + { + mOutput = AudioSystem.getSourceDataLine( sAUDIO_FORMAT ); + mOutput.open( sAUDIO_FORMAT, (int)( sAUDIO_FORMAT.getSampleRate() ) ); + mOutput.start(); + } + catch ( LineUnavailableException e ) + { + Log.warning( "AudioOutput - couldn't open audio speakers " + + "for playback:" + e.getLocalizedMessage() ); + } + } + + @Override + public void setSquelch( SquelchState state ) + { + mSquelchState = state; + } + + public void setAudioPlaybackEnabled( boolean enabled ) + { + mEnabled = enabled; + } + + public void dispose() + { + mOutput.stop(); + mOutput.close(); + mOutput = null; + } + + @Override + public void receive( Float sample ) + { + float outputSample = mAdapter.get( sample ); + + if( mEnabled && mSquelchState == SquelchState.UNSQUELCH ) + { + mBuffer.put( outputSample ); + } + else + { + /* Use 0-valued samples when we're disabled or squelched */ + mBuffer.put( 0.0f ); + } + + if( !mBuffer.hasRemaining() ) + { + ByteBuffer buffer = mBuffer.get(); + + mOutput.write( buffer.array(), 0, buffer.array().length ); + + mBuffer.reset(); + } + } + + @Override + public void setAudioType( AudioType type ) + { + mAdapter.setAudioType( type ); + } +} diff --git a/src/audio/AudioType.java b/src/audio/AudioType.java new file mode 100644 index 00000000..643f5ce5 --- /dev/null +++ b/src/audio/AudioType.java @@ -0,0 +1,87 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package audio; + +public enum AudioType +{ + NORMAL( "Normal Audio", "Clear", 0 ), + MUTE( "Muted Audio", "Muted", 0 ), + INV2500( "Inverted Audio 2500Hz", "Inverted", 2500 ), + INV2550( "Inverted Audio 2550Hz", "Inverted", 2550 ), + INV2600( "Inverted Audio 2600Hz", "Inverted", 2600 ), + INV2632( "Inverted Audio 2632Hz", "Inverted", 2632 ), + INV2675( "Inverted Audio 2675Hz", "Inverted", 2675 ), + INV2718( "Inverted Audio 2718Hz", "Inverted", 2718 ), + INV2725( "Inverted Audio 2725Hz", "Inverted", 2725 ), + INV2750( "Inverted Audio 2750Hz", "Inverted", 2750 ), + INV2760( "Inverted Audio 2760Hz", "Inverted", 2760 ), + INV2775( "Inverted Audio 2775Hz", "Inverted", 2775 ), + INV2800( "Inverted Audio 2800Hz", "Inverted", 2800 ), + INV2825( "Inverted Audio 2825Hz", "Inverted", 2825 ), + INV2868( "Inverted Audio 2868Hz", "Inverted", 2868 ), + INV3023( "Inverted Audio 3023Hz", "Inverted", 3023 ), + INV3107( "Inverted Audio 3107Hz", "Inverted", 3107 ), + INV3196( "Inverted Audio 3196Hz", "Inverted", 3196 ), + INV3333( "Inverted Audio 3333Hz", "Inverted", 3333 ), + INV3339( "Inverted Audio 3339Hz", "Inverted", 3339 ), + INV3360( "Inverted Audio 3360Hz", "Inverted", 3360 ), + INV3375( "Inverted Audio 3375Hz", "Inverted", 3375 ), + INV3400( "Inverted Audio 3400Hz", "Inverted", 3400 ), + INV3450( "Inverted Audio 3450Hz", "Inverted", 3450 ), + INV3496( "Inverted Audio 3496Hz", "Inverted", 3496 ), + INV3729( "Inverted Audio 3729Hz", "Inverted", 3729 ), + INV4096( "Inverted Audio 4096Hz", "Inverted", 4096 ), + INV4300( "Inverted Audio 4300Hz", "Inverted", 4300 ), + INV4500( "Inverted Audio 4500Hz", "Inverted", 4500 ), + INV4700( "Inverted Audio 4700Hz", "Inverted", 4700 ), + INV4900( "Inverted Audio 4900Hz", "Inverted", 4900 ); + + private String mDisplayString; + private String mShortDisplayString; + private int mAudioInversionFrequency; + + AudioType( String displayString, + String shortDisplayString, + int inversionFrequency ) + { + mDisplayString = displayString; + mShortDisplayString = shortDisplayString; + mAudioInversionFrequency = inversionFrequency; + } + + public String getDisplayString() + { + return mDisplayString; + } + + public String getShortDisplayString() + { + return mShortDisplayString; + } + + public int getAudioInversionFrequency() + { + return mAudioInversionFrequency; + } + + @Override + public String toString() + { + return mDisplayString; + } +} diff --git a/src/audio/AudioTypeListener.java b/src/audio/AudioTypeListener.java new file mode 100644 index 00000000..48ddbeae --- /dev/null +++ b/src/audio/AudioTypeListener.java @@ -0,0 +1,23 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package audio; + +public interface AudioTypeListener +{ + public void setAudioType( AudioType type ); +} diff --git a/src/audio/InversionFrequency.java b/src/audio/InversionFrequency.java new file mode 100644 index 00000000..da493f19 --- /dev/null +++ b/src/audio/InversionFrequency.java @@ -0,0 +1,59 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package audio; + +/** + * Standard voice inversion foldover frequencies + * + * Reference: http://en.wikipedia.org/wiki/Voice_inversion + */ +public enum InversionFrequency +{ + HZ_2500( 2500 ), + HZ_2550( 2550 ), + HZ_2600( 2600 ), + HZ_2632( 2632 ), + HZ_2675( 2675 ), + HZ_2718( 2718 ), + HZ_2800( 2800 ), + HZ_2868( 2868 ), + HZ_2950( 2950 ), + HZ_3023( 3023 ), + HZ_3075( 3075 ), + HZ_3107( 3107 ), + HZ_3150( 3150 ), + HZ_3196( 3196 ), + HZ_3333( 3333 ), + HZ_3339( 3339 ), + HZ_3400( 3400 ), + HZ_3496( 3496 ), + HZ_3729( 3729 ), + HZ_4096( 4096 ); + + private int mFrequency; + + private InversionFrequency( int frequency ) + { + mFrequency = frequency; + } + + public int getFrequency() + { + return mFrequency; + } +} diff --git a/src/audio/SquelchListener.java b/src/audio/SquelchListener.java new file mode 100644 index 00000000..db55d6ab --- /dev/null +++ b/src/audio/SquelchListener.java @@ -0,0 +1,31 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package audio; + +public interface SquelchListener +{ + /** + * Squelch Control: + */ + public void setSquelch( SquelchState state ); + + public enum SquelchState + { + UNSQUELCH, SQUELCH; + } +} diff --git a/src/bits/BitSetBuffer.java b/src/bits/BitSetBuffer.java new file mode 100644 index 00000000..d72c23f9 --- /dev/null +++ b/src/bits/BitSetBuffer.java @@ -0,0 +1,322 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package bits; + +import java.util.BitSet; + +public class BitSetBuffer extends BitSet +{ + private static final long serialVersionUID = 1L; + + /** + * Logical (ie constructed) size of this bitset, despite the actual size of + * the super bitset that this class is based on + */ + private int mSize = 0; + + /** + * Pointer to the next fill index location, when adding bits to this bitset + * one at a time. + */ + private int mPointer = 0; + + /** + * Bitset that buffers bits added one at a time, up to the size of the this + * bitset. + * + * Note: the super class bitset behind this class may have a size larger + * that the size parameter specified. + * @param size + */ + public BitSetBuffer( int size ) + { + super( size ); + mSize = size; + } + + /** + * Constructs a bitset buffer and preloads it with the bits contained in + * the bitsToPreload parameter. If the bitsToPreload are longer than the + * size of the bitset, only those bits that fit will be preloaded + * + * @param size + * @param bitsToPreload + */ + public BitSetBuffer( int size, boolean[] bitsToPreload ) + { + this( size ); + + int pointer = 0; + + while( !this.isFull() && pointer < bitsToPreload.length ) + { + try + { + this.add( bitsToPreload[ pointer ] ); + } + catch( BitSetFullException e ) + { + e.printStackTrace(); + } + + pointer++; + } + } + + /** + * Constructs a new BitSetBuffer from an existing one + */ + private BitSetBuffer( BitSetBuffer toCopyFrom ) + { + this( toCopyFrom.size() ); + this.or( toCopyFrom ); + this.mPointer = toCopyFrom.pointer(); + } + + /** + * Current pointer index + */ + public int pointer() + { + return mPointer; + } + + /** + * Static method to construct a new BitSetBuffer, preloaded with the bits + * from the preload parameter, and then filled with the bits from the + * second bitsetbuffer parameter. + * + * @param preloadBits - boolean array of bits to be prepended to the new + * bitset + * @param bitsetToAppend - full bitset to be appended to the residual bits array + * @return - new Bitset preloaded with residual bits and new bitset + */ + public static BitSetBuffer merge( boolean[] preloadBits, BitSetBuffer bitsetToAppend ) + { + BitSetBuffer returnValue = new BitSetBuffer( preloadBits.length + bitsetToAppend.size(), preloadBits ); + + int pointer = 0; + + while( pointer < bitsetToAppend.size() && !returnValue.isFull() ) + { + try + { + returnValue.add( bitsetToAppend.get( pointer ) ); + } + catch( BitSetFullException e ) + { + e.printStackTrace(); + } + + pointer++; + } + + return returnValue; + } + + /** + * Returns a (new) copy of this bitsetbuffer + * @return + */ + public BitSetBuffer copy() + { + return new BitSetBuffer( this ); + } + + public boolean isFull() + { + return mPointer >= mSize; + } + + /** + * Overrides the in-build size() method of the bitset and returns the value + * specified at instantiation. The actual bitset size may be larger than + * this value, and that size is managed by the super class. + */ + @Override + public int size() + { + return mSize; + } + + /** + * Clears (sets to false or 0) the bits in this bitset and resets the + * pointer to zero. + */ + @Override + public void clear() + { + this.clear( 0, mSize ); + mPointer = 0; + } + + /** + * Adds a the bit parameters to this bitset, placing it in the index + * specified by mPointer, and incrementing mPointer to prepare for the next + * call to this method + * @param value + * @throws BitSetFullException - if the size specified at construction is + * exceeded. Invoke full() to determine if the bitset is full either before + * adding a new bit, or after adding a bit. + */ + public void add( boolean value ) throws BitSetFullException + { + if( !isFull() ) + { + this.set( mPointer++, value ); + } + else + { + throw new BitSetFullException( "bitset is full -- contains " + ( mPointer + 1 ) + " bits" ); + } + } + + public String toString() + { + StringBuilder sb = new StringBuilder(); + + for( int x = 0; x < mSize; x++ ) + { + sb.append( ( this.get( x ) ? "1" : "0" ) ); + } + + return sb.toString(); + } + + /** + * Returns a boolean array from startIndex to end of the bitset + */ + public boolean[] getBits( int startIndex ) + { + return getBits( startIndex, mSize - 1 ); + } + + /** + * Returns a boolean array of the right-most bitCount number of bits + */ + public boolean[] right( int bitCount ) + { + return getBits( mSize - bitCount - 1 ); + } + + /** + * Returns a boolean array representing the bits located from startIndex + * through endIndex + */ + public boolean[] getBits( int startIndex, int endIndex ) + { + boolean[] returnValue = null; + + if( startIndex >= 0 && + startIndex < endIndex && + endIndex < mSize ) + { + returnValue = new boolean[ endIndex - startIndex + 1 ]; + + int bitsetPointer = startIndex; + int returnPointer = 0; + + while( bitsetPointer <= endIndex ) + { + returnValue[ returnPointer ] = this.get( bitsetPointer ); + bitsetPointer++; + returnPointer++; + } + } + + return returnValue; + } + + /** + * Returns the integer value represented by the bit array + * @param bits - an array of bit positions that will be treated as if they + * were contiguous bits, with index 0 being the MSB and index + * length - 1 being the LSB + * @return - integer value of the bit array + */ + public int getInt( int[] bits ) + { + int retVal = 0; + + for( int x = 0; x < bits.length; x++ ) + { + if( get( bits[ x ] ) ) + { + retVal += 1<<( bits.length - 1 - x ); + } + } + + return retVal; + } + + public String getHex( int[] bits, int digitDisplayCount ) + { + int value = getInt( bits ); + + return String.format( "%0" + digitDisplayCount + "X", value ); + } + + /** + * Returns the integer value represented by the bit range + * @param start - MSB of the integer + * @param end - LSB of the integer + * @return - integer value of the bit range + */ + public int getInt( int start, int end ) + { + if( end - start > 32 ) + { + throw new IllegalArgumentException( "BitSetBuffer - requested bit " + + "length [" + ( end - start ) + "] is larger than an " + + "integer (32 bits)" ); + } + + int retVal = 0; + + for( int x = start; x <= end; x++ ) + { + if( get( x ) ) + { + retVal += 1<<( end - x ); + } + } + + return retVal; + } + + /** + * Returns the long value represented by the bit range + * @param start - MSB of the long + * @param end - LSB of the long + * @return - long value of the bit range + */ + public long getLong( int start, int end ) + { + long retVal = 0; + + for( int x = start; x <= end; x++ ) + { + if( get( x ) ) + { + retVal += 1<<( end - x ); + } + } + + return retVal; + } +} diff --git a/src/bits/BitSetFullException.java b/src/bits/BitSetFullException.java new file mode 100644 index 00000000..7467c809 --- /dev/null +++ b/src/bits/BitSetFullException.java @@ -0,0 +1,28 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package bits; + +public class BitSetFullException extends Exception +{ + private static final long serialVersionUID = 1L; + + public BitSetFullException( String text ) + { + super( text ); + } +} diff --git a/src/bits/MessageFramer.java b/src/bits/MessageFramer.java new file mode 100644 index 00000000..4be914da --- /dev/null +++ b/src/bits/MessageFramer.java @@ -0,0 +1,232 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package bits; + +import java.util.ArrayList; +import java.util.Iterator; + +import sample.Broadcaster; +import sample.Listener; +import dsp.SyncDetectListener; +import dsp.SyncDetectProvider; + +/** + * MessageFramer - processes bitsets looking for a sync pattern within + * the bits, and then extracts the message, including the sync + * pattern, for a total bit length of messageLength. + * + * Will extract multiple messages simultaneously, for each sync pattern that is + * encountered within the bitset bit stream. + */ +public class MessageFramer implements Listener, + SyncDetectProvider +{ + private boolean[] mSyncPattern; + private int mMessageLength; + private SyncDetectListener mSyncDetectListener; + private Broadcaster mBroadcaster = + new Broadcaster(); + private ArrayList mMessageAssemblers = + new ArrayList(); + + private ArrayList mCompletedMessageAssemblers = + new ArrayList(); + + private BitSetBuffer mPreviousBuffer = null; + + private SyncPatternMatcher mMatcher; + + public MessageFramer( boolean[] syncPattern, int messageLength ) + { + mSyncPattern = syncPattern; + mMatcher = new SyncPatternMatcher( syncPattern ); + mMessageLength = messageLength; + } + + public void dispose() + { + mBroadcaster.dispose(); + mCompletedMessageAssemblers.clear(); + mMessageAssemblers.clear(); + } + + @Override + public void receive( Boolean bit ) + { + mMatcher.receive( bit ); + + Iterator it = mMessageAssemblers.iterator(); + + MessageAssembler assembler; + + while( it.hasNext() ) + { + assembler = it.next(); + + /* Dispose and remove any completed assemblers */ + if( assembler.complete() ) + { + assembler.dispose(); + it.remove(); + } + /* Otherwise, send them the bit */ + else + { + assembler.receive( bit ); + } + } + + /* Check for sync match and add new message assembler */ + if( mMatcher.matches() ) + { + addMessageAssembler( new MessageAssembler( mMessageLength, mSyncPattern ) ); + + /* Notify any sync detect listener(s) */ + if( mSyncDetectListener != null ) + { + mSyncDetectListener.syncDetected(); + } + } + } + + /** + * Causes all messages currently under assembly to be forcibly + * sent (ie flushed) to all registered message listeners, and + * subsequently, all assemblers to be deleted + */ + public void flush() + { + for( MessageAssembler assembler: mMessageAssemblers ) + { + assembler.flush(); + } + } + + @Override + public void setSyncDetectListener( SyncDetectListener listener ) + { + mSyncDetectListener = listener; + } + + /** + * Allow a message listener to register with this framer to receive + * all framed messages + */ + public void addMessageListener( Listener listener ) + { + mBroadcaster.addListener( listener ); + } + + public void removeMessageListener( Listener listener ) + { + mBroadcaster.removeListener( listener ); + } + + /* + * Adds a message assembler to receive bits from the bit stream + */ + private void addMessageAssembler( MessageAssembler assembler ) + { + mMessageAssemblers.add( assembler ); + } + + private void removeMessageAssembler( MessageAssembler assembler ) + { + mMessageAssemblers.remove( assembler ); + } + + /** + * Assembles a binary message, starting with the initial fill of the + * identified sync pattern, and every bit thereafter. Once the accumulated + * bits equal the message length, the message is sent and the assembler + * flags itself as complete. + * + * By design, multiple message assemblers can exist at the same time, each + * assembling different, overlapping potential messages + */ + private class MessageAssembler implements Listener + { + BitSetBuffer mMessage; + boolean mComplete = false; + + MessageAssembler( int messageLength ) + { + mMessage = new BitSetBuffer( messageLength ); + } + + MessageAssembler( int messageLength, boolean[] initialFill ) + { + this( messageLength ); + + /* Pre-load the message with the sync pattern */ + for( int x = 0; x < initialFill.length; x++ ) + { + receive( initialFill[ x ] ); + } + } + + public void dispose() + { + mMessage = null; + } + + @Override + /** + * Receives one bit at a time, and assembles them into a message + */ + public void receive( Boolean bit ) + { + try + { + mMessage.add( bit ); + } + catch( BitSetFullException e ) + { + e.printStackTrace(); + } + + /* Once our message is complete (ie full), send it to all registered + * message listeners, and set complete flag so for auto-removal */ + if( mMessage.isFull() ) + { + mComplete = true; + flush(); + } + } + + /** + * Flushes/Sends the current message, or partial message, and sets + * complete flag to true, so that we can be auto-removed + */ + public void flush() + { + mBroadcaster.receive( mMessage ); + mComplete = true; + } + + /** + * Flag to indicate when this assembler has received all of the bits it + * is looking for (ie message length), and should then be removed from + * receiving any more bits + */ + public boolean complete() + { + return mComplete; + } + } +} diff --git a/src/bits/SyncPattern.java b/src/bits/SyncPattern.java new file mode 100644 index 00000000..817bad63 --- /dev/null +++ b/src/bits/SyncPattern.java @@ -0,0 +1,118 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package bits; + +public enum SyncPattern +{ + /* Revs (0xAA) and Sync (0x7650) = 01010 0111011001010000 */ + FLEETSYNC1( new boolean[] + { + false, true, false, true, false, //end of revs + false, true, true, true, //0111 0x7 + false, true, true, false, //0110 0x6 + false, true, false, true, //0101 0x5 + false, false, false, false //0000 0x0 + } ), + + /* Revs (0x0A) and Sync (0x23EB) = 10101 0010001111101011 */ + FLEETSYNC2( new boolean[] + { + false, true, false, true, false, //End of bit revs + false, false, true, false, //0010 0x2 + false, false, true, true, //0011 0x3 + true, true, true, false, //1110 0xE + true, false, true, true //1011 0xB + } ), + + /** + * Note: we use a truncated portion of the NRZ-I encoded sync pattern. + * The truncated version seems to work well enough for detecting the burst, + * while limiting falsing. + * + * Sync: .... .... 0000 0111 0000 1001 0010 1010 0100 0100 0110 1111 + * Rev & Sync: ...1 0101 0000 0111 0000 1001 0010 1010 0100 0100 0110 1111 + * NRZ-I: .... 1111 1000 0100 1000 1101 1011 1111 0110 0110 0101 1000 + * This: .... .... 1000 0100 1000 1101 1011 1111 .... .... .... .... + */ + /* Sync (0x07092A446F) = 0000011100001001001010100100010001101111 */ + MDC1200( new boolean[] + { + false, false, false, false,//0000 0x0 + false, true, true, true, //0111 0x7 + false, false, false, false,//0000 0x0 + true, false, false, true, //1001 0x9 + false, false, true, false, //0010 0x2 + true, false, true, false, //1010 0xA + false, true, false, false, //0100 0x4 + false, true, false, false, //0100 0x4 + false, true, true, false, //0110 0x6 + true, true, true, true //1111 0xF + } ), + + /* Revs (0xA) and Sync(0xA4D7) = 1010 1100010011010111 */ + MPT1327_CONTROL( new boolean[] + { + true, false, true, false, //Includes 4 of 16 rev bits +// false, true, false, true, //Includes 4 of 16 rev bits + true, true, false, false, //1100 0xA + false, true, false, false, //0100 0x4 + true, true, false, true, //1101 0xD + false, true, true, true //0111 0x7 + }), + + /* Revs (0xA) and Sync(0x3B28) = 1010 0011101100101000 */ + MPT1327_TRAFFIC( new boolean[] + { + true, false, true, false, //1010 Includes 4 of 16 rev bits +// false, true, false, true, //1010 Includes 4 of 16 rev bits + false, false, true, true, //0011 0x3 + true, false, true, true, //1011 0xB + false, false, true, false, //0010 0x2 + true, false, false, false //1000 0x8 + }), + + /* Sync (0x158) = 1101011000 */ + PASSPORT( new boolean[] + { + true, //0001 0x1 + false, true, false, true, //0101 0x5 + true, false, false, false //1000 0x8 + } ), + + LTR_STANDARD_OSW( new boolean[] + { + true,false,true,false,true,true,false,false,false + } ), + + LTR_STANDARD_ISW( new boolean[] + { + false, true, false, true,false, false, true, true, true + } ); + + boolean[] mBits; + + private SyncPattern( boolean[] bits ) + { + mBits = bits; + } + + public boolean[] getPattern() + { + return mBits; + } +} diff --git a/src/bits/SyncPatternMatcher.java b/src/bits/SyncPatternMatcher.java new file mode 100644 index 00000000..d3c3c1fa --- /dev/null +++ b/src/bits/SyncPatternMatcher.java @@ -0,0 +1,74 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package bits; + + +/** + * Sync pattern matcher. + * + * Note: works for sync patterns up to 63 bits (integer size - 1 ) long. + * If a 64 bit or larger sync pattern is applied, then the wrapping feature + * of the Integer.rotate methods will wrap the MSB around to the LSB and + * corrupt the matching value. + * + */ +public class SyncPatternMatcher +{ + private long mBits = 0; + private long mMask = 0; + private long mSync = 0; + + public SyncPatternMatcher( boolean[] syncPattern ) + { + //Setup a bit mask of all ones the length of the sync pattern + mMask = (long)( ( Math.pow( 2, syncPattern.length ) ) - 1 ); + + //Convert the sync bits into a long value for comparison + for( int x = 0; x < syncPattern.length; x++ ) + { + if( syncPattern[ x ] ) + { + mSync += 1l << ( syncPattern.length - 1 - x ); + } + } + } + + public void receive( boolean bit ) + { + //Left shift the previous value + mBits = Long.rotateLeft( mBits, 1 ); + + //Apply the mask to erase the previous MSB + mBits &= mMask; + + //Add in the new bit + if( bit ) + { + mBits += 1; + } + } + + /** + * Indicates if the most recently received bit sequence matches the + * sync pattern + */ + public boolean matches() + { + return ( mBits == mSync ); + } +} diff --git a/src/buffer/AveragingCircularBooleanBuffer.java b/src/buffer/AveragingCircularBooleanBuffer.java new file mode 100644 index 00000000..714d34dd --- /dev/null +++ b/src/buffer/AveragingCircularBooleanBuffer.java @@ -0,0 +1,107 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package buffer; + +import java.util.Arrays; + + +/** + * Circular Buffer - implements a circular buffer with a method to get the + * current head of the buffer, and put a new value in its place at the same + * time + * + * Also provides averaging over the elements in the buffer + */ +public class AveragingCircularBooleanBuffer { + + private boolean[] mBuffer; + private int mBufferPointer; + + public AveragingCircularBooleanBuffer( int length ) + { + mBuffer = new boolean[ length ]; + + //Preload the array with false values + Arrays.fill( mBuffer, false ); + } + + public boolean get( boolean newValue ) + { + //Fetch current boolean value + boolean retVal = mBuffer[ mBufferPointer ]; + + //Load the new value into the buffer + put( newValue ); + + return retVal; + } + + /** + * Loads the newValue into this buffer and adjusts the buffer pointer + * to prepare for the next get/put cycle + */ + private void put( boolean newValue ) + { + //Store the new value to the buffer + mBuffer[ mBufferPointer ] = newValue; + + //Increment the buffer pointer + mBufferPointer++; + + //Wrap the buffer pointer around to 0 when necessary + if( mBufferPointer >= mBuffer.length ) + { + mBufferPointer = 0; + } + } + + /** + * Loads the newValue into the buffer, calculates the average + * and returns that average from this method + * + * This effectively performs low-pass filtering + */ + public boolean getAverage( boolean newValue ) + { + //Load the new value into the buffer + put( newValue ); + + int trueCount = 0; + + for( int x = 0; x < mBuffer.length; x++ ) + { + if( mBuffer[ x ] ) + { + trueCount++; + } + } + + /** + * If the number of true values in the buffer is + * more than half, return true, otherwise, false + */ + if( trueCount > (int)( mBuffer.length / 2 ) ) + { + return true; + } + else + { + return false; + } + } +} diff --git a/src/buffer/BufferOverflowException.java b/src/buffer/BufferOverflowException.java new file mode 100644 index 00000000..4b6e1a57 --- /dev/null +++ b/src/buffer/BufferOverflowException.java @@ -0,0 +1,33 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package buffer; + +public class BufferOverflowException extends Exception +{ + private static final long serialVersionUID = 1L; + + public BufferOverflowException( String text ) + { + super( text ); + } + + public BufferOverflowException( String text, Throwable exc ) + { + super( text, exc ); + } +} diff --git a/src/buffer/BufferOverflowListener.java b/src/buffer/BufferOverflowListener.java new file mode 100644 index 00000000..2f8acb03 --- /dev/null +++ b/src/buffer/BufferOverflowListener.java @@ -0,0 +1,23 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package buffer; + +public interface BufferOverflowListener +{ + public void overflow( Object source, String message ); +} diff --git a/src/buffer/ByteArrayDelayBuffer.java b/src/buffer/ByteArrayDelayBuffer.java new file mode 100644 index 00000000..fc60703a --- /dev/null +++ b/src/buffer/ByteArrayDelayBuffer.java @@ -0,0 +1,60 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package buffer; + +public class ByteArrayDelayBuffer +{ + private int mBufferSize; + private byte[][] mBuffer = null; + private int mBufferPointer = 0; + + /** + * Implements a delay buffer for byte arrays. As a new byte[] is placed + * into the buffer, the oldest byte[] is returned + * @param bufferSize - number of byte arrays to buffer + */ + public ByteArrayDelayBuffer( int bufferSize ) + { + mBufferSize = bufferSize; + } + + /** + * Inserts new bytes into the buffer + * + * @return - oldest byte[] from the buffer, or null if the buffer is not + * yet full + */ + public byte[] get( byte[] bytes ) + { + if( mBuffer == null ) + { + mBuffer = new byte[ mBufferSize ][ bytes.length ]; + } + + byte[] returnBytes = mBuffer[ mBufferPointer ]; + + mBuffer[ mBufferPointer++ ] = bytes; + + if( mBufferPointer >= mBufferSize ) + { + mBufferPointer = 0; + } + + return returnBytes; + } +} diff --git a/src/buffer/ByteArrayListener.java b/src/buffer/ByteArrayListener.java new file mode 100644 index 00000000..021b8e2a --- /dev/null +++ b/src/buffer/ByteArrayListener.java @@ -0,0 +1,23 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package buffer; + +public interface ByteArrayListener +{ + public void receive( byte[] data ); +} diff --git a/src/buffer/ByteBufferListener.java b/src/buffer/ByteBufferListener.java new file mode 100644 index 00000000..7df629a0 --- /dev/null +++ b/src/buffer/ByteBufferListener.java @@ -0,0 +1,25 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package buffer; + +import java.nio.ByteBuffer; + +public interface ByteBufferListener +{ + public void receive( ByteBuffer buffer ); +} diff --git a/src/buffer/ByteBufferProvider.java b/src/buffer/ByteBufferProvider.java new file mode 100644 index 00000000..f85962c3 --- /dev/null +++ b/src/buffer/ByteBufferProvider.java @@ -0,0 +1,33 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package buffer; + + + +public interface ByteBufferProvider +{ + /** + * Adds a sample listener to receive samples + */ + public void addByteBufferListener( ByteBufferListener listener ); + + /** + * Removes the sample listener, if it is currently registered as a listener + */ + public void removeByteBufferListener( ByteBufferListener listener ); +} diff --git a/src/buffer/ComplexByteBuffer.java b/src/buffer/ComplexByteBuffer.java new file mode 100644 index 00000000..0fc4e95b --- /dev/null +++ b/src/buffer/ComplexByteBuffer.java @@ -0,0 +1,91 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package buffer; + +import java.nio.ByteBuffer; +import java.util.Enumeration; + +import sample.complex.ComplexSample; + +/** + * Provides a complex sample enumeration wrapper around a byte array containing + * interleaved byte-length I & Q sample values in the range of 0 - 255, converting + * those samples into complex samples with value range of -128 to 0 to 127. + */ +public class ComplexByteBuffer implements Enumeration +{ + private static float[] mValues; + + /** + * Creates a lookup table that converts the 8-bit valued range from 0 - 255 + * into scaled float values of -128 to 0 to 127 + */ + static + { + mValues = new float[ 256 ]; + + float scale = (float)( 1.0f / 127.0f ); + + for( int x = 0; x < 256; x++ ) + { + mValues[ x ] = (float)( x - 128 ) * scale; + } + } + + private ByteBuffer mBytes; +// private byte[] mBytes; +// private int mPointer; + + public ComplexByteBuffer( byte[] bytes ) + { + mBytes = ByteBuffer.wrap( bytes ); +// mBytes = bytes; +// mPointer = 0; + } + + public byte[] toArray() + { +// return mBytes; + return mBytes.array(); + } + + /** + * Indicates if there are more complex samples remaining. + */ + @Override + public boolean hasMoreElements() + { +// return mPointer < mBytes.length; + return mBytes.hasRemaining(); + } + + /** + * Returns the next complex sample. + * + * Note: does not check for buffer overrun. Use the hasMoreElements() + * method to ensure you do not read past the end of the buffer. + */ + @Override + public ComplexSample nextElement() + { +// return new ComplexSample( mValues[ ( mBytes[ mPointer++ ] & 0xFF ) ], +// mValues[ ( mBytes[ mPointer++ ] & 0xFF ) ] ); + return new ComplexSample( mValues[ ( mBytes.get() & 0xFF ) ], + mValues[ ( mBytes.get() & 0xFF ) ] ); + } +} diff --git a/src/buffer/ComplexByteBufferListener.java b/src/buffer/ComplexByteBufferListener.java new file mode 100644 index 00000000..efdb3a1a --- /dev/null +++ b/src/buffer/ComplexByteBufferListener.java @@ -0,0 +1,23 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package buffer; + +public interface ComplexByteBufferListener +{ + public void receive( ComplexByteBuffer buffer ); +} diff --git a/src/buffer/ComplexCircularBuffer.java b/src/buffer/ComplexCircularBuffer.java new file mode 100644 index 00000000..d782ea31 --- /dev/null +++ b/src/buffer/ComplexCircularBuffer.java @@ -0,0 +1,90 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package buffer; + +import java.util.Arrays; + +import sample.complex.ComplexSample; + +/** + * Circular sample buffer - allocates a buffer and stores samples in a circular + * fashion, overwriting older samples with newly arrived samples. Initially + * fills buffer with 0-valued samples + * + * Can be used as a delay-type buffer, to delay samples by the 'size' amount + */ +public class ComplexCircularBuffer +{ + ComplexSample[] mBuffer; + int mBufferPointer = 0; + + public ComplexCircularBuffer( int size ) + { + mBuffer = new ComplexSample[ size ]; + + Arrays.fill( mBuffer, new ComplexSample( 1.0f, 1.0f ) ); + } + + /** + * Returns the sample currently pointed to in the buffer and stores the + * new sample in its place + * @param sample + * @return + */ + public ComplexSample get( ComplexSample sample ) + { + ComplexSample retVal = mBuffer[ mBufferPointer ]; + + put( sample ); + + return retVal; + } + + public void put( ComplexSample sample ) + { + mBuffer[ mBufferPointer ] = sample; + + mBufferPointer++; + + if( mBufferPointer >= mBuffer.length ) + { + mBufferPointer = 0; + } + } + + public ComplexSample[] get( int count ) + { + ComplexSample[] samples = new ComplexSample[ count ]; + + int tempCounter = mBufferPointer; + + for( int x = 0; x < count; x++ ) + { + samples[ x ] = mBuffer[ tempCounter ]; + + tempCounter++; + + if( tempCounter > mBuffer.length - 1 ) + { + tempCounter = 0; + } + } + + return samples; + } +} diff --git a/src/buffer/ComplexSampleBufferAssembler.java b/src/buffer/ComplexSampleBufferAssembler.java new file mode 100644 index 00000000..c5b2497f --- /dev/null +++ b/src/buffer/ComplexSampleBufferAssembler.java @@ -0,0 +1,73 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package buffer; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.util.Arrays; + +import sample.complex.ComplexSample; + +public class ComplexSampleBufferAssembler +{ + private ByteBuffer mByteBuffer; + private int mSizeInBytes; + + public ComplexSampleBufferAssembler( int size ) + { + mSizeInBytes = size * 4; + + //Allocate a byte buffer with 4 bytes per sample (ie 2 x 16-bit samples) + mByteBuffer = ByteBuffer.allocate( mSizeInBytes ); + mByteBuffer.order( ByteOrder.LITTLE_ENDIAN ); + } + + /** + * Load the sample's bytes, left then right, little endian, into the + * backing byte array + */ + public void put( ComplexSample sample ) + { + mByteBuffer.putShort( (short)sample.left() ); + mByteBuffer.putShort( (short)sample.right() ); + } + + /** + * Indicates if there is room for more samples in the buffer, or if it is full + */ + public boolean hasRemaining() + { + return mByteBuffer.hasRemaining(); + } + + /** + * Returns a copy of the byte array backing this buffer + */ + public ByteBuffer get() + { + return ByteBuffer.wrap( Arrays.copyOf( mByteBuffer.array(), mSizeInBytes ) ); + } + + /** + * Clear the bytes from the underlying buffer and reset the pointer to 0. + */ + public void clear() + { + mByteBuffer.clear(); + } +} diff --git a/src/buffer/ComplexTappedCircularBuffer.java b/src/buffer/ComplexTappedCircularBuffer.java new file mode 100644 index 00000000..1fad24ef --- /dev/null +++ b/src/buffer/ComplexTappedCircularBuffer.java @@ -0,0 +1,87 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package buffer; + +import sample.complex.ComplexSample; + +/** + * Implements a FIFO circular buffer for complex samples that provides access + * to each complex sample by index number, where index 0 is the newest/latest + * complex sample and index (length - 1) is the oldest sample in the buffer. + */ +public class ComplexTappedCircularBuffer +{ + private ComplexSample[] mBuffer; + private int mBufferPointer = 0; + + public ComplexTappedCircularBuffer( int length ) + { + mBuffer = new ComplexSample[ length ]; + + //Pre-load the buffer with 0-valued samples + for( int x = 0; x < length; x++ ) + { + mBuffer[ x ] = new ComplexSample( 0, 0 ); + } + } + + /** + * Adds the sample to the buffer and removes the oldest sample + * @param sample + */ + public void add( ComplexSample sample ) + { + mBuffer[ mBufferPointer++ ] = sample; + + //Reset the buffer pointer once it points past the end of the buffer + if( mBufferPointer >= mBuffer.length ) + { + mBufferPointer = 0; + } + } + + /** + * Retrieves a sample currently in the buffer by index position. + * + * Index 0 is the newest/latest sample in the buffer and index (length - 1) + * is the oldest. + * + * Note: the buffer is preloaded with complex samples of 0,0 value. + * + * @param index where 0 is the newest/latest sample and length-1 is the oldest + * @return complex sample from the buffer. + * @throws ArrayIndexOutOfBoundsException if you attempt to access a sample + * outside of the buffer size + */ + public ComplexSample get( int index ) + { + /* Since the samples are stored in a forward fashion, we have to access + * the samples in reverse to comply with the access contract, where + * index 0 is the newest and index (length -1) is the oldest, all + * relative to the current buffer pointer */ + + int pointer = mBuffer.length - 1 + mBufferPointer - index; + + if( pointer >= mBuffer.length ) + { + pointer -= mBuffer.length; + } + + return mBuffer[ pointer ]; + } +} diff --git a/src/buffer/FloatArrayCircularAveragingBuffer.java b/src/buffer/FloatArrayCircularAveragingBuffer.java new file mode 100644 index 00000000..5003d789 --- /dev/null +++ b/src/buffer/FloatArrayCircularAveragingBuffer.java @@ -0,0 +1,97 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package buffer; + +import java.util.ArrayList; +import java.util.Arrays; + +public class FloatArrayCircularAveragingBuffer +{ + private ArrayList mBuffer; + private int mBufferPointer; + private int mBufferSize; + private float[] mAverage; + + public FloatArrayCircularAveragingBuffer( int bufferSize ) + { + mBufferSize = bufferSize; + mBuffer = new ArrayList(); + } + + /** + * Replaces the oldest float array element in the buffer with this new + * float array element, and then returns the average of the buffer contents + * with the newElement values included in the average array + */ + public float[] get( float[] newElement ) + { + /** + * If we've never setup the average array, or the newElements length is + * different than what we've been processing, reset the buffer & averages + */ + if( mAverage == null || mAverage.length != newElement.length ) + { + reset( newElement.length ); + } + + float[] oldElement = mBuffer.get( mBufferPointer ); + + for( int x = 0; x < newElement.length; x++ ) + { + /** + * Change the current average value in each array element by the + * difference of the new element minus the old element, divided + * by the buffer size ... average it + */ + mAverage[ x ] += + ( ( newElement[ x ] - oldElement[ x ] ) / mBufferSize ); + } + + mBuffer.set( mBufferPointer, newElement ); + + mBufferPointer++; + + if( mBufferPointer >= mBufferSize ) + { + mBufferPointer = 0; + } + + return mAverage; + } + + /** + * Clears buffer and average and fills buffer with zero-valued float arrays + */ + private void reset( int arrayLength ) + { + mAverage = new float[ arrayLength ]; + Arrays.fill( mAverage, 0.0f ); + + mBuffer.clear(); + mBufferPointer = 0; + + float[] emptyArray = new float[ arrayLength ]; + Arrays.fill( emptyArray, 0.0f ); + + for( int x = 0; x < mBufferSize; x++ ) + { + mBuffer.add( emptyArray ); + } + } + +} diff --git a/src/buffer/FloatAveragingBuffer.java b/src/buffer/FloatAveragingBuffer.java new file mode 100644 index 00000000..9fb88be3 --- /dev/null +++ b/src/buffer/FloatAveragingBuffer.java @@ -0,0 +1,57 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package buffer; + +public class FloatAveragingBuffer +{ + private float[] mBuffer; + private float mAverage = 0.0f; + private int mBufferSize; + private int mBufferPointer; + + public FloatAveragingBuffer( int size ) + { + mBufferSize = size; + mBuffer = new float[ size ]; + } + + public float get( float newValue ) + { + float oldValue = mBuffer[ mBufferPointer ]; + + if( Float.isInfinite( newValue ) || Float.isNaN( newValue ) ) + { + mAverage = mAverage - ( oldValue / mBufferSize ); + + mBuffer[ mBufferPointer++ ] = 0.0f; + } + else + { + mAverage = mAverage - ( oldValue / mBufferSize ) + ( newValue / mBufferSize ); + + mBuffer[ mBufferPointer++ ] = newValue; + } + + if( mBufferPointer >= mBufferSize ) + { + mBufferPointer = 0; + } + + return mAverage; + } +} diff --git a/src/buffer/FloatBufferAssembler.java b/src/buffer/FloatBufferAssembler.java new file mode 100644 index 00000000..651a1794 --- /dev/null +++ b/src/buffer/FloatBufferAssembler.java @@ -0,0 +1,70 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package buffer; + +import java.util.Arrays; + +import sample.Listener; + + +/** + * Assembles single samples into sample blocks (ie arrays) for broadcast to + * registered SampleBlockListeners + */ +public class FloatBufferAssembler implements Listener +{ + private Float[] mFloatBuffer; + private int mBufferSize; + private int mBufferPointer = 0; + private Listener mListener; + + public FloatBufferAssembler( int bufferSize ) + { + mBufferSize = bufferSize; + mFloatBuffer = new Float[ mBufferSize ]; + } + + /** + * Stores received samples in the sample block and manages the storage + * pointer. Sends sample blocks when full. + */ + @Override + public void receive( Float sample ) + { + mFloatBuffer[ mBufferPointer++ ] = sample; + + if( mBufferPointer == mBufferSize ) + { + if( mListener != null ) + { + mListener.receive( Arrays.copyOf( mFloatBuffer, mBufferSize ) ); + } + + mBufferPointer = 0; + } + } + + /** + * Adds a new sampleblock listener to receive sample blocks as they are + * assembled from the sample stream + */ + public void setListener( Listener listener ) + { + mListener = listener; + } +} diff --git a/src/buffer/FloatCircularAveragingBuffer.java b/src/buffer/FloatCircularAveragingBuffer.java new file mode 100644 index 00000000..6a05ea61 --- /dev/null +++ b/src/buffer/FloatCircularAveragingBuffer.java @@ -0,0 +1,110 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package buffer; + +import java.util.Arrays; + +/** + * Circular sample buffer - allocates a buffer and stores samples in a circular + * fashion, overwriting older samples with newly arrived samples. Initially + * fills buffer with 0-valued samples + * + * Produces no output. Obtain the average buffer value on-demand via the + * getAverage() method + */ +public class FloatCircularAveragingBuffer +{ + private float[] mBuffer; + private int mBufferPointer = 0; + private int mSampleCountForAveraging; + private int mCurrentSampleCount = 0; + private float mAverage = 0.0f; + + public FloatCircularAveragingBuffer( int size, int sampleCountForAveraging ) + { + mBuffer = new float[ size ]; + mSampleCountForAveraging = sampleCountForAveraging; + Arrays.fill( mBuffer, 0.0f ); + } + + /** + * Convenience wrapper to throw away to put the new sample and throwaway + * the oldest sample + */ + public void put( float newestSample ) + { + get( newestSample ); + } + + /** + * Loads the newest sample into the buffer and returns the oldest sample + */ + public float get( float newestSample ) + { + float oldestSample = mBuffer[ mBufferPointer ]; + + mBuffer[ mBufferPointer ] = newestSample; + + mBufferPointer++; + + if( mBufferPointer >= mBuffer.length ) + { + mBufferPointer = 0; + } + + mCurrentSampleCount++; + + if( mCurrentSampleCount >= mSampleCountForAveraging ) + { + calculateAverage(); + + mCurrentSampleCount = 0; + } + + return oldestSample; + } + + /** + * Calculates the average of the maximum and minimum values present in + * the buffer + */ + private void calculateAverage() + { + float min = mBuffer[ 0 ]; + float max = mBuffer[ 0 ]; + + for( int x = 0; x < mBuffer.length; x++ ) + { + if( mBuffer[ x ] < min ) + { + min = mBuffer[ x ]; + } + if( mBuffer[ x ] > max ) + { + max = mBuffer[ x ]; + } + } + + mAverage = (float)( ( min + max ) / 2.0f ); + } + + public float getAverage() + { + return mAverage; + } +} diff --git a/src/buffer/FloatCircularAveragingBuffer2.java b/src/buffer/FloatCircularAveragingBuffer2.java new file mode 100644 index 00000000..da27d207 --- /dev/null +++ b/src/buffer/FloatCircularAveragingBuffer2.java @@ -0,0 +1,68 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package buffer; + +import java.util.Arrays; + +/** + * Circular sample buffer - stores samples in a circular buffer + * overwriting older samples with newly arrived samples. Initially + * fills buffer with 0-valued samples + * + * Continuously calculates the average of the samples in the buffer, with the + * average available from the average() method. + */ +public class FloatCircularAveragingBuffer2 +{ + private float[] mBuffer; + private int mBufferPointer = 0; + private float mAverage = 0.0f; + + public FloatCircularAveragingBuffer2( int size ) + { + mBuffer = new float[ size ]; + Arrays.fill( mBuffer, 0.0f ); + } + + /** + * Loads the newest sample into the buffer and returns the oldest sample + */ + public float get( float newestSample ) + { + float oldestSample = mBuffer[ mBufferPointer ]; + + mBuffer[ mBufferPointer ] = newestSample; + + mBufferPointer++; + + if( mBufferPointer >= mBuffer.length ) + { + mBufferPointer = 0; + } + + mAverage = mAverage - ( oldestSample / mBuffer.length ) + + ( newestSample / mBuffer.length ); + + return oldestSample; + } + + public float average() + { + return mAverage; + } +} diff --git a/src/buffer/FloatCircularBuffer.java b/src/buffer/FloatCircularBuffer.java new file mode 100644 index 00000000..c03ec75e --- /dev/null +++ b/src/buffer/FloatCircularBuffer.java @@ -0,0 +1,108 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package buffer; + +import java.util.Arrays; + +/** + * Circular sample buffer - allocates a buffer and stores samples in a circular + * fashion, overwriting older samples with newly arrived samples. Initially + * fills buffer with 0-valued samples + * + * Can be used as a delay-type buffer, to delay samples by the 'size' amount + * @author denny + * + */ +public class FloatCircularBuffer +{ + float[] mBuffer; + int mBufferPointer = 0; + + public FloatCircularBuffer( int size ) + { + mBuffer = new float[ size ]; + + Arrays.fill( mBuffer, (float)0.0 ); + } + + /** + * Puts the new value into the buffer and returns the oldest buffer value + * that it replaced + * @param newValue + * @return + */ + public float get( float newValue ) + { + float oldestSample = mBuffer[ mBufferPointer ]; + + mBuffer[ mBufferPointer ] = newValue; + + mBufferPointer++; + + if( mBufferPointer >= mBuffer.length ) + { + mBufferPointer = 0; + } + + return oldestSample; + } + + /** + * Convenience method to put a new value in the buffer, while discarding + * the old value + */ + public void put( float newValue ) + { + get( newValue ); + } + + /** + * Returns an array of arraySize, loaded with sampleCount of the + * oldest samples, loaded into the array starting at index 0. + * + * This is useful for fetching an array filled half way with samples to + * use in FFT processing. + * @param arraySize - size of the array (must be larger than sampleCount) + * @param sampleCount - number of the oldest samples in the buffer to load + * @return + */ + public float[] get( int arraySize, int sampleCount ) + { + float[] samples = new float[ arraySize ]; + + if( arraySize > sampleCount ) + { + int tempCounter = mBufferPointer; + + for( int x = 0; x < sampleCount; x++ ) + { + samples[ x ] = mBuffer[ tempCounter ]; + + tempCounter++; + + if( tempCounter > mBuffer.length ) + { + tempCounter = 0; + } + } + } + + return samples; + } + +} diff --git a/src/buffer/FloatSampleBuffer.java b/src/buffer/FloatSampleBuffer.java new file mode 100644 index 00000000..bca43189 --- /dev/null +++ b/src/buffer/FloatSampleBuffer.java @@ -0,0 +1,66 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package buffer; + +import java.nio.ByteBuffer; + + +public class FloatSampleBuffer +{ + private static final long serialVersionUID = 1L; + + private int mSize; + private float[] mBuffer; + private int mBufferPointer = 0; + + public FloatSampleBuffer( int size ) + { + super(); + mBuffer = new float[ size ]; + } + + public void put( float sample ) + { + if( !isFull() ) + { + mBuffer[ mBufferPointer++ ] = sample; + } + } + + public void reset() + { + mBufferPointer = 0; + } + + public boolean isFull() + { + return mBufferPointer >= mBuffer.length; + } + + public byte[] getBytes() + { + ByteBuffer bb = ByteBuffer.allocate( mBuffer.length * 2 ); + + for( int x = 0; x < mBuffer.length; x++ ) + { + bb.putShort( ((Float)mBuffer[ x ]).shortValue() ); + } + + return bb.array(); + } +} diff --git a/src/buffer/FloatSampleBufferAssembler.java b/src/buffer/FloatSampleBufferAssembler.java new file mode 100644 index 00000000..babb7a8b --- /dev/null +++ b/src/buffer/FloatSampleBufferAssembler.java @@ -0,0 +1,72 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package buffer; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; + +public class FloatSampleBufferAssembler +{ + private ByteBuffer mByteBuffer; + private int mSizeInBytes; + + public FloatSampleBufferAssembler( int size ) + { + mSizeInBytes = size * 2; + + //Allocate a byte buffer with 2 bytes per sample (ie 1 x 16-bit sample) + mByteBuffer = ByteBuffer.allocate( mSizeInBytes ); + mByteBuffer.order( ByteOrder.LITTLE_ENDIAN ); + } + + /** + * Load the sample's bytes, left then right, little endian, into the + * backing byte array + */ + public void put( float sample ) + { + mByteBuffer.putShort( Float.valueOf( sample ).shortValue() ); + } + + /** + * Indicates if there is room for more samples in the buffer, or if it is full + */ + public boolean hasRemaining() + { + return mByteBuffer.hasRemaining(); + } + + /** + * Returns a copy of the byte array backing this buffer + */ + public ByteBuffer get() + { + return mByteBuffer; +// return ByteBuffer.wrap( Arrays.copyOf( mByteBuffer.array(), mSizeInBytes ) ); + } + + /** + * Clear the bytes from the underlying buffer and reset the pointer to 0. + */ + public void reset() + { +// mByteBuffer.clear(); + mByteBuffer = ByteBuffer.allocate( mSizeInBytes ); + mByteBuffer.order( ByteOrder.LITTLE_ENDIAN ); + } +} diff --git a/src/buffer/FloatTappedCircularBuffer.java b/src/buffer/FloatTappedCircularBuffer.java new file mode 100644 index 00000000..5d0c3e7f --- /dev/null +++ b/src/buffer/FloatTappedCircularBuffer.java @@ -0,0 +1,86 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package buffer; + + +/** + * Implements a FIFO circular buffer for floats that provides access + * to each sample by index number, where index 0 is the newest/latest + * complex sample and index (length - 1) is the oldest sample in the buffer. + */ +public class FloatTappedCircularBuffer +{ + private Float[] mBuffer; + private int mBufferPointer = 0; + + public FloatTappedCircularBuffer( int length ) + { + mBuffer = new Float[ length ]; + + //Pre-load the buffer with 0-valued samples + for( int x = 0; x < length; x++ ) + { + mBuffer[ x ] = Float.valueOf( 0.0f ); + } + } + + /** + * Adds the sample to the buffer and removes the oldest sample + * @param sample + */ + public void add( Float sample ) + { + mBuffer[ mBufferPointer++ ] = sample; + + //Reset the buffer pointer once it points past the end of the buffer + if( mBufferPointer >= mBuffer.length ) + { + mBufferPointer = 0; + } + } + + /** + * Retrieves a sample currently in the buffer by index position. + * + * Index 0 is the newest/latest sample in the buffer and index (length - 1) + * is the oldest. + * + * Note: the buffer is preloaded with complex samples of 0,0 value. + * + * @param index where 0 is the newest/latest sample and length-1 is the oldest + * @return complex sample from the buffer. + * @throws ArrayIndexOutOfBoundsException if you attempt to access a sample + * outside of the buffer size + */ + public Float get( int index ) + { + /* Since the samples are stored in a forward fashion, we have to access + * the samples in reverse to comply with the access contract, where + * index 0 is the newest and index (length -1) is the oldest, all + * relative to the current buffer pointer */ + + int pointer = mBuffer.length - 1 + mBufferPointer - index; + + if( pointer >= mBuffer.length ) + { + pointer -= mBuffer.length; + } + + return mBuffer[ pointer ]; + } +} diff --git a/src/buffer/FloatToByteBufferAssembler.java b/src/buffer/FloatToByteBufferAssembler.java new file mode 100644 index 00000000..5f24ffce --- /dev/null +++ b/src/buffer/FloatToByteBufferAssembler.java @@ -0,0 +1,69 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package buffer; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; + +import sample.Broadcaster; +import sample.Listener; + +public class FloatToByteBufferAssembler implements Listener +{ + private Broadcaster mBroadcaster = new Broadcaster(); + + private int mBufferSize = 0; + private ByteBuffer mBuffer; + + public FloatToByteBufferAssembler( int bufferSize ) + { + mBufferSize = bufferSize; + resetBuffer(); + } + + public void addByteBufferListener( Listener listener ) + { + mBroadcaster.addListener( listener ); + } + + public void removeByteBufferListener( Listener listener ) + { + mBroadcaster.removeListener( listener ); + } + + private void resetBuffer() + { + mBuffer = ByteBuffer.allocate( mBufferSize ); + mBuffer.order( ByteOrder.BIG_ENDIAN ); + } + + @Override + public void receive( Float sample ) + { + mBuffer.putShort( Float.valueOf( sample ).shortValue() ); + + if( mBuffer.position() >= mBufferSize ) + { + //Buffer is full ... send it + mBroadcaster.receive( mBuffer ); + + //Reset the buffer + resetBuffer(); + } + } +} diff --git a/src/buffer/ShortComplexByteBuffer.java b/src/buffer/ShortComplexByteBuffer.java new file mode 100644 index 00000000..2a613797 --- /dev/null +++ b/src/buffer/ShortComplexByteBuffer.java @@ -0,0 +1,53 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package buffer; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.util.Enumeration; + +import sample.complex.ComplexSample; + +public class ShortComplexByteBuffer implements Enumeration +{ + private ByteBuffer mByteBuffer; + + public ShortComplexByteBuffer( byte[] bytes, ByteOrder endianness ) + { + mByteBuffer = ByteBuffer.wrap( bytes ); + mByteBuffer.order( endianness ); + } + + public byte[] array() + { + return mByteBuffer.array(); + } + + @Override + public boolean hasMoreElements() + { + return mByteBuffer.hasRemaining(); + } + + @Override + public ComplexSample nextElement() + { + return new ComplexSample( (float)mByteBuffer.getShort(), + (float)mByteBuffer.getShort() ); + } +} diff --git a/src/buffer/StereoByteBuffer.java b/src/buffer/StereoByteBuffer.java new file mode 100644 index 00000000..1d7688da --- /dev/null +++ b/src/buffer/StereoByteBuffer.java @@ -0,0 +1,59 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package buffer; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.util.Enumeration; + +import sample.complex.ComplexSample; +import source.mixer.MixerChannel; + +public class StereoByteBuffer implements Enumeration +{ + private ByteBuffer mByteBuffer; + + public StereoByteBuffer( byte[] bytes, int frames ) + { + mByteBuffer = ByteBuffer.wrap( bytes ); + mByteBuffer.order( ByteOrder.LITTLE_ENDIAN ); + } + + public byte[] array() + { + return mByteBuffer.array(); + } + + public ByteBuffer getChannel( MixerChannel channel ) + { + return StereoToMonoByteBufferConverter.getChannel( mByteBuffer, channel ); + } + + @Override + public boolean hasMoreElements() + { + return mByteBuffer.hasRemaining(); + } + + @Override + public ComplexSample nextElement() + { + return new ComplexSample( (float)mByteBuffer.getShort(), + (float)mByteBuffer.getShort() ); + } +} diff --git a/src/buffer/StereoByteBufferListener.java b/src/buffer/StereoByteBufferListener.java new file mode 100644 index 00000000..641305c1 --- /dev/null +++ b/src/buffer/StereoByteBufferListener.java @@ -0,0 +1,24 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package buffer; + + +public interface StereoByteBufferListener +{ + public void receive( StereoByteBuffer buffer ); +} diff --git a/src/buffer/StereoByteBufferProvider.java b/src/buffer/StereoByteBufferProvider.java new file mode 100644 index 00000000..76f91930 --- /dev/null +++ b/src/buffer/StereoByteBufferProvider.java @@ -0,0 +1,32 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package buffer; + + +public interface StereoByteBufferProvider +{ + /** + * Adds a sample listener to receive samples + */ + public void addStereoByteBufferListener( StereoByteBufferListener listener ); + + /** + * Removes the sample listener, if it is currently registered as a listener + */ + public void removeStereoByteBufferListener( StereoByteBufferListener listener ); +} diff --git a/src/buffer/StereoToMonoByteBufferConverter.java b/src/buffer/StereoToMonoByteBufferConverter.java new file mode 100644 index 00000000..55e43aca --- /dev/null +++ b/src/buffer/StereoToMonoByteBufferConverter.java @@ -0,0 +1,58 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package buffer; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; + +import source.mixer.MixerChannel; + +public class StereoToMonoByteBufferConverter +{ + public static ByteBuffer getChannel( ByteBuffer buffer, MixerChannel channel ) + { + byte[] channelBuffer = new byte[ buffer.capacity() / 2 ]; + + + int channelBufferPointer = 0; + + if( channel == MixerChannel.RIGHT ) + { + //throw away the first two bytes + buffer.get(); + buffer.get(); + } + + while( channelBufferPointer < channelBuffer.length ) + { + if( buffer.hasRemaining() ) + { + channelBuffer[ channelBufferPointer++ ] = buffer.get(); + channelBuffer[ channelBufferPointer++ ] = buffer.get(); + } + + if( buffer.hasRemaining() ) + { + buffer.get(); + buffer.get(); + } + } + + return ByteBuffer.wrap( channelBuffer ).order( ByteOrder.LITTLE_ENDIAN ); + } +} diff --git a/src/controller/BaseNode.java b/src/controller/BaseNode.java new file mode 100644 index 00000000..8d9de45a --- /dev/null +++ b/src/controller/BaseNode.java @@ -0,0 +1,177 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package controller; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import javax.swing.JPanel; +import javax.swing.JPopupMenu; +import javax.swing.tree.DefaultMutableTreeNode; + +public class BaseNode extends DefaultMutableTreeNode + implements Comparable +{ + private static final long serialVersionUID = 1L; + + /** + * Model backing the JTree and nodes. Since we're primarily interacting + * with the model via JTree tree nodes via context menus, injecting a + * reference to the model facilitates nodal access to the model's methods + */ + protected ConfigurationControllerModel mModel = null; + + /** + * Implements a base node that has a reference to the underlying + * tree data model to facilitate tree node operations from nodal context + * menus. + */ + public BaseNode( Object object ) + { + super( object ); + } + + /** + * Override this method to return a custom icon for the node + * @return + */ + public String getIconPath() + { + return null; + } + + public String toString() + { + if( getUserObject() != null ) + { + return (String)getUserObject(); + } + else + { + return "SDRTrunk"; + } + } + + public JPanel getEditor() + { + return new EmptyEditor(); + } + + /** + * Comparator to support nodal sorting/ordering + */ + @Override + public int compareTo( BaseNode node ) + { + return toString().compareTo( node.toString() ); + } + + /** + * This method only needs to be called once on the root node and all nodes + * in the tree will then have a reference to the model via getModel() + */ + public void setModel( ConfigurationControllerModel model ) + { + mModel = model; + } + + /** + * Returns reference to the SystemTreeModel. Only ConfigurableNode types + * or children are allowed in this tree, so this method assumes that the + * node or parent is of ConfigurableNode class or subclass. + * + * Reference to the model is required to enable node context operations + * like adding/deleting/stopping/starting. + */ + public ConfigurationControllerModel getModel() + { + if( mModel == null ) + { + return ((BaseNode)getParent()).getModel(); + } + else + { + return mModel; + } + } + + /** + * Supports a context menu for any children of the BaseNode. Override the + * method in the child node to provide a custom context menu for that node. + */ + public JPopupMenu getContextMenu() + { + return null; + } + + /** + * Invokes a recursive delete() on all children to perform any cleanup + * operations prior to deleting. Override this method to perform cleanup + * operations specific to the node. Remember to also either invoke + * super.delete(), or specifically call .delete() on any children of the + * node with the custom delete method. + */ + public void delete() + { + for( int x = 0; x < getChildCount(); x++ ) + { + BaseNode node = (BaseNode)getChildAt( x ); + + //Tell the node to delete its children + node.delete(); + + //Delete the child node + getModel().deleteNode( node ); + } + } + + public void show() + { + getModel().showNode( this ); + } + + public void refresh() + { + getModel().nodeChanged( this ); + } + + public void sort() + { + List nodesToSort = new ArrayList(); + + while( children().hasMoreElements() ) + { + BaseNode child = (BaseNode)children().nextElement(); + + nodesToSort.add( child ); + + getModel().removeNodeFromParent( child ); + } + + if( !nodesToSort.isEmpty() ) + { + Collections.sort( nodesToSort ); + + for( BaseNode node: nodesToSort ) + { + getModel().insertNodeInto( node, this, getChildCount() ); + } + } + } +} diff --git a/src/controller/ConfigurableNode.java b/src/controller/ConfigurableNode.java new file mode 100644 index 00000000..f9a6e3be --- /dev/null +++ b/src/controller/ConfigurableNode.java @@ -0,0 +1,44 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package controller; + + +public abstract class ConfigurableNode extends BaseNode +{ + private static final long serialVersionUID = 1L; + + public ConfigurableNode( Object object ) + { + super( object ); + } + + /** + * Saves changes to the node + */ + public void save() + { + getModel().getResourceManager().getPlaylistManager().save(); + + BaseNode parent = (BaseNode)getParent(); + + if( parent instanceof ConfigurableNode ) + { + ((ConfigurableNode)parent).sort(); + } + } +} diff --git a/src/controller/ConfigurationControllerModel.java b/src/controller/ConfigurationControllerModel.java new file mode 100644 index 00000000..a441cf21 --- /dev/null +++ b/src/controller/ConfigurationControllerModel.java @@ -0,0 +1,280 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package controller; + +import java.util.ArrayList; +import java.util.Enumeration; +import java.util.Iterator; +import java.util.List; + +import javax.swing.JTree; +import javax.swing.tree.DefaultMutableTreeNode; +import javax.swing.tree.DefaultTreeModel; +import javax.swing.tree.MutableTreeNode; +import javax.swing.tree.TreePath; + +import map.IconManager; +import playlist.PlaylistNode; +import source.config.SourceConfigTuner; +import source.tuner.Tuner; +import source.tuner.TunerGroupNode; +import source.tuner.TunerNode; +import source.tuner.TunerSelectionListener; +import controller.channel.Channel; +import controller.channel.ChannelListener; +import controller.channel.ChannelNode; +import controller.site.Site; +import controller.system.System; +import controller.system.SystemListNode; +import controller.system.SystemNode; +import decode.DecoderType; +import decode.config.DecodeConfigFactory; + +public class ConfigurationControllerModel extends DefaultTreeModel +{ + private static final long serialVersionUID = 1L; + + private JTree mTree; + + private ResourceManager mResourceManager; + + private ArrayList mChannelConfigChangeListeners = + new ArrayList(); + + private ArrayList mTunerSelectionListeners = + new ArrayList(); + + + private PlaylistNode mPlaylistNode; + + private TunerGroupNode mTunerGroupNode; + + private IconManager mIconManagerFrame; + + /** + * Implements a merged system controller and (tree) model. The user will + * primarily interact with the system nodes via the tree view, so from a + * design perspective, the model and the control are necessarily merged to + * work well with the JTree as the system view. + */ + public ConfigurationControllerModel( ResourceManager resourceManager ) + { + super( new BaseNode( null ) ); + + mResourceManager = resourceManager; + + /** + * Give the root node a reference to this model, so that all nodes can + * invoke add/delete operations on the tree via context menu + */ + ((BaseNode)getRoot()).setModel( this ); + } + + public JTree getTree() + { + return mTree; + } + + public void showIconManager() + { + if( mIconManagerFrame == null ) + { + mIconManagerFrame = new IconManager( mResourceManager, mTree ); + + mResourceManager.getSettingsManager().addListener( mIconManagerFrame ); + } + + if( !mIconManagerFrame.isVisible() ) + { + mIconManagerFrame.setVisible( true ); + } + + mIconManagerFrame.toFront(); + } + + /** + * init() gets invoked manually/externally, after all of the references + * are established in the constructor + */ + public void init() + { + /** + * Add the root tuner group node + */ + mTunerGroupNode = new TunerGroupNode(); + insertNodeInto( mTunerGroupNode, (MutableTreeNode)root, 0 ); + addTuners( mTunerGroupNode ); + + /** + * Add the playlist node + */ + mPlaylistNode = new PlaylistNode(); + insertNodeInto( mPlaylistNode, (MutableTreeNode)root, 1 ); + mPlaylistNode.loadPlaylist(); + } + + public void showNode( DefaultMutableTreeNode node ) + { + if( mTree != null && node != null ) + { + mTree.setSelectionPath( new TreePath( node.getPath() ) ); + } + } + + public ResourceManager getResourceManager() + { + return mResourceManager; + } + + public void setTree( JTree tree ) + { + mTree = tree; + } + + private void addTuners( TunerGroupNode parent ) + { + List tuners = mResourceManager.getTunerManager().getTuners(); + + for( Tuner tuner: tuners ) + { + TunerNode child = new TunerNode( tuner ); + + insertNodeInto( child, parent, parent.getChildCount() ); + + parent.sort(); + + show( child ); + } + + } + + /** + * Convenience method to add a new default system, site and channel to + * the tree + */ + public void createChannel( int frequency, DecoderType decoder ) + { + System system = new System(); + + Site site = new Site(); + system.addSite( site ); + + Channel channel = new Channel(); + channel.setDecodeConfiguration( + DecodeConfigFactory.getDecodeConfiguration( decoder ) ); + SourceConfigTuner sourceConfig = new SourceConfigTuner(); + sourceConfig.setFrequency( frequency ); + channel.setSourceConfiguration( sourceConfig ); + channel.setEnabled( true, true ); + site.addChannel( channel ); + + mPlaylistNode.getPlaylist().getSystemList().addSystem( system ); + + SystemNode systemNode = new SystemNode( system ); + SystemListNode systemListNode = mPlaylistNode.getSystemListNode(); + addNode( systemNode, systemListNode, systemListNode.getChildCount() ); + systemNode.init(); + + systemNode.save(); + } + + public void addNode( BaseNode child, BaseNode parent, int index ) + { + insertNodeInto( child, parent, index ); + show( child ); + } + + public void deleteNode( BaseNode node ) + { + node.delete(); + } + + private void show( BaseNode node ) + { + if( mTree != null ) + { + mTree.scrollPathToVisible( new TreePath( node.getPath() ) ); + } + } + + public void addListener( TunerSelectionListener listener ) + { + mTunerSelectionListeners.add( listener ); + } + + public void removeListener( TunerSelectionListener listener ) + { + mTunerSelectionListeners.remove( listener ); + } + + /** + * Notifies registered listeners that the tuner has been selected + */ + public void fireTunerSelectedEvent( Tuner tuner ) + { + Iterator it = + mTunerSelectionListeners.iterator(); + + while( it.hasNext() ) + { + it.next().tunerSelected( tuner ); + } + } + + /** + * Fires a tuner selection event for the first tuner listed in the tree. + * This is mainly used on application startup to show the spectral display + * of the first tuner in the spectrum panel. + */ + public void fireFirstTunerSelected() + { + if( mTunerGroupNode.getChildCount() > 0 ) + { + TunerNode node = (TunerNode)mTunerGroupNode.getChildAt( 0 ); + + fireTunerSelectedEvent( node.getTuner() ); + } + } + + public ArrayList getCurrentChannelConfigs() + { + return getChannelConfigs( mPlaylistNode ); + } + + private ArrayList getChannelConfigs( DefaultMutableTreeNode parent ) + { + ArrayList configs = new ArrayList(); + + Enumeration nodes = parent.children(); + + while( nodes.hasMoreElements() ) + { + DefaultMutableTreeNode node = nodes.nextElement(); + + if( node instanceof ChannelNode ) + { + configs.add( ((ChannelNode)node).getChannel() ); + } + + //Recursive call + configs.addAll( getChannelConfigs( node ) ); + } + + return configs; + } +} diff --git a/src/controller/ConfigurationEditor.java b/src/controller/ConfigurationEditor.java new file mode 100644 index 00000000..a07765dd --- /dev/null +++ b/src/controller/ConfigurationEditor.java @@ -0,0 +1,75 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package controller; + + +import java.awt.BorderLayout; + +import javax.swing.BorderFactory; +import javax.swing.JPanel; +import javax.swing.JScrollPane; +import javax.swing.JTree; +import javax.swing.event.TreeSelectionEvent; +import javax.swing.event.TreeSelectionListener; + +public class ConfigurationEditor extends JPanel + implements TreeSelectionListener +{ + private static final long serialVersionUID = 1L; + + /** + * Host for configuration editor display panels + */ + public ConfigurationEditor() + { + init(); + } + + public void init() + { + setPanel( new EmptyEditor() ); + } + + public void setPanel( JPanel display ) + { + removeAll(); + + setLayout( new BorderLayout() ); + + JScrollPane scroll = new JScrollPane( display ); + + add( scroll, BorderLayout.CENTER ); + + revalidate(); + repaint(); + } + + @Override + public void valueChanged( TreeSelectionEvent e ) + { + JTree tree = (JTree)e.getSource(); + + BaseNode node = + (BaseNode)tree.getLastSelectedPathComponent(); + + if( node != null ) + { + setPanel( node.getEditor() ); + } + } +} diff --git a/src/controller/ConfigurationTreePanel.java b/src/controller/ConfigurationTreePanel.java new file mode 100644 index 00000000..329de155 --- /dev/null +++ b/src/controller/ConfigurationTreePanel.java @@ -0,0 +1,117 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package controller; + +import java.awt.event.MouseEvent; +import java.awt.event.MouseListener; + +import javax.swing.JPanel; +import javax.swing.JPopupMenu; +import javax.swing.JScrollPane; +import javax.swing.JTree; +import javax.swing.SwingUtilities; +import javax.swing.event.TreeSelectionListener; +import javax.swing.tree.TreeSelectionModel; + +import net.miginfocom.swing.MigLayout; + +public class ConfigurationTreePanel extends JPanel +{ + private static final long serialVersionUID = 1L; + + protected JTree mSystemTree; + protected ConfigurationControllerModel mTreeModel; + + public ConfigurationTreePanel( ConfigurationControllerModel model ) + { + mTreeModel = model; + + initGUI(); + } + + private void initGUI() + { + setLayout( new MigLayout( "insets 0 0 0 0", "[grow,fill]", "[grow,fill][]") ); + + mSystemTree = new JTree( mTreeModel ); + mSystemTree.getSelectionModel().setSelectionMode( + TreeSelectionModel.SINGLE_TREE_SELECTION ); + mSystemTree.setShowsRootHandles( true ); + mSystemTree.addMouseListener( new Handler() ); + mSystemTree.setCellRenderer( new ControllerTreeCellRenderer() ); + + //Give the model a reference to the tree, so that it can force newly + //added nodes to be displayed + mTreeModel.setTree( mSystemTree ); + + JScrollPane treeScroll = new JScrollPane( mSystemTree ); + add( treeScroll, "cell 0 0,span,grow" ); + } + + public void addTreeSelectionListener( TreeSelectionListener listener ) + { + mSystemTree.addTreeSelectionListener( listener ); + } + + public class Handler implements MouseListener + { + public Handler() + { + } + + @Override + public void mouseClicked( MouseEvent event ) + { + if( SwingUtilities.isRightMouseButton( event ) ) + { + int row = mSystemTree.getRowForLocation( event.getX(), + event.getY() ); + + if( row != -1 ) + { + mSystemTree.setSelectionRow( row ); + + Object selectedNode = + mSystemTree.getLastSelectedPathComponent(); + + if( selectedNode instanceof BaseNode ) + { + JPopupMenu contextMenu = + ((BaseNode)selectedNode).getContextMenu(); + + if( contextMenu != null ) + { + contextMenu.show( mSystemTree, + event.getX(), + event.getY() ); + } + } + } + } + } + + @Override + public void mousePressed( MouseEvent e ){} + @Override + public void mouseReleased( MouseEvent e ){} + @Override + public void mouseEntered( MouseEvent e ){} + @Override + public void mouseExited( MouseEvent e ){} + } +} diff --git a/src/controller/ControllerPanel.java b/src/controller/ControllerPanel.java new file mode 100644 index 00000000..3ddbc439 --- /dev/null +++ b/src/controller/ControllerPanel.java @@ -0,0 +1,134 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package controller; + +import java.awt.Color; +import java.awt.Dimension; + +import javax.swing.JPanel; +import javax.swing.JScrollPane; +import javax.swing.JTable; + +import map.MapPanel; +import message.Message; +import net.miginfocom.swing.MigLayout; +import sample.Listener; + +import com.jidesoft.swing.JideSplitPane; +import com.jidesoft.swing.JideTabbedPane; + +import controller.activity.CallEventPanel; +import controller.activity.MessageActivityPanel; +import controller.state.ChannelStateList; + +public class ControllerPanel extends JPanel +{ + private static final long serialVersionUID = 1L; + + private ChannelStateList mChannelStateList; + + private CallEventPanel mCallEventPanel = new CallEventPanel(); + + private MessageActivityPanel mMessageActivityPanel = + new MessageActivityPanel(); + + private JideTabbedPane mTabbedPane; + + protected ConfigurationTreePanel mSystemControlViewPanel; + protected ConfigurationEditor mConfigurationEditor; + protected JideSplitPane mSystemControlSplitPane; + + protected JTable mChannelActivityTable = new JTable(); + protected ResourceManager mResourceManager; + + public ControllerPanel( ResourceManager resourceManager ) + { + mResourceManager = resourceManager; + init(); + } + + private void init() + { + setLayout( new MigLayout( "insets 0 0 0 0 ", + "[grow,fill]", + "[grow,fill]") ); + + //System Configuration View and Editor + mConfigurationEditor = new ConfigurationEditor(); + mSystemControlViewPanel = + new ConfigurationTreePanel( mResourceManager.getController() ); + mSystemControlViewPanel.addTreeSelectionListener( mConfigurationEditor ); + + mSystemControlSplitPane = new JideSplitPane( JideSplitPane.HORIZONTAL_SPLIT ); + mSystemControlSplitPane.setDividerSize( 5 ); + mSystemControlSplitPane.add( mSystemControlViewPanel ); + mSystemControlSplitPane.add( mConfigurationEditor ); + + //Tabbed View - configuration, calls, messages, map + mTabbedPane = new JideTabbedPane(); + mTabbedPane.setFont( this.getFont() ); + mTabbedPane.setForeground( Color.BLACK ); + mTabbedPane.addTab( "Configuration", mSystemControlSplitPane ); + mTabbedPane.addTab( "Calls", mCallEventPanel ); + mTabbedPane.addTab( "Messages", mMessageActivityPanel ); + + /** + * Add mapping services and map panel to a new tab + */ + + /* Add Map Service as message listener to receive all messages */ + mResourceManager.getChannelManager().addListener( + (Listener)mResourceManager.getMapService() ); + + MapPanel mapPanel = new MapPanel( mResourceManager ); + mTabbedPane.addTab( "Map", mapPanel ); + + + /* Channel state list */ + mChannelStateList = new ChannelStateList( mResourceManager.getSettingsManager() ); + + /* Add message activity panel to receive channel selection events so + * that we can display the correct message listings */ + mResourceManager.getChannelManager().addListener( mMessageActivityPanel ); + + /* Add call event panel to receive channel selection events so + * that we can display the correct call event listings */ + mResourceManager.getChannelManager().addListener( mCallEventPanel ); + + /* Register ChannelStateList as a listener on the channel manager to + * receive channel starts and stops and deletes */ + mResourceManager.getChannelManager().addListener( mChannelStateList ); + + JScrollPane channelStateListScroll = new JScrollPane(); + channelStateListScroll.getViewport().setView( mChannelStateList ); + channelStateListScroll.setPreferredSize( new Dimension( 150, + (int)channelStateListScroll.getPreferredSize().getHeight() ) ); + + JideSplitPane channelSplit = new JideSplitPane( JideSplitPane.HORIZONTAL_SPLIT ); + channelSplit.setDividerSize( 5 ); + channelSplit.add( channelStateListScroll ); + channelSplit.add( mTabbedPane ); + + add( channelSplit ); + } + + public ConfigurationControllerModel getController() + { + return mResourceManager.getController(); + } +} diff --git a/src/controller/ControllerTreeCellRenderer.java b/src/controller/ControllerTreeCellRenderer.java new file mode 100644 index 00000000..46740357 --- /dev/null +++ b/src/controller/ControllerTreeCellRenderer.java @@ -0,0 +1,106 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package controller; + +import java.awt.Component; +import java.awt.Image; +import java.util.HashMap; + +import javax.swing.ImageIcon; +import javax.swing.JTree; +import javax.swing.tree.DefaultTreeCellRenderer; + +public class ControllerTreeCellRenderer extends DefaultTreeCellRenderer +{ + private static final long serialVersionUID = 1L; + + public static final int MAX_ICON_HEIGHT = 16; + private HashMap mIcons = new HashMap(); + + public ControllerTreeCellRenderer() + { + } + + @Override + public Component getTreeCellRendererComponent( JTree tree, + Object value, boolean selected, boolean expanded, boolean isLeaf, + int row, boolean focused ) + { + Component c = super.getTreeCellRendererComponent( tree, value, selected, + expanded, isLeaf, row, focused ); + + if( value instanceof BaseNode ) + { + ImageIcon icon = getIcon( (BaseNode)value ); + + if( icon != null ) + { + if( icon.getIconHeight() > 16 ) + { + + } + else + { + setIcon( icon ); + } + } + } + + return c; + } + + private ImageIcon getIcon( BaseNode node ) + { + ImageIcon icon = null; + + String filepath = node.getIconPath(); + + if( filepath != null ) + { + if( mIcons.containsKey( filepath ) ) + { + icon = mIcons.get( filepath ); + } + else + { + icon = new ImageIcon( filepath ); + + if( icon.getIconHeight() > MAX_ICON_HEIGHT ) + { + Image image = icon.getImage(); + + double scale = (double)icon.getIconHeight() / (double)MAX_ICON_HEIGHT; + + int scaledWidth = (int)( (double)icon.getIconWidth() / scale ); + + Image scaledImage = image.getScaledInstance( scaledWidth, + MAX_ICON_HEIGHT, java.awt.Image.SCALE_SMOOTH ); + + mIcons.put( filepath, new ImageIcon( scaledImage ) ); + } + else + { + mIcons.put( filepath, icon ); + } + + } + } + + return icon; + } +} diff --git a/src/controller/Editor.java b/src/controller/Editor.java new file mode 100644 index 00000000..b938eb87 --- /dev/null +++ b/src/controller/Editor.java @@ -0,0 +1,36 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package controller; + +import javax.swing.JPanel; + +import net.miginfocom.swing.MigLayout; + +public abstract class Editor extends JPanel +{ + private static final long serialVersionUID = 1L; + + public Editor() + { + setLayout( new MigLayout() ); + } + + public abstract void save(); + + public abstract void reset(); +} diff --git a/src/controller/EmptyEditor.java b/src/controller/EmptyEditor.java new file mode 100644 index 00000000..83f21de3 --- /dev/null +++ b/src/controller/EmptyEditor.java @@ -0,0 +1,59 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package controller; + +import java.awt.BorderLayout; +import java.awt.Dimension; + +import javax.swing.JLabel; +import javax.swing.JPanel; + +public class EmptyEditor extends JPanel +{ + private static final long serialVersionUID = 1L; + + public EmptyEditor() + { + setLayout( new BorderLayout() ); + setPreferredSize( new Dimension( 200, (int)getPreferredSize().getHeight() ) ); + + String text = "" + + "

SDR Trunk

" + + "
" + + "

Use the tree structure to the left to configure SDRTrunk" + + " to use multiple SDR tuners and sound card sources to" + + " decode multiple trunked and digital radio channels.

" + + "
" + + "

Each tree branch can be accessed by clicking on the branch" + + " and a configuration editor will appear here.

" + + "
" + + "

Many of the branches contain context sensitive menus that " + + " you can access by right-clicking on the node.

" + + "
" + + "

To begin, right-click on the 'Systems' branch under 'Playlist'" + + "and create a new 'System'. Right-click the 'System' node to create " + + "a new 'Site'. Right-click on the 'Site' node to create a new Channel.

" + + "
" + + "

In the new channel, select a channel source and decoder " + + "and select event logging and recording options. Check 'Enabled'" + + " and hit the 'Save' button to begin decoding.

" + + ""; + + add( new JLabel( text ), BorderLayout.NORTH ); + } +} diff --git a/src/controller/ResourceManager.java b/src/controller/ResourceManager.java new file mode 100644 index 00000000..3075dddb --- /dev/null +++ b/src/controller/ResourceManager.java @@ -0,0 +1,159 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package controller; + +import map.MapService; +import playlist.PlaylistManager; +import record.RecorderManager; +import settings.SettingsManager; +import source.SourceManager; +import source.tuner.TunerManager; +import controller.channel.ChannelManager; +import eventlog.EventLogManager; + +/** + * Manager for system wide resources + */ +public class ResourceManager +{ + private ChannelManager mChannelManager; + private EventLogManager mEventLogManager; + private PlaylistManager mPlaylistManager; + private RecorderManager mRecorderManager; + private SettingsManager mSettingsManager; + private SourceManager mSourceManager; + private TunerManager mTunerManager; + private ConfigurationControllerModel mController; + private MapService mMapService; + private ThreadPoolManager mThreadPoolManager; + + public ResourceManager() + { + this( new SettingsManager(), + new PlaylistManager(), + new EventLogManager(), + new RecorderManager() ); + } + + public ResourceManager( SettingsManager settingsManager, + PlaylistManager playlistManager, + EventLogManager eventLogManager, + RecorderManager recorderManager ) + { + mSettingsManager = settingsManager; + mPlaylistManager = playlistManager; + mEventLogManager = eventLogManager; + mRecorderManager = recorderManager; + + /** + * ChannelManager requires a reference to the ResourceManager so that + * channel processing chains can access system wide resources + */ + mChannelManager = new ChannelManager( this ); + mSourceManager = new SourceManager( this ); + mTunerManager = new TunerManager( this ); + mController = new ConfigurationControllerModel( this ); + mMapService = new MapService( this ); + mThreadPoolManager = new ThreadPoolManager(); + } + + public ConfigurationControllerModel getController() + { + return mController; + } + + public ThreadPoolManager getThreadPoolManager() + { + return mThreadPoolManager; + } + + public void setController( ConfigurationControllerModel controller ) + { + mController = controller; + } + + public TunerManager getTunerManager() + { + return mTunerManager; + } + + public MapService getMapService() + { + return mMapService; + } + + public SettingsManager getSettingsManager() + { + return mSettingsManager; + } + + public void setSettingsManager( SettingsManager manager ) + { + mSettingsManager = manager; + } + + public PlaylistManager getPlaylistManager() + { + return mPlaylistManager; + } + + public void setPlaylistManager( PlaylistManager aliasManager ) + { + mPlaylistManager = aliasManager; + } + + public EventLogManager getEventLogManager() + { + return mEventLogManager; + } + + public void setEventLogManager( EventLogManager eventLogManager ) + { + mEventLogManager = eventLogManager; + } + + public RecorderManager getRecorderManager() + { + return mRecorderManager; + } + + public void setRecorderManager( RecorderManager recorderManager ) + { + mRecorderManager = recorderManager; + } + + public SourceManager getSourceManager() + { + return mSourceManager; + } + + public void setSourceManager( SourceManager sourceManager ) + { + mSourceManager = sourceManager; + } + + public ChannelManager getChannelManager() + { + return mChannelManager; + } + + public void setChannelManager( ChannelManager channelManager ) + { + mChannelManager = channelManager; + } +} diff --git a/src/controller/ThreadPoolManager.java b/src/controller/ThreadPoolManager.java new file mode 100644 index 00000000..158a5b19 --- /dev/null +++ b/src/controller/ThreadPoolManager.java @@ -0,0 +1,97 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package controller; + +import java.util.HashMap; +import java.util.concurrent.Executors; +import java.util.concurrent.RejectedExecutionException; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; + +public class ThreadPoolManager +{ + private int THREAD_POOL_SIZE = 10; + private int MAX_TASK_COUNT = 24; + + private HashMap,ThreadType> mTasks = + new HashMap,ThreadType>(); + + private ScheduledExecutorService mExecutor; + + public ThreadPoolManager() + { + } + + public ScheduledFuture schedule( ThreadType type, + Runnable command, + long period, + TimeUnit unit ) + throws RejectedExecutionException + { + if( mExecutor == null ) + { + mExecutor = Executors.newScheduledThreadPool( THREAD_POOL_SIZE ); + } + + if( mTasks.size() <= MAX_TASK_COUNT ) + { + ScheduledFuture task = + mExecutor.scheduleAtFixedRate( command, 0, period, unit ); + + mTasks.put( task, type ); + + return task; + } + else + { + throw new RejectedExecutionException( "Cannot schedule task - at " + + "max task count [" + mTasks.size() + "]" ); + } + + } + + public void cancel( ScheduledFuture task ) + { + task.cancel( true ); + + ThreadType type = mTasks.remove( task ); + } + + public int getTaskCount( ThreadType type ) + { + int count = 0; + + for( ThreadType current: mTasks.values() ) + { + if( current == type ) + { + count++; + } + } + + return count; + } + + public enum ThreadType + { + SOURCE_SAMPLE_PROCESSING, + DECIMATION, + DECODER; + } +} diff --git a/src/controller/activity/ActivitySummaryFrame.java b/src/controller/activity/ActivitySummaryFrame.java new file mode 100644 index 00000000..c4d7bbaf --- /dev/null +++ b/src/controller/activity/ActivitySummaryFrame.java @@ -0,0 +1,69 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package controller.activity; + +import java.awt.Component; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; + +import javax.swing.JButton; +import javax.swing.JFrame; +import javax.swing.JScrollPane; +import javax.swing.JTextArea; + +import net.miginfocom.swing.MigLayout; + +public class ActivitySummaryFrame extends JFrame +{ + private static final long serialVersionUID = 1L; + + public ActivitySummaryFrame( String summary ) + { + this( summary, null ); + } + + public ActivitySummaryFrame( String summary, Component displayOver ) + { + setTitle( "Activity Summary" ); + setLocationRelativeTo( displayOver ); + setSize( 400, 400 ); + setDefaultCloseOperation( JFrame.DISPOSE_ON_CLOSE ); + setLayout( new MigLayout( "", "[grow,fill]", "[grow,fill][]" ) ); + + JTextArea summaryText = new JTextArea( summary ); + + JScrollPane scroller = new JScrollPane( summaryText ); + scroller.setViewportView( summaryText ); + + add( scroller, "wrap" ); + + JButton close = new JButton( "Close" ); + close.addActionListener( new ActionListener() + { + @Override + public void actionPerformed( ActionEvent e ) + { + dispose(); + } + } ); + + add( close ); + + setVisible( true ); + } +} diff --git a/src/controller/activity/ActivitySummaryProvider.java b/src/controller/activity/ActivitySummaryProvider.java new file mode 100644 index 00000000..91e84253 --- /dev/null +++ b/src/controller/activity/ActivitySummaryProvider.java @@ -0,0 +1,26 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package controller.activity; + +import message.Message; +import sample.Listener; + +public interface ActivitySummaryProvider extends Listener +{ + public String getSummary(); +} diff --git a/src/controller/activity/CallEvent.java b/src/controller/activity/CallEvent.java new file mode 100644 index 00000000..9bbc7bcf --- /dev/null +++ b/src/controller/activity/CallEvent.java @@ -0,0 +1,226 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package controller.activity; + +import java.text.SimpleDateFormat; +import java.util.Date; + +import alias.Alias; +import alias.AliasList; +import decode.DecoderType; + +public abstract class CallEvent +{ + protected long mEventTime = System.currentTimeMillis(); + + private SimpleDateFormat mSDF = new SimpleDateFormat( "yyyyMMdd','HHmmss" ); + + protected DecoderType mDecoderType; + protected CallEventType mCallEventType; + protected AliasList mAliasList; + protected String mFromID; + protected String mToID; + protected String mDetails; + + public CallEvent( DecoderType decoder, + CallEventType callEventType, + AliasList aliasList, + String fromID, + String toID, + String details ) + { + mDecoderType = decoder; + mCallEventType = callEventType; + mAliasList = aliasList; + mFromID = fromID; + mToID = toID; + mDetails = details; + } + + public DecoderType getDecoderType() + { + return mDecoderType; + } + + public CallEventType getCallEventType() + { + return mCallEventType; + } + + public AliasList getAliasList() + { + return mAliasList; + } + + public void setAliasList( AliasList aliasList ) + { + mAliasList = aliasList; + } + + public boolean hasAliasList() + { + return mAliasList != null; + } + + public long getEventTime() + { + return mEventTime; + } + + public String getFromID() + { + return mFromID; + } + + public void setFromID( String id ) + { + mFromID = id; + } + + public String getToID() + { + return mToID; + } + + public void setToID( String id ) + { + mToID = id; + } + + public String getDetails() + { + return mDetails; + } + + public void setDetails( String details ) + { + mDetails = details; + } + + public abstract Alias getFromIDAlias(); + + public abstract Alias getToIDAlias(); + + public abstract int getChannel(); + + public abstract int getFrequency(); + + public static String getCSVHeader() + { + return "DATE,TIME,DECODER,EVENT,FROM,FROM_ALIAS,TO,TO_ALIAS," + + "CHANNEL,FREQUENCY,DETAILS"; + } + + public String toCSV() + { + StringBuilder sb = new StringBuilder(); + + sb.append( "'" ); + sb.append( mSDF.format( new Date( getEventTime() ) ) ); + sb.append( "','" ); + sb.append( getDecoderType().toString() ); + sb.append( "','" ); + sb.append( getCallEventType().toString() ); + sb.append( "'," ); + if( getFromID() != null ) + { + sb.append( "'" ); + sb.append( getFromID() ); + sb.append( "'" ); + } + sb.append( "," ); + + Alias fromAlias = getFromIDAlias(); + + if( fromAlias != null ) + { + sb.append( "'" ); + sb.append( fromAlias.getName() ); + sb.append( "'" ); + } + sb.append( "," ); + + if( getToID() != null ) + { + sb.append( "'" ); + sb.append( getToID() ); + sb.append( "'" ); + } + sb.append( "," ); + + Alias toAlias = getToIDAlias(); + + if( toAlias != null ) + { + sb.append( "'" ); + sb.append( toAlias.getName() ); + sb.append( "'" ); + } + sb.append( ",'" ); + sb.append( getChannel() ); + sb.append( "','" ); + sb.append( getFrequency() ); + sb.append( "'," ); + if( getDetails() != null ) + { + sb.append( "'" ); + sb.append( getDetails() ); + sb.append( "'" ); + } + + return sb.toString(); + } + + public enum CallEventType + { + ACKNOWLEDGE( "Acknowledge" ), + CALL_DETECT( "Call Detect" ), + CALL_START( "Call Start" ), + CALL_START_UNIQUE_ID( "Call Start" ), + CALL_START_NO_TUNER( "Call - No Tuner" ), + CALL_END( "Call End" ), + CALL_TIMEOUT( "Call Timeout" ), + EMERGENCY( "EMERGENCY" ), + GPS( "GPS" ), + ID_ANI( "ANI" ), + ID_UNIQUE( "Unique ID" ), + PAGE( "Page" ), + REGISTER( "Register" ), + REGISTER_ESN( "ESN" ), + SDM( "SDM" ), + STATUS( "Status" ), + UNKNOWN( "Unknown" ); + + private String mLabel; + + private CallEventType( String label ) + { + mLabel = label; + } + + public String getLabel() + { + return mLabel; + } + + public String toString() + { + return mLabel; + } + } +} diff --git a/src/controller/activity/CallEventAliasCellRenderer.java b/src/controller/activity/CallEventAliasCellRenderer.java new file mode 100644 index 00000000..88c35bd4 --- /dev/null +++ b/src/controller/activity/CallEventAliasCellRenderer.java @@ -0,0 +1,72 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package controller.activity; + +import javax.swing.ImageIcon; +import javax.swing.table.DefaultTableCellRenderer; + +import settings.SettingsManager; +import alias.Alias; + +/** + * Custom renderer for JTable cells that contain Alias objects. Renders the + * alias name, and if there is an icon name, attempts to get that icon from + * the settings manager and render it. + */ +public class CallEventAliasCellRenderer extends DefaultTableCellRenderer +{ + private SettingsManager mSettingsManager; + + public CallEventAliasCellRenderer( SettingsManager settingsManager ) + { + super(); + mSettingsManager = settingsManager; + } + + public void setValue( Object obj ) + { + if( obj != null && obj instanceof Alias ) + { + Alias alias = (Alias)obj; + + setText( alias.getName() ); + + ImageIcon icon = getIcon( alias, 12 ); + + if( icon != null ) + { + setIcon( icon ); + } + } + else + { + setText( " " ); + setIcon( null ); + } + } + + private ImageIcon getIcon( Alias alias, int height ) + { + if( mSettingsManager != null && alias != null ) + { + return mSettingsManager.getImageIcon( alias.getIconName(), height ); + } + + return null; + } +} diff --git a/src/controller/activity/CallEventModel.java b/src/controller/activity/CallEventModel.java new file mode 100644 index 00000000..0618e2a4 --- /dev/null +++ b/src/controller/activity/CallEventModel.java @@ -0,0 +1,176 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package controller.activity; + +import java.awt.EventQueue; +import java.text.SimpleDateFormat; +import java.util.LinkedList; + +import javax.swing.table.AbstractTableModel; + +import log.Log; +import sample.Broadcaster; +import sample.Listener; + +public class CallEventModel extends AbstractTableModel +{ + private static final long serialVersionUID = 1L; + + private static final int DEFAULT_ICON_HEIGHT = 14; + + private static final int sTIME = 0; + private static final int sEVENT = 1; + private static final int sFROM_ID = 2; + private static final int sFROM_ALIAS = 3; + private static final int sTO_ID = 4; + private static final int sTO_ALIAS =5; + private static final int sCHANNEL = 6; + private static final int sFREQUENCY = 7; + private static final int sDETAILS = 8; + + protected int mMaxMessages = 500; + + protected LinkedList mEvents = new LinkedList(); + + protected String[] mHeaders = new String[] { "Time", + "Event", + "From", + "From Alias", + "To", + "To Alias", + "Channel", + "Frequency", + "Details" }; + + private SimpleDateFormat mSDFTime = new SimpleDateFormat( "HH:mm:ss" ); + + private Broadcaster mBroadcaster = new Broadcaster(); + private boolean mNewMessagesFirst = true; + + public CallEventModel() + { + } + + public void dispose() + { + mEvents.clear(); + mBroadcaster.dispose(); + } + + public void flush() + { + /* Flush all events to listeners ( normally the call event logger ) */ + for( CallEvent event: mEvents ) + { + mBroadcaster.receive( event ); + } + } + + public int getMaxMessageCount() + { + return mMaxMessages; + } + + public void setMaxMessageCount( int count ) + { + mMaxMessages = count; + } + + public void add( final CallEvent event ) + { + EventQueue.invokeLater( new Runnable() + { + @Override + public void run() + { + mEvents.addFirst( event ); + + CallEventModel.this.fireTableRowsInserted( 0, 0 ); + + prune(); + } + } ); + } + + private void prune() + { + while( mEvents.size() > mMaxMessages ) + { + CallEvent event = mEvents.removeLast(); + + mBroadcaster.receive( event ); + + super.fireTableRowsDeleted( mEvents.size(), mEvents.size() ); + } + } + + @Override + public int getRowCount() + { + return mEvents.size(); + } + + @Override + public int getColumnCount() + { + return mHeaders.length; + } + + public String getColumnName( int column ) + { + return mHeaders[ column ]; + } + + @Override + public Object getValueAt( int rowIndex, int columnIndex ) + { + switch( columnIndex ) + { + case sTIME: + return mSDFTime.format( mEvents.get( rowIndex ).getEventTime() ); + case sEVENT: + return mEvents.get( rowIndex ).getCallEventType(); + case sFROM_ID: + return mEvents.get( rowIndex ).getFromID(); + case sFROM_ALIAS: + return mEvents.get( rowIndex ).getFromIDAlias(); + case sTO_ID: + return mEvents.get( rowIndex ).getToID(); + case sTO_ALIAS: + return mEvents.get( rowIndex ).getToIDAlias(); + case sCHANNEL: + return mEvents.get( rowIndex ).getChannel(); + case sFREQUENCY: + return mEvents.get( rowIndex ).getFrequency(); + case sDETAILS: + return mEvents.get( rowIndex ).getDetails(); + } + + return null; + } + + public void addListener( Listener listener ) + { + mBroadcaster.addListener( listener ); + } + + public void removeListener( Listener listener ) + { + mBroadcaster.removeListener( listener ); + } +} diff --git a/src/controller/activity/CallEventPanel.java b/src/controller/activity/CallEventPanel.java new file mode 100644 index 00000000..62846d98 --- /dev/null +++ b/src/controller/activity/CallEventPanel.java @@ -0,0 +1,84 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package controller.activity; + +import javax.swing.JPanel; +import javax.swing.JScrollPane; +import javax.swing.JTable; + +import net.miginfocom.swing.MigLayout; +import controller.channel.Channel; +import controller.channel.Channel.ChannelEvent; +import controller.channel.ChannelListener; + +public class CallEventPanel extends JPanel implements ChannelListener +{ + private static final long serialVersionUID = 1L; + + private JScrollPane mEmptyScroller; + private Channel mDisplayedChannel; + + public CallEventPanel() + { + setLayout( new MigLayout("insets 0 0 0 0 ", "[grow,fill]", "[grow,fill]") ); + + JTable table = new JTable( new CallEventModel() ); + table.setAutoCreateRowSorter( true ); + table.setAutoResizeMode( JTable.AUTO_RESIZE_LAST_COLUMN ); + mEmptyScroller = new JScrollPane( table ); + + add( mEmptyScroller ); + } + + @Override + public void occurred( Channel channel, ChannelEvent event ) + { + if( event == ChannelEvent.CHANGE_SELECTED && channel.getSelected() ) + { + if( mDisplayedChannel == null || + ( mDisplayedChannel != null && mDisplayedChannel != channel ) ) + { + removeAll(); + + JScrollPane scroller = new JScrollPane( + channel.getProcessingChain().getChannelState() + .getCallEventTable() ); + + add( scroller ); + + mDisplayedChannel = channel; + + revalidate(); + repaint(); + } + } + else if( event == ChannelEvent.PROCESSING_STOPPED || + event == ChannelEvent.CHANNEL_DELETED ) + { + if( mDisplayedChannel != null && mDisplayedChannel == channel ) + { + mDisplayedChannel = null; + removeAll(); + add( mEmptyScroller ); + + revalidate(); + repaint(); + } + } + } +} diff --git a/src/controller/activity/MessageActivityModel.java b/src/controller/activity/MessageActivityModel.java new file mode 100644 index 00000000..cfb5ea54 --- /dev/null +++ b/src/controller/activity/MessageActivityModel.java @@ -0,0 +1,155 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package controller.activity; + +import java.awt.EventQueue; +import java.text.SimpleDateFormat; +import java.util.LinkedList; + +import javax.swing.table.AbstractTableModel; + +import message.Message; +import sample.Listener; + +public class MessageActivityModel extends AbstractTableModel + implements Listener +{ + private static final long serialVersionUID = 1L; + private static final int sTIME = 0; + private static final int sPROTOCOL = 1; + private static final int sERROR_STATUS = 2; + private static final int sMESSAGE = 3; + private static final int sMESSAGE_BITS = 4; + + protected int mMaxMessages = 500; + protected LinkedList mMessages = new LinkedList(); + + protected int[] mColumnWidths = { 110, 110, 110, -1, -1 }; + + protected String[] mHeaders = new String[] { "Time", + "Protocol", + "Error Check", + "Message", + "Binary" }; + + private SimpleDateFormat mSDFTime = new SimpleDateFormat( "HH:mm:ss" ); + + private boolean mNewMessagesFirst = true; + + public MessageActivityModel() + { + } + + public void dispose() + { + mMessages.clear(); + } + + public int[] getColumnWidths() + { + return mColumnWidths; + } + + public void setColumnWidths( int[] widths ) + { + if( widths.length != 5 ) + { + throw new IllegalArgumentException( "MessageActivityModel - " + + "column widths array should have 5 elements" ); + } + else + { + mColumnWidths = widths; + } + } + + public int getMaxMessageCount() + { + return mMaxMessages; + } + + public void setMaxMessageCount( int count ) + { + mMaxMessages = count; + } + + public void receive( final Message message ) + { + EventQueue.invokeLater( new Runnable() + { + @Override + public void run() + { + mMessages.addFirst( message ); + + MessageActivityModel.this.fireTableRowsInserted( 0, 0 ); + + prune(); + } + } ); + } + + private void prune() + { + while( mMessages.size() > mMaxMessages ) + { + mMessages.removeLast(); + + super.fireTableRowsDeleted( mMessages.size() - 1, mMessages.size() - 1 ); + } + } + + @Override + public int getRowCount() + { + return mMessages.size(); + } + + @Override + public int getColumnCount() + { + return mHeaders.length; + } + + public String getColumnName( int column ) + { + return mHeaders[ column ]; + } + + @Override + public Object getValueAt( int rowIndex, int columnIndex ) + { + Message message = mMessages.get( rowIndex ); + + switch( columnIndex ) + { + case sTIME: + return mSDFTime.format( message.getDateReceived() ); + case sPROTOCOL: + return message.getProtocol(); + case sERROR_STATUS: + return message.getErrorStatus(); + case sMESSAGE: + return message.getMessage(); + case sMESSAGE_BITS: + return message.getBinaryMessage(); + } + + return null; + } +} diff --git a/src/controller/activity/MessageActivityPanel.java b/src/controller/activity/MessageActivityPanel.java new file mode 100644 index 00000000..5783a19d --- /dev/null +++ b/src/controller/activity/MessageActivityPanel.java @@ -0,0 +1,82 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package controller.activity; + +import javax.swing.JPanel; +import javax.swing.JScrollPane; +import javax.swing.JTable; + +import net.miginfocom.swing.MigLayout; +import controller.channel.Channel; +import controller.channel.Channel.ChannelEvent; +import controller.channel.ChannelListener; + +public class MessageActivityPanel extends JPanel implements ChannelListener +{ + private static final long serialVersionUID = 1L; + + private JScrollPane mEmptyScrollPane; + private Channel mDisplayedChannel; + + public MessageActivityPanel() + { + setLayout( new MigLayout("insets 0 0 0 0 ", "[grow,fill]", "[grow,fill]") ); + + mEmptyScrollPane = + new JScrollPane( new JTable( new MessageActivityModel() ) ); + + add( mEmptyScrollPane ); + } + + @Override + public void occurred( Channel channel, ChannelEvent event ) + { + if( event == ChannelEvent.CHANGE_SELECTED && channel.getSelected() ) + { + if( mDisplayedChannel == null || + ( mDisplayedChannel != null && mDisplayedChannel != channel ) ) + { + removeAll(); + + JScrollPane scroll = + new JScrollPane( channel.getProcessingChain() + .getChannelState().getMessageActivityTable() ); + + add( scroll ); + + mDisplayedChannel = channel; + + revalidate(); + repaint(); + } + } + else if( event == ChannelEvent.PROCESSING_STOPPED || + event == ChannelEvent.CHANNEL_DELETED ) + { + if( mDisplayedChannel != null && mDisplayedChannel == channel ) + { + mDisplayedChannel = null; + removeAll(); + add( mEmptyScrollPane ); + + revalidate(); + repaint(); + } + } + } +} diff --git a/src/controller/activity/MessageDetailsProvider.java b/src/controller/activity/MessageDetailsProvider.java new file mode 100644 index 00000000..97672505 --- /dev/null +++ b/src/controller/activity/MessageDetailsProvider.java @@ -0,0 +1,82 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package controller.activity; + +import alias.Alias; + +/** + * Getter methods to support the Message Activity Table/Model + * @author denny + * + */ +public interface MessageDetailsProvider +{ + /** + * Indicates if the message is valid and has passed crc/integrity checks + */ + public abstract boolean isValid(); + + /** + * Status of the CRC check of the message + */ + public abstract String getErrorStatus(); + + /** + * Parsed Message + * @return + */ + public abstract String getMessage(); + + /** + * Raw ( 0 & 1 ) message bits + */ + public abstract String getBinaryMessage(); + + + /** + * Decoded protocol + */ + public abstract String getProtocol(); + + /** + * Event - call, data, idle, etc. + */ + public abstract String getEventType(); + + /** + * Formatted from identifier + */ + public abstract String getFromID(); + + /** + * From identifier alias (from AliasManager) + */ + public abstract Alias getFromIDAlias(); + + /** + * Formatted to identifier + */ + public abstract String getToID(); + + /** + * To identifier alias (from AliasManager) + * @return + */ + public abstract Alias getToIDAlias(); + +} diff --git a/src/controller/channel/AbstractChannelEditor.java b/src/controller/channel/AbstractChannelEditor.java new file mode 100644 index 00000000..41c3b4aa --- /dev/null +++ b/src/controller/channel/AbstractChannelEditor.java @@ -0,0 +1,42 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package controller.channel; + +import controller.Editor; + +public abstract class AbstractChannelEditor extends Editor +{ + private static final long serialVersionUID = 1L; + + protected ChannelNode mChannelNode; + + public AbstractChannelEditor( ChannelNode channel ) + { + super(); + mChannelNode = channel; + } + + public ChannelNode getChannelNode() + { + return mChannelNode; + } + + public abstract void save(); + + public abstract void reset(); +} diff --git a/src/controller/channel/Channel.java b/src/controller/channel/Channel.java new file mode 100644 index 00000000..ee964ea9 --- /dev/null +++ b/src/controller/channel/Channel.java @@ -0,0 +1,902 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package controller.channel; + +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; + +import javax.swing.JMenu; +import javax.swing.JMenuItem; +import javax.swing.JSeparator; +import javax.xml.bind.annotation.XmlAttribute; +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlRootElement; +import javax.xml.bind.annotation.XmlSeeAlso; +import javax.xml.bind.annotation.XmlTransient; + +import log.Log; +import message.Message; +import record.config.RecordConfiguration; +import sample.Listener; +import source.SourceType; +import source.config.SourceConfigFactory; +import source.config.SourceConfigTuner; +import source.config.SourceConfiguration; +import source.tuner.TunerChannel; +import source.tuner.TunerChannel.Type; +import controller.ResourceManager; +import controller.activity.ActivitySummaryFrame; +import controller.config.Configuration; +import controller.site.Site; +import controller.state.ChannelState; +import controller.state.ChannelState.State; +import controller.system.System; +import decode.config.AuxDecodeConfiguration; +import decode.config.DecodeConfigFactory; +import decode.config.DecodeConfiguration; +import eventlog.config.EventLogConfiguration; + +@XmlSeeAlso( { Configuration.class } ) +@XmlRootElement( name = "channel" ) +public class Channel extends Configuration +{ + public enum ChannelType + { + STANDARD, + TRAFFIC + }; + + private static final boolean ENABLED = true; + private static final boolean DISABLED = false; + private static final boolean BROADCAST_CHANGE = true; + + private CopyOnWriteArrayList mChannelListeners = + new CopyOnWriteArrayList(); + + private CopyOnWriteArrayList> mMessageListeners = + new CopyOnWriteArrayList>(); + + private HashMap mTrafficChannels = + new HashMap(); + + private DecodeConfiguration mDecodeConfiguration = + DecodeConfigFactory.getDefaultDecodeConfiguration(); + private AuxDecodeConfiguration mAuxDecodeConfiguration = + new AuxDecodeConfiguration(); + private SourceConfiguration mSourceConfiguration = + SourceConfigFactory.getDefaultSourceConfiguration(); + private EventLogConfiguration mEventLogConfiguration = + new EventLogConfiguration(); + private RecordConfiguration mRecordConfiguration = new RecordConfiguration(); + + private ResourceManager mResourceManager; + private String mAliasListName; + private System mSystem; + private Site mSite; + private String mName; + private boolean mEnabled; + private boolean mSelected; + private ChannelType mChannelType; + private ProcessingChain mProcessingChain; + + /** + * Constructs a new standard channel with a default name of "New Channel" + */ + public Channel() + { + this( "New Channel", ChannelType.STANDARD ); + } + + /** + * Constructs a new standard channel with the specified channel name + */ + public Channel( String channelName ) + { + this( channelName, ChannelType.STANDARD ); + } + + /** + * Constructs a new channel with the specified channel name and type. + * + * @param channelName + * @param channelType + */ + public Channel( String channelName, ChannelType channelType ) + { + mName = channelName; + mChannelType = channelType; + } + + public ChannelType getChannelType() + { + return mChannelType; + } + + /** + * Sets the resource manager responsible for this channel and registers + * the channel manager as a listener to this channel. Fires channel add + * channel event to the newly added channel manager channel listener. + */ + public void setResourceManager( ResourceManager resourceManager ) + { + mResourceManager = resourceManager; + + /* Add system-wide channel listeners onto this channel */ + addListeners( mResourceManager.getChannelManager().getChannelListeners() ); + + /* If we're enabled, fire enable changed event to get processing started */ + if( mEnabled ) + { + enableChanged(); + } + } + + @XmlTransient + public ResourceManager getResourceManager() + { + return mResourceManager; + } + + /** + * Returns the processing chain for this channel. If the processing chain + * is not currently constructed and running, this method will return null. + */ + @XmlTransient + public ProcessingChain getProcessingChain() + { + return mProcessingChain; + } + + /** + * Indicates if this channel has a processing chain constructed and it + * is processing. + */ + public boolean isProcessing() + { + return mProcessingChain != null && mProcessingChain.isProcessing(); + } + + /** + * Indicates if this channel has been selected for audio output + */ + @XmlTransient + public boolean getSelected() + { + return mSelected; + } + + public void setSelected( boolean selected ) + { + mSelected = selected; + + if( mProcessingChain != null && mProcessingChain.isProcessing() ) + { + mProcessingChain.getAudioOutput().setAudioPlaybackEnabled( selected ); + } + + fireChannelEvent( ChannelEvent.CHANGE_SELECTED ); + } + + public JMenu getContextMenu() + { + JMenu menu = new JMenu( mSite + "-" + mName ); + + if( mEnabled ) + { + JMenuItem disable = new JMenuItem( "Disable" ); + disable.addActionListener( new ActionListener() + { + @Override + public void actionPerformed( ActionEvent e ) + { + setEnabled( DISABLED, BROADCAST_CHANGE ); + getResourceManager().getPlaylistManager().save(); + } + } ); + + menu.add( disable ); + + menu.add( new JSeparator() ); + + JMenuItem actySummaryItem = + new JMenuItem( "Activity Summary" ); + + actySummaryItem.addActionListener( new ActionListener() + { + @Override + public void actionPerformed( ActionEvent e ) + { + if( mResourceManager != null ) + { + ProcessingChain chain = getProcessingChain(); + + if( chain != null ) + { + ChannelState state = chain.getChannelState(); + + if( state != null ) + { + String summary = state.getActivitySummary(); + + new ActivitySummaryFrame( summary, + mResourceManager.getController().getTree() ); + } + } + } + } + } ); + + menu.add( actySummaryItem ); + } + else + { + JMenuItem enable = new JMenuItem( "Enable" ); + enable.addActionListener( new ActionListener() + { + @Override + public void actionPerformed( ActionEvent e ) + { + setEnabled( ENABLED, BROADCAST_CHANGE ); + getResourceManager().getPlaylistManager().save(); } + } ); + + menu.add( enable ); + } + + menu.add( new JSeparator() ); + + JMenuItem deleteItem = new JMenuItem( "Delete" ); + deleteItem.addActionListener( new ActionListener() + { + @Override + public void actionPerformed( ActionEvent e ) + { + dispose(); + getResourceManager().getPlaylistManager().save(); } + } ); + + menu.add( deleteItem ); + + + return menu; + } + + /** + * Orderly shutdown method when this channel is going to be deleted. + */ + public void dispose() + { + setEnabled( DISABLED, BROADCAST_CHANGE ); + + /* Broadcast channel deleted event */ + fireChannelEvent( ChannelEvent.CHANNEL_DELETED ); + } + + /** + * Returns a full channel name containing the system, site and channel name. + */ + public String getChannelDisplayName() + { + StringBuilder sb = new StringBuilder(); + + if( mSystem != null ) + { + sb.append( mSystem.getName() ); + } + else + { + sb.append( "-" ); + } + + sb.append( "/" ); + + if( mSite != null ) + { + sb.append( mSite.getName() ); + } + else + { + sb.append( "-" ); + } + + sb.append( "/" ); + + sb.append( mName ); + + return sb.toString(); + } + + /** + * Returns the tuner channel requirement for this channel when the source + * configuration specifies a tuner and frequency as the source for this + * channel. + * + * If the source configuration is anything else, this method returns null. + */ + public TunerChannel getTunerChannel() + { + TunerChannel retVal = null; + + if( mSourceConfiguration.getSourceType() == SourceType.TUNER ) + { + SourceConfigTuner config = (SourceConfigTuner)mSourceConfiguration; + + retVal = new TunerChannel( Type.LOCKED, config.getFrequency(), + mDecodeConfiguration.getDecoderType().getChannelBandwidth() ); + } + + return retVal; + } + + /** + * Returns the owning system for this channel. + */ + public System getSystem() + { + return mSystem; + } + + /** + * Sets the owning system for this channel and optionally broadcasts a + * system change channel event. + */ + public void setSystem( System system, boolean fireChangeEvent ) + { + mSystem = system; + + if( fireChangeEvent ) + { + fireChannelEvent( ChannelEvent.CHANGE_SYSTEM ); + } + } + + /** + * Returns the owning site for this channel. + */ + public Site getSite() + { + return mSite; + } + + /** + * Sets the owning site for this channel and optionally broadcasts a site + * change channel event. + */ + public void setSite( Site site, boolean fireChangeEvent ) + { + mSite = site; + + if( fireChangeEvent ) + { + fireChannelEvent( ChannelEvent.CHANGE_SITE ); + } + } + + /** + * Default display string for this channel -- the channel name. + */ + public String toString() + { + return mName; + } + + /** + * Indicates if this channel is enabled for processing. + */ + @XmlAttribute + public boolean getEnabled() + { + return mEnabled; + } + + /** + * Enables/disables this channel for processing. Automatically starts or + * stops channel processing in response to this change. + */ + public void setEnabled( boolean enabled ) + { + mEnabled = enabled; + + enableChanged(); + } + + /** + * Enables/disables this channel for processing. Automatically starts or + * stops channel processing in response to this change. Optionally + * broadcasts a enabled change channel event. + */ + public void setEnabled( boolean enabled, boolean fireChannelEvent ) + { + setEnabled( enabled ); + + if( fireChannelEvent ) + { + fireChannelEvent( ChannelEvent.CHANGE_ENABLED ); + } + } + + /** + * Returns the alias list that is used by this channel for looking up alias + * values for the various identifiers produced by the decoder. + */ + @XmlElement( name = "alias_list_name" ) + public String getAliasListName() + { + return mAliasListName; + } + + /** + * Sets the alias list to be used for looking up the alias values for the + * various identifiers produced by the decoder + */ + public void setAliasListName( String name ) + { + mAliasListName = name; + } + + /** + * Sets the alias list to be used for looking up the alias values for the + * various identifiers produced by the decoder. This method is the same + * as setAliasName(name) and optionally broadcasts an alias list change + * channel event. + */ + public void setAliasListName( String name, boolean fireChangeEvent ) + { + mAliasListName = name; + + if( fireChangeEvent ) + { + fireChannelEvent( ChannelEvent.CHANGE_ALIAS_LIST ); + } + } + + /** + * Gets the decoder configuration used by this channel + */ + @XmlElement( name = "decode_configuration" ) + public DecodeConfiguration getDecodeConfiguration() + { + return mDecodeConfiguration; + } + + /** + * Sets the decoder configuration used by this channel + */ + public void setDecodeConfiguration( DecodeConfiguration config ) + { + mDecodeConfiguration = config; + + if( mProcessingChain != null && mProcessingChain.isRunning() ) + { + mProcessingChain.updateDecoder(); + } + } + + /** + * Gets the aux decoder configuration used by this channel + */ + @XmlElement( name = "aux_decode_configuration" ) + public AuxDecodeConfiguration getAuxDecodeConfiguration() + { + return mAuxDecodeConfiguration; + } + + /** + * Sets the decoder configuration used by this channel + */ + public void setAuxDecodeConfiguration( AuxDecodeConfiguration config ) + { + mAuxDecodeConfiguration = config; + + if( mProcessingChain != null && mProcessingChain.isRunning() ) + { + mProcessingChain.updateDecoder(); + } + } + + /** + * Sets the decoder configuration used by this channel and optionally + * broadcasts a decoder change channel event. + */ + public void setDecodeConfiguration( DecodeConfiguration config, + boolean fireChangeEvent ) + { + setDecodeConfiguration( config ); + + if( fireChangeEvent ) + { + fireChannelEvent( ChannelEvent.CHANGE_DECODER ); + } + } + + /** + * Returns the source configuration for this channel. + */ + @XmlElement( name = "source_configuration" ) + public SourceConfiguration getSourceConfiguration() + { + return mSourceConfiguration; + } + + /** + * Sets the source configuration for this channel. + */ + public void setSourceConfiguration( SourceConfiguration config ) + { + mSourceConfiguration = config; + + if( mProcessingChain != null && mProcessingChain.isRunning() ) + { + mProcessingChain.updateSource(); + } + } + + /** + * Sets the source configuration for this channel and optionally broadcasts + * a source change channel event. + */ + public void setSourceConfiguration( SourceConfiguration config, + boolean fireChangeEvent ) + { + setSourceConfiguration( config ); + + if( fireChangeEvent ) + { + fireChannelEvent( ChannelEvent.CHANGE_SOURCE ); + } + } + + /** + * Returns the event logger configuration for this channel. + */ + @XmlElement( name = "event_log_configuration" ) + public EventLogConfiguration getEventLogConfiguration() + { + return mEventLogConfiguration; + } + + /** + * Sets the event logger configuration for this channel. + */ + public void setEventLogConfiguration( EventLogConfiguration config ) + { + mEventLogConfiguration = config; + + if( mProcessingChain != null && mProcessingChain.isRunning() ) + { + mProcessingChain.updateEventLogging(); + } + } + + /** + * Sets the event logger configuration for this channel and optionally + * broadcasts an event logger change channel event. + */ + public void setEventLogConfiguration( EventLogConfiguration config, + boolean fireChangeEvent ) + { + setEventLogConfiguration( config ); + + if( fireChangeEvent ) + { + fireChannelEvent( ChannelEvent.CHANGE_EVENT_LOGGER ); + } + } + + /** + * Returns the recorder configuration for this channel. + */ + @XmlElement( name = "record_configuration" ) + public RecordConfiguration getRecordConfiguration() + { + return mRecordConfiguration; + } + + /** + * Sets the recorder configuration for this channel. + */ + public void setRecordConfiguration( RecordConfiguration config ) + { + mRecordConfiguration = config; + + if( mProcessingChain != null && mProcessingChain.isRunning() ) + { + mProcessingChain.updateRecording(); + } + } + + /** + * Sets the recorder configuration for this channel and optionally broadcasts + * a recorder configuration change channel event. + */ + public void setRecordConfiguration( RecordConfiguration config, + boolean fireChangeEvent ) + { + setRecordConfiguration( config ); + + if( fireChangeEvent ) + { + fireChannelEvent( ChannelEvent.CHANGE_RECORDER ); + } + } + + /** + * Returns the name of this channel. + */ + @XmlAttribute + public String getName() + { + return mName; + } + + /** + * Sets the name of this channel. + */ + public void setName( String name ) + { + mName = name; + } + + /** + * Sets the name of this channel and optionally broadcasts a name change + * channel event. + */ + public void setName( String name, boolean fireChangeEvent ) + { + mName = name; + + if( fireChangeEvent ) + { + fireChannelEvent( ChannelEvent.CHANGE_NAME ); + } + } + + /** + * Responds to changes in the enabled setting by starting the processing + * change if the channel is now enabled, or stopping the procesing chain + * if it was already started. + */ + private void enableChanged() + { + if( mEnabled ) + { + start(); + } + else + { + stop(); + } + } + + /** + * Starts the channel processing chain. + */ + private void start() + { + if( mEnabled && mProcessingChain == null && mResourceManager != null ) + { + mProcessingChain = new ProcessingChain( this, mResourceManager ); + + /* Add system-wide message listeners */ + mProcessingChain.addListeners( + mResourceManager.getChannelManager().getMessageListeners() ); + + /* Add individual message listeners */ + mProcessingChain.addListeners( mMessageListeners ); + + mProcessingChain.start(); + + fireChannelEvent( ChannelEvent.PROCESSING_STARTED ); + } + + /* If this is a traffic channel, override the call fade timeout before + * we set the state to call, so that it will auto-expire after the + * fade timeout and be auto deleted */ + if( mChannelType == ChannelType.TRAFFIC ) + { + if( mProcessingChain.isProcessing() ) + { + mProcessingChain.getChannelState().setCallFadeTimeout( 40000 ); + mProcessingChain.getChannelState().setState( State.CALL ); + } + else + { + mProcessingChain.getChannelState().setCallFadeTimeout( 5000 ); + /* Channel state should already be no_tuner */ + } + + } + } + + /** + * Stops the channel processing chain + */ + private void stop() + { + /* Stop and remove any traffic channels */ + for( Channel traffic: mTrafficChannels.values() ) + { + traffic.stop(); + traffic.dispose(); + } + + mTrafficChannels.clear(); + + if( !mEnabled && mProcessingChain != null ) + { + mProcessingChain.stop(); + + mProcessingChain.dispose(); + + mProcessingChain = null; + + mSelected = false; + } + + fireChannelEvent( ChannelEvent.PROCESSING_STOPPED ); + } + + /** + * Broadcasts a channel change event to all registered listeners + */ + public void fireChannelEvent( ChannelEvent event ) + { + for( ChannelListener listener: mChannelListeners ) + { + listener.occurred( this, event ); + } + } + + /** + * Adds a channel listener to receive all channel events from this channel + * and automatically sends the listener a channel add event. + */ + public void addListener( ChannelListener listener ) + { + if( !mChannelListeners.contains( listener ) ) + { + mChannelListeners.add( listener ); + + listener.occurred( this, ChannelEvent.CHANNEL_ADDED ); + } + else + { + Log.error( "Channel - attempt to add already existing channel listener [" + listener.getClass() + "]" ); + } + } + + /** + * Adds a list of channel listeners to receive all channel events from this + * channel and automatically sends each one a channel add event. + */ + public void addListeners( List listeners ) + { + for( ChannelListener listener: listeners ) + { + addListener( listener ); + } + } + + /** + * Removes a channel listener from receiving channel events from this channel + */ + public void removeListener( ChannelListener listener ) + { + mChannelListeners.remove( listener ); + } + + public void addListener( Listener listener ) + { + mMessageListeners.add( listener ); + + if( isProcessing() ) + { + mProcessingChain.addListener( listener ); + } + } + + public void removeListener( Listener listener ) + { + mMessageListeners.remove( listener ); + + if( isProcessing() ) + { + mProcessingChain.removeListener( listener ); + } + } + + /** + * Indicates if any part of this channel is contained within the + * minimum and maximum frequency values. + */ + public boolean isWithin( int minimum, int maximum ) + { + return getTunerChannel() != null && + getTunerChannel().isWithin( minimum, maximum ); + } + + public void addTrafficChannel( int channelNumber, Channel channel ) + { + mTrafficChannels.put( channelNumber, channel ); + } + + public boolean hasTrafficChannel( Channel channel ) + { + return mTrafficChannels.containsValue( channel ); + } + + public void removeTrafficChannel( Channel channel ) + { + int keyToRemove = -1; + + if( mTrafficChannels.values().contains( channel ) ) + { + for( Integer channelNumber: mTrafficChannels.keySet() ) + { + if( mTrafficChannels.get( channelNumber ).equals( channel ) ) + { + keyToRemove = channelNumber; + } + } + } + + if( keyToRemove != -1 ) + { + mTrafficChannels.remove( keyToRemove ); + } + } + + public Collection getTrafficChannels() + { + return Collections.unmodifiableCollection( mTrafficChannels.values() ); + } + + public boolean hasTrafficChannel( int channelNumber ) + { + return mTrafficChannels.containsKey( channelNumber ); + } + + /** + * Channel Events - used to specify channel events and changes to channel + * configurations and settings. + */ + public enum ChannelEvent + { + CHANGE_ALIAS_LIST, + CHANGE_DECODER, + CHANGE_ENABLED, + CHANGE_EVENT_LOGGER, + CHANGE_NAME, + CHANGE_SITE, + CHANGE_RECORDER, + CHANGE_SELECTED, + CHANGE_SOURCE, + CHANGE_SYSTEM, + + CHANNEL_ADDED, + CHANNEL_DELETED, + + PROCESSING_STARTED, + PROCESSING_STOPPED, + } +} diff --git a/src/controller/channel/ChannelEditor.java b/src/controller/channel/ChannelEditor.java new file mode 100644 index 00000000..ef64415a --- /dev/null +++ b/src/controller/channel/ChannelEditor.java @@ -0,0 +1,235 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package controller.channel; + + +import java.awt.Color; +import java.awt.ComponentOrientation; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.util.Collections; +import java.util.List; + +import javax.swing.DefaultComboBoxModel; +import javax.swing.JButton; +import javax.swing.JCheckBox; +import javax.swing.JComboBox; +import javax.swing.JLabel; +import javax.swing.JTextField; +import javax.swing.tree.DefaultTreeModel; +import javax.swing.tree.TreePath; + +import net.miginfocom.swing.MigLayout; +import record.RecordComponentEditor; +import source.SourceComponentEditor; +import alias.AliasList; + +import com.jidesoft.swing.JideTabbedPane; + +import controller.Editor; +import decode.AuxDecodeComponentEditor; +import decode.DecodeComponentEditor; +import eventlog.EventLogComponentEditor; + +public class ChannelEditor extends Editor implements ActionListener +{ + private static final long serialVersionUID = 1L; + private DefaultTreeModel mTreeModel; + private ChannelNode mChannelNode; + + private DecodeComponentEditor mDecodeEditor; + private AuxDecodeComponentEditor mAuxDecodeEditor; + private EventLogComponentEditor mEventLogEditor; + private RecordComponentEditor mRecordEditor; + private SourceComponentEditor mSourceEditor; + + private JTextField mChannelName; + private JCheckBox mChannelEnabled = new JCheckBox(); + private JComboBox mComboAliasLists; + + public ChannelEditor( ChannelNode channel ) + { + super(); + + mChannelNode = channel; + + initGUI(); + } + + private void initGUI() + { + setLayout( new MigLayout( "fill,wrap 2", "[right,grow][grow]", "[][][][][grow][]" ) ); + + add( new JLabel( "Channel Configuration" ), "span, align center" ); + + add( new JLabel( "Enabled:" ) ); + + mChannelEnabled.setSelected( + mChannelNode.getChannel().getEnabled() ); + mChannelEnabled.setComponentOrientation( ComponentOrientation.RIGHT_TO_LEFT ); + add( mChannelEnabled ); + + add( new JLabel( "Name:" ) ); + + mChannelName = new JTextField( mChannelNode.getChannel().getName() ); + add( mChannelName, "growx,push" ); + + /** + * ComboBox: Alias Lists + */ + add( new JLabel( "Aliases:" ) ); + + mComboAliasLists = new JComboBox(); + + List lists = mChannelNode.getModel().getResourceManager() + .getPlaylistManager().getPlayist().getAliasDirectory().getAliasList(); + + Collections.sort( lists ); + + mComboAliasLists.setModel( new DefaultComboBoxModel( + lists.toArray( new AliasList[ lists.size() ] ) ) ); + + String aliasListName = + mChannelNode.getChannel().getAliasListName(); + + if( aliasListName != null ) + { + AliasList selected = mChannelNode.getModel().getResourceManager() + .getPlaylistManager().getPlayist().getAliasDirectory() + .getAliasList( aliasListName ); + + mComboAliasLists.setSelectedItem( selected ); + } + + add( mComboAliasLists, "growx,push" ); + + JideTabbedPane tabs = new JideTabbedPane(); + tabs.setFont( this.getFont() ); + tabs.setForeground( Color.BLACK ); + + mSourceEditor = new SourceComponentEditor( mChannelNode ); + tabs.addTab( "Source", mSourceEditor ); + + mDecodeEditor = new DecodeComponentEditor( mChannelNode ); + tabs.addTab( "Decoder", mDecodeEditor ); + + mAuxDecodeEditor = new AuxDecodeComponentEditor( mChannelNode ); + tabs.addTab( "Aux", mAuxDecodeEditor ); + + mEventLogEditor = new EventLogComponentEditor( mChannelNode ); + tabs.addTab( "Event Log", mEventLogEditor ); + + mRecordEditor = new RecordComponentEditor( mChannelNode ); + tabs.addTab( "Record", mRecordEditor ); + + add( tabs, "span,grow,push" ); + + JButton btnSave = new JButton( "Save" ); + btnSave.addActionListener( ChannelEditor.this ); + add( btnSave, "growx,push" ); + + JButton btnReset = new JButton( "Reset" ); + btnReset.addActionListener( ChannelEditor.this ); + add( btnReset, "growx,push" ); + } + + @Override + public void actionPerformed( ActionEvent e ) + { + String command = e.getActionCommand(); + + if( command.contentEquals( "Save" ) ) + { + save(); + } + else if( command.contentEquals( "Reset" ) ) + { + reset(); + } + + if( mTreeModel != null ) + { + mTreeModel.nodeChanged( mChannelNode ); + } + } + + @Override + public void save() + { + boolean expanded = mChannelNode.getModel().getTree() + .isExpanded( new TreePath( mChannelNode ) ); + + mSourceEditor.save(); + mDecodeEditor.save(); + mAuxDecodeEditor.save(); + mEventLogEditor.save(); + mRecordEditor.save(); + + mChannelNode.getChannel().setName( mChannelName.getText(), false ); + mChannelNode.getChannel().setEnabled( mChannelEnabled.isSelected(), false ); + + AliasList selected = mComboAliasLists.getItemAt( + mComboAliasLists.getSelectedIndex() ); + if( selected != null ) + { + mChannelNode.getChannel().setAliasListName( selected.getName(), false ); + } + + + mChannelNode.save(); + mChannelNode.show(); + + if( expanded ) + { + mChannelNode.getModel().getTree().expandPath( new TreePath( mChannelNode ) ); + } + } + + @Override + public void reset() + { + mChannelName.setText( + mChannelNode.getChannel().getName() ); + mChannelName.requestFocus(); + mChannelName.requestFocusInWindow(); + + mChannelEnabled.setSelected( + mChannelNode.getChannel().getEnabled() ); + mChannelEnabled.requestFocus(); + mChannelEnabled.requestFocusInWindow(); + + String aliasListName = + mChannelNode.getChannel().getAliasListName(); + + if( aliasListName != null ) + { + AliasList selected = mChannelNode.getModel().getResourceManager() + .getPlaylistManager().getPlayist().getAliasDirectory() + .getAliasList( aliasListName ); + + mComboAliasLists.setSelectedItem( selected ); + } + + + mSourceEditor.reset(); + mDecodeEditor.reset(); + mAuxDecodeEditor.reset(); + mEventLogEditor.reset(); + mRecordEditor.reset(); + } +} diff --git a/src/controller/channel/ChannelListener.java b/src/controller/channel/ChannelListener.java new file mode 100644 index 00000000..c42324bd --- /dev/null +++ b/src/controller/channel/ChannelListener.java @@ -0,0 +1,32 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package controller.channel; + +import controller.channel.Channel.ChannelEvent; + +/** + * Interface for receiving channel events. Each listener will receive + * notification of all channel events. + */ +public interface ChannelListener +{ + /** + * Notifies the listener that a channel has changed and what has changed + */ + public void occurred( Channel channel, ChannelEvent event ); +} diff --git a/src/controller/channel/ChannelManager.java b/src/controller/channel/ChannelManager.java new file mode 100644 index 00000000..2ca807fc --- /dev/null +++ b/src/controller/channel/ChannelManager.java @@ -0,0 +1,127 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package controller.channel; + +import java.util.ArrayList; + +import message.Message; +import sample.Listener; +import controller.ResourceManager; +import controller.channel.Channel.ChannelEvent; + +public class ChannelManager implements ChannelListener +{ + private ArrayList mChannels = new ArrayList(); + + private ArrayList mChannelListeners = + new ArrayList(); + private ArrayList> mMessageListeners = + new ArrayList>(); + + public ChannelManager( ResourceManager resourceManager ) + { + mChannelListeners.add( this ); + } + + /** + * Notification that a channel has been added or deleted, so that we can + * keep track of all channels and manage system-wide listeners for each + * of those channels + */ + @SuppressWarnings( "incomplete-switch" ) + @Override + public void occurred( Channel channel, ChannelEvent component ) + { + switch( component ) + { + case CHANNEL_ADDED: + if( !mChannels.contains( channel ) ) + { + mChannels.add( channel ); + } + break; + case CHANNEL_DELETED: + mChannels.remove( channel ); + break; + } + } + + /** + * Adds a channel listener that will be added to all channels to receive + * any channel change events. This listener will automatically receive + * a channel add event as it is added to each of the existing channels. + */ + public void addListener( ChannelListener listener ) + { + mChannelListeners.add( listener ); + + for( Channel channel: mChannels ) + { + channel.addListener( listener ); + } + } + + /** + * Removes a channel listener from being automatically added to all channels + * to receive channel change events. + */ + public void removeListener( ChannelListener listener ) + { + mChannelListeners.remove( listener ); + + for( Channel channel: mChannels ) + { + channel.removeListener( listener ); + } + } + + /** + * Returns the list of channel change listeners that will be automatically + * added to all channels to receive channel change events + */ + public ArrayList getChannelListeners() + { + return mChannelListeners; + } + + /** + * Returns the list of channel change listeners that will be automatically + * added to all channels to receive channel change events + */ + public ArrayList> getMessageListeners() + { + return mMessageListeners; + } + + /** + * Adds a message listener that will be added to all channels to receive + * any messages. + */ + public void addListener( Listener listener ) + { + mMessageListeners.add( listener ); + } + + /** + * Removes a message listener. + */ + public void removeListener( Listener listener ) + { + mMessageListeners.remove( listener ); + } +} diff --git a/src/controller/channel/ChannelMap.java b/src/controller/channel/ChannelMap.java new file mode 100644 index 00000000..92bb2ce7 --- /dev/null +++ b/src/controller/channel/ChannelMap.java @@ -0,0 +1,120 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package controller.channel; + +import java.util.ArrayList; + +import javax.xml.bind.annotation.XmlAttribute; +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlSeeAlso; + +@XmlSeeAlso( { ChannelRange.class } ) +public class ChannelMap +{ + private String mName; + + private ArrayList mRanges = new ArrayList(); + + public ChannelMap() + { + this( "New Channel Map" ); + } + + public ChannelMap( String name ) + { + mName = name; + } + + public String toString() + { + return mName; + } + /** + * Creates a copy of this channel map + */ + public ChannelMap copyOf() + { + ChannelMap map = new ChannelMap( mName ); + + for( ChannelRange range: mRanges ) + { + map.addRange( range.copyOf() ); + } + + return map; + } + + public int getInvalidRange() + { + for( int x = 0; x < mRanges.size(); x++ ) + { + if( !mRanges.get( x ).isValid() ) + { + return x; + } + } + + return -1; + } + + public ArrayList getRanges() + { + return mRanges; + } + + @XmlElement( name = "range" ) + public void setRanges( ArrayList ranges ) + { + mRanges = ranges; + } + + @XmlAttribute + public String getName() + { + return mName; + } + + public void setName( String name ) + { + mName = name; + } + + public int getFrequency( int channelNumber ) + { + for( ChannelRange range: mRanges ) + { + if( range.hasChannel( channelNumber ) ) + { + return range.getFrequency( channelNumber ); + } + } + + return 0; + } + + public void addRange( ChannelRange range ) + { + mRanges.add( range ); + } + + public void deleteRange( ChannelRange range ) + { + mRanges.remove( range ); + } + +} diff --git a/src/controller/channel/ChannelMapEditor.java b/src/controller/channel/ChannelMapEditor.java new file mode 100644 index 00000000..4f79d4da --- /dev/null +++ b/src/controller/channel/ChannelMapEditor.java @@ -0,0 +1,223 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package controller.channel; + +import java.awt.Component; +import java.awt.Dimension; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.awt.event.FocusEvent; +import java.awt.event.FocusListener; + +import javax.swing.AbstractCellEditor; +import javax.swing.BorderFactory; +import javax.swing.JButton; +import javax.swing.JFormattedTextField; +import javax.swing.JLabel; +import javax.swing.JOptionPane; +import javax.swing.JPanel; +import javax.swing.JScrollPane; +import javax.swing.JTable; +import javax.swing.JTextField; +import javax.swing.table.TableCellEditor; +import javax.swing.text.InternationalFormatter; + +import log.Log; +import net.miginfocom.swing.MigLayout; + +public class ChannelMapEditor extends JPanel implements ActionListener +{ + private static final long serialVersionUID = 1L; + private ChannelMapNode mChannelMapNode; + + private JLabel mNameLabel; + private JTextField mNameText; + private ChannelMapModel mRangeModel; + private JTable mRangeTable; + private JButton mDelete; + private ChannelMap mChannelMapCopy; + + public ChannelMapEditor( ChannelMapNode ChannelMap ) + { + mChannelMapNode = ChannelMap; + mChannelMapCopy = mChannelMapNode.getChannelMap().copyOf(); + initGUI(); + } + + private void initGUI() + { + setLayout( new MigLayout( "fill,wrap 2", "[right][grow]", "[][][grow][][]" ) ); + + add( new JLabel( "Channel Map" ), "span, align center" ); + + mNameLabel = new JLabel( "Name:" ); + add( mNameLabel ); + + mNameText = new JTextField( mChannelMapCopy.getName() ); + mNameText.addFocusListener( new FocusListener() + { + @Override public void focusGained( FocusEvent arg0 ) {} + + @Override + public void focusLost( FocusEvent arg0 ) + { + Log.info( "text field action event fired, new value:" + mNameText.getText() ); + mChannelMapCopy.setName( mNameText.getText() ); + } + } ); + add( mNameText, "growx, push" ); + + mRangeModel = new ChannelMapModel( mChannelMapCopy ); + + mRangeTable = new JTable( mRangeModel ); + mRangeTable.setPreferredScrollableViewportSize( new Dimension( 300, 150 ) ); + mRangeTable.setFillsViewportHeight( true ); + mRangeTable.setAutoCreateRowSorter( true ); + for( int x = 0; x < mRangeTable.getColumnCount(); x++ ) + { + mRangeTable.getColumnModel().getColumn( x ) + .setCellEditor( new RangeTableEditor() ); + } + + JScrollPane scrollPane = new JScrollPane( mRangeTable ); + add( scrollPane, "span,grow,push"); + + JButton btnNew = new JButton( "New Range" ); + btnNew.addActionListener( ChannelMapEditor.this ); + add( btnNew ); + + mDelete = new JButton( "Delete" ); + mDelete.addActionListener( ChannelMapEditor.this ); + add( mDelete, "wrap" ); + + JButton btnSave = new JButton( "Save" ); + btnSave.addActionListener( ChannelMapEditor.this ); + add( btnSave, "growx" ); + + JButton btnReset = new JButton( "Reset " ); + btnReset.addActionListener( ChannelMapEditor.this ); + add( btnReset, "wrap" ); + } + + private boolean checkForValidRanges() + { + int invalidRow = mChannelMapCopy.getInvalidRange(); + + if( invalidRow != -1 && invalidRow <= mRangeTable.getRowCount() ) + { + mRangeTable.setRowSelectionInterval( invalidRow, invalidRow ); + + JOptionPane.showMessageDialog( this, + "Starting channel number must be less than ending channel number", + "Channel number range error", + JOptionPane.WARNING_MESSAGE ); + + return false; + } + + return true; + } + + @Override + public void actionPerformed( ActionEvent e ) + { + String command = e.getActionCommand(); + + switch( command ) + { + case "New Range": + mRangeModel.addRange( new ChannelRange() ); + break; + case "Save": + if( checkForValidRanges() ) + { + mChannelMapNode.getChannelMap() + .setName( mChannelMapCopy.getName() ); + mChannelMapNode.getChannelMap() + .setRanges( mChannelMapCopy.getRanges() ); + + mChannelMapNode.save(); + + mChannelMapNode.refresh(); + + mChannelMapNode.show(); + } + case "Reset": + mChannelMapCopy = mChannelMapNode.getChannelMap(); + mRangeModel.setChannelMap( mChannelMapCopy ); + mNameText.setText( mChannelMapCopy.getName() ); + break; + case "Delete": + int row = mRangeTable.getSelectedRow(); + + if( row < 0 ) + { + JOptionPane.showMessageDialog( this, + "Please select a channel range to delete", + "Select a range", + JOptionPane.WARNING_MESSAGE ); + } + else + { + mRangeModel.removeRange( + mRangeTable.convertRowIndexToModel( row ) ); + } + break; + } + } + + public class RangeTableEditor extends AbstractCellEditor + implements TableCellEditor, ActionListener + { + JFormattedTextField mEditor; + + public RangeTableEditor() + { + InternationalFormatter nf = new InternationalFormatter(); + nf.setMinimum( 1 ); + nf.setAllowsInvalid( false ); + + mEditor = new JFormattedTextField( nf ); + } + + @Override + public Object getCellEditorValue() + { + Log.info( "getCellEditorValue - I've been invoked!" ); + return mEditor.getText(); + } + + @Override + public void actionPerformed( ActionEvent e ) + { + Log.info( "Action Performed:" + e.getActionCommand() ); + } + + @Override + public Component getTableCellEditorComponent( JTable table, + Object value, + boolean isSelected, + int row, + int column ) + { + mEditor.setValue( value ); + + return mEditor; + } + } +} diff --git a/src/controller/channel/ChannelMapList.java b/src/controller/channel/ChannelMapList.java new file mode 100644 index 00000000..38ba11ee --- /dev/null +++ b/src/controller/channel/ChannelMapList.java @@ -0,0 +1,76 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package controller.channel; + +import java.util.ArrayList; + +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlSeeAlso; + +@XmlSeeAlso( ChannelMap.class ) +public class ChannelMapList +{ + private ArrayList mChannelMap = new ArrayList(); + + public ChannelMapList() + { + /* Empty constructor for JAXB */ + } + + @XmlElement( name = "channel_map" ) + public ArrayList getChannelMap() + { + return mChannelMap; + } + + public void setChannelMap( ArrayList ChannelMaps ) + { + mChannelMap = ChannelMaps; + } + + public void addChannelMap( ChannelMap ChannelMap ) + { + mChannelMap.add( ChannelMap ); + } + + public void removeChannelMap( ChannelMap ChannelMap ) + { + mChannelMap.remove( ChannelMap ); + } + + public void clearChannelMaps() + { + mChannelMap.clear(); + } + + /** + * Gets the named channel map, or returns an empty map + */ + public ChannelMap getChannelMap( String name ) + { + for( ChannelMap map: mChannelMap ) + { + if( map.getName().equalsIgnoreCase( name ) ) + { + return map; + } + } + + return new ChannelMap(); + } +} diff --git a/src/controller/channel/ChannelMapListNode.java b/src/controller/channel/ChannelMapListNode.java new file mode 100644 index 00000000..158b21d4 --- /dev/null +++ b/src/controller/channel/ChannelMapListNode.java @@ -0,0 +1,87 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package controller.channel; + +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; + +import javax.swing.JMenuItem; +import javax.swing.JPopupMenu; + +import controller.ConfigurableNode; + +public class ChannelMapListNode extends ConfigurableNode +{ + private static final long serialVersionUID = 1L; + + public ChannelMapListNode( ChannelMapList list ) + { + super( list ); + } + + public ChannelMapList getChannelMapList() + { + return (ChannelMapList)getUserObject(); + } + + public void init() + { + for( ChannelMap channelMap: getChannelMapList().getChannelMap() ) + { + ChannelMapNode node = new ChannelMapNode( channelMap ); + + getModel().addNode( node, ChannelMapListNode.this, getChildCount() ); + + node.init(); + } + + sort(); + } + + public String toString() + { + return "Channel Maps [" + this.getChildCount() + "]"; + } + + public JPopupMenu getContextMenu() + { + JPopupMenu retVal = new JPopupMenu(); + + JMenuItem addSystemItem = new JMenuItem( "Add Channel Map" ); + addSystemItem.addActionListener( new ActionListener() + { + @Override + public void actionPerformed( ActionEvent e ) + { + ChannelMap channelMap = new ChannelMap(); + + getChannelMapList().addChannelMap( channelMap ); + + getModel().addNode( new ChannelMapNode( channelMap ), + ChannelMapListNode.this, + ChannelMapListNode.this.getChildCount() ); + + save(); + } + } ); + + retVal.add( addSystemItem ); + + return retVal; + } +} diff --git a/src/controller/channel/ChannelMapModel.java b/src/controller/channel/ChannelMapModel.java new file mode 100644 index 00000000..bcb3fadb --- /dev/null +++ b/src/controller/channel/ChannelMapModel.java @@ -0,0 +1,167 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package controller.channel; + +import javax.swing.table.AbstractTableModel; + +import log.Log; + +public class ChannelMapModel extends AbstractTableModel +{ + private static final long serialVersionUID = 1L; + private static final int sSTART = 0; + private static final int sSTOP = 1; + private static final int sBASE = 2; + private static final int sSIZE = 3; + + protected int[] mColumnWidths = { 110, 110, 110, -1 }; + + protected String[] mHeaders = new String[] { "Begin", + "End", + "Base", + "Size" }; + + private ChannelMap mChannelMap; + + public ChannelMapModel( ChannelMap map ) + { + mChannelMap = map; + } + + public ChannelMap getChannelMap() + { + return mChannelMap; + } + + public void setChannelMap( ChannelMap map ) + { + mChannelMap = map; + + fireTableDataChanged(); + } + + public void addRange( ChannelRange range ) + { + mChannelMap.addRange( range ); + + int index = mChannelMap.getRanges().size() - 1; + + fireTableRowsInserted( index, index ); + } + + public void removeRange( int rangeIndex ) + { + if( rangeIndex < mChannelMap.getRanges().size() ) + { + mChannelMap.getRanges().remove( rangeIndex ); + } + + fireTableRowsDeleted( rangeIndex, rangeIndex ); + } + + public int[] getColumnWidths() + { + return mColumnWidths; + } + + public void setColumnWidths( int[] widths ) + { + if( widths.length != 4 ) + { + throw new IllegalArgumentException( "ChannelMapRangeModel - " + + "column widths array should have 5 elements" ); + } + else + { + mColumnWidths = widths; + } + } + + @Override + public int getRowCount() + { + return mChannelMap.getRanges().size(); + } + + @Override + public int getColumnCount() + { + return mHeaders.length; + } + + public String getColumnName( int column ) + { + return mHeaders[ column ]; + } + + @Override + public Object getValueAt( int rowIndex, int columnIndex ) + { + ChannelRange range = mChannelMap.getRanges().get( rowIndex ); + + switch( columnIndex ) + { + case sSTART: + return range.getFirstChannelNumber(); + case sSTOP: + return range.getLastChannelNumber(); + case sBASE: + return range.getBase(); + case sSIZE: + return range.getSize(); + } + + return null; + } + + public boolean isCellEditable( int row, int column ) + { + return true; + } + + public void setValueAt( Object value, int row, int column ) + { + try + { + int parsedValue = Integer.parseInt( (String)value ); + + ChannelRange range = mChannelMap.getRanges().get( row ); + + switch( column ) + { + case sSTART: + range.setFirstChannelNumber( parsedValue ); + break; + case sSTOP: + range.setLastChannelNumber( parsedValue ); + break; + case sBASE: + range.setBase( parsedValue ); + break; + case sSIZE: + range.setSize( parsedValue ); + break; + } + } + catch( Exception e ) + { + Log.info( "ChannelMapModel - couldn't parse integer value " + + "for [" + value + "] - " + e.getLocalizedMessage() ); + } + } +} diff --git a/src/controller/channel/ChannelMapNode.java b/src/controller/channel/ChannelMapNode.java new file mode 100644 index 00000000..d9b68419 --- /dev/null +++ b/src/controller/channel/ChannelMapNode.java @@ -0,0 +1,102 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package controller.channel; + + +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; + +import javax.swing.JMenuItem; +import javax.swing.JPanel; +import javax.swing.JPopupMenu; + +import controller.ConfigurableNode; + +public class ChannelMapNode extends ConfigurableNode +{ + private static final long serialVersionUID = 1L; + + public ChannelMapNode( ChannelMap ChannelMap ) + { + super( ChannelMap ); + } + + public void init() + { + //For consistency, but not used + } + + @Override + public String getIconPath() + { + return "images/ChannelMap.png"; + } + + @Override + public JPanel getEditor() + { + return new ChannelMapEditor( this ); + } + + public ChannelMap getChannelMap() + { + return (ChannelMap)getUserObject(); + } + + public String toString() + { + return getChannelMap().getName(); + } + + public void delete() + { + ChannelMapListNode parent = (ChannelMapListNode)getParent(); + + parent.getChannelMapList().removeChannelMap( getChannelMap() ); + + save(); + + getModel().removeNodeFromParent( ChannelMapNode.this ); + + /* Show the first child, if one exists so that we don't have a + * collapsed list */ + if( parent.getChildCount() > 0 ) + { + ((ConfigurableNode)parent.getFirstChild()).show(); + } + } + + public JPopupMenu getContextMenu() + { + JPopupMenu retVal = new JPopupMenu(); + + JMenuItem deleteItem = new JMenuItem( "Delete" ); + deleteItem.addActionListener( new ActionListener() + { + @Override + public void actionPerformed( ActionEvent e ) + { + delete(); + } + } ); + + retVal.add( deleteItem ); + + return retVal; + } +} diff --git a/src/controller/channel/ChannelNode.java b/src/controller/channel/ChannelNode.java new file mode 100644 index 00000000..0dc6cce9 --- /dev/null +++ b/src/controller/channel/ChannelNode.java @@ -0,0 +1,168 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package controller.channel; + +import java.text.DecimalFormat; + +import javax.swing.JPanel; +import javax.swing.JPopupMenu; + +import log.Log; +import source.tuner.TunerChannel; +import controller.ConfigurableNode; +import controller.channel.Channel.ChannelEvent; +import controller.site.SiteNode; +import controller.system.SystemNode; + +/** + * A channel has all of the pieces needed to wire together a source, decoder, + * event logger and recorder and be started and stopped. + */ +public class ChannelNode extends ConfigurableNode implements ChannelListener +{ + private static final long serialVersionUID = 1L; + + private static DecimalFormat sFORMAT = new DecimalFormat( "0.0000" ); + + public ChannelNode( Channel channel ) + { + super( channel ); + } + + public void init() + { + /* Set the owning system and site names for the channel */ + SiteNode siteNode = (SiteNode)getParent(); + + if( siteNode != null ) + { + getChannel().setSite( siteNode.getSite(), false ); + + SystemNode systemNode = (SystemNode)siteNode.getParent(); + + if( systemNode != null ) + { + getChannel().setSystem( systemNode.getSystem(), false ); + } + } + + /* Add this node as listener to receive changes from underlying channel */ + getChannel().addListener( this ); + + /* Add the resource manager to the channel so that the channel + * can provide channel change events to all system resources */ + getChannel().setResourceManager( getModel().getResourceManager() ); + } + + /** + * Listener method to receive channel changes from the underlying chnannel + */ + @Override + public void occurred( Channel channel, ChannelEvent event ) + { + switch( event ) + { + /* Refresh the node for each of these events */ + case CHANGE_DECODER: + case CHANGE_ENABLED: + case CHANGE_NAME: + case CHANGE_SELECTED: + case CHANGE_SITE: + case CHANGE_SYSTEM: + case PROCESSING_STARTED: + case PROCESSING_STOPPED: + this.refresh(); + break; + /* We're being deleted, so cleanup */ + case CHANNEL_DELETED: + delete(); + break; + default: + break; + } + } + + @Override + public String getIconPath() + { + StringBuilder sb = new StringBuilder(); + + Channel channel = getChannel(); + + sb.append( channel.getDecodeConfiguration().getDecoderType().getIconPrefix() ); + + if( channel.getEnabled() ) + { + sb.append( channel.isProcessing() ? "_green" : "_red" ); + } + + sb.append( ".png" ); + + return sb.toString(); + } + + @Override + public JPanel getEditor() + { + return new ChannelEditor( this ); + } + + public Channel getChannel() + { + return (Channel)this.getUserObject(); + } + + public String toString() + { + StringBuilder sb = new StringBuilder(); + + sb.append( getChannel().getName() ); + + TunerChannel channel = getChannel().getTunerChannel(); + + if( channel != null ) + { + sb.append( " (" ); + sb.append( sFORMAT.format( (double)channel.getFrequency() / 1000000.0d ) + " MHz" ); + sb.append( ")" ); + + } + + return sb.toString(); + } + + public JPopupMenu getContextMenu() + { + JPopupMenu popupMenu = new JPopupMenu( "Channel Menu" ); + popupMenu.add( getChannel().getContextMenu() ); + + return popupMenu; + } + + public void delete() + { + Log.info( "Channel Node - deleting ..." ); + ((SiteNode)getParent()).getSite().removeChannel( getChannel() ); + + Log.info( "Channel Node - saving after channel delete" ); + save(); + + Log.info( "Channel Node - removing from site node" ); + getModel().removeNodeFromParent( ChannelNode.this ); + } +} diff --git a/src/controller/channel/ChannelRange.java b/src/controller/channel/ChannelRange.java new file mode 100644 index 00000000..e641f8a0 --- /dev/null +++ b/src/controller/channel/ChannelRange.java @@ -0,0 +1,122 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package controller.channel; + +import javax.xml.bind.annotation.XmlAttribute; +import javax.xml.bind.annotation.XmlRootElement; +import javax.xml.bind.annotation.XmlType; + +@XmlRootElement +@XmlType( name = "range" ) +public class ChannelRange +{ + private int mFirst = 1; + private int mLast = 1023; + private int mBase = 450000000; + private int mSize = 12500; + + /* JAXB requirement - do not remove */ + public ChannelRange() {} + + public ChannelRange( int first, int last, int base, int size ) + { + mFirst = first; + mLast = last; + mBase = base; + mSize = size; + } + + public ChannelRange copyOf() + { + return new ChannelRange( mFirst, mLast, mBase, mSize ); + } + + public boolean isValid() + { + return mFirst < mLast; + } + + public boolean hasChannel( int channel ) + { + return mFirst <= channel && channel <= mLast; + } + + public int getFrequency( int channel ) + { + if( hasChannel( channel ) ) + { + return mBase + ( ( channel - mFirst ) * mSize ); + } + else + { + return 0; + } + } + + @XmlAttribute( name = "first" ) + public int getFirstChannelNumber() + { + return mFirst; + } + + public void setFirstChannelNumber( int first ) + { + mFirst = first; + } + + @XmlAttribute( name = "last" ) + public void setLastChannelNumber( int last ) + { + mLast = last; + } + + public int getLastChannelNumber() + { + return mLast; + } + + /** + * Sets the base frequency + * @param base frequency in hertz + */ + @XmlAttribute( name = "base" ) + public void setBase( int base ) + { + mBase = base; + } + + public int getBase() + { + return mBase; + } + + /** + * Sets the channel size + * @param size in hertz + */ + @XmlAttribute( name = "size" ) + public void setSize( int size ) + { + mSize = size; + } + + public int getSize() + { + return mSize; + } +} diff --git a/src/controller/channel/ChannelSelectionListener.java b/src/controller/channel/ChannelSelectionListener.java new file mode 100644 index 00000000..9719ea6f --- /dev/null +++ b/src/controller/channel/ChannelSelectionListener.java @@ -0,0 +1,23 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package controller.channel; + +public interface ChannelSelectionListener +{ + public void channelSelected( ProcessingChain channel ); +} diff --git a/src/controller/channel/ProcessingChain.java b/src/controller/channel/ProcessingChain.java new file mode 100644 index 00000000..5c2f019c --- /dev/null +++ b/src/controller/channel/ProcessingChain.java @@ -0,0 +1,721 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package controller.channel; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.LinkedTransferQueue; +import java.util.concurrent.RejectedExecutionException; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; + +import log.Log; +import message.Message; +import record.Recorder; +import record.wave.ComplexWaveRecorder; +import record.wave.FloatWaveRecorder; +import sample.Broadcaster; +import sample.Listener; +import sample.Provider; +import sample.complex.ComplexSample; +import source.ComplexSource; +import source.FloatArraySource; +import source.FloatSource; +import source.Source; +import source.Source.SampleType; +import source.config.SourceConfigMixer; +import source.tuner.Tuner; +import source.tuner.TunerChannelSource; +import alias.AliasList; +import audio.AudioOutput; +import audio.AudioTypeListener; +import audio.SquelchListener; +import controller.ResourceManager; +import controller.ThreadPoolManager.ThreadType; +import controller.activity.CallEvent; +import controller.channel.Channel.ChannelType; +import controller.state.ChannelState; +import controller.state.ChannelState.State; +import decode.Decoder; +import decode.DecoderFactory; +import eventlog.EventLogType; +import eventlog.EventLogger; + +/** + * A channel has all of the pieces needed to wire together a source, decoder, + * event logger and recorder and be started and stopped. + */ +public class ProcessingChain implements Listener +{ + protected AtomicBoolean mRunning = new AtomicBoolean(); + protected Channel mChannel; + protected Source mSource; + protected Decoder mDecoder; + protected List mRecorders = new ArrayList(); + protected ArrayList mEventLoggers = new ArrayList(); + protected AudioOutput mAudioOutput; + protected ChannelState mChannelState; + protected ResourceManager mResourceManager; + private ScheduledFuture mProcessorTask; + private AliasList mAliasList; + + private ComplexListener mComplexListener = new ComplexListener(); + private LinkedTransferQueue> mComplexBuffer = + new LinkedTransferQueue>(); + + private FloatListener mFloatListener = new FloatListener(); + private FloatArrayListener mFloatArrayListener = new FloatArrayListener(); + private LinkedTransferQueue mFloatBuffer = + new LinkedTransferQueue(); + + protected Broadcaster mBroadcaster = new Broadcaster(); + + public ProcessingChain( Channel channel, ResourceManager resourceManager ) + { + mChannel = channel; + mResourceManager = resourceManager; + + if( mResourceManager != null ) + { + mAliasList = mResourceManager.getPlaylistManager() + .getPlayist().getAliasDirectory().getAliasList( + mChannel.getAliasListName() ); + } + } + + public Listener> getComplexListener() + { + return mComplexListener; + } + + public Listener getFloatListener() + { + return mFloatListener; + } + + public Listener getFloatArrayListener() + { + return mFloatArrayListener; + } + + public String toString() + { + return mChannel.getName(); + } + + public AliasList getAliasList() + { + return mAliasList; + } + + public void dispose() + { + if( mProcessorTask != null ) + { + mResourceManager.getThreadPoolManager().cancel( mProcessorTask ); + } + + if( mChannelState != null ) + { + mChannelState.dispose(); + } + + mChannelState = null; + + if( mDecoder != null ) + { + mDecoder.dispose(); + } + + mBroadcaster.dispose(); + mEventLoggers.clear(); + mRecorders.clear(); + + + if( mAudioOutput != null ) + { + mAudioOutput.dispose(); + } + + mResourceManager = null; + } + + public ResourceManager getResourceManager() + { + return mResourceManager; + } + + public void start() + { + if( !mRunning.get() && mChannel.getEnabled() ) + { + updateSource(); + + updateDecoder(); + + updateEventLogging(); + + updateRecording(); + + mRunning.set( true ); + } + else + { + Log.info( getLogPrefix() + "start() invoked, but processing chain is " + + "already started" ); + } + } + + /** + * Performs closeout operations to prepare for channel deletion + */ + public void stop() + { + if( mRunning.get() ) + { + updateSource(); + + updateDecoder(); + + updateRecording(); + + /* Flush call events to the logger */ + if( getChannel().getChannelType() == ChannelType.STANDARD ) + { + getChannelState().getCallEventModel().flush(); + } + + updateEventLogging(); + + if( mAudioOutput != null ) + { + mAudioOutput.setAudioPlaybackEnabled( false ); + } + + mRunning.set( false ); + } + else + { + Log.info( getLogPrefix() + "stop() invoked, but processing chain was " + + "already stopped" ); + } + } + + private String getLogPrefix() + { + return "Channel [" + mChannel.getName() + "] - "; + } + + public void updateSource() + { + /* Cleanup existing source */ + if( mSource != null ) + { + if( mSource instanceof ComplexSource ) + { + ((ComplexSource)mSource).removeListener( this.getComplexListener() ); + } + else if( mSource instanceof FloatSource ) + { + ((FloatSource)mSource).removeListener( this.getFloatListener() ); + } + else if( mSource instanceof FloatArraySource ) + { + ((FloatArraySource)mSource) + .removeListener( this.getFloatArrayListener() ); + } + + if( mProcessorTask != null ) + { + mResourceManager.getThreadPoolManager().cancel( mProcessorTask ); + } + + mSource.dispose(); + + mSource = null; + } + + /* Get a new source if needed and start the sample processor thread */ + if( mChannel.getEnabled() && mResourceManager != null ) + { + mSource = mResourceManager.getSourceManager() + .getSource( ProcessingChain.this ); + + if( mSource != null ) + { + if( mSource instanceof ComplexSource ) + { + try + { + mProcessorTask = + mResourceManager.getThreadPoolManager() + .schedule( ThreadType.DECODER, + new ComplexProcessor(), + 50, TimeUnit.MILLISECONDS ); + + ((ComplexSource)mSource) + .setListener( this.getComplexListener() ); + } + catch( RejectedExecutionException ree ) + { + Log.error( getLogPrefix() + "ProcessingChain - " + + "error scheduling complex sample processing " + + "thread - " + ree.getLocalizedMessage() ); + + mSource.dispose(); + } + } + else if( mSource instanceof FloatSource ) + { + try + { + mProcessorTask = + mResourceManager.getThreadPoolManager() + .schedule( ThreadType.DECODER, + new FloatProcessor(), + 50, TimeUnit.MILLISECONDS ); + + ((FloatSource)mSource) + .setListener( this.getFloatListener() ); + } + catch( RejectedExecutionException ree ) + { + Log.error( getLogPrefix() + "ProcessingChain - " + + "error scheduling float sample processing " + + "thread - " + ree.getLocalizedMessage() ); + + mSource.dispose(); + } + } + else if( mSource instanceof FloatArraySource ) + { + try + { + mProcessorTask = + mResourceManager.getThreadPoolManager() + .schedule( ThreadType.DECODER, + new FloatProcessor(), + 50, TimeUnit.MILLISECONDS ); + + ((FloatArraySource)mSource) + .setListener( this.getFloatArrayListener() ); + } + catch( RejectedExecutionException ree ) + { + Log.error( getLogPrefix() + "ProcessingChain - " + + "error scheduling float sample processing " + + "thread - " + ree.getLocalizedMessage() ); + + mSource.dispose(); + } + } + } + } + } + + public void updateDecoder() + { + if( mChannel.getEnabled() ) + { + AliasList aliasList = null; + + if( mResourceManager != null ) + { + aliasList = mResourceManager.getPlaylistManager() + .getPlayist().getAliasDirectory().getAliasList( + mChannel.getAliasListName() ); + } + + /* Setup the channel state */ + mChannelState = DecoderFactory.getChannelStateNew( this, aliasList ); + + if( mSource == null ) + { + mChannelState.setState( State.NO_TUNER ); + } + else + { + mDecoder = DecoderFactory. + getDecoder( this, mSource.getSampleType(), aliasList ); + + if( mDecoder != null ) + { + /* Register to receive decoded messages and auxiliary messages */ + mDecoder.addMessageListener( ProcessingChain.this ); + + /* Setup the audio output processing thread */ + String threadName = "SDRTrunk Audio - " + + mChannel.getSite().getName() + + "-" + mChannel.getName(); + + mAudioOutput = new AudioOutput( threadName ); + addFloatListener( mAudioOutput ); + } + } + + if( mAudioOutput != null ) + { + /* Register audio output to be controlled by the channel state */ + mChannelState.addListener( (SquelchListener)mAudioOutput ); + mChannelState.setListener( (AudioTypeListener)mAudioOutput ); + } + } + } + + /** + * Starts, stops or changes event logging + */ + public void updateEventLogging() + { + /* Shutdown and remove existing event loggers */ + if( mEventLoggers != null ) + { + for( EventLogger logger: mEventLoggers ) + { + logger.stop(); + } + + mEventLoggers.clear(); + } + + /* Construct event loggers and add as message listeners */ + if( mChannel.getEnabled() && + mResourceManager != null ) + { + //Get the event loggers + ArrayList types = getChannel() + .getEventLogConfiguration().getLoggers(); + + for( EventLogType type: types ) + { + EventLogger logger; + + if( type == EventLogType.CALL_EVENT ) + { + logger = mResourceManager.getEventLogManager() + .getLogger( this, type ); + + getChannelState().getCallEventModel() + .addListener( (Listener)logger ); + } + else + { + logger = mResourceManager.getEventLogManager() + .getLogger( this, type ); + + addListener( (Listener)logger ); + } + + logger.start(); + + mEventLoggers.add( logger ); + } + } + } + + /** + * Starts, stops or changes data recording + */ + public void updateRecording() + { + /* Remove any existing recorders */ + if( mRecorders != null ) + { + for( Recorder recorder: mRecorders ) + { + if( recorder != null ) + { + try + { + recorder.stop(); + } + catch ( IOException ioe ) + { + Log.error( getLogPrefix() + "Couldn't stop recorder [" + + recorder.getFileName() + "] " + + ioe.getLocalizedMessage() ); + } + } + } + } + + /* Start any new recorders */ + if( mChannel.getEnabled() && + mResourceManager != null && + mDecoder != null ) + { + mRecorders = mResourceManager.getRecorderManager() + .getRecorder( this ); + + /* Iterate any returned recorders and start/add them */ + for( Recorder recorder: mRecorders ) + { + try + { + recorder.start(); + + if( recorder instanceof FloatWaveRecorder ) + { + Log.info( getLogPrefix() + "- started audio recording [" + + recorder.getFileName() + "]" ); + + mDecoder.addFloatListener( (FloatWaveRecorder)recorder ); + } + else if( recorder instanceof ComplexWaveRecorder ) + { + Log.info( getLogPrefix() + "- started baseband recording [" + + recorder.getFileName() + "]" ); + + mDecoder.addComplexListener( (ComplexWaveRecorder )recorder ); + } + + } + catch ( IOException e ) + { + Log.error( getLogPrefix() + "Unable to start recorder [" + + recorder.getFileName() + "]" ); + } + } + } + } + + public Channel getChannel() + { + return mChannel; + } + + public AudioOutput getAudioOutput() + { + return mAudioOutput; + } + + public Source getSource() + { + return mSource; + } + + /** + * Returns the tuner that is sourcing this channel, or null if it's being + * sourced from some other source + */ + public Tuner getTuner() + { + Tuner retVal = null; + + if( mSource instanceof TunerChannelSource ) + { + retVal = ((TunerChannelSource)mSource).getTuner(); + } + + return retVal; + } + + public Decoder getDecoder() + { + return mDecoder; + } + + public ChannelState getChannelState() + { + return mChannelState; + } + + public boolean isRunning() + { + return mRunning.get(); + } + + public boolean isProcessing() + { + return isRunning() && mSource != null; + } + + private source.mixer.MixerChannel getMixerChannel() + { + source.mixer.MixerChannel retVal = null; + + if( mChannel.getSourceConfiguration() instanceof SourceConfigMixer ) + { + SourceConfigMixer mixer = (SourceConfigMixer)mChannel.getSourceConfiguration(); + + retVal = mixer.getChannel(); + } + + return retVal; + } + + @Override + public void receive( Message message ) + { + if( message.isValid() ) + { + mBroadcaster.receive( message ); + } + + } + + public void addFloatListener( Listener listener ) + { + if( mDecoder != null ) + { + mDecoder.addFloatListener( listener ); + } + } + + public void removeFloatListener( Listener listener ) + { + if( mDecoder != null ) + { + mDecoder.removeFloatListener( listener ); + } + } + + public void addComplexListener( Listener listener ) + { + if( mDecoder != null ) + { + mDecoder.addComplexListener( listener ); + } + else + { + Log.error( getLogPrefix() + "attempt to add a complex sample " + + "listener, but the decoder is null" ); + } + } + + public void removeComplexListener( Listener listener ) + { + if( mDecoder != null ) + { + mDecoder.removeComplexListener( listener ); + } + else + { + Log.error( getLogPrefix() + "attempt to remove a complex sample " + + "listener, but the decoder is null" ); + } + } + + public void addListener( Listener listener ) + { + mBroadcaster.addListener( listener ); + } + + public void addListeners( List> listeners ) + { + for( Listener listener: listeners ) + { + addListener( listener ); + } + } + + public void removeListener( Listener listener ) + { + mBroadcaster.removeListener( listener ); + } + + /** + * Runnable float sample processing task. Schedule this task to run 20 + * times a second when we're receiving complex samples from our source. + * Schedule execution via the thread pool manager, obtainable from the + * resource manager, + */ + private class FloatProcessor implements Runnable + { + @Override + public void run() + { + List samples = new ArrayList(); + + mFloatBuffer.drainTo( samples, 48000 ); + + for( Float sample: samples ) + { + if( mDecoder != null ) + { + mDecoder.getFloatReceiver().receive( sample ); + } + } + + samples.clear(); + } + } + + /** + * Runnable complex sample processing task. Schedule this task to run 20 + * times a second when we're receiving complex samples from our source. + * Schedule execution via the thread pool manager, obtainable from the + * resource manager, + */ + private class ComplexProcessor implements Runnable + { + @Override + public void run() + { + List> sampleSets = + new ArrayList>(); + + mComplexBuffer.drainTo( sampleSets, 16 ); + + for( List samples: sampleSets ) + { + for( ComplexSample sample: samples ) + { + if( sample != null && mDecoder != null ) + { + mDecoder.getComplexReceiver().receive( sample ); + } + } + + samples.clear(); + } + + sampleSets.clear(); + } + } + + public class ComplexListener implements Listener> + { + @Override + public void receive( List samples ) + { + mComplexBuffer.add( samples ); + } + } + + public class FloatListener implements Listener + { + @Override + public void receive( Float sample ) + { + mFloatBuffer.add( sample ); + } + } + + public class FloatArrayListener implements Listener + { + @Override + public void receive( Float[] samples ) + { + mFloatBuffer.addAll( Arrays.asList( samples ) ); + } + } +} diff --git a/src/controller/config/Configuration.java b/src/controller/config/Configuration.java new file mode 100644 index 00000000..ff979c4a --- /dev/null +++ b/src/controller/config/Configuration.java @@ -0,0 +1,40 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package controller.config; + +import javax.xml.bind.annotation.XmlRootElement; +import javax.xml.bind.annotation.XmlSeeAlso; + +import record.config.RecordConfiguration; +import source.config.SourceConfiguration; +import decode.config.AuxDecodeConfiguration; +import decode.config.DecodeConfiguration; +import eventlog.config.EventLogConfiguration; + +@XmlSeeAlso( { AuxDecodeConfiguration.class, + DecodeConfiguration.class, + EventLogConfiguration.class, + RecordConfiguration.class, + SourceConfiguration.class } ) +@XmlRootElement( name = "configuration" ) +public abstract class Configuration +{ + public Configuration() + { + } +} diff --git a/src/controller/site/Site.java b/src/controller/site/Site.java new file mode 100644 index 00000000..2cc24419 --- /dev/null +++ b/src/controller/site/Site.java @@ -0,0 +1,86 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package controller.site; + +import java.util.ArrayList; + +import javax.xml.bind.annotation.XmlAttribute; +import javax.xml.bind.annotation.XmlRootElement; +import javax.xml.bind.annotation.XmlSeeAlso; + +import controller.channel.Channel; + +@XmlSeeAlso( { Channel.class } ) +@XmlRootElement( name = "site" ) +public class Site +{ + private String mName; + + private ArrayList mChannel = new ArrayList(); + + public Site() + { + this( "New Site" ); + } + + public Site( String name ) + { + mName = name; + } + + public String toString() + { + return mName; + } + + @XmlAttribute + public String getName() + { + return mName; + } + + public void setName( String name ) + { + mName = name; + } + + public ArrayList getChannel() + { + return mChannel; + } + + public void setChannel( ArrayList configs ) + { + mChannel = configs; + } + + public void addChannel( Channel channel ) + { + mChannel.add( channel ); + } + + public void removeChannel( Channel channel ) + { + mChannel.remove( channel ); + } + + public void removeAllChannels() + { + mChannel.clear(); + } +} diff --git a/src/controller/site/SiteEditor.java b/src/controller/site/SiteEditor.java new file mode 100644 index 00000000..b4d4a74e --- /dev/null +++ b/src/controller/site/SiteEditor.java @@ -0,0 +1,101 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package controller.site; + +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; + +import javax.swing.BorderFactory; +import javax.swing.JButton; +import javax.swing.JLabel; +import javax.swing.JOptionPane; +import javax.swing.JPanel; +import javax.swing.JTextField; +import javax.swing.tree.TreePath; + +import net.miginfocom.swing.MigLayout; + +public class SiteEditor extends JPanel implements ActionListener +{ + private static final long serialVersionUID = 1L; + private SiteNode mSiteNode; + + private JLabel mLabelName; + private JTextField mTextSite; + + public SiteEditor( SiteNode site ) + { + mSiteNode = site; + initGUI(); + } + + private void initGUI() + { + setLayout( new MigLayout() ); + + setBorder( BorderFactory.createTitledBorder( "Site" ) ); + + mLabelName = new JLabel( "Name:" ); + add( mLabelName, "align right" ); + + mTextSite = new JTextField( mSiteNode.getSite().getName() ); + add( mTextSite, "grow, wrap" ); + + JButton btnSave = new JButton( "Save" ); + btnSave.addActionListener( SiteEditor.this ); + add( btnSave ); + + JButton btnReset = new JButton( "Reset" ); + btnReset.addActionListener( SiteEditor.this ); + add( btnReset, "wrap" ); + } + + @Override + public void actionPerformed( ActionEvent e ) + { + String command = e.getActionCommand(); + + if( command.contentEquals( "Save" ) ) + { + boolean expanded = mSiteNode.getModel().getTree() + .isExpanded( new TreePath( mSiteNode ) ); + + String name = mTextSite.getText(); + + if( name != null ) + { + mSiteNode.getSite().setName( name ); + mSiteNode.save(); + mSiteNode.show(); + + if( expanded ) + { + mSiteNode.getModel().getTree().expandPath( new TreePath( mSiteNode ) ); + } + } + else + { + JOptionPane.showMessageDialog( SiteEditor.this, "Please enter a system name" ); + } + } + else if( command.contentEquals( "Reset" ) ) + { + mTextSite.setText( mSiteNode.getSite().getName() ); + } + } +} diff --git a/src/controller/site/SiteNode.java b/src/controller/site/SiteNode.java new file mode 100644 index 00000000..6e30141e --- /dev/null +++ b/src/controller/site/SiteNode.java @@ -0,0 +1,167 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package controller.site; + + +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.util.Enumeration; + +import javax.swing.JMenuItem; +import javax.swing.JPanel; +import javax.swing.JPopupMenu; +import javax.swing.JSeparator; +import javax.swing.tree.DefaultMutableTreeNode; + +import controller.ConfigurableNode; +import controller.channel.Channel; +import controller.channel.ChannelNode; +import controller.system.System; +import controller.system.SystemNode; + +public class SiteNode extends ConfigurableNode +{ + private static final long serialVersionUID = 1L; + + public SiteNode( Site site ) + { + super( site ); + } + + public void init() + { + for( Channel channel: getSite().getChannel() ) + { + ChannelNode node = new ChannelNode( channel ); + + getModel().insertNodeInto( node, SiteNode.this, getChildCount() ); + + node.init(); + } + + sort(); + } + + public String getIconPath() + { + String retVal = "images/site.png"; + + @SuppressWarnings( "unchecked" ) + Enumeration nodes = children(); + + while( nodes.hasMoreElements() ) + { + ChannelNode child = (ChannelNode)nodes.nextElement(); + + if( child.getIconPath().contains( "green.png" ) ) + { + return "images/site_green.png"; + } + else if( child.getIconPath().contains( "red.png" ) ) + { + retVal = "images/site_red.png"; + } + } + + return retVal; + } + + @Override + public JPanel getEditor() + { + return new SiteEditor( this ); + } + + public Site getSite() + { + return (Site)getUserObject(); + } + + public String toString() + { + return getSite().getName(); + } + + public void delete() + { + for( int x = 0; x < getChildCount(); x++ ) + { + ((ChannelNode)getChildAt( x )).delete(); + } + + ((SystemNode)getParent()).getSystem().removeSite( getSite() ); + + save(); + + getModel().removeNodeFromParent( SiteNode.this ); + } + + public JPopupMenu getContextMenu() + { + JPopupMenu retVal = new JPopupMenu(); + + JMenuItem addChannelItem = new JMenuItem( "Add Channel" ); + addChannelItem.addActionListener( new ActionListener() + { + @Override + public void actionPerformed( ActionEvent e ) + { + System system = ((SystemNode)getParent()).getSystem(); + + Channel channel = new Channel(); + channel.setSystem( system, false ); + channel.setSite( getSite(), false ); + + getSite().addChannel( channel ); + + ChannelNode node = new ChannelNode( channel ); + + getModel().addNode( node, + SiteNode.this, + SiteNode.this.getChildCount() ); + + sort(); + save(); + node.show(); + + /* Give channel reference to the resource manager and the + * channel will register the listeners and send a channel + * add event */ + channel.setResourceManager( getModel().getResourceManager() ); + } + } ); + + retVal.add( addChannelItem ); + + retVal.add( new JSeparator() ); + + JMenuItem deleteItem = new JMenuItem( "Delete" ); + deleteItem.addActionListener( new ActionListener() + { + @Override + public void actionPerformed( ActionEvent e ) + { + delete(); + } + } ); + + retVal.add( deleteItem ); + + return retVal; + } +} diff --git a/src/controller/state/AuxChannelState.java b/src/controller/state/AuxChannelState.java new file mode 100644 index 00000000..218c37cc --- /dev/null +++ b/src/controller/state/AuxChannelState.java @@ -0,0 +1,80 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package controller.state; + +import message.Message; +import sample.Broadcaster; +import sample.Listener; +import controller.state.ChannelState.ChangedAttribute; + +/** + * ChannelState for auxiliary decoders. + */ +public abstract class AuxChannelState implements Listener +{ + protected ChannelState mParentChannelState; + protected Broadcaster mChangeBroadcaster = + new Broadcaster(); + + public AuxChannelState( ChannelState parentState ) + { + mParentChannelState = parentState; + } + + /** + * Prepare for garbage collect + */ + public void dispose() + { + mChangeBroadcaster.dispose(); + mParentChannelState = null; + } + + /** + * Call fade - change display and await a call to reset() + */ + public abstract void fade(); + + /** + * Activity summary for this decoder + */ + public abstract String getActivitySummary(); + + /** + * Call end - cleanup the display + */ + public abstract void reset(); + + public void addListener( Listener listener ) + { + mChangeBroadcaster.addListener( listener ); + } + + public void removeListener( Listener listener ) + { + mChangeBroadcaster.removeListener( listener ); + } + + public void broadcastChange( ChangedAttribute attribute ) + { + if( mChangeBroadcaster != null ) + { + mChangeBroadcaster.broadcast( attribute ); + } + } +} diff --git a/src/controller/state/AuxStatePanel.java b/src/controller/state/AuxStatePanel.java new file mode 100644 index 00000000..8d3bdba9 --- /dev/null +++ b/src/controller/state/AuxStatePanel.java @@ -0,0 +1,143 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package controller.state; + +import java.awt.Color; +import java.awt.Font; + +import javax.swing.ImageIcon; +import javax.swing.JLabel; +import javax.swing.JPanel; + +import net.miginfocom.swing.MigLayout; +import sample.Listener; +import settings.ColorSetting; +import settings.ColorSetting.ColorSettingName; +import settings.Setting; +import settings.SettingChangeListener; +import settings.SettingsManager; +import alias.Alias; +import controller.state.ChannelState.ChangedAttribute; + +public abstract class AuxStatePanel extends JPanel + implements Listener, + SettingChangeListener +{ + private static final long serialVersionUID = 1L; + + protected Font mFontDetails = new Font( Font.MONOSPACED, Font.PLAIN, 10 ); + protected Font mFontAuxDecoder = new Font( Font.MONOSPACED, Font.PLAIN, 10 ); + + protected Color mColorLabelDetails; + protected Color mColorLabelAuxDecoder; + + protected SettingsManager mSettingsManager; + protected AuxChannelState mAuxChannelState; + + public AuxStatePanel( SettingsManager settingsManager, + AuxChannelState auxChannelState ) + { + mSettingsManager = settingsManager; + mAuxChannelState = auxChannelState; + + /* Register to receive event updates */ + mAuxChannelState.addListener( this ); + + /* Register to receive settings updates */ + mSettingsManager.addListener( this ); + + getColors(); + + setLayout( new MigLayout( "insets 1 0 0 0", "[grow,fill]", "[]0[]0[]") ); + } + + public void dispose() + { + /* Deregister from changed settings events */ + mSettingsManager.removeListener( this ); + mSettingsManager = null; + + mAuxChannelState = null; + } + + protected void addLabel( JLabel label, boolean wrap ) + { + label.setFont( mFontAuxDecoder ); + label.setForeground( mColorLabelAuxDecoder ); + + if( wrap ) + { + add( label, "wrap" ); + } + else + { + add( label ); + } + } + + protected void updateAliasLabel( JLabel label, Alias alias ) + { + if( alias != null ) + { + label.setText( alias.getName() ); + + String iconName = alias.getIconName(); + + ImageIcon icon = mSettingsManager.getImageIcon( iconName, 12 ); + + label.setIcon( icon ); + } + else + { + label.setText( null ); + label.setIcon( null ); + } + } + + private void getColors() + { + mColorLabelDetails = mSettingsManager.getColorSetting( + ColorSettingName.CHANNEL_STATE_LABEL_DETAILS ).getColor(); + mColorLabelAuxDecoder = mSettingsManager.getColorSetting( + ColorSettingName.CHANNEL_STATE_LABEL_AUX_DECODER ).getColor(); + } + + @Override + public void settingChanged( Setting setting ) + { + if( setting instanceof ColorSetting ) + { + ColorSetting colorSetting = (ColorSetting)setting; + + switch( colorSetting.getColorSettingName() ) + { + case CHANNEL_STATE_LABEL_AUX_DECODER: + mColorLabelAuxDecoder = colorSetting.getColor(); + repaint(); + break; + case CHANNEL_STATE_LABEL_DETAILS: + mColorLabelDetails = colorSetting.getColor(); + repaint(); + break; + } + } + } + + @Override + public void settingDeleted( Setting setting ) {} +} diff --git a/src/controller/state/ChannelState.java b/src/controller/state/ChannelState.java new file mode 100644 index 00000000..0c87c0a2 --- /dev/null +++ b/src/controller/state/ChannelState.java @@ -0,0 +1,664 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package controller.state; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.ScheduledThreadPoolExecutor; +import java.util.concurrent.TimeUnit; + +import javax.swing.JTable; + +import log.Log; +import message.Message; +import properties.SystemProperties; +import sample.Broadcaster; +import sample.Listener; +import alias.AliasList; +import audio.AudioType; +import audio.AudioTypeListener; +import audio.SquelchListener; +import audio.SquelchListener.SquelchState; +import controller.activity.CallEvent; +import controller.activity.CallEvent.CallEventType; +import controller.activity.CallEventAliasCellRenderer; +import controller.activity.CallEventModel; +import controller.activity.MessageActivityModel; +import controller.channel.Channel.ChannelType; +import controller.channel.ProcessingChain; + +/** + * ChannelState provides a state machine for tracking a voice or data call + * through it's life cycle of CALL/DATA, FADE, and then END, and then back to + * an IDLE state. This class also provides squelch control for unsquelching + * sound to the speakers, and/or sound to an audio recorder + * + * State Descriptions: + * IDLE No voice or data call activity + * CALL/DATA A voice or data call has started, or is continuing + * CONTROL Control channel + * FADE The phase after a voice or data call when either an explicit + * call end has been received, or when no new signalling updates + * have been received, and the fade timer has expired. This phase + * allows for gui updates to signal to the user that the call is + * ended, while continuing to display the call details for the user + * END The phase after the voice or data call FADE, when the call is + * now over, and the gui should be reset to IDLE. + * + * When a call is started, invoke mTimerService.startFadeTimer() to start the + * call fade monitoring timer process that will automatically signal a call end + * once no more call signalling has been detected after the fade timeout has + * occurred, by invoking the fade() method. + * + * When a call ends, invoke the mTimerService.stopFadeTimer() and then invoke + * the mTimerService.startResetTimer(). Once the reset timeout occurs, + * the reset() method will be called, allowing the channel state to perform + * any reset actions, like clearing status and id labels on any view panels. + * + * AuxChannelState class allows for auxiliary decoders like Fleetsync to both + * have a state and leverage the state machine of the parent ChannelState. + * This allows a non-squelching channel state like for conventional fm to have + * an aux decoder like fleetsync, and allow the fleetsync auxchannelstate to + * leverage the call/end states of the parent conventional channelstate. + * + */ +public abstract class ChannelState implements Listener +{ + private State mState = State.IDLE; + private Object mCallFadeLock = new Object(); + private Object mCallResetLock = new Object(); + private long mFadeTimeout = -1; + private long mResetTimeout = -1; + + protected long mCallFadeTimeout; + protected long mCallResetTimeout; + protected TimerService mTimerService = new TimerService(); + protected ArrayList mSquelchListeners = + new ArrayList(); + protected ArrayList mAuxChannelStates = + new ArrayList(); + protected Broadcaster mChangeBroadcaster = + new Broadcaster(); + protected AudioTypeListener mAudioTypeListener; + protected AudioType mAudioType = AudioType.NORMAL; + + + protected CallEventModel mCallEventModel = new CallEventModel(); + + protected JTable mCallEventTable; + + protected MessageActivityModel mMessageActivityModel = + new MessageActivityModel(); + protected JTable mMessageActivityTable; + + protected ProcessingChain mProcessingChain; + + protected AliasList mAliasList; + + public ChannelState( ProcessingChain processingChain, AliasList aliasList ) + { + mProcessingChain = processingChain; + mAliasList = aliasList; + + + /* Register to receive decoded messages from the processing chain */ + mProcessingChain.addListener( this ); + + SystemProperties props = SystemProperties.getInstance(); + mCallFadeTimeout = props.get( "call.fade.timeout", 2000 ); + mCallResetTimeout = props.get( "call.reset.timeout", 4000 ); + } + + /** + * Allows setting the call event model to use for traffic channels, so that + * all traffic channels can share the parent control channel call event + * model. + * @param model + */ + public void setCallEventModel( CallEventModel model ) + { + mCallEventModel = model; + } + + public CallEventModel getCallEventModel() + { + return mCallEventModel; + } + + public void receiveCallEvent( CallEvent event ) + { + mCallEventModel.add( event ); + } + + public void dispose() + { + mTimerService.dispose(); + + mAuxChannelStates.clear(); + mSquelchListeners.clear(); + mChangeBroadcaster.dispose(); + mChangeBroadcaster = null; + + if( mProcessingChain.getChannel().getChannelType() == ChannelType.STANDARD ) + { + mCallEventModel.dispose(); + } + /* If we're a traffic channel, set the call event model to null so that + * we don't dispose the parent control channel's call event model */ + else + { + mCallEventModel = null; + } + + mMessageActivityModel.dispose(); + mMessageActivityModel = null; + + mAliasList = null; + mAudioTypeListener = null; + mCallEventModel = null; + mProcessingChain = null; + } + + /** + * Channel Activity Summary - implemented by the sub-class. + */ + public abstract String getActivitySummary(); + + public AliasList getAliasList() + { + return mAliasList; + } + + public boolean hasAliasList() + { + return mAliasList != null; + } + + public void broadcastChange( ChangedAttribute attribute ) + { + if( mChangeBroadcaster != null ) + { + mChangeBroadcaster.broadcast( attribute ); + } + } + + public void setCallFadeTimeout( long milliseconds ) + { + mCallFadeTimeout = milliseconds; + } + + public JTable getCallEventTable() + { + if( mCallEventTable == null ) + { + mCallEventTable = new JTable( mCallEventModel ); + mCallEventTable.setAutoCreateRowSorter( true ); + mCallEventTable.setAutoResizeMode( JTable.AUTO_RESIZE_LAST_COLUMN ); + + CallEventAliasCellRenderer renderer = + new CallEventAliasCellRenderer( mProcessingChain + .getResourceManager().getSettingsManager() ); + + mCallEventTable.getColumnModel().getColumn( 3 ) + .setCellRenderer( renderer ); + + mCallEventTable.getColumnModel().getColumn( 5 ) + .setCellRenderer( renderer ); + } + + return mCallEventTable; + } + + /** + * Returns the message activity model wrapped in a JTable. Although this + * may seem to violate separation between model and view, it simplifies + * retaining the JTable state for each channel by keeping a local reference + * to reuse + */ + public JTable getMessageActivityTable() + { + if( mMessageActivityTable == null ) + { + mMessageActivityTable = new JTable( mMessageActivityModel ); + mMessageActivityTable.setAutoResizeMode( JTable.AUTO_RESIZE_LAST_COLUMN ); + } + + return mMessageActivityTable; + } + + public void addAuxChannelState( AuxChannelState state ) + { + mAuxChannelStates.add( state ); + } + + public ArrayList getAuxChannelStates() + { + return mAuxChannelStates; + } + + public void receive( Message message ) + { + if( mMessageActivityModel != null ) + { + mMessageActivityModel.receive( message ); + } + + for( AuxChannelState state: mAuxChannelStates ) + { + if( state != null ) + { + state.receive( message ); + } + } + } + + public ProcessingChain getProcessingChain() + { + return mProcessingChain; + } + + public State getState() + { + return mState; + } + + public void setState( State state ) + { + /* Let all CALL state changes go through in order to update the fade + * timer, otherwise, don't update the state if it hasn't changed */ + if( mState != state || state == State.CALL ) + { + if( mState.canChangeTo( state ) ) + { + mState = state; + + switch( state ) + { + case CALL: + setSquelchState( SquelchState.UNSQUELCH ); + case DATA: + mTimerService.stopResetTimer(); + mTimerService.startFadeTimer(); + break; + case NO_TUNER: + mTimerService.stopResetTimer(); + if( getProcessingChain().getChannel() + .getChannelType() == ChannelType.TRAFFIC ) + { + mTimerService.startFadeTimer(); + } + break; + case FADE: + mTimerService.stopFadeTimer(); + mTimerService.startResetTimer(); + setSquelchState( SquelchState.SQUELCH ); + if( mAudioTypeListener != null ) + { + mAudioTypeListener.setAudioType( mAudioType ); + } + break; + case END: + mTimerService.stopResetTimer(); + mTimerService.stopFadeTimer(); + setSquelchState( SquelchState.SQUELCH ); + if( mAudioTypeListener != null ) + { + mAudioTypeListener.setAudioType( mAudioType ); + } + + /* If this is a traffic channel, stop it */ + if( mProcessingChain.getChannel() + .getChannelType() == ChannelType.TRAFFIC ) + { + mProcessingChain.getChannel().setEnabled( false ); + } + break; + case IDLE: + break; + default: + break; + } + + broadcastChange( ChangedAttribute.CHANNEL_STATE ); + } + else + { + Log.error( "Channel State - can't change from [" + + mState.toString() + "] to [" + state.toString() + "]" ); + } + } + } + + /** + * End method to reset the channel state once a call has completely + * ended and now you want to return the channel state to idle + */ + protected void reset() + { + setState( State.END ); + + for( AuxChannelState state: mAuxChannelStates ) + { + state.reset(); + } + + //Reset the state to idle + setState( State.IDLE ); + } + + /** + * Method to perform any call fade actions. On fade, you should change the + * channel state to fading, and construct a call-reset timer to execute a few + * seconds later. This allows the view to signal to the user that the call + * has faded/ended, and display faded information to the user, until the + * cleanup() task is run. + */ + protected void fade( CallEventType type ) + { + for( AuxChannelState state: mAuxChannelStates ) + { + state.fade(); + } + + setState( State.FADE ); + } + + public void setListener( AudioTypeListener listener ) + { + mAudioTypeListener = listener; + } + + public void addListener( SquelchListener listener ) + { + mSquelchListeners.add( listener ); + } + + public void removeListener( SquelchListener listener ) + { + mSquelchListeners.remove( listener ); + } + + protected void setSquelchState( SquelchState state ) + { + Iterator it = mSquelchListeners.iterator(); + + while( it.hasNext() ) + { + it.next().setSquelch( state ); + } + } + + /** + * Timer service + */ + public class TimerService extends ScheduledThreadPoolExecutor + { + private long mTimerMonitorRevisitRate = 100; //100 milliseconds + + private ScheduledFuture mTimerMonitor; + + public TimerService() + { + super( 1 ); + } + + public void dispose() + { + if( mTimerMonitor != null ) + { + mTimerMonitor.cancel( true ); + } + + this.shutdownNow(); + } + + private void checkMonitor() + { + boolean createTimer = false; + + synchronized( mCallFadeLock ) + { + if( mFadeTimeout != -1 ) + { + createTimer = true; + } + } + + synchronized( mCallResetLock ) + { + if( mResetTimeout != -1 ) + { + createTimer = true; + } + } + + if( createTimer ) + { + //Check to see if the timer is already running, if not, start it + + if( mTimerMonitor == null || mTimerMonitor.isDone() ) + { + scheduleAtFixedRate( new StateTimeoutMonitor(), + mTimerMonitorRevisitRate, + mTimerMonitorRevisitRate, + TimeUnit.MILLISECONDS ); + } + } + else + { + if( mTimerMonitor != null ) + { + mTimerMonitor.cancel( true ); + } + } + } + + public void startFadeTimer() + { + synchronized( mCallFadeLock ) + { + mFadeTimeout = System.currentTimeMillis() + mCallFadeTimeout; + } + + checkMonitor(); + } + + public void resetFadeTimer() + { + startFadeTimer(); + } + + public void stopFadeTimer() + { + synchronized( mCallFadeLock ) + { + mFadeTimeout = -1; + } + + checkMonitor(); + } + + public void startResetTimer() + { + synchronized( mCallResetLock ) + { + mResetTimeout = System.currentTimeMillis() + mCallResetTimeout; + } + + checkMonitor(); + } + + public void stopResetTimer() + { + synchronized( mCallResetLock ) + { + mResetTimeout = -1; + } + + checkMonitor(); + } + } + + public class StateTimeoutMonitor implements Runnable + { + @Override + public void run() + { + long now = System.currentTimeMillis(); + + synchronized( mCallFadeLock ) + { + if( mFadeTimeout != -1 && now >= mFadeTimeout ) + { + mFadeTimeout = -1; + + fade( CallEventType.CALL_TIMEOUT ); + } + } + + synchronized( mCallResetLock ) + { + if( mResetTimeout != -1 && now >= mResetTimeout ) + { + mResetTimeout = -1; + + reset(); + } + } + } + } + + + public enum State + { + IDLE( "IDLE" ) { + @Override + public boolean canChangeTo( State state ) + { + return state == IDLE || + state == CALL || + state == CONTROL || + state == DATA || + state == FADE || + state == NO_TUNER; + } + }, + CALL( "CALL" ) { + @Override + public boolean canChangeTo( State state ) + { + return state == CALL || + state == CONTROL || + state == DATA || + state == FADE; + } + }, + DATA( "DATA" ) { + @Override + public boolean canChangeTo( State state ) + { + return state == CALL || + state == CONTROL || + state == DATA || + state == FADE; + } + }, + CONTROL( "CONTROL" ) + { + @Override + public boolean canChangeTo( State state ) + { + return true; + } + }, + FADE( "FADE" ) { + @Override + public boolean canChangeTo( State state ) + { + return state != FADE; //All states except fade allowed + } + }, + END( "END" ) { + @Override + public boolean canChangeTo( State state ) + { + return state != FADE; //Fade is only disallowed state + } + }, + NO_TUNER( "NO TUNER" ) { + @Override + public boolean canChangeTo( State state ) + { + return state == FADE || state == END; + } + }; + + private String mDisplayValue; + + private State( String displayValue ) + { + mDisplayValue = displayValue; + } + + public abstract boolean canChangeTo( State state ); + + public String getDisplayValue() + { + return mDisplayValue; + } + } + + /** + * Registers a listener to receive channel state attribute changes + */ + public void addListener( Listener listener ) + { + mChangeBroadcaster.addListener( listener ); + } + + /** + * Removes a listener from receiving channel state attribute changes + */ + public void removeListener( Listener listener ) + { + mChangeBroadcaster.removeListener( listener ); + } + + public enum ChangedAttribute + { + AUDIO_TYPE, + CHANNEL_NUMBER, + CHANNEL_NAME, + CHANNEL_SITE_NUMBER, + CHANNEL_SITE_NUMBER_ALIAS, + CHANNEL_STATE, + FROM_TALKGROUP, + FROM_TALKGROUP_ALIAS, + FROM_TALKGROUP_TYPE, + MESSAGE, + MESSAGE_TYPE, + SITE_NAME, + SOURCE, + SYSTEM_NAME, + SQUELCH, + TO_TALKGROUP, + TO_TALKGROUP_ALIAS, + TO_TALKGROUP_TYPE; + } +} diff --git a/src/controller/state/ChannelStateList.java b/src/controller/state/ChannelStateList.java new file mode 100644 index 00000000..6db9b475 --- /dev/null +++ b/src/controller/state/ChannelStateList.java @@ -0,0 +1,442 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package controller.state; + +import java.awt.Color; +import java.awt.Component; +import java.awt.Point; +import java.awt.event.MouseEvent; +import java.awt.event.MouseListener; +import java.util.HashMap; + +import javax.swing.JMenu; +import javax.swing.JPanel; +import javax.swing.JPopupMenu; +import javax.swing.JSeparator; +import javax.swing.SwingUtilities; + +import net.miginfocom.swing.MigLayout; +import settings.ColorSetting.ColorSettingName; +import settings.ColorSettingMenuItem; +import settings.ColorSettingResetMenuItem; +import settings.SettingsManager; +import controller.channel.Channel; +import controller.channel.Channel.ChannelEvent; +import controller.channel.Channel.ChannelType; +import controller.channel.ChannelListener; +import decode.fleetsync2.FleetsyncChannelState; +import decode.fleetsync2.FleetsyncPanel; +import decode.ltrnet.LTRNetChannelState; +import decode.ltrnet.LTRNetPanel; +import decode.ltrstandard.LTRChannelState; +import decode.ltrstandard.LTRPanel; +import decode.mdc1200.MDCChannelState; +import decode.mdc1200.MDCPanel; +import decode.mpt1327.MPT1327ChannelState; +import decode.mpt1327.MPT1327Panel; +import decode.nbfm.NBFMChannelState; +import decode.nbfm.NBFMPanel; +import decode.passport.PassportChannelState; +import decode.passport.PassportPanel; + +public class ChannelStateList extends JPanel implements ChannelListener +{ + private static final long serialVersionUID = 1L; + + private HashMap mDisplayedPanels = + new HashMap(); + + private SettingsManager mSettingsManager; + + public ChannelStateList( SettingsManager settingsManager ) + { + mSettingsManager = settingsManager; + + init(); + } + + private void init() + { + setLayout( new MigLayout( "insets 0 0 0 0", "[grow,fill]", "[]0[]") ); + setBackground( Color.BLACK ); + + addMouseListener( new ListSelectionListener() ); + } + + @Override + public void occurred( Channel channel, ChannelEvent event ) + { + ChannelType type = channel.getChannelType(); + + switch( event ) + { + case PROCESSING_STARTED: + if( type == ChannelType.STANDARD ) + { + channelStarted( channel ); + } + else if( type == ChannelType.TRAFFIC ) + { + for( Channel parent: mDisplayedPanels.keySet() ) + { + if( parent.hasTrafficChannel( channel ) ) + { + mDisplayedPanels.get( parent ).trafficChannelAdded( channel ); + validate(); + } + } + } + break; + case PROCESSING_STOPPED: + if( type == ChannelType.STANDARD ) + { + channelStopped( channel ); + } + /* Since we don't have a reference to the parent channel that + * owns the traffic channel, iterate all channels to find the + * parent */ + else if( type == ChannelType.TRAFFIC ) + { + for( Channel control: mDisplayedPanels.keySet() ) + { + if( control.hasTrafficChannel( channel ) ) + { + control.removeTrafficChannel( channel ); + + mDisplayedPanels.get( control ).trafficChannelDeleted( channel ); + + validate(); + } + } + } + break; + default: + break; + } + } + + public void channelStarted( Channel channel ) + { + if( !mDisplayedPanels.containsKey( channel ) ) + { + ChannelState state = channel.getProcessingChain().getChannelState(); + + ChannelStatePanel panel = null; + + if( state instanceof NBFMChannelState ) + { + NBFMChannelState convState = + (NBFMChannelState)state; + + panel = new NBFMPanel( channel ); + + for( AuxChannelState aux: convState.getAuxChannelStates() ) + { + if( aux instanceof FleetsyncChannelState ) + { + FleetsyncPanel fsPanel = + new FleetsyncPanel( mSettingsManager, + (FleetsyncChannelState)aux ); + + panel.addPanel( fsPanel ); + } + else if( aux instanceof MDCChannelState ) + { + MDCPanel mdsPanel = new MDCPanel( mSettingsManager, + (MDCChannelState)aux ); + + panel.addPanel( mdsPanel ); + } + } + } + else if( state instanceof LTRChannelState ) + { + LTRChannelState ltrState = (LTRChannelState)state; + + panel = new LTRPanel( mSettingsManager, channel ); + + for( AuxChannelState aux: ltrState.getAuxChannelStates() ) + { + if( aux instanceof FleetsyncChannelState ) + { + FleetsyncPanel fsPanel = new FleetsyncPanel( mSettingsManager, + (FleetsyncChannelState)aux ); + + panel.addPanel( fsPanel ); + } + } + } + else if( state instanceof LTRNetChannelState ) + { + LTRNetChannelState ltrState = (LTRNetChannelState)state; + + panel = new LTRNetPanel( channel ); + + for( AuxChannelState aux: ltrState.getAuxChannelStates() ) + { + if( aux instanceof FleetsyncChannelState ) + { + FleetsyncPanel fsPanel = new FleetsyncPanel( mSettingsManager, + (FleetsyncChannelState)aux ); + + panel.addPanel( fsPanel ); + } + } + } + else if( state instanceof MPT1327ChannelState ) + { + MPT1327ChannelState mptState = (MPT1327ChannelState)state; + + panel = new MPT1327Panel( channel ); + } + else if( state instanceof PassportChannelState ) + { + PassportChannelState passportState = (PassportChannelState)state; + + panel = new PassportPanel( mSettingsManager, channel ); + + for( AuxChannelState aux: passportState.getAuxChannelStates() ) + { + if( aux instanceof FleetsyncChannelState ) + { + FleetsyncPanel fsPanel = new FleetsyncPanel( mSettingsManager, + (FleetsyncChannelState)aux ); + + panel.addPanel( fsPanel ); + } + } + } + + if( panel != null ) + { + add( panel, "wrap" ); + + mDisplayedPanels.put( channel, panel ); + + validate(); + } + } + } + + public void channelStopped( Channel channel ) + { + if( mDisplayedPanels.containsKey( channel ) ) + { + ChannelStatePanel panel = mDisplayedPanels.get( channel ); + + if( panel != null ) + { + remove( panel ); + + validate(); + } + + mDisplayedPanels.remove( channel ); + } + } + + public void setSelectedChannel( ChannelStatePanel selectedPanel ) + { + boolean current = selectedPanel.getSelected(); + + for( ChannelStatePanel panel: mDisplayedPanels.values() ) + { + //Turn selected off + panel.setSelected( false ); + } + + selectedPanel.setSelected( !current ); + } + + public class ListSelectionListener implements MouseListener + { + @Override + public void mouseClicked( MouseEvent e ) + { + Component component = getComponentAt( e.getPoint() ); + + if( component instanceof ChannelStatePanel ) + { + ChannelStatePanel panel = (ChannelStatePanel)component; + + /* If the panel has child traffic channels, attempt to get + * one of them for the mouse event */ + if( panel.getChannel().getTrafficChannels().size() > 0 ) + { + for( ChannelStatePanel trafficPanel: panel.getTrafficPanels().values() ) + { + Point translatedPoint = SwingUtilities + .convertPoint( e.getComponent(), e.getPoint(), + trafficPanel ); + + if( trafficPanel.contains( translatedPoint ) ) + { + panel = trafficPanel; + break; + } + } + } + + if( e.getButton() == MouseEvent.BUTTON1 ) + { + setSelectedChannel( panel ); + } + else if( e.getButton() == MouseEvent.BUTTON3 ) + { + JPopupMenu popupMenu = new JPopupMenu(); + JMenu menu = panel.getChannel().getContextMenu(); + + popupMenu.add( menu ); + + JMenu colorMenu = new JMenu( "Color" ); + popupMenu.add( colorMenu ); + + JMenu backgroundMenu = new JMenu( "Background" ); + colorMenu.add( backgroundMenu ); + + backgroundMenu.add( new ColorSettingMenuItem( mSettingsManager, + ColorSettingName.CHANNEL_STATE_BACKGROUND ) ); + backgroundMenu.add( new ColorSettingResetMenuItem( mSettingsManager, + ColorSettingName.CHANNEL_STATE_BACKGROUND ) ); + + JMenu channelsMenu = new JMenu( "Channel State" ); + colorMenu.add( channelsMenu ); + + JMenu callMenu = new JMenu( "Call" ); + channelsMenu.add( callMenu ); + + callMenu.add( new ColorSettingMenuItem( mSettingsManager, + ColorSettingName.CHANNEL_STATE_GRADIENT_MIDDLE_CALL ) ); + callMenu.add( new ColorSettingResetMenuItem( mSettingsManager, + ColorSettingName.CHANNEL_STATE_GRADIENT_MIDDLE_CALL ) ); + callMenu.add( new JSeparator() ); + + callMenu.add( new ColorSettingMenuItem( mSettingsManager, + ColorSettingName.CHANNEL_STATE_GRADIENT_TOP_CALL ) ); + callMenu.add( new ColorSettingResetMenuItem( mSettingsManager, + ColorSettingName.CHANNEL_STATE_GRADIENT_TOP_CALL ) ); + + JMenu controlMenu = new JMenu( "Control" ); + channelsMenu.add( controlMenu ); + + controlMenu.add( new ColorSettingMenuItem( mSettingsManager, + ColorSettingName.CHANNEL_STATE_GRADIENT_MIDDLE_CONTROL ) ); + controlMenu.add( new ColorSettingResetMenuItem( mSettingsManager, + ColorSettingName.CHANNEL_STATE_GRADIENT_MIDDLE_CONTROL ) ); + controlMenu.add( new JSeparator() ); + + controlMenu.add( new ColorSettingMenuItem( mSettingsManager, + ColorSettingName.CHANNEL_STATE_GRADIENT_TOP_CONTROL ) ); + controlMenu.add( new ColorSettingResetMenuItem( mSettingsManager, + ColorSettingName.CHANNEL_STATE_GRADIENT_TOP_CONTROL ) ); + + JMenu dataMenu = new JMenu( "Data" ); + channelsMenu.add( dataMenu ); + + dataMenu.add( new ColorSettingMenuItem( mSettingsManager, + ColorSettingName.CHANNEL_STATE_GRADIENT_MIDDLE_DATA ) ); + dataMenu.add( new ColorSettingResetMenuItem( mSettingsManager, + ColorSettingName.CHANNEL_STATE_GRADIENT_MIDDLE_DATA ) ); + dataMenu.add( new JSeparator() ); + + dataMenu.add( new ColorSettingMenuItem( mSettingsManager, + ColorSettingName.CHANNEL_STATE_GRADIENT_TOP_DATA ) ); + dataMenu.add( new ColorSettingResetMenuItem( mSettingsManager, + ColorSettingName.CHANNEL_STATE_GRADIENT_TOP_DATA ) ); + + JMenu fadeMenu = new JMenu( "Fade" ); + channelsMenu.add( fadeMenu ); + + fadeMenu.add( new ColorSettingMenuItem( mSettingsManager, + ColorSettingName.CHANNEL_STATE_GRADIENT_MIDDLE_FADE ) ); + fadeMenu.add( new ColorSettingResetMenuItem( mSettingsManager, + ColorSettingName.CHANNEL_STATE_GRADIENT_MIDDLE_FADE ) ); + fadeMenu.add( new JSeparator() ); + + fadeMenu.add( new ColorSettingMenuItem( mSettingsManager, + ColorSettingName.CHANNEL_STATE_GRADIENT_TOP_FADE ) ); + fadeMenu.add( new ColorSettingResetMenuItem( mSettingsManager, + ColorSettingName.CHANNEL_STATE_GRADIENT_TOP_FADE ) ); + + JMenu idleMenu = new JMenu( "Idle" ); + channelsMenu.add( idleMenu ); + + idleMenu.add( new ColorSettingMenuItem( mSettingsManager, + ColorSettingName.CHANNEL_STATE_GRADIENT_MIDDLE_IDLE ) ); + idleMenu.add( new ColorSettingResetMenuItem( mSettingsManager, + ColorSettingName.CHANNEL_STATE_GRADIENT_MIDDLE_IDLE ) ); + idleMenu.add( new JSeparator() ); + + idleMenu.add( new ColorSettingMenuItem( mSettingsManager, + ColorSettingName.CHANNEL_STATE_GRADIENT_TOP_IDLE ) ); + idleMenu.add( new ColorSettingResetMenuItem( mSettingsManager, + ColorSettingName.CHANNEL_STATE_GRADIENT_TOP_IDLE ) ); + + JMenu noTunerMenu = new JMenu( "No Tuner" ); + channelsMenu.add( noTunerMenu ); + + noTunerMenu.add( new ColorSettingMenuItem( mSettingsManager, + ColorSettingName.CHANNEL_STATE_GRADIENT_MIDDLE_NO_TUNER ) ); + noTunerMenu.add( new ColorSettingResetMenuItem( mSettingsManager, + ColorSettingName.CHANNEL_STATE_GRADIENT_MIDDLE_NO_TUNER ) ); + noTunerMenu.add( new JSeparator() ); + + noTunerMenu.add( new ColorSettingMenuItem( mSettingsManager, + ColorSettingName.CHANNEL_STATE_GRADIENT_TOP_NO_TUNER ) ); + noTunerMenu.add( new ColorSettingResetMenuItem( mSettingsManager, + ColorSettingName.CHANNEL_STATE_GRADIENT_TOP_NO_TUNER ) ); + + JMenu labelMenu = new JMenu( "Labels" ); + colorMenu.add( labelMenu ); + + labelMenu.add( new ColorSettingMenuItem( mSettingsManager, + ColorSettingName.CHANNEL_STATE_LABEL_AUX_DECODER ) ); + labelMenu.add( new ColorSettingResetMenuItem( mSettingsManager, + ColorSettingName.CHANNEL_STATE_LABEL_AUX_DECODER ) ); + labelMenu.add( new JSeparator() ); + + labelMenu.add( new ColorSettingMenuItem( mSettingsManager, + ColorSettingName.CHANNEL_STATE_LABEL_DECODER ) ); + labelMenu.add( new ColorSettingResetMenuItem( mSettingsManager, + ColorSettingName.CHANNEL_STATE_LABEL_DECODER ) ); + labelMenu.add( new JSeparator() ); + + labelMenu.add( new ColorSettingMenuItem( mSettingsManager, + ColorSettingName.CHANNEL_STATE_LABEL_DETAILS ) ); + labelMenu.add( new ColorSettingResetMenuItem( mSettingsManager, + ColorSettingName.CHANNEL_STATE_LABEL_DETAILS ) ); + + JMenu selectedMenu = new JMenu( "Selected" ); + colorMenu.add( selectedMenu ); + + selectedMenu.add( new ColorSettingMenuItem( mSettingsManager, + ColorSettingName.CHANNEL_STATE_SELECTED_CHANNEL ) ); + selectedMenu.add( new ColorSettingResetMenuItem( mSettingsManager, + ColorSettingName.CHANNEL_STATE_SELECTED_CHANNEL ) ); + + popupMenu.show( ChannelStateList.this, e.getX(), e.getY() ); + } + } + } + + public void mousePressed( MouseEvent e ) {} + public void mouseReleased( MouseEvent e ) {} + public void mouseEntered( MouseEvent e ) {} + public void mouseExited( MouseEvent e ) {} + } +} diff --git a/src/controller/state/ChannelStatePanel.java b/src/controller/state/ChannelStatePanel.java new file mode 100644 index 00000000..9937fc5f --- /dev/null +++ b/src/controller/state/ChannelStatePanel.java @@ -0,0 +1,386 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package controller.state; + +import java.awt.Color; +import java.awt.EventQueue; +import java.awt.Font; +import java.awt.GradientPaint; +import java.awt.Graphics; +import java.awt.Graphics2D; +import java.awt.Paint; +import java.util.HashMap; + +import javax.swing.JMenu; +import javax.swing.JPanel; + +import log.Log; +import net.miginfocom.swing.MigLayout; +import sample.Listener; +import settings.ColorSetting; +import settings.ColorSetting.ColorSettingName; +import settings.Setting; +import settings.SettingChangeListener; +import settings.SettingsManager; +import controller.channel.Channel; +import controller.state.ChannelState.ChangedAttribute; +import controller.state.ChannelState.State; + +public abstract class ChannelStatePanel extends JPanel + implements Listener, + SettingChangeListener +{ + private static final long serialVersionUID = 1L; + + protected Font mFontDetails = new Font( Font.MONOSPACED, Font.PLAIN, 10 ); + protected Font mFontDecoder = new Font( Font.MONOSPACED, Font.PLAIN, 10 ); + protected Font mFontAuxDecoder = new Font( Font.MONOSPACED, Font.PLAIN, 10 ); + + protected Color mColorChannelBackground; + protected Color mColorChannelSelected; + protected Color mColorTopCall; + protected Color mColorMiddleCall; + protected Color mColorTopControl; + protected Color mColorMiddleControl; + protected Color mColorTopData; + protected Color mColorMiddleData; + protected Color mColorTopFade; + protected Color mColorMiddleFade; + protected Color mColorTopIdle; + protected Color mColorMiddleIdle; + protected Color mColorTopNoTuner; + protected Color mColorMiddleNoTuner; + protected Color mColorLabelDetails; + protected Color mColorLabelDecoder; + protected Color mColorLabelAuxDecoder; + + private HashMap mTrafficPanels = + new HashMap(); + + protected Channel mChannel; + + public ChannelStatePanel( Channel channel ) + { + mChannel = channel; + + /* Register to receive settings updates */ + getSettingsManager().addListener( this ); + + /* Register to receive channel updates */ + mChannel.getProcessingChain().getChannelState().addListener( this ); + + getColors(); + + setLayout( new MigLayout( "insets 3 2 2 2", "[grow,fill]", "[]0[]0[]") ); + } + + public SettingsManager getSettingsManager() + { + return mChannel.getResourceManager().getSettingsManager(); + } + + public void dispose() + { + mTrafficPanels.clear(); + mChannel = null; + } + + private void getColors() + { + mColorChannelBackground = getSettingsManager().getColorSetting( + ColorSettingName.CHANNEL_STATE_BACKGROUND ).getColor(); + mColorChannelSelected = getSettingsManager().getColorSetting( + ColorSettingName.CHANNEL_STATE_SELECTED_CHANNEL ).getColor(); + mColorTopCall = getSettingsManager().getColorSetting( + ColorSettingName.CHANNEL_STATE_GRADIENT_TOP_CALL ).getColor(); + mColorMiddleCall = getSettingsManager().getColorSetting( + ColorSettingName.CHANNEL_STATE_GRADIENT_MIDDLE_CALL ).getColor(); + mColorTopControl = getSettingsManager().getColorSetting( + ColorSettingName.CHANNEL_STATE_GRADIENT_TOP_CONTROL ).getColor(); + mColorMiddleControl = getSettingsManager().getColorSetting( + ColorSettingName.CHANNEL_STATE_GRADIENT_MIDDLE_CONTROL ).getColor(); + mColorTopData = getSettingsManager().getColorSetting( + ColorSettingName.CHANNEL_STATE_GRADIENT_TOP_DATA ).getColor(); + mColorMiddleData = getSettingsManager().getColorSetting( + ColorSettingName.CHANNEL_STATE_GRADIENT_MIDDLE_DATA ).getColor(); + mColorTopFade = getSettingsManager().getColorSetting( + ColorSettingName.CHANNEL_STATE_GRADIENT_TOP_FADE ).getColor(); + mColorMiddleFade = getSettingsManager().getColorSetting( + ColorSettingName.CHANNEL_STATE_GRADIENT_MIDDLE_FADE ).getColor(); + mColorTopIdle = getSettingsManager().getColorSetting( + ColorSettingName.CHANNEL_STATE_GRADIENT_TOP_IDLE ).getColor(); + mColorMiddleIdle = getSettingsManager().getColorSetting( + ColorSettingName.CHANNEL_STATE_GRADIENT_MIDDLE_IDLE ).getColor(); + mColorTopNoTuner = getSettingsManager().getColorSetting( + ColorSettingName.CHANNEL_STATE_GRADIENT_TOP_NO_TUNER ).getColor(); + mColorMiddleNoTuner = getSettingsManager().getColorSetting( + ColorSettingName.CHANNEL_STATE_GRADIENT_MIDDLE_NO_TUNER ).getColor(); + + mColorLabelDetails = getSettingsManager().getColorSetting( + ColorSettingName.CHANNEL_STATE_LABEL_DETAILS ).getColor(); + mColorLabelDecoder = getSettingsManager().getColorSetting( + ColorSettingName.CHANNEL_STATE_LABEL_DECODER ).getColor(); + mColorLabelAuxDecoder = getSettingsManager().getColorSetting( + ColorSettingName.CHANNEL_STATE_LABEL_AUX_DECODER ).getColor(); + } + + public HashMap getTrafficPanels() + { + return mTrafficPanels; + } + + public JMenu getContextMenu() + { + return mChannel.getContextMenu(); + } + + public Channel getChannel() + { + return mChannel; + } + + public ChannelState getChannelState() + { + if( mChannel.getProcessingChain() != null ) + { + return mChannel.getProcessingChain().getChannelState(); + } + + return null; + } + + public boolean getSelected() + { + return mChannel.getSelected(); + } + + public void addPanel( final JPanel panel ) + { + EventQueue.invokeLater( new Runnable() + { + @Override + public void run() + { + add( panel, "span,wrap" ); + validate(); + } + } ); + } + + public void removePanel( final JPanel panel ) + { + EventQueue.invokeLater( new Runnable() + { + @Override + public void run() + { + remove( panel ); + + validate(); + + getParent().validate(); + + if( panel instanceof ChannelStatePanel ) + { + ((ChannelStatePanel)panel).dispose(); + } + else if( panel instanceof AuxStatePanel ) + { + ((AuxStatePanel)panel).dispose(); + } + } + } ); + } + + public void trafficChannelAdded( Channel traffic ) + { + //Disregard -- sub class can override and implement + } + + public void trafficChannelDeleted( Channel traffic ) + { + ChannelStatePanel panelToRemove = + getTrafficPanels().remove( traffic ); + + removePanel( panelToRemove ); + } + + public void setSelected( boolean selected ) + { + mChannel.setSelected( selected ); + + for( ChannelStatePanel traffic: getTrafficPanels().values() ) + { + traffic.setSelected( false ); + } + + repaint(); + } + + private GradientPaint getGradient( Color top, Color middle ) + { + return new GradientPaint( 0f, -50.0f, top, + 0f, (float)getHeight() / 2.2f, middle, true ); + } + + @Override + protected void paintComponent(Graphics g) + { + super.paintComponent( g ); + + setBackground( mColorChannelBackground ); + + Graphics2D g2 = (Graphics2D)g.create(); + + Paint p = null; + + ChannelState channelState = getChannelState(); + + if( channelState != null ) + { + State state = channelState.getState(); + + if( state != null ) + { + switch( state ) + { + case CALL: + p = getGradient( mColorTopCall, mColorMiddleCall ); + break; + case CONTROL: + p = getGradient( mColorTopControl, mColorMiddleControl ); + break; + case DATA: + p = getGradient( mColorTopData, mColorMiddleData ); + break; + case FADE: + p = getGradient( mColorTopFade, mColorMiddleFade ); + break; + case NO_TUNER: + p = getGradient( mColorTopNoTuner, mColorMiddleNoTuner ); + break; + case END: + case IDLE: + default: + p = getGradient( mColorTopIdle, mColorMiddleIdle ); + break; + } + } + } + + g2.setPaint( p ); + g2.fillRect( 0, 0, getWidth(), getHeight() ); + + /* Draw bottom separator line */ + g2.setColor( Color.LIGHT_GRAY ); + g2.drawLine( 0, getHeight() - 1, getWidth(), getHeight() - 1 ); + + /* Draw channel selected box */ + if( mChannel.getSelected() ) + { + g2.setColor( mColorChannelSelected ); + g2.drawRect( 1, 1, getWidth() - 2, getHeight() - 2 ); + } + + g2.dispose(); + } + + @Override + public void settingChanged( Setting setting ) + { + if( setting instanceof ColorSetting ) + { + ColorSetting colorSetting = (ColorSetting)setting; + + switch( colorSetting.getColorSettingName() ) + { + case CHANNEL_STATE_BACKGROUND: + mColorChannelBackground = colorSetting.getColor(); + repaint(); + break; + case CHANNEL_STATE_GRADIENT_MIDDLE_CALL: + mColorMiddleCall = colorSetting.getColor(); + repaint(); + break; + case CHANNEL_STATE_GRADIENT_TOP_CALL: + mColorTopCall = colorSetting.getColor(); + repaint(); + break; + case CHANNEL_STATE_GRADIENT_MIDDLE_CONTROL: + mColorMiddleControl = colorSetting.getColor(); + repaint(); + break; + case CHANNEL_STATE_GRADIENT_TOP_CONTROL: + mColorTopControl = colorSetting.getColor(); + repaint(); + break; + case CHANNEL_STATE_GRADIENT_MIDDLE_DATA: + mColorMiddleData = colorSetting.getColor(); + repaint(); + break; + case CHANNEL_STATE_GRADIENT_TOP_DATA: + mColorTopData = colorSetting.getColor(); + repaint(); + break; + case CHANNEL_STATE_GRADIENT_MIDDLE_FADE: + mColorMiddleFade = colorSetting.getColor(); + repaint(); + break; + case CHANNEL_STATE_GRADIENT_TOP_FADE: + mColorTopFade = colorSetting.getColor(); + repaint(); + break; + case CHANNEL_STATE_GRADIENT_MIDDLE_IDLE: + mColorMiddleIdle = colorSetting.getColor(); + repaint(); + break; + case CHANNEL_STATE_GRADIENT_TOP_IDLE: + mColorTopIdle = colorSetting.getColor(); + repaint(); + break; + case CHANNEL_STATE_GRADIENT_MIDDLE_NO_TUNER: + mColorMiddleNoTuner = colorSetting.getColor(); + repaint(); + break; + case CHANNEL_STATE_GRADIENT_TOP_NO_TUNER: + mColorTopNoTuner = colorSetting.getColor(); + repaint(); + break; + case CHANNEL_STATE_LABEL_AUX_DECODER: + mColorLabelAuxDecoder = colorSetting.getColor(); + repaint(); + break; + case CHANNEL_STATE_LABEL_DECODER: + mColorLabelDecoder = colorSetting.getColor(); + repaint(); + break; + case CHANNEL_STATE_LABEL_DETAILS: + mColorLabelDetails = colorSetting.getColor(); + repaint(); + break; + case CHANNEL_STATE_SELECTED_CHANNEL: + mColorChannelSelected = colorSetting.getColor(); + repaint(); + break; + } + } + } + + @Override + public void settingDeleted( Setting setting ) {} +} diff --git a/src/controller/state/ChannelStateSelectionListener.java b/src/controller/state/ChannelStateSelectionListener.java new file mode 100644 index 00000000..2e4c611a --- /dev/null +++ b/src/controller/state/ChannelStateSelectionListener.java @@ -0,0 +1,24 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package controller.state; + + +public interface ChannelStateSelectionListener +{ + public void selected( ChannelState channelState ); +} diff --git a/src/controller/system/System.java b/src/controller/system/System.java new file mode 100644 index 00000000..e88432f9 --- /dev/null +++ b/src/controller/system/System.java @@ -0,0 +1,85 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package controller.system; + +import java.util.ArrayList; + +import javax.xml.bind.annotation.XmlAttribute; +import javax.xml.bind.annotation.XmlRootElement; +import javax.xml.bind.annotation.XmlSeeAlso; + +import controller.site.Site; + +@XmlSeeAlso( { Site.class } ) +@XmlRootElement( name = "system" ) +public class System +{ + private String mName; + private ArrayList mSite = new ArrayList(); + + public System() + { + this( "New System" ); + } + + public System( String name ) + { + mName = name; + } + + @XmlAttribute + public String getName() + { + return mName; + } + + public void setName( String name ) + { + mName = name; + } + + public ArrayList getSite() + { + return mSite; + } + + public void setSite( ArrayList sites ) + { + mSite = sites; + } + + public void addSite( Site site ) + { + mSite.add( site ); + } + + public void removeSite( Site site ) + { + mSite.remove( site ); + } + + public void clearSites() + { + mSite.clear(); + } + + public String toString() + { + return mName; + } +} diff --git a/src/controller/system/SystemEditor.java b/src/controller/system/SystemEditor.java new file mode 100644 index 00000000..f9c4e8b4 --- /dev/null +++ b/src/controller/system/SystemEditor.java @@ -0,0 +1,101 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package controller.system; + +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; + +import javax.swing.BorderFactory; +import javax.swing.JButton; +import javax.swing.JLabel; +import javax.swing.JOptionPane; +import javax.swing.JPanel; +import javax.swing.JTextField; +import javax.swing.tree.TreePath; + +import net.miginfocom.swing.MigLayout; + +public class SystemEditor extends JPanel implements ActionListener +{ + private static final long serialVersionUID = 1L; + private SystemNode mSystemNode; + + private JLabel mLabelSystem; + private JTextField mTextSystem; + + public SystemEditor( SystemNode system ) + { + mSystemNode = system; + initGUI(); + } + + private void initGUI() + { + setLayout( new MigLayout() ); + + setBorder( BorderFactory.createTitledBorder( "System" ) ); + + mLabelSystem = new JLabel( "Name:" ); + add( mLabelSystem, "align right" ); + + mTextSystem = new JTextField( mSystemNode.getSystem().getName() ); + add( mTextSystem, "grow, wrap" ); + + JButton btnSave = new JButton( "Save" ); + btnSave.addActionListener( SystemEditor.this ); + add( btnSave ); + + JButton btnReset = new JButton( "Reset" ); + btnReset.addActionListener( SystemEditor.this ); + add( btnReset, "wrap" ); + } + + @Override + public void actionPerformed( ActionEvent e ) + { + String command = e.getActionCommand(); + + if( command.contentEquals( "Save" ) ) + { + boolean expanded = mSystemNode.getModel().getTree() + .isExpanded( new TreePath( mSystemNode ) ); + + String name = mTextSystem.getText(); + + if( name != null ) + { + mSystemNode.getSystem().setName( name ); + mSystemNode.save(); + mSystemNode.show(); + + if( expanded ) + { + mSystemNode.getModel().getTree().expandPath( new TreePath( mSystemNode ) ); + } + } + else + { + JOptionPane.showMessageDialog( SystemEditor.this, "Please enter a system name" ); + } + } + else if( command.contentEquals( "Reset" ) ) + { + mTextSystem.setText( mSystemNode.getSystem().getName() ); + } + } +} diff --git a/src/controller/system/SystemList.java b/src/controller/system/SystemList.java new file mode 100644 index 00000000..91fcacec --- /dev/null +++ b/src/controller/system/SystemList.java @@ -0,0 +1,60 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package controller.system; + +import java.util.ArrayList; + +import javax.xml.bind.annotation.XmlRootElement; +import javax.xml.bind.annotation.XmlSeeAlso; + +@XmlSeeAlso( { controller.system.System.class } ) + +@XmlRootElement( name = "system_list" ) +public class SystemList +{ + private ArrayList mSystem = new ArrayList(); + + public SystemList() + { + } + + public ArrayList getSystem() + { + return mSystem; + } + + public void setSystem( ArrayList systems ) + { + mSystem = systems; + } + + public void addSystem( System system ) + { + mSystem.add( system ); + } + + public void removeSystem( System system ) + { + mSystem.remove( system ); + } + + public void clearSystems() + { + mSystem.clear(); + } +} diff --git a/src/controller/system/SystemListNode.java b/src/controller/system/SystemListNode.java new file mode 100644 index 00000000..4629133d --- /dev/null +++ b/src/controller/system/SystemListNode.java @@ -0,0 +1,85 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package controller.system; + +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; + +import javax.swing.JMenuItem; +import javax.swing.JPopupMenu; + +import controller.ConfigurableNode; + +public class SystemListNode extends ConfigurableNode +{ + private static final long serialVersionUID = 1L; + + public SystemListNode( SystemList list ) + { + super( list ); + } + + public SystemList getSystemList() + { + return (SystemList)getUserObject(); + } + + public void init() + { + for( System system: getSystemList().getSystem() ) + { + SystemNode node = new SystemNode( system ); + + getModel().addNode( node, SystemListNode.this, getChildCount() ); + + node.init(); + } + + sort(); + } + + public String toString() + { + return "Systems [" + this.getChildCount() + "]"; + } + + public JPopupMenu getContextMenu() + { + JPopupMenu retVal = new JPopupMenu(); + + JMenuItem addSystemItem = new JMenuItem( "Add System" ); + addSystemItem.addActionListener( new ActionListener() + { + @Override + public void actionPerformed( ActionEvent e ) + { + System system = new System(); + + getSystemList().addSystem( system ); + + getModel().addNode( new SystemNode( system ), + SystemListNode.this, + SystemListNode.this.getChildCount() ); + } + } ); + + retVal.add( addSystemItem ); + + return retVal; + } +} diff --git a/src/controller/system/SystemNode.java b/src/controller/system/SystemNode.java new file mode 100644 index 00000000..7452c0cd --- /dev/null +++ b/src/controller/system/SystemNode.java @@ -0,0 +1,165 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package controller.system; + + +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.util.Enumeration; + +import javax.swing.JMenuItem; +import javax.swing.JPanel; +import javax.swing.JPopupMenu; +import javax.swing.JSeparator; +import javax.swing.tree.DefaultMutableTreeNode; + +import controller.ConfigurableNode; +import controller.site.Site; +import controller.site.SiteNode; + +public class SystemNode extends ConfigurableNode +{ + private static final long serialVersionUID = 1L; + + public SystemNode( System system ) + { + super( system ); + } + + public void init() + { + for( Site site: getSystem().getSite() ) + { + SiteNode node = new SiteNode( site ); + + getModel().insertNodeInto( node, SystemNode.this, getChildCount() ); + + node.init(); + } + + sort(); + } + + @Override + public String getIconPath() + { + String retVal = "images/system.png"; + + @SuppressWarnings( "unchecked" ) + Enumeration nodes = children(); + + while( nodes.hasMoreElements() ) + { + SiteNode child = (SiteNode)nodes.nextElement(); + + if( child.getIconPath().contains( "green.png" ) ) + { + return "images/system_green.png"; + } + else if( child.getIconPath().contains( "red.png" ) ) + { + retVal = "images/system_red.png"; + } + } + + return retVal; + } + + @Override + public JPanel getEditor() + { + return new SystemEditor( this ); + } + + public System getSystem() + { + return (System)getUserObject(); + } + + public String toString() + { + return getSystem().getName(); + } + + public void delete() + { + for( int x = 0; x < getChildCount(); x++ ) + { + ((SiteNode)getChildAt( x )).delete(); + } + + SystemListNode parent = (SystemListNode)getParent(); + + parent.getSystemList().removeSystem( getSystem() ); + + save(); + + getModel().removeNodeFromParent( SystemNode.this ); + + /* Show the first child, if one exists so that we don't have a + * collapsed list */ + if( parent.getChildCount() > 0 ) + { + ((ConfigurableNode)parent.getFirstChild()).show(); + } + } + + public JPopupMenu getContextMenu() + { + JPopupMenu retVal = new JPopupMenu(); + + JMenuItem addSiteItem = new JMenuItem( "Add Site" ); + addSiteItem.addActionListener( new ActionListener() + { + @Override + public void actionPerformed( ActionEvent e ) + { + Site site = new Site(); + getSystem().addSite( site ); + + SiteNode node = new SiteNode( site ); + + getModel().addNode( node, + SystemNode.this, + SystemNode.this.getChildCount() ); + + sort(); + + node.show(); + } + } ); + + retVal.add( addSiteItem ); + + retVal.add( new JSeparator() ); + + JMenuItem deleteItem = new JMenuItem( "Delete" ); + deleteItem.addActionListener( new ActionListener() + { + @Override + public void actionPerformed( ActionEvent e ) + { + delete(); + } + } ); + + retVal.add( deleteItem ); + + return retVal; + } +} diff --git a/src/crc/CRC.java b/src/crc/CRC.java new file mode 100644 index 00000000..509081f2 --- /dev/null +++ b/src/crc/CRC.java @@ -0,0 +1,68 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package crc; + +public enum CRC +{ + PASSED ( "*", "Pass" ), + PASSED_INV ( "#", "Pass Invert" ), + FAILED_CRC ( "f", "Fail CRC" ), + FAILED_PARITY( "p", "Fail Parity" ), + CORRECTED ( "C", "Corrected" ), + UNKNOWN ( "-", "Unknown"); + + private String mAbbreviation; + private String mDisplayText; + + CRC( String abbreviation, String displayText ) + { + mAbbreviation = abbreviation; + mDisplayText = displayText; + } + + public String getAbbreviation() + { + return mAbbreviation; + } + + public String getDisplayText() + { + return mDisplayText; + } + + public static String format( CRC[] checks ) + { + StringBuilder sb = new StringBuilder(); + + for( int x = 0; x < checks.length; x++ ) + { + CRC check = checks[ x ]; + + if( check != null ) + { + sb.append( check.getAbbreviation() ); + } + else + { + sb.append( CRC.UNKNOWN.getAbbreviation() ); + } + } + + return sb.toString(); + } +} diff --git a/src/crc/CRCFleetsync.java b/src/crc/CRCFleetsync.java new file mode 100644 index 00000000..742bf682 --- /dev/null +++ b/src/crc/CRCFleetsync.java @@ -0,0 +1,207 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package crc; + +import java.util.BitSet; + +/** + * Fleetsync CRC checksum utility + * + * Fleetsync message blocks are 64 bits in length as follows: + * 0 - 47: message bits + * 48 - 62: CRC-15 check bits + * 63: even parity bit + * + * Fleetsync uses a CRC-15 (0x6815) with an initial fill of 0x0001, coupled with + * an even parity checkbit. + * + * CRC-15 Generating Polynomial: x15 + x14 + x13 + x11 + x4 + x2 + 1 (0x6815) + */ +public class CRCFleetsync +{ + private static short[] sCHECKSUMS = new short[] { + 0x740A, // 111010000001010 Bit 0 + 0x3A05, // 011101000000101 Bit 1 + 0x6908, // 110100100001000 Bit 2 + 0x3484, // 011010010000100 Bit 3 + 0x1A42, // 001101001000010 Bit 4 + 0x0D21, // 000110100100001 Bit 5 + 0x729A, // 111001010011010 Bit 6 + 0x394D, // 011100101001101 Bit 7 + 0x68AC, // 110100010101100 Bit 8 + 0x3456, // 011010001010110 Bit 9 + 0x1A2B, // 001101000101011 Bit 10 + 0x791F, // 111100100011111 Bit 11 + 0x4885, // 100100010000101 Bit 12 + 0x5048, // 101000001001000 Bit 13 + 0x2824, // 010100000100100 Bit 14 + 0x1412, // 001010000010010 Bit 15 + 0x0A09, // 000101000001001 Bit 16 + 0x710E, // 111000100001110 Bit 17 + 0x3887, // 011100010000111 Bit 18 + 0x6849, // 110100001001001 Bit 19 + 0x402E, // 100000000101110 Bit 20 + 0x2017, // 010000000010111 Bit 21 + 0x6401, // 110010000000001 Bit 22 + 0x460A, // 100011000001010 Bit 23 + 0x2305, // 010001100000101 Bit 24 + 0x6588, // 110010110001000 Bit 25 + 0x32C4, // 011001011000100 Bit 26 + 0x1962, // 001100101100010 Bit 27 + 0x0CB1, // 000110010110001 Bit 28 + 0x7252, // 111001001010010 Bit 29 + 0x3929, // 011100100101001 Bit 30 + 0x689E, // 110100010011110 Bit 31 + 0x344F, // 011010001001111 Bit 32 + 0x6E2D, // 110111000101101 Bit 33 + 0x431C, // 100001100011100 Bit 34 + 0x218E, // 010000110001110 Bit 35 + 0x10C7, // 001000011000111 Bit 36 + 0x7C69, // 111110001101001 Bit 37 + 0x4A3E, // 100101000111110 Bit 38 + 0x251F, // 010010100011111 Bit 39 + 0x6685, // 110011010000101 Bit 40 + 0x4748, // 100011101001000 Bit 41 + 0x23A4, // 010001110100100 Bit 42 + 0x11D2, // 001000111010010 Bit 43 + 0x08E9, // 000100011101001 Bit 44 + 0x707E, // 111000001111110 Bit 45 + 0x383F, // 011100000111111 Bit 46 + 0x6815, // 110100000010101 Bit 47 + + //Single bit errors in the CRC Checksum + 0x4000, //Bit 48 + 0x2000, //Bit 49 + 0x1000, //Bit 50 + 0x0800, //Bit 51 + 0x0400, //Bit 52 + 0x0200, //Bit 53 + 0x0100, //Bit 54 + 0x0080, //Bit 55 + 0x0040, //Bit 56 + 0x0020, //Bit 57 + 0x0010, //Bit 58 + 0x0008, //Bit 59 + 0x0004, //Bit 60 + 0x0002, //Bit 61 + 0x0001 //Bit 62 + }; + + /** + * Determines if message bits 0 - 47 pass the Fleetsync CRC checksum + * contained in bits 48 - 63, using a lookup table of CRC checksum values + * derived from the CRC-15 value, and verifies the message has even parity + */ + public static CRC check( BitSet msg ) + { + CRC crc = CRC.UNKNOWN; + + int calculated = 1; //Starting value + + //Check even parity + if( msg.cardinality() % 2 == 0 ) + { + //Iterate bits that are set and XOR running checksum with lookup value + for (int i = msg.nextSetBit( 0 ); i >= 0 && i < 48; i = msg.nextSetBit( i+1 ) ) + { + calculated ^= sCHECKSUMS[ i ]; + } + + if( calculated == getChecksum( msg ) ) + { + crc = CRC.PASSED; + } + else + { + crc = CRC.FAILED_CRC; + } + } + else + { + crc = CRC.FAILED_PARITY; + } + + return crc; + } + + /** + * Returns the integer value of the 15 bit crc checksum + */ + public static int getChecksum( BitSet msg ) + { + int retVal = 0; + + for( int x = 0; x < 15; x++ ) + { + if( msg.get( x + 48 ) ) + { + retVal += 1<<( 14 - x ); + } + } + + return retVal; + } + + /** + * Determines the errant bit positions and returns them in an array of + * integer bit positions. + * + * Note: currently only detects single-bit errors + * + * @param msg to be checked for errors + * @return - array of integer positions of bits that need flipped + */ + public static int[] findBitErrors( BitSet msg ) + { + int[] retVal = null; + + int checksum = getChecksum( msg ); + + //Remove the initial fill value (1) + checksum ^= 1; + + //Iterate set message bits, removing their respective checksum value + //from the transmitted checksum, to arrive at the remainder + for (int i = msg.nextSetBit( 0 ); i >= 0 && i < 48; i = msg.nextSetBit( i+1 ) ) + { + checksum ^= sCHECKSUMS[ i ]; + } + + //If at this point the checksum is 0, then we have a parity bit error + if( checksum == 0 ) + { + retVal = new int[ 1 ]; + retVal[ 0 ] = 63; + } + //Otherwise, try to lookup the syndrome for a single bit error + else + { + for( int x = 0; x < 63; x++ ) + { + if( checksum == sCHECKSUMS[ x ] ) + { + //return this bit position + retVal = new int[ 1 ]; + retVal[ 0 ] = x; + } + } + } + + return retVal; + } +} diff --git a/src/crc/CRCLTR.java b/src/crc/CRCLTR.java new file mode 100644 index 00000000..34bc07da --- /dev/null +++ b/src/crc/CRCLTR.java @@ -0,0 +1,185 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package crc; + +import java.util.BitSet; + +import message.MessageDirection; + +/** + * LTR CRC checksum utility + * + * LTR message blocks are 41 bits in length as follows: + * 0 - 9: Sync + * 10 - 34: message bits + * 35 - 41: CRC-7 check bits + * + * LTR uses a CRC-7 (0xFD) with an initial fill of 0x00. + * + * CRC-7 Generating Polynomial: x7 + x6 + x5 + x4 + x3 + x2 + 1 (0xFD) + */ +public class CRCLTR +{ + private static byte[] sCHECKSUMS = new byte[] + { + 0x38, //Area + 0x1C, //Channel 4 + 0x0E, //Channel 3 + 0x46, //Channel 2 + 0x23, //Channel 1 + 0x51, //Channel 0 + 0x68, //Home 4 + 0x75, //Home 3 + 0x7A, //Home 2 + 0x3D, //Home 1 + 0x1F, //Home 0 + 0x4F, //Group 7 + 0x26, //Group 6 + 0x52, //Group 5 + 0x29, //Group 4 + 0x15, //Group 3 + 0x0B, //Group 2 + 0x45, //Group 1 + 0x62, //Group 0 + 0x31, //Free 4 + 0x19, //Free 3 + 0x0D, //Free 2 + 0x07, //Free 1 + 0x43 //Free 0 + }; + + /** + * Determines if message bits 10 - 34 pass the LTR CRC checksum + * contained in bits 35 - 41, using a lookup table of CRC checksum values + * derived from the CRC-7 value. + */ + public static CRC check( BitSet msg, MessageDirection direction ) + { + CRC crc = CRC.UNKNOWN; + + int calculated = getCalculatedChecksum( msg ); + int transmitted = getTransmittedChecksum( msg ); + + /* Normal CRC only for Outbound Status Word */ + if( direction == MessageDirection.OSW ) + { + if( calculated == transmitted ) + { + return CRC.PASSED; + } + } + /* Normal or Inverted CRC for Inbound Status Word */ + else + { + if( ( calculated ^ 127 ) == transmitted ) + { + return CRC.PASSED_INV; + } + else if( calculated == transmitted ) + { + return CRC.PASSED; + } + } + + return CRC.FAILED_CRC; + } + + public static byte[] getChecks() + { + return sCHECKSUMS; + } + + public static int getCalculatedChecksum( BitSet msg ) + { + int calculated = 0; + + //Iterate bits that are set and XOR running checksum with lookup value + for (int i = msg.nextSetBit( 9 ); i >= 9 && i < 33; i = msg.nextSetBit( i+1 ) ) + { + calculated ^= sCHECKSUMS[ i - 9 ]; + } + + return calculated; + } + + /** + * Returns the integer value of the 7 bit crc checksum + */ + public static int getTransmittedChecksum( BitSet msg ) + { + int retVal = 0; + + for( int x = 0; x < 7; x++ ) + { + if( msg.get( x + 33 ) ) + { + retVal += 1<<( 6 - x ); + } + } + + return retVal; + } + + /** + * Determines the errant bit positions and returns them in an array of + * integer bit positions. + * + * Note: currently only detects single-bit errors + * + * @param msg to be checked for errors + * @return - array of integer positions of bits that need flipped + */ + public static int[] findBitErrors( BitSet msg ) + { + int[] retVal = null; + + int checksum = getTransmittedChecksum( msg ); + + //Remove the initial fill value (1) + checksum ^= 1; + + //Iterate set message bits, removing their respective checksum value + //from the transmitted checksum, to arrive at the remainder + for (int i = msg.nextSetBit( 0 ); i >= 0 && i < 47; i = msg.nextSetBit( i+1 ) ) + { + checksum ^= sCHECKSUMS[ i ]; + } + + //If at this point the checksum is 0, then the errant bit is the parity + //bit + if( checksum == 0 ) + { + msg.flip( 62 ); + } + //Otherwise, try to lookup the syndrome for a single bit error + else + { + for( int x = 0; x < 47; x++ ) + { + if( checksum == sCHECKSUMS[ x ] ) + { + //return this bit position + retVal = new int[ 1 ]; + retVal[ 0 ] = x; + } + } + } + + return retVal; + } +} diff --git a/src/crc/CRCPassport.java b/src/crc/CRCPassport.java new file mode 100644 index 00000000..d5bb9dee --- /dev/null +++ b/src/crc/CRCPassport.java @@ -0,0 +1,215 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package crc; + +import java.util.BitSet; + +import bits.BitSetBuffer; + +/** + * Passport CRC checksum utility + * + * Passport message blocks are 68 bits in length as follows: + * 0 - 8: Sync + * 9 - 59: message bits + * 60 - 67: CRC-7 check bits plus 1 parity + * + * Passport uses a CRC-7 (0x44) with an initial fill of 0x44 plus parity set. + * + * CRC-7 Generating Polynomial: x6 + x2 + 1 (0x44) + */ +public class CRCPassport +{ + private static final byte sFILL_00 = (byte)0x00; + + private static byte[] sCHECKSUMS = new byte[] + { + (byte)0x6E, //DCC 1 + (byte)0xBF, //DCC 0 + (byte)0xD6, //LCN 10 + (byte)0xE3, //LCN 9 + (byte)0xF8, //LCN 8 + (byte)0x7C, //LCN 7 + (byte)0x3E, //LCN 6 + (byte)0x97, //LCN 5 + (byte)0xC2, //LCN 4 + (byte)0xE9, //LCN 3 + (byte)0x75, //LCN 2 + (byte)0x3B, //LCN 1 + (byte)0x94, //LCN 0 + (byte)0x4A, //SITE 6 + (byte)0xAD, //SITE 5 + (byte)0x57, //SITE 4 + (byte)0xA2, //SITE 3 + (byte)0xD9, //SITE 2 + (byte)0x6D, //SITE 1 + (byte)0x37, //SITE 0 + (byte)0x92, //GROUP 15 + (byte)0xC1, //GROUP 14 + (byte)0x61, //GROUP 13 + (byte)0x31, //GROUP 12 + (byte)0x19, //GROUP 11 + (byte)0x0D, //GROUP 10 + (byte)0x07, //GROUP 9 + (byte)0x8A, //GROUP 8 + (byte)0xCD, //GROUP 7 + (byte)0x67, //GROUP 6 + (byte)0xBA, //GROUP 5 + (byte)0xD5, //GROUP 4 + (byte)0x6B, //GROUP 3 + (byte)0xBC, //GROUP 2 + (byte)0x5E, //GROUP 1 + (byte)0xA7, //GROUP 0 + (byte)0xDA, //TYPE 3 + (byte)0xE5, //TYPE 2 + (byte)0x73, //TYPE 1 + (byte)0xB0, //TYPE 0 + (byte)0x58, //FREE 10 + (byte)0x2C, //FREE 9 + (byte)0x16, //FREE 8 + (byte)0x83, //FREE 7 + (byte)0xC8, //FREE 6 + (byte)0x64, //FREE 5 + (byte)0x32, //FREE 4 + (byte)0x91, //FREE 3 + (byte)0x49, //FREE 2 + (byte)0x25, //FREE 1 + (byte)0x13 //FREE 0 + }; + + /** + * Determines if message bits 9 - 59 pass the CRC checksum + * contained in bits 60 - 68, using a lookup table of CRC checksum values + * derived from the CRC-7 value and the final parity bit + */ + public static CRC check( BitSet msg ) + { + CRC crc = CRC.UNKNOWN; + + byte calculated = 0x0; //Starting value for an OSW + + //Iterate bits that are set and XOR running checksum with lookup value + for (int i = msg.nextSetBit( 9 ); i >= 9 && i <= 59; i = msg.nextSetBit( i+1 ) ) + { + calculated ^= sCHECKSUMS[ i - 9 ]; + } + + //Apply the message checksum to derive the residual + calculated ^= getChecksum( msg ); + + switch( (byte)calculated ) + { + case sFILL_00: + crc = CRC.PASSED; + break; + default: + crc = CRC.FAILED_CRC; + } + + return crc; + } + + public static byte getResidual( BitSet msg ) + { + byte calculated = 0x0; //Initial fill of zero + + //Iterate bits that are set and XOR running checksum with lookup value + for (int i = msg.nextSetBit( 9 ); i >= 9 && i <= 59; i = msg.nextSetBit( i+1 ) ) + { + calculated ^= sCHECKSUMS[ i - 9 ]; + } + + calculated ^= getChecksum( msg ); + + return calculated; + } + + public static byte[] getChecks() + { + return sCHECKSUMS; + } + + /** + * Returns the integer value of the 7 bit crc checksum + */ + public static byte getChecksum( BitSet msg ) + { + byte retVal = 0x0; + + for( int x = 0; x < 8; x++ ) + { + if( msg.get( x + 60 ) ) + { + retVal ^= (byte)( 1<<( 7 - x ) ); + } + } + + return retVal; + } + + /** + * Initial take on error correction. The majority of the errors detected + * are in the final 2 bits, owing to the forward looking soft bit detection + * used in the decoder. For right now, correct any messages where the + * residual indicates that the final 1 or 2 bits are bad. + */ + public static BitSetBuffer correct( BitSetBuffer msg ) + { + int residual = (int)( 0xFF & getResidual( msg ) ); + + switch( residual ) + { + case 1: + case 136: + msg.flip( 67 ); + break; + case 3: + case 138: + msg.flip( 66 ); + msg.flip( 67 ); + break; + case 2: + case 139: + msg.flip( 66 ); + break; + case 140: + msg.flip( 65 ); + msg.flip( 67 ); + break; + case 153: + msg.flip( 67 ); + break; + case 142: + msg.flip( 65 ); + msg.flip( 66 ); + msg.flip( 67 ); + break; + } + + return msg; + } + + /** + * Performs an even parity check on the bitset. If the number of bits that + * are set is an even number, then it passes the even parity check + */ + public static boolean isEvenParity( BitSetBuffer bits ) + { + return ( bits.cardinality() % 2 == 0 ); + } +} diff --git a/src/crc/CRCUtil.java b/src/crc/CRCUtil.java new file mode 100644 index 00000000..0c42a195 --- /dev/null +++ b/src/crc/CRCUtil.java @@ -0,0 +1,58 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package crc; + +import java.util.BitSet; + +public class CRCUtil +{ + public static BitSet check( BitSet originalMessage, + int messageLength, + BitSet polynomial, + int polynomialLength ) + { + //Make a copy of the message, since we're going to modify it + BitSet copy = originalMessage.get( 0, messageLength ); + + //Iterate through the message bits + for( int x = 0; x < messageLength; x++ ) + { + //If we're aligned against a starting 1 in the message, divide it + if( copy.get( 0 ) ) + { + copy.xor( polynomial ); + } + + //Left shift the message + copy = copy.get( 1, messageLength + polynomialLength - x ); + } + + return copy.get( 0, polynomialLength ); + } + + public static void log( String message ) + { + System.out.println( message ); + } + + public enum Status + { + PASS, FAIL, BITINVERTED, WORDINVERTED; + } + +} diff --git a/src/crc/Checksum.java b/src/crc/Checksum.java new file mode 100644 index 00000000..bf7d8b27 --- /dev/null +++ b/src/crc/Checksum.java @@ -0,0 +1,52 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package crc; + +import java.util.BitSet; + +/** + * Calculates a 7-bit checksum using an array of xor values from the + * ChecksumType class + */ +public class Checksum +{ + public static boolean validate7BitChecksum( BitSet message, + byte messageChecksum, + ChecksumType checksumType ) + { + return ( messageChecksum & get7BitChecksum( message, checksumType ) ) == 0; + } + + public static byte get7BitChecksum( BitSet message, + ChecksumType checksumType ) + { + byte checksum = 0x00; + + short[] checkValues = checksumType.getValues(); + + for( int x = 0; x < checkValues.length; x++ ) + { + if( message.get( x ) ) + { + checksum ^= checkValues[ x ]; + } + } + + return checksum; + } +} diff --git a/src/crc/ChecksumTest.java b/src/crc/ChecksumTest.java new file mode 100644 index 00000000..bd0bd1dc --- /dev/null +++ b/src/crc/ChecksumTest.java @@ -0,0 +1,96 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package crc; + +import java.util.BitSet; + +public class ChecksumTest +{ + + /** + * @param args + */ + public static void main( String[] args ) + { + log( "START!" ); + + String msg = "111000001000010010001110"; + log( msg + " check:" + String.format( "%02X", Checksum.get7BitChecksum( load( msg ), ChecksumType.LTR ) ) ); + + log( "DONE!" ); + } + + public static void log( String message ) + { + System.out.println( message ); + } + + public static String getString( BitSet bitset, int length ) + { + StringBuilder sb = new StringBuilder(); + for( int x = 0; x < length; x++ ) + { + if( bitset.get( x ) ) + { + sb.append( "1" ); + } + else + { + sb.append( "0" ); + } + } + + return sb.toString(); + } + + public static String getStringReverse( BitSet bitset, int length ) + { + StringBuilder sb = new StringBuilder(); + for( int x = length - 1; x > -1; x-- ) + { + if( bitset.get( x ) ) + { + sb.append( "1" ); + } + else + { + sb.append( "0" ); + } + } + + return sb.toString(); + } + + + public static BitSet load( String message ) + { + BitSet retVal = new BitSet(); + + for( int x = 0; x < message.length(); x++ ) + { + if( message.substring( x, x + 1 ).contentEquals( "1" ) ) + { + retVal.set( x ); + } + } + + return retVal; + } + + +} diff --git a/src/crc/ChecksumType.java b/src/crc/ChecksumType.java new file mode 100644 index 00000000..42d9d6d1 --- /dev/null +++ b/src/crc/ChecksumType.java @@ -0,0 +1,223 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package crc; + +public enum ChecksumType +{ + FLEETSYNC_ANI( new short[] + { + 0x0000, //Unknown Always One + 0x0000, //Status Msg Flag + 0x14C3, //Status Message 6 001010011000011 + 0x6908, //Status Message 5 110100100001000 + 0x3484, //Status Message 4 011010010000100 + 0x1A42, //Status Message 3 001101001000010 + 0x0D21, //Status Message 2 000110100100001 + 0x729A, //Status Message 1 111001010011010 + 0x394D, //Status Message 0 011100101001101 + 0x0000, //Type 7 + 0x0000, //Type 6 + 0x0000, //Type 5 + 0x0000, //Type 4 + 0x0000, //Type 3 + 0x0000, //Type 2 + 0x0000, //Type 1 + 0x0000, //Type 0 + 0x0000, //Fleet From 7 + 0x0000, //Fleet From 6 + 0x0000, //Fleet From 5 + 0x0000, //Fleet From 4 + 0x402E, //Fleet From 3 100000000101110 + 0x2017, //Fleet From 2 010000000010111 + 0x6401, //Fleet From 1 110010000000001 + 0x460A, //Fleet From 0 100011000001010 + 0x0000, //Ident From 11 + 0x0000, //Ident From 10 + 0x0000, //Ident From 9 + 0x0000, //Ident From 8 + 0x0CB1, //Ident From 7 000110010110001 + 0x7252, //Ident From 6 111001001010010 + 0x3929, //Ident From 5 011100100101001 + 0x689E, //Ident From 4 110100010011110 + 0x344F, //Ident From 3 011010001001111 + 0x6E2D, //Ident From 2 110111000101101 + 0x431C, //Ident From 1 100001100011100 + 0x218E, //Ident From 0 010000110001110 + 0x0000, //Ident To 11 + 0x0000, //Ident To 10 + 0x0000, //Ident To 9 + 0x0000, //Ident To 8 + 0x6685, //Ident To 7 110011010000101 + 0x4748, //Ident To 6 100011101001000 + 0x23A4, //Ident To 5 010001110100100 + 0x11D2, //Ident To 4 001000111010010 + 0x08E9, //Ident To 3 000100011101001 + 0x707E, //Ident To 2 111000001111110 + 0x383F, //Ident To 1 011100000111111 + 0x6815, //Ident To 0 110100000010101 + }), + + LTR( new short[] + { 0x0038, //Area + 0x001C, //Channel 4 + 0x000E, //Channel 3 + 0x0046, //Channel 2 + 0x0023, //Channel 1 + 0x0051, //Channel 0 + 0x0068, //Home 4 + 0x0075, //Home 3 + 0x007A, //Home 2 + 0x003D, //Home 1 + 0x001F, //Home 0 + 0x004F, //Group 7 + 0x0026, //Group 6 + 0x0052, //Group 5 + 0x0029, //Group 4 + 0x0015, //Group 3 + 0x000B, //Group 2 + 0x0045, //Group 1 + 0x0062, //Group 0 + 0x0031, //Free 4 + 0x0019, //Free 3 + 0x000D, //Free 2 + 0x0007, //Free 1 + 0x0043 //Free 0 + } ), + + PASSPORT( new short[] + { +// 0x6e, +// 0xbf, +// 0xd6, +// 0xe3, +// 0xf8, +// 0x7c, +// 0x3e, +// 0x97, +// 0xc2, +// 0xe9, +// 0x75, +// 0x3b, +// 0x94, +// 0x4a, +// 0xad, +// 0x57, +// 0xa2, +// 0xd9, +// 0x6d, +// 0x37, +// 0x92, +// 0xc1, +// 0x61, +// 0x31, +// 0x19, +// 0x0d, +// 0x07, +// 0x8a, +// 0xcd, +// 0x67, +// 0xba, +// 0xd5, +// 0x6b, // 0111110 +// 0xbc, // 1111100 +// 0x5e, // 1101001 +// 0xa7, // 1000011 +// 0xda, // 0010111 +// 0xe5, // 0101110 +// 0x73, // 1011100 +// 0xb0, // 0101001 +// 0x58, // 1010010 +// 0x2c, // 0110101 Bit 43 +// 0x16, // 1101010 +// 0x83, // 1000101 Bit 45 +// 0xc8, // 0011011 Bit 46 +// 0x64, // 0110110 +// 0x32, // 1101100 +// 0x91, // 1001001 +// 0x49, +// 0x25, +// 0x13, + } ), + + MULTINET( new short[] + { +// 0x96, // 0010110 +// 0xAC, // 0101100 +// 0xd8, // 1011000 +// 0x21, // 0100001 +// 0x42, // 1000010 +// 0x95, // 0010101 +// 0xaa, // 0101010 +// 0xd4, // 1010100 +// 0x39, // 0111001 +// 0x72, // 1110010 +// 0xf5, // 1110101 Bit 10 +// 0x7b, // 1111011 +// 0xe7, // 1100111 +// 0x5f, // 1011111 +// 0xaf, // 0101111 +// 0xde, // 1011110 +// 0x2d, // 0101101 +// 0x5a, // 1011010 +// 0xa5, // 0100101 +// 0xca, // 1001010 +// 0x05, // 0000101 +// 0x0a, // 0001010 +// 0x14, // 0010100 +// 0x28, // 0101000 +// 0x50, // 1010000 +// 0xb1, // 0110001 +// 0xe2, // 1100010 +// 0x55, // 1010101 +// 0xbb, // 0111011 +// 0xf6, // 1110110 +// 0x7d, // 1111101 +// 0xeb, // 1101011 +// 0x47, // 1000111 +// 0x9f, // 0011111 Bit 33 +// 0xbe, // 0111110 +// 0xfc, // 1111100 +// 0x69, // 1101001 +// 0xc3, // 1000011 +// 0x17, // 0010111 +// 0x2e, // 0101110 +// 0x5c, // 1011100 +// 0xA9, // 0101001 +// 0xD2, // 1010010 +// 0x35, // 0110101 Bit 43 +// 0x6A, // 1101010 +// 0xC5, // 1000101 Bit 45 +// 0x1B, // 0011011 Bit 46 +// 0x36, // 0110110 +// 0x6C, // 1101100 +// 0xC9 // 1001001 + }); + + private short[] mValues; + + ChecksumType( short[] values ) + { + mValues = values; + } + + public short[] getValues() + { + return mValues; + } + +} diff --git a/src/decode/AuxDecodeComponentEditor.java b/src/decode/AuxDecodeComponentEditor.java new file mode 100644 index 00000000..16b37ac6 --- /dev/null +++ b/src/decode/AuxDecodeComponentEditor.java @@ -0,0 +1,110 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package decode; + +import java.util.Collections; +import java.util.HashMap; +import java.util.List; + +import javax.swing.BorderFactory; +import javax.swing.JCheckBox; +import javax.swing.SwingUtilities; + +import controller.channel.AbstractChannelEditor; +import controller.channel.ChannelNode; +import decode.config.AuxDecodeConfiguration; + +public class AuxDecodeComponentEditor extends AbstractChannelEditor +{ + private static final long serialVersionUID = 1L; + + private HashMap mControls = + new HashMap(); + + public AuxDecodeComponentEditor( ChannelNode channelNode ) + { + super( channelNode ); + + List decoders = DecoderType.getAuxDecoders(); + + Collections.sort( decoders ); + + for( DecoderType decoder: decoders ) + { + AuxDecoderCheckBox control = new AuxDecoderCheckBox( decoder ); + + if( getConfig().getAuxDecoders().contains( decoder ) ) + { + control.setSelected( true ); + } + + add( control, "wrap" ); + + mControls.put( decoder, control ); + } + } + + public AuxDecodeConfiguration getConfig() + { + return getChannelNode().getChannel().getAuxDecodeConfiguration(); + } + + @Override + public void save() + { + getConfig().clearAuxDecoders(); + + for( DecoderType decoder: mControls.keySet() ) + { + if( mControls.get( decoder ) .isSelected() ) + { + getConfig().addAuxDecoder( decoder ); + } + } + } + + @Override + public void reset() + { + SwingUtilities.invokeLater(new Runnable() + { + @Override + public void run() + { + for( DecoderType decoder: mControls.keySet() ) + { + mControls.get( decoder ).setSelected( + getConfig().getAuxDecoders().contains( decoder ) ); + } + } + }); + + } + + public class AuxDecoderCheckBox extends JCheckBox + { + private DecoderType mDecoderType; + + public AuxDecoderCheckBox( DecoderType decoder ) + { + super( decoder.getDisplayString() ); + + mDecoderType = decoder; + } + } +} diff --git a/src/decode/DecodeComponentEditor.java b/src/decode/DecodeComponentEditor.java new file mode 100644 index 00000000..114dce05 --- /dev/null +++ b/src/decode/DecodeComponentEditor.java @@ -0,0 +1,134 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package decode; + + +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; + +import javax.swing.BorderFactory; +import javax.swing.DefaultComboBoxModel; +import javax.swing.JComboBox; +import javax.swing.SwingUtilities; + +import controller.channel.AbstractChannelEditor; +import controller.channel.ChannelNode; +import decode.config.DecodeConfigFactory; +import decode.config.DecodeConfiguration; + +public class DecodeComponentEditor extends AbstractChannelEditor +{ + private static final long serialVersionUID = 1L; + + private JComboBox mComboDecoders; + + private DecodeEditor mEditor; + + public DecodeComponentEditor( ChannelNode channelNode ) + { + super( channelNode ); + + /** + * ComboBox: Decoders + */ + mComboDecoders = new JComboBox(); + + DefaultComboBoxModel model = + new DefaultComboBoxModel(); + + for( DecoderType type: DecoderType.getAvailableDecoders() ) + { + model.addElement( type ); + } + + mComboDecoders.setModel( model ); + + mComboDecoders.addActionListener( new ActionListener() + { + @Override + public void actionPerformed( ActionEvent e ) + { + DecoderType selected = mComboDecoders + .getItemAt( mComboDecoders.getSelectedIndex() ); + + if( selected != null ) + { + DecodeConfiguration config; + + if( mChannelNode.getChannel() + .getDecodeConfiguration().getDecoderType() == + selected ) + { + config = mChannelNode.getChannel() + .getDecodeConfiguration(); + } + else + { + config = DecodeConfigFactory + .getDecodeConfiguration( selected ); + } + + //Remove the existing one + if( mEditor != null ) + { + remove( mEditor ); + } + + //Change to the new one + mEditor = DecodeEditorFactory.getPanel( config, mChannelNode ); + + add( mEditor, "span 2" ); + + revalidate(); + repaint(); + } + } + }); + add( mComboDecoders, "wrap" ); + + reset(); + } + + public void reset() + { + final DecoderType decoder = mChannelNode.getChannel() + .getDecodeConfiguration().getDecoderType(); + + SwingUtilities.invokeLater(new Runnable() + { + @Override + public void run() { + mComboDecoders.setSelectedItem( decoder ); + + mComboDecoders.requestFocus(); + + mComboDecoders.requestFocusInWindow(); + } + }); + } + + @Override + public void save() + { + mEditor.save(); + + //The component that calls save will invoke the change broadcast + mChannelNode.getChannel() + .setDecodeConfiguration( mEditor.getConfig(), false ); + } +} diff --git a/src/decode/DecodeEditor.java b/src/decode/DecodeEditor.java new file mode 100644 index 00000000..cdc1170c --- /dev/null +++ b/src/decode/DecodeEditor.java @@ -0,0 +1,52 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package decode; + +import controller.Editor; +import decode.config.DecodeConfiguration; + +public class DecodeEditor extends Editor +{ + private static final long serialVersionUID = 1L; + + protected DecodeConfiguration mConfig; + + public DecodeEditor( DecodeConfiguration config ) + { + mConfig = config; + } + + public DecodeConfiguration getConfig() + { + return mConfig; + } + + @Override + public void save() + { + // TODO Auto-generated method stub + + } + + @Override + public void reset() + { + // TODO Auto-generated method stub + + } +} diff --git a/src/decode/DecodeEditorFactory.java b/src/decode/DecodeEditorFactory.java new file mode 100644 index 00000000..ed95699a --- /dev/null +++ b/src/decode/DecodeEditorFactory.java @@ -0,0 +1,59 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package decode; + +import controller.channel.ChannelNode; +import decode.config.DecodeConfiguration; +import decode.ltrnet.LTRNetEditor; +import decode.ltrstandard.LTRStandardEditor; +import decode.mpt1327.MPT1327Editor; +import decode.nbfm.NBFMEditor; +import decode.passport.PassportEditor; + +public class DecodeEditorFactory +{ + public static DecodeEditor getPanel( DecodeConfiguration config, + ChannelNode channelNode ) + { + DecodeEditor configuredPanel; + + switch( config.getDecoderType() ) + { + case NBFM: + configuredPanel = new NBFMEditor( config ); + break; + case LTR_STANDARD: + configuredPanel = new LTRStandardEditor( config ); + break; + case LTR_NET: + configuredPanel = new LTRNetEditor( config ); + break; + case MPT1327: + configuredPanel = new MPT1327Editor( config, channelNode ); + break; + case PASSPORT: + configuredPanel = new PassportEditor( config ); + break; + default: + configuredPanel = new DecodeEditor( config ); + break; + } + + return configuredPanel; + } +} diff --git a/src/decode/Decoder.java b/src/decode/Decoder.java new file mode 100644 index 00000000..add1f52a --- /dev/null +++ b/src/decode/Decoder.java @@ -0,0 +1,148 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package decode; + +import java.util.ArrayList; +import java.util.List; + +import message.Message; +import sample.Broadcaster; +import sample.Listener; +import sample.complex.ComplexSample; +import source.Source.SampleType; +import eventlog.MessageEventLogger; + +public abstract class Decoder implements Listener +{ + private MessageEventLogger mEventLogger; + private Broadcaster mMessageBroadcaster = new Broadcaster(); + private Broadcaster mFloatBroadcaster = new Broadcaster(); + private Broadcaster mComplexBroadcaster = + new Broadcaster(); + + protected SampleType mSourceSampleType; + protected ArrayList mAuxiliaryDecoders = new ArrayList(); + + public Decoder( SampleType sampleType ) + { + mSourceSampleType = sampleType; + } + + /** + * Returns a float listener interface for connecting this decoder to a + * float stream provider + */ + public Listener getFloatReceiver() + { + return (Listener)mFloatBroadcaster; + } + + /** + * Returns a complex listener interface for connecting this decoder to a + * float stream provider + */ + public Listener getComplexReceiver() + { + return (Listener)mComplexBroadcaster; + } + + public abstract DecoderType getType(); + + public void addAuxiliaryDecoder( Decoder decoder ) + { + mAuxiliaryDecoders.add( decoder ); + mFloatBroadcaster.addListener( decoder.getFloatReceiver() ); + decoder.addMessageListener( this ); + } + + /** + * Cleanup method. Invoke this method after stop and before delete. + */ + public void dispose() + { + for( Decoder auxiliaryDecoder: mAuxiliaryDecoders ) + { + auxiliaryDecoder.dispose(); + } + + mAuxiliaryDecoders.clear(); + + mMessageBroadcaster.clear(); + mComplexBroadcaster.clear(); + mFloatBroadcaster.clear(); + } + + public List getAuxiliaryDecoders() + { + return mAuxiliaryDecoders; + } + + public void setEventLogger( MessageEventLogger eventLogger ) + { + mEventLogger = eventLogger; + } + + public MessageEventLogger getEventLogger() + { + return mEventLogger; + } + + @Override + public void receive( Message message ) + { + send( message ); + } + + public void send( Message message ) + { + if( mMessageBroadcaster != null ) + { + mMessageBroadcaster.receive( message ); + } + } + + public void addMessageListener( Listener listener ) + { + mMessageBroadcaster.addListener( listener ); + } + + public void removeMessageListener( Listener listener ) + { + mMessageBroadcaster.removeListener( listener ); + } + + public void addFloatListener( Listener listener ) + { + mFloatBroadcaster.addListener( listener ); + } + + public void removeFloatListener( Listener listener ) + { + mFloatBroadcaster.removeListener( listener ); + } + + public void addComplexListener( Listener listener ) + { + mComplexBroadcaster.addListener( listener ); + } + + public void removeComplexListener( Listener listener ) + { + mComplexBroadcaster.removeListener( listener ); + } +} diff --git a/src/decode/DecoderFactory.java b/src/decode/DecoderFactory.java new file mode 100644 index 00000000..402529b3 --- /dev/null +++ b/src/decode/DecoderFactory.java @@ -0,0 +1,181 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package decode; + +import source.Source.SampleType; +import alias.AliasList; +import controller.channel.ChannelMap; +import controller.channel.ProcessingChain; +import controller.state.ChannelState; +import decode.config.AuxDecodeConfiguration; +import decode.config.DecodeConfigLTRNet; +import decode.config.DecodeConfigLTRStandard; +import decode.config.DecodeConfigMPT1327; +import decode.config.DecodeConfigNBFM; +import decode.config.DecodeConfiguration; +import decode.fleetsync2.Fleetsync2Decoder; +import decode.fleetsync2.FleetsyncChannelState; +import decode.ltrnet.LTRNetChannelState; +import decode.ltrnet.LTRNetDecoder; +import decode.ltrstandard.LTRChannelState; +import decode.ltrstandard.LTRStandardDecoder; +import decode.mdc1200.MDCChannelState; +import decode.mdc1200.MDCDecoder; +import decode.mpt1327.MPT1327ChannelState; +import decode.mpt1327.MPT1327Decoder; +import decode.nbfm.NBFMChannelState; +import decode.nbfm.NBFMConventionalDecoder; +import decode.passport.PassportChannelState; +import decode.passport.PassportDecoder; + +public class DecoderFactory +{ + /** + * Returns a fully configured decoder with embedded auxiliary decoders + * @param channelConfig + * @return + */ + public static Decoder getDecoder( ProcessingChain chain, + SampleType sampleType, + AliasList aliasList ) + { + Decoder retVal = null; + + DecodeConfiguration config = + chain.getChannel().getDecodeConfiguration(); + + + switch( config.getDecoderType() ) + { + case NBFM: + retVal = new NBFMConventionalDecoder( sampleType ); + break; + case LTR_STANDARD: + DecodeConfigLTRStandard ltrStandardConfig = + (DecodeConfigLTRStandard)config; + + retVal = new LTRStandardDecoder( sampleType, aliasList, + ltrStandardConfig.getMessageDirection() ); + break; + case LTR_NET: + DecodeConfigLTRNet ltrNetConfig = (DecodeConfigLTRNet)config; + + retVal = new LTRNetDecoder( sampleType, aliasList, + ltrNetConfig.getMessageDirection() ); + break; + case MPT1327: + retVal = new MPT1327Decoder( sampleType, aliasList ); + break; + case PASSPORT: + retVal = new PassportDecoder( sampleType, aliasList ); + break; + default: + throw new IllegalArgumentException( + "Unknown decoder type [" + + config.getDecoderType().toString() + "]" ); + } + + if( retVal != null ) + { + AuxDecodeConfiguration auxConfig = + chain.getChannel().getAuxDecodeConfiguration(); + + for( DecoderType auxDecoder: auxConfig.getAuxDecoders() ) + { + switch( auxDecoder ) + { + case FLEETSYNC2: + retVal.addAuxiliaryDecoder( + new Fleetsync2Decoder( aliasList ) ); + break; + case MDC1200: + retVal.addAuxiliaryDecoder( + new MDCDecoder( aliasList ) ); + break; + } + } + } + + return retVal; + } + + public static ChannelState getChannelStateNew( ProcessingChain chain, + AliasList aliasList ) + { + ChannelState retVal = null; + + DecodeConfiguration decodeConfig = + chain.getChannel().getDecodeConfiguration(); + + switch( decodeConfig.getDecoderType() ) + { + case NBFM: + retVal = new NBFMChannelState( chain, aliasList ); + + DecodeConfigNBFM conv = + (DecodeConfigNBFM)decodeConfig; + + break; + case LTR_STANDARD: + retVal = new LTRChannelState( chain, aliasList ); + break; + case LTR_NET: + retVal = new LTRNetChannelState( chain, aliasList ); + break; + case MPT1327: + DecodeConfigMPT1327 mpt = (DecodeConfigMPT1327)decodeConfig; + + String channelMapName = mpt.getChannelMapName(); + + ChannelMap map = chain.getResourceManager().getPlaylistManager() + .getPlayist().getChannelMapList() + .getChannelMap( channelMapName ); + + retVal = new MPT1327ChannelState( chain, aliasList, map ); + break; + case PASSPORT: + retVal = new PassportChannelState( chain, aliasList ); + break; + default: + break; + } + + if( retVal != null ) + { + AuxDecodeConfiguration auxConfig = + chain.getChannel().getAuxDecodeConfiguration(); + + for( DecoderType auxDecoder: auxConfig.getAuxDecoders() ) + { + switch( auxDecoder ) + { + case FLEETSYNC2: + retVal.addAuxChannelState( + new FleetsyncChannelState( retVal ) ); + break; + case MDC1200: + retVal.addAuxChannelState( + new MDCChannelState( retVal ) ); + break; + } + } + } + + return retVal; + } +} diff --git a/src/decode/DecoderType.java b/src/decode/DecoderType.java new file mode 100644 index 00000000..ef192fc2 --- /dev/null +++ b/src/decode/DecoderType.java @@ -0,0 +1,110 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package decode; + +import java.util.ArrayList; +import java.util.EnumSet; + +/** + * Enumeration of decoder types + */ +public enum DecoderType +{ + FLEETSYNC2( "Fleetsync II", "Fsync2", "images/fm", 12500 ), + LTR_STANDARD( "LTR-Standard", "LTR", "images/ltr_standard", 10000 ), + LTR_NET( "LTR-Net", "LTR-Net", "images/ltr_net", 10000 ), + MDC1200( "MDC1200", "MDC1200", "images/fm", 12500 ), + MPT1327( "MPT1327", "MPT1327", "images/mpt1327", 12500 ), + NBFM( "NBFM", "NBFM", "images/fm", 12500 ), + PASSPORT( "Passport", "Passport", "images/passport", 12500 ); + + private String mDisplayString; + private String mShortDisplayString; + private String mIconPrefix; + private int mChannelBandwidth; + + DecoderType( String displayString, String shortDisplayString, + String iconPrefix, int bandwidth ) + { + mDisplayString = displayString; + mShortDisplayString = shortDisplayString; + mIconPrefix = iconPrefix; + mChannelBandwidth = bandwidth; + } + + /** + * Primary decoders + */ + public static EnumSet getAvailableDecoders() + { + return EnumSet.of( DecoderType.LTR_NET, + DecoderType.LTR_STANDARD, + DecoderType.MPT1327, + DecoderType.NBFM, + DecoderType.PASSPORT ); + } + + /** + * Decoders that can be used in the viewer application + */ + public static EnumSet getInstrumentableDecoders() + { + return EnumSet.of( DecoderType.FLEETSYNC2, + DecoderType.MDC1200, + DecoderType.MPT1327 ); + } + + /** + * Returns listing of auxiliary decoders. + */ + public static ArrayList getAuxDecoders() + { + ArrayList decoders = new ArrayList(); + + decoders.add( DecoderType.FLEETSYNC2 ); + decoders.add( DecoderType.MDC1200 ); + + return decoders; + } + + public String getDisplayString() + { + return mDisplayString; + } + + public String getShortDisplayString() + { + return mShortDisplayString; + } + + public String getIconPrefix() + { + return mIconPrefix; + } + + public int getChannelBandwidth() + { + return mChannelBandwidth; + } + + @Override + public String toString() + { + return mDisplayString; + } +} diff --git a/src/decode/config/AuxDecodeConfiguration.java b/src/decode/config/AuxDecodeConfiguration.java new file mode 100644 index 00000000..dcb31f40 --- /dev/null +++ b/src/decode/config/AuxDecodeConfiguration.java @@ -0,0 +1,60 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package decode.config; + +import java.util.ArrayList; + +import javax.xml.bind.annotation.XmlElement; + +import controller.config.Configuration; +import decode.DecoderType; + +public class AuxDecodeConfiguration extends Configuration +{ + ArrayList mAuxDecoders = new ArrayList(); + + public AuxDecodeConfiguration() + { + } + + @XmlElement( name="aux_decoder" ) + public ArrayList getAuxDecoders() + { + return mAuxDecoders; + } + + public void setAuxDecoders( ArrayList decoders ) + { + mAuxDecoders = decoders; + } + + public void addAuxDecoder( DecoderType decoder ) + { + mAuxDecoders.add( decoder ); + } + + public void removeAuxDecoder( DecoderType decoder ) + { + mAuxDecoders.remove( decoder ); + } + + public void clearAuxDecoders() + { + mAuxDecoders.clear(); + } +} diff --git a/src/decode/config/DecodeConfigFactory.java b/src/decode/config/DecodeConfigFactory.java new file mode 100644 index 00000000..723d5ffd --- /dev/null +++ b/src/decode/config/DecodeConfigFactory.java @@ -0,0 +1,58 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package decode.config; + +import decode.DecoderType; + +public class DecodeConfigFactory +{ + public static DecodeConfiguration getDefaultDecodeConfiguration() + { + return getDecodeConfiguration( DecoderType.NBFM ); + } + + public static DecodeConfiguration + getDecodeConfiguration( DecoderType decoder ) + { + DecodeConfiguration retVal; + + switch( decoder ) + { + case NBFM: + retVal = new DecodeConfigNBFM(); + break; + case LTR_STANDARD: + retVal = new DecodeConfigLTRStandard(); + break; + case LTR_NET: + retVal = new DecodeConfigLTRNet(); + break; + case MPT1327: + retVal = new DecodeConfigMPT1327(); + break; + case PASSPORT: + retVal = new DecodeConfigPassport(); + break; + default: + throw new IllegalArgumentException( "DecodeConfigFactory - " + + "unknown decoder type [" + decoder.toString() + "]" ); + } + + return retVal; + } +} diff --git a/src/decode/config/DecodeConfigLTRNet.java b/src/decode/config/DecodeConfigLTRNet.java new file mode 100644 index 00000000..6c62e6ff --- /dev/null +++ b/src/decode/config/DecodeConfigLTRNet.java @@ -0,0 +1,44 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package decode.config; + +import javax.xml.bind.annotation.XmlAttribute; + +import message.MessageDirection; +import decode.DecoderType; + +public class DecodeConfigLTRNet extends DecodeConfiguration +{ + private MessageDirection mMessageDirection = MessageDirection.OSW; + + public DecodeConfigLTRNet() + { + super( DecoderType.LTR_NET ); + } + + @XmlAttribute( name = "direction" ) + public MessageDirection getMessageDirection() + { + return mMessageDirection; + } + + public void setMessageDirection( MessageDirection direction ) + { + mMessageDirection = direction; + } +} diff --git a/src/decode/config/DecodeConfigLTRStandard.java b/src/decode/config/DecodeConfigLTRStandard.java new file mode 100644 index 00000000..62118f0f --- /dev/null +++ b/src/decode/config/DecodeConfigLTRStandard.java @@ -0,0 +1,44 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package decode.config; + +import javax.xml.bind.annotation.XmlAttribute; + +import message.MessageDirection; +import decode.DecoderType; + +public class DecodeConfigLTRStandard extends DecodeConfiguration +{ + private MessageDirection mMessageDirection = MessageDirection.OSW; + + public DecodeConfigLTRStandard() + { + super( DecoderType.LTR_STANDARD ); + } + + @XmlAttribute( name = "direction" ) + public MessageDirection getMessageDirection() + { + return mMessageDirection; + } + + public void setMessageDirection( MessageDirection direction ) + { + mMessageDirection = direction; + } +} diff --git a/src/decode/config/DecodeConfigMPT1327.java b/src/decode/config/DecodeConfigMPT1327.java new file mode 100644 index 00000000..2a5db097 --- /dev/null +++ b/src/decode/config/DecodeConfigMPT1327.java @@ -0,0 +1,43 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package decode.config; + +import javax.xml.bind.annotation.XmlElement; + +import decode.DecoderType; + +public class DecodeConfigMPT1327 extends DecodeConfiguration +{ + private String mChannelMapName; + + public DecodeConfigMPT1327() + { + super( DecoderType.MPT1327 ); + } + + @XmlElement + public String getChannelMapName() + { + return mChannelMapName; + } + + public void setChannelMapName( String name ) + { + mChannelMapName = name; + } +} diff --git a/src/decode/config/DecodeConfigNBFM.java b/src/decode/config/DecodeConfigNBFM.java new file mode 100644 index 00000000..625ffffc --- /dev/null +++ b/src/decode/config/DecodeConfigNBFM.java @@ -0,0 +1,28 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package decode.config; + +import decode.DecoderType; + +public class DecodeConfigNBFM extends DecodeConfiguration +{ + public DecodeConfigNBFM() + { + super( DecoderType.NBFM ); + } +} diff --git a/src/decode/config/DecodeConfigPassport.java b/src/decode/config/DecodeConfigPassport.java new file mode 100644 index 00000000..f6d8d9ae --- /dev/null +++ b/src/decode/config/DecodeConfigPassport.java @@ -0,0 +1,28 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package decode.config; + +import decode.DecoderType; + +public class DecodeConfigPassport extends DecodeConfiguration +{ + public DecodeConfigPassport() + { + super( DecoderType.PASSPORT ); + } +} diff --git a/src/decode/config/DecodeConfiguration.java b/src/decode/config/DecodeConfiguration.java new file mode 100644 index 00000000..8ede32d2 --- /dev/null +++ b/src/decode/config/DecodeConfiguration.java @@ -0,0 +1,50 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package decode.config; + +import javax.xml.bind.annotation.XmlRootElement; +import javax.xml.bind.annotation.XmlSeeAlso; + +import controller.config.Configuration; +import decode.DecoderType; + +@XmlSeeAlso( { DecodeConfigNBFM.class, + DecodeConfigLTRNet.class, + DecodeConfigLTRStandard.class, + DecodeConfigMPT1327.class, + DecodeConfigPassport.class } ) +@XmlRootElement( name = "decode_configuration" ) +public abstract class DecodeConfiguration extends Configuration +{ + private DecoderType mDecoderType = DecoderType.NBFM; + + public DecodeConfiguration() + { + this( DecoderType.NBFM ); + } + + public DecodeConfiguration( DecoderType type ) + { + mDecoderType = type; + } + + public DecoderType getDecoderType() + { + return mDecoderType; + } +} diff --git a/src/decode/fleetsync1/Fleetsync1Message.java b/src/decode/fleetsync1/Fleetsync1Message.java new file mode 100644 index 00000000..8e45d25b --- /dev/null +++ b/src/decode/fleetsync1/Fleetsync1Message.java @@ -0,0 +1,175 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package decode.fleetsync1; + +import java.text.SimpleDateFormat; +import java.util.Date; + +import map.Plottable; +import message.Message; +import alias.Alias; +import bits.BitSetBuffer; + +public class Fleetsync1Message extends Message +{ + private static SimpleDateFormat mSDF = new SimpleDateFormat( "yyyy-MM-dd HH:mm:ss" ); + + private BitSetBuffer mMessage; + + public Fleetsync1Message( BitSetBuffer message ) + { + mMessage = message; + } + + public boolean isValid() + { + return true; + } + + @Override + public String toString() + { + StringBuilder sb = new StringBuilder(); + + sb.append( mSDF.format( new Date( System.currentTimeMillis() ) ) ); + sb.append( " " ); + sb.append( "FS1 [" + mMessage.toString() + "]" ); + + return sb.toString(); + } + + /** + * Pads spaces onto the end of the value to make it 'places' long + */ + public String pad( String value, int places, String padCharacter ) + { + StringBuilder sb = new StringBuilder(); + + sb.append( value ); + + while( sb.length() < places ) + { + sb.append( padCharacter ); + } + + return sb.toString(); + } + + /** + * Pads an integer value with additional zeroes to make it decimalPlaces long + */ + public String format( int number, int decimalPlaces ) + { + StringBuilder sb = new StringBuilder(); + + int paddingRequired = decimalPlaces - ( String.valueOf( number ).length() ); + + for( int x = 0; x < paddingRequired; x++) + { + sb.append( "0" ); + } + + sb.append( number ); + + return sb.toString(); + } + + @SuppressWarnings( "unused" ) + private int getInt( int[] bits ) + { + int retVal = 0; + + for( int x = 0; x < bits.length; x++ ) + { + if( mMessage.get( bits[ x ] ) ) + { + retVal += 1< + ******************************************************************************/ +package decode.fleetsync1; + +import log.Log; +import message.Message; +import sample.Broadcaster; +import sample.Listener; +import bits.BitSetBuffer; + +public class Fleetsync1MessageProcessor implements Listener +{ + private Broadcaster mBroadcaster = new Broadcaster(); + + @Override + public void receive( BitSetBuffer buffer ) + { + Fleetsync1Message message = new Fleetsync1Message( buffer ); + + Log.info( message.toString() ); + + mBroadcaster.receive( message ); + } + + public void addMessageListener( Listener listener ) + { + mBroadcaster.addListener( listener ); + } + + public void removeMessageListener( Listener listener ) + { + mBroadcaster.removeListener( listener ); + } +} diff --git a/src/decode/fleetsync2/Fleetsync2Decoder.java b/src/decode/fleetsync2/Fleetsync2Decoder.java new file mode 100644 index 00000000..2e56ad9d --- /dev/null +++ b/src/decode/fleetsync2/Fleetsync2Decoder.java @@ -0,0 +1,184 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package decode.fleetsync2; + +import instrument.Instrumentable; +import instrument.tap.Tap; +import instrument.tap.stream.BinaryTap; +import instrument.tap.stream.FloatTap; + +import java.util.ArrayList; +import java.util.List; + +import sample.Listener; +import source.Source.SampleType; +import alias.AliasList; +import bits.MessageFramer; +import bits.SyncPattern; +import decode.Decoder; +import decode.DecoderType; +import dsp.filter.Filters; +import dsp.filter.FloatFIRFilter; +import dsp.filter.FloatHalfBandFilter; +import dsp.fsk.FSK2Decoder; +import dsp.fsk.FSK2Decoder.Output; + +/** + * Fleetsync II Decoder - 1200 baud 2FSK decoder that can process 48k sample rate + * complex or floating point samples and output fully framed Fleetsync II + * messages + */ +public class Fleetsync2Decoder extends Decoder implements Instrumentable +{ + /* Decimated sample rate ( 48,000 / 2 = 24,000 ) feeding the decoder */ + private static final int sDECIMATED_SAMPLE_RATE = 24000; + + /* Baud or Symbol Rate */ + private static final int sSYMBOL_RATE = 1200; + + /* Message length - 5 x REVS + 16 x SYNC + 8 x 64Bit Blocks */ + private static final int sMESSAGE_LENGTH = 537; + + /* Instrumentation Taps */ + private ArrayList mAvailableTaps; + private static final String INSTRUMENT_INPUT = + "Tap Point: Float Input"; + private static final String INSTRUMENT_BANDPASS_FILTER_TO_FSK2_DEMOD = + "Tap Point: Bandpass Filter > < FSK2 Decoder"; + private static final String INSTRUMENT_FSK2_DECODER_TO_MESSAGE_FRAMER = + "Tap Point: FSK2 Decoder > < Message Framer"; + + private FSK2Decoder mFSKDecoder; + private FloatHalfBandFilter mDecimationFilter; + private FloatFIRFilter mBandPassFilter; + private MessageFramer mMessageFramer; + private Fleetsync2MessageProcessor mMessageProcessor; + + public Fleetsync2Decoder( AliasList aliasList ) + { + super( SampleType.FLOAT ); + + mDecimationFilter = new FloatHalfBandFilter( + Filters.FIR_HALF_BAND_31T_ONE_EIGHTH_FCO, 1.0002 ); + addFloatListener( mDecimationFilter ); + + mBandPassFilter = new FloatFIRFilter( + Filters.FIRBP_1200FSK_24000FS.getCoefficients(), 1.02 ); + mDecimationFilter.setListener( mBandPassFilter ); + + mFSKDecoder = new FSK2Decoder( sDECIMATED_SAMPLE_RATE, + sSYMBOL_RATE, Output.INVERTED ); + mBandPassFilter.setListener( mFSKDecoder ); + + mMessageFramer = new MessageFramer( + SyncPattern.FLEETSYNC2.getPattern(), sMESSAGE_LENGTH ); + mFSKDecoder.setListener( mMessageFramer ); + + mMessageProcessor = new Fleetsync2MessageProcessor( aliasList ); + mMessageFramer.addMessageListener( mMessageProcessor ); + mMessageProcessor.addMessageListener( this ); + } + + public void dispose() + { + super.dispose(); + + mDecimationFilter.dispose(); + mBandPassFilter.dispose(); + mFSKDecoder.dispose(); + mMessageFramer.dispose(); + mMessageProcessor.dispose(); + } + + @Override + public DecoderType getType() + { + return DecoderType.FLEETSYNC2; + } + + /** + * Returns a float listener interface for connecting this decoder to a + * float stream provider + */ + public Listener getFloatReceiver() + { + return (Listener)mDecimationFilter; + } + + @Override + public List getTaps() + { + if( mAvailableTaps == null ) + { + mAvailableTaps = new ArrayList(); + + mAvailableTaps.add( new FloatTap( INSTRUMENT_INPUT, 0, 1.0f ) ); + mAvailableTaps.add( new FloatTap( + INSTRUMENT_BANDPASS_FILTER_TO_FSK2_DEMOD, 0, 0.5f ) ); + mAvailableTaps.addAll( mFSKDecoder.getTaps() ); + mAvailableTaps.add( new BinaryTap( + INSTRUMENT_FSK2_DECODER_TO_MESSAGE_FRAMER, 0, 0.025f ) ); + } + + return mAvailableTaps; + } + + @Override + public void addTap( Tap tap ) + { + mFSKDecoder.addTap( tap ); + + switch( tap.getName() ) + { + case INSTRUMENT_INPUT: + FloatTap inputTap = (FloatTap)tap; + addFloatListener( inputTap ); + break; + case INSTRUMENT_BANDPASS_FILTER_TO_FSK2_DEMOD: + FloatTap bpTap = (FloatTap)tap; + mBandPassFilter.setListener( bpTap ); + bpTap.setListener( mFSKDecoder ); + break; + case INSTRUMENT_FSK2_DECODER_TO_MESSAGE_FRAMER: + BinaryTap decoderTap = (BinaryTap)tap; + mFSKDecoder.setListener( decoderTap ); + decoderTap.setListener( mMessageFramer ); + break; + } + } + + @Override + public void removeTap( Tap tap ) + { + mFSKDecoder.removeTap( tap ); + + switch( tap.getName() ) + { + case INSTRUMENT_INPUT: + FloatTap inputTap = (FloatTap)tap; + removeFloatListener( inputTap ); + break; + case INSTRUMENT_BANDPASS_FILTER_TO_FSK2_DEMOD: + mBandPassFilter.setListener( mFSKDecoder ); + break; + case INSTRUMENT_FSK2_DECODER_TO_MESSAGE_FRAMER: + mFSKDecoder.setListener( mMessageFramer ); + break; + } + } +} diff --git a/src/decode/fleetsync2/Fleetsync2MessageProcessor.java b/src/decode/fleetsync2/Fleetsync2MessageProcessor.java new file mode 100644 index 00000000..1c5f7dda --- /dev/null +++ b/src/decode/fleetsync2/Fleetsync2MessageProcessor.java @@ -0,0 +1,59 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package decode.fleetsync2; + +import message.Message; +import sample.Broadcaster; +import sample.Listener; +import alias.AliasList; +import bits.BitSetBuffer; + +public class Fleetsync2MessageProcessor implements Listener +{ + private Broadcaster mBroadcaster = new Broadcaster(); + + private AliasList mAliasList; + + public Fleetsync2MessageProcessor( AliasList list ) + { + mAliasList = list; + } + + public void dispose() + { + mBroadcaster.dispose(); + } + + @Override + public void receive( BitSetBuffer buffer ) + { + FleetsyncMessage message = new FleetsyncMessage( buffer, mAliasList ); + + mBroadcaster.receive( message ); + } + + public void addMessageListener( Listener listener ) + { + mBroadcaster.addListener( listener ); + } + + public void removeMessageListener( Listener listener ) + { + mBroadcaster.removeListener( listener ); + } +} diff --git a/src/decode/fleetsync2/FleetsyncActivitySummary.java b/src/decode/fleetsync2/FleetsyncActivitySummary.java new file mode 100644 index 00000000..c1540651 --- /dev/null +++ b/src/decode/fleetsync2/FleetsyncActivitySummary.java @@ -0,0 +1,171 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package decode.fleetsync2; + +import java.util.Iterator; +import java.util.TreeSet; + +import message.Message; +import sample.Listener; +import alias.Alias; +import alias.AliasList; +import controller.activity.ActivitySummaryProvider; + +public class FleetsyncActivitySummary implements ActivitySummaryProvider, + Listener +{ + private AliasList mAliasList; + private TreeSet mIdents = new TreeSet(); + private TreeSet mEmergencyIdents = new TreeSet(); + + /** + * Compiles a summary of active talkgroups, unique radio ids encountered + * in the decoded messages. Talkgroups have to be heard a minimum of twice + * to be considered valid ... in order to weed out the invalid ones. + * + * Returns a textual summary of the activity + */ + public FleetsyncActivitySummary( AliasList list ) + { + mAliasList = list; + } + + public void dispose() + { + mIdents.clear(); + mEmergencyIdents.clear(); + } + + @Override + public void receive( Message message ) + { + if( message instanceof FleetsyncMessage ) + { + FleetsyncMessage fs = ((FleetsyncMessage)message); + + if( fs.isValid() ) + { + String from = fs.getFromID(); + mIdents.add( from ); + + String to = fs.getToID(); + + if( !to.startsWith( "xxx" )) + { + mIdents.add( to ); + } + } + + FleetsyncMessageType type = fs.getMessageType(); + + switch( type ) + { + case EMERGENCY: + case LONE_WORKER_EMERGENCY: + mEmergencyIdents.add( fs.getFromID() ); + break; + } + } + } + + @Override + public String getSummary() + { + StringBuilder sb = new StringBuilder(); + + sb.append( "=============================\n" ); + sb.append( "Decoder:\tFleetsync II\n\n" ); + sb.append( "Fleetsync Idents\n" ); + + if( mIdents.isEmpty() ) + { + sb.append( " None\n" ); + } + else + { + Iterator it = mIdents.iterator(); + + while( it.hasNext() ) + { + String ident = it.next(); + + sb.append( " " ); + sb.append( formatIdent( ident ) ); + sb.append( " " ); + + Alias alias = mAliasList.getFleetsyncAlias( ident ); + + if( alias != null ) + { + sb.append( alias.getName() ); + } + + sb.append( "\n" ); + } + } + + sb.append( "\nFleetsync Emergency Activations\n" ); + + if( mEmergencyIdents.isEmpty() ) + { + sb.append( " None\n" ); + } + else + { + Iterator it = mEmergencyIdents.iterator(); + + while( it.hasNext() ) + { + String ident = it.next(); + + sb.append( " " ); + sb.append( formatIdent( ident ) ); + sb.append( " " ); + + Alias alias = mAliasList.getFleetsyncAlias( ident ); + + if( alias != null ) + { + sb.append( alias.getName() ); + } + + sb.append( "\n" ); + } + } + + return sb.toString(); + } + + public static String formatIdent( String ident ) + { + StringBuilder sb = new StringBuilder(); + + if( ident.length() == 7 ) + { + sb.append( ident.substring( 0, 3 ) ); + sb.append( "-" ); + sb.append( ident.substring( 3, 7 ) ); + + return sb.toString(); + } + else + { + return ident; + } + } +} diff --git a/src/decode/fleetsync2/FleetsyncCallEvent.java b/src/decode/fleetsync2/FleetsyncCallEvent.java new file mode 100644 index 00000000..bd3468b9 --- /dev/null +++ b/src/decode/fleetsync2/FleetsyncCallEvent.java @@ -0,0 +1,188 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package decode.fleetsync2; + +import alias.Alias; +import alias.AliasList; +import controller.activity.CallEvent; +import decode.DecoderType; + +public class FleetsyncCallEvent extends CallEvent +{ + public FleetsyncCallEvent( CallEventType callEventType, + AliasList aliasList, + String fromID, + String toID, + String details ) + { + super( DecoderType.FLEETSYNC2, callEventType, + aliasList, fromID, toID, details ); + } + + private Alias getAlias( String ident ) + { + if( hasAliasList() ) + { + return getAliasList().getFleetsyncAlias( ident ); + } + + return null; + } + + @Override + public Alias getFromIDAlias() + { + return getAlias( getFromID() ); + } + + @Override + public Alias getToIDAlias() + { + return getAlias( getToID() ); + } + + @Override + public int getChannel() + { + return 0; + } + + @Override + public int getFrequency() + { + return 0; + } + + public static class Builder + { + /* Required parameters */ + private CallEventType mCallEventType; + + /* Optional parameters */ + private AliasList mAliasList; + private String mFromID; + private String mToID; + private String mDetails; + + public Builder( CallEventType callEventType ) + { + mCallEventType = callEventType; + } + + public Builder aliasList( AliasList aliasList ) + { + mAliasList = aliasList; + return this; + } + + public Builder from( String val ) + { + mFromID = val; + return this; + } + + public Builder details( String details ) + { + mDetails = details; + return this; + } + + public Builder to( String toID ) + { + mToID = toID; + return this; + } + + public FleetsyncCallEvent build() + { + return new FleetsyncCallEvent( this ); + } + } + + /** + * Private constructor for the builder + */ + private FleetsyncCallEvent( Builder builder ) + { + this( builder.mCallEventType, + builder.mAliasList, + builder.mFromID, + builder.mToID, + builder.mDetails ); + } + + public static FleetsyncCallEvent + getFleetsync2Event( FleetsyncMessage message ) + { + CallEventType type = CallEventType.UNKNOWN; + StringBuilder sbDetails = new StringBuilder(); + sbDetails.append( "Fleetsync " ); + + switch( message.getMessageType() ) + { + case ACKNOWLEDGE: + type = CallEventType.ACKNOWLEDGE; + break; + case ANI: + type = CallEventType.ID_ANI; + break; + case EMERGENCY: + type = CallEventType.EMERGENCY; + break; + case GPS: + type = CallEventType.GPS; + sbDetails.append( message.getGPSLocation() ); + break; + case LONE_WORKER_EMERGENCY: + type = CallEventType.EMERGENCY; + sbDetails.append( "LONE WORKER EMERGENCY ALERT" ); + break; + case PAGING: + type = CallEventType.PAGE; + break; + case STATUS: + type = CallEventType.STATUS; + sbDetails.append( "Status: " ); + sbDetails.append( message.getStatus() ); + + if( message.getStatusAlias() != null ) + { + sbDetails.append( "/" ); + sbDetails.append( message.getStatusAlias().getName() ); + } + break; + } + + if( type == CallEventType.ID_ANI || + type == CallEventType.EMERGENCY ) + { + return new FleetsyncCallEvent.Builder( type ) + .details( sbDetails.toString() ) + .from( message.getFromID() ) + .build(); + } + else + { + return new FleetsyncCallEvent.Builder( type ) + .details( sbDetails.toString() ) + .from( message.getFromID() ) + .to( message.getToID() ) + .build(); + } + } +} diff --git a/src/decode/fleetsync2/FleetsyncChannelState.java b/src/decode/fleetsync2/FleetsyncChannelState.java new file mode 100644 index 00000000..cc2c3194 --- /dev/null +++ b/src/decode/fleetsync2/FleetsyncChannelState.java @@ -0,0 +1,195 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package decode.fleetsync2; + +import message.Message; +import alias.Alias; +import controller.state.AuxChannelState; +import controller.state.ChannelState; +import controller.state.ChannelState.ChangedAttribute; +import controller.state.ChannelState.State; + +public class FleetsyncChannelState extends AuxChannelState +{ + private static final String PROTOCOL = "FSync II"; + + private String mFleetFrom; + private Alias mFleetFromAlias; + + private String mFleetTo; + private Alias mFleetToAlias; + + private String mMessage; + private String mMessageType; + + private FleetsyncActivitySummary mActivitySummary; + + public FleetsyncChannelState( ChannelState parentState ) + { + super( parentState ); + + mActivitySummary = + new FleetsyncActivitySummary( parentState.getAliasList() ); + } + + @Override + public void reset() + { + setFleetIDFrom( null ); + setFleetIDFromAlias( null ); + setFleetIDTo( null ); + setFleetIDToAlias( null ); + setMessage( null ); + setMessageType( null ); + } + + @Override + public void fade() + { + } + + @Override + public void receive( Message message ) + { + if( message instanceof FleetsyncMessage ) + { + mActivitySummary.receive( message ); + + FleetsyncMessage fleetsync = (FleetsyncMessage)message; + + setFleetIDFrom( fleetsync.getFromID() ); + setFleetIDFromAlias( fleetsync.getFromIDAlias() ); + + FleetsyncMessageType type = fleetsync.getMessageType(); + + if( type != FleetsyncMessageType.ANI ) + { + setFleetIDTo( fleetsync.getToID() ); + setFleetIDToAlias( fleetsync.getToIDAlias() ); + } + + setMessageType( type.getLabel() ); + + switch( type ) + { + case GPS: + setMessage( fleetsync.getGPSLocation() ); + break; + case STATUS: + StringBuilder sb = new StringBuilder(); + sb.append( fleetsync.getStatus() ); + + Alias status = fleetsync.getStatusAlias(); + + if( status != null ) + { + sb.append( "/" ); + sb.append( status.getName() ); + } + + setMessage( sb.toString() ); + break; + default: + break; + } + + /* Set the state to CALL on the parent channel state, so that the + * parent state timer can track turning off any updates we apply */ + mParentChannelState.setState( State.CALL ); + + FleetsyncCallEvent fsCallEvent = + FleetsyncCallEvent.getFleetsync2Event( fleetsync ); + + fsCallEvent.setAliasList( mParentChannelState.getAliasList() ); + + mParentChannelState.receiveCallEvent( fsCallEvent ); + } + } + + public String getFleetIDFrom() + { + return mFleetFrom; + } + + public void setFleetIDFrom( String from ) + { + mFleetFrom = from; + broadcastChange( ChangedAttribute.FROM_TALKGROUP ); + } + + public Alias getFleetIDFromAlias() + { + return mFleetFromAlias; + } + + public void setFleetIDFromAlias( Alias alias ) + { + mFleetFromAlias = alias; + broadcastChange( ChangedAttribute.FROM_TALKGROUP_ALIAS ); + } + + public String getFleetIDTo() + { + return mFleetTo; + } + + public void setFleetIDTo( String to ) + { + mFleetTo = to; + broadcastChange( ChangedAttribute.TO_TALKGROUP ); + } + + public Alias getFleetIDToAlias() + { + return mFleetToAlias; + } + + public void setFleetIDToAlias( Alias alias ) + { + mFleetToAlias = alias; + broadcastChange( ChangedAttribute.TO_TALKGROUP_ALIAS ); + } + + public String getMessage() + { + return mMessage; + } + + public void setMessage( String message ) + { + mMessage = message; + broadcastChange( ChangedAttribute.MESSAGE ); + } + + public String getMessageType() + { + return mMessageType; + } + + public void setMessageType( String type ) + { + mMessageType = type; + broadcastChange( ChangedAttribute.MESSAGE_TYPE ); + } + + @Override + public String getActivitySummary() + { + return mActivitySummary.getSummary(); + } +} diff --git a/src/decode/fleetsync2/FleetsyncMessage.java b/src/decode/fleetsync2/FleetsyncMessage.java new file mode 100644 index 00000000..8dff9001 --- /dev/null +++ b/src/decode/fleetsync2/FleetsyncMessage.java @@ -0,0 +1,863 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package decode.fleetsync2; + +import java.text.DecimalFormat; +import java.text.SimpleDateFormat; +import java.util.BitSet; +import java.util.Calendar; +import java.util.Date; +import java.util.GregorianCalendar; + +import map.Plottable; +import message.Message; + +import org.jdesktop.swingx.mapviewer.GeoPosition; + +import alias.Alias; +import alias.AliasList; +import bits.BitSetBuffer; +import crc.CRC; +import crc.CRCFleetsync; + +public class FleetsyncMessage extends Message +{ + private static DecimalFormat mDecimalFormatter = new DecimalFormat("#.#####"); + private static SimpleDateFormat mSDF = new SimpleDateFormat( "yyyyMMdd HHmmss" ); + + //Calendar to use in calculating time hacks + Calendar mCalendar = new GregorianCalendar(); + + //Message parts are identified in big-endian order for correct translation + + //Message Header + private static int[] sRevs = { 4,3,2,1,0 }; + private static int[] sSync = { 20,19,18,17,16,15,14,13,12,11,10,9,8,7,6,5 }; + + //Message Block 1 + private static int[] sStatusMessage = { 27,26,25,24,23,22,21 }; + private static int[] sMessageType = { 33,32,31,30,29 }; + + private static int sEmergencyFlag = 22; + private static int sLoneWorkerFlag = 24; + private static int sPagingFlag = 26; + private static int sEndOfTransmissionFlag = 27; + private static int sManualFlag = 28; + private static int sANIFlag = 29; + private static int sStatusFlag = 30; + private static int sAcknowledgeFlag = 31; + //32 - unknown - always 0 + //33 - unknown - always 0 + //34 - unknown - set for ACKNOWLEDGE + private static int sGPSExtensionFlag = 35; + private static int sFleetExtensionFlag = 36; + private static int[] sFleetFrom = { 44,43,42,41,40,39,38,37 }; + private static int[] sIdentFrom = { 56,55,54,53,52,51,50,49,48,47,46,45 }; + private static int[] sIdentTo = { 68,67,66,65,64,63,62,61,60,59,58,57 }; + private static int[] sCRC1 = { 84,83,82,81,80,79,78,77,76,75,74,73,72,71,70,69 }; + + //Message Block 2 + private static int[] sFleetTo = { 92,91,90,89,88,87,86,85 }; + private static int[] sCRC2 = { 148,147,146,145,144,143,142,141,140,139,138,137,136,135,134,132 }; + + //Message Block 3 + private static int[] sGPSHours = { 176,175,174,173,172 }; + private static int[] sGPSMinutes = { 182,181,180,179,178,177 }; + private static int[] sGPSSeconds = { 188,187,186,185,184,183 }; + private static int[] sCRC3 = { 212,211,210,209,208,207,206,205,204,203,202,201,200,199,198,197 }; + + //Message Block 4 + private static int[] sGPSChecksum = { 220,219,218,217,216,215,214,213 }; + private static int[] sLatitudeDegreesMinutes = { 236,235,234,233,232,231,230,229,228,227,226,225,224,223,222,221 }; + private static int[] sLatitudeDecimalMinutes = { 251,250,249,248,247,246,245,244,243,242,241,240,239,238 }; + private static int[] sSpeed = { 276,275,274,273,272,271,270,269,268,267,266,265,264,263,262,261,260,259,258,257,256,255,254,253,252 }; + private static int[] sCRC4 = { 276,275,274,273,272,271,270,269,268,267,266,265,264,263,262,261 }; + + //Message Block 5 + private static int[] sGPSCentury = { 284,283,282,281,280,279,278,277 }; + private static int[] sGPSYear = { 291,290,289,288,287,286,285 }; + private static int[] sGPSMonth = { 295,294,293,292 }; + private static int[] sGPSDay = { 300,299,298,297,296 }; + private static int[] sLongitudeDegreesMinutes = { 316,315,314,313,312,311,310,309,308,307,306,305,304,303,302,301 }; + private static int[] sLongitudeDecimalMinutes = { 331,330,329,328,327,326,325,324,323,322,321,320,319,318 }; + private static int[] sCRC5 = { 340,339,338,337,336,335,334,333,332,331,330,329,328,327,326,325 }; + + //Message Block 6 + private static int[] sGPSUnknown1 = { 352,351,350,349 }; + private static int[] sGPSHeading = { 365,363,362,361,360,359,358,357,356,355,354,353 }; + private static int[] sCRC6 = { 404,403,402,401,400,399,398,397,396,395,394,393,392,391,390,389 }; + + //Message Block 7 + private static int[] sBLK7WhatIsIt = { 444,443,442,441,440,439,438,437,436,435,434,433,432,431,430,429 }; + private static int[] sCRC7 = { 468,467,466,465,464,463,462,461,460,459,458,457,456,455,454,453 }; + + //Message Block 8 + private static int[] sGPSSpeed = { 491,490,489,488,487,486,485,484 }; + private static int[] sGPSSpeedFractional = { 499,498,497,496,495,494,493,492 }; + private static int[] sCRC8 = { 532,531,530,529,528,527,526,525,524,523,522,521,520,519,518,517 }; + + private BitSetBuffer mMessage; + private CRC[] mCRC = new CRC[ 8 ]; + private AliasList mAliasList; + + public FleetsyncMessage( BitSetBuffer message, AliasList list ) + { + mMessage = message; + mAliasList = list; + checkParity(); + } + + private void checkParity() + { + //Check message block 1 + mCRC[ 0 ] = detectAndCorrect( 21, 85 ); + + //Only check subsequent blocks if we know block 1 is correct + if( mCRC[ 0 ] == CRC.PASSED || mCRC[ 0 ] == CRC.CORRECTED ) + { + if( hasFleetExtensionFlag() ) + { + //Check message block 2 + mCRC[ 1 ] = detectAndCorrect( 85, 149 ); + } + + if( hasGPSFlag() ) + { + //Check message block 3 + mCRC[ 2 ] = detectAndCorrect( 149, 213 ); + //Check message block 4 + mCRC[ 3 ] = detectAndCorrect( 213, 277 ); + //Check message block 5 + mCRC[ 4 ] = detectAndCorrect( 277, 341 ); + //Check message block 6 + mCRC[ 5 ] = detectAndCorrect( 341, 405 ); + //Check message block 7 + mCRC[ 6 ] = detectAndCorrect( 405, 469 ); + //Check message block 8 + mCRC[ 7 ] = detectAndCorrect( 469, 533 ); + } + } + } + + private CRC detectAndCorrect( int start, int end ) + { + BitSet original = mMessage.get( start, end ); + + CRC retVal = CRCFleetsync.check( original ); + + //Attempt to correct single-bit errors + if( retVal == CRC.FAILED_PARITY ) + { + int[] errorBitPositions = CRCFleetsync.findBitErrors( original ); + + if( errorBitPositions != null ) + { + for( int errorBitPosition: errorBitPositions ) + { + mMessage.flip( start + errorBitPosition ); + } + + retVal = CRC.CORRECTED; + } + } + + return retVal; + } + + public boolean isValid() + { + boolean valid = true; + + for( int x = 0; x < mCRC.length; x++ ) + { + CRC crc = mCRC[ x ]; + + if( crc != null ) + { + if( crc == CRC.FAILED_CRC || + crc == CRC.FAILED_PARITY || + crc == CRC.UNKNOWN ) + { + valid = false; + } + } + } + + return valid; + } + + @Override + public String toString() + { + StringBuilder sb = new StringBuilder(); + + sb.append( mSDF.format( new Date( System.currentTimeMillis() ) ) ); + + sb.append( " FSync2 " + getParity() ); + + sb.append( getMessage() ); + + sb.append( " [" + mMessage.toString() + "]" ); + + return sb.toString(); + } + + /** + * Pads spaces onto the end of the value to make it 'places' long + */ + public String pad( String value, int places, String padCharacter ) + { + StringBuilder sb = new StringBuilder(); + + sb.append( value ); + + while( sb.length() < places ) + { + sb.append( padCharacter ); + } + + return sb.toString(); + } + + /** + * Pads an integer value with additional zeroes to make it decimalPlaces long + */ + public String format( int number, int decimalPlaces ) + { + StringBuilder sb = new StringBuilder(); + + int paddingRequired = decimalPlaces - ( String.valueOf( number ).length() ); + + for( int x = 0; x < paddingRequired; x++) + { + sb.append( "0" ); + } + + sb.append( number ); + + return sb.toString(); + } + + public FleetsyncMessageType getMessageType() + { + if( hasStatusFlag() ) + { + if( hasAcknowledgeFlag() ) + { + return FleetsyncMessageType.ACKNOWLEDGE; + } + + if( hasGPSFlag() ) + { + return FleetsyncMessageType.GPS; + } + else + { + return FleetsyncMessageType.STATUS; + } + } + else + { + if( hasAcknowledgeFlag() ) + { + return FleetsyncMessageType.ACKNOWLEDGE; + } + + if( hasANIFlag() ) + { + return FleetsyncMessageType.ANI; + } + + if( hasGPSFlag() ) + { + return FleetsyncMessageType.GPS; + } + + if( hasPagingFlag() ) + { + return FleetsyncMessageType.PAGING; + } + + if( hasEmergencyFlag() ) + { + if( hasLoneWorkerFlag() ) + { + return FleetsyncMessageType.LONE_WORKER_EMERGENCY; + } + else + return FleetsyncMessageType.EMERGENCY; + } + } + + return FleetsyncMessageType.UNKNOWN; + } + + public int getStatus() + { + return getStatusNumber() + 9; + } + + /** + * Returns the RAW status number. The actual status number should be + * accessed via the getStatus() method. + * @return + */ + public int getStatusNumber() + { + return getInt( sStatusMessage ); + } + + /** + * @return + */ + public Alias getStatusAlias() + { + if( mAliasList != null ) + { + return mAliasList.getStatus( getStatus() ); + } + + return null; + } + + public int getFleetFrom() + { + return getInt( sFleetFrom ) + 99; + } + + public int getIdentifierFrom() + { + return getInt( sIdentFrom ) + 999; + } + + /** + * Inverted Flags - 0 = flag is true + * @return + */ + public boolean hasEndOfTransmissionFlag() + { + return !mMessage.get( sEndOfTransmissionFlag ); + } + + public boolean hasEmergencyFlag() + { + return !mMessage.get( sEmergencyFlag ); + } + + public boolean hasLoneWorkerFlag() + { + return !mMessage.get( sLoneWorkerFlag ); + } + + public boolean hasPagingFlag() + { + return !mMessage.get( sPagingFlag ); + } + + + /** + * Normal Flags - 1 = flag is true + * @return + */ + public boolean hasANIFlag() + { + return mMessage.get( sANIFlag ); + } + + public boolean hasAcknowledgeFlag() + { + return mMessage.get( sAcknowledgeFlag ); + } + + public boolean hasFleetExtensionFlag() + { + return mMessage.get( sFleetExtensionFlag ); + } + + public boolean hasGPSFlag() + { + return mMessage.get( sGPSExtensionFlag ); + } + + public boolean hasStatusFlag() + { + return mMessage.get( sStatusFlag ); + } + + + public int getFleetTo() + { + if( hasFleetExtensionFlag() ) + { + return getInt( sFleetTo ) + 99; + } + else + { + return getInt( sFleetFrom ) + 99; + } + } + + public int getIdentifierTo() + { + return getInt( sIdentTo ) + 999; + } + + /** + * GPS Heading + * @return + */ + public double getHeading() + { + double retVal = 0.0; + + int heading = getInt( sGPSHeading ); + + if( heading != 4095 ) + { + retVal = (double)(heading / 10.0 ); + } + + return retVal; + } + + public int getBlock7WhatIsIt() + { + int value = getInt( sBLK7WhatIsIt ); + + if( value == 65535 ) + { + value = -1; + } + + return value; + } + + /** + * Speed in Kph - whole numbers and three decimal digits of precision + * @return + */ + public double getSpeed() + { + double retVal = 0.0; + + int temp = getInt( sSpeed ); + + if( temp != 0 ) + { + retVal = (double)temp / 1000.0D; + } + return retVal; + } + + public double getLatitude() + { + //TODO: determine the correct hemisphere indicator and replace this + //hardcoded "0" with the correct hemisphere value + + return convertDDMToDD( 0, + getInt( sLatitudeDegreesMinutes ), + getInt( sLatitudeDecimalMinutes ) ); + } + + public double getLongitude() + { + //TODO: determine the correct hemisphere indicator and replace this + //hardcoded "1" with the correct hemisphere value + + return convertDDMToDD( 1, + getInt( sLongitudeDegreesMinutes ), + getInt( sLongitudeDecimalMinutes ) ); + } + + /** + * Converts Degrees Decimal Minutes to Decimal Degrees + * + * Latitude and Longitude values are represented by: + * + * @param hemisphere - 0=North & East, 1=South & West + * @param degreesMinutes - an integer value with the first 2-3 digits representing + * the degrees and the last two digits representing the minutes + * + * @param decimalDegrees - an integer value representing the fractional + * minutes + * + * @return - decimal degrees formatted value + */ + public double convertDDMToDD( int hemisphere, int degreesMinutes, int decimalDegrees ) + { + double retVal = 0.0; + + if( degreesMinutes != 0 ) + { + //Degrees - divide value by 100 and retain the whole number value (ie degrees) + retVal += (double)( degreesMinutes / 100 ); + + //Minutes - modulus by 100 to get the whole minutes value + int wholeMinutes = degreesMinutes % 100; + + if( wholeMinutes != 0 ) + { + retVal += (double)( wholeMinutes / 60.0D ); + } + } + + if( decimalDegrees != 0 ) + { + //Fractional Minutes - divide by 10,000 to get the decimal place correct + //then divide by 60 (minutes) to get the decimal value + //10,000 * 60 = 600,000 + retVal += (double)( decimalDegrees / 600000.0D ); + } + + //Adjust the value +/- for the hemisphere + if( hemisphere == 1 ) //South and West values + { + retVal = -retVal; + } + + return retVal; + } + + public String getGPSTime() + { + StringBuilder sb = new StringBuilder(); + sb.append( format( getInt( sGPSHours ), 2 ) ); + sb.append( ":" ); + sb.append( format( getInt( sGPSMinutes ), 2 ) ); + sb.append( ":" ); + sb.append( format( getInt( sGPSSeconds ), 2 ) ); + sb.append( "z" ); + + return sb.toString(); + } + + + /** + * 7 bit checksum that is part of the GPGGA message, I think + * @return + */ + public int getGPSChecksum() + { + return getInt( sGPSChecksum ); + } + + public String getGPSChecksumString() + { + String sum = Integer.toHexString( getGPSChecksum() ); + + if( sum.length() == 1 ) + { + return "*0" + sum; + } + else + { + return "*" + sum; + } + } + + /** + * Returns 1-based calendar day of month + * 1 = 1st day of month + */ + public int getGPSDay() + { + return getInt( sGPSDay ) + 1; + } + + /** + * Returns 0-based GPS Month + * 0 = January + * + * Note: actual day of month value is 1-based, so we subtract 1 to get + * the actual day of month value + */ + public int getGPSMonth() + { + return getInt( sGPSMonth ); + } + + /** + * Returns year, which is a combination of the century bit field + 1, times + * 100, plus the year bit field + */ + public int getGPSYear() + { +// return ( ( getInt( sGPSCentury ) + 1 ) * 100 ) + getInt( sGPSYear ); + return ( 2000 ) + getInt( sGPSYear ); + } + + public String getGPSLocation() + { + StringBuilder sb = new StringBuilder(); + sb.append( pad( String.valueOf( mDecimalFormatter.format( getLatitude() ) ), 8, "0" ) + " " ); + sb.append( pad( String.valueOf( mDecimalFormatter.format( getLongitude() ) ), 10, "0" ) ); + + return sb.toString(); + } + + + public String getGPSDate() + { + StringBuilder sb = new StringBuilder(); + + sb.append( getGPSYear() + "-" ); + + int month = getGPSMonth(); + + if( month < 10 ) + { + sb.append( "0" ); + } + + sb.append( month + "-" ); + + int day = getGPSDay(); + + if( day < 10 ) + { + sb.append( "0" ); + } + + sb.append( day ); + + return sb.toString(); + } + + private int getInt( int[] bits ) + { + int retVal = 0; + + for( int x = 0; x < bits.length; x++ ) + { + if( mMessage.get( bits[ x ] ) ) + { + retVal += 1< + ******************************************************************************/ +package decode.fleetsync2; + +public enum FleetsyncMessageType +{ + ACKNOWLEDGE( "ACK" ), + ANI( "ANI" ), + EMERGENCY( "EMERG" ), + GPS( "GPS" ), + LONE_WORKER_EMERGENCY( "LONE WORKER" ), + PAGING( "PAGE" ), + STATUS( "STATUS" ), + UNKNOWN( "UNK" ); + + private String mLabel; + + private FleetsyncMessageType( String label ) + { + mLabel = label; + } + + public String getLabel() + { + return mLabel; + } +} diff --git a/src/decode/fleetsync2/FleetsyncPanel.java b/src/decode/fleetsync2/FleetsyncPanel.java new file mode 100644 index 00000000..a601baab --- /dev/null +++ b/src/decode/fleetsync2/FleetsyncPanel.java @@ -0,0 +1,167 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package decode.fleetsync2; + +import java.awt.EventQueue; + +import javax.swing.JLabel; + +import net.miginfocom.swing.MigLayout; +import settings.ColorSetting; +import settings.ColorSetting.ColorSettingName; +import settings.Setting; +import settings.SettingsManager; +import controller.state.AuxStatePanel; +import controller.state.ChannelState.ChangedAttribute; + +public class FleetsyncPanel extends AuxStatePanel +{ + private static final long serialVersionUID = 1L; + private static final String PROTOCOL = "FSync II"; + + private JLabel mFromLabel = new JLabel( " " ); + private JLabel mFrom = new JLabel(); + private JLabel mFromAlias = new JLabel(); + + private JLabel mToLabel = new JLabel(); + private JLabel mTo = new JLabel( " " ); + private JLabel mToAlias = new JLabel(); + + private JLabel mProtocol = new JLabel( PROTOCOL ); + private JLabel mMessage = new JLabel(); + private JLabel mMessageType = new JLabel(); + + public FleetsyncPanel( SettingsManager settingsManager, + FleetsyncChannelState state ) + { + super( settingsManager, state ); + + init(); + } + + public void dispose() + { + super.dispose(); + } + + public FleetsyncChannelState getState() + { + return (FleetsyncChannelState)mAuxChannelState; + } + + private void init() + { + setOpaque( false ); //Use the parent panel's background color + + setLayout( new MigLayout( "insets 1 0 0 0", "[grow,fill]", "[]0[]0[]") ); + + addLabel( mFromLabel, false ); + addLabel( mFrom, false ); + addLabel( mFromAlias, true ); + + addLabel( mToLabel, false ); + addLabel( mTo, false ); + addLabel( mToAlias, true ); + + addLabel( mProtocol, false ); + addLabel( mMessageType, false ); + addLabel( mMessage, true ); + } + + @Override + public void receive( final ChangedAttribute changedAttribute ) + { + EventQueue.invokeLater( new Runnable() + { + @Override + public void run() + { + switch( changedAttribute ) + { + case FROM_TALKGROUP: + String from = getState().getFleetIDFrom(); + + if( from != null ) + { + mFromLabel.setText( "FM:" ); + } + else + { + mFromLabel.setText( " " ); + } + mFrom.setText( from ); + break; + case FROM_TALKGROUP_ALIAS: + updateAliasLabel( mFromAlias, + getState().getFleetIDFromAlias() ); + break; + case TO_TALKGROUP: + String to = getState().getFleetIDTo(); + + if( to != null ) + { + mToLabel.setText( "TO:" ); + } + else + { + mToLabel.setText( " " ); + } + mTo.setText( to ); + break; + case TO_TALKGROUP_ALIAS: + updateAliasLabel( mToAlias, + getState().getFleetIDToAlias() ); + break; + case MESSAGE: + mMessage.setText( getState().getMessage() ); + break; + case MESSAGE_TYPE: + mMessageType.setText( getState().getMessageType() ); + break; + } + + repaint(); + } + } ); + } + + @Override + public void settingChanged( Setting setting ) + { + super.settingChanged( setting ); + + if( setting instanceof ColorSetting ) + { + ColorSetting color = (ColorSetting)setting; + + if( color.getColorSettingName() == + ColorSettingName.CHANNEL_STATE_LABEL_AUX_DECODER ) + { + mFromLabel.setForeground( color.getColor() ); + mFrom.setForeground( color.getColor() ); + mFromAlias.setForeground( color.getColor() ); + mToLabel.setForeground( color.getColor() ); + mTo.setForeground( color.getColor() ); + mToAlias.setForeground( color.getColor() ); + mProtocol.setForeground( color.getColor() ); + mMessageType.setForeground( color.getColor() ); + mMessage.setForeground( color.getColor() ); + } + } + } +} diff --git a/src/decode/ltrnet/LTRCallEvent.java b/src/decode/ltrnet/LTRCallEvent.java new file mode 100644 index 00000000..5adc7fd5 --- /dev/null +++ b/src/decode/ltrnet/LTRCallEvent.java @@ -0,0 +1,184 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package decode.ltrnet; + +import alias.Alias; +import alias.AliasList; +import controller.activity.CallEvent; +import decode.DecoderType; + +public class LTRCallEvent extends CallEvent +{ + private int mChannel; + private int mFrequency; + + public LTRCallEvent( CallEventType callEventType, + AliasList aliasList, + String fromID, + String toID, + int channel, + int frequency, + String details ) + { + super( DecoderType.LTR_STANDARD, callEventType, + aliasList, fromID, toID, details ); + + mChannel = channel; + mFrequency = frequency; + } + + private Alias getAlias( String talkgroup ) + { + if( hasAliasList() ) + { + return getAliasList().getTalkgroupAlias( talkgroup ); + } + + return null; + } + + @Override + public Alias getFromIDAlias() + { + if( mCallEventType == CallEventType.CALL_START || + mCallEventType == CallEventType.REGISTER || + mCallEventType == CallEventType.ID_UNIQUE ) + { + if( hasAliasList() ) + { + try + { + int radioID = Integer.parseInt( getFromID() ); + + return mAliasList.getUniqueID( radioID ); + } + catch( Exception e ) + { + //Do nothing, we couldn't parse the int radio id value + } + } + } + else if( mCallEventType == CallEventType.REGISTER_ESN ) + { + if( hasAliasList() ) + { + return mAliasList.getESNAlias( getFromID() ); + } + } + else + { + return getAlias( getFromID() ); + } + + return null; + } + + @Override + public Alias getToIDAlias() + { + return getAlias( getToID() ); + } + + @Override + public int getChannel() + { + return mChannel; + } + + @Override + public int getFrequency() + { + return mFrequency; + } + + public static class Builder + { + /* Required parameters */ + private CallEventType mCallEventType; + + /* Optional parameters */ + private AliasList mAliasList; + private String mFromID; + private String mToID; + private String mDetails; + private int mChannel; + private int mFrequency; + + public Builder( CallEventType callEventType ) + { + mCallEventType = callEventType; + } + + public Builder aliasList( AliasList aliasList ) + { + mAliasList = aliasList; + return this; + } + + public Builder from( String val ) + { + mFromID = val; + return this; + } + + public Builder channel( int channel ) + { + mChannel = channel; + return this; + } + + public Builder details( String details ) + { + mDetails = details; + return this; + } + + public Builder frequency( int frequency ) + { + mFrequency = frequency; + return this; + } + + public Builder to( String toID ) + { + mToID = toID; + return this; + } + + public LTRCallEvent build() + { + return new LTRCallEvent( this ); + } + + } + + /** + * Private constructor for the builder + */ + private LTRCallEvent( Builder builder ) + { + this( builder.mCallEventType, + builder.mAliasList, + builder.mFromID, + builder.mToID, + builder.mChannel, + builder.mFrequency, + builder.mDetails ); + } + +} diff --git a/src/decode/ltrnet/LTRNetActivitySummary.java b/src/decode/ltrnet/LTRNetActivitySummary.java new file mode 100644 index 00000000..97966a77 --- /dev/null +++ b/src/decode/ltrnet/LTRNetActivitySummary.java @@ -0,0 +1,424 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package decode.ltrnet; + +import java.text.DecimalFormat; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.TreeSet; + +import message.Message; +import alias.Alias; +import alias.AliasList; +import controller.activity.ActivitySummaryProvider; + +public class LTRNetActivitySummary implements ActivitySummaryProvider +{ + private static final int sINT_NULL_VALUE = -1; + private DecimalFormat mDecimalFormatter = new DecimalFormat("0.00000"); + + private AliasList mAliasList; + private HashSet mTalkgroupsFirstHeard = new HashSet(); + private TreeSet mTalkgroups = new TreeSet(); + private TreeSet mESNs = new TreeSet(); + private TreeSet mUniqueIDs = new TreeSet(); + private TreeSet mNeighborIDs = new TreeSet(); + private TreeSet mSiteIDs = new TreeSet(); + private HashMap mReceiveFrequencies = + new HashMap(); + private HashMap mTransmitFrequencies = + new HashMap(); + private int mMonitoredLCN; + + /** + * Compiles a summary of active talkgroups, unique radio ids encountered + * in the decoded messages. Talkgroups have to be heard a minimum of twice + * to be considered valid ... in order to weed out the invalid ones. + * + * Returns a textual summary of the activity + */ + public LTRNetActivitySummary( AliasList list ) + { + mAliasList = list; + } + + @Override + public void receive( Message message ) + { + if( message instanceof LTRNetOSWMessage ) + { + LTRNetOSWMessage ltr = ((LTRNetOSWMessage)message); + + if( ltr.isValid() ) + { + switch( ltr.getMessageType() ) + { + case CA_STRT: + case CA_ENDD: + String talkgroup = ltr.getTalkgroupID( false ); + + if( mTalkgroupsFirstHeard.contains( talkgroup ) ) + { + mTalkgroups.add( talkgroup ); + } + else + { + mTalkgroupsFirstHeard.add( talkgroup ); + } + break; + case SY_IDLE: + mMonitoredLCN = ltr.getChannel(); + break; + case MA_CHNH: + break; + case MA_CHNL: + break; + case FQ_RXHI: + mReceiveFrequencies.put( ltr.getHomeRepeater(), + ltr.getFrequency() ); + break; + case FQ_RXLO: + break; + case FQ_TXHI: + mTransmitFrequencies.put( ltr.getHomeRepeater(), + ltr.getFrequency() ); + break; + case FQ_TXLO: + break; + case ID_ESNH: + break; + case ID_ESNL: + break; + case ID_NBOR: + int neighborID = ltr.getNeighborID(); + + if( neighborID != sINT_NULL_VALUE ) + { + mNeighborIDs.add( neighborID ); + } + break; + case ID_UNIQ: + int uniqueid = ltr.getRadioUniqueID(); + + if( uniqueid != sINT_NULL_VALUE ) + { + mUniqueIDs.add( uniqueid ); + } + + break; + case ID_SITE: + int siteID = ltr.getSiteID(); + + if( siteID != sINT_NULL_VALUE ) + { + mSiteIDs.add( siteID ); + } + break; + case UN_KNWN: + break; + default: + break; + } + } + } + else if( message instanceof LTRNetISWMessage ) + { + LTRNetISWMessage ltr = ((LTRNetISWMessage)message); + + if( ltr.isValid() ) + { + switch( ltr.getMessageType() ) + { + case CA_STRT: + case CA_ENDD: + String talkgroup = ltr.getTalkgroupID(); + + if( mTalkgroupsFirstHeard.contains( talkgroup ) ) + { + mTalkgroups.add( talkgroup ); + } + else + { + mTalkgroupsFirstHeard.add( talkgroup ); + } + break; + case ID_ESNH: + case ID_ESNL: + String esn = ltr.getESN(); + + if( !esn.contains( "xxxx" ) ) + { + mESNs.add( ltr.getESN() ); + } + break; + case ID_UNIQ: + int uniqueid = ltr.getRadioUniqueID(); + + if( uniqueid != sINT_NULL_VALUE ) + { + mUniqueIDs.add( uniqueid ); + } + + break; + default: + break; + } + } + } + + } + + @Override + public String getSummary() + { + StringBuilder sb = new StringBuilder(); + + sb.append( "Activity Summary\n" ); + sb.append( "Decoder:\tLTR-Net\n\n" ); + + if( mSiteIDs.isEmpty() ) + { + sb.append( "Site:\tUnknown\n" ); + } + else + { + Iterator it = mSiteIDs.iterator(); + + while( it.hasNext() ) + { + sb.append( "Site:\t" ); + + int siteID = it.next(); + + sb.append( siteID ); + + Alias siteAlias = mAliasList.getSiteID( siteID ); + + if( siteAlias != null ) + { + sb.append( " " ); + sb.append( siteAlias.getName() ); + } + + sb.append( "\n" ); + } + } + + sb.append( "\nLCNs (transmit | receive)\n" ); + + if( mReceiveFrequencies.isEmpty() && mTransmitFrequencies.isEmpty() ) + { + sb.append( " None\n" ); + } + else + { + for( int x = 1; x < 21; x++ ) + { + double rcv = 0.0d; + + if( mReceiveFrequencies.containsKey( x ) ) + { + rcv = mReceiveFrequencies.get( x ); + } + + double xmt = 0.0d; + + if( mTransmitFrequencies.containsKey( x ) ) + { + xmt = mTransmitFrequencies.get( x ); + } + + if( rcv > 0.0d || xmt > 0.0d ) + { + if( x < 10 ) + { + sb.append( " " ); + } + + sb.append( x ); + sb.append( ": " ); + + if( xmt == -1.0d ) + { + sb.append( "---.-----" ); + } + else + { + sb.append( mDecimalFormatter.format( xmt ) ); + } + sb.append( " | " ); + + if( rcv == -1.0d ) + { + sb.append( "---.-----" ); + } + else + { + sb.append( mDecimalFormatter.format( rcv ) ); + } + + if( x == mMonitoredLCN ) + { + sb.append( " **" ); + } + + sb.append( "\n" ); + } + } + } + + sb.append( "\nTalkgroups\n" ); + + if( mTalkgroups.isEmpty() ) + { + sb.append( " None\n" ); + } + else + { + Iterator it = mTalkgroups.iterator(); + + while( it.hasNext() ) + { + String tgid = it.next(); + + sb.append( " " ); + sb.append( formatTalkgroup( tgid ) ); + sb.append( " " ); + + Alias alias = mAliasList.getTalkgroupAlias( tgid ); + + if( alias != null ) + { + sb.append( alias.getName() ); + } + + sb.append( "\n" ); + } + } + + sb.append( "\nRadio Unique IDs\n" ); + + if( mUniqueIDs.isEmpty() ) + { + sb.append( " None\n" ); + } + else + { + Iterator it = mUniqueIDs.iterator(); + + while( it.hasNext() ) + { + int uid = it.next(); + + sb.append( " " ); + sb.append( uid ); + sb.append( " " ); + + Alias alias = mAliasList.getUniqueID( uid ); + + if( alias != null ) + { + sb.append( alias.getName() ); + } + + sb.append( "\n" ); + } + } + + sb.append( "\nESNs\n" ); + + if( mESNs.isEmpty() ) + { + sb.append( " None\n" ); + } + else + { + Iterator it = mESNs.iterator(); + + while( it.hasNext() ) + { + String esn = it.next(); + + sb.append( " " ); + sb.append( esn ); + sb.append( " " ); + + Alias alias = mAliasList.getESNAlias( esn ); + + if( alias != null ) + { + sb.append( alias.getName() ); + } + + sb.append( "\n" ); + } + } + + sb.append( "\nNeighbor Sites\n" ); + + if( mNeighborIDs.isEmpty() ) + { + sb.append( " None\n" ); + } + else + { + Iterator it = mNeighborIDs.iterator(); + + while( it.hasNext() ) + { + int neighbor = it.next(); + + sb.append( " " ); + sb.append( neighbor ); + sb.append( " " ); + + Alias alias = mAliasList.getSiteID( neighbor ); + + if( alias != null ) + { + sb.append( alias.getName() ); + } + + sb.append( "\n" ); + } + } + + return sb.toString(); + } + + public static String formatTalkgroup( String talkgroup ) + { + StringBuilder sb = new StringBuilder(); + + if( talkgroup.length() == 6 ) + { + sb.append( talkgroup.substring( 0, 1 ) ); + sb.append( "-" ); + sb.append( talkgroup.substring( 1, 3 ) ); + sb.append( "-" ); + sb.append( talkgroup.substring( 3, 6 ) ); + + return sb.toString(); + } + else + { + return talkgroup; + } + } +} diff --git a/src/decode/ltrnet/LTRNetChannelState.java b/src/decode/ltrnet/LTRNetChannelState.java new file mode 100644 index 00000000..4dfb07c8 --- /dev/null +++ b/src/decode/ltrnet/LTRNetChannelState.java @@ -0,0 +1,331 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package decode.ltrnet; + +import message.Message; +import alias.Alias; +import alias.AliasList; +import controller.activity.CallEvent.CallEventType; +import controller.channel.ProcessingChain; +import controller.state.AuxChannelState; +import controller.state.ChannelState; + +public class LTRNetChannelState extends ChannelState +{ + private String mToTalkgroup; + private Alias mToTalkgroupAlias; + private String mFromTalkgroup; + private Alias mFromTalkgroupAlias; + private String mFromTalkgroupType; + private int mChannelNumber; + private String mUniqueID; + private LTRCallEvent mCurrentCallEvent; + private LTRNetActivitySummary mActivitySummary; + + public LTRNetChannelState( ProcessingChain channel, AliasList aliasList ) + { + super( channel, aliasList ); + + mActivitySummary = new LTRNetActivitySummary( aliasList ); + } + + /** + * Intercept the fade event so that we can generate a call end event + */ + @Override + public void fade( final CallEventType type ) + { + /* + * We can receive multiple call tear-down messages -- only post a call + * end event for the one that can change the state to fade + */ + if( getState().canChangeTo( State.FADE ) ) + { + mCallEventModel.add( + new LTRCallEvent.Builder( type ) + .aliasList( getAliasList() ) + .channel( mChannelNumber ) + .frequency( mProcessingChain.getChannel() + .getTunerChannel().getFrequency() ) + .to( mToTalkgroup ) + .build() ); + + } + + mCurrentCallEvent = null; + + super.fade( type ); + } + + @Override + public String getActivitySummary() + { + StringBuilder sb = new StringBuilder(); + + sb.append( mActivitySummary.getSummary() ); + + for( AuxChannelState state: mAuxChannelStates ) + { + sb.append( state.getActivitySummary() ); + sb.append( "\n\n" ); + } + + return sb.toString(); + } + + @Override + public void receive( Message message ) + { + super.receive( message ); + + if( message instanceof LTRNetOSWMessage ) + { + mActivitySummary.receive( message ); + + LTRNetOSWMessage ltr = (LTRNetOSWMessage)message; + + Alias talkgroupAlias = ltr.getTalkgroupIDAlias(); + + switch( ltr.getMessageType() ) + { + case CA_STRT: + mToTalkgroup = ltr.getTalkgroupID(); + broadcastChange( ChangedAttribute.TO_TALKGROUP ); + + if( talkgroupAlias != null ) + { + mToTalkgroupAlias = talkgroupAlias; + broadcastChange( ChangedAttribute.TO_TALKGROUP_ALIAS ); + } + + mChannelNumber = ltr.getChannel(); + broadcastChange( ChangedAttribute.CHANNEL_NUMBER ); + + if( getState() != ChannelState.State.CALL ) + { + mCallEventModel.add( + new LTRCallEvent.Builder( CallEventType.CALL_START ) + .aliasList( mAliasList ) + .channel( ltr.getChannel() ) + .frequency( mProcessingChain.getChannel() + .getTunerChannel().getFrequency() ) + .to( mToTalkgroup ) + .build() ); + } + + setState( State.CALL ); + break; + case CA_ENDD: + mToTalkgroup = ltr.getTalkgroupID(); + broadcastChange( ChangedAttribute.TO_TALKGROUP ); + + if( talkgroupAlias != null ) + { + mToTalkgroupAlias = talkgroupAlias; + broadcastChange( ChangedAttribute.TO_TALKGROUP_ALIAS ); + } + + fade( CallEventType.CALL_END ); + break; + case ID_UNIQ: + mFromTalkgroupType = "UID"; + broadcastChange( ChangedAttribute.FROM_TALKGROUP_TYPE ); + + mFromTalkgroup = String.valueOf( ltr.getRadioUniqueID() ); + broadcastChange( ChangedAttribute.FROM_TALKGROUP ); + + mFromTalkgroupAlias = ltr.getRadioUniqueIDAlias(); + broadcastChange( ChangedAttribute.FROM_TALKGROUP_ALIAS ); + + mCallEventModel.add( + new LTRCallEvent.Builder( CallEventType.REGISTER ) + .aliasList( mAliasList ) + .details( "Radio Unique ID" ) + .frequency( mProcessingChain.getChannel() + .getTunerChannel().getFrequency() ) + .from( String.valueOf( ltr.getRadioUniqueID() ) ) + .build() ); + + setState( State.CALL ); + default: + break; + } + + } + else if( message instanceof LTRNetISWMessage ) + { + mActivitySummary.receive( message ); + + LTRNetISWMessage ltr = (LTRNetISWMessage)message; + + Alias talkgroupAlias = ltr.getTalkgroupIDAlias(); + + switch( ltr.getMessageType() ) + { + case CA_STRT: + mToTalkgroup = ltr.getTalkgroupID(); + broadcastChange( ChangedAttribute.TO_TALKGROUP ); + + if( talkgroupAlias != null ) + { + mToTalkgroupAlias = talkgroupAlias; + broadcastChange( ChangedAttribute.TO_TALKGROUP_ALIAS ); + } + + if( getState() != State.CALL ) + { + mCurrentCallEvent = + new LTRCallEvent.Builder( CallEventType.CALL_START ) + .aliasList( mAliasList ) + .channel( ltr.getChannel() ) + .frequency( mProcessingChain.getChannel() + .getTunerChannel().getFrequency() ) + .to( mToTalkgroup ) + .build(); + + mCallEventModel.add( mCurrentCallEvent ); + } + setState( State.CALL ); + break; + case CA_ENDD: + mToTalkgroup = ltr.getTalkgroupID(); + broadcastChange( ChangedAttribute.TO_TALKGROUP ); + + if( talkgroupAlias != null ) + { + mToTalkgroupAlias = talkgroupAlias; + broadcastChange( ChangedAttribute.TO_TALKGROUP_ALIAS ); + } + fade( CallEventType.CALL_END ); + break; + case ID_UNIQ: + mFromTalkgroupType = "UID"; + broadcastChange( ChangedAttribute.FROM_TALKGROUP_TYPE ); + + mFromTalkgroup = String.valueOf( ltr.getRadioUniqueID() ); + broadcastChange( ChangedAttribute.FROM_TALKGROUP ); + + mFromTalkgroupAlias = ltr.getRadioUniqueIDAlias(); + broadcastChange( ChangedAttribute.FROM_TALKGROUP_ALIAS ); + + if( getState() != State.CALL ) + { + mCallEventModel.add( new LTRCallEvent.Builder( + CallEventType.ID_UNIQUE ) + .aliasList( mAliasList ) + .details( "Unique ID" ) + .frequency( mProcessingChain.getChannel() + .getTunerChannel().getFrequency() ) + .from( String.valueOf( ltr.getRadioUniqueID() ) ) + .build() ); + } + else + { + if( mCurrentCallEvent != null ) + { + mCurrentCallEvent.setFromID( + String.valueOf( ltr.getRadioUniqueID() ) ); + mCurrentCallEvent.setDetails( "Unique ID" ); + } + } + setState( State.CALL ); + break; + case ID_ESNH: + case ID_ESNL: + mFromTalkgroupType = "ESN"; + broadcastChange( ChangedAttribute.FROM_TALKGROUP_TYPE ); + + mFromTalkgroup = ltr.getESN(); + broadcastChange( ChangedAttribute.FROM_TALKGROUP ); + + mFromTalkgroupAlias = ltr.getESNAlias(); + broadcastChange( ChangedAttribute.FROM_TALKGROUP_ALIAS ); + + setState( State.CALL ); + + mCallEventModel.add( + new LTRCallEvent.Builder( CallEventType.REGISTER_ESN ) + .aliasList( mAliasList ) + .details( "ESN:" + ltr.getESN() ) + .frequency( mProcessingChain.getChannel() + .getTunerChannel().getFrequency() ) + .from( ltr.getESN() ) + .build() ); + break; + default: + break; + } + + } + } + + public void reset() + { + mToTalkgroup = null; + broadcastChange( ChangedAttribute.TO_TALKGROUP ); + + mToTalkgroupAlias = null; + broadcastChange( ChangedAttribute.TO_TALKGROUP_ALIAS ); + + mFromTalkgroup = null; + broadcastChange( ChangedAttribute.FROM_TALKGROUP ); + + mFromTalkgroupAlias = null; + broadcastChange( ChangedAttribute.FROM_TALKGROUP_ALIAS ); + + mFromTalkgroupType = " "; + broadcastChange( ChangedAttribute.FROM_TALKGROUP_TYPE ); + + super.reset(); + } + + public String getToTalkgroup() + { + return mToTalkgroup; + } + + public Alias getToTalkgroupAlias() + { + return mToTalkgroupAlias; + } + + public String getFromTalkgroup() + { + return mFromTalkgroup; + } + + public Alias getFromTalkgroupAlias() + { + return mFromTalkgroupAlias; + } + + public String getFromTalkgroupType() + { + return mFromTalkgroupType; + } + + public boolean hasChannelNumber() + { + return mChannelNumber != 0; + } + + public int getChannelNumber() + { + return mChannelNumber; + } +} diff --git a/src/decode/ltrnet/LTRNetDecoder.java b/src/decode/ltrnet/LTRNetDecoder.java new file mode 100644 index 00000000..23c0b07b --- /dev/null +++ b/src/decode/ltrnet/LTRNetDecoder.java @@ -0,0 +1,118 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package decode.ltrnet; + +import message.MessageDirection; +import source.Source.SampleType; +import alias.AliasList; +import bits.MessageFramer; +import bits.SyncPattern; +import decode.Decoder; +import decode.DecoderType; +import decode.ltrstandard.LTRFSKDemodulator; +import dsp.filter.DCRemovalFilter2; +import dsp.nbfm.FilteringNBFMDemodulator; + +public class LTRNetDecoder extends Decoder +{ + /** + * This value determines how quickly the DC remove filter responds to + * changes in frequency. + */ + private static final double sDC_REMOVAL_RATIO = 0.000003; + + public static final int sLTR_STANDARD_MESSAGE_LENGTH = 40; + private FilteringNBFMDemodulator mDemodulator; + private DCRemovalFilter2 mDCRemovalFilter; + private LTRFSKDemodulator mLTRFSKDemodulator; + private MessageFramer mLTRMessageFramer; + private LTRNetMessageProcessor mLTRMessageProcessor; + + public LTRNetDecoder( SampleType sampleType, + AliasList aliasList, + MessageDirection direction ) + { + super( sampleType ); + + /** + * Only setup a demod chain if we're receiving complex samples. If + * we're receiving demodulated samples, they'll be handled the same + * was as we handle the output of the demodulator. + */ + if( mSourceSampleType == SampleType.COMPLEX ) + { + /** + * The Decoder super class is both a Complex listener and a Float + * listener. So, we add the demod to listen to the incoming + * quadrature samples, and we wire the output of the demod right + * back to this class, so we can receive the demodulated output + * to process + */ + mDemodulator = new FilteringNBFMDemodulator(); + this.addComplexListener( mDemodulator ); + + /** + * Remove the DC component that is present when we're mistuned + */ + mDCRemovalFilter = new DCRemovalFilter2( sDC_REMOVAL_RATIO ); + mDemodulator.addListener( mDCRemovalFilter ); + + mDCRemovalFilter.setListener( this.getFloatReceiver() ); + } + + mLTRFSKDemodulator = new LTRFSKDemodulator(); + + /** + * The DC removal filter will impact performance of the LTRFSKDemod, + * so we get the samples straight from the NBFM demod + */ + if( mSourceSampleType == SampleType.COMPLEX ) + { + mDemodulator.addListener( mLTRFSKDemodulator ); + } + else + { + mDCRemovalFilter.setListener( mLTRFSKDemodulator ); + } + + if( direction == MessageDirection.OSW ) + { + mLTRMessageFramer = + new MessageFramer( SyncPattern.LTR_STANDARD_OSW.getPattern(), + sLTR_STANDARD_MESSAGE_LENGTH ); + } + else + { + mLTRMessageFramer = + new MessageFramer( SyncPattern.LTR_STANDARD_ISW.getPattern(), + sLTR_STANDARD_MESSAGE_LENGTH ); + } + + mLTRFSKDemodulator.addListener( mLTRMessageFramer ); + + mLTRMessageProcessor = new LTRNetMessageProcessor( direction, aliasList ); + mLTRMessageFramer.addMessageListener( mLTRMessageProcessor ); + mLTRMessageProcessor.addMessageListener( this ); + } + + @Override + public DecoderType getType() + { + return DecoderType.LTR_NET; + } +} diff --git a/src/decode/ltrnet/LTRNetEditor.java b/src/decode/ltrnet/LTRNetEditor.java new file mode 100644 index 00000000..39ae0eb5 --- /dev/null +++ b/src/decode/ltrnet/LTRNetEditor.java @@ -0,0 +1,70 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package decode.ltrnet; + +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; + +import javax.swing.DefaultComboBoxModel; +import javax.swing.JComboBox; + +import message.MessageDirection; +import decode.DecodeEditor; +import decode.config.DecodeConfigLTRNet; +import decode.config.DecodeConfiguration; + +public class LTRNetEditor extends DecodeEditor +{ + private static final long serialVersionUID = 1L; + + private JComboBox mComboDirection; + + public LTRNetEditor( DecodeConfiguration config ) + { + super( config ); + + initGUI(); + } + + private void initGUI() + { + mComboDirection = new JComboBox(); + + mComboDirection.setModel( + new DefaultComboBoxModel( MessageDirection.values() ) ); + + mComboDirection.setSelectedItem( ((DecodeConfigLTRNet)mConfig).getMessageDirection() ); + + mComboDirection.addActionListener( new ActionListener() + { + @Override + public void actionPerformed( ActionEvent e ) + { + MessageDirection selected = mComboDirection + .getItemAt( mComboDirection.getSelectedIndex() ); + + if( selected != null ) + { + ((DecodeConfigLTRNet)mConfig).setMessageDirection( selected ); + } + } + } ); + + add( mComboDirection, "span 2,wrap" ); + } +} diff --git a/src/decode/ltrnet/LTRNetISWMessage.java b/src/decode/ltrnet/LTRNetISWMessage.java new file mode 100644 index 00000000..ded45ebc --- /dev/null +++ b/src/decode/ltrnet/LTRNetISWMessage.java @@ -0,0 +1,312 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package decode.ltrnet; + +import java.util.Date; + +import map.Plottable; +import message.MessageDirection; +import message.MessageType; +import alias.Alias; +import alias.AliasList; +import bits.BitSetBuffer; +import crc.CRC; +import crc.CRCLTR; + +public class LTRNetISWMessage extends LTRNetMessage +{ + private static final int sCHIU_UNIQUE_ID = 24; + private static final int sCHIU_ESN_LOW = 27; + private static final int sCHIU_ESN_HIGH = 29; + + private static final int sFREE_CALL = 21; + private static final int sFREE_END_CALL = 23; + private static final int sFREE_REQUEST_ACCESS = 31; + + + private LTRNetISWMessage mAuxMessage; + + public LTRNetISWMessage( BitSetBuffer message, AliasList list ) + { + super( message, MessageDirection.ISW, list ); + + /** + * If the CRC fails, test for a transmitted CRC of 127 and then check + * the Free field for special messages + */ + if( mCRC == CRC.FAILED_CRC && CRCLTR.getTransmittedChecksum( mMessage ) == 127 ) + { + if( getFree() == sFREE_REQUEST_ACCESS || + getFree() == sFREE_END_CALL ) + { + mCRC = CRC.PASSED; + } + } + + mMessageType = getMessageType(); + } + + public void setAuxiliaryMessage( LTRNetISWMessage message ) + { + if( message != null ) + { + mAuxMessage = message; + } + } + + public String getESN() + { + if( mMessageType == MessageType.ID_ESNH ) + { + if( mAuxMessage != null && + mAuxMessage.getMessageType() == MessageType.ID_ESNL ) + { + return getESNHigh() + mAuxMessage.getESNLow(); + } + else + { + return getESNHigh() + "xxxx"; + } + } + else + { + if( mAuxMessage != null && + mAuxMessage.getMessageType() == MessageType.ID_ESNH ) + { + return mAuxMessage.getESNHigh() + getESNLow(); + } + else + { + return "xxxx" + getESNLow(); + } + } + } + + public Alias getESNAlias() + { + if( mAliasList != null ) + { + return mAliasList.getESNAlias( getESN() ); + } + + return null; + } + + public String getESNHigh() + { + int esnHigh = getInt( sSIXTEEN_BITS ); + + return String.format("%04X", esnHigh & 0xFFFF ); + } + + public String getESNLow() + { + int esnLow = getInt( sSIXTEEN_BITS ); + + return String.format("%04X", esnLow & 0xFFFF ); + } + + @Override + public String toString() + { + StringBuilder sb = new StringBuilder(); + sb.append( mDatestampFormatter.format( new Date( System.currentTimeMillis() ) ) ); + sb.append( " LTRNet ISW [" ); + sb.append( mCRC.getAbbreviation() ); + sb.append( "] " ); + sb.append( getMessage() ); + pad( sb, 100 ); + sb.append( mMessage.toString() ); + + return sb.toString(); + } + + @Override + public String getMessage() + { + StringBuilder sb = new StringBuilder(); + + switch( mMessageType ) + { + case CA_STRT: + sb.append( "CALL LCN:" ); + sb.append( format( getChannel(), 2 ) ); + sb.append( " TG [ " ); + sb.append( getTalkgroupID() ); + sb.append( "/" ); + sb.append( getTalkgroupIDAlias() ); + sb.append( " ] FREE:" ); + sb.append( format( getFree(), 2 ) ); + break; + case CA_ENDD: + sb.append( "END* AREA:" ); + sb.append( getArea() ); + sb.append( " LCN:" ); + sb.append( format( getChannel(), 2 ) ); + sb.append( " TG [" ); + sb.append( getTalkgroupID() ); + sb.append( "/" ); + sb.append( getTalkgroupIDAlias() ); + sb.append( "] FREE:" ); + sb.append( format( getFree(), 2 ) ); + break; + case RQ_ACCE: + sb.append( "REQUEST ACCESS LCN:" ); + sb.append( format( getChannel(), 2 ) ); + sb.append( " TG [ " ); + sb.append( getTalkgroupID() ); + sb.append( "/" ); + sb.append( getTalkgroupIDAlias() ); + sb.append( " ] FREE:" ); + sb.append( format( getFree(), 2 ) ); + break; + case ID_ESNL: + sb.append( "ESN LOW " ); + sb.append( getESN() ); + sb.append( "/" ); + sb.append( getESNAlias() ); + break; + case ID_ESNH: + sb.append( "ESN HIGH " ); + sb.append( getESN() ); + sb.append( "/" ); + sb.append( getESNAlias() ); + break; + case ID_UNIQ: + sb.append( "RADIO UNIQUE ID " ); + sb.append( getRadioUniqueID() ); + + if( getRadioUniqueIDAlias() != null ) + { + sb.append( "/" ); + sb.append( getRadioUniqueIDAlias().getName() ); + } + break; + default: + sb.append( "UNKNOWN " ); + sb.append( getArea() ); + sb.append( " " ); + sb.append( format( getChannel(), 2 ) ); + sb.append( " " ); + sb.append( format( getHomeRepeater(), 2 ) ); + sb.append( " " ); + sb.append( format( getGroup(), 3 ) ); + sb.append( " " ); + sb.append( format( getFree(), 2 ) ); + sb.append( " CRC:" ); + sb.append( CRCLTR.getTransmittedChecksum( mMessage ) ); + sb.append( " CALC:" ); + sb.append( CRCLTR.getCalculatedChecksum( mMessage ) ); + } + + return sb.toString(); + } + + public int getRadioUniqueID() + { + int retVal = sINT_NULL_VALUE; + + if( mMessageType == MessageType.ID_UNIQ ) + { + retVal = getInt( sSIXTEEN_BITS ); + } + + return retVal; + } + + public Alias getRadioUniqueIDAlias() + { + if( mAliasList != null ) + { + return mAliasList.getUniqueID( getRadioUniqueID() ); + } + + return null; + } + + public MessageType getMessageType() + { + MessageType retVal = MessageType.UN_KNWN; + + int channel = getChannel(); + + if( channel == 31 ) + { + retVal = MessageType.CA_ENDD; + } + else if( channel > 20 ) + { + switch( channel ) + { + case sCHIU_UNIQUE_ID: + retVal = MessageType.ID_UNIQ; + break; + case sCHIU_ESN_LOW: + retVal = MessageType.ID_ESNH; + break; + case sCHIU_ESN_HIGH: + retVal = MessageType.ID_ESNL; + break; + } + } + else if( channel > 0 ) + { + int free = getFree(); + + switch( free ) + { + case sFREE_CALL: + retVal = MessageType.CA_STRT; + break; + case sFREE_END_CALL: + retVal = MessageType.CA_ENDD; + break; + case sFREE_REQUEST_ACCESS: + retVal = MessageType.RQ_ACCE; + break; + } + } + + return retVal; + } + @Override + public String getFromID() + { + return null; + } + + @Override + public Alias getFromIDAlias() + { + return null; + } + + @Override + public void setAuxiliaryMessage( LTRNetOSWMessage message ) + { + // TODO Auto-generated method stub + + } + + @Override + public Plottable getPlottable() + { + // TODO Auto-generated method stub + return null; + } +} diff --git a/src/decode/ltrnet/LTRNetMessage.java b/src/decode/ltrnet/LTRNetMessage.java new file mode 100644 index 00000000..8e86f3f8 --- /dev/null +++ b/src/decode/ltrnet/LTRNetMessage.java @@ -0,0 +1,243 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package decode.ltrnet; + +import java.text.DecimalFormat; +import java.text.SimpleDateFormat; + +import message.Message; +import message.MessageDirection; +import message.MessageType; +import alias.Alias; +import alias.AliasList; +import bits.BitSetBuffer; +import crc.CRC; +import crc.CRCLTR; +import decode.DecoderType; + +public abstract class LTRNetMessage extends Message +{ + protected static final String sUNKNOWN = "**UNKNOWN**"; + protected static final int sINT_NULL_VALUE = -1; + protected static final double sDOUBLE_NULL_VALUE = -1.0D; + protected SimpleDateFormat mDatestampFormatter = + new SimpleDateFormat( "yyyyMMdd HHmmss" ); + protected DecimalFormat mDecimalFormatter = new DecimalFormat("0.00000"); + + protected static final int[] sSYNC = { 8,7,6,5,4,3,2,1,0 }; + protected static final int[] sAREA = { 9 }; + protected static final int[] sCHANNEL = { 14,13,12,11,10 }; + protected static final int[] sHOME_REPEATER = { 19,18,17,16,15 }; + protected static final int[] sGROUP = { 27,26,25,24,23,22,21,20 }; + protected static final int[] sFREE = { 32,31,30,29,28 }; + protected static final int[] sCRC = { 39,38,37,36,35,34,33 }; + protected static final int[] sSIXTEEN_BITS = { 32,31,30,29,28,27,26,25,24,23,22,21,20,19,18,17 }; + protected static final int[] sMANUFACTURER_ID = { 23,22,21,20,19,18,17 }; + + protected BitSetBuffer mMessage; + protected MessageType mMessageType; + protected CRC mCRC; + protected AliasList mAliasList; + + public LTRNetMessage( BitSetBuffer message, + MessageDirection direction, + AliasList list ) + { + mMessage = message; + mCRC = CRCLTR.check( message, direction ); + mAliasList = list; + } + + public MessageType getMessageType() + { + return mMessageType; + } + + public abstract void setAuxiliaryMessage( LTRNetOSWMessage message ); + + public boolean isValid() + { + return mCRC != CRC.FAILED_CRC && mCRC != CRC.FAILED_PARITY; + } + + /** + * Appends spaces to the end of the stringbuilder to make it length long + */ + protected void pad( StringBuilder sb, int length ) + { + while( sb.length() < length ) + { + sb.append( " " ); + } + } + + @Override + public String getBinaryMessage() + { + return mMessage.toString(); + } + + /** + * Returns a value formatted in hex notation. + * + * @param value - integer value + * @return - hex digits returned + */ + protected static String getHex( int value, int digits ) + { + return String.format( "%0" + digits + "X", value ); + } + + protected int getInt( int[] bits ) + { + int retVal = 0; + + for( int x = 0; x < bits.length; x++ ) + { + if( mMessage.get( bits[ x ] ) ) + { + retVal += 1< + ******************************************************************************/ +package decode.ltrnet; + +import java.util.HashMap; + +import message.Message; +import message.MessageDirection; +import message.MessageType; +import sample.Broadcaster; +import sample.Listener; +import alias.AliasList; +import bits.BitSetBuffer; + +public class LTRNetMessageProcessor implements Listener +{ + private Broadcaster mBroadcaster = new Broadcaster(); + private HashMap mReceiveAuxMessages = + new HashMap(); + private HashMap mTransmitAuxMessages = + new HashMap(); + private LTRNetISWMessage mESNHighMessage; + private LTRNetISWMessage mESNLowMessage; + + private MessageDirection mDirection; + private AliasList mAliasList; + + public LTRNetMessageProcessor( MessageDirection direction, + AliasList list ) + { + mDirection = direction; + mAliasList = list; + } + + @Override + public void receive( BitSetBuffer buffer ) + { + LTRNetMessage message; + + if( mDirection == MessageDirection.OSW ) + { + message = new LTRNetOSWMessage( buffer, mAliasList ); + + if( message.isValid() ) + { + if( message.getMessageType() == MessageType.FQ_TXHI ) + { + message.setAuxiliaryMessage( + mTransmitAuxMessages.get( message.getHomeRepeater() ) ); + } + else if( message.getMessageType() == MessageType.FQ_TXLO ) + { + mTransmitAuxMessages.put( message.getHomeRepeater(), + (LTRNetOSWMessage)message ); + } + else if( message.getMessageType() == MessageType.FQ_RXHI ) + { + message.setAuxiliaryMessage( + mReceiveAuxMessages.get( message.getHomeRepeater() ) ); + } + else if( message.getMessageType() == MessageType.FQ_RXLO ) + { + mReceiveAuxMessages.put( message.getHomeRepeater(), + (LTRNetOSWMessage)message ); + } + } + } + else + { + buffer.flip( 0, 40 ); + + message = new LTRNetISWMessage( buffer, mAliasList ); + + if( message.isValid() ) + { + /** + * Catch ESN High messages to marry up with ESN Low messages. Purge + * the ESN high message after 2 messages, so that it doesn't get + * paired with the wrong esn low message + */ + if( message.getMessageType() == MessageType.ID_ESNH ) + { + mESNHighMessage = (LTRNetISWMessage)message; + + if( mESNLowMessage != null ) + { + /* Check that the stored message is not older than + * 5 seconds */ + if( System.currentTimeMillis() - + mESNLowMessage.getTimeReceived() < 5000 ) + { + ((LTRNetISWMessage)message).setAuxiliaryMessage( mESNLowMessage ); + } + else + { + mESNLowMessage = null; + } + } + } + else if( message.getMessageType() == MessageType.ID_ESNL ) + { + mESNLowMessage = (LTRNetISWMessage)message; + + if( mESNHighMessage != null ) + { + /* Check that the stored message is not older than 5 seconds */ + if( System.currentTimeMillis() - mESNHighMessage.getTimeReceived() < 5000 ) + { + ((LTRNetISWMessage)message).setAuxiliaryMessage( mESNHighMessage ); + } + else + { + mESNHighMessage = null; + } + } + } + } + } + + mBroadcaster.receive( message ); + } + + public void addMessageListener( Listener listener ) + { + mBroadcaster.addListener( listener ); + } + + public void removeMessageListener( Listener listener ) + { + mBroadcaster.removeListener( listener ); + } +} diff --git a/src/decode/ltrnet/LTRNetOSWMessage.java b/src/decode/ltrnet/LTRNetOSWMessage.java new file mode 100644 index 00000000..76bb899d --- /dev/null +++ b/src/decode/ltrnet/LTRNetOSWMessage.java @@ -0,0 +1,579 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package decode.ltrnet; + +import java.util.ArrayList; +import java.util.Date; + +import map.Plottable; +import message.MessageDirection; +import message.MessageType; +import alias.Alias; +import alias.AliasList; +import bits.BitSetBuffer; + +public class LTRNetOSWMessage extends LTRNetMessage +{ + private static final int sCHANNEL_FREQUENCY_MESSAGE_TYPE_BIT = 20; + private static final int sCHANNEL_MAP_MESSAGE_TYPE_BIT = 17; + + private static final int sCHIU_UNIQUE_ID = 17; + private static final int sCHIU_SITE_ID = 18; + private static final int sCHIU_TRANSMIT_FREQUENCY = 24; + private static final int sCHIU_RECEIVE_FREQUENCY = 25; + private static final int sCHIU_NEIGHBOR = 26; + private static final int sCHIU_MAP = 28; + private static final int sCHIU_CALL_END = 31; + + private static final int sHOME_DIRECTED_GROUP_CALL = 29; + private static final int sHOME_CHANNEL_IN_USE_MUTE_AUDIO = 30; + + private static final int sGROUP_DO_NOTHING = 253; + private static final int sGROUP_CWID = 254; + private static final int sGROUP_CHANNEL_IDLE = 255; + + private static final int sFREE_ALL_CHANNELS_BUSY = 0; + private static final int sFREE_LTR_GROUP_CALL = 30; + private static final int sFREE_LTRNET_GROUP_CALL = 31; + + private LTRNetOSWMessage mAuxMessage; + + public LTRNetOSWMessage( BitSetBuffer message, AliasList list ) + { + super( message, MessageDirection.OSW, list ); + mMessageType = getType(); + } + + public void setAuxiliaryMessage( LTRNetOSWMessage message ) + { + if( message != null ) + { + mAuxMessage = message; + } + } + + public String getRadioUniqueIDText() + { + StringBuilder sb = new StringBuilder(); + + int radioId = getRadioUniqueID(); + + if( radioId != sINT_NULL_VALUE ) + { + sb.append( " RADIO UNIQUE ID: " ); + sb.append( radioId ); + } + + return sb.toString(); + } + + @Override + public String toString() + { + StringBuilder sb = new StringBuilder(); + sb.append( mDatestampFormatter.format( + new Date( System.currentTimeMillis() ) ) ); + sb.append( " LTRNet OSW [" ); + sb.append( mCRC.getAbbreviation() ); + sb.append( "] " ); + sb.append( getMessage() ); + pad( sb, 100 ); + sb.append( mMessage.toString() ); + + return sb.toString(); + } + + @Override + public String getMessage() + { + StringBuilder sb = new StringBuilder(); + + switch( mMessageType ) + { + case SY_IDLE: + sb.append( "IDLE AREA:" ); + sb.append( getArea() ); + sb.append( " LCN:" ); + sb.append( format( getChannel(), 2 ) ); + sb.append( " HOME:" ); + sb.append( format( getHomeRepeater(), 2 ) ); + sb.append( " GRP:" ); + sb.append( format( getGroup(), 3 ) ); + sb.append( " FREE:" ); + sb.append( format( getFree(), 2 ) ); + break; + case CA_STRT: + sb.append( "CALL LCN:" ); + sb.append( format( getChannel(), 2 ) ); + sb.append( " TG [ " ); + sb.append( getTalkgroupID() ); + sb.append( "/" ); + sb.append( getTalkgroupIDAlias() ); + sb.append( " ] FREE:" ); + sb.append( format( getFree(), 2 ) ); + break; + case CA_ENDD: + sb.append( "END* AREA:" ); + sb.append( getArea() ); + sb.append( " LCN:" ); + sb.append( format( getChannel(), 2 ) ); + sb.append( " TG [" ); + sb.append( getTalkgroupID() ); + sb.append( "/" ); + sb.append( getTalkgroupIDAlias() ); + sb.append( "] FREE:" ); + sb.append( format( getFree(), 2 ) ); + break; + case MA_CHNL: + sb.append( "CHAN MAP LOW LCN [" ); + sb.append( getChannelMapAsString( getChannelMapLow() ) ); + sb.append( "]" ); + break; + case MA_CHNH: + sb.append( "CHAN MAP HIGH LCN [" ); + sb.append( getChannelMapAsString( getChannelMapHigh() ) ); + sb.append( "]" ); + break; + case FQ_RXLO: + sb.append( "CHAN RX LOW LCN:" ); + sb.append( format( getHomeRepeater(), 2 ) ); + sb.append( " = " ); + double rxfreqhi = getFrequency(); + if( rxfreqhi != sDOUBLE_NULL_VALUE ) + { + sb.append( mDecimalFormatter.format( rxfreqhi ) ); + } + else + { + sb.append( "000.00000" ); + } + sb.append( " MHz" ); + break; + case FQ_RXHI: + sb.append( "CHAN RX HIGH LCN:" ); + sb.append( format( getHomeRepeater(), 2 ) ); + sb.append( " MSG 1 OF 2" ); + break; + case FQ_TXHI: + sb.append( "CHAN TX LOW LCN:" ); + sb.append( format( getHomeRepeater(), 2 ) ); + sb.append( " = " ); + double txfreq = getFrequency(); + if( txfreq != sDOUBLE_NULL_VALUE ) + { + sb.append( mDecimalFormatter.format( txfreq ) ); + } + else + { + sb.append( "000.00000" ); + } + sb.append( " MHz" ); + break; + case FQ_TXLO: + sb.append( "CHAN TX HIGH LCN:" ); + sb.append( format( getHomeRepeater(), 2 ) ); + sb.append( " MSG 1 OF 2" ); + break; + case ID_SITE: + int siteId = getSiteID(); + if( siteId != sINT_NULL_VALUE ) + { + sb.append( "SITE ID: " ); + sb.append( siteId ); + sb.append( " " ); + sb.append( getSiteIDAlias() ); + } + break; + case ID_NBOR: + int neighborRank = getNeighborRank(); + int neighborId = getNeighborID(); + + if( neighborRank != sINT_NULL_VALUE && + neighborId != sINT_NULL_VALUE ) + { + sb.append( "NEIGHBOR SITE " ); + sb.append( neighborRank ); + sb.append( ": " ); + sb.append( neighborId ); + sb.append( " " ); + sb.append( getNeighborIDAlias() ); + } + break; + case ID_UNIQ: + sb.append( "REGISTER RADIO UNIQUE ID:" ); + sb.append( getRadioUniqueID() ); + break; + default: + sb.append( "UNKNOWN " ); + sb.append( getArea() ); + sb.append( " " + format( getChannel(), 2 ) ); + sb.append( " " + format( getHomeRepeater(), 2 ) ); + sb.append( " " + format( getGroup(), 3 ) ); + sb.append( " " + format( getFree(), 2 ) ); + sb.append( " A:" ); + sb.append( getArea() ); + sb.append( " C:" ); + sb.append( format( getChannel(), 2 ) ); + sb.append( " H:" ); + sb.append( format( getHomeRepeater(), 2 ) ); + sb.append( " G:" ); + sb.append( format( getGroup(), 3 ) ); + sb.append( " F:" ); + sb.append( format( getFree(), 2 ) ); + } + + return sb.toString(); + } + + public String getTalkgroupID() + { + return getTalkgroupID( true ); + } + + public String getTalkgroupID( boolean format ) + { + StringBuilder sb = new StringBuilder(); + + sb.append( getArea() ); + + if( format ) + { + sb.append( "-" ); + } + + sb.append( format( getHomeRepeater(), 2 ) ); + + if( format ) + { + sb.append( "-" ); + } + + sb.append( format( getGroup(), 3 ) ); + + return sb.toString(); + } + + public Alias getTalkgroupIDAlias() + { + if( mAliasList != null ) + { + return mAliasList.getTalkgroupAlias( getTalkgroupID() ); + } + + return null; + } + + public ArrayList getChannelMapLow() + { + ArrayList retVal = new ArrayList(); + + if( mMessageType == MessageType.MA_CHNL ) + { + for( int x = 27; x >= 18; x-- ) + { + if( mMessage.get( x ) ) + { + retVal.add( 28 - x ); + } + } + } + + return retVal; + } + + public ArrayList getChannelMapHigh() + { + ArrayList retVal = new ArrayList(); + + if( mMessageType == MessageType.MA_CHNH ) + { + for( int x = 27; x >= 18; x-- ) + { + if( mMessage.get( x ) ) + { + retVal.add( 38 - x ); + } + } + } + + return retVal; + } + + public String getChannelMapAsString( ArrayList channels ) + { + StringBuilder sb = new StringBuilder(); + + for( Integer channel: channels ) + { + sb.append( channel ); + sb.append( " " ); + } + + return sb.toString(); + } + + public int getRadioUniqueID() + { + int retVal = sINT_NULL_VALUE; + + if( mMessageType == MessageType.ID_UNIQ ) + { + retVal = mMessage.getInt( 17, 32 ); + } + + return retVal; + } + + public Alias getRadioUniqueIDAlias() + { + if( mAliasList != null ) + { + return mAliasList.getUniqueID( getRadioUniqueID() ); + } + + return null; + } + + public int getSiteID() + { + int retVal = sINT_NULL_VALUE; + + if( mMessageType == MessageType.ID_SITE ) + { + retVal = mMessage.getInt( 23, 32 ); + } + + return retVal; + } + + public Alias getSiteIDAlias() + { + if( mAliasList != null ) + { + return mAliasList.getSiteID( getSiteID() ); + } + + return null; + } + + public int getNeighborID() + { + int retVal = sINT_NULL_VALUE; + + if( mMessageType == MessageType.ID_NBOR ) + { + retVal = mMessage.getInt( 23, 32 ); + } + + return retVal; + } + + private Alias getNeighborIDAlias() + { + if( mAliasList != null ) + { + return mAliasList.getSiteID( getNeighborID() ); + } + + return null; + } + + public int getNeighborRank() + { + int retVal = sINT_NULL_VALUE; + + if( mMessageType == MessageType.ID_NBOR ) + { + retVal = mMessage.getInt( 15, 18 ) + 1; + } + + return retVal; + } + + public double getFrequency() + { + double retVal = sDOUBLE_NULL_VALUE; + + if( mAuxMessage != null ) + { + int high = getHighChannelUnits(); + int low = mAuxMessage.getLowChannelUnits(); + + if( high != sINT_NULL_VALUE && low != sINT_NULL_VALUE ) + { + retVal = ( high + low ) * .00125 + 150.0; + } + } + + return retVal; + } + + /** + * Returns the value of the high order (15 - 12) frequency bits which are + * the lower 4 bits of the Free field, left shifted by 12, representing the + * frequency value in units of .00125 MHz + */ + public int getHighChannelUnits() + { + int retVal = sINT_NULL_VALUE; + + if( mAuxMessage != null && + ( mMessageType == MessageType.FQ_RXHI || + mMessageType == MessageType.FQ_TXHI ) ) + { + retVal = mMessage.getInt( 29, 32 ); + + retVal = Integer.rotateLeft( retVal, 12 ); + } + + return retVal; + } + + /** + * Returns the value of the low order (11 - 0) frequency bits which are + * contained in the Group and Free fields representing the frequency value + * units of .00125 MHz + */ + public int getLowChannelUnits() + { + int retVal = sINT_NULL_VALUE; + + if( mMessageType == MessageType.FQ_RXLO || + mMessageType == MessageType.FQ_TXLO ) + { + retVal = mMessage.getInt( 21, 32 ); + } + + return retVal; + } + + public MessageType getType() + { + MessageType retVal = MessageType.UN_KNWN; + + int channel = getChannel(); + + int home = getHomeRepeater(); + + //LTR-Net messages + if( home != 31 && ( channel > 20 || home > 20 ) ) + { + switch( channel ) + { + case sCHIU_UNIQUE_ID: + retVal = MessageType.ID_UNIQ; + break; + case sCHIU_SITE_ID: + retVal = MessageType.ID_SITE; + break; + case sCHIU_TRANSMIT_FREQUENCY: + if( getHomeRepeater() == 24 ) + { + retVal = MessageType.ID_UNIQ; + } + else + { + if( mMessage.get( sCHANNEL_FREQUENCY_MESSAGE_TYPE_BIT ) ) + { + retVal = MessageType.FQ_TXHI; + } + else + { + retVal = MessageType.FQ_TXLO; + } + } + break; + case sCHIU_RECEIVE_FREQUENCY: + if( mMessage.get( sCHANNEL_FREQUENCY_MESSAGE_TYPE_BIT ) ) + { + retVal = MessageType.FQ_RXHI; + } + else + { + retVal = MessageType.FQ_RXLO; + } + break; + case sCHIU_NEIGHBOR: + retVal = MessageType.ID_NBOR; + break; + case sCHIU_MAP: + if( mMessage.get( sCHANNEL_MAP_MESSAGE_TYPE_BIT ) ) + { + retVal = MessageType.MA_CHNH; + } + else + { + retVal = MessageType.MA_CHNL; + } + break; + case sCHIU_CALL_END: + retVal = MessageType.CA_ENDD; + } + } + else + { + int group = getGroup(); + + if( group == sGROUP_CHANNEL_IDLE ) + { + retVal = MessageType.SY_IDLE; + } + else + { + retVal = MessageType.CA_STRT; + } + } + + return retVal; + } + + @Override + public String getFromID() + { + if( mMessageType == MessageType.CA_STRT || + mMessageType == MessageType.CA_ENDD ) + { + return getTalkgroupID(); + } + else + { + return null; + } + } + + @Override + public Alias getFromIDAlias() + { + if( mMessageType == MessageType.CA_STRT || + mMessageType == MessageType.CA_ENDD ) + { + return getTalkgroupIDAlias(); + } + else + { + return null; + } + } + + @Override + public Plottable getPlottable() + { + // TODO Auto-generated method stub + return null; + } +} diff --git a/src/decode/ltrnet/LTRNetPanel.java b/src/decode/ltrnet/LTRNetPanel.java new file mode 100644 index 00000000..5d7e4a2c --- /dev/null +++ b/src/decode/ltrnet/LTRNetPanel.java @@ -0,0 +1,229 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package decode.ltrnet; + +import java.awt.EventQueue; + +import javax.swing.JLabel; + +import settings.ColorSetting; +import settings.Setting; +import alias.Alias; +import controller.channel.Channel; +import controller.state.ChannelState.ChangedAttribute; +import controller.state.ChannelStatePanel; + +public class LTRNetPanel extends ChannelStatePanel +{ + private static final long serialVersionUID = 1L; + + private JLabel mStateLabel; + private JLabel mSourceLabel; + private JLabel mChannelLabel; + + private JLabel mProtocol = new JLabel( "LTR-Net" ); + private JLabel mToTalkgroup = new JLabel(); + private JLabel mToTalkgroupAlias = new JLabel(); + + private JLabel mFromType = new JLabel( " " ); + private JLabel mFromTalkgroup = new JLabel(); + private JLabel mFromTalkgroupAlias = new JLabel(); + + public LTRNetPanel( Channel channel ) + { + super( channel ); + + init(); + } + + public void init() + { + mStateLabel = new JLabel( mChannel.getProcessingChain(). + getChannelState().getState().getDisplayValue() ); + mStateLabel.setFont( mFontDecoder ); + mStateLabel.setForeground( mColorLabelDecoder ); + + mSourceLabel = new JLabel( mChannel.getSourceConfiguration().getDescription() ); + mSourceLabel.setFont( mFontDetails ); + mSourceLabel.setForeground( mColorLabelDetails ); + + mChannelLabel = new JLabel( mChannel.getChannelDisplayName() ); + mChannelLabel.setFont( mFontDetails ); + mChannelLabel.setForeground( mColorLabelDetails ); + + mProtocol.setFont( mFontDecoder ); + mProtocol.setForeground( mColorLabelDecoder ); + + mToTalkgroup.setFont( mFontDecoder ); + mToTalkgroup.setForeground( mColorLabelDecoder ); + + mToTalkgroupAlias.setFont( mFontDecoder ); + mToTalkgroupAlias.setForeground( mColorLabelDecoder ); + + mFromType.setFont( mFontDecoder ); + mFromType.setForeground( mColorLabelDecoder ); + + mFromTalkgroup.setFont( mFontDecoder ); + mFromTalkgroup.setForeground( mColorLabelDecoder ); + + mFromTalkgroupAlias.setFont( mFontDecoder ); + mFromTalkgroupAlias.setForeground( mColorLabelDecoder ); + + add( mStateLabel ); + add( mSourceLabel ); + add( mChannelLabel, "wrap" ); + + add( mProtocol ); + add( mToTalkgroup ); + add( mToTalkgroupAlias, "wrap" ); + + add( mFromType ); + add( mFromTalkgroup ); + add( mFromTalkgroupAlias, "wrap" ); + } + + @Override + public void receive( final ChangedAttribute changedAttribute ) + { + EventQueue.invokeLater( new Runnable() + { + @Override + public void run() + { + LTRNetChannelState state = (LTRNetChannelState)mChannel + .getProcessingChain().getChannelState(); + + switch( changedAttribute ) + { + case CHANNEL_STATE: + mStateLabel.setText( state.getState().getDisplayValue() ); + break; + case SOURCE: + mSourceLabel.setText( mChannel + .getSourceConfiguration().getDescription() ); + break; + case CHANNEL_NAME: + case SITE_NAME: + case SYSTEM_NAME: + mChannelLabel.setText( mChannel.getChannelDisplayName() ); + break; + case FROM_TALKGROUP: + mFromTalkgroup.setText( state.getFromTalkgroup() ); + break; + case FROM_TALKGROUP_ALIAS: + Alias fromAlias = state.getFromTalkgroupAlias(); + + if( fromAlias != null ) + { + mFromTalkgroupAlias.setText( fromAlias.getName() ); + String iconNameString = fromAlias.getIconName(); + mFromTalkgroupAlias.setIcon( getSettingsManager() + .getImageIcon( iconNameString, 12 ) ); + } + else + { + mFromTalkgroupAlias.setText( null ); + mFromTalkgroupAlias.setIcon( null ); + } + break; + case FROM_TALKGROUP_TYPE: + mFromType.setText( state.getFromTalkgroupType() ); + break; + case TO_TALKGROUP: + mToTalkgroup.setText( state.getToTalkgroup() ); + break; + case TO_TALKGROUP_ALIAS: + Alias tgAlias = state.getToTalkgroupAlias(); + + if( tgAlias != null ) + { + mToTalkgroupAlias.setText( tgAlias.getName() ); + String iconNameString = tgAlias.getIconName(); + mToTalkgroupAlias.setIcon( getSettingsManager() + .getImageIcon( iconNameString, 12 ) ); + } + else + { + mToTalkgroupAlias.setText( null ); + mToTalkgroupAlias.setIcon( null ); + } + break; + default: + break; + } + + repaint(); + } + }); + } + + @Override + public void settingChanged( Setting setting ) + { + super.settingChanged( setting ); + + if( setting instanceof ColorSetting ) + { + ColorSetting colorSetting = (ColorSetting)setting; + + switch( colorSetting.getColorSettingName() ) + { + case CHANNEL_STATE_LABEL_DECODER: + if( mStateLabel != null ) + { + mStateLabel.setForeground( mColorLabelDecoder ); + } + if( mProtocol != null ) + { + mProtocol.setForeground( mColorLabelDecoder ); + } + if( mToTalkgroup != null ) + { + mToTalkgroup.setForeground( mColorLabelDecoder ); + } + if( mToTalkgroupAlias != null ) + { + mToTalkgroupAlias.setForeground( mColorLabelDecoder ); + } + if( mFromType != null ) + { + mFromType.setForeground( mColorLabelDecoder ); + } + if( mFromTalkgroup != null ) + { + mFromTalkgroup.setForeground( mColorLabelDecoder ); + } + if( mFromTalkgroupAlias != null ) + { + mFromTalkgroupAlias.setForeground( mColorLabelDecoder ); + } + break; + case CHANNEL_STATE_LABEL_DETAILS: + if( mSourceLabel != null ) + { + mSourceLabel.setForeground( mColorLabelDetails ); + } + if( mChannelLabel != null ) + { + mChannelLabel.setForeground( mColorLabelDetails ); + } + break; + } + } + } +} diff --git a/src/decode/ltrstandard/LTRChannelState.java b/src/decode/ltrstandard/LTRChannelState.java new file mode 100644 index 00000000..12b2077e --- /dev/null +++ b/src/decode/ltrstandard/LTRChannelState.java @@ -0,0 +1,208 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package decode.ltrstandard; + +import java.util.HashMap; + +import message.Message; +import alias.Alias; +import alias.AliasList; +import controller.activity.CallEvent.CallEventType; +import controller.channel.ProcessingChain; +import controller.state.AuxChannelState; +import controller.state.ChannelState; +import decode.ltrnet.LTRCallEvent; + +public class LTRChannelState extends ChannelState +{ + private String mTalkgroup; + private Alias mTalkgroupAlias; + private int mChannelNumber; + private HashMap mActiveCalls = new HashMap(); + private LTRStandardActivitySummary mActivitySummary; + + public LTRChannelState( ProcessingChain channel, AliasList aliasList ) + { + super( channel, aliasList ); + + mActivitySummary = new LTRStandardActivitySummary( aliasList ); + } + + @Override + public String getActivitySummary() + { + StringBuilder sb = new StringBuilder(); + + sb.append( mActivitySummary.getSummary() ); + + for( AuxChannelState state: mAuxChannelStates ) + { + sb.append( state.getActivitySummary() ); + sb.append( "\n\n" ); + } + + return sb.toString(); + } + + @Override + public void receive( Message message ) + { + super.receive( message ); + + if( message instanceof LTRStandardOSWMessage ) + { + mActivitySummary.receive( message ); + + LTRStandardOSWMessage ltr = (LTRStandardOSWMessage)message; + + switch( ltr.getMessageType() ) + { + case CA_STRT: + if( 0 < ltr.getChannel() && ltr.getChannel() <=20 ) + { + /* If this channel is the same as the talkgroup's home + * repeater, then the channel number refers to the + * currently tuned frequency and we capture the channel + * number ... otherwise, it might be reflecting a call + * on another LCN */ + if( ltr.getChannel() == ltr.getHomeRepeater() ) + { + mChannelNumber = ltr.getChannel(); + broadcastChange( ChangedAttribute.CHANNEL_NUMBER ); + } + + /* If this call event is for this channel, then update + * talkgroup and alias */ + if( ltr.getChannel() == mChannelNumber ) + { + mTalkgroup = ltr.getTalkgroupID(); + broadcastChange( ChangedAttribute.FROM_TALKGROUP ); + + mTalkgroupAlias = ltr.getTalkgroupIDAlias(); + broadcastChange( ChangedAttribute.FROM_TALKGROUP_ALIAS ); + } + + /* If this is a new call, log a call event */ + if( !mActiveCalls.containsKey( ltr.getChannel() ) || + !mActiveCalls.get( ltr.getChannel() ) + .contentEquals( ltr.getTalkgroupID() ) ) + { + mActiveCalls.put( ltr.getChannel(), ltr.getTalkgroupID() ); + + CallEventType callEventType = + ( ltr.getChannel() == mChannelNumber ) ? + CallEventType.CALL_START : + CallEventType.CALL_DETECT; + + mCallEventModel.add( + new LTRCallEvent.Builder( callEventType ) + .to( ltr.getTalkgroupID() ) + .aliasList( getAliasList() ) + .channel( ltr.getChannel() ) + .frequency( ( ltr.getChannel() == mChannelNumber ) ? + mProcessingChain.getChannel() + .getTunerChannel().getFrequency() : 0 ) + .build() ); + } + + setState( State.CALL ); + } + break; + case CA_ENDD: + mTalkgroup = ltr.getTalkgroupID(); + broadcastChange( ChangedAttribute.FROM_TALKGROUP ); + + mTalkgroupAlias = ltr.getTalkgroupIDAlias(); + broadcastChange( ChangedAttribute.FROM_TALKGROUP_ALIAS ); + + fade( CallEventType.CALL_END ); + break; + case SY_IDLE: + int channel = ltr.getChannel(); + + if( 0 < channel && channel <= 20 && + channel == ltr.getHomeRepeater() && + channel == ltr.getFree() ) + { + mChannelNumber = channel; + broadcastChange( ChangedAttribute.CHANNEL_NUMBER ); + } + break; + default: + break; + } + } + } + + /** + * Intercept the fade event so that we can generate a call end event + */ + @Override + public void fade( final CallEventType type ) + { + /* + * We can receive multiple call tear-down messages -- only post a call + * end event for the one that can change the state to fade + */ + if( getState().canChangeTo( State.FADE ) ) + { + mActiveCalls.clear(); + + mCallEventModel.add( + new LTRCallEvent.Builder( type ) + .aliasList( getAliasList() ) + .channel( mChannelNumber ) + .frequency( mProcessingChain.getChannel().getTunerChannel().getFrequency() ) + .to( mTalkgroup ) + .build() ); + } + + super.fade( type ); + } + + public void reset() + { + mTalkgroup = null; + broadcastChange( ChangedAttribute.FROM_TALKGROUP ); + + mTalkgroupAlias = null; + broadcastChange( ChangedAttribute.FROM_TALKGROUP_ALIAS ); + + super.reset(); + } + + public String getTalkgroup() + { + return mTalkgroup; + } + + public Alias getTalkgroupAlias() + { + return mTalkgroupAlias; + } + + public boolean hasChannelNumber() + { + return mChannelNumber != 0; + } + + public int getChannelNumber() + { + return mChannelNumber; + } +} diff --git a/src/decode/ltrstandard/LTRFSKDemodulator.java b/src/decode/ltrstandard/LTRFSKDemodulator.java new file mode 100644 index 00000000..a0091f7c --- /dev/null +++ b/src/decode/ltrstandard/LTRFSKDemodulator.java @@ -0,0 +1,132 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package decode.ltrstandard; + +import sample.Broadcaster; +import sample.Listener; +import dsp.BooleanZeroCrossingDetector; +import dsp.FloatToBinarySlicer; +import dsp.filter.BooleanAveragingFilter; +import dsp.filter.Filters; +import dsp.filter.FloatFIRFilter; +import dsp.filter.FloatHalfBandFilter; +import dsp.filter.FloatHalfBandNoDecimateFilter; +import dsp.filter.SquaringFilter; + +public class LTRFSKDemodulator implements Listener +{ + FloatHalfBandFilter mHBFilter1; + FloatHalfBandFilter mHBFilter2; + FloatHalfBandFilter mHBFilter3; + FloatHalfBandFilter mHBFilter4; + FloatHalfBandFilter mHBFilter5; + FloatHalfBandNoDecimateFilter mHBFilter6; + FloatFIRFilter mDCBlocker; + + SquaringFilter mSquaringFilter; + BooleanAveragingFilter mAveragingFilter; + BooleanZeroCrossingDetector mZeroCrossingDetector; + FloatToBinarySlicer mSlicer; + Broadcaster mBroadcaster = new Broadcaster(); + + //debug +// FloatWaveRecorder mRecorder; + /** + * Implements a Logic Trunked Radio sub-audible 300 baud FSK signaling + * decoder. Expects a 48k short-valued sample rate, which is decimated + * to a final 1.5k sample rate via five cascaded decimating half-band + * filters, and applies a sixth, non-decimating half-band filter to bring + * the overall bandwidth down to .75k. Each half band filter requires + * 8 multiply/accumulate (MAC) cycles per input sample, totaling 2560 + * MAC filtering cycles per output bit, to get from 48k input to 300-baud + * bit stream output. + * + * Applies a balancer to the demodulated fsk waveform to normalize the + * values about the zero-axis and to offset fluctuations induced by the + * DC correction filter. + * + * Converts filtered, decimated samples to squared bit values and then + * applies a zero crossing detector to make a baud decision every 5 samples. + * + * Outputs the decoded bit stream. + */ + public LTRFSKDemodulator() + { + mHBFilter1 = new FloatHalfBandFilter( + Filters.FIR_HALF_BAND_31T_ONE_EIGHTH_FCO, 1.0002 ); + + mHBFilter2 = new FloatHalfBandFilter( + Filters.FIR_HALF_BAND_31T_ONE_EIGHTH_FCO, 1.0002 ); + mHBFilter1.setListener( mHBFilter2 ); + + mHBFilter3 = new FloatHalfBandFilter( + Filters.FIR_HALF_BAND_31T_ONE_EIGHTH_FCO, 1.0002 ); + mHBFilter2.setListener( mHBFilter3 ); + + mHBFilter4 = new FloatHalfBandFilter( + Filters.FIR_HALF_BAND_31T_ONE_EIGHTH_FCO, 1.0002 ); + mHBFilter3.setListener( mHBFilter4 ); + + mHBFilter5 = new FloatHalfBandFilter( + Filters.FIR_HALF_BAND_31T_ONE_EIGHTH_FCO, 1.0002 ); + mHBFilter4.setListener( mHBFilter5 ); + + mHBFilter6 = new FloatHalfBandNoDecimateFilter( + Filters.FIR_HALF_BAND_31T_ONE_EIGHTH_FCO, 1.0002 ); + mHBFilter5.setListener( mHBFilter6 ); + +// mDCBlocker = new FloatFIRFilter( +// Filters.FIRHP_LTR_DCBLOCK_750FS_50FC.getCoefficients(), 1.0f ); +// mHBFilter6.setListener( mDCBlocker ); + + /** + * Set the slicer's long buffer to a sample length of 14 baud periods + * ( 5 * 14 )since 12 bauds should be the longest string of ones we'll + * encounter + * + * Set the short buffer to two baud periods (5 * 2) + */ + //Set the buffer to 5 samples per baud times 12 bauds. Set the sync + //lock period to 5 samples per baud times 31 bits (155 samples) + mSlicer = new FloatToBinarySlicer( 70, 5 ); + mHBFilter6.setListener( mSlicer ); +// mDCBlocker.setListener( mSlicer ); + + mAveragingFilter = new BooleanAveragingFilter( 5 ); + mSlicer.setListener( mAveragingFilter ); + + mZeroCrossingDetector = new BooleanZeroCrossingDetector( 5 ); + mAveragingFilter.setListener( mZeroCrossingDetector ); + } + + @Override + public void receive( Float sample ) + { + mHBFilter1.receive( sample ); + } + + public void addListener( Listener listener ) + { + mZeroCrossingDetector.setListener( listener ); + } + + public void removeListener( Listener listener ) + { + mZeroCrossingDetector.removeListener( listener ); + } +} diff --git a/src/decode/ltrstandard/LTRFSKDemodulator2.java b/src/decode/ltrstandard/LTRFSKDemodulator2.java new file mode 100644 index 00000000..8eb37223 --- /dev/null +++ b/src/decode/ltrstandard/LTRFSKDemodulator2.java @@ -0,0 +1,122 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package decode.ltrstandard; + +import sample.Listener; +import dsp.BooleanZeroCrossingDetector; +import dsp.FloatToBinarySlicerLTR; +import dsp.SyncDetectListener; +import dsp.filter.BooleanAveragingFilter; +import dsp.filter.Filters; +import dsp.filter.FloatHalfBandFilter; +import dsp.filter.FloatHalfBandNoDecimateFilter; +import dsp.filter.SquaringFilter; + +public class LTRFSKDemodulator2 implements Listener, SyncDetectListener +{ + FloatHalfBandFilter mHBFilter1; + FloatHalfBandFilter mHBFilter2; + FloatHalfBandFilter mHBFilter3; + FloatHalfBandFilter mHBFilter4; + FloatHalfBandFilter mHBFilter5; + FloatHalfBandNoDecimateFilter mHBFilter6; + + SquaringFilter mSquaringFilter; + BooleanAveragingFilter mAveragingFilter; + BooleanZeroCrossingDetector mZeroCrossingDetector; + FloatToBinarySlicerLTR mSlicer; + + /** + * Implements a Logic Trunked Radio sub-audible 300 baud FSK signaling + * decoder. Expects a 48k short-valued sample rate, which is decimated + * to a final 1.5k sample rate via five cascaded decimating half-band + * filters, and applies a sixth, non-decimating half-band filter to bring + * the overall bandwidth down to .75k. Each half band filter requires + * 8 multiply/accumulate (MAC) cycles per input sample, totaling 2560 + * MAC filtering cycles per output bit, to get from 48k input to 300-baud + * bit stream output. + * + * Applies a balancer to the demodulated fsk waveform to normalize the + * values about the zero-axis and to offset fluctuations induced by the + * DC correction filter. + * + * Converts filtered, decimated samples to squared bit values and then + * applies a zero crossing detector to make a baud decision every 5 samples. + * + * Outputs the decoded bit stream. + */ + public LTRFSKDemodulator2( int syncLength, int messageLength ) + { + mHBFilter1 = new FloatHalfBandFilter( + Filters.FIR_HALF_BAND_31T_ONE_EIGHTH_FCO, 1.0002 ); + + mHBFilter2 = new FloatHalfBandFilter( + Filters.FIR_HALF_BAND_31T_ONE_EIGHTH_FCO, 1.0002 ); + mHBFilter1.setListener( mHBFilter2 ); + + mHBFilter3 = new FloatHalfBandFilter( + Filters.FIR_HALF_BAND_31T_ONE_EIGHTH_FCO, 1.0002 ); + mHBFilter2.setListener( mHBFilter3 ); + + mHBFilter4 = new FloatHalfBandFilter( + Filters.FIR_HALF_BAND_31T_ONE_EIGHTH_FCO, 1.0002 ); + mHBFilter3.setListener( mHBFilter4 ); + + mHBFilter5 = new FloatHalfBandFilter( + Filters.FIR_HALF_BAND_31T_ONE_EIGHTH_FCO, 1.0002 ); + mHBFilter4.setListener( mHBFilter5 ); + + mHBFilter6 = new FloatHalfBandNoDecimateFilter( + Filters.FIR_HALF_BAND_31T_ONE_EIGHTH_FCO, 1.0002 ); + mHBFilter5.setListener( mHBFilter6 ); + + mSlicer = new FloatToBinarySlicerLTR( syncLength, messageLength, 5 ); + mHBFilter6.setListener( mSlicer ); + + mAveragingFilter = new BooleanAveragingFilter( 5 ); + mSlicer.setListener( mAveragingFilter ); + + mZeroCrossingDetector = new BooleanZeroCrossingDetector( 5 ); + mAveragingFilter.setListener( mZeroCrossingDetector ); + } + + @Override + public void receive( Float sample ) + { + mHBFilter1.receive( sample ); + } + + public void setListener( Listener listener ) + { + mZeroCrossingDetector.setListener( listener ); + } + + public void removeListener( Listener listener ) + { + mZeroCrossingDetector.removeListener( listener ); + } + + /** + * Pass the sync detection down to the slicer + */ + @Override + public void syncDetected() + { + mSlicer.syncDetected(); + } +} diff --git a/src/decode/ltrstandard/LTRPanel.java b/src/decode/ltrstandard/LTRPanel.java new file mode 100644 index 00000000..ca8790ee --- /dev/null +++ b/src/decode/ltrstandard/LTRPanel.java @@ -0,0 +1,219 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package decode.ltrstandard; + +import java.awt.EventQueue; + +import javax.swing.JLabel; + +import settings.ColorSetting; +import settings.Setting; +import settings.SettingsManager; +import alias.Alias; +import controller.channel.Channel; +import controller.state.ChannelState.ChangedAttribute; +import controller.state.ChannelStatePanel; + +public class LTRPanel extends ChannelStatePanel +{ + private static final long serialVersionUID = 1L; + + private JLabel mStateLabel; + private JLabel mSourceLabel; + private JLabel mChannelLabel; + + private JLabel mProtocol = new JLabel( "LTR Standard" ); + private JLabel mTalkgroup = new JLabel(); + private JLabel mTalkgroupAlias = new JLabel(); + + private JLabel mLCNLabel = new JLabel( "LCN" ); + private JLabel mLCN = new JLabel(); + + private SettingsManager mSettingsManager; + + public LTRPanel( SettingsManager settingsManager, Channel channel ) + { + super( channel ); + + channel.getProcessingChain().getChannelState().addListener( this ); + + mSettingsManager = settingsManager; + init(); + } + + public void dispose() + { + super.dispose(); + mSettingsManager = null; + } + + public void init() + { + mStateLabel = new JLabel( mChannel.getProcessingChain(). + getChannelState().getState().getDisplayValue() ); + mStateLabel.setFont( mFontDecoder ); + mStateLabel.setForeground( mColorLabelDecoder ); + + mSourceLabel = new JLabel( mChannel.getSourceConfiguration().getDescription() ); + mSourceLabel.setFont( mFontDetails ); + mSourceLabel.setForeground( mColorLabelDetails ); + + mChannelLabel = new JLabel( mChannel.getChannelDisplayName() ); + mChannelLabel.setFont( mFontDetails ); + mChannelLabel.setForeground( mColorLabelDetails ); + + mProtocol.setFont( mFontDecoder ); + mProtocol.setForeground( mColorLabelDecoder ); + + mTalkgroup.setFont( mFontDecoder ); + mTalkgroup.setForeground( mColorLabelDecoder ); + + mTalkgroupAlias.setFont( mFontDecoder ); + mTalkgroupAlias.setForeground( mColorLabelDecoder ); + + mLCNLabel.setFont( mFontDecoder ); + mLCNLabel.setForeground( mColorLabelDetails ); + + mLCN.setFont( mFontDecoder ); + mLCN.setForeground( mColorLabelDecoder ); + + add( mStateLabel ); + add( mSourceLabel ); + add( mChannelLabel, "wrap" ); + + add( mProtocol ); + add( mTalkgroup ); + add( mTalkgroupAlias, "wrap" ); + + add( mLCNLabel ); + add( mLCN, "wrap" ); + } + + @Override + public void receive( final ChangedAttribute attribute ) + { + EventQueue.invokeLater( new Runnable() + { + @Override + public void run() + { + LTRChannelState state = (LTRChannelState)mChannel + .getProcessingChain().getChannelState(); + + switch( attribute ) + { + case CHANNEL_NUMBER: + if( state.getChannelNumber() != 0 ) + { + mLCN.setText( + String.valueOf( state.getChannelNumber() ) ); + } + else + { + mLCN.setText( "" ); + } + break; + case FROM_TALKGROUP: + mTalkgroup.setText( state.getTalkgroup() ); + /* fall-through */ + case FROM_TALKGROUP_ALIAS: + Alias alias = state.getTalkgroupAlias(); + + if( alias != null ) + { + mTalkgroupAlias.setText( alias.getName() ); + mTalkgroupAlias.setIcon( mSettingsManager + .getImageIcon( alias.getIconName(), 12 ) ); + } + else + { + mTalkgroupAlias.setText( null ); + mTalkgroupAlias.setIcon( null ); + } + case CHANNEL_STATE: + mStateLabel.setText( state.getState().getDisplayValue() ); + break; + case SOURCE: + mSourceLabel.setText( mChannel + .getSourceConfiguration().getDescription() ); + break; + case CHANNEL_NAME: + case SITE_NAME: + case SYSTEM_NAME: + mChannelLabel.setText( mChannel.getChannelDisplayName() ); + break; + default: + break; + } + + repaint(); + } + }); + } + + @Override + public void settingChanged( Setting setting ) + { + super.settingChanged( setting ); + + if( setting instanceof ColorSetting ) + { + ColorSetting colorSetting = (ColorSetting)setting; + + switch( colorSetting.getColorSettingName() ) + { + case CHANNEL_STATE_LABEL_DECODER: + if( mStateLabel != null ) + { + mStateLabel.setForeground( mColorLabelDecoder ); + } + if( mProtocol != null ) + { + mProtocol.setForeground( mColorLabelDecoder ); + } + if( mTalkgroup != null ) + { + mTalkgroup.setForeground( mColorLabelDecoder ); + } + if( mTalkgroupAlias != null ) + { + mTalkgroupAlias.setForeground( mColorLabelDecoder ); + } + if( mLCN != null ) + { + mLCN.setForeground( mColorLabelDecoder ); + } + break; + case CHANNEL_STATE_LABEL_DETAILS: + if( mSourceLabel != null ) + { + mSourceLabel.setForeground( mColorLabelDetails ); + } + if( mChannelLabel != null ) + { + mChannelLabel.setForeground( mColorLabelDetails ); + } + if( mLCNLabel != null ) + { + mLCNLabel.setForeground( mColorLabelDetails ); + } + break; + } + } + } +} diff --git a/src/decode/ltrstandard/LTRStandardActivitySummary.java b/src/decode/ltrstandard/LTRStandardActivitySummary.java new file mode 100644 index 00000000..cee0a23e --- /dev/null +++ b/src/decode/ltrstandard/LTRStandardActivitySummary.java @@ -0,0 +1,209 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package decode.ltrstandard; + +import java.text.DecimalFormat; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Iterator; +import java.util.TreeSet; + +import message.Message; +import sample.Listener; +import alias.Alias; +import alias.AliasList; +import controller.activity.ActivitySummaryProvider; + +public class LTRStandardActivitySummary implements ActivitySummaryProvider, + Listener +{ + private static final int sINT_NULL_VALUE = -1; + private DecimalFormat mDecimalFormatter = new DecimalFormat("0.00000"); + + private AliasList mAliasList; + private HashSet mTalkgroupsFirstHeard = new HashSet(); + private TreeSet mTalkgroups = new TreeSet(); + private TreeSet mActiveLCNs = new TreeSet(); + private int mMonitoredLCN; + + /** + * Compiles a summary of active talkgroups, unique radio ids encountered + * in the decoded messages. Talkgroups have to be heard a minimum of twice + * to be considered valid ... in order to weed out the invalid ones. + * + * Returns a textual summary of the activity + */ + public LTRStandardActivitySummary( AliasList list ) + { + mAliasList = list; + } + + @Override + public void receive( Message message ) + { + if( message instanceof LTRStandardOSWMessage ) + { + LTRStandardOSWMessage ltr = ((LTRStandardOSWMessage)message); + + if( ltr.isValid() ) + { + switch( ltr.getMessageType() ) + { + case CA_STRT: + case CA_ENDD: + int home = ltr.getHomeRepeater(); + + if( 0 < home && home <= 20 ) + { + String talkgroup = ltr.getTalkgroupID( false ); + + if( mTalkgroupsFirstHeard.contains( talkgroup ) ) + { + mTalkgroups.add( talkgroup ); + } + else + { + mTalkgroupsFirstHeard.add( talkgroup ); + } + } + + int channel = ltr.getChannel(); + + if( 0 < channel && channel <= 20 ) + { + if( channel == home ) + { + mMonitoredLCN = channel; + mActiveLCNs.add( channel ); + } + + int free = ltr.getFree(); + + if( 0 < free && free <= 20 ) + { + mActiveLCNs.add( free ); + } + } + + break; + case SY_IDLE: + int idleChannel = ltr.getChannel(); + + if( idleChannel == ltr.getHomeRepeater() && + idleChannel == ltr.getFree() ) + { + mMonitoredLCN = idleChannel; + mActiveLCNs.add( idleChannel ); + } + break; + case UN_KNWN: + default: + break; + } + } + } + } + + @Override + public String getSummary() + { + StringBuilder sb = new StringBuilder(); + + sb.append( "Activity Summary\n\n" ); + + sb.append( "Decoder:\tLTR-Standard\n" ); + + sb.append( "Monitored LCN: " ); + + if( mMonitoredLCN > 0 ) + { + sb.append( mMonitoredLCN ); + } + else + { + sb.append( "unknown" ); + } + + sb.append( "\n" ); + + Integer[] lcns = mActiveLCNs.toArray( new Integer[0] ); + + sb.append( "Active LCNs:\t" ); + + if( lcns.length > 0 ) + { + sb.append( Arrays.toString( lcns ) ); + } + else + { + sb.append( "none" ); + } + sb.append( "\n\n" ); + + sb.append( "Talkgroups\n" ); + + if( mTalkgroups.isEmpty() ) + { + sb.append( " None\n" ); + } + else + { + Iterator it = mTalkgroups.iterator(); + + while( it.hasNext() ) + { + String tgid = it.next(); + + sb.append( " " ); + sb.append( formatTalkgroup( tgid ) ); + sb.append( " " ); + + Alias alias = mAliasList.getTalkgroupAlias( tgid ); + + if( alias != null ) + { + sb.append( alias.getName() ); + } + + sb.append( "\n" ); + } + } + + return sb.toString(); + } + + public static String formatTalkgroup( String talkgroup ) + { + StringBuilder sb = new StringBuilder(); + + if( talkgroup.length() == 6 ) + { + sb.append( talkgroup.substring( 0, 1 ) ); + sb.append( "-" ); + sb.append( talkgroup.substring( 1, 3 ) ); + sb.append( "-" ); + sb.append( talkgroup.substring( 3, 6 ) ); + + return sb.toString(); + } + else + { + return talkgroup; + } + } +} diff --git a/src/decode/ltrstandard/LTRStandardDecoder.java b/src/decode/ltrstandard/LTRStandardDecoder.java new file mode 100644 index 00000000..423e4c27 --- /dev/null +++ b/src/decode/ltrstandard/LTRStandardDecoder.java @@ -0,0 +1,117 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package decode.ltrstandard; + +import message.MessageDirection; +import source.Source.SampleType; +import alias.AliasList; +import bits.MessageFramer; +import bits.SyncPattern; +import decode.Decoder; +import decode.DecoderType; +import dsp.filter.DCRemovalFilter2; +import dsp.nbfm.FilteringNBFMDemodulator; + +public class LTRStandardDecoder extends Decoder +{ + /** + * This value determines how quickly the DC remove filter responds to + * changes in frequency. + */ + private static final double sDC_REMOVAL_RATIO = 0.000003; + + public static final int sLTR_STANDARD_MESSAGE_LENGTH = 40; + private FilteringNBFMDemodulator mDemodulator; + private DCRemovalFilter2 mDCRemovalFilter; + private LTRFSKDemodulator mLTRFSKDemodulator; + private MessageFramer mLTRMessageFramer; + private LTRStandardMessageProcessor mLTRMessageProcessor; + + public LTRStandardDecoder( SampleType sampleType, + AliasList aliasList, + MessageDirection direction ) + { + super( sampleType ); + + /** + * Only setup a demod chain if we're receiving complex samples. If + * we're receiving demodulated samples, they'll be handled the same + * was as we handle the output of the demodulator. + */ + if( mSourceSampleType == SampleType.COMPLEX ) + { + /** + * The Decoder super class is both a Complex listener and a Short + * listener. So, we add the demod to listen to the incoming + * quadrature samples, and we wire the output of the demod right + * back to this class, so we can receive the demodulated output + * to process + */ + mDemodulator = new FilteringNBFMDemodulator(); + this.addComplexListener( mDemodulator ); + + /** + * Remove the DC component that is present when we're mistuned + */ + mDCRemovalFilter = new DCRemovalFilter2( sDC_REMOVAL_RATIO ); + mDemodulator.addListener( mDCRemovalFilter ); + + mDCRemovalFilter.setListener( this.getFloatReceiver() ); + } + + mLTRFSKDemodulator = new LTRFSKDemodulator(); + + /** + * The DC removal filter will impact performance of the LTRFSKDemod, + * so we get the samples straight from the NBFM demod + */ + if( mSourceSampleType == SampleType.COMPLEX ) + { + mDemodulator.addListener( mLTRFSKDemodulator ); + } + else + { + mDCRemovalFilter.setListener( mLTRFSKDemodulator ); + } + + if( direction == MessageDirection.OSW ) + { + mLTRMessageFramer = + new MessageFramer( SyncPattern.LTR_STANDARD_OSW.getPattern(), + sLTR_STANDARD_MESSAGE_LENGTH ); + } + else + { + mLTRMessageFramer = + new MessageFramer( SyncPattern.LTR_STANDARD_ISW.getPattern(), + sLTR_STANDARD_MESSAGE_LENGTH ); + } + + mLTRFSKDemodulator.addListener( mLTRMessageFramer ); + + mLTRMessageProcessor = new LTRStandardMessageProcessor( direction, aliasList ); + mLTRMessageFramer.addMessageListener( mLTRMessageProcessor ); + mLTRMessageProcessor.addMessageListener( this ); + } + + @Override + public DecoderType getType() + { + return DecoderType.LTR_STANDARD; + } +} diff --git a/src/decode/ltrstandard/LTRStandardEditor.java b/src/decode/ltrstandard/LTRStandardEditor.java new file mode 100644 index 00000000..57a7de7c --- /dev/null +++ b/src/decode/ltrstandard/LTRStandardEditor.java @@ -0,0 +1,70 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package decode.ltrstandard; + +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; + +import javax.swing.DefaultComboBoxModel; +import javax.swing.JComboBox; + +import message.MessageDirection; +import decode.DecodeEditor; +import decode.config.DecodeConfigLTRStandard; +import decode.config.DecodeConfiguration; + +public class LTRStandardEditor extends DecodeEditor +{ + private static final long serialVersionUID = 1L; + + private JComboBox mComboDirection; + + public LTRStandardEditor( DecodeConfiguration config ) + { + super( config ); + + initGUI(); + } + + private void initGUI() + { + mComboDirection = new JComboBox(); + + mComboDirection.setModel( + new DefaultComboBoxModel( MessageDirection.values() ) ); + + mComboDirection.setSelectedItem( ((DecodeConfigLTRStandard)mConfig).getMessageDirection() ); + + mComboDirection.addActionListener( new ActionListener() + { + @Override + public void actionPerformed( ActionEvent e ) + { + MessageDirection selected = mComboDirection + .getItemAt( mComboDirection.getSelectedIndex() ); + + if( selected != null ) + { + ((DecodeConfigLTRStandard)mConfig).setMessageDirection( selected ); + } + } + } ); + + add( mComboDirection, "span 2" ); + } +} diff --git a/src/decode/ltrstandard/LTRStandardISWMessage.java b/src/decode/ltrstandard/LTRStandardISWMessage.java new file mode 100644 index 00000000..5d87d599 --- /dev/null +++ b/src/decode/ltrstandard/LTRStandardISWMessage.java @@ -0,0 +1,212 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package decode.ltrstandard; + +import java.util.Date; + +import map.Plottable; +import message.MessageDirection; +import message.MessageType; +import alias.Alias; +import alias.AliasList; +import bits.BitSetBuffer; + +public class LTRStandardISWMessage extends LTRStandardMessage +{ + private static final int sCALL_END = 31; + private static final int sIDLE = 255; + + public LTRStandardISWMessage( BitSetBuffer message, AliasList list ) + { + super( message, MessageDirection.OSW, list ); + mMessageType = getType(); + } + + @Override + public String toString() + { + StringBuilder sb = new StringBuilder(); + sb.append( mDatestampFormatter.format( + new Date( System.currentTimeMillis() ) ) ); + sb.append( " LTR OSW [" ); + sb.append( mCRC.getAbbreviation() ); + sb.append( "] " ); + sb.append( getMessage() ); + pad( sb, 100 ); + sb.append( mMessage.toString() ); + + return sb.toString(); + } + + @Override + public String getMessage() + { + StringBuilder sb = new StringBuilder(); + + switch( mMessageType ) + { + case SY_IDLE: + sb.append( "IDLE AREA:" ); + sb.append( getArea() ); + sb.append( " LCN:" ); + sb.append( format( getChannel(), 2 ) ); + sb.append( " HOME:" ); + sb.append( format( getHomeRepeater(), 2 ) ); + sb.append( " GRP:" ); + sb.append( format( getGroup(), 3 ) ); + sb.append( " FREE:" ); + sb.append( format( getFree(), 2 ) ); + break; + case CA_STRT: + sb.append( "CALL LCN:" ); + sb.append( format( getChannel(), 2 ) ); + sb.append( " TG [ " ); + sb.append( getTalkgroupID() ); + sb.append( "/" ); + sb.append( getTalkgroupIDAlias() ); + sb.append( " ] FREE:" ); + sb.append( format( getFree(), 2 ) ); + break; + case CA_ENDD: + sb.append( "END* AREA:" ); + sb.append( getArea() ); + sb.append( " LCN:" ); + sb.append( format( getChannel(), 2 ) ); + sb.append( " TG [" ); + sb.append( getTalkgroupID() ); + sb.append( "/" ); + sb.append( getTalkgroupIDAlias() ); + sb.append( "] FREE:" ); + sb.append( format( getFree(), 2 ) ); + break; + default: + sb.append( "UNKNOWN A:" ); + sb.append( getArea() ); + sb.append( " C:" ); + sb.append( format( getChannel(), 2 ) ); + sb.append( " H:" ); + sb.append( format( getHomeRepeater(), 2 ) ); + sb.append( " G:" ); + sb.append( format( getGroup(), 3 ) ); + sb.append( " F:" ); + sb.append( format( getFree(), 2 ) ); + } + + return sb.toString(); + } + + public String getTalkgroupID() + { + return getTalkgroupID( true ); + } + + public String getTalkgroupID( boolean format ) + { + StringBuilder sb = new StringBuilder(); + + sb.append( getArea() ); + + if( format ) + { + sb.append( "-" ); + } + + sb.append( format( getHomeRepeater(), 2 ) ); + + if( format ) + { + sb.append( "-" ); + } + + sb.append( format( getGroup(), 3 ) ); + + return sb.toString(); + } + + public Alias getTalkgroupIDAlias() + { + StringBuilder sb = new StringBuilder(); + sb.append( getArea() ); + sb.append( format( getHomeRepeater(), 2 ) ); + sb.append( format( getGroup(), 3 ) ); + + return mAliasList.getTalkgroupAlias( sb.toString() ); + } + + public MessageType getType() + { + MessageType retVal = MessageType.UN_KNWN; + + int group = getGroup(); + + if( group == sIDLE ) + { + retVal = MessageType.SY_IDLE; + } + else + { + int channel = getChannel(); + + if( channel == sCALL_END ) + { + retVal = MessageType.CA_ENDD; + } + else + { + retVal = MessageType.CA_STRT; + } + } + + return retVal; + } + + @Override + public String getFromID() + { + if( mMessageType == MessageType.CA_STRT || + mMessageType == MessageType.CA_ENDD ) + { + return getTalkgroupID(); + } + else + { + return null; + } + } + + @Override + public Alias getFromIDAlias() + { + if( mMessageType == MessageType.CA_STRT || + mMessageType == MessageType.CA_ENDD ) + { + return getTalkgroupIDAlias(); + } + else + { + return null; + } + } + + @Override + public Plottable getPlottable() + { + // TODO Auto-generated method stub + return null; + } +} diff --git a/src/decode/ltrstandard/LTRStandardMessage.java b/src/decode/ltrstandard/LTRStandardMessage.java new file mode 100644 index 00000000..5ade8980 --- /dev/null +++ b/src/decode/ltrstandard/LTRStandardMessage.java @@ -0,0 +1,256 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package decode.ltrstandard; + +import java.text.DecimalFormat; +import java.text.SimpleDateFormat; + +import log.Log; +import message.Message; +import message.MessageDirection; +import message.MessageType; +import alias.Alias; +import alias.AliasList; +import bits.BitSetBuffer; +import crc.CRC; +import crc.CRCLTR; +import decode.DecoderType; + +public abstract class LTRStandardMessage extends Message +{ + protected static final String sUNKNOWN = "**UNKNOWN**"; + protected static final int sINT_NULL_VALUE = -1; + protected static final double sDOUBLE_NULL_VALUE = -1.0D; + protected SimpleDateFormat mDatestampFormatter = + new SimpleDateFormat( "yyyyMMdd HHmmss" ); + protected DecimalFormat mDecimalFormatter = new DecimalFormat("0.00000"); + + protected static final int[] sSYNC = { 8,7,6,5,4,3,2,1,0 }; + protected static final int[] sAREA = { 9 }; + protected static final int[] sCHANNEL = { 14,13,12,11,10 }; + protected static final int[] sHOME_REPEATER = { 19,18,17,16,15 }; + protected static final int[] sGROUP = { 27,26,25,24,23,22,21,20 }; + protected static final int[] sFREE = { 32,31,30,29,28 }; + protected static final int[] sCRC = { 39,38,37,36,35,34,33 }; + + protected BitSetBuffer mMessage; + protected MessageType mMessageType; + protected CRC mCRC; + protected AliasList mAliasList; + + public LTRStandardMessage( BitSetBuffer message, + MessageDirection direction, + AliasList list ) + { + mMessage = message; + mCRC = CRCLTR.check( message, direction ); + mAliasList = list; + } + + public MessageType getMessageType() + { + return mMessageType; + } + + public boolean isValid() + { + return mCRC != CRC.FAILED_CRC && mCRC != CRC.FAILED_PARITY; + } + + /** + * Appends spaces to the end of the stringbuilder to make it length long + */ + protected void pad( StringBuilder sb, int length ) + { + while( sb.length() < length ) + { + sb.append( " " ); + } + } + + @Override + public String getBinaryMessage() + { + return mMessage.toString(); + } + + /** + * Returns a value formatted in hex notation. + * + * @param value - integer value + * @return - hex digits returned + */ + protected static String getHex( int value, int digits ) + { + return String.format( "%0" + digits + "X", value ); + } + + protected int getInt( int[] bits ) + { + int retVal = 0; + + for( int x = 0; x < bits.length; x++ ) + { + if( mMessage.get( bits[ x ] ) ) + { + retVal += 1< + ******************************************************************************/ +package decode.ltrstandard; + +import message.Message; +import message.MessageDirection; +import sample.Broadcaster; +import sample.Listener; +import alias.AliasList; +import bits.BitSetBuffer; + +public class LTRStandardMessageProcessor implements Listener +{ + private MessageDirection mDirection; + private Broadcaster mBroadcaster = new Broadcaster(); + private AliasList mAliasList; + + public LTRStandardMessageProcessor( MessageDirection direction, AliasList list ) + { + mDirection = direction; + mAliasList = list; + } + + @Override + public void receive( BitSetBuffer buffer ) + { + LTRStandardMessage message; + + if( mDirection == MessageDirection.OSW ) + { + message = new LTRStandardOSWMessage( buffer, mAliasList ); + } + else + { + buffer.flip( 0, 40 ); + + message = new LTRStandardISWMessage( buffer, mAliasList ); + } + + mBroadcaster.receive( message ); + } + + public void addMessageListener( Listener listener ) + { + mBroadcaster.addListener( listener ); + } + + public void removeMessageListener( Listener listener ) + { + mBroadcaster.removeListener( listener ); + } +} diff --git a/src/decode/ltrstandard/LTRStandardOSWMessage.java b/src/decode/ltrstandard/LTRStandardOSWMessage.java new file mode 100644 index 00000000..ed44f2d2 --- /dev/null +++ b/src/decode/ltrstandard/LTRStandardOSWMessage.java @@ -0,0 +1,222 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package decode.ltrstandard; + +import java.util.Date; + +import map.Plottable; +import message.MessageDirection; +import message.MessageType; +import alias.Alias; +import alias.AliasList; +import bits.BitSetBuffer; + +public class LTRStandardOSWMessage extends LTRStandardMessage +{ + private static final int sCALL_END = 31; + private static final int sIDLE = 255; + + public LTRStandardOSWMessage( BitSetBuffer message, AliasList list ) + { + super( message, MessageDirection.OSW, list ); + mMessageType = getType(); + } + + @Override + public String toString() + { + StringBuilder sb = new StringBuilder(); + sb.append( mDatestampFormatter.format( + new Date( System.currentTimeMillis() ) ) ); + sb.append( " LTR OSW [" ); + sb.append( mCRC.getAbbreviation() ); + sb.append( "] " ); + sb.append( getMessage() ); + pad( sb, 100 ); + sb.append( mMessage.toString() ); + + return sb.toString(); + } + + @Override + public String getMessage() + { + StringBuilder sb = new StringBuilder(); + + switch( mMessageType ) + { + case SY_IDLE: + sb.append( "IDLE AREA:" ); + sb.append( getArea() ); + sb.append( " LCN:" ); + sb.append( format( getChannel(), 2 ) ); + sb.append( " HOME:" ); + sb.append( format( getHomeRepeater(), 2 ) ); + sb.append( " GRP:" ); + sb.append( format( getGroup(), 3 ) ); + sb.append( " FREE:" ); + sb.append( format( getFree(), 2 ) ); + break; + case CA_STRT: + sb.append( "CALL LCN:" ); + sb.append( format( getChannel(), 2 ) ); + sb.append( " TG [ " ); + sb.append( getTalkgroupID() ); + sb.append( "/" ); + Alias alias = getTalkgroupIDAlias(); + if( alias != null ) + { + sb.append( getTalkgroupIDAlias().getName() ); + } + else + { + sb.append( "UNKNOWN" ); + } + sb.append( " ] FREE:" ); + sb.append( format( getFree(), 2 ) ); + break; + case CA_ENDD: + sb.append( "END* AREA:" ); + sb.append( getArea() ); + sb.append( " LCN:" ); + sb.append( format( getChannel(), 2 ) ); + sb.append( " TG [" ); + sb.append( getTalkgroupID() ); + sb.append( "/" ); + Alias endAlias = getTalkgroupIDAlias(); + if( endAlias != null ) + { + sb.append( getTalkgroupIDAlias().getName() ); + } + else + { + sb.append( "UNKNOWN" ); + } + sb.append( "] FREE:" ); + sb.append( format( getFree(), 2 ) ); + break; + default: + sb.append( "UNKNOWN A:" ); + sb.append( getArea() ); + sb.append( " C:" ); + sb.append( format( getChannel(), 2 ) ); + sb.append( " H:" ); + sb.append( format( getHomeRepeater(), 2 ) ); + sb.append( " G:" ); + sb.append( format( getGroup(), 3 ) ); + sb.append( " F:" ); + sb.append( format( getFree(), 2 ) ); + } + + return sb.toString(); + } + + public String getTalkgroupID() + { + return getTalkgroupID( true ); + } + + public String getTalkgroupID( boolean format ) + { + StringBuilder sb = new StringBuilder(); + + sb.append( getArea() ); + + if( format ) + { + sb.append( "-" ); + } + + sb.append( format( getHomeRepeater(), 2 ) ); + + if( format ) + { + sb.append( "-" ); + } + + sb.append( format( getGroup(), 3 ) ); + + return sb.toString(); + } + + public Alias getTalkgroupIDAlias() + { + return mAliasList.getTalkgroupAlias( getTalkgroupID() ); + } + + public MessageType getType() + { + MessageType retVal = MessageType.UN_KNWN; + + /* Group 255 when the channel equals the free is the IDLE message */ + if( getGroup() == sIDLE && getChannel() == getFree() ) + { + retVal = MessageType.SY_IDLE; + } + else + { + int channel = getChannel(); + + if( channel == sCALL_END ) + { + retVal = MessageType.CA_ENDD; + } + else + { + retVal = MessageType.CA_STRT; + } + } + + return retVal; + } + + @Override + public String getFromID() + { + if( mMessageType == MessageType.CA_STRT || + mMessageType == MessageType.CA_ENDD ) + { + return getTalkgroupID(); + } + else + { + return null; + } + } + + @Override + public Alias getFromIDAlias() + { + if( mMessageType == MessageType.CA_STRT || + mMessageType == MessageType.CA_ENDD ) + { + return getTalkgroupIDAlias(); + } + else + { + return null; + } + } + + @Override + public Plottable getPlottable() + { + // TODO Auto-generated method stub + return null; + } +} diff --git a/src/decode/mdc1200/MDCActivitySummary.java b/src/decode/mdc1200/MDCActivitySummary.java new file mode 100644 index 00000000..f937ad91 --- /dev/null +++ b/src/decode/mdc1200/MDCActivitySummary.java @@ -0,0 +1,131 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package decode.mdc1200; + +import java.util.Iterator; +import java.util.TreeSet; + +import message.Message; +import sample.Listener; +import alias.Alias; +import alias.AliasList; +import controller.activity.ActivitySummaryProvider; + +public class MDCActivitySummary implements ActivitySummaryProvider, + Listener +{ + private AliasList mAliasList; + private TreeSet mIdents = new TreeSet(); + private TreeSet mEmergencyIdents = new TreeSet(); + + /** + * Compiles a summary of active talkgroups, unique radio ids encountered + * in the decoded messages. Talkgroups have to be heard a minimum of twice + * to be considered valid ... in order to weed out the invalid ones. + * + * Returns a textual summary of the activity + */ + public MDCActivitySummary( AliasList list ) + { + mAliasList = list; + } + + @Override + public void receive( Message message ) + { + if( message instanceof MDCMessage ) + { + MDCMessage mdc = ((MDCMessage)message); + + mIdents.add( mdc.getUnitID() ); + + if( mdc.isEmergency() ) + { + mEmergencyIdents.add( mdc.getUnitID() ); + } + } + } + + @Override + public String getSummary() + { + StringBuilder sb = new StringBuilder(); + + sb.append( "=============================\n" ); + sb.append( "Decoder:\tMDC-1200\n\n" ); + sb.append( "MDC-1200 Idents\n" ); + + if( mIdents.isEmpty() ) + { + sb.append( " None\n" ); + } + else + { + Iterator it = mIdents.iterator(); + + while( it.hasNext() ) + { + String ident = it.next(); + + sb.append( " " ); + sb.append( ident ); + sb.append( " " ); + + Alias alias = mAliasList.getMDC1200Alias( ident ); + + if( alias != null ) + { + sb.append( alias.getName() ); + } + + sb.append( "\n" ); + } + } + + sb.append( "MDC-1200 Emergency Idents\n" ); + + if( mEmergencyIdents.isEmpty() ) + { + sb.append( " None\n" ); + } + else + { + Iterator it = mEmergencyIdents.iterator(); + + while( it.hasNext() ) + { + String ident = it.next(); + + sb.append( " " ); + sb.append( ident ); + sb.append( " " ); + + Alias alias = mAliasList.getMDC1200Alias( ident ); + + if( alias != null ) + { + sb.append( alias.getName() ); + } + + sb.append( "\n" ); + } + } + + return sb.toString(); + } +} diff --git a/src/decode/mdc1200/MDCCallEvent.java b/src/decode/mdc1200/MDCCallEvent.java new file mode 100644 index 00000000..2a4637be --- /dev/null +++ b/src/decode/mdc1200/MDCCallEvent.java @@ -0,0 +1,159 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package decode.mdc1200; + +import alias.Alias; +import alias.AliasList; +import controller.activity.CallEvent; +import decode.DecoderType; + +public class MDCCallEvent extends CallEvent +{ + public MDCCallEvent( CallEventType callEventType, + AliasList aliasList, + String fromID, + String toID, + String details ) + { + super( DecoderType.MDC1200, callEventType, + aliasList, fromID, toID, details ); + } + + private Alias getAlias( String ident ) + { + if( hasAliasList() ) + { + return getAliasList().getMDC1200Alias( ident ); + } + + return null; + } + + @Override + public Alias getFromIDAlias() + { + return getAlias( getFromID() ); + } + + @Override + public Alias getToIDAlias() + { + return getAlias( getToID() ); + } + + @Override + public int getChannel() + { + return 0; + } + + @Override + public int getFrequency() + { + return 0; + } + + public static class Builder + { + /* Required parameters */ + private CallEventType mCallEventType; + + /* Optional parameters */ + private AliasList mAliasList; + private String mFromID; + private String mToID; + private String mDetails; + + public Builder( CallEventType callEventType ) + { + mCallEventType = callEventType; + } + + public Builder aliasList( AliasList aliasList ) + { + mAliasList = aliasList; + return this; + } + + public Builder from( String val ) + { + mFromID = val; + return this; + } + + public Builder details( String details ) + { + mDetails = details; + return this; + } + + public Builder to( String toID ) + { + mToID = toID; + return this; + } + + public MDCCallEvent build() + { + return new MDCCallEvent( this ); + } + } + + /** + * Private constructor for the builder + */ + private MDCCallEvent( Builder builder ) + { + this( builder.mCallEventType, + builder.mAliasList, + builder.mFromID, + builder.mToID, + builder.mDetails ); + } + + public static MDCCallEvent + getMDCCallEvent( MDCMessage message ) + { + CallEventType type = CallEventType.UNKNOWN; + StringBuilder sbDetails = new StringBuilder(); + sbDetails.append( "MDC-1200 OPCODE:" ); + sbDetails.append( String.valueOf( message.getOpcode() ) ); + + switch( message.getMessageType() ) + { + case ACKNOWLEDGE: + type = CallEventType.ACKNOWLEDGE; + break; + case ANI: + type = CallEventType.ID_ANI; + break; + case EMERGENCY: + type = CallEventType.EMERGENCY; + break; + case PAGING: + type = CallEventType.PAGE; + break; + } + + return new MDCCallEvent.Builder( type ) + .details( sbDetails.toString() ) + .from( message.getFromID() ) + .to( message.getToID() ) + .build(); + } +} diff --git a/src/decode/mdc1200/MDCChannelState.java b/src/decode/mdc1200/MDCChannelState.java new file mode 100644 index 00000000..b73c9d9a --- /dev/null +++ b/src/decode/mdc1200/MDCChannelState.java @@ -0,0 +1,183 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package decode.mdc1200; + +import message.Message; +import alias.Alias; +import controller.state.AuxChannelState; +import controller.state.ChannelState; +import controller.state.ChannelState.ChangedAttribute; +import controller.state.ChannelState.State; + +public class MDCChannelState extends AuxChannelState +{ + private String mFrom; + private Alias mFromAlias; + private String mTo; + private Alias mToAlias; + private String mMessage; + private String mMessageType; + + private MDCActivitySummary mActivitySummary; + + public MDCChannelState( ChannelState parentState ) + { + super( parentState ); + + mActivitySummary = new MDCActivitySummary( parentState.getAliasList() ); + } + + @Override + public void reset() + { + setFrom( null ); + setFromAlias( null ); + setTo( null); + setToAlias( null ); + setMessage( null ); + setMessageType( null ); + } + + @Override + public void fade() {} + + @Override + public void receive( Message message ) + { + if( message instanceof MDCMessage ) + { + mActivitySummary.receive( message ); + + MDCMessage mdc = (MDCMessage)message; + + setFrom( mdc.getFromID() ); + setFromAlias( mdc.getFromIDAlias() ); + + MDCMessageType type = mdc.getMessageType(); + + setMessageType( type.getLabel() ); + + StringBuilder sb = new StringBuilder(); + + switch( type ) + { + case ACKNOWLEDGE: + case ANI: + case EMERGENCY: + case PAGING: + case STATUS: + default: + sb.append( "OPCODE " ); + sb.append( String.valueOf( mdc.getOpcode() ) ); + + if( mdc.isBOT() ) + { + sb.append( " TYPE:BOT" ); + } + + if( mdc.isEOT() ) + { + sb.append( " TYPE:EOT" ); + } + break; + } + + setMessage( sb.toString() ); + + //Set the state to CALL on the parent channel state, so that the + //timer can track turning off any updates we apply + mParentChannelState.setState( State.CALL ); + + MDCCallEvent event = MDCCallEvent.getMDCCallEvent( mdc ); + event.setAliasList( mParentChannelState.getAliasList() ); + mParentChannelState.receiveCallEvent( event ); + } + } + + public String getFrom() + { + return mFrom; + } + + public void setFrom( String from ) + { + mFrom = from; + broadcastChange( ChangedAttribute.FROM_TALKGROUP ); + } + + public Alias getFromAlias() + { + return mFromAlias; + } + + public void setFromAlias( Alias alias ) + { + mFromAlias = alias; + broadcastChange( ChangedAttribute.FROM_TALKGROUP_ALIAS ); + } + + public String getTo() + { + return mTo; + } + + public void setTo( String to ) + { + mTo = to; + broadcastChange( ChangedAttribute.TO_TALKGROUP ); + } + + public Alias getToAlias() + { + return mToAlias; + } + + public void setToAlias( Alias alias ) + { + mToAlias = alias; + broadcastChange( ChangedAttribute.TO_TALKGROUP_ALIAS ); + } + + public String getMessage() + { + return mMessage; + } + + public void setMessage( String message ) + { + mMessage = message; + broadcastChange( ChangedAttribute.MESSAGE ); + } + + public String getMessageType() + { + return mMessageType; + } + + public void setMessageType( String type ) + { + mMessageType = type; + broadcastChange( ChangedAttribute.MESSAGE_TYPE ); + } + + @Override + public String getActivitySummary() + { + return mActivitySummary.getSummary(); + } +} diff --git a/src/decode/mdc1200/MDCDecoder.java b/src/decode/mdc1200/MDCDecoder.java new file mode 100644 index 00000000..16662c9f --- /dev/null +++ b/src/decode/mdc1200/MDCDecoder.java @@ -0,0 +1,202 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package decode.mdc1200; + +import instrument.Instrumentable; +import instrument.tap.Tap; +import instrument.tap.stream.BinaryTap; +import instrument.tap.stream.FloatTap; + +import java.util.ArrayList; +import java.util.List; + +import source.Source.SampleType; +import alias.AliasList; +import bits.MessageFramer; +import bits.SyncPattern; +import decode.Decoder; +import decode.DecoderType; +import dsp.NRZDecoder; +import dsp.filter.Filters; +import dsp.filter.FloatFIRFilter; +import dsp.filter.FloatHalfBandFilter; +import dsp.fsk.FSK2Decoder; +import dsp.fsk.FSK2Decoder.Output; + +/** + * MDC1200 Decoder - 1200 baud 2FSK decoder that can process 48k sample rate + * complex or floating point samples and output fully framed MDC1200 messages + */ +public class MDCDecoder extends Decoder implements Instrumentable +{ + /* Decimated sample rate ( 48,000 / 2 = 24,000 ) feeding the decoder */ + private static final int sDECIMATED_SAMPLE_RATE = 24000; + + /* Baud or Symbol Rate */ + private static final int sSYMBOL_RATE = 1200; + + /* Message length */ + private static final int sMESSAGE_LENGTH = 304; + + /* Instrumentation Taps */ + private ArrayList mAvailableTaps; + private static final String INSTRUMENT_INPUT = + "Tap Point: Float Input"; + private static final String INSTRUMENT_BANDPASS_FILTER_TO_FSK2_DEMOD = + "Tap Point: Bandpass Filter > < FSK2 Decoder"; + private static final String INSTRUMENT_FSK2_DECODER_TO_NRZ_DECODER = + "Tap Point: FSK2 Decoder > < NRZI Decoder"; + private static final String INSTRUMENT_NRZI_DECODER_TO_MESSAGE_FRAMER = + "Tap Point: NRZ Decoder > < Message Framer"; + + private FSK2Decoder mFSKDecoder; + private FloatHalfBandFilter mDecimationFilter; + private FloatFIRFilter mBandPassFilter; + private NRZDecoder mNRZDecoder; + private MessageFramer mMessageFramer; + private MDCMessageProcessor mMessageProcessor; + + public MDCDecoder( AliasList aliasList ) + { + super( SampleType.FLOAT ); + + /* Decimation filter: 48000 / 2 = 24000 */ + mDecimationFilter = new FloatHalfBandFilter( + Filters.FIR_HALF_BAND_31T_ONE_EIGHTH_FCO, 1.0002 ); + addFloatListener( mDecimationFilter ); + + /* Bandpass filter - predefined */ + mBandPassFilter = new FloatFIRFilter( + Filters.FIRBP_1200FSK_24000FS.getCoefficients(), 1.02 ); + mDecimationFilter.setListener( mBandPassFilter ); + + /* 2FSK Decoder */ + mFSKDecoder = new FSK2Decoder( sDECIMATED_SAMPLE_RATE, + sSYMBOL_RATE, Output.NORMAL ); + mBandPassFilter.setListener( mFSKDecoder ); + + /* NRZ Decoder */ + mNRZDecoder = new NRZDecoder( NRZDecoder.MODE_INVERTED ); + mFSKDecoder.setListener( mNRZDecoder ); + + /* Message Framer */ + mMessageFramer = new MessageFramer( SyncPattern.MDC1200.getPattern(), + sMESSAGE_LENGTH ); + mNRZDecoder.setListener( mMessageFramer ); + + /* Message Processor */ + mMessageProcessor = new MDCMessageProcessor( aliasList ); + mMessageProcessor.addMessageListener( this ); + mMessageFramer.addMessageListener( mMessageProcessor ); + } + + public void dispose() + { + super.dispose(); + + mDecimationFilter.dispose(); + mBandPassFilter.dispose(); + mFSKDecoder.dispose(); + mNRZDecoder.dispose(); + mMessageFramer.dispose(); + mMessageProcessor.dispose(); + } + + @Override + public DecoderType getType() + { + return DecoderType.MDC1200; + } + + /* Instrumentation */ + @Override + public List getTaps() + { + if( mAvailableTaps == null ) + { + mAvailableTaps = new ArrayList(); + + mAvailableTaps.add( + new FloatTap( INSTRUMENT_INPUT, 0, 1.0f ) ); + mAvailableTaps.add( + new FloatTap( INSTRUMENT_BANDPASS_FILTER_TO_FSK2_DEMOD, 0, 0.5f ) ); + mAvailableTaps.addAll( mFSKDecoder.getTaps() ); + mAvailableTaps.add( + new BinaryTap( INSTRUMENT_FSK2_DECODER_TO_NRZ_DECODER, 0, 0.025f ) ); + mAvailableTaps.add( + new BinaryTap( INSTRUMENT_NRZI_DECODER_TO_MESSAGE_FRAMER, 0, 0.025f ) ); + } + + return mAvailableTaps; + } + + @Override + public void addTap( Tap tap ) + { + mFSKDecoder.addTap( tap ); + + switch( tap.getName() ) + { + case INSTRUMENT_INPUT: + addFloatListener( (FloatTap)tap ); + break; + case INSTRUMENT_BANDPASS_FILTER_TO_FSK2_DEMOD: + /* wire the tap between the bandpass filter and the demod */ + FloatTap floatTap = (FloatTap)tap; + mBandPassFilter.setListener( floatTap ); + floatTap.setListener( mFSKDecoder ); + break; + case INSTRUMENT_FSK2_DECODER_TO_NRZ_DECODER: + /* wire between FSK2 decoder and NRZ decoder */ + BinaryTap binTap = (BinaryTap)tap; + mFSKDecoder.setListener( binTap ); + binTap.setListener( mNRZDecoder ); + break; + case INSTRUMENT_NRZI_DECODER_TO_MESSAGE_FRAMER: + /* wire between demod and framer */ + BinaryTap binInvTap = (BinaryTap)tap; + mNRZDecoder.setListener( binInvTap ); + binInvTap.setListener( mMessageFramer ); + break; + default: + } + } + + @Override + public void removeTap( Tap tap ) + { + mFSKDecoder.removeTap( tap ); + + switch( tap.getName() ) + { + case INSTRUMENT_INPUT: + removeFloatListener( (FloatTap)tap ); + break; + case INSTRUMENT_BANDPASS_FILTER_TO_FSK2_DEMOD: + mBandPassFilter.setListener( mFSKDecoder ); + break; + case INSTRUMENT_FSK2_DECODER_TO_NRZ_DECODER: + mFSKDecoder.setListener( mNRZDecoder ); + break; + case INSTRUMENT_NRZI_DECODER_TO_MESSAGE_FRAMER: + mNRZDecoder.setListener( mMessageFramer ); + break; + default: + } + } +} diff --git a/src/decode/mdc1200/MDCMessage.java b/src/decode/mdc1200/MDCMessage.java new file mode 100644 index 00000000..4e1cb16f --- /dev/null +++ b/src/decode/mdc1200/MDCMessage.java @@ -0,0 +1,319 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package decode.mdc1200; + +import java.text.SimpleDateFormat; +import java.util.Date; + +import map.Plottable; +import message.Message; +import alias.Alias; +import alias.AliasList; +import bits.BitSetBuffer; + +public class MDCMessage extends Message +{ + private static SimpleDateFormat mSDF = new SimpleDateFormat( "yyyyMMdd HHmmss" ); + + private static int[] sSYNC1 = { 0,1,2,3,4,5,6,7,8,9, + 10,11,12,13,14,15,16,17,18,19, + 20,21,22,23,24,25,26,27,28,29, + 30,31,32,33,34,35,36,37,38,39 }; + + private static int[] sOPCODE = { 47,46,45,44,43,42,41,40 }; + private static int sANI_FLAG = 40; + private static int sDIRECTION_FLAG = 45; + private static int sACKNOWLEDGE_REQUIRED_FLAG = 46; + private static int sPACKET_TYPE_FLAG = 47; + private static int sEMERGENCY_FLAG = 48; + private static int[] sARGUMENT = { 49,50,51,52,53,54 }; + private static int sBOT_EOT_FLAG = 55; + private static int[] sDIGIT_2 = { 59,58,57,56 }; + private static int[] sDIGIT_1 = { 63,62,61,60 }; + private static int[] sDIGIT_4 = { 67,66,65,64 }; + private static int[] sDIGIT_3 = { 71,70,69,68 }; + + private BitSetBuffer mMessage; + private AliasList mAliasList; + + public MDCMessage( BitSetBuffer message, AliasList list ) + { + mMessage = message; + mAliasList = list; + } + + public boolean isValid() + { + //TODO: add CRC and/or convolution decoding/repair + return true; + } + + public PacketType getPacketType() + { + if( mMessage.get( sPACKET_TYPE_FLAG ) ) + { + return PacketType.DATA; + } + else + { + return PacketType.CMND; + } + } + + public Acknowledge getResponse() + { + if( mMessage.get( sACKNOWLEDGE_REQUIRED_FLAG ) ) + { + return Acknowledge.YES; + } + else + { + return Acknowledge.NO; + } + } + + public Direction getDirection() + { + if( mMessage.get( sDIRECTION_FLAG ) ) + { + return Direction.OUT; + } + else + { + return Direction.IN; + } + } + + public int getOpcode() + { + return mMessage.getInt( sOPCODE ); + } + + public int getArgument() + { + return mMessage.getInt( sARGUMENT ); + } + + public boolean isEmergency() + { + return mMessage.get( sEMERGENCY_FLAG ); + } + + + public boolean isANI() + { + return mMessage.get( sANI_FLAG ); + } + + public boolean isBOT() + { + return mMessage.get( sBOT_EOT_FLAG ); + } + + public boolean isEOT() + { + return !mMessage.get( sBOT_EOT_FLAG ); + } + + public String getUnitID() + { + StringBuilder sb = new StringBuilder(); + + sb.append( Integer.toHexString( mMessage.getInt( sDIGIT_1 ) ).toUpperCase() ); + sb.append( Integer.toHexString( mMessage.getInt( sDIGIT_2 ) ).toUpperCase() ); + sb.append( Integer.toHexString( mMessage.getInt( sDIGIT_3 ) ).toUpperCase() ); + sb.append( Integer.toHexString( mMessage.getInt( sDIGIT_4 ) ).toUpperCase() ); + + return sb.toString(); + } + + public MDCMessageType getMessageType() + { + switch( getOpcode() ) + { + case 0: + if( isEmergency() ) + { + return MDCMessageType.EMERGENCY; + } + case 1: + return MDCMessageType.ANI; + default: + return MDCMessageType.UNKNOWN; + } + } + + @Override + public String toString() + { + StringBuilder sb = new StringBuilder(); + + sb.append( mSDF.format( new Date( System.currentTimeMillis() ) ) ); + + sb.append( getMessage() ); + + sb.append( " [" + mMessage.toString() + "]" ); + + return sb.toString(); + } + + /** + * Pads spaces onto the end of the value to make it 'places' long + */ + public String pad( String value, int places, String padCharacter ) + { + StringBuilder sb = new StringBuilder(); + + sb.append( value ); + + while( sb.length() < places ) + { + sb.append( padCharacter ); + } + + return sb.toString(); + } + + /** + * Pads an integer value with additional zeroes to make it decimalPlaces long + */ + public String format( int number, int decimalPlaces ) + { + StringBuilder sb = new StringBuilder(); + + int paddingRequired = decimalPlaces - ( String.valueOf( number ).length() ); + + for( int x = 0; x < paddingRequired; x++) + { + sb.append( "0" ); + } + + sb.append( number ); + + return sb.toString(); + } + + @SuppressWarnings( "unused" ) + private int getInt( int[] bits ) + { + int retVal = 0; + + for( int x = 0; x < bits.length; x++ ) + { + if( mMessage.get( bits[ x ] ) ) + { + retVal += 1< + ******************************************************************************/ +package decode.mdc1200; + +import message.Message; +import sample.Broadcaster; +import sample.Listener; +import alias.AliasList; +import bits.BitSetBuffer; + +public class MDCMessageProcessor implements Listener +{ + private static int sMESSAGE_LENGTH = 112; + + private Broadcaster mBroadcaster = new Broadcaster(); + private AliasList mAliasList; + + public MDCMessageProcessor( AliasList aliasList ) + { + mAliasList = aliasList; + } + + public void dispose() + { + mBroadcaster.dispose(); + } + + @Override + public void receive( BitSetBuffer buffer ) + { + /** + * De-interleave the 112 bits of message one, starting after the sync + * pattern + */ + deinterleave( buffer, 40 ); + + /** + * For simplicity, we also deinterleave message 2, which may or may not + * be there, starting after the initial sync ( 0 - 39 ) and the initial + * message ( 40 - 151 ) and the second sync ( 152 - 191 ). + */ + deinterleave( buffer, 192 ); + + /** + * Wrap the buffer in a message along with the designated alias list + * and send it on its merry way + */ + MDCMessage message = new MDCMessage( buffer, mAliasList ); + + mBroadcaster.receive( message ); + } + + public void addMessageListener( Listener listener ) + { + mBroadcaster.addListener( listener ); + } + + public void removeMessageListener( Listener listener ) + { + mBroadcaster.removeListener( listener ); + } + + /** + * Removes NRZ-I encoding from the entire buffer. Assumes that the initial + * bit will be a 0 after decode, despite what is currently in the buffer, + * in order to maintain correct polarity of the decoded message. + */ + private void removeNRZIEncoding( BitSetBuffer buffer ) + { + /** + * Clear bit position 0, the first bit of the sync pattern, to start + * the decode with the correct polarity + */ + buffer.clear( 0 ); + + /** + * Set each bit according to the state of the previous bit using XOR + * as follows: + * + * 0 = same as previous state + * 1 = inverse of previous state + */ + for( int x = 1; x < buffer.size(); x++ ) + { + if( buffer.get( x - 1 ) ^ buffer.get( x ) ) + { + buffer.set( x ); + } + else + { + buffer.clear( x ); + } + } + } + + /** + * Deinterleaves a 112-bit packet, starting at the offset into the buffer + */ + private void deinterleave( BitSetBuffer buffer, int offset ) + { + if( buffer.size() < sMESSAGE_LENGTH + offset ) + { + throw new IllegalArgumentException( "MDCMessageProcessor - " + + "cannot deinterleave message - message buffer too short" ); + } + + BitSetBuffer deinterleaved = new BitSetBuffer( 112 ); + + int deinterleavedPointer = 0; + + for( int column = 0; column < 16; column++ ) + { + for( int row = 0; row < 7; row++ ) + { + if( buffer.get( ( row * 16 ) + column + offset ) ) + { + deinterleaved.set( deinterleavedPointer ); + } + + deinterleavedPointer++; + } + } + + //Overlay the deinterleaved bits back onto the source message and + //return it + buffer.clear( offset, offset + sMESSAGE_LENGTH ); + + for( int x = 0; x < sMESSAGE_LENGTH; x++ ) + { + if( deinterleaved.get( x ) ) + { + buffer.set( x + offset ); + } + else + { + buffer.clear( x + offset ); + } + } + } +} diff --git a/src/decode/mdc1200/MDCMessageType.java b/src/decode/mdc1200/MDCMessageType.java new file mode 100644 index 00000000..084d34ff --- /dev/null +++ b/src/decode/mdc1200/MDCMessageType.java @@ -0,0 +1,40 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package decode.mdc1200; + +public enum MDCMessageType +{ + ACKNOWLEDGE( "Acknowledge" ), + ANI( "ANI" ), + EMERGENCY( "Emergency" ), + PAGING( "Paging" ), + STATUS( "Status" ), + UNKNOWN( "Unk" ); + + private String mLabel; + + private MDCMessageType( String label ) + { + mLabel = label; + } + + public String getLabel() + { + return mLabel; + } +} diff --git a/src/decode/mdc1200/MDCPanel.java b/src/decode/mdc1200/MDCPanel.java new file mode 100644 index 00000000..22226a57 --- /dev/null +++ b/src/decode/mdc1200/MDCPanel.java @@ -0,0 +1,161 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package decode.mdc1200; + +import java.awt.EventQueue; + +import javax.swing.JLabel; + +import net.miginfocom.swing.MigLayout; +import settings.ColorSetting; +import settings.ColorSetting.ColorSettingName; +import settings.Setting; +import settings.SettingsManager; +import controller.state.AuxStatePanel; +import controller.state.ChannelState.ChangedAttribute; + +public class MDCPanel extends AuxStatePanel +{ + private static final long serialVersionUID = 1L; + private static final String sPROTOCOL = "MDC-1200"; + + private JLabel mFromLabel = new JLabel( " " ); + private JLabel mFrom = new JLabel(); + private JLabel mFromAlias = new JLabel(); + + private JLabel mToLabel = new JLabel( " " ); + private JLabel mTo = new JLabel(); + private JLabel mToAlias = new JLabel(); + + private JLabel mProtocol = new JLabel( sPROTOCOL ); + private JLabel mMessage = new JLabel(); + private JLabel mMessageType = new JLabel(); + + public MDCPanel( SettingsManager settingsManager, MDCChannelState state ) + { + super( settingsManager, state ); + + init(); + } + + public MDCChannelState getState() + { + return (MDCChannelState)mAuxChannelState; + } + + private void init() + { + setOpaque( false ); //Use the parent panel's background color + + setLayout( new MigLayout( "insets 1 0 0 0", "[grow,fill]", "[]0[]0[]") ); + + addLabel( mFromLabel, false ); + addLabel( mFrom, false ); + addLabel( mFromAlias, true ); + + addLabel( mToLabel, false ); + addLabel( mTo, false ); + addLabel( mToAlias, true ); + + addLabel( mProtocol, false ); + addLabel( mMessageType, false ); + addLabel( mMessage, true ); + } + + @Override + public void receive( final ChangedAttribute changedAttribute ) + { + EventQueue.invokeLater( new Runnable() + { + @Override + public void run() + { + switch( changedAttribute ) + { + case FROM_TALKGROUP: + String from = getState().getFrom(); + + if( from != null ) + { + mFromLabel.setText( "FM:" ); + } + else + { + mFromLabel.setText( " " ); + } + mFrom.setText( from ); + break; + case FROM_TALKGROUP_ALIAS: + updateAliasLabel( mFromAlias, + getState().getFromAlias() ); + break; + case TO_TALKGROUP: + String to = getState().getTo(); + + if( to != null ) + { + mToLabel.setText( "TO:" ); + } + else + { + mToLabel.setText( " " ); + } + mTo.setText( to ); + break; + case TO_TALKGROUP_ALIAS: + updateAliasLabel( mToAlias, + getState().getToAlias() ); + break; + case MESSAGE: + mMessage.setText( getState().getMessage() ); + break; + case MESSAGE_TYPE: + mMessageType.setText( getState().getMessageType() ); + break; + } + + repaint(); + } + } ); + } + + @Override + public void settingChanged( Setting setting ) + { + super.settingChanged( setting ); + + if( setting instanceof ColorSetting ) + { + ColorSetting color = (ColorSetting)setting; + + if( color.getColorSettingName() == + ColorSettingName.CHANNEL_STATE_LABEL_AUX_DECODER ) + { + mFromLabel.setForeground( color.getColor() ); + mFrom.setForeground( color.getColor() ); + mFromAlias.setForeground( color.getColor() ); + mToLabel.setForeground( color.getColor() ); + mTo.setForeground( color.getColor() ); + mToAlias.setForeground( color.getColor() ); + mProtocol.setForeground( color.getColor() ); + mMessageType.setForeground( color.getColor() ); + mMessage.setForeground( color.getColor() ); + } + } + } +} diff --git a/src/decode/mpt1327/MPT1327ActivitySummary.java b/src/decode/mpt1327/MPT1327ActivitySummary.java new file mode 100644 index 00000000..12a98b88 --- /dev/null +++ b/src/decode/mpt1327/MPT1327ActivitySummary.java @@ -0,0 +1,171 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package decode.mpt1327; + +import java.util.Iterator; +import java.util.TreeSet; + +import message.Message; +import sample.Listener; +import alias.Alias; +import alias.AliasList; +import controller.activity.ActivitySummaryProvider; + +public class MPT1327ActivitySummary implements ActivitySummaryProvider, + Listener +{ + private static final int sINT_NULL_VALUE = -1; + + private AliasList mAliasList; + private TreeSet mIdents = new TreeSet(); + private int mSite; + + /** + * Compiles a summary of active talkgroups, unique radio ids encountered + * in the decoded messages. Talkgroups have to be heard a minimum of twice + * to be considered valid ... in order to weed out the invalid ones. + * + * Returns a textual summary of the activity + */ + public MPT1327ActivitySummary( AliasList list ) + { + mAliasList = list; + } + + /** + * Cleanup method + */ + public void dispose() + { + mIdents.clear(); + } + + @Override + public void receive( Message message ) + { + if( message instanceof MPT1327Message ) + { + MPT1327Message mpt = ((MPT1327Message)message); + + if( mpt.isValid() ) + { + switch( mpt.getMessageType() ) + { + case ACK: + mIdents.add( mpt.getFromID() ); + break; + case ACKI: + mIdents.add( mpt.getFromID() ); + mIdents.add( mpt.getToID() ); + break; + case ALH: + mSite = mpt.getSiteID(); + break; + case GTC: + if( mpt.hasFromID() ) + { + mIdents.add( mpt.getFromID() ); + } + if( mpt.hasToID() ) + { + mIdents.add( mpt.getToID() ); + } + break; + } + } + } + } + + @Override + public String getSummary() + { + StringBuilder sb = new StringBuilder(); + + sb.append( "Activity Summary\n" ); + sb.append( "Decoder:\tMPT-1327\n" ); + sb.append( "Site:\t" ); + + if( mSite != 0 ) + { + sb.append( mSite ); + + if( mAliasList != null ) + { + Alias siteAlias = mAliasList.getSiteID( mSite ); + + if( siteAlias != null ) + { + sb.append( " " ); + sb.append( siteAlias.getName() ); + sb.append( "\n" ); + } + } + + sb.append( "Network:\t" ); + + if( ( mSite & 0x4000 ) == 0x4000 ) + { + sb.append( "National #" ); + int net = (int)Long.rotateRight( ( mSite & 0x3000 ) , 12 ); + sb.append( net ); + sb.append( "\n" ); + } + else + { + sb.append( "Regional\n" ); + } + + } + else + { + sb.append( "Unknown\n" ); + } + + sb.append( "\n" ); + + sb.append( "Talkgroups\n" ); + if( mIdents.isEmpty() ) + { + sb.append( " None\n" ); + } + else + { + Iterator it = mIdents.iterator(); + + while( it.hasNext() ) + { + String ident = it.next(); + + sb.append( " " ); + sb.append( ident ); + sb.append( " " ); + + Alias alias = mAliasList.getMPT1327Alias( ident ); + + if( alias != null ) + { + sb.append( alias.getName() ); + } + + sb.append( "\n" ); + } + } + + return sb.toString(); + } +} diff --git a/src/decode/mpt1327/MPT1327CallEvent.java b/src/decode/mpt1327/MPT1327CallEvent.java new file mode 100644 index 00000000..a795797c --- /dev/null +++ b/src/decode/mpt1327/MPT1327CallEvent.java @@ -0,0 +1,154 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package decode.mpt1327; + +import alias.Alias; +import alias.AliasList; +import controller.activity.CallEvent; +import decode.DecoderType; + +public class MPT1327CallEvent extends CallEvent +{ + private int mChannel; + private int mFrequency; + + public MPT1327CallEvent( CallEventType callEventType, + AliasList aliasList, + String fromID, + String toID, + int channel, + int frequency, + String details ) + { + super( DecoderType.MPT1327, callEventType, + aliasList, fromID, toID, details ); + + mChannel = channel; + mFrequency = frequency; + } + + private Alias getAlias( String mpt1327Talkgroup ) + { + if( hasAliasList() ) + { + return getAliasList().getMPT1327Alias( mpt1327Talkgroup ); + } + + return null; + } + + @Override + public Alias getFromIDAlias() + { + return getAlias( getFromID() ); + } + + @Override + public Alias getToIDAlias() + { + return getAlias( getToID() ); + } + + @Override + public int getChannel() + { + return mChannel; + } + + @Override + public int getFrequency() + { + return mFrequency; + } + + public static class Builder + { + /* Required parameters */ + private CallEventType mCallEventType; + + /* Optional parameters */ + private AliasList mAliasList; + private String mFromID; + private String mToID; + private String mDetails; + private int mChannel; + private int mFrequency; + + public Builder( CallEventType callEventType ) + { + mCallEventType = callEventType; + } + + public Builder aliasList( AliasList aliasList ) + { + mAliasList = aliasList; + return this; + } + + public Builder from( String val ) + { + mFromID = val; + return this; + } + + public Builder channel( int channel ) + { + mChannel = channel; + return this; + } + + public Builder details( String details ) + { + mDetails = details; + return this; + } + + public Builder frequency( int frequency ) + { + mFrequency = frequency; + return this; + } + + public Builder to( String toID ) + { + mToID = toID; + return this; + } + + public MPT1327CallEvent build() + { + return new MPT1327CallEvent( this ); + } + + } + + /** + * Private constructor for the builder + */ + private MPT1327CallEvent( Builder builder ) + { + this( builder.mCallEventType, + builder.mAliasList, + builder.mFromID, + builder.mToID, + builder.mChannel, + builder.mFrequency, + builder.mDetails ); + } + +} diff --git a/src/decode/mpt1327/MPT1327ChannelState.java b/src/decode/mpt1327/MPT1327ChannelState.java new file mode 100644 index 00000000..62b57c21 --- /dev/null +++ b/src/decode/mpt1327/MPT1327ChannelState.java @@ -0,0 +1,346 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package decode.mpt1327; + +import message.Message; +import sample.Listener; +import source.config.SourceConfigTuner; +import alias.AliasList; +import audio.SquelchListener; +import audio.SquelchListener.SquelchState; +import controller.activity.CallEvent.CallEventType; +import controller.channel.Channel; +import controller.channel.Channel.ChannelType; +import controller.channel.ChannelMap; +import controller.channel.ProcessingChain; +import controller.state.AuxChannelState; +import controller.state.ChannelState; +import decode.config.DecodeConfigMPT1327; + +public class MPT1327ChannelState extends ChannelState +{ + private int mSite; + private String mFromTalkgroup; + private String mToTalkgroup; + private int mChannelNumber; + private ChannelType mChannelType; + private ChannelMap mChannelMap; + private MPT1327ActivitySummary mActivitySummary; + + public MPT1327ChannelState( ProcessingChain processingChain, + AliasList aliasList, + ChannelMap channelMap ) + { + super( processingChain, aliasList ); + + mActivitySummary = new MPT1327ActivitySummary( aliasList ); + + mChannelMap = channelMap; + + mChannelType = processingChain.getChannel().getChannelType(); + } + + public void dispose() + { + super.dispose(); + + mActivitySummary.dispose(); + mActivitySummary = null; + } + + @Override + public String getActivitySummary() + { + StringBuilder sb = new StringBuilder(); + + sb.append( mActivitySummary.getSummary() ); + + for( AuxChannelState state: mAuxChannelStates ) + { + sb.append( state.getActivitySummary() ); + sb.append( "\n\n" ); + } + + return sb.toString(); + } + + @Override + public void receive( Message message ) + { + super.receive( message ); + + if( message instanceof MPT1327Message ) + { + mActivitySummary.receive( message ); + + MPT1327Message mpt = (MPT1327Message)message; + + if( mpt.isValid() ) + { + switch( mpt.getMessageType() ) + { + case AHYC: + mCallEventModel.add( + new MPT1327CallEvent.Builder( CallEventType.SDM ) + .aliasList( getProcessingChain().getAliasList() ) + .channel( mChannelNumber ) + .details( "Short Data Message" ) + .frequency( mChannelMap.getFrequency( mChannelNumber ) ) + .from( mpt.getFromID() ) + .to( mpt.getToID() ) + .build() ); + break; + case AHYQ: + mCallEventModel.add( + new MPT1327CallEvent.Builder( CallEventType.STATUS ) + .aliasList( getProcessingChain().getAliasList() ) + .channel( mChannelNumber ) + .details( mpt.getStatusMessage() ) + .frequency( mChannelMap.getFrequency( mChannelNumber ) ) + .from( mpt.getFromID() ) + .to( mpt.getToID() ) + .build() ); + break; + case ALH: + int sys = mpt.getSiteID(); + + if( sys != 0 && sys != mSite ) + { + mSite = sys; + broadcastChange( ChangedAttribute.CHANNEL_SITE_NUMBER ); + } + setState( State.CONTROL ); + break; + case GTC: + int channelNumber = mpt.getChannel(); + + if( !getProcessingChain().getChannel() + .hasTrafficChannel( channelNumber ) ) + { + String aliasListName = mProcessingChain + .getChannel().getAliasListName(); + + Channel traffic = getTrafficChannel( channelNumber, + aliasListName ); + + /* Set the system and site to same as control channel */ + traffic.setSystem( getProcessingChain().getChannel() + .getSystem(), false ); + traffic.setSite( getProcessingChain().getChannel() + .getSite(), false ); + + /* Add the traffic channel to the parent control channel */ + getProcessingChain().getChannel() + .addTrafficChannel( channelNumber, traffic ); + + /* Start the traffic channel */ + traffic.setEnabled( true ); + + /* Set traffic channel state info */ + MPT1327ChannelState trafficState = + (MPT1327ChannelState)traffic + .getProcessingChain().getChannelState(); + + MPT1327ChannelState controlState = + (MPT1327ChannelState)getProcessingChain() + .getChannelState(); + + trafficState.setChannelMap( controlState.getChannelMap() ); + trafficState.setChannelNumber( channelNumber ); + trafficState.setFromTalkgroup( mpt.getFromID() ); + trafficState.setToTalkgroup( mpt.getToID() ); + + /* Add this control channel as message listener on the + * traffic channel so we can receive call tear-down + * messages */ + traffic.addListener( (Listener)this ); + + CallEventType type = traffic.isProcessing() ? + CallEventType.CALL_START : + CallEventType.CALL_START_NO_TUNER; + + /* + * Set the traffic channel's call event model to + * share the control channel's call event model + */ + traffic.getProcessingChain().getChannelState() + .setCallEventModel( mCallEventModel ); + + mCallEventModel.add( + new MPT1327CallEvent.Builder( type ) + .aliasList( traffic.getProcessingChain().getAliasList() ) + .channel( channelNumber ) + .frequency( mChannelMap.getFrequency( channelNumber ) ) + .from( mpt.getFromID() ) + .to( mpt.getToID() ) + .build() ); + } + + int frequency = mChannelMap.getFrequency( channelNumber ); + + break; + case CLEAR: + case MAINT: + if( mChannelType == ChannelType.TRAFFIC ) + { + fade( CallEventType.CALL_END ); + } + break; + } + } + } + } + + /** + * Intercept the fade event so that we can generate a call end event + */ + @Override + public void fade( final CallEventType type ) + { + /* + * We can receive multiple call tear-down messages -- only post a call + * end event for the one that can change the state to fade + */ + if( getState().canChangeTo( State.FADE ) ) + { + mCallEventModel.add( + new MPT1327CallEvent.Builder( type ) + .aliasList( getAliasList() ) + .channel( mChannelNumber ) + .frequency( mChannelMap.getFrequency( mChannelNumber ) ) + .from( mFromTalkgroup ) + .to( mToTalkgroup ) + .build() ); + } + + super.fade( type ); + } + + /** + * Make the ConventionalChannelState always unsquelched + */ + public void addListener( SquelchListener listener ) + { + super.addListener( listener ); + + super.setSquelchState( SquelchState.UNSQUELCH ); + } + + public void reset() + { + mFromTalkgroup = null; + broadcastChange( ChangedAttribute.FROM_TALKGROUP ); + mToTalkgroup = null; + broadcastChange( ChangedAttribute.TO_TALKGROUP ); + + super.reset(); + } + + public int getSite() + { + return mSite; + } + + public ChannelMap getChannelMap() + { + return mChannelMap; + } + + public void setChannelMap( ChannelMap channelMap ) + { + mChannelMap = channelMap; + } + + public String getFromTalkgroup() + { + return mFromTalkgroup; + } + + /** + * Set the talkgroup. This is used primarily for traffic channels since + * the talkgroup will already have been identified prior to the traffic + * channel being created. + */ + public void setFromTalkgroup( String fromTalkgroup ) + { + mFromTalkgroup = fromTalkgroup; + + broadcastChange( ChangedAttribute.FROM_TALKGROUP ); + } + + public String getToTalkgroup() + { + return mToTalkgroup; + } + + /** + * Set the talkgroup. This is used primarily for traffic channels since + * the talkgroup will already have been identified prior to the traffic + * channel being created. + */ + public void setToTalkgroup( String toTalkgroup ) + { + mToTalkgroup = toTalkgroup; + + broadcastChange( ChangedAttribute.TO_TALKGROUP ); + } + + public int getChannelNumber() + { + return mChannelNumber; + } + /** + * Set the channel number. This is used primarily for traffic channels since + * the channel will already have been identified prior to the traffic + * channel being created. + */ + public void setChannelNumber( int channel ) + { + mChannelNumber = channel; + + broadcastChange( ChangedAttribute.CHANNEL_NUMBER ); + } + + public String getAliasListName() + { + return mProcessingChain.getChannel().getAliasListName(); + } + + private Channel getTrafficChannel( int channelNumber, String aliasListName ) + { + Channel traffic = new Channel( "Traffic Channel " + channelNumber, + ChannelType.TRAFFIC ); + + traffic.setResourceManager( getProcessingChain().getResourceManager() ); + + SourceConfigTuner source = new SourceConfigTuner(); + + int frequency = mChannelMap.getFrequency( channelNumber ); + + source.setFrequency( frequency ); + + traffic.setSourceConfiguration( source ); + + DecodeConfigMPT1327 decode = new DecodeConfigMPT1327(); + traffic.setDecodeConfiguration( decode ); + + traffic.setAliasListName( aliasListName ); + + return traffic; + } +} diff --git a/src/decode/mpt1327/MPT1327Decoder.java b/src/decode/mpt1327/MPT1327Decoder.java new file mode 100644 index 00000000..55378951 --- /dev/null +++ b/src/decode/mpt1327/MPT1327Decoder.java @@ -0,0 +1,283 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package decode.mpt1327; + +import instrument.Instrumentable; +import instrument.tap.Tap; +import instrument.tap.stream.BinaryTap; +import instrument.tap.stream.FloatTap; + +import java.util.ArrayList; +import java.util.List; + +import sample.Broadcaster; +import source.Source.SampleType; +import alias.AliasList; +import bits.MessageFramer; +import bits.SyncPattern; +import decode.Decoder; +import decode.DecoderType; +import dsp.filter.DCRemovalFilter; +import dsp.filter.FilterFactory; +import dsp.filter.Filters; +import dsp.filter.FloatFIRFilter; +import dsp.filter.FloatHalfBandFilter; +import dsp.filter.Window.WindowType; +import dsp.fsk.FSK2Decoder; +import dsp.fsk.FSK2Decoder.Output; +import dsp.nbfm.NBFMDemodulator; + +/** + * MPT1327 Decoder - 1200 baud 2FSK decoder that can process 48k sample rate + * complex or floating point samples and output fully framed MPT1327 control and + * traffic messages. + */ +public class MPT1327Decoder extends Decoder implements Instrumentable +{ + /* Determines how quickly the DC remove filter responds */ + private static final double sDC_REMOVAL_RATIO = 0.95; + + /* Decimated sample rate ( 48,000 / 2 = 24,000 ) feeding the decoder */ + private static final int sDECIMATED_SAMPLE_RATE = 24000; + + /* Baud or Symbol Rate */ + private static final int sSYMBOL_RATE = 1200; + + /* Message length -- longest possible message is: + * 4xREVS + 16xSYNC + 64xADD1 + 64xDCW1 + 64xDCW2 + 64xDCW3 + 64xDCW4 */ + private static final int sMESSAGE_LENGTH = 350; + + /* Instrumentation Taps */ + private static final String INSTRUMENT_INPUT = "Tap Point: Input"; + private static final String INSTRUMENT_HB1_FILTER_TO_LOW_PASS = + "Tap Point: Half-band Decimation Filter > < Low Pass Filter"; + private static final String INSTRUMENT_LOW_PASS_TO_DECODER = + "Tap Point: Low Pass Filter > < Decoder"; + private static final String INSTRUMENT_DECODER_TO_FRAMER = + "Tap Point: Decoder > < Sync Detect/Message Framer"; + private List mAvailableTaps; + + private NBFMDemodulator mNBFMDemodulator; + private FloatHalfBandFilter mDecimationFilter; + private FloatFIRFilter mLowPassFilter; + private DCRemovalFilter mDCRemovalFilter; + private FSK2Decoder mFSKDecoder; + private Broadcaster mSymbolBroadcaster; + private MessageFramer mControlMessageFramer; + private MessageFramer mTrafficMessageFramer; + private MPT1327MessageProcessor mMessageProcessor; + + public MPT1327Decoder( SampleType sampleType, AliasList aliasList ) + { + super( sampleType ); + + /* If we're receiving complex samples, do FM demodulation and DC removal + * and feed the output back to this decoder */ + if( mSourceSampleType == SampleType.COMPLEX ) + { + /* I/Q low pass filtering narrow band FM demodulator */ + mNBFMDemodulator = new NBFMDemodulator( + FilterFactory.getLowPass( 48000, 4000, 73, WindowType.HAMMING ), + 1.0002 ); + + this.addComplexListener( mNBFMDemodulator ); + + /* DC removal filter */ + mDCRemovalFilter = new DCRemovalFilter( sDC_REMOVAL_RATIO ); + + if( mNBFMDemodulator != null ) + { + mNBFMDemodulator.addListener( mDCRemovalFilter ); + mDCRemovalFilter.setListener( this.getFloatReceiver() ); + } + } + + /* Demodulated float sample processing */ + + /* Decimation filter - 48000 / 2 = 24000 output */ + mDecimationFilter = new FloatHalfBandFilter( + Filters.FIR_HALF_BAND_31T_ONE_EIGHTH_FCO, 1.0002 ); + addFloatListener( mDecimationFilter ); + + /* Low pass filter: 2kHz to pass the 1200 & 1800 Hz FSK */ + mLowPassFilter = new FloatFIRFilter( + FilterFactory.getLowPass( 24000, //sampleRate, + 2000, //passFrequency, + 4000, //stopFrequency, + 60, //attenuation, + WindowType.HANNING, //windowType, + true ) //forceOddLength + , 2.0 ); //Gain + mDecimationFilter.setListener( mLowPassFilter ); + + /* 2FSK Decoder with output inverted */ + mFSKDecoder = new FSK2Decoder( 24000, 1200, Output.INVERTED ); + mLowPassFilter.setListener( mFSKDecoder ); + + mSymbolBroadcaster = new Broadcaster(); + mFSKDecoder.setListener( mSymbolBroadcaster ); + + /* Message framer for control channel messages */ + mControlMessageFramer = + new MessageFramer( SyncPattern.MPT1327_CONTROL.getPattern(), + sMESSAGE_LENGTH ); + mSymbolBroadcaster.addListener( mControlMessageFramer ); + + /* Message framer for traffic channel massages */ + mTrafficMessageFramer = + new MessageFramer( SyncPattern.MPT1327_TRAFFIC.getPattern(), + sMESSAGE_LENGTH ); + mSymbolBroadcaster.addListener( mTrafficMessageFramer ); + + /* Fully decoded and framed messages processor */ + mMessageProcessor = new MPT1327MessageProcessor( aliasList ); + mMessageProcessor.addMessageListener( this ); + mControlMessageFramer.addMessageListener( mMessageProcessor ); + mTrafficMessageFramer.addMessageListener( mMessageProcessor ); + } + + @Override + public DecoderType getType() + { + return DecoderType.MPT1327; + } + + /** + * Cleanup method + */ + public void dispose() + { + super.dispose(); + + mSymbolBroadcaster.dispose(); + + mControlMessageFramer.dispose(); + + if( mDCRemovalFilter != null ) + { + mDCRemovalFilter.dispose(); + } + + if( mNBFMDemodulator != null ) + { + mNBFMDemodulator.dispose(); + } + + mFSKDecoder.dispose(); + + mDecimationFilter.dispose(); + + mLowPassFilter.dispose(); + + mMessageProcessor.dispose(); + + + mTrafficMessageFramer.dispose(); + } + + /* Instrumentation Taps */ + @Override + public List getTaps() + { + if( mAvailableTaps == null ) + { + mAvailableTaps = new ArrayList(); + + mAvailableTaps.add( + new FloatTap( INSTRUMENT_INPUT, 0, 1.0f ) ); + mAvailableTaps.add( + new FloatTap( INSTRUMENT_HB1_FILTER_TO_LOW_PASS, 0, 0.5f ) ); + mAvailableTaps.add( + new FloatTap( INSTRUMENT_LOW_PASS_TO_DECODER, 0, 0.5f ) ); + mAvailableTaps.add( + new BinaryTap( INSTRUMENT_DECODER_TO_FRAMER, 0, 0.025f ) ); + + /* Add the taps from the FSK decoder */ + mAvailableTaps.addAll( mFSKDecoder.getTaps() ); + } + + return mAvailableTaps; + } + + @Override + public void addTap( Tap tap ) + { + /* Send request to decoder */ + mFSKDecoder.addTap( tap ); + + switch( tap.getName() ) + { + case INSTRUMENT_INPUT: + FloatTap inputTap = (FloatTap)tap; + if( mNBFMDemodulator != null ) + { + mNBFMDemodulator.addListener( inputTap ); + } + else + { + addFloatListener( inputTap ); + } + break; + case INSTRUMENT_HB1_FILTER_TO_LOW_PASS: + FloatTap hb1Tap = (FloatTap)tap; + mDecimationFilter.setListener( hb1Tap ); + hb1Tap.setListener( mLowPassFilter ); + break; + case INSTRUMENT_LOW_PASS_TO_DECODER: + FloatTap lowTap = (FloatTap)tap; + mLowPassFilter.setListener( lowTap ); + lowTap.setListener( mFSKDecoder ); + break; + case INSTRUMENT_DECODER_TO_FRAMER: + BinaryTap decoderTap = (BinaryTap)tap; + mFSKDecoder.setListener( decoderTap ); + decoderTap.setListener( mSymbolBroadcaster ); + break; + } + } + + @Override + public void removeTap( Tap tap ) + { + mFSKDecoder.removeTap( tap ); + + switch( tap.getName() ) + { + case INSTRUMENT_INPUT: + FloatTap inputTap = (FloatTap)tap; + if( mNBFMDemodulator != null ) + { + mNBFMDemodulator.removeListener( inputTap ); + } + else + { + removeFloatListener( inputTap ); + } + break; + case INSTRUMENT_HB1_FILTER_TO_LOW_PASS: + mDecimationFilter.setListener( mLowPassFilter ); + break; + case INSTRUMENT_LOW_PASS_TO_DECODER: + mLowPassFilter.setListener( mFSKDecoder ); + break; + case INSTRUMENT_DECODER_TO_FRAMER: + mFSKDecoder.setListener( mSymbolBroadcaster ); + break; + } + } +} diff --git a/src/decode/mpt1327/MPT1327Editor.java b/src/decode/mpt1327/MPT1327Editor.java new file mode 100644 index 00000000..c5ac2a38 --- /dev/null +++ b/src/decode/mpt1327/MPT1327Editor.java @@ -0,0 +1,103 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package decode.mpt1327; + +import java.util.ArrayList; + +import javax.swing.DefaultComboBoxModel; +import javax.swing.JComboBox; +import javax.swing.JLabel; + +import controller.channel.ChannelMap; +import controller.channel.ChannelNode; +import decode.DecodeEditor; +import decode.config.DecodeConfigMPT1327; +import decode.config.DecodeConfiguration; + +public class MPT1327Editor extends DecodeEditor +{ + private static final long serialVersionUID = 1L; + + private JComboBox mComboChannelMaps; + private ChannelNode mChannelNode; + + public MPT1327Editor( DecodeConfiguration config, ChannelNode channelNode ) + { + super( config ); + + mChannelNode = channelNode; + + initGUI(); + } + + private void initGUI() + { + /** + * ComboBox: Alias Lists + */ + ArrayList maps = mChannelNode.getModel().getResourceManager() + .getPlaylistManager().getPlayist().getChannelMapList().getChannelMap(); + + ChannelMap[] mapArray = maps.toArray( new ChannelMap[ maps.size() ] ); + + mComboChannelMaps = new JComboBox(); + mComboChannelMaps.setModel( + new DefaultComboBoxModel( mapArray ) ); + + add( new JLabel( "Channel Map" ), "wrap" ); + add( mComboChannelMaps, "wrap" ); + + reset(); + } + + @Override + public void save() + { + ChannelMap selected = (ChannelMap)mComboChannelMaps.getSelectedItem(); + + if( selected != null ) + { + ((DecodeConfigMPT1327)mConfig).setChannelMapName( selected.getName() ); + } + } + + @Override + public void reset() + { + String name = ((DecodeConfigMPT1327)mConfig).getChannelMapName(); + + ChannelMap selected = null; + + if( name != null ) + { + for( ChannelMap map: mChannelNode.getModel().getResourceManager() + .getPlaylistManager().getPlayist().getChannelMapList() + .getChannelMap() ) + { + if( map.getName().equalsIgnoreCase( name ) ) + { + selected = map; + } + } + } + + mComboChannelMaps.setSelectedItem( selected ); + mComboChannelMaps.requestFocus(); + mComboChannelMaps.requestFocusInWindow(); + } +} diff --git a/src/decode/mpt1327/MPT1327Message.java b/src/decode/mpt1327/MPT1327Message.java new file mode 100644 index 00000000..2e618ece --- /dev/null +++ b/src/decode/mpt1327/MPT1327Message.java @@ -0,0 +1,986 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package decode.mpt1327; + +import java.text.DecimalFormat; +import java.text.SimpleDateFormat; +import java.util.BitSet; +import java.util.Calendar; +import java.util.Date; +import java.util.GregorianCalendar; + +import map.Plottable; +import message.Message; +import alias.Alias; +import alias.AliasList; +import bits.BitSetBuffer; +import crc.CRC; +import crc.CRCFleetsync; + +public class MPT1327Message extends Message +{ + private static DecimalFormat mDecimalFormatter = new DecimalFormat("#.#####"); + private static SimpleDateFormat mSDF = new SimpleDateFormat( "yyyyMMdd HHmmss" ); + + //Calendar to use in calculating time hacks + Calendar mCalendar = new GregorianCalendar(); + + //Message parts are identified in big-endian order for correct translation + private static int BLOCK_1_START = 20; + private static int BLOCK_2_START = 84; + private static int BLOCK_3_START = 148; + private static int BLOCK_4_START = 212; + private static int BLOCK_5_START = 276; + private static int BLOCK_5_END = 340; + private static int[] REVS = { 0,1,2,3 }; + private static int[] SYNC = { 4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19 }; + + /* Block 1 Fields */ + private static int[] B1_PREFIX = { 21,22,23,24,25,26,27 }; + private static int[] B1_IDENT1 = { 28,29,30,31,32,33,34,35,36,37,38,39,40 }; + private static int[] B1_IDENT2 = { 53,54,55,56,57,58,59,60,61,62,63,64,65 }; + private static int[] B1_GTC_CHAN = { 43,44,45,46,47,48,49,50,51,52 }; + private static int[] B1_MESSAGE_TYPE = { 41,42,43,44,45,46,47,48,49 }; + private static int[] B1_STATUS_MESSAGE = { 66,67,68,69,70 }; + private static int[] B1_SYSTEM_ID = { 26,27,28,29,30,31,32,33,34,35,36,37,38,39,40 }; + private static int[] B1_SYSDEF = { 21,22,23,24,25 }; + private static int[] B1_CHANNEL = { 35,36,37,38,39,40,41,42,43,44 }; + private static int[] B1_TRAFFIC_CHANNEL = { 21,22,23,24,25,26,27,28,29,30 }; + private static int[] B1_CONTROL_CHANNEL = { 31,32,33,34,35,36,37,38,39,40 }; + private static int[] B1_ADJSITE = { 49,50,51,52 }; + + /* Block 2 Fields */ + private static int[] B2_SYSTEM_ID = { 85,86,87,88,89,90,91,92,93,94,95,96,97,98,99 }; + private static int[] B2_PREFIX = { 112,113,114,115,116,117,118 }; + private static int[] B2_IDENT2 = { 119,120,121,122,123,124,125,126,127,128,129,130,131 }; + + private BitSetBuffer mMessage; + private CRC[] mCRC = new CRC[ 5 ]; + private AliasList mAliasList; + private MPTMessageType mMessageType; + + public MPT1327Message( BitSetBuffer message, AliasList list ) + { + mMessage = message; + mAliasList = list; + + checkParity( 0, BLOCK_1_START, BLOCK_2_START ); + + if( isValid() ) + { + mMessageType = getMessageType(); + + switch( mMessageType ) + { + /* 1 data block messages */ + case CLEAR: + case MAINT: + case MOVE: + break; + /* 2 data block messages */ + case AHYQ: + case ALH: + checkParity( 1, BLOCK_2_START, BLOCK_3_START ); + break; + /* 3 data block messages */ + /* 4 data block messages */ + case ACKT: + checkParity( 1, BLOCK_2_START, BLOCK_3_START ); + checkParity( 2, BLOCK_3_START, BLOCK_4_START ); + checkParity( 3, BLOCK_4_START, BLOCK_5_START ); + break; + /* 5 data block messages */ + case AHYC: + checkParity( 1, BLOCK_2_START, BLOCK_3_START ); + checkParity( 2, BLOCK_3_START, BLOCK_4_START ); + checkParity( 3, BLOCK_4_START, BLOCK_5_START ); + checkParity( 3, BLOCK_5_START, BLOCK_5_END ); + break; + case ACK: + case ACKB: + case ACKE: + case ACKI: + case ACKQ: + case ACKV: + case ACKX: + case AHOY: + case AHYP: + case AHYX: + case ALHD: + case ALHE: + case ALHF: + case ALHR: + case ALHS: + case ALHX: + case BCAST: + case GTC: + case MARK: + break; + default: + break; + } + } + else + { + mMessageType = MPTMessageType.UNKN; + } + } + + private void checkParity( int section, int start, int end ) + { + mCRC[ section ] = detectAndCorrect( start, end ); + } + + private CRC detectAndCorrect( int start, int end ) + { + BitSet original = mMessage.get( start, end ); + + CRC retVal = CRCFleetsync.check( original ); + + //Attempt to correct single-bit errors + if( retVal == CRC.FAILED_PARITY ) + { + int[] errorBitPositions = CRCFleetsync.findBitErrors( original ); + + if( errorBitPositions != null ) + { + for( int errorBitPosition: errorBitPositions ) + { + mMessage.flip( start + errorBitPosition ); + } + + retVal = CRC.CORRECTED; + } + } + + return retVal; + } + + /** + * String representing results of the parity check + * + * [P] = passes parity check + * [f] = fails parity check + * [C] = corrected message + * [-] = message section not present + */ + public String getParity() + { + return "[" + CRC.format( mCRC ) + "]"; + } + + public boolean isValid() + { + return mCRC[ 0 ] == CRC.PASSED || + mCRC[ 0 ] == CRC.CORRECTED; + } + + @Override + public String toString() + { + StringBuilder sb = new StringBuilder(); + + sb.append( mSDF.format( new Date() ) ); + + sb.append( " MPT1327 " ); + + sb.append( getParity() ); + + sb.append( " " ); + + sb.append( getMessage() ); + + sb.append( getFiller( sb, 100 ) ); + + sb.append( " [" + mMessage.toString() + "]" ); + + return sb.toString(); + } + + public MPTMessageType getMessageType() + { + int type = mMessage.getInt( B1_MESSAGE_TYPE ); + + return MPTMessageType.fromNumber( type ); + } + + public int getSiteID() + { + if( mMessageType == MPTMessageType.BCAST ) + { + return mMessage.getInt( B1_SYSTEM_ID ); + } + else if( mMessageType == MPTMessageType.ALH ) + { + return mMessage.getInt( B2_SYSTEM_ID ); + } + else + { + return 0; + } + } + + public boolean hasSystemID() + { + return getSiteID() != 0; + } + + public SystemDefinition getSystemDefinition() + { + int sysdef = mMessage.getInt( B1_SYSDEF ); + + return SystemDefinition.fromNumber( sysdef ); + } + + public int getChannel() + { + switch( mMessageType ) + { + case CLEAR: + return mMessage.getInt( B1_TRAFFIC_CHANNEL ); + case GTC: + return mMessage.getInt( B1_GTC_CHAN ); + default: + return mMessage.getInt( B1_CHANNEL ); + } + } + + public int getReturnToChannel() + { + if( mMessageType == MPTMessageType.CLEAR ) + { + return mMessage.getInt( B1_CONTROL_CHANNEL ); + } + else + { + return 0; + } + } + + public int getAdjacentSiteSerialNumber() + { + return mMessage.getInt( B1_ADJSITE ); + } + + public int getPrefix() + { + return mMessage.getInt( B1_PREFIX ); + } + + public int getBlock2Prefix() + { + return mMessage.getInt( B2_PREFIX ); + } + + public int getIdent1() + { + return mMessage.getInt( B1_IDENT1 ); + } + + public int getIdent2() + { + return mMessage.getInt( B1_IDENT2 ); + } + + public int getBlock2Ident2() + { + return mMessage.getInt( B2_IDENT2 ); + } + + public String getStatusMessage() + { + int status = mMessage.getInt( B1_STATUS_MESSAGE ); + + switch( status ) + { + case 0: + return "STATUS: Request Speech Call"; + case 31: + return "STATUS: Cancel Request Speech Call"; + default: + return "STATUS: " + status; + } + } + + /** + * Returns spaces to the fill the string builder to ensure length is >= index + */ + public String getFiller( StringBuilder sb, int index ) + { + if( sb.length() < index ) + { + return String.format( "%" + ( index - sb.length() ) + "s", " " ); + } + else + { + return ""; + } + } + + /** + * Pads spaces onto the end of the value to make it 'places' long + */ + public String pad( String value, int places, String padCharacter ) + { + StringBuilder sb = new StringBuilder(); + + sb.append( value ); + + while( sb.length() < places ) + { + sb.append( padCharacter ); + } + + return sb.toString(); + } + + /** + * Pads an integer value with additional zeroes to make it decimalPlaces long + */ + public String format( int number, int decimalPlaces ) + { + StringBuilder sb = new StringBuilder(); + + int paddingRequired = decimalPlaces - ( String.valueOf( number ).length() ); + + for( int x = 0; x < paddingRequired; x++) + { + sb.append( "0" ); + } + + sb.append( number ); + + return sb.toString(); + } + + @Override + public String getMessage() + { + StringBuilder sb = new StringBuilder(); + + sb.append( pad( mMessageType.toString(), 5, " " ) ); + + switch( mMessageType ) + { + case ACK: + sb.append( " MESSAGE ACKNOWLEDGED" ); + + if( hasFromID() ) + { + sb.append( " FROM:" ); + sb.append( getFromID() ); + } + break; + case ACKI: + sb.append( " SYSTEM:" ); + sb.append( format( getSiteID(), 5 ) ); + + sb.append( " MESSAGE ACKNOWLEDGED - MORE TO FOLLOW" ); + + if( hasFromID() ) + { + sb.append( " FROM:" ); + sb.append( getFromID() ); + } + + if( hasToID() ) + { + sb.append( " TO:" ); + sb.append( getToID() ); + } + break; + case ACKT: + sb.append( " SITE:" ); + sb.append( format( getSiteID(), 5 ) ); + + sb.append( " LONG ACK MESSAGE" ); + + if( hasFromID() ) + { + sb.append( " FROM:" ); + sb.append( getFromID() ); + } + + if( hasToID() ) + { + sb.append( " TO:" ); + sb.append( getToID() ); + } + + sb.append( "**********************************" ); + break; + case ACKQ: + sb.append( " SYSTEM:" ); + sb.append( format( getSiteID(), 5 ) ); + + sb.append( " CALL QUEUED FROM:" ); + + if( hasFromID() ) + { + sb.append( " FROM:" ); + sb.append( getFromID() ); + } + + if( hasToID() ) + { + sb.append( " TO:" ); + sb.append( getToID() ); + } + break; + case ACKX: + sb.append( " SYSTEM:" ); + sb.append( format( getSiteID(), 5 ) ); + + sb.append( " MESSAGE REJECTED FROM:" ); + + if( hasFromID() ) + { + sb.append( " FROM:" ); + sb.append( getFromID() ); + } + + if( hasToID() ) + { + sb.append( " TO:" ); + sb.append( getToID() ); + } + break; + case AHYC: + /* SDM */ + sb.append( " SHORT DATA INVITATION MESSAGE ********* " ); + break; + case AHYQ: + /* Status Message */ + sb.append( " STATUS MESSAGE" ); + + if( hasFromID() ) + { + sb.append( " FROM:" ); + sb.append( getFromID() ); + } + + if( hasToID() ) + { + sb.append( " TO:" ); + sb.append( getToID() ); + } + sb.append( " " ); + sb.append( getStatusMessage() ); + break; + case ALH: + case ALHD: + case ALHS: + case ALHE: + case ALHR: + case ALHX: + case ALHF: + sb.append( " SYSTEM:" ); + sb.append( format( getSiteID(), 5 ) ); + + if( hasToID() ) + { + sb.append( " ID:" ); + sb.append( getToID() ); + } + break; + case BCAST: + SystemDefinition sysdef = getSystemDefinition(); + + sb.append( " " ); + sb.append( sysdef.getLabel() ); + + switch( sysdef ) + { + case ANNOUNCE_CONTROL_CHANNEL: + case WITHDRAW_CONTROL_CHANNEL: + sb.append( " CHAN:" ); + sb.append( getChannel() ); + break; + case BROADCAST_ADJACENT_SITE_CONTROL_CHANNEL_NUMBER: + sb.append( " CHAN:" ); + sb.append( getChannel() ); + sb.append( " SER:" ); + sb.append( getAdjacentSiteSerialNumber() ); + //debug + sb.append( " *************************" ); + break; + } + break; + case CLEAR: + sb.append( " TRAFFIC CHANNEL:" ); + sb.append( getChannel() ); + sb.append( " RETURN TO CONTROL CHANNEL:" ); + sb.append( getReturnToChannel() ); + break; + case GTC: + if( hasFromID() ) + { + sb.append( " FROM:" ); + sb.append( getFromID() ); + } + + if( hasToID() ) + { + sb.append( " TO:" ); + sb.append( getToID() ); + } + + sb.append( " CHAN:" ); + sb.append( getChannel() ); + break; + case MAINT: + if( hasToID() ) + { + sb.append( " ID:" ); + sb.append( getToID() ); + } + + break; + } + + return sb.toString(); + } + + @Override + public String getBinaryMessage() + { + return mMessage.toString(); + } + + @Override + public String getProtocol() + { + return "MPT-1327"; + } + + @Override + public String getEventType() + { + // TODO Auto-generated method stub + return null; + } + + @Override + public String getFromID() + { + StringBuilder sb = new StringBuilder(); + + int ident2 = getIdent2(); + + IdentType type = IdentType.fromIdent( ident2 ); + + /* Inter-Prefix - the from and to idents are different prefixes */ + if( type == IdentType.IPFIXI ) + { + sb.append( format( getBlock2Prefix(), 3 ) ); + sb.append( "-" ); + sb.append( format( getBlock2Ident2(), 4) ); + } + else + { + sb.append( format( getPrefix(), 3 ) ); + sb.append( "-" ); + sb.append( format( ident2, 4) ); + } + + return sb.toString(); + } + + public boolean hasFromID() + { + return getFromID() != null; + } + + @Override + public Alias getFromIDAlias() + { + return null; + } + + @Override + public String getToID() + { + StringBuilder sb = new StringBuilder(); + + int prefix = getPrefix(); + int ident = getIdent1(); + + switch( IdentType.fromIdent( ident ) ) + { + case USER: + sb.append( format( prefix, 3 ) ); + sb.append( "-" ); + sb.append( format( ident, 4) ); + break; + case IPFIXI: + sb.append( "INTER-PREFIX" ); + break; + case ALLI: + sb.append( "ALL RADIOS" ); + break; + case PABXI: + sb.append( "PABX EXT" ); + break; + case PSTNSI1: + case PSTNSI2: + case PSTNSI3: + case PSTNSI4: + case PSTNSI5: + case PSTNSI6: + case PSTNSI7: + case PSTNSI8: + case PSTNSI9: + case PSTNSI10: + case PSTNSI11: + case PSTNSI12: + case PSTNSI13: + case PSTNSI14: + case PSTNSI15: + sb.append( "PRE-DEFINED PSTN" ); + break; + case PSTNGI: + sb.append( "PSTN GATEWAY" ); + break; + case TSCI: + sb.append( "CONTROLLER" ); + break; + case DIVERTI: + sb.append( "CALL DIVERT" ); + break; + } + + return sb.toString(); + } + + public boolean hasToID() + { + return getToID() != null; + } + + @Override + public Alias getToIDAlias() + { + if( hasToID() ) + { + return mAliasList.getMPT1327Alias( getToID() ); + } + else + { + return null; + } + } + + @Override + public String getErrorStatus() + { + return getParity(); + } + + @Override + public Plottable getPlottable() + { + return null; + } + + public enum IdentType + { + ALLI( "All Subscribers" ), + DIVERTI( "Divert" ), + DNI( "Data Network Gateway" ), + DUMMYI( "Dummy Ident" ), + INCI( "Include in Call" ), + IPFIXI( "Inter-Prefix" ), + PABXI( "PABX Gateway" ), + PSTNGI( "PSTN Gateway" ), + PSTNSI1( "PSTN or Network 1" ), + PSTNSI2( "PSTN or Network 2" ), + PSTNSI3( "PSTN or Network 3" ), + PSTNSI4( "PSTN or Network 4" ), + PSTNSI5( "PSTN or Network 5" ), + PSTNSI6( "PSTN or Network 6" ), + PSTNSI7( "PSTN or Network 7" ), + PSTNSI8( "PSTN or Network 8" ), + PSTNSI9( "PSTN or Network 9" ), + PSTNSI10( "PSTN or Network 10" ), + PSTNSI11( "PSTN or Network 11" ), + PSTNSI12( "PSTN or Network 12" ), + PSTNSI13( "PSTN or Network 13" ), + PSTNSI14( "PSTN or Network 14" ), + PSTNSI15( "PSTN or Network 15" ), + REGI( "Registration" ), + RESERVED( "Reserved" ), + SDMI( "Short Data Message" ), + SPARE( "Spare" ), + TSCI( "TSC" ), + USER( "Individual or Group Ident" ), + UNKNOWN( "Unknown" ); + + private String mLabel; + + private IdentType( String label ) + { + mLabel = label; + } + + public String getLabel() + { + return mLabel; + } + + public static IdentType fromIdent( int ident ) + { + switch( ident ) + { + case 0: + return DUMMYI; + case 8101: + return PSTNGI; + case 8102: + return PABXI; + case 8103: + return DNI; + case 8121: + return PSTNSI1; + case 8122: + return PSTNSI2; + case 8123: + return PSTNSI3; + case 8124: + return PSTNSI4; + case 8125: + return PSTNSI5; + case 8126: + return PSTNSI6; + case 8127: + return PSTNSI7; + case 8128: + return PSTNSI8; + case 8129: + return PSTNSI9; + case 8130: + return PSTNSI10; + case 8131: + return PSTNSI11; + case 8132: + return PSTNSI12; + case 8133: + return PSTNSI13; + case 8134: + return PSTNSI14; + case 8135: + return PSTNSI15; + case 8181: + case 8182: + case 8183: + case 8184: + return RESERVED; + case 8185: + return REGI; + case 8186: + return INCI; + case 8187: + return DIVERTI; + case 8188: + return SDMI; + case 8189: + return IPFIXI; + case 8190: + return TSCI; + case 8191: + return ALLI; + default: + if( 1 <= ident && ident <= 8100 ) + { + return USER; + } + else if( ( 8104 <= ident && ident <= 8120 ) || + ( 8136 <= ident && ident <= 8180 ) ) + { + return SPARE; + } + else + { + return UNKNOWN; + } + } + } + } + + public enum SystemDefinition + { + UNKNOWN( "UNKNOWN" ), + ANNOUNCE_CONTROL_CHANNEL( "ANNOUNCE CONTROL CHANNEL" ), + WITHDRAW_CONTROL_CHANNEL( "WITHDRAW CONTROL CHANNEL" ), + SPECIFY_CALL_MAINTENANCE_PARAMETERS( "CALL MAINT PARAMETERS" ), + SPECIFY_REGISTRATION_PARAMETERS( "REGISTRATION PARAMETERS" ), + BROADCAST_ADJACENT_SITE_CONTROL_CHANNEL_NUMBER( "NEIGHBOR" ), + VOTE_NOW_ADVICE( "VOTE NOW" ); + + private String mLabel; + + private SystemDefinition( String label ) + { + mLabel = label; + } + + public String getLabel() + { + return mLabel; + } + + public static SystemDefinition fromNumber( int number ) + { + switch( number ) + { + case 0: + return ANNOUNCE_CONTROL_CHANNEL; + case 1: + return WITHDRAW_CONTROL_CHANNEL; + case 2: + return SPECIFY_CALL_MAINTENANCE_PARAMETERS; + case 3: + return SPECIFY_REGISTRATION_PARAMETERS; + case 4: + return BROADCAST_ADJACENT_SITE_CONTROL_CHANNEL_NUMBER; + case 5: + return VOTE_NOW_ADVICE; + default: + return UNKNOWN; + } + } + } + + public enum MPTMessageType + { + UNKN( -1, "Unknown" ), + GTC( 0, "GTC - Goto Channel" ), + ALH( 256, "ALH - Aloha" ), + ALHS( 257, "ALHS - Standard Data Excluded" ), + ALHD( 258, "ALHD - Simple Calls Excluded" ), + ALHE( 259, "ALHE - Emergency Calls Only" ), + ALHR( 260, "ALHR - Emergency or Registration" ), + ALHX( 261, "ALHX - Registration Excluded" ), + ALHF( 262, "ALHF - Fallback Mode" ), + + ACK( 264, "ACK - Acknowledge" ), + ACKI( 265, "ACKI - More To Follow" ), + ACKQ( 266, "ACKQ - Call Queued" ), + ACKX( 267, "ACKX - Message Rejected" ), + ACKV( 268, "ACKV - Called Unit Unavailable" ), + ACKE( 269, "ACKE - Emergency" ), + ACKT( 270, "ACKT - Try On Given Address" ), + ACKB( 271, "ACKB - Call Back/Negative Ack" ), + + AHOY( 272, "AHOY - General Availability Check" ), + AHYX( 274, "AHYX - Cancel Alert/Waiting Status" ), + AHYP( 277, "AHYP - Called Unit Presence Monitoring" ), + AHYQ( 278, "AHYQ - Status Message" ), + AHYC( 279, "AHYC - Short Data Message" ), + + MARK( 280, "MARK - Control Channel Marker" ), + MAINT(281, "MAINT - Call Maintenance Message"), + CLEAR(282, "CLEAR - Down From Allocated Channel" ), + MOVE( 283, "MOVE - To Specified Channel" ), + BCAST(284, "BCAST - System Parameters" ); + + /* There are more to fill in here */ + + private int mNumber; + private String mDescription; + + private MPTMessageType( int number, String description ) + { + mNumber = number; + mDescription = description; + } + + public int getMessageNumber() + { + return mNumber; + } + + public String getDescription() + { + return mDescription; + } + + public static MPTMessageType fromNumber( int number ) + { + if( number < 256 ) + { + return GTC; + } + + switch( number ) + { + case 256: + return ALH; + case 257: + return ALHS; + case 258: + return ALHD; + case 259: + return ALHE; + case 260: + return ALHR; + case 261: + return ALHX; + case 262: + return ALHF; + case 264: + return ACK; + case 265: + return ACKI; + case 266: + return ACKQ; + case 267: + return ACKX; + case 268: + return ACKV; + case 269: + return ACKE; + case 270: + return ACKT; + case 271: + return ACKB; + case 272: + return AHOY; + case 274: + return AHYX; + case 277: + return AHYP; + case 278: + return AHYQ; + case 279: + return AHYC; + case 280: + return MARK; + case 281: + return MAINT; + case 282: + return CLEAR; + case 283: + return MOVE; + case 284: + return BCAST; + default: + return UNKN; + } + } + } +} diff --git a/src/decode/mpt1327/MPT1327MessageProcessor.java b/src/decode/mpt1327/MPT1327MessageProcessor.java new file mode 100644 index 00000000..5e36dfbe --- /dev/null +++ b/src/decode/mpt1327/MPT1327MessageProcessor.java @@ -0,0 +1,59 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package decode.mpt1327; + +import message.Message; +import sample.Broadcaster; +import sample.Listener; +import alias.AliasList; +import bits.BitSetBuffer; + +public class MPT1327MessageProcessor implements Listener +{ + private Broadcaster mBroadcaster = new Broadcaster(); + + private AliasList mAliasList; + + public MPT1327MessageProcessor( AliasList list ) + { + mAliasList = list; + } + + public void dispose() + { + mBroadcaster.dispose(); + } + + @Override + public void receive( BitSetBuffer buffer ) + { + MPT1327Message message = new MPT1327Message( buffer, mAliasList ); + + mBroadcaster.receive( message ); + } + + public void addMessageListener( Listener listener ) + { + mBroadcaster.addListener( listener ); + } + + public void removeMessageListener( Listener listener ) + { + mBroadcaster.removeListener( listener ); + } +} diff --git a/src/decode/mpt1327/MPT1327Panel.java b/src/decode/mpt1327/MPT1327Panel.java new file mode 100644 index 00000000..60db75cf --- /dev/null +++ b/src/decode/mpt1327/MPT1327Panel.java @@ -0,0 +1,282 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package decode.mpt1327; + +import java.awt.EventQueue; + +import javax.swing.ImageIcon; +import javax.swing.JLabel; + +import settings.ColorSetting; +import settings.Setting; +import alias.Alias; +import alias.AliasList; +import controller.channel.Channel; +import controller.state.ChannelState.ChangedAttribute; +import controller.state.ChannelStatePanel; + +public class MPT1327Panel extends ChannelStatePanel +{ + private static final long serialVersionUID = 1L; + + private JLabel mProtocol; + private JLabel mSourceLabel; + private JLabel mChannelLabel; + + private JLabel mStateLabel; + private JLabel mTalkgroup; + private JLabel mTalkgroupAlias; + + private JLabel mSiteLabel; + private JLabel mSite; + private JLabel mSiteAliasLabel; + + private MPT1327ChannelState mState; + + private AliasList mAliasList; + + public MPT1327Panel( Channel channel ) + { + super( channel ); + mState = (MPT1327ChannelState)channel.getProcessingChain().getChannelState(); + mState.addListener( this ); + + mAliasList = mState.getAliasList(); + + init(); + } + + public void dispose() + { + super.dispose(); + mState = null; + mAliasList = null; + } + + public void init() + { + mProtocol = new JLabel( "MPT1327" ); + mProtocol.setFont( mFontDecoder ); + mProtocol.setForeground( mColorLabelDecoder ); + + mSourceLabel = new JLabel( mChannel.getSourceConfiguration().getDescription() ); + mSourceLabel.setFont( mFontDetails ); + mSourceLabel.setForeground( mColorLabelDetails ); + + mChannelLabel = new JLabel( mChannel.getChannelDisplayName() ); + mChannelLabel.setFont( mFontDetails ); + mChannelLabel.setForeground( mColorLabelDetails ); + + mStateLabel = new JLabel( mChannel.getProcessingChain(). + getChannelState().getState().getDisplayValue() ); + mStateLabel.setFont( mFontDecoder ); + mStateLabel.setForeground( mColorLabelDecoder ); + + mTalkgroup = new JLabel( mState.getFromTalkgroup() ); + mTalkgroup.setFont( mFontDecoder ); + mTalkgroup.setForeground( mColorLabelDecoder ); + + mTalkgroupAlias = new JLabel(); + mTalkgroupAlias.setFont( mFontDecoder ); + mTalkgroupAlias.setForeground( mColorLabelDecoder ); + + Alias alias = getTalkgroupAlias( mState.getFromTalkgroup() ); + + if( alias != null ) + { + mTalkgroupAlias.setText( alias.getName() ); + mTalkgroupAlias.setIcon( getIcon( alias ) ); + } + + mSiteLabel = new JLabel( "Site:" ); + mSiteLabel.setFont( mFontDetails ); + mSiteLabel.setForeground( mColorLabelDetails ); + + mSite = new JLabel( String.valueOf( mState.getSite() ) ); + mSite.setFont( mFontDecoder ); + mSite.setForeground( mColorLabelDecoder ); + + mSiteAliasLabel = new JLabel(); + mSiteAliasLabel.setFont( mFontDecoder ); + mSiteAliasLabel.setForeground( mColorLabelDecoder ); + + Alias siteAlias = getSiteAlias( mState.getSite() ); + + if( siteAlias != null ) + { + mSiteAliasLabel.setText( siteAlias.getName() ); + } + + add( mProtocol ); + add( mSourceLabel ); + add( mChannelLabel, "wrap" ); + + add( mStateLabel ); + add( mTalkgroup ); + add( mTalkgroupAlias, "wrap" ); + + add( mSiteLabel ); + add( mSite ); + add( mSiteAliasLabel, "wrap" ); + } + + @Override + public void receive( final ChangedAttribute changedAttribute ) + { + EventQueue.invokeLater( new Runnable() + { + @Override + public void run() + { + switch( changedAttribute ) + { + case CHANNEL_STATE: + mStateLabel.setText( mState.getState().getDisplayValue() ); + break; + case CHANNEL_SITE_NUMBER: + mSite.setText( String.valueOf( mState.getSite() ) ); + + Alias siteAlias = getSiteAlias( mState.getSite() ); + + if( siteAlias != null ) + { + mSiteAliasLabel.setText( siteAlias.getName() ); + } + repaint(); + break; + case FROM_TALKGROUP: + mTalkgroup.setText( mState.getFromTalkgroup() ); + + Alias alias = getTalkgroupAlias( mState.getFromTalkgroup() ); + + if( alias != null ) + { + mTalkgroupAlias.setText( alias.getName() ); + mTalkgroupAlias.setIcon( getIcon( alias ) ); + } + repaint(); + break; + } + } + } ); + } + + private ImageIcon getIcon( Alias alias ) + { + if( alias != null ) + { + String iconName = alias.getIconName(); + + if( iconName != null ) + { + return getSettingsManager().getImageIcon( iconName, 12 ); + } + } + + return null; + } + + private Alias getSiteAlias( int site ) + { + if( site != 0 && mAliasList != null ) + { + return mAliasList.getSiteID( site ); + } + + return null; + } + + private Alias getTalkgroupAlias( String talkgroup ) + { + if( talkgroup != null && mAliasList != null ) + { + return mAliasList.getTalkgroupAlias( talkgroup ); + } + + return null; + } + + @Override + public void trafficChannelAdded( Channel traffic ) + { + if( !getTrafficPanels().keySet().contains( traffic ) ) + { + MPT1327TrafficPanel panel = + new MPT1327TrafficPanel( traffic ); + + getTrafficPanels().put( traffic, panel ); + + addPanel( panel ); + } + } + + @Override + public void settingChanged( Setting setting ) + { + super.settingChanged( setting ); + + if( setting instanceof ColorSetting ) + { + ColorSetting colorSetting = (ColorSetting)setting; + + switch( colorSetting.getColorSettingName() ) + { + case CHANNEL_STATE_LABEL_DECODER: + if( mProtocol != null ) + { + mProtocol.setForeground( mColorLabelDecoder ); + } + if( mStateLabel != null ) + { + mStateLabel.setForeground( mColorLabelDecoder ); + } + if( mTalkgroup != null ) + { + mTalkgroup.setForeground( mColorLabelDecoder ); + } + if( mTalkgroupAlias != null ) + { + mTalkgroupAlias.setForeground( mColorLabelDecoder ); + } + if( mSite != null ) + { + mSite.setForeground( mColorLabelDecoder ); + } + if( mSiteAliasLabel != null ) + { + mSiteAliasLabel.setForeground( mColorLabelDecoder ); + } + break; + case CHANNEL_STATE_LABEL_DETAILS: + if( mSourceLabel != null ) + { + mSourceLabel.setForeground( mColorLabelDetails ); + } + if( mChannelLabel != null ) + { + mChannelLabel.setForeground( mColorLabelDetails ); + } + if( mSiteLabel != null ) + { + mSiteLabel.setForeground( mColorLabelDetails ); + } + break; + } + } + } +} diff --git a/src/decode/mpt1327/MPT1327TrafficPanel.java b/src/decode/mpt1327/MPT1327TrafficPanel.java new file mode 100644 index 00000000..e2647d04 --- /dev/null +++ b/src/decode/mpt1327/MPT1327TrafficPanel.java @@ -0,0 +1,258 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package decode.mpt1327; + +import java.awt.EventQueue; + +import javax.swing.JLabel; + +import settings.ColorSetting; +import settings.Setting; +import alias.Alias; +import alias.AliasList; +import controller.channel.Channel; +import controller.channel.ProcessingChain; +import controller.state.ChannelState.ChangedAttribute; +import controller.state.ChannelStatePanel; + +public class MPT1327TrafficPanel extends ChannelStatePanel +{ + private static final long serialVersionUID = 1L; + + private JLabel mProtocolLabel; + private JLabel mFrequencyLabel; + private JLabel mChannelLabel; + + private JLabel mStateLabel; + private JLabel mFromTalkgroupLabel; + private JLabel mFromTalkgroupAliasLabel; + + private JLabel mToLabel; + private JLabel mToTalkgroupLabel; + private JLabel mToTalkgroupAliasLabel; + + private AliasList mAliasList; + + public MPT1327TrafficPanel( Channel channel ) + { + super( channel ); + + mAliasList = channel.getProcessingChain().getChannelState().getAliasList(); + + init(); + } + + public void dispose() + { + super.dispose(); + mAliasList = null; + } + + public void init() + { + mProtocolLabel = new JLabel( "MPT1327" ); + mProtocolLabel.setFont( mFontDecoder ); + mProtocolLabel.setForeground( mColorLabelDecoder ); + + mFrequencyLabel = new JLabel( mChannel.getSourceConfiguration().getDescription() ); + mFrequencyLabel.setFont( mFontDetails ); + mFrequencyLabel.setForeground( mColorLabelDetails ); + + MPT1327ChannelState state = + (MPT1327ChannelState)mChannel.getProcessingChain().getChannelState(); + int channelNumber = state.getChannelNumber(); + + mChannelLabel = new JLabel( "Channel " + channelNumber ); + mChannelLabel.setFont( mFontDetails ); + mChannelLabel.setForeground( mColorLabelDetails ); + + mStateLabel = new JLabel( mChannel.getProcessingChain(). + getChannelState().getState().getDisplayValue() ); + mStateLabel.setFont( mFontDecoder ); + mStateLabel.setForeground( mColorLabelDecoder ); + + mFromTalkgroupLabel = new JLabel( state.getFromTalkgroup() ); + mFromTalkgroupLabel.setFont( mFontDecoder ); + mFromTalkgroupLabel.setForeground( mColorLabelDecoder ); + + mFromTalkgroupAliasLabel = new JLabel(); + mFromTalkgroupAliasLabel.setFont( mFontDecoder ); + mFromTalkgroupAliasLabel.setForeground( mColorLabelDecoder ); + + if( mAliasList != null ) + { + Alias alias = mAliasList.getMPT1327Alias( state.getFromTalkgroup() ); + + if( alias != null ) + { + mFromTalkgroupAliasLabel.setText( alias.getName() ); + + mFromTalkgroupAliasLabel.setIcon( getChannel() + .getResourceManager().getSettingsManager() + .getImageIcon( alias.getIconName(), 12 ) ); + } + } + + mToLabel = new JLabel( "TO:" ); + mToLabel.setFont( mFontDetails ); + mToLabel.setForeground( mColorLabelDetails ); + + mToTalkgroupLabel = new JLabel( state.getFromTalkgroup() ); + mToTalkgroupLabel.setFont( mFontDecoder ); + mToTalkgroupLabel.setForeground( mColorLabelDecoder ); + + mToTalkgroupAliasLabel = new JLabel(); + mToTalkgroupAliasLabel.setFont( mFontDecoder ); + mToTalkgroupAliasLabel.setForeground( mColorLabelDecoder ); + + if( mAliasList != null ) + { + Alias alias = mAliasList.getMPT1327Alias( state.getFromTalkgroup() ); + + if( alias != null ) + { + mToTalkgroupAliasLabel.setText( alias.getName() ); + + mToTalkgroupAliasLabel.setIcon( getChannel() + .getResourceManager().getSettingsManager() + .getImageIcon( alias.getIconName(), 12 ) ); + } + } + + add( mProtocolLabel ); + add( mFrequencyLabel ); + add( mChannelLabel, "wrap" ); + + add( mStateLabel ); + add( mFromTalkgroupLabel ); + add( mFromTalkgroupAliasLabel, "wrap" ); + + add( mToLabel ); + add( mToTalkgroupLabel ); + add( mToTalkgroupAliasLabel, "wrap" ); + } + + @Override + public void receive( final ChangedAttribute changedAttribute ) + { + EventQueue.invokeLater( new Runnable() + { + @Override + public void run() + { + ProcessingChain chain = getChannel().getProcessingChain(); + + if( chain != null ) + { + final MPT1327ChannelState state = + (MPT1327ChannelState)chain.getChannelState(); + + if( state != null ) + { + switch( changedAttribute ) + { + case CHANNEL_STATE: + mStateLabel.setText( state.getState().getDisplayValue() ); + break; + case CHANNEL_NUMBER: + mChannelLabel.setText( "Channel " + state.getChannelNumber() ); + break; + case FROM_TALKGROUP: + String talkgroup = state.getFromTalkgroup(); + + mFromTalkgroupLabel.setText( talkgroup ); + + if( mAliasList != null ) + { + Alias alias = mAliasList.getMPT1327Alias( talkgroup ); + + if( alias != null ) + { + mFromTalkgroupAliasLabel.setText( alias.getName() ); + + String icon = alias.getIconName(); + + mFromTalkgroupAliasLabel.setIcon( getChannel() + .getResourceManager().getSettingsManager() + .getImageIcon( icon, 12 ) ); + } + } + break; + case TO_TALKGROUP: + String toTalkgroup = state.getToTalkgroup(); + + mToTalkgroupLabel.setText( toTalkgroup ); + + if( mAliasList != null ) + { + Alias alias = mAliasList.getMPT1327Alias( toTalkgroup ); + + if( alias != null ) + { + mToTalkgroupAliasLabel.setText( alias.getName() ); + + String icon = alias.getIconName(); + + if( icon != null ) + { + mToTalkgroupAliasLabel.setIcon( getChannel() + .getResourceManager().getSettingsManager() + .getImageIcon( icon, 12 ) ); + } + } + } + break; + default: + break; + } + + repaint(); + } + } + } + } ); + } + + @Override + public void settingChanged( Setting setting ) + { + super.settingChanged( setting ); + + if( setting instanceof ColorSetting ) + { + ColorSetting colorSetting = (ColorSetting)setting; + + switch( colorSetting.getColorSettingName() ) + { + case CHANNEL_STATE_LABEL_DECODER: + mProtocolLabel.setForeground( mColorLabelDecoder ); + mStateLabel.setForeground( mColorLabelDecoder ); + mFromTalkgroupLabel.setForeground( mColorLabelDecoder ); + mFromTalkgroupAliasLabel.setForeground( mColorLabelDecoder ); + mToTalkgroupLabel.setForeground( mColorLabelDecoder ); + mToTalkgroupAliasLabel.setForeground( mColorLabelDecoder ); + break; + case CHANNEL_STATE_LABEL_DETAILS: + mFrequencyLabel.setForeground( mColorLabelDetails ); + mChannelLabel.setForeground( mColorLabelDetails ); + mToLabel.setForeground( mColorLabelDetails ); + break; + } + } + } +} diff --git a/src/decode/nbfm/NBFMChannelState.java b/src/decode/nbfm/NBFMChannelState.java new file mode 100644 index 00000000..fa4cd66a --- /dev/null +++ b/src/decode/nbfm/NBFMChannelState.java @@ -0,0 +1,69 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package decode.nbfm; + +import alias.AliasList; +import audio.SquelchListener; +import audio.SquelchListener.SquelchState; +import controller.channel.ProcessingChain; +import controller.state.AuxChannelState; +import controller.state.ChannelState; + +/** + * Conventional channel state - this class doesn't do anything at the moment, + * it is a place holder for future expansion for the conventional decoder + */ +public class NBFMChannelState extends ChannelState +{ + public NBFMChannelState( ProcessingChain channel, AliasList aliasList ) + { + super( channel, aliasList ); + } + + /** + * Make the ConventionalChannelState always unsquelched + */ + public void addListener( SquelchListener listener ) + { + super.addListener( listener ); + + super.setSquelchState( SquelchState.UNSQUELCH ); + } + + @Override + public void setSquelchState( SquelchState state ) + { + //do nothing ... we want the squelch state always unsquelched (for now) + } + @Override + public String getActivitySummary() + { + StringBuilder sb = new StringBuilder(); + + sb.append( "Activity Summary\n" ); + sb.append( "\tDecoder:\tNBFM\n\n" ); + + for( AuxChannelState state: mAuxChannelStates ) + { + sb.append( state.getActivitySummary() ); + sb.append( "\n\n" ); + } + + return sb.toString(); + } +} diff --git a/src/decode/nbfm/NBFMConventionalDecoder.java b/src/decode/nbfm/NBFMConventionalDecoder.java new file mode 100644 index 00000000..2d67cd2c --- /dev/null +++ b/src/decode/nbfm/NBFMConventionalDecoder.java @@ -0,0 +1,77 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package decode.nbfm; + +import source.Source.SampleType; +import decode.Decoder; +import decode.DecoderType; +import dsp.filter.DCRemovalFilter2; +import dsp.nbfm.FilteringNBFMDemodulator; + +public class NBFMConventionalDecoder extends Decoder +{ + /** + * This value determines how quickly the DC remove filter responds to + * changes in frequency. + */ + private static final double sDC_REMOVAL_RATIO = 0.000003; + + private FilteringNBFMDemodulator mDemodulator; + private DCRemovalFilter2 mDCRemovalFilter; + + public NBFMConventionalDecoder( SampleType sampleType ) + { + super( sampleType ); + + /** + * Only setup a demod chain if we're receiving complex samples. If + * we're receiving demodulated samples, they'll be handled the same + * was as we handle the output of the demodulator. + */ + if( mSourceSampleType == SampleType.COMPLEX ) + { + /** + * The Decoder super class is both a Complex listener and a float + * listener. So, we add the demod to listen to the incoming + * quadrature samples, and we wire the output of the demod right + * back to this class, so we can receive the demodulated output + * to process + */ + mDemodulator = new FilteringNBFMDemodulator(); + this.addComplexListener( mDemodulator ); + + /** + * Remove the DC component that is present when we're mistuned + */ + mDCRemovalFilter = new DCRemovalFilter2( sDC_REMOVAL_RATIO ); + mDemodulator.addListener( mDCRemovalFilter ); + + /** + * Route the demodulated, filtered samples back to this class to send + * to all registered listeners + */ + mDCRemovalFilter.setListener( this.getFloatReceiver() ); + } + } + + @Override + public DecoderType getType() + { + return DecoderType.NBFM; + } +} diff --git a/src/decode/nbfm/NBFMEditor.java b/src/decode/nbfm/NBFMEditor.java new file mode 100644 index 00000000..4fa0472a --- /dev/null +++ b/src/decode/nbfm/NBFMEditor.java @@ -0,0 +1,31 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package decode.nbfm; + +import decode.DecodeEditor; +import decode.config.DecodeConfiguration; + +public class NBFMEditor extends DecodeEditor +{ + private static final long serialVersionUID = 1L; + + public NBFMEditor( DecodeConfiguration config ) + { + super( config ); + } +} diff --git a/src/decode/nbfm/NBFMPanel.java b/src/decode/nbfm/NBFMPanel.java new file mode 100644 index 00000000..f1555a49 --- /dev/null +++ b/src/decode/nbfm/NBFMPanel.java @@ -0,0 +1,136 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package decode.nbfm; + +import java.awt.EventQueue; + +import javax.swing.JLabel; + +import settings.ColorSetting; +import settings.Setting; +import controller.channel.Channel; +import controller.state.ChannelState.ChangedAttribute; +import controller.state.ChannelStatePanel; + +public class NBFMPanel extends ChannelStatePanel +{ + private static final long serialVersionUID = 1L; + + private JLabel mStateLabel; + private JLabel mSourceLabel; + private JLabel mChannelLabel; + + private JLabel mProtocol = new JLabel( "NBFM" ); + + public NBFMPanel( Channel channel ) + { + super( channel ); + + init(); + } + + public void init() + { + mStateLabel = new JLabel( mChannel.getProcessingChain(). + getChannelState().getState().getDisplayValue() ); + mStateLabel.setFont( mFontDecoder ); + mStateLabel.setForeground( mColorLabelDecoder ); + + mSourceLabel = new JLabel( mChannel.getSourceConfiguration().getDescription() ); + mSourceLabel.setFont( mFontDetails ); + mSourceLabel.setForeground( mColorLabelDetails ); + + mChannelLabel = new JLabel( mChannel.getChannelDisplayName() ); + mChannelLabel.setFont( mFontDetails ); + mChannelLabel.setForeground( mColorLabelDetails ); + + mProtocol.setFont( mFontDecoder ); + mProtocol.setForeground( mColorLabelDecoder ); + + add( mStateLabel ); + add( mSourceLabel ); + add( mChannelLabel, "wrap" ); + + add( mProtocol, "wrap" ); + } + + @Override + public void receive( final ChangedAttribute changedAttribute ) + { + EventQueue.invokeLater( new Runnable() + { + @Override + public void run() + { + switch( changedAttribute ) + { + case CHANNEL_STATE: + mStateLabel.setText( mChannel.getProcessingChain(). + getChannelState().getState().getDisplayValue() ); + break; + case SOURCE: + mSourceLabel.setText( mChannel.getSourceConfiguration() + .getDescription() ); + break; + case CHANNEL_NAME: + case SITE_NAME: + case SYSTEM_NAME: + mChannelLabel.setText( mChannel.getChannelDisplayName() ); + break; + } + + repaint(); + } + }); + } + + @Override + public void settingChanged( Setting setting ) + { + super.settingChanged( setting ); + + if( setting instanceof ColorSetting ) + { + ColorSetting colorSetting = (ColorSetting)setting; + + switch( colorSetting.getColorSettingName() ) + { + case CHANNEL_STATE_LABEL_DECODER: + if( mStateLabel != null ) + { + mStateLabel.setForeground( mColorLabelDecoder ); + } + if( mProtocol != null ) + { + mProtocol.setForeground( mColorLabelDecoder ); + } + break; + case CHANNEL_STATE_LABEL_DETAILS: + if( mSourceLabel != null ) + { + mSourceLabel.setForeground( mColorLabelDetails ); + } + if( mChannelLabel != null ) + { + mChannelLabel.setForeground( mColorLabelDetails ); + } + break; + } + } + } +} diff --git a/src/decode/passport/PassportActivitySummary.java b/src/decode/passport/PassportActivitySummary.java new file mode 100644 index 00000000..055ca424 --- /dev/null +++ b/src/decode/passport/PassportActivitySummary.java @@ -0,0 +1,233 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package decode.passport; + +import java.text.DecimalFormat; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.TreeSet; + +import message.Message; +import sample.Listener; +import alias.Alias; +import alias.AliasList; +import controller.activity.ActivitySummaryProvider; + +public class PassportActivitySummary implements ActivitySummaryProvider, + Listener +{ + private static final int sINT_NULL_VALUE = -1; + private DecimalFormat mDecimalFormatter = new DecimalFormat("0.00000"); + + private AliasList mAliasList; + private HashSet mTalkgroupsFirstHeard = new HashSet(); + private TreeSet mTalkgroups = new TreeSet(); + private TreeSet mMobileIDs = new TreeSet(); + private HashMap mSiteLCNs = new HashMap(); + private HashMap mNeighborLCNs = new HashMap(); + + /** + * Compiles a summary of active talkgroups, unique radio ids encountered + * in the decoded messages. Talkgroups have to be heard a minimum of twice + * to be considered valid ... in order to weed out the invalid ones. + * + * Returns a textual summary of the activity + */ + public PassportActivitySummary( AliasList list ) + { + mAliasList = list; + } + + @Override + public void receive( Message message ) + { + if( message instanceof PassportMessage ) + { + PassportMessage passport = ((PassportMessage)message); + + if( passport.isValid() ) + { + switch( passport.getMessageType() ) + { + case CA_STRT: + mSiteLCNs.put( passport.getLCN(), + passport.getLCNFrequencyFormatted() ); + case CA_ENDD: + String talkgroup = + String.valueOf( passport.getTalkgroupID() ); + + if( mTalkgroupsFirstHeard.contains( talkgroup ) ) + { + mTalkgroups.add( talkgroup ); + } + else + { + mTalkgroupsFirstHeard.add( talkgroup ); + } + break; + case ID_RDIO: + String min = passport.getMobileID(); + + if( min != null ) + { + mMobileIDs.add( min ); + } + break; + case SY_IDLE: + if( passport.getFree() != 0 ) + { + mNeighborLCNs.put( passport.getFree(), + passport.getFreeFrequencyFormatted() ); + } + break; + default: + break; + } + } + } + } + + @Override + public String getSummary() + { + StringBuilder sb = new StringBuilder(); + + sb.append( "Activity Summary\n" ); + sb.append( "Decoder:\tPassport\n\n" ); + sb.append( "Site Channels\n" ); + + if( mSiteLCNs.isEmpty() ) + { + sb.append( " None\n" ); + } + else + { + ArrayList channels = new ArrayList( mSiteLCNs.keySet() ); + Collections.sort( channels ); + + for( Integer channel: channels ) + { + sb.append( " " + channel ); + sb.append( "\t" + mSiteLCNs.get( channel ) ); + sb.append( "\n" ); + } + } + + sb.append( "\nNeighbor Channels\n" ); + + if( mNeighborLCNs.isEmpty() ) + { + sb.append( " None\n" ); + } + else + { + ArrayList channels = new ArrayList( mNeighborLCNs.keySet() ); + Collections.sort( channels ); + + for( Integer channel: channels ) + { + sb.append( " " + channel ); + sb.append( "\t" + mNeighborLCNs.get( channel ) ); + sb.append( "\n" ); + } + } + + sb.append( "\nTalkgroups\n" ); + + if( mTalkgroups.isEmpty() ) + { + sb.append( " None\n" ); + } + else + { + Iterator it = mTalkgroups.iterator(); + + while( it.hasNext() ) + { + String tgid = it.next(); + + sb.append( " " ); + sb.append( tgid ); + sb.append( " " ); + + Alias alias = mAliasList.getTalkgroupAlias( tgid ); + + if( alias != null ) + { + sb.append( alias.getName() ); + } + + sb.append( "\n" ); + } + } + + sb.append( "\nMobile ID Numbers\n" ); + + if( mMobileIDs.isEmpty() ) + { + sb.append( " None\n" ); + } + else + { + Iterator it = mMobileIDs.iterator(); + + while( it.hasNext() ) + { + String min = it.next(); + + sb.append( " " ); + sb.append( min ); + sb.append( " " ); + + Alias alias = mAliasList.getMobileIDNumberAlias( min ); + + if( alias != null ) + { + sb.append( alias.getName() ); + } + + sb.append( "\n" ); + } + } + + return sb.toString(); + } + + public static String formatTalkgroup( String talkgroup ) + { + StringBuilder sb = new StringBuilder(); + + if( talkgroup.length() == 6 ) + { + sb.append( talkgroup.substring( 0, 1 ) ); + sb.append( "-" ); + sb.append( talkgroup.substring( 1, 3 ) ); + sb.append( "-" ); + sb.append( talkgroup.substring( 3, 6 ) ); + + return sb.toString(); + } + else + { + return talkgroup; + } + } +} diff --git a/src/decode/passport/PassportBand.java b/src/decode/passport/PassportBand.java new file mode 100644 index 00000000..8d03d245 --- /dev/null +++ b/src/decode/passport/PassportBand.java @@ -0,0 +1,71 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package decode.passport; + +public enum PassportBand +{ + BAND_800( "800", 800000000 ), + BAND_900( "900", 900000000 ), + BAND_400( "400", 400000000 ), + BAND_420( "420", 420000000 ), + BAND_440( "440", 440000000 ), + BAND_450( "450", 450000000 ), + BAND_460( "460", 460000000 ), + BAND_470( "470", 470000000 ), + BAND_480( "480", 480000000 ), + BAND_490( "490", 490000000 ), + BAND_CANADA( "CAN", 0 ), + BAND_NTIA( "NTIA", 0 ), + BAND_RESERVED12( "RESVD", 0 ), + BAND_216( "216", 216000000 ), + BAND_RESERVED14( "RESVD", 0 ), + BAND_700( "700", 700000000 ), + BAND_VHF( "VHF", 0 ), + BAND_UNKNOWN( "UNK", 0 ); + + private String mDescription; + private int mBase; + + private PassportBand( String description, int base ) + { + mDescription = description; + mBase = base; + } + + public static PassportBand lookup( int band ) + { + if( band >= 0 && band < 16 ) + { + return PassportBand.values()[ band ]; + } + else + { + return PassportBand.BAND_UNKNOWN; + } + } + + public String getDescription() + { + return mDescription; + } + + public int getBase() + { + return mBase; + } +} diff --git a/src/decode/passport/PassportCallEvent.java b/src/decode/passport/PassportCallEvent.java new file mode 100644 index 00000000..08d0f96e --- /dev/null +++ b/src/decode/passport/PassportCallEvent.java @@ -0,0 +1,164 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package decode.passport; + +import alias.Alias; +import alias.AliasList; +import controller.activity.CallEvent; +import decode.DecoderType; + +public class PassportCallEvent extends CallEvent +{ + private int mChannel; + private int mFrequency; + + public PassportCallEvent( CallEventType callEventType, + AliasList aliasList, + String fromID, + String toID, + int channel, + int frequency, + String details ) + { + super( DecoderType.PASSPORT, callEventType, + aliasList, fromID, toID, details ); + + mChannel = channel; + mFrequency = frequency; + } + + private Alias getAlias( String talkgroup ) + { + if( hasAliasList() ) + { + return getAliasList().getTalkgroupAlias( talkgroup ); + } + + return null; + } + + private Alias getMobileIDAlias( String mobileID ) + { + if( hasAliasList() ) + { + return getAliasList().getMobileIDNumberAlias( mobileID ); + } + + return null; + } + + @Override + public Alias getFromIDAlias() + { + return getMobileIDAlias( getFromID() ); + } + + @Override + public Alias getToIDAlias() + { + return getAlias( getToID() ); + } + + @Override + public int getChannel() + { + return mChannel; + } + + @Override + public int getFrequency() + { + return mFrequency; + } + + public static class Builder + { + /* Required parameters */ + private CallEventType mCallEventType; + + /* Optional parameters */ + private AliasList mAliasList; + private String mFromID; + private String mToID; + private String mDetails; + private int mChannel; + private int mFrequency; + + public Builder( CallEventType callEventType ) + { + mCallEventType = callEventType; + } + + public Builder aliasList( AliasList aliasList ) + { + mAliasList = aliasList; + return this; + } + + public Builder from( String val ) + { + mFromID = val; + return this; + } + + public Builder channel( int channel ) + { + mChannel = channel; + return this; + } + + public Builder details( String details ) + { + mDetails = details; + return this; + } + + public Builder frequency( int frequency ) + { + mFrequency = frequency; + return this; + } + + public Builder to( String toID ) + { + mToID = toID; + return this; + } + + public PassportCallEvent build() + { + return new PassportCallEvent( this ); + } + + } + + /** + * Private constructor for the builder + */ + private PassportCallEvent( Builder builder ) + { + this( builder.mCallEventType, + builder.mAliasList, + builder.mFromID, + builder.mToID, + builder.mChannel, + builder.mFrequency, + builder.mDetails ); + } + +} diff --git a/src/decode/passport/PassportChannelState.java b/src/decode/passport/PassportChannelState.java new file mode 100644 index 00000000..3fe452ad --- /dev/null +++ b/src/decode/passport/PassportChannelState.java @@ -0,0 +1,236 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package decode.passport; + +import message.Message; +import alias.Alias; +import alias.AliasList; +import audio.AudioType; +import audio.SquelchListener; +import controller.activity.CallEvent.CallEventType; +import controller.channel.ProcessingChain; +import controller.state.AuxChannelState; +import controller.state.ChannelState; + +public class PassportChannelState extends ChannelState +{ + private String mTalkgroup; + private Alias mTalkgroupAlias; + private String mMobileID; + private Alias mMobileIDAlias; + private int mChannelNumber; + private int mFrequency; + private PassportCallEvent mCurrentCallEvent; + private PassportActivitySummary mActivitySummary; + + public PassportChannelState( ProcessingChain channel, AliasList aliasList ) + { + super( channel, aliasList ); + + mActivitySummary = new PassportActivitySummary( aliasList ); + } + + @Override + public String getActivitySummary() + { + StringBuilder sb = new StringBuilder(); + + sb.append( mActivitySummary.getSummary() ); + + for( AuxChannelState state: mAuxChannelStates ) + { + sb.append( state.getActivitySummary() ); + sb.append( "\n\n" ); + } + + return sb.toString(); + } + + @Override + @SuppressWarnings( "incomplete-switch" ) + public void receive( Message message ) + { + super.receive( message ); + + if( message instanceof PassportMessage ) + { + mActivitySummary.receive( message ); + + PassportMessage passport = (PassportMessage)message; + + if( passport.isValid() ) + { + switch( passport.getMessageType() ) + { + case CA_STRT: + setTalkgroup( String.valueOf( passport.getTalkgroupID() ) ); + setTalkgroupAlias( passport.getTalkgroupIDAlias() ); + + if( mTalkgroupAlias != null ) + { + try + { + if( mTalkgroupAlias.hasAudioType() ) + { + mAudioTypeListener.setAudioType( + mTalkgroupAlias.getAudioType() ); + } + } + catch( Exception e ) + { + e.printStackTrace(); + } + } + + mChannelNumber = passport.getLCN(); + mFrequency = passport.getLCNFrequency(); + + if( getState() != State.CALL ) + { + mCurrentCallEvent = new PassportCallEvent + .Builder( CallEventType.CALL_START ) + .aliasList( mAliasList ) + .channel( mChannelNumber ) + .frequency( mFrequency ) + .to( String.valueOf( passport.getTalkgroupID() ) ) + .build(); + + mCallEventModel.add( mCurrentCallEvent ); + } + + setState( State.CALL ); + break; + case DA_STRT: + setTalkgroup( String.valueOf( passport.getTalkgroupID() ) ); + setTalkgroupAlias( passport.getTalkgroupIDAlias() ); + setState( State.DATA ); + break; + case CA_ENDD: + setTalkgroup( String.valueOf( passport.getTalkgroupID() ) ); + setTalkgroupAlias( passport.getTalkgroupIDAlias() ); + fade( CallEventType.CALL_END ); + break; + case ID_RDIO: + setMobileID( passport.getMobileID() ); + setMobileIDAlias( passport.getMobileIDAlias() ); + + if( mCurrentCallEvent != null ) + { + mCurrentCallEvent.setFromID( mMobileID ); + } + setState( State.CALL ); + break; + } + } + } + } + + /** + * Intercept the fade event so that we can generate a call end event + */ + @Override + public void fade( final CallEventType type ) + { + /* + * We can receive multiple call tear-down messages -- only post a call + * end event for the one that can change the state to fade + */ + if( getState().canChangeTo( State.FADE ) ) + { + mCallEventModel.add( + new PassportCallEvent.Builder( type ) + .aliasList( getAliasList() ) + .channel( mChannelNumber ) + .frequency( mProcessingChain.getChannel() + .getTunerChannel().getFrequency() ) + .to( mTalkgroup ) + .build() ); + } + + super.fade( type ); + } + + + + /** + * Make the ConventionalChannelState always unsquelched + */ + public void addListener( SquelchListener listener ) + { + super.addListener( listener ); + } + + public void reset() + { + mAudioTypeListener.setAudioType( AudioType.NORMAL ); + + mCurrentCallEvent = null; + + setTalkgroup( null ); + setTalkgroupAlias( null ); + + setMobileID( null ); + setMobileIDAlias( null ); + + super.reset(); + } + + public String getTalkgroup() + { + return mTalkgroup; + } + + public void setTalkgroup( String talkgroup ) + { + mTalkgroup = talkgroup; + broadcastChange( ChangedAttribute.TO_TALKGROUP ); + } + + public Alias getTalkgroupAlias() + { + return mTalkgroupAlias; + } + + public void setTalkgroupAlias( Alias alias ) + { + mTalkgroupAlias = alias; + broadcastChange( ChangedAttribute.TO_TALKGROUP_ALIAS ); + } + + public String getMobileID() + { + return mMobileID; + } + + public void setMobileID( String id ) + { + mMobileID = id; + broadcastChange( ChangedAttribute.FROM_TALKGROUP ); + } + + public Alias getMobileIDAlias() + { + return mMobileIDAlias; + } + + public void setMobileIDAlias( Alias alias ) + { + mMobileIDAlias = alias; + broadcastChange( ChangedAttribute.FROM_TALKGROUP_ALIAS ); + } +} diff --git a/src/decode/passport/PassportDecoder.java b/src/decode/passport/PassportDecoder.java new file mode 100644 index 00000000..27d38d14 --- /dev/null +++ b/src/decode/passport/PassportDecoder.java @@ -0,0 +1,108 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package decode.passport; + +import source.Source.SampleType; +import alias.AliasList; +import bits.MessageFramer; +import bits.SyncPattern; +import decode.Decoder; +import decode.DecoderType; +import decode.ltrstandard.LTRFSKDemodulator; +import dsp.filter.DCRemovalFilter2; +import dsp.nbfm.FilteringNBFMDemodulator; + +public class PassportDecoder extends Decoder +{ + /** + * This value determines how quickly the DC remove filter responds to + * changes in frequency. + */ + private static final double sDC_REMOVAL_RATIO = 0.000003; + + public static final int sPASSPORT_MESSAGE_LENGTH = 68; + public static final int sPASSPORT_SYNC_LENGTH = 9; + private FilteringNBFMDemodulator mNBFMDemodulator; + private DCRemovalFilter2 mDCRemovalFilter; + private LTRFSKDemodulator mPassportFSKDemodulator; + private MessageFramer mPassportMessageFramer; + private PassportMessageProcessor mPassportMessageProcessor; + + public PassportDecoder( SampleType sampleType, AliasList aliasList ) + { + super( sampleType ); + + /** + * Only setup a demod chain if we're receiving complex samples. If + * we're receiving demodulated samples, they'll be handled the same + * was as we handle the output of the demodulator. + */ + if( mSourceSampleType == SampleType.COMPLEX ) + { + /** + * The Decoder super class is both a Complex listener and a Short + * listener. So, we add the demod to listen to the incoming + * quadrature samples, and we wire the output of the demod right + * back to this class, so we can receive the demodulated output + * to process + */ + mNBFMDemodulator = new FilteringNBFMDemodulator(); + this.addComplexListener( mNBFMDemodulator ); + + /** + * Remove the DC component that is present when we're mistuned + */ + mDCRemovalFilter = new DCRemovalFilter2( sDC_REMOVAL_RATIO ); + mNBFMDemodulator.addListener( mDCRemovalFilter ); + mDCRemovalFilter.setListener( this.getFloatReceiver() ); + } + + mPassportFSKDemodulator = + new LTRFSKDemodulator(); + + /** + * The DC removal filter will impact performance of the LTRFSKDemod, + * so we get the samples straight from the NBFM demod + */ + if( mSourceSampleType == SampleType.COMPLEX ) + { + mNBFMDemodulator.addListener( mPassportFSKDemodulator ); + } + else + { + addFloatListener( mPassportFSKDemodulator ); + } + + + mPassportMessageFramer = + new MessageFramer( SyncPattern.PASSPORT.getPattern(), + sPASSPORT_MESSAGE_LENGTH ); + + mPassportFSKDemodulator.addListener( mPassportMessageFramer ); + + mPassportMessageProcessor = new PassportMessageProcessor( aliasList ); + mPassportMessageFramer.addMessageListener( mPassportMessageProcessor ); + mPassportMessageProcessor.addMessageListener( this ); + } + + @Override + public DecoderType getType() + { + return DecoderType.PASSPORT; + } +} diff --git a/src/decode/passport/PassportEditor.java b/src/decode/passport/PassportEditor.java new file mode 100644 index 00000000..26cc814f --- /dev/null +++ b/src/decode/passport/PassportEditor.java @@ -0,0 +1,31 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package decode.passport; + +import decode.DecodeEditor; +import decode.config.DecodeConfiguration; + +public class PassportEditor extends DecodeEditor +{ + private static final long serialVersionUID = 1L; + + public PassportEditor( DecodeConfiguration config ) + { + super( config ); + } +} diff --git a/src/decode/passport/PassportMessage.java b/src/decode/passport/PassportMessage.java new file mode 100644 index 00000000..283e7d57 --- /dev/null +++ b/src/decode/passport/PassportMessage.java @@ -0,0 +1,634 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package decode.passport; + + +import java.text.DecimalFormat; +import java.text.SimpleDateFormat; + +import map.Plottable; +import message.Message; +import message.MessageType; +import alias.Alias; +import alias.AliasList; +import bits.BitSetBuffer; +import crc.CRC; +import crc.CRCPassport; +import decode.DecoderType; + +public class PassportMessage extends Message +{ + private static final String sUNKNOWN = "**UNKNOWN**"; + private static final int sINT_NULL_VALUE = -1; + private static final double sDOUBLE_NULL_VALUE = -1.0D; + private SimpleDateFormat mSDF = new SimpleDateFormat( "yyyyMMdd HHmmss" ); + private DecimalFormat mDecimalFormatter = new DecimalFormat("0.00000"); + + @SuppressWarnings( "unused" ) + private static final int[] sSYNC = { 8,7,6,5,4,3,2,1,0 }; + private static final int[] sDCC = { 10,9 }; //Digital Color Code + private static final int[] sLCN = { 21,20,19,18,17,16,15,14,13,12,11 }; + private static final int[] sSITE = { 28,27,26,25,24,23,22 }; + private static final int[] sGROUP = { 44,43,42,41,40,39,38,37,36,35,34,33,32,31,30,29 }; + private static final int[] sRADIO_ID = { 44,43,42,41,40,39,38,37,36,35,34,33,32,31,30,29,28,27,26,25,24,23,22 }; +// private static final int[] sNEIGHBOR_OFFSET = { 32,31,30 }; + private static final int[] sNEIGHBOR_BAND = { 36,35,34,33 }; + private static final int[] sNEIGHBOR_REGISTRATION = { 29 }; + private static final int[] sSITE_OFFSET = { 40,39,38 }; + private static final int[] sSITE_BAND = { 44,43,42,41 }; + private static final int[] sSITE_REGISTRATION = { 37 }; + private static final int[] sTYPE = { 48,47,46,45 }; + private static final int[] sFREE = { 59,58,57,56,55,54,53,52,51,50,49 }; + private static final int[] sCHECKSUM = { 67,66,65,64,63,62,61,60 }; + private BitSetBuffer mMessage; + private CRC mCRC; + private AliasList mAliasList; + private PassportMessage mIdleMessage; + + public PassportMessage( BitSetBuffer message, + PassportMessage idleMessage, + AliasList list ) + { + mMessage = CRCPassport.correct( message ); + mIdleMessage = idleMessage; + mCRC = CRCPassport.check( mMessage ); + mAliasList = list; + } + + public PassportMessage( BitSetBuffer message, AliasList list ) + { + this( message, null, list ); + } + + public BitSetBuffer getBitSetBuffer() + { + return mMessage; + } + + public boolean isValid() + { + return mCRC != CRC.FAILED_CRC && + mCRC != CRC.FAILED_PARITY && + mCRC != CRC.UNKNOWN; + } + + public CRC getCRC() + { + return mCRC; + } + + public MessageType getMessageType() + { + MessageType retVal = MessageType.UN_KNWN; + + int type = getMessageTypeNumber(); + int lcn = getLCN(); + + switch( type ) + { + case 0: //Group Call + retVal = MessageType.CA_STRT; + break; + case 1: + if( getFree() == 2042 ) + { + retVal = MessageType.ID_TGAS; + } + else if( lcn < 1792 ) + { + retVal = MessageType.CA_STRT; + } + else if( lcn == 1792 || lcn == 1793 ) + { + retVal = MessageType.SY_IDLE; + } + else if( lcn == 2047 ) + { + retVal = MessageType.CA_ENDD; + } + break; + case 2: + retVal = MessageType.CA_STRT; + break; + case 5: + retVal = MessageType.CA_PAGE; + break; + case 6: + retVal = MessageType.ID_RDIO; + break; + case 9: + retVal = MessageType.DA_STRT; + break; + case 11: + retVal = MessageType.RA_REGI; + break; + default: + break; + } + + return retVal; + } + + + public int getDCC() + { + return getInt( sDCC ); + } + + public int getSite() + { + return getInt( sSITE ); + } + + public int getMessageTypeNumber() + { + return getInt( sTYPE ); + } + + public int getTalkgroupID() + { + if( getMessageType() == MessageType.SY_IDLE ) + { + return 0; + } + else + { + return getInt( sGROUP ); + } + } + + public Alias getTalkgroupIDAlias() + { + int tg = getTalkgroupID(); + + return mAliasList.getTalkgroupAlias( String.valueOf( tg ) ); + } + + public int getLCN() + { + return getInt( sLCN ); + } + + public int getLCNFrequency() + { + return getSiteFrequency( getLCN() ); + } + + public String getLCNFrequencyFormatted() + { + return mDecimalFormatter.format( (double)getLCNFrequency() / 1000000.0d ); + } + + public PassportBand getSiteBand() + { + return PassportBand.lookup( getInt( sSITE_BAND ) ); + } + + public PassportBand getNeighborBand() + { + return PassportBand.lookup( getInt( sNEIGHBOR_BAND ) ); + } + + public int getFrequency( int base, int channel ) + { + return ( base + ( channel * 12500 ) ); + } + + public int getFree() + { + return getInt( sFREE ); + } + + public int getFreeFrequency() + { + return getSiteFrequency( getFree() ); + } + + public String getFreeFrequencyFormatted() + { + return mDecimalFormatter.format( (double)getFreeFrequency() / 1000000.0d ); + } + + public int getNeighborFrequency() + { + int retVal = 0; + + if( getMessageType() == MessageType.SY_IDLE ) + { + PassportBand band = getNeighborBand(); + + retVal = getFrequency( band.getBase(), getFree() ); + } + + return retVal; + } + + /** + * Returns the radio id in hex format, or null if not the correct message + * type + * + * @return - radio id or null + */ + public String getMobileID() + { + if( getMessageType() == MessageType.ID_RDIO ) + { + int radioId = getInt( sRADIO_ID ); + + return String.format("%06X", radioId & 0xFFFFFF ); + } + else + { + return null; + } + } + + public Alias getMobileIDAlias() + { + String min = getMobileID(); + + if( min != null ) + { + return mAliasList.getMobileIDNumberAlias( min ); + } + + return null; + } + + /** + * Appends spaces to the end of the stringbuilder to make it length long + */ + private void pad( StringBuilder sb, int length ) + { + while( sb.length() < length ) + { + sb.append( " " ); + } + } + + @Override + public String getBinaryMessage() + { + return mMessage.toString(); + } + + /** + * Returns a value formatted in hex notation. + * + * @param value - integer value + * @return - hex digits returned + */ + private static String getHex( int value, int digits ) + { + return String.format( "%0" + digits + "X", value ); + } + + private int getInt( int[] bits ) + { + int retVal = 0; + + for( int x = 0; x < bits.length; x++ ) + { + if( mMessage.get( bits[ x ] ) ) + { + retVal += 1< 0 && free < 896 ) + { + sb.append( "/" ); + sb.append( getFreeFrequencyFormatted() ); + } + sb.append( " TYP:" ); + sb.append( format( getMessageTypeNumber(), 2 ) ); + sb.append( " TG:" ); + sb.append( format( getTalkgroupID(), 5 ) ); + break; + } + + return sb.toString(); + } + + @Override + public String toString() + { + StringBuilder sb = new StringBuilder(); + + sb.append( mSDF.format( mTimeReceived ) ); + sb.append( " " ); + sb.append( getMessage() ); + pad( sb, 100 ); + sb.append( mCRC.toString() ); + pad( sb, 110 ); + sb.append( " [" ); + sb.append( mMessage ); + sb.append( "]" ); + + return sb.toString(); + } + + @Override + public String getErrorStatus() + { + return mCRC.getDisplayText(); + } + + public boolean matches( PassportMessage otherMessage ) + { + return this.getBitSetBuffer().equals( otherMessage.getBitSetBuffer() ); + } + + @Override + public Plottable getPlottable() + { + // TODO Auto-generated method stub + return null; + } + +} diff --git a/src/decode/passport/PassportMessageProcessor.java b/src/decode/passport/PassportMessageProcessor.java new file mode 100644 index 00000000..f0a80921 --- /dev/null +++ b/src/decode/passport/PassportMessageProcessor.java @@ -0,0 +1,109 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package decode.passport; + +import java.util.HashMap; + +import message.Message; +import message.MessageType; +import sample.Broadcaster; +import sample.Listener; +import alias.AliasList; +import bits.BitSetBuffer; + +public class PassportMessageProcessor implements Listener +{ + private Broadcaster mBroadcaster = new Broadcaster(); + private IdleMessageFinder mIdleFinder = new IdleMessageFinder(); + private AliasList mAliasList; + private PassportMessage mIdleMessage; + + public PassportMessageProcessor( AliasList list ) + { + mAliasList = list; + } + + @Override + public void receive( BitSetBuffer buffer ) + { + PassportMessage message; + + if( mIdleMessage != null ) + { + message = new PassportMessage( buffer, mIdleMessage, mAliasList ); + } + else + { + message = new PassportMessage( buffer, mAliasList ); + mIdleFinder.receive( message ); + } + + mBroadcaster.receive( message ); + } + + public void addMessageListener( Listener listener ) + { + mBroadcaster.addListener( listener ); + } + + public void removeMessageListener( Listener listener ) + { + mBroadcaster.removeListener( listener ); + } + + public class IdleMessageFinder + { + public HashMap mMessageCounts = + new HashMap(); + + public boolean mIdleMessageFound = false; + + public IdleMessageFinder() + { + } + + public void receive( PassportMessage message ) + { + if( !mIdleMessageFound && + message.isValid() && + message.getMessageType() == MessageType.SY_IDLE ) + { + if( mMessageCounts.containsKey( message.getBinaryMessage() ) ) + { + int count = mMessageCounts.get( message.getBinaryMessage() ); + + if( count >= 3 ) + { + mIdleMessageFound = true; + mIdleMessage = message; + mMessageCounts = null; + } + else + { + count++; + mMessageCounts.put( message.getBinaryMessage(), count ); + } + } + else + { + mMessageCounts.put( message.getBinaryMessage(), 1 ); + } + } + } + } +} diff --git a/src/decode/passport/PassportPanel.java b/src/decode/passport/PassportPanel.java new file mode 100644 index 00000000..41e606ad --- /dev/null +++ b/src/decode/passport/PassportPanel.java @@ -0,0 +1,255 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package decode.passport; + +import java.awt.EventQueue; + +import javax.swing.JLabel; + +import settings.ColorSetting; +import settings.Setting; +import settings.SettingsManager; +import alias.Alias; +import controller.channel.Channel; +import controller.state.ChannelState.ChangedAttribute; +import controller.state.ChannelStatePanel; + +public class PassportPanel extends ChannelStatePanel +{ + private static final long serialVersionUID = 1L; + + private JLabel mStateLabel; + private JLabel mSourceLabel; + private JLabel mChannelLabel; + + private JLabel mProtocol = new JLabel( "Passport" ); + private JLabel mToTalkgroup = new JLabel(); + private JLabel mToTalkgroupAlias = new JLabel(); + + private JLabel mFromTalkgroupType = new JLabel( " " ); + private JLabel mFromTalkgroup = new JLabel(); + private JLabel mFromTalkgroupAlias = new JLabel(); + + private SettingsManager mSettingsManager; + + public PassportPanel( SettingsManager settingsManager, Channel channel ) + { + super( channel ); + + mSettingsManager = settingsManager; + + init(); + } + + public void dispose() + { + super.dispose(); + mSettingsManager = null; + } + + public void init() + { + mStateLabel = new JLabel( mChannel.getProcessingChain(). + getChannelState().getState().getDisplayValue() ); + mStateLabel.setFont( mFontDecoder ); + mStateLabel.setForeground( mColorLabelDecoder ); + + mSourceLabel = new JLabel( mChannel.getSourceConfiguration().getDescription() ); + mSourceLabel.setFont( mFontDetails ); + mSourceLabel.setForeground( mColorLabelDetails ); + + mChannelLabel = new JLabel( mChannel.getChannelDisplayName() ); + mChannelLabel.setFont( mFontDetails ); + mChannelLabel.setForeground( mColorLabelDetails ); + + mProtocol.setFont( mFontDecoder ); + mProtocol.setForeground( mColorLabelDecoder ); + + mToTalkgroup.setFont( mFontDecoder ); + mToTalkgroup.setForeground( mColorLabelDecoder ); + + mToTalkgroupAlias.setFont( mFontDecoder ); + mToTalkgroupAlias.setForeground( mColorLabelDecoder ); + + mFromTalkgroupType.setFont( mFontDecoder ); + mFromTalkgroupType.setForeground( mColorLabelDecoder ); + + mFromTalkgroup.setFont( mFontDecoder ); + mFromTalkgroup.setForeground( mColorLabelDecoder ); + + mFromTalkgroupAlias.setFont( mFontDecoder ); + mFromTalkgroupAlias.setForeground( mColorLabelDecoder ); + + add( mStateLabel ); + add( mSourceLabel ); + add( mChannelLabel, "wrap" ); + + add( mProtocol ); + add( mToTalkgroup ); + add( mToTalkgroupAlias, "wrap" ); + + add( mFromTalkgroupType ); + add( mFromTalkgroup ); + add( mFromTalkgroupAlias, "wrap" ); + } + + @Override + public void receive( final ChangedAttribute changedAttribute ) + { + EventQueue.invokeLater( new Runnable() + { + @Override + public void run() + { + PassportChannelState state = (PassportChannelState)getChannel() + .getProcessingChain().getChannelState(); + + + switch( changedAttribute ) + { + case CHANNEL_STATE: + mStateLabel.setText( state.getState().getDisplayValue() ); + break; + case SOURCE: + mSourceLabel.setText( mChannel.getSourceConfiguration() + .getDescription() ); + break; + case CHANNEL_NAME: + case SITE_NAME: + case SYSTEM_NAME: + mChannelLabel.setText( mChannel.getChannelDisplayName() ); + break; + case TO_TALKGROUP: + mToTalkgroup.setText( state.getTalkgroup() ); + break; + case TO_TALKGROUP_ALIAS: + Alias toAlias = state.getTalkgroupAlias(); + + if( toAlias != null ) + { + mToTalkgroupAlias.setText( toAlias.getName() ); + + mToTalkgroupAlias.setIcon( mSettingsManager + .getImageIcon( toAlias.getIconName(), 12 ) ); + } + else + { + mToTalkgroupAlias.setText( null ); + mToTalkgroupAlias.setIcon( null ); + } + break; + case FROM_TALKGROUP: + String mid = state.getMobileID(); + + mFromTalkgroup.setText( state.getMobileID() ); + + if( mid != null ) + { + mFromTalkgroupType.setText( "Mobile ID" ); + } + else + { + mFromTalkgroupType.setText( " " ); + } + break; + case FROM_TALKGROUP_ALIAS: + Alias fromAlias = state.getMobileIDAlias(); + + if( fromAlias != null ) + { + mFromTalkgroupAlias.setText( fromAlias.getName() ); + + mFromTalkgroupAlias.setIcon( mSettingsManager + .getImageIcon( fromAlias.getIconName(), 12 ) ); + } + else + { + mFromTalkgroupAlias.setText( null ); + mFromTalkgroupAlias.setIcon( null ); + } + break; + default: + break; + } + + repaint(); + } + }); + } + + @Override + public void settingChanged( Setting setting ) + { + super.settingChanged( setting ); + + if( setting instanceof ColorSetting ) + { + ColorSetting colorSetting = (ColorSetting)setting; + + switch( colorSetting.getColorSettingName() ) + { + case CHANNEL_STATE_LABEL_DECODER: + if( mColorLabelDecoder != null ) + { + if( mStateLabel != null ) + { + mStateLabel.setForeground( mColorLabelDecoder ); + } + if( mProtocol != null ) + { + mProtocol.setForeground( mColorLabelDecoder ); + } + if( mToTalkgroup != null ) + { + mToTalkgroup.setForeground( mColorLabelDecoder ); + } + if( mToTalkgroupAlias != null ) + { + mToTalkgroupAlias.setForeground( mColorLabelDecoder ); + } + if( mFromTalkgroupType != null ) + { + mFromTalkgroupType.setForeground( mColorLabelDecoder ); + } + if( mFromTalkgroup != null ) + { + mFromTalkgroup.setForeground( mColorLabelDecoder ); + } + if( mFromTalkgroupAlias != null ) + { + mFromTalkgroupAlias.setForeground( mColorLabelDecoder ); + } + } + break; + case CHANNEL_STATE_LABEL_DETAILS: + if( mColorLabelDetails != null ) + { + if( mSourceLabel != null ) + { + mSourceLabel.setForeground( mColorLabelDetails ); + } + if( mChannelLabel != null ) + { + mChannelLabel.setForeground( mColorLabelDetails ); + } + } + break; + } + } + } +} diff --git a/src/dsp/BooleanZeroCrossingDetector.java b/src/dsp/BooleanZeroCrossingDetector.java new file mode 100644 index 00000000..c04d47f5 --- /dev/null +++ b/src/dsp/BooleanZeroCrossingDetector.java @@ -0,0 +1,117 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package dsp; + +import sample.Listener; + +public class BooleanZeroCrossingDetector implements Listener +{ + private int mBaudLength; + private int mCounter = 0; + private int mThreshold; + private Listener mListener; + private Cycle mCycle = Cycle.POSITIVE; + + /** + * Implements a zero crossing detector for use with squared, filtered + * binary values. Makes a baud decision at 100% of the baud + * length, or at 80% of the baud length during a zero crossing. Counter + * is reset at each zero crossing. + * + * @param baudLength - estimated number of boolean samples per baud + */ + public BooleanZeroCrossingDetector( int baudLength ) + { + mBaudLength = baudLength; + mThreshold = (int)( baudLength * .80 ); + } + + @Override + public void receive( Boolean sample ) + { + mCounter++; + + switch( mCycle ) + { + case POSITIVE: + if( sample ) + { + if( mCounter >= mBaudLength ) + { + send( true ); + mCounter -= mBaudLength; + } + } + else + { + if( mCounter >= mThreshold ) + { + send( true ); + mCounter -= mBaudLength; + } + + mCycle = Cycle.NEGATIVE; + } + break; + + case NEGATIVE: + if( sample ) + { + if( mCounter >= mThreshold ) + { + send( false ); + + mCounter -= mBaudLength; + } + + mCycle = Cycle.POSITIVE; + } + else + { + if( mCounter >= mBaudLength ) + { + send( false ); + + mCounter -= mBaudLength; + } + } + + break; + } + } + + private void send( boolean bit ) + { + if( mListener != null ) + { + mListener.receive( bit ); + } + } + + public void setListener( Listener listener ) + { + mListener = listener; + } + + public void removeListener( Listener listener ) + { + mListener = null; + } + + public enum Cycle { POSITIVE, NEGATIVE }; +} diff --git a/src/dsp/FloatToBinarySlicer.java b/src/dsp/FloatToBinarySlicer.java new file mode 100644 index 00000000..97061994 --- /dev/null +++ b/src/dsp/FloatToBinarySlicer.java @@ -0,0 +1,69 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package dsp; + +import sample.Listener; +import buffer.FloatCircularAveragingBuffer; + +public class FloatToBinarySlicer implements Listener + +{ + private FloatCircularAveragingBuffer mAveragingBuffer; + private Listener mListener; + + /** + * Provides binary slicing of the stream of floats, using a circular buffer + * to calculate a running average, then basing the slice decision based + * on the oldest sample leaving the circular buffer, compared to the running + * average. + * + * Designed to operate with a DC bias inherent in the sample stream. + * + * Tracks a long average and a short average to compensate for drifting + * transmitters. + * + * The long buffer compensates for long strings of ones or + * zeros. Set the long buffer size to a few baud periods longer than the + * longest anticipated string of like symbols. + * + */ + public FloatToBinarySlicer( int bufferSize, int sampleCountForAveraging ) + { + mAveragingBuffer = new FloatCircularAveragingBuffer( bufferSize, + sampleCountForAveraging ); + } + + @Override + public void receive( Float newestSample ) + { + float oldestSample = mAveragingBuffer.get( newestSample ); + + //Dispatch the decision + mListener.receive( oldestSample > mAveragingBuffer.getAverage() ); + } + + public void setListener( Listener listener ) + { + mListener = listener; + } + + public void clearListener() + { + mListener = null; + } +} diff --git a/src/dsp/FloatToBinarySlicerLTR.java b/src/dsp/FloatToBinarySlicerLTR.java new file mode 100644 index 00000000..9160980e --- /dev/null +++ b/src/dsp/FloatToBinarySlicerLTR.java @@ -0,0 +1,137 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package dsp; + +import sample.Listener; +import buffer.FloatCircularAveragingBuffer2; + +/** + * Binary slicer tuned to operate on LTR type short burst signals in a heavy + * DC bias environment coupled with transmitter automatic frequency correction + * being applied as the transmission progresses. + * + * Uses a pair of float buffers that calculate the average of the samples + * contained in the buffer. + * + * The leading and trailing buffers are the same length as the sync pattern. + * + * The slicer uses the Leading buffer's average as the slicing reference for + * normal operation. + * + * Once a sync pattern is detected, the trailing buffer should have the sync + * pattern bauds and the leading buffer should have the first of the message + * bits. At this point, ie sync detect, we switch to using the average of both + * buffers as the slicing reference point. + * + * Once the reference counter counts down to indicate that there are sync length + * bauds remaining of the message, and those remaining bauds should be in the + * leading buffer, we swich to using the trailing buffer as the slicing + * reference point for the remainder of the message. + * + * As the reference counter decrements to 0, meaning the message should be + * clear of both buffers, we switch back to normal operation of using the + * leading buffer as the slicing reference. + * + * An external sync detector indicates a sync pattern match by using the + * syncDetected() method, which immediately switches the slicing average + * reference to both, and sets the counter to the expected message length. + */ +public class FloatToBinarySlicerLTR implements Listener, + SyncDetectListener + +{ + private enum SlicingReference + { + LEADING, TRAILING, BOTH; + } + + private SlicingReference mSlicingReference = SlicingReference.LEADING; + private FloatCircularAveragingBuffer2 mLeadingBuffer; + private FloatCircularAveragingBuffer2 mTrailingBuffer; + private int mReferenceCounter = 0; + private int mSyncLength; + private int mMessageLength; + private Listener mListener; + + public FloatToBinarySlicerLTR( int syncLength, + int messageLength, + int samplesPerBaud ) + { + mSyncLength = syncLength * samplesPerBaud; + mMessageLength = messageLength * samplesPerBaud; + + mLeadingBuffer = new FloatCircularAveragingBuffer2( mSyncLength ); + mTrailingBuffer = new FloatCircularAveragingBuffer2( mSyncLength ); + } + + @Override + public void receive( Float newestSample ) + { + float middleSample = mLeadingBuffer.get( newestSample ); + mTrailingBuffer.get( middleSample ); + + //Dispatch the decision + if( mListener != null ) + { + switch( mSlicingReference ) + { + case LEADING: + mListener.receive( middleSample > mLeadingBuffer.average() ); + break; + case BOTH: + mListener.receive( middleSample > + ( ( mLeadingBuffer.average() + + mTrailingBuffer.average() ) / 2.0f ) ); + break; + case TRAILING: + mListener.receive( middleSample > mTrailingBuffer.average() ); + break; + } + + mReferenceCounter--; + + if( mReferenceCounter <= mSyncLength && mReferenceCounter > 0 ) + { + mSlicingReference = SlicingReference.TRAILING; + } + else if( mReferenceCounter <= 0 ) + { + mSlicingReference = SlicingReference.LEADING; + + mReferenceCounter = 0; + } + } + } + + @Override + public void syncDetected() + { + mSlicingReference = SlicingReference.BOTH; + mReferenceCounter = mMessageLength; + } + + public void setListener( Listener listener ) + { + mListener = listener; + } + + public void clearListener() + { + mListener = null; + } +} diff --git a/src/dsp/NRZDecoder.java b/src/dsp/NRZDecoder.java new file mode 100644 index 00000000..d8d30073 --- /dev/null +++ b/src/dsp/NRZDecoder.java @@ -0,0 +1,77 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package dsp; + +import sample.Listener; + +public class NRZDecoder implements Listener +{ + public final static boolean MODE_NORMAL = true; + public final static boolean MODE_INVERTED = false; + + private Listener mListener; + private boolean mMode = MODE_NORMAL; + private boolean mPrevious; + + /** + * Non Return To Zero (NRZ) and NRZ Inverted (NRZ-I) Decoder. + * + * Use default constructor for NRZ output. Use the alternate constructor + * with the MODE_INVERTED argument for NRZ-I output. + * + * Performs XOR of incoming bit with previous output and sends the result + * to the registered listener. + */ + public NRZDecoder( boolean mode ) + { + mMode = mode; + } + + public NRZDecoder() + { + this( MODE_NORMAL ); + } + + public void dispose() + { + mListener = null; + } + + @Override + public void receive( Boolean bit ) + { + boolean result = mPrevious ^ bit; + + if( mListener != null ) + { + mListener.receive( mMode ? result : !result ); + } + + mPrevious = result; + } + + public void setListener( Listener listener ) + { + mListener = listener; + } + + public void removeListener( Listener listener ) + { + mListener = null; + } +} diff --git a/src/dsp/SyncDetectListener.java b/src/dsp/SyncDetectListener.java new file mode 100644 index 00000000..6e883e77 --- /dev/null +++ b/src/dsp/SyncDetectListener.java @@ -0,0 +1,27 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package dsp; + +/** + * Interface to allow an external process indicate that it has detected a + * sync pattern in the bit stream + */ +public interface SyncDetectListener +{ + public void syncDetected(); +} diff --git a/src/dsp/SyncDetectProvider.java b/src/dsp/SyncDetectProvider.java new file mode 100644 index 00000000..f352a059 --- /dev/null +++ b/src/dsp/SyncDetectProvider.java @@ -0,0 +1,23 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package dsp; + +public interface SyncDetectProvider +{ + public void setSyncDetectListener( SyncDetectListener listener ); +} diff --git a/src/dsp/filter/BooleanAveragingFilter.java b/src/dsp/filter/BooleanAveragingFilter.java new file mode 100644 index 00000000..bdfe7185 --- /dev/null +++ b/src/dsp/filter/BooleanAveragingFilter.java @@ -0,0 +1,110 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package dsp.filter; + +import java.util.Arrays; + +import sample.Listener; + +/** + * Averages (low pass filter) boolean values over the defined length + */ +public class BooleanAveragingFilter implements Listener +{ + private boolean[] mBuffer; + private int mBufferPointer; + private int mThreshold; + private Listener mListener; + + public BooleanAveragingFilter( int length ) + { + mBuffer = new boolean[ length ]; + + if( length % 2 == 0 ) + { + mThreshold = (int)( length /2 ); + } + else + { + mThreshold = (int)( ( length + 1 ) / 2 ); + } + + //Preload the array with false values + Arrays.fill( mBuffer, false ); + } + + /** + * Loads the newValue into this buffer and adjusts the buffer pointer + * to prepare for the next get/put cycle + */ + public void put( boolean newValue ) + { + //Store the new value to the buffer + mBuffer[ mBufferPointer ] = newValue; + + //Increment the buffer pointer + mBufferPointer++; + + //Wrap the buffer pointer around to 0 when necessary + if( mBufferPointer >= mBuffer.length ) + { + mBufferPointer = 0; + } + } + + /** + * Loads the newValue into the buffer, calculates the average + * and returns that average from this method + * + * This effectively performs low-pass filtering + */ + public void receive( Boolean newValue ) + { + //Load the new value into the buffer + put( newValue ); + + int trueCount = 0; + + for( int x = 0; x < mBuffer.length; x++ ) + { + if( mBuffer[ x ] ) + { + trueCount++; + } + } + + /** + * If the number of true values in the buffer is + * more than half, send true, otherwise, false + */ + if( mListener != null ) + { + mListener.receive( trueCount >= mThreshold ); + } + } + + public void setListener( Listener listener ) + { + mListener = listener; + } + + public void clearListener() + { + mListener = null; + } +} diff --git a/src/dsp/filter/ComplexCICDecimate.java b/src/dsp/filter/ComplexCICDecimate.java new file mode 100644 index 00000000..7367f87b --- /dev/null +++ b/src/dsp/filter/ComplexCICDecimate.java @@ -0,0 +1,341 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package dsp.filter; + +import sample.Listener; +import sample.Provider; +import sample.complex.ComplexSample; +import dsp.filter.Window.WindowType; + +/** + * Cascaded Integrator Comb (CIC) filter - anti-aliasing decimating filter for + * reducing a high rate complex sample stream to a lower rate by an integral + * decimation (ie reduction) value. + * + * Note: comb delay is fixed at 1 for this filter. + */ +public class ComplexCICDecimate extends ComplexFilter + implements Provider +{ + private Integrator[] mIntegrator; + private Comb[] mComb; + private Decimator mDecimator; + private Output mOutput; + + /** + * Constructs the complex sample CIC filter + * + * @param decimation - decimation factor + * + * @param stageCount - number of integrator/comb sections to use. The + * higher the number of stages, the sharper dropoff of the filter response + * + * @param outputSampleRate - output sample rate is used to calculate the + * coefficients for the final cleanup filter to adjust for frequency + * response droop induced by this CIC filter + */ + public ComplexCICDecimate( int decimation, + int stageCount, + int outputSampleRate ) + { + /* Construct integrator(s) and comb(s) */ + mIntegrator = new Integrator[ stageCount ]; + mComb = new Comb[ stageCount ]; + + for( int x = 0; x < stageCount; x++ ) + { + mIntegrator[ x ] = new Integrator(); + mComb[ x ] = new Comb(); + } + + /* Connect each stage */ + int y = 1; + + while( y < stageCount ) + { + mIntegrator[ y - 1 ].setListener( mIntegrator[ y ] ); + mComb[ y - 1 ].setListener( mComb[ y ] ); + + y++; + } + + /* Connect decimator to integrator output and comb input */ + mDecimator = new Decimator( decimation ); + + mIntegrator[ stageCount - 1 ].setListener( mDecimator ); + mDecimator.setListener( mComb[ 0 ] ); + + /* Connect output adapter to the comb output */ + mOutput = new Output( outputSampleRate, stageCount, decimation ); + mComb[ stageCount - 1 ].setListener( mOutput ); + } + + /** + * Sets a new decimation rate for this filter. This method can be invoked + * after filter construction to change the decimation rate dynamically. + */ + public void setDecimationRate( int rate ) + { + mDecimator.setDecimationRate( rate ); + mOutput.setDecimationRate( rate ); + } + + /** + * Input method for this filter to receive complex samples. + */ + @Override + public void receive( ComplexSample sample ) + { + mIntegrator[ 0 ].receive( (double)sample.left(), (double)sample.right() ); + } + + /** + * Sets the listener to receive the output of this CIC filter + */ + @Override + public void setListener( Listener listener ) + { + mOutput.setListener( listener ); + } + + /** + * Removes the listener from receiving the output of this CIC filter + */ + @Override + public void removeListener( Listener listener ) + { + mOutput.removeListener( listener ); + } + + @Override + public Listener getListener() + { + return mOutput.getListener(); + } + + /** + * CIC Comb filter section + */ + public class Comb implements InternalListener + { + private double mPreviousLeft; + private double mPreviousRight; + private InternalListener mCombListener; + + /** + * Performs comb filtering on the received values, subtracting the + * previous received samples from these samples. + */ + @Override + public void receive( double left, double right ) + { + mCombListener.receive( ( left - mPreviousLeft ), + ( right - mPreviousRight ) ); + + mPreviousLeft = left; + mPreviousRight = right; + } + + /** + * Sets the comb output listener. + */ + public void setListener( InternalListener listener ) + { + mCombListener = listener; + } + } + + /** + * CIC Integrator section + */ + public class Integrator implements InternalListener + { + private double mRunningSumLeft = 0.0d; + private double mRunningSumRight = 0.0d; + private InternalListener mIntegratorListener; + + /** + * Integrates the received values with the previous running sum and + * sends the new running sum to the registered listener + */ + @Override + public void receive( double left, double right ) + { + mRunningSumLeft += left; + mRunningSumRight += right; + + mIntegratorListener.receive( mRunningSumLeft, mRunningSumRight ); + } + + /** + * Sets the integrator output listener + */ + public void setListener( InternalListener listener ) + { + mIntegratorListener = listener; + } + } + + /** + * Decimator - passes only 1 of xx samples to a registered listener and + * discards all other samples. + */ + public class Decimator implements InternalListener + { + private InternalListener mDecimatorListener; + private int mPointer = 0; + private int mDecimationRate; + + /** + * Constructs a new Decimator object with the specified decimation rate. + */ + public Decimator( int rate ) + { + mDecimationRate = rate; + } + + /** + * Sets a new decimation rate. Rate can be changed after this object + * is constructed. + */ + public synchronized void setDecimationRate( int rate ) + { + mDecimationRate = rate; + } + + /** + * Receives samples allowing only 1 of every (rate) sample to go on + * to the registered listener + */ + @Override + public void receive( double left, double right ) + { + mPointer++; + + if( mPointer >= mDecimationRate ) + { + mDecimatorListener.receive( left, right ); + + mPointer = 0; + } + } + + /** + * Sets the decimated output listener + */ + public void setListener( InternalListener listener ) + { + mDecimatorListener = listener; + } + } + + /** + * Output adapter - applies gain correction, applies cleanup filter, casts + * double-valued samples into float-valued complex sample and sends the + * result to the registered listener. + */ + public class Output implements InternalListener + { + private int mStageCount; + private int mDecimation; + private double mGain = 1.0d; + private ComplexFilter mCleanupFilter; + + public Output( int outputSampleRate, int stageCount, int decimation ) + { + mStageCount = stageCount; + mDecimation = decimation; + mCleanupFilter = new ComplexFIRFilter( FilterFactory + .getCICCleanupFilter( outputSampleRate, + stageCount, + WindowType.BLACKMAN ), 1.0d ); + + setGain(); + } + + /** + * Receiver method for the output adapter to receive a filtered, + * decimated sample, apply gain correction, apply cleanup filtering + * and output the sample values as a complex sample. + */ + @Override + public void receive( double left, double right ) + { + mCleanupFilter.receive( + new ComplexSample( (float)( left * mGain ), + (float)( right * mGain ) ) ); + } + + /** + * Sets a new decimation rate + */ + public void setDecimationRate( int rate ) + { + mDecimation = rate; + setGain(); + } + + /** + * Adds a listener to receive output samples + */ + public void setListener( Listener listener ) + { + mCleanupFilter.setListener( listener ); + } + + /** + * Removes the listener from receiving output samples + */ + public void removeListener( Listener listener ) + { + mCleanupFilter.setListener( null ); + } + + public Listener getListener() + { + return mCleanupFilter.getListener(); + } + + /** + * Adjusts the gain correction factor for output samples. + * + * The CIC decimation filter induces significant gain as a factor of + * delay, decimation rate and stage count. So, we apply a gain reduction + * to offset the added gain estimate: + * + * gain = ( decimation * delay ) to power of stageCount + * + * Delay is ONE for the Comb used in this filter. + */ + private void setGain() + { + /* decimation is NOT multiplied by delay since delay is 1 */ + double gain = Math.pow( (double)mDecimation, (double)mStageCount ); + + mGain = 10.0d / gain; + } + } + + /** + * Internal interface for each of the CIC components to pass samples + */ + private interface InternalListener + { + public void receive( double left, double right ); + } +} diff --git a/src/dsp/filter/ComplexFIRFilter.java b/src/dsp/filter/ComplexFIRFilter.java new file mode 100644 index 00000000..22f13482 --- /dev/null +++ b/src/dsp/filter/ComplexFIRFilter.java @@ -0,0 +1,160 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package dsp.filter; + +import java.util.ArrayList; + +import sample.complex.ComplexSample; + +public class ComplexFIRFilter extends ComplexFilter +{ + private ArrayList mBuffer; + private int mBufferSize = 1; //Temporary initial value + private int mBufferPointer = 0; + private double[] mCoefficients; + private int[][] mIndexMap; + private int mCenterCoefficient; + private int mCenterCoefficientMapIndex; + private double mGain; + + public ComplexFIRFilter( double[] coefficients, double gain ) + { + mCoefficients = coefficients; + mBuffer = new ArrayList(); + mBufferSize = mCoefficients.length; + mGain = gain; + + //Fill the buffer with zero valued samples, so we don't have to check for null + for( int x = 0; x < mCoefficients.length; x++ ) + { + mBuffer.add( new ComplexSample( (float)0.0, (float)0.0 ) ); + } + + generateIndexMap( mCoefficients.length ); + } + + public void dispose() + { + mBuffer.clear(); + mBuffer = null; + + mCoefficients = null; + mIndexMap = null; + } + + public void receive( ComplexSample newSample ) + { + //Add the new sample to the buffer + mBuffer.set( mBufferPointer, newSample ); + + //Increment & Adjust the buffer pointer for circular wrap around + mBufferPointer++; + + if( mBufferPointer >= mBufferSize ) + { + mBufferPointer = 0; + } + + //Convolution - multiply filter coefficients by the circular buffer + //samples to calculate a new filtered value + double leftAccumulator = 0; + double rightAccumulator = 0; + + //Start with the center tap value + leftAccumulator += mCoefficients[ mCenterCoefficient ] * + mBuffer.get( mIndexMap[ mBufferPointer ][ mCenterCoefficientMapIndex ] ).left(); + rightAccumulator += mCoefficients[ mCenterCoefficient ] * + mBuffer.get( mIndexMap[ mBufferPointer ][ mCenterCoefficientMapIndex ] ).right(); + + //For the remaining coefficients, add the symmetric samples, oldest and newest + //first, then multiply by the single coefficient + for( int x = 0; x < mCenterCoefficient; x++ ) + { + leftAccumulator += mCoefficients[ x ] * + ( mBuffer.get( mIndexMap[ mBufferPointer ][ x ] ).left() + + mBuffer.get( mIndexMap[ mBufferPointer ][ x + mCenterCoefficient ] ).left() ); + + rightAccumulator += mCoefficients[ x ] * + ( mBuffer.get( mIndexMap[ mBufferPointer ][ x ] ).right() + + mBuffer.get( mIndexMap[ mBufferPointer ][ x + mCenterCoefficient ] ).right() ); + } + + //We're almost finished ... apply gain, cast the doubles to floats and + //send it on it's merry way + send( new ComplexSample( (float)( leftAccumulator * mGain ), + (float)( rightAccumulator * mGain ) ) ); + } + + /** + * @param odd-sized number of filter taps (ie coefficients) and buffer + */ + private void generateIndexMap( int size ) + { + //Ensure we have an odd size + assert( size % 2 == 1 ); + + mIndexMap = new int[ size ][ size ]; + + //Last column will be the center coefficient index value for each row + mCenterCoefficientMapIndex = size - 1; + mCenterCoefficient = (int)( size / 2 ); + + //Setup the first row. Offset is the first row's center coefficient value + //and will become the index value we place in the last column + mIndexMap[ 0 ][ mCenterCoefficientMapIndex ] = mCenterCoefficient; + + //Indexes 0 to 1/2 are their index value, and indexes 1/2 to end are + //an offset of the first half of the values + for( int x = 0; x < mCenterCoefficient; x++ ) + { + mIndexMap[ 0 ][ x ] = x; + mIndexMap[ 0 ][ x + mCenterCoefficient ] = size - 1 - x; + } + + //For each subsequent map row, increment the value of the preceding row + //same column by 1, wrapping to zero when we exceed the size value + for( int x = 1; x < size; x++ ) + { + for( int y = 0; y < size; y++ ) + { + mIndexMap[ x ][ y ] = mIndexMap[ x - 1 ][ y ] + 1; + + if( mIndexMap[ x ][ y ] >= size ) + { + mIndexMap[ x ][ y ] = 0; + } + } + } + } + + public String printIndexMap() + { + StringBuilder sb = new StringBuilder(); + + for( int x = 0; x < mCoefficients.length; x++ ) + { + for( int y = 0; y < mCoefficients.length; y++ ) + { + sb.append( mIndexMap[ x ][ y ] + " " ); + } + sb.append( "\n" ); + } + + return sb.toString(); + } +} diff --git a/src/dsp/filter/ComplexFilter.java b/src/dsp/filter/ComplexFilter.java new file mode 100644 index 00000000..d1b4f266 --- /dev/null +++ b/src/dsp/filter/ComplexFilter.java @@ -0,0 +1,54 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package dsp.filter; + +import sample.Listener; +import sample.complex.ComplexSample; + +public abstract class ComplexFilter implements Listener +{ + private Listener mListener; + + public void setListener( Listener listener ) + { + mListener = listener; + } + + public void dispose() + { + mListener = null; + } + + public Listener getListener() + { + return mListener; + } + + public boolean hasListener() + { + return mListener != null; + } + + protected void send( ComplexSample sample ) + { + if( mListener != null ) + { + mListener.receive( sample ); + } + } +} diff --git a/src/dsp/filter/ComplexHalfBandFilter.java b/src/dsp/filter/ComplexHalfBandFilter.java new file mode 100644 index 00000000..49589286 --- /dev/null +++ b/src/dsp/filter/ComplexHalfBandFilter.java @@ -0,0 +1,186 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package dsp.filter; + +import java.util.ArrayList; + +import sample.complex.ComplexSample; + +public class ComplexHalfBandFilter extends ComplexFilter +{ + private ArrayList mBuffer; + private int mBufferSize = 1; //Temporary initial value + private int mBufferPointer = 0; + private double mGain; + private double[] mCoefficients; + private int[][] mIndexMap; + private int mCenterCoefficient; + private int mCenterCoefficientMapIndex; + private boolean mDispatchFlag = false; + + /** + * Half-Band filter with decimation by 2 against complex samples composed of + * real valued floats. + * + * Takes advantage of the symmetrical nature of FIR filter coefficients by + * adding oldest and newest sample first, then multiplying once by the + * corresponding coefficient + * + * Also, takes advantage of the 0-valued FIR half-band coefficents inherent + * in the half-band filter, and does not calculate those coefficients. + * + * This reduces the workload to (tap-size - 1) / 4 + 1 calculations per sample. + * + * @param filter - filter coefficients + * @param gain - gain multiplier. Use 1.0 for unity/no gain + */ + public ComplexHalfBandFilter( Filters filter, double gain ) + { + mCoefficients = filter.getCoefficients(); + mBuffer = new ArrayList(); + mBufferSize = mCoefficients.length; + + //Fill the buffer with zero valued samples + for( int x = 0; x < mCoefficients.length; x++ ) + { + mBuffer.add( new ComplexSample( (float)0.0, (float)0.0 ) ); + } + + generateIndexMap( mCoefficients.length ); + mGain = gain; + } + + /** + * Calculate the filtered value by applying the coefficients against + * the complex samples in mBuffer + */ + public void receive( ComplexSample newSample ) + { + //Add the new sample to the buffer + mBuffer.set( mBufferPointer, newSample ); + + //Increment & Adjust the buffer pointer for circular wrap around + mBufferPointer++; + + if( mBufferPointer >= mBufferSize ) + { + mBufferPointer = 0; + } + + //Toggle the flag every time, so that we can calculate and dispatch a + //sample every other time, when the flag is true (ie decimate by 2) + mDispatchFlag = !mDispatchFlag; + + //Calculate a filtered sample when the flag is true + if( mDispatchFlag ) + { + //Convolution - multiply filter coefficients by the circular buffer + //samples to calculate a new filtered value + double leftAccumulator = 0; + double rightAccumulator = 0; + + //Start with the center tap value + leftAccumulator += mCoefficients[ mCenterCoefficient ] * + mBuffer.get( mIndexMap[ mBufferPointer ][ mCenterCoefficientMapIndex ] ).left(); + rightAccumulator += mCoefficients[ mCenterCoefficient ] * + mBuffer.get( mIndexMap[ mBufferPointer ][ mCenterCoefficientMapIndex ] ).right(); + + //For the remaining coefficients, add the symmetric samples, oldest and newest + //first, then multiply by the single coefficient + for( int x = 0; x < mCenterCoefficientMapIndex; x += 2 ) + { + leftAccumulator += mCoefficients[ x ] * + ( mBuffer.get( mIndexMap[ mBufferPointer ][ x ] ).left() + + mBuffer.get( mIndexMap[ mBufferPointer ][ x + 1 ] ).left() ); + + rightAccumulator += mCoefficients[ x ] * + ( mBuffer.get( mIndexMap[ mBufferPointer ][ x ] ).right() + + mBuffer.get( mIndexMap[ mBufferPointer ][ x + 1 ] ).right() ); + } + + //We're almost finished ... apply gain, cast the doubles to floats and + //send it on it's merry way + send( new ComplexSample( (float)( leftAccumulator * mGain ), + (float)( rightAccumulator * mGain ) ) ); + } + } + + /** + * Creates an n X (n + 1 / 2) index map enabling quick access to the + * circular buffer samples. + * + * As the buffer shifts right with each subsequent sample, we have to move + * the index pointers with it, for efficient access of the samples. + * + * The first array index value in the index map corresponds to the current + * buffer pointer location. + * + * The second array index value points to the samples that should be + * multiplied by the coefficients as follows: + * + * 0 = center tap sample, to be multiplied by center coefficient + * + * 0 = sample( 1 ) + * 1 = sample( size - 1 ) + * + * Indexes 0 and 1 will be multiplied by coefficient( 0 ). + * + * Subsequent indexes 3, 4, etc, point to the oldest and newest samples that + * correspond to the matching ( 3 ) coefficient index. + * + * @param odd-sized number of filter taps (ie coefficients) and buffer + */ + private void generateIndexMap( int size ) + { + //Ensure we have an odd size + assert( size % 2 == 1 ); + + int mapWidth = ( ( size + 1 ) / 2 ) + 1; + + //Set center tap index for coefficients array + mCenterCoefficient = ( size - 1 ) / 2; + mCenterCoefficientMapIndex = mCenterCoefficient + 1; + + mIndexMap = new int[ size ][ mapWidth ]; + + //Setup the first row, buffer pointer index 0, as a starting point + for( int x = 0; x < mapWidth - 2; x += 2 ) + { + mIndexMap[ 0 ][ x ] = x; + mIndexMap[ 0 ][ x + 1 ] = size - 1 - x; + } + + //Place center tap index in last element + mIndexMap[ 0 ][ mCenterCoefficientMapIndex ] = mCenterCoefficient; + + //For each subsequent row, increment the previous row's value by 1, + //subtracting size as needed, to keep the values between 0 and size - 1 + for( int x = 1; x < size; x++ ) + { + for( int y = 0; y < mapWidth; y++ ) + { + mIndexMap[ x ][ y ] = mIndexMap[ x - 1 ][ y ] + 1; + + if( mIndexMap[ x ][ y ] >= size ) + { + mIndexMap[ x ][ y ] -= size; + } + } + } + } +} diff --git a/src/dsp/filter/ComplexHalfBandNoDecimateFilter.java b/src/dsp/filter/ComplexHalfBandNoDecimateFilter.java new file mode 100644 index 00000000..9b71a55f --- /dev/null +++ b/src/dsp/filter/ComplexHalfBandNoDecimateFilter.java @@ -0,0 +1,180 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package dsp.filter; + +import java.util.ArrayList; + +import sample.Listener; +import sample.complex.ComplexSample; + +public class ComplexHalfBandNoDecimateFilter extends ComplexFilter +{ + private Listener mListener; + private ArrayList mBuffer; + private int mBufferSize = 1; //Temporary initial value + private int mBufferPointer = 0; + private double mGain; + private double[] mCoefficients; + private int[][] mIndexMap; + private int mCenterCoefficient; + private int mCenterCoefficientMapIndex; + + + /** + * Half-Band filter with no decimation against complex samples composed of + * real valued floats. + * + * Takes advantage of the symmetrical nature of FIR filter coefficients by + * adding oldest and newest sample first, then multiplying once by the + * corresponding coefficient + * + * Also, takes advantage of the 0-valued FIR half-band coefficents inherent + * in the half-band filter, and does not calculate those coefficients. + * + * This reduces the workload to (tap-size - 1) / 4 + 1 calculations per sample. + * + * @param filter - filter coefficients + * @param gain - gain multiplier. Use 1.0 for unity/no gain + */ + public ComplexHalfBandNoDecimateFilter( Filters filter, double gain ) + { + mCoefficients = filter.getCoefficients(); + mBuffer = new ArrayList(); + mBufferSize = mCoefficients.length; + + //Fill the buffer with zero valued samples + for( int x = 0; x < mCoefficients.length; x++ ) + { + mBuffer.add( new ComplexSample( (float)0.0, (float)0.0 ) ); + } + + generateIndexMap( mCoefficients.length ); + mGain = gain; + } + + /** + * Calculate the filtered value by applying the coefficients against + * the complex samples in mBuffer + */ + public void receive( ComplexSample newSample ) + { + //Add the new sample to the buffer + mBuffer.set( mBufferPointer, newSample ); + + //Increment & Adjust the buffer pointer for circular wrap around + mBufferPointer++; + + if( mBufferPointer >= mBufferSize ) + { + mBufferPointer = 0; + } + + //Convolution - multiply filter coefficients by the circular buffer + //samples to calculate a new filtered value + double leftAccumulator = 0; + double rightAccumulator = 0; + + //Start with the center tap value + leftAccumulator += mCoefficients[ mCenterCoefficient ] * + mBuffer.get( mIndexMap[ mBufferPointer ][ mCenterCoefficientMapIndex ] ).left(); + rightAccumulator += mCoefficients[ mCenterCoefficient ] * + mBuffer.get( mIndexMap[ mBufferPointer ][ mCenterCoefficientMapIndex ] ).right(); + + //For the remaining coefficients, add the symmetric samples, oldest and newest + //first, then multiply by the single coefficient + for( int x = 0; x < mCenterCoefficientMapIndex; x += 2 ) + { + leftAccumulator += mCoefficients[ x ] * + ( mBuffer.get( mIndexMap[ mBufferPointer ][ x ] ).left() + + mBuffer.get( mIndexMap[ mBufferPointer ][ x + 1 ] ).left() ); + + rightAccumulator += mCoefficients[ x ] * + ( mBuffer.get( mIndexMap[ mBufferPointer ][ x ] ).right() + + mBuffer.get( mIndexMap[ mBufferPointer ][ x + 1 ] ).right() ); + } + + //We're almost finished ... apply gain, cast the doubles to floats and + //send it on it's merry way + send( new ComplexSample( (float)( leftAccumulator * mGain ), + (float)( rightAccumulator * mGain ) ) ); + } + + /** + * Creates an n X (n + 1 / 2) index map enabling quick access to the + * circular buffer samples. + * + * As the buffer shifts right with each subsequent sample, we have to move + * the index pointers with it, for efficient access of the samples. + * + * The first array index value in the index map corresponds to the current + * buffer pointer location. + * + * The second array index value points to the samples that should be + * multiplied by the coefficients as follows: + * + * 0 = center tap sample, to be multiplied by center coefficient + * + * 0 = sample( 1 ) + * 1 = sample( size - 1 ) + * + * Indexes 0 and 1 will be multiplied by coefficient( 0 ). + * + * Subsequent indexes 3, 4, etc, point to the oldest and newest samples that + * correspond to the matching ( 3 ) coefficient index. + * + * @param odd-sized number of filter taps (ie coefficients) and buffer + */ + private void generateIndexMap( int size ) + { + //Ensure we have an odd size + assert( size % 2 == 1 ); + + int mapWidth = ( ( size + 1 ) / 2 ) + 1; + + //Set center tap index for coefficients array + mCenterCoefficient = ( size - 1 ) / 2; + mCenterCoefficientMapIndex = mCenterCoefficient + 1; + + mIndexMap = new int[ size ][ mapWidth ]; + + //Setup the first row, buffer pointer index 0, as a starting point + for( int x = 0; x < mapWidth - 2; x += 2 ) + { + mIndexMap[ 0 ][ x ] = x; + mIndexMap[ 0 ][ x + 1 ] = size - 1 - x; + } + + //Place center tap index in last element + mIndexMap[ 0 ][ mCenterCoefficientMapIndex ] = mCenterCoefficient; + + //For each subsequent row, increment the previous row's value by 1, + //subtracting size as needed, to keep the values between 0 and size - 1 + for( int x = 1; x < size; x++ ) + { + for( int y = 0; y < mapWidth; y++ ) + { + mIndexMap[ x ][ y ] = mIndexMap[ x - 1 ][ y ] + 1; + + if( mIndexMap[ x ][ y ] >= size ) + { + mIndexMap[ x ][ y ] -= size; + } + } + } + } +} diff --git a/src/dsp/filter/ComplexPolyphaseDecimationFilter.java b/src/dsp/filter/ComplexPolyphaseDecimationFilter.java new file mode 100644 index 00000000..e3e71e05 --- /dev/null +++ b/src/dsp/filter/ComplexPolyphaseDecimationFilter.java @@ -0,0 +1,177 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package dsp.filter; + +import sample.Listener; +import sample.complex.ComplexSample; +import buffer.ComplexTappedCircularBuffer; + +public class ComplexPolyphaseDecimationFilter extends ComplexFilter +{ + private FilterPhase[] mFilterPhases; + private int mPhasePointer = 0; + private int mPhaseCount; + + /** + * Implements a decimating poly-phase filter bank for complex samples, as + * described in Understanding Digital Signal Processing, 3e, Lyons, in + * section 10.7, figure 10-14. + * + * Constraints: + * - Low-pass filter coefficient length must be an integer multiple + * of the decimation rate: + * + * Consideration: + * - Low pass filter cutoff frequency must be less than: + * ( input sample rate / 2 / decimation ) + * + * @param coefficients - low pass filter coefficients. + * @param decimationRate - integer decimation rate + */ + public ComplexPolyphaseDecimationFilter( double[] coefficients, + int decimationRate ) + { + /* Verify that the filter length is an integer multiple of the + * decimation rate */ + assert( coefficients.length % decimationRate == 0 ); + + mPhaseCount = decimationRate; + + /* Construct a number of filter phases equal to the decimation rate */ + mFilterPhases = new FilterPhase[ mPhaseCount ]; + + for( int phaseNumber = 0; phaseNumber < mPhaseCount; phaseNumber++ ) + { + double[] phaseCoefficients = + getPhaseCoefficients( coefficients, decimationRate, phaseNumber ); + + mFilterPhases[ phaseNumber ] = new FilterPhase( phaseCoefficients ); + } + } + + /** + * Inputs a new sample into the filter at the non-decimated rate + */ + @Override + public void receive( ComplexSample sample ) + { + mFilterPhases[ mPhasePointer++ ].receive( sample ); + + if( mPhasePointer >= mPhaseCount ) + { + /* Broadcast a new output sample */ + calculate(); + + /* Reset the pointer */ + mPhasePointer = 0; + } + } + + /** + * Outputs a complex sample at the decimated rate, comprised of the sum of + * the outputs of the individual filter stages, trigger by a decimation rate + * counter on the input side. + */ + private void calculate() + { + if( hasListener() ) + { + /* Accumulators */ + float left = 0.0f; + float right = 0.0f; + + for( int x = 0; x < mPhaseCount; x++ ) + { + ComplexSample phaseSample = mFilterPhases[ x ].calculate(); + + left += phaseSample.left(); + right += phaseSample.right(); + } + + send( new ComplexSample( left, right ) ); + } + } + + /** + * Returns the subset of filter coefficients that belong to the filter bank + * phase. + */ + private double[] getPhaseCoefficients( double[] coefficients, + int phaseCount, + int phaseNumber ) + { + int phaseFilterLength = (int)( coefficients.length / phaseCount ); + + double[] phaseCoefficients = new double[ phaseFilterLength ]; + + for( int x = 0; x < phaseFilterLength; x++ ) + { + phaseCoefficients[ x ] = coefficients[ x * phaseCount + phaseNumber ]; + } + + return phaseCoefficients; + } + + /** + * Implements a single phase of a poly-phase filter bank + */ + public class FilterPhase implements Listener + { + private int mLength; + private double[] mCoefficients; + private ComplexTappedCircularBuffer mBuffer; + + /** + * @param coefficients - subset of the filter coefficients for this + * phase of the poly-phase filter bank + */ + public FilterPhase( double[] coefficients ) + { + mCoefficients = coefficients; + mLength = coefficients.length; + + /* The constructor preloads the buffer with 0-valued samples */ + mBuffer = new ComplexTappedCircularBuffer( coefficients.length ); + } + + @Override + public void receive( ComplexSample sample ) + { + mBuffer.add( sample ); + } + + /** + * Performs convolution by multiplying the buffer samples by their + * corresponding filter coefficients and returns the sum + */ + public ComplexSample calculate() + { + /* Accumulators */ + double left = 0.0d; + double right = 0.0d; + + for( int x = 0; x < mLength; x++ ) + { + left += ( mBuffer.get( x ).left() * mCoefficients[ x ] ); + right += ( mBuffer.get( x ).right() * mCoefficients[ x ] ); + } + + return new ComplexSample( (float)left, (float)right ); + } + } +} diff --git a/src/dsp/filter/DCRemovalFilter.java b/src/dsp/filter/DCRemovalFilter.java new file mode 100644 index 00000000..ed61a29a --- /dev/null +++ b/src/dsp/filter/DCRemovalFilter.java @@ -0,0 +1,65 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package dsp.filter; + +import sample.Listener; + +/** + * Implements the real-time general DC removal algorithm described on page + * 762, figure 13-62d, in Understanding Digital Signal Processing 3e, Lyons. + */ +public class DCRemovalFilter implements Listener +{ + private double mAlpha; + private float mPrevious; + private Listener mListener; + + public DCRemovalFilter( double alpha ) + { + mAlpha = alpha; + } + + public void dispose() + { + mListener = null; + } + + @Override + public void receive( Float sample ) + { + send( (float)( sample + ( mPrevious * mAlpha ) - mPrevious ) ); + + mPrevious = sample; + } + + /** + * Sends the filtered sample to the listener + */ + private void send( float sample ) + { + if( mListener != null ) + { + mListener.receive( sample ); + } + } + + public void setListener( Listener listener ) + { + mListener = listener; + } +} diff --git a/src/dsp/filter/DCRemovalFilter2.java b/src/dsp/filter/DCRemovalFilter2.java new file mode 100644 index 00000000..eb89e565 --- /dev/null +++ b/src/dsp/filter/DCRemovalFilter2.java @@ -0,0 +1,56 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package dsp.filter; + +import sample.Listener; + +public class DCRemovalFilter2 implements Listener +{ + private double mAverage; + private double mRatio; + private Listener mListener; + + public DCRemovalFilter2( double ratio ) + { + mRatio = ratio; + } + + @Override + public void receive( Float sample ) + { + mAverage += mRatio * ( (double)sample - mAverage ); + + send( (float)( sample - mAverage ) ); + } + + /** + * Sends the filtered sample to all registered listeners + */ + private void send( float sample ) + { + if( mListener != null ) + { + mListener.receive( sample ); + } + } + + public void setListener( Listener listener ) + { + mListener = listener; + } +} diff --git a/src/dsp/filter/FilterFactory.java b/src/dsp/filter/FilterFactory.java new file mode 100644 index 00000000..dbea3a2d --- /dev/null +++ b/src/dsp/filter/FilterFactory.java @@ -0,0 +1,729 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package dsp.filter; + +import java.util.Arrays; +import java.util.Set; +import java.util.TreeSet; + +import log.Log; +import dsp.filter.Window.WindowType; +import edu.emory.mathcs.jtransforms.fft.DoubleFFT_1D; + +public class FilterFactory +{ + + public static void main( String[] args ) + { + Log.info( "Testing filter factory" ); + + double[] coefficients = getCICCleanupFilter( 48000, 1, WindowType.BLACKMAN ); + + for( int x = 0; x < coefficients.length; x++ ) + { + Log.info( x + ": " + coefficients[ x ] ); + } + + Log.info( "Finished" ); + } + /** + * Generates coefficients for a unity-gain, windowed low-pass filter + * @param sampleRate - hertz + * @param frequency - cutoff in hertz + * @param length - filter length + * @param window - to apply against the coefficients + * @return + */ + public static double[] getSinc( int sampleRate, + int frequency, + int length, + WindowType window ) + { + //Ensure we have an odd length + assert( length % 2 == 0 ); + + //Get unity response array (one element longer to align with IDFT size) + double[] frequencyResponse = getUnityResponseArray( sampleRate, frequency, length + 1 ); + + //Apply Inverse DFT against frequency response unity values, leaving the + //IDFT bin results in the frequency response array + DoubleFFT_1D idft = new DoubleFFT_1D( length + 1 ); + idft.realInverseFull( frequencyResponse, false ); + + //Transfer the IDFT results to the return array + double[] coefficients = new double[ length ]; + int middleCoefficient = (int)( length / 2 ); + + //Bin 0 of the idft is our center coefficient + coefficients[ middleCoefficient ] = frequencyResponse[ 0 ]; + + //The remaining idft bins from 1 to (middle - 1) are the mirror image + //coefficients + for( int x = 1; x < middleCoefficient; x++ ) + { + coefficients[ middleCoefficient + x ] = frequencyResponse[ 2 * x ]; + coefficients[ middleCoefficient - x ] = frequencyResponse[ 2 * x ]; + } + + //Apply the window against the coefficients + coefficients = Window.apply( window, coefficients ); + + //Normalize to unity (1) gain + coefficients = normalize( coefficients ); + + return coefficients; + } + + /** + * Normalizes all filter coefficients to achieve unity (1) gain, by ensuring + * that the sum of the absolute value of all coefficients adds up to 1. + * @param coefficients + * @return + */ + private static double[] normalize( double[] coefficients ) + { + double accumulator = 0; + + for( int x = 0; x < coefficients.length; x++ ) + { + accumulator += Math.abs( coefficients[ x ] ); + } + + for( int x = 0; x < coefficients.length; x++ ) + { + coefficients[ x ] = coefficients[ x ] / accumulator; + } + + return coefficients; + } + + /** + * Constructs an array of unity (1) response values representing the + * desired (pre-windowing) frequency response, used by the sync function + * + * Returns an array twice the length, filled with unity (1) response values + * in the desired pass-band with the positive frequency response starting at + * the lower end of the array, and the negative frequency response at the + * higher end of the first half of the array. The remaining zero-valued + * indexes in the second half will store the results of the JTransforms + * inverse DFT operation + * + * @param sampleRate + * @param frequency + * @param length + * @return + */ + public static double[] getUnityResponseArray( int sampleRate, + int frequency, + int length ) + { + double[] unityArray = new double[ length * 2 ]; + + int binCount = (int)( ( Math.round( + (double)frequency / (double)sampleRate * (double)length ) ) ); + +// if( length % 2 == 0 ) //even length +// { +// for( int x = 0; x < binCount; x++ ) +// { +// unityArray[ x ] = 1.0d; +// unityArray[ length - 1 - x ] = 1.0d; +// } +// } +// else //odd length +// { + unityArray[ 0 ] = 1.0d; + + for( int x = 1; x <= binCount; x++ ) + { + unityArray[ x ] = 1.0d; + unityArray[ length - x ] = 1.0d; + } +// } + + return unityArray; + } + + public static double[] getSine( double sampleRate, double frequency, int length ) + { + //Ensure we have an odd length + assert( length % 2 == 0 ); + + double[] retVal = new double[ length ]; + + double radianFrequency = 2.0D * Math.PI * ( frequency / sampleRate ); + + int middle = (int)( length / 2 ); + + for( int x = 0; x < middle; x++ ) + { + double val = Math.sin( radianFrequency * x ); + + retVal[ middle + x ] = val; + retVal[ middle - x ] = -val; + } + + return retVal; + } + + /** + * Applies a repeating sequence of 1, -1 to the coefficients to invert + * the frequency response of the filter. Used in converting a low-pass + * filter to a high-pass filter. + * + * Inverts the sign of all odd index coefficients ( 1, 3, 5, etc.) + * returning: + * Index 0: same + * Index 1: inverted + * Index 2: same + * ... + * Index length - 1: same + */ + public static double[] invert( double[] coefficients ) + { + for( int x = 1; x < coefficients.length; x += 2 ) + { + coefficients[ x ] = -coefficients[ x ]; + } + + return coefficients; + } + + /** + * Generates filter coefficients for a unity-gain, odd-length, windowed, + * low pass filter with passband from 0-hertz to the cutoff frequency. + * + * @param sampleRate - hertz + * @param cutoff - frequency in hertz + * @param filterLength - odd filter length + * @param windowType - window to apply against the generated coefficients + * @return + */ + public static double[] getLowPass( int sampleRate, + int cutoff, + int filterLength, + WindowType windowType ) + { + if( filterLength % 2 == 0 ) //even length + { + double[] values = getSinc( sampleRate, cutoff, filterLength + 2, windowType ); + + //throw away the 0 index and the last index + return Arrays.copyOfRange( values, 1, values.length - 2 ); + } + else + { + double[] values = getSinc( sampleRate, cutoff, filterLength + 1, windowType ); + + //throw away the 0 index + return Arrays.copyOfRange( values, 1, values.length ); + } + } + + /** + * Creates a low-pass filter with ~ 0.1 db ripple in the pass band + * + * Note: stop frequency - pass frequency defines the transition band. + */ + /** + * Creates a low-pass filter with ~0.1 db ripple in the pass band. The + * transition region is defined by the stop frequency minus the pass + * frequency. + * + * Requires: + * - passFrequency < stopFrequency + * - stopFrequency <= sampleRate/2 + */ + public static double[] getLowPass( int sampleRate, + int passFrequency, + int stopFrequency, + int attenuation, + WindowType windowType, + boolean forceOddLength ) + { + if( stopFrequency < passFrequency || stopFrequency > ( sampleRate / 2 ) ) + { + throw new IllegalArgumentException( "FilterFactory - low pass " + + "filter pass frequency [" + passFrequency + "] must be " + + "less than the stop frequency [" + stopFrequency + "] " + + "and must be less than half [" + (int)( sampleRate / 2 ) + + "] of the sample rate [" + sampleRate + "]" ); + } + + int tapCount = getTapCount( sampleRate, + passFrequency, + stopFrequency, + attenuation ); + + if( forceOddLength ) + { + if( tapCount % 2 == 0 ) + { + tapCount--; + } + } + + return getLowPass( sampleRate, passFrequency, tapCount, windowType ); + } + + /** + * Generates filter coefficients for a unity-gain, odd-length, windowed, + * high pass filter with passband from cutoff frequency to half the sample + * rate. + * + * @param sampleRate - hertz + * @param cutoff - frequency in hertz + * @param filterLength - odd filter length + * @param windowType - window to apply against the generated coefficients + * @return + */ + public static double[] getHighPass( int sampleRate, + int cutoff, + int filterLength, + WindowType windowType ) + { + //Convert the high frequency cutoff to its low frequency cutoff + //equivalent, so that when we generate the low pass filter, prior to + //inversion, its at the correct frequency + int convertedCutoff = sampleRate / 2 - cutoff; + + return invert( getSinc( sampleRate, + convertedCutoff, + filterLength, + windowType ) ); + } + + public static double[] getHighPass( int sampleRate, + int stopFrequency, + int passFrequency, + int attenuation, + WindowType windowType, + boolean forceOddLength ) + { + /* reverse the stop and pass frequency to get the low pass variant */ + int tapCount = getTapCount( sampleRate, + stopFrequency, + passFrequency, + attenuation ); + + if( forceOddLength ) + { + if( tapCount % 2 == 0 ) + { + tapCount--; + } + } + + return invert( getLowPass( sampleRate, stopFrequency, tapCount, windowType ) ); + } + + /** + * Utility to log the arrays of doubles with line breaks + */ + public static String arrayToString( double[] array, boolean breaks ) + { + StringBuilder sb = new StringBuilder(); + for( int x = 0; x < array.length; x++ ) + { + sb.append( x + ": " + array[ x ] ); + + if( breaks ) + { + sb.append( "\n" ); + } + else + { + if( x % 8 == 7 ) + { + sb.append( "\n" ); + } + else + { + sb.append( "\t" ); + } + } + } + + return sb.toString(); + } + + /** + * Determines the number of fir filter taps required to produce the + * specified frequency response with passband ripple near .1dB. + * + * Implements the algorithm from Understanding Digital Signal Processing, 3e + * , Lyons, section 5.10.5. + * + * @param sampleRate in hertz + * @param pass pass frequency in hertz + * @param stop stop frequency in hertz + * @param attenuation in dB + * @return + */ + public static int getTapCount( int sampleRate, + int pass, + int stop, + int attenuation ) + { + double frequency = ( (double)stop - (double)pass ) / (double)sampleRate; + + return (int)( Math.round( (double)attenuation / ( 22.0d * frequency ) ) ); + } + + public static ComplexFilter[] getDecimationFilters( int sampleRate, + int decimatedRate, int passFrequency, int attenuation, + WindowType windowType ) + { + ComplexFilter[] filters; + + if( sampleRate == 96000 && decimatedRate == 48000 ) + { + filters = new ComplexFilter[ 1 ]; + filters[ 0 ] = new ComplexHalfBandFilter( + Filters.FIR_HALF_BAND_31T_ONE_EIGHTH_FCO, 1.0002 ); + } + else if( sampleRate == 192000 && decimatedRate == 48000 ) + { + filters = new ComplexFilter[ 2 ]; + filters[ 0 ] = new ComplexHalfBandFilter( + Filters.FIR_HALF_BAND_31T_ONE_EIGHTH_FCO, 1.0002 ); + filters[ 1 ] = new ComplexHalfBandFilter( + Filters.FIR_HALF_BAND_31T_ONE_EIGHTH_FCO, 1.0002 ); + } + else + { + filters = new ComplexFilter[ 1 ]; + + int decimate = (int)( sampleRate / decimatedRate ); + + filters[ 0 ] = new ComplexCICDecimate( decimate, 2, decimatedRate ); + +// int finalStopFrequency = decimatedRate - passFrequency; +// +// int[] rates = getPolyphaseDecimationRates( sampleRate, +// decimatedRate, +// passFrequency, +// finalStopFrequency ); +// +// filters = new ComplexFilter[ rates.length ]; +// +// int counter = 0; +// int currentSampleRate = sampleRate; +// int currentDecimatedRate = sampleRate; +// +// while( counter < rates.length ) +// { +// currentDecimatedRate /= rates[ counter ]; +// +// int stopFrequency = currentDecimatedRate- passFrequency; +// +// int proposedFilterLength = getTapCount( currentSampleRate, +// passFrequency, +// stopFrequency, +// attenuation ); +// +// /* check filter length is multiple of decimation rate */ +// int multiplier = (int)( proposedFilterLength / rates[ counter ] ); +// +// /* increase filter length to the next multiple, if needed */ +// if( multiplier * rates[ counter ] < proposedFilterLength ) +// { +// multiplier++; +// } +// +// double[] filter = getLowPass( currentSampleRate, +// passFrequency, +// rates[counter] * multiplier, +// windowType ); +// +// filters[ counter ] = +// new ComplexPolyphaseDecimationFilter( filter, rates[ counter ] ); +// +// Log.info( "FilterFactory - PFD filter [" + ( counter + 1 ) + "/" + rates.length + +// "] overall rate [" + sampleRate + +// "] decimate [" + rates[ counter ] + +// "] filter taps [" + filter.length + +// "] rate [" + currentSampleRate + +// "] pass [" + passFrequency + +// "]" ); +// +// currentSampleRate /= rates[ counter ]; +// counter++; +// } + } + + return filters; + } + + /** + * Determines decimation rate(s) for a polyphase decimation filter. + * + * @param sampleRate - starting sample rate + * @param decimatedRate - final (decimated) output rate + * @return - set of integer decimation rates for a single or multi-stage + * polyphase filter decimation chain + * @throws - AssertionException if sample rate is not a multiple of 48 kHz + */ + public static int[] getPolyphaseDecimationRates( int sampleRate, + int decimatedRate, + int passFrequency, + int stopFrequency ) + { + int[] rates; + + if( sampleRate % decimatedRate != 0 ) + { + throw new IllegalArgumentException( "Decimated rate must be an " + + "integer multiple of sample rate" ); + } + + int decimation = (int)( sampleRate / decimatedRate ); + + //Decimation rates below 20 will use single stage polyphase filter + if( decimation < 20 ) + { + rates = new int[ 1 ]; + rates[ 0 ] = decimation; + return rates; + } + else + { + int optimalStage1 = + getOptimalStageOneRate( sampleRate, decimation, passFrequency, stopFrequency ); + + Set factors = getFactors( decimation ); + + int stage1 = findClosest( optimalStage1, factors ); + + Log.info( "Decimation rate [" + decimation + + "] stage1 optimal [" + optimalStage1 + + "] stage1 actual [" + stage1 + + "]"); + + if( stage1 == decimation || stage1 == 1 ) + { + rates = new int[ 1 ]; + rates[ 0 ] = decimation; + return rates; + } + else + { + rates = new int[ 2 ]; + + int stage2 = (int)( decimation / stage1); + + if( stage1 > stage2 ) + { + rates[ 0 ] = stage1; + rates[ 1 ] = stage2; + } + else + { + rates[ 0 ] = stage2; + rates[ 1 ] = stage1; + } + return rates; + } + } + } + + /** + * Finds the factor that is closest to the desired factor, from an ordered + * list of factors. + */ + private static int findClosest( int desiredFactor, Set factors ) + { + int bestFactor = 1; + int bestDelta = desiredFactor; + + for( Integer factor: factors ) + { + int testDelta = Math.abs( desiredFactor - factor ); + + if( testDelta < bestDelta ) + { + bestDelta = testDelta; + bestFactor = factor; + } + } + + return bestFactor; + } + + /** + * Determines the factors that make up an integer. Uses a brute force + * method to iterate all integers from 1 to value, determining which factors + * are evenly divisible into the value. + * + * @param value - integer decimation value + * @return - ordered set of factors for value + */ + private static Set getFactors( int value ) + { + Set factors = new TreeSet(); + + /* Brute force */ + for( int x = 1; x <= value; x++ ) + { + int remainder = (int)( value / x ); + + if( remainder * x == value ) + { + factors.add( x ); + } + } + + return factors; + } + + /** + * Determines the optimal decimation rate for stage 1 of a two-stage + * poly-phase decimation filter chain, to produce a final sample rate of + * 48 kHz using a pass bandwidth of 25 kHz. + * + * Use for total decimation rates of 20 or higher. + * + * Implements the algorithm described in Lyons, Understanding Digital Signal + * Processing, 3e, section 10.2.1, page 511. + * + * @param sampleRate + * @param decimation + * @param passFrequency frequency of the pass band + * @return optimum integer decimation rate for the first stage decimation + * filter + */ + public static int getOptimalStageOneRate( int sampleRate, + int decimation, + int passFrequency, + int stopFrequency ) + { + double ratio = getBandwidthRatio( passFrequency, stopFrequency ); + + double numerator = 1.0d - ( + Math.sqrt( (double)decimation * ratio / ( 2.0d - ratio ) ) ); + + double denominator = 2.0d - ( ratio * ( decimation + 1.0d ) ); + + int retVal = (int)( 2.0d * decimation * ( numerator / denominator ) ); + + Log.info( "Optimal Stage 1 Decimation - rate [" + sampleRate + + "] pass [" + passFrequency + + "] bw ratio [" + ratio + + "] optimal [" + retVal + + "]" ); + + return retVal; + } + + /** + * Determines the F ratio as described in Lyons, Understanding Digital + * Signal Processing, 3e, section 10.2.1, page 511 + * + * Used in conjunction with the optimal stage one decimation rate method + * above. + */ + private static double getBandwidthRatio( int passFrequency, int stopFrequency ) + { + assert( passFrequency < stopFrequency ); + + return ( (double)( stopFrequency - passFrequency ) / + (double)stopFrequency ); + } + + /** + * Assumes that the pass band is 1/4 of the output sample rate. + * + * Assumes the stop band is: pass + (pass * .25). + * @param outputSampleRate + * @param stageCount + * @return + */ + public static double[] getCICCleanupFilter( int outputSampleRate, + int stageCount, + WindowType window ) + { + int pass = (int)( outputSampleRate / 4 ); + int stop = pass + (int)( (double)pass * 0.25d ); + int attenuation = 60; //db + + int taps = getTapCount( outputSampleRate, pass, stop, attenuation ); + + /* Make tap count odd */ + if( taps% 2 == 0 ) + { + taps ++; + } + + double[] frequencyResponse = + getCICResponseArray( outputSampleRate, pass, taps, stageCount ); + + //Apply Inverse DFT against frequency response unity values, leaving the + //IDFT bin results in the frequency response array + DoubleFFT_1D idft = new DoubleFFT_1D( taps ); + idft.realInverseFull( frequencyResponse, false ); + + //Transfer the IDFT results to the odd length return array + double[] coefficients = new double[ taps ]; + int middleCoefficient = (int)( taps / 2 ); + + //Bin 0 of the idft is our center coefficient + coefficients[ middleCoefficient ] = frequencyResponse[ 0 ]; + + //The remaining idft bins from 1 to (middle - 1) are the mirror image + //coefficients + for( int x = 1; x <= middleCoefficient; x++ ) + { + coefficients[ middleCoefficient + x ] = frequencyResponse[ 2 * x ]; + coefficients[ middleCoefficient - x ] = frequencyResponse[ 2 * x ]; + } + + //Apply the window against the coefficients + coefficients = Window.apply( window, coefficients ); + + return coefficients; + } + + public static double[] getCICResponseArray( int sampleRate, + int frequency, + int length, + int stageCount ) + { + double[] cicArray = new double[ length * 2 ]; + + int binCount = (int)( ( Math.round( + (double)frequency / (double)sampleRate * (double)length ) ) ); + + cicArray[ 0 ] = 1.0d; + + double unityResponse = Math.pow( Math.sin( 1.0d / (double)length ) / + ( 1.0d / (double)length ), stageCount ); + + for( int x = 1; x <= binCount; x++ ) + { + /* Calculate unity response plus amplification for droop */ + double compensated = 1.0d + ( unityResponse - + Math.pow( ( Math.sin( (double)x / (double)length ) / + ( (double)x / (double)length ) ), stageCount ) ); + + cicArray[ x ] = compensated; + cicArray[ length - x ] = compensated; + } + + return cicArray; + } +} diff --git a/src/dsp/filter/Filters.java b/src/dsp/filter/Filters.java new file mode 100644 index 00000000..ab881c89 --- /dev/null +++ b/src/dsp/filter/Filters.java @@ -0,0 +1,1238 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package dsp.filter; + +public enum Filters +{ + FIRHP_LTR_DCBLOCK_750FS_50FC( new double[] + { + 0.0679294122464217, + -0.07324095797199973, + -0.03838823381974882, + -0.013430897335300878, + 0.006449207638138327, + 0.022750490924149575, + 0.034175688012529584, + 0.037600969376502945, + 0.030002787357015913, + 0.009961076769098255, + -0.02214359076495753, + -0.062375656373163425, + -0.10487871348895818, + -0.14241968373731975, + -0.16818470634383087, + 0.8226518956829582, + -0.16818470634383087, + -0.14241968373731975, + -0.10487871348895818, + -0.062375656373163425, + -0.02214359076495753, + 0.009961076769098255, + 0.030002787357015913, + 0.037600969376502945, + 0.034175688012529584, + 0.022750490924149575, + 0.006449207638138327, + -0.013430897335300878, + -0.03838823381974882, + -0.07324095797199973, + 0.0679294122464217 + }), + + FIRLP_LTR_1500FS( new double[] + { + -0.008272676875607624, + -0.0024939020178146617, + 0.010376092557732189, + 0.011962525412519345, + -0.003977368587769054, + -0.013289773851060555, + -0.0029497606301098858, + 0.003941811083306652, + -0.003154954069081114, + 0.005485070321679545, + 0.03171909301543344, + 0.015387284536986508, + -0.055467824110903244, + -0.07246950346172203, + 0.03466378905394898, + 0.1322297692443333, + 0.04683098737206109, + -0.13469992806577527, + -0.14450919452286415, + 0.0578348928408703, + 0.18926196545202326, + 0.0578348928408703, + -0.14450919452286415, + -0.13469992806577527, + 0.04683098737206109, + 0.1322297692443333, + 0.03466378905394898, + -0.07246950346172203, + -0.055467824110903244, + 0.015387284536986508, + 0.03171909301543344, + 0.005485070321679545, + -0.003154954069081114, + 0.003941811083306652, + -0.0029497606301098858, + -0.013289773851060555, + -0.003977368587769054, + 0.011962525412519345, + 0.010376092557732189, + -0.0024939020178146617, + -0.008272676875607624 + }), + + FIRBP_1200FSK_24000FS( new double[] + { + 0.004792788663425844, + 0.004034377807422182, + 0.004699808237099943, + 0.00443210843891432, + 0.0028638644082473, + -0.00015292185838515035, + -0.004394278846594682, + -0.009212560275995686, + -0.013596659741332163, + -0.016383584186535002, + -0.016573550574018152, + -0.013657566836436204, + -0.007851047182221552, + -0.00014425394402752814, + 0.007885453072038891, + 0.014465082677564993, + 0.01811703090261803, + 0.01810997536219774, + 0.014727244087571567, + 0.009244939917342813, + 0.0036035605020527947, + -0.00014781830127321208, + -0.0004913597198315713, + 0.0030136991548897693, + 0.009453462203915387, + 0.01666055505335309, + 0.02170180561798826, + 0.021656701887523472, + 0.014498683513932197, + -0.0001565221289617459, + -0.02065343799220559, + -0.04339467795574746, + -0.06347784859804807, + -0.07576298936541592, + -0.07612281593093975, + -0.06256712023008996, + -0.03596397841747506, + -0.00012170810308311971, + 0.03885147059625151, + 0.07377979316631962, + 0.09792980519752477, + 0.10655596332800969, + 0.09792980519752477, + 0.07377979316631962, + 0.03885147059625151, + -0.00012170810308311971, + -0.03596397841747506, + -0.06256712023008996, + -0.07612281593093975, + -0.07576298936541592, + -0.06347784859804807, + -0.04339467795574746, + -0.02065343799220559, + -0.0001565221289617459, + 0.014498683513932197, + 0.021656701887523472, + 0.02170180561798826, + 0.01666055505335309, + 0.009453462203915387, + 0.0030136991548897693, + -0.0004913597198315713, + -0.00014781830127321208, + 0.0036035605020527947, + 0.009244939917342813, + 0.014727244087571567, + 0.01810997536219774, + 0.01811703090261803, + 0.014465082677564993, + 0.007885453072038891, + -0.00014425394402752814, + -0.007851047182221552, + -0.013657566836436204, + -0.016573550574018152, + -0.016383584186535002, + -0.013596659741332163, + -0.009212560275995686, + -0.004394278846594682, + -0.00015292185838515035, + 0.0028638644082473, + 0.00443210843891432, + 0.004699808237099943, + 0.004034377807422182, + 0.004792788663425844 + }), + + FIRLP_3K( new double[] + { + /* + + FIR filter designed with + http://t-filter.appspot.com + + sampling frequency: 48000 Hz + + * 0 Hz - 3000 Hz + gain = 1 + desired ripple = 5 dB + actual ripple = 4.1593978370041 dB + + * 4000 Hz - 24000 Hz + gain = 0 + desired attenuation = -80 dB + actual attenuation = -80.03322246183818 dB + + */ + + -0.00011443124614437417, + -0.00017293152659867192, + -0.0002537395679787903, + -0.0002960101735353313, + -0.00023955416024142116, + -0.000005428557963321982, + 0.0004964100334261843, + 0.0013538842757409063, + 0.002634918910611058, + 0.00436693575408337, + 0.0065175161697321555, + 0.00897952475800971, + 0.011566379161822652, + 0.014018876408451886, + 0.016028053953955006, + 0.017270887068042688, + 0.01745751423179922, + 0.0163833187291431, + 0.013977100662194252, + 0.010337169842377791, + 0.005746282323474722, + 0.0006589019184637925, + -0.004341600521758248, + -0.008613131831428159, + -0.011545791615276019, + -0.012660543617859996, + -0.011700549440186059, + -0.008698639621450092, + -0.004006684845818583, + 0.00172317360274727, + 0.007608807120062409, + 0.012654244324839246, + 0.015898391911284164, + 0.016575938784747404, + 0.014265883702010186, + 0.009002241493722784, + 0.0013255862247370318, + -0.007739007276591945, + -0.0167810834076902, + -0.024172675639705345, + -0.02828537119270343, + -0.027727506101182933, + -0.02156733481426441, + -0.009507700677949931, + 0.008016566866990643, + 0.02983764491633055, + 0.05415546347475946, + 0.0787307236559999, + 0.10114644750135711, + 0.11910360136213424, + 0.13071022495045617, + 0.13472344727490876, + 0.13071022495045617, + 0.11910360136213424, + 0.10114644750135711, + 0.0787307236559999, + 0.05415546347475946, + 0.02983764491633055, + 0.008016566866990643, + -0.009507700677949931, + -0.02156733481426441, + -0.027727506101182933, + -0.02828537119270343, + -0.024172675639705345, + -0.0167810834076902, + -0.007739007276591945, + 0.0013255862247370318, + 0.009002241493722784, + 0.014265883702010186, + 0.016575938784747404, + 0.015898391911284164, + 0.012654244324839246, + 0.007608807120062409, + 0.00172317360274727, + -0.004006684845818583, + -0.008698639621450092, + -0.011700549440186059, + -0.012660543617859996, + -0.011545791615276019, + -0.008613131831428159, + -0.004341600521758248, + 0.0006589019184637925, + 0.005746282323474722, + 0.010337169842377791, + 0.013977100662194252, + 0.0163833187291431, + 0.01745751423179922, + 0.017270887068042688, + 0.016028053953955006, + 0.014018876408451886, + 0.011566379161822652, + 0.00897952475800971, + 0.0065175161697321555, + 0.00436693575408337, + 0.002634918910611058, + 0.0013538842757409063, + 0.0004964100334261843, + -0.000005428557963321982, + -0.00023955416024142116, + -0.0002960101735353313, + -0.0002537395679787903, + -0.00017293152659867192, + -0.00011443124614437417 + } ), + + FIRLP_8K( new double[] { + + /* + + FIR filter designed with + http://t-filter.appspot.com + + sampling frequency: 48000 Hz + + * 0 Hz - 4000 Hz + gain = 1 + desired ripple = 5 dB + actual ripple = 4.166465745565044 dB + + * 4600 Hz - 24000 Hz + gain = 0 + desired attenuation = -60 dB + actual attenuation = -60.01902518862098 dB + + */ + + -0.0005876043141107231, + 0.0000395798795893033, + 0.0007936108691418746, + 0.002271220583758663, + 0.0045690708238380244, + 0.007632833240537176, + 0.011210445195711924, + 0.014848077882921171, + 0.01794155760934873, + 0.019840991376714804, + 0.01999155424676768, + 0.018081523929298803, + 0.01415831726867514, + 0.008676730505642374, + 0.0024562153938831365, + -0.0034571527303437705, + -0.008001879410852286, + -0.010340843984508226, + -0.010072395969237698, + -0.007354895192546896, + -0.0029039203557233197, + 0.0021492484218134564, + 0.006514352308475102, + 0.009056570176096615, + 0.009090754245710363, + 0.006576743943510026, + 0.002156484338606342, + -0.0029881335341335434, + -0.007431040256860951, + -0.009881347307264159, + -0.009547761214420764, + -0.006387716720906473, + -0.00116384277476187, + 0.004722257199180802, + 0.0095865822102477, + 0.011924484237795421, + 0.010856600363787754, + 0.006429626923814576, + -0.00032219256325637487, + -0.007591488679896054, + -0.01325346967699737, + -0.015451428687349951, + -0.013159262174429455, + -0.006567549275313882, + 0.0028476895325932673, + 0.012598008810195174, + 0.019781804328972176, + 0.02186025227953089, + 0.017422058628364847, + 0.006738234527549823, + -0.008066547704296321, + -0.023306710233840622, + -0.0344567035523893, + -0.037158269971142174, + -0.028289474602001076, + -0.006838858354697991, + 0.02563946671507246, + 0.06512400470969, + 0.10578445793560229, + 0.141076536562424, + 0.16504202130157974, + 0.17352664849913732, + 0.16504202130157974, + 0.141076536562424, + 0.10578445793560229, + 0.06512400470969, + 0.02563946671507246, + -0.006838858354697991, + -0.028289474602001076, + -0.037158269971142174, + -0.0344567035523893, + -0.023306710233840622, + -0.008066547704296321, + 0.006738234527549823, + 0.017422058628364847, + 0.02186025227953089, + 0.019781804328972176, + 0.012598008810195174, + 0.0028476895325932673, + -0.006567549275313882, + -0.013159262174429455, + -0.015451428687349951, + -0.01325346967699737, + -0.007591488679896054, + -0.00032219256325637487, + 0.006429626923814576, + 0.010856600363787754, + 0.011924484237795421, + 0.0095865822102477, + 0.004722257199180802, + -0.00116384277476187, + -0.006387716720906473, + -0.009547761214420764, + -0.009881347307264159, + -0.007431040256860951, + -0.0029881335341335434, + 0.002156484338606342, + 0.006576743943510026, + 0.009090754245710363, + 0.009056570176096615, + 0.006514352308475102, + 0.0021492484218134564, + -0.0029039203557233197, + -0.007354895192546896, + -0.010072395969237698, + -0.010340843984508226, + -0.008001879410852286, + -0.0034571527303437705, + 0.0024562153938831365, + 0.008676730505642374, + 0.01415831726867514, + 0.018081523929298803, + 0.01999155424676768, + 0.019840991376714804, + 0.01794155760934873, + 0.014848077882921171, + 0.011210445195711924, + 0.007632833240537176, + 0.0045690708238380244, + 0.002271220583758663, + 0.0007936108691418746, + 0.0000395798795893033, + -0.0005876043141107231 + } ), + + FIRLP_15K( new double[] { + /* + FIR filter designed with http://t-filter.appspot.com + sampling frequency: 48000 Hz + * 0 Hz - 7000 Hz + gain = 1 + desired ripple = 5 dB + actual ripple = 3.6923464109664326 dB + * 9000 Hz - 24000 Hz + gain = 0 + desired attenuation = -40 dB + actual attenuation = -41.03342332843521 dB + */ + -0.014082901225560598, + -0.02333969799770533, + -0.022033345340200808, + -0.0004492742003176776, + 0.034217272170844526, + 0.05847499164066433, + 0.04845184454729058, + 0.0029885635174003813, + -0.045972124010673326, + -0.04957432533335844, + 0.02225593631403953, + 0.15189293741405197, + 0.27718300156351594, + 0.32893125787628796, + 0.27718300156351594, + 0.15189293741405197, + 0.02225593631403953, + -0.04957432533335844, + -0.045972124010673326, + 0.0029885635174003813, + 0.04845184454729058, + 0.05847499164066433, + 0.034217272170844526, + -0.0004492742003176776, + -0.022033345340200808, + -0.02333969799770533, + -0.014082901225560598 + } ), + + //FS=192000 FC=40000 WIN: Kaiser-Bessel + FIR_HALF_BAND_31T_ONE_EIGHTH_FCO( new double[] { + -0.000045886616046836, 0, + 0.000591369980811269, 0, + -0.002865894676690263, 0, + 0.008258847333251895, 0, + -0.019555838964709603, 0, + 0.041885910942306370, 0, + -0.091494773870957058, 0, + 0.313146427083389300, 0.5, //.499981677577289670, + 0.313146427083389300, 0, + -0.091494773870957058, 0, + 0.041885910942306370, 0, + -0.019555838964709603, 0, + 0.008258847333251895, 0, + -0.002865894676690263, 0, + 0.000591369980811269, 0, + -0.000045886616046836 + } ), + FIRHP_31T_48KFS_300FCO( new double[] { 0.079454311268354116, + -0.431252134555296540, -0.080866558157993096, -0.018201087229858796, + -0.006959451230405745, -0.005082182047181806, -0.004827982359353442, + -0.004678622485731351, -0.004518783884113521, -0.004590986714193629, + -0.004788367734908081, -0.004864687584309190, -0.004847881778381928, + -0.004819115432733451, -0.004899065399109821, 0.995074461482826860, + -0.004899065399109821, -0.004819115432733451, -0.004847881778381928, + -0.004864687584309190, -0.004788367734908081, -0.004590986714193629, + -0.004518783884113521, -0.004678622485731351, -0.004827982359353442, + -0.005082182047181806, -0.006959451230405745, -0.018201087229858796, + -0.080866558157993096, -0.431252134555296540, 0.079454311268354116 } ), + + FIRHP_31T_12KFS_400FCO( new double[] { 0.053389156955298771, + 0.182002884434506210, -0.086127791245869528, -0.016516534724731153, + -0.036968390772931715, -0.035475484228869217, -0.040734954194563675, + -0.044789081155425826, -0.049513180269900807, -0.054086500917566893, + -0.058563271660686450, -0.062449865988963169, -0.065441928590436960, + -0.068170371167388846, -0.069540820637351994, 0.930065488549365990, + -0.069540820637351994, -0.068170371167388846, -0.065441928590436960, + -0.062449865988963169, -0.058563271660686450, -0.054086500917566893, + -0.049513180269900807, -0.044789081155425826, -0.040734954194563675, + -0.035475484228869217, -0.036968390772931715, -0.016516534724731153, + -0.086127791245869528, 0.182002884434506210, 0.053389156955298771 } ), + + /** + * FIR Low Pass Filter + * Sample Rate: 48000 + * Taps: 55 + * Cutoff Freq: 3000 Hz + * Designer: http://t-filter.appspot.com/fir/index.html + */ + + FIRLP_55TAP_48000FS_3000FC( new double[] { -0.0052419787903715655, + -0.0003789909443307852, 0.0012970137854134386, 0.004210460544067679, + 0.008221266221665309, 0.012951576579356689, 0.017794312853913318, + 0.021959871931790928, 0.024583800118549094, 0.024878144443784132, + 0.022296319647647538, 0.016685495012933472, 0.008390268267799136, + -0.001718264469387386, -0.012288389660616603, -0.02161633130497537, + -0.027882128567957774, -0.02943377233328899, -0.025006516193566884, + -0.014040074880106284, 0.0032217508351258103, 0.025660031586092195, + 0.05136009984280478, 0.0778336106888181, 0.10230926486919244, + 0.12209961841343948, 0.13496813439824185, 0.13943117253534673, + 0.13496813439824185, 0.12209961841343948, 0.10230926486919244, + 0.0778336106888181, 0.05136009984280478, 0.025660031586092195, + 0.0032217508351258103, -0.014040074880106284, -0.025006516193566884, + -0.02943377233328899, -0.027882128567957774, -0.02161633130497537, + -0.012288389660616603, -0.001718264469387386, 0.008390268267799136, + 0.016685495012933472, 0.022296319647647538, 0.024878144443784132, + 0.024583800118549094, 0.021959871931790928, 0.017794312853913318, + 0.012951576579356689, 0.008221266221665309, 0.004210460544067679, + 0.0012970137854134386, -0.0003789909443307852, -0.0052419787903715655 } ), + + /* + + FIR filter designed with + http://t-filter.appspot.com + + sampling frequency: 24000 Hz + + * 0 Hz - 1400 Hz + gain = 1 + desired ripple = 5 dB + actual ripple = 4.175011388566792 dB + + * 1500 Hz - 12000 Hz + gain = 0 + desired attenuation = -40 dB + actual attenuation = -40.00189415137373 dB + + */ + FIRLP_24000FS_TO_3000FS( new double[] { + + /* + + FIR filter designed with + http://t-filter.appspot.com + + sampling frequency: 48000 Hz + + * 0 Hz - 3000 Hz + gain = 1 + desired ripple = 5 dB + actual ripple = 3.939100294265316 dB + + * 3500 Hz - 24000 Hz + gain = 0 + desired attenuation = -40 dB + actual attenuation = -40.489198844529355 dB + + */ + + .2,.2,.2,.2,.2,.2,.2 + }), + + FIRLP_3000FS_TO_150FC( new double[] { + /* + + FIR filter designed with + http://t-filter.appspot.com + + sampling frequency: 3000 Hz + + * 0 Hz - 155 Hz + gain = 1 + desired ripple = 5 dB + actual ripple = 4.1713329390800755 dB + + * 160 Hz - 1500 Hz + gain = 0 + desired attenuation = -40 dB + actual attenuation = -40.00926353319547 dB + + */ + + -0.006122920352830965, + -0.0023884346335371165, + -0.002599310268302821, + -0.002619226598223891, + -0.0024058915455907196, + -0.0019288301578398123, + -0.0011732914244696805, + -0.00014348647168516363, + 0.0011360279713313209, + 0.002620018618160862, + 0.004244279668461443, + 0.005929041211240287, + 0.007583700132903604, + 0.009112816853143859, + 0.010422968037142797, + 0.011429759533610568, + 0.012064461696610605, + 0.01227985703081096, + 0.012054670302231996, + 0.011395973228693276, + 0.010339492321850014, + 0.008947826461137941, + 0.007306289027941143, + 0.005516680433668245, + 0.003689888284206567, + 0.0019376716631701368, + 0.00036406537300421297, + -0.0009423273945619157, + -0.0019147615431778597, + -0.0025137861299055595, + -0.00272885000805846, + -0.002579780077995427, + -0.00211370881796881, + -0.001401086778987431, + -0.0005294931107453994, + 0.00040506185948651675, + 0.0013056441129537509, + 0.0020838354221374305, + 0.0026667562654551246, + 0.003003052919640715, + 0.003066845756573675, + 0.002858826770125135, + 0.002405247908616018, + 0.0017550758096436146, + 0.0009750187684900944, + 0.0001428944628485573, + -0.0006600057708364918, + -0.0013572018806166464, + -0.0018849613428995964, + -0.0021983440926621103, + -0.002274533536918245, + -0.0021136819231921474, + -0.0017380160407736585, + -0.0011899674040168643, + -0.0005293364966829129, + 0.00017203581592664565, + 0.0008397052861045773, + 0.0014076463254305174, + 0.0018238726340835366, + 0.0020485384828633777, + 0.002054368304440125, + 0.00184518937028339, + 0.001463759939291864, + 0.0009254779772329933, + 0.00030908834306052194, + -0.00033072041687547837, + -0.0009242244673079876, + -0.001411151838185113, + -0.0017423327816480623, + -0.001885184212057672, + -0.001827180245955061, + -0.0015767572410517868, + -0.0011623018660948076, + -0.0006290757127071664, + -0.000034277486241399186, + 0.0005590116846630576, + 0.0010883547994854647, + 0.0014984460140958136, + 0.0017469004431845741, + 0.0018085733127357138, + 0.00167799070404902, + 0.0013699023886079994, + 0.0009178041117923993, + 0.00037029761935479554, + -0.00021414834948363428, + -0.0007733385753572961, + -0.0012479991812573157, + -0.0015880719715790936, + -0.0017577706864892022, + -0.0017393843977854672, + -0.0015352614436320124, + -0.0011668823340746201, + -0.0006738084260047715, + -0.00010821707107113083, + 0.00046927876230741587, + 0.0009969768021073388, + 0.001418713685742809, + 0.001689241246422699, + 0.0017799814611489096, + 0.0016808579160732414, + 0.001401938565514821, + 0.0009725070918705811, + 0.00043789676780500817, + -0.00014493157532811753, + -0.0007134285789517371, + -0.0012066408059786802, + -0.0015718235937901522, + -0.0017696752225057275, + -0.0017782857455891608, + -0.0015955792180418218, + -0.001239958862296361, + -0.00074891505028209, + -0.0001750373426426684, + 0.0004203972508663324, + 0.0009743368473658559, + 0.001427583798294352, + 0.0017300695180374488, + 0.0018474313131084748, + 0.0017665803736364497, + 0.0014959523321337515, + 0.0010620969968500548, + 0.0005087929550228872, + -0.00010287888971484906, + -0.0007071978327354896, + -0.0012439856702169039, + -0.0016485023632017621, + -0.0018816779265696613, + -0.0019137822019732152, + -0.0017410363220249359, + -0.0013797190792108685, + -0.0008669516848493666, + -0.0002564820252824784, + 0.00038702570052167175, + 0.0009946245737452108, + 0.0015005576753828536, + 0.0018494543772917837, + 0.002002271293795752, + 0.0019405399480025168, + 0.001668583653638679, + 0.0012133648938024923, + 0.0006218226272857692, + -0.00004396586555373278, + -0.0007131888239163173, + -0.0013137753201852866, + -0.0017802301804471922, + -0.002060772342951284, + -0.0021230164816546412, + -0.0019576726289922586, + -0.001579698202436061, + -0.0010269473735399154, + -0.0003566571442405154, + 0.00036046532149158213, + 0.001047910209033917, + 0.0016308959748307101, + 0.0020457879850275806, + 0.0022452826272775055, + 0.0022053677261764473, + 0.0019268362424314605, + 0.001436104198970914, + 0.000783301048300022, + 0.000036170113001030666, + -0.0007258491765731604, + -0.0014208542690678963, + -0.001973351045206107, + -0.0023218012876373207, + -0.002425430974535461, + -0.0022692307984843695, + -0.0018659822196923194, + -0.0012553733554405261, + -0.0005001275988053344, + 0.0003207533398910953, + 0.0011202470135660719, + 0.0018119245687932325, + 0.0023191095738080646, + 0.0025840361579153536, + 0.0025746366972740037, + 0.0022874947808971076, + 0.0017482590981993708, + 0.001010469529718797, + 0.00015080284754578053, + -0.0007401321025391216, + -0.0015677281601321103, + -0.002241555313336647, + -0.002685049904781321, + -0.002847415468166842, + -0.002706332216634437, + -0.0022693572465456347, + -0.0015807551810501244, + -0.0007073681818317356, + 0.00025858155245919815, + 0.0012156374711696657, + 0.0020601250277244316, + 0.002698864465909288, + 0.003058968365749582, + 0.003095930462187589, + 0.002799180323785052, + 0.0021938662312239255, + 0.0013387749941891575, + 0.00032073673872905727, + -0.0007540149497279023, + -0.0017707823148707424, + -0.0026186149816637165, + -0.0032023981947833136, + -0.0034533803843191375, + -0.0033370666249195324, + -0.002857634299302074, + -0.0020583398437455315, + -0.0010178803764328268, + 0.00015714303240366978, + 0.00134312158248896, + 0.0024123674642028033, + 0.0032466971416879246, + 0.003750429521389635, + 0.0038610298842211605, + 0.0035563826341158213, + 0.0028590450497876694, + 0.0018334905123878731, + 0.0005825119501486656, + -0.0007653431253644171, + -0.002066921044075508, + -0.00318050748566926, + -0.003981099599522876, + -0.004372950526822947, + -0.004302245531766623, + -0.0037634623427922525, + -0.0028010281264617036, + -0.0015063684237781073, + -0.000009312999857163947, + 0.0015348340498211675, + 0.002960705851903475, + 0.00411025264482529, + 0.004850497409323919, + 0.00508852374186094, + 0.004782051229146126, + 0.003945677268175109, + 0.002651861057270526, + 0.001024987337059874, + -0.000770898857558077, + -0.0025480549808695363, + -0.0041134371938038455, + -0.005289473467472686, + -0.0059345294154884536, + -0.005958790652492603, + -0.0053348632172493865, + -0.004104133685973202, + -0.0023757889373754375, + -0.0003154863263286177, + 0.0018679968592255536, + 0.003942664171895544, + 0.005680643711722206, + 0.006877057686831076, + 0.007378348952494577, + 0.007096715159487296, + 0.0060250187066667035, + 0.004239992353086172, + 0.0018984740602875209, + -0.0007749973681354589, + -0.0035085807133030038, + -0.0060094188052075385, + -0.007993440148818328, + -0.009215708970605762, + -0.00949819102645701, + -0.008752165260963618, + -0.006992771039067642, + -0.004343750499151247, + -0.0010313778997952953, + 0.002632475569255834, + 0.006276994042887084, + 0.009506525921431109, + 0.011939275012512792, + 0.013247115357734634, + 0.013192709905840089, + 0.011660378948982, + 0.008677354388180201, + 0.0044226259943339384, + -0.0007776763994276021, + -0.006468598011493521, + -0.01209866234178173, + -0.01705984630504337, + -0.020740073978089375, + -0.022572656974485978, + -0.022091348052909576, + -0.018974044064189968, + -0.01307870343426492, + -0.004467461602914141, + 0.006588374127832776, + 0.019617756508396658, + 0.03397630130130575, + 0.04888871785298119, + 0.06350208150118342, + 0.07694602080488765, + 0.08839667839951919, + 0.09713857345310689, + 0.10261800054825827, + 0.10448449122868045, + 0.10261800054825827, + 0.09713857345310689, + 0.08839667839951919, + 0.07694602080488765, + 0.06350208150118342, + 0.04888871785298119, + 0.03397630130130575, + 0.019617756508396658, + 0.006588374127832776, + -0.004467461602914141, + -0.01307870343426492, + -0.018974044064189968, + -0.022091348052909576, + -0.022572656974485978, + -0.020740073978089375, + -0.01705984630504337, + -0.01209866234178173, + -0.006468598011493521, + -0.0007776763994276021, + 0.0044226259943339384, + 0.008677354388180201, + 0.011660378948982, + 0.013192709905840089, + 0.013247115357734634, + 0.011939275012512792, + 0.009506525921431109, + 0.006276994042887084, + 0.002632475569255834, + -0.0010313778997952953, + -0.004343750499151247, + -0.006992771039067642, + -0.008752165260963618, + -0.00949819102645701, + -0.009215708970605762, + -0.007993440148818328, + -0.0060094188052075385, + -0.0035085807133030038, + -0.0007749973681354589, + 0.0018984740602875209, + 0.004239992353086172, + 0.0060250187066667035, + 0.007096715159487296, + 0.007378348952494577, + 0.006877057686831076, + 0.005680643711722206, + 0.003942664171895544, + 0.0018679968592255536, + -0.0003154863263286177, + -0.0023757889373754375, + -0.004104133685973202, + -0.0053348632172493865, + -0.005958790652492603, + -0.0059345294154884536, + -0.005289473467472686, + -0.0041134371938038455, + -0.0025480549808695363, + -0.000770898857558077, + 0.001024987337059874, + 0.002651861057270526, + 0.003945677268175109, + 0.004782051229146126, + 0.00508852374186094, + 0.004850497409323919, + 0.00411025264482529, + 0.002960705851903475, + 0.0015348340498211675, + -0.000009312999857163947, + -0.0015063684237781073, + -0.0028010281264617036, + -0.0037634623427922525, + -0.004302245531766623, + -0.004372950526822947, + -0.003981099599522876, + -0.00318050748566926, + -0.002066921044075508, + -0.0007653431253644171, + 0.0005825119501486656, + 0.0018334905123878731, + 0.0028590450497876694, + 0.0035563826341158213, + 0.0038610298842211605, + 0.003750429521389635, + 0.0032466971416879246, + 0.0024123674642028033, + 0.00134312158248896, + 0.00015714303240366978, + -0.0010178803764328268, + -0.0020583398437455315, + -0.002857634299302074, + -0.0033370666249195324, + -0.0034533803843191375, + -0.0032023981947833136, + -0.0026186149816637165, + -0.0017707823148707424, + -0.0007540149497279023, + 0.00032073673872905727, + 0.0013387749941891575, + 0.0021938662312239255, + 0.002799180323785052, + 0.003095930462187589, + 0.003058968365749582, + 0.002698864465909288, + 0.0020601250277244316, + 0.0012156374711696657, + 0.00025858155245919815, + -0.0007073681818317356, + -0.0015807551810501244, + -0.0022693572465456347, + -0.002706332216634437, + -0.002847415468166842, + -0.002685049904781321, + -0.002241555313336647, + -0.0015677281601321103, + -0.0007401321025391216, + 0.00015080284754578053, + 0.001010469529718797, + 0.0017482590981993708, + 0.0022874947808971076, + 0.0025746366972740037, + 0.0025840361579153536, + 0.0023191095738080646, + 0.0018119245687932325, + 0.0011202470135660719, + 0.0003207533398910953, + -0.0005001275988053344, + -0.0012553733554405261, + -0.0018659822196923194, + -0.0022692307984843695, + -0.002425430974535461, + -0.0023218012876373207, + -0.001973351045206107, + -0.0014208542690678963, + -0.0007258491765731604, + 0.000036170113001030666, + 0.000783301048300022, + 0.001436104198970914, + 0.0019268362424314605, + 0.0022053677261764473, + 0.0022452826272775055, + 0.0020457879850275806, + 0.0016308959748307101, + 0.001047910209033917, + 0.00036046532149158213, + -0.0003566571442405154, + -0.0010269473735399154, + -0.001579698202436061, + -0.0019576726289922586, + -0.0021230164816546412, + -0.002060772342951284, + -0.0017802301804471922, + -0.0013137753201852866, + -0.0007131888239163173, + -0.00004396586555373278, + 0.0006218226272857692, + 0.0012133648938024923, + 0.001668583653638679, + 0.0019405399480025168, + 0.002002271293795752, + 0.0018494543772917837, + 0.0015005576753828536, + 0.0009946245737452108, + 0.00038702570052167175, + -0.0002564820252824784, + -0.0008669516848493666, + -0.0013797190792108685, + -0.0017410363220249359, + -0.0019137822019732152, + -0.0018816779265696613, + -0.0016485023632017621, + -0.0012439856702169039, + -0.0007071978327354896, + -0.00010287888971484906, + 0.0005087929550228872, + 0.0010620969968500548, + 0.0014959523321337515, + 0.0017665803736364497, + 0.0018474313131084748, + 0.0017300695180374488, + 0.001427583798294352, + 0.0009743368473658559, + 0.0004203972508663324, + -0.0001750373426426684, + -0.00074891505028209, + -0.001239958862296361, + -0.0015955792180418218, + -0.0017782857455891608, + -0.0017696752225057275, + -0.0015718235937901522, + -0.0012066408059786802, + -0.0007134285789517371, + -0.00014493157532811753, + 0.00043789676780500817, + 0.0009725070918705811, + 0.001401938565514821, + 0.0016808579160732414, + 0.0017799814611489096, + 0.001689241246422699, + 0.001418713685742809, + 0.0009969768021073388, + 0.00046927876230741587, + -0.00010821707107113083, + -0.0006738084260047715, + -0.0011668823340746201, + -0.0015352614436320124, + -0.0017393843977854672, + -0.0017577706864892022, + -0.0015880719715790936, + -0.0012479991812573157, + -0.0007733385753572961, + -0.00021414834948363428, + 0.00037029761935479554, + 0.0009178041117923993, + 0.0013699023886079994, + 0.00167799070404902, + 0.0018085733127357138, + 0.0017469004431845741, + 0.0014984460140958136, + 0.0010883547994854647, + 0.0005590116846630576, + -0.000034277486241399186, + -0.0006290757127071664, + -0.0011623018660948076, + -0.0015767572410517868, + -0.001827180245955061, + -0.001885184212057672, + -0.0017423327816480623, + -0.001411151838185113, + -0.0009242244673079876, + -0.00033072041687547837, + 0.00030908834306052194, + 0.0009254779772329933, + 0.001463759939291864, + 0.00184518937028339, + 0.002054368304440125, + 0.0020485384828633777, + 0.0018238726340835366, + 0.0014076463254305174, + 0.0008397052861045773, + 0.00017203581592664565, + -0.0005293364966829129, + -0.0011899674040168643, + -0.0017380160407736585, + -0.0021136819231921474, + -0.002274533536918245, + -0.0021983440926621103, + -0.0018849613428995964, + -0.0013572018806166464, + -0.0006600057708364918, + 0.0001428944628485573, + 0.0009750187684900944, + 0.0017550758096436146, + 0.002405247908616018, + 0.002858826770125135, + 0.003066845756573675, + 0.003003052919640715, + 0.0026667562654551246, + 0.0020838354221374305, + 0.0013056441129537509, + 0.00040506185948651675, + -0.0005294931107453994, + -0.001401086778987431, + -0.00211370881796881, + -0.002579780077995427, + -0.00272885000805846, + -0.0025137861299055595, + -0.0019147615431778597, + -0.0009423273945619157, + 0.00036406537300421297, + 0.0019376716631701368, + 0.003689888284206567, + 0.005516680433668245, + 0.007306289027941143, + 0.008947826461137941, + 0.010339492321850014, + 0.011395973228693276, + 0.012054670302231996, + 0.01227985703081096, + 0.012064461696610605, + 0.011429759533610568, + 0.010422968037142797, + 0.009112816853143859, + 0.007583700132903604, + 0.005929041211240287, + 0.004244279668461443, + 0.002620018618160862, + 0.0011360279713313209, + -0.00014348647168516363, + -0.0011732914244696805, + -0.0019288301578398123, + -0.0024058915455907196, + -0.002619226598223891, + -0.002599310268302821, + -0.0023884346335371165, + -0.006122920352830965 + } ); + + double[] mCoefficients; + + private Filters( double[] coefficients ) + { + mCoefficients = coefficients; + } + + public double[] getCoefficients() + { + return mCoefficients; + } +} diff --git a/src/dsp/filter/FloatFIRFilter.java b/src/dsp/filter/FloatFIRFilter.java new file mode 100644 index 00000000..cb6ee841 --- /dev/null +++ b/src/dsp/filter/FloatFIRFilter.java @@ -0,0 +1,177 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package dsp.filter; + +import java.util.ArrayList; + +import sample.Listener; + +public class FloatFIRFilter implements Listener +{ + private Listener mListener; + private ArrayList mBuffer; + private int mBufferSize = 1; //Temporary initial value + private int mBufferPointer = 0; + private double[] mCoefficients; + private int[][] mIndexMap; + private int mCenterCoefficient; + private int mCenterCoefficientMapIndex; + private double mGain; + + public FloatFIRFilter( double[] coefficients, double gain ) + { + mCoefficients = coefficients; + mBuffer = new ArrayList(); + mBufferSize = mCoefficients.length; + mGain = gain; + + //Fill the buffer with zero valued samples, so we don't have to check for null + for( int x = 0; x < mCoefficients.length; x++ ) + { + mBuffer.add( 0.0f ); + } + + generateIndexMap( mCoefficients.length ); + } + + public void dispose() + { + mListener = null; + + mBuffer.clear(); + } + + public int getTapCount() + { + return mCoefficients.length; + } + + public void setListener( Listener listener ) + { + mListener = listener; + } + + public void receive( Float newSample ) + { + send( get( newSample ) ); + } + + public float get( float newSample ) + { + //Add the new sample to the buffer + mBuffer.set( mBufferPointer, newSample ); + + //Increment & Adjust the buffer pointer for circular wrap around + mBufferPointer++; + + if( mBufferPointer >= mBufferSize ) + { + mBufferPointer = 0; + } + + //Convolution - multiply filter coefficients by the circular buffer + //samples to calculate a new filtered value + double accumulator = 0.0d; + + //Start with the center tap value + accumulator += mCoefficients[ mCenterCoefficient ] * + mBuffer.get( mIndexMap[ mBufferPointer ][ mCenterCoefficientMapIndex ] ); + + //For the remaining coefficients, add the symmetric samples, oldest and newest + //first, then multiply by the single coefficient + for( int x = 0; x < mCenterCoefficient; x++ ) + { + accumulator += mCoefficients[ x ] * + ( mBuffer.get( mIndexMap[ mBufferPointer ][ x ] ) + + mBuffer.get( mIndexMap[ mBufferPointer ][ x + mCenterCoefficient ] ) ); + } + + //We're almost finished ... apply gain, cast the doubles to shorts and + //return the value + return (float)( accumulator * mGain ); + } + + /** + * Dispatch the filtered sample to all registered listeners + */ + private void send( float sample ) + { + if( mListener != null ) + { + mListener.receive( sample ); + } + } + + public String printIndexMap() + { + StringBuilder sb = new StringBuilder(); + + for( int x = 0; x < mCoefficients.length; x++ ) + { + for( int y = 0; y < mCoefficients.length; y++ ) + { + sb.append( mIndexMap[ x ][ y ] + " " ); + } + sb.append( "\n" ); + } + + return sb.toString(); + } + + /** + * @param odd-sized number of filter taps (ie coefficients) and buffer + */ + private void generateIndexMap( int size ) + { + //Ensure we have an odd size + assert( size % 2 == 1 ); + + mIndexMap = new int[ size ][ size ]; + + //Last column will be the center coefficient index value for each row + mCenterCoefficientMapIndex = size - 1; + mCenterCoefficient = (int)( size / 2 ); + + //Setup the first row. Offset is the first row's center coefficient value + //and will become the index value we place in the last column + mIndexMap[ 0 ][ mCenterCoefficient ] = mCenterCoefficient; + + //Indexes 0 to 1/2 are their index value, and indexes 1/2 to end are + //an offset of the first half of the values + for( int x = 0; x < mCenterCoefficient; x++ ) + { + mIndexMap[ 0 ][ x ] = x; + mIndexMap[ 0 ][ x + mCenterCoefficient ] = size - 1 - x; + } + + //For each subsequent map row, increment the value of the preceding row + //same column by 1, wrapping to zero when we exceed the size value + for( int x = 1; x < size; x++ ) + { + for( int y = 0; y < size; y++ ) + { + mIndexMap[ x ][ y ] = mIndexMap[ x - 1 ][ y ] + 1; + + if( mIndexMap[ x ][ y ] >= size ) + { + mIndexMap[ x ][ y ] = 0; + } + } + } + } +} diff --git a/src/dsp/filter/FloatHalfBandFilter.java b/src/dsp/filter/FloatHalfBandFilter.java new file mode 100644 index 00000000..b832c35a --- /dev/null +++ b/src/dsp/filter/FloatHalfBandFilter.java @@ -0,0 +1,203 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package dsp.filter; + +import java.util.ArrayList; + +import sample.Listener; + +public class FloatHalfBandFilter implements Listener +{ + private Listener mListener; + private ArrayList mBuffer; + private int mBufferSize = 1; //Temporary initial value + private int mBufferPointer = 0; + private double mGain; + private double[] mCoefficients; + private int[][] mIndexMap; + private int mCenterCoefficient; + private int mCenterCoefficientMapIndex; + private boolean mDispatchFlag = false; + + + /** + * Half-Band filter with decimation by 2 against real valued floats. + * + * Takes advantage of the symmetrical nature of FIR filter coefficients by + * adding oldest and newest sample first, then multiplying once by the + * corresponding coefficient + * + * Also, takes advantage of the 0-valued FIR half-band coefficents inherent + * in the half-band filter, and does not calculate those coefficients. + * + * This reduces the workload to (tap-size - 1) / 4 + 1 calculations per sample. + * + * @param filter - filter coefficients + * @param gain - gain multiplier. Use 1.0 for unity/no gain + */ + public FloatHalfBandFilter( Filters filter, double gain ) + { + mCoefficients = filter.getCoefficients(); + mBuffer = new ArrayList(); + mBufferSize = mCoefficients.length; + + //Fill the buffer with zero valued samples + for( int x = 0; x < mCoefficients.length; x++ ) + { + mBuffer.add( 0.0f ); + } + + generateIndexMap( mCoefficients.length ); + mGain = gain; + } + + public void dispose() + { + mListener = null; + } + + /** + * Calculate the filtered value by applying the coefficients against + * the complex samples in mBuffer + */ + public void receive( Float newSample ) + { + //Add the new sample to the buffer + mBuffer.set( mBufferPointer, newSample ); + + //Increment & Adjust the buffer pointer for circular wrap around + mBufferPointer++; + + if( mBufferPointer >= mBufferSize ) + { + mBufferPointer = 0; + } + + //Toggle the flag every time, so that we can calculate and dispatch a + //sample every other time, when the flag is true (ie decimate by 2) + mDispatchFlag = !mDispatchFlag; + + //Calculate a filtered sample when the flag is true + if( mDispatchFlag ) + { + //Convolution - multiply filter coefficients by the circular buffer + //samples to calculate a new filtered value + double accumulator = 0; + + //Start with the center tap value + accumulator += mCoefficients[ mCenterCoefficient ] * + mBuffer.get( mIndexMap[ mBufferPointer ][ mCenterCoefficientMapIndex ] ); + + //For the remaining coefficients, add the symmetric samples, oldest and newest + //first, then multiply by the single coefficient + for( int x = 0; x < mCenterCoefficientMapIndex; x += 2 ) + { + accumulator += mCoefficients[ x ] * + ( mBuffer.get( mIndexMap[ mBufferPointer ][ x ] ) + + mBuffer.get( mIndexMap[ mBufferPointer ][ x + 1 ] ) ); + } + + //We're almost finished ... apply gain, cast the doubles to shorts and + //send it on it's merry way + if( mListener != null ) + { + mListener.receive( (float)( accumulator * mGain ) ); + } + } + } + + /** + * Creates an n X (n + 1 / 2) index map enabling quick access to the + * circular buffer samples. + * + * As the buffer shifts right with each subsequent sample, we have to move + * the index pointers with it, for efficient access of the samples. + * + * The first array index value in the index map corresponds to the current + * buffer pointer location. + * + * The second array index value points to the samples that should be + * multiplied by the coefficients as follows: + * + * 0 = center tap sample, to be multiplied by center coefficient + * + * 0 = sample( 1 ) + * 1 = sample( size - 1 ) + * + * Indexes 0 and 1 will be multiplied by coefficient( 0 ). + * + * Subsequent indexes 3, 4, etc, point to the oldest and newest samples that + * correspond to the matching ( 3 ) coefficient index. + * + * @param odd-sized number of filter taps (ie coefficients) and buffer + */ + private void generateIndexMap( int size ) + { + //Ensure we have an odd size + assert( size % 2 == 1 ); + + int mapWidth = ( ( size + 1 ) / 2 ) + 1; + + //Set center tap index for coefficients array + mCenterCoefficient = ( size - 1 ) / 2; + mCenterCoefficientMapIndex = mCenterCoefficient + 1; + + mIndexMap = new int[ size ][ mapWidth ]; + + //Setup the first row, buffer pointer index 0, as a starting point + for( int x = 0; x < mapWidth - 2; x += 2 ) + { + mIndexMap[ 0 ][ x ] = x; + mIndexMap[ 0 ][ x + 1 ] = size - 1 - x; + } + + //Place center tap index in last element + mIndexMap[ 0 ][ mCenterCoefficientMapIndex ] = mCenterCoefficient; + + //For each subsequent row, increment the previous row's value by 1, + //subtracting size as needed, to keep the values between 0 and size - 1 + for( int x = 1; x < size; x++ ) + { + for( int y = 0; y < mapWidth; y++ ) + { + mIndexMap[ x ][ y ] = mIndexMap[ x - 1 ][ y ] + 1; + + if( mIndexMap[ x ][ y ] >= size ) + { + mIndexMap[ x ][ y ] -= size; + } + } + } + } + + /** + * Registers a listener for filtered samples + */ + public void setListener( Listener listener ) + { + mListener = listener; + } + + /** + * Removes (if exists) a registered filtered sample listener + */ + public void clearListener() + { + mListener = null; + } +} diff --git a/src/dsp/filter/FloatHalfBandNoDecimateFilter.java b/src/dsp/filter/FloatHalfBandNoDecimateFilter.java new file mode 100644 index 00000000..9529bfd8 --- /dev/null +++ b/src/dsp/filter/FloatHalfBandNoDecimateFilter.java @@ -0,0 +1,193 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package dsp.filter; + +import java.util.ArrayList; + +import sample.Listener; + +public class FloatHalfBandNoDecimateFilter implements Listener +{ + private Listener mListener; + private ArrayList mBuffer; + private int mBufferSize = 1; //Temporary initial value + private int mBufferPointer = 0; + private double mGain; + private double[] mCoefficients; + private int[][] mIndexMap; + private int mCenterCoefficient; + private int mCenterCoefficientMapIndex; + + /** + * Half-Band filter with decimation by a real valued float. + * + * Takes advantage of the symmetrical nature of FIR filter coefficients by + * adding oldest and newest sample first, then multiplying once by the + * corresponding coefficient + * + * Also, takes advantage of the 0-valued FIR half-band coefficents inherent + * in the half-band filter, and does not calculate those coefficients. + * + * This reduces the workload to (tap-size - 1) / 4 + 1 calculations per sample. + * + * @param filter - filter coefficients + * @param gain - gain multiplier. Use 1.0 for unity/no gain + */ + public FloatHalfBandNoDecimateFilter( Filters filter, double gain ) + { + mCoefficients = filter.getCoefficients(); + mBuffer = new ArrayList(); + mBufferSize = mCoefficients.length; + + //Fill the buffer with zero valued samples + for( int x = 0; x < mCoefficients.length; x++ ) + { + mBuffer.add( 0.0f ); + } + + generateIndexMap( mCoefficients.length ); + mGain = gain; + } + + public void dispose() + { + mListener = null; + } + + /** + * Calculate the filtered value by applying the coefficients against + * the complex samples in mBuffer + */ + public void receive( Float newSample ) + { + //Add the new sample to the buffer + mBuffer.set( mBufferPointer, newSample ); + + //Increment & Adjust the buffer pointer for circular wrap around + mBufferPointer++; + + if( mBufferPointer >= mBufferSize ) + { + mBufferPointer = 0; + } + + //Convolution - multiply filter coefficients by the circular buffer + //samples to calculate a new filtered value + double accumulator = 0; + + //Start with the center tap value + accumulator += mCoefficients[ mCenterCoefficient ] * + mBuffer.get( mIndexMap[ mBufferPointer ][ mCenterCoefficientMapIndex ] ); + + //For the remaining coefficients, add the symmetric samples, oldest and newest + //first, then multiply by the single coefficient + for( int x = 0; x < mCenterCoefficientMapIndex; x += 2 ) + { + accumulator += mCoefficients[ x ] * + ( mBuffer.get( mIndexMap[ mBufferPointer ][ x ] ) + + mBuffer.get( mIndexMap[ mBufferPointer ][ x + 1 ] ) ); + } + + //We're almost finished ... apply gain, cast the doubles to floats and + //send it on it's merry way + if( mListener != null ) + { + mListener.receive( (float)( accumulator * mGain ) ); + } + } + + /** + * Creates an n X (n + 1 / 2) index map enabling quick access to the + * circular buffer samples. + * + * As the buffer shifts right with each subsequent sample, we have to move + * the index pointers with it, for efficient access of the samples. + * + * The first array index value in the index map corresponds to the current + * buffer pointer location. + * + * The second array index value points to the samples that should be + * multiplied by the coefficients as follows: + * + * 0 = center tap sample, to be multiplied by center coefficient + * + * 0 = sample( 1 ) + * 1 = sample( size - 1 ) + * + * Indexes 0 and 1 will be multiplied by coefficient( 0 ). + * + * Subsequent indexes 3, 4, etc, point to the oldest and newest samples that + * correspond to the matching ( 3 ) coefficient index. + * + * @param odd-sized number of filter taps (ie coefficients) and buffer + */ + private void generateIndexMap( int size ) + { + //Ensure we have an odd size + assert( size % 2 == 1 ); + + int mapWidth = ( ( size + 1 ) / 2 ) + 1; + + //Set center tap index for coefficients array + mCenterCoefficient = ( size - 1 ) / 2; + mCenterCoefficientMapIndex = mCenterCoefficient + 1; + + mIndexMap = new int[ size ][ mapWidth ]; + + //Setup the first row, buffer pointer index 0, as a starting point + for( int x = 0; x < mapWidth - 2; x += 2 ) + { + mIndexMap[ 0 ][ x ] = x; + mIndexMap[ 0 ][ x + 1 ] = size - 1 - x; + } + + //Place center tap index in last element + mIndexMap[ 0 ][ mCenterCoefficientMapIndex ] = mCenterCoefficient; + + //For each subsequent row, increment the previous row's value by 1, + //subtracting size as needed, to keep the values between 0 and size - 1 + for( int x = 1; x < size; x++ ) + { + for( int y = 0; y < mapWidth; y++ ) + { + mIndexMap[ x ][ y ] = mIndexMap[ x - 1 ][ y ] + 1; + + if( mIndexMap[ x ][ y ] >= size ) + { + mIndexMap[ x ][ y ] -= size; + } + } + } + } + + /** + * Registers a listener for filtered samples + */ + public void setListener( Listener listener ) + { + mListener = listener; + } + + /** + * Removes (if exists) a registered filtered sample listener + */ + public void clearListener() + { + mListener = null; + } +} diff --git a/src/dsp/filter/Gain.java b/src/dsp/filter/Gain.java new file mode 100644 index 00000000..e73c17eb --- /dev/null +++ b/src/dsp/filter/Gain.java @@ -0,0 +1,45 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package dsp.filter; + +import sample.Listener; + +public class Gain implements Listener +{ + private double mGain; + private Listener mListener; + + public Gain( double gain ) + { + mGain = gain; + } + + @Override + public void receive( Float sample ) + { + if( mListener != null ) + { + mListener.receive( (float)( sample * mGain ) ); + } + } + + public void setListener( Listener listener ) + { + mListener = listener; + } +} diff --git a/src/dsp/filter/GoertzelFilter.java b/src/dsp/filter/GoertzelFilter.java new file mode 100644 index 00000000..9747425d --- /dev/null +++ b/src/dsp/filter/GoertzelFilter.java @@ -0,0 +1,161 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package dsp.filter; + +import dsp.filter.Window.WindowType; + + +/** + * Implements the Goertzel Filter as described in + * http://en.wikipedia.org/wiki/Goertzel_algorithm + * + * Calculates the magnitude of a specific frequency component within a window of + * samples, using a Goertzel Filter (ie an optimized DFT). + */ +public class GoertzelFilter +{ + private int mSampleRate; + private int mTargetFrequency; + private int mBlockSize; + private double[] mWindowCoefficients; + + private double mCoefficient; + + /** + * Constructs the Goertzel Filter to use for block processing of + * time-domain signal samples. + * + * @param sampleRate + * - sample rate (Hz) + * @param targetFrequency + * - target frequency (Hz) + * @param blockSize + * - number of samples per block of processing + * @param window + * - window to apply against the samples prior to calculating + * the magnitude + */ + public GoertzelFilter( int sampleRate, + int targetFrequency, + int blockSize, + WindowType window ) + { + mSampleRate = sampleRate; + mTargetFrequency = targetFrequency; + mBlockSize = blockSize; + mWindowCoefficients = Window.getWindow( window, blockSize ); + init(); + } + + /** + * Establish the pre-calculated values to use in the filter + */ + private void init() + { + double normalizedFrequency = (double) mTargetFrequency / mSampleRate; + mCoefficient = 2.0D * Math.cos( 2 * Math.PI * normalizedFrequency ); + } + + + /** + * Returns the power (dB) of the target frequency within the block of + * samples as normalized against the bin(0) magnitude value + * + * @param samples + * - array of time-domain samples -- array size must be the same + * size as the windowSize parameter passed upon construction + * + * @return - power measurement in dB + + * @throws IllegalArgumentException if the sample array size is not equal + * to the defined block size + */ + public int getPower( float[] samples ) throws IllegalArgumentException + { + // Verify size of samples array against block size + if ( samples.length != mBlockSize ) + { + throw new IllegalArgumentException( + "Sample array size does not equal Block Size" ); + } + + // Apply the window against the samples + samples = Window.apply( mWindowCoefficients, samples ); + + // Process the samples + double s = 0.0D; + double s_prev = 0.0D; + double s_prev2 = 0.0D; + + for ( int x = 0; x < samples.length; x++ ) + { + s = samples[x] + ( mCoefficient * s_prev ) - s_prev2; + s_prev2 = s_prev; + s_prev = s; + } + + //power = s_prev2 * s_prev2 + s_prev * s_prev - coeff * s_prev * s_prev2 ; + + double magnitude = ( s_prev2 * s_prev2 ) + ( s_prev * s_prev) - (mCoefficient * s_prev * s_prev2); + int binZero = getBinZeroPower( samples ); + + int power = (int) (20 * Math.log10( magnitude / binZero ) ); + + return power; + } + + /** + * Sums all of the sample values to derive the bin 0 power + */ + private int getBinZeroPower( float[] samples ) + { + int retVal = 0; + + for( int x = 0; x < samples.length; x++ ) + { + retVal += samples[ x ]; + } + + return retVal; + } + + /** + * @return the Sample Rate + */ + public int getSampleRate() + { + return mSampleRate; + } + + /** + * @return the Target Frequency + */ + public int getTargetFrequency() + { + return mTargetFrequency; + } + + /** + * @return the Block Size + */ + public int getBlockSize() + { + return mBlockSize; + } + +} diff --git a/src/dsp/filter/IIRBiQuadraticFilter.java b/src/dsp/filter/IIRBiQuadraticFilter.java new file mode 100644 index 00000000..7e17042b --- /dev/null +++ b/src/dsp/filter/IIRBiQuadraticFilter.java @@ -0,0 +1,197 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package dsp.filter; + +/*************************************************************************** + * Copyright (C) 2011 by Paul Lutus * + * lutusp@arachnoid.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; either version 2 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 for more details. * + * * + * You should have received a copy of the GNU General Public License * + * along with this program; if not, write to the * + * Free Software Foundation, Inc., * + * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. * + ***************************************************************************/ +// http://en.wikipedia.org/wiki/Digital_biquad_filter + +final public class IIRBiQuadraticFilter { + + public enum Type { + + BANDPASS, LOWPASS, HIGHPASS, NOTCH, PEAK, LOWSHELF, HIGHSHELF + }; + double a0, a1, a2, b0, b1, b2; + double x1, x2, y, y1, y2; + double gain_abs; + Type type; + double center_freq, sample_rate, Q, gainDB; + + public IIRBiQuadraticFilter() { + } + + public IIRBiQuadraticFilter(Type type, double center_freq, double sample_rate, double Q, double gainDB) { + configure(type, center_freq, sample_rate, Q, gainDB); + } + + // constructor without gain setting + public IIRBiQuadraticFilter(Type type, double center_freq, double sample_rate, double Q) { + configure(type, center_freq, sample_rate, Q, 0); + } + + public void reset() { + x1 = x2 = y1 = y2 = 0; + } + + public double frequency() { + return center_freq; + } + + public void configure(Type type, double center_freq, double sample_rate, double Q, double gainDB) { + reset(); + Q = (Q == 0) ? 1e-9 : Q; + this.type = type; + this.sample_rate = sample_rate; + this.Q = Q; + this.gainDB = gainDB; + reconfigure(center_freq); + } + + public void configure(Type type, double center_freq, double sample_rate, double Q) { + configure(type, center_freq, sample_rate, Q, 0); + } + + // allow parameter change while running + public void reconfigure(double cf) { + center_freq = cf; + // only used for peaking and shelving filters + gain_abs = Math.pow(10, gainDB / 40); + double omega = 2 * Math.PI * cf / sample_rate; + double sn = Math.sin(omega); + double cs = Math.cos(omega); + double alpha = sn / (2 * Q); + double beta = Math.sqrt(gain_abs + gain_abs); + switch (type) { + case BANDPASS: + b0 = alpha; + b1 = 0; + b2 = -alpha; + a0 = 1 + alpha; + a1 = -2 * cs; + a2 = 1 - alpha; + break; + case LOWPASS: + b0 = (1 - cs) / 2; + b1 = 1 - cs; + b2 = (1 - cs) / 2; + a0 = 1 + alpha; + a1 = -2 * cs; + a2 = 1 - alpha; + break; + case HIGHPASS: + b0 = (1 + cs) / 2; + b1 = -(1 + cs); + b2 = (1 + cs) / 2; + a0 = 1 + alpha; + a1 = -2 * cs; + a2 = 1 - alpha; + break; + case NOTCH: + b0 = 1; + b1 = -2 * cs; + b2 = 1; + a0 = 1 + alpha; + a1 = -2 * cs; + a2 = 1 - alpha; + break; + case PEAK: + b0 = 1 + (alpha * gain_abs); + b1 = -2 * cs; + b2 = 1 - (alpha * gain_abs); + a0 = 1 + (alpha / gain_abs); + a1 = -2 * cs; + a2 = 1 - (alpha / gain_abs); + break; + case LOWSHELF: + b0 = gain_abs * ((gain_abs + 1) - (gain_abs - 1) * cs + beta * sn); + b1 = 2 * gain_abs * ((gain_abs - 1) - (gain_abs + 1) * cs); + b2 = gain_abs * ((gain_abs + 1) - (gain_abs - 1) * cs - beta * sn); + a0 = (gain_abs + 1) + (gain_abs - 1) * cs + beta * sn; + a1 = -2 * ((gain_abs - 1) + (gain_abs + 1) * cs); + a2 = (gain_abs + 1) + (gain_abs - 1) * cs - beta * sn; + break; + case HIGHSHELF: + b0 = gain_abs * ((gain_abs + 1) + (gain_abs - 1) * cs + beta * sn); + b1 = -2 * gain_abs * ((gain_abs - 1) + (gain_abs + 1) * cs); + b2 = gain_abs * ((gain_abs + 1) + (gain_abs - 1) * cs - beta * sn); + a0 = (gain_abs + 1) - (gain_abs - 1) * cs + beta * sn; + a1 = 2 * ((gain_abs - 1) - (gain_abs + 1) * cs); + a2 = (gain_abs + 1) - (gain_abs - 1) * cs - beta * sn; + break; + } + // prescale flter constants + b0 /= a0; + b1 /= a0; + b2 /= a0; + a1 /= a0; + a2 /= a0; + } + + // provide a static amplitude result for testing + public double result(double f) { + double phi = Math.pow((Math.sin(2.0 * Math.PI * f / (2.0 * sample_rate))), 2.0); + return (Math.pow(b0 + b1 + b2, 2.0) - 4.0 * (b0 * b1 + 4.0 * b0 * b2 + b1 * b2) * phi + 16.0 * b0 * b2 * phi * phi) / (Math.pow(1.0 + a1 + a2, 2.0) - 4.0 * (a1 + 4.0 * a2 + a1 * a2) * phi + 16.0 * a2 * phi * phi); + } + + // provide a static decibel result for testing + public double log_result(double f) { + double r; + try { + r = 10 * Math.log10(result(f)); + } catch (Exception e) { + r = -100; + } + if(Double.isInfinite(r) || Double.isNaN(r)) { + r = -100; + } + return r; + } + + // return the constant set for this filter + public double[] constants() { + return new double[]{b0, b1, b2, a1, a2}; + } + + // perform one filtering step + public double filter(double x) { + y = b0 * x + b1 * x1 + b2 * x2 - a1 * y1 - a2 * y2; + x2 = x1; + x1 = x; + y2 = y1; + y1 = y; + return (y); + } +} diff --git a/src/dsp/filter/SquaringFilter.java b/src/dsp/filter/SquaringFilter.java new file mode 100644 index 00000000..b169c45d --- /dev/null +++ b/src/dsp/filter/SquaringFilter.java @@ -0,0 +1,51 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package dsp.filter; + +import sample.Listener; + +public class SquaringFilter implements Listener +{ + private Listener mListener; + + /** + * Transforms a short sample waveform into a square wave + */ + public SquaringFilter() + { + } + + @Override + public void receive( Float sample ) + { + if( mListener != null ) + { + mListener.receive( ( sample >= 0 ? true : false ) ); + } + } + + public void setListener( Listener listener ) + { + mListener = listener; + } + + public void clearListener( Listener listener ) + { + mListener = null; + } +} diff --git a/src/dsp/filter/Window.java b/src/dsp/filter/Window.java new file mode 100644 index 00000000..eaf1588c --- /dev/null +++ b/src/dsp/filter/Window.java @@ -0,0 +1,239 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package dsp.filter; + +import java.util.Arrays; + +/** + * Window creator + * + * Blackman, Hamming and Hanning windows + * based on windowing methods described at: + * http://www.mstarlabs.com/dsp/goertzel/goertzel.html + * and + * Understanding Digital Signal Processing 3e, Lyons, p.91 + * and + * Digital Filters for Everyone, Allred, p. 132 + * + */ +public class Window +{ + public static double[] getWindow( WindowType type, int length ) + { + switch( type ) + { + case BLACKMAN: + return getBlackmanWindow( length ); + case COSINE: + return getCosineWindow( length ); + case HAMMING: + return getHammingWindow( length ); + case HANNING: + return getHanningWindow( length ); + case NONE: + default: + return getRectangularWindow( length ); + } + } + + public static double[] getRectangularWindow( int length ) + { + double[] coefficients = new double[ length ]; + + Arrays.fill( coefficients, 1.0d ); + + return coefficients; + } + + public static double[] getCosineWindow( int length ) + { + double[] coefficients = new double[ length ]; + + + if( length % 2 == 0 ) //Even length + { + int half = (int)( ( length - 1 ) / 2 ); + + for( int x = -half; x < length / 2 + 1; x++ ) + { + coefficients[ x + half ] = Math.cos( + ( (double)x * Math.PI ) / ( (double)length + 1.0d ) ); + } + } + else //Odd length + { + int half = (int) length / 2; + + for( int x = -half; x < half + 1; x++ ) + { + coefficients[ x + half ] = Math.cos( + ( (double)x * Math.PI ) / ( (double)length + 1.0d ) ); + } + } + + return coefficients; + } + + public static double[] getBlackmanWindow( int length ) + { + double[] coefficients = new double[ length ]; + + for( int x = 0; x < length; x++ ) + { + coefficients[ x ] = .426591D - + ( .496561D * Math.cos( ( Math.PI * 2.0D * (double)x ) / (double)( length - 1 ) ) + + ( .076848D * Math.cos( ( Math.PI * 4.0D * (double)x ) / (double)( length - 1 ) ) ) ); + } + + return coefficients; + } + + public static double[] getHammingWindow( int length ) + { + double[] coefficients = new double[ length ]; + + if( length % 2 == 0 ) //Even length + { + for( int x = 0; x < length; x++ ) + { + coefficients[ x ] = .54D - ( .46D * + Math.cos( ( Math.PI * ( 2.0D * (double)x + 1.0d ) ) / (double)( length - 1 ) ) ); + } + } + else //Odd length + { + for( int x = 0; x < length; x++ ) + { + coefficients[ x ] = .54D - ( .46D * + Math.cos( ( 2.0D * Math.PI * (double)x ) / (double)( length - 1 ) ) ); + } + } + + return coefficients; + } + + public static double[] getHanningWindow( int length ) + { + double[] coefficients = new double[ length ]; + + if( length % 2 == 0 ) //Even length + { + for( int x = 0; x < length; x++ ) + { + coefficients[ x ] = .5D - ( .5D * + Math.cos( ( Math.PI * ( 2.0D * (double)x + 1 ) ) / (double)( length - 1 ) ) ); + } + } + else //Odd length + { + for( int x = 0; x < length; x++ ) + { + coefficients[ x ] = .5D - ( .5D * + Math.cos( ( 2.0D * Math.PI * (double)x ) / (double)( length - 1 ) ) ); + } + } + + + return coefficients; + } + + /** + * Apply the window against an array of float-type samples + */ + public static float[] apply( double[] coefficients, float[] samples ) + { + for( int x = 0; x < samples.length; x++ ) + { + samples[ x ] = (float)( samples[ x ] * coefficients[ x ] ); + } + + return samples; + } + + public static float[] apply( WindowType type, float[] samples ) + { + double[] coefficients = getWindow( type, samples.length ); + + return apply( coefficients, samples ); + } + + /** + * Apply the window against an array of float-type samples + */ + public static double[] apply( double[] coefficients, double[] samples ) + { + for( int x = 0; x < samples.length; x++ ) + { + samples[ x ] = samples[ x ] * coefficients[ x ]; + } + + return samples; + } + + public static double[] apply( WindowType type, double[] samples ) + { + double[] coefficients = getWindow( type, samples.length ); + + return apply( coefficients, samples ); + } + + + + /** + * Window types + */ + public enum WindowType + { + BLACKMAN, COSINE, HAMMING, HANNING, NONE; + } + + /** + * Utility to log the double arrays + */ + public static String arrayToString( double[] array, boolean breaks ) + { + StringBuilder sb = new StringBuilder(); + for( int x = 0; x < array.length; x++ ) + { + sb.append( x + ":" + array[ x ] ); + + if( breaks ) + { + sb.append( "\n" ); + } + else + { + if( x % 8 == 7 ) + { + sb.append( "\n" ); + } + else + { + sb.append( "\t" ); + } + } + } + + return sb.toString(); + } + + public static void main( String[] args ) + { + System.out.println( arrayToString( getBlackmanWindow( 31 ), false ) ); + } +} diff --git a/src/dsp/fsk/FSK2Decoder.java b/src/dsp/fsk/FSK2Decoder.java new file mode 100644 index 00000000..5e36dbf9 --- /dev/null +++ b/src/dsp/fsk/FSK2Decoder.java @@ -0,0 +1,287 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package dsp.fsk; + +import instrument.Instrumentable; +import instrument.tap.Tap; +import instrument.tap.stream.SymbolEventTap; + +import java.util.ArrayList; +import java.util.BitSet; +import java.util.List; + +import sample.Listener; +import buffer.AveragingCircularBooleanBuffer; +import dsp.fsk.SymbolEvent.Shift; + +/** + * Binary Frequency Shift Keying (FSK) decoder. Implements a BFSK correlation + * decoder. Provides normal or inverted decoded output. Requires the symbol + * rate to be an integer multiple of the sample rate. + * + * Implements a correlation decoder that correlates a modulated FSK signal upon + * against a one-baud delayed version of itself, converting all float samples to + * binary (0 or 1), and uses XOR to produce the correlation value. Low pass + * filtering smoothes the correlated output. + * + * Automatically aligns to symbol timing during symbol transitions (0 to 1, or + * 1 to 0) by inspecting the samples at the symbol edges and advancing or + * retarding symbol window to maintain continuous symbol alignment. + * + * Use a DC-removal filter prior to this decoder to ensure samples don't have + * a DC component. + * + * Implements instrumentable interface, so that slice events can be received + * externally to analyze decoder performance. + */ +public class FSK2Decoder implements Instrumentable, Listener +{ + public enum Output{ NORMAL, INVERTED }; + + /* Instrumentation taps */ + private static final String INSTRUMENT_DECISION = + "Tap Point: FSK2 Symbol Decision"; + private ArrayList mAvailableTaps; + private ArrayList mTaps = new ArrayList(); + + private Listener mListener; + private AveragingCircularBooleanBuffer mDelayBuffer; + private AveragingCircularBooleanBuffer mLowPassFilter; + private Slicer mSlicer; + private boolean mNormalOutput; + private int mSamplesPerSymbol; + private int mSymbolRate; + + public FSK2Decoder( int sampleRate, int symbolRate, Output output ) + { + /* Ensure we're using an integral symbolRate of the sampleRate */ + assert( sampleRate % symbolRate == 0 ); + + mSamplesPerSymbol = (int)( sampleRate / symbolRate ); + mNormalOutput = ( output == Output.NORMAL ); + mSymbolRate = symbolRate; + + mDelayBuffer = new AveragingCircularBooleanBuffer( mSamplesPerSymbol ); + mLowPassFilter = new AveragingCircularBooleanBuffer( mSamplesPerSymbol ); + mSlicer = new Slicer( mSamplesPerSymbol ); + } + + /** + * Disposes of all references to prepare for garbage collection + */ + public void dispose() + { + mListener = null; + } + + /** + * Primary sample input + */ + @Override + public void receive( Float floatSample ) + { + /* Square the sample. Greater than zero is a 1 (true) and less than + * zero is a 0 (false) */ + boolean bitSample = ( floatSample >= 0.0f ); + + /* Feed the delay buffer and fetch the one-baud delayed sample */ + boolean delayedBitSample = mDelayBuffer.get( bitSample ); + + /* Correlation: xor current bit with delayed bit */ + boolean softBit = bitSample ^ delayedBitSample; + + /* Low pass filter to smooth the correlated values */ + boolean filteredSoftBit = mLowPassFilter.getAverage( softBit ); + + /* Send the filtered correlated bit to the slicer */ + mSlicer.receive( filteredSoftBit ); + } + + /** + * Registers a listener to receive the decoded FSK bits + */ + public void setListener( Listener listener ) + { + mListener = listener; + } + + /** + * Removes the listener + */ + public void removeListener( Listener listener ) + { + mListener = null; + } + + /** + * Symbol slicer with auto-aligning baud timing + */ + public class Slicer + { + private BitSet mBitSet = new BitSet(); + private int mSymbolLength; + private int mDecisionThreshold; + private int mSampleCounter; + + public Slicer( int samplesPerSymbol ) + { + mSymbolLength = samplesPerSymbol; + + mDecisionThreshold = (int)( mSymbolLength / 2 ); + + /* Adjust for an odd number of samples per baud */ + if( mSymbolLength % 2 == 1 ) + { + mDecisionThreshold++; + } + } + + public void receive( boolean softBit ) + { + if( mSampleCounter >= 0 ) + { + if( softBit ) + { + mBitSet.set( mSampleCounter ); + } + else + { + mBitSet.clear( mSampleCounter ); + } + } + + mSampleCounter++; + + if( mSampleCounter >= mSymbolLength ) + { + boolean decision = mBitSet.cardinality() >= mDecisionThreshold; + + send( decision ); + + /* Shift timing left if the left bit in the bitset is opposite + * the decision and the right bit is the same */ + if( ( mBitSet.get( 0 ) ^ decision ) && + ( !( mBitSet.get( mSymbolLength - 1 ) ^ decision ) ) ) + { + sendTapEvent( mBitSet, Shift.LEFT, decision ); + + reset(); + + mSampleCounter--; + } + /* Shift timing right if the left bit is the same as the + * decision and the right bit is opposite */ + else if( ( !( mBitSet.get( 0 ) ^ decision ) ) && + ( mBitSet.get( mSymbolLength - 1 ) ^ decision ) ) + { + sendTapEvent( mBitSet, Shift.RIGHT, decision ); + + /* Last bit from previous symbol to pre-fill next symbol */ + boolean previousSoftBit = mBitSet.get( mSymbolLength - 1 ); + + reset(); + + if( previousSoftBit ) + { + mBitSet.set( 0 ); + } + + mSampleCounter++; + } + /* No shift */ + else + { + sendTapEvent( mBitSet, Shift.NONE, decision ); + + reset(); + } + } + } + + /** + * Sends the bit decision to the listener + */ + private void send( boolean decision ) + { + if( mListener != null ) + { + mListener.receive( mNormalOutput ? decision : !decision ); + } + } + + private void reset() + { + mBitSet.clear(); + mSampleCounter = 0; + } + + /** + * Sends instrumentation tap event to all registered listeners + */ + private void sendTapEvent( BitSet bitset, Shift shift, boolean decision ) + { + for( SymbolEventTap tap: mTaps ) + { + SymbolEvent event = + new SymbolEvent( bitset.get( 0, mSymbolLength ), + mSymbolLength, + decision, + shift ); + + tap.receive( event ); + } + } + } + + /** + * Get instrumentation taps + */ + @Override + public List getTaps() + { + if( mAvailableTaps == null ) + { + mAvailableTaps = new ArrayList(); + mAvailableTaps.add( + new SymbolEventTap( INSTRUMENT_DECISION, 0, .025f ) ); + } + + return mAvailableTaps; + } + + /** + * Add instrumentation tap + */ + @Override + public void addTap( Tap tap ) + { + if( tap instanceof SymbolEventTap && !mTaps.contains( tap ) ) + { + mTaps.add( (SymbolEventTap)tap ); + } + } + + /** + * Remove instrumentation tap + */ + @Override + public void removeTap( Tap tap ) + { + mTaps.remove( tap ); + } +} diff --git a/src/dsp/fsk/SymbolEvent.java b/src/dsp/fsk/SymbolEvent.java new file mode 100644 index 00000000..6a497adb --- /dev/null +++ b/src/dsp/fsk/SymbolEvent.java @@ -0,0 +1,75 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package dsp.fsk; + +import java.util.BitSet; + +public class SymbolEvent +{ + private BitSet mBitset; + private int mSamplesPerSymbol; + private boolean mDecision; + private Shift mShift; + + public SymbolEvent( BitSet bitset, int samplesPerSymbol, boolean decision, Shift shift ) + { + mBitset = bitset; + mSamplesPerSymbol = samplesPerSymbol; + mDecision = decision; + mShift = shift; + } + + public BitSet getBitSet() + { + return mBitset; + } + + public int getSamplesPerSymbol() + { + return mSamplesPerSymbol; + } + + public boolean getDecision() + { + return mDecision; + } + + public Shift getShift() + { + return mShift; + } + + public enum Shift + { + LEFT( "<" ), + RIGHT( ">" ), + NONE( "=" ); + + private String mLabel; + + private Shift( String label ) + { + mLabel = label; + } + + public String getLabel() + { + return mLabel; + } + } +} diff --git a/src/dsp/nbfm/FMDiscriminator.java b/src/dsp/nbfm/FMDiscriminator.java new file mode 100644 index 00000000..3029a4b5 --- /dev/null +++ b/src/dsp/nbfm/FMDiscriminator.java @@ -0,0 +1,100 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package dsp.nbfm; + +import sample.Listener; +import sample.complex.ComplexSample; + +public class FMDiscriminator implements Listener +{ + private Listener mListener; + private ComplexSample mPreviousSample = new ComplexSample( 0.0f, 0.0f ); + private double mGain; + + /** + * Implements a polar discriminator with ArcTangent angle estimator to + * demodulate complex sampled frequency modulated signals. + * + * @param gain - gain to be applied to the demodulated output - can be + * dynamically applied to continuously adjust audio output + */ + public FMDiscriminator( double gain ) + { + mGain = gain; + } + + public void dispose() + { + mListener = null; + mPreviousSample = null; + } + + public synchronized void setGain( double gain ) + { + mGain = gain; + } + + @Override + public void receive( ComplexSample currentSample ) + { + /** + * Multiply the current sample against the complex conjugate of the + * previous sample to derive the phase delta between the two samples + * + * Negating the previous sample quadrature produces the conjugate + */ + double i = ( currentSample.inphase() * mPreviousSample.inphase() ) - + ( currentSample.quadrature() * -mPreviousSample.quadrature() ); + double q = ( currentSample.quadrature() * mPreviousSample.inphase() ) + + ( currentSample.inphase() * -mPreviousSample.quadrature() ); + + double angle; + + //Check for divide by zero + if( i == 0 ) + { + angle = 0.0; + } + else + { + /** + * Use the arcus tangent of imaginary (q) divided by real (i) to + * get the phase angle (+/-) which was directly manipulated by the + * original message waveform during the modulation. This value now + * serves as the instantaneous amplitude of the demodulated signal + */ + double denominator = 1.0d / i; + angle = Math.atan( (double)q * denominator ); + } + + if( mListener != null ) + { + mListener.receive( (float)( angle * mGain ) ); + } + + /** + * Store the current sample to use during the next iteration + */ + mPreviousSample = currentSample; + } + + public void setListener( Listener listener ) + { + mListener = listener; + } +} diff --git a/src/dsp/nbfm/FilteringNBFMDemodulator.java b/src/dsp/nbfm/FilteringNBFMDemodulator.java new file mode 100644 index 00000000..267dcee5 --- /dev/null +++ b/src/dsp/nbfm/FilteringNBFMDemodulator.java @@ -0,0 +1,92 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package dsp.nbfm; + +import sample.Broadcaster; +import sample.Listener; +import sample.complex.ComplexSample; +import dsp.filter.ComplexFIRFilter; +import dsp.filter.FilterFactory; +import dsp.filter.FloatFIRFilter; +import dsp.filter.Window.WindowType; + +public class FilteringNBFMDemodulator implements Listener +{ + private Broadcaster mBroadcaster = new Broadcaster(); + private ComplexFIRFilter mIQFilter; + private FloatFIRFilter mAudioFilter; + private FMDiscriminator mDiscriminator; + + public FilteringNBFMDemodulator( double[] iqFilter, double iqGain, + double[] audioFilter, double audioGain ) + { + mIQFilter = new ComplexFIRFilter( iqFilter, iqGain ); + + mAudioFilter = new FloatFIRFilter( audioFilter, audioGain ); + + /** + * Add the FM polar discriminator with a gain of 32768, to match + * the fact that we're producing demodulated float values + */ + mDiscriminator = new FMDiscriminator( 32768 ); + + mIQFilter.setListener( mDiscriminator ); + + mDiscriminator.setListener( mAudioFilter ); + + mAudioFilter.setListener( mBroadcaster ); + } + + /** + * Implements a quadrature narrow-band demodulator that produces float + * valued demodulated, audio filtered output samples. + */ + public FilteringNBFMDemodulator() + { + this( FilterFactory.getLowPass( 48000, 4000, 6000, 48, WindowType.HAMMING, true ), + 1.0002, + FilterFactory.getLowPass( 48000, 3000, 6000, 48, WindowType.HAMMING, true ), + 2 ); + } + + /** + * Receive method for complex samples that are fed into this class to be + * processed + */ + @Override + public void receive( ComplexSample quadratureSample ) + { + mIQFilter.receive( quadratureSample ); + } + + /** + * Adds a listener to receive demodulated samples + */ + public void addListener( Listener listener ) + { + mBroadcaster.addListener( listener ); + } + + /** + * Removes a listener from receiving demodulated samples + */ + public void removeListener( Listener listener ) + { + mBroadcaster.removeListener( listener ); + } +} diff --git a/src/dsp/nbfm/NBFMDemodulator.java b/src/dsp/nbfm/NBFMDemodulator.java new file mode 100644 index 00000000..7eb918c3 --- /dev/null +++ b/src/dsp/nbfm/NBFMDemodulator.java @@ -0,0 +1,92 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package dsp.nbfm; + +import sample.Broadcaster; +import sample.Listener; +import sample.complex.ComplexSample; +import dsp.filter.ComplexFIRFilter; +import dsp.filter.FilterFactory; +import dsp.filter.Window.WindowType; + +public class NBFMDemodulator implements Listener +{ + private Broadcaster mBroadcaster = new Broadcaster(); + private ComplexFIRFilter mIQFilter; + private FMDiscriminator mDiscriminator; + + public NBFMDemodulator( double[] iqFilter, double iqGain ) + { + mIQFilter = new ComplexFIRFilter( iqFilter, iqGain ); + + /** + * Add the FM polar discriminator with a gain of 32768, to match + * the fact that we're producing demodulated float values + */ + mDiscriminator = new FMDiscriminator( 32768 ); + + mIQFilter.setListener( mDiscriminator ); + + mDiscriminator.setListener( mBroadcaster ); + } + + public void dispose() + { + mDiscriminator.dispose(); + + mBroadcaster.dispose(); + + mIQFilter.dispose(); + } + + /** + * Implements a quadrature narrow-band demodulator that produces float + * valued demodulated, audio filtered output samples. + */ + public NBFMDemodulator() + { + this( FilterFactory.getLowPass( 48000, 4000, 73, WindowType.HAMMING ), + 1.0002 ); + } + + /** + * Receive method for complex samples that are fed into this class to be + * processed + */ + @Override + public void receive( ComplexSample sample ) + { + mIQFilter.receive( sample ); + } + + /** + * Adds a listener to receive demodulated samples + */ + public void addListener( Listener listener ) + { + mBroadcaster.addListener( listener ); + } + + /** + * Removes a listener from receiving demodulated samples + */ + public void removeListener( Listener listener ) + { + mBroadcaster.removeListener( listener ); + } +} diff --git a/src/eventlog/CallEventLogger.java b/src/eventlog/CallEventLogger.java new file mode 100644 index 00000000..a2195090 --- /dev/null +++ b/src/eventlog/CallEventLogger.java @@ -0,0 +1,43 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package eventlog; + +import java.nio.file.Path; + +import sample.Listener; +import controller.activity.CallEvent; + +public class CallEventLogger extends EventLogger implements Listener +{ + public CallEventLogger( Path logDirectory, String fileNameSuffix ) + { + super( logDirectory, fileNameSuffix ); + } + + @Override + public void receive( CallEvent callEvent ) + { + write( callEvent.toCSV() ); + } + + @Override + public String getHeader() + { + return CallEvent.getCSVHeader(); + } +} diff --git a/src/eventlog/EventLogComponentEditor.java b/src/eventlog/EventLogComponentEditor.java new file mode 100644 index 00000000..7bd42d85 --- /dev/null +++ b/src/eventlog/EventLogComponentEditor.java @@ -0,0 +1,98 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package eventlog; + +import java.util.ArrayList; + +import javax.swing.BorderFactory; +import javax.swing.JCheckBox; +import javax.swing.SwingUtilities; + +import controller.channel.AbstractChannelEditor; +import controller.channel.ChannelNode; +import eventlog.config.EventLogConfiguration; + +public class EventLogComponentEditor extends AbstractChannelEditor +{ + private static final long serialVersionUID = 1L; + + private JCheckBox mBinaryLogger = new JCheckBox( "Binary Messages" ); + private JCheckBox mDecodedLogger = new JCheckBox( "Decoded Messages" ); + private JCheckBox mCallEventLogger = new JCheckBox( "Call Events" ); + + public EventLogComponentEditor( ChannelNode channelNode ) + { + super( channelNode ); + + add( mBinaryLogger, "span" ); + add( mDecodedLogger, "span" ); + add( mCallEventLogger, "span" ); + + reset(); + } + + /** + * Sets the combo box to the type stored in the config object, causing the + * editor panel to reset with the stored config + */ + public void reset() + { + SwingUtilities.invokeLater(new Runnable() + { + final ArrayList mLoggers = mChannelNode.getChannel() + .getEventLogConfiguration().getLoggers(); + + @Override + public void run() + { + mBinaryLogger.setSelected( + mLoggers.contains( EventLogType.BINARY_MESSAGE ) ); + mDecodedLogger.setSelected( + mLoggers.contains( EventLogType.DECODED_MESSAGE ) ); + mCallEventLogger.setSelected( + mLoggers.contains( EventLogType.CALL_EVENT ) ); + } + }); + } + + /** + * Saves the current configuration as the stored configuration + */ + @Override + public void save() + { + EventLogConfiguration config = mChannelNode.getChannel() + .getEventLogConfiguration(); + + config.clear(); + + if( mBinaryLogger.isSelected() ) + { + config.addLogger( EventLogType.BINARY_MESSAGE ); + } + + if( mDecodedLogger.isSelected() ) + { + config.addLogger( EventLogType.DECODED_MESSAGE ); + } + if( mCallEventLogger.isSelected() ) + { + config.addLogger( EventLogType.CALL_EVENT ); + } + } +} diff --git a/src/eventlog/EventLogManager.java b/src/eventlog/EventLogManager.java new file mode 100644 index 00000000..2db0b41b --- /dev/null +++ b/src/eventlog/EventLogManager.java @@ -0,0 +1,60 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package eventlog; + +import java.nio.file.Path; + +import properties.SystemProperties; +import controller.channel.ProcessingChain; +import eventlog.MessageEventLogger.Type; + +public class EventLogManager +{ + private Path mDirectory; + + public EventLogManager() + { + mDirectory = SystemProperties.getInstance() + .getApplicationFolder( "event_logs" ); + } + + public EventLogger getLogger( ProcessingChain chain, EventLogType eventLogType ) + { + StringBuilder sb = new StringBuilder(); + + sb.append( chain.getChannel().getSystem() ); + sb.append( "_" ); + sb.append( chain.getChannel().getSite() ); + sb.append( "_" ); + sb.append( chain.getChannel().getName() ); + sb.append( eventLogType.getFileSuffix() ); + sb.append( ".log" ); + + switch( eventLogType ) + { + case BINARY_MESSAGE: + return new MessageEventLogger( mDirectory, sb.toString(), Type.BINARY ); + case DECODED_MESSAGE: + return new MessageEventLogger( mDirectory, sb.toString(), Type.DECODED ); + case CALL_EVENT: + return new CallEventLogger( mDirectory, sb.toString() ); + default: + return null; + } + } +} diff --git a/src/eventlog/EventLogType.java b/src/eventlog/EventLogType.java new file mode 100644 index 00000000..27db5f1a --- /dev/null +++ b/src/eventlog/EventLogType.java @@ -0,0 +1,51 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package eventlog; + + +public enum EventLogType +{ + BINARY_MESSAGE( "Binary Messages", "_binary_messages" ), + DECODED_MESSAGE( "Decoded Messages", "_decoded_messages" ), + CALL_EVENT( "Call Events", "_call_events" ); + + private String mDisplayString; + private String mFileSuffix; + + EventLogType( String displayString, String fileSuffix ) + { + mDisplayString = displayString; + mFileSuffix = fileSuffix; + } + + public String getDisplayString() + { + return mDisplayString; + } + + public String getFileSuffix() + { + return mFileSuffix; + } + + @Override + public String toString() + { + return mDisplayString; + } +} diff --git a/src/eventlog/EventLogger.java b/src/eventlog/EventLogger.java new file mode 100644 index 00000000..f7a47e12 --- /dev/null +++ b/src/eventlog/EventLogger.java @@ -0,0 +1,111 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package eventlog; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.OutputStreamWriter; +import java.io.Writer; +import java.nio.file.Path; + +import log.Log; +import util.TimeStamp; + +public abstract class EventLogger +{ + private Path mLogDirectory; + private String mFileNameSuffix; + private String mLogFileName; + protected Writer mLogFile; + + public EventLogger( Path logDirectory, String fileNameSuffix ) + { + mLogDirectory = logDirectory; + mFileNameSuffix = fileNameSuffix; + } + + public String toString() + { + if( mLogFileName != null ) + { + return mLogFileName; + } + else + { + return "Unknown"; + } + } + + public abstract String getHeader(); + + public void start() + { + + try + { + StringBuilder sb = new StringBuilder(); + sb.append( mLogDirectory ); + sb.append( File.separator ); + sb.append( TimeStamp.getTimeStamp( "_" ) ); + sb.append( "_" ); + sb.append( mFileNameSuffix ); + + mLogFileName = sb.toString(); + + Log.info( "Creating log file:" + mLogFileName ); + + mLogFile = new OutputStreamWriter(new FileOutputStream( mLogFileName ) ); + + write( getHeader() ); + } + catch (FileNotFoundException e) + { + Log.error("Couldn't create log file in directory:" + mLogDirectory ); + } + } + + public void stop() + { + if( mLogFile != null ) + { + try + { + mLogFile.flush(); + mLogFile.close(); + } + catch( Exception e ) + { + Log.error( "Couldn't close log file:" + mFileNameSuffix ); + } + } + } + + protected void write( String eventLogEntry ) + { + try + { + mLogFile.write( eventLogEntry + "\n" ); + } + catch ( IOException e ) + { + Log.error( "Error writing entry to event log file:" + e.getLocalizedMessage() ); + } + } +} diff --git a/src/eventlog/MessageEventLogger.java b/src/eventlog/MessageEventLogger.java new file mode 100644 index 00000000..4ce454dc --- /dev/null +++ b/src/eventlog/MessageEventLogger.java @@ -0,0 +1,65 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package eventlog; + +import java.nio.file.Path; + +import message.Message; +import sample.Listener; +import util.TimeStamp; + +public class MessageEventLogger extends EventLogger implements Listener +{ + public enum Type { BINARY, DECODED }; + + private Type mType; + + public MessageEventLogger( Path logDirectory, String fileNameSuffix, Type type ) + { + super( logDirectory, fileNameSuffix ); + + mType = type; + } + + @Override + public void receive( Message message ) + { + StringBuilder sb = new StringBuilder(); + sb.append( TimeStamp.getTimeStamp( " " ) ); + sb.append( "," ); + sb.append( ( message.isValid() ? "PASSED" : "FAILED" ) ); + sb.append( "," ); + + if( mType == Type.BINARY ) + { + sb.append( message.getBinaryMessage() ); + } + else + { + sb.append( message.getMessage() ); + } + + write( sb.toString() ); + } + + @Override + public String getHeader() + { + return mType.toString() + " Message Logger\n"; + } +} diff --git a/src/eventlog/config/EventLogConfiguration.java b/src/eventlog/config/EventLogConfiguration.java new file mode 100644 index 00000000..f939328a --- /dev/null +++ b/src/eventlog/config/EventLogConfiguration.java @@ -0,0 +1,57 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package eventlog.config; + +import java.util.ArrayList; + +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlRootElement; + +import controller.config.Configuration; +import eventlog.EventLogType; + +@XmlRootElement( name = "event_log_configuration" ) +public class EventLogConfiguration extends Configuration +{ + protected ArrayList mLoggers = new ArrayList(); + + public EventLogConfiguration() + { + } + + @XmlElement( name = "logger" ) + public ArrayList getLoggers() + { + return mLoggers; + } + + public void setLoggers( ArrayList loggers ) + { + mLoggers = loggers; + } + + public void addLogger( EventLogType logger ) + { + mLoggers.add( logger ); + } + + public void clear() + { + mLoggers.clear(); + } +} diff --git a/src/gui/SDRTrunk.java b/src/gui/SDRTrunk.java new file mode 100644 index 00000000..6b08bc10 --- /dev/null +++ b/src/gui/SDRTrunk.java @@ -0,0 +1,318 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package gui; + + +import java.awt.AWTException; +import java.awt.Dimension; +import java.awt.EventQueue; +import java.awt.Robot; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.awt.image.BufferedImage; +import java.io.IOException; +import java.nio.file.FileSystems; +import java.nio.file.Files; +import java.nio.file.Path; + +import javax.imageio.ImageIO; +import javax.swing.JFrame; +import javax.swing.JMenu; +import javax.swing.JMenuBar; +import javax.swing.JMenuItem; +import javax.swing.JSeparator; + +import log.Log; +import net.miginfocom.swing.MigLayout; +import properties.SystemProperties; +import source.tuner.TunerSelectionListener; +import spectrum.SpectralDisplayPanel; +import util.TimeStamp; + +import com.jidesoft.swing.JideSplitPane; + +import controller.ControllerPanel; +import controller.ResourceManager; + +public class SDRTrunk +{ + private ResourceManager mResourceManager; + private ControllerPanel mControllerPanel; + private SpectralDisplayPanel mSpectralPanel; + private JFrame mMainGui = new JFrame(); + + public SDRTrunk() + { + //Setup the application home directory + Path home = getHomePath(); + + //Load properties file + if( home != null ) + { + loadProperties( home ); + } + + //Start the logger + Log.init(); + + //Log current properties setting + SystemProperties.getInstance().logCurrentSettings(); + + /** + * Construct the resource manager now, so that it can use the system + * properties that were just loaded + */ + mResourceManager = new ResourceManager(); + + //Initialize the GUI + initGUI(); + + Log.info( "Starting the main gui" ); + //Start the gui + EventQueue.invokeLater( new Runnable() + { + public void run() + { + try + { + mMainGui.setVisible( true ); + } + catch( Exception e ) + { + e.printStackTrace(); + } + } + } ); + } + + /** + * Launch the application. + */ + public static void main( String[] args ) + { + new SDRTrunk(); + } + + /** + * Initialize the contents of the frame. + */ + private void initGUI() + { + mMainGui.setLayout( new MigLayout( "insets 0 0 0 0 ", + "[grow,fill]", + "[grow,fill]") ); + + mControllerPanel = new ControllerPanel( mResourceManager ); + + mSpectralPanel = new SpectralDisplayPanel( mResourceManager ); + + //Register spectral panel to receive tuner selection events + mControllerPanel.getController() + .addListener( (TunerSelectionListener)mSpectralPanel ); + + //Add spectrum panel to receive channel change events + mResourceManager.getChannelManager().addListener( mSpectralPanel ); + + //init() the controller to load tuners and playlists + mControllerPanel.getController().init(); + + //Fire first tuner selection so it is displayed in the spectrum/waterfall + mControllerPanel.getController().fireFirstTunerSelected(); + + /** + * Setup main JFrame window + */ + mMainGui.setTitle( "SDRTrunk" ); + mMainGui.setBounds( 100, 100, 1280, 800 ); + mMainGui.setDefaultCloseOperation( JFrame.EXIT_ON_CLOSE ); + + mSpectralPanel.setPreferredSize( new Dimension( 1280, 400 ) ); + + JideSplitPane splitPane = new JideSplitPane( JideSplitPane.VERTICAL_SPLIT ); + splitPane.setDividerSize( 5 ); + splitPane.add( "Denny", mSpectralPanel ); + splitPane.add( "Denny2", mControllerPanel ); + + mMainGui.add( splitPane, "cell 0 0,span,grow"); + + /** + * Menu items + */ + JMenuBar menuBar = new JMenuBar(); + mMainGui.setJMenuBar( menuBar ); + + JMenu fileMenu = new JMenu( "File" ); + menuBar.add( fileMenu ); + + JMenuItem settingsMenu = new JMenuItem( "Icon Manager" ); + settingsMenu.addActionListener( new ActionListener() + { + @Override + public void actionPerformed( ActionEvent arg0 ) + { + mControllerPanel.getController().showIconManager(); + } + } ); + fileMenu.add( settingsMenu ); + + fileMenu.add( new JSeparator() ); + + JMenuItem exitMenu = new JMenuItem( "Exit" ); + exitMenu.addActionListener( + new ActionListener() + { + public void actionPerformed( ActionEvent event ) + { + System.exit( 0 ); + } + } + ); + + fileMenu.add( exitMenu ); + + JMenuItem screenCaptureItem = new JMenuItem( "Screen Capture" ); + screenCaptureItem.addActionListener( new ActionListener() + { + @Override + public void actionPerformed( ActionEvent arg0 ) + { + try + { + Robot robot = new Robot(); + + final BufferedImage image = + robot.createScreenCapture( mMainGui.getBounds() ); + + SystemProperties props = SystemProperties.getInstance(); + + Path capturePath = props.getApplicationFolder( "screen_captures" ); + + if( !Files.exists( capturePath ) ) + { + try + { + Files.createDirectory( capturePath ); + } + catch ( IOException e ) + { + Log.error( "Couldn't create 'screen_captures' " + + "subdirectory in the " + + "SDRTrunk application directory" ); + } + } + + String filename = TimeStamp.getTimeStamp( "_" ) + + "_screen_capture.png"; + + final Path captureFile = capturePath.resolve( filename ); + + EventQueue.invokeLater( new Runnable() + { + @Override + public void run() + { + try + { + ImageIO.write( image, "png", + captureFile.toFile() ); + } + catch ( IOException e ) + { + Log.error( "Couldn't write screen capture to " + + "file [" + captureFile.toString() + "]" ); + } + }} ); + } + catch ( AWTException e ) + { + Log.error( "Exception while taking screen capture - " + + e.getLocalizedMessage() ); + } + } + } ); + + menuBar.add( screenCaptureItem ); + } + + /** + * Loads the application properties file from the user's home directory, + * creating the properties file for the first-time, if necessary + */ + private void loadProperties( Path homePath ) + { + Path propsPath = homePath.resolve( "SDRTrunk.properties" ); + + if( !Files.exists( propsPath ) ) + { + try + { + Log.info( "SDRTrunk - creating application properties file [" + + propsPath.toAbsolutePath() + "]" ); + + Files.createFile( propsPath ); + } + catch ( IOException e ) + { + Log.error( "SDRTrunk - couldn't create application properties " + + "file [" + propsPath.toAbsolutePath() ); + } + } + + if( Files.exists( propsPath ) ) + { + SystemProperties.getInstance().load( propsPath ); + } + else + { + Log.error( "SDRTrunk - couldn't find or recreate the SDRTrunk " + + "application properties file" ); + } + } + + /** + * Gets (or creates) the SDRTRunk application home directory. + * + * Note: the user can change this setting to allow log files and other + * files to reside elsewhere on the file system. + */ + private Path getHomePath() + { + Path homePath = FileSystems.getDefault() + .getPath( System.getProperty( "user.home" ), "SDRTrunk" ); + + if( !Files.exists( homePath ) ) + { + try + { + Files.createDirectory( homePath ); + + Log.info( "SDRTrunk - created application home directory [" + + homePath.toString() + "]" ); + } + catch ( Exception e ) + { + homePath = null; + + Log.error( "SDRTrunk: exception while creating SDRTrunk home " + + "directory in the user's home directory" ); + } + } + + return homePath; + } +} diff --git a/src/gui/control/JFrequencyControl.java b/src/gui/control/JFrequencyControl.java new file mode 100644 index 00000000..e673a6c6 --- /dev/null +++ b/src/gui/control/JFrequencyControl.java @@ -0,0 +1,423 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package gui.control; + +import java.awt.Color; +import java.awt.Cursor; +import java.awt.Font; +import java.awt.Image; +import java.awt.Point; +import java.awt.Toolkit; +import java.awt.event.KeyEvent; +import java.awt.event.KeyListener; +import java.awt.event.MouseEvent; +import java.awt.event.MouseListener; +import java.awt.event.MouseWheelEvent; +import java.awt.event.MouseWheelListener; +import java.text.ParseException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Iterator; + +import javax.swing.BorderFactory; +import javax.swing.JLabel; +import javax.swing.JPanel; +import javax.swing.JTextField; + +import log.Log; +import net.miginfocom.swing.MigLayout; +import source.tuner.FrequencyChangeListener; + +public class JFrequencyControl extends JPanel implements FrequencyChangeListener +{ + private static final long serialVersionUID = 1L; + private ArrayList mListeners = + new ArrayList(); + + private Color mHighlightColor = Color.YELLOW; + + private int mFrequency; + + private Cursor mBlankCursor; + + private HashMap mDigits = new HashMap(); + + public JFrequencyControl() + { + init(); + } + + public JFrequencyControl( int value ) + { + this(); + mFrequency = value; + } + + private void init() + { + setLayout( new MigLayout( "", "[]0[]", "" ) ); + + Font font = new Font( Font.MONOSPACED, Font.BOLD, 30 ); + + for( int x = 9; x >= 0; x-- ) + { + Digit digit = null; + + try + { + digit = new Digit( x ); + } + catch ( ParseException e ) + { + Log.error( "JFrequencyControl - parse exception " + + "constructing a digit - " + e.getLocalizedMessage() ); + } + + if( digit != null ) + { + mDigits.put( x, digit ); + + add( digit ); + + digit.setFont( font ); + + if( x == 6 ) + { + JLabel period = new JLabel( "." ); + + add( period ); + } + } + } + + add( new JLabel( " MHz" ) ); + + /** + * Create a blank cursor to use when the mouse is over the digits + */ + //Create an empty byte array + byte[] imageByte = new byte[ 0 ]; + + //Create image for cursor using empty array + Image cursorImage = Toolkit.getDefaultToolkit().createImage( imageByte ); + + mBlankCursor = Toolkit.getDefaultToolkit() + .createCustomCursor( cursorImage, new Point( 0, 0 ), "cursor" ); + + revalidate(); + } + + /** + * Receives a frequency change event invoked by another control. We don't + * rebroadcast this event, just set the control to indicate the new frequency. + */ + @Override + public void frequencyChanged( int frequency, int bandwidth ) + { + setFrequency( frequency, false ); + } + + /** + * Loads the frequency into the display and optionally fires a change event + * to all registered listeners + */ + public void setFrequency( int frequency, boolean fireChangeEvent ) + { + mFrequency = frequency; + + for( Digit digit: mDigits.values() ) + { + digit.setFrequency( frequency, fireChangeEvent ); + } + } + + public int getFrequency() + { + return mFrequency; + } + + private void updateFrequency() + { + int frequency = 0; + + for( Digit digit: mDigits.values() ) + { + frequency += digit.getFrequency(); + } + + mFrequency = frequency; + } + + private void fireFrequencyChanged() + { + updateFrequency(); + + Iterator it = mListeners.iterator(); + + while( it.hasNext() ) + { + it.next().frequencyChanged( mFrequency, -1 ); + } + } + + public void addListener( FrequencyChangeListener listener ) + { + mListeners.add( listener ); + } + + public void removeListener( FrequencyChangeListener listener ) + { + mListeners.remove( listener ); + } + + public class Digit extends JTextField + { + private static final long serialVersionUID = 1L; + private int mPower = 0; + private int mValue = 0; + + private Digit( int position ) throws ParseException + { + super( "0" ); + + mPower = position; + + Listener listener = new Listener(); + + this.addKeyListener( listener ); + this.addMouseListener( listener ); + this.addMouseWheelListener( listener ); + } + + /** + * Sets this digit to the value of the column in frequency that corresponds + * to the power (ie column) set for this digit. Optionally, fires a + * value change event to all listeners. + */ + public void setFrequency( int frequency, boolean fireChangeEvent ) + { + //Strip the digits higher than this one + int lower = frequency % (int)( Math.pow( 10, mPower + 1) ); + + //Set the value to int value of dividing by 10 to this power + int value = (int)( lower / (int)( Math.pow( 10, mPower ) ) ); + + set( value, fireChangeEvent ); + } + + public int getFrequency() + { + return mValue * (int)Math.pow( 10, mPower ); + } + + public void increment() + { + increment( true ); + } + + public void increment( boolean fireChangeEvent ) + { + set( mValue + 1, fireChangeEvent ); + } + + public void decrement() + { + decrement( true ); + } + + public void decrement( boolean fireChangeEvent ) + { + set( mValue - 1, fireChangeEvent ); + } + + /** + * Convenience wrapper to change amount and fire change event + */ + private void set( int amount ) + { + set( amount, true ); + } + + /** + * Changes the value and optionally fires change event to listeners + */ + private void set( int amount, boolean fireChangeEvent ) + { + mValue = amount; + + while( mValue < 0 ) + { + mValue += 10; + + Digit nextHigherDigit = mDigits.get( mPower + 1 ); + + if( nextHigherDigit != null ) + { + nextHigherDigit.decrement( false ); + } + + } + + while( mValue > 9 ) + { + mValue -= 10; + + Digit nextHigherDigit = mDigits.get( mPower + 1 ); + + if( nextHigherDigit != null ) + { + nextHigherDigit.increment( false ); + } + } + + setText( String.valueOf( mValue ) ); + + repaint(); + + if( fireChangeEvent ) + { + fireFrequencyChanged(); + } + + } + + private class Listener implements KeyListener, + MouseListener, + MouseWheelListener + { + @Override + public void keyReleased( KeyEvent e ) + { + int key = e.getKeyCode(); + + switch( key ) + { + case KeyEvent.VK_0: + set( 0 ); + Digit.this.transferFocus(); + break; + case KeyEvent.VK_1: + set( 1 ); + Digit.this.transferFocus(); + break; + case KeyEvent.VK_2: + set( 2 ); + Digit.this.transferFocus(); + break; + case KeyEvent.VK_3: + set( 3 ); + Digit.this.transferFocus(); + break; + case KeyEvent.VK_4: + set( 4 ); + Digit.this.transferFocus(); + break; + case KeyEvent.VK_5: + set( 5 ); + Digit.this.transferFocus(); + break; + case KeyEvent.VK_6: + set( 6 ); + Digit.this.transferFocus(); + break; + case KeyEvent.VK_7: + set( 7 ); + Digit.this.transferFocus(); + break; + case KeyEvent.VK_8: + set( 8 ); + Digit.this.transferFocus(); + break; + case KeyEvent.VK_9: + set( 9 ); + Digit.this.transferFocus(); + break; + case KeyEvent.VK_LEFT: + Digit.this.transferFocusBackward(); + break; + case KeyEvent.VK_RIGHT: + Digit.this.transferFocus(); + break; + case KeyEvent.VK_UP: + increment(); + break; + case KeyEvent.VK_DOWN: + decrement(); + break; + case KeyEvent.VK_TAB: + break; + default: + set( mValue ); + break; + } + + repaint(); + } + + @Override + public void keyPressed( KeyEvent e ) {} + @Override + public void keyTyped( KeyEvent e ) {} + + @Override + public void mouseClicked( MouseEvent e ) + { + switch( e.getButton() ) + { + case MouseEvent.BUTTON1: + increment(); + break; + case MouseEvent.BUTTON2: + break; + case MouseEvent.BUTTON3: + decrement(); + break; + } + } + + public void mousePressed( MouseEvent e ) {} + public void mouseReleased( MouseEvent e ) {} + + @Override + public void mouseEntered( MouseEvent e ) + { + Digit.this.setBackground( mHighlightColor ); + + setCursor( mBlankCursor ); + + repaint(); + } + + @Override + public void mouseExited( MouseEvent e ) + { + Digit.this.setBackground( Color.WHITE ); + + setCursor( Cursor.getDefaultCursor() ); + + repaint(); + } + + @Override + public void mouseWheelMoved( MouseWheelEvent e ) + { + set( mValue - e.getWheelRotation() ); + } + } + } +} diff --git a/src/instrument/Instrumentable.java b/src/instrument/Instrumentable.java new file mode 100644 index 00000000..ab98d1d1 --- /dev/null +++ b/src/instrument/Instrumentable.java @@ -0,0 +1,53 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package instrument; + +import instrument.tap.Tap; + +import java.util.List; + +/** + * Interface for classes that want to expose internal data, streams or events + * to external listeners. + */ +public interface Instrumentable +{ + /** + * Returns the list of available tap points. Use the addTap() and + * removeTap() to register a tap on the instrumentable class and then add + * your TapListener(s) to the registered tap. + */ + public List getTaps(); + + /** + * Registers a tap on the instrumentable object. + * + * @param tap - one of the tap(s) obtained from getTaps() from the + * instrumentable source + */ + public void addTap( Tap tap ); + + /** + * Unregisters a tap on the instrumentable object, if the tap is currently + * registered. + * + * @param tap - one of the tap(s) obtained from getTaps() from the + * instrumentable source + */ + public void removeTap( Tap tap ); +} diff --git a/src/instrument/gui/BinaryTapViewPanel.java b/src/instrument/gui/BinaryTapViewPanel.java new file mode 100644 index 00000000..60cabaee --- /dev/null +++ b/src/instrument/gui/BinaryTapViewPanel.java @@ -0,0 +1,152 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package instrument.gui; + +import instrument.tap.Tap; +import instrument.tap.stream.BinaryTap; + +import java.awt.Color; +import java.awt.FontMetrics; +import java.awt.Graphics; +import java.awt.Graphics2D; +import java.awt.geom.Path2D; +import java.awt.geom.Rectangle2D; +import java.util.List; +import java.util.Observable; + +public class BinaryTapViewPanel extends TapViewPanel +{ + private Tap mTap; + private List mSamples; + private int mSampleCount; + private int mOffset = 0; + + public BinaryTapViewPanel( BinaryTap tap ) + { + super( new SampleModel(), tap.getName() ); + + mTap = tap; + mTap.addListener( getModel() ); + + getModel().setSampleCount( + (int)( getModel().getSampleCount() * tap.getSampleRateRatio() ) ); + } + + @Override + public void update( Observable arg0, Object arg1 ) + { + mSamples = (List)getModel().getSamples(); + mSampleCount = getModel().getSampleCount(); + repaint(); + } + + public void paintComponent( Graphics g ) + { + super.paintComponent( g ); + + Graphics2D g2 = (Graphics2D)g; + + int height = getHeight(); + int width = getWidth(); + + g2.setColor( Color.DARK_GRAY ); + + float middle = (float)height / 2.0f; + + g2.drawLine( 0, (int)middle, width, (int)middle ); + + if( mSamples != null && mSamples.size() > 0 ) + { + g2.setColor( getForeground() ); + + g2.drawString( mLabel, 5 , 15 ); + + for( int x = 1; x < mSamples.size(); x++ ) + { + paintBit( g, x, mSamples.get( x ) ); + } + } + + g2.dispose(); + } + + /** + * Paints a polyline representing a boolean value with a 0 or 1 label in + * the middle of it + * + * @param graphics + * @param index - index of the sample int the sample buffer + * @param value - bit value, 0 or 1 + */ + private void paintBit( Graphics graphics, int index, boolean value ) + { + Graphics2D g2 = (Graphics2D)graphics; + + Path2D.Float polyline = + new Path2D.Float( Path2D.Float.WIND_EVEN_ODD, mSamples.size() ); + + float middleY = ( (float)getHeight() / 2.0f ); + + float indexWidth = (float)( getWidth() ) / (float)mSampleCount; + + float startX = (float)index * indexWidth; + float stopX = (float)(index + 1 ) * indexWidth; + float fivePercent = indexWidth * .05f; + float leftX = startX + fivePercent; + float rightX = stopX - fivePercent; + + /* Start */ + polyline.moveTo( startX, middleY ); + + /* Left horizonal segment */ + polyline.lineTo( leftX, middleY ); + + float y = ( value ? ( middleY - ( middleY * 0.5f) ) : + ( middleY + ( middleY * 0.5f) ) ); + + /* Left vertical segment */ + polyline.lineTo( leftX, y ); + + /* Middle horizontal segment */ + polyline.lineTo( rightX, y ); + + /* Right vertical segment */ + polyline.lineTo( rightX, middleY ); + + /* Right horizontal segment */ + polyline.lineTo( stopX, middleY ); + + g2.draw( polyline ); + + /* Draw the bit value label */ + String bit = value ? "1" : "0"; + + FontMetrics fontMetrics = graphics.getFontMetrics( this.getFont() ); + + Rectangle2D label = fontMetrics.getStringBounds( bit, graphics ); + + float offsetX = (float)label.getWidth() / 2.0f; + float offsetY = (float)label.getWidth() / 2.0f; + + float middleX = startX + ( indexWidth / 2.0f ); + + graphics.drawString( bit, + (int)( middleX - offsetX ), + (int)( middleY - offsetY ) ); + } +} diff --git a/src/instrument/gui/DecoderViewFrame.java b/src/instrument/gui/DecoderViewFrame.java new file mode 100644 index 00000000..103fddf2 --- /dev/null +++ b/src/instrument/gui/DecoderViewFrame.java @@ -0,0 +1,232 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package instrument.gui; + +import instrument.Instrumentable; +import instrument.tap.Tap; +import instrument.tap.stream.BinaryTap; +import instrument.tap.stream.FloatTap; +import instrument.tap.stream.SymbolEventTap; + +import java.awt.Dimension; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.awt.event.MouseEvent; +import java.awt.event.MouseListener; +import java.util.HashMap; + +import javax.swing.JCheckBoxMenuItem; +import javax.swing.JInternalFrame; +import javax.swing.JMenu; +import javax.swing.JPopupMenu; +import javax.swing.JSeparator; + +import log.Log; +import message.Message; +import net.miginfocom.swing.MigLayout; +import sample.Listener; +import source.Source.SampleType; +import source.wave.FloatWaveSource; +import source.wave.WaveSource; +import source.wave.WaveSource.PositionListener; +import decode.Decoder; + +public class DecoderViewFrame extends JInternalFrame + implements PositionListener, Listener +{ + private Decoder mDecoder; + private WaveSource mWaveSource; + + private HashMap mPanelMap = + new HashMap(); + + public DecoderViewFrame( Decoder decoder, WaveSource source ) + { + mDecoder = decoder; + mWaveSource = source; + + if( source.getSampleType() == SampleType.COMPLEX ) + { + Log.error( "Hey, we're not wired up yet for complex" ); + } + else if( source.getSampleType() == SampleType.FLOAT ) + { + FloatWaveSource fws = (FloatWaveSource)mWaveSource; + fws.setListener( mDecoder.getFloatReceiver() ); + } + + mDecoder.addMessageListener( this ); + + initGui(); + } + + private void initGui() + { + setLayout( new MigLayout( "insets 0 0 0 0 ", "[grow,fill]", "[grow,fill]" ) ); + + setTitle( "Decoder [" + mDecoder.getType().getDisplayString() + "]" ); + setPreferredSize( new Dimension( 450, 250 ) ); + setSize( 450, 250 ); + + setResizable( true ); + setClosable( true ); + setIconifiable( true ); + setMaximizable( false ); + + addMouseListener( new MouseListener() + { + @Override + public void mouseClicked( MouseEvent me ) + { + if( me.getButton() == MouseEvent.BUTTON3 ) + { + JPopupMenu popup = new JPopupMenu(); + + if( mDecoder instanceof Instrumentable ) + { + JMenu tapMenu = new JMenu( "Taps" ); + popup.add( tapMenu ); + + Instrumentable i = (Instrumentable)mDecoder; + + for( Tap tap: i.getTaps() ) + { + tapMenu.add( new TapSelectionItem( tap ) ); + } + } + + if( !mPanelMap.values().isEmpty() ) + { + popup.add( new JSeparator() ); + + for( TapViewPanel panel: mPanelMap.values() ) + { + popup.add( panel.getContextMenu() ); + } + } + + popup.show( DecoderViewFrame.this, me.getX(), me.getY() ); + } + } + + @Override + public void mouseEntered( MouseEvent arg0 ) {} + @Override + public void mouseExited( MouseEvent arg0 ) {} + @Override + public void mousePressed( MouseEvent arg0 ) {} + @Override + public void mouseReleased( MouseEvent arg0 ) {} + + } ); + } + + public void add( Tap tap ) + { + TapViewPanel panel = null; + + switch( tap.getType() ) + { + case EVENT_SYNC_DETECT: + break; + case STREAM_BINARY: + panel = new BinaryTapViewPanel( (BinaryTap)tap ); + break; + case STREAM_COMPLEX: + break; + case STREAM_FLOAT: + panel = new FloatTapViewPanel( (FloatTap)tap ); + break; + case STREAM_SYMBOL: + panel = new SymbolEventTapViewPanel( (SymbolEventTap)tap ); + break; + } + + add( panel, "span" ); + + validate(); + + mPanelMap.put( tap, panel ); + + ((Instrumentable)mDecoder).addTap( tap ); + } + + public void remove( Tap tap ) + { + ((Instrumentable)mDecoder).removeTap( tap ); + + TapViewPanel panel = mPanelMap.get( tap ); + + remove( panel ); + + validate(); + + mPanelMap.remove( tap ); + } + + public class TapSelectionItem extends JCheckBoxMenuItem + { + private Tap mTap; + + public TapSelectionItem( Tap tap ) + { + super( tap.getName() ); + + mTap = tap; + + if( mPanelMap.keySet().contains( tap ) ) + { + setSelected( true ); + } + + addActionListener( new ActionListener() + { + @Override + public void actionPerformed( ActionEvent arg0 ) + { + if( mPanelMap.keySet().contains( mTap ) ) + { + DecoderViewFrame.this.remove( mTap ); + } + else + { + DecoderViewFrame.this.add( mTap ); + } + } + } ); + } + } + + @Override + public void positionUpdated( long position, boolean reset ) + { + if( reset ) + { + for( TapViewPanel panel: mPanelMap.values() ) + { + panel.reset(); + } + } + } + + @Override + public void receive( Message message ) + { + Log.info( "Decoder message: " + message.toString() ); + } +} diff --git a/src/instrument/gui/FloatTapViewPanel.java b/src/instrument/gui/FloatTapViewPanel.java new file mode 100644 index 00000000..d6b6b1cb --- /dev/null +++ b/src/instrument/gui/FloatTapViewPanel.java @@ -0,0 +1,119 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package instrument.gui; + +import instrument.tap.Tap; +import instrument.tap.stream.FloatTap; + +import java.awt.Color; +import java.awt.Graphics; +import java.awt.Graphics2D; +import java.awt.geom.Path2D; +import java.awt.geom.Point2D; +import java.util.List; +import java.util.Observable; + +public class FloatTapViewPanel extends TapViewPanel +{ + private Tap mTap; + private List mSamples; + private int mSampleCount; + + public FloatTapViewPanel( FloatTap tap ) + { + super( new SampleModel(), tap.getName() ); + + mTap = tap; + mTap.addListener( getModel() ); + + getModel().setSampleCount( + (int)( getModel().getSampleCount() * tap.getSampleRateRatio() ) ); + } + + @Override + public void update( Observable arg0, Object arg1 ) + { + mSamples = (List)getModel().getSamples(); + mSampleCount = getModel().getSampleCount(); + repaint(); + } + + public void paintComponent( Graphics g ) + { + super.paintComponent( g ); + + Graphics2D g2 = (Graphics2D)g; + + int height = getHeight(); + int width = getWidth(); + + g2.setColor( Color.DARK_GRAY ); + + float middle = (float)height / 2.0f; + + g2.drawLine( 0, (int)middle, width, (int)middle ); + + if( mSamples != null && mSamples.size() > 0 ) + { + g2.setColor( getForeground() ); + + g2.drawString( mLabel, 5 , 15 ); + + Path2D.Float polyline = + new Path2D.Float( Path2D.Float.WIND_EVEN_ODD, mSamples.size() ); + + /* Move to the first point */ + Point2D.Float first = resolve( 0, mSamples.get( 0 ) ); + polyline.moveTo( first.x, first.y ); + + for( int x = 1; x < mSamples.size(); x++ ) + { + Point2D.Float point = resolve( x, mSamples.get( x ) ); + + polyline.lineTo( point.x, point.y ); + } + + g2.draw(polyline); + } + + g2.dispose(); + } + + private Point2D.Float resolve( int index, float value ) + { + int middle = (int)( getHeight() / 2 ); + + float scaledValue = value / (float)Short.MAX_VALUE; + + float y = middle - ( middle * scaledValue * mVerticalZoom ); + + /* Clip y to zero as necessary */ + y = ( y < 0 ) ? 0 : y; + + /* Clip y to max height as necessary */ + y = ( y > getHeight() ) ? getHeight() : y; + + float indexWidth = (float)getWidth() / (float)mSampleCount; + + float x = (float)index * indexWidth; + + Point2D.Float point = new Point2D.Float( x, y ); + + return new Point2D.Float( x , y ); + } +} diff --git a/src/instrument/gui/SampleModel.java b/src/instrument/gui/SampleModel.java new file mode 100644 index 00000000..32dc0702 --- /dev/null +++ b/src/instrument/gui/SampleModel.java @@ -0,0 +1,78 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package instrument.gui; + +import instrument.tap.TapListener; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Observable; + +public class SampleModel extends Observable implements TapListener +{ + private int mSampleCount = 2000; + private ArrayList mSamples = new ArrayList(); + + public SampleModel() + { + } + + public List getSamples() + { + return Collections.unmodifiableList( mSamples ); + } + + public void clearSamples() + { + mSamples.clear(); + + changed(); + } + + public int getSampleCount() + { + return mSampleCount; + } + + public void setSampleCount( int count ) + { + mSampleCount = count; + + changed(); + } + + @Override + public void receive( T t ) + { + mSamples.add( t ); + + while( mSamples.size() > mSampleCount ) + { + mSamples.remove( 0 ); + } + + changed(); + } + + private void changed() + { + setChanged(); + notifyObservers(); + } +} diff --git a/src/instrument/gui/SymbolEventTapViewPanel.java b/src/instrument/gui/SymbolEventTapViewPanel.java new file mode 100644 index 00000000..1663002d --- /dev/null +++ b/src/instrument/gui/SymbolEventTapViewPanel.java @@ -0,0 +1,186 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package instrument.gui; + +import instrument.tap.stream.SymbolEventTap; + +import java.awt.Color; +import java.awt.FontMetrics; +import java.awt.Graphics; +import java.awt.Graphics2D; +import java.awt.geom.Path2D; +import java.awt.geom.Rectangle2D; +import java.util.List; +import java.util.Observable; + +import dsp.fsk.SymbolEvent; + +public class SymbolEventTapViewPanel extends TapViewPanel +{ + private SymbolEventTap mTap; + private List mSamples; + private int mSampleCount; + private int mOffset = 0; + + public SymbolEventTapViewPanel( SymbolEventTap tap ) + { + super( new SampleModel(), tap.getName() ); + + mTap = tap; + mTap.addListener( getModel() ); + + getModel().setSampleCount( + (int)( getModel().getSampleCount() * tap.getSampleRateRatio() ) ); + } + + @Override + public void update( Observable arg0, Object arg1 ) + { + mSamples = (List)getModel().getSamples(); + mSampleCount = getModel().getSampleCount(); + repaint(); + } + + public void paintComponent( Graphics g ) + { + super.paintComponent( g ); + + Graphics2D g2 = (Graphics2D)g; + + int height = getHeight(); + int width = getWidth(); + + g2.setColor( Color.DARK_GRAY ); + + float middle = (float)height / 2.0f; + + g2.drawLine( 0, (int)middle, width, (int)middle ); + + if( mSamples != null && mSamples.size() > 0 ) + { + g2.setColor( getForeground() ); + + g2.drawString( mLabel, 5 , 15 ); + + for( int x = 1; x < mSamples.size(); x++ ) + { + paint( g, x, mSamples.get( x ) ); + } + } + + g2.dispose(); + } + + /** + * Paints a polyline representing a boolean value with a 0 or 1 label in + * the middle of it + * + * @param graphics + * @param index - index of the sample int the sample buffer + * @param value - bit value, 0 or 1 + */ + private void paint( Graphics graphics, int index, SymbolEvent symbol ) + { + Graphics2D g2 = (Graphics2D)graphics; + + g2.setColor( getForeground() ); + + Path2D.Float polyline = + new Path2D.Float( Path2D.Float.WIND_EVEN_ODD, mSamples.size() ); + + float middleY = ( (float)getHeight() / 2.0f ); + + float indexWidth = (float)( getWidth() ) / (float)mSampleCount; + + float startX = (float)index * indexWidth; + float stopX = (float)(index + 1 ) * indexWidth; + float fivePercent = indexWidth * .05f; + float leftX = startX + fivePercent; + float rightX = stopX - fivePercent; + + /* Start */ + polyline.moveTo( startX, middleY ); + + /* Left horizonal segment */ + polyline.lineTo( leftX, middleY ); + + float y = ( symbol.getDecision() ? ( middleY - ( middleY * 0.5f) ) : + ( middleY + ( middleY * 0.5f) ) ); + + /* Left vertical segment */ + polyline.lineTo( leftX, y ); + + /* Middle horizontal segment */ + polyline.lineTo( rightX, y ); + + /* Right vertical segment */ + polyline.lineTo( rightX, middleY ); + + /* Right horizontal segment */ + polyline.lineTo( stopX, middleY ); + + g2.draw( polyline ); + + /* Draw the bit value label */ + String bit = symbol.getDecision() ? "1" : "0"; + + FontMetrics fontMetrics = graphics.getFontMetrics( this.getFont() ); + + Rectangle2D label = fontMetrics.getStringBounds( bit, graphics ); + + float offsetX = (float)label.getWidth() / 2.0f; + float offsetY = (float)label.getHeight() / 2.0f; + + float middleX = startX + ( indexWidth / 2.0f ); + + graphics.drawString( bit, + (int)( middleX - offsetX ), + (int)( middleY - ( ( middleY < y ) ? offsetY : -( 2.0f * offsetY ) ) ) ); + + /* Draw samples */ + float sampleWidth = ( rightX - leftX ) / (float)( symbol.getSamplesPerSymbol() + 1 ); + + g2.setColor( Color.GREEN ); + + for( int x = 0; x < symbol.getSamplesPerSymbol(); x++ ) + { + Path2D.Float line = + new Path2D.Float( Path2D.Float.WIND_EVEN_ODD, 2 ); + + float sampleX = leftX + ( ( x + 1 ) * sampleWidth ); + +// line.moveTo( sampleX, middleY ); + line.moveTo( sampleX, ( symbol.getBitSet().get( x ) ? + middleY - ( middleY * .05f ) : + middleY + ( middleY * .05f ) ) ); + + float sampleY = ( symbol.getBitSet().get( x ) ? + ( middleY - ( middleY * 0.45f ) ) : + ( middleY + ( middleY * 0.45f ) ) ); + + line.lineTo( sampleX, sampleY ); + + g2.draw( line ); + + graphics.drawString( symbol.getShift().getLabel(), + (int)( middleX - offsetX ), + (int)( y - ( ( y < middleY ) ? offsetY : -( 2.0f * offsetY ) ) ) ); + + } + } +} diff --git a/src/instrument/gui/TapViewPanel.java b/src/instrument/gui/TapViewPanel.java new file mode 100644 index 00000000..ad5e58af --- /dev/null +++ b/src/instrument/gui/TapViewPanel.java @@ -0,0 +1,129 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package instrument.gui; + +import java.awt.Color; +import java.awt.Dimension; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.util.Observer; + +import javax.swing.JCheckBoxMenuItem; +import javax.swing.JMenu; +import javax.swing.JPanel; + +public abstract class TapViewPanel extends JPanel implements Observer +{ + protected SampleModel mModel; + protected String mLabel; + + protected float mVerticalZoom = 1.0f; + + public TapViewPanel( SampleModel model, String label ) + { + mModel = model; + mModel.addObserver( this ); + + mLabel = label; + + initGui(); + } + + private void initGui() + { + setBackground( Color.WHITE ); + setForeground( Color.BLUE ); + + setSize( new Dimension( 400, 200 ) ); + } + + public void reset() + { + mModel.clearSamples(); + } + + public JMenu getContextMenu() + { + JMenu menu = new JMenu( mLabel ); + + JMenu verticalMenu = new JMenu( "Vertical Zoom" ); + menu.add( verticalMenu ); + + verticalMenu.add( new VerticalZoomItem( this, "8", 8.0f ) ); + verticalMenu.add( new VerticalZoomItem( this, "4", 4.0f ) ); + verticalMenu.add( new VerticalZoomItem( this, "2", 2.0f ) ); + verticalMenu.add( new VerticalZoomItem( this, "1", 1.0f ) ); + verticalMenu.add( new VerticalZoomItem( this, ".5", 0.5f ) ); + verticalMenu.add( new VerticalZoomItem( this, ".25", 0.25f ) ); + + return menu; + } + + public SampleModel getModel() + { + return mModel; + } + + public int getSampleCount() + { + return getModel().getSampleCount(); + } + + public void setSampleCount( int sampleCount ) + { + getModel().setSampleCount( sampleCount ); + } + + public float getVerticalZoom() + { + return mVerticalZoom; + } + + public void setVerticalZoom( float zoom ) + { + mVerticalZoom = zoom; + repaint(); + } + + public class VerticalZoomItem extends JCheckBoxMenuItem + { + private TapViewPanel mPanel; + private float mZoom; + + public VerticalZoomItem( TapViewPanel panel, String label, float zoom ) + { + super( label ); + mPanel = panel; + mZoom = zoom; + + if( mPanel.getVerticalZoom() == zoom ) + { + setSelected( true ); + } + + addActionListener( new ActionListener() + { + @Override + public void actionPerformed( ActionEvent arg0 ) + { + mPanel.setVerticalZoom( mZoom ); + } + } ); + } + } +} diff --git a/src/instrument/gui/Viewer.java b/src/instrument/gui/Viewer.java new file mode 100644 index 00000000..7c152038 --- /dev/null +++ b/src/instrument/gui/Viewer.java @@ -0,0 +1,234 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package instrument.gui; + +import java.awt.EventQueue; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.io.File; + +import javax.swing.JDesktopPane; +import javax.swing.JFileChooser; +import javax.swing.JFrame; +import javax.swing.JMenu; +import javax.swing.JMenuBar; +import javax.swing.JMenuItem; +import javax.swing.JSeparator; +import javax.swing.filechooser.FileFilter; + +import log.Log; +import source.Source; +import source.wave.ComplexWaveSource; +import source.wave.FloatWaveSource; +import source.wave.WaveSource; + +public class Viewer +{ + private JFrame mFrame; + private JDesktopPane mDesktop; + + private Source mSource; + + public Viewer() + { + initGUI(); + + EventQueue.invokeLater( new Runnable() + { + public void run() + { + try + { + mFrame.setVisible( true ); + } + catch( Exception e ) + { + e.printStackTrace(); + } + } + } ); + } + + /** + * Initialize the contents of the frame. + */ + private void initGUI() + { + /* Setup main JFrame window */ + mFrame = new JFrame(); + mFrame.setTitle( "SDRTrunk - Sample File Viewer" ); + mFrame.setBounds( 100, 100, 800, 600 ); + mFrame.setDefaultCloseOperation( JFrame.EXIT_ON_CLOSE ); + + /* Multi-window desktop */ + mDesktop = new JDesktopPane(); + mFrame.add( mDesktop ); + +// JInternalFrame frame = +// new JInternalFrame( "New", true, true, true, true ); +// frame.add( new JLabel( "This is a test" ) ); +// frame.pack(); +// mDesktop.add( frame ); +// frame.setVisible( true ); + + /* Menu items */ + JMenuBar menuBar = new JMenuBar(); + mFrame.setJMenuBar( menuBar ); + + JMenu fileMenu = new JMenu( "File" ); + menuBar.add( fileMenu ); + + fileMenu.add( new SourceFileItem() ); + fileMenu.add( new JSeparator() ); + + JMenuItem exitMenu = new JMenuItem( "Exit" ); + exitMenu.addActionListener( new ActionListener() + { + public void actionPerformed( ActionEvent event ) + { + System.exit( 0 ); + } + } ); + + fileMenu.add( exitMenu ); + +// JMenuItem windowItem = new JMenuItem( "New Window" ); +// windowItem.addActionListener( new ActionListener() +// { +// @Override +// public void actionPerformed( ActionEvent arg0 ) +// { +// JInternalFrame frame = +// new JInternalFrame( "New", true, true, true, true ); +// +// frame.add( new JLabel( "This is a test" ) ); +// frame.pack(); +// +// mDesktop.add( frame ); +// +// frame.setVisible( true ); +// } +// } ); +// menuBar.add( windowItem ); + } + + private void setSourceFile( File file ) + { + Log.info( "File selected [" + file.getAbsolutePath() + "]"); + + WaveSource source = null; + + /* Attempt to open file as a 1-channel float source */ + try + { + FloatWaveSource floatSource = new FloatWaveSource( file, false ); + floatSource.open(); + + source = floatSource; + + Log.info( "File opened as float wave file" ); + } + catch( Exception e ) + { + Log.error( "Couldn't open file as float source - " + + e.getLocalizedMessage() ); + } + + if( source == null ) + { + /* Attempt to open file as a 2-channel complex source */ + try + { + ComplexWaveSource complex = new ComplexWaveSource( file, false ); + complex.open(); + + source = complex; + + Log.info( "File opened as complex wave file" ); + } + catch( Exception e ) + { + Log.error( "Couldn't open file as float source - " + + e.getLocalizedMessage() ); + } + } + + if( source != null ) + { + mSource = source; + + WaveSourceFrame sourcePanel = + new WaveSourceFrame( source, mDesktop ); + sourcePanel.setVisible( true ); + mDesktop.add( sourcePanel ); + } + } + + public static void main( String[] args ) + { + final Viewer viewer = new Viewer(); + } + + public class SourceFileItem extends JMenuItem + { + public SourceFileItem() + { + super( "Open" ); + + addActionListener( new ActionListener() + { + @Override + public void actionPerformed( ActionEvent arg0 ) + { + final JFileChooser fc = new JFileChooser(); + fc.setFileFilter( new FileFilter() + { + @Override + public boolean accept( File file ) + { + return file.isDirectory() || + file.getName().endsWith( ".wav" ); + } + + @Override + public String getDescription() + { + return "Float or Complex 16-bit Wave Files"; + } + } ); + + int val = fc.showOpenDialog( mFrame ); + + if( val == JFileChooser.APPROVE_OPTION ) + { + final File file = fc.getSelectedFile(); + + EventQueue.invokeLater( new Runnable() + { + @Override + public void run() + { + setSourceFile( file ); + } + } ); + } + } + } ); + } + } +} diff --git a/src/instrument/gui/WaveSourceFrame.java b/src/instrument/gui/WaveSourceFrame.java new file mode 100644 index 00000000..706d7c69 --- /dev/null +++ b/src/instrument/gui/WaveSourceFrame.java @@ -0,0 +1,242 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package instrument.gui; + +import java.awt.Dimension; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.awt.event.FocusEvent; +import java.awt.event.FocusListener; +import java.io.IOException; + +import javax.swing.DefaultComboBoxModel; +import javax.swing.JButton; +import javax.swing.JComboBox; +import javax.swing.JDesktopPane; +import javax.swing.JInternalFrame; +import javax.swing.JLabel; +import javax.swing.JOptionPane; +import javax.swing.JPanel; +import javax.swing.JTextField; + +import log.Log; +import net.miginfocom.swing.MigLayout; +import source.Source.SampleType; +import source.wave.WaveSource; +import source.wave.WaveSource.PositionListener; +import decode.Decoder; +import decode.DecoderType; +import decode.fleetsync2.Fleetsync2Decoder; +import decode.mdc1200.MDCDecoder; +import decode.mpt1327.MPT1327Decoder; + +public class WaveSourceFrame extends JInternalFrame implements PositionListener +{ + private WaveSource mSource; + private JDesktopPane mDesktop; + + private JLabel mCurrentPosition = new JLabel( "0" ); + private JComboBox mComboDecoders; + + public WaveSourceFrame( WaveSource source, JDesktopPane desktop ) + { + mSource = source; + mSource.addListener( this ); + + mDesktop = desktop; + + initGui(); + } + + private void initGui() + { + setTitle( "Wave Source [" + mSource.getSampleType().toString() + "]" ); + setPreferredSize( new Dimension( 600, 150 ) ); + setSize( 600, 150 ); + + setResizable( true ); + setClosable( true ); + setIconifiable( true ); + setMaximizable( false ); + + JPanel panel = new JPanel(); + panel.setLayout( new MigLayout() ); + + JLabel fileLabel = new JLabel( "File: " + mSource.getFile().getName() ); + fileLabel.setToolTipText( mSource.getFile().getName() ); + panel.add( fileLabel, "span,wrap" ); + + panel.add( new JLabel( "Jump:" ) ); + panel.add( new JumpToField( mSource ) ); + panel.add( new NextSampleButton( mSource, ">", 1 ) ); + panel.add( new NextSampleButton( mSource, "> 100 >", 100 ) ); + panel.add( new NextSampleButton( mSource, "> 1000 >", 1000 ) ); + panel.add( new JLabel( "Posn:" ) ); + panel.add( mCurrentPosition, "wrap" ); + + /* ComboBox: Decoders */ + mComboDecoders = new JComboBox(); + + DefaultComboBoxModel model = + new DefaultComboBoxModel(); + + for( DecoderType type: DecoderType.getInstrumentableDecoders() ) + { + model.addElement( type ); + } + + mComboDecoders.setModel( model ); + + panel.add( new JLabel( "Decoders:" ) ); + panel.add( mComboDecoders, "span 3,grow" ); + panel.add( new AddDecoderButton(), "grow" ); + + add( panel ); + } + + public class AddDecoderButton extends JButton + { + public AddDecoderButton() + { + super( "Add" ); + + addActionListener( new ActionListener() + { + @Override + public void actionPerformed( ActionEvent arg0 ) + { + DecoderType selected = + (DecoderType)mComboDecoders.getSelectedItem(); + + Log.info( "Selected:" + selected.getDisplayString() ); + + Decoder decoder = null; + + switch( selected ) + { + case FLEETSYNC2: + decoder = new Fleetsync2Decoder( null ); + break; + case MDC1200: + decoder = new MDCDecoder( null ); + break; + case MPT1327: + decoder = new MPT1327Decoder( SampleType.FLOAT, null ); + break; + } + + if( decoder != null ) + { + DecoderViewFrame decoderFrame = + new DecoderViewFrame( decoder, mSource ); + + decoderFrame.setVisible( true ); + mDesktop.add( decoderFrame ); + } + } + } ); + } + } + + public class NextSampleButton extends JButton + { + private WaveSource mSource; + private int mCount; + + public NextSampleButton( WaveSource source, String label, int count ) + { + super( label ); + mSource = source; + mCount = count; + + addActionListener( new ActionListener() + { + @Override + public void actionPerformed( ActionEvent arg0 ) + { + try + { + mSource.next( mCount ); + } + catch ( IOException e ) + { + Log.error( "Viewer - error trying to fetch next [" + + mCount + "] samples - " + e.getLocalizedMessage() ); + + JOptionPane.showMessageDialog( WaveSourceFrame.this, + "Cannot read " + mCount + " more samples [" + + e.getLocalizedMessage() + "]", + "Wave File Error", + JOptionPane.ERROR_MESSAGE ); + } + } + } ); + } + } + + public class JumpToField extends JTextField + { + private WaveSource mSource; + + public JumpToField( WaveSource source ) + { + super( "0" ); + + mSource = source; + + setMinimumSize( new Dimension( 100, getHeight() ) ); + + addFocusListener( new FocusListener() + { + @Override + public void focusGained( FocusEvent arg0 ) {} + + @Override + public void focusLost( FocusEvent arg0 ) + { + try + { + long position = Long.parseLong( getText() ); + mSource.jumpTo( position ); + } + catch( Exception e ) + { + Log.error( "WaveSourceFrame - exception during jump " + + "to - " + e.getLocalizedMessage() ); + + JOptionPane.showMessageDialog( WaveSourceFrame.this, + "Can't jump to position [" + getText() + "]", + "Error", + JOptionPane.ERROR_MESSAGE ); + } + } + } ); + } + } + + @Override + public void positionUpdated( long position, boolean reset ) + { + mCurrentPosition.setText( String.valueOf( position ) ); + + if( reset ) + { + + } + } +} diff --git a/src/instrument/tap/Tap.java b/src/instrument/tap/Tap.java new file mode 100644 index 00000000..c0d29c05 --- /dev/null +++ b/src/instrument/tap/Tap.java @@ -0,0 +1,96 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package instrument.tap; + +import java.util.ArrayList; + +public abstract class Tap +{ + protected TapType mType; + protected String mName; + protected int mDelay; + + protected ArrayList mListeners = new ArrayList(); + + /** + * Instrumentation tap. Provides a tap into a data stream or event stream + * within a process, so that registered listeners can be notified every + * time a new data sample is availabe, or an event occurrs. + * + * @param name - displayable name to use for the tap + * + * @param delay - indicates the number of delay units from when the data + * begins the process until this event occurs. When this data or event is + * plotted on a flow graph, the delay value will be used to adjust placement + * on the display to correctly align events. + */ + public Tap( TapType type, String name, int delay ) + { + mType = type; + mName = name; + mDelay = delay; + } + + public TapType getType() + { + return mType; + } + + /** + * Display name for this tap + */ + public String getName() + { + return mName; + } + + /** + * Delay for this tap + */ + public int getDelay() + { + return mDelay; + } + + /** + * Registers a listener for data or events produced by this tap + */ + public void addListener( TapListener listener ) + { + if( !mListeners.contains( listener ) ) + { + mListeners.add( listener ); + } + } + + /** + * Removes the listener from receiving data or events from this tap + */ + public void removeListener( TapListener listener ) + { + mListeners.remove( listener ); + } + + /** + * Number of listeners currently registered on this tap + */ + public int getListenerCount() + { + return mListeners.size(); + } +} diff --git a/src/instrument/tap/TapListener.java b/src/instrument/tap/TapListener.java new file mode 100644 index 00000000..f6fdd25e --- /dev/null +++ b/src/instrument/tap/TapListener.java @@ -0,0 +1,23 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package instrument.tap; + +public interface TapListener +{ + public void receive( T t ); +} diff --git a/src/instrument/tap/TapType.java b/src/instrument/tap/TapType.java new file mode 100644 index 00000000..b9bb9ed3 --- /dev/null +++ b/src/instrument/tap/TapType.java @@ -0,0 +1,27 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package instrument.tap; + +public enum TapType +{ + EVENT_SYNC_DETECT, + STREAM_BINARY, + STREAM_COMPLEX, + STREAM_FLOAT, + STREAM_SYMBOL; +} diff --git a/src/instrument/tap/event/SyncDetectTap.java b/src/instrument/tap/event/SyncDetectTap.java new file mode 100644 index 00000000..5375a76b --- /dev/null +++ b/src/instrument/tap/event/SyncDetectTap.java @@ -0,0 +1,60 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package instrument.tap.event; + +import instrument.tap.Tap; +import instrument.tap.TapListener; +import instrument.tap.TapType; +import dsp.SyncDetectListener; +import dsp.SyncDetectProvider; + +public class SyncDetectTap extends Tap + implements SyncDetectListener, + SyncDetectProvider +{ + private SyncDetectListener mListener; + + /** + * Sync detect event tap. Indicates when message sync word has been + * detected within a binary stream of data. + */ + public SyncDetectTap() + { + super( TapType.EVENT_SYNC_DETECT, "Sync Detected", 0 ); + } + + @Override + public void syncDetected() + { + if( mListener != null ) + { + mListener.syncDetected(); + } + + for( TapListener listener: mListeners ) + { + listener.receive( getName() ); + } + } + + @Override + public void setSyncDetectListener( SyncDetectListener listener ) + { + mListener = listener; + } +} diff --git a/src/instrument/tap/stream/BinaryTap.java b/src/instrument/tap/stream/BinaryTap.java new file mode 100644 index 00000000..030fcf3d --- /dev/null +++ b/src/instrument/tap/stream/BinaryTap.java @@ -0,0 +1,59 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package instrument.tap.stream; + +import instrument.tap.TapListener; +import instrument.tap.TapType; +import sample.Listener; + +public class BinaryTap extends StreamTap implements Listener + +{ + private Listener mListener; + + public BinaryTap( String name, + int delay, + float sampleRateRatio ) + { + super( TapType.STREAM_BINARY, name, delay, sampleRateRatio ); + } + + @Override + public void receive( Boolean bit ) + { + if( mListener != null ) + { + mListener.receive( bit ); + } + + for( TapListener listener: mListeners ) + { + listener.receive( bit ); + } + } + + public void setListener( Listener listener ) + { + mListener = listener; + } + + public void removeListener( Listener listener ) + { + mListener = null; + } +} diff --git a/src/instrument/tap/stream/ComplexTap.java b/src/instrument/tap/stream/ComplexTap.java new file mode 100644 index 00000000..26f89214 --- /dev/null +++ b/src/instrument/tap/stream/ComplexTap.java @@ -0,0 +1,60 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package instrument.tap.stream; + +import instrument.tap.TapListener; +import instrument.tap.TapType; +import sample.Listener; +import sample.complex.ComplexSample; + +public class ComplexTap extends StreamTap + implements Listener +{ + private Listener mListener; + + public ComplexTap( String name, + int delay, + float sampleRateRatio ) + { + super( TapType.STREAM_COMPLEX, name, delay, sampleRateRatio ); + } + + @Override + public void receive( ComplexSample t ) + { + if( mListener != null ) + { + mListener.receive( t ); + } + + for( TapListener listener: mListeners ) + { + listener.receive( t ); + } + } + + public void setListener( Listener listener ) + { + mListener = listener; + } + + public void removeListener( Listener listener ) + { + mListener = null; + } +} diff --git a/src/instrument/tap/stream/FloatTap.java b/src/instrument/tap/stream/FloatTap.java new file mode 100644 index 00000000..15540dbf --- /dev/null +++ b/src/instrument/tap/stream/FloatTap.java @@ -0,0 +1,58 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package instrument.tap.stream; + +import instrument.tap.TapListener; +import instrument.tap.TapType; +import sample.Listener; + +public class FloatTap extends StreamTap implements Listener +{ + private Listener mListener; + + public FloatTap( String name, + int delay, + float sampleRateRatio ) + { + super( TapType.STREAM_FLOAT, name, delay, sampleRateRatio ); + } + + @Override + public void receive( Float sample ) + { + if( mListener != null ) + { + mListener.receive( sample ); + } + + for( TapListener listener: mListeners ) + { + listener.receive( sample ); + } + } + + public void setListener( Listener listener ) + { + mListener = listener; + } + + public void removeListener( Listener listener ) + { + mListener = null; + } +} diff --git a/src/instrument/tap/stream/StreamTap.java b/src/instrument/tap/stream/StreamTap.java new file mode 100644 index 00000000..28ed549c --- /dev/null +++ b/src/instrument/tap/stream/StreamTap.java @@ -0,0 +1,41 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package instrument.tap.stream; + +import instrument.tap.Tap; +import instrument.tap.TapType; + +public abstract class StreamTap extends Tap +{ + protected float mSampleRateRatio; + + public StreamTap( TapType type, + String name, + int delay, + float sampleRate ) + { + super( type, name, delay ); + + mSampleRateRatio = sampleRate; + } + + public float getSampleRateRatio() + { + return mSampleRateRatio; + } +} diff --git a/src/instrument/tap/stream/SymbolEventTap.java b/src/instrument/tap/stream/SymbolEventTap.java new file mode 100644 index 00000000..a5b05185 --- /dev/null +++ b/src/instrument/tap/stream/SymbolEventTap.java @@ -0,0 +1,58 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package instrument.tap.stream; + +import instrument.tap.TapListener; +import instrument.tap.TapType; +import sample.Listener; +import dsp.fsk.SymbolEvent; + +public class SymbolEventTap extends StreamTap implements Listener +{ + private Listener mListener; + + public SymbolEventTap( String name, int delay, float sampleRate ) + { + super( TapType.STREAM_SYMBOL, name, delay, sampleRate ); + } + + @Override + public void receive( SymbolEvent symbolEvent ) + { + if( mListener != null ) + { + mListener.receive( symbolEvent ); + } + + for( TapListener listener: mListeners ) + { + listener.receive( symbolEvent ); + } + } + + public void setListener( Listener listener ) + { + mListener = listener; + } + + public void removeListener( Listener listener ) + { + mListener = null; + } + +} diff --git a/src/log/Log.java b/src/log/Log.java new file mode 100644 index 00000000..058b09fc --- /dev/null +++ b/src/log/Log.java @@ -0,0 +1,233 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package log; + +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.OutputStreamWriter; +import java.io.Writer; +import java.nio.file.Files; +import java.nio.file.Path; + +import properties.SystemProperties; +import util.TimeStamp; + +public class Log +{ + private static Writer mLogFile; + + public Log() + { + } + + private static void log( String msg ) + { + log( msg, null); + } + + /** + * Logs just the raw message, without a preceeding timestamp + */ + public void logRaw( String rawmsg ) + { + try + { + mLogFile.write( rawmsg ); + } + catch ( IOException e ) + { + System.out.println( + "Couldn't write warning msg to log file: " + rawmsg ); + } + } + + private static void log( String msg, String label) + { + if( mLogFile != null ) + { + try + { + mLogFile.write( TimeStamp.getTimeStamp( " " ) ); + mLogFile.write( " " ); + + if( label != null ) + { + mLogFile.write( label + ":" ); + } + + mLogFile.write( msg + "\n" ); + mLogFile.flush(); + + } + catch (IOException e) + { + System.out.println( + "Couldn't write warning msg to log file: " + msg ); + } + } + + //echo to console + System.out.println( ( label == null ? "" : label + ": " ) + msg ); + } + + public static void header( String name ) + { + StringBuilder sb = new StringBuilder(); + + sb.append( "********** " ); + sb.append( name ); + sb.append( " **********" ); + + log( sb.toString() ); + } + + public static void info( String msg ) + { + log( msg ); + } + + public static void info( byte[] bytes ) + { + StringBuilder sb = new StringBuilder(); + + for( int x = 0; x < bytes.length; x++ ) + { + sb.append( String.format( "%02X ", bytes[ x ] ) ); + } + + info( sb.toString() ); + } + + public static void info( String header, byte[] bytes ) + { + StringBuilder sb = new StringBuilder(); + + sb.append( header ); + + for( int x = 0; x < bytes.length; x++ ) + { + sb.append( String.format( "%02X ", bytes[ x ] ) ); + } + + info( sb.toString() ); + } + + public static void warning( String msg ) + { + log( msg, "WARNING" ); + } + + public static void error( String msg ) + { + log( msg, "ERROR" ); + } + + public static void debug( String msg ) + { + log( msg, "DEBUG" ); + } + + /** + * Initializes logging to the specified filename + */ + public static void init( String filename ) + { + try + { + mLogFile = new OutputStreamWriter( + new FileOutputStream( filename ) ); + + Log.info( "SDRTrunk Multi-Channel Trunking Decoder Application Log" ); + Log.info( "Log - application logging started [" + filename + "]" ); + + logUserEnvironment(); + } + catch (FileNotFoundException e) + { + System.out.println("Couldn't open log file:" + filename ); + } + } + + /** + * Initializes the log file using system properties settings + */ + public static void init() + { + SystemProperties props = SystemProperties.getInstance(); + + String logBaseFileName = props.get( "log.basefilename", + "_SDRTrunk_application" ); + + Path logPath = props.getApplicationFolder( "logs" ); + + if( !Files.exists( logPath ) ) + { + try + { + Files.createDirectory( logPath ); + } + catch ( IOException e ) + { + Log.error( "Log - couldn't create 'logs' subdirectory in the " + + "SDRTrunk application directory" ); + } + } + + String logFileName = TimeStamp.getTimeStamp( "_" ) + logBaseFileName + + ".log"; + + Path logFile = logPath.resolve( logFileName ); + + init( logFile.toString() ); + } + + public static void logUserEnvironment() + { + Log.header( "User Environment" ); + Log.info( "OS:\t\t\t" + System.getProperty( "os.name" ) ); + Log.info( "OS Version:\t\t" + System.getProperty( "os.version" ) ); + Log.info( "OS Arch:\t\t" + System.getProperty( "os.arch" ) ); + Log.info( "Java Vendor:\t\t" + System.getProperty( "java.vendor" ) ); + Log.info( "Java Vendor URL:\t" + System.getProperty( "java.vendor.url" ) ); + Log.info( "Java Version:\t\t" + System.getProperty( "java.version" ) ); + Log.info( "User:\t\t\t" + System.getProperty( "user.name" ) ); + Log.info( "User Home:\t\t" + System.getProperty( "user.home" ) ); + Log.info( "User Directory:\t\t" + System.getProperty( "user.dir" ) ); + + Log.header( "Java Class Path" ); + Log.info( System.getProperty( "java.class.path" ) ); + + } + + public static void close() + { + if( mLogFile != null ) + { + try + { + mLogFile.flush(); + mLogFile.close(); + } + catch( Exception e ) + { + System.out.println( "Couldn't close log file" ); + } + } + } +} diff --git a/src/map/DefaultIcon.java b/src/map/DefaultIcon.java new file mode 100644 index 00000000..0be8dd87 --- /dev/null +++ b/src/map/DefaultIcon.java @@ -0,0 +1,36 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package map; + +import settings.Setting; + +public class DefaultIcon extends Setting +{ + public DefaultIcon( String name ) + { + super( name ); + } + + /** + * Don't use this constructor. This is used by JAXB to unmarshall saved + * map icons. + */ + public DefaultIcon() + { + } +} diff --git a/src/map/IconManager.java b/src/map/IconManager.java new file mode 100644 index 00000000..13697c99 --- /dev/null +++ b/src/map/IconManager.java @@ -0,0 +1,494 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package map; + +import java.awt.Component; +import java.awt.Dimension; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.awt.event.MouseEvent; +import java.awt.event.MouseListener; +import java.io.File; +import java.util.Arrays; + +import javax.swing.JButton; +import javax.swing.JFileChooser; +import javax.swing.JFrame; +import javax.swing.JLabel; +import javax.swing.JList; +import javax.swing.JMenuItem; +import javax.swing.JOptionPane; +import javax.swing.JPanel; +import javax.swing.JPopupMenu; +import javax.swing.JScrollPane; +import javax.swing.JSplitPane; +import javax.swing.JTextArea; +import javax.swing.JTextField; +import javax.swing.event.ListSelectionEvent; +import javax.swing.event.ListSelectionListener; +import javax.swing.filechooser.FileFilter; + +import log.Log; +import net.miginfocom.swing.MigLayout; +import settings.Setting; +import settings.SettingChangeListener; +import controller.ResourceManager; + +public class IconManager extends JFrame + implements ActionListener, SettingChangeListener + +{ + public enum Mode { NORMAL, NEW, EDIT } + + private static final long serialVersionUID = 1L; + + private ResourceManager mResourceManager; + + private static final String sMSG_NEW = "Type a name for the new icon, " + + "select the file, and click Save,\nor Cancel to reset"; + private static final String sMSG_EDIT = "Change the name for the selected \n" + + "icon and/or the file, and click Save\nto update, or Cancel to reset"; + private static final String sMSG_NORMAL = "Select an icon to edit, or \n" + + "click New to add an icon"; + + private JTextField mIconName; + private JTextField mIconPath; + private JButton mButtonLeft = new JButton( "New" ); + private JButton mButtonRight = new JButton( "Edit" ); + private JButton mButtonFile = new JButton( "File" ); + private JTextArea mInstructionLabel = new JTextArea(); + private JList mIconList; + private JFileChooser mFileChooser; + private Mode mMode = Mode.NORMAL; + + public IconManager( ResourceManager resourceManager, Component displayOver ) + { + mResourceManager = resourceManager; + + init( displayOver ); + } + + private void init( Component displayOver ) + { + setTitle( "Icon Manager" ); + setLocationRelativeTo( displayOver ); + setSize( 400, 400 ); + setDefaultCloseOperation( JFrame.DISPOSE_ON_CLOSE ); + setLayout( new MigLayout( "", "[grow,fill]", "[grow,fill]" ) ); + + /** + * Editor Panel + */ + JPanel editorPanel = new JPanel(); + editorPanel.setLayout( new MigLayout( "", "", "[][][][grow,fill]" ) ); + + mButtonFile.addActionListener( this ); + editorPanel.add( mButtonFile, "align right" ); + + mIconPath = new JTextField(); + editorPanel.add( mIconPath, "grow,wrap" ); + + editorPanel.add( new JLabel( "Name:" ), "align right" ); + + mIconName = new JTextField(); + editorPanel.add( mIconName, "grow,wrap" ); + + mButtonLeft.addActionListener( this ); + editorPanel.add( mButtonLeft ); + + mButtonRight.addActionListener( this ); + mButtonRight.setEnabled( false ); + editorPanel.add( mButtonRight, "wrap" ); + + mInstructionLabel.setText( sMSG_NORMAL ); + mInstructionLabel.setRows( 3 ); + mInstructionLabel.setBackground( getBackground() ); + editorPanel.add( mInstructionLabel, "span,grow,wrap" ); + + /** + * MapIcon list + */ + MapIcon[] icons = mResourceManager.getSettingsManager().getMapIcons(); + Arrays.sort( icons ); + + mIconList = new JList( icons ); + + MapIconListCellRenderer renderer = new MapIconListCellRenderer(); + renderer.setPreferredSize( new Dimension( 200, 30 ) ); + mIconList.setCellRenderer( renderer ); + + mIconList.addListSelectionListener( new ListSelectionListener() + { + @Override + public void valueChanged( ListSelectionEvent event ) + { + /** + * Refresh the display by setting the mode to normal. If we + * were in editing or new mode, and the user clicks in the list + * throw away the changes and set the mode to normal + */ + setMode( Mode.NORMAL ); + } + } ); + + mIconList.addMouseListener( new MouseListener() + { + @Override + public void mouseClicked( MouseEvent event ) + { + if( event.getButton() == MouseEvent.BUTTON3 ) + { + //Select the item + mIconList.setSelectedIndex( + mIconList.locationToIndex( event.getPoint() ) ); + + //Determine if it can be deleted + final MapIcon icon = mIconList.getSelectedValue(); + + if( icon != null ) + { + JPopupMenu context = new JPopupMenu(); + + JMenuItem setDefault = new JMenuItem( "Set As Default" ); + + setDefault.addActionListener( new ActionListener() + { + @Override + public void actionPerformed( ActionEvent e ) + { + mResourceManager.getSettingsManager() + .setDefaultIcon( icon ); + + refreshList(); + } + } ); + + context.add( setDefault ); + + if( icon.isEditable() ) + { + JMenuItem deleteItem = new JMenuItem( "Delete Icon" ); + deleteItem.addActionListener( new ActionListener() + { + @Override + public void actionPerformed( ActionEvent arg0 ) + { + mResourceManager.getSettingsManager() + .deleteMapIcon( icon ); + } + } ); + + context.add( deleteItem ); + } + + context.show( mIconList, event.getX(), event.getY() ); + } + } + } + + //Unused + public void mouseEntered( MouseEvent arg0 ) {} + public void mouseExited( MouseEvent arg0 ) {} + public void mousePressed( MouseEvent arg0 ) {} + public void mouseReleased( MouseEvent arg0 ) {} + } ); + + JScrollPane scroller = new JScrollPane( mIconList ); + + /** + * Splitpane to hold editor and icon list + */ + JSplitPane splitPane = new JSplitPane( JSplitPane.VERTICAL_SPLIT, + scroller, + editorPanel ); + splitPane.setResizeWeight( 0.7D ); + + add( splitPane ); + } + + private void setMode( Mode mode ) + { + MapIcon selected = mIconList.getSelectedValue(); + + switch( mode ) + { + case NORMAL: + setTitle( "Icon Manager" ); + mButtonLeft.setText( "New" ); + mButtonRight.setText( "Edit" ); + + if( selected != null ) + { + if( selected.isEditable() ) + { + mButtonRight.setEnabled( true ); + mIconName.setText( selected.getName() ); + mIconPath.setText( selected.getPath() ); + } + else + { + mButtonRight.setEnabled( false ); + mIconName.setText( selected.getName() + " [NO EDIT]" ); + mIconPath.setText( null ); + } + } + else + { + mButtonRight.setEnabled( false ); + mIconName.setText( null ); + mIconPath.setText( null ); + } + mInstructionLabel.setText( sMSG_NORMAL ); + break; + case EDIT: + if( selected != null ) + { + setTitle( "Icon Manager - editing [" + selected.getName() + "]" ); + } + else + { + setTitle( "Icon Manager - edit [*]" ); + } + mButtonLeft.setText( "Save" ); + mButtonRight.setText( "Cancel" ); + mButtonRight.setEnabled( true ); + mInstructionLabel.setText( sMSG_EDIT ); + break; + case NEW: + setTitle( "Icon Manager - add new icon" ); + mButtonLeft.setText( "Save" ); + mButtonRight.setText( "Cancel" ); + mButtonRight.setEnabled( true ); + mIconName.setText( null ); + mIconPath.setText( null ); + mInstructionLabel.setText( sMSG_NEW ); + break; + } + + mMode = mode; + + repaint(); + } + + public void actionPerformed(ActionEvent e) + { + if( e.getSource() == mButtonFile ) + { + if ( mFileChooser == null ) + { + mFileChooser = new JFileChooser(); + + //Add a custom file filter and disable the default + mFileChooser.addChoosableFileFilter( new ImageFilter() ); + mFileChooser.setAcceptAllFileFilterUsed( false ); + } + + int returnVal = mFileChooser.showDialog( this, "Select" ); + + if ( returnVal == JFileChooser.APPROVE_OPTION ) + { + File file = mFileChooser.getSelectedFile(); + + if( file != null ) + { + mIconPath.setText( file.getAbsolutePath() ); + } + } + + //Reset the file chooser for the next time it's shown. + mFileChooser.setSelectedFile( null ); + } + else if( e.getSource() == mButtonLeft ) + { + switch( mMode ) + { + case EDIT: //Save + save(); + break; + case NEW: //Save + save(); + break; + case NORMAL: + setMode( Mode.NEW ); + break; + } + } + else if( e.getSource() == mButtonRight ) + { + switch( mMode ) + { + case EDIT: //Cancel + case NEW: //Cancel + Log.info( "Right clicked - cancel" ); + setMode( Mode.NORMAL ); + break; + case NORMAL: //Edit + Log.info( "Right clicked - edit" ); + setMode( Mode.EDIT ); + break; + } + } + } + + private void refreshList() + { + MapIcon[] icons = mResourceManager.getSettingsManager().getMapIcons(); + + Arrays.sort( icons ); + + mIconList.setListData( icons ); + + repaint(); + } + + private void save() + { + String name = mIconName.getText(); + String path = mIconPath.getText(); + + switch( mMode ) + { + case NEW: + if( name != null && !name.isEmpty() ) + { + if( path != null && !path.isEmpty() ) + { + mResourceManager.getSettingsManager() + .addMapIcon( new MapIcon( name, path, true ) ); + } + else + { + JOptionPane.showMessageDialog( this, + "Please select an image file", + "Filename and path missing", + JOptionPane.ERROR_MESSAGE ); + } + } + else + { + JOptionPane.showMessageDialog( this, + "Please type a name for the new icon", + "Name missing", + JOptionPane.ERROR_MESSAGE ); + } + break; + + case EDIT: + MapIcon selected = mIconList.getSelectedValue(); + + if( name != null && !name.isEmpty() ) + { + if( path != null && !path.isEmpty() ) + { + selected.setName( name ); + selected.setPath( path ); + mResourceManager.getSettingsManager() + .updatMapIcon( selected, name, path ); + } + else + { + JOptionPane.showMessageDialog( this, + "Please select an image file", + "Filename and path missing", + JOptionPane.ERROR_MESSAGE ); + } + } + else + { + JOptionPane.showMessageDialog( this, + "Please type a name for the new icon", + "Name missing", + JOptionPane.ERROR_MESSAGE ); + } + + break; + } + + } + + public class ImageFilter extends FileFilter + { + //Accept all directories and all gif, jpg, tiff, or png files. + public boolean accept(File f) + { + if ( f.isDirectory() ) + { + return true; + } + + String extension = getExtension( f ); + + if (extension != null) + { + if (extension.equals( "tiff" ) || + extension.equals( "tif" ) || + extension.equals( "gif" ) || + extension.equals( "jpeg" ) || + extension.equals( "jpg" ) || + extension.equals( "png" ) ) + { + return true; + } + else + { + return false; + } + } + + return false; + } + + //The description of this filter + public String getDescription() + { + return "Images (*.gif,*.jpg,*.png."; + } + + public String getExtension( File f ) + { + String ext = null; + String s = f.getName(); + int i = s.lastIndexOf('.'); + + if (i > 0 && i < s.length() - 1) { + ext = s.substring(i+1).toLowerCase(); + } + return ext; + } + } + + @Override + public void settingChanged( Setting setting ) + { + if( setting instanceof MapIcon ) + { + refreshList(); + setMode( Mode.NORMAL ); + } + } + + @Override + public void settingDeleted( Setting setting ) + { + if( setting instanceof MapIcon ) + { + refreshList(); + setMode( Mode.NORMAL ); + } + } +} diff --git a/src/map/MapIcon.java b/src/map/MapIcon.java new file mode 100644 index 00000000..ffe3bf0e --- /dev/null +++ b/src/map/MapIcon.java @@ -0,0 +1,188 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package map; + +import java.awt.Image; + +import javax.swing.ImageIcon; +import javax.xml.bind.annotation.XmlAttribute; +import javax.xml.bind.annotation.XmlTransient; + +import settings.Setting; + +public class MapIcon extends Setting implements Comparable +{ + private static final int sMAX_IMAGE_DIMENSION = 48; + private String mPath; + private ImageIcon mImageIcon; + + /** + * Only map icons created at runtime can be marked as non-editable, and + * therefore this property is transient. + */ + @XmlTransient + private boolean mEditable; + + @XmlTransient + private boolean mDefaultIcon; + + /** + * Wrapper class for a map icon. + * @param name - name of the icon - also used as key to lookup the icon + * @param path - file path to the icon + * @param editable - defines if the map icon or details can be edited + * + * Note: the default icons are constructed with editable = false, so that + * they cannot be deleted from the Icon Manager editor window + */ + public MapIcon( String name, String path, boolean editable ) + { + super( name ); + mPath = path; + mEditable = editable; + } + + public MapIcon( String name, String path ) + { + this( name, path, true ); + } + + /** + * Don't use this constructor. This is used by JAXB to unmarshall saved + * map icons. + */ + public MapIcon() + { + mEditable = true; + } + + + @XmlAttribute( name="editable" ) + public boolean isEditable() + { + return mEditable; + } + + @XmlAttribute( name="default" ) + public boolean isDefaultIcon() + { + return mDefaultIcon; + } + + public void setDefaultIcon( boolean isDefault ) + { + mDefaultIcon = isDefault; + } + + public ImageIcon getImageIcon() + { + if( mImageIcon == null ) + { + mImageIcon = new ImageIcon( mPath ); + + /** + * If the image is too big, scale it down to max pixel size squared + */ + if( mImageIcon.getIconWidth() > sMAX_IMAGE_DIMENSION || + mImageIcon.getIconHeight() > sMAX_IMAGE_DIMENSION ) + { + /** + * getScaled instance will correct any negative value to the + * correct value, maintaining original aspect ratio. So, we + * only scale the larger value, and allow the image class to + * determine the correct value for the other measurement + */ + int height = -1; + int width = -1; + + /** + * Use the larger width or height value to determine the + * scaling factor + */ + if( mImageIcon.getIconHeight() > mImageIcon.getIconWidth() ) + { + height = sMAX_IMAGE_DIMENSION; + } + else + { + width = sMAX_IMAGE_DIMENSION; + } + + mImageIcon = new ImageIcon( mImageIcon.getImage() + .getScaledInstance( width, height, Image.SCALE_SMOOTH ) ); + } + + } + + return mImageIcon; + } + + @XmlAttribute( name="path" ) + public String getPath() + { + return mPath; + } + + public void setPath( String path ) + { + mPath = path; + } + + public String toString() + { + if( mDefaultIcon ) + { + return getName() + " (default)"; + } + else + { + return getName(); + } + } + + @Override + public boolean equals( Object obj ) + { + if( obj instanceof MapIcon ) + { + MapIcon other = (MapIcon)obj; + + return other.getName().contentEquals( getName() ) && + other.getPath().contentEquals( getPath() ); + } + else + { + return false; + } + } + + @Override + public int hashCode() + { + return getName().hashCode() + getPath().hashCode(); + } + + /** + * Sort order is determined by the icon name + */ + @Override + public int compareTo( MapIcon other ) + { + return getName().compareTo( other.getName() ); + } +} diff --git a/src/map/MapIconListCellRenderer.java b/src/map/MapIconListCellRenderer.java new file mode 100644 index 00000000..2e0508fe --- /dev/null +++ b/src/map/MapIconListCellRenderer.java @@ -0,0 +1,57 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package map; + +import java.awt.Component; + +import javax.swing.JLabel; +import javax.swing.JList; +import javax.swing.ListCellRenderer; + +public class MapIconListCellRenderer extends JLabel + implements ListCellRenderer +{ + private static final long serialVersionUID = 1L; + + public MapIconListCellRenderer() + { + setOpaque( true ); + } + + @Override + public Component getListCellRendererComponent( + JList list, MapIcon value, int index, + boolean isSelected, boolean cellHasFocus ) + { + setIcon( value.getImageIcon() ); + setText( value.toString() ); + + if ( isSelected ) + { + setBackground( list.getSelectionBackground() ); + setForeground( list.getSelectionForeground() ); + } + else + { + setBackground( list.getBackground() ); + setForeground( list.getForeground() ); + } + + return this; + } +} diff --git a/src/map/MapMouseListener.java b/src/map/MapMouseListener.java new file mode 100644 index 00000000..96e5b007 --- /dev/null +++ b/src/map/MapMouseListener.java @@ -0,0 +1,183 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package map; + +import java.awt.Cursor; +import java.awt.Point; +import java.awt.Rectangle; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.awt.event.MouseEvent; +import java.awt.event.MouseWheelListener; +import java.awt.geom.Point2D; + +import javax.swing.JMenu; +import javax.swing.JMenuItem; +import javax.swing.JPopupMenu; +import javax.swing.SwingUtilities; +import javax.swing.event.MouseInputAdapter; + +import log.Log; + +import org.jdesktop.swingx.JXMapViewer; +import org.jdesktop.swingx.mapviewer.GeoPosition; + +import settings.SettingsManager; +import controller.ResourceManager; + +public class MapMouseListener extends MouseInputAdapter implements MouseWheelListener +{ + private JXMapViewer mJXMapViewer; + private SettingsManager mSettingsManager; + private Point mPreviousPoint; + private Point mCurrentPoint; + + public MapMouseListener( JXMapViewer viewer, SettingsManager settingsManager ) + { + mJXMapViewer = viewer; + mSettingsManager = settingsManager; + } + + @Override + public void mouseDragged( MouseEvent event ) + { + if ( !SwingUtilities.isLeftMouseButton( event ) ) + { + return; + } + + Point current = event.getPoint(); + + double x = mJXMapViewer.getCenter().getX() - + ( current.getX() - mPreviousPoint.getX() ); + + double y = mJXMapViewer.getCenter().getY() - + (current.getY() - mPreviousPoint.getY() ); + + if ( !mJXMapViewer.isNegativeYAllowed() ) + { + if ( y < 0 ) + { + y = 0; + } + } + + int maxHeight = (int)( mJXMapViewer.getTileFactory() + .getMapSize( mJXMapViewer.getZoom() ).getHeight() * + mJXMapViewer.getTileFactory() + .getTileSize( mJXMapViewer.getZoom() ) ); + + if (y > maxHeight) + { + y = maxHeight; + } + + mPreviousPoint = current; + + mJXMapViewer.setCenter( new Point2D.Double( x, y ) ); + + mJXMapViewer.repaint(); + + /* Set cursor to dragging */ + mJXMapViewer.setCursor( Cursor.getPredefinedCursor( Cursor.MOVE_CURSOR ) ); + } + + @Override + public void mouseReleased( MouseEvent event ) + { + if ( !SwingUtilities.isLeftMouseButton( event ) ) + { + return; + } + + mPreviousPoint = null; + + /* Reset the curson */ + mJXMapViewer.setCursor( + Cursor.getPredefinedCursor( Cursor.DEFAULT_CURSOR ) ); + } + + @Override + public void mouseEntered( MouseEvent event ) + { + SwingUtilities.invokeLater( new Runnable() + { + @Override + public void run() + { + mJXMapViewer.requestFocusInWindow(); + } + }); + } + + @Override + public void mousePressed( MouseEvent event ) + { + mCurrentPoint = event.getPoint(); + mPreviousPoint = event.getPoint(); + + boolean left = SwingUtilities.isLeftMouseButton( event ); + + boolean middle = SwingUtilities.isMiddleMouseButton( event ); + + boolean right = SwingUtilities.isRightMouseButton( event ); + + boolean doubleClick = ( event.getClickCount() == 2 ); + + if (middle || ( left && doubleClick ) ) + { + recenterMap( event ); + } + else if( right ) + { + JPopupMenu popup = new JPopupMenu(); + + JMenuItem mapViewItem = new JMenuItem( "Set Default Location & Zoom" ); + mapViewItem.addActionListener( new ActionListener() + { + @Override + public void actionPerformed( ActionEvent arg0 ) + { + GeoPosition position = mJXMapViewer + .convertPointToGeoPosition( mCurrentPoint ); + + mSettingsManager.setMapViewSetting( "Default", position, + mJXMapViewer.getZoom() ); + } + } ); + popup.add( mapViewItem ); + + popup.show( mJXMapViewer, event.getX(), event.getY() ); + } + } + + private void recenterMap( MouseEvent event ) + { + Rectangle bounds = mJXMapViewer.getViewportBounds(); + + double x = bounds.getX() + event.getX(); + + double y = bounds.getY() + event.getY(); + + mJXMapViewer.setCenter( new Point2D.Double( x, y ) ); + + mJXMapViewer.setZoom( mJXMapViewer.getZoom() - 1 ); + + mJXMapViewer.repaint(); + } +} diff --git a/src/map/MapPanel.java b/src/map/MapPanel.java new file mode 100644 index 00000000..a5a29ec7 --- /dev/null +++ b/src/map/MapPanel.java @@ -0,0 +1,148 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package map; + +import java.awt.EventQueue; + +import javax.swing.JPanel; + +import net.miginfocom.swing.MigLayout; + +import org.jdesktop.swingx.JXMapViewer; +import org.jdesktop.swingx.OSMTileFactoryInfo; +import org.jdesktop.swingx.input.PanKeyListener; +import org.jdesktop.swingx.input.ZoomMouseWheelListenerCursor; +import org.jdesktop.swingx.mapviewer.DefaultTileFactory; +import org.jdesktop.swingx.mapviewer.GeoPosition; +import org.jdesktop.swingx.mapviewer.TileFactoryInfo; + +import settings.MapViewSetting; +import settings.SettingsManager; +import controller.ResourceManager; + +public class MapPanel extends JPanel implements PlottableUpdateListener +{ + private static final long serialVersionUID = 1L; + + private SettingsManager mSettingsManager; + private MapService mMapService; + private JXMapViewer mMapViewer = new JXMapViewer(); + private PlottableEntityPainter mMapPainter; + + public MapPanel( ResourceManager resourceManager ) + { + mSettingsManager = resourceManager.getSettingsManager(); + mMapService = resourceManager.getMapService(); + mMapPainter = new PlottableEntityPainter( mSettingsManager ); + init(); + } + + private void init() + { + setLayout( new MigLayout( "insets 0 0 0 0", + "[grow,fill]", "[grow,fill]") ); + + /** + * Set the entity painter as the overlay painter and register this panel + * to receive new messages (plots) + */ + mMapViewer.setOverlayPainter( mMapPainter ); + mMapService.addListener( this ); + + /** + * Map image source + */ + TileFactoryInfo info = new OSMTileFactoryInfo(); + DefaultTileFactory tileFactory = new DefaultTileFactory( info ); + mMapViewer.setTileFactory( tileFactory ); + + /** + * Defines how many threads will be used to fetch the background map + * tiles (graphics) + */ + tileFactory.setThreadPoolSize( 8 ); + + /** + * Set initial location and zoom for the map upon display + */ + GeoPosition syracuse = new GeoPosition( 43.048, -76.147 ); + int zoom = 7; + + MapViewSetting view = mSettingsManager + .getMapViewSetting( "Default", syracuse, zoom ); + + mMapViewer.setAddressLocation( view.getGeoPosition() ); + mMapViewer.setZoom( view.getZoom() ); + + /** + * Add a mouse adapter for panning and scrolling + */ + MapMouseListener listener = + new MapMouseListener( mMapViewer, mSettingsManager ); + mMapViewer.addMouseListener( listener ); + mMapViewer.addMouseMotionListener( listener ); + + /* Map zoom listener */ + mMapViewer.addMouseWheelListener( + new ZoomMouseWheelListenerCursor( mMapViewer ) ); + + /* Keyboard panning listener */ + mMapViewer.addKeyListener( new PanKeyListener( mMapViewer ) ); + + /** + * Add a selection listener + */ + SelectionAdapter sa = new SelectionAdapter( mMapViewer ); + mMapViewer.addMouseListener( sa ); + mMapViewer.addMouseMotionListener( sa ); + + /** + * Map component + */ + add( mMapViewer, "span" ); + } + + @Override + public void entitiesUpdated() + { + EventQueue.invokeLater( new Runnable() + { + @Override + public void run() + { + mMapViewer.repaint(); + } + } ); + } + + @Override + public void addPlottableEntity( PlottableEntity entity ) + { + mMapPainter.addEntity( entity ); + + entitiesUpdated(); + } + + @Override + public void removePlottableEntity( PlottableEntity entity ) + { + mMapPainter.removeEntity( entity ); + + entitiesUpdated(); + } +} diff --git a/src/map/MapService.java b/src/map/MapService.java new file mode 100644 index 00000000..5757e331 --- /dev/null +++ b/src/map/MapService.java @@ -0,0 +1,183 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package map; + +import java.awt.Color; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.Set; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; + +import message.Message; +import sample.Listener; +import settings.SettingsManager; +import alias.Alias; +import controller.ResourceManager; + +public class MapService implements Listener +{ + private int mMaxHistory = 2; + private int mCullThresholdMinutes = 60; + + private static final Color sDEFAULT_COLOR = Color.BLACK; + + private final ScheduledExecutorService scheduler = + Executors.newScheduledThreadPool(1); + + private ArrayList mListeners = + new ArrayList(); + + private HashMap mEntities = + new HashMap(); + + private ResourceManager mResourceManager; + + public MapService( ResourceManager resourceManager ) + { + mResourceManager = resourceManager; + +// scheduler.scheduleAtFixedRate( new CullThread(), 5, 5, TimeUnit.MINUTES ); + } + + /** + * Returns the max plottables (locations) history setting that is applied + * to all plottable entities + */ + public int getMaxHistory() + { + return mMaxHistory; + } + + /** + * Sets the max history trail for entities. Value is applied to all newly + * created PlottableEntity and all existing entities. + */ + public void setMaxHistory( int maxHistory ) + { + mMaxHistory = maxHistory; + + /** + * Update any existing entities with the new max history value + */ + for( PlottableEntity entity: mEntities.values() ) + { + entity.setMaxHistory( mMaxHistory ); + } + } + + @Override + public void receive( Message message ) + { + Plottable plottable = message.getPlottable(); + + if( plottable != null ) + { + PlottableEntity entity = mEntities.get( plottable.getID() ); + + if( entity == null ) + { + Alias alias = plottable.getAlias(); + + Color color = null; + MapIcon icon = null; + + if( alias != null ) + { + color = alias.getMapColor(); + + String iconName = alias.getIconName(); + + if( iconName == null ) + { + iconName = SettingsManager.DEFAULT_ICON; + } + + icon = mResourceManager.getSettingsManager() + .getMapIcon( iconName ); + } + + if( color == null ) + { + color = sDEFAULT_COLOR; + } + + entity = new PlottableEntity( plottable, color ); + + entity.setMaxHistory( mMaxHistory ); + + mEntities.put( plottable.getID(), entity ); + + for( PlottableUpdateListener listener: mListeners ) + { + listener.addPlottableEntity( entity ); + } + } + else + { + entity.addPlottable( plottable ); + + for( PlottableUpdateListener listener: mListeners ) + { + listener.entitiesUpdated();; + } + } + } + } + + public void addListener( PlottableUpdateListener listener ) + { + mListeners.add( listener ); + } + + public void removeListener( PlottableUpdateListener listener ) + { + mListeners.remove( listener ); + } + + public class CullThread implements Runnable + { + @Override + public void run() + { + long cutoffTime = System.currentTimeMillis() - + ( mCullThresholdMinutes * 1000 ); + + Set ids = Collections.unmodifiableSet( mEntities.keySet() ); + + for( String id: ids ) + { + PlottableEntity entity = mEntities.get( id ); + + if( entity.getCurrentPlottable().getTimestamp() < cutoffTime ) + { + mEntities.remove( id ); + + /** + * Let all listeners know we've removed (culled) an entity + */ + for( PlottableUpdateListener listener: mListeners ) + { + listener.removePlottableEntity( entity ); + } + } + } + } + } +} diff --git a/src/map/Plottable.java b/src/map/Plottable.java new file mode 100644 index 00000000..424de6cc --- /dev/null +++ b/src/map/Plottable.java @@ -0,0 +1,139 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package map; + +import java.util.HashMap; + +import org.jdesktop.swingx.mapviewer.GeoPosition; + +import alias.Alias; + +public class Plottable implements Comparable +{ + private long mTimestamp; + private GeoPosition mGeoPosition; + private String mID; + private Alias mAlias; + + HashMap mAttributes = new HashMap(); + + public Plottable() + { + } + + public Plottable( long timestamp, + GeoPosition position, + String id, + Alias alias, + HashMap attributes ) + { + this( timestamp, position, id, alias ); + mAttributes = attributes; + } + + public Plottable( long timestamp, + GeoPosition position, + String id, + Alias alias ) + { + mTimestamp = timestamp; + mGeoPosition = position; + mID = id; + mAlias = alias; + } + + public long getTimestamp() + { + return mTimestamp; + } + + public void setTimestamp( long timestamp ) + { + mTimestamp = timestamp; + } + + public GeoPosition getGeoPosition() + { + return mGeoPosition; + } + + public void setGeoPosition( GeoPosition position ) + { + mGeoPosition = position; + } + + public String getID() + { + return mID; + } + + public void setID( String id ) + { + mID = id; + } + + public Alias getAlias() + { + return mAlias; + } + + public void setAlias( Alias alias ) + { + mAlias = alias; + } + + public HashMap getAttributes() + { + return mAttributes; + } + + public void addAttribute( String key, String value ) + { + mAttributes.put( key, value ); + } + + public void removeAttribute( String key ) + { + mAttributes.remove( key ); + } + + public void clearAttributes() + { + mAttributes.clear(); + } + + /** + * Comparisons for sorting of plottables is based on timestamps + */ + @Override + public int compareTo( Plottable o ) + { + if( mTimestamp == o.mTimestamp ) + { + return 0; + } + else if( mTimestamp < o.mTimestamp ) + { + return -1; + } + else + { + return 1; + } + } +} diff --git a/src/map/PlottableEntity.java b/src/map/PlottableEntity.java new file mode 100644 index 00000000..01972a41 --- /dev/null +++ b/src/map/PlottableEntity.java @@ -0,0 +1,171 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package map; + +import java.awt.Color; +import java.util.Collections; +import java.util.Set; +import java.util.TreeSet; + +import org.jdesktop.swingx.mapviewer.GeoPosition; + +import alias.Alias; + +public class PlottableEntity implements Comparable +{ + private TreeSet mPlottables = new TreeSet(); + + private Alias mAlias; + private Color mColor; + private String mID; + + private int mMaxHistory = 2; + + /** + * Constructs the entity using the group and alias from the plottable and + * adds the plottable as the first plottable entry in the history + */ + public PlottableEntity( Plottable plottable, Color color ) + { + addPlottable( plottable ); + mColor = color; + } + + public int getMaxHistory() + { + return mMaxHistory; + } + + public void setMaxHistory( int maxHistory ) + { + if( maxHistory >= 1 ) + { + mMaxHistory = maxHistory; + } + } + + public Plottable getCurrentPlottable() + { + return mPlottables.last(); + } + + public GeoPosition getCurrentGeoPosition() + { + return getCurrentPlottable().getGeoPosition(); + } + + /** + * Adds the plottable to this entity + */ + public void addPlottable( Plottable plottable ) + { + /** + * Grab the entity settings from the latest plot + */ + mAlias = plottable.getAlias(); + mID = plottable.getID(); + + /** + * Add the plottable to the list + */ + mPlottables.add( plottable ); + + while( mPlottables.size() > mMaxHistory ) + { + mPlottables.remove( mPlottables.first() ); + } + } + + public String getLabel() + { + if( mAlias == null ) + { + return mID; + } + else + { + return mAlias.getName(); + } + } + + public String getMapIconName() + { + if( mAlias != null ) + { + return mAlias.getIconName(); + } + else + { + return null; + } + } + + /** + * Color for rendering this entity (label, route, etc.) + */ + public Color getColor() + { + return mColor; + } + + public void setColor( Color color ) + { + mColor = color; + } + + /** + * Default id to use in the label + */ + public String getID() + { + return mID; + } + + /** + * Gets the alias for this entity + */ + public Alias getAlias() + { + return mAlias; + } + + /** + * Returns an unmodifiable sorted (time order) list of plottables + */ + public Set getPlottables() + { + return Collections.unmodifiableSet( mPlottables ); + } + + /** + * Clears all plottables from this entity + */ + public void clear() + { + mPlottables.clear(); + } + + /** + * Compares by alias name + */ + @Override + public int compareTo( PlottableEntity o ) + { + return this.getAlias().getName().compareTo( o.getAlias().getName() ); + } +} diff --git a/src/map/PlottableEntityPainter.java b/src/map/PlottableEntityPainter.java new file mode 100644 index 00000000..5ed0387c --- /dev/null +++ b/src/map/PlottableEntityPainter.java @@ -0,0 +1,71 @@ +/* + * WaypointMapOverlay.java + * + * Created on April 1, 2006, 4:59 PM + * + * To change this template, choose Tools | Template Manager + * and open the template in the editor. + */ + +package map; + +import java.awt.Graphics2D; +import java.awt.Rectangle; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; + +import org.jdesktop.swingx.JXMapViewer; +import org.jdesktop.swingx.painter.AbstractPainter; + +import settings.SettingsManager; + +public class PlottableEntityPainter extends AbstractPainter +{ + private PlottableEntityRenderer mRenderer; + private Set mEntities = new HashSet(); + + public PlottableEntityPainter( SettingsManager settingsManager ) + { + mRenderer = new PlottableEntityRenderer( settingsManager ); + setAntialiasing( true ); + setCacheable( false ); + } + + public void addEntity( PlottableEntity entity ) + { + mEntities.add( entity ); + } + + public void removeEntity( PlottableEntity entity ) + { + mEntities.remove( entity ); + } + + public void clearEntities() + { + mEntities.clear(); + } + + private Set getEntities() + { + return Collections.unmodifiableSet( mEntities ); + } + + @Override + protected void doPaint(Graphics2D g, JXMapViewer map, int width, int height) + { + Rectangle viewportBounds = map.getViewportBounds(); + + g.translate( -viewportBounds.getX(), -viewportBounds.getY() ); + + Set entities = getEntities(); + + for (PlottableEntity entity: entities ) + { + mRenderer.paintPlottableEntity( g, map, entity, true ); + } + + g.translate( viewportBounds.getX(), viewportBounds.getY() ); + } +} diff --git a/src/map/PlottableEntityRenderer.java b/src/map/PlottableEntityRenderer.java new file mode 100644 index 00000000..faca942b --- /dev/null +++ b/src/map/PlottableEntityRenderer.java @@ -0,0 +1,169 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package map; + +import java.awt.BasicStroke; +import java.awt.Color; +import java.awt.Graphics2D; +import java.awt.RenderingHints; +import java.awt.geom.Point2D; +import java.util.Set; + +import javax.swing.ImageIcon; + +import org.jdesktop.swingx.JXMapViewer; + +import settings.SettingsManager; + +public class PlottableEntityRenderer +{ + private SettingsManager mSettingsManager; + + public PlottableEntityRenderer( SettingsManager settingsManager ) + { + mSettingsManager = settingsManager; + } + + public void paintPlottableEntity( Graphics2D g, + JXMapViewer viewer, + PlottableEntity entity, + boolean antiAliasing ) + { + Graphics2D graphics = (Graphics2D)g.create(); + + if( antiAliasing ) + { + graphics.setRenderingHint( RenderingHints.KEY_ANTIALIASING, + RenderingHints.VALUE_ANTIALIAS_ON ); + } + + /** + * Use the entity's preferred color for lines and labels + */ + graphics.setColor( entity.getColor() ); + + /** + * Convert the lat/long geoposition to an x/y point on the viewer + */ + Point2D point = viewer.getTileFactory().geoToPixel( + entity.getCurrentGeoPosition(), viewer.getZoom() ); + + /** + * Paint the route first, so the icon and label overlay it + */ + paintRoute( graphics, viewer, entity ); + + /** + * Paint the icon at the current location + */ + ImageIcon icon = getIcon( entity ).getImageIcon(); + + paintIcon( graphics, point, icon ); + + /** + * Paint the label offset to the right of the icon + */ + paintLabel( graphics, point, entity, (int)( icon.getIconWidth() / 2 ), 0 ); + + /** + * Cleanup + */ + graphics.dispose(); + } + + private MapIcon getIcon( PlottableEntity entity ) + { + return mSettingsManager.getMapIcon( entity.getMapIconName() ); + } + + private void paintIcon( Graphics2D graphics, + Point2D point, + ImageIcon icon ) + { + graphics.drawImage( icon.getImage(), + (int)point.getX() - ( icon.getIconWidth() / 2 ), + (int)point.getY() - ( icon.getIconHeight() / 2 ), + null ); + + } + + private void paintLabel( Graphics2D graphics, + Point2D point, + PlottableEntity entity, + int xOffset, + int yOffset ) + { + graphics.drawString( entity.getLabel(), + (int)point.getX() + xOffset, + (int)point.getY() + yOffset ); + } + + /** + * Paints a two-tone route from the entity's list of plottables (locations). + * using black as a wider background route, and the entity's preferred color + * as a narrower foreground route. + */ + private void paintRoute( Graphics2D graphics, + JXMapViewer viewer, + PlottableEntity entity ) + { + Set plottables = entity.getPlottables(); + + if( plottables.size() > 1 ) + { + // Draw the route with a black background line + graphics.setColor( Color.BLACK ); + graphics.setStroke( new BasicStroke( 3 ) ); + + drawRoute( plottables, graphics, viewer ); + + // Draw the route again, in the entity's preferred color + graphics.setColor( entity.getColor() ); + graphics.setStroke( new BasicStroke( 1 ) ); + + drawRoute( plottables, graphics, viewer ); + } + } + + /** + * Draws a route from a list of plottables + */ + private void drawRoute( Set plottables, + Graphics2D g, + JXMapViewer viewer ) + { + Point2D lastPoint = null; + + for (Plottable plottable: plottables ) + { + // convert geo-coordinate to world bitmap pixel + Point2D currentPoint = viewer.getTileFactory() + .geoToPixel( plottable.getGeoPosition(), viewer.getZoom() ); + + if( lastPoint != null ) + { + g.drawLine( (int)lastPoint.getX(), (int)lastPoint.getY(), + (int)currentPoint.getX(), (int)currentPoint.getY() ); + } + + lastPoint = currentPoint; + } + } +} + + diff --git a/src/map/PlottableUpdateListener.java b/src/map/PlottableUpdateListener.java new file mode 100644 index 00000000..d239aa0c --- /dev/null +++ b/src/map/PlottableUpdateListener.java @@ -0,0 +1,27 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package map; + +public interface PlottableUpdateListener +{ + public void entitiesUpdated(); + + public void addPlottableEntity( PlottableEntity entity ); + + public void removePlottableEntity( PlottableEntity entity ); +} diff --git a/src/map/SelectionAdapter.java b/src/map/SelectionAdapter.java new file mode 100644 index 00000000..62c2c9df --- /dev/null +++ b/src/map/SelectionAdapter.java @@ -0,0 +1,103 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package map; + +import java.awt.Rectangle; +import java.awt.event.MouseAdapter; +import java.awt.event.MouseEvent; +import java.awt.geom.Point2D; + +import org.jdesktop.swingx.JXMapViewer; + +/** + * Creates a selection rectangle based on mouse input + * Also triggers repaint events in the viewer + * @author Martin Steiger + */ +public class SelectionAdapter extends MouseAdapter +{ + private boolean dragging; + private JXMapViewer viewer; + + private Point2D startPos = new Point2D.Double(); + private Point2D endPos = new Point2D.Double(); + + /** + * @param viewer the jxmapviewer + */ + public SelectionAdapter(JXMapViewer viewer) + { + this.viewer = viewer; + } + + @Override + public void mousePressed(MouseEvent e) + { + if (e.getButton() != MouseEvent.BUTTON3) + return; + + startPos.setLocation(e.getX(), e.getY()); + endPos.setLocation(e.getX(), e.getY()); + + dragging = true; + } + + @Override + public void mouseDragged(MouseEvent e) + { + if (!dragging) + return; + + endPos.setLocation(e.getX(), e.getY()); + + viewer.repaint(); + } + + @Override + public void mouseReleased(MouseEvent e) + { + if (!dragging) + return; + + if (e.getButton() != MouseEvent.BUTTON3) + return; + + viewer.repaint(); + + dragging = false; + } + + /** + * @return the selection rectangle + */ + public Rectangle getRectangle() + { + if (dragging) + { + int x1 = (int) Math.min(startPos.getX(), endPos.getX()); + int y1 = (int) Math.min(startPos.getY(), endPos.getY()); + int x2 = (int) Math.max(startPos.getX(), endPos.getX()); + int y2 = (int) Math.max(startPos.getY(), endPos.getY()); + + return new Rectangle(x1, y1, x2-x1, y2-y1); + } + + return null; + } + +} diff --git a/src/map/SelectionPainter.java b/src/map/SelectionPainter.java new file mode 100644 index 00000000..5ed7a5aa --- /dev/null +++ b/src/map/SelectionPainter.java @@ -0,0 +1,58 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package map; + +import java.awt.Color; +import java.awt.Graphics2D; +import java.awt.Rectangle; + +import org.jdesktop.swingx.painter.Painter; + +/** + * Paints a selection rectangle + * @author Martin Steiger + */ +public class SelectionPainter implements Painter +{ + private Color fillColor = new Color(128, 192, 255, 128); + private Color frameColor = new Color(0, 0, 255, 128); + + private SelectionAdapter adapter; + + /** + * @param adapter the selection adapter + */ + public SelectionPainter(SelectionAdapter adapter) + { + this.adapter = adapter; + } + + @Override + public void paint(Graphics2D g, Object t, int width, int height) + { + Rectangle rc = adapter.getRectangle(); + + if (rc != null) + { + g.setColor(frameColor); + g.draw(rc); + g.setColor(fillColor); + g.fill(rc); + } + } +} diff --git a/src/message/Message.java b/src/message/Message.java new file mode 100644 index 00000000..9783b8f5 --- /dev/null +++ b/src/message/Message.java @@ -0,0 +1,63 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package message; + + +import java.util.Date; + +import map.Plottable; +import controller.activity.MessageDetailsProvider; + +public abstract class Message implements MessageDetailsProvider +{ + protected long mTimeReceived; + protected MessageType mType; + + public Message() + { + this( MessageType.UN_KNWN ); + } + + public Message( MessageType type ) + { + mTimeReceived = System.currentTimeMillis(); + mType = type; + } + + public abstract Plottable getPlottable(); + + public abstract String toString(); + + public abstract boolean isValid(); + + public long getTimeReceived() + { + return mTimeReceived; + } + + public Date getDateReceived() + { + return new Date( mTimeReceived ); + } + + public MessageType getType() + { + return mType; + } + +} diff --git a/src/message/MessageDirection.java b/src/message/MessageDirection.java new file mode 100644 index 00000000..37210c26 --- /dev/null +++ b/src/message/MessageDirection.java @@ -0,0 +1,24 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package message; + +public enum MessageDirection +{ + ISW, //Inbound Status Word + OSW; //Outbound Status Word +} diff --git a/src/message/MessageType.java b/src/message/MessageType.java new file mode 100644 index 00000000..25672de5 --- /dev/null +++ b/src/message/MessageType.java @@ -0,0 +1,74 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package message; + +public enum MessageType +{ + CA_STRT( "Call Start" ), + CA_ENDD( "Call End" ), + CA_TOUT( "Call Timeout" ), + CA_PAGE( "Call Page" ), + DA_STRT( "Data Start" ), + DA_ENDD( "Data End" ), + DA_TOUT( "Data Timeout" ), + FQ_CCHN( "Control Channel Frequency" ), + FQ_RXCV( "Receive Frequency" ), + FQ_RXLO( "Receive Frequency Low" ), + FQ_RXHI( "Receive Frequency High" ), + FQ_TXMT( "Transmit Frequency" ), + FQ_TXLO( "Transmit Frequency Low" ), + FQ_TXHI( "Transmit Frequency High" ), + ID_ANIX( "ANI ID" ), + ID_CHAN( "Channel ID" ), + ID_ESNX( "ESN ID" ), + ID_ESNH( "ESN ID Low" ), + ID_ESNL( "ESN ID High" ), + ID_FROM( "From ID" ), + ID_MIDN( "Mobile ID" ), + ID_NBOR( "Neighbor ID" ), + ID_RDIO( "Radio ID" ), + ID_SITE( "Site ID" ), + ID_SYST( "System ID" ), + ID_TGAS( "Assign Talkgroup ID" ), + ID_TOTO( "To ID" ), + ID_UNIQ( "Unique ID" ), + MA_CHAN( "Channel Map" ), + MA_CHNL( "Channel Map Low" ), + MA_CHNH( "Channel Map High" ), + MS_TEXT( "Text Message" ), + MS_MGPS( "GPS Message"), + MS_STAT( "Status Message" ), + RA_KILL( "Radio Kill" ), + RA_REGI( "Radio Register" ), + RA_STUN( "Radio Stun" ), + RQ_ACCE( "Request Access" ), + SY_IDLE( "System Idle" ), + UN_KNWN( "Unknown" ); + + private String mDisplayText; + + MessageType( String displayText ) + { + mDisplayText = displayText; + } + + public String getDisplayText() + { + return mDisplayText; + } +} diff --git a/src/org/jdesktop/beans/AbstractBean.java b/src/org/jdesktop/beans/AbstractBean.java new file mode 100644 index 00000000..931e94b3 --- /dev/null +++ b/src/org/jdesktop/beans/AbstractBean.java @@ -0,0 +1,504 @@ +/* + * $Id: AbstractBean.java 4088 2011-11-17 19:53:49Z kschaefe $ + * + * Copyright 2004 Sun Microsystems, Inc., 4150 Network Circle, + * Santa Clara, California 95054, U.S.A. All rights reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + */ + +package org.jdesktop.beans; + +import java.beans.PropertyChangeEvent; +import java.beans.PropertyChangeListener; +import java.beans.PropertyChangeSupport; +import java.beans.PropertyVetoException; +import java.beans.VetoableChangeListener; +import java.beans.VetoableChangeSupport; + +/** + *

+ * A convenience class from which to extend all non-visual AbstractBeans. It + * manages the PropertyChange notification system, making it relatively trivial + * to add support for property change events in getters/setters. + *

+ * + *

+ * A non-visual java bean is a Java class that conforms to the AbstractBean + * patterns to allow visual manipulation of the bean's properties and event + * handlers at design-time. + *

+ * + *

+ * Here is a simple example bean that contains one property, foo, and the proper + * pattern for implementing property change notification: + * + *


+ * public class ABean extends AbstractBean {
+ *     private String foo;
+ * 
+ *     public void setFoo(String newFoo) {
+ *         String old = getFoo();
+ *         this.foo = newFoo;
+ *         firePropertyChange("foo", old, getFoo());
+ *     }
+ * 
+ *     public String getFoo() {
+ *         return foo;
+ *     }
+ * }
+ * 
+ * + *

+ * + *

+ * You will notice that "getFoo()" is used in the setFoo method rather than + * accessing "foo" directly for the gets. This is done intentionally so that if + * a subclass overrides getFoo() to return, for instance, a constant value the + * property change notification system will continue to work properly. + *

+ * + *

+ * The firePropertyChange method takes into account the old value and the new + * value. Only if the two differ will it fire a property change event. So you + * can be assured from the above code fragment that a property change event will + * only occur if old is indeed different from getFoo() + *

+ * + *

+ * AbstractBean also supports vetoable + * {@link PropertyChangeEvent} events. These events are similar to + * PropertyChange events, except a special exception can be used + * to veto changing the property. For example, perhaps the property is changing + * from "fred" to "red", but a listener deems that "red" is unexceptable. In + * this case, the listener can fire a veto exception and the property must + * remain "fred". For example: + * + *


+ *  public class ABean extends AbstractBean {
+ *    private String foo;
+ *    
+ *    public void setFoo(String newFoo) throws PropertyVetoException {
+ *      String old = getFoo();
+ *      this.foo = newFoo;
+ *      fireVetoableChange("foo", old, getFoo());
+ *    }
+ *    public String getFoo() {
+ *      return foo;
+ *    }
+ *  }
+ * 
+ *  public class Tester {
+ *    public static void main(String... args) {
+ *      try {
+ *        ABean a = new ABean();
+ *        a.setFoo("fred");
+ *        a.addVetoableChangeListener(new VetoableChangeListener() {
+ *          public void vetoableChange(PropertyChangeEvent evt) throws PropertyVetoException {
+ *            if ("red".equals(evt.getNewValue()) {
+ *              throw new PropertyVetoException("Cannot be red!", evt);
+ *            }
+ *          }
+ *        }
+ *        a.setFoo("red");
+ *      } catch (Exception e) {
+ *        e.printStackTrace(); // this will be executed
+ *      }
+ *    }
+ *  }
+ * 
+ * + *

+ *

+ * {@code AbstractBean} is not {@link java.io.Serializable}. Special care must + * be taken when creating {@code Serializable} subclasses, as the + * {@code Serializable} listeners will not be saved. Subclasses will need to + * manually save the serializable listeners. The {@link AbstractSerializableBean} + * is {@code Serializable} and already handles the listeners correctly. If + * possible, it is recommended that {@code Serializable} beans should extend + * {@code AbstractSerializableBean}. If it is not possible, the + * {@code AbstractSerializableBean} bean implementation provides details on + * how to correctly serialize an {@code AbstractBean} subclass. + *

+ * + * @see AbstractSerializableBean + * @status REVIEWED + * @author rbair + */ +@SuppressWarnings("nls") +public abstract class AbstractBean { + /** + * Helper class that manages all the property change notification machinery. + * PropertyChangeSupport cannot be extended directly because it requires + * a bean in the constructor, and the "this" argument is not valid until + * after super construction. Hence, delegation instead of extension + */ + private transient PropertyChangeSupport pcs; + + /** + * Helper class that manages all the veto property change notification machinery. + */ + private transient VetoableChangeSupport vcs; + + /** Creates a new instance of AbstractBean */ + protected AbstractBean() { + pcs = new PropertyChangeSupport(this); + vcs = new VetoableChangeSupport(this); + } + + /** + * Creates a new instance of AbstractBean, using the supplied PropertyChangeSupport and + * VetoableChangeSupport delegates. Neither of these may be null. + * @param pcs the property change support + * @param vcs the vetoable change support + */ + protected AbstractBean(PropertyChangeSupport pcs, VetoableChangeSupport vcs) { + if (pcs == null) { + throw new NullPointerException("PropertyChangeSupport must not be null"); + } + if (vcs == null) { + throw new NullPointerException("VetoableChangeSupport must not be null"); + } + + this.pcs = pcs; + this.vcs = vcs; + } + + /** + * Add a PropertyChangeListener to the listener list. + * The listener is registered for all properties. + * The same listener object may be added more than once, and will be called + * as many times as it is added. + * If listener is null, no exception is thrown and no action + * is taken. + * + * @param listener The PropertyChangeListener to be added + */ + public final void addPropertyChangeListener(PropertyChangeListener listener) { + pcs.addPropertyChangeListener(listener); + } + + /** + * Remove a PropertyChangeListener from the listener list. + * This removes a PropertyChangeListener that was registered + * for all properties. + * If listener was added more than once to the same event + * source, it will be notified one less time after being removed. + * If listener is null, or was never added, no exception is + * thrown and no action is taken. + * + * @param listener The PropertyChangeListener to be removed + */ + public final void removePropertyChangeListener(PropertyChangeListener listener) { + pcs.removePropertyChangeListener(listener); + } + + /** + * Returns an array of all the listeners that were added to the + * PropertyChangeSupport object with addPropertyChangeListener(). + *

+ * If some listeners have been added with a named property, then + * the returned array will be a mixture of PropertyChangeListeners + * and PropertyChangeListenerProxys. If the calling + * method is interested in distinguishing the listeners then it must + * test each element to see if it's a + * PropertyChangeListenerProxy, perform the cast, and examine + * the parameter. + * + *

+     * PropertyChangeListener[] listeners = bean.getPropertyChangeListeners();
+     * for (int i = 0; i < listeners.length; i++) {
+     *     if (listeners[i] instanceof PropertyChangeListenerProxy) {
+     *     PropertyChangeListenerProxy proxy = 
+     *                    (PropertyChangeListenerProxy)listeners[i];
+     *     if (proxy.getPropertyName().equals("foo")) {
+     *       // proxy is a PropertyChangeListener which was associated
+     *       // with the property named "foo"
+     *     }
+     *   }
+     * }
+     *
+ * + * @see java.beans.PropertyChangeListenerProxy + * @return all of the PropertyChangeListeners added or an + * empty array if no listeners have been added + */ + public final PropertyChangeListener[] getPropertyChangeListeners() { + return pcs.getPropertyChangeListeners(); + } + + /** + * Add a PropertyChangeListener for a specific property. The listener + * will be invoked only when a call on firePropertyChange names that + * specific property. + * The same listener object may be added more than once. For each + * property, the listener will be invoked the number of times it was added + * for that property. + * If propertyName or listener is null, no + * exception is thrown and no action is taken. + * + * @param propertyName The name of the property to listen on. + * @param listener The PropertyChangeListener to be added + */ + public final void addPropertyChangeListener(String propertyName, PropertyChangeListener listener) { + pcs.addPropertyChangeListener(propertyName, listener); + } + + /** + * Remove a PropertyChangeListener for a specific property. + * If listener was added more than once to the same event + * source for the specified property, it will be notified one less time + * after being removed. + * If propertyName is null, no exception is thrown and no + * action is taken. + * If listener is null, or was never added for the specified + * property, no exception is thrown and no action is taken. + * + * @param propertyName The name of the property that was listened on. + * @param listener The PropertyChangeListener to be removed + */ + public final void removePropertyChangeListener(String propertyName, PropertyChangeListener listener) { + pcs.removePropertyChangeListener(propertyName, listener); + } + + /** + * Returns an array of all the listeners which have been associated + * with the named property. + * + * @param propertyName The name of the property being listened to + * @return all of the PropertyChangeListeners associated with + * the named property. If no such listeners have been added, + * or if propertyName is null, an empty array is + * returned. + */ + public final PropertyChangeListener[] getPropertyChangeListeners(String propertyName) { + return pcs.getPropertyChangeListeners(propertyName); + } + + /** + * Report a bound property update to any registered listeners. + * No event is fired if old and new are equal and non-null. + * + *

+ * This is merely a convenience wrapper around the more general + * firePropertyChange method that takes {@code + * PropertyChangeEvent} value. + * + * @param propertyName The programmatic name of the property + * that was changed. + * @param oldValue The old value of the property. + * @param newValue The new value of the property. + */ + protected final void firePropertyChange(String propertyName, Object oldValue, Object newValue) { + pcs.firePropertyChange(propertyName, oldValue, newValue); + } + + /** + * Fire an existing PropertyChangeEvent to any registered listeners. + * No event is fired if the given event's old and new values are + * equal and non-null. + * @param evt The PropertyChangeEvent object. + */ + protected final void firePropertyChange(PropertyChangeEvent evt) { + pcs.firePropertyChange(evt); + } + + + /** + * Report a bound indexed property update to any registered + * listeners. + *

+ * No event is fired if old and new values are equal + * and non-null. + * + *

+ * This is merely a convenience wrapper around the more general + * firePropertyChange method that takes {@code PropertyChangeEvent} value. + * + * @param propertyName The programmatic name of the property that + * was changed. + * @param index index of the property element that was changed. + * @param oldValue The old value of the property. + * @param newValue The new value of the property. + */ + protected final void fireIndexedPropertyChange(String propertyName, int index, + Object oldValue, Object newValue) { + pcs.fireIndexedPropertyChange(propertyName, index, oldValue, newValue); + } + + /** + * Check if there are any listeners for a specific property, including + * those registered on all properties. If propertyName + * is null, only check for listeners registered on all properties. + * + * @param propertyName the property name. + * @return true if there are one or more listeners for the given property + */ + protected final boolean hasPropertyChangeListeners(String propertyName) { + return pcs.hasListeners(propertyName); + } + + /** + * Check if there are any listeners for a specific property, including + * those registered on all properties. If propertyName + * is null, only check for listeners registered on all properties. + * + * @param propertyName the property name. + * @return true if there are one or more listeners for the given property + */ + protected final boolean hasVetoableChangeListeners(String propertyName) { + return vcs.hasListeners(propertyName); + } + + /** + * Add a VetoableListener to the listener list. + * The listener is registered for all properties. + * The same listener object may be added more than once, and will be called + * as many times as it is added. + * If listener is null, no exception is thrown and no action + * is taken. + * + * @param listener The VetoableChangeListener to be added + */ + + public final void addVetoableChangeListener(VetoableChangeListener listener) { + vcs.addVetoableChangeListener(listener); + } + + /** + * Remove a VetoableChangeListener from the listener list. + * This removes a VetoableChangeListener that was registered + * for all properties. + * If listener was added more than once to the same event + * source, it will be notified one less time after being removed. + * If listener is null, or was never added, no exception is + * thrown and no action is taken. + * + * @param listener The VetoableChangeListener to be removed + */ + public final void removeVetoableChangeListener(VetoableChangeListener listener) { + vcs.removeVetoableChangeListener(listener); + } + + /** + * Returns the list of VetoableChangeListeners. If named vetoable change listeners + * were added, then VetoableChangeListenerProxy wrappers will returned + *

+ * @return List of VetoableChangeListeners and VetoableChangeListenerProxys + * if named property change listeners were added. + */ + public final VetoableChangeListener[] getVetoableChangeListeners(){ + return vcs.getVetoableChangeListeners(); + } + + /** + * Add a VetoableChangeListener for a specific property. The listener + * will be invoked only when a call on fireVetoableChange names that + * specific property. + * The same listener object may be added more than once. For each + * property, the listener will be invoked the number of times it was added + * for that property. + * If propertyName or listener is null, no + * exception is thrown and no action is taken. + * + * @param propertyName The name of the property to listen on. + * @param listener The VetoableChangeListener to be added + */ + + public final void addVetoableChangeListener(String propertyName, + VetoableChangeListener listener) { + vcs.addVetoableChangeListener(propertyName, listener); + } + + /** + * Remove a VetoableChangeListener for a specific property. + * If listener was added more than once to the same event + * source for the specified property, it will be notified one less time + * after being removed. + * If propertyName is null, no exception is thrown and no + * action is taken. + * If listener is null, or was never added for the specified + * property, no exception is thrown and no action is taken. + * + * @param propertyName The name of the property that was listened on. + * @param listener The VetoableChangeListener to be removed + */ + + public final void removeVetoableChangeListener(String propertyName, + VetoableChangeListener listener) { + vcs.removeVetoableChangeListener(propertyName, listener); + } + + /** + * Returns an array of all the listeners which have been associated + * with the named property. + * + * @param propertyName The name of the property being listened to + * @return all the VetoableChangeListeners associated with + * the named property. If no such listeners have been added, + * or if propertyName is null, an empty array is + * returned. + */ + public final VetoableChangeListener[] getVetoableChangeListeners(String propertyName) { + return vcs.getVetoableChangeListeners(propertyName); + } + + /** + * Report a vetoable property update to any registered listeners. If + * anyone vetos the change, then fire a new event reverting everyone to + * the old value and then rethrow the PropertyVetoException. + *

+ * No event is fired if old and new are equal and non-null. + * + * @param propertyName The programmatic name of the property + * that is about to change.. + * @param oldValue The old value of the property. + * @param newValue The new value of the property. + * @exception PropertyVetoException if the recipient wishes the property + * change to be rolled back. + */ + protected final void fireVetoableChange(String propertyName, + Object oldValue, Object newValue) + throws PropertyVetoException { + vcs.fireVetoableChange(propertyName, oldValue, newValue); + } + + /** + * Fire a vetoable property update to any registered listeners. If + * anyone vetos the change, then fire a new event reverting everyone to + * the old value and then rethrow the PropertyVetoException. + *

+ * No event is fired if old and new are equal and non-null. + * + * @param evt The PropertyChangeEvent to be fired. + * @exception PropertyVetoException if the recipient wishes the property + * change to be rolled back. + */ + protected final void fireVetoableChange(PropertyChangeEvent evt) + throws PropertyVetoException { + vcs.fireVetoableChange(evt); + } + + /** + * {@inheritDoc} + */ + @Override + public Object clone() throws CloneNotSupportedException { + AbstractBean result = (AbstractBean) super.clone(); + result.pcs = new PropertyChangeSupport(result); + result.vcs = new VetoableChangeSupport(result); + return result; + } +} \ No newline at end of file diff --git a/src/org/jdesktop/beans/AbstractSerializableBean.java b/src/org/jdesktop/beans/AbstractSerializableBean.java new file mode 100644 index 00000000..31c6c14a --- /dev/null +++ b/src/org/jdesktop/beans/AbstractSerializableBean.java @@ -0,0 +1,104 @@ +/* + * $Id: AbstractSerializableBean.java 4088 2011-11-17 19:53:49Z kschaefe $ + * + * Copyright 2008 Sun Microsystems, Inc., 4150 Network Circle, + * Santa Clara, California 95054, U.S.A. All rights reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + */ +package org.jdesktop.beans; + +import java.beans.PropertyChangeListener; +import java.beans.PropertyChangeSupport; +import java.beans.VetoableChangeListener; +import java.beans.VetoableChangeSupport; +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.io.Serializable; + +/** + * This subclass enhances {@code AbstractBean} by implementing the + * {@code Serializable} interface. {@code AbstractSerializableBean} correctly + * serializes all {@code Serializable} listeners that it contains. Implementors + * that need to extends {@code AbstractBean} or one of its subclasses and + * require serialization should use this class if possible. If it is not + * possible to extend this class, the implementation can guide implementors on + * how to properly serialize the listeners. + * + * @author Karl George Schaefer + * + * @see Serializable + * @see ObjectInputStream + * @see ObjectOutputStream + */ +@SuppressWarnings("serial") +public abstract class AbstractSerializableBean extends AbstractBean implements + Serializable { + /** + * Creates a new instance of {@code AbstractSerializableBean}. + */ + protected AbstractSerializableBean() { + super(); + } + + /** + * Creates a new instance of {@code AbstractSerializableBean}, using the + * supplied support delegates. Neither of these may be {@code null}. + * + * @param pcs + * the property change support class to use + * @param vcs + * the vetoable change support class to use + * @throws NullPointerException + * if any parameter is {@code null} + */ + protected AbstractSerializableBean(PropertyChangeSupport pcs, + VetoableChangeSupport vcs) { + super(pcs, vcs); + } + + private void writeObject(ObjectOutputStream s) throws IOException { + s.defaultWriteObject(); + + for (PropertyChangeListener l : getPropertyChangeListeners()) { + if (l instanceof Serializable) { + s.writeObject(l); + } + } + + for (VetoableChangeListener l : getVetoableChangeListeners()) { + if (l instanceof Serializable) { + s.writeObject(l); + } + } + + s.writeObject(null); + } + + private void readObject(ObjectInputStream s) throws ClassNotFoundException, + IOException { + s.defaultReadObject(); + + Object listenerOrNull; + while (null != (listenerOrNull = s.readObject())) { + if (listenerOrNull instanceof PropertyChangeListener) { + addPropertyChangeListener((PropertyChangeListener) listenerOrNull); + } else if (listenerOrNull instanceof VetoableChangeListener) { + addVetoableChangeListener((VetoableChangeListener) listenerOrNull); + } + } + } +} diff --git a/src/org/jdesktop/swingx/JXMapKit.form b/src/org/jdesktop/swingx/JXMapKit.form new file mode 100644 index 00000000..59423d37 --- /dev/null +++ b/src/org/jdesktop/swingx/JXMapKit.form @@ -0,0 +1,145 @@ + + +

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/org/jdesktop/swingx/JXMapKit.java b/src/org/jdesktop/swingx/JXMapKit.java new file mode 100644 index 00000000..da422898 --- /dev/null +++ b/src/org/jdesktop/swingx/JXMapKit.java @@ -0,0 +1,724 @@ +/* + * JXMapKit.java + * + * Created on November 19, 2006, 3:52 AM + */ + +package org.jdesktop.swingx; + +import java.awt.Color; +import java.awt.Graphics2D; +import java.awt.Rectangle; +import java.awt.event.ActionEvent; +import java.awt.geom.Point2D; +import java.beans.PropertyChangeEvent; +import java.beans.PropertyChangeListener; +import java.util.HashSet; +import java.util.Set; + +import javax.swing.AbstractAction; +import javax.swing.Action; +import javax.swing.Icon; +import javax.swing.ImageIcon; +import javax.swing.JButton; +import javax.swing.JFrame; +import javax.swing.JPanel; +import javax.swing.JSlider; +import javax.swing.SwingConstants; +import javax.swing.SwingUtilities; + +import org.jdesktop.swingx.mapviewer.DefaultTileFactory; +import org.jdesktop.swingx.mapviewer.DefaultWaypoint; +import org.jdesktop.swingx.mapviewer.GeoPosition; +import org.jdesktop.swingx.mapviewer.TileFactory; +import org.jdesktop.swingx.mapviewer.TileFactoryInfo; +import org.jdesktop.swingx.mapviewer.Waypoint; +import org.jdesktop.swingx.mapviewer.WaypointPainter; +import org.jdesktop.swingx.mapviewer.bmng.CylindricalProjectionTileFactory; +import org.jdesktop.swingx.painter.AbstractPainter; +import org.jdesktop.swingx.painter.CompoundPainter; +import org.jdesktop.swingx.painter.Painter; + +/** + *

The JXMapKit is a pair of JXMapViewers preconfigured to be easy to use + * with common features built in. This includes zoom buttons, a zoom slider, + * and a mini-map in the lower right corner showing an overview of the map. + * Each feature can be turned off using an appropriate + * isXvisible property. For example, to turn + * off the minimap call + * + *

jxMapKit.setMiniMapVisible(false);
+ *

+ * + *

+ * The JXMapViewer is preconfigured to connect to maps.swinglabs.org which + * serves up global satellite imagery from NASA's + * Blue + * Marble NG image collection. + *

+ * @author joshy + */ +public class JXMapKit extends JPanel +{ + private static final long serialVersionUID = -8366577998349912380L; + private boolean miniMapVisible = true; + private boolean zoomSliderVisible = true; + private boolean zoomButtonsVisible = true; + private final boolean sliderReversed = false; + + @SuppressWarnings("javadoc") + public enum DefaultProviders + { + SwingLabsBlueMarble, OpenStreetMaps, Custom + } + + private DefaultProviders defaultProvider = DefaultProviders.SwingLabsBlueMarble; + + private boolean addressLocationShown = true; + + private boolean dataProviderCreditShown = true; + + /** + * Creates a new JXMapKit + */ + public JXMapKit() + { + initComponents(); + setDataProviderCreditShown(false); + + zoomSlider.setOpaque(false); + try + { + Icon minusIcon = new ImageIcon(getClass().getResource("/org/jdesktop/swingx/mapviewer/resources/minus.png")); + this.zoomOutButton.setIcon(minusIcon); + this.zoomOutButton.setText(""); + Icon plusIcon = new ImageIcon(getClass().getResource("/org/jdesktop/swingx/mapviewer/resources/plus.png")); + this.zoomInButton.setIcon(plusIcon); + this.zoomInButton.setText(""); + } + catch (Throwable thr) + { + System.out.println("error: " + thr.getMessage()); + thr.printStackTrace(); + } + + setTileFactory(new CylindricalProjectionTileFactory()); + + mainMap.setCenterPosition(new GeoPosition(0, 0)); + miniMap.setCenterPosition(new GeoPosition(0, 0)); + mainMap.setRestrictOutsidePanning(true); + miniMap.setRestrictOutsidePanning(true); + + rebuildMainMapOverlay(); + + /* + * // adapter to move the minimap after the main map has moved MouseInputAdapter ma = new MouseInputAdapter() { + * public void mouseReleased(MouseEvent e) { miniMap.setCenterPosition(mapCenterPosition); } }; + * mainMap.addMouseMotionListener(ma); mainMap.addMouseListener(ma); + */ + + mainMap.addPropertyChangeListener("center", new PropertyChangeListener() + { + @Override + public void propertyChange(PropertyChangeEvent evt) + { + Point2D mapCenter = (Point2D) evt.getNewValue(); + TileFactory tf = mainMap.getTileFactory(); + GeoPosition mapPos = tf.pixelToGeo(mapCenter, mainMap.getZoom()); + miniMap.setCenterPosition(mapPos); + } + }); + + mainMap.addPropertyChangeListener("centerPosition", new PropertyChangeListener() + { + @Override + public void propertyChange(PropertyChangeEvent evt) + { + mapCenterPosition = (GeoPosition) evt.getNewValue(); + miniMap.setCenterPosition(mapCenterPosition); + Point2D pt = miniMap.getTileFactory().geoToPixel(mapCenterPosition, miniMap.getZoom()); + miniMap.setCenter(pt); + miniMap.repaint(); + } + }); + + mainMap.addPropertyChangeListener("zoom", new PropertyChangeListener() + { + @Override + public void propertyChange(PropertyChangeEvent evt) + { + zoomSlider.setValue(mainMap.getZoom()); + miniMap.setZoom(mainMap.getZoom() + 4); + } + }); + + // an overlay for the mini-map which shows a rectangle representing the main map + miniMap.setOverlayPainter(new Painter() + { + @Override + public void paint(Graphics2D g, JXMapViewer map, int width, int height) + { + // get the viewport rect of the main map + Rectangle mainMapBounds = mainMap.getViewportBounds(); + + // convert to Point2Ds + Point2D upperLeft2D = mainMapBounds.getLocation(); + Point2D lowerRight2D = new Point2D.Double(upperLeft2D.getX() + mainMapBounds.getWidth(), upperLeft2D + .getY() + mainMapBounds.getHeight()); + + // convert to GeoPostions + GeoPosition upperLeft = mainMap.getTileFactory().pixelToGeo(upperLeft2D, mainMap.getZoom()); + GeoPosition lowerRight = mainMap.getTileFactory().pixelToGeo(lowerRight2D, mainMap.getZoom()); + + // convert to Point2Ds on the mini-map + upperLeft2D = map.getTileFactory().geoToPixel(upperLeft, map.getZoom()); + lowerRight2D = map.getTileFactory().geoToPixel(lowerRight, map.getZoom()); + + g = (Graphics2D) g.create(); + Rectangle rect = map.getViewportBounds(); + // p("rect = " + rect); + g.translate(-rect.x, -rect.y); +// Point2D centerpos = map.getTileFactory().geoToPixel(mapCenterPosition, map.getZoom()); + // p("center pos = " + centerpos); + g.setPaint(Color.RED); + // g.drawRect((int)centerpos.getX()-30,(int)centerpos.getY()-30,60,60); + g.drawRect((int) upperLeft2D.getX(), (int) upperLeft2D.getY(), + (int) (lowerRight2D.getX() - upperLeft2D.getX()), + (int) (lowerRight2D.getY() - upperLeft2D.getY())); + g.setPaint(new Color(255, 0, 0, 50)); + g.fillRect((int) upperLeft2D.getX(), (int) upperLeft2D.getY(), + (int) (lowerRight2D.getX() - upperLeft2D.getX()), + (int) (lowerRight2D.getY() - upperLeft2D.getY())); + // g.drawOval((int)lowerRight2D.getX(),(int)lowerRight2D.getY(),1,1); + g.dispose(); + } + }); + + if (getDefaultProvider() == DefaultProviders.OpenStreetMaps) + { + setZoom(10); + } + else + { + setZoom(3);// joshy: hack, i shouldn't need this here + } + this.setCenterPosition(new GeoPosition(0, 0)); + } + + // private Point2D mapCenter = new Point2D.Double(0,0); + private GeoPosition mapCenterPosition = new GeoPosition(0, 0); + private boolean zoomChanging = false; + + /** + * Set the current zoomlevel for the main map. The minimap will be updated accordingly + * @param zoom the new zoom level + */ + public void setZoom(int zoom) + { + zoomChanging = true; + mainMap.setZoom(zoom); + miniMap.setZoom(mainMap.getZoom() + 4); + if (sliderReversed) + { + zoomSlider.setValue(zoomSlider.getMaximum() - zoom); + } + else + { + zoomSlider.setValue(zoom); + } + zoomChanging = false; + } + + /** + * Returns an action which can be attached to buttons or menu items to make the map zoom out + * @return a preconfigured Zoom Out action + */ + public Action getZoomOutAction() + { + Action act = new AbstractAction() + { + /** + * + */ + private static final long serialVersionUID = 5525706163434375107L; + + @Override + public void actionPerformed(ActionEvent e) + { + setZoom(mainMap.getZoom() - 1); + } + }; + act.putValue(Action.NAME, "-"); + return act; + } + + /** + * Returns an action which can be attached to buttons or menu items to make the map zoom in + * @return a preconfigured Zoom In action + */ + public Action getZoomInAction() + { + Action act = new AbstractAction() + { + /** + * + */ + private static final long serialVersionUID = 5779971489365451352L; + + @Override + public void actionPerformed(ActionEvent e) + { + setZoom(mainMap.getZoom() + 1); + } + }; + act.putValue(Action.NAME, "+"); + return act; + } + + /** + * This method is called from within the constructor to initialize the form. WARNING: Do NOT modify this code. The + * content of this method is always regenerated by the Form Editor. + */ + // //GEN-BEGIN:initComponents + private void initComponents() + { + java.awt.GridBagConstraints gridBagConstraints; + + mainMap = new org.jdesktop.swingx.JXMapViewer(); + miniMap = new org.jdesktop.swingx.JXMapViewer(); + jPanel1 = new javax.swing.JPanel(); + zoomInButton = new javax.swing.JButton(); + zoomOutButton = new javax.swing.JButton(); + zoomSlider = new javax.swing.JSlider(); + + setLayout(new java.awt.GridBagLayout()); + + mainMap.setLayout(new java.awt.GridBagLayout()); + + miniMap.setBorder(javax.swing.BorderFactory.createLineBorder(new java.awt.Color(0, 0, 0))); + miniMap.setMinimumSize(new java.awt.Dimension(100, 100)); + miniMap.setPreferredSize(new java.awt.Dimension(100, 100)); + miniMap.setLayout(new java.awt.GridBagLayout()); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 1; + gridBagConstraints.gridy = 0; + gridBagConstraints.anchor = java.awt.GridBagConstraints.SOUTHEAST; + gridBagConstraints.weightx = 1.0; + gridBagConstraints.weighty = 1.0; + mainMap.add(miniMap, gridBagConstraints); + + jPanel1.setOpaque(false); + jPanel1.setLayout(new java.awt.GridBagLayout()); + + zoomInButton.setAction(getZoomOutAction()); + zoomInButton.setIcon(new javax.swing.ImageIcon(getClass().getResource( + "/org/jdesktop/swingx/mapviewer/resources/plus.png"))); + zoomInButton.setMargin(new java.awt.Insets(2, 2, 2, 2)); + zoomInButton.setMaximumSize(new java.awt.Dimension(20, 20)); + zoomInButton.setMinimumSize(new java.awt.Dimension(20, 20)); + zoomInButton.setOpaque(false); + zoomInButton.setPreferredSize(new java.awt.Dimension(20, 20)); + zoomInButton.addActionListener(new java.awt.event.ActionListener() + { + @Override + public void actionPerformed(java.awt.event.ActionEvent evt) + { + zoomInButtonActionPerformed(evt); + } + }); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 1; + gridBagConstraints.gridy = 1; + gridBagConstraints.anchor = java.awt.GridBagConstraints.NORTHEAST; + gridBagConstraints.weightx = 1.0; + gridBagConstraints.weighty = 1.0; + jPanel1.add(zoomInButton, gridBagConstraints); + + zoomOutButton.setAction(getZoomInAction()); + zoomOutButton.setIcon(new javax.swing.ImageIcon(getClass().getResource( + "/org/jdesktop/swingx/mapviewer/resources/minus.png"))); + zoomOutButton.setMargin(new java.awt.Insets(2, 2, 2, 2)); + zoomOutButton.setMaximumSize(new java.awt.Dimension(20, 20)); + zoomOutButton.setMinimumSize(new java.awt.Dimension(20, 20)); + zoomOutButton.setOpaque(false); + zoomOutButton.setPreferredSize(new java.awt.Dimension(20, 20)); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 1; + gridBagConstraints.anchor = java.awt.GridBagConstraints.NORTHWEST; + gridBagConstraints.weighty = 1.0; + jPanel1.add(zoomOutButton, gridBagConstraints); + + zoomSlider.setMajorTickSpacing(1); + zoomSlider.setMaximum(15); + zoomSlider.setMinimum(10); + zoomSlider.setMinorTickSpacing(1); + zoomSlider.setOrientation(SwingConstants.VERTICAL); + zoomSlider.setPaintTicks(true); + zoomSlider.setSnapToTicks(true); + zoomSlider.setMinimumSize(new java.awt.Dimension(35, 100)); + zoomSlider.setPreferredSize(new java.awt.Dimension(35, 190)); + zoomSlider.addChangeListener(new javax.swing.event.ChangeListener() + { + @Override + public void stateChanged(javax.swing.event.ChangeEvent evt) + { + zoomSliderStateChanged(evt); + } + }); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 0; + gridBagConstraints.gridwidth = 2; + gridBagConstraints.fill = java.awt.GridBagConstraints.VERTICAL; + gridBagConstraints.anchor = java.awt.GridBagConstraints.NORTH; + jPanel1.add(zoomSlider, gridBagConstraints); + + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 0; + gridBagConstraints.anchor = java.awt.GridBagConstraints.SOUTHWEST; + gridBagConstraints.weightx = 1.0; + gridBagConstraints.weighty = 1.0; + gridBagConstraints.insets = new java.awt.Insets(4, 4, 4, 4); + mainMap.add(jPanel1, gridBagConstraints); + + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.fill = java.awt.GridBagConstraints.BOTH; + gridBagConstraints.weightx = 1.0; + gridBagConstraints.weighty = 1.0; + add(mainMap, gridBagConstraints); + }// //GEN-END:initComponents + + @SuppressWarnings("unused") + private void zoomInButtonActionPerformed(java.awt.event.ActionEvent evt) + {// GEN-FIRST:event_zoomInButtonActionPerformed + // TODO add your handling code here: + }// GEN-LAST:event_zoomInButtonActionPerformed + + @SuppressWarnings("unused") + private void zoomSliderStateChanged(javax.swing.event.ChangeEvent evt) + {// GEN-FIRST:event_zoomSliderStateChanged + if (!zoomChanging) + { + setZoom(zoomSlider.getValue()); + } + // TODO add your handling code here: + }// GEN-LAST:event_zoomSliderStateChanged + + // Variables declaration - do not modify//GEN-BEGIN:variables + private javax.swing.JPanel jPanel1; + private org.jdesktop.swingx.JXMapViewer mainMap; + private org.jdesktop.swingx.JXMapViewer miniMap; + private javax.swing.JButton zoomInButton; + private javax.swing.JButton zoomOutButton; + private javax.swing.JSlider zoomSlider; + + // End of variables declaration//GEN-END:variables + + /** + * Indicates if the mini-map is currently visible + * @return the current value of the mini-map property + */ + public boolean isMiniMapVisible() + { + return miniMapVisible; + } + + /** + * Sets if the mini-map should be visible + * @param miniMapVisible a new value for the miniMap property + */ + public void setMiniMapVisible(boolean miniMapVisible) + { + boolean old = this.isMiniMapVisible(); + this.miniMapVisible = miniMapVisible; + miniMap.setVisible(miniMapVisible); + firePropertyChange("miniMapVisible", old, this.isMiniMapVisible()); + } + + /** + * Indicates if the zoom slider is currently visible + * @return the current value of the zoomSliderVisible property + */ + public boolean isZoomSliderVisible() + { + return zoomSliderVisible; + } + + /** + * Sets if the zoom slider should be visible + * @param zoomSliderVisible the new value of the zoomSliderVisible property + */ + public void setZoomSliderVisible(boolean zoomSliderVisible) + { + boolean old = this.isZoomSliderVisible(); + this.zoomSliderVisible = zoomSliderVisible; + zoomSlider.setVisible(zoomSliderVisible); + firePropertyChange("zoomSliderVisible", old, this.isZoomSliderVisible()); + } + + /** + * Indicates if the zoom buttons are visible. This is a bound property and can be listed for using a + * PropertyChangeListener + * @return current value of the zoomButtonsVisible property + */ + public boolean isZoomButtonsVisible() + { + return zoomButtonsVisible; + } + + /** + * Sets if the zoom buttons should be visible. This ia bound property. Changes can be listened for using a + * PropertyChaneListener + * @param zoomButtonsVisible new value of the zoomButtonsVisible property + */ + public void setZoomButtonsVisible(boolean zoomButtonsVisible) + { + boolean old = this.isZoomButtonsVisible(); + this.zoomButtonsVisible = zoomButtonsVisible; + zoomInButton.setVisible(zoomButtonsVisible); + zoomOutButton.setVisible(zoomButtonsVisible); + firePropertyChange("zoomButtonsVisible", old, this.isZoomButtonsVisible()); + } + + /** + * Sets the tile factory for both embedded JXMapViewer components. Calling this method will also reset the center + * and zoom levels of both maps, as well as the bounds of the zoom slider. + * @param fact the new TileFactory + */ + public void setTileFactory(TileFactory fact) + { + mainMap.setTileFactory(fact); + mainMap.setZoom(fact.getInfo().getDefaultZoomLevel()); + mainMap.setCenterPosition(new GeoPosition(0, 0)); + miniMap.setTileFactory(fact); + miniMap.setZoom(fact.getInfo().getDefaultZoomLevel() + 3); + miniMap.setCenterPosition(new GeoPosition(0, 0)); + zoomSlider.setMinimum(fact.getInfo().getMinimumZoomLevel()); + zoomSlider.setMaximum(fact.getInfo().getMaximumZoomLevel()); + } + + /** + * @param pos the new center position + */ + public void setCenterPosition(GeoPosition pos) + { + mainMap.setCenterPosition(pos); + miniMap.setCenterPosition(pos); + } + + /** + * @return the center geo position + */ + public GeoPosition getCenterPosition() + { + return mainMap.getCenterPosition(); + } + + /** + * @return the adress location + */ + public GeoPosition getAddressLocation() + { + return mainMap.getAddressLocation(); + } + + /** + * @param pos the address location + */ + public void setAddressLocation(GeoPosition pos) + { + mainMap.setAddressLocation(pos); + } + + /** + * Returns a reference to the main embedded JXMapViewer component + * @return the main map + */ + public JXMapViewer getMainMap() + { + return this.mainMap; + } + + /** + * Returns a reference to the mini embedded JXMapViewer component + * @return the minimap JXMapViewer component + */ + public JXMapViewer getMiniMap() + { + return this.miniMap; + } + + /** + * returns a reference to the zoom in button + * @return a jbutton + */ + public JButton getZoomInButton() + { + return this.zoomInButton; + } + + /** + * returns a reference to the zoom out button + * @return a jbutton + */ + public JButton getZoomOutButton() + { + return this.zoomOutButton; + } + + /** + * returns a reference to the zoom slider + * @return a jslider + */ + public JSlider getZoomSlider() + { + return this.zoomSlider; + } + + /** + * @param b the visibility flag + */ + public void setAddressLocationShown(boolean b) + { + boolean old = isAddressLocationShown(); + this.addressLocationShown = b; + addressLocationPainter.setVisible(b); + firePropertyChange("addressLocationShown", old, b); + repaint(); + } + + /** + * @return true if the address location is shown + */ + public boolean isAddressLocationShown() + { + return addressLocationShown; + } + + /** + * @param b the visibility flag + */ + public void setDataProviderCreditShown(boolean b) + { + boolean old = isDataProviderCreditShown(); + this.dataProviderCreditShown = b; + dataProviderCreditPainter.setVisible(b); + repaint(); + firePropertyChange("dataProviderCreditShown", old, b); + } + + /** + * @return true if the data provider credit is shown + */ + public boolean isDataProviderCreditShown() + { + return dataProviderCreditShown; + } + + @SuppressWarnings("unchecked") + private void rebuildMainMapOverlay() + { + CompoundPainter cp = new CompoundPainter(); + cp.setCacheable(false); + /* + * List ptrs = new ArrayList(); if(isDataProviderCreditShown()) { + * ptrs.add(dataProviderCreditPainter); } if(isAddressLocationShown()) { ptrs.add(addressLocationPainter); } + */ + cp.setPainters(dataProviderCreditPainter, addressLocationPainter); + mainMap.setOverlayPainter(cp); + } + + /** + * @param prov the default provider + */ + public void setDefaultProvider(DefaultProviders prov) + { + DefaultProviders old = this.defaultProvider; + this.defaultProvider = prov; + if (prov == DefaultProviders.SwingLabsBlueMarble) + { + setTileFactory(new CylindricalProjectionTileFactory()); + setZoom(3); + } + if (prov == DefaultProviders.OpenStreetMaps) + { + TileFactoryInfo info = new OSMTileFactoryInfo(); + TileFactory tf = new DefaultTileFactory(info); + setTileFactory(tf); + setZoom(11); + setAddressLocation(new GeoPosition(51.5, 0)); + } + firePropertyChange("defaultProvider", old, prov); + repaint(); + } + + /** + * @return the default provider + */ + public DefaultProviders getDefaultProvider() + { + return this.defaultProvider; + } + + private AbstractPainter dataProviderCreditPainter = new AbstractPainter(false) + { + @Override + protected void doPaint(Graphics2D g, JXMapViewer map, int width, int height) + { + g.setPaint(Color.WHITE); + g.drawString("data ", 50, map.getHeight() - 10); + } + }; + + private WaypointPainter addressLocationPainter = new WaypointPainter() + { + @Override + public Set getWaypoints() + { + Set set = new HashSet(); + if (getAddressLocation() != null) + { + set.add(new DefaultWaypoint(getAddressLocation())); + } + else + { + set.add(new DefaultWaypoint(0, 0)); + } + return set; + } + }; + + /** + * @param args the program args + */ + public static void main(String... args) + { + SwingUtilities.invokeLater(new Runnable() + { + @Override + public void run() + { + JXMapKit kit = new JXMapKit(); + kit.setDefaultProvider(DefaultProviders.OpenStreetMaps); + + TileFactoryInfo info = new OSMTileFactoryInfo(); + TileFactory tf = new DefaultTileFactory(info); + kit.setTileFactory(tf); + kit.setZoom(14); + kit.setAddressLocation(new GeoPosition(51.5, 0)); + kit.getMainMap().setDrawTileBorders(true); + kit.getMainMap().setRestrictOutsidePanning(true); + kit.getMainMap().setHorizontalWrapped(false); + + ((DefaultTileFactory) kit.getMainMap().getTileFactory()).setThreadPoolSize(8); + JFrame frame = new JFrame("JXMapKit test"); + frame.add(kit); + frame.pack(); + frame.setSize(500, 300); + frame.setVisible(true); + } + }); + } +} diff --git a/src/org/jdesktop/swingx/JXMapViewer.java b/src/org/jdesktop/swingx/JXMapViewer.java new file mode 100644 index 00000000..90bccaa5 --- /dev/null +++ b/src/org/jdesktop/swingx/JXMapViewer.java @@ -0,0 +1,781 @@ +/* + * MapViewer.java + * + * Created on March 14, 2006, 2:14 PM + * + * To change this template, choose Tools | Template Manager + * and open the template in the editor. + */ + +package org.jdesktop.swingx; + +import java.awt.Color; +import java.awt.Dimension; +import java.awt.Graphics; +import java.awt.Graphics2D; +import java.awt.Image; +import java.awt.Insets; +import java.awt.Rectangle; +import java.awt.geom.Point2D; +import java.awt.geom.Rectangle2D; +import java.awt.image.BufferedImage; +import java.beans.DesignMode; +import java.beans.PropertyChangeEvent; +import java.beans.PropertyChangeListener; +import java.net.URL; +import java.util.Set; + +import javax.imageio.ImageIO; +import javax.swing.JPanel; + +import org.jdesktop.swingx.mapviewer.GeoPosition; +import org.jdesktop.swingx.mapviewer.Tile; +import org.jdesktop.swingx.mapviewer.TileFactory; +import org.jdesktop.swingx.mapviewer.TileFactoryInfo; +import org.jdesktop.swingx.mapviewer.TileListener; +import org.jdesktop.swingx.mapviewer.empty.EmptyTileFactory; +import org.jdesktop.swingx.painter.AbstractPainter; +import org.jdesktop.swingx.painter.Painter; + +/** + * A tile oriented map component that can easily be used with tile sources + * on the web like Google and Yahoo maps, satellite data such as NASA imagery, + * and also with file based sources like pre-processed NASA images. + * A known map provider can be used with the SLMapServerInfo, + * which will connect to a 2km resolution version of NASA's Blue Marble Next Generation + * imagery. @see SLMapServerInfo for more information. + * + * Note, the JXMapViewer has three center point properties. The addressLocation property + * represents an abstract center of the map. This would usually be something like the first item + * in a search result. It is a {@link GeoPosition}. The centerPosition property represents + * the current center point of the map. If the user pans the map then the centerPosition point will + * change but the addressLocation will not. Calling recenterToAddressLocation() will move the map + * back to that center address. The center property represents the same point as the centerPosition + * property, but as a Point2D in pixel space instead of a GeoPosition in lat/long space. Note that + * the center property is a Point2D in the entire world bitmap, not in the portion of the map currently + * visible. You can use the getViewportBounds() method to find the portion of the map currently visible + * and adjust your calculations accordingly. Changing the center property will change the centerPosition + * property and vice versa. All three properties are bound. + * @author Joshua.Marinacci@sun.com + * @see org.jdesktop.swingx.mapviewer.bmng.SLMapServerInfo + */ +public class JXMapViewer extends JPanel implements DesignMode +{ + private static final long serialVersionUID = -3530746298586937321L; + + private final boolean isNegativeYAllowed = true; // maybe rename to isNorthBounded and isSouthBounded? + + /** + * The zoom level. Generally a value between 1 and 15 (TODO Is this true for all the mapping worlds? What does this + * mean if some mapping system doesn't support the zoom level? + */ + private int zoomLevel = 1; + + /** + * The position, in map coordinates of the center point. This is defined as the distance from the top and + * left edges of the map in pixels. Dragging the map component will change the center position. Zooming in/out will + * cause the center to be recalculated so as to remain in the center of the new "map". + */ + private Point2D center = new Point2D.Double(0, 0); + + /** + * Indicates whether or not to draw the borders between tiles. Defaults to false. TODO Generally not very nice + * looking, very much a product of testing Consider whether this should really be a property or not. + */ + private boolean drawTileBorders = false; + + /** + * Factory used by this component to grab the tiles necessary for painting the map. + */ + private TileFactory factory; + + /** + * The position in latitude/longitude of the "address" being mapped. This is a special coordinate that, when moved, + * will cause the map to be moved as well. It is separate from "center" in that "center" tracks the current center + * (in pixels) of the viewport whereas this will not change when panning or zooming. Whenever the addressLocation is + * changed, however, the map will be repositioned. + */ + private GeoPosition addressLocation; + + /** + * The overlay to delegate to for painting the "foreground" of the map component. This would include painting + * waypoints, day/night, etc. Also receives mouse events. + */ + private Painter overlay; + + private boolean designTime; + + private Image loadingImage; + + private boolean restrictOutsidePanning = true; + private boolean horizontalWrapped = true; + + /** + * Create a new JXMapViewer. By default it will use the EmptyTileFactory + */ + public JXMapViewer() + { + factory = new EmptyTileFactory(); + // setTileFactory(new GoogleTileFactory()); + + // make a dummy loading image + try + { + URL url = this.getClass().getResource("mapviewer/resources/loading.png"); + this.setLoadingImage(ImageIO.read(url)); + } + catch (Throwable ex) + { + System.out.println("could not load 'loading.png'"); + BufferedImage img = new BufferedImage(16, 16, BufferedImage.TYPE_INT_ARGB); + Graphics2D g2 = img.createGraphics(); + g2.setColor(Color.black); + g2.fillRect(0, 0, 16, 16); + g2.dispose(); + this.setLoadingImage(img); + } + + // setAddressLocation(new GeoPosition(37.392137,-121.950431)); // Sun campus + } + + @Override + protected void paintComponent(Graphics g) + { + super.paintComponent(g); + + doPaintComponent(g); + } + + // the method that does the actual painting + private void doPaintComponent(Graphics g) + {/* + * if (isOpaque() || isDesignTime()) { g.setColor(getBackground()); g.fillRect(0,0,getWidth(),getHeight()); } + */ + + if (isDesignTime()) + { + // do nothing + } + else + { + int z = getZoom(); + Rectangle viewportBounds = getViewportBounds(); + drawMapTiles(g, z, viewportBounds); + drawOverlays(z, g, viewportBounds); + } + + super.paintBorder(g); + } + + /** + * Indicate that the component is being used at design time, such as in a visual editor like NetBeans' Matisse + * @param b indicates if the component is being used at design time + */ + @Override + public void setDesignTime(boolean b) + { + this.designTime = b; + } + + /** + * Indicates whether the component is being used at design time, such as in a visual editor like NetBeans' Matisse + * @return boolean indicating if the component is being used at design time + */ + @Override + public boolean isDesignTime() + { + return designTime; + } + + /** + * Draw the map tiles. This method is for implementation use only. + * @param g Graphics + * @param zoom zoom level to draw at + * @param viewportBounds the bounds to draw within + */ + protected void drawMapTiles(final Graphics g, final int zoom, Rectangle viewportBounds) + { + int size = getTileFactory().getTileSize(zoom); + Dimension mapSize = getTileFactory().getMapSize(zoom); + + // calculate the "visible" viewport area in tiles + int numWide = viewportBounds.width / size + 2; + int numHigh = viewportBounds.height / size + 2; + + // TilePoint topLeftTile = getTileFactory().getTileCoordinate( + // new Point2D.Double(viewportBounds.x, viewportBounds.y)); + TileFactoryInfo info = getTileFactory().getInfo(); + int tpx = (int) Math.floor(viewportBounds.getX() / info.getTileSize(0)); + int tpy = (int) Math.floor(viewportBounds.getY() / info.getTileSize(0)); + // TilePoint topLeftTile = new TilePoint(tpx, tpy); + + // p("top tile = " + topLeftTile); + // fetch the tiles from the factory and store them in the tiles cache + // attach the tileLoadListener + for (int x = 0; x <= numWide; x++) + { + for (int y = 0; y <= numHigh; y++) + { + int itpx = x + tpx;// topLeftTile.getX(); + int itpy = y + tpy;// topLeftTile.getY(); + // TilePoint point = new TilePoint(x + topLeftTile.getX(), y + topLeftTile.getY()); + // only proceed if the specified tile point lies within the area being painted + if (g.getClipBounds().intersects( + new Rectangle(itpx * size - viewportBounds.x, itpy * size - viewportBounds.y, size, size))) + { + Tile tile = getTileFactory().getTile(itpx, itpy, zoom); + int ox = ((itpx * getTileFactory().getTileSize(zoom)) - viewportBounds.x); + int oy = ((itpy * getTileFactory().getTileSize(zoom)) - viewportBounds.y); + + // if the tile is off the map to the north/south, then just don't paint anything + if (isTileOnMap(itpx, itpy, mapSize)) + { + if (isOpaque()) + { + g.setColor(getBackground()); + g.fillRect(ox, oy, size, size); + } + } + else if (tile.isLoaded()) + { + g.drawImage(tile.getImage(), ox, oy, null); + } + else + { + // Use tile at higher zoom level with 200% magnification + Tile superTile = getTileFactory().getTile(itpx / 2, itpy / 2, zoom + 1); + + if (superTile.isLoaded()) + { + int offX = (itpx % 2) * size / 2; + int offY = (itpy % 2) * size / 2; + g.drawImage(superTile.getImage(), ox, oy, ox + size, oy + size, offX, offY, offX + size / 2, offY + size / 2, null); + } + else + { + int imageX = (getTileFactory().getTileSize(zoom) - getLoadingImage().getWidth(null)) / 2; + int imageY = (getTileFactory().getTileSize(zoom) - getLoadingImage().getHeight(null)) / 2; + g.setColor(Color.GRAY); + g.fillRect(ox, oy, size, size); + g.drawImage(getLoadingImage(), ox + imageX, oy + imageY, null); + } + } + if (isDrawTileBorders()) + { + + g.setColor(Color.black); + g.drawRect(ox, oy, size, size); + g.drawRect(ox + size / 2 - 5, oy + size / 2 - 5, 10, 10); + g.setColor(Color.white); + g.drawRect(ox + 1, oy + 1, size, size); + + String text = itpx + ", " + itpy + ", " + getZoom(); + g.setColor(Color.BLACK); + g.drawString(text, ox + 10, oy + 30); + g.drawString(text, ox + 10 + 2, oy + 30 + 2); + g.setColor(Color.WHITE); + g.drawString(text, ox + 10 + 1, oy + 30 + 1); + } + } + } + } + } + + @SuppressWarnings("unused") + private void drawOverlays(final int zoom, final Graphics g, final Rectangle viewportBounds) + { + if (overlay != null) + { + overlay.paint((Graphics2D) g, this, getWidth(), getHeight()); + } + } + + @SuppressWarnings("unused") + private boolean isTileOnMap(int x, int y, Dimension mapSize) + { + return !isNegativeYAllowed && y < 0 || y >= mapSize.getHeight(); + } + + /** + * Sets the map overlay. This is a Painter which will paint on top of the map. It can be used to draw waypoints, + * lines, or static overlays like text messages. + * @param overlay the map overlay to use + */ + public void setOverlayPainter(Painter overlay) + { + Painter old = getOverlayPainter(); + this.overlay = overlay; + + PropertyChangeListener listener = new PropertyChangeListener() + { + @Override + public void propertyChange(PropertyChangeEvent evt) + { + if (evt.getNewValue().equals(Boolean.TRUE)) + { + repaint(); + } + } + }; + + if (old instanceof AbstractPainter) + { + AbstractPainter ap = (AbstractPainter) overlay; + ap.removePropertyChangeListener("dirty", listener); + } + + if (overlay instanceof AbstractPainter) + { + AbstractPainter ap = (AbstractPainter) overlay; + ap.addPropertyChangeListener("dirty", listener); + } + + firePropertyChange("mapOverlay", old, getOverlayPainter()); + repaint(); + } + + /** + * Gets the current map overlay + * @return the current map overlay + */ + public Painter getOverlayPainter() + { + return overlay; + } + + /** + * Returns the bounds of the viewport in pixels. This can be used to transform points into the world bitmap + * coordinate space. + * @return the bounds in pixels of the "view" of this map + */ + public Rectangle getViewportBounds() + { + return calculateViewportBounds(getCenter()); + } + + private Rectangle calculateViewportBounds(Point2D centr) + { + Insets insets = getInsets(); + // calculate the "visible" viewport area in pixels + int viewportWidth = getWidth() - insets.left - insets.right; + int viewportHeight = getHeight() - insets.top - insets.bottom; + double viewportX = (centr.getX() - viewportWidth / 2); + double viewportY = (centr.getY() - viewportHeight / 2); + return new Rectangle((int) viewportX, (int) viewportY, viewportWidth, viewportHeight); + } + + /** + * Set the current zoom level + * @param zoom the new zoom level + */ + public void setZoom(int zoom) + { + if (zoom == this.zoomLevel) + { + return; + } + + TileFactoryInfo info = getTileFactory().getInfo(); + // don't repaint if we are out of the valid zoom levels + if (info != null && (zoom < info.getMinimumZoomLevel() || zoom > info.getMaximumZoomLevel())) + { + return; + } + + // if(zoom >= 0 && zoom <= 15 && zoom != this.zoom) { + int oldzoom = this.zoomLevel; + Point2D oldCenter = getCenter(); + Dimension oldMapSize = getTileFactory().getMapSize(oldzoom); + this.zoomLevel = zoom; + this.firePropertyChange("zoom", oldzoom, zoom); + + Dimension mapSize = getTileFactory().getMapSize(zoom); + + setCenter(new Point2D.Double(oldCenter.getX() * (mapSize.getWidth() / oldMapSize.getWidth()), oldCenter.getY() + * (mapSize.getHeight() / oldMapSize.getHeight()))); + + repaint(); + } + + /** + * Gets the current zoom level + * @return the current zoom level + */ + public int getZoom() + { + return this.zoomLevel; + } + + /** + * Gets the current address location of the map. This property does not change when the user pans the map. This + * property is bound. + * @return the current map location (address) + */ + public GeoPosition getAddressLocation() + { + return addressLocation; + } + + /** + * Gets the current address location of the map + * @param addressLocation the new address location + */ + public void setAddressLocation(GeoPosition addressLocation) + { + GeoPosition old = getAddressLocation(); + this.addressLocation = addressLocation; + setCenter(getTileFactory().geoToPixel(addressLocation, getZoom())); + + firePropertyChange("addressLocation", old, getAddressLocation()); + repaint(); + } + + /** + * Re-centers the map to have the current address location be at the center of the map, accounting for the map's + * width and height. + */ + public void recenterToAddressLocation() + { + setCenter(getTileFactory().geoToPixel(getAddressLocation(), getZoom())); + repaint(); + } + + /** + * Indicates if the tile borders should be drawn. Mainly used for debugging. + * @return the value of this property + */ + public boolean isDrawTileBorders() + { + return drawTileBorders; + } + + /** + * Set if the tile borders should be drawn. Mainly used for debugging. + * @param drawTileBorders new value of this drawTileBorders + */ + public void setDrawTileBorders(boolean drawTileBorders) + { + boolean old = isDrawTileBorders(); + this.drawTileBorders = drawTileBorders; + firePropertyChange("drawTileBorders", old, isDrawTileBorders()); + repaint(); + } + + /** + * A property indicating the center position of the map + * @param geoPosition the new property value + */ + public void setCenterPosition(GeoPosition geoPosition) + { + GeoPosition oldVal = getCenterPosition(); + setCenter(getTileFactory().geoToPixel(geoPosition, zoomLevel)); + repaint(); + GeoPosition newVal = getCenterPosition(); + firePropertyChange("centerPosition", oldVal, newVal); + } + + /** + * A property indicating the center position of the map + * @return the current center position + */ + public GeoPosition getCenterPosition() + { + return getTileFactory().pixelToGeo(getCenter(), zoomLevel); + } + + /** + * Get the current factory + * @return the current property value + */ + public TileFactory getTileFactory() + { + return factory; + } + + /** + * Set the current tile factory (must not be null) + * @param factory the new property value + */ + public void setTileFactory(TileFactory factory) + { + if (factory == null) + throw new NullPointerException("factory must not be null"); + + this.factory.removeTileListener(tileLoadListener); + this.factory.dispose(); + + this.factory = factory; + this.setZoom(factory.getInfo().getDefaultZoomLevel()); + + factory.addTileListener(tileLoadListener); + + repaint(); + } + + /** + * A property for an image which will be display when an image is still loading. + * @return the current property value + */ + public Image getLoadingImage() + { + return loadingImage; + } + + /** + * A property for an image which will be display when an image is still loading. + * @param loadingImage the new property value + */ + public void setLoadingImage(Image loadingImage) + { + this.loadingImage = loadingImage; + } + + /** + * Gets the current pixel center of the map. This point is in the global bitmap coordinate system, not as lat/longs. + * @return the current center of the map as a pixel value + */ + public Point2D getCenter() + { + return center; + } + + /** + * Sets the new center of the map in pixel coordinates. + * @param center the new center of the map in pixel coordinates + */ + public void setCenter(Point2D center) + { + Point2D old = this.getCenter(); + + double centerX = center.getX(); + double centerY = center.getY(); + + Dimension mapSize = getTileFactory().getMapSize(getZoom()); + int mapHeight = (int) mapSize.getHeight() * getTileFactory().getTileSize(getZoom()); + int mapWidth = (int) mapSize.getWidth() * getTileFactory().getTileSize(getZoom()); + + if (isRestrictOutsidePanning()) + { + Insets insets = getInsets(); + int viewportHeight = getHeight() - insets.top - insets.bottom; + int viewportWidth = getWidth() - insets.left - insets.right; + + // don't let the user pan over the top edge + Rectangle newVP = calculateViewportBounds(center); + if (newVP.getY() < 0) + { + centerY = viewportHeight / 2; + } + + // don't let the user pan over the left edge + if (!isHorizontalWrapped() && newVP.getX() < 0) + { + centerX = viewportWidth / 2; + } + + // don't let the user pan over the bottom edge + if (newVP.getY() + newVP.getHeight() > mapHeight) + { + centerY = mapHeight - viewportHeight / 2; + } + + // don't let the user pan over the right edge + if (!isHorizontalWrapped() && (newVP.getX() + newVP.getWidth() > mapWidth)) + { + centerX = mapWidth - viewportWidth / 2; + } + + // if map is to small then just center it vert + if (mapHeight < newVP.getHeight()) + { + centerY = mapHeight / 2;// viewportHeight/2;// - mapHeight/2; + } + + // if map is too small then just center it horiz + if (!isHorizontalWrapped() && mapWidth < newVP.getWidth()) + { + centerX = mapWidth / 2; + } + } + + // If center is outside (0, 0,mapWidth, mapHeight) + // compute modulo to get it back in. + { + centerX = centerX % mapWidth; + centerY = centerY % mapHeight; + + if (centerX < 0) + centerX += mapWidth; + + if (centerY < 0) + centerY += mapHeight; + } + + GeoPosition oldGP = this.getCenterPosition(); + this.center = new Point2D.Double(centerX, centerY); + firePropertyChange("center", old, this.center); + firePropertyChange("centerPosition", oldGP, this.getCenterPosition()); + repaint(); + } + + /** + * Calculates a zoom level so that all points in the specified set will be visible on screen. This is useful if you + * have a bunch of points in an area like a city and you want to zoom out so that the entire city and it's points + * are visible without panning. + * @param positions A set of GeoPositions to calculate the new zoom from + */ + public void calculateZoomFrom(Set positions) + { + // u.p("calculating a zoom based on: "); + // u.p(positions); + if (positions.size() < 2) + { + return; + } + + int zoom = getZoom(); + Rectangle2D rect = generateBoundingRect(positions, zoom); + // Rectangle2D viewport = map.getViewportBounds(); + int count = 0; + while (!getViewportBounds().contains(rect)) + { + // u.p("not contained"); + Point2D centr = new Point2D.Double(rect.getX() + rect.getWidth() / 2, rect.getY() + rect.getHeight() / 2); + GeoPosition px = getTileFactory().pixelToGeo(centr, zoom); + // u.p("new geo = " + px); + setCenterPosition(px); + count++; + if (count > 30) + break; + + if (getViewportBounds().contains(rect)) + { + // u.p("did it finally"); + break; + } + zoom = zoom + 1; + if (zoom > 15) + { + break; + } + setZoom(zoom); + rect = generateBoundingRect(positions, zoom); + } + } + + private Rectangle2D generateBoundingRect(final Set positions, int zoom) + { + Point2D point1 = getTileFactory().geoToPixel(positions.iterator().next(), zoom); + Rectangle2D rect = new Rectangle2D.Double(point1.getX(), point1.getY(), 0, 0); + + for (GeoPosition pos : positions) + { + Point2D point = getTileFactory().geoToPixel(pos, zoom); + rect.add(point); + } + return rect; + } + + // a property change listener which forces repaints when tiles finish loading + private TileListener tileLoadListener = new TileListener() + { + @Override + public void tileLoaded(Tile tile) + { + if (tile.getZoom() == getZoom()) + { + repaint(); + /* this optimization doesn't save much and it doesn't work if you + * wrap around the world + Rectangle viewportBounds = getViewportBounds(); + TilePoint tilePoint = t.getLocation(); + Point point = new Point(tilePoint.getX() * getTileFactory().getTileSize(), tilePoint.getY() * getTileFactory().getTileSize()); + Rectangle tileRect = new Rectangle(point, new Dimension(getTileFactory().getTileSize(), getTileFactory().getTileSize())); + if (viewportBounds.intersects(tileRect)) { + //convert tileRect from world space to viewport space + repaint(new Rectangle( + tileRect.x - viewportBounds.x, + tileRect.y - viewportBounds.y, + tileRect.width, + tileRect.height + )); + }*/ + } + } + + }; + + /** + * @return true if panning is restricted or not + */ + public boolean isRestrictOutsidePanning() + { + return restrictOutsidePanning; + } + + /** + * @param restrictOutsidePanning set if panning is restricted or not + */ + public void setRestrictOutsidePanning(boolean restrictOutsidePanning) + { + this.restrictOutsidePanning = restrictOutsidePanning; + } + + /** + * @return true if horizontally wrapped or not + */ + public boolean isHorizontalWrapped() + { + return horizontalWrapped; + } + + /** + * @param horizontalWrapped true if horizontal wrap is enabled + */ + public void setHorizontalWrapped(boolean horizontalWrapped) + { + this.horizontalWrapped = horizontalWrapped; + } + + /** + * Converts the specified GeoPosition to a point in the JXMapViewer's local coordinate space. This method is + * especially useful when drawing lat/long positions on the map. + * @param pos a GeoPosition on the map + * @return the point in the local coordinate space of the map + */ + public Point2D convertGeoPositionToPoint(GeoPosition pos) + { + // convert from geo to world bitmap + Point2D pt = getTileFactory().geoToPixel(pos, getZoom()); + // convert from world bitmap to local + Rectangle bounds = getViewportBounds(); + return new Point2D.Double(pt.getX() - bounds.getX(), pt.getY() - bounds.getY()); + } + + /** + * Converts the specified Point2D in the JXMapViewer's local coordinate space to a GeoPosition on the map. This + * method is especially useful for determining the GeoPosition under the mouse cursor. + * @param pt a point in the local coordinate space of the map + * @return the point converted to a GeoPosition + */ + public GeoPosition convertPointToGeoPosition(Point2D pt) + { + // convert from local to world bitmap + Rectangle bounds = getViewportBounds(); + Point2D pt2 = new Point2D.Double(pt.getX() + bounds.getX(), pt.getY() + bounds.getY()); + + // convert from world bitmap to geo + GeoPosition pos = getTileFactory().pixelToGeo(pt2, getZoom()); + return pos; + } + + + + /** + * @return isNegativeYAllowed + */ + public boolean isNegativeYAllowed() + { + return isNegativeYAllowed; + } +} diff --git a/src/org/jdesktop/swingx/OSMTileFactoryInfo.java b/src/org/jdesktop/swingx/OSMTileFactoryInfo.java new file mode 100644 index 00000000..4c0510d3 --- /dev/null +++ b/src/org/jdesktop/swingx/OSMTileFactoryInfo.java @@ -0,0 +1,59 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +// Fraunhofer Institute for Computer Graphics Research (IGD) +// Department Information Visualization and Visual Analytics +// +// Copyright (c) Fraunhofer IGD. All rights reserved. +// +// This source code is property of the Fraunhofer IGD and underlies +// copyright restrictions. It may only be used with explicit +// permission from the respective owner. + +package org.jdesktop.swingx; + +import org.jdesktop.swingx.mapviewer.TileFactoryInfo; + +/** + * Uses OpenStreetMap + * @author Martin Dummer + */ +public class OSMTileFactoryInfo extends TileFactoryInfo +{ + private static final int max = 19; + + /** + * Default constructor + */ + public OSMTileFactoryInfo() + { + super("OpenStreetMap", + 1, max - 2, max, + 256, true, true, // tile size is 256 and x/y orientation is normal + "http://tile.openstreetmap.org", + "x", "y", "z"); // 5/15/10.png + } + + @Override + public String getTileUrl(int x, int y, int zoom) + { + zoom = max - zoom; + String url = this.baseURL + "/" + zoom + "/" + x + "/" + y + ".png"; + return url; + } + +} diff --git a/src/org/jdesktop/swingx/VirtualEarthTileFactoryInfo.java b/src/org/jdesktop/swingx/VirtualEarthTileFactoryInfo.java new file mode 100644 index 00000000..d9784011 --- /dev/null +++ b/src/org/jdesktop/swingx/VirtualEarthTileFactoryInfo.java @@ -0,0 +1,134 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package org.jdesktop.swingx; + +import org.jdesktop.swingx.mapviewer.TileFactoryInfo; + +/******************************************************************************* + * http://www.viavirtualearth.com/vve/Articles/RollYourOwnTileServer.ashx + * @author Fabrizio Giudici + * @version $Id: MicrosoftVirtualEarthProvider.java 115 2007-11-08 22:04:36Z + * fabriziogiudici $ + ********************************************************************************/ +public class VirtualEarthTileFactoryInfo extends TileFactoryInfo +{ + /** + * Use road map + */ + public final static MVEMode MAP = new MVEMode("map", "map", "r", ".png"); + + /** + * Use satellite map + */ + public final static MVEMode SATELLITE = new MVEMode("satellite", "satellite", "a", ".jpeg"); + + /** + * Use hybrid map + */ + public final static MVEMode HYBRID = new MVEMode("hybrid", "hybrid", "h", ".jpeg"); + + /** + * The map mode + */ + public static class MVEMode + { + private String type; + private String ext; + private String name; + private String label; + + private MVEMode(final String name, final String label, final String type, final String ext) + { + this.type = type; + this.ext = ext; + this.name = name; + this.label = label; + } + } + + private final static int TOP_ZOOM_LEVEL = 19; + + private final static int MAX_ZOOM_LEVEL = 17; + + private final static int MIN_ZOOM_LEVEL = 2; + + private final static int TILE_SIZE = 256; + + private MVEMode mode; + + /** + * @param mode the mode + */ + public VirtualEarthTileFactoryInfo(MVEMode mode) + { + super("Virtual Earth", MIN_ZOOM_LEVEL, MAX_ZOOM_LEVEL, TOP_ZOOM_LEVEL, TILE_SIZE, false, false, "", "", "", ""); + + this.mode = mode; + } + + /** + * @return the name of the selected mode + */ + public String getModeName() + { + return mode.name; + } + + /** + * @return the label of the selected mode + */ + public String getModeLabel() + { + return mode.label; + } + + @Override + public String getTileUrl(final int x, final int y, final int zoom) + { + final String quad = tileToQuadKey(x, y, TOP_ZOOM_LEVEL - 0 - zoom); + return "http://" + mode.type + quad.charAt(quad.length() - 1) + + ".ortho.tiles.virtualearth.net/tiles/" + + mode.type + quad + mode.ext + "?g=1"; + } + + private String tileToQuadKey(final int tx, final int ty, final int zl) + { + String quad = ""; + + for (int i = zl; i > 0; i--) + { + int mask = 1 << (i - 1); + int cell = 0; + + if ((tx & mask) != 0) + { + cell++; + } + + if ((ty & mask) != 0) + { + cell += 2; + } + + quad += cell; + } + + return quad; + } +} + diff --git a/src/org/jdesktop/swingx/input/CenterMapListener.java b/src/org/jdesktop/swingx/input/CenterMapListener.java new file mode 100644 index 00000000..34eb7fa1 --- /dev/null +++ b/src/org/jdesktop/swingx/input/CenterMapListener.java @@ -0,0 +1,72 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package org.jdesktop.swingx.input; + +import java.awt.Rectangle; +import java.awt.event.MouseAdapter; +import java.awt.event.MouseEvent; +import java.awt.geom.Point2D; + +import javax.swing.SwingUtilities; + +import org.jdesktop.swingx.JXMapViewer; + +/** + * Centers the map on the mouse cursor + * if left is double-clicked or middle mouse + * button is pressed. + * @author Martin Steiger + * @author joshy + */ +public class CenterMapListener extends MouseAdapter +{ + private JXMapViewer viewer; + + /** + * @param viewer the jxmapviewer + */ + public CenterMapListener(JXMapViewer viewer) + { + this.viewer = viewer; + } + + @Override + public void mousePressed(MouseEvent evt) + { + boolean left = SwingUtilities.isLeftMouseButton(evt); + boolean middle = SwingUtilities.isMiddleMouseButton(evt); + boolean doubleClick = (evt.getClickCount() == 2); + + if (middle || (left && doubleClick)) + { + recenterMap(evt); + } + } + + private void recenterMap(MouseEvent evt) + { + Rectangle bounds = viewer.getViewportBounds(); + double x = bounds.getX() + evt.getX(); + double y = bounds.getY() + evt.getY(); + viewer.setCenter(new Point2D.Double(x, y)); + viewer.setZoom(viewer.getZoom() - 1); + viewer.repaint(); + } +} + + diff --git a/src/org/jdesktop/swingx/input/PanKeyListener.java b/src/org/jdesktop/swingx/input/PanKeyListener.java new file mode 100644 index 00000000..6ccd2bba --- /dev/null +++ b/src/org/jdesktop/swingx/input/PanKeyListener.java @@ -0,0 +1,76 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package org.jdesktop.swingx.input; + +import java.awt.Rectangle; +import java.awt.event.KeyAdapter; +import java.awt.event.KeyEvent; +import java.awt.geom.Point2D; + +import org.jdesktop.swingx.JXMapViewer; + +/** + * used to pan using the arrow keys + * @author joshy + */ +public class PanKeyListener extends KeyAdapter +{ + private static final int OFFSET = 10; + + private JXMapViewer viewer; + + /** + * @param viewer the jxmapviewer + */ + public PanKeyListener(JXMapViewer viewer) + { + this.viewer = viewer; + } + + @Override + public void keyPressed(KeyEvent e) + { + int delta_x = 0; + int delta_y = 0; + + switch (e.getKeyCode()) + { + case KeyEvent.VK_LEFT: + delta_x = -OFFSET; + break; + case KeyEvent.VK_RIGHT: + delta_x = OFFSET; + break; + case KeyEvent.VK_UP: + delta_y = -OFFSET; + break; + case KeyEvent.VK_DOWN: + delta_y = OFFSET; + break; + } + + if (delta_x != 0 || delta_y != 0) + { + Rectangle bounds = viewer.getViewportBounds(); + double x = bounds.getCenterX() + delta_x; + double y = bounds.getCenterY() + delta_y; + viewer.setCenter(new Point2D.Double(x, y)); + viewer.repaint(); + } + } +} diff --git a/src/org/jdesktop/swingx/input/PanMouseInputListener.java b/src/org/jdesktop/swingx/input/PanMouseInputListener.java new file mode 100644 index 00000000..739d772f --- /dev/null +++ b/src/org/jdesktop/swingx/input/PanMouseInputListener.java @@ -0,0 +1,106 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package org.jdesktop.swingx.input; + +import java.awt.Cursor; +import java.awt.Point; +import java.awt.event.MouseEvent; +import java.awt.geom.Point2D; + +import javax.swing.SwingUtilities; +import javax.swing.event.MouseInputAdapter; + +import org.jdesktop.swingx.JXMapViewer; + +/** + * Used to pan using press and drag mouse gestures + * @author joshy + */ +public class PanMouseInputListener extends MouseInputAdapter +{ + private Point prev; + private JXMapViewer viewer; + + /** + * @param viewer the jxmapviewer + */ + public PanMouseInputListener(JXMapViewer viewer) + { + this.viewer = viewer; + } + + @Override + public void mousePressed(MouseEvent evt) + { + prev = evt.getPoint(); + } + + @Override + public void mouseDragged(MouseEvent evt) + { + if (!SwingUtilities.isLeftMouseButton(evt)) + return; + + Point current = evt.getPoint(); + double x = viewer.getCenter().getX() - (current.x - prev.x); + double y = viewer.getCenter().getY() - (current.y - prev.y); + + if (!viewer.isNegativeYAllowed()) + { + if (y < 0) + { + y = 0; + } + } + + int maxHeight = (int) (viewer.getTileFactory().getMapSize(viewer.getZoom()).getHeight() * viewer + .getTileFactory().getTileSize(viewer.getZoom())); + if (y > maxHeight) + { + y = maxHeight; + } + + prev = current; + viewer.setCenter(new Point2D.Double(x, y)); + viewer.repaint(); + viewer.setCursor(Cursor.getPredefinedCursor(Cursor.MOVE_CURSOR)); + } + + @Override + public void mouseReleased(MouseEvent evt) + { + if (!SwingUtilities.isLeftMouseButton(evt)) + return; + + prev = null; + viewer.setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR)); + } + + @Override + public void mouseEntered(MouseEvent e) + { + SwingUtilities.invokeLater(new Runnable() + { + @Override + public void run() + { + viewer.requestFocusInWindow(); + } + }); + } +} diff --git a/src/org/jdesktop/swingx/input/ZoomMouseWheelListenerCenter.java b/src/org/jdesktop/swingx/input/ZoomMouseWheelListenerCenter.java new file mode 100644 index 00000000..ce639740 --- /dev/null +++ b/src/org/jdesktop/swingx/input/ZoomMouseWheelListenerCenter.java @@ -0,0 +1,46 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package org.jdesktop.swingx.input; + +import java.awt.event.MouseWheelEvent; +import java.awt.event.MouseWheelListener; + +import org.jdesktop.swingx.JXMapViewer; + +/** + * zooms using the mouse wheel on the view center + * @author joshy + */ +public class ZoomMouseWheelListenerCenter implements MouseWheelListener +{ + private JXMapViewer viewer; + + /** + * @param viewer the jxmapviewer + */ + public ZoomMouseWheelListenerCenter(JXMapViewer viewer) + { + this.viewer = viewer; + } + + @Override + public void mouseWheelMoved(MouseWheelEvent e) + { + viewer.setZoom(viewer.getZoom() + e.getWheelRotation()); + } +} diff --git a/src/org/jdesktop/swingx/input/ZoomMouseWheelListenerCursor.java b/src/org/jdesktop/swingx/input/ZoomMouseWheelListenerCursor.java new file mode 100644 index 00000000..dfcca236 --- /dev/null +++ b/src/org/jdesktop/swingx/input/ZoomMouseWheelListenerCursor.java @@ -0,0 +1,71 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package org.jdesktop.swingx.input; + +import java.awt.Dimension; +import java.awt.Point; +import java.awt.Rectangle; +import java.awt.event.MouseWheelEvent; +import java.awt.event.MouseWheelListener; +import java.awt.geom.Point2D; + +import org.jdesktop.swingx.JXMapViewer; + +/** + * zooms to the current mouse cursor + * using the mouse wheel + * @author Martin Steiger + */ +public class ZoomMouseWheelListenerCursor implements MouseWheelListener +{ + private JXMapViewer viewer; + + /** + * @param viewer the jxmapviewer + */ + public ZoomMouseWheelListenerCursor(JXMapViewer viewer) + { + this.viewer = viewer; + } + + @Override + public void mouseWheelMoved(MouseWheelEvent evt) + { + Point current = evt.getPoint(); + Rectangle bound = viewer.getViewportBounds(); + + double dx = current.x - bound.width / 2; + double dy = current.y - bound.height / 2; + + Dimension oldMapSize = viewer.getTileFactory().getMapSize(viewer.getZoom()); + + viewer.setZoom(viewer.getZoom() + evt.getWheelRotation()); + + Dimension mapSize = viewer.getTileFactory().getMapSize(viewer.getZoom()); + + Point2D center = viewer.getCenter(); + + double dzw = (mapSize.getWidth() / oldMapSize.getWidth()); + double dzh = (mapSize.getHeight() / oldMapSize.getHeight()); + + double x = center.getX() + dx * (dzw - 1); + double y = center.getY() + dy * (dzh - 1); + + viewer.setCenter(new Point2D.Double(x, y)); + } +} diff --git a/src/org/jdesktop/swingx/mapviewer/AbstractTileFactory.java b/src/org/jdesktop/swingx/mapviewer/AbstractTileFactory.java new file mode 100644 index 00000000..1c7646a9 --- /dev/null +++ b/src/org/jdesktop/swingx/mapviewer/AbstractTileFactory.java @@ -0,0 +1,401 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package org.jdesktop.swingx.mapviewer; + +import java.awt.image.BufferedImage; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.lang.ref.SoftReference; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URL; +import java.util.Comparator; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.PriorityBlockingQueue; +import java.util.concurrent.ThreadFactory; + +import javax.imageio.ImageIO; +import javax.swing.SwingUtilities; + +import log.Log; + +import org.jdesktop.swingx.mapviewer.util.GeoUtil; + +/** + * The AbstractTileFactory provides + * a basic implementation for the TileFactory. + */ +public abstract class AbstractTileFactory extends TileFactory +{ + /** + * Creates a new instance of DefaultTileFactory using the spcified TileFactoryInfo + * @param info a TileFactoryInfo to configure this TileFactory + */ + public AbstractTileFactory(TileFactoryInfo info) + { + super(info); + } + + // private static final boolean doEagerLoading = true; + + private int threadPoolSize = 4; + private ExecutorService service; + + // TODO the tile map should be static ALWAYS, regardless of the number + // of GoogleTileFactories because each tile is, really, a singleton. + private Map tileMap = new HashMap(); + + private TileCache cache = new TileCache(); + + /** + * Returns the tile that is located at the given tilePoint + * for this zoom. For example, if getMapSize() returns 10x20 + * for this zoom, and the tilePoint is (3,5), then the + * appropriate tile will be located and returned. + */ + @Override + public Tile getTile(int x, int y, int zoom) + { + return getTile(x, y, zoom, true); + } + + private Tile getTile(int tpx, int tpy, int zoom, boolean eagerLoad) + { + // wrap the tiles horizontally --> mod the X with the max width + // and use that + int tileX = tpx;// tilePoint.getX(); + int numTilesWide = (int) getMapSize(zoom).getWidth(); + if (tileX < 0) + { + tileX = numTilesWide - (Math.abs(tileX) % numTilesWide); + } + + tileX = tileX % numTilesWide; + int tileY = tpy; + // TilePoint tilePoint = new TilePoint(tileX, tpy); + String url = getInfo().getTileUrl(tileX, tileY, zoom);// tilePoint); + // System.out.println("loading: " + url); + + Tile.Priority pri = Tile.Priority.High; + if (!eagerLoad) + { + pri = Tile.Priority.Low; + } + Tile tile = null; + // System.out.println("testing for validity: " + tilePoint + " zoom = " + zoom); + if (!tileMap.containsKey(url)) + { + if (!GeoUtil.isValidTile(tileX, tileY, zoom, getInfo())) + { + tile = new Tile(tileX, tileY, zoom); + } + else + { + tile = new Tile(tileX, tileY, zoom, url, pri, this); + startLoading(tile); + } + tileMap.put(url, tile); + } + else + { + tile = tileMap.get(url); + // if its in the map but is low and isn't loaded yet + // but we are in high mode + if (tile.getPriority() == Tile.Priority.Low && eagerLoad && !tile.isLoaded()) + { + // System.out.println("in high mode and want a low"); + // tile.promote(); + promote(tile); + } + } + + /* + * if (eagerLoad && doEagerLoading) { for (int i = 0; i<1; i++) { for (int j = 0; j<1; j++) { // preload the 4 + * tiles under the current one if(zoom > 0) { eagerlyLoad(tilePoint.getX()*2, tilePoint.getY()*2, zoom-1); + * eagerlyLoad(tilePoint.getX()*2+1, tilePoint.getY()*2, zoom-1); eagerlyLoad(tilePoint.getX()*2, + * tilePoint.getY()*2+1, zoom-1); eagerlyLoad(tilePoint.getX()*2+1, tilePoint.getY()*2+1, zoom-1); } } } } + */ + + return tile; + } + + /* + * private void eagerlyLoad(int x, int y, int zoom) { TilePoint t1 = new TilePoint(x,y); if(!isLoaded(t1,zoom)) { + * getTile(t1,zoom,false); } } + */ + + // private boolean isLoaded(int x, int y, int zoom) { + // String url = getInfo().getTileUrl(zoom,x,y); + // return tileMap.containsKey(url); + // } + + /** + * @return the tile cache + */ + public TileCache getTileCache() + { + return cache; + } + + /** + * @param cache the tile cache + */ + public void setTileCache(TileCache cache) + { + this.cache = cache; + } + + /** ==== threaded tile loading stuff === */ + /** + * Thread pool for loading the tiles + */ + private static BlockingQueue tileQueue = new PriorityBlockingQueue(5, new Comparator() + { + @Override + public int compare(Tile o1, Tile o2) + { + if (o1.getPriority() == Tile.Priority.Low && o2.getPriority() == Tile.Priority.High) + { + return 1; + } + if (o1.getPriority() == Tile.Priority.High && o2.getPriority() == Tile.Priority.Low) + { + return -1; + } + return 0; + + } + }); + + /** + * Subclasses may override this method to provide their own executor services. This method will be called each time + * a tile needs to be loaded. Implementations should cache the ExecutorService when possible. + * @return ExecutorService to load tiles with + */ + protected synchronized ExecutorService getService() + { + if (service == null) + { + // System.out.println("creating an executor service with a threadpool of size " + threadPoolSize); + service = Executors.newFixedThreadPool(threadPoolSize, new ThreadFactory() + { + private int count = 0; + + @Override + public Thread newThread(Runnable r) + { + Thread t = new Thread(r, "tile-pool-" + count++); + t.setPriority(Thread.MIN_PRIORITY); + t.setDaemon(true); + return t; + } + }); + } + return service; + } + + @Override + public void dispose() + { + if (service != null) + { + service.shutdown(); + service = null; + } + } + + /** + * Set the number of threads to use for loading the tiles. This controls the number of threads used by the + * ExecutorService returned from getService(). Note, this method should be called before loading the first tile. + * Calls after the first tile are loaded will have no effect by default. + * @param size the thread pool size + */ + public void setThreadPoolSize(int size) + { + if (size <= 0) + { + throw new IllegalArgumentException("size invalid: " + size + + ". The size of the threadpool must be greater than 0."); + } + threadPoolSize = size; + } + + @Override + protected synchronized void startLoading(Tile tile) + { + if (tile.isLoading()) + { + System.out.println("already loading. bailing"); + return; + } + tile.setLoading(true); + try + { + tileQueue.put(tile); + getService().submit(createTileRunner(tile)); + } + catch (Exception ex) + { + ex.printStackTrace(); + } + } + + /** + * Subclasses can override this if they need custom TileRunners for some reason + * @param tile the tile (unused!) + * @return the tile runner + */ + protected Runnable createTileRunner(Tile tile) + { + return new TileRunner(); + } + + /** + * Increase the priority of this tile so it will be loaded sooner. + * @param tile the tile + */ + public synchronized void promote(Tile tile) + { + if (tileQueue.contains(tile)) + { + try + { + tileQueue.remove(tile); + tile.setPriority(Tile.Priority.High); + tileQueue.put(tile); + } + catch (Exception ex) + { + ex.printStackTrace(); + } + } + } + + /** + * An inner class which actually loads the tiles. Used by the thread queue. Subclasses can override this if + * necessary. + */ + private class TileRunner implements Runnable + { + /** + * Gets the full URI of a tile. + * @param tile the tile + * @throws URISyntaxException if the URI is invalid + * @return a URI for the tile + */ + protected URI getURI(Tile tile) throws URISyntaxException + { + if (tile.getURL() == null) + { + return null; + } + return new URI(tile.getURL()); + } + + /** + * implementation of the Runnable interface. + */ + @Override + public void run() + { + /* + * 3 strikes and you're out. Attempt to load the url. If it fails, decrement the number of tries left and + * try again. Log failures. If I run out of try s just get out. This way, if there is some kind of serious + * failure, I can get out and let other tiles try to load. + */ + final Tile tile = tileQueue.remove(); + + int trys = 3; + while (!tile.isLoaded() && trys > 0) + { + try + { + BufferedImage img = null; + URI uri = getURI(tile); + img = cache.get(uri); + if (img == null) + { + byte[] bimg = cacheInputStream(uri.toURL()); + // img = PaintUtils.loadCompatibleImage(new ByteArrayInputStream(bimg)); + img = ImageIO.read(new ByteArrayInputStream(bimg)); + cache.put(uri, bimg, img); + img = cache.get(uri); + } + if (img == null) + { + System.out.println("error loading: " + uri); + Log.info("Failed to load: " + uri); + trys--; + } + else + { + final BufferedImage i = img; + SwingUtilities.invokeAndWait(new Runnable() + { + @Override + public void run() + { + tile.image = new SoftReference(i); + tile.setLoaded(true); + fireTileLoadedEvent(tile); + } + }); + } + } + catch (OutOfMemoryError memErr) + { + cache.needMoreMemory(); + } + catch (Throwable e) + { + if (trys == 0) + { + Log.error("Failed to load a tile at url: " + tile.getURL() + ", stopping - " + e.getLocalizedMessage() ); + } + else + { + Log.warning("Failed to load a tile at url: " + tile.getURL() + ", retrying - " + e.getLocalizedMessage() ); + trys--; + } + } + } + tile.setLoading(false); + } + + private byte[] cacheInputStream(URL url) throws IOException + { + InputStream ins = url.openStream(); + ByteArrayOutputStream bout = new ByteArrayOutputStream(); + byte[] buf = new byte[256]; + while (true) + { + int n = ins.read(buf); + if (n == -1) + break; + bout.write(buf, 0, n); + } + return bout.toByteArray(); + } + } +} diff --git a/src/org/jdesktop/swingx/mapviewer/DefaultTileFactory.java b/src/org/jdesktop/swingx/mapviewer/DefaultTileFactory.java new file mode 100644 index 00000000..2d430f3a --- /dev/null +++ b/src/org/jdesktop/swingx/mapviewer/DefaultTileFactory.java @@ -0,0 +1,28 @@ +/* + * DefaultTileFactory.java + * + * Created on June 27, 2006, 2:20 PM + * + * To change this template, choose Tools | Template Manager + * and open the template in the editor. + */ + +package org.jdesktop.swingx.mapviewer; + +/** + * A tile factory which configures itself using a TileFactoryInfo object and uses a Google Maps like mercator + * projection. + * @author joshy + */ +public class DefaultTileFactory extends AbstractTileFactory +{ + /** + * Creates a new instance of DefaultTileFactory using the spcified TileFactoryInfo + * @param info a TileFactoryInfo to configure this TileFactory + */ + public DefaultTileFactory(TileFactoryInfo info) + { + super(info); + } + +} diff --git a/src/org/jdesktop/swingx/mapviewer/DefaultWaypoint.java b/src/org/jdesktop/swingx/mapviewer/DefaultWaypoint.java new file mode 100644 index 00000000..d37dd46e --- /dev/null +++ b/src/org/jdesktop/swingx/mapviewer/DefaultWaypoint.java @@ -0,0 +1,63 @@ +/* + * Waypoint.java + * + * Created on March 30, 2006, 5:22 PM + * + * To change this template, choose Tools | Template Manager + * and open the template in the editor. + */ + +package org.jdesktop.swingx.mapviewer; + +import org.jdesktop.beans.AbstractBean; + +/** + * @author joshy + */ +public class DefaultWaypoint extends AbstractBean implements Waypoint +{ + private GeoPosition position; + + /** + * Creates a new instance of Waypoint + */ + public DefaultWaypoint() + { + this(new GeoPosition(0, 0)); + } + + /** + * @param latitude the latitude + * @param longitude the longitude + */ + public DefaultWaypoint(double latitude, double longitude) + { + this(new GeoPosition(latitude, longitude)); + } + + /** + * @param coord the geo coordinate + */ + public DefaultWaypoint(GeoPosition coord) + { + this.position = coord; + } + + @Override + public GeoPosition getPosition() + { + return position; + } + + /** + * Set a new GeoPosition for this Waypoint + * @param coordinate a new position + */ + public void setPosition(GeoPosition coordinate) + { + GeoPosition old = getPosition(); + this.position = coordinate; + firePropertyChange("position", old, getPosition()); + } + +} diff --git a/src/org/jdesktop/swingx/mapviewer/DefaultWaypointRenderer.java b/src/org/jdesktop/swingx/mapviewer/DefaultWaypointRenderer.java new file mode 100644 index 00000000..a30b608c --- /dev/null +++ b/src/org/jdesktop/swingx/mapviewer/DefaultWaypointRenderer.java @@ -0,0 +1,58 @@ +/* + * WaypointRenderer.java + * + * Created on March 30, 2006, 5:24 PM + * + * To change this template, choose Tools | Template Manager + * and open the template in the editor. + */ + +package org.jdesktop.swingx.mapviewer; + +import java.awt.Graphics2D; +import java.awt.geom.Point2D; +import java.awt.image.BufferedImage; + +import javax.imageio.ImageIO; + +import log.Log; + +import org.jdesktop.swingx.JXMapViewer; + +/** + * This is a standard waypoint renderer. + * @author joshy + */ +public class DefaultWaypointRenderer implements WaypointRenderer +{ + private BufferedImage img = null; + + /** + * Uses a default waypoint image + */ + public DefaultWaypointRenderer() + { + try + { + img = ImageIO.read(getClass().getResource("resources/standard_waypoint.png")); + } + catch (Exception ex) + { + Log.warning("couldn't read standard_waypoint.png - " + ex.getLocalizedMessage() ); + } + } + + @Override + public void paintWaypoint(Graphics2D g, JXMapViewer map, Waypoint w) + { + if (img == null) + return; + + Point2D point = map.getTileFactory().geoToPixel(w.getPosition(), map.getZoom()); + + int x = (int)point.getX() -img.getWidth() / 2; + int y = (int)point.getY() -img.getHeight(); + + g.drawImage(img, x, y, null); + } +} diff --git a/src/org/jdesktop/swingx/mapviewer/GeoBounds.java b/src/org/jdesktop/swingx/mapviewer/GeoBounds.java new file mode 100644 index 00000000..f7a74eb9 --- /dev/null +++ b/src/org/jdesktop/swingx/mapviewer/GeoBounds.java @@ -0,0 +1,158 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package org.jdesktop.swingx.mapviewer; + +import java.awt.geom.Rectangle2D; +import java.util.Set; + +/** + * The GeoBounds class provides access the the North East and South West corners of the bounds and provides + * an intersects method. + * @author Dan Andrews + */ +public class GeoBounds +{ + + /** Internal representation of the bounds */ + private Rectangle2D[] rects; + + /** + * Constructor. + * @param minLat The minimum latitude. + * @param minLng The minimum longitude. + * @param maxLat The maximum latitude. + * @param maxLng The maximum longitude. + */ + public GeoBounds(double minLat, double minLng, double maxLat, double maxLng) + { + setRect(minLat, minLng, maxLat, maxLng); + } + + /** + * Constructor. + * @param geoPositions A non null list of 2 or more different GeoBounds objects. + */ + public GeoBounds(Set geoPositions) + { + if (geoPositions == null || geoPositions.size() < 2) + { + throw new IllegalArgumentException("The attribute 'geoPositions' cannot be null and must " + + "have 2 or more elements."); + } + double minLat = Integer.MAX_VALUE; + double minLng = Integer.MAX_VALUE; + double maxLat = Integer.MIN_VALUE; + double maxLng = Integer.MIN_VALUE; + for (GeoPosition position : geoPositions) + { + minLat = Math.min(minLat, position.getLatitude()); + minLng = Math.min(minLng, position.getLongitude()); + maxLat = Math.max(maxLat, position.getLatitude()); + maxLng = Math.max(maxLng, position.getLongitude()); + } + setRect(minLat, minLng, maxLat, maxLng); + } + + /** + * Sets the internal rectangle representation. + * @param minLat The minimum latitude. + * @param minLng The minimum longitude. + * @param maxLat The maximum latitude. + * @param maxLng The maximum longitude. + */ + private void setRect(double minLat, double minLng, double maxLat, double maxLng) + { + if (!(minLat < maxLat)) + { + throw new IllegalArgumentException("GeoBounds is not valid - minLat must be less that maxLat."); + } + if (!(minLng < maxLng)) + { + if (minLng > 0 && minLng < 180 && maxLng < 0) + { + rects = new Rectangle2D[] { + // split into two rects e.g. 176.8793 to 180 and -180 to + // -175.0104 + new Rectangle2D.Double(minLng, minLat, 180 - minLng, maxLat - minLat), + new Rectangle2D.Double(-180, minLat, maxLng + 180, maxLat - minLat) }; + } + else + { + rects = new Rectangle2D[] { new Rectangle2D.Double(minLng, minLat, maxLng - minLng, maxLat - minLat) }; + + throw new IllegalArgumentException("GeoBounds is not valid - minLng must be less that maxLng or " + + "minLng must be greater than 0 and maxLng must be less than 0."); + } + } + else + { + rects = new Rectangle2D[] { new Rectangle2D.Double(minLng, minLat, maxLng - minLng, maxLat - minLat) }; + } + } + + /** + * Determines if this bounds intersects the other bounds. + * @param other The other bounds to test for intersection with. + * @return Returns true if bounds intersect. + */ + public boolean intersects(GeoBounds other) + { + boolean rv = false; + for (Rectangle2D r1 : rects) + { + for (Rectangle2D r2 : other.rects) + { + rv = r1.intersects(r2); + if (rv) + { + break; + } + } + if (rv) + { + break; + } + + } + return rv; + } + + /** + * Gets the north west position. + * @return Returns the north west position. + */ + public GeoPosition getNorthWest() + { + return new GeoPosition(rects[0].getX(), rects[0].getMaxY()); + } + + /** + * Gets the south east position. + * @return Returns the south east position. + */ + public GeoPosition getSouthEast() + { + Rectangle2D r = rects[0]; + if (rects.length > 1) + { + r = rects[1]; + } + return new GeoPosition(r.getMaxX(), r.getY()); + } + +} diff --git a/src/org/jdesktop/swingx/mapviewer/GeoPosition.java b/src/org/jdesktop/swingx/mapviewer/GeoPosition.java new file mode 100644 index 00000000..de3b1db4 --- /dev/null +++ b/src/org/jdesktop/swingx/mapviewer/GeoPosition.java @@ -0,0 +1,113 @@ +/* + * GeoPosition.java + * + * Created on March 31, 2006, 9:15 PM + * + * To change this template, choose Tools | Template Manager + * and open the template in the editor. + */ + +package org.jdesktop.swingx.mapviewer; + +/** + * An immutable coordinate in the real (geographic) world, + * composed of a latitude and a longitude. + * @author rbair + */ +public class GeoPosition { + private double latitude; + private double longitude; + + /** + * Creates a new instance of GeoPosition from the specified + * latitude and longitude. These are double values in decimal degrees, not + * degrees, minutes, and seconds. Use the other constructor for those. + * @param latitude a latitude value in decmial degrees + * @param longitude a longitude value in decimal degrees + */ + public GeoPosition(double latitude, double longitude) { + this.latitude = latitude; + this.longitude = longitude; + } + // must be an array of length two containing lat then long in that order. + /** + * Creates a new instance of GeoPosition from the specified + * latitude and longitude as an array of two doubles, with the + * latitude first. These are double values in decimal degrees, not + * degrees, minutes, and seconds. Use the other constructor for those. + * @param coords latitude and longitude as a double array of length two + */ + public GeoPosition(double [] coords) { + this.latitude = coords[0]; + this.longitude = coords[1]; + } + + /** + * Creates a new instance of GeoPosition from the specified + * latitude and longitude. + * Each are specified as degrees, minutes, and seconds; not + * as decimal degrees. Use the other constructor for those. + * @param latDegrees the degrees part of the current latitude + * @param latMinutes the minutes part of the current latitude + * @param latSeconds the seconds part of the current latitude + * @param lonDegrees the degrees part of the current longitude + * @param lonMinutes the minutes part of the current longitude + * @param lonSeconds the seconds part of the current longitude + */ + public GeoPosition(int latDegrees, int latMinutes, int latSeconds, + int lonDegrees, int lonMinutes, int lonSeconds) { + this(latDegrees + (latMinutes + latSeconds/60.0)/60.0, + lonDegrees + (lonMinutes + lonSeconds/60.0)/60.0); + } + + /** + * Get the latitude as decimal degrees + * @return the latitude as decimal degrees + */ + public double getLatitude() { + return latitude; + } + + /** + * Get the longitude as decimal degrees + * @return the longitude as decimal degrees + */ + public double getLongitude() { + return longitude; + } + + @Override + public int hashCode() + { + final int prime = 31; + int result = 1; + long temp; + temp = Double.doubleToLongBits(latitude); + result = prime * result + (int) (temp ^ (temp >>> 32)); + temp = Double.doubleToLongBits(longitude); + result = prime * result + (int) (temp ^ (temp >>> 32)); + return result; + } + + @Override + public boolean equals(Object obj) + { + if (this == obj) + return true; + if (obj == null) + return false; + if (!(obj instanceof GeoPosition)) + return false; + GeoPosition other = (GeoPosition) obj; + if (Double.doubleToLongBits(latitude) != Double.doubleToLongBits(other.latitude)) + return false; + if (Double.doubleToLongBits(longitude) != Double.doubleToLongBits(other.longitude)) + return false; + return true; + } + + @Override + public String toString() { + return "[" + latitude + ", " + longitude + "]"; + } +} \ No newline at end of file diff --git a/src/org/jdesktop/swingx/mapviewer/LocalResponseCache.java b/src/org/jdesktop/swingx/mapviewer/LocalResponseCache.java new file mode 100644 index 00000000..f802168e --- /dev/null +++ b/src/org/jdesktop/swingx/mapviewer/LocalResponseCache.java @@ -0,0 +1,322 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package org.jdesktop.swingx.mapviewer; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.CacheRequest; +import java.net.CacheResponse; +import java.net.HttpURLConnection; +import java.net.MalformedURLException; +import java.net.ResponseCache; +import java.net.URI; +import java.net.URLConnection; +import java.util.List; +import java.util.Map; + +import log.Log; + +/** + * @author joshy + */ +public class LocalResponseCache extends ResponseCache +{ + private final File cacheDir; + + private boolean checkForUpdates; + + private String baseURL; + + /** + * Private constructor to prevent instantiation. + * @param baseURL the URI that should be cached or null (for all URLs) + * @param cacheDir the cache directory + * @param checkForUpdates true if the URL is queried for newer versions of a file first + */ + private LocalResponseCache(String baseURL, File cacheDir, boolean checkForUpdates) + { + this.baseURL = baseURL; + this.cacheDir = cacheDir; + this.checkForUpdates = checkForUpdates; + + if (!cacheDir.exists()) + { + cacheDir.mkdirs(); + } + } + + /** + * Sets this cache as default response cache + * @param baseURL the URL, the caching should be restricted to or null for none + * @param cacheDir the cache directory + * @param checkForUpdates true if the URL is queried for newer versions of a file first + */ + public static void installResponseCache(String baseURL, File cacheDir, boolean checkForUpdates) + { + ResponseCache.setDefault(new LocalResponseCache(baseURL, cacheDir, checkForUpdates)); + } + + /** + * Returns the local File corresponding to the given remote URI. + * @param remoteUri the remote URI + * @return the corresponding local file + */ + public File getLocalFile(URI remoteUri) + { + if (baseURL != null) + { + String remote = remoteUri.toString(); + + if (!remote.startsWith(baseURL)) + { + return null; + } + } + + + StringBuilder sb = new StringBuilder(); + + String host = remoteUri.getHost(); + String query = remoteUri.getQuery(); + String path = remoteUri.getPath(); + String fragment = remoteUri.getFragment(); + + if (host != null) + { + sb.append(host); + } + if (path != null) + { + sb.append(path); + } + if (query != null) + { + sb.append('?'); + sb.append(query); + } + if (fragment != null) + { + sb.append('#'); + sb.append(fragment); + } + + String name; + + final int maxLen = 250; + + if (sb.length() < maxLen) + { + name = sb.toString(); + } + else + { + name = sb.substring(0, maxLen); + } + + name = name.replace('?', '$'); + name = name.replace('*', '$'); + name = name.replace(':', '$'); + name = name.replace('<', '$'); + name = name.replace('>', '$'); + name = name.replace('"', '$'); + + File f = new File(cacheDir, name); + + return f; + } + + /** + * @param remoteUri the remote URI + * @param localFile the corresponding local file + * @return true if the resource at the given remote URI is newer than the resource cached locally. + */ + private static boolean isUpdateAvailable(URI remoteUri, File localFile) + { + URLConnection conn; + try + { + conn = remoteUri.toURL().openConnection(); + } + catch (MalformedURLException ex) + { + Log.error("An exception occurred - " + ex.getLocalizedMessage() ); + return false; + } + catch (IOException ex) + { + Log.error("An exception occurred - " + ex.getLocalizedMessage() ); + return false; + } + if (!(conn instanceof HttpURLConnection)) + { + // don't bother with non-http connections + return false; + } + + long localLastMod = localFile.lastModified(); + long remoteLastMod = 0L; + HttpURLConnection httpconn = (HttpURLConnection) conn; + // disable caching so we don't get in feedback loop with ResponseCache + httpconn.setUseCaches(false); + try + { + httpconn.connect(); + remoteLastMod = httpconn.getLastModified(); + } + catch (IOException ex) + { + // log.error("An exception occurred", ex);(); + return false; + } + finally + { + httpconn.disconnect(); + } + + return (remoteLastMod > localLastMod); + } + + @Override + public CacheResponse get(URI uri, String rqstMethod, Map> rqstHeaders) throws IOException + { + File localFile = getLocalFile(uri); + + if (localFile == null) + { + // we don't want to cache this URL + return null; + } + + if (!localFile.exists()) + { + // the file isn't already in our cache, return null + return null; + } + + if (checkForUpdates) + { + if (isUpdateAvailable(uri, localFile)) + { + // there is an update available, so don't return cached version + return null; + } + } + + return new LocalCacheResponse(localFile, rqstHeaders); + } + + @Override + public CacheRequest put(URI uri, URLConnection conn) throws IOException + { + // only cache http(s) GET requests + if (!(conn instanceof HttpURLConnection) || !(((HttpURLConnection) conn).getRequestMethod().equals("GET"))) + { + return null; + } + + File localFile = getLocalFile(uri); + + if (localFile == null) + { + // we don't want to cache this URL + return null; + } + + new File(localFile.getParent()).mkdirs(); + return new LocalCacheRequest(localFile); + } + + private class LocalCacheResponse extends CacheResponse + { + private FileInputStream fis; + private final Map> headers; + + private LocalCacheResponse(File localFile, Map> rqstHeaders) + { + try + { + this.fis = new FileInputStream(localFile); + } + catch (FileNotFoundException ex) + { + // should not happen, since we already checked for existence + Log.error("An exception occurred - " + ex.getLocalizedMessage() ); + } + this.headers = rqstHeaders; + } + + @Override + public Map> getHeaders() throws IOException + { + return headers; + } + + @Override + public InputStream getBody() throws IOException + { + return fis; + } + } + + private class LocalCacheRequest extends CacheRequest + { + private final File localFile; + private FileOutputStream fos; + + private LocalCacheRequest(File localFile) + { + this.localFile = localFile; + try + { + this.fos = new FileOutputStream(localFile); + } + catch (FileNotFoundException ex) + { + // should not happen if cache dir is valid + Log.error("An exception occurred" + ex.getLocalizedMessage() ); + } + } + + @Override + public OutputStream getBody() throws IOException + { + return fos; + } + + @Override + public void abort() + { + // abandon the cache attempt by closing the stream and deleting + // the local file + try + { + fos.close(); + localFile.delete(); + } + catch (IOException e) + { + // ignore + } + } + } +} diff --git a/src/org/jdesktop/swingx/mapviewer/Tile.java b/src/org/jdesktop/swingx/mapviewer/Tile.java new file mode 100644 index 00000000..1aa33f09 --- /dev/null +++ b/src/org/jdesktop/swingx/mapviewer/Tile.java @@ -0,0 +1,214 @@ +/* + * Tile.java + * + * Created on March 14, 2006, 4:53 PM + * + * To change this template, choose Tools | Template Manager + * and open the template in the editor. + */ + +package org.jdesktop.swingx.mapviewer; + +import java.awt.image.BufferedImage; +import java.lang.ref.SoftReference; + +import org.jdesktop.beans.AbstractBean; + +/** + * The Tile class represents a particular square image piece of the world bitmap at a particular zoom level. + * @author joshy + */ + +public class Tile extends AbstractBean +{ + /** + * The loading priority + */ + @SuppressWarnings("javadoc") + public enum Priority + { + High, Low + } + + private Priority priority = Priority.High; + + private TileFactory dtf; + + private boolean isLoading = false; + + // Most Recently Accessed Tiles. These are strong references, to prevent reloading + // of most recently used tiles. + // private static final Map recentlyAccessed = new HashMap(); + // private static final TileCache cache = new TileCache(); + + /** + * The url of the image to load for this tile + */ + private String url; + + /** + * Indicates that loading has succeeded. A PropertyChangeEvent will be fired when the loading is completed + */ + + private boolean loaded = false; + /** + * The zoom level this tile is for + */ + private int zoom, x, y; + + /** + * The image loaded for this Tile + */ + SoftReference image = new SoftReference(null); + + /** + * Create a new Tile at the specified tile point and zoom level + * @param x the x value + * @param y the y value + * @param zoom the zoom level + */ + public Tile(int x, int y, int zoom) + { + loaded = false; + this.zoom = zoom; + this.x = x; + this.y = y; + } + + /** + * Create a new Tile that loads its data from the given URL. The URL must resolve to an image + * @param x the x value + * @param y the y value + * @param zoom the zoom level + * @param url the URL + * @param priority the priority + * @param dtf the tile factory + */ + Tile(int x, int y, int zoom, String url, Priority priority, TileFactory dtf) + { + this.url = url; + loaded = false; + this.zoom = zoom; + this.x = x; + this.y = y; + this.priority = priority; + this.dtf = dtf; + // startLoading(); + } + + /** + * Indicates if this tile's underlying image has been successfully loaded yet. + * @return true if the Tile has been loaded + */ + public synchronized boolean isLoaded() + { + return loaded; + } + + /** + * Toggles the loaded state, and fires the appropriate property change notification + * @param loaded the loaded flag + */ + synchronized void setLoaded(boolean loaded) + { + boolean old = isLoaded(); + this.loaded = loaded; + firePropertyChange("loaded", old, isLoaded()); + } + + /** + * @return the Image associated with this Tile. This is a read only property This may return null at any time, + * however if this returns null, a load operation will automatically be started for it. + */ + public BufferedImage getImage() + { + BufferedImage img = image.get(); + if (img == null) + { + setLoaded(false); + + // tile factory can be null if the tile has invalid coords or zoom + if (dtf != null) + { + dtf.startLoading(this); + } + } + + return img; + } + + /** + * @return the location in the world at this zoom level that this tile should be placed + */ + /* + * public TilePoint getLocation() { return location; } + */ + + /** + * @return the zoom level that this tile belongs in + */ + public int getZoom() + { + return zoom; + } + + /** + * @return the isLoading + */ + public boolean isLoading() + { + return isLoading; + } + + /** + * @param isLoading the isLoading to set + */ + public void setLoading(boolean isLoading) + { + this.isLoading = isLoading; + } + + /** + * Gets the loading priority of this tile. + * @return the priority + */ + public Priority getPriority() + { + return priority; + } + + /** + * Set the loading priority of this tile. + * @param priority the priority to set + */ + public void setPriority(Priority priority) + { + this.priority = priority; + } + + /** + * Gets the URL of this tile. + * @return the url + */ + public String getURL() + { + return url; + } + + /** + * @return the x value + */ + public int getX() + { + return x; + } + + /** + * @return the y value + */ + public int getY() + { + return y; + } + +} diff --git a/src/org/jdesktop/swingx/mapviewer/TileCache.java b/src/org/jdesktop/swingx/mapviewer/TileCache.java new file mode 100644 index 00000000..592f39af --- /dev/null +++ b/src/org/jdesktop/swingx/mapviewer/TileCache.java @@ -0,0 +1,141 @@ +/* + * TileCache.java + * + * Created on January 2, 2007, 7:17 PM + * + * To change this template, choose Tools | Template Manager + * and open the template in the editor. + */ + +package org.jdesktop.swingx.mapviewer; + +import java.awt.image.BufferedImage; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.net.URI; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.Map; + +import javax.imageio.ImageIO; + +/** + * An implementation only class for now. For internal use only. + * @author joshua.marinacci@sun.com + */ +public class TileCache +{ + private Map imgmap = new HashMap(); + private LinkedList imgmapAccessQueue = new LinkedList(); + private int imagesize = 0; + private Map bytemap = new HashMap(); + private LinkedList bytemapAccessQueue = new LinkedList(); + private int bytesize = 0; + + /** + * Default constructor - Empty + */ + public TileCache() + { + } + + /** + * Put a tile image into the cache. This puts both a buffered image and array of bytes that make up the compressed + * image. + * @param uri URI of image that is being stored in the cache + * @param bimg bytes of the compressed image, ie: the image file that was loaded over the network + * @param img image to store in the cache + */ + public void put(URI uri, byte[] bimg, BufferedImage img) + { + synchronized (bytemap) + { + while (bytesize > 1000 * 1000 * 50) + { + URI olduri = bytemapAccessQueue.removeFirst(); + byte[] oldbimg = bytemap.remove(olduri); + bytesize -= oldbimg.length; + log("removed 1 img from byte cache"); + } + + bytemap.put(uri, bimg); + bytesize += bimg.length; + bytemapAccessQueue.addLast(uri); + } + addToImageCache(uri, img); + } + + /** + * Returns a buffered image for the requested URI from the cache. This method must return null if the image is not + * in the cache. If the image is unavailable but it's compressed version *is* available, then the compressed version + * will be expanded and returned. + * @param uri URI of the image previously put in the cache + * @return the image matching the requested URI, or null if not available + * @throws IOException if retrieval fails + */ + public BufferedImage get(URI uri) throws IOException + { + synchronized (imgmap) + { + if (imgmap.containsKey(uri)) + { + imgmapAccessQueue.remove(uri); + imgmapAccessQueue.addLast(uri); + return imgmap.get(uri); + } + } + synchronized (bytemap) + { + if (bytemap.containsKey(uri)) + { + log("retrieving from bytes"); + bytemapAccessQueue.remove(uri); + bytemapAccessQueue.addLast(uri); + BufferedImage img = ImageIO.read(new ByteArrayInputStream(bytemap.get(uri))); + addToImageCache(uri, img); + return img; + } + } + return null; + } + + /** + * Request that the cache free up some memory. How this happens or how much memory is freed is up to the TileCache + * implementation. Subclasses can implement their own strategy. The default strategy is to clear out all buffered + * images but retain the compressed versions. + */ + public void needMoreMemory() + { + imgmap.clear(); + log("HACK! need more memory: freeing up memory"); + } + + private void addToImageCache(final URI uri, final BufferedImage img) + { + synchronized (imgmap) + { + try { + while (imagesize > 1000 * 1000 * 50) + { + URI olduri = imgmapAccessQueue.removeFirst(); + BufferedImage oldimg = imgmap.remove(olduri); + imagesize -= oldimg.getWidth() * oldimg.getHeight() * 4; + log("removed 1 img from image cache"); + } + imgmap.put(uri, img); + imagesize += img.getWidth() * img.getHeight() * 4; + imgmapAccessQueue.addLast(uri); + } catch (Exception ex) { + log("Failed to load tile at URL. Tile is null"); + } + } +// log("added to cache: " + " uncompressed = " + imgmap.keySet().size() + " / " + imagesize / 1000 + "k" +// + " compressed = " + bytemap.keySet().size() + " / " + bytesize / 1000 + "k"); + } + + private void log(String string) + { +// Log.debug( string ); + } + +} diff --git a/src/org/jdesktop/swingx/mapviewer/TileFactory.java b/src/org/jdesktop/swingx/mapviewer/TileFactory.java new file mode 100644 index 00000000..c1eba196 --- /dev/null +++ b/src/org/jdesktop/swingx/mapviewer/TileFactory.java @@ -0,0 +1,144 @@ +/* + * TileFactory.java + * + * Created on March 17, 2006, 8:01 PM + * + * To change this template, choose Tools | Template Manager + * and open the template in the editor. + */ + +package org.jdesktop.swingx.mapviewer; + +import java.awt.Dimension; +import java.awt.geom.Point2D; +import java.util.ArrayList; +import java.util.List; + +import org.jdesktop.swingx.mapviewer.util.GeoUtil; + +/** + * A class that can produce tiles and convert coordinates to pixels + * @author joshy + */ +public abstract class TileFactory +{ + private TileFactoryInfo info; + private List tileListeners = new ArrayList(); + + /** + * Creates a new instance of TileFactory + * @param info a TileFactoryInfo to configure this TileFactory + */ + protected TileFactory(TileFactoryInfo info) + { + this.info = info; + } + + /** + * Gets the size of an edge of a tile in pixels at the current zoom level. Tiles must be square. + * @param zoom the current zoom level + * @return the size of an edge of a tile in pixels + */ + public int getTileSize(int zoom) + { + return getInfo().getTileSize(zoom); + } + + /** + * Returns a Dimension containing the width and height of the map, in tiles at the current zoom level. So a + * Dimension that returns 10x20 would be 10 tiles wide and 20 tiles tall. These values can be multipled by + * getTileSize() to determine the pixel width/height for the map at the given zoom level + * @return the size of the world bitmap in tiles + * @param zoom the current zoom level + */ + public Dimension getMapSize(int zoom) + { + return GeoUtil.getMapSize(zoom, getInfo()); + } + + /** + * Return the Tile at a given TilePoint and zoom level + * @return the tile that is located at the given tilePoint for this zoom level. For example, if getMapSize() returns + * 10x20 for this zoom, and the tilePoint is (3,5), then the appropriate tile will be located and returned. This + * method must not return null. However, it can return dummy tiles that contain no data if it wants. This is + * appropriate, for example, for tiles which are outside of the bounds of the map and if the factory doesn't + * implement wrapping. + * @param x the x value + * @param y the y value + * @param zoom the current zoom level + */ + public abstract Tile getTile(int x, int y, int zoom); + + /** + * Convert a pixel in the world bitmap at the specified zoom level into a GeoPosition + * @param pixelCoordinate a Point2D representing a pixel in the world bitmap + * @param zoom the zoom level of the world bitmap + * @return the converted GeoPosition + */ + public GeoPosition pixelToGeo(Point2D pixelCoordinate, int zoom) + { + return GeoUtil.getPosition(pixelCoordinate, zoom, getInfo()); + } + + /** + * Convert a GeoPosition to a pixel position in the world bitmap a the specified zoom level. + * @param c a GeoPosition + * @param zoomLevel the zoom level to extract the pixel coordinate for + * @return the pixel point + */ + public Point2D geoToPixel(GeoPosition c, int zoomLevel) + { + return GeoUtil.getBitmapCoordinate(c, zoomLevel, getInfo()); + } + + /** + * Get the TileFactoryInfo describing this TileFactory + * @return a TileFactoryInfo + */ + public TileFactoryInfo getInfo() + { + return info; + } + + /** + * Adds a tile listener + * @param listener the listener + */ + public void addTileListener(TileListener listener) + { + tileListeners.add(listener); + } + + /** + * Removes a tile listener + * @param listener the listener + */ + public void removeTileListener(TileListener listener) + { + tileListeners.remove(listener); + } + + /** + * Disposes this TileFactory + */ + public abstract void dispose(); + + /** + * Notifies all tile listeners + * @param tile the tile + */ + protected void fireTileLoadedEvent(Tile tile) + { + for (TileListener listener : tileListeners) + { + listener.tileLoaded(tile); + } + } + + /** + * Override this method to load the tile using, for example, an ExecutorService. + * @param tile The tile to load. + */ + protected abstract void startLoading(Tile tile); + +} diff --git a/src/org/jdesktop/swingx/mapviewer/TileFactoryInfo.java b/src/org/jdesktop/swingx/mapviewer/TileFactoryInfo.java new file mode 100644 index 00000000..3fb97564 --- /dev/null +++ b/src/org/jdesktop/swingx/mapviewer/TileFactoryInfo.java @@ -0,0 +1,325 @@ +/* + * TileFactoryInfo.java + * + * Created on June 26, 2006, 10:47 AM + * + * To change this template, choose Tools | Template Manager + * and open the template in the editor. + */ + +package org.jdesktop.swingx.mapviewer; + +import java.awt.geom.Point2D; + +/** + * A TileFactoryInfo encapsulates all information specific to a map server. This includes everything from the url to + * load the map tiles from to the size and depth of the tiles. Theoretically any map server can be used by installing a + * customized TileFactoryInfo. Currently + * @author joshy + */ +public class TileFactoryInfo +{ + private int minimumZoomLevel; + private int maximumZoomLevel; + private int totalMapZoom; + // the size of each tile (assumes they are square) + private int tileSize = 256; + + /* + * The number of tiles wide at each zoom level + */ + private int[] mapWidthInTilesAtZoom; + /** + * An array of coordinates in pixels that indicates the center in the world map for the given zoom level. + */ + private Point2D[] mapCenterInPixelsAtZoom;// = new Point2D.Double[18]; + + /** + * An array of doubles that contain the number of pixels per degree of longitude at a give zoom level. + */ + private double[] longitudeDegreeWidthInPixels; + + /** + * An array of doubles that contain the number of radians per degree of longitude at a given zoom level (where + * longitudeRadianWidthInPixels[0] is the most zoomed out) + */ + private double[] longitudeRadianWidthInPixels; + + /** + * The base url for loading tiles from. + */ + protected String baseURL; + private String xparam; + private String yparam; + private String zparam; + private boolean xr2l = true; + private boolean yt2b = true; + + private int defaultZoomLevel; + + /** A name for this info. */ + private String name; + + /** + * Creates a new instance of TileFactoryInfo. Note that TileFactoryInfo should be considered invariate, meaning that + * subclasses should ensure all of the properties stay the same after the class is constructed. Returning different + * values of getTileSize() for example is considered an error and may result in unexpected behavior. + * @param minimumZoomLevel The minimum zoom level + * @param maximumZoomLevel the maximum zoom level + * @param totalMapZoom the top zoom level, essentially the height of the pyramid + * @param tileSize the size of the tiles in pixels (must be square) + * @param xr2l if the x goes r to l (is this backwards?) + * @param yt2b if the y goes top to bottom + * @param baseURL the base url for grabbing tiles + * @param xparam the x parameter for the tile url + * @param yparam the y parameter for the tile url + * @param zparam the z parameter for the tile url + */ + /* + * @param xr2l true if tile x is measured from the far left of the map to the far right, or else false if based on + * the center line. + * @param yt2b true if tile y is measured from the top (north pole) to the bottom (south pole) or else false if + * based on the equator. + */ + public TileFactoryInfo(int minimumZoomLevel, int maximumZoomLevel, int totalMapZoom, int tileSize, boolean xr2l, + boolean yt2b, String baseURL, String xparam, String yparam, String zparam) + { + this("name not provided", minimumZoomLevel, maximumZoomLevel, totalMapZoom, tileSize, xr2l, yt2b, baseURL, + xparam, yparam, zparam); + } + + /** + * Creates a new instance of TileFactoryInfo. Note that TileFactoryInfo should be considered invariate, meaning that + * subclasses should ensure all of the properties stay the same after the class is constructed. Returning different + * values of getTileSize() for example is considered an error and may result in unexpected behavior. + * @param name A name to identify this information. + * @param minimumZoomLevel The minimum zoom level + * @param maximumZoomLevel the maximum zoom level + * @param totalMapZoom the top zoom level, essentially the height of the pyramid + * @param tileSize the size of the tiles in pixels (must be square) + * @param xr2l if the x goes r to l (is this backwards?) + * @param yt2b if the y goes top to bottom + * @param baseURL the base url for grabbing tiles + * @param xparam the x parameter for the tile url + * @param yparam the y parameter for the tile url + * @param zparam the z parameter for the tile url + */ + /* + * @param xr2l true if tile x is measured from the far left of the map to the far right, or else false if based on + * the center line. + * @param yt2b true if tile y is measured from the top (north pole) to the bottom (south pole) or else false if + * based on the equator. + */ + public TileFactoryInfo(String name, int minimumZoomLevel, int maximumZoomLevel, int totalMapZoom, int tileSize, + boolean xr2l, boolean yt2b, String baseURL, String xparam, String yparam, String zparam) + { + this.name = name; + this.minimumZoomLevel = minimumZoomLevel; + this.maximumZoomLevel = maximumZoomLevel; + this.totalMapZoom = totalMapZoom; + this.baseURL = baseURL; + this.xparam = xparam; + this.yparam = yparam; + this.zparam = zparam; + this.setXr2l(xr2l); + this.setYt2b(yt2b); + + this.tileSize = tileSize; + + // init the num tiles wide + int tilesize = this.getTileSize(0); + + longitudeDegreeWidthInPixels = new double[totalMapZoom + 1]; + longitudeRadianWidthInPixels = new double[totalMapZoom + 1]; + mapCenterInPixelsAtZoom = new Point2D.Double[totalMapZoom + 1]; + mapWidthInTilesAtZoom = new int[totalMapZoom + 1]; + + // for each zoom level + for (int z = totalMapZoom; z >= 0; --z) + { + // how wide is each degree of longitude in pixels + longitudeDegreeWidthInPixels[z] = tilesize / 360.0; + // how wide is each radian of longitude in pixels + longitudeRadianWidthInPixels[z] = tilesize / (2.0 * Math.PI); + int t2 = tilesize / 2; + mapCenterInPixelsAtZoom[z] = new Point2D.Double(t2, t2); + mapWidthInTilesAtZoom[z] = tilesize / this.getTileSize(0); + tilesize *= 2; + } + + } + + /** + * @return the minimum zoom level + */ + public int getMinimumZoomLevel() + { + return minimumZoomLevel; + } + + // public void setMinimumZoomLevel(int minimumZoomLevel) { + // this.minimumZoomLevel = minimumZoomLevel; + // } + + /** + * @return the maximum zoom level + */ + public int getMaximumZoomLevel() + { + return maximumZoomLevel; + } + + /** + * @return the total map zoom level + */ + public int getTotalMapZoom() + { + return totalMapZoom; + } + + /** + * @param zoom the zoom level + * @return the map width in tiles + */ + public int getMapWidthInTilesAtZoom(int zoom) + { + return mapWidthInTilesAtZoom[zoom]; + } + + /** + * @param zoom the zoom level + * @return the map center in pixels + */ + public Point2D getMapCenterInPixelsAtZoom(int zoom) + { + return mapCenterInPixelsAtZoom[zoom]; + } + + /** + * Returns the tile url for the specified tile at the specified zoom level. By default it will generate a tile url + * using the base url and parameters specified in the constructor. Thus if
baseURl =
+	 * http://www.myserver.com/maps?version=0.1 xparam = x yparam = y zparam = z tilepoint = [1,2] zoom level = 3
+	 *  
then the resulting url would be: + *
http://www.myserver.com/maps?version=0.1&x=1&y=2&z=3
Note that the URL can be + * a file: url. + * @param zoom the zoom level + * @param x the x value + * @param y the y value + * @return a valid url to load the tile + */ + + public String getTileUrl(int x, int y, int zoom) + { + // System.out.println("getting tile at zoom: " + zoom); + // System.out.println("map width at zoom = " + getMapWidthInTilesAtZoom(zoom)); + String ypart = "&" + yparam + "=" + y; + // System.out.println("ypart = " + ypart); + + if (!yt2b) + { + int tilemax = getMapWidthInTilesAtZoom(zoom); + // int y = tilePoint.getY(); + ypart = "&" + yparam + "=" + (tilemax / 2 - y - 1); + } + // System.out.println("new ypart = " + ypart); + String url = baseURL + "&" + xparam + "=" + x + ypart + + // "&" + yparam + "=" + tilePoint.getY() + + "&" + zparam + "=" + zoom; + return url; + } + + /** + * Get the tile size. + * @param zoom the zoom level + * @return the tile size + */ + public int getTileSize(int zoom) + { + return tileSize; + } + + /** + * @param zoom the zoom level + * @return the longitude degree width in pixels + */ + public double getLongitudeDegreeWidthInPixels(int zoom) + { + return longitudeDegreeWidthInPixels[zoom]; + } + + /** + * @param zoom the zoom level + * @return the longitude radian width in pixels + */ + public double getLongitudeRadianWidthInPixels(int zoom) + { + return longitudeRadianWidthInPixels[zoom]; + } + + /** + * @return indicates if the X coordinates of tiles go from right to left or left to right. + */ + public boolean isXr2l() + { + return xr2l; + } + + /** + * @param xr2l indicates if the X coordinates of tiles go from right to left or left to right. + */ + public void setXr2l(boolean xr2l) + { + this.xr2l = xr2l; + } + + /** + * @return indicates if the Y coordinates of tiles go from right to left or left to right. + */ + public boolean isYt2b() + { + return yt2b; + } + + /** + * @param yt2b indicates if the Y coordinates of tiles go from right to left or left to right. + */ + public void setYt2b(boolean yt2b) + { + this.yt2b = yt2b; + } + + /** + * @return the default zoom level + */ + public int getDefaultZoomLevel() + { + return defaultZoomLevel; + } + + /** + * @param defaultZoomLevel the default zoom level + */ + public void setDefaultZoomLevel(int defaultZoomLevel) + { + this.defaultZoomLevel = defaultZoomLevel; + } + + /** + * The name of this info. + * @return Returns the name of this info class for debugging or GUI widgets. + */ + public String getName() + { + return name; + } + + /** + * @return the base URL + */ + public String getBaseURL() + { + return baseURL; + } + + +} diff --git a/src/org/jdesktop/swingx/mapviewer/TileListener.java b/src/org/jdesktop/swingx/mapviewer/TileListener.java new file mode 100644 index 00000000..55d1d13b --- /dev/null +++ b/src/org/jdesktop/swingx/mapviewer/TileListener.java @@ -0,0 +1,32 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package org.jdesktop.swingx.mapviewer; + +/** + * Notified when the status of a tile has changed + * @author Martin Steiger + */ +public interface TileListener +{ + /** + * Notification when a tile is loaded + * @param tile the tile + */ + public void tileLoaded(Tile tile); + +} diff --git a/src/org/jdesktop/swingx/mapviewer/Waypoint.java b/src/org/jdesktop/swingx/mapviewer/Waypoint.java new file mode 100644 index 00000000..9666dc2a --- /dev/null +++ b/src/org/jdesktop/swingx/mapviewer/Waypoint.java @@ -0,0 +1,25 @@ +/* + * Waypoint.java + * + * Created on March 30, 2006, 5:22 PM + * + * To change this template, choose Tools | Template Manager + * and open the template in the editor. + */ + +package org.jdesktop.swingx.mapviewer; + + +/** + * A Waypoint is a GeoPosition that can be + * drawn on a may using a WaypointPainter. + * @author joshy + */ +public interface Waypoint +{ + /** + * Get the current GeoPosition of this Waypoint + * @return the current position + */ + public GeoPosition getPosition(); +} diff --git a/src/org/jdesktop/swingx/mapviewer/WaypointPainter.java b/src/org/jdesktop/swingx/mapviewer/WaypointPainter.java new file mode 100644 index 00000000..cd4fe88a --- /dev/null +++ b/src/org/jdesktop/swingx/mapviewer/WaypointPainter.java @@ -0,0 +1,90 @@ +/* + * WaypointMapOverlay.java + * + * Created on April 1, 2006, 4:59 PM + * + * To change this template, choose Tools | Template Manager + * and open the template in the editor. + */ + +package org.jdesktop.swingx.mapviewer; + +import java.awt.Graphics2D; +import java.awt.Rectangle; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; + +import org.jdesktop.swingx.JXMapViewer; +import org.jdesktop.swingx.painter.AbstractPainter; + +/** + * Paints waypoints on the JXMapViewer. This is an + * instance of Painter that only can draw on to JXMapViewers. + * @param the waypoint type + * @author rbair + */ +public class WaypointPainter extends AbstractPainter +{ + private WaypointRenderer renderer = new DefaultWaypointRenderer(); + private Set waypoints = new HashSet(); + + /** + * Creates a new instance of WaypointPainter + */ + public WaypointPainter() + { + setAntialiasing(true); + setCacheable(false); + } + + /** + * Sets the waypoint renderer to use when painting waypoints + * @param r the new WaypointRenderer to use + */ + public void setRenderer(WaypointRenderer r) + { + this.renderer = r; + } + + /** + * Gets the current set of waypoints to paint + * @return a typed Set of Waypoints + */ + public Set getWaypoints() + { + return Collections.unmodifiableSet(waypoints); + } + + /** + * Sets the current set of waypoints to paint + * @param waypoints the new Set of Waypoints to use + */ + public void setWaypoints(Set waypoints) + { + this.waypoints.clear(); + this.waypoints.addAll(waypoints); + } + + @Override + protected void doPaint(Graphics2D g, JXMapViewer map, int width, int height) + { + if (renderer == null) + { + return; + } + + Rectangle viewportBounds = map.getViewportBounds(); + + g.translate(-viewportBounds.getX(), -viewportBounds.getY()); + + for (W w : getWaypoints()) + { + renderer.paintWaypoint(g, map, w); + } + + g.translate(viewportBounds.getX(), viewportBounds.getY()); + + } + +} diff --git a/src/org/jdesktop/swingx/mapviewer/WaypointRenderer.java b/src/org/jdesktop/swingx/mapviewer/WaypointRenderer.java new file mode 100644 index 00000000..c20f7a00 --- /dev/null +++ b/src/org/jdesktop/swingx/mapviewer/WaypointRenderer.java @@ -0,0 +1,32 @@ +/* + * WaypointRenderer.java + * + * Created on March 30, 2006, 5:56 PM + * + * To change this template, choose Tools | Template Manager + * and open the template in the editor. + */ + +package org.jdesktop.swingx.mapviewer; + +import java.awt.Graphics2D; + +import org.jdesktop.swingx.JXMapViewer; + +/** + * A interface that draws waypoints. Implementations of WaypointRenderer can + * be set on a WayPointPainter to draw waypoints on a JXMapViewer + * @param the waypoint type + * @author joshua.marinacci@sun.com + */ +public interface WaypointRenderer +{ + /** + * paint the specified waypoint on the specified map and graphics context + * @param g the graphics2D object + * @param map the map + * @param waypoint the waypoint + */ + public void paintWaypoint(Graphics2D g, JXMapViewer map, W waypoint); + +} diff --git a/src/org/jdesktop/swingx/mapviewer/bmng/CylindricalProjectionTileFactory.java b/src/org/jdesktop/swingx/mapviewer/bmng/CylindricalProjectionTileFactory.java new file mode 100644 index 00000000..b4864b6d --- /dev/null +++ b/src/org/jdesktop/swingx/mapviewer/bmng/CylindricalProjectionTileFactory.java @@ -0,0 +1,101 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package org.jdesktop.swingx.mapviewer.bmng; + +import java.awt.Dimension; +import java.awt.geom.Point2D; + +import org.jdesktop.swingx.mapviewer.DefaultTileFactory; +import org.jdesktop.swingx.mapviewer.GeoPosition; + +/** + * @author joshy + */ +public class CylindricalProjectionTileFactory extends DefaultTileFactory +{ + /** + * Uses {@link SLMapServerInfo} + */ + public CylindricalProjectionTileFactory() + { + this(new SLMapServerInfo()); + } + + /** + * @param info the tile factory info + */ + public CylindricalProjectionTileFactory(SLMapServerInfo info) + { + super(info); + } + + @Override + public Dimension getMapSize(int zoom) + { + int midpoint = ((SLMapServerInfo) getInfo()).getMidpoint(); + if (zoom < midpoint) + { + int w = (int) Math.pow(2, midpoint - zoom); + return new Dimension(w, w / 2); + // return super.getMapSize(zoom); + } + return new Dimension(2, 1); + } + + @Override + public Point2D geoToPixel(GeoPosition c, int zoom) + { + // calc the pixels per degree + Dimension mapSizeInTiles = getMapSize(zoom); + // double size_in_tiles = (double)getInfo().getMapWidthInTilesAtZoom(zoom); + // double size_in_tiles = Math.pow(2, getInfo().getTotalMapZoom() - zoom); + double size_in_pixels = mapSizeInTiles.getWidth() * getInfo().getTileSize(zoom); + double ppd = size_in_pixels / 360; + + // the center of the world + double centerX = this.getTileSize(zoom) * mapSizeInTiles.getWidth() / 2; + double centerY = this.getTileSize(zoom) * mapSizeInTiles.getHeight() / 2; + + double x = c.getLongitude() * ppd + centerX; + double y = -c.getLatitude() * ppd + centerY; + + return new Point2D.Double(x, y); + } + + @Override + public GeoPosition pixelToGeo(Point2D pix, int zoom) + { + // calc the pixels per degree + Dimension mapSizeInTiles = getMapSize(zoom); + double size_in_pixels = mapSizeInTiles.getWidth() * getInfo().getTileSize(zoom); + double ppd = size_in_pixels / 360; + + // the center of the world + double centerX = this.getTileSize(zoom) * mapSizeInTiles.getWidth() / 2; + double centerY = this.getTileSize(zoom) * mapSizeInTiles.getHeight() / 2; + + double lon = (pix.getX() - centerX) / ppd; + double lat = -(pix.getY() - centerY) / ppd; + + return new GeoPosition(lat, lon); + } + + /* + * x = lat * ppd + fact x - fact = lat * ppd (x - fact)/ppd = lat y = -lat*ppd + fact -(y-fact)/ppd = lat + */ +} diff --git a/src/org/jdesktop/swingx/mapviewer/bmng/SLMapServerInfo.java b/src/org/jdesktop/swingx/mapviewer/bmng/SLMapServerInfo.java new file mode 100644 index 00000000..6b3ed54a --- /dev/null +++ b/src/org/jdesktop/swingx/mapviewer/bmng/SLMapServerInfo.java @@ -0,0 +1,129 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package org.jdesktop.swingx.mapviewer.bmng; + +import org.jdesktop.swingx.mapviewer.TileFactoryInfo; + +/** + * A TileFactoryInfo subclass which knows how to connect to the SwingLabs map server. This server contains 2k resolution + * Blue Marble data from NASA. + */ +public class SLMapServerInfo extends TileFactoryInfo +{ + private static final int pyramid_top = 8 + 1; + private static final int midpoint = 5; + private static final int normal_tile_size = 675; + + /** + * Uses the default base URL + */ + public SLMapServerInfo() + { + this("http://maps.joshy.net/tiles/bmng_tiles_3"); + } + + /** + * @param baseURL the base URL + */ + public SLMapServerInfo(String baseURL) + { + // joshy: this was version one of the tiles + // super(0, 5, 5, 256, true, false, "http://maps.joshy.net/bmng_tiles_1", "", "", ""); + // super(0, pyramid_top-1, pyramid_top, + // 675, + // true, false, "http://maps.joshy.net/bmng_tiles_2", "", "", ""); + // super(0, pyramid_top-1, pyramid_top, + // normal_tile_size, + // true, false, "file:/Users/joshy/projects/java.net/ImageTileCutter/tiles", "", "", ""); + super(0, pyramid_top - 1, pyramid_top, normal_tile_size, true, false, baseURL, "", "", ""); + setDefaultZoomLevel(0); + } + + /** + * @return the midpoint + */ + public int getMidpoint() + { + return midpoint; + } + + @Override + public int getTileSize(int zoom) + { + int size = super.getTileSize(zoom); + if (zoom < midpoint) + { + return size; + } + else + { + for (int i = 0; i < zoom + 1 - midpoint; i++) + { + size = size / 2; + } + return size; + } + } + + @Override + public int getMapWidthInTilesAtZoom(int zoom) + { + if (zoom < midpoint) + { + return (int) Math.pow(2, midpoint - zoom); + } + else + { + return 1; + } + } + + @Override + public String getTileUrl(int x, int y, int zoom) + { + int ty = y; + int tx = x; + + // int width_in_tiles = (int)Math.pow(2,pyramid_top-zoom); + int width_in_tiles = getMapWidthInTilesAtZoom(zoom); + // System.out.println("width in tiles = " + width_in_tiles + " x = " + tx + " y = " + ty); + if (ty < 0) + { + return null; + } + if (zoom < midpoint) + { + if (ty >= width_in_tiles / 2) + { + return null; + } + } + else + { + if (ty != 0) + { + return null; + } + } + + String url = this.baseURL + "/" + zoom + "/" + ty + "/" + tx + ".jpg"; + // System.out.println("returning: " + url); + return url; + } + +} diff --git a/src/org/jdesktop/swingx/mapviewer/empty/EmptyTileFactory.java b/src/org/jdesktop/swingx/mapviewer/empty/EmptyTileFactory.java new file mode 100644 index 00000000..596fc0af --- /dev/null +++ b/src/org/jdesktop/swingx/mapviewer/empty/EmptyTileFactory.java @@ -0,0 +1,103 @@ +/* + * EmptyTileFactory.java + * + * Created on June 7, 2006, 4:58 PM + * + * To change this template, choose Tools | Template Manager + * and open the template in the editor. + */ + +package org.jdesktop.swingx.mapviewer.empty; + +import java.awt.Color; +import java.awt.Graphics2D; +import java.awt.RenderingHints; +import java.awt.image.BufferedImage; + +import org.jdesktop.swingx.mapviewer.Tile; +import org.jdesktop.swingx.mapviewer.TileFactory; +import org.jdesktop.swingx.mapviewer.TileFactoryInfo; + +/** + * A null implementation of TileFactory. Draws empty areas. + * @author joshy + */ +public class EmptyTileFactory extends TileFactory +{ + /** + * The empty tile image. + */ + private BufferedImage emptyTile; + + /** + * Creates a new instance of EmptyTileFactory + */ + public EmptyTileFactory() + { + this(new TileFactoryInfo("EmptyTileFactory 256x256", 1, 15, 17, 256, true, true, "", "x", "y", "z")); + } + + /** + * Creates a new instance of EmptyTileFactory using the specified info. + * @param info the tile factory info + */ + public EmptyTileFactory(TileFactoryInfo info) + { + super(info); + int tileSize = info.getTileSize(info.getMinimumZoomLevel()); + emptyTile = new BufferedImage(tileSize, tileSize, BufferedImage.TYPE_INT_ARGB); + Graphics2D g = emptyTile.createGraphics(); + g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); + g.setColor(Color.GRAY); + g.fillRect(0, 0, tileSize, tileSize); + g.setColor(Color.WHITE); + g.drawOval(10, 10, tileSize - 20, tileSize - 20); + g.fillOval(70, 50, 20, 20); + g.fillOval(tileSize - 90, 50, 20, 20); + g.fillOval(tileSize / 2 - 10, tileSize / 2 - 10, 20, 20); + g.dispose(); + } + + /** + * Gets an instance of an empty tile for the given tile position and zoom on the world map. + * @param x The tile's x position on the world map. + * @param y The tile's y position on the world map. + * @param zoom The current zoom level. + */ + @Override + public Tile getTile(int x, int y, int zoom) + { + return new Tile(x, y, zoom) + { + @Override + public synchronized boolean isLoaded() + { + return true; + } + + @Override + public BufferedImage getImage() + { + return emptyTile; + } + + }; + } + + @Override + public void dispose() + { + // noop + } + + /** + * Override this method to load the tile using, for example, an ExecutorService. + * @param tile The tile to load. + */ + @Override + protected void startLoading(Tile tile) + { + // noop + } + +} diff --git a/src/org/jdesktop/swingx/mapviewer/esri/ESRITileFactory.java b/src/org/jdesktop/swingx/mapviewer/esri/ESRITileFactory.java new file mode 100644 index 00000000..b4f0290b --- /dev/null +++ b/src/org/jdesktop/swingx/mapviewer/esri/ESRITileFactory.java @@ -0,0 +1,98 @@ +/* + * ESRITileFactory.java + * + * Created on November 7, 2006, 10:51 AM + * + * To change this template, choose Tools | Template Manager + * and open the template in the editor. + */ + +package org.jdesktop.swingx.mapviewer.esri; + +import java.awt.geom.Point2D; +import java.math.BigDecimal; +import java.math.RoundingMode; + +import org.jdesktop.swingx.mapviewer.DefaultTileFactory; +import org.jdesktop.swingx.mapviewer.GeoPosition; +import org.jdesktop.swingx.mapviewer.TileFactoryInfo; +import org.jdesktop.swingx.mapviewer.util.GeoUtil; + +/** + * @author rbair + */ +public class ESRITileFactory extends DefaultTileFactory +{ + private static final String projection = "8"; // mercator projection + private static final String format = "png"; // get pngs back + private String userId; + private String datasource; // should be enum + + /** Creates a new instance of ESRITileFactory */ + public ESRITileFactory() + { + super(new ESRITileProviderInfo()); + ((ESRITileProviderInfo) super.getInfo()).factory = this; + datasource = "ArcWeb:TA.Streets.NA"; + } + + /** + * @param id the ESRI user id + */ + public void setUserID(String id) + { + this.userId = id; + } + + private static final class ESRITileProviderInfo extends TileFactoryInfo + { + private ESRITileFactory factory; + + private ESRITileProviderInfo() + { + super(0, 17, 18, 256, false, true, "http://www.arcwebservices.com/services/v2006/restmap?actn=getMap", "","", ""); + } + + @Override + public String getTileUrl(int x, int y, int zoom) + { + // "&usrid=&ds=&c=-117.1817|34.0556&sf=52500&fmt=&ocs="; + // System.out.println("getting tile at zoom: " + zoom); + // System.out.println("map width at zoom = " + getMapWidthInTilesAtZoom(zoom)); + + // provide the center point of the tile, in lat/long coords + int tileY = y; + int tileX = x; + int pixelX = tileX * factory.getTileSize(zoom) + (factory.getTileSize(zoom) / 2); + int pixelY = tileY * factory.getTileSize(zoom) + (factory.getTileSize(zoom) / 2); + + GeoPosition latlong = GeoUtil.getPosition(new Point2D.Double(pixelX, pixelY), zoom, this); + + // Chris is going to hate me for this (relying on 72dpi!), but: + // 72 pixels per inch. The earth is 24,859.82 miles in circumference, at the equator. + // Thus, the earth is 24,859.82 * 5280 * 12 * 72 pixels in circumference. + + double numFeetPerDegreeLong = 24859.82 * 5280 / 360; // the number of feet per degree longitude at the + // equator + double numPixelsPerDegreeLong = getLongitudeDegreeWidthInPixels(zoom); + double numPixelsPerFoot = 96 * 12; + int sf = (int) (numFeetPerDegreeLong / (numPixelsPerDegreeLong / numPixelsPerFoot)); + + // round lat and long to 5 decimal places + BigDecimal lat = new BigDecimal(latlong.getLatitude()); + BigDecimal lon = new BigDecimal(latlong.getLongitude()); + lat = lat.setScale(5, RoundingMode.DOWN); + lon = lon.setScale(5, RoundingMode.DOWN); + System.out.println("Tile : [" + tileX + ", " + tileY + "]"); + System.out.println("Pixel : [" + pixelX + ", " + pixelY + "]"); + System.out.println("Lat/Long : [" + latlong.getLatitude() + ", " + latlong.getLongitude() + "]"); + System.out.println("Lat2/Long2: [" + lat.doubleValue() + ", " + lon.doubleValue() + "]"); + + String url = baseURL + "&usrid=" + factory.userId + "&ds=" + factory.datasource + "&c=" + lon.doubleValue() + + "%7C" + lat.doubleValue() + "&sf=" + sf + // 52500" + + "&fmt=" + format + "&ocs=" + projection; + System.out.println("the URL: " + url); + return url; + } + } +} diff --git a/src/org/jdesktop/swingx/mapviewer/resources/loading.png b/src/org/jdesktop/swingx/mapviewer/resources/loading.png new file mode 100644 index 0000000000000000000000000000000000000000..c65be32c23e470023ef11171cfe8eea1872231d6 GIT binary patch literal 3318 zcmVjzP)KLZ*U+IBfRsybQWXdwQbLP>6pAqfylh#{fb6;Z(vMMVS~$e@S=j*ftg6;Uhf59&ghTmgWD0l;*T zI709Y^p6lP1rIRMx#05C~cW=H_Aw*bJ-5DT&Z2n+x)QHX^p z00esgV8|mQcmRZ%02D^@S3L16t`O%c004NIvOKvYIYoh62rY33S640`D9%Y2D-rV&neh&#Q1i z007~1e$oCcFS8neI|hJl{-P!B1ZZ9hpmq0)X0i`JwE&>$+E?>%_LC6RbVIkUx0b+_+BaR3cnT7Zv!AJxW zizFb)h!jyGOOZ85F;a?DAXP{m@;!0_IfqH8(HlgRxt7s3}k3K`kFu>>-2Q$QMFfPW!La{h336o>X zu_CMttHv6zR;&ZNiS=X8v3CR#fknUxHUxJ0uoBa_M6WNWeqIg~6QE69c9o#eyhGvpiOA@W-aonk<7r1(?fC{oI5N*U!4 zfg=2N-7=cNnjjOr{yriy6mMFgG#l znCF=fnQv8CDz++o6_Lscl}eQ+l^ZHARH>?_s@|##Rr6KLRFA1%Q+=*RRWnoLsR`7U zt5vFIcfW3@?wFpwUVxrVZ>QdQz32KIeJ}k~{cZZE^+ya? z2D1z#2HOnI7(B%_ac?{wFUQ;QQA1tBKtrWrm0_3Rgps+?Jfqb{jYbcQX~taRB;#$y zZN{S}1|}gUOHJxc?wV3fxuz+mJ4`!F$IZ;mqRrNsHJd##*D~ju=bP7?-?v~|cv>vB zsJ6IeNwVZxrdjT`yl#bBIa#GxRa#xMMy;K#CDyyGyQdMSxlWT#tDe?p!?5wT$+oGt z8L;Kp2HUQ-ZMJ=3XJQv;x5ci*?vuTfeY$;({XGW_huIFR9a(?@3)XSs8O^N5RyOM=TTmp(3=8^+zpz2r)C z^>JO{deZfso3oq3?Wo(Y?l$ge?uXo;%ru`Vo>?<<(8I_>;8Eq#KMS9gFl*neeosSB zfoHYnBQIkwkyowPu(zdms`p{<7e4kra-ZWq<2*OsGTvEV%s0Td$hXT+!*8Bnh2KMe zBmZRodjHV?r+_5^X9J0WL4jKW`}lf%A-|44I@@LTvf1rHjG(ze6+w@Jt%Bvjts!X0 z?2xS?_ve_-kiKB_KiJlZ$9G`c^=E@oNG)mWWaNo-3TIW8)$Hg0Ub-~8?KhvJ>$ z3*&nim@mj(aCxE5!t{lw7O5^0EIO7zOo&c6l<+|iDySBWCGrz@C5{St!X3hAA}`T4 z(TLbXTq+(;@<=L8dXnssyft|w#WSTW<++3>sgS%(4NTpeI-VAqb|7ssJvzNHgOZVu zaYCvgO_R1~>SyL=cFU|~g|hy|Zi}}s9+d~lYqOB71z9Z$wnC=pR9Yz4DhIM>Wmjgu z&56o6maCpC&F##y%G;1PobR9i?GnNg;gYtchD%p19a!eQtZF&3JaKv33gZ<8D~47E ztUS1iwkmDaPpj=$m#%)jCVEY4fnLGNg2A-`YwHVD3gv};>)hAvT~AmqS>Lr``i7kw zJ{5_It`yrBmlc25DBO7E8;5VoznR>Ww5hAaxn$2~(q`%A-YuS64wkBy=9dm`4cXeX z4c}I@?e+FW+b@^RDBHV(wnMq2zdX3SWv9u`%{xC-q*U}&`cyXV(%rRT*Z6MH?i+i& z_B8C(+grT%{XWUQ+f@NoP1R=AW&26{v-dx)iK^-Nmiuj8txj!m?Z*Ss1N{dh4z}01 z)YTo*JycSU)+_5r4#yw9{+;i4Ee$peRgIj+;v;ZGdF1K$3E%e~4LaI(jC-u%2h$&R z9cLXcYC@Xwnns&bn)_Q~Te?roKGD|d-g^8;+aC{{G(1^(O7m37Y1-+6)01cN&y1aw zoqc{T`P^XJqPBbIW6s}d4{z_f5Om?vMgNQEJG?v2T=KYd^0M3I6IZxbny)%vZR&LD zJpPl@Psh8QyPB@KTx+@RdcC!KX7}kEo;S|j^u2lU7XQ}Oo;f|;z4Ll+_r>@1-xl3| zawq-H%e&ckC+@AhPrP6BKT#_XdT7&;F71j}Joy zkC~6lh7E@6o;W@^IpRNZ{ptLtL(gQ-CY~4mqW;US7Zxvm_|@yz&e53Bp_lTPlfP|z zrTyx_>lv@x#=^!PzR7qqF<$gm`|ZJZ+;<)Cqu&ot2z=00004XF*Lt006O$eEU(80000WV@Og>004R=004l4008;_004mL004C` z008P>0026e000+nl3&F}0006JNkl!&15YZtR4M^M zaH`bR(W#(&Ko_A95S*MH-JD!3Iu*gGLs4*YDilJYLZ)JA_&NA1gpjn)=GI&5IdHjf z&!2lf-h20)%d1G@GRPG!i3CW11W14cNPq-LfCR|-$<@EG$0AS#hQRi}1TqIa0#<=L z6ZD(FTi`WtkZRLP;`_)7@Cs-GA3eS~;W?lU-1XQ2o&fKX>?|qOSl4g)AgLHxZ+X2n zvevep>^JlNsUW&esy9JbSXWHY^{ne&1dwG(N0MGn(Vql@{f->x%z32vMz(w^-So*c zE(P!fxCVTkpszXx!1Y{pV5Due)8j5BwLYyI5C8xG07*qoM6N<$f_Kp< ARR910 literal 0 HcmV?d00001 diff --git a/src/org/jdesktop/swingx/mapviewer/resources/minus.png b/src/org/jdesktop/swingx/mapviewer/resources/minus.png new file mode 100644 index 0000000000000000000000000000000000000000..8fe9a8d4a7c71beb78e2dd3a8e23a608cb26334b GIT binary patch literal 75 zcmeAS@N?(olHy`uVBq!ia0vp^93afW0wnX;%77$;kf)1d2uE~sf&z1nL-PzK6=sI# WYTS%-PRDHmNqf5bxvXoH-87 dGniDQxfn!@g&l*IPP`0K<>~6@vd$@?2>?jX5lsL9 literal 0 HcmV?d00001 diff --git a/src/org/jdesktop/swingx/mapviewer/resources/standard_waypoint.png b/src/org/jdesktop/swingx/mapviewer/resources/standard_waypoint.png new file mode 100644 index 0000000000000000000000000000000000000000..7b13069568c90ef90a459f63d0a8d829f3a090ed GIT binary patch literal 1152 zcmV-`1b_R9P)klVi6Mo;(swRmP&18S;1iN&U}A(Q z`}hAp!{_(U8Sb24!*G6g55t#tkAFf9-3BuNAb^k!bOz#`vRZD;?nz4-`1nN_{s7(a z_aCZ_OhC-a%)s#F>qmy&3*s5>U)Tsz5e&o|K=J?q1T#V4zF!8Jaag9N701!YR1Cv=;xtLW= z!x(=5V;~uT^r#pHGO%+Ag3SU5AP`|8B&Er~3yjTw|4BCxq=%DRgh5zZ7sS;A2q2Jw z0^I!449tuSWCJFkDLjHQASMq$0D&Wgk%eCET;8(l&;N zSHJ~yB{q!!0ffuIBS3um;=V}?KYx6KyYfFUOfT-62x7hj;+fbq0t65fo`}r*^!EP$ z`sgHHoM01+~9-j6Tu{=a^7f#K_yH^Afqs^b|J z;nM&RKr93dyamLc-#j_b#|X6g@9&?Wq`ncK27mx!AtKA(112GZzrR5>B?Bn!y~3v$ zAb^N5@G>y-8~pnI3B&+i@q3sF$gW4KC4F+czjW8lNDAD_eOo$G`Y z0|XE;2EP9H_ZI`Sn0!r0F+cz@5n3=lxXAU+@lMe!dXmL;SZAiw}9zDO9j Sgvwn20000tiles is valid at this zoom level. For example, if the zoom level is 0 + * (zoomed all the way out, where there is only one tile), then x,y must be 0,0 + */ + public static boolean isValidTile(int x, int y, int zoomLevel, TileFactoryInfo info) + { + // int x = (int)coord.getX(); + // int y = (int)coord.getY(); + // if off the map to the top or left + if (x < 0 || y < 0) + { + return false; + } + // if of the map to the right + if (info.getMapCenterInPixelsAtZoom(zoomLevel).getX() * 2 <= x * info.getTileSize(zoomLevel)) + { + return false; + } + // if off the map to the bottom + if (info.getMapCenterInPixelsAtZoom(zoomLevel).getY() * 2 <= y * info.getTileSize(zoomLevel)) + { + return false; + } + // if out of zoom bounds + if (zoomLevel < info.getMinimumZoomLevel() || zoomLevel > info.getMaximumZoomLevel()) + { + return false; + } + return true; + } + + /** + * Given a position (latitude/longitude pair) and a zoom level, return the appropriate point in pixels. The + * zoom level is necessary because pixel coordinates are in terms of the zoom level + * @param c A lat/lon pair + * @param zoomLevel the zoom level to extract the pixel coordinate for + * @param info the tile factory info + * @return the coordinate + */ + public static Point2D getBitmapCoordinate(GeoPosition c, int zoomLevel, TileFactoryInfo info) + { + return getBitmapCoordinate(c.getLatitude(), c.getLongitude(), zoomLevel, info); + } + + /** + * Given a position (latitude/longitude pair) and a zoom level, return the appropriate point in pixels. The + * zoom level is necessary because pixel coordinates are in terms of the zoom level + * @param latitude the latitude + * @param longitude the longitude + * @param zoomLevel the zoom level to extract the pixel coordinate for + * @param info the tile factory info + * @return the coordinate + */ + public static Point2D getBitmapCoordinate(double latitude, double longitude, int zoomLevel, TileFactoryInfo info) + { + double x = info.getMapCenterInPixelsAtZoom(zoomLevel).getX() + longitude + * info.getLongitudeDegreeWidthInPixels(zoomLevel); + double e = Math.sin(latitude * (Math.PI / 180.0)); + if (e > 0.9999) + { + e = 0.9999; + } + if (e < -0.9999) + { + e = -0.9999; + } + double y = info.getMapCenterInPixelsAtZoom(zoomLevel).getY() + 0.5 * Math.log((1 + e) / (1 - e)) * -1 + * (info.getLongitudeRadianWidthInPixels(zoomLevel)); + return new Point2D.Double(x, y); + } + + /** + * Convert an on screen pixel coordinate and a zoom level to a geo position + * @param pixelCoordinate the coordinate in pixels + * @param zoom the zoom level + * @param info the tile factory info + * @return a geo position + */ + public static GeoPosition getPosition(Point2D pixelCoordinate, int zoom, TileFactoryInfo info) + { + // p(" --bitmap to latlon : " + coord + " " + zoom); + double wx = pixelCoordinate.getX(); + double wy = pixelCoordinate.getY(); + // this reverses getBitmapCoordinates + double flon = (wx - info.getMapCenterInPixelsAtZoom(zoom).getX()) / info.getLongitudeDegreeWidthInPixels(zoom); + double e1 = (wy - info.getMapCenterInPixelsAtZoom(zoom).getY()) + / (-1 * info.getLongitudeRadianWidthInPixels(zoom)); + double e2 = (2 * Math.atan(Math.exp(e1)) - Math.PI / 2) / (Math.PI / 180.0); + double flat = e2; + GeoPosition wc = new GeoPosition(flat, flon); + return wc; + } + + /** + * Gets the map bounds. + * @param mapViewer The map viewer. + * @return Returns the bounds. + */ + public static GeoBounds getMapBounds(JXMapViewer mapViewer) + { + return new GeoBounds(getMapGeoBounds(mapViewer)); + } + + /** + * Gets the bounds as a set of two GeoPosition objects. + * @param mapViewer The map viewer. + * @return Returns the set of two GeoPosition objects that represent the north west and south east + * corners of the map. + */ + private static Set getMapGeoBounds(JXMapViewer mapViewer) + { + Set set = new HashSet(); + TileFactory tileFactory = mapViewer.getTileFactory(); + int zoom = mapViewer.getZoom(); + Rectangle2D bounds = mapViewer.getViewportBounds(); + Point2D pt = new Point2D.Double(bounds.getX(), bounds.getY()); + set.add(tileFactory.pixelToGeo(pt, zoom)); + pt = new Point2D.Double(bounds.getX() + bounds.getWidth(), bounds.getY() + bounds.getHeight()); + set.add(tileFactory.pixelToGeo(pt, zoom)); + return set; + } + +} diff --git a/src/org/jdesktop/swingx/mapviewer/util/MercatorUtils.java b/src/org/jdesktop/swingx/mapviewer/util/MercatorUtils.java new file mode 100644 index 00000000..e9eb5f52 --- /dev/null +++ b/src/org/jdesktop/swingx/mapviewer/util/MercatorUtils.java @@ -0,0 +1,78 @@ +/* + * MercatorUtils.java + * + * Created on October 7, 2006, 6:02 PM + * + * To change this template, choose Tools | Template Manager + * and open the template in the editor. + */ + +package org.jdesktop.swingx.mapviewer.util; + +/** + * A utility class of methods that help when + * dealing with standard Mercator projections. + * @author joshua.marinacci@sun.com + */ +public final class MercatorUtils +{ + + /** + * Creates a new instance of MercatorUtils + */ + private MercatorUtils() + { + } + + /** + * @param longitudeDegrees the longitude in degrees + * @param radius the world radius in pixels + * @return the x value + */ + public static int longToX(double longitudeDegrees, double radius) + { + double longitude = Math.toRadians(longitudeDegrees); + return (int) (radius * longitude); + } + + /** + * @param latitudeDegrees the latitude in degrees + * @param radius the world radius in pixels + * @return the y value + */ + public static int latToY(double latitudeDegrees, double radius) + { + double latitude = Math.toRadians(latitudeDegrees); + double y = radius / 2.0 * Math.log((1.0 + Math.sin(latitude)) / (1.0 - Math.sin(latitude))); + return (int) y; + } + + /** + * @param x the x value + * @param radius the world radius in pixels + * @return the longitude in degrees + */ + public static double xToLong(int x, double radius) + { + double longRadians = x / radius; + double longDegrees = Math.toDegrees(longRadians); + /* + * The user could have panned around the world a lot of times. Lat long goes from -180 to 180. So every time a + * user gets to 181 we want to subtract 360 degrees. Every time a user gets to -181 we want to add 360 degrees. + */ + int rotations = (int) Math.floor((longDegrees + 180) / 360); + double longitude = longDegrees - (rotations * 360); + return longitude; + } + + /** + * @param y the y value + * @param radius the world radius in pixels + * @return the latitude in degrees + */ + public static double yToLat(int y, double radius) + { + double latitude = (Math.PI / 2) - (2 * Math.atan(Math.exp(-1.0 * y / radius))); + return Math.toDegrees(latitude); + } +} diff --git a/src/org/jdesktop/swingx/mapviewer/wms/WMSService.java b/src/org/jdesktop/swingx/mapviewer/wms/WMSService.java new file mode 100644 index 00000000..b74f9201 --- /dev/null +++ b/src/org/jdesktop/swingx/mapviewer/wms/WMSService.java @@ -0,0 +1,113 @@ +/* + * WMSService.java + * + * Created on October 7, 2006, 6:06 PM + * + * To change this template, choose Tools | Template Manager + * and open the template in the editor. + */ + +package org.jdesktop.swingx.mapviewer.wms; + +import org.jdesktop.swingx.mapviewer.util.MercatorUtils; + +/** + * A class that represents a WMS mapping service. + * See http://en.wikipedia.org/wiki/Web_Map_Service for more information. + * @author joshy + */ +public class WMSService +{ + private String baseUrl; + private String layer; + + /** + * Creates a new instance of WMSService + * TODO: not working -> remove + */ + public WMSService() + { + // by default use a known nasa server + setLayer("BMNG"); + setBaseUrl("http://wms.jpl.nasa.gov/wms.cgi?"); + } + + /** + * @param baseUrl the base URL + * @param layer the layer + */ + public WMSService(String baseUrl, String layer) + { + this.baseUrl = baseUrl; + this.layer = layer; + } + + /** + * Convertes to a WMS URL + * @param x the x coordinate + * @param y the y coordinate + * @param zoom the zomm factor + * @param tileSize the tile size + * @return a URL request string + */ + public String toWMSURL(int x, int y, int zoom, int tileSize) + { + String format = "image/jpeg"; + String styles = ""; + String srs = "EPSG:4326"; + int ts = tileSize; + int circumference = widthOfWorldInPixels(zoom, tileSize); + double radius = circumference / (2 * Math.PI); + double ulx = MercatorUtils.xToLong(x * ts, radius); + double uly = MercatorUtils.yToLat(y * ts, radius); + double lrx = MercatorUtils.xToLong((x + 1) * ts, radius); + double lry = MercatorUtils.yToLat((y + 1) * ts, radius); + String bbox = ulx + "," + uly + "," + lrx + "," + lry; + String url = getBaseUrl() + "version=1.1.1&request=" + "GetMap&Layers=" + layer + "&format=" + format + + "&BBOX=" + bbox + "&width=" + ts + "&height=" + ts + "&SRS=" + srs + "&Styles=" + styles + + // "&transparent=TRUE"+ + ""; + return url; + } + + private int widthOfWorldInPixels(int zoom, int TILE_SIZE) + { + // int TILE_SIZE = 256; + int tiles = (int) Math.pow(2, zoom); + int circumference = TILE_SIZE * tiles; + return circumference; + } + + /** + * @return the layer + */ + public String getLayer() + { + return layer; + } + + /** + * @param layer the layer + */ + public void setLayer(String layer) + { + this.layer = layer; + } + + /** + * @return the base URL + */ + public String getBaseUrl() + { + return baseUrl; + } + + /** + * @param baseUrl the base URL + */ + public void setBaseUrl(String baseUrl) + { + this.baseUrl = baseUrl; + } + +} diff --git a/src/org/jdesktop/swingx/mapviewer/wms/WMSTileFactory.java b/src/org/jdesktop/swingx/mapviewer/wms/WMSTileFactory.java new file mode 100644 index 00000000..dd3a6f99 --- /dev/null +++ b/src/org/jdesktop/swingx/mapviewer/wms/WMSTileFactory.java @@ -0,0 +1,42 @@ +/* + * WMSTileFactory.java + * + * Created on October 7, 2006, 6:07 PM + * + * To change this template, choose Tools | Template Manager + * and open the template in the editor. + */ + +package org.jdesktop.swingx.mapviewer.wms; + +import org.jdesktop.swingx.mapviewer.DefaultTileFactory; +import org.jdesktop.swingx.mapviewer.TileFactoryInfo; + +/** + * A tile factory that uses a WMS service. + * @author joshy + */ +public class WMSTileFactory extends DefaultTileFactory +{ + /** + * Creates a new instance of WMSTileFactory + * @param wms the WMSService + */ + public WMSTileFactory(final WMSService wms) + { + // tile size and x/y orientation is r2l & t2b + super(new TileFactoryInfo(0, 15, 17, 500, true, true, "", "x", "y", "zoom") + { + @Override + public String getTileUrl(int x, int y, int zoom) + { + int zz = 17 - zoom; + int z = 4; + z = (int) Math.pow(2, (double) zz - 1); + return wms.toWMSURL(x - z, z - 1 - y, zz, getTileSize(zoom)); + } + + }); + } + +} diff --git a/src/org/jdesktop/swingx/painter/AbstractPainter.java b/src/org/jdesktop/swingx/painter/AbstractPainter.java new file mode 100644 index 00000000..9ee25a72 --- /dev/null +++ b/src/org/jdesktop/swingx/painter/AbstractPainter.java @@ -0,0 +1,441 @@ +/* + * $Id: AbstractPainter.java 4082 2011-11-15 18:39:43Z kschaefe $ + * + * Copyright 2004 Sun Microsystems, Inc., 4150 Network Circle, + * Santa Clara, California 95054, U.S.A. All rights reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + */ + +package org.jdesktop.swingx.painter; + +import java.awt.AlphaComposite; +import java.awt.Composite; +import java.awt.Graphics2D; +import java.awt.RenderingHints; +import java.awt.image.BufferedImage; +import java.awt.image.BufferedImageOp; +import java.lang.ref.SoftReference; + +import org.jdesktop.beans.AbstractBean; +import org.jdesktop.swingx.util.GraphicsUtilities; + +/** + *

A convenient base class from which concrete {@link Painter} implementations may + * extend. It extends {@link org.jdesktop.beans.AbstractBean} as a convenience for + * adding property change notification support. In addition, AbstractPainter + * provides subclasses with the ability to cacheable painting operations, configure the + * drawing surface with common settings (such as antialiasing and interpolation), and + * toggle whether a subclass paints or not via the visibility property.

+ * + *

Subclasses of AbstractPainter generally need only override the + * {@link #doPaint(Graphics2D, Object, int, int)} method. If a subclass requires more control + * over whether caching is enabled, or for configuring the graphics state, then it + * may override the appropriate protected methods to interpose its own behavior.

+ * + *

For example, here is the doPaint method of a simple Painter that + * paints an opaque rectangle: + *


+ *  public void doPaint(Graphics2D g, T obj, int width, int height) {
+ *      g.setPaint(Color.BLUE);
+ *      g.fillRect(0, 0, width, height);
+ *  }
+ * 

+ * + * @author rbair + * @param an optional configuration parameter + */ +@SuppressWarnings("nls") +public abstract class AbstractPainter extends AbstractBean implements Painter { + /** + * An enum representing the possible interpolation values of Bicubic, Bilinear, and + * Nearest Neighbor. These map to the underlying RenderingHints, + * but are easier to use and serialization safe. + */ + public enum Interpolation { + /** + * use bicubic interpolation + */ + Bicubic(RenderingHints.VALUE_INTERPOLATION_BICUBIC), + /** + * use bilinear interpolation + */ + Bilinear(RenderingHints.VALUE_INTERPOLATION_BILINEAR), + /** + * use nearest neighbor interpolation + */ + NearestNeighbor(RenderingHints.VALUE_INTERPOLATION_NEAREST_NEIGHBOR); + + private Object value; + + Interpolation(Object value) { + this.value = value; + } + + private void configureGraphics(Graphics2D g) { + g.setRenderingHint(RenderingHints.KEY_INTERPOLATION, value); + } + } + + //--------------------------------------------------- Instance Variables + /** + * The cached image, if shouldUseCache() returns true + */ + private transient SoftReference cachedImage; + private boolean cacheCleared = true; + private boolean cacheable = false; + private boolean dirty = false; + private BufferedImageOp[] filters = new BufferedImageOp[0]; + private boolean antialiasing = true; + private Interpolation interpolation = Interpolation.NearestNeighbor; + private boolean visible = true; + private boolean inPaintContext; + + /** + * Creates a new instance of AbstractPainter. + */ + public AbstractPainter() { } + + /** + * Creates a new instance of AbstractPainter. + * @param cacheable indicates if this painter should be cacheable + */ + public AbstractPainter(boolean cacheable) { + setCacheable(cacheable); + } + + /** + * A defensive copy of the Effects to apply to the results + * of the AbstractPainter's painting operation. The array may + * be empty but it will never be null. + * @return the array of filters applied to this painter + */ + public final BufferedImageOp[] getFilters() { + BufferedImageOp[] results = new BufferedImageOp[filters.length]; + System.arraycopy(filters, 0, results, 0, results.length); + return results; + } + + /** + *

A convenience method for specifying the filters to use based on + * BufferedImageOps. These will each be individually wrapped by an ImageFilter + * and then setFilters(Effect... filters) will be called with the resulting + * array

+ * + * + * @param effects the BufferedImageOps to wrap as filters + */ + public void setFilters(BufferedImageOp ... effects) { + if (effects == null) effects = new BufferedImageOp[0]; + BufferedImageOp[] old = getFilters(); + this.filters = new BufferedImageOp[effects.length]; + System.arraycopy(effects, 0, this.filters, 0, this.filters.length); + setDirty(true); + firePropertyChange("filters", old, getFilters()); + } + + /** + * Returns if antialiasing is turned on or not. The default value is true. + * This is a bound property. + * @return the current antialiasing setting + */ + public boolean isAntialiasing() { + return antialiasing; + } + /** + * Sets the antialiasing setting. This is a bound property. + * @param value the new antialiasing setting + */ + public void setAntialiasing(boolean value) { + boolean old = isAntialiasing(); + antialiasing = value; + if (old != value) setDirty(true); + firePropertyChange("antialiasing", old, isAntialiasing()); + } + + /** + * Gets the current interpolation setting. This property determines if interpolation will + * be used when drawing scaled images. @see java.awt.RenderingHints.KEY_INTERPOLATION. + * @return the current interpolation setting + */ + public Interpolation getInterpolation() { + return interpolation; + } + + /** + * Sets a new value for the interpolation setting. This setting determines if interpolation + * should be used when drawing scaled images. @see java.awt.RenderingHints.KEY_INTERPOLATION. + * @param value the new interpolation setting + */ + public void setInterpolation(Interpolation value) { + Object old = getInterpolation(); + this.interpolation = value == null ? Interpolation.NearestNeighbor : value; + if (old != value) setDirty(true); + firePropertyChange("interpolation", old, getInterpolation()); + } + + /** + * Gets the visible property. This controls if the painter should + * paint itself. It is true by default. Setting visible to false + * is good when you want to temporarily turn off a painter. An example + * of this is a painter that you only use when a button is highlighted. + * + * @return current value of visible property + */ + public boolean isVisible() { + return this.visible; + } + + /** + *

Sets the visible property. This controls if the painter should + * paint itself. It is true by default. Setting visible to false + * is good when you want to temporarily turn off a painter. An example + * of this is a painter that you only use when a button is highlighted.

+ * + * @param visible New value of visible property. + */ + public void setVisible(boolean visible) { + boolean old = isVisible(); + this.visible = visible; + if (old != visible) setDirty(true); //not the most efficient, but I must do this otherwise a CompoundPainter + //or other aggregate painter won't know that it is now invalid + //there might be a tricky solution but that is a performance optimization + firePropertyChange("visible", old, isVisible()); + } + + /** + *

Gets whether this AbstractPainter can be cached as an image. + * If caching is enabled, then it is the responsibility of the developer to + * invalidate the painter (via {@link #clearCache}) if external state has + * changed in such a way that the painter is invalidated and needs to be + * repainted.

+ * + * @return whether this is cacheable + */ + public boolean isCacheable() { + return cacheable; + } + + /** + *

Sets whether this AbstractPainter can be cached as an image. + * If true, this is treated as a hint. That is, a cacheable may or may not be used. + * The {@link #shouldUseCache} method actually determines whether the cacheable is used. + * However, if false, then this is treated as an absolute value. That is, no + * cacheable will be used.

+ * + *

If set to false, then #clearCache is called to free system resources.

+ * + * @param cacheable the cache flag + */ + public void setCacheable(boolean cacheable) { + boolean old = isCacheable(); + this.cacheable = cacheable; + firePropertyChange("cacheable", old, isCacheable()); + if (!isCacheable()) { + clearCache(); + } + } + + /** + *

Call this method to clear the cacheable. This may be called whether there is + * a cacheable being used or not. If cleared, on the next call to paint, + * the painting routines will be called.

+ * + *

SubclassesIf overridden in subclasses, you + * must call super.clearCache, or physical + * resources (such as an Image) may leak.

+ */ + public void clearCache() { + BufferedImage cache = cachedImage == null ? null : cachedImage.get(); + if (cache != null) { + cache.flush(); + } + cacheCleared = true; + if (!isCacheable()) { + cachedImage = null; + } + } + +// /** +// * Only made package private for testing. Don't call this method outside +// * of this class! This is NOT a bound property +// */ +// private boolean isCacheCleared() { +// return cacheCleared; +// } + + /** + *

Called to allow Painter subclasses a chance to see if any state + * in the given object has changed from the last paint operation. If it has, then + * the Painter has a chance to mark itself as dirty, thus causing a + * repaint, even if cached.

+ * + * @param object the object to validate + */ + protected void validate(T object) { /* do nothing */ } + + /** + * Ye olde dirty bit. If true, then the painter is considered dirty and in need of + * being repainted. This is a bound property. + * + * @return true if the painter state has changed and the painter needs to be + * repainted. + */ + protected boolean isDirty() { + return dirty; + } + + /** + * Sets the dirty bit. If true, then the painter is considered dirty, and the cache + * will be cleared. This property is bound. + * + * @param d whether this Painter is dirty. + */ + protected void setDirty(boolean d) { + boolean old = isDirty(); + this.dirty = d; + firePropertyChange("dirty", old, isDirty()); + if (isDirty()) { + clearCache(); + } + } + + boolean isInPaintContext() { + return inPaintContext; + } + + void setInPaintContext(boolean inPaintContext) { + this.inPaintContext = inPaintContext; + } + + /** + *

Returns true if the painter should use caching. This method allows subclasses to + * specify the heuristics regarding whether to cache or not. If a Painter + * has intelligent rules regarding painting times, and can more accurately indicate + * whether it should be cached, it could implement that logic in this method.

+ * + * @return whether or not a cache should be used + */ + protected boolean shouldUseCache() { + return isCacheable() || filters.length > 0; //NOTE, I can only do this because getFilters() is final + } + + /** + *

This method is called by the paint method prior to + * any drawing operations to configure the drawing surface. The default + * implementation sets the rendering hints that have been specified for + * this AbstractPainter.

+ * + *

This method can be overridden by subclasses to modify the drawing + * surface before any painting happens.

+ * + * @param g the graphics surface to configure. This will never be null. + * @see #paint(Graphics2D, Object, int, int) + */ + protected void configureGraphics(Graphics2D g) { + //configure antialiasing + if(isAntialiasing()) { + g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, + RenderingHints.VALUE_ANTIALIAS_ON); + } else { + g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, + RenderingHints.VALUE_ANTIALIAS_OFF); + } + + getInterpolation().configureGraphics(g); + } + + /** + * Subclasses must implement this method and perform custom painting operations + * here. + * @param width the width + * @param height the height + * @param g The Graphics2D object in which to paint + * @param object an optional configuration parameter + */ + protected abstract void doPaint(Graphics2D g, T object, int width, int height); + + @Override + public final void paint(Graphics2D g, T obj, int width, int height) { + if (g == null) { + throw new NullPointerException("The Graphics2D must be supplied"); + } + + if(!isVisible() || width < 1 || height < 1) { + return; + } + + configureGraphics(g); + + //paint to a temporary image if I'm caching, or if there are filters to apply + if (shouldUseCache() || filters.length > 0) { + validate(obj); + BufferedImage cache = cachedImage == null ? null : cachedImage.get(); + boolean invalidCache = null == cache || + cache.getWidth() != width || + cache.getHeight() != height; + + if (cacheCleared || invalidCache || isDirty()) { + //rebuild the cacheable. I do this both if a cacheable is needed, and if any + //filters exist. I only *save* the resulting image if caching is turned on + if (invalidCache) { + cache = GraphicsUtilities.createCompatibleTranslucentImage(width, height); + } + if (cache != null) { + Graphics2D gfx = cache.createGraphics(); + + try { + gfx.setClip(0, 0, width, height); + + if (!invalidCache) { + // If we are doing a repaint, but we didn't have to + // recreate the image, we need to clear it back + // to a fully transparent background. + Composite composite = gfx.getComposite(); + gfx.setComposite(AlphaComposite.Clear); + gfx.fillRect(0, 0, width, height); + gfx.setComposite(composite); + } + + configureGraphics(gfx); + doPaint(gfx, obj, width, height); + } finally { + gfx.dispose(); + } + + if (!isInPaintContext()) { + for (BufferedImageOp f : getFilters()) { + cache = f.filter(cache, null); + } + } + + //only save the temporary image as the cacheable if I'm caching + if (shouldUseCache()) { + cachedImage = new SoftReference(cache); + cacheCleared = false; + } + } + } + + g.drawImage(cache, 0, 0, null); + } else { + //can't use the cacheable, so just paint + doPaint(g, obj, width, height); + } + + //painting has occured, so restore the dirty bit to false + setDirty(false); + } +} diff --git a/src/org/jdesktop/swingx/painter/CompoundPainter.java b/src/org/jdesktop/swingx/painter/CompoundPainter.java new file mode 100644 index 00000000..ed7c128b --- /dev/null +++ b/src/org/jdesktop/swingx/painter/CompoundPainter.java @@ -0,0 +1,497 @@ +/* + * $Id: CompoundPainter.java 4147 2012-02-01 17:13:24Z kschaefe $ + * + * Copyright 2004 Sun Microsystems, Inc., 4150 Network Circle, + * Santa Clara, California 95054, U.S.A. All rights reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + */ + +package org.jdesktop.swingx.painter; + +import java.awt.Graphics2D; +import java.awt.geom.AffineTransform; +import java.beans.PropertyChangeEvent; +import java.beans.PropertyChangeListener; +import java.lang.ref.WeakReference; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; + +//import org.jdesktop.beans.JavaBean; + +/** + *

A {@link Painter} implementation composed of an array of Painters. + * CompoundPainter provides a means for combining several individual + * Painters, or groups of them, into one logical unit. Each of the + * Painters are executed in order. BufferedImageOp filter effects can + * be applied to them together as a whole. The entire set of painting operations + * may be cached together.

+ * + *

+ * + *

For example, if I want to create a CompoundPainter that started with a blue + * background, had pinstripes on it running at a 45 degree angle, and those + * pinstripes appeared to "fade in" from left to right, I would write the following: + *


+ *  Color blue = new Color(0x417DDD);
+ *  Color translucent = new Color(blue.getRed(), blue.getGreen(), blue.getBlue(), 0);
+ *  panel.setBackground(blue);
+ *  panel.setForeground(Color.LIGHT_GRAY);
+ *  GradientPaint blueToTranslucent = new GradientPaint(
+ *    new Point2D.Double(.4, 0),
+ *    blue,
+ *    new Point2D.Double(1, 0),
+ *    translucent);
+ *  MattePainter veil = new MattePainter(blueToTranslucent);
+ *  veil.setPaintStretched(true);
+ *  Painter pinstripes = new PinstripePainter(45);
+ *  Painter backgroundPainter = new RectanglePainter(this.getBackground(), null);
+ *  Painter p = new CompoundPainter(backgroundPainter, pinstripes, veil);
+ *  panel.setBackgroundPainter(p);
+ * 

+ * + * @author rbair + * @param an optional configuration parameter + */ +//@JavaBean +public class CompoundPainter extends AbstractPainter +{ + private static class Handler implements PropertyChangeListener + { + private final WeakReference> ref; + + public Handler(CompoundPainter painter) + { + ref = new WeakReference>(painter); + } + + /** + * {@inheritDoc} + */ + @Override + public void propertyChange(PropertyChangeEvent evt) + { + CompoundPainter painter = ref.get(); + + if (painter == null) + { + AbstractPainter src = (AbstractPainter) evt.getSource(); + src.removePropertyChangeListener(this); + } + else + { + String property = evt.getPropertyName(); + + if ("dirty".equals(property) && evt.getNewValue() == Boolean.FALSE) + { + return; + } + + painter.setDirty(true); + } + } + } + + private Handler handler; + + private List> painters = new CopyOnWriteArrayList>(); + private AffineTransform transform; + private boolean clipPreserved = false; + + private boolean checkForDirtyChildPainters = true; + + /** Creates a new instance of CompoundPainter */ + public CompoundPainter() + { + this((Painter[]) null); + } + + /** + * Convenience constructor for creating a CompoundPainter for an array + * of painters. A defensive copy of the given array is made, so that future + * modification to the array does not result in changes to the CompoundPainter. + * + * @param painters array of painters, which will be painted in order + */ + public CompoundPainter(Painter... painters) + { + handler = new Handler(this); + + setPainters(painters); + } + + /** + * Convenience constructor for creating a CompoundPainter for a list + * of painters. A defensive copy of the given array is made, so that future + * modification to the list does not result in changes to the CompoundPainter. + * + * @param painters array of painters, which will be painted in order + */ + public CompoundPainter(List> painters) + { + handler = new Handler(this); + + setPainters(painters); + } + /** + * Sets the array of Painters to use. These painters will be executed in + * order. A null value will be treated as an empty array. To prevent unexpected + * behavior all values in provided array are copied to internally held array. + * Any changes to the original array will not be reflected. + * + * @param painters array of painters, which will be painted in order + */ + public void setPainters(List> painters) + { + Collection> old = new ArrayList>(getPainters()); + + for (Painter p : old) + { + if (p instanceof AbstractPainter) + { + ((AbstractPainter) p).removePropertyChangeListener(handler); + } + } + + this.painters.clear(); + this.painters.addAll(painters); + + for (Painter p : this.painters) + { + if (p instanceof AbstractPainter) + { + ((AbstractPainter) p).addPropertyChangeListener(handler); + } + } + + setDirty(true); + firePropertyChange("painters", old, getPainters()); + } + + /** + * Sets the array of Painters to use. These painters will be executed in + * order. A null value will be treated as an empty array. To prevent unexpected + * behavior all values in provided array are copied to internally held array. + * Any changes to the original array will not be reflected. + * + * @param painters array of painters, which will be painted in order + */ + public void setPainters(Painter... painters) + { + List> l; + + if (painters == null) + l = Collections.emptyList(); + else + l = Arrays.asList(painters); + + setPainters(l); + } + + /** + * Adds a painter to the queue of painters + * @param painter the painter that is added + */ + public void addPainter(Painter painter) + { + Collection> old = new ArrayList>(getPainters()); + + this.painters.add(painter); + + if (painter instanceof AbstractPainter) + { + ((AbstractPainter) painter).addPropertyChangeListener(handler); + } + + setDirty(true); + firePropertyChange("painters", old, getPainters()); + } + + /** + * Removes a painter from the queue of painters + * @param painter the painter that is added + */ + public void removePainter(Painter painter) + { + Collection> old = new ArrayList>(getPainters()); + + this.painters.remove(painter); + + if (painter instanceof AbstractPainter) + { + ((AbstractPainter) painter).removePropertyChangeListener(handler); + } + + setDirty(true); + firePropertyChange("painters", old, getPainters()); + } + + + /** + * Gets the array of painters used by this CompoundPainter + * @return a defensive copy of the painters used by this CompoundPainter. + * This will never be null. + */ + public final Collection> getPainters() + { + return Collections.unmodifiableCollection(painters); + } + + /** + * Indicates if the clip produced by any painter is left set once it finishes painting. + * Normally the clip will be reset between each painter. Setting clipPreserved to + * true can be used to let one painter mask other painters that come after it. + * @return if the clip should be preserved + * @see #setClipPreserved(boolean) + */ + public boolean isClipPreserved() + { + return clipPreserved; + } + + /** + * Sets if the clip should be preserved. + * Normally the clip will be reset between each painter. Setting clipPreserved to + * true can be used to let one painter mask other painters that come after it. + * + * @param shouldRestoreState new value of the clipPreserved property + * @see #isClipPreserved() + */ + public void setClipPreserved(boolean shouldRestoreState) + { + boolean oldShouldRestoreState = isClipPreserved(); + this.clipPreserved = shouldRestoreState; + setDirty(true); + firePropertyChange("clipPreserved", oldShouldRestoreState, shouldRestoreState); + } + + /** + * Gets the current transform applied to all painters in this CompoundPainter. May be null. + * @return the current AffineTransform + */ + public AffineTransform getTransform() + { + return transform; + } + + /** + * Set a transform to be applied to all painters contained in this CompoundPainter + * @param transform a new AffineTransform + */ + public void setTransform(AffineTransform transform) + { + AffineTransform old = getTransform(); + this.transform = transform; + setDirty(true); + firePropertyChange("transform", old, transform); + } + + /** + *

Iterates over all child Painters and gives them a chance + * to validate themselves. If any of the child painters are dirty, then + * this CompoundPainter marks itself as dirty.

+ */ + @Override + protected void validate(T object) + { + boolean dirty = false; + for (Painter p : painters) + { + if (p instanceof AbstractPainter) + { + AbstractPainter ap = (AbstractPainter) p; + ap.validate(object); + if (ap.isDirty()) + { + dirty = true; + break; + } + } + } + clearLocalCacheOnly = true; + setDirty(dirty); //super will call clear cache + clearLocalCacheOnly = false; + } + + //indicates whether the local cache should be cleared only, as opposed to the + //cache's of all of the children. This is needed to optimize the caching strategy + //when, during validate, the CompoundPainter is marked as dirty + private boolean clearLocalCacheOnly = false; + + /** + * Used by {@link #isDirty()} to check if the child Painters + * should be checked for their dirty flag as part of + * processing.
+ * Default value is: true
+ * This should be set to false if the cacheable state + * of the child Painters are different from each other. This + * will allow the cacheable == true Painters to + * keep their cached image during regular repaints. In this case, + * client code should call {@link #clearCache()} manually when the cacheable + * Painters should be updated. + * @return the dirty check flag + * + * + * @see #isDirty() + */ + public boolean isCheckingDirtyChildPainters() + { + return checkForDirtyChildPainters; + } + + /** + * Set the flag used by {@link #isDirty()} to check if the + * child Painters should be checked for their + * dirty flag as part of processing. + * @param b the dirty check flag + * + * @see #isCheckingDirtyChildPainters() + * @see #isDirty() + */ + public void setCheckingDirtyChildPainters(boolean b) + { + boolean old = isCheckingDirtyChildPainters(); + this.checkForDirtyChildPainters = b; + firePropertyChange("checkingDirtyChildPainters", old, isCheckingDirtyChildPainters()); + } + + /** + *

This CompoundPainter is dirty if it, or (optionally) + * any of its children, are dirty. If the super implementation returns + * true, we return true. Otherwise, if + * {@link #isCheckingDirtyChildPainters()} is true, we iterate + * over all child Painters and query them to see + * if they are dirty. If so, then true is returned. + * Otherwise, we return false.

+ * + * @see #isCheckingDirtyChildPainters() + */ + @Override + protected boolean isDirty() + { + boolean dirty = super.isDirty(); + if (dirty) + { + return true; + } + else if (isCheckingDirtyChildPainters()) + { + for (Painter p : painters) + { + if (p instanceof AbstractPainter) + { + AbstractPainter ap = (AbstractPainter) p; + if (ap.isDirty()) + { + return true; + } + } + } + } + return false; + } + + @Override + protected void setDirty(boolean d) + { + boolean old = super.isDirty(); + boolean ours = isDirty(); + + super.setDirty(d); + + //must perform this check to ensure we do not double notify + if (d != old && d == ours) + { + firePropertyChange("dirty", old, isDirty()); + } + } + + /** + *

Clears the cache of this Painter, and all child + * Painters. This is done to ensure that resources + * are collected, even if clearCache is called by some framework + * or other code that doesn't realize this is a CompoundPainter.

+ * + *

Call #clearLocalCache if you only want to clear the cache of this + * CompoundPainter + */ + @Override + public void clearCache() + { + if (!clearLocalCacheOnly) + { + for (Painter p : painters) + { + if (p instanceof AbstractPainter) + { + AbstractPainter ap = (AbstractPainter) p; + ap.clearCache(); + } + } + } + super.clearCache(); + } + + /** + *

Clears the cache of this painter only, and not of any of the children.

+ */ + public void clearLocalCache() + { + super.clearCache(); + } + + @Override + protected void doPaint(Graphics2D g, T component, int width, int height) + { + for (Painter p : getPainters()) + { + Graphics2D temp = (Graphics2D) g.create(); + + try + { + p.paint(temp, component, width, height); + if (isClipPreserved()) + { + g.setClip(temp.getClip()); + } + } + finally + { + temp.dispose(); + } + } + } + + @Override + protected void configureGraphics(Graphics2D g) + { + //applies the transform + AffineTransform tx = getTransform(); + if (tx != null) + { + g.setTransform(tx); + } + } + + @Override + protected boolean shouldUseCache() + { + return super.shouldUseCache(); // || (painters != null && painters.length > 1); + } +} diff --git a/src/org/jdesktop/swingx/painter/Painter.java b/src/org/jdesktop/swingx/painter/Painter.java new file mode 100644 index 00000000..1c9f69bd --- /dev/null +++ b/src/org/jdesktop/swingx/painter/Painter.java @@ -0,0 +1,104 @@ +/* + * $Id: Painter.java 3860 2010-10-26 01:14:53Z kschaefe $ + * + * Copyright 2006 Sun Microsystems, Inc., 4150 Network Circle, + * Santa Clara, California 95054, U.S.A. All rights reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + */ + +package org.jdesktop.swingx.painter; + +import java.awt.Graphics2D; + +/** + *

A painting delegate. The Painter interface defines exactly one method, + * paint. It is used in situations where the developer can change + * the painting routine of a component without having to resort to subclassing + * the component.

+ * + *

Painters are simply encapsulations of Java2D code and make + * it fairly trivial to reuse existing Painters or to combine + * them together. Implementations of this interface are also trivial to write, + * such that if you can't find a Painter that does what you need, + * you can write one with minimal effort. Writing a Painter requires + * knowledge of Java2D.

+ * + *

A Painter may be created with a type parameter. This type will be + * expected in the paint method. For example, you may wish to write a + * Painter that only works with subclasses of {@link java.awt.Component}. + * In that case, when the Painter is declared, you may declare that + * it requires a Component, allowing the paint method to be type safe. Ex: + *


+ *     Painter<Component> p = new Painter<Component>() {
+ *         public void paint(Graphics2D g, Component c, int width, int height) {
+ *             g.setColor(c.getBackground());
+ *             //and so forth
+ *         }
+ *     }
+ * 

+ * + *

This class is not threadsafe.

+ * + * @author rbair + * @param an optional configuration parameter + * @see AbstractPainter + * @see CompoundPainter + */ +public interface Painter { + /** + *

Renders to the given {@link java.awt.Graphics2D} object. Implementations + * of this method may modify state on the Graphics2D, and are not + * required to restore that state upon completion. In most cases, it is recommended + * that the caller pass in a scratch graphics object. The Graphics2D + * must never be null.

+ * + *

State on the graphics object may be honored by the paint method, + * but may not be. For instance, setting the antialiasing rendering hint on the + * graphics may or may not be respected by the Painter implementation.

+ * + *

The supplied object parameter acts as an optional configuration argument. + * For example, it could be of type Component. A Painter + * that expected it could then read state from that Component and + * use the state for painting. For example, an implementation may read the + * backgroundColor and use that.

+ * + *

Generally, to enhance reusability, most standard Painters ignore + * this parameter. They can thus be reused in any context. The object + * may be null. Implementations must not throw a NullPointerException if the object + * parameter is null.

+ * + *

Finally, the width and height arguments specify the + * width and height that the Painter should paint into. More + * specifically, the specified width and height instruct the painter that it should + * paint fully within this width and height. Any specified clip on the + * g param will further constrain the region.

+ * + *

For example, suppose I have a Painter implementation that draws + * a gradient. The gradient goes from white to black. It "stretches" to fill the + * painted region. Thus, if I use this Painter to paint a 500 x 500 + * region, the far left would be black, the far right would be white, and a smooth + * gradient would be painted between. I could then, without modification, reuse the + * Painter to paint a region that is 20x20 in size. This region would + * also be black on the left, white on the right, and a smooth gradient painted + * between.

+ * + * @param g The Graphics2D to render to. This must not be null. + * @param object an optional configuration parameter. This may be null. + * @param width width of the area to paint. + * @param height height of the area to paint. + */ + public void paint(Graphics2D g, T object, int width, int height); +} diff --git a/src/org/jdesktop/swingx/util/GraphicsUtilities.java b/src/org/jdesktop/swingx/util/GraphicsUtilities.java new file mode 100644 index 00000000..64a91062 --- /dev/null +++ b/src/org/jdesktop/swingx/util/GraphicsUtilities.java @@ -0,0 +1,871 @@ +/* + * $Id: GraphicsUtilities.java 4082 2011-11-15 18:39:43Z kschaefe $ + * + * Dual-licensed under LGPL (Sun and Romain Guy) and BSD (Romain Guy). + * + * Copyright 2005 Sun Microsystems, Inc., 4150 Network Circle, + * Santa Clara, California 95054, U.S.A. All rights reserved. + * + * Copyright (c) 2006 Romain Guy + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * 3. The name of the author may not be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR + * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, + * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF + * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package org.jdesktop.swingx.util; + +import java.awt.AlphaComposite; +import java.awt.Color; +import java.awt.Graphics; +import java.awt.Graphics2D; +import java.awt.GraphicsConfiguration; +import java.awt.GraphicsEnvironment; +import java.awt.Image; +import java.awt.Insets; +import java.awt.RenderingHints; +import java.awt.Shape; +import java.awt.Transparency; +import java.awt.image.BufferedImage; +import java.awt.image.ColorModel; +import java.awt.image.Raster; +import java.awt.image.WritableRaster; +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; + +import javax.imageio.ImageIO; +import javax.swing.JComponent; + +/** + *

GraphicsUtilities contains a set of tools to perform + * common graphics operations easily. These operations are divided into + * several themes, listed below.

+ * + *

Compatible Images

+ * + *

Compatible images can, and should, be used to increase drawing + * performance. This class provides a number of methods to load compatible + * images directly from files or to convert existing images to compatibles + * images.

+ * + *

Creating Thumbnails

+ * + *

This class provides a number of methods to easily scale down images. + * Some of these methods offer a trade-off between speed and result quality and + * should be used all the time. They also offer the advantage of producing + * compatible images, thus automatically resulting into better runtime + * performance.

+ * + *

All these methods are both faster than + * {@link java.awt.Image#getScaledInstance(int, int, int)} and produce + * better-looking results than the various drawImage() methods + * in {@link java.awt.Graphics}, which can be used for image scaling.

+ * + *

Image Manipulation

+ * + *

This class provides two methods to get and set pixels in a buffered image. + * These methods try to avoid unmanaging the image in order to keep good + * performance.

+ * + * @author Romain Guy + * @author rbair + * @author Karl Schaefer + */ +@SuppressWarnings("nls") +public class GraphicsUtilities { + private GraphicsUtilities() { + } + + // Returns the graphics configuration for the primary screen + private static GraphicsConfiguration getGraphicsConfiguration() { + return GraphicsEnvironment.getLocalGraphicsEnvironment(). + getDefaultScreenDevice().getDefaultConfiguration(); + } + + private static boolean isHeadless() { + return GraphicsEnvironment.isHeadless(); + } + + /** + * Converts the specified image into a compatible buffered image. + * + * @param img + * the image to convert + * @return a compatible buffered image of the input + */ + public static BufferedImage convertToBufferedImage(Image img) { + BufferedImage buff = createCompatibleTranslucentImage( + img.getWidth(null), img.getHeight(null)); + Graphics2D g2 = buff.createGraphics(); + + try { + g2.drawImage(img, 0, 0, null); + } finally { + g2.dispose(); + } + + return buff; + } + + /** + *

Returns a new BufferedImage using the same color model + * as the image passed as a parameter. The returned image is only compatible + * with the image passed as a parameter. This does not mean the returned + * image is compatible with the hardware.

+ * + * @param image the reference image from which the color model of the new + * image is obtained + * @return a new BufferedImage, compatible with the color model + * of image + */ + public static BufferedImage createColorModelCompatibleImage(BufferedImage image) { + ColorModel cm = image.getColorModel(); + return new BufferedImage(cm, + cm.createCompatibleWritableRaster(image.getWidth(), + image.getHeight()), + cm.isAlphaPremultiplied(), null); + } + + /** + *

Returns a new compatible image with the same width, height and + * transparency as the image specified as a parameter. That is, the + * returned BufferedImage will be compatible with the graphics hardware. + * If this method is called in a headless environment, then + * the returned BufferedImage will be compatible with the source + * image.

+ * + * @see java.awt.Transparency + * @see #createCompatibleImage(int, int) + * @see #createCompatibleImage(java.awt.image.BufferedImage, int, int) + * @see #createCompatibleTranslucentImage(int, int) + * @see #loadCompatibleImage(java.net.URL) + * @see #toCompatibleImage(java.awt.image.BufferedImage) + * @param image the reference image from which the dimension and the + * transparency of the new image are obtained + * @return a new compatible BufferedImage with the same + * dimension and transparency as image + */ + public static BufferedImage createCompatibleImage(BufferedImage image) { + return createCompatibleImage(image, image.getWidth(), image.getHeight()); + } + + /** + *

Returns a new compatible image of the specified width and height, and + * the same transparency setting as the image specified as a parameter. + * That is, the returned BufferedImage is compatible with + * the graphics hardware. If the method is called in a headless + * environment, then the returned BufferedImage will be compatible with + * the source image.

+ * + * @see java.awt.Transparency + * @see #createCompatibleImage(java.awt.image.BufferedImage) + * @see #createCompatibleImage(int, int) + * @see #createCompatibleTranslucentImage(int, int) + * @see #loadCompatibleImage(java.net.URL) + * @see #toCompatibleImage(java.awt.image.BufferedImage) + * @param width the width of the new image + * @param height the height of the new image + * @param image the reference image from which the transparency of the new + * image is obtained + * @return a new compatible BufferedImage with the same + * transparency as image and the specified dimension + */ + public static BufferedImage createCompatibleImage(BufferedImage image, + int width, int height) { + return isHeadless() ? + new BufferedImage(width, height, image.getType()) : + getGraphicsConfiguration().createCompatibleImage(width, height, + image.getTransparency()); + } + + /** + *

Returns a new opaque compatible image of the specified width and + * height. That is, the returned BufferedImage is compatible with + * the graphics hardware. If the method is called in a headless + * environment, then the returned BufferedImage will be compatible with + * the source image.

+ * + * @see #createCompatibleImage(java.awt.image.BufferedImage) + * @see #createCompatibleImage(java.awt.image.BufferedImage, int, int) + * @see #createCompatibleTranslucentImage(int, int) + * @see #loadCompatibleImage(java.net.URL) + * @see #toCompatibleImage(java.awt.image.BufferedImage) + * @param width the width of the new image + * @param height the height of the new image + * @return a new opaque compatible BufferedImage of the + * specified width and height + */ + public static BufferedImage createCompatibleImage(int width, int height) { + return isHeadless() ? + new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB) : + getGraphicsConfiguration().createCompatibleImage(width, height); + } + + /** + *

Returns a new translucent compatible image of the specified width and + * height. That is, the returned BufferedImage is compatible with + * the graphics hardware. If the method is called in a headless + * environment, then the returned BufferedImage will be compatible with + * the source image.

+ * + * @see #createCompatibleImage(java.awt.image.BufferedImage) + * @see #createCompatibleImage(java.awt.image.BufferedImage, int, int) + * @see #createCompatibleImage(int, int) + * @see #loadCompatibleImage(java.net.URL) + * @see #toCompatibleImage(java.awt.image.BufferedImage) + * @param width the width of the new image + * @param height the height of the new image + * @return a new translucent compatible BufferedImage of the + * specified width and height + */ + public static BufferedImage createCompatibleTranslucentImage(int width, + int height) { + return isHeadless() ? + new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB) : + getGraphicsConfiguration().createCompatibleImage(width, height, + Transparency.TRANSLUCENT); + } + + /** + *

+ * Returns a new compatible image from a stream. The image is loaded from + * the specified stream and then turned, if necessary into a compatible + * image. + *

+ * + * @see #createCompatibleImage(java.awt.image.BufferedImage) + * @see #createCompatibleImage(java.awt.image.BufferedImage, int, int) + * @see #createCompatibleImage(int, int) + * @see #createCompatibleTranslucentImage(int, int) + * @see #toCompatibleImage(java.awt.image.BufferedImage) + * @param in + * the stream of the picture to load as a compatible image + * @return a new translucent compatible BufferedImage of the + * specified width and height + * @throws java.io.IOException + * if the image cannot be read or loaded + */ + public static BufferedImage loadCompatibleImage(InputStream in) throws IOException { + BufferedImage image = ImageIO.read(in); + if(image == null) return null; + return toCompatibleImage(image); + } + + /** + *

Returns a new compatible image from a URL. The image is loaded from the + * specified location and then turned, if necessary into a compatible + * image.

+ * + * @see #createCompatibleImage(java.awt.image.BufferedImage) + * @see #createCompatibleImage(java.awt.image.BufferedImage, int, int) + * @see #createCompatibleImage(int, int) + * @see #createCompatibleTranslucentImage(int, int) + * @see #toCompatibleImage(java.awt.image.BufferedImage) + * @param resource the URL of the picture to load as a compatible image + * @return a new translucent compatible BufferedImage of the + * specified width and height + * @throws java.io.IOException if the image cannot be read or loaded + */ + public static BufferedImage loadCompatibleImage(URL resource) + throws IOException { + BufferedImage image = ImageIO.read(resource); + return toCompatibleImage(image); + } + + /** + *

Return a new compatible image that contains a copy of the specified + * image. This method ensures an image is compatible with the hardware, + * and therefore optimized for fast blitting operations.

+ * + *

If the method is called in a headless environment, then the returned + * BufferedImage will be the source image.

+ * + * @see #createCompatibleImage(java.awt.image.BufferedImage) + * @see #createCompatibleImage(java.awt.image.BufferedImage, int, int) + * @see #createCompatibleImage(int, int) + * @see #createCompatibleTranslucentImage(int, int) + * @see #loadCompatibleImage(java.net.URL) + * @param image the image to copy into a new compatible image + * @return a new compatible copy, with the + * same width and height and transparency and content, of image + */ + public static BufferedImage toCompatibleImage(BufferedImage image) { + if (isHeadless()) { + return image; + } + + if (image.getColorModel().equals( + getGraphicsConfiguration().getColorModel())) { + return image; + } + + BufferedImage compatibleImage = + getGraphicsConfiguration().createCompatibleImage( + image.getWidth(), image.getHeight(), + image.getTransparency()); + Graphics g = compatibleImage.getGraphics(); + + try { + g.drawImage(image, 0, 0, null); + } finally { + g.dispose(); + } + + return compatibleImage; + } + + /** + *

Returns a thumbnail of a source image. newSize defines + * the length of the longest dimension of the thumbnail. The other + * dimension is then computed according to the dimensions ratio of the + * original picture.

+ *

This method favors speed over quality. When the new size is less than + * half the longest dimension of the source image, + * {@link #createThumbnail(BufferedImage, int)} or + * {@link #createThumbnail(BufferedImage, int, int)} should be used instead + * to ensure the quality of the result without sacrificing too much + * performance.

+ * + * @see #createThumbnailFast(java.awt.image.BufferedImage, int, int) + * @see #createThumbnail(java.awt.image.BufferedImage, int) + * @see #createThumbnail(java.awt.image.BufferedImage, int, int) + * @param image the source image + * @param newSize the length of the largest dimension of the thumbnail + * @return a new compatible BufferedImage containing a + * thumbnail of image + * @throws IllegalArgumentException if newSize is larger than + * the largest dimension of image or <= 0 + */ + public static BufferedImage createThumbnailFast(BufferedImage image, + int newSize) { + float ratio; + int width = image.getWidth(); + int height = image.getHeight(); + + if (width > height) { + if (newSize >= width) { + throw new IllegalArgumentException("newSize must be lower than" + + " the image width"); + } else if (newSize <= 0) { + throw new IllegalArgumentException("newSize must" + + " be greater than 0"); + } + + ratio = (float) width / (float) height; + width = newSize; + height = (int) (newSize / ratio); + } else { + if (newSize >= height) { + throw new IllegalArgumentException("newSize must be lower than" + + " the image height"); + } else if (newSize <= 0) { + throw new IllegalArgumentException("newSize must" + + " be greater than 0"); + } + + ratio = (float) height / (float) width; + height = newSize; + width = (int) (newSize / ratio); + } + + BufferedImage temp = createCompatibleImage(image, width, height); + Graphics2D g2 = temp.createGraphics(); + + try { + g2.setRenderingHint(RenderingHints.KEY_INTERPOLATION, + RenderingHints.VALUE_INTERPOLATION_BILINEAR); + g2.drawImage(image, 0, 0, temp.getWidth(), temp.getHeight(), null); + } finally { + g2.dispose(); + } + + return temp; + } + + /** + *

Returns a thumbnail of a source image.

+ *

This method favors speed over quality. When the new size is less than + * half the longest dimension of the source image, + * {@link #createThumbnail(BufferedImage, int)} or + * {@link #createThumbnail(BufferedImage, int, int)} should be used instead + * to ensure the quality of the result without sacrificing too much + * performance.

+ * + * @see #createThumbnailFast(java.awt.image.BufferedImage, int) + * @see #createThumbnail(java.awt.image.BufferedImage, int) + * @see #createThumbnail(java.awt.image.BufferedImage, int, int) + * @param image the source image + * @param newWidth the width of the thumbnail + * @param newHeight the height of the thumbnail + * @return a new compatible BufferedImage containing a + * thumbnail of image + * @throws IllegalArgumentException if newWidth is larger than + * the width of image or if code>newHeight is larger + * than the height of image or if one of the dimensions + * is <= 0 + */ + public static BufferedImage createThumbnailFast(BufferedImage image, + int newWidth, int newHeight) { + if (newWidth >= image.getWidth() || + newHeight >= image.getHeight()) { + throw new IllegalArgumentException("newWidth and newHeight cannot" + + " be greater than the image" + + " dimensions"); + } else if (newWidth <= 0 || newHeight <= 0) { + throw new IllegalArgumentException("newWidth and newHeight must" + + " be greater than 0"); + } + + BufferedImage temp = createCompatibleImage(image, newWidth, newHeight); + Graphics2D g2 = temp.createGraphics(); + + try { + g2.setRenderingHint(RenderingHints.KEY_INTERPOLATION, + RenderingHints.VALUE_INTERPOLATION_BILINEAR); + g2.drawImage(image, 0, 0, temp.getWidth(), temp.getHeight(), null); + } finally { + g2.dispose(); + } + + return temp; + } + + /** + *

Returns a thumbnail of a source image. newSize defines + * the length of the longest dimension of the thumbnail. The other + * dimension is then computed according to the dimensions ratio of the + * original picture.

+ *

This method offers a good trade-off between speed and quality. + * The result looks better than + * {@link #createThumbnailFast(java.awt.image.BufferedImage, int)} when + * the new size is less than half the longest dimension of the source + * image, yet the rendering speed is almost similar.

+ * + * @see #createThumbnailFast(java.awt.image.BufferedImage, int, int) + * @see #createThumbnailFast(java.awt.image.BufferedImage, int) + * @see #createThumbnail(java.awt.image.BufferedImage, int, int) + * @param image the source image + * @param newSize the length of the largest dimension of the thumbnail + * @return a new compatible BufferedImage containing a + * thumbnail of image + * @throws IllegalArgumentException if newSize is larger than + * the largest dimension of image or <= 0 + */ + public static BufferedImage createThumbnail(BufferedImage image, + int newSize) { + int width = image.getWidth(); + int height = image.getHeight(); + + boolean isTranslucent = image.getTransparency() != Transparency.OPAQUE; + boolean isWidthGreater = width > height; + + if (isWidthGreater) { + if (newSize >= width) { + throw new IllegalArgumentException("newSize must be lower than" + + " the image width"); + } + } else if (newSize >= height) { + throw new IllegalArgumentException("newSize must be lower than" + + " the image height"); + } + + if (newSize <= 0) { + throw new IllegalArgumentException("newSize must" + + " be greater than 0"); + } + + float ratioWH = (float) width / (float) height; + float ratioHW = (float) height / (float) width; + + BufferedImage thumb = image; + BufferedImage temp = null; + + Graphics2D g2 = null; + + try { + int previousWidth = width; + int previousHeight = height; + + do { + if (isWidthGreater) { + width /= 2; + if (width < newSize) { + width = newSize; + } + height = (int) (width / ratioWH); + } else { + height /= 2; + if (height < newSize) { + height = newSize; + } + width = (int) (height / ratioHW); + } + + if (temp == null || isTranslucent) { + if (g2 != null) { + //do not need to wrap with finally + //outer finally block will ensure + //that resources are properly reclaimed + g2.dispose(); + } + temp = createCompatibleImage(image, width, height); + g2 = temp.createGraphics(); + g2.setRenderingHint(RenderingHints.KEY_INTERPOLATION, + RenderingHints.VALUE_INTERPOLATION_BILINEAR); + } + if (g2 != null) // always the case + g2.drawImage(thumb, 0, 0, width, height, + 0, 0, previousWidth, previousHeight, null); + + previousWidth = width; + previousHeight = height; + + thumb = temp; + } while (newSize != (isWidthGreater ? width : height)); + } finally { + if (g2 != null) { + g2.dispose(); + } + } + + if (width != thumb.getWidth() || height != thumb.getHeight()) { + temp = createCompatibleImage(image, width, height); + g2 = temp.createGraphics(); + + try { + g2.setRenderingHint(RenderingHints.KEY_INTERPOLATION, + RenderingHints.VALUE_INTERPOLATION_BILINEAR); + g2.drawImage(thumb, 0, 0, width, height, 0, 0, width, height, null); + } finally { + g2.dispose(); + } + + thumb = temp; + } + + return thumb; + } + + /** + *

Returns a thumbnail of a source image.

+ *

This method offers a good trade-off between speed and quality. + * The result looks better than + * {@link #createThumbnailFast(java.awt.image.BufferedImage, int)} when + * the new size is less than half the longest dimension of the source + * image, yet the rendering speed is almost similar.

+ * + * @see #createThumbnailFast(java.awt.image.BufferedImage, int) + * @see #createThumbnailFast(java.awt.image.BufferedImage, int, int) + * @see #createThumbnail(java.awt.image.BufferedImage, int) + * @param image the source image + * @param newWidth the width of the thumbnail + * @param newHeight the height of the thumbnail + * @return a new compatible BufferedImage containing a + * thumbnail of image + * @throws IllegalArgumentException if newWidth is larger than + * the width of image or if code>newHeight is larger + * than the height of image or if one the dimensions is not > 0 + */ + public static BufferedImage createThumbnail(BufferedImage image, + int newWidth, int newHeight) { + int width = image.getWidth(); + int height = image.getHeight(); + + boolean isTranslucent = image.getTransparency() != Transparency.OPAQUE; + + if (newWidth >= width || newHeight >= height) { + throw new IllegalArgumentException("newWidth and newHeight cannot" + + " be greater than the image" + + " dimensions"); + } else if (newWidth <= 0 || newHeight <= 0) { + throw new IllegalArgumentException("newWidth and newHeight must" + + " be greater than 0"); + } + + BufferedImage thumb = image; + BufferedImage temp = null; + + Graphics2D g2 = null; + + try { + int previousWidth = width; + int previousHeight = height; + + do { + if (width > newWidth) { + width /= 2; + if (width < newWidth) { + width = newWidth; + } + } + + if (height > newHeight) { + height /= 2; + if (height < newHeight) { + height = newHeight; + } + } + + if (temp == null || isTranslucent) { + if (g2 != null) { + //do not need to wrap with finally + //outer finally block will ensure + //that resources are properly reclaimed + g2.dispose(); + } + temp = createCompatibleImage(image, width, height); + g2 = temp.createGraphics(); + g2.setRenderingHint(RenderingHints.KEY_INTERPOLATION, + RenderingHints.VALUE_INTERPOLATION_BILINEAR); + } + if (g2 != null) // always the case + g2.drawImage(thumb, 0, 0, width, height, + 0, 0, previousWidth, previousHeight, null); + + previousWidth = width; + previousHeight = height; + + thumb = temp; + } while (width != newWidth || height != newHeight); + } finally { + if (g2 != null) { + g2.dispose(); + } + } + + if (width != thumb.getWidth() || height != thumb.getHeight()) { + temp = createCompatibleImage(image, width, height); + g2 = temp.createGraphics(); + + try { + g2.setRenderingHint(RenderingHints.KEY_INTERPOLATION, + RenderingHints.VALUE_INTERPOLATION_BILINEAR); + g2.drawImage(thumb, 0, 0, width, height, 0, 0, width, height, null); + } finally { + g2.dispose(); + } + + thumb = temp; + } + + return thumb; + } + + /** + *

Returns an array of pixels, stored as integers, from a + * BufferedImage. The pixels are grabbed from a rectangular + * area defined by a location and two dimensions. Calling this method on + * an image of type different from BufferedImage.TYPE_INT_ARGB + * and BufferedImage.TYPE_INT_RGB will unmanage the image.

+ * + * @param img the source image + * @param x the x location at which to start grabbing pixels + * @param y the y location at which to start grabbing pixels + * @param w the width of the rectangle of pixels to grab + * @param h the height of the rectangle of pixels to grab + * @param pixels a pre-allocated array of pixels of size w*h; can be null + * @return pixels if non-null, a new array of integers + * otherwise + * @throws IllegalArgumentException is pixels is non-null and + * of length < w*h + */ + public static int[] getPixels(BufferedImage img, + int x, int y, int w, int h, int[] pixels) { + if (w == 0 || h == 0) { + return new int[0]; + } + + if (pixels == null) { + pixels = new int[w * h]; + } else if (pixels.length < w * h) { + throw new IllegalArgumentException("pixels array must have a length" + + " >= w*h"); + } + + int imageType = img.getType(); + if (imageType == BufferedImage.TYPE_INT_ARGB || + imageType == BufferedImage.TYPE_INT_RGB) { + Raster raster = img.getRaster(); + return (int[]) raster.getDataElements(x, y, w, h, pixels); + } + + // Unmanages the image + return img.getRGB(x, y, w, h, pixels, 0, w); + } + + /** + *

Writes a rectangular area of pixels in the destination + * BufferedImage. Calling this method on + * an image of type different from BufferedImage.TYPE_INT_ARGB + * and BufferedImage.TYPE_INT_RGB will unmanage the image.

+ * + * @param img the destination image + * @param x the x location at which to start storing pixels + * @param y the y location at which to start storing pixels + * @param w the width of the rectangle of pixels to store + * @param h the height of the rectangle of pixels to store + * @param pixels an array of pixels, stored as integers + * @throws IllegalArgumentException is pixels is non-null and + * of length < w*h + */ + public static void setPixels(BufferedImage img, + int x, int y, int w, int h, int[] pixels) { + if (pixels == null || w == 0 || h == 0) { + return; + } else if (pixels.length < w * h) { + throw new IllegalArgumentException("pixels array must have a length" + + " >= w*h"); + } + + int imageType = img.getType(); + if (imageType == BufferedImage.TYPE_INT_ARGB || + imageType == BufferedImage.TYPE_INT_RGB) { + WritableRaster raster = img.getRaster(); + raster.setDataElements(x, y, w, h, pixels); + } else { + // Unmanages the image + img.setRGB(x, y, w, h, pixels, 0, w); + } + } + + /** + * Clears the data from the image. + * + * @param img + * the image to erase + */ + public static void clear(Image img) { + Graphics g = img.getGraphics(); + + try { + if (g instanceof Graphics2D) { + ((Graphics2D) g).setComposite(AlphaComposite.Clear); + } else { + g.setColor(new Color(0, 0, 0, 0)); + } + + g.fillRect(0, 0, img.getWidth(null), img.getHeight(null)); + } finally { + g.dispose(); + } + } + + /** + * Sets the clip on a graphics object by merging a supplied clip with the existing one. The new + * clip will be an intersection of the old clip and the supplied clip. The old clip shape will + * be returned. This is useful for resetting the old clip after an operation is performed. + * + * @param g + * the graphics object to update + * @param clip + * a new clipping region to add to the graphics clip. + * @return the current clipping region of the supplied graphics object. This may return {@code + * null} if the current clip is {@code null}. + * @throws NullPointerException + * if any parameter is {@code null} + * @deprecated Use {@link ShapeUtils#mergeClip(Graphics,Shape)} instead + */ + @Deprecated + public static Shape mergeClip(Graphics g, Shape clip) { + return ShapeUtils.mergeClip(g, clip); + } + + /** + * Draws an image on top of a component by doing a 3x3 grid stretch of the image + * using the specified insets. + * @param g the graphics object + * @param comp the component + * @param img the image + * @param ins the insets + */ + public static void tileStretchPaint(Graphics g, + JComponent comp, + BufferedImage img, + Insets ins) { + + int left = ins.left; + int right = ins.right; + int top = ins.top; + int bottom = ins.bottom; + + // top + g.drawImage(img, + 0,0,left,top, + 0,0,left,top, + null); + g.drawImage(img, + left, 0, + comp.getWidth() - right, top, + left, 0, + img.getWidth() - right, top, + null); + g.drawImage(img, + comp.getWidth() - right, 0, + comp.getWidth(), top, + img.getWidth() - right, 0, + img.getWidth(), top, + null); + + // middle + g.drawImage(img, + 0, top, + left, comp.getHeight()-bottom, + 0, top, + left, img.getHeight()-bottom, + null); + + g.drawImage(img, + left, top, + comp.getWidth()-right, comp.getHeight()-bottom, + left, top, + img.getWidth()-right, img.getHeight()-bottom, + null); + + g.drawImage(img, + comp.getWidth()-right, top, + comp.getWidth(), comp.getHeight()-bottom, + img.getWidth()-right, top, + img.getWidth(), img.getHeight()-bottom, + null); + + // bottom + g.drawImage(img, + 0,comp.getHeight()-bottom, + left, comp.getHeight(), + 0,img.getHeight()-bottom, + left,img.getHeight(), + null); + g.drawImage(img, + left, comp.getHeight()-bottom, + comp.getWidth()-right, comp.getHeight(), + left, img.getHeight()-bottom, + img.getWidth()-right, img.getHeight(), + null); + g.drawImage(img, + comp.getWidth()-right, comp.getHeight()-bottom, + comp.getWidth(), comp.getHeight(), + img.getWidth()-right, img.getHeight()-bottom, + img.getWidth(), img.getHeight(), + null); + } +} diff --git a/src/org/jdesktop/swingx/util/ShapeUtils.java b/src/org/jdesktop/swingx/util/ShapeUtils.java new file mode 100644 index 00000000..45a79ab1 --- /dev/null +++ b/src/org/jdesktop/swingx/util/ShapeUtils.java @@ -0,0 +1,159 @@ +/* + * $Id: ShapeUtils.java 4082 2011-11-15 18:39:43Z kschaefe $ + * + * Copyright 2006 Sun Microsystems, Inc., 4150 Network Circle, + * Santa Clara, California 95054, U.S.A. All rights reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + */ +package org.jdesktop.swingx.util; + +import java.awt.Font; +import java.awt.Graphics; +import java.awt.Graphics2D; +import java.awt.Polygon; +import java.awt.Shape; +import java.awt.font.GlyphVector; +import java.awt.geom.AffineTransform; +import java.awt.geom.Area; +import java.awt.geom.Ellipse2D; +import java.awt.geom.GeneralPath; +import java.awt.geom.Point2D; +import java.awt.geom.Rectangle2D; +import java.awt.image.BufferedImage; + +/** + * + * @author joshy + */ +public final class ShapeUtils { + + /** Creates a new instance of ShapeUtils */ + private ShapeUtils() { + } + + /** + * Generates a polygon with an inside radius of 0. + * @param sides number of sides + * @param outsideRadius the outside radius + * @param normalize normalize + * @return the generated shape + */ + public static Shape generatePolygon(int sides, int outsideRadius, boolean normalize) { + return generatePolygon(sides, outsideRadius, 0, normalize); + } + + /** + * Generates a polygon + * @param sides number of sides + * @param outsideRadius the outside radius + * @param insideRadius the inside radius + * @param normalize normalize + * @return the generated shape + */ + public static Shape generatePolygon(int sides, int outsideRadius, int insideRadius, + boolean normalize) { + Shape shape = generatePolygon(sides, outsideRadius, insideRadius); + if (normalize) { + Rectangle2D bounds = shape.getBounds2D(); + GeneralPath path = new GeneralPath(shape); + shape = path.createTransformedShape(AffineTransform.getTranslateInstance( + -bounds.getX(), -bounds.getY())); + } + return shape; + } + + /** + * Generates a polygon + * @param sides number of sides + * @param outsideRadius the outside radius + * @param insideRadius the inside radius + * @return the generated shape + */ + public static Shape generatePolygon(int sides, int outsideRadius, int insideRadius) { + if (sides < 3) { + return new Ellipse2D.Float(0, 0, 10, 10); + } + + AffineTransform trans = new AffineTransform(); + Polygon poly = new Polygon(); + for (int i = 0; i < sides; i++) { + trans.rotate(Math.PI * 2 / sides / 2); + Point2D out = trans.transform(new Point2D.Float(0, outsideRadius), null); + poly.addPoint((int) out.getX(), (int) out.getY()); + trans.rotate(Math.PI * 2 / sides / 2); + if (insideRadius > 0) { + Point2D in = trans.transform(new Point2D.Float(0, insideRadius), null); + poly.addPoint((int) in.getX(), (int) in.getY()); + } + } + + return poly; + } + + /** + * @param font the font + * @param ch a single character + * @return the shape + */ + public static Shape generateShapeFromText(Font font, char ch) { + return generateShapeFromText(font, String.valueOf(ch)); + } + + /** + * @param font the font + * @param string the text string + * @return the shape + */ + public static Shape generateShapeFromText(Font font, String string) { + BufferedImage img = GraphicsUtilities.createCompatibleTranslucentImage(1, 1); + Graphics2D g2 = img.createGraphics(); + + try { + GlyphVector vect = font.createGlyphVector(g2.getFontRenderContext(), string); + Shape shape = vect.getOutline(0f, (float) -vect.getVisualBounds().getY()); + + return shape; + } finally { + g2.dispose(); + } + } + + /** + * Sets the clip on a graphics object by merging a supplied clip with the existing one. The new + * clip will be an intersection of the old clip and the supplied clip. The old clip shape will + * be returned. This is useful for resetting the old clip after an operation is performed. + * + * @param g + * the graphics object to update + * @param clip + * a new clipping region to add to the graphics clip. + * @return the current clipping region of the supplied graphics object. This may return + * {@code null} if the current clip is {@code null}. + * @throws NullPointerException + * if any parameter is {@code null} + */ + public static Shape mergeClip(Graphics g, Shape clip) { + Shape oldClip = g.getClip(); + if (oldClip == null) { + g.setClip(clip); + return null; + } + Area area = new Area(oldClip); + area.intersect(new Area(clip));// new Rectangle(0,0,width,height))); + g.setClip(area); + return oldClip; + } +} diff --git a/src/playlist/Playlist.java b/src/playlist/Playlist.java new file mode 100644 index 00000000..c44214d7 --- /dev/null +++ b/src/playlist/Playlist.java @@ -0,0 +1,77 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package playlist; + +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlRootElement; +import javax.xml.bind.annotation.XmlSeeAlso; + +import alias.AliasDirectory; +import controller.channel.ChannelMapList; +import controller.system.SystemList; + +@XmlSeeAlso( { AliasDirectory.class, + ChannelMapList.class, + SystemList.class } ) + +@XmlRootElement( name = "playlist" ) +public class Playlist +{ + private AliasDirectory mAliasDirectory = new AliasDirectory(); + private ChannelMapList mChannelMapList = new ChannelMapList(); + private SystemList mSystemList = new SystemList(); + + public Playlist() + { + } + + @XmlElement( name = "alias_directory" ) + public AliasDirectory getAliasDirectory() + { + mAliasDirectory.refresh(); + + return mAliasDirectory; + } + + public void setAliasDirectory( AliasDirectory directory ) + { + mAliasDirectory = directory; + } + + @XmlElement( name = "system_list" ) + public SystemList getSystemList() + { + return mSystemList; + } + + public void setSystemList( SystemList list ) + { + mSystemList = list; + } + + @XmlElement( name = "channel_maps" ) + public ChannelMapList getChannelMapList() + { + return mChannelMapList; + } + + public void setChannelMapList( ChannelMapList list ) + { + mChannelMapList = list; + } +} diff --git a/src/playlist/PlaylistManager.java b/src/playlist/PlaylistManager.java new file mode 100644 index 00000000..60366013 --- /dev/null +++ b/src/playlist/PlaylistManager.java @@ -0,0 +1,214 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package playlist; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.file.Files; +import java.nio.file.Path; + +import javax.xml.bind.JAXBContext; +import javax.xml.bind.JAXBException; +import javax.xml.bind.Marshaller; +import javax.xml.bind.Unmarshaller; + +import log.Log; +import properties.SystemProperties; + +public class PlaylistManager +{ + private Playlist mPlaylist = new Playlist(); + + public PlaylistManager() + { + init(); + } + + public Playlist getPlayist() + { + return mPlaylist; + } + + /** + * Loads playlist from the current playlist file, or the default playlist file, + * as specified in the current SDRTRunk system settings + */ + private void init() + { + SystemProperties props = SystemProperties.getInstance(); + + Path playlistFolder = props.getApplicationFolder( "playlist" ); + + String defaultPlaylistFile = + props.get( "playlist.defaultfilename", "playlist.xml" ); + + String playlistFile = + props.get( "playlist.currentfilename", defaultPlaylistFile ); + + load( playlistFolder.resolve( playlistFile ) ); + } + + + public void save() + { + JAXBContext context = null; + + SystemProperties props = SystemProperties.getInstance(); + + Path playlistPath = props.getApplicationFolder( "playlist" ); + + String playlistDefault = props.get( "playlist.defaultfilename", + "playlist.xml" ); + + String playlistCurrent = props.get( "playlist.currentfilename", + playlistDefault ); + + Path filePath = playlistPath.resolve( playlistCurrent ); + + File outputFile = new File( filePath.toString() ); + + try + { + if( !outputFile.exists() ) + { + outputFile.createNewFile(); + } + } + catch( Exception e ) + { + Log.error( "PlaylistManager - couldn't create file to save playlist [" + filePath.toString() + "]" ); + } + + OutputStream out = null; + + try + { + out = new FileOutputStream( outputFile ); + + try + { + context = JAXBContext.newInstance( Playlist.class ); + + Marshaller m = context.createMarshaller(); + + m.setProperty( Marshaller.JAXB_FORMATTED_OUTPUT, true ); + + m.marshal( mPlaylist, out ); + } + catch ( JAXBException e ) + { + Log.error( "PlaylistManager - jaxb exception while saving " + + "playlist: " + e.getLocalizedMessage() ); + e.printStackTrace(); + } + } + catch ( Exception e ) + { + Log.error( "PlaylistManager - coulcn't open outputstream to " + + "save playlist [" + filePath.toString() + "]" ); + } + finally + { + if( out != null ) + { + try + { + out.close(); + } + catch ( IOException e ) + { + e.printStackTrace(); + } + } + } + } + + /** + * Erases current playlist and loads playlist from the playlistPath filename, + * if it exists. + */ + public void load( Path playlistPath ) + { + if( Files.exists( playlistPath ) ) + { + Log.info( "PlaylistManager - loading playlist file [" + + playlistPath.toString() + "]" ); + + JAXBContext context = null; + + InputStream in = null; + + try + { + in = new FileInputStream( playlistPath.toString() ); + + try + { + context = JAXBContext.newInstance( Playlist.class ); + + Unmarshaller m = context.createUnmarshaller(); + + mPlaylist = (Playlist)m.unmarshal( in ); + } + catch ( JAXBException e ) + { + Log.error( "PlaylistManager - jaxb exception while loading " + + "playlist: " + e.getLocalizedMessage() ); + + e.printStackTrace(); + } + } + catch ( Exception e ) + { + e.printStackTrace(); + Log.error( "PlaylistManager - coulcn't open inputstream to " + + "load playlist [" + playlistPath.toString() + "]" ); + } + finally + { + if( in != null ) + { + try + { + in.close(); + } + catch ( IOException e ) + { + Log.error( "PlaylistManager - exception while closing " + + "the playlist file inputstream reader - " + + e.getLocalizedMessage() ); + } + } + } + } + else + { + Log.info( "PlaylistManager - playlist does not exist [" + + playlistPath.toString() + "]" ); + } + + if( mPlaylist == null ) + { + mPlaylist = new Playlist(); + } + } +} diff --git a/src/playlist/PlaylistNode.java b/src/playlist/PlaylistNode.java new file mode 100644 index 00000000..1f740a57 --- /dev/null +++ b/src/playlist/PlaylistNode.java @@ -0,0 +1,147 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package playlist; + +import javax.swing.JPanel; + +import alias.AliasDirectory; +import alias.AliasDirectoryNode; +import controller.BaseNode; +import controller.EmptyEditor; +import controller.channel.ChannelMapList; +import controller.channel.ChannelMapListNode; +import controller.system.SystemList; +import controller.system.SystemListNode; + +public class PlaylistNode extends BaseNode +{ + private static final long serialVersionUID = 1L; + private AliasDirectoryNode mAliasDirectoryNode; + private ChannelMapListNode mChannelMapListNode; + private SystemListNode mSystemListNode; + + public PlaylistNode() + { + super( null ); + } + + private void init() + { + /** + * Delete any existing children + */ + delete(); + + /** + * Load the AliasDirectory + */ + AliasDirectory directory = getPlaylist().getAliasDirectory(); + + if( directory == null ) + { + directory = new AliasDirectory(); + getPlaylist().setAliasDirectory( directory ); + } + + mAliasDirectoryNode = new AliasDirectoryNode( directory ); + + getModel().insertNodeInto( mAliasDirectoryNode, this, 0 ); + + mAliasDirectoryNode.init(); + + /** + * Load the Channel Map List + */ + ChannelMapList channelMapList = getPlaylist().getChannelMapList(); + + if( channelMapList == null ) + { + channelMapList = new ChannelMapList(); + } + + mChannelMapListNode = new ChannelMapListNode( channelMapList ); + + getModel().insertNodeInto( mChannelMapListNode, this, 1 ); + + mChannelMapListNode.init(); + + /** + * Load the SystemList + */ + SystemList list = getPlaylist().getSystemList(); + + if( list == null ) + { + list = new SystemList(); + getPlaylist().setSystemList( list ); + } + + mSystemListNode = new SystemListNode( list ); + + getModel().insertNodeInto( mSystemListNode, this, 2 ); + + mSystemListNode.init(); + + mSystemListNode.show(); + } + + public SystemListNode getSystemListNode() + { + return mSystemListNode; + } + + public AliasDirectoryNode getAliasDirectoryNode() + { + return mAliasDirectoryNode; + } + + public void loadPlaylist() + { + Playlist playlist = + getModel().getResourceManager().getPlaylistManager().getPlayist(); + + setUserObject( playlist ); + + init(); + } + + + public Playlist getPlaylist() + { + Playlist retVal = null; + + Object userObject = getUserObject(); + + if( userObject != null ) + { + retVal = (Playlist)userObject; + } + + return retVal; + } + + public String toString() + { + return "Playlist"; + } + + public JPanel getEditor() + { + return new EmptyEditor(); + } +} diff --git a/src/properties/SystemProperties.java b/src/properties/SystemProperties.java new file mode 100644 index 00000000..1bbc43dc --- /dev/null +++ b/src/properties/SystemProperties.java @@ -0,0 +1,307 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package properties; + +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Properties; + +import log.Log; + +/** + * SystemProperties - provides an isolated instance of properties for the + * application + */ + +public class SystemProperties +{ + private static String sDEFAULT_APP_ROOT = "SDRTrunk"; + private static String sPROPERTIES_FILENAME = "SDRTrunk.properties"; + + private static SystemProperties mInstance; + private static Properties mProperties; + private Path mPropertiesPath; + + + private SystemProperties() + { + mProperties = new Properties(); + } + + /** + * Returns a SINGLETON instance of the application properties set + * @return + */ + public static SystemProperties getInstance() + { + if( mInstance == null ) + { + mInstance = new SystemProperties(); + } + + return mInstance; + } + + /** + * Saves any currently changed settings to the application properties file + */ + public void save() + { + Path propsPath = + getApplicationRootPath().resolve( sPROPERTIES_FILENAME ); + + OutputStream out = null; + + try + { + out = new FileOutputStream( propsPath.toString() ); + + String comments = + "SDRTrunk - SDR Trunking Decoder Application Settings"; + + mProperties.store( out, comments ); + + Log.info( "SystemProperties - saved current properties [" + + propsPath.toString() + "]" ); + } + catch( Exception e ) + { + Log.error( "SystemProperties - exception while saving " + + "application properties:" + e.getLocalizedMessage() ); + } + finally + { + if( out != null ) + { + try + { + out.close(); + } + catch ( IOException e ) + { + } + } + } + } + + /** + * Application root directory. Normally returns "SDRTRunk" from the user's + * home directory, unless that has been changed to another location on the + * file system by the user. + */ + public Path getApplicationRootPath() + { + Path retVal = null; + + String root = get( "root.directory", sDEFAULT_APP_ROOT ); + + if( root.equalsIgnoreCase( sDEFAULT_APP_ROOT ) ) + { + retVal = Paths.get( + System.getProperty( "user.home" ), sDEFAULT_APP_ROOT ); + } + else + { + retVal = Paths.get( root ); + } + + return retVal; + } + + public Path getApplicationFolder( String folder ) + { + Path retVal = getApplicationRootPath().resolve( folder ); + + if( !Files.exists( retVal ) ) + { + try + { + Files.createDirectory( retVal ); + } + catch ( IOException e ) + { + Log.error( "SystemProperties - exception while creating " + + "app folder [" + folder + "]" ); + } + } + + return retVal; + } + + public void logCurrentSettings() + { + if( mPropertiesPath == null ) + { + Log.info( "SystemProperties - no properties file loaded - using defaults" ); + } + else + { + Log.info( "SystemProperties - application properties loaded [" + mPropertiesPath.toString() + "]" ); + } + } + + /** + * Loads the properties file into this properties set + * @param file + */ + public void load( Path propertiesPath ) + { + if( propertiesPath != null ) + { + mPropertiesPath = propertiesPath; + + InputStream in = null; + + try + { + in = new FileInputStream( propertiesPath.toString() ); + } + catch ( FileNotFoundException e ) + { + Log.error( "SDRTrunk - exception while opening inputstream on " + + "application properties file - " + e.getLocalizedMessage() ); + } + + if( in != null ) + { + try + { + mProperties.load( in ); + } + catch ( IOException e ) + { + Log.error( "SDRTrunk - exception while loading properties " + + "inputstream into SystemProperties - " + + e.getLocalizedMessage() ); + } + finally + { + try + { + in.close(); + } + catch ( IOException e ) + { + } + } + } + } + + Log.info( "SystemProperties - loaded [" + + propertiesPath.toString() + "]" ); + } + + /** + * Returns the value of the property, or null if the property doesn't exist + */ + private String get( String key ) + { + return mProperties.getProperty( key ); + } + + /** + * Returns the value of the property, or the defaultValue if the + * property doesn't exist + */ + public String get( String key, String defaultValue ) + { + String value = get( key ); + + if( value != null ) + { + return value; + } + + set( key, defaultValue ); + + return defaultValue; + } + + /** + * Returns the value of the property, or the defaultValue if the + * property doesn't exist + */ + public boolean get( String key, boolean defaultValue ) + { + String value = get( key ); + + if( value != null ) + { + try + { + boolean stored = Boolean.parseBoolean( value ); + + return stored; + } + catch( Exception e ) + { + //Do nothing, we couldn't parse the stored value + } + } + + set( key, String.valueOf( defaultValue ) ); + + return defaultValue; + } + + /** + * Returns the value of the property, or the defaultValue if the + * property doesn't exist + */ + public int get( String key, int defaultValue ) + { + String value = get( key ); + + if( value != null ) + { + try + { + int stored = Integer.parseInt( value ); + + return stored; + } + catch( Exception e ) + { + //Do nothing, we couldn't parse the stored value + } + } + + set( key, String.valueOf( defaultValue ) ); + + return defaultValue; + } + + /** + * Sets (overrides) the property key with the new value + */ + public void set( String key, String value ) + { + mProperties.setProperty( key, value ); + + save(); + } + + +} diff --git a/src/record/RecordComponentEditor.java b/src/record/RecordComponentEditor.java new file mode 100644 index 00000000..2cd44e14 --- /dev/null +++ b/src/record/RecordComponentEditor.java @@ -0,0 +1,83 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package record; + + +import javax.swing.BorderFactory; +import javax.swing.JCheckBox; + +import record.config.RecordConfiguration; +import controller.channel.AbstractChannelEditor; +import controller.channel.ChannelNode; + +public class RecordComponentEditor extends AbstractChannelEditor +{ + private static final long serialVersionUID = 1L; + + private JCheckBox mAudioRecorder = new JCheckBox( "Audio" ); + private JCheckBox mBasebandRecorder = new JCheckBox( "Baseband I/Q" ); + + public RecordComponentEditor( ChannelNode channelNode ) + { + super( channelNode ); + + add( mAudioRecorder, "span" ); + add( mBasebandRecorder, "span" ); + + reset(); + } + + @Override + public void save() + { + RecordConfiguration config = + getChannelNode().getChannel().getRecordConfiguration(); + + config.clearRecorders(); + + if( mAudioRecorder.isSelected() ) + { + config.addRecorder( RecorderType.AUDIO ); + } + + if( mBasebandRecorder.isSelected() ) + { + config.addRecorder( RecorderType.BASEBAND ); + } + } + + @Override + public void reset() + { + javax.swing.SwingUtilities.invokeLater(new Runnable() + { + @Override + public void run() + { + RecordConfiguration config = + getChannelNode().getChannel().getRecordConfiguration(); + + mAudioRecorder.setSelected( config.getRecorders() + .contains( RecorderType.AUDIO ) ); + + mBasebandRecorder.setSelected( config.getRecorders() + .contains( RecorderType.BASEBAND ) ); + } + }); + } +} diff --git a/src/record/RecordEditor.java b/src/record/RecordEditor.java new file mode 100644 index 00000000..ab416e0a --- /dev/null +++ b/src/record/RecordEditor.java @@ -0,0 +1,52 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package record; + +import record.config.RecordConfiguration; +import controller.Editor; + +public class RecordEditor extends Editor +{ + private static final long serialVersionUID = 1L; + + protected RecordConfiguration mConfig; + + public RecordEditor( RecordConfiguration config ) + { + mConfig = config; + } + + public RecordConfiguration getConfig() + { + return mConfig; + } + + @Override + public void save() + { + // TODO Auto-generated method stub + + } + + @Override + public void reset() + { + // TODO Auto-generated method stub + + } +} diff --git a/src/record/Recorder.java b/src/record/Recorder.java new file mode 100644 index 00000000..ba3d8c86 --- /dev/null +++ b/src/record/Recorder.java @@ -0,0 +1,60 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package record; + +import java.io.IOException; + +public class Recorder +{ + RecorderType mRecorderType; + + public Recorder() + { + } + + public String getFileName() + { + return "No Recording"; + } + + public void start() throws IOException + { + } + + public void stop() throws IOException + { + } + + public void pause() + { + } + + public void resume() + { + } + + public Recorder( RecorderType type ) + { + mRecorderType = type; + } + + public RecorderType getRecorderType() + { + return mRecorderType; + } +} diff --git a/src/record/RecorderManager.java b/src/record/RecorderManager.java new file mode 100644 index 00000000..388fd5b7 --- /dev/null +++ b/src/record/RecorderManager.java @@ -0,0 +1,74 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package record; + +import java.io.File; +import java.util.ArrayList; +import java.util.List; + +import properties.SystemProperties; +import record.config.RecordConfiguration; +import record.wave.ComplexWaveRecorder; +import record.wave.FloatWaveRecorder; +import util.TimeStamp; +import controller.channel.ProcessingChain; + +public class RecorderManager +{ + public static final int sSAMPLE_RATE = 48000; + + public RecorderManager() + { + } + + public List getRecorder( ProcessingChain channel ) + { + /* Note: the file suffix (ie .wav) gets added by the recorder */ + + //Get the base recording filename + StringBuilder sb = new StringBuilder(); + sb.append( SystemProperties.getInstance() + .getApplicationFolder( "recordings" ) ); + sb.append( File.separator ); + sb.append( TimeStamp.getTimeStamp( "_" ) ); + sb.append( "_" ); + sb.append( channel.getChannel().getName() ); + + ArrayList retVal = new ArrayList(); + + RecordConfiguration config = channel.getChannel().getRecordConfiguration(); + + for( RecorderType recorder: config.getRecorders() ) + { + switch( recorder ) + { + case AUDIO: + retVal.add( new FloatWaveRecorder( sSAMPLE_RATE, + sb.toString() + "_audio" ) ); + break; + case BASEBAND: + retVal.add( new ComplexWaveRecorder( sSAMPLE_RATE, + sb.toString() + "_baseband" ) ); + break; + } + } + + + return retVal; + } +} diff --git a/src/record/RecorderType.java b/src/record/RecorderType.java new file mode 100644 index 00000000..a055ea63 --- /dev/null +++ b/src/record/RecorderType.java @@ -0,0 +1,42 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package record; + +public enum RecorderType +{ + AUDIO( "Audio (.wav)"), + BASEBAND( "Baseband I/Q (.wav)"); + + private String mDisplayString; + + RecorderType( String displayString ) + { + mDisplayString = displayString; + } + + public String getDisplayString() + { + return mDisplayString; + } + + @Override + public String toString() + { + return mDisplayString; + } +} diff --git a/src/record/config/RecordConfiguration.java b/src/record/config/RecordConfiguration.java new file mode 100644 index 00000000..93c32cf9 --- /dev/null +++ b/src/record/config/RecordConfiguration.java @@ -0,0 +1,57 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package record.config; + +import java.util.ArrayList; + +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlRootElement; + +import record.RecorderType; +import controller.config.Configuration; + +@XmlRootElement( name = "record_configuration" ) +public class RecordConfiguration extends Configuration +{ + private ArrayList mRecorders = new ArrayList(); + + public RecordConfiguration() + { + } + + @XmlElement( name = "recorder" ) + public ArrayList getRecorders() + { + return mRecorders; + } + + public void setRecorders( ArrayList recorders ) + { + mRecorders = recorders; + } + + public void addRecorder( RecorderType recorder ) + { + mRecorders.add( recorder ); + } + + public void clearRecorders() + { + mRecorders.clear(); + } +} diff --git a/src/record/wave/BooleanWaveRecorder.java b/src/record/wave/BooleanWaveRecorder.java new file mode 100644 index 00000000..ae7ebc45 --- /dev/null +++ b/src/record/wave/BooleanWaveRecorder.java @@ -0,0 +1,173 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package record.wave; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.concurrent.LinkedBlockingQueue; + +import javax.sound.sampled.AudioFormat; + +import log.Log; +import record.Recorder; +import record.RecorderType; +import sample.Listener; +import util.waveaudio.WaveWriter; +import buffer.FloatSampleBufferAssembler; + +/** + * Threaded WAVE audio recorder for recording mono short samples to a wave file + */ +public class BooleanWaveRecorder extends Recorder implements Listener +{ + private AudioFormat mAudioFormat; + private boolean mRunning = false; + private boolean mPaused = false; + private WaveWriter mWriter; + private String mFileName; + private BufferProcessor mBufferProcessor; + + //Sample buffer queue ... constrained to 512 sample buffers (arbitrary size) + private LinkedBlockingQueue mToWriteBufferQueue = + new LinkedBlockingQueue( 512 ); + private FloatSampleBufferAssembler mCurrentBuffer; + + public BooleanWaveRecorder( int sampleRate, String filename ) + { + super( RecorderType.AUDIO ); + + mAudioFormat = new AudioFormat( sampleRate, //SampleRate + 16, //Sample Size + 1, //Channels + true, //Signed + false ); //Little Endian + + mCurrentBuffer = new FloatSampleBufferAssembler( sampleRate ); + + mFileName = filename; + } + + @Override + public String getFileName() + { + return mFileName + ".wav"; + } + + public void start() throws IOException + { + if( !mRunning ) + { + mWriter = new WaveWriter( mFileName + ".wav", mAudioFormat ); + mWriter.start(); + + mBufferProcessor = new BufferProcessor(); + mBufferProcessor.start(); + + mRunning = true; + } + } + + public void stop() throws IOException + { + if( mRunning ) + { + mBufferProcessor = null; + + mWriter.stop(); + mWriter = null; + + mRunning = false; + } + } + + public void pause() + { + mPaused = true; + } + + public void resume() + { + mPaused = false; + } + + /** + * Assumes that the byte[] represents samples corresponding to the + * audio format object defined at construction + */ + public void receive( ByteBuffer buffer ) + { + if( mRunning && !mPaused ) + { + boolean success = mToWriteBufferQueue.offer( buffer ); + + if( !success ) + { + Log.error( "Wave recorder buffer overflow - throwing away " + buffer.capacity() + " samples" ); + } + } + } + + @Override + public void receive( Boolean sample ) + { + /** + * If the sample is true, put short value of 5000, otherwise + * put a short value of -5000 + */ + mCurrentBuffer.put( sample ? 5000 : (short)-5000 ); + + if( !mCurrentBuffer.hasRemaining() ) + { + //Dispatch the buffer to be written to disk + if( !mToWriteBufferQueue.offer( mCurrentBuffer.get() ) ) + { + //We had an overflow ... log it + Log.error( "ShortWaveRecorder: buffer overflow - throwing away 1 second of sample data" ); + } + + mCurrentBuffer.reset(); + } + } + + public class BufferProcessor extends Thread + { + public void run() + { + while( true ) + { + try + { + if( mWriter != null ) + { + mWriter.write( mToWriteBufferQueue.take() ); + } + } + catch ( IOException ioe ) + { + Log.error( "IOException while trying to write to the wave writer - " + ioe.getLocalizedMessage() ); + } + catch ( InterruptedException e ) + { + Log.error( "Oops ... error while processing the buffer queue for " + + " the wave recorder - " + e.getLocalizedMessage() ); + } + } + } + } + +} diff --git a/src/record/wave/ComplexWaveRecorder.java b/src/record/wave/ComplexWaveRecorder.java new file mode 100644 index 00000000..6b3fc23e --- /dev/null +++ b/src/record/wave/ComplexWaveRecorder.java @@ -0,0 +1,175 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package record.wave; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.concurrent.LinkedBlockingQueue; + +import javax.sound.sampled.AudioFormat; + +import log.Log; +import record.Recorder; +import record.RecorderType; +import sample.Listener; +import sample.complex.ComplexSample; +import util.waveaudio.WaveWriter; +import buffer.ComplexSampleBufferAssembler; + +/** + * Threaded WAVE audio recorder for recording complex (I&Q) samples to a wave + * file + */ +public class ComplexWaveRecorder extends Recorder implements Listener +{ + private AudioFormat mAudioFormat; + private boolean mRunning = false; + private boolean mPaused = false; + private WaveWriter mWriter; + private String mFileName; + private BufferProcessor mBufferProcessor; + + //Sample buffer queue ... constrained to 512 sample buffers (arbitrary size) + private LinkedBlockingQueue mToWriteBufferQueue = + new LinkedBlockingQueue( 512 ); + private ComplexSampleBufferAssembler mCurrentBuffer; + + public ComplexWaveRecorder( int sampleRate, String filename ) + { + super( RecorderType.AUDIO ); + + mAudioFormat = new AudioFormat( sampleRate, //SampleRate + 16, //Sample Size + 2, //Channels + true, //Signed + false ); //Little Endian + + mCurrentBuffer = new ComplexSampleBufferAssembler( sampleRate ); + + mFileName = filename; + } + + @Override + public String getFileName() + { + return mFileName + ".wav"; + } + + public void start() throws IOException + { + if( !mRunning ) + { + mWriter = new WaveWriter( mFileName + ".wav", mAudioFormat ); + mWriter.start(); + + mBufferProcessor = new BufferProcessor(); + mBufferProcessor.start(); + + mRunning = true; + } + } + + public void stop() throws IOException + { + if( mRunning ) + { + mBufferProcessor = null; + + mWriter.stop(); + mWriter = null; + + mRunning = false; + } + } + + public void pause() + { + mPaused = true; + } + + public void resume() + { + mPaused = false; + } + + /** + * Assumes that the byte[] represents samples corresponding to the + * audio format object defined at construction + */ + public void receive( ByteBuffer buffer ) + { + if( mRunning && !mPaused ) + { + boolean success = mToWriteBufferQueue.offer( buffer ); + + if( !success ) + { + Log.error( "Wave recorder buffer overflow - throwing away " + + buffer.capacity() + " samples" ); + } + } + } + + @Override + public void receive( ComplexSample sample ) + { + mCurrentBuffer.put( sample ); + + if( !mCurrentBuffer.hasRemaining() ) + { + //Dispatch the buffer to be written to disk + if( !mToWriteBufferQueue.offer( mCurrentBuffer.get() ) ) + { + //We had an overflow ... log it + Log.error( "ComplexWaveRecorder: buffer overflow - throwing " + + "away 1 second of sample data" ); + } + + mCurrentBuffer.clear(); + } + } + + public class BufferProcessor extends Thread + { + public void run() + { + while( true ) + { + try + { + if( mWriter != null ) + { + mWriter.write( mToWriteBufferQueue.take() ); + } + } + catch ( IOException ioe ) + { + Log.error( "IOException while trying to write to the wave " + + "writer - " + ioe.getLocalizedMessage() ); + } + catch ( InterruptedException e ) + { + Log.error( "Oops ... error while processing the buffer " + + "queue for the wave recorder - " + + e.getLocalizedMessage() ); + } + } + } + } + +} diff --git a/src/record/wave/FloatWaveRecorder.java b/src/record/wave/FloatWaveRecorder.java new file mode 100644 index 00000000..95f62056 --- /dev/null +++ b/src/record/wave/FloatWaveRecorder.java @@ -0,0 +1,173 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package record.wave; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.concurrent.LinkedBlockingQueue; + +import javax.sound.sampled.AudioFormat; + +import log.Log; +import record.Recorder; +import record.RecorderType; +import sample.Listener; +import util.waveaudio.WaveWriter; +import buffer.FloatSampleBufferAssembler; + +/** + * Threaded WAVE audio recorder for recording mono float samples to a wave file + */ +public class FloatWaveRecorder extends Recorder implements Listener +{ + private AudioFormat mAudioFormat; + private boolean mRunning = false; + private boolean mPaused = false; + private WaveWriter mWriter; + private String mFileName; + private BufferProcessor mBufferProcessor; + + //Sample buffer queue ... constrained to 512 sample buffers (arbitrary size) + private LinkedBlockingQueue mToWriteBufferQueue = + new LinkedBlockingQueue( 512 ); + private FloatSampleBufferAssembler mCurrentBuffer; + + public FloatWaveRecorder( int sampleRate, String filename ) + { + super( RecorderType.AUDIO ); + + mAudioFormat = new AudioFormat( sampleRate, //SampleRate + 16, //Sample Size + 1, //Channels + true, //Signed + false ); //Little Endian + + mCurrentBuffer = new FloatSampleBufferAssembler( sampleRate ); + + mFileName = filename; + } + + @Override + public String getFileName() + { + return mFileName + ".wav"; + } + + public void start() throws IOException + { + if( !mRunning ) + { + mWriter = new WaveWriter( mFileName + ".wav", mAudioFormat ); + mWriter.start(); + + mBufferProcessor = new BufferProcessor(); + mBufferProcessor.start(); + + mRunning = true; + } + } + + public void stop() throws IOException + { + if( mRunning ) + { + mBufferProcessor = null; + + mWriter.stop(); + mWriter = null; + + mRunning = false; + } + } + + public void pause() + { + mPaused = true; + } + + public void resume() + { + mPaused = false; + } + + /** + * Assumes that the byte[] represents samples corresponding to the + * audio format object defined at construction + */ + public void receive( ByteBuffer buffer ) + { + if( mRunning && !mPaused ) + { + boolean success = mToWriteBufferQueue.offer( buffer ); + + if( !success ) + { + Log.error( "Wave recorder buffer overflow - throwing away " + + buffer.capacity() + " samples" ); + } + } + } + + @Override + public void receive( Float sample ) + { + mCurrentBuffer.put( sample ); + + if( !mCurrentBuffer.hasRemaining() ) + { + //Dispatch the buffer to be written to disk + if( !mToWriteBufferQueue.offer( mCurrentBuffer.get() ) ) + { + //We had an overflow ... log it + Log.error( "FloatWaveRecorder: buffer overflow - throwing " + + "away 1 second of sample data" ); + } + + mCurrentBuffer.reset(); + } + } + + public class BufferProcessor extends Thread + { + public void run() + { + while( true ) + { + try + { + if( mWriter != null ) + { + mWriter.write( mToWriteBufferQueue.take() ); + } + } + catch ( IOException ioe ) + { + Log.error( "IOException while trying to write to the wave " + + "writer - " + ioe.getLocalizedMessage() ); + } + catch ( InterruptedException e ) + { + Log.error( "Oops ... error while processing the buffer " + + "queue for the wave recorder - " + + e.getLocalizedMessage() ); + } + } + } + } + +} diff --git a/src/record/wave/WaveRecorder.java b/src/record/wave/WaveRecorder.java new file mode 100644 index 00000000..dd12e65d --- /dev/null +++ b/src/record/wave/WaveRecorder.java @@ -0,0 +1,134 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package record.wave; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.concurrent.LinkedBlockingQueue; + +import javax.sound.sampled.AudioFormat; + +import log.Log; +import record.Recorder; +import record.RecorderType; +import util.waveaudio.WaveWriter; + +/** + * Threaded WAVE audio recorder. + */ +public class WaveRecorder extends Recorder +{ + private AudioFormat mAudioFormat; + private boolean mRunning = false; + private boolean mPaused = false; + private WaveWriter mWriter; + private String mFileName; + private BufferProcessor mBufferProcessor; + + //Sample buffer queue ... constrained to 512 sample buffers (arbitrary size) + LinkedBlockingQueue mBufferQueue = + new LinkedBlockingQueue( 512 ); + + public WaveRecorder( AudioFormat format, String filename ) + { + super( RecorderType.AUDIO ); + + mAudioFormat = format; + mFileName = filename; + } + + public void start() throws IOException + { + if( !mRunning ) + { + mWriter = new WaveWriter( mFileName + ".wav", mAudioFormat ); + mWriter.start(); + + mBufferProcessor = new BufferProcessor(); + mBufferProcessor.start(); + + mRunning = true; + } + } + + public void stop() throws IOException + { + if( mRunning ) + { + mBufferProcessor = null; + + mWriter.stop(); + mWriter = null; + + mRunning = false; + } + } + + public void pause() + { + mPaused = true; + } + + public void resume() + { + mPaused = false; + } + + /** + * Assumes that the byte[] represents samples corresponding to the + * audio format object defined at construction + */ + public void receive( ByteBuffer buffer ) + { + if( mRunning && !mPaused ) + { + boolean success = mBufferQueue.offer( buffer ); + + if( !success ) + { + Log.error( "Wave recorder buffer overflow - throwing away " + buffer.capacity() + " samples" ); + } + } + } + + public class BufferProcessor extends Thread + { + public void run() + { + while( true ) + { + try + { + if( mWriter != null ) + { + mWriter.write( mBufferQueue.take() ); + } + } + catch ( IOException ioe ) + { + Log.error( "IOException while trying to write to the wave writer - " + ioe.getLocalizedMessage() ); + } + catch ( InterruptedException e ) + { + Log.error( "Oops ... error while processing the buffer queue for " + + " the wave recorder - " + e.getLocalizedMessage() ); + } + } + } + } +} diff --git a/src/record/wave/WriteExample.java b/src/record/wave/WriteExample.java new file mode 100644 index 00000000..159a8fe7 --- /dev/null +++ b/src/record/wave/WriteExample.java @@ -0,0 +1,72 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package record.wave; + +import java.io.File; + +import util.waveaudio.WavFile; + + +public class WriteExample +{ + public static void main(String[] args) + { + try + { + int sampleRate = 44100; // Samples per second + double duration = 5.0; // Seconds + + // Calculate the number of frames required for specified duration + long numFrames = (long)(duration * sampleRate); + + // Create a wav file with the name specified as the first argument + WavFile wavFile = WavFile.newWavFile(new File(args[0]), 2, numFrames, 16, sampleRate); + + // Create a buffer of 100 frames + double[][] buffer = new double[2][100]; + + // Initialise a local frame counter + long frameCounter = 0; + + // Loop until all frames written + while (frameCounter < numFrames) + { + // Determine how many frames to write, up to a maximum of the buffer size + long remaining = wavFile.getFramesRemaining(); + int toWrite = (remaining > 100) ? 100 : (int) remaining; + + // Fill the buffer, one tone per channel + for (int s=0 ; s + ******************************************************************************/ +package sample; + +import java.util.concurrent.CopyOnWriteArrayList; + +/** + * Broadcasts a received float sample to multiple listeners + */ +public class Broadcaster implements Listener +{ + private CopyOnWriteArrayList> mListeners = + new CopyOnWriteArrayList>(); + + @Override + public void receive( T t ) + { + broadcast( t ); + } + + /** + * Clear listeners to prepare for garbage collection + */ + public void dispose() + { + mListeners.clear(); + } + + public boolean hasListeners() + { + return !mListeners.isEmpty(); + } + + public int getListenerCount() + { + return mListeners.size(); + } + + public void addListener( Listener listener ) + { + mListeners.add( listener ); + } + + public void removeListener( Listener listener ) + { + mListeners.remove( listener ); + } + + public void clear() + { + mListeners.clear(); + } + + public void broadcast( T t ) + { + for( Listener listener: mListeners ) + { + listener.receive( t ); + } + } +} diff --git a/src/sample/BufferOverflow.java b/src/sample/BufferOverflow.java new file mode 100644 index 00000000..847a7559 --- /dev/null +++ b/src/sample/BufferOverflow.java @@ -0,0 +1,36 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package sample; + +/** + * Buffer overflow event. Used for broadcasting overflow events to listeners. + */ +public class BufferOverflow +{ + private String mSource; + + public BufferOverflow( String source ) + { + mSource = source; + } + + public String getSource() + { + return mSource; + } +} diff --git a/src/sample/ComplexToFloatSampleConverter.java b/src/sample/ComplexToFloatSampleConverter.java new file mode 100644 index 00000000..2dba6104 --- /dev/null +++ b/src/sample/ComplexToFloatSampleConverter.java @@ -0,0 +1,56 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package sample; + +import java.util.List; + +import sample.complex.ComplexSample; +import source.mixer.MixerChannel; + +/** + * Receives complex samples and send either the left channel or the right + * channel to the short sample listener + */ +public class ComplexToFloatSampleConverter implements Listener> +{ + private MixerChannel mChannel; + private Listener mListener; + + public ComplexToFloatSampleConverter( Listener listener, + MixerChannel channel ) + { + mListener = listener; + mChannel = channel; + } + + @Override + public void receive( List samples ) + { + for( ComplexSample sample: samples ) + { + mListener.receive( mChannel == MixerChannel.LEFT ? + sample.left() : + sample.right() ); + } + } + + public Listener getListener() + { + return mListener; + } +} diff --git a/src/sample/Decimator.java b/src/sample/Decimator.java new file mode 100644 index 00000000..e6958a60 --- /dev/null +++ b/src/sample/Decimator.java @@ -0,0 +1,70 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package sample; + + +public class Decimator implements Listener +{ + private int mDecimationFactor; + private int mDecimationPointer = 0; + private Listener mListener; + + /** + * Constructs the class to consume samples and decimate them by the + * DECIMATIONFACTOR. + * + * @param decimationFactor - number of samples to decimate. Provides one + * sample to registered listeners and discards the next decimationFactor - 1 + * samples. + */ + public Decimator( int decimationFactor ) + { + mDecimationFactor = decimationFactor; + } + + /** + * Receives and broadcasts a sample and then discards the next + * decimationFactor -1 samples + */ + public void receive( T t ) + { + if( mDecimationPointer == 0 ) + { + if( mListener != null ) + { + mListener.receive( t ); + } + } + + mDecimationPointer++; + + //Reset the decimation pointer once it equals the decimation factor + if( mDecimationPointer == mDecimationFactor ) + { + mDecimationPointer = 0; + } + } + + /** + * Adds a sample listener to receive samples + */ + public void setListener( Listener listener ) + { + mListener = listener; + } +} diff --git a/src/sample/FloatArrayToComplexAdapter.java b/src/sample/FloatArrayToComplexAdapter.java new file mode 100644 index 00000000..e55ef201 --- /dev/null +++ b/src/sample/FloatArrayToComplexAdapter.java @@ -0,0 +1,60 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package sample; + +import sample.complex.ComplexSample; + +public class FloatArrayToComplexAdapter implements Listener +{ + private Listener mListener; + + @Override + public void receive( Float[] samples ) + { + if( samples != null ) + { + for( int x = 0; x < samples.length; x += 2 ) + { + if( mListener != null && + samples[ x ] != null && + samples[ x + 1 ] != null ) + { + mListener.receive( new ComplexSample( samples[ x ], + samples[ x + 1 ] ) ); + } + } + + samples = null; + } + } + + public void setListener( Listener listener ) + { + mListener = listener; + } + + public void removeListener( Listener listener ) + { + mListener = null; + } + + public void dispose() + { + mListener = null; + } +} diff --git a/src/sample/Listener.java b/src/sample/Listener.java new file mode 100644 index 00000000..d462468d --- /dev/null +++ b/src/sample/Listener.java @@ -0,0 +1,23 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package sample; + +public interface Listener +{ + public void receive( T t ); +} diff --git a/src/sample/Provider.java b/src/sample/Provider.java new file mode 100644 index 00000000..b9ccd5a5 --- /dev/null +++ b/src/sample/Provider.java @@ -0,0 +1,25 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package sample; + +public interface Provider +{ + public void setListener( Listener listener ); + + public void removeListener( Listener listener ); +} diff --git a/src/sample/complex/ComplexSample.java b/src/sample/complex/ComplexSample.java new file mode 100644 index 00000000..7c7ecd0d --- /dev/null +++ b/src/sample/complex/ComplexSample.java @@ -0,0 +1,225 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package sample.complex; + +import java.io.Serializable; + +import log.Log; + +/** + * ComplexSample for handling left/right channel audio, or inphase/quadrature + * samples, etc. + */ +public class ComplexSample implements Serializable +{ + private static final long serialVersionUID = 1L; + + private float mLeft; + private float mRight; + + public ComplexSample( float left, float right ) + { + mLeft = left; + mRight = right; + } + + public static void main( String[] args ) + { + Log.info( "Starting ... " ); + + int frequency = 15000; + int sampleRate = 2000000; + + float anglePerSample = (float)( 2.0d * Math.PI * (float)frequency / (float)sampleRate ); + + ComplexSample current = new ComplexSample( 0.0f, -1.0f ); + + ComplexSample stepPerSample = ComplexSample.fromAngle( anglePerSample ); + + long start = System.currentTimeMillis(); + + for( int x = 0; x < sampleRate; x++ ) + { +// Log.info( x + " " + current.toString() ); + current.multiply( stepPerSample ); + current.fastNormalize(); + } + + Log.info( "Elapsed: " + ( System.currentTimeMillis() - start ) ); + + Log.info( "Finished!" ); + } + + public ComplexSample copy() + { + return new ComplexSample( mLeft, mRight ); + } + + public String toString() + { + return "I:" + mLeft + " Q:" + mRight; + } + + /** + * Returns a new sample representing the conjugate of this one + */ + public ComplexSample conjugate() + { + return new ComplexSample( mLeft, -mRight ); + } + + /** + * Multiplies this sample by the scalor value + */ + public void multiply( float scalor ) + { + mLeft *= scalor; + mRight *= scalor; + } + + /** + * Multiplies this sample by the multiplier sample + */ + public void multiply( ComplexSample multiplier ) + { + float inphase = ( inphase() * multiplier.inphase() ) - + ( quadrature() * multiplier.quadrature() ); + + float quadrature = ( quadrature() * multiplier.inphase() ) + + ( inphase() * multiplier.quadrature() ); + + mLeft = inphase; + mRight = quadrature; + } + + public static ComplexSample multiply( ComplexSample sample1, + ComplexSample sample2 ) + { + float inphase = ( sample1.inphase() * sample2.inphase() ) - + ( sample1.quadrature() * sample2.quadrature() ); + + float quadrature = ( sample1.quadrature() * sample2.inphase() ) + + ( sample1.inphase() * sample2.quadrature() ); + + return new ComplexSample( inphase, quadrature ); + } + + public static ComplexSample multiply( ComplexSample sample, + Float left, Float right ) + { + float l = ( sample.left() * left ) - ( sample.right() * right ); + float r = ( sample.right() * left ) + ( sample.left() * right ); + + return new ComplexSample( l, r ); + } + + /** + * Adds the adder sample values to this sample + */ + public void add( ComplexSample adder ) + { + mLeft += adder.left(); + mRight += adder.right(); + } + + /** + * Magnitude of this sample + */ + public float magnitude() + { + return (float)Math.sqrt( magnitudeSquared() ); + } + + /** + * Magnitude squared of this sample + */ + public float magnitudeSquared() + { + return (float)( ( inphase() * inphase() ) + + ( quadrature() * quadrature() ) ); + } + + /** + * Returns the vector length to 1 (unit circle) + */ + public void normalize() + { + multiply( (float)( 1.0f / magnitude() ) ); + } + + /** + * Returns the vector length to 1 (unit circle), + * avoiding square root multiplication + */ + public void fastNormalize() + { + multiply( (float)( 1.95f - magnitudeSquared() ) ); + } + + public float left() + { + return mLeft; + } + + public float right() + { + return mRight; + } + + public float inphase() + { + return mLeft; + } + + public float quadrature() + { + return mRight; + } + + public float x() + { + return mLeft; + } + + public float y() + { + return mRight; + } + + public float real() + { + return mLeft; + } + + public float imaginery() + { + return mRight; + } + + /** + * Creates a new complex sample representing the angle with unit circle + * magnitude + * @param angle in radians + * @return + */ + public static ComplexSample fromAngle( float angle ) + { + return new ComplexSample( (float)Math.cos( angle ), + (float)Math.sin( angle ) ); + } +} diff --git a/src/sample/complex/ComplexSampleAssembler.java b/src/sample/complex/ComplexSampleAssembler.java new file mode 100644 index 00000000..ab30b868 --- /dev/null +++ b/src/sample/complex/ComplexSampleAssembler.java @@ -0,0 +1,72 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package sample.complex; + +import java.util.ArrayList; +import java.util.List; + +import sample.Listener; +import sample.Provider; + +public class ComplexSampleAssembler implements Listener, + Provider> +{ + private ArrayList mBuffer; + private int mBufferSize; + private Listener> mListener; + + public ComplexSampleAssembler( int bufferSize ) + { + mBufferSize = bufferSize; + mBuffer = new ArrayList(); + } + + public void dispose() + { + mBuffer.clear(); + mListener = null; + } + + @Override + public void receive( ComplexSample sample ) + { + mBuffer.add( sample ); + + if( mBuffer.size() >= mBufferSize ) + { + if( mListener != null ) + { + mListener.receive( mBuffer ); + } + + mBuffer = new ArrayList(); + } + } + + @Override + public void setListener( Listener> listener ) + { + mListener = listener; + } + + @Override + public void removeListener( Listener> listener ) + { + mListener = null; + } +} diff --git a/src/sample/complex/ComplexSampleListListener.java b/src/sample/complex/ComplexSampleListListener.java new file mode 100644 index 00000000..ed2cc1ea --- /dev/null +++ b/src/sample/complex/ComplexSampleListListener.java @@ -0,0 +1,25 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package sample.complex; + +import java.util.List; + +public interface ComplexSampleListListener +{ + public void receive( List samples ); +} diff --git a/src/sample/complex/ComplexSampleRateCounter.java b/src/sample/complex/ComplexSampleRateCounter.java new file mode 100644 index 00000000..b7f48ecf --- /dev/null +++ b/src/sample/complex/ComplexSampleRateCounter.java @@ -0,0 +1,107 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package sample.complex; + +import java.util.List; + +import log.Log; +import sample.Listener; +import source.tuner.FrequencyChangeListener; +import buffer.FloatAveragingBuffer; + +public class ComplexSampleRateCounter implements Listener, + ComplexSampleListListener, + FrequencyChangeListener +{ + private int mCounter; + private int mAverageCounter; + private int mExpectedSampleRate; + private long mLastRun = 0; + private int mBufferSize = 100; + private FloatAveragingBuffer mAveragingBuffer; + + public ComplexSampleRateCounter( int expectedSampleRate, int bufferSize ) + { + mExpectedSampleRate = expectedSampleRate; + mBufferSize = bufferSize; + + mAveragingBuffer = new FloatAveragingBuffer( mBufferSize ); + + mLastRun = System.currentTimeMillis(); + + Log.info( "Complex Sample Rate Counter started ...." ); + } + + @Override + public void receive( ComplexSample sample ) + { + mCounter++; + + if( mCounter >= mExpectedSampleRate ) + { + mCounter = 0; + + long now = System.currentTimeMillis(); + long elapsed = now - mLastRun; + mLastRun = now; + + float average = (float)mExpectedSampleRate * 1000.0f / (float)elapsed; + float runningAverage = mAveragingBuffer.get( average ); + + mAverageCounter++; + + Log.info( "Complex Sample Rate Avg " + mAverageCounter + + " current [" + elapsed + " ms/" + average + + " hz] average [" + runningAverage + " hz] expected [" + + mExpectedSampleRate + " hz] over [" + + mBufferSize + "] buffer length" ); + } + } + + @Override + public void receive( List samples ) + { + mCounter += samples.size(); + + if( mCounter >= mExpectedSampleRate ) + { + mCounter -= mExpectedSampleRate; + + long now = System.currentTimeMillis(); + long elapsed = now - mLastRun; + mLastRun = now; + + float average = (float)mExpectedSampleRate * 1000.0f / (float)elapsed; + float runningAverage = mAveragingBuffer.get( average ); + + mAverageCounter++; + + Log.info( "Complex Sample Rate Avg " + mAverageCounter + + " current [" + elapsed + " ms/" + average + + " hz] average [" + runningAverage + " hz] expected [" + + mExpectedSampleRate + " hz] over [" + + mBufferSize + "] buffer length" ); + } + } + + @Override + public void frequencyChanged( int frequency, int bandwidth ) + { + mExpectedSampleRate = bandwidth; + } +} diff --git a/src/sample/complex/ComplexSampleUtils.java b/src/sample/complex/ComplexSampleUtils.java new file mode 100644 index 00000000..ba95a39b --- /dev/null +++ b/src/sample/complex/ComplexSampleUtils.java @@ -0,0 +1,49 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package sample.complex; + + +public class ComplexSampleUtils +{ + + public static ComplexSample multiply( ComplexSample a, + ComplexSample b ) + { + return new ComplexSample( + ( a.inphase() * b.inphase() ) - ( a.quadrature() * b.quadrature() ), + ( ( a.quadrature() * b.inphase() ) + ( a.inphase() * b.quadrature() ) ) ); + } + + public static ComplexSample minus( ComplexSample a, ComplexSample b ) + { + return new ComplexSample( a.inphase() - b.inphase(), + a.quadrature() - b.quadrature() ); + } + + public static double magnitude( ComplexSample sample ) + { + return Math.sqrt( magnitudeSquared( sample ) ); + } + + public static int magnitudeSquared( ComplexSample sample ) + { + return (int)( ( sample.inphase() * sample.inphase() ) + + ( sample.quadrature() * sample.quadrature() ) ); + } + +} diff --git a/src/sample/complex/RTLComplexSample.java b/src/sample/complex/RTLComplexSample.java new file mode 100644 index 00000000..5959e685 --- /dev/null +++ b/src/sample/complex/RTLComplexSample.java @@ -0,0 +1,57 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package sample.complex; + +@Deprecated +public class RTLComplexSample extends ComplexSample +{ + private static final long serialVersionUID = 1L; + + private static float[] mValues; + + /** + * Creates a lookup table that converts the 8-bit valued range from 0 - 255 + * into scaled float values of -128 to 0 to 127 + */ + static + { + mValues = new float[ 256 ]; + + float scale = (float)( 1.0f / 127.0f ); + + for( int x = 0; x < 256; x++ ) + { + mValues[ x ] = (float)( x - 128 ) * scale; + } + + mValues[ 128 ] = 0.0000001f; + } + + /** + * Constructs a complex sample from the sample format produced by the + * RTL-2832 chip, where the byte values run from 0 - 255, but represent + * the value -127 to 127 + * + * @param left + * @param right + */ + public RTLComplexSample( byte left, byte right ) + { + super( mValues[ left & 0xFF ], mValues[ right & 0xFF ] ); + } +} diff --git a/src/sample/complex/ThreadKiller.java b/src/sample/complex/ThreadKiller.java new file mode 100644 index 00000000..3d695332 --- /dev/null +++ b/src/sample/complex/ThreadKiller.java @@ -0,0 +1,33 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package sample.complex; + +/** + * Poison Pill class to kill a processing thread. Where a thread accepts + * ComplexSample objects, send this class to the thread as a sentinal value + * to kill the processing thread. + */ +public class ThreadKiller extends ComplexSample +{ + private static final long serialVersionUID = 1L; + + public ThreadKiller() + { + super( 0, 0 ); + } +} diff --git a/src/settings/ColorSetting.java b/src/settings/ColorSetting.java new file mode 100644 index 00000000..6040fce4 --- /dev/null +++ b/src/settings/ColorSetting.java @@ -0,0 +1,190 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package settings; + +import java.awt.Color; + +import javax.xml.bind.annotation.XmlAttribute; +import javax.xml.bind.annotation.XmlTransient; + +public class ColorSetting extends Setting +{ + private static final int NO_TRANSLUCENCY = 255; + private static final int SPECTRUM_TRANSLUCENCY = 128; + private static final int CONFIG_TRANSLUCENCY = 60; + + private String mRGB; + private int mAlpha; + private ColorSettingName mColorSettingName = ColorSettingName.UNKNOWN; + + public ColorSetting() + { + setColor( mColorSettingName.getDefaultColor() ); + } + + public ColorSetting( ColorSettingName name ) + { + setColor( name.getDefaultColor() ); + setColorSettingName( name ); + } + + @XmlAttribute + public ColorSettingName getColorSettingName() + { + return mColorSettingName; + } + + public void setColorSettingName( ColorSettingName name ) + { + mColorSettingName = name; + } + + @XmlAttribute + public String getRgb() + { + return mRGB; + } + + public void setRgb( String value ) + { + mRGB = value; + } + + @XmlAttribute + public int getAlpha() + { + return mAlpha; + } + + public void setAlpha( int value ) + { + mAlpha = value; + } + + @XmlTransient + public Color getColor() + { + Color temp = Color.decode( mRGB ); + + return new Color( temp.getRed(), + temp.getGreen(), + temp.getBlue(), + mAlpha ); + } + + public void setColor( Color color ) + { + mRGB = Integer.toHexString( color.getRGB() ); + mRGB = "#" + mRGB.substring( 2, mRGB.length() ); + mAlpha = color.getAlpha(); + } + + public static Color getTranslucent( Color color, int translucency ) + { + return new Color( color.getRed(), + color.getGreen(), + color.getBlue(), + translucency ); + } + + public enum ColorSettingName + { + CHANNEL_CONFIG( getTranslucent( Color.LIGHT_GRAY, CONFIG_TRANSLUCENCY ), + "Channel", "Channel Color" ), + CHANNEL_CONFIG_PROCESSING( getTranslucent( Color.GREEN, CONFIG_TRANSLUCENCY ), + "Channel Processing", "Processing Channel Color" ), + CHANNEL_CONFIG_SELECTED( getTranslucent( Color.BLUE, CONFIG_TRANSLUCENCY ), + "Channel Selected", "Selected Channel Color" ), + + CHANNEL_STATE_BACKGROUND( Color.BLACK, "Background", + "Channel State Background" ), + CHANNEL_STATE_GRADIENT_TOP_CALL( Color.BLACK, + "Call Gradient Top", "Channel Call State Gradient Top" ), + CHANNEL_STATE_GRADIENT_MIDDLE_CALL( Color.BLUE, + "Call Gradient Middle", "Channel Call State Gradient Middle" ), + CHANNEL_STATE_GRADIENT_TOP_CONTROL( Color.BLACK, + "Control Gradient Top", "Channel Control State Gradient Top" ), + CHANNEL_STATE_GRADIENT_MIDDLE_CONTROL( new Color( 0xC64F00 ), + "Control Gradient Middle", "Channel Control State Gradient Middle" ), + CHANNEL_STATE_GRADIENT_TOP_DATA( Color.BLACK, + "Data Gradient Top", "Channel Data State Gradient Top" ), + CHANNEL_STATE_GRADIENT_MIDDLE_DATA( Color.BLUE, + "Data Gradient Middle", "Channel Data State Gradient Middle" ), + CHANNEL_STATE_GRADIENT_TOP_FADE( Color.BLACK, + "Fade Gradient Top", "Channel Fade State Gradient Top" ), + CHANNEL_STATE_GRADIENT_MIDDLE_FADE( Color.DARK_GRAY, + "Fade Gradient Middle", "Channel Fade State Gradient Middle" ), + CHANNEL_STATE_GRADIENT_TOP_IDLE( Color.BLACK, + "Idle Gradient Top", "Channel Idle State Gradient Top" ), + CHANNEL_STATE_GRADIENT_MIDDLE_IDLE( Color.DARK_GRAY, + "Idle Gradient Middle", "Channel Idle State Gradient Middle" ), + CHANNEL_STATE_GRADIENT_TOP_NO_TUNER( Color.RED, + "No Tuner Gradient Top", "Channel No Tuner State Gradient Top" ), + CHANNEL_STATE_GRADIENT_MIDDLE_NO_TUNER( new Color( 0x990000 ), + "No Tuner Gradient Middle", "Channel No Tuner State Gradient Middle" ), + CHANNEL_STATE_LABEL_DETAILS( Color.LIGHT_GRAY, + "Details", "Details Label Color" ), + CHANNEL_STATE_LABEL_DECODER( Color.GREEN, + "Decoder", "Decoder Label Color" ), + CHANNEL_STATE_LABEL_AUX_DECODER( Color.YELLOW, + "Aux Decoder", "Aux Decoder Label Color" ), + CHANNEL_STATE_SELECTED_CHANNEL( Color.YELLOW, + "Selected Channel Indicator", "Selected Channel Indicator Color" ), + + SPECTRUM_BACKGROUND( Color.BLACK, + "Background", "Spectrum Background Color" ), + SPECTRUM_CURSOR( Color.ORANGE, + "Cursor", "Spectrum Cursor Color" ), + SPECTRUM_GRADIENT_BOTTOM( getTranslucent( Color.GREEN, SPECTRUM_TRANSLUCENCY ), + "Gradient Bottom", "Spectrum Gradient Bottom Color" ), + SPECTRUM_GRADIENT_TOP( getTranslucent( Color.WHITE, SPECTRUM_TRANSLUCENCY ), + "Gradient Top", "Spectrum Gradient Top Color" ), + SPECTRUM_LINE( getTranslucent( Color.LIGHT_GRAY, SPECTRUM_TRANSLUCENCY ), + "Line", "Spectrum Lines and Text Color" ), + + UNKNOWN( Color.RED, "Unknown", "Unknown Setting Color" ); + + private Color mDefaultColor; + private String mLabel; + private String mDialogTitle; + + private ColorSettingName( Color defaultColor, + String label, + String dialogTitle ) + { + mDefaultColor = defaultColor; + mLabel = label; + mDialogTitle = dialogTitle; + } + + public Color getDefaultColor() + { + return mDefaultColor; + } + + public String getLabel() + { + return mLabel; + } + + public String getDialogTitle() + { + return mDialogTitle; + } + } +} diff --git a/src/settings/ColorSettingMenuItem.java b/src/settings/ColorSettingMenuItem.java new file mode 100644 index 00000000..d6045aae --- /dev/null +++ b/src/settings/ColorSettingMenuItem.java @@ -0,0 +1,71 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package settings; + +import java.awt.Color; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; + +import javax.swing.JColorChooser; +import javax.swing.JMenuItem; + +import settings.ColorSetting.ColorSettingName; +import util.ColorIcon; + +/** + * JMenuItem for selecting a color and automatically setting (saving) the + * color selection in the settings manager + */ +public class ColorSettingMenuItem extends JMenuItem +{ + private static final long serialVersionUID = 1L; + + private ColorSettingName mColorSettingName; + private SettingsManager mSettingsManager; + private Color mCurrentColor; + + public ColorSettingMenuItem( SettingsManager settingsManager, + ColorSettingName colorSettingName ) + { + super( colorSettingName.getLabel() ); + + mSettingsManager = settingsManager; + mColorSettingName = colorSettingName; + + mCurrentColor = mSettingsManager + .getColorSetting( mColorSettingName ).getColor(); + + this.setIcon( new ColorIcon( mCurrentColor ) ); + + addActionListener( new ActionListener() + { + @Override + public void actionPerformed( ActionEvent e ) + { + Color newColor = JColorChooser.showDialog( ColorSettingMenuItem.this, + mColorSettingName.getDialogTitle(), + mCurrentColor ); + + if( newColor != null ) + { + mSettingsManager.setColorSetting( mColorSettingName, newColor ); + } + } + } ); + } +} diff --git a/src/settings/ColorSettingResetAllMenuItem.java b/src/settings/ColorSettingResetAllMenuItem.java new file mode 100644 index 00000000..49b5e209 --- /dev/null +++ b/src/settings/ColorSettingResetAllMenuItem.java @@ -0,0 +1,57 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package settings; + +import java.awt.Color; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; + +import javax.swing.JMenuItem; + +import settings.ColorSetting.ColorSettingName; + +/** + * JMenuItem for selecting a color and automatically setting (saving) the + * color selection in the settings manager + */ +public class ColorSettingResetAllMenuItem extends JMenuItem +{ + private static final long serialVersionUID = 1L; + + private ColorSettingName mColorSettingName; + private SettingsManager mSettingsManager; + private Color mCurrentColor; + + public ColorSettingResetAllMenuItem( SettingsManager settingsManager, + ColorSettingName colorSettingName ) + { + super( "Reset - " + colorSettingName.getLabel() ); + + mSettingsManager = settingsManager; + mColorSettingName = colorSettingName; + + addActionListener( new ActionListener() + { + @Override + public void actionPerformed( ActionEvent e ) + { + mSettingsManager.resetAllColorSettings(); + } + } ); + } +} diff --git a/src/settings/ColorSettingResetMenuItem.java b/src/settings/ColorSettingResetMenuItem.java new file mode 100644 index 00000000..f868d221 --- /dev/null +++ b/src/settings/ColorSettingResetMenuItem.java @@ -0,0 +1,57 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package settings; + +import java.awt.Color; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; + +import javax.swing.JMenuItem; + +import settings.ColorSetting.ColorSettingName; + +/** + * JMenuItem for selecting a color and automatically setting (saving) the + * color selection in the settings manager + */ +public class ColorSettingResetMenuItem extends JMenuItem +{ + private static final long serialVersionUID = 1L; + + private ColorSettingName mColorSettingName; + private SettingsManager mSettingsManager; + private Color mCurrentColor; + + public ColorSettingResetMenuItem( SettingsManager settingsManager, + ColorSettingName colorSettingName ) + { + super( "Reset - " + colorSettingName.getLabel() ); + + mSettingsManager = settingsManager; + mColorSettingName = colorSettingName; + + addActionListener( new ActionListener() + { + @Override + public void actionPerformed( ActionEvent e ) + { + mSettingsManager.resetColorSetting( mColorSettingName ); + } + } ); + } +} diff --git a/src/settings/FileSetting.java b/src/settings/FileSetting.java new file mode 100644 index 00000000..d9e7e460 --- /dev/null +++ b/src/settings/FileSetting.java @@ -0,0 +1,52 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package settings; + +import java.io.File; + + +public class FileSetting extends Setting +{ + private String mPath; + + public FileSetting() + { + super(); + } + + public FileSetting( String name, String path ) + { + super( name ); + setPath( path ); + } + + public String getPath() + { + return mPath; + } + + public void setPath( String value ) + { + mPath = value; + } + + public File getFile() + { + return new File( mPath ); + } +} diff --git a/src/settings/MapViewSetting.java b/src/settings/MapViewSetting.java new file mode 100644 index 00000000..8e68239c --- /dev/null +++ b/src/settings/MapViewSetting.java @@ -0,0 +1,79 @@ +package settings; + +import javax.xml.bind.annotation.XmlAttribute; +import javax.xml.bind.annotation.XmlTransient; + +import org.jdesktop.swingx.mapviewer.GeoPosition; + +public class MapViewSetting extends Setting +{ + private double mLatitude; + private double mLongitude; + private int mZoom; + + public MapViewSetting() + { + /* Empty constructor for JAXB */ + } + + public MapViewSetting( String name, double latitude, double longitude, int zoom ) + { + super( name ); + mLatitude = latitude; + mLongitude = longitude; + mZoom = zoom; + } + + public MapViewSetting( String name, GeoPosition position, int zoom ) + { + super( name ); + mLatitude = position.getLatitude(); + mLongitude = position.getLongitude(); + mZoom = zoom; + } + + @XmlAttribute + public double getLatitude() + { + return mLatitude; + } + + public void setLatitude( double latitude ) + { + mLatitude = latitude; + } + + @XmlAttribute + public double getLongitude() + { + return mLongitude; + } + + public void setLongitude( double longitude ) + { + mLongitude = longitude; + } + + @XmlAttribute + public int getZoom() + { + return mZoom; + } + + public void setZoom( int zoom ) + { + mZoom = zoom; + } + + @XmlTransient + public GeoPosition getGeoPosition() + { + return new GeoPosition( mLatitude, mLongitude ); + } + + public void setGeoPosition( GeoPosition position ) + { + mLatitude = position.getLatitude(); + mLongitude = position.getLongitude(); + } +} diff --git a/src/settings/Setting.java b/src/settings/Setting.java new file mode 100644 index 00000000..7bbf97a7 --- /dev/null +++ b/src/settings/Setting.java @@ -0,0 +1,45 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package settings; + +import javax.xml.bind.annotation.XmlAttribute; + +public class Setting +{ + protected String mName; + + public Setting() + { + } + + public Setting( String name ) + { + mName = name; + } + + @XmlAttribute + public String getName() + { + return mName; + } + + public void setName( String name ) + { + mName = name; + } +} diff --git a/src/settings/SettingChangeListener.java b/src/settings/SettingChangeListener.java new file mode 100644 index 00000000..b5550bb2 --- /dev/null +++ b/src/settings/SettingChangeListener.java @@ -0,0 +1,24 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package settings; + +public interface SettingChangeListener +{ + public void settingChanged( Setting setting ); + public void settingDeleted( Setting setting ); +} diff --git a/src/settings/Settings.java b/src/settings/Settings.java new file mode 100644 index 00000000..f799660a --- /dev/null +++ b/src/settings/Settings.java @@ -0,0 +1,295 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package settings; + +import java.util.ArrayList; +import java.util.List; + +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlRootElement; +import javax.xml.bind.annotation.XmlSeeAlso; + +import map.DefaultIcon; +import map.MapIcon; + +import org.jdesktop.swingx.mapviewer.GeoPosition; + +import settings.ColorSetting.ColorSettingName; +import source.tuner.TunerConfiguration; +import source.tuner.TunerConfigurationAssignment; +import source.tuner.TunerType; +import source.tuner.fcd.proV1.FCD1TunerConfiguration; +import source.tuner.fcd.proplusV2.FCD2TunerConfiguration; +import source.tuner.rtl.e4k.E4KTunerConfiguration; +import source.tuner.rtl.r820t.R820TTunerConfiguration; + +/** + * Support for persisting via JAXB all configured SDRTrunk settings + */ +@XmlSeeAlso( { ColorSetting.class, + DefaultIcon.class, + E4KTunerConfiguration.class, + FCD1TunerConfiguration.class, + FCD2TunerConfiguration.class, + FileSetting.class, + MapViewSetting.class, + MapIcon.class, + R820TTunerConfiguration.class, + Setting.class, + TunerConfiguration.class, + TunerConfigurationAssignment.class} ) + +@XmlRootElement( name = "SDRTrunk_settings" ) +public class Settings +{ + private ArrayList mSettings = new ArrayList(); + + private ArrayList mTunerConfiguration = + new ArrayList(); + + private ArrayList mConfigurationAssignments = + new ArrayList(); + + public Settings() + { + } + + @XmlElement( name="tuner_configuration" ) + public ArrayList getTunerConfigurations() + { + return mTunerConfiguration; + } + + public void setTunerConfigurations( ArrayList configs ) + { + mTunerConfiguration = configs; + } + + public void addTunerConfiguration( TunerConfiguration config ) + { + mTunerConfiguration.add( config ); + + } + + public void removeTunerConfiguration( TunerConfiguration config ) + { + mTunerConfiguration.remove( config ); + } + + public ArrayList getTunerConfigurations( TunerType type ) + { + ArrayList configs = + new ArrayList(); + + for( TunerConfiguration config: mTunerConfiguration ) + { + if( config.getTunerType() == type ) + { + configs.add( config ); + } + } + + return configs; + } + + @XmlElement( name="setting" ) + public ArrayList getSettings() + { + return mSettings; + } + + public void setSettings( ArrayList settings ) + { + mSettings = settings; + } + + public Setting getSetting( String name ) + { + for( Setting setting: mSettings ) + { + if( setting.getName().contentEquals( name ) ) + { + return setting; + } + } + + return null; + } + + public void addSetting( Setting setting ) + { + mSettings.add( setting ); + } + + public void removeSetting( Setting setting ) + { + mSettings.remove( setting ); + } + + public DefaultIcon getDefaultIcon() + { + for( Setting setting: mSettings ) + { + if( setting instanceof DefaultIcon ) + { + return (DefaultIcon)setting; + } + } + + return null; + } + + public ColorSetting getColorSetting( ColorSettingName name ) + { + for( Setting setting: mSettings ) + { + if( setting instanceof ColorSetting && + ((ColorSetting)setting).getColorSettingName() == name ) + { + return (ColorSetting)setting; + } + } + + return null; + } + + public List getColorSettings() + { + ArrayList colors = new ArrayList(); + + for( Setting setting: mSettings ) + { + if( setting instanceof ColorSetting ) + { + colors.add( (ColorSetting)setting ); + } + } + + return colors; + } + + public FileSetting getFileSetting( String name ) + { + for( Setting setting: mSettings ) + { + if( setting instanceof FileSetting && + setting.getName().contentEquals( name ) ) + { + return (FileSetting)setting; + } + } + + return null; + } + + public MapIcon getMapIcon( String name ) + { + for( Setting setting: mSettings ) + { + if( setting instanceof MapIcon && + setting.getName().contentEquals( name ) ) + { + return (MapIcon)setting; + } + } + + return null; + } + + public ArrayList getMapIcons() + { + ArrayList icons = new ArrayList(); + + for( Setting setting: mSettings ) + { + if( setting instanceof MapIcon ) + { + icons.add( (MapIcon)setting ); + } + } + + return icons; + } + + @XmlElement( name="tuner_configuration_assignment" ) + public ArrayList getSelectedTunerConfiguration() + { + return mConfigurationAssignments; + } + + public void setSelectedTunerConfiguration( ArrayList configs ) + { + mConfigurationAssignments = configs; + } + + public void addSelectedTunerConfiguration( TunerConfigurationAssignment config ) + { + mConfigurationAssignments.add( config ); + + } + + public void removeSelectedTunerConfiguration( TunerConfigurationAssignment config ) + { + mConfigurationAssignments.remove( config ); + } + + public TunerConfigurationAssignment getConfigurationAssignment( TunerType type, String uniqueID ) + { + for( TunerConfigurationAssignment config: mConfigurationAssignments ) + { + if( config.getTunerType() == type && + uniqueID.contentEquals( config.getUniqueID() ) ) + { + return config; + } + } + + return null; + } + + public void setConfigurationAssignment( TunerType type, + String uniqueID, + String tunerConfigurationName ) + { + TunerConfigurationAssignment config = getConfigurationAssignment( type, uniqueID ); + + if( config == null ) + { + config = new TunerConfigurationAssignment(); + config.setTunerType( type ); + config.setUniqueID( uniqueID ); + addSelectedTunerConfiguration( config ); + } + + config.setTunerConfigurationName( tunerConfigurationName ); + } + + public MapViewSetting getMapViewSetting( String name ) + { + for( Setting setting: mSettings ) + { + if( setting instanceof MapViewSetting && + setting.getName().contentEquals( name ) ) + { + return (MapViewSetting)setting; + } + } + + return null; + } +} diff --git a/src/settings/SettingsManager.java b/src/settings/SettingsManager.java new file mode 100644 index 00000000..83066d2c --- /dev/null +++ b/src/settings/SettingsManager.java @@ -0,0 +1,818 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package settings; + +import java.awt.Color; +import java.awt.Image; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Iterator; + +import javax.swing.ImageIcon; +import javax.xml.bind.JAXBContext; +import javax.xml.bind.JAXBException; +import javax.xml.bind.Marshaller; +import javax.xml.bind.Unmarshaller; + +import log.Log; +import map.DefaultIcon; +import map.MapIcon; + +import org.jdesktop.swingx.mapviewer.GeoPosition; + +import properties.SystemProperties; +import settings.ColorSetting.ColorSettingName; +import source.tuner.TunerConfiguration; +import source.tuner.TunerConfigurationAssignment; +import source.tuner.TunerType; +import source.tuner.fcd.proV1.FCD1TunerConfiguration; +import source.tuner.fcd.proplusV2.FCD2TunerConfiguration; +import source.tuner.rtl.e4k.E4KTunerConfiguration; +import source.tuner.rtl.r820t.R820TTunerConfiguration; + +public class SettingsManager +{ + private Settings mSettings = new Settings(); + + private ArrayList mListeners = + new ArrayList(); + + public static final String DEFAULT_ICON = "No Icon"; + public String mCurrentDefaultIconName = DEFAULT_ICON; + + private HashMap mIcons = new HashMap(); + + private HashMap mResizedIcons = + new HashMap(); + + public SettingsManager() + { + init(); + } + + /** + * Loads settings from the current settings file, or the default settings file, + * as specified in the current SDRTrunk system settings + */ + private void init() + { + SystemProperties props = SystemProperties.getInstance(); + + Path settingsFolder = props.getApplicationFolder( "settings" ); + + String defaultSettingsFile = + props.get( "settings.defaultFilename", "settings.xml" ); + + String settingsFile = + props.get( "settings.currentFilename", defaultSettingsFile ); + + load( settingsFolder.resolve( settingsFile ) ); + + refreshIcons(); + } + + /** + * Reloads the icon hash map, and sets up the default iconology. If the + * saved default icon name is not part of the current set of icons, we + * set the default icon to the standard system default name, which should + * be guaranteed to be available. + */ + public void refreshIcons() + { + mIcons.clear(); + + mCurrentDefaultIconName = DEFAULT_ICON; + + DefaultIcon defaultIcon = getDefaultIcon(); + + ArrayList icons = mSettings.getMapIcons(); + icons.addAll( getStandardIcons() ); + + for( MapIcon icon: icons ) + { + mIcons.put( icon.getName(), icon ); + + if( icon.getName().contentEquals( defaultIcon.getName() ) ) + { + mCurrentDefaultIconName = defaultIcon.getName(); + icon.setDefaultIcon( true ); + } + else + { + icon.setDefaultIcon( false ); + } + } + } + + public Settings getSettings() + { + return mSettings; + } + + public void setSettings( Settings settings ) + { + mSettings = settings; + } + + public Setting getSetting( String name ) + { + return mSettings.getSetting( name ); + } + + /** + * Returns the current setting, or if the setting doesn't exist + * returns a newly created setting with the specified parameters + */ + public ColorSetting getColorSetting( ColorSettingName name ) + { + ColorSetting setting = mSettings.getColorSetting( name ); + + if( setting == null ) + { + setting = new ColorSetting( name ); + + addSetting( setting ); + } + + return setting; + } + + /** + * Fetches the current setting and applies the parameter(s) to it. Creates + * the setting if it does not exist + */ + public void setColorSetting( ColorSettingName name, Color color ) + { + ColorSetting setting = getColorSetting( name ); + + setting.setColor( color ); + + broadcastSettingChange( setting ); + + save(); + } + + public void resetColorSetting( ColorSettingName name ) + { + setColorSetting( name, name.getDefaultColor() ); + } + + public void resetAllColorSettings() + { + for( ColorSetting color: mSettings.getColorSettings() ) + { + resetColorSetting( color.getColorSettingName() ); + } + } + + /** + * Returns the current setting, or if the setting doesn't exist + * returns a newly created setting with the specified parameters + */ + public FileSetting getFileSetting( String name, String defaultPath ) + { + FileSetting setting = mSettings.getFileSetting( name ); + + if( setting == null ) + { + setting = new FileSetting( name, defaultPath ); + + addSetting( setting ); + } + + return setting; + } + + /** + * Fetches the current setting and applies the parameter(s) to it. Creates + * the setting if it does not exist + */ + public void setFileSetting( String name, String path ) + { + FileSetting setting = getFileSetting( name, path ); + + setting.setPath( path ); + + broadcastSettingChange( setting ); + + save(); + } + + public MapIcon[] getMapIcons() + { + MapIcon[] returnIcons = new MapIcon[mIcons.values().size() ]; + + return mIcons.values().toArray( returnIcons ); + } + + private ArrayList getStandardIcons() + { + ArrayList icons = new ArrayList(); + + MapIcon defaultIcon = new MapIcon( DEFAULT_ICON, "images/no_icon.png", false ); + icons.add( defaultIcon ); + + MapIcon ambulance = + new MapIcon( "Ambulance", "images/ambulance.png", false ); + icons.add( ambulance ); + + MapIcon blockTruck = + new MapIcon( "Block Truck", "images/concrete_block_truck.png", false ); + icons.add( blockTruck ); + + MapIcon cwid = new MapIcon( "CWID", "images/cwid.png", false ); + icons.add( cwid ); + + MapIcon dispatcher = + new MapIcon( "Dispatcher", "images/dispatcher.png", false ); + icons.add( dispatcher ); + + MapIcon dumpTruck = + new MapIcon( "Dump Truck", "images/dump_truck_red.png", false ); + icons.add( dumpTruck ); + + MapIcon garbageTruck = + new MapIcon( "Garbage Truck", "images/garbage_truck.png", false ); + icons.add( garbageTruck ); + + MapIcon loader = new MapIcon( "Loader", "images/loader.png", false ); + icons.add( loader ); + + MapIcon police = + new MapIcon( "Police", "images/police.png", false ); + icons.add( police ); + + MapIcon propaneTruck = + new MapIcon( "Propane Truck", "images/propane_truck.png", false ); + icons.add( propaneTruck ); + + MapIcon schoolBus = + new MapIcon( "School Bus", "images/school_bus.png", false ); + icons.add( schoolBus ); + + MapIcon taxi = + new MapIcon( "Taxi", "images/taxi.png", false ); + icons.add( taxi ); + + MapIcon train = + new MapIcon( "Train", "images/train.png", false ); + icons.add( train ); + + MapIcon transportBus = + new MapIcon( "Transport Bus", "images/opt_bus.png", false ); + icons.add( transportBus ); + + MapIcon van = + new MapIcon( "Van", "images/van.png", false ); + icons.add( van ); + + return icons; + } + + /** + * Deletes the map icon + */ + public void deleteMapIcon( MapIcon icon ) + { + MapIcon existing = mIcons.get( icon.getName() ); + + if( existing != null ) + { + mIcons.remove( icon.getName() ); + + mSettings.removeSetting( existing ); + + broadcastSettingDeleted( existing ); + + save(); + } + } + + /** + * Adds the icon, overwriting any existing icon with the same name + * @param icon + */ + public void addMapIcon( MapIcon icon ) + { + MapIcon existing = mIcons.get( icon.getName() ); + + if( existing == null ) + { + MapIcon newIcon = new MapIcon( icon.getName(), icon.getPath() ); + mIcons.put( newIcon.getName(), newIcon ); + addSetting( newIcon ); + } + else + { + existing.setName( icon.getName() ); + existing.setPath( icon.getPath() ); + broadcastSettingChange( existing ); + save(); + } + } + + public void updatMapIcon( MapIcon icon, String newName, String newPath ) + { + MapIcon existing = mSettings.getMapIcon( icon.getName() ); + + if( existing == null ) + { + addMapIcon( new MapIcon( newName, newPath ) ); + } + else + { + existing.setName( newName ); + existing.setPath( newPath ); + broadcastSettingChange( existing ); + save(); + } + } + + /** + * Returns named map icon scaled to height, if necessary. + * @param name - name of icon + * @param heigh - height of icon in pixels + * + * @return - ImageIcon or null + */ + public ImageIcon getImageIcon( String name, int height ) + { + String mergedName = name + height; + + if( mResizedIcons.containsKey( mergedName ) ) + { + return mResizedIcons.get( mergedName ); + } + + MapIcon mapIcon = mIcons.get( name ); + + if( mapIcon != null ) + { + if( mapIcon.getImageIcon().getIconHeight() > height ) + { + ImageIcon scaled = getScaledIcon( mapIcon.getImageIcon(), height ); + + mResizedIcons.put( mergedName, scaled ); + + return scaled; + } + else + { + mResizedIcons.put( mergedName, mapIcon.getImageIcon() ); + } + } + + /* Use the default Icon */ + String mergedDefault = mCurrentDefaultIconName + height; + + if( mResizedIcons.containsKey( mergedDefault ) ) + { + return mResizedIcons.get( mergedDefault ); + } + else + { + MapIcon defaultIcon = mIcons.get( mCurrentDefaultIconName ); + + if( defaultIcon != null ) + { + if( defaultIcon.getImageIcon().getIconHeight() > height ) + { + ImageIcon scaledDefault = getScaledIcon( defaultIcon.getImageIcon(), height ); + mResizedIcons.put( mergedDefault, scaledDefault ); + return scaledDefault; + } + else + { + mResizedIcons.put( mergedDefault, defaultIcon.getImageIcon() ); + return defaultIcon.getImageIcon(); + } + } + } + + /* Something happened ... the above should always return an icon */ + Log.error( "SettingsManager - couldn't return an icon named [" + + name + "] of heigh [" + height + "]" ); + + return null; + } + + private ImageIcon getScaledIcon( ImageIcon original, int height ) + { + double scale = (double)original.getIconHeight() / (double)height; + + int scaledWidth = (int)( (double)original.getIconWidth() / scale ); + + Image scaledImage = original.getImage().getScaledInstance( scaledWidth, + height, java.awt.Image.SCALE_SMOOTH ); + + return new ImageIcon( scaledImage ); + } + + public MapIcon getMapIcon( String name ) + { + if( mIcons.containsKey( name ) ) + { + return mIcons.get( name ); + } + + /* Return the default */ + if( mIcons.containsKey( mCurrentDefaultIconName ) ) + { + return mIcons.get( mCurrentDefaultIconName ); + } + + /* Return the real default */ + return mIcons.get( DEFAULT_ICON ); + } + + /** + * Returns the named map icon, or creates it with the default path + * @param name - name of the icon + * @param defaultPath - path to icon image file + * @return - map icon + */ + public MapIcon getMapIcon( String name, String defaultPath ) + { + MapIcon icon = mSettings.getMapIcon( name ); + + if( icon == null ) + { + icon = new MapIcon( name, defaultPath ); + + addSetting( icon ); + } + + return icon; + } + + public DefaultIcon getDefaultIcon() + { + DefaultIcon icon = mSettings.getDefaultIcon(); + + if( icon == null ) + { + icon = new DefaultIcon( DEFAULT_ICON ); + + addSetting( icon ); + } + + return icon; + } + + public void setDefaultIcon( MapIcon icon ) + { + if( mIcons.containsValue( icon ) ) + { + MapIcon currentDefault = mIcons.get( mCurrentDefaultIconName ); + + currentDefault.setDefaultIcon( false ); + + icon.setDefaultIcon( true ); + + mCurrentDefaultIconName = icon.getName(); + + DefaultIcon defaultIconSetting = getDefaultIcon(); + + defaultIconSetting.setName( icon.getName() ); + + save(); + + broadcastSettingChange( defaultIconSetting ); + } + } + + /** + * Adds the setting and stores the set of settings + * @param setting + */ + private void addSetting( Setting setting ) + { + mSettings.addSetting( setting ); + + save(); + + broadcastSettingChange( setting ); + } + + public ArrayList getTunerConfigurations( TunerType type ) + { + ArrayList configs = getSettings() + .getTunerConfigurations( type ); + + if( configs.isEmpty() ) + { + configs.add( addNewTunerConfiguration( type, "Default" ) ); + } + + return configs; + } + + public void deleteTunerConfiguration( TunerConfiguration config ) + { + getSettings().removeTunerConfiguration( config ); + save(); + } + + public TunerConfiguration addNewTunerConfiguration( TunerType type, + String name ) + { + switch( type ) + { + case ELONICS_E4000: + E4KTunerConfiguration e4KConfig = + new E4KTunerConfiguration( name ); + + getSettings().addTunerConfiguration( e4KConfig ); + + save(); + return e4KConfig; + case FUNCUBE_DONGLE_PRO: + FCD1TunerConfiguration config = + new FCD1TunerConfiguration( name ); + + getSettings().addTunerConfiguration( config ); + + save(); + + return config; + case FUNCUBE_DONGLE_PRO_PLUS: + FCD2TunerConfiguration configPlus = + new FCD2TunerConfiguration( name ); + + getSettings().addTunerConfiguration( configPlus ); + + save(); + + return configPlus; + case RAFAELMICRO_R820T: + R820TTunerConfiguration r820TConfig = + new R820TTunerConfiguration( name ); + + getSettings().addTunerConfiguration( r820TConfig ); + + save(); + + return r820TConfig; + default: + throw new IllegalArgumentException( "TunerConfiguration" + + "Directory - tuner type is unrecognized [" + + type.toString() + "]" ); + } + } + + public TunerConfigurationAssignment getSelectedTunerConfiguration( + TunerType type, String address ) + { + return mSettings.getConfigurationAssignment( type, address ); + } + + public void setSelectedTunerConfiguration( TunerType type, + String address, TunerConfiguration config ) + { + mSettings.setConfigurationAssignment( type, address, config.getName() ); + + save(); + } + + public MapViewSetting getMapViewSetting( String name, GeoPosition position, int zoom ) + { + MapViewSetting loc = mSettings.getMapViewSetting( name ); + + if( loc != null ) + { + return loc; + } + else + { + MapViewSetting newLoc = new MapViewSetting( name, position, zoom ); + + addSetting( newLoc ); + + return newLoc; + } + } + + public void setMapViewSetting( String name, GeoPosition position, int zoom ) + { + MapViewSetting loc = getMapViewSetting( name, position, zoom ); + + loc.setGeoPosition( position ); + loc.setZoom( zoom ); + + save(); + } + + public void save() + { + JAXBContext context = null; + + SystemProperties props = SystemProperties.getInstance(); + + Path settingsPath = props.getApplicationFolder( "settings" ); + + String settingsDefault = props.get( "settings.defaultFilename", + "settings.xml" ); + + String settingsCurrent = props.get( "settings.currentFilename", + settingsDefault ); + + Path filePath = settingsPath.resolve( settingsCurrent ); + + File outputFile = new File( filePath.toString() ); + + try + { + if( !outputFile.exists() ) + { + outputFile.createNewFile(); + } + } + catch( Exception e ) + { + Log.error( "SettingsManager - couldn't create file to save " + + "settings [" + filePath.toString() + "]" ); + } + + OutputStream out = null; + + try + { + out = new FileOutputStream( outputFile ); + + try + { + context = JAXBContext.newInstance( Settings.class ); + + Marshaller m = context.createMarshaller(); + + m.setProperty( Marshaller.JAXB_FORMATTED_OUTPUT, true ); + + m.marshal( mSettings, out ); + } + catch ( JAXBException e ) + { + Log.error( "SettingsManager - jaxb exception while saving " + + "settings: " + e.getLocalizedMessage() ); + } + } + catch ( Exception e ) + { + Log.error( "SettingsManager - coulcn't open outputstream to " + + "save settings [" + filePath.toString() + "]" ); + } + finally + { + if( out != null ) + { + try + { + out.close(); + } + catch ( IOException e ) + { + e.printStackTrace(); + } + } + } + } + + /** + * Erases current settings and loads settings from the settingsPath filename, + * if it exists. + */ + public void load( Path settingsPath ) + { + if( Files.exists( settingsPath ) ) + { + Log.info( "SettingsManager - loading settings file [" + + settingsPath.toString() + "]" ); + + JAXBContext context = null; + + InputStream in = null; + + try + { + in = new FileInputStream( settingsPath.toString() ); + + try + { + context = JAXBContext.newInstance( Settings.class ); + + Unmarshaller m = context.createUnmarshaller(); + + mSettings = (Settings)m.unmarshal( in ); + } + catch ( JAXBException e ) + { + Log.error( "SettingsManager - jaxb exception while loading " + + "settings: " + e.getLocalizedMessage() ); + + e.printStackTrace(); + } + } + catch ( Exception e ) + { + Log.error( "SettingsManager - coulcn't open inputstream to " + + "load settings [" + settingsPath.toString() + "]" ); + } + finally + { + if( in != null ) + { + try + { + in.close(); + } + catch ( IOException e ) + { + Log.error( "SettingsManager - exception while closing " + + "the settings file inputstream reader - " + + e.getLocalizedMessage() ); + } + } + } + } + else + { + Log.info( "SettingsManager - settings does not exist [" + + settingsPath.toString() + "]" ); + } + + if( mSettings == null ) + { + mSettings = new Settings(); + } + } + + public void broadcastSettingChange( Setting setting ) + { + Iterator it = mListeners.iterator(); + + while( it.hasNext() ) + { + SettingChangeListener listener = it.next(); + + if( listener == null ) + { + it.remove(); + } + else + { + listener.settingChanged( setting ); + } + } + } + + public void broadcastSettingDeleted( Setting setting ) + { + Iterator it = mListeners.iterator(); + + while( it.hasNext() ) + { + SettingChangeListener listener = it.next(); + + if( listener == null ) + { + it.remove(); + } + else + { + listener.settingDeleted( setting ); + } + } + } + + public void addListener( SettingChangeListener listener ) + { + mListeners.add( listener ); + } + + public void removeListener( SettingChangeListener listener ) + { + mListeners.remove( listener ); + } +} diff --git a/src/source/BufferReceiver.java b/src/source/BufferReceiver.java new file mode 100644 index 00000000..c01d7f54 --- /dev/null +++ b/src/source/BufferReceiver.java @@ -0,0 +1,27 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package source; + +public interface BufferReceiver { + + /** + * Method for receiving a buffer of bytes + * @param buffer + */ + public void receive( byte[] buffer ); +} diff --git a/src/source/ComplexSource.java b/src/source/ComplexSource.java new file mode 100644 index 00000000..c1b697b7 --- /dev/null +++ b/src/source/ComplexSource.java @@ -0,0 +1,15 @@ +package source; + +import java.util.List; + +import sample.Provider; +import sample.complex.ComplexSample; + +public abstract class ComplexSource extends Source + implements Provider> +{ + public ComplexSource( String name ) + { + super( name, SampleType.COMPLEX ); + } +} diff --git a/src/source/EmptySourceEditor.java b/src/source/EmptySourceEditor.java new file mode 100644 index 00000000..4f711ca2 --- /dev/null +++ b/src/source/EmptySourceEditor.java @@ -0,0 +1,44 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package source; + +import javax.swing.JLabel; + +import source.config.SourceConfiguration; + +public class EmptySourceEditor extends SourceEditor +{ + private static final long serialVersionUID = 1L; + + public EmptySourceEditor( SourceConfiguration config ) + { + super( config ); + + initGUI(); + } + + public void reset() {} + + public void save() {} + + private void initGUI() + { + JLabel selectLabel = new JLabel( "Please select a source" ); + add( selectLabel ); + } +} diff --git a/src/source/FloatArraySource.java b/src/source/FloatArraySource.java new file mode 100644 index 00000000..da84e6fa --- /dev/null +++ b/src/source/FloatArraySource.java @@ -0,0 +1,12 @@ +package source; + +import sample.Provider; +import source.Source.SampleType; + +public abstract class FloatArraySource extends Source implements Provider +{ + public FloatArraySource( String name ) + { + super( name, SampleType.FLOAT ); + } +} diff --git a/src/source/FloatSource.java b/src/source/FloatSource.java new file mode 100644 index 00000000..6e7d849b --- /dev/null +++ b/src/source/FloatSource.java @@ -0,0 +1,11 @@ +package source; + +import sample.Provider; + +public abstract class FloatSource extends Source implements Provider +{ + public FloatSource( String name ) + { + super( name, SampleType.FLOAT ); + } +} diff --git a/src/source/SampleProvider.java b/src/source/SampleProvider.java new file mode 100644 index 00000000..d9af4796 --- /dev/null +++ b/src/source/SampleProvider.java @@ -0,0 +1,28 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package source; + +import source.Source.SampleType; + +public interface SampleProvider +{ + /** + * Real or Complex + */ + public abstract SampleType getSampleType(); +} diff --git a/src/source/Source.java b/src/source/Source.java new file mode 100644 index 00000000..256e258d --- /dev/null +++ b/src/source/Source.java @@ -0,0 +1,56 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package source; + +/** + * Abstract class to define the minimum functionality of a sample data provider. + */ +public abstract class Source +{ + protected String mName; + protected SampleType mSampleType; + + public enum SampleType { COMPLEX, FLOAT } + + public Source( String name, SampleType sampleType ) + { + mName = name; + mSampleType = sampleType; + } + + public String getName() + { + return mName; + } + + public abstract int getSampleRate() throws SourceException; + + public abstract int getFrequency() throws SourceException; + + public abstract void dispose(); + + public SampleType getSampleType() + { + return mSampleType; + } + + public void setSampleType( SampleType sampleType ) + { + mSampleType = sampleType; + } +} diff --git a/src/source/SourceComponentEditor.java b/src/source/SourceComponentEditor.java new file mode 100644 index 00000000..bb5c0606 --- /dev/null +++ b/src/source/SourceComponentEditor.java @@ -0,0 +1,119 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package source; + + +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; + +import javax.swing.BorderFactory; +import javax.swing.DefaultComboBoxModel; +import javax.swing.JComboBox; + +import source.config.SourceConfigFactory; +import source.config.SourceConfiguration; +import controller.channel.AbstractChannelEditor; +import controller.channel.ChannelNode; + +public class SourceComponentEditor extends AbstractChannelEditor +{ + private static final long serialVersionUID = 1L; + + private JComboBox mComboSources; + private SourceEditor mEditor; + + public SourceComponentEditor( ChannelNode channelNode ) + { + super( channelNode ); + + mComboSources = new JComboBox(); + mComboSources.setModel( new DefaultComboBoxModel( SourceType.values() ) ); + mComboSources.addActionListener( new ActionListener() + { + @Override + public void actionPerformed( ActionEvent e ) + { + SourceType selected = mComboSources.getItemAt( mComboSources.getSelectedIndex() ); + + if( selected != null ) + { + SourceConfiguration config; + + if( selected == mChannelNode.getChannel() + .getSourceConfiguration().getSourceType() ) + { + config = mChannelNode.getChannel() + .getSourceConfiguration(); + } + else + { + config = SourceConfigFactory + .getSourceConfiguration( selected ); + } + + //Remove the existing one + if( mEditor != null ) + { + remove( mEditor ); + } + + //Change to the new one + mEditor = SourceEditorFactory.getPanel( config ); + + //Add it to the jpanel + add( mEditor, "span 2" ); + + revalidate(); + repaint(); + } + } + }); + + add( mComboSources, "wrap" ); + + reset(); + } + + public void reset() + { + javax.swing.SwingUtilities.invokeLater(new Runnable() + { + + @Override + public void run() { + mComboSources.setSelectedItem( + mChannelNode.getChannel() + .getSourceConfiguration().getSourceType() ); + + mComboSources.requestFocus(); + + mComboSources.requestFocusInWindow(); + } + }); + } + + @Override + public void save() + { + mEditor.save(); + + //Let the calling save method send the config change event + mChannelNode.getChannel() + .setSourceConfiguration( mEditor.getConfig(), false ); + } +} diff --git a/src/source/SourceEditor.java b/src/source/SourceEditor.java new file mode 100644 index 00000000..2d9ea0b3 --- /dev/null +++ b/src/source/SourceEditor.java @@ -0,0 +1,38 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package source; + +import source.config.SourceConfiguration; +import controller.Editor; + +public abstract class SourceEditor extends Editor +{ + private static final long serialVersionUID = 1L; + + protected SourceConfiguration mConfig; + + public SourceEditor( SourceConfiguration config ) + { + mConfig = config; + } + + public SourceConfiguration getConfig() + { + return mConfig; + } +} diff --git a/src/source/SourceEditorFactory.java b/src/source/SourceEditorFactory.java new file mode 100644 index 00000000..af641318 --- /dev/null +++ b/src/source/SourceEditorFactory.java @@ -0,0 +1,46 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package source; + +import source.config.SourceConfiguration; +import source.mixer.MixerEditor; +import source.tuner.TunerEditor; + +public class SourceEditorFactory +{ + public static SourceEditor getPanel( SourceConfiguration config ) + { + SourceEditor configuredPanel; + + switch( config.getSourceType() ) + { + case MIXER: + configuredPanel = new MixerEditor( config ); + break; + case TUNER: + configuredPanel = new TunerEditor( config ); + break; + case NONE: + default: + configuredPanel = new EmptySourceEditor( config ); + break; + } + + return configuredPanel; + } +} diff --git a/src/source/SourceException.java b/src/source/SourceException.java new file mode 100644 index 00000000..2dc02443 --- /dev/null +++ b/src/source/SourceException.java @@ -0,0 +1,33 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package source; + +public class SourceException extends Exception +{ + private static final long serialVersionUID = 1L; + + public SourceException( String message ) + { + this( message, null ); + } + + public SourceException( String message, Throwable cause ) + { + super( message, cause ); + } +} diff --git a/src/source/SourceManager.java b/src/source/SourceManager.java new file mode 100644 index 00000000..a518ae2a --- /dev/null +++ b/src/source/SourceManager.java @@ -0,0 +1,66 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package source; + +import source.mixer.MixerManager; +import controller.ResourceManager; +import controller.channel.ProcessingChain; + +public class SourceManager { + /** + * NOTE: each source will auto-start once a listener registers on it to + * receive samples and should auto-stop once a listener de-registers + * + * Each channel object will handle the starting and stopping of each channel + * processing chain. + * + * Source Manager will reach out to the MixerManager or the TunerManager + * to get source objects to pass off to the channel object, as requested. + */ + + private ResourceManager mResourceManager; + + public SourceManager( ResourceManager resourceManager ) + { + mResourceManager = resourceManager; + } + + public Source getSource( ProcessingChain channel ) + { + Source retVal = null; + + if( channel != null ) + { + switch( channel.getChannel() + .getSourceConfiguration().getSourceType() ) + { + case MIXER: + retVal = MixerManager.getInstance().getSource( channel ); + break; + case TUNER: + retVal = mResourceManager.getTunerManager().getSource( channel ); + break; + case NONE: + default: + break; + } + } + + return retVal; + } +} diff --git a/src/source/SourceType.java b/src/source/SourceType.java new file mode 100644 index 00000000..d8947f7d --- /dev/null +++ b/src/source/SourceType.java @@ -0,0 +1,44 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package source; + +public enum SourceType +{ + NONE( "No Source" ), + MIXER( "Mixer/Sound Card" ), + TUNER( "Tuner" ); +// WAVE( "Wave File" ); + + private String mDisplayString; + + SourceType( String displayString ) + { + mDisplayString = displayString; + } + + public String getDisplayString() + { + return mDisplayString; + } + + @Override + public String toString() + { + return mDisplayString; + } +} diff --git a/src/source/config/SourceConfigFactory.java b/src/source/config/SourceConfigFactory.java new file mode 100644 index 00000000..9784548e --- /dev/null +++ b/src/source/config/SourceConfigFactory.java @@ -0,0 +1,50 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package source.config; + +import source.SourceType; + +public class SourceConfigFactory +{ + public static SourceConfiguration getDefaultSourceConfiguration() + { + return getSourceConfiguration( SourceType.NONE ); + } + + public static SourceConfiguration + getSourceConfiguration( SourceType source ) + { + SourceConfiguration retVal; + + switch( source ) + { + case MIXER: + retVal = new SourceConfigMixer(); + break; + case TUNER: + retVal = new SourceConfigTuner(); + break; + case NONE: + default: + retVal = new SourceConfigNone(); + break; + } + + return retVal; + } +} diff --git a/src/source/config/SourceConfigMixer.java b/src/source/config/SourceConfigMixer.java new file mode 100644 index 00000000..1074b3cf --- /dev/null +++ b/src/source/config/SourceConfigMixer.java @@ -0,0 +1,73 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package source.config; + +import javax.xml.bind.annotation.XmlAttribute; + +import source.SourceType; +import source.mixer.MixerChannel; + +public class SourceConfigMixer extends SourceConfiguration +{ + protected String mMixer; + protected MixerChannel mChannel = MixerChannel.LEFT; //default + + public SourceConfigMixer() + { + super( SourceType.MIXER ); + } + + @Override + public String getDescription() + { + return getName(); + } + + public String getName() + { + StringBuilder sb = new StringBuilder(); + + sb.append( mMixer ); + sb.append( "-" ); + sb.append( mChannel.toString() ); + + return sb.toString(); + } + + @XmlAttribute( name= "mixer" ) + public String getMixer() + { + return mMixer; + } + + public void setMixer( String mixer ) + { + mMixer = mixer; + } + + @XmlAttribute( name = "channel" ) + public MixerChannel getChannel() + { + return mChannel; + } + + public void setChannel( MixerChannel channel ) + { + mChannel = channel; + } +} diff --git a/src/source/config/SourceConfigNone.java b/src/source/config/SourceConfigNone.java new file mode 100644 index 00000000..57c8f6d0 --- /dev/null +++ b/src/source/config/SourceConfigNone.java @@ -0,0 +1,34 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package source.config; + +import source.SourceType; + +public class SourceConfigNone extends SourceConfiguration +{ + public SourceConfigNone() + { + super( SourceType.NONE ); + } + + @Override + public String getDescription() + { + return "No Source"; + } +} diff --git a/src/source/config/SourceConfigTuner.java b/src/source/config/SourceConfigTuner.java new file mode 100644 index 00000000..6273318a --- /dev/null +++ b/src/source/config/SourceConfigTuner.java @@ -0,0 +1,53 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package source.config; + +import java.text.DecimalFormat; + +import javax.xml.bind.annotation.XmlAttribute; + +import source.SourceType; + +public class SourceConfigTuner extends SourceConfiguration +{ + private static DecimalFormat sFORMAT = new DecimalFormat( "0.00000" ); + + private int mFrequency = 0; + + public SourceConfigTuner() + { + super( SourceType.TUNER ); + } + + @XmlAttribute( name = "frequency" ) + public int getFrequency() + { + return mFrequency; + } + + public void setFrequency( int frequency ) + { + mFrequency = frequency; + } + + @Override + public String getDescription() + { + return sFORMAT.format( (double)mFrequency / 1000000.0d ) + " MHz"; + } +} diff --git a/src/source/config/SourceConfiguration.java b/src/source/config/SourceConfiguration.java new file mode 100644 index 00000000..a0b9433a --- /dev/null +++ b/src/source/config/SourceConfiguration.java @@ -0,0 +1,59 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package source.config; + +import javax.xml.bind.annotation.XmlAttribute; +import javax.xml.bind.annotation.XmlRootElement; +import javax.xml.bind.annotation.XmlSeeAlso; + +import source.SourceType; +import controller.config.Configuration; + +@XmlSeeAlso( { SourceConfigMixer.class, SourceConfigNone.class, + SourceConfigTuner.class } ) +@XmlRootElement( name = "source_configuration" ) +public class SourceConfiguration extends Configuration +{ + protected SourceType mSourceType; + + public SourceConfiguration() + { + this( SourceType.NONE ); + } + + public SourceConfiguration( SourceType source ) + { + mSourceType = source; + } + + @XmlAttribute( name = "source_type" ) + public SourceType getSourceType() + { + return mSourceType; + } + + public void setSourceType( SourceType source ) + { + mSourceType = source; + } + + public String getDescription() + { + return "No Source"; + } +} diff --git a/src/source/mixer/ChannelShortAdapter.java b/src/source/mixer/ChannelShortAdapter.java new file mode 100644 index 00000000..0b6086a5 --- /dev/null +++ b/src/source/mixer/ChannelShortAdapter.java @@ -0,0 +1,85 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package source.mixer; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; + +import source.Source.SampleType; + +/** + * Converts 16-bit little endian byte data into an array of floats + */ +public class ChannelShortAdapter extends SampleAdapter +{ + private ByteOrder mByteOrder = ByteOrder.LITTLE_ENDIAN; + private MixerChannel mMixerChannel; + + public ChannelShortAdapter( MixerChannel channel ) + { + /* Only use with LEFT/RIGHT channels */ + assert( channel != MixerChannel.MONO ); + + mMixerChannel = channel; + } + + @Override + public Float[] convert( byte[] samples ) + { + Float[] processed = new Float[ samples.length / 4 ]; + + int pointer = 0; + + /* Wrap byte array in a byte buffer so we can process them as shorts */ + ByteBuffer buffer = ByteBuffer.wrap( samples ); + + /* Set endian to correct byte ordering */ + buffer.order( mByteOrder ); + + while( buffer.hasRemaining() ) + { + if( mMixerChannel == MixerChannel.LEFT ) + { + processed[ pointer ] = (float)buffer.getShort(); + + /* Throw away the right channel */ + buffer.getShort(); + } + else + { + /* Throw away the left channel */ + buffer.getShort(); + + processed[ pointer ] = (float)buffer.getShort(); + } + + pointer++; + } + + return processed; + } + + /** + * Set byte interpretation to little or big endian. Defaults to LITTLE + * endian + */ + public void setByteOrder( ByteOrder order ) + { + mByteOrder = order; + } +} diff --git a/src/source/mixer/MixerChannel.java b/src/source/mixer/MixerChannel.java new file mode 100644 index 00000000..7b45ae38 --- /dev/null +++ b/src/source/mixer/MixerChannel.java @@ -0,0 +1,23 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package source.mixer; + +public enum MixerChannel +{ + LEFT,RIGHT,MONO; +} diff --git a/src/source/mixer/MixerEditor.java b/src/source/mixer/MixerEditor.java new file mode 100644 index 00000000..7c23c2f2 --- /dev/null +++ b/src/source/mixer/MixerEditor.java @@ -0,0 +1,149 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package source.mixer; + +import java.util.Set; + +import javax.sound.sampled.Mixer; +import javax.swing.DefaultComboBoxModel; +import javax.swing.JComboBox; +import javax.swing.JLabel; + +import source.SourceEditor; +import source.config.SourceConfigMixer; +import source.config.SourceConfiguration; + +public class MixerEditor extends SourceEditor +{ + private static final long serialVersionUID = 1L; + private JComboBox mComboMixers; + private JComboBoxmComboChannels; + protected Mixer.Info mSelectedMixer = null; + + + public MixerEditor( SourceConfiguration config ) + { + super( config ); + + initGUI(); + } + + private void initGUI() + { + JLabel mixerLabel = new JLabel( "Mixer:" ); + add( mixerLabel, "align right" ); + + mComboMixers = new JComboBox(); + + add( mComboMixers, "wrap" ); + + resetMixers(); + + JLabel channelLabel = new JLabel( "Channel:" ); + add( channelLabel, "align right" ); + + mComboChannels = new JComboBox(); + + add( mComboChannels, "wrap" ); + + resetChannels(); + } + + public void reset() + { + resetMixers(); + resetChannels(); + } + + public void save() + { + SourceConfigMixer config = (SourceConfigMixer)mConfig; + + String mixer = mComboMixers.getItemAt( mComboMixers.getSelectedIndex() ); + + if( mixer != null ) + { + config.setMixer( mixer ); + } + + MixerChannel channel = mComboChannels.getItemAt( + mComboChannels.getSelectedIndex() ); + + if( channel != null ) + { + if( config.getChannel() != channel ) + { + config.setChannel( channel ); + } + } + } + + private void resetMixers() + { + + javax.swing.SwingUtilities.invokeLater(new Runnable() + { + @Override + public void run() + { + Set mixers = MixerManager.getInstance().getMixers(); + String[] mixerStrings = mixers.toArray( new String[ mixers.size() ] ); + + mComboMixers.setModel( + new DefaultComboBoxModel( mixerStrings ) ); + + SourceConfigMixer config = (SourceConfigMixer)mConfig; + String mixer = config.getMixer(); + + if( mixer != null && mixers.contains( mixer ) ) + { + mComboMixers.setSelectedItem( mixer ); + } + else + { + //Mixer either hasn't been set yet, or it was set to a + //mixer that is no longer available. Set the mixer to the + //first item, and then store the value + if( mixers.size() > 0 ) + { + mComboMixers.setSelectedIndex( 0 ); + + config.setMixer( mComboMixers.getItemAt( 0 ) ); + } + } + } + }); + } + + private void resetChannels() + { + javax.swing.SwingUtilities.invokeLater(new Runnable() + { + @Override + public void run() + { + mComboChannels.setModel( + new DefaultComboBoxModel( MixerChannel.values() ) ); + + MixerChannel channel = ((SourceConfigMixer)mConfig).getChannel(); + + mComboChannels.setSelectedItem( channel ); + } + }); + } +} diff --git a/src/source/mixer/MixerManager.java b/src/source/mixer/MixerManager.java new file mode 100644 index 00000000..86fe44e7 --- /dev/null +++ b/src/source/mixer/MixerManager.java @@ -0,0 +1,258 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package source.mixer; + +import java.util.Collection; +import java.util.HashMap; +import java.util.Set; + +import javax.sound.sampled.AudioFormat; +import javax.sound.sampled.AudioSystem; +import javax.sound.sampled.DataLine; +import javax.sound.sampled.Line; +import javax.sound.sampled.Mixer; +import javax.sound.sampled.TargetDataLine; + +import log.Log; +import source.config.SourceConfigMixer; +import source.config.SourceConfiguration; +import source.tuner.MixerTunerDataLine; +import source.tuner.MixerTunerType; +import controller.channel.ProcessingChain; + +public class MixerManager +{ + private static MixerManager sInstance = null; + + private HashMap mMixerLines = + new HashMap(); + + private HashMap mMixerTuners = + new HashMap(); + + public static AudioFormat MONO = new AudioFormat( 48000, //SampleRate + 16, //Sample Size + 1, //Channels + true, //Signed + false ); //Little Endian + + public static AudioFormat STEREO = new AudioFormat( 48000, //SampleRate + 16, //Sample Size + 2, //Channels + true, //Signed + false ); //Little Endian + + private MixerManager() + { + loadMixers(); + } + + public static MixerManager getInstance() + { + if( sInstance == null ) + { + sInstance = new MixerManager(); + } + + return sInstance; + } + + public MixerSource getSource( ProcessingChain channel ) + { + MixerSource retVal = null; + + SourceConfiguration config = + channel.getChannel().getSourceConfiguration(); + + if( config instanceof SourceConfigMixer ) + { + SourceConfigMixer mixerConfig = (SourceConfigMixer)config; + + //Get the name of the requested mixer + String mixerName = mixerConfig.getMixer(); + + if( mixerName != null ) + { + TargetDataLine tdl = mMixerLines.get( mixerName ); + + if( tdl != null ) + { + switch( mixerConfig.getChannel() ) + { + case MONO: + retVal = new MixerSource( mMixerLines.get( mixerName ), + MONO, + mixerName, + new ShortAdapter() ); + break; + case LEFT: + case RIGHT: + retVal = new MixerSource( mMixerLines.get( mixerName ), + STEREO, + mixerName, + new ChannelShortAdapter( mixerConfig.getChannel() ) ); + break; + } + } + } + } + + return retVal; + } + + public Set getMixers() + { + return mMixerLines.keySet(); + } + + public Collection getMixerTunerDataLines() + { + return mMixerTuners.values(); + } + + private void loadMixers() + { + StringBuilder sb = new StringBuilder(); + + sb.append( "MixerManager - loading system mixer devices\n" ); + + for( Mixer.Info mixerInfo: AudioSystem.getMixerInfo() ) + { + //Sort between the mixers and the tuner mixers, and load each + MixerTunerType mixerTunerType = MixerTunerType.getMixerTunerType( mixerInfo ); + + if( mixerTunerType == MixerTunerType.UNKNOWN ) + { + Mixer mixer = AudioSystem.getMixer( mixerInfo ); + + if( mixer != null ) + { + TargetDataLine tdl = getTargetDataLine( mixerInfo, STEREO ); + + if( tdl != null ) + { + mMixerLines.put( mixerInfo.getName(), tdl ); + + sb.append( "\t[LOADED] Mixer:" + mixerInfo.getName() + "\n" ); + } + else + { + sb.append( "\t[NOT LOADED] Mixer:" + mixerInfo.getName() + " - audio format not supported\n" ); + } + } + else + { + sb.append( "\t[NOT LOADED] Mixer:" + mixerInfo.getName() + " - couldn't get mixer\n" ); + } + } + else //Process as a Mixer-based tuner data line + { + TargetDataLine tdl = + getTargetDataLine( mixerInfo, + mixerTunerType.getAudioFormat() ); + + if( tdl != null ) + { + switch( mixerTunerType ) + { + case FUNCUBE_DONGLE_PRO: + case FUNCUBE_DONGLE_PRO_PLUS: + mMixerTuners.put( mixerInfo.getName(), + new MixerTunerDataLine( tdl, mixerTunerType ) ); + sb.append( "\t[LOADED] FunCube Dongle Mixer Tuner:" + mixerInfo.getName() + "[" + mixerTunerType.getDisplayString() + "]\n" ); + break; + case UNKNOWN: + default: + sb.append( "\t[NOT LOADED] Tuner:" + mixerInfo.getName() + " - unrecognized tuner type\n" ); + break; + } + } + else + { + sb.append( "\t[NOT LOADED] Tuner:" + mixerInfo.getName() + " - couldn't get target data line\n" ); + } + } + } + + Log.info( sb.toString() ); + } + + private TargetDataLine getTargetDataLine( Mixer.Info mixerInfo, AudioFormat format ) + { + TargetDataLine retVal = null; + + Mixer mixer = AudioSystem.getMixer( mixerInfo ); + + if( mixer != null ) + { + try + { + DataLine.Info datalineInfo= + new DataLine.Info(TargetDataLine.class, format ); + + retVal = (TargetDataLine) mixer.getLine( datalineInfo ); + } + catch( Exception e ) + { + //Do nothing ... we couldn't get the TDL + } + } + + return retVal; + } + + public static String getMixerDevices() + { + StringBuilder sb = new StringBuilder(); + + for( Mixer.Info mixerInfo: AudioSystem.getMixerInfo() ) + { + sb.append( "\n--------------------------------------------------" ); + sb.append( "\nMIXER name:" + mixerInfo.getName() + + "\n desc:" + mixerInfo.getDescription() + + "\n vendor:" + mixerInfo.getVendor() + + "\n version:" + mixerInfo.getVersion() + + "\n" ); + + Mixer mixer = AudioSystem.getMixer( mixerInfo ); + + Line.Info[] sourceLines = mixer.getSourceLineInfo(); + + for( Line.Info lineInfo: sourceLines ) + { + sb.append( " SOURCE LINE desc:" + lineInfo.toString() + + "\n class:" + lineInfo.getClass() + + "\n lineclass:" + lineInfo.getLineClass() + + "\n" ); + } + + Line.Info[] targetLines = mixer.getTargetLineInfo(); + + for( Line.Info lineInfo: targetLines ) + { + sb.append( " TARGET LINE desc:" + lineInfo.toString() + + "\n class:" + lineInfo.getClass() + + "\n lineclass:" + lineInfo.getLineClass() + + "\n" ); + } + } + + return sb.toString(); + } + +} diff --git a/src/source/mixer/MixerSource.java b/src/source/mixer/MixerSource.java new file mode 100644 index 00000000..cfb6c205 --- /dev/null +++ b/src/source/mixer/MixerSource.java @@ -0,0 +1,274 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package source.mixer; + +import java.util.Arrays; +import java.util.Iterator; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.atomic.AtomicBoolean; + +import javax.sound.sampled.AudioFormat; +import javax.sound.sampled.AudioSystem; +import javax.sound.sampled.LineUnavailableException; +import javax.sound.sampled.TargetDataLine; + +import log.Log; +import sample.Listener; +import source.FloatArraySource; +import source.SourceException; + +public class MixerSource extends FloatArraySource +{ + private int mFrequency = 0; + private int mBufferSize = 16384; + + private BufferReader mBufferReader = new BufferReader(); + private TargetDataLine mTargetDataLine; + private AudioFormat mAudioFormat; + private String mDisplayName; + private int mBytesPerFrame = 0; + private SampleAdapter mSampleAdapter; + + CopyOnWriteArrayList> mSampleListeners = + new CopyOnWriteArrayList>(); + + /** + * Complex Mixer Source - constructs a reader on the mixer/sound card target + * data line using the specified audio format (sample size, sample rate ) + * and broadcasts float[] sample data to all registered listeners. Reads + * sample buffers sized to 10% of the sample rate specified in audio format. + * + * @param targetDataLine - mixer or sound card to be used + * + * @param format - audio format + * + * @param name - token name to use for this source + * + * @param sampleAdapter - adapter to convert byte array data read from the + * mixer into float array data. Can optionally invert the channel data if + * the left/right stereo channels are inverted. + */ + public MixerSource( TargetDataLine targetDataLine, + AudioFormat format, + String name, + SampleAdapter sampleAdapter ) + { + super( name ); + + mTargetDataLine = targetDataLine; + mAudioFormat = format; + mSampleAdapter = sampleAdapter; + } + + public void setListener( Listener listener ) + { + mSampleListeners.add( listener ); + + /* If this is the first listener, start the reader thread */ + if( !mBufferReader.isRunning() ) + { + Thread thread = new Thread( mBufferReader ); + thread.setName( getName() + " Sample Reader" ); + thread.start(); + } + } + + public void removeListener( Listener listener ) + { + mSampleListeners.remove( listener ); + + /* If this is the laster listener, stop the reader thread */ + if( mSampleListeners.isEmpty() ) + { + mBufferReader.stop(); + } + } + + public void broadcast( Float[] samples ) + { + Iterator> it = mSampleListeners.iterator(); + + while( it.hasNext() ) + { + Listener next = it.next(); + + /* if this is the last (or only) listener, send him the original + * buffer, otherwise send him a copy of the buffer */ + if( it.hasNext() ) + { + next.receive( Arrays.copyOf( samples, samples.length ) ); + } + else + { + next.receive( samples ); + } + } + } + + @Override + public String toString() + { + return mDisplayName; + } + + public int getSampleRate() + { + if( mTargetDataLine != null ) + { + return (int)mTargetDataLine.getFormat().getSampleRate(); + } + else + { + return 0; + } + } + + /** + * Returns the frequency of this source. Default is 0. + */ + public int getFrequency() throws SourceException + { + return mFrequency; + } + + /** + * Specify the frequency that will be returned from this source. This may + * be useful if you are streaming an external audio source in through the + * sound card and you want to specify a frequency for that source + */ + public void setFrequency( int frequency ) + { + mFrequency = frequency; + } + + /** + * Reader thread. Performs blocking read against the mixer target data + * line, converts the samples to an array of floats using the specified + * adapter. Dispatches float arrays to all registered listeners. + */ + public class BufferReader implements Runnable + { + private AtomicBoolean mRunning = new AtomicBoolean(); + + @Override + public void run() + { + if( mRunning.compareAndSet( false, true ) ) + { + if( mTargetDataLine == null ) + { + mRunning.set( false ); + + Log.error( "ComplexMixerSource - mixer target data line" + + " is null" ); + } + else + { + mBytesPerFrame = mAudioFormat.getSampleSizeInBits() / 8; + + if( mBytesPerFrame == AudioSystem.NOT_SPECIFIED ) + { + mBytesPerFrame = 2; + } + + /* Set buffer size to 1/10 second of samples */ + mBufferSize = (int)( mAudioFormat.getSampleRate() + * 0.05 ) * mBytesPerFrame; + + /* We'll reuse the same buffer for each read */ + byte[] buffer = new byte[ mBufferSize ]; + + try + { + mTargetDataLine.open( mAudioFormat ); + + mTargetDataLine.start(); + } + catch ( LineUnavailableException e ) + { + Log.error( "ComplexMixerSource - mixer target data line" + + "not available to read data from - " + + e.getLocalizedMessage() ); + + mRunning.set( false ); + } + + while( mRunning.get() ) + { + try + { + /* Blocking read - waits until the buffer fills */ + mTargetDataLine.read( buffer, 0, buffer.length ); + + /* Convert samples to float array */ + Float[] convertedSamples = + mSampleAdapter.convert( buffer ); + + /* Dispatch samples to registered listeners */ + broadcast( convertedSamples ); + } + catch ( Exception e ) + { + Log.error( "ComplexMixerSource - error while reading" + + "from the mixer target data line - " + + e.getLocalizedMessage() ); + + mRunning.set( false ); + } + } + } + + /* Close the data line if it is still open */ + if( mTargetDataLine != null && mTargetDataLine.isOpen() ) + { + mTargetDataLine.close(); + } + } + } + + /** + * Stops the reader thread + */ + public void stop() + { + if( mRunning.compareAndSet( true, false ) ) + { + mTargetDataLine.stop(); + mTargetDataLine.close(); + } + } + + /** + * Indicates if the reader thread is running + */ + public boolean isRunning() + { + return mRunning.get(); + } + } + + @Override + public void dispose() + { + if( mBufferReader != null ) + { + mBufferReader.stop(); + mSampleListeners.clear(); + } + } +} diff --git a/src/source/mixer/SampleAdapter.java b/src/source/mixer/SampleAdapter.java new file mode 100644 index 00000000..1d73a0a1 --- /dev/null +++ b/src/source/mixer/SampleAdapter.java @@ -0,0 +1,29 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package source.mixer; + +import source.Source.SampleType; + +/* + * Adapter to convert byte data received from a data stream into float[] + * samples + */ +public abstract class SampleAdapter +{ + public abstract Float[] convert( byte[] samples ); +} diff --git a/src/source/mixer/ShortAdapter.java b/src/source/mixer/ShortAdapter.java new file mode 100644 index 00000000..b8f0bf09 --- /dev/null +++ b/src/source/mixer/ShortAdapter.java @@ -0,0 +1,63 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package source.mixer; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; + +import source.Source.SampleType; + +/** + * Converts 16-bit little endian byte data into an array of floats + */ +public class ShortAdapter extends SampleAdapter +{ + private ByteOrder mByteOrder = ByteOrder.LITTLE_ENDIAN; + + @Override + public Float[] convert( byte[] samples ) + { + Float[] processed = new Float[ samples.length / 2 ]; + + int pointer = 0; + + /* Wrap byte array in a byte buffer so we can process them as shorts */ + ByteBuffer buffer = ByteBuffer.wrap( samples ); + + /* Set endian to correct byte ordering */ + buffer.order( mByteOrder ); + + while( buffer.hasRemaining() ) + { + processed[ pointer ] = (float)buffer.getShort(); + + pointer++; + } + + return processed; + } + + /** + * Set byte interpretation to little or big endian. Defaults to LITTLE + * endian + */ + public void setByteOrder( ByteOrder order ) + { + mByteOrder = order; + } +} diff --git a/src/source/mixer/SystemMixerDevices.java b/src/source/mixer/SystemMixerDevices.java new file mode 100644 index 00000000..9da3584c --- /dev/null +++ b/src/source/mixer/SystemMixerDevices.java @@ -0,0 +1,116 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package source.mixer; + +import java.util.HashMap; + +import javax.sound.sampled.AudioFormat; +import javax.sound.sampled.AudioSystem; +import javax.sound.sampled.DataLine; +import javax.sound.sampled.Line; +import javax.sound.sampled.LineUnavailableException; +import javax.sound.sampled.Mixer; +import javax.sound.sampled.TargetDataLine; + +/** + * THIS CLASS IS SCHEDULED FOR DELETION ***************************** + */ + +public class SystemMixerDevices { + + /** + * Returns a hashmap of mixer names and target data lines that support the + * audio format argument + * + * @param format - requested audio format + * @return - mixer target data lines that support the requested audio format + */ + public static HashMap getMixers( AudioFormat format ) + { + HashMap retVal = new HashMap(); + + DataLine.Info datalineInfo=new DataLine.Info(TargetDataLine.class, format ); + + for( Mixer.Info mixerInfo: AudioSystem.getMixerInfo() ) + { + Mixer mixer = AudioSystem.getMixer( mixerInfo ); + + if( mixer != null ) + { + try + { + TargetDataLine tdl = (TargetDataLine) mixer.getLine( datalineInfo ); + + if( tdl != null ) + { + retVal.put( mixerInfo.getName(), tdl ); + } + } + catch( IllegalArgumentException iae ) + { + //Do nothing ... mixer doesn't support the audio format + } + catch( LineUnavailableException e ) + { + //Do nothing ... mixer doesn't support the audio format + } + } + } + + return retVal; + } + + public static String getSoundDevices() + { + StringBuilder sb = new StringBuilder(); + + for( Mixer.Info mixerInfo: AudioSystem.getMixerInfo() ) + { + sb.append( "\n--------------------------------------------------" ); + sb.append( "\nMIXER name:" + mixerInfo.getName() + + "\n desc:" + mixerInfo.getDescription() + + "\n vendor:" + mixerInfo.getVendor() + + "\n version:" + mixerInfo.getVersion() + + "\n" ); + + Mixer mixer = AudioSystem.getMixer( mixerInfo ); + + Line.Info[] sourceLines = mixer.getSourceLineInfo(); + + for( Line.Info lineInfo: sourceLines ) + { + sb.append( " SOURCE LINE desc:" + lineInfo.toString() + + "\n class:" + lineInfo.getClass() + + "\n lineclass:" + lineInfo.getLineClass() + + "\n" ); + } + + Line.Info[] targetLines = mixer.getTargetLineInfo(); + + for( Line.Info lineInfo: targetLines ) + { + sb.append( " TARGET LINE desc:" + lineInfo.toString() + + "\n class:" + lineInfo.getClass() + + "\n lineclass:" + lineInfo.getLineClass() + + "\n" ); + } + } + + return sb.toString(); + } +} diff --git a/src/source/tuner/DigitalDropChannel.java b/src/source/tuner/DigitalDropChannel.java new file mode 100644 index 00000000..73bbc796 --- /dev/null +++ b/src/source/tuner/DigitalDropChannel.java @@ -0,0 +1,152 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package source.tuner; + +import sample.Broadcaster; +import sample.Listener; +import sample.complex.ComplexSample; +import dsp.filter.ComplexFilter; +import dsp.filter.FilterFactory; +import dsp.filter.Window.WindowType; + +@Deprecated +public class DigitalDropChannel implements Listener, + FrequencyChangeListener +{ + /** + * Implements a frequency translating, decimating FIR filter channelizer. + * + * Uses multiple cascaded half-band low pass filters that each decimate by + * two, to achieve an overall power-of-two decimation factor. + * + * @param decimation - power of 2 decimation factor + * + * @param frequencyShift +/- frequency shift in hertz + */ + + private static int sCHANNEL_RATE = 48000; + private static int sCHANNEL_PASS_FREQUENCY = 12500; + + private NumericallyControlledOscillator mFrequencyTranslator; + + private ComplexFilter[] mFilters; + + private Broadcaster mComplexSampleBroadcaster = + new Broadcaster(); + + private int mSampleRate; + private int mFrequencyShift; + + public DigitalDropChannel( int sampleRate, int frequencyShift ) + { + if( sampleRate % sCHANNEL_RATE != 0 ) + { + throw new IllegalArgumentException( "Sample rate must be an " + + "integer multiple of 48000" ); + } + + mFrequencyShift = frequencyShift; + + setSampleRate( sampleRate ); + } + + private void setSampleRate( int sampleRate ) + { + if( sampleRate != mSampleRate ) + { + /* Setup or adjust the frequency translator */ + if( mFrequencyTranslator == null ) + { + mFrequencyTranslator = + new NumericallyControlledOscillator( mFrequencyShift, + sampleRate ); + } + else + { + mFrequencyTranslator.setSampleRate( sampleRate ); + } + + /* Get new decimation filters */ + mFilters = FilterFactory + .getDecimationFilters( sampleRate, + sCHANNEL_RATE, + sCHANNEL_PASS_FREQUENCY, + 48, //dB attenuation + WindowType.HAMMING ); + + /* wire the first filter stage to the frequency translator */ +// mFrequencyTranslator.setListener( mFilters[ 0 ] ); + + /* wire the remaining filters together */ + if( mFilters.length > 1 ) + { + for( int x = 1; x < mFilters.length; x++ ) + { + mFilters[ x - 1 ].setListener( mFilters[ x ] ); + } + } + + /* Add the sample broadcaster as a listener to the final filter */ +// mFilters[ mFilters.length - 1 ].setListener( mComplexSampleBroadcaster ); + + mSampleRate = sampleRate; + } + } + + @Override + public void frequencyChanged( int frequency, int bandwidth ) + { + mFrequencyTranslator.setFrequency( frequency ); + + if( bandwidth != mSampleRate ) + { + setSampleRate( bandwidth ); + } + } + + public int getFrequencyOffset() + { + return mFrequencyTranslator.getFrequency(); + } + + public void addListener( Listener listener ) + { + mComplexSampleBroadcaster.addListener( listener ); + } + + public void removeListener( Listener listener ) + { + mComplexSampleBroadcaster.removeListener( listener ); + } + + public int getListenerCount() + { + return mComplexSampleBroadcaster.getListenerCount(); + } + + public void removeListeners() + { + mComplexSampleBroadcaster.clear(); + } + + @Override + public void receive( ComplexSample sample ) + { + mFrequencyTranslator.receive( sample ); + } +} diff --git a/src/source/tuner/FrequencyChangeBroadcaster.java b/src/source/tuner/FrequencyChangeBroadcaster.java new file mode 100644 index 00000000..467f8a32 --- /dev/null +++ b/src/source/tuner/FrequencyChangeBroadcaster.java @@ -0,0 +1,25 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package source.tuner; + +public interface FrequencyChangeBroadcaster +{ + public abstract void addListener( FrequencyChangeListener listener ); + + public abstract void removeListener( FrequencyChangeListener listener ); +} diff --git a/src/source/tuner/FrequencyChangeListener.java b/src/source/tuner/FrequencyChangeListener.java new file mode 100644 index 00000000..a42aa797 --- /dev/null +++ b/src/source/tuner/FrequencyChangeListener.java @@ -0,0 +1,38 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package source.tuner; + +/** + * Interface for broadcasting changes to frequency and/or bandwidth/sample rate + * to all registered listeners. + * + * Note: broadcasted frequency should be the uncorrected, or displayable + * frequency value. All frequency correction aspects should be handled within + * the device that implements the tuning of the frequency. + */ +public interface FrequencyChangeListener +{ + /** + * Broadcasts a change to the (uncorrected) frequency and/or bandwidth + * or sample rate. + * + * @param frequency - uncorrected frequency in hertz + * @param bandwidth - bandwidth in hertz + */ + public void frequencyChanged( int frequency, int bandwidth ); +} diff --git a/src/source/tuner/FrequencyController.java b/src/source/tuner/FrequencyController.java new file mode 100644 index 00000000..a8d04378 --- /dev/null +++ b/src/source/tuner/FrequencyController.java @@ -0,0 +1,220 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package source.tuner; + +import java.util.ArrayList; + +import source.SourceException; + +public class FrequencyController +{ + private Tunable mTunable; + private int mFrequency = 101100000; + private int mTunedFrequency = 101100000; + private int mMinimumFrequency; + private int mMaximumFrequency; + private double mFrequencyCorrection = 0d; + private int mBandwidth = 0; + + private ArrayList mListeners = + new ArrayList(); + + public FrequencyController( Tunable tunable, + int minFrequency, + int maxFrequency, + double frequencyCorrection ) throws SourceException + { + mTunable = tunable; + mMinimumFrequency = minFrequency; + mMaximumFrequency = maxFrequency; + mFrequencyCorrection = frequencyCorrection; + } + + /** + * Get bandwidth/sample rate in hertz + */ + public int getBandwidth() + { + return mBandwidth; + } + + /** + * Set bandwidth/sample rate in hertz + */ + public void setBandwidth( int bandwidth ) + { + mBandwidth = bandwidth; + + broadcastFrequencyChange(); + } + + /** + * Get frequency in hertz + */ + public int getFrequency() + { + return mFrequency; + } + + /** + * Set frequency + * @param frequency in hertz + * @throws SourceException if tunable doesn't support tuning a corrected + * version of the requested frequency + */ + public void setFrequency( int frequency ) throws SourceException + { + setFrequency( frequency, true ); + } + + /** + * Set frequency with optional broadcast of frequency change event. This + * method supports changing the frequency correction value without + * broadcasting a frequency change event. + */ + private void setFrequency( int frequency, boolean broadcastChange ) + throws SourceException + { + int tunedFrequency = getTunedFrequency( frequency ); + + if( tunedFrequency < mMinimumFrequency || + tunedFrequency > mMaximumFrequency ) + { + throw new SourceException( "Cannot tune frequency ( requested: " + + frequency + " actual:" + tunedFrequency + "]" ); + } + + mFrequency = frequency; + mTunedFrequency = tunedFrequency; + + if( mTunable != null ) + { + mTunable.setTunedFrequency( mTunedFrequency ); + } + + /* Broadcast to all listeners that the frequency has changed */ + if( broadcastChange ) + { + broadcastFrequencyChange(); + } + } + + public int getTunedFrequency() + { + return mTunedFrequency; + } + + public int getMinimumFrequency() + { + return mMinimumFrequency; + } + + public int getMaximumFrequency() + { + return mMaximumFrequency; + } + + /** + * Calculate the tuned frequency by adding frequency correction to the + * corrected frequency. + * @param correctedFrequency + */ + private int getTunedFrequency( int correctedFrequency ) + { + return (int)( (double)correctedFrequency / + ( 1.0 + ( mFrequencyCorrection / 1000000.0 ) ) ); + } + + /** + * Calculate the corrected frequency by subtracting frequency correction + * from the tuned frequency. + * @param tunedFrequency + */ + private int getCorrectedFrequency( int tunedFrequency ) + { + return (int)( (double)tunedFrequency / + ( 1.0 - ( mFrequencyCorrection / 1000000.0 ) ) ); + } + + public double getFrequencyCorrection() + { + return mFrequencyCorrection; + } + + public void setFrequencyCorrection( double correction ) + throws SourceException + { + mFrequencyCorrection = correction; + + if( mFrequency > 0 ) + { + /* Reset the frequency, but don't broadcast a change */ + setFrequency( mFrequency, false ); + } + } + + /** + * Adds listener to receive frequency change events + */ + public void addListener( FrequencyChangeListener listener ) + { + if( !mListeners.contains( listener ) ) + { + mListeners.add( listener ); + } + } + + /** + * Removes listener from receiving frequency change events + */ + public void removeListener( FrequencyChangeListener listener ) + { + mListeners.remove( listener ); + } + + + /** + * Broadcasts a change/update to the current (uncorrected) frequency or the + * bandwidth/sample rate value. + */ + protected void broadcastFrequencyChange() + { + for( FrequencyChangeListener listener: mListeners ) + { + listener.frequencyChanged( mFrequency, mBandwidth ); + } + } + + public interface Tunable + { + /** + * Gets the tuned frequency of the device + */ + public int getTunedFrequency() throws SourceException; + + /** + * Sets the tuned frequency of the device + */ + public void setTunedFrequency( int frequency ) throws SourceException; + + /** + * Gets the current bandwidth setting of the device + */ + public int getCurrentSampleRate() throws SourceException; + } +} diff --git a/src/source/tuner/MixerTuner.java b/src/source/tuner/MixerTuner.java new file mode 100644 index 00000000..00abe681 --- /dev/null +++ b/src/source/tuner/MixerTuner.java @@ -0,0 +1,98 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package source.tuner; + +import java.util.ArrayList; + +import javax.sound.sampled.TargetDataLine; + +import sample.Listener; +import sample.complex.ComplexSampleListListener; +import source.mixer.MixerSource; +import source.mixer.SampleAdapter; + +public abstract class MixerTuner extends Tuner +{ + private TargetDataLine mTargetDataLine; + protected MixerTunerType mMixerTunerType; + protected MixerSource mMixerSource; + protected ArrayList mListeners = + new ArrayList(); + + /** + * Provides a wrapper around a MixerSource to treat it like a Tuner that + * can provide ComplexSample samples. Subclasses should couple this + * functionality with a tuner control class to allow tuning. + */ + public MixerTuner( String name, + MixerTunerType mixerTunerType, + TargetDataLine targetDataLine, + SampleAdapter sampleAdapter ) + { + super( name ); + + mMixerTunerType = mixerTunerType; + + mTargetDataLine = targetDataLine; + + mMixerSource = new MixerSource( targetDataLine, + mMixerTunerType.getAudioFormat(), + name, + sampleAdapter ); + } + + public MixerTuner( String name, + MixerTunerDataLine mixerTuner, + SampleAdapter sampleAdapter ) + { + this( name, + mixerTuner.getMixerTunerType(), + mixerTuner.getTargetDataLine(), + sampleAdapter ); + } + + public MixerTunerType getMixerTunerType() + { + return mMixerTunerType; + } + + public TargetDataLine getTargetDataLine() + { + return mTargetDataLine; + } + + /** + * Overrides the Tuner listener management methods and allows the + * complex mixer source to manage sample listeners + */ + @Override + public void addListener( Listener listener ) + { + mMixerSource.setListener( listener ); + } + + /** + * Overrides the Tuner listener management methods and allows the + * complex mixer source to manage sample listeners + */ + @Override + public void removeListener( Listener listener ) + { + mMixerSource.removeListener( listener ); + } +} diff --git a/src/source/tuner/MixerTunerDataLine.java b/src/source/tuner/MixerTunerDataLine.java new file mode 100644 index 00000000..8b434431 --- /dev/null +++ b/src/source/tuner/MixerTunerDataLine.java @@ -0,0 +1,42 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package source.tuner; + +import javax.sound.sampled.TargetDataLine; + +public class MixerTunerDataLine +{ + private TargetDataLine mTargetDataLine; + protected MixerTunerType mMixerTunerType; + + public MixerTunerDataLine( TargetDataLine line, MixerTunerType type ) + { + mTargetDataLine = line; + mMixerTunerType = type; + } + + public TargetDataLine getTargetDataLine() + { + return mTargetDataLine; + } + + public MixerTunerType getMixerTunerType() + { + return mMixerTunerType; + } +} diff --git a/src/source/tuner/MixerTunerType.java b/src/source/tuner/MixerTunerType.java new file mode 100644 index 00000000..4504bd19 --- /dev/null +++ b/src/source/tuner/MixerTunerType.java @@ -0,0 +1,117 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package source.tuner; + +import javax.sound.sampled.AudioFormat; +import javax.sound.sampled.Mixer; + +public enum MixerTunerType +{ + + FUNCUBE_DONGLE_PRO( TunerType.FUNCUBE_DONGLE_PRO, + "V10", + "FUNcube Dongle", + "FUNcube Dongle Pro V1.0", + new AudioFormat( 96000, //SampleRate + 16, //Sample Size + 2, //Channels + true, //Signed + false ) ), //Little Endian ) + + FUNCUBE_DONGLE_PRO_PLUS( TunerType.FUNCUBE_DONGLE_PRO_PLUS, + "V20", + "FUNcube Dongle V2.0", + "FUNcube Dongle Pro Plus V2.0", + new AudioFormat( 192000, //SampleRate + 16, //Sample Size + 2, //Channels + true, //Signed + false ) ), //Little Endian ) + + UNKNOWN( TunerType.UNKNOWN, + "UNK", + "Unknown", + "Unknown Mixer Tuner", + new AudioFormat( 48000, //SampleRate + 16, //Sample Size + 2, //Channels + true, //Signed + false ) ); //Little Endian ) + + private TunerType mTunerClass; + private String mMixerName; + private String mPartialDescription; + private String mDisplayString; + private AudioFormat mAudioFormat; + + MixerTunerType( TunerType tunerClass, + String mixerName, + String partialDescription, + String displayString, + AudioFormat format ) + { + mTunerClass = tunerClass; + mMixerName = mixerName; + mPartialDescription = partialDescription; + mDisplayString = displayString; + mAudioFormat = format; + } + + public TunerType getTunerClass() + { + return mTunerClass; + } + + public String getMixerName() + { + return mMixerName; + } + + public String getPartialDescription() + { + return mPartialDescription; + } + + public String getDisplayString() + { + return mDisplayString; + } + + public AudioFormat getAudioFormat() + { + return mAudioFormat; + } + + public static MixerTunerType getMixerTunerType( Mixer.Info info ) + { + MixerTunerType retVal = UNKNOWN; + + if( info.getName().contains( FUNCUBE_DONGLE_PRO_PLUS.mPartialDescription ) || + info.getDescription().contains( FUNCUBE_DONGLE_PRO_PLUS.mPartialDescription ) ) + { + retVal = FUNCUBE_DONGLE_PRO_PLUS; + } + else if( info.getName().contains( FUNCUBE_DONGLE_PRO.mPartialDescription ) || + info.getDescription().contains( FUNCUBE_DONGLE_PRO.mPartialDescription ) ) + { + retVal = FUNCUBE_DONGLE_PRO; + } + + return retVal; + } +} diff --git a/src/source/tuner/NumericallyControlledOscillator.java b/src/source/tuner/NumericallyControlledOscillator.java new file mode 100644 index 00000000..e15d3f00 --- /dev/null +++ b/src/source/tuner/NumericallyControlledOscillator.java @@ -0,0 +1,130 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package source.tuner; + +import log.Log; +import sample.Listener; +import sample.complex.ComplexSample; +import sample.complex.ComplexSampleUtils; +import util.Oscillator; + +public class NumericallyControlledOscillator implements Listener, + FrequencyChangeListener +{ + private int mFrequency; + private int mSampleRate; + private Oscillator mSineWaveGenerator; + private Listener mListener; + /** + * Translates (shifts) frequency spectrum up or down, depending on the sign + * of the frequency argument, by mixing received complex samples with a + * generated sine wave of the frequency argument. Positive frequency shifts + * the received spectrum higher in frequency. Negative frequency shifts the + * received spectrum lower in frequency. In both cases, spectrum that is + * shifted outside of the sampling bandwidth will wrap around, due to the + * nature of quadrature sampling. + */ + public NumericallyControlledOscillator( int frequencyShift, int sampleRate ) + { + mFrequency = frequencyShift; + mSampleRate = sampleRate; + + int defaultMixingSignalGain = 10; + + mSineWaveGenerator = new Oscillator( mFrequency, mSampleRate ); + } + + /** + * Listener method for this class to receive samples from a Complex Sample + * Provider. This method multiplies the incoming sample by the generated + * mixing frequency to translate (ie shift +/-) the incoming signal, and + * then send the translated signal on to all registered listeners + */ + @Override + public void receive( ComplexSample sample ) + { + if( mListener != null ) + { + mListener.receive( ComplexSampleUtils.multiply( sample, + mSineWaveGenerator.nextComplex() ) ); + } + } + + @Override + public void frequencyChanged( int frequency, int bandwidth ) + { + if( frequency != mFrequency ) + { + setFrequency( frequency ); + } + + if( bandwidth != mSampleRate ) + { + setSampleRate( bandwidth ); + } + } + + /** + * Changes the frequency value and resets the sine wave generator, so that + * all subsequent samples will be output at the new translated frequency + * @param frequency - hertz + */ + public void setFrequency( int frequency ) + { + Log.info( "NumericallyControlledOscillator - setting sine wave gen to freq:" + frequency ); + + mFrequency = frequency; + mSineWaveGenerator.setFrequency( frequency ); + } + + /** + * Current frequency shift setting + * @return - frequency - hertz + */ + public int getFrequency() + { + return mFrequency; + } + + /** + * Changes the sample rate, so that all new input/output values will be + * translated at the new sample rate + * @param sampleRate - hertz + */ + public void setSampleRate( int sampleRate ) + { + mSampleRate = sampleRate; + mSineWaveGenerator.setSampleRate( sampleRate ); + } + + /** + * Add listener to receive frequency translated complex samples + */ + public void setListener( Listener listener ) + { + mListener = listener; + } + + /** + * Remove listener from receiving frequency translated complex samples. + */ + public void clearListener() + { + mListener = null; + } +} diff --git a/src/source/tuner/Tuner.java b/src/source/tuner/Tuner.java new file mode 100644 index 00000000..a1341244 --- /dev/null +++ b/src/source/tuner/Tuner.java @@ -0,0 +1,190 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package source.tuner; + +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.RejectedExecutionException; + +import javax.swing.JPanel; + +import sample.Listener; +import source.SourceException; +import controller.ResourceManager; +import controller.ThreadPoolManager; + +/** + * Tuner - provides tuner channel sources, representing a channel frequency + */ +public abstract class Tuner implements FrequencyChangeBroadcaster, + FrequencyChangeListener +{ + private String mName; + + protected CopyOnWriteArrayList> + mSampleListeners = new CopyOnWriteArrayList>(); + + protected CopyOnWriteArrayList + mFrequencyChangeListeners = new CopyOnWriteArrayList(); + + public Tuner( String name ) + { + mName = name; + } + + /** + * Return an editor panel for the tuner + */ + public abstract JPanel getEditor( ResourceManager resourceManager ); + + + /** + * Applies the settings from a saved tuner configuration setting object + */ + public abstract void apply( TunerConfiguration config ) + throws SourceException; + + /** + * Unique identifier for this tuner, used to lookup a tuner configuration + * from the settings manager. + * + * @return - unique identifier like a serial number, or a usb bus location + * or ip address and port. Return some form of unique identification that + * allows this tuner to be identified from among several of the same types + * of tuners. + */ + public abstract String getUniqueID(); + + /** + * @return - tuner class enum entry + */ + public abstract TunerClass getTunerClass(); + + /** + * @return - tuner type enum entry + */ + public abstract TunerType getTunerType(); + + /** + * Name of this tuner object + * @return - string name of this tuner object + */ + public String getName() + { + return mName; + } + + /** + * Sample rate of complex data coming from the Tuner + * @return + */ + public abstract int getSampleRate(); + + /** + * Tuner Local Oscillator Frequency + * @return - frequency in Hertz + */ + public abstract int getFrequency() throws SourceException; + + /** + * Returns a tuner frequency channel source, tuned to the correct frequency + * + * Note: ensure the concrete implementation registers the returned + * tuner channel source as a listener on the tuner, to receive frequency + * change events. + * + * @param frequency - desired frequency + * + * @return - source for 48k sample rate + */ + public abstract TunerChannelSource getChannel( ThreadPoolManager threadPoolManager, + TunerChannel channel ) throws RejectedExecutionException, SourceException; + + /** + * Releases the tuned channel resources + * + * Note: ensure the concrete implementation unregisters the tuner channel + * source from the tuner as a frequency change listener + * + * @param channel - previously obtained tuner channel + */ + public abstract void releaseChannel( TunerChannelSource source ); + + /** + * Registers the listener to receive complex float sample arrays + */ + public void addListener( Listener listener ) + { + mSampleListeners.add( listener ); + } + + /** + * Removes the registered listener + */ + public void removeListener( Listener listener ) + { + mSampleListeners.remove( listener ); + } + + /** + * Broadcasts the samples to all registered listeners + */ + public void broadcast( Float[] samples ) + { + for( Listener listener: mSampleListeners ) + { + listener.receive( samples ); + } + } + + /** + * Adds the frequency change listener to receive frequency and/or bandwidth + * change events + */ + public void addListener( FrequencyChangeListener listener ) + { + mFrequencyChangeListeners.add( listener ); + } + + /** + * Removes the frequency change listener + */ + public void removeListener( FrequencyChangeListener listener ) + { + mFrequencyChangeListeners.remove( listener ); + } + + /** + * Broadcasts a frequency change event to all registered listeners + */ + public void broadcastFrequencyChange( int frequency, int bandwidth ) + { + for( FrequencyChangeListener listener: mFrequencyChangeListeners ) + { + listener.frequencyChanged( frequency, bandwidth ); + } + } + + /** + * Frequency change listener method. We receive change events from the + * controller and rebroadcast them to all registered listeners. + */ + public void frequencyChanged( int frequency, int bandwidth ) + { + broadcastFrequencyChange( frequency, bandwidth ); + } +} diff --git a/src/source/tuner/TunerChannel.java b/src/source/tuner/TunerChannel.java new file mode 100644 index 00000000..42974b65 --- /dev/null +++ b/src/source/tuner/TunerChannel.java @@ -0,0 +1,114 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package source.tuner; + +public class TunerChannel implements Comparable +{ + private Type mType; + private int mFrequency; + private int mBandwidth; + + public TunerChannel( Type type, int frequency, int bandwidth ) + { + mType = type; + mFrequency = frequency; + mBandwidth = bandwidth; + } + + public Type getType() + { + return mType; + } + + public void setType( Type type ) + { + mType = type; + } + + public int getFrequency() + { + return mFrequency; + } + + public void setFrequency( int frequency ) + { + mFrequency = frequency; + } + + public int getBandwidth() + { + return mBandwidth; + } + + public void setBandwidth( int bandwidth ) + { + mBandwidth = bandwidth; + } + + public int getMinFrequency() + { + return mFrequency - ( mBandwidth / 2 ); + } + + public int getMaxFrequency() + { + return mFrequency + ( mBandwidth / 2 ); + } + + /** + * Indicates if any part of this tuner channel is contained within the + * minimum and maximum frequency values. + */ + public boolean isWithin( int minimum, int maximum ) + { + return ( ( minimum <= getMinFrequency() && getMinFrequency() <= maximum ) || + ( minimum <= getMaxFrequency() && getMaxFrequency() <= maximum ) ); + } + + public String toString() + { + StringBuilder sb = new StringBuilder(); + + sb.append( "Channel: " ); + sb.append( ( mFrequency - ( mBandwidth / 2 ) ) ); + sb.append( "-" ); + sb.append( ( mFrequency + ( mBandwidth / 2 ) ) ); + + return sb.toString(); + } + + @Override + public int compareTo( TunerChannel otherLock ) + { + if( mFrequency < otherLock.getFrequency() ) + { + return -1; + } + else + { + return 1; + } + } + + public enum Type + { + LOCKED, //Use for decoding channels, so that they cannot be tuned out of range + TRAFFIC, //Like locked, but for temporary allocated traffic channels + TUNABLE; //Tunable -- can be tuned out of range + } +} diff --git a/src/source/tuner/TunerChannelSource.java b/src/source/tuner/TunerChannelSource.java new file mode 100644 index 00000000..b9baca1e --- /dev/null +++ b/src/source/tuner/TunerChannelSource.java @@ -0,0 +1,273 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package source.tuner; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.LinkedTransferQueue; +import java.util.concurrent.RejectedExecutionException; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; + +import sample.Listener; +import sample.Provider; +import sample.complex.ComplexSample; +import sample.complex.ComplexSampleAssembler; +import source.ComplexSource; +import source.Source.SampleType; +import source.SourceException; +import util.Oscillator; +import controller.ThreadPoolManager; +import controller.ThreadPoolManager.ThreadType; +import dsp.filter.ComplexFilter; +import dsp.filter.FilterFactory; +import dsp.filter.Window.WindowType; + +public class TunerChannelSource extends ComplexSource + implements FrequencyChangeListener, + Listener +{ + private static int sCHANNEL_RATE = 48000; + private static int sCHANNEL_PASS_FREQUENCY = 12500; + + private LinkedTransferQueue mBuffer = + new LinkedTransferQueue(); + private Tuner mTuner; + private TunerChannel mTunerChannel; + private Oscillator mSineWaveGenerator; + private ComplexFilter[] mDecimationFilters; + private DecimationProcessor mDecimationProcessor = new DecimationProcessor(); + + private ComplexSampleAssembler mBufferAssembler = + new ComplexSampleAssembler( 3000 ); + private int mTunerFrequency; + private int mTunerSampleRate; + private ThreadPoolManager mThreadPoolManager; + private ScheduledFuture mTaskHandle; + + public TunerChannelSource( ThreadPoolManager threadPoolManager, + Tuner tuner, + TunerChannel tunerChannel ) + throws RejectedExecutionException + { + super( "Tuner Channel Source" ); + + mThreadPoolManager = threadPoolManager; + + /* Schedule the decimation task to run 20 times a second */ + mTaskHandle = mThreadPoolManager.schedule( ThreadType.DECIMATION, + new DecimationProcessor(), 50, TimeUnit.MILLISECONDS ); + + mTuner = tuner; + mTuner.addListener( (FrequencyChangeListener)this ); + + mTunerChannel = tunerChannel; + + /* Setup the frequency translator to the current source frequency */ + try + { + frequencyChanged( mTuner.getFrequency(), mTuner.getSampleRate() ); + } + catch ( SourceException e ) + { + e.printStackTrace(); + } + + /* Finally, register to receive samples from the tuner */ + mTuner.addListener( (Listener)this ); + } + + @Override + public void dispose() + { + //Tell the tuner to release our resources + mTuner.removeListener( (FrequencyChangeListener)this ); + mTuner.releaseChannel( this ); + + if( mTaskHandle != null ) + { + mThreadPoolManager.cancel( mTaskHandle ); + } + + mBuffer.clear(); + mBuffer = null; + + for( ComplexFilter filter: mDecimationFilters ) + { + filter.dispose(); + } + + mBufferAssembler.dispose(); + + mTuner = null; + + mTunerChannel = null; + + mTunerChannel = null; + mTuner = null; + } + + public Tuner getTuner() + { + return mTuner; + } + + public TunerChannel getTunerChannel() + { + return mTunerChannel; + } + + @Override + public void receive( Float[] samples ) + { + mBuffer.add( samples ); + } + + @Override + public void setListener( Listener> listener ) + { + mBufferAssembler.setListener( listener ); + } + + @Override + public void removeListener( Listener> listener ) + { + mBufferAssembler.removeListener( listener ); + mDecimationFilters[ mDecimationFilters.length - 1 ] + .setListener( null ); + } + + @Override + public void frequencyChanged( int frequency, int bandwidth ) + { + + /* Frequency change */ + if( frequency != mTunerFrequency ) + { + int frequencyOffset = frequency - mTunerChannel.getFrequency(); + + if( mSineWaveGenerator == null ) + { + mSineWaveGenerator = + new Oscillator( frequencyOffset, bandwidth ); + } + else + { + mSineWaveGenerator.setFrequency( frequencyOffset ); + } + + /* Clear the buffer for the frequency to take immediate effect */ + mBuffer.clear(); + + mTunerFrequency = frequency; + } + + /* Bandwidth/Sample rate change */ + if( bandwidth != mTunerSampleRate ) + { + int frequencyOffset = frequency - mTunerChannel.getFrequency(); + + if( mSineWaveGenerator == null ) + { + mSineWaveGenerator = + new Oscillator( frequencyOffset, bandwidth ); + } + else + { + mSineWaveGenerator.setSampleRate( bandwidth ); + } + + /* Get the original decimation filter output listener */ + Listener listener = null; + + /* Get new decimation filters */ + mDecimationFilters = FilterFactory + .getDecimationFilters( bandwidth, + sCHANNEL_RATE, + sCHANNEL_PASS_FREQUENCY, + 48, //dB attenuation + WindowType.HAMMING ); + + /* wire the filters together */ + if( mDecimationFilters.length > 1 ) + { + for( int x = 1; x < mDecimationFilters.length; x++ ) + { + mDecimationFilters[ x - 1 ].setListener( mDecimationFilters[ x ] ); + } + } + + /* re-add the original output listener */ + mDecimationFilters[ mDecimationFilters.length - 1 ] + .setListener( mBufferAssembler ); + + mTunerSampleRate = bandwidth; + } + } + + public int getSampleRate() throws SourceException + { + return mTuner.getSampleRate(); + } + + public int getFrequency() throws SourceException + { + return mTuner.getFrequency(); + } + + public class DecimationProcessor implements Runnable + { + @Override + public void run() + { + List samplesList = new ArrayList(); + + if( mBuffer != null ) + { + /* Limit to 10, so that after a garbage collect we don't + * induce wild swings as it tries to take tons of samples + * and process them, causing delays and buffer refill */ + mBuffer.drainTo( samplesList, 10 ); + + for( Float[] samples: samplesList ) + { + for( int x = 0; x < samples.length; x += 2 ) + { + Float left = samples[ x ]; + Float right = samples[ x + 1 ]; + + if( left != null && right != null ) + { + /* perform frequency translation */ + ComplexSample translated = ComplexSample.multiply( + mSineWaveGenerator.nextComplex(), left, right ); + + /* decimate the sample rate */ + if( mDecimationFilters[ 0 ] != null ) + { + mDecimationFilters[ 0 ].receive( translated ); + } + } + } + } + + samplesList.clear(); + } + } + } +} diff --git a/src/source/tuner/TunerClass.java b/src/source/tuner/TunerClass.java new file mode 100644 index 00000000..9f9350e1 --- /dev/null +++ b/src/source/tuner/TunerClass.java @@ -0,0 +1,260 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package source.tuner; + +import javax.usb.UsbDeviceDescriptor; + +public enum TunerClass +{ + GENERIC_2832( TunerType.RTL2832_VARIOUS, "0BDA", "2832", "RTL2832", "SDR" ), + GENERIC_2838( TunerType.RTL2832_VARIOUS, "0BDA", "2838", "RTL2832", "SDR" ), + COMPRO_VIDEOMATE_U620F( TunerType.ELONICS_E4000, "185B", "0620", "Compro", "Videomate U620F" ), + COMPRO_VIDEOMATE_U650F( TunerType.ELONICS_E4000, "185B", "0650", "Compro", "Videomate U620F" ), + COMPRO_VIDEOMATE_U680F( TunerType.ELONICS_E4000, "185B", "0680", "Compro", "Videomate U620F" ), + DEXATEK_LOGILINK_VG002A( TunerType.FCI_FC2580, "1D19", "1101", "Dexatek", "Logilink VG0002A" ), + DEXATEK_DIGIVOX_MINI_II_REV3( TunerType.FCI_FC2580, "1D19", "1102", "Dexatek", "MSI Digivox Mini II v3.0" ), + DEXATEK_5217_DVBT( TunerType.FCI_FC2580, "1D19", "1103", "Dexatek", "5217 DVB-T" ), + ETTUS_USRP_B100( TunerType.ETTUS_VARIOUS, "2500", "0002", "Ettus Research", "USRP B100" ), + FUNCUBE_DONGLE_PRO( TunerType.FUNCUBE_DONGLE_PRO, "04D8", "FB56", "Hamlincrest", "Funcube Dongle Pro" ), + FUNCUBE_DONGLE_PRO_PLUS( TunerType.FUNCUBE_DONGLE_PRO_PLUS, "04D8", "FB31", "Hamlincrest", "Funcube Dongle Pro Plus" ), + GIGABYTE_GTU7300( TunerType.FITIPOWER_FC0012, "1B80", "D393", "Gigabyte", "GT-U7300" ), + GTEK_T803( TunerType.FITIPOWER_FC0012, "1F4D", "B803", "GTek", "T803" ), + LIFEVIEW_LV5T_DELUXE( TunerType.FITIPOWER_FC0012, "1F4D", "C803", "Liveview", "LV5T Deluxe" ), + MYGICA_TD312( TunerType.FITIPOWER_FC0012, "1F4D", "D286", "MyGica", "TD312" ), + PEAK_102569AGPK( TunerType.FITIPOWER_FC0012, "1B80", "D395", "Peak", "102569AGPK" ), + PROLECTRIX_DV107669( TunerType.FITIPOWER_FC0012, "1F4D", "D803", "Prolectrix", "DV107669" ), + SVEON_STV20( TunerType.FITIPOWER_FC0012, "1B80", "D39D", "Sveon", "STV20 DVB-T USB & FM" ), + TERRATEC_CINERGY_T_REV1( TunerType.FITIPOWER_FC0012, "0CCD", "00A9", "Terratec", "Cinergy T R1" ), + TERRATEC_CINERGY_T_REV3( TunerType.ELONICS_E4000, "0CCD", "00D3", "Terratec", "Cinergy T R3" ), + TERRATEC_NOXON_REV1_B3( TunerType.FITIPOWER_FC0013, "0CCD", "00B3", "Terratec", "NOXON R1 (B3)" ), + TERRATEC_NOXON_REV1_B4( TunerType.FITIPOWER_FC0013, "0CCD", "00B4", "Terratec", "NOXON R1 (B4)" ), + TERRATEC_NOXON_REV1_B7( TunerType.FITIPOWER_FC0013, "0CCD", "00B7", "Terratec", "NOXON R1 (B7)" ), + TERRATEC_NOXON_REV1_C6( TunerType.FITIPOWER_FC0013, "0CCD", "00C6", "Terratec", "NOXON R1 (C6)" ), + TERRATEC_NOXON_REV2( TunerType.ELONICS_E4000, "0CCD", "00E0", "Terratec", "NOXON R2" ), + TERRATEC_T_STICK_PLUS( TunerType.ELONICS_E4000, "0CCD", "00D7", "Terratec", "T Stick Plus" ), + TWINTECH_UT40( TunerType.FITIPOWER_FC0013, "1B80", "D3A4", "Twintech", "UT-40" ), + ZAAPA_ZTMINDVBZP( TunerType.FITIPOWER_FC0012, "1B80", "D398", "Zaapa", "ZT-MINDVBZP" ), + UNKNOWN( TunerType.UNKNOWN, "0", "0", "Unknown Manufacturer", "Unknown Device" ); + + private TunerType mTunerType; + private String mVendorID; + private String mDeviceID; + private String mVendorDescription; + private String mDeviceDescription; + + private TunerClass( TunerType tunerType, + String vendorID, + String deviceID, + String vendorDescription, + String deviceDescription ) + { + mTunerType = tunerType; + mVendorID = vendorID; + mDeviceID = deviceID; + mVendorDescription = vendorDescription; + mDeviceDescription = deviceDescription; + } + + public String toString() + { + return "USB" + + " Tuner:" + mTunerType.toString() + + " Vendor:" + mVendorDescription + + " Device:" + mDeviceDescription + + " Address:" + mVendorID + ":" + mDeviceID; + } + + public String getVendorDeviceLabel() + { + return mVendorDescription + " " + mDeviceDescription; + } + + public TunerType getTunerClass() + { + return mTunerType; + } + + public static TunerClass valueOf( UsbDeviceDescriptor descriptor ) + { + return valueOf( descriptor.idVendor(), descriptor.idProduct() ); + } + + public static TunerClass valueOf( short vendor, short product ) + { + TunerClass retVal = TunerClass.UNKNOWN; + + //Cast the short to integer so that we can switch on unsigned numbers + int vendorID = vendor & 0xFFFF; + int productID = product & 0xFFFF; + + switch( vendorID ) + { + case 1240: //04DA + if( productID == 64305 ) //FB31 + { + retVal = FUNCUBE_DONGLE_PRO_PLUS; + } + else if( productID == 64342 ) //FB56 + { + retVal = FUNCUBE_DONGLE_PRO; + } + break; + case 3034: //0BDA + if( productID == 10290 ) //2832 + { + retVal = GENERIC_2832; + } + else if( productID == 10296 ) //2838 + { + retVal = GENERIC_2838; + } + break; + case 6235: //185B + if( productID == 1568 ) //0620 + { + retVal = COMPRO_VIDEOMATE_U620F; + } + else if( productID == 1616 ) //0650 + { + retVal = COMPRO_VIDEOMATE_U650F; + } + else if( productID == 1664 ) //0680 + { + retVal = COMPRO_VIDEOMATE_U680F; + } + break; + case 7040: //1B80 + if( productID == 54163 ) //D393 + { + retVal = GIGABYTE_GTU7300; + } + else if( productID == 54165 ) //D395 + { + retVal = PEAK_102569AGPK; + } + else if( productID == 54168 ) //D398 + { + retVal = ZAAPA_ZTMINDVBZP; + } + else if( productID == 54173 ) //D39D + { + retVal = SVEON_STV20; + } + else if( productID == 54180 ) //D3A4 + { + retVal = TWINTECH_UT40; + } + break; + case 7449: //1D19 + if( productID == 4353 ) //1101 + { + retVal = DEXATEK_LOGILINK_VG002A; + } + else if( productID == 4354 ) //1102 + { + retVal = DEXATEK_DIGIVOX_MINI_II_REV3; + } + else if( productID == 4355 ) //1103 + { + retVal = DEXATEK_5217_DVBT; + } + break; + case 8013: //1F4D + if( productID == 47107 ) //B803 + { + retVal = GTEK_T803; + } + else if( productID == 51203 ) //C803 + { + retVal = LIFEVIEW_LV5T_DELUXE; + } + else if( productID == 53894 ) //D286 + { + retVal = MYGICA_TD312; + } + else if( productID == 55299 ) //D803 + { + retVal = PROLECTRIX_DV107669; + } + break; + case 3277: //0CCD + if( productID == 169 ) //00A9 + { + retVal = TERRATEC_CINERGY_T_REV1; + } + else if( productID == 179 ) //00B3 + { + retVal = TERRATEC_NOXON_REV1_B3; + } + else if( productID == 180 ) //00B4 + { + retVal = TERRATEC_NOXON_REV1_B4; + } + else if( productID == 181 ) //00B5 + { + retVal = TERRATEC_NOXON_REV1_B7; + } + else if( productID == 198 ) //00C6 + { + retVal = TERRATEC_NOXON_REV1_C6; + } + else if( productID == 211 ) //00D3 + { + retVal = TERRATEC_CINERGY_T_REV3; + } + else if( productID == 215 ) //00D7 + { + retVal = TERRATEC_T_STICK_PLUS; + } + else if( productID == 224) //00E0 + { + retVal = TERRATEC_NOXON_REV2; + } + break; + case 9472: //2500 + if( productID == 2 ) //0002 + { + retVal = ETTUS_USRP_B100; + } + break; + default: + } + + return retVal; + } + + public String getVendorID() + { + return mVendorID; + } + + public String getDeviceID() + { + return mDeviceID; + } + + public String getVendorDescription() + { + return mVendorDescription; + } + + public String getDeviceDescription() + { + return mDeviceDescription; + } +} diff --git a/src/source/tuner/TunerConfiguration.java b/src/source/tuner/TunerConfiguration.java new file mode 100644 index 00000000..d82977da --- /dev/null +++ b/src/source/tuner/TunerConfiguration.java @@ -0,0 +1,65 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package source.tuner; + +import javax.xml.bind.annotation.XmlAttribute; +import javax.xml.bind.annotation.XmlType; + +/** + * Abstract class to hold a named configuration for a specific type of tuner + */ +@XmlType( name = "tuner_configuration" ) +public abstract class TunerConfiguration +{ + protected String mName; + + /** + * Default constructor to support JAXB + */ + public TunerConfiguration() + { + this( "Unknown" ); + } + + public String toString() + { + return mName; + } + + /** + * Normal constructor + */ + public TunerConfiguration( String name ) + { + mName = name; + } + + @XmlAttribute( name = "name" ) + public String getName() + { + return mName; + } + + public void setName( String name ) + { + mName = name; + } + + @XmlAttribute( name = "tuner_type" ) + public abstract TunerType getTunerType(); +} diff --git a/src/source/tuner/TunerConfigurationAssignment.java b/src/source/tuner/TunerConfigurationAssignment.java new file mode 100644 index 00000000..0cb450bf --- /dev/null +++ b/src/source/tuner/TunerConfigurationAssignment.java @@ -0,0 +1,64 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package source.tuner; + +import javax.xml.bind.annotation.XmlAttribute; + +public class TunerConfigurationAssignment +{ + private TunerType mTunerType; + private String mUniqueID; + private String mTunerConfigurationName; + + public TunerConfigurationAssignment() + { + } + + @XmlAttribute( name = "type" ) + public TunerType getTunerType() + { + return mTunerType; + } + + public void setTunerType( TunerType tunerType ) + { + mTunerType = tunerType; + } + + @XmlAttribute( name = "id" ) + public String getUniqueID() + { + return mUniqueID; + } + + public void setUniqueID( String uniqueID ) + { + mUniqueID = uniqueID; + } + + @XmlAttribute( name = "configuration_name" ) + public String getTunerConfigurationName() + { + return mTunerConfigurationName; + } + + public void setTunerConfigurationName( String tunerConfigurationName ) + { + mTunerConfigurationName = tunerConfigurationName; + } +} diff --git a/src/source/tuner/TunerController.java b/src/source/tuner/TunerController.java new file mode 100644 index 00000000..efbb31cd --- /dev/null +++ b/src/source/tuner/TunerController.java @@ -0,0 +1,250 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package source.tuner; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Iterator; +import java.util.concurrent.RejectedExecutionException; + +import log.Log; +import source.SourceException; +import source.tuner.FrequencyController.Tunable; +import controller.ThreadPoolManager; + +public abstract class TunerController implements Tunable +{ + /* List of currently tuned channels being served to demod channels */ + protected ArrayList mTunedChannels = + new ArrayList(); + + protected FrequencyController mFrequencyController; + + /** + * Abstract tuner controller class. The tuner controller manages frequency + * bandwidth and currently tuned channels that are being fed samples from + * the tuner. + * + * @param minimumFrequency - minimum uncorrected tunable frequency + * @param maximumFrequency - maximum uncorrected tunable frequency + * @throws SourceException - for any issues related to constructing the + * class, tuning a frequency, or setting the bandwidth + */ + public TunerController( int minimumFrequency, int maximumFrequency ) + throws SourceException + { + mFrequencyController = new FrequencyController( this, + minimumFrequency, + maximumFrequency, + 0.0d ); + } + + /** + * Applies the settings in the tuner configuration + */ + public abstract void apply( TunerConfiguration config ) + throws SourceException; + + public int getBandwidth() + { + return mFrequencyController.getBandwidth(); + } + + /** + * Sets the center frequency of the local oscillator. + * + * @param frequency in hertz + * @throws SourceException - if the tuner has any issues + */ + public void setFrequency( int frequency ) throws SourceException + { + mFrequencyController.setFrequency( frequency ); + } + + /** + * Gets the center frequency of the local oscillator + * + * @return frequency in hertz + */ + public int getFrequency() + { + return mFrequencyController.getFrequency(); + } + + public double getFrequencyCorrection() + { + return mFrequencyController.getFrequencyCorrection(); + } + + public void setFrequencyCorrection( double correction ) throws SourceException + { + mFrequencyController.setFrequencyCorrection( correction ); + } + + public int getMinFrequency() + { + return mFrequencyController.getMinimumFrequency(); + } + + public int getMaxFrequency() + { + return mFrequencyController.getMaximumFrequency(); + } + + /** + * Indicates if the tuner can accomodate this new channel frequency and + * bandwidth, along with all of the existing tuned channels currently in + * place. + */ + public boolean canTuneChannel( TunerChannel channel ) + { + //Make sure we're within the tunable frequency range of this tuner + if( getMinFrequency() < channel.getMinFrequency() && + getMaxFrequency() > channel.getMaxFrequency() ) + { + //If this is the first lock, then we're good + if( mTunedChannels.isEmpty() ) + { + return true; + } + else + { + //Sort the existing locks and get the min/max locked frequencies + Collections.sort( mTunedChannels ); + + int minLockedFrequency = mTunedChannels.get( 0 ).getMinFrequency(); + int maxLockedFrequency = mTunedChannels + .get( mTunedChannels.size() - 1 ).getMaxFrequency(); + + //Requested channel is higher than min locked frequency + if( minLockedFrequency <= channel.getMinFrequency() && + ( channel.getMaxFrequency() - minLockedFrequency ) <= getBandwidth() ) + { + return true; + } + //Requested channel is lower than the max locked frequency + else if( channel.getMaxFrequency() <= maxLockedFrequency && + ( maxLockedFrequency - channel.getMinFrequency() ) <= getBandwidth() ) + { + return true; + } + } + } + + return false; + } + + public TunerChannelSource getChannel( ThreadPoolManager threadPoolManager, + Tuner tuner, TunerChannel tunerChannel ) + throws RejectedExecutionException, + SourceException + { + TunerChannelSource source = null; + + if( canTuneChannel( tunerChannel ) ) + { + mTunedChannels.add( tunerChannel ); + + updateLOFrequency(); + + source = new TunerChannelSource( threadPoolManager, tuner, tunerChannel ); + } + + return source; + } + + public void releaseChannel( TunerChannelSource tunerChannelSource ) + { + if( tunerChannelSource != null ) + { + mTunedChannels.remove( tunerChannelSource.getTunerChannel() ); + } + else + { + Log.error( "Tuner Controller - couldn't find the tuned channel " + + "to release it" ); + } + } + + /** + * Sets the Local Oscillator frequency to the middle of the currently + * locked frequency range, adjusting the left/right of a channel, if the + * middle falls within the locked range of any of the channels. Note: this + * will fail to set the correct frequency if multiple overlapping channel + * bandwidths are locked in the exact middle of the total locked channel + * frequency range. + * + * @throws SourceException + */ + public void updateLOFrequency() throws SourceException + { + Collections.sort( mTunedChannels ); + + int minLockedFrequency = mTunedChannels.get( 0 ).getMinFrequency(); + int maxLockedFrequency = mTunedChannels + .get( mTunedChannels.size() - 1 ).getMaxFrequency(); + + int middle = minLockedFrequency + + ( ( maxLockedFrequency - minLockedFrequency ) / 2 ); + int middleMin = middle - 10000; + int middleMax = middle + 10000; + + Iterator it = mTunedChannels.iterator(); + + while( it.hasNext() ) + { + TunerChannel lock = it.next(); + + //If a locked channel overlaps our middle frequency lockout, adjust to + //the left or the right of that channel, whichever is closer + if( lock.getMinFrequency() < middleMax && middleMin < lock.getMaxFrequency() ) + { + if( middleMax - lock.getMinFrequency() < lock.getMaxFrequency() - middleMin ) + { + middle = lock.getMinFrequency() - 10000; + } + else + { + middle = lock.getMaxFrequency() + 10000; + } + } + } + + mFrequencyController.setFrequency( middle ); + } + + /** + * Sets the listener to be notified any time that the tuner changes frequency + * or bandwidth/sample rate. + * + * Note: this is normally used by the Tuner. Any additional listeners can + * be registered on the tuner. + */ + public void addListener( FrequencyChangeListener listener ) + { + mFrequencyController.addListener( listener ); + } + + /** + * Removes the frequency change listener + */ + public void removeListener( FrequencyChangeListener listener ) + { + mFrequencyController.removeListener( listener ); + } +} diff --git a/src/source/tuner/TunerEditor.java b/src/source/tuner/TunerEditor.java new file mode 100644 index 00000000..ab51b52b --- /dev/null +++ b/src/source/tuner/TunerEditor.java @@ -0,0 +1,57 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package source.tuner; + +import gui.control.JFrequencyControl; +import source.SourceEditor; +import source.config.SourceConfigTuner; +import source.config.SourceConfiguration; + +public class TunerEditor extends SourceEditor +{ + private static final long serialVersionUID = 1L; + private JFrequencyControl mFrequencyControl; + + public TunerEditor( SourceConfiguration config ) + { + super( config ); + + initGUI(); + } + + public void reset() + { + mFrequencyControl.setFrequency( + ((SourceConfigTuner)mConfig).getFrequency(), false ); + } + + public void save() + { + ((SourceConfigTuner)mConfig).setFrequency( mFrequencyControl.getFrequency() ); + } + + private void initGUI() + { + mFrequencyControl = new JFrequencyControl(); + + mFrequencyControl.setFrequency( + ((SourceConfigTuner)mConfig).getFrequency(), false ); + + add( mFrequencyControl ); + } +} diff --git a/src/source/tuner/TunerGroupNode.java b/src/source/tuner/TunerGroupNode.java new file mode 100644 index 00000000..4a3c8247 --- /dev/null +++ b/src/source/tuner/TunerGroupNode.java @@ -0,0 +1,35 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package source.tuner; + +import controller.BaseNode; + +public class TunerGroupNode extends BaseNode +{ + private static final long serialVersionUID = 1L; + + public TunerGroupNode() + { + super( null ); + } + + public String toString() + { + return "Tuners [" + this.getChildCount() + "]"; + } +} diff --git a/src/source/tuner/TunerManager.java b/src/source/tuner/TunerManager.java new file mode 100644 index 00000000..615f2333 --- /dev/null +++ b/src/source/tuner/TunerManager.java @@ -0,0 +1,547 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package source.tuner; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Iterator; +import java.util.List; +import java.util.concurrent.RejectedExecutionException; + +import javax.usb.UsbDevice; +import javax.usb.UsbException; +import javax.usb.UsbHostManager; +import javax.usb.UsbHub; +import javax.usb.UsbServices; + +import log.Log; + +import org.usb4java.Context; +import org.usb4java.DeviceList; +import org.usb4java.LibUsb; +import org.usb4java.LibUsbException; + +import source.Source; +import source.SourceException; +import source.config.SourceConfigTuner; +import source.mixer.MixerManager; +import source.tuner.ettus.B100Tuner; +import source.tuner.fcd.FCDTuner; +import source.tuner.fcd.proV1.FCD1TunerController; +import source.tuner.fcd.proplusV2.FCD2TunerController; +import source.tuner.rtl.RTL2832Tuner; +import source.tuner.rtl.RTL2832TunerController; +import source.tuner.rtl.e4k.E4KTunerController; +import source.tuner.rtl.r820t.R820TTunerController; +import source.tuner.usb.USBTunerDevice; +import controller.ResourceManager; +import controller.channel.ProcessingChain; + +public class TunerManager +{ + private static final String sLOADED = "\t[LOADED]\t"; + private static final String sNOT_LOADED = "\t[NOT LOADED]\t"; + + private ResourceManager mResourceManager; + + private ArrayList mTuners = new ArrayList(); + + public TunerManager( ResourceManager resourceManager ) + { + mResourceManager = resourceManager; + loadTuners(); + } + + /** + * Iterates current tuners to get a tuner channel source for the frequency + * specified in the channel config's source config object + */ + public Source getSource( ProcessingChain channel ) + { + TunerChannelSource retVal = null; + + + if( channel.getChannel().getSourceConfiguration() + instanceof SourceConfigTuner ) + { + TunerChannel tunerChannel = channel.getChannel().getTunerChannel(); + + Iterator it = mTuners.iterator(); + + Tuner tuner; + + while( it.hasNext() && retVal == null ) + { + tuner = it.next(); + + try + { + retVal = tuner.getChannel( + mResourceManager.getThreadPoolManager(), tunerChannel ); + } + catch ( RejectedExecutionException ree ) + { + Log.error( "TunerManager - couldn't provide tuner channel " + + "source - " + ree.getLocalizedMessage() ); + } + catch ( SourceException e ) + { + Log.error( "TunerManager - error obtaining channel " + + "from tuner [" + tuner.getName() + "] - " + + e.getLocalizedMessage() ); + } + } + } + + return retVal; + } + + /** + * Get list of current tuners + */ + public List getTuners() + { + return mTuners; + } + + /** + * Loads all USB tuners and USB/Mixer tuner devices + */ + private void loadTuners() + { + mTuners.clear(); + + //Get list of attached USB tuner devices + ArrayList devices = getUSBTunerDevices(); + + StringBuilder sb = new StringBuilder(); + + sb.append( "Tuner Manager - discovered [" + devices.size() + + "] USB tuner devices\n" ); + + for( USBTunerDevice device: devices ) + { + String status = sNOT_LOADED; + String name = "Unknown"; + String reason = "No device driver class available"; + + TunerClass tunerClass = device.getTunerClass(); + + switch( tunerClass ) + { + case FUNCUBE_DONGLE_PRO: + //Locate the matching mixer tuner dataline + MixerTunerDataLine fcd1Dataline = + getMixerTunerDataLine( tunerClass.getTunerClass() ); + if( fcd1Dataline != null ) + { + FCD1TunerController fcd1Controller; + + try + { + fcd1Controller = new FCD1TunerController( device ); + + FCDTuner fcd1Tuner = + new FCDTuner( fcd1Dataline, fcd1Controller ); + + TunerConfiguration config = + getTunerConfiguration( fcd1Tuner ); + + if( config != null ) + { + fcd1Tuner.apply( config ); + } + + mTuners.add( fcd1Tuner ); + + name = fcd1Tuner.getName(); + status = sLOADED; + reason = null; + } + catch ( SourceException e ) + { + reason = "Controller Source Exception: " + + e.getLocalizedMessage(); + } + + } + else + { + reason = "Couldn't locate matching mixer/sound card device"; + } + break; + case FUNCUBE_DONGLE_PRO_PLUS: + //Locate the matching mixer tuner dataline + MixerTunerDataLine fcd2Dataline = + getMixerTunerDataLine( tunerClass.getTunerClass() ); + + if( fcd2Dataline != null ) + { + FCD2TunerController fcd2Controller; + + try + { + fcd2Controller = new FCD2TunerController( device ); + + FCDTuner fcd2Tuner = + new FCDTuner( fcd2Dataline, fcd2Controller ); + + TunerConfiguration config = + getTunerConfiguration( fcd2Tuner ); + + if( config != null ) + { + fcd2Tuner.apply( config ); + } + + mTuners.add( fcd2Tuner ); + + status = sLOADED; + name = fcd2Tuner.getName(); + reason = null; + } + catch ( SourceException e ) + { + reason = "Controller Source Exception: " + + e.getLocalizedMessage(); + } + } + else + { + reason = "Couldn't locate matching mixer/sound card device"; + } + break; + case GENERIC_2832: + case COMPRO_VIDEOMATE_U620F: + case COMPRO_VIDEOMATE_U650F: + case COMPRO_VIDEOMATE_U680F: + case DEXATEK_5217_DVBT: + case DEXATEK_DIGIVOX_MINI_II_REV3: + case DEXATEK_LOGILINK_VG002A: + case GENERIC_2838: + case GIGABYTE_GTU7300: + case GTEK_T803: + case LIFEVIEW_LV5T_DELUXE: + case MYGICA_TD312: + case PEAK_102569AGPK: + case PROLECTRIX_DV107669: + case SVEON_STV20: + case TERRATEC_CINERGY_T_REV1: + case TERRATEC_CINERGY_T_REV3: + case TERRATEC_NOXON_REV1_B3: + case TERRATEC_NOXON_REV1_B4: + case TERRATEC_NOXON_REV1_B7: + case TERRATEC_NOXON_REV1_C6: + case TERRATEC_NOXON_REV2: + case TERRATEC_T_STICK_PLUS: + case TWINTECH_UT40: + case ZAAPA_ZTMINDVBZP: + TunerType tunerType = tunerClass.getTunerClass(); + + if( tunerType == TunerType.RTL2832_VARIOUS ) + { + tunerType = RTL2832TunerController.identifyTunerType( device ); + } + + switch( tunerType ) + { + case ELONICS_E4000: + try + { + E4KTunerController controller = + new E4KTunerController( device ); + + controller.init(); + + RTL2832Tuner rtlTuner = + new RTL2832Tuner( tunerClass, controller ); + + TunerConfiguration config = + getTunerConfiguration( rtlTuner ); + + if( config != null ) + { + rtlTuner.apply( config ); + } + + mTuners.add( rtlTuner ); + status = sLOADED; + name = rtlTuner.getName(); + reason = null; + } + catch( SourceException se ) + { + status = sNOT_LOADED; + reason = "Error constructing E4K tuner " + + "controller - " + se.getLocalizedMessage(); + } + break; + case RAFAELMICRO_R820T: +// try +// { +// R820TTunerController controller = +// new R820TTunerController( device ); +// +// controller.init(); +// +// RTL2832Tuner rtlTuner = +// new RTL2832Tuner( tunerClass, controller ); +// +// +// TunerConfiguration config = +// getTunerConfiguration( rtlTuner ); +// +// if( config != null ) +// { +// rtlTuner.apply( config ); +// } +// +// mTuners.add( rtlTuner ); +// status = sLOADED; +// name = rtlTuner.getName(); +// reason = null; +// } +// catch( SourceException se ) +// { +// status = sNOT_LOADED; +// reason = "Error constructing E4K tuner " +// + "controller - " + se.getLocalizedMessage(); +// } +// break; + case FITIPOWER_FC0012: + case FITIPOWER_FC0013: + case RAFAELMICRO_R828D: + case UNKNOWN: + default: + status = sNOT_LOADED; + reason = "SDRTRunk doesn't currently support RTL2832 " + + "Dongle with [" + tunerType.toString() + + "] tuner"; + break; + } + break; + case ETTUS_USRP_B100: + mTuners.add( new B100Tuner( device ) ); + status = sLOADED; + reason = null; + break; + case UNKNOWN: + default: + break; + } + + /** + * Log the tuner loading status + */ + sb.append( status ); + sb.append( String.format( "%04X", + device.getDevice().getUsbDeviceDescriptor().idVendor() ) ); + sb.append( ":" ); + sb.append( String.format( "%04X", + device.getDevice().getUsbDeviceDescriptor().idProduct() ) ); + sb.append( " " ); +// sb.append( device.getTunerType().getVendorDescription() ); +// sb.append( " " ); +// sb.append( device.getTunerType().getDeviceDescription() ); + + sb.append( name ); + + if( reason != null ) + { + sb.append( " - " ); + sb.append( reason ); + } + + sb.append( "\n" ); + } + + Log.header( "Configuring USB Tuners" ); + Log.info( sb.toString() ); + } + + /** + * Get all USB tuner devices + */ + private ArrayList getUSBTunerDevices() + { + StringBuilder sb = new StringBuilder(); + + List devices = getUSBDevices(); + + sb.append( "TunerManager - discovered [" + devices.size() + + "] attached usb devices\n" ); + + ArrayList tuners = new ArrayList(); + + for (UsbDevice device : devices ) + { + String status = "\t[NOT RECOGNIZED]\t"; + + TunerClass type = TunerClass.valueOf( device.getUsbDeviceDescriptor() ); + + if( type != TunerClass.UNKNOWN ) + { + status = "\t[RECOGNIZED] \t"; + tuners.add( new USBTunerDevice( device, type) ); + } + + sb.append( status ); + sb.append( device.toString() ); + + if( type != TunerClass.UNKNOWN ) + { + sb.append( " " ); + sb.append( type.getVendorDescription() ); + sb.append( " " ); + sb.append( type.getDeviceDescription() ); + } + + sb.append( "\n" ); + } + + Log.header( "USB Device Discovery" ); + Log.info( sb.toString() ); + + return tuners; + } + + /** + * Gets all USB devices from the root hub downward, recursively, or + * returns empty list if none are discovered + */ + public static List getUSBDevices() + { + ArrayList devices = new ArrayList(); + + try + { + UsbServices services = UsbHostManager.getUsbServices(); + devices = getUsbChildDevices( services.getRootUsbHub() ); + } + catch( SecurityException e ) + { + Log.error( "TunerManager - security exception while getting " + + "USB devices" ); + } + catch( UsbException e ) + { + Log.error( "TunerManager - usb exception while getting USB " + + "devices - " + e.getLocalizedMessage() ); + } + + return devices; + } + + /** + * Get (recursively) all usb child devices below the usb hub + */ + public static ArrayList getUsbChildDevices( UsbHub hub ) + { + ArrayList devices = new ArrayList(); + + for ( UsbDevice device : (List) hub.getAttachedUsbDevices() ) + { + devices.add( device ); + + if ( device.isUsbHub() ) + { + devices.addAll( getUsbChildDevices( (UsbHub)device ) ); + } + } + + return devices; + } + + /** + * Get a stored tuner configuration or create a default tuner configuration + * for the tuner type, connected at the address. + * + * Note: a named tuner configuration will be stored for for each tuner type + * and address combination. + * @param type - tuner type + * @param address - current usb address/port + * @return - stored tuner configuration or default configuration + */ + private TunerConfiguration getTunerConfiguration( Tuner tuner ) + { + TunerConfigurationAssignment selected = mResourceManager + .getSettingsManager().getSelectedTunerConfiguration( + tuner.getTunerType(), tuner.getUniqueID() ); + + ArrayList configs = mResourceManager + .getSettingsManager().getTunerConfigurations( tuner.getTunerType() ); + + TunerConfiguration config = null; + + if( selected != null ) + { + for( TunerConfiguration tunerConfig: configs ) + { + if( tunerConfig.getName().contentEquals( selected.getTunerConfigurationName() ) ) + { + config = tunerConfig; + } + } + } + else + { + config = configs.get( 0 ); + } + + //If we're still null at this point, create a default config + if( config == null ) + { + config = mResourceManager.getSettingsManager() + .addNewTunerConfiguration( tuner.getTunerType(), "Default" ); + } + + //Persist the config as the selected tuner configuration. The method + //auto-saves the setting + mResourceManager.getSettingsManager().setSelectedTunerConfiguration( + tuner.getTunerType(), tuner.getUniqueID(), config ); + + return config; + } + + /** + * Gets the first tuner mixer dataline that corresponds to the tuner class. + * + * Note: this method is not currently able to align multiple tuner mixer + * data lines of the same tuner type. If you have multiple Funcube Dongle + * tuners of the same TYPE, there is no guarantee that you will get the + * correct mixer. + * + * @param tunerClass + * @return + */ + private MixerTunerDataLine getMixerTunerDataLine( TunerType tunerClass ) + { + Collection datalines = + MixerManager.getInstance().getMixerTunerDataLines(); + + for( MixerTunerDataLine mixerTDL: datalines ) + { + if( mixerTDL.getMixerTunerType().getTunerClass() == tunerClass ) + { + return mixerTDL; + } + } + + return null; + } +} diff --git a/src/source/tuner/TunerNode.java b/src/source/tuner/TunerNode.java new file mode 100644 index 00000000..e5b45229 --- /dev/null +++ b/src/source/tuner/TunerNode.java @@ -0,0 +1,127 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package source.tuner; + +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.text.DecimalFormat; + +import javax.swing.JMenuItem; +import javax.swing.JPanel; +import javax.swing.JPopupMenu; + +import log.Log; +import source.SourceException; +import spectrum.SpectrumFrame; +import controller.BaseNode; + +public class TunerNode extends BaseNode implements FrequencyChangeListener +{ + private static final long serialVersionUID = 1L; + private static DecimalFormat FREQUENCY_FORMAT = new DecimalFormat( "0.00000" ); + private static DecimalFormat SAMPLE_RATE_FORMAT = new DecimalFormat( "0.000" ); + + private int mFrequency; + private int mBandwidth; + + public TunerNode( Tuner tuner ) + { + super( tuner ); //does this make it a super tuner? + + tuner.addListener( this ); + + try + { + mFrequency = tuner.getFrequency(); + mBandwidth = tuner.getSampleRate(); + } + catch ( SourceException e ) + { + Log.error( "TunerNode - error reading frequency and sample rate " + + "from tuner - " + e.getLocalizedMessage() ); + } + } + + @Override + public JPanel getEditor() + { + return getTuner().getEditor( getModel().getResourceManager() ); + } + + public Tuner getTuner() + { + return (Tuner)getUserObject(); + } + + public String toString() + { + StringBuilder sb = new StringBuilder(); + + sb.append( getTuner().getName() ); + + sb.append( " " ); + sb.append( FREQUENCY_FORMAT.format( (double)mFrequency / 1E6d ) ); + sb.append( " [" ); + sb.append( SAMPLE_RATE_FORMAT.format( (double)mBandwidth / 1E6d ) ); + sb.append( "] MHz" ); + + return sb.toString(); + } + + public JPopupMenu getContextMenu() + { + JPopupMenu retVal = new JPopupMenu(); + + JMenuItem showSpectrumItem = new JMenuItem( "Show in Main Spectrum" ); + showSpectrumItem.addActionListener( new ActionListener() + { + @Override + public void actionPerformed( ActionEvent e ) + { + getModel().fireTunerSelectedEvent( getTuner() ); + } + } ); + + retVal.add( showSpectrumItem ); + + JMenuItem newSpectrumItem = new JMenuItem( "New Spectrum Window" ); + newSpectrumItem.addActionListener( new ActionListener() + { + @Override + public void actionPerformed( ActionEvent e ) + { + SpectrumFrame frame = + new SpectrumFrame( getModel().getResourceManager(), + getTuner() ); + } + } ); + + retVal.add( newSpectrumItem ); + + return retVal; + } + + @Override + public void frequencyChanged( int frequency, int bandwidth ) + { + mFrequency = frequency; + mBandwidth = bandwidth; + + getModel().nodeChanged( TunerNode.this ); + } +} diff --git a/src/source/tuner/TunerSelectionListener.java b/src/source/tuner/TunerSelectionListener.java new file mode 100644 index 00000000..52ebff6d --- /dev/null +++ b/src/source/tuner/TunerSelectionListener.java @@ -0,0 +1,23 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package source.tuner; + +public interface TunerSelectionListener +{ + public void tunerSelected( Tuner tuner ); +} diff --git a/src/source/tuner/TunerType.java b/src/source/tuner/TunerType.java new file mode 100644 index 00000000..0714eb53 --- /dev/null +++ b/src/source/tuner/TunerType.java @@ -0,0 +1,46 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package source.tuner; + +public enum TunerType +{ + ELONICS_E4000( "E4000" ), + ETTUS_WBX( "WBX" ), + ETTUS_VARIOUS( "Ettus Tuner" ), + FCI_FC2580( "FC2580" ), + FITIPOWER_FC0012( "FC0012" ), + FITIPOWER_FC0013( "FC0013" ), + FUNCUBE_DONGLE_PRO( "Funcube Dongle Pro" ), + FUNCUBE_DONGLE_PRO_PLUS( "Funcube Dongle Pro Plus" ), + RAFAELMICRO_R820T( "R820T" ), + RAFAELMICRO_R828D( "R828D" ), + RTL2832_VARIOUS( "Generic" ), + UNKNOWN( "Unknown" ); + + private String mLabel; + + private TunerType( String label ) + { + mLabel = label; + } + + public String getLabel() + { + return mLabel; + } +} diff --git a/src/source/tuner/ettus/B100Tuner.java b/src/source/tuner/ettus/B100Tuner.java new file mode 100644 index 00000000..7187d88a --- /dev/null +++ b/src/source/tuner/ettus/B100Tuner.java @@ -0,0 +1,106 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package source.tuner.ettus; + +import java.util.concurrent.RejectedExecutionException; + +import javax.swing.JPanel; + +import log.Log; +import source.SourceException; +import source.tuner.Tuner; +import source.tuner.TunerChannel; +import source.tuner.TunerChannelSource; +import source.tuner.TunerClass; +import source.tuner.TunerConfiguration; +import source.tuner.TunerType; +import source.tuner.usb.USBTunerDevice; +import controller.ResourceManager; +import controller.ThreadPoolManager; + +public class B100Tuner extends Tuner +{ + private USBTunerDevice mDevice; + + public B100Tuner( USBTunerDevice device ) + { + //TODO: this should extend a USBTuner class, so that we can push the + //complex sample listener and the frequency change listener up to that class + + super( "Ettus B100 with WBX Tuner" ); + + mDevice = device; + } + + @Override + public TunerClass getTunerClass() + { + return TunerClass.ETTUS_USRP_B100; + } + + @Override + public TunerType getTunerType() + { + return TunerType.ETTUS_WBX; + } + + @Override + public JPanel getEditor( ResourceManager resourceManager ) + { + return new B100TunerEditorPanel( this, resourceManager ); + } + + @Override + public void apply( TunerConfiguration config ) throws SourceException + { + Log.error( "B100 Tuner - can't apply tuner configuration" ); + } + + @Override + public int getSampleRate() + { + return 0; + } + + @Override + public int getFrequency() throws SourceException + { + return 0; + } + + @Override + public TunerChannelSource getChannel( ThreadPoolManager threadPoolManager, + TunerChannel channel ) throws RejectedExecutionException, SourceException + { + //TODO: pass this call to the tuner controller + return null; + } + + @Override + public void releaseChannel( TunerChannelSource source ) + { + //TODO: pass this call to the tuner controller + } + + @Override + public String getUniqueID() + { + // TODO Auto-generated method stub + return null; + } +} diff --git a/src/source/tuner/ettus/B100TunerEditorPanel.java b/src/source/tuner/ettus/B100TunerEditorPanel.java new file mode 100644 index 00000000..964ac4ee --- /dev/null +++ b/src/source/tuner/ettus/B100TunerEditorPanel.java @@ -0,0 +1,50 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package source.tuner.ettus; + +import javax.swing.BorderFactory; +import javax.swing.JLabel; +import javax.swing.JPanel; + +import net.miginfocom.swing.MigLayout; +import controller.ResourceManager; + +public class B100TunerEditorPanel extends JPanel +{ + private static final long serialVersionUID = 1L; + + private B100Tuner mTuner; + private ResourceManager mResourceManager; + + public B100TunerEditorPanel( B100Tuner tuner, ResourceManager resourceManager ) + { + mTuner = tuner; + mResourceManager = resourceManager; + + init(); + } + + private void init() + { + setLayout( new MigLayout( "", "[grow,fill]", "[grow,fill]" ) ); + + setBorder( BorderFactory.createTitledBorder( "Ettus B100 Tuner" ) ); + + add( new JLabel( "Note: SDRTrunk can only recognize the B100 at this time. It cannot use this tuner") ); + } +} diff --git a/src/source/tuner/fcd/FCDCommand.java b/src/source/tuner/fcd/FCDCommand.java new file mode 100644 index 00000000..821d933b --- /dev/null +++ b/src/source/tuner/fcd/FCDCommand.java @@ -0,0 +1,163 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package source.tuner.fcd; + +public enum FCDCommand +{ + /** + * Note: the javaxUSB and usb4Java libraries are slightly different than + * the signal9 HID libraries used in C, so you will see variations in the + * byte array length and indexes, when comparing this setup to the qthid + * source code. + */ + //Bootloader Mode Commands + BL_QUERY( (byte) 0x01, 64 ), //01 Decimal + BL_RESET( (byte) 0x08, 2 ), //08 + BL_ERASE( (byte) 0x19, 2 ), //24 + BL_SET_BYTE_ADDR( (byte) 0x1A, 6 ), //25 + BL_GET_BYTE_ADDR_RANGE( (byte) 0x1B, 6 ), //26 + BL_WRITE_FLASH_BLOCK( (byte) 0x1C, 52 ), //27 + BL_READ_FLASH_BLOCK( (byte) 0x1D, 48 ), //28 + + //Application Mode Commands + APP_SET_FREQUENCY_KHZ( (byte)0x64, 5 ), //100 + APP_SET_FREQUENCY_HZ( (byte)0x65, 6 ), //101 + APP_GET_FREQUENCY_HZ( (byte)0x66, 6 ), //102 + APP_GET_IF_RSSI( (byte) 0x68, 3 ), //104 + APP_GET_PLL_LOCKED( (byte) 0x69, 3 ), //105 + APP_SET_DC_CORRECTION( (byte) 0x6A, 5 ), //106 + APP_GET_DC_CORRECTION( (byte) 0x6B, 6 ), //107 + APP_SET_IQ_CORRECTION( (byte) 0x6C, 5 ), //108 + APP_GET_IQ_CORRECTION( (byte) 0x6D, 6 ), //109 + APP_SET_LNA_GAIN( (byte)0x6E, 2 ), //110 + APP_SET_LNA_ENHANCE( (byte)0x6F, 2 ), //111 + APP_SET_BAND( (byte)0x70, 2 ), //112 + APP_SET_RF_FILTER( (byte)0x71, 2 ), //113 + APP_SET_MIXER_GAIN( (byte)0x72, 2 ), //114 + APP_SET_BIAS_CURRENT( (byte)0x73, 2 ), //115 + APP_SET_MIXER_FILTER( (byte)0x74, 2 ), //116 + APP_SET_IF_GAIN1( (byte)0x75, 2 ), //117 + APP_SET_IF_GAIN_MODE( (byte)0x76, 2 ), //118 + APP_SET_IF_RC_FILTER( (byte)0x77, 2 ), //119 + APP_SET_IF_GAIN2( (byte)0x78, 2 ), //120 + APP_SET_IF_GAIN3( (byte)0x79, 2 ), //121 + APP_SET_IF_FILTER( (byte)0x7A, 2 ), //122 + APP_SET_IF_GAIN4( (byte)0x7B, 2 ), //123 + APP_SET_IF_GAIN5( (byte)0x7C, 2 ), //124 + APP_SET_IF_GAIN6( (byte)0x7D, 2 ), //125 + APP_SET_BIAS_TEE( (byte)0x7E, 2 ), //126 + + APP_GET_LNA_GAIN( (byte)0x96, 3 ), //150 + APP_GET_LNA_ENHANCE( (byte)0x97, 3 ), //151 + APP_GET_BAND( (byte)0x98, 3 ), //152 + APP_GET_RF_FILTER( (byte)0x99, 3 ), //153 + APP_GET_MIXER_GAIN( (byte)0x9A, 3 ), //154 + APP_GET_BIAS_CURRENT( (byte)0x9B, 3 ), //155 + APP_GET_MIXER_FILTER( (byte)0x9C, 3 ), //156 + APP_GET_IF_GAIN1( (byte)0x9D, 3 ), //157 + APP_GET_IF_GAIN_MODE( (byte)0x9E, 3 ), //158 + APP_GET_IF_RC_FILTER( (byte)0x9F, 3 ), //159 + APP_GET_IF_GAIN2( (byte)0xA0, 3 ), //160 + APP_GET_IF_GAIN3( (byte)0xA1, 3 ), //161 + APP_GET_IF_FILTER( (byte)0xA2, 3 ), //162 + APP_GET_IF_GAIN4( (byte)0xA3, 3 ), //163 + APP_GET_IF_GAIN5( (byte)0xA4, 3 ), //164 + APP_GET_IF_GAIN6( (byte)0xA5, 3 ), //165 + APP_GET_BIAS_TEE( (byte)0xA6, 3 ), //166 + + APP_SEND_I2C_BYTE( (byte)0xC8, 2 ), //200 + APP_RECV_I2C_BYTE( (byte)0xC9, 3 ), //201 + + APP_RESET( (byte)0xFF, 1 ); //255 + + /** + * Single-byte value to use for the command + */ + private byte mCommandByte; + + /** + * Required array length + */ + private int mArrayLength; + + private FCDCommand( byte value, int responseLength ) + { + mCommandByte = value; + mArrayLength = responseLength; + } + + /** + * Returns a byte array of the specified length with the command byte value + * set in index position zero. Fill in additional command arguments prior + * to sending to the device + */ + public byte[] getRequestTemplate() + { + byte[] retVal = new byte[ 64 ]; + + retVal[ 0 ] = mCommandByte; + retVal[ 1 ] = (byte)0x0; + + return retVal; + } + + /** + * Returns an empty byte array of the correct length to receive the response + * from the associated command + */ + public byte[] getResponseTemplate() + { + return new byte[ 64 ]; + } + + /** + * Byte value of the command + */ + public byte getCommand() + { + return mCommandByte; + } + + /** + * Total array length required to send the command or receive the response. + * + * Index 0: command is echoed back + * Index 1: 0-fail, 1-success + */ + public int getArrayLength() + { + return mArrayLength; + } + + /** + * Checks the response array to verify the pass/fail value that is placed + * in array index position 1, in responses from the device + */ + public static boolean checkResponse( FCDCommand command, byte[] response ) + { + boolean valid = false; + + if( command != null && response != null ) + { + valid = response[ 0 ] == command.getCommand() && + response[ 1 ] == 1; + } + + return valid; + } +} diff --git a/src/source/tuner/fcd/FCDModel.java b/src/source/tuner/fcd/FCDModel.java new file mode 100644 index 00000000..8c359b71 --- /dev/null +++ b/src/source/tuner/fcd/FCDModel.java @@ -0,0 +1,53 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package source.tuner.fcd; + +public enum FCDModel +{ + FUNCUBE_DONGLE_PRO( "FunCube Dongle Pro" ), + FUNCUBE_DONGLE_PRO_PLUS( "FunCube Dongle Pro Plus"), + FUNCUBE_UNKNOWN( "Unknown" ); + + private String mLabel; + + private FCDModel( String label ) + { + mLabel = label; + } + + public String getLabel() + { + return mLabel; + } + + public static FCDModel getFCD( String board ) + { + FCDModel retVal = FUNCUBE_UNKNOWN; + + if( board.equalsIgnoreCase( "Brd 1.1" ) ) + { + retVal = FUNCUBE_DONGLE_PRO; + } + else if( board.equalsIgnoreCase( "Brd 2.0" ) ) + { + retVal = FUNCUBE_DONGLE_PRO_PLUS; + } + + return retVal; + } +} diff --git a/src/source/tuner/fcd/FCDTuner.java b/src/source/tuner/fcd/FCDTuner.java new file mode 100644 index 00000000..8aa69767 --- /dev/null +++ b/src/source/tuner/fcd/FCDTuner.java @@ -0,0 +1,125 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package source.tuner.fcd; + +import java.util.concurrent.RejectedExecutionException; + +import javax.swing.JPanel; + +import sample.Listener; +import source.SourceException; +import source.mixer.ShortAdapter; +import source.tuner.FrequencyChangeListener; +import source.tuner.MixerTuner; +import source.tuner.MixerTunerDataLine; +import source.tuner.TunerChannel; +import source.tuner.TunerChannelSource; +import source.tuner.TunerClass; +import source.tuner.TunerConfiguration; +import source.tuner.TunerType; +import controller.ResourceManager; +import controller.ThreadPoolManager; + +public class FCDTuner extends MixerTuner +{ + private FCDTunerController mController; + + public FCDTuner( MixerTunerDataLine mixerTDL, + FCDTunerController controller ) + { + super( controller.getConfiguration().toString(), + mixerTDL, + new ShortAdapter() ); + + mController = controller; + mController.addListener( (FrequencyChangeListener)this ); + } + + public FCDTunerController getController() + { + return mController; + } + + @Override + public TunerClass getTunerClass() + { + return mController.getTunerClass(); + } + + @Override + public TunerType getTunerType() + { + return mController.getTunerType(); + } + + @Override + public String getUniqueID() + { + return mController.getAddress(); + } + + @Override + public JPanel getEditor( ResourceManager resourceManager ) + { + return mController.getEditor( this, resourceManager ); + } + + @Override + public void apply( TunerConfiguration config )throws SourceException + { + mController.apply( config ); + } + + @Override + public int getSampleRate() + { + return (int)mMixerTunerType.getAudioFormat().getSampleRate(); + } + + @Override + public int getFrequency() throws SourceException + { + return mController.getFrequency(); + } + + @Override + public TunerChannelSource getChannel( ThreadPoolManager threadPoolManager, + TunerChannel tunerChannel ) throws RejectedExecutionException, + SourceException + { + return mController.getChannel( threadPoolManager, this, tunerChannel ); + } + + /** + * Releases the tuned channel so that the tuner controller can tune to + * other frequencies as needed. + */ + @Override + public void releaseChannel( TunerChannelSource source ) + { + if( source != null ) + { + /* Unregister for receiving samples */ + removeListener( (Listener)source ); + + /* Tell the controller to release the channel and cleanup */ + /* This will release the channel as a frequency change listener */ + mController.releaseChannel( source ); + } + } +} diff --git a/src/source/tuner/fcd/FCDTunerController.java b/src/source/tuner/fcd/FCDTunerController.java new file mode 100644 index 00000000..11b9043b --- /dev/null +++ b/src/source/tuner/fcd/FCDTunerController.java @@ -0,0 +1,497 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package source.tuner.fcd; + +import javax.swing.JPanel; +import javax.usb.UsbClaimException; +import javax.usb.UsbConfiguration; +import javax.usb.UsbDevice; +import javax.usb.UsbEndpoint; +import javax.usb.UsbException; +import javax.usb.UsbInterface; +import javax.usb.UsbInterfacePolicy; +import javax.usb.UsbPipe; + +import log.Log; +import source.SourceException; +import source.tuner.TunerClass; +import source.tuner.TunerConfiguration; +import source.tuner.TunerController; +import source.tuner.TunerType; +import source.tuner.fcd.proV1.FCD1TunerController.Block; +import source.tuner.usb.USBTunerDevice; +import controller.ResourceManager; + +public abstract class FCDTunerController extends TunerController +{ + public final static int sINT_NULL_VALUE = -1; + public final static long sLONG_NULL_VALUE = -1l; + public final static double sDOUBLE_NULL_VALUE = -1.0D; + + public final static byte sFCD_INTERFACE = (byte)0x2; + public final static byte sFCD_ENDPOINT_IN = (byte)0x82; + public final static byte sFCD_ENDPOINT_OUT = (byte)0x2; + + public final static boolean sFCD_FORCE_CLAIM_HID_INTERFACE = true; + + private UsbDevice mFuncube; + private UsbConfiguration mUsbConfiguration; + private UsbInterface mFuncubeHID; + private UsbPipe mPipeIn; + private UsbPipe mPipeOut; + private FCDConfiguration mConfiguration = new FCDConfiguration(); + private int mSampleRate; + + public FCDTunerController( USBTunerDevice device, + int minTunableFrequency, + int maxTunableFrequency ) throws SourceException + { + super( minTunableFrequency, maxTunableFrequency ); + + mFuncube = device.getDevice(); + mUsbConfiguration = mFuncube.getActiveUsbConfiguration(); + mFuncubeHID = mUsbConfiguration.getUsbInterface( sFCD_INTERFACE ); + } + + public abstract int getCurrentSampleRate() throws SourceException; + + /** + * Returns the tuner class. Since this is a generic container for both + * types of funcube, we let the controller self identify the class and type + * + * @return - tuner class + */ + public abstract TunerClass getTunerClass(); + + /** + * Returns the tuner type. Since this is a generic container for both + * types of funcube, we let the controller self identify the class and type + * + * @return - tuner type + */ + public abstract TunerType getTunerType(); + + /** + * FCD editor panel + */ + public abstract JPanel getEditor( FCDTuner tuner, + ResourceManager resourceManager ); + + /** + * Applies the settings in the tuner configuration + */ + public abstract void apply( TunerConfiguration config ) + throws SourceException; + + /** + * Returns the applied tuner configuration or null if one hasn't yet been + * applied + */ + public abstract TunerConfiguration getTunerConfiguration(); + + /** + * USB address + */ + public String getAddress() + { + return mFuncube.toString(); + } + + /** + * Set fcd interface mode + */ + public void setFCDMode( Mode mode ) throws UsbException, UsbClaimException + { + byte[] response = null; + + switch( mode ) + { + case APPLICATION: + response = send( FCDCommand.BL_QUERY.getRequestTemplate(), + FCDCommand.BL_QUERY.getResponseTemplate() ); + break; + case BOOTLOADER: + response = send( FCDCommand.APP_RESET.getRequestTemplate(), + FCDCommand.APP_RESET.getResponseTemplate() ); + break; + case ERROR: + case UNKNOWN: + break; + } + + if( response != null ) + { + mConfiguration.set( response ); + } + else + { + mConfiguration.setModeUnknown(); + } + } + + /** + * Sets the actual (uncorrected) device frequency + */ + public void setTunedFrequency( int frequency ) throws SourceException + { + try + { + send( FCDCommand.APP_SET_FREQUENCY_HZ, frequency ); + } + catch( Exception e ) + { + throw new SourceException( "Couldn't set FCD Local " + + "Oscillator Frequency [" + frequency + "]", e ); + } + } + + /** + * Gets the actual (uncorrected) device frequency + */ + public int getTunedFrequency() throws SourceException + { + try + { + return (int)send( FCDCommand.APP_GET_FREQUENCY_HZ ); + } + catch( Exception e ) + { + throw new SourceException( "FCDTunerController - " + + "couldn't get LO frequency", e ); + } + } + + /** + * Returns the FCD device configuration + */ + public FCDConfiguration getConfiguration() + { + return mConfiguration; + } + + /** + * Claims the FCD USB HID interface. If another application currently has + * the interface claimed, the sFCD_FORCE_CLAIM_HID_INTERFACE setting + * will dictate if the interface is forcibly claimed from the other + * application + */ + private boolean claim() throws UsbException, UsbClaimException + { + boolean claimed = false; + + if( !mFuncubeHID.isClaimed() ) + { + mFuncubeHID.claim( new UsbInterfacePolicy() + { + @Override + public boolean forceClaim( UsbInterface arg0 ) + { + return sFCD_FORCE_CLAIM_HID_INTERFACE; + } + } ); + + claimed = true; + } + else + { + Log.info( "Attempted to claim the Funcube HID interface, but it's in use by another program" ); + } + + return claimed; + } + + /** + * Releases the claimed USB (FCD) interface + */ + private void release() + { + mPipeIn = null; + mPipeOut = null; + + try + { + mFuncubeHID.release(); + } + catch ( Exception e ) + { + Log.error( "Exception thrown while releasing the funcube HID interface" + e.getLocalizedMessage() ); + } + } + + /** + * Convenience wrapper for commands that don't take arguments + */ + public long send( FCDCommand command ) throws UsbException, UsbClaimException + { + return send( command, sLONG_NULL_VALUE ); + } + + /** + * Sends the command, optionally placing the argument value parsed into + * little endian bytes in the request array. Returns any integer value that + * part of the response, or sINT_NULL_VALUE (-1) if it fails. + * + * Note: we use a long return value to avoid any issues with the MSB of a + * 32-bit response value being misinterpreted as the integer sign bit. + * + * @param command + * @param commandArgument - command argument. Use -1 for no-argument commands + * @return - response long value, or -1 if fails + * @throws UsbException + * @throws UsbClaimException + */ + public long send( FCDCommand command, long commandArgument ) throws UsbException, + UsbClaimException + { + long retVal = sLONG_NULL_VALUE; + + byte[] request = command.getRequestTemplate(); + + if( commandArgument != sLONG_NULL_VALUE ) + { + //Parse the value argument into the request byte indices, according to command length + for( int x = 1; x < command.getArrayLength(); x++ ) + { + //Little endian + request[ x ] = (byte)( 0xFF & + Long.rotateRight( commandArgument, ( x - 1 ) * 8 ) ); + } + } + + byte[] response = send( request, command.getResponseTemplate() ); + + if( response != null ) + { + retVal = parseResponse( command, response ); + } + + return retVal; + } + + /** + * Sends the request byte array to the device and returns the response array + * filled with the response bytes from the device, or returns null if + * something failed along the way + */ + private byte[] send( byte[] request, byte[] response ) + throws UsbException, UsbClaimException + { + byte[] retVal = null; + + if( mPipeIn != null && mPipeOut != null ) + { + mPipeOut.syncSubmit( request ); + + mPipeIn.syncSubmit( response ); + + retVal = response; + } + + return retVal; + } + + /** + * Parses a multi-byte integer response value from a byte array that is + * 2 or more bytes long + */ + private long parseResponse( FCDCommand command, byte[] response ) + { + long retVal = 0; + + //Parse the value argument into the request byte indices, according to command length + for( int x = 2; x < command.getArrayLength(); x++ ) + { + retVal += Long.rotateLeft( ( response[ x ] & 0xFF ), ( x - 2 ) * 8 ); + } + + return retVal; + } + + protected void open() throws UsbException, UsbClaimException + { + if( claim() ) + { + //Get the USB endpoints + UsbEndpoint endpointIn = + mFuncubeHID.getUsbEndpoint( sFCD_ENDPOINT_IN ); + + UsbEndpoint endpointOut = + mFuncubeHID.getUsbEndpoint( sFCD_ENDPOINT_OUT ); + + //Setup the pipes + if( endpointIn != null && endpointOut != null ) + { + mPipeOut = endpointOut.getUsbPipe(); + mPipeOut.open(); + + mPipeIn = endpointIn.getUsbPipe(); + mPipeIn.open(); + + setFCDMode( Mode.APPLICATION ); + } + else + { + Log.error( "FCD Pro Tuner Controller - couldn't gain access " + + "to the USB in/out endpoints or pipes." ); + } + } + else + { + Log.error( "FCD Pro Tuner Controller - couldn't claim the USB " + + "interface. Check to see if another program is using it." ); + } + } + + protected void close() + { + release(); + } + + public class FCDConfiguration + { + private String mConfig; + private Mode mMode; + + public FCDConfiguration() + { + mConfig = null; + mMode = Mode.UNKNOWN; + } + + private void setModeUnknown() + { + mConfig = null; + mMode = Mode.UNKNOWN; + } + + public void set( byte[] data ) + { + if( FCDCommand.checkResponse( FCDCommand.BL_QUERY, data ) || + FCDCommand.checkResponse( FCDCommand.APP_RESET, data ) ) + { + mConfig = new String( data ); + mMode = Mode.getMode( mConfig ); + } + else + { + mConfig = null; + mMode = Mode.ERROR; + } + } + + public Mode getMode() + { + return mMode; + } + + public FCDModel getModel() + { + FCDModel retVal = FCDModel.FUNCUBE_UNKNOWN; + + switch( mMode ) + { + case APPLICATION: + retVal = FCDModel.getFCD( mConfig.substring( 15, 22 ) ); + break; + case BOOTLOADER: + case UNKNOWN: + case ERROR: + break; + } + + return retVal; + } + + public Block getBandBlocking() + { + Block retVal = Block.UNKNOWN; + + switch( mMode ) + { + case APPLICATION: + retVal = Block.getBlock( mConfig.substring( 23, 33 ).trim() ); + break; + case BOOTLOADER: + case UNKNOWN: + case ERROR: + break; + } + + return retVal; + } + + public String getFirmware() + { + String retVal = null; + + switch( mMode ) + { + case APPLICATION: + retVal = mConfig.substring( 9, 14 ); + break; + case BOOTLOADER: + case UNKNOWN: + case ERROR: + break; + } + + return retVal; + } + + public String toString() + { + return getModel().getLabel(); + } + } + + public enum Mode + { + APPLICATION, + BOOTLOADER, + ERROR, + UNKNOWN; + + public static Mode getMode( String config ) + { + Mode retVal = UNKNOWN; + + if( config == null ) + { + retVal = ERROR; + } + else + { + if( config.length() >= 8 ) + { + String mode = config.substring( 2, 8 ).trim(); + + if( mode.equalsIgnoreCase( "FCDAPP" ) ) + { + retVal = APPLICATION; + } + else if( mode.equalsIgnoreCase( "FCDBL" ) ) + { + retVal = BOOTLOADER; + } + } + } + + return retVal; + } + } +} diff --git a/src/source/tuner/fcd/FCDTunerDetailsPanel.java b/src/source/tuner/fcd/FCDTunerDetailsPanel.java new file mode 100644 index 00000000..1e020d72 --- /dev/null +++ b/src/source/tuner/fcd/FCDTunerDetailsPanel.java @@ -0,0 +1,48 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package source.tuner.fcd; + +import javax.swing.BorderFactory; +import javax.swing.JLabel; +import javax.swing.JPanel; + +import source.tuner.rtl.RTL2832TunerController.Descriptor; +import net.miginfocom.swing.MigLayout; + +public class FCDTunerDetailsPanel extends JPanel +{ + private static final long serialVersionUID = 1L; + private JLabel mAddress; + private JLabel mCellBlock; + private JLabel mFirmware; + + public FCDTunerDetailsPanel( FCDTunerController controller ) + { + setLayout( new MigLayout( "fill,wrap 2", "[right][left]", "[][][][grow]" ) ); + + add( new JLabel( "USB Address:" ) ); + add( new JLabel( controller.getAddress() ) ); + + add( new JLabel( "Cellular Band:" ) ); + add( new JLabel( controller.getConfiguration().getBandBlocking() + .getLabel() ) ); + + add( new JLabel( "Firmware:" ) ); + add( new JLabel( controller.getConfiguration().getFirmware() ) ); + } +} diff --git a/src/source/tuner/fcd/proV1/FCD1TunerConfiguration.java b/src/source/tuner/fcd/proV1/FCD1TunerConfiguration.java new file mode 100644 index 00000000..29a7b0f4 --- /dev/null +++ b/src/source/tuner/fcd/proV1/FCD1TunerConfiguration.java @@ -0,0 +1,145 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package source.tuner.fcd.proV1; + +import javax.xml.bind.annotation.XmlAttribute; + +import source.tuner.TunerConfiguration; +import source.tuner.TunerType; +import source.tuner.fcd.proV1.FCD1TunerController.LNAEnhance; +import source.tuner.fcd.proV1.FCD1TunerController.LNAGain; +import source.tuner.fcd.proV1.FCD1TunerController.MixerGain; + +public class FCD1TunerConfiguration extends TunerConfiguration +{ + private double mFrequencyCorrection = 22.0d; + private double mInphaseDCCorrection = 0.0d; + private double mQuadratureDCCorrection = 0.0d; + private double mPhaseCorrection = 0.0d; + private double mGainCorrection = 0.0d; + + private LNAGain mLNAGain = LNAGain.LNA_GAIN_PLUS_20_0; + private LNAEnhance mLNAEnhance = LNAEnhance.LNA_ENHANCE_OFF; + private MixerGain mMixerGain = MixerGain.MIXER_GAIN_PLUS_12_0; + + /** + * Default constructor for JAXB + */ + public FCD1TunerConfiguration() + { + this( "Default" ); + } + + public FCD1TunerConfiguration( String name ) + { + super( name ); + } + + @Override + public TunerType getTunerType() + { + return TunerType.FUNCUBE_DONGLE_PRO; + } + + public LNAGain getLNAGain() + { + return mLNAGain; + } + + public void setLNAGain( LNAGain gain ) + { + mLNAGain = gain; + } + + @XmlAttribute( name = "lna_gain" ) + public LNAEnhance getLNAEnhance() + { + return mLNAEnhance; + } + + public void setLNAEnhance( LNAEnhance enhance ) + { + mLNAEnhance = enhance; + } + + @XmlAttribute( name = "mixer_gain" ) + public MixerGain getMixerGain() + { + return mMixerGain; + } + + public void setMixerGain( MixerGain gain ) + { + mMixerGain = gain; + } + + @XmlAttribute( name = "frequency_correction" ) + public double getFrequencyCorrection() + { + return mFrequencyCorrection; + } + + public void setFrequencyCorrection( double value ) + { + mFrequencyCorrection = value; + } + + @XmlAttribute( name = "inphase_dc_correction" ) + public double getInphaseDCCorrection() + { + return mInphaseDCCorrection; + } + + public void setInphaseDCCorrection( double value ) + { + mInphaseDCCorrection = value; + } + + @XmlAttribute( name = "quadrature_dc_correction" ) + public double getQuadratureDCCorrection() + { + return mQuadratureDCCorrection; + } + + public void setQuadratureDCCorrection( double value ) + { + mQuadratureDCCorrection = value; + } + + @XmlAttribute( name = "phase_correction" ) + public double getPhaseCorrection() + { + return mPhaseCorrection; + } + + public void setPhaseCorrection( double value ) + { + mPhaseCorrection = value; + } + + @XmlAttribute( name = "gain_correction" ) + public double getGainCorrection() + { + return mGainCorrection; + } + + public void setGainCorrection( double value ) + { + mGainCorrection = value; + } +} diff --git a/src/source/tuner/fcd/proV1/FCD1TunerConfigurationPanel.java b/src/source/tuner/fcd/proV1/FCD1TunerConfigurationPanel.java new file mode 100644 index 00000000..167f3f66 --- /dev/null +++ b/src/source/tuner/fcd/proV1/FCD1TunerConfigurationPanel.java @@ -0,0 +1,667 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package source.tuner.fcd.proV1; + +import java.awt.Color; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.awt.event.FocusEvent; +import java.awt.event.FocusListener; +import java.text.DecimalFormat; +import java.util.ArrayList; + +import javax.swing.BorderFactory; +import javax.swing.ComboBoxModel; +import javax.swing.DefaultComboBoxModel; +import javax.swing.JButton; +import javax.swing.JComboBox; +import javax.swing.JLabel; +import javax.swing.JOptionPane; +import javax.swing.JPanel; +import javax.swing.JSpinner; +import javax.swing.JTextField; +import javax.swing.SpinnerModel; +import javax.swing.SpinnerNumberModel; +import javax.swing.SwingConstants; +import javax.swing.event.ChangeEvent; +import javax.swing.event.ChangeListener; +import javax.usb.UsbClaimException; +import javax.usb.UsbException; + +import com.jidesoft.swing.JideTabbedPane; + +import log.Log; +import net.miginfocom.swing.MigLayout; +import source.SourceException; +import source.tuner.TunerConfiguration; +import source.tuner.TunerType; +import source.tuner.fcd.proV1.FCD1TunerController.LNAEnhance; +import source.tuner.fcd.proV1.FCD1TunerController.LNAGain; +import source.tuner.fcd.proV1.FCD1TunerController.MixerGain; +import controller.ResourceManager; + +public class FCD1TunerConfigurationPanel extends JPanel +{ + private static final long serialVersionUID = 1L; + private ResourceManager mResourceManager; + private FCD1TunerController mController; + + private FCD1TunerConfiguration mSelectedConfig; + private JButton mNewConfiguration; + private JButton mDeleteConfiguration; + private JComboBox mComboConfigurations; + private JComboBox mComboLNAGain; + private JComboBox mComboLNAEnhance; + private JComboBox mComboMixerGain; + private JTextField mName; + private JSpinner mCorrectionFrequency; + private CorrectionSpinner mCorrectionDCI; + private CorrectionSpinner mCorrectionDCQ; + private CorrectionSpinner mCorrectionGain; + private CorrectionSpinner mCorrectionPhase; + + public FCD1TunerConfigurationPanel( ResourceManager resourceManager, + FCD1TunerController controller ) + { + mResourceManager = resourceManager; + mController = controller; + mSelectedConfig = controller.getTunerConfiguration(); + + init(); + } + + private void init() + { + setLayout( new MigLayout( "fill,wrap 2", "[right][left]", "[][][][][grow]" ) ); + + /** + * Tuner configurations combo + */ + mComboConfigurations = new JComboBox(); + mComboConfigurations.setToolTipText( "Select a tuner configuration. " + + "Create a separate tuner configuration for each of your " + + "funcube dongles by using the NEW button and typing a " + + "descriptive name" ); + + mComboConfigurations.setModel( getModel() ); + + mComboConfigurations.setSelectedItem( mSelectedConfig ); + + mComboConfigurations.addActionListener( new ActionListener() + { + @Override + public void actionPerformed( ActionEvent e ) + { + FCD1TunerConfiguration selected = + (FCD1TunerConfiguration)mComboConfigurations + .getItemAt( mComboConfigurations.getSelectedIndex() ); + + if( selected != null ) + { + update( selected ); + } + } + }); + + add( new JLabel( "Config:" ) ); + add( mComboConfigurations, "growx,push" ); + + /** + * Tuner Configuration - Name + */ + mName = new JTextField( mSelectedConfig.getName() ); + mName.setToolTipText( "Name for the selected tuner configuration" ); + + mName.addFocusListener( new FocusListener() + { + @Override + public void focusLost( FocusEvent e ) + { + mSelectedConfig.setName( mName.getText() ); + save(); + } + @Override + public void focusGained( FocusEvent e ) {} + } ); + + add( new JLabel( "Name:" ) ); + add( mName, "growx,push" ); + + JideTabbedPane tabs = new JideTabbedPane(); + tabs.setFont( this.getFont() ); + tabs.setForeground( Color.BLACK ); + add( tabs, "span,grow,push" ); + + JPanel gainPanel = new JPanel(); + gainPanel.setLayout( new MigLayout( "fill,wrap 2", "[right][left]", "[][][][grow]" ) ); + + /** + * LNA Gain + */ + mComboLNAGain = new JComboBox( LNAGain.values() ); + + mComboLNAGain.setToolTipText( "Adjust the low noise amplifier gain " + + "setting. Default value is 20db" ); + + mComboLNAGain.setSelectedItem( mSelectedConfig.getLNAGain() ); + + mComboLNAGain.addActionListener( new ActionListener() + { + @Override + public void actionPerformed( ActionEvent arg0 ) + { + LNAGain gain = (LNAGain)mComboLNAGain.getSelectedItem(); + + mSelectedConfig.setLNAGain( gain ); + + try + { + mController.setLNAGain( gain ); + } + catch ( UsbClaimException e1 ) + { + JOptionPane.showMessageDialog( + FCD1TunerConfigurationPanel.this, + "FCD Pro Tuner Controller - cannot claim FCD " + + "Controller to apply LNA gain [" + gain.toString() + "]" ); + + Log.error( "FuncubeDonglePro Controller - usb claim " + + "exception while applying LNA gain value [" + + gain.toString() + "] - " + + e1.getLocalizedMessage() ); + } + catch ( UsbException e1 ) + { + JOptionPane.showMessageDialog( + FCD1TunerConfigurationPanel.this, + "FCD Pro Tuner Controller - FCD Controller " + + "cannot apply LNA gain [" + gain.toString() + "]" ); + + Log.error( "FuncubeDonglePro Controller - " + + "exception while applying LNA gain value [" + + gain.toString() + "] - " + + e1.getLocalizedMessage() ); + } + + save(); + } + + } ); + + gainPanel.add( new JLabel( "LNA:" ) ); + gainPanel.add( mComboLNAGain, "growx,push" ); + + /** + * LNA Enhance + */ + mComboLNAEnhance = new JComboBox( LNAEnhance.values() ); + mComboLNAEnhance.setToolTipText( "Adjust the LNA enhance setting. " + + "Default value is OFF" ); + + mComboLNAEnhance.setSelectedItem( mSelectedConfig.getLNAEnhance() ); + + mComboLNAEnhance.addActionListener( new ActionListener() + { + @Override + public void actionPerformed( ActionEvent arg0 ) + { + LNAEnhance enhance = (LNAEnhance)mComboLNAEnhance.getSelectedItem(); + + mSelectedConfig.setLNAEnhance( enhance ); + + try + { + mController.setLNAEnhance( enhance ); + } + catch ( UsbClaimException e1 ) + { + JOptionPane.showMessageDialog( + FCD1TunerConfigurationPanel.this, + "FCD Pro Tuner Controller - cannot claim FCD " + + "Controller to apply LNA enhance [" + enhance.toString() + "]" ); + + Log.error( "FuncubeDonglePro Controller - usb claim " + + "exception while applying LNA enhance value [" + + enhance.toString() + "] - " + + e1.getLocalizedMessage() ); + } + catch ( UsbException e1 ) + { + JOptionPane.showMessageDialog( + FCD1TunerConfigurationPanel.this, + "FCD Pro Tuner Controller - FCD Controller " + + "cannot apply LNA enhance [" + enhance.toString() + "]" ); + + Log.error( "FuncubeDonglePro Controller - " + + "exception while applying LNA enhance value [" + + enhance.toString() + "] - " + + e1.getLocalizedMessage() ); + } + + save(); + } + + } ); + + gainPanel.add( new JLabel( "LNA Enhance:" ) ); + gainPanel.add( mComboLNAEnhance, "growx,push" ); + + /** + * Mixer Gain + */ + mComboMixerGain = new JComboBox( MixerGain.values() ); + mComboMixerGain.setToolTipText( "Adjust mixer gain setting" ); + + mComboMixerGain.setSelectedItem( mSelectedConfig.getMixerGain() ); + + mComboMixerGain.addActionListener( new ActionListener() + { + @Override + public void actionPerformed( ActionEvent arg0 ) + { + MixerGain gain = (MixerGain)mComboMixerGain.getSelectedItem(); + + mSelectedConfig.setMixerGain( gain ); + + try + { + mController.setMixerGain( gain ); + } + catch ( UsbClaimException e1 ) + { + JOptionPane.showMessageDialog( + FCD1TunerConfigurationPanel.this, + "FCD Pro Tuner Controller - cannot claim FCD " + + "Controller to apply Mixer gain [" + gain.toString() + "]" ); + + Log.error( "FuncubeDonglePro Controller - usb claim " + + "exception while applying Mixer gain value [" + + gain.toString() + "] - " + + e1.getLocalizedMessage() ); + } + catch ( UsbException e1 ) + { + JOptionPane.showMessageDialog( + FCD1TunerConfigurationPanel.this, + "FCD Pro Tuner Controller - FCD Controller " + + "cannot apply Mixer gain [" + gain.toString() + "]" ); + + Log.error( "FuncubeDonglePro Controller - " + + "exception while applying Mixer gain value [" + + gain.toString() + "] - " + + e1.getLocalizedMessage() ); + } + + save(); + } + + } ); + + gainPanel.add( new JLabel( "Mixer:" ) ); + gainPanel.add( mComboMixerGain, "growx,push" ); + + tabs.add( "Gain", gainPanel ); + + JPanel correctionPanel = new JPanel(); + correctionPanel.setLayout( new MigLayout( "fill,wrap 2", "[right][left]", "[][][][][][grow]" ) ); + + /** + * Frequency Correction + */ + SpinnerModel model = + new SpinnerNumberModel( 0.0, -1000.0, 1000.0, 0.1 ); //initial,min,max,step + + mCorrectionFrequency = new JSpinner( model ); + + mCorrectionFrequency.setToolTipText( "Frequency Correction: adjust " + + "value +/- to align the displayed frequency label with the " + + "actual signals" ); + + JSpinner.NumberEditor editor = + (JSpinner.NumberEditor)mCorrectionFrequency.getEditor(); + + DecimalFormat format = editor.getFormat(); + format.setMinimumFractionDigits( 1 ); + editor.getTextField().setHorizontalAlignment( SwingConstants.CENTER ); + + mCorrectionFrequency.setValue( mSelectedConfig.getFrequencyCorrection() ); + + mCorrectionFrequency.addChangeListener( new ChangeListener() + { + @Override + public void stateChanged( ChangeEvent e ) + { + double value = ((SpinnerNumberModel)mCorrectionFrequency + .getModel()).getNumber().doubleValue(); + + try + { + mSelectedConfig.setFrequencyCorrection( value ); + mController.setFrequencyCorrection( value ); + save(); + } + catch ( SourceException e1 ) + { + JOptionPane.showMessageDialog( + FCD1TunerConfigurationPanel.this, + "FCD Pro Tuner Controller - couldn't " + + "apply frequency correction value: " + value + + e1.getLocalizedMessage() ); + + Log.error( "FuncubeDonglePro Controller - couldn't apply " + + "frequency correction value: " + value + + e1.getLocalizedMessage() ); + } + } + } ); + + correctionPanel.add( new JLabel( "Frequency PPM:" ) ); + correctionPanel.add( mCorrectionFrequency, "growx,push" ); + + /** + * Inphase DC Correction + */ + mCorrectionDCI = new CorrectionSpinner( Correction.DC_INPHASE, + mSelectedConfig.getInphaseDCCorrection(), 0.00001, 5 ); + mCorrectionDCI.setToolTipText( "DC Bias Correction/Inphase " + + "Component: valid values are -1.0 to 1.0 (default: 0.0)" ); + correctionPanel.add( new JLabel( "DC Inphase:" ) ); + correctionPanel.add( mCorrectionDCI, "growx,push" ); + + /** + * Quadrature DC Correction + */ + mCorrectionDCQ = new CorrectionSpinner( Correction.DC_QUADRATURE, + mSelectedConfig.getQuadratureDCCorrection(), 0.00001, 5 ); + mCorrectionDCQ.setToolTipText( "DC Bias Correction/Quadrature " + + "Component: valid values are -1.0 to 1.0 (default: 0.0)" ); + correctionPanel.add( new JLabel( "DC Quadrature:" ) ); + correctionPanel.add( mCorrectionDCQ, "growx,push" ); + + /** + * Gain Correction + */ + mCorrectionGain = new CorrectionSpinner( Correction.GAIN, + mSelectedConfig.getGainCorrection(), 0.00001, 5 ); + mCorrectionGain.setToolTipText( "Gain Correction: valid values are " + + "-1.0 to 1.0 (default: 0.0)" ); + correctionPanel.add( new JLabel( "Gain:" ) ); + correctionPanel.add( mCorrectionGain, "growx,push" ); + + /** + * Phase Correction + */ + mCorrectionPhase = new CorrectionSpinner( Correction.PHASE, + mSelectedConfig.getPhaseCorrection(), 0.00001, 5 ); + mCorrectionPhase.setToolTipText( "Phase Correction: valid values are " + + "-1.0 to 1.0 (default: 0.0)" ); + correctionPanel.add( new JLabel( "Phase:" ) ); + correctionPanel.add( mCorrectionPhase, "growx,push" ); + + tabs.add( "Correction", correctionPanel ); + + /** + * Create a new configuration + */ + mNewConfiguration = new JButton( "New" ); + mNewConfiguration.setToolTipText( "Create a new tuner configuration " + + "for each of your dongles to keep track of the specific " + + "settings for each dongle" ); + + mNewConfiguration.addActionListener( new ActionListener() + { + @Override + public void actionPerformed( ActionEvent e ) + { + TunerConfiguration config = + mResourceManager.getSettingsManager() + .addNewTunerConfiguration( + TunerType.FUNCUBE_DONGLE_PRO, + "New Configuration" ); + + mComboConfigurations.setModel( getModel() ); + + mComboConfigurations.setSelectedItem( config ); + + repaint(); + } + } ); + + add( mNewConfiguration, "growx,push" ); + + /** + * Delete the currently selected configuration + */ + mDeleteConfiguration = new JButton( "Delete" ); + mDeleteConfiguration.setToolTipText( "Deletes the currently selected " + + "tuner configuration. Note: the DEFAULT tuner " + + "configuration will be recreated if you delete it, " + + "after you restart the application" ); + + mDeleteConfiguration.addActionListener( new ActionListener() + { + @Override + public void actionPerformed( ActionEvent e ) + { + FCD1TunerConfiguration selected = + (FCD1TunerConfiguration)mComboConfigurations + .getItemAt( mComboConfigurations.getSelectedIndex() ); + + if( selected != null ) + { + int n = JOptionPane.showConfirmDialog( + FCD1TunerConfigurationPanel.this, + "Are you sure you want to delete '" + + selected.getName() + "'?", + "Are you sure?", + JOptionPane.YES_NO_OPTION ); + + if( n == JOptionPane.YES_OPTION ) + { + mResourceManager.getSettingsManager() + .deleteTunerConfiguration( selected ); + + mComboConfigurations.setModel( getModel() ); + + mComboConfigurations.setSelectedIndex( 0 ); + + repaint(); + } + } + } + } ); + + add( mDeleteConfiguration, "growx,push" ); + } + + private void update( FCD1TunerConfiguration config ) + { + mSelectedConfig = config; + + try + { + mController.apply( config ); + + mName.setText( config.getName() ); + + mComboLNAGain.setSelectedItem( mSelectedConfig.getLNAGain() ); + mComboLNAEnhance.setSelectedItem( mSelectedConfig.getLNAEnhance() ); + mComboMixerGain.setSelectedItem( mSelectedConfig.getMixerGain() ); + + mCorrectionFrequency.setValue( config.getFrequencyCorrection() ); + mCorrectionDCI.setValue( config.getInphaseDCCorrection() ); + mCorrectionDCQ.setValue( config.getQuadratureDCCorrection() ); + mCorrectionGain.setValue( config.getGainCorrection() ); + mCorrectionPhase.setValue( config.getPhaseCorrection() ); + + mResourceManager.getSettingsManager() + .setSelectedTunerConfiguration( + TunerType.FUNCUBE_DONGLE_PRO, + mController.getAddress(), config ); + } + catch ( SourceException e1 ) + { + JOptionPane.showMessageDialog( + FCD1TunerConfigurationPanel.this, + "FCD Pro Tuner Controller - couldn't " + + "apply the tuner configuration settings - " + + e1.getLocalizedMessage() ); + + Log.error( "FuncubeDonglePro Controller - couldn't apply " + + "config [" + config.getName() + "] - " + + e1.getLocalizedMessage() ); + } + } + + private ComboBoxModel getModel() + { + ArrayList configs = + mResourceManager.getSettingsManager() + .getTunerConfigurations( TunerType.FUNCUBE_DONGLE_PRO ); + + DefaultComboBoxModel model = + new DefaultComboBoxModel(); + + for( TunerConfiguration config: configs ) + { + model.addElement( (FCD1TunerConfiguration)config ); + } + + return model; + } + + @SuppressWarnings( "unused" ) + private FCD1TunerConfiguration getNamedConfiguration( String name ) + { + ArrayList configs = + mResourceManager.getSettingsManager() + .getTunerConfigurations( TunerType.FUNCUBE_DONGLE_PRO ); + + for( TunerConfiguration config: configs ) + { + if( config.getName().contentEquals( name ) ) + { + return (FCD1TunerConfiguration)config; + } + } + + return null; + } + + private void save() + { + mResourceManager.getSettingsManager().save(); + } + + public enum Correction { GAIN, PHASE, DC_INPHASE, DC_QUADRATURE }; + + public class CorrectionSpinner extends JSpinner + { + private static final long serialVersionUID = 1L; + private static final double sMIN_VALUE = -1.0d; + private static final double sMAX_VALUE = 1.0d; + + private Correction mCorrectionComponent; + + public CorrectionSpinner( Correction component, + double initialValue, + double step, + int decimalPlaces ) + { + mCorrectionComponent = component; + + SpinnerModel model = new SpinnerNumberModel( initialValue, + sMIN_VALUE, sMAX_VALUE, step ); + + setModel( model ); + + JSpinner.NumberEditor editor = (JSpinner.NumberEditor)getEditor(); + + DecimalFormat format = editor.getFormat(); + format.setMinimumFractionDigits( decimalPlaces ); + + editor.getTextField().setHorizontalAlignment( SwingConstants.CENTER ); + + addChangeListener( new ChangeListener() + { + @Override + public void stateChanged( ChangeEvent e ) + { + double value = + ((SpinnerNumberModel)getModel()).getNumber().doubleValue(); + + try + { + switch( mCorrectionComponent ) + { + case DC_INPHASE: + mSelectedConfig.setInphaseDCCorrection( value ); + mController.setDCCorrectionInPhase( value ); + break; + case DC_QUADRATURE: + mSelectedConfig.setQuadratureDCCorrection( value ); + mController.setDCCorrectionQuadrature( value ); + break; + case GAIN: + mSelectedConfig.setGainCorrection( value ); + mController.setGainCorrection( value ); + break; + case PHASE: + mSelectedConfig.setPhaseCorrection( value ); + mController.setPhaseCorrection( value ); + break; + } + + //Save the change(s) to the selected tuner config + save(); + } + catch ( UsbClaimException e1 ) + { + JOptionPane.showMessageDialog( + FCD1TunerConfigurationPanel.this, + "FCD Pro Tuner Controller - cannot claim FCD " + + "Controller to apply " + + mCorrectionComponent.toString() + + " correction value [" + value + "]" ); + + Log.error( "FuncubeDonglePro Controller - usb claim " + + "exception while applying " + + mCorrectionComponent.toString() + + " correction value [" + value + "] - " + + e1.getLocalizedMessage() ); + } + catch ( UsbException e1 ) + { + JOptionPane.showMessageDialog( + FCD1TunerConfigurationPanel.this, + "FCD Pro Tuner Controller - USB error from FCD " + + "Controller while applying " + + mCorrectionComponent.toString() + + " correction value [" + value + "]" ); + + Log.error( "FuncubeDonglePro Controller - usb " + + "exception while applying " + + mCorrectionComponent.toString() + + " correction value [" + value + "] - " + + e1.getLocalizedMessage() ); + } + } + } ); + } + } +} diff --git a/src/source/tuner/fcd/proV1/FCD1TunerController.java b/src/source/tuner/fcd/proV1/FCD1TunerController.java new file mode 100644 index 00000000..40f99766 --- /dev/null +++ b/src/source/tuner/fcd/proV1/FCD1TunerController.java @@ -0,0 +1,698 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package source.tuner.fcd.proV1; + +import javax.swing.JPanel; +import javax.usb.UsbClaimException; +import javax.usb.UsbException; + +import log.Log; +import source.SourceException; +import source.tuner.TunerClass; +import source.tuner.TunerConfiguration; +import source.tuner.TunerType; +import source.tuner.fcd.FCDCommand; +import source.tuner.fcd.FCDTuner; +import source.tuner.fcd.FCDTunerController; +import source.tuner.usb.USBTunerDevice; +import controller.ResourceManager; + +public class FCD1TunerController extends FCDTunerController +{ + public static final int sMINIMUM_TUNABLE_FREQUENCY = 64000000; + public static final int sMAXIMUM_TUNABLE_FREQUENCY = 1700000000; + public static final int sSAMPLE_RATE = 96000; + + private double mDCCorrectionInPhase = 0.0; + private double mDCCorrectionQuadrature = 0.0; + private double mPhaseCorrection = 0.0; + private double mGainCorrection = 0.0; + private LNAGain mLNAGain; + private LNAEnhance mLNAEnhance; + private MixerGain mMixerGain; + private FCD1TunerConfiguration mTunerConfiguration; + private FCD1TunerEditorPanel mEditor; + + public FCD1TunerController( USBTunerDevice device ) + throws SourceException + { + super( device, + sMINIMUM_TUNABLE_FREQUENCY, + sMAXIMUM_TUNABLE_FREQUENCY ); + + mFrequencyController.setBandwidth( sSAMPLE_RATE ); + + try + { + open(); + + setFCDMode( Mode.APPLICATION ); + getPhaseAndGainCorrection(); + getDCIQCorrection(); + getLNAGainSetting(); + getLNAEnhanceSetting(); + getMixerGainSetting(); + + send( FCDCommand.APP_SET_MIXER_GAIN, 1 ); + } + catch( Exception e ) + { + e.printStackTrace(); + + throw new SourceException( "FCDTunerController error " + + "during construction", e ); + } + } + + public int getCurrentSampleRate() + { + return sSAMPLE_RATE; + } + + @Override + public TunerClass getTunerClass() + { + return TunerClass.FUNCUBE_DONGLE_PRO; + } + + @Override + public TunerType getTunerType() + { + return TunerType.FUNCUBE_DONGLE_PRO; + } + + /** + * Get the tuner configuration + * + * @return current config or null if one hasn't been applied yet + */ + public FCD1TunerConfiguration getTunerConfiguration() + { + return mTunerConfiguration; + } + + @Override + public void apply( TunerConfiguration tunerConfig ) + throws SourceException + { + FCD1TunerConfiguration config = (FCD1TunerConfiguration)tunerConfig; + + try + { + setFrequencyCorrection( config.getFrequencyCorrection() ); + setLNAGain( config.getLNAGain() ); + setLNAEnhance( config.getLNAEnhance() ); + setMixerGain( config.getMixerGain() ); + setDCCorrectionInPhase( config.getInphaseDCCorrection() ); + setDCCorrectionQuadrature( config.getQuadratureDCCorrection() ); + setGainCorrection( config.getGainCorrection() ); + setPhaseCorrection( config.getPhaseCorrection() ); + + //If we get to here without exception, store this config + mTunerConfiguration = config; + } + catch( Exception e) + { + //Re-cast any usb exceptions as source exceptions + throw new SourceException( "FCD 1 Controller encountered an " + + "exception while applying tuner config - " + + e.getLocalizedMessage() ); + } + } + + @Override + public JPanel getEditor( FCDTuner tuner, ResourceManager resourceManager ) + { + if( mEditor == null ) + { + mEditor = new FCD1TunerEditorPanel( tuner, resourceManager ); + } + + return mEditor; + } + + /** + * Gets the current LNA Gain value from the controller and stores it + */ + private void getLNAGainSetting() throws UsbClaimException, UsbException + { + int gain = (int)send( FCDCommand.APP_GET_LNA_GAIN ); + + Log.info( "FCD1: lna gain setting is:" + gain ); + + mLNAGain = LNAGain.valueOf( gain ); + } + + /** + * @return - current lna gain setting + */ + public LNAGain getLNAGain() + { + return mLNAGain; + } + + /** + * Sets lna gain for the controller + * @param gain + * @throws UsbException + * @throws UsbClaimException + */ + public void setLNAGain( LNAGain gain ) + throws UsbException, UsbClaimException + { + send( FCDCommand.APP_SET_LNA_GAIN, gain.getSetting() ); + } + + /** + * @return - current phase correction setting + */ + public double getPhaseCorrection() + { + return mPhaseCorrection; + } + + /** + * Sets the phase correction + * @param value - new phase correction + * @throws IllegalArgumentException for value outside limits (0.0 <> 1.0) + * @throws UsbClaimException - if cannot claim the FCD HID controller + * @throws UsbException - if there was a usb error + */ + public void setPhaseCorrection( double value ) + throws UsbClaimException, UsbException + { + if( value < -1.0 || value > 1.0 ) + { + throw new IllegalArgumentException( "Phase correction value [" + + value + "] outside limits ( -1.0 <> 1.0 )" ); + } + + mPhaseCorrection = value; + setPhaseAndGainCorrection(); + } + + /** + * @return current gain correction setting + */ + public double getGainCorrection() + { + return mGainCorrection; + } + + /** + * + * @param value - gain correction value + * @throws IllegalArgumentException for value outside limits (0.0 <> 1.0) + * @throws UsbClaimException - if cannot claim the FCD HID controller + * @throws UsbException - if there was a usb error + */ + public void setGainCorrection( double value ) + throws UsbClaimException, UsbException + { + if( value < -1.0 || value > 1.0 ) + { + throw new IllegalArgumentException( "Gain correction value [" + + value + "] outside limits ( -1.0 <> 1.0 )" ); + } + + mGainCorrection = value; + setPhaseAndGainCorrection(); + } + + /** + * Sends the phase and gain correction values to FCD. + * + * The interface expects an argument with the SIGNED phase value in the first + * 16 bits and the UNSIGNED gain value in the second 16 bits, both in + * big-endian format. Since we're sending the argument in java little endian + * format, we place the phase in the high-order bits and the gain in the + * low-order bits, so that the send(command) function will reorder the bytes + * into big endian format, and place the arguments correctly. + * + * Gain correction values range -1.0 to 0.0 to 1.0 and applied to the dongle + * as values 0 to 65534 + * + * Phase correction values range -1.0 to 0.0 to 1.0 and applied to the + * dongle as signed values -32768 to 32767 + */ + private void setPhaseAndGainCorrection() throws UsbClaimException, + UsbException + { + //UNSIGNED short gain value - masked into a long to avoid sign-extension + long gain = (long)( ( 1.0 + mGainCorrection ) * Short.MAX_VALUE ); + + //Left shift gain to place value in upper 16 bits + long longGain = Long.rotateLeft( ( 0xFFFF & gain ), 16 ); + + //SIGNED short phase value + short phase = (short)( mPhaseCorrection * Short.MAX_VALUE ); + + //Merge the results + long correction = longGain | phase; + + send( FCDCommand.APP_SET_IQ_CORRECTION, correction ); + } + + /** + * Retrieves the stored phase and gain correction values from the FCD. + * + * Note: testing shows that when the dongle is unplugged, any stored phase + * and gain correction values are reset. Reading the dongle after a fresh + * plugin shows the values reset. + * + * @throws UsbClaimException + * @throws UsbException + */ + private void getPhaseAndGainCorrection() throws UsbClaimException, + UsbException + { + long correction = send( FCDCommand.APP_GET_IQ_CORRECTION ); + + /** + * Gain: mask the upper 16 phase bits and right shift to get the + * unsigned short value and then divide by 32768 to get the double + * value. We place the unsigned short value in an int to avoid sign + * issues. + */ + int gain = (int)( Long.rotateRight( correction & 0xFFFF0000, 16 ) ); + + mGainCorrection = ( (double)gain / (double)Short.MAX_VALUE ) - 1.0; + + /** + * Phase: mask the lower 16 bits and divide by 32768 to get the double + * value + */ + short phase = (short)( correction & 0x0000FFFF ); + + mPhaseCorrection = ( (double)phase / (double)Short.MAX_VALUE ); + + Log.info( "FCD1: correction:" + correction + " gain:" + gain + " phase:" + phase ); + } + + /** + * @return current DC correction for the I (inphase) component + */ + public double getDCCorrectionInPhase() + { + return mDCCorrectionInPhase; + } + + /** + * Sets DC correction for the I (inphase) component and sends the new value + * to the FCD controller + * @param value - new DC correction value for the inphase component + * @throws IllegalArgumentException for values outside limits (-1.0 <> 1.0) + * @throws UsbClaimException - if cannot claim the FCD HID controller + * @throws UsbException - if there was a usb error + */ + public void setDCCorrectionInPhase( double value ) + throws UsbClaimException, UsbException + { + if( value < -1.0 || value > 1.0 ) + { + throw new IllegalArgumentException( "DC inphase correction value " + + "[" + value + "] outside limits ( 0.0 <> 1.0 )" ); + } + + mDCCorrectionInPhase = value; + setDCIQCorrection(); + } + + /** + * @return current DC correction for the Q (quadrature) component + */ + public double getDCCorrectionQuadrature() + { + return mDCCorrectionQuadrature; + } + + /** + * Sets the DC correction for the Q (quadrature) component and send the + * new value to the FCD controller + * @param value - new DC correction value for the quadrature component + * @throws IllegalArgumentException for values outside limits (-1.0 <> 1.0) + * @throws UsbClaimException - if cannot claim the FCD HID controller + * @throws UsbException - if there was a usb error + */ + public void setDCCorrectionQuadrature( double value ) + throws UsbClaimException, UsbException + { + if( value < -1.0 || value > 1.0 ) + { + throw new IllegalArgumentException( "DC quadrature correction " + + "value [" + value + "] outside limits ( 0.0 <> 1.0 )" ); + } + + mDCCorrectionQuadrature = value; + setDCIQCorrection(); + } + + /** + * Sends the I/Q DC offset correction values to FCD. + * + * The interface expects an argument with the signed inphase value in the + * first 16 bits and the signed quadrature value in the second 16 bits, + * both in big-endian format. + * + * Since we're sending the argument in java little endian format, we place + * the quadrature in the high-order bits and the inphase in the low-order + * bits, so that the send(command) function will reorder the bytes into big + * endian format, and place the arguments correctly. + * + * Both Inphase and Quadrature correction values range -1.0 to 0.0 to 1.0 + * and are applied to the dongle as signed short values -32768 to 0 to 32767 + */ + private void setDCIQCorrection() throws UsbClaimException, UsbException + { + //I & Q DC offset correction values are signed short values + short inphase = (short)( mDCCorrectionInPhase * Short.MAX_VALUE ); + short quadrature = (short)( mDCCorrectionQuadrature * Short.MAX_VALUE ); + + //Mask the shorts into longs to preserve the sign bit and prepare + //for merging to a single 32-bit value + long maskedInphase = inphase & 0x0000FFFF; + long maskedQuadrature = quadrature & 0x0000FFFF; + + //Left shift quadrature to place value in upper 16 bits + long shiftedQuadrature = Long.rotateLeft( maskedQuadrature, 16 ); + + //Merge the results + long correction = shiftedQuadrature | maskedInphase; + +// Log.info( "FCD1: setting iq correction," + +// " inphase:" + inphase + +// " masked inphase:" + maskedInphase + +// " quad:" + quadrature + +// " masked quad:" + maskedQuadrature + +// " shifted quadrature:" + shiftedQuadrature + +// " correction:" + correction ); + + send( FCDCommand.APP_SET_DC_CORRECTION, correction ); + } + + private void getDCIQCorrection() throws UsbClaimException, + UsbException + { + long correction = send( FCDCommand.APP_GET_DC_CORRECTION ); + + /** + * Quadrature: mask the upper 16 bits and divide by 32768 to get the + * stored quadrature value. Cast the 16-bit value to a short to get + * the signed short value + */ + long shiftedQuadrature = correction & 0xFFFF0000; + long quadrature = Long.rotateRight( shiftedQuadrature, 16 ); + + mDCCorrectionQuadrature = ( (short)quadrature / (double)Short.MAX_VALUE ); + + /** + * InPhase: mask the lower 16 bits to get the short + * value and then divide by 32768 to get the stored inphase value. + * Cast the 16-bit value to a short to get the signed short value + */ + long inphase = correction & 0x0000FFFF; + mDCCorrectionInPhase = ( (short)inphase / (double)Short.MAX_VALUE ); + +// Log.info( "FCD1:" + +// " correction:" + correction + +// " Q:" + quadrature + +// " I:" + inphase + +// " QCorr:" + mDCCorrectionQuadrature + +// " ICorr:" + mDCCorrectionInPhase ); + } + + private void getLNAEnhanceSetting() throws UsbClaimException, UsbException + { + int enhance = (int)send( FCDCommand.APP_GET_LNA_ENHANCE ); + + Log.info( "FCD1: lna enhance setting is: " + enhance ); + + mLNAEnhance = LNAEnhance.valueOf( enhance ); + } + + public LNAEnhance getLNAEnhance() + { + return mLNAEnhance; + } + + public void setLNAEnhance( LNAEnhance enhance ) + throws UsbClaimException, UsbException + { + send( FCDCommand.APP_SET_LNA_ENHANCE, enhance.getSetting() ); + } + + private void getMixerGainSetting() throws UsbClaimException, UsbException + { + int gain = (int)send( FCDCommand.APP_GET_MIXER_GAIN ); + + Log.info( "FCD1: mixer gain setting is:" + gain ); + + mMixerGain = MixerGain.valueOf( gain ); + } + + public MixerGain getMixerGain() + { + return mMixerGain; + } + + public void setMixerGain( MixerGain gain ) throws UsbClaimException, UsbException + { + send( FCDCommand.APP_SET_MIXER_GAIN, gain.getSetting() ); + } + + public enum Block + { + CELLULAR_BAND_BLOCKED( "Blocked" ), + NO_BAND_BLOCK( "Unblocked" ), + UNKNOWN( "Unknown" ); + + private String mLabel; + + private Block( String label ) + { + mLabel = label; + } + + public String getLabel() + { + return mLabel; + } + + public static Block getBlock( String block ) + { + Block retVal = UNKNOWN; + + if( block.equalsIgnoreCase( "No blk" ) ) + { + retVal = NO_BAND_BLOCK; + } + else if( block.equalsIgnoreCase( "Cell blk" ) ) + { + retVal = CELLULAR_BAND_BLOCKED; + } + + return retVal; + } + } + + /** + * LNA Gain values suppported by the FCD Pro 1.0 + */ + public enum LNAGain + { + LNA_GAIN_MINUS_5_0( 0, "-5.0 dB" ), + LNA_GAIN_MINUS_2_5( 1, "-2.5 dB" ), + LNA_GAIN_PLUS_0_0( 4, "0.0 dB" ), + LNA_GAIN_PLUS_2_5( 5, "2.5 dB" ), + LNA_GAIN_PLUS_5_0( 6, "5.0 dB" ), + LNA_GAIN_PLUS_7_5( 7, "7.5 dB" ), + LNA_GAIN_PLUS_10_0( 8, "10.0 dB" ), + LNA_GAIN_PLUS_12_5( 9, "12.5 dB" ), + LNA_GAIN_PLUS_15_0( 10, "15.0 dB" ), + LNA_GAIN_PLUS_17_5( 11, "17.5 dB" ), + LNA_GAIN_PLUS_20_0( 12, "20.0 dB" ), + LNA_GAIN_PLUS_25_0( 13, "25.0 dB" ), + LNA_GAIN_PLUS_30_0( 14, "30.0 dB" ); + + private int mSetting; + private String mLabel; + + private LNAGain( int setting, String label ) + { + mSetting = setting; + mLabel = label; + } + + public static LNAGain valueOf( int setting ) + { + switch( setting ) + { + case 0: + return LNA_GAIN_MINUS_5_0; + case 1: + return LNA_GAIN_MINUS_2_5; + case 4: + return LNA_GAIN_PLUS_0_0; + case 5: + return LNA_GAIN_PLUS_2_5; + case 6: + return LNA_GAIN_PLUS_5_0; + case 7: + return LNA_GAIN_PLUS_7_5; + case 8: + return LNA_GAIN_PLUS_10_0; + case 9: + return LNA_GAIN_PLUS_12_5; + case 10: + return LNA_GAIN_PLUS_15_0; + case 11: + return LNA_GAIN_PLUS_17_5; + case 12: + return LNA_GAIN_PLUS_20_0; + case 13: + return LNA_GAIN_PLUS_25_0; + case 14: + return LNA_GAIN_PLUS_30_0; + default: + throw new IllegalArgumentException( "FCD 1.0 Tuner " + + "Controller - unrecognized LNA gain setting " + + "value [" + setting + "]" ); + } + } + + public int getSetting() + { + return mSetting; + } + + public String getLabel() + { + return mLabel; + } + + public String toString() + { + return getLabel(); + } + } + + /** + * LNA Enhance values suppported by the FCD Pro 1.0 + */ + public enum LNAEnhance + { + LNA_ENHANCE_OFF( 0, "Off" ), + LNA_ENHANCE_0( 1, "0" ), + LNA_ENHANCE_1( 3, "1" ), + LNA_ENHANCE_2( 5, "2" ), + LNA_ENHANCE_3( 7, "3" ); + + private int mSetting; + private String mLabel; + + private LNAEnhance( int setting, String label ) + { + mSetting = setting; + mLabel = label; + } + + public static LNAEnhance valueOf( int setting ) + { + switch( setting ) + { + case 0: + return LNA_ENHANCE_OFF; + case 1: + return LNA_ENHANCE_0; + case 3: + return LNA_ENHANCE_1; + case 5: + return LNA_ENHANCE_2; + case 7: + return LNA_ENHANCE_3; + default: + throw new IllegalArgumentException( "FCD 1.0 Tuner " + + "Controller - unrecognized LNA enhance setting " + + "value [" + setting + "]" ); + } + } + + public int getSetting() + { + return mSetting; + } + + public String getLabel() + { + return mLabel; + } + + public String toString() + { + return getLabel(); + } + } + + /** + * Mixer Gain values suppported by the FCD Pro 1.0 + */ + public enum MixerGain + { + MIXER_GAIN_PLUS_4_0( 0, "4.0 dB" ), + MIXER_GAIN_PLUS_12_0( 1, "12.0 dB" ); + + private int mSetting; + private String mLabel; + + private MixerGain( int setting, String label ) + { + mSetting = setting; + mLabel = label; + } + + public static MixerGain valueOf( int setting ) + { + switch( setting ) + { + case 0: + return MIXER_GAIN_PLUS_4_0; + case 1: + return MIXER_GAIN_PLUS_12_0; + default: + throw new IllegalArgumentException( "FCD 1.0 Tuner " + + "Controller - unrecognized mixer gain setting " + + "value [" + setting + "]" ); + } + } + + public int getSetting() + { + return mSetting; + } + + public String getLabel() + { + return mLabel; + } + + public String toString() + { + return getLabel(); + } + } +} diff --git a/src/source/tuner/fcd/proV1/FCD1TunerEditorPanel.java b/src/source/tuner/fcd/proV1/FCD1TunerEditorPanel.java new file mode 100644 index 00000000..488286cb --- /dev/null +++ b/src/source/tuner/fcd/proV1/FCD1TunerEditorPanel.java @@ -0,0 +1,102 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package source.tuner.fcd.proV1; + +import java.awt.Color; + +import gui.control.JFrequencyControl; + +import javax.swing.BorderFactory; +import javax.swing.JLabel; +import javax.swing.JPanel; + +import com.jidesoft.swing.JideTabbedPane; + +import log.Log; +import net.miginfocom.swing.MigLayout; +import source.SourceException; +import source.tuner.FrequencyChangeListener; +import source.tuner.fcd.FCDTuner; +import source.tuner.fcd.FCDTunerDetailsPanel; +import controller.ResourceManager; + +public class FCD1TunerEditorPanel extends JPanel implements FrequencyChangeListener +{ + private static final long serialVersionUID = 1L; + + private FCDTuner mTuner; + private FCD1TunerController mController; + private ResourceManager mResourceManager; + + private JFrequencyControl mFrequencyControl; + + public FCD1TunerEditorPanel( FCDTuner tuner, ResourceManager resourceManager ) + { + mTuner = tuner; + mController = (FCD1TunerController)mTuner.getController(); + mResourceManager = resourceManager; + + initGUI(); + } + + private void initGUI() + { + setLayout( new MigLayout( "fill,wrap 2", "[grow,right][grow]", "[][][grow]" ) ); + + add( new JLabel( "Funcube Dongle Pro Tuner" ), "span, align center" ); + + mFrequencyControl = new JFrequencyControl(); + + mFrequencyControl.addListener( this ); + + /* Add frequency control as listener to tuner to get frequency changes + * that are invoked elsewhere, to keep the control in sync with the + * frequency */ + mTuner.addListener( mFrequencyControl ); + + mFrequencyControl.setFrequency( mController.getFrequency(), false ); + + add( mFrequencyControl, "span,align center" ); + + JideTabbedPane tabs = new JideTabbedPane(); + tabs.setFont( this.getFont() ); + tabs.setForeground( Color.BLACK ); + + tabs.add( "Configuration", new FCD1TunerConfigurationPanel( + mResourceManager, mController ) ); + + tabs.add( "Info", new FCDTunerDetailsPanel( mController ) ); + + add( tabs, "span,push,grow" ); + } + + @Override + public void frequencyChanged( int frequency, int bandwidth ) + { + //Disregard the bandwidth value, which shouldn't be changing + try + { + mController.setFrequency( frequency ); + } + catch ( SourceException e ) + { + Log.error( "FCTProController - error setting frequency [" + + frequency + "] - " + e.getLocalizedMessage() ); + } + } +} diff --git a/src/source/tuner/fcd/proplusV2/FCD2TunerConfiguration.java b/src/source/tuner/fcd/proplusV2/FCD2TunerConfiguration.java new file mode 100644 index 00000000..06a4de4d --- /dev/null +++ b/src/source/tuner/fcd/proplusV2/FCD2TunerConfiguration.java @@ -0,0 +1,82 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package source.tuner.fcd.proplusV2; + +import javax.xml.bind.annotation.XmlAttribute; + +import source.tuner.TunerConfiguration; +import source.tuner.TunerType; + +public class FCD2TunerConfiguration extends TunerConfiguration +{ + private boolean mGainLNA = true; + private boolean mGainMixer = true; + private double mFrequencyCorrection = -2.2d; + + /** + * Default constructor for JAXB + */ + public FCD2TunerConfiguration() + { + this( "Default" ); + } + + public FCD2TunerConfiguration( String name ) + { + super( name ); + } + + @Override + public TunerType getTunerType() + { + return TunerType.FUNCUBE_DONGLE_PRO_PLUS; + } + + @XmlAttribute( name = "lna_gain" ) + public boolean getGainLNA() + { + return mGainLNA; + } + + public void setGainLNA( boolean gain ) + { + mGainLNA = gain; + } + + @XmlAttribute( name = "mixer_gain" ) + public boolean getGainMixer() + { + return mGainMixer; + } + + public void setGainMixer( boolean gain ) + { + mGainMixer = gain; + } + + @XmlAttribute( name = "frequency_correction" ) + public double getFrequencyCorrection() + { + return mFrequencyCorrection; + } + + public void setFrequencyCorrection( double value ) + { + mFrequencyCorrection = value; + } +} diff --git a/src/source/tuner/fcd/proplusV2/FCD2TunerConfigurationPanel.java b/src/source/tuner/fcd/proplusV2/FCD2TunerConfigurationPanel.java new file mode 100644 index 00000000..f6e3e9eb --- /dev/null +++ b/src/source/tuner/fcd/proplusV2/FCD2TunerConfigurationPanel.java @@ -0,0 +1,377 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package source.tuner.fcd.proplusV2; + +import java.awt.ComponentOrientation; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.awt.event.FocusEvent; +import java.awt.event.FocusListener; +import java.text.DecimalFormat; +import java.util.ArrayList; + +import javax.swing.BorderFactory; +import javax.swing.ComboBoxModel; +import javax.swing.DefaultComboBoxModel; +import javax.swing.JButton; +import javax.swing.JCheckBox; +import javax.swing.JComboBox; +import javax.swing.JLabel; +import javax.swing.JOptionPane; +import javax.swing.JPanel; +import javax.swing.JSpinner; +import javax.swing.JTextField; +import javax.swing.SpinnerModel; +import javax.swing.SpinnerNumberModel; +import javax.swing.SwingConstants; +import javax.swing.event.ChangeEvent; +import javax.swing.event.ChangeListener; + +import log.Log; +import net.miginfocom.swing.MigLayout; +import source.SourceException; +import source.tuner.TunerConfiguration; +import source.tuner.TunerConfigurationAssignment; +import source.tuner.TunerType; +import controller.ResourceManager; + +public class FCD2TunerConfigurationPanel extends JPanel +{ + private static final long serialVersionUID = 1L; + private ResourceManager mResourceManager; + private FCD2TunerController mController; + private FCD2TunerConfiguration mSelectedConfig; + private JButton mNewConfiguration; + private JButton mDeleteConfiguration; + private JComboBox mComboConfigurations; + private JTextField mName; + private JCheckBox mLNAGain; + private JCheckBox mMixerGain; + private JSpinner mFrequencyCorrection; + + public FCD2TunerConfigurationPanel( ResourceManager resourceManager, + FCD2TunerController controller ) + { + mResourceManager = resourceManager; + mController = controller; + + init(); + } + + private void init() + { + setLayout( new MigLayout( "fill,wrap 2", "[right][left]", "[][][][][][grow]" ) ); + + mComboConfigurations = new JComboBox(); + + mComboConfigurations.setModel( getModel() ); + + mComboConfigurations.addActionListener( new ActionListener() + { + @Override + public void actionPerformed( ActionEvent e ) + { + FCD2TunerConfiguration selected = + (FCD2TunerConfiguration)mComboConfigurations + .getItemAt( mComboConfigurations.getSelectedIndex() ); + + if( selected != null ) + { + update( selected ); + } + } + }); + + add( new JLabel( "Config:" ) ); + add( mComboConfigurations, "growx,push" ); + + mName = new JTextField(); + + mName.addFocusListener( new FocusListener() + { + @Override + public void focusLost( FocusEvent e ) + { + mSelectedConfig.setName( mName.getText() ); + save(); + } + @Override + public void focusGained( FocusEvent e ) {} + } ); + + add( new JLabel( "Name" ) ); + add( mName, "growx,push" ); + + /** + * Frequency Correction + */ + + SpinnerModel model = + new SpinnerNumberModel( 0.0, //initial value + -1000.0, //min + 1000.0, //max + 0.1 ); //step + + mFrequencyCorrection = new JSpinner( model ); + + JSpinner.NumberEditor editor = + (JSpinner.NumberEditor)mFrequencyCorrection.getEditor(); + + DecimalFormat format = editor.getFormat(); + format.setMinimumFractionDigits( 1 ); + editor.getTextField().setHorizontalAlignment( SwingConstants.CENTER ); + + mFrequencyCorrection.addChangeListener( new ChangeListener() + { + @Override + public void stateChanged( ChangeEvent e ) + { + double value = ((SpinnerNumberModel)mFrequencyCorrection + .getModel()).getNumber().doubleValue(); + + try + { + mController.setFrequencyCorrection( value ); + mSelectedConfig.setFrequencyCorrection( value ); + save(); + } + catch ( SourceException e1 ) + { + JOptionPane.showMessageDialog( + FCD2TunerConfigurationPanel.this, + "FCD Pro Plus Tuner Controller - couldn't " + + "apply frequency correction value: " + value + + e1.getLocalizedMessage() ); + + Log.error( "FuncubeDongleProPlus Controller - couldn't apply " + + "frequency correction value: " + value + + e1.getLocalizedMessage() ); + } + } + } ); + + add( new JLabel( "Correction PPM:" ) ); + add( mFrequencyCorrection, "growx,push" ); + + /** + * LNA Gain + */ + mLNAGain = new JCheckBox( "LNA Gain:" ); + mLNAGain.setComponentOrientation( ComponentOrientation.RIGHT_TO_LEFT ); + + mLNAGain.addActionListener( new ActionListener() + { + @Override + public void actionPerformed( ActionEvent e ) + { + mSelectedConfig.setGainLNA( mLNAGain.isSelected() ); + + save(); + + update( mSelectedConfig ); + } + } ); + + add( mLNAGain ); + + /** + * Mixer Gain + */ + mMixerGain = new JCheckBox( "Mixer Gain:" ); + mMixerGain.setComponentOrientation( ComponentOrientation.RIGHT_TO_LEFT ); + + mMixerGain.addActionListener( new ActionListener() + { + @Override + public void actionPerformed( ActionEvent e ) + { + mSelectedConfig.setGainMixer( mMixerGain.isSelected() ); + + save(); + + update( mSelectedConfig ); + } + } ); + + add( mMixerGain ); + + /** + * Lookup the save config and apply that config to update all of the + * controls + */ + TunerConfigurationAssignment savedConfig = mResourceManager + .getSettingsManager().getSelectedTunerConfiguration( + TunerType.FUNCUBE_DONGLE_PRO_PLUS, + mController.getAddress() ); + + if( savedConfig != null ) + { + mSelectedConfig = getNamedConfiguration( + savedConfig.getTunerConfigurationName() ); + } + + if( mSelectedConfig == null ) + { + mSelectedConfig = mComboConfigurations.getItemAt( 0 ); + + //Store this config as the default for this tuner at this address + mResourceManager.getSettingsManager() + .setSelectedTunerConfiguration( + TunerType.FUNCUBE_DONGLE_PRO_PLUS, + mController.getAddress(), mSelectedConfig ); + } + + mComboConfigurations.setSelectedItem( mSelectedConfig ); + + /** + * Create a new configuration + */ + mNewConfiguration = new JButton( "New" ); + + mNewConfiguration.addActionListener( new ActionListener() + { + @Override + public void actionPerformed( ActionEvent e ) + { + TunerConfiguration config = + mResourceManager.getSettingsManager() + .addNewTunerConfiguration( + TunerType.FUNCUBE_DONGLE_PRO_PLUS, + "New Configuration" ); + + mComboConfigurations.setModel( getModel() ); + + mComboConfigurations.setSelectedItem( config ); + + repaint(); + } + } ); + + add( mNewConfiguration, "growx,push" ); + + /** + * Delete the currently selected configuration + */ + mDeleteConfiguration = new JButton( "Delete" ); + + mDeleteConfiguration.addActionListener( new ActionListener() + { + @Override + public void actionPerformed( ActionEvent e ) + { + FCD2TunerConfiguration selected = + (FCD2TunerConfiguration)mComboConfigurations + .getItemAt( mComboConfigurations.getSelectedIndex() ); + + if( selected != null ) + { + int n = JOptionPane.showConfirmDialog( + FCD2TunerConfigurationPanel.this, + "Are you sure you want to delete '" + + selected.getName() + "'?", + "Are you sure?", + JOptionPane.YES_NO_OPTION ); + + if( n == JOptionPane.YES_OPTION ) + { + mResourceManager.getSettingsManager() + .deleteTunerConfiguration( selected ); + + mComboConfigurations.setModel( getModel() ); + + mComboConfigurations.setSelectedIndex( 0 ); + + repaint(); + } + } + } + } ); + + add( mDeleteConfiguration, "growx,push" ); + } + + private void update( FCD2TunerConfiguration config ) + { + mSelectedConfig = config; + + try + { + mController.apply( config ); + + mName.setText( config.getName() ); + mLNAGain.setSelected( config.getGainLNA() ); + mMixerGain.setSelected( config.getGainMixer() ); + mFrequencyCorrection.setValue( config.getFrequencyCorrection() ); + + mResourceManager.getSettingsManager().setSelectedTunerConfiguration( + TunerType.FUNCUBE_DONGLE_PRO_PLUS, + mController.getAddress(), config ); + } + catch ( SourceException e1 ) + { + JOptionPane.showMessageDialog( + FCD2TunerConfigurationPanel.this, + "FCD Pro Plus Tuner Controller - couldn't " + + "apply the tuner configuration settings - " + + e1.getLocalizedMessage() ); + + Log.error( "FuncubeDongleProPlus Controller - couldn't apply " + + "config [" + config.getName() + "] - " + + e1.getLocalizedMessage() ); + } + } + + private ComboBoxModel getModel() + { + ArrayList configs = + mResourceManager.getSettingsManager() + .getTunerConfigurations( TunerType.FUNCUBE_DONGLE_PRO_PLUS ); + + DefaultComboBoxModel model = + new DefaultComboBoxModel(); + + for( TunerConfiguration config: configs ) + { + model.addElement( (FCD2TunerConfiguration)config ); + } + + return model; + } + + private FCD2TunerConfiguration getNamedConfiguration( String name ) + { + ArrayList configs = + mResourceManager.getSettingsManager() + .getTunerConfigurations( TunerType.FUNCUBE_DONGLE_PRO_PLUS ); + + for( TunerConfiguration config: configs ) + { + if( config.getName().contentEquals( name ) ) + { + return (FCD2TunerConfiguration)config; + } + } + + return null; + } + + private void save() + { + mResourceManager.getSettingsManager().save(); + } +} diff --git a/src/source/tuner/fcd/proplusV2/FCD2TunerController.java b/src/source/tuner/fcd/proplusV2/FCD2TunerController.java new file mode 100644 index 00000000..0e3cdcca --- /dev/null +++ b/src/source/tuner/fcd/proplusV2/FCD2TunerController.java @@ -0,0 +1,367 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package source.tuner.fcd.proplusV2; + +import javax.swing.JPanel; +import javax.usb.UsbClaimException; +import javax.usb.UsbException; + +import log.Log; +import source.SourceException; +import source.tuner.TunerClass; +import source.tuner.TunerConfiguration; +import source.tuner.TunerType; +import source.tuner.fcd.FCDCommand; +import source.tuner.fcd.FCDTuner; +import source.tuner.fcd.FCDTunerController; +import source.tuner.usb.USBTunerDevice; +import controller.ResourceManager; + +public class FCD2TunerController extends FCDTunerController +{ + public static final int sMINIMUM_TUNABLE_FREQUENCY = 150000; + public static final int sMAXIMUM_TUNABLE_FREQUENCY = 2050000000; + public static final int sSAMPLE_RATE = 192000; + + private FCD2TunerConfiguration mTunerConfiguration; + private FCD2TunerEditorPanel mEditor; + + public FCD2TunerController( USBTunerDevice device ) + throws SourceException + { + super( device, + sMINIMUM_TUNABLE_FREQUENCY, + sMAXIMUM_TUNABLE_FREQUENCY ); + + mFrequencyController.setBandwidth( sSAMPLE_RATE ); + + try + { + open(); + + setFCDMode( Mode.APPLICATION ); + } + catch( Exception e ) + { + throw new SourceException( "FCDTunerController error " + + "during construction", e ); + } + } + + public int getCurrentSampleRate() + { + return sSAMPLE_RATE; + } + + @Override + public TunerClass getTunerClass() + { + return TunerClass.FUNCUBE_DONGLE_PRO_PLUS; + } + + @Override + public TunerType getTunerType() + { + return TunerType.FUNCUBE_DONGLE_PRO_PLUS; + } + + public JPanel getEditor( FCDTuner tuner, ResourceManager resourceManager ) + { + if( mEditor == null ) + { + mEditor = new FCD2TunerEditorPanel( tuner, resourceManager ); + } + + return mEditor; + } + + @Override + public void apply( TunerConfiguration config ) throws SourceException + { + if( config instanceof FCD2TunerConfiguration ) + { + FCD2TunerConfiguration plusConfig = + (FCD2TunerConfiguration)config; + + setFrequencyCorrection( plusConfig.getFrequencyCorrection() ); + + try + { + if( plusConfig.getGainLNA() ) + { + send( FCDCommand.APP_SET_LNA_GAIN, 1 ); + + } + else + { + send( FCDCommand.APP_SET_LNA_GAIN, 0 ); + } + } + catch ( UsbClaimException e ) + { + throw new SourceException( "Exception while setting LNA Gain", e ); + } + catch ( UsbException e ) + { + throw new SourceException( "Exception while setting LNA Gain", e ); + } + + try + { + if( plusConfig.getGainMixer() ) + { + send( FCDCommand.APP_SET_MIXER_GAIN, 1 ); + + } + else + { + send( FCDCommand.APP_SET_MIXER_GAIN, 0 ); + } + } + catch ( UsbClaimException e ) + { + throw new SourceException( "Exception while setting mixer gain", e ); + } + catch ( UsbException e ) + { + throw new SourceException( "Exception while setting mixer gain", e ); + } + + mTunerConfiguration = plusConfig; + } + } + + @Override + public TunerConfiguration getTunerConfiguration() + { + return mTunerConfiguration; + } + + public int getDCCorrection() + { + int dcCorrection = -999; + + try + { + dcCorrection = (int)send( FCDCommand.APP_GET_DC_CORRECTION ); + } + catch ( UsbClaimException e ) + { + Log.error( "FCTTunerController - couldn't claim funcube HID to" + + " get dc correction value" ); + } + catch ( UsbException e ) + { + Log.error( "FCTTunerController - error getting dc correction " + + "value - " + e.getLocalizedMessage() ); + + } + + return dcCorrection; + } + + public void setDCCorrection( int value ) + { + try + { + send( FCDCommand.APP_SET_DC_CORRECTION, value ); + } + catch ( UsbClaimException e ) + { + Log.error( "FCDTunerController - error claiming usb hid while " + + "trying to set dc correction to [" + value + "]" ); + } + catch ( UsbException e ) + { + Log.error( "FCDTunerController - error while " + + "trying to set dc correction to [" + value + "] - " + + e.getLocalizedMessage() ); + } + } + + public int getIQCorrection() + { + int iqCorrection = -999; + + try + { + iqCorrection = (int)send( FCDCommand.APP_GET_IQ_CORRECTION ); + } + catch ( UsbClaimException e ) + { + Log.error( "FCTTunerController - couldn't claim funcube HID to" + + " get iq correction value" ); + } + catch ( UsbException e ) + { + Log.error( "FCTTunerController - error getting iq correction " + + "value - " + e.getLocalizedMessage() ); + + } + + return iqCorrection; + } + + public void setIQCorrection( int value ) + { + try + { + send( FCDCommand.APP_SET_IQ_CORRECTION, value ); + } + catch ( UsbClaimException e ) + { + Log.error( "FCDTunerController - error claiming usb hid while " + + "trying to set iq correction to [" + value + "]" ); + } + catch ( UsbException e ) + { + Log.error( "FCDTunerController - error while " + + "trying to set iq correction to [" + value + "] - " + + e.getLocalizedMessage() ); + } + } + + public enum Block + { + CELLULAR_BAND_BLOCKED( "Blocked" ), + NO_BAND_BLOCK( "Unblocked" ), + UNKNOWN( "Unknown" ); + + private String mLabel; + + private Block( String label ) + { + mLabel = label; + } + + public String getLabel() + { + return mLabel; + } + + public static Block getBlock( String block ) + { + Block retVal = UNKNOWN; + + if( block.equalsIgnoreCase( "No blk" ) ) + { + retVal = NO_BAND_BLOCK; + } + else if( block.equalsIgnoreCase( "Cell blk" ) ) + { + retVal = CELLULAR_BAND_BLOCKED; + } + + return retVal; + } + } + + public enum TunerRFFilter + { + TRF_0_4, + TRF_4_8, + TRF_8_16, + TRF_16_32, + TRF_32_75, + TRF_75_125, + TRF_125_250, + TRF_145, + TRF_410_875, + TRF_435, + TRF_875_2000, + UNKNOWN; + + public static TunerRFFilter get( int value ) + { + TunerRFFilter retVal = UNKNOWN; + + if( 0 <= value && value <= 10 ) + { + retVal = values()[ value ]; + } + + return retVal; + } + } + + public enum TunerIFFilter + { + TIF_200KHZ, + TIF_300KHZ, + TIF_600KHZ, + TIF_1536KHZ, + TIF_5MHZ, + TIF_6MHZ, + TIF_7MHZ, + TIF_8MHZ, + UNKNOWN; + + public static TunerIFFilter get( int value ) + { + TunerIFFilter retVal = UNKNOWN; + + if( 0 <= value && value <= 7 ) + { + retVal = values()[ value ]; + } + + return retVal; + } + } + + public enum LNAGain + { + LNA_GAIN_MINUS_5_0( 0, "-5.0 dB" ), + LNA_GAIN_MINUS_2_5( 1, "-2.5 dB" ), + LNA_GAIN_PLUS_0_0( 4, "0.0 dB" ), + LNA_GAIN_PLUS_2_5( 5, "2.5 dB" ), + LNA_GAIN_PLUS_5_0( 6, "5.0 dB" ), + LNA_GAIN_PLUS_7_5( 7, "7.5 dB" ), + LNA_GAIN_PLUS_10_0( 8, "10.0 dB" ), + LNA_GAIN_PLUS_12_5( 9, "12.5 dB" ), + LNA_GAIN_PLUS_15_0( 10, "15.0 dB" ), + LNA_GAIN_PLUS_17_5( 11, "17.5 dB" ), + LNA_GAIN_PLUS_20_0( 12, "20.0 dB" ), + LNA_GAIN_PLUS_25_0( 13, "25.0 dB" ), + LNA_GAIN_PLUS_30_0( 14, "30.0 dB" ); + + private int mSetting; + private String mLabel; + + private LNAGain( int setting, String label ) + { + mSetting = setting; + mLabel = label; + } + + public int getSetting() + { + return mSetting; + } + + public String getLabel() + { + return mLabel; + } + + public String toString() + { + return getLabel(); + } + } +} diff --git a/src/source/tuner/fcd/proplusV2/FCD2TunerEditorPanel.java b/src/source/tuner/fcd/proplusV2/FCD2TunerEditorPanel.java new file mode 100644 index 00000000..1e665402 --- /dev/null +++ b/src/source/tuner/fcd/proplusV2/FCD2TunerEditorPanel.java @@ -0,0 +1,101 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package source.tuner.fcd.proplusV2; + +import java.awt.Color; + +import gui.control.JFrequencyControl; + +import javax.swing.BorderFactory; +import javax.swing.JLabel; +import javax.swing.JPanel; + +import com.jidesoft.swing.JideTabbedPane; + +import log.Log; +import net.miginfocom.swing.MigLayout; +import source.SourceException; +import source.tuner.FrequencyChangeListener; +import source.tuner.fcd.FCDTuner; +import source.tuner.fcd.FCDTunerDetailsPanel; +import controller.ResourceManager; + +public class FCD2TunerEditorPanel extends JPanel implements FrequencyChangeListener +{ + private static final long serialVersionUID = 1L; + + private FCDTuner mTuner; + private FCD2TunerController mController; + private ResourceManager mResourceManager; + + private JFrequencyControl mFrequencyControl; + + public FCD2TunerEditorPanel( FCDTuner tuner, ResourceManager resourceManager ) + { + mTuner = tuner; + mController = (FCD2TunerController)mTuner.getController(); + mResourceManager = resourceManager; + + initGUI(); + } + + private void initGUI() + { + setLayout( new MigLayout( "fill,wrap 2", "[right,grow][grow]", "[][][grow]" ) ); + + add( new JLabel( "Funcube Dongle Pro Plus Tuner" ), "span,align center" ); + + mFrequencyControl = new JFrequencyControl(); + + mFrequencyControl.addListener( this ); + + /* Add frequency control as frequency change listener. This creates a + * feedback loop, so the control does not rebroadcast the event */ + mTuner.addListener( mFrequencyControl ); + + mFrequencyControl.setFrequency( mController.getFrequency(), false ); + + add( mFrequencyControl, "span,align center" ); + + JideTabbedPane tabs = new JideTabbedPane(); + tabs.setFont( this.getFont() ); + tabs.setForeground( Color.BLACK ); + + tabs.add( "Configuration", new FCD2TunerConfigurationPanel( + mResourceManager, mController ) ); + + tabs.add( "Info", new FCDTunerDetailsPanel( mController ) ); + + add( tabs, "span,grow,push" ); + } + + @Override + public void frequencyChanged( int frequency, int bandwidth ) + { + //Disregard the bandwidth, which shouldn't be changing + try + { + mController.setFrequency( frequency ); + } + catch ( SourceException e ) + { + Log.error( "FCTProController - error setting frequency [" + + frequency + "] - " + e.getLocalizedMessage() ); + } + } +} diff --git a/src/source/tuner/rtl/RTL2832InfoPanel.java b/src/source/tuner/rtl/RTL2832InfoPanel.java new file mode 100644 index 00000000..f65bfbc9 --- /dev/null +++ b/src/source/tuner/rtl/RTL2832InfoPanel.java @@ -0,0 +1,76 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package source.tuner.rtl; + +import javax.swing.JLabel; +import javax.swing.JPanel; + +import net.miginfocom.swing.MigLayout; +import source.tuner.rtl.RTL2832TunerController.Descriptor; + +public class RTL2832InfoPanel extends JPanel +{ + RTL2832TunerController mController; + + public RTL2832InfoPanel( RTL2832TunerController controller ) + { + mController = controller; + + init(); + } + + public void dispose() + { + mController = null; + } + + private void init() + { + setLayout( new MigLayout( "fill,wrap 2", "[right][left]", "[][][][][][][][grow]" ) ); + + Descriptor descriptor = mController.getDescriptor(); + + add( new JLabel( "RTL-2832 EEPROM Descriptor" ), "span,align center" ); + + if( descriptor == null ) + { + add( new JLabel( "No descriptor" ), "wrap" ) ; + } + else + { + add( new JLabel( "USB ID:" ) ) ; + add( new JLabel( descriptor.getVendorID() + ":" + + descriptor.getProductID() ), "wrap" ) ; + + add( new JLabel( "Vendor:" ) ) ; + add( new JLabel( descriptor.getVendorLabel() ), "wrap" ) ; + + add( new JLabel( "Product:" ) ) ; + add( new JLabel( descriptor.getProductLabel() ), "wrap" ) ; + + add( new JLabel( "Serial:" ) ) ; + add( new JLabel( descriptor.getSerial() ), "wrap" ) ; + + add( new JLabel( "IR Enabled:" ) ) ; + add( new JLabel( ( descriptor.irEnabled() ? "Yes" : "No" ) ), "wrap" ) ; + + add( new JLabel( "Remote Wake:" ) ) ; + add( new JLabel( ( descriptor.remoteWakeupEnabled() ? "Yes" : "No" ) ), "wrap" ) ; + } + } +} diff --git a/src/source/tuner/rtl/RTL2832SampleAdapter.java b/src/source/tuner/rtl/RTL2832SampleAdapter.java new file mode 100644 index 00000000..3ec92322 --- /dev/null +++ b/src/source/tuner/rtl/RTL2832SampleAdapter.java @@ -0,0 +1,59 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package source.tuner.rtl; + +import source.Source.SampleType; +import source.mixer.SampleAdapter; + +public class RTL2832SampleAdapter extends SampleAdapter +{ + private final static float[] mLOOKUP_VALUES; + + /** + * Creates a static lookup table that converts the 8-bit valued range + * from 0 - 255 into scaled float values of -128 to 0 to 127 + */ + static + { + mLOOKUP_VALUES = new float[ 256 ]; + + float scale = (float)( 1.0f / 127.0f ); + + for( int x = 0; x < 256; x++ ) + { + mLOOKUP_VALUES[ x ] = (float)( x - 128 ) * scale; + } + + mLOOKUP_VALUES[ 128 ] = 0.0000001f; + } + + @Override + public Float[] convert( byte[] samples ) + { + Float[] convertedSamples = new Float[ samples.length ]; + int pointer = 0; + + for( byte sample: samples ) + { + /* Convert byte value into float from the lookup table */ + convertedSamples[ pointer++ ] = mLOOKUP_VALUES[ ( sample & 0xFF ) ]; + } + + return convertedSamples; + } +} diff --git a/src/source/tuner/rtl/RTL2832Tuner.java b/src/source/tuner/rtl/RTL2832Tuner.java new file mode 100644 index 00000000..f226202c --- /dev/null +++ b/src/source/tuner/rtl/RTL2832Tuner.java @@ -0,0 +1,168 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package source.tuner.rtl; + +import java.util.concurrent.RejectedExecutionException; + +import javax.swing.JPanel; +import javax.usb.UsbException; + +import log.Log; +import sample.Listener; +import source.SourceException; +import source.tuner.FrequencyChangeListener; +import source.tuner.Tuner; +import source.tuner.TunerChannel; +import source.tuner.TunerChannelSource; +import source.tuner.TunerClass; +import source.tuner.TunerConfiguration; +import source.tuner.TunerType; +import source.tuner.rtl.RTL2832TunerController.SampleRate; +import controller.ResourceManager; +import controller.ThreadPoolManager; + +public class RTL2832Tuner extends Tuner +{ + private TunerClass mTunerClass; + protected RTL2832TunerController mController; + + public RTL2832Tuner( TunerClass tunerClass, + RTL2832TunerController controller ) + throws SourceException + { + super( tunerClass.getVendorDeviceLabel() + "/" + + controller.getTunerType().getLabel() + " #" + + controller.getUniqueID() ); + + mTunerClass = tunerClass; + mController = controller; + mController.addListener( (FrequencyChangeListener)this ); + } + + public RTL2832TunerController getController() + { + return mController; + } + + @Override + public String getUniqueID() + { + return mController.getUniqueID(); + } + + @Override + public TunerClass getTunerClass() + { + return mTunerClass; + } + + @Override + public TunerType getTunerType() + { + return mController.getTunerType(); + } + + @Override + public JPanel getEditor( ResourceManager resourceManager ) + { + return mController.getEditor( resourceManager ); + } + + @Override + public void apply( TunerConfiguration config ) throws SourceException + { + mController.apply( config ); + } + + @Override + public int getSampleRate() + { + try + { + return mController.getCurrentSampleRate(); + } + catch( SourceException e ) + { + Log.error( "RTL2832 Tuner - couldn't get sample rate - " + + e.getLocalizedMessage() ); + } + + return 0; + } + + public void setSampleRate( SampleRate sampleRate ) throws SourceException + { + try + { + mController.setSampleRate( sampleRate ); + } + catch( UsbException e ) + { + throw new SourceException( "RTL2832 Tuner - error setting " + + "sample rate", e ); + } + } + + @Override + public int getFrequency() throws SourceException + { + return mController.getFrequency(); + } + + public void setFrequency( int frequency ) throws SourceException + { + mController.setFrequency( frequency ); + } + + @Override + public TunerChannelSource getChannel( ThreadPoolManager threadPoolManager, + TunerChannel channel ) + throws RejectedExecutionException, + SourceException + { + return mController.getChannel( threadPoolManager, this, channel ); + } + /** + * Releases the tuned channel so that the tuner controller can tune to + * other frequencies as needed. + */ + @Override + public void releaseChannel( TunerChannelSource source ) + { + /* Unregister for receiving samples */ + removeListener( (Listener)source ); + + /* Tell the controller to release the channel and cleanup */ + if( source != null ) + { + mController.releaseChannel( source ); + } + } + + @Override + public void addListener( Listener listener ) + { + mController.addListener( listener ); + } + + @Override + public void removeListener( Listener listener ) + { + mController.removeListener( listener ); + } +} diff --git a/src/source/tuner/rtl/RTL2832TunerController.java b/src/source/tuner/rtl/RTL2832TunerController.java new file mode 100644 index 00000000..4c3bca67 --- /dev/null +++ b/src/source/tuner/rtl/RTL2832TunerController.java @@ -0,0 +1,1848 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * Java version based on librtlsdr + * Copyright (C) 2012-2013 by Steve Markgraf + * Copyright (C) 2012 by Dimitri Stolnikov + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package source.tuner.rtl; + +import java.nio.charset.Charset; +import java.text.DecimalFormat; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Iterator; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.Executors; +import java.util.concurrent.LinkedTransferQueue; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; + +import javax.swing.JPanel; +import javax.usb.UsbClaimException; +import javax.usb.UsbConfiguration; +import javax.usb.UsbControlIrp; +import javax.usb.UsbDevice; +import javax.usb.UsbDisconnectedException; +import javax.usb.UsbEndpoint; +import javax.usb.UsbException; +import javax.usb.UsbInterface; +import javax.usb.UsbInterfacePolicy; +import javax.usb.UsbIrp; +import javax.usb.UsbNotActiveException; +import javax.usb.UsbNotOpenException; +import javax.usb.UsbPipe; +import javax.usb.event.UsbPipeDataEvent; +import javax.usb.event.UsbPipeErrorEvent; +import javax.usb.event.UsbPipeListener; +import javax.usb.util.DefaultUsbControlIrp; + +import log.Log; + +import org.usb4java.LibUsb; + +import sample.Listener; +import source.SourceException; +import source.tuner.TunerController; +import source.tuner.TunerType; +import source.tuner.usb.USBTunerDevice; +import util.TimeStamp; +import controller.ResourceManager; + +public abstract class RTL2832TunerController extends TunerController +{ + public final static int sINT_NULL_VALUE = -1; + public final static long sLONG_NULL_VALUE = -1l; + public final static double sDOUBLE_NULL_VALUE = -1.0D; + + public final static byte sUSB_INTERFACE = (byte)0x0; + public final static byte sUSB_ENDPOINT = (byte)0x81; + public final static boolean sUSB_FORCE_CLAIM_INTERFACE = true; + + public final static byte sREQUEST_TYPE_IN = + (byte)( LibUsb.ENDPOINT_IN | LibUsb.REQUEST_TYPE_VENDOR ); + public final static byte sREQUEST_TYPE_OUT = + (byte)( LibUsb.ENDPOINT_OUT | LibUsb.REQUEST_TYPE_VENDOR ); + public final static byte sREQUEST_ZERO = (byte)0; + + public final static int sUSB_IRP_DATA_SIZE = 65536; + public final static int sUSB_IRP_POOL_SIZE = 10; + + public final static byte EEPROM_ADDRESS = (byte)0xA0; + + public final static byte BIT_0 = (byte)0x01; + public final static byte BIT_1 = (byte)0x02; + public final static byte BIT_2 = (byte)0x04; + public final static byte BIT_3 = (byte)0x08; + public final static byte BIT_4 = (byte)0x10; + public final static byte BIT_5 = (byte)0x20; + public final static byte BIT_6 = (byte)0x40; + public final static byte BIT_7 = (byte)0x80; + + public final static byte E4K_I2C_ADDRESS = (byte)0xC8; + public final static byte E4K_CHECK_ADDRESS = (byte)0x02; + public final static byte E4K_CHECK_VALUE = (byte)0x40; + + public final static byte FC0012_I2C_ADDRESS = (byte)0xC6; + public final static byte FC0012_CHECK_ADDRESS = (byte)0x00; + public final static byte FC0012_CHECK_VALUE = (byte)0xA1; + + public final static byte FC0013_I2C_ADDRESS = (byte)0xC6; + public final static byte FC0013_CHECK_ADDRESS = (byte)0x00; + public final static byte FC0013_CHECK_VALUE = (byte)0xA3; + + public final static byte FC2580_I2C_ADDRESS = (byte)0xAC; + public final static byte FC2580_CHECK_ADDRESS = (byte)0x01; + public final static byte FC2580_CHECK_VALUE = (byte)0x56; + + public final static byte R820T_I2C_ADDRESS = (byte)0x34; + public final static byte R820T_CHECK_ADDRESS = (byte)0x00; + public final static byte R820T_CHECK_VALUE = (byte)0x69; + + public final static byte R828D_I2C_ADDRESS = (byte)0x74; + public final static byte R828D_CHECK_ADDRESS = (byte)0x00; + public final static byte R828D_CHECK_VALUE = (byte)0x69; + + public static final int sMIN_OSCILLATOR_FREQUENCY = 28799000; + public static final int sMAX_OSCILLATOR_FREQUENCY = 28801000; + + public final static byte[] sFIR_COEFFICIENTS = + { + (byte)0xCA, (byte)0xDC, (byte)0xD7, (byte)0xD8, (byte)0xE0, + (byte)0xF2, (byte)0x0E, (byte)0x35, (byte)0x06, (byte)0x50, + (byte)0x9C, (byte)0x0D, (byte)0x71, (byte)0x11, (byte)0x14, + (byte)0x71, (byte)0x74, (byte)0x19, (byte)0x41, (byte)0xA5 + }; + + private static final DecimalFormat mDecimalFormatter = + new DecimalFormat( "###,###,###.#" ); + + private USBTunerDevice mUSBTunerDevice; + protected UsbDevice mUSBDevice; + private UsbConfiguration mUSBConfiguration; + protected UsbInterface mUSBInterface; + private UsbPipe mUSBPipe; + private Descriptor mDescriptor; + + public static final int sTWO_TO_22_POWER = 4194304; + public static final int sOSCILLATOR_FREQUENCY = 28800000; //28.8 MHz + public static final SampleRate sDEFAULT_SAMPLE_RATE = SampleRate.RATE_1_200MHZ; + + private SampleRate mSampleRate = sDEFAULT_SAMPLE_RATE; + + private BufferProcessor mBufferProcessor = new BufferProcessor(); + + private RTL2832SampleAdapter mSampleAdapter = new RTL2832SampleAdapter(); + + private CopyOnWriteArrayList> mSampleListeners = + new CopyOnWriteArrayList>(); + + private LinkedTransferQueue mRawSampleBuffer = + new LinkedTransferQueue(); + private AtomicInteger mSampleCounter = new AtomicInteger(); + private double mSampleRateAverageSum; + private int mSampleRateAverageCount; + + /** + * Abstract tuner controller device. Use the static getTunerClass() method + * to determine the tuner type, and construct the corresponding child + * tuner controller class for that tuner type. + */ + public RTL2832TunerController( USBTunerDevice tunerDevice, + int minTunableFrequency, + int maxTunableFrequency ) throws SourceException + { + super( minTunableFrequency, maxTunableFrequency ); + + mUSBTunerDevice = tunerDevice; + mUSBDevice = tunerDevice.getDevice(); + mUSBConfiguration = mUSBDevice.getActiveUsbConfiguration(); + mUSBInterface = mUSBConfiguration.getUsbInterface( sUSB_INTERFACE ); + + try + { + if( claim( mUSBInterface ) ) + { + /* Get the USB endpoint */ + UsbEndpoint endpoint = mUSBInterface.getUsbEndpoint( sUSB_ENDPOINT ); + mUSBPipe = endpoint.getUsbPipe(); + } + else + { + throw new SourceException( "RTL2832 Tuner Controller - couldn't " + + "claim USB interface" ); + } + } + catch( UsbException e ) + { + throw new SourceException( "RTL2832 Tuner Controller - couldn't " + + "claim USB interface or get endpoint or pipe", e ); + } + catch ( UsbDisconnectedException e ) + { + throw new SourceException( "RTL2832 Tuner Controller - usb device " + + "is disconnected", e ); + } + + } + + public void init() throws SourceException + { + try + { + setSampleRate( sDEFAULT_SAMPLE_RATE ); + + /* Read the contents of the 256-byte EEPROM */ + byte[] eeprom = readEEPROM( mUSBDevice, (short)0, 256 ); + + /* Create the descriptor with the contents from the EEPROM */ + mDescriptor = new Descriptor( eeprom ); + } + catch( Exception e ) + { + throw new SourceException( "RTL2832 Tuner Controller - couldn't " + + "set default sample rate or read EEPROM descriptor", e ); + } + } + + /** + * Descriptor contains all identifiers and labels parsed from the EEPROM. + * + * May return null if unable to get 256 byte eeprom descriptor from tuner + * or if the descriptor doesn't begin with byte values of 0x28 and 0x32 + * meaning it is a valid (and can be parsed) RTL2832 descriptor + */ + public Descriptor getDescriptor() + { + if( mDescriptor != null && mDescriptor.isValid() ) + { + return mDescriptor; + } + + return null; + } + +// public void start() +// { +// if( !mBufferProcessor.isRunning() ) +// { +// try +// { +// resetUSBBuffer(); +// +// /* Open the USB pipe */ +// openPipe(); +// +// mBufferProcessor.start(); +// } +// catch ( UsbException e ) +// { +// Log.error( "RTL2832TunerController - error starting sample " +// + "buffer processor - " + e.getLocalizedMessage() ); +// } +// } +// } + + public void setSamplingMode( SampleMode mode ) throws UsbException + { + switch( mode ) + { + case QUADRATURE: +// /* Initialize the tuner */ +// initTuner(); + + /* Set intermediate frequency to 0 Hz */ + setIFFrequency( 0 ); + + /* Enable I/Q ADC Input */ + writeDemodRegister( mUSBDevice, + Page.ZERO, + (short)0x08, + (short)0xCD, + 1 ); + + /* Enable zero-IF mode */ + writeDemodRegister( mUSBDevice, + Page.ONE, + (short)0xB1, + (short)0x1B, + 1 ); + + /* Set default i/q path */ + writeDemodRegister( mUSBDevice, + Page.ZERO, + (short)0x06, + (short)0x80, + 1 ); + break; + case DIRECT: + //TODO: add code for direct sampling + break; + default: + break; + + } + } + + public void setIFFrequency( int frequency ) throws UsbException + { + long ifFrequency = ( (long)sTWO_TO_22_POWER * (long)frequency ) / + (long)sOSCILLATOR_FREQUENCY * -1; + + /* Write byte 2 (high) */ + writeDemodRegister( mUSBDevice, + Page.ONE, + (short)0x19, + (short)( Long.rotateRight( ifFrequency, 16 ) & 0x3F ), + 1 ); + + /* Write byte 1 (middle) */ + writeDemodRegister( mUSBDevice, + Page.ONE, + (short)0x1A, + (short)( Long.rotateRight( ifFrequency, 8 ) & 0xFF ), + 1 ); + + /* Write byte 0 (low) */ + writeDemodRegister( mUSBDevice, + Page.ONE, + (short)0x1B, + (short)( ifFrequency & 0xFF ), + 1 ); + } + + public abstract void initTuner( boolean controlI2CRepeater ) + throws UsbException; + + /** + * Provides a unique identifier to use in distinctly identifying this + * tuner from among other tuners of the same type, so that we can fetch a + * tuner configuration from the settings manager for this specific tuner. + * + * @return serial number of the device + */ + public String getUniqueID() + { + if( mDescriptor != null && mDescriptor.hasSerial() ) + { + return mDescriptor.getSerial(); + } + else + { + int serial = + ( 0xFF & mUSBDevice.getUsbDeviceDescriptor().iSerialNumber() ); + + return "SER#" + serial; + } + } + + public abstract JPanel getEditor( ResourceManager resourceManager ); + + public abstract void setSampleRateFilters( int sampleRate ) throws UsbException; + + public abstract TunerType getTunerType(); + + public static TunerType identifyTunerType( USBTunerDevice tunerDevice ) + { + TunerType tunerClass = TunerType.UNKNOWN; + + try + { + UsbDevice device = tunerDevice.getDevice(); + UsbConfiguration config = device.getActiveUsbConfiguration(); + UsbInterface iface = config.getUsbInterface( sUSB_INTERFACE ); + + if( claim( iface ) ) + { + /* Perform a dummy write to see if the device needs reset */ + writeRegister( device, Block.USB, Address.USB_SYSCTL.getAddress(), 0x09, 1 ); + + /* Initialize the baseband */ + initBaseband( device ); + + enableI2CRepeater( device, true ); + + boolean controlI2CRepeater = false; + + /* Test for each tuner type until we find the correct one */ + if( isE4000Tuner( device, controlI2CRepeater ) ) + { + tunerClass = TunerType.ELONICS_E4000; + } + else if( isFC0013Tuner( device, controlI2CRepeater ) ) + { + tunerClass = TunerType.FITIPOWER_FC0013; + } + else if( isR820TTuner( device, controlI2CRepeater ) ) + { + tunerClass = TunerType.RAFAELMICRO_R820T; + } + else if( isR828DTuner( device, controlI2CRepeater ) ) + { + tunerClass = TunerType.RAFAELMICRO_R828D; + } + else if( isFC2580Tuner( device, controlI2CRepeater ) ) + { + tunerClass = TunerType.FCI_FC2580; + } + else if( isFC0012Tuner( device, controlI2CRepeater ) ) + { + tunerClass = TunerType.FITIPOWER_FC0012; + } + + enableI2CRepeater( device, false ); + + iface.release(); + } + } + catch( Exception e ) + { + Log.error( "RTL2832TunerController - error while determining " + + "tuner type - " + e.getLocalizedMessage() ); + } + + return tunerClass; + } + + public USBTunerDevice getUSBTunerDevice() + { + return mUSBTunerDevice; + } + +// protected void openPipe() throws UsbException, UsbClaimException +// { +// /* Get the USB endpoint */ +// UsbEndpoint endpoint = mUSBInterface.getUsbEndpoint( sUSB_ENDPOINT ); +// +// /* Get the pipe from the endpoint */ +// if( endpoint != null ) +// { +// mUSBPipe = endpoint.getUsbPipe(); +// +// if( mUSBPipe != null ) +// { +// if( !mUSBPipe.isOpen() ) +// { +// try +// { +// resetUSBBuffer(); +// +// mUSBPipe.open(); +// } +// catch( Exception e ) +// { +// Log.error( "RTL2832 Tuner Controller - exception while " + +// "opening the endpoint pipe." + +// e.getLocalizedMessage() ); +// } +// } +// } +// else +// { +// Log.error( "RTL2832 Tuner Controller - returned endpoint " +// + "OUT pipe is null" ); +// } +// } +// } +// + /** + * Claims the USB interface. If another application currently has + * the interface claimed, the sFCD_FORCE_CLAIM_HID_INTERFACE setting + * will dictate if the interface is forcibly claimed from the other + * application + */ + public static boolean claim( UsbInterface iface ) throws UsbException, + UsbClaimException + { + if( !iface.isClaimed() ) + { + iface.claim( new UsbInterfacePolicy() + { + @Override + public boolean forceClaim( UsbInterface arg0 ) + { + return sUSB_FORCE_CLAIM_INTERFACE; + } + } ); + + return iface.isClaimed(); + } + else + { + Log.error( "RTL2832 Tuner Controller - attempt to claim USB " + + "interface failed - in use by another application" ); + } + + return false; + } + + /** + * Releases the claimed USB interface + */ + private void closePipe() throws UsbException + { + mBufferProcessor.stop(); + + mUSBPipe.close(); + } + + /** + * Releases the USB interface + */ + public void release() + { + try + { + if( mBufferProcessor.isRunning() ) + { + mBufferProcessor.stop(); + } + + if( mUSBPipe.isOpen() ) + { + mUSBPipe.close(); + } + + mUSBInterface.release(); + } + catch ( Exception e ) + { + Log.error( "RTL2832 Tuner Controller - attempt to release USB " + + "interface failed - " + e.getLocalizedMessage() ); + } + } + + public void resetUSBBuffer() throws UsbException + { + writeRegister( mUSBDevice, Block.USB, Address.USB_EPA_CTL.getAddress(), 0x1002, 2 ); + writeRegister( mUSBDevice, Block.USB, Address.USB_EPA_CTL.getAddress(), 0x0000, 2 ); + } + + public static void initBaseband( UsbDevice device ) + throws IllegalArgumentException, UsbDisconnectedException, UsbException + { + /* Initialize USB */ + writeRegister( device, Block.USB, Address.USB_SYSCTL.getAddress(), 0x09, 1 ); + writeRegister( device, Block.USB, Address.USB_EPA_MAXPKT.getAddress(), 0x0002, 2 ); + writeRegister( device, Block.USB, Address.USB_EPA_CTL.getAddress(), 0x1002, 2 ); + + /* Power on demod */ + writeRegister( device, Block.SYS, Address.DEMOD_CTL_1.getAddress(), 0x22, 1 ); + writeRegister( device, Block.SYS, Address.DEMOD_CTL.getAddress(), 0xE8, 1 ); + + /* Reset demod */ + writeDemodRegister( device, Page.ONE, (short)0x01, 0x14, 1 ); //Bit 3 = soft reset + writeDemodRegister( device, Page.ONE, (short)0x01, 0x10, 1 ); + + /* Disable spectrum inversion and adjacent channel rejection */ + writeDemodRegister( device, Page.ONE, (short)0x15, 0x00, 1 ); + writeDemodRegister( device, Page.ONE, (short)0x16, 0x0000, 2 ); + + /* Clear DDC shift and IF frequency registers */ + writeDemodRegister( device, Page.ONE, (short)0x16, 0x00, 1 ); + writeDemodRegister( device, Page.ONE, (short)0x17, 0x00, 1 ); + writeDemodRegister( device, Page.ONE, (short)0x18, 0x00, 1 ); + writeDemodRegister( device, Page.ONE, (short)0x19, 0x00, 1 ); + writeDemodRegister( device, Page.ONE, (short)0x1A, 0x00, 1 ); + writeDemodRegister( device, Page.ONE, (short)0x1B, 0x00, 1 ); + + /* Set FIR coefficients */ + for( int x = 0; x < sFIR_COEFFICIENTS.length; x++ ) + { + writeDemodRegister( device, + Page.ONE, + (short)( 0x1C + x ), + sFIR_COEFFICIENTS[ x ], + 1 ); + } + + /* Enable SDR mode, disable DAGC (bit 5) */ + writeDemodRegister( device, Page.ZERO, (short)0x19, 0x05, 1 ); + + /* Init FSM state-holding register */ + writeDemodRegister( device, Page.ONE, (short)0x93, 0xF0, 1 ); + writeDemodRegister( device, Page.ONE, (short)0x94, 0x0F, 1 ); + + /* Disable AGC (en_dagc, bit 0) (seems to have no effect) */ + writeDemodRegister( device, Page.ONE, (short)0x11, 0x00, 1 ); + + /* Disable RF and IF AGC loop */ + writeDemodRegister( device, Page.ONE, (short)0x04, 0x00, 1 ); + + /* Disable PID filter */ + writeDemodRegister( device, Page.ZERO, (short)0x61, 0x60, 1 ); + + /* opt_adc_iq = 0, default ADC_I/ADC_Q datapath */ + writeDemodRegister( device, Page.ZERO, (short)0x06, 0x80, 1 ); + + /* Enable Zero-if mode (en_bbin bit), + * DC cancellation (en_dc_est), + * IQ estimation/compensation (en_iq_comp, en_iq_est) */ + writeDemodRegister( device, Page.ONE, (short)0xB1, 0x1B, 1 ); + + /* Disable 4.096 MHz clock output on pin TP_CK0 */ + writeDemodRegister( device, Page.ZERO, (short)0x0D, 0x83, 1 ); + } + + protected void deinitBaseband( UsbDevice device ) + throws IllegalArgumentException, UsbDisconnectedException, UsbException + { +// setI2CRepeater( device, true ); + + writeRegister( device, Block.SYS, Address.DEMOD_CTL.getAddress(), 0x20, 1); + +// setI2CRepeater( device, false ); + } + + /** + * Sets the General Purpose Input/Output (GPIO) register bit + * + * @param device - USB tuner device + * @param bitMask - bit mask with one for targeted register bits and zero + * for the non-targeted register bits + * @param enabled - true to set the bit and false to clear the bit + * @throws UsbDisconnectedException - if the tuner device is disconnected + * @throws UsbException - if there is a USB error while communicating with + * the device + */ + protected static void setGPIOBit( UsbDevice device, byte bitMask, boolean enabled ) + throws UsbDisconnectedException, UsbException + { + //Get current register value + int value = readRegister( device, Block.SYS, Address.GPO.getAddress(), 1 ); + + //Update the masked bits + if( enabled ) + { + value |= bitMask; + } + else + { + value &= ~bitMask; + } + + //Write the change back to the device + writeRegister( device, Block.SYS, Address.GPO.getAddress(), value, 1 ); + } + + /** + * Enables GPIO Output + * @param device - usb tuner device + * @param bitMask - mask containing one bit value in targeted bit field(s) + * @throws UsbDisconnectedException + * @throws UsbException + */ + protected static void setGPIOOutput( UsbDevice device, byte bitMask ) + throws UsbDisconnectedException, UsbException + { + //Get current register value + int value = readRegister( device, Block.SYS, Address.GPD.getAddress(), 1 ); + + //Mask the value and rewrite it + writeRegister( device, Block.SYS, Address.GPO.getAddress(), + value & ~bitMask, 1 ); + + //Get current register value + value = readRegister( device, Block.SYS, Address.GPOE.getAddress(), 1 ); + + //Mask the value and rewrite it + writeRegister( device, Block.SYS, Address.GPOE.getAddress(), + value | bitMask, 1 ); + } + + protected static void enableI2CRepeater( UsbDevice device, boolean enabled ) + throws IllegalArgumentException, UsbDisconnectedException, UsbException + { + Page page = Page.ONE; + short address = 1; + int value; + + if( enabled ) + { + value = 0x18; //ON + } + else + { + value = 0x10; //OFF + } + + writeDemodRegister( device, page, address, value, 1 ); + } + + protected boolean isI2CRepeaterEnabled() throws UsbException + { + int register = readDemodRegister( mUSBDevice, Page.ONE, (short)0x1, 1 ); + + return register == 0x18; + } + + protected static int readI2CRegister( UsbDevice device, + byte i2CAddress, + byte i2CRegister, + boolean controlI2CRepeater ) + throws IllegalArgumentException, UsbDisconnectedException, UsbException + { + short address = (short)( i2CAddress & 0xFF ); + + byte[] writeData = new byte[ 1 ]; + writeData[ 0 ] = i2CRegister; + + byte[] readData = new byte[ 1 ]; + + if( controlI2CRepeater ) + { + enableI2CRepeater( device, true ); + + write( device, Block.IIC, address, writeData ); + read( device, Block.IIC, address, readData ); + + enableI2CRepeater( device, false ); + } + else + { + write( device, Block.IIC, address, writeData ); + read( device, Block.IIC, address, readData ); + } + + return (int)( readData[ 0 ] & 0xFF ); + } + + protected void writeI2CRegister( UsbDevice device, + byte i2CAddress, + byte i2CRegister, + byte value, + boolean controlI2CRepeater ) throws UsbException + { + + short address = (short)( i2CAddress & 0xFF ); + + byte[] data = new byte[ 2 ]; + + data[ 0 ] = i2CRegister; + data[ 1 ] = value; + + if( controlI2CRepeater ) + { + enableI2CRepeater( device, true ); + write( mUSBDevice, Block.IIC, address, data ); + enableI2CRepeater( device, false ); + } + else + { + write( mUSBDevice, Block.IIC, address, data ); + } + } + + protected static void writeDemodRegister( UsbDevice device, Page page, + short address, int value, int length ) + throws IllegalArgumentException, UsbDisconnectedException, UsbException + { + byte[] data; + + if( length == 1 ) + { + data = new byte[ 1 ]; + data[0] = (byte)( value & 0xFF ); + } + else if( length == 2 ) + { + data = new byte[ 2 ]; + data[1] = (byte)( value & 0xFF ); //LSB + data[0] = (byte)( ( value >> 8 ) & 0xFF ); //MSB + } + else + { + throw new IllegalArgumentException( "Cannot write value greater " + + "than 16 bits to the register - length [" + length + "]" ); + } + + short index = (short)( 0x10 | page.getPage() ); + + short newAddress = (short)( address << 8 | 0x20 ); + + write( device, newAddress, index, data ); + + readDemodRegister( device, Page.TEN, (short)1, length ); + } + + protected static int readDemodRegister( UsbDevice device, + Page page, + short address, + int length ) + throws IllegalArgumentException, UsbDisconnectedException, UsbException + { + short index = page.getPage(); + short newAddress = (short)( ( address << 8 ) | 0x20 ); + byte[] empty = new byte[ 2 ]; + + byte[] data = read( device, newAddress, index, empty ); + + if( length == 2 ) + { + return (int)( ( data[ 1 ] << 8 | data[ 0 ] ) & 0xFFFF ); + } + else + { + return (int)( data[ 0 ] & 0xFF ); + } + } + + protected static void writeRegister( UsbDevice device, Block block, short address, + int value, int length ) + throws IllegalArgumentException, UsbDisconnectedException, UsbException + { + byte[] data; + + if( length == 1 ) + { + data = new byte[ 1 ]; + data[0] = (byte)( value & 0xFF ); + } + else if( length == 2 ) + { + data = new byte[ 2 ]; + data[1] = (byte)( value & 0xFF ); //LSB + data[0] = (byte)( ( value >> 8 ) & 0xFF ); //MSB + } + else + { + throw new IllegalArgumentException( "Cannot write value greater " + + "than 16 bits to the register - length [" + length + "]" ); + } + + write( device, block, address, data ); + } + + protected static int readRegister( UsbDevice device, Block block, short address, int length ) + throws IllegalArgumentException, UsbDisconnectedException, UsbException + { + byte[] empty = new byte[ 2 ]; + + byte[] data = read( device, block, address, empty ); + + if( length == 2 ) + { + return (int)( ( data[ 1 ] << 8 | data[ 0 ] ) & 0xFFFF ); + } + else + { + return (int)( data[ 0 ] & 0xFF ); + } + } + + /** + * Write array convenience method. Allow you to specify the Block that you + * are writing to. + * + * @param device + * @param block + * @param address + * @param data + * @throws IllegalArgumentException + * @throws UsbDisconnectedException + * @throws UsbException + */ + protected static void write( UsbDevice device, Block block, short address, byte[] data ) + throws IllegalArgumentException, UsbDisconnectedException, UsbException + { + write( device, address, block.getWriteIndex(), data ); + } + + /** + * Write byte array + * @param device - tuner + * @param value - + * @param index - start + * @param data - to write + * @throws IllegalArgumentException + * @throws UsbDisconnectedException + * @throws UsbException + */ + protected static void write( UsbDevice device, short value, short index, byte[] data ) + throws IllegalArgumentException, UsbDisconnectedException, UsbException + { + UsbControlIrp irp = + device.createUsbControlIrp( sREQUEST_TYPE_OUT, + sREQUEST_ZERO, + value, + index ); + + irp.setData( data ); + + device.syncSubmit( irp ); + } + + /** + * Reads byte array from index at the address. + * + * @return big-endian byte array (needs to be swapped to be usable) + * + */ + protected static byte[] read( UsbDevice device, Block block, short address, byte[] data ) + throws IllegalArgumentException, UsbDisconnectedException, UsbException + { + byte[] retVal; + + retVal = read( device, address, block.getReadIndex(), data ); + + return retVal; + } + + /** + * Reads the byte array from the index at the address with the length of the + * read determined by the size of the data array + * + * @return - big-endian byte array (needs to be swapped to be usable) + */ + protected static byte[] read( UsbDevice device, short address, short index, byte[] data ) + throws IllegalArgumentException, UsbDisconnectedException, UsbException + { + DefaultUsbControlIrp irp = + new DefaultUsbControlIrp( sREQUEST_TYPE_IN, + (byte)0, + address, + index ); + + irp.setData( data ); + + device.syncSubmit( irp ); + + return irp.getData(); + } + + protected static boolean isE4000Tuner( UsbDevice device, + boolean controlI2CRepeater ) throws UsbClaimException + { + try + { + int value = readI2CRegister( device, E4K_I2C_ADDRESS, + E4K_CHECK_ADDRESS, controlI2CRepeater ); + + return ( value == E4K_CHECK_VALUE ); + } + catch( UsbException e ) + { + //Do nothing ... it's not an E4000 + } + + return false; + } + + protected static boolean isFC0012Tuner( UsbDevice device, + boolean controlI2CRepeater ) throws UsbClaimException + { + try + { + /* Initialize the GPIOs */ + setGPIOOutput( device, BIT_5 ); + + /* Reset tuner before probing */ + setGPIOBit( device, BIT_5, true ); + setGPIOBit( device, BIT_5, false ); + + + int value = readI2CRegister( device, + FC0012_I2C_ADDRESS, + FC0012_CHECK_ADDRESS, + controlI2CRepeater ); + + return ( value == FC0012_CHECK_VALUE ); + } + catch( UsbException e ) + { + //Do nothing, it's not an FC0012 + } + + return false; + } + + protected static boolean isFC0013Tuner( UsbDevice device, + boolean controlI2CRepeater ) + throws UsbClaimException + { + try + { + int value = readI2CRegister( device, FC0013_I2C_ADDRESS, + FC0013_CHECK_ADDRESS, controlI2CRepeater ); + + return ( value == FC0013_CHECK_VALUE ); + } + catch( UsbException e ) + { + //Do nothing ... it's probably not an FC0013 + } + + return false; + } + + /** + * Determines if the device's tuner is an FC2580 Tuner. + * + * Note: call initBaseband(device) prior to calling this method. + * + * @param device - usb tuner device + * @return true if the tuner is an FC2580 + * @throws UsbException + * @throws UsbClaimException + */ + protected static boolean isFC2580Tuner( UsbDevice device, + boolean controlI2CRepeater ) throws UsbClaimException + { + try + { + /* Initialize the GPIOs */ + setGPIOOutput( device, BIT_5 ); + + /* Reset tuner before probing */ + setGPIOBit( device, BIT_5, true ); + setGPIOBit( device, BIT_5, false ); + + + /* Read the check value */ + int value = readI2CRegister( device, FC2580_I2C_ADDRESS, + FC2580_CHECK_ADDRESS, controlI2CRepeater ); + + /* Compare masked return value to the known check value */ + return ( ( value & 0x7F ) == FC2580_CHECK_VALUE ); + } + catch( UsbException e ) + { + //Do nothing, it's not an FC2580 + } + + return false; + } + + protected static boolean isR820TTuner( UsbDevice device, + boolean controlI2CRepeater ) throws UsbClaimException + { + try + { + int value = readI2CRegister( device, R820T_I2C_ADDRESS, + R820T_CHECK_ADDRESS, controlI2CRepeater ); + + return ( value == R820T_CHECK_VALUE ); + } + catch( UsbException e ) + { + //Do nothing, it's not an R820T + } + + return false; + } + + protected static boolean isR828DTuner( UsbDevice device, + boolean controlI2CRepeater ) throws UsbClaimException + { + try + { + int value = readI2CRegister( device, R828D_I2C_ADDRESS, + R828D_CHECK_ADDRESS, controlI2CRepeater ); + + return ( value == R828D_CHECK_VALUE ); + } + catch( UsbException e ) + { + //Do nothing, it's not an R828D + } + + return false; + } + + public int getCurrentSampleRate() throws SourceException + { + return mSampleRate.getRate(); + } + + public int getSampleRate() throws SourceException + { + try + { + int high= readDemodRegister( mUSBDevice, Page.ONE, (short)0x9F, 2 ); + int low = readDemodRegister( mUSBDevice, Page.ONE, (short)0xA1, 2 ); + + int ratio = Integer.rotateLeft( high, 16 ) | low; + + int rate = (int)( sOSCILLATOR_FREQUENCY * sTWO_TO_22_POWER / ratio ); + + SampleRate sampleRate = SampleRate.getClosest( rate ); + + /* If we're not currently set to this rate, set it as the current rate */ + if( sampleRate.getRate() != rate ) + { + setSampleRate( sampleRate ); + + return sampleRate.getRate(); + } + } + catch ( Exception e ) + { + throw new SourceException( "RTL2832 Tuner Controller - cannot get " + + "current sample rate", e ); + } + + return sDEFAULT_SAMPLE_RATE.getRate(); + } + + public void setSampleRate( SampleRate sampleRate ) + throws SourceException, UsbException + { + /* Write high-order 16-bits of sample rate ratio to demod register */ + writeDemodRegister( mUSBDevice, Page.ONE, (short)0x9F, + sampleRate.getRatioHighBits(), 2 ); + + /* Write low-order 16-bits of sample rate ratio to demod register */ + writeDemodRegister( mUSBDevice, Page.ONE, (short)0xA1, + sampleRate.getRatioLowBits(), 2 ); + + /* Set sample rate correction to 0 */ + setSampleRateFrequencyCorrection( 0 ); + + /* Reset the demod for the changes to take effect */ + writeDemodRegister( mUSBDevice, Page.ONE, (short)0x01, 0x14, 1 ); + writeDemodRegister( mUSBDevice, Page.ONE, (short)0x01, 0x10, 1 ); + + /* Apply any tuner specific sample rate filter settings */ + setSampleRateFilters( sampleRate.getRate() ); + + mSampleRate = sampleRate; + + mFrequencyController.setBandwidth( sampleRate.getRate() ); + } + + public void setSampleRateFrequencyCorrection( int ppm ) + throws SourceException, UsbException + { + int offset = -ppm * sTWO_TO_22_POWER / 1000000; + + writeDemodRegister( mUSBDevice, + Page.ONE, + (short)0x3F, + ( offset & 0xFF ), + 1 ); + writeDemodRegister( mUSBDevice, + Page.ONE, + (short)0x3E, + ( Integer.rotateRight( offset, 8 ) & 0xFF ), + 1 ); + /* Test to retune controller to apply frequency correction */ + mFrequencyController.setFrequency( mFrequencyController.getFrequency() ); + } + + public int getSampleRateFrequencyCorrection() throws UsbException + { + int high = readDemodRegister( mUSBDevice, Page.ONE, (short)0x3E, 1 ); + int low = readDemodRegister( mUSBDevice, Page.ONE, (short)0x3F, 1 ); + + return ( Integer.rotateLeft( high, 8 ) | low ); + } + + /** + * Returns contents of the 256-byte EEPROM. The contents are as follows: + * + * 256-byte EEPROM (in hex): + * 00/01 - 2832 Signature + * 03/02 - 0BDA Vendor ID + * 05/04 - 2832 Product ID + * 06 - A5 (has serial id?) + * 07 - 16 (bit field - bit 0 = remote wakeup, bit 1 = IR enabled + * 08 - 02 or 12 + * 10/09 0310 ETX(0x03) plus label length (includes length and ETX bytes) + * 12/11 First UTF-16 character + * 14/13 Second UTF-16 character ... + * + * Label 1: vendor + * Label 2: product + * Label 3: serial + * Label 4,5 ... (user defined) + */ + public byte[] readEEPROM( UsbDevice device, short offset, int length ) + throws IllegalArgumentException, UsbDisconnectedException, UsbException + { + if( offset + length > 256 ) + { + throw new IllegalArgumentException( "RTL2832 Tuner Controller - " + + "cannot read more than 256 bytes from EEPROM" ); + } + + byte[] data = new byte[ length ]; + byte[] onebyte = new byte[ 1 ]; + + /* Tell the RTL-2832 to address the EEPROM */ + writeRegister( device, Block.IIC, EEPROM_ADDRESS, (byte)offset, 1 ); + + for( int x = 0; x < length; x++ ) + { + byte[] temp = read( device, Block.IIC, EEPROM_ADDRESS, onebyte ); + data[ x ] = temp[ 0 ]; + } + + return data; + } + + /** + * Writes a single byte to the 256-byte EEPROM using the specified offset. + * + * Note: introduce a 5 millisecond delay between each successive write to + * the EEPROM or subsequent writes may fail. + */ + public void writeEEPROMByte( UsbDevice device, byte offset, byte value ) + throws IllegalArgumentException, UsbDisconnectedException, UsbException + { + if( offset < 0 || offset > 255 ) + { + throw new IllegalArgumentException( "RTL2832 Tuner Controller - " + + "EEPROM offset must be within range of 0 - 255" ); + } + + int offsetAndValue = Integer.rotateLeft( ( 0xFF & offset ), 8 ) | + ( 0xFF & value ); + + writeRegister( device, Block.IIC, EEPROM_ADDRESS, offsetAndValue, 2 ); + } + + public enum Address + { + USB_SYSCTL( 0x2000 ), + USB_CTRL( 0x2010 ), + USB_STAT( 0x2014 ), + USB_EPA_CFG( 0x2144 ), + USB_EPA_CTL( 0x2148 ), + USB_EPA_MAXPKT( 0x2158 ), + USB_EPA_MAXPKT_2( 0x215A ), + USB_EPA_FIFO_CFG( 0x2160 ), + DEMOD_CTL( 0x3000 ), + GPO( 0x3001 ), + GPI( 0x3002 ), + GPOE( 0x3003 ), + GPD( 0x3004 ), + SYSINTE( 0x3005 ), + SYSINTS( 0x3006 ), + GP_CFG0( 0x3007 ), + GP_CFG1( 0x3008 ), + SYSINTE_1( 0x3009 ), + SYSINTS_1( 0x300A ), + DEMOD_CTL_1( 0x300B ), + IR_SUSPEND( 0x300C ); + + private int mAddress; + + private Address( int address ) + { + mAddress = address; + } + + public short getAddress() + { + return (short)mAddress; + } + } + + public enum Page + { + ZERO( 0x0 ), + ONE( 0x1 ), + TEN( 0xA ); + + private int mPage; + + private Page( int page ) + { + mPage = page; + } + + public byte getPage() + { + return (byte)( mPage & 0xFF ); + } + } + + public enum SampleMode + { + QUADRATURE, DIRECT; + } + + public enum Block + { + DEMOD( 0 ), + USB( 1 ), + SYS( 2 ), + TUN( 3 ), + ROM( 4 ), + IR( 5 ), + IIC( 6 ); //I2C controller + + private int mValue; + + private Block( int value ) + { + mValue = value; + } + + public int getValue() + { + return mValue; + } + + /** + * Returns the value left shifted 8 bits + */ + public short getReadIndex() + { + return (short)Integer.rotateLeft( mValue, 8 ); + } + + public short getWriteIndex() + { + return (short)( getReadIndex() | 0x10 ); + } + } + + /** + * Sample rates supported by the RTL-2832. + * + * Formula to calculate the ratio value: + * + * ratio = ( ( crystal_frequency * 2^22 ) / sample_rate ) & ~3 + * + * Default crystal_frequency is 28,800,000 + * + * This produces a 32-bit value that has to be set in 2 x 16-bit registers. + * Place the high 16-bit value in ratioMSB and the low 16-bit value in + * ratioLSB. Use integer for these values to avoid sign-extension issues. + * + * Mask the value with 0xFFFF when setting the register. + */ + public enum SampleRate + { + /* Note: sample rates below 1.0MHz are subject to aliasing */ + RATE_0_240MHZ( 0x0DFC, 0x0000, 240000, "0.240 MHz" ), + RATE_0_288MHZ( 0x08FC, 0x0000, 288000, "0.288 MHz" ), + + RATE_0_912MHZ( 0x07E6, 0x0000, 912000, "0.912 MHz" ), + RATE_0_960MHZ( 0x0780, 0x0000, 960000, "0.960 MHz" ), + RATE_1_200MHZ( 0x05F4, 0x0000, 1200000, "1.200 MHz" ), + RATE_1_440MHZ( 0x0500, 0x0000, 1440000, "1.440 MHz" ), + RATE_1_680MHZ( 0x044A, 0x0000, 1680000, "1.680 MHz" ), + RATE_1_824MHZ( 0x03F3, 0x0000, 1824000, "1.824 MHz" ), + RATE_2_016MHZ( 0x038E, 0x0000, 2016000, "2.016 MHz" ); + + /* Note: usb4java library buffer processing scheme doesn't seem capable + * of supporting these rates */ + //RATE_2_400MHZ( 0x0300, 0x0000, 2400000, "2.400 MHz" ), + //RATE_2_880MHZ( 0x0280, 0x0000, 2880000, "2.880 MHz" ), + //RATE_3_072MHZ( 0x0258, 0x0000, 3072000, "3.072 MHz" ), + //RATE_3_216MHZ( 0x023D, 0x0000, 3216000, "3.216 MHz" ); + + private int mRatioHigh; + private int mRatioLow; + private int mRate; + private String mLabel; + + private SampleRate( int ratioHigh, int ratioLow, int rate, String label ) + { + mRatioHigh = ratioHigh; + mRatioLow = ratioLow; + mRate = rate; + mLabel = label; + } + + public int getRatioHighBits() + { + return mRatioHigh; + } + + public int getRatioLowBits() + { + return mRatioLow; + } + + public int getRate() + { + return mRate; + } + + public String getLabel() + { + return mLabel; + } + + public String toString() + { + return mLabel; + } + + /** + * Returns the sample rate that is equal to the argument or the next + * higher sample rate + * @param sampleRate + * @return + */ + public static SampleRate getClosest( int sampleRate ) + { + for( SampleRate rate: values () ) + { + if( rate.getRate() >= sampleRate ) + { + return rate; + } + } + + return sDEFAULT_SAMPLE_RATE; + } + } + + /** + * Adds a sample listener. If the buffer processing thread is + * not currently running, starts it running in a new thread. + */ + public void addListener( Listener listener ) + { + mSampleListeners.add( listener ); + + mBufferProcessor.start(); + } + + /** + * Removes the sample listener. If this is the last registered listener, + * shuts down the buffer processing thread. + */ + public void removeListener( Listener listener ) + { + mSampleListeners.remove( listener ); + + if( mSampleListeners.isEmpty() ) + { + mBufferProcessor.stop(); + } + } + + /** + * Dispatches float sample buffers to all registered listeners + */ + public void broadcast( Float[] samples ) + { + Iterator> it = mSampleListeners.iterator(); + + while( it.hasNext() ) + { + Listener next = it.next(); + + /* if this is the last (or only) listener, send him the original + * buffer, otherwise send him a copy of the buffer */ + if( it.hasNext() ) + { + next.receive( Arrays.copyOf( samples, samples.length ) ); + } + else + { + next.receive( samples ); + } + } + } + + /** + * Buffer processing thread. Fetches samples from the RTL2832 Tuner and + * dispatches them to all registered listeners + */ + public class BufferProcessor implements UsbPipeListener + { + private ScheduledExecutorService mExecutor = + Executors.newScheduledThreadPool( 2 ); + private ScheduledFuture mSampleDispatcherTask; + private ScheduledFuture mSampleRateCounterTask; + + private AtomicBoolean mRunning = new AtomicBoolean(); + private AtomicBoolean mResetting = new AtomicBoolean(); + private Boolean mTransitioning = false; + + public void start() + { + synchronized( mTransitioning ) + { + if( mRunning.compareAndSet( false, true ) ) + { + Log.debug( "RTL2832TunerController [" + mDescriptor.getSerial() + "] - starting sample fetch thread" ); + + ArrayList irps = new ArrayList(); + + for( int x = 0; x < sUSB_IRP_POOL_SIZE; x++ ) + { + UsbIrp irp = mUSBPipe.createUsbIrp(); + irp.setData( new byte[ sUSB_IRP_DATA_SIZE ] ); + irps.add( irp ); + } + + try + { + if( mUSBPipe.isActive() ) + { + if( !mUSBPipe.isOpen() ) + { + resetUSBBuffer(); + + mUSBPipe.open(); + + //TODO: move this to the constructor + mUSBPipe.addUsbPipeListener( this ); + } + + if( mUSBPipe.isOpen() ) + { + mSampleDispatcherTask = mExecutor + .scheduleAtFixedRate( new BufferDispatcher(), + 0, 20, TimeUnit.MILLISECONDS ); + + mSampleRateCounterTask = mExecutor + .scheduleAtFixedRate( new SampleRateMonitor(), + 60, 60, TimeUnit.SECONDS ); + + mUSBPipe.asyncSubmit( irps ); + } + } + } + catch ( Exception e ) + { + e.printStackTrace(); + } + } + } + } + + /** + * Stops the sample fetching thread + */ + public void stop() + { + synchronized( mTransitioning ) + { + if( mRunning.compareAndSet( true, false ) ) + { + Log.debug( "RTL2832TunerController - stopping sample fetch thread" ); + + mUSBPipe.abortAllSubmissions(); + + if( mSampleDispatcherTask != null ) + { + mSampleDispatcherTask.cancel( true ); + mSampleRateCounterTask.cancel( true ); + mRawSampleBuffer.clear(); + mSampleCounter.set( 0 ); + } + } + } + } + + /** + * Indicates if this thread is running + */ + public boolean isRunning() + { + return mRunning.get(); + } + + /** + * This method is invoked by the usb pipe when a response is returned + * from the data request. If there is byte sample data in the response + * then we will buffer it and allow the buffer processor to convert it + * and dispatch it to listeners. We automatically insert another + * irp to get more data. + * + * This method runs on the usb4java library processing thread, so we + * don't want to weigh it down. + */ + @Override + public synchronized void dataEventOccurred( UsbPipeDataEvent event ) + { + UsbIrp irp = event.getUsbIrp(); + + byte[] data = irp.getData(); + + if( data.length > 0 ) + { + mRawSampleBuffer.add( data ); + mSampleCounter.addAndGet( data.length ); + } + + if( mRunning.get() ) + { + try + { + mUSBPipe.asyncSubmit( new byte[ sUSB_IRP_DATA_SIZE ] ); + } + catch ( UsbNotActiveException | UsbNotOpenException + | IllegalArgumentException | UsbDisconnectedException + | UsbException e ) + { + Log.error( "RTL2832TunerController - error while " + + "processing samples - " + e.getLocalizedMessage() ); + } + } + } + + @Override + public void errorEventOccurred( UsbPipeErrorEvent e ) + { + /* When an error occurs, we'll get errors on all of the + * queued IRPS in succession. We use the mResetting to control + * the reset process and ignore all subsequent errors */ + if( mResetting.compareAndSet( false, true ) ) + { + Log.error( "RTL2832TunerController - error during data transfer - " + + "resetting USB pipe to RTL-2832 - " + + e.getUsbException().getLocalizedMessage() ); + + stop(); + + if( !mSampleListeners.isEmpty() ) + { + start(); + } + + mResetting.set( false ); + } + } + } + + /** + * Fetches byte[] chunks from the raw sample buffer. Converts each byte + * array and broadcasts the array to all registered listeners + */ + public class BufferDispatcher implements Runnable + { + @Override + public void run() + { + ArrayList buffers = new ArrayList(); + + mRawSampleBuffer.drainTo( buffers ); + + for( byte[] buffer: buffers ) + { + Float[] samples = mSampleAdapter.convert( buffer ); + broadcast( samples ); + } + } + } + + /** + * Averages the sample rate over a 10-second period. The count is for the + * number of bytes received from the tuner. There are two bytes for each + * sample. So, we divide by 20 to get the average sample rate. + */ + public class SampleRateMonitor implements Runnable + { + @Override + public void run() + { + int count = mSampleCounter.getAndSet( 0 ); + + double rate = (double)count / 120.0d; + + mSampleRateAverageSum += rate; + mSampleRateAverageCount++; + + double average = mSampleRateAverageSum / (double)mSampleRateAverageCount; + + StringBuilder sb = new StringBuilder(); + sb.append( TimeStamp.getTimeStamp( " " ) ); + sb.append( " RTL-2832 [" ); + if( mDescriptor != null ) + { + sb.append( mDescriptor.getSerial() ); + } + else + { + sb.append( "DESCRIPTOR IS NULL" ); + } + sb.append( "] Sample Rate - current: " ); + sb.append( mDecimalFormatter.format( rate ) ); + sb.append( "\taverage: " ); + sb.append( mDecimalFormatter.format( average ) ); + sb.append( " count:" ); + sb.append( mSampleRateAverageCount ); + + Log.info( sb.toString() ); + } + } + + /** + * RTL2832 EEPROM byte array descriptor parsing class + */ + public class Descriptor + { + private byte[] mData; + private ArrayList mLabels = new ArrayList(); + + public Descriptor( byte[] data ) + { + mData = data; + + getLabels(); + } + + public boolean isValid() + { + return mData[ 0 ] == (byte)0x28 && + mData[ 1 ] == (byte)0x32; + } + + public String getVendorID() + { + int id = Integer.rotateLeft( ( 0xFF & mData[ 3] ), 8 ) | + ( 0xFF & mData[ 2 ] ); + + return String.format("%04X", id ); + } + + public String getVendorLabel() + { + return mLabels.get( 0 ); + } + + public String getProductID() + { + int id = Integer.rotateLeft( ( 0xFF & mData[ 5] ), 8 ) | + ( 0xFF & mData[ 4 ] ); + + return String.format("%04X", id ); + } + + public String getProductLabel() + { + return mLabels.get( 1 ); + } + + public boolean hasSerial() + { + return mData[ 6 ] == (byte)0xA5; + } + + public String getSerial() + { + return mLabels.get( 2 ); + } + + public boolean remoteWakeupEnabled() + { + byte mask = (byte)0x01; + + return ( mData[ 7 ] & mask ) == mask; + } + + public boolean irEnabled() + { + byte mask = (byte)0x02; + + return ( mData[ 7 ] & mask ) == mask; + } + + private void getLabels() + { + mLabels.clear(); + + int start = 0x09; + + while( start < 256 ) + { + start = getLabel( start ); + } + } + + private int getLabel( int start ) + { + /* Validate length and check second byte for ETX (0x03) */ + if( start > 254 || mData[ start + 1 ] != (byte)0x03 ) + { + return 256; + } + + /* Get label length, including the length and ETX bytes */ + int length = 0xFF & mData[ start ]; + + if( start + length > 255 ) + { + return 256; + } + + /* Get the label bytes */ + byte[] data = Arrays.copyOfRange( mData, start + 2, start + length ); + + /* Translate the bytes as UTF-16 Little Endian and store the label */ + String label = new String( data, Charset.forName( "UTF-16LE" ) ); + + mLabels.add( label ); + + return start + length; + } + + public String toString() + { + StringBuilder sb = new StringBuilder(); + sb.append( "RTL-2832 EEPROM Descriptor\n" ); + + sb.append( "Vendor: " ); + sb.append( getVendorID() ); + sb.append( " [" ); + sb.append( getVendorLabel() ); + sb.append( "]\n" ); + + sb.append( "Product: " ); + sb.append( getProductID() ); + sb.append( " [" ); + sb.append( getProductLabel() ); + sb.append( "]\n" ); + + sb.append( "Serial: " ); + if( hasSerial() ) + { + sb.append( "yes [" ); + sb.append( getSerial() ); + sb.append( "]\n" ); + } + else + { + sb.append( "no\n" ); + } + + sb.append( "Remote Wakeup Enabled: " ); + sb.append( ( remoteWakeupEnabled() ? "yes" : "no" ) ); + sb.append( "\n" ); + + sb.append( "IR Enabled: " ); + sb.append( ( irEnabled() ? "yes" : "no" ) ); + sb.append( "\n" ); + + if( mLabels.size() > 3 ) + { + sb.append( "Additional Labels: " ); + + for( int x = 3; x < mLabels.size(); x++ ) + { + sb.append( " [" ); + sb.append( mLabels.get( x ) ); + sb.append( "\n" ); + } + } + + return sb.toString(); + } + } +} diff --git a/src/source/tuner/rtl/RTL2832TunerType.java b/src/source/tuner/rtl/RTL2832TunerType.java new file mode 100644 index 00000000..72233f32 --- /dev/null +++ b/src/source/tuner/rtl/RTL2832TunerType.java @@ -0,0 +1,28 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package source.tuner.rtl; + +public enum RTL2832TunerType +{ + E4000, + FC0012, + FC0013, + R820T, + R828D, + UNKNOWN; +} diff --git a/src/source/tuner/rtl/e4k/E4KTunerConfiguration.java b/src/source/tuner/rtl/e4k/E4KTunerConfiguration.java new file mode 100644 index 00000000..f2b4a1b6 --- /dev/null +++ b/src/source/tuner/rtl/e4k/E4KTunerConfiguration.java @@ -0,0 +1,124 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package source.tuner.rtl.e4k; + +import javax.xml.bind.annotation.XmlAttribute; + +import source.tuner.TunerConfiguration; +import source.tuner.TunerType; +import source.tuner.rtl.RTL2832TunerController.SampleRate; +import source.tuner.rtl.e4k.E4KTunerController.E4KEnhanceGain; +import source.tuner.rtl.e4k.E4KTunerController.E4KGain; +import source.tuner.rtl.e4k.E4KTunerController.E4KLNAGain; +import source.tuner.rtl.e4k.E4KTunerController.E4KMixerGain; + +public class E4KTunerConfiguration extends TunerConfiguration +{ + private E4KGain mMasterGain = E4KGain.AUTOMATIC; + private E4KMixerGain mMixerGain = E4KMixerGain.AUTOMATIC; + private E4KLNAGain mLNAGain = E4KLNAGain.AUTOMATIC; + private E4KEnhanceGain mEnhanceGain = E4KEnhanceGain.AUTOMATIC; + private double mFrequencyCorrection = 8.9d; + private SampleRate mSampleRate = SampleRate.RATE_1_200MHZ; + + /** + * Default constructor for JAXB + */ + public E4KTunerConfiguration() + { + this( "Default" ); + } + + public E4KTunerConfiguration( String name ) + { + super( name ); + } + + @Override + public TunerType getTunerType() + { + return TunerType.ELONICS_E4000; + } + + @XmlAttribute( name="master_gain" ) + public E4KGain getMasterGain() + { + return mMasterGain; + } + + public void setMasterGain( E4KGain gain ) + { + mMasterGain = gain; + } + + @XmlAttribute( name="mixer_gain" ) + public E4KMixerGain getMixerGain() + { + return mMixerGain; + } + + public void setMixerGain( E4KMixerGain mixerGain ) + { + mMixerGain = mixerGain; + } + + @XmlAttribute( name="lna_gain" ) + public E4KLNAGain getLNAGain() + { + return mLNAGain; + } + + public void setLNAGain( E4KLNAGain lnaGain ) + { + mLNAGain = lnaGain; + } + + @XmlAttribute( name="enhance_gain" ) + public E4KEnhanceGain getEnhanceGain() + { + return mEnhanceGain; + } + + public void setEnhanceGain( E4KEnhanceGain enhanceGain ) + { + mEnhanceGain = enhanceGain; + } + + @XmlAttribute( name="frequency_correction" ) + public double getFrequencyCorrection() + { + return mFrequencyCorrection; + } + + public void setFrequencyCorrection( double value ) + { + mFrequencyCorrection = value; + } + + @XmlAttribute( name="sample_rate" ) + public SampleRate getSampleRate() + { + return mSampleRate; + } + + public void setSampleRate( SampleRate sampleRate ) + { + mSampleRate = sampleRate; + } + +} diff --git a/src/source/tuner/rtl/e4k/E4KTunerConfigurationPanel.java b/src/source/tuner/rtl/e4k/E4KTunerConfigurationPanel.java new file mode 100644 index 00000000..bc593329 --- /dev/null +++ b/src/source/tuner/rtl/e4k/E4KTunerConfigurationPanel.java @@ -0,0 +1,688 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package source.tuner.rtl.e4k; + +import java.awt.EventQueue; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.awt.event.FocusEvent; +import java.awt.event.FocusListener; +import java.text.DecimalFormat; +import java.util.ArrayList; + +import javax.swing.BorderFactory; +import javax.swing.ComboBoxModel; +import javax.swing.DefaultComboBoxModel; +import javax.swing.JButton; +import javax.swing.JComboBox; +import javax.swing.JLabel; +import javax.swing.JOptionPane; +import javax.swing.JPanel; +import javax.swing.JSpinner; +import javax.swing.JTextField; +import javax.swing.SpinnerModel; +import javax.swing.SpinnerNumberModel; +import javax.swing.SwingConstants; +import javax.swing.event.ChangeEvent; +import javax.swing.event.ChangeListener; +import javax.usb.UsbException; + +import com.jidesoft.swing.JideTabbedPane; + +import log.Log; +import net.miginfocom.swing.MigLayout; +import source.SourceException; +import source.tuner.TunerConfiguration; +import source.tuner.TunerConfigurationAssignment; +import source.tuner.TunerType; +import source.tuner.rtl.RTL2832TunerController.SampleRate; +import source.tuner.rtl.e4k.E4KTunerController.E4KEnhanceGain; +import source.tuner.rtl.e4k.E4KTunerController.E4KGain; +import source.tuner.rtl.e4k.E4KTunerController.E4KLNAGain; +import source.tuner.rtl.e4k.E4KTunerController.E4KMixerGain; +import controller.ResourceManager; + +public class E4KTunerConfigurationPanel extends JPanel +{ + private static final long serialVersionUID = 1L; + private ResourceManager mResourceManager; + private E4KTunerController mController; + private E4KTunerConfiguration mSelectedConfig; + + private JButton mNewConfiguration; + private JButton mDeleteConfiguration; + private JComboBox mComboConfigurations; + + private JTextField mName; + + private JSpinner mFrequencyCorrection; + private JSpinner mSampleRateCorrection; + private JComboBox mComboMasterGain; + private JComboBox mComboMixerGain; + private JComboBox mComboLNAGain; + private JComboBox mComboEnhanceGain; + + private JComboBox mComboSampleRate; + + public E4KTunerConfigurationPanel( ResourceManager resourceManager, + E4KTunerController controller ) + { + mResourceManager = resourceManager; + mController = controller; + + init(); + } + + /** + * Initializes the gui components + */ + private void init() + { + setLayout( new MigLayout( "fill,wrap 2", "[grow,right][grow]", "[][][][][][][grow]" ) ); + + /** + * Tuner configuration selector + */ + mComboConfigurations = new JComboBox(); + + mComboConfigurations.setModel( getModel() ); + + /* Determine which tuner configuration should be selected/displayed */ + TunerConfigurationAssignment savedConfig = mResourceManager + .getSettingsManager().getSelectedTunerConfiguration( + TunerType.ELONICS_E4000, mController.getUniqueID() ); + + if( savedConfig != null ) + { + mSelectedConfig = getNamedConfiguration( + savedConfig.getTunerConfigurationName() ); + } + + /* If we couldn't determine the saved/selected config, use the first one */ + if( mSelectedConfig == null ) + { + mSelectedConfig = mComboConfigurations.getItemAt( 0 ); + + //Store this config as the default for this tuner at this address + mResourceManager.getSettingsManager() + .setSelectedTunerConfiguration( + TunerType.ELONICS_E4000, + mController.getUniqueID(), mSelectedConfig ); + } + + mComboConfigurations.setSelectedItem( mSelectedConfig ); + + mComboConfigurations.addActionListener( new ActionListener() + { + @Override + public void actionPerformed( ActionEvent e ) + { + E4KTunerConfiguration selected = + (E4KTunerConfiguration)mComboConfigurations + .getItemAt( mComboConfigurations.getSelectedIndex() ); + + if( selected != null ) + { + update( selected ); + } + } + }); + + add( new JLabel( "Config:" ) ); + add( mComboConfigurations, "growx,push" ); + + /** + * Tuner Configuration Name + */ + add( new JLabel( "Name:" ) ); + mName = new JTextField(); + mName.setText( mSelectedConfig.getName() ); + + mName.addFocusListener( new FocusListener() + { + @Override + public void focusLost( FocusEvent e ) + { + /* Update the config name */ + mSelectedConfig.setName( mName.getText() ); + + /* Reset this named config as the assigned config for this + * tuner */ + mResourceManager.getSettingsManager() + .setSelectedTunerConfiguration( + TunerType.ELONICS_E4000, + mController.getUniqueID(), mSelectedConfig ); + + save(); + } + @Override + public void focusGained( FocusEvent e ) {} + } ); + + add( mName, "growx,push" ); + + /** + * Frequency Correction + */ + SpinnerModel model = + new SpinnerNumberModel( 0.0, //initial value + -1000.0, //min + 1000.0, //max + 0.1 ); //step + + mFrequencyCorrection = new JSpinner( model ); + + JSpinner.NumberEditor editor = + (JSpinner.NumberEditor)mFrequencyCorrection.getEditor(); + + DecimalFormat format = editor.getFormat(); + format.setMinimumFractionDigits( 1 ); + editor.getTextField().setHorizontalAlignment( SwingConstants.CENTER ); + + mFrequencyCorrection.setValue( mSelectedConfig.getFrequencyCorrection() ); + + mFrequencyCorrection.addChangeListener( new ChangeListener() + { + @Override + public void stateChanged( ChangeEvent e ) + { + final double value = ((SpinnerNumberModel)mFrequencyCorrection + .getModel()).getNumber().doubleValue(); + + EventQueue.invokeLater( new Runnable() + { + @Override + public void run() + { + try + { + mController.setFrequencyCorrection( value ); + mSelectedConfig.setFrequencyCorrection( value ); + save(); + } + catch ( SourceException e1 ) + { + JOptionPane.showMessageDialog( + E4KTunerConfigurationPanel.this, + "E4K Tuner Controller - couldn't apply " + + "frequency correction value: " + value + + e1.getLocalizedMessage() ); + + Log.error( "E4K Tuner Controller - couldn't apply " + + "frequency correction value: " + value + + e1.getLocalizedMessage() ); + } + } + } ); + } + } ); + + add( new JLabel( "Correction PPM:" ) ); + add( mFrequencyCorrection, "growx,push" ); + + /** + * Sample Rate + */ + mComboSampleRate = new JComboBox<>( SampleRate.values() ); + mComboSampleRate.setSelectedItem( mSelectedConfig.getSampleRate() ); + mComboSampleRate.addActionListener( new ActionListener() + { + @Override + public void actionPerformed( ActionEvent e ) + { + SampleRate sampleRate = + (SampleRate)mComboSampleRate.getSelectedItem(); + try + { + + mController.setSampleRate( sampleRate ); + + mSelectedConfig.setSampleRate( sampleRate ); + save(); + } + catch ( SourceException | UsbException eSampleRate ) + { + JOptionPane.showMessageDialog( + E4KTunerConfigurationPanel.this, + "E4K Tuner Controller - couldn't apply the sample " + + "rate setting [" + sampleRate.getLabel() + "] " + + eSampleRate.getLocalizedMessage() ); + + Log.error( "E4K Tuner Controller - couldn't apply sample " + + "rate setting [" + sampleRate.getLabel() + "] " + + eSampleRate.getLocalizedMessage() ); + } + } + } ); + add( new JLabel( "Sample Rate:" ) ); + add( mComboSampleRate, "growx,push" ); + +// /** +// * Sample Rate Correction +// */ +// SpinnerModel sampleRateCorrectionModel = +// new SpinnerNumberModel( 0, //initial value +// -32768, //min +// 32767, //max +// 1 ); //step +// +// mSampleRateCorrection = new JSpinner( sampleRateCorrectionModel ); +// +// JSpinner.NumberEditor sampleRateCorrectionEditor = +// (JSpinner.NumberEditor)mSampleRateCorrection.getEditor(); +// +// DecimalFormat sampleRateCorrectionFormat = +// sampleRateCorrectionEditor.getFormat(); +// +// sampleRateCorrectionFormat.setMinimumFractionDigits( 0 ); +// +// sampleRateCorrectionEditor.getTextField() +// .setHorizontalAlignment( SwingConstants.CENTER ); +// +// mSampleRateCorrection.addChangeListener( new ChangeListener() +// { +// @Override +// public void stateChanged( ChangeEvent e ) +// { +// final int value = ((SpinnerNumberModel)mSampleRateCorrection +// .getModel()).getNumber().intValue(); +// +// EventQueue.invokeLater( new Runnable() +// { +// @Override +// public void run() +// { +// try +// { +// mController.setSampleRateFrequencyCorrection( value ); +// } +// catch ( SourceException | UsbException e1 ) +// { +// JOptionPane.showMessageDialog( +// E4KTunerConfigurationPanel.this, +// "E4K Tuner Controller - couldn't apply " +// + "sample rate correction value: " + value + +// e1.getLocalizedMessage() ); +// +// Log.error( "E4K Tuner Controller - couldn't apply " +// + "sample rate correction value: " + value + +// e1.getLocalizedMessage() ); +// } +// } +// } ); +// } +// } ); +// +// add( mSampleRateCorrection ); +// add( new JLabel( "Sample Rate Correction (ppm)" ), "wrap" ); + + /** + * Gain Controls + */ + JPanel gainPanel = new JPanel(); + gainPanel.setLayout( new MigLayout( "fill", "[right][left][right][left]", "" ) ); + gainPanel.setBorder( BorderFactory.createTitledBorder( "Gain" ) ); + + /* Master Gain Control */ + mComboMasterGain = new JComboBox( E4KGain.values() ); + mComboMasterGain.setSelectedItem( mSelectedConfig.getMasterGain() ); + + mComboMasterGain.addActionListener( new ActionListener() + { + @Override + public void actionPerformed( ActionEvent arg0 ) + { + try + { + E4KGain gain = (E4KGain)mComboMasterGain.getSelectedItem(); + + mController.setGain( (E4KGain)mComboMasterGain.getSelectedItem(), true ); + + if( gain == E4KGain.MANUAL ) + { + mComboMixerGain.setSelectedItem( + mController.getMixerGain( true ) ); + mComboMixerGain.setEnabled( true ); + + mComboLNAGain.setSelectedItem( + mController.getLNAGain( true ) ); + mComboLNAGain.setEnabled( true ); + + mComboEnhanceGain.setEnabled( true ); + mComboEnhanceGain.setSelectedItem( + mController.getEnhanceGain( true ) ); + } + else + { + mComboMixerGain.setEnabled( false ); + mComboMixerGain.setSelectedItem( gain.getMixerGain() ); + + mComboLNAGain.setEnabled( false ); + mComboLNAGain.setSelectedItem( gain.getLNAGain() ); + + mComboEnhanceGain.setEnabled( false ); + mComboEnhanceGain.setSelectedItem( gain.getEnhanceGain() ); + } + + mSelectedConfig.setMasterGain( gain ); + save(); + } + catch ( UsbException e ) + { + JOptionPane.showMessageDialog( + E4KTunerConfigurationPanel.this, + "E4K Tuner Controller - couldn't apply the gain " + + "setting - " + e.getLocalizedMessage() ); + + Log.error( "E4K Tuner Controller - couldn't apply " + + "gain setting - " + e.getLocalizedMessage() ); + } + } + } ); + mComboMasterGain.setToolTipText( "Select AUTOMATIC for auto " + + "gain, MANUAL to enable
independent control of " + + "Mixer, LNA and Enhance gain
" + + "settings, or one of the individual gain settings for
" + + "semi-manual gain control" ); + gainPanel.add( new JLabel( "Master" ) ); + gainPanel.add( mComboMasterGain ); + + /* Mixer Gain Control */ + mComboMixerGain = new JComboBox( E4KMixerGain.values() ); + mComboMixerGain.setSelectedItem( mSelectedConfig.getMixerGain() ); + mComboMixerGain.addActionListener( new ActionListener() + { + @Override + public void actionPerformed( ActionEvent arg0 ) + { + EventQueue.invokeLater( new Runnable() + { + @Override + public void run() + { + try + { + E4KMixerGain mixerGain = + (E4KMixerGain)mComboMixerGain.getSelectedItem(); + + if( mComboMixerGain.isEnabled() ) + { + mController.setMixerGain( mixerGain, true ); + } + + mSelectedConfig.setMixerGain( mixerGain ); + save(); + } + catch ( UsbException e ) + { + JOptionPane.showMessageDialog( + E4KTunerConfigurationPanel.this, + "E4K Tuner Controller - couldn't apply the mixer " + + "gain setting - " + e.getLocalizedMessage() ); + + Log.error( "E4K Tuner Controller - couldn't apply mixer " + + "gain setting - " + e.getLocalizedMessage() ); + } + } + } ); + } + } ); + mComboMixerGain.setToolTipText( "Mixer Gain. Set master gain " + + "to MASTER to enable adjustment" ); + mComboMixerGain.setEnabled( false ); + gainPanel.add( new JLabel( "Mixer" ) ); + gainPanel.add( mComboMixerGain, "wrap" ); + + /* LNA Gain Control */ + mComboLNAGain = new JComboBox( E4KLNAGain.values() ); + mComboLNAGain.setSelectedItem( mSelectedConfig.getLNAGain() ); + mComboLNAGain.addActionListener( new ActionListener() + { + @Override + public void actionPerformed( ActionEvent arg0 ) + { + try + { + E4KLNAGain lnaGain = (E4KLNAGain)mComboLNAGain.getSelectedItem(); + + if( mComboLNAGain.isEnabled() ) + { + mController.setLNAGain( lnaGain, true ); + } + + mSelectedConfig.setLNAGain( lnaGain ); + save(); + } + catch ( UsbException e ) + { + JOptionPane.showMessageDialog( + E4KTunerConfigurationPanel.this, + "E4K Tuner Controller - couldn't apply the LNA " + + "gain setting - " + e.getLocalizedMessage() ); + + Log.error( "E4K Tuner Controller - couldn't apply LNA " + + "gain setting - " + e.getLocalizedMessage() ); + } + } + } ); + mComboLNAGain.setToolTipText( "LNA Gain. Set master gain " + + "to MANUAL to enable adjustment" ); + mComboLNAGain.setEnabled( false ); + gainPanel.add( new JLabel( "LNA" ) ); + gainPanel.add( mComboLNAGain ); + + /* Enhance Gain Control */ + mComboEnhanceGain = new JComboBox( E4KEnhanceGain.values() ); + mComboEnhanceGain.setSelectedItem( mSelectedConfig.getEnhanceGain() ); + mComboEnhanceGain.addActionListener( new ActionListener() + { + @Override + public void actionPerformed( ActionEvent arg0 ) + { + try + { + E4KEnhanceGain enhanceGain = + (E4KEnhanceGain)mComboEnhanceGain.getSelectedItem(); + + if( mComboEnhanceGain.isEnabled() ) + { + mController.setEnhanceGain( enhanceGain, true ); + } + + mSelectedConfig.setEnhanceGain( enhanceGain ); + save(); + } + catch ( UsbException e ) + { + JOptionPane.showMessageDialog( + E4KTunerConfigurationPanel.this, + "E4K Tuner Controller - couldn't apply the enhance " + + "gain setting - " + e.getLocalizedMessage() ); + + Log.error( "E4K Tuner Controller - couldn't apply enhance " + + "gain setting - " + e.getLocalizedMessage() ); + } + } + } ); + mComboEnhanceGain.setToolTipText( "Enhance Gain. Set master gain " + + "to MANUAL to enable adjustment" ); + mComboEnhanceGain.setEnabled( false ); + gainPanel.add( new JLabel( "Enhance" ) ); + gainPanel.add( mComboEnhanceGain, "wrap" ); + + add( gainPanel, "span,grow,push" ); + + /** + * Create a new configuration + */ + mNewConfiguration = new JButton( "New" ); + + mNewConfiguration.addActionListener( new ActionListener() + { + @Override + public void actionPerformed( ActionEvent e ) + { + TunerConfiguration config = + mResourceManager.getSettingsManager() + .addNewTunerConfiguration( + TunerType.ELONICS_E4000, + "New Configuration" ); + + mComboConfigurations.setModel( getModel() ); + + mComboConfigurations.setSelectedItem( config ); + + repaint(); + } + } ); + + add( mNewConfiguration, "growx,push" ); + + /** + * Delete the currently selected configuration + */ + mDeleteConfiguration = new JButton( "Delete" ); + + mDeleteConfiguration.addActionListener( new ActionListener() + { + @Override + public void actionPerformed( ActionEvent e ) + { + E4KTunerConfiguration selected = + (E4KTunerConfiguration)mComboConfigurations + .getItemAt( mComboConfigurations.getSelectedIndex() ); + + if( selected != null ) + { + int n = JOptionPane.showConfirmDialog( + E4KTunerConfigurationPanel.this, + "Are you sure you want to delete '" + + selected.getName() + "'?", + "Are you sure?", + JOptionPane.YES_NO_OPTION ); + + if( n == JOptionPane.YES_OPTION ) + { + mResourceManager.getSettingsManager() + .deleteTunerConfiguration( selected ); + + mComboConfigurations.setModel( getModel() ); + + mComboConfigurations.setSelectedIndex( 0 ); + + repaint(); + } + } + } + } ); + + add( mDeleteConfiguration, "growx,push,wrap" ); + } + + /** + * Updates gui controls with the values from the tuner configuration + * @param config - tuner configuration + */ + private void update( E4KTunerConfiguration config ) + { + mSelectedConfig = config; + + try + { + mController.apply( config ); + + mName.setText( config.getName() ); + mFrequencyCorrection.setValue( config.getFrequencyCorrection() ); + + mComboMixerGain.setSelectedItem( mSelectedConfig.getMixerGain() ); + mComboLNAGain.setSelectedItem( mSelectedConfig.getLNAGain() ); + mComboEnhanceGain.setSelectedItem( mSelectedConfig.getEnhanceGain() ); + + /* Apply master gain last so that the Mixer, LNA, and Enhance gain + * settings are updated with the master setting where necessary */ + mComboMasterGain.setSelectedItem( mSelectedConfig.getMasterGain() ); + + mComboSampleRate.setSelectedItem( mSelectedConfig.getSampleRate() ); + + mResourceManager.getSettingsManager().setSelectedTunerConfiguration( + TunerType.ELONICS_E4000, mController.getUniqueID(), config ); + } + catch ( SourceException e1 ) + { + JOptionPane.showMessageDialog( + E4KTunerConfigurationPanel.this, + "E4K Tuner Controller - couldn't " + + "apply the tuner configuration settings - " + + e1.getLocalizedMessage() ); + + Log.error( "E4K Tuner Controller - couldn't apply " + + "config [" + config.getName() + "] - " + + e1.getLocalizedMessage() ); + } + } + + /** + * Constructs a combo box model for the tuner configuration combo component + * by assembling a list of the tuner configurations appropriate for this + * specific tuner + */ + private ComboBoxModel getModel() + { + ArrayList configs = + mResourceManager.getSettingsManager() + .getTunerConfigurations( TunerType.ELONICS_E4000 ); + + DefaultComboBoxModel model = + new DefaultComboBoxModel(); + + for( TunerConfiguration config: configs ) + { + model.addElement( (E4KTunerConfiguration)config ); + } + + return model; + } + + /** + * Retrieves a specific (named) tuner configuration + */ + private E4KTunerConfiguration getNamedConfiguration( String name ) + { + ArrayList configs = + mResourceManager.getSettingsManager() + .getTunerConfigurations( TunerType.ELONICS_E4000 ); + + for( TunerConfiguration config: configs ) + { + if( config.getName().contentEquals( name ) ) + { + return (E4KTunerConfiguration)config; + } + } + + return null; + } + + /** + * Saves current tuner configuration settings + */ + private void save() + { + mResourceManager.getSettingsManager().save(); + } +} diff --git a/src/source/tuner/rtl/e4k/E4KTunerController.java b/src/source/tuner/rtl/e4k/E4KTunerController.java new file mode 100644 index 00000000..47c4e9b9 --- /dev/null +++ b/src/source/tuner/rtl/e4k/E4KTunerController.java @@ -0,0 +1,2452 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package source.tuner.rtl.e4k; + +import javax.swing.JPanel; +import javax.usb.UsbException; + +import source.SourceException; +import source.tuner.TunerConfiguration; +import source.tuner.TunerType; +import source.tuner.rtl.RTL2832TunerController; +import source.tuner.usb.USBTunerDevice; +import controller.ResourceManager; + +public class E4KTunerController extends RTL2832TunerController +{ + /** + * Note: E4K tuner max frequency range is 2200 MHz, but the + * max value that can be assigned to a signed integer is + * 2147xxxxxx, so we constrain the E4K to the max integer value, for now + * + * TODO: change frequency to a long primitive type + */ + public static final int sMIN_FREQUENCY = 52000000; + public static final int sMIN_FREQUENCY_EXTENDED = 52000000; + public static final int sMAX_FREQUENCY = 2147000000; + public static final int sMAX_FREQUENCY_EXTENDED = 2147000000; + public static final long sMIN_VCO = 2600000000l; //2.6 GHz + public static final long sMAX_VCO = 3900000000l; //3.9 GHz + + /* The local oscillator is defined by whole (integer) units of the oscillator + * frequency and fractional units representing 1/65536th of the oscillator + * frequency, meaning we can only tune the local oscillator in units of + * 439.453125 hertz. */ + public static final long sE4K_PLL_Y = 65536l; /* 16-bit fractional register */ + public static final byte MASTER1_RESET = (byte)0x01; + public static final byte MASTER1_NORM_STBY = (byte)0x02; + public static final byte MASTER1_POR_DET = (byte)0x04; + + public static final byte SYNTH1_PLL_LOCK = (byte)0x01; + public static final byte SYNTH1_BAND_SHIF = (byte)0x01; + + public static final byte SYNTH7_3PHASE_EN = (byte)0x08; + + public static final byte SYNTH8_VCOCAL_UPD = (byte)0x04; + + public static final byte FILT3_MASK = (byte)0x20; + public static final byte FILT3_ENABLE = (byte)0x00; + public static final byte FILT3_DISABLE = (byte)0x20; + + public static final byte MIXER_FILTER_MASK = (byte)0xF0; + public static final byte IF_CHANNEL_FILTER_MASK = (byte)0x1F; + public static final byte IF_RC_FILTER_MASK = (byte)0x0F; + + + public static final byte AGC1_LIN_MODE = (byte)0x10; + public static final byte AGC1_LNA_UPDATE = (byte)0x20; + public static final byte AGC1_LNA_G_LOW = (byte)0x40; + public static final byte AGC1_LNA_G_HIGH = (byte)0x80; + public static final byte AGC1_MOD_MASK = (byte)0xF; + + public static final byte GAIN1_MOD_MASK = (byte)0xF; + public static final byte IF_GAIN_MODE_SWITCHING_MASK = (byte)0x1; + + public static final byte AGC6_LNA_CAL_REQ = (byte)0x10; + + public static final byte AGC7_MIXER_GAIN_MASK = (byte)0x01; + public static final byte AGC7_MIX_GAIN_MANUAL = (byte)0x00; + public static final byte AGC7_MIX_GAIN_AUTO = (byte)0x01; + public static final byte AGC7_GAIN_STEP_5DB = (byte)0x20; + + public static final byte AGC8_SENS_LIN_AUTO = (byte)0x01; + + public static final byte AGC11_LNA_GAIN_ENH = (byte)0x01; + public static final byte ENH_GAIN_MOD_MASK = (byte)0x07; + public static final byte MIXER_GAIN_MASK = (byte)0x01; + + public static final byte DC1_CAL_REQ = (byte)0x01; + + public static final byte DC5_I_LUT_EN = (byte)0x01; + public static final byte DC5_Q_LUT_EN = (byte)0x02; + public static final byte DC5_RANGE_DETECTOR_ENABLED_MASK = (byte)0x04; //DC Offset Detector Enabled + public static final byte DC5_RANGE_DETECTOR_ENABLED = (byte)0x04; //DC Offset Detector Enabled + public static final byte DC5_RANGE_EN = (byte)0x08; + public static final byte DC5_RANGE_DETECTOR_DISABLED_MASK = (byte)0x03; + public static final byte DC5_RANGE_DETECTOR_DISABLED = (byte)0x00; + + public static final byte DC5_TIMEVAR_EN = (byte)0x10; + + public static final byte CLKOUT_DISABLE = (byte)0x96; + + public static final byte CHFCALIB_CMD = (byte)0x01; + + private E4KTunerEditorPanel mEditor; + + public E4KTunerController( USBTunerDevice device ) throws SourceException + { + super( device, sMIN_FREQUENCY, sMAX_FREQUENCY ); + } + + @Override + public TunerType getTunerType() + { + return TunerType.ELONICS_E4000; + } + + @Override + public void apply( TunerConfiguration config ) throws SourceException + { + if( config != null && config instanceof E4KTunerConfiguration ) + { + E4KTunerConfiguration e4kConfig = (E4KTunerConfiguration)config; + + try + { + SampleRate sampleRate = e4kConfig.getSampleRate(); + setSampleRate( sampleRate ); + + double correction = e4kConfig.getFrequencyCorrection(); + setFrequencyCorrection( correction ); + + E4KGain masterGain = e4kConfig.getMasterGain(); + setGain( masterGain, true ); + + if( masterGain == E4KGain.MANUAL ) + { + E4KMixerGain mixerGain = e4kConfig.getMixerGain(); + setMixerGain( mixerGain, true ); + + E4KLNAGain lnaGain = e4kConfig.getLNAGain(); + setLNAGain( lnaGain, true ); + + E4KEnhanceGain enhanceGain = e4kConfig.getEnhanceGain(); + setEnhanceGain( enhanceGain, true ); + } + } + catch( UsbException e ) + { + throw new SourceException( "E4KTunerController - usb error " + + "while applying tuner config", e ); + } + } + else + { + throw new IllegalArgumentException( "E4KTunerController - cannot " + + "apply tuner configuration of type:[" + + config.getClass() + "]" ); + } + + } + + public void init() throws SourceException + { + try + { + /* Dummy write to test USB interface */ + writeRegister( mUSBDevice, Block.USB, + Address.USB_SYSCTL.getAddress(), (short)0x09, 1 ); + + initBaseband( mUSBDevice ); + + enableI2CRepeater( mUSBDevice, true ); + + boolean i2CRepeaterControl = false; + + initTuner( i2CRepeaterControl ); + + enableI2CRepeater( mUSBDevice, false ); + + /* Initialize the super class */ + super.init(); + } + catch( UsbException e ) + { + e.printStackTrace(); + throw new SourceException( "E4K Tuner Controller - error during " + + "init()", e ); + } + } + + public JPanel getEditor( ResourceManager resourceManager ) + { + if( mEditor == null ) + { + mEditor = new E4KTunerEditorPanel( this, resourceManager ); + } + + return mEditor; + } + + /** + * Sets the IF filters (mixer, channel and RC) to the correct filter setting + * for the selected bandwidth/sample rate + */ + @Override + public void setSampleRateFilters( int bandwidth ) throws UsbException + { + /* Determine repeater state so we can restore it when done */ + boolean i2CRepeaterEnabled = isI2CRepeaterEnabled(); + + if( !i2CRepeaterEnabled ) + { + enableI2CRepeater( mUSBDevice, true ); + } + + boolean controlI2CRepeater = false; + + MixerFilter mixer = MixerFilter.getFilter( bandwidth ); + setMixerFilter( mixer, controlI2CRepeater ); + + ChannelFilter channel = ChannelFilter.getFilter( bandwidth ); + setChannelFilter( channel, controlI2CRepeater ); + + RCFilter rc = RCFilter.getFilter( bandwidth ); + setRCFilter( rc, controlI2CRepeater ); + + if( !i2CRepeaterEnabled ) + { + enableI2CRepeater( mUSBDevice, false ); + } + } + + @Override + public int getTunedFrequency() throws SourceException + { + try + { + /* Determine repeater state so we can restore it when done */ + boolean i2CRepeaterEnabled = isI2CRepeaterEnabled(); + + if( !i2CRepeaterEnabled ) + { + enableI2CRepeater( mUSBDevice, true ); + } + + boolean controlI2CRepeater = false; + + byte z = (byte)readE4KRegister( Register.SYNTH3, controlI2CRepeater ); + + int xHigh = readE4KRegister( Register.SYNTH4, controlI2CRepeater ); + int xLow = readE4KRegister( Register.SYNTH5, controlI2CRepeater ); + + int x = ( Integer.rotateLeft( xHigh, 8 ) ) | xLow; + + int pllSetting = readE4KRegister( Register.SYNTH7, controlI2CRepeater ); + PLL pll = PLL.fromSetting( pllSetting ); + + /* Return the repeater to its previous state */ + if( !i2CRepeaterEnabled ) + { + enableI2CRepeater( mUSBDevice, false ); + } + + return calculateActualFrequency( pll, z, x ); + } + catch( UsbException e ) + { + throw new SourceException( "E4K tuner controller - couldn't get " + + "tuned frequency", e ); + } + } + + @Override + public void setTunedFrequency( int frequency ) throws SourceException + { + /* Get the phase locked loop setting */ + PLL pll = PLL.fromFrequency( frequency ); + + /* Z is an integer representing the number of scaled oscillator frequency + * increments the multiplied frequency. */ + byte z = (byte)( frequency / pll.getScaledOscillator() ); + + /* remainder is just as it describes. It is what is left over after we + * carve out scaled oscillator frequency increments (z) from the desired + * frequency. */ + int remainder = frequency - ( ( z & 0xFF ) * pll.getScaledOscillator() ); + + /* X is a 16-bit representation of the remainder */ + int x = (int)( (double)remainder / (double)pll.getScaledOscillator() * sE4K_PLL_Y ); + + /* Calculate the exact (tunable) frequency and apply that to the tuner */ + int actualFrequency = calculateActualFrequency( pll, z, x ); + + /** + * Hack: if we're trying to set the minimum frequency for the E4K, due + * to rounding errors, 52 mhz becomes 51.999993, so we need to adjust + * x to get 52.000003 mhz ... otherwise the PLL won't lock on the freq + */ + if( actualFrequency < getMinFrequency() ) + { + x++; + actualFrequency = calculateActualFrequency( pll, z, x ); + } + + /* Apply the actual frequency */ + try + { + enableI2CRepeater( mUSBDevice, true ); + + boolean controlI2CRepeater = false; + + /* Write the PLL setting */ + writeE4KRegister( Register.SYNTH7, pll.getIndex(), controlI2CRepeater ); + + /* Write z (integral) value */ + writeE4KRegister( Register.SYNTH3, z, controlI2CRepeater ); + + /* Write the x (fractional) value high-order byte to synth4 register */ + writeE4KRegister( Register.SYNTH4, (byte)( x & 0xFF ), controlI2CRepeater ); + + /* Write the x (fractional) value low-order byte to synth5 register */ + writeE4KRegister( Register.SYNTH5, + (byte)( ( Integer.rotateRight( x, 8 ) ) & 0xFF ), + controlI2CRepeater ); + + /* Set the band for the new frequency */ + setBand( actualFrequency, controlI2CRepeater ); + + /* Set the filter */ + setRFFilter( actualFrequency, controlI2CRepeater ); + + /* Check for PLL lock */ + int lock = readE4KRegister( Register.SYNTH1, controlI2CRepeater ); + + if( !( ( lock & 0x1 ) == 0x1 ) ) + { + throw new SourceException( "E4K tuner controller - couldn't " + + "achieve PLL lock for frequency [" + + actualFrequency + "]" ); + } + + enableI2CRepeater( mUSBDevice, false ); + } + catch( UsbException e ) + { + throw new SourceException( "E4K tuner controller - error tuning " + + "frequency [" + frequency + "]", e ); + } + } + + private int calculateActualFrequency( PLL pll, byte z, int x ) + { + int whole = (int)( pll.getScaledOscillator() * ( z & 0xFF ) ); + + int fractional = (int)( pll.getScaledOscillator() * + ( (double)x / (double)sE4K_PLL_Y ) ); + + return whole + fractional; + } + + public void initTuner( boolean controlI2CRepeater ) throws UsbException + { + if( controlI2CRepeater ) + { + enableI2CRepeater( mUSBDevice, true ); + } + + boolean i2CRepeaterControl = false; + + /* Perform dummy read */ + readE4KRegister( Register.DUMMY, i2CRepeaterControl ); + + /* Reset everything and clear POR indicator */ + /* NOTE: register value remains 0010 even after we write 0111 to it */ + writeE4KRegister( Register.MASTER1, (byte)( MASTER1_RESET | + MASTER1_NORM_STBY | + MASTER1_POR_DET ), + i2CRepeaterControl ); + + /* Configure clock input */ + writeE4KRegister( Register.CLK_INP, (byte)0x00, i2CRepeaterControl ); + + /* Disable clock output */ + writeE4KRegister( Register.REF_CLK, (byte)0x00, i2CRepeaterControl ); + writeE4KRegister( Register.CLKOUT_PWDN, (byte)0x96, i2CRepeaterControl ); + + /* Magic Init */ + magicInit( i2CRepeaterControl ); + +// /* Set common mode voltage a bit higher for more margin - 850 mv */ +// writeMaskedE4KRegister( Register.DC7, (byte)0x7, +// (byte)0x4, i2CRepeaterControl ); +// +// /* Initialize DC offset lookup tables */ +// generateDCOffsetTables( i2CRepeaterControl ); +// +// /* Enable time variant DC correction */ +// writeE4KRegister( Register.DCTIME1, (byte)0x01, i2CRepeaterControl ); +// writeE4KRegister( Register.DCTIME2, (byte)0x01, i2CRepeaterControl ); +// + /* Set LNA Mode */ + writeE4KRegister( Register.AGC4, (byte)0x10, i2CRepeaterControl ); //High Threshold + writeE4KRegister( Register.AGC5, (byte)0x04, i2CRepeaterControl ); //Low Threshold + writeE4KRegister( Register.AGC6, (byte)0x1A, i2CRepeaterControl ); //LNA calibrate + loop rate + + //TODO: lna and mixer gain were already set to manual when we generated + //the DC offset tables. Check the LNA gain setting after we set the + //LNA mode above, to see if the lna or mixer gain settings have changed + //and if not, remove the next two steps + +// /* Set LNA gain to manual using current auto LNA gain setting */ +// LNAGain lnaGain = getLNAGain( i2CRepeaterControl ); +// setLNAGain( lnaGain, i2CRepeaterControl ); +// +// /* Set Mixer gain to manual using current auto Mixer gain setting */ +// MixerGain mixerGain = getMixerGain( i2CRepeaterControl ); +// setMixerGain( mixerGain, i2CRepeaterControl ); + + //Temp - manual set lna mode to manual + writeMaskedE4KRegister( Register.AGC1, + AGC1_MOD_MASK, + AGCMode.SERIAL.getValue(), + false ); + //Temp - set mixer gain to manual + writeMaskedE4KRegister( Register.AGC7, + AGC7_MIXER_GAIN_MASK, + AGC7_MIX_GAIN_MANUAL, + false ); + + +// /* Enable enhance gain */ +// setEnhanceGain( EnhanceGain.GAIN_5, i2CRepeaterControl ); +// +// /* Enable automatic IF gain mode switching */ +// writeMaskedE4KRegister( Register.AGC8, +// IF_GAIN_MODE_SWITCHING_MASK, +// AGC8_SENS_LIN_AUTO, +// i2CRepeaterControl ); + + + /* Use automatic gain as a default */ + setLNAGain( E4KLNAGain.AUTOMATIC, i2CRepeaterControl ); + setMixerGain( E4KMixerGain.AUTOMATIC, i2CRepeaterControl ); + setEnhanceGain( E4KEnhanceGain.AUTOMATIC, i2CRepeaterControl ); + + /* Set IF gain stages */ + setIFStage1Gain( IFStage1Gain.GAIN_PLUS6, i2CRepeaterControl ); + setIFStage2Gain( IFStage2Gain.GAIN_PLUS0, i2CRepeaterControl ); + setIFStage3Gain( IFStage3Gain.GAIN_PLUS0, i2CRepeaterControl ); + setIFStage4Gain( IFStage4Gain.GAIN_PLUS0, i2CRepeaterControl ); + setIFStage5Gain( IFStage5Gain.GAIN_PLUS9, i2CRepeaterControl ); + setIFStage6Gain( IFStage6Gain.GAIN_PLUS9, i2CRepeaterControl ); + + /* Set the most narrow filter we can possible use */ + setMixerFilter( MixerFilter.BW_1M9, i2CRepeaterControl ); + setRCFilter( RCFilter.BW_1M0, i2CRepeaterControl ); + setChannelFilter( ChannelFilter.BW_2M15, i2CRepeaterControl ); + + setChannelFilterEnabled( true, i2CRepeaterControl ); + +// /* Disable DC detector */ +// setDCRangeDetectorEnabled( false, i2CRepeaterControl ); + + /* Disable time variant DC correction */ + writeMaskedE4KRegister( Register.DC5, (byte)0x03, (byte)0, i2CRepeaterControl ); + writeMaskedE4KRegister( Register.DCTIME1, (byte)0x03, (byte)0, i2CRepeaterControl ); + writeMaskedE4KRegister( Register.DCTIME2, (byte)0x03, (byte)0, i2CRepeaterControl ); + + if( controlI2CRepeater ) + { + enableI2CRepeater( mUSBDevice, false ); + } + } + + private void generateDCOffsetTables( boolean controlI2CRepeater ) + throws UsbException + { + boolean i2CRepeaterControl = false; + + if( controlI2CRepeater ) + { + enableI2CRepeater( mUSBDevice, true ); + } + + /* Capture current gain settings to reapply at the end */ + IFStage1Gain stage1 = getIFStage1Gain( i2CRepeaterControl ); + IFStage2Gain stage2 = getIFStage2Gain( i2CRepeaterControl ); + IFStage3Gain stage3 = getIFStage3Gain( i2CRepeaterControl ); + IFStage4Gain stage4 = getIFStage4Gain( i2CRepeaterControl ); + IFStage5Gain stage5 = getIFStage5Gain( i2CRepeaterControl ); + IFStage6Gain stage6 = getIFStage6Gain( i2CRepeaterControl ); + + /* Set Mixer gain to manual using current auto Mixer gain setting */ + E4KMixerGain mixerGain = getMixerGain( i2CRepeaterControl ); + setMixerGain( mixerGain, i2CRepeaterControl ); + + /* Set LNA gain to manual using current auto LNA gain setting */ + E4KLNAGain lnaGain = getLNAGain( i2CRepeaterControl ); + setLNAGain( lnaGain, i2CRepeaterControl ); + + /* Set IF Stage 2 - 6 gains to maximum */ + setIFStage2Gain( IFStage2Gain.GAIN_PLUS9, i2CRepeaterControl ); + setIFStage3Gain( IFStage3Gain.GAIN_PLUS9, i2CRepeaterControl ); + setIFStage4Gain( IFStage4Gain.GAIN_PLUS2B, i2CRepeaterControl ); + setIFStage5Gain( IFStage5Gain.GAIN_PLUS15D, i2CRepeaterControl ); + setIFStage6Gain( IFStage6Gain.GAIN_PLUS15D, i2CRepeaterControl ); + + /** + * Iterate all DC gain combinations, apply gain settings, read the + * DC offset value and apply it to the lookup table entry + */ + for( DCGainCombination combo: DCGainCombination.values() ) + { + /* Set mixer and stage1 gain values */ + setMixerGain( combo.getMixerGain(), i2CRepeaterControl ); + setIFStage1Gain( combo.getIFStage1Gain(), i2CRepeaterControl ); + + /* Turn on the DC range detector */ + setDCRangeDetectorEnabled( true, i2CRepeaterControl ); + + /* Calibrate the DC value */ + writeE4KRegister( Register.DC1, (byte)0x01, i2CRepeaterControl ); + + /* Get the I/Q offset and range values */ + byte offsetI = (byte)( readE4KRegister( Register.DC2, + i2CRepeaterControl ) & 0x3F ); + byte offsetQ = (byte)( readE4KRegister( Register.DC3, + i2CRepeaterControl ) & 0x3F ); + byte range = (byte)readE4KRegister( Register.DC4, + i2CRepeaterControl ); + byte rangeI = (byte)( range & 0x3 ); + byte rangeQ = (byte)( ( range >> 4 ) & 0x3 ); + + /* Write the offset and range values to the lookup table */ + writeE4KRegister( combo.getIRegister(), + (byte)( offsetI | ( rangeI << 6 ) ), + i2CRepeaterControl ); + + writeE4KRegister( combo.getQRegister(), + (byte)( offsetQ | ( rangeQ << 6 ) ), + i2CRepeaterControl ); + } + + if( controlI2CRepeater ) + { + enableI2CRepeater( mUSBDevice, false ); + } + } + + public void setDCRangeDetectorEnabled( boolean enabled, + boolean controlI2CRepeater ) throws UsbException + { + if( enabled ) + { + writeMaskedE4KRegister( Register.DC5, + DC5_RANGE_DETECTOR_ENABLED_MASK, + DC5_RANGE_DETECTOR_ENABLED, + controlI2CRepeater ); + } + else + { + writeMaskedE4KRegister( Register.DC5, + DC5_RANGE_DETECTOR_DISABLED_MASK, + DC5_RANGE_DETECTOR_DISABLED, + controlI2CRepeater ); + } + } + + /** + * Sets the LNA gain within the E4K Tuner + * + * Note: requires I2C repeater enabled + * + * @param gain + * @throws UsbException + */ + public void setLNAGain( E4KLNAGain gain, boolean controlI2CRepeater ) + throws UsbException + { + if( controlI2CRepeater ) + { + enableI2CRepeater( mUSBDevice, true ); + } + + if( gain == E4KLNAGain.AUTOMATIC ) + { + writeMaskedE4KRegister( Register.AGC1, + AGC1_MOD_MASK, + AGCMode.IF_SERIAL_LNA_AUTON.getValue(), + false ); + } + else + { + writeMaskedE4KRegister( Register.AGC1, + AGC1_MOD_MASK, + AGCMode.SERIAL.getValue(), + false ); + + + writeMaskedE4KRegister( E4KLNAGain.getRegister(), + E4KLNAGain.getMask(), + gain.getValue(), + false ); + } + + if( controlI2CRepeater ) + { + enableI2CRepeater( mUSBDevice, false ); + } + } + + /** + * Reads LNA gain from E4K tuner + * + * Note: requires I2C repeater enabled + */ + public E4KLNAGain getLNAGain( boolean controlI2CRepeater ) throws UsbException + { + return E4KLNAGain.fromRegisterValue( + readE4KRegister( E4KLNAGain.getRegister(), controlI2CRepeater ) ); + } + + /** + * Sets enhanced gain for E4K repeater. + * + * Note: requires I2C repeater enabled + * + * @param gain + * @throws UsbException + */ + public void setEnhanceGain( E4KEnhanceGain gain, boolean controlI2CRepeater ) + throws UsbException + { + writeMaskedE4KRegister( E4KEnhanceGain.getRegister(), + E4KEnhanceGain.getMask(), + gain.getValue(), + controlI2CRepeater ); + } + + /** + * Gets the enhanced gain setting in the E4K tuner. + * + * Note: requires I2C repeater + * @return + * @throws UsbException + */ + public E4KEnhanceGain getEnhanceGain( boolean controlI2CRepeater ) + throws UsbException + { + return E4KEnhanceGain.fromRegisterValue( + readE4KRegister( E4KEnhanceGain.getRegister(), controlI2CRepeater ) ); + } + + public void setMixerGain( E4KMixerGain gain, boolean controlI2CRepeater ) + throws UsbException + { + if( controlI2CRepeater ) + { + enableI2CRepeater( mUSBDevice, true ); + } + + boolean localI2CRepeaterControl = false; + + if( gain == E4KMixerGain.AUTOMATIC ) + { + writeMaskedE4KRegister( Register.AGC7, + AGC7_MIXER_GAIN_MASK, + AGC7_MIX_GAIN_AUTO, + localI2CRepeaterControl ); + } + else + { + writeMaskedE4KRegister( Register.AGC7, + AGC7_MIXER_GAIN_MASK, + AGC7_MIX_GAIN_MANUAL, + localI2CRepeaterControl ); + + /* Set the desired manual gain setting */ + writeMaskedE4KRegister( E4KMixerGain.getRegister(), + E4KMixerGain.getMask(), + gain.getValue(), + localI2CRepeaterControl ); + } + + if( controlI2CRepeater ) + { + enableI2CRepeater( mUSBDevice, false ); + } + } + + public E4KMixerGain getMixerGain( boolean controlI2CRepeater ) + throws UsbException + { + byte autoOrManual = readMaskedE4KRegister( Register.AGC7, + AGC7_MIXER_GAIN_MASK, + controlI2CRepeater ); + + if( autoOrManual == AGC7_MIX_GAIN_AUTO ) + { + return E4KMixerGain.AUTOMATIC; + } + else + { + int register = readE4KRegister( E4KMixerGain.getRegister(), + controlI2CRepeater ); + + return E4KMixerGain.fromRegisterValue( register ); + } + } + + public void setIFStage1Gain( IFStage1Gain gain, + boolean controlI2CRepeater ) throws UsbException + { + writeMaskedE4KRegister( IFStage1Gain.getRegister(), + IFStage1Gain.getMask(), + gain.getValue(), + controlI2CRepeater ); + } + + public IFStage1Gain getIFStage1Gain( boolean controlI2CRepeater ) + throws UsbException + { + return IFStage1Gain.fromRegisterValue( + readE4KRegister( IFStage1Gain.getRegister(), controlI2CRepeater ) ); + } + + public void setIFStage2Gain( IFStage2Gain gain, + boolean controlI2CRepeater ) throws UsbException + { + writeMaskedE4KRegister( IFStage2Gain.getRegister(), + IFStage2Gain.getMask(), + gain.getValue(), + controlI2CRepeater ); + } + + public IFStage2Gain getIFStage2Gain( boolean controlI2CRepeater ) + throws UsbException + { + return IFStage2Gain.fromRegisterValue( + readE4KRegister( IFStage2Gain.getRegister(), controlI2CRepeater ) ); + } + + public void setIFStage3Gain( IFStage3Gain gain, + boolean controlI2CRepeater ) throws UsbException + { + writeMaskedE4KRegister( IFStage3Gain.getRegister(), + IFStage3Gain.getMask(), + gain.getValue(), + controlI2CRepeater ); + } + + public IFStage3Gain getIFStage3Gain( boolean controlI2CRepeater ) + throws UsbException + { + return IFStage3Gain.fromRegisterValue( + readE4KRegister( IFStage3Gain.getRegister(), controlI2CRepeater ) ); + } + + public void setIFStage4Gain( IFStage4Gain gain, + boolean controlI2CRepeater ) throws UsbException + { + writeMaskedE4KRegister( IFStage4Gain.getRegister(), + IFStage4Gain.getMask(), + gain.getValue(), + controlI2CRepeater ); + } + + public IFStage4Gain getIFStage4Gain( boolean controlI2CRepeater ) + throws UsbException + { + return IFStage4Gain.fromRegisterValue( + readE4KRegister( IFStage4Gain.getRegister(), controlI2CRepeater ) ); + } + + public void setIFStage5Gain( IFStage5Gain gain, + boolean controlI2CRepeater ) throws UsbException + { + writeMaskedE4KRegister( IFStage5Gain.getRegister(), + IFStage5Gain.getMask(), + gain.getValue(), + controlI2CRepeater ); + } + + public IFStage5Gain getIFStage5Gain( boolean controlI2CRepeater ) + throws UsbException + { + return IFStage5Gain.fromRegisterValue( + readE4KRegister( IFStage5Gain.getRegister(), controlI2CRepeater ) ); + } + + public void setIFStage6Gain( IFStage6Gain gain, + boolean controlI2CRepeater ) throws UsbException + { + writeMaskedE4KRegister( IFStage6Gain.getRegister(), + IFStage6Gain.getMask(), + gain.getValue(), + controlI2CRepeater ); + } + + public IFStage6Gain getIFStage6Gain( boolean controlI2CRepeater ) + throws UsbException + { + return IFStage6Gain.fromRegisterValue( + readE4KRegister( IFStage6Gain.getRegister(), controlI2CRepeater ) ); + } + + public void setMixerFilter( MixerFilter filter, + boolean controlI2CRepeater ) throws UsbException + { + writeMaskedE4KRegister( MixerFilter.getRegister(), + MixerFilter.getMask(), + filter.getValue(), + controlI2CRepeater ); + } + + public MixerFilter getMixerFilter( boolean controlI2CRepeater ) + throws UsbException + { + int value = readE4KRegister( MixerFilter.getRegister(), + controlI2CRepeater ); + + return MixerFilter.fromRegisterValue( value ); + } + + public void setRCFilter( RCFilter filter, + boolean controlI2CRepeater ) throws UsbException + { + writeMaskedE4KRegister( RCFilter.getRegister(), + RCFilter.getMask(), + filter.getValue(), + controlI2CRepeater ); + } + + public RCFilter getRCFilter( boolean controlI2CRepeater ) throws UsbException + { + int value = readE4KRegister( RCFilter.getRegister(), controlI2CRepeater ); + + return RCFilter.fromRegisterValue( value ); + } + + public void setChannelFilter( ChannelFilter filter, + boolean controlI2CRepeater ) throws UsbException + { + writeMaskedE4KRegister( ChannelFilter.getRegister(), + ChannelFilter.getMask(), + filter.getValue(), + controlI2CRepeater ); + } + + public ChannelFilter getChannelFilter( boolean controlI2CRepeater ) + throws UsbException + { + int value = readE4KRegister( ChannelFilter.getRegister(), controlI2CRepeater ); + + return ChannelFilter.fromRegisterValue( value ); + + } + + public void setChannelFilterEnabled( boolean enabled, + boolean controlI2CRepeater ) throws UsbException + { + if( enabled ) + { + writeMaskedE4KRegister( Register.FILT3, + FILT3_MASK, + FILT3_ENABLE, + controlI2CRepeater ); + } + else + { + writeMaskedE4KRegister( Register.FILT3, + FILT3_MASK, + FILT3_DISABLE, + controlI2CRepeater ); + } + } + + public void setBand( int frequency, + boolean controlI2CRepeater ) throws UsbException + { + if( controlI2CRepeater ) + { + enableI2CRepeater( mUSBDevice, true ); + } + + Band band = Band.fromFrequency( frequency ); + + /* Set the bias */ + switch( band ) + { + case UHF: + case VHF2: + case VHF3: + writeE4KRegister( Register.BIAS, (byte)0x3, false ); + break; + case L: + default: + writeE4KRegister( Register.BIAS, (byte)0x0, false ); + break; + + } + + /** + * Workaround - reset register (set value to 0) before writing to it, to + * avoid a gap around 325-350 MHz + */ + writeMaskedE4KRegister( Register.SYNTH1, Band.getMask(), (byte)0x0, false ); + writeMaskedE4KRegister( Register.SYNTH1, Band.getMask(), band.getValue(), false ); + + if( controlI2CRepeater ) + { + enableI2CRepeater( mUSBDevice, false ); + } + } + + private void setRFFilter( int frequency, + boolean controlI2CRepeater ) throws UsbException + { + RFFilter filter = RFFilter.fromFrequency( frequency ); + + writeMaskedE4KRegister( Register.FILT1, + RFFilter.getMask(), + filter.getValue(), + controlI2CRepeater ); + } + + /** + * Sets the gain combination setting. + * + * Note: does not respond to the MANUAL setting. Use the manual setting + * to configure gui components to expose the individual Mixer, LNA and + * Enhance gain settings, to allow the user to change those settings + * individually. + * + * @param gain - requested gain setting + * @throws UsbException if there are any errors + */ + public void setGain( E4KGain gain, boolean controlI2CRepeater ) throws UsbException + { + if( gain != E4KGain.MANUAL ) + { + if( controlI2CRepeater ) + { + enableI2CRepeater( mUSBDevice, true ); + } + + boolean i2CRepeaterControl = false; + + setLNAGain( gain.getLNAGain(), i2CRepeaterControl ); + setMixerGain( gain.getMixerGain(), i2CRepeaterControl ); + setEnhanceGain( gain.getEnhanceGain(), i2CRepeaterControl ); + + if( controlI2CRepeater ) + { + enableI2CRepeater( mUSBDevice, false ); + } + } + } + + /* + * Performs magic initialization ... and that's all we need to know, I guess + */ + private void magicInit( boolean controlI2CRepeater ) throws UsbException + { + if( controlI2CRepeater ) + { + enableI2CRepeater( mUSBDevice, true ); + } + + writeE4KRegister( Register.MAGIC_1, (byte)0x01, false ); + writeE4KRegister( Register.MAGIC_2, (byte)0xFE, false ); + writeE4KRegister( Register.MAGIC_3, (byte)0x00, false ); + writeE4KRegister( Register.MAGIC_4, (byte)0x50, false ); //Polarity A + writeE4KRegister( Register.MAGIC_5, (byte)0x20, false ); + writeE4KRegister( Register.MAGIC_6, (byte)0x01, false ); + writeE4KRegister( Register.MAGIC_7, (byte)0x7F, false ); + writeE4KRegister( Register.MAGIC_8, (byte)0x07, false ); + + if( controlI2CRepeater ) + { + enableI2CRepeater( mUSBDevice, false ); + } + } + + private byte readMaskedE4KRegister( Register register, + byte mask, + boolean controlI2CRepeater ) + throws UsbException + { + int temp = readE4KRegister( register, controlI2CRepeater ); + return (byte)( temp & mask ); + } + + private void writeMaskedE4KRegister( Register register, + byte mask, + byte value, + boolean controlI2CRepeater ) throws UsbException + { + int temp = readE4KRegister( register, controlI2CRepeater ); + + /* If the register is not set to the masked value, then change it */ + if( (byte)( temp & mask ) != value ) + { + writeE4KRegister( register, + (byte)( ( temp & ~mask ) | ( value & mask ) ), + controlI2CRepeater ); + } + } + + private int readE4KRegister( Register register, + boolean controlI2CRepeater ) throws UsbException + { + return readI2CRegister( mUSBDevice, Register.I2C_REGISTER.getValue(), + register.getValue(), controlI2CRepeater ); + } + + + private void writeE4KRegister( Register register, + byte value, + boolean controlI2CRepeater ) throws UsbException + { + writeI2CRegister( mUSBDevice, Register.I2C_REGISTER.getValue(), + register.getValue(), value, controlI2CRepeater ); + } + + public enum Band + { + VHF2( 0 ), + VHF3( 2 ), + UHF( 4 ), + L( 6 ); + + private int mValue; + + private Band( int value ) + { + mValue = value; + } + + public static byte getMask() + { + return (byte)0x06; + } + + public byte getValue() + { + return (byte)mValue; + } + + public String getLabel() + { + return toString(); + } + + public static Band fromFrequency( int frequency ) + { + if( frequency < 140000000 ) //140 MHz + { + return VHF2; + } + else if( frequency < 350000000 ) //350 MHz + { + return VHF3; + } + else if( frequency < 1135000000 ) //1135 MHz + { + return UHF; + } + else + { + return L; + } + } + } + + public enum PLL + { + PLL_72M4( 0x0F, 72400000, 48, 600000, true, "72.4 MHz" ), + PLL_81M2( 0x0E, 81200000, 40, 720000, true, "81.2 MHz" ), + PLL_108M3( 0x0D, 108300000, 32, 900000, true, "108.3 MHz" ), + PLL_162M5( 0x0C, 162500000, 24, 1200000, true, "162.5 MHz" ), + PLL_216M6( 0x0B, 216600000, 16, 1800000, true, "216.6 MHz" ), + PLL_325M0( 0x0A, 325000000, 12, 2400000, true, "325.0 MHz" ), + PLL_350M0( 0x09, 350000000, 8, 3600000, true, "350.0 MHz" ), + PLL_432M0( 0x03, 432000000, 8, 3600000, false, "432.0 MHz" ), + PLL_667M0( 0x02, 667000000, 6, 4800000, false, "667.0 MHz" ), + PLL_1200M0( 0x01, 1200000000, 4, 7200000, false, "1200.0 MHz" ); + + private int mValue; + private int mFrequency; + private int mMultiplier; + private int mScaledOscillator; + private boolean mRequires3PhaseMixing; + private String mLabel; + + private PLL( int value, + int frequency, + int multiplier, + int scaledOscillator, + boolean requires3Phase, + String label ) + { + mValue = value; + mFrequency = frequency; + mMultiplier = multiplier; + mScaledOscillator = scaledOscillator; + mRequires3PhaseMixing = requires3Phase; + mLabel = label; + + } + + public byte getIndex() + { + return (byte)mValue; + } + + public int getFrequency() + { + return mFrequency; + } + + public byte getMultiplier() + { + return (byte)mMultiplier; + } + + public int getScaledOscillator() + { + return mScaledOscillator; + } + + public boolean requires3PhaseMixing() + { + return mRequires3PhaseMixing; + } + + public String getLabel() + { + return mLabel; + } + + public String toString() + { + return mLabel; + } + + /* Returns the PLL setting with the closest frequency that is greater + * than the frequency argument */ + public static PLL fromFrequency( int frequency ) + { + for( PLL pll: values() ) + { + if( frequency < pll.getFrequency() ) + { + return pll; + } + } + + //Default + return PLL.PLL_72M4; + } + + public static PLL fromSetting( int setting ) + { + int value = setting & 0xF; + + for( PLL pll: values() ) + { + if( value == pll.getIndex() ) + { + return pll; + } + } + + //Default + return PLL.PLL_72M4; + } + } + + /** + * RF filters - UHF Band. Each enum entry defines the center frequency for + * the filter and the applicable frequency minimums and maximums for the + * filter. + * + * Note: max frequency overlaps with the next minimum frequency, so valid + * frequencies should be less than the maximum frequency defined for the + * filter. + */ + public enum RFFilter + { + NO_FILTER( 0, 0, 0 ), + + //UHF band Filters + M360( 0, 350000000, 370000000 ), + M380( 1, 370000000, 392500000 ), + M405( 2, 392500000, 417500000 ), + M425( 3, 417500000, 437500000 ), + M450( 4, 437500000, 462500000 ), + M475( 5, 462500000, 490000000 ), + M505( 6, 490000000, 522500000 ), + M540( 7, 522500000, 557500000 ), + M575( 8, 557500000, 595000000 ), + M615( 9, 595000000, 642500000 ), + M670( 10, 642500000, 695000000 ), + M720( 11, 695000000, 740000000 ), + M760( 12, 740000000, 800000000 ), + M840( 13, 800000000, 865000000 ), + M890( 14, 865000000, 930000000 ), + M970( 15, 930000000, 1135000000 ), + + //L band filters + M1300( 0, 1135000000, 1310000000 ), + M1320( 1, 1310000000, 1340000000 ), + M1360( 2, 1340000000, 1385000000 ), + M1410( 3, 1385000000, 1427500000 ), + M1445( 4, 1427500000, 1452500000 ), + M1460( 5, 1452500000, 1475000000 ), + M1490( 6, 1475000000, 1510000000 ), + M1530( 7, 1510000000, 1545000000 ), + M1560( 8, 1545000000, 1575000000 ), + M1590( 9, 1575000000, 1615000000 ), + M1640( 10, 1615000000, 1650000000 ), + M1660( 11, 1650000000, 1670000000 ), + M1680( 12, 1670000000, 1690000000 ), + M1700( 13, 1690000000, 1710000000 ), + M1720( 14, 1710000000, 1735000000 ), + + /* Note: max frequency is currently limited by the max integer value + * and we set the max frequency for this filter to 2147 MHz */ + M1750( 15, 1735000000, 2147000000 ); + + private int mValue; + private int mMinFrequency; + private int mMaxFrequency; + + private RFFilter( int value, int minFrequency, int maxFrequency ) + { + mValue = value; + mMinFrequency = minFrequency; + mMaxFrequency = maxFrequency; + } + + public static Register getRegister() + { + return Register.FILT1; + } + + public static byte getMask() + { + return (byte)0xF; //TODO: make this a constant + } + + public byte getValue() + { + return (byte)mValue; + } + + /** + * Minimum frequency for this filter. + */ + public int getMinimumFrequency() + { + return mMinFrequency; + } + + /** + * Maximum frequency for this filter is less than this value, but does + * not include this value. + */ + public int getMaximumFrequency() + { + return mMaxFrequency; + } + + /** + * Selects the appropriate filter for the frequency + * + * @param frequency + * @return selected filter + * @throws IllegalArgumentException if the frequency is outside the + * max value (2200 MHz) + */ + public static RFFilter fromFrequency( int frequency ) + { + if( frequency < 350000000 ) + { + return NO_FILTER; + } + + for( RFFilter filter: values() ) + { + if( filter.getMinimumFrequency() <= frequency && + frequency < filter.getMaximumFrequency() ) + { + return filter; + } + } + + throw new IllegalArgumentException( "E4KTunerController - " + + "cannot find RF filter for frequency [" + frequency + "]" ); + } + } + + public enum MixerFilter + { + BW_27M0( 0x00, 27000000, 4800000, 28800000, "27.0 MHz" ), + BW_4M6( 0x80, 4600000, 4400000, 4800000, "4.6 MHz" ), + BW_4M2( 0x90, 4200000, 4000000, 4400000, "4.2 MHz" ), + BW_3M8( 0xA0, 3800000, 3600000, 4000000, "3.8 MHz" ), + BW_3M4( 0xB0, 3400000, 3200000, 3600000, "3.4 MHz" ), + BW_3M0( 0xC0, 3000000, 2850000, 3200000, "3.0 MHz" ), + BW_2M7( 0xD0, 2700000, 2500000, 2850000, "2.7 MHz" ), + BW_2M3( 0xE0, 2300000, 2100000, 2500000, "2.3 MHz" ), + BW_1M9( 0xF0, 1900000, 0, 2100000, "1.9 MHz" ); + + private int mValue; + private int mBandwidth; + private int mMinBandwidth; + private int mMaxBandwidth; + private String mLabel; + + private MixerFilter( int value, + int bandwidth, + int minimumBandwidth, + int maximumBandwidth, + String label ) + { + mValue = value; + mBandwidth = bandwidth; + mMinBandwidth = minimumBandwidth; + mMaxBandwidth = maximumBandwidth; + mLabel = label; + } + + public static Register getRegister() + { + return Register.FILT2; + } + + public static byte getMask() + { + return MIXER_FILTER_MASK; + } + + public byte getValue() + { + return (byte)mValue; + } + + public int getBandwidth() + { + return mBandwidth; + } + + public int getMinimumBandwidth() + { + return mMinBandwidth; + } + + public int getMaximumBandwidth() + { + return mMaxBandwidth; + } + + public String getLabel() + { + return mLabel; + } + + /** + * Returns the correct filter to use for the bandwidth/sample rate + * @param bandwidth - current sample rate + * @return - correct filter to use for the bandwidth/sample rate + */ + public static MixerFilter getFilter( int bandwidth ) + { + for( MixerFilter filter: values() ) + { + if( filter.getMinimumBandwidth() <= bandwidth && + bandwidth < filter.getMaximumBandwidth() ) + { + return filter; + } + } + + /* default */ + return MixerFilter.BW_27M0; + } + + public static MixerFilter fromRegisterValue( int registerValue ) + { + int value = registerValue & getMask(); + + for( MixerFilter filter: values() ) + { + if( value == filter.getValue() ) + { + return filter; + } + } + + throw new IllegalArgumentException( "E4KTunerController - " + + "unrecognized mixer filter value [" + value + "]" ); + } + } + + public enum RCFilter + { + BW_21M4( 0x00, 21400000, 21200000, 28800000, "21.4 MHz" ), + BW_21M0( 0x01, 21000000, 19300000, 21200000, "21.0 MHz" ), + BW_17M6( 0x02, 17600000, 16150000, 19300000, "17.6 MHz" ), + BW_14M7( 0x03, 14700000, 13550000, 16150000, "14.7 MHz" ), + BW_12M4( 0x04, 12400000, 11500000, 13550000, "12.4 MHz" ), + BW_10M6( 0x05, 10600000, 9800000, 11500000, "10.6 MHz" ), + BW_9M0( 0x06, 9000000, 8350000, 9800000, "9.0 MHz" ), + BW_7M7( 0x07, 7700000, 7050000, 8350000, "7.7 MHz" ), + BW_6M4( 0x08, 6400000, 5805000, 7050000, "6.4 MHz" ), + BW_5M3( 0x09, 5300000, 4850000, 5805000, "5.3 MHz" ), + BW_4M4( 0x0A, 4400000, 3900000, 4850000, "4.4 MHz" ), + BW_3M4( 0x0B, 3400000, 3000000, 3900000, "3.4 MHz" ), + BW_2M6( 0x0C, 2600000, 2200000, 3000000, "2.6 MHz" ), + BW_1M8( 0x0D, 1800000, 1500000, 2200000, "1.8 MHz" ), + BW_1M2( 0x0E, 1200000, 1100000, 1500000, "1.2 MHz" ), + BW_1M0( 0x0F, 1000000, 0, 1100000, "1.0 MHz" ); + + private int mValue; + private int mBandwidth; + private int mMinBandwidth; + private int mMaxBandwidth; + private String mLabel; + + private RCFilter( int value, + int bandwidth, + int minimumBandwidth, + int maximumBandwidth, + String label ) + { + mValue = value; + mBandwidth = bandwidth; + mMinBandwidth = minimumBandwidth; + mMaxBandwidth = maximumBandwidth; + mLabel = label; + } + + public static Register getRegister() + { + return Register.FILT2; + } + + public static byte getMask() + { + return IF_RC_FILTER_MASK; + } + + public byte getValue() + { + return (byte)mValue; + } + + public int getBandwidth() + { + return mBandwidth; + } + + public int getMinimumBandwidth() + { + return mMinBandwidth; + } + + public int getMaximumBandwidth() + { + return mMaxBandwidth; + } + + public String getLabel() + { + return mLabel; + } + + /** + * Returns the correct filter to use for the bandwidth/sample rate + * @param bandwidth - current sample rate + * @return - correct filter to use for the bandwidth/sample rate + */ + public static RCFilter getFilter( int bandwidth ) + { + for( RCFilter filter: values() ) + { + if( filter.getMinimumBandwidth() <= bandwidth && + bandwidth < filter.getMaximumBandwidth() ) + { + return filter; + } + } + + /* default */ + return RCFilter.BW_21M4; + } + + public static RCFilter fromRegisterValue( int registerValue ) + { + int value = registerValue & getMask(); + + for( RCFilter filter: values() ) + { + if( value == filter.getValue() ) + { + return filter; + } + } + + throw new IllegalArgumentException( "E4KTunerController - " + + "unrecognized rc filter value [" + value + "]" ); + } + } + + public enum ChannelFilter + { + BW_5M50( 0x00, 5500000, 5400000, 28800000, "5.50 MHz" ), + BW_5M30( 0x01, 5300000, 5150000, 5400000, "5.30 MHz" ), + BW_5M00( 0x02, 5000000, 4900000, 5150000, "5.00 MHz" ), + BW_4M80( 0x03, 4800000, 4700000, 4900000, "4.80 MHz" ), + BW_4M60( 0x04, 4600000, 4500000, 4700000, "4.60 MHz" ), + BW_4M40( 0x05, 4400000, 4350000, 4500000, "4.40 MHz" ), + BW_4M30( 0x06, 4300000, 4200000, 4350000, "4.30 MHz" ), + BW_4M10( 0x07, 4100000, 4000000, 4200000, "4.10 MHz" ), + BW_3M90( 0x08, 3900000, 3850000, 4000000, "3.90 MHz" ), + BW_3M80( 0x09, 3800000, 3750000, 3850000, "3.80 MHz" ), + BW_3M70( 0x0A, 3700000, 3650000, 3750000, "3.70 MHz" ), + BW_3M60( 0x0B, 3600000, 3500000, 3650000, "3.60 MHz" ), + BW_3M40( 0x0C, 3400000, 3350000, 3500000, "3.40 MHz" ), + BW_3M30( 0x0D, 3300000, 3250000, 3350000, "3.30 MHz" ), + BW_3M20( 0x0E, 3200000, 3150000, 3250000, "3.20 MHz" ), + BW_3M10( 0x0F, 3100000, 3050000, 3150000, "3.10 MHz" ), + BW_3M00( 0x10, 3000000, 2975000, 3050000, "3.00 MHz" ), + BW_2M95( 0x11, 2950000, 2925000, 2975000, "2.95 MHz" ), + BW_2M90( 0x12, 2900000, 2850000, 2925000, "2.90 MHz" ), + BW_2M80( 0x13, 2800000, 2775000, 2850000, "2.80 MHz" ), + BW_2M75( 0x14, 2750000, 2750000, 2775000, "2.75 MHz" ), + BW_2M70( 0x15, 2700000, 2650000, 2750000, "2.70 MHz" ), + BW_2M60( 0x16, 2600000, 2575000, 2650000, "2.60 MHz" ), + BW_2M55( 0x17, 2550000, 2525000, 2575000, "2.55 MHz" ), + BW_2M50( 0x18, 2500000, 2475000, 2525000, "2.50 MHz" ), + BW_2M45( 0x19, 2450000, 2425000, 2475000, "2.45 MHz" ), + BW_2M40( 0x1A, 2400000, 2350000, 2425000, "2.40 MHz" ), + BW_2M30( 0x1B, 2300000, 2290000, 2350000, "2.30 MHz" ), + BW_2M28( 0x1C, 2280000, 2260000, 2290000, "2.28 MHz" ), + BW_2M24( 0x1D, 2240000, 2220000, 2260000, "2.24 MHz" ), + BW_2M20( 0x1E, 2200000, 2175000, 2220000, "2.20 MHz" ), + BW_2M15( 0x1F, 2150000, 0, 2175000, "2.15 MHz" ); + + private int mValue; + private int mBandwidth; + private int mMinBandwidth; + private int mMaxBandwidth; + private String mLabel; + + private ChannelFilter( int value, + int bandwidth, + int minimumBandwidth, + int maximumBandwidth, + String label ) + { + mValue = value; + mBandwidth = bandwidth; + mMinBandwidth = minimumBandwidth; + mMaxBandwidth = maximumBandwidth; + mLabel = label; + } + + public static Register getRegister() + { + return Register.FILT3; + } + + public static byte getMask() + { + return IF_CHANNEL_FILTER_MASK; + } + + public byte getValue() + { + return (byte)mValue; + } + + public int getBandwidth() + { + return mBandwidth; + } + + public int getMinimumBandwidth() + { + return mMinBandwidth; + } + + public int getMaximumBandwidth() + { + return mMaxBandwidth; + } + + public String getLabel() + { + return mLabel; + } + + /** + * Returns the correct filter to use for the bandwidth/sample rate + * @param bandwidth - current sample rate + * @return - correct filter to use for the bandwidth/sample rate + */ + public static ChannelFilter getFilter( int bandwidth ) + { + for( ChannelFilter filter: values() ) + { + if( filter.getMinimumBandwidth() <= bandwidth && + bandwidth < filter.getMaximumBandwidth() ) + { + return filter; + } + } + + /* default */ + return ChannelFilter.BW_5M50; + } + + public static ChannelFilter fromRegisterValue( int registerValue ) + { + int value = registerValue & getMask(); + + for( ChannelFilter filter: values() ) + { + if( value == filter.getValue() ) + { + return filter; + } + } + + throw new IllegalArgumentException( "E4KTunerController - " + + "unrecognized channel filter value [" + value + "]" ); + } + } + + public enum E4KLNAGain + { + AUTOMATIC( -1, "Auto" ), + GAIN_MINUS_50( 0, "-5.0 db" ), + GAIN_MINUS_25( 1, "-2.5 db" ), + GAIN_PLUS_0( 4, "0.0 db" ), + GAIN_PLUS_25( 5, "2.5 db" ), + GAIN_PLUS_50( 6, "5.0 db" ), + GAIN_PLUS_75( 7, "7.5 db" ), + GAIN_PLUS_100( 8, "10.0 db" ), + GAIN_PLUS_125( 9, "12.5 db" ), + GAIN_PLUS_150( 10, "15.0 db" ), + GAIN_PLUS_175( 11, "17.5 db" ), + GAIN_PLUS_200( 12, "20.0 db" ), + GAIN_PLUS_250( 13, "25.0 db" ), + GAIN_PLUS_300( 14, "30.0 db" ); + + private int mValue; + private String mLabel; + + private E4KLNAGain( int value, String label ) + { + mValue = value; + mLabel = label; + } + + public static Register getRegister() + { + return Register.GAIN1; + } + + public static byte getMask() + { + return GAIN1_MOD_MASK; + } + + public byte getValue() + { + return (byte)mValue; + } + + public String getLabel() + { + return mLabel; + } + + public String toString() + { + return mLabel; + } + + public static E4KLNAGain fromRegisterValue( int registerValue ) + { + int value = registerValue & getMask(); + + for( E4KLNAGain setting: values() ) + { + if( value == setting.getValue() ) + { + return setting; + } + } + + throw new IllegalArgumentException( "E4KTunerController" + + " - unrecognized LNA Gain value [" + + value + "]" ); + } + } + + /** + * Defines the default gain settings that are exposed to the user. + * + * Manual is an override that keeps each of the settings intact and allows + * the user to change each of them individually + */ + public enum E4KGain + { + AUTOMATIC( "Auto", E4KMixerGain.AUTOMATIC, E4KLNAGain.AUTOMATIC, E4KEnhanceGain.AUTOMATIC ), + MANUAL( "Manual", null, null, null ), + MINUS_10( "-1.0 db", E4KMixerGain.GAIN_4, E4KLNAGain.GAIN_MINUS_50, E4KEnhanceGain.AUTOMATIC ), + PLUS_15( "1.5 db", E4KMixerGain.GAIN_4, E4KLNAGain.GAIN_MINUS_25, E4KEnhanceGain.AUTOMATIC ), + PLUS_40( "4.0 db", E4KMixerGain.GAIN_4, E4KLNAGain.GAIN_PLUS_0, E4KEnhanceGain.AUTOMATIC ), + PLUS_65( "6.5 db", E4KMixerGain.GAIN_4, E4KLNAGain.GAIN_PLUS_25, E4KEnhanceGain.AUTOMATIC ), + PLUS_90( "9.0 db", E4KMixerGain.GAIN_4, E4KLNAGain.GAIN_PLUS_50, E4KEnhanceGain.AUTOMATIC ), + PLUS_115( "11.5 db", E4KMixerGain.GAIN_4, E4KLNAGain.GAIN_PLUS_75, E4KEnhanceGain.AUTOMATIC ), + PLUS_140( "14.0 db", E4KMixerGain.GAIN_4, E4KLNAGain.GAIN_PLUS_100, E4KEnhanceGain.AUTOMATIC ), + PLUS_165( "16.5 db", E4KMixerGain.GAIN_4, E4KLNAGain.GAIN_PLUS_125, E4KEnhanceGain.AUTOMATIC ), + PLUS_190( "19.0 db", E4KMixerGain.GAIN_4, E4KLNAGain.GAIN_PLUS_150, E4KEnhanceGain.AUTOMATIC ), + PLUS_215( "21.5 db", E4KMixerGain.GAIN_4, E4KLNAGain.GAIN_PLUS_175, E4KEnhanceGain.AUTOMATIC ), + PLUS_240( "24.0 db", E4KMixerGain.GAIN_4, E4KLNAGain.GAIN_PLUS_200, E4KEnhanceGain.AUTOMATIC ), + PLUS_290( "29.0 db", E4KMixerGain.GAIN_4, E4KLNAGain.GAIN_PLUS_250, E4KEnhanceGain.AUTOMATIC ), + PLUS_340( "34.0 db", E4KMixerGain.GAIN_4, E4KLNAGain.GAIN_PLUS_300, E4KEnhanceGain.AUTOMATIC ), + PLUS_420( "42.0 db", E4KMixerGain.GAIN_12, E4KLNAGain.GAIN_PLUS_300, E4KEnhanceGain.AUTOMATIC ); + + private String mLabel; + private E4KMixerGain mMixerGain; + private E4KLNAGain mLNAGain; + private E4KEnhanceGain mEnhanceGain; + + private E4KGain( String label, + E4KMixerGain mixerGain, + E4KLNAGain lnaGain, + E4KEnhanceGain enhanceGain ) + { + mLabel = label; + mMixerGain = mixerGain; + mLNAGain = lnaGain; + mEnhanceGain = enhanceGain; + } + + public String getLabel() + { + return mLabel; + } + + public String toString() + { + return mLabel; + } + + public E4KMixerGain getMixerGain() + { + return mMixerGain; + } + + public E4KLNAGain getLNAGain() + { + return mLNAGain; + } + + public E4KEnhanceGain getEnhanceGain() + { + return mEnhanceGain; + } + } + + public enum E4KEnhanceGain + { + AUTOMATIC( 0, "Auto" ), + GAIN_1( 1, "10 db" ), + GAIN_3( 3, "30 db" ), + GAIN_5( 5, "50 db" ), + GAIN_7( 7, "70 db" ); + + private int mValue; + private String mLabel; + + private E4KEnhanceGain( int value, String label ) + { + mValue = value; + mLabel = label; + } + + public static Register getRegister() + { + return Register.AGC11; + } + + public static byte getMask() + { + return ENH_GAIN_MOD_MASK; + } + + public byte getValue() + { + return (byte)mValue; + } + + public String getLabel() + { + return mLabel; + } + + public String toString() + { + return mLabel; + } + + public static E4KEnhanceGain fromRegisterValue( int registerValue ) + { + int value = registerValue & getMask(); + + for( E4KEnhanceGain setting: values() ) + { + if( value == setting.getValue() ) + { + return setting; + } + } + + throw new IllegalArgumentException( "E4KTunerController" + + " - unrecognized Enhance Gain value [" + + value + "]" ); + } + } + + public enum DCGainCombination + { + LOOKUP_TABLE_0( Register.QLUT0, + Register.ILUT0, + E4KMixerGain.GAIN_4, + IFStage1Gain.GAIN_MINUS3 ), + LOOKUP_TABLE_1( Register.QLUT1, + Register.ILUT1, + E4KMixerGain.GAIN_4, + IFStage1Gain.GAIN_PLUS6 ), + LOOKUP_TABLE_2( Register.QLUT2, + Register.ILUT2, + E4KMixerGain.GAIN_12, + IFStage1Gain.GAIN_MINUS3 ), + LOOKUP_TABLE_3( Register.QLUT3, + Register.ILUT3, + E4KMixerGain.GAIN_12, + IFStage1Gain.GAIN_PLUS6 ); + + private Register mQRegister; + private Register mIRegister; + private E4KMixerGain mMixerGain; + private IFStage1Gain mIFStage1Gain; + + private DCGainCombination( Register qRegister, + Register iRegister, + E4KMixerGain mixer, + IFStage1Gain stage1 ) + { + mQRegister = qRegister; + mIRegister = iRegister; + mMixerGain = mixer; + mIFStage1Gain = stage1; + } + + public Register getQRegister() + { + return mQRegister; + } + + public Register getIRegister() + { + return mIRegister; + } + + public E4KMixerGain getMixerGain() + { + return mMixerGain; + } + + public IFStage1Gain getIFStage1Gain() + { + return mIFStage1Gain; + } + } + + + public enum E4KMixerGain + { + AUTOMATIC( -1, "Auto" ), + GAIN_4( 0, "4 db" ), + GAIN_12( 1, "12 db" ); + + private int mValue; + private String mLabel; + + private E4KMixerGain( int value, String label ) + { + mValue = value; + mLabel = label; + } + + public static Register getRegister() + { + return Register.GAIN2; + } + + public static byte getMask() + { + return MIXER_GAIN_MASK; + } + + public byte getValue() + { + return (byte)mValue; + } + + public String getLabel() + { + return mLabel; + } + + public String toString() + { + return mLabel; + } + + public static E4KMixerGain fromRegisterValue( int registerValue ) + { + int value = registerValue & getMask(); + + for( E4KMixerGain setting: values() ) + { + if( value == setting.getValue() ) + { + return setting; + } + } + + throw new IllegalArgumentException( "E4KTunerController" + + " - unrecognized Mixer Gain value [" + + value + "]" ); + } + } + + public enum IFStage1Gain + { + GAIN_MINUS3( 0x0, "-3 db" ), + GAIN_PLUS6( 0x1, "6 db" ); + + private int mValue; + private String mLabel; + + private IFStage1Gain( int value, String label ) + { + mValue = value; + mLabel = label; + } + + public static Register getRegister() + { + return Register.GAIN3; + } + + public static byte getMask() + { + return (byte)0x1; + } + + public byte getValue() + { + return (byte)mValue; + } + + public String getLabel() + { + return mLabel; + } + + public String toString() + { + return mLabel; + } + + public static IFStage1Gain fromRegisterValue( int registerValue ) + { + int value = registerValue & getMask(); + + for( IFStage1Gain setting: values() ) + { + if( value == setting.getValue() ) + { + return setting; + } + } + + throw new IllegalArgumentException( "E4KTunerController" + + " - unrecognized IF Gain Stage 1 value [" + + value + "]" ); + } + } + + public enum IFStage2Gain + { + GAIN_PLUS0( 0x0, "0 db" ), + GAIN_PLUS3( 0x2, "3 db" ), + GAIN_PLUS6( 0x4, "6 db" ), + GAIN_PLUS9( 0x6, "9 db" ); + + private int mValue; + private String mLabel; + + private IFStage2Gain( int value, String label ) + { + mValue = value; + mLabel = label; + } + + public static Register getRegister() + { + return Register.GAIN3; + } + + public static byte getMask() + { + return (byte)0x6; + } + + public byte getValue() + { + return (byte)mValue; + } + + public String getLabel() + { + return mLabel; + } + + public String toString() + { + return mLabel; + } + + public static IFStage2Gain fromRegisterValue( int registerValue ) + { + int value = registerValue & getMask(); + + for( IFStage2Gain setting: values() ) + { + if( value == setting.getValue() ) + { + return setting; + } + } + + throw new IllegalArgumentException( "E4KTunerController" + + " - unrecognized IF Gain Stage 2 value [" + + value + "]" ); + } + } + + public enum IFStage3Gain + { + GAIN_PLUS0( 0x00, "0 db" ), + GAIN_PLUS3( 0x08, "3 db" ), + GAIN_PLUS6( 0x10, "6 db" ), + GAIN_PLUS9( 0x30, "9 db" ); + + private int mValue; + private String mLabel; + + private IFStage3Gain( int value, String label ) + { + mValue = value; + mLabel = label; + } + + public static Register getRegister() + { + return Register.GAIN3; + } + + public static byte getMask() + { + return (byte)0x18; + } + + public byte getValue() + { + return (byte)mValue; + } + + public String getLabel() + { + return mLabel; + } + + public String toString() + { + return mLabel; + } + + public static IFStage3Gain fromRegisterValue( int registerValue ) + { + int value = registerValue & getMask(); + + for( IFStage3Gain setting: values() ) + { + if( value == setting.getValue() ) + { + return setting; + } + } + + throw new IllegalArgumentException( "E4KTunerController" + + " - unrecognized IF Gain Stage 3 value [" + + value + "]" ); + } + } + + public enum IFStage4Gain + { + GAIN_PLUS0( 0x00, "0 db" ), + GAIN_PLUS1( 0x20, "1 db" ), + GAIN_PLUS2A( 0x40, "2 db" ), + GAIN_PLUS2B( 0x60, "2 db" ); + + private int mValue; + private String mLabel; + + private IFStage4Gain( int value, String label ) + { + mValue = value; + mLabel = label; + } + + public static Register getRegister() + { + return Register.GAIN3; + } + + public static byte getMask() + { + return (byte)0x60; + } + + public byte getValue() + { + return (byte)mValue; + } + + public String getLabel() + { + return mLabel; + } + + public String toString() + { + return mLabel; + } + + public static IFStage4Gain fromRegisterValue( int registerValue ) + { + int value = registerValue & getMask(); + + for( IFStage4Gain setting: values() ) + { + if( value == setting.getValue() ) + { + return setting; + } + } + + throw new IllegalArgumentException( "E4KTunerController" + + " - unrecognized IF Gain Stage 4 value [" + + value + "]" ); + } + } + + public enum IFStage5Gain + { + GAIN_PLUS3( 0x0, "3 db" ), + GAIN_PLUS6( 0x1, "6 db" ), + GAIN_PLUS9( 0x2, "9 db" ), + GAIN_PLUS12( 0x3, "12 db" ), + GAIN_PLUS15A( 0x4, "15 db" ), + GAIN_PLUS15B( 0x5, "15 db" ), + GAIN_PLUS15C( 0x6, "15 db" ), + GAIN_PLUS15D( 0x7, "15 db" ); + + private int mValue; + private String mLabel; + + private IFStage5Gain( int value, String label ) + { + mValue = value; + mLabel = label; + } + + public static Register getRegister() + { + return Register.GAIN4; + } + + public static byte getMask() + { + return (byte)0x7; + } + + public byte getValue() + { + return (byte)mValue; + } + + public String getLabel() + { + return mLabel; + } + + public String toString() + { + return mLabel; + } + + public static IFStage5Gain fromRegisterValue( int registerValue ) + { + int value = registerValue & getMask(); + + for( IFStage5Gain setting: values() ) + { + if( value == setting.getValue() ) + { + return setting; + } + } + + throw new IllegalArgumentException( "E4KTunerController" + + " - unrecognized IF Gain Stage 5 value [" + + value + "]" ); + } + + } + + public enum IFStage6Gain + { + GAIN_PLUS3( 0x00, "3 db" ), + GAIN_PLUS6( 0x08, "6 db" ), + GAIN_PLUS9( 0x10, "9 db" ), + GAIN_PLUS12( 0x18, "12 db" ), + GAIN_PLUS15A( 0x20, "15 db" ), + GAIN_PLUS15B( 0x28, "15 db" ), + GAIN_PLUS15C( 0x30, "15 db" ), + GAIN_PLUS15D( 0x38, "15 db" ); + + private int mValue; + private String mLabel; + + private IFStage6Gain( int value, String label ) + { + mValue = value; + mLabel = label; + } + + public static Register getRegister() + { + return Register.GAIN4; + } + + public static byte getMask() + { + return (byte)0x38; + } + + public byte getValue() + { + return (byte)mValue; + } + + public String getLabel() + { + return mLabel; + } + + public String toString() + { + return mLabel; + } + + public static IFStage6Gain fromRegisterValue( int registerValue ) + { + int value = registerValue & getMask(); + + for( IFStage6Gain setting: values() ) + { + if( value == setting.getValue() ) + { + return setting; + } + } + + throw new IllegalArgumentException( "E4KTunerController" + + " - unrecognized IF Gain Stage 6 value [" + + value + "]" ); + } + } + + public enum IFFilter + { + MIX( "Mix" ), + CHAN( "Channel" ), + RC( "RC" ); + + private String mLabel; + + private IFFilter( String label ) + { + mLabel = label; + } + + public String getLabel() + { + return mLabel; + } + } + + public enum AGCMode + { + SERIAL( (byte)0x0 ), + IF_PWM_LNA_SERIAL( (byte)0x1 ), + IF_PWM_LNA_AUTONL( (byte)0x2 ), + IF_PWM_LNA_SUPERV( (byte)0x3 ), + IF_SERIAL_LNA_PWM( (byte)0x4 ), + IF_PWM_LNA_PWM( (byte)0x5 ), + IF_DIG_LNA_SERIAL( (byte)0x6 ), + IF_DIG_LNA_AUTON( (byte)0x7 ), + IF_DIG_LNA_SUPERV( (byte)0x8 ), + IF_SERIAL_LNA_AUTON( (byte)0x9 ), + IF_SERIAL_LNA_SUPERV( (byte)0xA ); + + private byte mValue; + + private AGCMode( byte value ) + { + mValue = value; + } + + public byte getValue() + { + return mValue; + } + } + + public enum Register + { + DUMMY( 0x00 ), + MASTER1( 0x00 ), + MASTER2( 0x01 ), + MASTER3( 0x02 ), + MASTER4( 0x03 ), + MASTER5( 0x04 ), + CLK_INP( 0x05 ), + REF_CLK( 0x06 ), + SYNTH1( 0x07 ), + SYNTH2( 0x08 ), + SYNTH3( 0x09 ), + SYNTH4( 0x0A ), + SYNTH5( 0x0B ), + SYNTH6( 0x0C ), + SYNTH7( 0x0D ), + SYNTH8( 0x0E ), + SYNTH9( 0x0F ), + FILT1( 0x10 ), + FILT2( 0x11 ), + FILT3( 0x12 ), + GAIN1( 0x14 ), + GAIN2( 0x15 ), + GAIN3( 0x16 ), + GAIN4( 0x17 ), + AGC1( 0x1A ), + AGC2( 0x1B ), + AGC3( 0x1C ), + AGC4( 0x1D ), + AGC5( 0x1E ), + AGC6( 0x1F ), + AGC7( 0x20 ), + AGC8( 0x21 ), + AGC11( 0x24 ), + AGC12( 0x25 ), + DC1( 0x29 ), + DC2( 0x2A ), + DC3( 0x2B ), + DC4( 0x2C ), + DC5( 0x2D ), + DC6( 0x2E ), + DC7( 0x2F ), + DC8( 0x30 ), + QLUT0( 0x50 ), + QLUT1( 0x51 ), + QLUT2( 0x52 ), + QLUT3( 0x53 ), + ILUT0( 0x60 ), + ILUT1( 0x61 ), + ILUT2( 0x62 ), + ILUT3( 0x63 ), + DCTIME1( 0x70 ), + DCTIME2( 0x71 ), + DCTIME3( 0x72 ), + DCTIME4( 0x73 ), + PWM1( 0x74 ), + PWM2( 0x75 ), + PWM3( 0x76 ), + PWM4( 0x77 ), + BIAS( 0x78 ), + CLKOUT_PWDN( 0x7A ), + CHFILT_CALIB( 0x7B ), + I2C_REG_ADDR( 0x7D ), + MAGIC_1( 0x7E ), + MAGIC_2( 0x7F ), + MAGIC_3( 0x82 ), + MAGIC_4( 0x86 ), + MAGIC_5( 0x87 ), + MAGIC_6( 0x88 ), + MAGIC_7( 0x9F ), + MAGIC_8( 0xA0 ), + I2C_REGISTER( 0xC8 ); + + private int mValue; + + private Register( int value ) + { + mValue = value; + } + + public byte getValue() + { + return (byte)mValue; + } + } +} diff --git a/src/source/tuner/rtl/e4k/E4KTunerEditorPanel.java b/src/source/tuner/rtl/e4k/E4KTunerEditorPanel.java new file mode 100644 index 00000000..909bd93f --- /dev/null +++ b/src/source/tuner/rtl/e4k/E4KTunerEditorPanel.java @@ -0,0 +1,111 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package source.tuner.rtl.e4k; + +import gui.control.JFrequencyControl; + +import java.awt.Color; +import java.awt.EventQueue; + +import javax.swing.BorderFactory; +import javax.swing.JLabel; +import javax.swing.JPanel; + +import com.jidesoft.swing.JideTabbedPane; + +import log.Log; +import net.miginfocom.swing.MigLayout; +import source.SourceComponentEditor; +import source.SourceException; +import source.tuner.FrequencyChangeListener; +import source.tuner.rtl.RTL2832InfoPanel; +import controller.ResourceManager; + +public class E4KTunerEditorPanel extends JPanel implements FrequencyChangeListener +{ + private static final long serialVersionUID = 1L; + + private E4KTunerController mController; + private ResourceManager mResourceManager; + + private JFrequencyControl mFrequencyControl; + + public E4KTunerEditorPanel( E4KTunerController controller, + ResourceManager resourceManager ) + { + mController = controller; + mResourceManager = resourceManager; + + initGUI(); + } + + private void initGUI() + { + setLayout( new MigLayout( "fill,wrap 2", "[right][grow]", "[][][grow]" ) ); + + add( new JLabel( "RTL2832 with E4000 Tuner" ), "span 2, align center, wrap" ); + + mFrequencyControl = new JFrequencyControl(); + + mFrequencyControl.addListener( this ); + + /* Add frequency control as listener on the controller, to maintain + * sync with the tuned frequency value */ + mController.addListener( mFrequencyControl ); + + mFrequencyControl.setFrequency( mController.getFrequency(), false ); + + add( mFrequencyControl, "span 2, align center, wrap" ); + + JideTabbedPane tabs = new JideTabbedPane(); + tabs.setFont( this.getFont() ); + tabs.setForeground( Color.BLACK ); + + tabs.addTab( "Configuration", + new E4KTunerConfigurationPanel( mResourceManager, mController ) ); + + tabs.addTab( "Info", new RTL2832InfoPanel( mController ) ); + + add( tabs, "grow,push,span" ); + } + + /** + * Frequency change listener for the frequency control gui component to + * apply end-user requested frequency changes against the tuner + */ + @Override + public void frequencyChanged( final int frequency, int bandwidth ) + { + EventQueue.invokeLater( new Runnable() + { + @Override + public void run() + { + try + { + mController.setFrequency( frequency ); + } + catch ( SourceException e ) + { + Log.error( "E4KTunerController - error setting frequency [" + + frequency + "] - " + e.getLocalizedMessage() ); + } + } + } ); + } +} diff --git a/src/source/tuner/rtl/r820t/R820TTunerConfiguration.java b/src/source/tuner/rtl/r820t/R820TTunerConfiguration.java new file mode 100644 index 00000000..3b53e875 --- /dev/null +++ b/src/source/tuner/rtl/r820t/R820TTunerConfiguration.java @@ -0,0 +1,123 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package source.tuner.rtl.r820t; + +import javax.xml.bind.annotation.XmlAttribute; + +import source.tuner.TunerConfiguration; +import source.tuner.TunerType; +import source.tuner.rtl.RTL2832TunerController.SampleRate; +import source.tuner.rtl.r820t.R820TTunerController.R820TEnhanceGain; +import source.tuner.rtl.r820t.R820TTunerController.R820TGain; +import source.tuner.rtl.r820t.R820TTunerController.R820TLNAGain; +import source.tuner.rtl.r820t.R820TTunerController.R820TMixerGain; + +public class R820TTunerConfiguration extends TunerConfiguration +{ + private R820TGain mMasterGain = R820TGain.AUTOMATIC; + private R820TMixerGain mMixerGain = R820TMixerGain.AUTOMATIC; + private R820TLNAGain mLNAGain = R820TLNAGain.AUTOMATIC; + private R820TEnhanceGain mEnhanceGain = R820TEnhanceGain.AUTOMATIC; + private double mFrequencyCorrection = -12.0d; + private SampleRate mSampleRate = SampleRate.RATE_1_200MHZ; + + /** + * Default constructor for JAXB + */ + public R820TTunerConfiguration() + { + this( "Default" ); + } + + public R820TTunerConfiguration( String name ) + { + super( name ); + } + + @Override + public TunerType getTunerType() + { + return TunerType.ELONICS_E4000; + } + + @XmlAttribute( name = "master_gain" ) + public R820TGain getMasterGain() + { + return mMasterGain; + } + + public void setMasterGain( R820TGain gain ) + { + mMasterGain = gain; + } + + @XmlAttribute( name = "mixer_gain" ) + public R820TMixerGain getMixerGain() + { + return mMixerGain; + } + + public void setMixerGain( R820TMixerGain mixerGain ) + { + mMixerGain = mixerGain; + } + + @XmlAttribute( name = "lna_gain" ) + public R820TLNAGain getLNAGain() + { + return mLNAGain; + } + + public void setLNAGain( R820TLNAGain lnaGain ) + { + mLNAGain = lnaGain; + } + + @XmlAttribute( name = "enhance_gain" ) + public R820TEnhanceGain getEnhanceGain() + { + return mEnhanceGain; + } + + public void setEnhanceGain( R820TEnhanceGain enhanceGain ) + { + mEnhanceGain = enhanceGain; + } + + @XmlAttribute( name = "frequency_correction" ) + public double getFrequencyCorrection() + { + return mFrequencyCorrection; + } + + public void setFrequencyCorrection( double value ) + { + mFrequencyCorrection = value; + } + + @XmlAttribute( name = "sample_rate" ) + public SampleRate getSampleRate() + { + return mSampleRate; + } + + public void setSampleRate( SampleRate sampleRate ) + { + mSampleRate = sampleRate; + } +} diff --git a/src/source/tuner/rtl/r820t/R820TTunerConfigurationPanel.java b/src/source/tuner/rtl/r820t/R820TTunerConfigurationPanel.java new file mode 100644 index 00000000..6cee8f22 --- /dev/null +++ b/src/source/tuner/rtl/r820t/R820TTunerConfigurationPanel.java @@ -0,0 +1,619 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package source.tuner.rtl.r820t; + +import java.awt.EventQueue; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.awt.event.FocusEvent; +import java.awt.event.FocusListener; +import java.text.DecimalFormat; +import java.util.ArrayList; + +import javax.swing.BorderFactory; +import javax.swing.ComboBoxModel; +import javax.swing.DefaultComboBoxModel; +import javax.swing.JButton; +import javax.swing.JComboBox; +import javax.swing.JLabel; +import javax.swing.JOptionPane; +import javax.swing.JPanel; +import javax.swing.JSpinner; +import javax.swing.JTextField; +import javax.swing.SpinnerModel; +import javax.swing.SpinnerNumberModel; +import javax.swing.SwingConstants; +import javax.swing.event.ChangeEvent; +import javax.swing.event.ChangeListener; +import javax.usb.UsbException; + +import log.Log; +import net.miginfocom.swing.MigLayout; +import source.SourceException; +import source.tuner.TunerConfiguration; +import source.tuner.TunerConfigurationAssignment; +import source.tuner.TunerType; +import source.tuner.rtl.RTL2832TunerController.SampleRate; +import source.tuner.rtl.r820t.R820TTunerController.R820TEnhanceGain; +import source.tuner.rtl.r820t.R820TTunerController.R820TGain; +import source.tuner.rtl.r820t.R820TTunerController.R820TLNAGain; +import source.tuner.rtl.r820t.R820TTunerController.R820TMixerGain; +import controller.ResourceManager; + +public class R820TTunerConfigurationPanel extends JPanel +{ + private static final long serialVersionUID = 1L; + private ResourceManager mResourceManager; + private R820TTunerController mController; + private R820TTunerConfiguration mSelectedConfig; + + private JButton mNewConfiguration; + private JButton mDeleteConfiguration; + private JComboBox mComboConfigurations; + + private JTextField mName; + + private JSpinner mFrequencyCorrection; + private JComboBox mComboMasterGain; + private JComboBox mComboMixerGain; + private JComboBox mComboLNAGain; + private JComboBox mComboEnhanceGain; + + private JComboBox mComboSampleRate; + + public R820TTunerConfigurationPanel( ResourceManager resourceManager, + R820TTunerController controller ) + { + mResourceManager = resourceManager; + mController = controller; + + init(); + } + + /** + * Initializes the gui components + */ + private void init() + { + setLayout( new MigLayout( "", "[grow,fill]", "[grow,fill]" ) ); + + setBorder( BorderFactory.createTitledBorder( "Tuner Configuration" ) ); + + /** + * Tuner configuration selector + */ + mComboConfigurations = new JComboBox(); + + mComboConfigurations.setModel( getModel() ); + + /* Determine which tuner configuration should be selected/displayed */ + TunerConfigurationAssignment savedConfig = mResourceManager + .getSettingsManager().getSelectedTunerConfiguration( + TunerType.RAFAELMICRO_R820T, mController.getUniqueID() ); + + if( savedConfig != null ) + { + mSelectedConfig = getNamedConfiguration( + savedConfig.getTunerConfigurationName() ); + } + + /* If we couldn't determine the saved/selected config, use the first one */ + if( mSelectedConfig == null ) + { + mSelectedConfig = mComboConfigurations.getItemAt( 0 ); + + //Store this config as the default for this tuner at this address + mResourceManager.getSettingsManager() + .setSelectedTunerConfiguration( + TunerType.RAFAELMICRO_R820T, + mController.getUniqueID(), mSelectedConfig ); + } + + mComboConfigurations.setSelectedItem( mSelectedConfig ); + + mComboConfigurations.addActionListener( new ActionListener() + { + @Override + public void actionPerformed( ActionEvent e ) + { + R820TTunerConfiguration selected = + (R820TTunerConfiguration)mComboConfigurations + .getItemAt( mComboConfigurations.getSelectedIndex() ); + + if( selected != null ) + { + update( selected ); + } + } + }); + + add( mComboConfigurations, "span 2,wrap" ); + + + /** + * Tuner Configuration Name + */ + mName = new JTextField(); + mName.setText( mSelectedConfig.getName() ); + + mName.addFocusListener( new FocusListener() + { + @Override + public void focusLost( FocusEvent e ) + { + mSelectedConfig.setName( mName.getText() ); + save(); + } + @Override + public void focusGained( FocusEvent e ) {} + } ); + + add( mName ); + add( new JLabel( "Name" ), "wrap" ); + + /** + * Frequency Correction + */ + SpinnerModel model = + new SpinnerNumberModel( 0.0, //initial value + -1000.0, //min + 1000.0, //max + 0.1 ); //step + + mFrequencyCorrection = new JSpinner( model ); + + JSpinner.NumberEditor editor = + (JSpinner.NumberEditor)mFrequencyCorrection.getEditor(); + + DecimalFormat format = editor.getFormat(); + format.setMinimumFractionDigits( 1 ); + editor.getTextField().setHorizontalAlignment( SwingConstants.CENTER ); + + mFrequencyCorrection.setValue( mSelectedConfig.getFrequencyCorrection() ); + + mFrequencyCorrection.addChangeListener( new ChangeListener() + { + @Override + public void stateChanged( ChangeEvent e ) + { + final double value = ((SpinnerNumberModel)mFrequencyCorrection + .getModel()).getNumber().doubleValue(); + + EventQueue.invokeLater( new Runnable() + { + @Override + public void run() + { + try + { + mController.setFrequencyCorrection( value ); + mSelectedConfig.setFrequencyCorrection( value ); + save(); + } + catch ( SourceException e1 ) + { + JOptionPane.showMessageDialog( + R820TTunerConfigurationPanel.this, + "E4K Tuner Controller - couldn't apply " + + "frequency correction value: " + value + + e1.getLocalizedMessage() ); + + Log.error( "E4K Tuner Controller - couldn't apply " + + "frequency correction value: " + value + + e1.getLocalizedMessage() ); + } + } + } ); + } + } ); + + add( mFrequencyCorrection ); + add( new JLabel( "Frequency Correction (ppm)" ), "wrap" ); + + /** + * Sample Rate + */ + mComboSampleRate = new JComboBox<>( SampleRate.values() ); + mComboSampleRate.setSelectedItem( mSelectedConfig.getSampleRate() ); + mComboSampleRate.addActionListener( new ActionListener() + { + @Override + public void actionPerformed( ActionEvent e ) + { + SampleRate sampleRate = + (SampleRate)mComboSampleRate.getSelectedItem(); + try + { + + mController.setSampleRate( sampleRate ); + + mSelectedConfig.setSampleRate( sampleRate ); + save(); + } + catch ( SourceException | UsbException eSampleRate ) + { + JOptionPane.showMessageDialog( + R820TTunerConfigurationPanel.this, + "E4K Tuner Controller - couldn't apply the sample " + + "rate setting [" + sampleRate.getLabel() + "] " + + eSampleRate.getLocalizedMessage() ); + + Log.error( "E4K Tuner Controller - couldn't apply sample " + + "rate setting [" + sampleRate.getLabel() + "] " + + eSampleRate.getLocalizedMessage() ); + } + } + } ); + add( mComboSampleRate ); + add( new JLabel( "Sample Rate" ), "wrap" ); + + /** + * Gain Controls + */ + JPanel gainPanel = new JPanel(); + gainPanel.setLayout( new MigLayout( "", "[grow,fill]", "[grow,fill]" ) ); + gainPanel.setBorder( BorderFactory.createTitledBorder( "Gain" ) ); + + /* Master Gain Control */ + mComboMasterGain = new JComboBox( R820TGain.values() ); + mComboMasterGain.setSelectedItem( mSelectedConfig.getMasterGain() ); + + mComboMasterGain.addActionListener( new ActionListener() + { + @Override + public void actionPerformed( ActionEvent arg0 ) + { + try + { + R820TGain gain = (R820TGain)mComboMasterGain.getSelectedItem(); + + mController.setGain( (R820TGain)mComboMasterGain.getSelectedItem(), true ); + + if( gain == R820TGain.MANUAL ) + { + mComboMixerGain.setSelectedItem( + mController.getMixerGain( true ) ); + mComboMixerGain.setEnabled( true ); + + mComboLNAGain.setSelectedItem( + mController.getLNAGain( true ) ); + mComboLNAGain.setEnabled( true ); + + mComboEnhanceGain.setEnabled( true ); + mComboEnhanceGain.setSelectedItem( + mController.getEnhanceGain( true ) ); + } + else + { + mComboMixerGain.setEnabled( false ); + mComboMixerGain.setSelectedItem( gain.getMixerGain() ); + + mComboLNAGain.setEnabled( false ); + mComboLNAGain.setSelectedItem( gain.getLNAGain() ); + + mComboEnhanceGain.setEnabled( false ); + mComboEnhanceGain.setSelectedItem( gain.getEnhanceGain() ); + } + + mSelectedConfig.setMasterGain( gain ); + save(); + } + catch ( UsbException e ) + { + JOptionPane.showMessageDialog( + R820TTunerConfigurationPanel.this, + "E4K Tuner Controller - couldn't apply the gain " + + "setting - " + e.getLocalizedMessage() ); + + Log.error( "E4K Tuner Controller - couldn't apply " + + "gain setting - " + e.getLocalizedMessage() ); + } + } + } ); + mComboMasterGain.setToolTipText( "Select AUTOMATIC for auto " + + "gain, MANUAL to enable
independent control of " + + "Mixer, LNA and Enhance gain
" + + "settings, or one of the individual gain settings for
" + + "semi-manual gain control" ); + gainPanel.add( mComboMasterGain ); + gainPanel.add( new JLabel( "Master" ), "wrap" ); + + /* Mixer Gain Control */ + mComboMixerGain = new JComboBox( R820TMixerGain.values() ); + mComboMixerGain.setSelectedItem( mSelectedConfig.getMixerGain() ); + mComboMixerGain.addActionListener( new ActionListener() + { + @Override + public void actionPerformed( ActionEvent arg0 ) + { + EventQueue.invokeLater( new Runnable() + { + @Override + public void run() + { + try + { + R820TMixerGain mixerGain = + (R820TMixerGain)mComboMixerGain.getSelectedItem(); + + if( mComboMixerGain.isEnabled() ) + { + mController.setMixerGain( mixerGain, true ); + } + + mSelectedConfig.setMixerGain( mixerGain ); + save(); + } + catch ( UsbException e ) + { + JOptionPane.showMessageDialog( + R820TTunerConfigurationPanel.this, + "E4K Tuner Controller - couldn't apply the mixer " + + "gain setting - " + e.getLocalizedMessage() ); + + Log.error( "E4K Tuner Controller - couldn't apply mixer " + + "gain setting - " + e.getLocalizedMessage() ); + } + } + } ); + } + } ); + mComboMixerGain.setToolTipText( "Mixer Gain. Set master gain " + + "to MASTER to enable adjustment" ); + mComboMixerGain.setEnabled( false ); + gainPanel.add( mComboMixerGain ); + gainPanel.add( new JLabel( "Mixer" ), "wrap" ); + + /* LNA Gain Control */ + mComboLNAGain = new JComboBox( R820TLNAGain.values() ); + mComboLNAGain.setSelectedItem( mSelectedConfig.getLNAGain() ); + mComboLNAGain.addActionListener( new ActionListener() + { + @Override + public void actionPerformed( ActionEvent arg0 ) + { + try + { + R820TLNAGain lnaGain = (R820TLNAGain)mComboLNAGain.getSelectedItem(); + + if( mComboLNAGain.isEnabled() ) + { + mController.setLNAGain( lnaGain, true ); + } + + mSelectedConfig.setLNAGain( lnaGain ); + save(); + } + catch ( UsbException e ) + { + JOptionPane.showMessageDialog( + R820TTunerConfigurationPanel.this, + "E4K Tuner Controller - couldn't apply the LNA " + + "gain setting - " + e.getLocalizedMessage() ); + + Log.error( "E4K Tuner Controller - couldn't apply LNA " + + "gain setting - " + e.getLocalizedMessage() ); + } + } + } ); + mComboLNAGain.setToolTipText( "LNA Gain. Set master gain " + + "to MANUAL to enable adjustment" ); + mComboLNAGain.setEnabled( false ); + gainPanel.add( mComboLNAGain ); + gainPanel.add( new JLabel( "LNA" ), "wrap" ); + + /* Enhance Gain Control */ + mComboEnhanceGain = new JComboBox( R820TEnhanceGain.values() ); + mComboEnhanceGain.setSelectedItem( mSelectedConfig.getEnhanceGain() ); + mComboEnhanceGain.addActionListener( new ActionListener() + { + @Override + public void actionPerformed( ActionEvent arg0 ) + { + try + { + R820TEnhanceGain enhanceGain = + (R820TEnhanceGain)mComboEnhanceGain.getSelectedItem(); + + if( mComboEnhanceGain.isEnabled() ) + { + mController.setEnhanceGain( enhanceGain, true ); + } + + mSelectedConfig.setEnhanceGain( enhanceGain ); + save(); + } + catch ( UsbException e ) + { + JOptionPane.showMessageDialog( + R820TTunerConfigurationPanel.this, + "E4K Tuner Controller - couldn't apply the enhance " + + "gain setting - " + e.getLocalizedMessage() ); + + Log.error( "E4K Tuner Controller - couldn't apply enhance " + + "gain setting - " + e.getLocalizedMessage() ); + } + } + } ); + mComboEnhanceGain.setToolTipText( "Enhance Gain. Set master gain " + + "to MANUAL to enable adjustment" ); + mComboEnhanceGain.setEnabled( false ); + gainPanel.add( mComboEnhanceGain ); + gainPanel.add( new JLabel( "Enhance" ), "wrap" ); + + add( gainPanel, "span" ); + + /** + * Create a new configuration + */ + mNewConfiguration = new JButton( "New" ); + + mNewConfiguration.addActionListener( new ActionListener() + { + @Override + public void actionPerformed( ActionEvent e ) + { + TunerConfiguration config = + mResourceManager.getSettingsManager() + .addNewTunerConfiguration( + TunerType.ELONICS_E4000, + "New Configuration" ); + + mComboConfigurations.setModel( getModel() ); + + mComboConfigurations.setSelectedItem( config ); + + repaint(); + } + } ); + + add( mNewConfiguration ); + + /** + * Delete the currently selected configuration + */ + mDeleteConfiguration = new JButton( "Delete" ); + + mDeleteConfiguration.addActionListener( new ActionListener() + { + @Override + public void actionPerformed( ActionEvent e ) + { + R820TTunerConfiguration selected = + (R820TTunerConfiguration)mComboConfigurations + .getItemAt( mComboConfigurations.getSelectedIndex() ); + + if( selected != null ) + { + int n = JOptionPane.showConfirmDialog( + R820TTunerConfigurationPanel.this, + "Are you sure you want to delete '" + + selected.getName() + "'?", + "Are you sure?", + JOptionPane.YES_NO_OPTION ); + + if( n == JOptionPane.YES_OPTION ) + { + mResourceManager.getSettingsManager() + .deleteTunerConfiguration( selected ); + + mComboConfigurations.setModel( getModel() ); + + mComboConfigurations.setSelectedIndex( 0 ); + + repaint(); + } + } + } + } ); + + add( mDeleteConfiguration, "wrap" ); + } + + /** + * Updates gui controls with the values from the tuner configuration + * @param config - tuner configuration + */ + private void update( R820TTunerConfiguration config ) + { + mSelectedConfig = config; + + try + { + mController.apply( config ); + + mName.setText( config.getName() ); + mFrequencyCorrection.setValue( config.getFrequencyCorrection() ); + + mComboMixerGain.setSelectedItem( mSelectedConfig.getMixerGain() ); + mComboLNAGain.setSelectedItem( mSelectedConfig.getLNAGain() ); + mComboEnhanceGain.setSelectedItem( mSelectedConfig.getEnhanceGain() ); + + /* Apply master gain last so that the Mixer, LNA, and Enhance gain + * settings are updated with the master setting where necessary */ + mComboMasterGain.setSelectedItem( mSelectedConfig.getMasterGain() ); + + mComboSampleRate.setSelectedItem( mSelectedConfig.getSampleRate() ); + + mResourceManager.getSettingsManager().setSelectedTunerConfiguration( + TunerType.ELONICS_E4000, mController.getUniqueID(), config ); + } + catch ( SourceException e1 ) + { + JOptionPane.showMessageDialog( + R820TTunerConfigurationPanel.this, + "E4K Tuner Controller - couldn't " + + "apply the tuner configuration settings - " + + e1.getLocalizedMessage() ); + + Log.error( "E4K Tuner Controller - couldn't apply " + + "config [" + config.getName() + "] - " + + e1.getLocalizedMessage() ); + } + } + + /** + * Constructs a combo box model for the tuner configuration combo component + * by assembling a list of the tuner configurations appropriate for this + * specific tuner + */ + private ComboBoxModel getModel() + { + ArrayList configs = + mResourceManager.getSettingsManager() + .getTunerConfigurations( TunerType.RAFAELMICRO_R820T ); + + DefaultComboBoxModel model = + new DefaultComboBoxModel(); + + for( TunerConfiguration config: configs ) + { + model.addElement( (R820TTunerConfiguration)config ); + } + + return model; + } + + /** + * Retrieves a specific (named) tuner configuration + */ + private R820TTunerConfiguration getNamedConfiguration( String name ) + { + ArrayList configs = + mResourceManager.getSettingsManager() + .getTunerConfigurations( TunerType.RAFAELMICRO_R820T ); + + for( TunerConfiguration config: configs ) + { + if( config.getName().contentEquals( name ) ) + { + return (R820TTunerConfiguration)config; + } + } + + return null; + } + + /** + * Saves current tuner configuration settings + */ + private void save() + { + mResourceManager.getSettingsManager().save(); + } +} diff --git a/src/source/tuner/rtl/r820t/R820TTunerController.java b/src/source/tuner/rtl/r820t/R820TTunerController.java new file mode 100644 index 00000000..dc0a3e2e --- /dev/null +++ b/src/source/tuner/rtl/r820t/R820TTunerController.java @@ -0,0 +1,2408 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package source.tuner.rtl.r820t; + +import javax.swing.JPanel; +import javax.usb.UsbException; + +import log.Log; +import source.SourceException; +import source.tuner.TunerConfiguration; +import source.tuner.TunerType; +import source.tuner.rtl.RTL2832TunerController; +import source.tuner.usb.USBTunerDevice; +import controller.ResourceManager; + +public class R820TTunerController extends RTL2832TunerController +{ + /** + * Note: E4K tuner max frequency range is 2200 MHz, but the + * max value that can be assigned to a signed integer is + * 2147xxxxxx, so we constrain the E4K to the max integer value, for now + * + * TODO: change frequency to a long primitive type + */ + public static final int sMIN_FREQUENCY = 52000000; + public static final int sMIN_FREQUENCY_EXTENDED = 52000000; + public static final int sMAX_FREQUENCY = 2147000000; + public static final int sMAX_FREQUENCY_EXTENDED = 2147000000; + public static final long sMIN_VCO = 2600000000l; //2.6 GHz + public static final long sMAX_VCO = 3900000000l; //3.9 GHz + + /* The local oscillator is defined by whole (integer) units of the oscillator + * frequency and fractional units representing 1/65536th of the oscillator + * frequency, meaning we can only tune the local oscillator in units of + * 439.453125 hertz. */ + public static final long sE4K_PLL_Y = 65536l; /* 16-bit fractional register */ + public static final byte MASTER1_RESET = (byte)0x01; + public static final byte MASTER1_NORM_STBY = (byte)0x02; + public static final byte MASTER1_POR_DET = (byte)0x04; + + public static final byte SYNTH1_PLL_LOCK = (byte)0x01; + public static final byte SYNTH1_BAND_SHIF = (byte)0x01; + + public static final byte SYNTH7_3PHASE_EN = (byte)0x08; + + public static final byte SYNTH8_VCOCAL_UPD = (byte)0x04; + + public static final byte FILT3_MASK = (byte)0x20; + public static final byte FILT3_ENABLE = (byte)0x00; + public static final byte FILT3_DISABLE = (byte)0x20; + + public static final byte MIXER_FILTER_MASK = (byte)0xF0; + public static final byte IF_CHANNEL_FILTER_MASK = (byte)0x1F; + public static final byte IF_RC_FILTER_MASK = (byte)0x0F; + + + public static final byte AGC1_LIN_MODE = (byte)0x10; + public static final byte AGC1_LNA_UPDATE = (byte)0x20; + public static final byte AGC1_LNA_G_LOW = (byte)0x40; + public static final byte AGC1_LNA_G_HIGH = (byte)0x80; + public static final byte AGC1_MOD_MASK = (byte)0xF; + + public static final byte GAIN1_MOD_MASK = (byte)0xF; + public static final byte IF_GAIN_MODE_SWITCHING_MASK = (byte)0x1; + + public static final byte AGC6_LNA_CAL_REQ = (byte)0x10; + + public static final byte AGC7_MIXER_GAIN_MASK = (byte)0x01; + public static final byte AGC7_MIX_GAIN_MANUAL = (byte)0x00; + public static final byte AGC7_MIX_GAIN_AUTO = (byte)0x01; + public static final byte AGC7_GAIN_STEP_5DB = (byte)0x20; + + public static final byte AGC8_SENS_LIN_AUTO = (byte)0x01; + + public static final byte AGC11_LNA_GAIN_ENH = (byte)0x01; + public static final byte ENH_GAIN_MOD_MASK = (byte)0x07; + public static final byte MIXER_GAIN_MASK = (byte)0x01; + + public static final byte DC1_CAL_REQ = (byte)0x01; + + public static final byte DC5_I_LUT_EN = (byte)0x01; + public static final byte DC5_Q_LUT_EN = (byte)0x02; + public static final byte DC5_RANGE_DETECTOR_ENABLED_MASK = (byte)0x04; //DC Offset Detector Enabled + public static final byte DC5_RANGE_DETECTOR_ENABLED = (byte)0x04; //DC Offset Detector Enabled + public static final byte DC5_RANGE_EN = (byte)0x08; + public static final byte DC5_RANGE_DETECTOR_DISABLED_MASK = (byte)0x03; + public static final byte DC5_RANGE_DETECTOR_DISABLED = (byte)0x00; + + public static final byte DC5_TIMEVAR_EN = (byte)0x10; + + public static final byte CLKOUT_DISABLE = (byte)0x96; + + public static final byte CHFCALIB_CMD = (byte)0x01; + + public R820TTunerController( USBTunerDevice device ) throws SourceException + { + super( device, sMIN_FREQUENCY, sMAX_FREQUENCY ); + } + + @Override + public TunerType getTunerType() + { + return TunerType.RAFAELMICRO_R820T; + } + + public void apply( R820TTunerConfiguration config ) throws SourceException + { + //TODO: apply the config settings + } + + public void init() throws SourceException + { + try + { + /* Dummy write to test USB interface */ + writeRegister( mUSBDevice, Block.USB, + Address.USB_SYSCTL.getAddress(), (short)0x09, 1 ); + + initBaseband( mUSBDevice ); + + enableI2CRepeater( mUSBDevice, true ); + + boolean i2CRepeaterControl = false; + + initTuner( i2CRepeaterControl ); + + enableI2CRepeater( mUSBDevice, false ); + } + catch( UsbException e ) + { + e.printStackTrace(); + throw new SourceException( "E4K Tuner Controller - error during " + + "init()", e ); + } + } + + public JPanel getEditor( ResourceManager resourceManager ) + { + return new R820TTunerEditorPanel( this, resourceManager ); + } + + /** + * Sets the IF filters (mixer, channel and RC) to the correct filter setting + * for the selected bandwidth/sample rate + */ + @Override + public void setSampleRateFilters( int bandwidth ) throws UsbException + { + /* Determine repeater state so we can restore it when done */ + boolean i2CRepeaterEnabled = isI2CRepeaterEnabled(); + + if( !i2CRepeaterEnabled ) + { + enableI2CRepeater( mUSBDevice, true ); + } + + boolean controlI2CRepeater = false; + + MixerFilter mixer = MixerFilter.getFilter( bandwidth ); + setMixerFilter( mixer, controlI2CRepeater ); + + ChannelFilter channel = ChannelFilter.getFilter( bandwidth ); + setChannelFilter( channel, controlI2CRepeater ); + + RCFilter rc = RCFilter.getFilter( bandwidth ); + setRCFilter( rc, controlI2CRepeater ); + + if( !i2CRepeaterEnabled ) + { + enableI2CRepeater( mUSBDevice, false ); + } + } + + @Override + public int getTunedFrequency() throws SourceException + { + try + { + /* Determine repeater state so we can restore it when done */ + boolean i2CRepeaterEnabled = isI2CRepeaterEnabled(); + + if( !i2CRepeaterEnabled ) + { + enableI2CRepeater( mUSBDevice, true ); + } + + boolean controlI2CRepeater = false; + + byte z = (byte)readE4KRegister( Register.SYNTH3, controlI2CRepeater ); + + int xHigh = readE4KRegister( Register.SYNTH4, controlI2CRepeater ); + int xLow = readE4KRegister( Register.SYNTH5, controlI2CRepeater ); + + int x = ( Integer.rotateLeft( xHigh, 8 ) ) | xLow; + + int pllSetting = readE4KRegister( Register.SYNTH7, controlI2CRepeater ); + PLL pll = PLL.fromSetting( pllSetting ); + + /* Return the repeater to its previous state */ + if( !i2CRepeaterEnabled ) + { + enableI2CRepeater( mUSBDevice, false ); + } + + return calculateActualFrequency( pll, z, x ); + } + catch( UsbException e ) + { + throw new SourceException( "E4K tuner controller - couldn't get " + + "tuned frequency", e ); + } + } + + @Override + public void setTunedFrequency( int frequency ) throws SourceException + { + /* Get the phase locked loop setting */ + PLL pll = PLL.fromFrequency( frequency ); + + /* Z is an integer representing the number of scaled oscillator frequency + * increments the multiplied frequency. */ + byte z = (byte)( frequency / pll.getScaledOscillator() ); + + /* remainder is just as it describes. It is what is left over after we + * carve out scaled oscillator frequency increments (z) from the desired + * frequency. */ + int remainder = frequency - ( ( z & 0xFF ) * pll.getScaledOscillator() ); + + /* X is a 16-bit representation of the remainder */ + int x = (int)( (double)remainder / (double)pll.getScaledOscillator() * sE4K_PLL_Y ); + + /* Calculate the exact (tunable) frequency and apply that to the tuner */ + int actualFrequency = calculateActualFrequency( pll, z, x ); + + /* Apply the actual frequency */ + try + { +// /* Determine repeater state so we can restore it when done */ +// boolean i2CRepeaterEnabled = isI2CRepeaterEnabled(); +// +// if( !i2CRepeaterEnabled ) +// { + enableI2CRepeater( mUSBDevice, true ); +// } + + boolean controlI2CRepeater = false; + + /* Write the PLL setting */ + writeE4KRegister( Register.SYNTH7, pll.getIndex(), controlI2CRepeater ); + + /* Write z (integral) value */ + writeE4KRegister( Register.SYNTH3, z, controlI2CRepeater ); + + /* Write the x (fractional) value high-order byte to synth4 register */ + writeE4KRegister( Register.SYNTH4, (byte)( x & 0xFF ), controlI2CRepeater ); + + /* Write the x (fractional) value low-order byte to synth5 register */ + writeE4KRegister( Register.SYNTH5, + (byte)( ( Integer.rotateRight( x, 8 ) ) & 0xFF ), + controlI2CRepeater ); + + /* Set the band for the new frequency */ + setBand( actualFrequency, controlI2CRepeater ); + + /* Set the filter */ + setRFFilter( actualFrequency, controlI2CRepeater ); + + /* Check for PLL lock */ + int lock = readE4KRegister( Register.SYNTH1, controlI2CRepeater ); + + if( !( ( lock & 0x1 ) == 0x1 ) ) + { + throw new SourceException( "E4K tuner controller - couldn't " + + "achieve PLL lock for frequency [" + + actualFrequency + "]" ); + } + + /* Return the repeater to its previous state */ +// if( !i2CRepeaterEnabled ) +// { + enableI2CRepeater( mUSBDevice, false ); +// } + } + catch( UsbException e ) + { + throw new SourceException( "E4K tuner controller - error tuning " + + "frequency [" + frequency + "]", e ); + } + } + + private int calculateActualFrequency( PLL pll, byte z, int x ) + { + int whole = (int)( pll.getScaledOscillator() * ( z & 0xFF ) ); + + int fractional = (int)( pll.getScaledOscillator() * + ( (double)x / (double)sE4K_PLL_Y ) ); + + return whole + fractional; + } + + public void initTuner( boolean controlI2CRepeater ) throws UsbException + { + if( controlI2CRepeater ) + { + enableI2CRepeater( mUSBDevice, true ); + } + + boolean i2CRepeaterControl = false; + + /* Perform dummy read */ + readE4KRegister( Register.DUMMY, i2CRepeaterControl ); + + /* Reset everything and clear POR indicator */ + /* NOTE: register value remains 0010 even after we write 0111 to it */ + writeE4KRegister( Register.MASTER1, (byte)( MASTER1_RESET | + MASTER1_NORM_STBY | + MASTER1_POR_DET ), + i2CRepeaterControl ); + + /* Configure clock input */ + writeE4KRegister( Register.CLK_INP, (byte)0x00, i2CRepeaterControl ); + + /* Disable clock output */ + writeE4KRegister( Register.REF_CLK, (byte)0x00, i2CRepeaterControl ); + writeE4KRegister( Register.CLKOUT_PWDN, (byte)0x96, i2CRepeaterControl ); + + /* Magic Init */ + magicInit( i2CRepeaterControl ); + +// /* Set common mode voltage a bit higher for more margin - 850 mv */ +// writeMaskedE4KRegister( Register.DC7, (byte)0x7, +// (byte)0x4, i2CRepeaterControl ); +// +// /* Initialize DC offset lookup tables */ +// generateDCOffsetTables( i2CRepeaterControl ); +// +// /* Enable time variant DC correction */ +// writeE4KRegister( Register.DCTIME1, (byte)0x01, i2CRepeaterControl ); +// writeE4KRegister( Register.DCTIME2, (byte)0x01, i2CRepeaterControl ); +// + /* Set LNA Mode */ + writeE4KRegister( Register.AGC4, (byte)0x10, i2CRepeaterControl ); //High Threshold + writeE4KRegister( Register.AGC5, (byte)0x04, i2CRepeaterControl ); //Low Threshold + writeE4KRegister( Register.AGC6, (byte)0x1A, i2CRepeaterControl ); //LNA calibrate + loop rate + + //TODO: lna and mixer gain were already set to manual when we generated + //the DC offset tables. Check the LNA gain setting after we set the + //LNA mode above, to see if the lna or mixer gain settings have changed + //and if not, remove the next two steps + +// /* Set LNA gain to manual using current auto LNA gain setting */ +// LNAGain lnaGain = getLNAGain( i2CRepeaterControl ); +// setLNAGain( lnaGain, i2CRepeaterControl ); +// +// /* Set Mixer gain to manual using current auto Mixer gain setting */ +// MixerGain mixerGain = getMixerGain( i2CRepeaterControl ); +// setMixerGain( mixerGain, i2CRepeaterControl ); + + //Temp - manual set lna mode to manual + writeMaskedE4KRegister( Register.AGC1, + AGC1_MOD_MASK, + AGCMode.SERIAL.getValue(), + false ); + //Temp - set mixer gain to manual + writeMaskedE4KRegister( Register.AGC7, + AGC7_MIXER_GAIN_MASK, + AGC7_MIX_GAIN_MANUAL, + false ); + + +// /* Enable enhance gain */ +// setEnhanceGain( EnhanceGain.GAIN_5, i2CRepeaterControl ); +// +// /* Enable automatic IF gain mode switching */ +// writeMaskedE4KRegister( Register.AGC8, +// IF_GAIN_MODE_SWITCHING_MASK, +// AGC8_SENS_LIN_AUTO, +// i2CRepeaterControl ); + + + /* Use automatic gain as a default */ + setLNAGain( R820TLNAGain.AUTOMATIC, i2CRepeaterControl ); + setMixerGain( R820TMixerGain.AUTOMATIC, i2CRepeaterControl ); + setEnhanceGain( R820TEnhanceGain.AUTOMATIC, i2CRepeaterControl ); + + /* Set IF gain stages */ + setIFStage1Gain( IFStage1Gain.GAIN_PLUS6, i2CRepeaterControl ); + setIFStage2Gain( IFStage2Gain.GAIN_PLUS0, i2CRepeaterControl ); + setIFStage3Gain( IFStage3Gain.GAIN_PLUS0, i2CRepeaterControl ); + setIFStage4Gain( IFStage4Gain.GAIN_PLUS0, i2CRepeaterControl ); + setIFStage5Gain( IFStage5Gain.GAIN_PLUS9, i2CRepeaterControl ); + setIFStage6Gain( IFStage6Gain.GAIN_PLUS9, i2CRepeaterControl ); + + /* Set the most narrow filter we can possible use */ + setMixerFilter( MixerFilter.BW_1M9, i2CRepeaterControl ); + setRCFilter( RCFilter.BW_1M0, i2CRepeaterControl ); + setChannelFilter( ChannelFilter.BW_2M15, i2CRepeaterControl ); + + setChannelFilterEnabled( true, i2CRepeaterControl ); + +// /* Disable DC detector */ +// setDCRangeDetectorEnabled( false, i2CRepeaterControl ); + + /* Disable time variant DC correction */ + writeMaskedE4KRegister( Register.DC5, (byte)0x03, (byte)0, i2CRepeaterControl ); + writeMaskedE4KRegister( Register.DCTIME1, (byte)0x03, (byte)0, i2CRepeaterControl ); + writeMaskedE4KRegister( Register.DCTIME2, (byte)0x03, (byte)0, i2CRepeaterControl ); + + if( controlI2CRepeater ) + { + enableI2CRepeater( mUSBDevice, false ); + } + } + + private void generateDCOffsetTables( boolean controlI2CRepeater ) + throws UsbException + { + boolean i2CRepeaterControl = false; + + if( controlI2CRepeater ) + { + enableI2CRepeater( mUSBDevice, true ); + } + + /* Capture current gain settings to reapply at the end */ + IFStage1Gain stage1 = getIFStage1Gain( i2CRepeaterControl ); + IFStage2Gain stage2 = getIFStage2Gain( i2CRepeaterControl ); + IFStage3Gain stage3 = getIFStage3Gain( i2CRepeaterControl ); + IFStage4Gain stage4 = getIFStage4Gain( i2CRepeaterControl ); + IFStage5Gain stage5 = getIFStage5Gain( i2CRepeaterControl ); + IFStage6Gain stage6 = getIFStage6Gain( i2CRepeaterControl ); + + /* Set Mixer gain to manual using current auto Mixer gain setting */ + R820TMixerGain mixerGain = getMixerGain( i2CRepeaterControl ); + setMixerGain( mixerGain, i2CRepeaterControl ); + + /* Set LNA gain to manual using current auto LNA gain setting */ + R820TLNAGain lnaGain = getLNAGain( i2CRepeaterControl ); + setLNAGain( lnaGain, i2CRepeaterControl ); + + /* Set IF Stage 2 - 6 gains to maximum */ + setIFStage2Gain( IFStage2Gain.GAIN_PLUS9, i2CRepeaterControl ); + setIFStage3Gain( IFStage3Gain.GAIN_PLUS9, i2CRepeaterControl ); + setIFStage4Gain( IFStage4Gain.GAIN_PLUS2B, i2CRepeaterControl ); + setIFStage5Gain( IFStage5Gain.GAIN_PLUS15D, i2CRepeaterControl ); + setIFStage6Gain( IFStage6Gain.GAIN_PLUS15D, i2CRepeaterControl ); + + /** + * Iterate all DC gain combinations, apply gain settings, read the + * DC offset value and apply it to the lookup table entry + */ + for( DCGainCombination combo: DCGainCombination.values() ) + { + /* Set mixer and stage1 gain values */ + setMixerGain( combo.getMixerGain(), i2CRepeaterControl ); + setIFStage1Gain( combo.getIFStage1Gain(), i2CRepeaterControl ); + + /* Turn on the DC range detector */ + setDCRangeDetectorEnabled( true, i2CRepeaterControl ); + + /* Calibrate the DC value */ + writeE4KRegister( Register.DC1, (byte)0x01, i2CRepeaterControl ); + + /* Get the I/Q offset and range values */ + byte offsetI = (byte)( readE4KRegister( Register.DC2, + i2CRepeaterControl ) & 0x3F ); + byte offsetQ = (byte)( readE4KRegister( Register.DC3, + i2CRepeaterControl ) & 0x3F ); + byte range = (byte)readE4KRegister( Register.DC4, + i2CRepeaterControl ); + byte rangeI = (byte)( range & 0x3 ); + byte rangeQ = (byte)( ( range >> 4 ) & 0x3 ); + + /* Write the offset and range values to the lookup table */ + writeE4KRegister( combo.getIRegister(), + (byte)( offsetI | ( rangeI << 6 ) ), + i2CRepeaterControl ); + + writeE4KRegister( combo.getQRegister(), + (byte)( offsetQ | ( rangeQ << 6 ) ), + i2CRepeaterControl ); + } + + if( controlI2CRepeater ) + { + enableI2CRepeater( mUSBDevice, false ); + } + } + + public void setDCRangeDetectorEnabled( boolean enabled, + boolean controlI2CRepeater ) throws UsbException + { + if( enabled ) + { + writeMaskedE4KRegister( Register.DC5, + DC5_RANGE_DETECTOR_ENABLED_MASK, + DC5_RANGE_DETECTOR_ENABLED, + controlI2CRepeater ); + } + else + { + writeMaskedE4KRegister( Register.DC5, + DC5_RANGE_DETECTOR_DISABLED_MASK, + DC5_RANGE_DETECTOR_DISABLED, + controlI2CRepeater ); + } + } + + /** + * Sets the LNA gain within the E4K Tuner + * + * Note: requires I2C repeater enabled + * + * @param gain + * @throws UsbException + */ + public void setLNAGain( R820TLNAGain gain, boolean controlI2CRepeater ) + throws UsbException + { + if( controlI2CRepeater ) + { + enableI2CRepeater( mUSBDevice, true ); + } + + if( gain == R820TLNAGain.AUTOMATIC ) + { + writeMaskedE4KRegister( Register.AGC1, + AGC1_MOD_MASK, + AGCMode.IF_SERIAL_LNA_AUTON.getValue(), + false ); + } + else + { + writeMaskedE4KRegister( Register.AGC1, + AGC1_MOD_MASK, + AGCMode.SERIAL.getValue(), + false ); + + + writeMaskedE4KRegister( R820TLNAGain.getRegister(), + R820TLNAGain.getMask(), + gain.getValue(), + false ); + } + + if( controlI2CRepeater ) + { + enableI2CRepeater( mUSBDevice, false ); + } + } + + /** + * Reads LNA gain from E4K tuner + * + * Note: requires I2C repeater enabled + */ + public R820TLNAGain getLNAGain( boolean controlI2CRepeater ) throws UsbException + { + return R820TLNAGain.fromRegisterValue( + readE4KRegister( R820TLNAGain.getRegister(), controlI2CRepeater ) ); + } + + /** + * Sets enhanced gain for E4K repeater. + * + * Note: requires I2C repeater enabled + * + * @param gain + * @throws UsbException + */ + public void setEnhanceGain( R820TEnhanceGain gain, boolean controlI2CRepeater ) + throws UsbException + { + writeMaskedE4KRegister( R820TEnhanceGain.getRegister(), + R820TEnhanceGain.getMask(), + gain.getValue(), + controlI2CRepeater ); + } + + /** + * Gets the enhanced gain setting in the E4K tuner. + * + * Note: requires I2C repeater + * @return + * @throws UsbException + */ + public R820TEnhanceGain getEnhanceGain( boolean controlI2CRepeater ) + throws UsbException + { + return R820TEnhanceGain.fromRegisterValue( + readE4KRegister( R820TEnhanceGain.getRegister(), controlI2CRepeater ) ); + } + + public void setMixerGain( R820TMixerGain gain, boolean controlI2CRepeater ) + throws UsbException + { + if( controlI2CRepeater ) + { + enableI2CRepeater( mUSBDevice, true ); + } + + boolean localI2CRepeaterControl = false; + + if( gain == R820TMixerGain.AUTOMATIC ) + { + writeMaskedE4KRegister( Register.AGC7, + AGC7_MIXER_GAIN_MASK, + AGC7_MIX_GAIN_AUTO, + localI2CRepeaterControl ); + } + else + { + writeMaskedE4KRegister( Register.AGC7, + AGC7_MIXER_GAIN_MASK, + AGC7_MIX_GAIN_MANUAL, + localI2CRepeaterControl ); + + /* Set the desired manual gain setting */ + writeMaskedE4KRegister( R820TMixerGain.getRegister(), + R820TMixerGain.getMask(), + gain.getValue(), + localI2CRepeaterControl ); + } + + if( controlI2CRepeater ) + { + enableI2CRepeater( mUSBDevice, false ); + } + } + + public R820TMixerGain getMixerGain( boolean controlI2CRepeater ) + throws UsbException + { + byte autoOrManual = readMaskedE4KRegister( Register.AGC7, + AGC7_MIXER_GAIN_MASK, + controlI2CRepeater ); + + if( autoOrManual == AGC7_MIX_GAIN_AUTO ) + { + return R820TMixerGain.AUTOMATIC; + } + else + { + int register = readE4KRegister( R820TMixerGain.getRegister(), + controlI2CRepeater ); + + return R820TMixerGain.fromRegisterValue( register ); + } + } + + public void setIFStage1Gain( IFStage1Gain gain, + boolean controlI2CRepeater ) throws UsbException + { + writeMaskedE4KRegister( IFStage1Gain.getRegister(), + IFStage1Gain.getMask(), + gain.getValue(), + controlI2CRepeater ); + } + + public IFStage1Gain getIFStage1Gain( boolean controlI2CRepeater ) + throws UsbException + { + return IFStage1Gain.fromRegisterValue( + readE4KRegister( IFStage1Gain.getRegister(), controlI2CRepeater ) ); + } + + public void setIFStage2Gain( IFStage2Gain gain, + boolean controlI2CRepeater ) throws UsbException + { + writeMaskedE4KRegister( IFStage2Gain.getRegister(), + IFStage2Gain.getMask(), + gain.getValue(), + controlI2CRepeater ); + } + + public IFStage2Gain getIFStage2Gain( boolean controlI2CRepeater ) + throws UsbException + { + return IFStage2Gain.fromRegisterValue( + readE4KRegister( IFStage2Gain.getRegister(), controlI2CRepeater ) ); + } + + public void setIFStage3Gain( IFStage3Gain gain, + boolean controlI2CRepeater ) throws UsbException + { + writeMaskedE4KRegister( IFStage3Gain.getRegister(), + IFStage3Gain.getMask(), + gain.getValue(), + controlI2CRepeater ); + } + + public IFStage3Gain getIFStage3Gain( boolean controlI2CRepeater ) + throws UsbException + { + return IFStage3Gain.fromRegisterValue( + readE4KRegister( IFStage3Gain.getRegister(), controlI2CRepeater ) ); + } + + public void setIFStage4Gain( IFStage4Gain gain, + boolean controlI2CRepeater ) throws UsbException + { + writeMaskedE4KRegister( IFStage4Gain.getRegister(), + IFStage4Gain.getMask(), + gain.getValue(), + controlI2CRepeater ); + } + + public IFStage4Gain getIFStage4Gain( boolean controlI2CRepeater ) + throws UsbException + { + return IFStage4Gain.fromRegisterValue( + readE4KRegister( IFStage4Gain.getRegister(), controlI2CRepeater ) ); + } + + public void setIFStage5Gain( IFStage5Gain gain, + boolean controlI2CRepeater ) throws UsbException + { + writeMaskedE4KRegister( IFStage5Gain.getRegister(), + IFStage5Gain.getMask(), + gain.getValue(), + controlI2CRepeater ); + } + + public IFStage5Gain getIFStage5Gain( boolean controlI2CRepeater ) + throws UsbException + { + return IFStage5Gain.fromRegisterValue( + readE4KRegister( IFStage5Gain.getRegister(), controlI2CRepeater ) ); + } + + public void setIFStage6Gain( IFStage6Gain gain, + boolean controlI2CRepeater ) throws UsbException + { + writeMaskedE4KRegister( IFStage6Gain.getRegister(), + IFStage6Gain.getMask(), + gain.getValue(), + controlI2CRepeater ); + } + + public IFStage6Gain getIFStage6Gain( boolean controlI2CRepeater ) + throws UsbException + { + return IFStage6Gain.fromRegisterValue( + readE4KRegister( IFStage6Gain.getRegister(), controlI2CRepeater ) ); + } + + public void setMixerFilter( MixerFilter filter, + boolean controlI2CRepeater ) throws UsbException + { + writeMaskedE4KRegister( MixerFilter.getRegister(), + MixerFilter.getMask(), + filter.getValue(), + controlI2CRepeater ); + } + + public MixerFilter getMixerFilter( boolean controlI2CRepeater ) + throws UsbException + { + int value = readE4KRegister( MixerFilter.getRegister(), + controlI2CRepeater ); + + return MixerFilter.fromRegisterValue( value ); + } + + public void setRCFilter( RCFilter filter, + boolean controlI2CRepeater ) throws UsbException + { + writeMaskedE4KRegister( RCFilter.getRegister(), + RCFilter.getMask(), + filter.getValue(), + controlI2CRepeater ); + } + + public RCFilter getRCFilter( boolean controlI2CRepeater ) throws UsbException + { + int value = readE4KRegister( RCFilter.getRegister(), controlI2CRepeater ); + + return RCFilter.fromRegisterValue( value ); + } + + public void setChannelFilter( ChannelFilter filter, + boolean controlI2CRepeater ) throws UsbException + { + writeMaskedE4KRegister( ChannelFilter.getRegister(), + ChannelFilter.getMask(), + filter.getValue(), + controlI2CRepeater ); + } + + public ChannelFilter getChannelFilter( boolean controlI2CRepeater ) + throws UsbException + { + int value = readE4KRegister( ChannelFilter.getRegister(), controlI2CRepeater ); + + return ChannelFilter.fromRegisterValue( value ); + + } + + public void setChannelFilterEnabled( boolean enabled, + boolean controlI2CRepeater ) throws UsbException + { + if( enabled ) + { + writeMaskedE4KRegister( Register.FILT3, + FILT3_MASK, + FILT3_ENABLE, + controlI2CRepeater ); + } + else + { + writeMaskedE4KRegister( Register.FILT3, + FILT3_MASK, + FILT3_DISABLE, + controlI2CRepeater ); + } + } + + public void setBand( int frequency, + boolean controlI2CRepeater ) throws UsbException + { + if( controlI2CRepeater ) + { + enableI2CRepeater( mUSBDevice, true ); + } + + Band band = Band.fromFrequency( frequency ); + + /* Set the bias */ + switch( band ) + { + case UHF: + case VHF2: + case VHF3: + writeE4KRegister( Register.BIAS, (byte)0x3, false ); + break; + case L: + default: + writeE4KRegister( Register.BIAS, (byte)0x0, false ); + break; + + } + + /** + * Workaround - reset register (set value to 0) before writing to it, to + * avoid a gap around 325-350 MHz + */ + writeMaskedE4KRegister( Register.SYNTH1, Band.getMask(), (byte)0x0, false ); + writeMaskedE4KRegister( Register.SYNTH1, Band.getMask(), band.getValue(), false ); + + if( controlI2CRepeater ) + { + enableI2CRepeater( mUSBDevice, false ); + } + } + + private void setRFFilter( int frequency, + boolean controlI2CRepeater ) throws UsbException + { + RFFilter filter = RFFilter.fromFrequency( frequency ); + + writeMaskedE4KRegister( Register.FILT1, + RFFilter.getMask(), + filter.getValue(), + controlI2CRepeater ); + } + + /** + * Sets the gain combination setting. + * + * Note: does not respond to the MANUAL setting. Use the manual setting + * to configure gui components to expose the individual Mixer, LNA and + * Enhance gain settings, to allow the user to change those settings + * individually. + * + * @param gain - requested gain setting + * @throws UsbException if there are any errors + */ + public void setGain( R820TGain gain, boolean controlI2CRepeater ) throws UsbException + { + if( gain != R820TGain.MANUAL ) + { + if( controlI2CRepeater ) + { + enableI2CRepeater( mUSBDevice, true ); + } + + boolean i2CRepeaterControl = false; + + setLNAGain( gain.getLNAGain(), i2CRepeaterControl ); + setMixerGain( gain.getMixerGain(), i2CRepeaterControl ); + setEnhanceGain( gain.getEnhanceGain(), i2CRepeaterControl ); + + if( controlI2CRepeater ) + { + enableI2CRepeater( mUSBDevice, false ); + } + } + } + + /* + * Performs magic initialization ... and that's all we need to know, I guess + */ + private void magicInit( boolean controlI2CRepeater ) throws UsbException + { + if( controlI2CRepeater ) + { + enableI2CRepeater( mUSBDevice, true ); + } + + writeE4KRegister( Register.MAGIC_1, (byte)0x01, false ); + writeE4KRegister( Register.MAGIC_2, (byte)0xFE, false ); + writeE4KRegister( Register.MAGIC_3, (byte)0x00, false ); + writeE4KRegister( Register.MAGIC_4, (byte)0x50, false ); //Polarity A + writeE4KRegister( Register.MAGIC_5, (byte)0x20, false ); + writeE4KRegister( Register.MAGIC_6, (byte)0x01, false ); + writeE4KRegister( Register.MAGIC_7, (byte)0x7F, false ); + writeE4KRegister( Register.MAGIC_8, (byte)0x07, false ); + + if( controlI2CRepeater ) + { + enableI2CRepeater( mUSBDevice, false ); + } + } + + private byte readMaskedE4KRegister( Register register, + byte mask, + boolean controlI2CRepeater ) + throws UsbException + { + int temp = readE4KRegister( register, controlI2CRepeater ); + return (byte)( temp & mask ); + } + + private void writeMaskedE4KRegister( Register register, + byte mask, + byte value, + boolean controlI2CRepeater ) throws UsbException + { + int temp = readE4KRegister( register, controlI2CRepeater ); + + /* If the register is not set to the masked value, then change it */ + if( (byte)( temp & mask ) != value ) + { + writeE4KRegister( register, + (byte)( ( temp & ~mask ) | ( value & mask ) ), + controlI2CRepeater ); + } + } + + private int readE4KRegister( Register register, + boolean controlI2CRepeater ) throws UsbException + { + return readI2CRegister( mUSBDevice, Register.I2C_REGISTER.getValue(), + register.getValue(), controlI2CRepeater ); + } + + + private void writeE4KRegister( Register register, + byte value, + boolean controlI2CRepeater ) throws UsbException + { + writeI2CRegister( mUSBDevice, Register.I2C_REGISTER.getValue(), + register.getValue(), value, controlI2CRepeater ); + } + + public enum Band + { + VHF2( 0 ), + VHF3( 2 ), + UHF( 4 ), + L( 6 ); + + private int mValue; + + private Band( int value ) + { + mValue = value; + } + + public static byte getMask() + { + return (byte)0x06; + } + + public byte getValue() + { + return (byte)mValue; + } + + public String getLabel() + { + return toString(); + } + + public static Band fromFrequency( int frequency ) + { + if( frequency < 140000000 ) //140 MHz + { + return VHF2; + } + else if( frequency < 350000000 ) //350 MHz + { + return VHF3; + } + else if( frequency < 1135000000 ) //1135 MHz + { + return UHF; + } + else + { + return L; + } + } + } + + public enum PLL + { + PLL_72M4( 0x0F, 72400000, 48, 600000, true, "72.4 MHz" ), + PLL_81M2( 0x0E, 81200000, 40, 720000, true, "81.2 MHz" ), + PLL_108M3( 0x0D, 108300000, 32, 900000, true, "108.3 MHz" ), + PLL_162M5( 0x0C, 162500000, 24, 1200000, true, "162.5 MHz" ), + PLL_216M6( 0x0B, 216600000, 16, 1800000, true, "216.6 MHz" ), + PLL_325M0( 0x0A, 325000000, 12, 2400000, true, "325.0 MHz" ), + PLL_350M0( 0x09, 350000000, 8, 3600000, true, "350.0 MHz" ), + PLL_432M0( 0x03, 432000000, 8, 3600000, false, "432.0 MHz" ), + PLL_667M0( 0x02, 667000000, 6, 4800000, false, "667.0 MHz" ), + PLL_1200M0( 0x01, 1200000000, 4, 7200000, false, "1200.0 MHz" ); + + private int mValue; + private int mFrequency; + private int mMultiplier; + private int mScaledOscillator; + private boolean mRequires3PhaseMixing; + private String mLabel; + + private PLL( int value, + int frequency, + int multiplier, + int scaledOscillator, + boolean requires3Phase, + String label ) + { + mValue = value; + mFrequency = frequency; + mMultiplier = multiplier; + mScaledOscillator = scaledOscillator; + mRequires3PhaseMixing = requires3Phase; + mLabel = label; + + } + + public byte getIndex() + { + return (byte)mValue; + } + + public int getFrequency() + { + return mFrequency; + } + + public byte getMultiplier() + { + return (byte)mMultiplier; + } + + public int getScaledOscillator() + { + return mScaledOscillator; + } + + public boolean requires3PhaseMixing() + { + return mRequires3PhaseMixing; + } + + public String getLabel() + { + return mLabel; + } + + public String toString() + { + return mLabel; + } + + /* Returns the PLL setting with the closest frequency that is greater + * than the frequency argument */ + public static PLL fromFrequency( int frequency ) + { + for( PLL pll: values() ) + { + if( frequency < pll.getFrequency() ) + { + return pll; + } + } + + //Default + return PLL.PLL_72M4; + } + + public static PLL fromSetting( int setting ) + { + int value = setting & 0xF; + + for( PLL pll: values() ) + { + if( value == pll.getIndex() ) + { + return pll; + } + } + + //Default + return PLL.PLL_72M4; + } + } + + /** + * RF filters - UHF Band. Each enum entry defines the center frequency for + * the filter and the applicable frequency minimums and maximums for the + * filter. + * + * Note: max frequency overlaps with the next minimum frequency, so valid + * frequencies should be less than the maximum frequency defined for the + * filter. + */ + public enum RFFilter + { + NO_FILTER( 0, 0, 0 ), + + //UHF band Filters + M360( 0, 350000000, 370000000 ), + M380( 1, 370000000, 392500000 ), + M405( 2, 392500000, 417500000 ), + M425( 3, 417500000, 437500000 ), + M450( 4, 437500000, 462500000 ), + M475( 5, 462500000, 490000000 ), + M505( 6, 490000000, 522500000 ), + M540( 7, 522500000, 557500000 ), + M575( 8, 557500000, 595000000 ), + M615( 9, 595000000, 642500000 ), + M670( 10, 642500000, 695000000 ), + M720( 11, 695000000, 740000000 ), + M760( 12, 740000000, 800000000 ), + M840( 13, 800000000, 865000000 ), + M890( 14, 865000000, 930000000 ), + M970( 15, 930000000, 1135000000 ), + + //L band filters + M1300( 0, 1135000000, 1310000000 ), + M1320( 1, 1310000000, 1340000000 ), + M1360( 2, 1340000000, 1385000000 ), + M1410( 3, 1385000000, 1427500000 ), + M1445( 4, 1427500000, 1452500000 ), + M1460( 5, 1452500000, 1475000000 ), + M1490( 6, 1475000000, 1510000000 ), + M1530( 7, 1510000000, 1545000000 ), + M1560( 8, 1545000000, 1575000000 ), + M1590( 9, 1575000000, 1615000000 ), + M1640( 10, 1615000000, 1650000000 ), + M1660( 11, 1650000000, 1670000000 ), + M1680( 12, 1670000000, 1690000000 ), + M1700( 13, 1690000000, 1710000000 ), + M1720( 14, 1710000000, 1735000000 ), + + /* Note: max frequency is currently limited by the max integer value + * and we set the max frequency for this filter to 2147 MHz */ + M1750( 15, 1735000000, 2147000000 ); + + private int mValue; + private int mMinFrequency; + private int mMaxFrequency; + + private RFFilter( int value, int minFrequency, int maxFrequency ) + { + mValue = value; + mMinFrequency = minFrequency; + mMaxFrequency = maxFrequency; + } + + public static Register getRegister() + { + return Register.FILT1; + } + + public static byte getMask() + { + return (byte)0xF; //TODO: make this a constant + } + + public byte getValue() + { + return (byte)mValue; + } + + /** + * Minimum frequency for this filter. + */ + public int getMinimumFrequency() + { + return mMinFrequency; + } + + /** + * Maximum frequency for this filter is less than this value, but does + * not include this value. + */ + public int getMaximumFrequency() + { + return mMaxFrequency; + } + + /** + * Selects the appropriate filter for the frequency + * + * @param frequency + * @return selected filter + * @throws IllegalArgumentException if the frequency is outside the + * max value (2200 MHz) + */ + public static RFFilter fromFrequency( int frequency ) + { + if( frequency < 350000000 ) + { + return NO_FILTER; + } + + for( RFFilter filter: values() ) + { + if( filter.getMinimumFrequency() <= frequency && + frequency < filter.getMaximumFrequency() ) + { + return filter; + } + } + + throw new IllegalArgumentException( "E4KTunerController - " + + "cannot find RF filter for frequency [" + frequency + "]" ); + } + } + + public enum MixerFilter + { + BW_27M0( 0x00, 27000000, 4800000, 28800000, "27.0 MHz" ), + BW_4M6( 0x80, 4600000, 4400000, 4800000, "4.6 MHz" ), + BW_4M2( 0x90, 4200000, 4000000, 4400000, "4.2 MHz" ), + BW_3M8( 0xA0, 3800000, 3600000, 4000000, "3.8 MHz" ), + BW_3M4( 0xB0, 3400000, 3200000, 3600000, "3.4 MHz" ), + BW_3M0( 0xC0, 3000000, 2850000, 3200000, "3.0 MHz" ), + BW_2M7( 0xD0, 2700000, 2500000, 2850000, "2.7 MHz" ), + BW_2M3( 0xE0, 2300000, 2100000, 2500000, "2.3 MHz" ), + BW_1M9( 0xF0, 1900000, 0, 2100000, "1.9 MHz" ); + + private int mValue; + private int mBandwidth; + private int mMinBandwidth; + private int mMaxBandwidth; + private String mLabel; + + private MixerFilter( int value, + int bandwidth, + int minimumBandwidth, + int maximumBandwidth, + String label ) + { + mValue = value; + mBandwidth = bandwidth; + mMinBandwidth = minimumBandwidth; + mMaxBandwidth = maximumBandwidth; + mLabel = label; + } + + public static Register getRegister() + { + return Register.FILT2; + } + + public static byte getMask() + { + return MIXER_FILTER_MASK; + } + + public byte getValue() + { + return (byte)mValue; + } + + public int getBandwidth() + { + return mBandwidth; + } + + public int getMinimumBandwidth() + { + return mMinBandwidth; + } + + public int getMaximumBandwidth() + { + return mMaxBandwidth; + } + + public String getLabel() + { + return mLabel; + } + + /** + * Returns the correct filter to use for the bandwidth/sample rate + * @param bandwidth - current sample rate + * @return - correct filter to use for the bandwidth/sample rate + */ + public static MixerFilter getFilter( int bandwidth ) + { + for( MixerFilter filter: values() ) + { + if( filter.getMinimumBandwidth() <= bandwidth && + bandwidth < filter.getMaximumBandwidth() ) + { + return filter; + } + } + + /* default */ + return MixerFilter.BW_27M0; + } + + public static MixerFilter fromRegisterValue( int registerValue ) + { + int value = registerValue & getMask(); + + for( MixerFilter filter: values() ) + { + if( value == filter.getValue() ) + { + return filter; + } + } + + throw new IllegalArgumentException( "E4KTunerController - " + + "unrecognized mixer filter value [" + value + "]" ); + } + } + + public enum RCFilter + { + BW_21M4( 0x00, 21400000, 21200000, 28800000, "21.4 MHz" ), + BW_21M0( 0x01, 21000000, 19300000, 21200000, "21.0 MHz" ), + BW_17M6( 0x02, 17600000, 16150000, 19300000, "17.6 MHz" ), + BW_14M7( 0x03, 14700000, 13550000, 16150000, "14.7 MHz" ), + BW_12M4( 0x04, 12400000, 11500000, 13550000, "12.4 MHz" ), + BW_10M6( 0x05, 10600000, 9800000, 11500000, "10.6 MHz" ), + BW_9M0( 0x06, 9000000, 8350000, 9800000, "9.0 MHz" ), + BW_7M7( 0x07, 7700000, 7050000, 8350000, "7.7 MHz" ), + BW_6M4( 0x08, 6400000, 5805000, 7050000, "6.4 MHz" ), + BW_5M3( 0x09, 5300000, 4850000, 5805000, "5.3 MHz" ), + BW_4M4( 0x0A, 4400000, 3900000, 4850000, "4.4 MHz" ), + BW_3M4( 0x0B, 3400000, 3000000, 3900000, "3.4 MHz" ), + BW_2M6( 0x0C, 2600000, 2200000, 3000000, "2.6 MHz" ), + BW_1M8( 0x0D, 1800000, 1500000, 2200000, "1.8 MHz" ), + BW_1M2( 0x0E, 1200000, 1100000, 1500000, "1.2 MHz" ), + BW_1M0( 0x0F, 1000000, 0, 1100000, "1.0 MHz" ); + + private int mValue; + private int mBandwidth; + private int mMinBandwidth; + private int mMaxBandwidth; + private String mLabel; + + private RCFilter( int value, + int bandwidth, + int minimumBandwidth, + int maximumBandwidth, + String label ) + { + mValue = value; + mBandwidth = bandwidth; + mMinBandwidth = minimumBandwidth; + mMaxBandwidth = maximumBandwidth; + mLabel = label; + } + + public static Register getRegister() + { + return Register.FILT2; + } + + public static byte getMask() + { + return IF_RC_FILTER_MASK; + } + + public byte getValue() + { + return (byte)mValue; + } + + public int getBandwidth() + { + return mBandwidth; + } + + public int getMinimumBandwidth() + { + return mMinBandwidth; + } + + public int getMaximumBandwidth() + { + return mMaxBandwidth; + } + + public String getLabel() + { + return mLabel; + } + + /** + * Returns the correct filter to use for the bandwidth/sample rate + * @param bandwidth - current sample rate + * @return - correct filter to use for the bandwidth/sample rate + */ + public static RCFilter getFilter( int bandwidth ) + { + for( RCFilter filter: values() ) + { + if( filter.getMinimumBandwidth() <= bandwidth && + bandwidth < filter.getMaximumBandwidth() ) + { + return filter; + } + } + + /* default */ + return RCFilter.BW_21M4; + } + + public static RCFilter fromRegisterValue( int registerValue ) + { + int value = registerValue & getMask(); + + for( RCFilter filter: values() ) + { + if( value == filter.getValue() ) + { + return filter; + } + } + + throw new IllegalArgumentException( "E4KTunerController - " + + "unrecognized rc filter value [" + value + "]" ); + } + } + + public enum ChannelFilter + { + BW_5M50( 0x00, 5500000, 5400000, 28800000, "5.50 MHz" ), + BW_5M30( 0x01, 5300000, 5150000, 5400000, "5.30 MHz" ), + BW_5M00( 0x02, 5000000, 4900000, 5150000, "5.00 MHz" ), + BW_4M80( 0x03, 4800000, 4700000, 4900000, "4.80 MHz" ), + BW_4M60( 0x04, 4600000, 4500000, 4700000, "4.60 MHz" ), + BW_4M40( 0x05, 4400000, 4350000, 4500000, "4.40 MHz" ), + BW_4M30( 0x06, 4300000, 4200000, 4350000, "4.30 MHz" ), + BW_4M10( 0x07, 4100000, 4000000, 4200000, "4.10 MHz" ), + BW_3M90( 0x08, 3900000, 3850000, 4000000, "3.90 MHz" ), + BW_3M80( 0x09, 3800000, 3750000, 3850000, "3.80 MHz" ), + BW_3M70( 0x0A, 3700000, 3650000, 3750000, "3.70 MHz" ), + BW_3M60( 0x0B, 3600000, 3500000, 3650000, "3.60 MHz" ), + BW_3M40( 0x0C, 3400000, 3350000, 3500000, "3.40 MHz" ), + BW_3M30( 0x0D, 3300000, 3250000, 3350000, "3.30 MHz" ), + BW_3M20( 0x0E, 3200000, 3150000, 3250000, "3.20 MHz" ), + BW_3M10( 0x0F, 3100000, 3050000, 3150000, "3.10 MHz" ), + BW_3M00( 0x10, 3000000, 2975000, 3050000, "3.00 MHz" ), + BW_2M95( 0x11, 2950000, 2925000, 2975000, "2.95 MHz" ), + BW_2M90( 0x12, 2900000, 2850000, 2925000, "2.90 MHz" ), + BW_2M80( 0x13, 2800000, 2775000, 2850000, "2.80 MHz" ), + BW_2M75( 0x14, 2750000, 2750000, 2775000, "2.75 MHz" ), + BW_2M70( 0x15, 2700000, 2650000, 2750000, "2.70 MHz" ), + BW_2M60( 0x16, 2600000, 2575000, 2650000, "2.60 MHz" ), + BW_2M55( 0x17, 2550000, 2525000, 2575000, "2.55 MHz" ), + BW_2M50( 0x18, 2500000, 2475000, 2525000, "2.50 MHz" ), + BW_2M45( 0x19, 2450000, 2425000, 2475000, "2.45 MHz" ), + BW_2M40( 0x1A, 2400000, 2350000, 2425000, "2.40 MHz" ), + BW_2M30( 0x1B, 2300000, 2290000, 2350000, "2.30 MHz" ), + BW_2M28( 0x1C, 2280000, 2260000, 2290000, "2.28 MHz" ), + BW_2M24( 0x1D, 2240000, 2220000, 2260000, "2.24 MHz" ), + BW_2M20( 0x1E, 2200000, 2175000, 2220000, "2.20 MHz" ), + BW_2M15( 0x1F, 2150000, 0, 2175000, "2.15 MHz" ); + + private int mValue; + private int mBandwidth; + private int mMinBandwidth; + private int mMaxBandwidth; + private String mLabel; + + private ChannelFilter( int value, + int bandwidth, + int minimumBandwidth, + int maximumBandwidth, + String label ) + { + mValue = value; + mBandwidth = bandwidth; + mMinBandwidth = minimumBandwidth; + mMaxBandwidth = maximumBandwidth; + mLabel = label; + } + + public static Register getRegister() + { + return Register.FILT3; + } + + public static byte getMask() + { + return IF_CHANNEL_FILTER_MASK; + } + + public byte getValue() + { + return (byte)mValue; + } + + public int getBandwidth() + { + return mBandwidth; + } + + public int getMinimumBandwidth() + { + return mMinBandwidth; + } + + public int getMaximumBandwidth() + { + return mMaxBandwidth; + } + + public String getLabel() + { + return mLabel; + } + + /** + * Returns the correct filter to use for the bandwidth/sample rate + * @param bandwidth - current sample rate + * @return - correct filter to use for the bandwidth/sample rate + */ + public static ChannelFilter getFilter( int bandwidth ) + { + for( ChannelFilter filter: values() ) + { + if( filter.getMinimumBandwidth() <= bandwidth && + bandwidth < filter.getMaximumBandwidth() ) + { + return filter; + } + } + + /* default */ + return ChannelFilter.BW_5M50; + } + + public static ChannelFilter fromRegisterValue( int registerValue ) + { + int value = registerValue & getMask(); + + for( ChannelFilter filter: values() ) + { + if( value == filter.getValue() ) + { + return filter; + } + } + + throw new IllegalArgumentException( "E4KTunerController - " + + "unrecognized channel filter value [" + value + "]" ); + } + } + + public enum R820TLNAGain + { + AUTOMATIC( -1, "Auto" ), + GAIN_MINUS_50( 0, "-5.0 db" ), + GAIN_MINUS_25( 1, "-2.5 db" ), + GAIN_PLUS_0( 4, "0.0 db" ), + GAIN_PLUS_25( 5, "2.5 db" ), + GAIN_PLUS_50( 6, "5.0 db" ), + GAIN_PLUS_75( 7, "7.5 db" ), + GAIN_PLUS_100( 8, "10.0 db" ), + GAIN_PLUS_125( 9, "12.5 db" ), + GAIN_PLUS_150( 10, "15.0 db" ), + GAIN_PLUS_175( 11, "17.5 db" ), + GAIN_PLUS_200( 12, "20.0 db" ), + GAIN_PLUS_250( 13, "25.0 db" ), + GAIN_PLUS_300( 14, "30.0 db" ); + + private int mValue; + private String mLabel; + + private R820TLNAGain( int value, String label ) + { + mValue = value; + mLabel = label; + } + + public static Register getRegister() + { + return Register.GAIN1; + } + + public static byte getMask() + { + return GAIN1_MOD_MASK; + } + + public byte getValue() + { + return (byte)mValue; + } + + public String getLabel() + { + return mLabel; + } + + public String toString() + { + return mLabel; + } + + public static R820TLNAGain fromRegisterValue( int registerValue ) + { + int value = registerValue & getMask(); + + for( R820TLNAGain setting: values() ) + { + if( value == setting.getValue() ) + { + return setting; + } + } + + throw new IllegalArgumentException( "E4KTunerController" + + " - unrecognized LNA Gain value [" + + value + "]" ); + } + } + + /** + * Defines the default gain settings that are exposed to the user. + * + * Manual is an override that keeps each of the settings intact and allows + * the user to change each of them individually + */ + public enum R820TGain + { + AUTOMATIC( "Auto", R820TMixerGain.AUTOMATIC, R820TLNAGain.AUTOMATIC, R820TEnhanceGain.AUTOMATIC ), + MANUAL( "Manual", null, null, null ), + MINUS_10( "-1.0 db", R820TMixerGain.GAIN_4, R820TLNAGain.GAIN_MINUS_50, R820TEnhanceGain.AUTOMATIC ), + PLUS_15( "1.5 db", R820TMixerGain.GAIN_4, R820TLNAGain.GAIN_MINUS_25, R820TEnhanceGain.AUTOMATIC ), + PLUS_40( "4.0 db", R820TMixerGain.GAIN_4, R820TLNAGain.GAIN_PLUS_0, R820TEnhanceGain.AUTOMATIC ), + PLUS_65( "6.5 db", R820TMixerGain.GAIN_4, R820TLNAGain.GAIN_PLUS_25, R820TEnhanceGain.AUTOMATIC ), + PLUS_90( "9.0 db", R820TMixerGain.GAIN_4, R820TLNAGain.GAIN_PLUS_50, R820TEnhanceGain.AUTOMATIC ), + PLUS_115( "11.5 db", R820TMixerGain.GAIN_4, R820TLNAGain.GAIN_PLUS_75, R820TEnhanceGain.AUTOMATIC ), + PLUS_140( "14.0 db", R820TMixerGain.GAIN_4, R820TLNAGain.GAIN_PLUS_100, R820TEnhanceGain.AUTOMATIC ), + PLUS_165( "16.5 db", R820TMixerGain.GAIN_4, R820TLNAGain.GAIN_PLUS_125, R820TEnhanceGain.AUTOMATIC ), + PLUS_190( "19.0 db", R820TMixerGain.GAIN_4, R820TLNAGain.GAIN_PLUS_150, R820TEnhanceGain.AUTOMATIC ), + PLUS_215( "21.5 db", R820TMixerGain.GAIN_4, R820TLNAGain.GAIN_PLUS_175, R820TEnhanceGain.AUTOMATIC ), + PLUS_240( "24.0 db", R820TMixerGain.GAIN_4, R820TLNAGain.GAIN_PLUS_200, R820TEnhanceGain.AUTOMATIC ), + PLUS_290( "29.0 db", R820TMixerGain.GAIN_4, R820TLNAGain.GAIN_PLUS_250, R820TEnhanceGain.AUTOMATIC ), + PLUS_340( "34.0 db", R820TMixerGain.GAIN_4, R820TLNAGain.GAIN_PLUS_300, R820TEnhanceGain.AUTOMATIC ), + PLUS_420( "42.0 db", R820TMixerGain.GAIN_12, R820TLNAGain.GAIN_PLUS_300, R820TEnhanceGain.AUTOMATIC ); + + private String mLabel; + private R820TMixerGain mMixerGain; + private R820TLNAGain mLNAGain; + private R820TEnhanceGain mEnhanceGain; + + private R820TGain( String label, + R820TMixerGain mixerGain, + R820TLNAGain lnaGain, + R820TEnhanceGain enhanceGain ) + { + mLabel = label; + mMixerGain = mixerGain; + mLNAGain = lnaGain; + mEnhanceGain = enhanceGain; + } + + public String getLabel() + { + return mLabel; + } + + public String toString() + { + return mLabel; + } + + public R820TMixerGain getMixerGain() + { + return mMixerGain; + } + + public R820TLNAGain getLNAGain() + { + return mLNAGain; + } + + public R820TEnhanceGain getEnhanceGain() + { + return mEnhanceGain; + } + } + + public enum R820TEnhanceGain + { + AUTOMATIC( 0, "Auto" ), + GAIN_1( 1, "10 db" ), + GAIN_3( 3, "30 db" ), + GAIN_5( 5, "50 db" ), + GAIN_7( 7, "70 db" ); + + private int mValue; + private String mLabel; + + private R820TEnhanceGain( int value, String label ) + { + mValue = value; + mLabel = label; + } + + public static Register getRegister() + { + return Register.AGC11; + } + + public static byte getMask() + { + return ENH_GAIN_MOD_MASK; + } + + public byte getValue() + { + return (byte)mValue; + } + + public String getLabel() + { + return mLabel; + } + + public String toString() + { + return mLabel; + } + + public static R820TEnhanceGain fromRegisterValue( int registerValue ) + { + int value = registerValue & getMask(); + + for( R820TEnhanceGain setting: values() ) + { + if( value == setting.getValue() ) + { + return setting; + } + } + + throw new IllegalArgumentException( "E4KTunerController" + + " - unrecognized Enhance Gain value [" + + value + "]" ); + } + } + + public enum DCGainCombination + { + LOOKUP_TABLE_0( Register.QLUT0, + Register.ILUT0, + R820TMixerGain.GAIN_4, + IFStage1Gain.GAIN_MINUS3 ), + LOOKUP_TABLE_1( Register.QLUT1, + Register.ILUT1, + R820TMixerGain.GAIN_4, + IFStage1Gain.GAIN_PLUS6 ), + LOOKUP_TABLE_2( Register.QLUT2, + Register.ILUT2, + R820TMixerGain.GAIN_12, + IFStage1Gain.GAIN_MINUS3 ), + LOOKUP_TABLE_3( Register.QLUT3, + Register.ILUT3, + R820TMixerGain.GAIN_12, + IFStage1Gain.GAIN_PLUS6 ); + + private Register mQRegister; + private Register mIRegister; + private R820TMixerGain mMixerGain; + private IFStage1Gain mIFStage1Gain; + + private DCGainCombination( Register qRegister, + Register iRegister, + R820TMixerGain mixer, + IFStage1Gain stage1 ) + { + mQRegister = qRegister; + mIRegister = iRegister; + mMixerGain = mixer; + mIFStage1Gain = stage1; + } + + public Register getQRegister() + { + return mQRegister; + } + + public Register getIRegister() + { + return mIRegister; + } + + public R820TMixerGain getMixerGain() + { + return mMixerGain; + } + + public IFStage1Gain getIFStage1Gain() + { + return mIFStage1Gain; + } + } + + + public enum R820TMixerGain + { + AUTOMATIC( -1, "Auto" ), + GAIN_4( 0, "4 db" ), + GAIN_12( 1, "12 db" ); + + private int mValue; + private String mLabel; + + private R820TMixerGain( int value, String label ) + { + mValue = value; + mLabel = label; + } + + public static Register getRegister() + { + return Register.GAIN2; + } + + public static byte getMask() + { + return MIXER_GAIN_MASK; + } + + public byte getValue() + { + return (byte)mValue; + } + + public String getLabel() + { + return mLabel; + } + + public String toString() + { + return mLabel; + } + + public static R820TMixerGain fromRegisterValue( int registerValue ) + { + int value = registerValue & getMask(); + + for( R820TMixerGain setting: values() ) + { + if( value == setting.getValue() ) + { + return setting; + } + } + + throw new IllegalArgumentException( "E4KTunerController" + + " - unrecognized Mixer Gain value [" + + value + "]" ); + } + } + + public enum IFStage1Gain + { + GAIN_MINUS3( 0x0, "-3 db" ), + GAIN_PLUS6( 0x1, "6 db" ); + + private int mValue; + private String mLabel; + + private IFStage1Gain( int value, String label ) + { + mValue = value; + mLabel = label; + } + + public static Register getRegister() + { + return Register.GAIN3; + } + + public static byte getMask() + { + return (byte)0x1; + } + + public byte getValue() + { + return (byte)mValue; + } + + public String getLabel() + { + return mLabel; + } + + public String toString() + { + return mLabel; + } + + public static IFStage1Gain fromRegisterValue( int registerValue ) + { + int value = registerValue & getMask(); + + for( IFStage1Gain setting: values() ) + { + if( value == setting.getValue() ) + { + return setting; + } + } + + throw new IllegalArgumentException( "E4KTunerController" + + " - unrecognized IF Gain Stage 1 value [" + + value + "]" ); + } + } + + public enum IFStage2Gain + { + GAIN_PLUS0( 0x0, "0 db" ), + GAIN_PLUS3( 0x2, "3 db" ), + GAIN_PLUS6( 0x4, "6 db" ), + GAIN_PLUS9( 0x6, "9 db" ); + + private int mValue; + private String mLabel; + + private IFStage2Gain( int value, String label ) + { + mValue = value; + mLabel = label; + } + + public static Register getRegister() + { + return Register.GAIN3; + } + + public static byte getMask() + { + return (byte)0x6; + } + + public byte getValue() + { + return (byte)mValue; + } + + public String getLabel() + { + return mLabel; + } + + public String toString() + { + return mLabel; + } + + public static IFStage2Gain fromRegisterValue( int registerValue ) + { + int value = registerValue & getMask(); + + for( IFStage2Gain setting: values() ) + { + if( value == setting.getValue() ) + { + return setting; + } + } + + throw new IllegalArgumentException( "E4KTunerController" + + " - unrecognized IF Gain Stage 2 value [" + + value + "]" ); + } + } + + public enum IFStage3Gain + { + GAIN_PLUS0( 0x00, "0 db" ), + GAIN_PLUS3( 0x08, "3 db" ), + GAIN_PLUS6( 0x10, "6 db" ), + GAIN_PLUS9( 0x30, "9 db" ); + + private int mValue; + private String mLabel; + + private IFStage3Gain( int value, String label ) + { + mValue = value; + mLabel = label; + } + + public static Register getRegister() + { + return Register.GAIN3; + } + + public static byte getMask() + { + return (byte)0x18; + } + + public byte getValue() + { + return (byte)mValue; + } + + public String getLabel() + { + return mLabel; + } + + public String toString() + { + return mLabel; + } + + public static IFStage3Gain fromRegisterValue( int registerValue ) + { + int value = registerValue & getMask(); + + for( IFStage3Gain setting: values() ) + { + if( value == setting.getValue() ) + { + return setting; + } + } + + throw new IllegalArgumentException( "E4KTunerController" + + " - unrecognized IF Gain Stage 3 value [" + + value + "]" ); + } + } + + public enum IFStage4Gain + { + GAIN_PLUS0( 0x00, "0 db" ), + GAIN_PLUS1( 0x20, "1 db" ), + GAIN_PLUS2A( 0x40, "2 db" ), + GAIN_PLUS2B( 0x60, "2 db" ); + + private int mValue; + private String mLabel; + + private IFStage4Gain( int value, String label ) + { + mValue = value; + mLabel = label; + } + + public static Register getRegister() + { + return Register.GAIN3; + } + + public static byte getMask() + { + return (byte)0x60; + } + + public byte getValue() + { + return (byte)mValue; + } + + public String getLabel() + { + return mLabel; + } + + public String toString() + { + return mLabel; + } + + public static IFStage4Gain fromRegisterValue( int registerValue ) + { + int value = registerValue & getMask(); + + for( IFStage4Gain setting: values() ) + { + if( value == setting.getValue() ) + { + return setting; + } + } + + throw new IllegalArgumentException( "E4KTunerController" + + " - unrecognized IF Gain Stage 4 value [" + + value + "]" ); + } + } + + public enum IFStage5Gain + { + GAIN_PLUS3( 0x0, "3 db" ), + GAIN_PLUS6( 0x1, "6 db" ), + GAIN_PLUS9( 0x2, "9 db" ), + GAIN_PLUS12( 0x3, "12 db" ), + GAIN_PLUS15A( 0x4, "15 db" ), + GAIN_PLUS15B( 0x5, "15 db" ), + GAIN_PLUS15C( 0x6, "15 db" ), + GAIN_PLUS15D( 0x7, "15 db" ); + + private int mValue; + private String mLabel; + + private IFStage5Gain( int value, String label ) + { + mValue = value; + mLabel = label; + } + + public static Register getRegister() + { + return Register.GAIN4; + } + + public static byte getMask() + { + return (byte)0x7; + } + + public byte getValue() + { + return (byte)mValue; + } + + public String getLabel() + { + return mLabel; + } + + public String toString() + { + return mLabel; + } + + public static IFStage5Gain fromRegisterValue( int registerValue ) + { + int value = registerValue & getMask(); + + for( IFStage5Gain setting: values() ) + { + if( value == setting.getValue() ) + { + return setting; + } + } + + throw new IllegalArgumentException( "E4KTunerController" + + " - unrecognized IF Gain Stage 5 value [" + + value + "]" ); + } + + } + + public enum IFStage6Gain + { + GAIN_PLUS3( 0x00, "3 db" ), + GAIN_PLUS6( 0x08, "6 db" ), + GAIN_PLUS9( 0x10, "9 db" ), + GAIN_PLUS12( 0x18, "12 db" ), + GAIN_PLUS15A( 0x20, "15 db" ), + GAIN_PLUS15B( 0x28, "15 db" ), + GAIN_PLUS15C( 0x30, "15 db" ), + GAIN_PLUS15D( 0x38, "15 db" ); + + private int mValue; + private String mLabel; + + private IFStage6Gain( int value, String label ) + { + mValue = value; + mLabel = label; + } + + public static Register getRegister() + { + return Register.GAIN4; + } + + public static byte getMask() + { + return (byte)0x38; + } + + public byte getValue() + { + return (byte)mValue; + } + + public String getLabel() + { + return mLabel; + } + + public String toString() + { + return mLabel; + } + + public static IFStage6Gain fromRegisterValue( int registerValue ) + { + int value = registerValue & getMask(); + + for( IFStage6Gain setting: values() ) + { + if( value == setting.getValue() ) + { + return setting; + } + } + + throw new IllegalArgumentException( "E4KTunerController" + + " - unrecognized IF Gain Stage 6 value [" + + value + "]" ); + } + } + + public enum IFFilter + { + MIX( "Mix" ), + CHAN( "Channel" ), + RC( "RC" ); + + private String mLabel; + + private IFFilter( String label ) + { + mLabel = label; + } + + public String getLabel() + { + return mLabel; + } + } + + public enum AGCMode + { + SERIAL( (byte)0x0 ), + IF_PWM_LNA_SERIAL( (byte)0x1 ), + IF_PWM_LNA_AUTONL( (byte)0x2 ), + IF_PWM_LNA_SUPERV( (byte)0x3 ), + IF_SERIAL_LNA_PWM( (byte)0x4 ), + IF_PWM_LNA_PWM( (byte)0x5 ), + IF_DIG_LNA_SERIAL( (byte)0x6 ), + IF_DIG_LNA_AUTON( (byte)0x7 ), + IF_DIG_LNA_SUPERV( (byte)0x8 ), + IF_SERIAL_LNA_AUTON( (byte)0x9 ), + IF_SERIAL_LNA_SUPERV( (byte)0xA ); + + private byte mValue; + + private AGCMode( byte value ) + { + mValue = value; + } + + public byte getValue() + { + return mValue; + } + } + + public enum Register + { + DUMMY( 0x00 ), + MASTER1( 0x00 ), + MASTER2( 0x01 ), + MASTER3( 0x02 ), + MASTER4( 0x03 ), + MASTER5( 0x04 ), + CLK_INP( 0x05 ), + REF_CLK( 0x06 ), + SYNTH1( 0x07 ), + SYNTH2( 0x08 ), + SYNTH3( 0x09 ), + SYNTH4( 0x0A ), + SYNTH5( 0x0B ), + SYNTH6( 0x0C ), + SYNTH7( 0x0D ), + SYNTH8( 0x0E ), + SYNTH9( 0x0F ), + FILT1( 0x10 ), + FILT2( 0x11 ), + FILT3( 0x12 ), + GAIN1( 0x14 ), + GAIN2( 0x15 ), + GAIN3( 0x16 ), + GAIN4( 0x17 ), + AGC1( 0x1A ), + AGC2( 0x1B ), + AGC3( 0x1C ), + AGC4( 0x1D ), + AGC5( 0x1E ), + AGC6( 0x1F ), + AGC7( 0x20 ), + AGC8( 0x21 ), + AGC11( 0x24 ), + AGC12( 0x25 ), + DC1( 0x29 ), + DC2( 0x2A ), + DC3( 0x2B ), + DC4( 0x2C ), + DC5( 0x2D ), + DC6( 0x2E ), + DC7( 0x2F ), + DC8( 0x30 ), + QLUT0( 0x50 ), + QLUT1( 0x51 ), + QLUT2( 0x52 ), + QLUT3( 0x53 ), + ILUT0( 0x60 ), + ILUT1( 0x61 ), + ILUT2( 0x62 ), + ILUT3( 0x63 ), + DCTIME1( 0x70 ), + DCTIME2( 0x71 ), + DCTIME3( 0x72 ), + DCTIME4( 0x73 ), + PWM1( 0x74 ), + PWM2( 0x75 ), + PWM3( 0x76 ), + PWM4( 0x77 ), + BIAS( 0x78 ), + CLKOUT_PWDN( 0x7A ), + CHFILT_CALIB( 0x7B ), + I2C_REG_ADDR( 0x7D ), + MAGIC_1( 0x7E ), + MAGIC_2( 0x7F ), + MAGIC_3( 0x82 ), + MAGIC_4( 0x86 ), + MAGIC_5( 0x87 ), + MAGIC_6( 0x88 ), + MAGIC_7( 0x9F ), + MAGIC_8( 0xA0 ), + I2C_REGISTER( 0xC8 ); + + private int mValue; + + private Register( int value ) + { + mValue = value; + } + + public byte getValue() + { + return (byte)mValue; + } + } + + @Override + public void apply( TunerConfiguration config ) throws SourceException + { + Log.error( "********* HEY - apply tuner config not yet implemented - R820T tuner controller" ); + } +} diff --git a/src/source/tuner/rtl/r820t/R820TTunerEditorPanel.java b/src/source/tuner/rtl/r820t/R820TTunerEditorPanel.java new file mode 100644 index 00000000..b1d29f83 --- /dev/null +++ b/src/source/tuner/rtl/r820t/R820TTunerEditorPanel.java @@ -0,0 +1,93 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package source.tuner.rtl.r820t; + +import gui.control.JFrequencyControl; + +import java.awt.EventQueue; + +import javax.swing.BorderFactory; +import javax.swing.JPanel; + +import log.Log; +import net.miginfocom.swing.MigLayout; +import source.SourceException; +import source.tuner.FrequencyChangeListener; +import controller.ResourceManager; + +public class R820TTunerEditorPanel extends JPanel implements FrequencyChangeListener +{ + private static final long serialVersionUID = 1L; + + private R820TTunerController mController; + private ResourceManager mResourceManager; + + private JFrequencyControl mFrequencyControl; + + public R820TTunerEditorPanel( R820TTunerController controller, + ResourceManager resourceManager ) + { + mController = controller; + mResourceManager = resourceManager; + + initGUI(); + } + + private void initGUI() + { + setLayout( new MigLayout( "", "[grow,fill]", "[grow,fill]" ) ); + + setBorder( BorderFactory.createTitledBorder( "Rafael Micro R820T Tuner" ) ); + + mFrequencyControl = new JFrequencyControl(); + + mFrequencyControl.addListener( this ); + + mFrequencyControl.setFrequency( mController.getFrequency(), false ); + + add( mFrequencyControl, "wrap" ); + + add( new R820TTunerConfigurationPanel( mResourceManager, mController ), + "wrap" ); + } + + /** + * Frequency change listener for the frequency control gui component to + * apply end-user requested frequency changes against the tuner + */ + @Override + public void frequencyChanged( final int frequency, int bandwidth ) + { + EventQueue.invokeLater( new Runnable() + { + @Override + public void run() + { + try + { + mController.setFrequency( frequency ); + } + catch ( SourceException e ) + { + Log.error( "E4KTunerController - error setting frequency [" + + frequency + "] - " + e.getLocalizedMessage() ); + } + } + } ); + } +} diff --git a/src/source/tuner/usb/USBTunerDevice.java b/src/source/tuner/usb/USBTunerDevice.java new file mode 100644 index 00000000..71ef9c6d --- /dev/null +++ b/src/source/tuner/usb/USBTunerDevice.java @@ -0,0 +1,44 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package source.tuner.usb; + +import javax.usb.UsbDevice; + +import source.tuner.TunerClass; + +public class USBTunerDevice +{ + private UsbDevice mDevice; + private TunerClass mTunerClass; + + public USBTunerDevice( UsbDevice device, TunerClass tunerClass ) + { + mDevice = device; + mTunerClass = tunerClass; + } + + public UsbDevice getDevice() + { + return mDevice; + } + + public TunerClass getTunerClass() + { + return mTunerClass; + } +} diff --git a/src/source/tuner/usb/USBUtils.java b/src/source/tuner/usb/USBUtils.java new file mode 100644 index 00000000..8621adf1 --- /dev/null +++ b/src/source/tuner/usb/USBUtils.java @@ -0,0 +1,114 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package source.tuner.usb; + +import java.io.UnsupportedEncodingException; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +import javax.usb.UsbConfiguration; +import javax.usb.UsbDevice; +import javax.usb.UsbDisconnectedException; +import javax.usb.UsbEndpoint; +import javax.usb.UsbException; +import javax.usb.UsbHostManager; +import javax.usb.UsbHub; +import javax.usb.UsbInterface; +import javax.usb.UsbServices; + +public class USBUtils +{ + /** + * Returns a List of currently connected usb devices + */ + public static List getDevices() + throws SecurityException, UsbException, UnsupportedEncodingException + { + ArrayList devices = new ArrayList(); + + UsbServices services = UsbHostManager.getUsbServices(); + + UsbHub root = services.getRootUsbHub(); + + devices.addAll( getHubDevices( root ) ); + + return devices; + } + + /** + * Returns a list of devices attached to the hub + */ + public static List getHubDevices( UsbHub hub ) + throws UnsupportedEncodingException, UsbException + { + ArrayList devices = new ArrayList(); + + @SuppressWarnings( "unchecked" ) + List children = hub.getAttachedUsbDevices(); + + Iterator it = children.iterator(); + + while( it.hasNext() ) + { + UsbDevice child = it.next(); + + if( child.isUsbHub() ) + { + devices.addAll( getHubDevices( (UsbHub)child ) ); + } + else + { + devices.add( child ); + } + } + + return devices; + } + + public static String getDeviceDetails( UsbDevice device ) + throws UsbException, UnsupportedEncodingException, UsbDisconnectedException + { + StringBuilder sb = new StringBuilder(); + + sb.append( device.getUsbDeviceDescriptor().toString() + "\n\n" ); + + for( Object configObject: device.getUsbConfigurations() ) + { + UsbConfiguration config = (UsbConfiguration)configObject; + + sb.append( config.getUsbConfigurationDescriptor().toString() + "\n\n" ); + + for( Object interfaceObject: config.getUsbInterfaces() ) + { + UsbInterface iface = (UsbInterface)interfaceObject; + + sb.append( iface.getUsbInterfaceDescriptor().toString() + "\n\n" ); + + for( Object endpointObject: iface.getUsbEndpoints() ) + { + UsbEndpoint endpoint = (UsbEndpoint)endpointObject; + + sb.append( endpoint.getUsbEndpointDescriptor().toString() + "\n\n" ); + } + } + } + + return sb.toString(); + } +} diff --git a/src/source/wave/ComplexWaveSource.java b/src/source/wave/ComplexWaveSource.java new file mode 100644 index 00000000..d1f2f273 --- /dev/null +++ b/src/source/wave/ComplexWaveSource.java @@ -0,0 +1,300 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package source.wave; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.nio.ShortBuffer; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Iterator; + +import javax.sound.sampled.AudioInputStream; +import javax.sound.sampled.AudioSystem; +import javax.sound.sampled.UnsupportedAudioFileException; + +import sample.Listener; +import sample.complex.ComplexSample; + +public class ComplexWaveSource extends WaveSource +{ + private boolean mRunning = false; + private boolean mLoop = false; + + private AudioInputStream mInputStream = null; + private int mBytesPerFrame = 0; + private int mFrequency = 0; + + private byte[] mBuffer; + private int mBufferPointer = 0; + private int mBufferLength; + private ShortBuffer mBufferWrapper; + + private static int BUFFER_SAMPLE_SIZE = 2000; + + private ArrayList> mListeners = + new ArrayList>(); + + /** + * Single channel (mono) wave file playback source with optional looping + * for continous playback. + * + * Playback can be invoked manually with the next() and next(x) methods to + * get sample(s). + * + * Plaback can be run automatically by invoking the start() and stop() + * methods. + * + * Registered float sample listener(s) will receive any samples produced + * by the manual or automatic methods. + * + * @param loop - true for looping + * + * @throws FileNotFoundException if filename is not found + */ + public ComplexWaveSource( File file, boolean loop ) throws IOException + { + super( file, SampleType.COMPLEX ); + mLoop = loop; + } + + @Override + public int getSampleRate() + { + return (int)mInputStream.getFormat().getSampleRate(); + } + + /** + * Returns the frequency set for this file. Normally returns zero, but + * the value can be set with setFrequency() method. + */ + public int getFrequency() + { + return mFrequency; + } + + /** + * Changes the value returned from getFrequency() for this source. + */ + public void setFrequency( int frequency ) + { + mFrequency = frequency; + } + + /** + * Closes the audio input stream + * @throws IOException + */ + public void close() throws IOException + { + if( mInputStream != null ) + { + mInputStream.close(); + } + + mInputStream = null; + } + + public void open() throws IOException + { + try + { + mInputStream = AudioSystem.getAudioInputStream( getFile() ); + } + catch( UnsupportedAudioFileException e ) + { + throw new IOException( "MonoWaveSource - unsupported audio file " + + "exception - ", e ); + } + + mBytesPerFrame = mInputStream.getFormat().getFrameSize(); + + if( mBytesPerFrame != 2 || mInputStream.getFormat().getChannels() != 2 ) + { + throw new IOException( "Unsupported Wave Format - requires two " + + "channels with 16-bit samples" ); + } + + mBuffer = new byte[ mBytesPerFrame * BUFFER_SAMPLE_SIZE ]; + + readBuffer(); + } + + + /** + * Reads a sample from the wave file and broadcasts the sample to all + * registered float listeners. + * + * @return - true if data was read or false if no more data. If loop is + * set to true, then this method will continue to produce samples as long + * as there are not IO exceptions. + * + * @throws IOException if unable to read data from the wave file + */ + /** + * Reads the next buffer from the wave file. + * @throws IOException + */ + public void readBuffer() throws IOException + { + if( mInputStream != null ) + { + /* reset the buffer pointer and fill the buffer with zeros */ + mBufferPointer = 0; + mBufferLength = 0; + Arrays.fill( mBuffer, (byte)0 ); + + while( mBufferLength == 0 ) + { + boolean reset = false; + + /* Fill the buffer with samples from the file */ + mBufferLength = (int)( mInputStream.read( mBuffer ) / + mBytesPerFrame ); + + if( reset && mBufferLength == 0 ) + { + return; + } + + /* If we get a partial buffer, reset the file for the next read */ + if( mBufferLength < mBuffer.length && mLoop ) + { + close(); + open(); + + reset = true; + } + } + + if( mBufferLength > 0 ) + { + mBufferWrapper = ByteBuffer.wrap( mBuffer ).order( + ByteOrder.LITTLE_ENDIAN ).asShortBuffer(); + } + else + { + mBufferWrapper = null; + } + } + } + + private boolean next( boolean broadcast ) throws IOException + { + boolean success = false; + + if( mBufferPointer >= mBufferLength ) + { + readBuffer(); + } + + if( mBufferLength != 0 && + mBufferWrapper != null && + mBufferWrapper.hasRemaining() ) + { + short i = mBufferWrapper.get(); + short q = mBufferWrapper.get(); + + mBufferPointer++; + incrementCurrentLocation( 1, false ); + + if( broadcast ) + { + send( new ComplexSample( (float)i, (float)q ) ); + } + + success = true; + } + + return success; + } + + public boolean next() throws IOException + { + return next( true ); + } + + /** + * Reads the next (count) samples from the wave file and broadcasts them + * to the registered listeners. + * @param count - number of samples to broadcast + * @return - true if successful + * @throws IOException for any IO issues + */ + public boolean next( int count ) throws IOException + { + for( int x = 0; x < count; x++ ) + { + next(); + } + + return true; + } + + @Override + public void jumpTo( long index ) throws IOException + { + if( index < mCurrentPosition ) + { + close(); + open(); + } + + while( mCurrentPosition < index ) + { + next( false ); + } + + incrementCurrentLocation( 0, true ); + } + + /** + * Broadcasts a sample to the registered listeners + */ + private void send( ComplexSample sample ) + { + Iterator> it = mListeners.iterator(); + + while( it.hasNext() ) + { + it.next().receive( sample ); + } + } + + /** + * Adds a new listener to receive samples as they are read from the wave file + */ + public void addListener( Listener listener ) + { + mListeners.add( listener ); + } + + public void removeListener( Listener listener ) + { + mListeners.remove( listener ); + } + + @Override + public void dispose() + { + /* Method not implemented */ + } +} diff --git a/src/source/wave/FloatWaveSource.java b/src/source/wave/FloatWaveSource.java new file mode 100644 index 00000000..ee81d7ee --- /dev/null +++ b/src/source/wave/FloatWaveSource.java @@ -0,0 +1,290 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package source.wave; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.nio.ShortBuffer; +import java.util.Arrays; + +import javax.sound.sampled.AudioInputStream; +import javax.sound.sampled.AudioSystem; +import javax.sound.sampled.UnsupportedAudioFileException; + +import sample.Listener; + +public class FloatWaveSource extends WaveSource +{ + private boolean mRunning = false; + private boolean mLoop = false; + + private AudioInputStream mInputStream = null; + private int mBytesPerFrame = 0; + private int mFrequency = 0; + + private byte[] mBuffer; + private int mBufferPointer = 0; + private int mBufferLength; + private ShortBuffer mBufferWrapper; + private Listener mListener; + + private static int BUFFER_SAMPLE_SIZE = 2000; + + + /** + * Single channel (mono) wave file playback source with optional looping + * for continous playback. + * + * Playback can be invoked manually with the next() and next(x) methods to + * get sample(s). + * + * Plaback can be run automatically by invoking the start() and stop() + * methods. + * + * Registered float sample listener(s) will receive any samples produced + * by the manual or automatic methods. + * + * @param filename - wave filename + * + * @param loop - true for looping + * + * @throws FileNotFoundException if filename is not found + */ + public FloatWaveSource( File file, boolean loop ) throws IOException + { + super( file, SampleType.FLOAT ); + mLoop = loop; + } + + @Override + public int getSampleRate() + { + return (int)mInputStream.getFormat().getSampleRate(); + } + + /** + * Returns the frequency set for this file. Normally returns zero, but + * the value can be set with setFrequency() method. + */ + public int getFrequency() + { + return mFrequency; + } + + /** + * Changes the value returned from getFrequency() for this source. + */ + public void setFrequency( int frequency ) + { + mFrequency = frequency; + } + + /** + * Closes the audio input stream + * @throws IOException + */ + public void close() throws IOException + { + if( mInputStream != null ) + { + mInputStream.close(); + } + + mInputStream = null; + } + + public void open() throws IOException + { + if( mFile == null || !mFile.isFile() ) + { + throw new IOException( "Empty or null file" ); + } + + try + { + mInputStream = AudioSystem.getAudioInputStream( getFile() ); + } + catch( UnsupportedAudioFileException e ) + { + throw new IOException( "MonoWaveSource - unsupported audio file " + + "exception - ", e ); + } + + mBytesPerFrame = mInputStream.getFormat().getFrameSize(); + + if( mBytesPerFrame != 2 || mInputStream.getFormat().getChannels() != 1 ) + { + throw new IOException( "Unsupported Wave Format - requires mono " + + "channel with 16-bit samples" ); + } + + mBuffer = new byte[ mBytesPerFrame * BUFFER_SAMPLE_SIZE ]; + + readBuffer(); + } + + + /** + * Reads a sample from the wave file and broadcasts the sample to all + * registered float listeners. + * + * @return - true if data was read or false if no more data. If loop is + * set to true, then this method will continue to produce samples as long + * as there are not IO exceptions. + * + * @throws IOException if unable to read data from the wave file + */ + /** + * Reads the next buffer from the wave file. + * @throws IOException + */ + public void readBuffer() throws IOException + { + if( mInputStream != null ) + { + /* reset the buffer pointer and fill the buffer with zeros */ + mBufferPointer = 0; + mBufferLength = 0; + Arrays.fill( mBuffer, (byte)0 ); + + while( mBufferLength == 0 ) + { + boolean reset = false; + + /* Fill the buffer with samples from the file */ + mBufferLength = (int)( mInputStream.read( mBuffer ) / + mBytesPerFrame ); + + if( reset && mBufferLength == 0 ) + { + return; + } + + /* If we get a partial buffer, reset the file for the next read */ + if( mBufferLength < mBuffer.length && mLoop ) + { + close(); + open(); + + reset = true; + } + } + + if( mBufferLength > 0 ) + { + mBufferWrapper = ByteBuffer.wrap( mBuffer ).order( + ByteOrder.LITTLE_ENDIAN ).asShortBuffer(); + } + else + { + mBufferWrapper = null; + } + } + } + + private boolean next( boolean broadcast ) throws IOException + { + boolean success = false; + + if( mBufferPointer >= mBufferLength ) + { + readBuffer(); + } + + if( mBufferLength != 0 && + mBufferWrapper != null && + mBufferWrapper.hasRemaining() ) + { + short sample = mBufferWrapper.get(); + + mBufferPointer++; + incrementCurrentLocation( 1, false ); + + if( broadcast && mListener != null ) + { + /* Convert the short into a float value */ + mListener.receive( (float)sample ); + } + + success = true; + } + + return success; + } + + public boolean next() throws IOException + { + return next( true ); + } + + /** + * Reads the next (count) samples from the wave file and broadcasts them + * to the registered listeners. + * @param count - number of samples to broadcast + * @return - true if successful + * @throws IOException for any IO issues + */ + public boolean next( int count ) throws IOException + { + for( int x = 0; x < count; x++ ) + { + next(); + } + + return true; + } + + @Override + public void jumpTo( long index ) throws IOException + { + if( index < mCurrentPosition ) + { + close(); + open(); + } + + while( mCurrentPosition < index ) + { + next( false ); + } + + incrementCurrentLocation( 0, true ); + } + + /** + * Adds a new listener to receive samples as they are read from the wave file + */ + public void setListener( Listener listener ) + { + mListener = listener; + } + + public void removeListener( Listener listener ) + { + mListener = null; + } + + @Override + public void dispose() + { + mListener = null; + } +} diff --git a/src/source/wave/WaveSource.java b/src/source/wave/WaveSource.java new file mode 100644 index 00000000..55956520 --- /dev/null +++ b/src/source/wave/WaveSource.java @@ -0,0 +1,76 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package source.wave; + +import java.io.File; +import java.io.IOException; + +import source.Source; + +public abstract class WaveSource extends Source +{ + protected File mFile; + protected long mCurrentPosition = 0; + protected PositionListener mListener; + + public WaveSource( File file, SampleType sampleType ) + { + super( sampleType.toString() + " Wave Source", sampleType ); + mFile = file; + } + + public File getFile() + { + return mFile; + } + + public long getCurrentPosition() + { + return mCurrentPosition; + } + + public void incrementCurrentLocation( long value, boolean reset ) + { + mCurrentPosition += value; + + if( mListener != null ) + { + mListener.positionUpdated( mCurrentPosition, reset ); + } + } + + public void addListener( PositionListener listener ) + { + mListener = listener; + } + + public abstract void open() throws IOException; + + public abstract void close() throws IOException; + + public abstract boolean next() throws IOException; + + public abstract boolean next( int count ) throws IOException; + + public abstract void jumpTo( long index ) throws IOException; + + public interface PositionListener + { + public void positionUpdated( long position, boolean reset ); + } +} diff --git a/src/spectrum/.directory b/src/spectrum/.directory new file mode 100644 index 00000000..18c505d9 --- /dev/null +++ b/src/spectrum/.directory @@ -0,0 +1,4 @@ +[Dolphin] +Timestamp=2014,2,4,19,35,46 +Version=3 +ViewMode=1 diff --git a/src/spectrum/DFTProcessor.java b/src/spectrum/DFTProcessor.java new file mode 100644 index 00000000..09b06295 --- /dev/null +++ b/src/spectrum/DFTProcessor.java @@ -0,0 +1,546 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package spectrum; + +import java.util.Arrays; +import java.util.Iterator; +import java.util.concurrent.ArrayBlockingQueue; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; + +import log.Log; +import sample.Listener; +import source.Source; +import source.Source.SampleType; +import source.tuner.FrequencyChangeListener; +import dsp.filter.Window; +import dsp.filter.Window.WindowType; +import edu.emory.mathcs.jtransforms.fft.FloatFFT_1D; + +/** + * Processes both complex samples or float samples and dispatches a float array + * of DFT results, using configurable fft size and output dispatch timelines. + */ +public class DFTProcessor implements Listener, + DFTResultsProvider, + FrequencyChangeListener +{ + private CopyOnWriteArrayList mListeners = + new CopyOnWriteArrayList(); + + private ArrayBlockingQueue mQueue = + new ArrayBlockingQueue( 200 ); + + private ScheduledExecutorService mScheduler = + Executors.newScheduledThreadPool(1); + + private FFTWidth mFFTWidth = FFTWidth.FFT04096; + private FFTWidth mNewFFTWidth = FFTWidth.FFT04096; + + private double[] mWindow; + private WindowType mWindowType = Window.WindowType.HANNING; + + private FloatFFT_1D mFFT = new FloatFFT_1D( mFFTWidth.getWidth() ); + + private int mFrameRate; + private int mSampleRate; + private float mOverlapPerFrame; + private float mOverlapResidual; + private float[] mPreviousFrame = new float[ 8192 ]; + + private Float[] mCurrentBuffer; + private int mCurrentBufferPointer = 0; + + private SampleType mSampleType; + + private AtomicBoolean mRunning = new AtomicBoolean(); + + public DFTProcessor( SampleType sampleType ) + { + setSampleType( sampleType ); + setFrameRate( 20 ); + } + + public void dispose() + { + try + { + clearScheduler(); + } + catch ( InterruptedException e ) + { + Log.error( "DFTProcessor - exception shutting down processor - " + + e.getLocalizedMessage() ); + } + + mScheduler.shutdown(); + + mListeners.clear(); + mQueue.clear(); + mWindow = null; + mCurrentBuffer = null; + } + + public WindowType getWindowType() + { + return mWindowType; + } + + public void setWindowType( WindowType windowType ) + { + mWindowType = windowType; + + if( mSampleType == SampleType.COMPLEX ) + { + mWindow = Window.getWindow( mWindowType, + mFFTWidth.getWidth() * 2 ); + } + else + { + mWindow = Window.getWindow( mWindowType, + mFFTWidth.getWidth() ); + } + } + + /** + * Sets the processor mode to Float or Complex, depending on the sample + * types that will be delivered for processing + */ + public void setSampleType( SampleType type ) + { + mSampleType = type; + setWindowType( mWindowType ); + } + + public Source.SampleType getSampleType() + { + return mSampleType; + } + + /** + * Queues an FFT size change request. The scheduled executor will apply + * the change when it runs. + */ + public void setFFTSize( FFTWidth width ) + { + mNewFFTWidth = width; + } + + public FFTWidth getFFTWidth() + { + return mFFTWidth; + } + + public int getFrameRate() + { + return mFrameRate; + } + + public void setFrameRate( int framesPerSecond ) + { + //TODO: make sure frame rate & sample rate sample requirement doesn't + //expect overlap greater than the previous frame length + + if( framesPerSecond < 1 || framesPerSecond > 1000 ) + { + throw new IllegalArgumentException( "DFTProcessor cannot run " + + "more than 1000 times per second -- requested setting:" + + framesPerSecond ); + } + else + { + mFrameRate = framesPerSecond; + + calculateOverlap(); + + /** + * Shutdown the scheduler and clear out any remaining tasks + */ + try + { + clearScheduler(); + } + catch ( InterruptedException e ) + { + Log.error( "DFTProcessor - exception while awaiting shutdown of " + + "calculation scheduler for reset" ); + } + + /** + * Reset the scheduler + */ + mScheduler = Executors.newScheduledThreadPool(1); + + /** + * Schedule the DFT to run calculations at a fixed rate + */ + int initialDelay = 0; + int period = (int)( 1000 / mFrameRate ); + TimeUnit unit = TimeUnit.MILLISECONDS; + + mScheduler.scheduleAtFixedRate( new DFTCalculationTask(), + initialDelay, period, unit ); + + } + } + + /** + * Shuts down the scheduler and blocks up to 100 milliseconds until all + * tasks have completed. + */ + private void clearScheduler() throws InterruptedException + { + mScheduler.shutdown(); + + mScheduler.awaitTermination( 100, TimeUnit.MILLISECONDS ); + } + + public int getCalculationsPerSecond() + { + return mFrameRate; + } + + /** + * Places the sample into a transfer queue for future processing. + */ + @Override + public void receive( Float[] samples ) + { + if( !mQueue.offer( samples ) ) + { + Log.error( "DFTProcessor - queue is full, purging queue, samples[" + samples + "]" ); + mQueue.clear(); + mQueue.offer( samples ); + } + } + + private void getNextBuffer() + { + mCurrentBuffer = null; + + long start = System.currentTimeMillis(); + + try + { + mCurrentBuffer = mQueue.take(); + } + catch ( InterruptedException e ) + { + mCurrentBuffer = null; + } + + mCurrentBufferPointer = 0; + } + + private float[] getSamples() + { + int remaining; + + if( mSampleType == SampleType.COMPLEX ) + { + remaining = mFFTWidth.getWidth() * 2; + } + else + { + remaining = mFFTWidth.getWidth(); + } + + float[] currentFrame = new float[ mFFTWidth.getWidth() * 2 ]; + int currentFramePointer = 0; + + /* Get the integer component of the overlap, leaving the fractional + * residual for the next frame */ + float requiredOverlap = mOverlapPerFrame + mOverlapResidual; + int overlap = (int)requiredOverlap; + mOverlapResidual = requiredOverlap - overlap; + + /* If positive overlap, we have to re-use samples from previous frame */ + if( overlap > 0 ) + { + int previousFramePointer; + int length; + + if( mSampleType == SampleType.COMPLEX ) + { + length = overlap * 2; + previousFramePointer = mPreviousFrame.length - length; + } + else + { + length = overlap; + previousFramePointer = (int)(mPreviousFrame.length / 2) - length; + } + + System.arraycopy( mPreviousFrame, previousFramePointer, + currentFrame, currentFramePointer, length ); + + remaining -= length; + currentFramePointer += length; + } + + /* If negative overlap, we have to throw away samples */ + else + { + if( mSampleType == SampleType.COMPLEX ) + { + purge( -( overlap * 2 ) ); + } + else + { + purge( -overlap ); + } + } + + while( mRunning.get() && remaining > 0 ) + { + if( mCurrentBuffer == null || + mCurrentBufferPointer == mCurrentBuffer.length ) + { + getNextBuffer(); + } + + int samplesAvailable = mCurrentBuffer.length - mCurrentBufferPointer; + + if( samplesAvailable >= remaining ) + { + while( remaining > 0 ) + { + currentFrame[ currentFramePointer ] = + (float)mCurrentBuffer[ mCurrentBufferPointer ]; + + currentFramePointer++; + mCurrentBufferPointer++; + remaining --; + } + + mCurrentBufferPointer += remaining; + remaining = 0; + } + else + { + while( samplesAvailable > 0 ) + { + currentFrame[ currentFramePointer ] = + (float)mCurrentBuffer[ mCurrentBufferPointer ]; + + currentFramePointer++; + mCurrentBufferPointer++; + samplesAvailable--; + remaining--; + } + } + } + + /* If frames are overlapping, store current frame to use next time */ + if( mOverlapPerFrame > 0 ) + { + mPreviousFrame = null; + mPreviousFrame = Arrays.copyOf( currentFrame, currentFrame.length ); + } + + return currentFrame; + } + + private void calculate() + { + float[] samples = getSamples(); + + Window.apply( mWindow, samples ); + + if( mSampleType == SampleType.FLOAT ) + { + mFFT.realForwardFull( samples ); + } + else + { + mFFT.complexForward( samples ); + } + + dispatch( samples ); + } + + + private void purge( int samplesToPurge ) + { + if( samplesToPurge <= 0 ) + { + throw new IllegalArgumentException( "DFTProcessor - cannot purge " + + "negative sample amount" ); + } + + while( mRunning.get() && samplesToPurge > 0 ) + { + if( mCurrentBuffer == null || + mCurrentBufferPointer == mCurrentBuffer.length ) + { + getNextBuffer(); + } + + int samplesAvailable = mCurrentBuffer.length - mCurrentBufferPointer; + + if( samplesAvailable >= samplesToPurge ) + { + mCurrentBufferPointer += samplesToPurge; + samplesToPurge = 0; + } + else + { + samplesToPurge -= samplesAvailable; + mCurrentBufferPointer = mCurrentBuffer.length; + } + } + } + + /** + * Takes a calculated DFT results set, reformats the data, and sends it + * out to all registered listeners. + */ + private void dispatch( float[] results ) + { + float dc = Math.abs( results[ 0 ] ); + + /* Ensure we have power in the DC power bin (bin 0), otherwise don't + * dispatch the results */ + if( dc != 0 ) + { + float[] processed = new float[ results.length / 2 ]; + + int half = results.length / 2; + int quarter = results.length / 4; + + for( int x = 2; x < half; x += 2 ) + { + processed[ quarter + ( x / 2 ) ] = + (float)( Math.log10( + (double)( Math.abs( results[ x ] ) / + dc ) ) ); + } + + for( int x = 0; x < quarter - 1; x++ ) + { + processed[ x ] = + (float)( Math.log10( + (double)( Math.abs( results[ half + ( ( x + 1 ) * 2 ) ] ) / + dc ) ) ); + } + + //Real bin n/2 -1 + processed[ quarter - 1 ] = (float)( Math.log10( + (double)( Math.abs( results[ 1 ] ) / dc ) ) ); + + Iterator it = mListeners.iterator(); + + while( it.hasNext() ) + { + it.next().receive( processed ); + } + } + + results = null; + } + + @Override + public void addListener( DFTResultsListener listener ) + { + mListeners.add( listener ); + } + + @Override + public void removeListener( DFTResultsListener listener ) + { + mListeners.remove( listener ); + } + + private class DFTCalculationTask implements Runnable + { + @Override + public void run() + { + /* Only run if we're not currently running */ + if( mRunning.compareAndSet( false, true ) ) + { + checkFFTSize(); + + calculate(); + + mRunning.set( false ); + } + else + { + Log.error( "DFTProcessor - calculation already in progress" + + " - skipping this iteration" ); + } + } + } + + /** + * Checks for a queued FFT width change request and applies it. This + * method will only be accessed by the scheduled executor that gains + * access to run a calculate method, thus providing thread safety. + */ + private void checkFFTSize() + { + if( mNewFFTWidth.getWidth() != mFFTWidth.getWidth() ) + { + mFFTWidth = mNewFFTWidth; + + calculateOverlap(); + + mWindow = Window.getWindow( WindowType.HANNING, + ( mFFTWidth.getWidth() * 2 ) ); + + mPreviousFrame = new float[ mFFTWidth.getWidth() * 2 ]; + + mFFT = new FloatFFT_1D( mFFTWidth.getWidth() ); + } + } + + public void clearBuffer() + { + mQueue.clear(); + } + + @Override + public void frequencyChanged( int frequency, int bandwidth ) + { + mSampleRate = bandwidth; + + calculateOverlap(); + } + + /** + * Positive overlap means we have to re-use samples from the previous + * frame + * + * Negative overlap means we have to throw away samples + */ + private void calculateOverlap() + { + mOverlapPerFrame = 0.0f; + mOverlapResidual = 0.0f; + + int requiredSampleRate = mFFTWidth.getWidth() * mFrameRate; + + int overlap = requiredSampleRate - mSampleRate; + + mOverlapPerFrame = (float)overlap / (float)mFrameRate; + } +} diff --git a/src/spectrum/DFTResultsListener.java b/src/spectrum/DFTResultsListener.java new file mode 100644 index 00000000..18b2fb11 --- /dev/null +++ b/src/spectrum/DFTResultsListener.java @@ -0,0 +1,26 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package spectrum; + +/** + * Interface for passing the output of DFT processing + */ +public interface DFTResultsListener +{ + public void receive( float[] results ); +} diff --git a/src/spectrum/DFTResultsProvider.java b/src/spectrum/DFTResultsProvider.java new file mode 100644 index 00000000..b26f8ebb --- /dev/null +++ b/src/spectrum/DFTResultsProvider.java @@ -0,0 +1,25 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package spectrum; + +public interface DFTResultsProvider +{ + public void addListener( DFTResultsListener listener ); + + public void removeListener( DFTResultsListener listener ); +} diff --git a/src/spectrum/FFTWidth.java b/src/spectrum/FFTWidth.java new file mode 100644 index 00000000..17e52cdd --- /dev/null +++ b/src/spectrum/FFTWidth.java @@ -0,0 +1,46 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package spectrum; + +public enum FFTWidth +{ + FFT00512( 512 ), + FFT01024( 1024 ), + FFT02048( 2048 ), + FFT04096( 4096 ), + FFT08192( 8192 ), + FFT16384( 16384 ), + FFT32768( 32768 ); + + private int mWidth; + + private FFTWidth( int width ) + { + mWidth = width; + } + + public int getWidth() + { + return mWidth; + } + + public String getLabel() + { + return String.valueOf( mWidth ); + } +} diff --git a/src/spectrum/OverlayPanel.java b/src/spectrum/OverlayPanel.java new file mode 100644 index 00000000..a54a70bc --- /dev/null +++ b/src/spectrum/OverlayPanel.java @@ -0,0 +1,590 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package spectrum; + +import java.awt.Color; +import java.awt.Font; +import java.awt.FontMetrics; +import java.awt.Graphics; +import java.awt.Graphics2D; +import java.awt.Point; +import java.awt.RenderingHints; +import java.awt.geom.Line2D; +import java.awt.geom.Rectangle2D; +import java.text.DecimalFormat; +import java.util.ArrayList; +import java.util.concurrent.CopyOnWriteArrayList; + +import javax.swing.JPanel; + +import settings.ColorSetting; +import settings.ColorSetting.ColorSettingName; +import settings.Setting; +import settings.SettingChangeListener; +import source.tuner.FrequencyChangeListener; +import source.tuner.TunerChannel; +import controller.ResourceManager; +import controller.channel.Channel; +import controller.channel.Channel.ChannelEvent; +import controller.channel.Channel.ChannelType; +import controller.channel.ChannelListener; + +public class OverlayPanel extends JPanel + implements ChannelListener, + FrequencyChangeListener, + SettingChangeListener +{ + private static final long serialVersionUID = 1L; + + private static DecimalFormat sFORMAT = new DecimalFormat( "000.000" ); + private static DecimalFormat sCURSOR_FORMAT = new DecimalFormat( "000.00000" ); + private int mFrequency = 0; + private int mBandwidth = 0; + private Point mCursorLocation = new Point( 0, 0 ); + private boolean mCursorVisible = false; + + /** + * Colors used by this component + */ + private Color mColorChannelConfig; + private Color mColorChannelConfigProcessing; + private Color mColorChannelConfigSelected; + private Color mColorSpectrumBackground; + private Color mColorSpectrumCursor; + private Color mColorSpectrumLine; + + //All channels + private ArrayList mChannels = new ArrayList(); + + //Currently visible/displayable channels + private CopyOnWriteArrayList mVisibleChannels = + new CopyOnWriteArrayList(); + + //Defines the offset at the bottom of the spectral display to account for + //the frequency labels + private float mSpectrumInset = 20.0f; + + private ResourceManager mResourceManager; + + /** + * Translucent overlay panel for displaying channel configurations, + * processing channels, selected channels, frequency labels and lines, and + * a cursor with a frequency readout. + */ + public OverlayPanel( ResourceManager resourceManager ) + { + mResourceManager = resourceManager; + + //Set the background transparent, so the spectrum display can be seen + setOpaque( false ); + + //Fetch color settings from settings manager + setColors(); + } + + public void dispose() + { + mChannels.clear(); + mVisibleChannels.clear(); + + mResourceManager = null; + } + + public void setCursorLocation( Point point ) + { + mCursorLocation = point; + + repaint(); + } + + public void setCursorVisible( boolean visible ) + { + mCursorVisible = visible; + + repaint(); + } + + /** + * Fetches the color settings from the settings manager + */ + private void setColors() + { + mColorChannelConfig = getColor( ColorSettingName.CHANNEL_CONFIG ); + + mColorChannelConfigProcessing = + getColor( ColorSettingName.CHANNEL_CONFIG_PROCESSING ); + + mColorChannelConfigSelected = + getColor( ColorSettingName.CHANNEL_CONFIG_SELECTED ); + + mColorSpectrumCursor = getColor( ColorSettingName.SPECTRUM_CURSOR ); + + mColorSpectrumLine = getColor( ColorSettingName.SPECTRUM_LINE ); + + mColorSpectrumBackground = + getColor( ColorSettingName.SPECTRUM_BACKGROUND ); + } + + /** + * Fetches a named color setting from the settings manager. If the setting + * doesn't exist, creates the setting using the defaultColor + */ + private Color getColor( ColorSettingName name ) + { + ColorSetting setting = + mResourceManager.getSettingsManager() + .getColorSetting( name ); + + return setting.getColor(); + } + + /** + * Monitors for setting changes. Colors can be changed by external actions + * and will automatically update in this class + */ + @Override + public void settingChanged( Setting setting ) + { + if( setting instanceof ColorSetting ) + { + ColorSetting colorSetting = (ColorSetting)setting; + + switch( colorSetting.getColorSettingName() ) + { + case CHANNEL_CONFIG: + mColorChannelConfig = colorSetting.getColor(); + break; + case CHANNEL_CONFIG_PROCESSING: + mColorChannelConfigProcessing = colorSetting.getColor(); + break; + case CHANNEL_CONFIG_SELECTED: + mColorChannelConfigSelected = colorSetting.getColor(); + break; + case SPECTRUM_BACKGROUND: + mColorSpectrumBackground = colorSetting.getColor(); + break; + case SPECTRUM_CURSOR: + mColorSpectrumCursor = colorSetting.getColor(); + break; + case SPECTRUM_LINE: + mColorSpectrumLine = colorSetting.getColor(); + break; + } + } + } + + /** + * Renders the channel configs, lines, labels, and cursor + */ + @Override + public void paintComponent( Graphics g ) + { + super.paintComponent( g ); + + Graphics2D graphics = (Graphics2D) g; + graphics.setBackground( mColorSpectrumBackground ); + + RenderingHints renderHints = + new RenderingHints( RenderingHints.KEY_ANTIALIASING, + RenderingHints.VALUE_ANTIALIAS_ON ); + + renderHints.put( RenderingHints.KEY_RENDERING, + RenderingHints.VALUE_RENDER_QUALITY ); + + graphics.setRenderingHints( renderHints ); + + drawFrequencies( graphics ); + drawChannels( graphics ); + drawCursor( graphics ); + } + + /** + * Draws a cursor on the panel, whenever the mouse is hovering over the + * panel + */ + private void drawCursor( Graphics2D graphics ) + { + if( mCursorVisible ) + { + drawFrequencyLine( graphics, + mCursorLocation.x, + mColorSpectrumCursor ); + + String frequency = sCURSOR_FORMAT.format( + getFrequencyFromAxis( mCursorLocation.getX() ) / 1000000.0D ); + + graphics.drawString( frequency , + mCursorLocation.x + 5, + mCursorLocation.y ); + } + } + + /** + * Draws the frequency lines and labels every 10kHz + */ + private void drawFrequencies( Graphics2D graphics ) + { + int minFrequency = getMinFrequency(); + int maxFrequency = getMaxFrequency(); + +//TODO: the increment should be changeable according to the overall bandwidth +// so that when we move to higher sample rates, the display reacts accordingly + + int frequency; + + if( mBandwidth < 200000 ) + { + //Start with the first diplayable frequency (ie whole 10kHz increment) + frequency = minFrequency - ( minFrequency % 10000 ); + frequency += 10000; + + //Draw a line and label at each 10kHz interval + while( frequency < maxFrequency ) + { + drawFrequencyLineAndLabel( graphics, frequency ); + + frequency += 10000; + } + } + else + { + //Start with the first diplayable frequency (ie whole 100kHz increment) + frequency = minFrequency - ( minFrequency % 100000 ); + frequency += 100000; + + //Draw a line and label at each 10kHz interval + while( frequency < maxFrequency ) + { + drawFrequencyLineAndLabel( graphics, frequency ); + + frequency += 100000; + } + } + } + + /** + * Draws a vertical line and a corresponding frequency label at the bottom + */ + private void drawFrequencyLineAndLabel( Graphics2D graphics, int frequency ) + { + float xAxis = getAxisFromFrequency( frequency ); + + drawFrequencyLine( graphics, xAxis, mColorSpectrumLine ); + + graphics.setColor( mColorSpectrumLine ); + + drawFrequencyLabel( graphics, xAxis, frequency ); + } + + /** + * Draws a vertical line at the xaxis + */ + private void drawFrequencyLine( Graphics2D graphics, float xaxis, Color color ) + { + graphics.setColor( color ); + + graphics.draw( new Line2D.Float( xaxis, 0, + xaxis, (float)(getSize().getHeight()) - mSpectrumInset ) ); + } + + /** + * Returns the x-axis value corresponding to the frequency + */ + public float getAxisFromFrequency( int frequency ) + { + float canvasMiddle = (float)getSize().getWidth() / 2; + + //Determine frequency offset from middle + int frequencyOffset = mFrequency - frequency; + + //Determine ratio of offset to bandwidth + float ratio = (float)frequencyOffset / (float)mBandwidth; + + //Calculate offset against the total width + float xOffset = (float)getSize().getWidth() * ratio; + + //Apply the offset against the canvas middle + return canvasMiddle - xOffset; + } + + /** + * Returns the frequency corresponding to the x-axis value + */ + public int getFrequencyFromAxis( double xAxis ) + { + float width = (float)getSize().getWidth(); + + double offset = xAxis / width; + + return getMinFrequency() + (int)( mBandwidth * offset ); + } + + /** + * Draws a frequency label at the x-axis position, at the bottom of the panel + */ + private void drawFrequencyLabel( Graphics2D graphics, + float xaxis, + int frequency ) + { + String label = sFORMAT.format( (float)frequency / 1000000.0f ); + + FontMetrics fontMetrics = graphics.getFontMetrics( this.getFont() ); + + Rectangle2D rect = fontMetrics.getStringBounds( label, graphics ); + + float offset = (float)rect.getWidth() / 2; + + graphics.drawString( label, xaxis - offset, + (float)getSize().getHeight() - ( mSpectrumInset * 0.2f ) ); + } + + + /** + * Draws visible channel configs as translucent shaded frequency regions + */ + private void drawChannels( Graphics2D graphics ) + { + for( Channel channel: mVisibleChannels ) + { + //Choose the correct background color to use + if( channel.getSelected() ) + { + graphics.setColor( mColorChannelConfigSelected ); + } + else if( channel.isProcessing() ) + { + graphics.setColor( mColorChannelConfigProcessing ); + } + else + { + graphics.setColor( mColorChannelConfig ); + } + + TunerChannel tunerChannel = channel.getTunerChannel(); + + if( tunerChannel != null ) + { + float xAxis = getAxisFromFrequency( + (int)( tunerChannel.getFrequency() ) ); + + float width = (float)( tunerChannel.getBandwidth() ) / + (float)mBandwidth * (float)getSize().getWidth(); + + Rectangle2D.Float box = + new Rectangle2D.Float( xAxis - ( width / 2 ), + 0, + width, + (float)( getSize().getHeight() - + mSpectrumInset ) ); + + //Fill the box with the correct color + graphics.fill( box ); + + graphics.draw( box ); + + //Change to the line color to render the channel name, etc. + graphics.setColor( mColorSpectrumLine ); + + //Draw the labels starting at yAxis position 0 + float yAxis = 0; + + //Draw the system label and adjust the y-axis position + yAxis += drawLabel( graphics, + channel.getSystem().getName(), + this.getFont(), + xAxis, + yAxis, + width ); + + //Draw the site label and adjust the y-axis position + yAxis += drawLabel( graphics, + channel.getSite().getName(), + this.getFont(), + xAxis, + yAxis, + width ); + + //Draw the channel label and adjust the y-axis position + yAxis += drawLabel( graphics, + channel.getName(), + this.getFont(), + xAxis, + yAxis, + width ); + + //Draw the decoder label + drawLabel( graphics, + channel.getDecodeConfiguration().getDecoderType() + .getShortDisplayString(), + this.getFont(), + xAxis, + yAxis, + width ); + } + } + } + /** + * Draws a textual label at the x/y position, clipping the end of the text + * to fit within the maxwidth value. + * + * @return height of the drawn label + */ + private float drawLabel( Graphics2D graphics, String text, Font font, + float x, float baseY, float maxWidth ) + { + FontMetrics fontMetrics = graphics.getFontMetrics( font ); + + Rectangle2D label = fontMetrics.getStringBounds( text, graphics ); + + float offset = (float)label.getWidth() / 2; + float y = baseY + (float)label.getHeight(); + + /** + * If the label is wider than the max width, left justify the text and + * clip the end of it + */ + if( offset > ( maxWidth / 2 ) ) + { + label.setRect( x - ( maxWidth / 2 ), + y - label.getHeight(), + maxWidth, + label.getHeight() ); //* 2 + + graphics.setClip( label ); + + graphics.drawString( text, x - ( maxWidth / 2 ), y ); + + graphics.setClip( null ); + } + else + { + graphics.drawString( text, x - offset, y ); + } + + return (float)label.getHeight(); + } + + /** + * Frequency change event handler + */ + @Override + public void frequencyChanged( int frequency, int bandwidth ) + { + mBandwidth = bandwidth; + mFrequency = frequency; + + /** + * Reset the visible channel configs list + */ + mVisibleChannels.clear(); + + int minimum = getMinFrequency(); + int maximum = getMaxFrequency(); + + for( Channel channel: mChannels ) + { + if( channel.isWithin( minimum, maximum ) ) + { + mVisibleChannels.add( channel ); + } + } + } + + /** + * Channel change event handler + */ + @Override + @SuppressWarnings( "incomplete-switch" ) + public void occurred( Channel channel, ChannelEvent component ) + { + switch( component ) + { + case CHANNEL_ADDED: + if( !mChannels.contains( channel ) ) + { + mChannels.add( channel ); + } + + if( channel.isWithin( getMinFrequency(), getMaxFrequency() ) && + !mVisibleChannels.contains( channel ) ) + { + mVisibleChannels.add( channel ); + } + break; + case CHANNEL_DELETED: + mChannels.remove( channel ); + mVisibleChannels.remove( channel ); + break; + case PROCESSING_STOPPED: + if( channel.getChannelType() == ChannelType.TRAFFIC ) + { + mChannels.remove( channel ); + mVisibleChannels.remove( channel ); + } + break; + } + + repaint(); + } + + /** + * Currently displayed minimum frequency + */ + private int getMinFrequency() + { + return mFrequency - ( mBandwidth / 2 ); + } + + /** + * Currently displayed maximum frequency + */ + private int getMaxFrequency() + { + return mFrequency + ( mBandwidth / 2 ); + } + + /** + * Returns a list of channel configs that contain the frequency within their + * min/max frequency settings. + */ + public ArrayList getChannelsAtFrequency( int frequency ) + { + ArrayList configs = new ArrayList(); + + for( Channel config: mVisibleChannels ) + { + TunerChannel channel = config.getTunerChannel(); + + if( channel != null && + channel.getMinFrequency() <= frequency && + channel.getMaxFrequency() >= frequency ) + { + configs.add( config ); + } + } + + return configs; + } + + @Override + public void settingDeleted( Setting setting ) + { + // TODO Auto-generated method stub + + } +} diff --git a/src/spectrum/Pausable.java b/src/spectrum/Pausable.java new file mode 100644 index 00000000..bc752e40 --- /dev/null +++ b/src/spectrum/Pausable.java @@ -0,0 +1,29 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package spectrum; + +/** + * Interface to add pause action to moving displays like the spectrum and + * waterfall panels + */ +public interface Pausable +{ + public void setPaused( boolean paused ); + + public boolean isPaused(); +} diff --git a/src/spectrum/SpectralDisplayPanel.java b/src/spectrum/SpectralDisplayPanel.java new file mode 100644 index 00000000..784b2964 --- /dev/null +++ b/src/spectrum/SpectralDisplayPanel.java @@ -0,0 +1,785 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package spectrum; + +import java.awt.Color; +import java.awt.Component; +import java.awt.Dimension; +import java.awt.EventQueue; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.awt.event.ComponentEvent; +import java.awt.event.ComponentListener; +import java.awt.event.MouseEvent; +import java.awt.event.MouseListener; +import java.awt.event.MouseMotionListener; +import java.text.DecimalFormat; +import java.util.ArrayList; + +import javax.swing.JCheckBoxMenuItem; +import javax.swing.JLayeredPane; +import javax.swing.JMenu; +import javax.swing.JMenuItem; +import javax.swing.JPanel; +import javax.swing.JPopupMenu; +import javax.swing.JSeparator; +import javax.swing.JSplitPane; +import javax.swing.SwingUtilities; + +import log.Log; +import net.miginfocom.swing.MigLayout; +import sample.Listener; +import settings.ColorSetting; +import settings.ColorSetting.ColorSettingName; +import settings.ColorSettingMenuItem; +import settings.Setting; +import settings.SettingChangeListener; +import settings.SettingsManager; +import source.Source.SampleType; +import source.SourceException; +import source.tuner.FrequencyChangeListener; +import source.tuner.Tuner; +import source.tuner.TunerSelectionListener; + +import com.jidesoft.swing.JideSplitPane; + +import controller.ResourceManager; +import controller.channel.Channel; +import controller.channel.Channel.ChannelEvent; +import controller.channel.ChannelListener; +import decode.DecoderType; +import dsp.filter.Window.WindowType; + +/** + * Spectral Display Panel - comprises a DFTProcessor coupled with a waterfall + * display and a spectrum display, with an overlay of frequency data and channel + * data. + * + * Processes float or complex sample data and displays the frequency content + * in the spectral and waterfall displays. + */ +public class SpectralDisplayPanel extends JPanel + implements ChannelListener, + Listener, + FrequencyChangeListener, + SettingChangeListener, + TunerSelectionListener + { + private static final long serialVersionUID = 1L; + private static DecimalFormat sCURSOR_FORMAT = new DecimalFormat( "000.00000" ); + + /** + * Colors used by this component + */ + private Color mColorChannelConfig; + private Color mColorChannelConfigProcessing; + private Color mColorChannelConfigSelected; + private Color mColorSpectrumBackground; + private Color mColorSpectrumCursor; + private Color mColorSpectrumGradientBottom; + private Color mColorSpectrumGradientTop; + private Color mColorSpectrumLine; + + private JLayeredPane mLayeredPanel; + private SpectrumPanel mSpectrumPanel; + private WaterfallPanel mWaterfallPanel; + private OverlayPanel mChannelPanel; + private DFTProcessor mDFTProcessor; + private ResourceManager mResourceManager; + private Tuner mTuner; + + public SpectralDisplayPanel( ResourceManager resourceManager ) + { + mResourceManager = resourceManager; + + setColors(); + + init(); + } + + public void dispose() + { + /* De-register from receiving samples when the window closes */ + clearTuner(); + + mResourceManager.getChannelManager().removeListener( this ); + + mDFTProcessor.dispose(); + mDFTProcessor = null; + + mSpectrumPanel.dispose(); + mSpectrumPanel = null; + + mWaterfallPanel.dispose(); + mWaterfallPanel = null; + + mChannelPanel.dispose(); + mChannelPanel = null; + + mResourceManager = null; + + mTuner = null; + } + + /** + * Fetches the color settings from the settings manager + */ + private void setColors() + { + mColorChannelConfig = getColor( ColorSettingName.CHANNEL_CONFIG ); + + mColorChannelConfigProcessing = + getColor( ColorSettingName.CHANNEL_CONFIG_PROCESSING ); + + mColorChannelConfigSelected = + getColor( ColorSettingName.CHANNEL_CONFIG_SELECTED ); + + mColorSpectrumCursor = getColor( ColorSettingName.SPECTRUM_CURSOR ); + + mColorSpectrumLine = getColor( ColorSettingName.SPECTRUM_LINE ); + + mColorSpectrumBackground = + getColor( ColorSettingName.SPECTRUM_BACKGROUND ); + + mColorSpectrumGradientBottom = + getColor( ColorSettingName.SPECTRUM_GRADIENT_BOTTOM ); + + mColorSpectrumGradientTop = + getColor( ColorSettingName.SPECTRUM_GRADIENT_TOP ); + } + + /** + * Fetches a named color setting from the settings manager. If the setting + * doesn't exist, creates the setting using the defaultColor + */ + private Color getColor( ColorSettingName name ) + { + ColorSetting setting = mResourceManager.getSettingsManager() + .getColorSetting( name ); + + return setting.getColor(); + } + + /** + * Stores the named color setting + */ + private void setColor( ColorSettingName name, Color color, int translucency ) + { + Color adjustedColor = ColorSetting.getTranslucent( color, translucency ); + + mResourceManager.getSettingsManager() + .setColorSetting( name, adjustedColor ); + } + + /** + * Monitors for setting changes. Colors can be changed by external actions + * and will automatically update in this class + */ + @Override + public void settingChanged( Setting setting ) + { + if( mChannelPanel != null ) + { + mChannelPanel.settingChanged( setting ); + } + + if( mWaterfallPanel != null ) + { + mWaterfallPanel.settingChanged( setting ); + } + + if( mSpectrumPanel != null ) + { + mSpectrumPanel.settingChanged( setting ); + } + + if( setting instanceof ColorSetting ) + { + ColorSetting colorSetting = (ColorSetting)setting; + + switch( ((ColorSetting)setting).getColorSettingName() ) + { + case CHANNEL_CONFIG: + mColorChannelConfig = colorSetting.getColor(); + break; + case CHANNEL_CONFIG_PROCESSING: + mColorChannelConfigProcessing = colorSetting.getColor(); + break; + case CHANNEL_CONFIG_SELECTED: + mColorChannelConfigSelected = colorSetting.getColor(); + break; + case SPECTRUM_BACKGROUND: + mColorSpectrumBackground = colorSetting.getColor(); + break; + case SPECTRUM_CURSOR: + mColorSpectrumCursor = colorSetting.getColor(); + break; + case SPECTRUM_GRADIENT_BOTTOM: + mColorSpectrumGradientBottom = colorSetting.getColor(); + break; + case SPECTRUM_GRADIENT_TOP: + mColorSpectrumGradientTop = colorSetting.getColor(); + break; + case SPECTRUM_LINE: + mColorSpectrumLine = colorSetting.getColor(); + break; + } + } + } + + /** + * Overrides JComponent method to return false, since we have overlapping + * panels with the spectrum and channel panels + */ + public boolean isOptimizedDrawingEnabled() + { + return false; + } + + private void init() + { + setLayout( new MigLayout( "insets 0 0 0 0", "[grow]", "[grow]") ); + + //Register for any future settings (ie color) changes + mResourceManager.getSettingsManager().addListener( this ); + + /** + * The layered pane holds the overlapping spectrum and channel panels + * and manages the sizing of each panel with the resize listener + */ + mLayeredPanel = new JLayeredPane(); + mLayeredPanel.addComponentListener( new ResizeListener() ); + + /** + * Create a mouse adapter to handle mouse events over the spectrum + * and waterfall panels + */ + MouseEventProcessor mouser = new MouseEventProcessor(); + + //Create the spectrum panel + mSpectrumPanel = new SpectrumPanel( mResourceManager ); + + //Create the channel panel + mChannelPanel = new OverlayPanel( mResourceManager ); + mChannelPanel.addMouseListener( mouser ); + mChannelPanel.addMouseMotionListener( mouser ); + + //Add the spectrum and channel panels to the layered panel + mLayeredPanel.add( mSpectrumPanel, new Integer( 0 ), 0 ); + mLayeredPanel.add( mChannelPanel, new Integer( 1 ), 0 ); + + //Create the waterfall + mWaterfallPanel = new WaterfallPanel( mResourceManager ); + mWaterfallPanel.addMouseListener( mouser ); + mWaterfallPanel.addMouseMotionListener( mouser ); + + /* Attempt to set a 50/50 split preferred size for the split pane */ + double totalHeight = mLayeredPanel.getPreferredSize().getHeight() + + mWaterfallPanel.getPreferredSize().getHeight(); + + mLayeredPanel.setPreferredSize( new Dimension( (int)mLayeredPanel + .getPreferredSize().getWidth(), (int)( totalHeight / 2.0d ) ) ); + + mWaterfallPanel.setPreferredSize( new Dimension( (int)mWaterfallPanel + .getPreferredSize().getWidth(), (int)( totalHeight / 2.0d ) ) ); + + //Create the split pane to hold the layered pane and the waterfall + JideSplitPane splitPane = new JideSplitPane( JSplitPane.VERTICAL_SPLIT ); + splitPane.setDividerSize( 5 ); + splitPane.add( mLayeredPanel ); + splitPane.add( mWaterfallPanel ); + + add( splitPane, "grow" ); + + /** + * Setup DFTProcessor to process samples and register the waterfall and + * spectrum panel to receive the processed dft results + */ + mDFTProcessor = new DFTProcessor( SampleType.COMPLEX ); + mDFTProcessor.addListener( (DFTResultsListener)mSpectrumPanel ); + mDFTProcessor.addListener( (DFTResultsListener)mWaterfallPanel ); + } + + /** + * Receives frequency change events -- primarily from tuner components. + */ + @Override + public void frequencyChanged( int frequency, int bandwidth ) + { + mChannelPanel.frequencyChanged( frequency, bandwidth ); + mDFTProcessor.frequencyChanged( frequency, bandwidth ); + } + + /** + * Channel config change events + */ + @Override + public void occurred( Channel config, ChannelEvent component ) + { + mChannelPanel.occurred( config, component ); + } + + @Override + public void receive( Float[] samples ) + { + mDFTProcessor.receive( samples ); + } + + /** + * Tuner selected events + */ + @Override + public void tunerSelected( Tuner tuner ) + { + clearTuner(); + + mDFTProcessor.clearBuffer(); + + mTuner = tuner; + + if( mTuner != null ) + { + //Register to receive frequency change events + mTuner.addListener( (FrequencyChangeListener)this ); + + //Register the dft processor to receive samples from the tuner + mTuner.addListener( (Listener)mDFTProcessor ); + + //Fire a frequency change event so that everyone can init + try + { + frequencyChanged( mTuner.getFrequency(), + mTuner.getSampleRate() ); + } + catch ( SourceException e ) + { + Log.info( "DFTProcessor - exception during new tuner setup - " + + "couldn't get frequency from the tuner" ); + } + } + } + + /** + * Tuner de-selection cleanup method + */ + public void clearTuner() + { + if( mTuner != null ) + { + //Deregister for frequency change events from the tuner + mTuner.removeListener( (FrequencyChangeListener)this ); + + //Deregister the dft processor from receiving samples + mTuner.removeListener( (Listener)mDFTProcessor ); + mTuner = null; + } + } + + /** + * Monitors the sizing of the layered pane and resizes the spectrum and + * channel panels whenever the layered pane is resized + */ + public class ResizeListener implements ComponentListener + { + @Override + public void componentResized( ComponentEvent e ) + { + Component c = e.getComponent(); + + mSpectrumPanel.setBounds( 0, 0, c.getWidth(), c.getHeight() ); + mChannelPanel.setBounds( 0, 0, c.getWidth(), c.getHeight() ); + } + + @Override + public void componentHidden( ComponentEvent arg0 ) {} + @Override + public void componentMoved( ComponentEvent arg0 ) {} + @Override + public void componentShown( ComponentEvent arg0 ) {} + } + + /** + * Mouse event handler for the channel panel. + */ + public class MouseEventProcessor implements MouseMotionListener, MouseListener + { + + @Override + public void mouseMoved( MouseEvent event ) + { + update( event ); + } + @Override + public void mouseDragged( MouseEvent event ) + { + update( event ); + } + + private void update( MouseEvent event ) + { + if( event.getComponent() == mChannelPanel ) + { + mChannelPanel.setCursorLocation( event.getPoint() ); + } + else + { + mWaterfallPanel.setCursorLocation( event.getPoint() ); + mWaterfallPanel.setCursorFrequency( + mChannelPanel.getFrequencyFromAxis( event.getPoint().x ) ); + } + } + + @Override + public void mouseEntered( MouseEvent e ) + { + if( e.getComponent() == mChannelPanel ) + { + mChannelPanel.setCursorVisible( true ); + } + else + { + mWaterfallPanel.setCursorVisible( true ); + } + } + + @Override + public void mouseExited( MouseEvent e ) + { + mChannelPanel.setCursorVisible( false ); + mWaterfallPanel.setCursorVisible( false ); + } + + /** + * Displays the context menu. + */ + @Override + public void mouseClicked( MouseEvent event ) + { + if( SwingUtilities.isRightMouseButton( event ) ) + { + JPopupMenu contextMenu = new JPopupMenu(); + + if( event.getComponent() == mWaterfallPanel ) + { + contextMenu.add( new PauseItem( mWaterfallPanel, "Pause" ) ); + } + + int frequency = + mChannelPanel.getFrequencyFromAxis( event.getX() ); + + if( event.getComponent() == mChannelPanel ) + { + ArrayList channels = + mChannelPanel.getChannelsAtFrequency( frequency ); + + for( Channel channel: channels ) + { + JMenu menu = channel.getContextMenu(); + + if( menu != null ) + { + contextMenu.add( menu ); + } + } + + if( !channels.isEmpty() ) + { + contextMenu.add( new JSeparator() ); + } + + } + + JMenu frequencyMenu = new JMenu( + sCURSOR_FORMAT.format( (float)frequency / 1000000.0f ) ); + + JMenu decoderMenu = new JMenu( "Add Decoder" ); + + for( DecoderType type: DecoderType.getAvailableDecoders() ) + { + decoderMenu.add( new DecoderItem( mResourceManager, + frequency, + type ) ); + + } + + frequencyMenu.add( decoderMenu ); + + contextMenu.add( frequencyMenu ); + + contextMenu.add( new JSeparator() ); + + /** + * Color Menus + */ + JMenu colorMenu = new JMenu( "Color" ); + + SettingsManager sm = mResourceManager.getSettingsManager(); + + colorMenu.add( new ColorSettingMenuItem( sm, + ColorSettingName.CHANNEL_CONFIG ) ); + + colorMenu.add( new ColorSettingMenuItem( sm, + ColorSettingName.CHANNEL_CONFIG_PROCESSING ) ); + + colorMenu.add( new ColorSettingMenuItem( sm, + ColorSettingName.CHANNEL_CONFIG_SELECTED ) ); + + colorMenu.add( new ColorSettingMenuItem( sm, + ColorSettingName.SPECTRUM_CURSOR ) ); + + colorMenu.add( new ColorSettingMenuItem( sm, + ColorSettingName.SPECTRUM_LINE ) ); + + colorMenu.add( new ColorSettingMenuItem( sm, + ColorSettingName.SPECTRUM_BACKGROUND ) ); + + colorMenu.add( new ColorSettingMenuItem( sm, + ColorSettingName.SPECTRUM_GRADIENT_BOTTOM ) ); + + colorMenu.add( new ColorSettingMenuItem( sm, + ColorSettingName.SPECTRUM_GRADIENT_TOP ) ); + + contextMenu.add( colorMenu ); + + /** + * Display items: fft and frame rate + */ + JMenu displayMenu = new JMenu( "Display" ); + contextMenu.add( displayMenu ); + + /** + * FFT width + */ + JMenu fftWidthMenu = new JMenu( "FFT Width" ); + displayMenu.add( fftWidthMenu ); + + for( FFTWidth width: FFTWidth.values() ) + { + fftWidthMenu.add( new FFTWidthItem( mDFTProcessor, width ) ); + } + + /** + * DFT Processor Frame Rate + */ + JMenu frameRateMenu = new JMenu( "Frame Rate" ); + displayMenu.add( frameRateMenu ); + + frameRateMenu.add( new FrameRateItem( mDFTProcessor, 14 ) ); + frameRateMenu.add( new FrameRateItem( mDFTProcessor, 16 ) ); + frameRateMenu.add( new FrameRateItem( mDFTProcessor, 18 ) ); + frameRateMenu.add( new FrameRateItem( mDFTProcessor, 20 ) ); + frameRateMenu.add( new FrameRateItem( mDFTProcessor, 25 ) ); + frameRateMenu.add( new FrameRateItem( mDFTProcessor, 30 ) ); + frameRateMenu.add( new FrameRateItem( mDFTProcessor, 40 ) ); + frameRateMenu.add( new FrameRateItem( mDFTProcessor, 50 ) ); + + /** + * FFT Window Type + */ + JMenu fftWindowType = new JMenu( "Window Type" ); + displayMenu.add( fftWindowType ); + + for( WindowType type: WindowType.values() ) + { + fftWindowType.add( + new FFTWindowTypeItem( mDFTProcessor, type ) ); + } + + if( contextMenu != null ) + { + if( event.getComponent() == mChannelPanel ) + { + contextMenu.show( mChannelPanel, + event.getX(), + event.getY() ); + } + else + { + contextMenu.show( mWaterfallPanel, + event.getX(), + event.getY() ); + } + } + } + } + + @Override + public void mousePressed( MouseEvent e ) {} + @Override + public void mouseReleased( MouseEvent e ) {} + } + + public class PauseItem extends JCheckBoxMenuItem + { + private static final long serialVersionUID = 1L; + + private Pausable mPausable; + + public PauseItem( Pausable pausable, String label ) + { + super( label ); + + final boolean paused = pausable.isPaused(); + + setSelected( paused ); + + mPausable = pausable; + + addActionListener( new ActionListener() + { + @Override + public void actionPerformed( ActionEvent e ) + { + EventQueue.invokeLater( new Runnable() + { + @Override + public void run() + { + mPausable.setPaused( !paused ); + } + } ); + } + } ); + } + } + + public class FFTWidthItem extends JCheckBoxMenuItem + { + private static final long serialVersionUID = 1L; + + private DFTProcessor mDFTProcessor; + private FFTWidth mFFTWidth; + + public FFTWidthItem( DFTProcessor processor, FFTWidth width ) + { + super( width.getLabel() ); + + mDFTProcessor = processor; + mFFTWidth = width; + + if( processor.getFFTWidth() == mFFTWidth ) + { + setSelected( true ); + } + + addActionListener( new ActionListener() + { + @Override + public void actionPerformed( ActionEvent arg0 ) + { + mDFTProcessor.setFFTSize( mFFTWidth ); + } + } ); + } + } + + public class FFTWindowTypeItem extends JCheckBoxMenuItem + { + private static final long serialVersionUID = 1L; + + private DFTProcessor mDFTProcessor; + private WindowType mWindowType; + + public FFTWindowTypeItem( DFTProcessor processor, WindowType windowType ) + { + super( windowType.toString() ); + + mDFTProcessor = processor; + mWindowType = windowType; + + if( processor.getWindowType() == mWindowType ) + { + setSelected( true ); + } + + addActionListener( new ActionListener() + { + @Override + public void actionPerformed( ActionEvent arg0 ) + { + mDFTProcessor.setWindowType( mWindowType ); + } + } ); + } + } + + public class FrameRateItem extends JCheckBoxMenuItem + { + private static final long serialVersionUID = 1L; + + private DFTProcessor mDFTProcessor; + private int mFrameRate; + + public FrameRateItem( DFTProcessor processor, int frameRate ) + { + super( String.valueOf( frameRate ) ); + + mDFTProcessor = processor; + mFrameRate = frameRate; + + if( processor.getFrameRate() == mFrameRate ) + { + setSelected( true ); + } + + addActionListener( new ActionListener() + { + @Override + public void actionPerformed( ActionEvent arg0 ) + { + mDFTProcessor.setFrameRate( mFrameRate ); + } + } ); + } + } + + /** + * Context menu item to provide one-click access to starting a channel + * processing with the selected decoder + */ + public class DecoderItem extends JMenuItem + { + private static final long serialVersionUID = 1L; + + private ResourceManager mResourceManager; + private int mFrequency; + private DecoderType mDecoder; + + public DecoderItem( ResourceManager manager, int frequency, DecoderType type ) + { + super( type.getDisplayString() ); + + mResourceManager = manager; + mFrequency = frequency; + mDecoder = type; + + addActionListener( new ActionListener() + { + @Override + public void actionPerformed( ActionEvent e ) + { + mResourceManager.getController() + .createChannel( mFrequency, mDecoder ); + } + } ); + } + } + + @Override + public void settingDeleted( Setting setting ) {} +} diff --git a/src/spectrum/SpectrumFrame.java b/src/spectrum/SpectrumFrame.java new file mode 100644 index 00000000..544df18a --- /dev/null +++ b/src/spectrum/SpectrumFrame.java @@ -0,0 +1,84 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package spectrum; + +import java.awt.EventQueue; +import java.awt.event.WindowEvent; +import java.awt.event.WindowListener; + +import javax.swing.JFrame; + +import net.miginfocom.swing.MigLayout; +import source.tuner.Tuner; +import controller.ResourceManager; + +public class SpectrumFrame extends JFrame implements WindowListener +{ + private static final long serialVersionUID = 1L; + + private ResourceManager mResourceManager; + private SpectralDisplayPanel mSpectralDisplayPanel; + + public SpectrumFrame( ResourceManager resourceManager, Tuner tuner ) + { + mResourceManager = resourceManager; + + setTitle( "SDRTRunk [" + tuner.getName() + "]" ); + setBounds( 100, 100, 1280, 600 ); + setDefaultCloseOperation( JFrame.DISPOSE_ON_CLOSE ); + + setLayout( new MigLayout( "insets 0 0 0 0", "[grow]", "[grow]") ); + + mSpectralDisplayPanel = new SpectralDisplayPanel( resourceManager ); + mSpectralDisplayPanel.tunerSelected( tuner ); + add( mSpectralDisplayPanel, "grow" ); + + /* Register spectral display on channel manager to get channel updates */ + mResourceManager.getChannelManager().addListener( mSpectralDisplayPanel ); + + /* Register a shutdown listener */ + this.addWindowListener( this ); + + EventQueue.invokeLater( new Runnable() + { + public void run() + { + setVisible( true ); + } + } ); + } + + @Override + public void windowClosed( WindowEvent arg0 ) + { + mSpectralDisplayPanel.dispose(); + } + + @Override + public void windowActivated( WindowEvent arg0 ) {} + @Override + public void windowClosing( WindowEvent arg0 ) {} + @Override + public void windowDeactivated( WindowEvent arg0 ) {} + @Override + public void windowDeiconified( WindowEvent arg0 ) {} + @Override + public void windowIconified( WindowEvent arg0 ) {} + @Override + public void windowOpened( WindowEvent arg0 ) {} +} diff --git a/src/spectrum/SpectrumPanel.java b/src/spectrum/SpectrumPanel.java new file mode 100644 index 00000000..be9035c1 --- /dev/null +++ b/src/spectrum/SpectrumPanel.java @@ -0,0 +1,257 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package spectrum; + +import java.awt.Color; +import java.awt.Dimension; +import java.awt.GradientPaint; +import java.awt.Graphics; +import java.awt.Graphics2D; +import java.awt.Rectangle; +import java.awt.RenderingHints; +import java.awt.geom.GeneralPath; +import java.awt.geom.Line2D; + +import javax.swing.JPanel; + +import settings.ColorSetting; +import settings.ColorSetting.ColorSettingName; +import settings.Setting; +import settings.SettingChangeListener; +import buffer.FloatArrayCircularAveragingBuffer; +import controller.ResourceManager; + +public class SpectrumPanel extends JPanel + implements DFTResultsListener, + SettingChangeListener +{ + private static final long serialVersionUID = 1L; + + /* Set display bins size to 1, so that we're guaranteed a reset to the + * correct width once the first sample set arrives */ + private float[] mDisplayFFTBins = new float[ 1 ]; + private FloatArrayCircularAveragingBuffer mFFTAveragingBuffer = + new FloatArrayCircularAveragingBuffer( 4 ); + + /** + * Spectral Display Color Settings + */ + private static final String sSPECTRUM_BACKGROUND = "spectrum_background"; + private static final String sSPECTRUM_GRADIENT_TOP = "spectrum_gradient_top"; + private static final String sSPECTRUM_GRADIENT_BOTTOM = "spectrum_gradient_bottom"; + private static final String sSPECTRUM_LINE = "spectrum_line"; + private static final int sSPECTRUM_TRANSLUCENCY = 128; + + private Color mColorSpectrumBackground; + private Color mColorSpectrumGradientTop; + private Color mColorSpectrumGradientBottom; + private Color mColorSpectrumLine; + + private float mSpectrumInset = 20.0f; + + private ResourceManager mResourceManager; + + public SpectrumPanel( ResourceManager resourceManager ) + { + mResourceManager = resourceManager; + + setColors(); + } + + public void dispose() + { + mResourceManager = null; + } + + private void setColors() + { + mColorSpectrumBackground = + getColor( ColorSettingName.SPECTRUM_BACKGROUND ); + + mColorSpectrumGradientBottom = + getColor( ColorSettingName.SPECTRUM_GRADIENT_BOTTOM ); + + mColorSpectrumGradientTop = + getColor( ColorSettingName.SPECTRUM_GRADIENT_TOP ); + + mColorSpectrumLine = getColor( ColorSettingName.SPECTRUM_LINE ); + } + + private Color getColor( ColorSettingName name ) + { + ColorSetting setting = + mResourceManager.getSettingsManager().getColorSetting( name ); + + return setting.getColor(); + } + + @Override + public void settingChanged( Setting setting ) + { + if( setting instanceof ColorSetting ) + { + ColorSetting colorSetting = (ColorSetting)setting; + + switch( ( (ColorSetting) setting ).getColorSettingName() ) + { + case SPECTRUM_BACKGROUND: + mColorSpectrumBackground = colorSetting.getColor(); + break; + case SPECTRUM_GRADIENT_BOTTOM: + mColorSpectrumGradientBottom = colorSetting.getColor(); + break; + case SPECTRUM_GRADIENT_TOP: + mColorSpectrumGradientTop = colorSetting.getColor(); + break; + case SPECTRUM_LINE: + mColorSpectrumLine = colorSetting.getColor(); + break; + } + } + } + + + /** + * DFTResultsListener interface method - for receiving the processed data + * to display + */ + public void receive( float[] currentFFTBins ) + { + //Construct and/or resize our fft results variables + if( mDisplayFFTBins == null || + mDisplayFFTBins.length != currentFFTBins.length ) + { + mDisplayFFTBins = new float[ currentFFTBins.length ]; + } + + /** + * Store the new FFT bins in the buffer and get the average of the FFT + * bin buffer contents into the display fft bins variable + */ + mDisplayFFTBins = mFFTAveragingBuffer.get( currentFFTBins ); + + repaint(); + } + + @Override + public void paintComponent( Graphics g ) + { + super.paintComponent( g ); + + Graphics2D graphics = (Graphics2D) g; + graphics.setBackground( mColorSpectrumBackground ); + + RenderingHints renderHints = + new RenderingHints( RenderingHints.KEY_ANTIALIASING, + RenderingHints.VALUE_ANTIALIAS_ON ); + + renderHints.put( RenderingHints.KEY_RENDERING, + RenderingHints.VALUE_RENDER_QUALITY ); + + graphics.setRenderingHints( renderHints ); + + drawSpectrum( graphics ); + } + + /** + * Draws the current fft spectrum with a line and a gradient fill. + */ + private void drawSpectrum( Graphics2D graphics ) + { + Dimension size = getSize(); + + //Draw the background + Rectangle background = new Rectangle( 0, 0, size.width, size.height ); + graphics.setColor( mColorSpectrumBackground ); + graphics.draw( background ); + graphics.fill( background ); + + //Define the gradient + GradientPaint gradient = new GradientPaint( 0, + 0, + mColorSpectrumGradientTop, + 0, + getSize().height, + mColorSpectrumGradientBottom ); + + graphics.setBackground( mColorSpectrumBackground ); + + GeneralPath spectrumShape = new GeneralPath(); + + //Start at the lower right inset point + spectrumShape.moveTo( size.getWidth(), + size.getHeight() - mSpectrumInset ); + + //Draw to the lower left + spectrumShape.lineTo( 0, size.getHeight() - mSpectrumInset ); + + //If we have FFT data to display ... + if( mDisplayFFTBins != null ) + { + float insideHeight = size.height - mSpectrumInset; + + float insideWidth = size.width; + + float binSize = insideWidth / ( mDisplayFFTBins.length - 1 ); + float heightMultiplier = (float)( insideHeight * .15 ); + + float spectrumMiddle = insideHeight * .23f; + + for( int x = 0; x < mDisplayFFTBins.length; x++ ) + { + float height = spectrumMiddle - ( mDisplayFFTBins[ x ] * heightMultiplier ); + + if( height > size.getHeight() - mSpectrumInset ) + { + height = (float)size.getHeight() - mSpectrumInset; + } + + spectrumShape.lineTo( binSize * x, height ); + } + } + //Otherwise show an empty spectrum + else + { + //Draw Left Size + graphics.setPaint( gradient ); + spectrumShape.lineTo( 0, size.getHeight() - mSpectrumInset ); + //Draw Middle + spectrumShape.lineTo( size.getWidth(), + size.getHeight() - mSpectrumInset ); + } + + //Draw Right Side + spectrumShape.lineTo( size.getWidth(), + size.getHeight() - mSpectrumInset ); + + graphics.setPaint( gradient ); + graphics.draw( spectrumShape ); + graphics.fill( spectrumShape ); + + graphics.setPaint( mColorSpectrumLine ); + + //Draw the bottom line under the spectrum + graphics.draw( new Line2D.Float( 0, + size.height - mSpectrumInset, + size.width, + size.height - mSpectrumInset ) ); + } + + @Override + public void settingDeleted( Setting setting ) {} +} diff --git a/src/spectrum/WaterfallColorModel.java b/src/spectrum/WaterfallColorModel.java new file mode 100644 index 00000000..7f338df4 --- /dev/null +++ b/src/spectrum/WaterfallColorModel.java @@ -0,0 +1,94 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package spectrum; + +import java.awt.image.IndexColorModel; + + +public class WaterfallColorModel +{ + public static IndexColorModel getDefaultColorModel() + { + int bitDepth = 8; + int indexSize = 256; + + byte[] red = new byte[ indexSize ]; + byte[] green = new byte[ indexSize ]; + byte[] blue = new byte[ indexSize ]; + + //Background noise + for( int x = 0; x < 16; x++ ) //Blue + { + red[ x ] = (byte)0; + + green[ x ] = (byte)0; + + blue[ x ] = (byte)127; + } + + for( int x = 16; x < 32; x++ ) //Blue + { + red[ x ] = (byte)0; + + green[ x ] = (byte)0; + + blue[ x ] = (byte)191; + } + + int r = 0; + int g = 0; + int b = 191; + + for( int x = 32; x < 60; x++ ) + { + red[ x ] = (byte)r; + r += 9; + + green[ x ] = (byte)g; + g += 9; + + blue[ x ] = (byte)b; + b -= 6; + } + + r = 255; + g = 255; + b = 0; + + for( int x = 60; x < 188; x++ ) //Yellow + { + red[ x ] = (byte)r; + + green[ x ] = (byte)g; + g -= 2; + + blue[ x ] = (byte)b; + } + + for( int x = 188; x < 256; x++ ) //Red + { + red[ x ] = (byte)255; + + green[ x ] = (byte)0; + + blue[ x ] = (byte)0; + } + + return new IndexColorModel( bitDepth, indexSize, red, green, blue ); + } +} diff --git a/src/spectrum/WaterfallPanel.java b/src/spectrum/WaterfallPanel.java new file mode 100644 index 00000000..f1ef4d1a --- /dev/null +++ b/src/spectrum/WaterfallPanel.java @@ -0,0 +1,287 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package spectrum; + +import java.awt.Color; +import java.awt.EventQueue; +import java.awt.Graphics; +import java.awt.Graphics2D; +import java.awt.Image; +import java.awt.Point; +import java.awt.event.MouseEvent; +import java.awt.event.MouseListener; +import java.awt.geom.Line2D; +import java.awt.image.ColorModel; +import java.awt.image.MemoryImageSource; +import java.text.DecimalFormat; +import java.util.concurrent.atomic.AtomicBoolean; + +import javax.swing.JPanel; + +import settings.ColorSetting; +import settings.ColorSetting.ColorSettingName; +import settings.Setting; +import settings.SettingChangeListener; +import buffer.FloatAveragingBuffer; +import controller.ResourceManager; + +/** + * Produces a JPanel filled with a waterfall image derived from a continuous + * stream of received Discrete Fourier Transform (DFT) results from the + * DFTProcessor class. + */ +public class WaterfallPanel extends JPanel implements DFTResultsListener, + Pausable, + SettingChangeListener +{ + private static final long serialVersionUID = 1L; + private static DecimalFormat sCURSOR_FORMAT = new DecimalFormat( "000.00000" ); + private static final String sSPECTRUM_CURSOR = "spectrum_cursor"; + private static final String sPAUSED = "PAUSED"; + + private byte[] mPixels; + private int mFFTSize = 1024; + private int mImageHeight = 700; + private MemoryImageSource mMemoryImageSource; + private ColorModel mColorModel = WaterfallColorModel.getDefaultColorModel(); + private Color mColorSpectrumCursor; + private Image mWaterfallImage; + + float mAverage = 0.0f; + double mAlpha = 0.95d; + int mScalor = 85; + int temp = 0; + + private Point mCursorLocation = new Point( 0, 0 ); + private boolean mCursorVisible = false; + private int mCursorFrequency = 0; + private AtomicBoolean mPaused = new AtomicBoolean(); + + private ResourceManager mResourceManager; + + private FloatAveragingBuffer mIntraBinAveragingBuffer = new FloatAveragingBuffer( 9 ); + + public WaterfallPanel( ResourceManager resourceManager ) + { + super(); + + mResourceManager = resourceManager; + + mColorSpectrumCursor = getColor( ColorSettingName.SPECTRUM_CURSOR ); + + reset(); + } + + public void dispose() + { + mResourceManager = null; + mMemoryImageSource = null; + } + + private void reset() + { + mPixels = new byte[ mFFTSize * mImageHeight ]; + + mMemoryImageSource = new MemoryImageSource( mFFTSize, + mImageHeight, + mColorModel, + mPixels, + 0, + mFFTSize ); + + mMemoryImageSource.setAnimated( true ); + + mWaterfallImage = createImage( mMemoryImageSource ); + } + + /** + * Pausable interface - pauses updates to the screen + */ + public void setPaused( boolean paused ) + { + mPaused.set( paused ); + + repaint(); + } + + public boolean isPaused() + { + return mPaused.get(); + } + + /** + * Fetches a named color setting from the settings manager. If the setting + * doesn't exist, creates the setting using the defaultColor + */ + private Color getColor( ColorSettingName name ) + { + ColorSetting setting = mResourceManager.getSettingsManager() + .getColorSetting( name ); + + return setting.getColor(); + } + + /** + * Monitors for setting changes. Colors can be changed by external actions + * and will automatically update in this class + */ + @Override + public void settingChanged( Setting setting ) + { + if( setting instanceof ColorSetting ) + { + ColorSetting colorSetting = (ColorSetting)setting; + + if( ( (ColorSetting) setting ).getColorSettingName() == + ColorSettingName.SPECTRUM_CURSOR ) + { + mColorSpectrumCursor = colorSetting.getColor(); + } + } + } + + public void setCursorLocation( Point point ) + { + mCursorLocation = point; + + repaint(); + } + + public void setCursorFrequency( int frequency ) + { + mCursorFrequency = frequency; + } + + public void setCursorVisible( boolean visible ) + { + mCursorVisible = visible; + + repaint(); + } + + public void paintComponent( Graphics g ) + { + g.drawImage( mWaterfallImage, 0, 0, getWidth(), mImageHeight, this ); + + Graphics2D graphics = (Graphics2D) g; + + if( mCursorVisible ) + { + + graphics.setColor( mColorSpectrumCursor ); + + graphics.draw( new Line2D.Float( mCursorLocation.x, + 0, + mCursorLocation.x, + (float)(getSize().getHeight() ) ) ); + + String frequency = sCURSOR_FORMAT.format( mCursorFrequency / 1000000.0D ); + + graphics.drawString( frequency , + mCursorLocation.x + 5, + mCursorLocation.y ); + + } + + if( mPaused.get() ) + { + graphics.drawString( sPAUSED, 20, 20 ); + } + + graphics.dispose(); + } + + @Override + public void receive( float[] update ) + { + //If our FFT size changes, reset our pixel map and image source + if( mFFTSize != update.length ) + { + mFFTSize = update.length; + + reset(); + } + + //Move the pixels down a row to make room for the new results + System.arraycopy( mPixels, 0, + mPixels, mFFTSize, mPixels.length - mFFTSize ); + + //Convert the power spectrum bin values into a byte (0-255) + //value to use with the color mapping + + double averagingTotal = 0.0; + + for( int x = 0; x < update.length; x++ ) + { + float value = update[ x ]; + + averagingTotal += value; + + //Average the values toward zero + float pixel = value - mAverage; + + //If we're below the average, make the negative value a positive + if( pixel < 0 ) + { +// pixel = -pixel; + pixel = 0.0f; + } + + //Scale the value + pixel *= mScalor; + +// mPixels[ x ] = (byte)pixel; + mPixels[ x ] = (byte)( mIntraBinAveragingBuffer.get( pixel ) ); + } + + //Update the average value + mAverage = (float) averagingTotal / update.length; + + if( !mPaused.get() ) + { + EventQueue.invokeLater( new Runnable() + { + @Override + public void run() + { + if( mMemoryImageSource != null ) + { + mMemoryImageSource.newPixels( mPixels, mColorModel, 0, mFFTSize ); + } + } + } ); + } + } + + public class Handler implements MouseListener + { + @Override + public void mouseClicked( MouseEvent e ) + { + //Display context menu to reset the display reset(); + } + + public void mousePressed( MouseEvent e ) {} + public void mouseReleased( MouseEvent e ) {} + public void mouseEntered( MouseEvent e ) {} + public void mouseExited( MouseEvent e ){} + } + + @Override + public void settingDeleted( Setting setting ) {} +} diff --git a/src/test/TestCRC.java b/src/test/TestCRC.java new file mode 100644 index 00000000..e09bdb0a --- /dev/null +++ b/src/test/TestCRC.java @@ -0,0 +1,309 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package test; + +import java.util.BitSet; + +import crc.CRCLTR; + + +public class TestCRC +{ + /** + * @param args + */ + public static void main( String[] args ) + { + log( "Starting ... " ); + +// String a = "10101100001001110011111111111000010111100"; +// String b = "10101100000110100011010000000011000101101"; +// String c = "10101100001001110011111111111001010110001"; +// String d = "10101100000100101100000000000110011001001"; +// String e = "10101100000110000000110000101100000011010"; + + String a = "00111101000011111111111001110010000110101"; + String b = "10110100011000000001011000101100000110101"; + String c = "10001101010011111111111001110010000110101"; + String d = "10010011001100000000000110100100000110101"; + String e = "01011000000110100001100000001100000110101"; + + for( int poly = 0; poly < 64; poly++ ) + { + BitSet m1 = load( a ); + int residual1 = applyChecksum( m1, poly ); + + BitSet m2 = load( b ); + int residual2 = applyChecksum( m2, poly ); + + BitSet m3 = load( c ); + int residual3 = applyChecksum( m3, poly ); + + BitSet m4 = load( d ); + int residual4 = applyChecksum( m4, poly ); + + BitSet m5 = load( e ); + int residual5 = applyChecksum( m5, poly ); + + log( "Poly:" + poly + ">> M1:" + residual1 + + " M2:" + residual2 + + " M3:" + residual3 + + " M4:" + residual4 + + " M5:" + residual5 ); + } + + log( "Done!" ); + } + + private static void getChecksum( int bit ) + { + BitSet m = getInitialFill(); + m.set( bit ); + + int psn = bit; + + while( psn < 34 ) + { +// log( "Before:" + format( m, 41 ) ); + +// BitSet poly = getPoly( psn ); +// log( " Poly:" + format( poly, 41 ) + "\n" ); + +// m.xor( poly ); + + psn = m.nextSetBit( psn + 1 ); + } + + log( "Result:" + format( m, 41 ) + " Bit:" + bit + " Hex:" + getHex( m.get( 34, 41 ), 7) ); + } + + //new +// private static boolean test( BitSet m, int poly, int initialFill ) +// { +// +// } + + //New + private static int applyChecksum( BitSet m, int poly ) + { + int psn = m.nextSetBit( 0 ); + + while( psn != -1 && psn < 35 ) + { +// log( "Before:" + format( m, 41 ) ); + + BitSet mask = getPoly( poly, psn ); +// log( " Poly:" + format( mask, 41 ) + "\n" ); + + m.xor( mask ); + + psn = m.nextSetBit( psn + 1 ); + } + +// log( "Result:" + format( m, 41 ) + " Hex:" + getHex( m.get( 34, 41 ), 7) ); + + return getInt( m.get( 34, 41 ), 7 ); + } + + //new + private static BitSet getInitialFill( int value ) + { + BitSet b = new BitSet(); + + for( int x = 0; x < 7; x++ ) + { + if( ( ( 1<<( 6 - x ) ) & value ) > 0 ) + { + b.set( 34 + x ); + } + } + + return b; + } + + //new + private static BitSet getPoly( int poly, int startPsn ) + { + //Load poly 1xxx xxx1 into new BitSet starting at startPsn + BitSet b = new BitSet(); + b.set( startPsn ); + b.set( startPsn + 7 ); //changed + + for( int x = 0; x < 6; x++ ) //changed + { + if( ( ( 1<<( 5 - x ) ) & poly ) > 0 ) //changed + { + b.set( startPsn + x + 1 ); + } + } + return b; + } + + private static BitSet getInitialFill() + { + //Loads poly 1100 0011 into new BitSet starting at startPsn + BitSet b = new BitSet(); + + b.set( 39 ); + b.set( 34 ); + + return b; + } + + private static String getChars( String c, int count ) + { + StringBuilder sb = new StringBuilder(); + + for( int x = 0; x < count; x++ ) + { + sb.append( c ); + } + + return sb.toString(); + } + + private static void applyFlips( BitSet m, BitSet c, int[] psns ) + { + for( int x = 0; x < psns.length; x++ ) + { + flip( m, c, psns[ x ] ); + } + } + + private static BitSet diff( BitSet b1, BitSet b2 ) + { + b1.xor( b2 ); + + return b1; + } + + private static void flip( BitSet m, BitSet c, int psn ) + { + m.flip( psn ); + apply( c, CRCLTR.getChecks()[ psn ] ); + } + + private static void apply( BitSet c, byte mask ) + { + BitSet maskBits = load( mask, true ); + + c.xor( maskBits ); + } + + private static BitSet load( byte b, boolean fullByte ) + { + BitSet retVal = new BitSet(); + + if( fullByte ) + { + for( int x = 0; x < 8; x++ ) + { + if( ( b & ( 1 << x ) ) > 0 ) + { + retVal.set( 7 - x ); + } + } + } + else + { + for( int x = 0; x < 7; x++ ) + { + if( ( b & ( 1 << x ) ) > 0 ) + { + retVal.set( 6 - x ); + } + } + } + + return retVal; + } + + private static void logDiff( BitSet m1, BitSet m2, BitSet c1, BitSet c2 ) + { + log( "M1:" + format( m1, 24 ) + " Cr1:" + format( c1, 7 ) + " Hex:" + getHex( c1, 7 ) ); + log( "M2:" + format( m2, 24 ) + " Cr2:" + format( c2, 7 ) + " Hex:" + getHex( c2, 7 ) ); + + m1.xor( m2 ); + c1.xor( c2 ); + + log( "DI:" + format( m1, 24 ) + " Crc:" + format( c1, 7 ) + " Hex:" + getHex( c1, 7 ) ); + } + + private static String getHex( BitSet b, int length ) + { + return String.format( "%02X", getInt( b, length ) ); + } + + private static int getInt( BitSet b, int length ) + { + int retVal = 0; + + for( int x = 0; x < length; x++ ) + { + if( b.get( x ) ) + { + retVal += 1<<( length - 1 - x ); + } + } + + return retVal; + } + + + private static BitSet load( String bits ) + { + BitSet retVal = new BitSet(); + + for( int x = 0; x < bits.length() - 9; x++ ) //changed** -1 + { + if( bits.substring( x, x + 1 ).contentEquals( "1" ) ) + { + retVal.set( x ); + } + } + + return retVal; + } + + private static String format( BitSet bitset, int length ) + { + StringBuilder sb = new StringBuilder(); + for( int x = 0; x < length; x++ ) + { + if( bitset.get( x ) ) + { + sb.append( "1" ); + } + else + { + sb.append( "0" ); + } + } + return sb.toString(); + } + + private static void log( BitSet m, BitSet c, String header ) + { + log( header + ":" + format( m, 24 ) + " Cr1:" + format( c, 7 ) + " Hex:" + getHex( c, 7 ) ); + } + + private static void log( String message ) + { + System.out.println( message ); + } +} diff --git a/src/util/Angle.java b/src/util/Angle.java new file mode 100644 index 00000000..5cb1c2d2 --- /dev/null +++ b/src/util/Angle.java @@ -0,0 +1,32 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package util; + +public class Angle +{ + + public static double degreesFromRadians( double radians ) + { + return radians * ( 180 / Math.PI ); + } + + public static double radiansFromDegrees( double degrees ) + { + return degrees * ( Math.PI / 180 ); + } +} diff --git a/src/util/ArcTangent.java b/src/util/ArcTangent.java new file mode 100644 index 00000000..528717c0 --- /dev/null +++ b/src/util/ArcTangent.java @@ -0,0 +1,262 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + * + * ---------------------------------------------- + * Adapted from GNU Radio's gr_fast_atan2f() method, retrieved Jan 2013 from + * https://raw.github.com/gnuradio/gnuradio/master/gnuradio-core/src/lib/general/gr_fast_atan2f.cc + * + * Copyright 2005 Free Software Foundation, Inc. + * + * This file is part of GNU Radio + * + * GNU Radio 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; either version 3, or (at your option) + * any later version. + * + * GNU Radio 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with GNU Radio; see the file COPYING. If not, write to + * the Free Software Foundation, Inc., 51 Franklin Street, + * Boston, MA 02110-1301, USA. + */ +package util; + +import sample.complex.ComplexSample; + +public class ArcTangent +{ + public static final double sTANGENT_MAP_RESOLUTION = 0.003921549; + public static final double sRADIANS_PER_DEGREE = 0.017453293; + public static final int sTANGENT_MAP_SIZE = 256; + public static final double sHALF_PI = Math.PI / 2; + + //ArcTangents from 0 to pi/4 radians + private static double[] sLOOKUP_TABLE = new double[] { + 0.000000e+00, 3.921549e-03, 7.842976e-03, 1.176416e-02, + 1.568499e-02, 1.960533e-02, 2.352507e-02, 2.744409e-02, + 3.136226e-02, 3.527947e-02, 3.919560e-02, 4.311053e-02, + 4.702413e-02, 5.093629e-02, 5.484690e-02, 5.875582e-02, + 6.266295e-02, 6.656816e-02, 7.047134e-02, 7.437238e-02, + 7.827114e-02, 8.216752e-02, 8.606141e-02, 8.995267e-02, + 9.384121e-02, 9.772691e-02, 1.016096e-01, 1.054893e-01, + 1.093658e-01, 1.132390e-01, 1.171087e-01, 1.209750e-01, + 1.248376e-01, 1.286965e-01, 1.325515e-01, 1.364026e-01, + 1.402496e-01, 1.440924e-01, 1.479310e-01, 1.517652e-01, + 1.555948e-01, 1.594199e-01, 1.632403e-01, 1.670559e-01, + 1.708665e-01, 1.746722e-01, 1.784728e-01, 1.822681e-01, + 1.860582e-01, 1.898428e-01, 1.936220e-01, 1.973956e-01, + 2.011634e-01, 2.049255e-01, 2.086818e-01, 2.124320e-01, + 2.161762e-01, 2.199143e-01, 2.236461e-01, 2.273716e-01, + 2.310907e-01, 2.348033e-01, 2.385093e-01, 2.422086e-01, + 2.459012e-01, 2.495869e-01, 2.532658e-01, 2.569376e-01, + 2.606024e-01, 2.642600e-01, 2.679104e-01, 2.715535e-01, + 2.751892e-01, 2.788175e-01, 2.824383e-01, 2.860514e-01, + 2.896569e-01, 2.932547e-01, 2.968447e-01, 3.004268e-01, + 3.040009e-01, 3.075671e-01, 3.111252e-01, 3.146752e-01, + 3.182170e-01, 3.217506e-01, 3.252758e-01, 3.287927e-01, + 3.323012e-01, 3.358012e-01, 3.392926e-01, 3.427755e-01, + 3.462497e-01, 3.497153e-01, 3.531721e-01, 3.566201e-01, + 3.600593e-01, 3.634896e-01, 3.669110e-01, 3.703234e-01, + 3.737268e-01, 3.771211e-01, 3.805064e-01, 3.838825e-01, + 3.872494e-01, 3.906070e-01, 3.939555e-01, 3.972946e-01, + 4.006244e-01, 4.039448e-01, 4.072558e-01, 4.105574e-01, + 4.138496e-01, 4.171322e-01, 4.204054e-01, 4.236689e-01, + 4.269229e-01, 4.301673e-01, 4.334021e-01, 4.366272e-01, + 4.398426e-01, 4.430483e-01, 4.462443e-01, 4.494306e-01, + 4.526070e-01, 4.557738e-01, 4.589307e-01, 4.620778e-01, + 4.652150e-01, 4.683424e-01, 4.714600e-01, 4.745676e-01, + 4.776654e-01, 4.807532e-01, 4.838312e-01, 4.868992e-01, + 4.899573e-01, 4.930055e-01, 4.960437e-01, 4.990719e-01, + 5.020902e-01, 5.050985e-01, 5.080968e-01, 5.110852e-01, + 5.140636e-01, 5.170320e-01, 5.199904e-01, 5.229388e-01, + 5.258772e-01, 5.288056e-01, 5.317241e-01, 5.346325e-01, + 5.375310e-01, 5.404195e-01, 5.432980e-01, 5.461666e-01, + 5.490251e-01, 5.518738e-01, 5.547124e-01, 5.575411e-01, + 5.603599e-01, 5.631687e-01, 5.659676e-01, 5.687566e-01, + 5.715357e-01, 5.743048e-01, 5.770641e-01, 5.798135e-01, + 5.825531e-01, 5.852828e-01, 5.880026e-01, 5.907126e-01, + 5.934128e-01, 5.961032e-01, 5.987839e-01, 6.014547e-01, + 6.041158e-01, 6.067672e-01, 6.094088e-01, 6.120407e-01, + 6.146630e-01, 6.172755e-01, 6.198784e-01, 6.224717e-01, + 6.250554e-01, 6.276294e-01, 6.301939e-01, 6.327488e-01, + 6.352942e-01, 6.378301e-01, 6.403565e-01, 6.428734e-01, + 6.453808e-01, 6.478788e-01, 6.503674e-01, 6.528466e-01, + 6.553165e-01, 6.577770e-01, 6.602282e-01, 6.626701e-01, + 6.651027e-01, 6.675261e-01, 6.699402e-01, 6.723452e-01, + 6.747409e-01, 6.771276e-01, 6.795051e-01, 6.818735e-01, + 6.842328e-01, 6.865831e-01, 6.889244e-01, 6.912567e-01, + 6.935800e-01, 6.958943e-01, 6.981998e-01, 7.004964e-01, + 7.027841e-01, 7.050630e-01, 7.073330e-01, 7.095943e-01, + 7.118469e-01, 7.140907e-01, 7.163258e-01, 7.185523e-01, + 7.207701e-01, 7.229794e-01, 7.251800e-01, 7.273721e-01, + 7.295557e-01, 7.317307e-01, 7.338974e-01, 7.360555e-01, + 7.382053e-01, 7.403467e-01, 7.424797e-01, 7.446045e-01, + 7.467209e-01, 7.488291e-01, 7.509291e-01, 7.530208e-01, + 7.551044e-01, 7.571798e-01, 7.592472e-01, 7.613064e-01, + 7.633576e-01, 7.654008e-01, 7.674360e-01, 7.694633e-01, + 7.714826e-01, 7.734940e-01, 7.754975e-01, 7.774932e-01, + 7.794811e-01, 7.814612e-01, 7.834335e-01, 7.853983e-01, + 7.853983e-01 }; + + /** + * Lookup radian angle from polar coordinates represented by the + * ComplexSample + * + * @param sample representing polar coordinates + * @return - angle in radians + */ + public static double getAngle( ComplexSample sample ) + { + return getAngle( ( (double) sample.inphase() ), + ( (double) sample.quadrature() ) ); + } + /** + * Lookup radian angle from polar coordinates x and y + * @param x - x polar coordinate + * @param y - y polar coordinate + * @return - angle in radians + */ + public static double getAngle( double x, double y ) + { + /** + * Calculates the angle of the vector (x,y) based on a table lookup and + * linear interpolation. The table uses a 256 point table covering + * -45 to +45 degrees and uses symmetry to determine the final angle + * value in the range of -180 to 180 degrees. Note that this function + * uses the small angle approximation for values close to zero. This + * routine calculates the arc tangent with an average error of + * +/- 0.045 degrees. + */ + double alpha; + double angle; + double base_angle; + int index; + + //Check for divide by zero + if( x == 0.0 && y == 0.0 ) + { + return 0.0; + } + + //Normalize to +/- 45 degree range + double x_abs = Math.abs( x ); + double y_abs = Math.abs( y ); + + //Calculate the ratio ( opposite/adjacent ) + double z; + + if( x_abs < y_abs ) + { + z = x_abs / y_abs; + } + else + { + z = y_abs / x_abs; + } + + //When the ratio approaches table resolution, the angle is best + //approximated with the argument itself + if( z < sTANGENT_MAP_RESOLUTION ) + { + base_angle = z; + } + else + { + alpha = z * (double)( sTANGENT_MAP_SIZE - .5 ); + index = (int)alpha; + alpha -= (double)index; + + //Determine base angle based on quadrant and add or subtract table + //value from base angle, based on quadrant + base_angle = sLOOKUP_TABLE[ index ]; + base_angle += ( sLOOKUP_TABLE[ index + 1 ] - + sLOOKUP_TABLE[ index ] ) * alpha; + } + + //Map the angle to the correct quadrant + if( x_abs > y_abs ) // -45 to 45, or 135 to -135 + { + if( x >= 0.0 ) // -45 to 45 + { + if( y >= 0.0 ) + { + angle = base_angle; // 0 to 45 + } + else + { + angle = -base_angle; // -45 to 0 + } + } + else // 135 to 180 or -180 to -135 + { + angle = Math.PI; + + if( y >= 0.0 ) + { + angle -= base_angle; // 135 to 180 + } + else + { + angle = base_angle - angle; // 180 to -135 + } + } + } + else // 45 to 135, or -135 to -45 + { + if( y >= 0.0 ) //45 to 135 + { + angle = sHALF_PI; + + if( x >= 0.0 ) + { + angle -= base_angle; // 45 to 90 + } + else + { + angle += base_angle; // 90 to 135 + } + } + else + { + angle = -sHALF_PI; + + if( x >= 0.0 ) + { + angle += base_angle; //-90 to -45 + } + else + { + angle -= base_angle; //-135 to -90 + } + } + } + +// //Check for negative radian angle value +// +// if( angle < 0 ) +// { +// angle += 2 * Math.PI; +// } + + return angle; + } +} diff --git a/src/util/ColorIcon.java b/src/util/ColorIcon.java new file mode 100644 index 00000000..85a80255 --- /dev/null +++ b/src/util/ColorIcon.java @@ -0,0 +1,60 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package util; + +import java.awt.Color; +import java.awt.Component; +import java.awt.Graphics; + +import javax.swing.Icon; + +public class ColorIcon implements Icon +{ + private static final int sSIZE = 12; + private static final int sARC_SIZE = 3; + private Color mColor; + + public ColorIcon( Color color ) + { + mColor = color; + } + + @Override + public void paintIcon(Component c, Graphics g, int x, int y) + { + Color temp = g.getColor(); + + g.setColor( mColor ); + + g.fillRoundRect( x, y, sSIZE, sSIZE, sARC_SIZE, sARC_SIZE ); + + g.setColor( temp ); + } + + @Override + public int getIconWidth() + { + return sSIZE; + } + + @Override + public int getIconHeight() + { + return sSIZE; + } +} diff --git a/src/util/Oscillator.java b/src/util/Oscillator.java new file mode 100644 index 00000000..8ffa044d --- /dev/null +++ b/src/util/Oscillator.java @@ -0,0 +1,97 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package util; + +import sample.complex.ComplexSample; + +public class Oscillator +{ + private double mFrequency; + private double mSampleRate; + + private ComplexSample mAnglePerSample; + private ComplexSample mCurrentAngle = new ComplexSample( 0.0f, -1.0f ); + + /** + * Oscillator produces complex or float samples corresponding to a sine wave + * oscillating at the specified frequency and sample rate + * + * @param frequency - positive or negative frequency in hertz + * @param sampleRate - in hertz + */ + public Oscillator( int frequency, int sampleRate ) + { + mSampleRate = (double)sampleRate; + mFrequency = (double)frequency; + + update(); + } + + private void update() + { + float anglePerSample = + (float)( 2.0d * Math.PI * mFrequency / mSampleRate ); + + mAnglePerSample = ComplexSample.fromAngle( anglePerSample ); + } + + /** + * Sets or changes the frequency of this oscillator + */ + public void setFrequency( int frequency ) + { + mFrequency = (double)frequency; + update(); + } + + /** + * Sets or changes the sample rate of this oscillator + */ + public void setSampleRate( int sampleRate ) + { + mSampleRate = (double)sampleRate; + update(); + } + + /** + * Steps the current angle by the angle per sample amount + */ + private void rotate() + { + mCurrentAngle.multiply( mAnglePerSample ); + mCurrentAngle.fastNormalize(); + } + + /** + * Get next complex sample + */ + public ComplexSample nextComplex() + { + rotate(); + return mCurrentAngle; + } + + /** + * Get the next float sample + */ + public float nextFloat() + { + rotate(); + return mCurrentAngle.real(); + } +} diff --git a/src/util/TimeStamp.java b/src/util/TimeStamp.java new file mode 100644 index 00000000..c4777c10 --- /dev/null +++ b/src/util/TimeStamp.java @@ -0,0 +1,84 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package util; + +import java.text.SimpleDateFormat; +import java.util.Date; + +public class TimeStamp +{ + private static SimpleDateFormat mSDFDate = new SimpleDateFormat( "yyyyMMdd" ); + private static SimpleDateFormat mSDFTime = new SimpleDateFormat( "HHmmss" ); + + /** + * Returns the current system date formatted as yyyy-MM-dd + */ + public static String getFormattedDate() + { + return getFormattedDate( System.currentTimeMillis() ); + } + + /** + * Returns the timestamp formatted as a date of yyyy-MM-dd + */ + public static String getFormattedDate( long timestamp ) + { + return mSDFDate.format( new Date( timestamp ) ); + } + + /** + * Returns the current system time formatted as HH:mm:ss + */ + public static String getFormattedTime() + { + return getFormattedTime( System.currentTimeMillis() ); + } + + /** + * Returns the timestamp formatted as a time of HH:mm:ss + */ + public static String getFormattedTime( long timestamp ) + { + return mSDFTime.format( new Date( timestamp ) ); + } + + /** + * Returns current system time formatted as yyyy-MM-dd*HH:mm:ss + * with the * representing the separator attribute + */ + public static String getTimeStamp( String separator ) + { + return getTimeStamp( System.currentTimeMillis(), separator ); + } + + /** + * Returns timestamp formatted as yyyy-MM-dd*HH:mm:ss + * with the * representing the separator attribute + */ + public static String getTimeStamp( long timestamp, String separator ) + { + StringBuilder sb = new StringBuilder(); + + sb.append( getFormattedDate( timestamp ) ); + sb.append( separator ); + sb.append( getFormattedTime( timestamp ) ); + + return sb.toString(); + } +} + diff --git a/src/util/waveaudio/WavFile.java b/src/util/waveaudio/WavFile.java new file mode 100644 index 00000000..71004db0 --- /dev/null +++ b/src/util/waveaudio/WavFile.java @@ -0,0 +1,864 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package util.waveaudio; + + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.PrintStream; + + + +public class WavFile +{ + private enum IOState {READING, WRITING, CLOSED}; + private final static int BUFFER_SIZE = 4096; + + private final static int FMT_CHUNK_ID = 0x20746D66; + private final static int DATA_CHUNK_ID = 0x61746164; + private final static int RIFF_CHUNK_ID = 0x46464952; + private final static int RIFF_TYPE_ID = 0x45564157; + + private File file; // File that will be read from or written to + private IOState ioState; // Specifies the IO State of the Wav File (used for snaity checking) + private int bytesPerSample; // Number of bytes required to store a single sample + private long numFrames; // Number of frames within the data section + private FileOutputStream oStream; // Output stream used for writting data + private FileInputStream iStream; // Input stream used for reading data + private double floatScale; // Scaling factor used for int <-> float conversion + private double floatOffset; // Offset factor used for int <-> float conversion + private boolean wordAlignAdjust; // Specify if an extra byte at the end of the data chunk is required for word alignment + + // Wav Header + private int numChannels; // 2 bytes unsigned, 0x0001 (1) to 0xFFFF (65,535) + private long sampleRate; // 4 bytes unsigned, 0x00000001 (1) to 0xFFFFFFFF (4,294,967,295) + // Although a java int is 4 bytes, it is signed, so need to use a long + private int blockAlign; // 2 bytes unsigned, 0x0001 (1) to 0xFFFF (65,535) + private int validBits; // 2 bytes unsigned, 0x0002 (2) to 0xFFFF (65,535) + + // Buffering + private byte[] buffer; // Local buffer used for IO + private int bufferPointer; // Points to the current position in local buffer + private int bytesRead; // Bytes read after last read into local buffer + private long frameCounter; // Current number of frames read or written + + // Cannot instantiate WavFile directly, must either use newWavFile() or openWavFile() + private WavFile() + { + buffer = new byte[BUFFER_SIZE]; + } + + public int getNumChannels() + { + return numChannels; + } + + public long getNumFrames() + { + return numFrames; + } + + public long getFramesRemaining() + { + return numFrames - frameCounter; + } + + public long getSampleRate() + { + return sampleRate; + } + + public int getValidBits() + { + return validBits; + } + + public static WavFile newWavFile(File file, int numChannels, long numFrames, int validBits, long sampleRate) throws IOException, WavFileException + { + // Instantiate new Wavfile and initialise + WavFile wavFile = new WavFile(); + wavFile.file = file; + wavFile.numChannels = numChannels; + wavFile.numFrames = numFrames; + wavFile.sampleRate = sampleRate; + wavFile.bytesPerSample = (validBits + 7) / 8; + wavFile.blockAlign = wavFile.bytesPerSample * numChannels; + wavFile.validBits = validBits; + + // Sanity check arguments + if (numChannels < 1 || numChannels > 65535) throw new WavFileException("Illegal number of channels, valid range 1 to 65536"); + if (numFrames < 0) throw new WavFileException("Number of frames must be positive"); + if (validBits < 2 || validBits > 65535) throw new WavFileException("Illegal number of valid bits, valid range 2 to 65536"); + if (sampleRate < 0) throw new WavFileException("Sample rate must be positive"); + + // Create output stream for writing data + wavFile.oStream = new FileOutputStream(file); + + // Calculate the chunk sizes + long dataChunkSize = wavFile.blockAlign * numFrames; + long mainChunkSize = 4 + // Riff Type + 8 + // Format ID and size + 16 + // Format data + 8 + // Data ID and size + dataChunkSize; + + // Chunks must be word aligned, so if odd number of audio data bytes + // adjust the main chunk size + if (dataChunkSize % 2 == 1) { + mainChunkSize += 1; + wavFile.wordAlignAdjust = true; + } + else { + wavFile.wordAlignAdjust = false; + } + + // Set the main chunk size + putLE(RIFF_CHUNK_ID, wavFile.buffer, 0, 4); + putLE(mainChunkSize, wavFile.buffer, 4, 4); + putLE(RIFF_TYPE_ID, wavFile.buffer, 8, 4); + + // Write out the header + wavFile.oStream.write(wavFile.buffer, 0, 12); + + // Put format data in buffer + long averageBytesPerSecond = sampleRate * wavFile.blockAlign; + + putLE(FMT_CHUNK_ID, wavFile.buffer, 0, 4); // Chunk ID + putLE(16, wavFile.buffer, 4, 4); // Chunk Data Size + putLE(1, wavFile.buffer, 8, 2); // Compression Code (Uncompressed) + putLE(numChannels, wavFile.buffer, 10, 2); // Number of channels + putLE(sampleRate, wavFile.buffer, 12, 4); // Sample Rate + putLE(averageBytesPerSecond, wavFile.buffer, 16, 4); // Average Bytes Per Second + putLE(wavFile.blockAlign, wavFile.buffer, 20, 2); // Block Align + putLE(validBits, wavFile.buffer, 22, 2); // Valid Bits + + // Write Format Chunk + wavFile.oStream.write(wavFile.buffer, 0, 24); + + // Start Data Chunk + putLE(DATA_CHUNK_ID, wavFile.buffer, 0, 4); // Chunk ID + putLE(dataChunkSize, wavFile.buffer, 4, 4); // Chunk Data Size + + // Write Format Chunk + wavFile.oStream.write(wavFile.buffer, 0, 8); + + // Calculate the scaling factor for converting to a normalised double + if (wavFile.validBits > 8) + { + // If more than 8 validBits, data is signed + // Conversion required multiplying by magnitude of max positive value + wavFile.floatOffset = 0; + wavFile.floatScale = Long.MAX_VALUE >> (64 - wavFile.validBits); + } + else + { + // Else if 8 or less validBits, data is unsigned + // Conversion required dividing by max positive value + wavFile.floatOffset = 1; + wavFile.floatScale = 0.5 * ((1 << wavFile.validBits) - 1); + } + + // Finally, set the IO State + wavFile.bufferPointer = 0; + wavFile.bytesRead = 0; + wavFile.frameCounter = 0; + wavFile.ioState = IOState.WRITING; + + return wavFile; + } + + public static WavFile openWavFile(File file) throws IOException, WavFileException + { + // Instantiate new Wavfile and store the file reference + WavFile wavFile = new WavFile(); + wavFile.file = file; + + // Create a new file input stream for reading file data + wavFile.iStream = new FileInputStream(file); + + // Read the first 12 bytes of the file + int bytesRead = wavFile.iStream.read(wavFile.buffer, 0, 12); + if (bytesRead != 12) throw new WavFileException("Not enough wav file bytes for header"); + + // Extract parts from the header + long riffChunkID = getLE(wavFile.buffer, 0, 4); + long chunkSize = getLE(wavFile.buffer, 4, 4); + long riffTypeID = getLE(wavFile.buffer, 8, 4); + + // Check the header bytes contains the correct signature + if (riffChunkID != RIFF_CHUNK_ID) throw new WavFileException("Invalid Wav Header data, incorrect riff chunk ID"); + if (riffTypeID != RIFF_TYPE_ID) throw new WavFileException("Invalid Wav Header data, incorrect riff type ID"); + + // Check that the file size matches the number of bytes listed in header + if (file.length() != chunkSize+8) { + throw new WavFileException("Header chunk size (" + chunkSize + ") does not match file size (" + file.length() + ")"); + } + + boolean foundFormat = false; + boolean foundData = false; + + // Search for the Format and Data Chunks + while (true) + { + // Read the first 8 bytes of the chunk (ID and chunk size) + bytesRead = wavFile.iStream.read(wavFile.buffer, 0, 8); + if (bytesRead == -1) throw new WavFileException("Reached end of file without finding format chunk"); + if (bytesRead != 8) throw new WavFileException("Could not read chunk header"); + + // Extract the chunk ID and Size + long chunkID = getLE(wavFile.buffer, 0, 4); + chunkSize = getLE(wavFile.buffer, 4, 4); + + // Word align the chunk size + // chunkSize specifies the number of bytes holding data. However, + // the data should be word aligned (2 bytes) so we need to calculate + // the actual number of bytes in the chunk + long numChunkBytes = (chunkSize%2 == 1) ? chunkSize+1 : chunkSize; + + if (chunkID == FMT_CHUNK_ID) + { + // Flag that the format chunk has been found + foundFormat = true; + + // Read in the header info + bytesRead = wavFile.iStream.read(wavFile.buffer, 0, 16); + + // Check this is uncompressed data + int compressionCode = (int) getLE(wavFile.buffer, 0, 2); + if (compressionCode != 1) throw new WavFileException("Compression Code " + compressionCode + " not supported"); + + // Extract the format information + wavFile.numChannels = (int) getLE(wavFile.buffer, 2, 2); + wavFile.sampleRate = getLE(wavFile.buffer, 4, 4); + wavFile.blockAlign = (int) getLE(wavFile.buffer, 12, 2); + wavFile.validBits = (int) getLE(wavFile.buffer, 14, 2); + + if (wavFile.numChannels == 0) throw new WavFileException("Number of channels specified in header is equal to zero"); + if (wavFile.blockAlign == 0) throw new WavFileException("Block Align specified in header is equal to zero"); + if (wavFile.validBits < 2) throw new WavFileException("Valid Bits specified in header is less than 2"); + if (wavFile.validBits > 64) throw new WavFileException("Valid Bits specified in header is greater than 64, this is greater than a long can hold"); + + // Calculate the number of bytes required to hold 1 sample + wavFile.bytesPerSample = (wavFile.validBits + 7) / 8; + if (wavFile.bytesPerSample * wavFile.numChannels != wavFile.blockAlign) + throw new WavFileException("Block Align does not agree with bytes required for validBits and number of channels"); + + // Account for number of format bytes and then skip over + // any extra format bytes + numChunkBytes -= 16; + if (numChunkBytes > 0) wavFile.iStream.skip(numChunkBytes); + } + else if (chunkID == DATA_CHUNK_ID) + { + // Check if we've found the format chunk, + // If not, throw an exception as we need the format information + // before we can read the data chunk + if (foundFormat == false) throw new WavFileException("Data chunk found before Format chunk"); + + // Check that the chunkSize (wav data length) is a multiple of the + // block align (bytes per frame) + if (chunkSize % wavFile.blockAlign != 0) throw new WavFileException("Data Chunk size is not multiple of Block Align"); + + // Calculate the number of frames + wavFile.numFrames = chunkSize / wavFile.blockAlign; + + // Flag that we've found the wave data chunk + foundData = true; + + break; + } + else + { + // If an unknown chunk ID is found, just skip over the chunk data + wavFile.iStream.skip(numChunkBytes); + } + } + + // Throw an exception if no data chunk has been found + if (foundData == false) throw new WavFileException("Did not find a data chunk"); + + // Calculate the scaling factor for converting to a normalised double + if (wavFile.validBits > 8) + { + // If more than 8 validBits, data is signed + // Conversion required dividing by magnitude of max negative value + wavFile.floatOffset = 0; + wavFile.floatScale = 1 << (wavFile.validBits - 1); + } + else + { + // Else if 8 or less validBits, data is unsigned + // Conversion required dividing by max positive value + wavFile.floatOffset = -1; + wavFile.floatScale = 0.5 * ((1 << wavFile.validBits) - 1); + } + + wavFile.bufferPointer = 0; + wavFile.bytesRead = 0; + wavFile.frameCounter = 0; + wavFile.ioState = IOState.READING; + + return wavFile; + } + + // Get and Put little endian data from local buffer + // ------------------------------------------------ + private static long getLE(byte[] buffer, int pos, int numBytes) + { + numBytes --; + pos += numBytes; + + long val = buffer[pos] & 0xFF; + for (int b=0 ; b>= 8; + pos ++; + } + } + + // Sample Writing and Reading + // -------------------------- + private void writeSample(long val) throws IOException + { + for (int b=0 ; b>= 8; + bufferPointer ++; + } + } + + private long readSample() throws IOException, WavFileException + { + long val = 0; + + for (int b=0 ; b 0) oStream.write(buffer, 0, bufferPointer); + + // If an extra byte is required for word alignment, add it to the end + if (wordAlignAdjust) oStream.write(0); + + // Close the stream and set to null + oStream.close(); + oStream = null; + } + + // Flag that the stream is closed + ioState = IOState.CLOSED; + } + + public void display() + { + display(System.out); + } + + public void display(PrintStream out) + { + out.printf("File: %s\n", file); + out.printf("Channels: %d, Frames: %d\n", numChannels, numFrames); + out.printf("IO State: %s\n", ioState); + out.printf("Sample Rate: %d, Block Align: %d\n", sampleRate, blockAlign); + out.printf("Valid Bits: %d, Bytes per sample: %d\n", validBits, bytesPerSample); + } + + public static void main(String[] args) + { + if (args.length < 1) + { + System.err.println("Must supply filename"); + System.exit(1); + } + + try + { + for (String filename : args) + { + WavFile readWavFile = openWavFile(new File(filename)); + readWavFile.display(); + + long numFrames = readWavFile.getNumFrames(); + int numChannels = readWavFile.getNumChannels(); + int validBits = readWavFile.getValidBits(); + long sampleRate = readWavFile.getSampleRate(); + + WavFile writeWavFile = newWavFile(new File("out.wav"), numChannels, numFrames, validBits, sampleRate); + + final int BUF_SIZE = 5001; + +// int[] buffer = new int[BUF_SIZE * numChannels]; +// long[] buffer = new long[BUF_SIZE * numChannels]; + double[] buffer = new double[BUF_SIZE * numChannels]; + + int framesRead = 0; + int framesWritten = 0; + + do + { + framesRead = readWavFile.readFrames(buffer, BUF_SIZE); + framesWritten = writeWavFile.writeFrames(buffer, BUF_SIZE); + System.out.printf("%d %d\n", framesRead, framesWritten); + } + while (framesRead != 0); + + readWavFile.close(); + writeWavFile.close(); + } + + WavFile writeWavFile = newWavFile(new File("out2.wav"), 1, 10, 23, 44100); + double[] buffer = new double[10]; + writeWavFile.writeFrames(buffer, 10); + writeWavFile.close(); + } + catch (Exception e) + { + System.err.println(e); + e.printStackTrace(); + } + } +} diff --git a/src/util/waveaudio/WavFileException.java b/src/util/waveaudio/WavFileException.java new file mode 100644 index 00000000..f2335f2a --- /dev/null +++ b/src/util/waveaudio/WavFileException.java @@ -0,0 +1,43 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package util.waveaudio; + +public class WavFileException extends Exception +{ + + public WavFileException() + { + super(); + } + + public WavFileException(String message) + { + super(message); + } + + public WavFileException(String message, Throwable cause) + { + super(message, cause); + } + + public WavFileException(Throwable cause) + { + super(cause); + } + +} diff --git a/src/util/waveaudio/WaveUtils.java b/src/util/waveaudio/WaveUtils.java new file mode 100644 index 00000000..f2659cd4 --- /dev/null +++ b/src/util/waveaudio/WaveUtils.java @@ -0,0 +1,149 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package util.waveaudio; + + +public class WaveUtils +{ + /** + * Returns a byte array representing a wave file RIFF chunk descriptor, + * for mono channel, signed 16-bit audio sample recording. + * + * @param sampleRate - audio sample rate + * + * @param duration - recording duration in seconds. Note: this is a + * temporary value, since you won't know the exact duration until after the + * recording is complete. Your code should overwrite this initial value + * with the correct value, upon completion of the recording + */ + public static byte[] getRIFFChunkDescriptor( float sampleRate, int duration ) + { + //Byte array large enough to hold the descriptor + byte[] buffer = new byte[ 12 ]; + + //Load "RIFF" into the descriptor (4-bytes) + loadDouble( buffer, 0x46464952, 0, 4 ); + + //Initial total size value ... will be overwritten later (4-bytes) + //Note: assumes that sample rate is an integer value + int totalSize = 36 + ( (int)sampleRate * duration ); + + loadDouble( buffer, totalSize, 4, 4); + + //Load "WAVE" into the descriptor (4-bytes) + loadDouble( buffer, 0x45564157, 8, 4); + + return buffer; + } + + /** + * WAVE format descriptor + * @param sampleRate + */ + public static byte[] getWAVEFormatDescriptor( float sampleRate, + int numberOfChannels, + int bitsPerSample ) + { + int bytesPerSample = bitsPerSample / 8; + + //Byte array large enough to hold the descriptor + byte[] buffer = new byte[ 24 ]; + + //SubChunk1ID = "fmt " + loadDouble( buffer, 0x20746D66, 0, 4); + + //SubChunk1Size = 16 + loadDouble( buffer, 16, 4, 4); + + //AudioFormat = PCM ( 1 = not compressed ) + loadDouble( buffer, 1, 8, 2 ); + + //NumberOfChannels + loadDouble( buffer, numberOfChannels, 10, 2); + + //SampleRate + //Note: assumes sample rate is an integer value + loadDouble( buffer, (int)sampleRate, 12, 4 ); + + //ByteRate ( sampleRate * numberOfChannels * bytesPerSample ) + loadDouble( buffer, + ( (int)sampleRate * numberOfChannels * bytesPerSample ), + 16, + 4 ); + + //BlockAlign ( numberOfChannels * bytesPerSample ) + loadDouble( buffer, ( numberOfChannels * bytesPerSample ), 20, 2 ); + + //BitsPerSample + loadDouble( buffer, bitsPerSample, 22, 2 ); + + return buffer; + } + + /** + * Returns formatted data chunk header, based on the following values: + * + * @param numberSamples - number of samples that will be in the chunk + * @param numberChannels - number of channels + * @param bitsPerSample - bits per sample + * @return + */ + public static byte[] getDataChunkHeader( int numberBytes, + int numberChannels ) + { + //Byte array large enough to hold the descriptor + byte[] buffer = new byte[ 24 ]; + + //SubChunk2ID = "data" + loadDouble( buffer, 0x61746164, 0, 4 ); + + //SubChunk2Size + loadDouble( buffer, ( numberBytes * numberChannels ), 4, 4 ); + + return buffer; + } + + /** + * Loads valueToLoad into the byte buffer at start location + * @param buffer - byte array + * @param valueToLoad + * @param start + * @param numberOfBytes + */ + protected static void loadDouble ( byte[] buffer, + long valueToLoad, + int start, + int numberOfBytes ) + { + if( !( ( start + numberOfBytes ) <= buffer.length ) ) + { + throw new IllegalArgumentException( "Byte array buffer is not " + + "large enough to load value:" + valueToLoad ); + } + + for ( int x = 0; x < numberOfBytes; x++ ) + { + buffer[ start ] = (byte) ( valueToLoad & 0xFF); + + valueToLoad >>= 8; + + start++; + } + } + +} diff --git a/src/util/waveaudio/WaveWriter.java b/src/util/waveaudio/WaveWriter.java new file mode 100644 index 00000000..0d49f9c0 --- /dev/null +++ b/src/util/waveaudio/WaveWriter.java @@ -0,0 +1,170 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * 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, either 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package util.waveaudio; + +import java.io.BufferedOutputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.RandomAccessFile; +import java.nio.ByteBuffer; + +import javax.sound.sampled.AudioFormat; + +import log.Log; + +public class WaveWriter +{ + private static final int sTOTAL_BYTE_LIMIT = Integer.MAX_VALUE; + private BufferedOutputStream mOutputStream; + private boolean mRunning = false; + private AudioFormat mAudioFormat; + private String mFileName; + private int mTotalByteCount = 0; + private int mFileRolloverCounter = 0; + + public WaveWriter( String fileName, AudioFormat format ) + { + mAudioFormat = format; + mFileName = fileName; + } + + public void start() throws IOException + { + if( !mRunning ) + { + openFile(); + + mRunning = true; + } + } + + public void stop() throws IOException + { + if( mRunning ) + { + closeFile(); + + //Increment file rollover count, in case we re-start the recorder + mFileRolloverCounter++; + + mRunning = false; + } + } + + public void write( ByteBuffer buffer ) throws IOException + { + checkFileRollover( buffer.capacity() ); + + if( mRunning && mOutputStream != null ) + { + //Write the data + mOutputStream.write( buffer.array() ); + mTotalByteCount += buffer.capacity(); + } + } + + protected void checkFileRollover( int byteCount ) throws IOException + { + if( mTotalByteCount + byteCount >= sTOTAL_BYTE_LIMIT ) + { + closeFile(); + + mFileRolloverCounter++; + + openFile(); + } + } + + private void closeFile() throws IOException + { + mOutputStream.flush(); + mOutputStream.close(); + + String filename = mFileName; + + //If this is a rollover file, append the rollover count to the filename + if( mFileRolloverCounter > 0 ) + { + filename += mFileRolloverCounter; + } + + //Correct the total count. Subtract 8 bytes from the total, to account + //for the ChunkID and ChunkSize bytes, at the beginning of the file + correctWAVEFileByteCount( filename, ( mTotalByteCount ) ); + } + + private void correctWAVEFileByteCount( String fileName, int newByteCount ) throws IOException + { + RandomAccessFile raf = new RandomAccessFile( fileName, "rw" ); + + //Get byte array for newByteCount minus header (8 bytes) + byte[] newTotalSize = new byte[ 4 ]; + WaveUtils.loadDouble( newTotalSize, ( newByteCount - 8 ), 0, 4 ); + + //Seek to byte position 4 and write the 4 byte length value + raf.seek( 4 ); + raf.write( newTotalSize ); + + //Get byte array for newByteCount minus all headers (44 bytes) + byte[] newDataSize = new byte[ 4 ]; + WaveUtils.loadDouble( newDataSize, ( newByteCount - 44 ), 0, 4 ); + + //Seek to byte position 4 and write the 4 byte length value + raf.seek( 40 ); + raf.write( newDataSize ); + + //debug + Log.info( "Closing recording [" + mFileName + "] amending file byte size to:" + newByteCount ); + + raf.close(); + } + + private void openFile() throws IOException + { + mTotalByteCount = 0; + + String filename = mFileName; + + if( mFileRolloverCounter > 0 ) + { + filename += mFileRolloverCounter; + } + + mOutputStream = new BufferedOutputStream( new FileOutputStream( filename ) ); + + //Write the RIFF descriptor + byte[] riff = WaveUtils.getRIFFChunkDescriptor( + mAudioFormat.getSampleRate(), 10 ); + mOutputStream.write( riff ); + mTotalByteCount += riff.length; + + //Write the WAVE descriptor + byte[] wave = WaveUtils.getWAVEFormatDescriptor( + mAudioFormat.getSampleRate(), + mAudioFormat.getChannels(), + mAudioFormat.getSampleSizeInBits() ); + mOutputStream.write( wave ); + mTotalByteCount += wave.length; + + //Write the data chunk header - for 10 seconds of audio + byte[] header = WaveUtils.getDataChunkHeader( + (int)(10 * mAudioFormat.getSampleRate() ), mAudioFormat.getChannels() ); + mOutputStream.write( header ); + mTotalByteCount += header.length; + } +}