first commit
This commit is contained in:
commit
9a7bf649f3
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
/target
|
||||||
|
*.csv
|
||||||
|
*.png
|
||||||
446
Cargo.lock
generated
Normal file
446
Cargo.lock
generated
Normal file
@ -0,0 +1,446 @@
|
|||||||
|
# This file is automatically @generated by Cargo.
|
||||||
|
# It is not intended for manual editing.
|
||||||
|
version = 3
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "android-tzdata"
|
||||||
|
version = "0.1.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "android_system_properties"
|
||||||
|
version = "0.1.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "anstream"
|
||||||
|
version = "0.6.18"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b"
|
||||||
|
dependencies = [
|
||||||
|
"anstyle",
|
||||||
|
"anstyle-parse",
|
||||||
|
"anstyle-query",
|
||||||
|
"anstyle-wincon",
|
||||||
|
"colorchoice",
|
||||||
|
"is_terminal_polyfill",
|
||||||
|
"utf8parse",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "anstyle"
|
||||||
|
version = "1.0.10"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "anstyle-parse"
|
||||||
|
version = "0.2.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9"
|
||||||
|
dependencies = [
|
||||||
|
"utf8parse",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "anstyle-query"
|
||||||
|
version = "1.1.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c"
|
||||||
|
dependencies = [
|
||||||
|
"windows-sys",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "anstyle-wincon"
|
||||||
|
version = "3.0.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ca3534e77181a9cc07539ad51f2141fe32f6c3ffd4df76db8ad92346b003ae4e"
|
||||||
|
dependencies = [
|
||||||
|
"anstyle",
|
||||||
|
"once_cell",
|
||||||
|
"windows-sys",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "autocfg"
|
||||||
|
version = "1.4.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "bumpalo"
|
||||||
|
version = "3.17.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cc"
|
||||||
|
version = "1.2.16"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "be714c154be609ec7f5dad223a33bf1482fff90472de28f7362806e6d4832b8c"
|
||||||
|
dependencies = [
|
||||||
|
"shlex",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cfg-if"
|
||||||
|
version = "1.0.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "chrono"
|
||||||
|
version = "0.4.40"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1a7964611d71df112cb1730f2ee67324fcf4d0fc6606acbbe9bfe06df124637c"
|
||||||
|
dependencies = [
|
||||||
|
"android-tzdata",
|
||||||
|
"iana-time-zone",
|
||||||
|
"js-sys",
|
||||||
|
"num-traits",
|
||||||
|
"wasm-bindgen",
|
||||||
|
"windows-link",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "clap"
|
||||||
|
version = "4.5.31"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "027bb0d98429ae334a8698531da7077bdf906419543a35a55c2cb1b66437d767"
|
||||||
|
dependencies = [
|
||||||
|
"clap_builder",
|
||||||
|
"clap_derive",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "clap_builder"
|
||||||
|
version = "4.5.31"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5589e0cba072e0f3d23791efac0fd8627b49c829c196a492e88168e6a669d863"
|
||||||
|
dependencies = [
|
||||||
|
"anstream",
|
||||||
|
"anstyle",
|
||||||
|
"clap_lex",
|
||||||
|
"strsim",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "clap_derive"
|
||||||
|
version = "4.5.28"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "bf4ced95c6f4a675af3da73304b9ac4ed991640c36374e4b46795c49e17cf1ed"
|
||||||
|
dependencies = [
|
||||||
|
"heck",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "clap_lex"
|
||||||
|
version = "0.7.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "colorchoice"
|
||||||
|
version = "1.0.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "core-foundation-sys"
|
||||||
|
version = "0.8.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "heck"
|
||||||
|
version = "0.5.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "iana-time-zone"
|
||||||
|
version = "0.1.61"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "235e081f3925a06703c2d0117ea8b91f042756fd6e7a6e5d901e8ca1a996b220"
|
||||||
|
dependencies = [
|
||||||
|
"android_system_properties",
|
||||||
|
"core-foundation-sys",
|
||||||
|
"iana-time-zone-haiku",
|
||||||
|
"js-sys",
|
||||||
|
"wasm-bindgen",
|
||||||
|
"windows-core",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "iana-time-zone-haiku"
|
||||||
|
version = "0.1.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f"
|
||||||
|
dependencies = [
|
||||||
|
"cc",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "is_terminal_polyfill"
|
||||||
|
version = "1.70.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "js-sys"
|
||||||
|
version = "0.3.77"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f"
|
||||||
|
dependencies = [
|
||||||
|
"once_cell",
|
||||||
|
"wasm-bindgen",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "libc"
|
||||||
|
version = "0.2.170"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "875b3680cb2f8f71bdcf9a30f38d48282f5d3c95cbf9b3fa57269bb5d5c06828"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "log"
|
||||||
|
version = "0.4.26"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "30bde2b3dc3671ae49d8e2e9f044c7c005836e7a023ee57cffa25ab82764bb9e"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "num-traits"
|
||||||
|
version = "0.2.19"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
|
||||||
|
dependencies = [
|
||||||
|
"autocfg",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "once_cell"
|
||||||
|
version = "1.20.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "945462a4b81e43c4e3ba96bd7b49d834c6f61198356aa858733bc4acf3cbe62e"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "proc-macro2"
|
||||||
|
version = "1.0.94"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a31971752e70b8b2686d7e46ec17fb38dad4051d94024c88df49b667caea9c84"
|
||||||
|
dependencies = [
|
||||||
|
"unicode-ident",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "quote"
|
||||||
|
version = "1.0.39"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c1f1914ce909e1658d9907913b4b91947430c7d9be598b15a1912935b8c04801"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rrtlpower"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"chrono",
|
||||||
|
"clap",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rustversion"
|
||||||
|
version = "1.0.20"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "eded382c5f5f786b989652c49544c4877d9f015cc22e145a5ea8ea66c2921cd2"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "shlex"
|
||||||
|
version = "1.3.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "strsim"
|
||||||
|
version = "0.11.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "syn"
|
||||||
|
version = "2.0.99"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e02e925281e18ffd9d640e234264753c43edc62d64b2d4cf898f1bc5e75f3fc2"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"unicode-ident",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "unicode-ident"
|
||||||
|
version = "1.0.18"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "utf8parse"
|
||||||
|
version = "0.2.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasm-bindgen"
|
||||||
|
version = "0.2.100"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"once_cell",
|
||||||
|
"rustversion",
|
||||||
|
"wasm-bindgen-macro",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasm-bindgen-backend"
|
||||||
|
version = "0.2.100"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6"
|
||||||
|
dependencies = [
|
||||||
|
"bumpalo",
|
||||||
|
"log",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
"wasm-bindgen-shared",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasm-bindgen-macro"
|
||||||
|
version = "0.2.100"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407"
|
||||||
|
dependencies = [
|
||||||
|
"quote",
|
||||||
|
"wasm-bindgen-macro-support",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasm-bindgen-macro-support"
|
||||||
|
version = "0.2.100"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
"wasm-bindgen-backend",
|
||||||
|
"wasm-bindgen-shared",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasm-bindgen-shared"
|
||||||
|
version = "0.2.100"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d"
|
||||||
|
dependencies = [
|
||||||
|
"unicode-ident",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-core"
|
||||||
|
version = "0.52.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9"
|
||||||
|
dependencies = [
|
||||||
|
"windows-targets",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-link"
|
||||||
|
version = "0.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6dccfd733ce2b1753b03b6d3c65edf020262ea35e20ccdf3e288043e6dd620e3"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-sys"
|
||||||
|
version = "0.59.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b"
|
||||||
|
dependencies = [
|
||||||
|
"windows-targets",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-targets"
|
||||||
|
version = "0.52.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
|
||||||
|
dependencies = [
|
||||||
|
"windows_aarch64_gnullvm",
|
||||||
|
"windows_aarch64_msvc",
|
||||||
|
"windows_i686_gnu",
|
||||||
|
"windows_i686_gnullvm",
|
||||||
|
"windows_i686_msvc",
|
||||||
|
"windows_x86_64_gnu",
|
||||||
|
"windows_x86_64_gnullvm",
|
||||||
|
"windows_x86_64_msvc",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows_aarch64_gnullvm"
|
||||||
|
version = "0.52.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows_aarch64_msvc"
|
||||||
|
version = "0.52.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows_i686_gnu"
|
||||||
|
version = "0.52.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows_i686_gnullvm"
|
||||||
|
version = "0.52.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows_i686_msvc"
|
||||||
|
version = "0.52.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows_x86_64_gnu"
|
||||||
|
version = "0.52.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows_x86_64_gnullvm"
|
||||||
|
version = "0.52.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows_x86_64_msvc"
|
||||||
|
version = "0.52.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
|
||||||
8
Cargo.toml
Normal file
8
Cargo.toml
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
[package]
|
||||||
|
name = "rrtlpower"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
chrono = "0.4.40"
|
||||||
|
clap = { version = "4.5.31", features = ["derive"] }
|
||||||
628
heatmap.py
Executable file
628
heatmap.py
Executable file
@ -0,0 +1,628 @@
|
|||||||
|
#! /usr/bin/env python
|
||||||
|
|
||||||
|
from PIL import Image, ImageDraw, ImageFont
|
||||||
|
import os, sys, gzip, math, argparse, colorsys, datetime
|
||||||
|
from collections import defaultdict
|
||||||
|
from itertools import *
|
||||||
|
|
||||||
|
urlretrieve = lambda a, b: None
|
||||||
|
try:
|
||||||
|
import urllib.request
|
||||||
|
urlretrieve = urllib.request.urlretrieve
|
||||||
|
except:
|
||||||
|
import urllib
|
||||||
|
urlretrieve = urllib.urlretrieve
|
||||||
|
|
||||||
|
# todo:
|
||||||
|
# matplotlib powered --interactive
|
||||||
|
# arbitrary freq marker spacing
|
||||||
|
# ppm
|
||||||
|
# blue-less marker grid
|
||||||
|
# fast summary thing
|
||||||
|
# gain normalization
|
||||||
|
# check pil version for brokenness
|
||||||
|
|
||||||
|
vera_url = "https://github.com/keenerd/rtl-sdr-misc/raw/master/heatmap/Vera.ttf"
|
||||||
|
vera_path = os.path.join(sys.path[0], "Vera.ttf")
|
||||||
|
|
||||||
|
tape_height = 25
|
||||||
|
tape_pt = 10
|
||||||
|
|
||||||
|
if not os.path.isfile(vera_path):
|
||||||
|
urlretrieve(vera_url, vera_path)
|
||||||
|
|
||||||
|
try:
|
||||||
|
font = ImageFont.truetype(vera_path, 10)
|
||||||
|
except:
|
||||||
|
print('Please download the Vera.ttf font and place it in the current directory.')
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
def build_parser():
|
||||||
|
parser = argparse.ArgumentParser(description='Convert rtl_power CSV files into graphics.')
|
||||||
|
parser.add_argument('input_path', metavar='INPUT', type=str,
|
||||||
|
help='Input CSV file. (may be a .csv.gz)')
|
||||||
|
parser.add_argument('output_path', metavar='OUTPUT', type=str,
|
||||||
|
help='Output image. (various extensions supported)')
|
||||||
|
parser.add_argument('--offset', dest='offset_freq', default=None,
|
||||||
|
help='Shift the entire frequency range, for up/down converters.')
|
||||||
|
parser.add_argument('--ytick', dest='time_tick', default=None,
|
||||||
|
help='Place ticks along the Y axis every N seconds/minutes/hours/days.')
|
||||||
|
parser.add_argument('--db', dest='db_limit', nargs=2, default=None,
|
||||||
|
help='Minimum and maximum db values.')
|
||||||
|
parser.add_argument('--compress', dest='compress', default=0,
|
||||||
|
help='Apply a gradual asymptotic time compression. Values > 1 are the new target height, values < 1 are a scaling factor.')
|
||||||
|
slicegroup = parser.add_argument_group('Slicing',
|
||||||
|
'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.')
|
||||||
|
slicegroup.add_argument('--low', dest='low_freq', default=None,
|
||||||
|
help='Minimum frequency for a subrange.')
|
||||||
|
slicegroup.add_argument('--high', dest='high_freq', default=None,
|
||||||
|
help='Maximum frequency for a subrange.')
|
||||||
|
slicegroup.add_argument('--begin', dest='begin_time', default=None,
|
||||||
|
help='Timestamp to start at.')
|
||||||
|
slicegroup.add_argument('--end', dest='end_time', default=None,
|
||||||
|
help='Timestamp to stop at.')
|
||||||
|
slicegroup.add_argument('--head', dest='head_time', default=None,
|
||||||
|
help='Duration to use, starting at the beginning.')
|
||||||
|
slicegroup.add_argument('--tail', dest='tail_time', default=None,
|
||||||
|
help='Duration to use, stopping at the end.')
|
||||||
|
parser.add_argument('--palette', dest='palette', default='default',
|
||||||
|
help='Set Color Palette: default, extended, charolastra, twente')
|
||||||
|
return parser
|
||||||
|
|
||||||
|
def frange(start, stop, step):
|
||||||
|
i = 0
|
||||||
|
while (i*step + start <= stop):
|
||||||
|
yield i*step + start
|
||||||
|
i += 1
|
||||||
|
|
||||||
|
def min_filter(row):
|
||||||
|
size = 3
|
||||||
|
result = []
|
||||||
|
for i in range(size):
|
||||||
|
here = row[i]
|
||||||
|
near = row[0:i] + row[i+1:size]
|
||||||
|
if here > min(near):
|
||||||
|
result.append(here)
|
||||||
|
continue
|
||||||
|
result.append(min(near))
|
||||||
|
for i in range(size-1, len(row)):
|
||||||
|
here = row[i]
|
||||||
|
near = row[i-(size-1):i]
|
||||||
|
if here > min(near):
|
||||||
|
result.append(here)
|
||||||
|
continue
|
||||||
|
result.append(min(near))
|
||||||
|
return result
|
||||||
|
|
||||||
|
def floatify(zs):
|
||||||
|
# nix errors with -inf, windows errors with -1.#J
|
||||||
|
zs2 = []
|
||||||
|
previous = 0 # awkward for single-column rows
|
||||||
|
for z in zs:
|
||||||
|
try:
|
||||||
|
z = float(z)
|
||||||
|
except ValueError:
|
||||||
|
z = previous
|
||||||
|
if math.isinf(z):
|
||||||
|
z = previous
|
||||||
|
if math.isnan(z):
|
||||||
|
z = previous
|
||||||
|
zs2.append(z)
|
||||||
|
previous = z
|
||||||
|
return zs2
|
||||||
|
|
||||||
|
def freq_parse(s):
|
||||||
|
suffix = 1
|
||||||
|
if s.lower().endswith('k'):
|
||||||
|
suffix = 1e3
|
||||||
|
if s.lower().endswith('m'):
|
||||||
|
suffix = 1e6
|
||||||
|
if s.lower().endswith('g'):
|
||||||
|
suffix = 1e9
|
||||||
|
if suffix != 1:
|
||||||
|
s = s[:-1]
|
||||||
|
return float(s) * suffix
|
||||||
|
|
||||||
|
def duration_parse(s):
|
||||||
|
suffix = 1
|
||||||
|
if s.lower().endswith('s'):
|
||||||
|
suffix = 1
|
||||||
|
if s.lower().endswith('m'):
|
||||||
|
suffix = 60
|
||||||
|
if s.lower().endswith('h'):
|
||||||
|
suffix = 60 * 60
|
||||||
|
if s.lower().endswith('d'):
|
||||||
|
suffix = 24 * 60 * 60
|
||||||
|
if suffix != 1 or s.lower().endswith('s'):
|
||||||
|
s = s[:-1]
|
||||||
|
return float(s) * suffix
|
||||||
|
|
||||||
|
def date_parse(s):
|
||||||
|
if '-' not in s:
|
||||||
|
return datetime.datetime.fromtimestamp(int(s))
|
||||||
|
return datetime.datetime.strptime(s, '%Y-%m-%d %H:%M:%S')
|
||||||
|
|
||||||
|
def palette_parse(s):
|
||||||
|
palettes = {'default': default_palette,
|
||||||
|
'extended': extended_palette,
|
||||||
|
'charolastra': charolastra_palette,
|
||||||
|
'twente': twente_palette,
|
||||||
|
}
|
||||||
|
if s not in palettes:
|
||||||
|
print('WARNING: %s not a valid palette' % s)
|
||||||
|
return palettes.get(s, default_palette)
|
||||||
|
|
||||||
|
def gzip_wrap(path):
|
||||||
|
"hides silly CRC errors"
|
||||||
|
iterator = gzip.open(path, 'rb')
|
||||||
|
running = True
|
||||||
|
while running:
|
||||||
|
try:
|
||||||
|
line = next(iterator)
|
||||||
|
if type(line) == bytes:
|
||||||
|
line = line.decode('utf-8')
|
||||||
|
yield line
|
||||||
|
except IOError:
|
||||||
|
running = False
|
||||||
|
|
||||||
|
def time_compression(y, decay):
|
||||||
|
return int(round((1/decay)*math.exp(y*decay) - 1/decay))
|
||||||
|
|
||||||
|
def reparse(args, label, fn):
|
||||||
|
if args.__getattribute__(label) is None:
|
||||||
|
return
|
||||||
|
args.__setattr__(label, fn(args.__getattribute__(label)))
|
||||||
|
|
||||||
|
def prepare_args():
|
||||||
|
# hack, http://stackoverflow.com/questions/9025204/
|
||||||
|
for i, arg in enumerate(sys.argv):
|
||||||
|
if (arg[0] == '-') and arg[1].isdigit():
|
||||||
|
sys.argv[i] = ' ' + arg
|
||||||
|
parser = build_parser()
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
reparse(args, 'low_freq', freq_parse)
|
||||||
|
reparse(args, 'high_freq', freq_parse)
|
||||||
|
reparse(args, 'offset_freq', freq_parse)
|
||||||
|
if args.offset_freq is None:
|
||||||
|
args.offset_freq = 0
|
||||||
|
reparse(args, 'time_tick', duration_parse)
|
||||||
|
reparse(args, 'begin_time', date_parse)
|
||||||
|
reparse(args, 'end_time', date_parse)
|
||||||
|
reparse(args, 'head_time', duration_parse)
|
||||||
|
reparse(args, 'tail_time', duration_parse)
|
||||||
|
reparse(args, 'palette', palette_parse)
|
||||||
|
reparse(args, 'head_time', lambda s: datetime.timedelta(seconds=s))
|
||||||
|
reparse(args, 'tail_time', lambda s: datetime.timedelta(seconds=s))
|
||||||
|
args.compress = float(args.compress)
|
||||||
|
|
||||||
|
if args.db_limit:
|
||||||
|
a,b = args.db_limit
|
||||||
|
args.db_limit = (float(a), float(b))
|
||||||
|
|
||||||
|
if args.begin_time and args.tail_time:
|
||||||
|
print("Can't combine --begin and --tail")
|
||||||
|
sys.exit(2)
|
||||||
|
if args.end_time and args.head_time:
|
||||||
|
print("Can't combine --end and --head")
|
||||||
|
sys.exit(2)
|
||||||
|
if args.head_time and args.tail_time:
|
||||||
|
print("Can't combine --head and --tail")
|
||||||
|
sys.exit(2)
|
||||||
|
return args
|
||||||
|
|
||||||
|
def open_raw_data(path):
|
||||||
|
raw_data = lambda: open(path)
|
||||||
|
if path.endswith('.gz'):
|
||||||
|
raw_data = lambda: gzip_wrap(path)
|
||||||
|
return raw_data
|
||||||
|
|
||||||
|
def slice_columns(columns, low_freq, high_freq):
|
||||||
|
start_col = 0
|
||||||
|
stop_col = len(columns)
|
||||||
|
if low_freq is not None and low <= low_freq <= high:
|
||||||
|
start_col = sum(f<low_freq for f in columns)
|
||||||
|
if high_freq is not None and low <= high_freq <= high:
|
||||||
|
stop_col = sum(f<=high_freq for f in columns)
|
||||||
|
return start_col, stop_col-1
|
||||||
|
|
||||||
|
def summarize_pass(args):
|
||||||
|
"pumps a bunch of data back into the args construct"
|
||||||
|
freqs = set()
|
||||||
|
f_cache = set()
|
||||||
|
times = set()
|
||||||
|
labels = set()
|
||||||
|
min_z = 0
|
||||||
|
max_z = -100
|
||||||
|
start, stop = None, None
|
||||||
|
|
||||||
|
for line in raw_data():
|
||||||
|
line = [s.strip() for s in line.strip().split(',')]
|
||||||
|
#line = [line[0], line[1]] + [float(s) for s in line[2:] if s]
|
||||||
|
line = [s for s in line if s]
|
||||||
|
|
||||||
|
low = int(line[2]) + args.offset_freq
|
||||||
|
high = int(line[3]) + args.offset_freq
|
||||||
|
step = float(line[4])
|
||||||
|
t = line[0] + ' ' + line[1]
|
||||||
|
if '-' not in line[0]:
|
||||||
|
t = line[0]
|
||||||
|
|
||||||
|
if args.low_freq is not None and high < args.low_freq:
|
||||||
|
continue
|
||||||
|
if args.high_freq is not None and args.high_freq < low:
|
||||||
|
continue
|
||||||
|
if args.begin_time is not None and date_parse(t) < args.begin_time:
|
||||||
|
continue
|
||||||
|
if args.end_time is not None and date_parse(t) > args.end_time:
|
||||||
|
break
|
||||||
|
times.add(t)
|
||||||
|
columns = list(frange(low, high, step))
|
||||||
|
start_col, stop_col = slice_columns(columns, args.low_freq, args.high_freq)
|
||||||
|
f_key = (columns[start_col], columns[stop_col], step)
|
||||||
|
zs = line[6+start_col:6+stop_col+1]
|
||||||
|
if not zs:
|
||||||
|
continue
|
||||||
|
if f_key not in f_cache:
|
||||||
|
freq2 = list(frange(*f_key))[:len(zs)]
|
||||||
|
freqs.update(freq2)
|
||||||
|
#freqs.add(f_key[1]) # high
|
||||||
|
#labels.add(f_key[0]) # low
|
||||||
|
f_cache.add(f_key)
|
||||||
|
|
||||||
|
if not args.db_limit:
|
||||||
|
zs = floatify(zs)
|
||||||
|
min_z = min(min_z, min(zs))
|
||||||
|
max_z = max(max_z, max(zs))
|
||||||
|
|
||||||
|
if start is None:
|
||||||
|
start = date_parse(t)
|
||||||
|
stop = date_parse(t)
|
||||||
|
if args.head_time is not None and args.end_time is None:
|
||||||
|
args.end_time = start + args.head_time
|
||||||
|
|
||||||
|
if not args.db_limit:
|
||||||
|
args.db_limit = (min_z, max_z)
|
||||||
|
|
||||||
|
if args.tail_time is not None:
|
||||||
|
times = [t for t in times if date_parse(t) >= (stop - args.tail_time)]
|
||||||
|
start = date_parse(min(times))
|
||||||
|
|
||||||
|
freqs = list(sorted(list(freqs)))
|
||||||
|
times = list(sorted(list(times)))
|
||||||
|
labels = list(sorted(list(labels)))
|
||||||
|
|
||||||
|
if len(labels) == 1:
|
||||||
|
delta = (max(freqs) - min(freqs)) / (len(freqs) / 500.0)
|
||||||
|
delta = round(delta / 10**int(math.log10(delta))) * 10**int(math.log10(delta))
|
||||||
|
delta = int(delta)
|
||||||
|
lower = int(math.ceil(min(freqs) / delta) * delta)
|
||||||
|
labels = list(range(lower, int(max(freqs)), delta))
|
||||||
|
|
||||||
|
height = len(times)
|
||||||
|
pix_height = height
|
||||||
|
if args.compress:
|
||||||
|
if args.compress > height:
|
||||||
|
args.compress = 0
|
||||||
|
print("Image too short, disabling time compression")
|
||||||
|
if 0 < args.compress < 1:
|
||||||
|
args.compress *= height
|
||||||
|
if args.compress:
|
||||||
|
args.compress = -1 / args.compress
|
||||||
|
pix_height = time_compression(height, args.compress)
|
||||||
|
|
||||||
|
print("x: %i, y: %i, z: (%f, %f)" % (len(freqs), pix_height, args.db_limit[0], args.db_limit[1]))
|
||||||
|
args.freqs = freqs
|
||||||
|
args.times = times
|
||||||
|
args.labels = labels
|
||||||
|
args.pix_height = pix_height
|
||||||
|
args.start_stop = (start, stop)
|
||||||
|
args.pixel_bandwidth = step
|
||||||
|
|
||||||
|
def default_palette():
|
||||||
|
return [(i, i, 50) for i in range(256)]
|
||||||
|
|
||||||
|
def extended_palette():
|
||||||
|
p = [(0,0,50)]
|
||||||
|
for i in range(1, 256):
|
||||||
|
p.append((i, i-1, 50))
|
||||||
|
p.append((i-1, i, 50))
|
||||||
|
p.append((i, i, 50))
|
||||||
|
return p
|
||||||
|
|
||||||
|
def charolastra_palette():
|
||||||
|
p = []
|
||||||
|
for i in range(1024):
|
||||||
|
g = i / 1023.0
|
||||||
|
c = colorsys.hsv_to_rgb(0.65-(g-0.08), 1, 0.2+g)
|
||||||
|
p.append((int(c[0]*256), int(c[1]*256), int(c[2]*256)))
|
||||||
|
return p
|
||||||
|
|
||||||
|
def twente_palette():
|
||||||
|
p = []
|
||||||
|
for i in range(20, 100, 2):
|
||||||
|
p.append((0, 0, i))
|
||||||
|
for i in range(256):
|
||||||
|
g = i / 255.0
|
||||||
|
p.append((int(g*255), 0, int(g*155)+100))
|
||||||
|
for i in range(256):
|
||||||
|
p.append((255, i, 255))
|
||||||
|
# intentionally blow out the highs
|
||||||
|
for i in range(100):
|
||||||
|
p.append((255, 255, 255))
|
||||||
|
return p
|
||||||
|
|
||||||
|
def rgb_fn(palette, min_z, max_z):
|
||||||
|
"palette is a list of tuples, returns a function of z"
|
||||||
|
def rgb_inner(z):
|
||||||
|
tone = (z - min_z) / (max_z - min_z)
|
||||||
|
tone_scaled = int(tone * (len(palette)-1))
|
||||||
|
return palette[tone_scaled]
|
||||||
|
return rgb_inner
|
||||||
|
|
||||||
|
def collate_row(x_size):
|
||||||
|
# this is more fragile than the old code
|
||||||
|
# sensitive to timestamps that are out of order
|
||||||
|
old_t = None
|
||||||
|
row = [0.0] * x_size
|
||||||
|
for line in raw_data():
|
||||||
|
line = [s.strip() for s in line.strip().split(',')]
|
||||||
|
#line = [line[0], line[1]] + [float(s) for s in line[2:] if s]
|
||||||
|
line = [s for s in line if s]
|
||||||
|
t = line[0] + ' ' + line[1]
|
||||||
|
if '-' not in line[0]:
|
||||||
|
t = line[0]
|
||||||
|
if t not in args.times:
|
||||||
|
continue # happens with live files and time cropping
|
||||||
|
if old_t is None:
|
||||||
|
old_t = t
|
||||||
|
low = int(line[2]) + args.offset_freq
|
||||||
|
high = int(line[3]) + args.offset_freq
|
||||||
|
step = float(line[4])
|
||||||
|
columns = list(frange(low, high, step))
|
||||||
|
start_col, stop_col = slice_columns(columns, args.low_freq, args.high_freq)
|
||||||
|
if args.low_freq and columns[stop_col] < args.low_freq:
|
||||||
|
continue
|
||||||
|
if args.high_freq and columns[start_col] > args.high_freq:
|
||||||
|
continue
|
||||||
|
start_freq = columns[start_col]
|
||||||
|
if args.low_freq:
|
||||||
|
start_freq = max(args.low_freq, start_freq)
|
||||||
|
# sometimes fails? skip or abort?
|
||||||
|
x_start = args.freqs.index(start_freq)
|
||||||
|
zs = floatify(line[6+start_col:6+stop_col+1])
|
||||||
|
if t != old_t:
|
||||||
|
yield old_t, row
|
||||||
|
row = [0.0] * x_size
|
||||||
|
old_t = t
|
||||||
|
for i in range(len(zs)):
|
||||||
|
x = x_start + i
|
||||||
|
if x >= x_size:
|
||||||
|
continue
|
||||||
|
row[x] = zs[i]
|
||||||
|
yield old_t, row
|
||||||
|
|
||||||
|
def push_pixels(args):
|
||||||
|
"returns PIL img"
|
||||||
|
width = len(args.freqs)
|
||||||
|
rgb = rgb_fn(args.palette(), args.db_limit[0], args.db_limit[1])
|
||||||
|
img = Image.new("RGB", (width, tape_height + args.pix_height + 1))
|
||||||
|
pix = img.load()
|
||||||
|
x_size = img.size[0]
|
||||||
|
average = [0.0] * width
|
||||||
|
tally = 0
|
||||||
|
old_y = None
|
||||||
|
height = len(args.times)
|
||||||
|
for t, zs in collate_row(x_size):
|
||||||
|
y = args.times.index(t)
|
||||||
|
if not args.compress:
|
||||||
|
for x in range(len(zs)):
|
||||||
|
pix[x,y+tape_height+1] = rgb(zs[x])
|
||||||
|
continue
|
||||||
|
# ugh
|
||||||
|
y = args.pix_height - time_compression(height - y, args.compress)
|
||||||
|
if old_y is None:
|
||||||
|
old_y = y
|
||||||
|
if old_y != y:
|
||||||
|
for x in range(len(average)):
|
||||||
|
pix[x,old_y+tape_height+1] = rgb(average[x]/tally)
|
||||||
|
tally = 0
|
||||||
|
average = [0.0] * width
|
||||||
|
old_y = y
|
||||||
|
for x in range(len(zs)):
|
||||||
|
average[x] += zs[x]
|
||||||
|
tally += 1
|
||||||
|
return img
|
||||||
|
|
||||||
|
def closest_index(n, m_list, interpolate=False):
|
||||||
|
"assumes sorted m_list, returns two points for interpolate"
|
||||||
|
i = len(m_list) // 2
|
||||||
|
jump = len(m_list) // 2
|
||||||
|
while jump > 1:
|
||||||
|
i_down = i - jump
|
||||||
|
i_here = i
|
||||||
|
i_up = i + jump
|
||||||
|
if i_down < 0:
|
||||||
|
i_down = i
|
||||||
|
if i_up >= len(m_list):
|
||||||
|
i_up = i
|
||||||
|
e_down = abs(m_list[i_down] - n)
|
||||||
|
e_here = abs(m_list[i_here] - n)
|
||||||
|
e_up = abs(m_list[i_up] - n)
|
||||||
|
e_best = min([e_down, e_here, e_up])
|
||||||
|
if e_down == e_best:
|
||||||
|
i = i_down
|
||||||
|
if e_up == e_best:
|
||||||
|
i = i_up
|
||||||
|
if e_here == e_best:
|
||||||
|
i = i_here
|
||||||
|
jump = jump // 2
|
||||||
|
if not interpolate:
|
||||||
|
return i
|
||||||
|
if n < m_list[i] and i > 0:
|
||||||
|
return i-1, i
|
||||||
|
if n > m_list[i] and i < len(m_list)-1:
|
||||||
|
return i, i+1
|
||||||
|
return i, i
|
||||||
|
|
||||||
|
def word_aa(label, pt, fg_color, bg_color):
|
||||||
|
f = ImageFont.truetype(vera_path, pt*3)
|
||||||
|
#s = f.getsize(label)
|
||||||
|
s = f.getlength(label)
|
||||||
|
#s = (s[0], pt*3 + 3) # getsize lies, manually compute
|
||||||
|
s = (int(s + 0.5), pt*3 + 3) # getsize lies, manually compute
|
||||||
|
w_img = Image.new("RGB", s, bg_color)
|
||||||
|
w_draw = ImageDraw.Draw(w_img)
|
||||||
|
w_draw.text((0, 0), label, font=f, fill=fg_color)
|
||||||
|
#return w_img.resize((s[0]//3, s[1]//3), Image.ANTIALIAS)
|
||||||
|
return w_img.resize((s[0]//3, s[1]//3), Image.Resampling.LANCZOS)
|
||||||
|
|
||||||
|
def blend(percent, c1, c2):
|
||||||
|
"c1 and c2 are RGB tuples"
|
||||||
|
# probably isn't gamma correct
|
||||||
|
r = c1[0] * percent + c2[0] * (1 - percent)
|
||||||
|
g = c1[1] * percent + c2[1] * (1 - percent)
|
||||||
|
b = c1[2] * percent + c2[2] * (1 - percent)
|
||||||
|
c3 = map(int, map(round, [r,g,b]))
|
||||||
|
return tuple(c3)
|
||||||
|
|
||||||
|
def tape_lines(draw, freqs, interval, y1, y2, used=set()):
|
||||||
|
min_f = min(freqs)
|
||||||
|
max_f = max(freqs)
|
||||||
|
"returns the number of lines"
|
||||||
|
low_f = (min_f // interval) * interval
|
||||||
|
high_f = (1 + max_f // interval) * interval
|
||||||
|
hits = 0
|
||||||
|
blur = lambda p: blend(p, (255, 255, 0), (0, 0, 0))
|
||||||
|
for i in range(int(low_f), int(high_f), int(interval)):
|
||||||
|
if not (min_f < i < max_f):
|
||||||
|
continue
|
||||||
|
hits += 1
|
||||||
|
if i in used:
|
||||||
|
continue
|
||||||
|
x1,x2 = closest_index(i, args.freqs, interpolate=True)
|
||||||
|
if x1 == x2:
|
||||||
|
draw.line([x1,y1,x1,y2], fill='black')
|
||||||
|
else:
|
||||||
|
percent = (i - args.freqs[x1]) / float(args.freqs[x2] - args.freqs[x1])
|
||||||
|
draw.line([x1,y1,x1,y2], fill=blur(percent))
|
||||||
|
draw.line([x2,y1,x2,y2], fill=blur(1-percent))
|
||||||
|
used.add(i)
|
||||||
|
return hits
|
||||||
|
|
||||||
|
def tape_text(img, freqs, interval, y, used=set()):
|
||||||
|
min_f = min(freqs)
|
||||||
|
max_f = max(freqs)
|
||||||
|
low_f = (min_f // interval) * interval
|
||||||
|
high_f = (1 + max_f // interval) * interval
|
||||||
|
for i in range(int(low_f), int(high_f), int(interval)):
|
||||||
|
if i in used:
|
||||||
|
continue
|
||||||
|
if not (min_f < i < max_f):
|
||||||
|
continue
|
||||||
|
x = closest_index(i, freqs)
|
||||||
|
s = str(i)
|
||||||
|
if interval >= 1e6:
|
||||||
|
s = '%iM' % (i/1e6)
|
||||||
|
elif interval > 1000:
|
||||||
|
s = '%ik' % ((i/1e3) % 1000)
|
||||||
|
if s.startswith('0'):
|
||||||
|
s = '%iM' % (i/1e6)
|
||||||
|
else:
|
||||||
|
s = '%i' % (i%1000)
|
||||||
|
if s.startswith('0'):
|
||||||
|
s = '%ik' % ((i/1e3) % 1000)
|
||||||
|
if s.startswith('0'):
|
||||||
|
s = '%iM' % (i/1e6)
|
||||||
|
w = word_aa(s, tape_pt, 'black', 'yellow')
|
||||||
|
img.paste(w, (x - w.size[0]//2, y))
|
||||||
|
used.add(i)
|
||||||
|
|
||||||
|
def shadow_text(draw, x, y, s, font, fg_color='white', bg_color='black'):
|
||||||
|
draw.text((x+1, y+1), s, font=font, fill=bg_color)
|
||||||
|
draw.text((x, y), s, font=font, fill=fg_color)
|
||||||
|
|
||||||
|
def create_labels(args, img):
|
||||||
|
draw = ImageDraw.Draw(img)
|
||||||
|
font = ImageFont.load_default()
|
||||||
|
pixel_bandwidth = args.pixel_bandwidth
|
||||||
|
|
||||||
|
draw.rectangle([0,0,img.size[0],tape_height], fill='yellow')
|
||||||
|
min_freq = min(args.freqs)
|
||||||
|
max_freq = max(args.freqs)
|
||||||
|
delta = max_freq - min_freq
|
||||||
|
width = len(args.freqs)
|
||||||
|
height = len(args.times)
|
||||||
|
label_base = 9
|
||||||
|
|
||||||
|
for i in range(label_base, 0, -1):
|
||||||
|
interval = int(10**i)
|
||||||
|
low_f = (min_freq // interval) * interval
|
||||||
|
high_f = (1 + max_freq // interval) * interval
|
||||||
|
hits = len(range(int(low_f), int(high_f), interval))
|
||||||
|
if hits >= 4:
|
||||||
|
label_base = i
|
||||||
|
break
|
||||||
|
label_base = 10**label_base
|
||||||
|
|
||||||
|
for scale,y in [(1,10), (5,15), (10,19), (50,22), (100,24), (500, 25)]:
|
||||||
|
hits = tape_lines(draw, args.freqs, label_base/scale, y, tape_height)
|
||||||
|
pixels_per_hit = width / hits
|
||||||
|
if pixels_per_hit > 50:
|
||||||
|
tape_text(img, args.freqs, label_base/scale, y-tape_pt)
|
||||||
|
if pixels_per_hit < 10:
|
||||||
|
break
|
||||||
|
|
||||||
|
start, stop = args.start_stop
|
||||||
|
duration = stop - start
|
||||||
|
duration = duration.days * 24*60*60 + duration.seconds + 30
|
||||||
|
pixel_height = duration / len(args.times)
|
||||||
|
hours = int(duration / 3600)
|
||||||
|
minutes = int((duration - 3600*hours) / 60)
|
||||||
|
|
||||||
|
if args.time_tick:
|
||||||
|
label_format = "%H:%M:%S"
|
||||||
|
if args.time_tick % (60*60*24) == 0:
|
||||||
|
label_format = "%Y-%m-%d"
|
||||||
|
elif args.time_tick % 60 == 0:
|
||||||
|
label_format = "%H:%M"
|
||||||
|
label_next = datetime.datetime(start.year, start.month, start.day, start.hour)
|
||||||
|
tick_delta = datetime.timedelta(seconds = args.time_tick)
|
||||||
|
while label_next < start:
|
||||||
|
label_next += tick_delta
|
||||||
|
last_y = -100
|
||||||
|
full_height = args.pix_height
|
||||||
|
for y,t in enumerate(args.times):
|
||||||
|
label_time = date_parse(t)
|
||||||
|
if label_time < label_next:
|
||||||
|
continue
|
||||||
|
if args.compress:
|
||||||
|
y = full_height - time_compression(height - y, args.compress)
|
||||||
|
if y - last_y > 15:
|
||||||
|
shadow_text(draw, 2, y+tape_height, label_next.strftime(label_format), font)
|
||||||
|
last_y = y
|
||||||
|
label_next += tick_delta
|
||||||
|
|
||||||
|
margin = 2
|
||||||
|
if args.time_tick:
|
||||||
|
margin = 60
|
||||||
|
shadow_text(draw, margin, img.size[1] - 45, 'Duration: %i:%02i' % (hours, minutes), font)
|
||||||
|
shadow_text(draw, margin, img.size[1] - 35, 'Range: %.2fMHz - %.2fMHz' % (min_freq/1e6, (max_freq+pixel_bandwidth)/1e6), font)
|
||||||
|
shadow_text(draw, margin, img.size[1] - 25, 'Pixel: %.2fHz x %is' % (pixel_bandwidth, int(round(pixel_height))), font)
|
||||||
|
shadow_text(draw, margin, img.size[1] - 15, 'Started: {0}'.format(start), font)
|
||||||
|
# bin size
|
||||||
|
|
||||||
|
print("loading")
|
||||||
|
args = prepare_args()
|
||||||
|
raw_data = open_raw_data(args.input_path)
|
||||||
|
summarize_pass(args)
|
||||||
|
|
||||||
|
print("drawing")
|
||||||
|
img = push_pixels(args)
|
||||||
|
|
||||||
|
print("labeling")
|
||||||
|
create_labels(args, img)
|
||||||
|
|
||||||
|
print("saving")
|
||||||
|
img.save(args.output_path)
|
||||||
|
|
||||||
135
src/main.rs
Normal file
135
src/main.rs
Normal file
@ -0,0 +1,135 @@
|
|||||||
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
use chrono::{Datelike, Timelike};
|
||||||
|
use clap::Parser;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
struct FreqRange {
|
||||||
|
lower: String,
|
||||||
|
upper: String,
|
||||||
|
bin_size: String,
|
||||||
|
}
|
||||||
|
impl std::fmt::Display for FreqRange {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
write!(f, "{}:{}:{}", self.lower, self.upper, self.bin_size)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromStr for FreqRange {
|
||||||
|
type Err = std::io::Error;
|
||||||
|
|
||||||
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
|
use std::io::ErrorKind;
|
||||||
|
let mut ittr = s.split(':');
|
||||||
|
let lower = ittr
|
||||||
|
.next()
|
||||||
|
.ok_or(Self::Err::from(ErrorKind::InvalidInput))?
|
||||||
|
.to_string();
|
||||||
|
let upper = ittr
|
||||||
|
.next()
|
||||||
|
.ok_or(Self::Err::from(ErrorKind::InvalidInput))?
|
||||||
|
.to_string();
|
||||||
|
let bin_size = ittr
|
||||||
|
.next()
|
||||||
|
.ok_or(Self::Err::from(ErrorKind::InvalidInput))?
|
||||||
|
.to_string();
|
||||||
|
Ok(FreqRange {
|
||||||
|
lower,
|
||||||
|
upper,
|
||||||
|
bin_size,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// rrtlpower, a simple front end to rtl_power
|
||||||
|
#[derive(Parser, Debug)]
|
||||||
|
#[command(version, about, long_about = None)]
|
||||||
|
struct CliArgs {
|
||||||
|
#[arg(short, long)]
|
||||||
|
freq_range: FreqRange,
|
||||||
|
|
||||||
|
#[arg(short, long)]
|
||||||
|
integration_interval: Option<String>,
|
||||||
|
|
||||||
|
#[arg(short, long)]
|
||||||
|
exit_timer: Option<String>,
|
||||||
|
|
||||||
|
#[arg(short, long)]
|
||||||
|
tuner_gain: Option<String>,
|
||||||
|
|
||||||
|
#[arg(short, long)]
|
||||||
|
crop_percent: Option<String>,
|
||||||
|
}
|
||||||
|
fn main() {
|
||||||
|
let args = CliArgs::parse();
|
||||||
|
let now = chrono::Utc::now();
|
||||||
|
|
||||||
|
let filename = format!(
|
||||||
|
"{:04}-{:02}-{:02}-{:02}:{:02}:{:02}z_{}",
|
||||||
|
now.year(),
|
||||||
|
now.month(),
|
||||||
|
now.day(),
|
||||||
|
now.hour(),
|
||||||
|
now.minute(),
|
||||||
|
now.second(),
|
||||||
|
args.freq_range,
|
||||||
|
);
|
||||||
|
println!("Using filename {filename}.csv");
|
||||||
|
|
||||||
|
let mut rtl_cmd = std::process::Command::new("rtl_power");
|
||||||
|
rtl_cmd.arg("-f");
|
||||||
|
rtl_cmd.arg(args.freq_range.to_string());
|
||||||
|
// Optional args
|
||||||
|
if let Some(interval) = args.integration_interval {
|
||||||
|
rtl_cmd.arg("-i");
|
||||||
|
rtl_cmd.arg(interval.clone());
|
||||||
|
}
|
||||||
|
if let Some(timer) = args.exit_timer {
|
||||||
|
rtl_cmd.arg("-e");
|
||||||
|
rtl_cmd.arg(timer);
|
||||||
|
}
|
||||||
|
if let Some(gain) = args.tuner_gain {
|
||||||
|
rtl_cmd.arg("-g");
|
||||||
|
rtl_cmd.arg(gain);
|
||||||
|
}
|
||||||
|
if let Some(crop) = args.crop_percent {
|
||||||
|
rtl_cmd.arg("-c");
|
||||||
|
rtl_cmd.arg(crop);
|
||||||
|
}
|
||||||
|
rtl_cmd.arg(format!("{filename}.csv"));
|
||||||
|
|
||||||
|
println!(
|
||||||
|
"rtl_power{}",
|
||||||
|
rtl_cmd
|
||||||
|
.get_args()
|
||||||
|
// TODO: Waiting for https://github.com/rust-lang/rust/issues/79524
|
||||||
|
.map(|arg| format!(" {}", arg.to_str().unwrap()))
|
||||||
|
.collect::<String>()
|
||||||
|
);
|
||||||
|
|
||||||
|
match rtl_cmd.status() {
|
||||||
|
Ok(status) if status.success() => {}
|
||||||
|
//TODO: More verbose error status message
|
||||||
|
_ => {
|
||||||
|
println!("\nError running rtl_power! Exiting early");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut heatmap_cmd = std::process::Command::new("python3");
|
||||||
|
heatmap_cmd.arg("heatmap.py");
|
||||||
|
heatmap_cmd.arg(format!("{filename}.csv"));
|
||||||
|
heatmap_cmd.arg(format!("{filename}.png"));
|
||||||
|
|
||||||
|
/*println!(
|
||||||
|
"\npython3{}",
|
||||||
|
heatmap_cmd
|
||||||
|
.get_args()
|
||||||
|
// TODO: Waiting for https://github.com/rust-lang/rust/issues/79524
|
||||||
|
.map(|arg| format!(" {}", arg.to_str().unwrap()))
|
||||||
|
.collect::<String>()
|
||||||
|
);*/
|
||||||
|
println!("python3 heatmap.py {filename}.csv {filename}.png");
|
||||||
|
|
||||||
|
println!("{}", heatmap_cmd.status().unwrap());
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user