感谢@whc2001提供的C#版本源代码
用于解压某电子琴学习机的数据包文件
闲着没事移植去了Python,方便使用

import os
import struct
from typing import List, Tuple

Encoding = 'gbk'
key = None

def getKey(file_data: bytes) -> bytes:
    key_a = file_data[1024:1024+32]
    key_b = file_data[1056:1056+32]
    ret = bytearray(32)
    for i in range(32):
        ret[i] = (key_b[i] - key_a[i]) % 256
    return ret


def getDir(file_data: bytes) -> List[Tuple[str, int, int]]:
    ret = []
    dir_num = struct.unpack('I', file_data[252:256])[0]
    for i in range(dir_num):
        offset = 1280 + i * 128
        item = bytearray(file_data[offset:offset+128])
        for j in range(len(item)):
            item[j] = (item[j] - key[j % 32]) % 256
        dir_name = item[:120].decode(Encoding).rstrip('\x00')
        dir_data_length = struct.unpack('I', item[120:124])[0] << 7
        dir_data_offset = struct.unpack('I', item[124:128])[0]
        ret.append((dir_name, dir_data_length, dir_data_offset))
    return ret


def getFile(file_data: bytes, dir_data_offset: int, dir_data_length: int) -> List[Tuple[str, int, int]]:
    ret = []
    for i in range(dir_data_length // 128):
        offset = dir_data_offset + i * 128
        item = bytearray(file_data[offset:offset+128])
        for j in range(len(item)):
            item[j] = (item[j] - key[j % 32]) % 256
        file_name = item[:64].decode(Encoding).rstrip('\x00')
        song_data_offset = struct.unpack('I', item[64:68])[0]
        song_data_length = struct.unpack('I', item[68:72])[0]
        ret.append((file_name, song_data_offset, song_data_length))
    return ret


def getSong(file_data: bytes, song_data_offset: int, song_data_length: int) -> bytes:
    return file_data[song_data_offset:song_data_offset+song_data_length]

def main(input_path: str, output_path: str):
    global key
    if not os.path.exists(input_path):
        print(f"File Not Found in {input_path}")
        exit(1)
    if not os.path.exists(output_path):
        os.makedirs(output_path)

    with open(input_path, 'rb') as f:
        data = f.read()
    key = getKey(data)
    dirs = getDir(data)
    for dir_name, dir_data_length, dir_data_offset in dirs:
        dir_path = os.path.join(output_path, dir_name)
        os.makedirs(dir_path, exist_ok=True)
        songs = getFile(data, dir_data_offset, dir_data_length)
        for file_name, song_data_offset, song_data_length in songs:
            print(f"{dir_name} -> {file_name}")
            song_data = getSong(data, song_data_offset, song_data_length)
            with open(os.path.join(dir_path, f"{file_name}.mid"), 'wb') as f:
                f.write(song_data)

if __name__ == "__main__":
    import sys
    if len(sys.argv) != 3:
        print("Usage: PianoCatSongDataExtractor.py <INPUT_FILE> <OUTPUT_FOLDER>")
        exit(1)
    main(sys.argv[1], sys.argv[2])

前言

之前在玩VIVO的老手机,其中涉及到了一些VIVO对fastboot的魔改
通过修改fastboot本身的源码,目的就是为了添加两个指令,感觉不是很优雅。
还是使用Libusb来解决为好,也省得需要配置编译器再编译可执行文件的麻烦事儿了。
同时也挖掘出了一个新的指令,可以快速切换设备去下载模式
同时,为了使用方便,使用TK实现了一个UI。

依赖项

Libusb Win32 Filter
下载
PyUSB
Python3

截图

WeiwoTool_UI

功能

支持新老平台的FASTBOOT解锁和上锁,支持特定机种的引导到下载模式

代码

import usb.core
import usb.util
import traceback
from tkinter import *
from tkinter import messagebox

FB_VID = 0x18d1
FB_PID = 0xd00d

def initUsb():
    dev = usb.core.find(idVendor=FB_VID, idProduct=FB_PID)
    if not dev:
        messagebox.showerror("Error", 'NOT FOUND WEIWO FASTBOOT DEVICES')
        exit(-1)
    intf = dev[0][(0, 0)]
    usb.util.claim_interface(dev, intf)
    messagebox.showinfo("Info", 'Init phone: finished')
    return dev, intf, intf[0], intf[1]

def clearHalt(phone, in_end, out_end):
    phone.clear_halt(in_end)
    phone.clear_halt(out_end)
    messagebox.showinfo("Info", 'Clear halt: finished')

def sendBulk(phone, in_end, out_end, cmd):
    assert phone.write(out_end, cmd) == len(cmd)
    ret = phone.read(in_end, 100)
    sret = ''.join([chr(x) for x in ret])
    messagebox.showinfo("Response", f'RESP>> {sret}')
    return sret

def unlockBlOld(phone, in_end, out_end):
    cmd = 'bbk unlock_vivo'
    sendBulk(phone, in_end, out_end, cmd)
    cmd = 'reboot-bootloader'
    sendBulk(phone, in_end, out_end, cmd)

def lockBlOld(phone, in_end, out_end):
    cmd = 'bbk lock_vivo'
    sendBulk(phone, in_end, out_end, cmd)

def changeDloadOld(phone, in_end, out_end):
    cmd = 'bbk change_dload_vivo'
    sendBulk(phone, in_end, out_end, cmd)

def unlockBlNew(phone, in_end, out_end):
    cmd = 'vivo_bsp unlock_vivo'
    sendBulk(phone, in_end, out_end, cmd)
    cmd = 'reboot-bootloader'
    sendBulk(phone, in_end, out_end, cmd)

def lockBlNew(phone, in_end, out_end):
    cmd = 'vivo_bsp lock_vivo'
    sendBulk(phone, in_end, out_end, cmd)

try:
    phone, interface, in_end, out_end = initUsb()

    def performAction(action):
        try:
            {'1': unlockBlOld, '2': lockBlOld, '3': changeDloadOld, '4': unlockBlNew, '5': lockBlNew}[action](phone, in_end, out_end)
        except KeyError:
            messagebox.showerror("Error", 'Invalid operation')
        except:
            traceback.print_exc()
        finally:
            usb.util.release_interface(phone, interface)

    root = Tk()
    root.title("WWTool")

    Label(root, text="WeiWo Tool").pack()

    Button(root, text="Unlock Bootloader (Old)", command=lambda: performAction('1')).pack()
    Button(root, text="Lock Bootloader (Old)", command=lambda: performAction('2')).pack()
    Button(root, text="Dload Mode (Old)", command=lambda: performAction('3')).pack()
    Button(root, text="Unlock Bootloader (New)", command=lambda: performAction('4')).pack()
    Button(root, text="Lock Bootloader (New)", command=lambda: performAction('5')).pack()

    root.mainloop()

except:
    traceback.print_exc()

起因

前段时间手痒,看到了跟了自己很久的绿联读卡器,想折腾折腾。但是又不知道它的详细硬件情况,于是就在网上搜常见的USB3读卡器方案商,逐个尝试固件更新工具,然后成功使用了创惟科技的固件升级工具读到了版本号,确定了其使用了创惟GL3224主控。便接下来了开始的作死流程:首先直接点击了刷入,1536版本固件是随官方固件更新随工具附带的,刷入后自然而然也没有任何问题,然而我开始在互联网上寻找是否有更新的固件。而后在数码之家的一篇帖子中找到了随一款新的读卡器附带的1539版本,立即来了兴趣,就花了家元下载。下载后发现原作者提供的是FLASH的直接Dump,与官方工具内的固件结构和大小都不相同,于是选择了照猫画虎的办法,依照已有的1536版本官方固件进行裁剪,裁剪后直接就进行了刷入。刷入了之后,设备仅能认出是USB大容量存储设备,没有了分区磁盘设备,插入SD卡后也没了反应,打开官方的更新工具也直接提示无设备(但在此处可以看到是1539的固件版本,应该是校验错误导致的不启动)。
GLF_Error1539
GLF_NoDeviceUpdate

“修复”

在弄坏了这个读卡器之后,因为读卡器这类东西价格并不是很高,所以立即就重新购买了新的,这个坏的则被放在了一边。
今天突然想起来还有这么一个事情,便尝试开始了修复。
首先,在创惟科技官方网站上找到了这个读卡器芯片的Block Diagram
GL3224_QFN48_BLK_DGM
发现其由于支持低成本设计,FLASH并不是强制要求,其晶圆上还有内部ROM
现在我们的目的就是让内部ROM上的版本固件跑起来,从而先恢复正常启动的功能,而后再继续尝试刷入固件到SPI闪存。

“破坏”

这个读卡器的外壳使用的是超声波焊接工艺,没有螺丝也没有可以下翘片的地方,按照网络上的教程,拆解超声波焊接的设备只需使用毛巾裹住,然后向地上砸去即可。我也照做了,的确拆出了主板,但是忘记了这个读卡器买了很久了,塑料有些脆化,当我打开毛巾之时,外壳已经碎裂成了很多半。
GLF_BrokenCase
不过最终还是成功得到了主板,主板也很简单,一片VSOP封装的SPI闪存,一片GL3224主控,一粒晶振以及两个卡连接器就构成了整体设计
GLF_RawBoard
在SPI闪存中,依据个人经验,短接CS(片选信号)脚和DO(输出信号)脚即可暂时让主控读不到SPI
GLF_FlashPin
在当前情况下,我们这样短接,再插入电脑,即可使用ROM版本的固件来启动了
GLF_SpiShort
果然成功启动!
GLF_RomBootDevice
再通过一样的方法检查固件版本,发现其为1532版,因此可以确定其为ROM版本固件!
GLF_RomBootComplete
此时我们再使用官方的固件更新工具,可以看到其成功识别到了设备,且可以执行更新!
GLF_1532to1536
执行更新,等待提示成功,此时拔下再连接,读卡器完全恢复了正常!
GLF_1532to1536_Success

“不死心”

当然,对于没有成功更新到1539版本,我还是耿耿于怀。于是再一次尝试对FLASH Dump版本的认真裁剪,成功的将其升级到了真正的1539版本,且可以直接通过固件更新工具更新。
GLF_1536to1539

下载

创惟科技(GENESYS) GL3224的固件更新工具和两个版本的固件,有1536和1539两个版本可供选择
GL3224_Update.7z

版权

此頁面中的Block Diagram圖片及韌體和韌體更新工具均為創惟科技股份有限公司版權所有
此頁面中的SPI NAND快閃記憶體Pin Diagram圖片由華邦電子股份有限公司版權所有,來自W25Q80DV_July,2015的Datasheet

介绍

最近总有需求表示需要通过电脑给一些支持网络调试的设备安装软件
虽然每次我都建议直接使用platform-tools里面的ADB工具,但显然并不是所有人都有类似的能力来操作命令行。
所以快速的使用批处理脚本写了个小型安装器,只需要简单的输入和拖拽即可安装APK。
当前版本基于Platform-Tools r34

截图

EAI_Start
EAI_Main

版权

Platform-Tools来自Google,Inc

下载

下载解压后打开eai.bat即可使用
下载

最近有迁移个人存储内容到DSM的需求,本来服务器上使用的是CloudReve。
其文件夹结构和文件名都与上传时候的版本有诸多差异。
写了这个小工具来帮助解决,敬请使用。

#!/bin/bash

CLEAN_EMPTY=0
CLEAN_THUMB=0
CLEAN_FILENAME=0

while [ "$#" -gt 0 ]; do
    case $1 in
        --clsempty) CLEAN_EMPTY=1 ;;
        --clsthumb) CLEAN_THUMB=1 ;;
        --clsfilename) CLEAN_FILENAME=1 ;;
        *) break ;;
    esac
    shift
done

if [ "$#" -ne 1 ]; then
    echo "Cloudreve Cleaner 1.0"
    echo "Usage: ./clscrve.sh [--clsempty] [--clsthumb] [--clsfilename] <directory>"
    exit 1
fi

DIR=$1

if [ ! -d "$DIR" ]; then
    echo "Directory not found: $DIR"
    exit 1
fi

if [ "$CLEAN_FILENAME" -eq 1 ]; then
    echo "Cleaning filenames..."
    find "$DIR" -type f | while read -r file; do
        dir=$(dirname "$file")
        base=$(basename "$file")
        originname=$(echo "$base" | awk -F'_' '{ $1=$2=""; sub(/^  /,""); print }')
        if [ "$base" == "$originname" ]; then
            continue
        fi
        mv -n "$file" "$dir/$originname"
        echo "${base} -> ${originname}"
    done
fi

if [ "$CLEAN_EMPTY" -eq 1 ]; then
    echo "Cleaning empty directories..."
    find "$DIR" -type d -empty -delete
fi

if [ "$CLEAN_THUMB" -eq 1 ]; then
    echo "Cleaning thumbnail files..."
    find "$DIR" -type f -name '*. thumb' -delete
fi

echo "SUCCESS IN $(date '+%Y-%m-%d %H:%M:%S')"