2014年2月28日 星期五

新浪微博相片下載程式

Sina Blog Image Downloader

昨晚打開 Sita 的 iPhone App,發現所有相片、影片都消失了。看來開發商的服務已經結束。如果當日太陽娛樂找我開發,我一定會把 App 營運下去。幸而早前已把內容保存起來。

在 App 內周圍瀏覽,無意中進入了 Sita 的新浪微博,無意中發現有些相片不曾在 Facebook 及 Instagram 出現。於是今天寫了個簡單的 Javascript + PHP 程式一口氣把所有相片下載回來,保存在服務器內。我的《陳僖儀補完計劃》又多了一點內容。

2014年2月26日 星期三

5DPrint 無法連接 MakiBOX

MakiBOX Serial Issue

MakiBOX 的預設打印程式是《5DPrint》,一個 Chrome App。昨晚花了四個小時完成裝嵌後因為太倦,測試留待今日進行。MakiBOX 接上電源及 USB 線後,打開 5DPrint 出現「No device」,分別重啟 MakiBOX 及電腦也無法解決問題。換另一條 USB 線,情況依舊。打開裝置清單,發現機是連接了。問題出在 5DPrint 上。在官方討論區求救,懷疑是 Chrome 版本更新了導致 5DPrint 連接失敗,但已是一個星期前的事。5DPrint 沒有任個更新、沒有任何說明、沒有任何安排。網友發現《Repetier-Host》能用,於是嘗試,成功了。

第一次打印,噴頭不停地動,打印床出現一些紋理,可是沒有太多材料被擠出。原來紋理是因為高溫的噴頭把膠製的打印床整溶了 (T_T)!頂!原來打印床位置過高(說明書沒有提及適當的位置)。

立即調校一下打印床的高度後再印。成功了...一半。這次是太高,導致線曲曲的,不直。上下層也粘得不好。得到高人指點,正確方法是在打印床的四角放置一張卡片,把床貼近噴頭而不會拖動卡片。完成調整後第三次打印。


一會兒,噴頭在動但沒有材料噴出。發現上料的馬達不停前轉後倒退,不知問題為何。只好把這個部份拆開重裝,發現材料被馬達弄得爛爛了,可能就是這個原因而上不到料。



修理後似第四次打印,料在噴,舊的問題看來解決了;但新的問題又來。料雖然在噴,但不是向下直流,而是粘在噴咀,沒有粘到打印床。就算清潔過後,問題仍在。實在太累了,得休息休息!

2014年2月25日 星期二

MakiBOX A6 LT 入手


2013 年 7 月,讀到一篇 MakiBox A6 評論後,滿以為快將出貨(因朋友說過等出貨已有一段時間,快想退訂)。向 MakiBox 工作人員查詢,知道新的訂單能在 8 月開始生產、9 月附運,於是我毫不㥢疑地訂購了。一個夢魘的開始。

原本 9 月出貨,未果。客服指要完成比我早的訂單,要遲兩星期。兩星期又兩星期。改為 10 月。MakiBox 沒有說明是 10 月哪個時間,只好等。10 月過去了,仍然沒有出貨。唯有寄望能在 11 月生日前出貨,作為生日禮物。客服指快出貨了,兩個星期後。可惜幻滅了。12 月中,訂單狀態改為 PRODUCTION,心想兩個星期能造好吧!期望能在聖誕前到,作為聖誕禮物。可是繼續落空了。今次則說要改善流程能令生產加快,又遲多兩個星期。盼望能在 1 月春節前收到,結果沒有。發了電郵到客服要求退款。隔了兩星期沒有回覆,再發。再沒有回覆。朋友叫我到消費者委員會投訴。我沒有。我希望 MakiBox 作為香港的 Startup 能造出好的產品。遲不緊要,重要的是要有個交待。2 月 17 日,訂單狀態改為 PACKING,還有一點盼望能兩三天能完成包裝工程。到了 2 月 21 日,終於收到電郵通知我的 MakiBox 生產好了。今天,MakiBox 到手了,終於圓了一件事。

對於 MakiBox 這樣的態度,我很憤怒。想像一下,客人付了全數,但貨一直沒有出。沒有交代,只懂不停找借口,承諾了的限期沒有兌現。基本上是說謊。要求退款又置之不理。幸而,最後仍然負責任地完成生產及出貨。花了四小時才完成裝嵌,原因是說明書表達得不夠清楚。砌好了也沒有指引說明如何調校及測試。朋友要我推薦的 3D 打印機的話,不會是 MakiBOX。

2014年2月21日 星期五

拉闊圖書館・十四:正義

BeyondZ Library 14

前天舉行了第十四課《拉闊圖書館》,由高老闆分享 Michael Sandel《正義・一場思辨之旅》的讀書心得。由於《拉闊圖書館》是由我在公司提出,所以每課的安排工作也落在我身上。早在一星期前收到高老闆的介紹之後,已急不及待地買了這本書回來。看完第一章,故事是挺吸引,不過花在思索的時間也較多,故看得較慢。每當我看這類需要思考的書時,大腦特別容易疲倦,看一看便要小休一下...。

2014年2月20日 星期四

第 1000 次組譯


雖然《AMIGO Controller 2.00》是重新製作,但有很多片段都可以取材自 1.10 版本。不過,2.00 也不經不覺地 Build 了 1000 次,可想而知有很多改良了的地方。


目前的開發進度是加入 Dropbox 及 Google Drive 的動作資料上下載支援。繼 Dropbox 崩潰事件後,再出現 Dropbox 403 錯誤。全因多手把原本在 Dropbox 內的 Apps 目錄刪除所導致。幸而這些問題都一一解決。要是處理完 Google Drive 的話,雲端支援亦告一段落。

2014年2月19日 星期三

Desk Balancer

Desk Balancer from pacess on Sketchfab.


家中床頭的橫板是我的工作枱。由於安裝不良,導致向前傾斜。然而在 MakiBox 訂了八個月(原本只須兩個月時間)的 3D 打印機快將出貨,於是預先製作一個用來修補平衡的部件,希望在打印機來到時能立即進行製作。

2014年2月18日 星期二

Sita 陳僖儀紀念會


Sita 的生日快到了。陳家選擇在她生日前夕辦一個「Sita 紀念會」。詳情可到 Sita Facebook 瀏覽:
https://www.facebook.com/photo.php?fbid=658112384223941&set=a.205616162806901.44528.205610916140759&type=1&theater

2014年2月17日 星期一

網絡爬蟲


上星期看新聞,認識到「網絡爬蟲」這種東西原來這麼勁,似乎是很好玩的東西。之前對它只有很初步的概念,在網上看了相關資料,認識較深了。受到網友的啟發,我嘗試以 PHP + MySQL + Cronjob 編寫一隻很基本的「網絡爬蟲」替我抓取 Sita 的內容。用 PHP 是因為很多東西都在 PHP 上試過,開發較快;MySQL 則是用來儲存找到的數據;至於 Cronjob,考慮到不想太過消耗寬頻,以免影響日常運作。反正工作不趕急,慢慢抓也不壞。

每筆數據都會被評分,分數是來自「關鍵字」的出現數量。分數越高的網址會優先進行分析及擴散。找不到「關鍵字」則會被扣分,連累到由這顯生出來的網址。若「關鍵字」出現的網頁內容也會同時被儲存下來,方便進一步處理。

Website Found Website Done Website Waiting
Busy Busy Busy

2014年2月16日 星期日

修正 BLE mini 的問題

Tri-Robot 初號機在本周回到我的懷抱,是時候讓它跟《AMIGO Controller 2.00》同步。連接十分成功,可是數據收發不太穩定。一時成功,一時失敗。用《AMIGO Controller 1.10》卻次次成功。一定是 2.00 中出了甚麼問題。

多次測試後發現每逢初次連接時必定會出現問題,於是把首次及二次連接的記錄找來對比一下。原來首次連接後沒有調用 enableReadNotification 程序。追查下去才知是因為讀不到裝置名稱而判斷失敗。這是 BLE mini 自身的問題,在 HM-10 上沒有這個情況。以下是修改後的代碼:
if ([peripheralName isEqualToString:BLEMANAGER_NAME_BLEMINI] == YES || [peripheralName length] == 0)  {
 CBUUID *serviceUUID = [CBUUID UUIDWithString:BLEMANAGER_BLEMINI_UUID_SERVICE];
 CBUUID *readUUID = [CBUUID UUIDWithString:BLEMANAGER_BLEMINI_UUID_RX];
 [self notification:serviceUUID characteristicUUID:readUUID peripheral:peripheral on:YES];
 return;
}

2014年2月15日 星期六

Dropbox Crash


為《AMIGO Controller 2.00》加入 Dropbox 支援,基本上是把 1.10 的代碼拷過來就是,不幸地當連接 Dropbox 時卻彈出「-[__NSCFString objectForKey:]: unrecognized selector sent to instance 0x9782d00」錯誤然後系統崩潰。一直以為代碼出事,找了兩小時也沒有頭緒。最後找到是 Plist 中的「URL types」的「Item 0」錯誤地儲存數值「db-xxxxxxxxxx」。「Item 0」這個數值應該是「URL Schemes」,而「URL Schemes」內的「Item 0」才是「db-xxxxxxxxxx」。簡單來說是遺漏了一層而導致問題出現...。

2014年2月14日 星期五

給太太的情人節禮物


最新在 Facebook 分享了「美國肯塔基州參議院通過法例,讓孩子可以不讀外語,學寫程式」及「Singapore plans to introduce programming lessons in public schools to boost the economy」兩篇新聞。朋友好奇為何我這麼支持孩子學習編程。以下是我的想法:

我想法很簡單,因為懂得編程能填補自身能力的不足。例如我送給太太的情人節禮物,是由扭計骰拼成圖案。我不懂得玩扭計骰。但透過編寫程式,讓我教撓電腦扭計骰的基本操作,再讓電腦指引我如何去扭出圖案。要是不懂編程的話,可能要花很多時間摸索、又或者找懂的朋友幫忙。也很大機會會放棄。除此之外,當年自學編程時,學到的英文詞彙增加,加強日後理解英文文章的能力,都是一個好的副作用。

孩子的創意無限,假如他們能學習編程,就有給多加一件工具,提高讓他們把想法成真的機率。我相信能令社會變得更加美好。當然,如同術數的哲學所講,當把世界一分為二時:有好的東西出現,壞的東西也會跟著來。到時也會讓「怪獸家長」多一個催谷孩子的題目。不過,這是人為的社會因素。學習編程這件事情本身是美好的。

2014年2月13日 星期四

InnoDB 寫入困難症

Error Writeing Data into InnoDB

打算編寫《AMIGO Controller 2.00》的服務器部份,作新的嘗試。把 CentOS PHP 接口收到的數據,經內聯網寫入 QNAP 的 MySQL 中。前天編寫的《WeatherGrabber》已能成功跟 MySQL 連線並讀取內容,心想這樣的工作像舉手般容易,誰知搞了一晚都無法突破。

問題出在寫入方面。把數據寫入 MySQL 後,affected_rows 傳回 1 表示一項數據已被寫入。可是在 phpMyAdmin 中卻找不到那項數據。代碼一直源用都沒有問題,只是 MySQL 由同一台機轉到另一台。向朋友求教後,估計是 InnoDB 的問題。今次在 MySQL 中嘗上使用 InnoDB 格式,看看較能會否較好。查找過後,發現把代碼「$this->sqlDatabase->autocommit(false);」中的「false」改為「true」便能解決問題!

2014年2月12日 星期三

《AMIGO Controller》開發記錄:多重連線


每天都堆頭苦趕,希進能盡快完成《AMIGO Controller 2.00》推出市場。新版本其中一個特色是能同時對多部 Tri-Robot DMP 發出指令,方便進行團體動作,如:操兵、跳舞...等。目前連接不太穩定,有時接收不到機體發出的訊號,而機體也收不到來自 iPad 的指令。還未知在哪裡出錯,看來要點時間除錯...。

2014年2月11日 星期二

在 iTunes Connect 轉移 App


最近打算把我的 iOS 作品移到另一個帳號。一直都認為這是有可能的,只是不知要找 Apple 那個部門處理。今天在 iTunes Connect 內卻發現到這個項目,並成功把項目轉移。這時想把更多作品匯聚回單一帳號,不過事情並不順利。一個已停用多年的帳號內有一些不錯的應用,使用相同的方法轉移時卻要同意最新的條款;而網頁上找不到同意這些條款的地方,原因是帳號已過期。需要付出年費 US$99 才會出現...。我也隨即放棄了。

2014年2月9日 星期日

《AMIGO Controller》開發記錄:動作編輯畫面(六)


這就是「動作組」大概的操作方式。

2014年2月8日 星期六

《AMIGO Controller》開發記錄:動作編輯畫面(五)


大女身體不適,晚上又在家附近團拜,於是整天待在家中開發《AMIGO Controller 2.00》。花了六個小時編寫,動作編輯器已經完成大部份功能,錯也除得七七八八,只欠「動作組」便正式完成。

2014年2月7日 星期五

實現 PHP 下的 RESTful


上一個替客人開發的流動應用程式首次運用了 Ruby on Rails 及 RESTful 概念。由朋友把它有進公司,當時才初歲了解過是甚麼東東。新的項目剛剛展開,我希望後台繼續使用 RESTful 的設計。由於時間緊迫,未能及時學懂 Ruby on Rails,所以嘗試在 PHP 下把它實現。在網上找到 PHP 版的 RESTful 示範,簡單修改一下方便自己使用。

RESTful.inc
//========================================================================================
class RESTLoader  {
    private $control;
    private $segments;

    //----------------------------------------------------------------------------------------
    function __construct($Controller)  {
        $this->control = new $Controller;
    } 

    //----------------------------------------------------------------------------------------
    function run()  {
        if (PHP_SAPI == "cli")  {
            global $argv;
            $_SERVER["PATH_INFO"] = "/".implode("/", array_slice($argv, 1));
        }

        if (!isset($_SERVER["PATH_INFO"]) or $_SERVER["PATH_INFO"] == "/")  {
            $this->segments = false;
        }  else  {
            $this->segments = explode("/", $_SERVER["PATH_INFO"]);
        }

        if (!$this->segments)  {
            //  Without parameter
            return $this->control->index(); 
        }

        if (method_exists($this->control, $this->segments[1]))  {
            //  Request resource by traditional way
            $method = $this->segments[1];

            //  Deny directly invoke method of interface RESTMethod
            if (strpos($method, "rest") === 0)  { 
                header("HTTP/1.0 403 Forbidden");
                echo("The server understood the request, but is refusing to fulfill it. Authorization will not help and the request SHOULD NOT be repeated.");
                return false;
            }

            $objMethod = array($this->control, $method);
            $arguments = array_slice($this->segments, 2);

            call_user_func_array($objMethod, $arguments);
        }  else  {

            //  Request resource by RESTful way.
            $method = "rest".ucfirst(strtolower($_SERVER['REQUEST_METHOD']));
            $arguments = array_slice($this->segments, 1);
            $this->control->$method($arguments);
        }
    }
}

user.php
require("RESTLoader.inc");
require("user.inc");

//========================================================================================
header("Content-type: application/json");
header("Cache-Control: no-cache, must-revalidate");
header("Expires: Tue, 07 Nov 2000 00:00:00 GMT");

$loader = new RESTLoader("User");
$loader->run();

user.inc
//----------------------------------------------------------------------------------------
interface RESTMethod  {
    public function restGet($segments);
    public function restPost($segments);
    public function restPut($segments);
    public function restDelete($segments);
}

//========================================================================================
class User  {

    //----------------------------------------------------------------------------------------
    public function index()  {
        $result = "create|read|update|delete";
        echo(json_encode($result));
    }

    //----------------------------------------------------------------------------------------
    //  RESTful: controller/id  by POST.
    //  traditional: controller/create
    public function create()  {
        $result = "Create new user without name";
        echo(json_encode($result));
    }

    //----------------------------------------------------------------------------------------
    //  RESTful: controller/id  by GET.
    //  traditional: controller/id or controller/read/id
    public function read($id)  {
        $result = "Read user ".$id;
        echo(json_encode($result));
    }

    //----------------------------------------------------------------------------------------
    //  RESTful: controller/id  by PUT.
    //  traditional: controller/update/id  with POST data.
    public function update($id=false)  {
        if (!$id)  {return $this->create();}
        $result = "Update user ".$id;
        echo(json_encode($result));
    }

    //----------------------------------------------------------------------------------------
    // RESTful: controller/id  by DELETE.
    // traditional: controller/delete/id
    public function delete($id)  {
        $result = "Delete user ".$id;
        echo(json_encode($result));
    }

    //----------------------------------------------------------------------------------------
    //  Calling from RESTful method
    //----------------------------------------------------------------------------------------
    public function restPost($segments)  {
        return $this->create();
    }

    //----------------------------------------------------------------------------------------
    public function restGet($segments)  {
        return $this->read($segments[0]);
    }

    //----------------------------------------------------------------------------------------
    public function restPut($segments)  {
        return $this->update($segments[0]);
    }

    //----------------------------------------------------------------------------------------
    public function restDelete($segments)  {
        return $this->delete($segments[0]);
    }
}

為了讓 .php 在網址內消失,需要利用 .htaccess 達成:
##  Replace /*/* to /*.php/*
RewriteEngine On 
RewriteRule ^([a-z]*)/(.*)$ $1.php/$2

2014年2月6日 星期四

利用 Javascript 讀取系統訊息


過往為客人開發的後台介面都是 WEB 形式處理,不時都會出現問題,尤其是遇上可惡的 Internet Explorer。今次為新客人開發後台,除了要提升畫面質素外,我也特別多花心思在用戶體驗,同時也希望程序運作得順暢。要解決問題,有時需要提供系統的版本及相關資料;客人往往都未必知道如何取得。有見及此,我嘗試利用 Javascript 來讀取系統資料,顯示在後台內。日後客人只需要「Copy & Paste」內容給我就可以。

2014年2月5日 星期三

《AMIGO Controller》開發記錄:動作編輯畫面(四)


繼續開發《動作編輯畫面》餘下的功能。複製姿勢、刪除姿勢、全選姿勢、不選姿勢,通通都加入了。不過目前只限「姿勢」,稍後會對應「動作組」的編輯工作。雖然畫面看起來差不多,但還是有很多東西需要編寫,才能完善整個《AMIGO Controller》。

2014年2月4日 星期二

《AMIGO Controller》開發記錄:動作編輯畫面(三)


春節假期我沒有偷懶,帶著 MacBook Pro 四處拜年兼寫 Code。動作編輯畫面真的很多東西需要處理,介面已經長得像樣了。往後都是編寫很多很多細微的地方。當中還沒想好的是「動作組合」跟「姿勢」如何在同一條時間軸上能有簡潔的處理方式。想到頭都大了。