第三章 ISO 15118-3 SLAC协议原理与实现--3.6 软件实现方案--EvseSlac

3.6 软件实现方案--EvseSlac

在开源项目Everest中包含一个使用c++编写的EvseSlac模块,是在Evse端运行的slac程序,用来与EV端建立slac连接。作者详细分析了EvseSlac模块,认为该模块满足ISO15118-3协议规定的命令流程和时序,具备完备的状态机制,也具备和ISO15118-2层软件通信机制,是一份相当完整可靠的slac软件。
项目下载:  https://github.com/EVerest/
在本书后期将会用单独章节介绍对Everest项目的启动、结构、功能、流程分析,在此先对EvseSlac模块代码进行分析说明,方便读者与pyslac、XXX_SDK软件进行对比,触类旁通,增强对SLAC的理解和认识。
EvseSlac模块编译后是一个可执行程序,但是并不能单独运行,需要通过Everest项目启动,因此读者暂时不用纠结如何启动EvseSlac模块,先跟随作者学习该模块。
本文内容很长,大致会分成几个部分:
(1) 启动EvseSlac模块,实现slac匹配
(2) 模块中的类说明
(3) 状态机
(4) 外部接口
(5) 设计框架

3.6.1 启动EvseSlac模块

Everest模块默认配置文件中使用的是js编写的slac模块JsSlacSimulator,包含有EV、EVSE实例。但是EvseSlac模块的实例只有main(EVSE端),没有EV端实例,因此测试的两块板子分别运行EvseSlac、XXX_SDK才能发起连接。
在使用EvseSlac模块的板子上,对原始的config-sil.yaml改动如下。 核心是在yaml中关闭了ev_manager模块(原因是使用ev),用EvseSlac模块替换掉了 JsSlacSimulator。 
settings:
  telemetry_enabled: true
active_modules:
  api:
    connections:
      evse_manager:
        - implementation_id: evse
          module_id: connector_1
    module: API
  auth:
    config_module:
      connection_timeout: 10
      prioritize_authorization_over_stopping_transaction: true
      selection_algorithm: FindFirst
      ignore_connector_faults: true
    connections:
      evse_manager:
        - implementation_id: evse
          module_id: connector_1
      token_provider:
        - implementation_id: main
          module_id: token_provider
      token_validator:
        - implementation_id: main
          module_id: token_validator
    module: Auth
  # ev_manager:
  #   config_module:
  #     auto_enable: true
  #     auto_exec: false
  #     auto_exec_commands: sleep 1;iec_wait_pwr_ready;sleep 1;draw_power_regulated 16,3;sleep 30;unplug
  #     connector_id: 1
  #   connections:
  #     ev:
  #       - implementation_id: ev
  #         module_id: iso15118_car
  #     ev_board_support:
  #       - implementation_id: ev_board_support
  #         module_id: connector_1_powerpath
  #     slac:
  #       - implementation_id: ev
  #         module_id: slac
  #   module: JsEvManager
  energy_manager:
    connections:
      energy_trunk:
        - implementation_id: energy_grid
          module_id: grid_connection_point
    module: EnergyManager
  connector_1:
    config_module:
      ac_enforce_hlc: false
      ac_hlc_enabled: true
      ac_hlc_use_5percent: false
      ac_nominal_voltage: 230
      charge_mode: AC
      connector_id: 1
      country_code: DE
      ev_receipt_required: false
      evse_id: DE*PNX*E12345*1
      has_ventilation: true
      max_current_import_A: 32
      max_current_export_A: 32
      payment_enable_contract: true
      payment_enable_eim: true
      session_logging: true
      session_logging_path: /tmp/everest-logs
      session_logging_xml: false
      three_phases: true
    connections:
      bsp:
        - implementation_id: board_support
          module_id: connector_1_powerpath
      hlc:
        - implementation_id: charger
          module_id: iso15118_charger
      powermeter_grid_side:
        - implementation_id: powermeter
          module_id: connector_1_powerpath
      slac:
        # - implementation_id: evse
        - implementation_id: main
          module_id: slac
      ac_rcd:
        - implementation_id: rcd
          module_id: connector_1_powerpath
      connector_lock:
        - implementation_id: connector_lock
          module_id: connector_1_powerpath
    module: EvseManager
    telemetry:
      id: 1
  grid_connection_point:
    config_module:
      fuse_limit_A: 40
      phase_count: 3
    connections:
      energy_consumer:
        - implementation_id: energy_grid
          module_id: connector_1
    module: EnergyNode
  iso15118_car:
    config_module:
      device: auto
      supported_ISO15118_2: true
    connections: {}
    module: PyEvJosev
  iso15118_charger:
    config_module:
      device: auto
      tls_security: allow
    connections: {}
    module: EvseV2G
    connections:
      security:
        - module_id: evse_security
          implementation_id: main
  evse_security:
    module: EvseSecurity
    config_module:
      private_key_password: "123456"
  persistent_store:
    config_module:
      sqlite_db_file_path: everest_persistent_store.db
    connections: {}
    module: PersistentStore
  setup:
    config_module:
      initialized_by_default: true
      localization: true
      online_check_host: lfenergy.org
      setup_simulation: true
      setup_wifi: false
    connections:
      store:
        - implementation_id: main
          module_id: persistent_store
    module: Setup
  # slac:
  #   config_implementation:
  #     ev:
  #       ev_id: PIONIX_SAYS_HELLO
  #     evse:
  #       evse_id: PIONIX_SAYS_HELLO
  #       nid: pionix!
  #       number_of_sounds: 10
  #   connections: {}
  #   module: JsSlacSimulator
  slac:
    config_implementation:
      main:
        device: ens33
        evse_id: PIONIX_SAYS_HELLO
        nid: pionix!
        number_of_sounds: 10
    connections: {}
    module: EvseSlac  
  token_provider:
    config_implementation:
      main:
        timeout: 10
        token: DEADBEEF
    connections:
      evse:
        - implementation_id: evse
          module_id: connector_1
    module: DummyTokenProvider
  token_validator:
    config_implementation:
      main:
        sleep: 0.25
        validation_reason: Token seems valid
        validation_result: Accepted
    connections: {}
    module: DummyTokenValidator
  connector_1_powerpath:
    config_module:
      connector_id: 1
    connections: {}
    module: JsYetiSimulator
    telemetry:
      id: 1
'x-module-layout':
  api:
    position:
      x: 33
      y: 13
    terminals:
      bottom: []
      left:
        - id: evse_manager
          interface: evse_manager
          type: requirement
      right:
        - id: main
          interface: empty
          type: provide
      top: []
.......

Everest项目要求root权限才能运行,直接切换到root用户下,安装过此项目的依赖库:

root@tom-virtual-machine:~/checkout/everest-workspace/Josev# python3 -m pip install -r requirements.txt

终于运行成功了: 结果略。

出现上面黄色标记内容“Module slac initialized”表示已经初始化了EvseSlac模块。接下来一旦everest系统的cp状态切换A->B, 就会启动slac监听,直到车端发起slac连接,然后完成连接。

下面是在两块XXX板子上分别运行everest+slac的连接信息, 连接成功啦。
 结果略。

3.6.2  启动过程

main()  everest-core/build/generated/modules/EvseSlac/ld-ev.cpp
    创建Everest::ModuleLoader对象module_loader,
    执行module_loader.initialize()          everest-framework/lib/runtime.cpp
          this->callbacks.everest_register()  创建出EvseSlac对象 everest-core/build/generated/modules/EvseSlac/ld-ev.cpp   
         this->callbacks.init()     
              slacImpl::init()       everest-core/modules/EvseSlac/main/slacImpl.cpp     实现类的真正入口

3.6.2.1 slacImpl::init()

    此处先检查config.evse_id、 config.nid的长度是否合规, 分别是17和7字节。
    在配置文件/home/vboxuser/checkout/everest-workspace/everest-core/config/config-sil.yaml, 这两个id内容如下:
slac:
    config_implementation:
      main:
        device: ens33
        evse_id: PIONIX_SAYS_HELLO
        nid: pionix!
        number_of_sounds: 10
    connections: {}
    module: EvseSlac  

      注意这个device 指明使用的网卡。下面的线程会打开网卡进行slac通信,实际在板上应该是vc_ethspi0虚拟网卡。 

 随后启动分离线程 :
std::thread(&slacImpl::run, this).detach();
在线程入口,首先等待模块本身的ready信号(使用了std::promise对象)。当EvseSlac启动完成了就会调用slacImpl::ready(), 线程才能继续运行下去。

3.6.2.2 slacImpl::run()slac线程主体

线程主体做的事情: 
  •     创建SlacIO对象,
  •     初始化:打开网卡,建立通道
  •     创建回调函数
  •     启动SlacIO对象的run():  开启内部的poll模型,持续的接收HomePlug消息包并执行回调函数。
  •     启动fsm_ctrl的run(), 这也是线程。
slac_io.run([](slac::messages::HomeplugMessage& msg) { fsm_ctrl->signal_new_slac_message(msg); });

这行代码仔细看有些奇怪--- 匿名函数中使用了外部对象fsm_ctrl的函数, 而且这个函数被底层回调执行, 也就是说这个fsm_ctrl的函数被调用到了。 

3.6.3  PacketSocket类

/home/vboxuser/checkout/everest-workspace/libslac/src/packet_socket.cpp
/home/vboxuser/checkout/everest-workspace/libslac/src/packet_socket.hpp
这个类是对socket的包装, 实现了带超时时间的read和write数据, 读写的是最原始的字节数组。 
特殊之处: 创建的socket使用了原始套接字编程,直接面向链路层通信。因为是原始套接字,所以运行时要使用root权限才可以。
PacketSocket::PacketSocket(const InterfaceInfo& if_info, int protocol) {
    // FIXME (aw): do we need to use O_NONBLOCKING?
    socket_fd = socket(AF_PACKET, SOCK_RAW | SOCK_NONBLOCK, htons(protocol));

    if (socket_fd == -1) {
        error = "Couldn't create the socket: ";
        error += strerror(errno);
        return;
    }

    // bind this packet socket to a specific interface
    struct sockaddr_ll sock_addr = {
        AF_PACKET,                                       // sll_family
        htons(protocol),                                 // sll_protocol
        if_info.get_index(),                             // sll_ifindex
        0x00,                                            // sll_hatype, set on receiving
        0x00,                                            // sll_pkttype, set on receiving
        ETH_ALEN,                                        // sll_halen
        {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00} // sll_addr[8]
    };

    if (-1 == bind(socket_fd, (struct sockaddr*)&sock_addr, sizeof(sock_addr))) {
        error = "Failed to bind the socket: ";
        error += strerror(errno);
        close(socket_fd);
    }

    // everything should have worked out
    valid = true;
}

【原始套接字】  

一般我们编写的网络通信程序都是用TCP/UDP协议,创建socket时使用的参数是AF_INET+SOCK_STREAM/SOCK_DGRAM,实现TCP流和UDP报文通信。这种通信方式下,应用发送的数据需要依次经过传输层、IP层、链路层、物理层。示意图如下:
但是要想使用特殊格式的报文,就不能遵守TCP/UDP传输协议了,创建socket时使用的参数是AF_INET+SOCK_ROW,可以直接访问IP层。
再特殊一点, 创建socket时使用的参数是AF_PACKET+SOCK_ROW,可以直接访问链路层, 连IP层都绕过去了。
我们的SLAC连接过程就是这样特殊的通信方式, 必须要用原始套接字编程,按照HomePlug协议定义数据报文,直接传输到链路层。
【read、write】
PacketSocket类提供的read、write方法具备进行超时等待机制, 这是一般的read和write所不具备的,怎么做到的呢?
原来是使用了poll模型监听socket,设置timeout时间。
  • 超时返回失败,
  • 有POLLIN事件,就read数据;
  • 有POLLOUT事件,就write数据。
代码示例:
PacketSocket::IOResult PacketSocket::read(uint8_t* buffer, int timeout) {
    struct pollfd poll_fd = {
        socket_fd, // file descriptor
        POLLIN,    // requested event
        0          // returned event
    };
    int ret = poll(&poll_fd, 1, timeout);
    if (-1 == ret) {
        error = std::string("poll() failed with: ") + strerror(errno);
        return IOResult::Failure;
    }

    if (0 == ret) {
        return IOResult::Timeout;
    }

    if ((poll_fd.revents & POLLIN) == 0) {
        error = "poll() set other flag than POLLIN";
        return IOResult::Failure;
    }

    bytes_read = ::read(socket_fd, buffer, MIN_BUFFER_SIZE);
    if (bytes_read == -1) {
        error = std::string("read() failed with: ") + strerror(errno);
        return IOResult::Failure;
    }

    return IOResult::Ok;
}

3.6.4 Channel类

Channel类是对PacketSocket的包装,实现了读写结构化的数据和错误处理----就是HomeplugMessage类型结构, 这个类数据定义是1514字节。
/home/vboxuser/checkout/everest-workspace/libslac/src/channel.cpp
/home/vboxuser/checkout/everest-workspace/libslac/include/slac/channel.hpp
bool Channel::read(slac::messages::HomeplugMessage& msg, int timeout) {
    did_timeout = false;
    using IOResult = ::utils::PacketSocket::IOResult;
    if (socket) {
        switch (socket->read(reinterpret_cast<uint8_t*>(msg.get_raw_message_ptr()), timeout)) {
        // FIXME (aw): this enum conversion looks ugly
        case IOResult::Failure:
            error = socket->get_error();
            return false;
        case IOResult::Timeout:
            did_timeout = true;
            return false;
        case IOResult::Ok:
            return true;
        }
    }

    error = "No IO socket available\n";
    return false;
}

HomeplugMessage定义文件: /home/vboxuser/checkout/everest-workspace/libslac/include/slac/slac.hpp

homeplug_message结构体大小是1514字节。
typedef struct {
    struct ether_header ethernet_header;
    struct {
        uint8_t mmv;     // management message version
        uint16_t mmtype; // management message type

    } __attribute__((packed)) homeplug_header;

    // the rest of this message is potentially payload data
    uint8_t payload[ETH_FRAME_LEN - ETH_HLEN - sizeof(homeplug_header)];
} __attribute__((packed)) homeplug_message;

typedef struct {
    uint8_t fmni; // fragmentation management number information
    uint8_t fmsn; // fragmentation message sequence number
} __attribute__((packed)) homeplug_fragmentation_part;


class HomeplugMessage {
public:
    homeplug_message* get_raw_message_ptr() {
        return &raw_msg;
    };

    int get_raw_msg_len() const {
        return raw_msg_len;
    }

    void setup_payload(void const* payload, int len, uint16_t mmtype, const defs::MMV mmv);
    void setup_ethernet_header(const uint8_t dst_mac_addr[ETH_ALEN], const uint8_t src_mac_addr[ETH_ALEN] = nullptr);

    uint16_t get_mmtype() const;
    uint8_t* get_src_mac();

    template <typename T> const T& get_payload() {
        if (raw_msg.homeplug_header.mmv == static_cast<std::underlying_type_t<defs::MMV>>(defs::MMV::AV_1_0)) {
            return *reinterpret_cast<T*>(raw_msg.payload);
        }

        // if not av 1.0 message, we need to shift by the fragmentation part
        return *reinterpret_cast<T*>(raw_msg.payload + sizeof(homeplug_fragmentation_part));
    }

    bool is_valid() const;
    bool keep_source_mac() const {
        return keep_src_mac;
    }

private:
    homeplug_message raw_msg;

    int raw_msg_len{-1};
    bool keep_src_mac{false};
};

3.6.5  SlacIO类

SlacIO类是对Channel类的封装,实现了在线程中循环进行读取Channel数据,并调用回调函数进行处理。
/home/vboxuser/checkout/everest-workspace/everest-core/lib/staging/slac/io/src/io.cpp
/home/vboxuser/checkout/everest-workspace/everest-core/lib/staging/slac/io/include/slac/io.hpp
/home/vboxuser/checkout/everest-workspace/libslac/tools/evse/slac_io.cpp
/home/vboxuser/checkout/everest-workspace/libslac/tools/evse/slac_io.hpp
【疑问】上面有两组文件,内容都是一样的,到底EvseSlac模块使用的是那一组文件呢?
    根据slacImpl.cpp文件 #include <slac/io.hpp>, 确定使用的上面第一组文件。
void SlacIO::init(const std::string& if_name) {
    if (!slac_channel.open(if_name)) {
        throw std::runtime_error(slac_channel.get_error());
    }
}

void SlacIO::run(std::function<InputHandlerFnType> callback) {
    input_handler = callback;
    running = true;
    loop_thread = std::thread(&SlacIO::loop, this);
}

void SlacIO::quit() {
    if (!running) {
        return;
    }
    running = false;
    loop_thread.join();
}

void SlacIO::loop() {
    while (running) {
        if (slac_channel.read(incoming_msg, 10)) {
            input_handler(incoming_msg);
        }
    }
}

void SlacIO::send(slac::messages::HomeplugMessage& msg) {
    // FIXME (aw): handle errors
    slac_channel.write(msg, 1);
}

从SlacIO实现代码来看, 应用层应该调用顺序:

  • SlacIO.init(),   打开网卡,建立通道。
  • SlacIO.run(),  设置回调函数,启动命令接收线程,持续的进行poll轮转,读取报文并执行回调函数。
  • 当退出时执行SlacIO::quit(),会停止线程并等待线程结束。  【警告】这一步没找到调用代码。

3.6.6 关于Slac的回调函数类

仔细分析相关类: Context、ContextCallbacks、EvseSlacConfig, FSM
/home/vboxuser/checkout/everest-workspace/everest-core/lib/staging/slac/fsm/evse/include/slac/fsm/evse/context.hpp
/home/vboxuser/checkout/everest-workspace/libfsm/include/fsm/fsm.hpp

3.6.6.1 template <> 模版特殊化 

在context.hpp一开始就遇到了不熟悉的C++语言点: 模版特化,一般模版用于函数模版、类模版,这里偏偏使用到struct上!! 
template <typename SlacMessageType> struct MMTYPE;
template <> struct MMTYPE<slac::messages::cm_slac_parm_cnf> {
    static const uint16_t value = slac::defs::MMTYPE_CM_SLAC_PARAM | slac::defs::MMTYPE_MODE_CNF;
};

对于模版实例化时候需要指定具体类型,如果模版实例类只能对某一种类型有效或者部分代码要求指定具体类型,就称为模版完全特化或模版部分特化。

模版特化语法:
  • 关键字tempalte后面接一对空的尖括号(< >)
  • 函数名后接一对尖括号,尖括号中指定这个特化定义的模板形参, 然后是函数形参表、函数体
  • 类名、结构体名后接一对尖括号,尖括号中指定这个特化定义的模板形参

3.6.6.2 ContextCallbacks

/home/vboxuser/checkout/everest-workspace/everest-core/lib/staging/slac/fsm/evse/include/slac/fsm/evse/context.hpp
结构体定义了一些回调函数:
struct ContextCallbacks {
    std::function<void(slac::messages::HomeplugMessage&)> send_raw_slac{nullptr};
    std::function<void(const std::string&)> signal_state{nullptr};
    std::function<void(bool)> signal_dlink_ready{nullptr};
    std::function<void()> signal_error_routine_request{nullptr};
    std::function<void(const std::string&)> signal_ev_mac_address_parm_req{nullptr};
    std::function<void(const std::string&)> signal_ev_mac_address_match_cnf{nullptr};
    std::function<void(const std::string&)> log{nullptr};
};
实际上,填写的回调函数是这样的:
    // setup callbacks
    slac::fsm::evse::ContextCallbacks callbacks;
    callbacks.send_raw_slac = [&slac_io](slac::messages::HomeplugMessage& msg) { slac_io.send(msg); };      发送命令
    callbacks.signal_dlink_ready = [this](bool value) { publish_dlink_ready(value); };                      广播dlink_ready
    callbacks.signal_state = [this](const std::string& value) { publish_state(value); };                    广播state, 什么状态?
    callbacks.signal_error_routine_request = [this]() { publish_request_error_routine(nullptr); };          广播错误 
    callbacks.log = [](const std::string& text) { EVLOG_info << text; };                                    写log
    if (config.publish_mac_on_first_parm_req) {
        callbacks.signal_ev_mac_address_parm_req = [this](const std::string& mac) { publish_ev_mac_address(mac); };    收到第一个请求时广播ev的mac地址
    }
    if (config.publish_mac_on_match_cnf) {
        callbacks.signal_ev_mac_address_match_cnf = [this](const std::string& mac) { publish_ev_mac_address(mac); };   完成匹配时广播ev的mac地址
    }

3.6.6.3 FSM类定义

/home/vboxuser/checkout/everest-workspace/libfsm/include/fsm/fsm.hpp
/home/vboxuser/checkout/everest-workspace/libfsm/examples/light_switch/light_switch_state_diagram.svg

3.6.7 vendor相关代码

EvseSlac模块代码中定义了 厂家vendor,这个vendor用来干什么呢?
/home/vboxuser/checkout/everest-workspace/libslac/include/slac/slac.hpp
qualcomm:  vendor_mme[3] = {0x00, 0xb0, 0x52};
lumissil:  vendor_mme[3] = {0x00, 0x16, 0xE8};

实际上,运行这个代码时,如果启动了一个参数设置项就会强制检查PLC芯片的厂家参数,如果不是qualcomm和lumissil就导致失败退出。 这样的话就不能运行在其他芯片的板子上了。默认情况下并不会检查芯片厂家vendor。

3.6.8 Slac层代码MatchingSession、MatchingState

发现evse端的slac底层代码, 真正处理slac命令消息,驱动了slac匹配流程。
/home/vboxuser/checkout/everest-workspace/everest-core/lib/staging/slac/fsm/evse/include/slac/fsm/evse/states/matching.hpp
/home/vboxuser/checkout/everest-workspace/everest-core/lib/staging/slac/fsm/evse/src/states/matching.cpp
/home/vboxuser/checkout/everest-workspace/everest-core/lib/staging/slac/fsm/evse/src/states/matching_handle_slac.hpp
/home/vboxuser/checkout/everest-workspace/everest-core/lib/staging/slac/fsm/evse/src/states/matching_handle_slac.cpp

MatchingSession类:  本机是evse桩端,可能收到多个ev端的slac信号(可能是电力线路耦合干扰, 但是信号强度会有强弱区分)。对每一个新的请求都建立一个会话,测量线路上的信号强度,确定会话状态。每个会话独立保存在MatchingSession类中,区别是ev_mac地址不同。

MatchingState类:  保存会话列表,提供evse端处理命令函数。
先看看类结构和状态机。
3.6.9  状态机父类和子类
FSMController类的属性fsm就是状态机。 定义也是通过模版类定义的。
类图略
【注意】在基类 SimpleStateBase中定义的callback()是viretual函数,默认是返回{}对象,在部分子类中并没有重载callback()函数,仍然调用基类的函数。
重要的是InitState(初始化类)。当FSMController::run()启动起来后,建立的状态机就是InitState。
/home/vboxuser/checkout/everest-workspace/everest-core/modules/EvseSlac/main/fsm_controller.cpp
void FSMController::run() {
    ctx.log_info("Starting the SLAC state machine");
    fsm.reset<slac::fsm::evse::InitState>(ctx);
    std::unique_lock<std::mutex> feed_lck(feed_mtx);

    running = true;

    while (true) {
        auto feed_result = fsm.feed();

        if (feed_result.transition()) {
            // call immediately again
            continue;
        } else if (feed_result.internal_error() || feed_result.unhandled_event()) {
            // FIXME (aw): would need to log here!
        } else if (feed_result.has_value() == true) {
            const auto timeout = *feed_result;
            if (timeout == 0) {
                // call feed directly again
                continue;
            }
            new_event_cv.wait_for(feed_lck, std::chrono::milliseconds(timeout), [this] { return new_event; });
        } else {
            // nothing happened, no return value -> wait for new event
            new_event_cv.wait(feed_lck, [this] { return new_event; });
        }

        if (new_event) {
            // we got a new event, reset it and let run feed again
            new_event = false;
        }
    }
}

fsm.feed()   ,InitState类中没有重写feed()函数,此处调用模版类FSM中的feed()。该函数把callback()的返回结果转换了类型:FeedResultState::TRANSITION、UNHANDLED_EVENT、INTERNAL_ERROR。

/home/vboxuser/checkout/everest-workspace/libfsm/include/fsm/fsm.hpp    
    FeedResult<ReturnType> feed() {
        using FeedResultState = _impl::FeedResultState;
        if (current_state == nullptr) {
            return FeedResultState::INTERNAL_ERROR;
        }

        const auto result = current_state->callback();

        if (result.is_event) {
            switch (handle_event(result.event)) {
            case HandleEventResult::SUCCESS:
                return FeedResultState::TRANSITION;
            case HandleEventResult::UNHANDLED:
                return FeedResultState::UNHANDLED_EVENT;
            default:
                // NOTE: everything else should be an internal error
                return FeedResultState::INTERNAL_ERROR;
            }
        } else if (result.is_value_set) {
            return result.value;
        } else {
            return FeedResultState::NO_VALUE;
        }
    }

     current_state->callback();       此时调用的是子类InitState中的callback();   文件 /home/vboxuser/checkout/everest-workspace/libfsm/include/fsm/fsm.hpp

/home/vboxuser/checkout/everest-workspace/libfsm/include/fsm/fsm.hpp    
    FeedResult<ReturnType> feed() {
        using FeedResultState = _impl::FeedResultState;
        if (current_state == nullptr) {
            return FeedResultState::INTERNAL_ERROR;
        }

        const auto result = current_state->callback();

        if (result.is_event) {
            switch (handle_event(result.event)) {
            case HandleEventResult::SUCCESS:
                return FeedResultState::TRANSITION;
            case HandleEventResult::UNHANDLED:
                return FeedResultState::UNHANDLED_EVENT;
            default:
                // NOTE: everything else should be an internal error
                return FeedResultState::INTERNAL_ERROR;
            }
        } else if (result.is_value_set) {
            return result.value;
        } else {
            return FeedResultState::NO_VALUE;
        }
    }

这个callback()函数会反复执行3次,执行一次后就把 sub_state设置为下一步状态值, sub_state的初始值是QUALCOMM_OP_ATTR。 

如果发送的slac消息没有应答就出现超时,超时时间定义是100ms。
   第一次调用, 读取高通芯片的属性,消息是op_attr_req,
   第二次调用,读取lumissil的版本号,消息是nscm_get_version_req,
   第三次调用,直接返回Event::SUCCESS。

3.6.10 FSMController工作原理

FSM控制器主体是run(), 内部是一个while循环, 不断的通过feed() 调用fsm.callback(),然后根据结果唤醒条件变量的等待或继续等待。
控制器内包含一个状态机变量fsm,不同阶段指向不同的状态机。 第一个阶段对应的状态机是InitState。
一开始就执行InitState状态机的callback(),  会连续执行三次, 最后返回Event::SUCCESS, 驱动了while循环继续运行。

3.6.10.1 InitState状态机

在InitState::handle_event()中接收Event, 分别处理:
    Event::SLAC_MESSAGE--- 执行handle_slac_message(), 返回sa.PASS_ON
    Event::SUCCESS---  创建ResetState,
后来进入了ResetState状态,执行了 ResetState::callback():   
      到底是谁驱动变成Reset状态的???
【回答--从InitState->ResetState】
首先执行FSMController::run() ,在while循环中:
     fsm.feed()  内部调用顺序
              result = current_state->callback();
              handle_event(result.event)    这个handle_event是FSM模版类中的函数,内部调用当前状态机的handle_event()
                     InitState::handle_event()     这里处理Event::SUCCESS, 创建出 ResetState, 状态发生了切换!!!! 返回的是sa.PASS_ON
                     返回 HandleEventResult::UNHANDLED;
    返回FeedResultState::UNHANDLED_EVENT, 继续循环。
3.6.10.2 ResetState状态机 
    fsm.feed()调用ResetState::callback(), 设置CM_SET_KEY_REQ,  返回超时时间500ms。注意这个命令只发送一次,第二次调用就返回{}。
    此时while循环中进入条件等待,因为是PC上运行无PLC芯片,就出现超时退出等待, 重新进入循环。
    第二次调用fsm.feed(), 会进入因此进入到普通的等待中, 无超时时间,只有Event事件才能退出等待。
---- 000  handle_event: template <typename EventType, typename ReturnType> class FSM  
2024-08-28 18:33:04.818437 [INFO] slac:EvseSlac    :: Entered Reset state
2024-08-28 18:33:04.818465 [INFO] slac:EvseSlac    :: --feed_result.transition() ... 000
2024-08-28 18:33:04.818487 [INFO] slac:EvseSlac    :: ----feed_result.transition() ... 1111
2024-08-28 18:33:04.818542 [INFO] slac:EvseSlac    :: New NMK key: 44:43:46:54:52:4F:47:38:59:4E:51:32:38:37:48:46

如果PLC芯片发送了应答CM_SET_KEY_CNF, 那么就被送给 ResetState::handle_event() , 随之切换到 IdleState状态机。

3.6.10.3 IdleState状态机

 Idle状态下只处理两个Event, 无法处理slac消息。
       Event::ENTER_BCD---   表示插枪或EFtoBCD,CP状态切换到了BCD,要进行slac匹配,因此切换到MatchingState状态机。
       Event::RESET  ----  允许切回到上一个状态ResetState
【疑问:谁发出的Event::ENTER_BCD】
   代码中看到这样的调用关系:
     slacImpl::handle_enter_bcd()
           FSMController::signal_enter_bcd() 
                 FSMController::signal_simple_event(slac::fsm::evse::Event::ENTER_BCD);     
搜遍全部代码,看到是在启动阶段 自动调用的 slacImpl::handle_enter_bcd() 。   这里的cmds是否会被自动调用呢?  下面有介绍。
/home/vboxuser/checkout/everest-workspace/everest-core/build/generated/include/generated/interfaces/slac/Implementation.hpp
class slacImplBase : public Everest::ImplementationBase {
    void _gather_cmds(std::vector<Everest::cmd>& cmds){
        // enter_bcd command
        Everest::cmd enter_bcd_cmd;
        enter_bcd_cmd.impl_id = _name;
        enter_bcd_cmd.cmd_name = "enter_bcd";
        // cmd enter_bcd has no arguments
        enter_bcd_cmd.cmd = [this](Parameters args) -> Result {
            (void) args; // no arguments used for this callback


            auto result = this->handle_enter_bcd();
            return result;
        };
        enter_bcd_cmd.return_type = {"boolean"};
        cmds.emplace_back(std::move(enter_bcd_cmd));

3.6.10.4 MatchingState状态机

 在FSM::handle_event()中,会调用next_state->enter();  因此所有的新状态机都会执行enter()函数,完成一些初始化工作。
 进入MatchingState状态, 就会触发调用 MatchingState::enter(),  设置slac初始化时间为40s。 15118-3协议规定初始化时间 TT_EVSE_SLAC_init 是20~50s, 重试 C_EV_match_retry=2次。
之后等待cm_slac_parm_req消息。
 MatchingState状态下有内部子状态,根据收到的slac命令驱动到不同的子状态上。定义如下:
/home/vboxuser/checkout/everest-workspace/everest-core/lib/staging/slac/fsm/evse/include/slac/fsm/evse/states/matching.hpp
enum class MatchingSubState {
    WAIT_FOR_START_ATTEN_CHAR,
    SOUNDING,
    FINALIZE_SOUNDING,
    WAIT_FOR_ATTEN_CHAR_RSP,
    WAIT_FOR_SLAC_MATCH,
    RECEIVED_SLAC_MATCH,
    MATCH_COMPLETE,
    FAILED,
};
收到的slac消息都自动调用 handle_slac_message(), 里面根据MMEtype分别处理不同的命令:
/home/vboxuser/checkout/everest-workspace/everest-core/lib/staging/slac/fsm/evse/src/states/matching_handle_slac.cpp
    CM_SLAC_PARM.REQ:  收到广播命令,发送应答CM_SLAC_PARM.CNF, 设置收到下一条命令的超时时间400ms。  合乎15118-3协议的超时时间 TT_match_sequence 规定。  
                                            广播收到CM_SLAC_PARM.REQ。
    CM_START_ATTEN_CHAR.IND:这条命令会连发3次,无需应答。 设置超时时间600ms,对应协议的 TT_EVSE_match_MNBC。
    CM_MNBC_SOUND.IND: 开始连续的10条命令,无需应答。 
    CM_ATTEN_PROFILE.IND: 这是PLC芯片发给slac程序的内部消息,每次收到CM_MNBC_SOUND.IND就会在芯片内部产生一条CM_ATTEN_PROFILE.IND命令。
                                                收到命令后累加信号数值,检查是否达到10次,到10次后表示收听信号子状态结束, 重新设置超时时间45ms。
                                                在callback()中检测到超时,会计算信号平均增益,发送CM_ATTEN_CHAR.IND请求命令,等待应答超时时间200ms,对应协议的 TT_match_response 。
    CM_ATTEN_CHAR.RSP: 收到ev端的应答,并没有判断结果:成功、失败、待校验。 设置超时时间10s,对应协议的 TT_EVSE_match_session。
    CM_VALIDATE.REQ:  这是可选命令,everest不支持校验命令,回复失败。
    CM_SLAC_MATCH.REQ: 收到匹配请求,发送CM_SLAC_MATCH.CNF,开始加入同一个逻辑网络。子状态设置为MatchingSubState::MATCH_COMPLETE,
                                             广播match消息。 
                                             设置超时时间为0,表示立即执行。
MatchingState::callback()
  循环检查每一个会话,如果会话的超时标志有效,就更新等待时间。超时后一般子状态设置为MatchingSubState::FAILED;
  如果所有会话都失败, 返回Event::FAILED。
  子状态是MatchingSubState::MATCH_COMPLETE,就返回Event::MATCH_COMPLETE, 交给handle_event()处理。
MatchingState::handle_event()
  处理Event::MATCH_COMPLETE,  切换状态为 WaitForLinkState  或  MatchedState。
  处理Event::FAILED, 宣告进入FailedState状态机。
如果进入MatchingState后一直没有收到slac命令,就出现40s超时, 会再重新等待一次,如果还是超时,就进入FailedState状态。
广播消息的 过程:
   MatchingState状态会通过mqtt广播消息, 调用过程如下:
  ctx.signal_cm_slac_parm_req(tmp_ev_mac);
  ctx.signal_cm_slac_match_cnf(tmp_ev_mac);                         /home/vboxuser/checkout/everest-workspace/everest-core/lib/staging/slac/fsm/evse/src/states/matching_handle_slac.cpp
       callbacks.signal_ev_mac_address_match_cnf(mac_string);       /home/vboxuser/checkout/everest-workspace/everest-core/lib/staging/slac/fsm/evse/src/context.cpp
           callbacks.signal_ev_mac_address_parm_req = [this](const std::string& mac) { publish_ev_mac_address(mac);       /home/vboxuser/checkout/everest-workspace/everest-core/modules/EvseSlac/main/slacImpl.cpp

3.6.10.5 MatchedState状态机

此时已经连接成功,自动执行enter(), 通过mqtt广播 state 和 dlink_ready。
/home/vboxuser/checkout/everest-workspace/everest-core/lib/staging/slac/fsm/evse/include/slac/fsm/evse/states/others.hpp

void MatchedState::enter() {
    ctx.signal_state("MATCHED");
    ctx.signal_dlink_ready(true);
    ctx.log_info("Entered Matched state");
}

void MatchedState::leave() {
    ctx.signal_dlink_ready(false);
}

在已连接状态下,没有任何需要处理的slac命令,因此,MatchedState::handle_event很简单,只需要处理Event::RESET事件即可。

FSMSimpleState::HandleEventReturnType MatchedState::handle_event(AllocatorType& sa, Event ev) {
    if (ev == Event::RESET) {
        return sa.create_simple<ResetState>(ctx);
    } else {
        return sa.PASS_ON;
    }
}

当外部拔枪时,Manager模块调用EvseSlac模块的接口命令reset, 产生事件Event::RESET, 驱动状态切换成ResetState。

3.6.10.6 WaitForLinkState状态机 

按照15118-3协议,当evse发出CM_SLAC_MATCH.CNF,就开始加入逻辑网络,这个过程本质上双方设置同一个NMK,就自动加入了同一个逻辑网络。
双方是否加入成功,需要经过一个检测步骤, 在协议中并没有明确定义该如何检查。
当 加入逻辑网络操作完成后,PLC芯片会自动向上发送命令报告网络状态,如果返回值是1表示连接成功。
其他芯片厂家的做法是完全与高通、Lumissil一致的, 因此后二者也会收到报告的状态命令。
【警告】在Everest代码中就对此时的slac命令进行了厂家vendorID检查, 如果是Qualcomm、Lumissil就成功,其它厂家id就失败。
             成功后才切换到 MatchedState状态机,否则一直待在此状态机。
在everest代码中, 使用配置参数 link_status_detection 控制是否检查连接状态。
在配置文件中定义参数,默认是false,因此默认不会进入WaitForLinkState状态机。
/home/vboxuser/checkout/everest-workspace/everest-core/modules/EvseSlac/manifest.yaml
    link_status_detection:
        description: After matching.cnf, wait for link to come up before sending out d_link_ready=connected using LINK_STATUS Vendor MME Extension (Works on Qualcomm and Lumissil chips)
        type: boolean
        default: false

如果该参数设置为true,那么就会进入WaitForLinkState状态机,对slac命令处理过程如下。

/home/vboxuser/checkout/everest-workspace/everest-core/lib/staging/slac/fsm/evse/src/states/others.cpp

FSMSimpleState::HandleEventReturnType WaitForLinkState::handle_event(AllocatorType& sa, Event ev) {
    if (ev == Event::SLAC_MESSAGE) {
        if (handle_slac_message(ctx.slac_message_payload)) {
            return sa.create_simple<MatchedState>(ctx);
        } else {
            return sa.PASS_ON;
        }
    } else if (ev == Event::RETRY_MATCHING) {
        ctx.log_info("Link could not be established, resetting...");
        // Notify higher layers to on CP signal
        return sa.create_simple<FailedState>(ctx);
    } else {
        return sa.PASS_ON;
    }
}


bool WaitForLinkState::handle_slac_message(slac::messages::HomeplugMessage& message) {
    const auto mmtype = message.get_mmtype();

    if (ctx.modem_vendor == ModemVendor::Qualcomm &&
        mmtype == (slac::defs::qualcomm::MMTYPE_LINK_STATUS | slac::defs::MMTYPE_MODE_CNF)) {
        const auto success = message.get_payload<slac::messages::qualcomm::link_status_cnf>().link_status == 0x01;
        return success;

    } else if (ctx.modem_vendor == ModemVendor::Lumissil &&
               mmtype == (slac::defs::lumissil::MMTYPE_NSCM_GET_D_LINK_STATUS | slac::defs::MMTYPE_MODE_CNF)) {
        const auto success =
            message.get_payload<slac::messages::lumissil::nscm_get_d_link_status_cnf>().link_status == 0x01;
        return success;

    } else {
        // unexpected message
        ctx.log_info("Received non-expected SLAC message of type " + format_mmtype(mmtype));
        return false;
    }
}

【注意】启动这个参数就意味着进行PLC芯片厂家Vendor检查。如果检测失败,就一直停留在WaitForLinkState状态。 

3.6.10.7 FailedState状态机

前面状态机运行中等待应答超时超时失败、达到retry次数、芯片身份验证错误,都会进入失败状态,并且会一直保持该状态,直到高层发出reset命令才切换到RestState。
一旦进入失败状态就广播失败消息,通知高层尽快发送reset命令。
void FailedState::enter() {
    ctx.signal_error_routine_request();
    ctx.log_info("Entered Failed state");
}

FSMSimpleState::HandleEventReturnType FailedState::handle_event(AllocatorType& sa, Event ev) {
    if (ev == Event::RESET) {
        return sa.create_simple<ResetState>(ctx);
    } else {
        return sa.PASS_ON;
    }
}

3.6.11 状态机的切换顺序

ResetChipState状态是受配置参数cfg.chip_reset.enabled控制,可有可无。
WaitForLinkState状态受配置参数link_status_detection控制,可有可无。
InitState  -> ResetState  ->  [ ResetChipState ]  <->   IdleState  -> MatchingState   ->  [ WaitForLinkState]  ->  MatchedState  ->   FailedState
                                       
状态转换图略
当充电结束,会重新回到ResetState状态,再继续到IdleState,就此稳定下来。
如果出现错误,进入到FailedState状态,

3.6.12 EvseSlac模块的接口文件slac.yaml

EvseSlac模块启动时自动加载配置文件 everest-core/modules/EvseSlac/manifest.yaml
该文件指示interface文件是 slac, config是定义的一些变量参数。
description: Implementation of SLAC data link negotiation according to ISO15118-3.
provides:
  main:
    interface: slac
    description: SLAC interface implementation.
    config:
      device:
        description: Ethernet device used for PLC.
        type: string
        default: eth1
    ......

这个slac接口文件是:everest-core/build/dist/share/everest/interfaces/slac.yaml

定义了一组cmds, 一组vars。
cmds包含的命令是提供给外部调用的。
   利用这些命令可以手动控制slac过程:reset、enter_bcd、leave_bcd、dlink_terminate、dlink_error
vars包含的命令是模块向外部广播的数据。 state、dlink_ready、request_error_routine、ev_mac_address
description: ISO15118-3 SLAC interface for EVSE side
cmds:
  reset:
    description: Reset SLAC
    arguments:
      enable:
        description: 'true: start SLAC after reset, false: stop SLAC'
        type: boolean
  enter_bcd:
    description: Signal pilot state change to B/C/D from A/E/F.
    result:
      type: boolean
      description: >-
        True on success, returns False if transition was unexpected and
        cannot be handled by SLAC state machine.
  leave_bcd:
    description: Signal pilot state change to A/E/F from B/C/D.
    result:
      type: boolean
      description: >-
        True on success, returns False if transition was unexpected and
        cannot be handled by SLAC state machine.
  dlink_terminate:
    description: Terminate the data link and become UNMATCHED.
    result:
      type: boolean
      description: True on success.
  dlink_error:
    description: Terminate the data link and restart the matching process.
    result:
      type: boolean
      description: True on success.
  dlink_pause:
    description: Request power saving mode, while staying MATCHED.
    result:
      type: boolean
      description: True on success.
vars:
  state:
    description: Provides the state enum.
    type: string
    enum:
      - UNMATCHED
      - MATCHING
      - MATCHED
  dlink_ready:
    description: >-
      Inform higher layers about a change in data link status. Emits true
      if link was set up and false when the link is shut down.
    type: boolean
  request_error_routine:
    description: >-
      Inform the higher layer to execute the error routine for a SLAC connection
      retry
    type: 'null'
  ev_mac_address:
    description: >-
      Inform higher layers about the MAC address of the vehicle (upper case)
    type: string
    pattern: ^[A-F0-9]{2}(:[A-F0-9]{2}){5}$

这个yaml编译时自动生成接口文件: 

everest-core/build/generated/include/generated/interfaces/slac/Implementation.hpp  --供??调用
everest-core/build/generated/include/generated/interfaces/slac/Interface.hpp   --供EvseManager类中调用
里面生成了对应cmd和var的函数实现代码。

3.6.13 调用接口cmd

当所有模块都初始化后,发出ready消息,执行EvseManager.ready()。  这里面把CPEvent事件绑定到EvseSlac模块的call命令上。
 bsp->signal_event.connect使用的是信号槽机制。 
everest-core/modules/EvseManager/EvseManager.cpp
void EvseManager::ready() { 
   bsp->signal_event.connect([this](const CPEvent event) {
        // Forward events from BSP to SLAC module before we process the events in the charger
        if (slac_enabled) {
            if (event == CPEvent::EFtoBCD) {
                // this means entering BCD from E|F
                r_slac[0]->call_enter_bcd();
            } else if (event == CPEvent::BCDtoEF) {
                r_slac[0]->call_leave_bcd();
            } else if (event == CPEvent::CarPluggedIn) {
                // CC: right now we dont support energy saving mode, so no need to reset slac here.
                // It is more important to start slac as early as possible to avoid unneccesary retries
                // e.g. by Tesla cars which send the first SLAC_PARM_REQ directly after plugin.
                // If we start slac too late, Tesla will do a B->C->DF->B sequence for each retry which
                // may confuse the PWM state machine in some implementations.
                // r_slac[0]->call_reset(true);
                // This is entering BCD from state A
                car_manufacturer = types::evse_manager::CarManufacturer::Unknown;
                r_slac[0]->call_enter_bcd();
            } else if (event == CPEvent::CarUnplugged) {
                r_slac[0]->call_leave_bcd();
                r_slac[0]->call_reset(false);
            }
        }

对应关系:

CP事件
调用的接口命令
说明
CPEvent::EFtoBCD
enter_bcd
CPEvent::BCDtoEF
leave_bcd
CPEvent::CarPluggedIn
enter_bcd
插枪
CPEvent::CarUnplugged
leave_bcd
reset
拔枪,调用两个命令
在EvseSlac模块中向mqtt发送消息:
状态机
mqtt命令
说明
MatchedState
发送 state=MATCHED
匹配成功
MatchedState
发送dlink_ready=true
DLink建立成功
MatchedState
发送dlink_ready=false
MatchedState状态机下收到Reset命令,
通知上层DLink断开。
MatchingState
收到CM_SLAC_PARM.REQ:
收到CM_SLAC_MATCH.REQ
发送对方的mac地址
匹配开始:广播ev的mac地址。
加入逻辑网络:广播ev的mac地址。
FailedState
request_error_routine
进入失败状态,广播错误请求。
注意这里: 一进入idle状态就广播state("UNMATCHED").
void IdleState::enter() {
    ctx.signal_state("UNMATCHED");
    ctx.log_info("Entered Idle state");
}

实际捕捉到的消息:

everest/slac/evse/var {"data":"MATCHING","name":"state"}

everest/slac/evse/var {"data":"UNMATCHED","name":"state"}

EVSEManager模块中订阅消息, 接收上面广播的state、dlink_ready消息, 触发具体的处理过程。

everest-core/modules/EvseManager/EvseManager.cpp
EvseManager::ready()
    if (slac_enabled) {
        r_slac[0]->subscribe_state([this](const std::string& s) {
            session_log.evse(true, fmt::format("SLAC {}", s));
            // Notify charger whether matching was started (or is done) or not
            if (s == "UNMATCHED") {
                charger->set_matching_started(false);
            } else {
                charger->set_matching_started(true);
            }
        });


        r_slac[0]->subscribe_request_error_routine([this]() {
            EVLOG_info << "Received request error routine from SLAC in evsemanager\n";
            charger->request_error_sequence();
        });


        r_slac[0]->subscribe_dlink_ready([this](const bool value) {
            session_log.evse(true, fmt::format("D-LINK_READY ({})", value));
            if (hlc_enabled) {
                r_hlc[0]->call_dlink_ready(value);
            }
        });
    }

3.6.14 EvseSlac运行框架总结

【总结】EvseSlac使用了3个模型, 实现了内部和外部消息循环,驱动了状态机轮转。
分别是: 
  • SlacMessage模型:  poll模型,收发PLC线路上的slac命令。
  • FSMController模型:事件等待模型,监听Event事件,通过条件变量唤醒。
  • Mqtt模型:poll模型, 监听3种socket: mqtt_socket, event_fd, desconnect_event_fd
详细的内部模块连接关系图:
对EvseSlac分析结束。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

快活林高老大

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值