excloud.lua 85 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203220422052206220722082209221022112212221322142215221622172218221922202221222222232224222522262227222822292230223122322233223422352236223722382239224022412242224322442245224622472248224922502251225222532254225522562257225822592260226122622263226422652266226722682269227022712272227322742275227622772278227922802281228222832284228522862287228822892290229122922293229422952296229722982299230023012302230323042305230623072308230923102311231223132314231523162317231823192320232123222323232423252326232723282329233023312332233323342335233623372338233923402341234223432344234523462347234823492350235123522353235423552356235723582359236023612362236323642365236623672368236923702371237223732374237523762377237823792380238123822383238423852386238723882389239023912392239323942395239623972398239924002401240224032404240524062407240824092410
  1. --[[
  2. @summary excloud扩展库
  3. @version 1.0
  4. @date 2025.09.22
  5. @author 孟伟
  6. @usage
  7. -- 应用场景
  8. 该扩展库适用于各种物联网设备(如4G/WiFi/以太网设备)与云端服务器进行数据交互的场景。
  9. 可用于设备状态上报、数据采集、远程控制等物联网应用。
  10. 实现的功能:
  11. 1. 支持多种设备类型(4G/WiFi/以太网)的接入认证
  12. 2. 提供TCP和MQTT两种传输协议选择
  13. 3. 实现设备与云端的双向通信(数据上报和命令下发)
  14. 4. 支持数据的TLV格式编解码
  15. 5. 提供自动重连机制,保证连接稳定性
  16. 6. 支持不同数据类型(整数、浮点数、布尔值、字符串、二进制等)的传输
  17. -- 用法实例
  18. 本扩展库对外提供了以下6个接口:
  19. 1. excloud.setup(params) - 设置配置参数
  20. 2. excloud.on(cbfunc) - 注册回调函数
  21. 3. excloud.open() - 开启excloud服务
  22. 4. excloud.send(data, need_reply, is_auth_msg) - 发送数据
  23. 5. excloud.close() - 关闭excloud服务
  24. 6. excloud.status() - 获取当前状态
  25. 7. excloud.start_heartbeat(interval, custom_data) - 启动自动心跳机制,定期向云平台发送心跳消息;
  26. 8. excloud.stop_heartbeat() -停止自动心跳机制;
  27. 9. excloud.upload_image(file_path, file_name) -上传图片文件到云平台;注意,需要启用getip服务
  28. 10. excloud.upload_audio(file_path, file_name) -上传音频文件到云平台;注意,需要启用getip服务
  29. 11. excloud.get_server_info() - 获取getip获取的服务器信息
  30. 12. excloud.mtn_log(tag, ...) - 记录运维日志;
  31. ]]
  32. local excloud = {}
  33. local httpplus = require "httpplus"
  34. local exmtn = require "exmtn"
  35. local config = {
  36. device_type = 1, -- 默认设备类型: 4G
  37. device_id = "", -- 设备ID
  38. protocol_version = 1, -- 协议版本
  39. transport = "", -- 传输协议: tcp/mqtt
  40. host = "", -- 服务器地址
  41. port = nil, -- 服务器端口
  42. auth_key = nil, -- 用户鉴权密钥
  43. keepalive = 300, -- mqtt心跳
  44. auto_reconnect = true, -- 是否自动重连
  45. reconnect_interval = 10, -- 重连间隔(秒)
  46. max_reconnect = 3, -- 最大重连次数
  47. timeout = 30, -- 连接超时时间(秒)
  48. qos = 0, -- MQTT QoS等级
  49. retain = 0, -- MQTT retain标志
  50. clean_session = true, -- MQTT clean session标志
  51. ssl = false, -- 是否使用SSL
  52. username = nil, -- MQTT用户名
  53. password = nil, -- MQTT密码
  54. udp_auth_key = nil, -- UDP鉴权密钥
  55. -- 新增socket配置参数
  56. local_port = nil, -- 本地端口号,nil表示自动分配
  57. keep_idle = nil, -- TCP keepalive idle时间(秒)
  58. keep_interval = nil, -- TCP keepalive 探测间隔(秒)
  59. keep_cnt = nil, -- TCP keepalive 探测次数
  60. server_cert = nil, -- 服务器CA证书数据
  61. client_cert = nil, -- 客户端证书数据
  62. client_key = nil, -- 客户端私钥数据
  63. client_password = nil, -- 客户端私钥口令
  64. use_getip = true, -- 是否使用getip服务发现,默认为true
  65. -- MQTT扩展参数
  66. -- mqtt_rx_size = 32 * 1024, -- MQTT接收缓冲区大小,默认32K
  67. -- mqtt_conn_timeout = 30, -- MQTT连接超时时间
  68. -- mqtt_ipv6 = false, -- 是否使用IPv6连接
  69. -- getip相关配置
  70. getip_url = "https://gps.openluat.com/iam/iot/getip", -- 根据协议修正URL
  71. current_conninfo = {}, -- 当前连接信息
  72. current_imginfo = nil, -- 当前图片上传信息
  73. current_audinfo = nil, -- 当前音频上传信息
  74. current_mtninfo = nil, -- 新增:运维日志上传信息
  75. getip_retry_count = 0, -- getip重试次数
  76. max_getip_retry = 3, -- 最大getip重试次数
  77. -- 虚拟设备相关配置
  78. virtual_phone_number = nil, -- 手机号
  79. virtual_serial_num = 0, -- 序列号(0-999)
  80. -- 运维日志配置
  81. mtn_log_enabled = false, -- 是否启用运维日志
  82. aircloud_mtn_log_enabled = false, -- 是否启用aircloud运维日志:true-开启,false-关闭;开启后设备认证/重连等关键事件会自动记录到运维日志文件,便于云端统一收集分析
  83. mtn_log_blocks = 1, -- 每个文件的块数
  84. mtn_log_write_way = exmtn.CACHE_WRITE, -- 写入方式
  85. }
  86. local callback_func = nil -- 回调函数
  87. local is_open = false -- 服务是否开启
  88. local is_connected = false -- 是否已连接
  89. local is_authenticated = false -- 是否已鉴权
  90. local sequence_num = 1 -- 流水号
  91. local connection = nil -- 连接对象
  92. local device_id_binary = nil -- 二进制格式的设备ID
  93. local reconnect_timer = nil -- 重连定时器
  94. local reconnect_count = 0 -- 重连次数
  95. local pending_messages = {} -- 待发送消息队列
  96. local rxbuff = nil -- 接收缓冲区
  97. local connect_timeout_timer = nil -- 连接超时定时器
  98. local heartbeat_timer = nil -- 心跳定时器
  99. local heartbeat_interval = 300 -- 心跳间隔(秒),默认5分钟
  100. local heartbeat_data = {} -- 心跳数据,默认空表
  101. local is_heartbeat_running = false -- 心跳是否正在运行
  102. -- 数据类型定义
  103. local DATA_TYPES = {
  104. INTEGER = 0x0, -- 整数
  105. FLOAT = 0x1, -- 浮点数
  106. BOOLEAN = 0x2, -- 布尔值
  107. ASCII = 0x3, -- ASCII字符串
  108. BINARY = 0x4, -- 二进制数据
  109. UNICODE = 0x5 -- Unicode字符串
  110. }
  111. -- 字段含义定义
  112. local FIELD_MEANINGS = {
  113. -- 控制信令类型 (16-255)
  114. AUTH_REQUEST = 16, -- 鉴权请求
  115. AUTH_RESPONSE = 17, -- 鉴权回复
  116. REPORT_RESPONSE = 18, -- 上报回应
  117. CONTROL_COMMAND = 19, -- 控制命令
  118. CONTROL_RESPONSE = 20, -- 控制回应
  119. IRTU_DOWN = 21, -- iRTU下行命令
  120. IRTU_UP = 22, -- iRTU上行回复
  121. -- 文件上传控制信令 (23-24)
  122. FILE_UPLOAD_START = 23, -- 文件上传开始通知
  123. FILE_UPLOAD_FINISH = 24, -- 文件上传完成通知
  124. -- 运维日志控制信令 (25-27)
  125. MTN_LOG_UPLOAD_REQ_SIGNAL = 25, -- 运维日志上传请求 - 下行(信令类型)
  126. MTN_LOG_UPLOAD_RESP_SIGNAL = 26, -- 运维日志上传响应 - 上行(信令类型)
  127. MTN_LOG_UPLOAD_STATUS_SIGNAL = 27, -- 运维日志上传状态 - 上行(信令类型)
  128. -- 传感类 (256-511)
  129. TEMPERATURE = 256, -- 温度
  130. HUMIDITY = 257, -- 湿度
  131. PARTICULATE = 258, -- 颗粒数
  132. ACIDITY = 259, -- 酸度
  133. ALKALINITY = 260, -- 碱度
  134. ALTITUDE = 261, -- 海拔
  135. WATER_LEVEL = 262, -- 水位
  136. ENV_TEMPERATURE = 263, -- CPU温度/环境温度
  137. POWER_METERING = 264, -- 电量计量
  138. -- 资产管理类 (512-767)
  139. GNSS_LONGITUDE = 512, -- GNSS经度
  140. GNSS_LATITUDE = 513, -- GNSS纬度
  141. SPEED = 514, -- 行驶速度
  142. GNSS_CN = 515, -- 最强的4颗GNSS卫星的CN
  143. SATELLITES_TOTAL = 516, -- 搜到的所有卫星数
  144. SATELLITES_VISIBLE = 517, -- 可见卫星数
  145. HEADING = 518, -- 航向角
  146. LOCATION_METHOD = 519, -- 基站定位/GNSS定位标识
  147. GNSS_INFO = 520, -- GNSS芯片型号和固件版本号
  148. DIRECTION = 521, -- 方向
  149. -- 设备参数类 (768-1023)
  150. HEIGHT = 768, -- 高度
  151. WIDTH = 769, -- 宽度
  152. ROTATION_SPEED = 770, -- 转速
  153. BATTERY_LEVEL = 771, -- 电量(mV)
  154. SERVING_CELL = 772, -- 驻留频段
  155. CELL_INFO = 773, -- 驻留小区和邻区
  156. COMPONENT_MODEL = 774, -- 元器件型号
  157. GPIO_LEVEL = 775, -- GPIO高低电平
  158. BOOT_REASON = 776, -- 开机原因
  159. BOOT_COUNT = 777, -- 开机次数
  160. SLEEP_MODE = 778, -- 休眠模式
  161. WAKE_INTERVAL = 779, -- 定时唤醒间隔
  162. NETWORK_IP_TYPE = 780, -- 设备入网的IP类型
  163. NETWORK_TYPE = 781, -- 当前联网方式
  164. SIGNAL_STRENGTH_4G = 782, --4G信号强度
  165. SIM_ICCID = 783, -- SIM卡ICCID
  166. -- 文件上传业务字段 (784-787)
  167. FILE_UPLOAD_TYPE = 784, -- 文件上传类型(1:图片, 2:音频)
  168. FILE_NAME = 785, -- 文件名称
  169. FILE_SIZE = 786, -- 文件大小
  170. UPLOAD_RESULT_STATUS = 787, -- 上传结果状态
  171. -- 运维日志业务字段 (788-792)
  172. MTN_LOG_FILE_INDEX = 788, -- 运维日志文件序号
  173. MTN_LOG_FILE_TOTAL = 789, -- 运维日志文件总数
  174. MTN_LOG_FILE_SIZE = 790, -- 运维日志文件大小
  175. MTN_LOG_UPLOAD_STATUS_FIELD = 791, -- 运维日志上传状态
  176. MTN_LOG_FILE_NAME = 792, -- 运维日志文件名称
  177. -- 工牌设备参数字段 (793-797) - 新增
  178. BADGE_TOTAL_DISK = 793, -- 工牌总磁盘空间
  179. BADGE_AVAILABLE_DISK = 794, -- 工牌剩余磁盘空间
  180. BADGE_TOTAL_MEM = 795, -- 工牌总内存
  181. BADGE_AVAILABLE_MEM = 796, -- 工牌剩余内存
  182. BADGE_RECORD_COUNT = 797, -- 工牌录音数量
  183. -- 软件数据类 (1024-1279)
  184. LUA_CORE_ERROR = 1024, -- Lua核心库错误上报
  185. LUA_EXT_ERROR = 1025, -- Lua扩展卡错误上报
  186. LUA_APP_ERROR = 1026, -- Lua业务错误上报
  187. FIRMWARE_VERSION = 1027, -- 固件版本号
  188. SMS_FORWARD = 1028, -- SMS转发
  189. CALL_FORWARD = 1029, -- 来电转发
  190. -- 设备无关数据类 (1280-1535)
  191. TIMESTAMP = 1280, -- 时间
  192. RANDOM_DATA = 1281 -- 无意义数据
  193. }
  194. -- 运维日志上传状态
  195. local MTN_LOG_STATUS = {
  196. START = 0, -- 开始上传
  197. SUCCESS = 1, -- 上传成功
  198. FAILED = 2 -- 上传失败
  199. }
  200. -- 将数字转换为大端字节序列
  201. local function to_big_endian(num, bytes)
  202. local result = {}
  203. for i = bytes, 1, -1 do
  204. result[i] = string.char(num % 256)
  205. num = math.floor(num / 256)
  206. end
  207. return table.concat(result)
  208. end
  209. -- 从大端字节序列转换为数字
  210. local function from_big_endian(data, start, length)
  211. local value = 0
  212. for i = start, start + length - 1 do
  213. value = value * 256 + data:byte(i)
  214. end
  215. -- log.info("[excloud]from_big_endian", value)
  216. return value
  217. end
  218. -- 将设备ID进行编码
  219. local function packDeviceInfo(deviceType, deviceId)
  220. -- 验证设备类型
  221. if deviceType ~= 1 and deviceType ~= 2 and deviceType ~= 9 then
  222. log.info("[excloud]设备类型错误: 4G设备应为1, WIFI设备应为2")
  223. end
  224. -- 设备类型字节
  225. local result = { string.char(deviceType) }
  226. -- 清理设备ID(移除非数字和字母字符,并转换为大写)
  227. local cleanId = deviceId:gsub("[^%w]", ""):upper()
  228. -- 处理不同类型的设备ID
  229. if deviceType == 1 then
  230. -- 4G设备 - IMEI处理
  231. -- 只取前14位数字,忽略第15位
  232. cleanId = cleanId:gsub("%D", ""):sub(1, 14)
  233. -- 确保长度为14位(不足时前面补0)
  234. if #cleanId < 14 then
  235. cleanId = string.rep("0", 14 - #cleanId) .. cleanId
  236. end
  237. -- 转换为BCD格式的字节
  238. for i = 1, 14, 2 do
  239. local byte = (tonumber(cleanId:sub(i, i)) * 16) + tonumber(cleanId:sub(i + 1, i + 1))
  240. table.insert(result, string.char(byte))
  241. end
  242. elseif deviceType == 2 then
  243. -- WIFI设备 - MAC地址处理
  244. -- 移除非十六进制字符
  245. cleanId = cleanId:gsub("[^0-9A-Fa-f]", "")
  246. -- 确保长度为12个十六进制字符(6字节)
  247. if #cleanId < 12 then
  248. cleanId = string.rep("0", 12 - #cleanId) .. cleanId
  249. else
  250. cleanId = cleanId:sub(1, 12)
  251. end
  252. -- 转换为字节
  253. local bytes = {}
  254. for i = 1, 12, 2 do
  255. local byteStr = cleanId:sub(i, i + 1)
  256. table.insert(bytes, string.char(tonumber(byteStr, 16)))
  257. end
  258. -- 确保有7个字节(不足时前面补0)
  259. while #bytes < 7 do
  260. table.insert(bytes, 1, string.char(0))
  261. end
  262. -- 添加到结果中
  263. for _, byte in ipairs(bytes) do
  264. table.insert(result, byte)
  265. end
  266. elseif deviceType == 9 then
  267. -- 虚拟设备处理:11位手机号 + 3位序列号
  268. cleanId = cleanId:gsub("%D", ""):sub(1, 14)
  269. if #cleanId < 14 then
  270. cleanId = string.rep("0", 14 - #cleanId) .. cleanId
  271. end
  272. -- 转换为BCD格式的字节(每2位数字转换为1个字节)
  273. for i = 1, 14, 2 do
  274. local byte = (tonumber(cleanId:sub(i, i)) * 16) + tonumber(cleanId:sub(i + 1, i + 1))
  275. table.insert(result, string.char(byte))
  276. end
  277. else
  278. log.info("[excloud]未知设备类型 ")
  279. return deviceId
  280. end
  281. -- 返回8字节的二进制数据
  282. return table.concat(result)
  283. end
  284. -- 编码数据值
  285. local function encode_value(data_type, value)
  286. -- 添加参数类型检查
  287. if data_type == nil or value == nil then
  288. log.info("[excloud]Data type or value is nil")
  289. return ""
  290. end
  291. if data_type == DATA_TYPES.INTEGER then
  292. -- 验证value是否为数字
  293. if type(value) ~= "number" then
  294. log.info("[excloud]Integer value must be a number")
  295. return ""
  296. end
  297. return to_big_endian(math.floor(value), 4)
  298. elseif data_type == DATA_TYPES.FLOAT then
  299. -- 验证value是否为数字
  300. if type(value) ~= "number" then
  301. log.info("[excloud]Float value must be a number")
  302. return ""
  303. end
  304. -- 简化处理:将浮点数转换为整数,乘以1000以保留三位小数
  305. return to_big_endian(math.floor(value * 1000), 4)
  306. elseif data_type == DATA_TYPES.BOOLEAN then
  307. return value and "\1" or "\0"
  308. elseif data_type == DATA_TYPES.ASCII or data_type == DATA_TYPES.BINARY or data_type == DATA_TYPES.UNICODE then
  309. -- 确保value是字符串类型
  310. return tostring(value)
  311. else
  312. log.info("[excloud]Unsupported data type: " .. tostring(data_type))
  313. -- 返回空字符串而不是nil,避免后续处理出错
  314. return ""
  315. end
  316. end
  317. -- 解码数据值
  318. local function decode_value(data_type, value)
  319. if data_type == DATA_TYPES.INTEGER then
  320. return from_big_endian(value, 1, #value)
  321. elseif data_type == DATA_TYPES.FLOAT then
  322. -- 简化处理:将整数转换为浮点数(实际应使用IEEE 754格式)
  323. return from_big_endian(value, 1, #value) / 1000
  324. elseif data_type == DATA_TYPES.BOOLEAN then
  325. return value:byte(1) ~= 0
  326. elseif data_type == DATA_TYPES.ASCII then
  327. return value
  328. elseif data_type == DATA_TYPES.BINARY then
  329. return value
  330. elseif data_type == DATA_TYPES.UNICODE then
  331. return value
  332. else
  333. log.info("[excloud]Unsupported data type: " .. data_type)
  334. return nil
  335. end
  336. end
  337. -- 构建消息头
  338. -- @param need_reply boolean 是否需要服务器回复
  339. -- @param has_auth_key boolean 是否携带鉴权key
  340. -- @param data_length number 数据长度
  341. local function build_header(need_reply, is_udp_transport, data_length)
  342. sequence_num = sequence_num + 1
  343. if sequence_num > 65535 then
  344. sequence_num = 1
  345. end
  346. -- 消息标识字段
  347. local flags = config.protocol_version -- bit0-3: 协议版本号
  348. if need_reply then
  349. flags = flags + 16 -- bit4: 是否需要回复
  350. end
  351. if is_udp_transport then
  352. flags = flags + 32 -- bit5: 是否是UDP承载
  353. end
  354. log.info("[excloud]构建消息头", device_id_binary, to_big_endian(sequence_num, 2), to_big_endian(data_length, 2),
  355. to_big_endian(flags, 4))
  356. return device_id_binary ..
  357. to_big_endian(sequence_num, 2) ..
  358. to_big_endian(data_length, 2) ..
  359. to_big_endian(flags, 4)
  360. end
  361. -- 构建TLV字段
  362. local function build_tlv(field_meaning, data_type, value)
  363. if field_meaning == nil or data_type == nil or value == nil then
  364. log.info("[excloud]构建tlv参数不能为空")
  365. return false
  366. end
  367. local value_encoded = encode_value(data_type, value)
  368. if value_encoded == nil then
  369. log.info("[excloud]构建tlv打包数据时长度为0")
  370. -- 添加空字符串作为默认值,避免后续获取长度时出错
  371. value_encoded = ""
  372. end
  373. local length = #value_encoded
  374. -- 字段类型(字段含义 + 数据类型)
  375. local head = (field_meaning & 0x0FFF) | (data_type << 12) -- 2 字节头
  376. return true, to_big_endian(head, 2) ..
  377. to_big_endian(length, 2) ..
  378. value_encoded
  379. end
  380. -- 解析消息头
  381. local function parse_header(header)
  382. if #header < 16 then
  383. log.info("[excloud]消息头解析失败", "Header too short")
  384. return nil, "Header too short"
  385. end
  386. local device_id = header:sub(1, 8)
  387. local seq_num = from_big_endian(header, 9, 2)
  388. local msg_length = from_big_endian(header, 11, 2)
  389. local flags = from_big_endian(header, 13, 4)
  390. -- 提取标志位
  391. local protocol_version = flags % 16
  392. local need_reply = (flags % 32) >= 16
  393. local is_udp_transport = (flags % 64) >= 32
  394. -- 打印解析结果,方便调试
  395. -- log.info("[excloud]消息头解析结果",
  396. -- string.format(
  397. -- "device_id: %s, sequence_num: %d, msg_length: %d, protocol_version: %d, need_reply: %s, is_udp_transport: %s",
  398. -- string.toHex(device_id), seq_num, msg_length, protocol_version,
  399. -- tostring(need_reply), tostring(is_udp_transport)))
  400. return {
  401. device_id = string.toHex(device_id),
  402. sequence_num = seq_num,
  403. msg_length = msg_length,
  404. protocol_version = protocol_version,
  405. need_reply = need_reply,
  406. is_udp_transport = is_udp_transport
  407. }
  408. end
  409. -- 工具函数:解析TLV
  410. local function parse_tlv(data, startPos)
  411. -- 检查数据是否足够解析TLV的T的长度。
  412. if #data < startPos + 3 then
  413. return nil, startPos, "TLV data too short"
  414. end
  415. local fieldType = from_big_endian(data, startPos, 2)
  416. local length = from_big_endian(data, startPos + 2, 2)
  417. -- 提取原始字节值
  418. local value = data:sub(startPos + 4, startPos + 4 + length - 1)
  419. --解析TLV字段中的T
  420. -- bit0-11: 字段含义
  421. -- bit12-15: 数据类型
  422. local field_meaning = fieldType & 0x0FFF -- 取低12位作为字段含义
  423. local data_type = fieldType >> 12 -- 取高4位作为数据类型
  424. local decoded_value = decode_value(data_type, value)
  425. -- log.info("[excloud]消息体解析结果", field_meaning, data_type, decoded_value)
  426. return {
  427. field = field_meaning,
  428. type = data_type,
  429. value = decoded_value,
  430. length = length, --数据长度
  431. }, startPos + 4 + length
  432. end
  433. -- 解析完整消息
  434. local function parse_message(data)
  435. local header, err = parse_header(data:sub(1, 16))
  436. if not header then
  437. return nil, err
  438. end
  439. local auth_key = nil
  440. local body_start = 17
  441. -- 如果是UDP传输,解析认证key
  442. if header.is_udp_transport then
  443. if #data >= body_start + 64 - 1 then
  444. auth_key = data:sub(body_start, body_start + 64 - 1)
  445. body_start = body_start + 64
  446. else
  447. return nil, "Incomplete UDP authentication key"
  448. end
  449. end
  450. -- 解析TLV字段
  451. local tlvs = {}
  452. local pos = body_start
  453. local end_pos = 16 + (header.msg_length)
  454. if #data < end_pos then
  455. return nil, "Message incomplete"
  456. end
  457. while pos < end_pos do
  458. local tlv, new_pos, err = parse_tlv(data, pos)
  459. if not tlv then
  460. return nil, "Failed to parse TLV at position " .. err
  461. end
  462. table.insert(tlvs, tlv)
  463. -- 更新解析位置为解析完当前TLV字段后的新位置,以便继续解析后续的TLV字段
  464. pos = new_pos
  465. end
  466. return {
  467. header = header,
  468. auth_key = auth_key,
  469. tlvs = tlvs
  470. }
  471. end
  472. -- 发送鉴权请求
  473. local function send_auth_request()
  474. if not config.auth_key then
  475. return false, "No auth key configured"
  476. end
  477. local auth_data
  478. --设备实测时打开
  479. if config.device_type == 1 then
  480. auth_data = config.auth_key .. "-" .. mobile.imei() .. "-" .. mobile.muid()
  481. elseif config.device_type == 2 then
  482. auth_data = config.auth_key .. "-" .. wlan.getMac(nil, true) .. "-" .. mcu.unique_id():toHex()
  483. elseif config.device_type == 9 then --虚拟设备
  484. auth_data = config.auth_key .. "-" .. config.device_id
  485. else
  486. auth_data = config.auth_key .. "-"
  487. end
  488. local message = {
  489. {
  490. field_meaning = FIELD_MEANINGS.AUTH_REQUEST,
  491. data_type = DATA_TYPES.ASCII,
  492. value = auth_data
  493. }
  494. }
  495. -- log.info("[excloud]send auth request", message,message[1].value,message[1].data_type,message[1].field_meaning)
  496. return excloud.send(message, true, true) -- 鉴权消息需要设置 is_auth_msg 为 true
  497. end
  498. -- 处理认证响应
  499. -- local function handle_auth_response(tlvs)
  500. -- for _, tlv in ipairs(tlvs) do
  501. -- if tlv.field_meaning == FIELD_MEANINGS.AUTH_RESPONSE then
  502. -- local success = (tlv.value == "OK" or tlv.value == "SUCCESS")
  503. -- is_authenticated = true
  504. -- -- 记录认证结果到运维日志
  505. -- if config.aircloud_mtn_log_enabled then
  506. -- exmtn.log("info", "aircloud","auth", "认证结果", "success", success, "message", tlv.value)
  507. -- end
  508. -- if callback_func then
  509. -- callback_func("auth_result", {
  510. -- success = success,
  511. -- message = tlv.value
  512. -- })
  513. -- end
  514. -- -- 认证成功,发送待处理消息
  515. -- if success then
  516. -- for _, msg in ipairs(pending_messages) do
  517. -- excloud.send(msg.data, msg.need_reply)
  518. -- end
  519. -- pending_messages = {}
  520. -- end
  521. -- return success
  522. -- end
  523. -- end
  524. -- return false
  525. -- end
  526. -- 初始化运维日志模块
  527. local function init_mtn_log()
  528. if not config.mtn_log_enabled then
  529. log.info("[excloud]aircloud运维日志功能已禁用")
  530. return true
  531. end
  532. local ok, err = exmtn.init(config.mtn_log_blocks, config.mtn_log_write_way)
  533. if not ok then
  534. log.error("[excloud]运维日志初始化失败:", err)
  535. return false, err
  536. end
  537. log.info("[excloud]运维日志初始化成功")
  538. return true
  539. end
  540. -- 扫描运维日志文件
  541. local function scan_mtn_log_files()
  542. local log_files = {}
  543. -- 使用exmtn管理的文件信息
  544. for i = 1, 4 do -- exmtn管理4个日志文件
  545. local file_path = string.format("/hzmtn%d.trc", i)
  546. if io.exists(file_path) then
  547. local file_size = io.fileSize(file_path)
  548. if file_size > 0 then
  549. table.insert(log_files, {
  550. name = string.format("hzmtn%d.trc", i),
  551. path = file_path,
  552. size = file_size,
  553. index = i
  554. })
  555. log.info("发现运维日志文件", "路径:", file_path, "大小:", file_size, "序号:", i)
  556. else
  557. log.info("运维日志文件为空", "路径:", file_path)
  558. end
  559. else
  560. log.info("运维日志文件不存在", "路径:", file_path)
  561. end
  562. end
  563. -- 按文件序号排序
  564. table.sort(log_files, function(a, b)
  565. return a.index < b.index
  566. end)
  567. log.info("扫描运维日志文件完成", "有效文件数量:", #log_files)
  568. return log_files
  569. end
  570. -- 构建运维日志响应TLV数据
  571. local function build_mtn_log_response_tlv(total_files, latest_index)
  572. local sub_tlvs = ""
  573. -- 文件总数
  574. local success, tlv_data = build_tlv(FIELD_MEANINGS.MTN_LOG_FILE_TOTAL, DATA_TYPES.INTEGER, total_files)
  575. if success then
  576. sub_tlvs = sub_tlvs .. tlv_data
  577. else
  578. log.error("构建文件总数TLV失败")
  579. return ""
  580. end
  581. -- 当前最新文件序号
  582. success, tlv_data = build_tlv(FIELD_MEANINGS.MTN_LOG_FILE_INDEX, DATA_TYPES.INTEGER, latest_index)
  583. if success then
  584. sub_tlvs = sub_tlvs .. tlv_data
  585. else
  586. log.error("构建最新文件序号TLV失败")
  587. return ""
  588. end
  589. log.info("构建运维日志响应TLV", "文件总数:", total_files, "最新序号:", latest_index)
  590. return sub_tlvs
  591. end
  592. -- 发送运维日志上传状态
  593. local function send_mtn_log_status(status, file_index, file_name, file_size)
  594. local sub_tlvs = ""
  595. -- 上传状态(必须)
  596. local success, tlv_data = build_tlv(FIELD_MEANINGS.MTN_LOG_UPLOAD_STATUS_FIELD, DATA_TYPES.INTEGER, status)
  597. if success then
  598. sub_tlvs = sub_tlvs .. tlv_data
  599. else
  600. log.error("构建运维日志上传状态TLV失败")
  601. return false, "构建状态TLV失败"
  602. end
  603. -- 文件序号
  604. success, tlv_data = build_tlv(FIELD_MEANINGS.MTN_LOG_FILE_INDEX, DATA_TYPES.INTEGER, file_index)
  605. if success then
  606. sub_tlvs = sub_tlvs .. tlv_data
  607. else
  608. log.error("构建文件序号TLV失败")
  609. return false, "构建文件序号TLV失败"
  610. end
  611. -- 根据状态添加不同的字段
  612. if status == MTN_LOG_STATUS.START then
  613. -- 开始上传:包含文件名
  614. if file_name then
  615. success, tlv_data = build_tlv(FIELD_MEANINGS.MTN_LOG_FILE_NAME, DATA_TYPES.ASCII, file_name)
  616. if success then
  617. sub_tlvs = sub_tlvs .. tlv_data
  618. else
  619. log.warn("构建文件名TLV失败,但继续发送状态")
  620. end
  621. else
  622. log.warn("开始上传状态缺少文件名")
  623. end
  624. elseif status == MTN_LOG_STATUS.SUCCESS then
  625. -- 上传成功:包含文件大小
  626. if file_size then
  627. success, tlv_data = build_tlv(FIELD_MEANINGS.MTN_LOG_FILE_SIZE, DATA_TYPES.INTEGER, file_size)
  628. if success then
  629. sub_tlvs = sub_tlvs .. tlv_data
  630. else
  631. log.warn("构建文件大小TLV失败,但继续发送状态")
  632. end
  633. else
  634. log.warn("上传成功状态缺少文件大小")
  635. end
  636. end
  637. -- 上传失败:只需要状态和序号
  638. -- 发送状态消息
  639. local ok, err_msg = excloud.send({
  640. {
  641. field_meaning = FIELD_MEANINGS.MTN_LOG_UPLOAD_STATUS_SIGNAL,
  642. data_type = DATA_TYPES.BINARY,
  643. value = sub_tlvs
  644. }
  645. }, false)
  646. if not ok then
  647. log.error("发送运维日志上传状态失败: " .. (err_msg or "未知错误"))
  648. return false, err_msg
  649. end
  650. log.info("运维日志上传状态发送成功",
  651. "状态:", status,
  652. "文件序号:", file_index,
  653. "文件名:", file_name or "N/A",
  654. "文件大小:", file_size or "N/A")
  655. return true
  656. end
  657. -- 上传运维日志文件
  658. local function upload_mtn_log_files()
  659. sys.taskInit(function()
  660. local total_files = 4 -- 固定为4个日志文件
  661. local success_count = 0
  662. local failed_count = 0
  663. local processed_count = 0
  664. -- -- 通知开始上传
  665. -- if callback_func then
  666. -- callback_func("mtn_log_upload_start", {
  667. -- file_count = total_files
  668. -- })
  669. -- end
  670. -- 按顺序检查并上传每个日志文件
  671. for i = 1, 4 do
  672. local file_path = string.format("/hzmtn%d.trc", i)
  673. local file_name = string.format("/hzmtn%d.trc", i)
  674. -- 在上传前再次检查文件是否存在且不为空
  675. local file_size = io.fileSize(file_path)
  676. if file_size and file_size > 0 then
  677. processed_count = processed_count + 1
  678. -- 发送开始上传状态
  679. send_mtn_log_status(MTN_LOG_STATUS.START, i, file_name, file_size)
  680. log.info("[excloud]开始上传运维日志文件", "文件:", file_name, "大小:", file_size)
  681. -- 上传文件
  682. local success, err_msg = excloud.upload_mtnlog(file_path, file_name)
  683. if success then
  684. -- 发送上传成功状态
  685. log.info("运维日志文件上传成功", "文件:", file_name, "大小:", file_size)
  686. send_mtn_log_status(MTN_LOG_STATUS.SUCCESS, i, file_name, file_size)
  687. success_count = success_count + 1
  688. -- 记录上传成功的运维日志
  689. if config.aircloud_mtn_log_enabled then
  690. exmtn.log("info", "aircloud", "mtn_upload", "文件上传成功", "file", file_name, "size", file_size)
  691. end
  692. else
  693. -- 发送上传失败状态
  694. log.error("运维日志文件上传失败", "文件:", file_name, "错误:", err_msg)
  695. send_mtn_log_status(MTN_LOG_STATUS.FAILED, i, file_name, file_size)
  696. failed_count = failed_count + 1
  697. -- 记录上传失败的运维日志
  698. if config.aircloud_mtn_log_enabled then
  699. exmtn.log("info", "aircloud", "mtn_upload_error", "文件上传失败", "file", file_name, "error", err_msg)
  700. end
  701. end
  702. -- 通知上传进度
  703. if callback_func then
  704. callback_func("mtn_log_upload_progress", {
  705. current_file = processed_count,
  706. total_files = total_files,
  707. file_name = file_name,
  708. file_size = file_size,
  709. status = success and "success" or "failed",
  710. error_msg = err_msg
  711. })
  712. end
  713. else
  714. -- 文件不存在或为空,跳过上传
  715. log.info("运维日志文件不存在或为空,跳过上传", "文件:", file_name)
  716. end
  717. end
  718. log.info("运维日志上传完成", "成功:", success_count, "失败:", failed_count, "总计:", processed_count)
  719. -- 记录上传完成日志
  720. if config.aircloud_mtn_log_enabled then
  721. exmtn.log("info", "aircloud", "mtn_upload", "运维日志上传完成", "success", success_count, "failed", failed_count,
  722. "total", processed_count)
  723. end
  724. -- 通知上传完成
  725. if callback_func then
  726. callback_func("mtn_log_upload_complete", {
  727. success_count = success_count,
  728. failed_count = failed_count,
  729. total_files = processed_count
  730. })
  731. end
  732. end)
  733. end
  734. -- 处理运维日志上传请求
  735. local function handle_mtn_log_upload_request()
  736. local total_files = 4 -- 固定为4个日志文件
  737. local latest_index = 4 -- 最新序号固定为4
  738. if config.aircloud_mtn_log_enabled then
  739. exmtn.log("info", "aircloud", "cloud_cmd", "收到运维日志上传请求", "file_count", total_files)
  740. end
  741. log.info("开始处理运维日志上传请求", "文件总数:", total_files, "最新序号:", latest_index)
  742. -- 发送运维日志上传响应(信令26)- 在开始上传前发送
  743. local response_ok, err_msg = excloud.send({
  744. {
  745. field_meaning = FIELD_MEANINGS.MTN_LOG_UPLOAD_RESP_SIGNAL, -- 使用信令类型
  746. data_type = DATA_TYPES.BINARY,
  747. value = build_mtn_log_response_tlv(total_files, latest_index)
  748. }
  749. }, false)
  750. if not response_ok then
  751. log.error("发送运维日志上传响应失败: " .. err_msg)
  752. return
  753. end
  754. log.info("运维日志上传响应已发送", "文件总数:", total_files, "最新序号:", latest_index)
  755. -- 开始上传日志文件
  756. sys.timerStart(function()
  757. upload_mtn_log_files()
  758. end, 100)
  759. end
  760. -- 接收消息解析处理
  761. local function parse_data(data)
  762. local message, err = parse_message(data)
  763. if not message then
  764. log.info("[excloud]Failed to parse message: " .. err)
  765. return
  766. end
  767. -- 处理运维日志上传请求(信令25)
  768. for _, tlv in ipairs(message.tlvs) do
  769. if tlv.field == FIELD_MEANINGS.MTN_LOG_UPLOAD_REQ_SIGNAL then
  770. log.info("[excloud]收到运维日志上传请求")
  771. handle_mtn_log_upload_request()
  772. return
  773. end
  774. end
  775. -- 处理认证响应
  776. -- if not is_authenticated then
  777. -- for _, tlv in ipairs(message.tlvs) do
  778. -- if tlv.field == FIELD_MEANINGS.AUTH_RESPONSE then
  779. -- handle_auth_response(message.tlvs)
  780. -- return
  781. -- end
  782. -- end
  783. --end
  784. --数据返回给回调
  785. if callback_func then
  786. callback_func("message", message)
  787. end
  788. -- -- 如果需要回复,发送确认AAA
  789. -- if message.header.need_reply then
  790. -- local response = {
  791. -- {
  792. -- field_meaning = FIELD_MEANINGS.REPORT_RESPONSE,
  793. -- data_type = DATA_TYPES.ASCII,
  794. -- value = "ACK"
  795. -- }
  796. -- }
  797. -- excloud.send(response, false)
  798. -- end
  799. end
  800. function excloud.getip(getip_type)
  801. getip_type = getip_type or 3 -- 默认使用AirCloud TCP协议
  802. -- 添加参数验证
  803. if not config.auth_key or not config.device_id then
  804. return false, "缺少必要的认证参数: auth_key 或 device_id"
  805. end
  806. local key = config.auth_key .. "-" .. config.device_id
  807. log.info("[excloud]excloud.getip", "类型:", getip_type, "key:", key)
  808. -- 执行HTTP请求
  809. local code, response = httpplus.request(
  810. {
  811. method = "POST",
  812. url = config.getip_url,
  813. forms = { key = key, type = getip_type }
  814. })
  815. log.info("[excloud]excloud.getip响应", "HTTP Code:", code, "Body:", response.body:query())
  816. -- 添加对HTTP响应为空值的处理
  817. if not response or not response.body then
  818. log.error("[excloud]getip请求失败", "HTTP响应为空")
  819. return false, "HTTP响应为空"
  820. end
  821. local response_body = response.body:query()
  822. if not response_body or response_body == "" then
  823. log.error("[excloud]getip请求失败", "响应体为空")
  824. return false, "响应体为空"
  825. end
  826. log.info("[excloud]excloud.getip响应", "HTTP Code:", code, "Body:", response_body)
  827. -- 处理HTTP错误码
  828. if code ~= 200 then
  829. log.info("[excloud]getip请求失败", "HTTP Code:", code)
  830. return false, "HTTP请求失败: " .. tostring(code)
  831. end
  832. -- 解析JSON响应,添加对解析失败的处理
  833. local response_json = json.decode(response_body)
  834. if not response_json then
  835. return false, "JSON解析失败: " .. tostring(err)
  836. end
  837. -- 检查服务器返回状态
  838. if not response_json.msg then
  839. log.error("[excloud]getip响应格式错误", "缺少msg字段")
  840. return false, "服务器响应格式错误: 缺少msg字段"
  841. end
  842. if response_json.msg ~= "ok" then
  843. log.error("[excloud]服务器返回错误", "消息:", response_json.msg)
  844. return false, "服务器返回错误: " .. tostring(response_json.msg)
  845. end
  846. if getip_type >= 3 and getip_type <= 5 then
  847. -- AirCloud业务
  848. if response_json.conninfo then
  849. config.current_conninfo = response_json.conninfo
  850. -- 根据连接类型处理不同的连接信息
  851. if getip_type == 5 then -- MQTT连接
  852. log.info("[excloud]获取到MQTT连接信息",
  853. "host:", response_json.conninfo.ssl,
  854. "port:", response_json.conninfo.port,
  855. "username:", response_json.conninfo.username,
  856. "password:", response_json.conninfo.password)
  857. log.info("[excloud]实际MQTT连接将使用设备信息:",
  858. "client_id:", mobile.imei(),
  859. "username:", mobile.imei(),
  860. "password:", mobile.muid())
  861. else -- TCP/UDP连接
  862. log.info("[excloud]获取到TCP/UDP连接信息",
  863. "host:", response_json.conninfo.ipv4,
  864. "port:", response_json.conninfo.port)
  865. end
  866. else
  867. log.warn("[excloud]未获取到连接信息")
  868. end
  869. if response_json.imginfo then
  870. config.current_imginfo = response_json.imginfo
  871. log.info("[excloud]获取到图片上传信息")
  872. else
  873. log.warn("[excloud]未获取到图片上传信息")
  874. end
  875. if response_json.audinfo then
  876. config.current_audinfo = response_json.audinfo
  877. log.info("[excloud]获取到音频上传信息")
  878. else
  879. log.warn("[excloud]未获取到音频上传信息")
  880. end
  881. -- 新增:运维日志上传信息 (mtninfo)
  882. if response_json.mtninfo then
  883. config.current_mtninfo = response_json.mtninfo
  884. log.info("[excloud]获取到运维日志上传信息")
  885. else
  886. log.warn("[excloud]未获取到运维日志上传信息")
  887. end
  888. end
  889. -- 如果获取到连接信息,自动更新配置
  890. if config.current_conninfo then
  891. -- 根据连接类型设置不同的主机字段
  892. if getip_type == 5 then -- MQTT连接
  893. if config.current_conninfo.ssl then
  894. config.host = config.current_conninfo.ssl
  895. else
  896. log.warn("[excloud]MQTT连接信息中缺少SSL域名")
  897. end
  898. else -- TCP/UDP连接
  899. if config.current_conninfo.ipv4 then
  900. config.host = config.current_conninfo.ipv4
  901. else
  902. log.warn("[excloud]TCP/UDP连接信息中缺少IP地址")
  903. end
  904. end
  905. if config.current_conninfo.port then
  906. config.port = config.current_conninfo.port
  907. else
  908. log.warn("[excloud]连接信息中缺少端口号")
  909. end
  910. -- 更新MQTT认证信息
  911. if getip_type == 5 then
  912. if config.current_conninfo.username then
  913. config.username = config.current_conninfo.username
  914. end
  915. if config.current_conninfo.password then
  916. config.password = config.current_conninfo.password
  917. end
  918. end
  919. log.info("[excloud]excloud.getip", "更新配置:", config.host, config.port)
  920. else
  921. log.warn("[excloud]未获取到有效的连接信息,将使用原有配置")
  922. end
  923. return true, config.current_conninfo
  924. end
  925. -- 带重试的getip请求
  926. function excloud.getip_with_retry(getip_type)
  927. local retry_count = 0
  928. local max_retry = config.max_getip_retry or 3
  929. local success, result
  930. while retry_count < max_retry do
  931. success, result = excloud.getip(getip_type)
  932. if success and result then
  933. log.info("[excloud]excloud.getip", "成功:", success, "结果:", json.encode(result))
  934. config.getip_retry_count = 0
  935. return true, result
  936. end
  937. retry_count = retry_count + 1
  938. config.getip_retry_count = retry_count
  939. log.warn("excloud.getip重试", "次数:", retry_count, "错误:", result)
  940. if retry_count < max_retry then
  941. sys.wait(5000) -- 等待5秒后重试
  942. end
  943. end
  944. return false, "getip请求失败,已达最大重试次数: " .. (result or "未知错误")
  945. end
  946. -- 发送文件上传开始通知
  947. local function send_file_upload_start(file_type, file_name, file_size)
  948. -- 构建子TLV数据
  949. -- local sub_tlvs = ""
  950. -- 文件上传类型子TLV
  951. -- local success, tlv_data = build_tlv(FIELD_MEANINGS.FILE_UPLOAD_TYPE, DATA_TYPES.INTEGER, file_type)
  952. -- if success then
  953. -- sub_tlvs = sub_tlvs .. tlv_data
  954. -- end
  955. -- 文件名称子TLV
  956. -- success, tlv_data = build_tlv(FIELD_MEANINGS.FILE_NAME, DATA_TYPES.ASCII, file_name)
  957. -- if success then
  958. -- sub_tlvs = sub_tlvs .. tlv_data
  959. -- end
  960. -- 文件大小子TLV
  961. -- success, tlv_data = build_tlv(FIELD_MEANINGS.FILE_SIZE, DATA_TYPES.INTEGER, file_size)
  962. -- if success then
  963. -- sub_tlvs = sub_tlvs .. tlv_data
  964. -- end
  965. local sub_tlvs = 0
  966. -- 主TLV(文件上传开始通知)
  967. local message = {
  968. {
  969. field_meaning = FIELD_MEANINGS.FILE_UPLOAD_START,
  970. data_type = DATA_TYPES.INTEGER,
  971. value = sub_tlvs -- 子TLV数据作为二进制值
  972. },
  973. {
  974. field_meaning = FIELD_MEANINGS.FILE_UPLOAD_TYPE,
  975. data_type = DATA_TYPES.INTEGER,
  976. value = file_type
  977. },
  978. {
  979. field_meaning = FIELD_MEANINGS.FILE_NAME,
  980. data_type = DATA_TYPES.ASCII,
  981. value = file_name
  982. },
  983. {
  984. field_meaning = FIELD_MEANINGS.FILE_SIZE,
  985. data_type = DATA_TYPES.INTEGER,
  986. value = file_size
  987. }
  988. }
  989. return excloud.send(message, false)
  990. end
  991. -- 发送文件上传完成通知
  992. local function send_file_upload_finish(file_type, file_name, file_success)
  993. -- 构建子TLV数据
  994. -- local sub_tlvs = ""
  995. -- -- 文件上传类型子TLV
  996. -- local success, tlv_data = build_tlv(FIELD_MEANINGS.FILE_UPLOAD_TYPE, DATA_TYPES.INTEGER, file_type)
  997. -- if success then
  998. -- sub_tlvs = sub_tlvs .. tlv_data
  999. -- end
  1000. -- 文件名称子TLV
  1001. -- success, tlv_data = build_tlv(FIELD_MEANINGS.FILE_NAME, DATA_TYPES.ASCII, file_name)
  1002. -- if success then
  1003. -- sub_tlvs = sub_tlvs .. tlv_data
  1004. -- end
  1005. -- 上传结果状态子TLV
  1006. -- success, tlv_data = build_tlv(FIELD_MEANINGS.UPLOAD_RESULT_STATUS, DATA_TYPES.INTEGER, file_success and 0 or 1)
  1007. -- if success then
  1008. -- sub_tlvs = sub_tlvs .. tlv_data
  1009. -- end
  1010. local sub_tlvs = 0
  1011. -- 主TLV(文件上传完成通知)
  1012. local message = {
  1013. {
  1014. field_meaning = FIELD_MEANINGS.FILE_UPLOAD_FINISH,
  1015. data_type = DATA_TYPES.INTEGER,
  1016. value = sub_tlvs -- 子TLV数据作为二进制值
  1017. },
  1018. {
  1019. field_meaning = FIELD_MEANINGS.FILE_UPLOAD_TYPE,
  1020. data_type = DATA_TYPES.INTEGER,
  1021. value = file_type
  1022. },
  1023. {
  1024. field_meaning = FIELD_MEANINGS.FILE_NAME,
  1025. data_type = DATA_TYPES.ASCII,
  1026. value = file_name
  1027. },
  1028. {
  1029. field_meaning = FIELD_MEANINGS.UPLOAD_RESULT_STATUS,
  1030. data_type = DATA_TYPES.INTEGER,
  1031. value = file_success and 0 or 1
  1032. }
  1033. }
  1034. return excloud.send(message, false)
  1035. end
  1036. local function upload_file(file_type, file_path, file_name)
  1037. local upload_info
  1038. if file_type == 1 then
  1039. upload_info = config.current_imginfo
  1040. elseif file_type == 2 then
  1041. upload_info = config.current_audinfo
  1042. elseif file_type == 3 then
  1043. upload_info = config.current_mtninfo -- 新增:运维日志上传
  1044. else
  1045. return false, "不支持的文件类型"
  1046. end
  1047. if not upload_info then
  1048. return false, "未获取到上传配置信息,请先执行getip"
  1049. end
  1050. if not upload_info.url then
  1051. return false, "上传URL为空"
  1052. end
  1053. local file_size = io.fileSize(file_path)
  1054. if not file_size or file_size == 0 then
  1055. log.info("[excloud]文件不存在或为空", "文件:", file_name,file_size)
  1056. return false, "文件不存在或为空"
  1057. end
  1058. log.info("[excloud]开始文件上传", "类型:", file_type, "文件:", file_path, "大小:", file_size)
  1059. log.info("[excloud]开始文件上传", "类型:", file_type, "文件:", file_name, "大小:", file_size)
  1060. -- 发送上传开始通知(运维日志不需要)
  1061. if file_type ~= 3 then
  1062. local ok, err = send_file_upload_start(file_type, file_name, file_size)
  1063. if not ok then
  1064. log.warn("发送上传开始通知失败", err)
  1065. end
  1066. end
  1067. -- 执行HTTP请求,添加重传机制
  1068. local max_retries = 1
  1069. local retry_count = 0
  1070. local code, response
  1071. local upload_success = false
  1072. local result_msg = ""
  1073. while retry_count <= max_retries do
  1074. code, response = httpplus.request(
  1075. {
  1076. method = "POST",
  1077. url = upload_info.url,
  1078. forms = { ["key"] = upload_info.data_param.key },
  1079. files = { [upload_info.data_key or "f"] = file_path }
  1080. })
  1081. -- 检查响应
  1082. if response then
  1083. log.info("[excloud]excloud.getip文件上传响应", "HTTP Code:", code, "Body:", response.body:query(), "Body:",
  1084. json.encode(response))
  1085. if code == 200 then
  1086. local resp_data, err = json.decode(response.body:query())
  1087. if resp_data and resp_data.code == 0 then
  1088. upload_success = true
  1089. result_msg = "上传成功"
  1090. log.info("[excloud]文件上传成功", "URL:", resp_data.value and resp_data.value.uri or "未知")
  1091. break
  1092. else
  1093. result_msg = "服务器返回错误: " .. (resp_data and tostring(resp_data.code) or "未知")
  1094. log.error("文件上传失败", result_msg, "响应:", response.body:query())
  1095. end
  1096. else
  1097. result_msg = "HTTP请求失败: " .. tostring(code)
  1098. log.error("文件上传HTTP请求失败", result_msg)
  1099. end
  1100. else
  1101. log.error("[excloud]HTTP请求返回空响应")
  1102. result_msg = "HTTP请求失败: 空响应"
  1103. end
  1104. -- 如果失败且未达到最大重试次数,则重试
  1105. if not upload_success and retry_count < max_retries then
  1106. retry_count = retry_count + 1
  1107. log.info("[excloud]文件上传失败,开始第" .. retry_count .. "次重试")
  1108. sys.wait(1000) -- 等待1秒后重试
  1109. else
  1110. break
  1111. end
  1112. end
  1113. -- 发送上传完成通知(运维日志不需要)
  1114. if file_type ~= 3 then
  1115. local notify_ok, notify_err = send_file_upload_finish(file_type, file_name, upload_success)
  1116. if not notify_ok then
  1117. log.warn("发送上传完成通知失败", notify_err)
  1118. end
  1119. end
  1120. return upload_success, result_msg
  1121. end
  1122. -- 运维日志上传接口
  1123. function excloud.upload_mtnlog(file_path, file_name)
  1124. -- 判断是否是手动填写IP,不是getip的话,不允许上传文件
  1125. if not config.use_getip then
  1126. log.warn("[excloud]手动填写IP时不允许上传文件")
  1127. return false, "手动填写IP时不允许上传文件"
  1128. end
  1129. -- 检查文件是否存在
  1130. if not io.exists(file_path) then
  1131. return false, "文件不存在: " .. file_path
  1132. end
  1133. log.info("[excloud]excloud.upload_mtnlog", "文件路径:", file_path, "文件名:", file_name)
  1134. -- 如果没有文件上传配置,先获取
  1135. if not config.current_mtninfo then
  1136. log.info("[excloud]获取运维日志上传配置...")
  1137. local getip_type = config.transport == "tcp" and 3 or
  1138. config.transport == "udp" and 4 or
  1139. config.transport == "mqtt" and 5 or 3
  1140. local ok, err = excloud.getip_with_retry(getip_type)
  1141. if not ok then
  1142. return false, "获取运维日志上传配置失败: " .. err
  1143. end
  1144. end
  1145. log.info("[excloud]excloud.upload_mtnlog", "文件路径:", file_path, "文件名:", file_name)
  1146. return upload_file(3, file_path, file_name) -- 文件类型为3
  1147. end
  1148. -- 图片上传接口
  1149. function excloud.upload_image(file_path, file_name)
  1150. -- 判断是否是手动填写IP,不是getip的话,不允许上传文件
  1151. if not config.use_getip then
  1152. log.warn("[excloud]手动填写IP时不允许上传图片文件")
  1153. return false, "手动填写IP时不允许上传图片文件"
  1154. end
  1155. if not io.exists(file_path) then
  1156. return false, "文件不存在: " .. file_path
  1157. end
  1158. -- 没有连接时直接退出
  1159. if not is_connected then
  1160. return false, "没有连接到服务器"
  1161. end
  1162. file_name = file_name or "image_" .. os.time() .. ".jpg"
  1163. -- 如果没有图片上传配置,先获取
  1164. if not config.current_imginfo then
  1165. log.info("[excloud]excloud.upload_image", "获取图片上传配置...")
  1166. local getip_type = config.transport == "tcp" and 3 or
  1167. config.transport == "udp" and 4 or
  1168. config.transport == "mqtt" and 5 or 3
  1169. local ok, err = excloud.getip_with_retry(getip_type)
  1170. if not ok then
  1171. return false, "获取图片上传配置失败: " .. err
  1172. end
  1173. end
  1174. return upload_file(1, file_path, file_name)
  1175. end
  1176. -- 音频上传接口
  1177. function excloud.upload_audio(file_path, file_name)
  1178. -- 判断是否是手动填写IP,不是getip的话,不允许上传文件
  1179. if not config.use_getip then
  1180. log.warn("[excloud]手动填写IP时不允许上传音频文件")
  1181. return false, "手动填写IP时不允许上传音频文件"
  1182. end
  1183. if not io.exists(file_path) then
  1184. return false, "文件不存在: " .. file_path
  1185. end
  1186. -- 没有连接时直接退出
  1187. if not is_connected then
  1188. return false, "没有连接到服务器"
  1189. end
  1190. file_name = file_name or "audio_" .. os.time() .. ".mp3"
  1191. -- 如果没有音频上传配置,先获取
  1192. if not config.current_audinfo then
  1193. log.info("[excloud]excloud.upload_audio", "获取音频上传配置...")
  1194. local getip_type = config.transport == "tcp" and 3 or
  1195. config.transport == "udp" and 4 or
  1196. config.transport == "mqtt" and 5 or 3
  1197. local ok, err = excloud.getip_with_retry(getip_type)
  1198. if not ok then
  1199. return false, "获取音频上传配置失败: " .. err
  1200. end
  1201. end
  1202. return upload_file(2, file_path, file_name)
  1203. end
  1204. -- 记录运维日志
  1205. --[[
  1206. 输出运维日志并写入文件
  1207. @api excloud.mtn_log(level, tag, ...)
  1208. @string level 日志级别,必须是 "info", "warn", 或 "error"
  1209. @string tag 日志标识,必须是字符串
  1210. @... 需打印的参数
  1211. @return boolean 成功返回true,失败返回false
  1212. @usage
  1213. excloud.mtn_log("info", "message", 123)
  1214. excloud.mtn_log("warn", "message", 456)
  1215. excloud.mtn_log("error", "message", 789)
  1216. ]]
  1217. function excloud.mtn_log(level, tag, ...)
  1218. if not config.mtn_log_enabled then
  1219. return false, "运维日志功能已禁用" -- 禁用时返回失败
  1220. end
  1221. exmtn.log(level, tag, ...)
  1222. return true
  1223. end
  1224. -- 获取运维日志状态
  1225. function excloud.get_mtn_log_status()
  1226. if not config.mtn_log_enabled then
  1227. return {
  1228. enabled = false,
  1229. message = "运维日志功能已禁用"
  1230. }
  1231. end
  1232. local config_info = exmtn.get_config()
  1233. local log_files = scan_mtn_log_files()
  1234. local total_size = 0
  1235. for _, file in ipairs(log_files) do
  1236. total_size = total_size + file.size
  1237. end
  1238. return {
  1239. enabled = true,
  1240. config = config_info,
  1241. file_count = #log_files,
  1242. total_size = total_size,
  1243. files = log_files,
  1244. last_error = exmtn.get_last_error()
  1245. }
  1246. end
  1247. -- 重连
  1248. local function schedule_reconnect()
  1249. -- 检查是否已经关闭服务
  1250. if not is_open then
  1251. log.info("[excloud]服务已关闭,停止重连")
  1252. return
  1253. end
  1254. -- 检查是否达到最大重连次数
  1255. if reconnect_count >= config.max_reconnect then
  1256. log.info("[excloud]到达最大重连次数 " .. reconnect_count .. "/" .. config.max_reconnect)
  1257. -- 使用协程执行复杂的重连逻辑
  1258. sys.taskInit(function()
  1259. -- 执行紧急内存清理
  1260. collectgarbage("collect")
  1261. pending_messages = {}
  1262. -- 根据use_getip决定是否重新获取服务器信息
  1263. if config.use_getip then
  1264. log.info("[excloud]TCP连接多次失败,重新获取服务器信息...")
  1265. local getip_type = config.transport == "tcp" and 3 or
  1266. config.transport == "udp" and 4 or
  1267. config.transport == "mqtt" and 5 or 3
  1268. -- 清除当前连接信息,强制重新获取
  1269. config.current_conninfo = nil
  1270. local ok, result = excloud.getip_with_retry(getip_type)
  1271. if ok then
  1272. log.info("[excloud]重新获取服务器信息成功,重置重连计数",
  1273. "host:", config.host,
  1274. "port:", config.port,
  1275. "transport:", config.transport)
  1276. -- 重置重连计数
  1277. reconnect_count = 0
  1278. -- 使用新的服务器信息重新连接(先关闭再打开)
  1279. excloud.close() -- 确保完全关闭
  1280. sys.wait(200) -- 在协程中可以安全使用 wait
  1281. excloud.open() -- 重新打开
  1282. else
  1283. log.error("[excloud]重新获取服务器信息失败,停止重连")
  1284. if callback_func then
  1285. callback_func("reconnect_failed", {
  1286. count = reconnect_count,
  1287. max_reconnect = config.max_reconnect,
  1288. getip_failed = true
  1289. })
  1290. end
  1291. -- 彻底停止重连
  1292. is_open = false
  1293. end
  1294. else
  1295. -- 不使用getip,直接停止重连
  1296. log.info("[excloud]达到最大重连次数,停止重连")
  1297. if callback_func then
  1298. callback_func("reconnect_failed", {
  1299. count = reconnect_count,
  1300. max_reconnect = config.max_reconnect
  1301. })
  1302. end
  1303. is_open = false
  1304. end
  1305. end)
  1306. return
  1307. end
  1308. -- 增加重连计数
  1309. reconnect_count = reconnect_count + 1
  1310. log.info("[excloud]安排第 " ..
  1311. reconnect_count .. "/" .. config.max_reconnect .. " 次重连,等待 " .. config.reconnect_interval .. " 秒")
  1312. -- 使用定时器安排重连,在协程中执行
  1313. reconnect_timer = sys.timerStart(function()
  1314. sys.taskInit(function()
  1315. log.info("[excloud]执行第 " .. reconnect_count .. "/" .. config.max_reconnect .. " 次重连")
  1316. -- 在重连前检查服务状态
  1317. if not is_open then
  1318. log.info("[excloud]服务已关闭,取消重连")
  1319. return
  1320. end
  1321. -- 先执行内存清理
  1322. collectgarbage("collect")
  1323. -- 如果连接对象存在但连接已断开,先清理
  1324. if connection and not is_connected then
  1325. log.info("[excloud]清理残留的连接对象")
  1326. if config.transport == "tcp" then
  1327. socket.close(connection)
  1328. socket.release(connection)
  1329. elseif config.transport == "mqtt" then
  1330. connection:disconnect()
  1331. connection:close()
  1332. end
  1333. connection = nil
  1334. sys.wait(50) -- 在协程中安全等待
  1335. end
  1336. -- 重置连接状态但保持服务开启状态
  1337. is_connected = false
  1338. is_authenticated = false
  1339. -- 执行重连
  1340. local success, err = excloud.open()
  1341. if not success then
  1342. log.error("[excloud]重连失败:", err)
  1343. -- 重连失败会再次触发schedule_reconnect
  1344. else
  1345. log.info("[excloud]重连操作已发起")
  1346. end
  1347. end)
  1348. end, config.reconnect_interval * 1000)
  1349. end
  1350. -- TCP socket事件回调函数
  1351. local function tcp_socket_callback(netc, event, param)
  1352. log.info("[excloud]socket cb", netc, event, param)
  1353. -- 取消连接超时定时器
  1354. if connect_timeout_timer then
  1355. sys.timerStop(connect_timeout_timer)
  1356. connect_timeout_timer = nil
  1357. end
  1358. -- 记录连接状态变化的运维日志
  1359. if config.aircloud_mtn_log_enabled then
  1360. if event == socket.LINK then
  1361. exmtn.log("info", "aircloud", "net_conn", "网络连接成功")
  1362. elseif event == socket.ON_LINE then
  1363. exmtn.log("info", "aircloud", "net_conn", "TCP连接成功", "host", config.host, "port", config.port)
  1364. elseif event == socket.CLOSED then
  1365. exmtn.log("info", "aircloud", "net_conn", "TCP连接断开", "param", param)
  1366. end
  1367. end
  1368. if param ~= 0 then
  1369. log.info("[excloud]socket", "连接断开")
  1370. is_connected = false
  1371. is_authenticated = false
  1372. if callback_func then
  1373. callback_func("disconnect", {})
  1374. end
  1375. -- 连接断开,释放资源
  1376. socket.release(connection)
  1377. connection = nil
  1378. -- 尝试重连
  1379. if config.auto_reconnect and is_open then
  1380. -- is_open = false
  1381. schedule_reconnect()
  1382. end
  1383. return
  1384. end
  1385. if event == socket.LINK then
  1386. -- 网络连接成功
  1387. log.info("[excloud]socket", "网络连接成功")
  1388. elseif event == socket.ON_LINE then
  1389. -- TCP连接成功
  1390. log.info("[excloud]socket", "TCP连接成功")
  1391. is_connected = true
  1392. -- 重置重连计数,如果是重连的话,连接上服务器给重连计数重置为0
  1393. reconnect_count = 0
  1394. if callback_func then
  1395. callback_func("connect_result", { success = true })
  1396. end
  1397. -- 发送认证请求
  1398. send_auth_request()
  1399. elseif event == socket.EVENT then
  1400. -- 有数据到达
  1401. socket.rx(netc, rxbuff)
  1402. if rxbuff:used() > 0 then
  1403. local data = rxbuff:query()
  1404. log.info("[excloud]socket", "收到数据", #data, "字节", data:toHex())
  1405. -- 处理接收到的数据
  1406. parse_data(data)
  1407. end
  1408. -- -- 清空缓冲区
  1409. rxbuff:del()
  1410. elseif event == socket.TX_OK then
  1411. socket.wait(netc)
  1412. log.info("[excloud]socket", "发送完成")
  1413. elseif event == socket.CLOSED then
  1414. -- 连接错误或关闭
  1415. socket.release(connection)
  1416. connection = nil
  1417. log.info("[excloud]socket", "主动断开链接")
  1418. end
  1419. end
  1420. -- mqtt client的事件回调函数
  1421. local function mqtt_client_event_cbfunc(connected, event, data, payload, metas)
  1422. log.info("[excloud]mqtt_client_event_cbfunc", event, data, payload, json.encode(metas))
  1423. -- 取消连接超时定时器
  1424. if connect_timeout_timer then
  1425. sys.timerStop(connect_timeout_timer)
  1426. connect_timeout_timer = nil
  1427. end
  1428. -- 记录MQTT状态变化的运维日志
  1429. if config.aircloud_mtn_log_enabled then
  1430. if event == "conack" then
  1431. exmtn.log("info", "aircloud", "mqtt_conn", "MQTT连接成功", "host", config.host)
  1432. elseif event == "disconnect" then
  1433. exmtn.log("info", "aircloud", "mqtt_conn", "MQTT连接断开")
  1434. elseif event == "error" then
  1435. exmtn.log("info", "aircloud", "mqtt_error", "MQTT错误", "type", data, "code", payload)
  1436. end
  1437. end
  1438. -- mqtt连接成功
  1439. if event == "conack" then
  1440. is_connected = true
  1441. log.info("[excloud]MQTT connected")
  1442. -- 重置重连计数,如果是重连的话,连接上服务器给重连计数重置为0
  1443. reconnect_count = 0
  1444. -- 订阅主题
  1445. local device_id_hex = string.toHex(device_id_binary)
  1446. local auth_topic = "/AirCloud/down/" .. device_id_hex .. "/auth"
  1447. local all_topic = "/AirCloud/down/" .. device_id_hex .. "/all"
  1448. log.info("[excloud]mqtt_client_event_cbfunc", "订阅主题", auth_topic, all_topic)
  1449. connection:subscribe(auth_topic, 0)
  1450. connection:subscribe(all_topic, 0)
  1451. if callback_func then
  1452. callback_func("connect_result", { success = true })
  1453. end
  1454. -- 发送认证请求
  1455. send_auth_request()
  1456. -- 订阅成功
  1457. elseif event == "suback" then
  1458. -- 取消订阅成功
  1459. elseif event == "unsuback" then
  1460. -- 接收到服务器下发的publish数据
  1461. -- data:string类型,表示topic
  1462. -- payload:string类型,表示payload
  1463. -- metas:table类型,数据内容如下
  1464. -- {
  1465. -- qos: number类型,取值范围0,1,2
  1466. -- retain:number类型,取值范围0,1
  1467. -- dup:number类型,取值范围0,1
  1468. -- message_id: number类型
  1469. -- }
  1470. elseif event == "recv" then
  1471. log.info("[excloud]接收到MQTT消息",
  1472. "主题:", data,
  1473. "数据长度:", #payload,
  1474. "QoS:", metas and metas.qos or "unknown",
  1475. "消息ID:", metas and metas.message_id or "unknown")
  1476. -- 对接收到的publish数据处理
  1477. parse_data(payload)
  1478. -- 发送成功publish数据
  1479. -- data:number类型,表示message id
  1480. elseif event == "sent" then
  1481. -- 服务器断开mqtt连接
  1482. elseif event == "disconnect" then
  1483. is_connected = false
  1484. is_authenticated = false
  1485. log.info("[excloud]MQTT disconnected")
  1486. if callback_func then
  1487. callback_func("disconnect", {})
  1488. end
  1489. -- 尝试重连
  1490. if config.auto_reconnect and is_open then
  1491. -- is_open = false
  1492. schedule_reconnect()
  1493. end
  1494. -- 收到服务器的心跳应答
  1495. elseif event == "pong" then
  1496. -- 严重异常,本地会主动断开连接
  1497. -- data:string类型,表示具体的异常,有以下几种:
  1498. -- "connect":tcp连接失败
  1499. -- "tx":数据发送失败
  1500. -- "conack":mqtt connect后,服务器应答CONNACK鉴权失败,失败码为payload(number类型)
  1501. -- "other":其他异常
  1502. elseif event == "error" then
  1503. is_connected = false
  1504. is_authenticated = false
  1505. local error_msg = "Unknown MQTT error"
  1506. if data == "connect" then
  1507. error_msg = "TCP connection failed"
  1508. -- 连接失败,应该考虑重新获取服务器信息
  1509. if reconnect_count >= config.max_reconnect and config.use_getip then
  1510. log.info("[excloud]MQTT连接多次失败,需要重新获取服务器信息")
  1511. config.current_conninfo = nil
  1512. end
  1513. elseif data == "tx" then
  1514. error_msg = "Data transmission failed"
  1515. elseif data == "conack" then
  1516. error_msg = "MQTT authentication failed with code: " .. tostring(payload)
  1517. else
  1518. error_msg = "Other MQTT error: " .. tostring(data)
  1519. end
  1520. log.info("[excloud]MQTT error: " .. error_msg)
  1521. if callback_func then
  1522. callback_func("disconnect", { error = error_msg })
  1523. end
  1524. -- 安全释放连接资源
  1525. if connection then
  1526. connection:disconnect()
  1527. connection:close()
  1528. connection = nil
  1529. end
  1530. -- 尝试重连
  1531. if config.auto_reconnect and is_open then
  1532. -- is_open = false
  1533. schedule_reconnect()
  1534. end
  1535. end
  1536. end
  1537. -- 设置配置参数
  1538. function excloud.setup(params)
  1539. if is_open then
  1540. return false, "excloud is already open"
  1541. end
  1542. -- 合并配置参数
  1543. for k, v in pairs(params) do
  1544. config[k] = v
  1545. end
  1546. -- 验证必要参数
  1547. if not config.auth_key then
  1548. return false, "auth_key is required"
  1549. end
  1550. if config.device_type == 1 then
  1551. config.device_id = mobile.imei()
  1552. log.info("[excloud]4G设备", "IMEI:", config.device_id, "MUID:", mobile.muid())
  1553. elseif config.device_type == 2 then
  1554. config.device_id = wlan.getMac(nil, true)
  1555. --以太网设备
  1556. elseif config.device_type == 4 then
  1557. config.device_id = netdrv.mac(socket.LWIP_ETH)
  1558. elseif config.device_type == 9 then
  1559. -- 虚拟设备:验证手机号和序列号
  1560. if not config.virtual_phone_number then
  1561. return false, "虚拟设备需要配置 virtual_phone_number"
  1562. end
  1563. -- 验证手机号格式(11位数字)
  1564. local phone_clean = config.virtual_phone_number:gsub("%D", "")
  1565. if #phone_clean ~= 11 then
  1566. return false, "虚拟手机号必须为11位数字"
  1567. end
  1568. -- 设置默认序列号(如果未提供)
  1569. if config.virtual_serial_num == nil then
  1570. config.virtual_serial_num = 0
  1571. end
  1572. -- 序列号范围检查(0-999)
  1573. config.virtual_serial_num = config.virtual_serial_num % 1000
  1574. -- 生成设备ID:手机号 + 3位序列号
  1575. local serial_str = string.format("%03d", config.virtual_serial_num)
  1576. config.device_id = phone_clean .. serial_str
  1577. log.info("虚拟设备配置", "手机号:", config.virtual_phone_number, "序列号:", serial_str, "设备ID:", config.device_id)
  1578. else
  1579. log.info("[excloud]未知设备类型", config.device_type)
  1580. config.device_id = "unknown"
  1581. end
  1582. -- 打包设备id
  1583. device_id_binary = packDeviceInfo(config.device_type, config.device_id)
  1584. -- 初始化运维日志模块
  1585. local mtn_ok, mtn_err = init_mtn_log()
  1586. if not mtn_ok then
  1587. log.warn("[excloud]运维日志初始化失败,但继续excloud初始化:", mtn_err)
  1588. end
  1589. log.info("[excloud]excloud.setup", "初始化成功", "设备ID:", config.device_id)
  1590. return true
  1591. end
  1592. -- 注册回调函数
  1593. function excloud.on(cbfunc)
  1594. if type(cbfunc) ~= "function" then
  1595. return false, "Callback must be a function"
  1596. end
  1597. callback_func = cbfunc
  1598. return true
  1599. end
  1600. -- 开启excloud服务
  1601. function excloud.open()
  1602. -- 如果之前连接异常断开,但状态未重置,先清理
  1603. if is_open and not is_connected then
  1604. log.warn("[excloud]检测到状态不一致,先清理残留状态")
  1605. excloud.close()
  1606. end
  1607. -- 检查是否已打开
  1608. if is_open and is_connected then
  1609. return false, "excloud is already open and connected"
  1610. end
  1611. reconnect_count = 0
  1612. -- 判断是否初始化
  1613. if not device_id_binary then
  1614. return false, "excloud 没有初始化,请先调用setup"
  1615. end
  1616. -- 根据use_getip决定是否使用getip服务
  1617. if config.use_getip then
  1618. -- 使用getip服务发现
  1619. local getip_type
  1620. if config.transport == "tcp" then
  1621. getip_type = 3
  1622. elseif config.transport == "udp" then
  1623. getip_type = 4
  1624. elseif config.transport == "mqtt" then
  1625. getip_type = 5
  1626. else
  1627. return false, "不支持的传输协议: " .. config.transport
  1628. end
  1629. -- 获取服务器连接信息
  1630. if not config.current_conninfo or (config.transport ~= "mqtt" and not config.current_conninfo.ipv4) or
  1631. (config.transport == "mqtt" and not config.current_conninfo.ssl) then
  1632. log.info("[excloud]首次连接,获取服务器信息...")
  1633. local ok, result = excloud.getip_with_retry(getip_type)
  1634. if not ok then
  1635. return false, "获取服务器信息失败: " .. result
  1636. end
  1637. -- 更新连接配置
  1638. log.info("[excloud]服务器信息获取成功", "host:", config.host, "port:", config.port, "transport:", config.transport)
  1639. -- 保存文件上传信息
  1640. if result.imginfo then
  1641. config.current_imginfo = result.imginfo
  1642. end
  1643. if result.audinfo then
  1644. config.current_audinfo = result.audinfo
  1645. end
  1646. end
  1647. else
  1648. -- 不使用getip,直接使用用户配置的host和port
  1649. log.info("使用手动配置的服务器地址", config.host, config.port)
  1650. if not config.host or not config.port then
  1651. return false, "use_getip为false时,必须配置host和port"
  1652. end
  1653. end
  1654. -- 根据传输协议创建连接
  1655. if config.transport == "tcp" then
  1656. -- 创建接收缓冲区
  1657. rxbuff = zbuff.create(2048)
  1658. -- 创建TCP连接
  1659. log.info("[excloud]创建TCP连接")
  1660. connection = socket.create(nil, tcp_socket_callback)
  1661. if not connection then
  1662. return false, "Failed to create socket"
  1663. end
  1664. -- 准备SSL配置参数
  1665. local ssl_config = nil
  1666. if config.ssl then
  1667. if type(config.ssl) == "table" then
  1668. -- 使用详细的SSL配置
  1669. ssl_config = config.ssl
  1670. else
  1671. -- 简单的SSL启用
  1672. ssl_config = true
  1673. end
  1674. end
  1675. -- 配置socket参数
  1676. local config_success = socket.config(
  1677. connection,
  1678. config.local_port, -- 本地端口号
  1679. false, -- 是否是UDP,TCP连接为false
  1680. ssl_config and true or false, -- 是否是加密传输
  1681. config.keep_idle, -- keepalive idle时间
  1682. config.keep_interval, -- keepalive 探测间隔
  1683. config.keep_cnt, -- keepalive 探测次数
  1684. ssl_config and ssl_config.server_cert or nil, -- 服务器CA证书
  1685. ssl_config and ssl_config.client_cert or nil, -- 客户端证书
  1686. ssl_config and ssl_config.client_key or nil, -- 客户端私钥
  1687. ssl_config and ssl_config.client_password or nil -- 客户端私钥口令
  1688. )
  1689. if not config_success then
  1690. socket.release(connection)
  1691. connection = nil
  1692. return false, "Socket config failed"
  1693. end
  1694. socket.debug(connection, true)
  1695. -- 设置连接超时定时器
  1696. connect_timeout_timer = sys.timerStart(function()
  1697. if not is_connected then
  1698. log.error("TCP connection timeout")
  1699. if connection then
  1700. socket.close(connection)
  1701. socket.release(connection)
  1702. connection = nil
  1703. end
  1704. if callback_func then
  1705. callback_func("connect_result", { success = false, error = "Connection timeout" })
  1706. end
  1707. -- 尝试重连
  1708. if config.auto_reconnect and is_open then
  1709. -- is_open = false
  1710. schedule_reconnect()
  1711. end
  1712. end
  1713. end, config.timeout * 1000)
  1714. -- 连接到服务器
  1715. local ok, result = socket.connect(connection, config.host, config.port, config.mqtt_ipv6)
  1716. log.info("[excloud]TCP连接结果", ok, result)
  1717. if not ok then
  1718. --发生异常,强制close
  1719. socket.close(connection)
  1720. --释放资源
  1721. socket.release(connection)
  1722. connection = nil
  1723. if config.auto_reconnect then
  1724. is_open = false
  1725. schedule_reconnect()
  1726. end
  1727. return false, result
  1728. end
  1729. elseif config.transport == "mqtt" then
  1730. -- 准备MQTT SSL配置 - MQTT连接默认使用SSL加密
  1731. local ssl_config = true -- 最简单的SSL加密,不验证服务器证书
  1732. -- 如果有详细的SSL配置,使用详细配置
  1733. if config.ssl and type(config.ssl) == "table" then
  1734. ssl_config = config.ssl
  1735. end
  1736. -- 准备MQTT扩展参数
  1737. local mqtt_opts = {
  1738. rxSize = config.mqtt_rx_size or 32 * 1024, -- MQTT接收缓冲区大小,默认32K
  1739. conn_timeout = config.mqtt_conn_timeout or 30, -- MQTT连接超时时间,默认30秒
  1740. ipv6 = config.mqtt_ipv6 or false -- 是否使用IPv6连接,默认false
  1741. }
  1742. -- 创建MQTT客户端
  1743. connection = mqtt.create(nil, config.host, config.port, ssl_config, mqtt_opts)
  1744. if not connection then
  1745. return false, "Failed to create MQTT client"
  1746. end
  1747. -- 开启调试信息(可选)
  1748. if config.debug then
  1749. connection:debug(true)
  1750. end
  1751. -- 设置真实的MQTT认证信息
  1752. local client_id, username, password
  1753. if config.device_type == 1 then -- 4G设备
  1754. client_id = mobile.imei()
  1755. username = mobile.imei()
  1756. password = mobile.muid()
  1757. -- elseif config.device_type == 2 then -- WIFI设备
  1758. -- client_id = wlan.getMac(nil, true)
  1759. -- username = wlan.getMac(nil, true)
  1760. -- password = mobile.muid():toHex()
  1761. -- elseif config.device_type == 4 then -- 以太网设备
  1762. -- client_id = netdrv.mac(socket.LWIP_ETH)
  1763. -- username = netdrv.mac(socket.LWIP_ETH)
  1764. -- password = mobile.muid():toHex()
  1765. -- elseif config.device_type == 9 then -- 虚拟设备
  1766. -- -- 虚拟设备使用配置的设备ID
  1767. -- client_id = config.device_id
  1768. -- username = config.device_id
  1769. -- password = config.auth_key or config.device_id
  1770. else
  1771. return false, "MQTT connect failed, device_type not supported"
  1772. end
  1773. log.info("[excloud]MQTT认证信息",
  1774. "client_id:", client_id,
  1775. "username:", username,
  1776. "password:", password)
  1777. -- 设置认证信息(使用真实的设备信息,而不是getip返回的提示)
  1778. connection:auth(client_id, username, password, config.clean_session)
  1779. -- 设置保持连接间隔
  1780. connection:keepalive(config.keepalive or 240) -- 默认240秒
  1781. -- 设置遗嘱消息(如果需要)
  1782. if config.will_topic and config.will_payload then
  1783. local will_result = connection:will(
  1784. config.will_topic,
  1785. config.will_payload,
  1786. config.will_qos or 0,
  1787. config.will_retain or 0
  1788. )
  1789. if not will_result then
  1790. log.warn("[excloud]设置遗嘱消息失败")
  1791. end
  1792. end
  1793. -- 设置自动重连
  1794. if config.auto_reconnect then
  1795. connection:autoreconn(true, (config.reconnect_interval or 10) * 1000) -- 转换为毫秒
  1796. end
  1797. -- 注册事件回调
  1798. connection:on(mqtt_client_event_cbfunc)
  1799. -- 设置连接超时定时器
  1800. connect_timeout_timer = sys.timerStart(function()
  1801. if not is_connected then
  1802. log.error("MQTT connection timeout")
  1803. if connection then
  1804. connection:disconnect()
  1805. connection:close()
  1806. connection = nil
  1807. end
  1808. if callback_func then
  1809. callback_func("connect_result", { success = false, error = "Connection timeout" })
  1810. end
  1811. -- 尝试重连
  1812. if config.auto_reconnect and is_open then
  1813. -- is_open = false
  1814. schedule_reconnect()
  1815. end
  1816. end
  1817. end, config.timeout * 1000)
  1818. -- 连接到服务器
  1819. local ok = connection:connect()
  1820. if not ok then
  1821. --连接失败,释放资源
  1822. connection:close()
  1823. connection = nil
  1824. -- 发起连接失败,尝试重连
  1825. if config.auto_reconnect then
  1826. is_open = false
  1827. schedule_reconnect()
  1828. end
  1829. return false, "MQTT connect failed"
  1830. end
  1831. else
  1832. return false, "Unsupported transport: " .. config.transport
  1833. end
  1834. is_open = true
  1835. -- 记录服务启动日志
  1836. if config.aircloud_mtn_log_enabled then
  1837. exmtn.log("info", "aircloud", "system", "excloud服务启动", "transport", config.transport, "host", config.host, "port",
  1838. config.port)
  1839. end
  1840. log.info("[excloud]excloud service started")
  1841. return true
  1842. end
  1843. -- 发送数据
  1844. -- 发送消息到云端
  1845. -- @param data table 待发送的数据,每个元素是一个包含 field_meaning、data_type 和 value 的表
  1846. -- @param need_reply boolean 是否需要服务器回复,默认为 false
  1847. -- @param is_auth_msg boolean 是否是鉴权消息,默认为 false
  1848. function excloud.send(data, need_reply, is_auth_msg)
  1849. if not is_open then
  1850. return false, "excloud服务未开启"
  1851. end
  1852. if not is_connected then
  1853. return false, "未连接到服务器"
  1854. end
  1855. -- if not is_authenticated and not is_auth_msg then
  1856. -- return false, "设备未认证"
  1857. -- end
  1858. -- 检查参数是否为table
  1859. if type(data) ~= "table" then
  1860. return false, "data must be table"
  1861. end
  1862. if need_reply == nil then
  1863. need_reply = false
  1864. end
  1865. if is_auth_msg == nil then
  1866. is_auth_msg = false
  1867. end
  1868. -- 检查服务是否开启
  1869. if not is_open then
  1870. if callback_func then
  1871. callback_func("send_result", {
  1872. success = false,
  1873. error_msg = "excloud not open"
  1874. })
  1875. end
  1876. return false, "excloud not open"
  1877. end
  1878. -- 检查是否已连接
  1879. if not is_connected then
  1880. if callback_func then
  1881. callback_func("send_result", {
  1882. success = false,
  1883. error_msg = "excloud not connected"
  1884. })
  1885. end
  1886. return false, "excloud not connected"
  1887. end
  1888. -- 保存当前序列号用于回调
  1889. local current_sequence = sequence_num
  1890. -- 构建消息体
  1891. local message_body = ""
  1892. local parts = {}
  1893. for _, item in ipairs(data) do
  1894. log.info("[excloud]构建发送数据", item.field_meaning, item.data_type, item.value, message_body)
  1895. local success, tlv = build_tlv(item.field_meaning, item.data_type, item.value)
  1896. if not success then
  1897. return false, "excloud.send data is failed"
  1898. end
  1899. table.insert(parts, tlv)
  1900. -- message_body = message_body .. tlv
  1901. end
  1902. if #parts > 0 then
  1903. message_body = table.concat(parts)
  1904. parts = {}
  1905. else
  1906. log.warn("[excloud]没有有效的TLV数据可发送")
  1907. -- return false, "No valid TLV data to send"
  1908. end
  1909. -- 检查消息长度
  1910. local udp_auth_key = config.udp_auth_key and true or false
  1911. local total_length = #message_body + (udp_auth_key and 64 or 0)
  1912. log.info("[excloud]tlv发送数据长度4", total_length)
  1913. -- 构建消息头
  1914. local is_udp_transport = (config.transport == "udp")
  1915. local header = build_header(need_reply, is_udp_transport, total_length)
  1916. -- -- 添加鉴权key(如果是UDP的话)
  1917. local auth_key_part = ""
  1918. if config.transport == "udp" and udp_auth_key then
  1919. auth_key_part = config.udp_auth_key
  1920. if #auth_key_part < 64 then
  1921. auth_key_part = auth_key_part .. string.rep("\0", 64 - #auth_key_part)
  1922. elseif #auth_key_part > 64 then
  1923. auth_key_part = auth_key_part:sub(1, 64)
  1924. end
  1925. end
  1926. local full_message
  1927. -- 发送完整消息
  1928. if config.transport == "udp" then
  1929. full_message = header .. auth_key_part .. message_body
  1930. else
  1931. full_message = header .. message_body
  1932. end
  1933. log.info("[excloud]发送消息长度", #header, #message_body, #full_message, full_message:toHex())
  1934. local success, err_msg
  1935. if config.transport == "tcp" then
  1936. if not connection then
  1937. err_msg = "TCP connection not available"
  1938. success = false
  1939. else
  1940. success, err_msg = socket.tx(connection, full_message)
  1941. end
  1942. elseif config.transport == "mqtt" then
  1943. -- 根据是否为鉴权消息选择不同的topic
  1944. local topic
  1945. local device_id_hex = string.toHex(device_id_binary)
  1946. if is_auth_msg then
  1947. topic = "/AirCloud/up/" .. device_id_hex .. "/auth"
  1948. else
  1949. topic = "/AirCloud/up/" .. device_id_hex .. "/all"
  1950. end
  1951. log.info("[excloud]发布主题", topic, #full_message, full_message:toHex())
  1952. local message_id = connection:publish(topic, full_message, config.qos, config.retain)
  1953. if message_id then
  1954. success = true
  1955. if config.qos and config.qos > 0 then
  1956. log.info("[excloud]MQTT消息发布成功", "消息ID:", message_id)
  1957. else
  1958. log.info("[excloud]MQTT消息发布成功")
  1959. end
  1960. else
  1961. success = false
  1962. err_msg = "MQTT publish failed"
  1963. end
  1964. end
  1965. -- 通过回调返回发送结果
  1966. if callback_func then
  1967. callback_func("send_result", {
  1968. success = success,
  1969. error_msg = success and "Send successful" or err_msg,
  1970. sequence_num = current_sequence
  1971. })
  1972. end
  1973. collectgarbage("collect")
  1974. if success then
  1975. log.info("[excloud]数据发送成功", #full_message, "字节")
  1976. return true
  1977. else
  1978. log.error("数据发送失败", err_msg)
  1979. return false, err_msg
  1980. end
  1981. end
  1982. -- 关闭excloud服务
  1983. function excloud.close()
  1984. if not is_open then
  1985. return false, "excloud not open"
  1986. end
  1987. -- 停止所有定时器
  1988. if reconnect_timer then
  1989. sys.timerStop(reconnect_timer)
  1990. reconnect_timer = nil
  1991. end
  1992. if connect_timeout_timer then
  1993. sys.timerStop(connect_timeout_timer)
  1994. connect_timeout_timer = nil
  1995. end
  1996. -- 停止心跳
  1997. excloud.stop_heartbeat()
  1998. -- 关闭连接
  1999. if connection then
  2000. if config.transport == "tcp" then
  2001. socket.close(connection)
  2002. socket.release(connection)
  2003. elseif config.transport == "mqtt" then
  2004. -- 断开连接并释放资源
  2005. connection:disconnect()
  2006. connection:close()
  2007. end
  2008. connection = nil
  2009. end
  2010. -- 释放缓冲区
  2011. if rxbuff then
  2012. rxbuff = nil
  2013. end
  2014. -- 清空队列
  2015. pending_messages = {}
  2016. callback_func = nil
  2017. -- 记录服务关闭日志
  2018. if config.aircloud_mtn_log_enabled then
  2019. exmtn.log("info", "aircloud", "system", "excloud服务关闭")
  2020. end
  2021. -- 重置状态
  2022. is_open = false
  2023. is_connected = false
  2024. is_authenticated = false
  2025. pending_messages = {}
  2026. rxbuff = nil
  2027. reconnect_count = 0
  2028. is_heartbeat_running = false
  2029. collectgarbage("collect")
  2030. log.info("[excloud]excloud service stopped")
  2031. return true
  2032. end
  2033. -- 获取当前状态
  2034. function excloud.status()
  2035. return {
  2036. is_open = is_open,
  2037. is_connected = is_connected,
  2038. is_authenticated = is_authenticated,
  2039. sequence_num = sequence_num,
  2040. reconnect_count = reconnect_count,
  2041. pending_messages = #pending_messages,
  2042. }
  2043. end
  2044. -- 发送心跳消息
  2045. -- @param custom_data table 可选参数,自定义心跳内容
  2046. -- @param need_reply boolean 是否需要服务器回复,默认为false
  2047. -- @return boolean 是否发送成功
  2048. -- @return string 错误信息(如果失败)
  2049. function excloud.heartbeat(custom_data, need_reply)
  2050. -- 如果心跳数据未提供,则使用默认的心跳数据(空表)
  2051. local data = custom_data or heartbeat_data
  2052. -- 设置默认不需要回复
  2053. if need_reply == nil then
  2054. need_reply = false
  2055. end
  2056. -- 调用send函数发送心跳数据
  2057. return excloud.send(data, need_reply, false)
  2058. end
  2059. -- 启动自动心跳
  2060. -- @param interval number 心跳间隔(秒),默认300秒(5分钟)
  2061. -- @param custom_data table 自定义心跳内容,默认空表
  2062. -- @return boolean 是否启动成功
  2063. function excloud.start_heartbeat(interval, custom_data)
  2064. -- 停止现有的心跳定时器
  2065. if is_heartbeat_running then
  2066. excloud.stop_heartbeat()
  2067. end
  2068. -- 设置心跳间隔,默认5分钟
  2069. heartbeat_interval = interval or 300
  2070. -- 设置心跳数据
  2071. heartbeat_data = custom_data or {}
  2072. -- 创建并启动心跳定时器
  2073. heartbeat_timer = sys.timerLoopStart(function()
  2074. if is_open and is_connected then
  2075. local ok, err_msg = excloud.heartbeat()
  2076. if not ok then
  2077. log.info("[excloud]excloud", "心跳发送失败: " .. err_msg)
  2078. else
  2079. log.info("[excloud]excloud", "心跳发送成功")
  2080. end
  2081. end
  2082. end, heartbeat_interval * 1000) -- 转换为毫秒
  2083. is_heartbeat_running = true
  2084. log.info("[excloud]excloud", "自动心跳已启动,间隔 " .. heartbeat_interval .. " 秒")
  2085. return true
  2086. end
  2087. -- 停止自动心跳
  2088. -- @return boolean 是否停止成功
  2089. function excloud.stop_heartbeat()
  2090. if heartbeat_timer then
  2091. sys.timerStop(heartbeat_timer)
  2092. heartbeat_timer = nil
  2093. is_heartbeat_running = false
  2094. log.info("[excloud]excloud", "自动心跳已停止")
  2095. return true
  2096. end
  2097. return false
  2098. end
  2099. -- 获取当前服务器信息
  2100. function excloud.get_server_info()
  2101. return {
  2102. conninfo = config.current_conninfo,
  2103. imginfo = config.current_imginfo,
  2104. audinfo = config.current_audinfo,
  2105. mtninfo = config.current_mtninfo -- 新增:运维日志上传信息
  2106. }
  2107. end
  2108. -- 强制刷新服务器信息
  2109. -- function excloud.refresh_server_info()
  2110. -- config.current_conninfo = nil
  2111. -- config.current_imginfo = nil
  2112. -- config.current_audinfo = nil
  2113. -- return true
  2114. -- end
  2115. -- 导出常量
  2116. excloud.DATA_TYPES = DATA_TYPES
  2117. excloud.FIELD_MEANINGS = FIELD_MEANINGS
  2118. excloud.MTN_LOG_STATUS = MTN_LOG_STATUS
  2119. excloud.MTN_LOG_CACHE_WRITE = exmtn.CACHE_WRITE
  2120. excloud.MTN_LOG_ADD_WRITE = exmtn.ADD_WRITE
  2121. return excloud