streamlit-delta-PUU-calculator/main.py
2026-01-06 18:20:57 +01:00

976 lines
34 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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))
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()
# ----------------------------
# 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., 300800 RPM) for first tests",
"P1-12 / P1-13 Torque Limit: 3060% 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 1020 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",
"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(
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}
\]
### With a gearbox
- 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}}
\]
### 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.
"""
)
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")
st.session_state.motor_counts_per_rev = st.number_input(
"Motor encoder counts per motor revolution",
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).",
)
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 = 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=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.",
)
st.session_state.ratio_limit = st.number_input(
"Max integer limit for P1-44 / P1-45 (safety)",
min_value=1000,
value=int(st.session_state.ratio_limit),
step=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 = 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
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")
# 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}")
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**: **{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"""
<button onclick="navigator.clipboard.writeText('{txt}')"
style="padding:8px 12px;border-radius:8px;border:1px solid #888;cursor:pointer;">
Copy P1-44/P1-45 to clipboard
</button>
""",
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(
"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 (3060%) 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(),
language="markdown",
)
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 3060%."),
("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.")
# 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).")