#!/usr/bin/env python3
"""读取 FANUC J519 允许速度/加速度/jerk 上限表。

本脚本只做 UDP packet type=3 查询，不启动运行时服务，也不写入机器人参数。
响应中的 limit 值保留为控制器原始单位：速度 deg/s、加速度 deg/s^2、jerk deg/s^3。
"""

from __future__ import annotations

import argparse
import csv
import socket
import struct
import sys
from dataclasses import dataclass
from pathlib import Path


LIMIT_TYPES = {
    0: ("velocity", "deg/s"),
    1: ("acceleration", "deg/s^2"),
    2: ("jerk", "deg/s^3"),
}


@dataclass(frozen=True)
class LimitTable:
    """保存单个轴、单种限制类型的 20 档 no-payload / max-payload 上限表。"""

    axis: int
    limit_type: int
    vmax_mm_s: int
    intermediate_check_s: int
    no_payload: tuple[float, ...]
    max_payload: tuple[float, ...]


def pack_request(axis: int, limit_type: int) -> bytes:
    """按手册 Table 5 封装允许上限表查询请求。"""

    return struct.pack(">IIII", 3, 1, axis, limit_type)


def parse_response(packet: bytes, expected_axis: int, expected_type: int) -> LimitTable:
    """按手册 Table 6 解析允许上限表响应。"""

    if len(packet) != 184:
        raise ValueError(f"响应长度应为 184 字节，实际为 {len(packet)} 字节。")

    packet_type, version, axis, limit_type, vmax, check_time = struct.unpack_from(">IIIIII", packet, 0)
    if packet_type != 3 or version != 1:
        raise ValueError(f"响应头不正确：packet_type={packet_type}, version={version}。")
    if axis != expected_axis or limit_type != expected_type:
        raise ValueError(
            f"响应与请求不匹配：期望 axis={expected_axis}, type={expected_type}，"
            f"实际 axis={axis}, type={limit_type}。"
        )

    values = struct.unpack_from(">40f", packet, 24)
    return LimitTable(
        axis=axis,
        limit_type=limit_type,
        vmax_mm_s=vmax,
        intermediate_check_s=check_time,
        no_payload=tuple(values[:20]),
        max_payload=tuple(values[20:]),
    )


def query_table(sock: socket.socket, robot_ip: str, port: int, axis: int, limit_type: int) -> LimitTable:
    """发送单个查询请求并等待对应响应。"""

    sock.sendto(pack_request(axis, limit_type), (robot_ip, port))
    packet, address = sock.recvfrom(2048)
    if address[0] != robot_ip:
        raise ValueError(f"收到非目标机器人响应：{address[0]}:{address[1]}。")
    return parse_response(packet, axis, limit_type)


def iter_requested_tables(
    robot_ip: str,
    port: int,
    timeout_s: float,
    axes: range,
    limit_types: tuple[int, ...],
) -> list[LimitTable]:
    """按轴和限制类型顺序读取所有上限表。"""

    tables: list[LimitTable] = []
    with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as sock:
        sock.settimeout(timeout_s)
        for axis in axes:
            for limit_type in limit_types:
                tables.append(query_table(sock, robot_ip, port, axis, limit_type))
    return tables


def write_csv(path: Path, tables: list[LimitTable]) -> None:
    """把 20 档速度区间和两套 payload 表写成扁平 CSV。"""

    with path.open("w", newline="", encoding="utf-8") as file:
        writer = csv.writer(file)
        writer.writerow(
            [
                "axis",
                "limit_type",
                "unit",
                "vmax_mm_s",
                "intermediate_check_s",
                "speed_bin_index",
                "speed_bin_upper_mm_s",
                "no_payload_limit",
                "max_payload_limit",
            ]
        )
        for table in tables:
            limit_name, unit = LIMIT_TYPES[table.limit_type]
            for index, (no_payload, max_payload) in enumerate(zip(table.no_payload, table.max_payload), start=1):
                writer.writerow(
                    [
                        table.axis,
                        limit_name,
                        unit,
                        table.vmax_mm_s,
                        table.intermediate_check_s,
                        index,
                        table.vmax_mm_s * index / 20.0,
                        f"{no_payload:.9g}",
                        f"{max_payload:.9g}",
                    ]
                )


def print_summary(tables: list[LimitTable]) -> None:
    """打印每张表的首末档，便于现场快速确认返回值是否正常。"""

    for table in tables:
        limit_name, unit = LIMIT_TYPES[table.limit_type]
        print(
            f"axis={table.axis} type={limit_name:<12} unit={unit:<8} "
            f"vmax={table.vmax_mm_s}mm/s check={table.intermediate_check_s}s "
            f"no_payload[1]={table.no_payload[0]:.6g} no_payload[20]={table.no_payload[-1]:.6g} "
            f"max_payload[1]={table.max_payload[0]:.6g} max_payload[20]={table.max_payload[-1]:.6g}"
        )


def parse_args() -> argparse.Namespace:
    """解析命令行参数。"""

    parser = argparse.ArgumentParser(
        description="Read FANUC J519 packet type=3 allowable upper limit tables.",
    )
    parser.add_argument("robot_ip", help="机器人控制柜 IP，例如 192.168.10.11。")
    parser.add_argument("--port", type=int, default=60015, help="J519 UDP 端口，默认 60015。")
    parser.add_argument("--timeout", type=float, default=2.0, help="单次响应超时秒数，默认 2.0。")
    parser.add_argument("--axis-start", type=int, default=1, help="起始轴号，默认 1。")
    parser.add_argument("--axis-end", type=int, default=9, help="结束轴号，默认 9。")
    parser.add_argument(
        "--types",
        default="0,1,2",
        help="限制类型列表：0=velocity, 1=acceleration, 2=jerk；默认 0,1,2。",
    )
    parser.add_argument(
        "--csv",
        type=Path,
        default=Path("analysis/fanuc_allowable_limit_tables.csv"),
        help="CSV 输出路径。",
    )
    return parser.parse_args()


def main() -> int:
    """脚本入口。"""

    args = parse_args()
    limit_types = tuple(int(item.strip()) for item in args.types.split(",") if item.strip())
    unknown_types = [item for item in limit_types if item not in LIMIT_TYPES]
    if unknown_types:
        print(f"未知限制类型：{unknown_types}", file=sys.stderr)
        return 2
    if not 1 <= args.axis_start <= args.axis_end <= 9:
        print("轴范围必须满足 1 <= axis-start <= axis-end <= 9。", file=sys.stderr)
        return 2

    tables = iter_requested_tables(
        args.robot_ip,
        args.port,
        args.timeout,
        range(args.axis_start, args.axis_end + 1),
        limit_types,
    )
    args.csv.parent.mkdir(parents=True, exist_ok=True)
    write_csv(args.csv, tables)
    print_summary(tables)
    print(f"\nCSV 已写入：{args.csv}")
    return 0


if __name__ == "__main__":
    raise SystemExit(main())
