├── .gitignore ├── dump1090.rb ├── gr-baz.rb ├── libosmocore.rb ├── cmake.rb ├── rtlsdr.rb ├── bladerf.rb ├── gr-osmosdr.rb ├── gr-gsm.rb ├── librtlsdr.rb ├── gnuradio.rb ├── pyqwt.rb ├── pyqt.rb ├── qwt.rb ├── README.md ├── qt.rb └── heatmap.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.DS_Store 2 | -------------------------------------------------------------------------------- /dump1090.rb: -------------------------------------------------------------------------------- 1 | require 'formula' 2 | 3 | class Dump1090 < Formula 4 | homepage 'https://github.com/MalcolmRobb/dump1090' 5 | head 'https://github.com/MalcolmRobb/dump1090.git' 6 | 7 | depends_on 'pkg-config' => :build 8 | depends_on 'libusb' 9 | depends_on 'librtlsdr' 10 | 11 | def install 12 | system 'make', "PREFIX=#{prefix}", "SHAREDIR=#{prefix}/public_html" 13 | bin.install "dump1090", "view1090" 14 | prefix.install "public_html" 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /gr-baz.rb: -------------------------------------------------------------------------------- 1 | require 'formula' 2 | 3 | class GrBaz < Formula 4 | homepage 'http://wiki.spench.net/wiki/Gr-baz' 5 | head 'https://github.com/balint256/gr-baz.git' 6 | 7 | depends_on 'cmake' => :build 8 | depends_on 'gnuradio' 9 | 10 | def install 11 | mkdir 'build' do 12 | system 'cmake', '..', '-DPYTHON_LIBRARY=/usr/local/Frameworks/Python.framework/Versions/2.7/Python ', *std_cmake_args 13 | system 'make' 14 | system 'make install' 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /libosmocore.rb: -------------------------------------------------------------------------------- 1 | require "formula" 2 | 3 | class Libosmocore < Formula 4 | homepage "http://bb.osmocom.org/trac/wiki/libosmocore" 5 | head "https://github.com/nejohnson2/libosmocore.git" 6 | 7 | depends_on "libtool" => :build 8 | depends_on "automake" => :build 9 | depends_on "autoconf" => :build 10 | depends_on "pkg-config" => :build 11 | depends_on "pcsc-lite" 12 | 13 | def install 14 | system "autoreconf", "-i" 15 | system "./configure", "--prefix=#{prefix}" 16 | system "make" 17 | system "make", "install" 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /cmake.rb: -------------------------------------------------------------------------------- 1 | class Cmake < Formula 2 | url "https://cmake.org/files/v3.3/cmake-3.3.2.tar.gz" 3 | sha256 "e75a178d6ebf182b048ebfe6e0657c49f0dc109779170bad7ffcb17463f2fc22" 4 | 5 | 6 | def install 7 | args = %W[ 8 | --prefix=#{prefix} 9 | --no-system-libs 10 | --parallel=#{ENV.make_jobs} 11 | --datadir=/share/cmake 12 | --docdir=/share/doc/cmake 13 | --mandir=/share/man 14 | --system-zlib 15 | --system-bzip2 16 | --no-system-curl 17 | ] 18 | 19 | system "./bootstrap", *args 20 | system "make" 21 | system "make", "install" 22 | end 23 | end -------------------------------------------------------------------------------- /rtlsdr.rb: -------------------------------------------------------------------------------- 1 | require 'formula' 2 | 3 | class Rtlsdr < Formula 4 | homepage 'http://sdr.osmocom.org/trac/wiki/rtl-sdr' 5 | head 'git://git.osmocom.org/rtl-sdr.git' 6 | 7 | depends_on 'pkg-config' => :build 8 | depends_on 'automake' => :build 9 | depends_on 'libtool' => :build 10 | depends_on 'cmake' => :build 11 | depends_on 'libusb' 12 | 13 | if Float(MacOS::Xcode.version) >= 4.3 14 | depends_on 'autoconf' 15 | end 16 | 17 | def install 18 | args = ["--prefix=#{prefix}"] 19 | system "autoreconf -i" 20 | system "./configure", *args 21 | system "make install" 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /bladerf.rb: -------------------------------------------------------------------------------- 1 | require 'formula' 2 | 3 | class Bladerf < Formula 4 | homepage 'https://github.com/Nuand/bladeRF/wiki' 5 | head 'https://github.com/Nuand/bladeRF.git' 6 | 7 | depends_on 'pkg-config' => :build 8 | depends_on 'automake' => :build 9 | depends_on 'libtool' => :build 10 | depends_on 'cmake' => :build 11 | depends_on 'libusb' 12 | 13 | def install 14 | cd 'host/utilities/bladeRF-cli/src/cmd/doc' do 15 | system 'cp', 'cmd_help.h.in', 'cmd_help.h' 16 | end 17 | mkdir 'build' do 18 | system 'cmake', '..', *std_cmake_args 19 | system 'make' 20 | system 'make install' 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /gr-osmosdr.rb: -------------------------------------------------------------------------------- 1 | require 'formula' 2 | 3 | class GrOsmosdr < Formula 4 | homepage 'http://sdr.osmocom.org/trac/wiki/GrOsmoSDR' 5 | head 'git://git.osmocom.org/gr-osmosdr', :shallow => false 6 | 7 | depends_on 'cmake' => :build 8 | depends_on 'gnuradio' 9 | depends_on 'rtlsdr' 10 | 11 | def install 12 | mkdir 'build' do 13 | system 'cmake', '..', *std_cmake_args << "-DPYTHON_LIBRARY=#{python_path}/Frameworks/Python.framework/" 14 | system 'make' 15 | system 'make install' 16 | end 17 | end 18 | 19 | def python_path 20 | python = Formulary.factory('python') 21 | kegs = python.rack.children.reject { |p| p.basename.to_s == '.DS_Store' } 22 | kegs.find { |p| Keg.new(p).linked? } || kegs.last 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /gr-gsm.rb: -------------------------------------------------------------------------------- 1 | class GrGsm < Formula 2 | homepage "https://github.com/ptrkrysik/gr-gsm" 3 | head "https://github.com/ptrkrysik/gr-gsm.git" 4 | 5 | depends_on "cmake" => :build 6 | depends_on "doxygen" => :build 7 | depends_on "graphviz" => :build 8 | depends_on "swig" => :build 9 | depends_on "boost" 10 | depends_on "gnuradio" 11 | depends_on "libosmocore" 12 | 13 | def install 14 | mkdir "build" do 15 | ENV.append "LDFLAGS", "-Wl,-undefined,dynamic_lookup" 16 | # Point Python library to existing path or CMake test will fail. 17 | args = %W[ 18 | -DCMAKE_SHARED_LINKER_FLAGS='-Wl,-undefined,dynamic_lookup' 19 | -DPYTHON_LIBRARY='#{HOMEBREW_PREFIX}/lib/libgnuradio-runtime.dylib' 20 | ] + std_cmake_args 21 | 22 | ENV.deparallelize 23 | system "cmake", "..", *args 24 | system "make", "install" 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /librtlsdr.rb: -------------------------------------------------------------------------------- 1 | class Librtlsdr < Formula 2 | desc "Use Realtek DVT-T dongles as a cheap SDR" 3 | homepage "https://sdr.osmocom.org/trac/wiki/rtl-sdr" 4 | url "https://github.com/steve-m/librtlsdr/archive/v0.5.3.tar.gz" 5 | sha256 "98fb5c34ac94d6f2235a0bb41a08f8bed7949e1d1b91ea57a7c1110191ea58de" 6 | head "git://git.osmocom.org/rtl-sdr.git", :shallow => false 7 | 8 | bottle do 9 | cellar :any 10 | rebuild 1 11 | sha256 "bfeabfcc68c270b5dc4ef8829e466cf406c87e9068cc4a1985eebdc849e2c79c" => :sierra 12 | sha256 "63a2184d097f6da5f72eec471ed24f498efe3699834e45a25ba6b55c47b57df5" => :el_capitan 13 | sha256 "d9e6bf3b47b6600d9fb3251cdcb0c7d89dcb9d292609453808303944df2f8981" => :yosemite 14 | sha256 "3c7027468e4ae312373a62d166a2860be9e27711663fb5f0e52b6e3a3ddc5c6d" => :mavericks 15 | sha256 "1d6986e78140d3135492e087356435b19647f090d902b334b400315bc8baebd5" => :mountain_lion 16 | end 17 | 18 | option :universal 19 | 20 | depends_on "pkg-config" => :build 21 | depends_on "cmake" => :build 22 | depends_on "libusb" 23 | 24 | def install 25 | args = std_cmake_args 26 | 27 | if build.universal? 28 | ENV.universal_binary 29 | args << "-DCMAKE_OSX_ARCHITECTURES=#{Hardware::CPU.universal_archs.as_cmake_arch_flags}" 30 | end 31 | 32 | mkdir "build" do 33 | system "cmake", "..", *args 34 | system "make", "install" 35 | end 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /gnuradio.rb: -------------------------------------------------------------------------------- 1 | class Gnuradio < Formula 2 | desc "SDK providing the signal processing runtime and processing blocks" 3 | homepage "https://gnuradio.squarespace.com/" 4 | url "https://gnuradio.org/releases/gnuradio/gnuradio-3.7.9.1.tar.gz" 5 | sha256 "9c06f0f1ec14113203e0486fd526dd46ecef216dfe42f12d78d9b781b1ef967e" 6 | head 'git://gnuradio.org/gnuradio/gnuradio.git' 7 | 8 | depends_on 'cmake' => :build 9 | depends_on 'scipy' => 'python' 10 | depends_on 'boost' 11 | depends_on 'fftw' 12 | depends_on 'pygtk' 13 | depends_on 'swig' 14 | depends_on 'cppunit' 15 | depends_on 'pyqt' 16 | depends_on 'pyqwt' 17 | 18 | def options 19 | [ 20 | ['--with-qt', 'Build gr-qtgui.'], 21 | ] 22 | end 23 | 24 | def install 25 | ENV.prepend_create_path 'PYTHONPATH', libexec+'lib/python2.7/site-packages' 26 | install_args = [ "setup.py", "install", "--prefix=#{libexec}" ] 27 | 28 | mkdir 'build' do 29 | args = ["-DCMAKE_PREFIX_PATH=#{prefix}", "-DQWT_INCLUDE_DIRS=#{HOMEBREW_PREFIX}/lib/qwt.framework/Headers", "-DQWT_LIBRARIES=#{HOMEBREW_PREFIX}/lib/qwt.framework/qwt", ] + std_cmake_args 30 | args << '-DENABLE_GR_QTGUI=OFF' unless ARGV.include?('--with-qt') 31 | 32 | python_prefix = `python-config --prefix`.strip 33 | 34 | args << '-DENABLE_DOXYGEN=OFF' 35 | args << "-DPYTHON_LIBRARY='#{python_prefix}/Python'" 36 | args << "-DPYTHON_INCLUDE_DIR='#{python_prefix}/Headers'" 37 | args << "-DPYTHON_PACKAGES_PATH='#{lib}/#{which_python}/site-packages'" 38 | 39 | system 'cmake', '..', *args 40 | system 'make' 41 | system 'make install' 42 | end 43 | end 44 | 45 | def python_path 46 | python = Formula.factory('python') 47 | kegs = python.rack.children.reject { |p| p.basename.to_s == '.DS_Store' } 48 | kegs.find { |p| Keg.new(p).linked? } || kegs.last 49 | end 50 | 51 | def caveats 52 | <<-EOS.undent 53 | If you want to use custom blocks, create this file: 54 | ~/.gnuradio/config.conf 55 | [grc] 56 | local_blocks_path=/usr/local/share/gnuradio/grc/blocks 57 | EOS 58 | end 59 | 60 | def which_python 61 | "python" + `python -c 'import sys;print(sys.version[:3])'`.strip 62 | end 63 | end 64 | -------------------------------------------------------------------------------- /pyqwt.rb: -------------------------------------------------------------------------------- 1 | class Pyqwt < Formula 2 | desc "Python bindings for Qwt, widgets for science and engineering" 3 | homepage "http://pyqwt.sourceforge.net" 4 | url "https://downloads.sourceforge.net/project/pyqwt/pyqwt5/PyQwt-5.2.0/PyQwt-5.2.0.tar.gz" 5 | sha256 "98a8c7e0c76d07701c11dffb77793b05f071b664a8b520d6e97054a98179e70b" 6 | 7 | depends_on "python" 8 | depends_on "qt" 9 | depends_on "qwt" 10 | depends_on "sip" 11 | depends_on "nejohnson2/sdr/pyqt" 12 | 13 | # Patch to build system to allow for specific installation directories. 14 | patch :p0, :DATA 15 | 16 | def install 17 | cd "configure" do 18 | system "python", 19 | "configure.py", 20 | "--module-install-path=#{lib}/python2.7/site-packages/PyQt4/Qwt5", 21 | "--sip-install-path=#{share}/sip/Qwt5", 22 | "--uic-install-path=#{lib}/python2.7/site-packages/PyQt4", 23 | "-Q", "../qwt-5.2" 24 | system "make", "install" 25 | system "make", "clean" 26 | end 27 | end 28 | 29 | test do 30 | ENV["PYTHONPATH"] = lib+"python2.7/site-packages" 31 | system "python", "-c", "from PyQt4 import Qwt5 as Qwt" 32 | end 33 | end 34 | 35 | __END__ 36 | --- configure/configure.py 2011-10-24 19:14:41.000000000 -0500 37 | +++ configure/configure.py 2011-10-24 19:15:03.000000000 -0500 38 | @@ -846,14 +846,14 @@ 39 | pattern = os.path.join(os.pardir, 'sip', options.qwt, 'common', '*.sip') 40 | sip_files += [os.path.join(os.pardir, f) for f in glob.glob(pattern)] 41 | installs.append( 42 | - [sip_files, os.path.join(configuration.pyqt_sip_dir, 'Qwt5')]) 43 | + [sip_files, os.path.join(options.sip_install_path, 'Qwt5')]) 44 | 45 | # designer 46 | if configuration.qt_version > 0x03ffff: 47 | plugin_source_path = os.path.join( 48 | os.pardir, 'qt4lib', 'PyQt4', 'uic', 'widget-plugins') 49 | plugin_install_path = os.path.join( 50 | - configuration.pyqt_mod_dir, 'uic', 'widget-plugins') 51 | + options.uic_install_path, 'uic', 'widget-plugins') 52 | compileall.compile_dir(plugin_source_path, ddir=plugin_install_path) 53 | pattern = os.path.join(plugin_source_path, '*.py*') 54 | plugin_files = [os.path.join(os.pardir, f) for f in glob.glob(pattern)] 55 | @@ -1025,6 +1025,14 @@ 56 | '--module-install-path', default='', action='store', 57 | help= 'specify the install directory for the Python modules' 58 | ) 59 | + install_options.add_option( 60 | + '--sip-install-path', default='', action='store', 61 | + help= 'specify the install directory for the sip files [share/sip]' 62 | + ) 63 | + install_options.add_option( 64 | + '--uic-install-path', default='', action='store', 65 | + help= 'specify the install directory for the uic plugins [lib/python/PyQt4]' 66 | + ) 67 | parser.add_option_group(install_options) 68 | 69 | options, args = parser.parse_args() 70 | @@ -1084,6 +1092,10 @@ 71 | if not options.module_install_path: 72 | options.module_install_path = os.path.join( 73 | configuration.pyqt_mod_dir, 'Qwt5') 74 | + if not options.sip_install_path: 75 | + options.sip_install_path = configuration.pyqt_sip_dir 76 | + if not options.uic_install_path: 77 | + options.uic_install_path = configuration.pyqt_mod_dir 78 | 79 | print 80 | print 'Extended command line options:' 81 | -------------------------------------------------------------------------------- /pyqt.rb: -------------------------------------------------------------------------------- 1 | class Pyqt < Formula 2 | desc "Python bindings for Qt" 3 | homepage "https://www.riverbankcomputing.com/software/pyqt/intro" 4 | url "https://downloads.sf.net/project/pyqt/PyQt4/PyQt-4.11.4/PyQt-mac-gpl-4.11.4.tar.gz" 5 | sha256 "f178ba12a814191df8e9f87fb95c11084a0addc827604f1a18a82944225ed918" 6 | 7 | option "without-python", "Build without python 2 support" 8 | depends_on "python@2" => :optional 9 | 10 | if build.without?("python3") && build.without?("python") 11 | odie "pyqt: --with-python3 must be specified when using --without-python" 12 | end 13 | 14 | depends_on "nejohnson2/sdr/qt" 15 | 16 | if build.with? "python3" 17 | depends_on "sip" => "with-python3" 18 | else 19 | depends_on "sip" 20 | end 21 | 22 | def install 23 | # On Mavericks we want to target libc++, this requires a non default qt makespec 24 | if ENV.compiler == :clang && MacOS.version >= :mavericks 25 | ENV.append "QMAKESPEC", "unsupported/macx-clang-libc++" 26 | end 27 | 28 | Language::Python.each_python(build) do |python, version| 29 | ENV.append_path "PYTHONPATH", "#{Formula["sip"].opt_lib}/python#{version}/site-packages" 30 | 31 | args = %W[ 32 | --confirm-license 33 | --bindir=#{bin} 34 | --destdir=#{lib}/python#{version}/site-packages 35 | --sipdir=#{share}/sip 36 | ] 37 | 38 | # We need to run "configure.py" so that pyqtconfig.py is generated, which 39 | # is needed by QGIS, PyQWT (and many other PyQt interoperable 40 | # implementations such as the ROS GUI libs). This file is currently needed 41 | # for generating build files appropriate for the qmake spec that was used 42 | # to build Qt. The alternatives provided by configure-ng.py is not 43 | # sufficient to replace pyqtconfig.py yet (see 44 | # https://github.com/qgis/QGIS/pull/1508). Using configure.py is 45 | # deprecated and will be removed with SIP v5, so we do the actual compile 46 | # using the newer configure-ng.py as recommended. In order not to 47 | # interfere with the build using configure-ng.py, we run configure.py in a 48 | # temporary directory and only retain the pyqtconfig.py from that. 49 | 50 | require "tmpdir" 51 | dir = Dir.mktmpdir 52 | begin 53 | cp_r(Dir.glob("*"), dir) 54 | cd dir do 55 | system python, "configure.py", *args 56 | inreplace "pyqtconfig.py", Formula["cartr/qt4/qt"].prefix, Formula["cartr/qt4/qt"].opt_prefix 57 | (lib/"python#{version}/site-packages/PyQt4").install "pyqtconfig.py" 58 | end 59 | ensure 60 | remove_entry_secure dir 61 | end 62 | 63 | # On Mavericks we want to target libc++, this requires a non default qt makespec 64 | if ENV.compiler == :clang && MacOS.version >= :mavericks 65 | args << "--spec" << "unsupported/macx-clang-libc++" 66 | end 67 | 68 | system python, "configure-ng.py", *args 69 | system "make" 70 | system "make", "install" 71 | system "make", "clean" # for when building against multiple Pythons 72 | end 73 | end 74 | 75 | def caveats 76 | "Phonon support is broken." 77 | end 78 | 79 | test do 80 | Pathname("test.py").write <<-EOS.undent 81 | from PyQt4 import QtNetwork 82 | QtNetwork.QNetworkAccessManager().networkAccessible() 83 | EOS 84 | 85 | Language::Python.each_python(build) do |python, _version| 86 | system python, "test.py" 87 | end 88 | end 89 | 90 | bottle do 91 | root_url "https://dl.bintray.com/cartr/bottle-qt4" 92 | sha256 "9a24c78224b0b2c9d1ced22804dd3c01b82f9a35ce0a228aaa9db64c34376ef7" => :sierra 93 | end 94 | end 95 | -------------------------------------------------------------------------------- /qwt.rb: -------------------------------------------------------------------------------- 1 | class Qwt < Formula 2 | desc "Qt Widgets for Technical Applications (v5.1)" 3 | homepage "http://qwt.sourceforge.net/" 4 | url "https://downloads.sourceforge.net/project/qwt/qwt/6.1.3/qwt-6.1.3.tar.bz2" 5 | sha256 "f3ecd34e72a9a2b08422fb6c8e909ca76f4ce5fa77acad7a2883b701f4309733" 6 | 7 | bottle do 8 | cellar :any 9 | sha256 "e8c3fcd136e3da5ad5a9c9a08621585c30d610e7c9cebc1b39fb0d2da4582876" => :el_capitan 10 | sha256 "85b9c0169f8dc07eab6ce43d41882f55e96e6e990ef15c998e4d5b348828e89a" => :yosemite 11 | sha256 "0399fac9166e7a7a62cd07552253054b79c80ed8c182ad8d249b61d678ef3a95" => :mavericks 12 | end 13 | 14 | option "with-qwtmathml", "Build the qwtmathml library" 15 | option "without-plugin", "Skip building the Qt Designer plugin" 16 | 17 | depends_on "qt" 18 | 19 | # Update designer plugin linking back to qwt framework/lib after install 20 | # See: https://sourceforge.net/p/qwt/patches/45/ 21 | patch :DATA 22 | 23 | def install 24 | inreplace "qwtconfig.pri" do |s| 25 | s.gsub! /^\s*QWT_INSTALL_PREFIX\s*=(.*)$/, "QWT_INSTALL_PREFIX=#{prefix}" 26 | s.sub! /\+(=\s*QwtDesigner)/, "-\\1" if build.without? "plugin" 27 | 28 | # Install Qt plugin in `lib/qt4/plugins/designer`, not `plugins/designer`. 29 | s.sub! %r{(= \$\$\{QWT_INSTALL_PREFIX\})/(plugins/designer)$}, 30 | "\\1/lib/qt4/\\2" 31 | end 32 | 33 | args = ["-config", "release", "-spec"] 34 | # On Mavericks we want to target libc++, this requires a unsupported/macx-clang-libc++ flag 35 | if ENV.compiler == :clang && MacOS.version >= :mavericks 36 | args << "unsupported/macx-clang-libc++" 37 | else 38 | args << "macx-g++" 39 | end 40 | 41 | if build.with? "qwtmathml" 42 | args << "QWT_CONFIG+=QwtMathML" 43 | prefix.install "textengines/mathml/qtmmlwidget-license" 44 | end 45 | 46 | system "qmake", *args 47 | system "make" 48 | system "make", "install" 49 | end 50 | 51 | def caveats 52 | s = "" 53 | 54 | if build.with? "qwtmathml" 55 | s += <<-EOS.undent 56 | The qwtmathml library contains code of the MML Widget from the Qt solutions package. 57 | Beside the Qwt license you also have to take care of its license: 58 | #{opt_prefix}/qtmmlwidget-license 59 | EOS 60 | end 61 | 62 | s 63 | end 64 | 65 | test do 66 | (testpath/"test.cpp").write <<-EOS.undent 67 | #include 68 | int main() { 69 | QwtPlotCurve *curve1 = new QwtPlotCurve("Curve 1"); 70 | return (curve1 == NULL); 71 | } 72 | EOS 73 | system ENV.cxx, "test.cpp", "-o", "out", 74 | "-framework", "qwt", "-framework", "QtCore", 75 | "-F#{lib}", "-F#{Formula["qt"].opt_lib}", 76 | "-I#{lib}/qwt.framework/Headers", 77 | "-I#{Formula["qt"].opt_lib}/QtCore.framework/Headers", 78 | "-I#{Formula["qt"].opt_lib}/QtGui.framework/Headers" 79 | system "./out" 80 | end 81 | end 82 | 83 | __END__ 84 | diff --git a/designer/designer.pro b/designer/designer.pro 85 | index c269e9d..c2e07ae 100644 86 | --- a/designer/designer.pro 87 | +++ b/designer/designer.pro 88 | @@ -126,6 +126,16 @@ contains(QWT_CONFIG, QwtDesigner) { 89 | 90 | target.path = $${QWT_INSTALL_PLUGINS} 91 | INSTALLS += target 92 | + 93 | + macx { 94 | + contains(QWT_CONFIG, QwtFramework) { 95 | + QWT_LIB = qwt.framework/Versions/$${QWT_VER_MAJ}/qwt 96 | + } 97 | + else { 98 | + QWT_LIB = libqwt.$${QWT_VER_MAJ}.dylib 99 | + } 100 | + QMAKE_POST_LINK = install_name_tool -change $${QWT_LIB} $${QWT_INSTALL_LIBS}/$${QWT_LIB} $(DESTDIR)$(TARGET) 101 | + } 102 | } 103 | else { 104 | TEMPLATE = subdirs # do nothing 105 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # homebrew-sdr 2 | This repository is a collection of **Homebrew** formulas and directions to install various SDR software components. After much pain and suffering always trying to get thing installed properly I started using **Homebrew** which has really simplified the process. Certainly each install will be different, but I hope this provides a good foundation to get up and running with your SDR. 3 | 4 | Currently, there are formulas for: 5 | - Python 6 | - GNU Radio 3.7 7 | - rtlsdr 8 | - gr-osmosdr 9 | - GQRX 2.3.2 10 | - gr-gsm 11 | 12 | This installation works for macOS Sierra 10.12.1. 13 | 14 | ### Install XCode 15 | You'll need to install [Xcode](https://developer.apple.com/xcode/downloads/) and the ```Commandline Tools``` first. This installation was successful with Xcode 8. 16 | 17 | ```shell 18 | xcode-select --install 19 | softwareupdate --install --all 20 | ``` 21 | 22 | ### Install Homebrew 23 | 24 | ```shell 25 | ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)" 26 | ``` 27 | 28 | ### Install Python 29 | 30 | ```shell 31 | brew install python 32 | brew cask install xquartz 33 | 34 | brew tap homebrew/python 35 | brew install matplotlib 36 | brew install numpy 37 | brew install scipy 38 | 39 | # Dependencies 40 | pip install Cheetah 41 | pip install lxml 42 | pip install sphinx 43 | pip install Pillow # required for running the heatmap.py script 44 | ``` 45 | 46 | ### Install Dependencies 47 | 48 | ``` 49 | brew install gsl 50 | brew install pygtk 51 | brew install wxpython 52 | brew install boost 53 | brew install cppunit 54 | brew install fftw 55 | brew install zeromq 56 | brew install doxygen 57 | brew install uhd 58 | brew install portaudio 59 | brew install sdl 60 | brew install jack 61 | brew install swig 62 | brew install sip 63 | ``` 64 | 65 | ### Install GNU Radio 66 | 67 | Installing GNU Radio usually takes about 25 mins. The formula will make sure all dependencies are met. 68 | 69 | ```shell 70 | brew tap nejohnson2/homebrew-sdr 71 | brew install cmake # install cmake-3.3.2 since gnuradio doesnt like newer versions 72 | 73 | # version 3.7.9 74 | brew install gnuradio 75 | ``` 76 | 77 | ### Install SDR Tools 78 | 79 | I'm using the RTLSDR dongle. For other SDR devices, install the desired driver before installing ```gr-osmosdr``` and ```gqrx```. Check [this page](http://sdr.osmocom.org/trac/wiki/GrOsmoSDR) for more information. 80 | 81 | ```shell 82 | brew install librtlsdr 83 | brew install bladerf --HEAD 84 | brew install gr-osmosdr gr-baz --HEAD 85 | brew install gqrx 86 | ``` 87 | 88 | ### Install gr-gsm 89 | 90 | ``` 91 | brew install libosmocore --HEAD 92 | brew install gr-gsm --HEAD 93 | ``` 94 | 95 | ### SDR Testing 96 | 97 | #### GQRX 98 | Navigate to the Applications folder and launch GQRX. The application should launch and you should be able to see click the power button on the top left side of the interface to start. Then begin playing with all of the buttons. 99 | 100 | #### RTL_Power 101 | After installing the rtlsdr library, there are several python scripts that you can begin to use. The easiest one to use is the ```rtl_power``` located in ```/usr/local/bin```. [This blog](http://kmkeen.com/rtl-power/) give a good overview of specifics. Here is an example: 102 | 103 | ```shell 104 | rtl_power -f 90M:120M:8k -g 50 -i 2 -e 1h data.csv 105 | ``` 106 | 107 | The ```-f``` command specifies a range of frequencies to scan(lower:upper:bin size). ```-i``` sets the collection time in seconds/minutes/hour(s/m/h) format. ```-e``` sets the run time(though you can always use ```ctl-c``` to exit). And then give a file to save data to. 108 | 109 | After recording data, use the ```heatmap.py``` file to generate an high resolution image of the spectrum. It looks something like this: 110 | 111 | ```shell 112 | python heatmap.py input.csv output_file.png 113 | ``` 114 | 115 | ### Thanks 116 | 117 | Many thanks to [titanous](https://github.com/titanous/homebrew-gnuradio), [metacollin](https://github.com/metacollin/homebrew-gnuradio), [dholm](https://github.com/dholm/homebrew-sdr),[keenerd](https://github.com/keenerd/rtl-sdr-misc/tree/master/heatmap) and [chleggett](https://github.com/chleggett/homebrew-gqrx) from whom I compiled this code. 118 | 119 | More to come... 120 | -------------------------------------------------------------------------------- /qt.rb: -------------------------------------------------------------------------------- 1 | class Qt < Formula 2 | desc "Cross-platform application and UI framework" 3 | homepage "https://www.qt.io/" 4 | url "https://download.qt.io/official_releases/qt/4.8/4.8.7/qt-everywhere-opensource-src-4.8.7.tar.gz" 5 | mirror "https://www.mirrorservice.org/sites/download.qt-project.org/official_releases/qt/4.8/4.8.7/qt-everywhere-opensource-src-4.8.7.tar.gz" 6 | sha256 "e2882295097e47fe089f8ac741a95fef47e0a73a3f3cdf21b56990638f626ea0" 7 | revision 3 8 | 9 | head "https://code.qt.io/qt/qt.git", :branch => "4.8" 10 | 11 | # Backport of Qt5 commit to fix the fatal build error with Xcode 7, SDK 10.11. 12 | # https://code.qt.io/cgit/qt/qtbase.git/commit/?id=b06304e164ba47351fa292662c1e6383c081b5ca 13 | patch do 14 | url "https://raw.githubusercontent.com/Homebrew/formula-patches/480b7142c4e2ae07de6028f672695eb927a34875/qt/el-capitan.patch" 15 | sha256 "c8a0fa819c8012a7cb70e902abb7133fc05235881ce230235d93719c47650c4e" 16 | end 17 | 18 | # Backport of Qt5 patch to fix an issue with null bytes in QSetting strings. 19 | patch do 20 | url "https://raw.githubusercontent.com/cartr/homebrew-qt4/41669527a2aac6aeb8a5eeb58f440d3f3498910a/patches/qsetting-nulls.patch" 21 | sha256 "0deb4cd107853b1cc0800e48bb36b3d5682dc4a2a29eb34a6d032ac4ffe32ec3" 22 | end 23 | 24 | option "with-qt3support", "Build with deprecated Qt3Support module support" 25 | option "with-docs", "Build documentation" 26 | option "without-webkit", "Build without QtWebKit module" 27 | 28 | depends_on "openssl" 29 | depends_on "dbus" => :optional 30 | depends_on "mysql" => :optional 31 | depends_on "postgresql" => :optional 32 | 33 | deprecated_option "qtdbus" => "with-dbus" 34 | deprecated_option "with-d-bus" => "with-dbus" 35 | 36 | resource "test-project" do 37 | url "https://gist.github.com/tdsmith/f55e7e69ae174b5b5a03.git", 38 | :revision => "6f565390395a0259fa85fdd3a4f1968ebcd1cc7d" 39 | end 40 | 41 | def install 42 | args = %W[ 43 | -prefix #{prefix} 44 | -release 45 | -opensource 46 | -confirm-license 47 | -fast 48 | -system-zlib 49 | -qt-libtiff 50 | -qt-libpng 51 | -qt-libjpeg 52 | -nomake demos 53 | -nomake examples 54 | -cocoa 55 | ] 56 | 57 | if ENV.compiler == :clang 58 | args << "-platform" 59 | 60 | if MacOS.version >= :mavericks 61 | args << "unsupported/macx-clang-libc++" 62 | else 63 | args << "unsupported/macx-clang" 64 | end 65 | end 66 | 67 | # Phonon is broken on macOS 10.12+ and Xcode 8+ due to QTKit.framework 68 | # being removed. 69 | args << "-no-phonon" if MacOS.version >= :sierra || MacOS::Xcode.version >= "8.0" 70 | 71 | args << "-openssl-linked" 72 | args << "-I" << Formula["openssl"].opt_include 73 | args << "-L" << Formula["openssl"].opt_lib 74 | 75 | args << "-plugin-sql-mysql" if build.with? "mysql" 76 | args << "-plugin-sql-psql" if build.with? "postgresql" 77 | 78 | if build.with? "dbus" 79 | dbus_opt = Formula["dbus"].opt_prefix 80 | args << "-I#{dbus_opt}/lib/dbus-1.0/include" 81 | args << "-I#{dbus_opt}/include/dbus-1.0" 82 | args << "-L#{dbus_opt}/lib" 83 | args << "-ldbus-1" 84 | args << "-dbus-linked" 85 | end 86 | 87 | if build.with? "qt3support" 88 | args << "-qt3support" 89 | else 90 | args << "-no-qt3support" 91 | end 92 | 93 | args << "-nomake" << "docs" if build.without? "docs" 94 | 95 | if MacOS.prefer_64_bit? 96 | args << "-arch" << "x86_64" 97 | else 98 | args << "-arch" << "x86" 99 | end 100 | 101 | args << "-no-webkit" if build.without? "webkit" 102 | 103 | system "./configure", *args 104 | system "make" 105 | ENV.j1 106 | system "make", "install" 107 | 108 | # what are these anyway? 109 | (bin+"pixeltool.app").rmtree 110 | (bin+"qhelpconverter.app").rmtree 111 | # remove porting file for non-humans 112 | (prefix+"q3porting.xml").unlink if build.without? "qt3support" 113 | 114 | # Some config scripts will only find Qt in a "Frameworks" folder 115 | frameworks.install_symlink Dir["#{lib}/*.framework"] 116 | 117 | # The pkg-config files installed suggest that headers can be found in the 118 | # `include` directory. Make this so by creating symlinks from `include` to 119 | # the Frameworks' Headers folders. 120 | Pathname.glob("#{lib}/*.framework/Headers") do |path| 121 | include.install_symlink path => path.parent.basename(".framework") 122 | end 123 | 124 | # Make `HOMEBREW_PREFIX/lib/qt4/plugins` an additional plug-in search path 125 | # for Qt Designer to support formulae that provide Qt Designer plug-ins. 126 | system "/usr/libexec/PlistBuddy", 127 | "-c", "Add :LSEnvironment:QT_PLUGIN_PATH string \"#{HOMEBREW_PREFIX}/lib/qt4/plugins\"", 128 | "#{bin}/Designer.app/Contents/Info.plist" 129 | 130 | Pathname.glob("#{bin}/*.app") { |app| mv app, prefix } 131 | end 132 | 133 | def caveats; <<-EOS.undent 134 | We agreed to the Qt opensource license for you. 135 | If this is unacceptable you should uninstall. 136 | 137 | Qt Designer no longer picks up changes to the QT_PLUGIN_PATH environment 138 | variable as it was tweaked to search for plug-ins provided by formulae in 139 | #{HOMEBREW_PREFIX}/lib/qt4/plugins 140 | 141 | Phonon is not supported on macOS Sierra or with Xcode 8. 142 | EOS 143 | end 144 | 145 | test do 146 | Encoding.default_external = "UTF-8" unless RUBY_VERSION.start_with? "1." 147 | resource("test-project").stage testpath 148 | system bin/"qmake" 149 | system "make" 150 | assert_match(/GitHub/, pipe_output(testpath/"qtnetwork-test 2>&1", nil, 0)) 151 | end 152 | 153 | bottle do 154 | root_url "https://dl.bintray.com/cartr/bottle-qt4" 155 | sha256 "5d7fcd5f7925ed4be7724aa2d1b8e14eef6e9cf786f362138e501c845ed0034f" => :sierra 156 | end 157 | end -------------------------------------------------------------------------------- /heatmap.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python 2 | 3 | from PIL import Image, ImageDraw, ImageFont 4 | import os, sys, gzip, math, argparse, colorsys, datetime 5 | from collections import defaultdict 6 | from itertools import * 7 | 8 | urlretrieve = lambda a, b: None 9 | try: 10 | import urllib.request 11 | urlretrieve = urllib.request.urlretrieve 12 | except: 13 | import urllib 14 | urlretrieve = urllib.urlretrieve 15 | 16 | # todo: 17 | # matplotlib powered --interactive 18 | # arbitrary freq marker spacing 19 | # ppm 20 | # blue-less marker grid 21 | # fast summary thing 22 | # gain normalization 23 | 24 | vera_url = "https://github.com/keenerd/rtl-sdr-misc/raw/master/heatmap/Vera.ttf" 25 | vera_path = os.path.join(sys.path[0], "Vera.ttf") 26 | 27 | parser = argparse.ArgumentParser(description='Convert rtl_power CSV files into graphics.') 28 | parser.add_argument('input_path', metavar='INPUT', type=str, 29 | help='Input CSV file. (may be a .csv.gz)') 30 | parser.add_argument('output_path', metavar='OUTPUT', type=str, 31 | help='Output image. (various extensions supported)') 32 | parser.add_argument('--offset', dest='offset_freq', default=None, 33 | help='Shift the entire frequency range, for up/down converters.') 34 | parser.add_argument('--ytick', dest='time_tick', default=None, 35 | help='Place ticks along the Y axis every N seconds/minutes/hours/days.') 36 | parser.add_argument('--db', dest='db_limit', nargs=2, default=None, 37 | help='Minimum and maximum db values.') 38 | parser.add_argument('--compress', dest='compress', default=0, 39 | help='Apply a gradual asymptotic time compression. Values > 1 are the new target height, values < 1 are a scaling factor.') 40 | slicegroup = parser.add_argument_group('Slicing', 41 | 'Efficiently render a portion of the data. (optional) Frequencies can take G/M/k suffixes. Timestamps look like "YYYY-MM-DD HH:MM:SS" Durations take d/h/m/s suffixes.') 42 | slicegroup.add_argument('--low', dest='low_freq', default=None, 43 | help='Minimum frequency for a subrange.') 44 | slicegroup.add_argument('--high', dest='high_freq', default=None, 45 | help='Maximum frequency for a subrange.') 46 | slicegroup.add_argument('--begin', dest='begin_time', default=None, 47 | help='Timestamp to start at.') 48 | slicegroup.add_argument('--end', dest='end_time', default=None, 49 | help='Timestamp to stop at.') 50 | slicegroup.add_argument('--head', dest='head_time', default=None, 51 | help='Duration to use, starting at the beginning.') 52 | slicegroup.add_argument('--tail', dest='tail_time', default=None, 53 | help='Duration to use, stopping at the end.') 54 | 55 | # hack, http://stackoverflow.com/questions/9025204/ 56 | for i, arg in enumerate(sys.argv): 57 | if (arg[0] == '-') and arg[1].isdigit(): 58 | sys.argv[i] = ' ' + arg 59 | args = parser.parse_args() 60 | 61 | if not os.path.isfile(vera_path): 62 | urlretrieve(vera_url, vera_path) 63 | 64 | try: 65 | font = ImageFont.truetype(vera_path, 10) 66 | except: 67 | print('Please download the Vera.ttf font and place it in the current directory.') 68 | sys.exit(1) 69 | 70 | 71 | def frange(start, stop, step): 72 | i = 0 73 | while (i*step + start <= stop): 74 | yield i*step + start 75 | i += 1 76 | 77 | def min_filter(row): 78 | size = 3 79 | result = [] 80 | for i in range(size): 81 | here = row[i] 82 | near = row[0:i] + row[i+1:size] 83 | if here > min(near): 84 | result.append(here) 85 | continue 86 | result.append(min(near)) 87 | for i in range(size-1, len(row)): 88 | here = row[i] 89 | near = row[i-(size-1):i] 90 | if here > min(near): 91 | result.append(here) 92 | continue 93 | result.append(min(near)) 94 | return result 95 | 96 | def floatify(zs): 97 | # nix errors with -inf, windows errors with -1.#J 98 | zs2 = [] 99 | previous = 0 # awkward for single-column rows 100 | for z in zs: 101 | try: 102 | z = float(z) 103 | except ValueError: 104 | z = previous 105 | if math.isinf(z): 106 | z = previous 107 | if math.isnan(z): 108 | z = previous 109 | zs2.append(z) 110 | previous = z 111 | return zs2 112 | 113 | def freq_parse(s): 114 | suffix = 1 115 | if s.lower().endswith('k'): 116 | suffix = 1e3 117 | if s.lower().endswith('m'): 118 | suffix = 1e6 119 | if s.lower().endswith('g'): 120 | suffix = 1e9 121 | if suffix != 1: 122 | s = s[:-1] 123 | return float(s) * suffix 124 | 125 | def duration_parse(s): 126 | suffix = 1 127 | if s.lower().endswith('s'): 128 | suffix = 1 129 | if s.lower().endswith('m'): 130 | suffix = 60 131 | if s.lower().endswith('h'): 132 | suffix = 60 * 60 133 | if s.lower().endswith('d'): 134 | suffix = 24 * 60 * 60 135 | if suffix != 1 or s.lower().endswith('s'): 136 | s = s[:-1] 137 | return float(s) * suffix 138 | 139 | def date_parse(s): 140 | if '-' not in s: 141 | return datetime.datetime.fromtimestamp(int(s)) 142 | return datetime.datetime.strptime(s, '%Y-%m-%d %H:%M:%S') 143 | 144 | def gzip_wrap(path): 145 | "hides silly CRC errors" 146 | iterator = gzip.open(path, 'rb') 147 | running = True 148 | while running: 149 | try: 150 | line = next(iterator) 151 | if type(line) == bytes: 152 | line = line.decode('utf-8') 153 | yield line 154 | except IOError: 155 | running = False 156 | 157 | def reparse(label, fn): 158 | if args.__getattribute__(label) is None: 159 | return 160 | args.__setattr__(label, fn(args.__getattribute__(label))) 161 | 162 | path = args.input_path 163 | output = args.output_path 164 | 165 | raw_data = lambda: open(path) 166 | if path.endswith('.gz'): 167 | raw_data = lambda: gzip_wrap(path) 168 | 169 | reparse('low_freq', freq_parse) 170 | reparse('high_freq', freq_parse) 171 | reparse('offset_freq', freq_parse) 172 | if args.offset_freq is None: 173 | args.offset_freq = 0 174 | reparse('time_tick', duration_parse) 175 | reparse('begin_time', date_parse) 176 | reparse('end_time', date_parse) 177 | reparse('head_time', duration_parse) 178 | reparse('tail_time', duration_parse) 179 | reparse('head_time', lambda s: datetime.timedelta(seconds=s)) 180 | reparse('tail_time', lambda s: datetime.timedelta(seconds=s)) 181 | args.compress = float(args.compress) 182 | 183 | if args.begin_time and args.tail_time: 184 | print("Can't combine --begin and --tail") 185 | sys.exit(2) 186 | if args.end_time and args.head_time: 187 | print("Can't combine --end and --head") 188 | sys.exit(2) 189 | if args.head_time and args.tail_time: 190 | print("Can't combine --head and --tail") 191 | sys.exit(2) 192 | 193 | print("loading") 194 | 195 | def slice_columns(columns, low_freq, high_freq): 196 | start_col = 0 197 | stop_col = len(columns) 198 | if args.low_freq is not None and low <= args.low_freq <= high: 199 | start_col = sum(f args.end_time: 235 | break 236 | times.add(t) 237 | columns = list(frange(low, high, step)) 238 | start_col, stop_col = slice_columns(columns, args.low_freq, args.high_freq) 239 | f_key = (columns[start_col], columns[stop_col], step) 240 | zs = line[6+start_col:6+stop_col+1] 241 | if not zs: 242 | continue 243 | if f_key not in f_cache: 244 | freq2 = list(frange(*f_key))[:len(zs)] 245 | freqs.update(freq2) 246 | #freqs.add(f_key[1]) # high 247 | #labels.add(f_key[0]) # low 248 | f_cache.add(f_key) 249 | 250 | if not args.db_limit: 251 | zs = floatify(zs) 252 | min_z = min(min_z, min(zs)) 253 | max_z = max(max_z, max(zs)) 254 | 255 | if start is None: 256 | start = date_parse(t) 257 | stop = date_parse(t) 258 | if args.head_time is not None and args.end_time is None: 259 | args.end_time = start + args.head_time 260 | 261 | if args.tail_time is not None: 262 | times = [t for t in times if date_parse(t) >= (stop - args.tail_time)] 263 | start = date_parse(min(times)) 264 | 265 | freqs = list(sorted(list(freqs))) 266 | times = list(sorted(list(times))) 267 | labels = list(sorted(list(labels))) 268 | 269 | if len(labels) == 1: 270 | delta = (max(freqs) - min(freqs)) / (len(freqs) / 500.0) 271 | delta = round(delta / 10**int(math.log10(delta))) * 10**int(math.log10(delta)) 272 | delta = int(delta) 273 | lower = int(math.ceil(min(freqs) / delta) * delta) 274 | labels = list(range(lower, int(max(freqs)), delta)) 275 | 276 | def compression(y, decay): 277 | return int(round((1/decay)*math.exp(y*decay) - 1/decay)) 278 | 279 | height = len(times) 280 | height2 = height 281 | if args.compress: 282 | if args.compress > height: 283 | args.compress = 0 284 | print("Image too short, disabling compression") 285 | if 0 < args.compress < 1: 286 | args.compress *= height 287 | if args.compress: 288 | args.compress = -1 / args.compress 289 | height2 = compression(height, args.compress) 290 | 291 | print("x: %i, y: %i, z: (%f, %f)" % (len(freqs), height2, min_z, max_z)) 292 | 293 | def rgb2(z): 294 | g = (z - min_z) / (max_z - min_z) 295 | return (int(g*255), int(g*255), 50) 296 | 297 | def rgb3(z): 298 | g = (z - min_z) / (max_z - min_z) 299 | c = colorsys.hsv_to_rgb(0.65-(g-0.08), 1, 0.2+g) 300 | return (int(c[0]*256),int(c[1]*256),int(c[2]*256)) 301 | 302 | def collate_row(x_size): 303 | # this is more fragile than the old code 304 | # sensitive to timestamps that are out of order 305 | old_t = None 306 | row = [0.0] * x_size 307 | for line in raw_data(): 308 | line = [s.strip() for s in line.strip().split(',')] 309 | #line = [line[0], line[1]] + [float(s) for s in line[2:] if s] 310 | line = [s for s in line if s] 311 | t = line[0] + ' ' + line[1] 312 | if '-' not in line[0]: 313 | t = line[0] 314 | if t not in times: 315 | continue # happens with live files and time cropping 316 | if old_t is None: 317 | old_t = t 318 | low = int(line[2]) + args.offset_freq 319 | high = int(line[3]) + args.offset_freq 320 | step = float(line[4]) 321 | columns = list(frange(low, high, step)) 322 | start_col, stop_col = slice_columns(columns, args.low_freq, args.high_freq) 323 | if args.low_freq and columns[stop_col] < args.low_freq: 324 | continue 325 | if args.high_freq and columns[start_col] > args.high_freq: 326 | continue 327 | start_freq = columns[start_col] 328 | if args.low_freq: 329 | start_freq = max(args.low_freq, start_freq) 330 | # sometimes fails? skip or abort? 331 | x_start = freqs.index(start_freq) 332 | zs = floatify(line[6+start_col:6+stop_col+1]) 333 | if t != old_t: 334 | yield old_t, row 335 | row = [0.0] * x_size 336 | old_t = t 337 | for i in range(len(zs)): 338 | x = x_start + i 339 | if x >= x_size: 340 | continue 341 | row[x] = zs[i] 342 | yield old_t, row 343 | 344 | print("drawing") 345 | tape_height = 25 346 | img = Image.new("RGB", (len(freqs), tape_height + height2)) 347 | pix = img.load() 348 | x_size = img.size[0] 349 | average = [0.0] * len(freqs) 350 | tally = 0 351 | old_y = None 352 | for t, zs in collate_row(x_size): 353 | y = times.index(t) 354 | if not args.compress: 355 | for x in range(len(zs)): 356 | pix[x,y+tape_height] = rgb2(zs[x]) 357 | continue 358 | # ugh 359 | y = height2 - compression(height - y, args.compress) 360 | if old_y is None: 361 | old_y = y 362 | if old_y != y: 363 | for x in range(len(average)): 364 | pix[x,old_y+tape_height] = rgb2(average[x]/tally) 365 | tally = 0 366 | average = [0.0] * len(freqs) 367 | old_y = y 368 | for x in range(len(zs)): 369 | average[x] += zs[x] 370 | tally += 1 371 | 372 | 373 | def closest_index(n, m_list, interpolate=False): 374 | "assumes sorted m_list, returns two points for interpolate" 375 | i = len(m_list) // 2 376 | jump = len(m_list) // 2 377 | while jump > 1: 378 | i_down = i - jump 379 | i_here = i 380 | i_up = i + jump 381 | if i_down < 0: 382 | i_down = i 383 | if i_up >= len(m_list): 384 | i_up = i 385 | e_down = abs(m_list[i_down] - n) 386 | e_here = abs(m_list[i_here] - n) 387 | e_up = abs(m_list[i_up] - n) 388 | e_best = min([e_down, e_here, e_up]) 389 | if e_down == e_best: 390 | i = i_down 391 | if e_up == e_best: 392 | i = i_up 393 | if e_here == e_best: 394 | i = i_here 395 | jump = jump // 2 396 | if not interpolate: 397 | return i 398 | if n < m_list[i] and i > 0: 399 | return i-1, i 400 | if n > m_list[i] and i < len(m_list)-1: 401 | return i, i+1 402 | return i, i 403 | 404 | def word_aa(label, pt, fg_color, bg_color): 405 | f = ImageFont.truetype(vera_path, pt*3) 406 | s = f.getsize(label) 407 | s = (s[0], pt*3 + 3) # getsize lies, manually compute 408 | w_img = Image.new("RGB", s, bg_color) 409 | w_draw = ImageDraw.Draw(w_img) 410 | w_draw.text((0, 0), label, font=f, fill=fg_color) 411 | return w_img.resize((s[0]//3, s[1]//3), Image.ANTIALIAS) 412 | 413 | def blend(percent, c1, c2): 414 | "c1 and c2 are RGB tuples" 415 | # probably isn't gamma correct 416 | r = c1[0] * percent + c2[0] * (1 - percent) 417 | g = c1[1] * percent + c2[1] * (1 - percent) 418 | b = c1[2] * percent + c2[2] * (1 - percent) 419 | c3 = map(int, map(round, [r,g,b])) 420 | return tuple(c3) 421 | 422 | def tape_lines(interval, y1, y2, used=set()): 423 | "returns the number of lines" 424 | low_f = (min(freqs) // interval) * interval 425 | high_f = (1 + max(freqs) // interval) * interval 426 | hits = 0 427 | blur = lambda p: blend(p, (255, 255, 0), (0, 0, 0)) 428 | for i in range(int(low_f), int(high_f), int(interval)): 429 | if not (min(freqs) < i < max(freqs)): 430 | continue 431 | hits += 1 432 | if i in used: 433 | continue 434 | x1,x2 = closest_index(i, freqs, interpolate=True) 435 | if x1 == x2: 436 | draw.line([x1,y1,x1,y2], fill='black') 437 | else: 438 | percent = (i - freqs[x1]) / float(freqs[x2] - freqs[x1]) 439 | draw.line([x1,y1,x1,y2], fill=blur(percent)) 440 | draw.line([x2,y1,x2,y2], fill=blur(1-percent)) 441 | used.add(i) 442 | return hits 443 | 444 | def tape_text(interval, y, used=set()): 445 | low_f = (min(freqs) // interval) * interval 446 | high_f = (1 + max(freqs) // interval) * interval 447 | for i in range(int(low_f), int(high_f), int(interval)): 448 | if i in used: 449 | continue 450 | if not (min(freqs) < i < max(freqs)): 451 | continue 452 | x = closest_index(i, freqs) 453 | s = str(i) 454 | if interval >= 1e6: 455 | s = '%iM' % (i/1e6) 456 | elif interval > 1000: 457 | s = '%ik' % ((i/1e3) % 1000) 458 | if s.startswith('0'): 459 | s = '%iM' % (i/1e6) 460 | else: 461 | s = '%i' % (i%1000) 462 | if s.startswith('0'): 463 | s = '%ik' % ((i/1e3) % 1000) 464 | if s.startswith('0'): 465 | s = '%iM' % (i/1e6) 466 | w = word_aa(s, tape_pt, 'black', 'yellow') 467 | img.paste(w, (x - w.size[0]//2, y)) 468 | used.add(i) 469 | 470 | def shadow_text(x, y, s, font, fg_color='white', bg_color='black'): 471 | draw.text((x+1, y+1), s, font=font, fill=bg_color) 472 | draw.text((x, y), s, font=font, fill=fg_color) 473 | 474 | print("labeling") 475 | tape_pt = 10 476 | draw = ImageDraw.Draw(img) 477 | font = ImageFont.load_default() 478 | pixel_width = step 479 | 480 | draw.rectangle([0,0,img.size[0],tape_height], fill='yellow') 481 | min_freq = min(freqs) 482 | max_freq = max(freqs) 483 | delta = max_freq - min_freq 484 | width = len(freqs) 485 | label_base = 9 486 | 487 | for i in range(label_base, 0, -1): 488 | interval = int(10**i) 489 | low_f = (min_freq // interval) * interval 490 | high_f = (1 + max_freq // interval) * interval 491 | hits = len(range(int(low_f), int(high_f), interval)) 492 | if hits >= 4: 493 | label_base = i 494 | break 495 | label_base = 10**label_base 496 | 497 | for scale,y in [(1,10), (5,15), (10,19), (50,22), (100,24), (500, 25)]: 498 | hits = tape_lines(label_base/scale, y, tape_height) 499 | pixels_per_hit = width / hits 500 | if pixels_per_hit > 50: 501 | tape_text(label_base/scale, y-tape_pt) 502 | if pixels_per_hit < 10: 503 | break 504 | 505 | if args.time_tick: 506 | label_format = "%H:%M:%S" 507 | if args.time_tick % (60*60*24) == 0: 508 | label_format = "%Y-%m-%d" 509 | elif args.time_tick % 60 == 0: 510 | label_format = "%H:%M" 511 | label_next = datetime.datetime(start.year, start.month, start.day, start.hour) 512 | tick_delta = datetime.timedelta(seconds = args.time_tick) 513 | while label_next < start: 514 | label_next += tick_delta 515 | last_y = -100 516 | for y,t in enumerate(times): 517 | label_time = date_parse(t) 518 | if label_time < label_next: 519 | continue 520 | if args.compress: 521 | y = height2 - compression(height - y, args.compress) 522 | if y - last_y > 15: 523 | shadow_text(2, y+tape_height, label_next.strftime(label_format), font) 524 | last_y = y 525 | label_next += tick_delta 526 | 527 | 528 | duration = stop - start 529 | duration = duration.days * 24*60*60 + duration.seconds + 30 530 | pixel_height = duration / len(times) 531 | hours = int(duration / 3600) 532 | minutes = int((duration - 3600*hours) / 60) 533 | margin = 2 534 | if args.time_tick: 535 | margin = 60 536 | shadow_text(margin, img.size[1] - 45, 'Duration: %i:%02i' % (hours, minutes), font) 537 | shadow_text(margin, img.size[1] - 35, 'Range: %.2fMHz - %.2fMHz' % (min(freqs)/1e6, (max(freqs)+pixel_width)/1e6), font) 538 | shadow_text(margin, img.size[1] - 25, 'Pixel: %.2fHz x %is' % (pixel_width, int(round(pixel_height))), font) 539 | shadow_text(margin, img.size[1] - 15, 'Started: {0}'.format(start), font) 540 | # bin size 541 | 542 | print("saving") 543 | img.save(output) 544 | --------------------------------------------------------------------------------