|
| 1 | +/** |
| 2 | + * Copyright 2025 Benjamin Vernoux <bvernoux@hydrasdr.com> |
| 3 | + * |
| 4 | + * @file hydrasdr_async_rx.c |
| 5 | + * @brief Cross-platform asynchronous streaming example for HydraSDR RFOne. |
| 6 | + * |
| 7 | + * @details |
| 8 | + * This application demonstrates how to: |
| 9 | + * 1. Open a HydraSDR device. |
| 10 | + * 2. Configure RF parameters (Frequency, Linearity Gain, Bias-T). |
| 11 | + * 3. Configure Data parameters (Sample Rate, Sample Type). |
| 12 | + * 4. Perform non-blocking (async) streaming to a file. |
| 13 | + * 5. Deduce sample buffer sizes dynamically in the callback. |
| 14 | + * 6. Monitor Real-time statistics (Time, Rate, MSPS, Drops). |
| 15 | + * |
| 16 | + * Usage: |
| 17 | + * ./hydrasdr_async_rx [-f freq_hz] [-s rate_sps] [-t sample_type] [-g gain] [-b bias_on_off] [-o filename] |
| 18 | + * |
| 19 | + * Defaults: |
| 20 | + * - Frequency: 100 MHz |
| 21 | + * - Sample Rate: 2.5 MSPS |
| 22 | + * - Gain: 10 (Linearity Profile) |
| 23 | + * - Bias Tee: Off |
| 24 | + * - Sample Type: INT16 IQ |
| 25 | + * |
| 26 | + */ |
| 27 | + |
| 28 | +#include <stdio.h> |
| 29 | +#include <stdlib.h> |
| 30 | +#include <string.h> |
| 31 | +#include <stdint.h> |
| 32 | +#include <signal.h> |
| 33 | +#include <errno.h> |
| 34 | +#include <time.h> |
| 35 | + |
| 36 | +/* Include the HydraSDR API definition */ |
| 37 | +#include "hydrasdr.h" |
| 38 | + |
| 39 | +#define HYDRASDR_ASYNC_RX_VERSION "1.0.0" |
| 40 | + |
| 41 | +// Cross-platform utilities |
| 42 | +#ifdef _WIN32 |
| 43 | + #include <windows.h> |
| 44 | + #define SLEEP_MS(ms) Sleep(ms) |
| 45 | + typedef volatile long sig_atomic_bool_t; |
| 46 | + |
| 47 | + static double get_time_sec(void) |
| 48 | + { |
| 49 | + LARGE_INTEGER t, f; |
| 50 | + QueryPerformanceCounter(&t); |
| 51 | + QueryPerformanceFrequency(&f); |
| 52 | + return (double)t.QuadPart / (double)f.QuadPart; |
| 53 | + } |
| 54 | +#else /* Linux / macOS */ |
| 55 | + #include <unistd.h> |
| 56 | + #include <pthread.h> |
| 57 | + #include <sys/time.h> |
| 58 | + |
| 59 | + #define SLEEP_MS(ms) usleep((ms) * 1000) |
| 60 | + typedef volatile sig_atomic_t sig_atomic_bool_t; |
| 61 | + |
| 62 | + static double get_time_sec(void) |
| 63 | + { |
| 64 | + struct timespec ts; |
| 65 | + clock_gettime(CLOCK_MONOTONIC, &ts); |
| 66 | + return ts.tv_sec + (ts.tv_nsec / 1e9); |
| 67 | + } |
| 68 | +#endif |
| 69 | + |
| 70 | +// Default settings |
| 71 | +#define DEFAULT_FREQ_HZ 100000000ULL /* 100 MHz */ |
| 72 | +#define DEFAULT_SAMPLERATE 2500000U /* 2.5 MSPS */ |
| 73 | +#define DEFAULT_SAMPLETYPE HYDRASDR_SAMPLE_INT16_IQ |
| 74 | +#define DEFAULT_GAIN 10 /* 0-21 */ |
| 75 | +#define DEFAULT_FILENAME "capture.bin" |
| 76 | + |
| 77 | +// Global Application State |
| 78 | +static struct hydrasdr_device *g_dev = NULL; |
| 79 | +static FILE *g_out = NULL; |
| 80 | + |
| 81 | +static sig_atomic_bool_t g_exit_requested = 0; |
| 82 | + |
| 83 | +static volatile uint64_t g_total_bytes = 0; |
| 84 | +static volatile uint64_t g_total_dropped_samples = 0; |
| 85 | + |
| 86 | +// Signal Handling |
| 87 | +#ifdef _WIN32 |
| 88 | +BOOL WINAPI |
| 89 | +windows_signal_handler(DWORD signum) |
| 90 | +{ |
| 91 | + if (!g_exit_requested) |
| 92 | + fprintf(stderr, "\nCaught signal %lu\n", (unsigned long)signum); |
| 93 | + |
| 94 | + g_exit_requested = 1; |
| 95 | + return TRUE; |
| 96 | +} |
| 97 | +#else |
| 98 | +static void posix_signal_handler(int signum) |
| 99 | +{ |
| 100 | + if (!g_exit_requested) |
| 101 | + fprintf(stderr, "\nCaught signal %d\n", signum); |
| 102 | + |
| 103 | + g_exit_requested = 1; |
| 104 | +} |
| 105 | +#endif |
| 106 | + |
| 107 | +/** |
| 108 | + * @brief Helper to calculate bytes per sample based on API enum. |
| 109 | + * |
| 110 | + * @param type The hydrasdr_sample_type enum from the transfer struct. |
| 111 | + * @return size_t Number of bytes per single sample (I+Q combined if applicable). |
| 112 | + */ |
| 113 | +static inline size_t bytes_per_sample(enum hydrasdr_sample_type type) |
| 114 | +{ |
| 115 | + switch (type) { |
| 116 | + case HYDRASDR_SAMPLE_FLOAT32_IQ: return 8; // 4 bytes I + 4 bytes Q |
| 117 | + case HYDRASDR_SAMPLE_FLOAT32_REAL: return 4; |
| 118 | + case HYDRASDR_SAMPLE_INT16_IQ: return 4; // 2 bytes I + 2 bytes Q |
| 119 | + case HYDRASDR_SAMPLE_INT16_REAL: return 2; |
| 120 | + case HYDRASDR_SAMPLE_UINT16_REAL: return 2; |
| 121 | + case HYDRASDR_SAMPLE_RAW: return 2; /* Raw device stream */ |
| 122 | + default: return 2; |
| 123 | + } |
| 124 | +} |
| 125 | + |
| 126 | +static void print_usage(const char *prog) |
| 127 | +{ |
| 128 | + printf("Usage: %s [options]\n", prog); |
| 129 | + printf("\nOptions:\n"); |
| 130 | + printf(" -f <Hz> Set RF frequency (default: %llu Hz)\n", DEFAULT_FREQ_HZ); |
| 131 | + printf(" -s <SPS> Set sample rate (default: %u)\n", DEFAULT_SAMPLERATE); |
| 132 | + printf(" -t <type> Set sample type (default: %d = Int16 IQ)\n", DEFAULT_SAMPLETYPE); |
| 133 | + printf(" 0=FloatIQ, 1=FloatReal, 2=Int16IQ, 3=Int16Real, 5=Raw\n"); |
| 134 | + printf(" -g <0-21> Linearity gain (default: %d)\n", DEFAULT_GAIN); |
| 135 | + printf(" -b <0/1> Bias-T off/on (default: 0)\n"); |
| 136 | + printf(" -o <file> Output file (default: %s)\n", DEFAULT_FILENAME); |
| 137 | + printf(" -h Show help\n"); |
| 138 | +} |
| 139 | + |
| 140 | +/** |
| 141 | + * @brief Asynchronous Callback Function. |
| 142 | + * |
| 143 | + * @details This function is invoked by the HydraSDR library thread. |
| 144 | + * Critical Section: Execution time must be minimized. |
| 145 | + * |
| 146 | + * @param transfer Pointer to the transfer structure containing data and metadata. |
| 147 | + * @return int 0 to continue streaming, non-zero to request stop (internally). |
| 148 | + */ |
| 149 | +int rx_callback(hydrasdr_transfer_t *t) |
| 150 | +{ |
| 151 | + if (g_exit_requested) |
| 152 | + return 0; |
| 153 | + |
| 154 | + if (t->dropped_samples) |
| 155 | + g_total_dropped_samples += t->dropped_samples; |
| 156 | + |
| 157 | + const size_t bps = bytes_per_sample(t->sample_type); |
| 158 | + const size_t chunk_bytes = (size_t)t->sample_count * bps; |
| 159 | + |
| 160 | + if (g_out && chunk_bytes > 0) { |
| 161 | + const size_t w = fwrite(t->samples, 1, chunk_bytes, g_out); |
| 162 | + if (w != chunk_bytes) |
| 163 | + fprintf(stderr, "Disk write error\n"); |
| 164 | + } |
| 165 | + |
| 166 | + g_total_bytes += chunk_bytes; |
| 167 | + |
| 168 | + return 0; |
| 169 | +} |
| 170 | + |
| 171 | +/** |
| 172 | + * @brief Main Entry Point |
| 173 | + */ |
| 174 | + |
| 175 | +int main(int argc, char **argv) |
| 176 | +{ |
| 177 | + uint64_t freq_hz = DEFAULT_FREQ_HZ; |
| 178 | + uint32_t samplerate = DEFAULT_SAMPLERATE; |
| 179 | + uint8_t gain = DEFAULT_GAIN; |
| 180 | + uint8_t bias = 0; |
| 181 | + int sample_type = DEFAULT_SAMPLETYPE; |
| 182 | + const char *filename = DEFAULT_FILENAME; |
| 183 | + |
| 184 | + // 1. Parse Command Line Arguments (Manual parsing for no deps) |
| 185 | + for (int i = 1; i < argc; i++) { |
| 186 | + if (!strcmp(argv[i], "-f") && i + 1 < argc) { |
| 187 | + freq_hz = strtoull(argv[++i], NULL, 10); |
| 188 | + } |
| 189 | + else if (!strcmp(argv[i], "-s") && i + 1 < argc) { |
| 190 | + samplerate = (uint32_t)strtoul(argv[++i], NULL, 10); |
| 191 | + } |
| 192 | + else if (!strcmp(argv[i], "-t") && i + 1 < argc) { |
| 193 | + sample_type = atoi(argv[++i]); |
| 194 | + } |
| 195 | + else if (!strcmp(argv[i], "-g") && i + 1 < argc) { |
| 196 | + gain = (uint8_t)atoi(argv[++i]); |
| 197 | + } |
| 198 | + else if (!strcmp(argv[i], "-b") && i + 1 < argc) { |
| 199 | + bias = (uint8_t)atoi(argv[++i]); |
| 200 | + } |
| 201 | + else if (!strcmp(argv[i], "-o") && i + 1 < argc) { |
| 202 | + filename = argv[++i]; |
| 203 | + } |
| 204 | + else if (!strcmp(argv[i], "-h") || !strcmp(argv[i], "--help")) { |
| 205 | + print_usage(argv[0]); |
| 206 | + return 0; |
| 207 | + } |
| 208 | + else { |
| 209 | + fprintf(stderr, "Unknown option: %s\n", argv[i]); |
| 210 | + print_usage(argv[0]); |
| 211 | + return 1; |
| 212 | + } |
| 213 | + } |
| 214 | + |
| 215 | + // 2. Setup Signal Handling (Ctrl+C) |
| 216 | +#ifdef _WIN32 |
| 217 | + SetConsoleCtrlHandler(windows_signal_handler, TRUE); |
| 218 | +#else |
| 219 | + signal(SIGINT, posix_signal_handler); |
| 220 | + signal(SIGTERM, posix_signal_handler); |
| 221 | + signal(SIGABRT, posix_signal_handler); |
| 222 | +#endif |
| 223 | + |
| 224 | + printf("HydraSDR Async RX Tool v%s\n", HYDRASDR_ASYNC_RX_VERSION); |
| 225 | + |
| 226 | + // 3. Open Device |
| 227 | + // Note: hydrasdr_open opens the first available device |
| 228 | + int ret = hydrasdr_open(&g_dev); |
| 229 | + if (ret != HYDRASDR_SUCCESS) { |
| 230 | + fprintf(stderr, "ERROR: hydrasdr_open failed: %s\n", |
| 231 | + hydrasdr_error_name(ret)); |
| 232 | + return EXIT_FAILURE; |
| 233 | + } |
| 234 | + printf("[INFO] Device opened.\n"); |
| 235 | + |
| 236 | + // 4. Print supported samplerates |
| 237 | + uint32_t count = 0; |
| 238 | + hydrasdr_get_samplerates(g_dev, &count, 0); |
| 239 | + |
| 240 | + if (count > 0) { |
| 241 | + uint32_t *rates = malloc(count * sizeof(uint32_t)); |
| 242 | + if (rates) { |
| 243 | + hydrasdr_get_samplerates(g_dev, rates, count); |
| 244 | + printf("Available sample rates:\n"); |
| 245 | + for (uint32_t i = 0; i < count; i++) |
| 246 | + printf(" %u (%.3f MSPS)\n", rates[i], rates[i] / 1e6); |
| 247 | + free(rates); |
| 248 | + } |
| 249 | + } |
| 250 | + printf("\n"); |
| 251 | + |
| 252 | + // 5. Apply Configuration |
| 253 | + |
| 254 | + // A. Frequency |
| 255 | + printf("[CONF] Frequency: %llu Hz\n", (unsigned long long)freq_hz); |
| 256 | + ret = hydrasdr_set_freq(g_dev, freq_hz); |
| 257 | + if (ret != HYDRASDR_SUCCESS) { |
| 258 | + fprintf(stderr, "[ERROR] Failed set frequency: %s\n", hydrasdr_error_name(ret)); |
| 259 | + goto error; |
| 260 | + } |
| 261 | + |
| 262 | + // B. Sample Rate |
| 263 | + printf("[CONF] Samplerate: %u SPS\n", samplerate); |
| 264 | + ret = hydrasdr_set_samplerate(g_dev, samplerate); |
| 265 | + if (ret != HYDRASDR_SUCCESS) { |
| 266 | + fprintf(stderr, "[ERROR] Failed set sample rate: %s\n", hydrasdr_error_name(ret)); |
| 267 | + goto error; |
| 268 | + } |
| 269 | + |
| 270 | + // C. Sample Type |
| 271 | + if (sample_type < 0 || sample_type >= HYDRASDR_SAMPLE_END) { |
| 272 | + fprintf(stderr, "ERROR: Invalid sample type %d\n", sample_type); |
| 273 | + goto error; |
| 274 | + } |
| 275 | + printf("[CONF] Sample type: %d\n", sample_type); |
| 276 | + ret = hydrasdr_set_sample_type(g_dev, (enum hydrasdr_sample_type)sample_type); |
| 277 | + if (ret != HYDRASDR_SUCCESS) { |
| 278 | + fprintf(stderr, "[ERROR] Failed set sample type: %s\n", hydrasdr_error_name(ret)); |
| 279 | + goto error; |
| 280 | + } |
| 281 | + |
| 282 | + // D. Gain (Linearity Mode) |
| 283 | + printf("[CONF] Gain: %u\n", gain); |
| 284 | + ret = hydrasdr_set_linearity_gain(g_dev, gain); |
| 285 | + if (ret != HYDRASDR_SUCCESS) { |
| 286 | + fprintf(stderr, "[ERROR] Failed set linearity gain: %s\n", hydrasdr_error_name(ret)); |
| 287 | + goto error; |
| 288 | + } |
| 289 | + |
| 290 | + // E. Bias Tee |
| 291 | + printf("[CONF] Bias-T: %u\n", bias); |
| 292 | + ret = hydrasdr_set_rf_bias(g_dev, bias); |
| 293 | + if (ret != HYDRASDR_SUCCESS) { |
| 294 | + fprintf(stderr, "[ERROR] Failed set Bias Tee: %s\n", hydrasdr_error_name(ret)); |
| 295 | + goto error; |
| 296 | + } |
| 297 | + if (bias) |
| 298 | + printf("[WARN] Bias-T ENABLED.\n"); |
| 299 | + |
| 300 | + // 6. Open Output File |
| 301 | + g_out = fopen(filename, "wb"); |
| 302 | + if (!g_out) { |
| 303 | + fprintf(stderr, "ERROR: Cannot open '%s': %s\n", |
| 304 | + filename, strerror(errno)); |
| 305 | + goto error; |
| 306 | + } |
| 307 | + printf("[INFO] Writing to '%s'\n", filename); |
| 308 | + |
| 309 | + // 7. Start Streaming (Async) |
| 310 | + printf("[INFO] Starting stream... (Press Ctrl+C to stop)\n"); |
| 311 | + |
| 312 | + // Pass 'NULL' as user context (last arg) since we use global vars for this simple example |
| 313 | + ret = hydrasdr_start_rx(g_dev, rx_callback, NULL); |
| 314 | + if (ret) { |
| 315 | + fprintf(stderr, "ERROR: start_rx failed: %s\n", |
| 316 | + hydrasdr_error_name(ret)); |
| 317 | + goto error; |
| 318 | + } |
| 319 | + |
| 320 | + // 8. Monitoring Loop |
| 321 | + const double t_start = get_time_sec(); |
| 322 | + double t_last = t_start; |
| 323 | + uint64_t last_bytes = 0; |
| 324 | + |
| 325 | + const size_t bps = bytes_per_sample((enum hydrasdr_sample_type)sample_type); |
| 326 | + |
| 327 | + while (!g_exit_requested) { |
| 328 | + SLEEP_MS(1000); |
| 329 | + |
| 330 | + if (hydrasdr_is_streaming(g_dev) != HYDRASDR_TRUE) { |
| 331 | + fprintf(stderr, "\n[ERROR] Device stopped streaming.\n"); |
| 332 | + g_exit_requested = 1; |
| 333 | + break; |
| 334 | + } |
| 335 | + |
| 336 | + double t_now = get_time_sec(); |
| 337 | + double dt = t_now - t_last; |
| 338 | + |
| 339 | + if (dt >= 1.0 && !g_exit_requested) { |
| 340 | + const uint64_t bytes_now = g_total_bytes; |
| 341 | + const uint64_t dbytes = bytes_now - last_bytes; |
| 342 | + |
| 343 | + const double inst_msps = (dbytes / (double)bps) / (dt * 1e6); |
| 344 | + const double avg_msps = (bytes_now / (double)bps) / ((t_now - t_start) * 1e6); |
| 345 | + |
| 346 | + printf("Time %4.0fs | Inst %5.2f MSPS | Avg %5.2f MSPS | Vol %7.2f MB | Drops %llu\r", |
| 347 | + t_now - t_start, |
| 348 | + inst_msps, |
| 349 | + avg_msps, |
| 350 | + bytes_now / (1024.0 * 1024.0), |
| 351 | + (unsigned long long)g_total_dropped_samples); |
| 352 | + fflush(stdout); |
| 353 | + |
| 354 | + last_bytes = bytes_now; |
| 355 | + t_last = t_now; |
| 356 | + } |
| 357 | + } |
| 358 | + |
| 359 | + printf("\n\n[INFO] Stopping ...\n"); |
| 360 | + |
| 361 | +error: |
| 362 | + if (g_dev) { |
| 363 | + hydrasdr_stop_rx(g_dev); |
| 364 | + hydrasdr_close(g_dev); |
| 365 | + } |
| 366 | + if (g_out) |
| 367 | + fclose(g_out); |
| 368 | + |
| 369 | + printf("[INFO] Done.\n"); |
| 370 | + return ret == HYDRASDR_SUCCESS ? EXIT_SUCCESS : EXIT_FAILURE; |
| 371 | +} |
0 commit comments