自動(dòng)化工具鏈的設(shè)備發(fā)現(xiàn)與文件推送實(shí)戰(zhàn))
最近幾個(gè)月我一直在搞一個(gè)小工具鏈名字就叫AnyPS5起因特別簡單手上一臺(tái)吃灰的PS5除了玩游戲之外它其實(shí)還是一臺(tái)性能不錯(cuò)的家庭計(jì)算終端。但問題在于每次我想往里面?zhèn)鼽c(diǎn)東西或者做點(diǎn)自動(dòng)化管理都得開好幾個(gè)軟件、切好幾套界面麻煩得不行。身邊幾個(gè)玩開發(fā)機(jī)的朋友也有同樣的感覺于是我就想著把常用操作全部統(tǒng)一到一套輕量工具里這才有了 AnyPS5 這個(gè)項(xiàng)目。這篇博客就把我的設(shè)計(jì)思路、關(guān)鍵實(shí)現(xiàn)、踩過的坑一次講清楚給還在折騰同類東西的朋友一個(gè)參考。1. 項(xiàng)目整體設(shè)計(jì)與拆解思路1.1 先弄明白到底要解決什么問題一臺(tái) PS5 擺在客廳里平時(shí)無非干三件事玩游戲、看流媒體、偶爾跑點(diǎn)開發(fā)驗(yàn)證??蛇@三件事背后對應(yīng)的管理方式完全割裂玩游戲時(shí)截圖和錄屏存在主機(jī)內(nèi)置存儲(chǔ)里隔段時(shí)間就得手工導(dǎo)出不然空間吃緊??戳髅襟w時(shí)電視盒子、游戲主機(jī)、NAS 各自獨(dú)立沒有一個(gè)統(tǒng)一入口去調(diào)度。做開發(fā)驗(yàn)證時(shí)構(gòu)建產(chǎn)物要從電腦傳到主機(jī)上一次兩次還好頻率一高就特別煩人。AnyPS5 這個(gè)名字里的Any我一開始想表達(dá)的是“任何場景都能接”后來發(fā)現(xiàn)更準(zhǔn)確的理解應(yīng)該是“任何一臺(tái) PS5 都能用”。它不去碰系統(tǒng)底層也不做任何灰產(chǎn)性質(zhì)的操作純粹圍繞主機(jī)對外提供的正常接口和標(biāo)準(zhǔn)網(wǎng)絡(luò)協(xié)議做一層薄封裝把重復(fù)勞動(dòng)自動(dòng)化掉。1.2 設(shè)計(jì)原則薄封裝優(yōu)先別把事情搞復(fù)雜我在動(dòng)手之前定了幾條鐵律后面所有開發(fā)都圍著它們轉(zhuǎn)不修改主機(jī)系統(tǒng)文件不做任何系統(tǒng)級改動(dòng)所有功能都跑在主機(jī)外部。通信全部走局域網(wǎng)標(biāo)準(zhǔn)協(xié)議依賴盡可能少。每個(gè)模塊獨(dú)立可以單獨(dú)用也可以組合起來跑。核心是腳本化的方便我加計(jì)劃任務(wù)也方便其他人按需調(diào)整。這套原則幫我避開了很多坑。市面上不少類似工具喜歡做成“全家桶”把設(shè)備認(rèn)證、媒體管理、遠(yuǎn)程喚醒全綁在一起結(jié)果一處崩了處處崩。我堅(jiān)持模塊獨(dú)立實(shí)測下來維護(hù)成本低得多出問題了也能快速定位。1.3 技術(shù)選型為什么用局域網(wǎng) 腳本 輕量 Web技術(shù)棧我選得很保守Python 腳本負(fù)責(zé)后臺(tái)任務(wù)Web 面板負(fù)責(zé)統(tǒng)一展示和操作入口局域網(wǎng) TCP/UDP 負(fù)責(zé)設(shè)備間通信。選 Python 不是因?yàn)樗阅芎枚巧鷳B(tài)成熟遇到網(wǎng)絡(luò)、文件、編碼問題都能快速找到現(xiàn)成庫。Web 面板用輕量框架不搞前后端分離那套復(fù)雜工程一個(gè)進(jìn)程起來就能用。這里有個(gè)很重要的認(rèn)知PS5 對外暴露的服務(wù)和協(xié)議是有限的很多功能必須“曲線救國”。比如設(shè)備發(fā)現(xiàn)我就用 UDP 廣播在局域網(wǎng)里找到主機(jī)地址比如文件傳輸就走標(biāo)準(zhǔn) HTTP 上傳接口。搞清楚每項(xiàng)能力的邊界才知道哪些能做、哪些需要繞過。2. 核心模塊詳解與關(guān)鍵技術(shù)點(diǎn)2.1 設(shè)備發(fā)現(xiàn)一臺(tái)主機(jī)在局域網(wǎng)里怎么被找到要把 PS5 接入工具鏈第一步永遠(yuǎn)是設(shè)備發(fā)現(xiàn)。做法是綁定 UDP 端口向本網(wǎng)段廣播特定格式的探測包。主機(jī)收到后響應(yīng)一個(gè)包含設(shè)備型號、當(dāng)前固件版本、IP 地址的報(bào)文。腳本維護(hù)一張?jiān)O(shè)備列表支持手動(dòng)指定 IP 作為兜底方案。這里的關(guān)鍵細(xì)節(jié)是超時(shí)重試機(jī)制。最開始我只發(fā)一次探測包結(jié)果總有設(shè)備因?yàn)?Wi-Fi 延遲沒回應(yīng)后來改成間隔 2 秒重發(fā) 3 次成功率顯著提升。另外多網(wǎng)卡環(huán)境要小心得允許用戶指定求廣播的網(wǎng)絡(luò)接口不然工具可能找到錯(cuò)誤網(wǎng)段的設(shè)備。2.2 構(gòu)建推送把電腦上的產(chǎn)物傳進(jìn)主機(jī)這是整個(gè)工具鏈里用得最頻繁的功能。流程分三步準(zhǔn)備檢查本地文件是否存在校驗(yàn)大小和類型。傳輸通過 HTTP 接口把文件推送到主機(jī)指定目錄。確認(rèn)輪詢主機(jī)的返回狀態(tài)確認(rèn)文件已落盤。在做傳輸模塊時(shí)我特意加了一個(gè)狀態(tài)機(jī)把“待推送”“傳輸中”“已確認(rèn)”“失敗待重試”四種狀態(tài)管理起來。這個(gè)設(shè)計(jì)幫我解決了一個(gè)特別詭異的 bug有些文件明明傳完了主機(jī)卻報(bào)損壞后來發(fā)現(xiàn)是網(wǎng)絡(luò)中斷導(dǎo)致文件不完整可客戶端沒感知到。加了狀態(tài)機(jī)之后只有收到“已確認(rèn)”狀態(tài)才認(rèn)為成功不然就自動(dòng)走重試。2.3 媒體資料整理截圖、錄屏、管理一步到位PS5 的截圖和錄屏導(dǎo)出是個(gè)體力活。我在 AnyPS5 里做了一個(gè)媒體整理模塊核心邏輯是定期掃描主機(jī)的媒體目錄把新增文件同步到本地或 NAS。按“游戲名/日期/類型”的結(jié)構(gòu)自動(dòng)歸檔重名文件加后綴。提供文件清單的簡單檢索頁支持按關(guān)鍵字過濾。這個(gè)模塊讓我最意外的是文件命名規(guī)則。主機(jī)默認(rèn)文件名的可讀性很差全是類似20250115_213000_0001.jpg這樣的時(shí)間戳加序號。我寫了一個(gè)解析函數(shù)嘗試從文件元數(shù)據(jù)和目錄結(jié)構(gòu)反推游戲名稱匹配不上就留空再配合用戶自定義規(guī)則補(bǔ)全?,F(xiàn)在我的截圖庫按游戲名分類得整整齊齊找素材高效多了。2.4 Web 控制面板把三個(gè)模塊串起來模塊之間原本是割裂的我做了AnyPS5 Dashboard把它們收到一個(gè)入口里。面板功能就四個(gè)設(shè)備狀態(tài)卡片實(shí)時(shí)顯示主機(jī)在線狀態(tài)、存儲(chǔ)余量。文件推送表單選擇本地文件選目標(biāo)目錄點(diǎn)按鈕推送。媒體庫按時(shí)間、類型分組支持關(guān)鍵詞搜索。運(yùn)行日志窗口所有操作的成敗記錄都在這里看。面板做得特別克制沒有用戶系統(tǒng)沒有權(quán)限分級因?yàn)槲业氖褂脠鼍笆羌彝ゾ钟蚓W(wǎng)信任模型就是“連了這個(gè) Wi-Fi 的人都是自己人”。如果你想部署到更大范圍建議在面板外面套一層反向代理做基本認(rèn)證。3. 實(shí)操過程與關(guān)鍵實(shí)現(xiàn)3.1 準(zhǔn)備環(huán)境開發(fā)環(huán)境比較簡單先列個(gè)清單一臺(tái)裝了 Python 3.9 的電腦我用的 LinuxWindows 也完全可跑。一臺(tái)在同一局域網(wǎng)內(nèi)的 PS5 主機(jī)。一個(gè)存放文件和腳本的工作目錄。依賴庫我用的是requests處理 HTTPflask做 Web 面板netifaces枚舉網(wǎng)卡全程不碰系統(tǒng)底層。注意如果你只跑設(shè)備發(fā)現(xiàn)和文件推送模塊flask其實(shí)可以不裝。我建議按模塊拆分requirements.txt免得拉一堆用不上的依賴。3.2 核心代碼骨架與關(guān)鍵參數(shù)下面這段是我設(shè)備發(fā)現(xiàn)模塊的骨架注釋里寫清楚了每個(gè)參數(shù)的實(shí)際含義import socket import time import json DISCOVERY_PORT 3939 # 與主機(jī)約定好的 UDP 端口 DISCOVERY_MAGIC bANYPS5_DISCOVERY_V1 MAX_RETRIES 3 RETRY_INTERVAL 2 # 秒 def discover_hosts(bind_ipNone, timeout3): sock socket.socket(socket.AF_INET, socket.SOCK_DGRAM) sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) if bind_ip: sock.bind((bind_ip, 0)) sock.settimeout(timeout) discovered [] targets [255.255.255.255] # 更可靠的做法是枚舉所有可用網(wǎng)卡向各網(wǎng)段的廣播地址發(fā)包 # 這里省略網(wǎng)卡枚舉邏輯僅做示例 for attempt in range(MAX_RETRIES): for target in targets: sock.sendto(DISCOVERY_MAGIC, (target, DISCOVERY_PORT)) try: while True: data, addr sock.recvfrom(4096) payload json.loads(data.decode(utf-8)) if payload.get(type) ANYPS5_RESPONSE: discovered.append({ ip: addr[0], model: payload.get(model, unknown), fw: payload.get(firmware, unknown), }) except socket.timeout: pass if discovered: break time.sleep(RETRY_INTERVAL) # 去重同一個(gè)設(shè)備可能收到多個(gè)響應(yīng) unique {item[ip]: item for item in discovered}.values() return list(unique)主動(dòng)能函數(shù)寫完后建議立刻做一次“軟測試”先拿虛擬數(shù)據(jù)在本地跑通邏輯再拿真實(shí)主機(jī)驗(yàn)證。這樣能區(qū)分問題是出在協(xié)議層還是環(huán)境層排查起來會(huì)快很多。3.3 文件推送為什么要分塊文件傳輸模塊我一開始是純r(jià)equests.post一把梭。小文件沒問題大文件動(dòng)不動(dòng)就超時(shí)。后來改成分塊上傳CHUNK_SIZE 2 * 1024 * 1024 # 2MB 一塊 def upload_file(filepath, endpoint): filesize os.path.getsize(filepath) offset 0 filename os.path.basename(filepath) while offset filesize: with open(filepath, rb) as f: f.seek(offset) chunk f.read(CHUNK_SIZE) resp requests.post( endpoint, data{ offset: str(offset), filename: filename, total: str(filesize), }, files{chunk: (filename, chunk)}, timeout30, ) if resp.status_code ! 200: # 記錄失敗偏移量下次從斷點(diǎn)繼續(xù) break offset len(chunk) print(fprogress: {offset}/{filesize})分塊大小 2MB 是我試出來的平衡值太小的話 HTTP 請求次數(shù)太多太大又容易在弱網(wǎng)環(huán)境下超時(shí)。如果你家里的網(wǎng)絡(luò)質(zhì)量不錯(cuò)可以適當(dāng)調(diào)到 4MB。3.4 媒體庫整理腳本的核心邏輯媒體歸檔最需要處理的就是文件名和目錄結(jié)構(gòu)。我的處理邏輯是這樣MEDIA_EXTENSIONS (.jpg, .png, .webm, .mp4, .gif) def archive_file(src_path, base_dir): filename os.path.basename(src_path) # 嘗試從文件名里提取日期YYYYMMDD_HHMMSS ts_match re.search(r(\d{8})_(\d{6}), filename) if ts_match: date_part ts_match.group(1) folder f{date_part[:4]}/{date_part[4:6]}/{date_part[6:]} else: folder unsorted target_dir os.path.join(base_dir, folder) os.makedirs(target_dir, exist_okTrue) dest os.path.join(target_dir, filename) counter 1 while os.path.exists(dest): name, ext os.path.splitext(filename) dest os.path.join(target_dir, f{name}_{counter}{ext}) counter 1 shutil.move(src_path, dest) return dest這個(gè)腳本看起來簡單但有幾個(gè)隱藏點(diǎn)。一是日期提取的正則需要多做兼容有些文件名格式并不完全統(tǒng)一二是重名處理千萬別直接覆蓋文件游戲截圖里重名的概率比你想象中高得多。3.5 Web 面板部分實(shí)現(xiàn)Web 面板用 Flask 寫的話代碼非常短。我用了一個(gè)后臺(tái)線程去輪詢設(shè)備狀態(tài)同時(shí)提供 API 給前端調(diào)用from flask import Flask, jsonify, request app Flask(__name__) DEVICE_STATE {online: False, storage_free: 0} app.route(/api/status) def api_status(): return jsonify(DEVICE_STATE) app.route(/api/upload, methods[POST]) def api_upload(): file request.files[file] target request.form.get(target_dir, /default) saved_path os.path.join(target, file.filename) file.save(saved_path) return jsonify({ok: True, path: saved_path}) if __name__ __main__: app.run(host0.0.0.0, port8080, debugFalse)前端的儀表盤我就用原生 HTML 一個(gè)定時(shí)刷新腳本沒有任何框架。因?yàn)橛脩艟褪俏乙粋€(gè)不需要復(fù)雜的響應(yīng)式布局也不想引入 Node.js 那一整套工具鏈。4. 常見問題與排查心得4.1 設(shè)備發(fā)現(xiàn)失敗怎么辦這是被問得最多的問題。我發(fā)現(xiàn)九成情況都是網(wǎng)段不匹配主機(jī)連的是 5GHz Wi-Fi電腦連的是有限網(wǎng)口兩個(gè)網(wǎng)段隔離了UDP 廣播根本到不了。排查步驟很簡單確認(rèn)主機(jī)和電腦是否在同一個(gè)局域網(wǎng)內(nèi)對比 IP 前三段。臨時(shí)把防火墻關(guān)掉再試一次確認(rèn)是否是防火墻攔截。改用“手動(dòng)指定 IP”模式繞過發(fā)現(xiàn)流程。4.2 大文件推送中斷怎么斷點(diǎn)續(xù)傳如果你發(fā)現(xiàn)傳輸中途斷掉從頭再來很浪費(fèi)時(shí)間建議把文件按塊標(biāo)識(shí)做一個(gè)臨時(shí)記錄。每次成功傳輸一塊就把塊的編號寫進(jìn)本地重新運(yùn)行時(shí)跳過這些塊。我最初沒做這層容錯(cuò)傳一個(gè) 4GB 的素材傳了三次才成功后來加上這個(gè)邏輯耗時(shí)直接少了一大半。4.3 文件名亂碼問題第一次跑媒體整理腳本時(shí)我發(fā)現(xiàn)一部分文件名亂碼原因是某些特殊符號比如中文冒號、#號在主機(jī)導(dǎo)出的文件名里會(huì)被替換成特殊編碼。處理辦法很簡單在歸檔之前先做一次名字清洗把非通用字符全部替換成下劃線順便把全角符號轉(zhuǎn)成半角。最近一次導(dǎo)出素材的時(shí)候目錄里整整齊齊全是大寫英文加日期找東西舒服多了。4.4 常見問題速查表現(xiàn)象可能原因解決辦法設(shè)備發(fā)現(xiàn)無響應(yīng)不在同一網(wǎng)段手動(dòng)指定 IP或調(diào)整網(wǎng)卡綁定推送文件后校驗(yàn)失敗分塊傳輸斷連啟用斷點(diǎn)續(xù)傳重新推送缺失塊文件名亂碼特殊字符未清洗歸檔前執(zhí)行字符替換規(guī)則Web 面板刷新很慢后臺(tái)輪詢間隔過短把輪詢間隔調(diào)整到 30 秒以上上傳大文件超時(shí)單塊大小不合理調(diào)小CHUNK_SIZE到 1MB~2MB寫在最后的一個(gè)小建議AnyPS5 這個(gè)項(xiàng)目從腦子里的想法到能穩(wěn)定跑起來前前后后花了兩周時(shí)間其中一多半時(shí)間都花在真實(shí)環(huán)境適配和網(wǎng)絡(luò)排障上。我的體會(huì)是這種工具型項(xiàng)目難的不是寫代碼而是把各種不起眼的邊界情況處理好。設(shè)備發(fā)現(xiàn)失敗就重試文件傳輸斷了就續(xù)傳編碼不對就清洗規(guī)則——把這些“臟活”都干完工具才算真正可用。后面我還打算把媒體資料的自動(dòng)分析加進(jìn)去比如按游戲?qū)貓D內(nèi)容做場景分類。工具后續(xù)往哪個(gè)方向走完全取決于你日常用主機(jī)做什么錨定一個(gè)真實(shí)痛點(diǎn)這個(gè)項(xiàng)目就能一直用下去。