Chapter 11: Robot Software Architecture

Lesson 4: Logging, Monitoring, and Diagnostics

This lesson introduces the engineering foundations of logging, system monitoring, and model-based diagnostics for robots. We formalize what to log, how to compress and time-stamp data, and how to use control-theoretic residuals plus statistical tests to detect faults. The emphasis is on principled design choices that enable safe, debuggable, and reliable robot behavior.

1. Motivation and Definitions

Robots are cyber-physical systems: software decisions cause real forces, motion, and possible hazards. Therefore, we need:

  • Logging: persistent records of events and numeric signals.
  • Monitoring: online measurement of system health and performance.
  • Diagnostics: reasoning about why anomalies occur, ideally with models.

Formally, let the robot software produce an event stream \( e(t) \in \mathcal{E} \) and a signal vector \( \mathbf{s}(t) \in \mathbb{R}^m \). A log is a time-ordered sequence \( \mathcal{L} = \{(t_i, e_i, \mathbf{s}_i)\}_{i=1}^N \).

Since sensors and controllers are sampled, we often work in discrete time. Let \( T_s \) be the sampling period and \( k = \lfloor t/T_s \rfloor \). Then \( \mathbf{s}_k = \mathbf{s}(kT_s) \), and the log is \( \mathcal{L} = \{(k, e_k, \mathbf{s}_k)\}_{k=0}^{N-1} \).

flowchart TD
  A["Robot software"] --> B["Event + signal sources"]
  B --> C["Logging layer"]
  B --> D["Monitoring layer"]
  C --> E["Persistent storage: \nfiles / DB / ring buffer"]
  D --> F["Online dashboards / \nalerts"]
  D --> G["Diagnostics layer"]
  G --> H["Fault isolation + recovery actions"]
        

2. Logging Design: Rates, Time Stamps, and Compression

Logging every variable at maximum rate is infeasible. Logging is a sampling problem. Suppose the true continuous signal \( s(t) \) is band-limited with maximum frequency \( f_{\max} \). Nyquist sampling requires \( f_s \ge 2f_{\max} \), where \( f_s = 1/T_s \).

\[ f_s \ge 2 f_{\max} \quad \Longleftrightarrow \quad T_s \le \frac{1}{2 f_{\max}} . \]

If we log at \( f_\ell \) lower than \( f_s \), we effectively downsample: \( s^\ell_n = s_{nq} \) with downsample factor \( q = f_s/f_\ell \). Aliasing risk is controlled by pre-filtering (low-pass) or logging derived features.

Time-stamp jitter. Let measured time stamps be \( \hat{t}_k = kT_s + \delta_k \) where \( \delta_k \) is timing noise (jitter). For robust synchronization, we need bounded jitter: \( |\delta_k| \le \delta_{\max} \).

Storage budget. If each sample stores \( b \) bytes and we log \( m \) channels at rate \( f_\ell \), the data rate is \( R = b m f_\ell \) bytes/s and storage for duration \( T \) is \( S = R T \).

\[ R = b m f_\ell , \qquad S = b m f_\ell T . \]

To satisfy a storage limit \( S \le S_{\max} \), we must choose \( f_\ell \) or \( m \) (channels) accordingly:

\[ f_\ell \le \frac{S_{\max}}{b m T}. \]

Event vs. signal logging. For rare events (mode switches, safety trips), event-based logging is superior. If event arrivals follow a Poisson process with rate \( \lambda \), then expected events in time \( T \) is \( \mathbb{E}[N_T] = \lambda T \), so storage is linear in event density rather than sample rate.

3. Monitoring: Health Metrics and Change Detection

Monitoring is online, so we track low-dimensional metrics \( h_k = g(\mathbf{s}_k) \) (CPU load, battery voltage, joint temperature, control error). We compare \( h_k \) to limits.

Assume under normal operation \( h_k \) is stationary with mean \( \mu_0 \) and variance \( \sigma_0^2 \). A common alert uses a z-score: \( z_k = (h_k-\mu_0)/\sigma_0 \).

\[ z_k = \frac{h_k - \mu_0}{\sigma_0}. \]

If \( h_k \sim \mathcal{N}(\mu_0,\sigma_0^2) \), then \( z_k \sim \mathcal{N}(0,1) \). For false-alarm probability \( \alpha \), choose threshold \( \gamma = \Phi^{-1}(1-\alpha/2) \), and alert when \( |z_k| > \gamma \).

\[ \Pr(|z_k| > \gamma) = \alpha \quad \Longrightarrow \quad \gamma = \Phi^{-1}(1-\alpha/2). \]

Proof (normal false-alarm rate): Since \( z_k \sim \mathcal{N}(0,1) \), \( \Pr(z_k > \gamma) = 1-\Phi(\gamma) \). Symmetry gives \( \Pr(|z_k| > \gamma) = 2(1-\Phi(\gamma)) \). Setting this to \( \alpha \) yields the expression above.

Slow drift monitoring. A moving average \( \bar{h}_k = \frac{1}{W}\sum_{i=0}^{W-1} h_{k-i} \) reduces noise and detects trends. Under independence, \( \operatorname{Var}(\bar{h}_k) = \sigma_0^2/W \), so thresholds tighten by \( \sqrt{W} \).

\[ \bar{h}_k = \frac{1}{W}\sum_{i=0}^{W-1} h_{k-i}, \qquad \operatorname{Var}(\bar{h}_k)=\frac{\sigma_0^2}{W}. \]

4. Diagnostics: Residual Generation and Hypothesis Tests

Using your linear-control background, consider a discrete-time LTI plant:

\[ \mathbf{x}_{k+1} = \mathbf{A}\mathbf{x}_k + \mathbf{B}\mathbf{u}_k + \mathbf{w}_k, \qquad \mathbf{y}_k = \mathbf{C}\mathbf{x}_k + \mathbf{v}_k, \]

where \( \mathbf{w}_k \) and \( \mathbf{v}_k \) are process and sensor noise. A standard observer (e.g., Luenberger) is:

\[ \hat{\mathbf{x}}_{k+1} = \mathbf{A}\hat{\mathbf{x}}_k + \mathbf{B}\mathbf{u}_k + \mathbf{L}\left(\mathbf{y}_k - \mathbf{C}\hat{\mathbf{x}}_k\right). \]

Define the innovation residual \( \mathbf{r}_k = \mathbf{y}_k - \mathbf{C}\hat{\mathbf{x}}_k \). Under no fault, residuals are due only to noise. Under a fault (sensor bias, actuator loss), residuals become structured and nonzero.

flowchart TD
  U["Inputs u_k"] --> OBS["Observer"]
  Y["Measurements y_k"] --> OBS
  OBS --> XHAT["State estimate xhat_k"]
  Y --> RES["Residual r_k = y_k - C xhat_k"]
  RES --> TEST["Statistic J_k"]
  TEST -->|J_k > gamma| ALARM["Fault alarm"]
  TEST -->|J_k <= gamma| OK["Normal"]
        

Assume \( \mathbf{r}_k \) is approximately Gaussian: \( \mathbf{r}_k \sim \mathcal{N}(\mathbf{0},\mathbf{S}) \) when the system is healthy. A classic test is the quadratic form

\[ J_k = \mathbf{r}_k^\top \mathbf{S}^{-1} \mathbf{r}_k . \]

Theorem: If \( \mathbf{r}_k \sim \mathcal{N}(\mathbf{0},\mathbf{S}) \) and \( \mathbf{S} \) is positive definite, then \( J_k \) follows a chi-square distribution with \( p = \dim(\mathbf{y}) \) degrees of freedom: \( J_k \sim \chi^2_p \).

Proof: Let \( \mathbf{S}=\mathbf{Q}\mathbf{\Lambda}\mathbf{Q}^\top \) be an eigendecomposition with orthonormal \( \mathbf{Q} \) and diagonal \( \mathbf{\Lambda} \). Define whitened residual \( \mathbf{z}_k = \mathbf{\Lambda}^{-1/2}\mathbf{Q}^\top \mathbf{r}_k \). Since affine transforms of Gaussian variables are Gaussian, \( \mathbf{z}_k \sim \mathcal{N}(\mathbf{0},\mathbf{I}) \). Then

\[ J_k = \mathbf{r}_k^\top \mathbf{S}^{-1}\mathbf{r}_k = \mathbf{r}_k^\top \mathbf{Q}\mathbf{\Lambda}^{-1}\mathbf{Q}^\top \mathbf{r}_k = \mathbf{z}_k^\top \mathbf{z}_k = \sum_{i=1}^{p} z_{k,i}^2 . \]

Each \( z_{k,i} \sim \mathcal{N}(0,1) \) independently, so the sum of squares is \( \chi^2_p \). ∎

Therefore, select a detection threshold \( \gamma \) from the chi-square quantile: alert when \( J_k > \gamma \), where \( \Pr(J_k > \gamma)=\alpha \).

\[ \gamma = \chi^2_{p,\,1-\alpha}. \]

This connects diagnostics to a measurable false-alarm rate. Isolation can be done by defining multiple residuals targeting different subsystems.

5. Implementation Patterns (Python, C++, Java, Matlab/Simulink)

The goal is to show reusable patterns, not a specific middleware. We log structured events and compute a residual-based diagnostic statistic.

5.1 Python (standard logging + residual test)


import logging
import numpy as np
from collections import deque

# --- Logging setup ---
logging.basicConfig(
    filename="robot_run.log",
    level=logging.INFO,
    format="%(asctime)s | %(levelname)s | %(message)s"
)
logger = logging.getLogger("robot")

# --- Monitoring window ---
W = 50
health_window = deque(maxlen=W)

# --- Example LTI observer residual test ---
A = np.array([[1.0, 0.01],
              [0.0, 1.0]])
B = np.array([[0.0],
              [0.01]])
C = np.array([[1.0, 0.0]])
L = np.array([[0.2],
              [2.0]])

S = np.array([[0.04]])          # nominal residual covariance
S_inv = np.linalg.inv(S)
gamma = 6.635                   # chi-square(1, 0.99) threshold (alpha=0.01)

xhat = np.zeros((2,1))

def step(u_k, y_k, cpu_load):
    global xhat

    # Log raw signals
    logger.info(f"u={float(u_k):.3f}, y={float(y_k):.3f}, cpu={cpu_load:.2f}")

    # Monitoring metric with moving average
    health_window.append(cpu_load)
    cpu_avg = sum(health_window) / len(health_window)
    if cpu_avg > 0.9:
        logger.warning(f"High CPU average: {cpu_avg:.2f}")

    # Observer update
    r_k = y_k - C @ xhat
    xhat = A @ xhat + B @ u_k + L @ r_k

    # Diagnostic statistic
    J_k = float(r_k.T @ S_inv @ r_k)
    if J_k > gamma:
        logger.error(f"Fault detected: J={J_k:.3f} > gamma={gamma:.3f}")

# Example usage
for k in range(100):
    u = np.array([[0.1]])
    y = np.array([[0.0]])  # pretend measurement
    cpu = 0.5
    step(u, y, cpu)
      

Robotics note: many robot frameworks provide similar APIs; the essential idea is structured, time-stamped logs and lightweight online statistics.

5.2 C++ (logging macros + residual test)


#include <iostream>
#include <fstream>
#include <vector>
#include <cmath>

struct Logger {
  std::ofstream out;
  Logger(const std::string& path) : out(path, std::ios::app) {}
  void info(const std::string& msg) { out << "[INFO] " << msg << "\n"; }
  void warn(const std::string& msg) { out << "[WARN] " << msg << "\n"; }
  void error(const std::string& msg){ out << "[ERR ] " << msg << "\n"; }
};

int main() {
  Logger log("robot_run_cpp.log");

  // Scalar residual case
  double S = 0.04;
  double S_inv = 1.0 / S;
  double gamma = 6.635; // chi-square(1, 0.99)

  double xhat1 = 0.0, xhat2 = 0.0;
  double A11 = 1.0, A12 = 0.01, A22 = 1.0;
  double B2 = 0.01;
  double C1 = 1.0;
  double L1 = 0.2, L2 = 2.0;

  for(int k=0; k<100; ++k){
    double u = 0.1;
    double y = 0.0; // measurement
    double cpu = 0.5;

    log.info("k=" + std::to_string(k) +
             " u=" + std::to_string(u) +
             " y=" + std::to_string(y) +
             " cpu=" + std::to_string(cpu));

    double r = y - C1*xhat1;

    // observer
    double xhat1_next = A11*xhat1 + A12*xhat2 + L1*r;
    double xhat2_next = A22*xhat2 + B2*u + L2*r;
    xhat1 = xhat1_next;
    xhat2 = xhat2_next;

    double J = r*r*S_inv;
    if(J > gamma){
      log.error("Fault detected at k=" + std::to_string(k) +
                " J=" + std::to_string(J));
    }
  }
  return 0;
}
      

5.3 Java (structured logs + online checks)


import java.util.logging.*;
import java.io.IOException;
import java.util.ArrayDeque;

public class RobotMonitor {
    static Logger logger = Logger.getLogger("robot");
    static ArrayDeque<Double> window = new ArrayDeque<>();
    static int W = 50;

    public static void setup() throws IOException {
        FileHandler fh = new FileHandler("robot_run_java.log");
        fh.setFormatter(new SimpleFormatter());
        logger.addHandler(fh);
        logger.setLevel(Level.INFO);
    }

    public static void step(double u, double y, double cpu) {
        logger.info(String.format("u=%.3f, y=%.3f, cpu=%.2f", u, y, cpu));

        window.add(cpu);
        if(window.size() > W) window.poll();
        double avg = 0.0;
        for(double c : window) avg += c;
        avg /= window.size();

        if(avg > 0.9) {
            logger.warning("High CPU average: " + avg);
        }
    }

    public static void main(String[] args) throws IOException {
        setup();
        for(int k=0; k<100; k++){
            step(0.1, 0.0, 0.5);
        }
    }
}
      

Robotics note: Java robot stacks (e.g., ROSJava or embedded JVM systems) use the same patterns: severity levels, rotating files, and structured key-value logs.

5.4 Matlab/Simulink (logging signals + residual alarm)


% Example: log signals and compute residual statistic in MATLAB

Ts = 0.01;
A = [1 Ts; 0 1];
B = [0; Ts];
C = [1 0];
L = [0.2; 2.0];

S = 0.04;          % residual variance
gamma = 6.635;     % chi-square(1,0.99)

xhat = [0;0];

N = 100;
log_u = zeros(N,1);
log_y = zeros(N,1);
log_J = zeros(N,1);

for k=1:N
    u = 0.1;
    y = 0.0;

    r = y - C*xhat;
    xhat = A*xhat + B*u + L*r;

    J = (r'*r)/S;

    log_u(k)=u; log_y(k)=y; log_J(k)=J;

    if J > gamma
        fprintf("Fault detected at k=%d, J=%.3f\n", k, J);
    end
end

% Visualize logs
figure; plot((0:N-1)*Ts, log_J); grid on;
xlabel("time (s)"); ylabel("J_k");
title("Residual statistic over time");
      

Simulink mapping: use "To Workspace" or "Data Store Memory" blocks to log signals, and implement the residual and comparison with "MATLAB Function" blocks. The Simulink Data Inspector can replay logged runs for debugging.

6. Problems and Solutions

Problem 1 (Storage-rate tradeoff): A robot logs \( m=40 \) channels, each sample is \( b=8 \) bytes, for a 2-hour mission. Storage is limited to \( S_{\max}=1.5 \) GB. Find the maximum logging rate \( f_\ell \).

Solution:

\[ T = 2 \text{ hours} = 7200 \text{ s}, \qquad f_\ell \le \frac{S_{\max}}{b m T}. \]

\[ f_\ell \le \frac{1.5 \times 10^9}{8 \cdot 40 \cdot 7200} = \frac{1.5 \times 10^9}{2\,304\,000} \approx 651.0 \text{ Hz}. \]

So logging above about 650 Hz violates the storage budget unless compression or channel reduction is used.

Problem 2 (False-alarm threshold): A monitoring metric \( h_k \sim \mathcal{N}(\mu_0,\sigma_0^2) \). Choose a symmetric z-score threshold so that false alarms occur with probability \( \alpha = 0.02 \).

Solution:

\[ \gamma = \Phi^{-1}(1-\alpha/2) = \Phi^{-1}(0.99) \approx 2.326. \]

Alert when \( |z_k| > 2.326 \).

Problem 3 (Chi-square statistic): Let residuals be scalar with \( r_k \sim \mathcal{N}(0,S) \) under no fault. Show that \( J_k=r_k^2/S \sim \chi^2_1 \).

Solution:

Define \( z_k = r_k/\sqrt{S} \). Then \( z_k \sim \mathcal{N}(0,1) \) and \( J_k = z_k^2 \) which is by definition a chi-square with 1 degree of freedom. ∎

Problem 4 (Moving-average variance): If \( h_k \) are independent with variance \( \sigma_0^2 \), prove \( \operatorname{Var}(\bar{h}_k)=\sigma_0^2/W \) for the windowed mean \( \bar{h}_k=\frac{1}{W}\sum_{i=0}^{W-1} h_{k-i} \).

Solution:

\[ \operatorname{Var}(\bar{h}_k) = \operatorname{Var}\left(\frac{1}{W}\sum_{i=0}^{W-1} h_{k-i}\right) = \frac{1}{W^2}\sum_{i=0}^{W-1} \operatorname{Var}(h_{k-i}) = \frac{1}{W^2} \cdot W \sigma_0^2 = \frac{\sigma_0^2}{W}. \]

Independence removes cross-covariance terms. ∎

7. Summary

We treated logging as a sampling and storage-design problem, monitoring as online statistical supervision of health metrics, and diagnostics as a model-based residual test grounded in linear control. The key principle is to make robot behavior observable after the fact (logging) and detectable online (monitoring/diagnostics), enabling safe operation and rapid debugging.

8. References

  1. Willsky, A.S. (1976). A survey of design methods for failure detection in dynamic systems. Automatica, 12(6), 601–611.
  2. Beard, R.V. (1971). Failure accommodation in linear systems through self-reorganization. PhD Thesis, MIT. (Foundational observer-residual formulation.)
  3. Basseville, M., & Nikiforov, I.V. (1993). Detection of abrupt changes: Theory and application. Prentice Hall / theoretical monograph with strong journal roots.
  4. Gustafsson, F. (2000). Adaptive filtering and change detection. Wiley Series in Adaptive and Learning Systems.
  5. Ding, S.X. (2008). Model-based fault diagnosis techniques: Design schemes, algorithms, and tools. Springer Lecture Notes / theory-heavy treatment.
  6. Isermann, R. (2005). Model-based fault-detection and diagnosis—status and applications. Annual Reviews in Control, 29(1), 71–85.
  7. Patton, R.J., Frank, P.M., & Clark, R.N. (2000). Issues of fault diagnosis for dynamic systems. Springer / collection of theoretical papers.