1# NanoVNASaver 2# 3# A python program to view and export Touchstone data from a NanoVNA 4# Copyright (C) 2019, 2020 Rune B. Broberg 5# Copyright (C) 2020 NanoVNA-Saver Authors 6# 7# This program is free software: you can redistribute it and/or modify 8# it under the terms of the GNU General Public License as published by 9# the Free Software Foundation, either version 3 of the License, or 10# (at your option) any later version. 11# 12# This program is distributed in the hope that it will be useful, 13# but WITHOUT ANY WARRANTY; without even the implied warranty of 14# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15# GNU General Public License for more details. 16# 17# You should have received a copy of the GNU General Public License 18# along with this program. If not, see <https://www.gnu.org/licenses/>. 19import logging 20import struct 21from typing import List 22 23import serial 24import numpy as np 25from PyQt5 import QtGui 26 27from NanoVNASaver.Hardware.Serial import drain_serial, Interface 28from NanoVNASaver.Hardware.VNA import VNA 29from NanoVNASaver.Version import Version 30 31logger = logging.getLogger(__name__) 32 33 34class NanoVNA(VNA): 35 name = "NanoVNA" 36 screenwidth = 320 37 screenheight = 240 38 39 def __init__(self, iface: Interface): 40 super().__init__(iface) 41 self.sweep_method = "sweep" 42 self.read_features() 43 self.start = 27000000 44 self.stop = 30000000 45 self._sweepdata = [] 46 47 def _capture_data(self) -> bytes: 48 timeout = self.serial.timeout 49 with self.serial.lock: 50 drain_serial(self.serial) 51 timeout = self.serial.timeout 52 self.serial.write("capture\r".encode('ascii')) 53 self.serial.readline() 54 self.serial.timeout = 4 55 image_data = self.serial.read( 56 self.screenwidth * self.screenheight * 2) 57 self.serial.timeout = timeout 58 self.serial.timeout = timeout 59 return image_data 60 61 def _convert_data(self, image_data: bytes) -> bytes: 62 rgb_data = struct.unpack( 63 f">{self.screenwidth * self.screenheight}H", 64 image_data) 65 rgb_array = np.array(rgb_data, dtype=np.uint32) 66 return (0xFF000000 + 67 ((rgb_array & 0xF800) << 8) + 68 ((rgb_array & 0x07E0) << 5) + 69 ((rgb_array & 0x001F) << 3)) 70 71 def getScreenshot(self) -> QtGui.QPixmap: 72 logger.debug("Capturing screenshot...") 73 if not self.connected(): 74 return QtGui.QPixmap() 75 try: 76 rgba_array = self._convert_data(self._capture_data()) 77 image = QtGui.QImage( 78 rgba_array, 79 self.screenwidth, 80 self.screenheight, 81 QtGui.QImage.Format_ARGB32) 82 logger.debug("Captured screenshot") 83 return QtGui.QPixmap(image) 84 except serial.SerialException as exc: 85 logger.exception( 86 "Exception while capturing screenshot: %s", exc) 87 return QtGui.QPixmap() 88 89 def resetSweep(self, start: int, stop: int): 90 list(self.exec_command(f"sweep {start} {stop} {self.datapoints}")) 91 list(self.exec_command("resume")) 92 93 def setSweep(self, start, stop): 94 self.start = start 95 self.stop = stop 96 if self.sweep_method == "sweep": 97 list(self.exec_command(f"sweep {start} {stop} {self.datapoints}")) 98 elif self.sweep_method == "scan": 99 list(self.exec_command(f"scan {start} {stop} {self.datapoints}")) 100 101 def read_features(self): 102 super().read_features() 103 if self.version >= Version("0.7.1"): 104 logger.debug("Using scan mask command.") 105 self.features.add("Scan mask command") 106 self.sweep_method = "scan_mask" 107 elif self.version >= Version("0.2.0"): 108 logger.debug("Using new scan command.") 109 self.features.add("Scan command") 110 self.sweep_method = "scan" 111 112 def readFrequencies(self) -> List[int]: 113 logger.debug("readFrequencies: %s", self.sweep_method) 114 if self.sweep_method != "scan_mask": 115 return super().readFrequencies() 116 return [int(line) for line in self.exec_command( 117 f"scan {self.start} {self.stop} {self.datapoints} 0b001")] 118 119 def readValues(self, value) -> List[str]: 120 if self.sweep_method != "scan_mask": 121 return super().readValues(value) 122 logger.debug("readValue with scan mask (%s)", value) 123 # Actually grab the data only when requesting channel 0. 124 # The hardware will return all channels which we will store. 125 if value == "data 0": 126 self._sweepdata = [] 127 for line in self.exec_command( 128 f"scan {self.start} {self.stop} {self.datapoints} 0b110"): 129 data = line.split() 130 self._sweepdata.append(( 131 f"{data[0]} {data[1]}", 132 f"{data[2]} {data[3]}")) 133 if value == "data 0": 134 return [x[0] for x in self._sweepdata] 135 if value == "data 1": 136 return [x[1] for x in self._sweepdata] 137