Browse Source

新增EC200U-CN 4G-LET模组支持

niujiuru 1 tháng trước cách đây
mục cha
commit
9ac1c5ad44
7 tập tin đã thay đổi với 1163 bổ sung0 xóa
  1. 42 0
      ec200u/Makefile
  2. 88 0
      ec200u/bridge.go
  3. 414 0
      ec200u/ec200u.c
  4. 42 0
      ec200u/ec200u.h
  5. 60 0
      ec200u/ec200u_test.c
  6. 263 0
      ec200u/en200u.go
  7. 254 0
      ec200u/usb0net.go

+ 42 - 0
ec200u/Makefile

@@ -0,0 +1,42 @@
+.PHONY: build clean
+
+# 头文件
+INCS += -I.
+INCS += -I../swapi
+INCS += -I./include
+
+# 源文件
+SRCS += $(filter-out ../swapi/testLib.c, $(wildcard ../swapi/*.c))
+SRCS += ../swapi/subjects/serial/serial.c ec200u.c
+
+# .o文件
+OBJS = $(SRCS:.c=.o)
+
+# 编译器
+CC = gcc
+CFLAGS = -Wall -fPIC -O2 -g
+DEFINS = -D_GNU_SOURCE
+
+target ?= x86_64
+ifeq ($(target),armv7hf)
+  CC := arm-linux-gnueabihf-gcc
+	AR := arm-linux-gnueabihf-ar
+endif
+
+# 库文件
+LIBS += -Wl,-Bdynamic -lc -lm -ldl -lpthread
+
+# 编译和清理
+build : libec200u.a ec200u_test.out
+
+%.o : %.c
+	$(CC) $(DEFINS) $(CFLAGS) -c $< $(INCS) -o $@
+
+libec200u.a : $(OBJS)
+	$(AR) -cr $@ $(OBJS)
+
+ec200u_test.out : $(OBJS)
+	$(CC) $(DEFINS) $(CFLAGS) $(OBJS) ec200u_test.c $(INCS) $(LIBS) -o $@
+
+clean :
+	rm -rf $(OBJS) *.out *.a config/ log/ status/ var/

+ 88 - 0
ec200u/bridge.go

@@ -0,0 +1,88 @@
+// Author: NiuJiuRu
+// Email: niujiuru@qq.com
+
+package ec200u
+
+/*
+#include "ec200u.h"
+*/
+import "C"
+
+import (
+	"fmt"
+	"strconv"
+	"unsafe"
+)
+
+// 打开与模块的通讯
+func ec200U_ComInit() (int, error) {
+	ret := int(C.EC200U_ComInit())
+	if ret != 0 {
+		return ret, fmt.Errorf("an error occurred while calling the C.EC200U_ComInit() function(%d)", ret)
+	}
+	return 0, nil
+}
+
+// 关闭与模块的通讯
+func ec200U_ComExit() error {
+	ret := int(C.EC200U_ComExit())
+	if ret != 0 {
+		return fmt.Errorf("an error occurred while calling the C.EC200U_ComExit() function(%d)", ret)
+	}
+	return nil
+}
+
+func ec200U_Repower() error {
+	ret := int(C.EC200U_Repower())
+	if ret != 0 {
+		return fmt.Errorf("an error occurred while calling the C.EC200U_Repower() function(%d)", ret)
+	}
+	return nil
+}
+
+// 获取模块的标识号
+func ec200U_GetIMEI() (string, error) {
+	var buf [16]byte
+	ret := int(C.EC200U_GetIMEI((*C.char)(unsafe.Pointer(&buf[0]))))
+	if ret != 15 {
+		return "", fmt.Errorf("an error occurred while calling the C.EC200U_GetIMEI() function(%d)", ret)
+	}
+	return C.GoString((*C.char)(unsafe.Pointer(&buf[0]))), nil
+}
+
+// 获取网络注册状态
+func ec200U_GetCregState() (int, error) {
+	ret := int(C.EC200U_GetCregState())
+	if ret < 0 {
+		return -1, fmt.Errorf("an error occurred while calling the C.EC200U_GetCregState() function(%d)", ret)
+	}
+	return ret, nil
+}
+
+// 获取当前信号强度
+func ec200U_GetRSSIFromCSQ() (string, error) {
+	ret := int(C.EC200U_GetRSSIFromCSQ())
+	if ret < 0 {
+		return "", fmt.Errorf("an error occurred while calling the C.EC200U_GetRSSIFromCSQ() function(%d)", ret)
+	}
+	return strconv.Itoa(ret), nil
+}
+
+// 返回电话卡的状态
+func ec200U_IsSimCardReady() (bool, error) {
+	ret := int(C.EC200U_IsSimCardReady())
+	if ret < 0 {
+		return false, fmt.Errorf("an error occurred while calling the C.EC200U_IsSimCardReady() function(%d)", ret)
+	}
+	return ret == 1, nil
+}
+
+// 获取电话卡标识号
+func ec200U_GetSimICCID() (string, error) {
+	var buf [21]byte
+	ret := int(C.EC200U_GetSimICCID((*C.char)(unsafe.Pointer(&buf[0]))))
+	if ret != 20 {
+		return "", fmt.Errorf("an error occurred while calling the C.EC200U_GetSimICCID() function(%d)", ret)
+	}
+	return C.GoString((*C.char)(unsafe.Pointer(&buf[0]))), nil
+}

+ 414 - 0
ec200u/ec200u.c

@@ -0,0 +1,414 @@
+#include "ec200u.h"
+#include "../swapi/subjects/serial/serial.h"
+
+// 模块名称
+static const char MODULE_NAME[] = "EC200U";
+
+#define MAX_CMD_CHARS    65535 // 单条指令最大字符数
+#define MAX_ACK_CHARS    65535 // 指令应答最大字符数
+#define WAIT_ACK_TIMEOUT 9000  // 等待应答超时时间ms
+#define R_SILENT_TIMEOUT 50    // 应答静默超时时间ms
+
+// 关闭命令回显模式
+static const char *AT_USE_ECHO   = "ATE0"    ;
+// 获取模块的IMEI号
+static const char *AT_GET_IMEI   = "AT+CGSN" ;
+// 查看网络注册状态
+static const char *AT_GET_CREG   = "AT+CREG?";
+// 获取当前信号强度
+static const char *AT_GET_CSQ    = "AT+CSQ"  ;
+// 获取SIM-PIN状态
+static const char *AT_GET_PINSTA = "AT+CPIN?";
+// 获取SIM-ICCID号
+static const char *AT_GET_ICCID  = "AT+ICCID";
+// 激活USB网络模式
+static const char *AT_QCFG_CMD1  = "AT+QCFG=\"usbnet\",1";
+static const char *AT_QCFG_CMD2  = "AT+qnetdevctl=1,1,1" ;
+
+// AT指令请求和应答
+typedef struct
+{
+  // 指令
+  struct
+  {
+    char data[MAX_CMD_CHARS+1]; // 指令数据(字符串)
+    int len; // 指令长度
+  } cmd;
+  // 等待执行结束的超时ms
+  int timeout;
+  // 等待执行结束的信号量
+  void *hEndSgl;
+  // 应答
+  struct
+  {
+    int ret; // 返回值: >0时执行成功, 表明应答数据的长度; <0时出错(-1超时)
+    char data[MAX_ACK_CHARS+1]; // 应答数据(字符串)
+  } ack;
+} SATCmdTrans;
+
+// 与模块的串口通讯
+typedef struct
+{
+  void *h;             // 打开的串口句柄
+  void *do_cmd_mutex;  // 指令执行互斥量, 同一时刻正在执行的指令只能有一条
+  SATCmdTrans *pTrans; // 执行的指令及其应答结果: 空闲时为空, 工作时不为空
+} SEC200UCom;
+
+static SEC200UCom s_myCom;
+
+// 定义AT指令格式符
+#define CR   '\r'      // 回车
+#define LF   '\n'      // 换行
+#define CRLF "\r\n"    // 回车换行
+#define ATOK "\r\nOK\r\n" // 应答成功
+#define ATER "ERROR"   // 应答失败
+
+// 执行一条指令, 返回: 大于0成功; 小于0失败(-1命令超时)
+static int comio_doCmd(SATCmdTrans *pTrans/*要执行的指令(IN&OUT)*/, int timeout/*等待应答的超时时间, 单位是毫秒*/)
+{
+  SEC200UCom *pComIO = &s_myCom; const char *log_prefix = serial_get_log_prefix(pComIO->h);
+  unsigned long stime, wtime, etime; char *pAtCmdStr; int atCmdLen, ret;
+
+  xgettickcount(&stime);
+  ret = sw_mutex_lock(pComIO->do_cmd_mutex, timeout);
+  if(ret != 0) { pTrans->ack.ret = -1; goto end_p; } // 互斥量上锁超时
+  xgettickcount(&etime); wtime = etime - stime;
+
+  pTrans->hEndSgl = sw_signal_create();
+  if(!pTrans->hEndSgl) { pTrans->ack.ret = -2; goto end_p; }
+
+  pAtCmdStr = (char *)sw_heap_malloc(pTrans->cmd.len+2);
+  if(!pAtCmdStr) { pTrans->ack.ret = -3; goto end_p; }
+  memcpy(pAtCmdStr, pTrans->cmd.data, pTrans->cmd.len);
+  pAtCmdStr[pTrans->cmd.len] = CR; pAtCmdStr[pTrans->cmd.len+1] = '\0';
+  atCmdLen = strlen((const char *)pAtCmdStr);
+
+  serial_recvThrd_pause(pComIO->h); sw_thrd_delay(THRDWAIT_DELAY); // "pComIO->pTrans"的控制权转移前, 先暂停串口数据的接收
+  ret = serial_send_data(pComIO->h, (unsigned char *)pAtCmdStr, atCmdLen); // 发送一条指令
+  sw_heap_free(pAtCmdStr); // 释放AT指令字符串内存
+  if(ret == atCmdLen)
+  {
+    sw_log_trace("[%s] %s send a cmd(%d bytes): %s", MODULE_NAME, log_prefix, pTrans->cmd.len, pTrans->cmd.data);
+  }
+  else
+  {
+    sw_log_error("[%s] %s failed to send a cmd(%d bytes, ret=%d): %s!!", MODULE_NAME, log_prefix, pTrans->cmd.len, ret, pTrans->cmd.data);
+    serial_recvThrd_resume(pComIO->h); pTrans->ack.ret = -4; goto end_p;
+  }
+
+  pTrans->timeout = (wtime > timeout ? 0 : (timeout - wtime));
+  pComIO->pTrans = pTrans;  serial_recvThrd_resume(pComIO->h); // 恢复串口数据接收, "pComIO->pTrans"的控制权转移给接收线程.
+  sw_signal_wait(pTrans->hEndSgl, WAIT_FOREVER); // 等待指令执行结束, "pComIO->pTrans"的控制权收回
+
+end_p:
+  if(pTrans->hEndSgl) sw_signal_destroy(pTrans->hEndSgl);
+  pComIO->pTrans = NULL;
+  sw_mutex_unlock(pComIO->do_cmd_mutex);
+  return pTrans->ack.ret;
+}
+
+// 接收、处理来自EC200U模块的数据报文帧, 串口-线程回调
+static int comio_data_recv_proc(unsigned long wParam/*传递打开的串口句柄*/, unsigned long lParam/*保留暂未使用*/)
+{
+  SEC200UCom *pComIO = &s_myCom; void *pSerial = pComIO->h; SATCmdTrans *pTrans = pComIO->pTrans;
+  const unsigned char *pRecvBuf = serial_get_recv_buffer(pSerial); int nRecvBytes = serial_get_recv_buffer_bytes(pSerial);
+  const char *log_prefix = serial_get_log_prefix(pSerial); bool bPrintRecvBuf = true;
+  static unsigned long enteredWaitAckTime = 0, now; static unsigned long enteredSilentTime = 0; static int nSilentRecvBytes = 0;
+
+  if(!pTrans) { goto ret_p2; } // 无主动指令时
+
+  // 1, 计算命令应答超时
+  if(enteredWaitAckTime == 0) xgettickcount(&enteredWaitAckTime);
+  xgettickcount(&now);
+
+  // 2, 分析应答回复内容
+  if(strstr((const char *)pRecvBuf, ATOK) != NULL || strstr((const char *)pRecvBuf, ATER) != NULL)
+  { // 已应答
+    if(nRecvBytes > nSilentRecvBytes) { enteredSilentTime = now; nSilentRecvBytes = nRecvBytes; }
+    if((now-enteredSilentTime) < R_SILENT_TIMEOUT) { goto ret_p3; } // 等待静默期结束(延期多接收一会数据, 避免"内容"在"ATOK"或"ATER"之后)
+    memcpy(pTrans->ack.data, pRecvBuf, nRecvBytes);
+    pTrans->ack.ret = nRecvBytes; pTrans->ack.data[pTrans->ack.ret] = '\0'; bPrintRecvBuf = false;
+    sw_log_trace("[%s] %s received a cmd ack(%d bytes): %s", MODULE_NAME, log_prefix, pTrans->ack.ret, pTrans->ack.data);
+    goto ret_p1;
+  }
+  else if((now-enteredWaitAckTime) >= pTrans->timeout)
+  { // 已超时
+    sw_log_error("[%s] %s timeout occurred while waiting for the \"%s\" command's ack!!", MODULE_NAME, log_prefix, pTrans->cmd.data);
+    pTrans->ack.ret = -1; pTrans->ack.data[0] = '\0';
+    goto ret_p1;
+  }
+  else
+  { // 继续收
+    goto ret_p3;
+  }
+
+  // 3, 缓存数据接收控制
+ret_p1: // 点亮信号量, 表明本指令执行结束
+  sw_signal_give(pComIO->pTrans->hEndSgl); enteredWaitAckTime = 0; enteredSilentTime = 0; nSilentRecvBytes = 0;
+  goto ret_p2;
+
+ret_p2: // 清接收缓存, 重新开始一轮新指令
+  if(nRecvBytes > 0 && bPrintRecvBuf)
+  {
+    serial_printf_recv_buffer(pSerial, LEVEL_TRACE); // 十六进制打印当前串口接收数据缓存
+    sw_log_warn("[%s] %s discarded %d bytes of meaningless data!", MODULE_NAME, log_prefix, nRecvBytes);
+  }
+  if(nRecvBytes > 0) serial_clear_recv_buffer(pSerial);
+  goto ret_p3;
+
+ret_p3: return 1; // 持续累加的接收新数据
+}
+
+// 打开与模块的通讯, 返回: 0成功, <0时失败
+int EC200U_ComInit()
+{
+  s_myCom.do_cmd_mutex = sw_mutex_create();
+  if(!s_myCom.do_cmd_mutex) { sw_log_error("[%s] error to create a mutex!!", MODULE_NAME); return -1; }
+  s_myCom.pTrans = NULL;
+
+  const char *serialName = "/dev/ttyUSB0"; int baudrate = 115200;
+  const char *parityCheck = "none"; // 无校检
+  s_myCom.h = serial_open(serialName, baudrate, parityCheck, \
+                          comio_data_recv_proc, comio_data_recv_proc, NULL);
+  if(!s_myCom.h)
+  {
+    sw_log_error("[%s] failed to open the \"%s:%d(%s parity)\" device!!", \
+                 MODULE_NAME, serialName, baudrate, parityCheck);
+    sw_mutex_destroy(s_myCom.do_cmd_mutex); return -1;
+  }
+
+  SATCmdTrans trans; memset(&trans, 0, sizeof(SATCmdTrans));
+  strcpy(trans.cmd.data, AT_USE_ECHO); trans.cmd.len = strlen(trans.cmd.data);
+  int ret = comio_doCmd(&trans, WAIT_ACK_TIMEOUT);
+  if(!(ret > 0 && strstr(trans.ack.data, ATOK)))
+  {
+    sw_log_error("[%s] failed to close echo mode!!", MODULE_NAME);
+    EC200U_ComExit(); return -2+ret;
+  }
+
+  memset(&trans, 0, sizeof(SATCmdTrans));
+  strcpy(trans.cmd.data, AT_QCFG_CMD1); trans.cmd.len = strlen(trans.cmd.data);
+  ret = comio_doCmd(&trans, WAIT_ACK_TIMEOUT);
+  if(!(ret > 0 && strstr(trans.ack.data, ATOK)))
+  {
+    sw_log_error("[%s] failed to configure USB network mode!!", MODULE_NAME);
+    EC200U_ComExit(); return -3+ret;
+  }
+
+  memset(&trans, 0, sizeof(SATCmdTrans));
+  strcpy(trans.cmd.data, AT_QCFG_CMD2); trans.cmd.len = strlen(trans.cmd.data);
+  ret = comio_doCmd(&trans, WAIT_ACK_TIMEOUT);
+  if(!(ret > 0 && strstr(trans.ack.data, ATOK)))
+  {
+    sw_log_error("[%s] failed to activate USB network mode!!", MODULE_NAME);
+    EC200U_ComExit(); return -4+ret;
+  }
+
+  return 0;
+}
+
+// 关闭与模块的通讯, 返回: 0成功, <0时失败
+int EC200U_ComExit()
+{
+  if(s_myCom.h) serial_close(s_myCom.h, WAITTHRD_SAFEEXIT_TIMEOUT);
+  if(s_myCom.do_cmd_mutex) sw_mutex_destroy(s_myCom.do_cmd_mutex);
+  memset(&s_myCom, 0, sizeof(s_myCom));
+  return 0;
+}
+
+// 改写sysfs虚拟文件, 返回: 0成功, <0失败
+static int sysfs_write(const char *path, const char *value)
+{
+  int fd = open(path, O_WRONLY);
+  if (fd < 0) return -1;
+  ssize_t len = write(fd, value, strlen(value));
+  if(len != strlen(value)) { close(fd); return -2; }
+  close(fd); return 0;
+}
+
+// 控制模块断电重启, 返回: 0成功, <0时失败
+// 138脚 -- gpio4.IO[20] (4-1)*32+20 = 116 -- 4G_POWER
+int EC200U_Repower()
+{
+  sysfs_write("/sys/class/gpio/unexport",             "116");                // 撤销原导出
+  if(sysfs_write("/sys/class/gpio/export",            "116") < 0) return -2; // 重设新导出
+  if(sysfs_write("/sys/class/gpio/gpio116/direction", "out") < 0) return -3; // 设置为输出
+  if(sysfs_write("/sys/class/gpio/gpio116/value",       "1") < 0) return -4; // 输出高电平-断电
+  sw_thrd_delay(5000);                                                       // 等一段时间-毫秒
+  if(sysfs_write("/sys/class/gpio/gpio116/value",       "0") < 0) return -5; // 输出低电平-上电
+  return 0;
+}
+
+// 获取模块的标识号, 返回: =15成功, <0失败(-1命令超时)
+int EC200U_GetIMEI(char buf[16]/*固定长15个十进制数字组成*/)
+{
+  SATCmdTrans trans; memset(&trans, 0, sizeof(SATCmdTrans));
+  strcpy(trans.cmd.data, AT_GET_IMEI);
+  trans.cmd.len = strlen(trans.cmd.data);
+  int ret = comio_doCmd(&trans, WAIT_ACK_TIMEOUT);
+  if(ret < 0) return ret; // 执行超时或出错
+  else if(strstr(trans.ack.data, ATOK) == NULL) goto end_p; // 未收到成功应答
+
+  const char *p = trans.ack.data, *pLineS, *pLineE; int lineLen, crlfLen = strlen(CRLF);
+lsa_p: // 行扫描, 逐行分析应答结果
+  if((*p) == '\0') goto end_p;
+  pLineS = strstr(p, CRLF); pLineE = NULL; lineLen = 0;
+  if(pLineS) { pLineS += crlfLen; pLineE = strstr(pLineS, CRLF); }
+  if(pLineE) { lineLen = pLineE - pLineS; }
+  if(lineLen == 0)
+  { // 连续两个CRLF, 上面的逻辑保证了"lineLen"只可能大于或等于0, 不会是小于0
+    if(pLineE) { p = pLineE; goto lsa_p; } // 跳第一个CRLF, 继续分析下一行
+    else goto end_p;
+  }
+  //// 示例: "359759002514931"
+  if(lineLen == 15)
+  {
+    bool isHex = true;
+    for(int i = 0; i < lineLen; i++) if(!xisxdigit(pLineS[i])) { isHex = false; break; }
+    if(isHex) { memcpy(buf, pLineS, lineLen); buf[lineLen] = '\0'; return lineLen; } // 成功读到模块的IMEI号
+  }
+  p = pLineE + crlfLen;
+  goto lsa_p; // 继续分析下一行
+
+end_p:
+  return -5;
+}
+
+// 获取网络注册状态, 返回: >=0成功, <0失败(-1命令超时)
+int EC200U_GetCregState()
+{
+  SATCmdTrans trans; memset(&trans, 0, sizeof(SATCmdTrans));
+  strcpy(trans.cmd.data, AT_GET_CREG);
+  trans.cmd.len = strlen(trans.cmd.data);
+  int ret = comio_doCmd(&trans, WAIT_ACK_TIMEOUT);
+  if(ret < 0) return ret; // 执行超时或出错
+  else if(strstr(trans.ack.data, ATOK) == NULL) goto end_p; // 未收到成功应答
+
+  const char *p = trans.ack.data, *pLineS, *pLineE; int lineLen, crlfLen = strlen(CRLF);
+  const char *p1, *p2; char statBuf[3]; int statLen;
+lsa_p: // 行扫描, 逐行分析应答结果
+  if((*p) == '\0') goto end_p;
+  pLineS = strstr(p, CRLF); pLineE = NULL; lineLen = 0;
+  if(pLineS) { pLineS += crlfLen; pLineE = strstr(pLineS, CRLF); }
+  if(pLineE) { lineLen = pLineE - pLineS; }
+  if(lineLen == 0)
+  { // 连续两个CRLF, 上面的逻辑保证了"lineLen"只可能大于或等于0, 不会是小于0
+    if(pLineE) { p = pLineE; goto lsa_p; } // 跳第一个CRLF, 继续分析下一行
+    else goto end_p;
+  }
+  //// 示例: "+CREG: <n>,<stat>"
+  //// 说明: <n>注册状态报告模式, <stat>当前网络注册状态
+  p1 = strstr(pLineS, "+CREG:"); p2 = NULL;
+  if(p1 && p1 < pLineE) { p1 += 6; p2 = strchr(p1, ','); }
+  if(p2 && p2 < pLineE)
+  {
+    p2++; while(*p2 == 0x20 && p2 < pLineE) { p2++; } // 跳过空格
+    statLen = (pLineE - p2); if(statLen != 1 && statLen != 2) return -5; // 状态值范围: 1或2个数字
+    memcpy(statBuf, p2, statLen); statBuf[statLen] = '\0';
+    bool isDigit = true;
+    for(int i = 0; i < strlen(statBuf); i++) if(!xisdigit(statBuf[i])) { isDigit = false; break; }
+    if(isDigit) return atoi(statBuf); // 成功读到模块的网络注册状态值
+    else return -6;
+  }
+  p = pLineE + crlfLen;
+  goto lsa_p; // 继续分析下一行
+
+end_p:
+  return -7;
+}
+
+// 获取当前信号强度, 返回: >=0成功, <0失败(-1命令超时)
+int EC200U_GetRSSIFromCSQ()
+{
+  SATCmdTrans trans; memset(&trans, 0, sizeof(SATCmdTrans));
+  strcpy(trans.cmd.data, AT_GET_CSQ);
+  trans.cmd.len = strlen(trans.cmd.data);
+  int ret = comio_doCmd(&trans, WAIT_ACK_TIMEOUT);
+  if(ret < 0) return ret; // 执行超时或出错
+  else if(strstr(trans.ack.data, ATOK) == NULL) goto end_p; // 未收到成功应答
+
+  const char *p = trans.ack.data, *pLineS, *pLineE; int lineLen, crlfLen = strlen(CRLF);
+  const char *p1, *p2; char rssiBuf[3]; int rssiLen;
+lsa_p: // 行扫描, 逐行分析应答结果
+  if((*p) == '\0') goto end_p;
+  pLineS = strstr(p, CRLF); pLineE = NULL; lineLen = 0;
+  if(pLineS) { pLineS += crlfLen; pLineE = strstr(pLineS, CRLF); }
+  if(pLineE) { lineLen = pLineE - pLineS; }
+  if(lineLen == 0)
+  { // 连续两个CRLF, 上面的逻辑保证了"lineLen"只可能大于或等于0, 不会是小于0
+    if(pLineE) { p = pLineE; goto lsa_p; } // 跳第一个CRLF, 继续分析下一行
+    else goto end_p;
+  }
+  //// 示例: "+CSQ: 15,99"
+  //// 说明: 逗号前第一个数值是RSSI的值
+  p1 = strstr(pLineS, "+CSQ:"); p2 = NULL;
+  if(p1 && p1 < pLineE) { p1 += 5; p2 = strchr(p1, ','); }
+  if(p2 && p2 < pLineE)
+  {
+    while(*p1 == 0x20 && p1 < p2) { p1++; } // 跳过空格
+    rssiLen = (p2 - p1); if(rssiLen != 1 && rssiLen != 2) return -5; // 强度值范围: 0-99, 1或2个数字
+    memcpy(rssiBuf, p1, rssiLen); rssiBuf[rssiLen] = '\0';
+    bool isDigit = true;
+    for(int i = 0; i < strlen(rssiBuf); i++) if(!xisdigit(rssiBuf[i])) { isDigit = false; break; }
+    if(isDigit) return atoi(rssiBuf); // 成功读到模块的接收信号强度值
+    else return -6;
+  }
+  p = pLineE + crlfLen;
+  goto lsa_p; // 继续分析下一行
+
+end_p:
+  return -7;
+}
+
+// 返回电话卡的状态, 返回: 1已可用, <0失败(-1命令超时)
+int EC200U_IsSimCardReady()
+{
+  SATCmdTrans trans; memset(&trans, 0, sizeof(SATCmdTrans));
+  strcpy(trans.cmd.data, AT_GET_PINSTA);
+  trans.cmd.len = strlen(trans.cmd.data);
+  int ret = comio_doCmd(&trans, WAIT_ACK_TIMEOUT);
+  if(ret < 0) return ret; // 执行超时或出错
+  else if(strstr(trans.ack.data, ATOK) && strstr(trans.ack.data, "+CPIN: READY")) return 1;
+  else return -5;
+}
+
+// 获取电话卡标识号, 返回: =20成功, <0失败(-1命令超时)
+int EC200U_GetSimICCID(char buf[21]/*一般由20个十进制数字组成*/)
+{
+  SATCmdTrans trans; memset(&trans, 0, sizeof(SATCmdTrans));
+  strcpy(trans.cmd.data, AT_GET_ICCID);
+  trans.cmd.len = strlen(trans.cmd.data);
+  int ret = comio_doCmd(&trans, WAIT_ACK_TIMEOUT);
+  if(ret < 0) return ret; // 执行超时或出错
+  else if(strstr(trans.ack.data, ATOK) == NULL) goto end_p; // 未收到成功应答
+
+  const char *p = trans.ack.data, *pLineS, *pLineE; int lineLen, crlfLen = strlen(CRLF);
+  const char *p1, *p2; int iccidLen;
+lsa_p: // 行扫描, 逐行分析应答结果
+  if((*p) == '\0') goto end_p;
+  pLineS = strstr(p, CRLF); pLineE = NULL; lineLen = 0;
+  if(pLineS) { pLineS += crlfLen; pLineE = strstr(pLineS, CRLF); }
+  if(pLineE) { lineLen = pLineE - pLineS; }
+  if(lineLen == 0)
+  { // 连续两个CRLF, 上面的逻辑保证了"lineLen"只可能大于或等于0, 不会是小于0
+    if(pLineE) { p = pLineE; goto lsa_p; } // 跳第一个CRLF, 继续分析下一行
+    else goto end_p;
+  }
+  //// 示例: "+ICCID: 89860117831003134201"
+  p1 = strstr(pLineS, "+ICCID:"); p2 = NULL;
+  if(p1 && p1 < pLineE) { p1 += 7; p2 = pLineE; }
+  if(p2)
+  {
+    while(*p1 == 0x20 && p1 < p2) { p1++; } // 跳过空格
+    iccidLen = (p2 - p1); if(iccidLen != 20) return -5; // 长度不对
+    bool isHex = true;
+    for(int i = 0; i < iccidLen; i++) if(!xisxdigit(p1[i])) { isHex = false; break; }
+    if(isHex) { memcpy(buf, p1, iccidLen); buf[iccidLen] = '\0'; return iccidLen; } // 成功读到SIM卡的ICCID号
+    else return -6;
+  }
+  p = pLineE + crlfLen;
+  goto lsa_p; // 继续分析下一行
+
+end_p:
+  return -7;
+}

+ 42 - 0
ec200u/ec200u.h

@@ -0,0 +1,42 @@
+// Author: NiuJiuRu
+// Email: niujiuru@qq.com
+// Date: 2025-11-11
+#ifndef __EC200U_H__
+#define __EC200U_H__
+
+#include "../swapi/include_swapiLib.h"
+
+#ifdef __cplusplus
+extern "C"
+{
+#endif
+
+// 打开与模块的通讯, 返回: 0成功, <0时失败
+int EC200U_ComInit();
+
+// 关闭与模块的通讯, 返回: 0成功, <0时失败
+int EC200U_ComExit();
+
+// 控制模块断电重启, 返回: 0成功, <0时失败
+int EC200U_Repower();
+
+// 获取模块的标识号, 返回: =15成功, <0失败(-1命令超时)
+int EC200U_GetIMEI(char buf[16]/*固定长15个十进制数字组成*/);
+
+// 获取网络注册状态, 返回: >=0成功, <0失败(-1命令超时)
+int EC200U_GetCregState();
+
+// 获取当前信号强度, 返回: >=0成功, <0失败(-1命令超时)
+int EC200U_GetRSSIFromCSQ();
+
+// 返回电话卡的状态, 返回: 1已可用, <0失败(-1命令超时)
+int EC200U_IsSimCardReady();
+
+// 获取电话卡标识号, 返回: =20成功, <0失败(-1命令超时)
+int EC200U_GetSimICCID(char buf[21]/*一般由20个十进制数字组成*/);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* __EC200U_H__ */

+ 60 - 0
ec200u/ec200u_test.c

@@ -0,0 +1,60 @@
+// +build ignore
+
+#include "ec200u.h"
+
+// 主函数入口, 测试联网
+int main(int argc,char *argv[])
+{
+  int ret = EC200U_ComInit();
+  if(ret < 0)
+  {
+    sw_log_error("an error occurred while calling the EC200U_ComInit() function(%d)!!", ret);
+    goto end_p;
+  }
+
+  char MMEI[16] = { 0 };
+  ret = EC200U_GetIMEI(MMEI);
+  if(ret < 0)
+  {
+    sw_log_error("an error occurred while calling the EC200U_GetIMEI() function(%d)!!", ret);
+    goto end_p;
+  }
+  sw_log_info("IMEI: %s", MMEI);
+
+  ret = EC200U_GetCregState();
+  if(ret < 0)
+  {
+    sw_log_error("an error occurred while calling the EC200U_GetCregState() function(%d)!!", ret);
+    goto end_p;
+  }
+  sw_log_info("Creg State: %d", ret);
+
+  ret = EC200U_GetRSSIFromCSQ();
+  if(ret < 0)
+  {
+    sw_log_error("an error occurred while calling the EC200U_GetRSSIFromCSQ() function(%d)!!", ret);
+    goto end_p;
+  }
+  sw_log_info("RSSI: %d", ret);
+
+  ret = EC200U_IsSimCardReady();
+  if(ret < 0)
+  {
+    sw_log_error("an error occurred while calling the EC200U_IsSimCardReady() function(%d)!!", ret);
+    goto end_p;
+  }
+  sw_log_info("SIM card status: %d", ret);
+
+  char ICCID[21] = { 0 };
+  ret = EC200U_GetSimICCID(ICCID);
+  if(ret < 0)
+  {
+    sw_log_error("an error occurred while calling the EC200U_GetSimICCID() function(%d)!!", ret);
+    goto end_p;
+  }
+  sw_log_info("ICCID: %s", ICCID);
+
+end_p:
+  EC200U_ComExit();
+  return ret;
+}

+ 263 - 0
ec200u/en200u.go

@@ -0,0 +1,263 @@
+// Author: NiuJiuRu
+// Email: niujiuru@qq.com
+
+package ec200u
+
+import (
+	"os"
+	"sync"
+	"time"
+
+	"hnyfkj.com.cn/rtu/linux/baseapp"
+)
+
+const MODULE_NAME = "EC200U"
+
+var (
+	imei  string // EC200U模块的IMEI号, 唯一标识
+	rssi  string // EC200U模块当前信号强度(RSSI)
+	iccid string // EC200U模块插入SIM卡的ICCID号
+
+	mu1, mu2, mu3          sync.Mutex
+	isRunning1, isRunning2 bool
+	exitCh1, exitCh2       chan struct{}
+	wg1, wg2               sync.WaitGroup
+)
+
+// 初始化4G网络模块
+func ModuleInit(bForceRepower bool /*是否强制重启*/) bool {
+	mu1.Lock()
+	defer mu1.Unlock()
+
+	if isRunning1 {
+		return true
+	}
+
+	var err error
+	var ret int
+	var bExists, bReady bool
+
+	_, statErr := os.Stat("/dev/ttyUSB0")
+	bExists = (statErr == nil)
+
+	if bExists && !bForceRepower {
+		ret, err = ec200U_ComInit()
+		if err == nil {
+			goto openOK
+		}
+		switch ret {
+		case -1: // 串口被占用(或不存在, 正常情况下只能是被占用)
+			freeTTYUSB0()
+		case -3: // 设备无应答(初始化时会发送一条关闭回显的指令)
+			ec200U_Repower() // 重新上电
+			bExists = false  // 等待复位, 标记设备已不存在
+		}
+	} else { // 设备不存在或函数调用明确要求强制模块重新启动时
+		ec200U_Repower() //// 重新上电
+		bExists = false  //// 等待复位, 标记设备已不存在
+	}
+
+	for range 6 {
+		if baseapp.IsExit1() {
+			ec200U_ComExit()
+			return false
+		}
+		if !bExists {
+			time.Sleep(5 * time.Second) // 等待设备文件再次出现
+		}
+		ret, err = ec200U_ComInit()
+		if err != nil && ret == -1 && !bExists { // 继续等待
+			continue
+		}
+		break
+	}
+
+	if err != nil {
+		ec200U_ComExit()
+		baseapp.Logger.Errorf("[%s] 错误: %v!!", MODULE_NAME, err)
+		return false
+	}
+
+openOK:
+	imei, err = ec200U_GetIMEI()
+	if err != nil {
+		ec200U_ComExit()
+		baseapp.Logger.Errorf("[%s] 错误: %v!!", MODULE_NAME, err)
+		return false
+	}
+	baseapp.Logger.Infof("[%s] 模块IMEI: %s", MODULE_NAME, imei)
+
+	for range 30 { // 等待模块完成蜂窝网络注册, 否则RSSI读取不到, 网也不可能不去
+		if baseapp.IsExit1() {
+			ec200U_ComExit()
+			return false
+		}
+		cs, err := ec200U_GetCregState()
+		if err != nil || cs == 1 /*已注册本地网*/ || cs == 5 /*已注册漫游网*/ {
+			break
+		}
+		time.Sleep(1 * time.Second)
+	}
+
+	rssi, err = ec200U_GetRSSIFromCSQ()
+	if err != nil {
+		ec200U_ComExit()
+		baseapp.Logger.Errorf("[%s] 错误: %v!!", MODULE_NAME, err)
+		return false
+	}
+	baseapp.Logger.Infof("[%s] 模块RSSI: %s", MODULE_NAME, rssi)
+
+	bReady, err = ec200U_IsSimCardReady()
+	if err != nil {
+		ec200U_ComExit()
+		baseapp.Logger.Errorf("[%s] 错误: %v!!", MODULE_NAME, err)
+		return false
+	}
+	baseapp.Logger.Infof("[%s] SIM卡状态: %v", MODULE_NAME, bReady)
+
+	if !bReady { // 未插入SIM卡, 也不影响初始化成功, 可等待之后的热插入
+		goto initOK
+	}
+
+	iccid, err = ec200U_GetSimICCID()
+	if err != nil {
+		ec200U_ComExit()
+		baseapp.Logger.Errorf("[%s] 错误: %v!!", MODULE_NAME, err)
+		return false
+	}
+	baseapp.Logger.Infof("[%s] SIM卡ICCID: %s", MODULE_NAME, iccid)
+
+initOK:
+	exitCh1 = make(chan struct{})
+	wg1.Add(1)
+	go func() { // 启动携程循环读取4G模块的信号强度和ICCID号等信息
+		defer wg1.Done()
+		monitorEC200UInfo(exitCh1)
+	}()
+
+	isRunning1 = true
+	return isRunning1
+}
+
+// 4G调制解调器退出
+func ModuleExit() {
+	mu1.Lock()
+	defer mu1.Unlock()
+
+	if !isRunning1 {
+		return
+	}
+
+	close(exitCh1)
+	wg1.Wait() // 等待循环读取4G模块的信号强度和ICCID号等信息的携程退出
+
+	ec200U_ComExit()
+	imei, rssi, iccid = "", "", ""
+	isRunning1 = false
+}
+
+// 获取模块的标识号
+func GetIMEI() string {
+	mu2.Lock()
+	defer mu2.Unlock()
+	return imei
+}
+
+// 获取当前信号强度
+func GetRSSI() string {
+	mu2.Lock()
+	defer mu2.Unlock()
+	return rssi
+}
+
+// 获取电话卡标识号
+func GetSimICCID() string {
+	mu2.Lock()
+	defer mu2.Unlock()
+	return iccid
+}
+
+// 循环读取刷新信息
+func monitorEC200UInfo(exitCh <-chan struct{}) {
+	t := time.NewTicker(time.Duration(5) * time.Second)
+	defer t.Stop()
+	for {
+		select {
+		case <-t.C:
+			mu2.Lock()
+			rssi, _ = ec200U_GetRSSIFromCSQ()
+			if bOK, _ := ec200U_IsSimCardReady(); bOK {
+				iccid, _ = ec200U_GetSimICCID()
+			} else {
+				iccid = ""
+			}
+			mu2.Unlock()
+		case <-exitCh:
+			return
+		} // select end
+	} // for end
+}
+
+// 打开连接到4G网络
+func Start4GNetwork() bool {
+	mu3.Lock()
+	defer mu3.Unlock()
+
+	if isRunning2 {
+		return true
+	}
+
+	killAllUdhcpc()
+	err := disableEthButUSB0()
+	if err != nil {
+		baseapp.Logger.Errorf("[%s] 错误: %v!!", MODULE_NAME, err)
+		return false
+	}
+
+	bExists, _ := udhcpcUSB0Exists()
+	if bExists {
+		killUSB0Udhcpc()
+	}
+
+	err = dialupUSB0()
+	if err != nil {
+		baseapp.Logger.Errorf("[%s] 拨号连接\"eth2\"时发生错误: %v!!", MODULE_NAME, err)
+		return false
+	}
+	ipv4, mask, err := getUSB0Addr()
+	if err != nil {
+		baseapp.Logger.Errorf("[%s] 读取\"eth2\"地址时发生错误: %v!!", MODULE_NAME, err)
+		return false
+	}
+	baseapp.Logger.Infof("[%s] \"eth2\"分配的地址: %s/%s", MODULE_NAME, ipv4, mask)
+
+	exitCh2 = make(chan struct{})
+	wg2.Add(1)
+	go func() { // 启动携程守护"eth2"网卡上的 "udhcpc"后台服务进程
+		defer wg2.Done()
+		monitorUSB0Udhcpc(exitCh2)
+	}()
+
+	isRunning2 = true
+	return isRunning2
+}
+
+// 从4G网络断开连接
+func Stop4GNetwork() {
+	mu3.Lock()
+	defer mu3.Unlock()
+
+	if !isRunning2 {
+		return
+	}
+
+	close(exitCh2)
+	wg2.Wait() // 等待守护"eth2"网卡上的 "udhcpc"后台服务进程的携程退出
+
+	bExists, _ := udhcpcUSB0Exists()
+	if bExists {
+		killUSB0Udhcpc()
+	}
+
+	isRunning2 = false
+}

+ 254 - 0
ec200u/usb0net.go

@@ -0,0 +1,254 @@
+// Author: NiuJiuRu
+// Email: niujiuru@qq.com
+// 以下函数需"root"权限运行
+
+package ec200u
+
+import (
+	"fmt"
+	"io/fs"
+	"net"
+	"os"
+	"os/exec"
+	"path/filepath"
+	"strconv"
+	"strings"
+	"syscall"
+	"time"
+
+	"github.com/sirupsen/logrus"
+	"github.com/vishvananda/netlink"
+	"hnyfkj.com.cn/rtu/linux/baseapp"
+)
+
+// 只保留需要使用的"usb0"网卡
+func disableEthButUSB0() error {
+	ifaces, err := net.Interfaces()
+	if err != nil {
+		return fmt.Errorf("获取网络接口列表失败: %w", err)
+	}
+
+	for _, iface := range ifaces {
+		if !strings.HasPrefix(iface.Name, "eth") || iface.Name == "usb0" {
+			continue
+		}
+
+		cmd := exec.Command("ip", "link", "set", "dev", iface.Name, "down")
+		cmd.Stdout = baseapp.Logger.WriterLevel(logrus.DebugLevel)
+		cmd.Stderr = baseapp.Logger.WriterLevel(logrus.ErrorLevel)
+		if err := cmd.Run(); err != nil {
+			return fmt.Errorf("关闭网口\"%s\"失败: %w", iface.Name, err)
+		}
+	}
+
+	return nil
+}
+
+// 启动"usb0"网口, 使其变可用
+func enableUSB0() error {
+	cmd := exec.Command("ip", "link", "set", "dev", "usb0", "up")
+	cmd.Stdout = baseapp.Logger.WriterLevel(logrus.DebugLevel)
+	cmd.Stderr = baseapp.Logger.WriterLevel(logrus.ErrorLevel)
+	if err := cmd.Run(); err != nil {
+		return fmt.Errorf("启动网口\"usb0\"失败: %w", err)
+	}
+	return nil
+}
+
+// 启动DHCP客户端, 请求IP地址
+func dialupUSB0() error {
+	if err := enableUSB0(); err != nil {
+		return err
+	}
+
+	usb0pid := baseapp.RUN_DIR + "/udhcpc.usb0.pid"
+	cmd := exec.Command("udhcpc", "-b", "-i", "usb0", "-p", usb0pid)
+	cmd.Stdout = baseapp.Logger.WriterLevel(logrus.DebugLevel)
+	cmd.Stderr = baseapp.Logger.WriterLevel(logrus.ErrorLevel)
+	if err := cmd.Run(); err != nil {
+		return fmt.Errorf("\"usb0\"请求地址失败: %w", err)
+	}
+
+	return nil
+}
+
+// 检测"usb0"网口上的"udhcpc"后台进程是否正在运行, 只保持一个实例
+func udhcpcUSB0Exists() (bool, error) {
+	usb0pid := baseapp.RUN_DIR + "/udhcpc.usb0.pid"
+	data, err := os.ReadFile(usb0pid)
+	if err != nil {
+		return false, err
+	}
+	pid, err := strconv.Atoi(strings.TrimSpace(string(data)))
+	if err != nil {
+		return false, err
+	}
+	err = syscall.Kill(pid, 0)
+	if err == nil { // 存在
+		return true, nil
+	}
+	if err == syscall.ESRCH { // 不存在
+		return false, nil
+	}
+	return false, err
+}
+
+// 杀死"usb0"网口上的"udhcpc"进程(根据上次运行时它的PID-文件记录)
+func killUSB0Udhcpc() error {
+	usb0pid := baseapp.RUN_DIR + "/udhcpc.usb0.pid"
+	data, err := os.ReadFile(usb0pid)
+	if err != nil {
+		return err
+	}
+	pid, err := strconv.Atoi(strings.TrimSpace(string(data)))
+	if err != nil {
+		return err
+	}
+	if err := syscall.Kill(pid, syscall.SIGTERM); err != nil && err != syscall.ESRCH {
+		syscall.Kill(pid, syscall.SIGKILL)
+	}
+	return nil
+}
+
+// 强制杀死所有运行中的"udhcpc"进程,包括驻留在后台运行的(SIGKILL)
+func killAllUdhcpc() {
+	cmd := exec.Command("sh", "-c", `ps | grep '[u]dhcpc' | awk '{print $1}'`)
+	output, err := cmd.Output()
+	if err != nil {
+		return // 没找到也不是错误
+	}
+
+	pids := strings.FieldsSeq(string(output))
+	for pidStr := range pids {
+		pid, err := strconv.Atoi(pidStr)
+		if err != nil {
+			continue
+		}
+		if err := syscall.Kill(pid, syscall.SIGTERM); err != nil && err != syscall.ESRCH {
+			syscall.Kill(pid, syscall.SIGKILL)
+		}
+	}
+}
+
+// 运行"udhcpc"后, 获取"usb0"网口分配到的IPv4地址, 并校检其合法性
+func getUSB0Addr() (ip, mask string, err error) {
+	iface, err := net.InterfaceByName("usb0")
+	if err != nil {
+		return "", "", fmt.Errorf("找不到网络接口\"usb0\": %w", err)
+	}
+
+	addrs, err := iface.Addrs()
+	if err != nil {
+		return "", "", fmt.Errorf("获取\"usb0\"的地址失败: %w", err)
+	}
+
+	for _, addr := range addrs {
+		ipNet, ok := addr.(*net.IPNet)
+		if !ok {
+			continue
+		}
+
+		ip4 := ipNet.IP.To4()
+		if ip4 == nil {
+			continue
+		}
+
+		if ip4.IsPrivate() { // 判断得到IP地址是否合法
+			return ip4.String(), net.IP(ipNet.Mask).String(), nil
+		} else {
+			return "", "", fmt.Errorf("分配给\"usb0\"的地址\"%s\"无效", ip4.String())
+		}
+	}
+
+	return "", "", fmt.Errorf("在\"usb0\"上未找到有效的IPv4 地址")
+}
+
+// 查找并杀死所有占用"/dev/ttyUSB0"设备的进程,返回被杀死进程的PID
+func freeTTYUSB0() ([]int, error) {
+	var pids []int
+	seen := make(map[int]struct{})
+
+	err := filepath.WalkDir("/proc", func(path string, d fs.DirEntry, walkErr error) error {
+		if walkErr != nil {
+			return nil
+		}
+		if filepath.Base(filepath.Dir(path)) != "fd" {
+			return nil
+		}
+		link, err := os.Readlink(path)
+		if err != nil || link != "/dev/ttyUSB0" {
+			return nil
+		}
+		parts := strings.Split(path, "/")
+		if len(parts) < 3 {
+			return nil
+		}
+		pid, err := strconv.Atoi(parts[2])
+		if err != nil {
+			return nil
+		}
+		if _, exists := seen[pid]; exists {
+			return nil // 已处理过这个PID, 跳过
+		}
+		seen[pid] = struct{}{}
+
+		if err := syscall.Kill(pid, syscall.SIGTERM); err != nil && err != syscall.ESRCH {
+			syscall.Kill(pid, syscall.SIGKILL)
+		}
+
+		pids = append(pids, pid)
+		return nil
+	})
+
+	return pids, err
+}
+
+// 定时检测"usb0"网卡上的"udhcpc"后台服务进程, 发现退出时自动拉起
+func monitorUSB0Udhcpc(exitCh <-chan struct{}) {
+	t := time.NewTicker(time.Duration(5) * time.Second)
+	defer t.Stop()
+
+	for {
+		select {
+		case <-t.C:
+			bExists, _ := udhcpcUSB0Exists()
+			if !bExists { // 清场一次
+				killAllUdhcpc()
+				disableEthButUSB0()
+			}
+			if !bExists && dialupUSB0() == nil { // 重新进场
+				ip, mask, _ := getUSB0Addr()
+				baseapp.Logger.Warnf("[%s] \"usb0\"重新分配的地址: %s/%s!", MODULE_NAME, ip, mask)
+			}
+		case <-exitCh:
+			return
+		} // select end
+	} // for end
+}
+
+func isInterfaceUp(name string) (bool, error) {
+	link, err := netlink.LinkByName(name)
+	if err != nil {
+		return false, err
+	}
+	return link.Attrs().Flags&net.FlagUp != 0, nil
+}
+
+func Is4GCableConnected() (bool, error) {
+	if up, _ := isInterfaceUp("usb0"); !up { // 管理状态
+		err := enableUSB0()
+		if err != nil {
+			return false, err
+		}
+	}
+
+	if data, err := os.ReadFile("/sys/class/net/usb0/carrier"); err == nil { // 物理状态
+		return strings.TrimSpace(string(data)) == "1", nil
+	}
+
+	link, err := netlink.LinkByName("usb0")
+	if err != nil {
+		return false, err
+	}
+	return link.Attrs().OperState == netlink.OperUp, nil // 操作状态
+}