当前位置: 首页 > 工具软件 > rate.sx > 使用案例 >

无线定位之一 SX1302 网关源码 thread_up 线程详解

谷梁建中
2023-12-01

前言

笔者从实践出发、本篇直接走读无线定位系统关键节点、网关 SX1302 源码框架,并在源码走读过程中、着重分析与无线定位相关的PPS时间的来龙去脉、并在后期文章中以实际代码讲解 TDOA 无线定位实现过程及多网关综合定位内容,敬请期待。

semtech 公司在 2020年06月份推出 LR1110\LR1120 两款GNSS、WIFI和Lora(LR-HFSS)混合定位芯片、并提供’定位云服务’的接入、国内与腾讯云合作,腾讯云也提供定位云服务接入,这是笔者对混合无线定位技术背景简单描述、此用意看官自行审度。

闲言少叙直奔主体、sx1302 网关数据处理逻辑在 lora_pkt_fwd.c 文件,看官阅读下面内容时、请打开源码对照阅读下、否则可能不知道笔者所云是何物。

本篇文章目标是快速建立起SX1302网关框架认识。

第1节 主程序代码走读

主线程基本功能:
<1>. 读取 *.conf.json 文件内容、并解析内容把变量赋值到相关全局变量中;
<2>. 启动各子线程、子线程清单如下所述;
<3>. 固定周期定时检测gps的时间戳、并上报网关的状态信息;
<4>. 等待退出信号量、网络断开信号量和各子线程退出.

子线程清单.

/* threads */
void thread_up(void);               //> 上行线程:负责接收lora模块的数据、并把数据通过网络上传至网络服务器;
void thread_down(void);             //> 下行线程:负责接收服务器的数据,并把数据通过lora无线下方给终端模块;
void thread_jit(void);              //> jit 时间线程:
void thread_gps(void);              //> gps 线程: 
void thread_valid(void);            //> 时钟校正线程
void thread_spectral_scan(void);    //> SCAN扫描线程:

主程序源码基本功能就这么多,笔者就不贴出源码对照了。

第2节 线程 thread_up 代码走读

此线程负责实时读取 Lora 模块的数据、并打包数据内容上传至网络服务器。

此线程代码逻辑如下:
1>. 配置网关网络上线通讯参数、并建立上行传输socket信道和下行传输的socket信道;
2>. 读取Lora sx1302 模块数据内容、并解码接收数据包内容;
3>. 配置上报数据帧内容和状态、发送数据包内容;
4>. 延时等待接收网络服务器回复 ACK 内容。

我们根据上面总体功能框架、逐步走读此部分代码;先看一下 lgw_pkt_rx_s 结构体内容定义:

2.1 通讯协议数据格式

/**
@struct lgw_pkt_rx_s
@brief Structure containing the metadata of a packet that was received and a pointer to the payload
*/
struct lgw_pkt_rx_s {
    uint32_t    freq_hz;        /*!> central frequency of the IF chain */
    int32_t     freq_offset;
    uint8_t     if_chain;       /*!> by which IF chain was packet received */
    uint8_t     status;         /*!> status of the received packet */
    uint32_t    count_us;       /*!> internal concentrator counter for timestamping, 1 microsecond resolution, 网关时间戳计数器 */
    uint8_t     rf_chain;       /*!> through which RF chain the packet was received, 数据包来源链路 */
    uint8_t     modem_id;       //>  模块ID号
    uint8_t     modulation;     /*!> modulation used by the packet, 本包调制模式 */
    uint8_t     bandwidth;      /*!> modulation bandwidth (LoRa only) */
    uint32_t    datarate;       /*!> RX datarate of the packet (SF for LoRa) */
    uint8_t     coderate;       /*!> error-correcting code of the packet (LoRa only) */
    float       rssic;          /*!> average RSSI of the channel in dB, 接收信息RSSI平均值 */
    float       rssis;          /*!> average RSSI of the signal in dB, 本包接收信息的RSSI 值 */
    float       snr;            /*!> average packet SNR, in dB (LoRa only) */
    float       snr_min;        /*!> minimum packet SNR, in dB (LoRa only) */
    float       snr_max;        /*!> maximum packet SNR, in dB (LoRa only) */
    uint16_t    crc;            /*!> CRC that was received in the payload */
    uint16_t    size;           /*!> payload size in bytes */
    uint8_t     payload[256];   /*!> buffer containing the payload */
    bool        ftime_received; /*!> a fine timestamp has been received ,接收到精准时间戳标志位 */
    uint32_t    ftime;          /*!> packet fine timestamp (nanoseconds since last PPS) 该报数据接收时间,TDOA 同步时间 */
};

根据此格式我们基本上得出、网关的通讯基本逻辑:
<1>. 数据通讯采用 json 格式、采用 key:value 模式组织数据;
<2>. 网关与Lora模块之间的数据帧、存放在 payload【】 数组中,此部分可以扩展私有协议内容;
<3>. 其他字段内容根据注释和字段名望文生义即可;
<4>. 与网络服务器之间采用异步通讯结构、上报数据后就接收ACK内容;网络服务器会根据上报内容
下发命令、是在下行线程中进行、与上行线程间无业务关联。

2.2 thread_up 线程网络通讯建立


/* prepare hints to open network sockets */
    memset(&hints, 0, sizeof hints);
    hints.ai_family = AF_INET; /* WA: Forcing IPv4 as AF_UNSPEC makes connection on localhost to fail */
    hints.ai_socktype = SOCK_DGRAM;  //> UDP 通讯方式

    /* look for server address w/ upstream port */
    i = getaddrinfo(serv_addr, serv_port_up, &hints, &result);
    if (i != 0) {
        MSG("ERROR: [up] getaddrinfo on address %s (PORT %s) returned %s\n", serv_addr, serv_port_up, gai_strerror(i));
        exit(EXIT_FAILURE);
    }
    
/* try to open socket for upstream traffic */
    for (q=result; q!=NULL; q=q->ai_next) {
        sock_up = socket(q->ai_family, q->ai_socktype,q->ai_protocol);
        if (sock_up == -1) continue; /* try next field */
        else break; /* success, get out of loop */
    }
/* connect so we can send/receive packet with the server only */
    i = connect(sock_up, q->ai_addr, q->ai_addrlen);
    if (i != 0) {
        MSG("ERROR: [up] connect returned %s\n", strerror(errno));
        exit(EXIT_FAILURE);
    }
/* look for server address w/ downstream port */
    i = getaddrinfo(serv_addr, serv_port_down, &hints, &result);
    if (i != 0) {
        MSG("ERROR: [down] getaddrinfo on address %s (port %s) returned %s\n", serv_addr, serv_port_down, gai_strerror(i));
        exit(EXIT_FAILURE);
    }
 /* set upstream socket RX timeout, 此部分代码在 thread_up 线程中 */
    i = setsockopt(sock_up, SOL_SOCKET, SO_RCVTIMEO, (void *)&push_timeout_half, sizeof push_timeout_half);
    if (i != 0) {
        MSG("ERROR: [up] setsockopt returned %s\n", strerror(errno));
        exit(EXIT_FAILURE);
    }    

网络服务器的ip地址是在 *.conf.json 文件中,如下:

 "gateway_conf": {
        "gateway_ID": "AA555A0000000000",
        /* change with default server address/ports */
        "server_address": "localhost",
        "serv_port_up": 1730,
        "serv_port_down": 1730,
        /* adjust the following parameters for your network */
        "keepalive_interval": 10,
        "stat_interval": 30,
        "push_timeout_ms": 100,
        /* forward only valid packets */
        "forward_crc_valid": true,
        "forward_crc_error": false,
        "forward_crc_disabled": false,
        /* GPS configuration */
        "gps_tty_path": "/dev/ttyS0",
        /* GPS reference coordinates */
        "ref_latitude": 0.0,
        "ref_longitude": 0.0,
        "ref_altitude": 0,
        /* Beaconing parameters */
        "beacon_period": 0,
        "beacon_freq_hz": 869525000,
        "beacon_datarate": 9,
        "beacon_bw_hz": 125000,
        "beacon_power": 14,
        "beacon_infodesc": 0
    },

此部分是网关的配置内容,其中 server_address 和 serv_port_up 是上行线程中的网络通讯参数,
此部分配置内容我们就望文生义呗、基本上可以理解差不多。

网关地址上报至网络服务器

/* gateway unique identifier (aka MAC address) (optional) */
    str = json_object_get_string(conf_obj, "gateway_ID");
    if (str != NULL) {
        sscanf(str, "%llx", &ull);
        lgwm = ull;
        MSG("INFO: gateway MAC address is configured to %016llX\n", ull);
    }

/* process some of the configuration variables */
    net_mac_h = htonl((uint32_t)(0xFFFFFFFF & (lgwm>>32)));
    net_mac_l = htonl((uint32_t)(0xFFFFFFFF &  lgwm  ));

/* pre-fill the data buffer with fixed fields */
    buff_up[0] = PROTOCOL_VERSION;                //> 协议版本 
    buff_up[3] = PKT_PUSH_DATA;                   //> 上行数据标识
    *(uint32_t *)(buff_up + 4) = net_mac_h;       //> 网关 ID
    *(uint32_t *)(buff_up + 8) = net_mac_l;

/* start composing datagram with the header */
        token_h = (uint8_t)rand(); /* random token */
        token_l = (uint8_t)rand(); /* random token */
        buff_up[1] = token_h;                   //> token
        buff_up[2] = token_l;    

上行信道通讯协议格式定义如下:
/--------------------------------------------------------------
| 12 bytes 帧头 | JSON 内容 {“rxpk”:[],“stat”:{}} |
--------------------------------------------------------------/

帧头格式: 协议版本、token、上行数据标识 和网关 ID 地址,总共 12 bytes ;

json 串中存放两个数据集,
 1>. rxpk : [{"jver":10, "tmst": , "time": , "tmms": , "ftime": , "chan": , "rfch": , "freq": , "mid": , "stat":-1, "modu":"FSK || LORA", "datr":"SF5", 
              "BW125":"", "codr":"4/5", "rssis":0.1, "lsnr":1.0, "foff":123, "rssi":0.123, "size": 5, "data":"base64_code"}] 
     是无线通讯物理参数,如频率、带宽、码速率和载荷数据等;
 2>. "stat":{"time":"2022-10-08 01:50:20 GMT","rxnb":0,"rxok":0,"rxfw":0,"ackr":0.0,"dwnb":0,"txnb":0,"temp":0.0}
     存放的是统计数据内容。

至此我们大致了解网关与网络服务器间的通讯协议格式, 网关 id 在帧头上传输、Lora 模块 id 在
json 串的 modem_id 中,基本通讯框架算是建立起来喽。

接下来看thread_up程序框架功能。

2.3>. thread_up 框架功能

下面代码已删减、只保留主体功能.

void thread_up(void) 
{

	while (!exit_sig && !quit_sig) 
	{
		//> 接收 Lora 模块数据内容
		nb_pkt = lgw_receive(NB_PKT_MAX, rxpkt);

		/* start composing datagram with the header,补充帧头 token 内容 */
        token_h = (uint8_t)rand(); /* random token */
        token_l = (uint8_t)rand(); /* random token */
        buff_up[1] = token_h;
        buff_up[2] = token_l;

        /* start of JSON structure , 下面就是json串数据打包过程  */
        buff_index = 12; /* 12-byte header */
        memcpy((void *)(buff_up + buff_index), (void *)"{\"rxpk\":[", 9);
        buff_index += 9;

        for (i = 0; i < nb_pkt; ++i) {
            p = &rxpkt[i];

            /* Get mote information from current packet (addr, fcnt),数据区 payload 内容,lora模块上报过来的数据 */
            /* FHDR - DevAddr */
            if (p->size >= 8) {
                mote_addr  = p->payload[1];
                mote_addr |= p->payload[2] << 8;
                mote_addr |= p->payload[3] << 16;
                mote_addr |= p->payload[4] << 24;
                /* FHDR - FCnt */
                mote_fcnt  = p->payload[6];
                mote_fcnt |= p->payload[7] << 8;
            } else {
                mote_addr = 0;
                mote_fcnt = 0;
            }
            
            meas_nb_rx_rcv += 1;
            switch(p->status) {
                case STAT_CRC_OK:
                    meas_nb_rx_ok += 1;
                    if (!fwd_valid_pkt) {
                        pthread_mutex_unlock(&mx_meas_up);
                        continue; /* skip that packet */
                    }
                    break;
                case STAT_CRC_BAD:
                    meas_nb_rx_bad += 1;
                    if (!fwd_error_pkt) {
                        pthread_mutex_unlock(&mx_meas_up);
                        continue; /* skip that packet */
                    }
                    break;
                case STAT_NO_CRC:
                    meas_nb_rx_nocrc += 1;
                    if (!fwd_nocrc_pkt) {
                        pthread_mutex_unlock(&mx_meas_up);
                        continue; /* skip that packet */
                    }
                    break;
                default:
                    MSG("WARNING: [up] received packet with unknown status %u (size %u, modulation %u, BW %u, DR %u, RSSI %.1f)\n", p->status, p->size, p->modulation, p->bandwidth, p->datarate, p->rssic);
                    pthread_mutex_unlock(&mx_meas_up);
                    continue; /* skip that packet */
                    // exit(EXIT_FAILURE);
            }
            meas_up_pkt_fwd += 1;
            meas_up_payload_byte += p->size;
        }

        /* JSON rxpk frame format version, 8 useful chars,帧格式类型 */
        j = snprintf((char *)(buff_up + buff_index), TX_BUFF_SIZE-buff_index, "\"jver\":%d", PROTOCOL_JSON_RXPK_FRAME_FORMAT );
        if (j > 0) {
            buff_index += j;
        } else {
                MSG("ERROR: [up] snprintf failed line %u\n", (__LINE__ - 4));
                exit(EXIT_FAILURE);
        }

        /* RAW timestamp, 8-17 useful chars, 网关内部时钟 */
        j = snprintf((char *)(buff_up + buff_index), TX_BUFF_SIZE-buff_index, ",\"tmst\":%u", p->count_us);
        if (j > 0) {
            buff_index += j;
        } else {
                MSG("ERROR: [up] snprintf failed line %u\n", (__LINE__ - 4));
                exit(EXIT_FAILURE);
        }

        /* Packet RX time (GPS based), 37 useful chars, 接收Lora数据帧时间 */
        if (ref_ok == true) {
            /* convert packet timestamp to UTC absolute time */
            j = lgw_cnt2utc(local_ref, p->count_us, &pkt_utc_time);
                if (j == LGW_GPS_SUCCESS) {
                    /* split the UNIX timestamp to its calendar components */
                    x = gmtime(&(pkt_utc_time.tv_sec));
                    j = snprintf((char *)(buff_up + buff_index), TX_BUFF_SIZE-buff_index, ",\"time\":\"%04i-%02i-%02iT%02i:%02i:%02i.%06liZ\"", 
                        (x->tm_year)+1900, (x->tm_mon)+1, x->tm_mday, x->tm_hour, x->tm_min, x->tm_sec, (pkt_utc_time.tv_nsec)/1000); /* ISO 8601 format */
                    if (j > 0) {
                        buff_index += j;
                    } else {
                        MSG("ERROR: [up] snprintf failed line %u\n", (__LINE__ - 4));
                        exit(EXIT_FAILURE);
                    }
                }
            /* convert packet timestamp to GPS absolute time */
            j = lgw_cnt2gps(local_ref, p->count_us, &pkt_gps_time);
                if (j == LGW_GPS_SUCCESS) {
                    pkt_gps_time_ms = pkt_gps_time.tv_sec * 1E3 + pkt_gps_time.tv_nsec / 1E6;
                    j = snprintf((char *)(buff_up + buff_index), TX_BUFF_SIZE-buff_index, ",\"tmms\":%" PRIu64 "", pkt_gps_time_ms); /* GPS time in milliseconds since 06.Jan.1980 */
                    if (j > 0) {
                        buff_index += j;
                    } else {
                        MSG("ERROR: [up] snprintf failed line %u\n", (__LINE__ - 4));
                        exit(EXIT_FAILURE);
                    }
                }
        }

        /* Fine timestamp */
        if (p->ftime_received == true) {
                j = snprintf((char *)(buff_up + buff_index), TX_BUFF_SIZE-buff_index, ",\"ftime\":%u", p->ftime);
                if (j > 0) {
                    buff_index += j;
                } else {
                    MSG("ERROR: [up] snprintf failed line %u\n", (__LINE__ - 4));
                    exit(EXIT_FAILURE);
                }
        }

        //> 省略打包内容项开始
        p->if_chain
        p->rf_chain
        ((double)p->freq_hz / 1e6)
        p->modem_id
        p->status
        p->datarate
        p->bandwidth
        p->rssis
        p->snr
        p->freq_offset
        status_report
        //> 省略打包内容项结束

        /* send datagram to server,打包数据上报到网络服务器 */
        send(sock_up, (void *)buff_up, buff_index, 0);
        clock_gettime(CLOCK_MONOTONIC, &send_time);
        pthread_mutex_lock(&mx_meas_up);
        meas_up_dgram_sent += 1;
        meas_up_network_byte += buff_index;

        /* wait for acknowledge (in 2 times, to catch extra packets),等待网络服务器器ACK内容 */
        for (i=0; i<2; ++i) {
            j = recv(sock_up, (void *)buff_ack, sizeof buff_ack, 0);
            clock_gettime(CLOCK_MONOTONIC, &recv_time);
            if (j == -1) {
                if (errno == EAGAIN) { /* timeout */
                    continue;
                } else { /* server connection error */
                    break;
                }
            } else if ((j < 4) || (buff_ack[0] != PROTOCOL_VERSION) || (buff_ack[3] != PKT_PUSH_ACK)) {
                //MSG("WARNING: [up] ignored invalid non-ACL packet\n");
                continue;
            } else if ((buff_ack[1] != token_h) || (buff_ack[2] != token_l)) {
                //MSG("WARNING: [up] ignored out-of sync ACK packet\n");
                continue;
            } else {
                MSG("INFO: [up] PUSH_ACK received in %i ms\n", (int)(1000 * difftimespec(recv_time, send_time)));
                meas_up_ack_rcv += 1;
                break;
            }
        }

	}
}

此线程主体内容就这么多,我们只了解程序基本框架功能,关于定位时间的描述会有专门文章来分析。

2.4 函数 lgw_receive() 函数实现功能:

源码路径@libloragw/loragw_hal.c

int lgw_receive(uint8_t max_pkt, struct lgw_pkt_rx_s *pkt_data) {
	 /* Get packets from SX1302, if any */
    res = sx1302_fetch(&nb_pkt_fetched);

    /* WARNING: this needs to be called regularly by the upper layer */
    res = sx1302_update();

    /* Apply RSSI temperature compensation */
    res = lgw_get_temperature(&current_temperature);

    res = sx1302_parse(&lgw_context, &pkt_data[nb_pkt_found]);

    /* Appli RSSI offset calibrated for the board */
    pkt_data[nb_pkt_found].rssic += CONTEXT_RF_CHAIN[pkt_data[nb_pkt_found].rf_chain].rssi_offset;
    pkt_data[nb_pkt_found].rssis += CONTEXT_RF_CHAIN[pkt_data[nb_pkt_found].rf_chain].rssi_offset;

    rssi_temperature_offset = sx1302_rssi_get_temperature_offset(&CONTEXT_RF_CHAIN[pkt_data[nb_pkt_found].rf_chain].rssi_tcomp, current_temperature);
    pkt_data[nb_pkt_found].rssic += rssi_temperature_offset;
    pkt_data[nb_pkt_found].rssis += rssi_temperature_offset;

    res = merge_packets(pkt_data, &nb_pkt_found);
}

此代码删减后内容、如上,基本功能总结如下:
1>. 获取 sx1302 的数据报状态, sx1302_fetch();
2>. 更新接收数据包的时间戳, sx1302_update();
3>. 获取电路板温度值,用以RSSI 温度补偿使用;
4>. 解析lora数据包内容,sx1302_parse();
5>. 双包数据合并优化接收时间戳, merge_packets().

2.5 payload[] 数据帧格式

在 sx1302_fetch() 函数中、读取Lora模块上报数据帧内容,其程序调用过程如下:

sx1302_fetch(uint8_t * nb_pkt)
  ==> err = rx_buffer_fetch(&rx_buffer);

源码路径@libloragw/loragw_sx1302_rx.c

int rx_buffer_fetch(rx_buffer_t * self) {
    int i, res;
    uint8_t buff[2];
    uint8_t payload_len;
    uint16_t next_pkt_idx;
    int idx;
    uint16_t nb_bytes_1, nb_bytes_2;

    /* Check input params */
    CHECK_NULL(self);

    /* Check if there is data in the FIFO */
    lgw_reg_rb(SX1302_REG_RX_TOP_RX_BUFFER_NB_BYTES_MSB_RX_BUFFER_NB_BYTES, buff, sizeof buff);
    nb_bytes_1 = (buff[0] << 8) | (buff[1] << 0);

    /* Workaround for multi-byte read issue: read again and ensure new read is not lower than the previous one */
    lgw_reg_rb(SX1302_REG_RX_TOP_RX_BUFFER_NB_BYTES_MSB_RX_BUFFER_NB_BYTES, buff, sizeof buff);
    nb_bytes_2 = (buff[0] << 8) | (buff[1] << 0);

    self->buffer_size = (nb_bytes_2 > nb_bytes_1) ? nb_bytes_2 : nb_bytes_1;

    /* Fetch bytes from fifo if any */
    if (self->buffer_size > 0) {
        DEBUG_MSG   ("-----------------\n");
        DEBUG_PRINTF("%s: nb_bytes to be fetched: %u (%u %u)\n", __FUNCTION__, self->buffer_size, buff[1], buff[0]);

        memset(self->buffer, 0, sizeof self->buffer);
        res = lgw_mem_rb(0x4000, self->buffer, self->buffer_size, true);     //> 读取数据内容
        if (res != LGW_REG_SUCCESS) {
            printf("ERROR: Failed to read RX buffer, SPI error\n");
            return LGW_REG_ERROR;
        }

        /* print debug info */
        DEBUG_MSG("RX_BUFFER: ");
        for (i = 0; i < self->buffer_size; i++) {
            DEBUG_PRINTF("%02X ", self->buffer[i]);
        }
        DEBUG_MSG("\n");

        /* Sanity check: is there at least 1 complete packet in the buffer */
        if (self->buffer_size < (SX1302_PKT_HEAD_METADATA + SX1302_PKT_TAIL_METADATA)) {   //> 判断数据长度合法性
            printf("WARNING: not enough data to have a complete packet, discard rx_buffer\n");
            return rx_buffer_del(self);
        }

        /* Sanity check: is there a syncword at 0 ? If not, move to the first syncword found */
        idx = 0;
        while (idx <= (self->buffer_size - 2)) {                                             //> 数据同步头合法性
            if ((self->buffer[idx] == SX1302_PKT_SYNCWORD_BYTE_0) && (self->buffer[idx + 1] == SX1302_PKT_SYNCWORD_BYTE_1)) {
                DEBUG_PRINTF("INFO: syncword found at idx %d\n", idx);
                break;
            } else {
                printf("INFO: syncword not found at idx %d\n", idx);
                idx += 1;
            }
        }

        if (idx > self->buffer_size - 2) {
            printf("WARNING: no syncword found, discard rx_buffer\n");
            return rx_buffer_del(self);
        }

        if (idx != 0) {
            printf("INFO: re-sync rx_buffer at idx %d\n", idx);
            memmove((void *)(self->buffer), (void *)(self->buffer + idx), self->buffer_size - idx);
            self->buffer_size -= idx;
        }

        /* Rewind and parse buffer to get the number of packet fetched */
        idx = 0;
        while (idx < self->buffer_size) {
            if ((self->buffer[idx] != SX1302_PKT_SYNCWORD_BYTE_0) || (self->buffer[idx + 1] != SX1302_PKT_SYNCWORD_BYTE_1)) {
                printf("WARNING: syncword not found at idx %d, discard the rx_buffer\n", idx);
                return rx_buffer_del(self);
            }
            /* One packet found in the buffer */
            self->buffer_pkt_nb += 1;

            /* Compute the number of bytes for this packet, 获取数据包内容 */
            payload_len = SX1302_PKT_PAYLOAD_LENGTH(self->buffer, idx);
            next_pkt_idx =  SX1302_PKT_HEAD_METADATA +
                            payload_len +
                            SX1302_PKT_TAIL_METADATA +
                            (2 * SX1302_PKT_NUM_TS_METRICS(self->buffer, idx + payload_len));

            /* Move to next packet */
            idx += (int)next_pkt_idx;
        }

    }

    /* Initialize the current buffer index to iterate on */
    self->buffer_index = 0;
    return LGW_REG_SUCCESS;
}

从 SX1302 中读取 Lora 无线接收到的数据内容。

第3节 总结

本篇内容就分析线程 thread_up 部分内容、下一篇介绍 thread_down 线程内容;然后笔者将参考loraWan的协议,进一步梳理对照此两块内容。
如果本篇文章对您有所启发或帮助、请给笔者点赞助力、鼓励笔者坚持把此系列内容尽快梳理、分享出来。谢谢。

后言

今天 csdn 给发了一篇纪念开博三周年的短文,有点小感动,继续耕耘.

 类似资料: