Files
xiaozhi-esp32/scripts/versions.py

250 lines
8.4 KiB
Python
Raw Permalink Normal View History

2024-10-17 18:34:51 +08:00
#! /usr/bin/env python3
from dotenv import load_dotenv
load_dotenv()
import os
import struct
import zipfile
import oss2
import json
2025-02-01 23:09:40 +08:00
import requests
from requests.exceptions import RequestException
2024-10-17 18:34:51 +08:00
2025-01-23 22:45:26 +08:00
# 切换到项目根目录
os.chdir(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
2024-10-17 18:34:51 +08:00
def get_chip_id_string(chip_id):
return {
0x0000: "esp32",
0x0002: "esp32s2",
0x0005: "esp32c3",
0x0009: "esp32s3",
0x000C: "esp32c2",
0x000D: "esp32c6",
0x0010: "esp32h2",
0x0011: "esp32c5",
0x0012: "esp32p4",
0x0017: "esp32c5",
}[chip_id]
def get_flash_size(flash_size):
MB = 1024 * 1024
return {
0x00: 1 * MB,
0x01: 2 * MB,
0x02: 4 * MB,
0x03: 8 * MB,
0x04: 16 * MB,
0x05: 32 * MB,
0x06: 64 * MB,
0x07: 128 * MB,
}[flash_size]
def get_app_desc(data):
magic = struct.unpack("<I", data[0x00:0x04])[0]
if magic != 0xabcd5432:
raise Exception("Invalid app desc magic")
version = data[0x10:0x30].decode("utf-8").strip('\0')
project_name = data[0x30:0x50].decode("utf-8").strip('\0')
time = data[0x50:0x60].decode("utf-8").strip('\0')
date = data[0x60:0x70].decode("utf-8").strip('\0')
idf_ver = data[0x70:0x90].decode("utf-8").strip('\0')
elf_sha256 = data[0x90:0xb0].hex()
return {
"name": project_name,
"version": version,
"compile_time": date + "T" + time,
"idf_version": idf_ver,
"elf_sha256": elf_sha256,
}
def get_board_name(folder):
basename = os.path.basename(folder)
if basename.startswith("v0.2"):
2024-10-30 06:58:29 +08:00
return "bread-simple"
if basename.startswith("v0.3") or basename.startswith("v0.4") or basename.startswith("v0.5") or basename.startswith("v0.6"):
2024-10-17 18:34:51 +08:00
if "ML307" in basename:
2024-10-30 06:58:29 +08:00
return "bread-compact-ml307"
2024-11-03 05:54:15 +08:00
elif "WiFi" in basename:
2024-10-30 06:58:29 +08:00
return "bread-compact-wifi"
2024-11-03 05:54:15 +08:00
elif "KevinBox1" in basename:
return "kevin-box-1"
2025-04-28 17:05:52 +08:00
if basename.startswith("v0.7") or basename.startswith("v0.8") or basename.startswith("v0.9") or basename.startswith("v1.") or basename.startswith("v2."):
2024-11-06 22:48:21 +08:00
return basename.split("_")[1]
2024-10-25 12:46:33 +08:00
raise Exception(f"Unknown board name: {basename}")
2024-10-17 18:34:51 +08:00
2025-08-12 14:53:17 +08:00
def find_app_partition(data):
partition_begin = 0x8000
partition_end = partition_begin + 0x4000
# find the first parition with type 0x00
for i in range(partition_begin, partition_end, 0x20):
# magic is aa 50
if data[i] == 0xaa and data[i + 1] == 0x50:
# type is app
if data[i + 2] == 0x00:
# read offset and size
offset = struct.unpack("<I", data[i + 4:i + 8])[0]
size = struct.unpack("<I", data[i + 8:i + 12])[0]
# then 16 bytes is label
label = data[i + 12:i + 28].decode("utf-8").strip('\0')
print(f"found app partition at 0x{i:08x}, offset: 0x{offset:08x}, size: 0x{size:08x}, label: {label}")
return {
"offset": offset,
"size": size,
"label": label,
}
return None
2024-10-17 18:34:51 +08:00
def read_binary(dir_path):
merged_bin_path = os.path.join(dir_path, "merged-binary.bin")
merged_bin_data = open(merged_bin_path, "rb").read()
# find app partition
2025-08-12 14:53:17 +08:00
app_partition = find_app_partition(merged_bin_data)
if app_partition is None:
print("no app partition found")
return
app_data = merged_bin_data[app_partition["offset"]:app_partition["offset"] + app_partition["size"]]
# check magic
if app_data[0] != 0xE9:
print("not a valid image")
2024-10-17 18:34:51 +08:00
return
# get flash size
2025-08-12 14:53:17 +08:00
flash_size = get_flash_size(app_data[0x3] >> 4)
chip_id = get_chip_id_string(app_data[0xC])
2024-10-17 18:34:51 +08:00
# get segments
2025-08-12 14:53:17 +08:00
segment_count = app_data[0x1]
2024-10-17 18:34:51 +08:00
segments = []
offset = 0x18
2025-08-12 14:53:17 +08:00
image_size = 0x18
2024-10-17 18:34:51 +08:00
for i in range(segment_count):
2025-08-12 14:53:17 +08:00
segment_size = struct.unpack("<I", app_data[offset + 4:offset + 8])[0]
image_size += 8 + segment_size
2024-10-17 18:34:51 +08:00
offset += 8
2025-08-12 14:53:17 +08:00
segment_data = app_data[offset:offset + segment_size]
2024-10-17 18:34:51 +08:00
offset += segment_size
segments.append(segment_data)
2025-08-12 14:53:17 +08:00
assert offset < len(app_data), "offset is out of bounds"
# add checksum size
image_size += 1
image_size = (image_size + 15) & ~15
# hash appended
if app_data[0x17] == 1:
image_size += 32
print(f"image size: {image_size}")
# verify the remaining data are all 0xFF
for i in range(image_size, len(app_data)):
if app_data[i] != 0xFF:
print(f"Failed to verify image, data at 0x{i:08x} is not 0xFF")
return
image_data = app_data[:image_size]
2024-10-17 18:34:51 +08:00
# extract bin file
bin_path = os.path.join(dir_path, "xiaozhi.bin")
if not os.path.exists(bin_path):
print("extract bin file to", bin_path)
2025-08-12 14:53:17 +08:00
open(bin_path, "wb").write(image_data)
2024-10-17 18:34:51 +08:00
# The app desc is in the first segment
desc = get_app_desc(segments[0])
return {
"chip_id": chip_id,
"flash_size": flash_size,
"board": get_board_name(dir_path),
"application": desc,
2025-08-12 14:53:17 +08:00
"firmware_size": image_size,
2024-10-17 18:34:51 +08:00
}
def extract_zip(zip_path, extract_path):
if not os.path.exists(extract_path):
os.makedirs(extract_path)
print(f"Extracting {zip_path} to {extract_path}")
with zipfile.ZipFile(zip_path, 'r') as zip_ref:
zip_ref.extractall(extract_path)
def upload_dir_to_oss(source_dir, target_dir):
auth = oss2.Auth(os.environ['OSS_ACCESS_KEY_ID'], os.environ['OSS_ACCESS_KEY_SECRET'])
bucket = oss2.Bucket(auth, os.environ['OSS_ENDPOINT'], os.environ['OSS_BUCKET_NAME'])
for filename in os.listdir(source_dir):
oss_key = os.path.join(target_dir, filename)
# check if is file
if not os.path.isfile(os.path.join(source_dir, filename)):
continue
2024-10-17 18:34:51 +08:00
print('uploading', oss_key)
bucket.put_object(oss_key, open(os.path.join(source_dir, filename), 'rb'))
2025-02-01 23:09:40 +08:00
def post_info_to_server(info):
"""
将固件信息发送到服务器
Args:
info: 包含固件信息的字典
"""
try:
# 从环境变量获取服务器URL和token
server_url = os.environ.get('VERSIONS_SERVER_URL')
server_token = os.environ.get('VERSIONS_TOKEN')
if not server_url or not server_token:
raise Exception("Missing SERVER_URL or TOKEN in environment variables")
# 准备请求头和数据
headers = {
'Authorization': f'Bearer {server_token}',
'Content-Type': 'application/json'
}
# 发送POST请求
response = requests.post(
server_url,
headers=headers,
json={'jsonData': json.dumps(info)}
)
# 检查响应状态
response.raise_for_status()
print(f"Successfully uploaded version info for tag: {info['tag']}")
except RequestException as e:
if hasattr(e.response, 'json'):
error_msg = e.response.json().get('error', str(e))
else:
error_msg = str(e)
print(f"Failed to upload version info: {error_msg}")
raise
except Exception as e:
print(f"Error uploading version info: {str(e)}")
raise
2024-10-17 18:34:51 +08:00
def main():
release_dir = "releases"
# look for zip files startswith "v"
for name in os.listdir(release_dir):
if name.startswith("v") and name.endswith(".zip"):
tag = name[:-4]
folder = os.path.join(release_dir, tag)
2025-02-01 23:09:40 +08:00
info_path = os.path.join(folder, "info.json")
if not os.path.exists(info_path):
if not os.path.exists(folder):
os.makedirs(folder)
extract_zip(os.path.join(release_dir, name), folder)
2024-10-17 18:34:51 +08:00
info = read_binary(folder)
target_dir = os.path.join("firmwares", tag)
info["tag"] = tag
info["url"] = os.path.join(os.environ['OSS_BUCKET_URL'], target_dir, "xiaozhi.bin")
2025-02-01 23:09:40 +08:00
open(info_path, "w").write(json.dumps(info, indent=4))
2024-10-17 18:34:51 +08:00
# upload all file to oss
upload_dir_to_oss(folder, target_dir)
2025-02-01 23:09:40 +08:00
# read info.json
info = json.load(open(info_path))
# post info.json to server
post_info_to_server(info)
2024-10-17 18:34:51 +08:00
if __name__ == "__main__":
main()