diff --git a/.gitignore b/.gitignore index 072cfc8..1f6b4fe 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ *.wav .ipynb_checkpoints /Manifest.toml +test/Manifest.toml /docs/build/ test/build -test/products \ No newline at end of file +test/products diff --git a/gen/Manifest.toml b/gen/Manifest.toml index 138e97d..33cd887 100644 --- a/gen/Manifest.toml +++ b/gen/Manifest.toml @@ -1,155 +1,249 @@ # This file is machine-generated - editing it directly is not advised -julia_version = "1.7.3" +julia_version = "1.11.4" manifest_format = "2.0" +project_hash = "450221ab4cf937bd3ccdc2e35d68c4171fbdb1fb" [[deps.ArgTools]] uuid = "0dad84c5-d112-42e6-8d28-ef12dabb789f" +version = "1.1.2" [[deps.Artifacts]] uuid = "56f22d72-fd6d-98f1-02f0-08ddc0907c33" +version = "1.11.0" [[deps.Base64]] uuid = "2a0f44e3-6c83-55bd-87e4-b1978d98bd5f" +version = "1.11.0" [[deps.CEnum]] -git-tree-sha1 = "eb4cb44a499229b3b8426dcfb5dd85333951ff90" +git-tree-sha1 = "389ad5c84de1ae7cf0e28e381131c98ea87d54fc" uuid = "fa961155-64e5-5f13-b03f-caf6b980ea82" -version = "0.4.2" +version = "0.5.0" + +[[deps.CSTParser]] +deps = ["Tokenize"] +git-tree-sha1 = "0157e592151e39fa570645e2b2debcdfb8a0f112" +uuid = "00ebfdb7-1f24-5e51-bd34-a7502290713f" +version = "3.4.3" [[deps.Clang]] deps = ["CEnum", "Clang_jll", "Downloads", "Pkg", "TOML"] -git-tree-sha1 = "b6998b85ea2abbc8a02ca1ccca9438338ed5f8e5" +git-tree-sha1 = "2397d5da17ba4970f772a9888b208a0a1d77eb5d" uuid = "40e3b903-d033-50b4-a0cc-940c62c95e31" -version = "0.15.8" +version = "0.18.3" [[deps.Clang_jll]] -deps = ["Artifacts", "JLLWrappers", "Libdl", "Pkg", "Zlib_jll", "libLLVM_jll"] -git-tree-sha1 = "8cf7e67e264dedc5d321ec87e78525e958aea057" +deps = ["Artifacts", "JLLWrappers", "Libdl", "TOML", "Zlib_jll", "libLLVM_jll"] +git-tree-sha1 = "0dc9bd89383fd6fffed127e03fc42ed409cc865b" uuid = "0ee61d77-7f21-5576-8119-9fcc46b10100" -version = "12.0.1+3" +version = "16.0.6+4" + +[[deps.CommonMark]] +deps = ["Crayons", "PrecompileTools"] +git-tree-sha1 = "5fdf00d1979fd4883b44b754fc3423175c9504b4" +uuid = "a80b9123-70ca-4bc0-993e-6e3bcb318db6" +version = "0.8.16" + +[[deps.Compat]] +deps = ["TOML", "UUIDs"] +git-tree-sha1 = "8ae8d32e09f0dcf42a36b90d4e17f5dd2e4c4215" +uuid = "34da2185-b29b-5c13-b0c7-acf172513d20" +version = "4.16.0" + + [deps.Compat.extensions] + CompatLinearAlgebraExt = "LinearAlgebra" + + [deps.Compat.weakdeps] + Dates = "ade2ca70-3891-5945-98fb-dc099432e06a" + LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" + +[[deps.Crayons]] +git-tree-sha1 = "249fe38abf76d48563e2f4556bebd215aa317e15" +uuid = "a8cc5b0e-0ffa-5ad4-8c14-923d3ee1735f" +version = "4.1.1" + +[[deps.DataStructures]] +deps = ["Compat", "InteractiveUtils", "OrderedCollections"] +git-tree-sha1 = "4e1fe97fdaed23e9dc21d4d664bea76b65fc50a0" +uuid = "864edb3b-99cc-5e75-8d2d-829cb0a9cfe8" +version = "0.18.22" [[deps.Dates]] deps = ["Printf"] uuid = "ade2ca70-3891-5945-98fb-dc099432e06a" +version = "1.11.0" [[deps.Downloads]] deps = ["ArgTools", "FileWatching", "LibCURL", "NetworkOptions"] uuid = "f43a241f-c20a-4ad4-852c-f6b1247861c6" +version = "1.6.0" [[deps.FileWatching]] uuid = "7b1f6079-737a-58dc-b8bc-7a2ca5c1b5ee" +version = "1.11.0" + +[[deps.Glob]] +git-tree-sha1 = "97285bbd5230dd766e9ef6749b80fc617126d496" +uuid = "c27321d9-0574-5035-807b-f59d2c89b15c" +version = "1.3.1" [[deps.InteractiveUtils]] deps = ["Markdown"] uuid = "b77e0a4c-d291-57a0-90e8-8db25a27a240" +version = "1.11.0" [[deps.JLLWrappers]] -deps = ["Preferences"] -git-tree-sha1 = "abc9885a7ca2052a736a600f7fa66209f96506e1" +deps = ["Artifacts", "Preferences"] +git-tree-sha1 = "a007feb38b422fbdab534406aeca1b86823cb4d6" uuid = "692b3bcd-3c85-4b1f-b108-f13ce0eb3210" -version = "1.4.1" +version = "1.7.0" + +[[deps.JuliaFormatter]] +deps = ["CSTParser", "CommonMark", "DataStructures", "Glob", "PrecompileTools", "TOML", "Tokenize"] +git-tree-sha1 = "59cf7ad64f1b0708a4fa4369879d33bad3239b56" +uuid = "98e50ef6-434e-11e9-1051-2b60c6c9e899" +version = "1.0.62" [[deps.LibCURL]] deps = ["LibCURL_jll", "MozillaCACerts_jll"] uuid = "b27032c2-a3e7-50c8-80cd-2d36dbcbfd21" +version = "0.6.4" [[deps.LibCURL_jll]] deps = ["Artifacts", "LibSSH2_jll", "Libdl", "MbedTLS_jll", "Zlib_jll", "nghttp2_jll"] uuid = "deac9b47-8bc7-5906-a0fe-35ac56dc84c0" +version = "8.6.0+0" [[deps.LibGit2]] -deps = ["Base64", "NetworkOptions", "Printf", "SHA"] +deps = ["Base64", "LibGit2_jll", "NetworkOptions", "Printf", "SHA"] uuid = "76f85450-5226-5b5a-8eaa-529ad045b433" +version = "1.11.0" + +[[deps.LibGit2_jll]] +deps = ["Artifacts", "LibSSH2_jll", "Libdl", "MbedTLS_jll"] +uuid = "e37daf67-58a4-590a-8e99-b0245dd2ffc5" +version = "1.7.2+0" [[deps.LibSSH2_jll]] deps = ["Artifacts", "Libdl", "MbedTLS_jll"] uuid = "29816b5a-b9ab-546f-933c-edad1886dfa8" +version = "1.11.0+1" [[deps.Libdl]] uuid = "8f399da3-3557-5675-b5ff-fb832c97cbdb" +version = "1.11.0" [[deps.Logging]] uuid = "56ddb016-857b-54e1-b83d-db4d58db5568" +version = "1.11.0" [[deps.Markdown]] deps = ["Base64"] uuid = "d6f4376e-aef5-505a-96c1-9c027394607a" +version = "1.11.0" [[deps.MbedTLS_jll]] deps = ["Artifacts", "Libdl"] uuid = "c8ffd9c3-330d-5841-b78e-0817d7145fa1" +version = "2.28.6+0" [[deps.MozillaCACerts_jll]] uuid = "14a3606d-f60d-562e-9121-12d972cd8159" +version = "2023.12.12" [[deps.NetworkOptions]] uuid = "ca575930-c2e3-43a9-ace4-1e988b2c1908" +version = "1.2.0" + +[[deps.OrderedCollections]] +git-tree-sha1 = "cc4054e898b852042d7b503313f7ad03de99c3dd" +uuid = "bac558e1-5e72-5ebc-8fee-abe8a469f55d" +version = "1.8.0" [[deps.Pkg]] -deps = ["Artifacts", "Dates", "Downloads", "LibGit2", "Libdl", "Logging", "Markdown", "Printf", "REPL", "Random", "SHA", "Serialization", "TOML", "Tar", "UUIDs", "p7zip_jll"] +deps = ["Artifacts", "Dates", "Downloads", "FileWatching", "LibGit2", "Libdl", "Logging", "Markdown", "Printf", "Random", "SHA", "TOML", "Tar", "UUIDs", "p7zip_jll"] uuid = "44cfe95a-1eb2-52ea-b672-e2afdf69b78f" +version = "1.11.0" + + [deps.Pkg.extensions] + REPLExt = "REPL" + + [deps.Pkg.weakdeps] + REPL = "3fa0cd96-eef1-5676-8a61-b3b8758bbffb" + +[[deps.PrecompileTools]] +deps = ["Preferences"] +git-tree-sha1 = "5aa36f7049a63a1528fe8f7c3f2113413ffd4e1f" +uuid = "aea7be01-6a6a-4083-8856-8a6e6704d82a" +version = "1.2.1" [[deps.Preferences]] deps = ["TOML"] -git-tree-sha1 = "47e5f437cc0e7ef2ce8406ce1e7e24d44915f88d" +git-tree-sha1 = "9306f6085165d270f7e3db02af26a400d580f5c6" uuid = "21216c6a-2e73-6563-6e65-726566657250" -version = "1.3.0" +version = "1.4.3" [[deps.Printf]] deps = ["Unicode"] uuid = "de0858da-6303-5e67-8744-51eddeeeb8d7" - -[[deps.REPL]] -deps = ["InteractiveUtils", "Markdown", "Sockets", "Unicode"] -uuid = "3fa0cd96-eef1-5676-8a61-b3b8758bbffb" +version = "1.11.0" [[deps.Random]] -deps = ["SHA", "Serialization"] +deps = ["SHA"] uuid = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" +version = "1.11.0" [[deps.SHA]] uuid = "ea8e919c-243c-51af-8825-aaa63cd721ce" - -[[deps.Serialization]] -uuid = "9e88b42a-f829-5b0c-bbe9-9e923198166b" - -[[deps.Sockets]] -uuid = "6462fe0b-24de-5631-8697-dd941f90decc" +version = "0.7.0" [[deps.TOML]] deps = ["Dates"] uuid = "fa267f1f-6049-4f14-aa54-33bafae1ed76" +version = "1.0.3" [[deps.Tar]] deps = ["ArgTools", "SHA"] uuid = "a4e569a6-e804-4fa4-b0f3-eef7a1d5b13e" +version = "1.10.0" + +[[deps.Tokenize]] +git-tree-sha1 = "468b4685af4abe0e9fd4d7bf495a6554a6276e75" +uuid = "0796e94c-ce3b-5d07-9a54-7f471281c624" +version = "0.5.29" [[deps.UUIDs]] deps = ["Random", "SHA"] uuid = "cf7118a7-6976-5b1a-9a39-7adc72f591a4" +version = "1.11.0" [[deps.Unicode]] uuid = "4ec0a83e-493e-50e2-b9ac-8f72acf5a8f5" +version = "1.11.0" [[deps.Zlib_jll]] deps = ["Libdl"] uuid = "83775a58-1f1d-513f-b197-d71354ab007a" +version = "1.2.13+1" [[deps.libLLVM_jll]] deps = ["Artifacts", "Libdl"] uuid = "8f36deef-c2a5-5394-99ed-8e07531fb29a" +version = "16.0.6+4" [[deps.nghttp2_jll]] deps = ["Artifacts", "Libdl"] uuid = "8e850ede-7688-5339-a07c-302acd2aaf8d" +version = "1.59.0+0" [[deps.p7zip_jll]] deps = ["Artifacts", "Libdl"] uuid = "3f19e933-33d8-53b3-aaab-bd5110c3b7a0" +version = "17.4.0+2" [[deps.soapysdr_jll]] deps = ["Artifacts", "JLLWrappers", "Libdl", "Pkg"] -git-tree-sha1 = "361c7cc6e6f6eb5dad6830a888ccd9b90ea7f9fc" +git-tree-sha1 = "ebf95e4b34f8e76a5f0567f5ca61ead27def8a01" uuid = "343a40d9-ed99-5d34-8b56-649aaa4ecee6" -version = "0.8.1+0" +version = "0.8.1+1" diff --git a/gen/Project.toml b/gen/Project.toml index aaa37b0..da4ed20 100644 --- a/gen/Project.toml +++ b/gen/Project.toml @@ -1,7 +1,8 @@ [deps] Clang = "40e3b903-d033-50b4-a0cc-940c62c95e31" +JuliaFormatter = "98e50ef6-434e-11e9-1051-2b60c6c9e899" soapysdr_jll = "343a40d9-ed99-5d34-8b56-649aaa4ecee6" [compat] -Clang = "0.15" +Clang = "0.18" soapysdr_jll = "0.8" diff --git a/gen/README.md b/gen/README.md deleted file mode 100644 index 825ef20..0000000 --- a/gen/README.md +++ /dev/null @@ -1,12 +0,0 @@ -# SoapySDR Binding generation via Clang.jl - -Instructions: - -``` -] activate . -] instantiate -] up -include("gen.jl") -``` - -This generates the files in `src/lowlevel/
.h.jl`. Files in lowlevel without this extension are maintained by hand. \ No newline at end of file diff --git a/gen/gen.jl b/gen/gen.jl index 504ba30..a78d680 100755 --- a/gen/gen.jl +++ b/gen/gen.jl @@ -2,37 +2,63 @@ using soapysdr_jll +using Clang using Clang.Generators -include_dir = joinpath(soapysdr_jll.artifact_dir, "include") |> normpath -clang_dir = joinpath(include_dir, "clang-c") +using JuliaFormatter -options = load_options(joinpath(@__DIR__, "generator.toml")) +function rewriter!(ctx, options) + for node in get_nodes(ctx.dag) + if Generators.is_function(node) && !Generators.is_variadic_function(node) + expr = node.exprs[1] + fn = String(expr.args[1].args[1]) + ccall_expr = expr.args[2].args[1] # assumes `use_ccall_macro` is false -@show options + if startswith(fn, "SoapySDRDevice") && + !in(fn, ["SoapySDRDevice_lastStatus", "SoapySDRDevice_lastError"]) + # insert `@soapy_checked` before each function with a `ccall` + expr.args[2].args[1] = Expr(:macrocall, Symbol("@soapy_checked"), nothing, ccall_expr) + end + end + end +end -# add compiler flags, e.g. "-DXXXXXXXXX" -args = get_default_args() -push!(args, "-I$include_dir") -@show args +function main() + include_dir = joinpath(soapysdr_jll.artifact_dir, "include") |> normpath + clang_dir = joinpath(include_dir, "clang-c") -headers = [ - joinpath(include_dir, "SoapySDR", header) for - header in readdir(joinpath(include_dir, "SoapySDR")) if endswith(header, ".h") -] -@show headers -@show basename.(headers) -# there is also an experimental `detect_headers` function for auto-detecting top-level headers in the directory -# headers = detect_headers(clang_dir, args) + options = load_options(joinpath(@__DIR__, "generator.toml")) -filter!(s -> basename(s) ∉ ["Config.h"], headers) # Deivce is hand-wrapped and COnfig is not needed + # add compiler flags, e.g. "-DXXXXXXXXX" + args = get_default_args() + push!(args, "-I$include_dir") -# A few macros that don't translate well, and not applicable -options["general"]["output_ignorelist"] = ["SOAPY_SDR_API", "SOAPY_SDR_LOCAL"] + headers = [ + joinpath(include_dir, "SoapySDR", header) for + header in readdir(joinpath(include_dir, "SoapySDR")) if endswith(header, ".h") + ] + filter!(s -> basename(s) ∉ ["Config.h"], headers) -# create context -@show options -ctx = create_context(headers, args, options) + # create context + ctx = create_context(headers, args, options) -# run generator -build!(ctx) + # run generator + build!(ctx, BUILDSTAGE_NO_PRINTING) + rewriter!(ctx, options) + build!(ctx, BUILDSTAGE_PRINTING_ONLY) + + output_file = options["general"]["output_file_path"] + + # prepend "autogenerated, do not edit!" comment + output_data = read(output_file, String) + open(output_file, "w") do io + println(io, """# This file is automatically generated. Do not edit! + # To re-generate, execute gen/gen.jl""") + println(io) + print(io, output_data) + end + + format_file(output_file) +end + +isinteractive() || main() diff --git a/gen/generator.toml b/gen/generator.toml index 0a7e29e..4916712 100644 --- a/gen/generator.toml +++ b/gen/generator.toml @@ -1,6 +1,11 @@ [general] library_name = "soapysdr" -output_file_path = "../src/lowlevel/auto_wrap.jl" +output_file_path = "../src/libsoapysdr.jl" extract_c_comment_style = "raw" wrap_variadic_function = true -print_using_CEnum = false \ No newline at end of file +print_using_CEnum = false + +output_ignorelist = [ + "SOAPY_SDR_API", + "SOAPY_SDR_LOCAL" +] diff --git a/src/SoapySDR.jl b/src/SoapySDR.jl index 4f3bc6a..bb61588 100644 --- a/src/SoapySDR.jl +++ b/src/SoapySDR.jl @@ -1,8 +1,8 @@ module SoapySDR using soapysdr_jll -const lib = soapysdr_jll.libsoapysdr const soapysdr = soapysdr_jll.libsoapysdr + using CEnum using Intervals @@ -13,34 +13,28 @@ const GC = Base.GC export @u_str -include("error.jl") -include("lowlevel/auto_wrap.jl") -include("unithelpers.jl") -include("typemap.jl") -include("typewrappers.jl") -include("highlevel.jl") -include("functionwraps.jl") +# auto-generated C library wrappers +include("errors.jl") +include("libsoapysdr.jl") + +# Julia wrappers +include("constants.jl") +include("types.jl") include("logger.jl") include("version.jl") -include("Modules.jl") - -const SDRStream = Stream - -export SDRStream, - SOAPY_SDR_TX, - SOAPY_SDR_RX, - SOAPY_SDR_END_BURST, - SOAPY_SDR_HAS_TIME, - SOAPY_SDR_END_ABRUPT, - SOAPY_SDR_ONE_PACKET, - SOAPY_SDR_MORE_FRAGMENTS, - SOAPY_SDR_WAIT_TRIGGER, - SOAPY_SDR_TIMEOUT, - SOAPY_SDR_STREAM_ERROR, - SOAPY_SDR_CORRUPTION, - SOAPY_SDR_OVERFLOW, - SOAPY_SDR_NOT_SUPPORTED, - SOAPY_SDR_TIME_ERROR, - SOAPY_SDR_UNDERFLOW +include("modules.jl") +include("device.jl") +include("device/channel.jl") +include("device/stream.jl") +include("device/sensor.jl") +include("device/time.jl") + +# helpers +include("typemap.jl") +include("unithelpers.jl") +include("wrappers.jl") # TODO: get rid of this + +# high-level functionality +include("components.jl") end diff --git a/src/components.jl b/src/components.jl new file mode 100644 index 0000000..8dbdebd --- /dev/null +++ b/src/components.jl @@ -0,0 +1,249 @@ +# High level API exports + +export dB, gainrange + +# Components is an internal mechaism to allow for dispatch and interface through the Julia API +# For example there may be several GainElements we list in a Channel. A Julian idiom for this is +# the set/getindex class of functions. + +abstract type AbstractComponent end +Base.print(io::IO, c::AbstractComponent) = print(io, c.name) +Base.convert(::Type{T}, s::Symbol) where {T<:AbstractComponent} = T(s) +Base.convert(::Type{T}, s::String) where {T<:AbstractComponent} = T(Symbol(s)) +Base.convert(::Type{Cstring}, s::AbstractComponent) = + Cstring(unsafe_convert(Ptr{UInt8}, s.name)) + +for e in ( + :TimeSource, + :ClockSource, + :GainElement, + :Antenna, + :FrequencyComponent, + :SensorComponent, + :Setting, + :Register, + :UART, + :GPIO, +) + @eval begin + struct $e <: AbstractComponent + name::Symbol + end + function $e(s::AbstractString) + $e(Symbol(s)) + end + end +end + +struct ComponentList{T<:AbstractComponent} <: AbstractVector{T} + s::StringList +end +Base.size(l::ComponentList) = (length(l.s),) +Base.getindex(l::ComponentList{T}, i::Integer) where {T} = T(Symbol(l.s[i])) +function Base.show(io::IO, l::ComponentList{T}) where {T} + print(io, T, "[") + for c in l + print(io, c, ",") + end + print(io, "]") +end + +for (e, f) in zip( + (:Antenna, :GainElement, :FrequencyComponent, :SensorComponent), + ( + :SoapySDRDevice_listAntennas, + :SoapySDRDevice_listGains, + :SoapySDRDevice_listFrequencies, + :SoapySDRDevice_listSensors, + ), +) + @eval begin + $f(channel::Channel) = $f(channel.device, channel.direction, channel.idx) + $(Symbol(string(e, "List")))(channel::Channel) = + ComponentList{$e}(StringList($f(channel)...)) + end +end + +function ComponentList(::Type{T}, d::Device) where {T<:AbstractComponent} + if T <: SensorComponent + ComponentList{SensorComponent}(StringList(SoapySDRDevice_listSensors(d.ptr)...)) + elseif T <: TimeSource + ComponentList{TimeSource}(StringList(SoapySDRDevice_listTimeSources(d.ptr)...)) + elseif T <: ClockSource + ComponentList{ClockSource}(StringList(SoapySDRDevice_listClockSources(d.ptr)...)) + elseif T <: UART + ComponentList{UART}(StringList(SoapySDRDevice_listUARTs(d.ptr)...)) + elseif T <: GPIO + ComponentList{GPIO}(StringList(SoapySDRDevice_listGPIOBanks(d.ptr)...)) + elseif T <: Register + ComponentList{Register}(StringList(SoapySDRDevice_listRegisterInterfaces(d.ptr)...)) + end +end + +function ComponentList(::Type{T}, d::Device, c::Channel) where {T<:AbstractComponent} + len = Ref{Csize_t}() + if T <: SensorComponent + s = SoapySDRDevice_listChannelSensors(d.ptr, c.direction, c.idx, len) + ComponentList{SensorComponent}(StringList(s, len[])) + end +end + +using Base: unsafe_convert +function Base.getindex(c::Channel, ge::GainElement) + SoapySDRDevice_getGainElement(c.device, c.direction, c.idx, ge.name) * dB +end + +function Base.getindex(c::Channel, fe::FrequencyComponent) + SoapySDRDevice_getFrequencyComponent(c.device, c.direction, c.idx, fe.name) * Hz +end + +function Base.getindex(c::Channel, se::SensorComponent) + unsafe_string(SoapySDRDevice_readChannelSensor(c.device, c.direction, c.idx, se.name)) +end + +function Base.getindex(c::Channel, se::Setting) + unsafe_string(SoapySDRDevice_readChannelSetting(c.device, c.direction, c.idx, se.name)) +end + + +function Base.getindex(d::Device, se::SensorComponent) + unsafe_string(SoapySDRDevice_readSensor(d.ptr, se.name)) +end + +function Base.getindex(d::Device, se::Setting) + unsafe_string(SoapySDRDevice_readSetting(d.ptr, se.name)) +end + +function Base.getindex(d::Device, se::Tuple{Register,<:Integer}) + SoapySDRDevice_readRegister(d.ptr, se[1].name, se[2]) +end + +function Base.setindex!(c::Channel, gain, ge::GainElement) + SoapySDRDevice_setGainElement(c.device, c.direction, c.idx, ge.name, gain.val) + return gain +end + +function Base.setindex!(c::Channel, frequency, ge::FrequencyComponent) + SoapySDRDevice_setFrequencyComponent( + c.device, + c.direction, + c.idx, + ge.name, + uconvert(u"Hz", frequency).val, + C_NULL, + ) + return frequency +end + +function Base.setindex!(d::Device, v::AbstractString, ge::Setting) + SoapySDRDevice_writeSetting(d, ge.name, v) + return v +end + +function Base.setindex!(c::Channel, v::AbstractString, se::Setting) + SoapySDRDevice_writeChannelSetting(c.device, c.direction, c.idx, se.name, v) +end + +""" +Set a register value on a device: + +``` +dev[SoapySDR.Register("LMS7002M")] = (0x1234, 0x5678) # tuple of: (addr, value) +dev[(SoapySDR.Register("LMS7002M"), 0x1234)] = 0x5678 # this is also equivalent, and symmetric to the getindex form to read +``` +""" +function Base.setindex!(d::Device, val::Tuple{<:Integer,<:Integer}, se::Register) + SoapySDRDevice_writeRegister(d.ptr, se.name, val[1], val[2]) +end + +function Base.setindex!(d::Device, val::Integer, se::Tuple{Register,<:Integer}) + SoapySDRDevice_writeRegister(d.ptr, se[1].name, se[2], val) +end + + +## GainElement + +function _gainrange(soapyr::SoapySDRRange) + if soapyr.step == 0.0 + # Represents an interval rather than a range + return (soapyr.minimum * dB) .. (soapyr.maximum * dB) + end + # TODO + @warn "Step ranges are not supported for gain elements. Returning Interval instead. Step: $(soapyr.step*dB)" + #return range(soapyr.minimum*dB; stop=soapyr.maximum*dB, step=soapyr.step*dB) + return (soapyr.minimum * dB) .. (soapyr.maximum * dB) +end + +function gainrange(c::Channel) + return _gainrange(SoapySDRDevice_getGainRange(c.device, c.direction, c.idx)) +end + +gainrange(c::Channel, ge::GainElement) = return _gainrange( + SoapySDRDevice_getGainElementRange(c.device, c.direction, c.idx, ge.name), +) + + +function _hzrange(soapyr::SoapySDRRange) + if soapyr.step == 0.0 + # Represents an interval rather than a range + return (soapyr.minimum * Hz) .. (soapyr.maximum * Hz) + end + return range(soapyr.minimum * Hz; stop = soapyr.maximum * Hz, step = soapyr.step * Hz) +end + +function bandwidth_ranges(c::Channel) + (ptr, len) = SoapySDRDevice_getBandwidthRange(c.device.ptr, c.direction, c.idx) + ptr == C_NULL && return SoapySDRRange[] + arr = map(_hzrange, unsafe_wrap(Array, Ptr{SoapySDRRange}(ptr), (len,))) + SoapySDR_free(ptr) + arr +end + +function frequency_ranges(c::Channel) + (ptr, len) = SoapySDRDevice_getFrequencyRange(c.device.ptr, c.direction, c.idx) + ptr == C_NULL && return SoapySDRRange[] + arr = map(_hzrange, unsafe_wrap(Array, Ptr{SoapySDRRange}(ptr), (len,))) + SoapySDR_free(ptr) + arr +end + +function frequency_ranges(c::Channel, fe::FrequencyComponent) + (ptr, len) = + SoapySDRDevice_getFrequencyRangeComponent(c.device.ptr, c.direction, c.idx, fe.name) + ptr == C_NULL && return SoapySDRRange[] + arr = map(_hzrange, unsafe_wrap(Array, Ptr{SoapySDRRange}(ptr), (len,))) + SoapySDR_free(ptr) + arr +end + +function gain_range(c::Channel, ge::GainElement) + ptr = SoapySDRDevice_getGainElementRange(c.device.ptr, c.direction, c.idx, ge.name) + ptr +end + +function sample_rate_ranges(c::Channel) + (ptr, len) = SoapySDRDevice_getSampleRateRange(c.device.ptr, c.direction, c.idx) + ptr == C_NULL && return SoapySDRRange[] + arr = map(_hzrange, unsafe_wrap(Array, Ptr{SoapySDRRange}(ptr), (len,))) + SoapySDR_free(ptr) + arr +end + +""" + list_sample_rates(::Channel) + +List the natively supported sample rates for a given channel. +""" +function list_sample_rates(c::Channel) + len = Ref{Csize_t}(0) + ptr = SoapySDRDevice_listSampleRates(c.device.ptr, c.direction, c.idx, len) + arr = unsafe_wrap(Array, Ptr{Float64}(ptr), (len[],)) * Hz + SoapySDR_free(ptr) + arr +end + +### Frequency Setting +struct FreqSpec{T} + val::T + kwargs::Dict{Any,String} +end diff --git a/src/constants.jl b/src/constants.jl new file mode 100644 index 0000000..8bbcb10 --- /dev/null +++ b/src/constants.jl @@ -0,0 +1,18 @@ +export SOAPY_SDR_TX, + SOAPY_SDR_RX, + SOAPY_SDR_END_BURST, + SOAPY_SDR_HAS_TIME, + SOAPY_SDR_END_ABRUPT, + SOAPY_SDR_ONE_PACKET, + SOAPY_SDR_MORE_FRAGMENTS, + SOAPY_SDR_WAIT_TRIGGER, + SOAPY_SDR_TIMEOUT, + SOAPY_SDR_STREAM_ERROR, + SOAPY_SDR_CORRUPTION, + SOAPY_SDR_OVERFLOW, + SOAPY_SDR_NOT_SUPPORTED, + SOAPY_SDR_TIME_ERROR, + SOAPY_SDR_UNDERFLOW + +"""SOAPY_SDR_TX SOAPY_SDR_RX""" +@enum Direction Tx = SOAPY_SDR_TX Rx = SOAPY_SDR_RX diff --git a/src/device.jl b/src/device.jl new file mode 100644 index 0000000..bfd98b6 --- /dev/null +++ b/src/device.jl @@ -0,0 +1,196 @@ +export Devices, Device + +""" + Devices() + Device(args...) + +Enumerates all detectable SDR devices on the system. +Indexing into the returned `Devices` object returns a list of +keywords used to create a `Device` struct. + +Optionally pass in a list of keywords to filter the returned list. +Example: + +``` +Devices(driver="rtlsdr") +``` +""" +struct Devices + kwargslist::KWArgsList + function Devices(args) + len = Ref{Csize_t}() + kwargs = SoapySDRDevice_enumerate(isnothing(args) ? C_NULL : args, len) + kwargs = KWArgsList(kwargs, len[]) + if isempty(kwargs) + @warn "No devices available! Make sure a supported SDR module is included." + end + new(kwargs) + end +end +Base.length(d::Devices) = length(d.kwargslist) + +function Devices(; kwargs...) + Devices(KWArgs(kwargs)) +end + +function Base.show(io::IO, d::Devices) + if length(d) == 0 + println(io, "No devices available! Make sure a supported SDR module is included.") + end + for (i, dev) in enumerate(d.kwargslist) + print(io, "[$i] ") + join(io, dev, ", ") + println(io) + end +end + +Base.getindex(d::Devices, i::Integer) = d.kwargslist[i] +Base.iterate(d::Devices, state = 1) = state > length(d) ? nothing : (d[state], state + 1) + +#################################################################################################### +# Device +#################################################################################################### + +""" + Device + +A device is a collection of SDR channels, obtained from the `Devices()` list. + +Fields: +- `info` +- `driver` +- `hardware` +- `tx` +- `rx` +- `sensors` +- `time_source` +- `time_sources` +- `clock_source` +- `clock_sources` +- `frontendmapping_rx` +- `frontendmapping_tx` +""" +mutable struct Device + ptr::Ptr{SoapySDRDevice} + function Device(args::KWArgs) + dev_ptr = SoapySDRDevice_make(args) + if dev_ptr == C_NULL + throw(ArgumentError("Unable to open device!")) + end + this = new(dev_ptr) + finalizer(this) do this + this.ptr != C_NULL && SoapySDRDevice_unmake(this.ptr) + this.ptr = Ptr{SoapySDRDevice}(C_NULL) + end + return this + end +end + +function Device(f::Function, args::KWArgs) + dev = Device(args) + try + f(dev) + finally + finalize(dev) + end +end + +Base.cconvert(::Type{<:Ptr{SoapySDRDevice}}, d::Device) = d +Base.unsafe_convert(::Type{<:Ptr{SoapySDRDevice}}, d::Device) = d.ptr +Base.isopen(d::Device) = d.ptr != C_NULL + +function Base.show(io::IO, d::Device) + println(io, "SoapySDR ", d.hardware, " device") + println(io, " driver: ", d.driver) + println(io, " number of TX channels:", length(d.tx)) + println(io, " number of RX channels:", length(d.rx)) + println(io, " sensors: ", d.sensors) + println(io, " time_source: ", d.time_source) + println(io, " time_sources:", d.time_sources) + println(io, " clock_source: ", d.clock_source) + println(io, " clock_sources:", d.clock_sources) + println(io, " frontendmapping_rx: ", d.frontendmapping_rx) + println(io, " frontendmapping_tx: ", d.frontendmapping_tx) + println(io, " uarts: ", d.uarts) + println(io, " gpios: ", d.gpios) + println(io, " registers: ", d.registers) + print(io, " master_clock_rate: ") + print_unit(io, d.master_clock_rate) +end + +function Base.getproperty(d::Device, s::Symbol) + if s === :info + KWArgs(SoapySDRDevice_getHardwareInfo(d)) + elseif s === :driver + Symbol(unsafe_string(SoapySDRDevice_getDriverKey(d))) + elseif s === :hardware + isopen(d) || throw(InvalidStateException("Device is closed!", :closed)) + Symbol(unsafe_string(SoapySDRDevice_getHardwareKey(d))) + elseif s === :tx + ChannelList(d, Tx) + elseif s === :rx + ChannelList(d, Rx) + elseif s === :sensors + ComponentList(SensorComponent, d) + elseif s === :time_sources + ComponentList(TimeSource, d) + elseif s === :uarts + ComponentList(UART, d) + elseif s === :registers + ComponentList(Register, d) + elseif s === :gpios + ComponentList(GPIO, d) + elseif s === :time_source + TimeSource(Symbol(unsafe_string(SoapySDRDevice_getTimeSource(d.ptr)))) + elseif s === :clock_sources + ComponentList(ClockSource, d) + elseif s === :clock_source + ClockSource(Symbol(unsafe_string(SoapySDRDevice_getClockSource(d.ptr)))) + elseif s === :frontendmapping_tx + unsafe_string(SoapySDRDevice_getFrontendMapping(d, Tx)) + elseif s === :frontendmapping_rx + unsafe_string(SoapySDRDevice_getFrontendMapping(d, Rx)) + elseif s === :master_clock_rate + SoapySDRDevice_getMasterClockRate(d.ptr) * u"Hz" + else + getfield(d, s) + end +end + +function Base.setproperty!(c::Device, s::Symbol, v) + if s === :frontendmapping_tx + SoapySDRDevice_setFrontendMapping(c.ptr, Tx, v) + elseif s === :frontendmapping_rx + SoapySDRDevice_setFrontendMapping(c.ptr, Rx, v) + elseif s === :time_source + SoapySDRDevice_setTimeSource(c.ptr, string(v)) + elseif s === :clock_source + SoapySDRDevice_setClockSource(c.ptr, string(v)) + elseif s === :master_clock_rate + SoapySDRDevice_setMasterClockRate(c.ptr, v) + else + return setfield!(c, s, v) + end +end + +function Base.propertynames(::Device) + return ( + :ptr, + :info, + :driver, + :hardware, + :tx, + :rx, + :sensors, + :time_source, + :timesources, + :clock_source, + :clock_source, + :frontendmapping_rx, + :frontendmapping_tx, + :uarts, + :registers, + :gpios, + :master_clock_rate, + ) +end diff --git a/src/device/channel.jl b/src/device/channel.jl new file mode 100644 index 0000000..17aba54 --- /dev/null +++ b/src/device/channel.jl @@ -0,0 +1,275 @@ +""" + Channel + +A channel on the given `Device`. + +Note!!: A Channel can be created from a `Device` or extracted from a `ChannelList`. It should rarely +be necessary to create a Channel directly. + +Has the following properties: +- `device::Device` - `Device` to which the `Channel` belongs +- `direction` - either `Tx` or `Rx` +- `idx` - channel index used by Soapy +- `info` - channel info consiting of `KWArgs` +- `antenna` - antenna name +- `gain_mode` - Automatic Gain control, `true`, `false`, or `missing` +- `gain_elements` - list of `GainElements` of the channel +- `gain` - effective gain, distributed amongst the `GainElements` +- `dc_offset_mode` - Automatic DC offset mode, `true`, `false` or `missing` +- `dc_offset` - DC offset value +- `iq_balance_mode` - Automatic IQ balance mode, `true`, `false` or `missing` +- `iq_balance` - IQ balance value +- `frequency_correction` - frequency correction value +- `sample_rate` - sample rate +- `bandwidth` - bandwidth +- `frequency` - center frequency +- `fullduplex` - full duplex mode with other (TX/RX) channels +- `native_stream_format` - native stream format +- `stream_formats` - supported stream formats (converted by Soapy) +- `fullscale` - full scale value +- `sensors` - sensor list + + +## Reading and writing to Components + +gains, antennas, and sensors may consist of a chain or selectable subcomponets. +To set or read e.g. a sensors, one may use the following syntax: + +dev = Devices()[1] +cr = dev.rx[1] + +# read a sensor value +s1 = cr.sensors[1] +cr[s1] + +# read and set the gain element +g1 = cr.gain_elements[1] +cr[g1] +cr[g1] = 4*u"dB" + +# read and set the frequency component +f1 = cr.frequency_components[1] +cr[f1] +cr[f1] = 2.498*u"GHz" + +""" +struct Channel + device::Device + direction::Direction + idx::Int +end + +function Base.show(io::IO, c::Channel) + println(io, " antenna: ", c.antenna) + println(io, " antennas: ", c.antennas) + print(io, " bandwidth [ ") + join(io, map(x -> sprint(print_hz_range, x), bandwidth_ranges(c)), ", ") + println(io, " ]: ", pick_freq_unit(c.bandwidth)) + print(io, " frequency [ ") + join(io, map(x -> sprint(print_hz_range, x), frequency_ranges(c)), ", ") + println(io, " ]: ", pick_freq_unit(c.frequency)) + for element in FrequencyComponentList(c) + print(io, " ", element, " [ ") + join(io, map(x -> sprint(print_hz_range, x), frequency_ranges(c, element)), ", ") + println(io, " ]: ", pick_freq_unit(c[element])) + end + println(io, " gain_mode (AGC=true/false/missing): ", c.gain_mode) + println(io, " gain: ", c.gain) + println(io, " gain_elements:") + for element in c.gain_elements + println(io, " ", element, " [", gain_range(c, element), "]: ", c[element]) + end + println(io, " fullduplex: ", c.fullduplex) + println(io, " stream_formats: ", c.stream_formats) + println(io, " native_stream_format: ", c.native_stream_format) + println(io, " fullscale: ", c.fullscale) + println(io, " sensors: ", c.sensors) + print(io, " sample_rate [ ") + join(io, map(x -> sprint(print_hz_range, x), sample_rate_ranges(c)), ", ") + println(io, " ]: ", pick_freq_unit(c.sample_rate)) + println(io, " dc_offset_mode (true/false/missing): ", c.dc_offset_mode) + println(io, " dc_offset: ", c.dc_offset) + println(io, " iq_balance_mode (true/false/missing): ", c.iq_balance_mode) + println(io, " iq_balance: ", c.iq_balance) + fc = c.frequency_correction + println(io, " frequency_correction: ", fc, ismissing(fc) ? "" : " ppm") +end + +""" + ChannelList + +A grouping of channels on the Device. +Note: This should not be called directly, but rather through the Device.rx and Device.tx properties. +""" +struct ChannelList <: AbstractVector{Channel} + device::Device + direction::Direction +end + +function Base.size(cl::ChannelList) + (SoapySDRDevice_getNumChannels(cl.device, cl.direction),) +end + +function Base.getindex(cl::ChannelList, i::Integer) + checkbounds(cl, i) + Channel(cl.device, cl.direction, i - 1) +end + +function Base.getproperty(c::Channel, s::Symbol) + if s === :info + return KWArgs(SoapySDRDevice_getChannelInfo(c.device.ptr, c.direction, c.idx)) + elseif s === :antenna + ant = SoapySDRDevice_getAntenna(c.device.ptr, c.direction, c.idx) + return Antenna(Symbol(ant == C_NULL ? "" : unsafe_string(ant))) + elseif s === :antennas + return AntennaList(c) + elseif s === :gain + return SoapySDRDevice_getGain(c.device.ptr, c.direction, c.idx) * dB + elseif s === :gain_elements + return GainElementList(c) + elseif s === :dc_offset_mode + if !SoapySDRDevice_hasDCOffsetMode(c.device.ptr, c.direction, c.idx) + return missing + end + return SoapySDRDevice_getDCOffsetMode(c.device.ptr, c.direction, c.idx) + elseif s === :dc_offset + if !SoapySDRDevice_hasDCOffset(c.device.ptr, c.direction, c.idx) + return missing + end + i = Ref{Cdouble}(0) + q = Ref{Cdouble}(0) + SoapySDRDevice_getDCOffset(c.device.ptr, c.direction, c.idx, i, q) + return i[], q[] + elseif s === :iq_balance_mode + if !SoapySDRDevice_hasIQBalanceMode(c.device.ptr, c.direction, c.idx) + return missing + end + return SoapySDRDevice_getIQBalanceMode(c.device.ptr, c.direction, c.idx) + elseif s === :iq_balance + if !SoapySDRDevice_hasIQBalance(c.device.ptr, c.direction, c.idx) + return missing + end + i = Ref{Cdouble}(0) + q = Ref{Cdouble}(0) + SoapySDRDevice_getIQBalance(c.device.ptr, c.direction, c.idx, i, q) + return Complex(i[], q[]) + elseif s === :gain_mode + if !SoapySDRDevice_hasGainMode(c.device.ptr, c.direction, c.idx) + return missing + end + return SoapySDRDevice_getGainMode(c.device.ptr, c.direction, c.idx) + elseif s === :frequency_correction + if !SoapySDRDevice_hasFrequencyCorrection(c.device.ptr, c.direction, c.idx) + return missing + end + # TODO: ppm unit? + return SoapySDRDevice_getFrequencyCorrection(c.device.ptr, c.direction, c.idx) + elseif s === :sample_rate + return SoapySDRDevice_getSampleRate(c.device.ptr, c.direction, c.idx) * Hz + elseif s === :sensors + ComponentList(SensorComponent, c.device, c) + elseif s === :bandwidth + return SoapySDRDevice_getBandwidth(c.device.ptr, c.direction, c.idx) * Hz + elseif s === :frequency + return SoapySDRDevice_getFrequency(c.device.ptr, c.direction, c.idx) * Hz + elseif s === :fullduplex + return Bool(SoapySDRDevice_getFullDuplex(c.device.ptr, c.direction, c.idx)) + elseif s === :stream_formats + slist = + StringList(SoapySDRDevice_getStreamFormats(c.device.ptr, c.direction, c.idx)...) + return map(_stream_map_soapy2jl, slist) + elseif s === :native_stream_format + fmt, _ = SoapySDRDevice_getNativeStreamFormat(c.device.ptr, c.direction, c.idx) + return _stream_map_soapy2jl(fmt == C_NULL ? "" : unsafe_string(fmt)) + elseif s === :fullscale + _, fullscale = + SoapySDRDevice_getNativeStreamFormat(c.device.ptr, c.direction, c.idx) + return fullscale + elseif s === :frequency_components + return FrequencyComponentList(c) + else + return getfield(c, s) + end +end + +function Base.propertynames(::SoapySDR.Channel) + return ( + :device, + :direction, + :idx, + :info, + :antenna, + :antennas, + :gain_elements, + :gain, + :dc_offset_mode, + :dc_offset, + :iq_balance_mode, + :iq_balance, + :gain_mode, + :frequency_correction, + :frequency_components, + :sample_rate, + :bandwidth, + :frequency, + :fullduplex, + :native_stream_format, + :stream_formats, + :fullscale, + :sensors, + ) +end + +function Base.setproperty!(c::Channel, s::Symbol, v) + if s === :antenna + if v isa Antenna + SoapySDRDevice_setAntenna(c.device.ptr, c.direction, c.idx, v.name) + elseif v isa Symbol || v isa String + SoapySDRDevice_setAntenna(c.device.ptr, c.direction, c.idx, v) + else + throw(ArgumentError("antenna must be an Antenna or a Symbol")) + end + elseif s === :frequency + if isa(v, Quantity) + SoapySDRDevice_setFrequency( + c.device.ptr, + c.direction, + c.idx, + uconvert(u"Hz", v).val, + C_NULL, + ) + elseif isa(v, FreqSpec) + throw(ArgumentError("FreqSpec unsupported")) + else + throw( + ArgumentError( + "Frequency must be specified as either a Quantity or a FreqSpec!", + ), + ) + end + elseif s === :bandwidth + SoapySDRDevice_setBandwidth( + c.device.ptr, + c.direction, + c.idx, + uconvert(u"Hz", v).val, + ) + elseif s === :gain_mode + SoapySDRDevice_setGainMode(c.device.ptr, c.direction, c.idx, v) + elseif s === :gain + SoapySDRDevice_setGain(c.device.ptr, c.direction, c.idx, uconvert(u"dB", v).val) + elseif s === :sample_rate + SoapySDRDevice_setSampleRate( + c.device.ptr, + c.direction, + c.idx, + uconvert(u"Hz", v).val, + ) + elseif s === :dc_offset_mode + SoapySDRDevice_setDCOffsetMode(c.device.ptr, c.direction, c.idx, v) + elseif s === :iq_balance + SoapySDRDevice_setIQBalance(c.device.ptr, c.direction, c.idx, real(v), imag(v)) + else + throw(ArgumentError("Channel has no property: $s")) + end +end diff --git a/src/device/sensor.jl b/src/device/sensor.jl new file mode 100644 index 0000000..b365986 --- /dev/null +++ b/src/device/sensor.jl @@ -0,0 +1,10 @@ +""" + get_sensor_info(::Device, ::String) + +Read the sensor extracted from `list_sensors`. +Returns: the value as a string. +Note: Appropriate conversions need to be done by the user. +""" +function get_sensor_info(d::Device, name) + SoapySDRDevice_getSensorInfo(d.ptr, name) +end diff --git a/src/device/stream.jl b/src/device/stream.jl new file mode 100644 index 0000000..e93360d --- /dev/null +++ b/src/device/stream.jl @@ -0,0 +1,359 @@ +""" + SoapySDR.Stream(channels) + SoapySDR.Stream(::Type{T}, channels) + +Constructs a `Stream{T}` where `T` is the stream type of the device. If unspecified, +the native format will be used. + +Fields: +- nchannels - The number of channels in the stream +- mtu - The stream Maximum Transmission Unit +- num_direct_access_buffers - The numer of direct access buffers available in the stream + +## Example +``` +SoapySDR.Stream(Devices()[1].rx) +``` +""" +mutable struct Stream{T} + d::Device + nchannels::Int + ptr::Ptr{SoapySDRStream} + function Stream{T}(d::Device, nchannels, ptr::Ptr{SoapySDRStream}) where {T} + this = new{T}(d, Int(nchannels), ptr) + finalizer(this) do obj + isopen(d) || return + SoapySDRDevice_closeStream(d, obj.ptr) + obj.ptr = Ptr{SoapySDRStream}(C_NULL) + end + return this + end +end + +# XXX: why this export? +export SDRStream +const SDRStream = Stream + +Base.cconvert(::Type{<:Ptr{SoapySDRStream}}, s::Stream) = s +Base.unsafe_convert(::Type{<:Ptr{SoapySDRStream}}, s::Stream) = s.ptr +Base.isopen(s::Stream) = s.ptr != C_NULL && isopen(s.d) + +streamtype(::Stream{T}) where {T} = T + +function Base.setproperty!(c::Stream, s::Symbol, v) + return setfield!(c, s, v) +end + +function Base.propertynames(::Stream) + return (:d, :nchannels, :mtu, :num_direct_access_buffers) +end + +function Base.getproperty(stream::Stream, s::Symbol) + if s === :mtu + if !isopen(stream) + throw(InvalidStateException("Stream is closed!", :closed)) + end + SoapySDRDevice_getStreamMTU(stream.d.ptr, stream.ptr) + elseif s === :num_direct_access_buffers + if !isopen(stream) + throw(InvalidStateException("Stream is closed!", :closed)) + end + SoapySDRDevice_getNumDirectAccessBuffers(stream.d.ptr, stream.ptr) + else + return getfield(stream, s) + end +end + +function Base.show(io::IO, s::Stream) + print(io, "Stream on ", s.d.hardware) +end + +function Stream(format::Type, device::Device, direction::Direction; kwargs...) + Stream{T}( + device, + 1, + SoapySDRDevice_setupStream( + device, + direction, + string(format), + C_NULL, + 0, + KWArgs(kwargs), + ), + ) +end + +function Stream(format::Type, channels::AbstractVector{T}; kwargs...) where {T<:Channel} + soapy_format = _stream_map_jl2soapy(format) + isempty(channels) && error( + "Must specify at least one channel or use the device/direction constructor for automatic.", + ) + device = first(channels).device + direction = first(channels).direction + if !all(channels) do channel + channel.device == device && channel.direction == direction + end + throw(ArgumentError("Channels must agree on device and direction")) + end + Stream{format}( + device, + length(channels), + SoapySDRDevice_setupStream( + device, + direction, + soapy_format, + map(x -> x.idx, channels), + length(channels), + KWArgs(kwargs), + ), + ) +end + +function Stream(channels::AbstractVector{T}; kwargs...) where {T<:Channel} + native_format = promote_type(map(c -> c.native_stream_format, channels)...) + if native_format <: AbstractComplexInteger + @warn "$(string(native_format)) may be poorly supported, it is recommend to specify a different type with Stream(format::Type, channels)" + end + Stream(native_format, channels; kwargs...) +end + +function Stream(format::Type, channel::Channel; kwargs...) + Stream(format, [channel], kwargs...) +end + +function Stream(channel::Channel; kwargs...) + Stream([channel], kwargs...) +end + +function Stream(f::Function, args...; kwargs...) + stream = Stream(args...; kwargs...) + try + f(stream) + finally + finalize(stream) + end +end + +const SoapyStreamFlags = Dict( + "END_BURST" => SOAPY_SDR_END_BURST, + "HAS_TIME" => SOAPY_SDR_HAS_TIME, + "END_ABRUPT" => SOAPY_SDR_END_ABRUPT, + "ONE_PACKET" => SOAPY_SDR_ONE_PACKET, + "MORE_FRAGMENTS" => SOAPY_SDR_MORE_FRAGMENTS, + "WAIT_TRIGGER" => SOAPY_SDR_WAIT_TRIGGER, +) + +function flags_to_set(flags) + s = Set{String}() + for (name, val) in SoapyStreamFlags + if val & flags != 0 + push!(s, name) + end + end + return s +end + +function Base.read!( + ::Stream, + ::NTuple; + kwargs... +) + error("Buffers should be a Vector of Vectors rather than NTuple.") +end + +""" + read!(s::SoapySDR.Stream{T}, buffers::AbstractVector{AbstractVector{T}}; [timeout], [flags::Ref{Int}], [throw_error=false]) + +Read data from the device into the given buffers. +""" +function Base.read!( + s::Stream{T}, + buffers::AbstractVector{<:AbstractVector{T}}; + timeout = nothing, + flags::Union{Ref{Int},Nothing} = nothing, + throw_error::Bool = false, +) where {T} + t_start = time() + timeout === nothing && (timeout = 0.1u"s") # Default from SoapySDR upstream + timeout_s = uconvert(u"s", timeout).val + timeout_us = uconvert(u"μs", timeout).val + + total_nread = 0 + samples_to_read = length(first(buffers)) + if !all(length(b) == samples_to_read for b in buffers) + throw(ArgumentError("Buffers must all be same length!")) + end + GC.@preserve buffers while total_nread < samples_to_read + # collect list of pointers to pass to SoapySDR + buff_ptrs = pointer(map(b -> pointer(b, total_nread + 1), buffers)) + nread, out_flags, timens = SoapySDRDevice_readStream( + s.d, + s, + buff_ptrs, + samples_to_read - total_nread, + timeout_us, + ) + + if typeof(flags) <: Ref + flags[] |= out_flags + end + + if nread < 0 + if throw_error + throw(SoapySDRDeviceError(nread, error_to_string(nread))) + end + else + total_nread += nread + end + + if time() > t_start + timeout_s + # We've timed out, return early and warn. Something is probably wrong. + @warn( + "readStream timeout!", + timeout = timeout_s, + total_nread, + samples_to_read, + flags = join(flags_to_set(out_flags), ","), + ) + return buffers + end + end + return buffers +end + +""" + read(s::SoapySDR.Stream{T}, nb::Integer; [timeout]) + +Read at most `nb` samples from s +""" +function Base.read(s::Stream{T}, n::Integer; timeout = nothing) where {T} + bufs = [Vector{T}(undef, n) for _ in 1:s.nchannels] + read!(s, bufs; timeout) + bufs +end + +function activate!(s::Stream; flags = 0, timens = nothing, numElems = nothing) + if !isopen(s) + throw(InvalidStateException("stream is closed!", :closed)) + end + if timens === nothing + timens = 0 + else + timens = uconvert(u"ns", timens).val + end + if numElems === nothing + numElems = 0 + end + + SoapySDRDevice_activateStream(s.d, s, flags, timens, numElems) + return nothing +end + +function activate!(f::Function, streams::AbstractVector{<:Stream}; kwargs...) + activated_streams = Vector{SoapySDR.Stream}(undef, length(streams)) + try + for i in eachindex(streams) + s = streams[i] + activate!(s; kwargs...) + activated_streams[i] = s + end + f() + finally + for s in activated_streams + deactivate!(s; kwargs...) + end + end +end + +function activate!(f::Function, s::Stream; kwargs...) + try + activate!(s; kwargs...) + f() + finally + deactivate!(s; kwargs...) + end +end + +function deactivate!(s::Stream; flags = 0, timens = nothing) + isopen(s) || throw(InvalidStateException("Stream is closed!", :closed)) + SoapySDRDevice_deactivateStream( + s.d, + s, + flags, + timens === nothing ? 0 : uconvert(u"ns", timens).val, + ) + return nothing +end + +function Base.write( + ::Stream, + ::NTuple; + kwargs... +) + error("Buffers should be a Vector of Vectors rather than NTuple.") +end + +""" + write(s::SoapySDR.Stream{T}, buffer::AbstractVector{AbstractVector{T}}; [timeout], [flags::Ref{Int}], [throw_error=false]) where {N, T} + +Write data from the given buffers into the device. The buffers must all be the same length. +""" +function Base.write( + s::Stream{T}, + buffers::AbstractVector{<:AbstractVector{T}}; + timeout = nothing, + flags::Union{Ref{Int},Nothing} = nothing, + throw_error::Bool = false, +) where {T} + t_start = time() + timeout === nothing && (timeout = 0.1u"s") # Default from SoapySDR upstream + timeout_s = uconvert(u"s", timeout).val + timeout_us = uconvert(u"μs", timeout).val + + total_nwritten = 0 + samples_to_write = length(first(buffers)) + if !all(length(b) == samples_to_write for b in buffers) + throw(ArgumentError("Buffers must all be same length!")) + end + if length(buffers) != s.nchannels + throw(ArgumentError("Must provide buffers for every channel in stream!")) + end + + GC.@preserve buffers while total_nwritten < samples_to_write + buff_ptrs = pointer(map(b -> pointer(b, total_nwritten + 1), buffers)) + nwritten, out_flags = SoapySDRDevice_writeStream( + s.d, + s, + buff_ptrs, + samples_to_write - total_nwritten, + 0, + 0, + timeout_us, + ) + + if typeof(flags) <: Ref + flags[] |= out_flags + end + + if nwritten < 0 + if throw_error + throw(SoapySDRDeviceError(nwritten, error_to_string(nwritten))) + end + else + total_nwritten += nwritten + end + + if time() > t_start + timeout_s + # We've timed out, return early and warn. Something is probably wrong. + @warn( + "writeStream timeout!", + timeout = timeout_s, + total_nwritten, + samples_to_write, + flags = join(flags_to_set(out_flags), ","), + ) + return buffers + end + end + return buffers +end diff --git a/src/device/time.jl b/src/device/time.jl new file mode 100644 index 0000000..3f089dd --- /dev/null +++ b/src/device/time.jl @@ -0,0 +1,26 @@ +""" + has_hardware_time(::Device, what::String) + +Query if the Device has hardware time for the given source. +""" +function has_hardware_time(d::Device, what::String) + SoapySDRDevice_hasHardwareTime(d.ptr, what) +end + +""" + get_hardware_time(::Device, what::String) + +Get hardware time for the given source. +""" +function get_hardware_time(d::Device, what::String) + SoapySDRDevice_getHardwareTime(d.ptr, what) +end + +""" + set_hardware_time(::Device, timeNs::Int64 what::String) + +Set hardware time for the given source. +""" +function set_hardware_time(d::Device, timeNs::Int64, what::String) + SoapySDRDevice_setHardwareTime(d.ptr, timeNs, what) +end diff --git a/src/error.jl b/src/errors.jl similarity index 100% rename from src/error.jl rename to src/errors.jl diff --git a/src/highlevel.jl b/src/highlevel.jl deleted file mode 100644 index 7a25186..0000000 --- a/src/highlevel.jl +++ /dev/null @@ -1,1131 +0,0 @@ -# High level API exports - -export Devices, Device, dB, gainrange - -""" - Devices() - Device(args...) - -Enumerates all detectable SDR devices on the system. -Indexing into the returned `Devices` object returns a list of -keywords used to create a `Device` struct. - -Optionally pass in a list of keywords to filter the returned list. -Example: - -``` -Devices(driver="rtlsdr") -``` -""" -struct Devices - kwargslist::KWArgsList - function Devices(args) - len = Ref{Csize_t}() - kwargs = SoapySDRDevice_enumerate(isnothing(args) ? C_NULL : args, len) - kwargs = KWArgsList(kwargs, len[]) - if isempty(kwargs) - @warn "No devices available! Make sure a supported SDR module is included." - end - new(kwargs) - end -end -Base.length(d::Devices) = length(d.kwargslist) - -function Devices(; kwargs...) - Devices(KWArgs(kwargs)) -end - -function Base.show(io::IO, d::Devices) - if length(d) == 0 - println(io, "No devices available! Make sure a supported SDR module is included.") - end - for (i, dev) in enumerate(d.kwargslist) - print(io, "[$i] ") - join(io, dev, ", ") - println(io) - end -end - -Base.getindex(d::Devices, i::Integer) = d.kwargslist[i] -Base.iterate(d::Devices, state = 1) = state > length(d) ? nothing : (d[state], state + 1) - -#################################################################################################### -# Device -#################################################################################################### - -""" - Device - -A device is a collection of SDR channels, obtained from the `Devices()` list. - -Fields: -- `info` -- `driver` -- `hardware` -- `tx` -- `rx` -- `sensors` -- `time_source` -- `time_sources` -- `clock_source` -- `clock_sources` -- `frontendmapping_rx` -- `frontendmapping_tx` -""" -mutable struct Device - ptr::Ptr{SoapySDRDevice} - function Device(args::KWArgs) - dev_ptr = SoapySDRDevice_make(args) - if dev_ptr == C_NULL - throw(ArgumentError("Unable to open device!")) - end - this = new(dev_ptr) - finalizer(this) do this - this.ptr != C_NULL && SoapySDRDevice_unmake(this.ptr) - this.ptr = Ptr{SoapySDRDevice}(C_NULL) - end - return this - end -end - -function Device(f::Function, args::KWArgs) - dev = Device(args) - try - f(dev) - finally - finalize(dev) - end -end - -Base.cconvert(::Type{<:Ptr{SoapySDRDevice}}, d::Device) = d -Base.unsafe_convert(::Type{<:Ptr{SoapySDRDevice}}, d::Device) = d.ptr -Base.isopen(d::Device) = d.ptr != C_NULL - -function Base.show(io::IO, d::Device) - println(io, "SoapySDR ", d.hardware, " device") - println(io, " driver: ", d.driver) - println(io, " number of TX channels:", length(d.tx)) - println(io, " number of RX channels:", length(d.rx)) - println(io, " sensors: ", d.sensors) - println(io, " time_source: ", d.time_source) - println(io, " time_sources:", d.time_sources) - println(io, " clock_source: ", d.clock_source) - println(io, " clock_sources:", d.clock_sources) - println(io, " frontendmapping_rx: ", d.frontendmapping_rx) - println(io, " frontendmapping_tx: ", d.frontendmapping_tx) - println(io, " uarts: ", d.uarts) - println(io, " gpios: ", d.gpios) - println(io, " registers: ", d.registers) - print(io, " master_clock_rate: ") - print_unit(io, d.master_clock_rate) -end - -function Base.getproperty(d::Device, s::Symbol) - if s === :info - KWArgs(SoapySDRDevice_getHardwareInfo(d)) - elseif s === :driver - Symbol(unsafe_string(SoapySDRDevice_getDriverKey(d))) - elseif s === :hardware - Symbol(unsafe_string(SoapySDRDevice_getHardwareKey(d))) - elseif s === :tx - ChannelList(d, Tx) - elseif s === :rx - ChannelList(d, Rx) - elseif s === :sensors - ComponentList(SensorComponent, d) - elseif s === :time_sources - ComponentList(TimeSource, d) - elseif s === :uarts - ComponentList(UART, d) - elseif s === :registers - ComponentList(Register, d) - elseif s === :gpios - ComponentList(GPIO, d) - elseif s === :time_source - TimeSource(Symbol(unsafe_string(SoapySDRDevice_getTimeSource(d.ptr)))) - elseif s === :clock_sources - ComponentList(ClockSource, d) - elseif s === :clock_source - ClockSource(Symbol(unsafe_string(SoapySDRDevice_getClockSource(d.ptr)))) - elseif s === :frontendmapping_tx - unsafe_string(SoapySDRDevice_getFrontendMapping(d, Tx)) - elseif s === :frontendmapping_rx - unsafe_string(SoapySDRDevice_getFrontendMapping(d, Rx)) - elseif s === :master_clock_rate - SoapySDRDevice_getMasterClockRate(d.ptr) * u"Hz" - else - getfield(d, s) - end -end - -function Base.setproperty!(c::Device, s::Symbol, v) - if s === :frontendmapping_tx - SoapySDRDevice_setFrontendMapping(c.ptr, Tx, v) - elseif s === :frontendmapping_rx - SoapySDRDevice_setFrontendMapping(c.ptr, Rx, v) - elseif s === :time_source - SoapySDRDevice_setTimeSource(c.ptr, string(v)) - elseif s === :clock_source - SoapySDRDevice_setClockSource(c.ptr, string(v)) - elseif s === :master_clock_rate - SoapySDRDevice_setMasterClockRate(c.ptr, v) - else - return setfield!(c, s, v) - end -end - - -function Base.propertynames(::Device) - return ( - :ptr, - :info, - :driver, - :hardware, - :tx, - :rx, - :sensors, - :time_source, - :timesources, - :clock_source, - :clock_source, - :frontendmapping_rx, - :frontendmapping_tx, - :uarts, - :registers, - :gpios, - :master_clock_rate, - ) -end - -#################################################################################################### -# Channel -#################################################################################################### -""" - Channel - -A channel on the given `Device`. - -Note!!: A Channel can be created from a `Device` or extracted from a `ChannelList`. It should rarely -be necessary to create a Channel directly. - -Has the following properties: -- `device::Device` - `Device` to which the `Channel` belongs -- `direction` - either `Tx` or `Rx` -- `idx` - channel index used by Soapy -- `info` - channel info consiting of `KWArgs` -- `antenna` - antenna name -- `gain_mode` - Automatic Gain control, `true`, `false`, or `missing` -- `gain_elements` - list of `GainElements` of the channel -- `gain` - effective gain, distributed amongst the `GainElements` -- `dc_offset_mode` - Automatic DC offset mode, `true`, `false` or `missing` -- `dc_offset` - DC offset value -- `iq_balance_mode` - Automatic IQ balance mode, `true`, `false` or `missing` -- `iq_balance` - IQ balance value -- `frequency_correction` - frequency correction value -- `sample_rate` - sample rate -- `bandwidth` - bandwidth -- `frequency` - center frequency -- `fullduplex` - full duplex mode with other (TX/RX) channels -- `native_stream_format` - native stream format -- `stream_formats` - supported stream formats (converted by Soapy) -- `fullscale` - full scale value -- `sensors` - sensor list - - -## Reading and writing to Components - -gains, antennas, and sensors may consist of a chain or selectable subcomponets. -To set or read e.g. a sensors, one may use the following syntax: - -dev = Devices()[1] -cr = dev.rx[1] - -# read a sensor value -s1 = cr.sensors[1] -cr[s1] - -# read and set the gain element -g1 = cr.gain_elements[1] -cr[g1] -cr[g1] = 4*u"dB" - -# read and set the frequency component -f1 = cr.frequency_components[1] -cr[f1] -cr[f1] = 2.498*u"GHz" - -""" -struct Channel - device::Device - direction::Direction - idx::Int -end - -function Base.show(io::IO, c::Channel) - println(io, " antenna: ", c.antenna) - println(io, " antennas: ", c.antennas) - print(io, " bandwidth [ ") - join(io, map(x -> sprint(print_hz_range, x), bandwidth_ranges(c)), ", ") - println(io, " ]: ", pick_freq_unit(c.bandwidth)) - print(io, " frequency [ ") - join(io, map(x -> sprint(print_hz_range, x), frequency_ranges(c)), ", ") - println(io, " ]: ", pick_freq_unit(c.frequency)) - for element in FrequencyComponentList(c) - print(io, " ", element, " [ ") - join(io, map(x -> sprint(print_hz_range, x), frequency_ranges(c, element)), ", ") - println(io, " ]: ", pick_freq_unit(c[element])) - end - println(io, " gain_mode (AGC=true/false/missing): ", c.gain_mode) - println(io, " gain: ", c.gain) - println(io, " gain_elements:") - for element in c.gain_elements - println(io, " ", element, " [", gain_range(c, element), "]: ", c[element]) - end - println(io, " fullduplex: ", c.fullduplex) - println(io, " stream_formats: ", c.stream_formats) - println(io, " native_stream_format: ", c.native_stream_format) - println(io, " fullscale: ", c.fullscale) - println(io, " sensors: ", c.sensors) - print(io, " sample_rate [ ") - join(io, map(x -> sprint(print_hz_range, x), sample_rate_ranges(c)), ", ") - println(io, " ]: ", pick_freq_unit(c.sample_rate)) - println(io, " dc_offset_mode (true/false/missing): ", c.dc_offset_mode) - println(io, " dc_offset: ", c.dc_offset) - println(io, " iq_balance_mode (true/false/missing): ", c.iq_balance_mode) - println(io, " iq_balance: ", c.iq_balance) - fc = c.frequency_correction - println(io, " frequency_correction: ", fc, ismissing(fc) ? "" : " ppm") -end - -""" - ChannelList - -A grouping of channels on the Device. -Note: This should not be called directly, but rather through the Device.rx and Device.tx properties. -""" -struct ChannelList <: AbstractVector{Channel} - device::Device - direction::Direction -end - -function Base.size(cl::ChannelList) - (SoapySDRDevice_getNumChannels(cl.device, cl.direction),) -end - -function Base.getindex(cl::ChannelList, i::Integer) - checkbounds(cl, i) - Channel(cl.device, cl.direction, i - 1) -end - -function Base.getproperty(c::Channel, s::Symbol) - if s === :info - return KWArgs(SoapySDRDevice_getChannelInfo(c.device.ptr, c.direction, c.idx)) - elseif s === :antenna - ant = SoapySDRDevice_getAntenna(c.device.ptr, c.direction, c.idx) - return Antenna(Symbol(ant == C_NULL ? "" : unsafe_string(ant))) - elseif s === :antennas - return AntennaList(c) - elseif s === :gain - return SoapySDRDevice_getGain(c.device.ptr, c.direction, c.idx) * dB - elseif s === :gain_elements - return GainElementList(c) - elseif s === :dc_offset_mode - if !SoapySDRDevice_hasDCOffsetMode(c.device.ptr, c.direction, c.idx) - return missing - end - return SoapySDRDevice_getDCOffsetMode(c.device.ptr, c.direction, c.idx) - elseif s === :dc_offset - if !SoapySDRDevice_hasDCOffset(c.device.ptr, c.direction, c.idx) - return missing - end - i = Ref{Cdouble}(0) - q = Ref{Cdouble}(0) - SoapySDRDevice_getDCOffset(c.device.ptr, c.direction, c.idx, i, q) - return i[], q[] - elseif s === :iq_balance_mode - if !SoapySDRDevice_hasIQBalanceMode(c.device.ptr, c.direction, c.idx) - return missing - end - return SoapySDRDevice_getIQBalanceMode(c.device.ptr, c.direction, c.idx) - elseif s === :iq_balance - if !SoapySDRDevice_hasIQBalance(c.device.ptr, c.direction, c.idx) - return missing - end - i = Ref{Cdouble}(0) - q = Ref{Cdouble}(0) - SoapySDRDevice_getIQBalance(c.device.ptr, c.direction, c.idx, i, q) - return Complex(i[], q[]) - elseif s === :gain_mode - if !SoapySDRDevice_hasGainMode(c.device.ptr, c.direction, c.idx) - return missing - end - return SoapySDRDevice_getGainMode(c.device.ptr, c.direction, c.idx) - elseif s === :frequency_correction - if !SoapySDRDevice_hasFrequencyCorrection(c.device.ptr, c.direction, c.idx) - return missing - end - # TODO: ppm unit? - return SoapySDRDevice_getFrequencyCorrection(c.device.ptr, c.direction, c.idx) - elseif s === :sample_rate - return SoapySDRDevice_getSampleRate(c.device.ptr, c.direction, c.idx) * Hz - elseif s === :sensors - ComponentList(SensorComponent, c.device, c) - elseif s === :bandwidth - return SoapySDRDevice_getBandwidth(c.device.ptr, c.direction, c.idx) * Hz - elseif s === :frequency - return SoapySDRDevice_getFrequency(c.device.ptr, c.direction, c.idx) * Hz - elseif s === :fullduplex - return Bool(SoapySDRDevice_getFullDuplex(c.device.ptr, c.direction, c.idx)) - elseif s === :stream_formats - slist = - StringList(SoapySDRDevice_getStreamFormats(c.device.ptr, c.direction, c.idx)...) - return map(_stream_map_soapy2jl, slist) - elseif s === :native_stream_format - fmt, _ = SoapySDRDevice_getNativeStreamFormat(c.device.ptr, c.direction, c.idx) - return _stream_map_soapy2jl(fmt == C_NULL ? "" : unsafe_string(fmt)) - elseif s === :fullscale - _, fullscale = - SoapySDRDevice_getNativeStreamFormat(c.device.ptr, c.direction, c.idx) - return fullscale - elseif s === :frequency_components - return FrequencyComponentList(c) - else - return getfield(c, s) - end -end - -function Base.propertynames(::SoapySDR.Channel) - return ( - :device, - :direction, - :idx, - :info, - :antenna, - :antennas, - :gain_elements, - :gain, - :dc_offset_mode, - :dc_offset, - :iq_balance_mode, - :iq_balance, - :gain_mode, - :frequency_correction, - :frequency_components, - :sample_rate, - :bandwidth, - :frequency, - :fullduplex, - :native_stream_format, - :stream_formats, - :fullscale, - :sensors, - ) -end - -function Base.setproperty!(c::Channel, s::Symbol, v) - if s === :antenna - if v isa Antenna - SoapySDRDevice_setAntenna(c.device.ptr, c.direction, c.idx, v.name) - elseif v isa Symbol || v isa String - SoapySDRDevice_setAntenna(c.device.ptr, c.direction, c.idx, v) - else - throw(ArgumentError("antenna must be an Antenna or a Symbol")) - end - elseif s === :frequency - if isa(v, Quantity) - SoapySDRDevice_setFrequency( - c.device.ptr, - c.direction, - c.idx, - uconvert(u"Hz", v).val, - C_NULL, - ) - elseif isa(v, FreqSpec) - throw(ArgumentError("FreqSpec unsupported")) - else - throw( - ArgumentError( - "Frequency must be specified as either a Quantity or a FreqSpec!", - ), - ) - end - elseif s === :bandwidth - SoapySDRDevice_setBandwidth( - c.device.ptr, - c.direction, - c.idx, - uconvert(u"Hz", v).val, - ) - elseif s === :gain_mode - SoapySDRDevice_setGainMode(c.device.ptr, c.direction, c.idx, v) - elseif s === :gain - SoapySDRDevice_setGain(c.device.ptr, c.direction, c.idx, uconvert(u"dB", v).val) - elseif s === :sample_rate - SoapySDRDevice_setSampleRate( - c.device.ptr, - c.direction, - c.idx, - uconvert(u"Hz", v).val, - ) - elseif s === :dc_offset_mode - SoapySDRDevice_setDCOffsetMode(c.device.ptr, c.direction, c.idx, v) - elseif s === :iq_balance - SoapySDRDevice_setIQBalance(c.device.ptr, c.direction, c.idx, real(v), imag(v)) - else - throw(ArgumentError("Channel has no property: $s")) - end -end - -#################################################################################################### -# Components -#################################################################################################### - -# Components is an internal mechaism to allow for dispatch and interface through the Julia API -# For example there may be several GainElements we list in a Channel. A Julian idiom for this is -# the set/getindex class of functions. - -abstract type AbstractComponent end -Base.print(io::IO, c::AbstractComponent) = print(io, c.name) -Base.convert(::Type{T}, s::Symbol) where {T<:AbstractComponent} = T(s) -Base.convert(::Type{T}, s::String) where {T<:AbstractComponent} = T(Symbol(s)) -Base.convert(::Type{Cstring}, s::AbstractComponent) = - Cstring(unsafe_convert(Ptr{UInt8}, s.name)) - -for e in ( - :TimeSource, - :ClockSource, - :GainElement, - :Antenna, - :FrequencyComponent, - :SensorComponent, - :Setting, - :Register, - :UART, - :GPIO, -) - @eval begin - struct $e <: AbstractComponent - name::Symbol - end - function $e(s::AbstractString) - $e(Symbol(s)) - end - end -end - -struct ComponentList{T<:AbstractComponent} <: AbstractVector{T} - s::StringList -end -Base.size(l::ComponentList) = (length(l.s),) -Base.getindex(l::ComponentList{T}, i::Integer) where {T} = T(Symbol(l.s[i])) -function Base.show(io::IO, l::ComponentList{T}) where {T} - print(io, T, "[") - for c in l - print(io, c, ",") - end - print(io, "]") -end - -for (e, f) in zip( - (:Antenna, :GainElement, :FrequencyComponent, :SensorComponent), - ( - :SoapySDRDevice_listAntennas, - :SoapySDRDevice_listGains, - :SoapySDRDevice_listFrequencies, - :SoapySDRDevice_listSensors, - ), -) - @eval begin - $f(channel::Channel) = $f(channel.device, channel.direction, channel.idx) - $(Symbol(string(e, "List")))(channel::Channel) = - ComponentList{$e}(StringList($f(channel)...)) - end -end - -function ComponentList(::Type{T}, d::Device) where {T<:AbstractComponent} - if T <: SensorComponent - ComponentList{SensorComponent}(StringList(SoapySDRDevice_listSensors(d.ptr)...)) - elseif T <: TimeSource - ComponentList{TimeSource}(StringList(SoapySDRDevice_listTimeSources(d.ptr)...)) - elseif T <: ClockSource - ComponentList{ClockSource}(StringList(SoapySDRDevice_listClockSources(d.ptr)...)) - elseif T <: UART - ComponentList{UART}(StringList(SoapySDRDevice_listUARTs(d.ptr)...)) - elseif T <: GPIO - ComponentList{GPIO}(StringList(SoapySDRDevice_listGPIOBanks(d.ptr)...)) - elseif T <: Register - ComponentList{Register}(StringList(SoapySDRDevice_listRegisterInterfaces(d.ptr)...)) - end -end - -function ComponentList(::Type{T}, d::Device, c::Channel) where {T<:AbstractComponent} - len = Ref{Csize_t}() - if T <: SensorComponent - s = SoapySDRDevice_listChannelSensors(d.ptr, c.direction, c.idx, len) - ComponentList{SensorComponent}(StringList(s, len[])) - end -end - -using Base: unsafe_convert -function Base.getindex(c::Channel, ge::GainElement) - SoapySDRDevice_getGainElement(c.device, c.direction, c.idx, ge.name) * dB -end - -function Base.getindex(c::Channel, fe::FrequencyComponent) - SoapySDRDevice_getFrequencyComponent(c.device, c.direction, c.idx, fe.name) * Hz -end - -function Base.getindex(c::Channel, se::SensorComponent) - unsafe_string(SoapySDRDevice_readChannelSensor(c.device, c.direction, c.idx, se.name)) -end - -function Base.getindex(c::Channel, se::Setting) - unsafe_string(SoapySDRDevice_readChannelSetting(c.device, c.direction, c.idx, se.name)) -end - - -function Base.getindex(d::Device, se::SensorComponent) - unsafe_string(SoapySDRDevice_readSensor(d.ptr, se.name)) -end - -function Base.getindex(d::Device, se::Setting) - unsafe_string(SoapySDRDevice_readSetting(d.ptr, se.name)) -end - -function Base.getindex(d::Device, se::Tuple{Register,<:Integer}) - SoapySDRDevice_readRegister(d.ptr, se[1].name, se[2]) -end - -function Base.setindex!(c::Channel, gain, ge::GainElement) - SoapySDRDevice_setGainElement(c.device, c.direction, c.idx, ge.name, gain.val) - return gain -end - -function Base.setindex!(c::Channel, frequency, ge::FrequencyComponent) - SoapySDRDevice_setFrequencyComponent( - c.device, - c.direction, - c.idx, - ge.name, - uconvert(u"Hz", frequency).val, - C_NULL, - ) - return frequency -end - -function Base.setindex!(d::Device, v::AbstractString, ge::Setting) - SoapySDRDevice_writeSetting(d, ge.name, v) - return v -end - -function Base.setindex!(c::Channel, v::AbstractString, se::Setting) - SoapySDRDevice_writeChannelSetting(c.device, c.direction, c.idx, se.name, v) -end - -""" -Set a register value on a device: - -``` -dev[SoapySDR.Register("LMS7002M")] = (0x1234, 0x5678) # tuple of: (addr, value) -dev[(SoapySDR.Register("LMS7002M"), 0x1234)] = 0x5678 # this is also equivalent, and symmetric to the getindex form to read -``` -""" -function Base.setindex!(d::Device, val::Tuple{<:Integer,<:Integer}, se::Register) - SoapySDRDevice_writeRegister(d.ptr, se.name, val[1], val[2]) -end - -function Base.setindex!(d::Device, val::Integer, se::Tuple{Register,<:Integer}) - SoapySDRDevice_writeRegister(d.ptr, se[1].name, se[2], val) -end - - -## GainElement - -function _gainrange(soapyr::SoapySDRRange) - if soapyr.step == 0.0 - # Represents an interval rather than a range - return (soapyr.minimum * dB) .. (soapyr.maximum * dB) - end - # TODO - @warn "Step ranges are not supported for gain elements. Returning Interval instead. Step: $(soapyr.step*dB)" - #return range(soapyr.minimum*dB; stop=soapyr.maximum*dB, step=soapyr.step*dB) - return (soapyr.minimum * dB) .. (soapyr.maximum * dB) -end - -function gainrange(c::Channel) - return _gainrange(SoapySDRDevice_getGainRange(c.device, c.direction, c.idx)) -end - -gainrange(c::Channel, ge::GainElement) = return _gainrange( - SoapySDRDevice_getGainElementRange(c.device, c.direction, c.idx, ge.name), -) - - -function _hzrange(soapyr::SoapySDRRange) - if soapyr.step == 0.0 - # Represents an interval rather than a range - return (soapyr.minimum * Hz) .. (soapyr.maximum * Hz) - end - return range(soapyr.minimum * Hz; stop = soapyr.maximum * Hz, step = soapyr.step * Hz) -end - -function bandwidth_ranges(c::Channel) - (ptr, len) = SoapySDRDevice_getBandwidthRange(c.device.ptr, c.direction, c.idx) - ptr == C_NULL && return SoapySDRRange[] - arr = map(_hzrange, unsafe_wrap(Array, Ptr{SoapySDRRange}(ptr), (len,))) - SoapySDR_free(ptr) - arr -end - -function frequency_ranges(c::Channel) - (ptr, len) = SoapySDRDevice_getFrequencyRange(c.device.ptr, c.direction, c.idx) - ptr == C_NULL && return SoapySDRRange[] - arr = map(_hzrange, unsafe_wrap(Array, Ptr{SoapySDRRange}(ptr), (len,))) - SoapySDR_free(ptr) - arr -end - -function frequency_ranges(c::Channel, fe::FrequencyComponent) - (ptr, len) = - SoapySDRDevice_getFrequencyRangeComponent(c.device.ptr, c.direction, c.idx, fe.name) - ptr == C_NULL && return SoapySDRRange[] - arr = map(_hzrange, unsafe_wrap(Array, Ptr{SoapySDRRange}(ptr), (len,))) - SoapySDR_free(ptr) - arr -end - -function gain_range(c::Channel, ge::GainElement) - ptr = SoapySDRDevice_getGainElementRange(c.device.ptr, c.direction, c.idx, ge.name) - ptr -end - -function sample_rate_ranges(c::Channel) - (ptr, len) = SoapySDRDevice_getSampleRateRange(c.device.ptr, c.direction, c.idx) - ptr == C_NULL && return SoapySDRRange[] - arr = map(_hzrange, unsafe_wrap(Array, Ptr{SoapySDRRange}(ptr), (len,))) - SoapySDR_free(ptr) - arr -end - -""" - list_sample_rates(::Channel) - -List the natively supported sample rates for a given channel. -""" -function list_sample_rates(c::Channel) - len = Ref{Csize_t}(0) - ptr = SoapySDRDevice_listSampleRates(c.device.ptr, c.direction, c.idx, len) - arr = unsafe_wrap(Array, Ptr{Float64}(ptr), (len[],)) * Hz - SoapySDR_free(ptr) - arr -end - -### Frequency Setting -struct FreqSpec{T} - val::T - kwargs::Dict{Any,String} -end - -#################################################################################################### -# Stream -#################################################################################################### - -""" - SoapySDR.Stream(channels) - SoapySDR.Stream(::Type{T}, channels) - -Constructs a `Stream{T}` where `T` is the stream type of the device. If unspecified, -the native format will be used. - -Fields: -- nchannels - The number of channels in the stream -- mtu - The stream Maximum Transmission Unit -- num_direct_access_buffers - The numer of direct access buffers available in the stream - -## Example -``` -SoapySDR.Stream(Devices()[1].rx) -``` -""" -mutable struct Stream{T} - d::Device - nchannels::Int - ptr::Ptr{SoapySDRStream} - function Stream{T}(d::Device, nchannels, ptr::Ptr{SoapySDRStream}) where {T} - this = new{T}(d, Int(nchannels), ptr) - finalizer(this) do obj - isopen(d) || return - SoapySDRDevice_closeStream(d, obj.ptr) - obj.ptr = Ptr{SoapySDRStream}(C_NULL) - end - return this - end -end - -Base.cconvert(::Type{<:Ptr{SoapySDRStream}}, s::Stream) = s -Base.unsafe_convert(::Type{<:Ptr{SoapySDRStream}}, s::Stream) = s.ptr -Base.isopen(s::Stream) = s.ptr != C_NULL && isopen(s.d) - -streamtype(::Stream{T}) where {T} = T - -function Base.setproperty!(c::Stream, s::Symbol, v) - return setfield!(c, s, v) -end - -function Base.propertynames(::Stream) - return (:d, :nchannels, :mtu, :num_direct_access_buffers) -end - -function Base.getproperty(stream::Stream, s::Symbol) - if s === :mtu - if !isopen(stream) - throw(InvalidStateException("Stream is closed!", :closed)) - end - SoapySDRDevice_getStreamMTU(stream.d.ptr, stream.ptr) - elseif s === :num_direct_access_buffers - if !isopen(stream) - throw(InvalidStateException("Stream is closed!", :closed)) - end - SoapySDRDevice_getNumDirectAccessBuffers(stream.d.ptr, stream.ptr) - else - return getfield(stream, s) - end -end - -function Base.show(io::IO, s::Stream) - print(io, "Stream on ", s.d.hardware) -end - -function Stream(format::Type, device::Device, direction::Direction; kwargs...) - Stream{T}( - device, - 1, - SoapySDRDevice_setupStream( - device, - direction, - string(format), - C_NULL, - 0, - KWArgs(kwargs), - ), - ) -end - -function Stream(format::Type, channels::AbstractVector{T}; kwargs...) where {T<:Channel} - soapy_format = _stream_map_jl2soapy(format) - isempty(channels) && error( - "Must specify at least one channel or use the device/direction constructor for automatic.", - ) - device = first(channels).device - direction = first(channels).direction - if !all(channels) do channel - channel.device == device && channel.direction == direction - end - throw(ArgumentError("Channels must agree on device and direction")) - end - Stream{format}( - device, - length(channels), - SoapySDRDevice_setupStream( - device, - direction, - soapy_format, - map(x -> x.idx, channels), - length(channels), - KWArgs(kwargs), - ), - ) -end - -function Stream(channels::AbstractVector{T}; kwargs...) where {T<:Channel} - native_format = promote_type(map(c -> c.native_stream_format, channels)...) - if native_format <: AbstractComplexInteger - @warn "$(string(native_format)) may be poorly supported, it is recommend to specify a different type with Stream(format::Type, channels)" - end - Stream(native_format, channels; kwargs...) -end - -function Stream(format::Type, channel::Channel; kwargs...) - Stream(format, [channel], kwargs...) -end - -function Stream(channel::Channel; kwargs...) - Stream([channel], kwargs...) -end - -function Stream(f::Function, args...; kwargs...) - stream = Stream(args...; kwargs...) - try - f(stream) - finally - finalize(stream) - end -end - -const SoapyStreamFlags = Dict( - "END_BURST" => SOAPY_SDR_END_BURST, - "HAS_TIME" => SOAPY_SDR_HAS_TIME, - "END_ABRUPT" => SOAPY_SDR_END_ABRUPT, - "ONE_PACKET" => SOAPY_SDR_ONE_PACKET, - "MORE_FRAGMENTS" => SOAPY_SDR_MORE_FRAGMENTS, - "WAIT_TRIGGER" => SOAPY_SDR_WAIT_TRIGGER, -) - -function flags_to_set(flags) - s = Set{String}() - for (name, val) in SoapyStreamFlags - if val & flags != 0 - push!(s, name) - end - end - return s -end - -function Base.read!( - ::Stream, - ::NTuple; - kwargs... -) - error("Buffers should be a Vector of Vectors rather than NTuple.") -end - -""" - read!(s::SoapySDR.Stream{T}, buffers::AbstractVector{AbstractVector{T}}; [timeout], [flags::Ref{Int}], [throw_error=false]) - -Read data from the device into the given buffers. -""" -function Base.read!( - s::Stream{T}, - buffers::AbstractVector{<:AbstractVector{T}}; - timeout = nothing, - flags::Union{Ref{Int},Nothing} = nothing, - throw_error::Bool = false, -) where {T} - t_start = time() - timeout === nothing && (timeout = 0.1u"s") # Default from SoapySDR upstream - timeout_s = uconvert(u"s", timeout).val - timeout_us = uconvert(u"μs", timeout).val - - total_nread = 0 - samples_to_read = length(first(buffers)) - if !all(length(b) == samples_to_read for b in buffers) - throw(ArgumentError("Buffers must all be same length!")) - end - GC.@preserve buffers while total_nread < samples_to_read - # collect list of pointers to pass to SoapySDR - buff_ptrs = pointer(map(b -> pointer(b, total_nread + 1), buffers)) - nread, out_flags, timens = SoapySDRDevice_readStream( - s.d, - s, - buff_ptrs, - samples_to_read - total_nread, - timeout_us, - ) - - if typeof(flags) <: Ref - flags[] |= out_flags - end - - if nread < 0 - if throw_error - throw(SoapySDRDeviceError(nread, error_to_string(nread))) - end - else - total_nread += nread - end - - if time() > t_start + timeout_s - # We've timed out, return early and warn. Something is probably wrong. - @warn( - "readStream timeout!", - timeout = timeout_s, - total_nread, - samples_to_read, - flags = join(flags_to_set(out_flags), ","), - ) - return buffers - end - end - return buffers -end - -""" - read(s::SoapySDR.Stream{T}, nb::Integer; [timeout]) - -Read at most `nb` samples from s -""" -function Base.read(s::Stream{T}, n::Integer; timeout = nothing) where {T} - bufs = [Vector{T}(undef, n) for _ in 1:s.nchannels] - read!(s, bufs; timeout) - bufs -end - -function activate!(s::Stream; flags = 0, timens = nothing, numElems = nothing) - if !isopen(s) - throw(InvalidStateException("stream is closed!", :closed)) - end - if timens === nothing - timens = 0 - else - timens = uconvert(u"ns", timens).val - end - if numElems === nothing - numElems = 0 - end - - SoapySDRDevice_activateStream(s.d, s, flags, timens, numElems) - return nothing -end - -function activate!(f::Function, streams::AbstractVector{<:Stream}; kwargs...) - activated_streams = Vector{SoapySDR.Stream}(undef, length(streams)) - try - for i in eachindex(streams) - s = streams[i] - activate!(s; kwargs...) - activated_streams[i] = s - end - f() - finally - for s in activated_streams - deactivate!(s; kwargs...) - end - end -end - -function activate!(f::Function, s::Stream; kwargs...) - try - activate!(s; kwargs...) - f() - finally - deactivate!(s; kwargs...) - end -end - -function deactivate!(s::Stream; flags = 0, timens = nothing) - SoapySDRDevice_deactivateStream( - s.d, - s, - flags, - timens === nothing ? 0 : uconvert(u"ns", timens).val, - ) - return nothing -end - -function Base.write( - ::Stream, - ::NTuple; - kwargs... -) - error("Buffers should be a Vector of Vectors rather than NTuple.") -end - -""" - write(s::SoapySDR.Stream{T}, buffer::AbstractVector{AbstractVector{T}}; [timeout], [flags::Ref{Int}], [throw_error=false]) where {N, T} - -Write data from the given buffers into the device. The buffers must all be the same length. -""" -function Base.write( - s::Stream{T}, - buffers::AbstractVector{<:AbstractVector{T}}; - timeout = nothing, - flags::Union{Ref{Int},Nothing} = nothing, - throw_error::Bool = false, -) where {T} - t_start = time() - timeout === nothing && (timeout = 0.1u"s") # Default from SoapySDR upstream - timeout_s = uconvert(u"s", timeout).val - timeout_us = uconvert(u"μs", timeout).val - - total_nwritten = 0 - samples_to_write = length(first(buffers)) - if !all(length(b) == samples_to_write for b in buffers) - throw(ArgumentError("Buffers must all be same length!")) - end - if length(buffers) != s.nchannels - throw(ArgumentError("Must provide buffers for every channel in stream!")) - end - - GC.@preserve buffers while total_nwritten < samples_to_write - buff_ptrs = pointer(map(b -> pointer(b, total_nwritten + 1), buffers)) - nwritten, out_flags = SoapySDRDevice_writeStream( - s.d, - s, - buff_ptrs, - samples_to_write - total_nwritten, - 0, - 0, - timeout_us, - ) - - if typeof(flags) <: Ref - flags[] |= out_flags - end - - if nwritten < 0 - if throw_error - throw(SoapySDRDeviceError(nwritten, error_to_string(nwritten))) - end - else - total_nwritten += nwritten - end - - if time() > t_start + timeout_s - # We've timed out, return early and warn. Something is probably wrong. - @warn( - "writeStream timeout!", - timeout = timeout_s, - total_nwritten, - samples_to_write, - flags = join(flags_to_set(out_flags), ","), - ) - return buffers - end - end - return buffers -end - - - -""" - get_sensor_info(::Device, ::String) - -Read the sensor extracted from `list_sensors`. -Returns: the value as a string. -Note: Appropriate conversions need to be done by the user. -""" -function get_sensor_info(d::Device, name) - SoapySDRDevice_getSensorInfo(d.ptr, name) -end - - - -## Time API - - -""" - has_hardware_time(::Device, what::String) - -Query if the Device has hardware time for the given source. -""" -function has_hardware_time(d::Device, what::String) - SoapySDRDevice_hasHardwareTime(d.ptr, what) -end - -""" - get_hardware_time(::Device, what::String) - -Get hardware time for the given source. -""" -function get_hardware_time(d::Device, what::String) - SoapySDRDevice_getHardwareTime(d.ptr, what) -end - -""" - set_hardware_time(::Device, timeNs::Int64 what::String) - -Set hardware time for the given source. -""" -function set_hardware_time(d::Device, timeNs::Int64, what::String) - SoapySDRDevice_setHardwareTime(d.ptr, timeNs, what) -end diff --git a/src/lowlevel/auto_wrap.jl b/src/libsoapysdr.jl similarity index 99% rename from src/lowlevel/auto_wrap.jl rename to src/libsoapysdr.jl index c7f92b0..acbd449 100644 --- a/src/lowlevel/auto_wrap.jl +++ b/src/libsoapysdr.jl @@ -1,3 +1,6 @@ +# This file is automatically generated. Do not edit! +# To re-generate, execute gen/gen.jl + # typedef void ( * SoapySDRConverterFunction ) ( const void * , void * , const size_t , const double ) """ A typedef for declaring a ConverterFunction to be maintained in the ConverterRegistry. @@ -353,9 +356,6 @@ to optimize for the underlying hardware. \\param device a pointer to a device instance """ function SoapySDRDevice_getHardwareKey(device) - if !isopen(device) - throw(InvalidStateException("Device is closed!", :closed)) - end @soapy_checked ccall( (:SoapySDRDevice_getHardwareKey, soapysdr), Ptr{Cchar}, @@ -750,9 +750,6 @@ In this case, the implementation returns SOAPY_SDR_NOT_SUPPORTED. \\return 0 for success or error code on failure """ function SoapySDRDevice_deactivateStream(device, stream, flags, timeNs) - if !isopen(stream) - throw(InvalidStateException("Stream is closed!", :closed)) - end @soapy_checked ccall( (:SoapySDRDevice_deactivateStream, soapysdr), Cint, @@ -2196,7 +2193,7 @@ Get the range of available reference clock rates. \\return a list of clock rate ranges in Hz """ function SoapySDRDevice_getReferenceClockRates(device, length) - ccall( + @soapy_checked ccall( (:SoapySDRDevice_getReferenceClockRates, soapysdr), Ptr{SoapySDRRange}, (Ptr{SoapySDRDevice}, Ptr{Csize_t}), @@ -3547,8 +3544,6 @@ const SOAPY_SDR_S8 = "S8" const SOAPY_SDR_U8 = "U8" -const SOAPY_SDR_SSI = SOAPY_SDR_SSI - const SOAPY_SDR_TRUE = "true" const SOAPY_SDR_FALSE = "false" diff --git a/src/Modules.jl b/src/modules.jl similarity index 100% rename from src/Modules.jl rename to src/modules.jl diff --git a/src/typemap.jl b/src/typemap.jl index 842b13d..d1b961b 100644 --- a/src/typemap.jl +++ b/src/typemap.jl @@ -1,8 +1,5 @@ # Map of Soapy stream formats to Julia types -"""SOAPY_SDR_TX SOAPY_SDR_RX""" -@enum Direction Tx = 0 Rx = 1 - """ Abstract type denoting a Complex(U)Int(12/4) type. diff --git a/src/typewrappers.jl b/src/types.jl similarity index 100% rename from src/typewrappers.jl rename to src/types.jl diff --git a/src/functionwraps.jl b/src/wrappers.jl similarity index 77% rename from src/functionwraps.jl rename to src/wrappers.jl index c072972..4969e75 100644 --- a/src/functionwraps.jl +++ b/src/wrappers.jl @@ -97,13 +97,6 @@ function SoapySDRDevice_getSampleRateRange(device, direction, channel) end function SoapySDRDevice_acquireReadBuffer(device::Device, stream, buffs, timeoutUs = 100000) - #SOAPY_SDR_API int SoapySDRDevice_acquireReadBuffer(SoapySDRDevice *device, - # SoapySDRStream *stream, - # size_t *handle, - # const void **buffs, - # int *flags, - # long long *timeNs, - # const long timeoutUs); handle = Ref{Csize_t}() flags = Ref{Cint}(0) timeNs = Ref{Clonglong}(-1) @@ -128,11 +121,6 @@ function SoapySDRDevice_acquireWriteBuffer( buffs, timeoutUs = 100000, ) - #SOAPY_SDR_API int SoapySDRDevice_acquireWriteBuffer(SoapySDRDevice *device, - # SoapySDRStream *stream, - # size_t *handle, - # void **buffs, - # const long timeoutUs); handle = Ref{Csize_t}() if !isopen(stream) throw(InvalidStateException("stream is closed!", :closed)) @@ -149,16 +137,7 @@ function SoapySDRDevice_releaseWriteBuffer( flags = Ref{Cint}(0), timeNs = 0, ) - #SOAPY_SDR_API void SoapySDRDevice_releaseWriteBuffer(SoapySDRDevice *device, - # SoapySDRStream *stream, - # const size_t handle, - # const size_t numElems, - # int *flags, - # const long long timeNs); - ccall( - (:SoapySDRDevice_releaseWriteBuffer, lib), - Cvoid, - (Ptr{SoapySDRDevice}, Ptr{SoapySDRStream}, Csize_t, Csize_t, Ref{Cint}, Clonglong), + SoapySDRDevice_releaseWriteBuffer( device, stream, handle, @@ -175,18 +154,7 @@ function SoapySDRDevice_readStream(device::Device, stream, buffs, numElems, time end flags = Ref{Cint}() timeNs = Ref{Clonglong}() - nelems = ccall( - (:SoapySDRDevice_readStream, lib), - Cint, - ( - Ptr{SoapySDRDevice}, - Ptr{SoapySDRStream}, - Ptr{Cvoid}, - Csize_t, - Ptr{Cint}, - Ptr{Clonglong}, - Clong, - ), + nelems = SoapySDRDevice_readStream( device, stream, buffs, @@ -211,18 +179,7 @@ function SoapySDRDevice_writeStream( throw(InvalidStateException("stream is closed!", :closed)) end flags = Ref{Cint}(flags) - nelems = ccall( - (:SoapySDRDevice_writeStream, lib), - Cint, - ( - Ptr{SoapySDRDevice}, - Ptr{SoapySDRStream}, - Ptr{Cvoid}, - Csize_t, - Ptr{Cint}, - Clonglong, - Clong, - ), + nelems = SoapySDRDevice_writeStream( device, stream, buffs,