The code basically works like this:
- Initial setup
- Update data
- Update plots
- Goto 2
Let's go through each of these.
Initial Setup
The initial setup creates the lists (actually deques) the data will be stored in, the TFT setup, and the plot setup including the axis and the plot lines. This ends up being a fair amount of the code. It's all this:
# Setup X data storage
x_time = [x * REFRESH_RATE for x in range(HIST_SIZE)]
x_time.reverse()
# Setup Y data storage
y_data = [ [deque([None] * HIST_SIZE, maxlen=HIST_SIZE) for _ in plot['line_config']]
for plot in PLOT_CONFIG
]
# Setup display
disp = ili9341.ILI9341(board.SPI(), baudrate = 24000000,
cs = digitalio.DigitalInOut(board.D4),
dc = digitalio.DigitalInOut(board.D5),
rst = digitalio.DigitalInOut(board.D6))
# Setup plot figure
plt.style.use('dark_background')
fig, ax = plt.subplots(2, 1, figsize=(disp.width / 100, disp.height / 100))
# Setup plot axis
ax[0].xaxis.set_ticklabels([])
for plot, a in enumerate(ax):
# add grid to all plots
a.grid(True, linestyle=':')
# limit and invert x time axis
a.set_xlim(min(x_time), max(x_time))
a.invert_xaxis()
# custom settings
if 'title' in PLOT_CONFIG[plot]:
a.set_title(PLOT_CONFIG[plot]['title'], position=(0.5, 0.8))
if 'ylim' in PLOT_CONFIG[plot]:
a.set_ylim(PLOT_CONFIG[plot]['ylim'])
# Setup plot lines
#pylint: disable=redefined-outer-name
plot_lines = []
for plot, config in enumerate(PLOT_CONFIG):
lines = []
for index, line_config in enumerate(config['line_config']):
# create line
line, = ax[plot].plot(x_time, y_data[plot][index])
# custom settings
if 'color' in line_config:
line.set_color(line_config['color'])
if 'width' in line_config:
line.set_linewidth(line_config['width'])
if 'style' in line_config:
line.set_linestyle(line_config['style'])
# add line to list
lines.append(line)
plot_lines.append(lines)
Update Data
Updating the data is done in the function update_data(). You would change this to be whatever you want (more info below). Wherever the new data comes from, you would then just add each new data point to the data stores. For the basic example, this is just random numbers and a sine curve:
def update_data():
''' Do whatever to update your data here. General form is:
y_data[plot][line].append(new_data_point)
'''
# upper plot data
for data in y_data[0]:
data.append(random.random())
# lower plot data
y_data[1][0].append(math.sin(0.5 * time.monotonic()))
Each line of the plot has a dedicated deque to store its data. Each plot stores these deques in a list. These lists are then stored in the variable y_data. Therefore, to access the line for a given plot, you'd use the syntax y_data[plot][line]. You'll generally want to use the append() method to add each new data point. The result is something like:
y_data[plot][line].append(new_data_point)
You can also use Python iterator syntax if that works for your scenario. That's how the random data is appended in the example above.
Also remember that indexing is 0 based. So the top plot is 0 and the bottom plot is 1.
Update Plots
Updating the plots is where Matplotlib and Pillow are used. This is all done in the function update_plot(). The basic idea is to update the ydata for each line with the current data. The plot is then re-rendered. Pillow is then used to generate an image object that can be sent to the TFT display.
You generally won't have to deal with this function.
def update_plot():
# update lines with latest data
for plot, lines in enumerate(plot_lines):
for index, line in enumerate(lines):
line.set_ydata(y_data[plot][index])
# autoscale if not specified
if 'ylim' not in PLOT_CONFIG[plot].keys():
ax[plot].relim()
ax[plot].autoscale_view()
# draw the plots
canvas = plt.get_current_fig_manager().canvas
plt.tight_layout()
canvas.draw()
# transfer into PIL image and display
image = Image.frombytes('RGB', canvas.get_width_height(),
canvas.tostring_rgb())
disp.image(image)
print("looping")
while True:
update_data()
update_plot()
time.sleep(REFRESH_RATE)
Customization
We skipped over a bunch of additional "initial setup" at the top of the code. This is all the code between the two comment lines:
#==| User Config |======================
This is where you'll edit the code to customize it for your use. There are two general parts:
- Configure behavior and aesthetics via the
CONSTANTS - Change the
update_data()function for your use case
Behavior and Aesthetics
The two constants REFRESH_RATE and HIST_SIZE are pretty straight forward. They determine how often the data plot is updated and how much total data to store. The two together define the total time window for the plot as REFRESH_RATE * HIST_SIZE.
The more interesting constant is the PLOT_CONFIG structure. As the name implies, this is how you will configure the two plots. It's a bit of a nested mess of tuples and dictionaries. But hopefully it's laid out in a way to make it easy to edit.
At a minimum, you need the line_config entry for each plot with one empty dictionary for each line of the plot. So the absolute minimum PLOT_CONFIG would look like:
PLOT_CONFIG = (
#--------------------
# PLOT 1 (upper plot)
#--------------------
{
'line_config' : (
{ },
)
},
#--------------------
# PLOT 2 (lower plot)
#--------------------
{
'line_config' : (
{ },
)
}
)
This would create a single plot line for both the upper and lower plots with default values used. A plot line is created for each dictionary entry in line_config. So if you wanted to add a second plot line to the upper plot, the above would become:
PLOT_CONFIG = (
#--------------------
# PLOT 1 (upper plot)
#--------------------
{
'line_config' : (
{ },
{ },
)
},
#--------------------
# PLOT 2 (lower plot)
#--------------------
{
'line_config' : (
{ },
)
}
)
The only change is one additional line with an empty dictionary { }.
The PLOT_CONFIG in the basic test example tries to demonstrate your various customization options. For any of them, if they are not specified, default values are used. So, looking at the basic test example:
PLOT_CONFIG = (
#--------------------
# PLOT 1 (upper plot)
#--------------------
{
'line_config' : (
{'color' : '#FF0000', 'width' : 2, 'style' : '--'},
{ },
)
},
#--------------------
# PLOT 2 (lower plot)
#--------------------
{
'title' : 'sin()',
'ylim' : (-1.5, 1.5),
'line_config' : (
{'color' : '#00FF00', 'width' : 4},
)
}
)
The upper plot has no title or y axis limits set. The y axis will autoscale. Two plot lines are setup in line_config. The first line has color, width, and style specified. The second line has nothing specified so will use default settings.
The lower plot specifies a plot title. This is simply some text that will be shown at the top of the plot. It also species specific y axis limits via ylim, therefore the y axis will not autoscale. A single plot line is setup in line_config with color and width specified. A default style will be used.
In general, you should match up the number of lines configured in line_config with the data that is updated in update_data().
Custom Data
The update_data() function can be whatever you want. As mentioned above, the general syntax for a given line of a given plot is:
y_data[plot][line].append(new_data_point)
The system status examples that follow will provide more examples on how this function is used.
Page last edited March 08, 2024
Text editor powered by tinymce.