2016年4月30日 星期六

啟動 Kali Linux 至 Console

安裝在 Raspberry Pi 2 的 Kali Linux 無故出現問題,導致雖然成功開機,但機乎所有指令都用不到,唯有花一個多小時重新安裝。新版的 Kali Linux 2.1.2 在啟動後會直入 LightDM,而我希望直入 Console。需要另行設定。上次做過沒有記錄下來,今次又要重新找過方法。所以今次要記錄在案,方便日後使用。

直入 Console
systemctl set-default multi-user.target

直入 LightDM
systemctl set-default graphical.target

2016年4月29日 星期五

WiFi Lamp 展示板


今次在 Maker Hive 的《Meet the Makers》活動是跟華輝合作。所以 WiFi Lamp 會華輝的攤位展出。為了更容易吸引觀眾,我特地設計了一塊展示板。希望打印出來的效果能有好的質素。

2016年4月28日 星期四

Facebook Messenger 機械人


早在微訊能開發對話機械人時,我已經很想嘗試。但公司及個人都申請不到微訊公眾帳號,被拒諸門外。現在,Facebook 終於加入這樣的功能,當然即刻試試。

按照官網的指示,需要一個 Facebook 應用程式及專頁來作為引子。我隨手拿來 N 年前建立了的應用程式來試。專頁則用 Tri-Robot。範例是 Node.js,不懂。自行以 PHP 編寫。Facebook Messenger 機械人使用 Webhooks 設計,每當有用戶發出訊息時,設定好的 Webhooks 地址會收到一個 JSON 封包;當中儲存著專頁號碼、寄件人 Facebook 編號、訊息內容、發出時間、順序號碼...等。只要設定 Webhooks 一次,自己的所有專頁都能使用。只是要在 Webhooks 程式內分辨訊息是來自哪個專頁。寫好後進行測試,卻發現沒有任何回應。試了幾次同樣結果,改用另一個專頁。今次成功了。改回 Tri-Robot,又沒有反應...。用第二個 Facebook 帳號測試,原本有反應的又變成沒反應。激氣!似乎只有自己能進行測試,還未對外公開?

2016年4月27日 星期三

Arduino 端的模擬訊號

為了在 Android 端能接收到不斷變化的數據,加上老師希望我能教學生在 Android 程式經藍芽讀取溫度數據,於是乎編寫了以下隨機傳回 15 至 25 度的數值。務求在 Android 端能模擬到像真的數據。
//------------------------------------------------------------------------------
//  Send Signal from Bluetooth for Android App Test
//------------------------------------------------------------------------------
//  Platform: Arduino UNO + ITEAD Wireless Bluetooth Shield Module Starter Kit
//  Written by Pacess HO
//  Copyright 2016 Pacess Studio.    All rights resvered.
//------------------------------------------------------------------------------

//------------------------------------------------------------------------------
#include <SoftwareSerial.h>

//------------------------------------------------------------------------------
SoftwareSerial mySerial(6, 7); // RX, TX

int _bufferIndex = 0;
byte _buffer[64];

int _count = 0;
int _major = 25;
int _minor = 0;

char _printBuffer[16];

//------------------------------------------------------------------------------
void setup()  {
    randomSeed(analogRead(0));

    //  Open serial communications and wait for port to open:
    Serial.begin(38400);

    //  Wait for serial port to connect. Needed for native USB port only
    while (!Serial);

    //  Please change to DAT mode on the shield
    mySerial.begin(9600);

    _count = 0;
    _bufferIndex = 0;
    Serial.println("Signal-BT is ready...");
    Serial.println("Copyright 2016 Pacess Studio.  All rights resvered.");
}

//------------------------------------------------------------------------------
void loop()  {
    if (_count++ > 20000)  {
        _count = 0;

        int value = random(100);
        if (value < 10)  {
            if (_minor > 0)  {_minor--;}
            else if (_major > 15)  {
                _major--;
                _minor = 9;
            }
        }    else if (value < 20)  {
            if (_minor < 9)  {_minor++;}
            else if (_major < 25)  {
                _major++;
                _minor = 0;
            }
        }

        sprintf(_printBuffer, "%d.%d\r\n", _major, _minor);
        Serial.write(_printBuffer);
        mySerial.write(_printBuffer);
    }

    if (mySerial.available())  {
        Serial.write(mySerial.read());
    }

    if (Serial.available())  {
        byte value = Serial.read();
        _buffer[_bufferIndex] = value;
        _bufferIndex++;

        if (value == '.')  {
            _buffer[_bufferIndex-1] = '\n';
            _buffer[_bufferIndex] = '\n';

            for (int i=0; i<=_bufferIndex; i++)  {
                value = _buffer[i];
                Serial.write(value);
                mySerial.write(value);
            }
            Serial.write("#");
            _bufferIndex = 0;
        }
    }
}

2016年4月26日 星期二

準備 Android 第三課


過去兩星期都是星期三左右才開始備課,但今個星期不行。因為本周將要講解 Android + Bluetooth 2.0 的編程方法,而我還沒有做過這樣的事情,要努力學習。

可是,麻煩的事總會來到。學校老師給我的 Arduino Uno + 藍芽附加板被人改名為 hello,同時密碼也被改了,不是 0000 也不是 1234。要是跟 Android 配對不來,又怎能繼續下一步的編程工作?附加板沒有說明書,幸好還有一個名稱。照著名稱找 Google 老師幫忙,終於找到了賣家的資料網頁。附加板能以 AT 指令更改設定。原本的以 Arduino 的串口範例來做設定,只要在 Serial Monitor 輸入指令,能經 Arduino 的串口傳送到附加板的串口。不論我怎樣去試,也沒有回應,也沒有效果。我只好改變設計,直接把 AT 指令寫到 Arduino 程式內,然後由 Arduino 直接跟附加板做設定。這招好像有效。名稱雖然還是 hello,但密碼卻更換成 1234。我終於可以繼續 Android 方面的編程...。
//------------------------------------------------------------------------------
//  Reset Bluetooth Settings
//------------------------------------------------------------------------------
//  Platform: Arduino UNO + ITEAD Wireless Bluetooth Shield Module Starter Kit
//  Written by Pacess HO
//  Copyright 2016 Pacess Studio.  All rights resvered.
//------------------------------------------------------------------------------

//  This program is to reset Bluetooth password to 1234
//  1. Remove shield from Arduino UNO
//  2. Power up Arduino UNO
//  3. Upload this program to Arduino UNO
//  4. Power off Arduino UNO
//  5. Set CMD mode on shield
//  6. Set D0 for TX pin and D1 for RX pin on shield
//  7. Power up Arduino UNO

//------------------------------------------------------------------------------
void setup()  {
  //  Open serial communications and wait for port to open:
  Serial.begin(38400);

  //  Wait for serial port to connect. Needed for native USB port only
  while (!Serial);

  //  Please change to CMD mode, D0 for TX pin and D1 for RX pin
  Serial.println("AT+PSWD=1234");
  Serial.println("AT+NAME=Android-BT");
}

//------------------------------------------------------------------------------
void loop()  {
}

2016年4月25日 星期一

繼續 AMIGO Arm 開發


同時間重拾 AMIGO Arm 及 AMIGO Cubebot 的開發,真的很花時間。不過,如果能完成兩部機體,相信會十分滿足。

早上畫了 AMIGO Cubebot 平台部件,成功輸出;晚間轉到 AMIGO Arm 程式開發。重讀一年前計算好的「Inverse Kinematics」,把它填入各關節的尺寸。先編寫 Javascript 程式按輸入的座標來計算四個伺服馬達的角度。用 Javascript 是方便快捷,待測試成功後便在 Arduino 端重寫一次。以是下 Javascript 代碼:
<html>
    <head>
        <script>
            var _pi = 3.141592654;

            //  All distance are in mm
            var _screenWidthMM = 148;
            var _screenHeightMM = 196;

            var _aArmDistanceMM = 143;                        // a
            var _bArmDistanceMM = 143;                        // b
            var _penAndJointDistanceMM = 42;                // c
            var _baseAndJointDistanceMM = 88;                // Z1
            var _penAndSurfaceDistanceMM = 40;                // Z2

            function radians2Degree(radians)  {
                var degrees = radians*(180/_pi);
                return degrees;
            }

            function calculate()  {
                var x = document.getElementById("x").value;
                var y = document.getElementById("y").value;

                //  X = 0 = Horizontal center
                x -= (768/2);

                var gamma = radians2Degree(Math.atan2(x, y));

                var verticalDistance = ((y*_screenHeightMM)/1024)-_penAndJointDistanceMM;
                var penJointAndBaseJointHeightMM = (_baseAndJointDistanceMM-_penAndSurfaceDistanceMM);

                //  L
                var baseJointAndTouchPointSlopeMM = Math.sqrt(Math.pow(verticalDistance, 2)+Math.pow(penJointAndBaseJointHeightMM, 2));

                var aSquare = Math.pow(_aArmDistanceMM, 2);
                var bSquare = Math.pow(_bArmDistanceMM, 2);
                var lSquare = Math.pow(baseJointAndTouchPointSlopeMM, 2);

                var alpha1 = Math.acos((lSquare+aSquare-bSquare)/(2*baseJointAndTouchPointSlopeMM*_aArmDistanceMM));
                var alpha2 = Math.acos((_baseAndJointDistanceMM-_penAndSurfaceDistanceMM)/baseJointAndTouchPointSlopeMM);
                var alpha = radians2Degree(alpha1+alpha2);

                var beta = radians2Degree(Math.acos((aSquare+bSquare-lSquare)/(2*_aArmDistanceMM*_bArmDistanceMM)));

                var delta = (alpha+beta)-90;

                document.getElementById("output").innerHTML = 
                    "Vertical distance: "+verticalDistance+" mm"+
                    "<br>L: "+baseJointAndTouchPointSlopeMM+" mm"+
                    "<br>Alpha: "+alpha+" degrees"+
                    "<br>Beta: "+beta+" degrees"+
                    "<br>Delta: "+delta+" degrees"+
                    "<br>Gamma: "+gamma+" degrees";
            }
        </script>
    </head>
    <body>
        Please enter a coordinate within 768x1024
         <br>X:<input id="x" value="768">
         <br>Y:<input id="y" value="1024">
         <br><button onclick="calculate();">Calculate</button>
         <br><div id="output" style="margin-top:10px;"></div>

        <script>calculate();</script>
    </body>
</html>

試過不同座標,角度值都看似正確;於是把程序轉換成 Arduino Nano 的 C 語言。以相同座標輸入,得出了不同的答案。原因是 Arduino Nano 這些小板,在處理變數類型時,數值範圍會來得細小。把一些出現較大數值的地方改為 double 或 long 後,再在某些運算中標明使用 double 後,最終得出跟 Javascript 端相同的結果。現在連 Arduino 端也準備好,下一步就是測試及調校了。希望能趕及 5 月 5 日展出!

2016年4月24日 星期日

AMIGO Cubebot


AMIGO Cubebot 的原意是用來當作生產線,替我輸出扭好的圖案。我本來是把這個概念當成一盤生意,所以成品價錢不能太高,用的魔方越細小越輕越好。至於每粒魔方所花的時間是不用考慮。我的策略是機器成本要平,就算慢,最多製作幾台同時運作。然而,今次製作的機器版本是展出之用,我們不希望觀眾等得太久,越快輸出圖案越好。細小的魔方較大的更容易卡著,需要更大的扭力,甚至乎是因為體積細小,導致出血位要非常精確。以目前立體打印機 ArrayZ 的情況來看,達不到要求的精細度。於是我決定改用大魔方。今早畫好平台部件,成功打印。之後便是最傷腦筋的硬件機器結構設計...。

2016年4月23日 星期六

Chrome JS Injection


昨天讀過「滲透 Facebook 的思路與發現」,真的眼界大開!今天突然閃過一個概念「究竟 Chrome 的 Console 是否有能力把網頁的 Javascript 作 run-time 修改?」。Google 了一下指「不行」。但我抱著好奇的心態決定一試,得出驚訝的結果。

我先在網頁服務器編寫一個簡單的 HTML。內容是容許用戶輸入一個電印地址,然後以 JS 來桃查格式是否正確。代碼如下:
<html>
    <head>
        <script>
            function isEmail(email)  {
                var regex = /^([a-zA-Z0-9_.+-])+\@(([a-zA-Z0-9-])+\.)+([a-zA-Z0-9]{2,4})+$/;
                return regex.test(email);
            }

            function submitNow()  {
                var email = document.getElementById("email").value;
                var result = isEmail(email);
                document.getElementById("output").innerHTML = result;
            }
        </script>
    </head>
    <body>
        Please enter a valid email address:
        <br><input id="email"><button onclick="submitNow();">Check</button>
        <br><div id="output"></div>
    </body>
</html>
打開 HTML 頁面,試過功能正常,進入 Injection 戲肉。在 Chrome Console 內,如圖所示先調用 isEmail("abc@cde.com"); 得出 true 正確結果。之後以 function isEmail(input) {return false;} 把功能取諦,再用相同的 isEmail("abc@cde.com"); 指令執行一次。今次出現 false,意味著 isEmail 功能已被修改。回到 HTML 頁面,輸入正確的電郵格式,點「Check」同樣傳回 false。確認了 isEmail 功能不止在 Console 被改,連實際網頁運作也被調換了。

讓我驚訝的是,這樣的取諦能成功的話,意味著系統更容易出現漏洞。如例子中的電郵格式檢查,只要編程人員信任客戶端由 JS 傳來的電郵格式是正確無誤,後台沒有再作檢查的話,這兒很大機會可以實現 PHP Injection,從而加入 Webshell 及其他程式碼,進行下一步攻擊。不過,今時今日,要是服務器端沒有好好檢查輸入的資料,就算 Chrome Console 沒有這個功能也很容易出現問題...。

2016年4月22日 星期五

管治問題

公司在科學園的投資出了狀況。當初我有份面見及聘請的同事,今日離職了。離職,本來沒甚麼大不了,只是他不是個別事件。上周五一位,本周五一位,下周五也有一位,更是勞苦功高,公司最資深的員工。較早前也有其他同事相繼離開,我也是徹退的一員,很理解他們的想法。一個又一個的離開,反應出嚴重的管治問題。現在香港辦公室已經空空如也,更加有遷移大陸的藉口。不過,遷移大陸之後就能解決問題?科學園 OK?我看未必。原本資深員工不就是大陸人麼?他那麼拼勁也最終選擇離開,不對症下藥的話只會陸續有來。希望那邊的當家回頭是岸,著數賺盡只會身敗名裂。

有時不需要自己犯錯,從別人的錯誤中學習也很有得著。

2016年4月21日 星期四

解決 (EE) no screens found 問題

root@kali:/etc/X11/xorg.conf.d# cat /var/log/Xorg.0.log
[   201.991] 
X.Org X Server 1.18.3
Release Date: 2016-04-04
[   201.992] X Protocol Version 11, Revision 0
[   201.992] Build Operating System: Linux 3.16.0-4-armmp-lpae armv7l Debian
[   201.992] Current Operating System: Linux kali 3.18.5-v7+ #1 SMP PREEMPT Fri Feb 6 23:06:57 CET 2015 armv7l
[   201.993] Kernel command line: dma.dmachans=0x7f35 bcm2708_fb.fbwidth=1824 bcm2708_fb.fbheight=984 bcm2709.boardrev=0xa01041 bcm2709.serial=0x18cd2f9 smsc95xx.macaddr=B8:27:EB:8C:D2:F9 bcm2708_fb.fbswap=1 bcm2709.uart_clock=3000000 bcm2709.disk_led_gpio=47 bcm2709.disk_led_active_low=0 vc_mem.mem_base=0x3dc00000 vc_mem.mem_size=0x3f000000  dwc_otg.fiq_fix_enable=2 kgdboc=ttyAMA0,115200 console=tty1 root=/dev/mmcblk0p2 rootfstype=ext4 rootwait rootflags=noload net.ifnames=0
[   202.018] Build Date: 05 April 2016  07:04:24AM
[   202.026] xorg-server 2:1.18.3-1 (http://www.debian.org/support) 
[   202.034] Current version of pixman: 0.33.6
[   202.050]  Before reporting problems, check http://wiki.x.org
 to make sure that you have the latest version.
[   202.050] Markers: (--) probed, (**) from config file, (==) default setting,
 (++) from command line, (!!) notice, (II) informational,
 (WW) warning, (EE) error, (NI) not implemented, (??) unknown.
[   202.082] (==) Log file: "/var/log/Xorg.0.log", Time: Wed Apr 20 13:19:10 2016
[   202.091] (==) Using system config directory "/usr/share/X11/xorg.conf.d"
[   202.093] (==) No Layout section.  Using the first Screen section.
[   202.093] (==) No screen section available. Using defaults.
[   202.093] (**) |-->Screen "Default Screen Section" (0)
[   202.093] (**) |   |-->Monitor ""
[   202.094] (==) No device specified for screen "Default Screen Section".
 Using the first device section listed.
[   202.094] (**) |   |-->Device "Allwinner A10/A13 FBDEV"
[   202.094] (==) No monitor specified for screen "Default Screen Section".
 Using a default monitor configuration.
[   202.094] (==) Automatically adding devices
[   202.095] (==) Automatically enabling devices
[   202.095] (==) Automatically adding GPU devices
[   202.095] (==) Max clients allowed: 256, resource mask: 0x1fffff
[   202.095] (WW) The directory "/usr/share/fonts/X11/cyrillic" does not exist.
[   202.095]  Entry deleted from font path.
[   202.095] (==) FontPath set to:
 /usr/share/fonts/X11/misc,
 /usr/share/fonts/X11/100dpi/:unscaled,
 /usr/share/fonts/X11/75dpi/:unscaled,
 /usr/share/fonts/X11/Type1,
 /usr/share/fonts/X11/100dpi,
 /usr/share/fonts/X11/75dpi,
 built-ins
[   202.095] (==) ModulePath set to "/usr/lib/xorg/modules"
[   202.095] (II) The server relies on udev to provide the list of input devices.
 If no devices become available, reconfigure udev or disable AutoAddDevices.
[   202.095] (II) Loader magic: 0x76fc1f68
[   202.095] (II) Module ABI versions:
[   202.095]  X.Org ANSI C Emulation: 0.4
[   202.095]  X.Org Video Driver: 20.0
[   202.095]  X.Org XInput driver : 22.1
[   202.096]  X.Org Server Extension : 9.0
[   202.102] (++) using VT number 1

[   202.115] (II) systemd-logind: took control of session /org/freedesktop/login1/session/c1
[   202.115] (II) no primary bus or device found
[   202.116] (II) LoadModule: "glx"
[   202.117] (II) Loading /usr/lib/xorg/modules/extensions/libglx.so
[   202.141] (II) Module glx: vendor="X.Org Foundation"
[   202.141]  compiled for 1.18.3, module version = 1.0.0
[   202.142]  ABI class: X.Org Server Extension, version 9.0
[   202.142] (==) AIGLX enabled
[   202.142] (II) LoadModule: "fbturbo"
[   202.143] (WW) Warning, couldn't open module fbturbo
[   202.143] (II) UnloadModule: "fbturbo"
[   202.143] (II) Unloading fbturbo
[   202.143] (EE) Failed to load module "fbturbo" (module does not exist, 0)
[   202.143] (EE) No drivers available.
[   202.143] (EE) 
Fatal server error:
[   202.143] (EE) no screens found(EE) 
[   202.144] (EE) 
Please consult the The X.Org Foundation support 
  at http://wiki.x.org
 for help. 
[   202.144] (EE) Please also check the log file at "/var/log/Xorg.0.log" for additional information.
[   202.144] (EE) 
[   202.168] (EE) Server terminated with error (1). Closing log file.
又花了點時間安裝哪個在淘寶入手的 LCD 顯示屏到 Raspberry Pi 上。今次是 Kali Linux。搞了很久都不成功,而且連原本能進的 startx 也失效。網上找的解答都不適用,最後出動撒手間:「rm /usr/share/X11/xorg.conf.d/99-fbturbo.conf」及「rm /etc/X11/xorg.conf.d/99-calibration.conf」使 startx 回復正常。因為這兩個檔案分別是 LCD 顯示屏及觸摸屏的設定。

2016年4月20日 星期三

伺服馬達力量測試


封塵已久的機械裝置概念,上周跟朋友談過後,引起對方的極大興趣,同時也讓我重拾興奮,想繼續開發下去。對於我的構思,朋友認為很「爆」。原因是大家做的都是還原,卻沒有像我的想法。若是真的實現了,程度足以吸引全球很多創客的眼球,藉以提升公司的知名度,甚至是帶來商機。所以他決定跟我一起開發,務求能在五月底,美國最大型的 Maker Faire 展出。我沒有太多時間能開發,但做得幾多得幾多。今日用 Arduino Nano 搭了個簡單的電路,配合 7.4V 鋰電,測試伺服馬達能否有足夠的力量去轉動魔方。第一關過了,剛好夠力。下一歲是機器設計及製作。

2016年4月19日 星期二

如何在非 NOOBS 上使用 IoT HAT


獨家收到 RedBear 在 Kickstarter 上熱籌的 IoT HAT 板及其在 NOOBS 1.8.0 的設定方法。收到體驗板後,當然即刻測試。不過,我最想試的是 IoT HAT 能否在 Kali Linux 上正常運作,甚至是啟動監聽模式。要是成功的話,Raspberry Pi Zero + IoT HAT 將會是黑客的好工具。我在 Kali Linux 2.1.2 上以「ifconfig」指令測試過,預設是無法找到 IoT HAT 的 WiFi 設備。解決方法是用 root 登入後,再作一次手動設定:
mkdir /rpi-boot
mount /dev/mmcblk0p1 /rpi-boot/
nano /rpi-boot/config.txt
輸入以下內容:
dtoverlay=sdio,poll_once=on
後按 Ctrl-X 儲存及離開。
nano /boot/cmdline.txt
刪除「console=ttyAMA0,115200」內容
umount /rpi-boot/
reboot
最後,按照平常設定 WiFi 的方法處理就可以了。


不過,在啟動監聽模式時出現不支援訊息。希望 RedBear 能在量產時改良,讓 IoT HAT 可以執行監聽模式。

2016年4月18日 星期一

ESP8266WebServer.send 的上限值


昨天病了,由早到晚都卧在床上,簡直被神偷走了 24 小時一樣。今天還是有點不舒服,休息一下,在家繼續工作。為了追回失落的 24 小時,本來昨天可以開發的 WiFi 燈,便在晚上繼續。

WiFi 燈用的是 NodeMCU,程序不難編,三兩下便完成功能部份,但在加入圖片時卻出現異常。由於 Arduino IDE 沒有加入圖檔的功能,圖檔理應是以 Base64 方式在代碼中出現。可是 WiFi 連線後,只顯示空白頁面。經驗話我知是 ESP8266WebServer.send 的發送限制。很多時小型機器很多程序都不支援太大量的訊息。問題是在網上找來找去也找不到限制數值及解決方法。最終查看 ESP8266WebServer 的代碼,得出上限是 2048 bytes;而我的 HTML 卻是 12795,遠遠拋離限制值,難怪畫面會變成空白了。

2016年4月16日 星期六

開發 Android 應用程式・第一課


課堂在九時開始,我慣性地早到 15 分鐘。老師及同學都在。安頓好後,等待所有同學到齊,正式開始已經是 09:15 了。

十四位同學,竟然有十二位是女生,真的讓我感到意外。不過,只有三位同學曾經學過一點點編程。花了三晚寫的 Keynote 只有廿多頁,原本以為能撐到大半天,但在頭一小時已經差不多講完。幸好還準備了三個實作,總算能支撐到整天的學習。最後的習作是「過三關」,看到同學們由理解到融會貫通,到自行加入重設邏輯,實在感到安慰。

2016年4月12日 星期二

開發 Android 應用程式


繼上年到澳門培正中學教授 iOS 程式開發後,上個月獲得邀請到中學教授 Android 程式開發。雖然我學習 Android 開發的時間很短,但 20 多年的編程經驗,不會影響教學質素;加上我認為「教學相長」,是一個雙贏的格局,於是一口答應了。課程只有四堂,一連四個星期六,每堂六小時。本周六就是第一課的時間,我正在趕緊編寫 Keynote,為課堂內容定立框架之餘,又可以提醒自己需要說明的事情。期待第一課能帶給同學們對編程的興趣。

2016年4月8日 星期五

讀取相機菲林最後的相片

在參考其他的拍照應用程式時,發現有程式能顯示相機菲林的最新相片。已很久沒有做過相關的東西,不知道原來在 iOS 8 開始,相簿方面的支援已經改進不少。查找過資料,要讀取相機菲林最後的相片是這樣做:
UIImageView *imageView = [[UIImageView alloc] initWithFrame:CGRectMake(160, 20, 100, 100)];
[self.view addSubview:imageView];

PHFetchResult *assetCollection = [PHAssetCollection fetchAssetCollectionsWithType:PHAssetCollectionTypeSmartAlbum subtype:PHAssetCollectionSubtypeSmartAlbumUserLibrary options:nil];
[assetCollection enumerateObjectsUsingBlock:^(PHAssetCollection *smartFolderAssetCollection, NSUInteger index, BOOL *stop)  {
   PHLog(@"smartFolderAssetCollection: %@", smartFolderAssetCollection);

   PHFetchResult *result = [PHAsset fetchAssetsInAssetCollection:smartFolderAssetCollection options:nil];
   [result enumerateObjectsUsingBlock:^(PHAsset *asset, NSUInteger index, BOOL *stop)  {
      PHLog(@"asset: %@", asset);
   }];

   PHAsset *asset = [result lastObject];
   [[PHImageManager defaultManager] requestImageForAsset:asset targetSize:CGSizeMake(100, 100) contentMode:PHImageContentModeAspectFit options:nil resultHandler:^(UIImage *result, NSDictionary *info)  {
      [imageView setImage:result];
   }];
}];

2016年4月7日 星期四

公司五歲生日


今天是公司五歲生日。當初我的夢想還歷歷在目,未來下一步該怎麼走好呢?機遇在哪裡?很想有導師能指導一下。

2016年4月6日 星期三

《美圖秀秀》海報是甚麼構造?

經常看到朋友把數張照片合成一張後上傳到 Facebook。很想把這個功能加入到新的影像類應用程式中,於是把女兒常用的《美圖秀秀》來一次逆向工程,看看它的海報是怎樣做。


打開《美圖秀秀》,隨意下載數張海報。利用 Charles Proxy 檢視當中的通訊,得到海報的 ZIP 檔案。檔案沒有密碼保護,直接能解壓。


那些 JPG 檔案也沒有被加密,一看便知道是不同解像度的預覽圖。其他的都不知道是甚麼,先來看看封包內的 ini.plist:

沒有太多內容。Image 是封包內預覽圖的檔名;Sucai 是普通話,即「素材」;id 很直接是編號。即是說,建立海報圖的「素材」是「10080001.hbpt-bj」這個檔啦。

用 HexEdit 打開「10080001.hbpt-bj」:

很容易看出 PNG 的檔頭。那麼前方的則是 HBPT 的檔頭了,但內容又是甚麼?要嘗試解讀,需要參考更多的 HBPT 檔頭:

這個檔頭大小一樣,當中只有少許數值不同。意味著那些位置的內容是因檔案而異。

這個檔頭較短,即是說檔頭不是固定大小。

那麼,先把 HBPT 檔頭刪去,讓它變成 PNG:

原來拼出 Sita 海報的素材是這樣!但為何中間沒有線把兩張相片,甚至三張相片分開?

是不是分隔線在其他檔案?打開來看看:

這個沒有 PNG 檔頭,不像是分隔線的圖檔。

這個不是,其他的也不是。由於素材是透明底色,意味著分隔線應該藏在 HBPT 檔案內。翻看檔頭因檔案而異的部份:

在位置 0x28 開始的四個數值:FF E1 95 C3,看來是 ARGB 內容。在 PhotoShop 輸入紅色 0xE1、綠色 0x95、藍色 0xC3,出來果然是跟邊框差不多的粉紅色。再找多個海報來試試:

同樣生效,今次是 FF FF FF FF,是純白色。看看解圖:

是白色的海報,分隔線也是白色啦。估計正確!


等等!這個不一樣。為甚麼呢?解開 PNG 來看看:

哦!底圖不是純色的!而且素材檔案內能找到兩個 PNG 檔頭!看看後面那張 PNG:

原來分開兩層的類型!既然一個檔內有兩張 PNG 圖,即是說需要有資料來告訴程式,下一張圖在檔案內的位置。

再看看檔頭 0x30 位置:

數值 C9 2C 01 00,在 Intel 的換算下是 0x00012CC9,應該是 PNG 的大小,加上 HBPT 檔頭大小 0x34 剛好是 0x00012CFD。亦即是圖中左上角的檔案大小。確認了這個是 PNG 大小的值。

好了。其他的是甚麼檔案?

依我估計,2.ptljb 是兩張相片時,每格相片的形狀資料;3.ptljb 是三張相片;4.ptljb 是四張相片,如此類推。再看看檔案大小,每加一格相片,大小增加 80 個字元,非常固定。意味著一格相片的資料為 80 字元。到目前已經了解《美圖秀秀》海報的運作,不打算再深究形狀方面的事情,就到此為止。

2016年4月5日 星期二

PHP+Cronjob 刪除過期檔案

同事離職後,經常都有善後工作。今天用家匯報後台出了問題,上傳不到路線資料,於是我進入系統的 phpMyAdmin 查看數據。不論我怎樣登入都不成功。直覺話我知服務器的空間用盡了,於是把一些相片壓縮檔刪除。選它是因為大,以及這些都是臨時檔案,可殺。處理完後,登入成功。用戶也能繼續使用。

作為一個負責任的編程人員,是要為自己程序所新增的臨時檔案做刪除處理。最簡單的就是檔案滿月便把它刪除。這樣的程序其實不難寫,用 PHP + Cronjob 就能自動解決。不需要每次人手處理。本來以為交給同事會懂得自行處理,然而他們有時間看 Facebook、有時間玩 Line 吹水、有時間抽煙、有時間看新聞網頁、有時間聽收音機...,都不會想想自己開發的系統有甚麼地方可以改善的地方。
<?php
//========================================================================================
//  ZIP File Cleaner
//----------------------------------------------------------------------------------------
//  Written by Pacess HO
//  Copyright 2016 Pacess Studio.  All rights reserved.
//========================================================================================

//  This application will looking for ZIP file in specified path, check if file
//  have been created more than 1 month.  If yes then remove it.

//----------------------------------------------------------------------------------------
function getDirectoryArray($directory)  {
    $resultArray = array(); 
    $fileArray = scandir($directory); 
    foreach ($fileArray as $key => $value)  {

        //  Skip . & ..
        if (in_array($value, array(".", "..")))  {continue;}

        $filePath = $value;
        if (is_dir($filePath) == false)  {$resultArray[] = $filePath;}
    } 
    return $resultArray; 
} 

//=========================================================================================
//  Main program
$zipFolder = "/home/www/merchandising/file/zip";
$cutOffTimeStamp = intval(date("Ymd", strtotime("-30 days")));

//----------------------------------------------------------------------------------------
$fileCount = 0;
$deleteCount = 0;
$fileArray = getDirectoryArray($zipFolder);
foreach ($fileArray as $key => $filename)  {

    $tail = strtolower(substr($filename, -4));
    if ($tail != ".zip")  {continue;}

    $fileCount++;
    $filePath = $zipFolder.DIRECTORY_SEPARATOR.$filename;

    $timeStamp = intval(substr($filename, 0, 8));
    if ($timeStamp >= $cutOffTimeStamp)  {
        print("Keep [$filePath].\n");
        continue;
    }

    $deleteCount++;

    print("Deleting [$filePath]...\n");
    unlink($filePath);
}

print("\nSummary:\n");
print("cutOffTimeStamp=$cutOffTimeStamp\n");
print("fileCount=$fileCount\n");
print("deleteCount=$deleteCount\n\n");

?>

2016年4月4日 星期一

自定 CIKernel 濾鏡


坊間很多攝影程式都有形形色色的濾鏡給用戶選擇;在開發《Not-Bag》應用程式時,同事們選了一條難走的路。今次由我操刀,先看看現成有哪些可行方案選擇,再決定應該如何做。

自從 iOS 8 開始,編程人員已經可以為 CoreImage 自定 Kernel。我試了一下。找來一個名叫《Sweetcorn》的開源程式,以視覺方式自行組合濾鏡,看到效果之餘,還生成 Kernel 用的代碼。把代碼加到程式中,便能在 iOS 上實現相同的濾鏡效果:
NSString *string = @"kernel vec4 color(__sample pixel)  {"\
                   @"   float var_2 = sqrt(pixel.b);"\
                   @"   float var_6 = 2.5;"\
                   @"   float var_4 = pow(pixel.r, var_6);"\
                   @"   return vec4(var_4, pixel.g, var_2, 1.0);"\
                   @}";

CIKernel *kernel = [CIKernel kernelWithString:string];
UIImage *image = [UIImage imageNamed:@"sita.jpg"];

CIImage *inputImage = [CIImage imageWithCGImage:image.CGImage];
CIImage *outputImage = [kernel applyWithExtent:inputImage.extent roiCallback:^CGRect(int index, CGRect rect)  {
    return inputImage.extent;
} arguments:@[inputImage]];

image = [UIImage imageWithCIImage:outputImage];
UIImageView *imageView = [[UIImageView alloc] initWithImage:image];
[imageView setCenter:self.view.center];
[self.view addSubview:imageView];