A robotic vehicle rolls through mud. Its Lidar lens is obscured. The sensor stops responding. What should the software do?
How do you design for that moment?
I have spent decades designing and implementing systems that operate in and interact with the real world, including developing survivable architectures and logistics-aware control software. Every one of these systems has detailed behavior and performance requirements, but the requirements alone do not tell the whole story. The story also needs to include what the system is intended to accomplish and what kinds of environmental conditions the system is expected to encounter.
This article will deal with a small software design feature for a teleoperated robotic ground vehicle.
Each vehicle is
equipped with many sensors. Sensor can include visual and infra-red
cameras, Lidar sensors and radar sensors. Every sensor generates data
very quickly. A human may consider the data generation to be
continuous, but in reality the data is generated in very quick
pulses. The rate the data is generated is commonly faster than a
robotic system’s compute resources can process the data. Data is
passed from the sensor to the compute system by having the sensor
write some data to a buffer, consider the buffer to be a bucket if
you will, and having the compute system read from the buffer as fast
as it can. Since the sensor fills the buffer faster than the compute
system can empty the buffer our system is faced with two choices:
Read the oldest data first, trying to follow all the sensor data, but never catching up.
Read only the newest data.
Option 1 results in reading stale data. Stale data is data that has been superseded by newer data. Stale data is very bad for command and control systems such as a teleoperated robotic ground vehicle. The system will always be reacting to the distant past and not the conditions representing NOW.
Option 2 results in skipping all the old data and only reading the NOW data. The data is as fresh as the system can handle.
I decided the
robotic control system needed the NOW and not the stale data. I
needed to design a data buffer that only held NOW data, allowing the
compute system to ignore any data it did not have time to process.
Whenever the compute system could read from the buffer it only
encountered NOW data. I decided the buffer only needed to hold one
data element at a time. The sensor would over-write that data each
time it delivered an new sensor message. The reader would read the
contents of the buffer whenever it could.
Having
decided on a single element buffer I then considered how many kinds
of sensors might be on a vehicle and how many sensors could be
obscured or damaged by interaction with the environment.
Environmental interactions could include sensors being damaged by
contact with tree branches, human structure, or obscured by sand,
mud, snow, or heavy rain. How should the system respond to a
non-responsive sensor? Ideally the system would simply wait for a
response from the particular non-responding sensor and continue
operating with the available sensor data. In technical terms the
system would continue operating in a degraded mode. If the obscurant
causing a particular sensor to not respond was cleared the system
should immediately resume processing that sensor data.
I
was using the Ada programming language, which has syntax within the
core language for dealing with concurrent processing issues. The
feature I needed to create my data buffer shared by the sensor output
and the compute system input is called a Protected Type. Furthermore,
I had recognized the need to create buffers for many different kinds
of sensor input. The Ada solution to this problem was the creation of
a generic Protected Type, allowing the message type to be passed as a
generic parameter.
My first attempt at the generic Protected Type.
A protected type allows three kinds of methods.
Procedures have unconditional read-write access to the protected object and implement an implicit read-write lock on the protected object.
Entries have conditional read-write access to the projected object and implement an implicit read-write lock on the protected object. The Ada task calling a protected entry will suspend while the specified condition evaluates to FALSE and will resume execution as soon as the specified condition evaluates to TRUE.
Functions have shared read-only access to the protected object and implement an implicit shared read-only lock on the protected object.
Protected types are written in two pieces. The interface to the protected type is written first. The implementation of the protected type is called the body of the protected type. It is written second.
This implementation encapsulates the protected type in a generic Ada package.
generic
type Message_T is private;
package single_element_buffer is
protected type Buffer is
procedure write (value : in Message_T);
function read return Message_T;
private
Msg_Buf : Message_T;
end Buffer;
end single_element_buffer;
The body of the package and the protected type is
package body single_element_buffer is
------------
-- Buffer --
------------
protected body Buffer is
-----------
-- write --
-----------
procedure write (value : in Message_T) is
begin
Msg_Buf := value;
end write;
----------
-- read --
----------
function read return Message_T is
begin
return Msg_Buf;
end read;
end Buffer;
end single_element_buffer;
My second attempt at the generic protected
type. This attempt replaced the protected function with a protected
entry.
This version adds a second data member to the protected type. That data member is a Boolean variable indicating whether the data in the buffer is new.
generic
type Message_T is private;
package single_element_buffer is
protected type Buffer is
procedure write (value : in Message_T);
entry read (value : out Message_T);
private
Msg_Buf : Message_T;
Is_New : Boolean := False;
end Buffer;
end single_element_buffer;
The second version protected
body deals with these changes.
package body single_element_buffer is
------------
-- Buffer --
------------
protected body Buffer is
-----------
-- write --
-----------
procedure write (value : in Message_T) is
begin
Msg_Buf := value;
Is_New := True;
end write;
----------
-- read --
----------
entry read (value : out Message_T) when Is_New is
begin
value := Msg_Buf;
Is_New := False;
end read;
end Buffer;
end single_element_buffer;
Now the write procedure sets Is_New to True every time it writes a value. The protected entry has a guard condition stated as “when Is_New”. This means the entry is allowed to execute when Is_New evaluates to True. Inside the entry the entry parameter named value is set to the value of Msg_Buf and Is_New is set to False.
The entry causes the
calling Ada task to suspend while Is_New evaluates to False and
allows the Ada task to execute the entry when Is_New evaluates to
True.
This version satisfies all my
considered requirements for the buffer used between a sensor and the
compute system.
This wasn’t just a buffer—it was a promise. A promise that the system would never act on stale data. That promise shaped the architecture, protected the mission, and taught the team how to design for trust.
In a world of shortcuts and fragile abstractions, this Ada pattern modeled sanctuary. It taught engineers to wait, to listen, and to act only when the system was ready. That’s not just good design—it’s covenantal stewardship.
Have you ever faced a design decision where simplicity conflicted with integrity? Where the easy path risked operational trust? I’d love to hear your story. Survivable systems begin with shared wisdom.
Comments
Post a Comment