<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>K3XEC</title><link>https://k3xec.com/</link><description>Recent content on K3XEC</description><generator>Hugo -- gohugo.io</generator><language>en-us</language><lastBuildDate>Tue, 19 May 2026 11:00:00 -0500</lastBuildDate><atom:link href="https://k3xec.com/index.xml" rel="self" type="application/rss+xml"/><item><title>designing arf, an sdr iq encoding format 🐶</title><link>https://k3xec.com/arf/</link><pubDate>Wed, 15 Apr 2026 11:43:00 -0400</pubDate><guid>https://k3xec.com/arf/</guid><description>&lt;div class="hz-alert-ok">
Interested in future updates? Follow me on mastodon at
&lt;a href="https://soylent.green/@paul">@paul@soylent.green&lt;/a>. Posts about
&lt;code>hz.tools&lt;/code> will be tagged
&lt;a href="https://soylent.green/@paul/tagged/hztools">#hztools&lt;/a>.&lt;br />
&lt;br />
🐶 Want to jump right to the draft? I'll be maintaining ARF going forward at
&lt;a href="https://k3xec.com/draft-tagliamonte-arf-00.txt">/draft-tagliamonte-arf-00.txt&lt;/a>.
&lt;/div>
&lt;p>It&amp;rsquo;s true &amp;ndash; processing data from software defined radios can be a bit
&lt;a href="http://localhost:1313/packrat-processing-iq/">complex&lt;/a>
👈😏👈 &amp;ndash; which tends to keep all but the most grizzled experts and bravest
souls from playing with it. While I wouldn&amp;rsquo;t describe myself as either, I will
say that I&amp;rsquo;ve stuck with it for longer than most would have expected of me.
One of the biggest takeaways I have from my adventures with software defined
radio is that there&amp;rsquo;s a lot of cool crossover opportunity between RF and
nearly every other field of engineering.&lt;/p>
&lt;p>Fairly early on, I decided on a very light metadata scheme to track SDR
captures, called &lt;a href="https://k3xec.com/rfcap/">rfcap&lt;/a>. rfcap has withstood my test
of time, and I can go back to even my earliest captures and still make sense of
what they are &amp;ndash; IQ format, capture frequencies, sample rates, etc. A huge
part of this was the simplicity of the scheme (fixed-lengh header, byte-aligned
to supported capture formats), which made it roughly as easy to work with as a
raw file of IQ samples.&lt;/p>
&lt;p>However, rfcap has a number of downsides. It&amp;rsquo;s only a single, fixed-length
header. If the frequency of operation changed during the capture, that change
is not represented in the capture information. It&amp;rsquo;s not possible to easily
represent mulit-channel coherent IQ streams, and additional metadata is
condemned to adjacent text files.&lt;/p>
&lt;h1 id="arf-archive-of-rf">ARF (Archive of RF)&lt;/h1>
&lt;p>A few years ago, I needed to finally solve some of these shortcomings and tried
to see if a new format would stick. I sat down and wrote out my design goals
before I started figuring out what it looked like.&lt;/p>
&lt;p>First, whatever I come up with must be capable of being streamed and processed
while being streamed. This includes streaming across the network or merely
written to disk as it&amp;rsquo;s being created. No post-processing required. This is
mostly an artifact of how I&amp;rsquo;ve built all my tools and how I intereact with my
SDRs. I use them extensively over the network (both locally, as well
as remotely by friends across my &lt;a href="https://tpl.house">wider&lt;/a>
&lt;a href="https://notes.pault.ag/tpl/">lan&lt;/a>). This decision sometimes even
prompts me to do some &lt;a href="https://k3xec.com/sparky-rtlsdr/">crazy things&lt;/a> from time
to time.&lt;/p>
&lt;p>I need actual, real support for multiple IQ channels from my multi-channel SDRs
(Ettus, Kerberos/Kracken SDR, etc) for playing with things like
&lt;a href="https://k3xec.com/simulating-phased-arrays/">beamforming&lt;/a>.
My new format must be capable of storing
multiple streams in a single capture file, rather than a pile of files in
a directory (and hope they&amp;rsquo;re aligned).&lt;/p>
&lt;p>Finally, metadata must be capable of being stored in-band. The initial set of
metadata I needed to formalize in-stream were &lt;code>Frequency Changes&lt;/code> and
&lt;code>Discontinuities&lt;/code>. Since then, ARF has grown a few more.&lt;/p>
&lt;p>After getting all that down, I opted to start at what I thought the simplest
container would look like,
&lt;a href="https://en.wikipedia.org/wiki/Type%E2%80%93length%E2%80%93value">TLV&lt;/a>
(tag-length-value) encoded packets. This is a fairly well trodden path,
and used by a bunch of existing protocols
&lt;a href="https://datatracker.ietf.org/doc/html/rfc4880">we&lt;/a>
&lt;a href="https://datatracker.ietf.org/doc/html/rfc4253">all&lt;/a>
&lt;a href="https://en.wikipedia.org/wiki/ASN.1">know&lt;/a>
and
&lt;a href="https://datatracker.ietf.org/doc/html/rfc6184">love&lt;/a>.
Each ARF file (or stream) was a set of
encoded &amp;ldquo;packets&amp;rdquo; (sometimes called data units in other specs). This means that
unknown packet types may be skipped (since the length is included) and
additional data can be added after the existing fields without breaking
existing decoders.&lt;/p>
&lt;div class="hz-abi">
&lt;div type="u8" class="hz-abi-yellow hz-abi-2b">
&lt;a href="#tag-field">tag&lt;/a>
&lt;/div>
&lt;div type="u8" class="hz-abi-yellow hz-abi-2b">
&lt;a href="#flags">flags&lt;/a>
&lt;/div>
&lt;div type="u16" class="hz-abi-yellow hz-abi-4b">
length
&lt;/div>
&lt;div type="[]u8" class="hz-abi-yellow hz-abi-Nb">
value
&lt;/div>
&lt;/div>
&lt;div class="hz-alert-error">
&lt;b>Heads up!&lt;/b>
Once this is posted, I'm not super likely to update this page. Once this
goes out, the latest stable copy of the ARF spec is maintained at
&lt;a href="https://k3xec.com/draft-tagliamonte-arf-00.txt">draft-tagliamonte-arf-00.txt&lt;/a>.
This page may quickly become out of date, so if you're actually interested in
implementing this, I've put a lot of effort into making the draft
comprehensive, and I plan to maintain it as I edit the format.
&lt;/div>
&lt;p>Unlike a &amp;ldquo;traditional&amp;rdquo; TLV structure, I opted to add &amp;ldquo;flags&amp;rdquo; to the top-level
packet. This gives me a bit of wiggle room down the line, and gives me a
feature that I like from ASN.1 &amp;ndash; a &amp;ldquo;critical&amp;rdquo; bit. The critical bit indicates
that the packet must be understood fully by implementers, which allows future
backward incompatible changes by marking a new packet type as critical. This
would only really be done if something meaningfully changed the interpretation
of the backwards compatible data to follow.&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;td class="hz-1-6th">Flag&lt;/td>
&lt;td class="hz-5-6th">Description&lt;/td>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>&lt;td>0x01&lt;/td>&lt;td>Critical (tag must be understood)&lt;/td>&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>Within each Packet is a &lt;code>tag&lt;/code> field. This tag indicates how the contents of the
&lt;code>value&lt;/code> field should be interpreted.&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;td class="hz-1-6th">Tag ID&lt;/td>
&lt;td class="hz-5-6th">Description&lt;/td>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>&lt;td>0x01&lt;/td>&lt;td>&lt;a href="#header">Header&lt;/a>&lt;/td>&lt;/tr>
&lt;tr>&lt;td>0x02&lt;/td>&lt;td>&lt;a href="#stream-header">Stream Header&lt;/a>&lt;/td>&lt;/tr>
&lt;tr>&lt;td>0x03&lt;/td>&lt;td>&lt;a href="#samples">Samples&lt;/a>&lt;/td>&lt;/tr>
&lt;tr>&lt;td>0x04&lt;/td>&lt;td>&lt;a href="#frequency-change">Frequency Change&lt;/a>&lt;/td>&lt;/tr>
&lt;tr>&lt;td>0x05&lt;/td>&lt;td>Timing&lt;/td>&lt;/tr>
&lt;tr>&lt;td>0x06&lt;/td>&lt;td>&lt;a href="#discontinuity">Discontinuity&lt;/a>&lt;/td>&lt;/tr>
&lt;tr>&lt;td>0x07&lt;/td>&lt;td>&lt;a href="#location">Location&lt;/a>&lt;/td>&lt;/tr>
&lt;tr>&lt;td>0xFE&lt;/td>&lt;td>&lt;a href="#vendor-extension">Vendor Extension&lt;/a>&lt;/td>&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>In order to help with checking the basic parsing and encoding of this format,
the following is an example packet which should parse without error.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-txt" data-lang="txt">&lt;span style="display:flex;">&lt;span> 00, // tag (0; no subpacket is 0 yet)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> 00, // flags (0; no flags)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> 00, 00 // length (0; no data)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> // data would go here, but there is none
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Additionally, throughout the rest of the subpackets, there are a few unique and
shared datatypes. I document them all more clearly in the draft, but to quickly
run through them here too:&lt;/p>
&lt;h3 id="uuid">UUID&lt;/h3>
&lt;p>This field represents a globally unique idenfifer, as defined by RFC 9562, as
16 raw bytes.&lt;/p>
&lt;h3 id="frequency">Frequency&lt;/h3>
&lt;p>Data encoded in a Frequency field is stored as microhz (1 Hz is stored as
1000000, 2 Hz is stored as 2000000) as an unsigned 64 bit integer. This has a
minimum value of 0 Hz, and a maximum value of 18446744073709551615 uHz, or just
above 18.4 THz. This is a bit of a tradeoff, but it&amp;rsquo;s a set of issues that I
would gladly contend with rather than deal with the related issues with storing
frequency data as a floating point value downstream. Not a huge factor, but as
an aside, this is also how my current generation SDR processing code (&lt;code>sparky&lt;/code>)
stores Frequency data internally, which makes conversion between the two
natural.&lt;/p>
&lt;h3 id="iq-samples">IQ samples&lt;/h3>
&lt;p>ARF supports IQ samples in a number of different formats. Part of the idea here
is I want it to be easy for capturing programs to encode ARF for a specific
radio without mandating a single iq format representation. For IQ types with
a scalar value which takes more than a single byte, this is always paired
with a Byte Order field, to indicate if the IQ scalar values are little or
big endian.&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;td class="hz-1-6th">ID&lt;/td>
&lt;td class="hz-1-6th">Name&lt;/td>
&lt;td class="hz-2-3rd">Description&lt;/td>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>&lt;td>0x01&lt;/td>&lt;td>f32&lt;/td>&lt;td>interleaved 32 bit floating point scalar values&lt;/td>&lt;/tr>
&lt;tr>&lt;td>0x02&lt;/td>&lt;td>i8&lt;/td> &lt;td>interleaved 8 bit signed integer scalar values&lt;/td>&lt;/tr>
&lt;tr>&lt;td>0x03&lt;/td>&lt;td>i16&lt;/td>&lt;td>interleaved 16 bit signed integer scalar values&lt;/td>&lt;/tr>
&lt;tr>&lt;td>0x04&lt;/td>&lt;td>u8&lt;/td> &lt;td>interleaved 8 bit unsigned integer scalar values&lt;/td>&lt;/tr>
&lt;tr>&lt;td>0x05&lt;/td>&lt;td>f64&lt;/td>&lt;td>interleaved 64 bit floating point scalar values&lt;/td>&lt;/tr>
&lt;tr>&lt;td>0x06&lt;/td>&lt;td>f16&lt;/td>&lt;td>interleaved 16 bit floating point scalar values&lt;/td>&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;h2 id="header">Header&lt;/h2>
&lt;p>Each ARF file must start with a specific Header packet. The header contains
information about the ARF stream writ large to follow. Header packets are
always marked as &amp;ldquo;critical&amp;rdquo;.&lt;/p>
&lt;div class="hz-abi">
&lt;div type="u64" class="hz-abi-blue hz-abi-2b">
magic
&lt;/div>
&lt;div type="u64" class="hz-abi-blue hz-abi-2b">
flags
&lt;/div>
&lt;div type="u64" class="hz-abi-blue hz-abi-2b">
start
&lt;/div>
&lt;div type="uuid" class="hz-abi-blue hz-abi-4b">
guid
&lt;/div>
&lt;div type="uuid" class="hz-abi-blue hz-abi-4b">
site guid
&lt;/div>
&lt;div type="u8" class="hz-abi-blue hz-abi-1b">
#st
&lt;/div>
&lt;/div>
&lt;p>In order to help with checking the basic parsing and encoding of this format,
the following is an example header subpacket (when encoded or decoded this
will be found inside an ARF packet as described above) which should parse
without error, with known values.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-txt" data-lang="txt">&lt;span style="display:flex;">&lt;span>00, 00, 00, fa, de, dc, ab, 1e, // magic
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>00, 00, 00, 00, 00, 00, 00, 00, // flags
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>18, 27, a6, c0, b5, 3b, 06, 07, // start time (1740543127)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>// guid (fb47f2f0-957f-4545-94b3-75bc4018dd4b)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>fb, 47, f2, f0, 95, 7f, 45, 45,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>94, b3, 75, bc, 40, 18, dd, 4b,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>// site_id (ba07c5ce-352b-4b20-a8ac-782628e805ca)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>ba, 07, c5, ce, 35, 2b, 4b, 20,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>a8, ac, 78, 26, 28, e8, 05, ca
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="stream-header">Stream Header&lt;/h2>
&lt;p>Immediately after the arf &lt;a href="#header">Header&lt;/a>, some number of Stream Headers
follow. There must be exactly the same number of Stream Header packets as are
indicated by the &lt;code>num streams&lt;/code> field of the Header. This has the nice effect of
enabling clients to read all the stream headers without requiring buffering of
&amp;ldquo;unread&amp;rdquo; packets from the stream.&lt;/p>
&lt;div class="hz-abi">
&lt;div type="u8" class="hz-abi-blue hz-abi-1b">
id
&lt;/div>
&lt;div type="u64" class="hz-abi-blue hz-abi-2b">
flags
&lt;/div>
&lt;div type="u8" class="hz-abi-blue hz-abi-1b">
fmt
&lt;/div>
&lt;div type="u8" class="hz-abi-blue hz-abi-1b">
bo
&lt;/div>
&lt;div type="u64" class="hz-abi-blue hz-abi-2b">
rate
&lt;/div>
&lt;div type="u64" class="hz-abi-blue hz-abi-2b">
freq
&lt;/div>
&lt;div type="uuid" class="hz-abi-blue hz-abi-2b">
guid
&lt;/div>
&lt;div type="uuid" class="hz-abi-blue hz-abi-2b">
site
&lt;/div>
&lt;/div>
&lt;p>In order to help with checking the basic parsing and encoding of this format,
the following is an example stream header subpacket (when encoded or decoded
this will be found inside an ARF packet as described above) which should parse
without error, with known values.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-txt" data-lang="txt">&lt;span style="display:flex;">&lt;span>00, 01, // id (1)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>00, 00, 00, 00, 00, 00, 00, 00, // flags
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>01, // format (float32)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>01, // byte order (Little Endian)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>00, 00, 01, d1, a9, 4a, 20, 00, // rate (2 MHz)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>00, 00, 5a, f3, 10, 7a, 40, 00, // frequency (100 MHz)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>// guid (7b98019d-694e-417a-8f18-167e2052be4d)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>7b, 98, 01, 9d, 69, 4e, 41, 7a,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>8f, 18, 16, 7e, 20, 52, be, 4d,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>// site_id (98c98dc7-c3c6-47fe-bc05-05fb37b2e0db)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>98, c9, 8d, c7, c3, c6, 47, fe,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>bc, 05, 05, fb, 37, b2, e0, db,
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="samples">Samples&lt;/h2>
&lt;p>Block of IQ samples in the format indicated by this stream&amp;rsquo;s &lt;code>format&lt;/code> and
&lt;code>byte_order&lt;/code> field sent in the related &lt;a href="#stream-header">Stream Header&lt;/a>.&lt;/p>
&lt;div class="hz-abi">
&lt;div type="u8" class="hz-abi-blue hz-abi-1b">
id
&lt;/div>
&lt;div type="[]iq" class="hz-abi-blue hz-abi-Nb">
iq samples
&lt;/div>
&lt;/div>
&lt;p>In order to help with checking the basic parsing and encoding of this format,
the following is an samples subpacket (when encoded or decoded
this will be found inside an ARF packet as described above). The IQ values
here are notional (and are either 2 8 bit samples, or 1 16 bit sample,
depending on what the related &lt;a href="#stream-header">Stream Header&lt;/a> was).&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-txt" data-lang="txt">&lt;span style="display:flex;">&lt;span>01, // id
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>ab, cd, ab, cd, // iq samples
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="frequency-change">Frequency Change&lt;/h2>
&lt;p>The center frequency of the IQ stream has changed since the
&lt;a href="#stream-header">Stream Header&lt;/a> or last &lt;a href="#frequency-change">Frequency Change&lt;/a>
has been sent. This is useful to capture IQ streams that are jumping
around in frequency during the duration of the capture, rather than
starting and stopping them.&lt;/p>
&lt;div class="hz-abi">
&lt;div type="u8" class="hz-abi-blue hz-abi-1b">
id
&lt;/div>
&lt;div type="u64" class="hz-abi-blue hz-abi-4b">
frequency
&lt;/div>
&lt;/div>
&lt;p>In order to help with checking the basic parsing and encoding of this format,
the following is a frequency change subpacket (when encoded or decoded
this will be found inside an ARF packet as described above).&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-txt" data-lang="txt">&lt;span style="display:flex;">&lt;span>01, // id
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>00, 00, b5, e6, 20, f4, 80, 00 // frequency (200 MHz)
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="discontinuity">Discontinuity&lt;/h2>
&lt;p>Since the last Samples packet for this stream, samples have been dropped
or not encoded to this stream. This can be used for a stream that has
dropped samples for some reason, a large gap (radio was needed for something
else), or communicating &amp;ldquo;iq snippits&amp;rdquo;.&lt;/p>
&lt;div class="hz-abi">
&lt;div type="u8" class="hz-abi-blue hz-abi-1b">
id
&lt;/div>
&lt;/div>
&lt;p>In order to help with checking the basic parsing and encoding of this format,
the following is a discontinuity subpacket (when encoded or decoded this will
be found inside an ARF packet as described above).&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-txt" data-lang="txt">&lt;span style="display:flex;">&lt;span>01, // id
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="location">Location&lt;/h2>
&lt;p>Up-to-date location as of this moment of the IQ stream, usually from a GPS.
This allows for in-band geospatial information to be marked in the IQ stream.
This can be used for all sorts of things (detected IQ packet snippits aligned
with a time and location or a survey of rf noise in an area)&lt;/p>
&lt;div class="hz-abi">
&lt;div type="u64" class="hz-abi-blue hz-abi-2b">
flags
&lt;/div>
&lt;div type="u8" class="hz-abi-blue hz-abi-1b">
&lt;a href="#location-geodetic-systems">sys&lt;/a>
&lt;/div>
&lt;div type="f64" class="hz-abi-blue hz-abi-2b">
lat
&lt;/div>
&lt;div type="f64" class="hz-abi-blue hz-abi-2b">
long
&lt;/div>
&lt;div type="f64" class="hz-abi-blue hz-abi-2b">
el
&lt;/div>
&lt;div type="f64" class="hz-abi-blue hz-abi-2b">
accuracy
&lt;/div>
&lt;/div>
&lt;p>The &lt;code>sys&lt;/code> field indicates the Geodetic system to be used for the provided
&lt;code>latitude&lt;/code>, &lt;code>longitude&lt;/code> and &lt;code>elevation&lt;/code> fields. The full list of supported
geodetic systems is currently just WGS84, but in case something meaningfully
changes in the future, it&amp;rsquo;d be nice to migrate forward.&lt;/p>
&lt;p>Unfortunately, being a bit of a coward here, the accuracy field is a bit of a
cop-out. I&amp;rsquo;d really rather it be what we see out of kinematic state estimation
tools like a kalman filter, or at minimum, some sort of ellipsoid. This is
neither of those - it&amp;rsquo;s a perfect sphere of error where we pick the largest
error in any direction and use that. Truthfully, I can&amp;rsquo;t be bothered to model
this accurately, and I don&amp;rsquo;t want to contort myself into half-assing something
I know I will half-ass just because I know better.&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;td class="hz-1-6th">System&lt;/td>
&lt;td class="hz-5-6th">Description&lt;/td>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>&lt;td>0x01&lt;/td>&lt;td>
&lt;a href="https://en.wikipedia.org/wiki/World_Geodetic_System#WGS_84">WGS84 - World Geodetic System 1984&lt;/a>
&lt;/td>&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>In order to help with checking the basic parsing and encoding of this format,
the following is a location subpacket (when encoded or decoded this will be
found inside an ARF packet as described above).&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-txt" data-lang="txt">&lt;span style="display:flex;">&lt;span>00, 00, 00, 00, 00, 00, 00, 00, // flags
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>01, // system (wgs84)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>3f, f3, be, 76, c8, b4, 39, 58, // latitude (1.234)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>40, 02, c2, 8f, 5c, 28, f5, c3, // longitude (2.345)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>40, 59, 00, 00, 00, 00, 00, 00, // elevation (100)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>40, 24, 00, 00, 00, 00, 00, 00 // accuracy (10)
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="vendor-extension">Vendor Extension&lt;/h2>
&lt;p>In addition to the fields I put in the spec, I expect that I may need custom
packet types I can&amp;rsquo;t think of now. There&amp;rsquo;s all sorts of useful data that could
be encoded into the stream, so I&amp;rsquo;d rather there be an officially sanctioned
mechanism that allows future work on the spec without constraining myself.&lt;/p>
&lt;p>Just an example, I&amp;rsquo;ve used a custom subpacket to create test vectors, the data
is encoded into a Vendor Extension, followed by the IQ for the modulated
packet. If the demodulated data and in-band original data don&amp;rsquo;t match, we&amp;rsquo;ve
regressed. You could imagine in-band speech-to-text, antenna rotator azimuth
information, or demodulated digital sideband data (like FM HDR data) too. Or
even things I can&amp;rsquo;t even think of!&lt;/p>
&lt;div class="hz-abi">
&lt;div type="uuid" class="hz-abi-blue hz-abi-2b">
id
&lt;/div>
&lt;div type="[]u8" class="hz-abi-blue hz-abi-Nb">
data
&lt;/div>
&lt;/div>
&lt;p>In order to help with checking the basic parsing and encoding of this format,
the following is a vendor extension subpacket (when encoded or decoded this
will be found inside an ARF packet as described above).&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-txt" data-lang="txt">&lt;span style="display:flex;">&lt;span>// extension id (b24305f6-ff73-4b7a-ae99-7a6b37a5d5cd)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>b2, 43, 05, f6, ff, 73, 4b, 7a,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>ae, 99, 7a, 6b, 37, a5, d5, cd,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>// data (0x01, 0x02, 0x03, 0x04, 0x05)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>01, 02, 03, 04, 05
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h1 id="tradeoffs">Tradeoffs&lt;/h1>
&lt;p>The biggest tradeoff that I&amp;rsquo;m not &lt;em>entirely&lt;/em> happy with is limiting the length
of a packet to &lt;code>u16&lt;/code> &amp;ndash; 65535 bytes. Given the u8 sample header, this limits us
to 8191 32 bit sample pairs at a time. I wound up believing that the overhead in
terms of additional packet framing is worth it &amp;ndash; because always encoding 4
byte lengths felt like overkill, and a dynamic length scheme ballooned
codepaths in the decoder that I was trying to keep as easy to change as
possible as I worked with the format.&lt;/p></description></item><item><title>librtlsdr.so for fun and profit</title><link>https://k3xec.com/sparky-rtlsdr/</link><pubDate>Fri, 27 Mar 2026 13:30:00 -0400</pubDate><guid>https://k3xec.com/sparky-rtlsdr/</guid><description>&lt;div class="hz-alert-ok">
Interested in future updates? Follow me on mastodon at
&lt;a href="https://soylent.green/@paul">@paul@soylent.green&lt;/a>. Posts about
&lt;code>hz.tools&lt;/code> will be tagged
&lt;a href="https://soylent.green/@paul/tagged/hztools">#hztools&lt;/a>.&lt;br />
&lt;/div>
&lt;p>It&amp;rsquo;s well known and universally agreed that radios are cool. Among the
contested field of coolest radios, Software Defined Radios (SDRs) are
definitely the most interesting to me. Out of all of my (entirely too many)
SDRs I own, the &lt;code>rtlsdr&lt;/code> is still my #1. It&amp;rsquo;s just &lt;em>good&lt;/em>. It&amp;rsquo;s a great price,
extremely capable, reliable, well-supported, and compact. Why bother with
anything else? Sure, it can&amp;rsquo;t transmit, uses a (fairly weird) &lt;a href="https://k3xec.com/packrat-processing-iq/#rtl-sdr">8 bit unsigned
integer IQ representation&lt;/a>,
limited sampling rate, limited frequency range &amp;ndash; but even with all that, it&amp;rsquo;s
still the radio I will pack first. Don&amp;rsquo;t get me wrong, I love my Ettus radios,
PlutoSDRs, HackRFs, my AirspyHF+ - they&amp;rsquo;re great! I just always find myself
falling back to an &lt;code>rtl-sdr&lt;/code>, every time.&lt;/p>
&lt;p>Perhaps the best reason to use an &lt;code>rtlsdr&lt;/code> is the absolutely mind-boggling
amount of cool stuff people have written for it. The &lt;code>rtlsdr&lt;/code> API is super easy
to use, widely supported if you&amp;rsquo;re building on top of existing radio processing
frameworks &amp;ndash; it&amp;rsquo;s still a &lt;em>shock&lt;/em> to me when something omits &lt;code>rtlsdr&lt;/code> support.&lt;/p>
&lt;h1 id="sparky">sparky&lt;/h1>
&lt;p>Over the last 7 years, I&amp;rsquo;ve been learning about radios &amp;ndash; I got my ham radio
license (&lt;code>de K3XEC&lt;/code>), &lt;a href="https://k3xec.com/christmas/">hacked&lt;/a>
&lt;a href="https://k3xec.com/power-output/">on&lt;/a> &lt;a href="https://k3xec.com/hztools/">some&lt;/a>
&lt;a href="https://k3xec.com/td158/">cool&lt;/a> &lt;a href="https://k3xec.com/su68g/">stuff&lt;/a> where I&amp;rsquo;ve
learned how radios work by &amp;ldquo;doing&amp;rdquo;, and even was lucky enough to give my first
rf-centric &lt;a href="https://k3xec.com/paging-all-radio-curious-hackers/">talk at districtcon&lt;/a>.
Embarrassingly, I still haven&amp;rsquo;t gotten around to learning how the fancy stuff
like &lt;a href="https://www.gnuradio.org/">GNU Radio&lt;/a> works. I&amp;rsquo;m sure I&amp;rsquo;m going to love
it when I do.&lt;/p>
&lt;p>As part of this, I&amp;rsquo;ve also cooked up some very unprofessional formats and
protocols I use for convenience. Locally, all my on-disk captures are stored in
&lt;a href="https://k3xec.com/rfcap/">rfcap&lt;/a> or more recently &lt;a href="https://k3xec.com/arf/">arf&lt;/a>,
while direct SDR access at my house is almost entirely a mix of
the widely used &lt;a href="https://k3xec.com/rtl-tcp/">rtl-tcp&lt;/a> protocol, and my
&amp;ldquo;&lt;code>riq&lt;/code>&amp;rdquo; protocol (post on this coming soon). Both &lt;code>rtl-tcp&lt;/code> and &lt;code>riq&lt;/code> operate
over the network, so I don&amp;rsquo;t have to bother with plugging things into USB ports,
and I can share my radios with &lt;a href="https://notes.pault.ag/tpl/">my friends&lt;/a>.&lt;/p>
&lt;p>All of that work sits in my current generation of radio processing code,
&amp;ldquo;sparky&amp;rdquo; (a reference to
&lt;a href="https://en.wikipedia.org/wiki/Spark-gap_transmitter">spark-gap transmitters&lt;/a>),
which is a heap of Rust, supporting everything from &lt;code>no_std&lt;/code> for embedded
experiments, conditional support for interfacing with all the radios I
own, and &lt;code>tokio&lt;/code>-based async support in addition to blocking i/o
for highly concurrent daemons. This quickly advanced beyond my old Go-based
code (&lt;a href="https://github.com/hztools/go-sdr">hz.tools/go-sdr&lt;/a>), which I archived
so I can focus on learning. I still think Go is a great language to write RF
code in &amp;ndash; but I can&amp;rsquo;t focus on that tech tree anymore.&lt;/p>
&lt;p>Of course, this now poses a new problem &amp;ndash; no one supports my format(s) or
radio protocol(s), since, well, I&amp;rsquo;m the only one using them. I&amp;rsquo;ve committed a
fair amount of my hardware to this setup, and yanking it from the rack to try
something out does pose a bit of a pickle. This isn&amp;rsquo;t a huge deal for learning,
but it does make it tedious to try out something from the internets.&lt;/p>
&lt;h1 id="librtlsdrso">librtlsdr.so&lt;/h1>
&lt;p>Thankfully, Rust has robust support for
&lt;a href="https://faultlore.com/blah/c-isnt-a-language/">wrap[ping itself] in a grotesque simulacra of C’s skin and mak[ing its] flesh undulate&lt;/a>,
which is an attractive nuisance if i&amp;rsquo;ve ever seen one. Naturally, my ability
to restrain myself from engaging in ill-advised rf adventures is basically
zero, so it&amp;rsquo;s time to do the thing any similarly situated person would do &amp;ndash;
reimplement the API and ABI of &lt;code>librtlsdr.so&lt;/code>, backed with &lt;code>sparky&lt;/code> instead.&lt;/p>
&lt;p>Since enumeration of devices is going to be annoying (specifically, they&amp;rsquo;re
over the network), I decided early-on to rely on an explicit list of
devices via a configuration file. I&amp;rsquo;d rather only load that once so programs
don&amp;rsquo;t get confused, so I opted to use a
&lt;a href="https://ftp.gnu.org/old-gnu/Manuals/ld-2.9.1/html_node/ld_26.html">CTOR&lt;/a>
to run a stub when the ELF is linked at runtime.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-rust" data-lang="rust">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">// lightly edited for clarity
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">#[used]&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">#[expect(unused)]&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">#[unsafe(link_section = &lt;/span>&lt;span style="color:#e6db74">&amp;#34;.init_array&amp;#34;&lt;/span>&lt;span style="color:#75715e">)]&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">pub&lt;/span> &lt;span style="color:#66d9ef">static&lt;/span> &lt;span style="color:#66d9ef">INITIALIZE&lt;/span>: &lt;span style="color:#a6e22e">extern&lt;/span> &lt;span style="color:#e6db74">&amp;#34;C&amp;#34;&lt;/span> &lt;span style="color:#66d9ef">fn&lt;/span>() &lt;span style="color:#f92672">=&lt;/span> sparky_rtlsdr_ctor;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">#[unsafe(no_mangle)]&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">pub&lt;/span> &lt;span style="color:#66d9ef">extern&lt;/span> &lt;span style="color:#e6db74">&amp;#34;C&amp;#34;&lt;/span> &lt;span style="color:#66d9ef">fn&lt;/span> &lt;span style="color:#a6e22e">sparky_rtlsdr_ctor&lt;/span>() {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">let&lt;/span> config: &lt;span style="color:#a6e22e">Config&lt;/span> &lt;span style="color:#f92672">=&lt;/span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">if&lt;/span> &lt;span style="color:#66d9ef">let&lt;/span> Ok(config_bytes) &lt;span style="color:#f92672">=&lt;/span> std::fs::read(&lt;span style="color:#e6db74">&amp;#34;/etc/sparky-rtlsdr.toml&amp;#34;&lt;/span>) {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> toml::from_slice(&lt;span style="color:#f92672">&amp;amp;&lt;/span>config_bytes).unwrap()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> } &lt;span style="color:#66d9ef">else&lt;/span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Config { device: &lt;span style="color:#a6e22e">vec&lt;/span>&lt;span style="color:#f92672">!&lt;/span>[] }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> };
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">CONFIG&lt;/span>.set(config);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Next, it&amp;rsquo;s time to start with the basics. Opening and closing a handle using
&lt;code>rtlsdr_open&lt;/code> and &lt;code>rtlsdr_close&lt;/code>. Given we don&amp;rsquo;t control the runtime, and the
&lt;code>rtl-sdr&lt;/code> device handle is opaque (for good reason!), I opted to smuggle a rust
&lt;code>Box&amp;lt;Device&amp;gt;&lt;/code> non-FFI safe heap-allocated struct through the device handle
pointer, and let C take ownership of the &lt;code>Box&lt;/code>. No one should be looking in
there anyway.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-rust" data-lang="rust">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">// lightly edited for clarity
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">#[unsafe(no_mangle)]&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">pub&lt;/span> &lt;span style="color:#66d9ef">unsafe&lt;/span> &lt;span style="color:#66d9ef">extern&lt;/span> &lt;span style="color:#e6db74">&amp;#34;C&amp;#34;&lt;/span> &lt;span style="color:#66d9ef">fn&lt;/span> &lt;span style="color:#a6e22e">rtlsdr_open&lt;/span>(dev: &lt;span style="color:#f92672">*&lt;/span>&lt;span style="color:#66d9ef">mut&lt;/span> &lt;span style="color:#f92672">*&lt;/span>&lt;span style="color:#66d9ef">mut&lt;/span> Handle, index: &lt;span style="color:#66d9ef">u32&lt;/span>) -&amp;gt; &lt;span style="color:#a6e22e">int&lt;/span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">let&lt;/span> config &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#f92672">&amp;amp;&lt;/span>&lt;span style="color:#66d9ef">CONFIG&lt;/span>.device[index &lt;span style="color:#66d9ef">as&lt;/span> &lt;span style="color:#66d9ef">usize&lt;/span>];
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">let&lt;/span> sdr &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#66d9ef">match&lt;/span> config.load() {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Ok(v) &lt;span style="color:#f92672">=&amp;gt;&lt;/span> v,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Err(err) &lt;span style="color:#f92672">=&amp;gt;&lt;/span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> &lt;span style="color:#f92672">-&lt;/span>&lt;span style="color:#ae81ff">1&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> };
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">let&lt;/span> handle &lt;span style="color:#f92672">=&lt;/span> Box::new(Handle { config, sdr });
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">unsafe&lt;/span> { &lt;span style="color:#f92672">*&lt;/span>dev &lt;span style="color:#f92672">=&lt;/span> Box::into_raw(handle) };
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ae81ff">0&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">#[unsafe(no_mangle)]&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">pub&lt;/span> &lt;span style="color:#66d9ef">unsafe&lt;/span> &lt;span style="color:#66d9ef">extern&lt;/span> &lt;span style="color:#e6db74">&amp;#34;C&amp;#34;&lt;/span> &lt;span style="color:#66d9ef">fn&lt;/span> &lt;span style="color:#a6e22e">rtlsdr_close&lt;/span>(dev: &lt;span style="color:#f92672">*&lt;/span>&lt;span style="color:#66d9ef">mut&lt;/span> Handle) -&amp;gt; &lt;span style="color:#a6e22e">int&lt;/span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">let&lt;/span> dev &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#66d9ef">unsafe&lt;/span> { Box::from_raw(dev) };
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> drop(dev);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ae81ff">0&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>With that in place, we can chip away at the API surface, translating calls
as best as we can. I won&amp;rsquo;t bother listing it all, since it&amp;rsquo;s not very
interesting &amp;ndash; but here&amp;rsquo;s an example implementation of &lt;code>rtlsdr_set_sample_rate&lt;/code>
and &lt;code>rtlsdr_get_sample_rate&lt;/code>. These calls are translating from an rtl-sdr
frequency (which is a &lt;code>u32&lt;/code> containing the value as Hz) into a sparky Frequency
type, and invoking &lt;code>get_sample_rate&lt;/code> or &lt;code>set_sample_rate&lt;/code> on the device&amp;rsquo;s
rust handle. Since each device implements the sparky &lt;code>Sdr&lt;/code> trait, the actual
underlying device doesn&amp;rsquo;t matter much here.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-rust" data-lang="rust">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">#[unsafe(no_mangle)]&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">pub&lt;/span> &lt;span style="color:#66d9ef">unsafe&lt;/span> &lt;span style="color:#66d9ef">extern&lt;/span> &lt;span style="color:#e6db74">&amp;#34;C&amp;#34;&lt;/span> &lt;span style="color:#66d9ef">fn&lt;/span> &lt;span style="color:#a6e22e">rtlsdr_set_sample_rate&lt;/span>(dev: &lt;span style="color:#f92672">*&lt;/span>&lt;span style="color:#66d9ef">mut&lt;/span> Handle, rate: &lt;span style="color:#66d9ef">u32&lt;/span>) -&amp;gt; &lt;span style="color:#a6e22e">int&lt;/span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">let&lt;/span> dev &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#66d9ef">unsafe&lt;/span> { &lt;span style="color:#f92672">&amp;amp;&lt;/span>&lt;span style="color:#66d9ef">mut&lt;/span> &lt;span style="color:#f92672">*&lt;/span>dev };
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">let&lt;/span> rate &lt;span style="color:#f92672">=&lt;/span> Frequency::from_hz(rate &lt;span style="color:#66d9ef">as&lt;/span> &lt;span style="color:#66d9ef">i64&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">if&lt;/span> &lt;span style="color:#66d9ef">let&lt;/span> Err(err) &lt;span style="color:#f92672">=&lt;/span> dev.sdr.set_sample_rate(dev.channel, rate) {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> &lt;span style="color:#f92672">-&lt;/span>&lt;span style="color:#ae81ff">1&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ae81ff">0&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">#[unsafe(no_mangle)]&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">pub&lt;/span> &lt;span style="color:#66d9ef">unsafe&lt;/span> &lt;span style="color:#66d9ef">extern&lt;/span> &lt;span style="color:#e6db74">&amp;#34;C&amp;#34;&lt;/span> &lt;span style="color:#66d9ef">fn&lt;/span> &lt;span style="color:#a6e22e">rtlsdr_get_sample_rate&lt;/span>(dev: &lt;span style="color:#f92672">*&lt;/span>&lt;span style="color:#66d9ef">mut&lt;/span> Handle) -&amp;gt; &lt;span style="color:#66d9ef">u32&lt;/span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">let&lt;/span> dev &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#66d9ef">unsafe&lt;/span> { &lt;span style="color:#f92672">&amp;amp;&lt;/span>&lt;span style="color:#66d9ef">mut&lt;/span> &lt;span style="color:#f92672">*&lt;/span>dev };
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">let&lt;/span> freq &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#66d9ef">match&lt;/span> dev.sdr.get_sample_rate(dev.channel) {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Ok(freq) &lt;span style="color:#f92672">=&amp;gt;&lt;/span> freq,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Err(err) &lt;span style="color:#f92672">=&amp;gt;&lt;/span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> &lt;span style="color:#ae81ff">0&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> };
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> freq.as_hz() &lt;span style="color:#66d9ef">as&lt;/span> &lt;span style="color:#66d9ef">u32&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>After repeating this process for the rest of the stubs I could (and otherwise
setting error conditions if the functionality is not supported), I was ready to
try it out. Within sparky, I patched my &amp;ldquo;MockSDR&amp;rdquo; (basically a &lt;code>Sdr&lt;/code> traited
Mock type) to implement the same testmode IQ protocol that the RTL-SDR has, and
decided to see if &lt;code>rtl_test&lt;/code> from &lt;code>apt&lt;/code> without any changes could be fooled.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-txt" data-lang="txt">&lt;span style="display:flex;">&lt;span>$ rtl_test
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>No supported devices found.
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Great, cool. No devices plugged in. Looks great. Let&amp;rsquo;s try it with my
&lt;code>librtlsdr.so&lt;/code> &lt;code>LD_PRELOAD&lt;/code>-ed into the binary first:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-txt" data-lang="txt">&lt;span style="display:flex;">&lt;span>$ LD_PRELOAD=target/release/librtlsdr.so rtl_test
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>Found 1 device(s):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> 0: hz.tools, mock sdr, SN: totally legit no tricks
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>Using device 0: sparky mock sdr
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>Supported gain values (0):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>Sampling at 2048000 S/s.
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>Info: This tool will continuously read from the device, and report if
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>samples get lost. If you observe no further output, everything is fine.
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>Reading samples in async mode...
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>^CSignal caught, exiting!
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>User cancel, exiting...
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>Samples per million lost (minimum): 0
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>$
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Outstanding. Even more outstandingly, if I change my testmode implementation to
skip samples, &lt;code>rtl_test&lt;/code> correctly reports the errors &amp;ndash; I think it&amp;rsquo;s showing
promise! On to try the real endgame here &amp;ndash; let&amp;rsquo;s have our new &lt;code>librtlsdr.so&lt;/code>
connect to an &lt;code>rtl-tcp&lt;/code> endpoint and see if &lt;code>rtl_fm&lt;/code> works:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-txt" data-lang="txt">&lt;span style="display:flex;">&lt;span>LD_PRELOAD=target/release/librtlsdr.so \
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> rtl_fm -d 1 -s 120k -E deemp -M fm -f 90.9M | \
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ffplay -f s16le -ar 120k -i -
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>Found 2 device(s):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> 0: hz.tools, mock sdr, SN: totally legit no tricks
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> 1: hz.tools, rtl-tcp, SN: node2.rf.lan:1202
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>Using device 1: sparky rtltcp node2
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>Tuner gain set to automatic.
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>Tuned to 91170000 Hz.
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>Oversampling input by: 9x.
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>Oversampling output by: 1x.
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>Buffer size: 7.59ms
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>Sampling at 1080000 S/s.
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>Output at 120000 Hz.
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>And there it was! Not the best audio quality (mostly due to my inability to
correctly read the &lt;code>rtl_fm&lt;/code> manpage to tune the filter and
downsample/oversampling rates to audio), but it&amp;rsquo;s &lt;em>definitely&lt;/em> passable.
I figured I&amp;rsquo;d try something that was a bit more interesting next &amp;ndash; &lt;code>gqrx&lt;/code>,
since it&amp;rsquo;s super handy, I use it a ton, and will definitely amuse me to no
end. To my surprise and delight, &lt;code>LD_PRELOAD=target/release/librtlsdr.so gqrx&lt;/code>
wound up running, and I saw my devices pop right up in the setting menu:&lt;/p>
&lt;img src="https://k3xec.com/imgs/sparky-rtlsdr/gqrx-settings.png" alt="" style="max-width: 20em;" />
&lt;p>Huge. Huge. Amazing. It did crash as soon as I tried to actually &lt;em>use&lt;/em> the
radio, but after fixing a few dangling bugs in the API surface (and some
assumptions I think some underlying gnuradio driver may be making that I need
to double check in the code), I was able to get a super solid stream of
broadcast fm radio, with gqrx being none the wiser. It thought it was
&amp;ldquo;just&amp;rdquo; talking to the device it knows as &lt;code>rtl=1&lt;/code>.&lt;/p>
&lt;img src="https://k3xec.com/imgs/sparky-rtlsdr/gqrx-waterfall.png" alt="" />
&lt;p>Nice. I can&amp;rsquo;t wait to try this with the rest of the rtl-sdr based tools I like
having around using my &lt;code>riq&lt;/code> protocol next. I don&amp;rsquo;t think that&amp;rsquo;ll be worth a
post, but hopefully I&amp;rsquo;ll get around to publishing details on that stack next.&lt;/p>
&lt;h1 id="epilogue">epilogue&lt;/h1>
&lt;p>Well. That&amp;rsquo;s it. End of story. A bit anti-climatic, sure. While this new shim
will provide me endless minutes of mild amusement, I could see using this to
expose my sparky testing utilities via &lt;code>librtlsdr.so&lt;/code> &amp;ndash; my &amp;ldquo;mock sdr&amp;rdquo; driver
allows for replaying captures off disk, which could be interesting to make sure
that signals are still properly decoded after changes, or instrument
performance changes (via SNR, BER, packets observed, etc) on reference samples
I have on my NAS. Maybe that&amp;rsquo;ll come in handy one day!&lt;/p>
&lt;p>Truth be told, I&amp;rsquo;m not sure I actually want to encourage anyone to do this for
real (although I think I&amp;rsquo;ll definitely be using it on my LAN to see what
happens). I also don&amp;rsquo;t have a repo to share &amp;ndash; I don&amp;rsquo;t particularly feel with
dealing with the secondary effects of publishing &lt;code>sparky&lt;/code> (and &lt;code>sparky-rtlsdr&lt;/code>)
yet, since i&amp;rsquo;m still getting my feet under me on the radio aspect of all this.&lt;/p>
&lt;p>I&amp;rsquo;ll be sure to post updates if anything changes with this here (tagged
&lt;a href="https://k3xec.com/tags/sparky/">sparky&lt;/a>) and at
&lt;a href="https://soylent.green/@paul">@paul@soylent.green&lt;/a>.
I can&amp;rsquo;t wait to post more about some of the odd sidequests (like this one!)
i&amp;rsquo;ve completed over the last few years &amp;ndash; I&amp;rsquo;ve been waiting to feel
confident that my work has matured and was withstood the new problems i&amp;rsquo;ve
thrown at it, and it largely has.&lt;/p>
&lt;p>It&amp;rsquo;s my hope that these projects (and this project in particular) has provided
a glimpse into the world of software defined radio for my systems friends, and
a bit about systems for my radio friends. It&amp;rsquo;s not &lt;em>all&lt;/em> magic, and I hope
someone out there feels inclined to have some fun with radios themselves!&lt;/p></description></item><item><title>Paging all Radio Curious Hackers</title><link>https://k3xec.com/paging-all-radio-curious-hackers/</link><pubDate>Mon, 02 Feb 2026 09:50:00 -0500</pubDate><guid>https://k3xec.com/paging-all-radio-curious-hackers/</guid><description>&lt;p>After years of thinking about and learning about how radios work, I figured it
was high-time to start to more aggressively share the things i&amp;rsquo;ve been
learning. I had a ton of fun at &lt;a href="https://www.districtcon.org/">DistrictCon&lt;/a>
year 0, so it was a pretty natural place to pitch an RF-focused introductory
talk.&lt;/p>
&lt;p>I was selected for Year 1, and able to give my first ever RF related talk about
how to set off restaurant pagers (including one on stage!) by reading and
writing IQ directly using a little bit of stdlib only Python.&lt;/p>
&lt;p>This talk is based around the work I&amp;rsquo;ve written about previously
(&lt;a href="https://k3xec.com/christmas/">here&lt;/a>, &lt;a href="https://k3xec.com/td158/">here&lt;/a> and
&lt;a href="https://k3xec.com/su68g/">here&lt;/a>), but the &amp;ldquo;all-in-one&amp;rdquo; form factor was
something I was hoping would help encourage folks out there to take a look
under the hood of some of the gear around them.&lt;/p>
&lt;iframe width="750" height="400" src="https://www.youtube-nocookie.com/embed/2EgNs88Ombo" title="Paul Tagliamonte | Paging all radio curious hackers!" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen>&lt;/iframe>
&lt;p>(In case the iframe above isn&amp;rsquo;t working, direct link to the YouTube video
recording is &lt;a href="https://www.youtube.com/watch?v=jEYYnWuRAH8">here&lt;/a>)&lt;/p>
&lt;p>I&amp;rsquo;ve posted my slides from the talk at
&lt;a href="https://k3xec.com/pdf/PARCH.pdf">PARCH.pdf&lt;/a> to hopefully give folks some time
to flip through them directly.&lt;/p>
&lt;p>All in all, the session was great &amp;ndash; It was truly humbling to see so many
folks interested in hearing me talk about radios. I had a bit of an own-goal in
picking a 20 minute form-factor, so the talk is paced wrong (it feels like it
went way too fast). Hopefully being able to see the slides and pause the video
is helpful.&lt;/p>
&lt;p>We had a short ad-hoc session after where I brought two sets of pagers and my
power switch; but unfortunately we didn&amp;rsquo;t have anyone who was able to trigger
any of the devices on their own (due to a mix of time between sessions and
computer set-up). Hopefully it was enough to get folks interested in trying
this on their own!&lt;/p></description></item><item><title>Reverse Engineering (another) Restaurant Pager system 🍽️</title><link>https://k3xec.com/su68g/</link><pubDate>Tue, 04 Mar 2025 10:00:00 -0500</pubDate><guid>https://k3xec.com/su68g/</guid><description>&lt;p>Some of you may remember that I recently felt a bit underwhelmed
by the last &lt;a href="https://k3xec.com/td158/">pager&lt;/a> I reverse engineered &amp;ndash; the Retekess TD-158,
mostly due to how intuitive their design decions were. It was pretty easy
to jump to conclusions because they had made some pretty good decisions on
how to do things.&lt;/p>
&lt;p>I figured I&amp;rsquo;d spin the wheel again and try a new pager system &amp;ndash; this time I
went for a SU-68G-10 pager, since I recognized the form factor as another
fairly common unit I&amp;rsquo;ve seen around town. Off to Amazon I went, bought a set,
and got to work trying to track down the FCC filings on this model. I
eventually found what seemed to be the right make/model, and it, once again,
indicated that this system should be operating in the &lt;code>433 MHz&lt;/code> ISM band likely
using OOK modulation. So, figured I&amp;rsquo;d start with the center of the band (again)
at &lt;code>433.92 MHz&lt;/code>, take a capture, test my luck, and was greeted with a now very
familiar sight.&lt;/p>
&lt;p>&lt;img src="https://k3xec.com/imgs/su68g/packet.png" alt="">&lt;/p>
&lt;p>Same as the last goarounds, except the premable here is a &lt;code>0&lt;/code> symbol followed
by 6-ish symbol durations of no data, followed by 25 bits of a packet. Careful
readers will observe 26 symbols above after the preamble &amp;ndash; I did too! The last
&lt;code>0&lt;/code> in the screenshot above is not actually a part of the packet &amp;ndash; rather,
it&amp;rsquo;s part of the next packet&amp;rsquo;s preamble. Each packet is packed in pretty tight.&lt;/p>
&lt;h1 id="by-hand-demodulation">By Hand Demodulation&lt;/h1>
&lt;p>Going off the same premise as last time, I figured i&amp;rsquo;d give it a manual demod
and see what shakes out (again). This is now the third time i&amp;rsquo;ve run this play,
so check out either of my prior &lt;a href="https://k3xec.com/christmas/">two&lt;/a> &lt;a href="https://k3xec.com/td158/">posts&lt;/a> for a
better written description of what&amp;rsquo;s going on here &amp;ndash; I&amp;rsquo;ll skip all the details
since i&amp;rsquo;d just be copy-pasting from those posts into here. Long story short, I
demodulated a call for pager 1, call for pager 10, and a power off command.&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;td class="hz-1-6th">What&lt;/td>
&lt;td class="hz-5-6th">Bits&lt;/td>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>&lt;td>Call 1 &lt;/td>&lt;td>&lt;code>1101111111100100100000000&lt;/code>&lt;/td>&lt;/tr>
&lt;tr>&lt;td>Call 10&lt;/td>&lt;td>&lt;code>1101111111100100010100000&lt;/code>&lt;/td>&lt;/tr>
&lt;tr>&lt;td>Off &lt;/td>&lt;td>&lt;code>1101111111100111101101110&lt;/code>&lt;/td>&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>A few things jump out at me here &amp;ndash; the first 14 bits are fixed (in my case,
&lt;code>11011111111001&lt;/code>), which means some mix of preamble, system id, or other
system-wide constant. Additionally, The last 9 bits also look like they are our
pager &amp;ndash; the &lt;code>1&lt;/code> and &lt;code>10&lt;/code> pager numbers (LSB bit order) jump right out
(&lt;code>100000000&lt;/code> and &lt;code>010100000&lt;/code>, respectively). That just leaves the two remaining
bits which look to be the &amp;ldquo;action&amp;rdquo; &amp;ndash; &lt;code>00&lt;/code> for a &amp;ldquo;Call&amp;rdquo;, and &lt;code>11&lt;/code> for a &amp;ldquo;Power
off&amp;rdquo;. I don&amp;rsquo;t super love this since command has two bits rather than one, the
base station ID seems really long, and a 9-bit Pager ID is just weird. Also,
what is up with that power-off pager id? Weird. So, let&amp;rsquo;s go and see what we
can do to narrow down and confirm things by hand.&lt;/p>
&lt;h1 id="testing-bit-flips">Testing bit flips&lt;/h1>
&lt;p>Rather than call it a day at that, I figure it&amp;rsquo;s worth a bit of diligence to
make sure it&amp;rsquo;s all correct &amp;ndash; so I figured we should try sending packets to
my pagers and see how they react to different messages after flipping bits
in parts of the packet.&lt;/p>
&lt;p>I implemented a simple base station for the pagers using my Ettus B210mini, and
threw together a simple OOK modulator and transmitter program which allows me
to send specifically crafted test packets on frequency. Implementing the base
station is pretty straightforward, because of the modulation of the signal
(OOK), it&amp;rsquo;s mostly a matter of setting a buffer to &lt;code>1&lt;/code> and &lt;code>0&lt;/code> for where the
carrier signal is on or off timed to the sample rate, and sending that off to
the radio. If you&amp;rsquo;re interested in a more detailed writeup on the steps
involved, there&amp;rsquo;s a bit more in my &lt;a href="https://k3xec.com/christmas/">christmas tree post&lt;/a>.&lt;/p>
&lt;p>First off, I&amp;rsquo;d like to check the base id. I want to know if all the bits in
what I&amp;rsquo;m calling the &amp;ldquo;base id&amp;rdquo; are truly part of the base station ID, or
perhaps they have some other purpose (version, preamble?). I wound up following
a three-step process for every base station id:&lt;/p>
&lt;ul>
&lt;li>Starting with an unmodified call packet for the pager under test:
&lt;ul>
&lt;li>Flip the Nth bit, and transmit the call. See if the pager reacts.&lt;/li>
&lt;li>Hold &amp;ldquo;SET&amp;rdquo;, and pair the pager with the new packet.&lt;/li>
&lt;li>Transmit the call. See if the pager reacts.&lt;/li>
&lt;li>After re-setting the ID, transmit the call with the physical base station,
see if the pager reacts.&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>Starting with an unmodified off packet for the pager system&lt;/li>
&lt;li>Flip the Nth bit, transmit the off, see if the pager reacts.&lt;/li>
&lt;/ul>
&lt;p>What wound up happening is that changing any bit in the first 14 bits meant
that the packet no longer worked with any pager until it was re-paired, at
which point it begun to work again. This likely means the first 14 bits are
part of the base station ID &amp;ndash; and not static between base stations, or some
constant like a version or something. All bits appear to be used.&lt;/p>
&lt;p>I repeated the same process with the &amp;ldquo;command&amp;rdquo; bits, and found that only &lt;code>11&lt;/code>
and &lt;code>00&lt;/code> caused the pagers to react for the pager ids i&amp;rsquo;ve tried.&lt;/p>
&lt;p>I repeated this process one last time with the &amp;ldquo;pager id&amp;rdquo; bits this time, and
found the last bit in the packet isn&amp;rsquo;t part of the pager ID, and can be either
a &lt;code>1&lt;/code> or a &lt;code>0&lt;/code> and still cause the pager to react as if it were a 0. This means
that the last bit is unknown but it has no impact on either a power off or
call, and all messages sent by my base station always have a 0 set. It&amp;rsquo;s not
clear if this is used by anything &amp;ndash; likely not since setting a bit there
doesn&amp;rsquo;t result in any change of behavior I can see yet.&lt;/p>
&lt;h1 id="final-packet-structure">Final Packet Structure&lt;/h1>
&lt;p>After playing around with flipping bits and testing, the final structure
I was able to come up with based on behavior I was able to observe from
transmitting hand-crafted packets and watching pagers buzz:&lt;/p>
&lt;div class="hz-abi">
&lt;div type="14 bits" class="hz-abi-green hz-abi-2b">base id&lt;/div>
&lt;div type="2 bits" class="hz-abi-green hz-abi-2b">command&lt;/div>
&lt;div type="8 bits" class="hz-abi-green hz-abi-2b">pager id&lt;/div>
&lt;div type="1 bit" class="hz-abi-green hz-abi-2b">???&lt;/div>
&lt;/div>
&lt;h2 id="commands">Commands&lt;/h2>
&lt;p>The &lt;code>command&lt;/code> section bit comes in two flavors &amp;ndash; either a &amp;ldquo;call&amp;rdquo; or an &amp;ldquo;off&amp;rdquo;
command.&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;td class="hz-1-6th">Type&lt;/td>
&lt;td class="hz-1-6th">Id (2 bits)&lt;/td>
&lt;td class="hz-4-6th">Description&lt;/td>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>&lt;td>Call&lt;/td>&lt;td>00&lt;/td>&lt;td>Call the pager identified by the id in &lt;code>pager id&lt;/code>&lt;/td>&lt;/tr>
&lt;tr>&lt;td>Off&lt;/td>&lt;td>11&lt;/td>&lt;td>Request pagers power off, &lt;code>pager id&lt;/code> is always &lt;code>10110111&lt;/code>&lt;/td>&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>As for the actual RF PHY characteristics, here&amp;rsquo;s my best guesses at what&amp;rsquo;s
going on with them:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;td class="hz-1-6th">What&lt;/td>
&lt;td class="hz-5-6th">Description&lt;/td>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Center Frequency&lt;/td>
&lt;td>433.92 MHz&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Modulation&lt;/td>
&lt;td>OOK&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Symbol Duration&lt;/td>
&lt;td>1300us&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Bits&lt;/td>
&lt;td>25&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Preamble&lt;/td>
&lt;td>325us of carrier, followed by 8800us of no carrier&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>I&amp;rsquo;m not 100% on the timings, but they appear to be close enough to work
reliably. Same with the center frequency, it&amp;rsquo;s roughly right but there
may be a slight difference i&amp;rsquo;m missing.&lt;/p>
&lt;h1 id="lingering-questions">Lingering Questions&lt;/h1>
&lt;p>This was all generally pretty understandable &amp;ndash; another system that had some
good decisions, and wasn&amp;rsquo;t too bad to reverse engineer. This was a bit more fun
to do, since there was a bit more ambiguity here, but still not crazy. At least
this one was a bit more ambiguous that needed a bit of followup to confirm
things, which made it a bit more fun.&lt;/p>
&lt;p>I am left with a few questions, though &amp;ndash; which I&amp;rsquo;m kinda interested in
understanding, but I&amp;rsquo;ll likely need a lot more data and/or original source:&lt;/p>
&lt;p>Why is the &amp;ldquo;command&amp;rdquo; two bits here? This was a bit tough to understand because
of the number of bits they have at their disposal &amp;ndash; given the one last bit at
the end of the packet that doesn&amp;rsquo;t seem to do anything, there&amp;rsquo;s no reason this
couldn&amp;rsquo;t have been a 16 bit base station id, and an 8 bit pager id along with a
single bit command (call or off).&lt;/p>
&lt;p>When sending an &amp;ldquo;off&amp;rdquo; &amp;ndash; why is power off that bit pattern? Other pager IDs
don&amp;rsquo;t seem to work with &amp;ldquo;off&amp;rdquo;, so it has some meaning, but I&amp;rsquo;m not sure what
that is. You press and hold 9 on the physical base station, but the code winds
up coming out to &lt;code>0xED&lt;/code>, &lt;code>237&lt;/code> or maybe &lt;code>-19&lt;/code> if it&amp;rsquo;s signed. I can&amp;rsquo;t quite
figure out &lt;em>why&lt;/em> it&amp;rsquo;s this value. Are there other codes?&lt;/p>
&lt;p>Finally &amp;ndash; what&amp;rsquo;s up with the last bit? Why is it 25 bits and not 24? It must
take more work to process something that isn&amp;rsquo;t 8 bit aligned &amp;ndash; and all for
something that&amp;rsquo;s not being used!&lt;/p></description></item><item><title>Reverse Engineering a Restaurant Pager system 🍽️</title><link>https://k3xec.com/td158/</link><pubDate>Fri, 14 Jun 2024 01:07:00 -0400</pubDate><guid>https://k3xec.com/td158/</guid><description>&lt;p>It&amp;rsquo;s been a while since I played with something new &amp;ndash; been stuck in a bit of a
rut with radios recently - working on refining and debugging stuff I mostly
understand for the time being. The other day, I was out getting some food and I
idly wondered how the restaurant pager system worked. Idle curiosity gave way
to the realization that I, in fact, likely had the means and ability to answer
this question, so I bought the first set of the most popular looking restaurant
pagers I could find on eBay, figuring it&amp;rsquo;d be a fun multi-week adventure.&lt;/p>
&lt;h1 id="order-up">Order up!&lt;/h1>
&lt;p>I wound up buying a Retekess brand TD-158 Restaurant Pager System (they looked
like ones I&amp;rsquo;d seen before and seemed to be low-cost and popular), and quickly
after, had a pack of 10 pagers and a base station in-hand. The manual stated
that the radios operated at &lt;code>433 MHz&lt;/code> (cool! can do! Love a good ISM band
device), and after taking an initial read through the manual for tips on the
PHY, I picked out a few interesting things. First is that the base station ID
was limited to 0-999, which is weird because it means the limiting factor is
likely the base-10 display on the base station, not the protocol &amp;ndash; we need
enough bits to store 999 &amp;ndash; at least 10 bits. Nothing else seemed to catch my
eye, so I figured may as well jump right to it.&lt;/p>
&lt;p>Not being the type to mess with success, I did exactly the same thing as I did
in my &lt;a href="https://k3xec.com/christmas">christmas tree post&lt;/a>, and took a capture at &lt;code>433.92MHz&lt;/code>
since it was in the middle of the band, and immediately got deja-vu. Not only
was the signal at &lt;code>433.92MHz&lt;/code>, but throwing the packet into &lt;code>inspectrum&lt;/code> gave
me the &lt;em>identical&lt;/em> plot of the OOK encoding scheme.&lt;/p>
&lt;p>&lt;img src="https://k3xec.com/imgs/td158/td158-packet.png" alt="">&lt;/p>
&lt;p>Not just similar &amp;ndash; identical. The only major difference was the baud rate and
bit structure of the packets, and the only minor difference was the existence
of what I think is a wakeup preamble packet (of all zeros), rather than a
preamble symbol that lasted longer than usual PHY symbol (which makes this
pager system a bit easier to work with than my tree, IMHO).&lt;/p>
&lt;p>Getting down to work, I took some measurements to determine what the symbol
duration was over the course of a few packets, I was able to determine the
symbol rate was somewhere around &lt;code>858&lt;/code> microseconds (&lt;code>0.000858&lt;/code> seconds per
symbol), which is a weird number, but maybe I&amp;rsquo;m slightly off or there&amp;rsquo;s some
larger math I&amp;rsquo;m missing that makes this number satisfyingly round (internal
low cost crystal clock or something? I assume this is some hardware constraint
with the pager?)&lt;/p>
&lt;p>Anyway, good enough. Moving along, let&amp;rsquo;s try our hand at a demod &amp;ndash; let&amp;rsquo;s just
assume it&amp;rsquo;s all the same as the chrismas tree post and demod ones and zeros
the same way here. That gives us 26 bits:&lt;/p>
&lt;pre tabindex="0">&lt;code>00001101110000001010001000
&lt;/code>&lt;/pre>&lt;p>Now, I know we need at least 10 bits for the base station ID, some number
of bits for the pager ID, and some bits for the command. This was a capture
of me hitting &amp;ldquo;call&amp;rdquo; from a base station ID of 55 to a pager with the ID of
10, so let&amp;rsquo;s blindly look for 10 bit chunks with the numbers we&amp;rsquo;re looking for:&lt;/p>
&lt;pre tabindex="0">&lt;code>0000110111 0000001010 001000
&lt;/code>&lt;/pre>&lt;p>Jeez. First try. 10 bits for the base station ID (55 in binary is
&lt;code>0000110111&lt;/code>), 10 bits for the pager ID (10 in binary is &lt;code>0000001010&lt;/code>), which
leaves us with 6 bits for a command (and maybe something else too?) &amp;ndash; which is
&lt;code>8&lt;/code> here. Great, cool, let&amp;rsquo;s work off that being the case and revisit it if
we hit bugs.&lt;/p>
&lt;p>Besides our data packet, there&amp;rsquo;s also a &amp;ldquo;preamble&amp;rdquo; packet that I&amp;rsquo;ll add in,
in case it&amp;rsquo;s used for signal detection or wakeup or something &amp;ndash; which is
fairly easy to do since it&amp;rsquo;s the same packet structure as the above, just
all zeros. Very kind of them to leave it with the same number of bits and
encoding scheme &amp;ndash; it&amp;rsquo;s nice that it can live outside the PHY.&lt;/p>
&lt;p>&lt;img src="https://k3xec.com/imgs/td158/td158-preamble.png" alt="">&lt;/p>
&lt;p>Once I got here, I wrote a quick and dirty modulator, and was able to ring up
pagers! Unmitigated success and good news &amp;ndash; only downside was that it took me
a single night, and not the multi-week adventure I was looking for. Well, let&amp;rsquo;s
finish the job and document what we&amp;rsquo;ve found for the sake of completeness.&lt;/p>
&lt;h1 id="boxing-everything-up">Boxing everything up&lt;/h1>
&lt;p>My best guess on the packet structure is as follows:&lt;/p>
&lt;div class="hz-abi">
&lt;div type="10 bits" class="hz-abi-green hz-abi-2b">base id&lt;/div>
&lt;div type="10 bits" class="hz-abi-green hz-abi-2b">argument&lt;/div>
&lt;div type="6 bits" class="hz-abi-green hz-abi-2b">command&lt;/div>
&lt;/div>
&lt;p>For a &lt;code>call&lt;/code> or &lt;code>F2&lt;/code> operation, the &lt;code>argument&lt;/code> is the Pager&amp;rsquo;s ID code,
but for other commands it&amp;rsquo;s a value or an enum, depending. Here&amp;rsquo;s a table
of my by-hand demodulation of all the packet types the base station
produces:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;td class="hz-1-6th">Type&lt;/td>
&lt;td class="hz-1-6th">Cmd Id&lt;/td>
&lt;td class="hz-4-6th">Description&lt;/td>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>&lt;td>Call&lt;/td>&lt;td>8&lt;/td>&lt;td>Call the pager identified by the id in &lt;code>argument&lt;/code>&lt;/td>&lt;/tr>
&lt;tr>&lt;td>Off&lt;/td>&lt;td>60&lt;/td>&lt;td>Request any pagers on the charger power off when power is removed, &lt;code>argument&lt;/code> is all zero&lt;/td>&lt;/tr>
&lt;tr>&lt;td>F2&lt;/td>&lt;td>40&lt;/td>&lt;td>Program a pager to the specified Pager ID (in &lt;code>argument&lt;/code>) and base station&lt;/td>&lt;/tr>
&lt;tr>&lt;td>F3&lt;/td>&lt;td>44&lt;/td>&lt;td>Set the reminder duration in seconds specified in &lt;code>argument&lt;/code>&lt;/td>&lt;/tr>
&lt;tr>&lt;td>F4&lt;/td>&lt;td>48&lt;/td>&lt;td>Set the pager's beep mode to the one in &lt;code>argument&lt;/code> (&lt;code>0&lt;/code> is disabled, &lt;code>1&lt;/code> is slow, &lt;code>2&lt;/code> is medium, &lt;code>3&lt;/code> is fast)&lt;/td>&lt;/tr>
&lt;tr>&lt;td>F5&lt;/td>&lt;td>52&lt;/td>&lt;td>Set the pager's vibration mode to the one in &lt;code>argument&lt;/code> (&lt;code>0&lt;/code> is disabled, &lt;code>1&lt;/code> is enabled)&lt;/td>&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;h1 id="kitchens-closed-for-the-night">Kitchen&amp;rsquo;s closed for the night&lt;/h1>
&lt;p>I&amp;rsquo;m not going to be publishing this code since I can&amp;rsquo;t think of a good use
anyone would have for this besides folks using a low cost SDR and annoying
local restaurants; but there&amp;rsquo;s enough here for folks who find this interesting
to try modulating this protocol on their own hardware if they want to buy their
own pack of pagers and give it a shot, which I do encourage! It&amp;rsquo;s fun! Radios
are great, and this is a good protocol to hack with &amp;ndash; it&amp;rsquo;s really nice.&lt;/p>
&lt;p>All in all, this wasn&amp;rsquo;t the multi-week adventure I was looking for, this was
still a great exercise and a fun reminder that I&amp;rsquo;ve come a far way from when
I&amp;rsquo;ve started. It felt a lot like cheating since I was able to infer a lot about
the PHY because I&amp;rsquo;d seen it before, but it was still a great time. I may grab a
few more restaurant pagers and see if I can find one with a more exotic PHY to
emulate next. I mean why not, I&amp;rsquo;ve already got the
&lt;a href="https://github.com/paultag/go-epson">thermal&lt;/a>
&lt;a href="https://crates.io/crates/epson">printer&lt;/a>
libraries
&lt;a href="https://soylent.green/@paul/111805908699444082">working&lt;/a>
🖨️&lt;/p></description></item><item><title>Writing a simulator to check phased array beamforming 🌀</title><link>https://k3xec.com/simulating-phased-arrays/</link><pubDate>Mon, 22 Jan 2024 15:11:41 +0000</pubDate><guid>https://k3xec.com/simulating-phased-arrays/</guid><description>&lt;div class="hz-alert-ok">
Interested in future updates? Follow me on mastodon at
&lt;a href="https://soylent.green/@paul">@paul@soylent.green&lt;/a>. Posts about
&lt;code>hz.tools&lt;/code> will be tagged
&lt;a href="https://soylent.green/@paul/tagged/hztools">#hztools&lt;/a>.
&lt;br />
&lt;br />
If you're on the Fediverse, I'd very much appreciate boosts on
&lt;a href="https://soylent.green/@paul/111788830823384684">my toot&lt;/a>!
&lt;/div>
&lt;p>While working on &lt;a href="https://k3xec.com/hztools">hz.tools&lt;/a>, I started to move my beamforming
code from 2-D (meaning, beamforming to some specific angle on the X-Y plane for
waves on the X-Y plane) to 3-D. I&amp;rsquo;ll have more to say about that once I get
around to publishing the code as soon as I&amp;rsquo;m sure it&amp;rsquo;s not completely wrong,
but in the meantime I decided to write a simple simulator to visually
check the beamformer against the textbooks. The results were pretty rad,
so I figured I&amp;rsquo;d throw together a post since it&amp;rsquo;s interesting all on its
own outside of beamforming as a general topic.&lt;/p>
&lt;p>I figured I&amp;rsquo;d write this in Rust, since I&amp;rsquo;ve been using Rust as my primary
language over at &lt;a href="https://zoo.dev/">zoo&lt;/a>, and it&amp;rsquo;s a good chance
to learn the language better.&lt;/p>
&lt;div class="hz-alert-warning">
⚠️ This post has some large GIFs&lt;br />
&lt;br />
It make take a little bit to load depending
on your internet connection. Sorry about that, I'm not clever enough to
do better without doing tons of complex engineering work. They may be
choppy while they load or something. I tried to compress an ensmall them,
so if they're loaded but fuzzy, click on them to load a slightly larger
version.
&lt;/div>
&lt;p>This post won&amp;rsquo;t cover the basics of how
&lt;a href="https://en.wikipedia.org/wiki/Phased_array">phased arrays work&lt;/a>
or the specifics of calculating the phase offsets for each antenna,
but I&amp;rsquo;ll dig into how I wrote a simple &amp;ldquo;simulator&amp;rdquo; and how I wound up
checking my phase offsets to generate the renders below.&lt;/p>
&lt;h1 id="assumptions">Assumptions&lt;/h1>
&lt;p>I didn&amp;rsquo;t want to build a general purpose RF simulator, anything particularly
generic, or something that would solve for any more than the things right
in front of me. To do this as simply (and quickly &amp;ndash; all this code took about
a day to write, including the beamforming math) &amp;ndash; I had to reduce the
amount of work in front of me.&lt;/p>
&lt;p>Given that I was concerend with visualizing what the antenna pattern would look
like in 3-D given some antenna geometry, operating frequency and configured
beam, I made the following assumptions:&lt;/p>
&lt;p>All anetnnas are perfectly isotropic &amp;ndash; they receive a signal that is
exactly the same strength no matter what direction the signal originates
from.&lt;/p>
&lt;p>There&amp;rsquo;s a single point-source isotropic emitter in the far-field (I modeled
this as being 1 million meters away &amp;ndash; 1000 kilometers) of the antenna system.&lt;/p>
&lt;p>There is no noise, multipath, loss or distortion in the signal as it travels
through space.&lt;/p>
&lt;p>Antennas will never interfere with each other.&lt;/p>
&lt;h1 id="2-d-polar-plots">2-D Polar Plots&lt;/h1>
&lt;p>The &lt;a href="https://soylent.green/@paul/109263478055233925">last time I wrote something like this&lt;/a>,
I generated 2-D GIFs which show a radiation pattern, not unlike the
&lt;a href="https://en.wikipedia.org/wiki/Microphone#Polar_patterns">polar plots you&amp;rsquo;d see on a microphone&lt;/a>.&lt;/p>
&lt;p>These are handy because it lets you visualize what the directionality of
the antenna looks like, as well as in what direction emissions are
captured, and in what directions emissions are nulled out. You can see
&lt;a href="https://en.wikipedia.org/wiki/Radiation_pattern">these plots&lt;/a>
on spec sheets for antennas in both 2-D and 3-D form.&lt;/p>
&lt;p>Now, let&amp;rsquo;s port the 2-D approach to 3-D and see how well it works out.&lt;/p>
&lt;h1 id="writing-the-3-d-simulator">Writing the 3-D simulator&lt;/h1>
&lt;p>As an EM wave travels through free space, the place at which you sample
the wave controls that phase you observe at each time-step. This means,
assuming perfectly synchronized clocks, a transmitter and receiver exactly
one RF wavelength apart will observe a signal in-phase, but a transmitter
and receiver a half wavelength apart will observe a signal 180 degrees
out of phase.&lt;/p>
&lt;p>This means that if we take the distance between our point-source and
antenna element, divide it by the wavelength, we can use the fractional
part of the resulting number to determine the phase observed. If we
multiply that number (in the range of 0 to just under 1) by
&lt;a href="https://tauday.com/">tau&lt;/a>, we can generate a complex number by taking the
&lt;code>cos&lt;/code> and &lt;code>sin&lt;/code> of the multiplied phase (in the range of 0 to tau), assuming
the transmitter is emitting a carrier wave at a static amplitude and all
clocks are in perfect sync.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-rust" data-lang="rust">&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">let&lt;/span> observed_phases: Vec&lt;span style="color:#f92672">&amp;lt;&lt;/span>Complex&lt;span style="color:#f92672">&amp;gt;&lt;/span> &lt;span style="color:#f92672">=&lt;/span> antennas
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .iter()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .map(&lt;span style="color:#f92672">|&lt;/span>antenna&lt;span style="color:#f92672">|&lt;/span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">let&lt;/span> distance &lt;span style="color:#f92672">=&lt;/span> (antenna &lt;span style="color:#f92672">-&lt;/span> tx).magnitude();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">let&lt;/span> distance &lt;span style="color:#f92672">=&lt;/span> distance &lt;span style="color:#f92672">-&lt;/span> (distance &lt;span style="color:#66d9ef">as&lt;/span> &lt;span style="color:#66d9ef">i64&lt;/span> &lt;span style="color:#66d9ef">as&lt;/span> &lt;span style="color:#66d9ef">f64&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ((distance &lt;span style="color:#f92672">/&lt;/span> wavelength) &lt;span style="color:#f92672">*&lt;/span> &lt;span style="color:#66d9ef">TAU&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> })
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .map(&lt;span style="color:#f92672">|&lt;/span>phase&lt;span style="color:#f92672">|&lt;/span> Complex(phase.cos(), phase.sin()))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .collect();
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>At this point, given some synthetic transmission point and each antenna, we
know what the expected complex sample would be at each antenna. At this point,
we can adjust the phase of each antenna according to the beamforming
phase offset configuration, and add up every sample in order to determine
what the entire system would collectively produce a sample as.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-rust" data-lang="rust">&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">let&lt;/span> beamformed_phases: Vec&lt;span style="color:#f92672">&amp;lt;&lt;/span>Complex&lt;span style="color:#f92672">&amp;gt;&lt;/span> &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#f92672">..&lt;/span>.;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">let&lt;/span> magnitude &lt;span style="color:#f92672">=&lt;/span> beamformed_phases
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .iter()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .zip(observed_phases.iter())
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .map(&lt;span style="color:#f92672">|&lt;/span>(beamformed, observed)&lt;span style="color:#f92672">|&lt;/span> observed &lt;span style="color:#f92672">*&lt;/span> beamformed)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .reduce(&lt;span style="color:#f92672">|&lt;/span>acc, el&lt;span style="color:#f92672">|&lt;/span> acc &lt;span style="color:#f92672">+&lt;/span> el)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .unwrap()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .abs();
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Armed with this information, it&amp;rsquo;s straight forward to generate some number
of (Azimuth, Elevation) points to sample, generate a transmission point
far away in that direction, resolve what the resulting Complex sample would be,
take its magnitude, and use that to create an &lt;code>(x, y, z)&lt;/code> point at
&lt;code>(azimuth, elevation, magnitude)&lt;/code>. The color attached two that point is
based on its distance from &lt;code>(0, 0, 0)&lt;/code>. I opted to use the
&lt;a href="https://en.wikipedia.org/wiki/The_Life_Aquatic_with_Steve_Zissou">Life Aquatic&lt;/a>
table for this one.&lt;/p>
&lt;p>After this process is complete, I have a
&lt;a href="https://en.wikipedia.org/wiki/Point_cloud">point cloud&lt;/a> of
&lt;code>((x, y, z), (r, g, b))&lt;/code> points. I wrote a small program using
&lt;a href="https://docs.rs/kiss3d/">kiss3d&lt;/a> to render point cloud using tons of
small spheres, and write out the frames to a set of PNGs, which get compiled
into a GIF.&lt;/p>
&lt;p>Now for the fun part, let&amp;rsquo;s take a look at some radiation patterns!&lt;/p>
&lt;h1 id="1x4-phased-array">1x4 Phased Array&lt;/h1>
&lt;p>The first configuration is a phased array where all the elements are
in perfect alignment on the &lt;code>y&lt;/code> and &lt;code>z&lt;/code> axis, and separated by some
offset in the &lt;code>x&lt;/code> axis. This configuration can sweep 180 degrees (not
the full 360), but can&amp;rsquo;t be steared in elevation at all.&lt;/p>
&lt;p>Let&amp;rsquo;s take a look at what this looks like for a well constructed
1x4 phased array:&lt;/p>
&lt;p>&lt;img src="https://k3xec.com/imgs/phased-array-simulations/1x4_overview.png" alt="">&lt;/p>
&lt;p>And now let&amp;rsquo;s take a look at the renders as we play with the configuration of
this array and make sure things look right. Our initial quarter-wavelength
spacing is very effective and has some outstanding performance characteristics.
Let&amp;rsquo;s check to see that everything looks right as a first test.&lt;/p>
&lt;p>&lt;img src="https://k3xec.com/imgs/phased-array-simulations/1x4_quarter_lambda_sweep_az_small.gif" alt="">&lt;/p>
&lt;p>Nice. Looks perfect. When pointing forward at &lt;code>(0, 0)&lt;/code>, we&amp;rsquo;d expect to see a
torus, which we do. As we sweep between 0 and 360, astute observers will notice
the pattern is mirrored along the axis of the antennas, when the beam is facing
forward to 0 degrees, it&amp;rsquo;ll also receive at 180 degrees just as strong. There&amp;rsquo;s
a small sidelobe that forms when it&amp;rsquo;s configured along the array, but
it also becomes the most directional, and the sidelobes remain fairly small.&lt;/p>
&lt;h2 id="long-compared-to-the-wavelength-1-λ">Long compared to the wavelength (1¼ λ)&lt;/h2>
&lt;p>Let&amp;rsquo;s try again, but rather than spacing each antenna ¼ of a wavelength
apart, let&amp;rsquo;s see about spacing each antenna 1¼ of a wavelength apart instead.&lt;/p>
&lt;p>&lt;img src="https://k3xec.com/imgs/phased-array-simulations/1x4_long_baseline_sweep_az_small.gif" alt="">&lt;/p>
&lt;p>The main lobe is a lot more narrow (not a bad thing!), but some significant
sidelobes have formed (not ideal). This can cause a lot of confusion when doing
things that require a lot of directional resolution unless they&amp;rsquo;re compensated
for.&lt;/p>
&lt;h2 id="going-from--to-5-λ">Going from (¼ to 5¼ λ)&lt;/h2>
&lt;p>The last model begs the question - what do things look like when you separate
the antennas from each other but without moving the beam? Let&amp;rsquo;s simulate moving
our antennas but not adjusting the configured beam or operating frequency.&lt;/p>
&lt;p>&lt;img src="https://k3xec.com/imgs/phased-array-simulations/1x4_baseline_quarter_to_5_lambda_small.gif" alt="">&lt;/p>
&lt;p>Very cool. As the spacing becomes longer in relation to the operating frequency,
we can see the sidelobes start to form out of the end of the antenna system.&lt;/p>
&lt;h1 id="2x2-phased-array">2x2 Phased Array&lt;/h1>
&lt;p>The second configuration I want to try is a phased array where the elements
are in perfect alignment on the &lt;code>z&lt;/code> axis, and separated by a fixed offset
in either the &lt;code>x&lt;/code> or &lt;code>y&lt;/code> axis by their neighbor, forming a square when
viewed along the x/y axis.&lt;/p>
&lt;p>Let&amp;rsquo;s take a look at what this looks like for a well constructed
2x2 phased array:&lt;/p>
&lt;p>&lt;img src="https://k3xec.com/imgs/phased-array-simulations/2x2_overview.png" alt="">&lt;/p>
&lt;p>Let&amp;rsquo;s do the same as above and take a look at the renders as we play with the
configuration of this array and see what things look like. This configuration
should suppress the sidelobes and give us good performance, and even give us
some amount of control in elevation while we&amp;rsquo;re at it.&lt;/p>
&lt;p>&lt;img src="https://k3xec.com/imgs/phased-array-simulations/2x2_quarter_lambda_sweep_az_el_small.gif" alt="">&lt;/p>
&lt;p>Sweet. Heck yeah. The array is quite directional in the configured direction,
and can even sweep a little bit in elevation, a definite improvement
from the 1x4 above.&lt;/p>
&lt;h2 id="long-compared-to-the-wavelength-1-λ-1">Long compared to the wavelength (1¼ λ)&lt;/h2>
&lt;p>Let&amp;rsquo;s do the same thing as the 1x4 and take a look at what happens when the
distance between elements is long compared to the frequency of operation &amp;ndash;
say, 1¼ of a wavelength apart? What happens to the sidelobes given this
spacing when the frequency of operation is much different than the physical
geometry?&lt;/p>
&lt;p>&lt;img src="https://k3xec.com/imgs/phased-array-simulations/2x2_long_baseline_sweep_az_el_small.gif" alt="">&lt;/p>
&lt;p>Mesmerising. This is my favorate render. The sidelobes are very fun to
watch come in and out of existence. It looks absolutely other-worldly.&lt;/p>
&lt;h2 id="going-from--to-5-λ-1">Going from (¼ to 5¼ λ)&lt;/h2>
&lt;p>Finally, for completeness&amp;rsquo; sake, what do things look like when you separate the
antennas from each other just as we did with the 1x4? Let&amp;rsquo;s simulate moving our
antennas but not adjusting the configured beam or operating frequency.&lt;/p>
&lt;p>&lt;img src="https://k3xec.com/imgs/phased-array-simulations/2x2_baseline_quarter_to_5_lambda_small.gif" alt="">&lt;/p>
&lt;p>Very very cool. The sidelobes wind up turning the very blobby cardioid into
an electromagnetic dog toy. I think we&amp;rsquo;ve proven to ourselves that using
a phased array much outside its designed frequency of operation seems like
a real bad idea.&lt;/p>
&lt;h1 id="future-work">Future Work&lt;/h1>
&lt;p>Now that I have a system to test things out, I&amp;rsquo;m a bit more confident that
my beamforming code is close to right! I&amp;rsquo;d love to push that code over
the line and blog about it, since it&amp;rsquo;s a really interesting topic on its own.
Once I&amp;rsquo;m sure the code involved isn&amp;rsquo;t full of lies, I&amp;rsquo;ll put it up on the
&lt;a href="https://github.com/hztools">hztools org&lt;/a>, and post about it here and on
mastodon.&lt;/p></description></item><item><title>Overview of the AudioSocket protocol 📞</title><link>https://k3xec.com/audio-socket/</link><pubDate>Wed, 13 Dec 2023 00:55:09 -0500</pubDate><guid>https://k3xec.com/audio-socket/</guid><description>&lt;p>The &lt;a href="https://www.asterisk.org/">asterisk&lt;/a> VoIP project has a protocol
built-in called &amp;ldquo;AudioSocket&amp;rdquo;. AudioSocket is built on top of TCP,
streaming int16 values at a sample rate of 8 kHz, neither of those
options are configurable (by design). AudioSocket will stream audio
from the connected phone to the tcp server, and play audio samples
sent from the tcp server to the phone.&lt;/p>
&lt;div class="hz-alert-warning">
This documentation is a work in progress, and a result of
source code spelunking or reverse engineering. It may contain
errors or outright lies. The names may not match the original
name, but it's been documented on a best-effort basis to help
future engineering efforts.
&lt;/div>
&lt;h2 id="audiosocket-packet">AudioSocket Packet&lt;/h2>
&lt;p>Data is exchanged over &lt;code>AudioSocket&lt;/code> by framing data into
&lt;a href="https://en.wikipedia.org/wiki/Type%E2%80%93length%E2%80%93value">TLV&lt;/a>
packets. This should be a pretty natural concept for anyone
who&amp;rsquo;s worked on other line encoding schemes like &lt;code>ASN.1&lt;/code>, &lt;code>SSH&lt;/code>, &lt;code>PGP&lt;/code>,
or &lt;code>protobuf&lt;/code>.&lt;/p>
&lt;p>The &lt;code>type&lt;/code> is a &lt;code>uint8&lt;/code>, length is transmitted as a &lt;code>uint16&lt;/code>, and the
payload is a variable sized block of data.&lt;/p>
&lt;p>The &lt;strong>header&lt;/strong> is encoded using
&lt;span class="hz-highlight">network byte order&lt;/span> (big endian).
The only field this really matters for is the &lt;code>length&lt;/code> field, since
the &lt;code>type&lt;/code> field is &lt;code>uint8&lt;/code>. The &lt;code>payload&lt;/code> format is dependent on the
&lt;code>type&lt;/code> of message.&lt;/p>
&lt;div class="hz-abi">
&lt;div type="uint8" class="hz-abi-yellow hz-abi-1b">
type
&lt;/div>
&lt;div type="uint16" class="hz-abi-yellow hz-abi-2b">
length
&lt;/div>
&lt;div type="[]uint8" class="hz-abi-yellow hz-abi-Nb">
payload
&lt;/div>
&lt;/div>
&lt;p>A full list of Commands, and the semantics of their Argument
is detailed on the table below.&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;td class="hz-1-6th">Command&lt;/td>
&lt;td class="hz-1-6th">Definition&lt;/td>
&lt;td class="hz-2-3rd">Payload&lt;/td>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>0x00&lt;/td>
&lt;td>Terminate&lt;/td>
&lt;td>none&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>0x01&lt;/td>
&lt;td>UUID&lt;/td>
&lt;td>16-byte UUID encoded as raw bytes.&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>0x10&lt;/td>
&lt;td>Audio Samples&lt;/td>
&lt;td>variable length buffer of &lt;b>little endian&lt;/b> signed 16 bit integers sampled at 8 kHz&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>0xFF&lt;/td>
&lt;td>Error&lt;/td>
&lt;td>byte (see table below)&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>The most simple (and also shortest) command for AudioSocket is the &amp;ldquo;&lt;code>Terminate&lt;/code>&amp;rdquo;
command, which can be used to indicate that the connection should be tore down,
which is a type of &lt;code>0x00&lt;/code>, and no payload (length of &lt;code>0&lt;/code>, no body). This would
be encoded as &lt;code>[0x00, 0x00, 0x00]&lt;/code>.&lt;/p>
&lt;h2 id="well-known-error-codes">Well known Error Codes&lt;/h2>
&lt;p>The length of the &lt;code>Error&lt;/code> packet is not defined, and may be any length.
According to an &lt;code>AudioSocket&lt;/code> Go library
(&lt;a href="https://github.com/CyCoreSystems/audiosocket">github.com/CyCoreSystems/audiosocket&lt;/a>),
Asterisk has the following well known error codes (although I can&amp;rsquo;t seem to
find these in the source, if anyone has a link). Given the most common
implementation is asterisk, I suspect mandating a 1-byte Error code is not
a bad idea.&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;td class="hz-1-6th">Code&lt;/td>
&lt;td class="hz-1-6th">Impl&lt;/td>
&lt;td class="hz-2-3rd">Description&lt;/td>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>0x01&lt;/td>
&lt;td>Asterisk&lt;/td>
&lt;td>Caller has hung up the Connection&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>0x02&lt;/td>
&lt;td>Asterisk&lt;/td>
&lt;td>Error forwarding the Frame to the caller&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>0x04&lt;/td>
&lt;td>Asterisk&lt;/td>
&lt;td>Internal memory allocation error&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;h1 id="example-packets">Example Packets&lt;/h1>
&lt;p>Terminate the connection:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-text" data-lang="text">&lt;span style="display:flex;">&lt;span>0x00 0x00 0x00
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Indicate an error state of &lt;code>0x11&lt;/code>&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-text" data-lang="text">&lt;span style="display:flex;">&lt;span>0xFF 0x00 0x01 0x11
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Send 2 audio samples of &lt;code>+1&lt;/code> and &lt;code>-1&lt;/code>&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-text" data-lang="text">&lt;span style="display:flex;">&lt;span>0x10 0x00 0x04 0x01 0x00 0xFF 0xFF
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h1 id="handshake">Handshake&lt;/h1>
&lt;p>After a TCP connection is established, the client is expected to send a
&lt;code>UUID&lt;/code> Packet to the server, which has an application dependent meaning. It
could indicate the audio stream to attach to, an identity, or an API key
depending on how the server uses it.&lt;/p>
&lt;p>After the &lt;code>UUID&lt;/code> packet is sent, both the Client and the Server begin to send
&lt;code>Audio&lt;/code> packets to their peer until the TCP connection is closed, the
&lt;code>Terminate&lt;/code> command is issued, or an &lt;code>Error&lt;/code> packet is sent.&lt;/p>
&lt;h1 id="implementation-notes">Implementation Notes&lt;/h1>
&lt;p>Because the aduio stream needs to be very low latency, it&amp;rsquo;s advisable to
set &lt;code>TCP_NODELAY&lt;/code>, in order to disable Nagle&amp;rsquo;s algorithm on the TCP connection.
The reason is that we&amp;rsquo;re sending many small packets with time sensitive
audio information which need to be sent right away, even if there is more data
to be sent very shortly after.&lt;/p>
&lt;p>Additionally, Asterisk specifically will be very upset if you send headers,
and reading the body takes more than 5ms, even if there&amp;rsquo;s a buffer you never
exhaust. This state is hard to hit when the audio data is contained in an IP
packet, but it&amp;rsquo;s very easy to trigger when you&amp;rsquo;re operating under Nagle&amp;rsquo;s
algorithm, since your packet is likely to be split along non-packet boundaries.&lt;/p></description></item><item><title>Announcing hz.tools</title><link>https://k3xec.com/hztools/</link><pubDate>Wed, 22 Feb 2023 21:00:00 -0500</pubDate><guid>https://k3xec.com/hztools/</guid><description>&lt;div class="hz-alert-ok">
Interested in future updates? Follow me on mastodon at
&lt;a href="https://soylent.green/@paul">@paul@soylent.green&lt;/a>. Posts about
&lt;code>hz.tools&lt;/code> will be tagged
&lt;a href="https://soylent.green/@paul/tagged/hztools">#hztools&lt;/a>.&lt;br />
&lt;br />
If you're on the Fediverse, I'd very much appreciate boosts on
&lt;a href="https://soylent.green/@paul/109911598120551418">my announcement toot&lt;/a>!
&lt;/div>
&lt;p>Ever since 2019, I&amp;rsquo;ve been learning about how radios work, and trying to learn
about using them &amp;ldquo;the hard way&amp;rdquo; &amp;ndash; by writing as much of the stack as is
practical (for some value of practical) myself. I wrote my first &amp;ldquo;Hello World&amp;rdquo;
in 2018, which was a simple FM radio player, which used &lt;code>librtlsdr&lt;/code> to read in
an IQ stream, did some filtering, and played the real valued audio stream via
&lt;code>pulseaudio&lt;/code>. Over 4 years this has slowly grown through persistence, lots of
questions to too many friends to thank (although I will try), and the eternal
patience of my wife hearing about radios nonstop &amp;ndash; for years &amp;ndash; into a number
of Go repos that can do quite a bit, and support a handful of radios.&lt;/p>
&lt;p>&lt;strong>I&amp;rsquo;ve resisted making the repos public not out of embarrassment or a desire to
keep secrets, but rather, an attempt to keep myself free of any maintenance
obligations to users &amp;ndash; so that I could freely break my own API, add and remove
API surface as I saw fit. The worst case was to have this project feel like
work, and I can&amp;rsquo;t imagine that will happen if I feel frustrated by PRs
that are &amp;ldquo;getting ahead of me&amp;rdquo; &amp;ndash; solving problems I didn&amp;rsquo;t yet know about, or
bugs I didn&amp;rsquo;t understand the fix for.&lt;/strong>&lt;/p>
&lt;p>As my rate of changes to the most central dependencies has slowed, i&amp;rsquo;ve begun
to entertain the idea of publishing them. After a bit of back and forth, I&amp;rsquo;ve
decided
&lt;span class="hz-highlight">it&amp;rsquo;s time to make a number of them public&lt;/span>,
and to start working on them in the open, as I&amp;rsquo;ve built up a bit of knowledge
in the space, and I and feel confident that the repo doesn&amp;rsquo;t contain overt
lies. That&amp;rsquo;s not to say it doesn&amp;rsquo;t contain lies, but those lies are likely
hidden and lurking in the dark. Beware.&lt;/p>
&lt;p>That being said, it shouldn&amp;rsquo;t be a surprise to say I&amp;rsquo;ve not published
everything yet &amp;ndash; for the same reasons as above. I plan to open repos as the
rate of changes slows and I understand the problems the library solves well
enough &amp;ndash; or if the project &amp;ldquo;dead ends&amp;rdquo; and I&amp;rsquo;ve stopped learning.&lt;/p>
&lt;h2 id="intention-behind-hztools">Intention behind hz.tools&lt;/h2>
&lt;p>It&amp;rsquo;s my sincere hope that my repos help to make Software Defined Radio (SDR)
code a bit easier to understand, and serves as an understandable framework to
learn with. It&amp;rsquo;s a large codebase, but one that is possible to sit down and
understand because, well, it was written by a single person. Frankly, I&amp;rsquo;m also
not productive enough in my free time in the middle of the night and on
weekends and holidays to create a codebase that&amp;rsquo;s too large to understand, I
hope!&lt;/p>
&lt;p>I remain wary of this project turning into work, so my goal is to be very
upfront about my boundaries, and the limits of what classes of contributions
i&amp;rsquo;m interested in seeing.&lt;/p>
&lt;p>Here&amp;rsquo;s some &lt;strong>goals&lt;/strong> of open sourcing these repos:&lt;/p>
&lt;ul>
&lt;li>I &lt;strong>do&lt;/strong> want this library to be used to learn with. Please go through it
all and use it to learn about radios and how software can control them!&lt;/li>
&lt;li>I &lt;strong>am&lt;/strong> interested in bugs if there&amp;rsquo;s a problem you discover. Such bugs
are likely a great chance for me to fix something I&amp;rsquo;ve misunderstood or
typoed.&lt;/li>
&lt;li>I &lt;strong>am&lt;/strong> interested in PRs fixing bugs you find. I may need a bit of a
back and forth to fully understand the problem if I do not understand
the bug and fix yet. I hope you may have some grace if it&amp;rsquo;s taking a long
time.&lt;/li>
&lt;/ul>
&lt;p>Here&amp;rsquo;s a list of some &lt;strong>anti-goals&lt;/strong> of open sourcing these repos.&lt;/p>
&lt;ul>
&lt;li>I do &lt;strong>not&lt;/strong> want this library to become a critical dependency of an
important project, since I do not have the time to deal with the maintenance
burden. Putting me in that position is going to make me very uncomfortable.&lt;/li>
&lt;li>I am &lt;strong>not&lt;/strong> interested in feature requests, the features have grown as
I&amp;rsquo;ve hit problems, I&amp;rsquo;m not interested in building or maintaining features
for features sake. The API surface should be exposed enough to allow others
to experiment with such things out-of-tree.&lt;/li>
&lt;li>I&amp;rsquo;m &lt;strong>not&lt;/strong> interested in clever code replacing clear code without a very
compelling reason.&lt;/li>
&lt;li>I use GNU/Linux (specifically Debian
&lt;img src="https://k3xec.com/imgs/emojo/Debian.png" style="width: 1em; height: 1em;"/>),
and from time-to-time I&amp;rsquo;ve made sure that my code runs on OpenBSD
&lt;img src="https://k3xec.com/imgs/emojo/OpenBSD.png" style="width: 1em; height: 1em;" /> too.
Platforms beyond that will likely not be supported at the expense of either
of those two. I&amp;rsquo;ll take fixes for bugs that fix a problem on another
platform, but not damage the code to work around issues / lack of features
on other platforms (like Windows).&lt;/li>
&lt;/ul>
&lt;p>&lt;span class="hz-highlight">I&amp;rsquo;m not saying all this to be a jerk, I do it to
make sure I can continue on my journey to learn about how radios work without
my full time job becoming maintaining a radio framework single-handedly for
other people to use &amp;ndash; even if it means I need to close PRs or bugs without
merging it or fixing the issue.&lt;/span>&lt;/p>
&lt;p>&lt;strong>With all that out of the way, I&amp;rsquo;m very happy to announce that the repos are now
public under &lt;a href="https://github.com/hztools">github.com/hztools&lt;/a>.&lt;/strong>&lt;/p>
&lt;h2 id="should-you-use-this">Should you use this?&lt;/h2>
&lt;p>&lt;strong>Probably not&lt;/strong>. The intent here is not to provide a general purpose Go SDR
framework for everyone to build on, although I am keenly aware it looks and
feels like it, since that what it is to me. This is a learning project, so for
any use beyond joining me in learning should use something like
&lt;a href="https://www.gnuradio.org/">GNU Radio&lt;/a>
or a similar framework that has a community behind it.&lt;/p>
&lt;p>In fact, I suspect most contributors ought to be contributing to GNU Radio, and
not this project. If I can encourage people to do so,
&lt;a href="https://wiki.gnuradio.org/index.php?title=HowToGetInvolved">contribute to GNU Radio&lt;/a>!
Nothing makes me happier than seeing GNU Radio continue to be the go-to, and
well supported. Consider
&lt;a href="https://donorbox.org/gnuradio">donating&lt;/a> to GNU Radio!&lt;/p>
&lt;h2 id="hztoolsrf---frequency-types">hz.tools/rf - Frequency types&lt;/h2>
&lt;p>The &lt;code>hz.tools/rf&lt;/code> library contains the abstract concept of frequency, and
some very basic helpers to interact with frequency ranges (such as helpers
to deal with frequency ranges, or frequency range math) as well as frequencies
and some very basic conversions (to meters, etc) and parsers (to parse values
like &lt;code>10MHz&lt;/code>). This ensures that all the &lt;code>hz.tools&lt;/code> libraries have a shared
understanding of Frequencies, a standard way of representing ranges of
Frequencies, and the ability to handle the IO boundary with things like CLI
arguments, JSON or YAML.&lt;/p>
&lt;p>The git repo can be found at
&lt;a href="https://github.com/hztools/go-rf">github.com/hztools/go-rf&lt;/a>, and is
importable as &lt;a href="https://pkg.go.dev/hz.tools/rf">hz.tools/rf&lt;/a>.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-go" data-lang="go">&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e">// Parse a frequency using hz.tools/rf.ParseHz, and print it to stdout.&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">freq&lt;/span> &lt;span style="color:#f92672">:=&lt;/span> &lt;span style="color:#a6e22e">rf&lt;/span>.&lt;span style="color:#a6e22e">MustParseHz&lt;/span>(&lt;span style="color:#e6db74">&amp;#34;-10kHz&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">fmt&lt;/span>.&lt;span style="color:#a6e22e">Printf&lt;/span>(&lt;span style="color:#e6db74">&amp;#34;Frequency: %s\n&amp;#34;&lt;/span>, &lt;span style="color:#a6e22e">freq&lt;/span>&lt;span style="color:#f92672">+&lt;/span>&lt;span style="color:#a6e22e">rf&lt;/span>.&lt;span style="color:#a6e22e">MHz&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e">// Prints: &amp;#39;Frequency: 990kHz&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e">// Return the Intersection between two RF ranges, and print&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e">// it to stdout.&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">r1&lt;/span> &lt;span style="color:#f92672">:=&lt;/span> &lt;span style="color:#a6e22e">rf&lt;/span>.&lt;span style="color:#a6e22e">Range&lt;/span>{&lt;span style="color:#a6e22e">rf&lt;/span>.&lt;span style="color:#a6e22e">KHz&lt;/span>, &lt;span style="color:#a6e22e">rf&lt;/span>.&lt;span style="color:#a6e22e">MHz&lt;/span>}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">r2&lt;/span> &lt;span style="color:#f92672">:=&lt;/span> &lt;span style="color:#a6e22e">rf&lt;/span>.&lt;span style="color:#a6e22e">Range&lt;/span>{&lt;span style="color:#a6e22e">rf&lt;/span>.&lt;span style="color:#a6e22e">Hz&lt;/span>(&lt;span style="color:#ae81ff">10&lt;/span>), &lt;span style="color:#a6e22e">rf&lt;/span>.&lt;span style="color:#a6e22e">KHz&lt;/span> &lt;span style="color:#f92672">*&lt;/span> &lt;span style="color:#ae81ff">100&lt;/span>}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">fmt&lt;/span>.&lt;span style="color:#a6e22e">Printf&lt;/span>(&lt;span style="color:#e6db74">&amp;#34;Range: %s\n&amp;#34;&lt;/span>, &lt;span style="color:#a6e22e">r1&lt;/span>.&lt;span style="color:#a6e22e">Intersection&lt;/span>(&lt;span style="color:#a6e22e">r2&lt;/span>))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e">// Prints: Range: 1000Hz-&amp;gt;100kHz&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>These can be used to represent tons of things - ranges can be used for things
like the tunable range of an SDR, the bandpass of a filter or the frequencies
that correspond to a bin of an FFT, while frequencies can be used for things
such as frequency offsets or the tuned center frequency.&lt;/p>
&lt;h2 id="hztoolssdr---sdr-io-and-iq-types">hz.tools/sdr - SDR I/O and IQ Types&lt;/h2>
&lt;p>This&amp;hellip; is the big one. This library represents the majority of the shared
types and bindings, and is likely the most useful place to look at when
learning about the IO boundary between a program and an SDR.&lt;/p>
&lt;p>The git repo can be found at
&lt;a href="https://github.com/hztools/go-sdr">github.com/hztools/go-sdr&lt;/a>, and is
importable as &lt;a href="https://pkg.go.dev/hz.tools/sdr">hz.tools/sdr&lt;/a>.&lt;/p>
&lt;p>This library is designed to look (and in some cases, mirror) the Go &lt;code>io&lt;/code> idioms
so that this library feels as idiomatic as it can, so that Go builtins interact
with IQ in a way that&amp;rsquo;s possible to reason about, and to avoid reinventing the
wheel by designing new API surface. While some of the API looks (and is even
called) the same thing as a similar function in &lt;code>io&lt;/code>, the implementation is
usually a &lt;strong>lot&lt;/strong> more naive, and may have unexpected sharp edges such as
concurrency issues or performance problems.&lt;/p>
&lt;p>The following IQ types are implemented using the &lt;code>sdr.Samples&lt;/code> interface. The
&lt;code>hz.tools/sdr&lt;/code> package contains helpers for conversion between types, and some
basic manipulation of IQ streams.&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>IQ Format&lt;/th>
&lt;th>hz.tools Name&lt;/th>
&lt;th>Underlying Go Type&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Interleaved uint8 (rtl-sdr)&lt;/td>
&lt;td>&lt;code>sdr.SamplesU8&lt;/code>&lt;/td>
&lt;td>&lt;code>[][2]uint8&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Interleaved int8 (hackrf, uhd)&lt;/td>
&lt;td>&lt;code>sdr.SamplesI8&lt;/code>&lt;/td>
&lt;td>&lt;code>[][2]int8&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Interleaved int16 (pluto, uhd)&lt;/td>
&lt;td>&lt;code>sdr.SamplesI16&lt;/code>&lt;/td>
&lt;td>&lt;code>[][2]int16&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Interleaved float32 (airspy, uhd)&lt;/td>
&lt;td>&lt;code>sdr.SamplesC64&lt;/code>&lt;/td>
&lt;td>&lt;code>[]complex64&lt;/code>&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>The following SDRs have implemented drivers in-tree.&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>SDR&lt;/th>
&lt;th>Format&lt;/th>
&lt;th>RX/TX&lt;/th>
&lt;th>State&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>rtl&lt;/td>
&lt;td>u8&lt;/td>
&lt;td>RX&lt;/td>
&lt;td>Good&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>HackRF&lt;/td>
&lt;td>i8&lt;/td>
&lt;td>RX/TX&lt;/td>
&lt;td>Good&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>PlutoSDR&lt;/td>
&lt;td>i16&lt;/td>
&lt;td>RX/TX&lt;/td>
&lt;td>Good&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>rtl kerberos&lt;/td>
&lt;td>u8&lt;/td>
&lt;td>RX&lt;/td>
&lt;td>Old&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>uhd&lt;/td>
&lt;td>i16/c64/i8&lt;/td>
&lt;td>RX/TX&lt;/td>
&lt;td>Good&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>airspyhf&lt;/td>
&lt;td>c64&lt;/td>
&lt;td>RX&lt;/td>
&lt;td>Exp&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>The following major packages and subpackages exist at the time of writing:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Import&lt;/th>
&lt;th>What is it?&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>hz.tools/sdr&lt;/td>
&lt;td>Core IQ types, supporting types and implementations that interact with the byte boundary&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>hz.tools/sdr/rtl&lt;/td>
&lt;td>&lt;code>sdr.Receiver&lt;/code> implementation using &lt;code>librtlsdr&lt;/code>.&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>hz.tools/sdr/rtl/kerberos&lt;/td>
&lt;td>Helpers to enable coherent RX using the Kerberos SDR.&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>hz.tools/sdr/rtl/e4k&lt;/td>
&lt;td>Helpers to interact with the &lt;a href="https://hz.tools/e4k/">E4000&lt;/a> RTL-SDR dongle.&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>hz.tools/sdr/fft&lt;/td>
&lt;td>Interfaces for performing an FFT, which are implemented by other packages.&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>hz.tools/sdr/rtltcp&lt;/td>
&lt;td>&lt;code>sdr.Receiver&lt;/code> implementation for &lt;a href="https://hz.tools/rtl_tcp/">rtl_tcp&lt;/a> servers.&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>hz.tools/sdr/pluto&lt;/td>
&lt;td>&lt;code>sdr.Transceiver&lt;/code> implementation for the PlutoSDR using &lt;code>libiio&lt;/code>.&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>hz.tools/sdr/uhd&lt;/td>
&lt;td>&lt;code>sdr.Transceiver&lt;/code> implementation for UHD radios, specifically the B210 and B200mini&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>hz.tools/sdr/hackrf&lt;/td>
&lt;td>&lt;code>sdr.Transceiver&lt;/code> implementation for the HackRF using &lt;code>libhackrf&lt;/code>.&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>hz.tools/sdr/mock&lt;/td>
&lt;td>Mock SDR for testing purposes.&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>hz.tools/sdr/airspyhf&lt;/td>
&lt;td>&lt;code>sdr.Receiver&lt;/code> implementation for the AirspyHF+ Discovery with &lt;code>libairspyhf&lt;/code>.&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>hz.tools/sdr/internal/simd&lt;/td>
&lt;td>SIMD helpers for IQ operations, written in Go ASM. This isn&amp;rsquo;t the best to learn from, and it contains pure go implemtnations alongside.&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>hz.tools/sdr/stream&lt;/td>
&lt;td>Common Reader/Writer helpers that operate on IQ streams.&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;h2 id="hztoolsfftw---hztoolssdrfft-implementation">hz.tools/fftw - hz.tools/sdr/fft implementation&lt;/h2>
&lt;p>The &lt;code>hz.tools/fftw&lt;/code> package contains bindings to &lt;code>libfftw3&lt;/code> to implement
the &lt;code>hz.tools/sdr/fft.Planner&lt;/code> type to transform between the time and
frequency domain.&lt;/p>
&lt;p>The git repo can be found at
&lt;a href="https://github.com/hztools/go-fftw">github.com/hztools/go-fftw&lt;/a>, and is
importable as &lt;a href="https://pkg.go.dev/hz.tools/fftw">hz.tools/fftw&lt;/a>.&lt;/p>
&lt;p>This is the default throughout most of my codebase, although that default is
only expressed at the &amp;ldquo;leaf&amp;rdquo; package &amp;ndash; libraries should not be hardcoding the
use of this library in favor of taking an &lt;code>fft.Planner&lt;/code>, unless it&amp;rsquo;s used as
part of testing. There are a bunch of ways to do an FFT out there, things like
&lt;code>clFFT&lt;/code> or a pure-go FFT implementation could be plugged in depending on what&amp;rsquo;s
being solved for.&lt;/p>
&lt;h2 id="hztoolsfmam---analog-audio-demodulation-and-modulation">hz.tools/{fm,am} - analog audio demodulation and modulation&lt;/h2>
&lt;p>The &lt;code>hz.tools/fm&lt;/code> and &lt;code>hz.tools/am&lt;/code> packages contain demodulators for
AM analog radio, and FM analog radio. This code is a bit old, so it has
a lot of room for cleanup, but it&amp;rsquo;ll do a very basic demodulation of IQ
to audio.&lt;/p>
&lt;p>The git repos can be found at
&lt;a href="https://github.com/hztools/go-fm">github.com/hztools/go-fm&lt;/a> and
&lt;a href="https://github.com/hztools/go-am">github.com/hztools/go-am&lt;/a>,
and are importable as
&lt;a href="https://pkg.go.dev/hz.tools/fm">hz.tools/fm&lt;/a> and
&lt;a href="https://pkg.go.dev/hz.tools/am">hz.tools/am&lt;/a>.&lt;/p>
&lt;p>As a bonus, the &lt;code>hz.tools/fm&lt;/code> package also contains a modulator, which has been
tested &amp;ldquo;on the air&amp;rdquo; and with some of my handheld radios. This code is a bit
old, since the &lt;code>hz.tools/fm&lt;/code> code is effectively the first IQ processing code
I&amp;rsquo;d ever written, but it still runs and I run it from time to time.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-go" data-lang="go">&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e">// Basic sketch for playing FM radio using a reader stream from&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e">// an SDR or other IQ stream.&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">bandwidth&lt;/span> &lt;span style="color:#f92672">:=&lt;/span> &lt;span style="color:#ae81ff">150&lt;/span>&lt;span style="color:#f92672">*&lt;/span>&lt;span style="color:#a6e22e">rf&lt;/span>.&lt;span style="color:#a6e22e">KHz&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">reader&lt;/span>, &lt;span style="color:#a6e22e">err&lt;/span> = &lt;span style="color:#a6e22e">stream&lt;/span>.&lt;span style="color:#a6e22e">ConvertReader&lt;/span>(&lt;span style="color:#a6e22e">reader&lt;/span>, &lt;span style="color:#a6e22e">sdr&lt;/span>.&lt;span style="color:#a6e22e">SampleFormatC64&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">if&lt;/span> &lt;span style="color:#a6e22e">err&lt;/span> &lt;span style="color:#f92672">!=&lt;/span> &lt;span style="color:#66d9ef">nil&lt;/span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">...&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">demod&lt;/span>, &lt;span style="color:#a6e22e">err&lt;/span> &lt;span style="color:#f92672">:=&lt;/span> &lt;span style="color:#a6e22e">fm&lt;/span>.&lt;span style="color:#a6e22e">Demodulate&lt;/span>(&lt;span style="color:#a6e22e">reader&lt;/span>, &lt;span style="color:#a6e22e">fm&lt;/span>.&lt;span style="color:#a6e22e">DemodulatorConfig&lt;/span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">Deviation&lt;/span>: &lt;span style="color:#a6e22e">bandwidth&lt;/span> &lt;span style="color:#f92672">/&lt;/span> &lt;span style="color:#ae81ff">2&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">Downsample&lt;/span>: &lt;span style="color:#ae81ff">8&lt;/span>, &lt;span style="color:#75715e">// some value here depending on sample rate&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">Planner&lt;/span>: &lt;span style="color:#a6e22e">fftw&lt;/span>.&lt;span style="color:#a6e22e">Plan&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> })
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">if&lt;/span> &lt;span style="color:#a6e22e">err&lt;/span> &lt;span style="color:#f92672">!=&lt;/span> &lt;span style="color:#66d9ef">nil&lt;/span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">...&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">speaker&lt;/span>, &lt;span style="color:#a6e22e">err&lt;/span> &lt;span style="color:#f92672">:=&lt;/span> &lt;span style="color:#a6e22e">pulseaudio&lt;/span>.&lt;span style="color:#a6e22e">NewWriter&lt;/span>(&lt;span style="color:#a6e22e">pulseaudio&lt;/span>.&lt;span style="color:#a6e22e">Config&lt;/span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">Format&lt;/span>: &lt;span style="color:#a6e22e">pulseaudio&lt;/span>.&lt;span style="color:#a6e22e">SampleFormatFloat32NE&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">Rate&lt;/span>: &lt;span style="color:#a6e22e">demod&lt;/span>.&lt;span style="color:#a6e22e">SampleRate&lt;/span>(),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">AppName&lt;/span>: &lt;span style="color:#e6db74">&amp;#34;rf&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">StreamName&lt;/span>: &lt;span style="color:#e6db74">&amp;#34;fm&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">Channels&lt;/span>: &lt;span style="color:#ae81ff">1&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">SinkName&lt;/span>: &lt;span style="color:#e6db74">&amp;#34;&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> })
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">if&lt;/span> &lt;span style="color:#a6e22e">err&lt;/span> &lt;span style="color:#f92672">!=&lt;/span> &lt;span style="color:#66d9ef">nil&lt;/span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">...&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">buf&lt;/span> &lt;span style="color:#f92672">:=&lt;/span> make([]&lt;span style="color:#66d9ef">float32&lt;/span>, &lt;span style="color:#ae81ff">1024&lt;/span>&lt;span style="color:#f92672">*&lt;/span>&lt;span style="color:#ae81ff">64&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">for&lt;/span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">i&lt;/span>, &lt;span style="color:#a6e22e">err&lt;/span> &lt;span style="color:#f92672">:=&lt;/span> &lt;span style="color:#a6e22e">demod&lt;/span>.&lt;span style="color:#a6e22e">Read&lt;/span>(&lt;span style="color:#a6e22e">buf&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">if&lt;/span> &lt;span style="color:#a6e22e">err&lt;/span> &lt;span style="color:#f92672">!=&lt;/span> &lt;span style="color:#66d9ef">nil&lt;/span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">...&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">if&lt;/span> &lt;span style="color:#a6e22e">i&lt;/span> &lt;span style="color:#f92672">==&lt;/span> &lt;span style="color:#ae81ff">0&lt;/span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> panic(&lt;span style="color:#e6db74">&amp;#34;...&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">if&lt;/span> &lt;span style="color:#a6e22e">err&lt;/span> &lt;span style="color:#f92672">:=&lt;/span> &lt;span style="color:#a6e22e">speaker&lt;/span>.&lt;span style="color:#a6e22e">Write&lt;/span>(&lt;span style="color:#a6e22e">buf&lt;/span>[:&lt;span style="color:#a6e22e">i&lt;/span>]); &lt;span style="color:#a6e22e">err&lt;/span> &lt;span style="color:#f92672">!=&lt;/span> &lt;span style="color:#66d9ef">nil&lt;/span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">...&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="hztoolsrfcap---byte-serialization-for-iq-data">hz.tools/rfcap - byte serialization for IQ data&lt;/h2>
&lt;p>The &lt;code>hz.tools/rfcap&lt;/code> package is the reference implementation of the
&lt;a href="https://hz.tools/rfcap/">rfcap&lt;/a> &amp;ldquo;spec&amp;rdquo;, and is how I store IQ captures
locally, and how I send them across a byte boundary.&lt;/p>
&lt;p>The git repo can be found at
&lt;a href="https://github.com/hztools/go-rfcap">github.com/hztools/go-rfcap&lt;/a>, and is
importable as &lt;a href="https://pkg.go.dev/hz.tools/rfcap">hz.tools/rfcap&lt;/a>.&lt;/p>
&lt;p>If you&amp;rsquo;re interested in storing IQ in a way others can use, the better approach
is to use &lt;a href="https://github.com/sigmf/SigMF">SigMF&lt;/a> &amp;ndash; &lt;code>rfcap&lt;/code> exists for cases
like using UNIX pipes to move IQ around, through APIs, or when I send
IQ data through an OS socket, to ensure the sample format (and other metadata)
is communicated with it.&lt;/p>
&lt;p>&lt;code>rfcap&lt;/code> has a number of limitations, for instance, it can not express a change
in frequency or sample rate during the capture, since the header is fixed at
the beginning of the file.&lt;/p></description></item><item><title>Decoding LDPC: k-Bit Brute Forcing</title><link>https://k3xec.com/ldpc-k-bit/</link><pubDate>Tue, 01 Nov 2022 19:00:00 -0400</pubDate><guid>https://k3xec.com/ldpc-k-bit/</guid><description>&lt;div class="hz-alert-error">
&lt;b>Before you go on:&lt;/b> I've been
&lt;a href="https://mastodon.social/@destevez/109272986530413236">warned off&lt;/a>
implementing this in practice on a few counts; namely, the space tradeoff isn't
worth it, and it's unlikely to correct meaningful errors. I'm going to leave
this post up, but please do take the content with a very large grain of salt!
&lt;/div>
&lt;p>My initial efforts to build a &lt;a href="https://en.wikipedia.org/wiki/Physical_layer">PHY&lt;/a>
and &lt;a href="https://en.wikipedia.org/wiki/Data_link_layer">Data Link&lt;/a> layer &amp;ndash; from
scratch using my own code &amp;ndash; have been progressing nicely since the initial
BPSK based protocol I&amp;rsquo;ve documented under the PACKRAT series. As part of that,
I&amp;rsquo;ve been diving deep into &lt;a href="https://en.wikipedia.org/wiki/Error_correction_code">FEC&lt;/a>,
and in particular, &lt;a href="https://en.wikipedia.org/wiki/Low-density_parity-check_code">LDPC&lt;/a>.&lt;/p>
&lt;p>I won&amp;rsquo;t be able to do an overview of LDPC justice in this post &amp;ndash; with any
luck that&amp;rsquo;ll come in a later post to come as part of the RATPACK series,
so some knowledge is assumed. As such this post is less useful for those
looking to learn about LDPC, and a bit more targeted to those who enjoy
talking and thinking about FEC.&lt;/p>
&lt;div class="hz-alert-warning">
&lt;b>Hey, heads up!&lt;/b> - This post contains extremely unvalidated and back of
the napkin quality work without any effort to prove this out generally.
Hopefully this work can be of help to others, but please double check anything
below if you need it for your own work!
&lt;/div>
&lt;p>While implementing LDPC, I&amp;rsquo;ve gotten an encoder and checker working, enough
to use LDPC like a checksum. The next big step is to write a Decoder, which
can do error correction. The two popular approaches for the actual correction
that I&amp;rsquo;ve seen while reading about LDPC are
&lt;a href="https://en.wikipedia.org/wiki/Belief_propagation">Belief Propagation&lt;/a>,
and some class of linear programming that I haven&amp;rsquo;t dug into yet.
I&amp;rsquo;m not thrilled at how expensive this all is in software, so while implementing
the stack I&amp;rsquo;ve been exploring every shady side ally to try and learn more about
how encoders and decoders work, both in theory - and in practice.&lt;/p>
&lt;h2 id="processing-an-ldpc-message">Processing an LDPC Message&lt;/h2>
&lt;p>Checking if a message is correct is fairly straightforward with LDPC (as with
encoding, I&amp;rsquo;ll note). As a quick refresher &amp;ndash; given the LDPC &lt;code>H&lt;/code> (check) matrix
of width &lt;code>N&lt;/code>, you can check your message vector (&lt;code>msg&lt;/code>) of length &lt;code>N&lt;/code> by
multiplying &lt;code>H&lt;/code> and &lt;code>msg&lt;/code>, and checking if the output vector is all zero.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-go" data-lang="go">&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e">// scheme contains our G (generator) and&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e">// H (check) matrices.&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">scheme&lt;/span> &lt;span style="color:#f92672">:=&lt;/span> {&lt;span style="color:#a6e22e">G&lt;/span>: &lt;span style="color:#a6e22e">Matrix&lt;/span>{&lt;span style="color:#f92672">...&lt;/span>}, &lt;span style="color:#a6e22e">H&lt;/span>: &lt;span style="color:#a6e22e">Matrix&lt;/span>{&lt;span style="color:#f92672">...&lt;/span>}}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e">// msg contains our LDPC message (data and&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e">// check bits).&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">msg&lt;/span> &lt;span style="color:#f92672">:=&lt;/span> &lt;span style="color:#a6e22e">Vector&lt;/span>{&lt;span style="color:#f92672">...&lt;/span>}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e">// N is also the length of the encoded&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e">// msg vector after check bits have been&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e">// added.&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">N&lt;/span> &lt;span style="color:#f92672">:=&lt;/span> &lt;span style="color:#a6e22e">scheme&lt;/span>.&lt;span style="color:#a6e22e">G&lt;/span>.&lt;span style="color:#a6e22e">Width&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e">// Now, let&amp;#39;s generate our &amp;#39;check&amp;#39; vector.&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">ch&lt;/span> &lt;span style="color:#f92672">:=&lt;/span> &lt;span style="color:#a6e22e">Multiply&lt;/span>(&lt;span style="color:#a6e22e">scheme&lt;/span>.&lt;span style="color:#a6e22e">H&lt;/span>, &lt;span style="color:#a6e22e">msg&lt;/span>)
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>We can now see if the message is correct or not:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-go" data-lang="go">&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e">// if the ch vector is all zeros, we know&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e">// that the message is valid, and we don&amp;#39;t&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e">// need to do anything.&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">if&lt;/span> &lt;span style="color:#a6e22e">ch&lt;/span>.&lt;span style="color:#a6e22e">IsZero&lt;/span>() {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e">// handle the case where the message&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e">// is fine as-is.&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> &lt;span style="color:#f92672">...&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e">// Expensive decode here&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>This is great for getting a thumbs up / thumbs down on the message being
correct, but correcting errors still requires pulling the LDPC matrix values
from the &lt;code>g&lt;/code> (generator) matrix out, building a bipartite graph, and
iteratively reprocessing the bit values, until constraints are satisfied and
the message has been corrected.&lt;/p>
&lt;p>This got me thinking - what &lt;em>is&lt;/em> the output vector when it&amp;rsquo;s not all zeros?
Since &lt;code>1&lt;/code> values in the output vector indicates consistency problems in the
message bits as they relate to the check bits, I wondered if this could be used
to speed up my LDPC decoder. It appears to work, so this post is half an attempt
to document this technique before I put it in my hot path, and half a plea for
those who &lt;em>do&lt;/em> like to talk about FEC to tell me what name this technique
actually is.&lt;/p>
&lt;h2 id="k-bit-brute-forcing">k-Bit Brute Forcing&lt;/h2>
&lt;p>Given that the output Vector&amp;rsquo;s non-zero bit pattern is set due to the position
of errors in the message vector, let&amp;rsquo;s use that fact to build up a table
of k-Bit errors that we can index into.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-go" data-lang="go">&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e">// for clarity&amp;#39;s sake, the Vector&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e">// type is being used as the lookup&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e">// key here, even though it may&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e">// need to be a hash or string in&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e">// some cases.&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">idx&lt;/span> &lt;span style="color:#f92672">:=&lt;/span> &lt;span style="color:#66d9ef">map&lt;/span>[&lt;span style="color:#a6e22e">Vector&lt;/span>]&lt;span style="color:#66d9ef">int&lt;/span>{}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">for&lt;/span> &lt;span style="color:#a6e22e">i&lt;/span> &lt;span style="color:#f92672">:=&lt;/span> &lt;span style="color:#ae81ff">0&lt;/span>; &lt;span style="color:#a6e22e">i&lt;/span> &amp;lt; &lt;span style="color:#a6e22e">N&lt;/span>; &lt;span style="color:#a6e22e">i&lt;/span>&lt;span style="color:#f92672">++&lt;/span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e">// Create a vector of length N&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">v&lt;/span> &lt;span style="color:#f92672">:=&lt;/span> &lt;span style="color:#a6e22e">Vector&lt;/span>{}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">v&lt;/span>.&lt;span style="color:#a6e22e">FlipBit&lt;/span>(&lt;span style="color:#a6e22e">i&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e">// Now, let&amp;#39;s use the generator matrix to encode&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e">// the data with checksums, and then use the&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e">// check matrix on the message to figure out what&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e">// bit pattern results&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">ev&lt;/span> &lt;span style="color:#f92672">:=&lt;/span> &lt;span style="color:#a6e22e">Multiply&lt;/span>(&lt;span style="color:#a6e22e">scheme&lt;/span>.&lt;span style="color:#a6e22e">H&lt;/span>, &lt;span style="color:#a6e22e">Multiply&lt;/span>(&lt;span style="color:#a6e22e">v&lt;/span>, &lt;span style="color:#a6e22e">scheme&lt;/span>.&lt;span style="color:#a6e22e">G&lt;/span>))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">idx&lt;/span>[&lt;span style="color:#a6e22e">ev&lt;/span>] = &lt;span style="color:#a6e22e">i&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>This can be extended to multiple bits (hence: k-Bits), but I&amp;rsquo;ve only done
one here for illustration. Now that we have our &lt;code>idx&lt;/code> mapping, we can now
go back to the hot path on Checking the incoming message data:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-go" data-lang="go">&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e">// if the ch vector is all zeros, we know&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e">// that the message is valid, and we don&amp;#39;t&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e">// need to do anything.&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">if&lt;/span> &lt;span style="color:#a6e22e">ch&lt;/span>.&lt;span style="color:#a6e22e">IsZero&lt;/span>() {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e">// handle the case where the message&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e">// is fine as-is.&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> &lt;span style="color:#f92672">...&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">errIdx&lt;/span>, &lt;span style="color:#a6e22e">ok&lt;/span> &lt;span style="color:#f92672">:=&lt;/span> &lt;span style="color:#a6e22e">idx&lt;/span>[&lt;span style="color:#a6e22e">ch&lt;/span>]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">if&lt;/span> &lt;span style="color:#a6e22e">ok&lt;/span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">msg&lt;/span>.&lt;span style="color:#a6e22e">FlipBit&lt;/span>(&lt;span style="color:#a6e22e">errIdx&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e">// Verify the LDPC message using&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e">// H again here.&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> &lt;span style="color:#f92672">...&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e">// Expensive decode here&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Since map lookups wind up a heck of a lot faster than message-passing
bit state, the hope here is this will short-circuit easy to solve errors
for k-Bits, for some value of k that the system memory can tolerate.&lt;/p>
&lt;h2 id="does-this-work">Does this work?&lt;/h2>
&lt;p>Frankly &amp;ndash; I have no idea. I&amp;rsquo;ve written a small program and brute forced
single-bit errors in all bit positions using random data to start with, and
I&amp;rsquo;ve not been able to find any collisions in the 1-bit error set, using the
LDPC matrix from &lt;code>802.3an-2006&lt;/code>. Even if I was to find a collision for a
higher-order k-Bit value, I&amp;rsquo;m tempted to continue with this approach, and treat
each set of bits in the Vector&amp;rsquo;s bin (like a hash-table), checking the LDPC
validity after each bit set in the bin. As long as the collision rate is small
enough, it should be possible to correct k-Bits of error faster than the more
expensive Belief Propagation approach. That being said, I&amp;rsquo;m not entirely
convinced collisions will be very common, but it&amp;rsquo;ll take a bit more time
working through the math to say that with any confidence.&lt;/p>
&lt;p>&lt;b>Have you seen this approach called something official in publications? See
an obvious flaw in the system? Send me a tip, please!&lt;/b>&lt;/p></description></item><item><title>k3xec.com/patty: Go bindings to patty</title><link>https://k3xec.com/patty/</link><pubDate>Mon, 11 Apr 2022 19:33:00 -0400</pubDate><guid>https://k3xec.com/patty/</guid><description>&lt;p>AX.25 is a tough protocol to use on UNIX systems. A lot of the support in
Linux, specifically, is pretty hard to use, and tends to be built into the
reptilian brain of the kernel. &lt;a href="https://kz3rox.us/">KZ3ROX&lt;/a> built a userland
AX.25 stack called &lt;a href="https://scm.xan.host/patty.git/">patty&lt;/a>, for which I have
now built some Go bindings on top of.&lt;/p>
&lt;p>Code needed to create AX.25 Sockets via Go can be found at
&lt;a href="https://github.com/k3xec/go-patty">github.com/k3xec/go-patty&lt;/a>,
and imported by Go source as
&lt;a href="https://pkg.go.dev/k3xec.com/patty">k3xec.com/patty&lt;/a>.&lt;/p>
&lt;h1 id="overview">Overview&lt;/h1>
&lt;p>Clint patty programs (including consumers of this Go library) work by
communicating with a userland daemon (&lt;code>pattyd&lt;/code>) via a UNIX named socket.
That daemon will communicate with a particular radio using a
&lt;a href="https://en.wikipedia.org/wiki/KISS_(TNC)">KISS TNC&lt;/a> serial device.&lt;/p>
&lt;p>The Go bindings implement as many standard Go library interfaces as is
practical, allowing for the &amp;ldquo;plug and play&amp;rdquo; use of &lt;code>patty&lt;/code> (and AX.25) in
places where you would expect a network socket (such as TCP) to work, such
as Go&amp;rsquo;s http library.&lt;/p>
&lt;h1 id="example">Example&lt;/h1>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-go" data-lang="go">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">package&lt;/span> &lt;span style="color:#a6e22e">main&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">import&lt;/span> (
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#e6db74">&amp;#34;fmt&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#e6db74">&amp;#34;log&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#e6db74">&amp;#34;net&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#e6db74">&amp;#34;os&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#e6db74">&amp;#34;time&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#e6db74">&amp;#34;k3xec.com/patty&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">func&lt;/span> &lt;span style="color:#a6e22e">main&lt;/span>() {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">callsign&lt;/span> &lt;span style="color:#f92672">:=&lt;/span> &lt;span style="color:#e6db74">&amp;#34;N0CALL-10&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">client&lt;/span>, &lt;span style="color:#a6e22e">err&lt;/span> &lt;span style="color:#f92672">:=&lt;/span> &lt;span style="color:#a6e22e">patty&lt;/span>.&lt;span style="color:#a6e22e">Open&lt;/span>(&lt;span style="color:#e6db74">&amp;#34;patty.sock&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">if&lt;/span> &lt;span style="color:#a6e22e">err&lt;/span> &lt;span style="color:#f92672">!=&lt;/span> &lt;span style="color:#66d9ef">nil&lt;/span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> panic(&lt;span style="color:#a6e22e">err&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">l&lt;/span>, &lt;span style="color:#a6e22e">err&lt;/span> &lt;span style="color:#f92672">:=&lt;/span> &lt;span style="color:#a6e22e">client&lt;/span>.&lt;span style="color:#a6e22e">Listen&lt;/span>(&lt;span style="color:#e6db74">&amp;#34;ax25&amp;#34;&lt;/span>, &lt;span style="color:#a6e22e">callsign&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">if&lt;/span> &lt;span style="color:#a6e22e">err&lt;/span> &lt;span style="color:#f92672">!=&lt;/span> &lt;span style="color:#66d9ef">nil&lt;/span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> panic(&lt;span style="color:#a6e22e">err&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">for&lt;/span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">log&lt;/span>.&lt;span style="color:#a6e22e">Printf&lt;/span>(&lt;span style="color:#e6db74">&amp;#34;Listening for requests to %s&amp;#34;&lt;/span>, &lt;span style="color:#a6e22e">l&lt;/span>.&lt;span style="color:#a6e22e">Addr&lt;/span>())
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">conn&lt;/span>, &lt;span style="color:#a6e22e">err&lt;/span> &lt;span style="color:#f92672">:=&lt;/span> &lt;span style="color:#a6e22e">l&lt;/span>.&lt;span style="color:#a6e22e">Accept&lt;/span>()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">if&lt;/span> &lt;span style="color:#a6e22e">err&lt;/span> &lt;span style="color:#f92672">!=&lt;/span> &lt;span style="color:#66d9ef">nil&lt;/span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">log&lt;/span>.&lt;span style="color:#a6e22e">Printf&lt;/span>(&lt;span style="color:#e6db74">&amp;#34;Error accepting: %s&amp;#34;&lt;/span>, &lt;span style="color:#a6e22e">err&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">continue&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">go&lt;/span> &lt;span style="color:#a6e22e">handle&lt;/span>(&lt;span style="color:#a6e22e">conn&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">func&lt;/span> &lt;span style="color:#a6e22e">handle&lt;/span>(&lt;span style="color:#a6e22e">c&lt;/span> &lt;span style="color:#a6e22e">net&lt;/span>.&lt;span style="color:#a6e22e">Conn&lt;/span>) &lt;span style="color:#66d9ef">error&lt;/span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">defer&lt;/span> &lt;span style="color:#a6e22e">c&lt;/span>.&lt;span style="color:#a6e22e">Close&lt;/span>()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">log&lt;/span>.&lt;span style="color:#a6e22e">Printf&lt;/span>(&lt;span style="color:#e6db74">&amp;#34;New connection from %s (local: %s)&amp;#34;&lt;/span>, &lt;span style="color:#a6e22e">c&lt;/span>.&lt;span style="color:#a6e22e">RemoteAddr&lt;/span>(), &lt;span style="color:#a6e22e">c&lt;/span>.&lt;span style="color:#a6e22e">LocalAddr&lt;/span>())
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">fmt&lt;/span>.&lt;span style="color:#a6e22e">Fprintf&lt;/span>(&lt;span style="color:#a6e22e">c&lt;/span>, &lt;span style="color:#e6db74">`
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#e6db74">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#e6db74">Hello! This is Paul&amp;#39;s experimental %s node. Feel free
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#e6db74">to poke around. Let me know if you spot anything funny.
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#e6db74">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#e6db74">Five pings are to follow!
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#e6db74">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#e6db74">`&lt;/span>, &lt;span style="color:#a6e22e">c&lt;/span>.&lt;span style="color:#a6e22e">LocalAddr&lt;/span>())
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">for&lt;/span> &lt;span style="color:#a6e22e">i&lt;/span> &lt;span style="color:#f92672">:=&lt;/span> &lt;span style="color:#ae81ff">0&lt;/span>; &lt;span style="color:#a6e22e">i&lt;/span> &amp;lt; &lt;span style="color:#ae81ff">5&lt;/span>; &lt;span style="color:#a6e22e">i&lt;/span>&lt;span style="color:#f92672">++&lt;/span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">time&lt;/span>.&lt;span style="color:#a6e22e">Sleep&lt;/span>(&lt;span style="color:#a6e22e">time&lt;/span>.&lt;span style="color:#a6e22e">Second&lt;/span> &lt;span style="color:#f92672">*&lt;/span> &lt;span style="color:#ae81ff">5&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">fmt&lt;/span>.&lt;span style="color:#a6e22e">Fprintf&lt;/span>(&lt;span style="color:#a6e22e">c&lt;/span>, &lt;span style="color:#e6db74">&amp;#34;Ping!\n&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> &lt;span style="color:#66d9ef">nil&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div></description></item><item><title>Proxying Ethernet Frames to PACKRAT (Part 5/5) 🐀</title><link>https://k3xec.com/packrat-proxy/</link><pubDate>Mon, 06 Dec 2021 11:00:00 -0500</pubDate><guid>https://k3xec.com/packrat-proxy/</guid><description>&lt;div class="hz-alert-ok">
🐀 This post is part of a series called "PACKRAT". If this is the first post
you've found, it'd be worth reading the
&lt;a href="https://k3xec.com/packrat-intro/">intro post&lt;/a> first and then looking over
&lt;a href="https://k3xec.com/tags/packrat/">all posts in the series&lt;/a>.
&lt;/div>
&lt;p>In the &lt;a href="https://k3xec.com/packrat-framing/">last post&lt;/a>, we left off at being able to send and receive PACKRAT frames
to and from devices. Since we can transport IPv4 packets over the network,
let&amp;rsquo;s go ahead and see if we can read/write
&lt;a href="https://en.wikipedia.org/wiki/Ethernet">Ethernet&lt;/a> frames from a Linux network
interface, and on the backend, read and write PACKRAT frames over the air.
This has the benefit of continuing to allow Linux userspace tools to work (like
cURL, as we&amp;rsquo;ll try!), which means we don&amp;rsquo;t have to do a lot of work to
implement higher level protocols or tactics to get a connection established
over the link.&lt;/p>
&lt;p>Given that this post is less RF and more Linuxy, I&amp;rsquo;m going to include more code
snippits than in prior posts, and those snippits are closer to runable Go, but
still not complete examples. There&amp;rsquo;s also a lot of different ways to do this,
I&amp;rsquo;ve just picked the easiest one for me to implement and debug given my
existing tooling &amp;ndash; for you, you may find another approach easier to implement!&lt;/p>
&lt;p>Again, deviation here is very welcome, and since this segment is the least
RF centric post in the series, the pace and tone is going to feel different.
If you feel lost here, that&amp;rsquo;s OK. This isn&amp;rsquo;t the most important part of
the series, and is mostly here to give a concrete ending to the story arc.
Any way you want to finish your own journey is the best way for you to finish
it!&lt;/p>
&lt;h4 id="implement-ethernet-conversion-code">Implement Ethernet conversion code&lt;/h4>
&lt;p>This assumes an importable package with a Frame struct, which we can use to
convert a Frame to/from Ethernet. Given that the PACKRAT frame has a field that
Ethernet doesn&amp;rsquo;t (namely, &lt;code>Callsign&lt;/code>), that will need to be explicitly passed
in when turning an Ethernet frame into a PACKRAT Frame.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-go" data-lang="go">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">...&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">// ToPackrat will create a packrat frame from an Ethernet frame.&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">func&lt;/span> &lt;span style="color:#a6e22e">ToPackrat&lt;/span>(&lt;span style="color:#a6e22e">callsign&lt;/span> [&lt;span style="color:#ae81ff">8&lt;/span>]&lt;span style="color:#66d9ef">byte&lt;/span>, &lt;span style="color:#a6e22e">frame&lt;/span> &lt;span style="color:#f92672">*&lt;/span>&lt;span style="color:#a6e22e">ethernet&lt;/span>.&lt;span style="color:#a6e22e">Frame&lt;/span>) (&lt;span style="color:#f92672">*&lt;/span>&lt;span style="color:#a6e22e">packrat&lt;/span>.&lt;span style="color:#a6e22e">Frame&lt;/span>, &lt;span style="color:#66d9ef">error&lt;/span>) {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">var&lt;/span> &lt;span style="color:#a6e22e">frameType&lt;/span> &lt;span style="color:#a6e22e">packrat&lt;/span>.&lt;span style="color:#a6e22e">FrameType&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">switch&lt;/span> &lt;span style="color:#a6e22e">frame&lt;/span>.&lt;span style="color:#a6e22e">EtherType&lt;/span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">case&lt;/span> &lt;span style="color:#a6e22e">ethernet&lt;/span>.&lt;span style="color:#a6e22e">EtherTypeIPv4&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">frameType&lt;/span> = &lt;span style="color:#a6e22e">packrat&lt;/span>.&lt;span style="color:#a6e22e">FrameTypeIPv4&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">default&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> &lt;span style="color:#66d9ef">nil&lt;/span>, &lt;span style="color:#a6e22e">fmt&lt;/span>.&lt;span style="color:#a6e22e">Errorf&lt;/span>(&lt;span style="color:#e6db74">&amp;#34;ethernet: unsupported ethernet type %x&amp;#34;&lt;/span>, &lt;span style="color:#a6e22e">frame&lt;/span>.&lt;span style="color:#a6e22e">EtherType&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> &lt;span style="color:#f92672">&amp;amp;&lt;/span>&lt;span style="color:#a6e22e">packrat&lt;/span>.&lt;span style="color:#a6e22e">Frame&lt;/span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">Destination&lt;/span>: &lt;span style="color:#a6e22e">frame&lt;/span>.&lt;span style="color:#a6e22e">Destination&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">Source&lt;/span>: &lt;span style="color:#a6e22e">frame&lt;/span>.&lt;span style="color:#a6e22e">Source&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">Type&lt;/span>: &lt;span style="color:#a6e22e">frameType&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">Callsign&lt;/span>: &lt;span style="color:#a6e22e">callsign&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">Payload&lt;/span>: &lt;span style="color:#a6e22e">frame&lt;/span>.&lt;span style="color:#a6e22e">Payload&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }, &lt;span style="color:#66d9ef">nil&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">// FromPackrat will create an Ethernet frame from a Packrat frame.&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">func&lt;/span> &lt;span style="color:#a6e22e">FromPackrat&lt;/span>(&lt;span style="color:#a6e22e">frame&lt;/span> &lt;span style="color:#f92672">*&lt;/span>&lt;span style="color:#a6e22e">packrat&lt;/span>.&lt;span style="color:#a6e22e">Frame&lt;/span>) (&lt;span style="color:#f92672">*&lt;/span>&lt;span style="color:#a6e22e">ethernet&lt;/span>.&lt;span style="color:#a6e22e">Frame&lt;/span>, &lt;span style="color:#66d9ef">error&lt;/span>) {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">var&lt;/span> &lt;span style="color:#a6e22e">etherType&lt;/span> &lt;span style="color:#a6e22e">ethernet&lt;/span>.&lt;span style="color:#a6e22e">EtherType&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">switch&lt;/span> &lt;span style="color:#a6e22e">frame&lt;/span>.&lt;span style="color:#a6e22e">Type&lt;/span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">case&lt;/span> &lt;span style="color:#a6e22e">packrat&lt;/span>.&lt;span style="color:#a6e22e">FrameTypeRaw&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> &lt;span style="color:#66d9ef">nil&lt;/span>, &lt;span style="color:#a6e22e">fmt&lt;/span>.&lt;span style="color:#a6e22e">Errorf&lt;/span>(&lt;span style="color:#e6db74">&amp;#34;ethernet: unsupported packrat type &amp;#39;raw&amp;#39;&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">case&lt;/span> &lt;span style="color:#a6e22e">packrat&lt;/span>.&lt;span style="color:#a6e22e">FrameTypeIPv4&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">etherType&lt;/span> = &lt;span style="color:#a6e22e">ethernet&lt;/span>.&lt;span style="color:#a6e22e">EtherTypeIPv4&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">default&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> &lt;span style="color:#66d9ef">nil&lt;/span>, &lt;span style="color:#a6e22e">fmt&lt;/span>.&lt;span style="color:#a6e22e">Errorf&lt;/span>(&lt;span style="color:#e6db74">&amp;#34;ethernet: unknown packrat type %x&amp;#34;&lt;/span>, &lt;span style="color:#a6e22e">frame&lt;/span>.&lt;span style="color:#a6e22e">Type&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e">// We lose the Callsign here, which is sad.&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> &lt;span style="color:#f92672">&amp;amp;&lt;/span>&lt;span style="color:#a6e22e">ethernet&lt;/span>.&lt;span style="color:#a6e22e">Frame&lt;/span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">Destination&lt;/span>: &lt;span style="color:#a6e22e">frame&lt;/span>.&lt;span style="color:#a6e22e">Destination&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">Source&lt;/span>: &lt;span style="color:#a6e22e">frame&lt;/span>.&lt;span style="color:#a6e22e">Source&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">EtherType&lt;/span>: &lt;span style="color:#a6e22e">etherType&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">Payload&lt;/span>: &lt;span style="color:#a6e22e">frame&lt;/span>.&lt;span style="color:#a6e22e">Payload&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }, &lt;span style="color:#66d9ef">nil&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Our helpers, &lt;code>ToPackrat&lt;/code> and &lt;code>FromPackrat&lt;/code> can now be used to transmorgify
PACKRAT into Ethernet, or Ethernet into PACKRAT. Let&amp;rsquo;s put them into use!&lt;/p>
&lt;h4 id="implement-a-tap-interface">Implement a TAP interface&lt;/h4>
&lt;p>On Linux, the networking stack can be exposed to userland using &lt;a href="https://en.wikipedia.org/wiki/TUN/TAP">TUN or
TAP&lt;/a> interfaces. TUN devices allow a
userspace program to read and write data at the Layer 3 / IP layer. TAP devices
allow a userspace program to read and write data at the Layer 2 Data Link /
Ethernet layer. Writing data at Layer 2 is what we want to do, since we&amp;rsquo;re
looking to transform our Layer 2 into Ethernet&amp;rsquo;s Layer 2 Frames. Our first job
here is to create the actual TAP interface, set the MAC address, and set the IP
range to our pre-coordinated IP range.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-go" data-lang="go">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">...&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">import&lt;/span> (
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#e6db74">&amp;#34;net&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#e6db74">&amp;#34;github.com/mdlayher/ethernet&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#e6db74">&amp;#34;github.com/songgao/water&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#e6db74">&amp;#34;github.com/vishvananda/netlink&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">...&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">config&lt;/span> &lt;span style="color:#f92672">:=&lt;/span> &lt;span style="color:#a6e22e">water&lt;/span>.&lt;span style="color:#a6e22e">Config&lt;/span>{&lt;span style="color:#a6e22e">DeviceType&lt;/span>: &lt;span style="color:#a6e22e">water&lt;/span>.&lt;span style="color:#a6e22e">TAP&lt;/span>}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">config&lt;/span>.&lt;span style="color:#a6e22e">Name&lt;/span> = &lt;span style="color:#e6db74">&amp;#34;rat0&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">iface&lt;/span>, &lt;span style="color:#a6e22e">err&lt;/span> &lt;span style="color:#f92672">:=&lt;/span> &lt;span style="color:#a6e22e">water&lt;/span>.&lt;span style="color:#a6e22e">New&lt;/span>(&lt;span style="color:#a6e22e">config&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">...&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">netIface&lt;/span>, &lt;span style="color:#a6e22e">err&lt;/span> &lt;span style="color:#f92672">:=&lt;/span> &lt;span style="color:#a6e22e">netlink&lt;/span>.&lt;span style="color:#a6e22e">LinkByName&lt;/span>(&lt;span style="color:#e6db74">&amp;#34;rat0&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">...&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e">// Pick a range here that works for you!&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e">//&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e">// For my local network, I&amp;#39;m using some IPs&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e">// that AMPR (ampr.org) was nice enough to&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e">// allocate to me for ham radio use. Thanks,&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e">// AMPR!&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e">//&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e">// Let&amp;#39;s just use 10.* here, though.&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e">//&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">ip&lt;/span>, &lt;span style="color:#a6e22e">cidr&lt;/span>, &lt;span style="color:#a6e22e">err&lt;/span> &lt;span style="color:#f92672">:=&lt;/span> &lt;span style="color:#a6e22e">net&lt;/span>.&lt;span style="color:#a6e22e">ParseCIDR&lt;/span>(&lt;span style="color:#e6db74">&amp;#34;10.0.0.1/24&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">...&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">cidr&lt;/span>.&lt;span style="color:#a6e22e">IP&lt;/span> = &lt;span style="color:#a6e22e">ip&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">err&lt;/span> = &lt;span style="color:#a6e22e">netlink&lt;/span>.&lt;span style="color:#a6e22e">AddrAdd&lt;/span>(&lt;span style="color:#a6e22e">netIface&lt;/span>, &lt;span style="color:#f92672">&amp;amp;&lt;/span>&lt;span style="color:#a6e22e">netlink&lt;/span>.&lt;span style="color:#a6e22e">Addr&lt;/span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">IPNet&lt;/span>: &lt;span style="color:#a6e22e">cidr&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">Peer&lt;/span>: &lt;span style="color:#a6e22e">cidr&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> })
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">...&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e">// Add all our neighbors to the ARP table&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">for&lt;/span> &lt;span style="color:#a6e22e">_&lt;/span>, &lt;span style="color:#a6e22e">neighbor&lt;/span> &lt;span style="color:#f92672">:=&lt;/span> &lt;span style="color:#66d9ef">range&lt;/span> &lt;span style="color:#a6e22e">neighbors&lt;/span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">netlink&lt;/span>.&lt;span style="color:#a6e22e">NeighAdd&lt;/span>(&lt;span style="color:#f92672">&amp;amp;&lt;/span>&lt;span style="color:#a6e22e">netlink&lt;/span>.&lt;span style="color:#a6e22e">Neigh&lt;/span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">LinkIndex&lt;/span>: &lt;span style="color:#a6e22e">netIface&lt;/span>.&lt;span style="color:#a6e22e">Attrs&lt;/span>().&lt;span style="color:#a6e22e">Index&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">Type&lt;/span>: &lt;span style="color:#a6e22e">netlink&lt;/span>.&lt;span style="color:#a6e22e">FAMILY_V4&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">State&lt;/span>: &lt;span style="color:#a6e22e">netlink&lt;/span>.&lt;span style="color:#a6e22e">NUD_PERMANENT&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">IP&lt;/span>: &lt;span style="color:#a6e22e">neighbor&lt;/span>.&lt;span style="color:#a6e22e">IP&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">HardwareAddr&lt;/span>: &lt;span style="color:#a6e22e">neighbor&lt;/span>.&lt;span style="color:#a6e22e">MAC&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> })
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e">// Pick a MAC that is globally unique here, this is&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e">// just used as an example!&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">addr&lt;/span>, &lt;span style="color:#a6e22e">err&lt;/span> &lt;span style="color:#f92672">:=&lt;/span> &lt;span style="color:#a6e22e">net&lt;/span>.&lt;span style="color:#a6e22e">ParseMAC&lt;/span>(&lt;span style="color:#e6db74">&amp;#34;FA:DE:DC:AB:LE:01&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">...&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">netlink&lt;/span>.&lt;span style="color:#a6e22e">LinkSetHardwareAddr&lt;/span>(&lt;span style="color:#a6e22e">netIface&lt;/span>, &lt;span style="color:#a6e22e">addr&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">...&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">err&lt;/span> = &lt;span style="color:#a6e22e">netlink&lt;/span>.&lt;span style="color:#a6e22e">LinkSetUp&lt;/span>(&lt;span style="color:#a6e22e">netIface&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">var&lt;/span> &lt;span style="color:#a6e22e">frame&lt;/span> = &lt;span style="color:#f92672">&amp;amp;&lt;/span>&lt;span style="color:#a6e22e">ethernet&lt;/span>.&lt;span style="color:#a6e22e">Frame&lt;/span>{}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">var&lt;/span> &lt;span style="color:#a6e22e">buf&lt;/span> = make([]&lt;span style="color:#66d9ef">byte&lt;/span>, &lt;span style="color:#ae81ff">1500&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">for&lt;/span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">n&lt;/span>, &lt;span style="color:#a6e22e">err&lt;/span> &lt;span style="color:#f92672">:=&lt;/span> &lt;span style="color:#a6e22e">iface&lt;/span>.&lt;span style="color:#a6e22e">Read&lt;/span>(&lt;span style="color:#a6e22e">buf&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">...&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">err&lt;/span> = &lt;span style="color:#a6e22e">frame&lt;/span>.&lt;span style="color:#a6e22e">UnmarshalBinary&lt;/span>(&lt;span style="color:#a6e22e">buf&lt;/span>[:&lt;span style="color:#a6e22e">n&lt;/span>])
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">...&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e">// process frame here (to come)&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">...&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Now that our network stack can resolve an IP to a MAC Address (via &lt;code>ip neigh&lt;/code>
according to our pre-defined neighbors), and send that IP packet to our daemon,
it&amp;rsquo;s now on us to send IPv4 data over the airwaves. Here, we&amp;rsquo;re going to take
packets coming in from our TAP interface, and marshal the Ethernet frame into a
PACKRAT Frame and transmit it. As with the rest of the RF code, we&amp;rsquo;ll leave
that up to the implementer, of course, using what was built during &lt;a href="https://k3xec.com/packrat-transmitting/">Part 2:
Transmitting BPSK symbols&lt;/a> and &lt;a href="https://k3xec.com/packrat-framing/">Part 4: Framing
data&lt;/a>.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-go" data-lang="go">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">...&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">for&lt;/span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e">// continued from above&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">n&lt;/span>, &lt;span style="color:#a6e22e">err&lt;/span> &lt;span style="color:#f92672">:=&lt;/span> &lt;span style="color:#a6e22e">iface&lt;/span>.&lt;span style="color:#a6e22e">Read&lt;/span>(&lt;span style="color:#a6e22e">buf&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">...&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">err&lt;/span> = &lt;span style="color:#a6e22e">frame&lt;/span>.&lt;span style="color:#a6e22e">UnmarshalBinary&lt;/span>(&lt;span style="color:#a6e22e">buf&lt;/span>[:&lt;span style="color:#a6e22e">n&lt;/span>])
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">...&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">switch&lt;/span> &lt;span style="color:#a6e22e">frame&lt;/span>.&lt;span style="color:#a6e22e">EtherType&lt;/span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">case&lt;/span> &lt;span style="color:#ae81ff">0x0800&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e">// ipv4 packet&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">pack&lt;/span>, &lt;span style="color:#a6e22e">err&lt;/span> &lt;span style="color:#f92672">:=&lt;/span> &lt;span style="color:#a6e22e">ToPackrat&lt;/span>(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e">// Add my callsign to all Frames, for now&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [&lt;span style="color:#ae81ff">8&lt;/span>]&lt;span style="color:#66d9ef">byte&lt;/span>{&lt;span style="color:#e6db74">&amp;#39;K&amp;#39;&lt;/span>, &lt;span style="color:#e6db74">&amp;#39;3&amp;#39;&lt;/span>, &lt;span style="color:#e6db74">&amp;#39;X&amp;#39;&lt;/span>, &lt;span style="color:#e6db74">&amp;#39;E&amp;#39;&lt;/span>, &lt;span style="color:#e6db74">&amp;#39;C&amp;#39;&lt;/span>},
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">frame&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> )
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">...&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">err&lt;/span> = &lt;span style="color:#a6e22e">transmitPacket&lt;/span>(&lt;span style="color:#a6e22e">pack&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">...&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">...&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Now that we have transmitting covered, let&amp;rsquo;s go ahead and handle the receive
path here. We&amp;rsquo;re going to listen on frequency using the code built in
&lt;a href="https://k3xec.com/packrat-receiving/">Part 3: Receiving BPSK symbols&lt;/a> and
&lt;a href="https://k3xec.com/packrat-framing/">Part 4: Framing data&lt;/a>. The Frames we decode from
the airwaves are expected to come back from the call
&lt;code>packratReader.Next&lt;/code> in the code below, and the exact way that works
is up to the implementer.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-go" data-lang="go">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">...&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">for&lt;/span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e">// pull the next packrat frame from&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e">// the symbol stream as we did in the&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e">// last post&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">packet&lt;/span>, &lt;span style="color:#a6e22e">err&lt;/span> &lt;span style="color:#f92672">:=&lt;/span> &lt;span style="color:#a6e22e">packratReader&lt;/span>.&lt;span style="color:#a6e22e">Next&lt;/span>()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">...&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e">// check for CRC errors and drop invalid&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e">// packets&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">err&lt;/span> = &lt;span style="color:#a6e22e">packet&lt;/span>.&lt;span style="color:#a6e22e">Check&lt;/span>()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">...&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">if&lt;/span> &lt;span style="color:#a6e22e">bytes&lt;/span>.&lt;span style="color:#a6e22e">Equal&lt;/span>(&lt;span style="color:#a6e22e">packet&lt;/span>.&lt;span style="color:#a6e22e">Source&lt;/span>, &lt;span style="color:#a6e22e">addr&lt;/span>) {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e">// if we&amp;#39;ve heard ourself transmitting&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e">// let&amp;#39;s avoid looping back&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">continue&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e">// create an ethernet frame&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">frame&lt;/span>, &lt;span style="color:#a6e22e">err&lt;/span> &lt;span style="color:#f92672">:=&lt;/span> &lt;span style="color:#a6e22e">FromPackrat&lt;/span>(&lt;span style="color:#a6e22e">packet&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">...&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">buf&lt;/span>, &lt;span style="color:#a6e22e">err&lt;/span> &lt;span style="color:#f92672">:=&lt;/span> &lt;span style="color:#a6e22e">frame&lt;/span>.&lt;span style="color:#a6e22e">MarshalBinary&lt;/span>()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">...&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e">// and inject it into the tap&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">err&lt;/span> = &lt;span style="color:#a6e22e">iface&lt;/span>.&lt;span style="color:#a6e22e">Write&lt;/span>(&lt;span style="color:#a6e22e">buf&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">...&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">...&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Phew. Right. Now we should be able to listen for PACKRAT frames on the
air and inject them into our TAP interface.&lt;/p>
&lt;h4 id="putting-it-all-together">Putting it all Together&lt;/h4>
&lt;p>After all this work &amp;ndash; weeks of work! &amp;ndash; we can finally get around to putting
some real packets over the air. For me, this was an incredibly satisfying
milestone, and tied together months of learning!&lt;/p>
&lt;p>I was able to start up a UDP server on a remote machine with an RTL-SDR dongle
attached to it, listening on the TAP interface&amp;rsquo;s host IP with my defined MAC
address, and send UDP packets to that server via PACKRAT using my laptop,
&lt;code>/dev/udp&lt;/code> and an Ettus B210, sending packets into the TAP interface.&lt;/p>
&lt;p>&lt;img src="https://k3xec.com/imgs/packrat/packrat-hello-world.png" alt="">&lt;/p>
&lt;p>Now that UDP was working, I was able to get TCP to work using two PlutoSDRs,
which allowed me to run the cURL command I pasted in the first post (both
simultaneously listen and transmit on behalf of my TAP interface).&lt;/p>
&lt;p>It&amp;rsquo;s my hope that someone out there will be inspired to implement their own
Layer 1 and Layer 2 as a learning exercise, and gets the same sense of
gratification that I did! If you&amp;rsquo;re reading this, and at a point
where you&amp;rsquo;ve been able to send IP traffic over your own Layer 1 / Layer 2,
please get in touch! I&amp;rsquo;d be thrilled to hear all about it. I&amp;rsquo;d love to link
to any posts or examples you publish here!&lt;/p></description></item><item><title>Framing data (Part 4/5) 🐀</title><link>https://k3xec.com/packrat-framing/</link><pubDate>Sun, 05 Dec 2021 11:00:00 -0500</pubDate><guid>https://k3xec.com/packrat-framing/</guid><description>&lt;div class="hz-alert-ok">
🐀 This post is part of a series called "PACKRAT". If this is the first post
you've found, it'd be worth reading the
&lt;a href="https://k3xec.com/packrat-intro/">intro post&lt;/a> first and then looking over
&lt;a href="https://k3xec.com/tags/packrat/">all posts in the series&lt;/a>.
&lt;/div>
&lt;p>In the &lt;a href="https://k3xec.com/packrat-receiving/">last post&lt;/a>, we we were able to build a functioning
Layer 1 PHY where we can encode symbols to transmit, and receive symbols on the
other end, we&amp;rsquo;re now at the point where we can encode and decode those symbols
as bits and frame blocks of data, marking them with a Sender and a Destination
for routing to the right host(s). This is a &amp;ldquo;Layer 2&amp;rdquo; scheme in the OSI model,
which is otherwise known as the &lt;a href="https://en.wikipedia.org/wiki/Data_link_layer">Data Link
Layer&lt;/a>. You&amp;rsquo;re using one to
view this website right now &amp;ndash; I&amp;rsquo;m willing to bet your data is going
through an Ethernet layer 2 as well as WiFi or maybe a cellular data
protocol like 5G or LTE.&lt;/p>
&lt;p>Given that this entire exercise is hard enough without designing a complex
Layer 2 scheme, I opted for simplicity in the hopes this would free me from the
complexity and research that has gone into this field for the last 50 years. I
settled on stealing a few ideas from Ethernet Frames &amp;ndash; namely, the use of &lt;a href="https://en.wikipedia.org/wiki/MAC_address">MAC
addresses&lt;/a> to identify parties, and
the &lt;code>EtherType&lt;/code> field to indicate the Payload type. I also stole the idea of
using a CRC at the end of the Frame to check for corruption, as well as the
specific CRC method (&lt;code>crc32&lt;/code> using &lt;code>0xedb88320&lt;/code> as the polynomial).&lt;/p>
&lt;p>Lastly, I added a &lt;code>callsign&lt;/code> field to make life easier on ham radio frequencies
if I was ever to seriously attempt to use a variant of this protocol over the
air with multiple users. However, given this scheme is not a commonly used
scheme, it&amp;rsquo;s best practice to use a nearby radio to identify your transmissions
on the same frequency while testing &amp;ndash; or use a Faraday box to test without
transmitting over the airwaves. I added the callsign field in an effort to lean
into the spirit of the Part 97 regulations, even if I relied on a phone
emission to identify the Frames.&lt;/p>
&lt;p>As an aside, I asked the ARRL for input here, and their stance to me over email
was I&amp;rsquo;d be OK according to the regs if I were to stick to UHF and put my
callsign into the BPSK stream using a widely understood encoding (even with no
knowledge of PACKRAT, the callsign is ASCII over BPSK and should be easily
demodulatable for followup with me). Even with all this, I opted to use FM
phone to transmit my callsign when I was active on the air (specifically, using
an SDR and a small bash script to automate transmission while I watched for
interference or other band users).&lt;/p>
&lt;p>Right, back to the Frame:&lt;/p>
&lt;div class="hz-abi">
&lt;div type="[3]byte" class="hz-abi-green hz-abi-2b">sync&lt;/div>
&lt;div type="[6]byte" class="hz-abi-green hz-abi-2b">dest&lt;/div>
&lt;div type="[6]byte" class="hz-abi-green hz-abi-2b">source&lt;/div>
&lt;div type="[8]byte" class="hz-abi-green hz-abi-2b">callsign&lt;/div>
&lt;div type="[2]byte" class="hz-abi-green hz-abi-2b">type&lt;/div>
&lt;div type="[]byte" class="hz-abi-green hz-abi-Nb">payload&lt;/div>
&lt;div type="uint32" class="hz-abi-green hz-abi-1b">crc&lt;/div>
&lt;/div>
&lt;p>With all that done, I put that layout into a struct, so that we can marshal
and unmarshal bytes to and from our Frame objects, and work with it
in software.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-go" data-lang="go">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">type&lt;/span> &lt;span style="color:#a6e22e">FrameType&lt;/span> [&lt;span style="color:#ae81ff">2&lt;/span>]&lt;span style="color:#66d9ef">byte&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">type&lt;/span> &lt;span style="color:#a6e22e">Frame&lt;/span> &lt;span style="color:#66d9ef">struct&lt;/span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">Destination&lt;/span> &lt;span style="color:#a6e22e">net&lt;/span>.&lt;span style="color:#a6e22e">HardwareAddr&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">Source&lt;/span> &lt;span style="color:#a6e22e">net&lt;/span>.&lt;span style="color:#a6e22e">HardwareAddr&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">Callsign&lt;/span> [&lt;span style="color:#ae81ff">8&lt;/span>]&lt;span style="color:#66d9ef">byte&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">Type&lt;/span> &lt;span style="color:#a6e22e">FrameType&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">Payload&lt;/span> []&lt;span style="color:#66d9ef">byte&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">CRC&lt;/span> &lt;span style="color:#66d9ef">uint32&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h4 id="time-to-pick-some-consts">Time to pick some consts&lt;/h4>
&lt;p>I picked a unique and distinctive &lt;code>sync&lt;/code> sequence, which the sender
will transmit before the Frame, while the receiver listens for that
sequence to know when it&amp;rsquo;s in byte alignment with the symbol stream.
My &lt;code>sync&lt;/code> sequence is &lt;code>[3]byte{'U', 'f', '~'}&lt;/code> which works out to be a
very pleasant bit sequence of &lt;code>01010101 01100110 01111110&lt;/code>. It&amp;rsquo;s important
to have soothing preambles for your Frames. We need all the good energy
we can get at this point.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-go" data-lang="go">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">var&lt;/span> (
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">FrameStart&lt;/span> = [&lt;span style="color:#ae81ff">3&lt;/span>]&lt;span style="color:#66d9ef">byte&lt;/span>{&lt;span style="color:#e6db74">&amp;#39;U&amp;#39;&lt;/span>, &lt;span style="color:#e6db74">&amp;#39;f&amp;#39;&lt;/span>, &lt;span style="color:#e6db74">&amp;#39;~&amp;#39;&lt;/span>}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">FrameMaxPayloadSize&lt;/span> = &lt;span style="color:#ae81ff">1500&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>)
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Next, I defined some &lt;code>FrameType&lt;/code> values for the &lt;code>type&lt;/code> field,
which I can use to determine what is done with that data next,
something Ethernet was originally missing, but has since grown
to depend on (who needs Length anyway? Not me. See below!)&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;td class="hz-1-6th">FrameType&lt;/td>
&lt;td class="hz-2-6th">Description&lt;/td>
&lt;td class="hz-1-3th">Bytes&lt;/td>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Raw&lt;/td>
&lt;td>Bytes in the Payload field are opaque and not to be parsed.&lt;/td>
&lt;td>[2]byte{0x00, 0x01}&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>IPv4&lt;/td>
&lt;td>Bytes in the Payload field are an IPv4 packet.&lt;/td>
&lt;td>[2]byte{0x00, 0x02}&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>And finally, I decided on a maximum length of the Payload, and decided on
limiting it to 1500 bytes to align with the MTU of Ethernet.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-go" data-lang="go">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">var&lt;/span> (
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">FrameTypeRaw&lt;/span> = &lt;span style="color:#a6e22e">FrameType&lt;/span>{&lt;span style="color:#ae81ff">0&lt;/span>, &lt;span style="color:#ae81ff">1&lt;/span>}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">FrameTypeIPv4&lt;/span> = &lt;span style="color:#a6e22e">FrameType&lt;/span>{&lt;span style="color:#ae81ff">0&lt;/span>, &lt;span style="color:#ae81ff">2&lt;/span>}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>)
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Given we know how we&amp;rsquo;re going to marshal and unmarshal binary data to and from
Frames, we can now move on to looking through the bit stream for our Frames.&lt;/p>
&lt;h4 id="why-is-there-no-length-field">Why is there no Length field?&lt;/h4>
&lt;p>I was initially a bit surprised that Ethernet Frames didn&amp;rsquo;t have a Length field
in use, but the more I thought about it, the more it seemed like a big ole'
failure mode without a good implementation outcome. Either the Length is right
(resulting in no action and used bits on every packet) or the Length is not the
length of the Payload and the driver needs to determine what to do with the
packet &amp;ndash; does it try and trim the overlong payload and ignore the rest? What
if both the end of the read bytes and the end of the subset of the packet
denoted by Length have a valid CRC? Which is used? Will everyone agree? What
if Length is longer than the Payload but the CRC is good where we detected a
lost carrier?&lt;/p>
&lt;p>I decided on simplicity. The end of a Frame is denoted by the loss of the BPSK
carrier &amp;ndash; when the signal is no longer being transmitted (or more correctly,
when the signal is no longer received), we know we&amp;rsquo;ve hit the end of a packet.
Missing a single symbol will result in the Frame being finalized. This can
cause some degree of corruption, but it&amp;rsquo;s also a lot easier than doing tricks
like bit stuffing to create an end of symbol stream delimiter.&lt;/p>
&lt;h4 id="finding-the-frame-start-in-a-symbol-stream">Finding the Frame start in a Symbol Stream&lt;/h4>
&lt;p>First thing we need to do is find our &lt;code>sync&lt;/code> bit pattern in the symbols we&amp;rsquo;re
receiving from our BPSK demodulator. There&amp;rsquo;s some smart ways to do this, but
given that I&amp;rsquo;m not much of a smart man, I again decided to go for simple
instead. Given our incoming vector of symbols (which are still &lt;code>float&lt;/code> values)
prepend one at a time to a vector of floats that is the same length as the
&lt;code>sync&lt;/code> phrase, and compare against the &lt;code>sync&lt;/code> phrase, to determine if we&amp;rsquo;re in
sync with the byte boundary within the symbol stream.&lt;/p>
&lt;p>The only trick here is that because we&amp;rsquo;re using BPSK to modulate and demodulate
the data, post phaselock we can be 180 degrees out of alignment (such that a +1
is demodulated as -1, or vice versa). To deal with that, I check against both
the &lt;code>sync&lt;/code> phrase as well as the inverse of the &lt;code>sync&lt;/code> phrase (both &lt;code>[1, -1, 1]&lt;/code> as well as &lt;code>[-1, 1, -1]&lt;/code>) where if the inverse sync is matched, all symbols
to follow will be inverted as well. This effectively turns our symbols back
into bits, even if we&amp;rsquo;re flipped out of phase. Other techniques like
&lt;a href="https://en.wikipedia.org/wiki/Non-return-to-zero">NRZI&lt;/a> will represent a 0 or
1 by a change in phase state &amp;ndash; which is great, but can often cascade into long
runs of bit errors, and is generally more complex to implement. That
representation isn&amp;rsquo;t ambiguous, given you look for a phase change, not the
absolute phase value, which is incredibly compelling.&lt;/p>
&lt;p>Here&amp;rsquo;s a notional example of how I&amp;rsquo;ve been thinking about the phrase sliding
window &amp;ndash; and how I&amp;rsquo;ve been thinking of the checks. Each row is a new symbol
taken from the BPSK receiver, and pushed to the head of the sliding window,
moving all symbols back in the vector by one.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-go" data-lang="go">&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">var&lt;/span> (
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">sync&lt;/span> = []&lt;span style="color:#66d9ef">float&lt;/span>{ &lt;span style="color:#f92672">...&lt;/span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">buf&lt;/span> = make([]&lt;span style="color:#66d9ef">float&lt;/span>, len(&lt;span style="color:#a6e22e">sync&lt;/span>))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">incomingSymbols&lt;/span> = []&lt;span style="color:#66d9ef">float&lt;/span>{ &lt;span style="color:#f92672">...&lt;/span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> )
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">for&lt;/span> &lt;span style="color:#a6e22e">_&lt;/span>, &lt;span style="color:#a6e22e">el&lt;/span> &lt;span style="color:#f92672">:=&lt;/span> &lt;span style="color:#66d9ef">range&lt;/span> &lt;span style="color:#a6e22e">incomingSymbols&lt;/span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> copy(&lt;span style="color:#a6e22e">buf&lt;/span>, &lt;span style="color:#a6e22e">buf&lt;/span>[&lt;span style="color:#ae81ff">1&lt;/span>:])
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">buf&lt;/span>[len(&lt;span style="color:#a6e22e">buf&lt;/span>)&lt;span style="color:#f92672">-&lt;/span>&lt;span style="color:#ae81ff">1&lt;/span>] = &lt;span style="color:#a6e22e">el&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">if&lt;/span> &lt;span style="color:#a6e22e">compare&lt;/span>(&lt;span style="color:#a6e22e">sync&lt;/span>, &lt;span style="color:#a6e22e">buf&lt;/span>) {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e">// we&amp;#39;re synced!&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">break&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Given the pseudocode above, let&amp;rsquo;s step through what the checks would be doing
at each step:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;td class="hz-1-3ed">Buffer&lt;/td>
&lt;td class="hz-1-3ed">Sync&lt;/td>
&lt;td class="hz-1-3ed">Inverse Sync&lt;/td>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>[…]float{0,…,0}&lt;/td>
&lt;td>❌ […]float{-1,…,-1}&lt;/td>
&lt;td>❌ […]float{1,…,1}&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>[…]float{0,…,1}&lt;/td>
&lt;td>❌ […]float{-1,…,-1}&lt;/td>
&lt;td>❌ […]float{1,…,1}&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>[more bits in]&lt;/td>
&lt;td>❌ […]float{-1,…,-1}&lt;/td>
&lt;td>❌ […]float{1,…,1}&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>[…]float{1,…,1}&lt;/td>
&lt;td>❌ […]float{-1,…,-1}&lt;/td>
&lt;td>✅ […]float{1,…,1}&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>After this notional set of comparisons, we know that at the last step, we are
now aligned to the frame and byte boundary &amp;ndash; the next symbol / bit will be the
MSB of the 0th Frame byte. Additionally, we know we&amp;rsquo;re also 180 degrees out of
phase, so we need to flip the symbol&amp;rsquo;s sign to get the bit. From this point on
we can consume 8 bits at a time, and re-assemble the byte stream. I don&amp;rsquo;t know
what this technique is called &amp;ndash; or even if this is used in real grown-up
implementations, but it&amp;rsquo;s been working for my toy implementation.&lt;/p>
&lt;h4 id="next-steps">Next Steps&lt;/h4>
&lt;p>Now that we can read/write Frames to and from PACKRAT, the next steps here are
going to be implementing code to encode and decode Ethernet traffic into
PACKRAT, coming next in &lt;a href="https://k3xec.com/packrat-proxy/">Part 5!&lt;/a>&lt;/p></description></item><item><title>Receiving BPSK symbols (Part 3/5) 🐀</title><link>https://k3xec.com/packrat-receiving/</link><pubDate>Sat, 04 Dec 2021 11:00:00 -0500</pubDate><guid>https://k3xec.com/packrat-receiving/</guid><description>&lt;div class="hz-alert-ok">
🐀 This post is part of a series called "PACKRAT". If this is the first post
you've found, it'd be worth reading the
&lt;a href="https://k3xec.com/packrat-intro/">intro post&lt;/a> first and then looking over
&lt;a href="https://k3xec.com/tags/packrat/">all posts in the series&lt;/a>.
&lt;/div>
&lt;p>In the &lt;a href="https://k3xec.com/packrat-transmitting/">last post&lt;/a>, we worked through how to generate
a BPSK signal, and hopefully transmit it using one of our SDRs.
Let&amp;rsquo;s take that and move on to Receiving BPSK and turning that back
into symbols!&lt;/p>
&lt;p>Demodulating BPSK data is a bit more tricky than transmitting BPSK data, mostly
due to tedious facts of life such as space, time, and hardware built with
compromises because not doing that makes the problem impossible. Unfortunately,
it&amp;rsquo;s now our job to work within our imperfect world to recover perfect data. We
need to handle the addition of noise, differences in frequency, clock
synchronization and interference in order to recover our information. This
makes life a lot harder than when we transmit information, and as a result, a
lot more complex.&lt;/p>
&lt;h4 id="coarse-sync">Coarse Sync&lt;/h4>
&lt;p>Our starting point for this section will be working from a capture of a number
of generated PACKRAT packets
&lt;a href="https://k3xec.s3.amazonaws.com/receive/1-rx-debug.rfcap.xz">as heard by a PlutoSDR at (xz compressed interleaved int16, 2,621,440 samples per second)&lt;/a>&lt;/p>
&lt;p>Every SDR has its own oscillator, which eventually controls a number of
different components of an SDR, such as the IF (if it&amp;rsquo;s a superheterodyne
architecture) and the sampling rate. Drift in oscillators lead to drifts in
frequency &amp;ndash; such that what one SDR may think is 100MHz may be 100.01MHz for
another radio. Even if the radios were perfectly in sync, other artifacts such
as &lt;a href="https://en.wikipedia.org/wiki/Relativistic_Doppler_effect">doppler time dilation due to motion&lt;/a>
can cause the frequency to appear higher or lower in frequency than it was
transmitted.&lt;/p>
&lt;p>All this is a long way of saying, we need to determine when we see a strong
signal that&amp;rsquo;s close-ish to our tuned frequency, and take steps to roughly
correct it to our center frequency (in the order of 100s of Hz to kHz) in order
to acquire a phase lock on the signal to attempt to decode information contained
within.&lt;/p>
&lt;p>The easiest way of detecting the loudest signal of interest is to use an
&lt;a href="https://en.wikipedia.org/wiki/Fast_Fourier_transform">FFT&lt;/a>. Getting into how
FFTs work is out of scope of this post, so if this is the first time you&amp;rsquo;re
seeing mention of an FFT, it may be a good place to take a quick break to learn
a bit about the time domain (which is what the IQ data we&amp;rsquo;ve been working with
so far is), frequency domain, and how the FFT and iFFT operations can convert
between them.&lt;/p>
&lt;p>Lastly, because FFTs average power over the window, swapping phases such that
the transmitted wave has the same number of in-phase and inverted-phase symbols
the power would wind up averaging to zero. This is not helpful, so I took a tip
from &lt;a href="https://pysdr.org/content/sync.html#coarse-frequency-synchronization">Dr. Marc Lichtman&amp;rsquo;s PySDR
project&lt;/a>
and used &lt;a href="https://ventrella.com/ComplexSquaring/">complex squaring&lt;/a> to drive
our BPSK signal into a single detectable carrier by squaring the IQ data.
Because points are on the unit circle and at &lt;code>tau/2&lt;/code> (specifically, &lt;code>tau/(2^1)&lt;/code>
for BPSK, &lt;code>2^2&lt;/code> for QPSK) angles, and given that squaring has the effect of
doubling the angle, and angles are all mod &lt;code>tau&lt;/code>, this will drive our wave
comprised of two opposite phases back into a continuous wave &amp;ndash; effectively
removing our BPSK modulation, making it much easier to detect in the
frequency domain. Thanks to &lt;a href="https://github.com/tomberek/">Tom Bereknyei&lt;/a>
for helping me with that!&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-go" data-lang="go">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">...&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">var&lt;/span> &lt;span style="color:#a6e22e">iq&lt;/span> []&lt;span style="color:#a6e22e">complex&lt;/span>{}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">var&lt;/span> &lt;span style="color:#a6e22e">freq&lt;/span> []&lt;span style="color:#a6e22e">complex&lt;/span>{}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">for&lt;/span> &lt;span style="color:#a6e22e">i&lt;/span> &lt;span style="color:#f92672">:=&lt;/span> &lt;span style="color:#66d9ef">range&lt;/span> &lt;span style="color:#a6e22e">iq&lt;/span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">iq&lt;/span>[&lt;span style="color:#a6e22e">i&lt;/span>] = &lt;span style="color:#a6e22e">iq&lt;/span>[&lt;span style="color:#a6e22e">i&lt;/span>] &lt;span style="color:#f92672">*&lt;/span> &lt;span style="color:#a6e22e">iq&lt;/span>[&lt;span style="color:#a6e22e">i&lt;/span>]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e">// perform an fft, computing the frequency&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e">// domain vector in `freq` given the iq data&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e">// contained in `iq`.&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">fft&lt;/span>(&lt;span style="color:#a6e22e">iq&lt;/span>, &lt;span style="color:#a6e22e">freq&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e">// get the array index of the max value in the&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e">// freq array given the magnitude value of the&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e">// complex numbers.&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">var&lt;/span> &lt;span style="color:#a6e22e">binIdx&lt;/span> = max(&lt;span style="color:#a6e22e">abs&lt;/span>(&lt;span style="color:#a6e22e">freq&lt;/span>))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">...&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Now, most FFT operations will lay the frequency domain data out a bit
differently than you may expect (as a human), which is that the 0th element of
the FFT is 0Hz, not the most negative number (like in a waterfall). Generally
speaking, &amp;ldquo;zero first&amp;rdquo; is the most common frequency domain layout (and
generally speaking the most safe assumption if there&amp;rsquo;s no other documentation
on fft layout). &amp;ldquo;Negative first&amp;rdquo; is usually used when the FFT is being rendered
for human consumption – such as a waterfall plot.&lt;/p>
&lt;p>Given that we now know which FFT bin (which is to say, which index into the
FFT array) contains the strongest signal, we&amp;rsquo;ll go ahead and figure out what
frequency that bin relates to.&lt;/p>
&lt;p>In the time domain, each complex number is the next time instant. In the
frequency domain, each bin is a discrete frequency – or more specifically – a
frequency range. The bandwidth of the bin is a function of the sampling rate
and number of time domain samples used to do the FFT operation. As you increase
the amount of time used to perform the FFT, the more precise the FFT
measurement of frequency can be, but it will cover the same bandwidth, as
defined by the sampling rate.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-go" data-lang="go">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">...&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">var&lt;/span> &lt;span style="color:#a6e22e">sampleRate&lt;/span> = &lt;span style="color:#ae81ff">2&lt;/span>,&lt;span style="color:#ae81ff">621&lt;/span>,&lt;span style="color:#ae81ff">440&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e">// bandwidth is the range of frequencies&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e">// contained inside a single FFT bin,&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e">// measured in Hz.&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">var&lt;/span> &lt;span style="color:#a6e22e">bandwidth&lt;/span> = &lt;span style="color:#a6e22e">sampleRate&lt;/span>&lt;span style="color:#f92672">/&lt;/span>len(&lt;span style="color:#a6e22e">freq&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">...&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Now that we know we have a zero-first layout and the bin bandwidth,
we can compute what our frequency offset is in Hz.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-go" data-lang="go">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">...&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e">// binIdx is the index into the freq slice&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e">// containing the frequency domain data.&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">var&lt;/span> &lt;span style="color:#a6e22e">binIdx&lt;/span> = &lt;span style="color:#ae81ff">0&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e">// binFreq is the frequency of the bin&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e">// denoted by binIdx&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">var&lt;/span> &lt;span style="color:#a6e22e">binFreq&lt;/span> = &lt;span style="color:#ae81ff">0&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">if&lt;/span> &lt;span style="color:#a6e22e">binIdx&lt;/span> &amp;gt; len(&lt;span style="color:#a6e22e">freq&lt;/span>)&lt;span style="color:#f92672">/&lt;/span>&lt;span style="color:#ae81ff">2&lt;/span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e">// This branch covers the case where the bin&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e">// is past the middle point - which is to say,&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e">// if this is a negative frequency.&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">binFreq&lt;/span> = &lt;span style="color:#a6e22e">bandwidth&lt;/span> &lt;span style="color:#f92672">*&lt;/span> (&lt;span style="color:#a6e22e">binIdx&lt;/span> &lt;span style="color:#f92672">-&lt;/span> len(&lt;span style="color:#a6e22e">freq&lt;/span>))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> } &lt;span style="color:#66d9ef">else&lt;/span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e">// This branch covers the case where the bin&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e">// is in the first half of the frequency array,&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e">// which is to say - if this frequency is&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e">// a positive frequency.&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">binFreq&lt;/span> = &lt;span style="color:#a6e22e">bandwidth&lt;/span> &lt;span style="color:#f92672">*&lt;/span> &lt;span style="color:#a6e22e">binIdx&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">...&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>However, sice we squared the IQ data, we&amp;rsquo;re off in frequency by twice the
actual frequency &amp;ndash; if we are reading 12kHz, the bin is actually 6kHz. We
need to adjust for that before continuing with processing.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-go" data-lang="go">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">...&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">var&lt;/span> &lt;span style="color:#a6e22e">binFreq&lt;/span> = &lt;span style="color:#ae81ff">0&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">...&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e">// [compute the binFreq as above]&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">...&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e">// Adjust for the squaring of our IQ data&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">binFreq&lt;/span> = &lt;span style="color:#a6e22e">binFreq&lt;/span> &lt;span style="color:#f92672">/&lt;/span> &lt;span style="color:#ae81ff">2&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">...&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Finally, we need to shift the frequency by the inverse of the &lt;code>binFreq&lt;/code>
by generating a carrier wave at a specific frequency and rotating every
sample by our carrier wave &amp;ndash; so that a wave at the same frequency will
slow down (or stand still!) as it approaches 0Hz relative to the carrier
wave.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-go" data-lang="go">&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">var&lt;/span> &lt;span style="color:#a6e22e">tau&lt;/span> = &lt;span style="color:#a6e22e">pi&lt;/span> &lt;span style="color:#f92672">*&lt;/span> &lt;span style="color:#ae81ff">2&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e">// ts tracks where in time we are (basically: phase)&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">var&lt;/span> &lt;span style="color:#a6e22e">ts&lt;/span> &lt;span style="color:#66d9ef">float&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e">// inc is the amount we step forward in time (seconds)&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e">// each sample.&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">var&lt;/span> &lt;span style="color:#a6e22e">inc&lt;/span> &lt;span style="color:#66d9ef">float&lt;/span> = (&lt;span style="color:#ae81ff">1&lt;/span> &lt;span style="color:#f92672">/&lt;/span> &lt;span style="color:#a6e22e">sampleRate&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e">// amount to shift frequencies, in Hz,&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e">// in this case, shift +12 kHz to 0Hz&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">var&lt;/span> &lt;span style="color:#a6e22e">shift&lt;/span> = &lt;span style="color:#f92672">-&lt;/span>&lt;span style="color:#ae81ff">12&lt;/span>,&lt;span style="color:#ae81ff">000&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">for&lt;/span> &lt;span style="color:#a6e22e">i&lt;/span> &lt;span style="color:#f92672">:=&lt;/span> &lt;span style="color:#66d9ef">range&lt;/span> &lt;span style="color:#a6e22e">iq&lt;/span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">ts&lt;/span> &lt;span style="color:#f92672">+=&lt;/span> &lt;span style="color:#a6e22e">inc&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">if&lt;/span> &lt;span style="color:#a6e22e">ts&lt;/span> &amp;gt; &lt;span style="color:#a6e22e">tau&lt;/span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e">// not actually needed, but keeps ts within&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e">// 0 to 2*pi (since it is modulus 2*pi anyway)&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">ts&lt;/span> &lt;span style="color:#f92672">-=&lt;/span> &lt;span style="color:#a6e22e">tau&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e">// Here, we&amp;#39;re going to create a carrier wave&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e">// at the provided frequency (in this case,&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e">// -12kHz)&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">cwIq&lt;/span> = complex(&lt;span style="color:#a6e22e">cos&lt;/span>(&lt;span style="color:#a6e22e">tau&lt;/span>&lt;span style="color:#f92672">*&lt;/span>&lt;span style="color:#a6e22e">shift&lt;/span>&lt;span style="color:#f92672">*&lt;/span>&lt;span style="color:#a6e22e">ts&lt;/span>), &lt;span style="color:#a6e22e">sin&lt;/span>(&lt;span style="color:#a6e22e">tau&lt;/span>&lt;span style="color:#f92672">*&lt;/span>&lt;span style="color:#a6e22e">shift&lt;/span>&lt;span style="color:#f92672">*&lt;/span>&lt;span style="color:#a6e22e">ts&lt;/span>))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">iq&lt;/span>[&lt;span style="color:#a6e22e">i&lt;/span>] = &lt;span style="color:#a6e22e">iq&lt;/span>[&lt;span style="color:#a6e22e">i&lt;/span>] &lt;span style="color:#f92672">*&lt;/span> &lt;span style="color:#a6e22e">cwIq&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Now we&amp;rsquo;ve got the strong signal we&amp;rsquo;ve observed (which may or may not be
our BPSK modulated signal!) close enough to 0Hz that we ought to be able
to Phase Lock the signal in order to begin demodulating the signal.&lt;/p>
&lt;h4 id="filter">Filter&lt;/h4>
&lt;p>After we&amp;rsquo;re roughly in the neighborhood of a few kHz, we can now take some
steps to cut out any high frequency components (both positive high frequencies
and negative high frequencies). The normal way to do this would be to do an
FFT, apply the filter in the frequency domain, and then do an iFFT to turn it
back into time series data. This will work in loads of cases, but I&amp;rsquo;ve
found it to be incredibly tricky to get right when doing PSK. As such, I&amp;rsquo;ve
opted to do this the old fashioned way in the time domain.&lt;/p>
&lt;p>I&amp;rsquo;ve &amp;ndash; again &amp;ndash; opted to go simple rather than correct, and haven&amp;rsquo;t used
nearly any of the advanced level trickery I&amp;rsquo;ve come across for fear of using
it wrong. As a result, our process here is going to be generating a
&lt;a href="https://en.wikipedia.org/wiki/Sinc_function">sinc&lt;/a> filter by computing a
number of taps, and applying that in the time domain directly on the IQ
stream.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-go" data-lang="go">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">// Generate sinc taps&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">func&lt;/span> &lt;span style="color:#a6e22e">sinc&lt;/span>(&lt;span style="color:#a6e22e">x&lt;/span> &lt;span style="color:#66d9ef">float&lt;/span>) &lt;span style="color:#66d9ef">float&lt;/span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">if&lt;/span> &lt;span style="color:#a6e22e">x&lt;/span> &lt;span style="color:#f92672">==&lt;/span> &lt;span style="color:#ae81ff">0&lt;/span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">var&lt;/span> &lt;span style="color:#a6e22e">v&lt;/span> = &lt;span style="color:#a6e22e">pi&lt;/span> &lt;span style="color:#f92672">*&lt;/span> &lt;span style="color:#a6e22e">x&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> &lt;span style="color:#a6e22e">sin&lt;/span>(&lt;span style="color:#a6e22e">v&lt;/span>) &lt;span style="color:#f92672">/&lt;/span> &lt;span style="color:#a6e22e">v&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">...&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">var&lt;/span> &lt;span style="color:#a6e22e">dst&lt;/span> []&lt;span style="color:#66d9ef">float&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">var&lt;/span> &lt;span style="color:#a6e22e">length&lt;/span> = float(len(&lt;span style="color:#a6e22e">dst&lt;/span>))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">if&lt;/span> int(&lt;span style="color:#a6e22e">length&lt;/span>)&lt;span style="color:#f92672">%&lt;/span>&lt;span style="color:#ae81ff">2&lt;/span> &lt;span style="color:#f92672">==&lt;/span> &lt;span style="color:#ae81ff">0&lt;/span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">length&lt;/span>&lt;span style="color:#f92672">++&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">for&lt;/span> &lt;span style="color:#a6e22e">j&lt;/span> &lt;span style="color:#f92672">:=&lt;/span> &lt;span style="color:#66d9ef">range&lt;/span> &lt;span style="color:#a6e22e">dst&lt;/span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">i&lt;/span> &lt;span style="color:#f92672">:=&lt;/span> float(&lt;span style="color:#a6e22e">j&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">dst&lt;/span>[&lt;span style="color:#a6e22e">j&lt;/span>] = &lt;span style="color:#a6e22e">sinc&lt;/span>(&lt;span style="color:#ae81ff">2&lt;/span> &lt;span style="color:#f92672">*&lt;/span> &lt;span style="color:#a6e22e">cutoff&lt;/span> &lt;span style="color:#f92672">*&lt;/span> (&lt;span style="color:#a6e22e">i&lt;/span> &lt;span style="color:#f92672">-&lt;/span> (&lt;span style="color:#a6e22e">length&lt;/span>&lt;span style="color:#f92672">-&lt;/span>&lt;span style="color:#ae81ff">1&lt;/span>)&lt;span style="color:#f92672">/&lt;/span>&lt;span style="color:#ae81ff">2&lt;/span>))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">...&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>then we apply it in the time domain&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-go" data-lang="go">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">...&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e">// Apply sinc taps to an IQ stream&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">var&lt;/span> &lt;span style="color:#a6e22e">iq&lt;/span> []&lt;span style="color:#a6e22e">complex&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e">// taps as created in `dst` above&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">var&lt;/span> &lt;span style="color:#a6e22e">taps&lt;/span> []&lt;span style="color:#66d9ef">float&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">var&lt;/span> &lt;span style="color:#a6e22e">delay&lt;/span> = make([]&lt;span style="color:#a6e22e">complex&lt;/span>, len(&lt;span style="color:#a6e22e">taps&lt;/span>))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">for&lt;/span> &lt;span style="color:#a6e22e">i&lt;/span> &lt;span style="color:#f92672">:=&lt;/span> &lt;span style="color:#66d9ef">range&lt;/span> &lt;span style="color:#a6e22e">iq&lt;/span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e">// let&amp;#39;s shift the next sample into&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e">// the delay buffer&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> copy(&lt;span style="color:#a6e22e">delay&lt;/span>[&lt;span style="color:#ae81ff">1&lt;/span>:], &lt;span style="color:#a6e22e">delay&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">delay&lt;/span>[&lt;span style="color:#ae81ff">0&lt;/span>] = &lt;span style="color:#a6e22e">iq&lt;/span>[&lt;span style="color:#a6e22e">i&lt;/span>]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">var&lt;/span> &lt;span style="color:#a6e22e">phasor&lt;/span> &lt;span style="color:#a6e22e">complex&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">for&lt;/span> &lt;span style="color:#a6e22e">j&lt;/span> &lt;span style="color:#f92672">:=&lt;/span> &lt;span style="color:#66d9ef">range&lt;/span> &lt;span style="color:#a6e22e">delay&lt;/span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e">// for each sample in the buffer, let&amp;#39;s&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e">// weight them by the tap values, and&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e">// create a new complex number based on&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e">// filtering the real and imag values.&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">phasor&lt;/span> &lt;span style="color:#f92672">+=&lt;/span> complex(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">taps&lt;/span>[&lt;span style="color:#a6e22e">j&lt;/span>] &lt;span style="color:#f92672">*&lt;/span> real(&lt;span style="color:#a6e22e">delay&lt;/span>[&lt;span style="color:#a6e22e">j&lt;/span>]),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">taps&lt;/span>[&lt;span style="color:#a6e22e">j&lt;/span>] &lt;span style="color:#f92672">*&lt;/span> imag(&lt;span style="color:#a6e22e">delay&lt;/span>[&lt;span style="color:#a6e22e">j&lt;/span>]),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> )
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e">// now that we&amp;#39;ve run this sample&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e">// through the filter, we can go ahead&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e">// and scale it back (since we multiply&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e">// above) and drop it back into the iq&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e">// buffer.&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">iq&lt;/span>[&lt;span style="color:#a6e22e">i&lt;/span>] = complex(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> real(&lt;span style="color:#a6e22e">phasor&lt;/span>) &lt;span style="color:#f92672">/&lt;/span> len(&lt;span style="color:#a6e22e">taps&lt;/span>),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> imag(&lt;span style="color:#a6e22e">phasor&lt;/span>) &lt;span style="color:#f92672">/&lt;/span> len(&lt;span style="color:#a6e22e">taps&lt;/span>),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> )
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">...&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>After running IQ samples through the taps and back out, we&amp;rsquo;ll have a signal
that&amp;rsquo;s been filtered to the shape of our designed Sinc filter &amp;ndash; which will
cut out captured high frequency components (both positive and negative).&lt;/p>
&lt;p>Astute observers will note that we&amp;rsquo;re using the real (float) valued taps
on both the real and imaginary values independently. I&amp;rsquo;m sure there&amp;rsquo;s a way
to apply taps using complex numbers, but it was a bit confusing to work
through without being positive of the outcome. I may revisit this in the
future!&lt;/p>
&lt;h4 id="downsample">Downsample&lt;/h4>
&lt;p>Now, post-filter, we&amp;rsquo;ve got a lot of extra RF bandwidth being represented in
our IQ stream at our high sample rate All the high frequency values are now
filtered out, which means we can reduce our sampling rate without losing much
information at all. We can either do nothing about it and process at the fairly
high sample rate we&amp;rsquo;re capturing at, or we can drop the sample rate down and
help reduce the volume of numbers coming our way.&lt;/p>
&lt;p>There&amp;rsquo;s two big ways of doing this; either you can take every Nth sample (e.g.,
take every other sample to half the sample rate, or take every 10th to decimate
the sample stream to a 10th of what it originally was) which is the easiest to
implement (and easy on the CPU too), or to average a number of samples to
create a new sample.&lt;/p>
&lt;p>A nice bonus to averaging samples is that you can trade-off some CPU time for a
&lt;a href="https://en.wikipedia.org/wiki/Effective_number_of_bits">higher effective number of bits
(ENOB)&lt;/a> in your IQ
stream, which helps reduce noise, among other things. Some hardware does
exactly this (called &amp;ldquo;Oversampling&amp;rdquo;), and like many things, it has
&lt;a href="https://www.ti.com/lit/an/slaa594a/slaa594a.pdf">some pros and some cons&lt;/a>.
I&amp;rsquo;ve opted to treat our IQ stream like an oversampled IQ stream
and average samples to get a marginal bump in ENOB.&lt;/p>
&lt;p>Taking a group of 4 samples and averaging them results in a bit of added
precision. That means that a stream of IQ data at 8 ENOB can be bumped to 9
ENOB of precision after the process of oversampling and averaging. That
resulting stream will be at 1/4 of the sample rate, and this process can be
repeated 4 samples can again be taken for a bit of added precision; which is
going to be 1/4 of the sample rate (again), or 1/16 of the original sample
rate. If we again take a group of 4 samples, we&amp;rsquo;ll wind up with another bit and
a sample rate that&amp;rsquo;s 1/64 of the original sample rate.&lt;/p>
&lt;h4 id="phase-lock">Phase Lock&lt;/h4>
&lt;p>Our starting point for this section is the same capture as above, but
&lt;a href="https://k3xec.s3.amazonaws.com/receiving/4-downsample-debug.rfcap.xz">post-coarse sync, filtering downsampling (xz compressed interleaved float32, 163,840 samples per second)&lt;/a>&lt;/p>
&lt;p>The &lt;a href="https://en.wikipedia.org/wiki/Phase-locked_loop">PLL&lt;/a> in PACKRAT was one
of the parts I spent the most time stuck on. There&amp;rsquo;s no shortage of discussions
of how hardware PLLs work, or even a few software PLLs, but very little by way
of how to apply them and/or troubleshoot them. After getting frustrated trying
to follow the well worn path, I decided to cut my own way through the bush
using what I had learned about the concept, and hope that it works well enough
to continue on.&lt;/p>
&lt;p>PLLs, in concept are fairly simple &amp;ndash; you generate a carrier wave at a
frequency, compare the real-world SDR IQ sample to where your carrier wave is
in phase, and use the difference between the local wave and the observed wave
to adjust the frequency and phase of your carrier wave. Eventually, if all goes
well, that delta is driven as small as possible, and your carrier wave can be
used as a reference clock to determine if the observed signal changes in
frequency or phase.&lt;/p>
&lt;p>In reality, tuning PLLs is a total pain, and basically no one outlines how to
apply them to BPSK signals in a descriptive way. I&amp;rsquo;ve had to steal an approach
I&amp;rsquo;ve seen in hardware to implement my software PLL, with any hope it&amp;rsquo;s close
enough that this isn&amp;rsquo;t a hazard to learners. The concept is to generate the
carrier wave (as above) and store some rolling averages to tune the carrier
wave over time. I use two constants, &amp;ldquo;alpha&amp;rdquo; and &amp;ldquo;beta&amp;rdquo; (which appear to be
traditional PLL variable names for this function) which control how quickly the
frequency and phase is changed according to observed mismatches. Alpha is set
fairly high, which means discrepancies between our carrier and observed data
are quickly applied to the phase, and a lower constant for Beta, which will
take long-term errors and attempt to use that to match frequency.&lt;/p>
&lt;p>This is all well and good. Getting to this point isn&amp;rsquo;t all that obscure, but
the trouble comes when processing a BPSK signal. Phase changes kick the PLL
out of alignment and it tends to require some time to get back into phase lock,
when we really shouldn&amp;rsquo;t even be losing it in the first place. My attempt is
to generate two predicted samples, one for each phase of our BPSK signal. The
delta is compared, and the lower error of the two is used to adjust the PLL,
but the carrier wave itself is used to rotate the sample.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-go" data-lang="go">&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">var&lt;/span> &lt;span style="color:#a6e22e">alpha&lt;/span> = &lt;span style="color:#ae81ff">0.1&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">var&lt;/span> &lt;span style="color:#a6e22e">beta&lt;/span> = (&lt;span style="color:#a6e22e">alpha&lt;/span> &lt;span style="color:#f92672">*&lt;/span> &lt;span style="color:#a6e22e">alpha&lt;/span>) &lt;span style="color:#f92672">/&lt;/span> &lt;span style="color:#ae81ff">2&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">var&lt;/span> &lt;span style="color:#a6e22e">phase&lt;/span> = &lt;span style="color:#ae81ff">0.0&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">var&lt;/span> &lt;span style="color:#a6e22e">frequency&lt;/span> = &lt;span style="color:#ae81ff">0.0&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">...&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">for&lt;/span> &lt;span style="color:#a6e22e">i&lt;/span> &lt;span style="color:#f92672">:=&lt;/span> &lt;span style="color:#66d9ef">range&lt;/span> &lt;span style="color:#a6e22e">iq&lt;/span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">predicted&lt;/span> = complex(&lt;span style="color:#a6e22e">cos&lt;/span>(&lt;span style="color:#a6e22e">phase&lt;/span>), &lt;span style="color:#a6e22e">sin&lt;/span>(&lt;span style="color:#a6e22e">phase&lt;/span>))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">sample&lt;/span> = &lt;span style="color:#a6e22e">iq&lt;/span>[&lt;span style="color:#a6e22e">i&lt;/span>] &lt;span style="color:#f92672">*&lt;/span> &lt;span style="color:#a6e22e">conj&lt;/span>(&lt;span style="color:#a6e22e">predicted&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">delta&lt;/span> = &lt;span style="color:#a6e22e">phase&lt;/span>(&lt;span style="color:#a6e22e">sample&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">predicted2&lt;/span> = complex(&lt;span style="color:#a6e22e">cos&lt;/span>(&lt;span style="color:#a6e22e">phase&lt;/span>&lt;span style="color:#f92672">+&lt;/span>&lt;span style="color:#a6e22e">pi&lt;/span>), &lt;span style="color:#a6e22e">sin&lt;/span>(&lt;span style="color:#a6e22e">phase&lt;/span>&lt;span style="color:#f92672">+&lt;/span>&lt;span style="color:#a6e22e">pi&lt;/span>))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">sample2&lt;/span> = &lt;span style="color:#a6e22e">iq&lt;/span>[&lt;span style="color:#a6e22e">i&lt;/span>] &lt;span style="color:#f92672">*&lt;/span> &lt;span style="color:#a6e22e">conj&lt;/span>(&lt;span style="color:#a6e22e">predicted2&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">delta2&lt;/span> = &lt;span style="color:#a6e22e">phase&lt;/span>(&lt;span style="color:#a6e22e">sample2&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">if&lt;/span> &lt;span style="color:#a6e22e">abs&lt;/span>(&lt;span style="color:#a6e22e">delta2&lt;/span>) &amp;lt; &lt;span style="color:#a6e22e">abs&lt;/span>(&lt;span style="color:#a6e22e">delta&lt;/span>) {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e">// note that we do not update &amp;#39;sample&amp;#39;.&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">delta&lt;/span> = &lt;span style="color:#a6e22e">delta2&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">phase&lt;/span> &lt;span style="color:#f92672">+=&lt;/span> &lt;span style="color:#a6e22e">alpha&lt;/span> &lt;span style="color:#f92672">*&lt;/span> &lt;span style="color:#a6e22e">delta&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">frequency&lt;/span> &lt;span style="color:#f92672">+=&lt;/span> &lt;span style="color:#a6e22e">beta&lt;/span> &lt;span style="color:#f92672">*&lt;/span> &lt;span style="color:#a6e22e">delta&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e">// adjust the iq sample to the PLL rotated&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e">// sample.&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">iq&lt;/span>[&lt;span style="color:#a6e22e">i&lt;/span>] = &lt;span style="color:#a6e22e">sample&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">...&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>If all goes well, this loop has the effect of driving a BPSK signal&amp;rsquo;s imaginary
values to 0, and the real value between +1 and -1.&lt;/p>
&lt;p>&lt;img src="https://k3xec.com/imgs/packrat/packrat-phase-locked.png" alt="">&lt;/p>
&lt;h4 id="average-idle--carrier-detect">Average Idle / Carrier Detect&lt;/h4>
&lt;p>Our starting point for this section is the same capture as above, but
&lt;a href="https://k3xec.s3.amazonaws.com/receiving/5-phase-debug.rfcap.xz">post-PLL (xz compressed interleaved float32, 163,840 samples per second)&lt;/a>&lt;/p>
&lt;p>When we start out, we have IQ samples that have been mostly driven to an
imaginary component of 0 and real value range between +1 and -1 for each symbol
period. Our goal now is to determine if we&amp;rsquo;re receiving a signal, and if so,
determine if it&amp;rsquo;s +1 or -1. This is a deceptively hard problem given it spans a
lot of other similarly entertaining hard problems. I&amp;rsquo;ve opted to not solve the
hard problems involved and hope that in practice my very haphazard
implementation works well enough. This turns out to be both good (not solving a
problem is a great way to not spend time on it) and bad (turns out it does
materially impact performance). This segment is the one I plan on revisiting,
first. Expect more here at some point!&lt;/p>
&lt;p>Given that I want to be able to encapsulate three states in the output from
this section (our Symbols are no carrier detected (&amp;ldquo;0&amp;rdquo;), real value 1 (&amp;ldquo;1&amp;rdquo;) or
real value -1 (&amp;quot;-1&amp;quot;)), which means spending cycles to determine what the
baseline noise is to try and identify when a signal breaks through the noise
becomes incredibly important.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-go" data-lang="go">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">var&lt;/span> &lt;span style="color:#a6e22e">idleThreshold&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">var&lt;/span> &lt;span style="color:#a6e22e">thresholdFactor&lt;/span> = &lt;span style="color:#ae81ff">10&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">...&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e">// sigThreshold is used to determine if the symbol&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e">// is -1, +1 or 0. It&amp;#39;s 1.3 times the idle signal&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e">// threshold.&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">var&lt;/span> &lt;span style="color:#a6e22e">sigThreshold&lt;/span> = (&lt;span style="color:#a6e22e">idleThreshold&lt;/span> &lt;span style="color:#f92672">*&lt;/span> &lt;span style="color:#ae81ff">0.3&lt;/span>) &lt;span style="color:#f92672">+&lt;/span> &lt;span style="color:#a6e22e">idleThreshold&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e">// iq contains a single symbol&amp;#39;s worth of IQ samples.&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e">// clock alignment isn&amp;#39;t really considered; so we&amp;#39;ll&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e">// get a bad packet if we have a symbol transition&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e">// in the middle of this buffer. No attempt is made&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e">// to correct for this yet.&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">var&lt;/span> &lt;span style="color:#a6e22e">iq&lt;/span> []&lt;span style="color:#a6e22e">complex&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e">// avg is used to average a chunk of samples in the&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e">// symbol buffer.&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">var&lt;/span> &lt;span style="color:#a6e22e">avg&lt;/span> &lt;span style="color:#66d9ef">float&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">var&lt;/span> &lt;span style="color:#a6e22e">mid&lt;/span> = len(&lt;span style="color:#a6e22e">iq&lt;/span>) &lt;span style="color:#f92672">/&lt;/span> &lt;span style="color:#ae81ff">2&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e">// midNum is used to determine how many symbols to&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e">// average at the middle of the symbol.&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">var&lt;/span> &lt;span style="color:#a6e22e">midNum&lt;/span> = len(&lt;span style="color:#a6e22e">iq&lt;/span>) &lt;span style="color:#f92672">/&lt;/span> &lt;span style="color:#ae81ff">50&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">for&lt;/span> &lt;span style="color:#a6e22e">j&lt;/span> &lt;span style="color:#f92672">:=&lt;/span> &lt;span style="color:#a6e22e">mid&lt;/span>; &lt;span style="color:#a6e22e">j&lt;/span> &amp;lt; &lt;span style="color:#a6e22e">mid&lt;/span>&lt;span style="color:#f92672">+&lt;/span>&lt;span style="color:#a6e22e">midNum&lt;/span>; &lt;span style="color:#a6e22e">j&lt;/span>&lt;span style="color:#f92672">++&lt;/span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">avg&lt;/span> &lt;span style="color:#f92672">+=&lt;/span> real(&lt;span style="color:#a6e22e">iq&lt;/span>[&lt;span style="color:#a6e22e">j&lt;/span>])
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">avg&lt;/span> &lt;span style="color:#f92672">/=&lt;/span> &lt;span style="color:#a6e22e">midNum&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">var&lt;/span> &lt;span style="color:#a6e22e">symbol&lt;/span> &lt;span style="color:#66d9ef">float&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">switch&lt;/span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">case&lt;/span> &lt;span style="color:#a6e22e">avg&lt;/span> &amp;gt; &lt;span style="color:#a6e22e">sigThreshold&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">symbol&lt;/span> = &lt;span style="color:#ae81ff">1&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">case&lt;/span> &lt;span style="color:#a6e22e">avg&lt;/span> &amp;lt; &lt;span style="color:#f92672">-&lt;/span>&lt;span style="color:#a6e22e">sigThreshold&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">symbol&lt;/span> = &lt;span style="color:#f92672">-&lt;/span>&lt;span style="color:#ae81ff">1&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">default&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">symbol&lt;/span> = &lt;span style="color:#ae81ff">0&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e">// update the idleThreshold using the thresholdFactor&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e">// to average the idleThreshold over more samples to&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e">// get a better idea of average noise.&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">idleThreshold&lt;/span> = (
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> (&lt;span style="color:#a6e22e">idleThreshold&lt;/span>&lt;span style="color:#f92672">*&lt;/span>(&lt;span style="color:#a6e22e">thresholdFactor&lt;/span>&lt;span style="color:#f92672">-&lt;/span>&lt;span style="color:#ae81ff">1&lt;/span>) &lt;span style="color:#f92672">+&lt;/span> &lt;span style="color:#a6e22e">symbol&lt;/span>) \
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">/&lt;/span> &lt;span style="color:#a6e22e">thresholdFactor&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> )
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e">// write symbol to output somewhere&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">...&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h4 id="next-steps">Next Steps&lt;/h4>
&lt;p>Now that we have a stream of values that are either &lt;code>+1&lt;/code>, &lt;code>-1&lt;/code> or &lt;code>0&lt;/code>, we
can frame / unframe the data contained in the stream, and decode Packets
contained inside, coming next in &lt;a href="https://k3xec.com/packrat-framing/">Part 4!&lt;/a>&lt;/p></description></item><item><title>Transmitting BPSK symbols (Part 2/5) 🐀</title><link>https://k3xec.com/packrat-transmitting/</link><pubDate>Fri, 03 Dec 2021 11:00:00 -0500</pubDate><guid>https://k3xec.com/packrat-transmitting/</guid><description>&lt;div class="hz-alert-ok">
🐀 This post is part of a series called "PACKRAT". If this is the first post
you've found, it'd be worth reading the
&lt;a href="https://k3xec.com/packrat-intro/">intro post&lt;/a> first and then looking over
&lt;a href="https://k3xec.com/tags/packrat/">all posts in the series&lt;/a>.
&lt;/div>
&lt;p>In the &lt;a href="https://k3xec.com/packrat-processing-iq/">last post&lt;/a>, we worked through what IQ is,
and different formats that it may be sent or received in. Let&amp;rsquo;s take that
and move on to Transmitting BPSK using IQ data!&lt;/p>
&lt;p>When we transmit and receive information through RF using an SDR, data is
traditionally encoded into a stream of
&lt;a href="https://en.wikipedia.org/wiki/Symbol_rate#Symbols">symbols&lt;/a>
which are then used by a program to modulate the IQ stream, and sent over
the airwaves.&lt;/p>
&lt;p>PACKRAT uses &lt;a href="https://en.wikipedia.org/wiki/Phase-shift_keying">BPSK&lt;/a> to
encode Symbols through RF. BPSK is the act of modulating the phase of a
sine wave to carry information. The transmitted wave swaps between two
states in order to convey a 0 or a 1. Our symbols modulate the transmitted
sine wave&amp;rsquo;s phase, so that it moves between in-phase with the SDR&amp;rsquo;s transmitter
and 180 degrees (or π radians) out of phase with the SDR&amp;rsquo;s transmitter.&lt;/p>
&lt;p>The difference between a &amp;ldquo;Bit&amp;rdquo; and a &amp;ldquo;Symbol&amp;rdquo; in PACKRAT is not incredibly
meaningful, and I&amp;rsquo;ll often find myself slipping up when talking about them.
I&amp;rsquo;ve done my best to try and use the right word at the right stage, but it&amp;rsquo;s
not as obvious where the line between bit and symbol is &amp;ndash; at least not as
obvious as it would be with QPSK or QAM. The biggest difference is that there
are three meaningful states for PACKRAT over BPSK - a 1 (for &amp;ldquo;In phase&amp;rdquo;), -1
(for &amp;ldquo;180 degrees out of phase&amp;rdquo;) and 0 (for &amp;ldquo;no carrier&amp;rdquo;). For my
implementation, a stream of all zeros will not transmit data over the airwaves,
a stream of all 1s will transmit all &amp;ldquo;1&amp;rdquo; bits over the airwaves, and a stream
of all -1s will transmit all &amp;ldquo;0&amp;rdquo; bits over the airwaves.&lt;/p>
&lt;p>We&amp;rsquo;re not going to cover turning a byte (or bit) into a symbol yet &amp;ndash; I&amp;rsquo;m going
to write more about that in a later section. So for now, let&amp;rsquo;s just worry about
symbols in, and symbols out.&lt;/p>
&lt;h4 id="transmitting-a-sine-wave-at-0hz">Transmitting a Sine wave at 0Hz&lt;/h4>
&lt;p>If we go back to thinking about IQ data as a precisely timed measurements of
energy over time at some particular specific frequency, we can consider what
a sine wave will look like in IQ. Before we dive into antennas and RF, let&amp;rsquo;s go
to something a bit more visual.&lt;/p>
&lt;p>For the first example, you can see an example of a camera who&amp;rsquo;s frame rate (or
Sampling Rate!) matches the exact number of rotations per second (or
Frequency!) of the propeller and it &lt;a href="https://www.youtube.com/watch?v=g5sPG6Ss5_E">appears to stand exactly
still&lt;/a>. Every time the Camera
takes a frame, it&amp;rsquo;s catching the propeller in the exact same place in space,
even though it&amp;rsquo;s made a complete rotation.&lt;/p>
&lt;p>The second example is very similar, it&amp;rsquo;s a light strobing (in this case, our
sampling rate, since the darkness is ignored by our brains) at the same rate
(frequency) as &lt;a href="https://www.youtube.com/watch?v=OtxlQTmx1LE">water dropping from a
faucet&lt;/a> &amp;ndash; and the video creator
is even nice enough to change the sampling frequency to have the droplets move
both forward and backward (positive and negative frequency) in comparison to the
faucet.&lt;/p>
&lt;p>IQ works the same way. If we catch something in perfect frequency alignment
with our radio, we&amp;rsquo;ll wind up with readings that are the same for the entire
stream of data. This means we can transmit a sine wave by setting all of the
IQ samples in our buffer to &lt;code>1+0i&lt;/code>, which will transmit a pure sine wave at
exactly the center frequency of the radio.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-go" data-lang="go">&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">var&lt;/span> &lt;span style="color:#a6e22e">sine&lt;/span> []&lt;span style="color:#a6e22e">complex&lt;/span>{}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">for&lt;/span> &lt;span style="color:#a6e22e">i&lt;/span> &lt;span style="color:#f92672">:=&lt;/span> &lt;span style="color:#66d9ef">range&lt;/span> &lt;span style="color:#a6e22e">sine&lt;/span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">sine&lt;/span>[&lt;span style="color:#a6e22e">i&lt;/span>] = complex(&lt;span style="color:#ae81ff">1.0&lt;/span>, &lt;span style="color:#ae81ff">0.0&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Alternatively, we can transmit a Sine wave (but with the opposite phase) by
flipping the real value from 1 to -1. The same Sine wave is transmitted on
the same Frequency, except when the wave goes high in the example above, the
wave will go low in the example below.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-go" data-lang="go">&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">var&lt;/span> &lt;span style="color:#a6e22e">sine&lt;/span> []&lt;span style="color:#a6e22e">complex&lt;/span>{}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">for&lt;/span> &lt;span style="color:#a6e22e">i&lt;/span> &lt;span style="color:#f92672">:=&lt;/span> &lt;span style="color:#66d9ef">range&lt;/span> &lt;span style="color:#a6e22e">sine&lt;/span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">sine&lt;/span>[&lt;span style="color:#a6e22e">i&lt;/span>] = complex(&lt;span style="color:#f92672">-&lt;/span>&lt;span style="color:#ae81ff">1.0&lt;/span>, &lt;span style="color:#ae81ff">0.0&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>In fact, we can make a carrier wave at any phase angle and amplitude by using a
bit of trig.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-go" data-lang="go">&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e">// angle is in radians - here we have&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e">// 1.5 Pi (0.75 Tau) or 270 degrees.&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">var&lt;/span> &lt;span style="color:#a6e22e">angle&lt;/span> = &lt;span style="color:#a6e22e">pi&lt;/span> &lt;span style="color:#f92672">*&lt;/span> &lt;span style="color:#ae81ff">1.5&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e">// amplitude controls the transmitted&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e">// strength of the carrier wave.&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">var&lt;/span> &lt;span style="color:#a6e22e">amplitude&lt;/span> = &lt;span style="color:#ae81ff">1.0&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e">// output buffer as above&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">var&lt;/span> &lt;span style="color:#a6e22e">sine&lt;/span> []&lt;span style="color:#a6e22e">complex&lt;/span>{}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">for&lt;/span> &lt;span style="color:#a6e22e">i&lt;/span> &lt;span style="color:#f92672">:=&lt;/span> &lt;span style="color:#66d9ef">range&lt;/span> &lt;span style="color:#a6e22e">sine&lt;/span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">sine&lt;/span>[&lt;span style="color:#a6e22e">i&lt;/span>] = complex(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">amplitude&lt;/span>&lt;span style="color:#f92672">*&lt;/span>&lt;span style="color:#a6e22e">cos&lt;/span>(&lt;span style="color:#a6e22e">angle&lt;/span>),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">amplitude&lt;/span>&lt;span style="color:#f92672">*&lt;/span>&lt;span style="color:#a6e22e">sin&lt;/span>(&lt;span style="color:#a6e22e">angle&lt;/span>),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> )
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>The amplitude of the transmitted wave is the absolute value of the IQ
sample (sometimes called magnitude), and the phase can be computed as the
angle (or argument). The amplitude remains constant (at 1) in both cases.
Remember back to the airplane propeller or water droplets &amp;ndash; we&amp;rsquo;re controlling
where we&amp;rsquo;re observing the sine wave. It looks like a consistent value
to us, but in reality it&amp;rsquo;s being transmitted as a pure carrier wave at the
provided frequency. Changing the angle of the number we&amp;rsquo;re transmitting will
control where in the sine wave cycle we&amp;rsquo;re &amp;ldquo;observing&amp;rdquo; it at.&lt;/p>
&lt;h4 id="generating-bpsk-modulated-iq-data">Generating BPSK modulated IQ data&lt;/h4>
&lt;p>Modulating our carrier wave with our symbols is fairly straightforward to
do &amp;ndash; we can multiply the symbol by 1 to get the real value to be used
in the IQ stream. Or, more simply - we can just use the symbol directly in
the constructed IQ data.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-go" data-lang="go">&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">var&lt;/span> &lt;span style="color:#a6e22e">sampleRate&lt;/span> = &lt;span style="color:#ae81ff">2&lt;/span>,&lt;span style="color:#ae81ff">621&lt;/span>,&lt;span style="color:#ae81ff">440&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">var&lt;/span> &lt;span style="color:#a6e22e">baudRate&lt;/span> = &lt;span style="color:#ae81ff">1024&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e">// This represents the number of IQ samples&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e">// required to send a single symbol at the&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e">// provided baud and sample rate. I picked&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e">// two numbers in order to avoid half samples.&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e">// We will transmit each symbol in blocks of&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e">// this size.&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">var&lt;/span> &lt;span style="color:#a6e22e">samplesPerSymbol&lt;/span> = &lt;span style="color:#a6e22e">sampleRate&lt;/span> &lt;span style="color:#f92672">/&lt;/span> &lt;span style="color:#a6e22e">baudRate&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">var&lt;/span> &lt;span style="color:#a6e22e">samples&lt;/span> = make([]&lt;span style="color:#a6e22e">complex&lt;/span>, &lt;span style="color:#a6e22e">samplesPerSymbol&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e">// symbol is one of 1, -1 or 0.&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">for&lt;/span> &lt;span style="color:#a6e22e">each&lt;/span> &lt;span style="color:#a6e22e">symbol&lt;/span> &lt;span style="color:#a6e22e">in&lt;/span> &lt;span style="color:#a6e22e">symbols&lt;/span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">for&lt;/span> &lt;span style="color:#a6e22e">i&lt;/span> &lt;span style="color:#f92672">:=&lt;/span> &lt;span style="color:#66d9ef">range&lt;/span> &lt;span style="color:#a6e22e">samples&lt;/span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">samples&lt;/span>[&lt;span style="color:#a6e22e">i&lt;/span>] = complex(&lt;span style="color:#a6e22e">symbol&lt;/span>, &lt;span style="color:#ae81ff">0&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e">// write the samples out to an output file&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e">// or radio.&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">write&lt;/span>(&lt;span style="color:#a6e22e">samples&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>If you want to check against a baseline capture, here&amp;rsquo;s
&lt;a href="https://k3xec.s3.amazonaws.com/transmit/10_raw_packets_2048000sps_c64.rfcap">10 example packets at 204800 samples per second&lt;/a>.&lt;/p>
&lt;h4 id="next-steps">Next Steps&lt;/h4>
&lt;p>Now that we can transmit data, we&amp;rsquo;ll start working on a receive path in Part 3,
in order to check our work when transmitting the packets, as well as being
able to hear packets we transmit from afar, coming up next in
&lt;a href="https://k3xec.com/packrat-receiving/">Part 3!&lt;/a>!&lt;/p></description></item><item><title>Processing IQ data formats (Part 1/5) 🐀</title><link>https://k3xec.com/packrat-processing-iq/</link><pubDate>Thu, 02 Dec 2021 12:00:00 -0500</pubDate><guid>https://k3xec.com/packrat-processing-iq/</guid><description>&lt;div class="hz-alert-ok">
🐀 This post is part of a series called "PACKRAT". If this is the first post
you've found, it'd be worth reading the
&lt;a href="https://k3xec.com/packrat-intro/">intro post&lt;/a> first and then looking over
&lt;a href="https://k3xec.com/tags/packrat/">all posts in the series&lt;/a>.
&lt;/div>
&lt;p>When working with SDRs, information about the signals your radio is receiving
are communicated by streams of
&lt;a href="https://en.wikipedia.org/wiki/In-phase_and_quadrature_components">IQ&lt;/a>
data. IQ is short for &amp;ldquo;In-phase&amp;rdquo; and &amp;ldquo;Quadrature&amp;rdquo;, which means 90 degrees
out of phase. Values in the IQ stream are
&lt;a href="https://en.wikipedia.org/wiki/Complex_number">complex numbers&lt;/a>, so
converting them to a native complex type in your language
helps greatly when processing the IQ data for meaning.&lt;/p>
&lt;p>I won&amp;rsquo;t get too deep into what IQ is or why complex numbers (mostly since I
don&amp;rsquo;t think I fully understand it well enough to explain it yet), but here&amp;rsquo;s
some basics in case this is your first interaction with IQ data before
going off and reading more.&lt;/p>
&lt;div class="hz-alert-warning">
Before we get started &amp;mdash;
at any point, if you feel lost in this post, it's OK to take a break to do
a bit of learning elsewhere in the internet. I'm still new to this, so I'm
sure my overview in one paragraph here won't help clarify things too much.
This took me months to sort out on my own. It's not you, really! I particularly
enjoyed reading
&lt;a href="https://visual-dsp.switchb.org/presentation">visual-dsp.switchb.org&lt;/a>
when it came to learning about how IQ represents signals, and
&lt;a href="https://www.analog.com/en/education/education-library/software-defined-radio-for-engineers.html">Software-Defined Radio for Engineers&lt;/a>
for a more general reference.
&lt;/div>
&lt;p>Each value in the stream is taken at a precisely spaced sampling interval
(called the sampling rate of the radio). Jitter in that sampling interval, or a
drift in the requested and actual sampling rate (usually represented in PPM, or
parts per million &amp;ndash; how many samples out of one million are missing) can cause
errors in frequency. In the case of a PPM error, one radio may think it&amp;rsquo;s
100.1MHz and the other may think it&amp;rsquo;s 100.2MHz, and jitter will result in added
noise in the resulting stream.&lt;/p>
&lt;p>A single IQ sample is both the real and imaginary values, together. The complex
number (both parts) is the sample. The number of samples per second is the
number of real and imaginary value pairs per second.&lt;/p>
&lt;p>Each sample is reading the electrical energy coming off the antenna at that
exact time instant. We&amp;rsquo;re looking to see how that goes up and down over time to
determine what frequencies we&amp;rsquo;re observing around us. If the IQ stream is only
real-valued measures (e.g., float values rather than complex values reading
voltage from a wire), you can still send and receive signals, but those signals
will be mirrored across your 0Hz boundary. That means if you&amp;rsquo;re tuned to
100MHz, and you have a nearby transmitter at 99.9MHz, you&amp;rsquo;d see it at 100.1MHz.
If you want to get an intuitive understanding of this concept before getting
into the heavy math, a good place to start is looking at how &lt;a href="https://en.wikipedia.org/wiki/Incremental_encoder#Quadrature_outputs">Quadrature
encoders&lt;/a>
work. Using complex numbers means we can see &amp;ldquo;up&amp;rdquo; in frequency as well as
&amp;ldquo;down&amp;rdquo; in frequency, and understand that those are different signals.&lt;/p>
&lt;p>The reason why we need negative frequencies is that our 0Hz is the center of
our SDR&amp;rsquo;s tuned frequency, not actually at 0Hz in nature. Generally speaking,
it&amp;rsquo;s doing loads in hardware (and firmware!) to mix the raw RF signals with a
local oscillator to a frequency that can be sampled at the requested rate
(fundamentally the same concept as a &lt;a href="https://en.wikipedia.org/wiki/Superheterodyne_receiver">superheterodyne
receiver&lt;/a>), so a
frequency of &amp;lsquo;-10MHz&amp;rsquo; means that signal is 10 MHz below the center of our SDR&amp;rsquo;s
tuned frequency.&lt;/p>
&lt;p>The sampling rate dictates the amount of frequency representable in the data
stream. You&amp;rsquo;ll sometimes see this called the
&lt;a href="https://en.wikipedia.org/wiki/Nyquist_frequency">Nyquist frequency&lt;/a>. The Nyquist
Frequency is one half of the sampling rate. Intuitively, if you think about the
amount of bandwidth observable as being 1:1 with the sampling rate of the stream,
and the middle of your bandwidth is 0 Hz, you would only have enough space to
go up in frequency for half of your bandwidth &amp;ndash; or half of your sampling rate.
Same for going down in frequency.&lt;/p>
&lt;h4 id="float-32--complex-64">Float 32 / Complex 64&lt;/h4>
&lt;p>IQ samples that are being processed by software are commonly processed as
an interleaved pair of 32 bit floating point numbers, or a 64 bit complex
number. The first float32 is the real value, and the second is the imaginary
value.&lt;/p>
&lt;div class="hz-abi">
&lt;div type="f32" class="hz-abi-green hz-abi-2b">I#0&lt;/div>
&lt;div type="f32" class="hz-abi-green hz-abi-2b">Q#0&lt;/div>
&lt;div type="f32" class="hz-abi-green hz-abi-2b">I#1&lt;/div>
&lt;div type="f32" class="hz-abi-green hz-abi-2b">Q#1&lt;/div>
&lt;div type="f32" class="hz-abi-green hz-abi-2b">I#2&lt;/div>
&lt;div type="f32" class="hz-abi-green hz-abi-2b">Q#2&lt;/div>
&lt;/div>
&lt;p>The complex number &lt;code>1+1i&lt;/code> is represented as &lt;code>1.0 1.0&lt;/code> and the complex number
&lt;code>-1-1i&lt;/code> is represented as &lt;code>-1.0 -1.0&lt;/code>. &lt;b>Unless otherwise specified, all the
IQ samples and pseudocode to follow assumes interleaved float32 IQ data
streams.&lt;/b>&lt;/p>
&lt;p>&lt;a href="https://k3xec.s3.amazonaws.com/iq/10hz_1024sps_c64.rfcap">Example interleaved float32 file (10Hz Wave at 1024 Samples per Second)&lt;/a>&lt;/p>
&lt;h4 id="rtl-sdr">RTL-SDR&lt;/h4>
&lt;p>IQ samples from the RTL-SDR are encoded as a stream of interleaved unsigned
8 bit integers (uint8 or u8). The first sample is the real (in-phase or I)
value, and the second is the imaginary (quadrature or Q) value. Together each
pair of values makes up a complex number at a specific time instant.&lt;/p>
&lt;div class="hz-abi">
&lt;div type="u8" class="hz-abi-green hz-abi-1b">I#0&lt;/div>
&lt;div type="u8" class="hz-abi-green hz-abi-1b">Q#0&lt;/div>
&lt;div type="u8" class="hz-abi-green hz-abi-1b">I#1&lt;/div>
&lt;div type="u8" class="hz-abi-green hz-abi-1b">Q#1&lt;/div>
&lt;div type="u8" class="hz-abi-green hz-abi-1b">I#2&lt;/div>
&lt;div type="u8" class="hz-abi-green hz-abi-1b">Q#2&lt;/div>
&lt;/div>
&lt;p>The complex number &lt;code>1+1i&lt;/code> is represented as &lt;code>0xFF 0xFF&lt;/code> and the complex number
&lt;code>-1-1i&lt;/code> is represented as &lt;code>0x00 0x00&lt;/code>. The complex number &lt;code>0+0i&lt;/code> is not easily
representable &amp;ndash; since half of &lt;code>0xFF&lt;/code> is &lt;code>127.5&lt;/code>.&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;td>Complex Number&lt;/td>
&lt;td>Representation&lt;/td>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>1+1i&lt;/td>
&lt;td>[]uint8{0xFF, 0xFF}&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>-1+1i&lt;/td>
&lt;td>[]uint8{0x00, 0xFF}&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>-1-1i&lt;/td>
&lt;td>[]uint8{0x00, 0x00}&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>0+0i&lt;/td>
&lt;td>[]uint8{0x80, 0x80} or []uint8{0x7F, 0x7F}&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>And finally, here&amp;rsquo;s some pseudocode to convert an rtl-sdr style IQ sample
to a floating point complex number:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-go" data-lang="go">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">...&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">in&lt;/span> = []&lt;span style="color:#66d9ef">uint8&lt;/span>{&lt;span style="color:#ae81ff">0x7F&lt;/span>, &lt;span style="color:#ae81ff">0x7F&lt;/span>}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">real&lt;/span> = (float(&lt;span style="color:#a6e22e">iq&lt;/span>[&lt;span style="color:#ae81ff">0&lt;/span>])&lt;span style="color:#f92672">-&lt;/span>&lt;span style="color:#ae81ff">127.5&lt;/span>)&lt;span style="color:#f92672">/&lt;/span>&lt;span style="color:#ae81ff">127.5&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">imag&lt;/span> = (float(&lt;span style="color:#a6e22e">iq&lt;/span>[&lt;span style="color:#ae81ff">1&lt;/span>])&lt;span style="color:#f92672">-&lt;/span>&lt;span style="color:#ae81ff">127.5&lt;/span>)&lt;span style="color:#f92672">/&lt;/span>&lt;span style="color:#ae81ff">127.5&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">out&lt;/span> = complex(&lt;span style="color:#a6e22e">real&lt;/span>, &lt;span style="color:#a6e22e">imag&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">...&lt;/span>.
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;a href="https://k3xec.s3.amazonaws.com/iq/10hz_1024sps_u8.rfcap">Example interleaved uint8 file (10Hz Wave at 1024 Samples per Second)&lt;/a>&lt;/p>
&lt;h4 id="hackrf">HackRF&lt;/h4>
&lt;p>IQ samples from the HackRF are encoded as a stream of interleaved signed
8 bit integers (int8 or i8). The first sample is the real (in-phase or I)
value, and the second is the imaginary (quadrature or Q) value. Together each
pair of values makes up a complex number at a specific time instant.&lt;/p>
&lt;div class="hz-abi">
&lt;div type="i8" class="hz-abi-green hz-abi-1b">I#0&lt;/div>
&lt;div type="i8" class="hz-abi-green hz-abi-1b">Q#0&lt;/div>
&lt;div type="i8" class="hz-abi-green hz-abi-1b">I#1&lt;/div>
&lt;div type="i8" class="hz-abi-green hz-abi-1b">Q#1&lt;/div>
&lt;div type="i8" class="hz-abi-green hz-abi-1b">I#2&lt;/div>
&lt;div type="i8" class="hz-abi-green hz-abi-1b">Q#2&lt;/div>
&lt;/div>
&lt;p>Formats that use signed integers do have one quirk due to
&lt;a href="https://en.wikipedia.org/wiki/Two%27s_complement">two&amp;rsquo;s complement&lt;/a>,
which is that the smallest negative number representable&amp;rsquo;s absolute
value is one more than the largest positive number. &lt;code>int8&lt;/code> values can
range between &lt;code>-128&lt;/code> to &lt;code>127&lt;/code>, which means there&amp;rsquo;s bit of ambiguity in
how +1, 0 and -1 are represented. Either you can create perfectly symmetric
ranges of values between +1 and -1, but 0 is not representable, have more
possible values in the negative range, or allow values above (or just below)
the maximum in the range to be allowed.&lt;/p>
&lt;p>Within my implementation, my approach has been to scale based on the max
integer value of the type, so the lowest possible signed value is actually
slightly smaller than &lt;code>-1&lt;/code>. Generally, if your code is seeing values that low
the difference in step between -1 and slightly less than -1 isn&amp;rsquo;t very
significant, even with only 8 bits. Just a curiosity to be aware of.&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;td>Complex Number&lt;/td>
&lt;td>Representation&lt;/td>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>1+1i&lt;/td>
&lt;td>[]int8{127, 127}&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>-1+1i&lt;/td>
&lt;td>[]int8{-128, 127}&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>-1-1i&lt;/td>
&lt;td>[]int8{-128, -128}&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>0+0i&lt;/td>
&lt;td>[]int8{0, 0}&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>And finally, here’s some pseudocode to convert a hackrf style IQ sample to a
floating point complex number:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-go" data-lang="go">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">...&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">in&lt;/span> = []&lt;span style="color:#66d9ef">int8&lt;/span>{&lt;span style="color:#f92672">-&lt;/span>&lt;span style="color:#ae81ff">5&lt;/span>, &lt;span style="color:#ae81ff">112&lt;/span>}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">real&lt;/span> = (float(&lt;span style="color:#a6e22e">in&lt;/span>[&lt;span style="color:#ae81ff">0&lt;/span>]))&lt;span style="color:#f92672">/&lt;/span>&lt;span style="color:#ae81ff">127&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">imag&lt;/span> = (float(&lt;span style="color:#a6e22e">in&lt;/span>[&lt;span style="color:#ae81ff">1&lt;/span>]))&lt;span style="color:#f92672">/&lt;/span>&lt;span style="color:#ae81ff">127&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">out&lt;/span> = complex(&lt;span style="color:#a6e22e">real&lt;/span>, &lt;span style="color:#a6e22e">imag&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">...&lt;/span>.
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;a href="https://k3xec.s3.amazonaws.com/iq/10hz_1024sps_i8.rfcap">Example interleaved int8 file (10Hz Wave at 1024 Samples per Second)&lt;/a>&lt;/p>
&lt;h4 id="plutosdr">PlutoSDR&lt;/h4>
&lt;p>IQ samples from the PlutoSDR are encoded as a stream of interleaved signed
16 bit integers (int16 or i16). The first sample is the real (in-phase or I)
value, and the second is the imaginary (quadrature or Q) value. Together each
pair of values makes up a complex number at a specific time instant.&lt;/p>
&lt;p>Almost no SDRs capture at a 16 bit depth natively, often you&amp;rsquo;ll see 12 bit
integers (as is the case with the PlutoSDR) being sent around as 16 bit
integers. This leads to the next possible question, which is are values LSB
or MSB aligned? The PlutoSDR sends data LSB aligned (which is to say, the
largest real or imaginary value in the stream will not exceed 4095), but
expects data being transmitted to be MSB aligned (which is to say the lowest
set bit possible is the 5th bit in the number, or values can only be set in
increments of 16).&lt;/p>
&lt;p>As a result, the quirk observed with the HackRF (that the range of values
between 0 and -1 is different than the range of values between 0 and +1) does
not impact us so long as we do not use the whole 16 bit range.&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;td>Complex Number&lt;/td>
&lt;td>Representation&lt;/td>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>1+1i&lt;/td>
&lt;td>[]int16{32767, 32767}&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>-1+1i&lt;/td>
&lt;td>[]int16{-32768, 32767}&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>-1-1i&lt;/td>
&lt;td>[]int16{-32768, -32768}&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>0+0i&lt;/td>
&lt;td>[]int16{0, 0}&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>And finally, here’s some pseudocode to convert a PlutoSDR style IQ sample to a
floating point complex number, including moving the sample from LSB to MSB
aligned:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-go" data-lang="go">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">...&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">in&lt;/span> = []&lt;span style="color:#66d9ef">int16&lt;/span>{&lt;span style="color:#f92672">-&lt;/span>&lt;span style="color:#ae81ff">15072&lt;/span>, &lt;span style="color:#ae81ff">496&lt;/span>}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e">// shift left 4 bits (16 bits - 12 bits = 4 bits)&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e">// to move from LSB aligned to MSB aligned.&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">in&lt;/span>[&lt;span style="color:#ae81ff">0&lt;/span>] = &lt;span style="color:#a6e22e">in&lt;/span>[&lt;span style="color:#ae81ff">0&lt;/span>] &lt;span style="color:#f92672">&amp;lt;&amp;lt;&lt;/span> &lt;span style="color:#ae81ff">4&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">in&lt;/span>[&lt;span style="color:#ae81ff">1&lt;/span>] = &lt;span style="color:#a6e22e">in&lt;/span>[&lt;span style="color:#ae81ff">1&lt;/span>] &lt;span style="color:#f92672">&amp;lt;&amp;lt;&lt;/span> &lt;span style="color:#ae81ff">4&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">real&lt;/span> = (float(&lt;span style="color:#a6e22e">in&lt;/span>[&lt;span style="color:#ae81ff">0&lt;/span>]))&lt;span style="color:#f92672">/&lt;/span>&lt;span style="color:#ae81ff">32767&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">imag&lt;/span> = (float(&lt;span style="color:#a6e22e">in&lt;/span>[&lt;span style="color:#ae81ff">1&lt;/span>]))&lt;span style="color:#f92672">/&lt;/span>&lt;span style="color:#ae81ff">32767&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">out&lt;/span> = complex(&lt;span style="color:#a6e22e">real&lt;/span>, &lt;span style="color:#a6e22e">imag&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">...&lt;/span>.
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;a href="https://k3xec.s3.amazonaws.com/iq/10hz_1024sps_i16.rfcap">Example interleaved i16 file (10Hz Wave at 1024 Samples per Second)&lt;/a>&lt;/p>
&lt;h4 id="next-steps">Next Steps&lt;/h4>
&lt;p>Now that we can read (and write!) IQ data, we can get started first on the
transmitter, which we can (in turn) use to test receiving our own BPSK signal,
&lt;a href="https://k3xec.com/packrat-transmitting/">coming next in Part 2!&lt;/a>&lt;/p></description></item><item><title>Intro to PACKRAT (Part 0/5) 🐀</title><link>https://k3xec.com/packrat-intro/</link><pubDate>Thu, 02 Dec 2021 11:00:00 -0500</pubDate><guid>https://k3xec.com/packrat-intro/</guid><description>&lt;p>Hello! Welcome. I&amp;rsquo;m so thrilled you&amp;rsquo;re here.&lt;/p>
&lt;p>Some of you may know this (as I&amp;rsquo;ve written about in the past), but if you&amp;rsquo;re
new to my RF travels, I’ve spent nights and weekends over the last two years
doing some self directed learning on how radios work. I’ve gone from a very
basic understanding of wireless communications, all the way through the process
of learning about and implementing a set of libraries to modulate and
demodulate data using my now formidable stash of
&lt;a href="https://en.wikipedia.org/wiki/Software-defined_radio">SDRs&lt;/a>. I’ve been
implementing all of the RF processing code from first principals and purely
based on other primitives I’ve written myself to prove to myself that I
understand each concept before moving on.&lt;/p>
&lt;p>I&amp;rsquo;ve just finished a large personal milestone &amp;ndash; I was able to successfully
send a cURL HTTP request through a network interface into my stack of
libraries, through my own
&lt;a href="https://en.wikipedia.org/wiki/Phase-shift_keying#Binary_phase-shift_keying_(BPSK)">BPSK&lt;/a>
implementation, framed in my own artisanal hand crafted Layer 2 framing scheme,
demodulated by my code on the other end, and sent into a Linux network
interface. The combination of the &lt;a href="https://en.wikipedia.org/wiki/Physical_layer">Layer 1
PHY&lt;/a> and &lt;a href="https://en.wikipedia.org/wiki/Data_link_layer">Layer 2 Data
Link&lt;/a> is something that I&amp;rsquo;ve
been calling &amp;ldquo;PACKRAT&amp;rdquo;.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-txt" data-lang="txt">&lt;span style="display:flex;">&lt;span>$ curl http://44.127.0.8:8000/
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>* Connected to 44.127.0.8 (44.127.0.8) port 8000 (#0)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&amp;gt; GET / HTTP/1.1
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&amp;gt; Host: localhost:1313
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&amp;gt; User-Agent: curl/7.79.1
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&amp;gt; Accept: */*
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>* Mark bundle as not supporting multiuse
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>* HTTP/1.0, assume close after body
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&amp;lt; HTTP/1.0 200 OK
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&amp;lt; Content-Length: 236
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&amp;lt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ____ _ ____ _ ______ _ _____
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>| _ \ / \ / ___| |/ / _ \ / \|_ _|
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>| |_) / _ \| | | &amp;#39; /| |_) | / _ \ | |
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>| __/ ___ \ |___| . \| _ &amp;lt; / ___ \| |
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>|_| /_/ \_\____|_|\_\_| \_\/_/ \_\_|
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>* Closing connection 0
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>In an effort to &amp;ldquo;pay it forward&amp;rdquo; to thank my friends for their time walking me
through huge chunks of this, and those who publish their work, I&amp;rsquo;m now spending
some time documenting how I was able to implement this protocol. I would never
have gotten as far as I did without the incredible patience and kindness of
friends spending time working with me, and educators publishing their hard work
for the world to learn from. Please accept my deepest thanks and appreciation.&lt;/p>
&lt;p>The PACKRAT posts are written from the perspective of a novice radio engineer,
but experienced software engineer. &lt;span class="hz-highlight">I&amp;rsquo;ll be leaving
out a lot of the technical details on the software end and specific software
implementation, focusing on the general gist of the implementation in the radio
critical components exclusively. The idea here is this is intended to be a
framework &amp;ndash; a jumping off point &amp;ndash; for those who are interested in doing this
themselves.&lt;/span> I hope that this series of blog posts will come to be useful
to those who embark on this incredibly rewarding journey after me.&lt;/p>
&lt;p>This is the first post in the series, and it will contain links to all the
posts to follow. This is going to be the landing page I link others to &amp;ndash; as I
publish additional posts, I&amp;rsquo;ll be updating the links on this page. The posts
will also grow a tag, which you can check back on, or follow along with
&lt;a href="https://k3xec.com/tags/packrat/">here&lt;/a>.&lt;/p>
&lt;h4 id="tau">Tau&lt;/h4>
&lt;p>Tau (𝜏) is a &lt;a href="https://tauday.com/tau-manifesto">much more natural expression of the mathematical constant used
for circles&lt;/a> which I use rather than Pi (π).
You may see me use Tau in code or text &amp;ndash; Tau is the same as 2π, so if you
see a Tau and don&amp;rsquo;t know what to do, feel free to mentally or textually
replace it with 2π. I just hate always writing 2π everywhere &amp;ndash; and only
using π (or worse yet &amp;ndash; 2π/2) .when I mean 1/2 of a circle (or, 𝜏/2).&lt;/p>
&lt;h4 id="pseudo-code">Pseudo-code&lt;/h4>
&lt;p>Basically none of the code contained in this series is valid on its own. It&amp;rsquo;s
very lightly basically Go, and only meant to express concepts in term of
software. The examples in the post shouldn&amp;rsquo;t be taken on their own as working
snippits to process IQ data, but rather, be used to guide implementations
to process the data in question. I&amp;rsquo;d love to invite all readers to try to
&amp;ldquo;play at home&amp;rdquo; with the examples, and try and work through the example data
captures!&lt;/p>
&lt;h4 id="captures">Captures&lt;/h4>
&lt;p>Speaking of captures, I&amp;rsquo;ve included live on-the-air captures of PACKRAT
packets, as transmitted from my implementation, in different parts of these
posts. This means you can go through the process of building code to parse and
receive PACKRAT packets, and then build a transmitter that is validated by your
receiver. It&amp;rsquo;s my hope folks will follow along at home and experiment with
software to process RF data on their own!&lt;/p>
&lt;h4 id="posts-in-this-series">Posts in this series&lt;/h4>
&lt;ul>
&lt;li>Part 1: &lt;a href="https://k3xec.com/packrat-processing-iq/">Processing IQ data&lt;/a>&lt;/li>
&lt;li>Part 2: &lt;a href="https://k3xec.com/packrat-transmitting/">Transmitting BPSK symbols&lt;/a>&lt;/li>
&lt;li>Part 3: &lt;a href="https://k3xec.com/packrat-receiving/">Receiving BPSK symbols&lt;/a>&lt;/li>
&lt;li>Part 4: &lt;a href="https://k3xec.com/packrat-framing/">Framing data&lt;/a>&lt;/li>
&lt;li>Part 5: &lt;a href="https://k3xec.com/packrat-proxy/">Proxying Ethernet Frames to PACKRAT&lt;/a>&lt;/li>
&lt;/ul></description></item><item><title>Measuring the Power Output of my SDRs ⚡</title><link>https://k3xec.com/power-output/</link><pubDate>Mon, 15 Nov 2021 22:06:00 -0500</pubDate><guid>https://k3xec.com/power-output/</guid><description>&lt;p>Over the last few years, I’ve often wondered what the true power output of my
SDRs are. It’s a question with a shocking amount of complexity in the
response, due to a number of factors (mostly Frequency). The ranges given in
spec sheets are often extremely vague, and if I’m being honest with myself,
not incredibly helpful for being able to determine what specific filters and
amplifiers I’ll need to get a clean signal transmitted.&lt;/p>
&lt;div class="hz-alert-warning">
&lt;b>Hey, heads up!&lt;/b> - This post contains extremely unvalidated and back of
the napkin quality work to understand how my equipment works. Hopefully this
work can be of help to others, but please double check any information you need
for your own work!
&lt;/div>
&lt;p>I was specifically interested in what gain output (in dBm) looks like across
the frequency range &amp;ndash; in particular, how variable the output dBm is when I
change frequencies. The second question I had was understanding how linear the
output gain is when adjusting the requested gain from the radio. Does a 2 dB
increase on a HackRF API mean 2 dB of gain in dBm, no matter what the absolute
value of the gain stage is?&lt;/p>
&lt;p>I’ve finally bit the bullet and undertaken work to characterize the hardware I
do have, with some outdated laboratory equipment I found on eBay. Of course, if
it’s worth doing, it’s worth overdoing, so I spent a bit of time automating
a handful of components in order to collect the data that I need from my
SDRs.&lt;/p>
&lt;p>&lt;img src="https://k3xec.com/imgs/power-output/HP437B.png" alt="">&lt;/p>
&lt;p>I bought an HP 437B, which is the cutting edge of 30 years ago, but still
accurate to within 0.01dBm. I paired this Power Meter with an Agilent 8481A
Power Sensor (-30 dBm to 20 dBm from 10MHz to 18GHz). For some of my radios, I
was worried about exceeding the 20 dBm mark, so I used a 20db attenuator while
I waited for a higher power power sensor. Finally, I was able to find a GPIB to
USB interface, and get that interface working with the GPIB Kernel driver on my
system.&lt;/p>
&lt;p>With all that out of the way, I was able to write &lt;a href="https://hz.tools/gpib/">Go bindings to my HP
437B&lt;/a> to allow for totally headless and automated
control in sync with my SDR’s RF output. This allowed me to script the
transmission of a sine wave at a controlled amplitude across a defined gain
range and frequency range and read the Power Sensor’s measured dBm output to
characterize the Gain across frequency and configured Gain.&lt;/p>
&lt;p>&lt;img src="https://k3xec.com/imgs/power-output/test-setup.png" alt="">&lt;/p>
&lt;h1 id="hackrf">HackRF&lt;/h1>
&lt;p>Looking at configured Gain against output power, the requested gain appears to
have a fairly linear relation to the output signal power. The measured dBm
ranged between the sensor noise floor to approx +13dBm. The average standard
deviation of all tested gain values over the frequency range swept was +/-2dBm,
with a minimum standard deviation of +/-0.8dBm, and a maximum of +/-3dBm.&lt;/p>
&lt;p>&lt;img src="https://k3xec.com/imgs/power-output/hackrf-gain-gain.png" alt="">&lt;/p>
&lt;p>When looking at output power over the frequency range swept, the HackRF
contains a distinctive (and frankly jarring) ripple across the Frequency range,
with a clearly visible jump in gain somewhere around 2.1GHz. I have no idea
what is causing this massive jump in output gain, nor what is causing these
distinctive ripples. I’d love to know more if anyone’s familiar with HackRF&amp;rsquo;s
RF internals!&lt;/p>
&lt;p>&lt;img src="https://k3xec.com/imgs/power-output/hackrf-freq-gain.png" alt="">&lt;/p>
&lt;h1 id="plutosdr">PlutoSDR&lt;/h1>
&lt;p>The power output is very linear when operating above -20dB TX channel
gain, but can get quite erratic the lower the output power is configured. The
PlutoSDR’s output power is directly related to the configured power level,
and is generally predictable once a minimum power level is reached. The
measured dBm ranged from the noise floor to 3.39 dBm, with an average standard
deviation of +/-1.98 dBm, a minimum standard deviation of +/-0.91 dBm and a
maximum standard deviation of +/-3.37 dBm.&lt;/p>
&lt;p>&lt;img src="https://k3xec.com/imgs/power-output/pluto-gain-gain.png" alt="">&lt;/p>
&lt;p>Generally, the power output is quite stable, and looks to have very even and
wideband gain control. There’s a few artifacts, which I have not confidently
isolated to the SDR TX gain, noise (transmit artifacts such as intermodulation)
or to my test setup. They appear fairly narrowband, so I’m not overly worried
about them yet. If anyone has any ideas what this could be, I’d very much
appreciate understanding why they exist!&lt;/p>
&lt;p>&lt;img src="https://k3xec.com/imgs/power-output/pluto-freq-gain.png" alt="">&lt;/p>
&lt;h1 id="ettus-b210">Ettus B210&lt;/h1>
&lt;p>The power output on the Ettus B210 is higher (in dBm) than any of my other
radios, but it has a very odd quirk where the power becomes nonlinear
somewhere around -55dB TX channel gain. After that point, adding gain has no
effect on the measured signal output in dBm up to 0 dB gain. The measured dBm
ranged from the noise floor to 18.31 dBm, with an average standard deviation of
+/-2.60 dBm, a minimum of +/-1.39 dBm and a maximum of +/-5.82 dBm.&lt;/p>
&lt;p>&lt;img src="https://k3xec.com/imgs/power-output/ettus-gain-gain.png" alt="">&lt;/p>
&lt;p>When the Gain is somewhere around the noise floor, the measured gain is
incredibly erratic, which throws the maximum standard deviation significantly.
I haven’t isolated that to my test setup or the radio itself. I’m inclined to
believe it’s my test setup. The radio has a fairly even and wideband gain, and
so long as you’re operating between -70dB to -55dB, fairly linear as well.&lt;/p>
&lt;p>&lt;img src="https://k3xec.com/imgs/power-output/ettus-freq-gain.png" alt="">&lt;/p>
&lt;h1 id="summary">Summary&lt;/h1>
&lt;p>Of all my radios, the Ettus B210 has the highest output (in dBm) over the
widest frequency range, but the HackRF is a close second, especially after the
gain bump kicks in around 2.1GHz. The Pluto SDR feels the most predictable and
consistent, but also a very low output, comparatively - right around 0 dBm.&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Name&lt;/th>
&lt;th>Max dBm&lt;/th>
&lt;th>stdev dBm&lt;/th>
&lt;th>stdev min dBm&lt;/th>
&lt;th>stdev max dBm&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>HackRF&lt;/td>
&lt;td>+12.6&lt;/td>
&lt;td>+/-2.0&lt;/td>
&lt;td>+/-0.8&lt;/td>
&lt;td>+/-3.0&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>PlutoSDR&lt;/td>
&lt;td>+3.3&lt;/td>
&lt;td>+/-2.0&lt;/td>
&lt;td>+/-0.9&lt;/td>
&lt;td>+/-3.7&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>B210&lt;/td>
&lt;td>+18.3&lt;/td>
&lt;td>+/-2.6&lt;/td>
&lt;td>+/-1.4&lt;/td>
&lt;td>+/-6.0&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>&lt;img src="https://k3xec.com/imgs/power-output/all-freq-gain.png" alt="">&lt;/p></description></item><item><title>Reverse Engineering my Christmas Tree 🎄</title><link>https://k3xec.com/christmas/</link><pubDate>Sat, 26 Dec 2020 11:24:00 -0500</pubDate><guid>https://k3xec.com/christmas/</guid><description>&lt;p>Over the course of the last year and a half, I&amp;rsquo;ve been doing some self-directed
learning on how radios work. I&amp;rsquo;ve gone from a very basic understanding of
wireless communications (there&amp;rsquo;s usually some sort of antenna, I guess?) all
the way through the process of learning about and implementing a set of
libraries to modulate and demodulate data using my now formidable stash of SDRs.
I&amp;rsquo;ve been implementing all of the RF processing code from first principals and
purely based on other primitives I&amp;rsquo;ve written myself to prove to myself that I
understand each concept before moving on.&lt;/p>
&lt;p>I figured that there was a fun &amp;ldquo;capstone&amp;rdquo; to be done here - the blind reverse
engineering and implementation of the protocol my cheep Amazon power switch
uses to turn on and off my Christmas Tree. All the work described in this post
was done over the course of a few hours thanks to help during the demodulation
from &lt;a href="https://github.com/tomberek/">Tom Bereknyei&lt;/a> and
&lt;a href="https://blog.setec.io/">hlieberman&lt;/a>.&lt;/p>
&lt;h1 id="going-in-blind">Going in blind&lt;/h1>
&lt;p>When I first got my switch, I checked it for any FCC markings in order to look
up the FCC filings to determine the operational frequency of the device, and
maybe some other information such as declared modulation or maybe even part
numbers and/or diagrams. However, beyond a few regulatory stickers, there were
no FCC ids or other distinguishing IDs on the device. Worse yet, it appeared to
be a whitelabeled version of another product, so searching Google for the
product name was very unhelpful.&lt;/p>
&lt;p>Since operation of this device is unlicensed, I figured I&amp;rsquo;d start looking in
the ISM band. The most common band used that I&amp;rsquo;ve seen is the band starting
at &lt;code>433.05MHz&lt;/code> up to &lt;code>434.79MHz&lt;/code>. I fired up my trusty waterfall tuned to a
center frequency of &lt;code>433.92MHz&lt;/code> (since it&amp;rsquo;s right in the middle of the band, and
it let me see far enough up and down the band to spot the remote) and pressed
a few buttons. Imagine my surprise when I realize the operational frequency of
this device is &lt;code>433.920MHz&lt;/code>, exactly dead center. Weird, but lucky!&lt;/p>
&lt;p>&lt;img src="https://k3xec.com/imgs/christmas/waterfall.gif" alt="">&lt;/p>
&lt;p>After taking a capture, I started to look at understanding what the modulation
type of the signal was, and how I may go about demodulating it.
Using &lt;a href="https://github.com/miek/inspectrum">inspectrum&lt;/a>, I was able to clearly
see the signal in the capture, and it immediately stuck out to my eye to be
encoded using OOK / ASK.&lt;/p>
&lt;p>&lt;img src="https://k3xec.com/imgs/christmas/ook.png" alt="">&lt;/p>
&lt;p>Next, I started to measure the smallest pulse, and see if I could infer the
symbols per second, and try to decode it by hand. These types of signals are
generally pretty easy to decode by eye.&lt;/p>
&lt;p>&lt;a href="https://k3xec.com/imgs/christmas/inspectrum-christmas-lights-4-bit.png">&lt;img src="https://k3xec.com/imgs/christmas/inspectrum-christmas-lights-4-strip.png" alt="">&lt;/a>&lt;/p>
&lt;p>This wound up giving me symbol rate of 2.2 Ksym/s, which is a lot faster than I
expected. While I was working by hand, &lt;a href="https://github.com/tomberek/">Tom&lt;/a>
demodulated a few messages in Python, and noticed that if you grouped the bits
into groups of 4, you either had a &lt;code>1000&lt;/code> or a &lt;code>1110&lt;/code> &amp;ndash; which caused me to
realize this was encoded using something I saw documented elsewhere, where the
0 is a &amp;ldquo;short&amp;rdquo; pulse, and a 1 is a &amp;ldquo;long&amp;rdquo; pulse, not unlike morse code, but
where each symbol takes up a fixed length of time (monospace morse code?).
Working on that assumption, I changed my inspectrum symbol width, and
demodulated a few more by hand. This wound up demodulating nicely (and the
preamble / clock sync could be represented as repeating &lt;code>0&lt;/code>s, which is handy!)
and gave us a symbol rate of 612(ish) symbols per second &amp;ndash; a lot closer to
what I was expecting.&lt;/p>
&lt;p>&lt;a href="https://k3xec.com/imgs/christmas/inspectrum-christmas-lights.png">&lt;img src="https://k3xec.com/imgs/christmas/inspectrum-christmas-lights-strip.png" alt="">&lt;/a>&lt;/p>
&lt;p>If we take the code for &amp;lsquo;on&amp;rsquo; in the inspectrum capture above and demodulate
it by hand, we get &lt;code>0000000000110101100100010&lt;/code> (treat a short pulse as a 0, and
a long pulse as a 1). If you&amp;rsquo;re interested in following along at home, click on
the inspectrum image, and write down the bits you see, and compare it to what
I have!&lt;/p>
&lt;p>Right, so it looks like from what we can tell so far that the packet looks
something like this:&lt;/p>
&lt;div class="hz-abi">
&lt;div type="10 bits" class="hz-abi-green hz-abi-4b">preamble / sync&lt;/div>
&lt;div type="15-16 bits" class="hz-abi-green hz-abi-8b">stuff&lt;/div>
&lt;/div>
&lt;p>Next, I took a capture of all the button presses and demodulated them by hand,
and put them into a table to try and understand the format of the messages:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;td class="hz-1-3rd">Button&lt;/td>
&lt;td class="hz-2-3rd">Demod'd Bits&lt;/td>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>On&lt;/td>
&lt;td>0000000000110101100100010&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Off&lt;/td>
&lt;td>00000000001101011001010000&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Dim Up&lt;/td>
&lt;td>0000000000110101100110100&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Dim Down&lt;/td>
&lt;td>0000000000110101100100100&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Timer 1h&lt;/td>
&lt;td>0000000000110101100110010&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Timer 2h&lt;/td>
&lt;td>0000000000110101100100110&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Timer 4h&lt;/td>
&lt;td>0000000000110101100100000&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Dim 100%&lt;/td>
&lt;td>0000000000110101000101010&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Dim 75%&lt;/td>
&lt;td>00000000001101010001001100&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Dim 50%&lt;/td>
&lt;td>00000000001101010001001000&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Dim 25%&lt;/td>
&lt;td>0000000000110101000100000&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>Great! So, this is enough to attempt to control the tree with, I think &amp;ndash; so I
wrote a simple modulator. My approach was to use the fact that I can break down
a single symbol into 4 &amp;ldquo;sub-symbol&amp;rdquo; components &amp;ndash; which is to say, go back to
representing a &lt;code>1&lt;/code> as &lt;code>1110&lt;/code>, and a &lt;code>0&lt;/code> as &lt;code>1000&lt;/code>. This let me allocate IQ
space for the symbol, break the bit into 4 symbols, and if that symbol is 1,
write out values from a carrier wave (&lt;code>cos&lt;/code> in the &lt;code>real&lt;/code> values, and &lt;code>sin&lt;/code> in
the &lt;code>imaginary&lt;/code> values) to the buffer. Now that I can go from bits to IQ data,
I can transmit that IQ data using my PlutoSDR or HackRF and try and control my
tree. I gave it a try, and the tree blinked off!&lt;/p>
&lt;p>🎉🎊 Success! 🎊🎉&lt;/p>
&lt;p>But wait &amp;ndash; that&amp;rsquo;s not enough for me &amp;ndash; I know I can&amp;rsquo;t just demodulate bits and
try and replay the bits forever &amp;ndash; there&amp;rsquo;s stuff like addresses and keys and
stuff, and I want to get a second one of these working. Let&amp;rsquo;s take a look at
the bits to see if we spot anything fun &amp;amp; interesting.&lt;/p>
&lt;p>At first glance, a few things jumped out at me as being&amp;hellip; weird? First is
that the preamble is 10 bits long (fine, let&amp;rsquo;s move along - maybe it
just needs 8 in a row and there&amp;rsquo;s two to ensure clocks sync?). Next is that
the messages are not all the same length. I double (and triple!) checked
the messages, and it&amp;rsquo;s true, the messages are not all the same length. Adding
an extra bit at the end didn&amp;rsquo;t break anything, but I wonder if that&amp;rsquo;s just due
to the implementation rather than the protocol.&lt;/p>
&lt;p>But, good news, it looks like we have a stable prefix to the messages from the
remote &amp;ndash; must be my device&amp;rsquo;s address! The stable 6 bits that jump out right
away are &lt;code>110101&lt;/code>. Something seems weird, though, 6 bits is a bit awkward, even
for a bit limited embedded device. Why 6? But hey, wait, we had 10 bits in the
preamble, what if we have an 8 bit address &amp;ndash; meaning my device is &lt;code>00110101&lt;/code>,
and the preamble is 8 &lt;code>0&lt;/code> symbols! Those are numbers that someone working on
an 8 bit aligned platform would pick! To test this, I added a &lt;code>0&lt;/code> to the
preamble to see if the message starts at the first &lt;code>1&lt;/code>, or if it requires all
the bits to be fully decoded, and lo and behold, the tree did not turn on or
off. This would seem to me to confirm that the 0s are part of the address,
and I can assume we have two 8 bit aligned bytes in the prefix of the message.&lt;/p>
&lt;div class="hz-abi">
&lt;div type="byte" class="hz-abi-green hz-abi-4b">preamble / sync&lt;/div>
&lt;div type="byte" class="hz-abi-green hz-abi-4b">address&lt;/div>
&lt;div type="9-10 bits" class="hz-abi-green hz-abi-4b">stuff&lt;/div>
&lt;/div>
&lt;p>Now, when we go through the 9-10 bits of &amp;ldquo;stuff&amp;rdquo;, we see all sorts of weird
bits floating all over the place. The first 4 bits look like it&amp;rsquo;s either
&lt;code>1001&lt;/code> or &lt;code>0001&lt;/code>, but other than that, there&amp;rsquo;s a lot of chaos. This is where
things get really squishy. I needed more information to try and figure this out,
but no matter how many times I sent a command it was always the same bits (so,
no counters), and things feel very opaque still.&lt;/p>
&lt;p>The only way I was going to make any progress is to get another switch and see
how the messages from the remote change. Off to Amazon I went, and ordered
another switch from the same page, and eagerly waited its arrival.&lt;/p>
&lt;h2 id="switch-2">Switch #2&lt;/h2>
&lt;p>The second switch showed up, and I hurriedly unboxed the kit, put batteries
into the remote, and fired up my SDR to take a capture. After I captured the
first button (&amp;ldquo;Off&amp;rdquo;), my heart sunk as I saw my lights connected to
Switch #1 flicker off. Apparently the new switch and the old switch have the
same exact address. To be sure, I demodulated the messages as before, and
came out with the exact same bit pattern. This is a setback and letdown &amp;ndash; I
was hoping to independently control my switches, but it also means I got no
additional information about the address or button format.&lt;/p>
&lt;p>The upside to all of this, though, is that because the switches are controlled
by either remote, I only needed one remote, so why not pull it apart and see if
I can figure out what components it&amp;rsquo;s using to transmit, and find any
datasheets I can. The PCB was super simple, and I wound up finding a &amp;ldquo;WL116SC&amp;rdquo;
IC on the PCB.&lt;/p>
&lt;p>&lt;img src="https://k3xec.com/imgs/christmas/remote-teardown.png" alt="">&lt;/p>
&lt;p>After some googling, I found a single lone
&lt;a href="https://web.archive.org/web/20201023121143/https://datasheet.lcsc.com/szlcsc/2007311538_WL-WL116AC-1527_C708721.pdf">datasheet&lt;/a>,
entirely in Chinese. Thankfully, Google Translate seems to have worked well
enough on technical words, and I was able to put together at least a little bit
of understanding based on the documentation that was made available. I took a
few screenshots below - I put the google translated text above the hanzi. From
that sheet, we can see we got the basics of the &amp;ldquo;1&amp;rdquo; and &amp;ldquo;0&amp;rdquo; symbol encoding
right (I was halfway expecting the bits to be flipped), and a huge find by way
of a description of the bits in the message!&lt;/p>
&lt;p>&lt;img src="https://k3xec.com/imgs/christmas/wl116sc-pulse-width.png" alt="">&lt;/p>
&lt;p>It&amp;rsquo;s a bummer that we missed the clock sync / preamble pulse before the data
message, but that&amp;rsquo;s OK somehow. It also turns out that 8 or 10 bit series of of
&amp;ldquo;0&amp;quot;s wasn&amp;rsquo;t clock sync at all - it was part of the address! Since it also turns
out that all devices made by this manufacturer have the hardcoded address of
&lt;code>[]byte{0x00, 0x35}&lt;/code>, that means that the vast majority of bits sent are always
going to be the same for any button press on any remote made by this vendor.
Seems like a waste of bits to me, but hey, what do I know.&lt;/p>
&lt;p>Additionally, this also tells us the trailing zeros are not part of the data
encoding scheme, which is progress!&lt;/p>
&lt;div class="hz-abi">
&lt;div type="uint16" class="hz-abi-green hz-abi-8b">address&lt;/div>
&lt;div type="byte" class="hz-abi-green hz-abi-4b">keycode&lt;/div>
&lt;/div>
&lt;p>Now, working on the assumptions validated by the datasheet, here&amp;rsquo;s the updated
list of scancodes we&amp;rsquo;ve found:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;td class="hz-1-3rd">Button&lt;/td>
&lt;td class="hz-1-3rd">Scancode Bits&lt;/td>
&lt;td class="hz-1-3rd">Integer&lt;/td>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>On&lt;/td>
&lt;td>10010001&lt;/td>
&lt;td>145 / 0x91&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Off&lt;/td>
&lt;td>10010100&lt;/td>
&lt;td>148 / 0x94&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Dim Up&lt;/td>
&lt;td>10011010&lt;/td>
&lt;td>154 / 0x9A&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Dim Down&lt;/td>
&lt;td>10010010&lt;/td>
&lt;td>146 / 0x92&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Timer 1h&lt;/td>
&lt;td>10011001&lt;/td>
&lt;td>154 / 0x99&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Timer 2h&lt;/td>
&lt;td>10010011&lt;/td>
&lt;td>147 / 0x93&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Timer 4h&lt;/td>
&lt;td>10010000&lt;/td>
&lt;td>144 / 0x90&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Dim 100%&lt;/td>
&lt;td>00010101&lt;/td>
&lt;td>21 / 0x15&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Dim 75%&lt;/td>
&lt;td>00010011&lt;/td>
&lt;td>19 / 0x13&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Dim 50%&lt;/td>
&lt;td>00010010&lt;/td>
&lt;td>18 / 0x12&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Dim 25%&lt;/td>
&lt;td>00010000&lt;/td>
&lt;td>16 / 0x10&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>Interestingly, I think the &amp;ldquo;Dim&amp;rdquo; keys may have a confirmation that we have
a good demod &amp;ndash; the codes on the bottom are missing the most significant
bit, and when I look back at the scancode table in the datasheet, they make an
interesting pattern &amp;ndash; the bottom two rows, right and left side values match
up! If you take a look, Dim 100% is &amp;ldquo;S1&amp;rdquo;, Dim 75% is &amp;ldquo;S19&amp;rdquo;, Dim 50% is &amp;ldquo;S8&amp;rdquo;,
and Dim 25% is &amp;ldquo;S20&amp;rdquo;. Cool!&lt;/p>
&lt;p>Since none of the other codes line up, I am willing to bet the most significant
bit is a &amp;ldquo;Combo&amp;rdquo; indicator, and not part of the button (leaving 7 bits for the
keycode).&lt;/p>
&lt;p>&lt;img src="https://k3xec.com/imgs/christmas/wl116sc-scancode.png" alt="">&lt;/p>
&lt;p>And even more interestingly, one of our scancodes (&amp;ldquo;Off&amp;rdquo;, which is 0x94) shows up just
below this table, in the examples.&lt;/p>
&lt;p>&lt;img src="https://k3xec.com/imgs/christmas/wl116sc-math.png" alt="">&lt;/p>
&lt;p>Over all, I think this tells us we have the right bits to look at for
determining the scan code! Great news there!&lt;/p>
&lt;h2 id="back-to-the-modulation">Back to the modulation!&lt;/h2>
&lt;p>So, armed with this knowledge, I was able to refactor my code to match the
timings and understanding outlined by the datasheet and ensure things still work.
The switch itself has a high degree of tolerance, so being wildly off frequency
or a wildly wrong symbol rate may actually still work. It&amp;rsquo;s hard to know if
this is more or less correct, but matching documentation seems like a more stable
foundation if nothing else.&lt;/p>
&lt;p>This code has been really reliable, and tends to work just as well as the
remote from what I&amp;rsquo;ve been able to determine. I&amp;rsquo;ve been using incredibly low
power to avoid any interference, and it&amp;rsquo;s been very robust - a testament to the
engineering that went into the outlet hardware, even though it cost less than
of a lot of other switches! I have a lot of respect for the folks who built
this device - it&amp;rsquo;s incredibly simple, reliable and my guess is this thing will
keep working even in some fairly harsh RF environments.&lt;/p>
&lt;p>The only downside is the fact the manufacturer used the same address for all
their devices, rather than programming a unique address for each outlet and
remote when the underlying WL116SC chip supports it. I&amp;rsquo;m sure this was done to
avoid complexity in assembly (e.g. pairing the remote and outlet, and having to
keep those two items together during assembly), but it&amp;rsquo;s still a bummer. I took
apart the switch to see if I could dump an EEPROM and change the address in
ROM, but the entire thing was potted in waterproof epoxy, which is a very nice
feature if this was ever used outdoors. Not good news for tinkering, though!&lt;/p>
&lt;h2 id="unsolved-mysteries">Unsolved Mysteries&lt;/h2>
&lt;p>At this point, even though I understand the protocol enough to control the
device, it still feels like I hit a dead end in my understanding. I&amp;rsquo;m not able
to figure out how exactly the scancodes are implemented, and break them down
into more specific parts. They are stable and based on the physical wiring of
the remote, so I think I&amp;rsquo;m going to leave it a magic number. I have what I was
looking for, and these magic constants appear to be the right one to use, even
if I did understand how to create the codes itself.&lt;/p>
&lt;p>This does leave us with a few bits we never resolved, which I&amp;rsquo;ll memorialize
below just to be sure I don&amp;rsquo;t forget about them.&lt;/p>
&lt;p>&lt;strong>Question #1:&lt;/strong> According to the datasheet there should be a preamble. Why do
I not see one leading the first message?&lt;/p>
&lt;p>My hunch is that the trailing &amp;ldquo;0&amp;rdquo; at the end of the payload is actually just
the preamble for the next message (always rendering the first message
invalid?). This would let us claim there&amp;rsquo;s an engineering reason why we are
ignoring the weird bit, and also explain away something from the documentation.
It&amp;rsquo;s just weird that it wouldn&amp;rsquo;t be present on the first message.&lt;/p>
&lt;p>This theory is mostly confirmed by measuring the timing and comparing it to the
datasheet, but it&amp;rsquo;s not &lt;em>exactly&lt;/em> in line with the datasheet timings either
(specifically, it&amp;rsquo;s off by 200µs, which is kinda a lot for a system using 400µs
timings). I think I could go either way on the last &amp;ldquo;0&amp;rdquo; being the preamble for
the next message. It could be that the first message is technically invalid, or
it could also be that this was not implemented or actively disabled by the
vendor for this specific application / device. It&amp;rsquo;s really hard to know
without getting the source code for the WL116SC chip in this specific remote
or the source in the outlet itself.&lt;/p>
&lt;p>&lt;strong>Question #2:&lt;/strong> Why are some keycodes 8 bits and others 9 bits?&lt;/p>
&lt;p>I still have no idea why there sometimes 8 bits (for instance, &amp;ldquo;On&amp;rdquo;) and
other times there are 9 bits (for instance, &amp;ldquo;Off&amp;rdquo;) in the 8 bit keycode
field.&lt;/p>
&lt;p>I spent some time playing with the &amp;ldquo;trailing&amp;rdquo; zeros, when I try and send an
&amp;ldquo;Off&amp;rdquo; with the most significant 8 bits (without the least significant / last
9th bit, which is a &amp;ldquo;0&amp;rdquo;), it does not turn the tree off. If I send an &amp;ldquo;On&amp;rdquo; with
9 bits (an additional 0 after the least significant bit), it does work,
but both &amp;ldquo;On&amp;rdquo; and &amp;ldquo;Off&amp;rdquo; work when I send 10, 11 or 12 bits padded with trailing
zeros. I suspect my outlet will ignore data after the switch is &amp;ldquo;done&amp;rdquo; reading
bits regardless of trailing zeros. The docs tell me there should only be 8 bits,
but it won&amp;rsquo;t work unless I send 9 bits for some commands. There&amp;rsquo;s something
fishy going on here, and the datasheet isn&amp;rsquo;t exactly right either way.&lt;/p>
&lt;p>&lt;strong>Question #3:&lt;/strong> How in the heck do those scancodes work?&lt;/p>
&lt;p>This one drove me &lt;em>nuts&lt;/em>. I&amp;rsquo;ve spent countless hours on trying to figure this
out, including emailing the company that makes the WL116SC (they&amp;rsquo;re really
nice!), and even though they were super kind and generous with documentation
and example source, I&amp;rsquo;m still having a hard time lining up their documentation
and examples with what I see from my remote. I think the manufacturer of my
remote and switch has modified the protocol enough to where there&amp;rsquo;s actually
something different going on here. Bummer.&lt;/p>
&lt;p>I wound up in my place of last resort &amp;ndash; asking friends over Signal to try and
see if they could find a pattern, as well as making
multiple please to the twittersphere, to no avail (but thank you to
&lt;a href="https://phasor.dev/">Ben Hilburn&lt;/a>,
&lt;a href="https://twitter.com/devnulling">devnulling&lt;/a>,
&lt;a href="https://activelow.net/">Andreas Bombe&lt;/a> and &lt;a href="https://twitter.com/larme">Larme&lt;/a>
for your repiles, help and advice!)&lt;/p>
&lt;p>I still don&amp;rsquo;t understand how they assemble the scan code &amp;ndash; for instance,
if you merely add, you won&amp;rsquo;t know if a key press of &lt;code>0x05&lt;/code> is &lt;code>0x03&lt;/code> + &lt;code>0x02&lt;/code>
or if it&amp;rsquo;s &lt;code>0x01&lt;/code> + &lt;code>0x04&lt;/code>. On the other hand, treating it as two 4-bit
integers won&amp;rsquo;t work for &lt;code>0x10&lt;/code> to &lt;code>0x15&lt;/code> (since they need 5 bits to
represent). It&amp;rsquo;s also likely the most significant bit is a combo indicator,
which only leaves 7 bits for the actual keypress data. Stuffing 10 bits of data
into 7 bits is likely resulting in some really intricate bit work.
On a last ditch whim, I tried to XOR the math into working, but some initial
brute forcing to make the math work given the provided examples did not result
in anything. It could be a bitpacked field that I don&amp;rsquo;t understand, but I don&amp;rsquo;t
think I can make progress on that without inside knowledge and much more work.&lt;/p>
&lt;p>Here&amp;rsquo;s the table containing the numbers I was working off of:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;td class="hz-1-3rd">Keys&lt;/td>
&lt;td class="hz-1-3rd">Key Codes&lt;/td>
&lt;td class="hz-1-3rd">Scancode&lt;/td>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>S3 + S9&lt;/td>
&lt;td>0x01 + 0x03&lt;/td>
&lt;td>0x96&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>S6 + S12&lt;/td>
&lt;td>0x07 + 0x09&lt;/td>
&lt;td>0x94&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>S22 + S10&lt;/td>
&lt;td>0x0D + 0x0F&lt;/td>
&lt;td>0x3F&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>If anyone has thoughts on how these codes work, I&amp;rsquo;d love to hear about it! Send
me an email or a tweet or something - I&amp;rsquo;m a bit stumped.&lt;/p>
&lt;p>There&amp;rsquo;s some trick here that is being used to encode the combo key in a way
that is decodeable. If it&amp;rsquo;s actually not decodeable (which is a real
possibility!), this may act as a unique button combo &amp;ldquo;hash&amp;rdquo; which allows the
receiver to not actually determine which keys are pressed, but have a unique
&amp;ldquo;button&amp;rdquo; that gets sent when a combo is used. I&amp;rsquo;m not sure I know enough to
have a theory as to which it may be.&lt;/p></description></item><item><title>Overview of the E4000 RTL SDR Tuner's IF stage 🎚️</title><link>https://k3xec.com/e4k/</link><pubDate>Tue, 03 Nov 2020 09:47:48 -0500</pubDate><guid>https://k3xec.com/e4k/</guid><description>&lt;p>This post is all about the E4000 (e4k) RTL-SDR Tuner, commonly found in the
Nooelec RTL-SDR. It&amp;rsquo;s one of my favorite RTL-SDR tuners, but it can be
incredibly frustrating to work with if it&amp;rsquo;s not left on
&lt;abbr title="Automatic Gain Control">AGC&lt;/abbr>.&lt;/p>
&lt;div class="hz-alert-warning">
This documentation is a work in progress, and a result of
source code spelunking or reverse engineering. It may contain
errors or outright lies. The names may not match the original
name, but it's been documented on a best-effort basis to help
future engineering efforts.
&lt;/div>
&lt;p>Specifically, this post covers a quirk of e4k rtlsdr dongles,
the addition of an IF gain stage.&lt;/p>
&lt;h2 id="what-is-the-if-stage">What is the IF stage?&lt;/h2>
&lt;p>The IF (or intermediate frequency) stage is where the input signal has been
shifted to a common frequency. This allows internal components to be designed
to work at one frequency (such as a single oscillator that works on a specific
frequency), and have a single component that takes any frequency and shifts it
to the common frequency before processing that signal. Transceivers that convert
signals to an IF for processing are sometimes called &amp;ldquo;Superheterodyne&amp;rdquo;
transceivers.&lt;/p>
&lt;h2 id="how-does-gain-work-on-the-e4k-rtl-sdr">How does gain work on the e4k rtl-sdr?&lt;/h2>
&lt;p>e4k based rtl-sdr devices have two gain stages, the first is the tuner gain stage,
which can be set using &lt;code>rtlsdr_set_tuner_gain&lt;/code>. Valid gain values
for the connected
rtl-sdr device can be queried via &lt;code>rtlsdr_get_tuner_gains&lt;/code>.
The second gain stage is the IF gain, which performs amplification on the
signal after it&amp;rsquo;s been converted to a single intermediate frequency.
Unfortunately, setting and creating value IF gain configurations is not quite
as easy as working with tuner gain. There&amp;rsquo;s no way to get the supported
if_tuner_gains, and confusingly, &lt;code>rtlsdr_set_tuner_if_gain&lt;/code> takes an
extra argument — stage!&lt;/p>
&lt;p>Without a bit of deeper knowledge about the e4k, It&amp;rsquo;s not super clear what
stage should be set to, nor what gain range or gain values are supported,
and documentation on this is very lacking if you&amp;rsquo;re searching for RTL-SDR
documentation. Don&amp;rsquo;t panic!&lt;/p>
&lt;p>Internally, the IF Gain on the e4k is made up of 6 stages, each of which can
be set to a specific set of values, but in practice (and as a user) you generally
set all 6 gains together.&lt;/p>
&lt;p>Each stage has a set number of gain values that are supported (which differ
per-stage), and can be a bit confusing to understand at first glace.&lt;/p>
&lt;table>
&lt;tbody>
&lt;tr>
&lt;td>Stage 1&lt;/td>&lt;td>-3dB&lt;/td>&lt;td>6db&lt;/td>&lt;td>&lt;/td>&lt;td>&lt;/td>&lt;td>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Stage 2&lt;/td>&lt;td>0dB&lt;/td>&lt;td>3dB&lt;/td>&lt;td>6dB&lt;/td>&lt;td>9dB&lt;/td>&lt;td>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Stage 3&lt;/td>&lt;td>0dB&lt;/td>&lt;td>3dB&lt;/td>&lt;td>6dB&lt;/td>&lt;td>9dB&lt;/td>&lt;td>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Stage 4&lt;/td>&lt;td>0dB&lt;/td>&lt;td>1db&lt;/td>&lt;td>2dB&lt;/td>&lt;td>&lt;/td>&lt;td>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Stage 5&lt;/td>&lt;td>3dB&lt;/td>&lt;td>6db&lt;/td>&lt;td>9dB&lt;/td>&lt;td>12dB&lt;/td>&lt;td>15dB&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Stage 6&lt;/td>&lt;td>3dB&lt;/td>&lt;td>6db&lt;/td>&lt;td>9dB&lt;/td>&lt;td>12dB&lt;/td>&lt;td>15dB&lt;/td>
&lt;/tr>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>Each of these gain stages can be added together to determine the total
gain of the IF stage. As an example, if we used the 0th values of the table
above, the final gain would be -3 + 0 + 0 + 0 + 3 + 3, giving us
a final gain of 3dB. You can set any valid values for each gain stage, and
it should provide you the right output gain amount.&lt;/p>
&lt;p>However, the e4k datasheet provides two tables, one optimized for linearity,
and one optimized for sensitivity. I&amp;rsquo;ve converted those tables into Go for use in my
SDR library. You can find the &lt;a href="">Sensitivity Table&lt;/a> and
&lt;a href="">Linerarity Table&lt;/a> in the bottom of this post.&lt;/p>
&lt;p>The reason why those tables are different (and have different performance
profiles) is that although the total gain is simply additive as shown above,
doing large gains at the early stage results in a different signal than doing large
gains at the final stages. Neither are wrong - but if you&amp;rsquo;re doing something like
&lt;abbr title="Frequency Modulation">FM&lt;/abbr>,
&lt;abbr title="Frequency Shift Keying">FSK&lt;/abbr>, or
&lt;abbr title="Orthogonal frequency division multiplexing">OFDM&lt;/abbr>,
linerarity matters a lot less than if you&amp;rsquo;re processing
&lt;abbr title="amplitude modulation">AM&lt;/abbr>.
Neither is a more correct configuration, but there are trade-offs, so do make a
conscious decision as to which table you use!&lt;/p>
&lt;h2 id="do-rlt-sdr-tuners-other-than-the-e4k-have-an-if-gain-control">Do RLT-SDR tuners other than the e4k have an IF gain control?&lt;/h2>
&lt;p>Not that I&amp;rsquo;m aware of! This scheme is only supported with the E4000 as far as
I understand. Other SDRs (like the HackRF) do have IF gain controls, but that&amp;rsquo;s
out of scope for this post.&lt;/p>
&lt;h2 id="tables">Tables&lt;/h2>
&lt;p>The following are two tables that have been transcribed from the elonics
e4000 datasheet into Go. If you&amp;rsquo;re using another language, you may need
to translate these values to a different format.&lt;/p>
&lt;p>Each table stores values in the same way RTL-SDR Tuner gains are stored,
which is in tenths of a DB. When you see -30, that&amp;rsquo;s really -3dB. Be
careful when computing total gain!&lt;/p>
&lt;p>The gain values in the comment are a bit confusing (even to me now) - they&amp;rsquo;re
taken from the E4000 documentation directly, but they don&amp;rsquo;t align with the
provided stage values. Here, the -3 + 3 + 3 is computed as 6dB,
which doesn&amp;rsquo;t match up the table values. I&amp;rsquo;ll update this post if I ever figure
out why the -3 dB attenuation isn&amp;rsquo;t being factored in. In my own code,
I&amp;rsquo;m using the values that I compute when adding values, in direct
contradiction of the datasheet. It&amp;rsquo;s likely wrong, but it&amp;rsquo;s not a large difference
for now.&lt;/p>
&lt;h3 id="sensitivity-table">Sensitivity Table&lt;/h3>&lt;/h3>
&lt;pre>
senIFGains = []Stages{
Stages{-30, 00, 00, 00, 30, 30}, // 6 dB gain
Stages{-30, 00, 00, 10, 30, 30}, // 7 dB gain
Stages{-30, 00, 00, 20, 30, 30}, // 8 dB gain
Stages{-30, 30, 00, 00, 30, 30}, // 9 dB gain
Stages{-30, 30, 00, 10, 30, 30}, // 10 dB gain
Stages{-30, 30, 00, 20, 30, 30}, // 11 dB gain
Stages{-30, 60, 00, 00, 30, 30}, // 12 dB gain
Stages{-30, 60, 00, 10, 30, 30}, // 13 dB gain
Stages{-30, 60, 00, 20, 30, 30}, // 14 dB gain
Stages{60, 00, 00, 00, 30, 30}, // 15 dB gain
Stages{60, 00, 00, 10, 30, 30}, // 16 dB gain
Stages{60, 00, 00, 20, 30, 30}, // 17 dB gain
Stages{60, 30, 00, 00, 30, 30}, // 18 dB gain
Stages{60, 30, 00, 10, 30, 30}, // 19 dB gain
Stages{60, 30, 00, 20, 30, 30}, // 20 dB gain
Stages{60, 60, 00, 00, 30, 30}, // 21 dB gain
Stages{60, 60, 00, 10, 30, 30}, // 22 dB gain
Stages{60, 60, 00, 20, 30, 30}, // 23 dB gain
Stages{60, 90, 00, 00, 30, 30}, // 24 dB gain
Stages{60, 90, 00, 10, 30, 30}, // 25 dB gain
Stages{60, 90, 00, 20, 30, 30}, // 26 dB gain
Stages{60, 90, 30, 00, 30, 30}, // 27 dB gain
Stages{60, 90, 30, 10, 30, 30}, // 28 dB gain
Stages{60, 90, 30, 20, 30, 30}, // 29 dB gain
Stages{60, 90, 60, 00, 30, 30}, // 30 dB gain
Stages{60, 90, 60, 10, 30, 30}, // 31 dB gain
Stages{60, 90, 60, 20, 30, 30}, // 32 dB gain
Stages{60, 90, 90, 00, 30, 30}, // 33 dB gain
Stages{60, 90, 90, 10, 30, 30}, // 34 dB gain
Stages{60, 90, 90, 20, 30, 30}, // 35 dB gain
Stages{60, 90, 90, 00, 60, 30}, // 36 dB gain
Stages{60, 90, 90, 10, 60, 30}, // 37 dB gain
Stages{60, 90, 90, 20, 60, 30}, // 38 dB gain
Stages{60, 90, 90, 00, 90, 30}, // 39 dB gain
Stages{60, 90, 90, 10, 90, 30}, // 40 dB gain
Stages{60, 90, 90, 20, 90, 30}, // 41 dB gain
Stages{60, 90, 90, 00, 120, 30}, // 42 dB gain
Stages{60, 90, 90, 10, 120, 30}, // 43 dB gain
Stages{60, 90, 90, 20, 120, 30}, // 44 dB gain
Stages{60, 90, 90, 00, 150, 30}, // 45 dB gain
Stages{60, 90, 90, 10, 150, 30}, // 46 dB gain
Stages{60, 90, 90, 20, 150, 30}, // 47 dB gain
Stages{60, 90, 90, 00, 150, 60}, // 48 dB gain
Stages{60, 90, 90, 10, 150, 60}, // 49 dB gain
Stages{60, 90, 90, 20, 150, 60}, // 50 dB gain
Stages{60, 90, 90, 00, 150, 90}, // 51 dB gain
Stages{60, 90, 90, 10, 150, 90}, // 52 dB gain
Stages{60, 90, 90, 20, 150, 90}, // 53 dB gain
Stages{60, 90, 90, 00, 150, 120}, // 54 dB gain
Stages{60, 90, 90, 10, 150, 120}, // 55 dB gain
Stages{60, 90, 90, 20, 150, 120}, // 56 dB gain
Stages{60, 90, 90, 00, 150, 150}, // 57 dB gain
Stages{60, 90, 90, 10, 150, 150}, // 58 dB gain
Stages{60, 90, 90, 20, 150, 150}, // 59 dB gain
Stages{60, 90, 90, 30, 150, 150}, // 60 dB gain
}
&lt;/pre>
&lt;h3 id="linerarity-table">Linerarity Table&lt;/h3>
&lt;pre>
linIFGains = []Stages{
Stages{-30, 00, 00, 00, 30, 30}, // 6 dB gain
Stages{-30, 00, 00, 10, 30, 30}, // 7 dB gain
Stages{-30, 00, 00, 20, 30, 30}, // 8 dB gain
Stages{-30, 00, 00, 00, 30, 60}, // 9 dB gain
Stages{-30, 00, 00, 10, 30, 60}, // 10 dB gain
Stages{-30, 00, 00, 20, 30, 60}, // 11 dB gain
Stages{-30, 00, 00, 00, 30, 90}, // 12 dB gain
Stages{-30, 00, 00, 10, 30, 90}, // 13 dB gain
Stages{-30, 00, 00, 20, 30, 90}, // 14 dB gain
Stages{-30, 00, 00, 00, 30, 120}, // 15 dB gain
Stages{-30, 00, 00, 10, 30, 120}, // 16 dB gain
Stages{-30, 00, 00, 20, 30, 120}, // 17 dB gain
Stages{-30, 00, 00, 00, 30, 150}, // 18 dB gain
Stages{-30, 00, 00, 10, 30, 150}, // 19 dB gain
Stages{-30, 00, 00, 20, 30, 150}, // 20 dB gain
Stages{-30, 00, 00, 00, 60, 150}, // 21 dB gain
Stages{-30, 00, 00, 10, 60, 150}, // 22 dB gain
Stages{-30, 00, 00, 20, 60, 150}, // 23 dB gain
Stages{-30, 00, 00, 00, 90, 150}, // 24 dB gain
Stages{-30, 00, 00, 10, 90, 150}, // 25 dB gain
Stages{-30, 00, 00, 20, 90, 150}, // 26 dB gain
Stages{-30, 00, 00, 00, 120, 150}, // 27 dB gain
Stages{-30, 00, 00, 10, 120, 150}, // 28 dB gain
Stages{-30, 00, 00, 20, 120, 150}, // 29 dB gain
Stages{-30, 00, 00, 00, 150, 150}, // 30 dB gain
Stages{-30, 00, 00, 10, 150, 150}, // 31 dB gain
Stages{-30, 00, 00, 20, 150, 150}, // 32 dB gain
Stages{-30, 00, 30, 00, 150, 150}, // 33 dB gain
Stages{-30, 00, 30, 10, 150, 150}, // 34 dB gain
Stages{-30, 00, 30, 20, 150, 150}, // 35 dB gain
Stages{-30, 00, 60, 00, 150, 150}, // 36 dB gain
Stages{-30, 00, 60, 10, 150, 150}, // 37 dB gain
Stages{-30, 00, 60, 20, 150, 150}, // 38 dB gain
Stages{-30, 00, 90, 00, 150, 150}, // 39 dB gain
Stages{-30, 00, 90, 10, 150, 150}, // 40 dB gain
Stages{-30, 00, 90, 20, 150, 150}, // 41 dB gain
Stages{-30, 30, 90, 00, 150, 150}, // 42 dB gain
Stages{-30, 30, 90, 10, 150, 150}, // 43 dB gain
Stages{-30, 30, 90, 20, 150, 150}, // 44 dB gain
Stages{-30, 60, 90, 00, 150, 150}, // 45 dB gain
Stages{-30, 60, 90, 10, 150, 150}, // 46 dB gain
Stages{-30, 60, 90, 20, 150, 150}, // 47 dB gain
Stages{60, 00, 90, 00, 150, 150}, // 48 dB gain
Stages{60, 00, 90, 10, 150, 150}, // 49 dB gain
Stages{60, 00, 90, 20, 150, 150}, // 50 dB gain
Stages{60, 30, 90, 00, 150, 150}, // 51 dB gain
Stages{60, 30, 90, 10, 150, 150}, // 52 dB gain
Stages{60, 30, 90, 20, 150, 150}, // 53 dB gain
Stages{60, 60, 90, 00, 150, 150}, // 54 dB gain
Stages{60, 60, 90, 10, 150, 150}, // 55 dB gain
Stages{60, 60, 90, 20, 150, 150}, // 56 dB gain
Stages{60, 90, 90, 00, 150, 150}, // 57 dB gain
Stages{60, 90, 90, 10, 150, 150}, // 58 dB gain
Stages{60, 90, 90, 20, 150, 150}, // 59 dB gain
Stages{60, 90, 90, 30, 150, 150}, // 60 dB gain
}
&lt;/pre></description></item><item><title>Overview of the RFCAP format 📸</title><link>https://k3xec.com/rfcap/</link><pubDate>Tue, 03 Nov 2020 09:47:48 -0500</pubDate><guid>https://k3xec.com/rfcap/</guid><description>&lt;p>rfcap is a file format with extremely small ambitions. rfcap files
contain a fixed size header, and then a stream of raw IQ data. The
rfcap header contains information about the IQ format type, and
capture metadata. The header is aligned to a 128 bit boundary, so
most iq formats can choose to ignore the header and throw out the
first window, meaning existing tools like &lt;code>gqrx&lt;/code> can read a subset
of rfcap files in the right IQ sample format.&lt;/p>
&lt;div class="hz-alert-ok">
This documentation is of a stable file format. Changes to this
spec will result in a non-breaking and careful change, or a
major version change. This format is safe to rely on.
&lt;/div>
&lt;p>The biggest advantage of the rfcap scheme is that IQ data can be
piped around without additional sample information such as
IQ format, or sample rate, and the metadata remains attached to
the stream.&lt;/p>
&lt;h2 id="reference-implementation">Reference implementation&lt;/h2>
&lt;p>The &lt;code>hz.tools/rfcap&lt;/code> package contains the reference
implementation of the rfcap spec, and will be maintained as the
spec evolves. Go programmers are strongly advised to use this
package as a dependency for reading and writing SDR captures
in Go.&lt;/p>
&lt;h2 id="format-description">Format Description&lt;/h2>
&lt;p>Each rfcap file is comprised of a single 48 byte header, followed
by a stream of IQ data, as described by the header.&lt;/p>
&lt;div class="hz-abi">
&lt;div type="Header (48 bytes)" class="hz-abi-yellow hz-abi-4b">
&lt;a href="">Header&lt;/a>
&lt;/div>
&lt;div type="[]IQ" class="hz-abi-yellow hz-abi-Nb">
Samples
&lt;/div>
&lt;/div>
&lt;p>Implementations are advised to store the header, in case samples
need to be written to disk, or piped to another application
outside of the current process.&lt;/p>
&lt;h3 id="header">Header&lt;/h3>
&lt;p>The Header is split up into fields containing metadata describing
the format and rate of the IQ samples to follow. At minimum,
implementations must be able to understand the
&lt;code>SampleRate&lt;/code> field, the &lt;code>SampleFormat&lt;/code>
field, and if not only using uint8, the &lt;code>Endianness&lt;/code>
field.&lt;/p>
&lt;p>The header itself is &lt;em>always&lt;/em> encoded
&lt;span class="hz-highlight">little endian&lt;/span>. This may make things a little
confusing when operating over the network, since you may be expecting network
order &amp;ndash; but it does make it significantly easier to consume on little endian
systems (which make up most of the target platforms), and makes things a little
easier when the encoded data is also little endian floating point numbers (which
is also usually the case on little endian systems).&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;td class="hz-1-6th">Field Name&lt;/td>
&lt;td class="hz-1-6th">Type&lt;/td>
&lt;td class="hz-2-3rd">Description&lt;/td>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td class="hz-1-6th">Magic&lt;/td>
&lt;td class="hz-1-6th">[6]byte&lt;/td>
&lt;td class="hz-2-3rd">Currently always `RFCAP1` for rfcap v1&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td class="hz-1-6th">Capture Time&lt;/td>
&lt;td class="hz-1-6th">int64&lt;/td>
&lt;td class="hz-2-3rd">
Number of nanoseconds since the Unix Epoch. Divide by
`1e+9` to get a Unix Epoch in seconds, and
perform a modulus to get the nanoseconds.
&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td class="hz-1-6th">Center Frequency&lt;/td>
&lt;td class="hz-1-6th">float64&lt;/td>
&lt;td class="hz-2-3rd">
Center Frequency of the capture, in Hz, as a floating
point 64 bit number.
&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td class="hz-1-6th">Sample Rate&lt;/td>
&lt;td class="hz-1-6th">uint32&lt;/td>
&lt;td class="hz-2-3rd">
Number of IQ samples per second this capture was taken at.
Each IQ sample is comprised of the real and imaginary
sample.
&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td class="hz-1-6th">Sample Format&lt;/td>
&lt;td class="hz-1-6th">uint8&lt;/td>
&lt;td class="hz-2-3rd">
Sample Format defined by the
`hz.tools/sdr.SampleFormat` enum.&lt;br />
&lt;br />
&lt;ul>
&lt;li>1: Complex64 (interleaved 32 bit floats)&lt;/li>
&lt;li>2: uint8 (interleaved 8 bit uints)&lt;/li>
&lt;li>3: int16 (interleaved 16 bit ints)&lt;/li>
&lt;li>4: int8 (interleaved 8 bit ints)&lt;/li>
&lt;/ul>
&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td class="hz-1-6th">Endianness&lt;/td>
&lt;td class="hz-1-6th">uint8&lt;/td>
&lt;td class="hz-2-3rd">
In order to retain compatibility with an earlier rfcap
version, Little Endian files are denoted with a 0, rather
than a 1.&lt;br />
&lt;br />
&lt;ul>
&lt;li>0: Little Endian&lt;/li>
&lt;li>1: Big Endian&lt;/li>
&lt;/ul>
&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td class="hz-1-6th">Reserved&lt;/td>
&lt;td class="hz-1-6th">[20]byte&lt;/td>
&lt;td class="hz-2-3rd">
Currently unused. Implementations must not rely on
any information in this range, and when writing headers,
the data in these bytes must be all zeros.
&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table></description></item><item><title>Overview of the RTL TCP Protocol 🔊</title><link>https://k3xec.com/rtl-tcp/</link><pubDate>Tue, 03 Nov 2020 09:47:48 -0500</pubDate><guid>https://k3xec.com/rtl-tcp/</guid><description>&lt;p>The rtl_tcp program will allow a client to remotely receive
iq samples from the rtl sdr over a tcp connection. This document
describes the mechanism by which the client and server communicate.&lt;/p>
&lt;div class="hz-alert-warning">
This documentation is a work in progress, and a result of
source code spelunking or reverse engineering. It may contain
errors or outright lies. The names may not match the original
name, but it's been documented on a best-effort basis to help
future engineering efforts.
&lt;/div>
&lt;p>All data sent to and from the server are encoded in
&lt;span class="hz-highlight">network byte order&lt;/span> (big endian). This
doesn&amp;rsquo;t matter for the actual &lt;i>iq&lt;/i> data, since it&amp;rsquo;s uint8 real/imag
interleaved, but it will matter for the header.&lt;/p>
&lt;h2 id="tcp-client-stream">TCP Client Stream&lt;/h2>
&lt;p>On connection to the rtl_tcp socket, the server will begin to stream
IQ samples to the client. The first 12 bytes are part of the
DongleInfo struct, but since it&amp;rsquo;s 2 byte aligned, clients can
choose to ignore the DongleInfo struct if they so desire.&lt;/p>
&lt;div class="hz-abi">
&lt;div type="DongleInfo" class="hz-abi-yellow hz-abi-4b">
&lt;a href="#dongle-info-struct">DongleInfo&lt;/a>
&lt;/div>
&lt;div type="[][2]uint8" class="hz-abi-yellow hz-abi-Nb">
IQ
&lt;/div>
&lt;/div>
&lt;p>Following the DongleInfo struct is a stream of infinite length
of interleaved IQ data, in the same format that the rtl-sdr library
will return data to the caller — a stream of interleaved
real and imaginary uint8 values, where 128 is 0, 255 is 1, and 0
is -1.&lt;/p>
&lt;h2 id="dongle-info-struct">Dongle Info struct&lt;/h2>
&lt;p>The first bytes sent to the client contain information about the
tuner at the remote end of the rtl_tcp connection. This allows
the client to determine ranges for gain values, or if there&amp;rsquo;s
an intermediate frequency gain stage.&lt;/p>
&lt;div class="hz-abi">
&lt;div type="[4]byte" class="hz-abi-blue-alt hz-abi-4b">Magic (RTL0)&lt;/div>
&lt;div type="uint32" class="hz-abi-blue-alt hz-abi-4b">Tuner Type&lt;/div>
&lt;div type="uint32" class="hz-abi-blue-alt hz-abi-4b">Tuner Gain Type&lt;/div>
&lt;/div>
&lt;p>These bytes are aligned to 2 byte boundaries (each value is a
4 byte uint32), so consumers need not care about this header if
it&amp;rsquo;s not using any information about the dongle.&lt;/p>
&lt;h2 id="request-struct">Request struct&lt;/h2>
&lt;p>The client may, during the course of the connection, send a
Request to the server in order to adjust the settings of the
remote device. Commonly, this is used to retune the device,
change the sample rate, or adjust gain settings.&lt;/p>
&lt;div class="hz-abi">
&lt;div type="byte" class="hz-abi-green hz-abi-1b">Cmd&lt;/div>
&lt;div type="uint32" class="hz-abi-green hz-abi-4b">Argument&lt;/div>
&lt;/div>
&lt;p>A full list of Commands, and the semantics of their Argument
is detailed on the table below.&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;td class="hz-1-6th">Command&lt;/td>
&lt;td class="hz-1-6th">Definition&lt;/td>
&lt;td class="hz-2-3rd">Argument&lt;/td>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>0x01&lt;/td>
&lt;td>Tune to a new center frequency&lt;/td>
&lt;td>Frequency, in Hz as a uint32&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>0x02&lt;/td>
&lt;td>Set the rate at which iq sample pairs are sent&lt;/td>
&lt;td>Number of samples (real and imaginary) per second as a uint32&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>0x03&lt;/td>
&lt;td>Set the tuner gain mode&lt;/td>
&lt;td>
&lt;ul>
&lt;li>0: automatic gain control&lt;/li>
&lt;li>1: manual gain control&lt;/li>
&lt;/ul>
&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>0x04&lt;/td>
&lt;td>Set the tuner gain level&lt;/td>
&lt;td>Gain, in tenths of a dB&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>0x05&lt;/td>
&lt;td>Set the tuner frequency correction&lt;/td>
&lt;td>Frequency correction, in PPM (parts per million)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>0x06&lt;/td>
&lt;td>Set the IF gain level&lt;/td>
&lt;td>
&lt;div class="hz-abi">
&lt;div type="u16" class="hz-abi-yellow hz-abi-2b">Stage&lt;/div>
&lt;div type="u16" class="hz-abi-yellow hz-abi-2b">Gain&lt;/div>
&lt;/div>
Two uint16 values, the least significant int16 (network byte order)
is the gain value in tenths of a dB, and the most significant
int16 is the gain stage.
&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>0x07&lt;/td>
&lt;td>Put the tuner into test mode&lt;/td>
&lt;td>
&lt;ul>
&lt;li>1: enable test mode&lt;/li>
&lt;li>0: disable test mode&lt;/li>
&lt;/ul>
&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>0x08&lt;/td>
&lt;td>
Set the automatic gain &lt;i>correction&lt;/i>, a software
step to correct the incoming signal, this is &lt;b>not&lt;/b>
automatic gain control on the hardware chip, that is
controlled by tuner gain mode.
&lt;/td>
&lt;td>
&lt;ul>
&lt;li>1: enable gain correction&lt;/li>
&lt;li>0: disable gain correction&lt;/li>
&lt;/ul>
&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>0x09&lt;/td>
&lt;td>Set direct sampling&lt;/td>
&lt;td>
&lt;ul>
&lt;li>0: disable direct sampling&lt;/li>
&lt;li>1: I-ADC input enabled&lt;/li>
&lt;li>2: Q-ADC input enabled&lt;/li>
&lt;/ul>
&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>0x0a&lt;/td>
&lt;td>Set offset tuning (TODO: EXPLAIN)&lt;/td>
&lt;td>
&lt;ul>
&lt;li>1: enable offset tuning&lt;/li>
&lt;li>0: disable offset tuning&lt;/li>
&lt;/ul>
&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>0x0d&lt;/td>
&lt;td>Set tuner gain by the tuner's gain index&lt;/td>
&lt;td>
Each tuner has a discrete set of supported gain values,
which are returned in a sorted order via
rtlsdr_get_tuner_gains. The argument here is
treated as an index into the tuner gains for the specific
tuner on the remote end, and will set the gain by index,
rather than in tenths of a dB.
&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>0x0e&lt;/td>
&lt;td>Set Bias Tee on GPIO pin 0&lt;/td>
&lt;td>
&lt;ul>
&lt;li>1: set pin high&lt;/li>
&lt;li>0: set pin low&lt;/li>
&lt;/ul>
&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table></description></item></channel></rss>