diff options
| author | Omar Rizwan <omar@omar.website> | 2022-11-01 02:31:38 +0000 |
|---|---|---|
| committer | Omar Rizwan <omar@omar.website> | 2022-11-01 02:31:38 +0000 |
| commit | 5360c625622d11608f4ffc76e87489134f4145fd (patch) | |
| tree | 5eb7bafd60a47d3d50ac69c350fa35ab3ec3be0b /virtual-programs | |
| parent | Add Ctrl-N shortcut while in program editor (diff) | |
| download | folk-5360c625622d11608f4ffc76e87489134f4145fd.tar.gz folk-5360c625622d11608f4ffc76e87489134f4145fd.zip | |
Start on radio stuff
Diffstat (limited to 'virtual-programs')
| -rw-r--r-- | virtual-programs/radio-stream.folk | 12 | ||||
| -rw-r--r-- | virtual-programs/radio-tick-count.folk | 17 | ||||
| -rw-r--r-- | virtual-programs/radio.folk | 603 |
3 files changed, 632 insertions, 0 deletions
diff --git a/virtual-programs/radio-stream.folk b/virtual-programs/radio-stream.folk new file mode 100644 index 00000000..b1731b13 --- /dev/null +++ b/virtual-programs/radio-stream.folk @@ -0,0 +1,12 @@ +Wish $this is highlighted red +Wish $this has filename "radio-stream.folk" + +When the ssh tunnel is /something/ { +When $::nodename has radio tick count /c/ { + Wish $this is labelled [time { + set sock [socket localhost 8099] + set data [read $sock] + close $sock + }] +} +}
\ No newline at end of file diff --git a/virtual-programs/radio-tick-count.folk b/virtual-programs/radio-tick-count.folk new file mode 100644 index 00000000..89e6c743 --- /dev/null +++ b/virtual-programs/radio-tick-count.folk @@ -0,0 +1,17 @@ +Wish $this is highlighted red +Wish $this has filename "radio-tick-count.folk" + +proc every {ms body} { + try $body + after $ms [list after idle [namespace code [info level 0]]] +} +set ::radioTickCount 0 +every 32 { + Retract $::nodename has radio tick count /c/ + Assert $::nodename has radio tick count [incr ::radioTickCount] + Step +} + +When $::nodename has radio tick count /c/ { + Wish $this is labelled $c +}
\ No newline at end of file diff --git a/virtual-programs/radio.folk b/virtual-programs/radio.folk new file mode 100644 index 00000000..27e08ac5 --- /dev/null +++ b/virtual-programs/radio.folk @@ -0,0 +1,603 @@ +Wish $this has filename "radio.folk" +Wish $this is highlighted blue + +set plutosdrC { + +// modified by Omar Rizwan based on: +// +// https://github.com/analogdevicesinc/libiio/blob/master/examples/ad9361-iiostream.c +// SPDX-License-Identifier: LGPL-2.1-or-later +/* + * libiio - AD9361 IIO streaming example + * + * Copyright (C) 2014 IABG mbH + * Author: Michael Feilen <feilen_at_iabg.de> + **/ + +#include <stdbool.h> +#include <stdint.h> +#include <string.h> +#include <signal.h> +#include <stdio.h> +#include <iio.h> +#include <math.h> + +#include <unistd.h> +#include <pthread.h> +#include <sys/socket.h> +#include <netinet/in.h> + +/* helper macros */ +#define KHZ(x) ((long long)(x*1000.0 + .5)) +#define MHZ(x) ((long long)(x*1000000.0 + .5)) +#define GHZ(x) ((long long)(x*1000000000.0 + .5)) + +#define IIO_ENSURE(expr) { \ + if (!(expr)) { \ + (void) fprintf(stderr, "assertion failed (%s:%d)\n", __FILE__, __LINE__); \ + (void) abort(); \ + } \ + } + +/* RX is input, TX is output */ +enum iodev { RX, TX }; + +/* common RX and TX streaming params */ +struct stream_cfg { + long long bw_hz; // Analog banwidth in Hz + long long fs_hz; // Baseband sample rate in Hz + long long lo_hz; // Local oscillator frequency in Hz + double gain; + const char* rfport; // Port name +}; + +/* static scratch mem for strings */ +static char tmpstr[64]; + +/* IIO structs required for streaming */ +static struct iio_context *ctx = NULL; +static struct iio_channel *rx0_i = NULL; +static struct iio_channel *rx0_q = NULL; +static struct iio_channel *tx0_i = NULL; +static struct iio_channel *tx0_q = NULL; +static struct iio_buffer *rxbuf = NULL; +static struct iio_buffer *txbuf = NULL; + +static bool stop; + +/* cleanup and exit */ +static void end() +{ + printf("* Destroying buffers\n"); + if (rxbuf) { iio_buffer_destroy(rxbuf); } + if (txbuf) { iio_buffer_destroy(txbuf); } + + printf("* Disabling streaming channels\n"); + if (rx0_i) { iio_channel_disable(rx0_i); } + if (rx0_q) { iio_channel_disable(rx0_q); } + if (tx0_i) { iio_channel_disable(tx0_i); } + if (tx0_q) { iio_channel_disable(tx0_q); } + + printf("* Destroying context\n"); + if (ctx) { iio_context_destroy(ctx); } + exit(0); +} + +static void handle_sig(int sig) +{ + printf("Waiting for process to finish... Got signal %d\n", sig); + stop = true; +} + +/* check return value of attr_write function */ +static void errchk(int v, const char* what) { + if (v < 0) { fprintf(stderr, "Error %d writing to channel \"%s\"\nvalue may not be supported.\n", v, what); end(); } +} + +/* write attribute: long long int */ +static void wr_ch_lli(struct iio_channel *chn, const char* what, long long val) +{ + errchk(iio_channel_attr_write_longlong(chn, what, val), what); +} + +/* write attribute: string */ +static void wr_ch_str(struct iio_channel *chn, const char* what, const char* str) +{ + errchk(iio_channel_attr_write(chn, what, str), what); +} + +/* helper function generating channel names */ +static char* get_ch_name(const char* type, int id) +{ + snprintf(tmpstr, sizeof(tmpstr), "%s%d", type, id); + return tmpstr; +} + +/* returns ad9361 phy device */ +static struct iio_device* get_ad9361_phy(void) +{ + struct iio_device *dev = iio_context_find_device(ctx, "ad9361-phy"); + IIO_ENSURE(dev && "No ad9361-phy found"); + return dev; +} + +/* finds AD9361 streaming IIO devices */ +static bool get_ad9361_stream_dev(enum iodev d, struct iio_device **dev) +{ + switch (d) { + case TX: *dev = iio_context_find_device(ctx, "cf-ad9361-dds-core-lpc"); return *dev != NULL; + case RX: *dev = iio_context_find_device(ctx, "cf-ad9361-lpc"); return *dev != NULL; + default: IIO_ENSURE(0); return false; + } +} + +/* finds AD9361 streaming IIO channels */ +static bool get_ad9361_stream_ch(enum iodev d, struct iio_device *dev, int chid, struct iio_channel **chn) +{ + *chn = iio_device_find_channel(dev, get_ch_name("voltage", chid), d == TX); + if (!*chn) + *chn = iio_device_find_channel(dev, get_ch_name("altvoltage", chid), d == TX); + return *chn != NULL; +} + +/* finds AD9361 phy IIO configuration channel with id chid */ +static bool get_phy_chan(enum iodev d, int chid, struct iio_channel **chn) +{ + switch (d) { + case RX: *chn = iio_device_find_channel(get_ad9361_phy(), get_ch_name("voltage", chid), false); return *chn != NULL; + case TX: *chn = iio_device_find_channel(get_ad9361_phy(), get_ch_name("voltage", chid), true); return *chn != NULL; + default: IIO_ENSURE(0); return false; + } +} + +/* finds AD9361 local oscillator IIO configuration channels */ +static bool get_lo_chan(enum iodev d, struct iio_channel **chn) +{ + switch (d) { + // LO chan is always output, i.e. true + case RX: *chn = iio_device_find_channel(get_ad9361_phy(), get_ch_name("altvoltage", 0), true); return *chn != NULL; + case TX: *chn = iio_device_find_channel(get_ad9361_phy(), get_ch_name("altvoltage", 1), true); return *chn != NULL; + default: IIO_ENSURE(0); return false; + } +} + +/* applies streaming configuration through IIO */ +bool cfg_ad9361_streaming_ch(struct stream_cfg *cfg, enum iodev type, int chid) +{ + struct iio_channel *chn = NULL; + + // Configure phy and lo channels + printf("* Acquiring AD9361 phy channel %d\n", chid); + if (!get_phy_chan(type, chid, &chn)) { return false; } + wr_ch_str(chn, "rf_port_select", cfg->rfport); + wr_ch_lli(chn, "rf_bandwidth", cfg->bw_hz); + wr_ch_lli(chn, "sampling_frequency", cfg->fs_hz); + // https://ez.analog.com/linux-software-drivers/f/q-a/85564/why-use-this-method-to-set-the-hardwaregain-cause-error + if (type == RX) { + printf("setting gain control mode to manual\n"); + wr_ch_str(chn, "gain_control_mode", "manual"); + wr_ch_lli(chn, "quadrature_tracking_en", 0); + wr_ch_lli(chn, "bb_dc_offset_tracking_en", 0); + wr_ch_lli(chn, "rf_dc_offset_tracking_en", 0); + } + printf("set gain %s %d\n", type == TX ? "TX" : "RX", iio_channel_attr_write_double(chn, "hardwaregain", cfg->gain)); + + // Configure LO channel + printf("* Acquiring AD9361 %s lo channel\n", type == TX ? "TX" : "RX"); + if (!get_lo_chan(type, &chn)) { return false; } + wr_ch_lli(chn, "frequency", cfg->lo_hz); + return true; +} + +// from https://github.com/zaphoxx/zuhf-rfid/blob/main/libraries/ZUHF_CC1101/ZUHF_CRC.h +// FIXME: rephrase +void crc5Append(uint8_t *q) +{ + uint8_t crc[] = {1,0,0,1,0}; + for(uint8_t i = 0; i < 17; i++) + { + uint8_t tmp[] = {0,0,0,0,0}; + tmp[4] = crc[3]; + if(crc[4] == 1) + { + if (q[i] == 1) + { + tmp[0] = 0; + tmp[1] = crc[0]; + tmp[2] = crc[1]; + tmp[3] = crc[2]; + } + else + { + tmp[0] = 1; + tmp[1] = crc[0]; + tmp[2] = crc[1]; + if(crc[2] == 1) + { + tmp[3] = 0; + } + else + { + tmp[3] = 1; + } + } + } + else + { + if (q[i] == 1) + { + tmp[0] = 1; + tmp[1] = crc[0]; + tmp[2] = crc[1]; + if(crc[2] == 1) + { + tmp[3] = 0; + } + else + { + tmp[3] = 1; + } + } + else + { + tmp[0] = 0; + tmp[1] = crc[0]; + tmp[2] = crc[1]; + tmp[3] = crc[2]; + } + } + memcpy(&crc,&tmp,5); + } + for (int i = 0; i < 5; i++){ + memcpy(q+17+i, &crc[4-i], 1); + } +} + +typedef uint8_t symbol_t; // logical symbols, not physical (not RF on/off) +#define Delimiter 2 +#define RTcal 3 +#define TRcal 4 + +typedef uint8_t pulse_t; // physical pulsewidth symbols (RF on/off) + +// Tari = 25 us +// PW = Tari/2 = 12.5 us + +int to_pulses(pulse_t* pulsebuf, symbol_t* syms, size_t symcount) { + int pulsecount = 0; + for (size_t i = 0; i < symcount; i++) { + switch (syms[i]) { + case 0: + pulsebuf[pulsecount++] = 1; + pulsebuf[pulsecount++] = 0; + break; + + case 1: + pulsebuf[pulsecount++] = 1; + pulsebuf[pulsecount++] = 1; + pulsebuf[pulsecount++] = 0; + break; + + case Delimiter: + pulsebuf[pulsecount++] = 0; // always 12.5 us + break; + + case RTcal: // (length of '0') + (length of '1') + pulsebuf[pulsecount++] = 1; + pulsebuf[pulsecount++] = 1; + pulsebuf[pulsecount++] = 1; + pulsebuf[pulsecount++] = 1; + pulsebuf[pulsecount++] = 0; + break; + + case TRcal: // max 3 * (length of RTcal) + // currently 10 pulses = 125 us + // => BLF = 64 KHz, Tpri = 15.6 us + pulsebuf[pulsecount++] = 1; + pulsebuf[pulsecount++] = 1; + pulsebuf[pulsecount++] = 1; + pulsebuf[pulsecount++] = 1; + pulsebuf[pulsecount++] = 1; + pulsebuf[pulsecount++] = 1; + pulsebuf[pulsecount++] = 1; + pulsebuf[pulsecount++] = 1; + pulsebuf[pulsecount++] = 1; + pulsebuf[pulsecount++] = 1; + pulsebuf[pulsecount++] = 0; + break; + } + } + + /* for (size_t i = 0; i < pulsecount; i++) { */ + /* printf("pulsebuf[%d] = %d\n", i, pulsebuf[i]); */ + /* } */ + return pulsecount; +} + +typedef struct { int16_t i; int16_t q; } iq_t; + +int cw(iq_t* iqbuf, size_t iqbufsize, long long samplerate, int us) { + // how many samples make 1 us ? + // how many samples make 12.5 us ? + // samplerate = how many samples make 1 second + // samplerate/1000000 = how many samples make 1 microsecond + int samplecount = us * samplerate/1000000; + int iqcount = 0; + for (int i = 0; i < samplecount; i++) { + iqbuf[iqcount++] = (iq_t) { 2047, 0 }; + } + return iqcount; +} + +int pulses_to_iq(iq_t* iqbuf, size_t iqbufsize, long long samplerate, pulse_t* pulses, size_t pulsecount) { + // how many samples make 1 us ? + // how many samples make 12.5 us ? + // samplerate = how many samples make 1 second + // samplerate/1000000 = how many samples make 1 microsecond + int samplesperpulse = 12.5 * samplerate/1000000; + /* printf("samplerate = %lld\nsamplesperpulse = %d\n", samplerate, samplesperpulse); */ + + int iqcount = 0; + for (size_t i = 0; i < pulsecount; i++) { + for (int j = 0; j < samplesperpulse; j++) { + iqbuf[iqcount++] = (iq_t) { pulses[i] * 2047, 0 }; + } + } + return iqcount; +} +int query(iq_t* iqbuf, size_t iqbufsize, long long samplerate) { + const symbol_t PREAMBLE[] = { + Delimiter, + 0, // '0' (Tari) + RTcal, + TRcal + }; + symbol_t QUERY[] = { + // Command (4) + 1, 0, 0, 0, + // DR (TRcal divide ratio) (1) + 0, // DR = 8 + // M (cycles per symbol) (2) + 0, 0, // FM0 encoding + // TRext (no pilot tone) (1) + 0, + // Sel (2) + 0, 0, + // Session (2) + 0, 0, + // Target (1) + 0, + // Q (4) + 0, 0, 0, 0, + // CRC (5) + 255, 255, 255, 255, 255 + }; + // find the crc + crc5Append(QUERY); + // concatenate the PREAMBLE+QUERY + symbol_t symbuf[sizeof(PREAMBLE) + sizeof(QUERY)]; + memcpy(symbuf, PREAMBLE, sizeof(PREAMBLE)); + memcpy(symbuf + sizeof(PREAMBLE), QUERY, sizeof(QUERY)); + + // compile symbols to pulses + pulse_t pulsebuf[sizeof(symbuf) * 10]; // multiply by pulse count of longest symbol + int pulsecount = to_pulses(pulsebuf, symbuf, sizeof(symbuf)); + + return pulses_to_iq(iqbuf, iqbufsize, samplerate, pulsebuf, pulsecount); +} + +struct stream_cfg rxcfg; + +// keep a global stream buffer +pthread_rwlock_t stream_rwlock; +iq_t *stream_buffer; +int stream_buffer_length; + +// spawn a pthread that spawns a tcp server that can be connected to +void *server_main(void *arg) { + (void)arg; + printf("server_main\n"); + + int fd = socket(AF_INET, SOCK_STREAM, 0); + if (fd == -1) { + perror("socket"); return NULL; + } + + int reuse = 1; + if (setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, (const char*)&reuse, sizeof(reuse)) < 0) + perror("setsockopt(SO_REUSEADDR) failed"); +#ifdef SO_REUSEPORT + if (setsockopt(fd, SOL_SOCKET, SO_REUSEPORT, (const char*)&reuse, sizeof(reuse)) < 0) + perror("setsockopt(SO_REUSEPORT) failed"); +#endif + + struct sockaddr_in addr; + addr.sin_family = AF_INET; + addr.sin_port = htons(8099); + addr.sin_addr.s_addr = htonl(INADDR_ANY); + if (bind(fd, (struct sockaddr *) &addr, sizeof(addr)) == -1) { + perror("bind"); return NULL; + } + listen(fd, 10); + + while (1) { + int client = accept(fd, NULL, NULL); + pthread_rwlock_rdlock(&stream_rwlock); + send(client, &rxcfg.lo_hz, sizeof(rxcfg.lo_hz), 0); + send(client, stream_buffer, sizeof(iq_t) * stream_buffer_length, 0); + pthread_rwlock_unlock(&stream_rwlock); + close(client); + } +} + + +/* simple configuration and streaming */ +/* usage: + * Default context, assuming local IIO devices, i.e., this script is run on ADALM-Pluto for example + $./a.out + * URI context, find out the uri by typing `iio_info -s` at the command line of the host PC + $./a.out usb:x.x.x +*/ +int main (int argc, char **argv) +{ + struct iio_device *tx; + struct iio_device *rx; + + // RX and TX sample counters + size_t nrx = 0; + size_t ntx = 0; + + // Stream configurations + struct stream_cfg txcfg; + + // Listen to ctrl+c and IIO_ENSURE + signal(SIGINT, handle_sig); + + // RX stream config + rxcfg.bw_hz = MHZ(2.5); // 2 MHz rf bandwidth + rxcfg.fs_hz = MHZ(2.5); // 2.5 MS/s rx sample rate + rxcfg.lo_hz = MHZ(902.75); + rxcfg.gain = 10; + rxcfg.rfport = "A_BALANCED"; // port A (select for rf freq.) + + // TX stream config + txcfg.bw_hz = MHZ(2.5); + txcfg.fs_hz = MHZ(2.5); + txcfg.lo_hz = MHZ(902.75); + txcfg.gain = -10; // assumes Nooelec amp + txcfg.rfport = "A"; // port A (select for rf freq.) + + // stream_buffer_length = 5000 * rxcfg.fs_hz/1000000; // record 5000 us + stream_buffer_length = 16384; + stream_buffer = calloc(stream_buffer_length, sizeof(iq_t)); + + pthread_rwlock_init(&stream_rwlock, NULL); + + pthread_t server; + pthread_create(&server, NULL, server_main, NULL); + + printf("* Acquiring IIO context\n"); + if (argc == 1) { + IIO_ENSURE((ctx = iio_create_default_context()) && "No context"); + } + else if (argc == 2) { + IIO_ENSURE((ctx = iio_create_context_from_uri(argv[1])) && "No context"); + } + IIO_ENSURE(iio_context_get_devices_count(ctx) > 0 && "No devices"); + + printf("* Acquiring AD9361 streaming devices\n"); + IIO_ENSURE(get_ad9361_stream_dev(TX, &tx) && "No tx dev found"); + IIO_ENSURE(get_ad9361_stream_dev(RX, &rx) && "No rx dev found"); + + printf("* Configuring AD9361 for streaming\n"); + IIO_ENSURE(cfg_ad9361_streaming_ch(&rxcfg, RX, 0) && "RX port 0 not found"); + IIO_ENSURE(cfg_ad9361_streaming_ch(&txcfg, TX, 0) && "TX port 0 not found"); + + printf("* Initializing AD9361 IIO streaming channels\n"); + IIO_ENSURE(get_ad9361_stream_ch(RX, rx, 0, &rx0_i) && "RX chan i not found"); + IIO_ENSURE(get_ad9361_stream_ch(RX, rx, 1, &rx0_q) && "RX chan q not found"); + IIO_ENSURE(get_ad9361_stream_ch(TX, tx, 0, &tx0_i) && "TX chan i not found"); + IIO_ENSURE(get_ad9361_stream_ch(TX, tx, 1, &tx0_q) && "TX chan q not found"); + + printf("* Enabling IIO streaming channels\n"); + iio_channel_enable(rx0_i); + iio_channel_enable(rx0_q); + iio_channel_enable(tx0_i); + iio_channel_enable(tx0_q); + + printf("* Creating non-cyclic IIO buffers\n"); + rxbuf = iio_device_create_buffer(rx, 16384, false); + if (!rxbuf) { + perror("Could not create RX buffer"); + end(); + } + txbuf = iio_device_create_buffer(tx, 16384, false); + if (!txbuf) { + perror("Could not create TX buffer"); + end(); + } + + printf("* Starting IO streaming (press CTRL+C to cancel)\n"); + while (!stop) { + ssize_t nbytes_rx, nbytes_tx; + char *p_dat, *p_end; + ptrdiff_t p_inc; + + // Schedule TX buffer + nbytes_tx = iio_buffer_push(txbuf); + if (nbytes_tx < 0) { printf("Error pushing buf %d\n", (int) nbytes_tx); end(); } + + // Refill RX buffer + nbytes_rx = iio_buffer_refill(rxbuf); + if (nbytes_rx < 0) { printf("Error refilling buf %d\n",(int) nbytes_rx); end(); } + + // READ: Get pointers to RX buf and read IQ from RX buf port 0 + { + p_inc = iio_buffer_step(rxbuf); + p_end = iio_buffer_end(rxbuf); + int streamidx = 0; + pthread_rwlock_wrlock(&stream_rwlock); + for (p_dat = (char *)iio_buffer_first(rxbuf, rx0_i); p_dat < p_end; p_dat += p_inc) { + const int16_t i = ((int16_t*)p_dat)[0]; // Real (I) + const int16_t q = ((int16_t*)p_dat)[1]; // Imag (Q) + + // Stream to buffer that gets sent out by the server thread + if (streamidx < stream_buffer_length) { + stream_buffer[streamidx++] = (iq_t) { i, q }; + } + } + pthread_rwlock_unlock(&stream_rwlock); + } + + // WRITE: Get pointers to TX buf and write IQ to TX buf port 0 + { + iq_t iqbuf[1000000]; + int iqcount = 0; + iqcount += cw(iqbuf + iqcount, sizeof(iqbuf)/sizeof(iq_t) - iqcount, txcfg.fs_hz, 1500); + iqcount += query(iqbuf + iqcount, sizeof(iqbuf)/sizeof(iq_t) - iqcount, txcfg.fs_hz); + // FIXME: how many microseconds should this followup CW actually be? + iqcount += cw(iqbuf + iqcount, sizeof(iqbuf)/sizeof(iq_t) - iqcount, txcfg.fs_hz, 16384 - iqcount); + + int iqidx = 0; + p_inc = iio_buffer_step(txbuf); + p_end = iio_buffer_end(txbuf); + for (p_dat = (char *)iio_buffer_first(txbuf, tx0_i); p_dat < p_end; p_dat += p_inc) { + int16_t i = 0; + int16_t q = 0; + if (iqidx < iqcount) { + i = iqbuf[iqidx].i; + q = iqbuf[iqidx].q; + iqidx++; + } + ((int16_t*)p_dat)[0] = i << 4; // Real (I) + ((int16_t*)p_dat)[1] = q << 4; // Imag (Q) + } + } + + // Sample counter increment and status output + nrx += nbytes_rx / iio_device_get_sample_size(rx); + ntx += nbytes_tx / iio_device_get_sample_size(tx); + /* printf("\tRX %8.2f MSmp, TX %8.2f MSmp\n", nrx/1e6, ntx/1e6); */ + } + + end(); + + return 0; +} +} + +set plutosdrCFile "[exec mktemp].c" +set fd [open $plutosdrCFile w]; puts $fd $plutosdrC; close $fd + +if {[catch {set cc [exec which arm-linux-gnueabihf-gcc]}]} { set cc [exec which arm-unknown-linux-gnueabihf-gcc] } + +exec -ignorestderr $cc -mfloat-abi=hard --sysroot=$::env(HOME)/pluto-0.30.sysroot -std=gnu99 -g -o ad9361-iiostream $plutosdrCFile -lpthread -liio -lm -Wall -Wextra + +catch {exec sshpass -p analog ssh root@folk-sdr-pluto-ib fuser -k 8099/tcp} +catch {pkill -9 $::ssh} +exec sshpass -p analog scp ad9361-iiostream root@folk-sdr-pluto-ib:/root +set ::ssh [exec sshpass -p analog ssh root@folk-sdr-pluto-ib -L 8099:localhost:8099 ./ad9361-iiostream >@stdout &] +Claim the ssh tunnel is $::ssh +Wish $this is labelled "Tunnel: $::ssh" + + |
