diff --git a/field-guides/README.md b/field-guides/README.md new file mode 100644 index 0000000..31c27c7 --- /dev/null +++ b/field-guides/README.md @@ -0,0 +1,11 @@ +# Condition Monitoring — Field Guides + +Practical guides for technicians taking readings on the Mechbase marine demo routes. + +| Guide | Sensor / Instrument | Measurement | +|---|---|---| +| [Vibration](vibration.md) | TE Connectivity WVS wireless tri-axial accelerometer | Velocity (mm/s RMS) | +| [Thermography](thermography.md) | FLIR Edge series handheld thermal camera | Surface temperature (°C) | +| [Ultrasound](ultrasound.md) | Handheld ultrasonic detector | Signal level (dB) | + +Each guide covers the instrument, how to set it up and take a reading correctly, and how to enter the result in Mechbase. diff --git a/field-guides/images/te-wvs-sensor.jpg b/field-guides/images/te-wvs-sensor.jpg new file mode 100644 index 0000000..c4a7ed6 Binary files /dev/null and b/field-guides/images/te-wvs-sensor.jpg differ diff --git a/field-guides/thermography.md b/field-guides/thermography.md new file mode 100644 index 0000000..1281db9 --- /dev/null +++ b/field-guides/thermography.md @@ -0,0 +1,79 @@ +# Thermography — Field Guide + +Camera: **FLIR Edge series** handheld (visual + thermal, MSX enhancement, FLIR ONE app) + +--- + +## How the camera measures temperature + +Thermal measurements show the **surface** temperature of the target. Accuracy depends on three factors: + +| Factor | What to watch | +|---|---| +| **Distance to target** | Closer = better spatial resolution and spot size. Get as close/zoomed as safely possible. | +| **Ambient temperature** | Note ambient; it is the reference for ΔT calculations. | +| **Emissivity** | How efficiently the surface radiates heat (see below). | + +### Emissivity + +- **Matte / painted / oxidised surfaces** — good emitters; the camera's default ~0.95 setting is a fair approximation. +- **Bare / glossy / shiny metal** — poor emitters; the default setting will under-read. Apply a strip of high-emissivity tape or a dab of matt paint over the spot, **or** manually set the known emissivity in the camera before reading. + +--- + +## Camera setup + +1. Power on; let the camera warm up briefly. +2. Set the **temperature range** to suit the expected target (e.g. a wider range for electrical gear under fault conditions). +3. Choose a suitable **colour palette** (e.g. Iron or Rainbow for electrical; Greyscale for quick scans). +4. Enable **MSX** and set the MSX distance to match the target distance — this overlays visual detail on the thermal image for easier interpretation. +5. Use the **adjustable measurement spots** to track the hottest and coldest points in the frame. The camera periodically auto-calibrates (mechanical shutter click + brief freeze) — this is normal; wait for it to complete before reading. + +--- + +## Inspection technique + +### General + +- Inspect under **normal / representative operating load** — an unloaded circuit or idle machine hides faults. +- Keep the **view angle within ~30° of perpendicular** to the target surface to limit reflection error. + +### Electrical (switchboards, distribution panels) + +1. Open the inspection cover or use an IR window where required (PPE: arc-flash rated). +2. Scan **busbars, breakers, incoming terminals, and all connections**. +3. Compare the **same component across all three phases** — a phase running hotter than its siblings under similar load indicates a loose or corroded connection, an overloaded circuit, or a failing device. +4. Note the hottest spot temperature (°C) and the ΔT above ambient or the reference phase. + +### Mechanical (motors, bearings, pumps) + +1. Scan **bearing housings** and **couplings**. +2. Compare each bearing housing against its pair or against its known baseline. +3. A bearing running hotter than its baseline or its paired housing suggests a lubrication or wear issue. + +--- + +## Judging severity + +Judge findings by **ΔT** (temperature rise over a reference point — ambient air, the established baseline, or a similar phase / component), not by absolute temperature alone. + +| ΔT (over reference) | Typical severity | +|---|---| +| < 10 °C | Monitor — recheck at next route interval | +| 10–30 °C | Investigate — plan corrective action | +| > 30 °C | Act promptly — potential imminent failure | + +*These are general guidelines; the Mechbase route item shows the configured limit band for this specific point.* + +--- + +## Safety + +- Do **not** point the camera at the sun or laser sources. +- For live electrical panels, maintain safe working distance and use appropriate PPE; use an IR window or a non-contact safe zone wherever possible. + +--- + +## Recording the reading in Mechbase + +Open the assigned **Route** → tap the **Measurement Item** for this thermal point → the item shows the limit band (minor alert: ~65 °C; major alert: ~90 °C in this demo). Enter the **maximum spot temperature** (°C) observed at the target and confirm. diff --git a/field-guides/ultrasound.md b/field-guides/ultrasound.md new file mode 100644 index 0000000..1bd678d --- /dev/null +++ b/field-guides/ultrasound.md @@ -0,0 +1,73 @@ +# Ultrasound — Field Guide + +Instrument: **Handheld ultrasonic detector** (contact/stinger probe + airborne horn; outputs a dB reading for manual entry) + +--- + +## Three main applications + +| Application | Probe | What you are detecting | +|---|---|---| +| **Bearing condition** | Contact / stinger probe on bearing housing | Structure-borne ultrasound from rolling-element wear or inadequate lubrication | +| **Leak detection** | Airborne horn | Compressed-air, steam-trap, and valve leaks | +| **Electrical** | Airborne horn | Partial discharge, arcing, and corona in switchgear | + +--- + +## Bearing condition checks + +1. Fit the **contact (stinger) probe** firmly to the bearing housing at the designated spot — same location and orientation every visit. +2. Adjust the instrument's frequency / sensitivity to the manufacturer's recommended setting for rotating-equipment surveys. +3. Listen through the headset and read the **dB level** displayed. +4. Record the value; compare to this point's baseline (see Interpretation below). + +**Tip:** ultrasound detects incipient bearing faults earlier than vibration or thermography, especially on slow-speed bearings. It is also the primary tool for identifying lack of lubrication before a bearing overheats. + +--- + +## Leak surveys (compressed air / steam) + +1. Fit the **airborne horn** (focusing cone). +2. Sweep the area methodically from a safe distance. +3. Home in on the direction of the **loudest dB reading** — leaks produce a strong high-frequency hiss. +4. Once located, move closer to confirm and note the location, dB level, and estimated leak size. + +**On a vessel:** cover compressor discharge lines, air receivers, valve glands, steam traps, and flexible hose connections. + +--- + +## Electrical surveys (switchgear / panels) + +1. Fit the **airborne horn**. +2. Hold the instrument near (not inside) the panel or cable tray. +3. Partial discharge, arcing, and corona all produce characteristic ultrasonic signatures. +4. Note the dB level and location of any hot spot. + +--- + +## Interpreting bearing readings — trend-based thresholds + +Always compare against the **same point's historical baseline**, not an absolute number. + +| Rise over baseline | Indication | +|---|---| +| < +8 dB | Normal variation — continue monitoring | +| ~+8 dB (sustained) | Early / incipient wear or lubrication deficiency — investigate, consider re-lubrication | +| +12–16 dB | Advanced wear — plan maintenance | +| > +16 dB | Imminent failure — act promptly | + +A single elevated reading can be caused by load spikes or probe placement variation. Confirm with a second reading before escalating. + +--- + +## Good technique habits + +- Use the **same probe, same spot, same pressure** every visit — variability in contact pressure changes the dB reading. +- Take readings when the machine is running under **normal operating load**. +- Note any unusual audible signatures (e.g. grinding, intermittent clicks) that the dB number alone does not capture. + +--- + +## Recording the reading in Mechbase + +Open the assigned **Route** → tap the **Measurement Item** for this ultrasound point → the item shows the limit band (minor alert: ~40 dB; major alert: ~50 dB in this demo). Enter the **dB value** displayed on the instrument and confirm. diff --git a/field-guides/vibration.md b/field-guides/vibration.md new file mode 100644 index 0000000..bf7552e --- /dev/null +++ b/field-guides/vibration.md @@ -0,0 +1,81 @@ +# Vibration Monitoring — Field Guide + +Sensor: **TE Connectivity WVS** wireless tri-axial accelerometer + +![TE Connectivity WVS vibration sensor](images/te-wvs-sensor.jpg) + +*Note the X/Y/Z axis marker on the hex base (align **X** with the shaft), the +magnetic twist-mount indicator, the threaded/stud base, and the Ex ia IIC (ATEX/IS) +rating for hazardous machinery spaces.* + +--- + +## Mounting method — decision tree (best → temporary) + +| Method | Use case | Notes | +|---|---|---| +| **Stud mount** | Permanent monitoring points | Best frequency response; solid metal-to-metal contact; conveys the full spectrum | +| **Epoxy mount** | Permanent; no stud possible | 2-part hard-curing epoxy (e.g. Loctite AA 330). Detach magnetic base, epoxy it to machine, re-thread sensor. | +| **Two-pole magnet** | Curved housings (e.g. motor end caps) | Two feet grip curved surfaces. Gently roll sensor into contact — do not thump it down. Do **not** use a flat magnet directly on a curved surface. | +| **Flat magnet on glued/soldered metal target** | Painted or curved surfaces where direct contact is impractical | Epoxy or solder a small flat metal target to the machine; mount flat magnet on the target. | +| **Flat magnet on clean flat surface** | Temporary spot-checks only | Magnets shift on dirty or irregular surfaces. For trend data, prefer a permanent mount. | + +--- + +## Surface preparation + +- Remove **all paint and rust** from the mounting area — multiple paint layers and rust severely dampen or block the signal. +- Vibration must travel through solid, continuous metal; any gap or joint in the path corrupts the reading. +- Target a **robust, flat, fully cleaned bearing-housing area**. + +--- + +## Where to mount — placement + +- Mount as close as feasible to the **monitored bearing**. +- Target the **load zone**: the section of the bearing housing that carries the rotating shaft — defects show up earliest there. + +**On marine machinery take points at:** + +1. Motor non-drive end (NDE) bearing housing +2. Motor drive end (DE) bearing housing +3. Driven-end bearing (pump / compressor / fan / purifier) + +--- + +## What to avoid + +Do **not** mount on: + +- Thin cooling fins +- Plastic covers or fan shrouds +- Component enclosures + +These surfaces resonate at their own frequencies or attenuate the true signal, producing inaccurate data. + +--- + +## Sensor orientation (axis alignment) + +The WVS is tri-axial (X / Y / Z axes). + +1. Align the **X axis** (marked on the sensor body) with the motor **drive shaft**. +2. Keep the **same orientation on every visit** and across comparable machines. +3. Take readings at the **same physical point** each visit. + +Inconsistent orientation makes trend analysis unreliable — today's X reading becomes last month's Y. + +--- + +## Taking the reading + +1. Mount the sensor at the designated point using the appropriate method above. +2. Allow a brief settle time after mounting (especially magnetic mounts). +3. Confirm the sensor is paired and transmitting in the WVS app / Mechbase. +4. Note the velocity value (mm/s RMS) displayed. + +--- + +## Recording the reading in Mechbase + +Open the assigned **Route** on your device → tap the **Measurement Item** for this point → the item shows the limit band (minor / major alert thresholds). Enter the velocity value (mm/s) and confirm. The system records the timestamp, value, and your user ID automatically. diff --git a/python/examples/seed_marine.py b/python/examples/seed_marine.py new file mode 100644 index 0000000..ae70ed5 --- /dev/null +++ b/python/examples/seed_marine.py @@ -0,0 +1,168 @@ +"""Standalone SDK example: drive the full client flow against a Mechbase instance. + +This demonstrates the end-to-end client path — discover the structure, batch-push +manual point readings (with a couple of curated faults), and run + complete one +execution of each route — using only the public ``mechbase`` SDK surface. + +NOTE: the Aeolian Fortune demo itself is now seeded entirely by mechbase-web's +``python manage.py bootstrap_marine`` command (structure + measurements + route +executions, via the ORM). This script is kept as a reusable SDK example; pointed +at an installation that bootstrap_marine created, it will push additional readings +and route runs through the live API. + + MECHBASE_TOKEN= MECHBASE_URL=https://app.mechbase.io \ + python examples/seed_marine.py [--dry-run] +""" +from __future__ import annotations + +import argparse +import os +import random + + +def healthy_reading(transducer_type: str, rng: random.Random) -> dict: + if transducer_type == "VB": + # mm/s. Must stay below the LOWEST ISO 10816 minor threshold across the + # machine classes in the demo — Class I is 1.8 mm/s — so healthy readings + # on small (Class I) machines don't falsely trip Minor. Keep margin: <1.5. + vel = round(rng.uniform(0.4, 1.4), 3) + return {"vel_10hz": vel, "rms": round(rng.uniform(0.3, 0.9), 3), + "peak": round(rng.uniform(0.8, 1.6), 3), "crest_factor": round(rng.uniform(1.4, 2.2), 2)} + if transducer_type in ("IR", "TM"): + return {"value": round(rng.uniform(35.0, 52.0), 1)} + if transducer_type == "US": + return {"rms": round(rng.uniform(20.0, 36.0), 1)} + return {"value": round(rng.uniform(1.0, 10.0), 1)} + + +def fault_reading(kind: str, rng: random.Random) -> tuple[dict, list[dict] | None]: + if kind == "vb_minor": + return ({"vel_10hz": round(rng.uniform(3.2, 4.5), 3), "rms": round(rng.uniform(0.8, 1.4), 3), + "peak": round(rng.uniform(3.0, 5.0), 3), "crest_factor": round(rng.uniform(2.5, 3.5), 2)}, None) + if kind == "ir_minor": + return ({"value": round(rng.uniform(68.0, 85.0), 1)}, None) + if kind == "ir_major": + return ({"value": round(rng.uniform(92.0, 105.0), 1)}, None) + raise ValueError(f"unknown fault kind {kind!r}") + + +from mechbase import Mechbase # noqa: E402 + +INSTALLATION_NAME = "Aeolian Fortune" +# Mirror of mechbase-web measurements.marine_demo.MARINE_FAULTS. +FAULTS = {"AF-SWP2-PDE": "vb_minor", "AF-MSB-BRK": "ir_major", "AF-FOT-IRM": "ir_minor"} +N_READINGS = 8 # readings per point (weekly cadence, ~60 days) +DAY_STEP = 7 + + +def _iso_days_ago(n: int) -> str: + # Lazy import keeps the pure-function tests free of datetime determinism issues. + from datetime import datetime, timedelta, timezone + return (datetime.now(timezone.utc) - timedelta(days=n)).isoformat() + + +def seed_measurements(inst, points, rng, *, dry_run: bool) -> int: + items = [] + for p in points: + if p.transducer_type == "PR": + continue + fault = FAULTS.get(p.external_id or "") + for i in range(N_READINGS): + days_ago = (N_READINGS - 1 - i) * DAY_STEP + recent = i >= N_READINGS - 2 # fault ramps on the last 2 readings + if fault and recent: + data, _ = fault_reading(fault, rng) + else: + data = healthy_reading(p.transducer_type, rng) + item = {"measurement_point_external_id": p.external_id, "data": data, + "timestamp": _iso_days_ago(days_ago), + "external_id": f"{p.external_id}-{i}"} + items.append(item) + if dry_run: + print(f"[dry-run] would push {len(items)} readings") + return len(items) + # The batch endpoint caps at 1000 items per request; chunk to stay under it. + chunk = 500 + created = duplicates = errors = 0 + for start in range(0, len(items), chunk): + result = inst.measurements.create_batch(items[start : start + chunk]) + created += result.created + duplicates += result.duplicates + errors += result.errors + print(f"readings: created={created} duplicates={duplicates} errors={errors}") + return created + + +def run_routes(inst, points_by_id, rng, *, dry_run: bool) -> int: + done = 0 + for route in inst.routes.list(limit=50): + detail = inst.routes.get(route.uuid) # dict: {..., "items": [...]} + if dry_run: + print(f"[dry-run] would run '{route.name}' ({len(detail['items'])} items)") + done += 1 + continue + execution = inst.routes.start(route.uuid) + for item in detail["items"]: + data = _response_for(item, points_by_id, rng) + execution.respond(route_item_uuid=item["uuid"], data=data) + execution.complete() + done += 1 + print(f"completed execution of '{route.name}'") + return done + + +def _response_for(item: dict, points_by_id: dict, rng: random.Random) -> dict: + it = item["item_type"] + if it == "pass_fail": + passed = rng.random() > 0.1 + return {"passed": passed} if passed else {"passed": False, "severity": "minor"} + if it == "numerical": + unit = (item.get("config") or {}).get("unit", "") + rng_map = {"°C": (20, 60), "bar": (2, 8)} + lo, hi = rng_map.get(unit, (0.5, 100)) + return {"value": round(rng.uniform(lo, hi), 1)} + if it == "multi_check": + return {"results": [{"label": c["label"], "passed": True} + for c in (item.get("config") or {}).get("checks", [])]} + if it == "measurement": + pt = points_by_id.get(item.get("measurement_point_id")) + ttype = pt.transducer_type if pt else "TM" + return healthy_reading(ttype, rng) + return {} + + +def main() -> None: + parser = argparse.ArgumentParser() + parser.add_argument("--dry-run", action="store_true") + args = parser.parse_args() + + token = os.environ["MECHBASE_TOKEN"] + base = os.environ.get("MECHBASE_URL", "https://app.mechbase.io") + rng = random.Random(2014) + + with Mechbase(token=token, base_url=base) as client: + me = client.me() + inst = client.for_installation(me.current_installation_id) + names = {i.installation_id: i.name for i in me.installations} + if names.get(me.current_installation_id) != INSTALLATION_NAME: + raise SystemExit( + f"Refusing to seed: token resolves to '{names.get(me.current_installation_id)}', " + f"not '{INSTALLATION_NAME}'. Use the token printed by bootstrap_marine.") + + points = [] + offset = 0 + while True: + page = inst.measurement_points.list(limit=100, offset=offset) + points.extend(page) + if len(page) < 100: + break + offset += 100 + points_by_id = {p.point_id: p for p in points} + + n = seed_measurements(inst, points, rng, dry_run=args.dry_run) + r = run_routes(inst, points_by_id, rng, dry_run=args.dry_run) + print(f"Done: {n} readings, {r} route executions on '{INSTALLATION_NAME}' ({base}).") + + +if __name__ == "__main__": + main() diff --git a/python/tests/test_seed_marine.py b/python/tests/test_seed_marine.py new file mode 100644 index 0000000..25fcfac --- /dev/null +++ b/python/tests/test_seed_marine.py @@ -0,0 +1,41 @@ +# tests/test_seed_marine.py +import random +import sys +import pathlib + +sys.path.insert(0, str(pathlib.Path(__file__).resolve().parent.parent / "examples")) +import seed_marine as sm # noqa: E402 + + +def test_healthy_vb_below_iso_minor(): + rng = random.Random(1) + for _ in range(50): + data = sm.healthy_reading("VB", rng) + assert "vel_10hz" in data + # Below the LOWEST ISO 10816 minor threshold in the demo (Class I = 1.8 mm/s), + # so small Class-I machines read healthy, not a false Minor. + assert data["vel_10hz"] < 1.8 + + +def test_healthy_ir_tm_in_safe_band(): + rng = random.Random(2) + for ttype, ceiling in (("IR", 65.0), ("TM", 55.0)): + for _ in range(50): + assert sm.healthy_reading(ttype, rng)["value"] < ceiling + + +def test_vb_minor_fault_breaches_minor_not_major(): + rng = random.Random(3) + data, clusters = sm.fault_reading("vb_minor", rng) + assert clusters is None + assert 2.8 < data["vel_10hz"] < 7.1 # Minor band + + +def test_ir_faults_breach_value_thresholds(): + rng = random.Random(4) + minor_data, minor_clusters = sm.fault_reading("ir_minor", rng) + major_data, major_clusters = sm.fault_reading("ir_major", rng) + assert minor_clusters is None and major_clusters is None + # IR alarm rule: minor=65, major=90 (°C). Minor band is (65, 90]; Major is > 90. + assert 65.0 < minor_data["value"] <= 90.0 + assert major_data["value"] > 90.0