Skip to content

Add Japan LoRa LBT compliance (ARIB STD-T108) with runtime frequency detection#2218

Open
jirogit wants to merge 9 commits into
meshcore-dev:devfrom
jirogit:feature/jp-lbt
Open

Add Japan LoRa LBT compliance (ARIB STD-T108) with runtime frequency detection#2218
jirogit wants to merge 9 commits into
meshcore-dev:devfrom
jirogit:feature/jp-lbt

Conversation

@jirogit

@jirogit jirogit commented Apr 1, 2026

Copy link
Copy Markdown
Contributor

Closes #2079

Summary

This PR implements Listen-Before-Talk (LBT) for Japan LoRa operation, as required by ARIB STD-T108 / Giteki (技適) certification. Activation is automatic based on operating frequency — no build flags required.

Changes

Radio accessor methods (RadioLibWrappers.h/cpp, Wrapper classes):

  • getCodingRate() — returns current LoRa coding rate at runtime
  • getFreqMHz() — returns operating frequency in MHz
  • getMaxTextLen() — returns dynamic DM message length limit based on CR
  • getMaxGroupTextLen() — returns dynamic channel message length limit based on CR
  • isJapanMode() — returns true when operating on JP LoRa frequencies (920.800 / 921.000 / 921.200 MHz)

Japan LBT (BaseChatMesh.cpp, RadioLibWrappers.cpp, Dispatcher.h):

  • 5ms continuous RSSI sensing before TX (−80dBm absolute threshold, ARIB STD-T108 §4.4)
  • Exponential backoff when channel busy (base 2000ms, max 16000ms)
  • Small jitter on channel-free detection (0–500ms) to desynchronize simultaneous TX
  • 50ms post-TX wait
  • Override getCADFailMaxDuration() to UINT32_MAX in Japan mode — prevents forced TX during LBT backoff (Dispatcher default is 4s, shorter than max backoff of 16s)
  • Dynamic message length limits per coding rate, derived from ARIB STD-T108 4-second maximum TX time limit, verified by measuring getTimeOnAir() on RAK WisMesh Tag (SF12/BW125):
CR DM limit airtime Channel limit airtime
4/5 64 bytes 3874ms 64 bytes 3710ms
4/6 48 bytes 3874ms 48 bytes 3678ms
4/7 32 bytes 3678ms 39 bytes 3907ms
4/8 24 bytes 3547ms 29 bytes 3809ms

DM and channel packets differ by 1 byte overhead, warranting separate limits for CR4/7 and CR4/8. Non-JP returns default bytes unchanged.

Build:

  • Fix build for non-FreeRTOS and non-SX1262 platforms (YIELD_TASK() macro)

Note on commit history

Early commits include CAD-based channel detection based on weebl2000's work (PR #1727). This was subsequently replaced with RSSI-based LBT, as ARIB STD-T108 requires energy detection, not LoRa preamble detection.

As of the current implementation (rebased onto upstream/dev which includes merged #1727), RSSI LBT and CAD operate independently. JP nodes run RSSI sensing first (ARIB requirement), then fall through to CAD if _cad_enabled.
Non-JP nodes use the existing _threshold and CAD path unchanged.
The commit history reflects the earlier exploration path before #1727 was merged.

Testing

Devices: RAK WisMesh Tag (SX1262), Wio Tracker L1 Pro (SX1262)
Settings: SF12 / BW125 / CR4-8 (JP standard)

  • 16-byte simultaneous DM: 3/3 success

  • 48-byte simultaneous DM: 3/3 success

  • Channel messages: all CR rates confirmed (CR4/5–CR4/8).
    Effective text limit is (channel_limit - 2 - sender_name_length) bytes,
    where 2 bytes are consumed by the ": " separator.
    Example (8-byte sender name): CR4/5=54B, CR4/6=38B, CR4/7=29B, CR4/8=19B

Known limitations

  • In Japan mode, the UI does not reflect the reduced message length limits. Channel messages longer than the CR-dependent limit will be silently truncated at send time. DM messages exceeding the limit will fail to send. A UI-level fix requires exposing getMaxTextLen()/getMaxGroupTextLen() to the UI layer and is left for a follow-up.
  • TX power limit (≤13dBm, ARIB STD-T108) is not enforced — relies on user configuration.
  • If the ambient noise floor exceeds −80 dBm, transmission will be blocked indefinitely. No forced TX occurs — this is intentional, as ARIB STD-T108 prohibits transmission while the channel is busy. Users can monitor the noise floor via the MeshCore companion app: tap the three-dot menu (⋮) in the top right corner → Tools → Noise Floor.

@jirogit jirogit marked this pull request as ready for review April 1, 2026 09:23
Comment thread src/helpers/radiolib/RadioLibWrappers.h Outdated
virtual uint8_t getCodingRate() const { return 8; } // default CR4/8, override in subclass
virtual float getFreqMHz() const { return 0.0f; } // default unknown, override in subclass
//
bool isJapanMode() const {

@4np 4np Apr 2, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The function name doesn't match the frequencies being checked. 920.8, 921.0, and 921.2 MHz are the default channels for AS923-2, which is the plan for Indonesia and Vietnam, not Japan. Japan uses AS923-1 with default channels at 923.2 and 923.4 MHz. This means the function will return false for most Japanese devices and true for Indonesian/Vietnamese ones, a false positive for an entirely different region. Suggest renaming to isAS923_2Mode() or correcting the frequencies to 923.2/923.4 MHz depending on the intended behavior.

@jirogit jirogit Apr 2, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The function name doesn't match the frequencies being checked. 920.8, 921.0, and 921.2 MHz are the default channels for AS923-2, which is the plan for Indonesia and Vietnam, not Japan. Japan uses AS923-1 with default channels at 923.2 and 923.4 MHz. This means the function will return false for most Japanese devices and true for Indonesian/Vietnamese ones, a false positive for an entirely different region. Suggest renaming to isAS923_2Mode() or correcting the frequencies to 923.2/923.4 MHz depending on the intended behavior.

Thank you for the review. You are correct that 920.8/921.0/921.2 MHz overlap with AS923-2 (Indonesia/Vietnam in LoRaWAN terms). However, these frequencies are legal under Japanese radio law (ARIB STD-T108, 920.6–928.0 MHz) and are within the band permitted for use in Japan.

Regarding the false positive concern for Indonesian/Vietnamese users: MeshCore does not currently have a defined default frequency for Indonesia. Vietnam's current default is 920.250 MHz, which does not match any of the three frequencies checked by isJapanMode() (920.800 / 921.000 / 921.200 MHz). In practice, the risk of unintended activation on non-Japanese nodes is low.

That said, the concern is valid in principle and points toward a broader architectural question: as MeshCore expands into regions with different regulatory requirements — duty cycle limits in the EU, LBT variants in other Asian countries, TX power caps, airtime limits — a more general regional compliance framework would be valuable.

A future "set radio_law / get radio_law" command (distinct from the existing "set region") could allow users to explicitly select their regulatory domain (e.g. JP, EU, FCC), enabling the firmware to apply the appropriate constraints automatically.

The current frequency-based isJapanMode() is intentionally minimal and pragmatic given the near-zero Japanese node count today, but we recognize it is not a scalable long-term solution. We are open to adding such a mechanism if the maintainer prefers a more structured approach, and would welcome guidance on the preferred direction for regional compliance in MeshCore going forward.

@jirogit

jirogit commented Apr 9, 2026

Copy link
Copy Markdown
Contributor Author

Related RFC Discussion: #2285

@dreirund

Copy link
Copy Markdown
Contributor

Question from #1727 (comment):

How does it differ from the current int.thresh implementation?

Regards!

@jirogit

jirogit commented May 1, 2026

Copy link
Copy Markdown
Contributor Author

How does it differ from the current int.thresh implementation?

Thank you very much for your question!

This PR does not use int.thresh.

The int.thresh-based mechanism uses a relative noise floor threshold (noise_floor + threshold) as a voluntary interference avoidance heuristic (currently hardcoded to 0 in Companion firmware — see #2051).

ARIB STD-T108 LBT requires an absolute −80 dBm threshold, a minimum 5ms continuous RSSI sensing window before each TX, and a defined backoff protocol — these are legal requirements, not preferences. The getCADFailMaxDuration() override to UINT32_MAX is also needed to prevent the Dispatcher's default 4s forced-TX timeout from firing during a legitimate LBT backoff.

So the two mechanisms are complementary: int.thresh applies generally as a configurable courtesy; JP LBT applies when on Japan frequencies as a non-negotiable legal obligation with specified parameters.

@jirogit

jirogit commented May 2, 2026

Copy link
Copy Markdown
Contributor Author

One more note on the jitter mechanism in this PR, in context of the txdelay discussion in #1727:

The 0–500ms random jitter added on channel-free detection serves a similar purpose to txdelay, but targets a different point in the transmission chain:

txdelay JP LBT jitter
Purpose Stagger repeater relay TX after receiving a packet Stagger any TX attempt the moment a node detects the channel as free
Target Repeater relay behavior All nodes, every TX attempt
Value Proportional to airtime (default factor 0.5) Random (0–500ms)
Configuration set txdelay CLI Internal to JP LBT

Both mechanisms address the same root problem — synchronized transmission from multiple nodes — at different points in the chain. txdelay acts on the repeater receive side (after a packet arrives); JP LBT jitter acts on the transmit side (just before each TX attempt by any node, when the channel is free).

syssi's video in #1727 clearly demonstrates what happens without staggering: with txdelay 0, all repeaters synchronize and collide simultaneously. The same collision pattern occurs at TX origination without jitter.

@jirogit

jirogit commented May 3, 2026

Copy link
Copy Markdown
Contributor Author

Update: airtime-scaled jitter (f2aad64)

Replaced fixed random(0, 500) jitter with airtime-scaled random(0, airtime_ms / JP_LBT_JITTER_DIVISOR) (divisor=32).

Jitter upper bound by setting:
SF12/BW125/CR4/8 (JP standard): ~245ms (was 500ms)
SF7/BW62.5/CR4/5 (USA/Canada preset): ~12ms

Tested divisors 16 and 32 at SF12/BW125/CR4/8 — both 3/3 success with no measurable difference in delivery time. Divisor 32 adopted as it halves the unnecessary wait without observable downside.
The divisor is a named constant (JP_LBT_JITTER_DIVISOR) to make future tuning straightforward.

@jirogit

jirogit commented May 5, 2026

Copy link
Copy Markdown
Contributor Author

I've renamed isJapanMode() to isAS923_1_JP() to address @4np's concern about AS923-2 frequency overlap.
Rationale: LoRaMac-node uses the same sub-plan naming (AS923_1_JP) to distinguish Japan's ARIB STD-T108 LBT requirement from AS923 in general — AS923 itself has no LBT mandate; LBT here is Japan-specific. isAS923_1_JP() makes that explicit.

@jirogit

jirogit commented May 5, 2026

Copy link
Copy Markdown
Contributor Author

Added commit 4e09f6a: JP LBT txdelay suppression.

Problem

In JP LBT mode (SF12/BW125/CR4/8), the standard txdelay formula (random(0, 5 × airtime × factor)) produces delays up to ~8.9s with default tx_delay_factor=0.5. Urban Japan (Akihabara being the design target) has a high noise floor that makes SF7/BW125 ("LongFast") impractical — SF12/BW125 is required for reliable communication. At SF12, airtime is ~29× longer than SF7, which makes the standard txdelay formula produce multi-second delays. In this environment, LBT backoff is already doing the heavy lifting for channel access; txdelay on top is pure latency penalty.

Why txdelay has limited value in JP LBT

The purpose of txdelay is to stagger retransmit timing across multiple repeaters to reduce collisions. In JP LBT mode, this role is already covered by two mechanisms:

  • LBT backoff: exponential backoff (1–16s) when RSSI > −80 dBm — handles environmental noise and active transmissions
  • Jitter (added in f2aad64): random(0, airtime/32) applied after a clean LBT sense — handles the case where multiple nodes clear simultaneously

With very few MeshCore nodes currently deployed in Japan, repeater-to-repeater collision is not a practical concern.

Fix

When isAS923_1_JP() is true, getRetransmitDelay() and getDirectRetransmitDelay() now return random(0, jitter_max) instead of the full txdelay window — matching the jitter scale used in isChannelActive():

SF12/BW125/CR4/8:  jitter_max ≈ 111ms
  before: txdelay max ≈ 8,900ms, average ≈ 4,400ms
  after:  txdelay max ≈   111ms, average ≈    56ms

Collision probability analysis (4 simultaneous repeaters, 1ms detection threshold):

  • jitter alone: ~10.4% chance of true collision
  • jitter + txdelay at jitter-scale: ~7.0% (33% improvement)
  • average added latency: 56ms vs 4,400ms

This scales naturally as CR decreases over time (CR4/8 → CR4/5): jitter_max shrinks with airtime, so txdelay tracks it automatically without any config change.

JP_LBT_JITTER_DIVISOR is promoted from a local static const in RadioLibWrappers.cpp to a public static constexpr in RadioLibWrappers.h so the examples can reference it directly.

Applied to simple_repeater, simple_room_server, and simple_sensor. Also aligns simple_sensor's non-JP path to use nextInt(0, 5*t+1) (was nextInt(0, 6)*t).

jirogit added 8 commits June 22, 2026 12:34
Add getCodingRate() and getFreqMHz() virtual methods to RadioLibWrapper
base class, enabling subclasses to report their operating frequency.
isJapanMode() detects JP 920MHz band (CH25-27) for ARIB STD-T108 compliance.
getMaxTextLen() and getMaxGroupTextLen() enforce 4-second airtime limits
per SF12/BW125 measurements.
In JP 920MHz band (CH25-27): 5ms continuous RSSI sensing at -80dBm
absolute threshold, exponential backoff (2000-16000ms) on busy, jitter
(0-500ms) on free, then falls through to CAD if enabled.
Non-JP path: existing relative-RSSI threshold + CAD unchanged.
Add YIELD_TASK() macro and _busy_count field for backoff tracking.
Add isJapanMode() virtual to mesh::Radio (default false) so Dispatcher
can query frequency context without depending on RadioLibWrapper.
Dispatcher::getCADFailMaxDuration() returns UINT32_MAX for JP nodes,
eliminating the 4-second forced-TX that would violate ARIB STD-T108.
Non-JP nodes retain the original 4-second safety timeout unchanged.
getCodingRate() returns codingRate+4 (RadioLib stores 1-4 internally,
expose as CR4/5-8 matching the rest of the codebase).
CustomLR1110Wrapper overrides both getCodingRate() and getFreqMHz() so
isJapanMode() and getMaxTextLen() work correctly on T1000-E.
Enables isJapanMode() and getMaxTextLen() to work on SX1262-based targets
(WisMesh Tag, T-Echo Lite, T114, XIAO nRF52, etc.).
codingRate accessed as SX1262 base class member +4 (same pattern as LR1110).
Add getMaxTextLen() and getMaxGroupTextLen() virtual to mesh::Radio
(default 160 bytes). BaseChatMesh uses these instead of the hardcoded
MAX_TEXT_LEN macro for limit checks in composeMsgPacket(),
sendCommandData(), and sendGroupMessage(). Stack buffers remain sized
to MAX_TEXT_LEN as a safe upper bound.
@jirogit

jirogit commented Jun 23, 2026

Copy link
Copy Markdown
Contributor Author

Rebased onto upstream/dev (includes #1727)

This branch has been completely rebuilt on top of the current upstream/dev (which includes the merged CAD LBT from #1727).

The previous history had #1727's CAD commits mixed in, causing conflicts across RadioLibWrappers.cpp/.h. Rather than resolving those conflict-by-conflict, I created a clean branch from upstream/dev and re-implemented the JP LBT changes on top.

isAS923_1_JP() LBT and CAD now operate independently:

JP mode: isAS923_1_JP() is active when frequency is set to 920.8 / 921.0 / 921.2 MHz, CAD optional — runs after RSSI clear if set cad on

Non-JP: isAS923_1_JP() not active, CAD optional, existing behavior unchanged

JP nodes run RSSI sensing first (ARIB requirement), then fall through to CAD if _cad_enabled. No existing behavior changes for non-JP nodes.

Also recovered two orphaned commits (f2aad64, 4e09f6a) that were not on any branch:

Airtime-scaled jitter (random(0, airtime/32)) instead of fixed 500ms
txdelay suppressed to jitter-scale in JP mode to avoid adding ~4400ms latency on top of LBT backoff

Hardware tested: RAK WisMesh Tag (SX1262) + LilyGo T1000-E (LR1110), companion firmware, SF12/BW125/CR4/8 at 920.8 MHz. DM and group messages at maximum text length delivered successfully.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants