Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/verb-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -1060,7 +1060,7 @@ Put one node down at a position, which is how a repeater a feed never carried ge
| `height_m` | number | optional | antenna height above ground in metres; absent is 10 |
| `tx_dbm` | number | optional | transmit power in decibel-milliwatts; absent is 22 |

**Answers** `placed`, `kind`, `regions`, `board`, `nodes`. `regions` and the firmware build are inherited from the neighbours rather than left empty: a node holding a region its neighbours do not is as silent as one holding none. `nodes` is the whole network's count afterwards. The links are not in the answer, because measuring them is a worker's job and this returns before it.
**Answers** `placed`, `kind`, `regions`, `board`, `nodes`. `regions` and the firmware build are inherited from the neighbours rather than left empty: a node holding a region its neighbours do not is as silent as one holding none. So is the radio: the modem every node already shares, or EU/UK (Narrow) where they do not agree, since a node on a channel nobody else uses hears nobody. `nodes` is the whole network's count afterwards. The links are not in the answer, because measuring them is a worker's job and this returns before it.

**Example** - put a repeater on the hill and see what it wins

Expand Down
2 changes: 1 addition & 1 deletion docs/verbs.json
Original file line number Diff line number Diff line change
Expand Up @@ -3649,7 +3649,7 @@
"board",
"nodes"
],
"answers": "`regions` and the firmware build are inherited from the neighbours rather than left empty: a node holding a region its neighbours do not is as silent as one holding none. `nodes` is the whole network's count afterwards. The links are not in the answer, because measuring them is a worker's job and this returns before it.",
"answers": "`regions` and the firmware build are inherited from the neighbours rather than left empty: a node holding a region its neighbours do not is as silent as one holding none. So is the radio: the modem every node already shares, or EU/UK (Narrow) where they do not agree, since a node on a channel nobody else uses hears nobody. `nodes` is the whole network's count afterwards. The links are not in the answer, because measuring them is a worker's job and this returns before it.",
"example": {
"params": {
"board": "Heltec_v3",
Expand Down
41 changes: 35 additions & 6 deletions internal/app/session/presetplace.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,11 @@ func registerPresetsAndPlace(st *state.Store, s *Sim) {
if only != "" && s.nodes[i].Name != only {
continue
}
s.nodes[i].Radio = scenario.RadioConfig{
CentreHz: preset.FreqMHz * 1e6,
BandwidthHz: preset.BwKHz * 1e3,
SpreadFactor: preset.SF,
CodingRate: preset.CR,
}
// The preset's own conversion. The table holds MeshCore's
// denominator (8 for 4/8) and a node's radio holds it minus four;
// copying the table across made every preset 4/12, half again the
// airtime, and a companion told CR 12 refuses it.
s.nodes[i].Radio = preset.Config()
n++
}
if n > 0 && len(s.nodes) > 0 {
Expand Down Expand Up @@ -73,6 +72,7 @@ func registerPresetsAndPlace(st *state.Store, s *Sim) {
}
node.HeightAGLm = 10
node.TxPowerDBm = 22
node.Radio = placedRadio(s.nodes)
if v, ok := namedNum(p, "height_m"); ok {
node.HeightAGLm = v
}
Expand Down Expand Up @@ -191,3 +191,32 @@ func regionsOfNeighbours(nodes []scenario.Node, lat, lon float64) []string {
}
return out
}

// placedRadio is the modem a placed node joins on: the one every node already
// here shares, or the documented default where they do not agree or there are
// none.
//
// Left empty, a placed node fell back to the engine's own figures - SF10 at
// 250 kHz - while its firmware booted on its build's SF8 at 62.5 kHz, so on any
// narrow mesh it was on a channel nothing else used and never heard anybody,
// with nothing to say why.
func placedRadio(nodes []scenario.Node) scenario.RadioConfig {
var shared *scenario.RadioConfig
for i := range nodes {
r := nodes[i].Radio
if r.CentreHz == 0 {
continue
}
if shared == nil {
shared = &r
continue
}
if *shared != r {
return scenario.DefaultRadio()
}
}
if shared == nil {
return scenario.DefaultRadio()
}
return *shared
}
2 changes: 1 addition & 1 deletion internal/app/session/presetplace.verbs.json
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@
"board",
"nodes"
],
"answers": "`regions` and the firmware build are inherited from the neighbours rather than left empty: a node holding a region its neighbours do not is as silent as one holding none. `nodes` is the whole network's count afterwards. The links are not in the answer, because measuring them is a worker's job and this returns before it.",
"answers": "`regions` and the firmware build are inherited from the neighbours rather than left empty: a node holding a region its neighbours do not is as silent as one holding none. So is the radio: the modem every node already shares, or EU/UK (Narrow) where they do not agree, since a node on a channel nobody else uses hears nobody. `nodes` is the whole network's count afterwards. The links are not in the answer, because measuring them is a worker's job and this returns before it.",
"example": {
"params": {
"board": "Heltec_v3",
Expand Down
73 changes: 73 additions & 0 deletions internal/app/session/presetradio_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
package session

import (
"context"
"testing"

"github.com/MeshBench/meshbench/internal/app/state"
"github.com/MeshBench/meshbench/internal/world/scenario"
)

func presetSim(t *testing.T, nodes []scenario.Node) (*state.Store, *Sim, context.Context, func()) {
t.Helper()
st := state.New(10)
s := &Sim{gpuAsked: true}
Register(st, s)
ctx, cancel := context.WithCancel(context.Background())
go st.Run(ctx)
s.build(nodes, 869.618)
return st, s, ctx, cancel
}

// A preset puts a node on the preset's coding rate. The table holds MeshCore's
// denominator, and copying it straight into the node's radio made EU/UK
// (Narrow) 4/12.
func TestAPresetKeepsItsCodingRate(t *testing.T) {
st, s, ctx, cancel := presetSim(t, []scenario.Node{repeaterNode("Alpha")})
defer cancel()
if _, err := st.Do(ctx, "radio.preset", map[string]any{"preset": "EU/UK (Narrow)"}); err != nil {
t.Fatal(err)
}
got := s.nodes[0].Radio
want, _ := scenario.PresetByLabel("EU/UK (Narrow)")
if !want.Matches(got) {
t.Fatalf("after the preset the node's radio is %+v, which is not EU/UK (Narrow)", got)
}
if got.CodingRate != 4 {
t.Fatalf("coding rate field %d, want 4 (4/8)", got.CodingRate)
}
}

// A placed node joins on the modem the mesh already shares, rather than on
// the engine's fallback that nothing else is using.
func TestAPlacedNodeJoinsTheMeshItIsPlacedIn(t *testing.T) {
nl, _ := scenario.PresetByLabel("Netherlands")
a, b := repeaterNode("Alpha"), repeaterNode("Bravo")
a.Radio, b.Radio = nl.Config(), nl.Config()
st, s, ctx, cancel := presetSim(t, []scenario.Node{a, b})
defer cancel()
if _, err := st.Do(ctx, "nodes.place", map[string]any{
"name": "Charlie", "kind": "companion", "lat": 56.31, "lon": -3.21,
}); err != nil {
t.Fatal(err)
}
for _, n := range s.nodes {
if n.Name == "Charlie" && n.Radio != nl.Config() {
t.Fatalf("Charlie was placed on %+v, the mesh is on %+v", n.Radio, nl.Config())
}
}
}

// With nothing to join, or a mesh that does not agree with itself, a placed
// node gets the default the presets table documents.
func TestAPlacedNodeOnNoAgreedModemGetsTheDefault(t *testing.T) {
if got := placedRadio(nil); got != scenario.DefaultRadio() {
t.Fatalf("an empty mesh placed on %+v", got)
}
nl, _ := scenario.PresetByLabel("Netherlands")
uk, _ := scenario.PresetByLabel("EU/UK (Narrow)")
mixed := []scenario.Node{{Radio: nl.Config()}, {Radio: uk.Config()}}
if got := placedRadio(mixed); got != scenario.DefaultRadio() {
t.Fatalf("a mixed mesh placed on %+v", got)
}
}
4 changes: 2 additions & 2 deletions internal/world/scenario/presets.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ type RadioPreset struct {
FreqMHz float64
BwKHz float64
SF int
// CR is the denominator minus four, as MeshCore's CLI takes it: 5..8 on the
// wire means 4/5..4/8.
// CR is the coding rate's denominator, as MeshCore's CLI takes it: 5..8
// means 4/5..4/8. A node's RadioConfig holds it minus four; Config converts.
CR int
}

Expand Down
Loading