2020-01-05 19:31:07 +08:00
|
|
|
import serial
|
|
|
|
import queue
|
|
|
|
import threading
|
2020-01-11 18:41:18 +08:00
|
|
|
|
2020-01-27 17:24:59 +08:00
|
|
|
import SoapySDR
|
2020-01-11 16:06:54 +08:00
|
|
|
import numpy as np
|
|
|
|
from scipy.signal import blackmanharris
|
2020-01-05 19:31:07 +08:00
|
|
|
|
|
|
|
|
2020-01-27 17:24:59 +08:00
|
|
|
class BufferedSDR:
|
|
|
|
def __init__(self, sdr, channels, bufsize, nbufs):
|
|
|
|
self.sdr = sdr
|
|
|
|
self.channels = channels
|
|
|
|
self.bufsize = bufsize
|
|
|
|
|
|
|
|
self.stream = None
|
|
|
|
self.terminate = False
|
|
|
|
self.thread = None
|
|
|
|
self.queue = queue.Queue(nbufs)
|
|
|
|
self.available_buffers = queue.Queue(nbufs)
|
|
|
|
for _ in range(nbufs):
|
|
|
|
self.available_buffers.put([np.array([0]*bufsize, np.complex64) for _ in self.channels])
|
|
|
|
|
|
|
|
def start(self):
|
|
|
|
self.stream = self.sdr.setupStream(SoapySDR.SOAPY_SDR_RX, SoapySDR.SOAPY_SDR_CF32, self.channels)
|
|
|
|
try:
|
|
|
|
self.thread = threading.Thread(target=self.thread_target)
|
|
|
|
self.thread.start()
|
|
|
|
except:
|
|
|
|
self.sdr.closeStream(self.stream)
|
|
|
|
raise
|
|
|
|
|
|
|
|
def stop(self):
|
|
|
|
self.terminate = True
|
|
|
|
self.thread.join()
|
|
|
|
self.sdr.closeStream(self.stream)
|
|
|
|
|
|
|
|
def get(self):
|
|
|
|
return self.queue.get()
|
|
|
|
|
|
|
|
def dispose(self, buffers):
|
|
|
|
self.available_buffers.put(buffers)
|
|
|
|
|
|
|
|
def thread_target(self):
|
|
|
|
self.sdr.activateStream(self.stream)
|
|
|
|
try:
|
|
|
|
while not self.terminate:
|
|
|
|
buffers = self.available_buffers.get()
|
|
|
|
sr = self.sdr.readStream(self.stream, buffers, self.bufsize)
|
|
|
|
if sr.ret != self.bufsize:
|
|
|
|
print("SDR sampling error")
|
|
|
|
return
|
|
|
|
self.queue.put(buffers)
|
|
|
|
finally:
|
|
|
|
self.sdr.deactivateStream(self.stream)
|
|
|
|
|
|
|
|
|
2020-01-27 17:25:14 +08:00
|
|
|
class DummyInductionHeater:
|
|
|
|
def __init__(self, port, induction_min, induction_max):
|
|
|
|
pass
|
|
|
|
|
|
|
|
def start(self):
|
|
|
|
pass
|
|
|
|
|
|
|
|
def set(self, amount):
|
|
|
|
print("induction", amount)
|
|
|
|
|
|
|
|
def stop(self):
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
2020-01-05 19:31:07 +08:00
|
|
|
class InductionHeater:
|
|
|
|
"""Interface to the MHS5200A function generator driving the LC tank"""
|
|
|
|
def __init__(self, port, induction_min, induction_max):
|
|
|
|
self.port = port
|
|
|
|
self.induction_min = induction_min
|
|
|
|
self.induction_max = induction_max
|
|
|
|
self.queue = queue.Queue(1)
|
|
|
|
|
|
|
|
def start(self):
|
|
|
|
self.serial = serial.Serial(self.port, 57600)
|
|
|
|
self.thread = threading.Thread(target=self.thread_target)
|
|
|
|
self.thread.start()
|
|
|
|
|
|
|
|
def thread_target(self):
|
|
|
|
while True:
|
|
|
|
amount = self.queue.get()
|
|
|
|
if amount is None:
|
|
|
|
break
|
|
|
|
|
|
|
|
amount = max(min(amount, 0.5), -0.5)
|
|
|
|
freq = ((self.induction_min + self.induction_max)/2
|
|
|
|
+ amount*(self.induction_max - self.induction_min))
|
|
|
|
|
|
|
|
command = ":s1f{:010d}\n".format(int(freq*1e2))
|
|
|
|
self.serial.write(command.encode())
|
|
|
|
self.serial.readline()
|
|
|
|
|
|
|
|
def set(self, amount):
|
|
|
|
self.queue.put(amount, block=False)
|
|
|
|
|
|
|
|
def stop(self):
|
|
|
|
self.queue.put(None, block=True)
|
|
|
|
self.thread.join()
|
|
|
|
self.serial.close()
|
2020-01-11 16:06:54 +08:00
|
|
|
|
|
|
|
|
|
|
|
# https://gist.github.com/endolith/255291
|
|
|
|
def parabolic(f, x):
|
|
|
|
xv = 1/2. * (f[x-1] - f[x+1]) / (f[x-1] - 2 * f[x] + f[x+1]) + x
|
|
|
|
yv = f[x] - 1/4. * (f[x-1] - f[x+1]) * (xv - x)
|
|
|
|
return (xv, yv)
|
|
|
|
|
|
|
|
|
|
|
|
class Stabilizer:
|
|
|
|
def __init__(self, freq_sample, amp_threshold, freq_target, k, tuner):
|
|
|
|
self.freq_sample = freq_sample
|
|
|
|
self.amp_threshold = amp_threshold
|
|
|
|
self.freq_target = freq_target
|
|
|
|
self.k = k
|
|
|
|
self.tuner = tuner
|
|
|
|
|
|
|
|
def input(self, samples):
|
|
|
|
spectrum = np.abs(np.fft.fft(samples*blackmanharris(len(samples)))[0:len(samples)//2])
|
|
|
|
for i in range(len(spectrum)//100):
|
|
|
|
spectrum[i] = 0
|
|
|
|
spectrum[-i] = 0
|
|
|
|
i = np.argmax(spectrum)
|
|
|
|
true_i, amplitude = parabolic(spectrum, i)
|
|
|
|
freq = 0.5 * self.freq_sample * true_i / len(spectrum)
|
|
|
|
|
2020-01-12 19:32:38 +08:00
|
|
|
if amplitude > self.amp_threshold:
|
|
|
|
tuning = (freq - self.freq_target)*self.k
|
2020-01-11 16:06:54 +08:00
|
|
|
else:
|
|
|
|
tuning = 0.0
|
|
|
|
self.tuner.set(tuning)
|
2020-01-11 18:41:18 +08:00
|
|
|
|
|
|
|
|
|
|
|
def continuous_unwrap(last_phase, last_phase_unwrapped, p):
|
|
|
|
# note: np.unwrap always preserves first element of array
|
|
|
|
p = np.unwrap(p)
|
|
|
|
glue = np.array([last_phase_unwrapped, last_phase_unwrapped + (p[0] - last_phase)])
|
|
|
|
new_p0 = np.unwrap(glue)[1]
|
|
|
|
return new_p0 + p - p[0]
|
|
|
|
|
|
|
|
|
|
|
|
class PositionTracker:
|
|
|
|
def __init__(self, leakage_avg):
|
|
|
|
self.last_phase = 0.0
|
|
|
|
self.last_position = 0.0
|
|
|
|
self.leakage = np.zeros(leakage_avg)
|
|
|
|
self.leakage_ptr = 0
|
|
|
|
|
|
|
|
def input(self, ref, meas):
|
|
|
|
demod = np.conjugate(ref)*meas
|
|
|
|
|
|
|
|
self.leakage[self.leakage_ptr] = np.real(np.sum(demod)/len(demod))
|
|
|
|
self.leakage_ptr = (self.leakage_ptr + 1) % len(self.leakage)
|
|
|
|
leakage = np.sum(self.leakage)/len(self.leakage)
|
|
|
|
|
|
|
|
phase = np.angle(demod - leakage)
|
|
|
|
position = continuous_unwrap(self.last_phase, self.last_position, phase)/(2.0*np.pi)
|
|
|
|
self.last_phase = phase[-1]
|
|
|
|
self.last_position = position[-1]
|
|
|
|
return position, leakage
|