diff --git a/main.py b/main.py index edd3bac..313285f 100644 --- a/main.py +++ b/main.py @@ -1,5 +1,24 @@ -import streamlit as st +# app.py +# Streamlit: Delta ASDA-A2 Engineering Tool (PUU + validation + speed/torque + templates + report export) +# +# Run: +# streamlit run app.py + +import io +import math +from dataclasses import dataclass +from datetime import datetime from math import gcd +import streamlit as st + +# Optional PDF export (reportlab is installed in your environment) +try: + from reportlab.lib.pagesizes import A4 + from reportlab.pdfgen import canvas + REPORTLAB_OK = True +except Exception: + REPORTLAB_OK = False + # ---------------------------- # Helpers @@ -12,7 +31,6 @@ def simplify_fraction(n: int, d: int) -> tuple[int, int]: g = gcd(abs(n), abs(d)) n //= g d //= g - # Keep denominator positive if d < 0: n *= -1 d *= -1 @@ -20,70 +38,379 @@ def simplify_fraction(n: int, d: int) -> tuple[int, int]: def scale_down_to_limit(n: int, d: int, limit: int) -> tuple[int, int]: - """ - If numerator/denominator exceed 'limit', scale them down proportionally. - Keeps ratio approximately the same (exact if divisible), then re-simplifies. - """ + """Scale down proportionally if n or d exceed limit; keep ratio approx; re-simplify.""" + if d == 0: + raise ValueError("Denominator cannot be 0.") if n == 0: return 0, 1 n_abs, d_abs = abs(n), abs(d) if n_abs <= limit and d_abs <= limit: return n, d - # Scale by the maximum overflow factor factor = max((n_abs + limit - 1) // limit, (d_abs + limit - 1) // limit) n2 = n // factor d2 = d // factor - - # Avoid zeroing out if n2 == 0: n2 = 1 if n > 0 else -1 if d2 == 0: d2 = 1 - return simplify_fraction(n2, d2) +def ratio_error_ppm(target: float, chosen: float) -> float: + if target == 0: + return 0.0 if chosen == 0 else float("inf") + return (chosen - target) / target * 1_000_000.0 + + +def format_ppm(ppm: float) -> str: + if math.isinf(ppm): + return "∞" + return f"{ppm:.2f} ppm" + + +def clamp_float(x: float, lo: float, hi: float) -> float: + return max(lo, min(hi, x)) + + +def build_markdown_report(data: dict) -> str: + ts = datetime.now().strftime("%Y-%m-%d %H:%M:%S") + lines = [] + lines.append(f"# Delta ASDA-A2 Commissioning Report") + lines.append("") + lines.append(f"- Generated: **{ts}**") + lines.append("") + lines.append("## Inputs") + for k, v in data["inputs"].items(): + lines.append(f"- **{k}:** {v}") + lines.append("") + lines.append("## PUU / Electronic Gear Result") + for k, v in data["puu"].items(): + lines.append(f"- **{k}:** {v}") + lines.append("") + lines.append("## Sanity Checks") + for k, v in data["sanity"].items(): + lines.append(f"- **{k}:** {v}") + lines.append("") + lines.append("## Validations") + if not data["validations"]: + lines.append("- (none)") + else: + for item in data["validations"]: + lines.append(f"- {item}") + lines.append("") + lines.append("## Speed Translator") + for k, v in data["speed"].items(): + lines.append(f"- **{k}:** {v}") + lines.append("") + lines.append("## Torque Estimator") + for k, v in data["torque"].items(): + lines.append(f"- **{k}:** {v}") + lines.append("") + lines.append("## Parameter Templates") + for name, items in data["templates"].items(): + lines.append(f"### {name}") + for line in items: + lines.append(f"- {line}") + lines.append("") + return "\n".join(lines) + + +def markdown_to_pdf_bytes(title: str, md_text: str) -> bytes: + """ + Minimal PDF exporter: + - Not a full markdown renderer; just prints lines (wraps long lines). + - Works well for a commissioning report. + """ + if not REPORTLAB_OK: + raise RuntimeError("reportlab not available") + + buffer = io.BytesIO() + c = canvas.Canvas(buffer, pagesize=A4) + width, height = A4 + + margin = 40 + y = height - margin + line_height = 12 + max_width_chars = 105 # rough wrap; avoids needing font metrics + + c.setTitle(title) + c.setFont("Helvetica", 11) + + def new_page(): + nonlocal y + c.showPage() + c.setFont("Helvetica", 11) + y = height - margin + + for raw in md_text.splitlines(): + # simple wrap + line = raw.rstrip("\n") + if not line: + y -= line_height + if y < margin: + new_page() + continue + + chunks = [line[i:i + max_width_chars] for i in range(0, len(line), max_width_chars)] + for chunk in chunks: + c.drawString(margin, y, chunk) + y -= line_height + if y < margin: + new_page() + + c.save() + buffer.seek(0) + return buffer.read() + + # ---------------------------- -# Page config + nav +# Domain logic +# ---------------------------- +@dataclass +class PUUResult: + n_exact: int + d_exact: int + n_final: int + d_final: int + target_ratio: float + chosen_ratio: float + error_ppm: float + + +def compute_puu( + motor_counts_per_rev: float, + gear_ratio: float, + units_per_load_rev: float, + controller_pulses_per_unit: float, + ratio_limit: int, +) -> PUUResult: + if motor_counts_per_rev <= 0: + raise ValueError("motor_counts_per_rev must be > 0") + if gear_ratio <= 0: + raise ValueError("gear_ratio must be > 0") + if units_per_load_rev <= 0: + raise ValueError("units_per_load_rev must be > 0") + if controller_pulses_per_unit <= 0: + raise ValueError("controller_pulses_per_unit must be > 0") + if ratio_limit < 1000: + raise ValueError("ratio_limit too small") + + motor_counts_per_load_rev = motor_counts_per_rev * gear_ratio + denom_units = units_per_load_rev * controller_pulses_per_unit + + target = motor_counts_per_load_rev / denom_units + + # convert to integer fraction with scaling for decimals + SCALE = 1_000_000 + n_raw = int(round(motor_counts_per_load_rev * SCALE)) + d_raw = int(round(denom_units * SCALE)) + + n_exact, d_exact = simplify_fraction(n_raw, d_raw) + n_final, d_final = scale_down_to_limit(n_exact, d_exact, int(ratio_limit)) + + chosen = n_final / d_final + err = ratio_error_ppm(target, chosen) + + return PUUResult( + n_exact=n_exact, + d_exact=d_exact, + n_final=n_final, + d_final=d_final, + target_ratio=target, + chosen_ratio=chosen, + error_ppm=err, + ) + + +def validate_delta_limits( + ratio: float, + min_ratio: float, + max_ratio: float, + n: int, + d: int, + int_limit: int, +) -> list[str]: + issues = [] + if ratio < min_ratio: + issues.append(f"❌ Electronic gear ratio {ratio:.6f} is **below** Delta limit {min_ratio:.6f}.") + elif ratio > max_ratio: + issues.append(f"❌ Electronic gear ratio {ratio:.6f} is **above** Delta limit {max_ratio:.6f}.") + else: + issues.append(f"✅ Electronic gear ratio {ratio:.6f} is within Delta limit [{min_ratio:.6f}, {max_ratio:.6f}].") + + if abs(n) > int_limit or abs(d) > int_limit: + issues.append(f"❌ P1-44/45 exceed integer limit {int_limit}.") + else: + issues.append(f"✅ P1-44/45 are within integer limit {int_limit}.") + + if d == 0: + issues.append("❌ Denominator (P1-45) is 0 (invalid).") + elif d < 0: + issues.append("⚠️ Denominator is negative (we usually keep it positive).") + + return issues + + +def suggestions_if_out_of_range(ratio: float, min_ratio: float, max_ratio: float) -> list[str]: + sugg = [] + if ratio < min_ratio: + sugg.append("Try **increasing** ratio by:") + sugg.append("- Increasing motor_counts_per_rev (if encoder mode supports it)") + sugg.append("- Increasing gear_ratio (if you entered it wrong)") + sugg.append("- Decreasing units_per_load_rev (e.g., use turns instead of degrees)") + sugg.append("- Decreasing controller_pulses_per_unit (controller scaling)") + elif ratio > max_ratio: + sugg.append("Try **decreasing** ratio by:") + sugg.append("- Increasing units_per_load_rev (degrees→micro-degrees, or mm→0.1mm units)") + sugg.append("- Increasing controller_pulses_per_unit (more pulses per user unit)") + sugg.append("- Decreasing motor_counts_per_rev (if encoder mode supports it)") + return sugg + + +def calc_speed_translator( + gear_ratio: float, + units_per_load_rev: float, + motor_rpm: float | None, + load_rpm: float | None, +) -> dict: + # Relations: motor_rpm = load_rpm * gear_ratio + if motor_rpm is None and load_rpm is None: + load_rpm = 10.0 + if motor_rpm is None: + motor_rpm = load_rpm * gear_ratio + if load_rpm is None: + load_rpm = motor_rpm / gear_ratio + + load_rps = load_rpm / 60.0 + motor_rps = motor_rpm / 60.0 + units_per_sec = load_rps * units_per_load_rev + units_per_min = load_rpm * units_per_load_rev + + return { + "Motor RPM": motor_rpm, + "Load RPM": load_rpm, + "Motor RPS": motor_rps, + "Load RPS": load_rps, + "User units / s": units_per_sec, + "User units / min": units_per_min, + } + + +def calc_torque_estimator( + motor_rated_nm: float, + motor_peak_nm: float, + gear_ratio: float, + efficiency: float, +) -> dict: + efficiency = clamp_float(efficiency, 0.01, 1.0) + out_rated = motor_rated_nm * gear_ratio * efficiency + out_peak = motor_peak_nm * gear_ratio * efficiency + return { + "Efficiency": efficiency, + "Estimated output rated torque (Nm)": out_rated, + "Estimated output peak torque (Nm)": out_peak, + } + + +def template_blocks() -> dict[str, list[str]]: + return { + "First power-on (SAFE)": [ + "P1-02 Speed Limit: low (e.g., 300–800 RPM) for first tests", + "P1-12 / P1-13 Torque Limit: 30–60% to avoid end-stop damage", + "P1-44 / P1-45: from PUU calculator", + "Verify direction with short jog", + "Verify 1 load rev equals expected user units (e.g., 360°)", + ], + "Tuning mode": [ + "P1-37 Load/Motor inertia: start 10–20 for gearbox systems", + "Run auto-tune (if available) at low risk speed", + "Increase torque limit step-by-step only if needed", + ], + "Production (NORMAL)": [ + "P1-02 Speed Limit: motor-rated safe speed (e.g., 3000 RPM) or machine limit", + "Torque limits set to protect mechanics and process", + "Store backup of parameters / report", + ], + } + + +# ---------------------------- +# Streamlit UI state # ---------------------------- st.set_page_config(page_title="Delta ASDA-A2 Engineering Tool", layout="wide") +# default session values +defaults = { + "motor_counts_per_rev": 128000.0, + "gear_ratio": 50.0, + "unit_preset": "Degrees at output (360 units / load rev)", + "units_per_load_rev": 360.0, + "lead_mm_per_rev": 5.0, + "pulley_circum_mm": 100.0, + "controller_pulses_per_unit": 1.0, + "ratio_limit": 2_147_483_647, + "delta_min_ratio": 1.0 / 50.0, # you referenced 1/50 + "delta_max_ratio": 25600.0, # you referenced 25600 + "smooth_min_counts_per_user_unit": 5000.0, + "motor_rated_rpm": 3000.0, + "motor_rated_torque_nm": 1.27, + "motor_peak_torque_nm": 3.82, + "gear_efficiency": 0.90, + "speed_input_mode": "Load RPM → Motor RPM", + "load_rpm_in": 10.0, + "motor_rpm_in": 500.0, +} +for k, v in defaults.items(): + st.session_state.setdefault(k, v) + st.sidebar.title("Configuration") page = st.sidebar.radio( "Go to", - ["Theory & Architecture", "PUU Calculator", "Parameter Guide", "ASDA-Soft Guide"], + [ + "Theory & Architecture", + "PUU Calculator", + "Speed Translator", + "Torque Estimator", + "Parameter Guide", + "Commissioning Checklist", + "ASDA-Soft Guide", + "Export Report", + ], ) +# Global reset +with st.sidebar: + if st.button("Reset all inputs"): + for k, v in defaults.items(): + st.session_state[k] = v + st.rerun() + # ---------------------------- # Pages # ---------------------------- if page == "Theory & Architecture": st.title("📚 Theory & Architecture") st.markdown( - """ -This tool helps you set up **electronic gearing (PUU / P1-44 & P1-45)** and the **core safety/tuning parameters** -for a system with a gearbox (e.g., **50:1**). - -### Concept (simple) -Most motion controllers output **position command pulses**. -The servo drive converts those pulses into **motor encoder counts** using an **electronic gear ratio**: + r""" +This tool helps you set up **electronic gearing (PUU / P1-44 & P1-45)** and speed/torque safety checks for gearbox systems. +### Core equation \[ -\\text{Motor encoder counts} = \\text{Command pulses} \\times \\frac{P1-44}{P1-45} +\text{Motor encoder counts} = \text{Command pulses} \times \frac{P1-44}{P1-45} \] -So you choose **P1-44 / P1-45** to match *your desired “user unit”* (degrees, mm, etc.) at the **load side**. - ### With a gearbox -- Motor counts per **load revolution** = *(motor encoder counts per motor rev)* × *(gear ratio)* -- If you want e.g. **360 units per load rev** (degrees), then: +- Motor counts per **load revolution** = (motor counts / motor rev) × (gear ratio) +- Choose user units per load revolution (e.g. **360 for degrees**) \[ -\\frac{P1-44}{P1-45} = \\frac{\\text{motor counts per load rev}}{\\text{units per load rev} \\times \\text{controller pulses per unit}} +\frac{P1-44}{P1-45} = \frac{\text{motor counts per load rev}}{\text{units per load rev}\times\text{controller pulses per unit}} \] -Use the **PUU Calculator** page to get clean integers. +### What “smoothness” really means +A good rule of thumb is keeping **counts per user unit** high enough to avoid coarse stepping at low speed. +This app shows **counts per user unit** and warns you if it drops below your chosen minimum. """ ) @@ -91,143 +418,379 @@ elif page == "PUU Calculator": st.title("🔢 Universal PUU Calculator (P1-44 / P1-45)") st.caption( - "Goal: convert your controller command pulses into the correct motor encoder counts, " - "including gearbox ratio, and optionally define a meaningful user unit (deg/mm/etc.)." + "Compute electronic gearing for Delta ASDA-A2. Includes presets, exact vs drive-friendly fraction, and validations." ) col1, col2 = st.columns(2) with col1: st.subheader("Input") - motor_counts_per_rev = st.number_input( + + st.session_state.motor_counts_per_rev = st.number_input( "Motor encoder counts per motor revolution", - min_value=1, - value=128000, - step=1000, - help="Common values: 128000, 131072, 1048576 ... depends on encoder/resolution & drive setting.", + min_value=1.0, + value=float(st.session_state.motor_counts_per_rev), + step=1000.0, + help="Examples: 128000, 131072, 1048576 (depends on encoder + drive setting).", ) - gear_ratio = st.number_input( + st.session_state.gear_ratio = st.number_input( "Gear ratio (motor rev per load rev)", - min_value=1.0, - value=50.0, + min_value=0.0001, + value=float(st.session_state.gear_ratio), step=1.0, - help="For 50:1 reduction, motor turns 50 rev while load turns 1 rev => enter 50.", + help="50:1 reduction => enter 50.", ) - units_per_load_rev = st.number_input( - "User units per load revolution", - min_value=1, - value=360, - step=1, - help="Examples: degrees=360, turns=1, mm per rev for a leadscrew (e.g. 5 mm/rev => 5).", + # Presets + preset = st.selectbox( + "User unit preset", + [ + "Degrees at output (360 units / load rev)", + "Turns at output (1 unit / load rev)", + "Leadscrew linear mm (units = mm)", + "Belt/Pulley linear mm (units = mm)", + "Custom", + ], + index=[ + "Degrees at output (360 units / load rev)", + "Turns at output (1 unit / load rev)", + "Leadscrew linear mm (units = mm)", + "Belt/Pulley linear mm (units = mm)", + "Custom", + ].index(st.session_state.unit_preset), ) + st.session_state.unit_preset = preset - controller_pulses_per_unit = st.number_input( + if preset == "Degrees at output (360 units / load rev)": + st.session_state.units_per_load_rev = 360.0 + elif preset == "Turns at output (1 unit / load rev)": + st.session_state.units_per_load_rev = 1.0 + elif preset == "Leadscrew linear mm (units = mm)": + st.session_state.lead_mm_per_rev = st.number_input( + "Leadscrew lead (mm per load revolution)", + min_value=0.0001, + value=float(st.session_state.lead_mm_per_rev), + step=0.1, + ) + st.session_state.units_per_load_rev = float(st.session_state.lead_mm_per_rev) + elif preset == "Belt/Pulley linear mm (units = mm)": + st.session_state.pulley_circum_mm = st.number_input( + "Pulley circumference (mm per load revolution)", + min_value=0.0001, + value=float(st.session_state.pulley_circum_mm), + step=1.0, + ) + st.session_state.units_per_load_rev = float(st.session_state.pulley_circum_mm) + else: + st.session_state.units_per_load_rev = st.number_input( + "User units per load revolution", + min_value=0.0001, + value=float(st.session_state.units_per_load_rev), + step=1.0, + ) + + st.session_state.controller_pulses_per_unit = st.number_input( "Controller command pulses per user unit", - min_value=1.0, - value=1.0, + min_value=0.0001, + value=float(st.session_state.controller_pulses_per_unit), step=1.0, - help="If your controller outputs 1 pulse = 1 user unit, keep 1. " - "If it outputs e.g. 10 pulses per degree, enter 10.", + help="If 1 pulse = 1 user unit, keep 1. If 10 pulses per degree, enter 10.", ) - ratio_limit = st.number_input( + st.session_state.ratio_limit = st.number_input( "Max integer limit for P1-44 / P1-45 (safety)", min_value=1000, - value=2_147_483_647, + value=int(st.session_state.ratio_limit), step=1000, - help="Keeps numbers within typical 32-bit signed range. " - "If you know the drive's tighter limit, set it here.", ) - # Calculate - # motor_counts_per_load_rev = motor_counts_per_rev * gear_ratio - # Desired motor counts per controller pulse = motor_counts_per_load_rev / (units_per_load_rev * controller_pulses_per_unit) - motor_counts_per_load_rev = motor_counts_per_rev * gear_ratio - denom_units = units_per_load_rev * controller_pulses_per_unit + with st.expander("Delta A2 ratio limits (editable)"): + st.session_state.delta_min_ratio = st.number_input( + "Min electronic gear ratio", + min_value=0.0, + value=float(st.session_state.delta_min_ratio), + step=0.0001, + format="%.6f", + ) + st.session_state.delta_max_ratio = st.number_input( + "Max electronic gear ratio", + min_value=0.0, + value=float(st.session_state.delta_max_ratio), + step=100.0, + format="%.3f", + ) - st.divider() + st.session_state.smooth_min_counts_per_user_unit = st.number_input( + "Recommended minimum counts per user unit (smoothness)", + min_value=1.0, + value=float(st.session_state.smooth_min_counts_per_user_unit), + step=100.0, + help="Rule of thumb to avoid stepping/vibration at low speeds.", + ) - # Convert to integer fraction: - # We want P1-44/P1-45 = motor_counts_per_load_rev / denom_units - # If denom_units isn't integer, scale to preserve precision. - # Use 1e6 scaling for decimals, then simplify. - SCALE = 1_000_000 - n_raw = int(round(motor_counts_per_load_rev * SCALE)) - d_raw = int(round(denom_units * SCALE)) + # Compute PUU + motor_counts_per_load_rev = st.session_state.motor_counts_per_rev * st.session_state.gear_ratio + denom_units = st.session_state.units_per_load_rev * st.session_state.controller_pulses_per_unit + target_ratio = motor_counts_per_load_rev / denom_units - n_s, d_s = simplify_fraction(n_raw, d_raw) - n_final, d_final = scale_down_to_limit(n_s, d_s, int(ratio_limit)) + try: + puu = compute_puu( + motor_counts_per_rev=st.session_state.motor_counts_per_rev, + gear_ratio=st.session_state.gear_ratio, + units_per_load_rev=st.session_state.units_per_load_rev, + controller_pulses_per_unit=st.session_state.controller_pulses_per_unit, + ratio_limit=int(st.session_state.ratio_limit), + ) + except Exception as e: + st.error(f"PUU computation error: {e}") + st.stop() + + counts_per_user_unit_load = motor_counts_per_load_rev / st.session_state.units_per_load_rev + units_per_motor_rev = st.session_state.units_per_load_rev / st.session_state.gear_ratio + + validations = validate_delta_limits( + ratio=puu.chosen_ratio, + min_ratio=float(st.session_state.delta_min_ratio), + max_ratio=float(st.session_state.delta_max_ratio), + n=puu.n_final, + d=puu.d_final, + int_limit=int(st.session_state.ratio_limit), + ) with col2: st.subheader("Result") - st.success("Recommended electronic gear ratio for PUU:") - st.markdown( - f""" -- **P1-44 (Numerator / N):** **{n_final}** -- **P1-45 (Denominator / M):** **{d_final}** - """ - ) + # Headline metrics + m1, m2, m3 = st.columns(3) + m1.metric("P1-44 (N)", f"{puu.n_final}") + m2.metric("P1-45 (M)", f"{puu.d_final}") + m3.metric("Ratio (N/M)", f"{puu.chosen_ratio:.6f}") - # Show what it means - pulses_to_motor_counts = n_final / d_final - st.write(f"✅ This means: **1 controller pulse → {pulses_to_motor_counts:.6f} motor encoder counts**") + st.markdown("### Exact vs drive-friendly") + st.write(f"- **Exact fraction:** {puu.n_exact} / {puu.d_exact}") + st.write(f"- **Final fraction:** {puu.n_final} / {puu.d_final}") + st.write(f"- **Target ratio:** {puu.target_ratio:.9f}") + st.write(f"- **Chosen ratio:** {puu.chosen_ratio:.9f}") + st.write(f"- **Error:** {format_ppm(puu.error_ppm)}") - # Derived helpful values - motor_counts_per_user_unit = motor_counts_per_load_rev / units_per_load_rev - controller_pulses_per_load_rev = units_per_load_rev * controller_pulses_per_unit - motor_counts_per_controller_rev = controller_pulses_per_load_rev * pulses_to_motor_counts + st.markdown("### Meaning") + st.write(f"✅ **1 controller pulse → {puu.chosen_ratio:.6f} motor encoder counts**") st.markdown("### Sanity checks") st.write(f"- Motor counts per **load rev**: **{motor_counts_per_load_rev:.3f}**") - st.write(f"- User units per **load rev**: **{units_per_load_rev:.3f}**") - st.write(f"- Motor counts per **user unit** (load side): **{motor_counts_per_user_unit:.3f}**") - st.write(f"- Controller pulses per **load rev**: **{controller_pulses_per_load_rev:.3f}**") - st.write(f"- Motor counts generated by **controller pulses per load rev**: **{motor_counts_per_controller_rev:.3f}**") + st.write(f"- User units per **load rev**: **{st.session_state.units_per_load_rev:.6f}**") + st.write(f"- Counts per **user unit** (load side): **{counts_per_user_unit_load:.3f}**") + st.write(f"- User units per **motor rev**: **{units_per_motor_rev:.6f}**") + + if counts_per_user_unit_load >= st.session_state.smooth_min_counts_per_user_unit: + st.success( + f"Smoothness OK: counts/user-unit = {counts_per_user_unit_load:.1f} ≥ {st.session_state.smooth_min_counts_per_user_unit:.1f}" + ) + else: + st.warning( + f"Low smoothness: counts/user-unit = {counts_per_user_unit_load:.1f} < {st.session_state.smooth_min_counts_per_user_unit:.1f}. " + "Consider increasing counts per unit (e.g., change pulses/unit, change unit scaling)." + ) + + st.markdown("### Validations") + for v in validations: + if v.startswith("❌"): + st.error(v) + elif v.startswith("⚠️"): + st.warning(v) + else: + st.success(v) + + # Suggestions if out-of-range + sugg = suggestions_if_out_of_range(puu.chosen_ratio, st.session_state.delta_min_ratio, st.session_state.delta_max_ratio) + if sugg: + st.markdown("### Fix suggestions") + for s in sugg: + st.write(s) + + st.divider() + + # Copy-to-clipboard (simple HTML button) + st.markdown("### Quick copy") + txt = f"P1-44={puu.n_final}, P1-45={puu.d_final}" + st.code(txt) + st.components.v1.html( + f""" + + """, + height=55, + ) + +elif page == "Speed Translator": + st.title("⚡ Speed Translator (Motor ↔ Gearbox ↔ Load ↔ User Units)") + + st.caption("Convert between motor RPM, load RPM, and user units/s based on gearbox and unit definition.") + + col1, col2 = st.columns(2) + + with col1: + st.subheader("Inputs") + + st.session_state.gear_ratio = st.number_input( + "Gear ratio (motor rev per load rev)", + min_value=0.0001, + value=float(st.session_state.gear_ratio), + step=1.0, + ) + + st.session_state.units_per_load_rev = st.number_input( + "User units per load revolution", + min_value=0.0001, + value=float(st.session_state.units_per_load_rev), + step=1.0, + ) + + st.session_state.motor_rated_rpm = st.number_input( + "Motor rated RPM (for warning)", + min_value=1.0, + value=float(st.session_state.motor_rated_rpm), + step=100.0, + ) + + st.session_state.speed_input_mode = st.radio( + "Input mode", + ["Load RPM → Motor RPM", "Motor RPM → Load RPM"], + index=0 if st.session_state.speed_input_mode == "Load RPM → Motor RPM" else 1, + ) + + load_rpm = None + motor_rpm = None + if st.session_state.speed_input_mode == "Load RPM → Motor RPM": + st.session_state.load_rpm_in = st.number_input( + "Load RPM", + min_value=0.0, + value=float(st.session_state.load_rpm_in), + step=1.0, + ) + load_rpm = float(st.session_state.load_rpm_in) + else: + st.session_state.motor_rpm_in = st.number_input( + "Motor RPM", + min_value=0.0, + value=float(st.session_state.motor_rpm_in), + step=10.0, + ) + motor_rpm = float(st.session_state.motor_rpm_in) + + speed = calc_speed_translator( + gear_ratio=float(st.session_state.gear_ratio), + units_per_load_rev=float(st.session_state.units_per_load_rev), + motor_rpm=motor_rpm, + load_rpm=load_rpm, + ) + + with col2: + st.subheader("Results") + a, b, c = st.columns(3) + a.metric("Motor RPM", f"{speed['Motor RPM']:.2f}") + b.metric("Load RPM", f"{speed['Load RPM']:.2f}") + c.metric("User units / s", f"{speed['User units / s']:.3f}") + + st.write(f"- Motor RPS: **{speed['Motor RPS']:.4f}**") + st.write(f"- Load RPS: **{speed['Load RPS']:.4f}**") + st.write(f"- User units/min: **{speed['User units / min']:.3f}**") + + if speed["Motor RPM"] > st.session_state.motor_rated_rpm: + st.error( + f"❌ Motor RPM exceeds rated RPM: {speed['Motor RPM']:.1f} > {st.session_state.motor_rated_rpm:.1f}" + ) + else: + st.success("✅ Motor RPM within rated RPM.") + +elif page == "Torque Estimator": + st.title("🧲 Torque Estimator (Gearbox Amplification)") + + st.caption("Quick estimator: output torque ≈ motor torque × gear ratio × efficiency.") + + col1, col2 = st.columns(2) + + with col1: + st.subheader("Inputs") + st.session_state.gear_ratio = st.number_input( + "Gear ratio (motor rev per load rev)", + min_value=0.0001, + value=float(st.session_state.gear_ratio), + step=1.0, + ) + st.session_state.motor_rated_torque_nm = st.number_input( + "Motor rated torque (Nm)", + min_value=0.0, + value=float(st.session_state.motor_rated_torque_nm), + step=0.1, + ) + st.session_state.motor_peak_torque_nm = st.number_input( + "Motor peak torque (Nm)", + min_value=0.0, + value=float(st.session_state.motor_peak_torque_nm), + step=0.1, + ) + st.session_state.gear_efficiency = st.number_input( + "Gearbox efficiency (0..1)", + min_value=0.01, + max_value=1.0, + value=float(st.session_state.gear_efficiency), + step=0.01, + ) + + tor = calc_torque_estimator( + motor_rated_nm=float(st.session_state.motor_rated_torque_nm), + motor_peak_nm=float(st.session_state.motor_peak_torque_nm), + gear_ratio=float(st.session_state.gear_ratio), + efficiency=float(st.session_state.gear_efficiency), + ) + + with col2: + st.subheader("Results") + m1, m2 = st.columns(2) + m1.metric("Output rated torque (Nm)", f"{tor['Estimated output rated torque (Nm)']:.2f}") + m2.metric("Output peak torque (Nm)", f"{tor['Estimated output peak torque (Nm)']:.2f}") st.info( - "Tip: The last line should match (or be very close to) motor counts per load rev. " - "If it's far off, check encoder counts, gear ratio, and pulses-per-unit." - ) - st.info( - "Recommended Minimum: It is generally recommended to keep the PUU per motor revolution above 5000 to avoid 'stepping' or vibration at low speeds." - ) - st.info( - f"Electronic Gear Ratio Limit: Delta A2 allows a ratio between $1/50$ and $25600$. " - f"Your ratio of {motor_counts_per_load_rev}/{units_per_load_rev} ≈ {motor_counts_per_load_rev / units_per_load_rev:.3f} " - f"is well within this safe range." + "Safety tip: during first tests, limit torque (P1-12/P1-13) because gearbox output torque can be huge." ) elif page == "Parameter Guide": st.title("🛠️ Essential Parameter Setup") - st.write("Beyond the PUU (P1-44/45), these parameters are critical for a 50:1 gearbox system.") + st.write("Beyond PUU (P1-44/45), these are high-impact parameters for gearbox systems.") st.subheader("1. The 'Big Three' for Gearbox Safety") st.markdown( """ | Parameter | Name | Recommended Value | Why? | | :--- | :--- | :--- | :--- | -| **P1-44** | Numerator (N) | *From Calculator* | Sets electronic gear / user units. | -| **P1-45** | Denominator (M) | *From Calculator* | Sets electronic gear / user units. | -| **P1-37** | Load to Motor Inertia | **10.0 ~ 20.0** | A 50:1 gearbox increases reflected inertia significantly. Start here for tuning. | +| **P1-44** | Numerator (N) | *From Calculator* | Electronic gear / user units. | +| **P1-45** | Denominator (M) | *From Calculator* | Electronic gear / user units. | +| **P1-37** | Load to Motor Inertia | **10.0 ~ 20.0** | Gearbox reflects inertia to the motor; start here for tuning. | """ ) st.subheader("2. Speed & Torque Limits") st.info( - "With 50:1 reduction, output torque is multiplied ~50× while output speed is divided ~50×. " - "Use limits to protect mechanics during first tests." + "Gear reduction increases output torque and reduces output speed. Use limits to protect mechanics." ) - st.markdown( """ -- **P1-02 (Speed Limit):** Set to motor-rated safe speed (e.g., 3000 RPM) to prevent overspeed. -- **P1-12 / P1-13 (Torque Limit):** Start conservative (e.g., 30–60%) during first motion tests to avoid smashing end-stops. +- **P1-02 (Speed Limit):** Set to motor-rated speed or your machine limit. +- **P1-12 / P1-13 (Torque Limit):** Start conservative (30–60%) for first movement tests. """ ) + st.subheader("3. Parameter templates (copy/paste style)") + tpl = template_blocks() + for name, items in tpl.items(): + with st.expander(name, expanded=False): + for line in items: + st.write(f"- {line}") + st.divider() st.subheader("🔄 Factory Reset (Set to Default)") @@ -257,6 +820,31 @@ elif page == "Parameter Guide": """ ) +elif page == "Commissioning Checklist": + st.title("✅ Commissioning Checklist (Field Workflow)") + + st.caption("A practical sequence to minimize mistakes and avoid mechanical damage.") + + steps = [ + ("Factory reset if needed", "Reset P2-08=10 (or ASDA-Soft Initial) if you're unsure about previous settings."), + ("Confirm encoder mode / counts", "Verify motor encoder counts per rev in drive settings."), + ("Set gearbox ratio", "Confirm gear ratio direction and value (motor rev per load rev)."), + ("Compute & set PUU", "Use PUU Calculator → write P1-44 and P1-45."), + ("Set SAFE limits", "Set P1-02 low; set P1-12/13 torque limit 30–60%."), + ("Jog test (low speed)", "Short jog, verify direction and smoothness."), + ("Verify scale", "One load revolution equals your expected user units (360° / mm etc.)."), + ("Tune", "Set P1-37 and do auto-tune if used; increase limits gradually."), + ("Production limits", "Set final speed/torque limits and safety interlocks."), + ("Export report", "Use Export Report page to save settings for backup & documentation."), + ] + + for i, (title, detail) in enumerate(steps, 1): + checked = st.checkbox(f"{i}. {title}", key=f"chk_{i}") + if checked: + st.success(detail) + else: + st.write(detail) + elif page == "ASDA-Soft Guide": st.title("💾 Implementation in ASDA-Soft") st.markdown( @@ -269,10 +857,119 @@ To apply your **P1-44** and **P1-45** values: 4. Click **Write to Drive** 5. **Power cycle / restart** the drive if required for the change to take effect -### Quick workflow -- Calculate PUU in the **PUU Calculator** -- Set **P1-44 / P1-45** +### Recommended workflow +- Compute PUU in **PUU Calculator** - Set safe limits (**P1-02**, **P1-12**, **P1-13**) -- Then tune (**P1-37** and auto-tuning if you use it) +- Verify motion direction and scaling +- Tune (**P1-37** and tuning flow) """ ) + +elif page == "Export Report": + st.title("🧾 Export Commissioning Report (Markdown / PDF)") + st.caption("Generates a report of your current inputs, PUU result, checks, speed and torque estimates.") + + # Recompute PUU + checks for report + puu = compute_puu( + motor_counts_per_rev=float(st.session_state.motor_counts_per_rev), + gear_ratio=float(st.session_state.gear_ratio), + units_per_load_rev=float(st.session_state.units_per_load_rev), + controller_pulses_per_unit=float(st.session_state.controller_pulses_per_unit), + ratio_limit=int(st.session_state.ratio_limit), + ) + motor_counts_per_load_rev = float(st.session_state.motor_counts_per_rev) * float(st.session_state.gear_ratio) + counts_per_user_unit_load = motor_counts_per_load_rev / float(st.session_state.units_per_load_rev) + units_per_motor_rev = float(st.session_state.units_per_load_rev) / float(st.session_state.gear_ratio) + + validations = validate_delta_limits( + ratio=puu.chosen_ratio, + min_ratio=float(st.session_state.delta_min_ratio), + max_ratio=float(st.session_state.delta_max_ratio), + n=puu.n_final, + d=puu.d_final, + int_limit=int(st.session_state.ratio_limit), + ) + sugg = suggestions_if_out_of_range( + puu.chosen_ratio, + float(st.session_state.delta_min_ratio), + float(st.session_state.delta_max_ratio), + ) + + speed = calc_speed_translator( + gear_ratio=float(st.session_state.gear_ratio), + units_per_load_rev=float(st.session_state.units_per_load_rev), + motor_rpm=float(st.session_state.motor_rpm_in) if st.session_state.speed_input_mode == "Motor RPM → Load RPM" else None, + load_rpm=float(st.session_state.load_rpm_in) if st.session_state.speed_input_mode == "Load RPM → Motor RPM" else None, + ) + tor = calc_torque_estimator( + motor_rated_nm=float(st.session_state.motor_rated_torque_nm), + motor_peak_nm=float(st.session_state.motor_peak_torque_nm), + gear_ratio=float(st.session_state.gear_ratio), + efficiency=float(st.session_state.gear_efficiency), + ) + + report_data = { + "inputs": { + "Motor counts / rev": st.session_state.motor_counts_per_rev, + "Gear ratio (motor/load)": st.session_state.gear_ratio, + "User units / load rev": st.session_state.units_per_load_rev, + "Controller pulses / user unit": st.session_state.controller_pulses_per_unit, + "Integer limit": st.session_state.ratio_limit, + "Delta min ratio": st.session_state.delta_min_ratio, + "Delta max ratio": st.session_state.delta_max_ratio, + "Smooth min counts/user-unit": st.session_state.smooth_min_counts_per_user_unit, + "Motor rated RPM": st.session_state.motor_rated_rpm, + "Motor rated torque (Nm)": st.session_state.motor_rated_torque_nm, + "Motor peak torque (Nm)": st.session_state.motor_peak_torque_nm, + "Gear efficiency": st.session_state.gear_efficiency, + }, + "puu": { + "P1-44 (N) final": puu.n_final, + "P1-45 (M) final": puu.d_final, + "Ratio (N/M)": f"{puu.chosen_ratio:.9f}", + "Exact fraction": f"{puu.n_exact}/{puu.d_exact}", + "Error": format_ppm(puu.error_ppm), + }, + "sanity": { + "Motor counts per load rev": f"{motor_counts_per_load_rev:.3f}", + "Counts per user unit (load side)": f"{counts_per_user_unit_load:.3f}", + "User units per motor rev": f"{units_per_motor_rev:.6f}", + }, + "validations": validations + (["\n".join(sugg)] if sugg else []), + "speed": { + "Motor RPM": f"{speed['Motor RPM']:.2f}", + "Load RPM": f"{speed['Load RPM']:.2f}", + "User units / s": f"{speed['User units / s']:.3f}", + "User units / min": f"{speed['User units / min']:.3f}", + }, + "torque": { + "Efficiency": f"{tor['Efficiency']:.3f}", + "Output rated torque (Nm)": f"{tor['Estimated output rated torque (Nm)']:.2f}", + "Output peak torque (Nm)": f"{tor['Estimated output peak torque (Nm)']:.2f}", + }, + "templates": template_blocks(), + } + + md = build_markdown_report(report_data) + + st.subheader("Preview") + st.markdown(md) + + st.subheader("Download") + st.download_button( + "Download Markdown report (.md)", + data=md.encode("utf-8"), + file_name="asda_a2_commissioning_report.md", + mime="text/markdown", + ) + + if REPORTLAB_OK: + pdf_bytes = markdown_to_pdf_bytes("ASDA-A2 Commissioning Report", md) + st.download_button( + "Download PDF report (.pdf)", + data=pdf_bytes, + file_name="asda_a2_commissioning_report.pdf", + mime="application/pdf", + ) + else: + st.info("PDF export unavailable (reportlab not installed in this environment).")