FrequencyExtractWidget.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362
  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.encodeStep.valueChanged.connect(self.updateRange)
  84. self.encodingControls.addWidget(self.createLabel("Encoding Step:"))
  85. self.encodingControls.addWidget(self.encodeStep)
  86. self.encodingRange = self.createLabel("NaN")
  87. self.encodingControls.addWidget(self.createLabel("Valid Range:"))
  88. self.encodingControls.addWidget(self.encodingRange)
  89. self.encodeBase.setValue(10)
  90. self.encodeStep.setValue(0.08)
  91. self.layout.addLayout(self.plotBox)
  92. self.layout.addLayout(self.controlsBox)
  93. self.layout.addLayout(self.frequencyTools)
  94. self.layout.addLayout(self.ethernetControls)
  95. self.layout.addWidget(self.createLabel("7-Bit Encoding controls (kHz):"))
  96. self.layout.addLayout(self.encodingControls)
  97. self.setLayout(self.layout)
  98. self.setWindowTitle("Frequency Extract")
  99. def connectButtonClicked(self):
  100. if not self.socketConnected:
  101. self.connectEthernet()
  102. else:
  103. self.disconnectEthernet()
  104. def connectEthernet(self):
  105. if self.socketConnected:
  106. return
  107. #closing a socket also frees the underlying File-Descriptor,
  108. #so we need to create a new socket every time we want to create
  109. #a new connection
  110. self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
  111. try:
  112. socket.inet_aton(self.ip.text())
  113. except OSError:
  114. print("Malformed IP: %s"%self.ip.text())
  115. return
  116. try:
  117. self.socket.connect((self.ip.text(), self.port.value()))
  118. self.socketConnected = True
  119. self.ethButton.setText("Disconnect")
  120. except:
  121. print("Failed to connect to %s"%self.ip.text())
  122. self.socket = None
  123. def closeSocket(self):
  124. try:
  125. self.socket.close()
  126. except:
  127. pass
  128. self.socketConnected = False
  129. self.ethButton.setText("Connect")
  130. def disconnectEthernet(self):
  131. if not self.socketConnected:
  132. return
  133. #We tell the other end that we want to close the connection
  134. #by sending 0xFF. Our values are usually 7-Bit encoded, meaning
  135. #0xFF is an "Invalid" value and can be used for signalling
  136. self.sendValue(0xFF)
  137. self.closeSocket()
  138. def sendValue(self, value):
  139. if not self.socketConnected:
  140. return
  141. #socket.sendall() has two different behaviours, based on the Python
  142. #version.
  143. #In Python2, socket.sendall() will only accept a string
  144. #But in Python3, socket.sendall() will only accept a byte-sequence
  145. toSend = None
  146. if sys.version_info[0] == 2:
  147. toSend = chr(value)
  148. else:
  149. #bytes converts an array of integers into their shortest possible
  150. #bytewise representation.
  151. toSend = bytes([value])
  152. try:
  153. self.socket.sendall(toSend)
  154. except:
  155. print("Failed to send... closing down connection")
  156. self.closeSocket()
  157. def sendFreqEth(self, freq):
  158. if not self.socketConnected:
  159. return
  160. #We work in the kHz domain
  161. freq /= 1000
  162. offset = self.encodeBase.value()
  163. step = self.encodeStep.value()
  164. max = (offset + (step * 127))
  165. if (freq > max) or (freq < offset):
  166. print("Frequency outside range: %.2fkhZ - %.2fkHz! Won't send..."%(offset,max))
  167. return
  168. tmp = freq - offset - step
  169. count = 0
  170. while tmp > 0:
  171. tmp -= step
  172. count += 1
  173. self.sendValue(count)
  174. def updateRange(self):
  175. rangeMin = self.encodeBase.value()
  176. rangeMax = rangeMin + (self.encodeStep.value() * 127)
  177. self.encodingRange.setText("%.2f - %.2f (kHz)"%(rangeMin, rangeMax))
  178. def dataSetChanged(self, data=None):
  179. self.data = data
  180. self.doPlot()
  181. def doPlot(self):
  182. if not self.data:
  183. return
  184. #PlotWidget's "plot" function expects the fft_mode to have two entries
  185. #which we don't need here. So we need to shift our index by +2 in order
  186. #to accomodate for the two missing options in the bucket_select ComboBox
  187. fft_mode = self.bucketSelect.currentIndex() + 2
  188. adc = self.adcSelect.currentIndex()
  189. self.fftData = self.data.fft(adc=self.adcSelect.currentIndex(),
  190. frm=self.fromBox.value(),
  191. to=self.toBox.value(),
  192. drop_first_bin=True,
  193. nobunching=False)
  194. self.plot.plot(
  195. self.fftData,
  196. autorange=False,
  197. xvalueborders=[self.data.fftFreqDist(), self.data.fftMaxFreq()],
  198. fft_mode=fft_mode,
  199. log=False)
  200. self.plot.plotItem.setLabel('left', 'Spectral Intensity')
  201. #I am cheating a little bit and getting the readily processed data
  202. #from the plotItems, so I don't have to process, convert, format, etc.
  203. #the fftData again. We already did that in the SubPlotWidget.plot
  204. #function, so no need to do it again. Just steal the data from the
  205. #plotItems!
  206. self.fftYValues = self.plot.plotItemPlot[0].yData
  207. self.fftXValues = self.plot.plotItemPlot[0].xData
  208. region = self.region.getRegion()
  209. #Clip to valid data ranges
  210. rLeft = ceil(region[0]) if region[0] >=0 else 0
  211. rRight = ceil(region[1]) if region[1] < len(self.fftYValues) else len(self.fftYValues)
  212. #Python2 wants this to be specifically cast to integers
  213. rLeft = int(rLeft)
  214. rRight = int(rRight)
  215. #Make sure the region is within the boundaries of our data array
  216. if rLeft >= len(self.fftYValues) or rRight < 0:
  217. self.freqText.setText("Region boundaries out of data range")
  218. self.line.hide()
  219. self.isFreqValid = False
  220. return
  221. #If the region is not really a region, ignore it
  222. if rLeft == rRight:
  223. self.freqText.setText("Region too small")
  224. self.line.hide()
  225. self.isFreqValid = False
  226. return
  227. chunk = self.fftYValues[rLeft:rRight]
  228. maxVal = np.max(chunk)
  229. maxIndexInChunk = np.where(chunk == maxVal)
  230. self.line.setValue(maxVal)
  231. self.line.show()
  232. #np.where returns a tuple of indices (because it supports
  233. #multidimensional arrays). But since we know that our array is only
  234. #1-Dimensional, we can simply pick only the first dimension
  235. maxIndexInChunk = maxIndexInChunk[0]
  236. indexInData = rLeft + maxIndexInChunk
  237. #We are basically just repeating what the plot item also did to
  238. #determine the X-Axis labeling, and use it to calculate the X-Value
  239. #at the index we have identified
  240. xAxisScale = (self.data.fftMaxFreq() - self.data.fftFreqDist()) / float(self.fftYValues.shape[0])
  241. self.freqText.setText(str(self.fftXValues[indexInData][0]*xAxisScale))
  242. self.isFreqValid = True
  243. self.sendFreqEth(self.fftXValues[indexInData][0]*xAxisScale)
  244. def closeEvent(self, event):
  245. global __widget_id__
  246. __widget_id__ = None
  247. del self.par.widgets[self.id]
  248. self.board_config.unobserve(self, 'lastDataSet')
  249. #Tell the receiving end that we want to terminate the connection.
  250. self.sendValue(0xFF)
  251. def addFrequencyExtractWidget():
  252. global __widget_id__
  253. if __widget_id__:
  254. global_objects.get_global('area').widgets[__widget_id__].setFocus()
  255. else:
  256. nid = kcgw.idg.genid()
  257. __widget_id__ = nid
  258. w = FrequencyExtractWidget(nid, global_objects.get_global('area'))
  259. global_objects.get_global('area').newWidget(w, "Frequency Extract", nid, widget_type=4)
  260. kcgw.register_widget(QtGui.QIcon(config.icon_path("bbb.png")), "Frequency Extract", addFrequencyExtractWidget, "Ctrl+e")