1
0
forked from M-Labs/thermostat

Compare commits

...

5 Commits

Author SHA1 Message Date
1a3c831a06 flake: Introduce PyThermostat scripts to devShell
Make Python scripts in PyThermostat available in the development shell
(thermostat_plot, thermostat_autotune, and thermostat_test).
2024-11-25 11:12:02 +08:00
d64c83953e flake: Add pythermostat to PYTHONPATH in devshell
For easier testing of PyThermostat client code in the development shell,
so that the following is possible right in the repo root:

```
$ python
Python 3.11.9 (main, Apr  2 2024, 08:25:04) [GCC 13.2.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> from pythermostat.client import Client
>>> c = Client()
>>>
```
2024-11-25 11:11:17 +08:00
36d80ebdff PyThermostat: Add entry points for runnables
Forms a more convienient interface.
2024-11-25 10:07:30 +08:00
09300b5d44 PyThermostat: Add main function to plot.py 2024-11-25 10:07:30 +08:00
9743dca775 PyThermostat: Move scripts into subfolder
As Thermostat Python scripts are not single-file Python modules and
should be packaged inside PyThermostat.
2024-11-25 10:07:20 +08:00
7 changed files with 165 additions and 133 deletions

View File

@ -13,7 +13,7 @@ When tuning Thermostat PID parameters, it is helpful to view the temperature, PI
To use the Python real-time plotting utility, run
```shell
python pythermostat/plot.py
python pythermostat/pythermostat/plot.py
```
![default view](./assets/default%20view.png)
@ -49,7 +49,7 @@ A PID auto tuning utility is provided in the PyThermostat library. The auto tuni
To run the auto tuning utility, run
```shell
python pythermostat/autotune.py
python pythermostat/pythermostat/autotune.py
```
After some time, the auto tuning utility will output the auto tuning results, below is a sample output

View File

@ -69,6 +69,19 @@
matplotlib
];
};
pythermostat-dev-wrappers = pkgs.runCommandNoCC "pythermostat-dev-wrappers" { } ''
mkdir -p $out/bin
for program in ${self}/pythermostat/*.py; do
if [ -x $program ]; then
progname=`basename -s .py $program`
outname=$out/bin/thermostat_$progname
echo "#!${pkgs.bash}/bin/bash" >> $outname
echo "exec python -m $progname \"\$@\"" >> $outname
chmod 755 $outname
fi
done
'';
in
{
packages.x86_64-linux = {
@ -90,11 +103,15 @@
openocd
dfu-util
rlwrap
pythermostat-dev-wrappers
]
++ (with python3Packages; [
numpy
matplotlib
]);
shellHook = ''
export PYTHONPATH=`git rev-parse --show-toplevel`/pythermostat:$PYTHONPATH
'';
};
formatter.x86_64-linux = nixpkgs.legacyPackages.x86_64-linux.nixfmt-rfc-style;

View File

@ -1,131 +0,0 @@
import time
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.animation as animation
from threading import Thread, Lock
from pythermostat.client import Client
TIME_WINDOW = 300.0
tec = Client()
target_temperature = tec.get_pid()[0]['target']
print("Channel 0 target temperature: {:.3f}".format(target_temperature))
class Series:
def __init__(self, conv=lambda x: x):
self.conv = conv
self.x_data = []
self.y_data = []
def append(self, x, y):
self.x_data.append(x)
self.y_data.append(self.conv(y))
def clip(self, min_x):
drop = 0
while drop < len(self.x_data) and self.x_data[drop] < min_x:
drop += 1
self.x_data = self.x_data[drop:]
self.y_data = self.y_data[drop:]
series = {
# 'adc': Series(),
# 'sens': Series(lambda x: x * 0.0001),
'temperature': Series(),
# 'i_set': Series(),
'pid_output': Series(),
# 'vref': Series(),
# 'dac_value': Series(),
# 'dac_feedback': Series(),
# 'i_tec': Series(),
'tec_i': Series(),
'tec_u_meas': Series(),
# 'interval': Series(),
}
series_lock = Lock()
quit = False
def recv_data(tec):
global last_packet_time
while True:
data = tec.get_report()
ch0 = data[0]
series_lock.acquire()
try:
for k, s in series.items():
if k in ch0:
v = ch0[k]
if type(v) is float:
s.append(ch0['time'], v)
finally:
series_lock.release()
if quit:
break
time.sleep(0.05)
thread = Thread(target=recv_data, args=(tec,))
thread.start()
fig, ax = plt.subplots()
for k, s in series.items():
s.plot, = ax.plot([], [], label=k)
legend = ax.legend()
def animate(i):
min_x, max_x, min_y, max_y = None, None, None, None
series_lock.acquire()
try:
for k, s in series.items():
s.plot.set_data(s.x_data, s.y_data)
if len(s.y_data) > 0:
s.plot.set_label("{}: {:.3f}".format(k, s.y_data[-1]))
if len(s.x_data) > 0:
min_x_ = min(s.x_data)
if min_x is None:
min_x = min_x_
else:
min_x = min(min_x, min_x_)
max_x_ = max(s.x_data)
if max_x is None:
max_x = max_x_
else:
max_x = max(max_x, max_x_)
if len(s.y_data) > 0:
min_y_ = min(s.y_data)
if min_y is None:
min_y = min_y_
else:
min_y = min(min_y, min_y_)
max_y_ = max(s.y_data)
if max_y is None:
max_y = max_y_
else:
max_y = max(max_y, max_y_)
if min_x and max_x - TIME_WINDOW > min_x:
for s in series.values():
s.clip(max_x - TIME_WINDOW)
finally:
series_lock.release()
if min_x != max_x:
ax.set_xlim(min_x, max_x)
if min_y != max_y:
margin_y = 0.01 * (max_y - min_y)
ax.set_ylim(min_y - margin_y, max_y + margin_y)
global legend
legend.remove()
legend = ax.legend()
ani = animation.FuncAnimation(
fig, animate, interval=1, blit=False, save_count=50)
plt.show()
quit = True
thread.join()

View File

137
pythermostat/pythermostat/plot.py Executable file
View File

@ -0,0 +1,137 @@
import time
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.animation as animation
from threading import Thread, Lock
from pythermostat.client import Client
def main():
TIME_WINDOW = 300.0
tec = Client()
target_temperature = tec.get_pid()[0]['target']
print("Channel 0 target temperature: {:.3f}".format(target_temperature))
class Series:
def __init__(self, conv=lambda x: x):
self.conv = conv
self.x_data = []
self.y_data = []
def append(self, x, y):
self.x_data.append(x)
self.y_data.append(self.conv(y))
def clip(self, min_x):
drop = 0
while drop < len(self.x_data) and self.x_data[drop] < min_x:
drop += 1
self.x_data = self.x_data[drop:]
self.y_data = self.y_data[drop:]
series = {
# 'adc': Series(),
# 'sens': Series(lambda x: x * 0.0001),
'temperature': Series(),
# 'i_set': Series(),
'pid_output': Series(),
# 'vref': Series(),
# 'dac_value': Series(),
# 'dac_feedback': Series(),
# 'i_tec': Series(),
'tec_i': Series(),
'tec_u_meas': Series(),
# 'interval': Series(),
}
series_lock = Lock()
quit = False
def recv_data(tec):
global last_packet_time
while True:
data = tec.get_report()
ch0 = data[0]
series_lock.acquire()
try:
for k, s in series.items():
if k in ch0:
v = ch0[k]
if type(v) is float:
s.append(ch0['time'], v)
finally:
series_lock.release()
if quit:
break
time.sleep(0.05)
thread = Thread(target=recv_data, args=(tec,))
thread.start()
fig, ax = plt.subplots()
for k, s in series.items():
s.plot, = ax.plot([], [], label=k)
legend = ax.legend()
def animate(i):
min_x, max_x, min_y, max_y = None, None, None, None
series_lock.acquire()
try:
for k, s in series.items():
s.plot.set_data(s.x_data, s.y_data)
if len(s.y_data) > 0:
s.plot.set_label("{}: {:.3f}".format(k, s.y_data[-1]))
if len(s.x_data) > 0:
min_x_ = min(s.x_data)
if min_x is None:
min_x = min_x_
else:
min_x = min(min_x, min_x_)
max_x_ = max(s.x_data)
if max_x is None:
max_x = max_x_
else:
max_x = max(max_x, max_x_)
if len(s.y_data) > 0:
min_y_ = min(s.y_data)
if min_y is None:
min_y = min_y_
else:
min_y = min(min_y, min_y_)
max_y_ = max(s.y_data)
if max_y is None:
max_y = max_y_
else:
max_y = max(max_y, max_y_)
if min_x and max_x - TIME_WINDOW > min_x:
for s in series.values():
s.clip(max_x - TIME_WINDOW)
finally:
series_lock.release()
if min_x != max_x:
ax.set_xlim(min_x, max_x)
if min_y != max_y:
margin_y = 0.01 * (max_y - min_y)
ax.set_ylim(min_y - margin_y, max_y + margin_y)
nonlocal legend
legend.remove()
legend = ax.legend()
ani = animation.FuncAnimation(
fig, animate, interval=1, blit=False, save_count=50)
plt.show()
quit = True
thread.join()
if __name__ == "__main__":
main()

View File

View File

@ -9,4 +9,13 @@ setup(
license="GPLv3",
install_requires=["setuptools"],
packages=find_packages(),
entry_points={
"gui_scripts": [
"thermostat_plot = pythermostat.plot:main",
],
"console_scripts": [
"thermostat_autotune = pythermostat.autotune:main",
"thermostat_test = pythermostat.test:main",
]
},
)