简介
Go实现 监控读卡器设备存储空间变化, 自动烧写SD Card, 烧写完成之后自动弹出, 显示执行状态, 还支持热插拔。
步骤
代码
lsblkParser.go
imageWriter.go
package actionimport ("fmt""os/exec"
)type ImagerWriter struct {image stringdevice string
}func NewImagerWriter(img, dev string) (*ImagerWriter, error) {var (writer = &ImagerWriter{image: img, device: dev})return writer, nil
}func (w *ImagerWriter) Write(img, dev string, process func(process float32) error) error {var (imgFilePath = imgdevice = deverr error)if 0 >= len(imgFilePath) {imgFilePath = w.image}if 0 >= len(device) {device = w.device}/*rpi-imager - Flash disk images to removable storage--cli Do not launch the graphical interface; just flash the image given to the specified drive.--disable-telemetry Do not report OS writes to http://rpi-imager-stats.raspberrypi.com/--disable-verify After writing the image, do not attempt to verify that the image was written correctly. Only valid when run with --cli.--quiet Suppress all console output. Only valid when run with --cli.*/cmd := exec.Command("rpi-imager", "--cli", "--disable-verify", "--quiet", imgFilePath, device)var output []byteif output, err = cmd.CombinedOutput(); nil != err {fmt.Println(string(output))return err}return err
}
state.go
package actionimport "fmt"type WorkState intconst (WS_WaitPlugIn = iota + WorkState(0)WS_WritingWS_WaitPlugOff
)type ResultState intconst (RS_Normal = iota + ResultState(0)RS_SucceedRS_Failed
)type ActionState struct {DeviceName stringWorkState WorkStateResultState ResultState
}func (w WorkState) String() string {switch w {case WS_WaitPlugOff:return "WaitPlugIn"case WS_Writing:return "Writing"default:return "WaitPlugIn"}
}func (w ResultState) String() string {switch w {case RS_Failed:return "Failed"case RS_Succeed:return "Succeed"default:return "Normal"}
}func (a ActionState) String() string {return fmt.Sprintf("%s, %s, %s", a.DeviceName, a.WorkState, a.ResultState)
}
action.go
package actionimport ("errors""pi_image_writer/utils""time"
)var canceled_error = errors.New("canceled")type StateCallbackFunc func(status ActionState) errortype Action struct {callback StateCallbackFuncdevice stringimage stringstarted boolcanceling chan struct{}lsblkParser utils.LSBlkParserwaitDeviceInterval time.DurationimagerWriter *ImagerWriter
}func NewAction(img, dev string, callback StateCallbackFunc) (*Action, error) {var (act = &Action{device: dev,image: img,callback: callback,canceling: make(chan struct{}),waitDeviceInterval: time.Millisecond * 200,}err error)act.imagerWriter, err = NewImagerWriter(img, dev)return act, err
}func (a *Action) Start() error {go a.run()return nil
}func (a *Action) Stop() error {if a.started {select {case a.canceling <- struct{}{}:case <-time.After(time.Second * 10):return errors.New("timeout error")}}return nil
}func (a *Action) run() {var (state = ActionState{DeviceName: a.device,WorkState: WS_WaitPlugIn,ResultState: RS_Normal,}err errorstep = int(WS_WaitPlugIn))for !errors.Is(err, canceled_error) {if step > int(WS_WaitPlugOff) {step = int(WS_WaitPlugIn)}/* update result status */switch WorkState(step) {case WS_WaitPlugIn:case WS_Writing:state.ResultState = RS_Normalcase WS_WaitPlugOff:}/* update work status */state.WorkState = WorkState(step)if err = a.callback(state); nil != err {state.ResultState = RS_Failedstate.WorkState = WS_WaitPlugOffstep = int(WS_WaitPlugOff)continue}switch WorkState(step) {case WS_WaitPlugIn:err = a.waitPlugIn()case WS_Writing:err = a.writing()case WS_WaitPlugOff:err = a.waitPlugOff()state.ResultState = RS_Succeed}if nil != err {state.ResultState = RS_Failedstate.WorkState = WS_WaitPlugOffstep = int(WS_WaitPlugOff)a.callback(state)continue}step += 1}
}func (a *Action) waitPlugIn() error {var item utils.LSBlkItemfor {items, err := a.lsblkParser.GetSizes([]string{a.device})if nil != err {return err}select {case <-a.canceling:return errors.New("canceled")default:}if 0 < len(items) {item = items[0]if 0 < item.Size {return nil}}time.Sleep(a.waitDeviceInterval)}
}func (a *Action) writing() (err error) {err = a.imagerWriter.Write("", "", nil)select {case <-a.canceling:return errors.New("canceled")default:}return err
}func (a *Action) waitPlugOff() error {var item utils.LSBlkItemfor {items, err := a.lsblkParser.GetSizes([]string{a.device})if nil != err {return err}select {case <-a.canceling:return errors.New("canceled")default:}if 0 < len(items) {item = items[0]if 0 >= item.Size {return nil}}time.Sleep(a.waitDeviceInterval)}
}
main.go
package mainimport ("fmt""os""pi_image_writer/action""sync""time"
)func outputTerminal(format string, args ...interface{}) {// 清除第一行的内容并返回行首fmt.Fprintf(os.Stdout, "\r"+"[%s] %s"+"\033[K", time.Now().Format("2006-01-02 15:04:05"), fmt.Sprintf(format, args...))// 刷新输出os.Stdout.Sync()
}func watcher(actStatusChan chan action.ActionState) {var (actStatus = make([]action.ActionState, 0)printStr stringfound bool)for {select {case status := <-actStatusChan:found = falsefor i := range actStatus {if actStatus[i].DeviceName == status.DeviceName {found = trueactStatus[i].WorkState = status.WorkStateactStatus[i].ResultState = status.ResultStatebreak}}if !found {actStatus = append(actStatus, status)}case <-time.After(time.Second):printStr = ""for i := range actStatus {if 0 < len(printStr) {printStr += "; "}printStr += actStatus[i].String()}outputTerminal(printStr)}}
}func main() {var (devs = []string{"/dev/sda", // SD Card读卡器1"/dev/sdb", // SD Card读卡器2}imageFile = "/home/pi/2024-03-15-raspios-bookworm-arm64-lite.img"waiter sync.WaitGroupactStatusChan = make(chan action.ActionState, 2))for _, dev := range devs {waiter.Add(1)act, err := action.NewAction(imageFile, dev, func(status action.ActionState) error {actStatusChan <- statusreturn nil})if nil != err {fmt.Printf(dev, err.Error())waiter.Done()continue}act.Start()}go watcher(actStatusChan)waiter.Wait()
}
补充说明
-
main函数上的设备是你读卡器的真实设备, 你可以自己使用lsblk查看, 插拔SD Card读卡器前后进行查看, 记住插拔时间隔3~5s再看
-
镜像文件需要自己先放到树莓派中, 再在main函数设置成你的路径
缺点
- 烧写中间拔插掉SD Card之后系统会一直显示存储空间还在, 但其实SD Car已拔出
重新插拔 SD Card读卡器即可