When a received packet carries two already-consumed hops and no WIDE1-1, the
WIDE2-n branch of the digipeater produces an invalid path where the station
callsign is followed by two asterisks (CALL**).
Observed over the air on 433.775 MHz (received by a nearby tracker):
StringcleanPathAsterisks(Stringpath){Stringterms[]={",WIDE1*",",WIDE2*","*"};for(Stringterm:terms){intindex=path.indexOf(term);if(index!=-1)path.remove(index,term.length());// only one occurrence
}returnpath;}
Each term is removed only once. When the path contains more than one consumed
hop, the extra asterisk survives. buildPacket() then does:
tempPath.replace("WIDE2-1",stationCallsign+"*");
which places CALL* immediately before the leftover *, yielding CALL**.
Note: the WIDE1-1 branch is protected by if (tempPath.indexOf("*") != -1) return "";, but the WIDE2- branch has no
equivalent guard.
Verified against both cases above: output becomes CALL-10* as expected.
Related consideration
indexOf("WIDE2-") also matches the consumed form WIDE2-1*, so the digi
currently decides to repeat packets whose WIDE2 hop is already spent. Rejecting
those (as the WIDE1-1 branch does) would be more correct and would make the
malformed output impossible in the first place.
## Summary
When a received packet carries **two already-consumed hops** and no `WIDE1-1`, the
WIDE2-n branch of the digipeater produces an invalid path where the station
callsign is followed by two asterisks (`CALL**`).
Observed over the air on 433.775 MHz (received by a nearby tracker):
```
F1ZCJ-3>APFD11,F6DEV,F6DEV-11,F4MLV-10**,WIDE3-3:!4321.45NV00225.50E# ...
^^^^^^^^^^^
```
Still present in **V3.2.4**.
## Cause
`src/digi_utils.cpp`, `cleanPathAsterisks()`:
```cpp
String cleanPathAsterisks(String path) {
String terms[] = {",WIDE1*", ",WIDE2*", "*"};
for (String term : terms) {
int index = path.indexOf(term);
if (index != -1) path.remove(index, term.length()); // only one occurrence
}
return path;
}
```
Each term is removed **only once**. When the path contains more than one consumed
hop, the extra asterisk survives. `buildPacket()` then does:
```cpp
tempPath.replace("WIDE2-1", stationCallsign + "*");
```
which places `CALL*` immediately before the leftover `*`, yielding `CALL**`.
Note: the `WIDE1-1` branch is protected by
`if (tempPath.indexOf("*") != -1) return "";`, but the `WIDE2-` branch has no
equivalent guard.
## Reproduction
Digi in `mode = 3`, callsign `CALL-10`:
| input path | current output | expected |
|---|---|---|
| `N0CALL*,N1CALL-11,WIDE2-1*,WIDE3-3` | `N0CALL,N1CALL-11,CALL-10**,WIDE3-3` | `N0CALL,N1CALL-11,CALL-10*,WIDE3-3` |
| `N0CALL,N1CALL-11*,WIDE2-1*,WIDE3-3` | `N0CALL,N1CALL-11,CALL-10**,WIDE3-3` | `N0CALL,N1CALL-11,CALL-10*,WIDE3-3` |
## Suggested fix
Loop until each term is exhausted:
```cpp
String cleanPathAsterisks(String path) {
String terms[] = {",WIDE1*", ",WIDE2*", "*"};
for (String term : terms) {
int index;
while ((index = path.indexOf(term)) != -1) path.remove(index, term.length());
}
return path;
}
```
Verified against both cases above: output becomes `CALL-10*` as expected.
## Related consideration
`indexOf("WIDE2-")` also matches the **consumed** form `WIDE2-1*`, so the digi
currently decides to repeat packets whose WIDE2 hop is already spent. Rejecting
those (as the `WIDE1-1` branch does) would be more correct and would make the
malformed output impossible in the first place.
<!-- github-sync-id:4942937961 -->
Closing this — my analysis was based on an outdated local clone (V3.1.7), not on current code.
In V3.2.4 the WIDE1-1 branch already rejects paths carrying a consumed hop:
if(tempPath.indexOf("*")!=-1)return"";
which covers the case I reported. Sorry for the noise, and thanks for the work.
Closing this — my analysis was based on an outdated local clone (V3.1.7), not on current code.
In V3.2.4 the `WIDE1-1` branch already rejects paths carrying a consumed hop:
```cpp
if (tempPath.indexOf("*") != -1) return "";
```
which covers the case I reported. Sorry for the noise, and thanks for the work.
<!-- github-comment-id:5039047616 -->
Reopening — I closed this too hastily, and I was wrong.
I had checked the WIDE1-1 branch, which does carry the guard if (tempPath.indexOf("*") != -1) return "";. But this report is about the WIDE2-n branch of mode = 3, which has no such guard: it relies on cleanPathAsterisks(), and that helper removes only one occurrence per term.
Captured off the air today, from a station running V3.2.4:
Reproduction against the V3.2.4 code, mode = 3, callsign F4MLV-10:
input path
V3.2.4 output
F6DEV*,F6DEV-11,WIDE2-1*,WIDE3-3
F6DEV,F6DEV-11,F4MLV-10**,WIDE3-3
F6DEV,F6DEV-11*,WIDE2-1*,WIDE3-3
F6DEV,F6DEV-11,F4MLV-10**,WIDE3-3
F6DEV,F6DEV-11,WIDE2-1*,WIDE3-3
F6DEV,F6DEV-11,F4MLV-10*,WIDE3-3 (ok — single *)
With two consumed hops, cleanPathAsterisks() clears only the first one; the
asterisk belonging to WIDE2-1* survives, and tempPath.replace("WIDE2-1", callsign + "*")
then lands right in front of it, producing CALL**.
Verified: both failing cases above then yield F4MLV-10* as expected.
Sorry for the churn — the initial close was my mistake, the report stands.
Reopening — I closed this too hastily, and I was wrong.
I had checked the `WIDE1-1` branch, which **does** carry the guard
`if (tempPath.indexOf("*") != -1) return "";`. But this report is about the
**`WIDE2-n` branch of `mode = 3`**, which has no such guard: it relies on
`cleanPathAsterisks()`, and that helper removes only **one occurrence per term**.
Captured off the air today, from a station running V3.2.4:
```
F1ZCP-3>APFD09,F6DEV,F6DEV-11,F4MLV-10**,WIDE3-3:=4256.07N200056.12E#PHG5730/...
^^^^^^^^^^^
```
Reproduction against the V3.2.4 code, `mode = 3`, callsign `F4MLV-10`:
| input path | V3.2.4 output |
|---|---|
| `F6DEV*,F6DEV-11,WIDE2-1*,WIDE3-3` | `F6DEV,F6DEV-11,F4MLV-10**,WIDE3-3` |
| `F6DEV,F6DEV-11*,WIDE2-1*,WIDE3-3` | `F6DEV,F6DEV-11,F4MLV-10**,WIDE3-3` |
| `F6DEV,F6DEV-11,WIDE2-1*,WIDE3-3` | `F6DEV,F6DEV-11,F4MLV-10*,WIDE3-3` (ok — single `*`) |
With two consumed hops, `cleanPathAsterisks()` clears only the first one; the
asterisk belonging to `WIDE2-1*` survives, and `tempPath.replace("WIDE2-1", callsign + "*")`
then lands right in front of it, producing `CALL**`.
Suggested fix, in `cleanPathAsterisks()`:
```cpp
for (String term : terms) {
int index;
while ((index = path.indexOf(term)) != -1) path.remove(index, term.length());
}
```
Verified: both failing cases above then yield `F4MLV-10*` as expected.
Sorry for the churn — the initial close was my mistake, the report stands.
<!-- github-comment-id:5039320856 -->
Direct capture from the offending station itself, so this is no longer a reconstruction.
Station running V3.2.4, digi.mode = 3, callsign F4MLV-2. Serial output:
---> LoRa Packet Tx : F1ZCJ-3>APFD11,F6DEV,F4MLV-10,F4MLV-2**,WIDE3-3:!4321.45NV00225.50E# (Relais VHF UHF Le Mont Simel)
^^^^^^^^^^
The incoming path carried two consumed hops (F4MLV-10* and WIDE2-1*). cleanPathAsterisks() stripped only the first asterisk, so the one belonging to WIDE2-1* survived, and tempPath.replace("WIDE2-1", callsign + "*") landed
directly in front of it.
Side effect visible in the same frame: F4MLV-10* comes out as F4MLV-10, i.e.
the previous digipeater's "has been repeated" mark is erased. Nothing in the path
then indicates that station already handled the frame.
Direct capture from the offending station itself, so this is no longer a reconstruction.
Station running **V3.2.4**, `digi.mode = 3`, callsign `F4MLV-2`. Serial output:
```
---> LoRa Packet Tx : F1ZCJ-3>APFD11,F6DEV,F4MLV-10,F4MLV-2**,WIDE3-3:!4321.45NV00225.50E# (Relais VHF UHF Le Mont Simel)
^^^^^^^^^^
```
The incoming path carried two consumed hops (`F4MLV-10*` and `WIDE2-1*`).
`cleanPathAsterisks()` stripped only the first asterisk, so the one belonging to
`WIDE2-1*` survived, and `tempPath.replace("WIDE2-1", callsign + "*")` landed
directly in front of it.
Side effect visible in the same frame: `F4MLV-10*` comes out as `F4MLV-10`, i.e.
the previous digipeater's "has been repeated" mark is erased. Nothing in the path
then indicates that station already handled the frame.
<!-- github-comment-id:5039377280 -->
Tested on hardware. My earlier suggested fix was wrong — please disregard it.
Why the previous suggestion was wrong
Making cleanPathAsterisks() strip every asterisk removes the ** symptom but makes
the root cause worse: a consumed WIDE2-1* becomes an ordinary-looking WIDE2-1, so
the digi still repeats packets that have no hop left, and the previous digipeaters'
"has been repeated" marks are destroyed.
Root cause
Two separate issues in the WIDE2- branch of buildPacket():
cleanPathAsterisks(path) is called before the decision, so a consumed WIDE2-1* is indistinguishable from a free token.
Both the lookup (indexOf) and the substitution (replace) are substring
operations, so they also match inside WIDE2-1*, and replace is global.
Fix that was validated
Decide on an exact comma-delimited token, and substitute positionally —
never with replace() in this branch. cleanPathAsterisks() is no longer called
here (it remains in use for the CrossFreq branch).
Tested on hardware. **My earlier suggested fix was wrong** — please disregard it.
## Why the previous suggestion was wrong
Making `cleanPathAsterisks()` strip *every* asterisk removes the `**` symptom but makes
the root cause worse: a consumed `WIDE2-1*` becomes an ordinary-looking `WIDE2-1`, so
the digi still repeats packets that have no hop left, and the previous digipeaters'
"has been repeated" marks are destroyed.
## Root cause
Two separate issues in the `WIDE2-` branch of `buildPacket()`:
1. `cleanPathAsterisks(path)` is called **before** the decision, so a consumed
`WIDE2-1*` is indistinguishable from a free token.
2. Both the lookup (`indexOf`) and the substitution (`replace`) are **substring**
operations, so they also match inside `WIDE2-1*`, and `replace` is global.
## Fix that was validated
Decide on an **exact comma-delimited token**, and substitute **positionally** —
never with `replace()` in this branch. `cleanPathAsterisks()` is no longer called
here (it remains in use for the CrossFreq branch).
```cpp
int pathTokenIndex(const String& path, const String& token) {
int start = 0;
while (start < path.length()) {
int end = path.indexOf(",", start);
if (end == -1) end = path.length();
if (path.substring(start, end) == token) return start;
start = end + 1;
}
return -1;
}
```
```cpp
} else if (tempPath.indexOf("WIDE2-") != -1 && digiMode == 3) {
int idx = pathTokenIndex(tempPath, "WIDE2-1");
if (idx != -1) {
tempPath = tempPath.substring(0, idx) + stationCallsign + "*"
+ tempPath.substring(idx + 7);
} else {
idx = pathTokenIndex(tempPath, "WIDE2-2");
if (idx == -1) return "";
tempPath = tempPath.substring(0, idx) + stationCallsign + "*,WIDE2-1"
+ tempPath.substring(idx + 7);
}
}
```
## Test cases
| input path | expected |
|---|---|
| `F6DEV,CALL-10*,WIDE2-1*,WIDE3-3` | `""` — no free hop, must not repeat |
| `F6DEV,CALL-10*,WIDE2-1,WIDE3-3` | `F6DEV,CALL-10*,MYCALL*,WIDE3-3` |
| `F6DEV,WIDE2-2` | `F6DEV,MYCALL*,WIDE2-1` |
| `F6DEV,WIDE2-2*` | `""` |
| `F6DEV,WIDE2-1*,WIDE2-1` | `F6DEV,WIDE2-1*,MYCALL*` — mixed used/free, the case that breaks `replace()` |
## On-air validation
Built and flashed on a TTGO LoRa32 v2.1.6, `digi.mode = 3`, 433.775 MHz.
Serial capture, station `F4MLV-2`:
Previous digipeater's mark preserved:
```
Rx : F6ZDD-10>APLRG1,F4MLV-10*,WIDE2-1
Tx : F6ZDD-10>APLRG1,F4MLV-10*,F4MLV-2*
```
`WIDE2-2` correctly decremented:
```
Rx : F4GCF-14>APWW11,F4MLV-10*,WIDE2-2
Tx : F4GCF-14>APWW11,F4MLV-10*,F4MLV-2*,WIDE2-1
```
Packets with no hop left are no longer repeated (previously they produced `CALL**`):
```
Rx : F6ZDW-3>APFD09,F6DEV,F4MLV-10*,WIDE2-1*,WIDE3-3 → no Tx
Rx : F4GCF-3>APFD32,F6DEV,F4MLV-10*,WIDE2-1*,WIDE4-4 → no Tx
Rx : F1ZCJ-3>APFD11,F6DEV,F4MLV-10*,WIDE2-1*,WIDE3-3 → no Tx
```
Across every frame repeated by the patched station: no `**`, no lost asterisk.
For reference, a neighbouring digi still running stock V3.2.4 keeps emitting the
malformed form, received here during the same session:
```
Rx : F6ZDW-3>APFD09,F6DEV,F6DEV-11,F4MLV-10**,WIDE3-3
```
<!-- github-comment-id:5043026849 -->
you should find the most recent branch with few fixes for this as well (but still , test it and don't share the news yet! I'm preparing info before releasing it)
you should find the most recent branch with few fixes for this as well (but still , test it and don't share the news yet! I'm preparing info before releasing it)
<!-- github-comment-id:5046288934 -->
I tested the recent branch as you asked ― keeping it off the public issue, of
course. Here is what I found, good and bad.
What works well
The reworked configuration menu is a real improvement. It is much easier to
navigate than the previous one, and the grouping makes far more sense.
The inline map is a very good fit for an iGate. An iGate has internet by
definition, so the tiles load, and seeing the heard stations on a map is exactly
what one wants there. Nice addition.
processMode3Path() is written the right way ― walking the path token by
token, checking token == stationCallsign, verifying the earlier tokens are
marked, and refusing when already digipeated. That is precisely the approach the
WIDE2- branch is missing (see below).
Issue: previous digipeaters' marks are erased
Tested on a TTGO LoRa32 v2.1.6, digi.mode = 2 (the new numbering), 433.775 MHz,
callsign F4MLV-15. Every single digipeat strips the * from the previous
digipeater:
cleanPath() is called on the whole path before substitution, so the first *
it finds gets removed ― and that one belongs to a station that really did
digipeat the frame. Once its mark is gone, nothing in the path stops that station
from repeating the same frame again if it hears it back. I have a capture from my
own network where F4MLV-10 appears twice in the same path, which is what
this leads to in practice.
Both would be solved by doing in the WIDE2- branch what processMode3Path()
already does: find an exact comma-delimited token, substitute at that
index rather than with replace(), and leave the other asterisks untouched.
Heads-up: the digi mode numbers changed meaning
In this branch:
WIDE1-1 branch is now digiMode == 1 || digiMode == 2 (was 2 || 3)
WIDE2-n branch is now digiMode == 2 (was 3)
digiMode == 3 is the new callsign-based digipeating
So an existing installation configured with mode = 3 (WIDE1 + WIDE2) will
silently become "only repeat if my callsign is explicitly in the path" after the
update ― it will stop digipeating WIDEn-N altogether, with nothing in the logs
to explain why.
Worth adding a migration when loading an older config, or at least a warning in
the release notes.
Awesome review.
But i got a little confused.
Is this "new" v4.0.0 working or what needs fix right away?
I mean: the * has to be taken away before repeating .... as there could be not there two * in the same path, right?
Ricardo.
________________________________
De: moricef ***@***.***>
Enviado: miércoles, 22 de julio de 2026 10:23
Para: richonguzman/LoRa_APRS_iGate ***@***.***>
Cc: Ricardo Guzman ***@***.***>; Comment ***@***.***>
Asunto: Re: [richonguzman/LoRa_APRS_iGate] Digipeater emits malformed path with a double asterisk (CALL**) in WIDE2-n mode (Issue #443)
[https://avatars.githubusercontent.com/u/23724137?s=20&v=4]moricef left a comment (richonguzman/LoRa_APRS_iGate#443)<https://github.com/richonguzman/LoRa_APRS_iGate/issues/443#issuecomment-5047375421>
Hi Ricardo,
I tested the recent branch as you asked ― keeping it off the public issue, of
course. Here is what I found, good and bad.
What works well
The reworked configuration menu is a real improvement. It is much easier to
navigate than the previous one, and the grouping makes far more sense.
The inline map is a very good fit for an iGate. An iGate has internet by
definition, so the tiles load, and seeing the heard stations on a map is exactly
what one wants there. Nice addition.
processMode3Path() is written the right way ― walking the path token by
token, checking token == stationCallsign, verifying the earlier tokens are
marked, and refusing when already digipeated. That is precisely the approach the
WIDE2- branch is missing (see below).
Issue: previous digipeaters' marks are erased
Tested on a TTGO LoRa32 v2.1.6, digi.mode = 2 (the new numbering), 433.775 MHz,
callsign F4MLV-15. Every single digipeat strips the * from the previous
digipeater:
Rx F6ZDD-10>APLRG1,F4MLV-10*,WIDE2-1 → Tx F6ZDD-10>APLRG1,F4MLV-10,F4MLV-15*
Rx F4JQT-10>APLRG1,F4MLV-10*,WIDE2-1 → Tx F4JQT-10>APLRG1,F4MLV-10,F4MLV-15*
Rx F6DEV-10>APLRG1,F4MLV-10*,WIDE2-1 → Tx F6DEV-10>APLRG1,F4MLV-10,F4MLV-15*
Rx F1OFG-10>APLRG1,F4MLV-10*,WIDE2-1 → Tx F1OFG-10>APLRG1,F4MLV-10,F4MLV-15*
Rx F5ZQB-10>APLRG1,F5ZQC-10*,WIDE2-1 → Tx F5ZQB-10>APLRG1,F5ZQC-10,F4MLV-15*
Rx F6DEV-13>APWW11,F4MLV-10*,WIDE2-1 → Tx F6DEV-13>APWW11,F4MLV-10,F4MLV-15*
Rx F4GCF-11>APIN21,F4MLV-10*,WIDE2-2 → Tx F4GCF-11>APIN21,F4MLV-10,F4MLV-15*,WIDE2-1
Rx F1ZCJ-3>APFD11,F6DEV*,WIDE2-2*,... → Tx F1ZCJ-3>APFD11,F6DEV,WIDE2-2*,...
cleanPath() is called on the whole path before substitution, so the first *
it finds gets removed ― and that one belongs to a station that really did
digipeat the frame. Once its mark is gone, nothing in the path stops that station
from repeating the same frame again if it hears it back. I have a capture from my
own network where F4MLV-10 appears twice in the same path, which is what
this leads to in practice.
The ** case reported in #443<https://github.com/richonguzman/LoRa_APRS_iGate/issues/443> is still present too: the same cleanPath() only
removes one occurrence per term, so when a consumed WIDE2-1* survives, the
substitution lands right in front of the leftover asterisk. I could not capture it
on this branch (the stations producing those paths are too far for my indoor test
antenna) but it is the same code path as in #443<https://github.com/richonguzman/LoRa_APRS_iGate/issues/443>.
Both would be solved by doing in the WIDE2- branch what processMode3Path()
already does: find an exact comma-delimited token, substitute at that
index rather than with replace(), and leave the other asterisks untouched.
Heads-up: the digi mode numbers changed meaning
In this branch:
* WIDE1-1 branch is now digiMode == 1 || digiMode == 2 (was 2 || 3)
* WIDE2-n branch is now digiMode == 2 (was 3)
* digiMode == 3 is the new callsign-based digipeating
So an existing installation configured with mode = 3 (WIDE1 + WIDE2) will
silently become "only repeat if my callsign is explicitly in the path" after the
update ― it will stop digipeating WIDEn-N altogether, with nothing in the logs
to explain why.
Worth adding a migration when loading an older config, or at least a warning in
the release notes.
73
―
Reply to this email directly, view it on GitHub<https://github.com/richonguzman/LoRa_APRS_iGate/issues/443?email_source=notifications&email_token=AQWBUGQPWZEUPQGCNIYC7DT5GDE77A5CNFSNUABFM5UWIORPF5TWS5BNNB2WEL2JONZXKZKDN5WW2ZLOOQXTKMBUG4ZTONJUGIY2M4TFMFZW63VHMNXW23LFNZ2KKZLWMVXHJLDGN5XXIZLSL5RWY2LDNM#issuecomment-5047375421>, or unsubscribe<https://github.com/notifications/unsubscribe-auth/AQWBUGUUX33YO6ZOWU2TYA35GDE77AVCNFSNUABFKJSXA33TNF2G64TZHM2TSOJVGU4TGNJXHNEXG43VMU5TIOJUGI4TGNZZGYY2C5QC>.
You are receiving this because you commented.Message ID: ***@***.***>
<!-- github-comment-id:5047775888 -->
Every token in the received path was already marked — there was no hop
left at
all, the frame should have been dropped. But indexOf("WIDE2-2") matched
inside
the spent WIDE2-2*, and the replace expanded it into CALL*,WIDE2-1. So
a brand
new WIDE2-1 token now exists in a path that was exhausted, and the frame
gets a
second life instead of dying.
This is the same root cause as the asterisk problem: substring match
instead of an
exact comma-delimited token.
2. The previous digipeater's mark is erased on every single repeat
F4MLV-10 no longer appears as having handled the frame, so another digi
can
legitimately repeat it again. I have a capture from my network where F4MLV-10
ends up twice in the same path — that is what this leads to in practice.
You already have the right code
processMode3Path() does exactly what the WIDE2- branch needs: it walks
the
path token by token, compares token == stationCallsign, checks that
earlier
tokens are marked, and refuses when already repeated.
Applying that same approach to the WIDE2- branch — exact token,
substitute at
its index, leave the other asterisks alone — fixes all three symptoms at
once
(the ** of #443, the erased marks, and the re-inflated spent path).
Heads-up before release: the mode numbers changed meaning
WIDE1-1 branch is now digiMode == 1 || 2 (was 2 || 3)
WIDE2-n branch is now digiMode == 2 (was 3)
digiMode == 3 is the new callsign-based digipeating
An existing installation set to mode = 3 (WIDE1 + WIDE2) will silently
become
"only repeat if my callsign is explicitly in the path" after updating — it
stops
digipeating WIDEn-N altogether, with nothing in the logs to explain why.
Worth a migration when loading an older config, or at least a line in the
release
notes. On a hilltop digipeater that one would be painful to diagnose
remotely.
73,
F4MLV
Le mer. 22 juil. 2026 à 16:58, Ricardo Guzman @.***> a
écrit :
I tested the recent branch as you asked ― keeping it off the public issue,
of
course. Here is what I found, good and bad.
What works well
The reworked configuration menu is a real improvement. It is much easier
to
navigate than the previous one, and the grouping makes far more sense.
The inline map is a very good fit for an iGate. An iGate has internet by
definition, so the tiles load, and seeing the heard stations on a map is
exactly
what one wants there. Nice addition.
processMode3Path() is written the right way ― walking the path token by
token, checking token == stationCallsign, verifying the earlier tokens are
marked, and refusing when already digipeated. That is precisely the
approach the
WIDE2- branch is missing (see below).
Issue: previous digipeaters' marks are erased
Tested on a TTGO LoRa32 v2.1.6, digi.mode = 2 (the new numbering), 433.775
MHz,
callsign F4MLV-15. Every single digipeat strips the * from the previous
digipeater:
cleanPath() is called on the whole path before substitution, so the first
*
it finds gets removed ― and that one belongs to a station that really did
digipeat the frame. Once its mark is gone, nothing in the path stops that
station
from repeating the same frame again if it hears it back. I have a capture
from my
own network where F4MLV-10 appears twice in the same path, which is what
this leads to in practice.
The ** case reported in #443< https://github.com/richonguzman/LoRa_APRS_iGate/issues/443> is still
present too: the same cleanPath() only
removes one occurrence per term, so when a consumed WIDE2-1* survives, the
substitution lands right in front of the leftover asterisk. I could not
capture it
on this branch (the stations producing those paths are too far for my
indoor test
antenna) but it is the same code path as in #443< https://github.com/richonguzman/LoRa_APRS_iGate/issues/443>.
Both would be solved by doing in the WIDE2- branch what processMode3Path()
already does: find an exact comma-delimited token, substitute at that
index rather than with replace(), and leave the other asterisks untouched.
Heads-up: the digi mode numbers changed meaning
In this branch:
WIDE1-1 branch is now digiMode == 1 || digiMode == 2 (was 2 || 3)
WIDE2-n branch is now digiMode == 2 (was 3)
digiMode == 3 is the new callsign-based digipeating
So an existing installation configured with mode = 3 (WIDE1 + WIDE2) will
silently become "only repeat if my callsign is explicitly in the path"
after the
update ― it will stop digipeating WIDEn-N altogether, with nothing in the
logs
to explain why.
Worth adding a migration when loading an older config, or at least a
warning in
the release notes.
Follow-up — I said "nothing else blocking", but the unit kept running and
caught
something worse. Two things, then a heads-up.
## 1. A fully spent path gets repeated AND re-inflated
```
Rx : F6DEV-11>APLRG1,F6DEV*,WIDE2-2*,F4MLV-10*
Tx : F6DEV-11>APLRG1,F6DEV,F4MLV-15*,WIDE2-1*,F4MLV-10*
```
Every token in the received path was already marked — there was **no hop
left at
all**, the frame should have been dropped. But `indexOf("WIDE2-2")` matched
inside
the spent `WIDE2-2*`, and the replace expanded it into `CALL*,WIDE2-1`. So
a brand
new `WIDE2-1` token now exists in a path that was exhausted, and the frame
gets a
second life instead of dying.
This is the same root cause as the asterisk problem: substring match
instead of an
exact comma-delimited token.
## 2. The previous digipeater's mark is erased on every single repeat
Not occasional — systematic:
```
Rx : F1OFG-10>APLRG1,F4MLV-10*,WIDE2-1 → Tx : ...,F4MLV-10,F4MLV-15*
Rx : F6ZDD-10>APLRG1,F4MLV-10*,WIDE2-1 → Tx : ...,F4MLV-10,F4MLV-15*
Rx : F4JQT-10>APLRG1,F4MLV-10*,WIDE2-1 → Tx : ...,F4MLV-10,F4MLV-15*
Rx : F4JQT-4>APLRG1,F4MLV-10*,WIDE2-1 → Tx : ...,F4MLV-10,F4MLV-15*
Rx : F6DEV-10>APLRG1,F4MLV-10*,WIDE2-1 → Tx : ...,F4MLV-10,F4MLV-15*
Rx : F6DEV-13>APWW11,F4MLV-10*,WIDE2-1 → Tx : ...,F4MLV-10,F4MLV-15*
Rx : F5ZQB-10>APLRG1,F5ZQC-10*,WIDE2-1 → Tx : ...,F5ZQC-10,F4MLV-15*
```
`F4MLV-10` no longer appears as having handled the frame, so another digi
can
legitimately repeat it again. I have a capture from my network where
`F4MLV-10`
ends up **twice** in the same path — that is what this leads to in practice.
## You already have the right code
`processMode3Path()` does exactly what the `WIDE2-` branch needs: it walks
the
path token by token, compares `token == stationCallsign`, checks that
earlier
tokens are marked, and refuses when already repeated.
Applying that same approach to the `WIDE2-` branch — exact token,
substitute at
its index, leave the other asterisks alone — fixes all three symptoms at
once
(the `**` of #443, the erased marks, and the re-inflated spent path).
## Heads-up before release: the mode numbers changed meaning
- `WIDE1-1` branch is now `digiMode == 1 || 2` (was `2 || 3`)
- `WIDE2-n` branch is now `digiMode == 2` (was `3`)
- `digiMode == 3` is the new callsign-based digipeating
An existing installation set to `mode = 3` (WIDE1 + WIDE2) will silently
become
"only repeat if my callsign is explicitly in the path" after updating — it
stops
digipeating `WIDEn-N` altogether, with nothing in the logs to explain why.
Worth a migration when loading an older config, or at least a line in the
release
notes. On a hilltop digipeater that one would be painful to diagnose
remotely.
73,
F4MLV
Le mer. 22 juil. 2026 à 16:58, Ricardo Guzman ***@***.***> a
écrit :
> *richonguzman* left a comment (richonguzman/LoRa_APRS_iGate#443)
> <https://github.com/richonguzman/LoRa_APRS_iGate/issues/443#issuecomment-5047775888>
> Awesome review.
>
> But i got a little confused.
>
> Is this "new" v4.0.0 working or what needs fix right away?
>
> I mean: the * has to be taken away before repeating .... as there could be
> not there two * in the same path, right?
>
> Ricardo.
>
>
> ________________________________
> De: moricef ***@***.***>
> Enviado: miércoles, 22 de julio de 2026 10:23
> Para: richonguzman/LoRa_APRS_iGate ***@***.***>
> Cc: Ricardo Guzman ***@***.***>; Comment ***@***.***>
> Asunto: Re: [richonguzman/LoRa_APRS_iGate] Digipeater emits malformed path
> with a double asterisk (CALL**) in WIDE2-n mode (Issue #443)
>
> [https://avatars.githubusercontent.com/u/23724137?s=20&v=4]moricef left a
> comment (richonguzman/LoRa_APRS_iGate#443)<
> https://github.com/richonguzman/LoRa_APRS_iGate/issues/443#issuecomment-5047375421>
>
>
> Hi Ricardo,
>
> I tested the recent branch as you asked ― keeping it off the public issue,
> of
> course. Here is what I found, good and bad.
>
> What works well
>
> The reworked configuration menu is a real improvement. It is much easier
> to
> navigate than the previous one, and the grouping makes far more sense.
>
> The inline map is a very good fit for an iGate. An iGate has internet by
> definition, so the tiles load, and seeing the heard stations on a map is
> exactly
> what one wants there. Nice addition.
>
> processMode3Path() is written the right way ― walking the path token by
> token, checking token == stationCallsign, verifying the earlier tokens are
> marked, and refusing when already digipeated. That is precisely the
> approach the
> WIDE2- branch is missing (see below).
>
> Issue: previous digipeaters' marks are erased
>
> Tested on a TTGO LoRa32 v2.1.6, digi.mode = 2 (the new numbering), 433.775
> MHz,
> callsign F4MLV-15. Every single digipeat strips the * from the previous
> digipeater:
>
> Rx F6ZDD-10>APLRG1,F4MLV-10*,WIDE2-1 → Tx
> F6ZDD-10>APLRG1,F4MLV-10,F4MLV-15*
> Rx F4JQT-10>APLRG1,F4MLV-10*,WIDE2-1 → Tx
> F4JQT-10>APLRG1,F4MLV-10,F4MLV-15*
> Rx F6DEV-10>APLRG1,F4MLV-10*,WIDE2-1 → Tx
> F6DEV-10>APLRG1,F4MLV-10,F4MLV-15*
> Rx F1OFG-10>APLRG1,F4MLV-10*,WIDE2-1 → Tx
> F1OFG-10>APLRG1,F4MLV-10,F4MLV-15*
> Rx F5ZQB-10>APLRG1,F5ZQC-10*,WIDE2-1 → Tx
> F5ZQB-10>APLRG1,F5ZQC-10,F4MLV-15*
> Rx F6DEV-13>APWW11,F4MLV-10*,WIDE2-1 → Tx
> F6DEV-13>APWW11,F4MLV-10,F4MLV-15*
> Rx F4GCF-11>APIN21,F4MLV-10*,WIDE2-2 → Tx
> F4GCF-11>APIN21,F4MLV-10,F4MLV-15*,WIDE2-1
> Rx F1ZCJ-3>APFD11,F6DEV*,WIDE2-2*,... → Tx
> F1ZCJ-3>APFD11,F6DEV,WIDE2-2*,...
>
>
> cleanPath() is called on the whole path before substitution, so the first
> *
> it finds gets removed ― and that one belongs to a station that really did
> digipeat the frame. Once its mark is gone, nothing in the path stops that
> station
> from repeating the same frame again if it hears it back. I have a capture
> from my
> own network where F4MLV-10 appears twice in the same path, which is what
> this leads to in practice.
>
> The ** case reported in #443<
> https://github.com/richonguzman/LoRa_APRS_iGate/issues/443> is still
> present too: the same cleanPath() only
> removes one occurrence per term, so when a consumed WIDE2-1* survives, the
> substitution lands right in front of the leftover asterisk. I could not
> capture it
> on this branch (the stations producing those paths are too far for my
> indoor test
> antenna) but it is the same code path as in #443<
> https://github.com/richonguzman/LoRa_APRS_iGate/issues/443>.
>
> Both would be solved by doing in the WIDE2- branch what processMode3Path()
> already does: find an exact comma-delimited token, substitute at that
> index rather than with replace(), and leave the other asterisks untouched.
>
> Heads-up: the digi mode numbers changed meaning
>
> In this branch:
>
> * WIDE1-1 branch is now digiMode == 1 || digiMode == 2 (was 2 || 3)
> * WIDE2-n branch is now digiMode == 2 (was 3)
> * digiMode == 3 is the new callsign-based digipeating
>
> So an existing installation configured with mode = 3 (WIDE1 + WIDE2) will
> silently become "only repeat if my callsign is explicitly in the path"
> after the
> update ― it will stop digipeating WIDEn-N altogether, with nothing in the
> logs
> to explain why.
>
> Worth adding a migration when loading an older config, or at least a
> warning in
> the release notes.
>
> 73
>
> ―
> Reply to this email directly, view it on GitHub<
> https://github.com/richonguzman/LoRa_APRS_iGate/issues/443?email_source=notifications&email_token=AQWBUGQPWZEUPQGCNIYC7DT5GDE77A5CNFSNUABFM5UWIORPF5TWS5BNNB2WEL2JONZXKZKDN5WW2ZLOOQXTKMBUG4ZTONJUGIY2M4TFMFZW63VHMNXW23LFNZ2KKZLWMVXHJLDGN5XXIZLSL5RWY2LDNM#issuecomment-5047375421>,
> or unsubscribe<
> https://github.com/notifications/unsubscribe-auth/AQWBUGUUX33YO6ZOWU2TYA35GDE77AVCNFSNUABFKJSXA33TNF2G64TZHM2TSOJVGU4TGNJXHNEXG43VMU5TIOJUGI4TGNZZGYY2C5QC>.
>
> You are receiving this because you commented.Message ID: ***@***.***>
>
> —
> Reply to this email directly, view it on GitHub
> <https://github.com/richonguzman/LoRa_APRS_iGate/issues/443?email_source=notifications&email_token=AFVAA2KDMKXB2OYSXC74BMD5GDJCTA5CNFSNUABFM5UWIORPF5TWS5BNNB2WEL2JONZXKZKDN5WW2ZLOOQXTKMBUG43TONJYHA4KM4TFMFZW63VMON2GC5DFL5RWQYLOM5S2KZLWMVXHJLDGN5XXIZLSL5RWY2LDNM#issuecomment-5047775888>,
> or unsubscribe
> <https://github.com/notifications/unsubscribe-auth/AFVAA2M5DJLEB3BR225O2IL5GDJCTAVCNFSNUABFKJSXA33TNF2G64TZHM2TSOJVGU4TGNJXHNEXG43VMU5TIOJUGI4TGNZZGYY2C5QC>
> .
> You are receiving this because you modified the open/close state.Message
> ID: ***@***.***>
>
<!-- github-comment-id:5048094165 -->
Checked against the V4.0.0 tag — the behaviour cannot have changed, because src/digi_utils.cpp is byte-identical to what it was when this issue was opened:
$ git log --since=2026-07-21 origin/main -- src/digi_utils.cpp
(no commits)
$ git show V4.0.0:src/digi_utils.cpp | md5sum
0e0d912556639028d7de025410f14903
$ git show <last commit before 2026-07-21>:src/digi_utils.cpp | md5sum
0e0d912556639028d7de025410f14903
So both halves of the cause are still in place on V4.0.0:
src/digi_utils.cpp on V4.0.0 — cleanPath() still removes a single occurrence per term:
String cleanPath(String path) {
String terms[] = {"WIDE1*,", "WIDE2*,", "*"};
for (String term : terms) {
int index = path.indexOf(term);
if (index != -1) path.remove(index, term.length()); // one occurrence only
}
return path;
}
and the WIDE2-n branch (line 95) still substitutes by substring rather than by token:
Exact trigger. I could not capture it on the air again — the stations producing those paths are out of reach of my indoor antenna — so I reproduced the two functions verbatim on the host. The ** appears when an asterisk sits anywhere earlier in the path and WIDE2-1 is itself already consumed:
in : F6DEV*,F6DEV-11,WIDE2-1*,WIDE3-3
out : F6DEV,F6DEV-11,F4MLV-10**,WIDE3-3 <-- malformed
That is byte-for-byte the tail of the frame reported in the issue. Step by step: cleanPath() finds the first * (the one on F6DEV) and removes it, leaving the one on WIDE2-1* in place; indexOf("WIDE2-1") then matches inside WIDE2-1*, and replace() inserts F4MLV-10* right in front of the surviving asterisk.
Note the second-order problem: WIDE2-1* is an already-used hop. It must not be consumed a second time at all — the malformed path is only the visible symptom.
Fix. Same approach processMode3Path() already takes: match an exact comma-delimited token and substitute at that index. No replace(), and the other asterisks are left alone.
static int pathTokenIndex(const String& path, const String& token) {
unsigned int start = 0;
while (start < path.length()) {
int end = path.indexOf(",", start);
if (end == -1) end = path.length();
if (path.substring(start, end) == token) return start;
start = end + 1;
}
return -1;
}
I have been running this on my own fork on F4MLV-10 (mountain-top digi, ~150 frames/h, mode 2). Confirmed on the air, e.g. F4MLV-7>APLRT1,WIDE1-1,WIDE2-1 relayed as F4MLV-7>APLRT1,F4MLV-10*,WIDE2-1 — WIDE1-1 consumed, WIDE2-1 left intact.
Happy to open a PR if that helps.
73
Hi Ricardo,
Checked against the V4.0.0 tag — the behaviour cannot have changed, because src/digi_utils.cpp is byte-identical to what it was when this issue was opened:
$ git log --since=2026-07-21 origin/main -- src/digi_utils.cpp
(no commits)
$ git show V4.0.0:src/digi_utils.cpp | md5sum
0e0d912556639028d7de025410f14903
$ git show <last commit before 2026-07-21>:src/digi_utils.cpp | md5sum
0e0d912556639028d7de025410f14903
So both halves of the cause are still in place on V4.0.0:
src/digi_utils.cpp on V4.0.0 — cleanPath() still removes a single occurrence per term:
String cleanPath(String path) {
String terms[] = {"WIDE1*,", "WIDE2*,", "*"};
for (String term : terms) {
int index = path.indexOf(term);
if (index != -1) path.remove(index, term.length()); // one occurrence only
}
return path;
}
and the WIDE2-n branch (line 95) still substitutes by substring rather than by token:
} else if (tempPath.indexOf("WIDE2-") != -1 && digiMode == 2) {
tempPath = cleanPath(path);
if (tempPath.indexOf("WIDE2-1") != -1) {
tempPath.replace("WIDE2-1", stationCallsign + "*");
Exact trigger. I could not capture it on the air again — the stations producing those paths are out of reach of my indoor antenna — so I reproduced the two functions verbatim on the host. The ** appears when an asterisk sits anywhere earlier in the path and WIDE2-1 is itself already consumed:
in : F6DEV*,F6DEV-11,WIDE2-1*,WIDE3-3
out : F6DEV,F6DEV-11,F4MLV-10**,WIDE3-3 <-- malformed
That is byte-for-byte the tail of the frame reported in the issue. Step by step: cleanPath() finds the first * (the one on F6DEV) and removes it, leaving the one on WIDE2-1* in place; indexOf("WIDE2-1") then matches inside WIDE2-1*, and replace() inserts F4MLV-10* right in front of the surviving asterisk.
Note the second-order problem: WIDE2-1* is an already-used hop. It must not be consumed a second time at all — the malformed path is only the visible symptom.
Fix. Same approach processMode3Path() already takes: match an exact comma-delimited token and substitute at that index. No replace(), and the other asterisks are left alone.
static int pathTokenIndex(const String& path, const String& token) {
unsigned int start = 0;
while (start < path.length()) {
int end = path.indexOf(",", start);
if (end == -1) end = path.length();
if (path.substring(start, end) == token) return start;
start = end + 1;
}
return -1;
}
} else if (tempPath.indexOf("WIDE2-") != -1 && digiMode == 2) {
int idx = pathTokenIndex(tempPath, "WIDE2-1");
if (idx != -1) {
tempPath = tempPath.substring(0, idx) + stationCallsign + "*" + tempPath.substring(idx + 7);
} else {
idx = pathTokenIndex(tempPath, "WIDE2-2");
if (idx == -1) return "";
tempPath = tempPath.substring(0, idx) + stationCallsign + "*,WIDE2-1" + tempPath.substring(idx + 7);
}
}
Same three inputs through the patched code:
F6DEV*,F6DEV-11,WIDE2-1*,WIDE3-3 -> <not digipeated> (hop already used, correct)
F6DEV*,F6DEV-11,WIDE2-1,WIDE3-3 -> F6DEV,F6DEV-11,F4MLV-10*,WIDE3-3
F6DEV*,F6DEV-11,WIDE2-2,WIDE3-3 -> F6DEV,F6DEV-11,F4MLV-10*,WIDE2-1,WIDE3-3
I have been running this on my own fork on F4MLV-10 (mountain-top digi, ~150 frames/h, mode 2). Confirmed on the air, e.g. F4MLV-7>APLRT1,WIDE1-1,WIDE2-1 relayed as F4MLV-7>APLRT1,F4MLV-10*,WIDE2-1 — WIDE1-1 consumed, WIDE2-1 left intact.
Happy to open a PR if that helps.
73
<!-- github-comment-id:5319603538 -->
Checked on 4.0.1 (2026-09-07): src/digi_utils.cpp is byte-identical to V4.0.0 — git log V4.0.0..main -- src/digi_utils.cpp returns nothing — so the behaviour is unchanged.
Since the trigger is hard to catch on the air, I wrapped it in a small host harness that compiles your actualdigi_utils.cpp against stub headers. No firmware source is modified, no board needed:
g++ -std=c++17 -Istubs -o run digi_host_test.cpp ../src/digi_utils.cpp && ./run
On current main, 16 cases covering modes 1/2/3, cross-frequency and exhausted hops:
13 passed, 3 failed
All three failures sit in the exhausted hops block:
FAIL mode 2 WIDE2-1 already used
in : F4MLV-7>APLRT1,F6DEV-10*,WIDE2-1*:=/8gk=NmQF[LWQ
expected : <not digipeated>
got : F4MLV-7>APLRT1,F6DEV-10,F4MLV-10**:=/8gk=NmQF[LWQ
FAIL mode 2 reported frame: earlier '*' + used WIDE2-1
got : F1ZCJ-3>APFD11,F6DEV,F6DEV-11,F4MLV-10**,WIDE3-3:!...
FAIL mode 2 two consumed hops, WIDE2-1 free
in : F4MLV-7>APLRT1,F6DEV*,F6DEV-11*,WIDE2-1:=/...
expected : F4MLV-7>APLRT1,F6DEV*,F6DEV-11*,F4MLV-10*:=/...
got : F4MLV-7>APLRT1,F6DEV,F6DEV-11*,F4MLV-10*:=/...
The ** is the cosmetic part. What matters more is that WIDE2-1* — a hop that has already been consumed — gets used again for digipeating. The third case shows a separate effect of the same function: cleanPath() erased the asterisk on F6DEV, losing the record that it had digipeated the frame.
The String stub mirrors WString.cpp from arduino-esp32 where it matters — replace() replaces every occurrence, remove() only one — so the harness reproduces the real semantics rather than approximating them.
With the token-based substitution the same 16 cases pass. Opened as #463, one file, +17/−6.
Happy to add the harness to the PR if you find it useful, or to leave the PR as the fix alone — whichever suits you.
73
Hi Ricardo,
Checked on `4.0.1` (2026-09-07): `src/digi_utils.cpp` is byte-identical to V4.0.0 — `git log V4.0.0..main -- src/digi_utils.cpp` returns nothing — so the behaviour is unchanged.
Since the trigger is hard to catch on the air, I wrapped it in a small host harness that compiles your **actual** `digi_utils.cpp` against stub headers. No firmware source is modified, no board needed:
```sh
g++ -std=c++17 -Istubs -o run digi_host_test.cpp ../src/digi_utils.cpp && ./run
```
On current `main`, 16 cases covering modes 1/2/3, cross-frequency and exhausted hops:
```
13 passed, 3 failed
```
All three failures sit in the `exhausted hops` block:
```
FAIL mode 2 WIDE2-1 already used
in : F4MLV-7>APLRT1,F6DEV-10*,WIDE2-1*:=/8gk=NmQF[LWQ
expected : <not digipeated>
got : F4MLV-7>APLRT1,F6DEV-10,F4MLV-10**:=/8gk=NmQF[LWQ
FAIL mode 2 reported frame: earlier '*' + used WIDE2-1
got : F1ZCJ-3>APFD11,F6DEV,F6DEV-11,F4MLV-10**,WIDE3-3:!...
FAIL mode 2 two consumed hops, WIDE2-1 free
in : F4MLV-7>APLRT1,F6DEV*,F6DEV-11*,WIDE2-1:=/...
expected : F4MLV-7>APLRT1,F6DEV*,F6DEV-11*,F4MLV-10*:=/...
got : F4MLV-7>APLRT1,F6DEV,F6DEV-11*,F4MLV-10*:=/...
```
The `**` is the cosmetic part. What matters more is that `WIDE2-1*` — a hop that has already been consumed — gets used again for digipeating. The third case shows a separate effect of the same function: `cleanPath()` erased the asterisk on `F6DEV`, losing the record that it had digipeated the frame.
The `String` stub mirrors `WString.cpp` from arduino-esp32 where it matters — `replace()` replaces every occurrence, `remove()` only one — so the harness reproduces the real semantics rather than approximating them.
With the token-based substitution the same 16 cases pass. Opened as #463, one file, +17/−6.
Happy to add the harness to the PR if you find it useful, or to leave the PR as the fix alone — whichever suits you.
73
<!-- github-comment-id:5617701218 -->
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Summary
When a received packet carries two already-consumed hops and no
WIDE1-1, theWIDE2-n branch of the digipeater produces an invalid path where the station
callsign is followed by two asterisks (
CALL**).Observed over the air on 433.775 MHz (received by a nearby tracker):
Still present in V3.2.4.
Cause
src/digi_utils.cpp,cleanPathAsterisks():Each term is removed only once. When the path contains more than one consumed
hop, the extra asterisk survives.
buildPacket()then does:which places
CALL*immediately before the leftover*, yieldingCALL**.Note: the
WIDE1-1branch is protected byif (tempPath.indexOf("*") != -1) return "";, but theWIDE2-branch has noequivalent guard.
Reproduction
Digi in
mode = 3, callsignCALL-10:N0CALL*,N1CALL-11,WIDE2-1*,WIDE3-3N0CALL,N1CALL-11,CALL-10**,WIDE3-3N0CALL,N1CALL-11,CALL-10*,WIDE3-3N0CALL,N1CALL-11*,WIDE2-1*,WIDE3-3N0CALL,N1CALL-11,CALL-10**,WIDE3-3N0CALL,N1CALL-11,CALL-10*,WIDE3-3Suggested fix
Loop until each term is exhausted:
Verified against both cases above: output becomes
CALL-10*as expected.Related consideration
indexOf("WIDE2-")also matches the consumed formWIDE2-1*, so the digicurrently decides to repeat packets whose WIDE2 hop is already spent. Rejecting
those (as the
WIDE1-1branch does) would be more correct and would make themalformed output impossible in the first place.
Closing this — my analysis was based on an outdated local clone (V3.1.7), not on current code.
In V3.2.4 the
WIDE1-1branch already rejects paths carrying a consumed hop:which covers the case I reported. Sorry for the noise, and thanks for the work.
Reopening — I closed this too hastily, and I was wrong.
I had checked the
WIDE1-1branch, which does carry the guardif (tempPath.indexOf("*") != -1) return "";. But this report is about theWIDE2-nbranch ofmode = 3, which has no such guard: it relies oncleanPathAsterisks(), and that helper removes only one occurrence per term.Captured off the air today, from a station running V3.2.4:
Reproduction against the V3.2.4 code,
mode = 3, callsignF4MLV-10:F6DEV*,F6DEV-11,WIDE2-1*,WIDE3-3F6DEV,F6DEV-11,F4MLV-10**,WIDE3-3F6DEV,F6DEV-11*,WIDE2-1*,WIDE3-3F6DEV,F6DEV-11,F4MLV-10**,WIDE3-3F6DEV,F6DEV-11,WIDE2-1*,WIDE3-3F6DEV,F6DEV-11,F4MLV-10*,WIDE3-3(ok — single*)With two consumed hops,
cleanPathAsterisks()clears only the first one; theasterisk belonging to
WIDE2-1*survives, andtempPath.replace("WIDE2-1", callsign + "*")then lands right in front of it, producing
CALL**.Suggested fix, in
cleanPathAsterisks():Verified: both failing cases above then yield
F4MLV-10*as expected.Sorry for the churn — the initial close was my mistake, the report stands.
Direct capture from the offending station itself, so this is no longer a reconstruction.
Station running V3.2.4,
digi.mode = 3, callsignF4MLV-2. Serial output:The incoming path carried two consumed hops (
F4MLV-10*andWIDE2-1*).cleanPathAsterisks()stripped only the first asterisk, so the one belonging toWIDE2-1*survived, andtempPath.replace("WIDE2-1", callsign + "*")landeddirectly in front of it.
Side effect visible in the same frame:
F4MLV-10*comes out asF4MLV-10, i.e.the previous digipeater's "has been repeated" mark is erased. Nothing in the path
then indicates that station already handled the frame.
Glad you tested ;)
Was it solved or not?
Tested on hardware. My earlier suggested fix was wrong — please disregard it.
Why the previous suggestion was wrong
Making
cleanPathAsterisks()strip every asterisk removes the**symptom but makesthe root cause worse: a consumed
WIDE2-1*becomes an ordinary-lookingWIDE2-1, sothe digi still repeats packets that have no hop left, and the previous digipeaters'
"has been repeated" marks are destroyed.
Root cause
Two separate issues in the
WIDE2-branch ofbuildPacket():cleanPathAsterisks(path)is called before the decision, so a consumedWIDE2-1*is indistinguishable from a free token.indexOf) and the substitution (replace) are substringoperations, so they also match inside
WIDE2-1*, andreplaceis global.Fix that was validated
Decide on an exact comma-delimited token, and substitute positionally —
never with
replace()in this branch.cleanPathAsterisks()is no longer calledhere (it remains in use for the CrossFreq branch).
Test cases
F6DEV,CALL-10*,WIDE2-1*,WIDE3-3""— no free hop, must not repeatF6DEV,CALL-10*,WIDE2-1,WIDE3-3F6DEV,CALL-10*,MYCALL*,WIDE3-3F6DEV,WIDE2-2F6DEV,MYCALL*,WIDE2-1F6DEV,WIDE2-2*""F6DEV,WIDE2-1*,WIDE2-1F6DEV,WIDE2-1*,MYCALL*— mixed used/free, the case that breaksreplace()On-air validation
Built and flashed on a TTGO LoRa32 v2.1.6,
digi.mode = 3, 433.775 MHz.Serial capture, station
F4MLV-2:Previous digipeater's mark preserved:
WIDE2-2correctly decremented:Packets with no hop left are no longer repeated (previously they produced
CALL**):Across every frame repeated by the patched station: no
**, no lost asterisk.For reference, a neighbouring digi still running stock V3.2.4 keeps emitting the
malformed form, received here during the same session:
you should find the most recent branch with few fixes for this as well (but still , test it and don't share the news yet! I'm preparing info before releasing it)
For the sake of discretion, I sent you an email at the address found on QRZ.
Awesome review.
But i got a little confused.
Is this "new" v4.0.0 working or what needs fix right away?
I mean: the * has to be taken away before repeating .... as there could be not there two * in the same path, right?
Ricardo.
De: moricef @.>
Enviado: miércoles, 22 de julio de 2026 10:23
Para: richonguzman/LoRa_APRS_iGate @.>
Cc: Ricardo Guzman @.>; Comment @.>
Asunto: Re: [richonguzman/LoRa_APRS_iGate] Digipeater emits malformed path with a double asterisk (CALL**) in WIDE2-n mode (Issue #443)
[https://avatars.githubusercontent.com/u/23724137?s=20&v=4]moricef left a comment (richonguzman/LoRa_APRS_iGate#443)https://github.com/richonguzman/LoRa_APRS_iGate/issues/443#issuecomment-5047375421
Hi Ricardo,
I tested the recent branch as you asked ― keeping it off the public issue, of
course. Here is what I found, good and bad.
What works well
The reworked configuration menu is a real improvement. It is much easier to
navigate than the previous one, and the grouping makes far more sense.
The inline map is a very good fit for an iGate. An iGate has internet by
definition, so the tiles load, and seeing the heard stations on a map is exactly
what one wants there. Nice addition.
processMode3Path() is written the right way ― walking the path token by
token, checking token == stationCallsign, verifying the earlier tokens are
marked, and refusing when already digipeated. That is precisely the approach the
WIDE2- branch is missing (see below).
Issue: previous digipeaters' marks are erased
Tested on a TTGO LoRa32 v2.1.6, digi.mode = 2 (the new numbering), 433.775 MHz,
callsign F4MLV-15. Every single digipeat strips the * from the previous
digipeater:
Rx F6ZDD-10>APLRG1,F4MLV-10*,WIDE2-1 → Tx F6ZDD-10>APLRG1,F4MLV-10,F4MLV-15*
Rx F4JQT-10>APLRG1,F4MLV-10*,WIDE2-1 → Tx F4JQT-10>APLRG1,F4MLV-10,F4MLV-15*
Rx F6DEV-10>APLRG1,F4MLV-10*,WIDE2-1 → Tx F6DEV-10>APLRG1,F4MLV-10,F4MLV-15*
Rx F1OFG-10>APLRG1,F4MLV-10*,WIDE2-1 → Tx F1OFG-10>APLRG1,F4MLV-10,F4MLV-15*
Rx F5ZQB-10>APLRG1,F5ZQC-10*,WIDE2-1 → Tx F5ZQB-10>APLRG1,F5ZQC-10,F4MLV-15*
Rx F6DEV-13>APWW11,F4MLV-10*,WIDE2-1 → Tx F6DEV-13>APWW11,F4MLV-10,F4MLV-15*
Rx F4GCF-11>APIN21,F4MLV-10*,WIDE2-2 → Tx F4GCF-11>APIN21,F4MLV-10,F4MLV-15*,WIDE2-1
Rx F1ZCJ-3>APFD11,F6DEV*,WIDE2-2*,... → Tx F1ZCJ-3>APFD11,F6DEV,WIDE2-2*,...
cleanPath() is called on the whole path before substitution, so the first *
it finds gets removed ― and that one belongs to a station that really did
digipeat the frame. Once its mark is gone, nothing in the path stops that station
from repeating the same frame again if it hears it back. I have a capture from my
own network where F4MLV-10 appears twice in the same path, which is what
this leads to in practice.
The ** case reported in #443https://github.com/richonguzman/LoRa_APRS_iGate/issues/443 is still present too: the same cleanPath() only
removes one occurrence per term, so when a consumed WIDE2-1* survives, the
substitution lands right in front of the leftover asterisk. I could not capture it
on this branch (the stations producing those paths are too far for my indoor test
antenna) but it is the same code path as in #443https://github.com/richonguzman/LoRa_APRS_iGate/issues/443.
Both would be solved by doing in the WIDE2- branch what processMode3Path()
already does: find an exact comma-delimited token, substitute at that
index rather than with replace(), and leave the other asterisks untouched.
Heads-up: the digi mode numbers changed meaning
In this branch:
So an existing installation configured with mode = 3 (WIDE1 + WIDE2) will
silently become "only repeat if my callsign is explicitly in the path" after the
update ― it will stop digipeating WIDEn-N altogether, with nothing in the logs
to explain why.
Worth adding a migration when loading an older config, or at least a warning in
the release notes.
73
―
Reply to this email directly, view it on GitHubhttps://github.com/richonguzman/LoRa_APRS_iGate/issues/443?email_source=notifications&email_token=AQWBUGQPWZEUPQGCNIYC7DT5GDE77A5CNFSNUABFM5UWIORPF5TWS5BNNB2WEL2JONZXKZKDN5WW2ZLOOQXTKMBUG4ZTONJUGIY2M4TFMFZW63VHMNXW23LFNZ2KKZLWMVXHJLDGN5XXIZLSL5RWY2LDNM#issuecomment-5047375421, or unsubscribehttps://github.com/notifications/unsubscribe-auth/AQWBUGUUX33YO6ZOWU2TYA35GDE77AVCNFSNUABFKJSXA33TNF2G64TZHM2TSOJVGU4TGNJXHNEXG43VMU5TIOJUGI4TGNZZGYY2C5QC.
You are receiving this because you commented.Message ID: @.***>
Follow-up — I said "nothing else blocking", but the unit kept running and
caught
something worse. Two things, then a heads-up.
1. A fully spent path gets repeated AND re-inflated
Every token in the received path was already marked — there was no hop
left at
all, the frame should have been dropped. But
indexOf("WIDE2-2")matchedinside
the spent
WIDE2-2*, and the replace expanded it intoCALL*,WIDE2-1. Soa brand
new
WIDE2-1token now exists in a path that was exhausted, and the framegets a
second life instead of dying.
This is the same root cause as the asterisk problem: substring match
instead of an
exact comma-delimited token.
2. The previous digipeater's mark is erased on every single repeat
Not occasional — systematic:
F4MLV-10no longer appears as having handled the frame, so another digican
legitimately repeat it again. I have a capture from my network where
F4MLV-10ends up twice in the same path — that is what this leads to in practice.
You already have the right code
processMode3Path()does exactly what theWIDE2-branch needs: it walksthe
path token by token, compares
token == stationCallsign, checks thatearlier
tokens are marked, and refuses when already repeated.
Applying that same approach to the
WIDE2-branch — exact token,substitute at
its index, leave the other asterisks alone — fixes all three symptoms at
once
(the
**of #443, the erased marks, and the re-inflated spent path).Heads-up before release: the mode numbers changed meaning
WIDE1-1branch is nowdigiMode == 1 || 2(was2 || 3)WIDE2-nbranch is nowdigiMode == 2(was3)digiMode == 3is the new callsign-based digipeatingAn existing installation set to
mode = 3(WIDE1 + WIDE2) will silentlybecome
"only repeat if my callsign is explicitly in the path" after updating — it
stops
digipeating
WIDEn-Naltogether, with nothing in the logs to explain why.Worth a migration when loading an older config, or at least a line in the
release
notes. On a hilltop digipeater that one would be painful to diagnose
remotely.
73,
F4MLV
Le mer. 22 juil. 2026 à 16:58, Ricardo Guzman @.***> a
écrit :
Hi Moricef, can we return to this. Could you check this test on your side with V4.0.0 ?
Hi Ricardo,
Checked against the V4.0.0 tag — the behaviour cannot have changed, because src/digi_utils.cpp is byte-identical to what it was when this issue was opened:
$ git log --since=2026-07-21 origin/main -- src/digi_utils.cpp
(no commits)
$ git show V4.0.0:src/digi_utils.cpp | md5sum
0e0d912556639028d7de025410f14903
$ git show <last commit before 2026-07-21>:src/digi_utils.cpp | md5sum
0e0d912556639028d7de025410f14903
So both halves of the cause are still in place on V4.0.0:
src/digi_utils.cpp on V4.0.0 — cleanPath() still removes a single occurrence per term:
String cleanPath(String path) {
String terms[] = {"WIDE1*,", "WIDE2*,", "*"};
for (String term : terms) {
int index = path.indexOf(term);
if (index != -1) path.remove(index, term.length()); // one occurrence only
}
return path;
}
and the WIDE2-n branch (line 95) still substitutes by substring rather than by token:
} else if (tempPath.indexOf("WIDE2-") != -1 && digiMode == 2) {
tempPath = cleanPath(path);
if (tempPath.indexOf("WIDE2-1") != -1) {
tempPath.replace("WIDE2-1", stationCallsign + "*");
Exact trigger. I could not capture it on the air again — the stations producing those paths are out of reach of my indoor antenna — so I reproduced the two functions verbatim on the host. The ** appears when an asterisk sits anywhere earlier in the path and WIDE2-1 is itself already consumed:
in : F6DEV*,F6DEV-11,WIDE2-1*,WIDE3-3
out : F6DEV,F6DEV-11,F4MLV-10**,WIDE3-3 <-- malformed
That is byte-for-byte the tail of the frame reported in the issue. Step by step: cleanPath() finds the first * (the one on F6DEV) and removes it, leaving the one on WIDE2-1* in place; indexOf("WIDE2-1") then matches inside WIDE2-1*, and replace() inserts F4MLV-10* right in front of the surviving asterisk.
Note the second-order problem: WIDE2-1* is an already-used hop. It must not be consumed a second time at all — the malformed path is only the visible symptom.
Fix. Same approach processMode3Path() already takes: match an exact comma-delimited token and substitute at that index. No replace(), and the other asterisks are left alone.
static int pathTokenIndex(const String& path, const String& token) {
unsigned int start = 0;
while (start < path.length()) {
int end = path.indexOf(",", start);
if (end == -1) end = path.length();
if (path.substring(start, end) == token) return start;
start = end + 1;
}
return -1;
}
} else if (tempPath.indexOf("WIDE2-") != -1 && digiMode == 2) {
int idx = pathTokenIndex(tempPath, "WIDE2-1");
if (idx != -1) {
tempPath = tempPath.substring(0, idx) + stationCallsign + "" + tempPath.substring(idx + 7);
} else {
idx = pathTokenIndex(tempPath, "WIDE2-2");
if (idx == -1) return "";
tempPath = tempPath.substring(0, idx) + stationCallsign + ",WIDE2-1" + tempPath.substring(idx + 7);
}
}
Same three inputs through the patched code:
F6DEV*,F6DEV-11,WIDE2-1*,WIDE3-3 -> (hop already used, correct)
F6DEV*,F6DEV-11,WIDE2-1,WIDE3-3 -> F6DEV,F6DEV-11,F4MLV-10*,WIDE3-3
F6DEV*,F6DEV-11,WIDE2-2,WIDE3-3 -> F6DEV,F6DEV-11,F4MLV-10*,WIDE2-1,WIDE3-3
I have been running this on my own fork on F4MLV-10 (mountain-top digi, ~150 frames/h, mode 2). Confirmed on the air, e.g. F4MLV-7>APLRT1,WIDE1-1,WIDE2-1 relayed as F4MLV-7>APLRT1,F4MLV-10*,WIDE2-1 — WIDE1-1 consumed, WIDE2-1 left intact.
Happy to open a PR if that helps.
73
Hi Ricardo,
Checked on
4.0.1(2026-09-07):src/digi_utils.cppis byte-identical to V4.0.0 —git log V4.0.0..main -- src/digi_utils.cppreturns nothing — so the behaviour is unchanged.Since the trigger is hard to catch on the air, I wrapped it in a small host harness that compiles your actual
digi_utils.cppagainst stub headers. No firmware source is modified, no board needed:On current
main, 16 cases covering modes 1/2/3, cross-frequency and exhausted hops:All three failures sit in the
exhausted hopsblock:The
**is the cosmetic part. What matters more is thatWIDE2-1*— a hop that has already been consumed — gets used again for digipeating. The third case shows a separate effect of the same function:cleanPath()erased the asterisk onF6DEV, losing the record that it had digipeated the frame.The
Stringstub mirrorsWString.cppfrom arduino-esp32 where it matters —replace()replaces every occurrence,remove()only one — so the harness reproduces the real semantics rather than approximating them.With the token-based substitution the same 16 cases pass. Opened as #463, one file, +17/−6.
Happy to add the harness to the PR if you find it useful, or to leave the PR as the fix alone — whichever suits you.
73
completely added to the firmare with a few APRSPacketLib updates too