World of Coffee 2019 Berlin


很興奮的來到柏林參加歐洲2019咖啡展~ 第一次來到以精密重機械/高級汽車等聞名全球的德國,而且是其首都柏林參展。聽說德國人很嚴謹,人都很嚴肅,而且要求精準~

仔細一想,德國對大部份旳台灣人來說,應該是個很陌生的地方,除了賓士/BMW/雙人牌/梅克爾之外,還有什麼?上次看到德國的電影,聽到德國的音樂,是什麼時候?在戰後他們經歷了是什麼樣的生活?不像是鄰國日本韓國,新聞多少會播,德國?不知道… 不清楚…

不過,真的來到柏林之後的感覺,怎麼說…
意外的不熱鬧~ 新北市的房子都比這邊高大很多很多~
意外的德國生活還蠻悠閒的~ 大家怎麼好像都像在度假~ 慢慢來~
意外的大部份人都蠻友善的,我們迷路10鐘內就有3個人來指路,嚴肅的德國人在那裡?

而且,更意外的,大部份的計程車,都不會輸給 TAXI 電影裡的拼勁跟氣魄!車跟車間的距離可以少於5公分刷過去~然後搞到差點撞到再互比手勢,哇賽!短短不到20分鐘的車程,可以讓我二同事都暈車~ 我是覺得像坐雲霄飛車一樣刺激,不過我也不愛雲霄飛車… 我說, TAXI 的國別弄錯了吧!?應該是德國才對!

今年應意大利廠商邀約,我們來柏林展出我們所研發的 IoT 手沖咖啡機 :

HIROIA JIMMY
HIROIA SAMANTHA

對於以意式咖啡為主的歐洲,IoT手沖咖啡機相對是新鮮的新玩意,所以也來了不少人想要了解。而 JIMMY 的設計在以時尚為日常的歐洲來說,也絲毫不會遜色,還是要感謝設計師 Jimmy 的堅持,雖然也是帶給硬體 / 韌體 / APP 團隊不少的挑戰~ 嗯,大家都差點沒有翻臉,但是看到客戶的反應還是必需說是值得的。Jimmy~ You are a barstard~ but you are right.

第一次跟意大利的團隊合作到柏林參展,真的是有趣的體驗,90%他們說的都是義大利語跟德語?反正都聽不懂也分不清楚,不過有點自毫的是發現我的破英文在這裡也不會輸給許多在地德國人,因為他們講得更爛,真意外~ 巴士司機不太會講,計程車司機也不太會講,連餐廳女待也不是每個都會講~ 或者是他們不肯講? 這就不得而知了,文化跟歷史更深層的刻印,不是我們三五天的過客能輕易的了解的。總之,我發現我的英文比很多德國人好,開心~

HIROIA & Nouva Ricombi @ Berlin
不過,意大利人的熱情跟團結真是沒話說,參展還沒展完,晚上就開始開趴囉~~ 包下整個大樓最高二層,在微風與夕陽下,輕快的音樂,各式各樣的雞尾酒,隨著時間越晚,人越來越多,嘰哩呱啦、嘰嘰喳喳、吧啦吧啦,雖然都聽不懂(全是意大利文),但是十分歡樂的感覺不用聽懂也能體會~ 這樣作生意真好,為什麼那麼多台灣傳產要用痛苦的態度來工作呢?
對了,今年柏林世界咖啡賽台灣選手參賽的有拉花、杯測、跟咖啡調酒三項。很可惜前二項都在八強止步,但是現場好HIGH呀~希望台灣選手們再繼續努力為國爭光~ 加油加油。更新:因為計分錯誤,拉花後來台灣改列第二名,太棒了!
不能不提 La Marzocco 就是氣派,先在門口停了輛紅車然後刻上他們最火紅的 KB90 意式咖啡機,這台貴鬆鬆的機器可是大家搶著買,展場裡除了自己有作意式機的之外,我看其它的幾乎都用上這台機器了~ 結果他們還很誇張的,Booth 擺在入場前~
烘豆機名牌 PROBAT 這裡可是他的主場,少不了要出來秀一下
杯子也可以擺得很美,不愧是歐洲
TONE 也是作 IoT 咖啡題材的公司,T Shirt 跟 Logo 作得不錯,機器看起來很厲害,沒試喝不知道味道如何~
另外不得不提這家用水彩渲染的方式來表達每個產地咖啡的風味,很美,令人印象深刻!
烘豆機、烘豆機、更多的烘豆機
意式咖啡機、意式咖啡機、更多的意式咖啡機,這裡是意式咖啡機的大本營

2019歐洲咖啡展順利結束,能得到不少的民眾的欣賞由衷感謝~回家後再與團隊再加油把產品與服務作得更好~

展後隔天,週日的柏林,又是另一個意外~ 除了咖啡廳之外,就只有教堂的鐘聲跟觀光客沒有休息了… WHAT!? Let me in please! any shop will do.

食尚玩家裡有說過,「每段旅程總會有些意外,所以才會有趣」,所以才會有理由再來~

雖然該節目已經結束了,但是食尚玩家的精神還是會永遠留在粉絲的心中~ 「整個世界都是我的遊樂場」,是吧?咦,他們沒說過嗎?應該有吧?沒有嗎?

Orange Chang, June 9, 2019 柏林隨筆

DynamoDB PHP Sample Codes

AWS 工具組功能強大,而且功能還以火箭速度不斷開發,每年 AWS 的研發支出還力壓 Google。 不過要說 AWS 缺點的話也不是沒有…

一個是它是 Amazon WEB Service, 所以沒有網路的話就完全無用,這聽來是癈話,但是在 IoT 應用方面卻是一大問題,因為 IoT Device 的網路連線不可能是 100% 暢通的。所以 Greengrass 技術是十分重要的…. 我期待Greengrass 技術繼續發展下去。

另一個缺點是,因為功能出得太快太新了,線上文件也不是很好看,而且因為 AWS 同時支援數個語言,像是 DynamoDB 就有 Java 、JavaScript 、Node.js、.NET、PHP、Python、Ruby,還有iOS SDK、 Android SDK …等。
支援的程式語言很多是好事,但是也引發了一個問題:要嘛很難查到線上範例,要嘛勉強查到的範例卻不是自己使用的語言,雖然可以參考但是還是不是那麼方便。我自己就有無數次到 Stack Overflow 去查問題,結果要嘛只查到問題無人回答,要嘛竟然連問題都沒有的情況。

在這裡分享許多我用過的 PHP example codes,對於要使用 PHP 撰寫 DynamoDB 程式的開發者應該多少會有點幫助:

AWS helper function

function array_from_aws_result($awsArray)
{
	$data_table = array();
	foreach($awsArray as $k => $v){

		if(array_key_exists('S',$v)){
			$data_table[$k]=$v['S'];
		}
		if(array_key_exists('N',$v)){
			$data_table[$k]=floatval($v['N']);
		}
	}
	return $data_table;
}

Scan all records

do{
    $request = [
        'TableName' => 'IotecUser'
    ];

    if(isset($result) && isset($result['LastEvaluatedKey'])) {
        $request['ExclusiveStartKey'] = $result['LastEvaluatedKey'];
    }
    $result = $dynamoDbClient -> scan($request);

    foreach ($result['Items'] as $record) {
        $list_table = array_from_aws_result($record);
    }
}while(isset($result['LastEvaluatedKey']));
if(count($list_table) != 0){
	// process the list table
}else{
	// record not found
}

Scan table using query index. Single attribute index.

Partition Key: s_deviceId
Paramter: $deviceId

do{
    $request = [
        'TableName' => 'DeviceList',
        'IndexName' => 's_deviceId-index',                      
        'KeyConditionExpression' => 's_deviceId = :v_deviceId ',
        'ExpressionAttributeValues' =>
            [
                ':v_deviceId' => ['S' => $deviceId]
            ]
    ];

    if(isset($result) && isset($result['LastEvaluatedKey'])) {
        $request['ExclusiveStartKey'] = $result['LastEvaluatedKey'];
    }
    $result = $dynamoDbClient -> query($request);

    foreach ($result['Items'] as $record) {
        $list_table = array_from_aws_result($record);
    }
}while(isset($result['LastEvaluatedKey']));

if(count($list_table) != 0){
	// process the list table
}else{
	// record not found
}

Scan table using query index. Double attributes index.

Partition Key: n_certified

do{
    $request = [
        'TableName' => 'IotecPost',
        'IndexName' => 'n_certified-n_view_count-index',                      
        'KeyConditionExpression' => 'n_certified = :v_certified ',
        'ScanIndexForward' => false, // switch forward or backward
        'FilterExpression' => 's_country_code = :v_country_code',
        'ExpressionAttributeValues' =>
            [
                ':v_certified' => ['N' => '1'],
                ':v_country_code' => ['S' => 'TW']
            ]
    ];


    if(isset($result) && isset($result['LastEvaluatedKey'])) {
        $request['ExclusiveStartKey'] = $result['LastEvaluatedKey'];
    }
    $result = $dynamoDbClient -> query($request);

	$list_table[] = array();
    foreach ($result['Items'] as $record) {
        $item_table = array();
       
        foreach($record as $k=>$v){
            if(array_key_exists('S',$v)){
                $item_table[$k]=$v['S'];
            }
            if(array_key_exists('N',$v)){
                $item_table[$k]=floatval($v['N']);
            }
        }
        $list_table[] = $item_table;
    }
// if LastEvaluatedKey is set, means this is no the last record. Continue next query
}while(isset($result['LastEvaluatedKey'])); 

if(count($list_table) != 0){
	// process the list table
}else{
	// record not found
}

Get a dedicate record using getItem with Partition key.

Partition key: n_post_sn

$result = $dynamoDbClient->getItem(array(
    'ConsistentRead' => true,
    'TableName' => 'IotecPost',
    'Key'       => array(
        'n_post_sn'   => array('N' => '')
    )
));
$postTable = array_from_aws_result($result['Item']);
var_dump($postTable);

Get a dedicate record using getItem with Partition key and sort key.

Partition key: n_post_sn
Sort key: s_email

$result = $dynamoDbClient->getItem(array(
    'ConsistentRead' => true,
    'TableName' => 'IotecPost',
    'Key'       => array(
        's_email'   => array('S' => ''.$s_email),
        'n_post_sn' => array('N' => ''.$n_post_sn)
    )
));

Update record with updateItem

Partition key: n_post_sn
Sort key: s_email

Parameter: $userEmail,$postSn, $attributeName, $attributeType, $attributeValue

$dynamoDbClient->updateItem(array(
    'TableName' => 'IotecPost',
    'Key' => array(
       's_email'       => array('S' => ''.$userEmail),
       'n_post_sn'     => array('N' => ''.$postSn)
    ),
    "UpdateExpression" => "SET #key = :val",
    "ExpressionAttributeNames" => ['#key' => $attributeName],
    "ExpressionAttributeValues" => [':val' => [$attributeType => ''.$attributeValue]]
));

Delete record with deleteItem

Partition key: n_post_sn
Sort key: s_email

Parameter: $userEmail,$postSn

$dynamoDbClient->deleteItem(array(
    'TableName' => 'IotecPost',
    'Key' => array(
       's_email'       => array('S' => ''.$userEmail),
       'n_post_sn'     => array('N' => ''.$postSn)
    )
));

Remove attribute

Partition key: n_post_sn
Sort key: s_email

Parameter: $userEmail,$postSn

$result = $dynamoDbClient->updateItem(array(
    'TableName' => 'IotecPost',
    'Key' => array(
       's_email'       => array('S' => ''.$userEmail),
       'n_post_sn'     => array('N' => ''.$postSn)
    ),
    "UpdateExpression"    => "REMOVE s_address2"
));

DynamoDB PHP 最常見的坑

參數不接受空字串!參數不接受空字串!參數不接受空字串!

因為太重要了,要重覆三次

舉例如下:

Partition key: s_email
Parameter: $userName, $userEmail

$dynamoDbClient->updateItem(array(
    'TableName' => 'IotecUser',
    'Key' => array(
       's_email'       => array('S' => ''.$userEmail)
    ),
    "UpdateExpression" => "SET #key = :val",
    "ExpressionAttributeNames" => ['#key' => 's_name'],
    "ExpressionAttributeValues" => [':val' => ['S' => ''.$userName]]
));

上面的 code, 在 $userName = ” 的情況下,會發生錯誤

… One or more parameter values were invalid: An AttributeValue may not contain an empty string  …

只要看到上述這個錯誤訊息,八成又發生某個參數是空字串了

Universal Link & URL Scheme

Universal Link & URL Scheme

如果你希望在 App A 按一個按鈕,立即切換至 App B
或是用手機瀏覽網頁時,按一個連結,自動打開 App

這個時候,需要使用的技術有二個,一個叫 URL Scheme, 一個叫 Universal Link,見下圖

這二種技術略有不同,分析如下:

程式範例

iOS URL Scheme (Objective C)
[application openURL:[NSURL URLWithString:@"iotecapp1://page/12"] options:@{}
completionHandler:^(BOOL success) {}];
Universal Link (HTML)
<a href="https://www.iotec.tw/page/12">Open IOTEC App </a>

設定方式

iOS URL Scheme

Xcode > TARGETS > your_app > Info > URL Types 下增加 URL Scheme 為 iotecapp1

Universal Link
  1. Xcode > TARGETS >your_app > Capabilities > Associated Domains 增加 applinks:www.iotec.tw
  2. www.iotec.tw 要支援 HTTPS
  3. 編寫 apple-app-site-association (詳見官方文件)
    註一: 文件類型需為 json且無副檔名
    註二:如果要支援 iOS 8, 還需要 sign 這個檔案,不過現在 iOS 8 (含)以下市佔率不足3%,還是算了吧~
  4. 將 apple-app-site-association 放至 https://www.iotec.tw/ 下 或是
    https://www.iotec.tw/.well-known/下
  5. 到 Apple Developer webpage 去將 App 的 Associated Domain 功能打開

可否接收完整 URL(也就是說,可否傳遞參數?)

iOS URL Scheme &Universal Link

二者皆可。可以取得完整的 URL,把後面的參數部份取出即可。

例: iotecapp1://page/12 後面的 page 與 12 等等即可傳遞參數

觸發方法 (iOS Objective C, AppDelegate.m)

URL Scheme
- (BOOL)application:(UIApplication *) application  openURL:(NSURL *)url
      options: (NSDictionary <UIApplicationOpenURLOptionsKey, id>*)options;
Universal Link
-(BOOL) application:(UIApplication *)application
          continueUserActivity:(NSUserActivity *)userActivity
          restorationHandler:(void (^)(NSArray * _Nullable))restorationHandler;

從網頁觸發時,如果沒有裝 App, 可否導向指定網頁?

URL Scheme

Partially YES
可以用 Timeout 方式處理,但不是 100% 成功

Universal Link

可以無縫接軌

可否從 APP 觸發?

URL Scheme

沒有問題,還可以判斷另一個 APP 是否有安裝


    [application openURL:URL options:@{} completionHandler:^(BOOL success) {
        if (success) {
            NSLog(@"Opened URL Scheme %@",scheme);
        }else{
            NSLog(@"Opened URL Scheme %@ FAILED",scheme);
        }
    }];
Universal Link

有些情況下可能有問題

用 UIWebview 或是用 SFSafariViewController 開啟 Universal Link
在官方文件上似乎是不建議的作法,
有文章提到 UIWebView 裡必需要在切換 domain 時才會觸發 Universal
Link。實際上會發生什麼事還需要有用過的人交流一下。

安全性?

URL Scheme

因為任何 APP 都可以註冊自己處理 iotecapp1的
prefix 的 URL Scheme,這會有潛在性的安全問題。
Ex: 如果您的官網透過 URL Scheme 來開啟您的 APP,使用的 URL Scheme prefix 為
“yellow://…”, 而很不巧的剛好有個限制級 APP 也使用 yellow 作為 URL Scheme prefix,
那麼就會有機率開錯了 App…
雖然手機上為何會裝該 APP 使用者本身也要負一點責任…. 不過這邊的行為與設計原意就不一致了

Universal Link

相對安全很多

參考官方文件
Support Universal Links

Communicating with Other Apps Using Custom URLs