Inspiration
Excavators work in places with no reliable internet: hard-rock mines, disaster-recovery sites, forest clear-cuts, deep pits. Two consequences follow: cloud-based predictive maintenance is impossible, and operators have no visibility into cumulative fatigue damage. A boom arm can be 60% through its fatigue life and still look fine on the outside — until it cracks at 3 AM, 40 km from the nearest road, at €2,000 per hour of downtime.
I wanted to prove that structural health monitoring does not require a network connection. The physics runs on the machine. The data lives on the machine. The cloud is optional.
What it does
The twin reads hydraulic pressure and boom angle from the machine's existing control system at 10 Hz. For each sample, it:
- Derives a load vector [Fx, Fy, Fz, Mx, My, Mz] from the sensor readings and the current boom geometry.
- Computes a Von Mises stress field across a tapered beam mesh — root, cylinder mount, tip.
- Accumulates fatigue damage using Basquin's S-N curve with Goodman mean-stress correction.
- Persists everything to a local SQLite database.
- Surfaces a live 3D stress view and warning banner to the operator.
Everything runs on localhost. The cloud sync engine drains the local queue opportunistically, with at-least-once delivery and durable retry across power cycles.
A live demo is available on Streamlit Community Cloud. Note that cloud rendering uses software OpenGL (Mesa llvmpipe) and is slower than the local or edge deployment — the architecture is unchanged.
How I built it
The core solver is written in C (c_src/stress_solver.c) and compiled to a shared library at build time. Python drives everything else: geometry generation, telemetry simulation, database writes, and the Streamlit dashboard. The C↔Python boundary uses ctypes with c_void_p pointers for cross-platform reliability (Windows, Linux, macOS).
I implemented a pure-NumPy fallback for the same physics, and a pytest suite verifies that both backends produce identical results. That parity check is the single most important test in the repo — if the C port ever drifts, I caught it immediately.
The fatigue model uses Basquin's law with exponent 10 (standard for structural steel per Eurocode 3) and Goodman's mean-stress correction. Endurance-limit behavior — cycles below ~150 MPa contribute essentially nothing — emerges naturally from the model, matching the real behavior of well-designed steel booms.
The local database is SQLite with WAL mode. Every telemetry row and every stress snapshot carries a synced flag. That flag is the cloud queue. The sync engine drains it in batches via a pluggable CloudTransport; the demo uses a FileTransport that writes JSONL batches to disk, but a real deployment would subclass it to POST to an HTTPS endpoint.
The dashboard is Streamlit with off-screen PyVista rendering. On Streamlit Cloud the container has no X server, so the app bootstraps Xvfb on first run (cached via st.cache_resource to prevent process leaks across reruns). The same Xvfb bootstrap also works on a Raspberry Pi in headless mode.
Challenges I ran into
Three, all instructive.
First, the physics lied to us. An early version of the fatigue model used an S-N exponent of 3, which corresponds to low-cycle fatigue. It predicted that 5,000 cycles at 250 MPa would consume 630× the material's life. That is absurd. I replaced it with a Basquin exponent of 10 and a fatigue-strength coefficient of 900 MPa (2× ultimate tensile strength for cast steel). The model then predicted ~1% damage per 5,000 duty cycles — physically defensible.
Second, my loads were inconsistent with my geometry. A 3 m boom with a 0.25 × 0.35 m root cross-section is a mini-excavator. Its boom cylinder has a ~100 mm bore, giving a piston area of 0.008 m². I previously used 0.02 m² — a parameter that corresponds to a much larger machine. Peak stress hit 614 MPa, well above the yield strength of cast steel. Halving the cylinder area brought peak stress back under yield and made the endurance-limit behavior visible.
Third, ctypes on Windows failed with a type error when I used POINTER(c_double) for the C function argtypes. The fix was to declare all pointer arguments as c_void_p and pass raw array addresses as Python ints. This works identically on Linux, macOS, and Windows, and it removed the platform-specific fragility.
The most satisfying fix was the Streamlit Cloud deployment. VTK's off-screen renderer segfaulted on the cloud container because there was no X server, no EGL, and no OSMesa. The solution was a packages.txt that apt-installs Xvfb, plus a st.cache_resource-guarded bootstrap that starts Xvfb exactly once per server process. The deployed app now renders the same 3D stress field the local version does.
Accomplishments that I am proud of
- A working digital twin that runs end-to-end on a laptop with Wi-Fi disabled.
- Verified C↔NumPy backend parity — 25 tests, all passing in under 2 seconds.
- A physically defensible fatigue model, not a scripted animation.
- An offline-first data layer with durable retry semantics.
- A live 3D stress field updating at interactive frame rates.
- A cloud deployment that required solving a real headless-GL problem, not just clicking a button.
What I learned
Physics code needs assertion windows that respect the physics. My first test suite over-asserted in four places — it assumed a 10× amplitude jump where the model was doing 2.5×, and it assumed peak-stress scaling would be exactly linear when the peak node actually shifts between the root and the cylinder mount. Every failed test was my assertion being wrong, not the solver. Iterating against real numbers is how you calibrate a test suite for a physical model.
Deploying to the cloud also taught us that "it works locally" is not a deployment strategy. The Xvfb fix exists because the local environment was giving us something the cloud container never had.
What's next for Off-Grid Excavator Digital Twin
- Replace synthetic telemetry with a real OPC-UA or CAN-bus reader.
- Ship as a systemd service on Raspberry Pi OS.
- Replace server-side PyVista with a browser-side WebGL renderer (stpyvista or Plotly Mesh3d) for faster cloud performance.
- Extend the geometry to full multi-body kinematics — bucket, arm, and boom.
- Add a fleet-level analytics dashboard that aggregates synced data across machines.
Log in or sign up for Devpost to join the conversation.