<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Usb on Tarragon</title><link>https://tarrragon.github.io/blog/tags/usb/</link><description>Recent content in Usb on Tarragon</description><generator>Hugo -- gohugo.io</generator><language>zh-TW</language><copyright>Tarragon (CC BY 4.0)</copyright><lastBuildDate>Tue, 08 Sep 2026 00:00:00 +0800</lastBuildDate><atom:link href="https://tarrragon.github.io/blog/tags/usb/index.xml" rel="self" type="application/rss+xml"/><item><title>claim 一個 USB 介面等於接管它，原本用它的系統功能會停止回應</title><link>https://tarrragon.github.io/blog/work-log/usb-claim-takes-the-hardware-away/</link><pubDate>Tue, 08 Sep 2026 00:00:00 +0800</pubDate><guid>https://tarrragon.github.io/blog/work-log/usb-claim-takes-the-hardware-away/</guid><description>&lt;p>Android 應用程式要與 USB 裝置通訊，取得權限之後還要 claim 該裝置的介面。claim 成功的語意是&lt;strong>獨佔&lt;/strong>：從那一刻起這個介面歸這支程式使用，作業系統原本掛在同一個介面上的功能停止運作。&lt;/p>
&lt;p>這條語意在觸控一體機上有一個具體的後果——螢幕的觸控面板本身就是一個 USB HID 裝置。應用程式 claim 到它，螢幕就不再回應觸碰，而應用程式本身完全正常，沒有例外也沒有閃退。&lt;/p>
&lt;h2 id="當掉是一個症狀詞指向四種不同的機制">「當掉」是一個症狀詞，指向四種不同的機制&lt;/h2>
&lt;p>使用者能觀察到的只有「畫面停住、點不動」，而這個描述涵蓋的機制各自需要不同的診斷方向。&lt;strong>下面這張表是事後歸納的通則，不是某一次排查實際走過的路徑&lt;/strong>——列出來的用途是讓下一次的第一個問句對得上，而不是重述一段經過：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>使用者看到的&lt;/th>
 &lt;th>底下發生的事&lt;/th>
 &lt;th>可查證的痕跡&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>畫面消失、回到桌面&lt;/td>
 &lt;td>未捕捉的例外導致行程結束&lt;/td>
 &lt;td>logcat 有 stack trace&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>畫面停住、系統跳出關閉對話框&lt;/td>
 &lt;td>主執行緒被阻塞超過門檻（ANR）&lt;/td>
 &lt;td>ANR trace 檔、logcat 有 ANR 紀錄&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>畫面停住、轉圈持續&lt;/td>
 &lt;td>等待某個逾時尚未到期&lt;/td>
 &lt;td>該次呼叫的起始 log 有、結束 log 無&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>畫面停住而畫面本身仍在更新&lt;/td>
 &lt;td>輸入通道被接管，繪製與邏輯都正常&lt;/td>
 &lt;td>三種痕跡都沒有&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>最後一種的鑑別特徵是&lt;strong>三種痕跡都沒有&lt;/strong>。這一種在只看 log 的排查裡形成一個空集合，而空集合容易被讀成「還沒找到線索」——&lt;strong>空集合本身就是線索&lt;/strong>：程式沒有壞，壞掉的是輸入到程式的路徑。&lt;/p>
&lt;p>分辨這一種的成本很低——請現場的人回報畫面上的時鐘或動畫有沒有繼續走。繼續走代表繪製與主執行緒都活著，剩下的可能就只有輸入通道。機器不在手邊時同一個判別也做得到：間隔幾秒連抓兩張畫面比對，或錄一段短影片。取畫面的指令與它的陷阱見 &lt;a href="https://tarrragon.github.io/blog/work-log/adb-remote-android-diagnosis/" data-link-title="用 adb 遠端診斷 Android 實機" data-link-desc="機器不在手邊，而要判斷畫面上是哪個 App 的視窗蓋住了操作、擷圖傳回電腦後無法開啟、或 logcat 的輸出裡找不到按下按鈕那一刻的紀錄時回來讀">adb 遠端診斷 Android 實機&lt;/a>，那裡給的是二進位輸出為什麼要走特定通道、以及多顯示器機器上編號取錯會得到什麼。&lt;/p>
&lt;h2 id="連線成功驗證的是連線動作不是連線對象">連線成功驗證的是連線動作，不是連線對象&lt;/h2>
&lt;p>USB 權限的授權對象是裝置，判斷依據是使用者同不同意，&lt;strong>而不是這個裝置適不適合當前用途&lt;/strong>。觸控面板是一個合法的 USB 裝置，權限請求會正常彈出、使用者會正常按下允許、claim 會正常成功。&lt;/p>
&lt;p>這件事推翻一種把關寫法：連線成功才儲存設定。那個寫法的隱含前提是「連得上的裝置就是對的裝置」，而權限的成功條件與 claim 的成功條件各自都只看裝置合不合法，所以它對選錯裝置這一類問題完全無效——&lt;strong>它驗的是連線這個動作成不成立，不是連線對象對不對&lt;/strong>。&lt;/p>
&lt;p>真正有效的把關位置在更前面：&lt;strong>列出候選裝置的那一刻&lt;/strong>。候選清單該用什麼條件篩，換掉的是「誰承受篩錯的代價」這一項，而&lt;a href="../filter-strategy-follows-asymmetric-failure-cost/">白名單與黑名單的選擇由這一項決定&lt;/a>——那一篇比較兩種篩法各自錯的時候誰承擔、補不補得回來。掃描結果送到使用者面前之前就把不該出現的裝置濾掉，使用者就沒有選錯的機會。把關放在連線之後，等於讓使用者先做出選擇、再由程式去驗一個驗不出對象正確性的條件。&lt;/p>
&lt;h2 id="獨佔的代價要在設計時就估">獨佔的代價要在設計時就估&lt;/h2>
&lt;p>claim 的獨佔語意帶來一個一般性的判斷點：&lt;strong>這支程式接管的硬體，有沒有其他人正在用它&lt;/strong>。&lt;/p>
&lt;p>在單一用途的外接裝置上，答案通常是沒有，獨佔沒有代價。在一體機上，答案經常是有——觸控面板、內建讀卡機、內部集線器底下的其他裝置都可能是系統功能的一部分，而接管它們的代價由使用者承擔，不由程式承擔（程式自己一切正常）。&lt;/p>
&lt;p>判斷標準因此是&lt;strong>claim 成功之後，誰失去了什麼&lt;/strong>，而不是 claim 會不會失敗。這個問題在寫程式的機器上問不出答案——開發機的螢幕通常不是 USB 裝置，那台機器上沒有這個衝突可以觀察。問題要在目標硬體上問。觸控螢幕有專門的查法（&lt;code>adb shell dumpsys input&lt;/code> 直接回答哪個裝置是它），而讀卡機、內部集線器底下的其他裝置沒有對應的單一指令——它們要靠拓樸盤點才認得出歸屬，做法見&lt;a href="../inventory-hardware-before-inferring-from-the-app/">陌生機器的硬體盤點&lt;/a>的 sysfs 那一段。&lt;/p>
&lt;h2 id="判斷標準">判斷標準&lt;/h2>
&lt;p>「畫面凍結」這個症狀的四種機制各自在別的地方留下痕跡，而輸入通道被接管的那一種在每一個地方都不留痕跡。&lt;strong>這使痕跡的缺席本身成為判別依據&lt;/strong>：例外留 stack trace、主執行緒阻塞留 ANR 紀錄、逾時留一個有起始無結束的呼叫，而輸入通道被接管的程式一切正常，所以三種紀錄同時空白、畫面又還在更新時，剩下的可能只有一種。&lt;/p>
&lt;p>設計會 claim 硬體的功能時，把關放在列出候選的階段，不放在連線之後——連線成功這個訊號不承載「對象正確」這個資訊。而接管硬體之前要先確認這塊硬體現在歸誰用，在目標機器上確認、不在開發機上推論。&lt;/p></description><content:encoded><![CDATA[<p>Android 應用程式要與 USB 裝置通訊，取得權限之後還要 claim 該裝置的介面。claim 成功的語意是<strong>獨佔</strong>：從那一刻起這個介面歸這支程式使用，作業系統原本掛在同一個介面上的功能停止運作。</p>
<p>這條語意在觸控一體機上有一個具體的後果——螢幕的觸控面板本身就是一個 USB HID 裝置。應用程式 claim 到它，螢幕就不再回應觸碰，而應用程式本身完全正常，沒有例外也沒有閃退。</p>
<h2 id="當掉是一個症狀詞指向四種不同的機制">「當掉」是一個症狀詞，指向四種不同的機制</h2>
<p>使用者能觀察到的只有「畫面停住、點不動」，而這個描述涵蓋的機制各自需要不同的診斷方向。<strong>下面這張表是事後歸納的通則，不是某一次排查實際走過的路徑</strong>——列出來的用途是讓下一次的第一個問句對得上，而不是重述一段經過：</p>
<table>
  <thead>
      <tr>
          <th>使用者看到的</th>
          <th>底下發生的事</th>
          <th>可查證的痕跡</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>畫面消失、回到桌面</td>
          <td>未捕捉的例外導致行程結束</td>
          <td>logcat 有 stack trace</td>
      </tr>
      <tr>
          <td>畫面停住、系統跳出關閉對話框</td>
          <td>主執行緒被阻塞超過門檻（ANR）</td>
          <td>ANR trace 檔、logcat 有 ANR 紀錄</td>
      </tr>
      <tr>
          <td>畫面停住、轉圈持續</td>
          <td>等待某個逾時尚未到期</td>
          <td>該次呼叫的起始 log 有、結束 log 無</td>
      </tr>
      <tr>
          <td>畫面停住而畫面本身仍在更新</td>
          <td>輸入通道被接管，繪製與邏輯都正常</td>
          <td>三種痕跡都沒有</td>
      </tr>
  </tbody>
</table>
<p>最後一種的鑑別特徵是<strong>三種痕跡都沒有</strong>。這一種在只看 log 的排查裡形成一個空集合，而空集合容易被讀成「還沒找到線索」——<strong>空集合本身就是線索</strong>：程式沒有壞，壞掉的是輸入到程式的路徑。</p>
<p>分辨這一種的成本很低——請現場的人回報畫面上的時鐘或動畫有沒有繼續走。繼續走代表繪製與主執行緒都活著，剩下的可能就只有輸入通道。機器不在手邊時同一個判別也做得到：間隔幾秒連抓兩張畫面比對，或錄一段短影片。取畫面的指令與它的陷阱見 <a href="/blog/work-log/adb-remote-android-diagnosis/" data-link-title="用 adb 遠端診斷 Android 實機" data-link-desc="機器不在手邊，而要判斷畫面上是哪個 App 的視窗蓋住了操作、擷圖傳回電腦後無法開啟、或 logcat 的輸出裡找不到按下按鈕那一刻的紀錄時回來讀">adb 遠端診斷 Android 實機</a>，那裡給的是二進位輸出為什麼要走特定通道、以及多顯示器機器上編號取錯會得到什麼。</p>
<h2 id="連線成功驗證的是連線動作不是連線對象">連線成功驗證的是連線動作，不是連線對象</h2>
<p>USB 權限的授權對象是裝置，判斷依據是使用者同不同意，<strong>而不是這個裝置適不適合當前用途</strong>。觸控面板是一個合法的 USB 裝置，權限請求會正常彈出、使用者會正常按下允許、claim 會正常成功。</p>
<p>這件事推翻一種把關寫法：連線成功才儲存設定。那個寫法的隱含前提是「連得上的裝置就是對的裝置」，而權限的成功條件與 claim 的成功條件各自都只看裝置合不合法，所以它對選錯裝置這一類問題完全無效——<strong>它驗的是連線這個動作成不成立，不是連線對象對不對</strong>。</p>
<p>真正有效的把關位置在更前面：<strong>列出候選裝置的那一刻</strong>。候選清單該用什麼條件篩，換掉的是「誰承受篩錯的代價」這一項，而<a href="../filter-strategy-follows-asymmetric-failure-cost/">白名單與黑名單的選擇由這一項決定</a>——那一篇比較兩種篩法各自錯的時候誰承擔、補不補得回來。掃描結果送到使用者面前之前就把不該出現的裝置濾掉，使用者就沒有選錯的機會。把關放在連線之後，等於讓使用者先做出選擇、再由程式去驗一個驗不出對象正確性的條件。</p>
<h2 id="獨佔的代價要在設計時就估">獨佔的代價要在設計時就估</h2>
<p>claim 的獨佔語意帶來一個一般性的判斷點：<strong>這支程式接管的硬體，有沒有其他人正在用它</strong>。</p>
<p>在單一用途的外接裝置上，答案通常是沒有，獨佔沒有代價。在一體機上，答案經常是有——觸控面板、內建讀卡機、內部集線器底下的其他裝置都可能是系統功能的一部分，而接管它們的代價由使用者承擔，不由程式承擔（程式自己一切正常）。</p>
<p>判斷標準因此是<strong>claim 成功之後，誰失去了什麼</strong>，而不是 claim 會不會失敗。這個問題在寫程式的機器上問不出答案——開發機的螢幕通常不是 USB 裝置，那台機器上沒有這個衝突可以觀察。問題要在目標硬體上問。觸控螢幕有專門的查法（<code>adb shell dumpsys input</code> 直接回答哪個裝置是它），而讀卡機、內部集線器底下的其他裝置沒有對應的單一指令——它們要靠拓樸盤點才認得出歸屬，做法見<a href="../inventory-hardware-before-inferring-from-the-app/">陌生機器的硬體盤點</a>的 sysfs 那一段。</p>
<h2 id="判斷標準">判斷標準</h2>
<p>「畫面凍結」這個症狀的四種機制各自在別的地方留下痕跡，而輸入通道被接管的那一種在每一個地方都不留痕跡。<strong>這使痕跡的缺席本身成為判別依據</strong>：例外留 stack trace、主執行緒阻塞留 ANR 紀錄、逾時留一個有起始無結束的呼叫，而輸入通道被接管的程式一切正常，所以三種紀錄同時空白、畫面又還在更新時，剩下的可能只有一種。</p>
<p>設計會 claim 硬體的功能時，把關放在列出候選的階段，不放在連線之後——連線成功這個訊號不承載「對象正確」這個資訊。而接管硬體之前要先確認這塊硬體現在歸誰用，在目標機器上確認、不在開發機上推論。</p>
]]></content:encoded></item><item><title>USB 類別代碼記在裝置層與介面層兩處，只讀裝置層的印表機會回報 0</title><link>https://tarrragon.github.io/blog/work-log/usb-class-code-lives-in-two-layers/</link><pubDate>Tue, 08 Sep 2026 00:00:00 +0800</pubDate><guid>https://tarrragon.github.io/blog/work-log/usb-class-code-lives-in-two-layers/</guid><description>&lt;p>USB 規範用一個位元組的類別代碼描述裝置的用途，而&lt;strong>同一個裝置身上有兩個地方可以記這個代碼&lt;/strong>：裝置描述符的 &lt;code>bDeviceClass&lt;/code>，以及每個介面描述符的 &lt;code>bInterfaceClass&lt;/code>。兩處各自獨立，值也可以不同。要按用途過濾裝置的程式若只讀其中一處，會在一整類硬體上靜默失效。&lt;/p>
&lt;h2 id="類別代碼本身是一組固定的數字">類別代碼本身是一組固定的數字&lt;/h2>
&lt;p>常見的幾個值：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>代碼&lt;/th>
 &lt;th>用途&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>&lt;code>1&lt;/code>&lt;/td>
 &lt;td>音訊&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;code>3&lt;/code>&lt;/td>
 &lt;td>人機介面（HID）：觸控面板、鍵盤、滑鼠、掃碼槍&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;code>7&lt;/code>&lt;/td>
 &lt;td>印表機&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;code>8&lt;/code>&lt;/td>
 &lt;td>大容量儲存&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;code>9&lt;/code>&lt;/td>
 &lt;td>集線器&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;code>14&lt;/code>&lt;/td>
 &lt;td>視訊&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;code>255&lt;/code>&lt;/td>
 &lt;td>供應商自訂&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>這組數字由 USB-IF 定義、跨作業系統一致，所以它是可以拿來做判斷的穩定依據——比裝置名稱字串可靠得多，名稱字串由廠商自由填寫。&lt;/p>
&lt;h2 id="兩層的存在來自複合裝置">兩層的存在來自複合裝置&lt;/h2>
&lt;p>一個 USB 裝置可以同時提供多種功能：一台印表機附帶讀卡機、一支鍵盤內含集線器。規範用&lt;strong>介面&lt;/strong>表達這種組合——裝置底下掛多個介面，每個介面各自宣告自己的類別。&lt;/p>
&lt;p>於是裝置層的那個代碼面對一個問題：這台裝置整體算哪一類。規範給的答案是允許它填 &lt;code>0&lt;/code>，語意是「看介面自己說」。這個選項對複合裝置是必要的，而&lt;strong>廠商對單一功能的裝置也常常這樣填&lt;/strong>——熱感印表機的裝置層類別多半是 &lt;code>0&lt;/code>，真正的 &lt;code>7&lt;/code> 記在介面上。&lt;/p>
&lt;p>這條路徑在 Android 上特別容易踩到，因為兩層各有各的取值方法而名字相似：&lt;/p>
&lt;ul>
&lt;li>&lt;code>UsbDevice.getDeviceClass()&lt;/code> 讀的是裝置層，對熱感印表機回傳 &lt;code>0&lt;/code>&lt;/li>
&lt;li>&lt;code>UsbDevice.getInterface(i).getInterfaceClass()&lt;/code> 讀的是介面層，回傳 &lt;code>7&lt;/code>&lt;/li>
&lt;/ul>
&lt;p>&lt;code>0&lt;/code> 在類別代碼裡有自己的語意（保留給「看介面」），與「查不到」不同，而程式若把它當成一個普通的類別值去比對 &lt;code>7&lt;/code>，比對結果是不相符——沒有例外、沒有錯誤訊息，過濾條件安靜地把每一台印表機都排除掉。&lt;/p>
&lt;h2 id="判斷邏輯的單位是介面不是裝置">判斷邏輯的單位是介面，不是裝置&lt;/h2>
&lt;p>由複合裝置推出兩個要求。&lt;/p>
&lt;p>&lt;strong>逐個介面檢查，而條件是「任一介面符合」。&lt;/strong> 一台裝置有三個介面而只有第二個是印表機時，只讀 &lt;code>getInterface(0)&lt;/code> 得到的答案是錯的。&lt;/p>
&lt;p>&lt;strong>裝置層的值只在它非零時有參考價值。&lt;/strong> 它非零代表廠商明確宣告了整體用途，可以直接採用；它是 &lt;code>0&lt;/code> 就必須往介面層走一趟。&lt;/p>
&lt;h2 id="255-讓白名單這條路走不通">&lt;code>255&lt;/code> 讓白名單這條路走不通&lt;/h2>
&lt;p>供應商自訂類別是規範留給廠商的出口：裝置的功能不落在既有分類裡，或廠商選擇用自家驅動程式而不走通用類別驅動。&lt;strong>而國產熱感印表機宣告成 &lt;code>255&lt;/code> 的情形確實存在——&lt;strong>這一條來自實作者的現場回報，不是本文核對過的裝置清單&lt;/strong>，所以它的效力止於存在性；要用到比例，得自己在手上的機型上量。&lt;/strong>&lt;/p>
&lt;p>這推翻「只收類別 &lt;code>7&lt;/code>」這個寫法的&lt;strong>事實前提&lt;/strong>——那個前提是全稱的（每一台印表機都宣告 &lt;code>7&lt;/code>），而一台反例就足以否定全稱命題。&lt;strong>但「前提被否定」與「這個寫法該不該用」是兩層。&lt;/strong> 該不該用是工程權衡，要靠代價而不是靠邏輯：白名單誤擋的後果是那台印表機從候選裡整個消失、而使用者沒有任何操作把它找回來。&lt;strong>這個代價與該機型的佔比無關——它對踩到的那一台是全損&lt;/strong>，所以即使佔比很低，白名單仍然是不划算的選擇。過濾策略因此要換一個方向——&lt;a href="../filter-strategy-follows-asymmetric-failure-cost/">讓失敗往可補救的方向倒&lt;/a>那一篇處理這個選擇。&lt;/p>
&lt;h2 id="判斷標準">判斷標準&lt;/h2>
&lt;p>要判定某個 USB 裝置的用途時，取值的順序由「哪一層的值可信」決定：裝置層非零就用它，裝置層是 &lt;code>0&lt;/code> 就逐個讀介面層並取任一符合。&lt;/p>
&lt;p>而&lt;strong>判定結果為「不是印表機」與「查不到類別」要分開處置&lt;/strong>。判定為不是印表機是有效的排除，查不到類別代表資料不足——藍牙裝置沒有 USB 類別欄位，把取不到值當成不合格會誤殺一整類裝置。&lt;/p>
&lt;p>驗證這件事的成本很低：&lt;code>adb shell dumpsys usb&lt;/code> 會把每個裝置的兩層類別都列出來，接上機器跑一次就知道手上這台實際填了什麼，而不必從程式行為反推。&lt;a href="../inventory-hardware-before-inferring-from-the-app/">盤點陌生機器的硬體&lt;/a>那一篇有完整的查法。&lt;/p></description><content:encoded><![CDATA[<p>USB 規範用一個位元組的類別代碼描述裝置的用途，而<strong>同一個裝置身上有兩個地方可以記這個代碼</strong>：裝置描述符的 <code>bDeviceClass</code>，以及每個介面描述符的 <code>bInterfaceClass</code>。兩處各自獨立，值也可以不同。要按用途過濾裝置的程式若只讀其中一處，會在一整類硬體上靜默失效。</p>
<h2 id="類別代碼本身是一組固定的數字">類別代碼本身是一組固定的數字</h2>
<p>常見的幾個值：</p>
<table>
  <thead>
      <tr>
          <th>代碼</th>
          <th>用途</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>1</code></td>
          <td>音訊</td>
      </tr>
      <tr>
          <td><code>3</code></td>
          <td>人機介面（HID）：觸控面板、鍵盤、滑鼠、掃碼槍</td>
      </tr>
      <tr>
          <td><code>7</code></td>
          <td>印表機</td>
      </tr>
      <tr>
          <td><code>8</code></td>
          <td>大容量儲存</td>
      </tr>
      <tr>
          <td><code>9</code></td>
          <td>集線器</td>
      </tr>
      <tr>
          <td><code>14</code></td>
          <td>視訊</td>
      </tr>
      <tr>
          <td><code>255</code></td>
          <td>供應商自訂</td>
      </tr>
  </tbody>
</table>
<p>這組數字由 USB-IF 定義、跨作業系統一致，所以它是可以拿來做判斷的穩定依據——比裝置名稱字串可靠得多，名稱字串由廠商自由填寫。</p>
<h2 id="兩層的存在來自複合裝置">兩層的存在來自複合裝置</h2>
<p>一個 USB 裝置可以同時提供多種功能：一台印表機附帶讀卡機、一支鍵盤內含集線器。規範用<strong>介面</strong>表達這種組合——裝置底下掛多個介面，每個介面各自宣告自己的類別。</p>
<p>於是裝置層的那個代碼面對一個問題：這台裝置整體算哪一類。規範給的答案是允許它填 <code>0</code>，語意是「看介面自己說」。這個選項對複合裝置是必要的，而<strong>廠商對單一功能的裝置也常常這樣填</strong>——熱感印表機的裝置層類別多半是 <code>0</code>，真正的 <code>7</code> 記在介面上。</p>
<p>這條路徑在 Android 上特別容易踩到，因為兩層各有各的取值方法而名字相似：</p>
<ul>
<li><code>UsbDevice.getDeviceClass()</code> 讀的是裝置層，對熱感印表機回傳 <code>0</code></li>
<li><code>UsbDevice.getInterface(i).getInterfaceClass()</code> 讀的是介面層，回傳 <code>7</code></li>
</ul>
<p><code>0</code> 在類別代碼裡有自己的語意（保留給「看介面」），與「查不到」不同，而程式若把它當成一個普通的類別值去比對 <code>7</code>，比對結果是不相符——沒有例外、沒有錯誤訊息，過濾條件安靜地把每一台印表機都排除掉。</p>
<h2 id="判斷邏輯的單位是介面不是裝置">判斷邏輯的單位是介面，不是裝置</h2>
<p>由複合裝置推出兩個要求。</p>
<p><strong>逐個介面檢查，而條件是「任一介面符合」。</strong> 一台裝置有三個介面而只有第二個是印表機時，只讀 <code>getInterface(0)</code> 得到的答案是錯的。</p>
<p><strong>裝置層的值只在它非零時有參考價值。</strong> 它非零代表廠商明確宣告了整體用途，可以直接採用；它是 <code>0</code> 就必須往介面層走一趟。</p>
<h2 id="255-讓白名單這條路走不通"><code>255</code> 讓白名單這條路走不通</h2>
<p>供應商自訂類別是規範留給廠商的出口：裝置的功能不落在既有分類裡，或廠商選擇用自家驅動程式而不走通用類別驅動。<strong>而國產熱感印表機宣告成 <code>255</code> 的情形確實存在——<strong>這一條來自實作者的現場回報，不是本文核對過的裝置清單</strong>，所以它的效力止於存在性；要用到比例，得自己在手上的機型上量。</strong></p>
<p>這推翻「只收類別 <code>7</code>」這個寫法的<strong>事實前提</strong>——那個前提是全稱的（每一台印表機都宣告 <code>7</code>），而一台反例就足以否定全稱命題。<strong>但「前提被否定」與「這個寫法該不該用」是兩層。</strong> 該不該用是工程權衡，要靠代價而不是靠邏輯：白名單誤擋的後果是那台印表機從候選裡整個消失、而使用者沒有任何操作把它找回來。<strong>這個代價與該機型的佔比無關——它對踩到的那一台是全損</strong>，所以即使佔比很低，白名單仍然是不划算的選擇。過濾策略因此要換一個方向——<a href="../filter-strategy-follows-asymmetric-failure-cost/">讓失敗往可補救的方向倒</a>那一篇處理這個選擇。</p>
<h2 id="判斷標準">判斷標準</h2>
<p>要判定某個 USB 裝置的用途時，取值的順序由「哪一層的值可信」決定：裝置層非零就用它，裝置層是 <code>0</code> 就逐個讀介面層並取任一符合。</p>
<p>而<strong>判定結果為「不是印表機」與「查不到類別」要分開處置</strong>。判定為不是印表機是有效的排除，查不到類別代表資料不足——藍牙裝置沒有 USB 類別欄位，把取不到值當成不合格會誤殺一整類裝置。</p>
<p>驗證這件事的成本很低：<code>adb shell dumpsys usb</code> 會把每個裝置的兩層類別都列出來，接上機器跑一次就知道手上這台實際填了什麼，而不必從程式行為反推。<a href="../inventory-hardware-before-inferring-from-the-app/">盤點陌生機器的硬體</a>那一篇有完整的查法。</p>
]]></content:encoded></item><item><title>兩種錯法的代價不對稱時，過濾策略要讓失敗倒向可補救的那一邊</title><link>https://tarrragon.github.io/blog/work-log/filter-strategy-follows-asymmetric-failure-cost/</link><pubDate>Tue, 08 Sep 2026 00:00:00 +0800</pubDate><guid>https://tarrragon.github.io/blog/work-log/filter-strategy-follows-asymmetric-failure-cost/</guid><description>&lt;p>過濾一組候選裝置有兩個方向：列出確定要的（白名單），或列出確定不要的（黑名單）。兩個方向的正確率可以完全相同，而&lt;strong>它們錯的時候代價不同&lt;/strong>，這個不對稱決定該選哪一個。&lt;/p>
&lt;h2 id="兩種誤判落在不同的人身上">兩種誤判落在不同的人身上&lt;/h2>
&lt;p>以印表機候選清單為例：&lt;/p>
&lt;p>&lt;strong>白名單誤擋&lt;/strong>——某台印表機的宣告不符合條件，它從清單裡消失。使用者看到的是一份沒有自己印表機的清單，沒有任何操作可以把它找回來，店家當下不能出單。這個失敗&lt;strong>使用者無法補救&lt;/strong>，而且它會打擊到一整批同型號的機器。&lt;/p>
&lt;p>&lt;strong>黑名單誤放&lt;/strong>——某個不該出現的裝置留在清單裡。使用者要主動選中它才會出事。這個失敗&lt;strong>需要人為誤選才會實現&lt;/strong>，且範圍限於選錯的那一台機器。&lt;/p>
&lt;p>兩種誤判的代價不對稱時，讓失敗落在使用者有辦法補救的那一種誤判上。所以這裡選黑名單：列出確定不是印表機的用途（HID、集線器、儲存、音訊、視訊），其餘一律放行。&lt;/p>
&lt;p>這個選擇的附帶效果正好解掉&lt;a href="../usb-class-code-lives-in-two-layers/">類別代碼的兩層問題&lt;/a>留下的難題——宣告成 &lt;code>255&lt;/code> 供應商自訂的熱感印表機不在排除清單裡，所以它留得住；而觸控面板是 HID，被精準擋掉。白名單擋掉 &lt;code>255&lt;/code> 的同時擋掉的是使用者真正要用的機器。&lt;/p>
&lt;h2 id="判斷不出來的時候放行">判斷不出來的時候放行&lt;/h2>
&lt;p>排除清單需要一條配套規則：&lt;strong>取不到判斷依據時放行，不當成不合格。&lt;/strong>&lt;/p>
&lt;p>藍牙印表機沒有 USB 類別欄位，這個欄位對它而言不存在。把「讀不到值」與「值不符合」歸成同一類處置，會讓整個藍牙路徑的裝置消失——而它們是完全合法的目標。&lt;/p>
&lt;p>這條規則的一般形態是：&lt;strong>過濾條件的射程要寫清楚，射程外的對象不套用該條件。&lt;/strong> 這裡的射程是「USB 裝置」，所以條件寫成「USB 裝置且用途落在排除清單裡才排除」，而不是「用途不在允許清單裡就排除」。兩種寫法對 USB 裝置的行為相同，對藍牙裝置的行為相反。&lt;/p>
&lt;p>同一條規則也管裝置型號的推斷：程式從機器簽章字串判斷廠牌、遇到認不得的機型時，正確的輸出是「不確定」而不是猜一個。不確定的處置是引導使用者去設定外接裝置，而不是硬套某個廠牌的專屬流程——硬套的失敗形態是靜默的（呼叫了一個不存在的服務、或送出對方看不懂的指令），而引導的失敗形態是使用者知道自己要做什麼。&lt;/p>
&lt;h2 id="藍牙與-usb-是兩套彼此獨立的分類體系">藍牙與 USB 是兩套彼此獨立的分類體系&lt;/h2>
&lt;p>USB 類別代碼只存在於 USB 裝置。傳統藍牙有自己的一套——Bluetooth Class of Device，用主類別加服務位元描述，印表機落在 Imaging 大類底下的一個位元。&lt;/p>
&lt;p>兩套體系的數值沒有對應關係，把 USB 的類別代碼拿去比對藍牙裝置是型別上成立而語意上無意義的比對。而藍牙那一套本身的可靠度也有限：國產藍牙熱感機把 Class of Device 填得不正確的情形存在（同樣是現場回報、非本文核對過的清單），據以過濾就會誤擋。&lt;/p>
&lt;p>這裡的處置是&lt;strong>把過濾條件限定在 USB 裝置，藍牙一律放行&lt;/strong>。這個決定有一個具體的依據：機器內部的硬體（觸控面板、內部集線器上的裝置）不會出現在藍牙已配對清單裡，所以藍牙路徑上根本沒有「選到自己的螢幕」這種風險，過濾在那裡沒有要防的對象。&lt;strong>這兩個理由各自獨立、任一個成立就足以支持同一個處置&lt;/strong>：宣告不可靠說的是在藍牙上過濾會做錯，而內部硬體不走藍牙說的是在藍牙上根本不必過濾。&lt;/p>
&lt;h2 id="型別對而語意錯的比對不會報錯">型別對而語意錯的比對不會報錯&lt;/h2>
&lt;p>過濾條件寫對了，清單仍然可能是空的——&lt;strong>同一個症狀有第二個成因，而它落在跨層傳遞的數值表示法上&lt;/strong>。排查「清單為什麼空」時這兩個成因都要考慮，所以它跟前面幾節屬於同一個問題的不同分支，而不是另一個主題。&lt;/p>
&lt;p>原生端把 USB 的 vendor id 以十進位字串交給上層時，寫成 &lt;code>0xABCD&lt;/code> 的那個值到上層手上是 &lt;code>&amp;quot;43981&amp;quot;&lt;/code>。用十六進位字面值寫比對規則的程式不會編譯失敗、不會拋出例外，比對單純不成立——與過濾條件寫錯的表現完全一樣。&lt;/p>
&lt;p>這一類的鑑別方式是把值印出來看，而不是重讀比對邏輯。比對邏輯讀起來是對的（那個十六進位值確實是該裝置的 vendor id），錯的是兩端對「這個欄位長什麼樣」的假設不一致，而那個假設沒有寫在任何一邊的程式碼裡。&lt;strong>上面的數值是佔位的&lt;/strong>——每一層實際用什麼表示法由那一層自己決定，要知道自己這一套是哪一種，只有把值印出來看。跨層邊界上的每個識別碼欄位都值得問一次：它在這一端是什麼型別、什麼進位、什麼大小寫。&lt;/p>
&lt;h2 id="判斷標準">判斷標準&lt;/h2>
&lt;p>選白名單還是黑名單，先問兩種誤判分別落在誰身上、以及他有沒有辦法自己補救。選的方向要讓誤判落在有辦法自己補救的人身上。&lt;/p>
&lt;p>過濾條件要寫出自己的射程，射程外的對象不套用；取不到判斷依據時放行，並把「不確定」當成一個要輸出的答案，不是一個要猜掉的空缺。&lt;/p>
&lt;p>跨層邊界上的識別碼比對失效時，先印值再讀邏輯——型別對而語意錯的比對在程式碼上看起來正確。&lt;/p></description><content:encoded><![CDATA[<p>過濾一組候選裝置有兩個方向：列出確定要的（白名單），或列出確定不要的（黑名單）。兩個方向的正確率可以完全相同，而<strong>它們錯的時候代價不同</strong>，這個不對稱決定該選哪一個。</p>
<h2 id="兩種誤判落在不同的人身上">兩種誤判落在不同的人身上</h2>
<p>以印表機候選清單為例：</p>
<p><strong>白名單誤擋</strong>——某台印表機的宣告不符合條件，它從清單裡消失。使用者看到的是一份沒有自己印表機的清單，沒有任何操作可以把它找回來，店家當下不能出單。這個失敗<strong>使用者無法補救</strong>，而且它會打擊到一整批同型號的機器。</p>
<p><strong>黑名單誤放</strong>——某個不該出現的裝置留在清單裡。使用者要主動選中它才會出事。這個失敗<strong>需要人為誤選才會實現</strong>，且範圍限於選錯的那一台機器。</p>
<p>兩種誤判的代價不對稱時，讓失敗落在使用者有辦法補救的那一種誤判上。所以這裡選黑名單：列出確定不是印表機的用途（HID、集線器、儲存、音訊、視訊），其餘一律放行。</p>
<p>這個選擇的附帶效果正好解掉<a href="../usb-class-code-lives-in-two-layers/">類別代碼的兩層問題</a>留下的難題——宣告成 <code>255</code> 供應商自訂的熱感印表機不在排除清單裡，所以它留得住；而觸控面板是 HID，被精準擋掉。白名單擋掉 <code>255</code> 的同時擋掉的是使用者真正要用的機器。</p>
<h2 id="判斷不出來的時候放行">判斷不出來的時候放行</h2>
<p>排除清單需要一條配套規則：<strong>取不到判斷依據時放行，不當成不合格。</strong></p>
<p>藍牙印表機沒有 USB 類別欄位，這個欄位對它而言不存在。把「讀不到值」與「值不符合」歸成同一類處置，會讓整個藍牙路徑的裝置消失——而它們是完全合法的目標。</p>
<p>這條規則的一般形態是：<strong>過濾條件的射程要寫清楚，射程外的對象不套用該條件。</strong> 這裡的射程是「USB 裝置」，所以條件寫成「USB 裝置且用途落在排除清單裡才排除」，而不是「用途不在允許清單裡就排除」。兩種寫法對 USB 裝置的行為相同，對藍牙裝置的行為相反。</p>
<p>同一條規則也管裝置型號的推斷：程式從機器簽章字串判斷廠牌、遇到認不得的機型時，正確的輸出是「不確定」而不是猜一個。不確定的處置是引導使用者去設定外接裝置，而不是硬套某個廠牌的專屬流程——硬套的失敗形態是靜默的（呼叫了一個不存在的服務、或送出對方看不懂的指令），而引導的失敗形態是使用者知道自己要做什麼。</p>
<h2 id="藍牙與-usb-是兩套彼此獨立的分類體系">藍牙與 USB 是兩套彼此獨立的分類體系</h2>
<p>USB 類別代碼只存在於 USB 裝置。傳統藍牙有自己的一套——Bluetooth Class of Device，用主類別加服務位元描述，印表機落在 Imaging 大類底下的一個位元。</p>
<p>兩套體系的數值沒有對應關係，把 USB 的類別代碼拿去比對藍牙裝置是型別上成立而語意上無意義的比對。而藍牙那一套本身的可靠度也有限：國產藍牙熱感機把 Class of Device 填得不正確的情形存在（同樣是現場回報、非本文核對過的清單），據以過濾就會誤擋。</p>
<p>這裡的處置是<strong>把過濾條件限定在 USB 裝置，藍牙一律放行</strong>。這個決定有一個具體的依據：機器內部的硬體（觸控面板、內部集線器上的裝置）不會出現在藍牙已配對清單裡，所以藍牙路徑上根本沒有「選到自己的螢幕」這種風險，過濾在那裡沒有要防的對象。<strong>這兩個理由各自獨立、任一個成立就足以支持同一個處置</strong>：宣告不可靠說的是在藍牙上過濾會做錯，而內部硬體不走藍牙說的是在藍牙上根本不必過濾。</p>
<h2 id="型別對而語意錯的比對不會報錯">型別對而語意錯的比對不會報錯</h2>
<p>過濾條件寫對了，清單仍然可能是空的——<strong>同一個症狀有第二個成因，而它落在跨層傳遞的數值表示法上</strong>。排查「清單為什麼空」時這兩個成因都要考慮，所以它跟前面幾節屬於同一個問題的不同分支，而不是另一個主題。</p>
<p>原生端把 USB 的 vendor id 以十進位字串交給上層時，寫成 <code>0xABCD</code> 的那個值到上層手上是 <code>&quot;43981&quot;</code>。用十六進位字面值寫比對規則的程式不會編譯失敗、不會拋出例外，比對單純不成立——與過濾條件寫錯的表現完全一樣。</p>
<p>這一類的鑑別方式是把值印出來看，而不是重讀比對邏輯。比對邏輯讀起來是對的（那個十六進位值確實是該裝置的 vendor id），錯的是兩端對「這個欄位長什麼樣」的假設不一致，而那個假設沒有寫在任何一邊的程式碼裡。<strong>上面的數值是佔位的</strong>——每一層實際用什麼表示法由那一層自己決定，要知道自己這一套是哪一種，只有把值印出來看。跨層邊界上的每個識別碼欄位都值得問一次：它在這一端是什麼型別、什麼進位、什麼大小寫。</p>
<h2 id="判斷標準">判斷標準</h2>
<p>選白名單還是黑名單，先問兩種誤判分別落在誰身上、以及他有沒有辦法自己補救。選的方向要讓誤判落在有辦法自己補救的人身上。</p>
<p>過濾條件要寫出自己的射程，射程外的對象不套用；取不到判斷依據時放行，並把「不確定」當成一個要輸出的答案，不是一個要猜掉的空缺。</p>
<p>跨層邊界上的識別碼比對失效時，先印值再讀邏輯——型別對而語意錯的比對在程式碼上看起來正確。</p>
]]></content:encoded></item><item><title>陌生機器的硬體盤點比從應用端反推便宜，因為它把假設換成可核對的值</title><link>https://tarrragon.github.io/blog/work-log/inventory-hardware-before-inferring-from-the-app/</link><pubDate>Tue, 08 Sep 2026 00:00:00 +0800</pubDate><guid>https://tarrragon.github.io/blog/work-log/inventory-hardware-before-inferring-from-the-app/</guid><description>&lt;p>從應用程式的行為往回推測硬體長什麼樣，每一步的產出都是一個未經核對的假設，而假設會互相污染——兩個假設同時可疑時，一次實驗改不動其中一個。直接把硬體攤開來看，產出換成可以逐項核對的值。&lt;strong>在一台不熟悉的機器上，盤點通常更快，而且它的結論不會因為換了一支程式就失效。&lt;/strong>&lt;/p>
&lt;p>這個優勢有它的射程。盤點換掉的是&lt;strong>識別與歸屬&lt;/strong>類的假設（這是什麼裝置、掛在哪、現在歸誰用），換不掉純邏輯的錯誤——硬體讀值正確而程式判斷寫錯時，硬體清單上看不出任何異常。而下面的每一條路徑都建立在&lt;strong>能取得 adb shell&lt;/strong> 這個前提上；量產機為了合規常關閉 USB 偵錯，那時這一整套方法都走不通，得改用機器自己的診斷介面或回到應用端。&lt;/p>
&lt;h2 id="從應用端反推的成本結構">從應用端反推的成本結構&lt;/h2>
&lt;p>從程式行為反推的過程&lt;strong>典型上&lt;/strong>長這樣（這是這條路線的形狀，不是某一次排查的逐字紀錄）：程式找不到印表機，所以推測過濾條件太嚴；改寬條件，出現一堆裝置，所以推測某幾個是內部硬體；試著連其中一個，觸控失效，所以推測那個是螢幕。&lt;/p>
&lt;p>每一步都要跑一次完整的建置與安裝循環，而每一步得到的是一個間接證據。更麻煩的是這些推測會互相污染——過濾條件與裝置識別兩個假設同時可疑時，一次實驗改不動其中一個。&lt;/p>
&lt;p>硬體盤點把這一段換成幾次唯讀查詢，而查詢的對象是作業系統自己記錄的狀態。&lt;/p>
&lt;h2 id="各項查詢回答的問題不同">各項查詢回答的問題不同&lt;/h2>
&lt;p>&lt;code>adb shell dumpsys usb&lt;/code> 回答「Android 認得哪些 USB 裝置」。它把假設換成值的地方在於：程式端只看得到自己過濾之後的結果，而這裡看到的是過濾之前的全集，兩者的差就是過濾條件實際擋掉了什麼。輸出含 vendor id、product id、製造商與產品名稱字串，以及每個介面的類別——&lt;a href="../usb-class-code-lives-in-two-layers/">類別代碼記在兩層&lt;/a>的那個問題可以在這裡直接看到答案，不必從程式的比對結果反推。&lt;/p>
&lt;p>sysfs 底下的 &lt;code>/sys/bus/usb/devices/*/&lt;/code> 給的資訊更完整。每個裝置目錄裡有 &lt;code>idVendor&lt;/code>、&lt;code>idProduct&lt;/code>、&lt;code>product&lt;/code>、&lt;code>manufacturer&lt;/code>、&lt;code>bDeviceClass&lt;/code>，介面的類別在 &lt;code>&amp;lt;裝置&amp;gt;:1.0/bInterfaceClass&lt;/code>。這條路徑額外提供的是&lt;strong>拓樸&lt;/strong>——目錄名稱本身編碼了裝置掛在哪個集線器的哪個埠底下，而一體機的內建印表機可能掛在內部集線器底下——拓樸看得出這件事，而裝置清單看不出來。知道拓樸就能分辨「這台裝置在機器內部」與「這台是使用者接上去的」。&lt;/p>
&lt;p>&lt;code>adb shell dumpsys input&lt;/code> 回答「哪個裝置是觸控螢幕」。它換掉的假設是歸屬——USB 清單只說有哪些裝置，說不出哪一台正被系統用著，而輸出裡的 &lt;code>DeviceType: touchScreen&lt;/code> 是把某個 USB 裝置與螢幕觸控連起來的&lt;strong>直接證據&lt;/strong>——沒有這一步，觸控面板在 USB 清單裡與任何一台 HID 裝置長得一樣。&lt;/p>
&lt;p>&lt;code>adb shell pm list packages&lt;/code> 與 &lt;code>dumpsys package&lt;/code> 回答「裝的是哪一版、是不是可偵錯的建置」。版本這一項換掉的假設是「現場跑的就是我手上這份程式碼」，而那個假設在排查裡經常是錯的。可偵錯的建置&lt;strong>一般可以&lt;/strong>用 &lt;code>run-as&lt;/code> 讀應用程式私有目錄，而這條路徑能回答「設定實際存了什麼值」——那個問題的答案決定&lt;a href="../validate-persisted-settings-on-read/">讀取端驗證&lt;/a>該驗什麼。&lt;/p>
&lt;p>&lt;code>adb exec-out screencap -p&lt;/code> 搭配 &lt;code>adb shell input tap&lt;/code> 讓遠端操作介面並取回畫面，不必每一次都請現場的人動手。這條路徑不經過螢幕，所以觸控已經失效的機器仍然操作得了。這裡走 &lt;code>exec-out&lt;/code> 而不走 &lt;code>shell&lt;/code>，因為 &lt;code>shell&lt;/code> 在配置了 pty 的時候會改寫換行、把 PNG 寫壞；&lt;a href="../adb-remote-android-diagnosis/">adb 遠端診斷時該信哪一種輸入&lt;/a>展開這個差別，並給畫面歸屬與 logcat 參數的取法。&lt;/p>
&lt;p>&lt;code>adb logcat -v time &amp;lt;TAG&amp;gt;:D &amp;quot;*:S&amp;quot;&lt;/code> 過濾特定標籤。這裡有一個會誤導歸因的細節：&lt;strong>zsh 底下 &lt;code>*:S&lt;/code> 要加引號&lt;/strong>，否則 shell 會先把它當成萬用字元展開、在找不到相符檔案時報錯。那個錯誤訊息來自 shell 而不是 adb，所以它看起來像是 adb 或裝置出了問題——&lt;strong>排查工具本身的失敗被讀成排查對象的失敗&lt;/strong>，而這正是盤點要避免的那種假設。&lt;/p>
&lt;h2 id="內建印表機有兩種而簽章字串分不出來">內建印表機有兩種，而簽章字串分不出來&lt;/h2>
&lt;p>一體機標榜的「內建印表機」在實作上分兩類：一類要用廠商自家的 SDK 才驅動得了，另一類就是掛在內部 USB 上的標準 ESC/POS 印表機、走通用路徑即可。&lt;/p>
&lt;p>判斷屬於哪一類，硬體盤點直接給答案：&lt;code>dumpsys usb&lt;/code> 或 sysfs 裡看得到一台類別為印表機（或 &lt;code>255&lt;/code>）的裝置掛在內部集線器底下，走的就是標準 ESC/POS 那一類。看不到，才需要考慮廠商 SDK。&lt;/p>
&lt;p>用機器簽章字串（製造商、型號）推斷廠牌有一個結構性的限制：&lt;strong>它只對已經認得的機型有效&lt;/strong>。認不得的機型回傳的正確答案是「不確定」，而系統這時該做的是引導使用者去設定外接裝置，不是猜一個廠牌硬套它的 SDK。判斷不出來時的處置在&lt;a href="../filter-strategy-follows-asymmetric-failure-cost/">過濾策略&lt;/a>那一篇有一般化的說法。&lt;/p>
&lt;h2 id="沒有相機的一體機比想像中多">沒有相機的一體機比想像中多&lt;/h2>
&lt;p>盤點時值得順手確認的還有相機。某些 POS 一體機沒有相機，相機初始化會回報可用相機為零台——那個零描述的是硬體組態，初始化本身正常完成。&lt;/p>
&lt;p>推論是掃條碼要配外接掃碼槍，而&lt;strong>掃碼槍是 USB HID&lt;/strong>，正好落在印表機候選的排除清單裡。這在設計上要確認一次：排除清單擋掉觸控面板的同時也擋掉掃碼槍，而掃碼槍本來就不該出現在印表機候選裡，所以這個結果是對的——但如果哪天要做「選擇掃碼槍」的功能，它需要自己的一份候選條件——印表機排除清單的反面是「類別為印表機或供應商自訂」，而掃碼槍是 HID，沿用反面會把它連同觸控面板、鍵盤、滑鼠一起排除掉。&lt;strong>互補集不等於目標集&lt;/strong>。&lt;/p>
&lt;h2 id="判斷標準">判斷標準&lt;/h2>
&lt;p>面對一台不熟悉的機器，先取硬體清單再讀程式行為——盤點的產出是可核對的值，反推的產出是待驗證的假設，而假設會互相污染。&lt;/p>
&lt;p>盤點的完整度用一個條件檢查：機器上每一個會被程式列進候選的裝置，都能說出它是什麼、掛在哪裡、現在歸誰用。第三項是最容易漏的一項，而它正是&lt;a href="../usb-claim-takes-the-hardware-away/">claim 一個介面等於接管它&lt;/a>要的輸入，因為接管之後誰失去了什麼，取決於這塊硬體現在歸誰。&lt;/p></description><content:encoded><![CDATA[<p>從應用程式的行為往回推測硬體長什麼樣，每一步的產出都是一個未經核對的假設，而假設會互相污染——兩個假設同時可疑時，一次實驗改不動其中一個。直接把硬體攤開來看，產出換成可以逐項核對的值。<strong>在一台不熟悉的機器上，盤點通常更快，而且它的結論不會因為換了一支程式就失效。</strong></p>
<p>這個優勢有它的射程。盤點換掉的是<strong>識別與歸屬</strong>類的假設（這是什麼裝置、掛在哪、現在歸誰用），換不掉純邏輯的錯誤——硬體讀值正確而程式判斷寫錯時，硬體清單上看不出任何異常。而下面的每一條路徑都建立在<strong>能取得 adb shell</strong> 這個前提上；量產機為了合規常關閉 USB 偵錯，那時這一整套方法都走不通，得改用機器自己的診斷介面或回到應用端。</p>
<h2 id="從應用端反推的成本結構">從應用端反推的成本結構</h2>
<p>從程式行為反推的過程<strong>典型上</strong>長這樣（這是這條路線的形狀，不是某一次排查的逐字紀錄）：程式找不到印表機，所以推測過濾條件太嚴；改寬條件，出現一堆裝置，所以推測某幾個是內部硬體；試著連其中一個，觸控失效，所以推測那個是螢幕。</p>
<p>每一步都要跑一次完整的建置與安裝循環，而每一步得到的是一個間接證據。更麻煩的是這些推測會互相污染——過濾條件與裝置識別兩個假設同時可疑時，一次實驗改不動其中一個。</p>
<p>硬體盤點把這一段換成幾次唯讀查詢，而查詢的對象是作業系統自己記錄的狀態。</p>
<h2 id="各項查詢回答的問題不同">各項查詢回答的問題不同</h2>
<p><code>adb shell dumpsys usb</code> 回答「Android 認得哪些 USB 裝置」。它把假設換成值的地方在於：程式端只看得到自己過濾之後的結果，而這裡看到的是過濾之前的全集，兩者的差就是過濾條件實際擋掉了什麼。輸出含 vendor id、product id、製造商與產品名稱字串，以及每個介面的類別——<a href="../usb-class-code-lives-in-two-layers/">類別代碼記在兩層</a>的那個問題可以在這裡直接看到答案，不必從程式的比對結果反推。</p>
<p>sysfs 底下的 <code>/sys/bus/usb/devices/*/</code> 給的資訊更完整。每個裝置目錄裡有 <code>idVendor</code>、<code>idProduct</code>、<code>product</code>、<code>manufacturer</code>、<code>bDeviceClass</code>，介面的類別在 <code>&lt;裝置&gt;:1.0/bInterfaceClass</code>。這條路徑額外提供的是<strong>拓樸</strong>——目錄名稱本身編碼了裝置掛在哪個集線器的哪個埠底下，而一體機的內建印表機可能掛在內部集線器底下——拓樸看得出這件事，而裝置清單看不出來。知道拓樸就能分辨「這台裝置在機器內部」與「這台是使用者接上去的」。</p>
<p><code>adb shell dumpsys input</code> 回答「哪個裝置是觸控螢幕」。它換掉的假設是歸屬——USB 清單只說有哪些裝置，說不出哪一台正被系統用著，而輸出裡的 <code>DeviceType: touchScreen</code> 是把某個 USB 裝置與螢幕觸控連起來的<strong>直接證據</strong>——沒有這一步，觸控面板在 USB 清單裡與任何一台 HID 裝置長得一樣。</p>
<p><code>adb shell pm list packages</code> 與 <code>dumpsys package</code> 回答「裝的是哪一版、是不是可偵錯的建置」。版本這一項換掉的假設是「現場跑的就是我手上這份程式碼」，而那個假設在排查裡經常是錯的。可偵錯的建置<strong>一般可以</strong>用 <code>run-as</code> 讀應用程式私有目錄，而這條路徑能回答「設定實際存了什麼值」——那個問題的答案決定<a href="../validate-persisted-settings-on-read/">讀取端驗證</a>該驗什麼。</p>
<p><code>adb exec-out screencap -p</code> 搭配 <code>adb shell input tap</code> 讓遠端操作介面並取回畫面，不必每一次都請現場的人動手。這條路徑不經過螢幕，所以觸控已經失效的機器仍然操作得了。這裡走 <code>exec-out</code> 而不走 <code>shell</code>，因為 <code>shell</code> 在配置了 pty 的時候會改寫換行、把 PNG 寫壞；<a href="../adb-remote-android-diagnosis/">adb 遠端診斷時該信哪一種輸入</a>展開這個差別，並給畫面歸屬與 logcat 參數的取法。</p>
<p><code>adb logcat -v time &lt;TAG&gt;:D &quot;*:S&quot;</code> 過濾特定標籤。這裡有一個會誤導歸因的細節：<strong>zsh 底下 <code>*:S</code> 要加引號</strong>，否則 shell 會先把它當成萬用字元展開、在找不到相符檔案時報錯。那個錯誤訊息來自 shell 而不是 adb，所以它看起來像是 adb 或裝置出了問題——<strong>排查工具本身的失敗被讀成排查對象的失敗</strong>，而這正是盤點要避免的那種假設。</p>
<h2 id="內建印表機有兩種而簽章字串分不出來">內建印表機有兩種，而簽章字串分不出來</h2>
<p>一體機標榜的「內建印表機」在實作上分兩類：一類要用廠商自家的 SDK 才驅動得了，另一類就是掛在內部 USB 上的標準 ESC/POS 印表機、走通用路徑即可。</p>
<p>判斷屬於哪一類，硬體盤點直接給答案：<code>dumpsys usb</code> 或 sysfs 裡看得到一台類別為印表機（或 <code>255</code>）的裝置掛在內部集線器底下，走的就是標準 ESC/POS 那一類。看不到，才需要考慮廠商 SDK。</p>
<p>用機器簽章字串（製造商、型號）推斷廠牌有一個結構性的限制：<strong>它只對已經認得的機型有效</strong>。認不得的機型回傳的正確答案是「不確定」，而系統這時該做的是引導使用者去設定外接裝置，不是猜一個廠牌硬套它的 SDK。判斷不出來時的處置在<a href="../filter-strategy-follows-asymmetric-failure-cost/">過濾策略</a>那一篇有一般化的說法。</p>
<h2 id="沒有相機的一體機比想像中多">沒有相機的一體機比想像中多</h2>
<p>盤點時值得順手確認的還有相機。某些 POS 一體機沒有相機，相機初始化會回報可用相機為零台——那個零描述的是硬體組態，初始化本身正常完成。</p>
<p>推論是掃條碼要配外接掃碼槍，而<strong>掃碼槍是 USB HID</strong>，正好落在印表機候選的排除清單裡。這在設計上要確認一次：排除清單擋掉觸控面板的同時也擋掉掃碼槍，而掃碼槍本來就不該出現在印表機候選裡，所以這個結果是對的——但如果哪天要做「選擇掃碼槍」的功能，它需要自己的一份候選條件——印表機排除清單的反面是「類別為印表機或供應商自訂」，而掃碼槍是 HID，沿用反面會把它連同觸控面板、鍵盤、滑鼠一起排除掉。<strong>互補集不等於目標集</strong>。</p>
<h2 id="判斷標準">判斷標準</h2>
<p>面對一台不熟悉的機器，先取硬體清單再讀程式行為——盤點的產出是可核對的值，反推的產出是待驗證的假設，而假設會互相污染。</p>
<p>盤點的完整度用一個條件檢查：機器上每一個會被程式列進候選的裝置，都能說出它是什麼、掛在哪裡、現在歸誰用。第三項是最容易漏的一項，而它正是<a href="../usb-claim-takes-the-hardware-away/">claim 一個介面等於接管它</a>要的輸入，因為接管之後誰失去了什麼，取決於這塊硬體現在歸誰。</p>
]]></content:encoded></item></channel></rss>