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 platform
21from collections import namedtuple
22from time import sleep
23from typing import List
24
25import serial
26from serial.tools import list_ports
27
28from NanoVNASaver.Hardware.AVNA import AVNA
29from NanoVNASaver.Hardware.NanoVNA import NanoVNA
30from NanoVNASaver.Hardware.NanoVNA_F import NanoVNA_F
31from NanoVNASaver.Hardware.NanoVNA_F_V2 import NanoVNA_F_V2
32from NanoVNASaver.Hardware.NanoVNA_H import NanoVNA_H
33from NanoVNASaver.Hardware.NanoVNA_H4 import NanoVNA_H4
34from NanoVNASaver.Hardware.NanoVNA_V2 import NanoVNA_V2
35from NanoVNASaver.Hardware.Serial import drain_serial, Interface
36from NanoVNASaver.Hardware.Sysctl import usb_vid_pid
37
38
39logger = logging.getLogger(__name__)
40
41USBDevice = namedtuple("Device", "vid pid name")
42
43USBDEVICETYPES = (
44    USBDevice(0x0483, 0x5740, "NanoVNA"),
45    USBDevice(0x16c0, 0x0483, "AVNA"),
46    USBDevice(0x04b4, 0x0008, "S-A-A-2"),
47)
48RETRIES = 3
49TIMEOUT = 0.2
50WAIT = 0.05
51
52# The USB Driver for NanoVNA V2 seems to deliver an
53# incompatible hardware info like:
54# 'PORTS\\VID_04B4&PID_0008\\DEMO'
55# This function will fix it.
56def _fix_v2_hwinfo(dev):
57    if dev.hwid == r'PORTS\VID_04B4&PID_0008\DEMO':
58        dev.vid, dev.pid = 0x04b4, 0x0008
59    return dev
60
61
62# Get list of interfaces with VNAs connected
63def get_interfaces() -> List[Interface]:
64    interfaces = []
65    # serial like usb interfaces
66    for d in list_ports.comports():
67        if platform.system() == 'FreeBSD':
68            logger.debug("Found FreeBSD USB port %s", d.device)
69            vid_pid = usb_vid_pid(d.device)
70            d.vid = vid_pid[0]
71            d.pid = vid_pid[1]
72
73        for t in USBDEVICETYPES:
74            if d.vid != t.vid or d.pid != t.pid:
75                continue
76            logger.debug("Found %s USB:(%04x:%04x) on port %s",
77                         t.name, d.vid, d.pid, d.device)
78            iface = Interface('serial', t.name)
79            iface.port = d.device
80            interfaces.append(iface)
81    return interfaces
82
83def get_VNA(iface: Interface) -> 'VNA':
84    # serial_port.timeout = TIMEOUT
85
86    logger.info("Finding correct VNA type...")
87    with iface.lock:
88        vna_version = detect_version(iface)
89
90    if vna_version == 'v2':
91        logger.info("Type: NanoVNA-V2")
92        return NanoVNA_V2(iface)
93
94    logger.info("Finding firmware variant...")
95    info = get_info(iface)
96    if info.find("AVNA + Teensy") >= 0:
97        logger.info("Type: AVNA")
98        return AVNA(iface)
99    if info.find("NanoVNA-H 4") >= 0:
100        logger.info("Type: NanoVNA-H4")
101        vna = NanoVNA_H4(iface)
102        return vna
103    if info.find("NanoVNA-H") >= 0:
104        logger.info("Type: NanoVNA-H")
105        vna = NanoVNA_H(iface)
106        return vna
107    if info.find("NanoVNA-F_V2") >= 0:
108        logger.info("Type: NanoVNA-F_V2")
109        return NanoVNA_F_V2(iface)
110    if info.find("NanoVNA-F") >= 0:
111        logger.info("Type: NanoVNA-F")
112        return NanoVNA_F(iface)
113    if info.find("NanoVNA") >= 0:
114        logger.info("Type: Generic NanoVNA")
115        return NanoVNA(iface)
116    logger.warning("Did not recognize NanoVNA type from firmware.")
117    return NanoVNA(iface)
118
119def detect_version(serial_port: serial.Serial) -> str:
120    data = ""
121    for i in range(RETRIES):
122        drain_serial(serial_port)
123        serial_port.write("\r".encode("ascii"))
124        sleep(0.05)
125        data = serial_port.read(128).decode("ascii")
126        if data.startswith("ch> "):
127            return "v1"
128        # -H versions
129        if data.startswith("\r\nch> "):
130            return "vh"
131        if data.startswith("2"):
132            return "v2"
133        logger.debug("Retry detection: %s", i + 1)
134    logger.error('No VNA detected. Hardware responded to CR with: %s', data)
135    return ""
136
137def get_info(serial_port: serial.Serial) -> str:
138    for _ in range(RETRIES):
139        drain_serial(serial_port)
140        serial_port.write("info\r".encode("ascii"))
141        lines = []
142        retries = 0
143        while True:
144            line = serial_port.readline()
145            line = line.decode("ascii").strip()
146            if not line:
147                retries += 1
148                if retries > RETRIES:
149                    return ""
150                sleep(WAIT)
151                continue
152            if line == "info":  # suppress echo
153                continue
154            if line.startswith("ch>"):
155                logger.debug("Needed retries: %s", retries)
156                break
157            lines.append(line)
158        return "\n".join(lines)
159