diff --git a/gui.py b/gui.py new file mode 100644 index 0000000..08a6bbb --- /dev/null +++ b/gui.py @@ -0,0 +1,84 @@ +import asyncio +import os +import logging + +import numpy as np +from quamash import QEventLoop, QtWidgets +import pyqtgraph as pg +from sipyco.pipe_ipc import AsyncioChildComm +from sipyco import pyon + + +class SpectrogramWidget(pg.PlotWidget): + def __init__(self, block_size=4096): + super(SpectrogramWidget, self).__init__() + + self.block_size = block_size + + self.img = pg.ImageItem() + self.addItem(self.img) + + self.img_array = np.zeros((100, block_size)) + + pos = np.array([0., 1., 0.5, 0.25, 0.75]) + color = np.array([[0,255,255,255], [255,255,0,255], [0,0,0,255], (0, 0, 255, 255), (255, 0, 0, 255)], dtype=np.ubyte) + cmap = pg.ColorMap(pos, color) + lut = cmap.getLookupTable(0.0, 1.0, 256) + + self.img.setLookupTable(lut) + self.img.setLevels([-50,40]) + + self.show() + + def update(self, block): + self.img_array = np.roll(self.img_array, -1, 0) + self.img_array[-1:] = block + self.img.setImage(self.img_array, autoLevels=True) + + +class IPCClient(AsyncioChildComm): + def set_close_cb(self, close_cb): + self.close_cb = close_cb + + async def read_pyon(self): + line = await self.readline() + return pyon.decode(line.decode()) + + async def listen(self, spectrogram): + while True: + obj = await self.read_pyon() + try: + action = obj["action"] + if action == "update": + spectrogram.update(obj["data"]) + if action == "terminate": + self.close_cb() + return + except: + logging.error("error processing parent message", + exc_info=True) + self.close_cb() + + +def main(): + app = QtWidgets.QApplication([]) + loop = QEventLoop(app) + asyncio.set_event_loop(loop) + + try: + ipc = IPCClient(os.getenv("NOPTICA2_IPC")) + loop.run_until_complete(ipc.connect()) + try: + main_widget = SpectrogramWidget() + main_widget.show() + ipc.set_close_cb(main_widget.close) + asyncio.ensure_future(ipc.listen(main_widget)) + loop.run_forever() + finally: + ipc.close() + finally: + loop.close() + + +if __name__ == "__main__": + main() diff --git a/gui_test.py b/gui_test.py new file mode 100644 index 0000000..e56b6a3 --- /dev/null +++ b/gui_test.py @@ -0,0 +1,68 @@ +import os +import sys +import subprocess +import time + +import numpy as np +from sipyco import pyon + + +# env QT_QPA_PLATFORM=wayland python gui_test.py + +class ParentComm: + def __init__(self): + self.c_rfd, self.p_wfd = os.pipe() + self.p_rfd, self.c_wfd = os.pipe() + self.rf = open(int(self.p_rfd), "rb", 0) + self.wf = open(int(self.p_wfd), "wb", 0) + self.process = None + + def get_address(self): + return "{},{}".format(self.c_rfd, self.c_wfd) + + def read(self, n): + return self.rf.read(n) + + def readline(self): + return self.rf.readline() + + def write(self, data): + return self.wf.write(data) + + def write_pyon(self, obj): + self.write(pyon.encode(obj).encode() + b"\n") + + def close(self): + self.rf.close() + self.wf.close() + if self.process is not None: + self.process.wait() + + def create_subprocess(self, *args): + env = os.environ.copy() + env["PYTHONUNBUFFERED"] = "1" + env["NOPTICA2_IPC"] = self.get_address() + self.process = subprocess.Popen( + *args, pass_fds={self.c_rfd, self.c_wfd}, + env=env) + os.close(self.c_rfd) + os.close(self.c_wfd) + + + +def main(): + gui = ParentComm() + try: + gui.create_subprocess([sys.executable, "gui.py"]) + for i in range(600): + obj = {"action": "update", "data": np.random.normal(size=4096)} + gui.write_pyon(obj) + time.sleep(1/60) + obj = {"action": "terminate"} + gui.write_pyon(obj) + finally: + gui.close() + + +if __name__ == "__main__": + main() diff --git a/shell.nix b/shell.nix index 8d6675f..3ddd800 100644 --- a/shell.nix +++ b/shell.nix @@ -16,12 +16,28 @@ let url = "https://www.nuand.com/fpga/v0.11.0/hostedxA4.rbf"; sha256 = "c172e35c4a92cf1e0ca3b37347a84d8376b275ece16cb9c5142b72b82b16fe8e"; }; + sipyco = pkgs.python3Packages.buildPythonPackage rec { + name = "sipyco"; + version = "1.1"; + src = pkgs.fetchFromGitHub { + owner = "m-labs"; + repo = "sipyco"; + rev = "v${version}"; + sha256 = "09vyrzfhnbp65ybd7w2g96gvvnhzafpn72syls2kbg2paqjjf9gs"; + }; + propagatedBuildInputs = [ pkgs.python3Packages.numpy ]; + }; in pkgs.mkShell { + nativeBuildInputs = [ pkgs.qt5.wrapQtAppsHook ]; buildInputs = [ - (pkgs.python3.withPackages(ps: [ps.soapysdr-with-plugins ps.scipy ps.pyserial ps.quamash ps.pyqt5 pyqtgraph-qt5])) + (pkgs.python3.withPackages(ps: [ps.soapysdr-with-plugins ps.scipy ps.pyserial ps.quamash ps.pyqt5 pyqtgraph-qt5 sipyco])) pkgs.libbladeRF pkgs.gqrx ]; + dontWrapQtApps = true; + postFixup = '' + wrapQtApp "$out/bin/python" + ''; shellHook = '' ${pkgs.libbladeRF}/bin/bladeRF-cli -l ${bitstream} ${pkgs.libbladeRF}/bin/bladeRF-cli -e "set biastee rx on"