ROS-Industrial 之 ABB_Driver ——ROS Server(1/3)

爲了研究ABB機器人與ROS的交互控制,最近和ABB的機器人還有ROS-Industrial槓上了,之前把ROSLAUNCH的.launch/XML文件的語法格式弄清楚了,現在搞abb_driver的東西!

1. 概述與前置條件

ABB_DriverROS-Industrial的一部分,而其中的ABB ROS Server是其中最重要的組成部分之一,其是用ABB自己開發的機器人編程語言RAPID編寫的,通過Socket接口和多任務處理完成與ROS的交互。ROS Server的代碼在IRC5控制器上測試可以使用,在其它類型的ABB控制器可能要經過調整,要在ABB機器人控制器上安裝ROS Server首先要滿足以下兩個條件:

  • 623-1: Multitasking
  • 616-1: PC Interface

就是說你的控制器裏要啓動這兩個功能組件。
上述條件滿足後先從機器人端開始研究ROS Server到底怎麼用。

2. ROS Server

ROS Server的文件組成

ROS Server按照功能主要由六類文件組成,對於單臂機器人來講每一類文件包含一個程序文件,但對於雙臂機器人來說,每類文件要包含兩個程序文件,因爲在程序執行的時候會在每一個單臂下生成各自的執行程序文件。從功能上講,這六類文件爲:

文件名 功能
ROS_common.sys 對共用的數據進行定義與賦值
ROS_messages.sys 對軌跡與關節點數據進行定義,並定義數據的接收與發送函數
ROS_socket.sys 定義Socket通訊功能相關的所有函數
ROS_stateServer.mod 狀態反饋服務器,向ROS發送機器人相關狀態
ROS_motionServer.mod 運動控制服務器
ROS_motion.mod 機器人實際運動控制程序

以上文件是於2017-12-22從GitHub裏直接下載下來的,還沒有進行代碼的修改,接下來先分析每一個文件的功能。

哎,等一下:關於權利聲明和軟件License什麼的都放在這裏了,適用於後面所有ROS Server的代碼,後面就不寫在代碼裏了。

Software License Agreement (BSD License)
Copyright (c) 2012, Edward Venator, Case Western Reserve University
Copyright (c) 2012, Jeremy Zoss, Southwest Research Institute

下面開始真正的程序內容。

ROS_common.sys

MODULE ROS_common(SYSMODULE)
RECORD ROS_joint_trajectory_pt
    robjoint joint_pos;
    num duration;
ENDRECORD
CONST num MAX_TRAJ_LENGTH := 100;
PERS bool ROS_trajectory_lock := false;
PERS ROS_joint_trajectory_pt ROS_trajectory{MAX_TRAJ_LENGTH};
PERS num ROS_trajectory_size := 0;
PERS bool ROS_new_trajectory := false;
ENDMODULE

此程序文件作爲控制器中的一個系統模塊對以下各大程序共用的變量和結構體進行了定義,下面一行一行分析每一行代碼的意思。
第一行代碼與最後一行代碼合起來看:

MODULE ROS_common(SYSMODULE)
······
ENDMODULE

這兩行代碼在RAPID中的意思是,在控制器中定義一個名爲ROS_common的系統模塊。MODULE和ENDMODULE是其中的關鍵字。(SYSMODULE)是程序屬性聲明。ROS_common就是定義的模塊名字了。

RECORD ROS_joint_trajectory_pt
    robjoint joint_pos;
    num duration;
ENDRECORD

定義一個關節空間內機器運行路徑的結構體,RECORD 和ENDRECORD爲定義結構體的關鍵字,ROS_joint_trajectory_pt爲定義的結構體的名稱,其中robjoint爲定義機器目標點的關鍵字,robjoint類數據用於儲存機械臂軸1到6的軸位置,以度計。將軸位置定義爲各軸(臂)從軸校準位置沿正方向或負方向旋轉的度數。其結構爲:

RECORD robjoint
    num rax_1; 
    num rax_2; 
    num rax_3; 
    num rax_4; 
    num rax_5; 
    num rax_6; 
ENDRECORD

也就是說robjoint是用於六軸機器人進行關節位置定義的數據結構,但對於像Yumi這類七軸的機器人,則要用另一種數據結構進行定義,等我們對Yumi進行控制的時候會說明如何對程序進行修改,現在的任務是弄懂每類程序的意思。

num duration;

這一行代碼用來進行關節空間的週期控制,通過對週期進行控制進而控制關節空間點到點的關節速度。

CONST num MAX_TRAJ_LENGTH := 100;

此行代碼是定義關節點序列最大序列點的個數爲100,是一個常量,也就是說控制器最多從ROS端接收100個關節位置序列。CONST代表常量,num代表數值類型,MAX_TRAJ_LENGTH := 100是將名爲MAX_TRAJ_LENGTH 的數值類型變量賦值爲100,而 :=爲RAPID裏的賦值運算符。

PERS bool ROS_trajectory_lock := false;

此行代碼對是對關節軌跡數據定義一個讀寫信號量,讀的時候可以不調用信號量,但是在寫的時候要對路徑數據進行鎖定,隔斷其它的讀操作。其中PERS是定義永久變量的關鍵字,可以類似的理解爲C++中的全局變量。bool是邏輯變量的關鍵字,在RAPID中其與C++中定義邏輯變量的關鍵字是一樣的。

PERS ROS_joint_trajectory_pt ROS_trajectory{MAX_TRAJ_LENGTH};

此行代碼定義全局的機器人運行路徑數據,是一個結構體數組,基礎元素爲ROS_joint_trajectory_pt,其中包括關節軸的度數數據與對應的關節空間點到點的週期,數組長度爲100。

PERS num ROS_trajectory_size := 0;

此行代碼定義機器人路徑數據中實際關節空間點的個數。初始值爲0。

PERS bool ROS_new_trajectory := false;

此行代碼定義機器人控制器是否有新的路徑軌跡。
總體上來講ROS_common.sys程序文件中定義了機器人運動的路徑信息及對路徑信息進行讀寫等控制的相關對應數據。

ROS_messages.sys

整個ROS_messages.sys的代碼如下所示:

MODULE ROS_messages(SYSMODULE)

RECORD ROS_msg_header
    num msg_type;
    num comm_type;
    num reply_code;
ENDRECORD

RECORD ROS_msg
    ROS_msg_header header;
    rawbytes data;
ENDRECORD

RECORD ROS_msg_joint_traj_pt
    ROS_msg_header header;
    num sequence_id;
    robjoint joints;  ! in DEGREES
    num velocity;
    num duration;
ENDRECORD

RECORD ROS_msg_joint_data
    ROS_msg_header header;
    num sequence_id;
    robjoint joints;  ! in DEGREES
ENDRECORD

! Message Type Codes (from simple_message/simple_message.h)
CONST num ROS_MSG_TYPE_INVALID       := 0;
CONST num ROS_MSG_TYPE_JOINT         := 10;  ! joint-position feedback
CONST num ROS_MSG_TYPE_JOINT_TRAJ_PT := 11;  ! joint-trajectory-point (for path downloading)
CONST num ROS_COM_TYPE_TOPIC         := 1;
CONST num ROS_COM_TYPE_SRV_REQ       := 2;
CONST num ROS_COM_TYPE_SRV_REPLY     := 3;
CONST num ROS_REPLY_TYPE_INVALID     := 0;
CONST num ROS_REPLY_TYPE_SUCCESS     := 1;
CONST num ROS_REPLY_TYPE_FAILURE     := 2;

! "Special" Sequence Codes (from simple_message/joint_traj_pt.h)
CONST num ROS_TRAJECTORY_START_DOWNLOAD := -1;
CONST num ROS_TRAJECTORY_END := -3;
CONST num ROS_TRAJECTORY_STOP := -4;

! Other message constants
CONST num ROS_MSG_MAX_JOINTS := 10;  ! from joint_data.h

PROC ROS_receive_msg_joint_traj_pt(VAR socketdev client_socket, VAR ROS_msg_joint_traj_pt message, \num wait_time)
    VAR ROS_msg raw_message;

    ! Read raw message data
    IF Present(wait_time) THEN
        ROS_receive_msg client_socket, raw_message, \wait_time:=wait_time;
    ELSE
        ROS_receive_msg client_socket, raw_message;
    ENDIF

    ! Integrity Check: Message Type
    IF (raw_message.header.msg_type <> ROS_MSG_TYPE_JOINT_TRAJ_PT) THEN
        ErrWrite \W, "ROS Socket Type Mismatch", "Unexpected message type",
                \RL2:="expected: " + ValToStr(ROS_MSG_TYPE_JOINT_TRAJ_PT),
                \RL3:="received: " + ValToStr(raw_message.header.msg_type);
        RAISE ERR_ARGVALERR;  ! TBD: define specific error code
    ENDIF

    ! Integrity Check: Data Size
    IF (RawBytesLen(raw_message.data) < 52) THEN
        ErrWrite \W, "ROS Socket Missing Data", "Insufficient data for joint_trajectory_pt",
                \RL2:="expected: 52",
                \RL3:="received: " + ValToStr(RawBytesLen(raw_message.data));
        RAISE ERR_OUTOFBND;  ! TBD: define specific error code
    ENDIF

    ! Copy Header data
    message.header := raw_message.header;

    ! Unpack data fields
    UnpackRawBytes raw_message.data, 1, message.sequence_id, \IntX:=DINT;
    UnpackRawBytes raw_message.data, 5, message.joints.rax_1, \Float4;
    UnpackRawBytes raw_message.data, 9, message.joints.rax_2, \Float4;
    UnpackRawBytes raw_message.data, 13, message.joints.rax_3, \Float4;
    UnpackRawBytes raw_message.data, 17, message.joints.rax_4, \Float4;
    UnpackRawBytes raw_message.data, 21, message.joints.rax_5, \Float4;
    UnpackRawBytes raw_message.data, 25, message.joints.rax_6, \Float4;
    ! Skip bytes 29-44.  UNUSED.  Reserved for Joints 7-10.  TBD: copy to extAx?
    UnpackRawBytes raw_message.data, 29+(ROS_MSG_MAX_JOINTS-6)*4, message.velocity, \Float4;
    UnpackRawBytes raw_message.data, 33+(ROS_MSG_MAX_JOINTS-6)*4, message.duration, \Float4;

    ! Convert data from ROS units to ABB units
    message.joints := rad2deg_robjoint(message.joints);
    ! TBD: convert velocity

ERROR
    RAISE;  ! raise errors to calling code
ENDPROC

PROC ROS_send_msg_joint_data(VAR socketdev client_socket, ROS_msg_joint_data message)
    VAR ROS_msg raw_message;
    VAR robjoint ROS_joints;
    VAR num i;

    ! Force message header to the correct values
    raw_message.header.msg_type := ROS_MSG_TYPE_JOINT;
    raw_message.header.comm_type := ROS_COM_TYPE_TOPIC;
    raw_message.header.reply_code := ROS_REPLY_TYPE_INVALID;

    ! Convert data from ABB units to ROS units
    ROS_joints := deg2rad_robjoint(message.joints);

    ! Pack data into message
    PackRawBytes message.sequence_id, raw_message.data,  1, \IntX:=DINT;
    PackRawBytes ROS_joints.rax_1,    raw_message.data,  5, \Float4;
    PackRawBytes ROS_joints.rax_2,    raw_message.data,  9, \Float4;
    PackRawBytes ROS_joints.rax_3,    raw_message.data, 13, \Float4;
    PackRawBytes ROS_joints.rax_4,    raw_message.data, 17, \Float4;
    PackRawBytes ROS_joints.rax_5,    raw_message.data, 21, \Float4;
    PackRawBytes ROS_joints.rax_6,    raw_message.data, 25, \Float4;
    FOR i FROM 1 TO ROS_MSG_MAX_JOINTS-6 DO   
    ! Insert placeholders for joints 7-10 (message expects 10 joints)
        PackRawBytes 0,               raw_message.data, 25+i*4, \Float4;
    ENDFOR

    ROS_send_msg client_socket, raw_message;

ERROR
    RAISE;  ! raise errors to calling code
ENDPROC

LOCAL FUNC num deg2rad(num deg)
    RETURN deg * pi / 180;
ENDFUNC

LOCAL FUNC robjoint deg2rad_robjoint(robjoint deg)
    VAR robjoint rad;
    rad.rax_1 := deg2rad(deg.rax_1);
    rad.rax_2 := deg2rad(deg.rax_2);
    rad.rax_3 := deg2rad(deg.rax_3);
    rad.rax_4 := deg2rad(deg.rax_4);
    rad.rax_5 := deg2rad(deg.rax_5);
    rad.rax_6 := deg2rad(deg.rax_6);

    RETURN rad;
ENDFUNC

LOCAL FUNC num rad2deg(num rad)
    RETURN rad * 180 / pi;
ENDFUNC

LOCAL FUNC robjoint rad2deg_robjoint(robjoint rad)
    VAR robjoint deg;
    deg.rax_1 := rad2deg(rad.rax_1);
    deg.rax_2 := rad2deg(rad.rax_2);
    deg.rax_3 := rad2deg(rad.rax_3);
    deg.rax_4 := rad2deg(rad.rax_4);
    deg.rax_5 := rad2deg(rad.rax_5);
    deg.rax_6 := rad2deg(rad.rax_6);

    RETURN deg;
ENDFUNC

ENDMODULE

接下來分段的理解:

RECORD ROS_msg_header
    num msg_type;
    num comm_type;
    num reply_code;
ENDRECORD

這部分結構體對機器人與控制器交換的信息的開頭端ROS_msg_header進行定義,在其中分別定義了信息的類型msg_type,向機器人傳遞的命令的類型comm_type,以及向ROS進行回覆的代碼reply_code,所有的類型表示與代碼表示都是num數值類型的。

RECORD ROS_msg
    ROS_msg_header header;
    rawbytes data;
ENDRECORD

這部分結構體對機器人控制器與ROS交互的信息整體進行了定義,ROS_msg的信息整體包含信息幀的開頭ROS_msg_header header和其後所接收到的對應指令的數據rawbytes datarawbytes 通過支持指令/函數,可用任意類型數據 num、byte、string來填充rawbytes數據。在rawbytes的任意變量中,系統同時儲存當前有效字節的長度。

RECORD ROS_msg_joint_traj_pt
    ROS_msg_header header;
    num sequence_id;
    robjoint joints;  ! in DEGREES
    num velocity;
    num duration;
ENDRECORD

定義關節空間運行機器人運行路徑的數據結構,包括信息頭,序列ID,關節位置,速度和週期。這個數據結構主要是機器人控制器的內部使用,用於對機器人的運動進行控制,所以需要的信息全面一些。

RECORD ROS_msg_joint_data
    ROS_msg_header header;
    num sequence_id;
    robjoint joints;  ! in DEGREES
ENDRECORD

定義關節位置數據的結構體,包括信息頭,序列ID和關節位置數據,比上一種數據類型少了速度與週期。這種數據格式主要用於向ROS發送機器人的狀態,也就是機器人的當前關節角度,所以需要的數據類型數量相對上一種數據格式要少。

! Message Type Codes (from simple_message/simple_message.h)
CONST num ROS_MSG_TYPE_INVALID       := 0;
CONST num ROS_MSG_TYPE_JOINT         := 10;  ! joint-position feedback
CONST num ROS_MSG_TYPE_JOINT_TRAJ_PT := 11;  ! joint-trajectory-point (for path downloading)
CONST num ROS_COM_TYPE_TOPIC         := 1;
CONST num ROS_COM_TYPE_SRV_REQ       := 2;
CONST num ROS_COM_TYPE_SRV_REPLY     := 3;
CONST num ROS_REPLY_TYPE_INVALID     := 0;
CONST num ROS_REPLY_TYPE_SUCCESS     := 1;
CONST num ROS_REPLY_TYPE_FAILURE     := 2;

這一段定義信息的類型對應於ROS_msg_header中的msg_type,是非常重要的一段代碼,對不同的信息類型進行數值劃分,用於數據的封裝與解析。無效數據都定義爲0,不管是接收的還是回覆的數據;關節位置反饋爲10,用於向ROS反饋當前關節位置;關節軌跡序列數據用11,代表後面還要進行序列的下載與解析;與ROS話題相關的數據用1表示,與ROS服務相關的數據用2表示,對服務的回覆數據用3表示;回覆無效爲0,成功爲1,失敗爲2。

CONST num ROS_TRAJECTORY_START_DOWNLOAD := -1;
CONST num ROS_TRAJECTORY_END := -3;
CONST num ROS_TRAJECTORY_STOP := -4;

特殊的序列代碼,軌跡開始下載到控制器爲-1,軌跡結束爲-3,軌跡停止爲-4。

CONST num ROS_MSG_MAX_JOINTS := 10;

此行代碼爲定義最大的關節軸數爲10。接下來這段函數很重要,關係到ROS的數據是否能夠正確的接收到解析,長的代碼直接把對每一行的理解寫在代碼段裏了。

PROC ROS_receive_msg_joint_traj_pt(VAR socketdev client_socket, VAR ROS_msg_joint_traj_pt message, \num wait_time)
<!---定義關節序列接收函數,參數分別爲客戶端(ROS)設備的套接字,關節空間路徑數據(解析的數據要寫入的變量),可選參數處理時間,最終函數返回的爲ROS_msg_joint_traj_pt類型信息--->

    VAR ROS_msg raw_message;
    <!---定義存儲socket數據的變量,ROS_msg只包含頭與字節數據--->

    IF Present(wait_time) THEN
    <!---Present()函數用來檢查對就的參數變量是否存在,如果存在則執行下一行代碼--->

        ROS_receive_msg client_socket, raw_message, \wait_time:=wait_time;
        <!---通過ROS_receive_msg ()函數將Socket數據從客戶端client_socket寫入到raw_message,並限定執行時間爲wait_time(可選參數)--->
    ELSE
        ROS_receive_msg client_socket, raw_message;
    ENDIF

    IF (raw_message.header.msg_type <> ROS_MSG_TYPE_JOINT_TRAJ_PT) THEN
        ErrWrite \W, "ROS Socket Type Mismatch", "Unexpected message type",
                \RL2:="expected: " + ValToStr(ROS_MSG_TYPE_JOINT_TRAJ_PT),
                \RL3:="received: " + ValToStr(raw_message.header.msg_type);
        RAISE ERR_ARGVALERR;  ! TBD: define specific error code
    ENDIF
     <!---檢查數據類型是否爲下載軌跡數據類型,否則輸出錯誤,並進行錯誤處理--->

    IF (RawBytesLen(raw_message.data) < 52) THEN
        ErrWrite \W, "ROS Socket Missing Data", "Insufficient data for joint_trajectory_pt",
                \RL2:="expected: 52",
                \RL3:="received: " + ValToStr(RawBytesLen(raw_message.data));
        RAISE ERR_OUTOFBND;  ! TBD: define specific error code
    ENDIF
    <!---檢查數據大小是否爲滿足最小的數據大小,否則輸出錯誤,並進行錯誤處理,爲什麼是52呢,其中包括sequence_id,還有10個軸和速度與週期,是13num的數據量,而每個num4個字節--->

    message.header := raw_message.header;
    <!---ROS_msg raw_message變量作爲臨時變量接收ROS傳遞過來的單個關節空間數據,並將解析後的數據賦值給ROS_msg_joint_traj_pt message,此行代碼爲頭信息的賦值,後面分別爲對應的ID,關節,速度與週期的賦值--->

    UnpackRawBytes raw_message.data, 1, message.sequence_id, \IntX:=DINT;
    <!---將raw_message.data的數據的1-4字節數據寫入message.sequence_id,\IntX指待裝入的Value具有num或dnum格式,數據類型爲inttypes,DINT爲有符號4字節整數--->
    UnpackRawBytes raw_message.data, 5, message.joints.rax_1, \Float4;
    UnpackRawBytes raw_message.data, 9, message.joints.rax_2, \Float4;
    UnpackRawBytes raw_message.data, 13, message.joints.rax_3, \Float4;
    UnpackRawBytes raw_message.data, 17, message.joints.rax_4, \Float4;
    UnpackRawBytes raw_message.data, 21, message.joints.rax_5, \Float4;
    UnpackRawBytes raw_message.data, 25, message.joints.rax_6, \Float4;
    <!---跳過了字節29-44,爲軸7-10進行保留--->
    UnpackRawBytes raw_message.data, 29+(ROS_MSG_MAX_JOINTS-6)*4, message.velocity, \Float4;
    UnpackRawBytes raw_message.data, 33+(ROS_MSG_MAX_JOINTS-6)*4, message.duration, \Float4;
    <!---UnpackRawBytes爲解析函數,UnpackRawBytes用於將rawbytes型容器的內容解包至byte、num、dnum或string型變量中--->

    message.joints := rad2deg_robjoint(message.joints);
    <!---將關節數據從弧度轉換到角度,rad2deg_robjoint函數爲自定義的轉換函數--->
    ! TBD: convert velocity

ERROR
    RAISE;  
    <!---錯誤處理--->
ENDPROC

以上是對socket接收到的數據進行解析的功能函數,下面分析數據發送函數。

PROC ROS_send_msg_joint_data(VAR socketdev client_socket, ROS_msg_joint_data message)
<!---數據發送函數,參數爲socket客戶端的有效套接字socketdev ,還有要發送的目標關節數據ROS_msg_joint_data --->
    VAR ROS_msg raw_message;
    <!---定義最終用於發送的數據格式--->
    VAR robjoint ROS_joints;
    <!---定義關節數據格式--->
    VAR num i;

    ! Force message header to the correct values
    raw_message.header.msg_type := ROS_MSG_TYPE_JOINT;
    raw_message.header.comm_type := ROS_COM_TYPE_TOPIC;
    raw_message.header.reply_code := ROS_REPLY_TYPE_INVALID;
    <!---定義要發送數據格式的頭信息--->

    ROS_joints := deg2rad_robjoint(message.joints);
    <!---將要發送的關節數據從角度值轉換到弧度值--->

    ! Pack data into message
    PackRawBytes message.sequence_id,raw_message.data,  1, \IntX:=DINT;
    PackRawBytes ROS_joints.rax_1,   raw_message.data,  5, \Float4;
    PackRawBytes ROS_joints.rax_2,   raw_message.data,  9, \Float4;
    PackRawBytes ROS_joints.rax_3,   raw_message.data, 13, \Float4;
    PackRawBytes ROS_joints.rax_4,   raw_message.data, 17, \Float4;
    PackRawBytes ROS_joints.rax_5,   raw_message.data, 21, \Float4;
    PackRawBytes ROS_joints.rax_6,   raw_message.data, 25, \Float4;
    <!---將要發送的關節數據寫入raw_message的對應位置--->
    FOR i FROM 1 TO ROS_MSG_MAX_JOINTS-6 DO
        PackRawBytes 0,               raw_message.data, 25+i*4, \Float4;
    ENDFOR
    <!---將7-10軸的關節數據(無則用0填充)寫入raw_message的對應位置--->
    ROS_send_msg client_socket, raw_message;
    <!---將填充好的數據發送到ROS客戶端--->
ERROR
    RAISE;
ENDPROC

以上代碼段用於控制器向ROS進行關節位置的反饋上傳。好了,上面兩個大的函數分析完了,下面來幾個簡單的函數壓壓驚:

LOCAL FUNC num deg2rad(num deg)
    RETURN deg * pi / 180;
ENDFUNC

上面定義了一個局部函數,這個說明一上PROC定義一個可調用程序(也可以理解爲函數,但是無返回值),FUNC定義一個函數,有返回值。上面這個函數的作用就是將角度值向弧度值進行轉換。

LOCAL FUNC robjoint deg2rad_robjoint(robjoint deg)
    VAR robjoint rad;
    rad.rax_1 := deg2rad(deg.rax_1);
    rad.rax_2 := deg2rad(deg.rax_2);
    rad.rax_3 := deg2rad(deg.rax_3);
    rad.rax_4 := deg2rad(deg.rax_4);
    rad.rax_5 := deg2rad(deg.rax_5);
    rad.rax_6 := deg2rad(deg.rax_6);

    RETURN rad;
ENDFUNC

上面這一段代碼就是將結構體robjoint 中的關節數據對應的轉換爲弧度值。

LOCAL FUNC num rad2deg(num rad)
    RETURN rad * 180 / pi;
ENDFUNC

這段代碼與上面的轉換函數類似不過是進行弧度到角度的轉換。

LOCAL FUNC robjoint rad2deg_robjoint(robjoint rad)
    VAR robjoint deg;
    deg.rax_1 := rad2deg(rad.rax_1);
    deg.rax_2 := rad2deg(rad.rax_2);
    deg.rax_3 := rad2deg(rad.rax_3);
    deg.rax_4 := rad2deg(rad.rax_4);
    deg.rax_5 := rad2deg(rad.rax_5);
    deg.rax_6 := rad2deg(rad.rax_6);

    RETURN deg;
ENDFUNC

通過上面三個代碼段的理解,這個應該就比較好理解了,就是將對應數據結構中的弧度值轉換爲角度值。
本來想一次性把六類文件都寫完,但是寫到這裏發現已經好長了,如果太長估計我自己以後就不會看了,所以這次先分析兩個程序文件,把後面的四類程序文件分成兩組放在後面的兩篇博客中。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章