2016年3月31日 星期四

Raspberry Pi 1B/1B+/2B/3B 市調與評估

這幾天想要再買一塊樹苺派板子作為自走車的主控板, 上網發現目前樹苺派已推出新款的 Pi 3B, 硬體規格大躍進, 但價格卻維持美金 35 元 (以 32 元匯率算, 約 1120 元), 台灣樹苺派官網目前賣 1600 元 :

台灣樹莓派 : Raspberry Pi 3 Model B (現貨) $1600

但在露天最便宜 $1385 可以買到 :

# 樹莓派Pi3 B款 Raspberry Pi3 ModelB (英國製) $1385
# 【莓亞科技】樹莓派Raspberry Pi 3 B(含稅現貨NT$1388)
# (微控制器科技) <現貨> 英國製 Raspberry Pi 3 B (送初學者電子書) $1395
# 微控制器科技 英國原裝 Raspberry Pi 3 B 樹莓派 (含高透明外殼 高階USB線 散熱片) $1550

樹苺派 3 最大賣點是 CPU 升級為 64 位元 ARMv8 架構, 以及內建 WiFi 與藍芽無線網路, 這樣就不需要再買一顆無線網卡 (約 200~400 元) 與藍芽收發器了 (約 100~200 元). 如果把這兩項成本 (300~600) 加進去, 其實就比買前一代的 Pi 2 要划算了 (目前價位約 1100~1200) :

# 清庫存 特價中 樹莓派 2 RS 英國原裝進口 Raspberry Pi 2 B (送散熱片 x2 初學者電子書 ) $1100

樹莓派自 2012 年面世後的規格差異如下, 參考 :

WiKi : 樹莓派
# WiKi : Raspberry Pi 

Pi 1B 主要規格 : (1B 於 2012-02-29 發布)
  1. CPU : Broadcomm BCM2835 (700MHz)
  2. DRAM : 512MB
  3. USB*2
  4. SD 擴充卡槽
  5. HDMI 1.3/1.4, RCA
Pi 1B+ 與 1B 的主要差異 : (B+ 於 2014-07-14 發布)
  1. 記憶卡槽從 SD 改為 Micro SD
  2. USB 槽從 2 組增加為 4 組
  3. GPIO 腳數從 26 增加為 40 腳 (前 26 Pin 相容)
  4. RCA 與耳機孔整合為一個複合式插孔
  5. 電源模組從線性改為交換式, 省電 20% 以上 (減少 0.5W~1W)
  6. USB 可熱插拔, 不會再重開機
Pi 2B 與 B+ 主要差異 : (2B 於 2015-02 發布)
  1. CPU 改為 900MHz 四核 Broadcomm BCM2836 (ARMv7 32 bits)
  2. DRAM 由 512M 增加為 1G
  3. 可執行 Windows 10 與 Ubuntu
Pi 3B 與 2B 主要差異 : (3B 於 2016-02 發布)
  1. CPU 改為 1.2GHz 四核 Broadcomm BCM2837 (ARMv8 64 bits)
  2. 增加 802.11n 無線網路
  3. 增加低功率 BLE 藍芽 4.1
從規格來看, Pi 2B 與 3B 時脈高, 功耗當然也比較高, 參考這兩篇 :

Raspberry Pi 3 vs Pi 2 power consumption and heat dissipation
How Much Less Power does the Raspberry Pi B+ use than the old model B?

Source : StackExchange

可見, Type B 系列中最省電的是 1B+, 無載時功耗差不多, 但有載時比 Pi 2B 省電 17%, 比 Pi 3B 省電 47% (省快一半了)! 畢竟 Pi 3B 除了時脈高, 又內建藍芽與 WiFi, 當然比較吃電. 如果用來作為電視機上盒播放影片, 當然要用最新的 Pi 3 啦! 但若不太要求效能 (例如控制機器人或自走車等), 其實 B+ 就很夠用了, 參考 :

Raspberry Pi新板子Model B+概略介紹與開箱

我找到下面這個 900 元的 B+ :

# 鎰盛(庫存店-Raspberry Pi Model B+ (英國原裝 樹莓派)1pcs + case X1pcs"免運費 $900

因為手上還有兩顆之前多買的 USB 網卡可以給這塊 B+ 用, 但如果要放在鄉下當 Home Security Gateway 主機, 就會考慮買內建 WiFi 的 Pi 3B 了.

2016-04-07 補充 :

其實若只是做智慧小車控制板, A+ 也許更適合, 雖然記憶體只有一半 ( 256MB), 應該還是綽綽有餘啦! 由上圖可知 A+ 的耗電量只有 B+ 的一半 (有載時比 Zero 還低哩!), 而大小只有 B+ 的 3/4 而已, 唯一的不方便是只有一個 USB 埠, 若要接 WiFi 網卡與無線小鍵盤滑鼠組就必須使用 USB 集線器. 參考 :

# Raspberry Pi推出新版本Model A+,新低價約台幣600元

下面這篇文章中的影片有展示用 A+ 製作的智慧小車 :

# Raspberry Pi A+ System Specs

目前露天找到最便宜的是 $780 元一塊 :

# [原廠] Raspberry Pi A+ / 另有 B+ / 樹莓派 / 歡迎散戶經銷商 / 經銷價請私訊 $780 (宅配/快遞 0元)

A+ 只有一個 USB 是比較麻煩的問題, 因為要灌 Rasbian 與做系統設定等需要滑鼠鍵盤組, 若要設定 wifi 網卡就至少需要兩個 USB. 下面這篇文章提出一個方法, 如果已經有 B/B+/2B/3B 等多 USB 槽電路板者, 可以先在 B 中灌好 Rasbian 與設定好網卡, 再移到 A+ 中使用 :

# How to connect to your Raspberry Pi Model A / A+ (A Plus) - headless configuration

另外下面這篇則認為 A+ 的 US$20 的價格很適合做專案實驗 :

# Tinkering with the Raspberry Pi A+

此文中提到 Google 為小朋友及初學者設計的 Coder 專案, 很有創意.

其他參考 :

# 台灣 IOT 樹莓派 2 英國歐諾時 RS 原裝進口 Raspberry Pi 2 B A7四核1G記憶體,支持Win10


2016年3月30日 星期三

九美元的單板電腦 C.H.I.P.

今天在露天看到 Orange Pi 這款與 Raspberry Pi 功能類似的單板微電腦, 價格大約是樹苺派的一半多一點而已, 這是中國一家廠商推出的產品, 價格上非常有競爭力 :

# Orange Pi PC香橙派 (raspberry pi 3 banana pi 網路電視盒 可參考) $790
# orange pi pc 四核心 a7 超越 raspberry pi 3 樹莓派 banana pi $790

其官網為 :

http://www.orangepi.org/index.html

此板比最新的 Raspberry Pi 3 勝出的地方是 :
  1. 內建紅外線接收
  2. 具有 USB OTG 接口
  3. 具有電源開關
  4. 具有 Debug UART 
但是我覺得這些優點都不是甚麼大不了的優勢, 最主要還是價格吸引人.

更吸引人的是下面這個在募資網站上打響名號的 C.H.I.P. 專案, 其特點是內建 wifi 與藍芽, 而且預售價格只要 9 美元, 預定 2016 年 7 月出貨 (上回好像是說 6 月喔).



此板使用成本較低的全志 1GHz CPU, 512M DRAM, 4G 內建記憶體 (似乎跟 Raspberry Pi Type B 差不多), 但不支援擴充卡. 目前可在官網預購, 每人最多可訂 5 片, 運送至台灣最低運費 $6.22 美元 (2 片以內), 最多 $11 美元 (買 5 片). 





我計算了一下, 單買一片最不划算, 含運費要 502 元; 買兩片運費跟買單片相同, 但平均只要 400 元不到; 買 3 片以上平均每片價格就差異不大了. 

1片: $9+$6.22=$15.22*33=502 元/片
2片: $18+$6.22=$24.22*33=799/2=399 元/片
3片: $27+$7.15=$34.15*33=1127/3=375 元/片
4片: $36+$9=$45*33=1485/4=371 元/片
5片: $45+$11=$56*33=1848/3=369 元/片


參考 :

http://getchip.com/pages/chip
比 Raspberry Pi 還便宜:這片小晶片 CHIP 讓你擁有美金 $9 的個人電腦
Raspberry Pi競爭對手CHIP電腦即將問世,只要9美元!
9美元的C.H.I.P.真能與Raspberry Pi比拼?
# 9美元電腦?! CHIP還要更CHEAP!
# 甚麼?9 美元就能買到一台電腦

樹莓派的 Wifi 設定 : 使用迅捷 FW150US

最近跟市圖預約的 "Raspberry Pi 最佳入門與實戰應用" (碁峰, 柯博文) 到館, 拿回來之後翻閱覺得這本確實是實戰之書, 裡面每個課題都是很實用的範例, 而且書附光碟有教學影片, 非常適合有心者自學.

Source : 博客來

這又燃起我心中的 maker 烈火. 找出塵封已久的 Raspberry Pi Type B 主板, 將 SD 卡中當初賣家幫我灌的 NOOBS 格式化掉, 然後去官網下載最新版的 NOOBS v1.9.0 (Kernel 4.1.19 #850, 2016-03-18), 將 ZIP 檔解開後直接在 PC 中複製進去 SD 卡即可, 不用像以前那樣需用 Win32 Disk Imager 去燒錄. 參考 :

樹苺派的 SD 卡與作業系統安裝

作業系統裝入 SD 卡後放進樹莓派裡, 開機即會自動進行安裝. 無線上網部分我按照這本書的 3-3 節說明, 先關電源插入以前那顆可用的 Edimax N8508 無線網卡再重開機, 結果竟然一直都只顯示 IPv6 位址, 沒顯示 IPv4 位址. 第二天早上想要繼續時發現網卡非常燙, 於是關機拔出, 發現溫度高到會燙人! 我想這顆應該毀了. 只好拿出迅捷 FW150US 這顆 CP 值頗高的中國製網卡來用, 但是這顆樹莓派沒支援, 必須自己安裝驅動程式, 參考葉難這篇 :

Raspberry Pi:USB無線網卡迅捷FW150US

但是文中所說的驅動程式已無法下載, 我又找到下面這篇 :

# Raspberry Pi無線網卡設定(rtl8188eu)

但是同樣不能用, 因為新版的 NOOBS 的 Kernel 是 4.1.19 版. 後來終於在下面這篇找到明確的解決辦法 :

# Raspberry Pi – Driver for RTL8179/8188EU

此文中與 Linux 4.1.19 版相應的 8188eu 驅動程式是 20160305 發布的這一版, 只要以下面這五個指令即可順利安裝驅動程式 :

wget https://dl.dropboxusercontent.com/u/80256631/8188eu-20160305.tar.gz

tar -zxvf 8188eu-20160305.tar.gz

sudo install -p -m 644 8188eu.ko /lib/modules/$(uname -r)/kernel/drivers/net/wireless

sudo insmod /lib/modules/$(uname -r)/kernel/drivers/net/wireless/8188eu.ko

sudo depmod -a

https://dl.dropboxusercontent.com/u/80256631/8188eu-20160305.tar.gz

我先用乙太網上網, 依序執行上面五個指令來下載並安裝驅動, 執行紀錄如下 :

login as: pi
pi@192.168.2.104's password:   (預設是 raspberry)

The programs included with the Debian GNU/Linux system are free software;
the exact distribution terms for each program are described in the
individual files in /usr/share/doc/*/copyright.

Debian GNU/Linux comes with ABSOLUTELY NO WARRANTY, to the extent
permitted by applicable law.
Last login: Tue Mar 29 19:25:40 2016
pi@raspberrypi:~ $ ifconfig       (只看到乙太網卡的 192.168.2.104)
eth0      Link encap:Ethernet  HWaddr b8:27:eb:97:b6:52
          inet addr:192.168.2.104  Bcast:192.168.2.255  Mask:255.255.255.0
          inet6 addr: fe80::873a:49c1:5be0:24db/64 Scope:Link
          UP BROADCAST RUNNING MULTICAST  MTU:1500  Metric:1
          RX packets:77 errors:0 dropped:0 overruns:0 frame:0
          TX packets:95 errors:0 dropped:0 overruns:0 carrier:0
          collisions:0 txqueuelen:1000
          RX bytes:7749 (7.5 KiB)  TX bytes:15196 (14.8 KiB)

lo        Link encap:Local Loopback
          inet addr:127.0.0.1  Mask:255.0.0.0
          inet6 addr: ::1/128 Scope:Host
          UP LOOPBACK RUNNING  MTU:65536  Metric:1
          RX packets:200 errors:0 dropped:0 overruns:0 frame:0
          TX packets:200 errors:0 dropped:0 overruns:0 carrier:0
          collisions:0 txqueuelen:0
          RX bytes:16656 (16.2 KiB)  TX bytes:16656 (16.2 KiB)

pi@raspberrypi:~ $ lsusb     (查看 USB 裝置 : 還讀不到網卡)
Bus 001 Device 004: ID 0c45:7403 Microdia Foot Switch
Bus 001 Device 003: ID 0424:ec00 Standard Microsystems Corp. SMSC9512/9514 Fast                                                                              Ethernet Adapter
Bus 001 Device 002: ID 0424:9512 Standard Microsystems Corp. LAN9500 Ethernet 10                                                                             /100 Adapter / SMSC9512/9514 Hub
Bus 001 Device 001: ID 1d6b:0002 Linux Foundation 2.0 root hub
pi@raspberrypi:~ $ uname -a   (顯示 Linux 版本)
Linux raspberrypi 4.1.19+ #858 Tue Mar 15 15:52:03 GMT 2016 armv6l GNU/Linux
pi@raspberrypi:~ $ wget https://dl.dropboxusercontent.com/u/80256631/8188eu-20160305.tar.gz           (下載驅動程式)                                                       --2016-03-29 19:44:43--  https://dl.dropboxusercontent.com/u/80256631/8188eu-20160305.tar.gz
Resolving dl.dropboxusercontent.com (dl.dropboxusercontent.com)... 108.160.172.5
Connecting to dl.dropboxusercontent.com (dl.dropboxusercontent.com)|108.160.172.5|:443... connected.
HTTP request sent, awaiting response... 200 OK
Length: 411563 (402K) [application/octet-stream]
Saving to: ‘8188eu-20160305.tar.gz’

8188eu-20160305.tar.gz                  100%[==============================================================================>] 401.92K   538KB/s   in 0.7s

2016-03-29 19:44:56 (538 KB/s) - ‘8188eu-20160305.tar.gz’ saved [411563/411563]

pi@raspberrypi:~ $ tar -zxvf 8188eu-20160305.tar.gz  (解壓縮)
8188eu.ko
8188eu.conf
install.sh
pi@raspberrypi:~ $ sudo install -p -m 644 8188eu.ko /lib/modules/$(uname -r)/kernel/drivers/net/wireless   (安裝驅動程式)
pi@raspberrypi:~ $ sudo insmod /lib/modules/$(uname -r)/kernel/drivers/net/wireless/8188eu.ko    (安裝驅動程式)
pi@raspberrypi:~ $ sudo depmod -a
pi@raspberrypi:~ $

哈哈! 沒有錯誤訊息, 這樣驅動程式就安裝成功了! 接下來必須修改網路介面設定檔 /etc/network/interfaces, 加入無線分享器的登入資訊, 用 nano 編輯此檔 :

pi@raspberrypi:~ $ sudo /etc/network/interfaces

編輯內容如下, 重點是 SSID 與連線密碼 :

auto lo
iface lo inet loopback

iface eth0 inet manual

allow-hotplug wlan0
auto wlan0

iface wlan0 inet dhcp
wpa-ssid "無線分享器 SSID"
wpa-psk "無線分享器連線密碼"

編好後先按 Ctrl+W 儲存檔案, 按 Enter 後再按 Ctrl+O 跳出 nano 編輯器, 再下 sudo poweroff 關機後重開機登入系統 :

login as: pi
pi@192.168.2.104's password:

The programs included with the Debian GNU/Linux system are free software;
the exact distribution terms for each program are described in the
individual files in /usr/share/doc/*/copyright.

Debian GNU/Linux comes with ABSOLUTELY NO WARRANTY, to the extent
permitted by applicable law.
Last login: Tue Mar 29 19:48:33 2016
pi@raspberrypi:~ $ lsusb    (顯示 USB 裝置)
Bus 001 Device 005: ID 0bda:8179 Realtek Semiconductor Corp.   (有讀到無線網卡了)
Bus 001 Device 004: ID 0c45:7403 Microdia Foot Switch
Bus 001 Device 003: ID 0424:ec00 Standard Microsystems Corp. SMSC9512/9514 Fast                                                                              Ethernet Adapter
Bus 001 Device 002: ID 0424:9512 Standard Microsystems Corp. LAN9500 Ethernet 10                                                                             /100 Adapter / SMSC9512/9514 Hub
Bus 001 Device 001: ID 1d6b:0002 Linux Foundation 2.0 root hub
pi@raspberrypi:~ $ ifconfig
eth0      Link encap:Ethernet  HWaddr b8:27:eb:97:b6:52
          inet addr:192.168.2.104  Bcast:192.168.2.255  Mask:255.255.255.0
          inet6 addr: fe80::873a:49c1:5be0:24db/64 Scope:Link
          UP BROADCAST RUNNING MULTICAST  MTU:1500  Metric:1
          RX packets:117 errors:0 dropped:0 overruns:0 frame:0
          TX packets:123 errors:0 dropped:0 overruns:0 carrier:0
          collisions:0 txqueuelen:1000
          RX bytes:11528 (11.2 KiB)  TX bytes:19569 (19.1 KiB)

lo        Link encap:Local Loopback
          inet addr:127.0.0.1  Mask:255.0.0.0
          inet6 addr: ::1/128 Scope:Host
          UP LOOPBACK RUNNING  MTU:65536  Metric:1
          RX packets:0 errors:0 dropped:0 overruns:0 frame:0
          TX packets:0 errors:0 dropped:0 overruns:0 carrier:0
          collisions:0 txqueuelen:0
          RX bytes:0 (0.0 B)  TX bytes:0 (0.0 B)

wlan0     Link encap:Ethernet  HWaddr 08:57:00:16:88:0c
          inet addr:192.168.2.105  Bcast:192.168.2.255  Mask:255.255.255.0
          inet6 addr: fe80::a57:ff:fe16:880c/64 Scope:Link
          UP BROADCAST RUNNING MULTICAST  MTU:1500  Metric:1
          RX packets:117 errors:0 dropped:57 overruns:0 frame:0
          TX packets:104 errors:0 dropped:1 overruns:0 carrier:0
          collisions:0 txqueuelen:1000
          RX bytes:23031 (22.4 KiB)  TX bytes:17043 (16.6 KiB)

pi@raspberrypi:~ $

可見有讀到無線網卡了, 其 IP 是 912.168.2.105, 在同網域另外一台電腦 ping 這個無線網卡所獲得的 IP 有回應了 :

C:\Users\petertw89>ping 192.168.2.105

Ping 192.168.2.105 (使用 32 位元組的資料):
回覆自 192.168.2.105: 位元組=32 時間=49ms TTL=64
回覆自 192.168.2.105: 位元組=32 時間=1ms TTL=64
回覆自 192.168.2.105: 位元組=32 時間=1ms TTL=64
回覆自 192.168.2.105: 位元組=32 時間=1ms TTL=64

192.168.2.105 的 Ping 統計資料:
    封包: 已傳送 = 4,已收到 = 4, 已遺失 = 0 (0% 遺失),
大約的來回時間 (毫秒):
    最小值 = 1ms,最大值 = 49ms,平均 = 13ms

如果於同網域 PC 再開啟一個 putty 視窗, 連線此無線網卡的 IP, 同樣可以連線 :

login as: pi
pi@192.168.2.105's password:

The programs included with the Debian GNU/Linux system are free software;
the exact distribution terms for each program are described in the
individual files in /usr/share/doc/*/copyright.

Debian GNU/Linux comes with ABSOLUTELY NO WARRANTY, to the extent
permitted by applicable law.
Last login: Tue Mar 29 19:50:57 2016 from 192.168.2.110
pi@raspberrypi:~ $

使用 sudo iwlist wlan0 scan 指令可以掃描附近有哪些無線基地台 :

pi@raspberrypi:~ $ sudo iwlist wlan0 scan
wlan0     Scan completed :
          Cell 01 - Address: 80:1F:02:2D:5A:XX
                    ESSID:"EDIMAX-tony"
                    Protocol:IEEE 802.11bgn
                    Mode:Master
                    Frequency:2.462 GHz (Channel 11)
                    Encryption key:on
                    Bit Rates:108 Mb/s
                    Extra:wpa_ie=dd160050f20101000050f20201000050f20201000050f202
                    IE: WPA Version 1
                        Group Cipher : TKIP
                        Pairwise Ciphers (1) : TKIP
                        Authentication Suites (1) : PSK
                    Extra:rsn_ie=30180100000fac020200000fac02000fac040100000fac020000
                    IE: IEEE 802.11i/WPA2 Version 1
                        Group Cipher : TKIP
                        Pairwise Ciphers (2) : TKIP CCMP
                        Authentication Suites (1) : PSK
                    IE: Unknown: DD7F0050F204104A0001101044000101103B0001031047001063041253101920061228801F022D5A9E102100001023000752544C387878781024000D45562D323030392D30322D30361042000F3132333435363738393031323334371054000800060050F20400011011000F576972656C65737320526F75746572100800020086
                    Quality=2/100  Signal level=34/100
          Cell 02 - Address: E8:DE:27:60:1A:XX
                    ESSID:"TP-LINK_601A04"
                    Protocol:IEEE 802.11bgn
                    Mode:Master
                    Frequency:2.412 GHz (Channel 1)
                    Encryption key:on
                    Bit Rates:300 Mb/s
                    Extra:wpa_ie=dd160050f20101000050f20401000050f20401000050f202
                    IE: WPA Version 1
                        Group Cipher : CCMP
                        Pairwise Ciphers (1) : CCMP
                        Authentication Suites (1) : PSK
                    Extra:rsn_ie=30140100000fac040100000fac040100000fac020000
                    IE: IEEE 802.11i/WPA2 Version 1
                        Group Cipher : CCMP
                        Pairwise Ciphers (1) : CCMP
                        Authentication Suites (1) : PSK
                    IE: Unknown: DDA50050F204104A0001101044000102103B00010310470010000102030405060708090A0B0C0D0E0F1021000754502D4C494E4B10230014544C2D57523934304E2F544C2D57523934314E4410240003352E3010420003312E301054000800060050F20400011011001A576972656C65737320526F7574657220544C2D57523934314E44100800020086103C000101104900140024E26002000101600000020001600100020001
                    Quality:0  Signal level:0  Noise level:0
          Cell 03 - Address: 74:DA:38:15:16:XX
                    ESSID:"edimax.setup"
                    Protocol:IEEE 802.11bg
                    Mode:Master
                    Frequency:2.457 GHz (Channel 10)
                    Encryption key:on
                    Bit Rates:54 Mb/s
                    Extra:wpa_ie=dd160050f20101000050f20201000050f20201000050f202
                    IE: WPA Version 1
                        Group Cipher : TKIP
                        Pairwise Ciphers (1) : TKIP
                        Authentication Suites (1) : PSK
                    Quality:0  Signal level:0  Noise level:0
          Cell 04 - Address: 9C:D6:43:65:28:XX
                    ESSID:"CHT Wi-Fi(HiNet)"
                    Protocol:IEEE 802.11bgn
                    Mode:Master
                    Frequency:2.457 GHz (Channel 10)
                    Encryption key:off
                    Bit Rates:144 Mb/s
                    Quality:0  Signal level:0  Noise level:0


這樣就完成迅捷 FW150US 網卡驅動程式安裝, 可以拔掉乙太網線了.

參考 :

# [基礎] 命令列設置無線網路
# Raspberry Pi 的基礎 - 使用 Wi-Fi 無線網卡連上網路 
# Problem wpa_supplicant rapsberry pi 2
使用no-ip申請域名讓浮動IP也能架設網站
1.4 有線 或 無線 的DHCP 設定或固定IP設定 (學習樹苺派)
# 在 Windows 設置 Raspberry Pi (樹莓派) 遠端編輯環境
# 使用no-ip申請域名讓浮動IP也能架設網站
# 如何在無線路由器上設定 DDNS(DynDNS)
# 社區網路 DDNS 無解問題
# NOIP申請完,架設FTP


2016年3月29日 星期二

2016 年第 13 周記事

時序已過春分, 來到三月底白天也變長些了, 週六 3/19 回鄉下時走里港支線比較快, 17:17 出發回到家剛好六點天還很亮. 計算里程從高雄家到鄉下家僅 40 公里, 時間僅花了 43 分鐘, 只是里港沿荖濃溪河堤有一段路是混泥土路, 震動彈跳較大. 3/20 回高雄時走市區上國十, 里程為 48 公里, 多了 8 公里卻花了快 1 小時.

週日下午拿鐮刀將後院的過貓全數砍除, 一株不剩, 連根也刨掉, 驚覺原來蕨類的根在土下盤根錯結, 土下兩公分幾乎全被佔據, 難怪魚腥草等藥草被驅逐殆盡. 這些過貓盤據此處久矣, 最早應該是媽為了摘取嫩芽當菜而移植, 結果越長越密, 變成藏蚊子之處, 每到傍晚總是轟轟響. 剪除之後清爽多了. 枯葉讓太陽曬個一周後再燒掉, 順便烤地瓜. 

週日傍晚拿 4/9 日媽添罐所需準備之物品清單去給有能伯, 委其幫我準備五牲, 發粄, 紅龜粄, 新丁粄這四項. 家祠修繕工程也在周六下午起工, 需施作大理石桌面與引道等. 順道去農業資材行買 250 公升儲水桶, 打算在雨季來臨前完成雨水回收與自動澆水系統, 免得爸要提水灌溉大門的七里香與花草. 想說貨比三家不吃虧, 先去小漢五金看, 結果同樣產品資材行賣 980, 五金行竟然賣 1300! 實在賺太多了吧! 當下即轉往資材行先買一個, 我打算放在三樓頂, 將鐵皮屋集水槽原本的排水管導入儲水桶內, 這樣水壓夠大, 不需要幫浦來打水. 下周要先丈量所需水管長度, 再去買 3 分的 PVC 管, 小漢一支四公尺賣 32 元, 估計需要 40 公尺. 

週六下午去母校圖書館將校友證交給館員建檔, 之後才能借書. 畢業三十年後首次走進母校圖書館, 記得入學時舊圖書館是約三層樓高的環形建築. 後來才改建為十幾層的大樓. 架上新舊書並陳, 甚至我們當年的老書都還在哩! 下樓來陽光仍很炙烈, 校門口的校訓碑在藍天下顯得非常亮麗呢!



2016年3月25日 星期五

Appfog 主機升版為 v2 後 twstockbot 改版問題

前陣子 Appfog 通知要升版為 v2, 我以為它要藉此把早期加入的免費會員掃地出門, 所以就趕緊下載程式與資料庫備份, 打算轉移陣地. 昨天連線網站, 出現如下無法連線畫面, 更加肯定真的不能用了 :

# Appfog v1 退場了


所以就著手把相對應的 GAE 應用服務修改為去 trigger 英國 Hostinger 主機. 但回頭想說難道原來的Appfog 登入網頁真的進不去了嗎? 試試居然還可以, 但進去後發現兩個應用程式狀態為 stopped, 我猜它只是在升版時把全部應用程式都關閉, 升版後卻沒自動幫我啟動服務而已, 手動開啟後 cron jobs 就恢復運作了 :

# https://console.appfog.com/


真是太好了! 這樣我就不用另闢蹊徑, 可以繼續使用 Appfog 的穩定服務啦! 從上面的 cron_log 紀錄可知 Appfog 是在 3/22 日凌晨快兩點時關閉了我的應用程式.

不過前陣子我的 EasyuiCMS 架站機有改版, 除了全部改用 mysqli 函式庫 (因為 Hostinger 已不支援 mysql 函式庫), 也修改了訪客列表, 增加顯示訪客來自哪一個國家, 這部分需要在手動調整好資料表後再更新程式. 要修改的部分參考 :

從 IP 查來源國家 (二)
# EasyuiCMS 改版為 v2 (mysqli)

依照步驟下載最新的 ip2nation.zip 檔解壓縮後得到 ip2nation.sql, 然後進入 phpmyadmin 後台的 import 上傳此 sql 檔, 結果出現 "No data was received to import" 錯誤 :


仔細看原來是伺服器預設最多只能上傳 2014KB 檔案, 而解壓後的 ip2nation.sql 為 3883KB, 已經超過了. 解決辦法是上傳壓縮檔, 但檔名必須符合其規範, 亦即要以 .sql.zip 結尾. 所以將下載來的 ip2nation.zip 改為 ip2nation.sql.zip (351KB) 再上傳就可以了 :


左方可見已建立 ip2nation 與 ip2nationCountries 資料表了.

接下來是在 phpmyadmin 的建立 nation 這張資料表來達成中文化, 從下列網址下載 nation.sql 檔 :

下載國名簡碼與中文國名對照表 nation.sql

其內容如下 :

CREATE TABLE IF NOT EXISTS `nation` (
  `code` varchar(4) COLLATE utf8_unicode_ci NOT NULL,
  `name` varchar(255) COLLATE utf8_unicode_ci DEFAULT NULL,
  PRIMARY KEY (`code`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;

INSERT INTO `nation` (`code`, `name`) VALUES
('ad', '安道爾'),
('ae', '阿拉伯聯合大公國'),
('af', '阿富汗'),
('ag', '安地卡'),
('ai', '安圭拉島'),
.....

('bl', '聖巴泰勒米'),
('mf', '法屬聖馬丁'),
('sx', '荷屬聖馬丁'),
('ss', '南蘇丹'),
('um', '美國本土外小島嶼');

此檔案僅 7KB 而已, 不須壓縮, 直接 import 即可.

建立了這三張表單就可以更新應用程式了.

開啟 command 視窗, 切換到 twstockbot 目錄, 下這兩個指令就可更新檔案了 :

af login

af update twstockbot

但是上傳應用程式後瀏覽網頁卻顯示系統安裝畫面, 我原以為是 db.php 弄錯了別的主機, 更正之後還是一樣, 後來我試著改回匯入 mysql 函式庫後就都恢復正常, 原來 Appfog 不支援 mysqli! 看來需要維持兩個版本的 EasyuiCMS 了, 如果應用服務部署在 Hostinger 就要用 mysqli 版本.


2016年3月23日 星期三

辦校友證

昨天想到在母校當教授的老同學, 但不知其在何系, 就連上母校網站查詢, 原來老同學是研究資料探勘與機器學習, 嘿, 這我有興趣, 改天來去找他聊聊. 基於同樣對技術的熱愛, 當年剛出社會時, 他在宏碁工作常去當時的公司找我, 但他後來出國念書就沒見過面, 自林口一別至今 20 餘年矣.

回頭看到圖書館網站, 進去逛逛發現還蠻多書哩! 有些是市圖沒有的, 就打個電話去問校友可不可以借, 原來只要辦校友證即可. 經登錄資料上傳大頭照之後, 今天就收到掛號信寄來的校友證, 母校職員辦事效率真高啊! 打電話問了一下館員, 校友可借十本, 借期 15 天, 借書在 21:45 前, 還書在 22:00 前, 僅國定假日休假.

現在我手上已有三個母校的校友證了, 第三母校高師大之前常常去借書, 使用率最高, 磨損得最多. 而第二母校中山大學, 畢業典禮時發的校友證則是因為離家太遠幾乎沒用過. 第一母校離家還算近, 找時間來去看看有哪些寶可挖, 順便看看當年常窩的圖書館變啥樣了.


登昌恆 Nettv3 Dual

兩三年前在公司福利社網站有賣登昌恆的 Nettv3 mobile (第四台轉成網路訊號), 當時定價約 NT$3700, 雖然手上有五千元的福利券可用, 但躊躇再三還是沒買, 因為我每天都在忙著寫程式, 哪有空看第四台.

最近水某對這種產品很有興趣, 因為可在手機看鄉下第四台節目. 於是上露天搜尋, 找到這款二手的 Nettv3 dual, 這是比 mobile 還高一級的產品, 可讓兩人同時遠端連線, 而 mobile 只能一個連線 (人在國外也可以看). 此外, 此款可配合登昌恆另外一款 Nettv3 on TV 連接遠端電視機觀看.

剛好看到有一位買家在詢問價格, 賣家說兩款一起買算 3000, 我看買家遲未答覆, 而且往想要睡覺了, 就直接下標 (據賣家說我動作真快, 原先詢價的買家很錯愕) :

# ☆閃炫電腦☆登昌恆 UPMOST netTV3 Dual 網路電視盒 二人可同時登入觀看不同頻道

上週日 3/19 帶回鄉下安裝, 測試正常可用, 不過需要照使用手冊調整家中寬頻分享器設定才行, 主要是區域網路 DHCP 與防火牆的 DMZ 設定. 鄉下網路是中華 ADSL 5M, 使用信舟 EDIMAX BR-6228nS 無線路由器, 安裝過程如下 :

1. 接線 :

以所附網路線連接 ETHERNET 插槽與無線路由器的 LAN 插槽.
以所附 AV 端子線連接 AV-OUT 插槽與電視機的 AV-IN 插槽.
將第四台同軸電纜插入 ANTENNA-IN 插槽, 再把 連接 ANTENNA-OUT 到電視機的 ANT.

由於我以前有買一個 Splitter (分岐器), 因此我是直接從分岐器接到 ANTENNA-IN, 電視機仍然吃分岐器另外一個輸出.

2. 設定 DHCP 固定 IP :

用電腦連線寬頻分享器管理網頁, 我的 Edimax BR-6228nS 為 192.168.2.2
切到 "區域網路" 頁面, 勾選 "開啟固定 DHCP", 輸入 MAC 位址, 並於 "IP" 欄位輸入 DHCP 較後面位址, 我這裡選定 192.168.2.199, 按 "新增" 後再按 "套用" 鈕. 勾選底下的 "開啟已配置的 IP 使用列表", 此固定 DHCP 位址與所配對的 MAC 位址應該就會出現在列表中了.

固定 DHCP 的原因是每次 Nettv3 開機都可能從 DHCP 獲取不同 IP, 這樣防火牆的 DMZ 也要隨同修改為此 IP, 這樣很麻煩. 固定之後每次重開機時, 路由器就會固定指配此 IP 給 Nettv3 了.
指配最後面的 IP 是因為 DHCP 是從前面往後面指派 IP, 結果不知為啥, 水某手機一打開 Wifi 就會被指配跟 Nettv3 一樣的 IP, 導致衝突而無法連線. 指配最後面的 IP 就可以避免衝突了.


3. 設定防火牆 DMZ :

切換寬頻分享器管理頁面至防火牆, 預設已經開啟 :


切到 DMZ 分頁, 勾選 "啟用 DMZ", "公用 IP 位址" 欄位維持預設之 "動態 IP", 在 "IP 位址" 欄輸入上面所設定的固定 DHCP 位址 192.168.2.199, 按 "新增" 就可在下面 DMZ 列表看到此 IP, 再按 "套用" 即可 :


這樣 Nettv3 dual 就可以讓外面連線進來了.

4. 在電腦上安裝光碟裡的 Nettv-2012, 執行後會出現機上盒列表 :


點選下一步會進入頻道掃描 (但掃到的頻道編號與第四台似乎不同), 參考 :

南國頻道節目表

外語台 :
12 : 國家地理
13 : Discovery
16 : 動物頻道

洋片台 :
63 : HBO
64 : 東森洋片
65 : CINEMAX
66 : Holywood
67 : Movies
68 : AXN

戲劇電影台 :
42 : 八大戲劇
43 : 衛視中文台
71 : 東森電影
72 : 緯來電影
76 : 緯來日本台

新聞台 :
50 : 壹新聞
51 : 年代新聞
52 : 東森新聞
53 : 中天新聞
55 : 民視新聞
56 : 三立新聞
57 : TVBS 新聞

財經台 :
59 : 東森財經
60 : 非凡新聞
85 : 非凡商業

老三台與公廣 :
1 : 民視 HD
18 : 台視 HD
20 : 中視 HD
22 : 華視 HD
79 : 公視 HD

直接點機上盒則會進入登入表單 :


連線成功後就可以觀看影片了.

5. 手機安裝 Nettv4 mobile :

到 Google Play 下載最新的 Nettv4 mobile, 安裝後開啟 App 輸入 ID (盒裝光碟底下有寫) 與密碼 (預設 admin), 按 Login 即連線至 Nettv3 dual 了 :


連線成功後就可看節目了, 畫面雖然不是非常清晰, 但還可以接受啦! 在螢幕點一下會彈出選台器, 再按一下就會消失. 格式用預設的 CIF 就可以了 (5M ADSL 只能用這個), 除非家裡用光纖 :


回來高雄後有試著安裝 Nettv3 on TV, 是正常可以使用, 不過因為是大螢幕, 所以感覺畫面品質比不上在手機上好, 因為模糊感被放大了. 但一樣還是可以接受啦!

參考 :

#《開箱文》NetTV3 Dual網路電視盒,出門在外也能用手機/平板看第四台
# UPMOST netTV2網路電視盒安裝教學


2016年3月22日 星期二

好書 : 松本行弘談程式世界的未來

偶然機會在網路上看到這本書的電子版, 搜尋市圖網站發現有收藏此書就借來看看. 其實我只是想知道, 身為 Ruby 創造者的松本行弘如何看待同樣是 Dynamic Scripting 語言的 Lua. 這兩個語言有一個共通點 : Lua 是唯一起源於南美洲的程式語言; 而 Ruby 則是唯一起源於亞洲的語言, 我很好奇一個語言的設計者會怎麼看另外一個語言.
Source : 金石堂

松本行弘在本書的 3-5 節談到 Lua, 重點摘要如下 :
  1. Lua 的特徵是考慮到嵌入應用程式的通用指令稿語言, 與其想法類似的還有 Tcl, 不過 Lua 比 Tcl 更接近於通用語言. Lua 與 Tcl 為此目的都將語言規格都限制在非常小的程度, 例如 Tcl 只有字串這一個資料型別, 而 Lua 則使用一種表格結構來實作其所支持的資料型態. 
  2. Lua 在實作上完全以 ANSI C 撰寫, 實現了極高的可移植性, Lua 的虛擬機器以高速聞名. 
  3. Lua 的數值資料型別全部用浮點數儲存, 沒有整數 (Perl 也是如此). 而使用者資料型別 (User Data) 用在以 C 語言等擴充 Lua 功能時, 例如檔案 I/O 就是利用 User Data 達成.
  4. Lua 的函式是頭等物件 (first class), 即與數值等其他資料型別一樣, 可以存入物件, 當成引數傳遞, 或作為傳回值等. 
  5. Lua 最具特徵的資料型別是表格, 可實現其他語言中的 array(陣列), hash(關聯式陣列), object(物件) 等結構, 是萬能資料型別. PHP 也是 array 與 hash 合而為一, 而 Javascript 雖然也有關聯式陣列, 但與陣列是獨立的資料結構.
    array={1,2,3}     --陣列
    print(array[1])    --輸出 1
    hash={x=1,y=2,z=3}   --關聯式陣列
    array["x"]=1    --array 與 hash 混合 (賦值方法一)
    array.y=2         --array 與 hash 混合 (賦值方法二)
    print(#array)    --輸出 3 (陣列長度運算子 # 不會將 hash 元素算在內)
  6. Lua 表格用法需特別注意的地方有兩點 : 一是表格當陣列使用時, 其索引是從 1 起算的 (以前的 FORTRAN 與 PASCAL 也是), 而 C 語言及其徒子徒孫都從 0 起算, 很容易搞錯. 其次是 # 運算子只能計算表格中以陣列方式儲存之元素, 不包括關聯式陣列元素. 
  7. Lua 原始設計本來並非以物件導向為目標, 它的物件導向是以 metatable (詮釋表格) 方式實現的. Lua 的物件與類別是以表格來實作, 而方法是用函式來實作. 使用 metatable 好像可以看到物件的內臟一樣, 與早期 Perl 的物件導向類似. 
  8. 以嵌入式用途為設計目標的 Lua 核心相當簡潔, 沒有基本資料型別以外的東西, 只有表格操作, 一些數學函數, 載入套件的功能, 以及包含樣式比對功能的字串, 函式庫只有檔案輸出入而已, 比起附帶大量功能的 Ruby 來說, 實在非常貧弱. 不過 Lua 很容易以 C 擴充功能, 內建的功能不過是拿來描述邏輯. 
  9. 同樣是作為嵌入式用途的 Tcl 語言, 由於附屬的 GUI 函式庫 Tk 相當成功, 反而常被拿來當作 GUI 開發語言, 相較之下, Lua 似乎還是比較能不忘初衷, 主要以嵌入其他程式為主. 
  10. Lua 直譯器的資料結構統一歸納在名為 lua_state 的結構裡, 一個 process 可以擁有許多直譯器, 因此要讓遊戲裡的每個角色都擁有個別的直譯器也是可行的. 在 multi-thread 環境下, 各個 thread 也都能擁有直譯器, 可把多核處理器的效能發揮到極限.
  11. Lua 在垃圾收集方面也下了許多功夫, 採用遞增收集演算法 (Incremental GC, 將垃圾收集工作細分, 逐一執行, 將中斷時間限縮在一定值內, 避免影響即時性) 盡量縮短中斷時間. 因為在遊戲這種對即時性要求很高的環境下, 垃圾收集機制會使遊戲中斷, 導致無法操作, 即使只有一秒也會影響玩家的感覺. 
  12. 以前 Cisco 路由器是內嵌 Tcl, 但現在的趨勢卻是 Lua.
  13. 在嵌入式應用上, Ruby 可能比不上 Lua. 雖說要將 Ruby 嵌入其他語言不是不可能 (例如 RPG 製作大師), 但卻不是 Ruby 擅長之事, 特別是 Ruby 沒有統一表現直譯器的結構, 使得一個 process 只能擁有一個直譯器, 無法達到 Lua 那樣可協調多個直譯器, 配置到多個 thread 的功能.  
  14. 隨著嵌入式硬體性能提升, 軟體複雜化, Lua 這類小規模執行環境, 為嵌入其他程式而設計的語言, 今後將有越來越多發揮的機會. 
  15. 這幾年程式語言不斷進化, 其中最劃時代的, 應該是 Chrome 內的 Javascript V8 引擎與 LuaJIT (Lua 的高速實作), 這兩個語言都具備了動態語言方便開發與令人驚異的執行速度, 甚至超越實作得較差的編譯式語言. 
松本行弘看來還蠻公道地評論了自家的 Ruby 與活躍在嵌入式世界的 Lua, 雖然口裡還是說對 Ruby 非常有信心, 但也承認了 Lua 在嵌入式方面無可取代的優勢. 我個人是覺得 Ruby 雖然以方便快速開發著稱, Lua 在這方面也不遑多讓啊! 只是還沒出現 "Lua on Rails" 這樣的框架罷了. Ruby 若不是 Ruby on Rails, 恐怕也沒有這麼風光吧!  Ruby on Rails 的特徵如下 :
  • 完全 MVC 架構
  • 排除設定檔 (XML)
  • 採用簡潔語法
  • 活用 Metaprogramming
  • 大膽擴充 Ruby 核心
除了 Lua 外, 松本也介紹並評價了 Go, Dart, Coffeescript 等語言.

Go 語言是 Google 力推的新語言 (靜態), 主要考量是要充分發揮多核心的 CPU 性能並解決 C/C++ 的缺點, 因此 Go 語言具備了設計 Concurrent 平行處理程式的能力 (電信業界採用的 Erlang 也具有設計平行處理程式的能力, 但那已是誕生於 1987 年的老語言了, 而且很奇怪, 這是個沒有迴圈的語言哩!).

Dart 語言是結構化又靈活的 Web 語言, 最大特徵是具有可省略的靜態型別. 其設計動機是為了取代 Javascript, 因為 Javascript 有下列缺點 :
  • 無法對應功能豐富的網路應用程式
  • 難以高速化
  • 無法支援多核與 GPU
  • 無法修正
但 Javascript 仍在進化, 而且擁有極多使用者社群, Dart 要達成目的恐怕有很大困難吧!

Coffeescript 的出現也是基於對 Javascript 的不滿, Javascript 語法因為太簡單, 使程式變得冗長. 而 Coffeescript 是 "為了撰寫 Javascript 而生的便利語言". Coffeescript 程式執行前要先用 Coffeescript 編譯器 (也是用 Javascript 寫的) 轉成 Javascript 程式, 然後執行此 Javascript 程式. 其語法受到 Ruby 與 Python 的強烈影響 (特別是 Python), 語法比單純過頭的 Javascript 稍微複雜. (奇怪, 居然還有人嫌語法太簡單的!)

松本也提到了與 FORTRAN, COBOL 並稱為 "古代三大程式語言" 的 Lisp, 此語言是創立者 John McCarthy 從所設計的 lambda 演算法進化而來的, 是從數學誕生的語言, 特點是以清單 (list) 結構來操作運算, 這使其廣泛應用在人工智慧領域, 其運算式充滿了許多小括弧, 閱讀起來蠻吃力的. 而我們熟知的虛擬機器與垃圾收集機制也是 Lisp 最早提出並實作出來的, 我都一直以為這是 Java 帶來的創新呢!

松本觀察程式語言的進化方向, 認為最重要的是抽象化 (黑箱化), 例如將處理過程命名就是重點之一, 抽象化的好處是程式設計者不用涉入太多語言的內部細節, 抽象化的自然演進結果之一就是物件導向, 物件就是抽象化的資料. 但也有刻意不要物件化的語言, 例如 Lisp 創立者 Paul Graham 發展的 Arc 語言就是. 松本曾對其學生出過 "預測 20 年後的語言" 這樣的作業, 大多數人預測 :

  • 能更簡單寫出程式的語言
  • 能以自然語言對電腦下令

這也是我的期望. 我認為 Lua 蠻符合第一個期待.

這本書其實還談了非常多東西, 有些比較深入探討語言設計與實作的太艱深, 看不懂 (比如閉包我也覺得非常難以駕馭). 但較淺顯的例如 Big data 常用的資料庫 NoSQL, 摩爾定律, 非阻塞式 I/O 的 node.js 等等, 蠻有可看性的, 從語言設計者的觀點來看語言的特質, 自然有不同的光景.


2016 年第 12 周記事

週六因為參加阿塱壹健行活動, 回到家已晚上 9 點半了, 所以週日早上才回鄉下, 還趕得上下午來台祖的掃墓. 每次掃墓雖然檢查再檢查, 仍然會掛一漏萬, 忘了帶某個重要部品. 其實去年我就整理了一張清單如下 :

 # 掃墓用品

這裡面最容易忘記的是這幾項 :
  • 開瓶器
  • 五福紙
  • 糨糊
  • 打火機
  • 塑膠袋
今年堂嬸沒來掃墓了, 據說身體違和, 但多方輾轉詢問仍不知情況, 似乎不欲人知, 因此我們也不便當面問. 堂哥則因今年掃墓離春節較久先回中國了, 首次由其三位千金代表他們那房. 由於我們這房三代單傳, 這些親族其實都是堂堂叔, 堂堂嬸, 堂堂堂姪女 ... 六大房子孫應該很多才對, 但實際上每年來掃墓的人約 20 個, 有一房每年都是堂叔堂嬸兩夫婦來, 從來不曾見其子女, 很奇怪, 為什麼都不要求他們來呢? 老的一輩日漸凋零, 以後怎辦? 更何況昔日的三合院大宅早已被拆除轉賣, 各房早就搬出去開枝散葉, 只有每年來台祖掃墓才會聚在一起, 掃墓實在應該盡量出席才對.

掃墓回來本想照計畫將後院的過貓砍除, 但小舅來訪, 一聊就到傍晚. 但他們回去後我還是拿起柴刀砍砍砍, 將遮住走道的全部砍掉, 下周回來再將根部拔除, 然後鋪上泥土, 把魚腥草復育回來.


2016年3月21日 星期一

草原之歌套馬杆

今天在 Youtube 又看到另外一個蒙古族女歌手烏蘭托婭唱的 "套馬杆" MV, 我對於豪邁的草原之歌非常喜歡, 蒙古女子真是生就一副好歌喉啊! 這首雖然已是 2010 年的老歌了, 但是好聽的旋律就是非常耐聽.

# 乌兰托娅 套马杆


烏蘭托婭出身內蒙錫林郭勒盟, 出過一系列草原之歌, 有 "草原百靈鳥" 美譽. 後來因為跟經紀公司有一些糾紛而解約, 無法再唱成名作套馬杆等歌曲, 經紀公司另外又找了同樣是蒙族的烏蘭圖雅來唱, 比較兩人的唱腔後, 我覺得還是烏蘭托婭唱得好, 不僅唱法狂野, MV 也比較能表現歌詞的內涵.

# 套马杆- 乌兰图雅


烏蘭圖雅在轉音的地方太突兀 (例如漢子, 原野, 熱辣等詞的轉換), 而烏蘭托婭就一氣呵成, 聽起來很自然.

"一望無際的原野隨你去流浪", 遼闊的呼倫貝爾大草原, 有機會我也想要去看看.


套馬桿歌詞

作詞:劉新圈
作曲:郭永利
演唱 : 烏蘭托婭/烏蘭圖雅

給我一片藍天
一輪初升的太陽
給我一片綠草
綿延向遠方
給我一隻雄鷹
一個威武的漢子
給我一個套馬桿
攥在他手上

給我一片白雲
一朵潔白的想像
給我一陣清風
吹開百花香
給我一次邂逅
在青青的牧場
給我一個眼神
熱辣滾燙

套馬的漢子你威武雄壯
飛馳的駿馬像疾風一樣
一望無際的原野隨你去流浪
你的心海和大地一樣寬廣
套馬的漢子你在我心上
我願融化在你寬闊的胸膛
一望無際的原野隨你去流浪
所有的日子像你一樣晴朗

給我一片白雲
一朵潔白的想像
給我一陣清風
吹開百花香
給我一次邂逅
在青青的牧場
給我一個眼神
熱辣滾燙

套馬的漢子你威武雄壯
飛馳的駿馬像疾風一樣
一望無際的原野隨你去流浪
你的心海和大地一樣寬廣
套馬的漢子你在我心上
我願融化在你寬闊的胸膛
一望無際的原野隨你去流浪
所有的日子像你一樣晴朗

套馬的漢子你威武雄壯
飛馳的駿馬像疾風一樣
一望無際的原野隨你去流浪
你的心海和大地一樣寬廣
套馬的漢子你在我心上
我願融化在你寬闊的胸膛
一望無際的原野隨你去流浪
所有的日子像你一樣晴朗


2016年3月20日 星期日

看恐怖片的猫

今天菁菁在臉書看到同學分享 Youtube 影片, 片中一隻貓正聚精會神看恐怖片, 那表情好可愛. 難道貓也怕鬼嗎?


這隻貓看起來像是我家以前那隻小咪, 應該是美國短毛貓.


2016年3月19日 星期六

阿塱壹古道

今天參加公司體育活動, 去走阿塱壹古道, 來回五, 六公里, 好熱又好累, 腳都快走斷了. 八點從高雄出發, 到枋寮往四重溪約 11 點到, 坐了好久的車. 而且牡丹鄉那一段山路彎來彎去, 我都快暈車了.

由於去年有人在觀音鼻被海浪捲走, 以北路段目前封起來, 所以這次我們從旭海只走到觀音鼻就折返, 但走回頭路我覺得反而比較累哩! 尤其是沙灘那一段特難走! 回到檢查哨後我都已步履蹣跚了.

晚上七點到東港的海鮮餐廳吃晚餐, 菜色不錯, 而且也餓昏了, 我連吃了兩碗!

今天實在太累了, 先睡覺去也.


2016年3月16日 星期三

兩首中國歌

同事的手機來電鈴聲是一首歌, 通常響幾聲就被接起來了, 所以也沒有很注意. 昨天卻因為手機放桌上響了很久沒人接讓我們聽了大半, 嘿, 還不錯嘛! 回頭問這是啥歌? 原來是 2012 年在中國爆紅的鳳凰傳奇樂團 "最炫民族風", 因為節奏感十足被拿來當作舞曲, 所謂的 "廣場舞" 歌曲是也, 現在算是老歌了.

女主唱為出身內蒙鄂爾多斯的楊魏玲花, 歌聲渾厚有力又高亢, 使得男主唱曾毅感覺好像只是和聲而已.

# 凤凰传奇 最炫民族风MV (KTV 字幕版)
# 凤凰传奇-最炫民族风 MV (原版)



另外一首我從草原來也很好聽 :

# 凤凰传奇-我从草原来 MV


另外一首老歌是王麟的傷不起, 最初是在一部短篇電影 (人在冏途演員所導) 中聽到, 昨天才知道歌名就叫做傷不起. 但姐姐說這首很俗, 我是覺得還蠻好聽的啦 (只好旋律感夠, 就會被我判定為可聽, 那種很吵聽不出旋律的搖滾, 我實在無法欣賞).

# 王麟-神曲《伤不起》正版MV首发.flv



2016年3月14日 星期一

2016 年第 11 周記事

前幾天的炎熱, 好像冬天的腳步似乎已經要遠離, 但這兩天又乍暖還寒, 趕緊把冬衣又拿出來穿. 這樣的天氣最容易感冒, 菁菁這個禮拜就感冒了, 咳了兩三天, 喝了幾次洋蔥汁也無效, 週六早上去看耳鼻喉科, 吃了藥照樣咳. 週六下午回鄉下前特地去全聯買酸梅與麥芽糖, 準備改煮魚腥草湯來治, 但全聯沒賣麥芽糖.

週日傍晚到後院去找魚腥草, 以前長滿魚腥草的景況, 現時卻被過貓掩蓋, 只採到十餘片葉子. 而餐櫥裡以前媽留下的罐子裡還有少許麥芽糖, 加上冰糖湊合著用還夠, 將魚腥草葉洗淨後, 與五顆酸梅, 冰糖, 麥芽糖置於碗公內, 加清水至八分滿, 放入電鍋, 以半碗水蒸熟即可, 主要是喝那酸酸甜甜的湯汁, 煮熟的魚腥草葉也可以吃掉, 菁菁則愛那已經不酸的酸梅. 這方法治咳嗽是從祖母時代傳下來的, 小時候每次感冒有咳嗽的話, 就是用這味. 還真有效哩, 昨晚睡覺就沒聽到菁菁在咳嗽了. 下周要將後院那些過貓拔除, 重新復育魚腥草.

去年底種的 20 棵玉米到本周已熟成, 超過 60 天了, 冬天日照較不足, 成熟期拉長了. 總共採收了近 40 支玉米, 都是甜嫩度剛剛好, 除了爸去串門子時拿十餘支給阿泉伯外, 還有 20 支實在太多, 而小舅媽他們本周沒有回鄉下沒辦法分享, 只得分兩鍋去煮, 在這陰雨又帶點寒氣的周日午後, 品嚐熱騰騰的鮮採甜玉米, 實乃一大享受也, 我一口氣連吃四支!
 

煮過玉米的湯汁也別倒掉了,  放涼了當茶喝, 有淡淡的甜味, 也是非常好的利尿劑呢!


今早雖然也下著細雨, 但為了去領務局辦新護照, 還是騎摩托車上班. 我護照雖未過期, 但效期也只到六月底, 旅行社的林妹妹說需辦新的, 上週資料已備妥, 但卻因為下雨開車沒去辦, 成功路那邊要停車有點麻煩. 中午吃過飯後才去辦真是失策, 因為連假前辦護照的人太多, 我等到近兩點才辦好, 回來只好請一個小時補休假, 如果 12 點就先去辦護照, 回來再吃午飯, 時間就剛剛好.

失蹤快一個月的大咪本周終於回來了, 真是鬆了一口氣, 但是這兩天卻沒回來, 所以我也一個月沒見到牠的面了.

 

2016年3月12日 星期六

Chrome 阻擋檔案下載的問題

今天在百度雲要下載一個 pdf 檔時, 竟然被 Chrome 認為是惡意軟體而遭到阻擋, 如下所示 :


雖然我知道 pdf 也有很多作怪的方法, 但是我可以下載後用 VirusTotal 來掃描看看啊! 網路搜尋發現, 原來 Chrome 把很多被人用來存放惡意軟體的網站 (例如百度雲) 列入黑名單, 隱私權設定預設會阻擋我們從這些網站下載任何檔案, 參考 :

如何關閉Google Chrome瀏覽器「阻擋釣魚網站及惡意程式」功能

從這篇文章可知, 只要進入 Chrome 的設定 (瀏覽器右上角的那三條槓), 將安全性設定取消即不會被阻擋了. 不過此文使用較舊的 Chrome, 目前最新的 Chrome 設定如下 :


點按最底下的 "顯示進階設定", 將 "隱私權" 設定裡的 "保護您和您的設備不受危險網站攻擊" 這一項取消勾選 (預設為勾選), 直接關掉設定分頁即可順利下載了.


但下載完請重新將此項勾選起來以策安全. 最好將下載的檔案上傳到 VirusTotal 去掃描確定檔案是否乾淨為宜.


2016年3月7日 星期一

2016 年第 10 周記事

因姊姊要趕下周的班展以及補畫室的課, 所以只有菁菁跟我回鄉下去.

進入三月後, 市圖借書 double 結束, 過去這段時間我快速擴張借書量, 實際上沒那麼多時間來消化, 這下市圖祭出追繳令, 借來沒看的只好被迫斷頭, 乖乖還回去. 至少還了 15 本, 但還是在滿檔狀態, 本周還要繼續努力看書還書.

由於今年所種冬瓜已經在上周全部用完, 所以這周就不再做冬瓜封了. 下午比較空閒開始整理菜園, 把車庫旁的舊椅子裝袋丟棄; 將之前的玉米, 甘蔗與咖啡樹枯枝全部燒掉, 趁機烤了四根地瓜. 


三本 Corona 的好書

在市圖找到兩本介紹 Corona SDK 的好書, Corona 主要是用來開發手機 App 遊戲的平台, 其成名是因為一個 14 歲小男孩使用它設計了一款名為 Bubble Ball 的遊戲, 不僅成功在 App Store 上架, 還打敗了當時最紅的 Angry Bird 遊戲. 我是因為學習 Lua 才注意到這個軟體的, 因為 Corona 的草稿語言就是使用 Lua, 因為 Lua 具有輕量與高效率的優點.

1. 10 天做好 App : Corona SDK 超直覺遊戲開發攻略 (PCuSER 出版, 魏巍, 左營)

此書作者德文系畢業, 出過唱片, 做過 DJ, 並非資訊電機科班出身, 一般人很難想像竟能短時間內學會 App 設計, 原來靠的是 Corona SDK 這個好用的工具. 厲害的是, 作者在 Google Play 與 App Store 上架的 App 從企劃, 設計, 到圖案, 音樂等元素, 全部自己包下了, 可謂全能型 App 設計者. 其個人網站如下 :

http://appsgaga.com/

這本書沒有附光碟, 範例程式可從下列網址下載 :

# http://goo.gl/Yzau1I


作者 2014 年出的第二本 Corona 書為實作篇 :

2. 10天做好APP【實作進化版】:Corona SDK跨平台遊戲開發攻略,不懂程式也沒差!(小港)




3. Corona SDK 跨平台 App 開發設計實戰 (博碩, 白乃遠, 河堤)

此書提供 8 個經典範例, 說明如何使用 Corona SDK 快速開發, 並對於 Google Play 與 App Store 上架程序有專章介紹.

2017-03-03 補充 :

最近在鄉下的市圖分館找到另一本 Corona 的書 :

# 利用 Corona 一次開發跨平台手機 App 桌面程式


這本書比上面的都還厚, 內容很豐富.

2016年3月5日 星期六

兩本 R 語言的原文好書

這兩本書是去年 12 月底從左新借來的 R 語言原文書, 都非常新 (因為沒有人會借, 我大概是第一個借的吧!), 我看續借一整年應該都沒有人會跟我搶. 哈哈哈, 原文書就有這個優點, 能耐心看的人不多 (我的耐心天下無敵, 嘿嘿), 所以會借的人也少, 書況大致都很不錯. 有些原文書進館三四年還是跟新版書一樣 (左新的工程科技類原文書最多了). 嗯, 半路出家去讀外文系還是有用滴.

本月份市圖 double 借書結束了, 必須先歸還, 現在沒時間玩 R 還是先還再說, 反正應該不會有人跟我搶.

Data mining and business analytics with R (左新)

(來源:金石堂)

這本書是以 R 語言來進行資料探勘與商業分析, 需要有堅實的統計學底子才看得下去, 例如線性回歸, 二元分類法, 貝氏分析等等, 書中範例含括社會, 醫學, 與商業等各方面, 例如歐洲蛋白質的消費, 收入預測, 攝護腺癌分析, MBA 入學資料分析等等.

# An introduction to statistical learning :with applications in R (左新)

(來源:金石堂)

這本書側重基本統計學與統計學習的介紹, 以 R 語言為計算工具.

2017-03-16 補充 :

這本書有簡體翻譯版, 參考 :

# 統計學習導論-基於R應用

Source : 三民


幾本 TCP/IP 的好書

以下這幾本跟總圖借的書暫時沒時間看要先還, 所以先做個紀錄, 以便將來按圖索驥, 等我回頭玩 Arduino 與 Raspbery Pi 時再借回來研究.

1. TCP/IP嵌入式系統Web伺服器第二版 (全華, 蕭榮修, 總館)


此書相當特別, 專門介紹如何在微控器 (書中採用 PIC) 上實作 TCP/IP 堆疊, 同類書幾乎沒有, 此書僅總館一本.

2. TCP/IP 通訊協定 第四版 (全華, 張承翃譯, 總館, 美濃)
此翻譯書應該是講 TCP/IP 規格最詳細的書, 圖表特多, 最重要的是包含 RTP 與 SCTP 協定, 這在其他書找不到.

3. 從零開始了解 TCP/IP 基礎架構 (佳魁, 楊波, 總館)


這本強國人寫的書以淺顯文字加漫畫向外行或初入門者介紹 TCP/IP, 可以當 做閒書來看.

4. 網路 TCP/IP 教本 修訂版 (全華, 江輔政, 總館)


此書特點是著重在封包的範例分析, 而非規約欄位的解說, 全書有上百個峰包範例, 詳細到擷取的 byte 都展開來解析印證, 是實戰級的好書, 適合理論課上完後來實際操練用.

5. TCP/IP 入門 (全華, 林立國譯, 總館)


這本小巧的日文翻譯書是針對 TCP/IP 入門者而寫,  對於基礎技術解說詳盡, 也是可以當閒書看的技術書籍.

6. TCP/IP最佳入門實用書第七版 (碁峰, 蕭文龍, 總館, 河堤)


能出到第七版表示書寫得好熱銷. 此書以 Wireshark 詳細解說規約格式與運作, 連 state machine 都畫出來. 後面有專章介紹 IPv6 與 MRTG (要安裝 Perl).

7. TCP/IP 協定觀念與實作 第二版 (旗標, 施威銘研究室, 總館岡山)


這本書對觀念講得非常細, 書中有許多補充說明. 總館為 93 年版, 岡山分館有 2008 年版.

8. TCP/IP網路通訊協定 第二版 (博碩 2012, 陳祥輝, 河堤)
這本書是我最早借的一本 TCP/IP 書, 特點是程序圖示多且詳細, 例如 TCP 三向交握就有多張流程圖解說.

9. 網路概論第三版 (博碩, 陳湘陽, 河堤)

此書顧名思義範圍較廣, 並非專講 TCP/IP, 還涉及無線網路與 3G/LTE 部分.

10. 電腦網路概論 2013 最新版 (全華, 陳雲龍, 河堤)


本書已出 2015 第五版, 此書可能是要做為教科書, 寫得精簡扼要, 但圖表很詳細, 後面有介紹用 wireshark 抓取信號.

20160930 補充 :

11. 讓網路上的每個封包都無所遁形 :精用Wireshark

Source : 金石堂

這本書是專門討論 Wireshark 應用的好書.



2016年3月4日 星期五

王添灯這個人

今天在 Yahoo 新聞看到這篇 :

# 二二八:「祖國」不關心台灣同胞 只關心台灣糖包

讀到前台灣參議員王添燈的悲慘遭遇, 令人不勝唏噓. 228 紀念日 (應該是悼念日才對) 雖已過去一周, 此文讀來仍讓我內心澎湃不已. 關於王添燈生平, 詳見維基 :

# Wiki : 王添燈

王氏因為在省參議會質詢行政長官陳儀官署公務人員貪汙舞弊而得罪當道, 更在報刊中抨擊國民黨與特務迫害農民, 在 1947 年 228 事件中更參與 228 處理委員會, 遭到掃蕩部隊逮捕, 於酷刑中仍破口大罵憲兵團長張慕陶,  結果被淋上汽油活活燒死, 屍體被丟進淡水河 ....

"根據一個憲兵第四團士兵的証詞:王添灯跟張慕陶大聲爭辯,被打得很厲害,鮮紅的血從臉上往下流,後來滿頭滿面都是血塊,但是王添灯絕不屈服,大罵張慕陶。張慕陶罵王添灯:「你這個野心家,想當臺北市長……」。王添灯回答說:「我從來沒有想過要當臺北市長,我就當……」。張慕陶暴跳如雷:「好!就讓你到陰間當臺北市長!」,命令衛兵往王添灯身上潑汽油,從頭上到腳底,都是濕淋淋的。最後拉到一個地方,點火把他燒死了。"

228 事件被平反後, 王添燈的遺族拒絕領取所謂的 "補償費", 因為他們根本不承認這個政權的合法性 ...

其實 228 之後一個禮拜 21 師在基隆登陸時, 基隆人的遭遇也很可怕 :

# 228走過一甲子 基隆大屠殺1


# 228走過一甲子 基隆大屠殺( 鐵絲穿掌、槍殺人民)2


我個人倒是認為國民黨與明末內戰中被打跑的大順政權流寇境況類似 (參見 Wiki 李自成, 郭沫若曾撰文甲申三百年祭來影射國民黨的貪腐), 日本投降之後就開始搜刮, 荼毒人民, 轉進自己口袋. 連大量的美援都被蔣介石的金主孔宋家族 A 走, 難怪杜魯門會破口大罵 "他們(國民黨)都是賊" !

"他們(國民黨)都是賊,個個都他媽的是賊……他們從我們給蔣送去的38億美元中偷去7.5億美元。他們偷了這筆錢,而且將這筆錢投資在巴西的聖保羅,以及就在這裏,紐約的房地產。但此說法經聯邦調查局調查後,最後以證據不足結案。"

嘿嘿! 好一個證據不足.


參見 :

# Wiki : 哈瑞·S·杜魯門

逝者已矣, 但冤魂不散, 仍然時時刻刻壟罩在國民黨這個擁有大量不明黨產的全世界最有錢政黨頭上. 世俗認為冤魂要超渡才能了結恩怨, 吾以為, 只有真誠的懺悔並接受果報, 才能真正超渡.


EasyuiCMS on GAE 新增應用程式的方法

完成 EasyuiCMS on GAE 基本架構之後, 感覺似乎離目標只剩一步之遙, 所以早上就趁著去上一個跟工作幾乎無關的無聊課 (防毒) 時, 把最後一塊拼圖補上, 那就是如何在此架站平台上新增應用服務呢?

在 PHP 版時只要單獨寫好應用程式與其安裝檔兩個檔案, 然後利用檔案上傳就可以自動處理應用程式安裝的所有工作, 安裝後馬上可用. 在 GAE 因為雲端平台架構的關係, 可能比較難做到這樣, 每次新增一個服務必須修改現有檔案並新增相關檔案. 以下便以新增一個工作日誌服務 JLOG 為例, 說明其增修程序.

首先修改資料儲存檔 model.py, 新增一個 Jlogtabs 頁籤的資料模型, 直接從系統的 Hometabs 複製過來改名稱即可 :

class Jlogtabs(db.Model):
    tab_name=db.StringProperty()
    tab_label=db.StringProperty()
    tab_link=db.StringProperty()
    tab_tip=db.StringProperty()
    tab_order=db.IntegerProperty()
    tab_admin=db.BooleanProperty()

並建立 jlog_home, jlog_knowledge, jlog_admin 三個頁籤的資料實體 :

jlogtab=Jlogtabs(key_name="jlog_home",tab_name="jlog_home",tab_label=u"工作日誌",tab_link="/jlog_home",tab_order=0,tab_admin=False)
jlogtab.put()
jlogtab=Jlogtabs(key_name="jlog_knowledge",tab_name="jlog_knowledge",tab_label=u"知識庫",tab_link="/jlog_knowledge",tab_order=98,tab_admin=False)
jlogtab.put()
jlogtab=Jlogtabs(key_name="jlog_admin",tab_name="jlog_admin",
    tab_label=u"管理",tab_link="/jlog_admin",tab_order=99,tab_admin=True)
jlogtab.put()

然後新增一個標頭連結 JLOG 就完成資料庫部分的調整作業了 :

headerlink=Headerlinks(key_name="JLOG",name="JLOG",title=u"JLOG",
    url="javascript:goJLOG()",target="_self",sequence=0,hint=u"JLOG")
headerlink.put()

這裡 url 中的 goJLOG() 函式是要在系統主版面 main.htm 中將工作日誌頁籤網頁載入 panel 的 center 區域, 所以必須到 template/main.htm 中新增此函式 :
   
    function gohome(){
      $(function(){
        var p=$("body").layout("panel","center");
        p.panel({href:"/hometabs"});
        });
      }
    function logout(){
      $(function(){
        $.messager.confirm("確認","確定要登出系統嗎?",function(btn){
          if (btn) {window.location.href="/logout";}
          });
        });
      }
    function goJLOG(){
      $(function(){
        var p=$("body").layout("panel","center");
        p.panel({href:"/jlogtabs"});
        });
      }

事實上就是將 gohome() 複製過來修改即可. 接下來就要去主控程式 main.py 中處理路徑與邏輯. 首先在最底下的 app 中新增上面新增的四個路徑 :

app = webapp2.WSGIApplication([
    ('/', MainHandler),
    ('/login', login),
    ('/check_member', check_member),
    ('/logout', logout),
    ('/hometabs', hometabs),
    ('/home', home),
    ('/change_theme', change_theme),
    ('/list_visitors', list_visitors),
    ('/get_visitors', get_visitors),
    ('/list_members', list_members),
    ('/get_members', get_members),
    ('/remove_visitors', remove_visitors),
    ('/add_member', add_member),
    ('/update_member', update_member),
    ('/remove_member', remove_member),
    ('/list_headerlinks', list_headerlinks),
    ('/get_headerlinks', get_headerlinks),
    ('/add_headerlink', add_headerlink),
    ('/update_headerlink', update_headerlink),
    ('/remove_headerlink', remove_headerlink),
    ('/list_navblocks', list_navblocks),
    ('/get_navblocks', get_navblocks),
    ('/add_navblock', add_navblock),
    ('/update_navblock', update_navblock),
    ('/remove_navblock', remove_navblock),
    ('/list_navlinks', list_navlinks),
    ('/get_navlinks', get_navlinks),
    ('/add_navlink', add_navlink),
    ('/update_navlink', update_navlink),
    ('/remove_navlink', remove_navlink),
    ('/settings', settings),
    ('/update_settings', update_settings),
    ('/member_settings', member_settings),
    ('/jlogtabs', jlogtabs),
    ('/jlog_home', jlog_home),
    ('/jlog_knowledge', jlog_knowledge),
    ('/jlog_admin', jlog_admin)
], debug=True, config=config)

然後建立四個類別來處理工作日誌的四個路徑, 首先是複製 hometabs 類別來改為 jlogtabs 類別,  然後複製 home 類別來改為 jlog_home, jlog_knowledge, 與 jlog_admin 類別, 分別渲染 hometabs.htm,  jlog_home.htm, jlog_knowledge.htm, 以及 jlog_admin.htm 這四個網頁模板 :

class jlogtabs(BaseHandler):
    def get(self):
        self.check_login()
        jlogtabs=m.Jlogtabs.all()
        jlogtabs.order("tab_order")  #sort by tab_order
        tabs=[]  #for storing tab objects
        is_admin=self.session.get('is_admin')  #True/False
        for t in jlogtabs:
            tab={}
            tab["tab_label"]=t.tab_label
            tab["tab_link"]=t.tab_link
            if t.tab_admin:  #this tab is for admin only
                if is_admin: #current user is admin
                    tabs.append(tab)
            else:  #this tab is for registered users
                tabs.append(tab)
        url="templates/jlogtabs.htm"
        path=os.path.join(os.path.dirname(__file__), url)
        content=template.render(path,{"tabs":tabs})
        self.response.out.write(content)

class jlog_home(BaseHandler):
    def get(self):
        self.check_login()
        url="templates/jlog_home.htm"
        path=os.path.join(os.path.dirname(__file__), url)
        content=template.render(path,{})
        self.response.out.write(content)

class jlog_knowledge(BaseHandler):
    def get(self):
        self.check_login()
        url="templates/jlog_knowledge.htm"
        path=os.path.join(os.path.dirname(__file__), url)
        content=template.render(path,{})
        self.response.out.write(content)

class jlog_admin(BaseHandler):
    def get(self):
        self.check_login()
        url="templates/jlog_admin.htm"
        path=os.path.join(os.path.dirname(__file__), url)
        content=template.render(path,{})
        self.response.out.write(content)

這樣 main.py 也調整好了, 接下來就是到 /template 目錄下, 複製 hometabs.htm 為 jlogtabs.htm 修改如下 (只改 id 即可) :

<div id="jlogtabs" class="easyui-tabs" data-options="fit:'true'">
{% for t in tabs %}
  <div class="tab" title="{{t.tab_label}}" data-options="href:'{{t.tab_link}}',loadingMessage:'載入中 ... '"></div>
{% endfor %}
</div>

然後複製 home.htm 為 jlog_home.htm, jlog_knowledge.htm, 以及 jlog_admin.htm, 其內容暫時只是文字而已, 例如 jlog_admin.htm 內容為 :

<p>工作日誌管理</p>

OK, 這樣就完成新增應用服務作業了, 上傳雲端主機後按標頭連結 JLOG 即顯示工作日誌頁籤 :


按首頁又會載入 hometabs.htm, 如此即可在多個應用服務之間切換了. 每一個應用服務的入口主要是上面的標頭連結, 當然也可以在左方導覽區另行建立第二入口.

另外, 在新增 JLOG 時也發現標頭連結的排序程式不妥而加以調整, 就是將 main.py 中根目錄處理類別 MainHandler 中的 headerlinks 查詢物件從原先的逆向排序改為正向排序 :

        #create param: headerlinks
        headerlinks=m.Headerlinks.all()
        headerlinks.order("sequence")  #sort by sequence

然後把 model.py 中的 headerlink 實體排序如下, 即把系統的首頁與登入 sequence 分別改為 98 與 99, 應用服務則從 0 開始編號, 這樣在正向排序下系統標頭連結會排在最後面 :

headerlink=Headerlinks(key_name="JLOG",name="JLOG",title=u"JLOG",
    url="javascript:goJLOG()",target="_self",sequence=0,hint=u"JLOG")
headerlink.put()
headerlink=Headerlinks(key_name="home",name="home",title=u"首頁",
    url="javascript:gohome()",target="_self",sequence=98,hint=u"首頁")
headerlink.put()
headerlink=Headerlinks(key_name="logout",name="logout",title=u"登出",
    url="javascript:logout()",target="_self",sequence=99,hint=u"登出")
headerlink.put()

這樣這個小專案總算拚完了, 當然安全性上還有待改進 (例如密碼欄位的加密, 以及重設密碼的機制), 但至少可以正常運轉. 完整 zip 檔案可在下列連結下載 :

# 具有 JLOG 應用範例的 EasyuiCMS on GAE


2016年3月3日 星期四

游泳 2/190

今天下班後又跑去游泳, 因為明天晚上要去菁菁的班親會沒空去游. 先回家收拾泳衣再騎小摺去, 這樣剛好可以暖身. 這次放聰明了, 下水前沖澡時改沖稍冷的溫水, 不再傻傻地沖熱水. 今天來回游了 14 趟, 再去用水柱沖會痠痛的後腰部, 感覺非常舒服.

很奇怪, 雖然游泳耗費體力, 但游泳回來都精神特好, 不覺得累, 不過還是早點睡為好, 明天可不是假日哩!

2016-03-06 補充 :

今天在班親會上第一次見到菁菁的班導怡如老師, 是個具有濃厚文學底蘊的人, 已經出版好幾本文學作品囉! 我很羨慕作家, 因為走過人生, 作家是那種能在世界上留下痕跡的人. 所謂立德立功立言的三達德, 作家至少能夠立言啊!

2016年3月2日 星期三

EasyuiCMS on GAE 總整理

過去兩個月嘗試把一年前用 PHP 寫的 EasyuiCMS 移植到 GAE 上, 終於有了初步成果, 自從 2009 年底開始接觸 Google 的雲端主機服務 GAE 以來, 就有在 GAE 上寫一個簡單的架站系統的想法, 這樣我就可以很容易地在上面寫各種應用服務程式了. 這幾年大部分時間都花在 PHP 與 Java 的學習上, 對於 Python 一直沒有做系統化的學習, 在 GAE 上用 Python 也是見招拆招而已.

去年底休假時無意中看到以前寫的 GAE 程式, 就突然想動手試試看是否真能在 GAE 上寫個 CMS, 沒想到一寫就不能罷手, 至今整整兩個月總算有了小收穫, 把猜想化為可能, 今天我把這些結果重新整理, 刪除多餘的測試部分, 打包起來存檔, 以備後用.

目錄結構如下所示, /static/easyui 下放置 Easyui 函式庫, /static/images 下放置系統所需圖檔, 而 /templates 下則放置網頁模板 :


應用程式設置檔 app.yaml 內容如下 :

application: gaeweb1
version: 1
runtime: python27
api_version: 1
threadsafe: yes

handlers:
- url: /favicon\.ico
  static_files: favicon.ico
  upload: favicon\.ico
- url: /static
  static_dir: static
- url: .*
  script: main.app

libraries:
- name: webapp2
  version: "2.5.2"

這裡唯一需要修改的部分是第一個參數 application, 其中 gaeweb1 是我所申請的 GAE 應用程式帳號, 如果要把這 CMS 用在別的帳號, 只要改掉 application 之值即可.

其次是資料模型程式 model.py, 其內容如下 :

# -*- coding: utf-8 -*-
from google.appengine.ext import db

class Members(db.Model):
    account=db.StringProperty(required=True)
    password=db.StringProperty(required=True)
    name=db.StringProperty(required=False)
    theme=db.StringProperty(required=True,default="default")
    is_admin=db.BooleanProperty(required=True,default=False)
    email=db.EmailProperty(required=False)
    mobile=db.StringProperty(required=False)
    login_count=db.IntegerProperty(default=0)
    last_login_time=db.DateTimeProperty(auto_now_add=True)

class Visitors(db.Model):
    ip=db.StringProperty()
    visit_time=db.DateTimeProperty()
    user_agent=db.StringProperty()

class Settings(db.Model):
    site_title=db.StringProperty()
    site_theme=db.StringProperty()
    site_state=db.StringProperty()
    site_created_time=db.DateTimeProperty(auto_now_add=True)

class Board(db.Model):
    poster=db.StringProperty()
    subject=db.StringProperty()
    content=db.TextProperty()
    post_time=db.DateTimeProperty()

class Themes(db.Model):
    theme=db.StringProperty()

class Hometabs(db.Model):
    tab_name=db.StringProperty()
    tab_label=db.StringProperty()
    tab_link=db.StringProperty()
    tab_tip=db.StringProperty()
    tab_order=db.IntegerProperty()
    tab_admin=db.BooleanProperty()

class Headerlinks(db.Model):
    name=db.StringProperty()
    title=db.StringProperty()
    url=db.StringProperty()
    target=db.StringProperty()
    sequence=db.IntegerProperty()
    hint=db.StringProperty()

class Navblocks(db.Model):
    name=db.StringProperty()
    title=db.StringProperty()
    sequence=db.IntegerProperty()
    display=db.BooleanProperty()

class Navlinks(db.Model):
    name=db.StringProperty()
    title=db.StringProperty()
    url=db.StringProperty()
    target=db.StringProperty()
    sequence=db.IntegerProperty()
    block_name=db.StringProperty()
    hint=db.StringProperty()

settings=Settings(key_name="settings",site_title="EasyUI-based CMS on GAE",
    site_theme="default",site_state="on")
settings.put()

member=Members(key_name="admin",account="admin",password="admin",name="admin",
    theme="default",is_admin=True,email="admin@foo.bar.com",mobile="0933")
member.put()
member=Members(key_name="guest",account="guest",password="guest",name="guest", theme="ui-cupertino",is_admin=False,email="guest@foo.bar.com",mobile="0932")
member.put()

theme=Themes(key_name="default",theme="default")
theme.put()
theme=Themes(key_name="gray",theme="gray")
theme.put()
theme=Themes(key_name="black",theme="black")
theme.put()
theme=Themes(key_name="bootstrap",theme="bootstrap")
theme.put()
theme=Themes(key_name="metro",theme="metro")
theme.put()
theme=Themes(key_name="metro-blue",theme="metro-blue")
theme.put()
theme=Themes(key_name="metro-gray",theme="metro-gray")
theme.put()
theme=Themes(key_name="metro-green",theme="metro-green")
theme.put()
theme=Themes(key_name="metro-orange",theme="metro-orange")
theme.put()
theme=Themes(key_name="metro-red",theme="metro-red")
theme.put()
theme=Themes(key_name="ui-cupertino",theme="ui-cupertino")
theme.put()
theme=Themes(key_name="ui-dark-hive",theme="ui-dark-hive")
theme.put()
theme=Themes(key_name="ui-pepper-grinder",theme="ui-pepper-grinder")
theme.put()
theme=Themes(key_name="ui-sunny",theme="ui-sunny")
theme.put()

hometab=Hometabs(key_name="home",tab_name="home",tab_label=u"首頁",
    tab_link="/home",tab_order=0,tab_admin=False)
hometab.put()
hometab=Hometabs(key_name="member_settings",tab_name="member_settings", tab_label=u"使用者設定",tab_link="/member_settings",tab_order=1,tab_admin=False)
hometab.put()
hometab=Hometabs(key_name="list_visitors",tab_name="list_visitors",
    tab_label=u"訪客",tab_link="/list_visitors",tab_order=2,tab_admin=True)
hometab.put()
hometab=Hometabs(key_name="list_members",tab_name="list_members",
    tab_label=u"使用者",tab_link="/list_members",tab_order=3,tab_admin=True)
hometab.put()
hometab=Hometabs(key_name="list_headerlinks",tab_name="list_headerlinks",
    tab_label=u"標頭連結",tab_link="/list_headerlinks",tab_order=4,tab_admin=True)
hometab.put()
hometab=Hometabs(key_name="list_navblocks",tab_name="list_navblocks",
    tab_label=u"導覽區塊",tab_link="/list_navblocks",tab_order=5,tab_admin=True)
hometab.put()
hometab=Hometabs(key_name="list_navlinks",tab_name="list_navlinks",
    tab_label=u"導覽連結",tab_link="/list_navlinks",tab_order=6,tab_admin=True)
hometab.put()
hometab=Hometabs(key_name="settings",tab_name="settings",
    tab_label=u"系統設定",tab_link="/settings",tab_order=7,tab_admin=True)
hometab.put()

headerlink=Headerlinks(key_name="home",name="home",title=u"首頁",
    url="javascript:gohome()",target="_self",sequence=0,hint=u"首頁")
headerlink.put()
headerlink=Headerlinks(key_name="logout",name="logout",title=u"登出",
    url="javascript:logout()",target="_self",sequence=0,hint=u"登出")
headerlink.put()

navblock=Navblocks(key_name="main",name="main",title=u"主功能",
    sequence=0,display=True)
navblock.put()

navlink=Navlinks(key_name="home",name="home",title=u"首頁",
    url="javascript:gohome()",target="_self",sequence=0,block_name="main",
    hint=u"首頁")
navlink.put()
navlink=Navlinks(key_name="logout",name="logout",title=u"登出",
    url="javascript:logout()",target="_self",sequence=0,block_name="main",
    hint=u"登出")
navlink.put()

這裡我給 Members 添加了 login_count 與 last_login_time 這兩個欄位來記錄使用者登入次數與最近一次登入時間, 在 main.py 中處理登入時會累計登入次數並更新登入時間. 其所渲染的 list_members.htm 也配合修改如下 :

      $('#members').datagrid({
        columns:[[
          {field:'account',title:'帳號',sortable:true},
          {field:'password',title:'密碼',sortable:true},
          {field:'theme',title:'主題布景',sortable:true},
          {field:'is_admin',title:'管理員',sortable:true},
          {field:'email',title:'Email',sortable:true},
          {field:'mobile',title:'行動電話',sortable:true},
          {field:'login_count',title:'登入次數',sortable:true},
          {field:'last_login_time',title:'最近登入',sortable:true}
          ]],
      url:"/get_members",
      method:"post",
      singleSelect:true,
      fitColumns:true,
      rownumbers:true,
      pagination:true,
      pageSize:10
      });


另外也拿掉以前測試 Visitors 時為了全文搜尋而加的 ip_list 與 user_agent_list 欄位, 僅做 "search_what%" 半模糊比對 (即僅開頭符合), 而且搜尋框也改用 textbox 內含 icon 方式. 其次是將原來的 Systabs 改為 Hometabs, 因為既然回首頁按鈕為呼叫 gohome(), 而且首頁就在此頁籤架構中, 稱為 Hometabs 較名符其實. 當然主程式 main.py 也要配合修改.

在 main.py 中我做了蠻大修訂, 首先為了安全性考量, 我在 BaseHandler 類別中增加一個 check_login() 的函式讓各路徑處理類別在一開始時呼叫, 以檢驗是否已建立連線 Session 物件, 沒有的話一律重導向至登入頁面, 避免使用者直接存取各 url 路徑. 當然, 各路徑處理類別除了 login 外都改為繼承 BaseHandler 而非 webapp2.RequestHandler, 因為要用到 Session 機制之故 (其實 BaseHandler 是 RequestHandler 的子類別) :

# -*- coding: utf-8 -*-
import os
import json
import datetime
import webapp2
import model as m
from datetime import timedelta
from webapp2_extras import sessions
from google.appengine.ext import db
from google.appengine.ext.webapp import template

class BaseHandler(webapp2.RequestHandler):
    def dispatch(self):
        # Get a session store for this request.
        self.session_store=sessions.get_store(request=self.request)
        try:
            #Dispatch the request.
            webapp2.RequestHandler.dispatch(self)
        finally:
            #Save all sessions.
            self.session_store.save_sessions(self.response)
    @webapp2.cached_property
    def session(self):
        #Returns a session using the default cookie key.
        sess=self.session_store.get_session()
        #add some default values:
        if not sess.get("theme"):
            sess["theme"]="default"
        return sess
    def check_login(self):
        account=self.session.get('account')
        if account is None:
            self.redirect("/login")

登入登出的路徑處理類別如下 :

class login(webapp2.RequestHandler):
    def get(self):
        s=m.Settings.get_by_key_name("settings")
        info={}
        if s is None:
            info["site_title"]=""
            info["site_theme"]="ui-cupertino"
        else:
            info["site_title"]=s.site_title
            info["site_theme"]=s.site_theme
        url="templates/login.htm"
        path=os.path.join(os.path.dirname(__file__), url)
        content=template.render(path,{"info":info})
        self.response.out.write(content)

class check_member(BaseHandler):
    def post(self):
        account=self.request.get("account", default_value="unknown")
        password=self.request.get("password", default_value="unknown")
        query=m.Members.gql("""WHERE account= :1
                               AND password= :2""",
                               account,password)
        mb=query.get()
        if mb is None: #user not existed
            content='{"result":"failure","reason":"帳號或密碼錯誤"}'
        else: #user existed
            #check site_state (on/off)
            s=m.Settings.get_by_key_name("settings")
            site_state=s.site_state
            #go main page if system is on or user is admin
            if site_state=="on" or mb.is_admin:
                #save session
                self.session['account']=mb.account     
                self.session['theme']=mb.theme         
                self.session['is_admin']=mb.is_admin
                #update last_login_time & login_count of the user
                mb.last_login_time=datetime.datetime.now() + timedelta(hours=+8)
                mb.login_count=mb.login_count + 1
                mb.put()
                content='{"result":"success"}'
            else: #system is on maintanance (off) and not admin
                content='{"result":"failure","reason":"系統維護中, 請稍候."}'
        self.response.out.write(content)

class logout(BaseHandler):
    def get(self):
        #clear session & back to login
        self.session.clear()
        self.redirect("/login")

在 login 類別中會將 site_theme 傳入 login.htm 中, 用來更改此網頁的主題布景, 這裡我改用字串處理方式來製作 css 檔的路徑, 這樣不管 easyui 是使用 CDN 與自行供檔都會產生所需之路徑. 注意, 在 check_member 類別中, 若登入成功, 會將 login_count 欄位增量 1, 且更新 last_login_time 欄位. 而當 sys_settings 資料表中的 site_state 被設為 off, 且使用者不是管理員時, 系統不會讓使用者登入, 登入檢查會失敗, 並回應系統維護中訊息.



另外我把原先的 login_check 類別改名為 check_member, 因為那很容易跟 BaseHandler 類別裡的 check_login() 函式搞混. 當然, login.htm 中登入網頁的路徑也要同步修改 :

      //將主題布景改為系統設定
      var href=$("#theme").attr("href");
      var css="themes/{{info.site_theme}}/easyui.css"
      href=href.substr(0,href.indexOf("themes")) + css;
      $("#theme").attr("href", href);
      $("#login-form").form({ //設定表單
        url:"/check_member",
        success:function(data){
          var data=eval('(' + data + ')');  //將 JSON 轉成物件
          if (data.result=="success") {window.location.href='/';}
          else {$("#msg").text(data.reason);}
          }
        });

下面是更改個人主題布景的路徑處理類別 :

class change_theme(BaseHandler):
    def get(self):
        self.check_login()
        theme=self.request.get("theme")  #get selected theme
        self.session['theme']=theme  #update user session
        account=self.session.get('account')
        member=m.Members.get_by_key_name(account)  #retrieve entity
        member.theme=theme
        member.put()  #update user theme in datastre

首頁頁籤架構處理 :

class hometabs(BaseHandler):
    def get(self):
        self.check_login()
        hometabs=m.Hometabs.all()
        hometabs.order("tab_order")  #sort by tab_order
        tabs=[]  #for storing tab objects
        is_admin=self.session.get('is_admin')  #True/False
        for t in hometabs:
            tab={}
            tab["tab_label"]=t.tab_label
            tab["tab_link"]=t.tab_link
            if t.tab_admin:  #this tab is for admin only
                if is_admin: #current user is admin
                    tabs.append(tab)
            else:  #this tab is for registered users
                tabs.append(tab)
        url="templates/hometabs.htm"
        path=os.path.join(os.path.dirname(__file__), url)
        content=template.render(path,{"tabs":tabs})
        self.response.out.write(content)

class home(BaseHandler):
    def get(self):
        self.check_login()
        url="templates/home.htm"
        path=os.path.join(os.path.dirname(__file__), url)
        content=template.render(path,{})
        self.response.out.write(content)

這個首頁 home.htm 也是維持僅顯示 "Welcome" 字樣, 寫應用程式時再視需要修改即可 (例如顯示應用服務的 Dashboard 畫面或服務摘要表等).

訪客紀錄器類別如下 :

class list_visitors(BaseHandler):
    def get(self):
        self.check_login()
        url="templates/list_visitors.htm"
        path=os.path.join(os.path.dirname(__file__), url)
        content=template.render(path,{})
        self.response.out.write(content)

class get_visitors(BaseHandler):
    def post(self):
        self.check_login()
        page=self.request.get("page")
        rows=self.request.get("rows")
        sort=self.request.get("sort")
        order=self.request.get("order")
        search_field=self.request.get("search_field")
        search_what=self.request.get("search_what")
        if len(page):
            page=int(page)
        else:
            page=1
        if len(rows):
            rows=int(rows)
        else:
            rows=10
        if not len(sort):
            sort="visit_time"
        if not len(order):
            order="desc"
        if len(search_field):
            query=m.Visitors.all()
            query.filter(search_field + " >= ", search_what)
            query.filter(search_field + " < ", search_what + u'\ufffd')
        else:
            query=m.Visitors.gql("ORDER BY %s %s" % (sort, order))
        visitors=query.fetch(rows, (page-1)*rows)
        rows=[]
        for v in visitors:
            visit_time=v.visit_time.strftime("%Y-%m-%d %H:%M:%S")
            visitor={"ip":v.ip,
                     "visit_time":visit_time,
                     "user_agent":v.user_agent}
            rows.append(visitor)
        count=query.count()
        obj={"total":count,"rows":rows}
        self.response.headers["Content-Type"]="application/json"
        self.response.out.write(json.dumps(obj))

class remove_visitors(BaseHandler):
    def post(self):
        self.check_login()
        visitors=m.Visitors.all()
        for v in visitors:
            db.delete(v)
        content='{"status":"success"}'          
        self.response.out.write(content)

因為在 model.py 中我已經拿掉 ip_list 與 user_agent_list 欄位, 因此這裡的資料實體搜尋也去掉對此兩欄位的全文搜尋, 改為 "search_what%" 的前面符合搜尋. 而它所渲染的 list_visitors.htm 網頁, 其中下拉式選單的選項值也要修改, 將 ip_list 與 user_agent_list 改為 ip 與 user_agent 如下 :

{% extends "jqueryeasyui.htm" %}
{% block style %}
  body {font: 80% "Trebuchet MS", sans-serif; margin: 50px;}
{% endblock%}
{% block body %}
  <table id="visitors" title="訪客列表" style="width:auto" data-options="tools:'#visitors_tools',toolbar:'#visitors_search_bar'"></table>
  <div id="visitors_tools">
    <a href="#" id="remove_visitors" class="icon-remove" title="全部刪除"></a>
    <a href="#" id="reload_visitors" class="icon-reload" title="重新載入"></a>
  </div>
  <div id="visitors_search_bar" style="text-align:right;padding:2px;">
    <select id="visitors_search_field" class="easyui-combobox" data-options="panelHeight:'auto'">
      <option value="ip">IP 位址</option>
      <option value="visit_time">到訪時間</option>
      <option value="user_agent">瀏覽器</option>
    </select>
    <input id="visitors_search_what" class="easyui-textbox">
  </div>
  <script language="javascript">
    $(document).ready(function(){
      $('#visitors').datagrid({
        url:'/get_visitors',
        method:"post",
        columns:[[
          {field:'ip',title:'IP 位址',width:150,sortable:true},
          {field:'visit_time',title:'到訪時間',width:150,sortable:true},
          {field:'user_agent',title:'瀏覽器',sortable:true}
          ]],
        singleSelect:true,
        fitColumns:true,
        collapsible:true,
        rownumbers:true,
        pagination:true,
        pageSize:10
        });
      $("#visitors_search_what").textbox({
        icons:[{
          iconCls:"icon-search",
          handler:function(e){
            $('#visitors').datagrid("load",{
              search_field:$("#visitors_search_field").combobox("getValue"),
              search_what:$("#visitors_search_what").textbox("getValue")
              });          
            }
          }]
        });
      $("#reload_visitors").bind("click",function(){
        $("#visitors").datagrid("load",{search_field:""});
        });
      $("#remove_visitors").bind("click",function(){
        $.messager.confirm("確認","確定要刪除全部訪客紀錄嗎?",function(btn){
          if (btn){
            var params={};
            var callback=function(data){
              if (data.status==="success"){$("#visitors").datagrid("reload");}
              else {$.messager.alert("訊息","刪除訪客紀錄失敗!","error");}
              }
            $.post("/remove_visitors",params,callback,"json");
            }
          });
        });
      });
  </script>
{% endblock%}


此處在搜尋框 search_what 中添加 iconCls:"icon-search" 就會在文字框內出現一個放大鏡搜尋圖樣按鈕. 當按右上角的 reload 按鈕時, 會觸發 datagrid 重新載入, 但是若之前有搜尋動作, 那麼重載時仍會傳送上次的 search_field 參數, 導致不是載入全部資料, 而是與上次搜尋相同的結果, 與預期動作不符. 解決之道是傳入一個空字串的 search_field 參數即可.

其次是我把 remove_visitor 類別改為 remove_visitors, 主要考量是 GAE 不像 MySQL 那樣可用一個自動增量主鍵 id 簡單鎖定要刪除的那筆紀錄, 雖然可用 key_name 來做, 但每一筆訪客紀錄都要設 key_name 也很麻煩, 只能使用日期時間, 但麻煩的是資料庫裡儲存的是精確的日期時間類型, 但網頁顯示的只到秒, 導致從 datagrid 點選一筆要刪除時根本無法真正將資料實體刪除, 因為在搜尋時找不到那筆實體. 當然可以乾脆將 visit_time 改為 StringProperty 類型來解決, 但是想到訪客紀錄有需要一筆一筆殺嗎? 它只是用來看看有哪些人來瀏覽而已, 看完就清空即可, 所以這裡 remove_visitors 類別是刪除全部訪客紀錄實體.

下面是使用者管理相關路徑處理類別, 與上一篇文章內容差不多, 只是改為繼承 BaseHandler 與加入 check_login() :

class list_members(BaseHandler):
    def get(self):
        self.check_login()
        #query Themes from datastore
        themes=m.Themes.all()
        info={}
        theme_list=[]
        for t in themes:
            theme_list.append(t.theme)
        info["themes"]=theme_list
        url="templates/list_members.htm"
        path=os.path.join(os.path.dirname(__file__), url)
        content=template.render(path,{'info':info})
        self.response.out.write(content)

class get_members(BaseHandler):
    def post(self):
        self.check_login()
        page=self.request.get("page")
        rows=self.request.get("rows")
        sort=self.request.get("sort")
        order=self.request.get("order")
        if len(page):
            page=int(page)
        else:
            page=1
        if len(rows):
            rows=int(rows)
        else:
            rows=10
        if not len(sort):
            sort="account"
        if not len(order):
            order="asc"
        query=m.Members.gql("ORDER BY %s %s" % (sort, order))
        count=query.count()
        mbs=query.fetch(rows, (page-1)*rows)
        rows=[]  #for storing objects
        for mb in mbs:
            member={"account":mb.account,
                    "password":mb.password,
                    "theme":mb.theme,
                    "is_admin":mb.is_admin,
                    "email":mb.email,
                    "mobile":mb.mobile}
            rows.append(member)
        obj={"total":count,"rows":rows}  #Easyui datagrid json format
        self.response.headers["Content-Type"]="application/json"
        self.response.out.write(json.dumps(obj))

class add_member(BaseHandler):
    def post(self):
        self.check_login()
        account=self.request.get("account")
        password=self.request.get("password")
        theme=self.request.get("theme")
        is_admin=self.request.get("is_admin")
        email=self.request.get("email", default_value="foo@bar.com")
        mobile=self.request.get("mobile", default_value="0933123456")
        #trans string to boolean
        if is_admin=="True":
            is_admin=True
        else:
            is_admin=False
        #check entity if exist
        mb=m.Members.get_by_key_name(account)
        if mb: #already exist
            result='{"status":"failure","reason":"帳號已存在!"}'
        else:  #new member
            member=m.Members(key_name=account,
                account=account,
                password=password,
                theme=theme,
                is_admin=is_admin,
                email=email,
                mobile=mobile
                )
            member.put()
            result='{"status":"success"}'      
        self.response.out.write(result)

class update_member(BaseHandler):
    def post(self):
        self.check_login()
        account=self.request.get("account")
        password=self.request.get("password")
        theme=self.request.get("theme")
        is_admin=self.request.get("is_admin")
        email=self.request.get("email")
        mobile=self.request.get("mobile", default_value="")
        #trans string to boolean
        if is_admin=="True":
            is_admin=True
        else:
            is_admin=False
        #get entity from store
        mb=m.Members.get_by_key_name(account)
        if mb: #entity exist
            mb.account=account
            mb.password=password
            mb.theme=theme
            mb.is_admin=is_admin
            mb.email=email
            mb.mobile=mobile
            mb.put()
            result='{"status":"success"}'
        else:  #member not existed
            result='{"status":"failure","reason":"使用者不存在!"}'      
        self.response.out.write(result)

class remove_member(BaseHandler):
    def post(self):
        self.check_login()
        account=self.request.get("account")
        #get entity from store
        mb=m.Members.get_by_key_name(account)
        if mb: #entity exist
            db.delete(mb)          
            result='{"status":"success"}'
        else:  #member not existed
            result='{"status":"failure","reason":"使用者不存在!"}'      
        self.response.out.write(result)

下面是標頭超連結管理相關之路徑處理類別 :

class list_headerlinks(BaseHandler):
    def get(self):
        self.check_login()
        url="templates/list_headerlinks.htm"
        path=os.path.join(os.path.dirname(__file__), url)
        content=template.render(path,{})
        self.response.out.write(content)

class get_headerlinks(BaseHandler):
    def post(self):
        self.check_login()
        page=self.request.get("page")
        rows=self.request.get("rows")
        sort=self.request.get("sort")
        order=self.request.get("order")
        if len(page):
            page=int(page)
        else:
            page=1
        if len(rows):
            rows=int(rows)
        else:
            rows=10
        if not len(sort):
            sort="sequence"
        if not len(order):
            order="asc"
        query=m.Headerlinks.gql("ORDER BY %s %s" % (sort, order))
        count=query.count()
        links=query.fetch(rows, (page-1)*rows)
        rows=[]  #for storing objects
        for h in links:
            link={"name":h.name,
                  "title":h.title,
                  "url":h.url,
                  "target":h.target,
                  "sequence":h.sequence,
                  "hint":h.hint}
            rows.append(link)
        obj={"total":count,"rows":rows}  #Easyui datagrid json format
        self.response.headers["Content-Type"]="application/json"
        self.response.out.write(json.dumps(obj))

class add_headerlink(BaseHandler):
    def post(self):
        self.check_login()
        name=self.request.get("name")
        #check entity if exist
        link=m.Headerlinks.get_by_key_name(name)
        if link: #already exist
            result='{"status":"failure","reason":"連結名稱已存在!"}'
        else:  #new link
            headerlink=m.Headerlinks(key_name=name,name=name,
                title=self.request.get("title"),
                url=self.request.get("url"),
                target=self.request.get("target"),
                sequence=int(self.request.get("sequence")),
                hint=self.request.get("hint")
                )
            headerlink.put()
            result='{"status":"success"}'      
        self.response.out.write(result)

class update_headerlink(BaseHandler):
    def post(self):
        self.check_login()
        name=self.request.get("name")
        #get entity from store
        link=m.Headerlinks.get_by_key_name(name)
        if link: #entity exist
            link.title=self.request.get("title")
            link.url=self.request.get("url")
            link.target=self.request.get("target")
            link.sequence=int(self.request.get("sequence"))
            link.hint=self.request.get("hint")
            link.put()
            result='{"status":"success"}'
        else:  #link not existed
            result='{"status":"failure","reason":"連結不存在!"}'      
        self.response.out.write(result)

class remove_headerlink(BaseHandler):
    def post(self):
        self.check_login()
        name=self.request.get("name")
        #get entity from store
        link=m.Headerlinks.get_by_key_name(name)
        if link: #entity exist
            db.delete(link)          
            result='{"status":"success"}'
        else:  #link not existed
            result='{"status":"failure","reason":"連結不存在!"}'      
        self.response.out.write(result)

下列是導覽區塊管理相關之路徑處理類別 :

class list_navblocks(BaseHandler):
    def get(self):
        self.check_login()
        url="templates/list_navblocks.htm"
        path=os.path.join(os.path.dirname(__file__), url)
        content=template.render(path,{})
        self.response.out.write(content)

class get_navblocks(BaseHandler):
    def post(self):
        self.check_login()
        page=self.request.get("page")
        rows=self.request.get("rows")
        sort=self.request.get("sort")
        order=self.request.get("order")
        if len(page):
            page=int(page)
        else:
            page=1
        if len(rows):
            rows=int(rows)
        else:
            rows=10
        if not len(sort):
            sort="sequence"
        if not len(order):
            order="asc"
        query=m.Navblocks.gql("ORDER BY %s %s" % (sort, order))
        count=query.count()
        blocks=query.fetch(rows, (page-1)*rows)
        rows=[]  #for storing objects
        for b in blocks:
            block={"name":b.name,
                   "title":b.title,
                   "sequence":b.sequence,
                   "display":b.display}
            rows.append(block)
        obj={"total":count,"rows":rows}  #Easyui datagrid json format
        self.response.headers["Content-Type"]="application/json"
        self.response.out.write(json.dumps(obj))

class add_navblock(BaseHandler):
    def post(self):
        self.check_login()
        name=self.request.get("name")
        display=self.request.get("display")
        if display=="True":
            display=True
        else:
            display=False
        #check entity if exist
        block=m.Navblocks.get_by_key_name(name)
        if block: #already exist
            result='{"status":"failure","reason":"導覽區塊已存在!"}'
        else:  #new block
            navblock=m.Navblocks(key_name=name,name=name,
                title=self.request.get("title"),
                sequence=int(self.request.get("sequence")),
                display=display
                )
            navblock.put()
            result='{"status":"success"}'      
        self.response.out.write(result)

class update_navblock(BaseHandler):
    def post(self):
        self.check_login()
        name=self.request.get("name")
        display=self.request.get("display")
        if display=="True":
            display=True
        else:
            display=False
        #get entity from store
        block=m.Navblocks.get_by_key_name(name)
        if block: #entity exist
            block.title=self.request.get("title")
            block.sequence=int(self.request.get("sequence"))
            block.display=display
            block.put()
            result='{"status":"success"}'
        else:  #block not existed
            result='{"status":"failure","reason":"導覽區塊不存在!"}'      
        self.response.out.write(result)

class remove_navblock(BaseHandler):
    def post(self):
        self.check_login()
        name=self.request.get("name")
        #get entity from store
        block=m.Navblocks.get_by_key_name(name)
        if block: #entity exist
            db.delete(block)
            #delete navlinks belong to this navblock
            query=m.Navlinks.all()
            links=query.filter("block_name",name)
            for link in links:
                db.delete(link)
            result='{"status":"success"}'
        else:  #block not existed
            result='{"status":"failure","reason":"導覽區塊不存在!"}'      
        self.response.out.write(result)

下面是導覽列超連結管理相關之路徑處理類別 :

class list_navlinks(BaseHandler):
    def get(self):
        self.check_login()
        blocks=m.Navblocks.all()
        info=[]
        for b in blocks:
            block={}
            block["block_name"]=b.name
            block["block_title"]=b.title
            #block["block_title"]=b.title + " (" + b.name + ")"
            info.append(block)
        url="templates/list_navlinks.htm"
        path=os.path.join(os.path.dirname(__file__), url)
        content=template.render(path,{"info":info})
        self.response.out.write(content)

class get_navlinks(BaseHandler):
    def post(self):
        self.check_login()
        page=self.request.get("page")
        rows=self.request.get("rows")
        sort=self.request.get("sort")
        order=self.request.get("order")
        if len(page):
            page=int(page)
        else:
            page=1
        if len(rows):
            rows=int(rows)
        else:
            rows=10
        if not len(sort):
            sort="sequence"
        if not len(order):
            order="asc"
        query=m.Navlinks.gql("ORDER BY %s %s" % (sort, order))
        count=query.count()
        links=query.fetch(rows, (page-1)*rows)
        rows=[]  #for storing objects
        for h in links:
            link={"name":h.name,
                  "title":h.title,
                  "url":h.url,
                  "target":h.target,
                  "sequence":h.sequence,
                  "block_name":h.block_name,
                  "hint":h.hint}
            rows.append(link)
        obj={"total":count,"rows":rows}  #Easyui datagrid json format
        self.response.headers["Content-Type"]="application/json"
        self.response.out.write(json.dumps(obj))

class add_navlink(BaseHandler):
    def post(self):
        self.check_login()
        name=self.request.get("name")
        #check entity if exist
        link=m.Navlinks.get_by_key_name(name)
        if link: #already exist
            result='{"status":"failure","reason":"連結名稱已存在!"}'
        else:  #new link
            navlink=m.Navlinks(key_name=name,name=name,
                title=self.request.get("title"),
                url=self.request.get("url"),
                target=self.request.get("target"),
                sequence=int(self.request.get("sequence")),
                block_name=self.request.get("block_name"),
                hint=self.request.get("hint")
                )
            navlink.put()
            result='{"status":"success"}'      
        self.response.out.write(result)

class update_navlink(BaseHandler):
    def post(self):
        self.check_login()
        name=self.request.get("name")
        #get entity from store
        link=m.Navlinks.get_by_key_name(name)
        if link: #entity exist
            link.title=self.request.get("title")
            link.url=self.request.get("url")
            link.target=self.request.get("target")
            link.sequence=int(self.request.get("sequence"))
            link.block_name=self.request.get("block_name")
            link.hint=self.request.get("hint")
            link.put()
            result='{"status":"success"}'
        else:  #link not existed
            result='{"status":"failure","reason":"連結不存在!"}'      
        self.response.out.write(result)

class remove_navlink(BaseHandler):
    def post(self):
        self.check_login()
        name=self.request.get("name")
        #get entity from store
        link=m.Navlinks.get_by_key_name(name)
        if link: #entity exist
            db.delete(link)          
            result='{"status":"success"}'
        else:  #link not existed
            result='{"status":"failure","reason":"連結不存在!"}'      
        self.response.out.write(result)

下面是系統設定管理相關之路徑處理類別 :

class settings(BaseHandler):
    def get(self):
        self.check_login()
        info={}  #for storing parameters
        #query settings from datastore
        settings=m.Settings.get_by_key_name("settings")
        if settings: #entity exist
            info["site_title"]=settings.site_title
            info["site_theme"]=settings.site_theme
            info["site_state"]=settings.site_state
        else:
            info["site_title"]=""
            info["site_theme"]="default"
            info["site_state"]="off"
        #query Themes from datastore
        themes=m.Themes.all()
        theme_list=[]
        for t in themes:
            theme_list.append(t.theme)
        info["themes"]=theme_list
        url="templates/settings.htm"
        path=os.path.join(os.path.dirname(__file__), url)
        content=template.render(path,{'info':info})
        self.response.out.write(content)

class update_settings(BaseHandler):
    def post(self):
        self.check_login()
        #get entity from store
        settings=m.Settings.get_by_key_name("settings")
        if settings: #entity exist
            settings.site_title=self.request.get("site_title")
            settings.site_theme=self.request.get("site_theme")
            settings.site_state=self.request.get("site_state")
            settings.put()
            result='{"status":"success"}'
        else:  #entity not existed
            result='{"status":"failure"}'      
        self.response.out.write(result)

最後是根目錄處理類別與路徑處理指派 :

class MainHandler(BaseHandler):
    def get(self):
        #save visitor info first
        ip=self.request.remote_addr
        user_agent=self.request.headers.get("User-Agent")
        # or user_agent=os.environ.get("HTTP_USER_AGENT")
        visitor=m.Visitors()
        visitor.ip=ip
        #adapt UTC to Taipei Time Zone
        visitor.visit_time=datetime.datetime.now() + timedelta(hours=+8)
        visitor.user_agent=user_agent
        visitor.ip_list=list(ip)
        visitor.user_agent_list=list(user_agent)
        visitor.put()
        #check login session
        self.check_login()
        #pass: already login, render main.htm
        info={}  #for storing parameters
        #create param: account, site_title, theme
        account=self.session.get('account')
        info["account"]=account
        s=m.Settings.get_by_key_name("settings")
        info["site_title"]=s.site_title
        theme=self.session.get('theme')
        info["theme"]=theme
        #create param: greeting
        today=datetime.date.today()
        week=[u"一",u"二",u"三",u"四",u"五",u"六",u"日"]
        info["greeting"]=u"您好! " + account + u", 今天是 " + \
            str(today.year) +  u" 年 " + str(today.month) + u" 月 " + \
            str(today.day) + u" 日 星期" + week[today.weekday()]
        #create param: themes
        theme_list=[]
        themes=m.Themes.all()
        for t in themes:
            theme_list.append(t.theme)
        info["themes"]=theme_list
        #create param: headerlinks
        headerlinks=m.Headerlinks.all()
        headerlinks.order("-sequence")  #sort by sequence (reverse)
        link_list=[]  #for storing headerlinks objects
        for h in headerlinks:
            link={}
            link["title"]=h.title
            link["url"]=h.url
            link["target"]=h.target
            link["sequence"]=h.sequence
            link["hint"]=h.hint
            link_list.append(link)
        info["headerlinks"]=link_list
        #create param: navblocks & navlinks
        navblocks=m.Navblocks.all()
        navblocks.filter("display =",True)
        navblocks.order("sequence")  #sort by sequence          
        navblock_list=[]  #for storing navblocks objects
        for nb in navblocks:
            navblock={}
            navblock["title"]=nb.title  #store block title
            #query nvavlinks belongs to this block
            query=m.Navlinks.all()
            navlinks=query.filter("block_name =",nb.name)
            navlinks.order("sequence")
            navlink_list=[]  #for storing navblinks objects
            for nl in navlinks:
                navlink={}
                navlink["title"]=nl.title
                navlink["url"]=nl.url
                navlink["target"]=nl.target
                navlink["hint"]=nl.hint
                navlink_list.append(navlink) #store this link
            navblock["navlinks"]=navlink_list #store block links
            navblock_list.append(navblock)  #store this block
        info["navblocks"]=navblock_list
        url="templates/main.htm"
        path=os.path.join(os.path.dirname(__file__), url)
        content=template.render(path,{'info':info})
        self.response.out.write(content)

config={}
config['webapp2_extras.sessions']={'secret_key':'my-super-secret-key'}
app = webapp2.WSGIApplication([
    ('/', MainHandler),
    ('/login', login),
    ('/check_member', check_member),
    ('/logout', logout),
    ('/hometabs', hometabs),
    ('/home', home),
    ('/change_theme', change_theme),
    ('/list_visitors', list_visitors),
    ('/get_visitors', get_visitors),
    ('/list_members', list_members),
    ('/get_members', get_members),
    ('/remove_visitors', remove_visitors),
    ('/add_member', add_member),
    ('/update_member', update_member),
    ('/remove_member', remove_member),
    ('/list_headerlinks', list_headerlinks),
    ('/get_headerlinks', get_headerlinks),
    ('/add_headerlink', add_headerlink),
    ('/update_headerlink', update_headerlink),
    ('/remove_headerlink', remove_headerlink),
    ('/list_navblocks', list_navblocks),
    ('/get_navblocks', get_navblocks),
    ('/add_navblock', add_navblock),
    ('/update_navblock', update_navblock),
    ('/remove_navblock', remove_navblock),
    ('/list_navlinks', list_navlinks),
    ('/get_navlinks', get_navlinks),
    ('/add_navlink', add_navlink),
    ('/update_navlink', update_navlink),
    ('/remove_navlink', remove_navlink),
    ('/settings', settings),
    ('/update_settings', update_settings)
], debug=True, config=config)

這裡藍色就是有修改的部分, 在 MainHandler 部分因為已利用 check_login() 檢查連線狀態, 因此通過後就直接渲染 main.htm 網頁了.

好了, 以上就是這次為期兩個月玩 GAE 的成果紀錄, 不記下來保證一個月後都忘光光. 檔案全部壓縮封存候用 (下載原始碼(備用下載點). GAE 要暫時 hold 一下, 因為我要開始忙工作日誌了, 希望年底前可以交差.

2016-03-04 補充 :

今天重新審視了一遍程式碼, 又發現了一些 bug 與須改進之處 :

1. 對話框增加取消鈕 :

新增資料對話框增加了 "取消" 按鈕, 按下時會將對話框關閉, 例如新增使用者對話框取消動作處理程式為 :

    $("#cancel_member").bind("click",function(){
      $("#member_dialog").dialog("close");
      });


2. Members 資料表新增 name 欄位, list_members 增加搜尋框.

3. 新增 "使用者設定" 頁籤, 讓使用者可自行改密碼, Email 等.


4. 導覽區塊, 導覽連結, 標頭連結等列表都改為分頁方式顯示.

5. 有搜尋的 Datagrid 重新載入鈕 (reload) 傳送參數 search_field="" :

避免上次搜尋的 search_field 值在 reload 時持續傳出值, 使得無法載入全部資料. 這樣就界定了 datagrid 列表右上角之 reload 為 "全部載入", 而下方分頁工具列上的 reload 為 "現況載入" (之前有搜尋的話會持續).

另外, 我測試了 CDN 供給 Easyui 函式庫 (刪除 jqueryeasyui.htm 這個檔案, 然後將 jqueryeasyui_cdn.htm 複製為 jqueryeasyui.htm), 發現目前已可使用了 (前陣子的 bug 改好了?), 所以可以把 static 目錄下的 easyui 資料夾刪除了, 這樣壓縮後的 zip 只有 33K 而已, 如果自備 Easyui 函式庫的話, 壓縮後就要 814K, 還真是大.

# 更新版下載