added nicegui plotting
This commit is contained in:
+72
-32
@@ -13,9 +13,16 @@ import numpy as np
|
||||
|
||||
from bsread import dispatcher, source
|
||||
|
||||
import plotext as plt
|
||||
import epics
|
||||
|
||||
try:
|
||||
from nicegui import ui, app, context, run
|
||||
except ImportError as e:
|
||||
print("nicegui not available, falling back to console plot.")
|
||||
print(e)
|
||||
|
||||
from plots import ConsolePlot, PlotlyPlot
|
||||
|
||||
|
||||
def clean_name(fname):
|
||||
"""Returns the filename with all special characters but / . _ - #
|
||||
@@ -156,6 +163,18 @@ def yield_pv_data(channel_names: list[str]):
|
||||
yield channel_data
|
||||
|
||||
|
||||
def yield_test_data(channel_names: list[str]):
|
||||
"""Yields test data from the given channels with timestamp."""
|
||||
import random
|
||||
import time
|
||||
|
||||
while True:
|
||||
channel_data = {
|
||||
channel: {"value": random.random(), "timestamp": time.time()} for channel in channel_names
|
||||
}
|
||||
yield channel_data
|
||||
|
||||
|
||||
def roundrobin(*iterables):
|
||||
"Visit input iterables in a cycle until each is exhausted."
|
||||
# roundrobin('ABC', 'D', 'EF') → A D E B F C
|
||||
@@ -174,7 +193,10 @@ def yield_data(channel_names: list, only_pv=False):
|
||||
"""
|
||||
bs_channels = dispatcher.get_current_channels()
|
||||
|
||||
# separate into BS and PV channels
|
||||
# separate into BS, PV and test channels
|
||||
test_list = [ch for ch in channel_names if ch.startswith("TEST_CHANNEL")]
|
||||
for test_ch in test_list:
|
||||
channel_names.remove(test_ch)
|
||||
|
||||
if only_pv:
|
||||
pv_list = channel_names
|
||||
@@ -183,7 +205,9 @@ def yield_data(channel_names: list, only_pv=False):
|
||||
bs_list = [ch for ch in channel_names if ch in bs_channels]
|
||||
pv_list = [ch for ch in channel_names if ch not in bs_list]
|
||||
|
||||
for channel_data in roundrobin(yield_bs_data(bs_list), yield_pv_data(pv_list)):
|
||||
for channel_data in roundrobin(
|
||||
yield_bs_data(bs_list), yield_pv_data(pv_list), yield_test_data(test_list)
|
||||
):
|
||||
yield channel_data
|
||||
|
||||
|
||||
@@ -203,34 +227,16 @@ def collect_data(channels: dict):
|
||||
yield channels
|
||||
|
||||
|
||||
def plot(channels, start_time=0, in_subplots=False, marker="hd"):
|
||||
"""Plots channels on the console."""
|
||||
|
||||
# plt.clf()
|
||||
plt.limit_size(False, False)
|
||||
plt.theme("pro")
|
||||
|
||||
plt.xlabel("Timestamp")
|
||||
plt.ylabel("Value")
|
||||
|
||||
num_channels = len(channels)
|
||||
|
||||
if in_subplots:
|
||||
plt.subplots(num_channels, 1)
|
||||
plt.main().plot_size(plt.tw(), plt.th() - 1)
|
||||
plt.clear_data()
|
||||
|
||||
for i, channel in enumerate(channels.values()):
|
||||
if in_subplots:
|
||||
plt.subplot(i + 1, 1)
|
||||
|
||||
plt.plot(channel.x - start_time, channel.y, label=channel.name, marker=marker)
|
||||
|
||||
plt.show()
|
||||
|
||||
|
||||
async def runner(
|
||||
channels: dict, acquire_interval=0.1, plot_interval=1, in_subplots=False, abort=False, relative_time=True
|
||||
channels: dict,
|
||||
plot_interface,
|
||||
acquire_interval=0.1,
|
||||
plot_interval=1,
|
||||
in_subplots=False,
|
||||
abort=False,
|
||||
relative_time=True,
|
||||
):
|
||||
"""
|
||||
Loop for data collection and plotting.
|
||||
@@ -248,6 +254,8 @@ async def runner(
|
||||
last_collect_time = time.time()
|
||||
last_plot_time = time.time()
|
||||
|
||||
plot = plot_interface
|
||||
|
||||
while not abort:
|
||||
now = time.time()
|
||||
elapsed_collect = now - last_collect_time
|
||||
@@ -258,7 +266,7 @@ async def runner(
|
||||
last_collect_time = time.time()
|
||||
|
||||
if elapsed_plot > plot_interval: # default: 1 Hz plot updates and save interval
|
||||
plot(channels, in_subplots=in_subplots, start_time=start_time)
|
||||
plot.update_plot(channels, in_subplots=in_subplots, start_time=start_time)
|
||||
|
||||
for channel in channels.values():
|
||||
channel.trigger_save()
|
||||
@@ -276,18 +284,50 @@ async def runner(
|
||||
@click.option("--plot-interval", default=1, help="Interval in seconds for plot update.")
|
||||
@click.option("--save-prefix", default=None, help="Prefix for the save filenames.")
|
||||
@click.option("--in-subplots", default=False, type=bool, help="Whether to plot in subplots.")
|
||||
@click.option(
|
||||
"--web", is_flag=True, default=False, type=bool, help="Whether to use the local web interface as a GUI."
|
||||
)
|
||||
def main(
|
||||
channel_names: Tuple[str, ...],
|
||||
acquire_interval=0.1,
|
||||
plot_interval=1,
|
||||
save_prefix: Optional[str] = None,
|
||||
in_subplots=False,
|
||||
web=False,
|
||||
):
|
||||
""" Plot channels from BS (beam synchronous) or PV (EPICS) sources. """
|
||||
"""Plot channels from BS (beam synchronous) or PV (EPICS) sources."""
|
||||
channels = {channel: ChannelData(channel, save_file_prefix=save_prefix) for channel in channel_names}
|
||||
asyncio.run(runner(channels, acquire_interval=float(acquire_interval), plot_interval=plot_interval, in_subplots=in_subplots))
|
||||
|
||||
start_time = time.time()
|
||||
|
||||
if not web: # console plot version
|
||||
plot = ConsolePlot(channels, start_time=start_time, in_subplots=in_subplots)
|
||||
asyncio.run(
|
||||
runner(
|
||||
channels,
|
||||
plot,
|
||||
acquire_interval=float(acquire_interval),
|
||||
plot_interval=plot_interval,
|
||||
in_subplots=in_subplots,
|
||||
)
|
||||
)
|
||||
|
||||
else: # nicegui version
|
||||
plot = PlotlyPlot(channels, start_time=start_time, in_subplots=in_subplots)
|
||||
|
||||
async def run_async():
|
||||
await runner(
|
||||
channels,
|
||||
plot,
|
||||
acquire_interval=float(acquire_interval),
|
||||
plot_interval=plot_interval,
|
||||
in_subplots=in_subplots,
|
||||
)
|
||||
|
||||
app.on_startup(run_async)
|
||||
|
||||
ui.run(reload=False, port=8004)
|
||||
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
Reference in New Issue
Block a user