Rosserial Arduino Library中從一行代碼開始探究系統原理


Rosserial Arduino Library中從一行代碼開始探究系統原理

俗話說,管中窺豹,可見一斑。
從一行代碼開始,分析rosserial arduino庫的脈絡。走碼觀花,到哪兒是哪兒。

編程寫個ros節點,這是不可缺少的一句。

ros::NodeHandle  nh;

其定義在文件ros.h內

  typedef NodeHandle_<ArduinoHardware, 25, 25, 280, 280> NodeHandle;


從這個類型的定義開始提線,看25, 25, 280, 280都分別配置了什麼?

找到文件ros/node_handle.h

/* Node Handle */
template<class Hardware,
         int MAX_SUBSCRIBERS = 25,/* 最大訂閱數 */
         int MAX_PUBLISHERS = 25,/* 最大發布數 */
         int INPUT_SIZE = 512,/* 輸入緩衝大小 */
         int OUTPUT_SIZE = 512>/* 輸出緩衝大小 */


實現:

namespace ros
{

class NodeHandle
{
public:
  virtual int publish(int id, const Msg* msg) = 0;
  virtual int spinOnce() = 0;
  virtual bool connected() = 0;
protected:
  Hardware hardware_;
  uint8_t message_in[INPUT_SIZE];//接收數據緩衝區大小,這個size握手協商階段會上報,上位機數據包字節數不能超過此限制。
  uint8_t message_out[OUTPUT_SIZE];//發送數據緩衝區大小,應估計最長數據來定義size。否則超出buf裝不下,消息將會dropped


//MAX_PUBLISHERS; MAX_SUBSCRIBERS這裏定義的數量爲aduino內創建的puber和suber的最大數量
  Publisher * publishers[MAX_PUBLISHERS];
  Subscriber_ * subscribers[MAX_SUBSCRIBERS];
/*
定義好er們,一般在setup中會執行這樣的語句。

  nh.advertise(puber);
  nh.subscribe(suber);
  nh.advertiseService(servicer_server);
  nh.serviceClient(servicer_client);
*/
//當調用advertise(puber)、subscribe(suber)時,就會被限定不能超出配置的數量。如果記不清有幾個puber和suber,最好看看返回值是否成功。
/* Register a new publisher */
  bool advertise(Publisher & p)
  {
    for (int i = 0; i < MAX_PUBLISHERS; i++)
    {
      if (publishers[i] == 0) // empty slot
      {
        publishers[i] = &p;//------------------add
        p.id_ = i + 100 + MAX_SUBSCRIBERS;//起始100,之前的id保留, + MAX_SUBSCRIBERS 意圖是讓pubid比subid大並且不會重複。
        p.nh_ = this;
        return true;
      }
    }
    return false;
  }

  /* Register a new subscriber */
  template<typename SubscriberT>
  bool subscribe(SubscriberT& s)
  {
    for (int i = 0; i < MAX_SUBSCRIBERS; i++)
    {
      if (subscribers[i] == 0) // empty slot
      {
        subscribers[i] = static_cast<Subscriber_*>(&s);//------------------add
        s.id_ = i + 100;//起始100,之前的id保留,
        return true;
      }
    }
    return false;
  }


可以看到定義的puber和suber的指針已經存儲到對應的“池”內,備用。
puber\suber是用來發布\獲取數據的,值得注意的是serviceserver和service client,他們工作要發佈\獲取數據。

puber和suber、serviceserver,serviceclient示例彙總


ros::NodeHandle  nh;
void messageCb( const std_msgs::Empty& toggle_msg){/*suber獲取利用msg的數據*/}
ros::Subscriber<std_msgs::Empty> sub("toggle_led", messageCb );
std_msgs::String str_msg;
ros::Publisher chatter("chatter", &str_msg);
char hello[13] = "hello world!";
void setup(){  nh.initNode();  nh.advertise(chatter);  nh.subscribe(sub);}//---------------------add suber和puber
void loop(){
  str_msg.data = hello;
  chatter.publish( &str_msg );//---------puber發佈數據
  nh.spinOnce();
  delay(500);
}
ros::NodeHandle  nh;
void callback(const Test::Request & req, Test::Response & res){/*獲取 req數據,填充res數據*/}
ros::ServiceServer<Test::Request, Test::Response> server("test_srv",&callback);
void setup(){  nh.initNode();  nh.advertiseService(server);}//-----------add servicer 
void loop(){
  nh.spinOnce();
}
ros::NodeHandle  nh;
ros::ServiceClient<Test::Request, Test::Response> client("test_srv");
void setup(){  nh.initNode();  nh.serviceClient(client);//-----------------add servicer 
  while(!nh.connected()) nh.spinOnce();
  nh.loginfo("Startup complete");
}
void loop(){
  Test::Request req;  Test::Response res;
  req.input = hello;//---------------填充請求數據
  client.call(req, res);
  //zzzzzz = res.output;//-----------利用獲取的響應數據
  nh.spinOnce();
}


Publisher、Subscriber源碼比較簡單,略過。分析ServiceServer、ServiceClient。
他們繼承了suber,包含了一個puber協同工作,
看ServiceServer 代碼

template<typename MReq , typename MRes, typename ObjT = void>
class ServiceServer : public Subscriber_
{
public:
  typedef void(ObjT::*CallbackT)(const MReq&,  MRes&);

  ServiceServer(const char* topic_name, CallbackT cb, ObjT* obj) :
    pub(topic_name, &resp, rosserial_msgs::TopicInfo::ID_SERVICE_SERVER + rosserial_msgs::TopicInfo::ID_PUBLISHER),//注意類型值疊加
    obj_(obj)
  {
    this->topic_ = topic_name;
    this->cb_ = cb;
  }

  // these refer to the subscriber
  virtual void callback(unsigned char *data)
  {
    req.deserialize(data);
    (obj_->*cb_)(req, resp);//--------------處理獲取到的數據,填充響應數據
    pub.publish(&resp);//------------------發佈響應數據
  }
//...
  virtual int getEndpointType()
  {
    return rosserial_msgs::TopicInfo::ID_SERVICE_SERVER + rosserial_msgs::TopicInfo::ID_SUBSCRIBER;//注意類型值疊加
  }
//...
  Publisher pub;
//...

看ServiceClient 代碼

template<typename MReq , typename MRes>
class ServiceClient : public Subscriber_
{
public:
  ServiceClient(const char* topic_name) :
    pub(topic_name, &req, rosserial_msgs::TopicInfo::ID_SERVICE_CLIENT + rosserial_msgs::TopicInfo::ID_PUBLISHER)//注意類型值疊加
  {
    this->topic_ = topic_name;
    this->waiting = true;
  }

  virtual void call(const MReq & request, MRes & response)
  {
    if (!pub.nh_->connected()) return;
    ret = &response;
    waiting = true;
    pub.publish(&request);//----------------------------發佈請求數據
    while (waiting && pub.nh_->connected())//----------------->>>等待響應,單片機spinOnce()卡住
      if (pub.nh_->spinOnce() < 0) break;//直到spinOnce接收數據成功,回調callback,完成整個流程;或者響應異常,請求失敗,注意失敗無返回數據。
  }
  // these refer to the subscriber
  virtual void callback(unsigned char *data)
  {
    ret->deserialize(data);
    waiting = false;//----------------------------------------------<<<正常響應完成
  }
//....
  virtual int getEndpointType()
  {
    return rosserial_msgs::TopicInfo::ID_SERVICE_CLIENT + rosserial_msgs::TopicInfo::ID_SUBSCRIBER;//注意類型值疊加
  }
//....
  Publisher pub;
//....

servicer中實例化的puber、suber。
它們也是存儲在“池”內的,定義所以定義MAX_PUBLISHERS  MAX_SUBSCRIBERS要考慮servicer 的量


  /* Register a new Service Server */
  template<typename MReq, typename MRes, typename ObjT>
  bool advertiseService(ServiceServer<MReq, MRes, ObjT>& srv)
  {
    bool v = advertise(srv.pub);//-------------------------------------------ServiceServer  添加srv的puber
    for (int i = 0; i < MAX_SUBSCRIBERS; i++)
    {
      if (subscribers[i] == 0) // empty slot
      {
        subscribers[i] = static_cast<Subscriber_*>(&srv);//-----------------ServiceServer  添加srv的suber
        srv.id_ = i + 100;
        return v;
      }
    }
    return false;
  }

  /* Register a new Service Client */
  template<typename MReq, typename MRes>
  bool serviceClient(ServiceClient<MReq, MRes>& srv)
  {
    bool v = advertise(srv.pub);//-------------------------------------------ServiceClient 添加srv的puber
    for (int i = 0; i < MAX_SUBSCRIBERS; i++)
    {
      if (subscribers[i] == 0) // empty slot
      {
        subscribers[i] = static_cast<Subscriber_*>(&srv);//-----------------ServiceClient 添加srv的suber
        srv.id_ = i + 100;
        return v;
      }
    }
    return false;
  }


//當配置好了puber、suber、servicer,如何運作?消息怎麼傳出去,又怎麼傳進來?
如果沒有spinOnce()驅動,他們中獲取數據的er們將會一直遊手好閒,無所事事。
spinOnce()是放在loop循環中不斷重複執行的。
spinOnce()內主要是接收數據解包的工作,處理好接收到的數據往message_in  裏填充,然後驅動對應er上工的邏輯。

首先spinOnce會等上位機發數據,來檢測上位機的ros程序已經正常啓動。
就像種子等待發芽的信號,春天來了,接收到暖陽和雨露的信號,就開始萌發。

觸發設備第一個數據包是這樣的

0xff 0xfe 0x00 0x00 0xff 0x00 0x00 0xff


spinOnce()內,接收數據逐個字節進行,解包並驗證。
其中的mode_變量控制解析過程的不同階段。
默認值爲MODE_FIRST_FF,
先檢查第一字節是否 0xff
然後 mode_++
看版本是否是0xfe
再 mode_++
...

根據下列數據看到解析過程經過8個階段

const uint8_t SYNC_SECONDS  = 5;
const uint8_t MODE_FIRST_FF = 0;
const uint8_t MODE_PROTOCOL_VER   = 1;
const uint8_t PROTOCOL_VER1       = 0xff; // through groovy
const uint8_t PROTOCOL_VER2       = 0xfe; // in hydro
const uint8_t PROTOCOL_VER        = PROTOCOL_VER2;
const uint8_t MODE_SIZE_L         = 2;
const uint8_t MODE_SIZE_H         = 3;
const uint8_t MODE_SIZE_CHECKSUM  = 4;    // checksum for msg size received from size L and H
const uint8_t MODE_TOPIC_L        = 5;    // waiting for topic id
const uint8_t MODE_TOPIC_H        = 6;
const uint8_t MODE_MESSAGE        = 7;
const uint8_t MODE_MSG_CHECKSUM   = 8;    // checksum for msg and topic id

這裏不一一展開,有興趣可以對照源碼參考前文進行測試。
前文
ROS中rosserial通訊協議初探
https://blog.csdn.net/qq_38288618/article/details/102931684

只說最後一步 

      else if (mode_ == MODE_MSG_CHECKSUM)    /* do checksum */
      {
        mode_ = MODE_FIRST_FF;
        if ((checksum_ % 256) == 255)
        {
          if (topic_ == TopicInfo::ID_PUBLISHER)
          {
//收到上位機第一個topicid==0的包,執行發送同步請求requestSyncTime,和協商話題。negotiateTopics,
            requestSyncTime();
            negotiateTopics();
            last_sync_time = c_time;
            last_sync_receive_time = c_time;
            return SPIN_ERR;
          }
          else if (topic_ == TopicInfo::ID_TIME)
          {
            syncTime(message_in);//--------------------------------------這裏用到數據buf
          }
          else if (topic_ == TopicInfo::ID_PARAMETER_REQUEST)
          {
            req_param_resp.deserialize(message_in);//--------------------------------------這裏用到數據buf
            param_recieved = true;
          }
          else if (topic_ == TopicInfo::ID_TX_STOP)
          {
            configured_ = false;
          }
          else
          {
//這裏訂閱的suber執行回調
            if (subscribers[topic_ - 100])//因爲之前add的時候subid=下標+100,這裏用topic_ 來找是哪個suber的數據
              subscribers[topic_ - 100]->callback(message_in);//--------------------------------------這裏用到數據buf
//如果是serviceserver回調處理完數據,再publish()發送數據
          }
        }
      }


這部分是接收完整包之後的邏輯
根據topic_ 不同類別分了5個分支


握手階段


分支1、同步時間與協商話題,

            requestSyncTime();
            negotiateTopics();


把池裏的suber和puber情況發送給上位機,上位機serial_node.py 的node根據情況進行配置,一邊與ros溝通,一邊與下位機溝通。


用到message_in數據的有3個分支


分支2、suber、servicer(server+client)這些suber的callback、
分支3、時間同步syncTime
分支4、參數獲取req_param_resp

還有一個分支是配置開關


分支5、configured_ = false;

message_in是裝載發過來的數據的buf,沒見有檢查溢出的代碼句子,因爲在握手階段就把自己的buf大小告訴了上位機。

上位機serial_node.py 應檢查要發送數據的大小,發來的數據不會超限。

分析握手階段,協商話題發送的數據

  void negotiateTopics()
  {
    rosserial_msgs::TopicInfo ti;
    int i;
    for (i = 0; i < MAX_PUBLISHERS; i++)
    {
      if (publishers[i] != 0) // non-empty slot
      {
        ti.topic_id = publishers[i]->id_;
        ti.topic_name = (char *) publishers[i]->topic_;
        ti.message_type = (char *) publishers[i]->msg_->getType();
        ti.md5sum = (char *) publishers[i]->msg_->getMD5();
        ti.buffer_size = OUTPUT_SIZE;
        publish(publishers[i]->getEndpointType(), &ti);//組織好每個puber的信息發送
      }
    }
    for (i = 0; i < MAX_SUBSCRIBERS; i++)
    {
      if (subscribers[i] != 0) // non-empty slot
      {
        ti.topic_id = subscribers[i]->id_;
        ti.topic_name = (char *) subscribers[i]->topic_;
        ti.message_type = (char *) subscribers[i]->getMsgType();
        ti.md5sum = (char *) subscribers[i]->getMsgMD5();
        ti.buffer_size = INPUT_SIZE;
        publish(subscribers[i]->getEndpointType(), &ti);//組織好每個suber的信息發送
      }
    }
    configured_ = true;
  }

 看看通用的打包方法

  virtual int publish(int id, const Msg * msg)
  {
    if (id >= 100 && !configured_)
      return 0;

    /* serialize message */
    int l = msg->serialize(message_out + 7);

    /* setup the header */
    message_out[0] = 0xff;
    message_out[1] = PROTOCOL_VER;
    message_out[2] = (uint8_t)((uint16_t)l & 255);
    message_out[3] = (uint8_t)((uint16_t)l >> 8);
    message_out[4] = 255 - ((message_out[2] + message_out[3]) % 256);
    message_out[5] = (uint8_t)((int16_t)id & 255);
    message_out[6] = (uint8_t)((int16_t)id >> 8);

    /* calculate checksum */
    int chk = 0;
    for (int i = 5; i < l + 7; i++)
      chk += message_out[i];
    l += 7;
    message_out[l++] = 255 - (chk % 256);
//判斷是否消息是不是超出message_out範圍了。我覺得應該靠前判斷,算了半天,結果一看超限了做了無用功。
    if (l <= OUTPUT_SIZE)
    {
      hardware_.write(message_out, l);
//串口發送
      return l;
    }
    else
    {
      //logerror("Message from device dropped: message larger than buffer.");
      logerror("MsgEr:OutBuf");
//log的字符串太長,多佔內存?
      return -1;
    }
  }


//上面的topic類型是 rosserial_msgs::TopicInfo,通訊協議依賴的消息類型。
可以找到rosserial_msgs/TopicInfo .h看一眼

namespace rosserial_msgs
{

  class TopicInfo : public ros::Msg
  {
    public:
      typedef uint16_t _topic_id_type;
      _topic_id_type topic_id;
      typedef const char* _topic_name_type;
      _topic_name_type topic_name;
      typedef const char* _message_type_type;
      _message_type_type message_type;
      typedef const char* _md5sum_type;
      _md5sum_type md5sum;
      typedef int32_t _buffer_size_type;
      _buffer_size_type buffer_size;
//這裏數值就是協議規定的分類
      enum { ID_PUBLISHER = 0 };
      enum { ID_SUBSCRIBER = 1 };
      enum { ID_SERVICE_SERVER = 2 }; 
      enum { ID_SERVICE_CLIENT = 4 };
      enum { ID_PARAMETER_REQUEST = 6 };
      enum { ID_LOG = 7 };
      enum { ID_TIME = 10 };
      enum { ID_TX_STOP = 11 };

    TopicInfo():
      topic_id(0),
      topic_name(""),
      message_type(""),
      md5sum(""),
      buffer_size(0)
    {
    }

    virtual int serialize(unsigned char *outbuffer) const
//。。。
    virtual int deserialize(unsigned char *inbuffer)
//。。。
    const char * getType(){ return "rosserial_msgs/TopicInfo"; };
    const char * getMD5(){ return "0ad51f88fc44892f8c10684077646005"; };

  };

}


跟其他消息沒什麼兩樣。類型和md5太佔內存。包名和msg名每一個字符多佔1byte的內存


    const char * getType(){ return "rosserial_msgs/TopicInfo"; };//24個字節
    const char * getMD5(){ return "0ad51f88fc44892f8c10684077646005"; };//32個字節

這個消息類24+32=56個字節就這樣被佔了。
md5要驗證沒辦法,包名和類型名真得簡化簡化

整理個簡圖,有助於看清整個系統運作的脈絡。

Rosserial Arduino 庫運行原理圖
再回頭看


  typedef NodeHandle_<ArduinoHardware, 25, 25, 280, 280> NodeHandle;

template<class Hardware,
         int MAX_SUBSCRIBERS = 25,//最大訂閱數
         int MAX_PUBLISHERS = 25,//最大發布數
         int INPUT_SIZE = 512,//輸入緩衝大小
         int OUTPUT_SIZE = 512>//輸出緩衝大小
如何更改數值大小。

分析得出每多1個servicer不管是server還是client,puber和suber數量各加1


MAX_PUBLISHERS =pubers+servicers
MAX_SUBSCRIBERS =subers+servicers

可以多加幾個量,指針數組不怎麼影響內存。

INPUT_SIZE 按上位機最大數據量估算
OUTPUT_SIZE 按單片機要發送的最大數據量估算。
上個前文的包

FF FE 48 00 B7 -------------------0x0048=72 包數據長度 0xb7=0xff-((0x48+0x00)%0x100)
00 00 -----------------------------0x0000= Topic ID(ID_PUBLISHER=0)
7D 00 -----------------------------0x7D=125 topic_id (publisher ID)
07 00 00 00 ----------------------0x0007=7,數據長度 topic_name="chatter"
63 68 61 74 74 65 72 
0F 00 00 00 ----------------------0x000f=15,數據長度 message_type ="std_msgs/String"
73 74 64 5F 6D 73 67 73 2F 53 
74 72 69 6E 67 
20 00 00 00 ----------------------0x0020=32,數據長度 md5sum="992ce8a1687cec8c8bd883ec73ca41d1"
39 39 32 63 65 38 61 31 36 38 
37 63 65 63 38 63 38 62 64 38 
38 33 65 63 37 33 63 61 34 31 
64 31 
18 01 00 00 ----------------------0x0118=280,是atmage328p的buffer_size  (看2.1)
0C --------------------------------0x0C 包校驗

這樣觀察更直接一些。
通信的這個數據72個字節,再加頭7個,尾部校驗1個,總共80個字節。

數據的大小與名字長短也有關係,
這些類型名字真得斟酌斟酌,畢竟單片機資源捉襟見肘。

比如有自定義包名zzz_protocol,消息名screen_1602 
zzz_protocol/screen_1602     24bytes
簡化
zp/s1602      8bytes
這樣省了16個字節

先告一段落。。。


綱舉目張。目太多,不知怎麼切入。有點碎。。。。
錯誤之處請多多指正!


 

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