FrequencyExtractWidget.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375
  1. from PyQt4 import QtCore, QtGui
  2. from ..base import kcgwidget as kcgw
  3. from ..base.globals import glob as global_objects
  4. from ..base.backend import board
  5. from ..base.backend.board import available_boards
  6. from .. import config
  7. import pyqtgraph as pg
  8. from math import ceil
  9. import numpy as np
  10. import socket
  11. import sys
  12. from .PlotWidget import SubPlotWidget
  13. __widget_id__ = None
  14. class FrequencyExtractWidget(kcgw.KCGWidgets):
  15. def __init__(self, unique_id, parent):
  16. super(FrequencyExtractWidget, self).__init__()
  17. self.id = unique_id
  18. self.par = parent
  19. #ToDo: Maybe find valid data from the get-go?
  20. self.data = None
  21. self.board_config = board.get_board_config(available_boards[0])
  22. self.board_config.observe(self, self.dataSetChanged, 'lastDataSet')
  23. self.numADCs = self.board_config.get('adc_number')
  24. #Use a VBox. Top half should be the SubPlotWidget, bottom
  25. #half should be the controls
  26. self.layout = QtGui.QVBoxLayout()
  27. self.plotBox = QtGui.QVBoxLayout()
  28. self.plot = SubPlotWidget()
  29. self.region = pg.LinearRegionItem(values=[10,20])
  30. self.plot.plotItem.addItem(self.region)
  31. self.region.sigRegionChangeFinished.connect(self.doPlot)
  32. self.line = pg.InfiniteLine(angle=0, label='Y={value:0.2f}',
  33. labelOpts={'movable': True})
  34. self.plot.plotItem.addItem(self.line)
  35. self.line.hide()
  36. self.plotBox.addWidget(self.plot)
  37. #Controls for the plot
  38. self.controlsBox = QtGui.QHBoxLayout()
  39. self.adcSelect = QtGui.QComboBox(self)
  40. for i in range(self.numADCs):
  41. self.adcSelect.addItem("ADC {}".format(i+1))
  42. self.adcSelect.currentIndexChanged.connect(self.doPlot)
  43. self.bucketSelect = QtGui.QComboBox(self)
  44. self.bucketSelect.addItem("Mean")
  45. for i in range(184):
  46. self.bucketSelect.addItem("Bucket {}".format(i+1))
  47. self.bucketSelect.currentIndexChanged.connect(self.doPlot)
  48. self.fromBox = self.createSpinbox(0, 100000000, interval=100, connect=self.doPlot)
  49. self.toBox = self.createSpinbox(0, 100000000, start_value=1000, interval=100, connect=self.doPlot)
  50. self.controlsBox.addWidget(self.adcSelect)
  51. self.controlsBox.addWidget(self.bucketSelect)
  52. self.controlsBox.addStretch()
  53. self.controlsBox.addWidget(self.createLabel(text="From:"))
  54. self.controlsBox.addWidget(self.fromBox)
  55. self.controlsBox.addWidget(self.createLabel(text="To:"))
  56. self.controlsBox.addWidget(self.toBox)
  57. self.frequencyTools = QtGui.QHBoxLayout()
  58. self.freqText = QtGui.QLineEdit(self)
  59. self.isFreqValid = False
  60. self.frequencyTools.addWidget(self.createLabel(text="Frequency:"))
  61. self.frequencyTools.addWidget(self.freqText)
  62. self.ethernetControls = QtGui.QHBoxLayout()
  63. self.ethernetControls.addWidget(self.createLabel("IP:"))
  64. self.ip = QtGui.QLineEdit(self)
  65. self.ethernetControls.addWidget(self.ip)
  66. self.ethernetControls.addWidget(self.createLabel("Port:"))
  67. self.port = self.createSpinbox(1024, 65535)
  68. self.port.setValue(56000)
  69. self.ethernetControls.addWidget(self.port)
  70. self.ethButton = self.createButton("Connect", connect=self.connectButtonClicked)
  71. self.ethernetControls.addWidget(self.ethButton)
  72. self.socketConnected = False
  73. #7-Bit encoding values
  74. self.encodingControls = QtGui.QHBoxLayout()
  75. self.encodeBase = self.createSpinbox(0, 10000, interval=1, connect=self.updateRange)
  76. self.encodingControls.addWidget(self.createLabel("Encoding Offset:"))
  77. self.encodingControls.addWidget(self.encodeBase)
  78. self.encodeStep = QtGui.QDoubleSpinBox()
  79. self.encodeStep.setDecimals(2)
  80. self.encodeStep.setMaximum(1000.)
  81. self.encodeStep.setMinimum(0.)
  82. self.encodeStep.setSingleStep(0.01)
  83. self.encodeDivider = QtGui.QDoubleSpinBox()
  84. self.encodeDivider.setMinimum(0.)
  85. self.encodeDivider.setMaximum(100.)
  86. self.encodeDivider.setDecimals(2)
  87. self.encodeDivider.setSingleStep(1.)
  88. self.encodeStep.valueChanged.connect(self.updateRange)
  89. self.encodingControls.addWidget(self.createLabel("Encoding Step:"))
  90. self.encodingControls.addWidget(self.encodeStep)
  91. self.encodingRange = self.createLabel("NaN")
  92. self.encodingControls.addWidget(self.createLabel("Valid Range:"))
  93. self.encodingControls.addWidget(self.encodingRange)
  94. self.encodingControls.addWidget(self.createLabel("Value Divider:"))
  95. self.encodingControls.addWidget(self.encodeDivider)
  96. self.encodeBase.setValue(10)
  97. self.encodeStep.setValue(0.08)
  98. self.encodeDivider.setValue(1.)
  99. self.layout.addLayout(self.plotBox)
  100. self.layout.addLayout(self.controlsBox)
  101. self.layout.addLayout(self.frequencyTools)
  102. self.layout.addLayout(self.ethernetControls)
  103. self.layout.addWidget(self.createLabel("7-Bit Encoding controls (kHz):"))
  104. self.layout.addLayout(self.encodingControls)
  105. self.setLayout(self.layout)
  106. self.setWindowTitle("Frequency Extract")
  107. def connectButtonClicked(self):
  108. if not self.socketConnected:
  109. self.connectEthernet()
  110. else:
  111. self.disconnectEthernet()
  112. def connectEthernet(self):
  113. if self.socketConnected:
  114. return
  115. #closing a socket also frees the underlying File-Descriptor,
  116. #so we need to create a new socket every time we want to create
  117. #a new connection
  118. self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
  119. ip = str(self.ip.text())
  120. try:
  121. socket.inet_aton(ip)
  122. except OSError:
  123. print("Malformed IP: %s"%ip)
  124. return
  125. try:
  126. self.socket.connect((self.ip.text(), self.port.value()))
  127. self.socketConnected = True
  128. self.ethButton.setText("Disconnect")
  129. except:
  130. print("Failed to connect to %s"%ip)
  131. self.socket = None
  132. def closeSocket(self):
  133. try:
  134. self.socket.close()
  135. except:
  136. pass
  137. self.socketConnected = False
  138. self.ethButton.setText("Connect")
  139. def disconnectEthernet(self):
  140. if not self.socketConnected:
  141. return
  142. #We tell the other end that we want to close the connection
  143. #by sending 0xFF. Our values are usually 7-Bit encoded, meaning
  144. #0xFF is an "Invalid" value and can be used for signalling
  145. self.sendValue(0xFF)
  146. self.closeSocket()
  147. def sendValue(self, value):
  148. if not self.socketConnected:
  149. return
  150. #socket.sendall() has two different behaviours, based on the Python
  151. #version.
  152. #In Python2, socket.sendall() will only accept a string
  153. #But in Python3, socket.sendall() will only accept a byte-sequence
  154. toSend = None
  155. if sys.version_info[0] == 2:
  156. toSend = chr(value)
  157. else:
  158. #bytes converts an array of integers into their shortest possible
  159. #bytewise representation.
  160. toSend = bytes([value])
  161. try:
  162. self.socket.sendall(toSend)
  163. except:
  164. print("Failed to send... closing down connection")
  165. self.closeSocket()
  166. def sendFreqEth(self, freq):
  167. if not self.socketConnected:
  168. return
  169. #We work in the kHz domain
  170. freq /= 1000
  171. offset = self.encodeBase.value()
  172. step = self.encodeStep.value()
  173. max = (offset + (step * 127))
  174. if (freq > max) or (freq < offset):
  175. print("Frequency outside range: %.2fkhZ - %.2fkHz! Won't send..."%(offset,max))
  176. return
  177. tmp = freq - offset - step
  178. count = 0
  179. while tmp > 0:
  180. tmp -= step
  181. count += 1
  182. self.sendValue(count)
  183. def updateRange(self):
  184. rangeMin = self.encodeBase.value()
  185. rangeMax = rangeMin + (self.encodeStep.value() * 127)
  186. self.encodingRange.setText("%.2f - %.2f (kHz)"%(rangeMin, rangeMax))
  187. def dataSetChanged(self, data=None):
  188. self.data = data
  189. self.doPlot()
  190. def doPlot(self):
  191. if not self.data:
  192. return
  193. #PlotWidget's "plot" function expects the fft_mode to have two entries
  194. #which we don't need here. So we need to shift our index by +2 in order
  195. #to accomodate for the two missing options in the bucket_select ComboBox
  196. fft_mode = self.bucketSelect.currentIndex() + 2
  197. adc = self.adcSelect.currentIndex()
  198. self.fftData = self.data.fft(adc=self.adcSelect.currentIndex(),
  199. frm=self.fromBox.value(),
  200. to=self.toBox.value(),
  201. drop_first_bin=True,
  202. nobunching=False)
  203. self.plot.plot(
  204. self.fftData,
  205. autorange=False,
  206. xvalueborders=[self.data.fftFreqDist(), self.data.fftMaxFreq()],
  207. fft_mode=fft_mode,
  208. log=False)
  209. self.plot.plotItem.setLabel('left', 'Spectral Intensity')
  210. #I am cheating a little bit and getting the readily processed data
  211. #from the plotItems, so I don't have to process, convert, format, etc.
  212. #the fftData again. We already did that in the SubPlotWidget.plot
  213. #function, so no need to do it again. Just steal the data from the
  214. #plotItems!
  215. self.fftYValues = self.plot.plotItemPlot[0].yData
  216. self.fftXValues = self.plot.plotItemPlot[0].xData
  217. region = self.region.getRegion()
  218. #Clip to valid data ranges
  219. rLeft = ceil(region[0]) if region[0] >=0 else 0
  220. rRight = ceil(region[1]) if region[1] < len(self.fftYValues) else len(self.fftYValues)
  221. #Python2 wants this to be specifically cast to integers
  222. rLeft = int(rLeft)
  223. rRight = int(rRight)
  224. #Make sure the region is within the boundaries of our data array
  225. if rLeft >= len(self.fftYValues) or rRight < 0:
  226. self.freqText.setText("Region boundaries out of data range")
  227. self.line.hide()
  228. self.isFreqValid = False
  229. return
  230. #If the region is not really a region, ignore it
  231. if rLeft == rRight:
  232. self.freqText.setText("Region too small")
  233. self.line.hide()
  234. self.isFreqValid = False
  235. return
  236. chunk = self.fftYValues[rLeft:rRight]
  237. maxVal = np.max(chunk)
  238. maxIndexInChunk = np.where(chunk == maxVal)
  239. self.line.setValue(maxVal)
  240. self.line.show()
  241. #np.where returns a tuple of indices (because it supports
  242. #multidimensional arrays). But since we know that our array is only
  243. #1-Dimensional, we can simply pick only the first dimension
  244. maxIndexInChunk = maxIndexInChunk[0]
  245. indexInData = rLeft + maxIndexInChunk
  246. #We are basically just repeating what the plot item also did to
  247. #determine the X-Axis labeling, and use it to calculate the X-Value
  248. #at the index we have identified
  249. xAxisScale = (self.data.fftMaxFreq() - self.data.fftFreqDist()) / float(self.fftYValues.shape[0])
  250. freq = self.fftXValues[indexInData][0]*xAxisScale
  251. freq = freq / self.encodeDivider.value()
  252. self.freqText.setText(str(freq))
  253. self.isFreqValid = True
  254. self.sendFreqEth(freq)
  255. def closeEvent(self, event):
  256. global __widget_id__
  257. __widget_id__ = None
  258. del self.par.widgets[self.id]
  259. self.board_config.unobserve(self, 'lastDataSet')
  260. #Tell the receiving end that we want to terminate the connection.
  261. self.sendValue(0xFF)
  262. def addFrequencyExtractWidget():
  263. global __widget_id__
  264. if __widget_id__:
  265. global_objects.get_global('area').widgets[__widget_id__].setFocus()
  266. else:
  267. nid = kcgw.idg.genid()
  268. __widget_id__ = nid
  269. w = FrequencyExtractWidget(nid, global_objects.get_global('area'))
  270. global_objects.get_global('area').newWidget(w, "Frequency Extract", nid, widget_type=4)
  271. kcgw.register_widget(QtGui.QIcon(config.icon_path("bbb.png")), "Frequency Extract", addFrequencyExtractWidget, "Ctrl+e")