分类 小项目 下的文章

就是一些不知道怎么分类的小项目

概要

在Android的FOTA更新包中,因为现在大家都去用payload.bin了,所以旧的Block更新的Sparse Image转Raw Image工具就没人更新了
之前留意到Brotli压缩版本的还有人写工具,但是最近发现在一些低性能电视盒子上使用的是lzma压缩方式就没有人在意了。
所以把这整个工具重构了一遍,使其更加好用且性能更好。

库安装

pip3 install Brotli

代码

#!/usr/bin/env python3
import sys
import os
import lzma
import brotli
from pathlib import Path
from typing import List, Tuple, BinaryIO

BLOCK_SIZE = 4096

def isTransferList(file_path: str, sample_size: int = 1024) -> bool:
    try:
        with open(file_path, 'rb') as f:
            return b'\0' not in f.read(sample_size)
    except IOError:
        return False

def decompressLzmaDat(input_file: str, output_file: str):
    with lzma.open(input_file, 'rb') as lzma_file:
        with open(output_file, 'wb') as decompressed_file:
            decompressed_file.write(lzma_file.read())

def decompressBrotliDat(input_file: str, output_file: str):
    with open(input_file, 'rb') as br_file:
        with open(output_file, 'wb') as decompressed_file:
            decompressed_file.write(brotli.decompress(br_file.read()))

def rangeSet(src: str) -> List[Tuple[int, int]]:
    src_set = [int(item) for item in src.split(',')]
    if len(src_set) != src_set[0] + 1:
        raise ValueError(f'Error parsing data to rangeSet: {src}')
    return [(src_set[i], src_set[i+1]) for i in range(1, len(src_set), 2)]

def parseTransferList(path: str) -> Tuple[int, int, List]:
    if not isTransferList(path):
        raise ValueError(f"The file '{path}' does not appear to be a valid transfer list file.")

    with open(path, 'r') as trans_list:
        version = int(trans_list.readline())
        new_blocks = int(trans_list.readline())

        if version >= 2:
            trans_list.readline()
            trans_list.readline()

        commands = []
        for line in trans_list:
            cmd, *params = line.split()
            if cmd in ['erase', 'new', 'zero']:
                commands.append([cmd, rangeSet(params[0])])
            elif not cmd[0].isdigit():
                raise ValueError(f'Command "{cmd}" is not valid.')

    return version, new_blocks, commands

def processFile(new_data_file: BinaryIO, output_img: BinaryIO, commands: List, max_file_size: int):
    for command in commands:
        if command[0] == 'new':
            for block in command[1]:
                begin, end = block
                block_count = end - begin
                print(f'Copying {block_count} blocks into position {begin}...')

                output_img.seek(begin * BLOCK_SIZE)
                output_img.write(new_data_file.read(block_count * BLOCK_SIZE))
        else:
            print(f'Skipping command {command[0]}...')

    if output_img.tell() < max_file_size:
        output_img.truncate(max_file_size)

def main(transfer_list_file: str, new_data_file: str, output_image_file: str):

    version, new_blocks, commands = parseTransferList(transfer_list_file)

    android_versions = {
        1: 'Android 5.0',
        2: 'Android 5.1',
        3: 'Android 6.0',
        4: 'Android 7.0 or Higher'
    }
    print(f'{android_versions.get(version, "Unknown")} Version Image Detected')

    output_img_path = Path(output_image_file)
    if output_img_path.exists():
        raise FileExistsError(f'Output file "{output_img_path}" already exists')

    decompressed_file = None
    if 'lzma' in new_data_file.lower():
        print("LZMA file detected. Decompressing...")
        decompressed_file = new_data_file + '.decompressed'
        decompressLzmaDat(new_data_file, decompressed_file)
        new_data_file = decompressed_file
        print("Decompression Completed!")
    elif new_data_file.lower().endswith('.br'):
        print("Brotli file detected. Decompressing...")
        decompressed_file = new_data_file + '.decompressed'
        decompressBrotliDat(new_data_file, decompressed_file)
        new_data_file = decompressed_file
        print("Decompression Completed!")

    with open(new_data_file, 'rb') as new_data, output_img_path.open('wb') as output_img:
        all_block_sets = [i for command in commands for i in command[1]]
        max_file_size = max(pair[1] for pair in all_block_sets) * BLOCK_SIZE

        processFile(new_data, output_img, commands, max_file_size)

    print(f'Done! Output image: {output_img_path.resolve()}')

    if decompressed_file:
        os.remove(decompressed_file)
        print("Temporary decompressed file removed.")

if __name__ == '__main__':
    if len(sys.argv) < 3:
        print('Usage: sdat2img_v2.py <transfer.list> <dat|dat.lzma|dat.br> [raw.img]')
        print('<transfer.list>:Transfer List File')
        print('<dat|dat.lzma|dat.br>:New Dat File (Support Uncompressed, LZMA or Brotli)')
        print('[raw.img]:Output File Name of Raw Image\n')
        sys.exit(1)

    transfer_list_file = sys.argv[1]
    new_data_file = sys.argv[2]
    
    if len(sys.argv) > 3:
        output_image_file = sys.argv[3]
    else:
        base_name = os.path.basename(sys.argv[1]).split('.')[0]
        output_image_file = f"{base_name}.raw.img"

    try:
        main(transfer_list_file, new_data_file, output_image_file)
    except Exception as e:
        print(f"An error occurred: {e}", file=sys.stderr)
        sys.exit(1)

可执行文件

For Windows x64

Yuu Web Synth Engine </>


介绍

YuWSE(Yu Web Synth Engine)是一个简单的封装了AudioContext声音合成器引擎的前端库,发源于MzkBtn小软件和ToneCAPTCHA引擎中的声音合成部分,旨在方便开发者以最简单的方式用非采样和仅代码方式产生有效的声音。
且同时包括了部分前端功能实现。其中包括了波形显示器,五线谱绘制,简五谱互转,触摸钢琴键盘扫描,节拍器显示,简易和复杂音序器(YUUWSEJSONV1/V2),预置音色库等功能。

范例和Demo

MzkBtn 音乐按钮

最早的玩具项目,也是最早版本的合成引擎,最早实现了波形显示
体验版

ToneCAPTCHA 音高验证码

基于音高的全自动区分计算机和人类的图灵测试
第二版本的合成引擎,加入了基于Web Canvas图形的即时五线谱绘制功能和简谱五线谱输入转换功能
文章
体验版

LePad 乐板

第三版本的合成引擎和前端实现,新增触摸钢琴键盘和音序器功能
体验版

YuWSE Metronome 网页节拍器

简易的网页节拍器,基于LePad的Lite分支
体验版

BSG 猪音发生器

Babi Sound Generator
全新的独立分支,用于测试捏音色这个新功能,当前可以产生猪叫声
B.S.G

一个大部分基于原生JavaScript和HTML5所构建的在线简易办公工具包

使用:App/WOT

💼首页
V01R:HTML基础页面
V02R:修改CSS
V03R:添加按钮浮出动画
V04R:新增Beta栏目

🔢计算器
V01R: 基本计算功能
V02R: 新增10步计算流程回溯功能 新增黑暗模式支持
V03R: 新增触摸屏设备的上下滑动回溯计算流程的操作手势
V04R: 新增依据算式长度自动调整输出区域字体大小功能
V05R: 调整CSS布局
V06R: 新增浮点功能
V07R: 新增单数字删除功能

🈯中文转换器
V01R: 基本简繁转换功能
V02R: 新增基于台湾术语表的简繁转换功能

📑文档
V01R: 基本文本输入功能
V02R: 新增黑暗模式支持
V03R: 新增LocalStorage存储功能

🛠️格式化者
V01R: 基本JSON格式化压缩功能
V02R: 新增对XML的支持
V03R: 新增行号功能

🔑帮助
V01R: 首次发布
V02R: 修改帮助文稿以负责新功能
V03R: 调整CSS布局

⌨️前端游乐场
V01R: 首次发布
V02R: 新增黑暗模式支持

📊演示
V01R: 基本演示功能
V02R: 调整为整页型布局 新增一键新增页面功能 新增全屏演示功能
V03R: 新增LocalStorage存储功能

🤳🏽二维码发生器
V01R: 首次发布
V02R: 调整CSS布局

📈数据表格
V01R: 首次发布
V02R: 新增SUM自动求和函数 新增黑暗模式支持
V03R: 新增AVERAGE/COUNT/COUNTA/COUNTIF/SUMIF/LEFT/RIGHT/MID/LEN/TRIM函数集
V04R: 函数算法优化
V05R: 二次函数算法优化 新增LocalStorage存储功能
V06B: 新增单元格拖拽功能

🔐睡眠锁定器
V02R:新增开关按钮动画
V01R:首次发布

🖼️白板
V01R: 首次发布
V02R: 新增黑暗模式支持 新增触摸屏设备支持
V03R: 完善黑暗模式支持 新增LocalStorage存储功能

感谢@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()