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:

  1. Read the oldest data first, trying to follow all the sensor data, but never catching up.

  2. 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

Popular posts from this blog

Threads of Confusion

Comparing Ada and High Integrity C++

Ada vs C++ Bit-fields