# 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 # ---------------------------- def simplify_fraction(n: int, d: int) -> tuple[int, int]: if d == 0: raise ValueError("Denominator cannot be 0.") if n == 0: return 0, 1 g = gcd(abs(n), abs(d)) n //= g d //= g if d < 0: n *= -1 d *= -1 return n, d def scale_down_to_limit(n: int, d: int, limit: int) -> tuple[int, int]: """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 factor = max((n_abs + limit - 1) // limit, (d_abs + limit - 1) // limit) n2 = n // factor d2 = d // factor 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)) # ---- SI-style numeric input (Slovenia formatting) ---- def format_si_number(x: float, decimals: int = 0) -> str: """Format number like 360.000 (thousands='.', decimal=',').""" if x is None: return "" sign = "-" if x < 0 else "" x = abs(float(x)) p = 10 ** decimals x = round(x * p) / p int_part = int(x) frac_part = x - int_part int_str = f"{int_part:,}".replace(",", ".") # thousands if decimals <= 0: return f"{sign}{int_str}" frac_str = f"{frac_part:.{decimals}f}".split(".")[1] return f"{sign}{int_str},{frac_str}" def parse_si_number(s: str, allow_float: bool = True) -> float: """ Accept: 360000 360.000 360 000 360.000,25 360000.25 """ if s is None: raise ValueError("Empty") t = s.strip().replace(" ", "") if t == "": raise ValueError("Empty") if "," in t: # decimal comma -> remove thousands '.' and convert comma to '.' t = t.replace(".", "") t = t.replace(",", ".") else: # no comma: dot might be thousands or decimal if "." in t: last = t.split(".")[-1] if len(last) == 3 and last.isdigit(): t = t.replace(".", "") # thousands separators # else keep '.' as decimal point val = float(t) if not allow_float and abs(val - int(val)) > 1e-12: raise ValueError("Integer required") return float(int(val)) if not allow_float else val def si_number_input( label: str, key: str, value: float, decimals: int = 0, allow_float: bool = False, min_value: float | None = None, help: str | None = None, ): txt_key = f"{key}__txt" err_key = f"{key}__err" # init state if key not in st.session_state: st.session_state[key] = value if txt_key not in st.session_state: st.session_state[txt_key] = format_si_number(value, decimals) if err_key not in st.session_state: st.session_state[err_key] = "" def _commit(): raw = st.session_state.get(txt_key, "") try: v = parse_si_number(raw, allow_float=allow_float) if min_value is not None and v < min_value: raise ValueError(f"Must be ≥ {min_value}") st.session_state[key] = v st.session_state[err_key] = "" # ✅ normalize display to SI formatting st.session_state[txt_key] = format_si_number(v, decimals) except Exception as e: st.session_state[err_key] = str(e) st.text_input(label, key=txt_key, help=help, on_change=_commit) # show error (if any) if st.session_state.get(err_key): st.error(f"Invalid number for '{label}': {st.session_state[err_key]}") return st.session_state[key] def build_markdown_report(data: dict) -> str: ts = datetime.now().strftime("%Y-%m-%d %H:%M:%S") lines = [] lines.append("# 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(): 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() # ---------------------------- # 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") 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", "Speed Translator", "Torque Estimator", "Parameter Guide", "Commissioning Checklist", "ASDA-Soft Guide", "Export Report", ], ) with st.sidebar: if st.button("Reset all inputs"): # reset numeric values for k, v in defaults.items(): st.session_state[k] = v # reset text inputs used by si_number_input for k in list(st.session_state.keys()): if k.endswith("__txt"): del st.session_state[k] st.rerun() # ---------------------------- # Pages # ---------------------------- if page == "Theory & Architecture": st.title("📚 Theory & Architecture") st.markdown("### What this tool does") st.write( "Helps you set **electronic gearing (PUU / P1-44 & P1-45)** and perform quick " "**speed/torque sanity checks** for gearbox systems." ) st.markdown("### Core equation") st.latex(r"\text{Motor encoder counts} = \text{Command pulses} \times \frac{P1\text{-}44}{P1\text{-}45}") st.markdown("### With a gearbox") st.markdown( "- Motor counts per **load revolution** = (motor counts / motor rev) × (gear ratio)\n" "- Choose **user units per load revolution** (example: **360** for degrees)\n" "- Include **controller pulses per user unit** if your controller scales commands" ) st.latex( r"\frac{P1\text{-}44}{P1\text{-}45}=" r"\frac{\text{motor counts per load rev}}{\text{units per load rev}\times\text{controller pulses per unit}}" ) st.markdown("### What “smoothness” means (practically)") st.write( "If **counts per user unit** is too low, motion can feel “steppy” at low speed. " "This app shows **counts/user-unit** and warns if you drop below your threshold." ) st.info( "Tip: If you command in **degrees**, 360 units/rev is a clean choice. " "If ratio becomes too large/small, scale units (e.g., 0.1° or 0.01°)." ) elif page == "PUU Calculator": st.title("🔢 Universal PUU Calculator (P1-44 / P1-45)") st.caption( "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") # BIG numbers => SI formatting (360.000 style) st.session_state.motor_counts_per_rev = si_number_input( "Motor encoder counts per motor revolution", key="motor_counts_per_rev", value=float(st.session_state.motor_counts_per_rev), decimals=0, allow_float=False, min_value=1, help="Examples: 128000, 131072, 1048576 (depends on encoder + drive setting).", ) # Smaller engineering values can remain number_input 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, help="50:1 reduction => enter 50.", ) # 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 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 = si_number_input( "User units per load revolution", key="units_per_load_rev", value=float(st.session_state.units_per_load_rev), decimals=0, # <-- shows 360 or 360.000, never 360.00 allow_float=False, # <-- integer only (degrees etc.) min_value=1, help="Example: 360 for degrees. You may type 360000 or 360.000 (same).", ) st.session_state.controller_pulses_per_unit = st.number_input( "Controller command pulses per user unit", min_value=0.0001, value=float(st.session_state.controller_pulses_per_unit), step=1.0, help="If 1 pulse = 1 user unit, keep 1. If 10 pulses per degree, enter 10.", ) # BIG integer => SI formatting st.session_state.ratio_limit = si_number_input( "Max integer limit for P1-44 / P1-45 (safety)", key="ratio_limit", value=float(st.session_state.ratio_limit), decimals=0, allow_float=False, min_value=1000, ) 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.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.", ) # Compute PUU motor_counts_per_load_rev = float(st.session_state.motor_counts_per_rev) * float(st.session_state.gear_ratio) denom_units = float(st.session_state.units_per_load_rev) * float(st.session_state.controller_pulses_per_unit) target_ratio = motor_counts_per_load_rev / denom_units try: 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), ) except Exception as e: st.error(f"PUU computation error: {e}") st.stop() 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), ) with col2: st.subheader("Result") 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}") 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)}") 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**: **{float(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 >= float(st.session_state.smooth_min_counts_per_user_unit): st.success( f"Smoothness OK: counts/user-unit = {counts_per_user_unit_load:.1f} ≥ {float(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} < {float(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) sugg = suggestions_if_out_of_range( puu.chosen_ratio, float(st.session_state.delta_min_ratio), float(st.session_state.delta_max_ratio), ) if sugg: st.markdown("### Fix suggestions") for s in sugg: st.write(s) st.divider() 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 = si_number_input( "User units per load revolution", key="units_per_load_rev", value=float(st.session_state.units_per_load_rev), decimals=0, # <-- shows 360 or 360.000, never 360.00 allow_float=False, # <-- integer only (degrees etc.) min_value=1, help="Example: 360 for degrees. You may type 360000 or 360.000 (same).", ) 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"] > float(st.session_state.motor_rated_rpm): st.error(f"❌ Motor RPM exceeds rated RPM: {speed['Motor RPM']:.1f} > {float(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("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 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* | 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("Gear reduction increases output torque and reduces output speed. Use limits to protect mechanics.") st.markdown( """ - **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)") st.warning("Warning: Resetting will erase all PUU and tuning settings!") col_a, col_b = st.columns(2) with col_a: st.markdown("#### Via Drive Keypad") st.code( """ 1. Go to Parameter P2-08. 2. Set value to 10. 3. Press 'SET'. 4. Power Cycle (Off/On). """.strip() ) with col_b: st.markdown("#### Via ASDA-Soft") st.markdown( """ 1. Open **Parameter Editor**. 2. Click **Initial** / **Factory Default**. 3. Confirm and click **Write**. 4. Restart the Drive. """ ) 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( """ To apply your **P1-44** and **P1-45** values: 1. Open **Parameter Editor** 2. Select **Group 1** (Basic Parameters) 3. Enter **P1-44** and **P1-45** 4. Click **Write to Drive** 5. **Power cycle / restart** the drive if required for the change to take effect ### Recommended workflow - Compute PUU in **PUU Calculator** - Set safe limits (**P1-02**, **P1-12**, **P1-13**) - 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.") 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).")