Control Hardware and Acquire Data in Parallel
This example shows how to simultaneously control hardware and perform data acquisition on parallel workers.
You can use the parfeval
function to asynchronously control hardware and acquire data with workers in a parallel pool. parfeval
does not block the client, so you can continue to run computations on the client or, like in this example, send instructions to the workers.
This example demonstrates how to set up workers for simultaneous hardware control and data acquisition using a simulated test rig for a quarter-car suspension model. One worker controls an actuator on the test rig, and the other workers acquire data from four sensors on the test rig and send it to the client for visualization. To control the actuator, you send messages to a worker using a PollableDataQueue
object.
You can adapt this approach for any application that requires simultaneous control and monitoring or data acquisition.
Start a parallel pool of five thread workers.
pool = parpool("Threads",5);
Starting parallel pool (parpool) using the 'Threads' profile ... Connected to parallel pool with 5 workers.
Define Road Profile Parameters
The actuator on the test rig follows a road profile to simulate road disturbances. Define the road profile function and its parameters. The profile function models a road surface as a cosine wave, and the parameters are frequency, disturbance duration, and time-step.
params.profileFun = @(t,f) 0.025*(1-cos(8*pi*f*t)); params.frequency = 1; params.duration = 5; params.timeStep = 0.0025;
Create Plot for Visualization
Create a figure and set up animated plots for visualizing the road displacement, suspension deflection, body travel, and body acceleration.
[fig,p] = createPlot;
Set Up Data Queues
Create a DataQueue
and use afterEach
to specify the function to execute each time the queue receives data. The displayOnClient
function plots the data from the workers and is defined at the end of the example.
resultsDq = parallel.pool.DataQueue; afterEach(resultsDq,@(readings) displayOnClient(p,readings));
To enable communication between the client and worker connected to the hardware device, create PollableDataQueue
objects with the Destination
argument set to "any"
. This type of PollableDataQueue
object allows both the client and worker to send and receive messages.
workerToClientPdq = parallel.pool.PollableDataQueue(Destination="any"); clientToWorkerPdq = parallel.pool.PollableDataQueue(Destination="any");
Start Data Acquisition on Workers
To start collecting data from the different sensors, use parfeval
to execute the readAndSend
function for each sensor. The readAndSend
function is defined at the end of the example.
readDuration = 30; % seconds numSensors = 4; sensorFutures(1,numSensors) = parallel.FevalFuture; for s = 1:numSensors sensorFutures(s) = parfeval(@readAndSend,2,s,readDuration,resultsDq); end set(fig,"Visible","on")
Perform Real-Time Hardware Control
To perform real-time hardware control, you define the worker function, connect to the hardware and send instructions to it, and then update the hardware parameters before stopping the process.
Define Worker Function for Hardware Control
Define a function for the worker that allows dynamic control of a hardware device in real time. The connectToActuator
function performs initial hardware setup and signals readiness to the client using the workerToClientPdq
queue. The function waits for initial parameters from the clientToWorkerPdq
queue and begins generating and sending road profiles to the actuator based on these parameters. In a loop, the function continues to send profiles while actively checking for client instructions to update parameters or stop operations. The generateRoadProfile
and sendToActuator
functions are included as supporting files to this example.
function connectToActuator(workerToClientPdq,clientToWorkerPdq) % Perform additional hardware setup. send(workerToClientPdq,"Ready"); % Wait for instructions. params = poll(clientToWorkerPdq,inf); send(workerToClientPdq,sprintf("Started sending road profiles to actuator.")); while true roadProfile = generateRoadProfile(params); sendToActuator(roadProfile); [change,OK] = poll(clientToWorkerPdq,roadProfile.time(end)-1); if OK if isstruct(change) params = change; send(workerToClientPdq,sprintf("Updated road profile parameters.")); else strcmp(change,"stop") send(workerToClientPdq,sprintf("Stopped sending profiles to actuator.")); return end end end end
Connect to Hardware
To instruct the worker to connect to the actuator, submit a parfeval
computation to run the connectToActuator
function on a worker. Poll the workerToClientPdq
queue to receive confirmation, waiting indefinitely to ensure the actuator is ready to begin.
actuatorFuture = parfeval(@connectToActuator,0,workerToClientPdq,clientToWorkerPdq); poll(workerToClientPdq,inf)
ans = "Ready"
Send Instructions to Hardware
Next, use the clientToWorkerPdq
queue to instruct the worker to start sending the road profiles to the actuator. Receive confirmation from the worker.
send(clientToWorkerPdq,params); poll(workerToClientPdq,inf)
ans = "Started sending road profiles to actuator."
The parfeval
function does not block the client, so you can continue working while the workers continue their computations. For this example, use pause
to allow the worker to send data to the actuator for four seconds.
pause(4)
Update Hardware Parameters and Stop the Process
Use the clientToWorkerPdq
queue to update the road profile parameters for the actuator. Change the road profile function and send the updated parameters to the worker. You can see a change in the road displacement plot after five seconds when the actuator receives the next road profile. Wait to receive confirmation from the worker.
params.profileFun = @(t,f) 0.02*sin(2*pi*f*t); send(clientToWorkerPdq,params); poll(workerToClientPdq,inf)
ans = "Updated road profile parameters."
To stop sending profiles to the actuator and terminate the parfeval
computation, send a "stop"
message to the worker. The road displacement plot shows a displacement of 0 after about 15 seconds. Again, wait to receive confirmation.
pause(4)
send(clientToWorkerPdq,"stop");
poll(workerToClientPdq,2)
ans = "Stopped sending profiles to actuator."
Retrieve Sensor Data
Wait for the sensor futures to complete and retrieve the sensor readings from the parfeval
futures.
wait(sensorFutures); [tAll,dAll] = fetchOutputs(sensorFutures);
Supporting Functions
readAndSend
The readAndSend
helper function reads sensor data in one-second increments and sends it to the client. The connectToSensor
function simulates the reading of data from different sensors in a quarter-car test rig and is attached to this example as a supporting file.
function [tAll,dAll] = readAndSend(sensorID,readDuration,resultDq) tAll = []; dAll = []; for duration = 1:readDuration [t,d] = connectToSensor(sensorID,1); readings.id = sensorID; readings.x = t; readings.y = d; tAll = [tAll,t]; dAll = [dAll,d]; send(resultDq,readings) end clear connectToSensor end
displayOnClient
The displayOnClient
function updates the animated plots with new data points received from the sensors.
function displayOnClient(p,readings) idx = readings.id; addpoints(p(idx),readings.x,readings.y) drawnow limitrate; end
createPlot
The createPlot
function sets up the figure and animated lines for displaying the sensor readings.
function [fig,p] = createPlot fig = figure(Name="Quarter Car Test Rig",Visible="off",Position=[263 429 1124 417]); tl = tiledlayout(fig,2,3,TileSpacing ="compact",Padding ="compact"); title(tl,"Quarter Car Test Rig"); nexttile(tl,[2 1]) imagesc(imread("car-suspension.png")); lineColor = ["k","b","g","m"]; titleStrs = ["Road Displacement","Suspension Deflection","Body Travel","Body Acceleration"]; yAxisStrs = ["Displacement (m)","Deflection (m)","Displacement (m)","Acceleration (m/s^2)"]; p = gobjects(4); for idx=1:4 nexttile(tl); xlabel("Time (s)"); ylabel(yAxisStrs(idx)); title(titleStrs(idx)) p(idx) = animatedline(NaN,NaN,Color=lineColor(idx)); xlim([0 20]) end end