kopia lustrzana https://github.com/NanoVNA-Saver/nanovna-saver
Merge branch 'upstream_Development' into feature/refactor_marker
commit
5c45f261cb
|
@ -48,6 +48,7 @@ class Chart(QtWidgets.QWidget):
|
|||
bands = None
|
||||
draggedMarker: Marker = None
|
||||
name = ""
|
||||
sweepTitle = ""
|
||||
drawLines = False
|
||||
minChartHeight = 200
|
||||
minChartWidth = 200
|
||||
|
@ -143,6 +144,10 @@ class Chart(QtWidgets.QWidget):
|
|||
self.markerSize = size
|
||||
self.update()
|
||||
|
||||
def setSweepTitle(self, title):
|
||||
self.sweepTitle = title
|
||||
self.update()
|
||||
|
||||
def getActiveMarker(self) -> Marker:
|
||||
if self.draggedMarker is not None:
|
||||
return self.draggedMarker
|
||||
|
@ -309,6 +314,15 @@ class Chart(QtWidgets.QWidget):
|
|||
number_y = y - self.markerSize - 3
|
||||
qp.drawText(number_x, number_y, str(number))
|
||||
|
||||
def drawTitle(self, qp: QtGui.QPainter, position: QtCore.QPoint = None):
|
||||
if self.sweepTitle != "":
|
||||
qp.setPen(self.textColor)
|
||||
if position is None:
|
||||
qf = QtGui.QFontMetricsF(self.font())
|
||||
width = qf.boundingRect(self.sweepTitle).width()
|
||||
position = QtCore.QPointF(self.width()/2 - width/2, 15)
|
||||
qp.drawText(position, self.sweepTitle)
|
||||
|
||||
|
||||
class FrequencyChart(Chart):
|
||||
fstart = 0
|
||||
|
@ -691,6 +705,16 @@ class FrequencyChart(Chart):
|
|||
qp.drawRect(rect)
|
||||
qp.end()
|
||||
|
||||
def drawChart(self, qp: QtGui.QPainter):
|
||||
qp.setPen(QtGui.QPen(self.textColor))
|
||||
qp.drawText(3, 15, self.name)
|
||||
qp.setPen(QtGui.QPen(self.foregroundColor))
|
||||
qp.drawLine(self.leftMargin, self.topMargin - 5,
|
||||
self.leftMargin, self.topMargin + self.chartHeight + 5)
|
||||
qp.drawLine(self.leftMargin-5, self.topMargin + self.chartHeight,
|
||||
self.leftMargin+self.chartWidth, self.topMargin + self.chartHeight)
|
||||
self.drawTitle(qp)
|
||||
|
||||
def drawFrequencyTicks(self, qp):
|
||||
fspan = self.fstop - self.fstart
|
||||
qp.setPen(self.textColor)
|
||||
|
@ -896,14 +920,6 @@ class PhaseChart(FrequencyChart):
|
|||
self.unwrap = unwrap
|
||||
self.update()
|
||||
|
||||
def drawChart(self, qp: QtGui.QPainter):
|
||||
qp.setPen(QtGui.QPen(self.textColor))
|
||||
qp.drawText(3, 15, self.name)
|
||||
qp.setPen(QtGui.QPen(self.foregroundColor))
|
||||
qp.drawLine(self.leftMargin, 20, self.leftMargin, self.topMargin + self.chartHeight + 5)
|
||||
qp.drawLine(self.leftMargin-5, self.topMargin+self.chartHeight,
|
||||
self.leftMargin + self.chartWidth, self.topMargin + self.chartHeight)
|
||||
|
||||
def drawValues(self, qp: QtGui.QPainter):
|
||||
if len(self.data) == 0 and len(self.reference) == 0:
|
||||
return
|
||||
|
@ -1055,15 +1071,6 @@ class VSWRChart(FrequencyChart):
|
|||
new_chart.logarithmicY = self.logarithmicY
|
||||
return new_chart
|
||||
|
||||
def drawChart(self, qp: QtGui.QPainter):
|
||||
qp.setPen(QtGui.QPen(self.textColor))
|
||||
qp.drawText(3, 15, self.name)
|
||||
qp.setPen(QtGui.QPen(self.foregroundColor))
|
||||
qp.drawLine(self.leftMargin, self.topMargin - 5,
|
||||
self.leftMargin, self.topMargin + self.chartHeight + 5)
|
||||
qp.drawLine(self.leftMargin-5, self.topMargin + self.chartHeight,
|
||||
self.leftMargin+self.chartWidth, self.topMargin + self.chartHeight)
|
||||
|
||||
def drawValues(self, qp: QtGui.QPainter):
|
||||
if len(self.data) == 0 and len(self.reference) == 0:
|
||||
return
|
||||
|
@ -1240,6 +1247,8 @@ class PolarChart(SquareChart):
|
|||
centerX - int(self.chartHeight / 2 * math.sin(math.pi / 4)),
|
||||
centerY + int(self.chartHeight / 2 * math.sin(math.pi / 4)))
|
||||
|
||||
self.drawTitle(qp)
|
||||
|
||||
def drawValues(self, qp: QtGui.QPainter):
|
||||
if len(self.data) == 0 and len(self.reference) == 0:
|
||||
return
|
||||
|
@ -1388,6 +1397,8 @@ class SmithChart(SquareChart):
|
|||
qp.drawArc(centerX - self.chartWidth*2, centerY,
|
||||
self.chartWidth*5, -self.chartHeight*5, int(-93.85 * 16), int(-18.85 * 16)) # Im(Z) = 0.2
|
||||
|
||||
self.drawTitle(qp)
|
||||
|
||||
qp.setPen(self.swrColor)
|
||||
for swr in self.swrMarkers:
|
||||
if swr <= 1:
|
||||
|
@ -1515,9 +1526,11 @@ class LogMagChart(FrequencyChart):
|
|||
qp.setPen(QtGui.QPen(self.textColor))
|
||||
qp.drawText(3, 15, self.name + " (dB)")
|
||||
qp.setPen(QtGui.QPen(self.foregroundColor))
|
||||
qp.drawLine(self.leftMargin, 20, self.leftMargin, self.topMargin+self.chartHeight+5)
|
||||
qp.drawLine(self.leftMargin, self.topMargin - 5,
|
||||
self.leftMargin, self.topMargin+self.chartHeight+5)
|
||||
qp.drawLine(self.leftMargin-5, self.topMargin+self.chartHeight,
|
||||
self.leftMargin+self.chartWidth, self.topMargin + self.chartHeight)
|
||||
self.drawTitle(qp)
|
||||
|
||||
def drawValues(self, qp: QtGui.QPainter):
|
||||
if len(self.data) == 0 and len(self.reference) == 0:
|
||||
|
@ -1714,7 +1727,8 @@ class SParameterChart(FrequencyChart):
|
|||
qp.drawText(10, 15, "Real")
|
||||
qp.drawText(self.leftMargin + self.chartWidth - 15, 15, "Imag")
|
||||
qp.setPen(QtGui.QPen(self.foregroundColor))
|
||||
qp.drawLine(self.leftMargin, 20, self.leftMargin, self.topMargin+self.chartHeight+5)
|
||||
qp.drawLine(self.leftMargin, self.topMargin - 5,
|
||||
self.leftMargin, self.topMargin+self.chartHeight+5)
|
||||
qp.drawLine(self.leftMargin-5, self.topMargin+self.chartHeight,
|
||||
self.leftMargin+self.chartWidth, self.topMargin + self.chartHeight)
|
||||
|
||||
|
@ -1890,7 +1904,8 @@ class CombinedLogMagChart(FrequencyChart):
|
|||
qp.drawText(10, 15, "S11")
|
||||
qp.drawText(self.leftMargin + self.chartWidth - 8, 15, "S21")
|
||||
qp.setPen(QtGui.QPen(self.foregroundColor))
|
||||
qp.drawLine(self.leftMargin, 20, self.leftMargin, self.topMargin+self.chartHeight+5)
|
||||
qp.drawLine(self.leftMargin, self.topMargin - 5,
|
||||
self.leftMargin, self.topMargin+self.chartHeight+5)
|
||||
qp.drawLine(self.leftMargin-5, self.topMargin+self.chartHeight,
|
||||
self.leftMargin+self.chartWidth, self.topMargin + self.chartHeight)
|
||||
|
||||
|
@ -2129,12 +2144,7 @@ class QualityFactorChart(FrequencyChart):
|
|||
self.setAutoFillBackground(True)
|
||||
|
||||
def drawChart(self, qp: QtGui.QPainter):
|
||||
qp.setPen(QtGui.QPen(self.textColor))
|
||||
qp.drawText(3, 15, self.name)
|
||||
qp.setPen(QtGui.QPen(self.foregroundColor))
|
||||
qp.drawLine(self.leftMargin, self.topMargin - 5, self.leftMargin, self.topMargin + self.chartHeight + 5)
|
||||
qp.drawLine(self.leftMargin-5, self.topMargin + self.chartHeight,
|
||||
self.leftMargin+self.chartWidth, self.topMargin + self.chartHeight)
|
||||
super().drawChart(qp)
|
||||
|
||||
# Make up some sensible scaling here
|
||||
if self.fixedValues:
|
||||
|
@ -2315,6 +2325,9 @@ class TDRChart(Chart):
|
|||
self.action_popout.triggered.connect(lambda: self.popoutRequested.emit(self))
|
||||
self.menu.addAction(self.action_popout)
|
||||
|
||||
self.chartWidth = self.width() - self.leftMargin - self.rightMargin
|
||||
self.chartHeight = self.height() - self.bottomMargin - self.topMargin
|
||||
|
||||
def contextMenuEvent(self, event):
|
||||
self.action_set_fixed_start.setText("Start (" + str(self.minDisplayLength) + ")")
|
||||
self.action_set_fixed_stop.setText("Stop (" + str(self.maxDisplayLength) + ")")
|
||||
|
@ -2405,6 +2418,27 @@ class TDRChart(Chart):
|
|||
if a0.buttons() == QtCore.Qt.RightButton:
|
||||
a0.ignore()
|
||||
return
|
||||
if a0.buttons() == QtCore.Qt.MiddleButton:
|
||||
# Drag the display
|
||||
a0.accept()
|
||||
if self.moveStartX != -1 and self.moveStartY != -1:
|
||||
dx = self.moveStartX - a0.x()
|
||||
dy = self.moveStartY - a0.y()
|
||||
self.zoomTo(self.leftMargin + dx, self.topMargin + dy,
|
||||
self.leftMargin + self.chartWidth + dx, self.topMargin + self.chartHeight + dy)
|
||||
|
||||
self.moveStartX = a0.x()
|
||||
self.moveStartY = a0.y()
|
||||
return
|
||||
if a0.modifiers() == QtCore.Qt.ControlModifier:
|
||||
# Dragging a box
|
||||
if not self.draggedBox:
|
||||
self.draggedBoxStart = (a0.x(), a0.y())
|
||||
self.draggedBoxCurrent = (a0.x(), a0.y())
|
||||
self.update()
|
||||
a0.accept()
|
||||
return
|
||||
|
||||
x = a0.x()
|
||||
absx = x - self.leftMargin
|
||||
if absx < 0 or absx > self.width() - self.rightMargin:
|
||||
|
@ -2440,6 +2474,8 @@ class TDRChart(Chart):
|
|||
|
||||
ticks = math.floor((self.width() - self.leftMargin)/100) # Number of ticks does not include the origin
|
||||
|
||||
self.drawTitle(qp)
|
||||
|
||||
if len(self.tdrWindow.td) > 0:
|
||||
if self.fixedSpan:
|
||||
max_index = np.searchsorted(self.tdrWindow.distance_axis, self.maxDisplayLength * 2)
|
||||
|
@ -2529,8 +2565,137 @@ class TDRChart(Chart):
|
|||
qp.drawText(marker_point.x() - 10, marker_point.y() - 5,
|
||||
str(round(self.tdrWindow.distance_axis[self.markerLocation] / 2, 2)) + "m")
|
||||
|
||||
if self.draggedBox and self.draggedBoxCurrent[0] != -1:
|
||||
dashed_pen = QtGui.QPen(self.foregroundColor, 1, QtCore.Qt.DashLine)
|
||||
qp.setPen(dashed_pen)
|
||||
top_left = QtCore.QPoint(self.draggedBoxStart[0], self.draggedBoxStart[1])
|
||||
bottom_right = QtCore.QPoint(self.draggedBoxCurrent[0], self.draggedBoxCurrent[1])
|
||||
rect = QtCore.QRect(top_left, bottom_right)
|
||||
qp.drawRect(rect)
|
||||
|
||||
qp.end()
|
||||
|
||||
def valueAtPosition(self, y):
|
||||
if len(self.tdrWindow.td) > 0:
|
||||
height = self.height() - self.topMargin - self.bottomMargin
|
||||
absy = (self.height() - y) - self.bottomMargin
|
||||
if self.fixedValues:
|
||||
min_impedance = self.minImpedance
|
||||
max_impedance = self.maxImpedance
|
||||
else:
|
||||
min_impedance = max(0, np.min(self.tdrWindow.step_response_Z) / 1.05)
|
||||
max_impedance = min(1000, np.max(self.tdrWindow.step_response_Z) * 1.05)
|
||||
y_step = (max_impedance - min_impedance) / height
|
||||
return y_step * absy + min_impedance
|
||||
else:
|
||||
return 0
|
||||
|
||||
def lengthAtPosition(self, x, limit=True):
|
||||
if len(self.tdrWindow.td) > 0:
|
||||
width = self.width() - self.leftMargin - self.rightMargin
|
||||
absx = x - self.leftMargin
|
||||
if self.fixedSpan:
|
||||
max_length = self.maxDisplayLength
|
||||
min_length = self.minDisplayLength
|
||||
x_step = (max_length - min_length) / width
|
||||
else:
|
||||
min_length = 0
|
||||
max_length = self.tdrWindow.distance_axis[math.ceil(len(self.tdrWindow.distance_axis) / 2)]/2
|
||||
x_step = max_length / width
|
||||
if limit and absx < 0:
|
||||
return min_length
|
||||
if limit and absx > width:
|
||||
return max_length
|
||||
return absx * x_step + min_length
|
||||
else:
|
||||
return 0
|
||||
|
||||
def zoomTo(self, x1, y1, x2, y2):
|
||||
logger.debug("Zoom to (x,y) by (x,y): (%d, %d) by (%d, %d)", x1, y1, x2, y2)
|
||||
val1 = self.valueAtPosition(y1)
|
||||
val2 = self.valueAtPosition(y2)
|
||||
|
||||
if val1 != val2:
|
||||
self.minImpedance = round(min(val1, val2), 3)
|
||||
self.maxImpedance = round(max(val1, val2), 3)
|
||||
self.setFixedValues(True)
|
||||
|
||||
len1 = max(0, self.lengthAtPosition(x1, limit=False))
|
||||
len2 = max(0, self.lengthAtPosition(x2, limit=False))
|
||||
|
||||
if len1 >= 0 and len2 >= 0 and len1 != len2:
|
||||
self.minDisplayLength = min(len1, len2)
|
||||
self.maxDisplayLength = max(len1, len2)
|
||||
self.setFixedSpan(True)
|
||||
|
||||
self.update()
|
||||
|
||||
def wheelEvent(self, a0: QtGui.QWheelEvent) -> None:
|
||||
if len(self.tdrWindow.td) == 0:
|
||||
a0.ignore()
|
||||
return
|
||||
chart_height = self.chartHeight
|
||||
chart_width = self.chartWidth
|
||||
do_zoom_x = do_zoom_y = True
|
||||
if a0.modifiers() == QtCore.Qt.ShiftModifier:
|
||||
do_zoom_x = False
|
||||
if a0.modifiers() == QtCore.Qt.ControlModifier:
|
||||
do_zoom_y = False
|
||||
if a0.angleDelta().y() > 0:
|
||||
# Zoom in
|
||||
a0.accept()
|
||||
# Center of zoom = a0.x(), a0.y()
|
||||
# We zoom in by 1/10 of the width/height.
|
||||
rate = a0.angleDelta().y() / 120
|
||||
if do_zoom_x:
|
||||
zoomx = rate * chart_width / 10
|
||||
else:
|
||||
zoomx = 0
|
||||
if do_zoom_y:
|
||||
zoomy = rate * chart_height / 10
|
||||
else:
|
||||
zoomy = 0
|
||||
absx = max(0, a0.x() - self.leftMargin)
|
||||
absy = max(0, a0.y() - self.topMargin)
|
||||
ratiox = absx/chart_width
|
||||
ratioy = absy/chart_height
|
||||
# TODO: Change zoom to center on the mouse if possible, or extend box to the side that has room if not.
|
||||
p1x = int(self.leftMargin + ratiox * zoomx)
|
||||
p1y = int(self.topMargin + ratioy * zoomy)
|
||||
p2x = int(self.leftMargin + chart_width - (1 - ratiox) * zoomx)
|
||||
p2y = int(self.topMargin + chart_height - (1 - ratioy) * zoomy)
|
||||
self.zoomTo(p1x, p1y, p2x, p2y)
|
||||
elif a0.angleDelta().y() < 0:
|
||||
# Zoom out
|
||||
a0.accept()
|
||||
# Center of zoom = a0.x(), a0.y()
|
||||
# We zoom out by 1/9 of the width/height, to match zoom in.
|
||||
rate = -a0.angleDelta().y() / 120
|
||||
if do_zoom_x:
|
||||
zoomx = rate * chart_width / 9
|
||||
else:
|
||||
zoomx = 0
|
||||
if do_zoom_y:
|
||||
zoomy = rate * chart_height / 9
|
||||
else:
|
||||
zoomy = 0
|
||||
absx = max(0, a0.x() - self.leftMargin)
|
||||
absy = max(0, a0.y() - self.topMargin)
|
||||
ratiox = absx/chart_width
|
||||
ratioy = absy/chart_height
|
||||
p1x = int(self.leftMargin - ratiox * zoomx)
|
||||
p1y = int(self.topMargin - ratioy * zoomy)
|
||||
p2x = int(self.leftMargin + chart_width + (1 - ratiox) * zoomx)
|
||||
p2y = int(self.topMargin + chart_height + (1 - ratioy) * zoomy)
|
||||
self.zoomTo(p1x, p1y, p2x, p2y)
|
||||
else:
|
||||
a0.ignore()
|
||||
|
||||
def resizeEvent(self, a0: QtGui.QResizeEvent) -> None:
|
||||
super().resizeEvent(a0)
|
||||
self.chartWidth = self.width() - self.leftMargin - self.rightMargin
|
||||
self.chartHeight = self.height() - self.bottomMargin - self.topMargin
|
||||
|
||||
|
||||
class RealImaginaryChart(FrequencyChart):
|
||||
def __init__(self, name=""):
|
||||
|
@ -2619,6 +2784,7 @@ class RealImaginaryChart(FrequencyChart):
|
|||
qp.drawLine(self.leftMargin, self.topMargin - 5, self.leftMargin, self.topMargin + self.chartHeight + 5)
|
||||
qp.drawLine(self.leftMargin-5, self.topMargin + self.chartHeight,
|
||||
self.leftMargin + self.chartWidth + 5, self.topMargin + self.chartHeight)
|
||||
self.drawTitle(qp)
|
||||
|
||||
def drawValues(self, qp: QtGui.QPainter):
|
||||
if len(self.data) == 0 and len(self.reference) == 0:
|
||||
|
@ -3012,14 +3178,6 @@ class MagnitudeChart(FrequencyChart):
|
|||
self.setPalette(pal)
|
||||
self.setAutoFillBackground(True)
|
||||
|
||||
def drawChart(self, qp: QtGui.QPainter):
|
||||
qp.setPen(QtGui.QPen(self.textColor))
|
||||
qp.drawText(3, 15, self.name)
|
||||
qp.setPen(QtGui.QPen(self.foregroundColor))
|
||||
qp.drawLine(self.leftMargin, 20, self.leftMargin, self.topMargin+self.chartHeight+5)
|
||||
qp.drawLine(self.leftMargin-5, self.topMargin+self.chartHeight,
|
||||
self.leftMargin+self.chartWidth, self.topMargin + self.chartHeight)
|
||||
|
||||
def drawValues(self, qp: QtGui.QPainter):
|
||||
if len(self.data) == 0 and len(self.reference) == 0:
|
||||
return
|
||||
|
@ -3158,14 +3316,6 @@ class MagnitudeZChart(FrequencyChart):
|
|||
self.setPalette(pal)
|
||||
self.setAutoFillBackground(True)
|
||||
|
||||
def drawChart(self, qp: QtGui.QPainter):
|
||||
qp.setPen(QtGui.QPen(self.textColor))
|
||||
qp.drawText(3, 15, self.name)
|
||||
qp.setPen(QtGui.QPen(self.foregroundColor))
|
||||
qp.drawLine(self.leftMargin, 20, self.leftMargin, self.topMargin+self.chartHeight+5)
|
||||
qp.drawLine(self.leftMargin-5, self.topMargin+self.chartHeight,
|
||||
self.leftMargin+self.chartWidth, self.topMargin + self.chartHeight)
|
||||
|
||||
def drawValues(self, qp: QtGui.QPainter):
|
||||
if len(self.data) == 0 and len(self.reference) == 0:
|
||||
return
|
||||
|
@ -3329,9 +3479,11 @@ class PermeabilityChart(FrequencyChart):
|
|||
qp.drawText(10, 15, "R")
|
||||
qp.drawText(self.leftMargin + self.chartWidth + 10, 15, "X")
|
||||
qp.setPen(QtGui.QPen(self.foregroundColor))
|
||||
qp.drawLine(self.leftMargin, self.topMargin - 5, self.leftMargin, self.topMargin + self.chartHeight + 5)
|
||||
qp.drawLine(self.leftMargin, self.topMargin - 5,
|
||||
self.leftMargin, self.topMargin + self.chartHeight + 5)
|
||||
qp.drawLine(self.leftMargin-5, self.topMargin + self.chartHeight,
|
||||
self.leftMargin + self.chartWidth + 5, self.topMargin + self.chartHeight)
|
||||
self.drawTitle(qp)
|
||||
|
||||
def drawValues(self, qp: QtGui.QPainter):
|
||||
if len(self.data) == 0 and len(self.reference) == 0:
|
||||
|
@ -3708,6 +3860,7 @@ class GroupDelayChart(FrequencyChart):
|
|||
qp.drawLine(self.leftMargin, 20, self.leftMargin, self.topMargin+self.chartHeight+5)
|
||||
qp.drawLine(self.leftMargin-5, self.topMargin+self.chartHeight,
|
||||
self.leftMargin+self.chartWidth, self.topMargin + self.chartHeight)
|
||||
self.drawTitle(qp)
|
||||
|
||||
def drawValues(self, qp: QtGui.QPainter):
|
||||
if len(self.data) == 0 and len(self.reference) == 0:
|
||||
|
@ -3776,8 +3929,56 @@ class GroupDelayChart(FrequencyChart):
|
|||
|
||||
self.drawFrequencyTicks(qp)
|
||||
|
||||
self.drawData(qp, self.data, self.sweepColor)
|
||||
self.drawData(qp, self.reference, self.referenceColor)
|
||||
color = self.sweepColor
|
||||
pen = QtGui.QPen(color)
|
||||
pen.setWidth(self.pointSize)
|
||||
line_pen = QtGui.QPen(color)
|
||||
line_pen.setWidth(self.lineThickness)
|
||||
qp.setPen(pen)
|
||||
for i in range(len(self.data)):
|
||||
x = self.getXPosition(self.data[i])
|
||||
y = self.getYPositionFromDelay(self.groupDelay[i])
|
||||
if self.isPlotable(x, y):
|
||||
qp.drawPoint(int(x), int(y))
|
||||
if self.drawLines and i > 0:
|
||||
prevx = self.getXPosition(self.data[i - 1])
|
||||
prevy = self.getYPositionFromDelay(self.groupDelay[i - 1])
|
||||
qp.setPen(line_pen)
|
||||
if self.isPlotable(x, y) and self.isPlotable(prevx, prevy):
|
||||
qp.drawLine(x, y, prevx, prevy)
|
||||
elif self.isPlotable(x, y) and not self.isPlotable(prevx, prevy):
|
||||
new_x, new_y = self.getPlotable(x, y, prevx, prevy)
|
||||
qp.drawLine(x, y, new_x, new_y)
|
||||
elif not self.isPlotable(x, y) and self.isPlotable(prevx, prevy):
|
||||
new_x, new_y = self.getPlotable(prevx, prevy, x, y)
|
||||
qp.drawLine(prevx, prevy, new_x, new_y)
|
||||
qp.setPen(pen)
|
||||
|
||||
color = self.referenceColor
|
||||
pen = QtGui.QPen(color)
|
||||
pen.setWidth(self.pointSize)
|
||||
line_pen = QtGui.QPen(color)
|
||||
line_pen.setWidth(self.lineThickness)
|
||||
qp.setPen(pen)
|
||||
for i in range(len(self.reference)):
|
||||
x = self.getXPosition(self.reference[i])
|
||||
y = self.getYPositionFromDelay(self.groupDelayReference[i])
|
||||
if self.isPlotable(x, y):
|
||||
qp.drawPoint(int(x), int(y))
|
||||
if self.drawLines and i > 0:
|
||||
prevx = self.getXPosition(self.reference[i - 1])
|
||||
prevy = self.getYPositionFromDelay(self.groupDelayReference[i - 1])
|
||||
qp.setPen(line_pen)
|
||||
if self.isPlotable(x, y) and self.isPlotable(prevx, prevy):
|
||||
qp.drawLine(x, y, prevx, prevy)
|
||||
elif self.isPlotable(x, y) and not self.isPlotable(prevx, prevy):
|
||||
new_x, new_y = self.getPlotable(x, y, prevx, prevy)
|
||||
qp.drawLine(x, y, new_x, new_y)
|
||||
elif not self.isPlotable(x, y) and self.isPlotable(prevx, prevy):
|
||||
new_x, new_y = self.getPlotable(prevx, prevy, x, y)
|
||||
qp.drawLine(prevx, prevy, new_x, new_y)
|
||||
qp.setPen(pen)
|
||||
|
||||
self.drawMarkers(qp)
|
||||
|
||||
def getYPosition(self, d: Datapoint) -> int:
|
||||
|
@ -3788,6 +3989,9 @@ class GroupDelayChart(FrequencyChart):
|
|||
delay = self.groupDelayReference[self.reference.index(d)]
|
||||
else:
|
||||
delay = 0
|
||||
return self.getYPositionFromDelay(delay)
|
||||
|
||||
def getYPositionFromDelay(self, delay: float):
|
||||
return self.topMargin + round((self.maxDelay - delay) / self.span * self.chartHeight)
|
||||
|
||||
def valueAtPosition(self, y) -> List[float]:
|
||||
|
@ -3825,6 +4029,7 @@ class CapacitanceChart(FrequencyChart):
|
|||
qp.drawLine(self.leftMargin, 20, self.leftMargin, self.topMargin+self.chartHeight+5)
|
||||
qp.drawLine(self.leftMargin-5, self.topMargin+self.chartHeight,
|
||||
self.leftMargin+self.chartWidth, self.topMargin + self.chartHeight)
|
||||
self.drawTitle(qp)
|
||||
|
||||
def drawValues(self, qp: QtGui.QPainter):
|
||||
if len(self.data) == 0 and len(self.reference) == 0:
|
||||
|
@ -3951,6 +4156,7 @@ class InductanceChart(FrequencyChart):
|
|||
qp.drawLine(self.leftMargin, 20, self.leftMargin, self.topMargin+self.chartHeight+5)
|
||||
qp.drawLine(self.leftMargin-5, self.topMargin+self.chartHeight,
|
||||
self.leftMargin+self.chartWidth, self.topMargin + self.chartHeight)
|
||||
self.drawTitle(qp)
|
||||
|
||||
def drawValues(self, qp: QtGui.QPainter):
|
||||
if len(self.data) == 0 and len(self.reference) == 0:
|
||||
|
|
|
@ -57,6 +57,18 @@ class VNA:
|
|||
logger.warning("Did not recognize NanoVNA type from firmware.")
|
||||
return NanoVNA(app, serial_port)
|
||||
|
||||
def readFeatures(self) -> List[str]:
|
||||
features = []
|
||||
raw_help = self.readFromCommand("help")
|
||||
logger.debug("Help command output:")
|
||||
logger.debug(raw_help)
|
||||
|
||||
# Detect features from the help command
|
||||
if "capture" in raw_help:
|
||||
features.append("Screenshots")
|
||||
|
||||
return features
|
||||
|
||||
def readFrequencies(self) -> List[str]:
|
||||
pass
|
||||
|
||||
|
@ -113,6 +125,29 @@ class VNA:
|
|||
logger.error("Unable to acquire serial lock to read firmware.")
|
||||
return ""
|
||||
|
||||
def readFromCommand(self, command) -> str:
|
||||
if self.app.serialLock.acquire():
|
||||
result = ""
|
||||
try:
|
||||
data = "a"
|
||||
while data != "":
|
||||
data = self.serial.readline().decode('ascii')
|
||||
self.serial.write((command + "\r").encode('ascii'))
|
||||
result = ""
|
||||
data = ""
|
||||
sleep(0.01)
|
||||
while data != "ch> ":
|
||||
data = self.serial.readline().decode('ascii')
|
||||
result += data
|
||||
except serial.SerialException as exc:
|
||||
logger.exception("Exception while reading " + command + ": %s", exc)
|
||||
finally:
|
||||
self.app.serialLock.release()
|
||||
return result
|
||||
else:
|
||||
logger.error("Unable to acquire serial lock to read " + command + ".")
|
||||
return ""
|
||||
|
||||
def readValues(self, value) -> List[str]:
|
||||
logger.debug("VNA reading %s", value)
|
||||
if self.app.serialLock.acquire():
|
||||
|
@ -212,6 +247,7 @@ class NanoVNA(VNA):
|
|||
logger.debug("Older than 0.2.0, using old sweep command.")
|
||||
self.features.append("Original sweep method")
|
||||
self.useScan = False
|
||||
self.features.extend(self.readFeatures())
|
||||
|
||||
def isValid(self):
|
||||
return True
|
||||
|
|
|
@ -29,19 +29,19 @@ FMT_FREQ_INPUT = SITools.Format(max_nr_digits=10, allow_strip=True,
|
|||
|
||||
FMT_Q_FACTOR = SITools.Format(max_nr_digits=4, assume_infinity=False,
|
||||
min_offset=0, max_offset=0, allow_strip=True)
|
||||
FMT_GROUP_DELAY = SITools.Format(max_nr_digits=5)
|
||||
FMT_GROUP_DELAY = SITools.Format(max_nr_digits=5, space_str=" ")
|
||||
FMT_REACT = SITools.Format(max_nr_digits=5, space_str=" ", allow_strip=True)
|
||||
COLOR_DEFAULT = QtGui.QColor()
|
||||
|
||||
|
||||
def format_frequency(freq: float, fmt=FMT_FREQ) -> str:
|
||||
return str(SITools.Value(freq, "Hz", fmt))
|
||||
def formatFrequency(freq: float) -> str:
|
||||
return str(SITools.Value(freq, "Hz", FMT_FREQ))
|
||||
|
||||
|
||||
def format_gain(val: float, invert: bool = False) -> str:
|
||||
if invert:
|
||||
val = -val
|
||||
return f"{val:.3f}dB"
|
||||
return f"{val:.3f} dB"
|
||||
|
||||
|
||||
def format_q_factor(val: float) -> str:
|
||||
|
@ -60,7 +60,7 @@ def format_resistance(val: float) -> str:
|
|||
return str(SITools.Value(val, "\N{OHM SIGN}", FMT_REACT))
|
||||
|
||||
|
||||
def format_capacity(val: float, allow_negative: bool = True) -> str:
|
||||
def format_capacitance(val: float, allow_negative: bool = True) -> str:
|
||||
if not allow_negative and val < 0:
|
||||
return "- pF"
|
||||
return str(SITools.Value(val, "F", FMT_REACT))
|
||||
|
@ -82,40 +82,23 @@ def format_phase(val: float) -> str:
|
|||
|
||||
def format_complex_imp(z: complex) -> str:
|
||||
if z.real > 0:
|
||||
s = f"{z.real:.4g}"
|
||||
if z.real >= 1000:
|
||||
s = f"{z.real/1000:.3g}k"
|
||||
else:
|
||||
s = f"{z.real:.4g}"
|
||||
else:
|
||||
s = "- "
|
||||
if z.imag < 0:
|
||||
s += "-j"
|
||||
s += " -j"
|
||||
else:
|
||||
s += "+j"
|
||||
return s + f"{abs(z.imag):.4g}\N{OHM SIGN}"
|
||||
|
||||
|
||||
class FrequencyInput(QtWidgets.QLineEdit):
|
||||
|
||||
def __init__(self, text=""):
|
||||
super().__init__(text)
|
||||
self.nextFrequency = -1
|
||||
self.previousFrequency = -1
|
||||
|
||||
def setText(self, text: str) -> None:
|
||||
super().setText(format_frequency(text, FMT_FREQ_INPUT))
|
||||
|
||||
def keyPressEvent(self, a0: QtGui.QKeyEvent) -> None:
|
||||
if a0.type() == QtCore.QEvent.KeyPress:
|
||||
if a0.key() == QtCore.Qt.Key_Up and self.nextFrequency != -1:
|
||||
a0.accept()
|
||||
self.setText(self.nextFrequency)
|
||||
self.textEdited.emit(self.text())
|
||||
return
|
||||
if a0.key() == QtCore.Qt.Key_Down and \
|
||||
self.previousFrequency != -1:
|
||||
a0.accept()
|
||||
self.setText(self.previousFrequency)
|
||||
self.textEdited.emit(self.text())
|
||||
return
|
||||
super().keyPressEvent(a0)
|
||||
s += " +j"
|
||||
if abs(z.imag) >= 1000:
|
||||
s += f"{abs(z.imag)/1000:.3g}k"
|
||||
elif abs(z.imag) < 0.1:
|
||||
s += f"{abs(z.imag)*1000:.3g}m"
|
||||
else:
|
||||
s += f"{abs(z.imag):.3g}"
|
||||
return s + " \N{OHM SIGN}"
|
||||
|
||||
|
||||
class Marker(QtCore.QObject):
|
||||
|
@ -131,6 +114,25 @@ class Marker(QtCore.QObject):
|
|||
|
||||
fieldSelection = []
|
||||
|
||||
class FrequencyInput(QtWidgets.QLineEdit):
|
||||
nextFrequency = -1
|
||||
previousFrequency = -1
|
||||
|
||||
def keyPressEvent(self, a0: QtGui.QKeyEvent) -> None:
|
||||
if a0.type() == QtCore.QEvent.KeyPress:
|
||||
if a0.key() == QtCore.Qt.Key_Up and self.nextFrequency != -1:
|
||||
a0.accept()
|
||||
self.setText(str(self.nextFrequency))
|
||||
self.textEdited.emit(self.text())
|
||||
return
|
||||
if a0.key() == QtCore.Qt.Key_Down and \
|
||||
self.previousFrequency != -1:
|
||||
a0.accept()
|
||||
self.setText(str(self.previousFrequency))
|
||||
self.textEdited.emit(self.text())
|
||||
return
|
||||
super().keyPressEvent(a0)
|
||||
|
||||
def __init__(self, name, initialColor, frequency=""):
|
||||
super().__init__()
|
||||
self.name = name
|
||||
|
@ -400,8 +402,6 @@ class Marker(QtCore.QObject):
|
|||
if self.location == -1:
|
||||
return
|
||||
s11 = s11data[self.location]
|
||||
if s21data:
|
||||
s21 = s21data[self.location]
|
||||
|
||||
imp = s11.impedance()
|
||||
cap_str = format_capacity(
|
||||
|
@ -454,6 +454,8 @@ class Marker(QtCore.QObject):
|
|||
if len(s21data) != len(s11data):
|
||||
return
|
||||
|
||||
s21 = s21data[self.location]
|
||||
|
||||
self.s21_phase_label.setText(format_phase(s21.phase))
|
||||
self.gain_label.setText(format_gain(s21.gain))
|
||||
# TODO: figure out if calculation is right (S11 no division by 2)
|
||||
|
|
|
@ -59,6 +59,8 @@ class NanoVNASaver(QtWidgets.QWidget):
|
|||
dataAvailable = QtCore.pyqtSignal()
|
||||
scaleFactor = 1
|
||||
|
||||
sweepTitle = ""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
if getattr(sys, 'frozen', False):
|
||||
|
@ -654,6 +656,7 @@ class NanoVNASaver(QtWidgets.QWidget):
|
|||
sleep(0.05)
|
||||
|
||||
self.vna = VNA.getVNA(self, self.serial)
|
||||
self.vna.validateInput = self.settings.value("SerialInputValidation", True, bool)
|
||||
self.worker.setVNA(self.vna)
|
||||
|
||||
logger.info(self.vna.readFirmware())
|
||||
|
@ -727,6 +730,8 @@ class NanoVNASaver(QtWidgets.QWidget):
|
|||
self.dataLock.release()
|
||||
if source is not None:
|
||||
self.sweepSource = source
|
||||
elif self.sweepTitle != "":
|
||||
self.sweepSource = self.sweepTitle + " " + strftime("%Y-%m-%d %H:%M:%S", localtime())
|
||||
else:
|
||||
self.sweepSource = strftime("%Y-%m-%d %H:%M:%S", localtime())
|
||||
|
||||
|
@ -964,6 +969,7 @@ class NanoVNASaver(QtWidgets.QWidget):
|
|||
|
||||
def showSweepError(self):
|
||||
self.showError(self.worker.error_message)
|
||||
self.serial.flushInput() # Remove any left-over data
|
||||
self.sweepFinished()
|
||||
|
||||
def popoutChart(self, chart: Chart):
|
||||
|
@ -1014,6 +1020,11 @@ class NanoVNASaver(QtWidgets.QWidget):
|
|||
m.getGroupBox().setFont(font)
|
||||
m.setScale(self.scaleFactor)
|
||||
|
||||
def setSweepTitle(self, title):
|
||||
self.sweepTitle = title
|
||||
for c in self.subscribing_charts:
|
||||
c.setSweepTitle(title)
|
||||
|
||||
|
||||
class DisplaySettingsWindow(QtWidgets.QWidget):
|
||||
def __init__(self, app: NanoVNASaver):
|
||||
|
@ -2023,6 +2034,20 @@ class SweepSettingsWindow(QtWidgets.QWidget):
|
|||
layout = QtWidgets.QVBoxLayout()
|
||||
self.setLayout(layout)
|
||||
|
||||
title_box = QtWidgets.QGroupBox("Sweep name")
|
||||
title_layout = QtWidgets.QFormLayout(title_box)
|
||||
self.sweep_title_input = QtWidgets.QLineEdit()
|
||||
title_layout.addRow("Sweep name", self.sweep_title_input)
|
||||
title_button_layout = QtWidgets.QHBoxLayout()
|
||||
btn_set_sweep_title = QtWidgets.QPushButton("Set")
|
||||
btn_set_sweep_title.clicked.connect(lambda: self.app.setSweepTitle(self.sweep_title_input.text()))
|
||||
btn_reset_sweep_title = QtWidgets.QPushButton("Reset")
|
||||
btn_reset_sweep_title.clicked.connect(lambda: self.app.setSweepTitle(""))
|
||||
title_button_layout.addWidget(btn_set_sweep_title)
|
||||
title_button_layout.addWidget(btn_reset_sweep_title)
|
||||
title_layout.addRow(title_button_layout)
|
||||
layout.addWidget(title_box)
|
||||
|
||||
settings_box = QtWidgets.QGroupBox("Settings")
|
||||
settings_layout = QtWidgets.QFormLayout(settings_box)
|
||||
|
||||
|
@ -2594,7 +2619,8 @@ class DeviceSettingsWindow(QtWidgets.QWidget):
|
|||
settings_layout = QtWidgets.QFormLayout(settings_box)
|
||||
|
||||
self.chkValidateInputData = QtWidgets.QCheckBox("Validate received data")
|
||||
self.chkValidateInputData.setChecked(True)
|
||||
validate_input = self.app.settings.value("SerialInputValidation", True, bool)
|
||||
self.chkValidateInputData.setChecked(validate_input)
|
||||
self.chkValidateInputData.stateChanged.connect(self.updateValidation)
|
||||
settings_layout.addRow("Validation", self.chkValidateInputData)
|
||||
|
||||
|
@ -2626,16 +2652,24 @@ class DeviceSettingsWindow(QtWidgets.QWidget):
|
|||
|
||||
self.featureList.clear()
|
||||
self.featureList.addItem(self.app.vna.name + " v" + str(self.app.vna.version))
|
||||
for item in self.app.vna.getFeatures():
|
||||
features = self.app.vna.getFeatures()
|
||||
for item in features:
|
||||
self.featureList.addItem(item)
|
||||
|
||||
if "Screenshots" in features:
|
||||
self.btnCaptureScreenshot.setDisabled(False)
|
||||
else:
|
||||
self.btnCaptureScreenshot.setDisabled(True)
|
||||
else:
|
||||
self.statusLabel.setText("Not connected.")
|
||||
self.calibrationStatusLabel.setText("Not connected.")
|
||||
self.featureList.clear()
|
||||
self.featureList.addItem("Not connected.")
|
||||
self.btnCaptureScreenshot.setDisabled(True)
|
||||
|
||||
def updateValidation(self, validate_data: bool):
|
||||
self.app.vna.validateInput = validate_data
|
||||
self.app.settings.setValue("SerialInputValidation", validate_data)
|
||||
|
||||
def captureScreenshot(self):
|
||||
if not self.app.worker.running:
|
||||
|
|
|
@ -41,8 +41,8 @@ def serial_to_parallel(z: complex) -> complex:
|
|||
z_sq_sum / z.imag)
|
||||
|
||||
|
||||
def impedance_to_capacity(z: complex, freq: float) -> float:
|
||||
"""Calculate capacitve equivalent for reactance"""
|
||||
def impedance_to_capacitance(z: complex, freq: float) -> float:
|
||||
"""Calculate capacitive equivalent for reactance"""
|
||||
if freq == 0:
|
||||
return -math.inf
|
||||
if z.imag == 0:
|
||||
|
@ -77,6 +77,13 @@ def gamma_to_impedance(gamma: complex, ref_impedance: float = 50) -> complex:
|
|||
return ((-gamma - 1) / (gamma - 1)) * ref_impedance
|
||||
|
||||
|
||||
def parseFrequency(freq: str) -> int:
|
||||
try:
|
||||
return int(Value(freq, "Hz", FMT_PARSE))
|
||||
except (ValueError, IndexError):
|
||||
return -1
|
||||
|
||||
|
||||
class Datapoint(NamedTuple):
|
||||
freq: int
|
||||
re: float
|
||||
|
@ -116,7 +123,7 @@ class Datapoint(NamedTuple):
|
|||
return abs(imp.imag / imp.real)
|
||||
|
||||
def capacitiveEquivalent(self, ref_impedance: float = 50) -> float:
|
||||
return impedance_to_capacity(
|
||||
return impedance_to_capacitance(
|
||||
self.impedance(ref_impedance), self.freq)
|
||||
|
||||
def inductiveEquivalent(self, ref_impedance: float = 50) -> float:
|
||||
|
@ -141,6 +148,7 @@ def groupDelay(data: List[Datapoint], index: int) -> float:
|
|||
|
||||
|
||||
class RFTools:
|
||||
# TODO: Remove this class when unused
|
||||
@staticmethod
|
||||
def formatFrequency(freq: Number) -> str:
|
||||
return str(Value(freq, "Hz", FMT_FREQ))
|
||||
|
@ -155,7 +163,4 @@ class RFTools:
|
|||
|
||||
@staticmethod
|
||||
def parseFrequency(freq: str) -> int:
|
||||
try:
|
||||
return int(Value(freq, "Hz", FMT_PARSE))
|
||||
except (ValueError, IndexError):
|
||||
return -1
|
||||
return parseFrequency(freq)
|
|
@ -3,7 +3,7 @@
|
|||
# Copyright (C) 2019. Rune B. Broberg
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published bynanovna
|
||||
# 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.
|
||||
#
|
||||
|
|
|
@ -142,7 +142,12 @@ class SweepWorker(QtCore.QRunnable):
|
|||
self.error_message = str(e)
|
||||
self.stopped = True
|
||||
self.running = False
|
||||
self.signals.fatalSweepError.emit()
|
||||
self.signals.sweepError.emit()
|
||||
except NanoVNASerialException as e:
|
||||
self.error_message = str(e)
|
||||
self.stopped = True
|
||||
self.running = False
|
||||
self.signals.sweepFatalError.emit()
|
||||
|
||||
while self.continuousSweep and not self.stopped:
|
||||
logger.debug("Continuous sweeping")
|
||||
|
@ -160,7 +165,12 @@ class SweepWorker(QtCore.QRunnable):
|
|||
self.error_message = str(e)
|
||||
self.stopped = True
|
||||
self.running = False
|
||||
self.signals.fatalSweepError.emit()
|
||||
self.signals.sweepError.emit()
|
||||
except NanoVNASerialException as e:
|
||||
self.error_message = str(e)
|
||||
self.stopped = True
|
||||
self.running = False
|
||||
self.signals.sweepFatalError.emit()
|
||||
|
||||
# Reset the device to show the full range
|
||||
logger.debug("Resetting NanoVNA sweep to full range: %d to %d",
|
||||
|
@ -332,16 +342,16 @@ class SweepWorker(QtCore.QRunnable):
|
|||
tmpdata = self.vna.readValues(data)
|
||||
if not tmpdata:
|
||||
logger.warning("Read no values")
|
||||
raise NanoVNAValueException("Failed reading data: Returned no values.")
|
||||
raise NanoVNASerialException("Failed reading data: Returned no values.")
|
||||
logger.debug("Read %d values", len(tmpdata))
|
||||
for d in tmpdata:
|
||||
a, b = d.split(" ")
|
||||
try:
|
||||
if self.vna.validateInput and float(a) < -9.5 or float(a) > 9.5:
|
||||
if self.vna.validateInput and (float(a) < -9.5 or float(a) > 9.5):
|
||||
logger.warning("Got a non-float data value: %s (%s)", d, a)
|
||||
logger.debug("Re-reading %s", data)
|
||||
done = False
|
||||
elif self.vna.validateInput and float(b) < -9.5 or float(b) > 9.5:
|
||||
elif self.vna.validateInput and (float(b) < -9.5 or float(b) > 9.5):
|
||||
logger.warning("Got a non-float data value: %s (%s)", d, b)
|
||||
logger.debug("Re-reading %s", data)
|
||||
done = False
|
||||
|
@ -359,7 +369,8 @@ class SweepWorker(QtCore.QRunnable):
|
|||
if count >= 20:
|
||||
logger.critical("Tried and failed to read %s %d times. Giving up.", data, count)
|
||||
raise NanoVNAValueException("Failed reading " + str(data) + " " + str(count) + " times.\n" +
|
||||
"Data outside expected valid ranges, or in an unexpected format.")
|
||||
"Data outside expected valid ranges, or in an unexpected format.\n\n" +
|
||||
"You can disable data validation on the device settings screen.")
|
||||
return returndata
|
||||
|
||||
def readFreq(self):
|
||||
|
@ -374,7 +385,7 @@ class SweepWorker(QtCore.QRunnable):
|
|||
tmpfreq = self.vna.readFrequencies()
|
||||
if not tmpfreq:
|
||||
logger.warning("Read no frequencies")
|
||||
raise NanoVNAValueException("Failed reading frequencies: Returned no values.")
|
||||
raise NanoVNASerialException("Failed reading frequencies: Returned no values.")
|
||||
for f in tmpfreq:
|
||||
if not f.isdigit():
|
||||
logger.warning("Got a non-digit frequency: %s", f)
|
||||
|
@ -407,3 +418,7 @@ class SweepWorker(QtCore.QRunnable):
|
|||
|
||||
class NanoVNAValueException(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class NanoVNASerialException(Exception):
|
||||
pass
|
||||
|
|
|
@ -21,7 +21,7 @@ from NanoVNASaver.RFTools import Datapoint, \
|
|||
norm_to_impedance, impedance_to_norm, \
|
||||
reflection_coefficient, gamma_to_impedance, clamp_value, \
|
||||
parallel_to_serial, serial_to_parallel, \
|
||||
impedance_to_capacity, impedance_to_inductance
|
||||
impedance_to_capacitance, impedance_to_inductance
|
||||
import math
|
||||
|
||||
|
||||
|
@ -83,10 +83,10 @@ class TestRFTools(unittest.TestCase):
|
|||
complex(52, 260))
|
||||
|
||||
def test_impedance_to_capacity(self):
|
||||
self.assertEqual(impedance_to_capacity(0, 0), -math.inf)
|
||||
self.assertEqual(impedance_to_capacity(0, 10), math.inf)
|
||||
self.assertEqual(impedance_to_capacitance(0, 0), -math.inf)
|
||||
self.assertEqual(impedance_to_capacitance(0, 10), math.inf)
|
||||
self.assertAlmostEqual(
|
||||
impedance_to_capacity(complex(50, 159.1549), 100000),
|
||||
impedance_to_capacitance(complex(50, 159.1549), 100000),
|
||||
1e-8)
|
||||
|
||||
def test_impedance_to_inductance(self):
|
||||
|
|
Ładowanie…
Reference in New Issue