TinyPilot: Month 2

Michael Lynch

TinyPilot:第二個月

原文由 Michael Lynch 發布,訂閱此部落格

一句話總結

TinyPilot 丟給我一個超大的意外難題,但整體仍持續向前推進。

亮點

  • 為了解決設計問題,我暫停了 TinyPilot 的銷售。
  • 正在為 TinyPilot 製造客製化的 USB 電源轉接頭。
  • 所有專案的總營收達 3.6 千美元,創下歷來最佳表現之一。

目標成績單

每個月月初,我都會訂下想完成的事。以下是本月的達成狀況:

銷售 30 組 TinyPilot 套件

  • 結果:售出 16 組 TinyPilot 套件
  • 成績:C

原本進度超前、有望達標,但後來出了狀況,不得不暫停銷售。

測試三個新的行銷管道

  • 結果:未測試任何行銷管道
  • 成績:F

基於上述相同原因,在重新上架之前,我暫緩了行銷方面的嘗試。

為 TinyPilot 實作滑鼠整合支援

這項功能比預期困難許多,不過我在月底前還是完成了。整合滑鼠後,使用者體驗的提升超乎我的預期。

TinyPilot

指標2020 年 7 月2020 年 8 月變化
不重複訪客4,9302,284-2,646 (-54%)
總瀏覽數10,4276,136-4,291 (-41%)
銷售營收$8,741.37$3,030.74-$5,710.63 (-65%)
捐款N/A$94.06+$94.06 (+inf%)
總營收$8,741.37$3,124.80-$5,616.57 (-64%)

雖然訪客數據不如上個月因在 Hacker News 上爆紅時那麼亮眼,但穩定的潛在客戶流量仍讓我感到滿意。

我未能達成銷售目標,部分原因是庫存補貨跟不上,更主要的原因是月中不得不暫停銷售。

捐款是個令人驚喜的收穫。我收到了將近 100 美元的捐款,來自想支持這個專案的人,其中還有一筆看起來像是機器人捐的。

為什麼偏偏是 Y 型線啊!

從 TinyPilot 最早期開始,我就一直在為一個大問題苦惱:供電。

Raspberry Pi 有一項特殊能力,可以模擬成其他 USB 裝置。這正是它能對目標電腦輸入按鍵的方式。它會告訴電腦自己是一把 USB 鍵盤,然後像一般鍵盤那樣送出按鍵訊號。

問題在於,唯一能模擬鍵盤的連接埠,同時也是主要的供電埠。電腦的 USB 埠確實會提供一點電力,但不足以滿足 Pi 官方要求的 3.0 安培。TinyPilot 初版只靠 0.5 安培運作,雖然跑得動,但我一直擔心供電不足會引發非預期的問題,所以急著想找到更好的解法。

最後,我找到了這條 USB OTG Y 型線,看起來正是我需要的:

Y 型線的照片

這條看起來正合需求的 USB OTG Y 型線

我買了一條,居然真的能用!它把 Raspberry Pi 的連接分開,讓我可以同時接上電源和目標電腦。我把整個供貨都轉換成整合這條線的 TinyPilot v2。在賣出六組套件、開始宣傳新版本後,有人來問我,這條線是否能防止電力回流。

電力回流?我根本沒意識到這會是個問題。

原來,這條 Y 型線並不是設計來連接兩個獨立電源的。理論上,外接電源和電腦的 USB 埠都會輸出 5 伏特的電力。但實際上,並無法保證兩邊都剛好是 5V。USB 供電規範允許的範圍是 4.4 至 5.25 伏特,所以如果電腦那端的輸出掉到 4.5 伏特,電流就會從外接電源反向流回電腦的 USB 埠,可能導致連接埠過載、甚至永久損壞。

讀者一提出這個風險,我就立刻暫停銷售,並聘請一家電機工程公司進行調查。他們確認風險確實存在,於是我聯繫了已購買的客戶,建議他們在找到解決方案前先拔除外接電源。幸好 TinyPilot 沒有外接電源也能運作,只是比較不方便。

從零開始,兩週內就能做出實體產品?

過去一個月最讓我驚訝的發現之一,就是製造的速度與成本已經變得如此之快、如此便宜。

就在一週前,8 月 27 日,我請 TinyPilot 的電機工程合作夥伴設計一個轉接頭來解決供電問題。設計在隔天就完成了,他們立刻下單訂製了 100 片電路板。預計這週末就會到貨。測試與組裝應該只需再花幾天。

同時,我也正與一家 3D 列印設計工作室合作,為這塊電路板製作外殼。3D 列印廠商在兩天內就完成了設計,現在正在列印前三個原型。一旦上線,他們每天最多可列印 50 個外殼。

TinyPilot 部落格文章排名第一的螢幕截圖TinyPilot 在 reddit 上的投稿截圖

正以驚人速度成形的 TinyPilot 電源轉接頭

如果一切順利,外殼和電路板最快下週就能出貨給客戶。這意味著我們只花了兩週,就從零做出了一個完整的實體產品。

包含設計、零件與人工,這一批的成本預計約為每組 13 美元。即使是這麼簡單的專案,我也完全沒想到交期與成本可以壓到這麼低。當然,前提是一切順利。

HID 描述符簡直是惡魔

前面提到的,TinyPilot 必須讓目標電腦把它識別為一把 USB 鍵盤。它透過 USB 連線傳送一種稱為人機介面裝置(HID)描述符的資料來達成。像鍵盤、滑鼠、隨身碟這類 USB 裝置,都有各自的 HID 描述符來宣告裝置能做什麼。

HID 描述符是一段二進位的資料區塊,看起來像這樣:

// HID descriptor for a keyboard
// Source: https://www.kernel.org/doc/html/latest/usb/gadget_hid.html
static struct hidg_func_descriptor my_hid_data = {
      .subclass               = 0, /* No subclass */
      .protocol               = 1, /* Keyboard */
      .report_length          = 8,
      .report_desc_length     = 63,
      .report_desc            = {
              0x05, 0x01,     /* USAGE_PAGE (Generic Desktop)           */
              0x09, 0x06,     /* USAGE (Keyboard)                       */
              0xa1, 0x01,     /* COLLECTION (Application)               */
              0x05, 0x07,     /*   USAGE_PAGE (Keyboard)                */
              0x19, 0xe0,     /*   USAGE_MINIMUM (Keyboard LeftControl) */
              0x29, 0xe7,     /*   USAGE_MAXIMUM (Keyboard Right GUI)   */
              0x15, 0x00,     /*   LOGICAL_MINIMUM (0)                  */
              0x25, 0x01,     /*   LOGICAL_MAXIMUM (1)                  */
              0x75, 0x01,     /*   REPORT_SIZE (1)                      */
              0x95, 0x08,     /*   REPORT_COUNT (8)                     */
              0x81, 0x02,     /*   INPUT (Data,Var,Abs)                 */
              0x95, 0x01,     /*   REPORT_COUNT (1)                     */
              0x75, 0x08,     /*   REPORT_SIZE (8)                      */
              0x81, 0x03,     /*   INPUT (Cnst,Var,Abs)                 */
              0x95, 0x05,     /*   REPORT_COUNT (5)                     */
              0x75, 0x01,     /*   REPORT_SIZE (1)                      */
              0x05, 0x08,     /*   USAGE_PAGE (LEDs)                    */
              0x19, 0x01,     /*   USAGE_MINIMUM (Num Lock)             */
              0x29, 0x05,     /*   USAGE_MAXIMUM (Kana)                 */
              0x91, 0x02,     /*   OUTPUT (Data,Var,Abs)                */
              0x95, 0x01,     /*   REPORT_COUNT (1)                     */
              0x75, 0x03,     /*   REPORT_SIZE (3)                      */
              0x91, 0x03,     /*   OUTPUT (Cnst,Var,Abs)                */
              0x95, 0x06,     /*   REPORT_COUNT (6)                     */
              0x75, 0x08,     /*   REPORT_SIZE (8)                      */
              0x15, 0x00,     /*   LOGICAL_MINIMUM (0)                  */
              0x25, 0x65,     /*   LOGICAL_MAXIMUM (101)                */
              0x05, 0x07,     /*   USAGE_PAGE (Keyboard)                */
              0x19, 0x00,     /*   USAGE_MINIMUM (Reserved)             */
              0x29, 0x65,     /*   USAGE_MAXIMUM (Keyboard Application) */
              0x81, 0x00,     /*   INPUT (Data,Ary,Abs)                 */
              0xc0            /* END_COLLECTION                         */
      }
};

為鍵盤建立 HID 描述符算是輕而易舉。很多人都曾用 Python 實作過虛擬鍵盤,相關文件也很完整。

實作虛擬滑鼠就困難多了,我必須深入了解 HID 描述符的運作方式。滑鼠有更多變數,例如按鍵數量、滾輪數量,以及定位方式(絕對定位 vs. 相對定位)。除錯非常痛苦,因為描述符只有「能用」或「不能用」兩種結果。如果產生了無效的描述符,完全不會有任何回饋告訴你哪裡出錯。更糟的是,每試一次描述符,就得重開一次 Raspberry Pi。

我花了五天枯燥的嘗試,才讓基本的滑鼠功能動起來。關鍵在於專注於工具的打造。一開始,我是把描述符當成一大坨沒有結構的資料來處理,像這樣:

echo -ne \x05\x01\x09\x02\xA1\x01\x05\x09\x19\x01\x29\x08\x15\x00\x25\x01\x95\x08\x75\x01\x81\x02\x05\x01\x09\x30\x09\x31\x16\x00\x00\x26\xFF\x7F\x75\x10\x95\x02\x81\x02\xC0 > "${MOUSE_FUNCTIONS_DIR}/report_desc"

這讓我很難思考描述符的內容,因為只要想改一點東西,就得全部重來。我寫了一個簡易的 JavaScript 小工具,可以把不同格式的範例 HID 描述符轉換成對等的 shell 指令,來產生對應的檔案:

我的 HID 格式化工具的螢幕截圖

我為了幫自己格式化 HID 描述符而寫的簡易 JavaScript 小工具

接著,我在自己的家目錄下寫了一些小小的工具腳本。這些腳本簡單到平常根本不值得獨立成檔:

#!/bin/bash

set -x

sudo journalctl -u init-usb-gadget

這些看似笨拙的小工具幫了我兩個忙。第一,當我連續幾天撞牆時,完成這些小工具至少給了我一點前進的成就感。即使 HID 描述符還沒搞定,我至少產出了一些能如預期運作的程式碼。其次,它們降低了我的認知負擔,讓我能把更多心力專注於核心問題上。與其回想查看 systemd 紀錄的語法,我只要輸入 ~/show-systemd-log 就好。

後來發現,我大多數的問題根本不在 HID 描述符本身,而是用來產生檔案的 shell 指令。一旦我把其他繁瑣的雜事從腦中清掉,我才想到應該去驗證磁碟上的檔案是否符合預期。結果並不符合。一旦發現這點,很快就做出了可用的滑鼠描述符。

使用 TinyPilot 在遠端筆電上模擬滑鼠與鍵盤操作

舊有專案

以下是一些我仍在維護、但已非開發重心的專案的簡短近況:

Is It Keto

指標2020 年 7 月2020 年 8 月變化
不重複訪客48,23149,981+1,750 (+4%)
總瀏覽數118,980125,599+6,619 (+6%)
網域評分 (Ahrefs)8.09.0+1.0 (+12%)
AdSense 收益$208.86$202.46-$6.40 (-3%)
AdThrive 收益N/A$35.00N/A
Amazon 聯盟行銷收益$134.45$129.88-$4.57 (-3%)
其他聯盟行銷收益$26.60$118.88+$92.28 (+347%)
總營收$369.91$486.22+$116.31 (+31%)

Is It Keto 本月小幅成長,不過我一直試著把所有心力都放在 TinyPilot 上,因為那邊的投資報酬率高得多。

Is It Keto 本月唯一值得一提的事,就是我把展示型廣告從 AdSense 換到了 AdThrive。令人沮喪的是,轉換過程比我想像中花了更多心力。導入流程包含一大堆零碎步驟,他們會要我填一份表格、等一週、再要我填另一份表格,如此反覆。

終於到了要把網站切換到 AdThrive 廣告的時候,卻發現他們的 JavaScript 程式碼片段在單頁式應用程式上無法正常運作,導致在以 Vue 打造的 Is It Keto 上出現異常。我能理解他們很多客戶可能都是 WordPress 網站,但……拜託,都 2020 年了,單頁式應用程式怎麼還會是個難題。

這下又捅了馬蜂窩,因為 AdThrive 不斷傳給我一些拼湊、充滿問題的 JavaScript,要我試著讓 AdThrive 與我的網站相容。我跑了一次、回報說沒用,他們就再傳來一個以不同方式出錯的新版本。

最後,我說服他們把程式碼放在他們那邊代管,讓我不用再捲入除錯的循環。我不太喜歡他們在測試似乎還不夠充分的情況下就直接推上正式環境的做法,但目前我實在沒多餘心力去煩惱這件事。

Zestful

指標2020 年 7 月2020 年 8 月變化
不重複訪客440324-116 (-26%)
總瀏覽數1,247841-406 (-33%)
RapidAPI 收益$18.05$9.36-$8.69 (-48%)
總營收$18.05$9.36-$8.69 (-48%)

Zestful 這邊依然平靜,不過我正在評估一個新的 API 市集。我一直渴望找到目前平台 RapidAPI 的替代方案。一家名為 Servernope 的新公司邀請我加入他們的 API 平台。我告訴他們我沒時間設定,但如果他們願意代為建立 Zestful 的頁面也歡迎。於是他們就這麼做了

我還沒有完全被說服。我對 RapidAPI 最大的不滿之一,就是他們的分析數據無法以有用的方式呈現。Servernope 似乎也有類似的問題,只不過我在 Servernope 上還沒有任何付費用戶,所以很難比較。

總結

完成了什麼?

  • 調查 TinyPilot 的供電問題,並開始製造修正用的零件。
  • 為 TinyPilot 新增滑鼠支援。
  • 聘請了一位接案人員接手庫存管理與部分研究工作。
  • 建立 eBay 賣場以在國際上銷售 TinyPilot。
    • 我正在研究如何透過 Shopify 來處理這一切,但 eBay 是目前方便的過渡方案。
  • 發表了一篇新部落格文章:《我是如何向無良商家討回欠款》

經驗教訓

  • 更早與電機工程專家合作。
    • 回顧起來,我當時的使用方式已經遠遠超出 Raspberry Pi 的主流範疇,應該更早請專業的電機工程師來審視我的設計。
  • 當你在難題上卡關時,打造能省去除錯工作的工具。
    • 打造工具能帶來前進的動力,並讓你的心智能專注於問題的本質。

下個月的目標

  • 銷售 60 組 TinyPilot 套件與電源轉接頭。
  • 測試三個新的行銷管道。
  • 訪談七位 IT 專業人員,了解他們是否會在工作中使用 TinyPilot。

本文章由 muse-spark-1.2-contributor 進行翻譯

留言