// This file is part of LeanSDR Copyright (C) 2016-2018 . // See the toplevel README for more information. // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with this program. If not, see . #ifndef LEANSDR_GUI_H #define LEANSDR_GUI_H #include #include "framework.h" namespace leansdr { ////////////////////////////////////////////////////////////////////// // GUI blocks ////////////////////////////////////////////////////////////////////// #ifdef GUI #include #include #include static const int DEFAULT_GUI_DECIMATION = 64; struct gfx { Display *display; int screen; int w, h; Window window; GC gc; Pixmap dbuf; gfx(scheduler *sch, const char *name) { window_placement *wp; for (wp = sch->windows; wp && wp->name; ++wp) if (!strcmp(wp->name, name)) break; if (wp && wp->name) init(wp->name, wp->x, wp->y, wp->w, wp->h); else { fprintf(stderr, "No placement hints for window '%s'\n", name); init(name, -1, -1, 320, 240); } } gfx(const char *name, int _x, int _y, int _w, int _h) { init(name, _x, _y, _w, _h); } void init(const char *name, int _x, int _y, int _w, int _h) { buttons = 0; clicks = 0; mmoved = false; w = _w; h = _h; display = XOpenDisplay(getenv("DISPLAY")); if (!display) fatal("display"); screen = DefaultScreen(display); XSetWindowAttributes xswa; xswa.event_mask = (ExposureMask | StructureNotifyMask | ButtonPressMask | ButtonReleaseMask | KeyPressMask | KeyReleaseMask | PointerMotionMask); xswa.background_pixel = BlackPixel(display, screen); window = XCreateWindow(display, DefaultRootWindow(display), 100, 100, w, h, 10, CopyFromParent, InputOutput, CopyFromParent, CWEventMask | CWBackPixel, &xswa); if (!window) fatal("window"); XStoreName(display, window, name); XMapWindow(display, window); if (_x >= 0 && _y >= 0) XMoveWindow(display, window, _x, _y); dbuf = XCreatePixmap(display, window, w, h, DefaultDepth(display, screen)); gc = XCreateGC(display, dbuf, 0, NULL); if (!gc) fatal("gc"); } void clear() { setfg(0, 0, 0); XFillRectangle(display, dbuf, gc, 0, 0, w, h); } void show() { XCopyArea(display, dbuf, window, gc, 0, 0, w, h, 0, 0); } void sync() { XSync(display, False); } void events() { XEvent ev; while (XCheckWindowEvent(display, window, -1, &ev)) { switch (ev.type) { case ButtonPress: { int b = ev.xbutton.button; buttons |= 1 << b; clicks |= 1 << b; mx = ev.xbutton.x; my = ev.xbutton.y; break; } case ButtonRelease: { int b = ev.xbutton.button; buttons &= ~(1 << b); mx = ev.xbutton.x; my = ev.xbutton.y; break; } case MotionNotify: mx = ev.xbutton.x; my = ev.xbutton.y; mmoved = true; break; } } } void setfg(unsigned char r, unsigned char g, unsigned char b) { XColor c; c.red = r << 8; c.green = g << 8; c.blue = b << 8; c.flags = DoRed | DoGreen | DoBlue; if (!XAllocColor(display, DefaultColormap(display, screen), &c)) fatal("color"); XSetForeground(display, gc, c.pixel); } void point(int x, int y) { XDrawPoint(display, dbuf, gc, x, y); } void line(int x0, int y0, int x1, int y1) { XDrawLine(display, dbuf, gc, x0, y0, x1, y1); } void text(int x, int y, const char *s) { XDrawString(display, dbuf, gc, x, y, s, strlen(s)); } void transient_text(int x, int y, const char *s) { XDrawString(display, window, gc, x, y, s, strlen(s)); } int buttons; // Mask of button states (2|4|8) int clicks; // Same, accumulated (must be cleared by owner) int mx, my; // Cursor position bool mmoved; // Pointer moved (must be cleared by owner) }; template struct cscope : runnable { T xymin, xymax; unsigned long decimation; unsigned long pixels_per_frame; cstln_base **cstln; // Optional ptr to optional constellation cscope(scheduler *sch, pipebuf> &_in, T _xymin, T _xymax, const char *_name = NULL) : runnable(sch, _name ? _name : _in.name), xymin(_xymin), xymax(_xymax), decimation(DEFAULT_GUI_DECIMATION), pixels_per_frame(1024), cstln(NULL), in(_in), phase(0), g(sch, name) { } void run() { while (in.readable() >= pixels_per_frame) { if (!phase) { draw_begin(); g.setfg(0, 255, 0); std::complex *p = in.rd(), *pend = p + pixels_per_frame; for (; p < pend; ++p) g.point(g.w * (p->re - xymin) / (xymax - xymin), g.h - g.h * (p->im - xymin) / (xymax - xymin)); if (cstln && (*cstln)) { // Plot constellation points g.setfg(255, 255, 255); for (int i = 0; i < (*cstln)->nsymbols; ++i) { std::complex *p = &(*cstln)->symbols[i]; int x = g.w * (p->re - xymin) / (xymax - xymin); int y = g.h - g.h * (p->im - xymin) / (xymax - xymin); for (int d = -2; d <= 2; ++d) { g.point(x + d, y); g.point(x, y + d); } } } g.show(); g.sync(); } in.read(pixels_per_frame); if (++phase >= decimation) phase = 0; } } //private: pipereader> in; unsigned long phase; gfx g; void draw_begin() { g.clear(); g.setfg(0, 255, 0); g.line(g.w / 2, 0, g.w / 2, g.h); g.line(0, g.h / 2, g.w, g.h / 2); } }; template struct wavescope : runnable { T ymin, ymax; unsigned long decimation; float hgrid; wavescope(scheduler *sch, pipebuf &_in, T _ymin, T _ymax, const char *_name = NULL) : runnable(sch, _name ? _name : _in.name), ymin(_ymin), ymax(_ymax), decimation(DEFAULT_GUI_DECIMATION), hgrid(0), in(_in), phase(0), g(sch, name), x(0) { g.clear(); } void run() { while (in.readable() >= g.w) { if (!phase) plot(in.rd(), g.w); in.read(g.w); if (++phase >= decimation) phase = 0; } } void plot(T *p, int count) { T *pend = p + count; g.clear(); g.setfg(128, 128, 0); g.line(0, g.h / 2, g.w - 1, g.h / 2); if (hgrid) { for (float x = 0; x < g.w; x += hgrid) g.line(x, 0, x, g.h - 1); } g.setfg(0, 255, 0); for (int x = 0; p < pend; ++x, ++p) { T v = *p; g.point(x, g.h - 1 - (g.h - 1) * (v - ymin) / (ymax - ymin)); } g.show(); g.sync(); } private: pipereader in; int phase; gfx g; int x; }; template struct slowmultiscope : runnable { struct chanspec { pipebuf *in; const char *name, *format; unsigned char rgb[3]; float scale; float ymin, ymax; enum flag { DEFAULT = 0, ASYNC = 1, // Read whatever is available COUNT = 2, // Display number of items read instead of value SUM = 4, // Display sum of values LINE = 8, // Connect points WRAP = 16, // Modulo min..max DISABLED = 32, // Ignore channel } flags; }; unsigned long samples_per_pixel; float sample_freq; // Sample rate in Hz (used for cursor operations) slowmultiscope(scheduler *sch, const chanspec *specs, int nspecs, const char *_name) : runnable(sch, _name ? _name : "slowmultiscope"), samples_per_pixel(1), sample_freq(1), g(sch, name), t(0), x(0), total_samples(0) { chans = new channel[nspecs]; nchans = 0; for (int i = 0; i < nspecs; ++i) { if (specs[i].flags & chanspec::DISABLED) continue; chans[nchans].spec = specs[i]; chans[nchans].in = new pipereader(*specs[i].in); chans[nchans].accum = 0; ++nchans; } g.clear(); } void run() { // Asynchronous channels: Read all available data for (channel *c = chans; c < chans + nchans; ++c) { if (c->spec.flags & chanspec::ASYNC) run_channel(c, c->in->readable()); } // Synchronous channels: Read up to one pixel worth of data // between display syncs. unsigned long count = samples_per_pixel; for (channel *c = chans; c < chans + nchans; ++c) if (!(c->spec.flags & chanspec::ASYNC)) count = min(count, c->in->readable()); for (int n = count; n--;) { for (channel *c = chans; c < chans + nchans; ++c) if (!(c->spec.flags & chanspec::ASYNC)) run_channel(c, 1); } t += count; g.show(); // Print instantatenous values as text for (int i = 0; i < nchans; ++i) { channel *c = &chans[i]; g.setfg(c->spec.rgb[0], c->spec.rgb[1], c->spec.rgb[2]); char text[256]; sprintf(text, c->spec.format, c->print_val); g.transient_text(5, 20 + 16 * i, text); } run_gui(); if (t >= samples_per_pixel) { t = 0; ++x; if (x >= g.w) x = 0; g.setfg(0, 0, 0); g.line(x, 0, x, g.h - 1); } run_gui(); g.sync(); total_samples += count; } private: int nchans; struct channel { chanspec spec; pipereader *in; float accum; int prev_y; float print_val; } * chans; void run_channel(channel *c, int nr) { g.setfg(c->spec.rgb[0], c->spec.rgb[1], c->spec.rgb[2]); int y = -1; while (nr--) { float v = *c->in->rd() * c->spec.scale; if (c->spec.flags & chanspec::COUNT) ++c->accum; else if (c->spec.flags & chanspec::SUM) c->accum += v; else { c->print_val = v; float nv = (v - c->spec.ymin) / (c->spec.ymax - c->spec.ymin); if (c->spec.flags & chanspec::WRAP) nv = nv - floor(nv); y = g.h - g.h * nv; } c->in->read(1); } // Display count/sum channels only when the cursor is about to move. if ((c->spec.flags & (chanspec::COUNT | chanspec::SUM)) && t + 1 >= samples_per_pixel) { T v = c->accum; y = g.h - 1 - g.h * (v - c->spec.ymin) / (c->spec.ymax - c->spec.ymin); c->accum = 0; c->print_val = v; } if (y >= 0) { if (c->spec.flags & chanspec::LINE) { if (x) g.line(x - 1, c->prev_y, x, y); c->prev_y = y; } else g.point(x, y); } } void run_gui() { g.events(); // Print cursor time float ct = g.mx * samples_per_pixel / sample_freq; float tt = total_samples / sample_freq; char text[256]; sprintf(text, "%.3f / %.3f s", ct, tt); g.setfg(255, 255, 255); g.transient_text(g.w * 3 / 4, 20, text); } gfx g; unsigned long t; // Time for synchronous channels int x; int total_samples; }; template struct spectrumscope : runnable { unsigned long size; float amax; unsigned long decimation; spectrumscope(scheduler *sch, pipebuf> &_in, T _max, const char *_name = NULL) : runnable(sch, _name ? _name : _in.name), size(4096), amax(_max / sqrtf(size)), decimation(DEFAULT_GUI_DECIMATION), in(_in), nmarkers(0), phase(0), g(sch, name), fft(NULL) { } void mark_freq(float f) { if (nmarkers == MAX_MARKERS) fail("Too many markers"); markers[nmarkers++] = f; } void run() { while (in.readable() >= size) { if (!phase) do_fft(in.rd()); in.read(size); if (++phase >= decimation) phase = 0; } } private: pipereader> in; static const int MAX_MARKERS = 4; float markers[MAX_MARKERS]; int nmarkers; int phase; gfx g; cfft_engine *fft; void do_fft(std::complex *input) { draw_begin(); if (!fft || fft->n != size) { if (fft) delete fft; fft = new cfft_engine(size); } std::complex *pin = input, *pend = pin + size; std::complex data[size], *pout = data; g.setfg(255, 0, 0); for (int x = 0; pin < pend; ++pin, ++pout, ++x) { pout->re = (float)pin->re; pout->im = (float)pin->im; // g.point(x, g.h/2-pout->re*g.h/2/ymax); } fft->inplace(data, true); g.setfg(0, 255, 0); for (int i = 0; i < size; ++i) { int x = ((i < size / 2) ? i + size / 2 : i - size / 2) * g.w / size; std::complex v = data[i]; ; float y = hypot(v.real(), v.imag()); g.line(x, g.h - 1, x, g.h - 1 - y * g.h / amax); } if (g.buttons) { char s[256]; float f = 2.4e6 * (g.mx - g.w / 2) / g.w; sprintf(s, "%f", f); g.text(16, 16, s); } g.show(); g.sync(); } void draw_begin() { g.clear(); g.setfg(255, 255, 255); g.line(g.w / 2, 0, g.w / 2, g.h); g.setfg(255, 0, 0); for (int i = 0; i < nmarkers; ++i) { int x = g.w * (0.5 + markers[i]); g.line(x, 0, x, g.h); } } }; template struct rfscope : runnable { unsigned long size; unsigned long decimation; float Fs; // Sampling freq for display (Hz) float Fc; // Center freq for display (Hz) int ncursors; float *cursors; // Cursor frequencies float hzoom; // Horizontal zoom factor float db0, dbrange; // Vertical range db0 .. db0+dbrange float bw; // Smoothing bandwidth rfscope(scheduler *sch, pipebuf> &_in, const char *_name = NULL) : runnable(sch, _name ? _name : _in.name), size(4096), decimation(DEFAULT_GUI_DECIMATION), Fs(1), Fc(0), ncursors(0), hzoom(1), db0(-25), dbrange(50), bw(0.05), in(_in), phase(0), g(sch, name), fft(NULL), filtered(NULL) { } void run() { while (in.readable() >= size) { if (!phase) do_fft(in.rd()); in.read(size); if (++phase >= decimation) phase = 0; } } private: pipereader> in; int phase; gfx g; cfft_engine *fft; float *filtered; void do_fft(std::complex *input) { g.events(); draw_begin(); if (!fft || fft->n != size) { if (fft) delete fft; fft = new cfft_engine(size); } // Convert to std::complex and transform std::complex *pin = input, *pend = pin + size; std::complex data[size], *pout = data; for (int x = 0; pin < pend; ++pin, ++pout, ++x) { pout->re = (float)pin->re; pout->im = (float)pin->im; } fft->inplace(data, true); float amp2[size]; for (int i = 0; i < size; ++i) { std::complex &v = data[i]; ; amp2[i] = (v.real() * v.real() + v.imag() * v.imag()) * size; } if (!filtered) { filtered = new float[size]; for (int i = 0; i < size; ++i) filtered[i] = amp2[i]; } float bwcomp = 1 - bw; g.setfg(0, 255, 0); for (int i = 0; i < size; ++i) { filtered[i] = amp2[i] * bw + filtered[i] * bwcomp; float db = filtered[i] ? 10 * logf(filtered[i]) / logf(10) : db0; int is = (i < size / 2) ? i : i - size; int x = g.w / 2 + is * hzoom * g.w / size; int y = g.h - 1 - (db - db0) * g.h / dbrange; g.line(x, g.h - 1, x, y); } if (g.buttons) { char s[256]; float freq = Fc + Fs * (g.mx - g.w / 2) / g.w / hzoom; float val = db0 + (float)((g.h - 1) - g.my) * dbrange / g.h; sprintf(s, "%f.3 Hz %f.2 dB", freq, val); g.setfg(255, 255, 255); g.text(16, 16, s); } // Draw cursors g.setfg(255, 255, 0); for (int i = 0; i < ncursors; ++i) { int x = g.w / 2 + (cursors[i] - Fc) * hzoom * g.w / Fs; g.line(x, 0, x, g.h - 1); } g.show(); g.sync(); } void draw_begin() { g.clear(); // dB scale g.setfg(64, 64, 64); for (float db = floorf(db0); db < db0 + dbrange; ++db) { int y = g.h - 1 - (db - db0) * g.h / dbrange; g.line(0, y, g.w - 1, y); } // DC line g.setfg(255, 255, 255); g.line(g.w / 2, 0, g.w / 2, g.h); } }; template struct genscope : runnable { struct render { int x, y; char dir; // 'h'orizontal or 'v'ertical }; struct chanspec { pipebuf *in; // NULL if disabled render r; }; genscope(scheduler *sch, chanspec *specs, int _nchans, const char *_name = NULL) : runnable(sch, _name ? _name : "genscope"), nchans(_nchans), g(sch, name) { chans = new channel[nchans]; for (int i = 0; i < nchans; ++i) { if (!specs[i].in) { chans[i].in = NULL; } else { chans[i].spec = specs[i]; chans[i].in = new pipereader(*specs[i].in); } } g.clear(); gettimeofday(&tv, NULL); } struct channel { chanspec spec; pipereader *in; } * chans; int nchans; struct timeval tv; void run() { g.setfg(0, 255, 0); for (channel *pc = chans; pc < chans + nchans; ++pc) { if (!pc->in) continue; int n = pc->in->readable(); T last = pc->in->rd()[n - 1]; pc->in->read(n); int dx = pc->spec.r.dir == 'h' ? last : 0; int dy = pc->spec.r.dir == 'v' ? last : 0; dx /= 3; dy /= 3; g.line(pc->spec.r.x - dx, pc->spec.r.y - dy, pc->spec.r.x + dx, pc->spec.r.y + dy); char txt[16]; sprintf(txt, "%d", (int)last); g.text(pc->spec.r.x + 5, pc->spec.r.y - 2, txt); } struct timeval newtv; gettimeofday(&newtv, NULL); int dt = (newtv.tv_sec - tv.tv_sec) * 1000 + (newtv.tv_usec - tv.tv_usec) / 1000; if (dt > 100) { fprintf(stderr, "#"); g.show(); g.sync(); g.clear(); tv = newtv; } } private: gfx g; }; #endif // GUI } // namespace leansdr #endif // LEANSDR_GUI_H