1091 lines
38 KiB
Python
1091 lines
38 KiB
Python
# 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"""
|
||
<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 = 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).")
|