kopia lustrzana https://github.com/NanoVNA-Saver/nanovna-saver
LiteVNA support (#754)
* feat: add LiteVNA64 detection Based on https://github.com/NanoVNA-Saver/nanovna-saver/issues/534#issuecomment-1837198040 data * chore: rename get_features * chore: optimize disconnect method * feat: go to idle mode after data readingpull/764/head
rodzic
12f958bc94
commit
30a3dc88c4
|
@ -49,7 +49,7 @@ Current features
|
|||
|
||||
|
||||
* Reading data from a NanoVNA -- Compatible devices: NanoVNA, NanoVNA-H,
|
||||
NanoVNA-H4, NanoVNA-F, AVNA via Teensy
|
||||
NanoVNA-H4, NanoVNA-F, AVNA via Teensy, LiteVNA
|
||||
* Reading data from a TinySA
|
||||
* Splitting a frequency range into multiple segments to increase resolution
|
||||
(tried up to >10k points)
|
||||
|
|
|
@ -151,6 +151,11 @@ class SerialControl(Control):
|
|||
def disconnect_device(self):
|
||||
with self.interface.lock:
|
||||
logger.info("Closing connection to %s", self.interface)
|
||||
try:
|
||||
self.app.vna.disconnect()
|
||||
except IOError as exc:
|
||||
logger.error("Unable to disconnect from VNA: %s", exc)
|
||||
|
||||
self.interface.close()
|
||||
self.btn_toggle.setText("Connect to device")
|
||||
self.btn_toggle.repaint()
|
||||
|
|
|
@ -38,6 +38,7 @@ from NanoVNASaver.Hardware.Serial import Interface, drain_serial
|
|||
from NanoVNASaver.Hardware.SV4401A import SV4401A
|
||||
from NanoVNASaver.Hardware.SV6301A import SV6301A
|
||||
from NanoVNASaver.Hardware.TinySA import TinySA, TinySA_Ultra
|
||||
from NanoVNASaver.Hardware.LiteVNA64 import LiteVNA64
|
||||
from NanoVNASaver.Hardware.VNA import VNA
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
@ -67,6 +68,7 @@ NAME2DEVICE = {
|
|||
"JNCRadio": JNCRadio_VNA_3G,
|
||||
"SV4401A": SV4401A,
|
||||
"SV6301A": SV6301A,
|
||||
"LiteVNA64": LiteVNA64,
|
||||
"Unknown": NanoVNA,
|
||||
}
|
||||
|
||||
|
@ -149,6 +151,8 @@ def get_comment(iface: Interface) -> str:
|
|||
vna_version = detect_version(iface)
|
||||
if vna_version == "v2":
|
||||
return "S-A-A-2"
|
||||
elif vna_version == "lite_vna_64":
|
||||
return "LiteVNA64"
|
||||
|
||||
logger.info("Finding firmware variant...")
|
||||
info = get_info(iface)
|
||||
|
@ -191,7 +195,7 @@ def detect_version(serial_port: serial.Serial) -> str:
|
|||
if data.startswith("\r\n?\r\nch> "):
|
||||
return "vh"
|
||||
if data.startswith("2"):
|
||||
return "v2"
|
||||
return "lite_vna_64" if LiteVNA64.is_lite_vna_64(serial_port) else "v2"
|
||||
logger.debug("Retry detection: %s", i + 1)
|
||||
logger.error("No VNA detected. Hardware responded to CR with: %s", data)
|
||||
return ""
|
||||
|
|
|
@ -31,7 +31,7 @@ class JNCRadio_VNA_3G(NanoVNA):
|
|||
name = "JNCRadio_VNA_3G"
|
||||
screenwidth = 800
|
||||
screenheight = 480
|
||||
valid_datapoints = (501, 11, 101, 1001)
|
||||
valid_datapoints = [501, 11, 101, 1001]
|
||||
sweep_points_min = 11
|
||||
sweep_points_max = 1001
|
||||
|
||||
|
|
|
@ -0,0 +1,275 @@
|
|||
import logging
|
||||
import platform
|
||||
from struct import iter_unpack, pack, unpack
|
||||
from time import sleep
|
||||
|
||||
from PyQt6.QtGui import QImage, QPixmap, QColor
|
||||
from serial import Serial, SerialException
|
||||
|
||||
from NanoVNASaver.Hardware.Serial import Interface
|
||||
|
||||
from ..utils.version import Version
|
||||
from .NanoVNA_V2 import (
|
||||
_ADDR_DEVICE_VARIANT,
|
||||
_ADDR_FW_MAJOR,
|
||||
_ADDR_FW_MINOR,
|
||||
_ADDR_HARDWARE_REVISION,
|
||||
_ADDR_RAW_SAMPLES_MODE,
|
||||
_ADF4350_TXPOWER_DESC_MAP,
|
||||
_CMD_READ,
|
||||
_CMD_READ2,
|
||||
_CMD_WRITE,
|
||||
WRITE_SLEEP,
|
||||
NanoVNA_V2,
|
||||
)
|
||||
|
||||
if platform.system() != "Windows":
|
||||
pass
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
EXPECTED_HW_VERSION = Version.build(2, 2, 0)
|
||||
EXPECTED_FW_VERSION = Version.build(2, 2, 0)
|
||||
|
||||
|
||||
_ADDR_VBAT_MILIVOLTS = 0x5C
|
||||
_ADDR_SCREENSHOT = 0xEE
|
||||
|
||||
|
||||
SUPPORTED_PIXEL_FORMAT = 16
|
||||
|
||||
|
||||
class ScreenshotData:
|
||||
header_size = 2 + 2 + 1
|
||||
|
||||
def __init__(self, width: int, height: int, pixel_size: int):
|
||||
self.width = width
|
||||
self.height = height
|
||||
self.pixel_size = pixel_size
|
||||
self.data = bytes()
|
||||
|
||||
def data_size(self) -> int:
|
||||
return self.width * self.height * int(self.pixel_size / 8)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"{self.width}x{self.height} {self.pixel_size}bits ({self.data_size()} Bytes)"
|
||||
|
||||
@staticmethod
|
||||
def from_header(header_data: bytes) -> "ScreenshotData":
|
||||
logger.debug("Screenshot header: %s", header_data)
|
||||
|
||||
width, height, depth = unpack("<HHB", header_data)
|
||||
return ScreenshotData(width, height, depth)
|
||||
|
||||
@staticmethod
|
||||
def rgb565_to_888(rgb565: int) -> tuple[int, int, int]:
|
||||
# Extract red, green, and blue components
|
||||
r = (rgb565 & 0xF800) >> 11
|
||||
g = (rgb565 & 0x07E0) >> 5
|
||||
b = rgb565 & 0x001F
|
||||
|
||||
# Scale to 8-bit values
|
||||
r = (r * 527 + 23) >> 6
|
||||
g = (g * 259 + 33) >> 6
|
||||
b = (b * 527 + 23) >> 6
|
||||
|
||||
return r, g, b
|
||||
|
||||
def get_rgb888_data(self) -> bytes:
|
||||
result = bytearray()
|
||||
for rgb565 in iter_unpack(">H", self.data):
|
||||
result.extend(self.rgb565_to_888(rgb565[0]))
|
||||
|
||||
return bytes(result)
|
||||
|
||||
|
||||
class LiteVNA64(NanoVNA_V2):
|
||||
name = "LiteVNA-64"
|
||||
valid_datapoints = [
|
||||
51,
|
||||
101,
|
||||
201,
|
||||
401,
|
||||
801,
|
||||
1024,
|
||||
1601,
|
||||
3201,
|
||||
4501,
|
||||
6401,
|
||||
12801,
|
||||
25601,
|
||||
]
|
||||
screenwidth = 480
|
||||
screenheight = 320
|
||||
sweep_points_max = 65535
|
||||
sweep_max_freq_Hz = 6300e6
|
||||
|
||||
def __init__(self, iface: Interface):
|
||||
super().__init__(iface)
|
||||
|
||||
self.datapoints = 1024
|
||||
|
||||
def read_fw_version(self) -> Version:
|
||||
with self.serial.lock:
|
||||
return LiteVNA64._get_fw_revision_serial(self.serial)
|
||||
|
||||
def init_features(self) -> None:
|
||||
# VBat state will be added dynamicly in get_features()
|
||||
|
||||
self.features.add("Customizable data points")
|
||||
self.features.add("Screenshots")
|
||||
|
||||
# TODO: more than one dp per freq
|
||||
self.features.add("Multi data points")
|
||||
|
||||
# TODO review this part, which was copy-pasted from NanoVNA_V2
|
||||
self.features.add("Set Average")
|
||||
self.features.add("Set TX power partial")
|
||||
# Can only set ADF4350 power, i.e. for >= 140MHz
|
||||
# See https://groups.io/g/liteVNA/message/318 for more details
|
||||
self.txPowerRanges = [
|
||||
(
|
||||
(140e6, self.sweep_max_freq_Hz),
|
||||
[_ADF4350_TXPOWER_DESC_MAP[value] for value in (3, 2, 1, 0)],
|
||||
),
|
||||
]
|
||||
|
||||
def get_features(self) -> set[str]:
|
||||
result = set(self.features)
|
||||
result.add(f"Vbat: {self.read_vbat()}V")
|
||||
return result
|
||||
|
||||
def read_vbat(self) -> str:
|
||||
with self.serial.lock:
|
||||
cmd = pack("<BB", _CMD_READ2, _ADDR_VBAT_MILIVOLTS)
|
||||
|
||||
self.serial.write(cmd)
|
||||
sleep(WRITE_SLEEP)
|
||||
# in a more predictive way
|
||||
resp = self.serial.read(2)
|
||||
vbat = int.from_bytes(resp, "little") / 1000.0
|
||||
|
||||
logger.debug("Vbat: %sV", vbat)
|
||||
|
||||
return f"{vbat}"
|
||||
|
||||
@staticmethod
|
||||
def _get_major_minor_version_serial(
|
||||
cmd_major_version: int, cmd_minor_version: int, serial: Serial
|
||||
) -> Version:
|
||||
cmd = pack(
|
||||
"<BBBB", _CMD_READ, cmd_major_version, _CMD_READ, cmd_minor_version
|
||||
)
|
||||
|
||||
serial.write(cmd)
|
||||
sleep(WRITE_SLEEP)
|
||||
# in a more predictive way
|
||||
resp = serial.read(2)
|
||||
|
||||
if len(resp) != 2: # noqa: PLR2004
|
||||
logger.error("Timeout reading version registers. Got: %s", resp)
|
||||
raise IOError("Timeout reading version registers")
|
||||
return Version.build(resp[0], resp[1])
|
||||
|
||||
@staticmethod
|
||||
def _get_fw_revision_serial(serial: Serial) -> Version:
|
||||
result = LiteVNA64._get_major_minor_version_serial(
|
||||
_ADDR_FW_MAJOR, _ADDR_FW_MINOR, serial
|
||||
)
|
||||
logger.debug("Firmware version: %s", result)
|
||||
return result
|
||||
|
||||
@staticmethod
|
||||
def _get_hw_revision_serial(serial: Serial) -> Version:
|
||||
result = LiteVNA64._get_major_minor_version_serial(
|
||||
_ADDR_DEVICE_VARIANT, _ADDR_HARDWARE_REVISION, serial
|
||||
)
|
||||
logger.debug(
|
||||
"Hardware version ({device_variant}.{hardware_revision}): %s",
|
||||
result,
|
||||
)
|
||||
return result
|
||||
|
||||
@staticmethod
|
||||
def is_lite_vna_64(serial: Serial) -> bool:
|
||||
hw_version = LiteVNA64._get_hw_revision_serial(serial)
|
||||
fw_version = LiteVNA64._get_fw_revision_serial(serial)
|
||||
return (
|
||||
hw_version == EXPECTED_HW_VERSION
|
||||
and fw_version == EXPECTED_FW_VERSION
|
||||
)
|
||||
|
||||
def disconnect(self):
|
||||
self._exit_usb_mode()
|
||||
super().disconnect()
|
||||
|
||||
def _exit_usb_mode(self) -> None:
|
||||
with self.serial.lock:
|
||||
self.serial.write(
|
||||
pack("<BBB", _CMD_WRITE, _ADDR_RAW_SAMPLES_MODE, 2)
|
||||
)
|
||||
sleep(WRITE_SLEEP)
|
||||
|
||||
def readValues(self, value) -> list[complex]:
|
||||
result = super().readValues(value)
|
||||
self._exit_usb_mode()
|
||||
return result
|
||||
|
||||
def setSweep(self, start, stop):
|
||||
# Device loose these value after going to idle mode
|
||||
# Do not try to cache them locally
|
||||
step = (stop - start) / (self.datapoints - 1)
|
||||
self.sweepStartHz = start
|
||||
self.sweepStepHz = step
|
||||
logger.info(
|
||||
"NanoVNAV2: set sweep start %d step %d",
|
||||
self.sweepStartHz,
|
||||
self.sweepStepHz,
|
||||
)
|
||||
self._updateSweep()
|
||||
|
||||
def getScreenshot(self) -> QPixmap:
|
||||
logger.debug("Capturing screenshot...")
|
||||
self.serial.timeout = 8
|
||||
if self.connected():
|
||||
try:
|
||||
screenshot = self._get_screenshot()
|
||||
|
||||
if screenshot.pixel_size != SUPPORTED_PIXEL_FORMAT:
|
||||
logger.warning(
|
||||
"Unsupported %d screenshot pixel format!",
|
||||
screenshot.pixel_size,
|
||||
)
|
||||
return QPixmap()
|
||||
|
||||
image = QImage(
|
||||
screenshot.get_rgb888_data(),
|
||||
screenshot.width,
|
||||
screenshot.height,
|
||||
QImage.Format.Format_RGB888,
|
||||
)
|
||||
|
||||
logger.debug("Screenshot was captured")
|
||||
return QPixmap(image)
|
||||
except SerialException as exc:
|
||||
logger.exception(
|
||||
"Exception while capturing screenshot: %s", exc
|
||||
)
|
||||
|
||||
logger.debug("Unable to get screenshot")
|
||||
return QPixmap()
|
||||
|
||||
def _get_screenshot(self) -> ScreenshotData:
|
||||
with self.serial.lock:
|
||||
self.serial.write(pack("<BBB", _CMD_WRITE, _ADDR_SCREENSHOT, 0))
|
||||
sleep(WRITE_SLEEP)
|
||||
|
||||
result = ScreenshotData.from_header(
|
||||
self.serial.read(ScreenshotData.header_size)
|
||||
)
|
||||
|
||||
logger.debug("Screenshot format: %s. Loading data...", result)
|
||||
|
||||
result.data = self.serial.read(result.data_size())
|
||||
|
||||
return result
|
|
@ -25,7 +25,7 @@ from PyQt6.QtGui import QImage, QPixmap
|
|||
|
||||
from NanoVNASaver.Hardware.Serial import Interface, drain_serial
|
||||
from NanoVNASaver.Hardware.VNA import VNA
|
||||
from NanoVNASaver.Version import Version
|
||||
from ..utils import Version
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
@ -38,7 +38,7 @@ class NanoVNA(VNA):
|
|||
def __init__(self, iface: Interface):
|
||||
super().__init__(iface)
|
||||
self.sweep_method = "sweep"
|
||||
self.read_features()
|
||||
self.init_features()
|
||||
logger.debug("Setting initial start,stop")
|
||||
self.start, self.stop = self._get_running_frequencies()
|
||||
self.sweep_max_freq_Hz = 300e6
|
||||
|
@ -111,13 +111,13 @@ class NanoVNA(VNA):
|
|||
elif self.sweep_method == "scan":
|
||||
list(self.exec_command(f"scan {start} {stop} {self.datapoints}"))
|
||||
|
||||
def read_features(self):
|
||||
super().read_features()
|
||||
if self.version >= Version("0.7.1"):
|
||||
def init_features(self) -> None:
|
||||
super().init_features()
|
||||
if self.version >= Version.parse("0.7.1"):
|
||||
logger.debug("Using scan mask command.")
|
||||
self.features.add("Scan mask command")
|
||||
self.sweep_method = "scan_mask"
|
||||
elif self.version >= Version("0.2.0"):
|
||||
elif self.version >= Version.parse("0.2.0"):
|
||||
logger.debug("Using new scan command.")
|
||||
self.features.add("Scan command")
|
||||
self.sweep_method = "scan"
|
||||
|
|
|
@ -27,7 +27,7 @@ class NanoVNA_H4(NanoVNA_H):
|
|||
name = "NanoVNA-H4"
|
||||
screenwidth = 480
|
||||
screenheight = 320
|
||||
valid_datapoints = (101, 11, 51, 201, 401)
|
||||
valid_datapoints = [101, 11, 51, 201, 401]
|
||||
|
||||
def __init__(self, iface: Interface):
|
||||
super().__init__(iface)
|
||||
|
@ -42,4 +42,4 @@ class NanoVNA_H4(NanoVNA_H):
|
|||
# if self.readFirmware().find("DiSlord") > 0:
|
||||
# self.features.add("Customizable data points")
|
||||
# logger.info("VNA has 201 datapoints capability")
|
||||
# self.valid_datapoints = (201, 11, 51,101)
|
||||
# self.valid_datapoints = [201, 11, 51, 101]
|
||||
|
|
|
@ -23,7 +23,7 @@ from time import sleep
|
|||
|
||||
from NanoVNASaver.Hardware.Serial import Interface
|
||||
from NanoVNASaver.Hardware.VNA import VNA
|
||||
from NanoVNASaver.Version import Version
|
||||
from ..utils import Version
|
||||
|
||||
if platform.system() != "Windows":
|
||||
import tty
|
||||
|
@ -69,7 +69,7 @@ _ADF4350_TXPOWER_DESC_REV_MAP = {
|
|||
|
||||
class NanoVNA_V2(VNA): # noqa: N801
|
||||
name = "NanoVNA-V2"
|
||||
valid_datapoints = (101, 11, 51, 201, 301, 501, 1023)
|
||||
valid_datapoints = [101, 11, 51, 201, 301, 501, 1023]
|
||||
screenwidth = 320
|
||||
screenheight = 240
|
||||
|
||||
|
@ -89,7 +89,7 @@ class NanoVNA_V2(VNA): # noqa: N801
|
|||
raise IOError("Device is in DFU mode")
|
||||
|
||||
if "S21 hack" in self.features:
|
||||
self.valid_datapoints = (101, 11, 51, 201, 301, 501, 1021)
|
||||
self.valid_datapoints = [101, 11, 51, 201, 301, 501, 1021]
|
||||
|
||||
self.sweepStartHz = 200e6
|
||||
self.sweepStepHz = 1e6
|
||||
|
@ -100,19 +100,19 @@ class NanoVNA_V2(VNA): # noqa: N801
|
|||
def getCalibration(self) -> str:
|
||||
return "Unknown"
|
||||
|
||||
def read_features(self):
|
||||
def init_features(self) -> None:
|
||||
self.features.add("Customizable data points")
|
||||
# TODO: more than one dp per freq
|
||||
self.features.add("Multi data points")
|
||||
self.board_revision = self.read_board_revision()
|
||||
if self.board_revision >= Version("2.0.4"):
|
||||
if self.board_revision >= Version.parse("2.0.4"):
|
||||
self.sweep_max_freq_Hz = 4400e6
|
||||
else:
|
||||
self.sweep_max_freq_Hz = 3000e6
|
||||
if self.version <= Version("1.0.1"):
|
||||
if self.version <= Version.parse("1.0.1"):
|
||||
logger.debug("Hack for s21 oddity in first sweeppoint")
|
||||
self.features.add("S21 hack")
|
||||
if self.version >= Version("1.0.2"):
|
||||
if self.version >= Version.parse("1.0.2"):
|
||||
self.features.update({"Set TX power partial", "Set Average"})
|
||||
# Can only set ADF4350 power, i.e. for >= 140MHz
|
||||
self.txPowerRanges = [
|
||||
|
@ -227,7 +227,7 @@ class NanoVNA_V2(VNA): # noqa: N801
|
|||
def resetSweep(self, start: int, stop: int):
|
||||
self.setSweep(start, stop)
|
||||
|
||||
def _read_version(self, cmd_0: int, cmd_1: int):
|
||||
def _read_version(self, cmd_0: int, cmd_1: int) -> Version:
|
||||
cmd = pack("<BBBB", _CMD_READ, cmd_0, _CMD_READ, cmd_1)
|
||||
with self.serial.lock:
|
||||
self.serial.write(cmd)
|
||||
|
@ -238,14 +238,14 @@ class NanoVNA_V2(VNA): # noqa: N801
|
|||
if len(resp) != 2: # noqa: PLR2004
|
||||
logger.error("Timeout reading version registers. Got: %s", resp)
|
||||
raise IOError("Timeout reading version registers")
|
||||
return Version(f"{resp[0]}.0.{resp[1]}")
|
||||
return Version.build(resp[0], 0, resp[1])
|
||||
|
||||
def readVersion(self) -> "Version":
|
||||
def read_fw_version(self) -> Version:
|
||||
result = self._read_version(_ADDR_FW_MAJOR, _ADDR_FW_MINOR)
|
||||
logger.debug("readVersion: %s", result)
|
||||
logger.debug("Firmware Version: %s", result)
|
||||
return result
|
||||
|
||||
def read_board_revision(self) -> "Version":
|
||||
def read_board_revision(self) -> Version:
|
||||
result = self._read_version(
|
||||
_ADDR_DEVICE_VARIANT, _ADDR_HARDWARE_REVISION
|
||||
)
|
||||
|
|
|
@ -31,7 +31,7 @@ class SV4401A(NanoVNA):
|
|||
name = "SV4401A"
|
||||
screenwidth = 1024
|
||||
screenheight = 600
|
||||
valid_datapoints = (501, 101, 1001)
|
||||
valid_datapoints = [501, 101, 1001]
|
||||
sweep_points_min = 101
|
||||
sweep_points_max = 1001
|
||||
|
||||
|
|
|
@ -31,7 +31,7 @@ class SV6301A(NanoVNA):
|
|||
name = "SV6301A"
|
||||
screenwidth = 1024
|
||||
screenheight = 600
|
||||
valid_datapoints = (501, 101, 1001)
|
||||
valid_datapoints = [501, 101, 1001]
|
||||
sweep_points_min = 101
|
||||
sweep_points_max = 1001
|
||||
|
||||
|
|
|
@ -17,7 +17,7 @@
|
|||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
import logging
|
||||
from threading import Lock
|
||||
from threading import RLock
|
||||
|
||||
import serial
|
||||
|
||||
|
@ -47,7 +47,7 @@ class Interface(serial.Serial):
|
|||
self.port = None
|
||||
self.baudrate = 115200
|
||||
self.timeout = 0.05
|
||||
self.lock = Lock()
|
||||
self.lock = RLock()
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.port} ({self.comment})"
|
||||
|
|
|
@ -25,7 +25,7 @@ from PyQt6.QtGui import QImage, QPixmap
|
|||
|
||||
from NanoVNASaver.Hardware.Serial import Interface, drain_serial
|
||||
from NanoVNASaver.Hardware.VNA import VNA
|
||||
from NanoVNASaver.Version import Version
|
||||
from ..utils import Version
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
@ -34,7 +34,7 @@ class TinySA(VNA):
|
|||
name = "tinySA"
|
||||
screenwidth = 320
|
||||
screenheight = 240
|
||||
valid_datapoints = (290,)
|
||||
valid_datapoints = [290]
|
||||
|
||||
def __init__(self, iface: Interface):
|
||||
super().__init__(iface)
|
||||
|
@ -132,7 +132,7 @@ class TinySA_Ultra(TinySA): # noqa: N801
|
|||
name = "tinySA Ultra"
|
||||
screenwidth = 480
|
||||
screenheight = 320
|
||||
valid_datapoints = (450, 51, 101, 145, 290)
|
||||
valid_datapoints = [450, 51, 101, 145, 290]
|
||||
hardware_revision = None
|
||||
|
||||
def __init__(self, iface: Interface):
|
||||
|
|
|
@ -23,7 +23,7 @@ from typing import Iterator
|
|||
from PyQt6 import QtGui
|
||||
|
||||
from NanoVNASaver.Hardware.Serial import Interface, drain_serial
|
||||
from NanoVNASaver.Version import Version
|
||||
from ..utils import Version
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
@ -53,27 +53,29 @@ def _max_retries(bandwidth: int, datapoints: int) -> int:
|
|||
|
||||
class VNA:
|
||||
name = "VNA"
|
||||
valid_datapoints = (101, 51, 11)
|
||||
valid_datapoints: list[int] = [101, 51, 11]
|
||||
wait = 0.05
|
||||
SN = "NOT SUPPORTED"
|
||||
sweep_points_max = 101
|
||||
sweep_points_min = 11
|
||||
|
||||
# Must be initilized in child classes
|
||||
sweep_max_freq_Hz = 0.0
|
||||
|
||||
def __init__(self, iface: Interface):
|
||||
self.serial = iface
|
||||
self.version = Version("0.0.0")
|
||||
self.features = set()
|
||||
self.version = Version.parse("0.0.0")
|
||||
self.features: set[str] = set()
|
||||
self.validateInput = False
|
||||
self.datapoints = self.valid_datapoints[0]
|
||||
self.bandwidth = 1000
|
||||
self.bw_method = "ttrftech"
|
||||
self.sweep_max_freq_Hz = None
|
||||
# [((min_freq, max_freq), [description]]. Order by increasing
|
||||
# [((min_freq, max_freq), [description])]. Order by increasing
|
||||
# frequency. Put default output power first.
|
||||
self.txPowerRanges = []
|
||||
self.txPowerRanges: list[tuple[tuple[float, float], list[str]]] = []
|
||||
if self.connected():
|
||||
self.version = self.readVersion()
|
||||
self.read_features()
|
||||
self.version = self.read_fw_version()
|
||||
self.init_features()
|
||||
logger.debug("Features: %s", self.features)
|
||||
# cannot read current bandwidth, so set to highest
|
||||
# to get initial sweep fast
|
||||
|
@ -121,7 +123,7 @@ class VNA:
|
|||
break
|
||||
yield line
|
||||
|
||||
def read_features(self):
|
||||
def init_features(self) -> None:
|
||||
result = " ".join(self.exec_command("help")).split()
|
||||
logger.debug("result:\n%s", result)
|
||||
if "capture" in result:
|
||||
|
@ -176,7 +178,7 @@ class VNA:
|
|||
def connected(self) -> bool:
|
||||
return self.serial.is_open
|
||||
|
||||
def getFeatures(self) -> set[str]:
|
||||
def get_features(self) -> set[str]:
|
||||
return self.features
|
||||
|
||||
def getCalibration(self) -> str:
|
||||
|
@ -208,10 +210,10 @@ class VNA:
|
|||
logger.debug("VNA done reading %s (%d values)", value, len(result))
|
||||
return result
|
||||
|
||||
def readVersion(self) -> "Version":
|
||||
def read_fw_version(self) -> Version:
|
||||
result = list(self.exec_command("version"))
|
||||
logger.debug("result:\n%s", result)
|
||||
return Version(result[0])
|
||||
logger.debug("Firmware Version:\n%s", result)
|
||||
return Version.parse(result[0])
|
||||
|
||||
def setSweep(self, start, stop):
|
||||
list(self.exec_command(f"sweep {start} {stop} {self.datapoints}"))
|
||||
|
|
|
@ -702,6 +702,9 @@ class NanoVNASaver(QWidget):
|
|||
Defaults.cfg.gui.splitter_sizes = self.splitter.saveState()
|
||||
Defaults.store(self.settings, Defaults.cfg)
|
||||
|
||||
# Dosconnect connected devices and release serial port
|
||||
self.serial_control.disconnect_device()
|
||||
|
||||
a0.accept()
|
||||
sys.exit()
|
||||
|
||||
|
|
|
@ -26,7 +26,7 @@ from urllib import error, request
|
|||
from PyQt6 import QtCore, QtGui, QtWidgets
|
||||
|
||||
from NanoVNASaver.About import INFO_URL, LATEST_URL, TAGS_KEY, TAGS_URL
|
||||
from NanoVNASaver.Version import Version
|
||||
from ..utils import Version
|
||||
from NanoVNASaver.Windows.Defaults import make_scrollable
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
@ -147,7 +147,7 @@ class AboutWindow(QtWidgets.QWidget):
|
|||
line = ln.decode("utf-8")
|
||||
found_latest_version = TAGS_KEY in line
|
||||
if found_latest_version:
|
||||
latest_version = Version(
|
||||
latest_version = Version.parse(
|
||||
re.search(r"(\d+\.\d+\.\d+)", line).group()
|
||||
)
|
||||
break
|
||||
|
|
|
@ -150,7 +150,7 @@ class DeviceSettingsWindow(QtWidgets.QWidget):
|
|||
self.label["calibration"].setText(self.app.vna.getCalibration())
|
||||
self.label["SN"].setText(self.app.vna.SN)
|
||||
self.featureList.clear()
|
||||
features = self.app.vna.getFeatures()
|
||||
features = self.app.vna.get_features()
|
||||
for item in features:
|
||||
self.featureList.addItem(item)
|
||||
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
from .version import Version
|
||||
|
||||
__all__ = ["Version"]
|
|
@ -34,24 +34,39 @@ _RXP = re.compile(
|
|||
)
|
||||
|
||||
|
||||
class _Version(typing.NamedTuple):
|
||||
class Version(typing.NamedTuple):
|
||||
"""Represents four components version: MAJOR.MAIN.REV-NOTE
|
||||
Plrease note:
|
||||
- `-NOTE` part is optional
|
||||
- please use #parse() or #build() hepler methods to prepare right version
|
||||
"""
|
||||
|
||||
major: int
|
||||
minor: int
|
||||
revision: int
|
||||
note: str
|
||||
|
||||
@staticmethod
|
||||
def parse(vstring: str = "0.0.0") -> "Version":
|
||||
if (match := _RXP.search(vstring)) is None:
|
||||
logger.error("Unable to parse version: %s", vstring)
|
||||
return Version(0, 0, 0, "")
|
||||
|
||||
return Version(
|
||||
int(match.group("major")),
|
||||
int(match.group("minor")),
|
||||
int(match.group("revision") or "0"),
|
||||
match.group("note"),
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def build(
|
||||
major: int, minor: int, revision: int = 0, note: str = ""
|
||||
) -> "Version":
|
||||
return Version(major, minor, revision, note)
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"{self.major}.{self.minor}" f".{self.revision}{self.note}"
|
||||
return f"{self.major}.{self.minor}.{self.revision}{self.note}"
|
||||
|
||||
|
||||
def Version(vstring: str = "0.0.0") -> "_Version":
|
||||
if (match := _RXP.search(vstring)) is None:
|
||||
logger.error("Unable to parse version: %s", vstring)
|
||||
return _Version(0, 0, 0, "")
|
||||
|
||||
return _Version(
|
||||
int(match.group("major")),
|
||||
int(match.group("minor")),
|
||||
int(match.group("revision") or "0"),
|
||||
match.group("note"),
|
||||
)
|
||||
def __repr__(self) -> str:
|
||||
return f"{self.major}.{self.minor}.{self.revision}{self.note}"
|
|
@ -0,0 +1,41 @@
|
|||
|
||||
from NanoVNASaver.Hardware.LiteVNA64 import ScreenshotData
|
||||
|
||||
VALID_HEADER = b'\xe0\x01@\x01\x10'
|
||||
|
||||
class TestScreenshotData:
|
||||
|
||||
@staticmethod
|
||||
def test_from_header() -> None:
|
||||
result = ScreenshotData.from_header(VALID_HEADER)
|
||||
|
||||
assert result.width == 480
|
||||
assert result.height == 320
|
||||
assert result.pixel_size == 16
|
||||
assert len(result.data) == 0
|
||||
|
||||
@staticmethod
|
||||
def test_data_size() -> None:
|
||||
assert ScreenshotData(0,0,0).data_size() == 0
|
||||
assert ScreenshotData(480,320,16).data_size() == 307200
|
||||
|
||||
@staticmethod
|
||||
def test_repr() -> None:
|
||||
assert f"{ScreenshotData(0,0,0)}" == "0x0 0bits (0 Bytes)"
|
||||
assert f"{ScreenshotData(480,320,16)}" == "480x320 16bits (307200 Bytes)"
|
||||
|
||||
@staticmethod
|
||||
def test_rgb565_to_888() -> None:
|
||||
assert ScreenshotData.rgb565_to_888(0x0000) == (0x00, 0x00, 0x00)
|
||||
assert ScreenshotData.rgb565_to_888(0xFFE0) == (0xFF, 0xFF, 0x00)
|
||||
assert ScreenshotData.rgb565_to_888(0xFFFF) == (0xFF, 0xFF, 0xFF)
|
||||
|
||||
@staticmethod
|
||||
def test_get_rgb888_data() -> None:
|
||||
img = ScreenshotData(0, 0, 0)
|
||||
img.data = b"\x00\x00\xff\xe0\xff\xff"
|
||||
|
||||
result = img.get_rgb888_data()
|
||||
|
||||
assert len(result) == 3 *3
|
||||
assert result == b'\x00\x00\x00\xFF\xFF\x00\xFF\xFF\xFF'
|
|
@ -1,41 +0,0 @@
|
|||
# NanoVNASaver
|
||||
#
|
||||
# A python program to view and export Touchstone data from a NanoVNA
|
||||
# Copyright (C) 2019, 2020 Rune B. Broberg
|
||||
# Copyright (C) 2020,2021 NanoVNA-Saver Authors
|
||||
#
|
||||
# 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 <https://www.gnu.org/licenses/>.
|
||||
import unittest
|
||||
|
||||
# Import targets to be tested
|
||||
from NanoVNASaver.Version import Version
|
||||
|
||||
|
||||
class TestCases(unittest.TestCase):
|
||||
|
||||
def test_version(self):
|
||||
ver = Version("v1.2.3-test")
|
||||
self.assertEqual(str(ver), "1.2.3-test")
|
||||
self.assertLessEqual(ver, Version("1.2.4"))
|
||||
self.assertFalse(ver > Version("1.2.4"))
|
||||
self.assertFalse(ver > Version("1.2.3-u"))
|
||||
self.assertTrue(Version("1.2.4") >= ver)
|
||||
self.assertTrue(ver < Version("1.2.4"))
|
||||
self.assertFalse(Version("0.0.0") == Version("0.0.0-rc"))
|
||||
self.assertEqual(ver.major, 1)
|
||||
self.assertEqual(ver.minor, 2)
|
||||
self.assertEqual(ver.revision, 3)
|
||||
self.assertEqual(ver.note, "-test")
|
||||
Version("asdasd")
|
||||
Version("1.2.invalid")
|
|
@ -0,0 +1,74 @@
|
|||
# NanoVNASaver
|
||||
#
|
||||
# A python program to view and export Touchstone data from a NanoVNA
|
||||
# Copyright (C) 2019, 2020 Rune B. Broberg
|
||||
# Copyright (C) 2020,2021 NanoVNA-Saver Authors
|
||||
#
|
||||
# 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 <https://www.gnu.org/licenses/>.
|
||||
|
||||
# Import targets to be tested
|
||||
from NanoVNASaver.utils import Version
|
||||
|
||||
|
||||
class TestCases:
|
||||
|
||||
@staticmethod
|
||||
def test_str() -> None:
|
||||
assert str(Version(1, 0, 0, "")) == "1.0.0"
|
||||
assert str(Version(1, 2, 0, "")) == "1.2.0"
|
||||
assert str(Version(1, 2, 3, "")) == "1.2.3"
|
||||
assert str(Version(1, 2, 3, "-test")) == "1.2.3-test"
|
||||
|
||||
@staticmethod
|
||||
def test_repr() -> None:
|
||||
ver = Version(1, 2, 3, "-test")
|
||||
|
||||
assert f"{ver}" == "1.2.3-test"
|
||||
|
||||
@staticmethod
|
||||
def test_parse_normal_case() -> None:
|
||||
# At least major and minot components must be specified
|
||||
assert Version.parse("v1.2") == Version(1, 2, 0, "")
|
||||
assert Version.parse("v1.2.3") == Version(1, 2, 3, "")
|
||||
assert Version.parse("v1.2.3-test") == Version(1, 2, 3, "-test")
|
||||
|
||||
# At least major and minot components must be specified
|
||||
assert Version.parse("1.2") == Version(1, 2, 0, "")
|
||||
assert Version.parse("1.2.3") == Version(1, 2, 3, "")
|
||||
assert Version.parse("1.2.3-test") == Version(1, 2, 3, "-test")
|
||||
|
||||
@staticmethod
|
||||
def test_parse_invalid_values() -> None:
|
||||
assert Version.parse("asdasd") == Version(0, 0, 0, "")
|
||||
assert Version.parse("1.2.invalid") == Version(1, 2, 0, "invalid")
|
||||
|
||||
# At least major and minot components must be specified
|
||||
assert Version.parse("v1") == Version(0, 0, 0, "")
|
||||
assert Version.parse("1") == Version(0, 0, 0, "")
|
||||
|
||||
@staticmethod
|
||||
def test_build_normal_case() -> None:
|
||||
|
||||
assert Version.build(1, 2) == Version(1, 2, 0, "")
|
||||
assert Version.build(1, 2, 3) == Version(1, 2, 3, "")
|
||||
assert Version.build(1, 2, 3, "test") == Version(1, 2, 3, "test")
|
||||
|
||||
@staticmethod
|
||||
def test_comparation() -> None:
|
||||
assert Version(1, 2, 3, "test") > Version(1, 2, 3, "")
|
||||
assert Version(1, 2, 3, "test") < Version(1, 2, 4, "")
|
||||
|
||||
assert Version(1, 2, 3, "test") <= Version(1, 2, 4, "u")
|
||||
|
||||
assert Version(0, 0, 0, "0") != Version(0, 0, 0, "-rc")
|
Ładowanie…
Reference in New Issue