From 179b30a94404defd401d00720ed620c3743986df Mon Sep 17 00:00:00 2001 From: yrx Date: Tue, 1 Sep 2026 11:07:46 +0800 Subject: [PATCH] two --- .vscode/settings.json | 3 + 4g.py | 114 +++- __pycache__/config.cpython-312.pyc | Bin 0 -> 8494 bytes __pycache__/gpio_high.cpython-312.pyc | Bin 0 -> 1373 bytes __pycache__/hardware.cpython-312.pyc | Bin 0 -> 8852 bytes __pycache__/main.cpython-312.pyc | Bin 0 -> 18448 bytes __pycache__/version.cpython-310.pyc | Bin 0 -> 273 bytes app.yaml | 5 + config.py | 23 + gpio_high.py | 27 + hardware.py | 80 ++- laser_manager.py | 116 ++-- main.py | 6 +- network.py | 2 +- official_pack_fip.py | 603 ++++++++++++++++++ power.py | 14 +- .../wifi_gpio_ina_isolation.cpython-312.pyc | Bin 0 -> 5878 bytes .../wifi_gpio_isolation.cpython-312.pyc | Bin 0 -> 4324 bytes test/test_gpio_a14.py | 62 ++ test/wifi_gpio_ina_isolation.py | 122 ++++ test/wifi_gpio_isolation.py | 88 +++ version.py | 2 +- 22 files changed, 1164 insertions(+), 103 deletions(-) create mode 100644 .vscode/settings.json create mode 100644 __pycache__/config.cpython-312.pyc create mode 100644 __pycache__/gpio_high.cpython-312.pyc create mode 100644 __pycache__/hardware.cpython-312.pyc create mode 100644 __pycache__/main.cpython-312.pyc create mode 100644 __pycache__/version.cpython-310.pyc create mode 100644 gpio_high.py create mode 100644 official_pack_fip.py create mode 100644 test/__pycache__/wifi_gpio_ina_isolation.cpython-312.pyc create mode 100644 test/__pycache__/wifi_gpio_isolation.cpython-312.pyc create mode 100644 test/test_gpio_a14.py create mode 100644 test/wifi_gpio_ina_isolation.py create mode 100644 test/wifi_gpio_isolation.py diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..2d92cd3 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "cmake.sourceDirectory": "E:/code/code/code/new/new/new/new/new/nw/archery - 副本/cpp_ext" +} \ No newline at end of file diff --git a/4g.py b/4g.py index 6763c55..0fb5594 100644 --- a/4g.py +++ b/4g.py @@ -109,6 +109,7 @@ from maix import app, uart, pinmap, time import hashlib import hmac +import re import ujson # ========== 配置 ========== @@ -130,53 +131,109 @@ def generate_token(device_id): return "Arrow_" + hmac.new((SALT + device_id).encode(), SALT2.encode(), hashlib.sha256).hexdigest() def send_cmd(cmd_str, timeout_ms=3000): - """发送 AT 指令并等待 OK / ERROR""" + """发送 AT 指令并返回完整响应;超时返回已收到的内容。""" print("[AT] =>", cmd_str) http_serial.write((cmd_str + "\r\n").encode()) buffer = b"" start = time.ticks_ms() - while time.ticks_ms() - start < timeout_ms: + while time.ticks_diff(time.ticks_ms(), start) < timeout_ms: data = http_serial.read(128) if data: buffer += data try: - decoded = buffer.decode() - print("<= ", decoded.strip()) - if "OK" in decoded: - return True - if "+CME ERROR" in decoded or "ERROR" in decoded: - return False + decoded = buffer.decode("utf-8", "ignore") + if "OK" in decoded or "+CME ERROR" in decoded or "ERROR" in decoded: + print("[AT] <=", decoded.strip()) + return decoded except: pass time.sleep_ms(10) + decoded = buffer.decode("utf-8", "ignore") + print("[AT] !! timeout", timeout_ms, "ms, response:", decoded.strip() or "") + return decoded + + +def response_ok(response): + return "OK" in response and "ERROR" not in response + + +def wait_modem_ready(): + """等待模组响应,并确认 PDP 上下文已经获得 IP。""" + for attempt in range(15): + if response_ok(send_cmd("AT", 1000)): + break + print("[4G] 等待模组启动", attempt + 1, "/15") + time.sleep_ms(1000) + else: + print("[4G] UART2 无 AT 响应,请检查模组供电、A28/A29 接线和串口占用") + return False + + send_cmd("ATE0", 1000) + cpin = send_cmd("AT+CPIN?", 3000) + if "READY" not in cpin: + print("[4G] SIM 卡未就绪:", cpin.strip()) + return False + + addr = send_cmd("AT+CGPADDR=1", 3000) + match = re.search(r'\+CGPADDR:\s*1,"([^\"]+)"', addr) + if match and match.group(1) != "0.0.0.0": + print("[4G] PDP ready, IP:", match.group(1)) + return True + + send_cmd("AT+MIPCALL=1,1", 15000) + for _ in range(20): + addr = send_cmd("AT+CGPADDR=1", 3000) + match = re.search(r'\+CGPADDR:\s*1,"([^\"]+)"', addr) + if match and match.group(1) != "0.0.0.0": + print("[4G] PDP ready, IP:", match.group(1)) + return True + time.sleep_ms(1000) + + print("[4G] PDP 未获得 IP,请检查 SIM 流量、信号和 APN") return False + +def clear_http_instances(): + for instance_id in range(6): + send_cmd(f"AT+MHTTPDEL={instance_id}", 1200) + def create_http_instance(url): cmd = f'AT+MHTTPCREATE="{url}"' - if send_cmd(cmd): - # 尝试提取 instance ID(如果模块返回) - # 注意:部分模块不会返回 ID,可忽略,直接用 0 或 1 - return True - return False + response = send_cmd(cmd, 8000) + match = re.search(r"\+MHTTPCREATE:\s*(\d+)", response) + if not response_ok(response) or not match: + print("❌ 创建 HTTP 实例失败,模组响应:", response.strip() or "") + return None + return int(match.group(1)) def send_http_request(url, api_path, token, device_id, json_data): # 1. 创建 HTTP 实例 - if not create_http_instance(url): - print("❌ 创建 HTTP 实例失败") + instance_id = create_http_instance(url) + if instance_id is None: return False - # 2. 设置 Headers(假设实例 ID 为 0,或根据模块默认) - instance_id = 0 # 大多数模块默认实例为 0;若支持多实例,需解析返回值 - send_cmd(f'AT+MHTTPCFG="header",{instance_id},"Content-Type: application/json"') - send_cmd(f'AT+MHTTPCFG="header",{instance_id},"Authorization: {token}"') - send_cmd(f'AT+MHTTPCFG="header",{instance_id},"DeviceId: {device_id}"') + # 2. 设置 Headers + commands = ( + f'AT+MHTTPCFG="header",{instance_id},"Content-Type: application/json"', + f'AT+MHTTPCFG="header",{instance_id},"Authorization: {token}"', + f'AT+MHTTPCFG="header",{instance_id},"DeviceId: {device_id}"', + ) + for command in commands: + if not response_ok(send_cmd(command)): + print("❌ HTTP Header 配置失败") + send_cmd(f"AT+MHTTPDEL={instance_id}", 2000) + return False # 3. 发送 Body json_str = ujson.dumps(json_data) - send_cmd(f'AT+MHTTPCONTENT={instance_id},0,0,"{json_str}"') + at_json = json_str.replace("\\", "\\\\").replace('"', '\\"') + if not response_ok(send_cmd(f'AT+MHTTPCONTENT={instance_id},0,0,"{at_json}"', 8000)): + print("❌ HTTP Body 配置失败") + send_cmd(f"AT+MHTTPDEL={instance_id}", 2000) + return False # 4. 发起 POST 请求 - if send_cmd(f'AT+MHTTPREQUEST={instance_id},2,0,"{api_path}"'): + if response_ok(send_cmd(f'AT+MHTTPREQUEST={instance_id},2,0,"{api_path}"', 15000)): print("✅ HTTP 请求已发送") return True else: @@ -199,7 +256,7 @@ def read_response(timeout_ms=5000): print("🚀 启动直接上传流程...") token = generate_token(device_id) -print("🔑 Token:", token) +print("🔑 Token 已生成:", token[:12] + "...") # 构造模拟数据 timestamp = int(time.time() * 1000) @@ -216,9 +273,16 @@ json_data = { } # 执行上传 -if send_http_request(url, api_path, token, device_id, json_data): +upload_ok = False +if not wait_modem_ready(): + print("💥 4G 模组未就绪") +else: + clear_http_instances() + upload_ok = send_http_request(url, api_path, token, device_id, json_data) + +if upload_ok: read_response() else: print("💥 上传流程失败") -print("🔚 程序结束") \ No newline at end of file +print("🔚 程序结束") diff --git a/__pycache__/config.cpython-312.pyc b/__pycache__/config.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..8174d18513dfb390429e4447bf2667e3be8892d7 GIT binary patch literal 8494 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 literal 0 HcmV?d00001 diff --git a/__pycache__/hardware.cpython-312.pyc b/__pycache__/hardware.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..53ebcd2fee7bad7f22713b74ebd83dffe1e0baf5 GIT binary patch literal 8852 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 EZ%0uIjQ{`u literal 0 HcmV?d00001 diff --git a/__pycache__/main.cpython-312.pyc b/__pycache__/main.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..215a5c68ea912cc66ec79f3e2323694061444dc3 GIT binary patch literal 18448 zcmch94Om;pmFSgp^@k*okOUGS5dUEFV=yr`#&!T>jE%uI#IEHyip&*QvVO>wo&Aa6LyiB)li{JbD z&fKfZ6&U}d{dO<*%$b=p=bSk+b7t<`Gx~1|MFIv7ujW;I&khXxElRMDFa~@+!pE?) z7=f8Ef=BQjJSX48cM41bCvL)>LX*%bGKrjGlh`RSNt{xXl!x#Hjs&O7B%@*6Aveh( z4|ga`3OY_SCDO6Tltjl$lM-T~L*-01CDV084z*KlQq!>5p>b+WS{jx(Qkj^0Izkx z%gohhAwp{{dmVd&&vB&nzsj^~d3=y(mHi-Fn~Pa_I65vP;8xzBK252MqrgHerAKM| zH!uP>mF+J($~%f36YRr|@`_V@I4Cb+=A*vI3xkpI=Og`>W`BHX_R@u06PISkUW|Nj z`m@Ueb8mfX?ybR@e|mB5r*F&-4t#cbFd=gC!rV7M{Ot0#XJ3D3X0(6i@B5(mw|&ns zgl0zHjP!pCl2uK{j)tAa`Mz)8`shtaw{_GRBd7XjKX?lgkyEdt+B8{6Hn-MQR<1G5 zzVc7={ikPNKLZ5`k@tTbIXw<+c35o3b{;ntwcC2ER>HWWuK8=l3Aa8xKiBsHl$uOi z6K2l$MTTFRdHuba*HDGZRh5sGR#cQ$RwN|M_n(^iyXQgjne%T$(X9_Zntf$#X6((# z8)qX!ug$&lv$=QPOqfR+EtXKCPK(n@S|~AL^Yl0@$0?!BY3a670!vR1g?nvID~0^bi7 zTAgM@%iKk}o#v`;D3jS-Hm}*{vVa!8&!ZF$i)T^Y6pt6!nmv{yRqZZN{mzT&ibRmaN$$Ln5r`6-3$x|skq}AcJ5N0oF zv$(n)R&%GtVLJ%av~0<7x5I5FtX^xU*G+b^LlVWv`U&9w=c^zD>xV2K=EQn2+Cm7x z3i5h+Jj{zDWc1iYOdKEf^8I{L<>%X_@wp64;N0AiV(8D$v(wgiF)o?nRj@hsL>A&= zLd4M-$_FZ#UBi{K5V?krN>bP~7ZXxK_;$h%!N#Mf7r#&al@F0Z#!2DQJ_J|J;STlY zVqPV~8^ge=HTDV~L8ltfshX|D#e~=|SZv)MB1xR?_F;siSMaQ0=)W*AXJeK~cjhr0on7@Kh^>L_RtP!^h-;G@U zMr2~5#@K3mJPT|jpGrWc%kA@;ogONw^N_XkFqjMr+2aD^VJm?JWxf&-%ZMY!ED14= zF?#cJU>u5-qzdpzEeeqGC=p{eDct7ja#KmjlG$8luua`0&?m(RN8)idphehy}H}Jw+iuAA|C#cBj+j3p3n35!(mJP}Rnl&Lw_05FD zDBmbGM)^{OeoCDgR+n8>mrZMQXLp?0F`66F6ijI{qC!kl9@St%VX=hu`v;=Z%Cw5IV1hO9wY3in9wL3$~oN1-*Zd3-< z?%pwCO8sq2CN+5 zP0}q|!7nBCf-OqHzbb{0ShS|0)rDTuehC((KdnE#KcnB!pV^;v7V|UvIGW^8f#DdY6QLUFyyjkl!#oc0k)LgmoG{%bWW zjlGJ6xY#eSui;WG^mcOGxJ9cw{n*#C5Eq|J^R8!8Tx+Ema95)fgkE^#gnbk~m=;H| zak(1are$-R+1zEvl9ote$1h92SLEIPz&fdfZfPAmoaZck*w)zFmX>f9v{&pG6Z#`O zl27P*#oh4|CN%DlVx3-ud_8T^>hK9g}f6jj!VH zU}P?wV%qJTZdiyL$2{x-t*+oGJ^*VqhsC|$+8rz}E+(qtFgf{L90m&!k8zmCRelAr z<^&Y^i_JqC|H`(K*u>VSNB0^S-R<{sAs%P3V7}yezs_RV`&fvJy(A0UzrjLWOw@6` z8mFn}ISdx^q&4Z@BszCb-r;7F= zmJ%1+PjfH}v3z=yfr6UY+^gxvPNoyelbJ;0Ndv?ggz97#vBkTMt<-J<(8MfU1w=BfPESF)vrv;>3INh?p!r`zGi`$DvUKg8de~pE>7|v5U zO3T@it(<;_b^$d`<0Bju7Gn9vjfrowWn6rCGaL;ax{n3k#_@}r?dQ4LEJSQ4T5LRG zN8Ct!a7#AFQRZ6lcPu?FW?KPva(!HTzQg8naU569dR^qo zS%|UEy=c8%bsrtJaxHab{uSdnbug=s?@u|IHJEOHi{;P7{#1_7V5UFy1RVFBgm};r9tKgF*?@A|zuzmC{Nw?L2P^FzkKZcgH1-q64KQ+idX z7Tub5t_(&8+riK>F5k5yu3h7-cDR@PP&-{W#wh-J!geDwDCQ+tyi z%_e#H!~CJ`$5qEwx$s43A2uu<>S4Y}!q0!t654}`k>wJ`nd7E4$Mt?8d|8fGC@)98(k z-VN;GEYDKg@Q^tXcOn$0)&H?ntHpbLXkWYF>Jxe4yOGm>??c~4_Sd(ybsR9pxuh5) zm-`}by=XjYaX8GL4JQO;tGj&0^$s6i+ zH14i5?`_`PvDMZGyD^G)j8epMw>CC6ZSAn#gcm#?`fydeM2>qzV8|L5iQvmwB4^CE zXDO|vEoadd5u?AZBljLDe5W=$tVgU4NMtxI#}9&I4(+3J)YfG)TYAi0peyuc*@re{ z`}a0)X)dkXdB7NX;e6zSSLa_iHT&Ti=6ilQ^ASFB@gL^i`)-YqGIZ>2u4`>-X*BO^ zYiTp@+}+sHR=3&Q)@nxm(m~}el-IY^HEcI8BwN}V>N*!{n^)YxHe0Jnm6Mo6YUm3?o?67hW- zkTww6(%o&%pbpvrVg5VMM^28;efS~cZ2)d(w)yz{h_cc|z#(c9pJ$&X8DLoo-Y_Dc}P=-6frFAAHW2J1xh| zu`JI#`V>Yb&>hrcCTt|#O-zSG2F8s`S$uYR2t3I^T5uPed37ARiC6AJA1n8_Lz_FE zfSzT_XWmBT#-iAOo%_iPb0783j|@cmE<>goY7_&lnBT^8--%p2RaRE!%UeV#^3#93 zb?K# z5NpL^v1s2IkIGBIn0oQbHr< ze)?k==d{BoT2g4gC1!4aA7jQFL!X}ROVDD5&xgE#m`-K7IYRe3bnrX@CG&rV^$PW} z1!jCF>Sgvp_0Yund?Vz(Dmse|iafmIn3pZbmh;OXzwU*v^vtuI{~@~~_wu|_7H@Fj zOE~SFC>)f+7k?Rsxp)MxS3vOn=*zlbFabi`&w~&=(;yTLN+1*uiXoKv1%&imqvCJpsJ;DtqxkA(=cVv?B+qC67OD58#q{k#6WGJ|9jUMFj7p zl#rj4KrAM(lL^PMVnr*x#Q9R7!v2}@S0XRHI5YS@IOQHNGMe9T0ylbB(P0H_1dOf? zCxk|)jfpJBP|9+o8*LFPx=u)pM=TDX)w6-(m5?n!vN!>JY~8LdTQ{YwYu=3(xAt}z zm}W;?Q_Q)`YwJAhLC#$Qht);lcDKz%320wikC&u`g!Q1Wn?!4Bv5Z9DX(^$%0-^H& z4LDP{!{)M*Xf;PZt>84~1JBokzOJ|wEClf>V8CH|d`>EHp`LdYkoKUCqJ6vi=!Yyv z#MS05htG2eeA74{C-OR-1ndLIvRa25{JuOJ%a*}f1;fANFZd`jk9*TBZui za<0m9Vm?_fw+%E-E7ij-lgjLYy6Z*7!?mN?SJk;=&kQu&OfDKr8IwdYUPa1ON?t&l zKiV;tKbAT!7_S>&4cXPHQ>nSbuF=M^lCjKj-$eVz>WOB^-@;3qN;6z2I#+aY^+nHE z%Vh4l$+Vh)etlFdP;H0`Foo)ma!i>LbHp0yxTeej=c{4j71_;H{eXNbRfmFf7SbCA z6F-MmVDn!>z$}K~o85ZAc#rd-uNpA+Glk5G2#(jVxPir-Kl|Ry`5(kQ8{fMG-iXMW z^#1^W$O}K7IsXAA>2V*mlJ2f9-zuQ;RjbdVxx*}F9gXc^;=m_8a^}NZmoCn|^}WdH zGtAB+Ui!Mt4Rh~aikyCpb$z6S2QA*tL$(PR2lL2iP>Bbv;4WxI-qFa(+lPEdV$F-Z z{oqF#FQGY#}8D2Sdh*vG_^a&5%;jTW^>rUefI_clX%b zL{07Bn+}fT9uiq6O6jqB&ES)6?((_7>Cx>X(FBgUl_QV!s%}a` zQ$6VO#31llrxNKraC_`@*sQQe0=G*WI6l(*eB|dz&q;c=k?3s!{M`XbNWy-K0DpGI zC*5oD`aG}~Cd^JZ)CMO^akmvRy(F2+2@&(Gm(vVc_j)n%_Jpk(m0e>rpmG;_U>dSnxfiG+XO- zZ*G9s05k3QYu?$`+}c4qol;8LtDkj)MZFm#WFx@IpYC!>3?swrK4_*hFC@9sOzzIQu?|lK=_?)dmDF~+qP^m!Uzd!^;yBc7WIo6W;UgUYZ5+-gPl-dG%ekfrqf}y&^IXJiYP72PxQ{y)CBZ9 zw!#9kwXJ0{C4ws&@Jc|oBk9n_oc7Bl(YB2;0?J-??cS};@PY;TZ5=SiC>$m+Q8U^$ktYy!df=5J>I$?3&}v2RTvUP?OuzY{ z1349|&9vI+1dBy!m`vzCq`p{PSb>-q8Z$ixd0mu@&48XD>k!L&6l_K;r9E&51a4oz zfKWnTdd;ZEV#bJ)FXX`Di|ASo)8#0oURZGJ>pB{?n%kRAjid#Uly~|_5=;O+2ACm$ zD!@&ct}a*u8Ev9fm4pu^7;4wl#FO$m#kJAscc8X)mB>6s6X+w37 zfNlMMeHeHh3zNfmZV=B6;rVw&SV~4%TNKn5g|%ftZP{@*8$HY z-D%gLYcesjuW1^W2IQq<-Q%5O2LcJ}LwM~pp7i_y_%@ z+TDRYreL*cvSR;a>63wy1Azli2NEnH{NODlMrjZ)4dLbYFqld(obSHaHRhV=oXo5b z7#cpV_~qXJz9;Ym5$Li_HrfN555wy1;OVFI#!>Z%A}ZiX3-1U7LeU)wCRB&<%pjh5 z{^-RcW6w@_CbJqARSw~Yzp}82sf*iRHgUlVD~M->@N7mqZ+E_z ze_8)g=KGnG>zc1L1=qC)_U;d^-9K6RD5kZ#X}S89ErE=>kJn7fw|y?)rzFfKBn{@jSa>%7O#aJ- zFjS;!@P(JB1PoO{S=E$2Yhc?gx$>3N^C^Ms4HLdeRpX?5%WaIWXy;8S(?+_2%DmeG zlmb`zSN5LY6Ug6oW%HzV*Q9cHRE$Y8KtP3xslI;ViAhz%q&brRHV1F0LUlUMQ_q9xm zrTs^rJKF#3bI%Stu8DJ_{LMV6c3Pz!c8oTR7hY4ZmBil#SU<62VpCw7DX_mgU^)~y z^f!TQe@N5&soHQc=dHpw3*Rbxvur#qlv6vMniEc46HHw*ej=1we?2{WDkJB@_H)|< zD_XCdxZ(&D?Z1}s&q`X;xlOHB9L;FUZcx0tMSbx|TmCV45^^t6}?@?SY(~ zA^4h00X<@&)dN3+rS7ilPWd+lcq_D&olo&(V>n18gl8qo6GL@X9$kgTs z@%#{8_-Wjom6b4KZ>H)3xF(7Vg!<{!!f-GiL?Yo+^554oC5ufoyNV#WPnH5%+ zK=+Q>12v97sWag22`QgphXr%X#TZtu2r5^ElqGCF8ZJ{xZB(Q{QsjiyYp$x-Olxw( znzcdA+K{GZIx{bvSrg2xnb;M~+&J76Hxl#eujv|YWtN3A*90@ygfrI%GuKaMZWwNg zCS%IXKvMoxcIj~9lw8N$aTyhd9NXAt_$|@L^FG@@&B@fq5CK&^H z)mM|MuPbv$n?T~5i|~~h7e!%XZO~YIRarZw%Ni}YswfrVbG_)e9tOzMr zPo)(Eiq{3x)(wlN)M+Cff4gxiH*dIwy+@QC(&Sy&WK89j2jDhOmT_4AsWLZ^R~1q| z7R9A1!?eaYntDxBd`ANlF08J(s;+^7t2IpP3c|XTLEXx?Tf-%d!IH+wlBSSuD~vUj zmY#=}5YS7{8ECoV;m3Oaw0pq)Mbs?jNmZ0wdG}5i50h)bTEn7oU8aIJ>eDTQErIkc zA3yVPXF$I#B;Wq${}UCW;=8~eq4I3mDs8_~cV%@Tzb&NO$yC|}l8WdHREbh;{H>|xE7@1n0mF`vvNg(= zDYP*=Ok1y~LYk-P2~ZhSSB_VO)NAi&`JN8x4zeAW7t$26c!y{>EvmpY>%NF4@_@cl z^XLB?HGmX%qm_`Rlp0otd@wTc>fSJ6nP?B_>%fXI>S};S6vWi6hx3CMX@$epu%~zr zxn0KG4a?pX(mc-2FEsCNaJS3W{M8g%nEn&P&@`+J8!Cf_%Hb{XJz4*8#mBjU{4F6} z(Dw!@Hr$xEoCZig)kmu#9{dW?I7y9pniB zxO*po?oj1@Z^Ttk@Wwp>!}^f2_F*^TR*kF11%VVayY9IW2Syk5Tkh!gYsyt$MEOiF zei?sJ?kc({mkpA9>CwXp$vUuUHp zN)i0}vCT!02xS%>%EhjUHURt^T!KPHqRoK)COy@r6#S+#8{nx`TE`~rx~MMIDaNLS zna(QghE79PV>e3kT_Ws%s_PYyxS3N|?ABrbCaX_{)U7-@3M*tiE3jL&Du5%2g`^RS zJcdeV^7viF!kH3jcZy)$*B0YP6%1I4XF6GQA$<` zZo}R?;c#Ygj6~n#NE`*|gpEFQU=E)oD2WcaNbtAAs3e44)?NNp6TCSKtIO7oGJ z&|wlKj{Uv@eR{*3`$*UyRG<&pm_sfF93a5YGQ@uEfjNMn&!LiH@A;(LW=7`&^!XEg z7DK=MBK^AHyaDu}+HO65&<(%U(CqTUWoBQGmpS@E2TpKu;&u1H$pUj+Mjv>g!w2Tn zs~AD(q=PxBLq`|nHWVC00exC6sdc&upToM5{I8G)2hf4l zRRsg0*gCR(aC<;kACfmrNpc4g!;;*fB=g@@w){pUPDMP5C%)%o9jn9gg`kf|JMVvX%qs#~== poll_ms: + voltage = get_bus_voltage() + percent = voltage_to_percent(voltage) if voltage > 0 else None + charging = is_charging() + low = percent is not None and percent <= int(config.STATUS_LED_LOW_BATTERY_PERCENT) + full = percent is not None and percent >= int(config.STATUS_LED_FULL_BATTERY_PERCENT) + if charging: + state = "full" if full else "charging" + else: + state = "low" if low else "normal" + sensor_elapsed = 0 + + if state == "low": + if green is not None: + green.value(inactive) + if red is not None: + red.value(active) + elif state == "charging": + if blink_elapsed >= blink_period: + blink_on = not blink_on + blink_elapsed = 0 + if green is not None: + green.value(active if blink_on else inactive) + if red is not None: + red.value(inactive) + else: # normal or full + if green is not None: + green.value(active) + if red is not None: + red.value(inactive) + if state != last_state: + print(f"[STATUS_LED] state={state} percent={percent} charging={charging}") + last_state = state + time.sleep_ms(tick_ms) + sensor_elapsed += tick_ms + blink_elapsed += tick_ms + except Exception as e: + self._status_led_monitor_started = False + print(f"[STATUS_LED] monitor failed: {e}") + def start_idle_timer(self): self._stop_timer = False self._last_active_time = time.time() diff --git a/laser_manager.py b/laser_manager.py index 2d5d148..21d7bd9 100644 --- a/laser_manager.py +++ b/laser_manager.py @@ -31,6 +31,7 @@ class LaserManager: # 私有状态 self._serial = None # 激光串口,由 laser_manager 自己持有 + self._laser_gpio = None # A14 激光开关,低电平开启、高电平关闭 self._calibration_active = False self._calibration_result = None self._calibration_lock = threading.Lock() @@ -69,10 +70,21 @@ class LaserManager: # ==================== 初始化方法 ==================== + def init_control_gpio(self): + """尽早初始化 A14,并拉高确保激光关闭。""" + from maix import gpio, pinmap + + pinmap.set_pin_function(config.LASER_CONTROL_PIN, config.LASER_CONTROL_GPIO) + if self._laser_gpio is None: + self._laser_gpio = gpio.GPIO(config.LASER_CONTROL_GPIO, gpio.Mode.OUT) + self._laser_gpio.value(config.LASER_CONTROL_OFF_LEVEL) + self._laser_turned_on = False + print(f"[LASER] {config.LASER_CONTROL_PIN}=HIGH,激光已关闭") + def init(self, serial_device=None, baudrate=None): """ - 初始化激光模块(包括串口) - 初始化完成后主动发送关闭命令,防止 UART 初始化噪声误触发激光 + 初始化激光模块(A14 开关 + 测距串口) + 初始化时先将 A14 拉高关闭激光,防止开机误触发 Args: serial_device: 串口设备路径,默认使用 config.DISTANCE_SERIAL_DEVICE @@ -82,23 +94,11 @@ class LaserManager: device = serial_device or config.DISTANCE_SERIAL_DEVICE baud = baudrate or config.DISTANCE_SERIAL_BAUDRATE + self.init_control_gpio() + self._serial = uart.UART(device, baud) print(f"[LASER] 激光串口初始化完成: device={device}, baudrate={baud}") - # 等待串口稳定后主动关闭激光,防止初始化噪声误触发 - time.sleep_ms(100) - try: - self._serial.read(-1) # 清空接收缓冲区 - except Exception: - pass - self._serial.write(config.LASER_OFF_CMD) - time.sleep_ms(60) - try: - self._serial.read(-1) # 清空回包 - except Exception: - pass - print("[LASER] 已发送关闭命令(防止开机误触发)") - # ==================== 业务方法 ==================== def load_laser_point(self): @@ -147,66 +147,38 @@ class LaserManager: return False def turn_on_laser(self): - """发送指令开启激光,并读取回包(部分模块支持)""" - if self._serial is None: - self.logger.error("[LASER] 激光串口未初始化,请先调用 init()") - return None - - # 打印调试信息 - self.logger.info(f"[LASER] 发送开启命令: {config.LASER_ON_CMD.hex()}") - - # 清空接收缓冲区 + """A14 输出低电平,开启激光。""" + if self._laser_gpio is None: + if self.logger: + self.logger.error("[LASER] A14 GPIO 未初始化,请先调用 init()") + return False try: - self._serial.read(-1) # 清空缓冲区 - except: - pass - - # 发送命令 - written = self._serial.write(config.LASER_ON_CMD) - self.logger.info(f"[LASER] 写入字节数: {written}") - - time.sleep_ms(60) - - # 读取回包 - resp = self._serial.read(len=20, timeout=10) - if resp: - self.logger.info(f"[LASER] 收到回包 ({len(resp)}字节): {resp.hex()}") - if resp == config.LASER_ON_CMD: - self.logger.info("✅ 激光开启指令已确认") - else: - self.logger.warning("🔇 无回包(可能正常或模块不支持回包)") - self._laser_turned_on = True - return resp + self._laser_gpio.value(config.LASER_CONTROL_ON_LEVEL) + self._laser_turned_on = True + if self.logger: + self.logger.info("[LASER] A14=LOW,激光开启") + return True + except Exception as e: + if self.logger: + self.logger.error(f"[LASER] A14 开启激光失败: {e}") + return False def turn_off_laser(self): - """发送指令关闭激光""" - if self._serial is None: - self.logger.error("[LASER] 激光串口未初始化,请先调用 init()") - return None - - # 打印调试信息 - self.logger.info(f"[LASER] 发送关闭命令: {config.LASER_OFF_CMD.hex()}") - - # 清空接收缓冲区 + """A14 输出高电平,关闭激光。""" + if self._laser_gpio is None: + if self.logger: + self.logger.error("[LASER] A14 GPIO 未初始化,请先调用 init()") + return False try: - self._serial.read(-1) - except: - pass - - # 发送命令 - written = self._serial.write(config.LASER_OFF_CMD) - self.logger.info(f"[LASER] 写入字节数: {written}") - - time.sleep_ms(60) - - # 读取回包 - resp = self._serial.read(20) - if resp: - self.logger.info(f"[LASER] 收到回包 ({len(resp)}字节): {resp.hex()}") - else: - self.logger.warning("🔇 无回包") - self._laser_turned_on = False - return resp + self._laser_gpio.value(config.LASER_CONTROL_OFF_LEVEL) + self._laser_turned_on = False + if self.logger: + self.logger.info("[LASER] A14=HIGH,激光关闭") + return True + except Exception as e: + if self.logger: + self.logger.error(f"[LASER] A14 关闭激光失败: {e}") + return False def flash_laser(self, duration_ms=1000): """闪一下激光(非阻塞版本)""" diff --git a/main.py b/main.py index a0862a3..447c4c7 100644 --- a/main.py +++ b/main.py @@ -76,12 +76,14 @@ def laser_calibration_worker(): import traceback traceback.print_exc() time.sleep_ms(1000) # 等待1秒后继续 - def cmd_str(): """主程序入口""" # ==================== 第一阶段:硬件初始化 ==================== # 按照 main104.py 的顺序,先完成所有硬件初始化 + # 开机第一步先拉高 A14 关闭激光,避免其他硬件初始化期间误亮。 + laser_manager.init_control_gpio() + # 1. 引脚功能映射 for pin, func in config.PIN_MAPPINGS.items(): try: @@ -103,6 +105,8 @@ def cmd_str(): print(f"[BOOT] init_ina226 开始 wall_s={_w_boot:.3f}") init_ina226() print(f"[BOOT] init_ina226 结束 wall +{int(round((wall_time.time() - _w_boot) * 1000))} ms") + # 启动 A25 绿灯和 A23 红灯状态指示。 + hardware_manager.start_status_led_monitor() # 4. 初始化显示和相机 _w_boot = wall_time.time() diff --git a/network.py b/network.py index ad66ac6..e8d7d16 100644 --- a/network.py +++ b/network.py @@ -2146,7 +2146,7 @@ class NetworkManager: } self.safe_enqueue(battery_data, 2) self.logger.info(f"电量上报: {battery_percent}% 充电: {is_charging()}") - if is_charging(): + if getattr(config, "CHARGING_AUTO_POWER_OFF_ENABLED", False) and is_charging(): self.safe_enqueue( { "cmd": 700, diff --git a/official_pack_fip.py b/official_pack_fip.py new file mode 100644 index 0000000..3ef254c --- /dev/null +++ b/official_pack_fip.py @@ -0,0 +1,603 @@ +#!/usr/bin/env python3 +# PYTHON_ARGCOMPLETE_OK + +import sys +import logging +import os +import re +import os.path +import collections +import uuid +import argparse +import tarfile +import io +from struct import pack, unpack + + +PYTHON_MIN_VERSION = (3, 5, 2) # Ubuntu 16.04 LTS contains Python v3.5.2 by default + + +if sys.version_info < PYTHON_MIN_VERSION: + print("Python >= %r is required" % (PYTHON_MIN_VERSION,)) + sys.exit(-1) + + +try: + import coloredlogs +except ImportError: + coloredlogs = None + +try: + import argcomplete +except ImportError: + argcomplete = None + +TOC_HEADER_NAME = 0xAA640001 +FIP_MAX_SIZE = 0xA0000 +FIP_ALIGN_SIZE = 2 * 1024 +ENTRY_SIZE = 0x28 + +IV_ZERO = b"\0" * 16 + + +class FIP_HEADER_FLAG: + BitRange = collections.namedtuple("BitRange", "shift, bits") + + REE_SCS = BitRange(0, 2) + REE_ENCRYPTION = BitRange(2, 2) + + @classmethod + def test(cls, value, flag): + v = value >> flag.shift + v &= (1 << flag.bits) - 1 + return v + + @classmethod + def value(cls, flag): + v = (1 << flag.bits) - 1 + v <<= flag.shift + return v + + +class FIP_UUID: + # from arm-trusted-firmware/include/tools_share/firmware_image_package.h + uuid_c_define = """ +/* ToC Entry UUIDs */ +#define UUID_LICENSE_FILE \ + {0x25360c62, 0x5151, 0x48ad, 0xb5, 0x91, {0x2d, 0x35, 0x67, 0x26, 0x85, 0xa5} } +#define UUID_TRUSTED_UPDATE_FIRMWARE_SCP_BL2U \ + {0x03279265, 0x742f, 0x44e6, 0x8d, 0xff, {0x57, 0x9a, 0xc1, 0xff, 0x06, 0x10} } +#define UUID_TRUSTED_UPDATE_FIRMWARE_BL2U \ + {0x37ebb360, 0xe5c1, 0x41ea, 0x9d, 0xf3, {0x19, 0xed, 0xa1, 0x1f, 0x68, 0x01} } +#define UUID_TRUSTED_UPDATE_FIRMWARE_NS_BL2U \ + {0x111d514f, 0xe52b, 0x494e, 0xb4, 0xc5, {0x83, 0xc2, 0xf7, 0x15, 0x84, 0x0a} } +#define UUID_TRUSTED_FWU_CERT \ + {0xb28a4071, 0xd618, 0x4c87, 0x8b, 0x2e, {0xc6, 0xdc, 0xcd, 0x50, 0xf0, 0x96} } +#define UUID_TRUSTED_BOOT_FIRMWARE_BL2 \ + {0x0becf95f, 0x224d, 0x4d3e, 0xa5, 0x44, {0xc3, 0x9d, 0x81, 0xc7, 0x3f, 0x0a} } +#define UUID_BLD \ + {0x3dfd6697, 0xbe89, 0x49e8, 0xae, 0x5d, {0x78, 0xa1, 0x40, 0x60, 0x82, 0x13} } +#define UUID_EL3_RUNTIME_FIRMWARE_BL31 \ + {0x6d08d447, 0xfe4c, 0x4698, 0x9b, 0x95, {0x29, 0x50, 0xcb, 0xbd, 0x5a, 0x00} } +#define UUID_SECURE_PAYLOAD_BL32 \ + {0x89e1d005, 0xdc53, 0x4713, 0x8d, 0x2b, {0x50, 0x0a, 0x4b, 0x7a, 0x3e, 0x38} } +#define UUID_NON_TRUSTED_FIRMWARE_BL33 \ + {0xa7eed0d6, 0xeafc, 0x4bd5, 0x97, 0x82, {0x99, 0x34, 0xf2, 0x34, 0xb6, 0xe4} } +/* Key certificates */ +#define UUID_ROT_KEY_CERT \ + {0x721d2d86, 0x60f8, 0x11e4, 0x92, 0x0b, {0x8b, 0xe7, 0x62, 0x16, 0x0f, 0x24} } +#define UUID_BLD1_KEY_CERT \ + {0x90e87e82, 0x60f8, 0x11e4, 0xa1, 0xb4, {0x77, 0x7a, 0x21, 0xb4, 0xf9, 0x4c} } +#define UUID_BLD2_KEY_CERT \ + {0xa1214202, 0x60f8, 0x11e4, 0x8d, 0x9b, {0xf3, 0x3c, 0x0e, 0x15, 0xa0, 0x14} } +#define UUID_SOC_FW_KEY_CERT \ + {0xccbeb88a, 0x60f9, 0x11e4, 0x9a, 0xd0, {0xeb, 0x48, 0x22, 0xd8, 0xdc, 0xf8} } +#define UUID_TRUSTED_OS_FW_KEY_CERT \ + {0x03d67794, 0x60fb, 0x11e4, 0x85, 0xdd, {0xb7, 0x10, 0x5b, 0x8c, 0xee, 0x04} } +#define UUID_BL33_KEY_CERT \ + {0x2a83d58a, 0x60fb, 0x11e4, 0x8a, 0xaf, {0xdf, 0x30, 0xbb, 0xc4, 0x98, 0x59} } +/* Content certificates */ +#define UUID_TRUSTED_BOOT_FW_CERT \ + {0xea69e2d6, 0x635d, 0x11e4, 0x8d, 0x8c, {0x9f, 0xba, 0xbe, 0x99, 0x56, 0xa5} } +#define UUID_BLD_CONTENT_CERT \ + {0x046fbe44, 0x635e, 0x11e4, 0xb2, 0x8b, {0x73, 0xd8, 0xea, 0xae, 0x96, 0x56} } +#define UUID_SOC_FW_CONTENT_CERT \ + {0x200cb2e2, 0x635e, 0x11e4, 0x9c, 0xe8, {0xab, 0xcc, 0xf9, 0x2b, 0xb6, 0x66} } +#define UUID_TRUSTED_OS_FW_CONTENT_CERT \ + {0x11449fa4, 0x635e, 0x11e4, 0x87, 0x28, {0x3f, 0x05, 0x72, 0x2a, 0xf3, 0x3d} } +#define UUID_BL33_CONTENT_CERT \ + {0xf3c1c48e, 0x635d, 0x11e4, 0xa7, 0xa9, {0x87, 0xee, 0x40, 0xb2, 0x3f, 0xa7} } +/* CV keys */ +#define UUID_CV_TRUSTED_KEY_CERT \ + {0x64fbfc49, 0x4b8c, 0x4ad3, 0xb9, 0x92, {0x93, 0x55, 0x89, 0xee, 0xf0, 0x12} } +#define UUID_CV_NON_TRUSTED_KEY_CERT \ + {0xcb48bf0d, 0x7012, 0x4201, 0xbc, 0x35, {0x8a, 0x51, 0xc4, 0x90, 0x90, 0x94} } + +/* DDR init*/ +#define UUID_CV_DDRINIT_KEY_CERT \ + {0xa61c53c9, 0x886c, 0x484f, 0x96, 0x5d, {0xd2, 0xda, 0xd7, 0xc3, 0xeb, 0x13} } +#define UUID_CV_DDRINIT_CONTENT_CERT \ + {0x9dfaabd2, 0x7f1b, 0x47e6, 0xa8, 0xa6, {0x6a, 0xc3, 0x10, 0xcc, 0xac, 0x91} } +#define UUID_CV_DDRINIT \ + {0x5888a5cd, 0x38fc, 0x4f66, 0xae, 0x3d, {0x2e, 0x18, 0x6d, 0x69, 0x41, 0xfb} } + +/* Fast boot */ +#define UUID_CV_FASTBOOT_KEY_CERT \ + {0x285df54e, 0x7b50, 0x4309, 0x9b, 0x52, {0x4b, 0xc4, 0x92, 0x82, 0x60, 0xdd} } +#define UUID_CV_FASTBOOT_CONTENT_CERT \ + {0x61f7595b, 0x8d77, 0x4e13, 0x91, 0x2a, {0x63, 0x6e, 0x58, 0xda, 0x5b, 0x69} } +#define UUID_CV_FASTBOOT \ + {0x43766198, 0xc363, 0x48db, 0xa9, 0x97, {0xf1, 0x0e, 0x93, 0x80, 0x4f, 0xea} } +""" + + @classmethod + def cls_init(cls): + txt = cls.uuid_c_define + txt = txt.replace("\r\n", "\n") + txt = txt.replace("\\\n", "\n") + rx = r""" + \#define\s+ + (?P\S+)\s+ + { + \s*(?P0x\S+)\s*,\s* + \s*(?P0x\S+)\s*,\s* + \s*(?P0x\S+)\s*,\s* + \s*(?P0x\S+)\s*,\s* + \s*(?P0x\S+)\s*,\s* + { + \s*(?P0x\S+)\s*,\s* + \s*(?P0x\S+)\s*,\s* + \s*(?P0x\S+)\s*,\s* + \s*(?P0x\S+)\s*,\s* + \s*(?P0x\S+)\s*,\s* + \s*(?P0x\S+)\s* + }\s*,?\s* + } + """ + for m in re.finditer(rx, txt, flags=re.X): + name = m.group("name") + u = m.group(*["u%d" % i for i in range(11)]) + u = [int(i, 0) for i in u] + u = pack("" % ( + self.name, + self.loc, + self.uuid.hex[:8], + self.address, + self.end, + self.size, + self.flag, + ) + + +class FIP: + ENTRY_NAMES = collections.OrderedDict( + [ + ("LICENSE_FILE", "UUID_LICENSE_FILE"), + ("BL2", "UUID_TRUSTED_BOOT_FIRMWARE_BL2"), + ("BLD", "UUID_BLD"), + ("BL31", "UUID_EL3_RUNTIME_FIRMWARE_BL31"), + ("BL32", "UUID_SECURE_PAYLOAD_BL32"), + ("BL33", "UUID_NON_TRUSTED_FIRMWARE_BL33"), + ("BLD1_KEY_CERT", "UUID_BLD1_KEY_CERT"), + ("BLD2_KEY_CERT", "UUID_BLD2_KEY_CERT"), + ("CV_TRUSTED_KEY_CERT", "UUID_CV_TRUSTED_KEY_CERT"), + ("SOC_FW_KEY_CERT", "UUID_SOC_FW_KEY_CERT"), + ("TRUSTED_OS_FW_KEY_CERT", "UUID_TRUSTED_OS_FW_KEY_CERT"), + ("CV_NON_TRUSTED_KEY_CERT", "UUID_CV_NON_TRUSTED_KEY_CERT"), + ("BL33_KEY_CERT", "UUID_BL33_KEY_CERT"), + ("TRUSTED_BOOT_FW_CERT", "UUID_TRUSTED_BOOT_FW_CERT"), + ("BLD_CONTENT_CERT", "UUID_BLD_CONTENT_CERT"), + ("SOC_FW_CONTENT_CERT", "UUID_SOC_FW_CONTENT_CERT"), + ("TRUSTED_OS_FW_CONTENT_CERT", "UUID_TRUSTED_OS_FW_CONTENT_CERT"), + ("BL33_CONTENT_CERT", "UUID_BL33_CONTENT_CERT"), + ("CV_DDRINIT", "UUID_CV_DDRINIT"), + ("CV_FASTBOOT", "UUID_CV_FASTBOOT"), + ] + ) + + TOC_Header = collections.namedtuple( + "TOC_Header", "name, serial, flag_res, flag_plat, flag_res2" + ) + + def __init__(self, path): + logging.info("FIP_BIN: %s", path) + self.path = path + + def load(self): + with open(self.path, "rb") as fp: + self.binary = fp.read(FIP_MAX_SIZE) + logging.info("%s is %d bytes", self.path, len(self.binary)) + + self.header = self.TOC_Header(*unpack(" 0: + pk, pv = ents[n - 1] + if v.loc != pv.loc + ENTRY_SIZE or v.address != pv.address + pv.size: + raise Exception("Invalid FIP") + + rest = self.binary[ents[-1][1].end :] + loc = rest.find(b"APLB") + if loc < 0: + raise Exception("No BLD/DDRC") + self.blp_ddrc_binary = rest[loc:] + logging.debug("blp_ddrc: 0x%04x at 0x%08x", len(self.blp_ddrc_binary), loc) + + self.ents = collections.OrderedDict(ents) + + def make_fip(self, output_path=None): + logging.info("New TOC header: flag_plat=0x%04x", self.header.flag_plat) + header_bin = pack(" 0: + fip_bin += b"\x00" * (FIP_ALIGN_SIZE - len(fip_bin) % FIP_ALIGN_SIZE) + fip_bin += self.blp_ddrc_binary + + if output_path: + path = output_path + else: + path = os.path.splitext(self.path) + path = path[0] + "_signed_encrypted" + path[1] + logging.info("Save new FIP image to %s", path) + with open(path, "wb") as fp: + fp.write(fip_bin) + + def dump_uuids(self): + for k, v in vars(FIP_UUID).items(): + if k.startswith("UUID_"): + print("%-38s" % k, v.hex) + + def find_entry(self, name): + # UUID=0, offset=any, size=0, flags=0 + nullm = re.search(rb"\0{16}.{8}\0{16}", self.binary, flags=re.DOTALL) + if nullm is None: + raise Exception("NULL TOC entry is not found") + + max_toc_size = nullm.start(0) + uuid = getattr(FIP_UUID, name) + loc = self.binary.find(uuid.bytes, 0, max_toc_size) + if loc < 0: + raise ValueError("%s is not found" % name) + return Entry.from_fip(name, loc, self.binary) + + +def entry(args): + logging.debug("cmd_fip") + + +def init_logging(log_file=None, file_level="DEBUG", stdout_level="WARNING"): + + root_logger = logging.getLogger() + root_logger.setLevel(logging.NOTSET) + + fmt = "%(asctime)s %(levelname)8s:%(name)s:%(message)s" + + if log_file is not None: + file_handler = logging.FileHandler(log_file, encoding="utf-8") + file_handler.setFormatter(logging.Formatter(fmt)) + file_handler.setLevel(file_level) + root_logger.addHandler(file_handler) + + if coloredlogs: + os.environ["COLOREDLOGS_DATE_FORMAT"] = "%H:%M:%S" + + field_styles = { + "asctime": {"color": "green"}, + "hostname": {"color": "magenta"}, + "levelname": {"color": "black", "bold": True}, + "name": {"color": "blue"}, + "programname": {"color": "cyan"}, + } + + level_styles = coloredlogs.DEFAULT_LEVEL_STYLES + level_styles["debug"]["color"] = "cyan" + + coloredlogs.install( + level=stdout_level, + fmt=fmt, + field_styles=field_styles, + level_styles=level_styles, + milliseconds=True, + ) + + +def parse_fip(fip_path): + logging.debug("parse_fip: %s", fip_path) + fip = FIP(fip_path) + fip.load() + + +def unpack_fip(fip_path): + logging.debug("unpack_fip: %s", fip_path) + fip = FIP(fip_path) + fip.load() + + def save(name, content): + fn = os.path.splitext(fip_path) + fn = "%s_%s%s" % (fn[0], name, fn[1]) + logging.info("Save %s", fn) + with open(fn, "wb") as fp: + fp.write(content) + + for k, v in fip.ents.items(): + save(k, v.content) + + save("BLP_DDRC", fip.blp_ddrc_binary) + + +def tar_bld(fip_path, output_path, multibin): + logging.debug("tar_bld: %s multibin=%r", fip_path, multibin) + fip = FIP(fip_path) + fip.load() + + members = [ + "BLD_CONTENT_CERT", + "BLD2_KEY_CERT", + "BLD1_KEY_CERT", + "CV_DDRINIT" if multibin else "BLD", + ] + + if not output_path: + output_path = os.path.join(os.path.dirname(fip_path), "bld.tar") + + logging.info("bld_tar_path=%s", output_path) + + with tarfile.open(output_path, "w") as tf: + for m in members: + logging.debug("Tar %s", m) + try: + fp = io.BytesIO(fip.ents[m].content) + except KeyError: + logging.warning("%s doesn't exist", m) + continue + info = tarfile.TarInfo(name=m + ".bin") + info.size = len(fp.getbuffer()) + tf.addfile(tarinfo=info, fileobj=fp) + + +def merge_fip(fip_path, inputs, output_path): + logging.debug("merge_fip: %s", fip_path) + fip = FIP(fip_path) + fip.load() + + for name in FIP.ENTRY_NAMES: + binary = inputs.get(name) + if not binary: + continue + + logging.debug("merge %s", name) + ent = fip.ents.get(name) + if ent: + ent.content = binary + else: + ent = Entry.make(getattr(FIP_UUID, "UUID_" + name), binary) + fip.ents[name] = ent + + binary = inputs.get("BLP_DDRC") + if binary: + fip.blp_ddrc_binary = binary + + if not output_path: + fn = os.path.splitext(fip_path) + fn = "%s_%s%s" % (fn[0], "merged", fn[1]) + output_path = fn + + fip.make_fip(output_path) + + +def round_up(n, k): + return (n + k - 1) // k * k + + +def read_blp_and_ddrc(inputs, blp_path, ddrc_path): + logging.info("Open %s and %s", blp_path, ddrc_path) + with open(blp_path, "rb") as fp: + blp_bin = fp.read() + + logging.info("Open %s", ddrc_path) + with open(ddrc_path, "rb") as fp: + ddrc_bin = fp.read() + + blp_bin += b"\0" * (round_up(len(blp_bin), FIP_ALIGN_SIZE) - len(blp_bin)) + ddrc_bin += b"\0" * (round_up(len(ddrc_bin), FIP_ALIGN_SIZE) - len(ddrc_bin)) + + inputs["BLP_DDRC"] = blp_bin + ddrc_bin + + +def read_bld_tar(inputs, bld_tar_path, multibin): + logging.info("Open %s multibin=%r", bld_tar_path, multibin) + members = [ + "BLD_CONTENT_CERT.bin", + "BLD2_KEY_CERT.bin", + "BLD1_KEY_CERT.bin", + "CV_DDRINIT.bin" if multibin else "BLD.bin", + ] + + with tarfile.open(bld_tar_path, "r") as tf: + for member in members: + try: + fp = tf.extractfile(member) + inputs[os.path.splitext(member)[0]] = fp.read() + except KeyError: + logging.warning("%s does not exist", member) + + +def main(): + parser = argparse.ArgumentParser(description="FIP packer") + + for name in FIP.ENTRY_NAMES: + parser.add_argument( + "--add-%s" % name.lower(), + dest=name, + type=str, + help="Merge %s into FIP" % name, + ) + parser.add_argument( + "--add-blp-ddrc", dest="BLP_DDRC", type=str, help="Merge BLP+DDRC into FIP" + ) + parser.add_argument("--add-blp", dest="BLP", type=str, help="Merge BLP into FIP") + parser.add_argument("--add-ddrc", dest="DDRC", type=str, help="Merge DDRC into FIP") + parser.add_argument( + "--add-bld-tar", dest="BLD_TAR", type=str, help="Merge BLD.tar into FIP" + ) + + parser.add_argument("--multibin", action="store_true", help="Use multibin") + + parser.add_argument("FIP_BIN", type=str, nargs=1, help="Input FIP binary") + parser.add_argument("--output", type=str, help="Output filename") + + parser.add_argument( + "--version", action="store_true", help="Output version information and exit" + ) + parser.add_argument( + "--verbose", + help="Increase output verbosity", + action="store_const", + const=logging.DEBUG, + default=logging.DEBUG, + ) + + parser.add_argument("--unpack", action="store_true", help="Unpack FIP.bin") + parser.add_argument("--parse", action="store_true", help="Parse FIP.bin") + parser.add_argument( + "--tar-bld", action="store_true", help="Extrace BLD.bin and tar" + ) + + if argcomplete: + argcomplete.autocomplete(parser) + + args = parser.parse_args() + + init_logging(stdout_level=args.verbose) + + logging.debug("args=%r", args) + + FIP_UUID.cls_init() + + if args.parse: + parse_fip(args.FIP_BIN[0]) + + if args.unpack: + unpack_fip(args.FIP_BIN[0]) + + if args.tar_bld: + tar_bld(args.FIP_BIN[0], args.output, args.multibin) + + inputs = collections.OrderedDict() + for name in list(FIP.ENTRY_NAMES) + ["BLP_DDRC"]: + fn = getattr(args, name) + if not fn: + continue + logging.info("Open %s", fn) + with open(fn, "rb") as fp: + inputs[name] = fp.read() + + if args.BLP or args.DDRC: + read_blp_and_ddrc(inputs, args.BLP, args.DDRC) + + if args.BLD_TAR: + read_bld_tar(inputs, args.BLD_TAR, args.multibin) + + if len(inputs): + merge_fip(args.FIP_BIN[0], inputs, args.output) + + +if __name__ == "__main__": + main() diff --git a/power.py b/power.py index 315f3e4..fb0ec26 100644 --- a/power.py +++ b/power.py @@ -7,10 +7,12 @@ import config import os import subprocess +import _thread from logger_manager import logger_manager from maix import time as maix_time _INA226_PRESENT = None +_INA226_LOCK = _thread.allocate_lock() def _ina226_ready() -> bool: @@ -32,7 +34,11 @@ def write_register(reg, value): data = [(value >> 8) & 0xFF, value & 0xFF] # 某些底层驱动在失败时只打印 “write failed” 并返回 -1,而不是抛异常; # 为避免误判“初始化成功”导致后续 readfrom_mem SIGSEGV,这里把失败显式转成异常。 - ret = hardware_manager.bus.writeto_mem(config.INA226_ADDR, reg, bytes(data)) + _INA226_LOCK.acquire() + try: + ret = hardware_manager.bus.writeto_mem(config.INA226_ADDR, reg, bytes(data)) + finally: + _INA226_LOCK.release() if isinstance(ret, int) and ret < 0: if logger: logger.error(f"[INA226] writeto_mem 失败: addr=0x{config.INA226_ADDR:02X} reg=0x{reg:02X} ret={ret}") @@ -42,7 +48,11 @@ def write_register(reg, value): def read_register(reg): """读取INA226寄存器""" from hardware import hardware_manager - data = hardware_manager.bus.readfrom_mem(config.INA226_ADDR, reg, 2) + _INA226_LOCK.acquire() + try: + data = hardware_manager.bus.readfrom_mem(config.INA226_ADDR, reg, 2) + finally: + _INA226_LOCK.release() return (data[0] << 8) | data[1] diff --git a/test/__pycache__/wifi_gpio_ina_isolation.cpython-312.pyc b/test/__pycache__/wifi_gpio_ina_isolation.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ce9bf7745a35e11f756c38a5fbf45d5c5737f349 GIT binary patch literal 5878 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 1: + command = sys.argv[1].strip() + if not set_level(output, command): + print("Invalid argument. Use 1 or 0.") + return + return + + output.value(1) + print("A14 laser test: input 0 for ON, 1 for OFF, q to quit.") + + try: + while True: + try: + command = input("A14> ").strip().lower() + except EOFError: + print("This runner has no stdin. Run from an SSH/serial shell with argument 1 or 0.") + return + if set_level(output, command): + continue + elif command in ("q", "quit", "exit"): + break + elif command: + print("Invalid input. Use 1, 0, or q.") + except KeyboardInterrupt: + print() + finally: + output.value(1) + print("A14 = HIGH, laser OFF, test stopped.") + + +if __name__ == "__main__": + main() diff --git a/test/wifi_gpio_ina_isolation.py b/test/wifi_gpio_ina_isolation.py new file mode 100644 index 0000000..62d3adc --- /dev/null +++ b/test/wifi_gpio_ina_isolation.py @@ -0,0 +1,122 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +"""Standalone WiFi/GPIO/INA226 isolation test for the official MaixPy tool. + +This file intentionally does not import project modules or start project +threads. Select TEST_MODE below, then run the file directly. +""" + +import time + +from maix import gpio, i2c, network, pinmap + + +# Change only this value before each run. +# wifi WiFi only +# a25 A25 only +# a23 A23 only +# gpio A23/A26 only +# ina INA226 only +# gpio_ina GPIOs, then INA226 +# a25_wifi A25, then WiFi +# a23_wifi A23, then WiFi +# all GPIOs, INA226, then WiFi +TEST_MODE = "a25_wifi" + +WIFI_SSID = "sheling4b02-5G" +WIFI_PASSWORD = "Aa12345678" +WIFI_TIMEOUT_S = 20 +I2C_BUS_NUM = 5 +INA226_ADDR = 0x40 + + +def init_leds(): + return init_selected_leds(True, True) + + +def init_selected_leds(use_a26, use_a23): + outputs = [] + if use_a26: + print("Initializing A25 -> GPIOA25") + pinmap.set_pin_function("A25", "GPIOA25") + green = gpio.GPIO("GPIOA25", gpio.Mode.OUT) + green.value(0) + outputs.append(("GPIOA25", green)) + print("GPIOA25 initialized LOW") + if use_a23: + print("Initializing A23 -> GPIOA23") + pinmap.set_pin_function("A23", "GPIOA23") + red = gpio.GPIO("GPIOA23", gpio.Mode.OUT) + red.value(0) + outputs.append(("GPIOA23", red)) + print("GPIOA23 initialized LOW") + return outputs + + +def test_ina226(): + # Match the board mapping used by the application before opening I2C5. + pinmap.set_pin_function("A15", "I2C5_SCL") + pinmap.set_pin_function("A27", "I2C5_SDA") + print("A15/A27 configured for I2C5") + print("Initializing I2C bus", I2C_BUS_NUM) + bus = i2c.I2C(I2C_BUS_NUM, i2c.Mode.MASTER) + print("Reading INA226 at 0x%02X" % INA226_ADDR) + config = bus.readfrom_mem(INA226_ADDR, 0x00, 2) + voltage_raw = bus.readfrom_mem(INA226_ADDR, 0x02, 2) + voltage = ((voltage_raw[0] << 8) | voltage_raw[1]) * 1.25 / 1000 + print("INA226 config=0x%02X%02X voltage=%.3fV" % (config[0], config[1], voltage)) + return bus + + +def test_wifi(): + print("Starting MaixPy WiFi connection...") + wifi = network.wifi.Wifi() + result = wifi.connect(WIFI_SSID, WIFI_PASSWORD, wait=True, timeout=WIFI_TIMEOUT_S) + print("WiFi connect result:", result) + print("WiFi connected:", wifi.is_connected()) + try: + print("WiFi IP:", wifi.get_ip()) + except Exception as exc: + print("WiFi status query failed:", exc) + + +def main(): + valid = ("wifi", "a25", "a23", "gpio", "ina", "gpio_ina", "a25_wifi", "a23_wifi", "all") + mode = TEST_MODE.lower() + if mode not in valid: + print("TEST_MODE must be one of:", ", ".join(valid)) + return 1 + + leds = [] + try: + print("=== Standalone WiFi/GPIO/INA226 isolation ===") + print("mode:", mode) + if mode in ("a25", "a25_wifi"): + leds = init_selected_leds(True, False) + time.sleep(1) + elif mode in ("a23", "a23_wifi"): + leds = init_selected_leds(False, True) + time.sleep(1) + elif mode in ("gpio", "gpio_ina", "all"): + leds = init_leds() + time.sleep(1) + if mode in ("ina", "gpio_ina", "all"): + test_ina226() + time.sleep(1) + if mode in ("wifi", "a25_wifi", "a23_wifi", "all"): + test_wifi() + print("TEST COMPLETE") + return 0 + except Exception as exc: + print("TEST FAILED:", repr(exc)) + return 1 + finally: + for name, led in leds: + try: + led.value(0) + print(name, "LOW") + except Exception as exc: + print(name, "cleanup failed:", exc) + + +main() diff --git a/test/wifi_gpio_isolation.py b/test/wifi_gpio_isolation.py new file mode 100644 index 0000000..bd71497 --- /dev/null +++ b/test/wifi_gpio_isolation.py @@ -0,0 +1,88 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +"""Standalone WiFi/GPIO isolation test. + +This script intentionally does not import any project module. It only tests +MaixPy WiFi startup with optional A23/A26 GPIO initialization. +""" + +import time + +from maix import gpio, network, pinmap + + +GREEN_PIN = "A26" +GREEN_GPIO = "GPIOA26" +RED_PIN = "A23" +RED_GPIO = "GPIOA23" + +# Run this file directly from the official MaixPy tool. +# Change only TEST_MODE between runs: none -> a26 -> a23 -> both. +TEST_MODE = "none" +WIFI_SSID = "sheling4b02-5G" +WIFI_PASSWORD = "Aa12345678" +WIFI_TIMEOUT_S = 20 + + +def init_gpio(mode): + outputs = [] + if mode in ("a26", "both"): + pinmap.set_pin_function(GREEN_PIN, GREEN_GPIO) + green = gpio.GPIO(GREEN_GPIO, gpio.Mode.OUT) + green.value(1) + outputs.append((GREEN_GPIO, green)) + print("GPIOA26 initialized HIGH") + if mode in ("a23", "both"): + pinmap.set_pin_function(RED_PIN, RED_GPIO) + red = gpio.GPIO(RED_GPIO, gpio.Mode.OUT) + red.value(1) + outputs.append((RED_GPIO, red)) + print("GPIOA23 initialized HIGH") + return outputs + + +def connect_wifi(ssid, password, timeout_s): + print("Starting MaixPy WiFi connection...") + wifi = network.wifi.Wifi() + result = wifi.connect(ssid, password, wait=True, timeout=timeout_s) + print("WiFi connect result:", result) + try: + print("WiFi connected:", wifi.is_connected()) + print("WiFi IP:", wifi.get_ip()) + except Exception as exc: + print("WiFi status query failed:", exc) + return result + + +def main(): + mode = TEST_MODE.lower() + if mode not in ("none", "a26", "a23", "both"): + print("TEST_MODE must be none, a26, a23, or both") + return 1 + ssid = WIFI_SSID + password = WIFI_PASSWORD + timeout_s = WIFI_TIMEOUT_S + + print("=== Standalone WiFi/GPIO isolation ===") + print("mode:", mode) + print("ssid:", ssid) + outputs = [] + try: + outputs = init_gpio(mode) + time.sleep(1) + connect_wifi(ssid, password, timeout_s) + return 0 + except Exception as exc: + print("TEST FAILED:", repr(exc)) + return 1 + finally: + for gpio_name, output in outputs: + try: + output.value(0) + print(gpio_name, "LOW") + except Exception as exc: + print(gpio_name, "cleanup failed:", exc) + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/version.py b/version.py index 82be004..4bf8f49 100644 --- a/version.py +++ b/version.py @@ -4,6 +4,6 @@ 应用版本号 每次 OTA 更新时,只需要更新这个文件中的版本号 """ -VERSION = '2.17.0' +VERSION = '2.18.0'