2023年12月1日金曜日

玉山登頂筆記 第一天 2023-11-11

一切都是從一句「假如抽得中就去」開始的...
朋友詢問要不要去玉山,筆者就這樣回答。
再次體會莫非定律。筆者自認運氣是非常差的。玩手遊保底很正常...
想說玉山應該不會抽到吧?結果抽到第15次,中了...

這個時期的好處是下雨的機率低,因為天候失敗的可能性較低。


跟富士山的行前準備一樣的每週兩次跑步三千公尺加上週末一次郊山。
然後就到了當天。這次的預計行程:
第一天:出發到東埔山莊

第二天:東埔山莊-排雲山莊

第三天:排雲山莊-主峰登頂-回程

至於什麼前東南西北峰的都是討論範圍外...目標只有一個:安全回家

 



交通與住宿的相關準備:

申請入園證:申請網址 這是玉山申請的第一步。中獎後要到出發日的五天前才會給入園證。

申請入山證:得到中獎通知之後就可以申請了。可以經由入園的網頁傳送資料到入山申請的頁面,就不需填兩份資料。截止日是三天前,不過還是建議提早處理以免忘記。

預訂新竹-嘉義的台鐵車票:07:07~09:15

預訂嘉義車站-阿里山的公車票:09:40~12:30

預訂阿里山-上東埔的公車(只有email預訂,無付款,上車使用電子票證付款)

預訂東埔山莊的住宿

預訂排雲山莊的住宿與三餐(當天晚餐,當天凌晨的早餐,當天早上的早餐) 


登山裝備:

大背包:
攻頂背包:建議越輕越好。堅固是必須的。有幾個地方可能背包脫落就撿不回來...

防寒衣物(氣象預測山頂溫度約攝氏1度):羽絨衣必須要有

頭燈:若是打算凌晨啟程,在山頂看日出的話必備。因為會用到3~4小時,建議準備兩份電池。飲用水與運動飲料:這次帶一公升的運動飲料,撐兩天覺得不足。飲用水一公升已足夠,非夏季,還有到排雲山莊可以裝水,比較沒問題

護膝:筆者為必備裝備,但是這次卻忘記了...  導致整體的進度拖慢。還是靠朋友出借的一個護膝,想說至少要保住比較容易出狀況的左腳。不然會下不了山

手套:因為有不少地方需要拉鐵鏈,也是必備

登山杖:必備。沿路沒有鋪裝路面,還有兩處:1.5km~2km與6km有相當大的高低落差

入山/入園證: 入山證記得要準備兩份紙本。塔塔加的派出所需要投入一份入山證跟成員名單。建議多一份供路上備查。另外,入園證可以使用手機截圖,但是條碼必須可辨認

手機:務必安裝離線地圖軟體,並確定地圖在離線狀態可以使用。台灣之星在排雲山莊到登頂前的沿路均無訊號,需注意

塑膠袋:裝垃圾,食物,換洗衣物,拖鞋,避免衣物沾濕,山頂附近保溫等等用處很多,建議帶3~5個

行動電源與充電線:東埔山莊有提供插座,有入住的話建議帶充電器。
排雲山莊無供電。若在路上消耗過多電力,手機會有電力不足的可能

雨天對策:基本上下雨的話登頂的危險度過高(登頂前有三處需要踩幾乎沒有立足點的地方),不建議登頂。就算如此也必須考慮回程。因此雨衣(建議是內襯塑膠的完全防水外套,兼具防寒效果)與背包套仍然為必需品

高山症對策:筆者使用丹木斯。加上前一天住東埔山莊,這次幸運的沒有任何高山症狀

食物的部分:
東埔山莊沒有伙食。塔塔加遊客中心有供餐。便當供應至下午兩點半。套餐供應到比較晚
第一天的中餐可以在塔塔加遊客中心處理
因此到排雲山莊之前,需要準備三份餐點:第一天的晚餐,第二天早餐,午餐


EMU3000的窗子。新車耶...

行前以為搭車點是在後站,看了時刻表是寫前站。還好沒走錯方向而浪費時間。
現在才發現10:10那班被刪掉了。查了公告是2023-05-25~2023-11-30暫時停開。搭的人有那麼少嗎?至少這次是客滿還有人坐地板。看來是因為班次少了所以客滿...
 

也還好有先買票,沒有想說到到現場再處理。這樣可能就會打算搭10:10那班而錯過班次,導致第一天行程就爆了...


上次到嘉義是拍日環蝕的時候


在車站旁的便利商店先買好晚餐跟明天的早午餐。事後檢討這個動作是對的
因為到阿里山就發現便利商店的餐點被掃光了
之前已經體驗過幾次山上購物碰壁的狀況,已經養成山下購物的習慣。同行的友人也直呼好險... 餓肚子還吃三餐麵包的話怎麼「爬」山啊


電子紙班車動態顯示,真好。我在新竹還沒看過...
這次搭的班次為7322D,會多繞奮起湖,時程多30分鐘,大約3小時到阿里山
在奮起湖會留5分鐘的時間上廁所
因為不確定奮起湖是否有停留時間,餐點已經在山下購入。不然是打算在奮起湖買午餐便當

被坐歪的地面燈。習慣可以再差一點... 不要坐壞了,摔到地面受傷還要申請國賠耶

事先購票還有一個好處,就是上車之後可以換座位(司機有保留換座位的時間)
只是這公車的座位真的很窄,座位上方也沒得置物。本來想換到座位前方是隔板的位置,換過去發現空間更窄,連背包都不能放腳下。只好維持原座

到阿里山之後因為換車時間極短(只剩10分鐘),沒時間拍照
要注意的是6739次只剩13:10這班。阿里山上的時刻表還沒有拿掉14:00這班的時刻
可能會讓旅客誤解

塔塔加遊客中心剛好在台18線跟台21線的交界


一下車就感受到滿出來的鳥。不愧是重要鳥點
星鴉,巨嘴鴉,白耳畫眉的聲音不絕於耳
不過因為公眾交通的不便,除非筆者有「房」車了才會來吧

到此一遊的拍照點

 從這個點似乎可以看到玉山,不過這時霧氣重,無法確認


 

飛鼠電子遊戲機



在塔塔加用餐,省錢的方案是買$130的便當。因為強制外帶(很合理就是),稟持無痕山林的做法,應該要把便當盒帶到山下的。本來的打算是沒垃圾桶的話就在餐廳用餐。有垃圾桶的話,要爬山也顧不了這麼多了...

遊客中心外牆整修就沒拍了

猴出沒注意。車窗記得要關好...

山羌這次沒見到...

塔塔加遊客中心的餐廳價目表。便當還蠻正常的價格

這把尺的設計相當喜歡

雞腿便當$130,意外的相當好吃。跟排雲的有天壤之別

東埔山莊接受入住的時間是下午四點。這時間沒事,就逛逛塔塔加遊客中心
有不少動物標本跟環境介紹的設施
「熊出沒注意」其實是日文文法...

飛鼠電子遊戲機
筆者被政府整到現在都沒可連線的大型電玩可以玩,偶而發洩一下




看了這張圖才發現嘉明湖不在國家公園範圍內,難怪可以帳篷搭好搭滿

只想抱抱這些鳥...

給雞拜年



要說可惜的就是標本們都有玻璃保護。濕度控制也是原因。拍照畫質總是會差些...


大約1530離開塔塔加遊客中心,往東埔山莊移動。距離不遠,走路約20分鐘可到
走路的話從公路邊有樓梯可以下切,不需走到車道,比較快


東埔山莊外觀。有一台飲水機供大家使用

 

用超廣角才拍得進整間建築,其實還不小

習慣喝冰水,看到只有溫水按鈕,心涼了一半
測試一下水溫,就算是溫水也只有13度,安心了

大廳。櫃台有人就不拍了...
用餐時段可能會座位不足

右邊的就是行李架。一人只有一個大背包的寬度可用


大廳往廚房跟浴室的通道

櫃檯擺設的當地交通方式告示牌
強烈建議不是自己開車,使用包車服務等等沒有超出預估時間的交通手段的話,務必取得雲之端接駁司機的聯絡方式。筆者上山之後就發現腳程是絕對無法趕在1330之前下山,想聯絡司機都沒辦法


筆者下山後請塔塔加派出所的警察協助,警察有提供雲之端接駁司機(其實就是塔塔加往玉山登山口的接駁車的營運單位)的聯絡方法
司機說最好前一天聯絡。當天的話最好提早4小時以上(可以從排雲出發的時間點起算)
阿里山下山的巴士,最後一班是17:10。若沒有預約,司機必須下班後才能處理

直到親身體驗,才知道要真正的離開玉山也不容易(要離開阿里山也不容易...)

塔塔加到玉山登山口的接駁車資訊。這段路程有2.8km
加上登山口到排雲山莊的路程,會超過11km

沒有長距離登山經驗的話,強烈建議留下體力跳過這段
另外,接駁車在假日有遇到三台車,但是平日只有一台車來回跑,一次只能載7人,
來回的時間大約20分鐘,打算搭13:15左右的公車離開的話要注意

 東埔山莊入住須知。除了圖上的幾點,筆者補充以下:

  • 目前沒有供餐,餐點需自行準備。有廚房可以自炊,需自行帶爐具
  • 廚房有提供洗碗精
  • 20:30熄燈後室內禁止說話
  • 寢室內不需換拖鞋
  • 建議攜帶多孔插座,方便手機充電


寢室狀況。個人覺得蠻乾淨的。

床位算是偏窄。長度也不足。以筆者的身高,腳會接近棉被邊緣。

有提供吹風機,不過高海拔的夜間容易受風寒...
也有洗手乳。

浴室只有兩間,沒有提供清潔用品。有盥洗需求的話建議盡早處理。

廁所還蠻乾淨的。

廚房有提供冷水跟熱水。



這台熱水機的溫度似乎不夠(溫度指針停在81度),要泡熱食的話,大家似乎都使用外面的那台機器。因此外面的飲水機在用餐時段的熱水常常會供應不足。需注意。

觀察完室內設施,大廳不知不覺就坐滿人,進入用餐時段
晚餐準備了握飯捲加上沖泡燕麥。跟同行友人討論明日的狀況,一下子就接近熄燈時間


因為想拍星空,就等到天空完全黑再試試
小米13pro的30秒長曝似乎還可以。銀河的樣子有出來。但是筆者沒想到用內建的長曝功能,出來的圖只有一千兩百萬畫素
後來試了專業模式可以曝光30秒,下次拍星空要改用專業模式

因為平常不會在2100前入睡,加上山上的低溫跟潛在的高山症狀,大約花了4小時入睡。睡眠品質其實不錯。除了鼾聲之外是很安靜的。也沒有被其他人的起床音干擾。

醒來發現所有人都不見了...有種驚悚感?

2023年11月1日水曜日

Plurk Custom EmotionIcon : Chrome / Firefox web extension 開發筆記 / oAuth流程與參數解說

Plurk的自訂表符,未使用噗幣的帳號,上限是60個,筆者覺得不夠用。當然是建議購買噗幣支持官方營運,就可以擴充上限至600個。
不過管理表符的功能還是需要的。嗯嗯...
原本是打算修改小耀博士開發的噗浪卡卡,之前有改出一版,不過後來因為chrome擋掉一些之前能做的動作(像是動態插入程式碼然後執行),評估後覺得修改困難,決定寫新的。


進入點:manifest.json與javascript相關

第一步:先看入門文件。

https://developer.chrome.com/docs/extensions/mv3/getstarted/

筆者覺得相當合用。下面只列出需要注意的部分。

manifest v3跟v2 有參數的差異。像是最難處理的permissions,v3分成「optional_permissions」與「host_permissions」。

「browser_action」在v3改為「action」。api也從v2的「browser.browserAction」改名為「browser.action」。
要注意的是api文字定義不通用。宣告v2只能用v2的定義「browser.browserAction」,宣告v3只能用v3的定義「browser.action」。

firefox跟chrome也有參數的差異。像是firefox用「background.scripts = [...]」的格式,chrome卻是用「backgound.service_worker = “filename.js”」的格式。
加上雙方都不允許自身未定義的keyword存在,
例如chrome不允許「browser_specific_settings」的參數,
導致manifest不能一份通用兩邊。

這次開發遇到最大的困難是firefox的檔案儲存功能。
firefox的可用程式碼分成下面幾種:

  • content_script:可以注入正在使用的網頁。browser api的限制比較多。
  • background_script:沒有browser api的限制,但是不能對正在使用的網頁進行html tag操作。也不能使用alert / confirm / prompt等等預設對話框。因此基本的設計方式就是在content_script裡面進行ui操作,需要突破browser api限制的動作丟到背景執行。

    背景執行的動作雖然會被瀏覽器暫停,有定義addlistener()等等event driven的設計的話,只要有event送過去,背景動作就會被喚醒。不用擔心會有重啟或是初始化的問題
  • action popup / sidebar:可在瀏覽頁面時同時顯示。可以使用script,但是需遵循content security policy。基本上就是不能使用script tag,所以網頁常用的load script手段都需要把程式碼下載之後跟套件一起打包,記述在manifest的content_scripts.js 參數裡面讓瀏覽器幫你載入。在html記述<script>也會直接無效。
  • options_ui(在擴充套件的設定頁,以html load script),也沒有browser api限制。但是「要使用者點擊打開」之後才會執行。

「content_script」可用的browser api:

https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/Content_scripts#webextension_apis

「background_script」「options_ui」可用的browser api:

https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/Background_scripts

搜尋了幾十頁,大概是關鍵字不對,得不到正確答案。跑去stack overflow才找到。
其實是列在mozilla的擴充套件介紹的一開始concepts的地方。算筆者沒有從頭讀吧。因為先讀了chrome的介紹,就懶了...

在firefox的擴充套件開發遇到的限制:

  • content_script不可使用downloads api(就算有允許權限也一樣)
  • content_script不可使用permissions api(就算是經由form的submit也一樣)。
  • browser.permissions.request() 雖然在sidebar / popup / option-ui 可以使用,但是「只允許經由使用者操作啟動」。
    而且「必須使用<form>tag的submit功能實作」,不能使用html tag event,例如onclick實作...
    會獲得錯誤訊息「permissions.request may only be called from a user input handler」
    另外,以submit實作的時候,一定要記得在event function的第一行指定「ev.preventDefault();」。不然預設的頁面重刷動作會導致下載失敗。

    例外:可以用在「browser.action.onClicked.addListener」的定義。感覺有條件限制有點複雜。應該要有文件list會比較好。也許還有其他地方可以...
  • background_script可以使用session / local storage。但是跟網頁的資料是各自獨立的。與content_script(網頁的ui注入的script)溝通,需要使用browser.runtime.onMessage()做溝通。也許有其他方式,這邊不研究。

 

chrome的檔案儲存相對簡單。使用以下的一連串動作就解決了:

window.showSaveFilePicker(filePickerOpt)
    .then((newHandle) => newHandle.createWritable())
    .then((writableStream) => writableStream.write(JSON.stringify(data, null, 2)))
    .then((writableStream) => writableStream.close())
    .then((_) => {
        alert('ok');
    })
    .catch((e) => {
        alert('canceled');
    });

 

如何在正在操作的網頁插入擴充套件的icon/image:似乎是沒有直接的方法。
目前是採用把icon轉成base64 data string的方式處理。
參考網址: https://www.atatus.com/tools/image-to-url

2023年5月15日月曜日

[android]app背景處理 with WorkManager

最近開始接觸android 13版之後,發現背景服務已經不能長存了。

總是app退到背景兩分鐘之後,service就會被系統砍掉。不管是不是用start_sticky標記。

android 8~11,使用setforeground的方式處理,已經讓開發者們怨聲載道了。(service啟動之後要在10秒內進行系統提醒channel的建立跟提醒的發送。) 

然後2021年出現了官方的解決方案:WorkManager。當時覺得還能動,剛出現的模組也不一定穩定,就沒留意。

但是現在不同了,service在app退到背景2分鐘之後就被砍了... 只能面對。所以又花了時間處理這部分。實際上沒那麼好解決,幾個要素交互影響不說,也需要測試裝置深眠狀態的反應。
寫下實作的方式,跟處理問題的時候遇到的狀況。有興趣可以參考。

能夠讓app盡可能啟動背景運作的方式

  • Google Cloud Messaging(以下簡稱「GCM」):假如有架伺服器的話,建議實作。這是權限相當高的動作,有能力啟動app的前景動作。
    要注意的是,GCM在android 12以後(不是很確定版本)不能使app在背景狀態之下啟動前景服務。就算是採用送Broadcast, 經由app在Manifest宣告的BroadcastReceiver觸發的方式,都會送你不能在背景狀態啟動前景服務的例外。
    那這樣不就廢了!?Orz
    目前的解決方案是導入「WorkManager」。
    上面有寫到,GCM有能力啟動app的前景服務。在實驗時發現app在背景狀態,甚至是裝置休眠狀態,裝置收到GCM訊息的當下,Worker會被喚起執行。這樣就有機會實作不容易被中斷的處理了。
  • WorkManager:本文的內容。在Service接近殘廢之後,目前的背景執行解決方案。
    可惜WorkManager也有缺點。因為是背景執行緒,在doWork()裡面不能執行必須在主執行序運作的動作。像是Lifecycle.addObserver,Livedata.addObserver等等...


決定要怎麼使用

基本的使用流程:

  1. 導入模組
  2. 撰寫Worker,定義執行內容
  3. 以Worker建立WorkRequest
  4. 把WorkRequest放進WorkManager  

詳細內容請參考官方文件:

https://developer.android.com/guide/background/persistent/getting-started

 

撰寫Worker

Worker分成兩種:

  • ListenableWorker:可以定義ListenableFuture,取得Worker的執行狀況。預設是在主執行緒執行。要在背景執行緒執行,需要做一些深入調整(主要是在AndroidManifest移除ListenableWorker預設的初始化工具,自行定義Configuration,指定executer為背景執行緒)。本文不研究這部分。
  • Worker:ListenableWorker的子class。預設在背景執行緒執行。缺點是除了模組提供的WorkInfo之外,沒有獲取執行狀況的方法。當然是可以用static singletone的方式暴力處理...

 

建立WorkRequest

WorkRequest有兩種:

  • OneTimeWorkRequest:只執行一次。若是在worker執行完畢回傳Result.Retry();,就會重試。重試的動作間隔是backoff的設計,可以定義linear(固定重試間隔) / exponential(指數增加間隔)
  • PeriodicWorkRequest:可以定義重複的間隔時間(固定間隔,跟每次執行可以跑多久)。最低的重複間隔是15分鐘... 相信有人會覺得不太合用...

WorkRequest可定義setExpedited(...),代表很重要,系統會儘快執行。會多快並不確定,筆者測試是毫秒等級,夠用了。
可使用constraint限制運作條件(例如是不是連網路,是不是在充電等等)。同時限制多個運作條件,就只有在條件同時滿足的時候才會執行。

 

依worker的管理方式區分

  • UniqueWork:指定一個字串id方便管理。要取代或是維持WorkRequest都使用這個字串做處理。
  • 非UniqueWork:還是可以使用取得WorkInfo的時候,回傳的uuid做處理。只是管理上相對麻煩。如何取得WorkInfo後面會介紹。
  • tag: WorkRequest可以定義tag,這樣在使用workManager.getInfosByTag(...)取得Workinfo的時候,可以用tag來取得同一個tag的所有workRequest的執行狀況。


依照需執行的動作的性質決定。本文只討論UniqueWork,大部分的場景已夠用。

建立Worker的例子:

public class MyWorker extends Worker
{
Context context;
// Worker的constructor會提供context。
public MyWorker(
Context context,
WorkerParameters params)
{
super(context, params);
this.context = context;
}


@Override
@NonNull
public Result doWork()
{
try
{
setForegroundAsync(getForegroundInfo());
} catch (Exception e)
{
e.printStackTrace();
}
...
}
@Override
@NonNull
public ForegroundInfo getForegroundInfo()
{

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)
{
createChannel();
}

Log.d(TAG, "getForegroundInfo");


String NOTIFICATION_CHANNEL_ID = context.getPackageName();
NotificationCompat.Builder notificationBuilder = new NotificationCompat.Builder(context, NOTIFICATION_CHANNEL_ID);
Notification notification = notificationBuilder.setOngoing(true)
.setSmallIcon(R.drawable.app_logo)
.setContentTitle("背景執行中")
.setPriority(NotificationManager.IMPORTANCE_MIN)
.setCategory(Notification.CATEGORY_SERVICE)
.build();

return new ForegroundInfo(0, notification);
}

@RequiresApi(Build.VERSION_CODES.O)
private void createChannel()
{
// Create a Notification channel
String NOTIFICATION_CHANNEL_ID = context.getPackageName();
String channelName = context.getResources().getString("背景執行中");
NotificationChannel chan = new NotificationChannel(NOTIFICATION_CHANNEL_ID, channelName, NotificationManager.IMPORTANCE_NONE);
chan.setLightColor(Color.BLUE);
chan.setLockscreenVisibility(Notification.VISIBILITY_PRIVATE);
assert notificationManager != null;
notificationManager.createNotificationChannel(chan);
}
}
 

建立OneTimeWorkRequest的例子:

OneTimeWorkRequest myWorkRequest = new OneTimeWorkRequest.Builder(MyWorker.class)
.setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST).build();

把WorkRequest加入WorkManager的例子:

WorkManager workManager = WorkManager.getInstance(context); 
WorkContinuation wc = workManager.beginUniqueWork("myService", ExistingWorkPolicy.REPLACE, myWorkRequest);
wc.enqueue();

取得workManager必須有context。

 

注意點:

  • 每次Worker執行完畢,不管結果是成功,取消,或是重試,下一次的Worker執行都會重新建立一個新的Worker。也就是worker的constructor會被執行。
  • 針對android 11以前的版本的相容性,也就是那個惡名昭彰的「啟動前景服務,10秒內沒有發送系統通知的話,就會直接送你RuntimeException」的規格,
    Worker必須定義「getForegroundInfo(ForegroundInfo)」。
    實作範例請參考 「Support for long-running workers」裡面的實作方式。上面的程式碼也有概略的寫法。
  • 另外,若是要讓Worker長時間執行,建議下指令「setForegroundAsync(getForegroundInfo(progress));」
  • 針對android 12以後的狀況,「setForegroundAsync(getForegroundInfo(progress));」建議使用try / catch包覆,因為有可能會失敗。
    雖然官方Worker模組的2.7版(目前最新版是2.8.1)文件載明「setForegroundAsync()已經棄用」,還是建議寫一下,增加相容性。
  • android官方文件的「Support for long-running workers」裡面的範例程式碼,定義了「private createForegroundInfo()」。雖然這樣不算有問題,不過跟必須定義「getForegroundInfo()」的限制有落差。
    直接定義 「getForegroundInfo()」,然後範例內的
    「setForegroundAsync(createForegroundInfo(progress));」
    改為
    「setForegroundAsync(getForegroundInfo(progress));」
    就ok。

 

 如何取得WorkRequest的Workinfo (uuid,運作狀態等等)

  • 有定義UniqueWork id,可以用專有函式:ListenableFuture< List<WorkInfo> > lf = workManager.getWorkInfosForUniqueWork("myService");
  • 執行workManager.beginUniqueWork之後,以workContinuation.getWorkInfos()取得。以下為範例:
    WorkContinuation workContinuation = workManager.beginUniqueWork("myService", ExistingWorkPolicy.REPLACE, myWorkRequest);
    ListenableFuture< List<WorkInfo> > lf = workContinuation.getWorkInfos();
     

應該有發現取得workinfo的動作,回傳大都是List。這是因為使用WorkManager加入單一或是定時重複work之後,回傳的是「WorkContinuation」結構。前面的範例是直接workContinuation.enqueue(),因為只有一個work要做。

WorkRequest是允許連續動作的。可以設計其他的OneTimeWorkRequest,然後使用workContinuation.then(oneTimeWorkRequest)加入。等到全部都設定完之後,使用wc.getWorkInfos(),回傳的就是整串的workinfo。

這時進行workContinuation.enqueue()加入WorkManager,就可以循序執行一連串的work。
WorkRequest的chaining就不討論了。