pretty code

顯示具有 Golang 標籤的文章。 顯示所有文章
顯示具有 Golang 標籤的文章。 顯示所有文章

2025年5月8日 星期四

使用全域變數真的要小心,尤其在 Golang

可能我不常寫多執行緒的程式再加上常用很深的遞迴,故有時候我都會直接用全域變數XD

在別的程式語言也許還好,但在 Golang 的時候,一個變數是否是新的變數端看他是否有用到 :=

如果這時不小心,在操作全域變數時加上 :=,這時存取的就是一個新的 local 變數。

雖然除錯也花不了多少時間,但也是很難第一時間就馬上想到。

另外,為了收集資料,我的函數有時可以 for single job,有時也可以 for  accumulated job,如果這時忘記清空原本的全域變數,就會導致資料變成好幾次的累積結果。

以後除了很深的遞迴,我看還是少用全域變數XD

有時候也不是想用全域變數,但一開始如果不用一個 struct 包住變數,再加上隨著需求逐漸明朗,很多時候函數的參數都會逐漸膨脹,很容易就超過 5 個以上,再加上我的 code 大部分都函數化,有時一改就是好幾層,只好偷懶用全域變數XD

我想以我的 coding style 來說,一定要養成 struct 進,struct 出的習慣,這樣應該能避免全域變數的使用?

不過這樣 struct 也會變得腫脹,真是兩難?

2025年4月22日 星期二

又回來 Golang 的世界了

花了兩天時間,將之前使用 Tcl + bash script + grep + sed 寫的工具,改用 Golang 重寫。

這一年來已經習慣了 script 的 free Coding 過程,突然回到編譯型語言,還真是有些彆扭XD

距離上一個使用 Golang 開發的專案,也有三年了吧?

不過,編譯型語言還是有好處的,比如一個原本要 54 秒的 netlist 相關報表,現在只要 15 秒即可搞定(之前慢是慢在 bash script,不是 Tcl)。

Golang 還有一個額外的好處就是很容易跨平台編譯,比如我在 Windows 就可以編譯出 Linux 上跑的 ELF 執行檔。

今天在電腦的 WSL2 環境,想要驗證一下功能是否正常,突然發現,原本上面那個例子,居然要跑到 200 秒?

我連 pprof 都用上了,但還是只能看到問題是出在 System Call 上,中間一度還懷疑到我有一個很大的 struct 變數,在遞迴函數裡並不是傳遞 pointer 的緣故!

也懷疑是否是用了大量的 fmt.Sprintf 導致!

最後看到一篇文章,才發現是檔案系統的問題,但我不太確定是 NTFS 的鍋還是 WSL2 + NTFS 的鍋就是了?

總之,要在 Linux 跑程式,還是要使用 Linux 相關的檔案系統比較不會有問題,尤其我的工具就是要寫一大堆跟 cell 有關的報表。

2023年11月20日 星期一

交接專案待釐清事項

Go module

go.mod 裡面的 module 是 local path,故重拉一個新的 git 下來時,需執行 go mod download、go get local module\pkg\xxxxx 等、go get local module\cmd,三年前開發時的做法是否是對的?

Node.js npm mongodb

N 年前開發時使用的是 3.1.6 版本,package.json 寫法是 "^3.1.6",此寫法表示是相容該指定版本,此時此刻在我電腦重新拉 git,npm 會安裝 3.7.4 版本,如果執行自動測試,connect MongoDB 便會有問題,錯誤是無法連接,在 package.json 拿掉 ^,再 npm install 一次,強制使用 3.1.6 版本就正常,應該是跟 driver 安全政策有關,待驗證。

更正:不是跟安全性政策有關,單純只是從 Node.js 18 開始,解析 DNS 時,某些環境會導致 localhost 被解析為 IPv6 address,請詳此處。 

其實錯誤訊息也可以看出被解析為 IPv6 address,昨天忙著寫文件沒有時間去解決問題,今天還是順利的找到原因了XD


有時候一個問題要不要修複,其實考量的點很多,絕對不是選擇一條最快解決的路就好。以我例子來說,這是個內部專案不會對外,故沒有安全性問題;同事依照我的文件是否可以正常執行沒有副作用?Yes(當然有極小可能性會有問題,畢竟其他 modules 不一定跟我當初版本一樣,就跟這個問題的發生原因一樣)。

故我昨天強制指定版本就是最快也是對我最方便的解決方式,當然沒有時間順便修掉 warning 也是有點小遺憾,就看之後時間允不允許囉?

2023/11/21 下午更新

早上還不覺得怎樣?下午越想越不對勁!如果說是因為 Node.js 實作的改變,mongodb 本身應該也有用到一些 Node.js 相關呼叫,否則無法解釋指定 3.1.6 版本就沒有問題的事實?

總之,這個問題解法真多,還有不改 code 直接帶參數給 node.exe 的解法!

2023/12/03 更新

版本真的是一個大問題!尤其專案是 N 年前開發的!陸續在其他交接專案都有遇到不能執行的問題,不論是我離職同事寫的 Server 還是我的 Device simulator,還好 N 年前開發時的資料夾還沒砍掉,順利找到當初使用的版本,只是虛驚一場。

這告訴我們一件事,package.json 版本千萬不要使用星號

2023年9月6日 星期三

Coding is magic

軟體工程師是很幸福的一個職業,假設不要碰到硬體的話?當然跟數學比起來,硬體還是簡單多了(所謂的簡單只是站在韌體的角度來看,且要控制的硬體有清楚的 Spec 可以參考)

不論是公事還是私事,只要有一台電腦而且是可以寫 Code 解決的事,我們都可以開心的寫 Code 來幫助工作?

今天看到一個專案 wphpfpm,它主要用途是在 Windows 下管理 php-cgi 的行程,也就是類似 Apache server 的 mod_fcgid。

還記得我的第一個 Web 服務就是用 C 寫一個 cgi 程式並透過 FastCGI 的協定與 Apache 溝通,當時的我 Web 相關技能只會 PHP 且對後端矇矇矓矓,才會這樣繞一大圈做這種脫褲子放屁的事(PHP 本身就可以發 web request 給其他網站了,既然都要寄生在 Apache 下,何苦使用 C 語言找自己麻煩?雖然我因為這樣多學會了很多編譯第三方函式庫的小技巧就是XD)

當初在開發的過程中,發現 C 確實沒那麼好用,也因為如此認識並學會了 Go 這個語言,雖然後來幾乎沒在寫 Web 服務,但也多學會了一技傍身。

巧的是這個作者也是使用 Go 來開發專案,讓我不禁想到那段往事,雖然我也想不太到這個專案的用途就是?既然都會 Go 了,Apache Server + PHP 都可以直接用 Go 做掉,使用 Go 開發一個 FastCGI 管理程式確實意義不是很大?

不過,Just for fun 無價,正所謂 Coding is magic …

2023年8月31日 星期四

The GUI framework of Go - Wails

太久沒有吸收新知了,Go 除了增加 generics 外,還有一個除了 Fyne 以外的 GUI framework 可以選擇,那就是 Wails!

Wails 是一個以 Go 為後端以及使用 vue 為前端的 GUI framework,除了 Android 不支援外,它支援三大平台,分別是 Windows、Linux 還有 macOS。

在 Windows 上,它需要 WebView2 runtime 來作渲染,故本質上很像個跑在瀏覽器上的 App,如果將其跑在未安裝 WebView2 runtime 的環境,執行時也會貼心的跳出提醒視窗並安裝。

以官方 Hello World 的例子來說,編譯出來的執行檔只有 8.38 MB,執行的速度還算不錯,只有啟動及關閉時會覺得稍有延遲。

這個專案其實已經發展了四年多(2019/4/25 - v.11.2),只是最近才在 PTT 軟體版推文中看到,如果不是太複雜的 App,也許以後可以考慮使用,當然為了方便的話,我用 wxWidgets 還是最快的,也可以跨平台跑在 Linux 上。

2023年1月11日 星期三

JSON array with array value in Go

太久沒寫 Golang 了,今天幫一個同事轉資料,資料格式類似 array 裡面又是 array,以 C 語言來類比就是 2 維陣列(真實例子比較複雜,維度有到 3 維,但概念是一樣的)。

一開始只想到用 struct 來定義格式並忽略欄位名稱,但這方式其實是錯的,還好後來有找回記憶,簡單記錄一下避免忘記。

簡單來說,Golang 的 struct 對應的是 JSON object,slice 對應的才是 JSON array,把握這個原則,JSON 應該就難不倒你了。

當然相比 Javascript 或是 Python,Go 內建的 encoding/json 模組確實沒那麼好用,網路上有很多人另外寫了 encoder/decoder 或是 parser,可以去 github 挑一個順眼的來用。

網路上也有神人寫好對應的 tool,可以來這裡測試。

另外,JSON 第一層一定要是 Object,不能是 Array。

想要的格式

{
    "array1": [
        [
            1,
            2,
            3
        ],
        [
            4,
            5,
            6
        ]
    ],
    "array2": [
        [
            1,
            2,
            3
        ],
        [
            4,
            5,
            6
        ]
    ]
}

程式碼

func main() {
array1 := [][]int{
{1, 2, 3},
{4, 5, 6},
}

array2 := [][]int{
{1, 2, 3},
{4, 5, 6},
}

arrays := make(map[string][][]int)

arrays["array1"] = array1
arrays["array2"] = array2

jsonStr, _ := json.MarshalIndent(arrays, "", "    ")

fmt.Println(string(jsonStr))
}

2022年1月1日 星期六

手裡拿著一把槌子,看什麼都覺得是釘子

寫程式的時候很容易掉到思考陷阱中!

昨天想要測試在 Windows 下抓螢幕及模擬滑鼠點擊動作,雖然我 10 幾年前都是用 Win32 API 搞定,但時代在進步,既然多會了幾種語言,就是想要使用這幾年常用的語言來達到我的需求。

我的目標就是想要快速的實作出來,故才會掉入思考陷阱。

一開始就覺得 Node.js 應該有一堆套件可用,但在編譯時遇到問題,單看錯誤訊息也不是一下可以解決的事。

接著使用 Golang,也是遇到編譯問題,看來不管是 Node.js 還是 Golang,底層還是呼叫 Win32 API,故才會有這些編譯的問題(謎之音,那一開始用 C 不就好了,搞不好只要 15 分鐘)?還好作者有提示使用的 C Compiler,安裝了作者使用的 Compiler 後馬上就搞定。順便一提,這兩個功能我一開始是分兩次搜尋,其實最後一套就可以搞定這 2 個需求。


花了約 2 個小時完成概念,行數只有 86 行,大部份的時間都在找套件及解決編譯問題。雖然有達到我想要做的事,但好不好用那又是另外一個故事了XD

直到今早我才想到,我的目標是想要快速實作出來,為什麼要捨近求遠,一開始直接去抄我以前寫的 C code 不就搞定?

另外則是執著在想要使用 Node.js 和 Golang,明明我以前也用過 AutoIt 來解決一些 MIS 工作的痛點,但我昨天就是沒有想到該語言。

我想,寫程式前如果多想一點,應該可以少走很多冤枉路XD

2021年9月10日 星期五

Named Capture Group in Golang

一直以為在 Regex 的匹配中,可以將 ( ) 裡面的匹配字串取出來是 Javascript 或是 Python 獨有的功能,導致我之前在取 Redfish 的 @odata.type 時,只能用 strings package 去解決我的問題。

今天才了解這是正規表示法的一部份,想當然爾 Golang 當然是有支援的,其用法如下:

re := regexp.MustCompile(`v([0-9]{1,})_([0-9]{1,})_([0-9]{1,})`)
matches := re.FindStringSubmatch("#EventService.v1_7_1.EventService")
fmt.Println(matches)

[v1_7_1, 1, 7, 1]

我們也可以為這些 Group 取名,例如 (?P<MAJOR>[0-9]{1,}),這樣 MAJOR 便會是這個 Group 的名稱,因為在 Golang 還要多一道手續自己作 mapping,故我覺得在這邊取變數的意義就不大了。

2021年8月26日 星期四

Golang os/exec command

最近為了抓取資料分析,需要定時執行一個 linux command 並儲存結果,由於最近都在使用 Golang,當然優先考慮使用 Golang 開發。

在 os/exec 有一個 Cmd struct,我們可以使用 exec.Command(...) 來得到這個 Cmd 指標,接著可以使用 Cmd.Output( ) 得到執行結果。

看似很單純的程式,但只有第一次執行成功,接著都會失敗,錯誤訊息為"exec: Stdout already set"。

看了一下 source code,原來是程式會檢查 c.Stdout 是不是有設定過,換句話說,這個 struct 只能使用一次,不能重覆使用,解決方式也很簡單,不要使用 for loop 外面的變數,每次在 loop 裡面重新 new 一個即可。

另外,由於 linux command 的結果已經帶有換行字元,我們在顯示執行結果時就不需要再加換行字元了。

可惡,害我要明天才能看到結果,今天沒有心情做事了XD

2021年6月25日 星期五

A pointer variable in the loop statement of Golang

雖然這幾個月幾乎都用 Golang 做事,但我學 Golang 已經是好幾年前的事,即使買了也看了好幾本書,但因為中間都沒有用它來做什麼專案,故很多坑洞都要等到真的碰到才知道發生什麼事了?

當然這裡的坑洞指的不是 Golang 本身的缺限,而是因為不常用所衍生出來的問題XD

今天下午就讓我碰到一個問題,我也是直到剛才把程式簡化才明白發生了什麼事?

簡單來說,如果我有一個 slice 裡面存放的是指標變數,在使用 for loop 語法時,在 for loop 裡面的那個指標變數,假設我們稱它為 p 好了,索引值假設是 0,也就是第 0 個元素。

此時雖然 p 和 slice[0] 指的都是同一個實體的位址,但 p 只是另外一個指標變數,並不等於 slice[0] 的那個變數,故假設我們把 p 指向另外一個實體的位址,原本 slice[0] 指的還是原本的地方。


整體概念其實有點像 C,也就是要變成指標的指標,才能將原本 slice[0] 指到另外一個位址,當然 Golang 裡面沒有這種東西,故我們要直接使用 slice[0] 變數,而不要使用 p 變數 。

模擬程式如下:


package main

import (
    "fmt"
)

type Device struct {
    ID string
}

type Arg struct {
    Devices []*Device
}

func main() {
    arg := Arg {
        Devices : []*Device{
            &Device {
                ID : "1",
            },
            &Device {
                ID : "2",
            },
        },
    }

    d3 := &Device {
        ID : "3",
    }

    for i, d := range arg.Devices {
        fmt.Printf("idx(%d) d=%p arg.Devices[%d]=%p d3=%p\n", i, d, i, arg.Devices[i], d3)
        //d = d3
        arg.Devices[i] = d3
        fmt.Printf("idx(%d) d=%p arg.Devices[%d]=%p d3=%p\n", i, d, i, arg.Devices[i], d3)
    }

    for i, d := range arg.Devices {
        fmt.Println(i, d.ID)
    }
} 

2021年3月22日 星期一

init function in Go package

撇開小程式不說,最近算是第一次把 Go 用在正式專案中,過程中不斷對架構修修改改,雖然我覺得這不啻是一件好事?表示有去思考怎樣寫對程式會更好,如果能在下筆前就先想清楚,寫起程式來應該會更行雲流水吧。

我算是以 C 語言出道的程式設計師,故很多時候我都只考慮到函數面向,坦白說光能夠適當的把函數拆分到不同的程式檔案中,這個程式已經有六成的可調整性,再來如果能夠把一些 Header 檔案的 include 相依性考慮清楚,我想九成的需求調整都不是什麼問題。

平常我在寫 Node.js 或是 Python 也是如此,Python 我不清楚,但 Node.js 是允許可以循環參照的,故只要不濫用全域變數(最多只用全域常數),大多時候應該是沒有什麼問題。

Go 本身不允許循環參照,我覺得這是一件好事,就像 Go 強制你要檢查 err != nil,藉由一些限制及規範,強迫使用者在寫程式前就先考慮清楚,其實也是方便你未來擴充程式。

以我的專案來說,我至少需要開發兩支獨立的執行檔,且這兩支用到的第三方模組都是一樣的,故當我在開發第一支執行檔時,我就打定主意使用多 package 的方式開發,這樣某些自己的 package 也可以給第二支執行檔使用。

雖然看了四、五本以上的 Go 書籍,但都已經是三年前的事,故我的程式架構在開發時覺得是順理成章的事,後來卻變成一個又一個的疑問,尤其現在開始開發第二支執行檔時特別明顯!

比如說,一個程式中有好多個 package,每個 package 都用了 package A,如果 package A 又有使用 init function,到底 init 會被執行幾次?雖然從結果來看只會執行一次,但我還是想要有個標準答案。

幸好 Go 本身就有提供 Spec 文件,不用像 C 一樣還要花錢買。於是很快的就在 Spec 中找到解答。

能夠每天在程式語言的道路上學習到新東西,那真的是一件快樂的事。

2021年3月4日 星期四

Golang data race with fmt package

Goruntine 是 Go 語言裡面一個很重要的功能,它是一個輕量化的執行緒,由 Go runtime 負責去排程執行,據官方說法,同時開啟好幾千個也沒問題。

使用 Golang 開發專案時或多或少都會使用到 Goruntine,我們也很習慣在 Goruntine 裡面直接使用 fmt package,但 fmt 並不保證 concurrency safe。

最近我在專案中使用了 GINlogrotate,發現在某些情況下會導致 data race,底下便是我精簡過的程式碼。


package main

import (
    "bufio"
    "errors"
    "path/filepath"
    "fmt"
    "log"
    "os"
    "sync"
    "time"
)

type Options struct {
    Directory string
    FileNameFunc func() string
}

type Writer struct {
    logger *log.Logger

    opts Options

    f *os.File
    bw *bufio.Writer
    bytesWritten int64

    queue chan []byte
    pending sync.WaitGroup
    closing chan struct{}
    done chan struct{}
}

func (w *Writer) Write(p []byte) (n int, err error) {
    //p := make([]byte, len(b))
    //copy(p, b)

    select {
    case <-w.closing:
        return 0, errors.New("writer is closing")
    default:
        w.pending.Add(1)
        defer w.pending.Done()
    }

    w.queue <- p

    return len(p), nil
}

func (w *Writer) Close() error {
    close(w.closing)
    w.pending.Wait()

    close(w.queue)
    <-w.done

    if w.f != nil {
        if err := w.closeCurrentFile(); err != nil {
            return err
        }
    }

    return nil
}

func (w *Writer) listen() {
    for b := range w.queue {
        if w.f == nil {
            if err := w.rotate(); err != nil {
                w.logger.Println("Failed to create log file", err)
            }
        }

        size := int64(len(b))

        if _, err := w.bw.Write(b); err != nil {
            w.logger.Println("Failed to write to file.", err)
        }
        w.bytesWritten += size
    }

    close(w.done)
}

func (w *Writer) closeCurrentFile() error {
    if err := w.bw.Flush(); err != nil {
        return errors.New("failed to flush buffered writer")
    }

    if err := w.f.Sync(); err != nil {
        return errors.New("failed to sync current log file")
    }

    if err := w.f.Close(); err != nil {
        return errors.New("failed to close current log file")
    }

    w.bytesWritten = 0
    return nil
}

func (w *Writer) rotate() error {
    if w.f != nil {
        if err := w.closeCurrentFile(); err != nil {
            return err
        }
    }

    path := filepath.Join(w.opts.Directory, w.opts.FileNameFunc())
    f, err := newFile(path)
    if err != nil {
        return errors.New("failed to create new file")
    }

    w.bw = bufio.NewWriter(f)
    w.f = f
    w.bytesWritten = 0

    return nil
}

func New(logger *log.Logger, opts Options) (*Writer, error) {
    if _, err := os.Stat(opts.Directory); os.IsNotExist(err) {
        if err := os.MkdirAll(opts.Directory, os.ModePerm); err != nil {
            return nil, errors.New("directory does not exist and could not be created")
        }
    }

    w := &Writer{
        logger:  logger,
        opts:    opts,
        queue:   make(chan []byte, 2000),
        closing: make(chan struct{}),
        done:    make(chan struct{}),
    }

    go w.listen()

    return w, nil
}

func newFile(path string) (*os.File, error) {
    return os.OpenFile(path, os.O_WRONLY|os.O_TRUNC|os.O_CREATE, 0666)
}

func test(wg *sync.WaitGroup, w *Writer) {
    i := 0

    var s2 string
    for i < 1 {
        s2 = "abcdefg"
        fmt.Println(s2)
        fmt.Fprint(w, s2)
        i++
    }

    wg.Done()
}

func FileNameFunc() (string) {
    t := time.Now()

    return fmt.Sprintf("%04d%02d%02d_%02d%02d%02d.log",
        t.Year(),
        t.Month(),
        t.Day(),
        t.Hour(),
        t.Minute(),
        t.Second(),
    )
}

func main() {
    var wg sync.WaitGroup

    logger := log.New(os.Stderr, "logrotate", 0)

    dir, err := os.Getwd()
    if err != nil {
        fmt.Println(err)
        return
    }

    opts := Options{
        Directory: dir,
        FileNameFunc: FileNameFunc,
    }

    writer, err := New(logger, opts)
    if err != nil {
        logger.Println(err)
        return
    }

    max := 2000
    i := 0
    for i < max {
        wg.Add(1)
        go test(&wg, writer)
        i++
    }

    wg.Wait()
}

這是測試的結果。

==================
WARNING: DATA RACE
Read at 0x00c00000a523 by goroutine 7:
  runtime.slicecopy()
      c:/go/src/runtime/slice.go:197 +0x0
  bufio.(*Writer).Write()
      c:/go/src/bufio/bufio.go:635 +0x3e5
  main.(*Writer).listen()
      E:/test.go:77 +0x1ab

Previous write at 0x00c00000a523 by goroutine 185:
  runtime.slicestringcopy()
      c:/go/src/runtime/slice.go:232 +0x0
  fmt.(*buffer).writeString()
      c:/go/src/fmt/print.go:82 +0x107
  fmt.(*fmt).padString()
      c:/go/src/fmt/format.go:110 +0x6f
  fmt.(*fmt).fmtS()
      c:/go/src/fmt/format.go:359 +0x75
  fmt.(*pp).fmtString()
      c:/go/src/fmt/print.go:447 +0x1ba
  fmt.(*pp).printArg()
      c:/go/src/fmt/print.go:698 +0xdcf
  fmt.(*pp).doPrint()
      c:/go/src/fmt/print.go:1161 +0x12c
  fmt.Fprint()
      c:/go/src/fmt/print.go:232 +0x6c
  main.test()
      E:/test.go:154 +0x130

Goroutine 7 (running) created at:
  main.New()
      E:/test.go:138 +0x29b
  main.main()
      E:/test.go:190 +0x271

Goroutine 185 (finished) created at:
  main.main()
      E:/test.go:200 +0x2e0
==================
==================
WARNING: DATA RACE
Read at 0x00c00008b1bd by goroutine 7:
  runtime.slicecopy()
      c:/go/src/runtime/slice.go:197 +0x0
  bufio.(*Writer).Write()
      c:/go/src/bufio/bufio.go:635 +0x3e5
  main.(*Writer).listen()
      E:/test.go:77 +0x1ab

Previous write at 0x00c00008b1bd by goroutine 702:
  runtime.slicestringcopy()
      c:/go/src/runtime/slice.go:232 +0x0
  fmt.(*buffer).writeString()
      c:/go/src/fmt/print.go:82 +0x107
  fmt.(*fmt).padString()
      c:/go/src/fmt/format.go:110 +0x6f
  fmt.(*fmt).fmtS()
      c:/go/src/fmt/format.go:359 +0x75
  fmt.(*pp).fmtString()
      c:/go/src/fmt/print.go:447 +0x1ba
  fmt.(*pp).printArg()
      c:/go/src/fmt/print.go:698 +0xdcf
  fmt.(*pp).doPrintln()
      c:/go/src/fmt/print.go:1173 +0xb4
  fmt.Fprintln()
      c:/go/src/fmt/print.go:264 +0x6c
  fmt.Println()
      c:/go/src/fmt/print.go:274 +0xc0
  main.test()
      E:/test.go:153 +0x42

Goroutine 7 (running) created at:
  main.New()
      E:/test.go:138 +0x29b
  main.main()
      E:/test.go:190 +0x271

Goroutine 702 (running) created at:
  main.main()
      E:/test.go:200 +0x2e0
==================
Found 2 data race(s)
exit status 66


從上面我們可以看到,不管是 fmt.Println 或是 fmt.Fprintf,都有機會引起 data race!

目前我的解決方式是把 buffer 先複製起來,再傳進 logrorate 的 channel 裡。

p := make([]byte, len(b))
copy(p, b)

解題靈感來自

2021年2月4日 星期四

Go Modules

A module is a collection of Go packages stored in a file tree with a go.mod file at its root.


Example

module servermanagement/apiserver
go 1.14
require github.com/buger/jsonparser v1.1.1
require github.com/go-resty/resty/v2 v2.4.0

2021年1月22日 星期五

GUI app on Kobo Clara HD (04)

還是不想浪費 Golang 跨平台的能力,再加上不想花費太多時間,故想嘗試 HTML Golang GUI 的解決方式。

這次選擇的是 gowut 這個專案,無奈在 Clara HD 實機上測試時,還是無法正常執行。

第一個問題是它無法自動帶出瀏覽器視窗,這個尚屬合理。

第二個問題是即使自己開啟瀏覽器輸入網址,還是無法連上 gowut Server。

第三個問題是我改用 gowut 自己提供的測試網址,雖然看起來 GUI 元件有正常運作,但一來速度太慢,二來是多操作幾下就會整個無回應,三來是畫面會因為重繪而閃爍(電子紙特性)。

目前看來 HTML GUI 解決方式不是正解,光畫面會閃爍就無法讓人接受,不過這也延伸出另外一個問題,即使我使用 Qt 撰寫 GUI App,我一樣會遭遇畫面閃爍的問題,除非 Kobo 本身有提供 SDK 讓使用者呼叫暫時停止重繪,好避免畫面閃爍。

感覺 GUI App 這個問題快走入死胡同了?

2021年1月8日 星期五

GUI app on Kobo Clara HD (02)

Fyne 缺少的 libGL.so.1 確定是無法靜態連結了,至少就我目前看到的資料是如此。

在不考慮 GUI app 的情況下,其實寫個 Golang app 是最快的,在 Wi-Fi 開啟時就自動同步 Google Drive,就像 Kindle 同步個人文檔一樣,第一次時也是全部下載,並沒有讓使用者選擇,只是之後還是可以任意刪除再下載,甚至回過頭來刪除 Kindle Server 的個人文檔。

我想像中的行為大概是這樣:

1. 每一次同步後就更新資料庫,在不被 Google 拒絕的情況下,分次把書下載下來。
2. 下次同步會檢查是否有下載過,已下載過的不會再重新下載。
3. 使用者從 Google Drive 刪除檔案,同步後 Local 的資料庫也會刪除,只要再把書放回 Google Drive,下次同步就會視為未同步過的檔案,便可以再下載一次。

在沒有 GUI 的情況下也只能這樣做了,雖然我還不清楚 Google 阻擋的頻率,但感覺這整套流程應該是可行的。

就把這選項當成 plan B 吧。

2021年1月5日 星期二

GUI app on Kobo Clara HD (01)

昨天下班前終於開始了我計劃的第一步,首先便是嘗試 Fyne 這個 Golang 專案在跨平台編譯後是否能順利的跑在 Clara HD 上。

一開始便照著指引在 Linux 上準備 docker 環境,只要使用 go get github.com/fyne-io/fyne-cross,便可以自動下載 docker 所需要的環境,當然系統還是要先有 docker 本體。

回家後使用手機的 OTG 功能,修改我的 Kobo 翻頁器 script,直接啟動編譯好的執行檔。很可惜的是,無法順利啟動,從 log 中看到是缺少了 libGL.so.1 這個函式庫。

接下來便是研究是否可以使用靜態連結解決?也許這條路行不通也說不一定?

2020年9月18日 星期五

Golang filepath.Walk 行為及注意事項

某些情況下我們需要去解析目錄下含子目錄共有多少檔案,一般來說是使用遞迴,不同程式語言有不同的名稱,可能叫 parseDir 或是 walkThrough 等。

Golang 在 path/filepath 這個 package 裡面就有提供類似的功能,其用法為 filepath.Walk,我們需要傳入 2 個參數:

1. 要解析的目錄名稱。

2. WalkFunc 型式的函數,定義如下。

type WalkFunc func(path string, info os.FileInfo, err error) error

比較特別的是,我們在裡面不需要再寫出類似遞迴呼叫的語法,Golang 本身會一直呼叫我們傳進去的 WalkFunc 直到結束。

另外,也可以傳入 UNC 路徑,這邊要注意一點,由於微軟檔案長度 255 字元的限制,如果我們傳入的路徑裡面有超出長度的檔案,此時會回傳 error,故後面就會停止 parsing,錯誤訊息如下。

CreateFile ERROR_FILE_NAME: The system cannot find the path specified.

因此,如果我們還是要繼續 parsing,可以選擇忽略這個錯誤而直接回傳 nil。

2020年7月17日 星期五

Kobo Clara HD 翻頁器 DIY

自從知道 KoboCloud 這個專案後,我就一直在想能不能套用這個概念,自己寫一個 Client/Server 架構的專案來實現翻頁器的功能?由於 porting 一個藍芽 driver 沒那麼簡單,可能還需要 OTG 等,故我這個專案的概念很簡單:

01. Run a Web-Server on Clara, accept two API, /left and /right。
02. Another Client, send two API to Clara.
03. Web-Server clicks screen to simulate to turn page when receiving two API.

看起來應該沒有什麼大問題,目前第一個問題是要選擇哪一種語言來寫 Web-Server?有了 KoboCloud 的信心後,我決定使用 Golang,因為 Golang 本身就內建 Cross-Compiler 的功能,不用像 C 一樣,要準備編譯環境。

底下是我陸續嘗試的過程,留下記錄以供日後參考。

Web-Server

目標:
01. 仿造 KoboCloud,寫一個 KoboServer 機制 - Done
02. 快速寫一個 Server,在 Clara 上驗證可行性,看是否能收到 API - Done
03. Google Linux Mouse Event 之用法 - Done
04. 研究 Linux udev 機制,確定 rule 之行為 - Done
05. 根據 04,寫出一個穩定的 Server,避免 udev rule 一直觸發,運行太多的 Server 實體,導致 Clara 當機 - Done

Client

目標:
01. 當 Server Ready 後,先使用 PC 打 API 即可 - Done
02. Write an Android App to send two API to Server - Done
03. Write another Android App to receive Bluetooth event, then send two API to Server - Done

2020/07/17 日記

由於工作環境都是 Windows,所有的 Script 換行字元都要改成 Linux 下的,否則 Shell 會不開心,另外不論是 Script 或是 Server 的執行檔,移到 Linux 下包裝成 KoboRoot.tgz 時,記得先給執行權限,不然不確定會不會有問題?故最好的方式是直接在 Linux 下 開發,反正我的 UltraEdit 26 也有 Linux 版的授權。

Golang Cross-Compile 方式 on Windows

SET PATH=%PATH%;C:\Go\bin
SET GOPATH=%CD%

SET GOOS=linux
SET GOARCH=arm
SET GOARM=7

2020/07/20 更新

原本以為觸發 Linux Mouse Event 是一件很簡單的事,沒想到 Event Structure 會跟 Kernel 版本有關,如果是用到不對的 Structure,不論我 Event 怎麼組,一定什麼事都不會發生!結果還是需要花點時間整理消化資料。

幸好之前就有想到一招,就是在組 KoboRoot.tgz 時,裡面預留一道指令,把相關程式搬到 Kobo 顯示的電腦磁碟機內,則不論我是要抽換 HTTPServer 程式,或是要測試一些指令都會很方便,之後也可以如法泡製 udev 的 Script ,目前先留一個測試 Script 方便我除錯就好。


2020/07/21 更新

Google 一個 ParseDir 的 Script 函數,想要看看 Kobo 系統裡面有什麼,結果印了 10 萬多行後,後面全部是亂碼,我猜是因為 Stack 爆掉了,因為 ParseDir 是使用遞迴,而印象中 Clara 記憶體只有 512 MB,晚點最好是只 Parse /etc or /usr/local 等等的資料夾,或者也可以直接下 cp 指令把整個 root 複製一份到 /mnt/onboard 上,我認為 udev 執行時應該是 root 的權限。

至少目前可以看出一些資訊了:(From dmesg command)

01. Linux version 4.1.15-00089-ga2737fc02713 (gallen@gallen-M51AC) 
02. gcc version 5.3.0 (GCC)
03. tps6518x 1-0068: PMIC TPS6518x for eInk display
04. mousedev: PS/2 mouse device common for all mice
05. Battery Table (Open Circuit Voltage)
PMU: ricoh61x_set_OCV_table : 00% voltage = 3590900 uV
PMU: ricoh61x_set_OCV_table : 10% voltage = 3687400 uV
PMU: ricoh61x_set_OCV_table : 20% voltage = 3742300 uV
PMU: ricoh61x_set_OCV_table : 30% voltage = 3774100 uV
PMU: ricoh61x_set_OCV_table : 40% voltage = 3788700 uV
PMU: ricoh61x_set_OCV_table : 50% voltage = 3814400 uV
PMU: ricoh61x_set_OCV_table : 60% voltage = 3874200 uV
PMU: ricoh61x_set_OCV_table : 70% voltage = 3927900 uV
PMU: ricoh61x_set_OCV_table : 80% voltage = 3982900 uV
PMU: ricoh61x_set_OCV_table : 90% voltage = 4057300 uV
PMU: ricoh61x_set_OCV_table : 100% voltage = 4141600 uV
06. PMU: ricoh61x_init_fgsoca : * Rbat = 233 mOhm   n_cap = 1385 mAH (1500 ?)
07. SD Card
mmc0: new ultra high speed DDR50 SDHC card at address aaaa
mmcblk0: mmc0:aaaa SS08G 7.40 GiB 
mmcblk0: p1 p2 p3
VFS: Mounted root (ext4 filesystem) on device 179:1.
08. PMU:_config_ricoh619_charger_params set SDP 500mA charging.
09. Event Information.
/dev/input/by-path/platform-1-0024-event
/dev/input/by-path/platform-ntx_event0-event
/dev/input/event0
/dev/input/event1
/dev/input/mice

晚上試著使用 cp 指令複製 /,一直無法成功,不知道是為什麼?
目的地要寫絕對路徑,不能用相對路徑。

ls -al / 結果
ls -al /usr/local 結果
cat /proc/bus/input/devices 結果

2020/07/22 更新

從 /proc/bus/input/devices 來看並搭配 Google 結果,/dev/input/event1 應該是 TouchPanel,所以我們應該從這下手,而不是找跟 Mouse Event 有關的。

先用手機來抓 Event Raw Data,我的手機是 /dev/input/event4(一個一個試出來的),使用 cat 記錄按下時的 data,結果如下圖。


struct input_event {
    struct timeval time;
    unsigned short type;
    unsigned short code;
    unsigned int value;
};


下班再來試試 Kobo 的 Raw Data 是否有不一樣的地方。

回家依樣畫葫蘆抓取 Raw Data,不抓不知道,看起來 Clara HD 是 32 位元 OS?


針對翻頁這件事終於開始有點曙光了,現在只剩是否能用寫檔方式模擬點擊 Touch Panel 動作?

2020/07/25 更新

終於把概念實做出來了,早上就已經完成了向右翻頁,但一整天都搞不定向左翻頁,現在雖然搞定了,但還需要花時間整理資料。

影片連結

程式碼

Event

參考資料

2020/07/26 更新


2020/07/27 更新

執行 Web Server 後,記憶體增加了 380 KB,佔總記憶體 0.07 %,我想應該還算可以吧?


2020/07/28 更新

看了一堆 Kobo hacks 的相關資料,貌似在閱讀界面時,Wi-Fi 是被 Kobo 關起來的?可能是為了省電!也許我可以在 HTTPServer 裡跑一個 goruntime,只要偵測到 Wi-Fi 關閉,就再把它打開,就看對耗電量的影響如何。

晚上實測的結果,即使一直使用我的 KoboPageTurner 翻頁,約莫 2 分 10 秒後,Wi-Fi 便會自己關閉,有空再用 ping command 來確認更精確的時間。

2020/07/29 更新

這是從 Clara HD 說明書看到的,看起來是被系統自己關掉 Wi-Fi 沒錯,但是沒有活動是如何判斷?我猜可能是判斷是否有在做同步,或是有進 Kobo Store,亦或是有開瀏覽器,而不是真正的判斷是否有使用中的 TCP/IP 封包。


晚上實測的結果,在不動作的情況下,不論是在首頁還是 Kobo Store,也是約 2 分鐘多一點,Wi-Fi 便會自動關閉。另外,也試了不要休眠,關掉自動喚醒功能,還是無法解決問題,看來是像說明書說的是由系統自動關閉。

2020/07/31 更新

早上突然想到,也許 Kobo 檢查的是外網的封包,而 Web Server 是屬於內網的封包,故會被認為 inactive?雖然我個人是覺得不合理就是!最簡單的方式就是在 Web Server 內開一個 goruntime,定時一分鐘便去連 Kobo Store,應該就可以避免 Wi-Fi 被關閉,待驗證?測試的結果,沒用。

哈,知道方向就好搞,在我多方 Google 之下,終於發現一個設定可以避免 Wi-Fi 被關閉,但是網路上說沒有效?晚上測試的結果,果然不會再自動斷線了,但是 Wi-Fi 也無法再手動關閉,故這個方式也不是正解。

[DeveloperSettings]
ForceWifiOn=true

另外,從 cyttsp5_mt_process_touch (kernel/drivers/input/touchscreen/cyttsp5_mt_common.c) 這支 function,看起來有解釋如何計算 ABS_X 和 ABS_Y,但我心算似乎不符合我從 raw data 抓到的?

2020/08/01 更新

忙了幾天,唯一的收獲是知道 Clara HD 的原點是在右上角,且設值時 X, Y 要互換。

2020/08/02 更新

改了一版程式,Web Server 開啟時,會去設定 "ForceWifiOn=true",離開 Web Server 時再把設定設成 false,實測的結果並沒有幫助,我確定設定檔都有更改成功!唯一可能的解釋是當我們在程式中動態改變設定檔時,對 Kobo System 來說,那些值可能已經在它的 memory 裡,故它並不知道要強制開啟 Wi-Fi,如果有方法可以強制它重讀設定檔,也許這個解決方式就能生效?

我不確定插上 USB 生成磁碟機,接著退出磁碟機的那個時間點,Kobo 系統是否會重新載入設定檔,如果這個流程是確定的,也許有機會解決?

另外,我發現這個動態更改設定檔的方式,似乎有機會讓 Wi-Fi 在開開關關幾次後就無法使用,可能是 Kobo 比對 memory 和 file 後,因為未同步而導致錯亂?

2020/08/15 更新

忙了兩個禮拜,終於有時間再來搞一下這個專案,Google 了一下,寫了一個簡單的 Android App,終於實現完我當初所有想做的事。



下一個新目標:使用 ESP8266 實做硬體翻頁器。

1. 如何實做 3.3 V 供電電路?
2. 供電電路是否需要穩壓?
3. 外殼使用 3 D 列印?

2018年10月18日 星期四

go-seo SEO 小工具

之前就很想幫 blog 某些文章增加搜尋度
故研究了一下要如何實現

後來發現只要下的關鍵字可以在 google 結果中找到 blog 頁面
這時再去點取該 blog 頁面
Blogger 系統便會在該 blog 頁面瀏覽次數加 1
於是就達成了我的目標

如此一來,我最受歡迎的頁面應該就不是大台北行政區圖了吧

相關程式請詳
https://github.com/tylpk1216/go-seo

2018年5月8日 星期二

Good Golang timing

Mat Ryer 在 Golang UK Conference "Idiomatic Go Tricks" talk 中
寫了一個 timer 的範例

看了這個 code 才知道,自己跟高手的差距有多大
至少我從來沒想到閉包可以這樣用