Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
3913af1
Add guarded Tab5 C6 OTA updater
fellpower Sep 1, 2026
455394a
Correct C6 OTA flashing guide and add German README
fellpower Sep 1, 2026
db98abb
Fix Zig and Tab5 CI workflows
fellpower Sep 2, 2026
3a64ad9
Fix Zig CI invocation on Linux
fellpower Sep 2, 2026
8166aac
Patch managed components after dependency resolution
fellpower Sep 2, 2026
4ca7a6f
Add guarded Tab5 S3 OTA updater
fellpower Sep 2, 2026
84d0fd5
Clarify one-time flash requirement for OTA
fellpower Sep 2, 2026
2eb4760
Use USB only for Tab5 OTA updates
fellpower Sep 2, 2026
d156dbf
Fix clean ESP-Hosted SDIO patch builds
fellpower Sep 3, 2026
de7a8bd
Patch managed components after initial resolution
fellpower Sep 3, 2026
c46fa87
Reconnect ESP-NOW S3 bridge automatically
fellpower Sep 3, 2026
244109f
Detect and recover a powered-down S3 bridge
fellpower Sep 3, 2026
838e4e9
Show real ESP-NOW bridge state in status bar
fellpower Sep 3, 2026
fbbdf1c
Require an air reply for S3 bridge status
fellpower Sep 3, 2026
ef28f59
Fix Wi-Fi connect SSID alias panic
fellpower Sep 4, 2026
2e81597
fix(tab5): restart host after C6 OTA activation
fellpower Sep 5, 2026
3a7b921
refactor(tab5): remove obsolete C6 restart button
fellpower Sep 5, 2026
5b2d605
fix(tab5): preserve disabled Wi-Fi across restart
fellpower Sep 5, 2026
f95e84e
fix(tab5): keep idle ESP-NOW CNC session open
fellpower Sep 5, 2026
41f23eb
fix(espnow): track S3 liveness with heartbeat
fellpower Sep 5, 2026
63ab820
Add remote S3 UART settings and updater dashboards
fellpower Sep 5, 2026
fd578e8
Add remote grblHAL UART connection test
fellpower Sep 5, 2026
670507f
Fix MPG control gating and restore Port A power
fellpower Sep 6, 2026
1307863
Package v3.1.3-ota release binaries as single-file full images
fellpower Sep 11, 2026
69ca844
Select C6 OTA completion by running firmware version
fellpower Sep 12, 2026
5ebed08
Refresh all-firmware bundle and document C6 factory OTA test
fellpower Sep 12, 2026
e7cf3b1
Delete video guide for Modulus v3.1.3-ota installation
fellpower Sep 12, 2026
e398526
Delete legacy testing instructions for C6
fellpower Sep 12, 2026
783c877
Delete VIDEOANLEITUNG.de.md
fellpower Sep 12, 2026
c8d1637
Delete C6-LEGACY-TEST.de.md
fellpower Sep 12, 2026
446a27f
Limit legacy C6 OTA blocks to fit the 1.4.1 RPC buffer
fellpower Sep 12, 2026
191272f
Merge remote-tracking branch 'origin/feature/tab5-ota' into feature/t…
fellpower Sep 12, 2026
2ce7632
Remove wired C6 flashing from documentation
fellpower Sep 12, 2026
17a1ccb
feat(zigbee): add configurable external nodes and NanoH2 OTA
fellpower Sep 14, 2026
770e502
docs: add hardware connection and pin overview
fellpower Sep 14, 2026
bebdc19
docs: feature project video in readme
fellpower Sep 14, 2026
286d3af
fix(espnow): recover Tab5 and S3 channel changes
fellpower Sep 14, 2026
ed7c13f
fix(espnow): make Tab5 the channel master
fellpower Sep 20, 2026
b0e5dba
feat(ui): include current M Panel redesign
fellpower Sep 20, 2026
2e40ec5
fix(ui): include Zigbee quick control bridge
fellpower Sep 20, 2026
1d01a07
fix(zigbee): recover invalid coordinator state
fellpower Sep 20, 2026
ed65674
docs: point installation guide to latest release
fellpower Sep 20, 2026
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
9 changes: 3 additions & 6 deletions .github/workflows/tab5-idf.yml
Original file line number Diff line number Diff line change
Expand Up @@ -36,14 +36,14 @@ jobs:
version: ${{ steps.zig.outputs.version }}

- name: Verify Zig patch matches pin
run: test "$(zig version | awk '{print $2}')" = "$(tr -d '\r\n' < .github/zig-version)"
run: test "$(zig version | tr -d '\r\n')" = "$(tr -d '\r\n' < .github/zig-version)"

- name: UI ASCII check
shell: pwsh
run: ./scripts/check_ui_ascii.ps1

- name: Zig CI gate (test + fuzz + translate + tab5-lib + headers + NVS)
run: zig build ci -- --test-timeout 60s
run: zig build ci --summary all

- name: Cache managed components
uses: actions/cache@v4
Expand All @@ -58,14 +58,11 @@ jobs:
with:
version: "v6.0.1"

- name: Patch IDF 6 managed deps
shell: pwsh
run: ./scripts/patch_tab5_idf6_deps.ps1

- name: IDF build
working-directory: firmware/tab5
run: |
export ZIG_EXE="$(command -v zig)"
export ZIG_LIB_DIR="$(zig env | python3 -c "import sys,re; m=re.search(r'lib_dir = \"([^\"]+)\"', sys.stdin.read()); print(m.group(1) if m else '')")"
idf.py set-target esp32p4
pwsh -File ../../scripts/patch_tab5_idf6_deps.ps1
idf.py build
5 changes: 4 additions & 1 deletion .github/workflows/zig-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,10 +38,13 @@ jobs:
version: ${{ steps.zig.outputs.version }}

- name: Verify Zig patch matches pin
run: test "$(zig version | awk '{print $2}')" = "$(tr -d '\r\n' < .github/zig-version)"
run: test "$(zig version | tr -d '\r\n')" = "$(tr -d '\r\n' < .github/zig-version)"

- name: Format check
run: zig fmt --check src/ tools/
# The imported upstream tree is not fully formatted for Zig 0.16 yet.
# Keep reporting drift without turning unrelated legacy files red.
continue-on-error: true

- name: Zig CI gate
run: zig build ci --summary all
Expand Down
12 changes: 12 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
# Firmware build outputs
firmware/**/build/
firmware/**/build-*/
firmware/**/build_ota*/
firmware/**/sdkconfig
firmware/**/sdkconfig.old
firmware/**/sdkconfig.ota*
firmware/**/managed_components/

# NOT dependencies.lock — it is tracked on purpose.
Expand All @@ -25,6 +28,15 @@ zig-pkg/
# Packaged flash images (upload via GitHub Releases — do not commit binaries)
dist/

# Local recordings and exported media (never firmware source)
*.mp4
*.mov
*.avi
*.mkv
*.webm
*.m4v
*.wmv

# Node / local tooling (not required to build/flash)
node_modules/
**/node_modules/
Expand Down
3 changes: 3 additions & 0 deletions FULL-IMAGES-v3.1.3-ota.sha256
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
05b627b4fe71e39d6bbb875d713df91026d84586dab51c715d49fcba0c22acc6 modulus-tab5-p4-full-v3.1.3-ota.bin
b8109efcceca2a58717981e6ae7d8ce547b15df027dca43f340df7672bd59488 modulus-tab5-c6-full-v3.1.3-ota.bin
d7e552bfd1dfe1ff5ca77f5fb2e3bcbd1f434f686516d43c41f0ee3194a6a947 modulus-xiao-s3-full-v3.1.3-ota.bin
80 changes: 80 additions & 0 deletions README.de.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
<p align="center">
<img src="assets/modulus-firmware-hero.png" alt="Modulus Firmware – Tab5 CNC-Pendant" width="720">
</p>

<p align="center">
<a href="https://www.youtube.com/watch?v=mvP2etHl_e0">
<img src="https://img.youtube.com/vi/mvP2etHl_e0/maxresdefault.jpg" alt="Modulus-Projektvideo auf YouTube ansehen" width="720">
</a>
</p>

<p align="center"><strong><a href="https://www.youtube.com/watch?v=mvP2etHl_e0">Modulus-Projektvideo ansehen</a></strong></p>

[English](README.md) · **Deutsch**

# Modulus Firmware

**Release:** v3.1.5-ota · **Branch:** `feature/tab5-ota` · **Lizenz:** [MIT](LICENSE)

**Danksagung:** Besonderer Dank an **Sae** und **Miklos** für die wiederholten
Tests der Tab5 ↔ S3-Funkverbindung und die Diagnose-Logs, die zur festen
Kanalsteuerung geführt haben.

Modulus macht das M5Stack Tab5 zum CNC-Pendant. Der P4 übernimmt Oberfläche und
Steuerungslogik, der C6 WLAN und ESP-NOW. Eine S3-Bridge verbindet das Pendant mit
der CNC-Steuerung. NanoH2 ist der optionale Zigbee-Hub.

## Verbindungs- und Pinübersicht

Die Grafik zeigt die kabelgebundenen Busse, Funkstrecken, Versorgung und die
aktuell verwendeten GPIOs von Tab5, NanoH2, S3-Bridge und externem Zigbee-Node.
Die dargestellte Relaisanbindung über den ULN2803A ist geplant und noch nicht
an der Hardware getestet.

![Modulus Verbindungs- und Pinübersicht](assets/modulus-connection-pin-overview.png)

## Erstinstallation

Die aktuellen Dateien stehen immer im [neuesten GitHub-Release](https://github.com/fellpower/Modulus-Firmware/releases/latest).
Der Reiter **Releases** enthält die Installationsdateien, Anleitung und Prüfsummen.

- Das **Full-Image** enthält alles für eine saubere Installation, löscht alte
Einstellungen und wird bei Adresse `0x0` geschrieben.
- Für den Tab5 wird `modulus-tab5-full.bin` verwendet.
- Für den S3 wird passend zum Board `modulus-s3-generic-full.bin` oder
`modulus-s3-xiao-full.bin` verwendet.
- Die S3-**OTA-Images** sind für spätere Aktualisierungen über das S3-Menü gedacht.

Das Ziel über seinen normalen USB-Anschluss verbinden und die passende einzelne
Full-BIN an Adresse `0x0` schreiben. Beispiel für den XIAO (`COM8` durch seinen
tatsächlichen Port ersetzen):

```powershell
python -m esptool --chip esp32s3 -p COM8 erase-flash
python -m esptool --chip esp32s3 -p COM8 write-flash 0x0 modulus-s3-xiao-full.bin
```

Für einen generischen S3 das Generic-Full-Image verwenden. Die vollständigen
Tab5- und S3-Befehle stehen in `FLASH-README.md` im Release.

## Spätere S3-Updates

Das passende `modulus-s3-*-ota.bin` aus dem neuesten Release ins Hauptverzeichnis des Sticks kopieren.
**M Panel → S3 → Firmware Update → Refresh USB → Check S3 image → Flash S3**.
Nach erfolgreicher Prüfung **Restart S3** drücken. Bei Bedarf S3-MAC und
passenden Funkkanal unter Settings → Wireless eintragen.

## Aus dem Quellcode bauen

ESP-IDF 6.0.1 und Zig 0.16 verwenden. P4 über `scripts/build_tab5.ps1` bauen;
das Skript wendet die nötigen ESP-IDF-/ESP-Hosted-Patches an. Kein direktes
`idf.py build` für den ersten P4-Build verwenden.

C6: `scripts/build_tab5_c6_modulus.ps1`, anschließend `network_adapter.bin` über
das Tab5-Menü installieren. Regressionstest: `python scripts/test_c6_ota.py`.
Weitere Architektur- und Entwicklerdetails stehen in der [englischen README](README.md).

## Sicherheit

Updates nur bei stillstehender Maschine durchführen. Der echte Maschinen-Not-Aus
bleibt die primäre Sicherheitseinrichtung; der Pendant-Not-Aus ersetzt ihn nicht.
173 changes: 88 additions & 85 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,71 @@
<img src="assets/modulus-firmware-hero.png" alt="Modulus Firmware — Tab5 CNC pendant" width="720">
</p>

<p align="center">
<a href="https://www.youtube.com/watch?v=mvP2etHl_e0">
<img src="https://img.youtube.com/vi/mvP2etHl_e0/maxresdefault.jpg" alt="Watch the Modulus project video on YouTube" width="720">
</a>
</p>

<p align="center"><strong><a href="https://www.youtube.com/watch?v=mvP2etHl_e0">Watch the Modulus project video</a></strong></p>

<p align="center">
<strong>English</strong> · <a href="README.de.md">Deutsch</a>
</p>

# Modulus Firmware

**Version:** 3.1.0
**Version:** 3.1.5-ota<br>
**Author:** D. McLean / BufferRoot
**Platform:** M5Stack Tab5 (ESP32-P4 + ESP32-C6)
**Stack:** Zig 0.16 + ESP-IDF 6
**Hackster:** [Modulus pendant](https://www.hackster.io/BufferRoot/modulus-the-ultimate-universal-smart-cnc-pendant-2587ed) · [M5Stack GIC 2026](https://m5stack.com/global-innovation-contest-2026)
**License:** [MIT](LICENSE)

**Thanks:** Special thanks to **Sae** and **Miklos** for repeatedly testing the
Tab5 ↔ S3 radio link and providing the diagnostics that led to the fixed-channel design.

**One Device, One Software. Real control for any machine — no lag, no brand lock-in, no compromise.**

## Why this OTA feature branch exists

Modulus runs across multiple processors, but the Tab5's ESP32-C6 radio and the
cabinet ESP32-S3 bridge traditionally required separate USB/bootloader access.
This branch adds guarded **C6 Update** and **S3 Update** pages to M Panel. C6
application images travel over the internal ESP-Hosted/SDIO link; S3
application images travel over ESP-NOW. Both updater pages read the root of a
FAT-formatted USB-A drive and reject images built for the wrong chip. microSD
support was useful during development, but was removed from the updater UI
because the slot is inaccessible in many installed enclosures and presenting
two maintenance sources made the workflow unnecessarily ambiguous. Other
Modulus microSD features are unchanged.

The maintenance sequence is **P4 first, then C6 and/or S3**: install the P4
firmware containing the OTA UI, boot Modulus normally, and select the matching
updater. The XIAO ESP32-S3 needs the OTA-capable full image once over USB; later
updates use only its application image. Image inspection, explicit arming,
progress feedback, and a guarded restart keep every write deliberate. After a
successful C6 activation, the P4 restarts automatically after a short delay to
resynchronize the ESP-Hosted/SDIO link.
Nothing is flashed automatically.

The complete C6 and XIAO S3 OTA paths, including an S3 update from USB-A and a
successful boot from its second OTA slot, have been verified on real hardware.

> [!IMPORTANT]
> **OTA must be enabled by a one-time wired flash first.** Flash the P4 package
> over USB so the Tab5 has the OTA menus. A new or previously installed XIAO
> ESP32-S3 must then be flashed once over USB at offset `0x0` with
> `modulus-s3-xiao-full.bin` from the latest release. This installs the OTA receiver
> and dual-slot partition table. Only after that first flash can subsequent S3
> updates use `modulus-s3-xiao-ota.bin` through **M Panel → S3 Update**.
> Do not send the first-flash/full image through the OTA menu.

For the Tab5 C6, the stock ESP-Hosted firmware already provides slave OTA. The
P4 OTA-enabled firmware still has to be installed first before **C6 Update** is
available. If the C6 no longer boots or answers over SDIO, restore its complete
flash package over the C6 USB bootloader before using OTA again.

Handheld DRO + MPG **client** on Tab5 — Zig dual-core anti-lag firmware talking to grblHAL (and other engines) over ESP-NOW or RS-485. It does not replace your motion controller.

Most M5 projects cram UI and radio onto one busy chip. Modulus uses Tab5 as designed: **P4** dual-core HMI/control, **C6** for Wi-Fi/BLE/ESP-NOW, **NanoH2** for Zigbee — so motion RF and shop-IoT RF never fight.
Expand All @@ -21,6 +75,13 @@ Most M5 projects cram UI and radio onto one busy chip. Modulus uses Tab5 as desi

## Four-firmware architecture

The following overview shows the wired buses, radio links, power connection,
and the GPIO assignments currently used by the Tab5, NanoH2, S3 bridge, and
external Zigbee node. The ULN2803A relay connection is planned and still awaits
the hardware test.

![Modulus connection and pin overview](assets/modulus-connection-pin-overview.png)

```
[Operator touch UI + MPG wheel]
│
Expand Down Expand Up @@ -123,7 +184,7 @@ Most M5 projects cram UI and radio onto one busy chip. Modulus uses Tab5 as desi

Pendant E-Stop is a **convenience layer**, not a safety-rated cutoff. It rides the active link (GPIO16 NO → feed-hold `!` then soft reset). **If the wireless link is down, pendant E-Stop will not stop the machine.** Machine mushroom E-Stop is primary. Software: `envelope.zig` soft limits, zero-while-running confirm, link-offline warnings, low-battery MPG lockout.

**Out of scope (roadmap):** SC2356 camera UI · on-screen FFT · OTA dual-partition · PCNT GPIO quadrature (replace I²C ExtEncoder).
**Out of scope (roadmap):** SC2356 camera UI · on-screen FFT · PCNT GPIO quadrature (replace I²C ExtEncoder).

---

Expand All @@ -145,98 +206,40 @@ LICENSE MIT

---

## Prebuilt flash images

Download **v3.1.0** assets (bootloader + partition table + app per target):

**https://github.com/BufferRoot/Modulus-Firmware/releases/tag/v3.1.0**

| Zip | Chip | What it is | Typical port |
|-----|------|------------|--------------|
| `modulus-tab5-p4-v3.1.0.zip` | ESP32-P4 | Pendant UI + control (Zig UI Engine) | COM5 (Tab5 USB) |
| `modulus-tab5-c6-v3.1.0.zip` | ESP32-C6 | ESP-Hosted / ESP-NOW radio | COM6 (C6 USB; hold **BOOT** if needed) |
| `modulus-nanoh2-v3.1.0.zip` | ESP32-H2 | Zigbee shop hub | NanoH2 USB-C (hold **BUTTON**) |
| `modulus-s3-bridge-v3.1.0.zip` | ESP32-S3 | Cabinet ESP-NOW → UART | COM8 (bridge board) |
| `SHA256SUMS.txt` | — | Checksums for every `.bin` | — |

### What you need

1. [esptool](https://docs.espressif.com/projects/esptool/) (`pip install esptool`) **or** ESP-IDF 6 `idf.py` / `python -m esptool`
2. USB cables + drivers for each chip
3. Unzip each package into its own folder (commands below assume you `cd` into that folder)

**Flash order (first bring-up):** C6 → P4 → S3 bridge → NanoH2 (optional). Power-cycle the Tab5 after C6+P4. Keep the machine E-Stop in reach.
## Install and update

COM ports on your PC may differ — change `-p COMx` to match Device Manager.
Download the firmware from the [latest GitHub release](https://github.com/fellpower/Modulus-Firmware/releases/latest).
The Releases tab always contains the current installation files and checksums.

### 1. Tab5 C6 (wireless slave)
### Full images and app images

Unzip `modulus-tab5-c6-v3.1.0.zip`, then:
- A **full image** contains everything required for a clean wired installation
and clears old settings. Flash it at address `0x0`.
- Select `modulus-tab5-full.bin` for Tab5.
- Select `modulus-s3-generic-full.bin` or `modulus-s3-xiao-full.bin` for the
matching S3 board.
- An S3 **OTA image** is used for subsequent updates from the Tab5 S3 update menu.

```powershell
cd path\to\tab5-c6
esptool.py --chip esp32c6 -p COM6 --before default-reset --after hard-reset write_flash `
--flash-mode dio --flash-freq 80m --flash-size 4MB `
0x0 bootloader.bin `
0x8000 partition-table.bin `
0xd000 ota_data_initial.bin `
0x10000 network_adapter.bin
```

If the port never appears: hold **BOOT** on the C6 while plugging USB, then run the command.
### Initial installation

### 2. Tab5 P4 (main pendant)

Unzip `modulus-tab5-p4-v3.1.0.zip`, then:
Connect the target through its normal USB port and write the matching single
full BIN at address `0x0`. This installs the complete bootloader, partition table,
and application. Example for a XIAO S3:

```powershell
cd path\to\tab5-p4
esptool.py --chip esp32p4 -p COM5 --before default-reset --after hard-reset write_flash `
--flash-mode dio --flash-freq 40m --flash-size 16MB `
0x2000 bootloader.bin `
0x8000 partition-table.bin `
0x10000 modulus_tab5.bin
python -m esptool --chip esp32s3 -p COM8 erase-flash
python -m esptool --chip esp32s3 -p COM8 write-flash 0x0 modulus-s3-xiao-full.bin
```

Power-cycle the Tab5. Cold boot should show wireless / SDIO ready (not `0x107`).

### 3. ESP32-S3 bridge (cabinet)

Unzip `modulus-s3-bridge-v3.1.0.zip`, then:

```powershell
cd path\to\s3-bridge
esptool.py --chip esp32s3 -p COM8 --before default-reset --after hard-reset write_flash `
--flash-mode dio --flash-freq 80m --flash-size 8MB `
0x0 bootloader.bin `
0x8000 partition-table.bin `
0x10000 s3_espnow_uart_bridge.bin
```

Wire S3 UART to your CNC (grblHAL) serial. On the Tab5: **Settings → Wireless → ESP-NOW → enter S3 MAC**, lock channel **1 / 6 / 11**.

### 4. NanoH2 (Zigbee hub, optional)

Enable **EXT5V** for Grove/H2 power. Unzip `modulus-nanoh2-v3.1.0.zip`, hold **BUTTON** on the Stamp, then:

```powershell
cd path\to\nanoh2
esptool.py --chip esp32h2 -p COM7 --before default-reset --after hard-reset write_flash `
--flash-mode dio --flash-freq 48m --flash-size 4MB `
0x0 bootloader.bin `
0x8000 partition-table.bin `
0x10000 modulus_nanoh2.bin
```

UART to Tab5 is GPIO6 TX / GPIO7 RX @ 460800. **Never** flash C6 with Zigbee-exclusive builds — Zigbee stays on NanoH2.

### Verify
Replace COM8 with the actual port. Use the generic full image for a generic S3.
See `FLASH-README.md` in the release for the complete Tab5 and S3 commands.

- Compare downloaded `.bin` hashes to `SHA256SUMS.txt`
- After P4+C6: idle dashboard ≥ ~55 s with no IDLE0 WDT
- ESP-NOW path: status shows **Connected** once S3 MAC/channel are set
### Later XIAO S3 updates

Each zip also includes `FLASH.md` with the same offsets.
Copy the matching `modulus-s3-*-ota.bin` from the latest release to the USB stick root. Open
**M Panel → S3 → Firmware Update**, then **Refresh USB → Check S3 image → Flash S3**.
After successful verification select **Restart S3**. Set matching S3 MAC/channel
under Settings → Wireless if required.

---

Expand All @@ -254,7 +257,7 @@ zig build tab5-lib # freestanding Zig library
| Target | Command |
|--------|---------|
| Tab5 P4 | `.\scripts\build_tab5.ps1` then `.\scripts\flash_tab5.ps1 -Port COM5` |
| Tab5 C6 + P4 | `.\scripts\flash_tab5_dual.ps1 -C6Port COM6 -P4Port COM5` *(never `-ZigbeeExclusive`)* |
| Tab5 C6 | Build with `scripts/build_tab5_c6_modulus.ps1`, then install the app via the Tab5 C6 update menu |
| NanoH2 | `idf.py -C firmware/nanoh2 flash` (hold BUTTON; enable EXT5V) |
| S3 bridge | `.\scripts\build_s3_bridge.ps1 -Action flash -Port COM8` |
| S3 XIAO | same image: USB shell `board xiao` (or factory ` -Board xiao`) then flash |
Expand All @@ -277,7 +280,7 @@ zig build tab5-lib # freestanding Zig library
> newer IDF than 6.0.1 and the build breaks
> ([#2](https://github.com/BufferRoot/Modulus-Firmware/issues/2)).

Re-pack local builds into release zips: `.\scripts\package_flash_images.ps1`.
Host regression test for C6 OTA: `python scripts/test_c6_ota.py` (requires Zig).

### Pinout (Tab5)

Expand Down
Binary file added assets/modulus-connection-pin-overview.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading