Welcome. This guide covers how to set up your environment, run the tests, and submit a contribution.
This project follows the Meshtastic Code of Conduct. Be excellent to one another.
We use the Developer Certificate of Origin, not a CLA. By signing off, you certify that you wrote the patch (or are authorized to submit it) and that you grant it under the project's license (GPL-3.0-only).
Sign every commit with -s / --signoff:
git commit -s -m "Your message"This appends a trailer:
Signed-off-by: Your Name <your.email@example.com>
Configure once globally so you don't forget:
git config --global user.name "Your Name"
git config --global user.email "your.email@example.com"
git config --global format.signOff trueThe org-wide GitHub DCO App blocks PRs whose commits aren't signed off. To fix retroactively:
git rebase --signoff HEAD~N # N = number of commits to amend
git push --force-with-lease| Tool | Version |
|---|---|
| JDK | 21 (Temurin recommended; install via sdk install java 21-tem) |
| Android Gradle Plugin (AGP) | 9.0+ (required for Kotlin 2.0 compatibility and KMP support; see Build requirements below) |
| Gradle | 8.4+ (bundled; see gradle/wrapper/gradle-wrapper.properties) |
| Android SDK | API 36 platform (only needed for Android targets); set ANDROID_HOME |
| Xcode | 15+ with iOS 14+ SDK (only needed for iOS targets, mac only) |
| Git | Any modern version with submodule support |
Clone with submodules:
git clone --recurse-submodules git@github.com:meshtastic/meshtastic-sdk.git
cd meshtastic-sdkIf you already cloned without submodules:
git submodule update --init --recursiveThe project requires AGP 9.0 or later for:
- Kotlin 2.0 compatibility — the compiler version in
libs.versions.tomlis Kotlin 2.3.x, which requires AGP 9.0+. - Kotlin Multiplatform (KMP) support — AGP 9.0+ is the first version with stable KMP support for Kotlin 2.0.
AGP 8.x is not supported. If your IDE or local environment has AGP 8.x, upgrade your Gradle plugin:
// build.gradle.kts (top-level or in a module)
plugins {
id("com.android.library") version "9.1.1" // or later
}The version is pinned in gradle/libs.versions.toml under agp = "9.1.1" (or later). Verify your IDE has picked up the new version by running:
./gradlew --version # Should show Gradle with AGP 9.xJDK 21 is the compile-time target. You can use any JDK 21+ toolchain (Temurin, Adoptium, etc.); IDEs typically auto-download it. If building from the CLI:
sdk install java 21-tem # sdkman
# or
brew install openjdk@21 # macOSThe proto/ directory is a git submodule pointing to the canonical Meshtastic protobufs. If you see strange MeshPacket, FromRadio, etc. type resolution errors, verify the submodule state:
git submodule status
# Should show something like: " 1a2b3c4d5e6f proto/src/protobufs (v1.2.3)"
# If detached or missing:
git submodule update --init --recursive| Task | Command |
|---|---|
| Full check | ./gradlew check |
| JVM tests | ./gradlew jvmTest |
| iOS sim tests (mac) | ./gradlew iosSimulatorArm64Test |
| API surface check | ./gradlew checkKotlinAbi |
| API surface dump (after intended change) | ./gradlew updateKotlinAbi |
| Lint | ./gradlew detekt spotlessCheck |
| Format | ./gradlew spotlessApply |
| Architecture rules | ./gradlew detekt :core:verifyModuleBoundary (matrix in docs/architecture/enforcement.md, rationale in ADR-008) |
| Docs preview | ./gradlew dokkaHtmlMultiModule (output: build/dokka/htmlMultiModule) |
Pre-commit hook (opt-in):
git config core.hooksPath .githooksRuns bash .github/tooling/check.sh before each commit. That script
validates agent tooling guardrails (skill frontmatter, prompt schema,
etc.); it intentionally does not run formatters or tests so commits
stay fast. Run ./gradlew spotlessApply detekt manually before
pushing.
The :core, :proto, :transport-*, :storage-sqldelight, :testing,
and :bom modules are publishable artifacts; their public Kotlin
surface is captured in <module>/api/<module>.klib.api (and
<module>/api/jvm/<module>.api for JVM). The Kotlin Gradle Plugin's
built-in ABI validator (Kotlin 2.3+, wired by PublishingConventionPlugin)
hard-fails CI when the live surface drifts from the committed dump.
Two tasks, one workflow:
| Task | Read/write | When to run |
|---|---|---|
./gradlew checkKotlinAbi |
read-only — diffs the live surface against committed dumps and fails on any mismatch | Before every push (auto-bound to check). This is what the api-check CI job runs. |
./gradlew updateKotlinAbi |
writes — regenerates <module>/api/*.api(.klib.api) files |
Only when public API changed intentionally. Commit the regenerated files in the same PR. |
Workflow:
- Make your change.
- Run
./gradlew checkKotlinAbi. Red? Inspect the diff — either:- Unintentional drift (a
publicslipped onto an internal type, a KDoc-only function widened): revert the source-side change. - Intentional API change: run
./gradlew updateKotlinAbi,git add api/, and commit the new dumps in the same PR as the source change. The PR description must call out what changed and the SemVer impact (perdocs/versioning.md).
- Unintentional drift (a
- Pre-1.0: any breaking change is allowed but requires the dump
refresh + a CHANGELOG
### Breakingentry. 1.0+: the ABI baseline is treated as the contract.
Never edit
api/*.apifiles by hand. They are generated artefacts.
:storage-sqldelight uses SQLDelight 2.x with verifyMigrations = true
(see storage-sqldelight/build.gradle.kts).
Two checks run in CI:
- The current schema in
Mesh.sqmust match the committeddatabases/<N>.dbsnapshot for the latest versionN. - Every committed migration
migration_<from>__<to>.sqmmust produce the next snapshot when applied to the previous one.
When you change the .sq schema:
- Bump the schema version (
sqldelight { databases { create("MeshDatabase") { schemaOutputDirectory.set(...) ; ... } } }already pins the version implicitly via the snapshot directory; the next snapshot file name is<N+1>.db). - Add
migration_N__N+1.sqmdescribing the SQL needed to migrate an on-disk vNdatabase to vN+1(e.g.,DROP TABLE messages;for the v1 → v2 migration that landed in the unreleased section). Use plain SQL — no SQLDelight DSL. - Regenerate the schema snapshot:
Commit the new
./gradlew :storage-sqldelight:generateCommonMainMeshDatabaseSchema
databases/<N+1>.dbalongside the migration. - Verify locally:
./gradlew :storage-sqldelight:verifySqlDelightMigration ./gradlew :storage-sqldelight:check
- Add a manual-test entry in
docs/manual-tests.mdunder section H if the migration touches user-visible data. - CHANGELOG
### Breakingentry if existing on-disk databases will lose data;### Changedotherwise. Cross-referencedocs/architecture/storage.md.
In-memory databases (:memory: driver, used by tests and by
SqlDelightStorageProvider(baseDir = "")) skip migrations entirely —
they always start at the latest schema.
The engine architecture has hard rules enforced by detekt + Gradle (see ADR-008):
:coredepends only on:proto. Never on a transport or storage implementation. (TheRadioTransport,StorageProvider, andDeviceStorageinterfaces live inside:coreitself — see ADR-006.)- No
java.*,android.*,kotlin.Result<T>incommonMainor public API. - No
Mutex/atomicfu/synchronizedin the engine package — the actor IS the synchronization primitive (see ADR-002). - Public byte payloads use
kotlinx.io.bytestring.ByteString, notokio.ByteString.
If you find these rules in your way, that's a design discussion — open an issue first; don't disable the rule.
- Fork and branch. Branch off
main. Use a short, descriptive branch name (fix/handshake-timeout,feat/transport-bluetooth-classic). - Discuss first for non-trivial work. Open an issue (or comment on an existing one) before sinking days of work into a large change. We'll happily collaborate on the design.
- Make the change.
- Add tests where there's behavior to verify. The
:testingmodule hasInMemoryStorageandFakeRadioTransportfor engine-level tests. - Update
docs/if the change touches the spec, API, or workflow. - Run
./gradlew checkand fix anything red. - If you changed the public API: run
./gradlew updateKotlinAbiand commit the regeneratedapi/*.apifiles.
- Add tests where there's behavior to verify. The
- Sign off every commit (
git commit -s). - Open the PR. Fill in the template — particularly the affirmations about license/DCO/proto compliance.
- Iterate on review. We use squash-merge; intermediate commits don't need to be perfect, but each PR should land as one logically-coherent commit.
Conventional Commits format encouraged but not enforced:
feat(transport-tcp): add IPv6 literal support
fix(engine): resolve handle to Failed(Disconnected) when supervisor cancels
docs(adr): add ADR-007 on plugin loader
The squash-merge commit subject is what shipped — keep it readable.
Significant decisions land as ADRs in docs/decisions/. Format: see existing ADRs for the template (Context / Decision / Alternatives / Consequences / Status).
When to write an ADR:
- A design choice that has multiple valid alternatives and the trade-off matters.
- A constraint we're locking in (tooling, license, API shape).
- Anything you'd want a future contributor to find when they ask "why did we do it this way?".
Number sequentially: NNN-slug.md. Once accepted, an ADR is immutable; supersede with a new ADR rather than editing.
If your change involves wire-level behavior (handshake, framing, ACK semantics, channel encryption, …):
- Verify against the firmware (
meshtastic/firmware). Cite specific files/lines in the PR description. - Cross-validate against
Meshtastic-AndroidandMeshtastic-Applewhere they implement the same thing. Cite their source paths. - Update
docs/protocol.md— the wire spec is the source of truth. - Add a manual-test entry in
docs/manual-tests.mdif the change can only be validated against a real device.
Routine bumps happen automatically via Renovate's git-submodules manager
(see renovate.json). Manually:
cd proto/src/protobufs
git fetch origin master
git checkout origin/master
cd ../../..
./gradlew updateKotlinAbi
git add proto/src/protobufs api/
git commit -s -m "chore(proto): bump submodule (<prev>..<new>)"Review the generated API diff carefully — new oneof arms break exhaustive when for consumers. SemVer impact per docs/versioning.md.
Per ADR-004, this SDK lives inside the meshtastic org and uses GPL-3.0, same as Meshtastic-Android/Meshtastic-Apple/firmware. Lifting code from those projects is allowed (and often the right answer for protocol-correct behavior). When you do:
- Add a second copyright line in the file header crediting the source repo + author.
- Note the origin in the commit message:
Origin: Meshtastic-Android/.../HeartbeatSender.kt @ <commit-sha>. - Keep the SPDX
GPL-3.0-onlyline.
Code from outside the org follows ordinary GPL-compatibility rules.
Open an issue using one of the templates. Include:
- SDK version (Maven coordinates).
- Target platform (Android API X, iOS X, JVM X, wasm browser X).
- Transport in use.
- Device firmware version (
AdminApi.getMetadata()or check the device's About screen). - Minimal reproduction (CLI command if possible).
- Logs at
LogLevel.Debugif applicable.
Don't open a public issue. Follow the disclosure process in SECURITY.md — file a private GitHub Security Advisory on this repo, or use the contacts listed there. See docs/security.md for what's in scope.
Discord / Discourse links live in the org README. For SDK-specific questions, GitHub Discussions on this repo (once enabled).