🐙 Upgrading from v1.x? See the Migration Guide below. The v2.0 API is not backward compatible — credentials are no longer stored inside ugit.py.
ugit syncs the entire file structure of an ESP32 (or any MicroPython board with WiFi) with a GitHub repository. Use it for OTA updates of your IoT devices in the field.
What's new in v2.1:
- Directory-level ignore —
/libin the ignore list protects all files under it (mip packages safe) - USB-CDC board safety — auto-detects ESP32-S2/S3/C3/C6/H2 and skips
machine.reset()to prevent bricking - Automated test suite — 65 pytest tests with GitHub Actions CI on every push/PR
- Branch-aware self-update —
ugit.update(branch='dev')to test development branches
Also in v2.0:
- Hash-based updates — only downloads files that actually changed (GitHub-compatible SHA1)
- Secure config — WiFi passwords and tokens stored in
/config.jsonon the device, never in your code - Safe updates —
safe_pull_all()checks storage space and creates a backup before updating - Rollback —
restore()recovers from backup if an update goes wrong - Update detection —
check_for_updates()tells you what changed without downloading anything - mip installable —
mip.install("github:turfptax/ugit") - Zero-length file support — no more OSError: 20
- Binary file support — .mpy files and other non-UTF-8 files work correctly
import mip
mip.install("github:turfptax/ugit")Copy ugit.py onto your MicroPython board using Thonny, mpremote, or ampy.
Run this once on your device to save your credentials. They are stored in /config.json on the device and never uploaded to GitHub.
import ugit
ugit.create_config(
ssid='YourWifiName',
password='YourWifiPassword',
user='your-github-username',
repository='your-repo-name',
branch='main',
token='' # optional: GitHub personal access token for private repos
)# boot.py or main.py — no secrets in this file!
import ugit
ugit.pull_all() # reset_after defaults to False; reset manually after verifyingThat's it! Your device will connect to WiFi, check GitHub for changes, and download only what's new.
USB-CDC boards (ESP32-S2, S3, C3, C6): ugit detects these boards and skips
machine.reset()automatically. On USB-CDC boards, a reset can make the device inaccessible if your code crashes before USB re-enumerates. Always test withreset_after=Falsefirst, and reset manually once you've confirmed your code works.
import ugit
ugit.wificonnect()
changes = ugit.check_for_updates(isconnected=True)
print(changes)
# {'new': ['/newfile.py'], 'changed': ['/boot.py'], 'deleted': []}
if changes['new'] or changes['changed']:
ugit.pull_all(isconnected=True)import ugit
# Checks free space, creates backup, then updates
# If update fails, run ugit.restore() to roll back
ugit.safe_pull_all()import ugit
# Only use reset_after=True on boards with a UART bridge (CP2102, CH340).
# On USB-CDC boards (ESP32-S2/S3/C3/C6), ugit will skip the reset automatically.
ugit.wificonnect()
ugit.pull_all(isconnected=True, reset_after=True)import ugit
import network
wlan = network.WLAN(network.STA_IF)
wlan.active(True)
wlan.connect('SSID', 'Password')
ugit.pull_all(isconnected=True)Files and directories can be ignored via create_config() or passed directly. Adding a directory like /data ignores all files under it. ugit always auto-protects /ugit.py, /config.json, /ugit.backup, /ugit_log.txt, /lib (mip-installed packages), and /sd (mounted SD cards).
ugit.create_config(
ssid='MyWifi',
password='MyPass',
user='turfptax',
repository='my-project',
ignore=['/my_local_data.json', '/calibration.py', '/data']
)>>> ugit.storage_info()
Storage: 2048 KB total, 80 KB used, 1968 KB free (96% free)# Create a backup
ugit.backup()
# Restore from backup (if an update went wrong)
ugit.restore()>>> ugit.show_config()
ssid: MyWifi
password: MyP***
user: turfptax
repository: my-project
branch: main
token:
ignore: []ugit.update()
# Reset device to use new version| Function | Description |
|---|---|
create_config(ssid, password, user, repository, ...) |
Save credentials to device (one-time setup) |
show_config() |
Display config with passwords masked |
pull_all(...) |
Sync device with GitHub repo (only downloads changes) |
safe_pull_all(...) |
Like pull_all but checks storage and creates backup first |
check_for_updates(...) |
Preview changes without downloading |
wificonnect(ssid, password) |
Connect to WiFi (reads from config if no args) |
pull(filepath, raw_url, token) |
Download a single file from GitHub |
backup(ignore) |
Backup all device files to /ugit.backup |
restore(ignore) |
Restore files from backup |
storage_info() |
Show device storage usage |
update(branch, token) |
Update ugit.py itself (supports dev branches) |
All functions read from /config.json when arguments are omitted.
ESP32-S2, ESP32-S3, ESP32-C3, and ESP32-C6 boards use native USB for their serial connection. Unlike boards with a UART bridge chip (CP2102, CH340), these boards lose their serial port entirely during machine.reset(). If boot.py or main.py crashes before USB re-enumerates, the device becomes inaccessible and requires reflashing.
ugit protects against this automatically:
- Detects USB-CDC boards via
os.uname().machine - Skips
machine.reset()on these boards even ifreset_after=True - Prints a warning asking you to reset manually
reset_afterdefaults toFalseon all boards
Safe pattern for any board:
# Update without auto-reset (the default)
ugit.pull_all(isconnected=True)
# Verify your code works, then reset manually:
import machine
machine.reset()If your device becomes inaccessible: Hold the BOOT button while pressing RESET to enter the ROM bootloader, then reflash MicroPython using esptool.
- Never commit
config.jsonto your GitHub repository. It contains your WiFi password and optionally a GitHub token. config.jsonis automatically protected — ugit will never delete or overwrite it during sync.- If using a private repository, create a GitHub personal access token with
reposcope and pass it astoken. show_config()masks passwords and tokens so they aren't displayed in full.
ugit includes a pytest suite that runs on a regular PC — no hardware needed. MicroPython-specific modules (urequests, machine, network) are mocked automatically.
pip install pytest
python -m pytest tests/ -vTests cover: ignore pattern matching, SHA1 hashing, config round-trips, backup/restore parsing, pull_all sync/delete logic, USB-CDC board detection, and the /lib directory protection that prevents the bricking bug.
Tests are also run automatically via GitHub Actions on every push and pull request.
v2.0 is a full rewrite. If you have devices running v1.x, here's what you need to know:
What changed:
- Credentials are no longer stored as variables inside
ugit.py— they go in/config.json pull_all()no longer uses module-level globals — pass arguments or usecreate_config()- The
ignore_filesvariable is gone — use theignoreparameter orcreate_config(ignore=[...]) - File hashing now matches GitHub's blob format — updates are incremental instead of full re-downloads
- Files are written in binary mode — .mpy and other non-text files work correctly
How to upgrade:
- Note your current settings from the top of your old
ugit.py(ssid, password, user, repository) - Replace
ugit.pyon the device with the new version - Run
create_config()once with your settings:import ugit ugit.create_config(ssid='...', password='...', user='...', repository='...')
- Update your
boot.py/main.py— remove any credential variables, just callugit.pull_all()
If you need the old version: The v1.x code is available at the v1.0 tag. You can pin to it, but it will not receive updates.
See the open issues for a list of proposed features and known issues.
- SHA1 hash-based incremental updates
- Rollback / restore function
- ugit.py self-update function
- Simplified logging
- Secure credential storage
- Storage space checking
- Zero-length file handling
- Binary file support
- Directory-level ignore matching
- USB-CDC board safety (auto-skip reset)
- Automated test suite (pytest + GitHub Actions CI)
- GitLab support
- Memory optimization for large repos
Distributed under the GNU GPL v3 License. See License.md for more information.
🐍🐙 Made with love for the OpenMuscle project 🐙🐍

