Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
[![Spectra 6](https://img.shields.io/badge/Display-4in_Spectra_6_Full_Color-00B2A9)](https://shop.m5stack.com/products/m5paper-color-esp32s3-dev-kit)
[![PlatformIO](https://img.shields.io/badge/PlatformIO-ready-8A2BE2)](https://platformio.org/)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
[![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)](https://github.com/DrDavidDa/eInk-Desk-Terminal/pulls)
[![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)](https://github.com/DrDavidDa/PaperColor-Desk-Terminal/pulls)

</div>

Expand Down Expand Up @@ -198,8 +198,8 @@ flowchart LR
### 方式 A:M5Burner 一键烧录(零环境,推荐)

1. 下载 [M5Burner](https://m5stack.com/pages/m5burner) → 右上角登录 M5Stack 社区账号
2. 左下角 **USER CUSTOM** → 搜索 **eInk Desk Terminal**(类别 paper · 作者 DrDavid)→ Burn
3. 或点 **Share Burn** 输入分享码:`h7QZeGjpDZQ6e8vv`
2. 左下角 **USER CUSTOM** → 搜索 **PaperColor**(PaperColor eInk Desk Terminal(类别 paper · 作者 DrDavid)→ Burn
3. 或点 **Share Burn** 输入分享码:`uqzYX4i61TQriRrU`

> 首次使用需向设备 SD 卡写入 `/config.ini`(WiFi + token),见下方「配置」。

Expand Down Expand Up @@ -328,7 +328,7 @@ M5Stack · PaperColor · M5Paper · M5Burner · eInk · E-Ink · E-Paper · 彩
如果你觉得它有点意思:

- 点个 **Star** ⭐ 让更多人看到
- 提 [Issue](https://github.com/DrDavidDa/eInk-Desk-Terminal/issues) 分享你的想法
- 提 [PR](https://github.com/DrDavidDa/eInk-Desk-Terminal/pulls) 一起完善
- 提 [Issue](https://github.com/DrDavidDa/PaperColor-Desk-Terminal/issues) 分享你的想法
- 提 [PR](https://github.com/DrDavidDa/PaperColor-Desk-Terminal/pulls) 一起完善

**墨水屏不该只用来「放那吃灰」——让它替你干活。** 📟
Binary file added docs/images/cover.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/images/cover.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
87 changes: 87 additions & 0 deletions make_cover.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
# -*- coding: utf-8 -*-
# 生成 M5Burner 封面:6 页实拍轮播动图 cover.gif(1280x720)+ 静态兜底 cover.jpg
# 布局:顶部标题带(常驻)+ 左侧实拍照片 + 右侧本页功能亮点,逐页轮播
# 用法: python make_cover.py
from PIL import Image, ImageDraw, ImageFont, ImageOps
import os

HERE = os.path.dirname(os.path.abspath(__file__))
IMGDIR = os.path.join(HERE, 'docs', 'images')
W, H = 1280, 720
BAND = 180 # 顶部标题带高度
PHOTO_W = 720 # 左侧照片宽(800x600 等比缩放为 720x540)
MSYHBD = 'C:/Windows/Fonts/msyhbd.ttc'
MSYH = 'C:/Windows/Fonts/msyh.ttc'

# (实拍图, 页名, 亮点行)
PAGES = [
('calendar.jpg', '日历黄历', ['农历 · 干支 · 每日宜忌', '大字月历 + 实时天气']),
('news.jpg', '资讯早报', ['IT之家 RSS 科技精选', '每日 10 条一屏速读']),
('study.jpg', '考点闪卡', ['Anki 间隔记忆算法', '问题 + 答案 + 速记口诀']),
('coding.jpg', 'CodingPlan', ['智谱额度三卡仪表', 'Token / 工具用量直览']),
('voice.jpg', '语音待办', ['说句话就记下', '离线中文 TTS 全文朗读']),
('dashboard.jpg', '状态仪表盘', ['温湿度 · 番茄钟', '备考倒计时全景']),
]


def font(path, size):
return ImageFont.truetype(path, size)


def gradient(w, h, top, bottom):
base = Image.new('RGB', (1, h))
for y in range(h):
t = y / max(1, h - 1)
base.putpixel((0, y), tuple(int(top[i] * (1 - t) + bottom[i] * t) for i in range(3)))
return base.resize((w, h))


def frame(idx, photo):
canvas = gradient(W, H, (17, 23, 32), (10, 14, 19))
d = ImageDraw.Draw(canvas)
# 顶部标题带(每帧常驻)
d.text((48, 30), 'M5STACK PAPERCOLOR · 4英寸 SPECTRA 6 全彩墨水屏',
font=font(MSYHBD, 26), fill=(255, 133, 51))
d.text((46, 66), 'PaperColor eInk Desk Terminal',
font=font(MSYHBD, 52), fill=(255, 255, 255))
d.text((48, 134), '黄历天气 · 科技早报 · 考点闪卡 · 番茄钟 · 语音待办 · 仪表盘',
font=font(MSYH, 27), fill=(154, 164, 178))
# 左侧实拍照片(800x600 → 720x540 等比)
ph = ImageOps.fit(photo, (PHOTO_W, H - BAND), Image.LANCZOS)
canvas.paste(ph.convert('RGB'), (0, BAND))
d.rectangle([PHOTO_W, BAND, PHOTO_W + 2, H], fill=(255, 102, 0))
# 右侧功能面板
d.rectangle([PHOTO_W + 2, BAND, W, H], fill=(9, 12, 17))
px = 762
_, title, lines = PAGES[idx]
d.text((px, 214), 'PAGE %02d / %02d' % (idx + 1, len(PAGES)),
font=font(MSYHBD, 22), fill=(107, 118, 132))
d.text((px, 256), title, font=font(MSYHBD, 46), fill=(255, 255, 255))
d.rectangle([px, 330, px + 72, 336], fill=(255, 102, 0))
y = 366
for ln in lines:
d.text((px, y), ln, font=font(MSYH, 26), fill=(184, 192, 204))
y += 46
d.text((px, 662), '全离线中文语音 · 待机 ~2mA · 一键烧录',
font=font(MSYH, 20), fill=(93, 104, 116))
return canvas


def main():
frames = []
for idx, (name, _, _) in enumerate(PAGES):
with Image.open(os.path.join(IMGDIR, name)) as im:
frames.append(frame(idx, im))
# 静态封面(第 1 帧,日历页)
frames[0].convert('RGB').save(os.path.join(IMGDIR, 'cover.jpg'), quality=90)
print('[OK] cover.jpg')
# 动图封面:逐帧自适应调色板
pframes = [f.convert('P', palette=Image.ADAPTIVE, colors=256) for f in frames]
out = os.path.join(IMGDIR, 'cover.gif')
pframes[0].save(out, save_all=True, append_images=pframes[1:],
duration=1800, loop=0, optimize=False)
print('[OK] cover.gif %.1f MB' % (os.path.getsize(out) / 1048576))


if __name__ == '__main__':
main()
79 changes: 68 additions & 11 deletions publish_m5burner.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,15 @@
# 发布: POST http://m5burner-api.m5stack.com/api/admin/firmware
# multipart: name/description/category/author/version/github/cover/firmware
# header: m5_auth_token=<token>
# 更新: PUT {API}/api/admin/firmware/{fid}/{versionInfo.file}
# multipart: name/description/category/author/version/github + 可选 cover/firmware
# (cover、firmware 不传则保留原值;fid 不变 → 分享码不失效,无需重传 16MB)
# 公开: PUT {API}/api/admin/firmware/{fid}/publish/{version}/1
# 分享码: POST {API}/api/admin/firmware/share/{fid}/{version}
# 用法:
# 1. 在 m5_credentials.txt 写两行:第1行邮箱 第2行密码(已被 .gitignore 排除)
# 2. python publish_m5burner.py
# 2. python publish_m5burner.py # 全量上传新固件
# python publish_m5burner.py --meta-only # 只更新名称/介绍/封面(不动 16MB 固件)
# 镜像合成(如尚未生成):
# python merge_firmware_image.py
import requests, sys, os, json
Expand All @@ -19,20 +23,25 @@
API = 'http://m5burner-api.m5stack.com'
HERE = os.path.dirname(os.path.abspath(__file__))
IMAGE = os.path.join(HERE, 'm5burner_image.bin')
COVER = os.path.join(HERE, 'docs', 'images', 'standby.jpg')
COVER = os.path.join(HERE, 'docs', 'images', 'cover.gif') # 轮播动图封面(make_cover.py 生成)
CREDS = os.path.join(HERE, 'm5_credentials.txt')

NAME = 'eInk Desk Terminal'
NAME = 'PaperColor eInk Desk Terminal'
VERSION = 'v1.0.0'
CATEGORY = 'paper' # M5Burner 官方 PaperColor 类别
GITHUB = 'https://github.com/DrDavidDa/eInk-Desk-Terminal'
DESCRIPTION = ('Full-featured eInk desk study terminal for M5Stack PaperColor '
'(ESP32-S3 + 4" Spectra 6 full-color E-Ink). 7 pages: lunar calendar '
'+ weather, tech news RSS, Anki-style flash cards with spaced repetition, '
'Zhipu CodingPlan quota dashboard, voice memo with offline Chinese TTS, '
'environment dashboard, QR viewer. Offline Chinese TTS + global voice '
'assistant "Xiaocai" (long-press C). Ultra-low-power standby ~2mA. '
'First boot: write WiFi config to SD /config.ini (see GitHub README).')
GITHUB = 'https://github.com/DrDavidDa/PaperColor-Desk-Terminal'
FID = '0fc711c40244fb17a7df65c434deda4d' # 首次上传分配的固件 ID(meta-only 按它定位)
DESCRIPTION = (
'🔥 M5Stack PaperColor 全彩墨水屏「完全体」固件:黄历天气 · 科技早报 · Anki 考点闪卡 · '
'番茄钟 · 语音待办 · 环境仪表盘,7 页桌面终端一屏掌控!\n'
'🎙 全离线中文 TTS + 全局语音助手「小彩」:任意页长按 C,说「打开日历」「开始番茄」,'
'一句话直达,彻底解放双手。\n'
'🌙 真·2mA 超低功耗待机,续航 25~50 天;墨水屏不闪烁、不烧屏,摆桌上常亮一整年。\n'
'⚡ M5Burner 一键烧录,SD 卡写入 WiFi 即联网,开箱即玩!\n'
'English: Turn your 4" Spectra 6 full-color eInk into an all-in-one desk terminal — '
'lunar calendar & weather, tech news RSS, spaced-repetition flashcards, Pomodoro, '
'voice memos & dashboard. Fully offline Chinese TTS + voice assistant (long-press C), '
'~2mA ultra-low-power standby. Burn once, enjoy for years. 🚀')


def read_creds():
Expand Down Expand Up @@ -132,10 +141,58 @@ def share_code(token, fid, version):
return r.text


def update_meta(token):
"""只更新名称/介绍/封面(fid 不变,分享码不失效,不重传 16MB 固件)"""
own = get_own(token)
item = None
for it in (own or []):
if (it.get('fid') or it.get('_id')) == FID:
item = it
break
if not item:
print('[FAIL] 未找到 fid=%s,先全量上传一次' % FID)
sys.exit(1)
vers = item.get('versions') or []
if not vers:
print('[FAIL] 固件无版本信息')
sys.exit(1)
file_hash = vers[-1]['file']
data = {
'name': NAME,
'description': DESCRIPTION,
'category': CATEGORY,
'author': item.get('author') or 'DrDavidDa',
'version': vers[-1].get('version') or VERSION,
'github': GITHUB,
}
files = {'cover': ('cover.gif', open(COVER, 'rb').read(), 'image/gif')}
print('[..] 更新元数据 fid=%s file=%s ...' % (FID, file_hash))
r = requests.put(API + '/api/admin/firmware/%s/version/%s' % (FID, file_hash),
data=data, files=files,
headers={'m5_auth_token': token}, timeout=120)
print('[%s] 更新 HTTP %d %s' % ('OK' if r.status_code == 200 else 'FAIL', r.status_code, r.text[:300]))
if r.status_code != 200:
sys.exit(1)
# 复核:重新拉列表确认新名称/封面已生效
own = get_own(token)
for it in (own or []):
if (it.get('fid') or it.get('_id')) == FID:
print('[OK] name=%s' % it.get('name'))
print('[OK] cover=%s' % it.get('cover'))
print('[OK] github=%s' % it.get('github'))
return


def main():
email, password = read_creds()
token, username = login(email, password)

# --meta-only:只改名/介绍/封面后直接结束
if '--meta-only' in sys.argv:
update_meta(token)
print('[DONE] 元数据更新完成')
return

# 已存在则跳过重复上传(按名称在"我的固件"里查)
own = get_own(token)
fid = None
Expand Down
Loading