2018年7月25日 星期三

Google Home mini 體驗心得以及 Google Assistant app 下載方式

Google Home mini with Chromecast

由於公司年初就決定要做音控,我自己也練習過 Alexa skill 的開發,但一直偷懶沒去測試 Actions on Google,只好懲罰一下,買個 Google home mini 了 (誤)

之前買過 AIY Voice Kit (反而比成品貴一倍),體驗音控服務的開發門檻,像是無間斷辨識關鍵字、語音辨識、個人助理服務,當時面對的僅只是一個維度(聲音),殊不知跟 Chromecast 搭配起來,就更威猛了,多了影像維度,光聲音+影像兩個維度就覺得開始看不到車尾燈了,更別說還搭配了 Search Service、個人資料、甚至 Youtube service 這些維度(常用的大概還會用 google calendar 等)

很佩服 Google Home mini 在收到 Hey, Google / OK, Google 時,會自動把 Chromecast 靜音,這 UX 已經加很多分了,更別說播放一些關鍵字時,Youtube 沒把握時,乾脆在 Chromecast 顯示選擇題!這大概就加分加到爆了 XD

令人想起 HTML 上一個 button click 設計了陰影、動畫、聲音來增加 UX 體驗,手機上則是震動等觸感,而此時看到 Voice Control 的 Google Home mini 跟 Chromecast 搭配,真的有那種 1+1 > 2 的體驗,分開使用是完全沒法體會的。

其他常見問題:

  • iPhone 要記得先切手機語系到英文,在用 Google Home app 設定 Google Home mini ,不然會一直踩到 kGSKErrorDomain 錯誤 0 或 -83902
  • iOS Device 想要下載 Google Assistant app,只需把現有的帳號切換到 US 地區即可n搜尋的到並可以下載
  • Android Device 想要下載 Google Assistant app 只能靠 VPN 切換過去下載,另外要留意 Android 裝置有 OS 版本要求跟記憶體要求(我就是卡在記憶體沒 1.4GB 啦)
  • 據說 Google Home 在 2018 年底要推出繁體辨識了?此時若要先買只能靠露天或蝦皮,有的會附服務費發票,價錢落在 1300-1500 之間(不含運),而 AIY Voice Kit 則是貴太多,包含 Pi 3 板子、Voice Kit 盒子等等,隨便弄弄都要破 2500 吧

2018年7月1日 星期日

[NAS] Private IP Server 與 Let's Encrypt Wildcard SSL 憑證 / HTTPS 服務

家裡擺了一台 Synology NAS 躲在防火牆後面,一直都是用 private ip 在服務的,以前 Let's Encrypt 有免費的 SSL 憑證服務,但驗證方式需要讓對方連到 server ,一直無法在 private ip server 啟用,可惜了點!然而,Let's Encrypt 開放 Wildcard SSL 憑證服務,採用 DNS TXT Record 驗證,這時就可以上場了!

我的 NAS 是幾年前買的 Synology DS216play ,他的 CPU 挺麻煩的,讓我額外安裝軟體都很不便利,原先想採用 certbot 來做事,安裝了 python3、pip 和 virtualenv 後,最後還是卡關,卡在安裝軟體還需要 compiler 來編譯,擺爛三個月後,我改用了 https://github.com/Neilpang/acme.sh

acme.sh 的使用很方便:

$ wget https://raw.githubusercontent.com/Neilpang/acme.sh/master/acme.sh
$ chmod a+x acme.sh
$ ~/acme.sh --issue -d *.changyy.org --dns --yes-I-know-dns-manual-mode-enough-go-ahead-please


如此後續再添加 TXT record 後,在執行

$ ~/acme.sh --renew -d *.changyy.org --dns --yes-I-know-dns-manual-mode-enough-go-ahead-please

就搞定了!剩下的步驟只是覆蓋憑證檔案跟重啟 web server :

$ grep certificate /etc/nginx/nginx.conf
    ssl_certificate           /usr/syno/etc/certificate/system/default/fullchain.pem;
    ssl_certificate_key       /usr/syno/etc/certificate/system/default/privkey.pem;
$ ls /usr/syno/etc/certificate/system/default/
cert.pem  chain.pem  fullchain.pem  privkey.pem


最後,我把 DNS 擺在 cloudflare 託管,還可以用 API 去處理 DNS TXT Record 更新,連續動作如下:

$ ls ~/.acme.sh/
$ wget https://raw.githubusercontent.com/Neilpang/acme.sh/master/dnsapi/dns_cf.sh -O ~/.acme.sh/dns_cf.sh
$ chmod 755 ~/.acme.sh/dns_cf.sh
$ CF_Key=YOUR_API_KEY_AT_PROFILE CF_Email=YOUR_CLOUDFLARE_EMAIL_ACCOUNT ~/acme.sh --issue --dns dns_cf -d *.changyy.org
$ sudo cp ~/.acme.sh/\*.changyy.org/\*.changyy.org.cer /usr/syno/etc/certificate/system/default/cert.pem
$ sudo cp ~/.acme.sh/\*.changyy.org/\*.changyy.org.key /usr/syno/etc/certificate/system/default/privkey.pem
$ sudo cp ~/.acme.sh/\*.changyy.org/ca.cer /usr/syno/etc/certificate/system/default/chain.pem
$ sudo cp ~/.acme.sh/\*.changyy.org/fullchain.cer /usr/syno/etc/certificate/system/default/fullchain.pem
$ sudo synoservicecfg --restart nginx


如此一來,就可以安排 crontab 用 root 每月跑一次了,另一個好處是可以在 NAS 執行完,再把憑證丟到一些 VPS 維運的機器上

# cat /root/lets-ssl-renew.sh
#!/bin/sh
CF_Key=YOUR_API_KEY_AT_PROFILE CF_Email=YOUR_CLOUDFLARE_EMAIL_ACCOUNT /root/acme.sh --issue --dns dns_cf -d *.changyy.org  && cp /root/.acme.sh/\*.changyy.org/\*.changyy.org.cer /usr/syno/etc/certificate/system/default/cert.pem && cp /root/.acme.sh/\*.changyy.org/\*.changyy.org.key /usr/syno/etc/certificate/system/default/privkey.pem && cp /root/.acme.sh/\*.changyy.org/ca.cer /usr/syno/etc/certificate/system/default/chain.pem && cp /root/.acme.sh/\*.changyy.org/fullchain.cer /usr/syno/etc/certificate/system/default/fullchain.pem && synoservicecfg --restart nginx && echo "SSL-Renew-Done" > /root/ssl-renew.log || echo "SSL-Renew-Failed" > /root/ssl-renew.log
echo date >> /root/ssl-renew.log

2018年6月27日 星期三

親愛的奇利,謝謝你來到我們家

20090124 - 撒嬌

幾天前在辦公室得知奇利過逝,遲疑著幾分鐘後,就請個特休搭高鐵返鄉。一路上淚也止不下來,沒想到早上加油時送的面紙派上了用場。和奇利的緣分近 13 年吧!在我們家處境最糟的時候,加入的成員。

還記得剛來我們家時,傲氣的奇利還不准人摸他的頭,每次要摸就立刻轉身,隨著彼此的生活步調同步著,我教會奇利如何上下樓梯,奇利甚至就學會了不在家裡亂尿尿,忍不住時就在廁所方便,實在貼心。

回顧起來,奇利過得也不錯,只有一開始一兩年住在籠子裡,後來家裡環境比較乾淨後,就不在需要籠子,可以隨處挑喜歡的地方睡覺,甚至跟我們一起吹冷氣,接著陪著我們到處輕旅行,追過每一年的燈會、國際上知名的黃色小鴨以及不少台灣老少咸宜的景點,或是來北部待過我租的每一間房間,這種情誼不是寵物兩字可道盡,只有家人可以詮釋情誼。

近一年奇利健康老化,來台北時,不知是不是長途關係,走著走著就倒下了!而後才知道,他的生命到了末曲,老化帶來的心臟病,心臟瓣膜脫垂會讓他喘,吸不到空氣,開始吃藥、開始緩慢的步調。

回顧起來,我們是不是用著藥物強制延續著緣分?安樂死真的是個課題。但人竟然可以這樣決定別人的生命?
定期進入全氧室能得到紓解、每天早晚的藥物控制可以穩定作息,卻都換不回那可以快樂奔跑的奇利。

喜歡那爽朗的笑容,這樣癡癡的過生活。
非常感謝和奇利的緣分。

他可能只是你的一個寵物,但你卻是他的全部。
越多的互動,真的越難釋懷,只能朝著,奇利,你終於不用在吃藥了,可以開心自在的到處散步

謝謝你來到我們家


回矇一笑

2018年6月2日 星期六

[Python] Line Chatbot 開發筆記 - Echo Service @ Ubuntu 18.04

LineChatbot01

入口:https://developers.line.me/en/
文件:https://developers.line.me/en/docs/messaging-api/overview/

登入開發者後,先來個 Add new provider ,此例為 StudyLineChatbot ,接著選擇 Messaging API,填寫完基本資料後,再按幾下就創建了

其中,重要的資訊:
  • Channel secret
  • Channel access token (long-lived)
而 Channel access token (long-lived) 預設為空,要按個 Issue 來產生一組。

接著,Webhook URL 要填寫對接的 API 位置,該 API 必須提供 https 服務,另外再把 Auto-reply message 關閉。如此一來就完成 Line Chatbot 的設定。

接著談談 Webhook URL 的實作方式,此例用 Python 的 Flask 和 line-bot-sdk-python 完工,簡短程式 hello.py 如下(其實就是 https://github.com/line/line-bot-sdk-python README 範例程式,多增加一點輸出訊息):

from flask import Flask, request, abort

from linebot import (
    LineBotApi, WebhookHandler
)
from linebot.exceptions import (
    InvalidSignatureError
)
from linebot.models import (
    MessageEvent, TextMessage, TextSendMessage,
)

app = Flask(__name__)

line_bot_api = LineBotApi('YOUR_LINE_CHATBOT Channel access token (long-lived)')
handler = WebhookHandler('YOUR_LINE_CHATBOT Channel secret')

@app.route("/", methods=['POST'])
def callback():
    print("[INFO] Get request:")
    print(request.__dict__)
    print()

    # get X-Line-Signature header value
    signature = request.headers['X-Line-Signature']
    # get request body as text
    body = request.get_data(as_text=True)
    print("[INFO] Request Body: ---\n"+body+"\n---\n")

    # handle webhook body
    try:
        events = handler.handle(body, signature)
    except InvalidSignatureError:
        abort(400)

    print("[INFO] Handle Events \n")
    for event in events:
        if event is None:
            continue
        if not isinstance(event, MessageEvent):
            continue
        if not isinstance(event.message, TextMessage):
            continue
        line_bot_api.reply_message(
            event.reply_token,
            TextSendMessage(text=event.message.text)
        )
    return '{}'

@handler.add(MessageEvent, message=TextMessage)
def handle_message(event):
    line_bot_api.reply_message(
        event.reply_token,
        TextSendMessage(text=event.message.text)
    )

if __name__ == "__main__":
    app.run(host='0.0.0.0', port=8080)


然後,選擇在 Ubuntu server 上,透過 nginx 跟 uWSGI 設定。

Nginx:

location / {

try_files $uri @chatbot;
}

location @chatbot {
include uwsgi_params;
uwsgi_pass unix:/tmp/chatbot-uwsgi.sock;
}


uWSGI:

$ vim /etc/systemd/system/chatbot.uwsgi.service
[Unit]
Description=uWSGI Emperor
After=syslog.target

[Service]
ExecStart=/usr/local/bin/uwsgi --ini /path/project/chatbot-uwsgi.ini
RuntimeDirectory=uwsgi
Restart=always
KillSignal=SIGQUIT
Type=notify
StandardError=syslog
NotifyAccess=all

[Install]
WantedBy=multi-user.target

$ vim /path/project/chatbot-uwsgi.ini
[uwsgi]
base = /path/project/

app = hello
module = %(app)
home = %(base)/venv
pythonpath = %(base)
callable = app

socket = /tmp/%n.sock
chmod-socket = 666
logto = /var/log/uwsgi/%n.log


後續都統一靠以下招數更新:

$ sudo systemctl restart chatbot.uwsgi

以及 hello.py 輸出的資料和運行結果,都可以在 /var/log/uwsgi/chatbot-uwsgi.log 找到:

[INFO] Get request:
{'environ': {'QUERY_STRING': '', 'REQUEST_METHOD': 'POST', 'CONTENT_TYPE': 'application/json;charset=UTF-8', 'CONTENT_LENGTH': '237', 'REQUEST_URI': '/', 'PATH_INFO': '/', 'DOCUMENT_ROOT': '/etc/nginx/html', 'SERVER_PROTOCOL': 'HTTP/1.1', 'REQUEST_SCHEME': 'https', 'HTTPS': 'on', 'REMOTE_ADDR': '###.###.###.###', 'REMOTE_PORT': '34994', 'SERVER_PORT': '443', 'SERVER_NAME': 'chatbot.example.com', 'HTTP_X_LINE_SIGNATURE': '#####################', 'HTTP_CONTENT_TYPE': 'application/json;charset=UTF-8', 'HTTP_CONTENT_LENGTH': '237', 'HTTP_HOST': 'chatbot.example.com', 'HTTP_ACCEPT': '*/*', 'HTTP_USER_AGENT': 'LineBotWebhook/1.0', 'wsgi.input': <uwsgi._Input object at 0x7f0d8a4477c8>, 'wsgi.file_wrapper': <built-in function uwsgi_sendfile>, 'wsgi.version': (1, 0), 'wsgi.errors': <_io.TextIOWrapper name=2 mode='w' encoding='UTF-8'>, 'wsgi.run_once': False, 'wsgi.multithread': False, 'wsgi.multiprocess': False, 'wsgi.url_scheme': 'https', 'uwsgi.version': b'2.0.17', 'uwsgi.node': b'###########', 'werkzeug.request': <Request 'https://chatbot.example.com/' [POST]>}, 'shallow': False, 'view_args': {}, 'url_rule': <Rule '/' (POST, OPTIONS) -> callback>, 'url': 'https://chatbot.example.com/'}

[INFO] Request Body: ---
{"events":[{"type":"message","replyToken":"###############","source":{"userId":"##############","type":"user"},"timestamp":###########,"message":{"type":"text","id":"###########","text":"HelloWorld"}}]}
---

2018年5月14日 星期一

iOS App 開發筆記 - 關於 case sensitive 檔案系統與 CocoaPods 和 Ruby/Gems 的處理

某天重灌 macOS 時,把主硬碟弄成 case sensitive ,想說自己都很熟悉 linux server 的行為,殊不知替自己埋了很多坑。首先,如果用 CocoaPods 維護套件時,別人的程式碼不見得會依照此規範,同理在 Ruby/Gems 更是如此。

解法?先切一個 partition 不區分大小寫吧!在把 Xcode build code 的暫存區也移至該 partition 來解!

未來還是推薦用 case insensitive 的 partition 吧 Orz 想搞龜毛還是用另一區,千萬別要跟青春過意不去啊。

將 Xcode build 臨時區移走:

$ mv ~/Library/Developer/Xcode/DerivedData/ ~/Library/Developer/Xcode/DerivedData-bak/
$ mkdir /Volumes/Data/XcodeDerivedData
$ ln -s /Volumes/Data/XcodeDerivedData ~/Library/Developer/Xcode/DerivedData


如此一來,關於 Xcode 與 CocoaPods 的大小寫問題應當可以解了,但...如果 build code 過程還有搭配 ruby script 做事,很抱歉,請自己改 ruby 了 Orz 例如:

require 'CFPropertyList'

很抱歉,在 Ruby 的 CFPropertyList 套件中,他實際是小寫的 Orz 請看:

https://github.com/ckruse/CFPropertyList/tree/master/lib

最重要的,這類還可能搞壞 gem 的管理狀態,這時解法就有點痛苦了,絕對不是三兩句就解掉的。