├── .classpath ├── .gitignore ├── .project ├── .settings ├── org.eclipse.core.resources.prefs ├── org.eclipse.jdt.core.prefs └── org.eclipse.m2e.core.prefs ├── .travis.yml ├── LICENSE ├── README.md ├── pom.xml ├── screenshots ├── 1.png └── 2.png ├── shortcuts.sh └── src ├── main ├── assembly │ └── assembly.xml ├── java │ ├── module-info.java │ └── ru │ │ └── r2cloud │ │ └── rtlspectrum │ │ ├── BinData.java │ │ ├── BinDataParser.java │ │ ├── Controller.java │ │ ├── FrequencyFormatter.java │ │ ├── Legend.java │ │ ├── LineChartWithMarkers.java │ │ ├── LoadFile.java │ │ ├── MacOsUtil.java │ │ ├── Main.java │ │ ├── MainApplication.java │ │ ├── PowerFormatter.java │ │ ├── RtlPowerProgress.java │ │ ├── RunRtlPower.java │ │ ├── SaveTask.java │ │ ├── StatusBar.java │ │ ├── StatusBarTask.java │ │ └── SubtractFile.java └── resources │ ├── dark.css │ ├── layout.fxml │ └── statusBar.fxml └── test ├── java └── ru │ └── r2cloud │ └── rtlspectrum │ ├── BinDataParserTest.java │ ├── FrequencyFormatterTest.java │ ├── MacOsUtilTest.java │ ├── PowerFormatterTest.java │ └── UITest.java └── resources ├── defaults_empty.sh ├── defaults_fail.sh ├── defaults_mock.sh ├── subtract.csv └── test.csv /.classpath: -------------------------------------------------------------------------------- 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 | 40 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled class file 2 | *.class 3 | 4 | # Log file 5 | *.log 6 | 7 | # BlueJ files 8 | *.ctxt 9 | 10 | # Mobile Tools for Java (J2ME) 11 | .mtj.tmp/ 12 | 13 | # Package Files # 14 | *.jar 15 | *.war 16 | *.nar 17 | *.ear 18 | *.zip 19 | *.tar.gz 20 | *.rar 21 | 22 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 23 | hs_err_pid* 24 | /target/ 25 | -------------------------------------------------------------------------------- /.project: -------------------------------------------------------------------------------- 1 | 2 | 3 | rtlSpectrum 4 | 5 | 6 | 7 | 8 | 9 | org.eclipse.jdt.core.javabuilder 10 | 11 | 12 | 13 | 14 | org.eclipse.m2e.core.maven2Builder 15 | 16 | 17 | 18 | 19 | 20 | org.eclipse.jdt.core.javanature 21 | org.eclipse.m2e.core.maven2Nature 22 | 23 | 24 | -------------------------------------------------------------------------------- /.settings/org.eclipse.core.resources.prefs: -------------------------------------------------------------------------------- 1 | eclipse.preferences.version=1 2 | encoding//src/main/java=UTF-8 3 | encoding//src/main/resources=UTF-8 4 | encoding//src/test/java=UTF-8 5 | encoding//src/test/resources=UTF-8 6 | encoding/=UTF-8 7 | -------------------------------------------------------------------------------- /.settings/org.eclipse.jdt.core.prefs: -------------------------------------------------------------------------------- 1 | eclipse.preferences.version=1 2 | org.eclipse.jdt.core.compiler.annotation.inheritNullAnnotations=disabled 3 | org.eclipse.jdt.core.compiler.annotation.missingNonNullByDefaultAnnotation=ignore 4 | org.eclipse.jdt.core.compiler.annotation.nonnull=org.eclipse.jdt.annotation.NonNull 5 | org.eclipse.jdt.core.compiler.annotation.nonnull.secondary= 6 | org.eclipse.jdt.core.compiler.annotation.nonnullbydefault=org.eclipse.jdt.annotation.NonNullByDefault 7 | org.eclipse.jdt.core.compiler.annotation.nonnullbydefault.secondary= 8 | org.eclipse.jdt.core.compiler.annotation.nullable=org.eclipse.jdt.annotation.Nullable 9 | org.eclipse.jdt.core.compiler.annotation.nullable.secondary= 10 | org.eclipse.jdt.core.compiler.annotation.nullanalysis=enabled 11 | org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled 12 | org.eclipse.jdt.core.compiler.codegen.methodParameters=do not generate 13 | org.eclipse.jdt.core.compiler.codegen.targetPlatform=11 14 | org.eclipse.jdt.core.compiler.codegen.unusedLocal=preserve 15 | org.eclipse.jdt.core.compiler.compliance=11 16 | org.eclipse.jdt.core.compiler.debug.lineNumber=generate 17 | org.eclipse.jdt.core.compiler.debug.localVariable=generate 18 | org.eclipse.jdt.core.compiler.debug.sourceFile=generate 19 | org.eclipse.jdt.core.compiler.problem.APILeak=warning 20 | org.eclipse.jdt.core.compiler.problem.annotationSuperInterface=warning 21 | org.eclipse.jdt.core.compiler.problem.assertIdentifier=error 22 | org.eclipse.jdt.core.compiler.problem.autoboxing=ignore 23 | org.eclipse.jdt.core.compiler.problem.comparingIdentical=warning 24 | org.eclipse.jdt.core.compiler.problem.deadCode=warning 25 | org.eclipse.jdt.core.compiler.problem.deprecation=warning 26 | org.eclipse.jdt.core.compiler.problem.deprecationInDeprecatedCode=disabled 27 | org.eclipse.jdt.core.compiler.problem.deprecationWhenOverridingDeprecatedMethod=disabled 28 | org.eclipse.jdt.core.compiler.problem.discouragedReference=ignore 29 | org.eclipse.jdt.core.compiler.problem.emptyStatement=warning 30 | org.eclipse.jdt.core.compiler.problem.enablePreviewFeatures=disabled 31 | org.eclipse.jdt.core.compiler.problem.enumIdentifier=error 32 | org.eclipse.jdt.core.compiler.problem.explicitlyClosedAutoCloseable=ignore 33 | org.eclipse.jdt.core.compiler.problem.fallthroughCase=ignore 34 | org.eclipse.jdt.core.compiler.problem.fatalOptionalError=enabled 35 | org.eclipse.jdt.core.compiler.problem.fieldHiding=ignore 36 | org.eclipse.jdt.core.compiler.problem.finalParameterBound=warning 37 | org.eclipse.jdt.core.compiler.problem.finallyBlockNotCompletingNormally=warning 38 | org.eclipse.jdt.core.compiler.problem.forbiddenReference=ignore 39 | org.eclipse.jdt.core.compiler.problem.hiddenCatchBlock=warning 40 | org.eclipse.jdt.core.compiler.problem.includeNullInfoFromAsserts=disabled 41 | org.eclipse.jdt.core.compiler.problem.incompatibleNonInheritedInterfaceMethod=warning 42 | org.eclipse.jdt.core.compiler.problem.incompleteEnumSwitch=warning 43 | org.eclipse.jdt.core.compiler.problem.indirectStaticAccess=ignore 44 | org.eclipse.jdt.core.compiler.problem.localVariableHiding=ignore 45 | org.eclipse.jdt.core.compiler.problem.methodWithConstructorName=warning 46 | org.eclipse.jdt.core.compiler.problem.missingDefaultCase=ignore 47 | org.eclipse.jdt.core.compiler.problem.missingDeprecatedAnnotation=ignore 48 | org.eclipse.jdt.core.compiler.problem.missingEnumCaseDespiteDefault=disabled 49 | org.eclipse.jdt.core.compiler.problem.missingHashCodeMethod=warning 50 | org.eclipse.jdt.core.compiler.problem.missingOverrideAnnotation=warning 51 | org.eclipse.jdt.core.compiler.problem.missingOverrideAnnotationForInterfaceMethodImplementation=enabled 52 | org.eclipse.jdt.core.compiler.problem.missingSerialVersion=warning 53 | org.eclipse.jdt.core.compiler.problem.missingSynchronizedOnInheritedMethod=ignore 54 | org.eclipse.jdt.core.compiler.problem.noEffectAssignment=warning 55 | org.eclipse.jdt.core.compiler.problem.noImplicitStringConversion=warning 56 | org.eclipse.jdt.core.compiler.problem.nonExternalizedStringLiteral=ignore 57 | org.eclipse.jdt.core.compiler.problem.nonnullParameterAnnotationDropped=warning 58 | org.eclipse.jdt.core.compiler.problem.nonnullTypeVariableFromLegacyInvocation=warning 59 | org.eclipse.jdt.core.compiler.problem.nullAnnotationInferenceConflict=error 60 | org.eclipse.jdt.core.compiler.problem.nullReference=error 61 | org.eclipse.jdt.core.compiler.problem.nullSpecViolation=error 62 | org.eclipse.jdt.core.compiler.problem.nullUncheckedConversion=warning 63 | org.eclipse.jdt.core.compiler.problem.overridingPackageDefaultMethod=warning 64 | org.eclipse.jdt.core.compiler.problem.parameterAssignment=ignore 65 | org.eclipse.jdt.core.compiler.problem.pessimisticNullAnalysisForFreeTypeVariables=warning 66 | org.eclipse.jdt.core.compiler.problem.possibleAccidentalBooleanAssignment=warning 67 | org.eclipse.jdt.core.compiler.problem.potentialNullReference=warning 68 | org.eclipse.jdt.core.compiler.problem.potentiallyUnclosedCloseable=ignore 69 | org.eclipse.jdt.core.compiler.problem.rawTypeReference=warning 70 | org.eclipse.jdt.core.compiler.problem.redundantNullAnnotation=warning 71 | org.eclipse.jdt.core.compiler.problem.redundantNullCheck=warning 72 | org.eclipse.jdt.core.compiler.problem.redundantSpecificationOfTypeArguments=ignore 73 | org.eclipse.jdt.core.compiler.problem.redundantSuperinterface=warning 74 | org.eclipse.jdt.core.compiler.problem.reportMethodCanBePotentiallyStatic=ignore 75 | org.eclipse.jdt.core.compiler.problem.reportMethodCanBeStatic=warning 76 | org.eclipse.jdt.core.compiler.problem.reportPreviewFeatures=warning 77 | org.eclipse.jdt.core.compiler.problem.specialParameterHidingField=disabled 78 | org.eclipse.jdt.core.compiler.problem.staticAccessReceiver=warning 79 | org.eclipse.jdt.core.compiler.problem.suppressOptionalErrors=disabled 80 | org.eclipse.jdt.core.compiler.problem.suppressWarnings=enabled 81 | org.eclipse.jdt.core.compiler.problem.syntacticNullAnalysisForFields=disabled 82 | org.eclipse.jdt.core.compiler.problem.syntheticAccessEmulation=ignore 83 | org.eclipse.jdt.core.compiler.problem.terminalDeprecation=warning 84 | org.eclipse.jdt.core.compiler.problem.typeParameterHiding=warning 85 | org.eclipse.jdt.core.compiler.problem.unavoidableGenericTypeProblems=enabled 86 | org.eclipse.jdt.core.compiler.problem.uncheckedTypeOperation=warning 87 | org.eclipse.jdt.core.compiler.problem.unclosedCloseable=warning 88 | org.eclipse.jdt.core.compiler.problem.undocumentedEmptyBlock=warning 89 | org.eclipse.jdt.core.compiler.problem.unhandledWarningToken=warning 90 | org.eclipse.jdt.core.compiler.problem.unlikelyCollectionMethodArgumentType=warning 91 | org.eclipse.jdt.core.compiler.problem.unlikelyCollectionMethodArgumentTypeStrict=disabled 92 | org.eclipse.jdt.core.compiler.problem.unlikelyEqualsArgumentType=info 93 | org.eclipse.jdt.core.compiler.problem.unnecessaryElse=ignore 94 | org.eclipse.jdt.core.compiler.problem.unnecessaryTypeCheck=warning 95 | org.eclipse.jdt.core.compiler.problem.unqualifiedFieldAccess=ignore 96 | org.eclipse.jdt.core.compiler.problem.unstableAutoModuleName=warning 97 | org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownException=warning 98 | org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownExceptionExemptExceptionAndThrowable=enabled 99 | org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownExceptionIncludeDocCommentReference=enabled 100 | org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownExceptionWhenOverriding=disabled 101 | org.eclipse.jdt.core.compiler.problem.unusedExceptionParameter=ignore 102 | org.eclipse.jdt.core.compiler.problem.unusedImport=warning 103 | org.eclipse.jdt.core.compiler.problem.unusedLabel=warning 104 | org.eclipse.jdt.core.compiler.problem.unusedLocal=warning 105 | org.eclipse.jdt.core.compiler.problem.unusedObjectAllocation=warning 106 | org.eclipse.jdt.core.compiler.problem.unusedParameter=warning 107 | org.eclipse.jdt.core.compiler.problem.unusedParameterIncludeDocCommentReference=enabled 108 | org.eclipse.jdt.core.compiler.problem.unusedParameterWhenImplementingAbstract=disabled 109 | org.eclipse.jdt.core.compiler.problem.unusedParameterWhenOverridingConcrete=disabled 110 | org.eclipse.jdt.core.compiler.problem.unusedPrivateMember=warning 111 | org.eclipse.jdt.core.compiler.problem.unusedTypeParameter=warning 112 | org.eclipse.jdt.core.compiler.problem.unusedWarningToken=warning 113 | org.eclipse.jdt.core.compiler.problem.varargsArgumentNeedCast=warning 114 | org.eclipse.jdt.core.compiler.release=enabled 115 | org.eclipse.jdt.core.compiler.source=11 116 | -------------------------------------------------------------------------------- /.settings/org.eclipse.m2e.core.prefs: -------------------------------------------------------------------------------- 1 | activeProfiles= 2 | eclipse.preferences.version=1 3 | resolveWorkspaceProjects=true 4 | version=1 5 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: java 2 | bundler_args: --retry 1 3 | os: linux 4 | sudo: false 5 | 6 | addons: 7 | sonarcloud: 8 | organization: "dernasherbrezon-github" 9 | token: 10 | secure: "ajg6cm5798rx2DHmwhie9VQko8J6OIiDDRwrOoP1zHLodmcjtJEhtvAnPpSEHjEINunLpOyhVncYpIPiSQW8Fq5L9FMtmDHaQpXPB1RZX24VGEVdB1BjUAQqe1c9FrNHjP7c/UdR6xXUkI83mejTlZI9VgVXpTpfG5geElo/2Ewhl/VsWinRKPDeE9fqdRZrw7eKfrNNOBn9PCqxBTdIsY/ed3eKQBXSdYsM5bIk/VE2UL3v7SYagKP5/B0sDDEbxLps4mVPC57yAx8DFm2LegqwXxh0RXNjnfuyCPLrx7CmP3p5P5Q/oDXaX48/wQd9bqfIxCfnLwaYjHpki8d6XJ85J3DLU5QgqQkUuJjDwvTbs5k1Z8QY5y2gTnEiw9EmAzDbi3NyTjfBwgioULB9I02+vGIv/SNuENxRaODbnGDCdw0071J9TiLGF5bzezAvXDgdGdQD/uGWbaCtaUhzHCbpbgEc7DkEgSmz96rORlR45JB/SBBb0KWi6Cf5I/nwCLsYCNaqI9fricpwU9YfB19YE/xPFY7YirkPHAiQRhL8fgD+u473R/b5PjHpYgQF9nfZG5tZXGRFdaVm4RcVawy1n2MXmiWxg8ZUS3gB5pgErpapH9S4LLk8ESpa/ciPYfMmWcAurv6ziSkdnh7oE8zgNodFDt/StHRrxeEndV4=" 11 | 12 | install: true 13 | 14 | script: 15 | - mvn clean verify sonar:sonar -Pcoverage -Dsonar.scm.disabled=true -Dsonar.language=java 16 | 17 | cache: 18 | directories: 19 | - $HOME/.m2/repository 20 | - $HOME/.sonar/cache 21 | 22 | before_cache: 23 | - rm -rf $HOME/.m2/repository/ru/r2cloud/ 24 | 25 | before_deploy: 26 | - mvn -Djavafx.platform=mac-aarch64 assembly:single 27 | - mvn -Djavafx.platform=mac assembly:single 28 | - mvn -Djavafx.platform=linux assembly:single 29 | - mvn -Djavafx.platform=win assembly:single 30 | 31 | deploy: 32 | provider: releases 33 | api_key: 34 | secure: "xydSkI4RpT7KrrUqnL325tqQHtkQl/UrGKylaWaoO3i9KrMkWGlPDen9tJ9M4WQGC0Kw8OmgOxUGQRabVJIphQdMrQXiLm2wRs01YlsIQSyoAY1JuQxEBnDizfLqDSjIm7nlp4HqOFy0HsPRztcezkxgiQO7uivr9NP+mR7WsNQDsnq/s0egq8/Ae0frr9SJJ7KUjK/ZRKjmNaGjbbCg7T+jjboORd1gUc+5+W6okfePSiH14S3ZHud++KA4P+RRyBmFhmfaFSmj3083+j2YP8y/FtFmNbqtbT5Pq/6HjY6LMOLW+oV17U203sy7Fh7FhF3hl8zlaDJl1fGHEADt9HcybWYmY8m22h0xJlrRrEBaUp5yISpFzfEIL0lS4HBqObYGmGTYt+oJ8f31cGbtug3DPOkioE+u7FyrihOzXUi2OJ1rpHdWLe+56T+GCMNOYwfDVZ6L6mAS0tZHqKe2UVc3vr8cWDqk2tSTGth+zK1fWlMveCxoJ9fIDAiJMSeoWfNV2SOJb7h9R5lpi1XNaf4GRcT8uM5qyOHDxNF8bpt6+lfo7kSaXXvS5ks9+AgikFgArRkJCAO7VKLweSo5MW321024dfSOZF1m3GaSWHpcF40l6bko6yXKM35av29ZRI65RojrU+dbmaYiWkHPQmAo6E/2GsejUOungE+6u3Q=" 35 | file_glob: true 36 | file: "target/rtlSpectrum_*.jar" 37 | skip_cleanup: true 38 | on: 39 | tags: true -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # rtlSpectrum [![Build Status](https://app.travis-ci.com/dernasherbrezon/rtlSpectrum.svg?branch=master)](https://app.travis-ci.com/github/dernasherbrezon/rtlSpectrum) [![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=ru.r2cloud%3ArtlSpectrum&metric=alert_status)](https://sonarcloud.io/dashboard?id=ru.r2cloud%3ArtlSpectrum) 2 | 3 | Analyze spectrograms created by rtl_power 4 | 5 | # Screenshots 6 | 7 | ![screen1](/screenshots/1.png?raw=true) 8 | ![screen2](/screenshots/2.png?raw=true) 9 | 10 | # Features 11 | 12 | * load from .csv file produced by rtl_power 13 | * run rtl_power directly. it should be available in the $PATH 14 | * add multiple graphs for analysis 15 | * substract one graph from another 16 | * save/export graph in the rtl_power based format 17 | 18 | # Installation 19 | 20 | * Ensure you have java installed. Required version is Java 11+ 21 | * Go to [Releases](https://github.com/dernasherbrezon/rtlSpectrum/releases) tab 22 | * Download the latest .jar file for your operating system. Supported operating systems are: 23 | * Linux 64bit 24 | * Windows 64bit 25 | * MacOS Intel 64bit 26 | * MacOS M1 27 | * Run it using the command ```java -jar rtlSpectrum_win.jar``` -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4.0.0 3 | ru.r2cloud 4 | rtlSpectrum 5 | 1.9-SNAPSHOT 6 | 2019 7 | rtlSpectrum 8 | Analyze spectrograms created by rtl_power 9 | 10 | 11 | UTF-8 12 | 18-ea+6 13 | 14 | 15 | 16 | 17 | org.testfx 18 | testfx-junit 19 | 4.0.15-alpha 20 | test 21 | 22 | 23 | org.openjfx 24 | javafx-controls 25 | ${javafx-version} 26 | 27 | 28 | org.openjfx 29 | javafx-fxml 30 | ${javafx-version} 31 | 32 | 33 | org.openjfx 34 | javafx-graphics 35 | ${javafx-version} 36 | 37 | 38 | 39 | 40 | 41 | 42 | maven-compiler-plugin 43 | 3.8.0 44 | 45 | 11 46 | 47 | 48 | 49 | maven-assembly-plugin 50 | 2.2-beta-5 51 | 52 | 53 | 54 | ru.r2cloud.rtlspectrum.Main 55 | 56 | 57 | 58 | jar-with-dependencies 59 | 60 | rtlSpectrum_${javafx.platform} 61 | false 62 | 63 | 64 | 65 | maven-deploy-plugin 66 | 2.7 67 | 68 | true 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | coverage 77 | 78 | 79 | 80 | org.jacoco 81 | jacoco-maven-plugin 82 | 0.8.3 83 | 84 | 85 | prepare-agent 86 | 87 | prepare-agent 88 | 89 | 90 | 91 | report 92 | 93 | report 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | release 103 | 104 | 105 | 106 | org.apache.maven.plugins 107 | maven-release-plugin 108 | 2.5.1 109 | 110 | true 111 | false 112 | release 113 | deploy 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | The Apache Software License, Version 2.0 124 | http://www.apache.org/licenses/LICENSE-2.0.txt 125 | 126 | 127 | 128 | 129 | DErNasherBrezson 130 | DErNasherBrezson 131 | dernasherbrezon@gmail.com 132 | 133 | 134 | 135 | 136 | https://github.com/dernasherbrezon/rtlSpectrum 137 | scm:git:git@github.com/dernasherbrezon/rtlSpectrum.git 138 | scm:git:git@github.com:dernasherbrezon/rtlSpectrum.git 139 | rtlSpectrum-1.6 140 | 141 | 142 | 143 | GitHub Issues 144 | https://github.com/dernasherbrezon/rtlSpectrum/issues 145 | 146 | -------------------------------------------------------------------------------- /screenshots/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dernasherbrezon/rtlSpectrum/572d556d602eb0daff43ab9894190862d26c4ead/screenshots/1.png -------------------------------------------------------------------------------- /screenshots/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dernasherbrezon/rtlSpectrum/572d556d602eb0daff43ab9894190862d26c4ead/screenshots/2.png -------------------------------------------------------------------------------- /shortcuts.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | mvn -P release release:clean release:prepare 4 | mvn -P release release:perform -------------------------------------------------------------------------------- /src/main/assembly/assembly.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | internalresources 6 | 7 | dir 8 | 9 | . 10 | false 11 | 12 | 13 | 14 | 15 | *:* 16 | 17 | . 18 | false 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /src/main/java/module-info.java: -------------------------------------------------------------------------------- 1 | module rtlspectrum { 2 | requires javafx.controls; 3 | requires javafx.fxml; 4 | 5 | opens ru.r2cloud.rtlspectrum to javafx.graphics,javafx.fxml; 6 | 7 | } -------------------------------------------------------------------------------- /src/main/java/ru/r2cloud/rtlspectrum/BinData.java: -------------------------------------------------------------------------------- 1 | package ru.r2cloud.rtlspectrum; 2 | 3 | import javafx.scene.chart.XYChart; 4 | 5 | public class BinData { 6 | 7 | private String date; 8 | private String time; 9 | private String frequencyStart; 10 | private long frequencyStartParsed; 11 | private String frequencyEnd; 12 | private String binSize; 13 | private String numberOfSamples; 14 | private XYChart.Data parsed; 15 | private double dbmAverage; 16 | 17 | private double dbmTotal; 18 | private int dbmCount; 19 | 20 | public BinData() { 21 | // do nothing 22 | } 23 | 24 | public BinData(BinData copy) { 25 | this.date = copy.date; 26 | this.time = copy.time; 27 | this.frequencyStart = copy.frequencyStart; 28 | this.frequencyStartParsed = copy.frequencyStartParsed; 29 | this.frequencyEnd = copy.frequencyEnd; 30 | this.binSize = copy.binSize; 31 | this.numberOfSamples = copy.numberOfSamples; 32 | this.parsed = copy.parsed; 33 | this.dbmAverage = copy.dbmAverage; 34 | this.dbmTotal = copy.dbmTotal; 35 | this.dbmCount = copy.dbmCount; 36 | } 37 | 38 | public long getFrequencyStartParsed() { 39 | return frequencyStartParsed; 40 | } 41 | 42 | public void setFrequencyStartParsed(long frequencyStartParsed) { 43 | this.frequencyStartParsed = frequencyStartParsed; 44 | } 45 | 46 | public int getDbmCount() { 47 | return dbmCount; 48 | } 49 | 50 | public void setDbmCount(int dbmCount) { 51 | this.dbmCount = dbmCount; 52 | } 53 | 54 | public double getDbmTotal() { 55 | return dbmTotal; 56 | } 57 | 58 | public void setDbmTotal(double dbmTotal) { 59 | this.dbmTotal = dbmTotal; 60 | } 61 | 62 | public XYChart.Data getParsed() { 63 | return parsed; 64 | } 65 | 66 | public void setParsed(XYChart.Data parsed) { 67 | this.parsed = parsed; 68 | } 69 | 70 | public String getDate() { 71 | return date; 72 | } 73 | 74 | public void setDate(String date) { 75 | this.date = date; 76 | } 77 | 78 | public String getTime() { 79 | return time; 80 | } 81 | 82 | public void setTime(String time) { 83 | this.time = time; 84 | } 85 | 86 | public String getFrequencyStart() { 87 | return frequencyStart; 88 | } 89 | 90 | public void setFrequencyStart(String frequencyStart) { 91 | this.frequencyStart = frequencyStart; 92 | } 93 | 94 | public String getFrequencyEnd() { 95 | return frequencyEnd; 96 | } 97 | 98 | public void setFrequencyEnd(String frequencyEnd) { 99 | this.frequencyEnd = frequencyEnd; 100 | } 101 | 102 | public String getBinSize() { 103 | return binSize; 104 | } 105 | 106 | public void setBinSize(String binSize) { 107 | this.binSize = binSize; 108 | } 109 | 110 | public String getNumberOfSamples() { 111 | return numberOfSamples; 112 | } 113 | 114 | public void setNumberOfSamples(String numberOfSamples) { 115 | this.numberOfSamples = numberOfSamples; 116 | } 117 | 118 | public void setDbmAverage(double dbmAverage) { 119 | this.dbmAverage = dbmAverage; 120 | } 121 | 122 | public double getDbmAverage() { 123 | return dbmAverage; 124 | } 125 | 126 | } 127 | -------------------------------------------------------------------------------- /src/main/java/ru/r2cloud/rtlspectrum/BinDataParser.java: -------------------------------------------------------------------------------- 1 | package ru.r2cloud.rtlspectrum; 2 | 3 | import java.util.ArrayList; 4 | import java.util.Collections; 5 | import java.util.Comparator; 6 | import java.util.HashMap; 7 | import java.util.List; 8 | import java.util.Map; 9 | import java.util.Map.Entry; 10 | import java.util.regex.Pattern; 11 | 12 | import javafx.scene.chart.XYChart; 13 | 14 | public class BinDataParser { 15 | 16 | private static final Pattern COMMA = Pattern.compile(","); 17 | private final Map cache = new HashMap<>(); 18 | 19 | public void addLine(String line) { 20 | Map cur = convertLine(line); 21 | if (cur.isEmpty()) { 22 | return; 23 | } 24 | for (Entry curEntry : cur.entrySet()) { 25 | BinData existingValue = cache.get(curEntry.getKey()); 26 | if (existingValue == null) { 27 | cache.put(curEntry.getKey(), curEntry.getValue()); 28 | continue; 29 | } 30 | existingValue.setDbmTotal(existingValue.getDbmTotal() + curEntry.getValue().getDbmTotal()); 31 | existingValue.setDbmCount(existingValue.getDbmCount() + curEntry.getValue().getDbmCount()); 32 | } 33 | } 34 | 35 | public List convert() { 36 | List result = new ArrayList<>(cache.values()); 37 | Collections.sort(result, new Comparator() { 38 | @Override 39 | public int compare(BinData o1, BinData o2) { 40 | return Long.compare(o1.getFrequencyStartParsed(), o2.getFrequencyStartParsed()); 41 | } 42 | }); 43 | for (BinData cur : result) { 44 | 45 | XYChart.Data parsed = new XYChart.Data<>(); 46 | parsed.setXValue(cur.getFrequencyStartParsed()); 47 | parsed.setYValue(cur.getDbmTotal() / cur.getDbmCount()); 48 | 49 | cur.setParsed(parsed); 50 | cur.setDbmAverage(cur.getDbmTotal() / cur.getDbmCount()); 51 | } 52 | return result; 53 | } 54 | 55 | // format is: 2019-06-07, 19:44:45, 40000000, 41000000, 1000000.00, 1, -24.22, 56 | // -24.22, ... 57 | static Map convertLine(String line) { 58 | String[] parts = COMMA.split(line); 59 | if (parts.length < 7) { 60 | return Collections.emptyMap(); 61 | } 62 | String date = parts[0].trim(); 63 | String time = parts[1].trim(); 64 | long frequencyStart = Long.parseLong(parts[2].trim()); 65 | double step = Double.parseDouble(parts[4].trim()); 66 | Map result = new HashMap<>(); 67 | for (int i = 0; i < parts.length - 7 + 1; i++) { 68 | BinData cur = new BinData(); 69 | cur.setDate(date); 70 | cur.setTime(time); 71 | cur.setFrequencyStartParsed((long) (frequencyStart + i * step)); 72 | cur.setFrequencyStart(String.valueOf(cur.getFrequencyStartParsed())); 73 | cur.setFrequencyEnd(parts[4].trim()); 74 | cur.setBinSize(parts[4].trim()); 75 | cur.setNumberOfSamples(parts[5].trim()); 76 | double value; 77 | try { 78 | value = Double.valueOf(parts[6 + i].trim()); 79 | } catch (NumberFormatException e) { 80 | continue; 81 | } 82 | 83 | cur.setDbmTotal(value); 84 | cur.setDbmCount(1); 85 | 86 | result.put(cur.getFrequencyStart(), cur); 87 | } 88 | return result; 89 | } 90 | 91 | } 92 | -------------------------------------------------------------------------------- /src/main/java/ru/r2cloud/rtlspectrum/Controller.java: -------------------------------------------------------------------------------- 1 | package ru.r2cloud.rtlspectrum; 2 | 3 | import java.io.File; 4 | import java.net.URL; 5 | import java.util.ArrayList; 6 | import java.util.List; 7 | import java.util.ResourceBundle; 8 | import java.util.UUID; 9 | import java.util.concurrent.ExecutorService; 10 | import java.util.concurrent.Executors; 11 | 12 | import javafx.fxml.FXML; 13 | import javafx.fxml.Initializable; 14 | import javafx.scene.chart.NumberAxis; 15 | import javafx.scene.chart.XYChart; 16 | import javafx.scene.control.Button; 17 | import javafx.scene.control.MenuBar; 18 | import javafx.scene.control.MenuItem; 19 | import javafx.scene.layout.HBox; 20 | import javafx.scene.layout.VBox; 21 | import javafx.stage.FileChooser; 22 | import javafx.stage.FileChooser.ExtensionFilter; 23 | 24 | public class Controller implements Initializable { 25 | 26 | @FXML 27 | private LineChartWithMarkers lineChart; 28 | 29 | @FXML 30 | private VBox welcomeMessage; 31 | 32 | @FXML 33 | private HBox statusBar; 34 | @FXML 35 | private StatusBar statusBarController; 36 | 37 | @FXML 38 | private Button runNowButton; 39 | @FXML 40 | private Button loadFileButton; 41 | @FXML 42 | private MenuItem loadFileMenu; 43 | @FXML 44 | private MenuItem runNowMenu; 45 | @FXML 46 | private MenuItem addFileMenu; 47 | @FXML 48 | private MenuItem subtractFileMenu; 49 | @FXML 50 | private MenuBar menuBar; 51 | 52 | private List> rawData = new ArrayList<>(); 53 | 54 | private ExecutorService executorService; 55 | private RunRtlPower rtlPowerTask; 56 | 57 | @Override 58 | public void initialize(URL location, ResourceBundle resources) { 59 | executorService = Executors.newFixedThreadPool(2); 60 | ((NumberAxis) lineChart.getXAxis()).setTickLabelFormatter(new FrequencyFormatter()); 61 | ((NumberAxis) lineChart.getYAxis()).setTickLabelFormatter(new PowerFormatter()); 62 | if (System.getProperty("testfx.running") != null) { 63 | menuBar.setUseSystemMenuBar(false); 64 | } 65 | } 66 | 67 | @FXML 68 | public void runNow() { 69 | RtlPowerProgress progressTask = new RtlPowerProgress(statusBarController); 70 | rtlPowerTask = new RunRtlPower(progressTask); 71 | rtlPowerTask.setOnRunning(succeesesEvent -> statusBarController.beginTask("null")); 72 | 73 | disableButtons(true); 74 | 75 | rtlPowerTask.setOnSucceeded(succeededEvent -> { 76 | List result = rtlPowerTask.getValue(); 77 | statusBarController.completeTask(); 78 | welcomeMessage.setVisible(false); 79 | disableButtons(false); 80 | setupChart(result, false); 81 | }); 82 | rtlPowerTask.setOnFailed(workerStateEvent -> { 83 | progressTask.cancel(true); 84 | statusBarController.completeError(rtlPowerTask.getException().getMessage()); 85 | disableButtons(false); 86 | }); 87 | 88 | executorService.execute(rtlPowerTask); 89 | executorService.execute(progressTask); 90 | } 91 | 92 | @FXML 93 | public void loadFile() { 94 | loadFile(false); 95 | } 96 | 97 | @FXML 98 | public void addFile() { 99 | loadFile(true); 100 | } 101 | 102 | @FXML 103 | public void save() { 104 | if (lineChart.getNoData() || rawData.isEmpty()) { 105 | return; 106 | } 107 | 108 | FileChooser fileChooser = new FileChooser(); 109 | fileChooser.setTitle("Save file"); 110 | fileChooser.getExtensionFilters().add(new ExtensionFilter("CSV files", "*.csv")); 111 | File selectedFile = fileChooser.showSaveDialog(welcomeMessage.getScene().getWindow()); 112 | if (selectedFile == null) { 113 | return; 114 | } 115 | 116 | disableButtons(true); 117 | 118 | SaveTask saveTask = new SaveTask(statusBarController, selectedFile, rawData.get(0)); 119 | saveTask.setOnRunning(succeesesEvent -> statusBarController.beginTask(selectedFile.getAbsolutePath())); 120 | saveTask.setOnSucceeded(succeededEvent -> { 121 | disableButtons(false); 122 | statusBarController.completeTask(); 123 | }); 124 | saveTask.setOnFailed(workerStateEvent -> { 125 | disableButtons(false); 126 | statusBarController.completeError(saveTask.getException().getMessage()); 127 | }); 128 | 129 | executorService.execute(saveTask); 130 | } 131 | 132 | @FXML 133 | public void clearChart() { 134 | lineChart.getData().clear(); 135 | } 136 | 137 | @FXML 138 | public void subtractFile() { 139 | File selectedFile = requestFileForOpen(); 140 | if (selectedFile == null) { 141 | return; 142 | } 143 | 144 | disableButtons(true); 145 | 146 | SubtractFile task = new SubtractFile(statusBarController, selectedFile, rawData); 147 | task.setOnRunning(succeesesEvent -> statusBarController.beginTask(selectedFile.getAbsolutePath())); 148 | task.setOnSucceeded(succeededEvent -> { 149 | List> result = task.getValue(); 150 | statusBarController.completeTask(); 151 | disableButtons(false); 152 | 153 | rawData = result; 154 | lineChart.getData().clear(); 155 | for (List curGraph : result) { 156 | XYChart.Series series = new XYChart.Series<>(); 157 | for (BinData cur : curGraph) { 158 | series.getData().add(cur.getParsed()); 159 | } 160 | lineChart.getData().add(series); 161 | } 162 | lineChart.setVisible(true); 163 | }); 164 | task.setOnFailed(workerStateEvent -> { 165 | disableButtons(false); 166 | statusBarController.completeError(task.getException().getMessage()); 167 | }); 168 | 169 | executorService.execute(task); 170 | } 171 | 172 | private void loadFile(boolean append) { 173 | File selectedFile = requestFileForOpen(); 174 | if (selectedFile == null) { 175 | return; 176 | } 177 | 178 | disableButtons(true); 179 | 180 | LoadFile readTask = new LoadFile(statusBarController, selectedFile); 181 | readTask.setOnRunning(succeesesEvent -> statusBarController.beginTask(selectedFile.getAbsolutePath())); 182 | readTask.setOnSucceeded(succeededEvent -> { 183 | List result = readTask.getValue(); 184 | statusBarController.completeTask(); 185 | welcomeMessage.setVisible(false); 186 | disableButtons(false); 187 | setupChart(result, append); 188 | }); 189 | readTask.setOnFailed(workerStateEvent -> { 190 | disableButtons(false); 191 | statusBarController.completeError(readTask.getException().getMessage()); 192 | }); 193 | 194 | executorService.execute(readTask); 195 | } 196 | 197 | private File requestFileForOpen() { 198 | FileChooser fileChooser = new FileChooser(); 199 | fileChooser.setTitle("Open file"); 200 | String defaultDirectory = System.getProperty("rtlSpectrum.defaultdirectory"); 201 | if (defaultDirectory != null) { 202 | fileChooser.setInitialDirectory(new File(defaultDirectory)); 203 | } 204 | fileChooser.getExtensionFilters().add(new ExtensionFilter("CSV files", "*.csv")); 205 | return fileChooser.showOpenDialog(welcomeMessage.getScene().getWindow()); 206 | } 207 | 208 | private void setupChart(List data, boolean append) { 209 | XYChart.Series series = new XYChart.Series<>(); 210 | for (BinData cur : data) { 211 | series.getData().add(cur.getParsed()); 212 | series.setName(UUID.randomUUID().toString()); 213 | } 214 | if (!append) { 215 | lineChart.getData().clear(); 216 | rawData = new ArrayList<>(); 217 | } 218 | rawData.add(data); 219 | lineChart.getData().add(series); 220 | lineChart.setVisible(true); 221 | } 222 | 223 | private void disableButtons(boolean value) { 224 | runNowButton.setDisable(value); 225 | loadFileButton.setDisable(value); 226 | loadFileMenu.setDisable(value); 227 | runNowMenu.setDisable(value); 228 | addFileMenu.setDisable(value); 229 | subtractFileMenu.setDisable(value); 230 | } 231 | 232 | public void stop() { 233 | if (executorService != null) { 234 | executorService.shutdownNow(); 235 | // explicitly cancel this task as rtl_power is on native code and interruptions do not work there 236 | if (rtlPowerTask != null) { 237 | rtlPowerTask.cancel(); 238 | } 239 | } 240 | } 241 | } 242 | -------------------------------------------------------------------------------- /src/main/java/ru/r2cloud/rtlspectrum/FrequencyFormatter.java: -------------------------------------------------------------------------------- 1 | package ru.r2cloud.rtlspectrum; 2 | 3 | import java.text.DecimalFormat; 4 | import java.text.NumberFormat; 5 | 6 | import javafx.util.StringConverter; 7 | 8 | public class FrequencyFormatter extends StringConverter { 9 | 10 | private static final long ONE_KILOHERZ = 1_000; 11 | private static final long ONE_MEGAHERZ = 1_000_000; 12 | private static final long ONE_GIGAHERZ = 1_000_000_000; 13 | 14 | private final NumberFormat format; 15 | 16 | public FrequencyFormatter() { 17 | format = new DecimalFormat("#.#"); 18 | } 19 | 20 | @Override 21 | public String toString(Number object) { 22 | if (object == null) { 23 | return ""; 24 | } 25 | if (object.longValue() < 0) { 26 | return ""; 27 | } 28 | if (object.longValue() < ONE_KILOHERZ) { 29 | return Long.valueOf(object.longValue()) + " Hz"; 30 | } 31 | if (object.longValue() < ONE_MEGAHERZ) { 32 | return format.format(object.doubleValue() / ONE_KILOHERZ) + " KHz"; 33 | } 34 | if (object.longValue() < ONE_GIGAHERZ) { 35 | return format.format(object.doubleValue() / ONE_MEGAHERZ) + " MHz"; 36 | } 37 | return format.format(object.doubleValue() / ONE_GIGAHERZ) + " GHz"; 38 | } 39 | 40 | @Override 41 | public Number fromString(String string) { 42 | return null; 43 | } 44 | 45 | } 46 | -------------------------------------------------------------------------------- /src/main/java/ru/r2cloud/rtlspectrum/Legend.java: -------------------------------------------------------------------------------- 1 | package ru.r2cloud.rtlspectrum; 2 | 3 | import javafx.scene.control.Label; 4 | import javafx.scene.layout.BorderPane; 5 | import javafx.scene.layout.HBox; 6 | import javafx.scene.paint.Color; 7 | import javafx.scene.paint.Paint; 8 | import javafx.scene.shape.Rectangle; 9 | 10 | public class Legend extends HBox { 11 | 12 | private static final Color BLACK = Color.rgb(0, 0, 0); 13 | private final Rectangle rectangle = new Rectangle(); 14 | private final Label label = new Label(); 15 | 16 | public Legend() { 17 | rectangle.setHeight(10.0); 18 | rectangle.setWidth(10.0); 19 | 20 | label.setStyle("-fx-padding: 0; -fx-text-fill: black;"); 21 | 22 | BorderPane pane = new BorderPane(); 23 | pane.setCenter(rectangle); 24 | getChildren().add(pane); 25 | getChildren().add(label); 26 | setSpacing(5.0); 27 | } 28 | 29 | public void setStroke(Paint stroke) { 30 | rectangle.setStroke(stroke); 31 | rectangle.setFill(stroke); 32 | } 33 | 34 | public void setText(String text) { 35 | label.setText(text); 36 | } 37 | 38 | public boolean hasStroke() { 39 | if (rectangle.getStroke() == null) { 40 | return false; 41 | } 42 | if (!(rectangle.getStroke() instanceof Color)) { 43 | return false; 44 | } 45 | Color stroke = (Color) rectangle.getStroke(); 46 | return !stroke.equals(BLACK); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/main/java/ru/r2cloud/rtlspectrum/LineChartWithMarkers.java: -------------------------------------------------------------------------------- 1 | package ru.r2cloud.rtlspectrum; 2 | 3 | import java.util.ArrayList; 4 | import java.util.Collections; 5 | import java.util.HashMap; 6 | import java.util.Iterator; 7 | import java.util.List; 8 | import java.util.Map; 9 | 10 | import javafx.beans.NamedArg; 11 | import javafx.beans.property.BooleanProperty; 12 | import javafx.beans.property.SimpleBooleanProperty; 13 | import javafx.beans.value.ChangeListener; 14 | import javafx.beans.value.ObservableValue; 15 | import javafx.collections.ListChangeListener; 16 | import javafx.collections.ObservableList; 17 | import javafx.event.EventHandler; 18 | import javafx.geometry.Point2D; 19 | import javafx.scene.Node; 20 | import javafx.scene.chart.Axis; 21 | import javafx.scene.chart.LineChart; 22 | import javafx.scene.chart.NumberAxis; 23 | import javafx.scene.control.Label; 24 | import javafx.scene.input.MouseEvent; 25 | import javafx.scene.layout.Pane; 26 | import javafx.scene.paint.Color; 27 | import javafx.scene.shape.Line; 28 | import javafx.scene.shape.LineTo; 29 | import javafx.scene.shape.MoveTo; 30 | import javafx.scene.shape.Path; 31 | import javafx.scene.shape.PathElement; 32 | import javafx.util.StringConverter; 33 | 34 | public class LineChartWithMarkers extends LineChart { 35 | 36 | private static final int CURSOR_X_MARGIN = 12; 37 | private static final int CURSOR_Y_MARGIN = 20; 38 | 39 | private Line line = new Line(); 40 | private StringConverter xConverter; 41 | private StringConverter yConverter; 42 | 43 | private final BooleanProperty noData = new SimpleBooleanProperty(this, "noData", true); 44 | private final Pane tooltip; 45 | private final Label tooltipXLabel; 46 | private Map legendByName = new HashMap<>(); 47 | 48 | public LineChartWithMarkers(@NamedArg("xAxis") Axis xAxis, @NamedArg("yAxis") Axis yAxis, @NamedArg("tooltip") Pane tooltip) { 49 | super(xAxis, yAxis); 50 | this.tooltip = tooltip; 51 | line.setVisible(false); 52 | line.setStroke(Color.GREY); 53 | line.getStrokeDashArray().add(4d); 54 | 55 | tooltipXLabel = (Label) tooltip.lookup("#tooltipXLabel"); 56 | getChildren().add(tooltip); 57 | 58 | if (xAxis instanceof NumberAxis) { 59 | NumberAxis numberedXAxis = (NumberAxis) xAxis; 60 | numberedXAxis.setForceZeroInRange(false); 61 | xConverter = numberedXAxis.getTickLabelFormatter(); 62 | numberedXAxis.tickLabelFormatterProperty().addListener(new ChangeListener>() { 63 | @Override 64 | public void changed(ObservableValue> observable, StringConverter oldValue, StringConverter newValue) { 65 | xConverter = newValue; 66 | } 67 | }); 68 | } 69 | if (yAxis instanceof NumberAxis) { 70 | NumberAxis numberedYAxis = (NumberAxis) yAxis; 71 | yConverter = numberedYAxis.getTickLabelFormatter(); 72 | numberedYAxis.tickLabelFormatterProperty().addListener(new ChangeListener>() { 73 | 74 | @Override 75 | public void changed(ObservableValue> observable, StringConverter oldValue, StringConverter newValue) { 76 | yConverter = newValue; 77 | } 78 | }); 79 | } 80 | 81 | final Node chartBackground2 = lookup(".chart-plot-background"); 82 | 83 | chartBackground2.setOnMouseMoved(new EventHandler() { 84 | @Override 85 | public void handle(MouseEvent mouseEvent) { 86 | Point2D point = new Point2D(mouseEvent.getX(), mouseEvent.getY()); 87 | String xLabel = getXLabel(point); 88 | if (xLabel.length() == 0) { 89 | return; 90 | } 91 | 92 | line.setStartX(point.getX()); 93 | line.setEndX(line.getStartX()); 94 | line.setStartY(0.0); 95 | line.setEndY(getBoundsInLocal().getHeight()); 96 | line.setVisible(true); 97 | 98 | tooltip.setVisible(true); 99 | tooltipXLabel.setText(xLabel); 100 | 101 | double xWithinPlotArea = CURSOR_X_MARGIN + point.getX(); 102 | double yWithinPlotArea = CURSOR_Y_MARGIN + point.getY(); 103 | 104 | double newX = xWithinPlotArea + chartBackground2.getLayoutX(); 105 | double newY = yWithinPlotArea + chartBackground2.getLayoutY(); 106 | 107 | if (xWithinPlotArea + tooltip.getWidth() > chartBackground2.getBoundsInLocal().getMaxX()) { 108 | newX = point.getX() - tooltip.getWidth() - CURSOR_X_MARGIN + chartBackground2.getLayoutX(); 109 | } 110 | if (yWithinPlotArea + tooltip.getHeight() > chartBackground2.getBoundsInLocal().getMaxY()) { 111 | newY = point.getY() - tooltip.getHeight(); 112 | } 113 | 114 | tooltip.setTranslateX(newX); 115 | tooltip.setTranslateY(newY); 116 | 117 | Map yValues = getNearestYValue(getXAxis().getValueForDisplay(point.getX())); 118 | for (Series series : getData()) { 119 | Legend legend = legendByName.get(series.getName()); 120 | if (legend == null) { 121 | continue; 122 | } 123 | Number yValue = yValues.get(series.getName()); 124 | if (yValue == null || Double.isNaN(yValue.doubleValue())) { 125 | legend.setVisible(false); 126 | continue; 127 | } 128 | legend.setVisible(true); 129 | if (!legend.hasStroke()) { 130 | legend.setStroke(((Path) series.getNode().lookup(".chart-series-line")).getStroke()); 131 | } 132 | if (yConverter != null) { 133 | legend.setText(yConverter.toString(yValue)); 134 | } else { 135 | legend.setText(yValue.toString()); 136 | } 137 | } 138 | } 139 | 140 | }); 141 | 142 | line.setOnMouseMoved(new EventHandler() { 143 | @Override 144 | public void handle(MouseEvent mouseEvent) { 145 | chartBackground2.getOnMouseMoved().handle(mouseEvent); 146 | } 147 | }); 148 | 149 | getPlotChildren().add(line); 150 | 151 | getData().addListener(new ListChangeListener>() { 152 | @Override 153 | public void onChanged(Change> c) { 154 | while (c.next()) { 155 | for (Series cur : c.getAddedSubList()) { 156 | Legend rec = new Legend(); 157 | rec.setVisible(false); 158 | legendByName.put(cur.getName(), rec); 159 | tooltip.getChildren().add(rec); 160 | } 161 | for (Series cur : c.getRemoved()) { 162 | Legend previous = legendByName.remove(cur.getName()); 163 | if (previous == null) { 164 | continue; 165 | } 166 | tooltip.getChildren().remove(previous); 167 | } 168 | } 169 | noData.set(c.getList().isEmpty()); 170 | } 171 | }); 172 | } 173 | 174 | @Override 175 | protected void layoutPlotChildren() { 176 | List constructedPath = new ArrayList<>(getData().size()); 177 | for (int seriesIndex = 0; seriesIndex < getData().size(); seriesIndex++) { 178 | Series series = getData().get(seriesIndex); 179 | if (series.getNode() instanceof Path) { 180 | ObservableList seriesLine = ((Path) series.getNode()).getElements(); 181 | seriesLine.clear(); 182 | constructedPath.clear(); 183 | MoveTo nextMoveTo = null; 184 | for (Iterator> it = getDisplayedDataIterator(series); it.hasNext();) { 185 | Data item = it.next(); 186 | double x = getXAxis().getDisplayPosition(item.getXValue()); 187 | double y = getYAxis().getDisplayPosition(getYAxis().toRealValue(getYAxis().toNumericValue(item.getYValue()))); 188 | if (Double.isNaN(x) || Double.isNaN(y)) { 189 | int index = series.getData().indexOf(item); 190 | if (index < series.getData().size() - 1) { 191 | Data next = series.getData().get(index + 1); 192 | double nextX = getXAxis().getDisplayPosition(next.getXValue()); 193 | double nextY = getYAxis().getDisplayPosition(getYAxis().toRealValue(getYAxis().toNumericValue(next.getYValue()))); 194 | nextMoveTo = new MoveTo(nextX, nextY); 195 | } 196 | } else { 197 | if (nextMoveTo != null) { 198 | constructedPath.add(nextMoveTo); 199 | nextMoveTo = null; 200 | } 201 | constructedPath.add(new LineTo(x, y)); 202 | Node symbol = item.getNode(); 203 | if (symbol != null) { 204 | double w = symbol.prefWidth(-1); 205 | double h = symbol.prefHeight(-1); 206 | symbol.resizeRelocate(x - (w / 2), y - (h / 2), w, h); 207 | } 208 | } 209 | } 210 | 211 | if (!constructedPath.isEmpty()) { 212 | PathElement first = constructedPath.get(0); 213 | seriesLine.add(new MoveTo(getX(first), getY(first))); 214 | seriesLine.addAll(constructedPath); 215 | } 216 | } 217 | } 218 | 219 | } 220 | 221 | private static double getX(PathElement element) { 222 | if (element instanceof LineTo) { 223 | return ((LineTo) element).getX(); 224 | } else if (element instanceof MoveTo) { 225 | return ((MoveTo) element).getX(); 226 | } else { 227 | throw new IllegalArgumentException(element + " is not a valid type"); 228 | } 229 | } 230 | 231 | private static double getY(PathElement element) { 232 | if (element instanceof LineTo) { 233 | return ((LineTo) element).getY(); 234 | } else if (element instanceof MoveTo) { 235 | return ((MoveTo) element).getY(); 236 | } else { 237 | throw new IllegalArgumentException(element + " is not a valid type"); 238 | } 239 | } 240 | 241 | @Override 242 | protected void layoutChildren() { 243 | super.layoutChildren(); 244 | final Pane chartContent = (Pane) lookup(".chart-content"); 245 | double top = chartContent.getLayoutY(); 246 | double left = chartContent.getLayoutX(); 247 | double right = snappedRightInset(); 248 | final double width = getWidth(); 249 | // copy paste from legend 250 | final double legendHeight = snapSizeX(tooltip.prefHeight(width - left - right)); 251 | final double legendWidth = boundedSize(snapSizeX(tooltip.prefWidth(legendHeight)), 0, width - left - right); 252 | tooltip.resizeRelocate(snapPositionX(left), snapPositionX(top), legendWidth, legendHeight); 253 | } 254 | 255 | private static double boundedSize(double value, double min, double max) { 256 | // if max < value, return max 257 | // if min > value, return min 258 | // if min > max, return min 259 | return Math.min(Math.max(value, min), Math.max(min, max)); 260 | } 261 | 262 | private Map getNearestYValue(Number xValue) { 263 | if (getData().isEmpty()) { 264 | return Collections.emptyMap(); 265 | } 266 | Map result = new HashMap<>(); 267 | for (Series series : getData()) { 268 | if (series.getData().isEmpty()) { 269 | continue; 270 | } 271 | Number yValue = getNumber(xValue, series); 272 | if (yValue == null) { 273 | continue; 274 | } 275 | result.put(series.getName(), yValue); 276 | } 277 | 278 | return result; 279 | } 280 | 281 | private static Number getNumber(Number xValue, Series series) { 282 | Data previous = null; 283 | for (Data cur : series.getData()) { 284 | double curX = cur.getXValue().doubleValue(); 285 | if (curX < xValue.doubleValue()) { 286 | previous = cur; 287 | continue; 288 | } 289 | if (curX == xValue.doubleValue()) { 290 | return cur.getYValue(); 291 | } 292 | 293 | if (previous == null) { 294 | return null; 295 | } 296 | // interpolate linearly 297 | return previous.getYValue().doubleValue() + ((xValue.doubleValue() - previous.getXValue().doubleValue()) / (cur.getXValue().doubleValue() - previous.getXValue().doubleValue())) * (cur.getYValue().doubleValue() - previous.getYValue().doubleValue()); 298 | } 299 | return null; 300 | } 301 | 302 | private String getXLabel(Point2D point) { 303 | Number xValue = getXAxis().getValueForDisplay(point.getX()); 304 | String textValue = ""; 305 | if (xConverter != null) { 306 | textValue += xConverter.toString(xValue); 307 | } else { 308 | textValue = xValue.toString(); 309 | } 310 | return textValue.trim(); 311 | } 312 | 313 | @Override 314 | protected void updateAxisRange() { 315 | final Axis xa = getXAxis(); 316 | final Axis ya = getYAxis(); 317 | List xData = null; 318 | List yData = null; 319 | if (xa.isAutoRanging()) 320 | xData = new ArrayList<>(); 321 | if (ya.isAutoRanging()) 322 | yData = new ArrayList<>(); 323 | if (xData != null || yData != null) { 324 | for (Series series : getData()) { 325 | for (Data data : series.getData()) { 326 | if (xData != null && !Double.isNaN(data.getXValue().doubleValue())) 327 | xData.add(data.getXValue()); 328 | if (yData != null && !Double.isNaN(data.getYValue().doubleValue())) 329 | yData.add(data.getYValue()); 330 | } 331 | } 332 | // RT-32838 No need to invalidate range if there is one data item - whose value 333 | // is zero. 334 | if (xData != null && !(xData.size() == 1 && getXAxis().toNumericValue(xData.get(0)) == 0)) { 335 | xa.invalidateRange(xData); 336 | } 337 | if (yData != null && !(yData.size() == 1 && getYAxis().toNumericValue(yData.get(0)) == 0)) { 338 | ya.invalidateRange(yData); 339 | } 340 | 341 | } 342 | } 343 | 344 | public BooleanProperty noDataProperty() { 345 | return noData; 346 | } 347 | 348 | public boolean getNoData() { 349 | return noData.get(); 350 | } 351 | 352 | public void setNoData(boolean value) { 353 | noData.set(value); 354 | } 355 | } 356 | -------------------------------------------------------------------------------- /src/main/java/ru/r2cloud/rtlspectrum/LoadFile.java: -------------------------------------------------------------------------------- 1 | package ru.r2cloud.rtlspectrum; 2 | 3 | import java.io.BufferedReader; 4 | import java.io.File; 5 | import java.io.FileReader; 6 | import java.util.List; 7 | 8 | public class LoadFile extends StatusBarTask> { 9 | 10 | private final File file; 11 | 12 | public LoadFile(StatusBar statusBar, File file) { 13 | super(statusBar); 14 | this.file = file; 15 | } 16 | 17 | @Override 18 | protected List call() throws Exception { 19 | updateMessage("Reading file: " + file.getAbsolutePath()); 20 | BinDataParser parser = new BinDataParser(); 21 | try (BufferedReader r = new BufferedReader(new FileReader(file))) { 22 | String curLine = null; 23 | while ((curLine = r.readLine()) != null) { 24 | parser.addLine(curLine); 25 | } 26 | } 27 | return parser.convert(); 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /src/main/java/ru/r2cloud/rtlspectrum/MacOsUtil.java: -------------------------------------------------------------------------------- 1 | package ru.r2cloud.rtlspectrum; 2 | 3 | import java.io.BufferedReader; 4 | import java.io.InputStreamReader; 5 | import java.nio.charset.StandardCharsets; 6 | import java.util.Locale; 7 | 8 | class MacOsUtil { 9 | 10 | static boolean isDark(String command) { 11 | ProcessBuilder builder = new ProcessBuilder().command(command, "read", "-g", "AppleInterfaceStyle"); 12 | Process process; 13 | try { 14 | process = builder.start(); 15 | int resultCode = process.waitFor(); 16 | if (resultCode != 0) { 17 | return false; 18 | } 19 | } catch (InterruptedException e1) { 20 | Thread.currentThread().interrupt(); 21 | e1.printStackTrace(); 22 | return false; 23 | } catch (Exception e) { 24 | e.printStackTrace(); 25 | return false; 26 | } 27 | try (BufferedReader r = new BufferedReader(new InputStreamReader(process.getInputStream(), StandardCharsets.ISO_8859_1))) { 28 | String result = r.readLine(); 29 | if (result == null) { 30 | return false; 31 | } 32 | return result.toLowerCase(Locale.UK).contains("dark"); 33 | } catch (Exception e) { 34 | e.printStackTrace(); 35 | return false; 36 | } 37 | } 38 | 39 | private MacOsUtil() { 40 | // do nothing 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/main/java/ru/r2cloud/rtlspectrum/Main.java: -------------------------------------------------------------------------------- 1 | package ru.r2cloud.rtlspectrum; 2 | 3 | public class Main { 4 | 5 | public static void main(String[] args) { 6 | MainApplication.main(args); 7 | } 8 | 9 | } 10 | -------------------------------------------------------------------------------- /src/main/java/ru/r2cloud/rtlspectrum/MainApplication.java: -------------------------------------------------------------------------------- 1 | package ru.r2cloud.rtlspectrum; 2 | 3 | import java.io.IOException; 4 | import java.io.InputStream; 5 | import java.util.Locale; 6 | 7 | import javafx.application.Application; 8 | import javafx.fxml.FXMLLoader; 9 | import javafx.scene.Parent; 10 | import javafx.scene.Scene; 11 | import javafx.stage.Stage; 12 | 13 | public class MainApplication extends Application { 14 | 15 | private Controller controller; 16 | 17 | @Override 18 | public void start(Stage stage) { 19 | Parent root; 20 | try (InputStream is = getClass().getClassLoader().getResourceAsStream("layout.fxml")) { 21 | FXMLLoader fxmlLoader = new FXMLLoader(getClass().getClassLoader().getResource("layout.fxml")); 22 | root = fxmlLoader.load(is); 23 | controller = fxmlLoader.getController(); 24 | } catch (IOException e) { 25 | throw new IllegalStateException(e); 26 | } 27 | 28 | Scene scene = new Scene(root, 640, 480); 29 | String osName = System.getProperty("os.name"); 30 | if (osName != null && osName.toLowerCase(Locale.UK).contains("mac") && MacOsUtil.isDark("/usr/bin/defaults")) { 31 | scene.getStylesheets().add("dark.css"); 32 | } 33 | stage.setScene(scene); 34 | stage.setTitle("rtlSpectrum"); 35 | stage.show(); 36 | } 37 | 38 | @Override 39 | public void stop() throws Exception { 40 | if (controller != null) { 41 | controller.stop(); 42 | } 43 | } 44 | 45 | public static void main(String[] args) { 46 | launch(); 47 | } 48 | 49 | } 50 | -------------------------------------------------------------------------------- /src/main/java/ru/r2cloud/rtlspectrum/PowerFormatter.java: -------------------------------------------------------------------------------- 1 | package ru.r2cloud.rtlspectrum; 2 | 3 | import java.text.DecimalFormat; 4 | import java.text.NumberFormat; 5 | 6 | import javafx.util.StringConverter; 7 | 8 | public class PowerFormatter extends StringConverter { 9 | 10 | private final NumberFormat format; 11 | 12 | public PowerFormatter() { 13 | format = new DecimalFormat("#.##"); 14 | } 15 | 16 | @Override 17 | public String toString(Number object) { 18 | if (object == null || Double.isNaN(object.doubleValue())) { 19 | return ""; 20 | } 21 | return format.format(object.doubleValue()); 22 | } 23 | 24 | @Override 25 | public Number fromString(String string) { 26 | return null; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/main/java/ru/r2cloud/rtlspectrum/RtlPowerProgress.java: -------------------------------------------------------------------------------- 1 | package ru.r2cloud.rtlspectrum; 2 | 3 | public class RtlPowerProgress extends StatusBarTask { 4 | 5 | public RtlPowerProgress(StatusBar statusBar) { 6 | super(statusBar); 7 | } 8 | 9 | @Override 10 | protected Void call() throws Exception { 11 | // taken from the reference rtl-sdr mac book air 2013 12 | long averageNumberOfSecondsPerFullSpectrum = 121; 13 | long numberOfSeconds = ((long) Math.ceil((double) RunRtlPower.NUMBER_OF_SECONDS / averageNumberOfSecondsPerFullSpectrum)) * averageNumberOfSecondsPerFullSpectrum; 14 | for (long i = 0; i < numberOfSeconds; i++) { 15 | updateProgress("Running rtl_power ETA: " + formatETA((numberOfSeconds - i) * 1000), i, numberOfSeconds); 16 | Thread.sleep(1000); 17 | } 18 | updateProgress("Running rtl_power ETA: almost done", -1, -1); 19 | return null; 20 | } 21 | 22 | } 23 | -------------------------------------------------------------------------------- /src/main/java/ru/r2cloud/rtlspectrum/RunRtlPower.java: -------------------------------------------------------------------------------- 1 | package ru.r2cloud.rtlspectrum; 2 | 3 | import java.io.BufferedReader; 4 | import java.io.InputStreamReader; 5 | import java.nio.charset.StandardCharsets; 6 | import java.util.List; 7 | import java.util.concurrent.TimeUnit; 8 | import java.util.regex.Pattern; 9 | 10 | import javafx.concurrent.Task; 11 | 12 | public class RunRtlPower extends Task> { 13 | 14 | static final long MINIMUM_FREQ = 24_000_000; 15 | static final long MAXIMUM_FREQ = 1_700_000_000; 16 | static final long STEP = 1_000_000; 17 | static final long NUMBER_OF_SECONDS = TimeUnit.MINUTES.toSeconds(2); 18 | 19 | private static final Pattern SPACE = Pattern.compile("\\s"); 20 | 21 | private Process process; 22 | private final RtlPowerProgress progressTask; 23 | 24 | public RunRtlPower(RtlPowerProgress progressTask) { 25 | this.progressTask = progressTask; 26 | } 27 | 28 | @Override 29 | protected List call() throws Exception { 30 | updateMessage("Running rtl_power"); 31 | ProcessBuilder processBuilder = new ProcessBuilder(SPACE.split("rtl_power -f " + MINIMUM_FREQ + ":" + MAXIMUM_FREQ + ":" + STEP + " -i " + NUMBER_OF_SECONDS + " -g 0 -c 20% -1 -")); 32 | process = processBuilder.start(); 33 | String curLine = null; 34 | BinDataParser parser = new BinDataParser(); 35 | try (BufferedReader r = new BufferedReader(new InputStreamReader(process.getInputStream(), StandardCharsets.ISO_8859_1))) { 36 | while ((curLine = r.readLine()) != null && !Thread.currentThread().isInterrupted()) { 37 | if (isCancelled()) { 38 | break; 39 | } 40 | parser.addLine(curLine); 41 | } 42 | } 43 | try (BufferedReader r = new BufferedReader(new InputStreamReader(process.getErrorStream(), StandardCharsets.ISO_8859_1))) { 44 | while ((curLine = r.readLine()) != null && !Thread.currentThread().isInterrupted()) { 45 | if (curLine.equalsIgnoreCase("No supported devices found.")) { 46 | throw new IllegalStateException(curLine); 47 | } 48 | if (curLine.startsWith("usb_claim_interface")) { 49 | throw new IllegalStateException(curLine); 50 | } 51 | if (curLine.startsWith("stdbuf:")) { 52 | throw new IllegalStateException(curLine); 53 | } 54 | if (isCancelled()) { 55 | break; 56 | } 57 | } 58 | } 59 | progressTask.cancel(true); 60 | return parser.convert(); 61 | } 62 | 63 | @Override 64 | protected void cancelled() { 65 | super.cancelled(); 66 | if (process != null) { 67 | process.destroyForcibly(); 68 | } 69 | } 70 | 71 | } 72 | -------------------------------------------------------------------------------- /src/main/java/ru/r2cloud/rtlspectrum/SaveTask.java: -------------------------------------------------------------------------------- 1 | package ru.r2cloud.rtlspectrum; 2 | 3 | import java.io.BufferedWriter; 4 | import java.io.File; 5 | import java.io.FileWriter; 6 | import java.util.List; 7 | 8 | public class SaveTask extends StatusBarTask { 9 | 10 | private final File file; 11 | private final List data; 12 | 13 | public SaveTask(StatusBar statusBar, File file, List data) { 14 | super(statusBar); 15 | this.file = file; 16 | this.data = data; 17 | } 18 | 19 | @Override 20 | protected Void call() throws Exception { 21 | updateMessage("Saving to file: " + file.getAbsolutePath()); 22 | try (BufferedWriter w = new BufferedWriter(new FileWriter(file))) { 23 | for (int i = 0; i < data.size(); i++) { 24 | BinData cur = data.get(i); 25 | w.append(cur.getDate()).append(',').append(cur.getTime()).append(',').append(cur.getFrequencyStart()); 26 | w.append(',').append(cur.getFrequencyEnd()).append(',').append(cur.getBinSize()).append(','); 27 | w.append(cur.getNumberOfSamples()); 28 | w.append(',').append(String.valueOf(cur.getDbmAverage())); 29 | w.append('\n'); 30 | updateProgress(i, data.size()); 31 | } 32 | } 33 | return null; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/main/java/ru/r2cloud/rtlspectrum/StatusBar.java: -------------------------------------------------------------------------------- 1 | package ru.r2cloud.rtlspectrum; 2 | 3 | import javafx.beans.property.DoubleProperty; 4 | import javafx.beans.property.StringProperty; 5 | import javafx.fxml.FXML; 6 | import javafx.scene.control.Label; 7 | import javafx.scene.control.ProgressBar; 8 | 9 | public class StatusBar { 10 | 11 | public static final String LAST_COMPLETED_TASK = "lastCompletedTask"; 12 | 13 | @FXML 14 | private ProgressBar progressBar; 15 | @FXML 16 | private Label statusMessage; 17 | 18 | private String taskId; 19 | 20 | public void beginTask(String taskId) { 21 | progressBar.setVisible(true); 22 | synchronized (this) { 23 | this.taskId = taskId; 24 | } 25 | } 26 | 27 | public void completeTask() { 28 | completeTask(null); 29 | } 30 | 31 | public void completeError(String message) { 32 | completeTask("Error: " + message); 33 | } 34 | 35 | public void completeTask(String message) { 36 | statusMessage.textProperty().unbind(); 37 | if (message != null) { 38 | statusMessage.textProperty().set(message); 39 | } else { 40 | statusMessage.textProperty().set("OK"); 41 | } 42 | progressBar.setVisible(false); 43 | progressBar.progressProperty().unbind(); 44 | progressBar.progressProperty().set(-1); 45 | synchronized (this) { 46 | progressBar.getProperties().put(LAST_COMPLETED_TASK, taskId); 47 | } 48 | } 49 | 50 | public DoubleProperty progressProperty() { 51 | return progressBar.progressProperty(); 52 | } 53 | 54 | public StringProperty messageProperty() { 55 | return statusMessage.textProperty(); 56 | } 57 | 58 | } 59 | -------------------------------------------------------------------------------- /src/main/java/ru/r2cloud/rtlspectrum/StatusBarTask.java: -------------------------------------------------------------------------------- 1 | package ru.r2cloud.rtlspectrum; 2 | 3 | import java.text.SimpleDateFormat; 4 | import java.util.Date; 5 | 6 | import javafx.concurrent.Task; 7 | 8 | public abstract class StatusBarTask extends Task { 9 | 10 | private final SimpleDateFormat sdf = new SimpleDateFormat("mm'm':ss's'"); 11 | protected final StatusBar statusBar; 12 | 13 | protected StatusBarTask(StatusBar statusBar) { 14 | this.statusBar = statusBar; 15 | this.statusBar.progressProperty().bind(progressProperty()); 16 | this.statusBar.messageProperty().bind(messageProperty()); 17 | } 18 | 19 | public void updateProgress(String message, long workDone, long max) { 20 | updateMessage(message); 21 | updateProgress(workDone, max); 22 | } 23 | 24 | public String formatETA(long time) { 25 | return sdf.format(new Date(time)); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/main/java/ru/r2cloud/rtlspectrum/SubtractFile.java: -------------------------------------------------------------------------------- 1 | package ru.r2cloud.rtlspectrum; 2 | 3 | import java.io.BufferedReader; 4 | import java.io.File; 5 | import java.io.FileReader; 6 | import java.util.ArrayList; 7 | import java.util.List; 8 | 9 | import javafx.scene.chart.XYChart; 10 | 11 | public class SubtractFile extends StatusBarTask>> { 12 | 13 | private final File file; 14 | private final List> rawData; 15 | 16 | public SubtractFile(StatusBar statusBar, File file, List> rawData) { 17 | super(statusBar); 18 | this.file = file; 19 | this.rawData = rawData; 20 | } 21 | 22 | @Override 23 | protected List> call() throws Exception { 24 | updateMessage("Reading file: " + file.getAbsolutePath()); 25 | BinDataParser parser = new BinDataParser(); 26 | try (BufferedReader r = new BufferedReader(new FileReader(file))) { 27 | String curLine = null; 28 | while ((curLine = r.readLine()) != null) { 29 | parser.addLine(curLine); 30 | } 31 | } 32 | List fileData = parser.convert(); 33 | List> result = new ArrayList<>(); 34 | for (List cur : rawData) { 35 | List resultGraph = new ArrayList<>(); 36 | for (BinData curBin : cur) { 37 | BinData fileBin = findByFrequency(fileData, curBin.getFrequencyStart()); 38 | if (fileBin == null) { 39 | // ignore missing buckets. They might contain NaN or mismatched frequencies. 40 | // keep only relevant buckets in the chart 41 | continue; 42 | } 43 | 44 | double dbm1 = curBin.getDbmAverage(); 45 | double dbm2 = fileBin.getDbmAverage(); 46 | 47 | double value = dbm1 - dbm2; 48 | 49 | XYChart.Data parsed = new XYChart.Data<>(); 50 | parsed.setXValue(curBin.getFrequencyStartParsed()); 51 | parsed.setYValue(value); 52 | 53 | BinData subtractedBin = new BinData(curBin); 54 | subtractedBin.setDbmAverage(value); 55 | subtractedBin.setDbmTotal(value); 56 | subtractedBin.setDbmCount(1); 57 | subtractedBin.setParsed(parsed); 58 | resultGraph.add(subtractedBin); 59 | } 60 | result.add(resultGraph); 61 | } 62 | return result; 63 | } 64 | 65 | private static BinData findByFrequency(List data, String frequency) { 66 | for (BinData cur : data) { 67 | if (cur.getFrequencyStart().equals(frequency)) { 68 | return cur; 69 | } 70 | } 71 | return null; 72 | } 73 | 74 | } 75 | -------------------------------------------------------------------------------- /src/main/resources/dark.css: -------------------------------------------------------------------------------- 1 | .root { 2 | -fx-base: rgba(60, 63, 65, 255); 3 | } -------------------------------------------------------------------------------- /src/main/resources/layout.fxml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 |
39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 52 | 53 | 54 | 55 |
67 | 68 | 69 | 70 |
-------------------------------------------------------------------------------- /src/main/resources/statusBar.fxml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 14 | 15 | 19 | 20 | -------------------------------------------------------------------------------- /src/test/java/ru/r2cloud/rtlspectrum/BinDataParserTest.java: -------------------------------------------------------------------------------- 1 | package ru.r2cloud.rtlspectrum; 2 | 3 | import static org.junit.Assert.assertEquals; 4 | 5 | import java.util.List; 6 | 7 | import org.junit.Before; 8 | import org.junit.Test; 9 | 10 | public class BinDataParserTest { 11 | 12 | private BinDataParser parser; 13 | 14 | @Test 15 | public void testMergeIntervals() { 16 | parser.addLine("2021-11-14, 20:27:18, 433006, 435994, 58.59, 342414, -27.70, nan"); 17 | parser.addLine("2021-11-14, 20:27:18, 433006, 435994, 58.59, 342414, nan, -26.70"); 18 | 19 | List result = parser.convert(); 20 | assertEquals(2, result.size()); 21 | assertBinData(result.get(0), "433006", -27.70); 22 | assertBinData(result.get(1), "433064", -26.70); 23 | } 24 | 25 | @Test 26 | public void testAverage() { 27 | parser.addLine("2021-11-14, 20:27:18, 433006, 435994, 58.59, 342414, -30.0, -60.0"); 28 | parser.addLine("2021-11-14, 20:28:18, 433006, 435994, 58.59, 342414, -60.0, -30.0"); 29 | 30 | List result = parser.convert(); 31 | assertEquals(2, result.size()); 32 | assertBinData(result.get(0), "433006", -45.0); 33 | assertBinData(result.get(1), "433064", -45.0); 34 | } 35 | 36 | private static void assertBinData(BinData actual, String frequencyStart, double value) { 37 | assertEquals(actual.getFrequencyStart(), frequencyStart); 38 | assertEquals(actual.getDbmAverage(), value, 0.0); 39 | } 40 | 41 | @Before 42 | public void start() { 43 | parser = new BinDataParser(); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/test/java/ru/r2cloud/rtlspectrum/FrequencyFormatterTest.java: -------------------------------------------------------------------------------- 1 | package ru.r2cloud.rtlspectrum; 2 | 3 | import static org.junit.Assert.assertEquals; 4 | 5 | import org.junit.Test; 6 | 7 | public class FrequencyFormatterTest { 8 | 9 | @Test 10 | public void formatNegative() { 11 | FrequencyFormatter formatter = new FrequencyFormatter(); 12 | assertEquals("", formatter.toString(-1)); 13 | } 14 | 15 | @Test 16 | public void formatHz() { 17 | FrequencyFormatter formatter = new FrequencyFormatter(); 18 | assertEquals("10 Hz", formatter.toString(10)); 19 | } 20 | 21 | @Test 22 | public void formatKHz() { 23 | FrequencyFormatter formatter = new FrequencyFormatter(); 24 | assertEquals("10.1 KHz", formatter.toString(10100)); 25 | } 26 | 27 | @Test 28 | public void formatMHz() { 29 | FrequencyFormatter formatter = new FrequencyFormatter(); 30 | assertEquals("10 MHz", formatter.toString(10_001_000)); 31 | } 32 | 33 | @Test 34 | public void formatGHz() { 35 | FrequencyFormatter formatter = new FrequencyFormatter(); 36 | assertEquals("10.1 GHz", formatter.toString(10_101_000_000L)); 37 | } 38 | 39 | } 40 | -------------------------------------------------------------------------------- /src/test/java/ru/r2cloud/rtlspectrum/MacOsUtilTest.java: -------------------------------------------------------------------------------- 1 | package ru.r2cloud.rtlspectrum; 2 | 3 | import static org.junit.Assert.assertFalse; 4 | import static org.junit.Assert.assertTrue; 5 | 6 | import java.io.File; 7 | import java.io.FileNotFoundException; 8 | import java.io.FileOutputStream; 9 | import java.io.IOException; 10 | import java.io.InputStream; 11 | import java.util.UUID; 12 | 13 | import org.junit.Rule; 14 | import org.junit.Test; 15 | import org.junit.rules.TemporaryFolder; 16 | 17 | public class MacOsUtilTest { 18 | 19 | @Rule 20 | public TemporaryFolder tempFolder = new TemporaryFolder(); 21 | 22 | private File command; 23 | 24 | @Test 25 | public void dark() throws Exception { 26 | setupCommand("defaults_mock.sh"); 27 | assertTrue(MacOsUtil.isDark(command.getAbsolutePath())); 28 | } 29 | 30 | @Test 31 | public void white() { 32 | assertFalse(MacOsUtil.isDark("echo")); 33 | } 34 | 35 | @Test 36 | public void testInvalidCode() throws Exception { 37 | setupCommand("defaults_fail.sh"); 38 | assertFalse(MacOsUtil.isDark(command.getAbsolutePath())); 39 | } 40 | 41 | @Test 42 | public void testUnknownCommand() { 43 | assertFalse(MacOsUtil.isDark("expected-unknown-command-" + UUID.randomUUID().toString())); 44 | } 45 | 46 | @Test 47 | public void testEmptyResponse() throws Exception { 48 | setupCommand("defaults_empty.sh"); 49 | assertFalse(MacOsUtil.isDark(command.getAbsolutePath())); 50 | } 51 | 52 | private void setupCommand(String file) throws IOException, FileNotFoundException { 53 | command = new File(tempFolder.getRoot(), UUID.randomUUID().toString() + ".sh"); 54 | try (FileOutputStream fos = new FileOutputStream(command); InputStream is = UITest.class.getClassLoader().getResourceAsStream(file)) { 55 | UITest.copy(is, fos); 56 | } 57 | command.setExecutable(true); 58 | } 59 | 60 | } 61 | -------------------------------------------------------------------------------- /src/test/java/ru/r2cloud/rtlspectrum/PowerFormatterTest.java: -------------------------------------------------------------------------------- 1 | package ru.r2cloud.rtlspectrum; 2 | 3 | import static org.junit.Assert.assertEquals; 4 | 5 | import org.junit.Test; 6 | 7 | public class PowerFormatterTest { 8 | 9 | @Test 10 | public void formatNan() { 11 | assertEquals("", new PowerFormatter().toString(Double.NaN)); 12 | } 13 | 14 | @Test 15 | public void format() { 16 | PowerFormatter formatter = new PowerFormatter(); 17 | assertEquals("23.46", formatter.toString(23.4567)); 18 | } 19 | 20 | } 21 | -------------------------------------------------------------------------------- /src/test/java/ru/r2cloud/rtlspectrum/UITest.java: -------------------------------------------------------------------------------- 1 | package ru.r2cloud.rtlspectrum; 2 | 3 | import static org.junit.Assert.assertEquals; 4 | 5 | import java.io.File; 6 | import java.io.FileOutputStream; 7 | import java.io.IOException; 8 | import java.io.InputStream; 9 | import java.io.OutputStream; 10 | import java.util.ArrayList; 11 | import java.util.List; 12 | import java.util.UUID; 13 | 14 | import org.junit.Ignore; 15 | import org.junit.Rule; 16 | import org.junit.Test; 17 | import org.junit.rules.TemporaryFolder; 18 | import org.testfx.framework.junit.ApplicationTest; 19 | import org.testfx.robot.Motion; 20 | 21 | import javafx.collections.ObservableList; 22 | import javafx.fxml.FXMLLoader; 23 | import javafx.scene.Parent; 24 | import javafx.scene.Scene; 25 | import javafx.scene.chart.LineChart; 26 | import javafx.scene.chart.XYChart; 27 | import javafx.scene.chart.XYChart.Data; 28 | import javafx.scene.control.ProgressBar; 29 | import javafx.scene.input.KeyCode; 30 | import javafx.stage.Stage; 31 | 32 | @Ignore 33 | public class UITest extends ApplicationTest { 34 | 35 | private static final long TIMEOUT = 100; 36 | private static final int MAX_RETRIES = 60; 37 | 38 | @Rule 39 | public TemporaryFolder tempFolder = new TemporaryFolder(); 40 | 41 | @Test 42 | public void loadFile() throws Exception { 43 | File subtractData = new File(tempFolder.getRoot(), "2_" + UUID.randomUUID().toString() + ".csv"); 44 | try (FileOutputStream fos = new FileOutputStream(subtractData); InputStream is = UITest.class.getClassLoader().getResourceAsStream("subtract.csv")) { 45 | copy(is, fos); 46 | } 47 | File testData = new File(tempFolder.getRoot(), "1_" + UUID.randomUUID().toString() + ".csv"); 48 | try (FileOutputStream fos = new FileOutputStream(testData); InputStream is = UITest.class.getClassLoader().getResourceAsStream("test.csv")) { 49 | copy(is, fos); 50 | } 51 | clickOn("#loadFileButton"); 52 | push(KeyCode.DIGIT1, KeyCode.ENTER); 53 | waitForCompletion(testData.getCanonicalPath()); 54 | 55 | // verify load file 56 | List> file1Data = expectedDataFromFile1(); 57 | assertData(file1Data); 58 | 59 | clickOn("#editMenu"); 60 | clickOn("#subtractFileMenu", Motion.VERTICAL_FIRST); 61 | push(KeyCode.DIGIT2, KeyCode.ENTER); 62 | waitForCompletion(subtractData.getCanonicalPath()); 63 | 64 | // verify subtract file 65 | List> expected = new ArrayList<>(); 66 | expected.add(new Data(24000000L, -2.0)); 67 | expected.add(new Data(25000000L, -2.0)); 68 | expected.add(new Data(26000000L, 2.0)); 69 | assertData(expected); 70 | 71 | clickOn("#fileMenu"); 72 | clickOn("#addFileMenu", Motion.VERTICAL_FIRST); 73 | push(KeyCode.DIGIT1, KeyCode.ENTER); 74 | waitForCompletion(testData.getCanonicalPath()); 75 | // verify add file 76 | assertData(expected, file1Data); 77 | } 78 | 79 | private static List> expectedDataFromFile1() { 80 | List> expected = new ArrayList<>(); 81 | expected.add(new Data(24000000L, -24.14)); 82 | expected.add(new Data(25000000L, -24.15)); 83 | expected.add(new Data(26000000L, 14.07)); 84 | return expected; 85 | } 86 | 87 | @Override 88 | public void start(Stage stage) { 89 | System.setProperty("rtlSpectrum.defaultdirectory", tempFolder.getRoot().getAbsolutePath()); 90 | System.setProperty("testfx.running", "true"); 91 | Parent root; 92 | try (InputStream is = getClass().getClassLoader().getResourceAsStream("layout.fxml")) { 93 | FXMLLoader fxmlLoader = new FXMLLoader(getClass().getClassLoader().getResource("layout.fxml")); 94 | root = fxmlLoader.load(is); 95 | } catch (IOException e) { 96 | throw new IllegalStateException(e); 97 | } 98 | 99 | Scene scene = new Scene(root, 640, 480); 100 | stage.setScene(scene); 101 | stage.show(); 102 | } 103 | 104 | @SafeVarargs 105 | private final void assertData(List>... data) { 106 | LineChart chart = lookup("#lineChart").query(); 107 | assertEquals(data.length, chart.getData().size()); 108 | for (int j = 0; j < data.length; j++) { 109 | ObservableList> curSeries = chart.getData().get(j).getData(); 110 | List> curExpected = data[j]; 111 | assertEquals(curExpected.size(), curSeries.size()); 112 | for (int i = 0; i < curExpected.size(); i++) { 113 | Data expected = curExpected.get(i); 114 | Data actual = curSeries.get(i); 115 | assertEquals(expected.getXValue(), actual.getXValue()); 116 | assertEquals(expected.getYValue(), actual.getYValue()); 117 | } 118 | } 119 | } 120 | 121 | private void waitForCompletion(String expectedTaskId) { 122 | ProgressBar bar = lookup("#progressBar").query(); 123 | int curRetry = 0; 124 | while (curRetry < MAX_RETRIES && !Thread.currentThread().isInterrupted()) { 125 | String taskId = (String) bar.getProperties().get(StatusBar.LAST_COMPLETED_TASK); 126 | if (taskId != null && taskId.equals(expectedTaskId)) { 127 | return; 128 | } 129 | try { 130 | Thread.sleep(TIMEOUT); 131 | } catch (InterruptedException e) { 132 | Thread.currentThread().interrupt(); 133 | break; 134 | } 135 | curRetry++; 136 | } 137 | throw new RuntimeException("task did not complete in time: " + expectedTaskId); 138 | } 139 | 140 | public static void copy(InputStream input, OutputStream output) throws IOException { 141 | byte[] buffer = new byte[1024 * 4]; 142 | int n = 0; 143 | while (-1 != (n = input.read(buffer))) { 144 | output.write(buffer, 0, n); 145 | } 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /src/test/resources/defaults_empty.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | -------------------------------------------------------------------------------- /src/test/resources/defaults_fail.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | exit 1 -------------------------------------------------------------------------------- /src/test/resources/defaults_mock.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | echo "dark" -------------------------------------------------------------------------------- /src/test/resources/subtract.csv: -------------------------------------------------------------------------------- 1 | 2019-06-16,23:10:56,24000000,25000000,1000000.00,1,-22.14,-22.14 2 | 2019-06-16,23:10:56,25000000,26000000,1000000.00,1,-22.15,-22.15 3 | 2019-06-16,23:10:56,26000000,27000000,1000000.00,1,12.07,12.07 -------------------------------------------------------------------------------- /src/test/resources/test.csv: -------------------------------------------------------------------------------- 1 | 2019-06-16,23:10:56,24000000,25000000,1000000.00,1,-24.14,-24.14 2 | 2019-06-16,23:10:56,25000000,26000000,1000000.00,1,-24.15,-24.15 3 | 2019-06-16,23:10:56,26000000,27000000,1000000.00,1,14.07,14.07 --------------------------------------------------------------------------------