The software package part of Cert3D will be PC-based application. It shall have the following features:
-
Have USB interface to the Cert3D hardware board and be able to download information.
-
(If possible) Interface with the printer directly to be able to send g codes and request status updates.
-
cert3d.py: this is the main file.
-
ScopeResultsWindow.py: This file is generated by wxFormBuilder and is the layout of the window.
-
ScopePanel.py: This is a custom widget to draw oscilloscope-type information on a panel.
-
Channel.py: This hold information about a singl channel, either digital or analog.
-
BilevelData.py: This holds data on a single channel of digital data.
-
PlotData.py: This holds xy data on a single channel.
This widget inherits from wx.Panel and nicely displays a number of channels on a form. Data can either be bilevel or plot.
To add a channel, we use the Add(data)
function where data
is either (for now) BilevelData
or PlotData
.
Channel names should be on the left-hand side of the widget. Each channel can have a custom height, with the minimum height being the height of the channel name.
The width of the channel name display shall be fixed to the minimum possible size and recalculated when a new channel is added.
This should hold aethestic information about the channel (color, height, scaling). It will also do the drawing.
The C3D board can be sent commands over USB. The following are implemented:
start
- Begin streaming over the USB port.
stop
- Stop streaming data.
info
- Send the configuration packet over the USB port. Streaming must be disabled.
The information packet contains information on how to interpret data coming over the USB. Note that some configuration is done over the USB.
char[9] start_string
- The string
InfoStart
to denote the start of this packet.
- The string
uint32_t clock
- System clock speed.
uint8_t signal_channel_count
- Number of signal channels in the packet.
uint8_t adc_channel_count
- Number of ADC channels in the packet.
- For each signal channel:
uint32_t clock
- Clock speed in Hz of this channel.
- For each ADC signal:
float zero_value
- Voltage value corresponding to a zero count value on the channel.
float high_value
- Voltage value corresponding to a max count (4095) on this channel.
char[8] end_string
- The string
InfoStop
to denote the start of this packet.
- The string
I added a third signal type: TriStateData
. This is similar to the bilevel data, except it has a third state. The third state will be high-z and will display as grayed out over the entire range. The idea is to use this to simplify bilevel data with millions of points for zoomed out views.
I expect to have massive data sets. If a STEP channel for a motor triggers at ~100kHz, that is 200,000 data points per second. One can see how this adds up. Drawing all data set points would be prohibitve.
To simplify bilevel data, we can approximate it using a
I also expect channels to be dormant for long periods of time. In a zoomed out view, it would be nice to be able to see period of inactivity and activity.
Data sets will be simplified by augmenting the data set with one or more simplified sets. These sets will have less points than the original and will be used when the data set is zoomed out.
Zoom value is a ticks/pixel value.
-
Look at the original data set. If it has less than X (maybe 1000?) edges, do not simplify. Else, continue.
-
Find the level of zoom at which we have no more than X (maybe 1000) edges per X (maybe asize(2000)?) pixels. This is our maximum zoom.
-
Find the level of zoom to fit the entire data set within X (maybe asize(2000)?) pixels. This is our minimum zoom.
-
Add zoom levels between min and max such that the zoom value jumps by no more than a factor of X (maybe 2.0)?
At this point, we have all zoom levels we need to create, with the most zoomed in level equal to the original data set.
Better idea:
- Find the maximum zoom level as in (2) above. Then, create a list of edge durations and sort it. Collapse each duration less than double the median edge duration. Calculate level of zoom for this new data set, and repeat until we have under 1000 edges in the set. This should only repeat a few times.
The problem with this is the "collapse a duration." Collapsing a single duration doesn't actuall achieve anything except reduce quality of the data set. We need to collapse two durations to reduce the number of edges. With that in mind, here is an improved algorithm:
-
Find the maximum zoom level as in (2) above.
-
Create a list of double edge durations (i.e. the duration between edge 0 and edge 2, edge 1 and edge, 3, edge 2 and edge 4, etc...). Take twice the median of this list. Collapse all durations which are less than this value.
-
Note that collapsing a duration means setting the value from 0 or 1 to 2, and combining adjacent durations of value 2.
This works really well. I implemented it within a DataCluster
object for now.
We have a lot of different objects which are all related. Maybe we can get rid of some and make the heirarchy flatter? Maybe we can eliminate or consolidate some *Data
types?
Where should all these objects live, in terms of files?
Here is our current file/object heirarchy:
- File
AnalysisWindowBase.py
- Object
AnalysisWindowBase
- (Auto-generated from wxFormBuilder)
- Object
- File
cert3d.py
- Object
AnalysisWindow
which inherits fromAnalysisWindowBase
- Inherits from
AnalysisWindowBase
- Inherits from
- Object
- File
ScopePanel.py
- Object
ScopePanel
which inherits fromwx.Panel
- Object
- File
ScopeChannel.py
- Object
Signal
- Object
ScopeChannel
- Object
- File
dpi.py
- Contains methods for working with high (and low) DPI displays.
- File
BilevelData.py
- Object
DataCluster
- Object
BilevelData
- Object
TriStateData
- Object
- File
PlotData.py
- Object
PlotData
- Object
Here is our window object heirarchy
- Object
AnalysisWindow
- Many standard objects like buttons, which are not listed here
- Object
ScopePanel
- Many visual properties like zoom state, channel length, etc.
- Time offset of start of display (in seconds)
- Zoom state (in pixels per second)
- Many
ScopeChannel
objects- channel height
- (if necessary as for
PlotData
), value at bottom and top - Each can have many
Signal
objects- name of signal
- color of signal
- thickness of signal
- reference to active data
- function to select active data given a zoom factor
DataCluster
object with min zoom state and data objects
I think I should eliminate the DataCluster
object and turn it into a property of a Signal
.
Here's what a Signal
could look like:
- Object
Signal
- property
name
for name of signal, displayed on left of the channel - property
color
of typewx.Colour
- property
thickness
for thickness in pixels of the signal drawn - property
active_data
which points to the active data based on zoom level of type (BilevelData
,TriStateData
, orPlotData
) - property
data_cluster
which is a list of (min_zoom
,data
)- property
min_zoom
is the minimum zoom at which this data can be active - property
data
is a data of type (BilevelData
,TriStateData
, orPlotData
)
- property
- function
draw_signal
which draws the signal on a rectangle- parameter
wx.DC
- parameter
wx.Rectangle
- parameter
pixels_per_second
- parameter
start_time
which is the time at the very left of the rectangle - parameter
value_range
which is the value at the bottom and top of the rectangle. Only used for data of typePlotData
.
- parameter
- property
The Signal
object should have the following:
- Parameter
start_time
for the start of the data time - Function
get_length()
which returns the length of the data
Each *Data
type should have the following parameters/functions:
- Parameter
points
which holds the data points in a format used only by itself. - Parameter
start_time
for the start of the data time - Function
get_edge_near_time(time)
which returns the time of the edge closest to the given time.
I did this, it seems way better. The cert3d.py
file is a little busy, but workable.
The slave thread is necessary because the buffers for the USB data on both the STM32 side and the computer side are small. In order to maximize throughput, the port needs to be read out very quickly. The slave this does this without any interruption from other things such as UI redrawing. Even with the PIL, this works well since most of the waiting is IO based.
The master thread needs to communicate with the slave. It does so through some global variables.
The master thread should be able to know when the slave thread has crashed. It can do so with this Thread.is_alive
function.
Here are some instructions we need to implement:
- Open USB port (or don't)
- Start or stop logging to file, or read but ignore data
- File is only open when logging is active
- Exit thread
Once we have the STEP
and DIR
channels, we need to estimate the position, velocity and acceleration of the channel. This is a nontrivial exercise.
-
To calculate
POS
, assume each active edge onSTEP
is immediate. -
After
POS
is calculated, post-process it by looking for regions of large idle time and add points to make thePOS
channel constant during the majority of these times. Use an idle time of 1ms.- For example, if we have data points
(0.0 s, 0)
,(1.0 s, 1)
, add a point at(0.999s, 0)
.
- For example, if we have data points
-
Calculate
VEL
as the sum of many triangular pulses, the area of each is the same and the duration is varied based on nearby points. This may be tuned.-
We need a way to calculate (x, y) points from a bunch of triangular pulses.
-
Put pulses in a list sorted by the endtime.
-
I did this. It works okay.
-
-
Calculate
ACC
as the exact derivative ofVEL
. Since VEL is piecewise linear, ACC is piecewise constant.- This produces some artifacts, which are real but undesired. One way to remove these would be to apply a slew rate control to the signal.
In the Test Runner window, I need a method for saving many different tests. I'll have a combo box populated with the name of each test along with a Save and Delete buttons. They will be saved in the LOCALAPPDATA folder in the tests.py
file in the following format:
test["basic_accel"] = ["G1 X0", "G1 X1"]
...
I can then use exec
to read these back in.
I implemented this. It's nice.
We need a way to create a simplified data set of out PlotData. Maybe we can have another Data type similar to PlotData, except it would have a range of values. I have two ideas.
-
For each interval, draw a rectangle with a stipple interior over the range of values it can have. This has one downside in that if the data is towards one side of the range, that information is lost in translation.
-
For each interval, find the mean value. Draw a line connecting these mean values. Also find the range of values and maybe draw a thick stippled line with this information.
After looking, there is a DrawPolygon function. I'm sure this could be useful.
How can I calculate somewhat tight-fitting min and max value curves?