-
-| # | Part | Qty | Detail to verify | Source |
-|---:|---|---:|---|---|
-| 1 | GOME Ikoka Stick 0.4.0 | 1 | Hardware revision, Canadian band, RF output, firmware target, recovery | [Project page](https://github.com/ndoo/ikoka-stick-meshtastic-device) |
-| 2 | Waveshare Solar Power Manager | 1 | Exact model, panel, battery and output limits, startup behaviour, temperature | [Waveshare](https://www.waveshare.com/wiki/Solar_Power_Manager) |
-| 3 | IP65 junction box, 220×170×110 mm | 1 | Material, ingress rating after modification, temperature, vent | [AliExpress](https://www.aliexpress.com/item/1005007587120013.html) |
-| 4 | 10 W / 18 V panel, 250×340 mm | 1 | Electrical limits, dimensions, mass, weather and load suitability | [Amazon](https://a.co/d/0eJo5GCr) |
-| 5 | 3P1S or 4P1S 18650 pack | 1 | Chemistry, matched cells, protection, construction, charger, temperature | [MP&W Supply](https://mpandw.ca/) |
-| 6 | Battery protection board | 1 | Thresholds, current, temperature, wiring, compatibility | [Space Hedgehog](https://space-hedgehog.com/products/battery-protection-with-low-voltage-cut-off) |
-| 7 | 890–960 MHz four-cavity band-pass filter, 50 W | 1 | Response at 902–928 MHz, insertion loss, connectors, weather, temperature | [Alibaba](https://www.alibaba.com/product-detail/50W-890-960MHz-4-Cavity-Filter_1601399651944.html) |
-| 8 | 10–15 cm SMA right-angle to N-type female bulkhead cable | 1 | Connector gender and polarity, loss, bend, seal | [AliExpress](https://www.aliexpress.com/item/1005008569444661.html) |
-| 9 | 7.5 cm SMA male right-angle cable | 1 | Connector match, loss, bend, power handling | [AliExpress](https://www.aliexpress.com/item/1005006702037541.html) |
-| 10 | 0.5 ft USB-A to USB-C right-angle cable | 1 | Data and power needs, current, fit, strain | [Amazon](https://a.co/d/045htrEG) |
-| 11–12 | M3×35 spacers and M3×5 screws | as needed | Material, strength, fit, corrosion | Local source |
-| 13 | Gorilla Epoxy, 25 ml | 1 | Material compatibility, cure, outdoor and temperature limits | [Home Depot](https://www.homedepot.ca/product/gorilla-epoxy-syringe-25ml/1000778451) |
-| 14 | Waterproof vent plug | 1 | Thread, airflow, ingress performance after install | [AliExpress](https://www.aliexpress.com/item/1005006370919409.html) |
-| 15 | Clear outdoor silicone sealant | 1 | Material compatibility, cure, UV and temperature limits | Local hardware source |
-| 16 | Adafruit INA3221, optional | 1 | Electrical interface, address, firmware support, isolation, calibration | [DigiKey](https://www.digikey.ca/en/products/detail/adafruit-industries-llc/6062/25660599) |
+
+
+| Tool | Notes |
+|------|-------|
+| Soldering iron | Fine tip recommended for I2C wiring |
+| Solder + flux | |
+| Multimeter | For continuity and voltage checks |
+| Step drill bit | For drilling clean holes in the junction box |
+| Jigsaw or rotary tool | For cutting the solar panel opening |
+| 1/8" drill bit | For opening up mounting holes |
+| Pencil | For marking the cutout |
+| Clamps + flat piece of wood | For clamping solar panel during epoxy cure |
+| Wire strippers | |
These parts are unverified leads. Check current specifications, availability, shipping, and total cost before buying.
-## Tools and downloads
+## Printed parts and downloads
-Plan for soldering and wire tools, a multimeter, suitable drills and cutting
-tools, clamps, eye protection, and a safe work area.
+Before starting assembly, you will need the following 3D-printed parts. Both are available for download below:
-Printable files to verify:
+- **Mounting plate** for the junction box (holds the filter, Ikoka, and Waveshare board)
+- **3P1S 18650 battery holder** (or sized to match your chosen battery configuration)
-- [Mounting plate and battery holder (.3mf)](files/repeater-solar-1w-diy-build-plate-and-battery-holder.3mf)
+- [Mounting plate + 3P1S 18650 battery holder (combined .3mf)](files/repeater-solar-1w-diy-build-plate-and-battery-holder.3mf)
- [Mounting plate (.stl)](files/repeater-solar-1w-diy-build-plate.stl)
- [3P1S 18650 battery holder (.stl)](files/repeater-solar-1w-diy-build-3x18650-battery-holder.stl)
-Confirm dimensions, material, temperature performance, fasteners, battery restraint, and print quality before use.
-
-## Assembly stages
-
-
+
+## Assembly steps
+
+Keep the solar panel, battery, and USB disconnected while cutting, soldering, or changing wiring.
+
+### Enclosure Preparation
+
+1. Flip the solar panel face-down. On the back, locate the black junction box where the red and black power wires exit. Measure this junction box with calipers or a ruler.
+
+2. On the **front face** of the gray junction box, use a pencil to trace a rectangle matching the solar panel junction box dimensions. Center it near the top of the panel.
+
+3. Using a step drill bit, drill a large starter hole in the center of the traced rectangle.
+
+4. Using a jigsaw or rotary tool, cut along the traced lines to remove the rectangular section. Clean up any rough edges.
+
+### Mounting the Solar Panel
+
+5. Mix a batch of Gorilla Epoxy and apply it generously across the entire front face of the junction box, covering the area where the solar panel will sit.
+
+6. Route the solar panel's power cable through the rectangular cutout from the outside into the box.
+
+ [{ .mc-build-photo width="300" height="400" loading=lazy }](images/repeater-solar-1w-diy-build-1.jpg)
+
+7. Press the solar panel onto the epoxied face of the junction box. Place a flat piece of wood across the front of the panel and use clamps on all four corners to apply even pressure. Be careful not to over-tighten, as this can crack the panel. Alternatively, lay the assembly flat on the ground with weights on the back of the box.
+
+8. Allow the epoxy to fully cure according to the manufacturer's instructions before removing the clamps.
+
+9. Using clear outdoor silicone caulk, apply a bead around all four edges where the solar panel meets the junction box. Smooth the caulk to create a weatherproof seal.
+
+### Mounting Plate and Filter
+
+10. Attach the four M3x35 spacers to the corners of the junction box mounting plate. You may need to use a 1/8" drill bit to open the holes slightly to fit M3 screws through the plate. Use the 3D-printed mount plate as a reference for hole placement.
+
+ [{ .mc-build-photo width="300" height="400" loading=lazy }](images/repeater-solar-1w-diy-build-2.jpg)
+
+11. Mount the RF bandpass filter to the mounting plate, positioning it roughly in the center.
+
+12. Connect the N-Type to SMA cable to the filter's **output** port. This cable will pass through the box wall to the external antenna.
+
+13. Determine where the N-Type connector should exit the junction box. Using a step drill bit, drill the hole, stepping up one size at a time and test-fitting after each step until the connector fits snugly.
+
+14. Feed the N-Type connector through the box wall from the inside out and tighten the O-ring nut to secure it.
+
+15. On the **bottom** of the junction box, drill a hole for the waterproof vent plug using the step drill bit.
+
+16. Install the vent plug and tighten it.
-## Check the readings
+ [{ .mc-build-photo width="300" height="400" loading=lazy }](images/repeater-solar-1w-diy-build-3.jpg)
-No numeric acceptance range has been peer-reviewed for this page. Do not substitute guessed voltages or currents.
+### Filter and Radio Preparation
-
+17. Connect the SMA-to-SMA cable to the filter's **input** port. Finger-tighten for now.
-| Check | Acceptable result | Stop condition |
-|---|---|---|
-| Disconnected continuity | No unintended short on the documented supply or RF paths | Unexpected continuity or uncertain meter setup |
-| Connector polarity | Measurements match the reviewed schematic and manufacturer pinout | Any disagreement or unknown pin |
-| Solar, battery, and output values | Each measured value is within every connected component's current documentation | Missing limit or out-of-range value |
-| First power | Stable expected startup with no heat, odour, swelling, smoke, noise, or cycling | Any abnormal physical or electrical behaviour |
-| RF path | Correct band, filter direction, connections, and attached antenna before transmit | Missing antenna, loose/forced connector, unknown filter response |
-| Optional telemetry | Independently reproduced measurement and firmware behaviour | Unverified wiring, address, calibration, or build target |
+18. **Waveshare Solar Power Manager prep (standard model):** The original build uses a wire bridge across the BOOT button pads so that output resumes after a power loss without a button press. With all power disconnected, solder a short wire across the pads on the back of the board, as shown below. This modification is specific to the standard model; do not assume the B, C, or D versions use the same circuit. Test power-loss recovery before sealing the enclosure.
+
+ [{ .mc-build-photo width="300" height="400" loading=lazy }](images/repeater-solar-1w-diy-build-4.jpg)
+
+### Component Mounting
+
+19. Outside the box (for easier access), mount the Ikoka Stick and the Waveshare Solar Power Manager to the 3D-printed mounting plate. If using the optional INA3221, mount it as well.
+
+20. **(Optional, Adafruit INA3221)** Using a soldering iron, carefully remove the Ikoka Stick's OLED display header pins. Work slowly and gently to avoid damaging nearby components.
+
+21. **(Optional, Adafruit INA3221)** Solder the INA3221 to the Ikoka's I2C bus using the display header pads. See the [INA3221 Wiring](#ina3221-wiring-optional) section below for the pin connections.
+
+ [{ .mc-build-photo width="300" height="400" loading=lazy }](images/repeater-solar-1w-diy-build-5.jpg)
+
+22. Install the mounting plate assembly into the junction box and secure it with M3x5 screws into the M3x35 spacers.
+
+### Battery Installation
+
+23. Attach the 3D-printed battery holder to the inside front of the case, below where the solar panel wires enter. Use double-sided tape or epoxy to secure it.
+
+24. If your 18650 battery pack does not have a built-in PCM (protection circuit module), wire the battery protection board between the battery and the Waveshare. Follow the polarity markings carefully.
+
+### Power Wiring
+
+25. **(Optional, Adafruit INA3221) Solar wiring:** Connect the solar panel positive wire to INA3221 **CH3+**. Connect CH3- to the Waveshare solar input positive. Connect the solar panel negative wire directly to the Waveshare solar input negative.
+
+26. **(Optional, Adafruit INA3221) Battery wiring:** Connect the battery (or the PCM) positive wire to INA3221 **CH1+**. Connect CH1- to the positive pin on the Waveshare battery JST PH2.0 connector. Connect the battery negative wire directly to the negative pin on the JST PH2.0 connector. Plug the connector into the Waveshare board.
+
+ **Note:** If you are not using the INA3221, wire the solar panel and battery directly to the Waveshare board's solar and battery inputs.
+
+### Final Connections
+
+27. Connect the SMA cable from the filter's input to the Ikoka Stick's SMA connector. Snug both ends with a suitable wrench without twisting the cable or overtightening. A loose RF connection will cause signal loss. Attach the external antenna before applying power.
+
+28. Run the USB-A to USB-C cable from the Waveshare Solar Power Manager's USB output to the Ikoka Stick's USB-C input.
+
+ *If using the Adafruit INA3221, your completed assembly should look similar to this:*
+
+ [{ .mc-build-photo width="300" height="400" loading=lazy }](images/repeater-solar-1w-diy-build-6.jpg)
+
+ *Without the INA3221:*
+
+ [{ .mc-build-photo width="300" height="400" loading=lazy }](images/repeater-solar-1w-diy-build-7.jpg)
+
+ *Full interior view with INA3221, battery pack, and all wiring complete:*
+
+ [{ .mc-build-photo width="300" height="225" loading=lazy }](images/repeater-solar-1w-diy-build-8.jpg)
+
+## INA3221 wiring (optional)
+
+The Adafruit INA3221 connects to the Ikoka Stick via I2C using the **OLED display header** pads. Remove the display header pins and solder wires directly to the pads.
+
+**I2C connection (Ikoka display header to INA3221):**
+
+
+
+| Ikoka Display Header | INA3221 Pin | Wire |
+|----------------------|-------------|------|
+| Pin 1 - GND | GND | Ground |
+| Pin 2 - VCC (3.3V) | VCC | Power |
+| Pin 3 - SCL | SCL | Clock |
+| Pin 4 - SDA | SDA | Data |
-## Test it on the bench
+**INA3221 channel assignments:**
+
+
+
+| Channel | Connected To | Purpose |
+|---------|--------------|---------|
+| CH1 | Battery | Monitor battery voltage and current draw |
+| CH2 | (unused) | Available for future use |
+| CH3 | Solar | Monitor solar input voltage and charging current |
+
+
+
+**Note:** VCC on the INA3221 is the board power pin (3.3V from the Ikoka). VIN1/VIN2/VIN3 are the measurement channel inputs, not the power pin.
+
+### Firmware build flags
+
+MeshCore defaults to address `0x42`; the Adafruit INA3221 uses `0x40`. Keep the inherited board flags and add the sensor settings to the 30 dBm repeater environment:
+
+```ini
+[env:ikoka_stick_nrf_30dbm_repeater]
+build_flags =
+ ${ikoka_stick_nrf_repeater.build_flags}
+ ${ikoka_stick_nrf_e22_30dbm.build_flags}
+ -D TELEM_INA3221_ADDRESS=0x40
+ -UENV_INCLUDE_INA219
+ -D TELEM_INA3221_SHUNT_VALUE=0.05
+```
+
+The address and INA219 flags are from the original guide: they select `0x40` and disable the conflicting INA219 driver. The shunt-value flag matches the Adafruit board's **0.05 Ω** resistors so current readings use the correct scale. See [Adafruit's pinout](https://learn.adafruit.com/adafruit-ina3221-breakout/pinouts) and [MeshCore's sensor defaults](https://github.com/meshcore-dev/MeshCore/blob/main/src/helpers/sensors/EnvironmentSensorManager.cpp).
+
+## Wiring diagram
+
+[{ .mc-build-photo .mc-build-diagram width="600" height="543" loading=lazy }](images/repeater-solar-1w-diy-build-9.svg)
-
- - Record exact parts, revisions, source documents, schematic, photographs, polarity, and measured values.
- - Use supervised and current-limited first-power methods appropriate to the reviewed hardware.
- - Confirm the unit starts after each intended power source is applied and after a controlled power interruption.
- - Confirm the repeater's identity, firmware, radio, region, path, advert, and access settings survive reboot.
- - Confirm a nearby companion receives an advert and the planned local routing test succeeds.
- - Measure the RF/power behaviour using the reviewer's approved method; do not infer output from a firmware setting alone.
- - Run a supervised charge/load test that covers the reviewed conditions without exceeding component limits.
- - Inspect cable strain and seals, then complete an enclosure-appropriate water-resistance test.
- - Prove USB recovery and document the removal/access plan.
-
+
-Do not deploy until a hardware/electrical reviewer, RF reviewer, and site owner approve the completed test record.
+## Check before powering on { #check-the-readings }
-## Recovery and undo
+- Check polarity and continuity, then inspect solder joints for shorts.
+- Use a protected, matched 1S battery pack and stay within the cells' charging-temperature limits.
+- Confirm the antenna, filter, and RF cables are connected.
+- Check that the power manager supplies the expected USB voltage before connecting the radio.
+- Test that power returns after an interruption. If fitted, compare INA3221 readings with a multimeter.
+- [Flash and configure the repeater](../start/repeater.md), then check that a nearby companion receives its advert before installing it.
-If a check fails, stop transmitting and disconnect solar, USB, and battery power using the reviewed safe sequence. Move the equipment to a suitable supervised work area. Do not reconnect a warm, swollen, damaged, leaking, or otherwise questionable lithium battery. Restore firmware by USB only after the electrical/RF fault is understood.
+## Troubleshooting and recovery { #recovery-and-undo }
-Cutouts, epoxy, removed pins, and solder bridges may be irreversible. Replace uncertain components rather than improvising a repair. The safe rollback from the optional telemetry branch is to omit it before modification, not to assume removed hardware can be restored.
+If the unit does not start, disconnect power and recheck the wiring and BOOT-button modification. Stop using any battery that is hot, swollen, leaking, or damaged. If a firmware change fails, use the [USB recovery steps](../meshcore/flash-repeater.md#recovery-plan) after checking the power and RF connections.
-## Maintenance
+
-No interval is approved. The site record must define inspections for mounting, panel/bond, water entry, vent, seals, corrosion, cables/connectors, filter, battery condition, charge behaviour, measured values, firmware/configuration, radio verification, and physical recovery. Reinspect after any severe event or unexplained behaviour defined by the site reviewer.
+After installation, check the mounting, panel bond, seals, vent, cables, and battery condition regularly and after severe weather.
## Source
-Based on experimental community notes contributed by MrAlders0n in 2026. No
-hardware or firmware claim on this page is verified.
+Restored from [MrAlders0n's pre-overhaul guide](https://github.com/MeshCore-ca/MeshCore-Canada/blob/76a4262a354a111d19ddf8cc4abb25b1267fd9db/docs/hardware/repeater-solar-1w-diy-build.md). The original parts, assembly sequence, photos, wiring tables, and downloads are retained. The optional telemetry example also includes the Adafruit shunt value.
-Prefer a reviewed option from the [repeater selection guide](recommended-repeaters.md).
-If this experiment is approved and commissioned, complete the
-[mounting plan](repeater-mounting-options.md) and
-[repeater configuration](../config/index.md).
+For the standard power manager's limits and connector markings, see the [Waveshare manual](https://files.waveshare.com/upload/6/6c/Solar_Power_Manager_user_manual_en.pdf).
diff --git a/mkdocs.yml b/mkdocs.yml
index 6eb0577..1a7e634 100644
--- a/mkdocs.yml
+++ b/mkdocs.yml
@@ -133,7 +133,7 @@ plugins:
Antennas: Antennes
Solar repeater build: Répéteur solaire
Build a 300 mW repeater: Monter un répéteur de 300 mW
- Build a 1 W repeater (experimental): Monter un répéteur de 1 W (expérimental)
+ Build a 1 W repeater: Monter un répéteur de 1 W
Wire connector types: Types de connecteurs de fils
Install a repeater: Installer un répéteur
Mounting options: Options de montage
@@ -242,7 +242,7 @@ nav:
- Antennas: hardware/recommended-antenna.md
- Solar repeater build:
- Build a 300 mW repeater: hardware/repeater-solar-300mw-diy-build.md
- - Build a 1 W repeater (experimental): hardware/repeater-solar-1w-diy-build.md
+ - Build a 1 W repeater: hardware/repeater-solar-1w-diy-build.md
- Wire connector types: hardware/wire-connector-types.md
- Install a repeater:
- Mounting options: hardware/repeater-mounting-options.md
diff --git a/tests/browser/critical-journeys.spec.mjs b/tests/browser/critical-journeys.spec.mjs
index 03137f1..132a86d 100644
--- a/tests/browser/critical-journeys.spec.mjs
+++ b/tests/browser/critical-journeys.spec.mjs
@@ -174,16 +174,16 @@ test("English and French homepages link directly to the region finder", async ({
}
});
-test("hardware landing links directly to the experimental 1 W build", async ({ page }, testInfo) => {
+test("hardware landing links directly to the restored 1 W build", async ({ page }, testInfo) => {
await page.goto(siteRoute("/hardware/"));
- const link = page.getByRole("link", { name: "Review the experimental 1 W build" });
+ const link = page.getByRole("link", { name: "Read the 1 W build guide" });
await expect(link).toBeVisible();
await expect(link).toHaveAttribute("href", "repeater-solar-1w-diy-build/");
await link.click();
await expect(page).toHaveURL(
resolveSiteRoute(testInfo.project.use.baseURL, "/hardware/repeater-solar-1w-diy-build/"),
);
- await expect(page.getByRole("heading", { level: 1 })).toContainText("Experimental 1 W Solar Repeater");
+ await expect(page.getByRole("heading", { level: 1 })).toContainText("Building a 1 W solar repeater — Ikoka Stick");
});
test("config place deep links resolve an online city search", async ({ page }) => {
diff --git a/tests/browser/restored-1w-guide.spec.mjs b/tests/browser/restored-1w-guide.spec.mjs
new file mode 100644
index 0000000..660afb4
--- /dev/null
+++ b/tests/browser/restored-1w-guide.spec.mjs
@@ -0,0 +1,49 @@
+import { expect, test } from "./site-fixtures.mjs";
+import AxeBuilder from "@axe-core/playwright";
+import { siteRoute } from "./site-route.mjs";
+
+for (const locale of ["", "fr/"]) {
+ test(`${locale || "en/"} restored 1 W guide has usable parts, photos, wiring, and downloads`, async ({ page }) => {
+ await page.goto(siteRoute(`/${locale}hardware/repeater-solar-1w-diy-build/`));
+ await expect(page.locator("h1")).toContainText("Ikoka Stick");
+ await expect(page.locator(".mc-guide-status, .mc-page-status")).toHaveCount(0);
+ const parts = page.locator(".mc-table-wrap table").first();
+ await expect(parts.locator("tbody tr")).toHaveCount(16);
+ await expect(parts).toContainText("M3x35");
+ await expect(parts).toContainText("M3x5");
+ expect(await parts.evaluate(table => parseFloat(getComputedStyle(table).fontSize))).toBeGreaterThanOrEqual(16);
+ expect(await parts.locator("tbody td").first().evaluate(cell => parseFloat(getComputedStyle(cell).paddingLeft))).toBeGreaterThanOrEqual(8);
+ await expect(page.locator('article a[href$=".3mf"]')).toHaveCount(1);
+ await expect(page.locator('article a[href$=".stl"]')).toHaveCount(2);
+ const assemblyCount = await page.locator("#assembly-steps").evaluate(heading => {
+ let count = 0;
+ for (let element = heading.nextElementSibling; element && element.tagName !== "H2"; element = element.nextElementSibling) {
+ if (element.tagName === "OL") count += element.children.length;
+ }
+ return count;
+ });
+ expect(assemblyCount).toBe(28);
+ const photos = page.locator("img.mc-build-photo");
+ await expect(photos).toHaveCount(9);
+ for (const photo of await photos.all()) {
+ await expect(photo).toHaveAttribute("width", /^[1-9]\d+$/);
+ await expect(photo).toHaveAttribute("height", /^[1-9]\d+$/);
+ await photo.scrollIntoViewIfNeeded();
+ await expect.poll(() => photo.evaluate(image => image.complete && image.naturalWidth > 0)).toBe(true);
+ expect(await photo.locator("..").getAttribute("href")).toMatch(/repeater-solar-1w-diy-build-\d+\.(jpg|svg)$/);
+ }
+ await expect(page.locator("pre")).toContainText("TELEM_INA3221_ADDRESS=0x40");
+ await expect(page.locator("pre")).toContainText("TELEM_INA3221_SHUNT_VALUE=0.05");
+ const diagram = page.locator(".mc-build-diagram");
+ await expect(diagram).toHaveCSS("background-color", "rgb(255, 255, 255)");
+ const layout = await page.evaluate(() => ({ width: document.documentElement.clientWidth, content: document.documentElement.scrollWidth }));
+ expect(layout.content).toBeLessThanOrEqual(layout.width + 1);
+ });
+
+ test(`${locale || "en/"} restored 1 W guide remains accessible`, async ({ page }) => {
+ await page.goto(siteRoute(`/${locale}hardware/repeater-solar-1w-diy-build/`));
+ await expect(page.locator(".mc-build-table table").first()).toHaveClass(/mc-table--responsive/);
+ const audit = await new AxeBuilder({ page }).include("article").withTags(["wcag2a", "wcag2aa", "wcag21aa"]).analyze();
+ expect(audit.violations).toEqual([]);
+ });
+}
diff --git a/tests/content/devices-builds.test.mjs b/tests/content/devices-builds.test.mjs
index 9cfcfd1..4a4380a 100644
--- a/tests/content/devices-builds.test.mjs
+++ b/tests/content/devices-builds.test.mjs
@@ -93,10 +93,9 @@ test("recommendation pages state compatibility and current-purchase limits", ()
}
});
-test("build guides provide staged, printable safety checks and records", () => {
+test("the 300 mW draft retains its staged safety checks and records", () => {
for (const path of [
"docs/hardware/repeater-solar-300mw-diy-build.md",
- "docs/hardware/repeater-solar-1w-diy-build.md",
]) {
const source = read(path);
for (const phrase of [
@@ -125,7 +124,7 @@ test("the 1 W build is listed in navigation while legacy workflows stay outside
assert.match(
config,
- /Build a 1 W repeater \(experimental\): hardware\/repeater-solar-1w-diy-build\.md/,
+ /Build a 1 W repeater: hardware\/repeater-solar-1w-diy-build\.md/,
);
assert.match(config, /Wire connector types: hardware\/wire-connector-types\.md/);
assert.doesNotMatch(config, /generate-repeater-id\.md/);
@@ -140,8 +139,9 @@ test("hardware landing makes both solar build guides easy to find", () => {
for (const source of [overview, repeaters]) {
assert.match(source, /repeater-solar-300mw-diy-build\//);
assert.match(source, /repeater-solar-1w-diy-build\//);
- assert.match(source, /Experimental 1 W solar repeater/);
- assert.match(source, /electrical, RF, and site review/i);
+ assert.match(source, /1 W solar repeater — Ikoka Stick/);
+ assert.match(source, /Read the 1 W build guide/);
+ assert.match(source, /assembly photos, wiring, and optional INA3221/);
}
});
diff --git a/tests/content/restored-1w-guide.test.mjs b/tests/content/restored-1w-guide.test.mjs
new file mode 100644
index 0000000..95d2156
--- /dev/null
+++ b/tests/content/restored-1w-guide.test.mjs
@@ -0,0 +1,64 @@
+import assert from "node:assert/strict";
+import { existsSync, readFileSync } from "node:fs";
+import test from "node:test";
+
+const sourceRevision = "76a4262a354a111d19ddf8cc4abb25b1267fd9db";
+const pages = ["docs/hardware/repeater-solar-1w-diy-build.md", "docs/hardware/repeater-solar-1w-diy-build.fr.md"];
+const downloads = ["plate-and-battery-holder.3mf", "plate.stl", "3x18650-battery-holder.stl"];
+
+for (const path of pages) {
+ const source = readFileSync(path, "utf8");
+ test(`${path} preserves the original contributor, parts, steps, and illustrations`, () => {
+ assert.match(source, /MrAlders0n \(Ottawa\)/);
+ assert.ok(source.includes(sourceRevision), "Link to the exact pre-overhaul source");
+ assert.match(source, /January 1, 2026|1er janvier 2026/);
+ assert.match(source, /^scope: ottawa-field-practice$/m);
+ assert.match(source, /^status: draft$/m);
+ const steps = [...source.matchAll(/^(\d+)\. /gm)].map(match => Number(match[1]));
+ assert.deepEqual(steps, Array.from({ length: 28 }, (_, i) => i + 1));
+ const parts = [...source.matchAll(/^\| (\d+) \|/gm)].map(match => Number(match[1]));
+ assert.deepEqual(parts, Array.from({ length: 16 }, (_, i) => i + 1));
+ for (let i = 1; i <= 9; i++) {
+ const file = `repeater-solar-1w-diy-build-${i}.${i === 9 ? "svg" : "jpg"}`;
+ assert.ok(source.includes(`images/${file}`), `${file} must remain visible`);
+ assert.ok(existsSync(`docs/hardware/images/${file}`));
+ }
+ for (const ending of downloads) {
+ const file = `repeater-solar-1w-diy-build-${ending}`;
+ assert.ok(source.includes(`files/${file}`), `${file} must remain downloadable`);
+ assert.ok(existsSync(`docs/hardware/files/${file}`));
+ }
+ assert.match(source, /mc-table-wrap/);
+ assert.match(source, /mc-build-diagram/);
+ });
+
+ test(`${path} retains the useful prices, quantities, wiring, and firmware values`, () => {
+ assert.match(source, /280[-–]290/);
+ assert.match(source, /300[-–]310/);
+ assert.match(source, /M3x35[^\n]*\| 4 \|/);
+ assert.match(source, /M3x5[^\n]*\| 8 \|/);
+ for (const term of ["0.4.0", "Callboost", "Alfa", "JST PH2.0", "CH1+", "CH1-", "CH3+", "CH3-", "BOOT", "GND", "VCC", "SCL", "SDA"]) {
+ assert.ok(source.includes(term), `${term} is restored build data`);
+ }
+ assert.match(source, /6[-–]8 dBi/);
+ assert.match(source, /5[.,]8 dBi/);
+ assert.match(source, /ikoka_stick_nrf_30dbm_repeater/);
+ assert.match(source, /\$\{ikoka_stick_nrf_repeater\.build_flags\}/);
+ assert.match(source, /\$\{ikoka_stick_nrf_e22_30dbm\.build_flags\}/);
+ assert.match(source, /-D TELEM_INA3221_ADDRESS=0x40/);
+ assert.match(source, /-UENV_INCLUDE_INA219/);
+ assert.match(source, /-D TELEM_INA3221_SHUNT_VALUE=0\.05/);
+ assert.ok(source.includes("https://www.aliexpress.com/item/1005004468960058.html"), "Alternative filter source");
+ assert.ok(source.includes("https://www.alibaba.com/product-detail/50W-890-960MHz-4-Cavity-Filter_1601399651944.html"));
+ assert.match(source, /No warranty|Sans garantie/);
+ assert.match(source, /Antenna required|Antenne obligatoire/);
+ assert.match(source, /^destructive: true$/m);
+ assert.doesNotMatch(source, /Do not build or deploy this design|Default:.*omit optional telemetry|No hardware or firmware claim|Ne construisez et ne déployez pas ce modèle/);
+ });
+}
+
+test("both restored guides use the same telemetry build configuration", () => {
+ const blocks = pages.map(path => readFileSync(path, "utf8").match(/```ini\n([\s\S]*?)\n```/)?.[1]);
+ assert.ok(blocks[0]);
+ assert.equal(blocks[0], blocks[1]);
+});