Skip to content

Commit 69cdf3c

Browse files
committed
Initial commit: ESP32-C3 firmware flasher for Xteink X4 devices
Cross-platform CLI tool implementing ESP32 ROM bootloader protocol over serial with SLIP framing and zlib compression
1 parent de0d3cd commit 69cdf3c

File tree

18 files changed

+1848
-451
lines changed

18 files changed

+1848
-451
lines changed

.github/workflows/build.yml

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
name: Build
2+
3+
on:
4+
push:
5+
branches: [main, master]
6+
pull_request:
7+
branches: [main, master]
8+
9+
jobs:
10+
build:
11+
strategy:
12+
matrix:
13+
include:
14+
- os: ubuntu-latest
15+
goos: linux
16+
goarch: amd64
17+
artifact: papyrix-flasher-linux-amd64
18+
- os: ubuntu-latest
19+
goos: linux
20+
goarch: arm64
21+
artifact: papyrix-flasher-linux-arm64
22+
- os: macos-latest
23+
goos: darwin
24+
goarch: amd64
25+
artifact: papyrix-flasher-darwin-amd64
26+
- os: macos-latest
27+
goos: darwin
28+
goarch: arm64
29+
artifact: papyrix-flasher-darwin-arm64
30+
- os: windows-latest
31+
goos: windows
32+
goarch: amd64
33+
artifact: papyrix-flasher-windows-amd64.exe
34+
35+
runs-on: ${{ matrix.os }}
36+
37+
steps:
38+
- uses: actions/checkout@v4
39+
40+
- name: Set up Go
41+
uses: actions/setup-go@v5
42+
with:
43+
go-version: '1.22'
44+
45+
- name: Build
46+
env:
47+
GOOS: ${{ matrix.goos }}
48+
GOARCH: ${{ matrix.goarch }}
49+
ARTIFACT: ${{ matrix.artifact }}
50+
COMMIT_SHA: ${{ github.sha }}
51+
shell: bash
52+
run: |
53+
go build -ldflags "-X main.version=${COMMIT_SHA} -X main.commit=${COMMIT_SHA} -X main.date=$(date -u +%Y-%m-%dT%H:%M:%SZ)" -o "${ARTIFACT}" ./cmd/papyrix-flasher
54+
55+
- name: Upload artifact
56+
uses: actions/upload-artifact@v4
57+
with:
58+
name: ${{ matrix.artifact }}
59+
path: ${{ matrix.artifact }}
60+
61+
test:
62+
runs-on: ubuntu-latest
63+
steps:
64+
- uses: actions/checkout@v4
65+
66+
- name: Set up Go
67+
uses: actions/setup-go@v5
68+
with:
69+
go-version: '1.22'
70+
71+
- name: Run tests
72+
run: go test -v ./...
73+
74+
- name: Run go vet
75+
run: go vet ./...

.github/workflows/release.yml

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
name: Release
2+
3+
on:
4+
push:
5+
tags:
6+
- 'v*'
7+
8+
permissions:
9+
contents: write
10+
11+
jobs:
12+
release:
13+
runs-on: ubuntu-latest
14+
15+
steps:
16+
- uses: actions/checkout@v4
17+
with:
18+
fetch-depth: 0
19+
20+
- name: Set up Go
21+
uses: actions/setup-go@v5
22+
with:
23+
go-version: '1.22'
24+
25+
- name: Build all platforms
26+
env:
27+
COMMIT_SHA: ${{ github.sha }}
28+
run: |
29+
VERSION="${GITHUB_REF#refs/tags/}"
30+
COMMIT="${COMMIT_SHA}"
31+
DATE=$(date -u +%Y-%m-%dT%H:%M:%SZ)
32+
LDFLAGS="-X main.version=${VERSION} -X main.commit=${COMMIT} -X main.date=${DATE}"
33+
34+
mkdir -p dist
35+
36+
# Linux amd64
37+
GOOS=linux GOARCH=amd64 go build -ldflags "${LDFLAGS}" -o dist/papyrix-flasher-linux-amd64 ./cmd/papyrix-flasher
38+
tar -czf "dist/papyrix-flasher-${VERSION}-linux-amd64.tar.gz" -C dist papyrix-flasher-linux-amd64
39+
40+
# Linux arm64
41+
GOOS=linux GOARCH=arm64 go build -ldflags "${LDFLAGS}" -o dist/papyrix-flasher-linux-arm64 ./cmd/papyrix-flasher
42+
tar -czf "dist/papyrix-flasher-${VERSION}-linux-arm64.tar.gz" -C dist papyrix-flasher-linux-arm64
43+
44+
# macOS amd64
45+
GOOS=darwin GOARCH=amd64 go build -ldflags "${LDFLAGS}" -o dist/papyrix-flasher-darwin-amd64 ./cmd/papyrix-flasher
46+
tar -czf "dist/papyrix-flasher-${VERSION}-darwin-amd64.tar.gz" -C dist papyrix-flasher-darwin-amd64
47+
48+
# macOS arm64
49+
GOOS=darwin GOARCH=arm64 go build -ldflags "${LDFLAGS}" -o dist/papyrix-flasher-darwin-arm64 ./cmd/papyrix-flasher
50+
tar -czf "dist/papyrix-flasher-${VERSION}-darwin-arm64.tar.gz" -C dist papyrix-flasher-darwin-arm64
51+
52+
# Windows amd64
53+
GOOS=windows GOARCH=amd64 go build -ldflags "${LDFLAGS}" -o dist/papyrix-flasher-windows-amd64.exe ./cmd/papyrix-flasher
54+
cd dist && zip "papyrix-flasher-${VERSION}-windows-amd64.zip" papyrix-flasher-windows-amd64.exe && cd ..
55+
56+
# Create version file for release action
57+
echo "${VERSION}" > dist/version.txt
58+
59+
- name: Get version
60+
id: version
61+
run: echo "VERSION=$(cat dist/version.txt)" >> $GITHUB_OUTPUT
62+
63+
- name: Create Release
64+
uses: softprops/action-gh-release@v1
65+
with:
66+
draft: false
67+
prerelease: false
68+
generate_release_notes: true
69+
files: |
70+
dist/papyrix-flasher-${{ steps.version.outputs.VERSION }}-linux-amd64.tar.gz
71+
dist/papyrix-flasher-${{ steps.version.outputs.VERSION }}-linux-arm64.tar.gz
72+
dist/papyrix-flasher-${{ steps.version.outputs.VERSION }}-darwin-amd64.tar.gz
73+
dist/papyrix-flasher-${{ steps.version.outputs.VERSION }}-darwin-arm64.tar.gz
74+
dist/papyrix-flasher-${{ steps.version.outputs.VERSION }}-windows-amd64.zip

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,3 +15,7 @@ vendor/
1515
# OS
1616
.DS_Store
1717
Thumbs.db
18+
.venv
19+
.idea
20+
CLAUDE.md
21+
.claude/

Makefile

Lines changed: 40 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -3,53 +3,55 @@ COMMIT ?= $(shell git rev-parse --short HEAD 2>/dev/null || echo "none")
33
DATE ?= $(shell date -u +"%Y-%m-%dT%H:%M:%SZ")
44
LDFLAGS := -ldflags "-X main.version=$(VERSION) -X main.commit=$(COMMIT) -X main.date=$(DATE)"
55

6-
.PHONY: build build-all clean test fmt lint release
6+
.PHONY: build build-all clean test fmt lint release help
77

8-
# Build for current platform
9-
build:
8+
.DEFAULT_GOAL := help
9+
10+
## Build:
11+
12+
build: ## Build for current platform
1013
go build $(LDFLAGS) -o bin/papyrix-flasher ./cmd/papyrix-flasher
1114

12-
# Build for all platforms
13-
build-all: build-linux build-darwin build-windows
15+
build-all: build-linux build-darwin build-windows ## Build for all platforms
1416

15-
build-linux:
17+
build-linux: ## Build for Linux (amd64, arm64)
1618
GOOS=linux GOARCH=amd64 go build $(LDFLAGS) -o bin/papyrix-flasher-linux-amd64 ./cmd/papyrix-flasher
1719
GOOS=linux GOARCH=arm64 go build $(LDFLAGS) -o bin/papyrix-flasher-linux-arm64 ./cmd/papyrix-flasher
1820

19-
build-darwin:
21+
build-darwin: ## Build for macOS (amd64, arm64)
2022
GOOS=darwin GOARCH=amd64 go build $(LDFLAGS) -o bin/papyrix-flasher-darwin-amd64 ./cmd/papyrix-flasher
2123
GOOS=darwin GOARCH=arm64 go build $(LDFLAGS) -o bin/papyrix-flasher-darwin-arm64 ./cmd/papyrix-flasher
2224

23-
build-windows:
25+
build-windows: ## Build for Windows (amd64)
2426
GOOS=windows GOARCH=amd64 go build $(LDFLAGS) -o bin/papyrix-flasher-windows-amd64.exe ./cmd/papyrix-flasher
2527

26-
# Clean build artifacts
27-
clean:
28-
rm -rf bin/
28+
## Development:
2929

30-
# Run tests
31-
test:
30+
test: ## Run tests
3231
go test -v ./...
3332

34-
# Format code
35-
fmt:
33+
fmt: ## Format code
3634
go fmt ./...
3735

38-
# Run linter
39-
lint:
36+
lint: ## Run linter (requires golangci-lint)
4037
golangci-lint run
4138

42-
# Create release archives
43-
release: build-all
39+
## Release:
40+
41+
release: build-all ## Create release archives
4442
mkdir -p release
4543
cd bin && tar czf ../release/papyrix-flasher-$(VERSION)-linux-amd64.tar.gz papyrix-flasher-linux-amd64
4644
cd bin && tar czf ../release/papyrix-flasher-$(VERSION)-linux-arm64.tar.gz papyrix-flasher-linux-arm64
4745
cd bin && tar czf ../release/papyrix-flasher-$(VERSION)-darwin-amd64.tar.gz papyrix-flasher-darwin-amd64
4846
cd bin && tar czf ../release/papyrix-flasher-$(VERSION)-darwin-arm64.tar.gz papyrix-flasher-darwin-arm64
4947
cd bin && zip ../release/papyrix-flasher-$(VERSION)-windows-amd64.zip papyrix-flasher-windows-amd64.exe
5048

51-
# Update embedded binaries from papyrix-reader
52-
update-embedded:
49+
## Maintenance:
50+
51+
clean: ## Clean build artifacts
52+
rm -rf bin/ release/
53+
54+
update-embedded: ## Update embedded binaries from papyrix-reader
5355
@if [ -d "../papyrix-reader/.pio/build/default" ]; then \
5456
cp ../papyrix-reader/.pio/build/default/bootloader.bin embedded/; \
5557
cp ../papyrix-reader/.pio/build/default/partitions.bin embedded/; \
@@ -59,6 +61,20 @@ update-embedded:
5961
exit 1; \
6062
fi
6163

62-
# Install locally
63-
install: build
64-
cp bin/papyrix-flasher $(GOPATH)/bin/ || cp bin/papyrix-flasher /usr/local/bin/
64+
install: build ## Install locally to GOPATH or /usr/local/bin
65+
cp bin/papyrix-flasher $(GOPATH)/bin/ 2>/dev/null || cp bin/papyrix-flasher /usr/local/bin/
66+
67+
## Help:
68+
69+
help: ## Show this help
70+
@echo "Papyrix Flasher - Build System"
71+
@echo ""
72+
@echo "Usage: make [target]"
73+
@echo ""
74+
@awk 'BEGIN {FS = ":.*##"; section=""} \
75+
/^##/ { section=substr($$0, 4); next } \
76+
/^[a-zA-Z_-]+:.*##/ { \
77+
if (section != "") { printf "\n\033[1m%s\033[0m\n", section; section="" } \
78+
printf " \033[36m%-15s\033[0m %s\n", $$1, $$2 \
79+
}' $(MAKEFILE_LIST)
80+
@echo ""

README.md

Lines changed: 62 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -70,19 +70,50 @@ papyrix-flasher version
7070

7171
The Xteink X4 has 16MB of flash memory, organized as:
7272

73-
| Region | Address | Size | Description |
74-
|--------|---------|------|-------------|
75-
| Bootloader | 0x0000 | ~12KB | ESP32-C3 second-stage bootloader |
76-
| Partitions | 0x8000 | 3KB | Partition table |
77-
| App (OTA 0) | 0x10000 | ~6.3MB | Main application |
78-
| App (OTA 1) | 0x650000 | ~6.3MB | OTA update partition |
79-
| SPIFFS | 0xC90000 | ~3.3MB | File storage |
73+
- **Bootloader** at `0x0000` (~12KB) - ESP32-C3 second-stage bootloader
74+
- **Partitions** at `0x8000` (3KB) - Partition table
75+
- **App (OTA 0)** at `0x10000` (~6.3MB) - Main application
76+
- **App (OTA 1)** at `0x650000` (~6.3MB) - OTA update partition
77+
- **SPIFFS** at `0xC90000` (~3.3MB) - File storage
8078

8179
By default, `papyrix-flasher` writes to:
8280
- Bootloader at 0x0000 (embedded in tool)
8381
- Partition table at 0x8000 (embedded in tool)
8482
- Firmware at 0x10000 (your file)
8583

84+
## How It Works
85+
86+
This tool implements the ESP32 ROM bootloader protocol to flash firmware over a USB serial connection.
87+
88+
### Communication Stack
89+
90+
- **Serial**: 921600 baud, 8N1, no flow control
91+
- **Framing**: SLIP (Serial Line Internet Protocol) with `0xC0` delimiters and escape sequences for special bytes
92+
- **Protocol**: ESP32 ROM bootloader binary protocol with request/response packets and XOR checksum
93+
94+
### Bootloader Entry
95+
96+
The ESP32-C3 enters bootloader mode via DTR/RTS signal sequence that controls the EN (reset) and GPIO0 (boot mode) pins through transistor drivers:
97+
98+
1. Assert EN low (reset the chip)
99+
2. Assert GPIO0 low while releasing EN (boot into download mode)
100+
3. Release GPIO0 (chip stays in bootloader)
101+
102+
### Flash Sequence
103+
104+
1. **Reset to bootloader** - DTR/RTS signal sequence
105+
2. **SYNC** - Establish communication with bootloader (up to 10 retries)
106+
3. **SPI_ATTACH** - Attach the SPI flash chip
107+
4. **SPI_SET_PARAMS** - Configure flash size (16MB)
108+
5. **FLASH_DEFL_BEGIN** - Start compressed flash session, erase sectors
109+
6. **FLASH_DEFL_DATA** - Send zlib-compressed firmware in 1KB blocks (with retry on failure)
110+
7. **FLASH_DEFL_END** - Finalize flash session
111+
8. **Hard reset** - Reboot into the new firmware
112+
113+
### Compression
114+
115+
Firmware is compressed using zlib (deflate) before transfer. The bootloader decompresses data on-the-fly, reducing transfer time significantly (typically 2-4x compression ratio).
116+
86117
## Troubleshooting
87118

88119
### Device not detected
@@ -119,7 +150,14 @@ papyrix-flasher/
119150
├── cmd/papyrix-flasher/ # CLI entry point
120151
├── internal/
121152
│ ├── slip/ # SLIP protocol encoding/decoding
153+
│ │ ├── slip.go
154+
│ │ └── slip_test.go
122155
│ ├── protocol/ # ESP32 bootloader protocol
156+
│ │ ├── commands.go
157+
│ │ ├── commands_test.go
158+
│ │ ├── packet.go
159+
│ │ ├── packet_test.go
160+
│ │ └── esp32c3.go
123161
│ ├── serial/ # Serial port abstraction
124162
│ ├── detect/ # Device auto-detection
125163
│ └── flasher/ # High-level flash operations
@@ -137,13 +175,27 @@ make build
137175
# Build for all platforms
138176
make build-all
139177

140-
# Run tests
141-
make test
142-
143178
# Update embedded binaries from papyrix-reader
144179
make update-embedded
145180
```
146181

182+
### Testing
183+
184+
```bash
185+
# Run all tests
186+
make test
187+
188+
# Run tests with verbose output
189+
go test -v ./...
190+
191+
# Run tests for specific packages
192+
go test -v ./internal/slip ./internal/protocol
193+
```
194+
195+
Unit tests cover the core protocol packages:
196+
- **slip**: SLIP framing encode/decode, escape sequences, frame extraction
197+
- **protocol**: Packet encoding/decoding, checksum calculation, command data generation
198+
147199
## References
148200

149201
- [Espressif esptool documentation](https://docs.espressif.com/projects/esptool/en/latest/)

0 commit comments

Comments
 (0)