diff --git a/DEVELOPER.md b/DEVELOPER.md new file mode 100644 index 0000000..9f4b51d --- /dev/null +++ b/DEVELOPER.md @@ -0,0 +1,111 @@ +# Firmware projects on MeshForge + +This guide is for maintainers of **GitHub-hosted PlatformIO firmware** that users open in MeshForge. + +## Quick tips + +- **Optional `meshforge.yaml`** — full reference in the next section. Commit it on your **default branch** so MeshForge can narrow the tag and target pickers. +- After you push tags or change how the project should be discovered, hit **Refresh tags** on the repo page so MeshForge pulls the latest from GitHub. +- Set your repository’s **default branch** in GitHub to the branch you commit custom changes on (many maintainers name it after the mod). MeshForge periodically scans that branch. + +## meshforge.yaml + +`meshforge.yaml` is an **optional file at the repository root** (same level as `platformio.ini`). It tells MeshForge how to **filter Git tags** and **PlatformIO environments** (`[env:…]` names) in the web UI so users only see combinations that make sense for your project. + +### Where MeshForge reads it + +The **tag** and **target** dropdowns use the copy of `meshforge.yaml` from your repo’s **GitHub default branch** (via the GitHub contents API). That means: + +- Put the file on whatever branch you set as **default** in GitHub settings. +- When you change the profile, merge or push to that default branch, then use **Refresh tags** so MeshForge refetches it. +- The profile is **not** read separately per selected tag for the pickers—one file on the default branch drives the filters for the whole repo view. + +### File shape + +MeshForge only understands a top-level `meshforge:` block with optional `tags:` and `targets:` sections. Other keys are ignored. The built-in parser is intentionally small: it expects **2-space indentation** under `meshforge:` (`tags:` / `targets:` at two spaces, their fields at four), supports **line comments** (`# …`), **quoted or unquoted** string values, and **inline lists** like `[wifi, ble]` for `require_capabilities`. If the file is missing or unparsable, MeshForge shows all tags and all scanned environments (subject to the normal scan). + +```yaml +meshforge: + tags: + include: ^v2\. + targets: + include: HELTEC|TLORA|TBEAM + require_capabilities: [wifi] +``` + +### `tags.include` + +- **Type:** string interpreted as a **JavaScript `RegExp`** source (same as `new RegExp(…)` in the browser). +- **Effect:** Only **tag names** that match this pattern stay in the tag dropdown. If the pattern is missing, every tag from GitHub is listed (still sorted as usual). +- **Invalid regex:** MeshForge treats the filter as absent and keeps the full tag list. + +Use this to hide CI-only tags, old experiments, or anything you do not want offered as a firmware ref—for example `^v\d` for SemVer-looking tags only. + +### `targets.include` + +- **Type:** string → **JavaScript `RegExp`** over **PlatformIO environment names** (not display names). +- **Effect:** When no `include_template` applies (see below), environments whose names do not match are hidden from the target picker. +- **Invalid regex:** the regex filter is skipped (capabilities can still apply). + +### `targets.include_template` (tag-aware targets) + +When you want **which targets appear** to depend on **which tag is selected**, use **`tags.include` together with `targets.include_template`**: + +1. `tags.include` must match the **currently selected tag**; the match is run with `RegExp.exec`, so you can use **capturing groups** `(...)` or **named groups** `(?…)` (JavaScript regex syntax). +2. `targets.include_template` is a string that becomes a **new regex** after **placeholders** are filled in. Each substituted piece is **regex-escaped** so literal tag text does not break the pattern. + +**Placeholders** + +| Placeholder | Meaning | +| ------------------ | ---------------------------------------------- | +| `${1}`, `${2}`, … | Numbered capture groups from `tags.include` | +| `${myName}` | Named group `(?…)` from `tags.include` | +| `${myName_snake}` | Same capture, converted to **snake_case** | +| `${myName_camel}` | **lowerCamelCase** | +| `${myName_pascal}` | **PascalCase** | + +**Precedence:** If both `include_template` and `tags.include` are set and the current tag **matches** `tags.include`, the expanded template is the target regex. Otherwise MeshForge uses **`targets.include`** as a plain static regex. If neither yields a valid regex, there is no name-based filter (capabilities may still apply). + +**Example:** Tags like `acme-1.0.0` where environments are named `acme_1_0_0` and `acme_1_0_0_debug`: + +```yaml +meshforge: + tags: + include: ^(?[a-z]+)-(?[0-9]+(?:\.[0-9]+)*)$ + targets: + include_template: ^${mod_snake}_${ver_snake} +``` + +The template becomes a regex that still matches if you suffix env names (for example `_debug`). Adjust the tag regex and template to match how **you** name tags vs `env:` sections. + +### `targets.require_capabilities` + +- **Type:** inline list of strings, e.g. `[wifi, ble]`. +- **Effect:** An environment is shown only if **every** listed capability is present on that env. Capabilities are **inferred** by MeshForge from PlatformIO’s `platform` / `board` for each env (not read from `meshforge.yaml`). Today that means roughly: + - **Espressif32** (including `pioarduino` URLs) → `wifi`, `ble` + - **nordicnrf52** → `ble` + - **Raspberry Pi / Pico** → `wifi` and `ble` only when the board looks like a **Pico W** (e.g. name contains `picow` or ends with `_w`) + - Other platforms → **no** inferred capabilities + +You can use **only** `require_capabilities` (no `include` / `include_template`) to, for example, restrict the list to ESP32-class boards. If every env shows **no** capabilities (for example an old scan completed before capability detection), open the repo again on that ref so MeshForge can **rescan**; otherwise the capability filter may hide everything. + +### Practical tips + +- **Start without a file**, add `meshforge.yaml` once tag and env lists feel noisy or unsafe for end users. +- **Test regexes** in a JavaScript console (`new RegExp('…').test('tag-name')`) before committing. +- **Refresh tags** after editing the file on the default branch so the UI picks up changes quickly. + +## Release tag naming + +Tag releases so users can read both your mod and the upstream base at a glance, for example: + +`---` + +That makes it clear which upstream firmware version a release is built on. + +## Maintaining a fork + +- **Start from upstream** — Pick a tagged upstream release (often the latest) or any commit, and branch from there for your project (Meshtastic, MeshCore, or any PlatformIO-based LoRa mesh firmware). +- **Default branch** — Set that branch as the GitHub default. MeshForge uses it for periodic scans. +- **When upstream tags a new release** — **Merge** that tag into your branch and continue development. That keeps older release tags meaningful. **Do not rebase** published history you care about, or those tags will no longer sit on the older base revision users expect. +- **Branch-only workflow** — You can maintain a branch without tags; MeshForge builds from the **tip** of that branch. That works well for experimental projects before you cut releases. diff --git a/README.md b/README.md index 4fb3f36..256b31e 100644 --- a/README.md +++ b/README.md @@ -1,20 +1,45 @@ -# Mesh Forge +# MeshForge -Build custom Meshtastic firmware right from your browser. No downloads, no toolchains—everything runs in the cloud. +Build custom Meshtastic firmware right from your browser. No downloads, no toolchains—everything runs in +the cloud. + +MeshForge **cloud-compiles** and **web-flashes** custom firmware for LoRa mesh devices. It understands **PlatformIO** projects the same way your local build does. If PlatformIO can build it, MeshForge can run that build in the cloud and flash the artifacts in the browser. ## Features -- **Zero Install** - Everything runs in your browser -- **Custom Firmware** - Build bespoke Meshtastic firmware tailored to your exact needs -- **Community Extensions** - Include community modules and extensions beyond core Meshtastic -- **Share & Remix** - Publish your build profiles and let others remix your configs -- **Cloud Builds** - Compile in the cloud, flash directly to your device +- **Zero install** — Everything runs in your browser +- **Custom firmware** — Build bespoke firmware tailored to your exact needs +- **Community extensions** — Include community modules and extensions beyond core projects +- **Share and remix** — Publish your build profiles and let others remix your configs +- **Cloud builds** — Compile in the cloud, flash directly to your device + +## MeshForge understands any GitHub URL + +MeshForge understands any GitHub project URL. Swap `github.com` for `meshforge.org` and you can build and flash from the cloud. + +``` +https://github.com/Reticulum-Community/microReticulum +``` + +becomes + +``` +https://meshforge.org/Reticulum-Community/microReticulum +``` + +Keep the same `owner/repo` path (and optional `/tree/…` ref). You land in MeshForge, where you choose **tags** (or refs) and **build targets** (PlatformIO environments). + +When someone has already built your exact tag and target, MeshForge can reuse that build and you skip the wait. If you are the first for that combination, wait for the build to finish, then flash—you have saved the next person time. + +## For project developers + +If you have a custom build of Meshtastic, MeshCore, or any other PlatformIO project, head over to **[DEVELOPER.md](DEVELOPER.md)** to find out how to make sure your project works well on MeshForge. ## Community Join our Discord community: [https://discord.gg/8KgJpvjfaJ](https://discord.gg/8KgJpvjfaJ) -## Contributing to Mesh Forge +## Contributing to MeshForge ```bash # Install dependencies diff --git a/components/FeaturedProjects.tsx b/components/FeaturedProjects.tsx index 55175e0..7f75758 100644 --- a/components/FeaturedProjects.tsx +++ b/components/FeaturedProjects.tsx @@ -29,7 +29,7 @@ function FeaturedTile({ project, onOpenRepoUrl }: { project: FeaturedProject; on Email - + Bluesky diff --git a/src/lib/formatBuildErrorSummary.ts b/src/lib/formatBuildErrorSummary.ts index 35aabfb..bb0a2ce 100644 --- a/src/lib/formatBuildErrorSummary.ts +++ b/src/lib/formatBuildErrorSummary.ts @@ -1,10 +1,10 @@ /** Turn noisy Convex / GitHub errors into short copy for the repo build card. */ export function formatBuildErrorSummary(summary: string | undefined): string { - if (!summary) return '' - if (summary.includes('Unexpected inputs provided')) { + if (!summary) return "" + if (summary.includes("Unexpected inputs provided")) { return ( - 'Mesh Forge’s own GitHub workflow didn’t accept the build request (inputs out of sync). ' + - 'That’s on the Mesh Forge service, not your firmware. Retry won’t help until that workflow YAML is updated.' + "MeshForge’s own GitHub workflow didn’t accept the build request (inputs out of sync). " + + "That’s on the MeshForge service, not your firmware. Retry won’t help until that workflow YAML is updated." ) } if (summary.length > 600) { @@ -13,44 +13,44 @@ export function formatBuildErrorSummary(summary: string | undefined): string { return summary } -/** Short headline + body for people browsing firmware, not operating Mesh Forge. */ +/** Short headline + body for people browsing firmware, not operating MeshForge. */ export function buildFailurePresentation(summary: string | undefined): { headline: string body: string } { if (!summary?.trim()) { - return { headline: 'Build did not finish.', body: '' } + return { headline: "Build did not finish.", body: "" } } - if (summary.includes('Unexpected inputs provided')) { + if (summary.includes("Unexpected inputs provided")) { return { - headline: 'Couldn’t start a cloud build', + headline: "Couldn’t start a cloud build", body: - 'Something on the Mesh Forge side didn’t line up with GitHub. It’s not a signal that your repo is broken. ' + - 'Retry usually won’t fix this until the service is updated.', + "Something on the MeshForge side didn’t line up with GitHub. It’s not a signal that your repo is broken. " + + "Retry usually won’t fix this until the service is updated.", } } if (/GitHub API failed:\s*5\d\d/.test(summary) || /GitHub API failed:\s*429/.test(summary)) { return { - headline: 'GitHub was temporarily unavailable', - body: 'Starting the build failed because GitHub returned an error or rate limit. Flash again — it often works on a second try.', + headline: "GitHub was temporarily unavailable", + body: "Starting the build failed because GitHub returned an error or rate limit. Flash again — it often works on a second try.", } } - if (/GitHub API failed:\s*4\d\d/.test(summary) && !summary.includes('422')) { + if (/GitHub API failed:\s*4\d\d/.test(summary) && !summary.includes("422")) { return { - headline: 'Couldn’t start the build', + headline: "Couldn’t start the build", body: formatBuildErrorSummary(summary), } } - if (summary.includes('GitHub API failed: 422')) { + if (summary.includes("GitHub API failed: 422")) { return { - headline: 'Couldn’t start the build', + headline: "Couldn’t start the build", body: formatBuildErrorSummary(summary), } } return { - headline: 'Build failed in CI', + headline: "Build failed in CI", body: - 'Often a compile error, missing PlatformIO dependency, or bad env config in the repo. Fix the project if you can, then use Flash again. ' + - 'Transient CI issues also happen — trying again is safe.', + "Often a compile error, missing PlatformIO dependency, or bad env config in the repo. Fix the project if you can, then use Flash again. " + + "Transient CI issues also happen — trying again is safe.", } } diff --git a/src/lib/repoTreeUrl.ts b/src/lib/repoTreeUrl.ts index 75af93d..6a4a4ab 100644 --- a/src/lib/repoTreeUrl.ts +++ b/src/lib/repoTreeUrl.ts @@ -1,5 +1,5 @@ /** - * Mesh Forge tree URLs: `/owner/repo/tree//target/` with optional `/flash` for the flasher-only view. + * MeshForge tree URLs: `/owner/repo/tree//target/` with optional `/flash` for the flasher-only view. * Source ref may contain `/` (nested tags are rare but allowed). `target` is a reserved final segment pair. */ const TARGET_TAIL = /\/target\/([^/]+)$/ diff --git a/src/pages/HomePage.tsx b/src/pages/HomePage.tsx index 72b8579..e3b02cb 100644 --- a/src/pages/HomePage.tsx +++ b/src/pages/HomePage.tsx @@ -3,9 +3,9 @@ import { FeaturedProjects } from "@/components/FeaturedProjects" import { Button } from "@/components/ui/button" import { useState } from "react" import { useNavigate } from "react-router-dom" +import { parseGithubUrl } from "../lib/parseGithubUrl" const MESH_FORGE_README_URL = "https://github.com/MeshEnvy/mesh-forge#readme" -import { parseGithubUrl } from "../lib/parseGithubUrl" function encodeTreePath(ref: string) { return ref.split("/").map(encodeURIComponent).join("/") @@ -43,12 +43,12 @@ export default function HomePage() {
- Mesh Forge + MeshForge

- Mesh Forge + MeshForge

An open ecosystem and web flasher for mesh plugins, extensions, and firmware. diff --git a/src/pages/NotFoundPage.tsx b/src/pages/NotFoundPage.tsx index 165affe..5d03051 100644 --- a/src/pages/NotFoundPage.tsx +++ b/src/pages/NotFoundPage.tsx @@ -1,11 +1,11 @@ -import { Button } from '@/components/ui/button' -import { Link } from 'react-router-dom' +import { Button } from "@/components/ui/button" +import { Link } from "react-router-dom" export default function NotFoundPage() { return (

Page not found

-

That URL does not match anything in Mesh Forge.

+

That URL does not match anything in MeshForge.

- ) : null} - + {build.githubRunId ? ( + + View run on GitHub + + ) : null} + + ) + })()} +
+ ) : null} + {build.status === "failed" && build.errorSummary ? ( +
+ {(() => { + const { headline, body } = buildFailurePresentation(build.errorSummary) + return ( + <> +

{headline}

+ {body ?

{body}

: null} +
+ Technical details +
+                        {build.errorSummary.length > 2500 ? `…${build.errorSummary.slice(-2500)}` : build.errorSummary}
+                      
+
+ + ) + })()} +
+ ) : null} + {build.status === "succeeded" && !flashUrl ? ( + <> + {flashPrep === "loading" ?

Preparing USB flasher…

: null} + {flashPrep === "error" ? ( +

+ Could not load a signed URL for flashing. Use Download bundle if you need the file. +

) : null} + {flashPrep !== "loading" && flashPrep !== "error" ? ( + + ) : null} + + ) : null} ) : null}
@@ -604,9 +599,7 @@ export default function RepoPage() {
-

- Web Flasher -

+

Web Flasher

Target -
- {resolvedTargetEnv ?? "—"} -
+
{resolvedTargetEnv ?? "—"}
@@ -713,11 +704,11 @@ export default function RepoPage() { + disabled={flashPrimaryDisabled} + onClick={queueFlashArtifacts} + > + Flash +
{statusStripEl}