Reorg for mesh control-based version
This commit is contained in:
parent
bf3ddfb440
commit
7424416b58
7 changed files with 21437 additions and 35 deletions
17
README.md
17
README.md
|
|
@ -1,17 +0,0 @@
|
||||||
|
|
||||||
# LoRa Modulation Jam
|
|
||||||
|
|
||||||
Programmatically send packets between two stations across a set of LoRa radio configurations.
|
|
||||||
|
|
||||||
Given nodes at two different locations with computers attached via USB, run one of these commands within a few minutes of each other on each computer:
|
|
||||||
|
|
||||||
`python modjam.py run --this-station=A` and `python modjam.py run --this-station=B`
|
|
||||||
|
|
||||||
Each station will iterate through the full permutation of LoRa radio settings.
|
|
||||||
|
|
||||||
Use the parameters to change the set of settings used:
|
|
||||||
|
|
||||||
This will run for all BWs, but only test SF 7 and 11, coding rate 5, and power level 1.
|
|
||||||
```
|
|
||||||
python3 ./modjam.py run --this-station=A --spread-factor=7 --spread-factor=11 --coding-rate=5 --power=1
|
|
||||||
```
|
|
||||||
10630
reference/A.tsv
Normal file
10630
reference/A.tsv
Normal file
File diff suppressed because it is too large
Load diff
10606
reference/B.tsv
Normal file
10606
reference/B.tsv
Normal file
File diff suppressed because it is too large
Load diff
139
reference/concept.md
Normal file
139
reference/concept.md
Normal file
|
|
@ -0,0 +1,139 @@
|
||||||
|
|
||||||
|
# LoRa Modulation Jam
|
||||||
|
|
||||||
|
This is a project to measure packet delivery rates across different permutations of LoRa modulation settings. Two or more nodes operate as test stations are set up to send and receive packets at the same LoRa settings. Another node operates as the control station and sends commands to initiate the tests.
|
||||||
|
|
||||||
|
This test framework uses MeshCore since it creates realistic packet sizes, has a useful python interface, and does not require restarting the node to change radio settings.
|
||||||
|
|
||||||
|
|
||||||
|
## Test Stations
|
||||||
|
|
||||||
|
Python service running on a Raspberry Pi and interfacing with a MeshCore node using the `meshcore` python library over USB Serial.
|
||||||
|
|
||||||
|
The service runs on Pi boot and connects to the USB-attached node. It reads the config from `~/modjam-config.json` that looks something like this:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"this_station_name": "A",
|
||||||
|
"channel_name": "modjam",
|
||||||
|
"channel_psk": "1234abcdefg==",
|
||||||
|
"idle_heartbeat_min": 15,
|
||||||
|
"control_radio": {
|
||||||
|
"frequency_mhz": 915.125,
|
||||||
|
"bandwidth_khz": 500,
|
||||||
|
"spread_factor": 12,
|
||||||
|
"coding_rate": 8,
|
||||||
|
"tx_power_dbm": 22,
|
||||||
|
},
|
||||||
|
"data_radio": {
|
||||||
|
"frequency_mhz": 915.125,
|
||||||
|
"bandwidth_khz": 500,
|
||||||
|
"spread_factor": 7,
|
||||||
|
"coding_rate": 5,
|
||||||
|
"tx_power_dbm": 1,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Commands
|
||||||
|
|
||||||
|
The Test Stations listen for commands from the Control Station that come on the configured channel (channel_name, channel_psk, control_radio).
|
||||||
|
|
||||||
|
Commands follow this format: `<cmd>[:<station>]|<attr1:val1,val2,…>|<attr2:val1,val2,…>…`. They may be unicast addressed to a specific station or multicast to all stations. The commands take named arguments that are pipe-separated. Each consists of a name followed by a colon followed by a comma-separated list of values (there may be no comma if there is only one value). The arguments are not ordered.
|
||||||
|
|
||||||
|
#### `START`
|
||||||
|
|
||||||
|
The `START` command is not directed at a specific Test Station, it is multicast to all stations. It provides a test name, set of frequencies, set of bandwidths, set of spread factors, set of coding rates, and set of transmit powers that together provide all the permutations to be tested. The named test will run for all combinations of these radio parameters. The command also lists all the stations that should participate in that test.
|
||||||
|
|
||||||
|
`START|name:<testname>|f:<freq1>,<freq2>,…|bw:<62.5|125|250|500>,…|sf:<6,7,8,9,10,11,12>,…|cr:<4,5,6,7,8>,…|pow:<pow1>,<pow2>|stations:<a>,<b>,…|duration:<s>|padding:<s>|spacing:<s>|size:<bytes1>,<bytes2>,…|at:<n>`
|
||||||
|
|
||||||
|
- name (required): the string name of the test run, used to name the test file and reference the results later
|
||||||
|
- f: the float frequencies to test in MHz (default: 916.1)
|
||||||
|
- bw: a choice of bandwidths to test from the possible options 62.5, 125, 250, 500 (default: all of these)
|
||||||
|
- sf: a choice of spread factors to test from the options 7,8,9,10,11,12 (default: all of these)
|
||||||
|
- cr: a choice of coding rates to test from the options of 5,6,7,8 (default: all of these)
|
||||||
|
- pow: the int tx powers to test (default: 22,)
|
||||||
|
- stations: the string names of stations to participate (default: 'A','B')
|
||||||
|
- duration: the int seconds of each test case (default: 300)
|
||||||
|
- padding: the int seconds between each test case (default: 60)
|
||||||
|
- spacing: the int seconds between each test transmission (default: 2)
|
||||||
|
- size: the list of int payload sizes to test in bytes (default: 40,)
|
||||||
|
- at: the int %nth minute of the hour to begin the test (default: 5)
|
||||||
|
|
||||||
|
These arguments have defaults so not all need to be specified for the `START` command to be valid. Only the `name` is required.
|
||||||
|
|
||||||
|
A complete example command:
|
||||||
|
`START|name:ab123|f:915.1|bw:62.5,125,250,500|sf:6,7,8,9,10,11,12|cr:4,5,6,7,8|pow:10,22|stations:A,B,C|padding:60|duration:600|spacing:2|at:5|size:40`
|
||||||
|
|
||||||
|
#### `STOP`
|
||||||
|
|
||||||
|
The `STOP` command is a multicast command to all Test Stations. When they receive this, if they are `RUNNING`, they switch to an `IDLE` state and stop their test protocol wherever they were in it.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### States
|
||||||
|
|
||||||
|
The Test Station has two states: `IDLE`, `RUNNING`:
|
||||||
|
|
||||||
|
#### IDLE
|
||||||
|
|
||||||
|
After reading the config, the process configures the attached node with a channel named the `encryption_name` and using the `encryption_psk` (if such a channel is not already configured), and sets the radio to the `control_radio` settings, then enters the `IDLE` state.
|
||||||
|
|
||||||
|
While in the `IDLE` state the node sends heartbeat messages every `idle_heartbeat_min` on the configured channel: `<now>|<this_station_name>|<state>|<noise_floor>` where `now` is the unix timestamp, `this_station_name` is from the config file, `state` is the current process state, and `noise_floor` is the noise floor as measured by the attached node.
|
||||||
|
|
||||||
|
An example heartbeat message: `1778184205|B|IDLE|-102`.
|
||||||
|
|
||||||
|
The service stays in this `IDLE` state, sending regular heartbeats, until it receives a `START` command.
|
||||||
|
|
||||||
|
#### RUNNING
|
||||||
|
|
||||||
|
The control node sends a `START` command on the `control_radio` settings. This is received by all Test Stations in `IDLE` state. They build a cuesheet based on the specified test parameters. Then they enter a `RUNNING` state, wait until the time specified by the `START` command, then proceed to iterate through the cuesheet-defined permutations and taking turns transmitting or listening.
|
||||||
|
|
||||||
|
While `RUNNING`, the process iterates through the cuesheet based on the defined timing. If the specified `sender` matches its own station name, it executes transmissions. Otherwise, it waits. Every packet it sends or receives gets logged to a tab-separated `<testname>.tsv` file.
|
||||||
|
|
||||||
|
In between each test case, during the `padding` interval specified by the start command, the node switches back to the `control_radio` setting so it can emit status information and listen for other commands. `10` seconds _before_ the start time of the test case it sends a "next" message: `<now_ts>|<this_station_name>|next:<f>/<bw>/<sf>/<cr>/<pow>/<size>`. `10` seconds _after_ the end of the test case it sends a "done" message: `<now_ts>|<this_station_name>|done:<f>/<bw>/<sf>/<cr>/<pow>/<size> <n_sent>/<n_rcvd>`.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## Example
|
||||||
|
|
||||||
|
Two Test Stations start up, with nearly identical configurations but one is named `'A'` and one is named `'B'`. They tune to the `control_radio` settings and enter `IDLE` state.
|
||||||
|
|
||||||
|
The Control Station sends a `START|test1|f:910.2|bw:62.5,500|sf:8,9,10|cr:5` command at clock time 10:07:23. Test Stations A and B hear this enter `RUNNING` states. At time 10:10:10, stations A and B send "next" messages:
|
||||||
|
|
||||||
|
- `1778148610|A|next:910.2/62.5/8/5/22/40`
|
||||||
|
- `1778148610|B|next:910.2/62.5/8/5/22/40`
|
||||||
|
|
||||||
|
At 10:10:20 stations A and B set their radios to frequency 910.2 MHz, bandwidth 62.5 kHz, spread factor 8, coding rate 5, tx power 22.
|
||||||
|
|
||||||
|
At 10:10:30 station A begins a series of transmissions spaced 2 seconds apart:
|
||||||
|
|
||||||
|
- 1778148610.2168489,30.12679696083069,1|551097625881289327832
|
||||||
|
- 1778148612.738883,32.648837089538574,2|738453771722320023996
|
||||||
|
- 1778148615.468757,35.378714084625244,3|7910624607885536300710
|
||||||
|
- …
|
||||||
|
|
||||||
|
Station A logs every message it queues for the radio and every message the radio reports it transmitted. Station B logs every message received.
|
||||||
|
|
||||||
|
At 10:14:30 station A stops its test transmissions.
|
||||||
|
|
||||||
|
At 10:15:30 station B starts a similar series of transmissions. It logs every message it queues and transmits. Station A logs every message it receives.
|
||||||
|
|
||||||
|
At 10:19:30 station B stops its test transmissions.
|
||||||
|
|
||||||
|
At 10:19:40 stations A and B tune to the control_radio settings
|
||||||
|
|
||||||
|
At 10:19:50 stations A and B send "done" messages:
|
||||||
|
|
||||||
|
- 1778149190|A|done:910.2/62.5/8/5/22/40 120/110
|
||||||
|
- 1778149190|B|done:910.2/62.5/8/5/22/40 120/114
|
||||||
|
|
||||||
|
At 10:20:10 stations A and B send "next" messages, at 10:20:20 tune to the next test radio settings (910.2 MHz, 62.5 BW, 9 SF, 5 CR, 22 power), and repeat the process of sending and listening.
|
||||||
|
|
||||||
|
They continue doing this until they have performed tests in each direction for all permutations, at 11:10:00, retune to the control radio settings and enter IDLE state.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -13,7 +13,7 @@ import sys
|
||||||
|
|
||||||
|
|
||||||
logfile = None
|
logfile = None
|
||||||
interface: SerialInterface | None = None
|
interface: SerialInterface | TCPInterface | None = None
|
||||||
|
|
||||||
|
|
||||||
type Bandwidth = Literal[62,125,250,500]
|
type Bandwidth = Literal[62,125,250,500]
|
||||||
|
|
@ -186,10 +186,12 @@ def log (**parts):
|
||||||
logfile.write(json.dumps({**parts, 'ts': ts}) + '\n')
|
logfile.write(json.dumps({**parts, 'ts': ts}) + '\n')
|
||||||
|
|
||||||
|
|
||||||
def configureRadio (conf: RadioConfig):
|
async def configureRadio (conf: RadioConfig):
|
||||||
if not interface:
|
if not interface:
|
||||||
raise Exception('No interface connected')
|
raise Exception('No interface connected')
|
||||||
|
await reconnectRadio()
|
||||||
node = interface.getNode('^all')
|
node = interface.getNode('^all')
|
||||||
|
print(node)
|
||||||
changed = False
|
changed = False
|
||||||
|
|
||||||
config_ok_to_mqtt = False
|
config_ok_to_mqtt = False
|
||||||
|
|
@ -245,25 +247,34 @@ def configureRadio (conf: RadioConfig):
|
||||||
node.writeConfig('lora')
|
node.writeConfig('lora')
|
||||||
node.commitSettingsTransaction()
|
node.commitSettingsTransaction()
|
||||||
print('changes applied, waiting for reboot')
|
print('changes applied, waiting for reboot')
|
||||||
|
await asyncio.sleep(10)
|
||||||
return changed
|
return changed
|
||||||
|
|
||||||
|
# raise MeshInterface.MeshInterfaceError(
|
||||||
def reconnectRadio ():
|
# "Timed out waiting for connection completion"
|
||||||
|
# )
|
||||||
|
# WARNING file:stream_interface.py __reader line:216 Meshtastic serial port disconnected, disconnecting... device reports readiness to read but returned no data (device disconnected or multiple access on port?)
|
||||||
|
async def reconnectRadio ():
|
||||||
global interface
|
global interface
|
||||||
if not interface:
|
if not interface:
|
||||||
raise Exception('No interface connected')
|
raise Exception('No interface connected')
|
||||||
reconnected = None
|
print('reconnectRadio disconnect', interface)
|
||||||
|
if interface.isConnected:
|
||||||
|
interface.close()
|
||||||
|
await asyncio.sleep(1)
|
||||||
|
print('reconnectRadio connect', interface)
|
||||||
|
if isinstance(interface, TCPInterface):
|
||||||
|
interface = TCPInterface(interface.hostname, noNodes=True)
|
||||||
|
elif isinstance(interface, SerialInterface):
|
||||||
|
interface = SerialInterface(interface.devPath, noNodes=True)
|
||||||
|
await asyncio.sleep(1)
|
||||||
reconnected_node = None
|
reconnected_node = None
|
||||||
while not reconnected and not reconnected_node:
|
while not reconnected_node:
|
||||||
try:
|
print('reconnectRadio getNode', interface.isConnected)
|
||||||
reconnected = SerialInterface(interface.devPath, noNodes=True)
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
sleep(1)
|
|
||||||
if reconnected:
|
|
||||||
reconnected_node = interface.getNode('^all')
|
reconnected_node = interface.getNode('^all')
|
||||||
sleep(2)
|
print(reconnected_node)
|
||||||
interface = reconnected
|
await asyncio.sleep(2)
|
||||||
|
# interface = reconnected
|
||||||
|
|
||||||
|
|
||||||
def onReceiveText (packet, interface):
|
def onReceiveText (packet, interface):
|
||||||
|
|
@ -280,6 +291,7 @@ txed = dict()
|
||||||
active_tx_id = None
|
active_tx_id = None
|
||||||
active_tx_ms = None
|
active_tx_ms = None
|
||||||
def onStatus (line: str):
|
def onStatus (line: str):
|
||||||
|
# print(line)
|
||||||
global active_tx_id
|
global active_tx_id
|
||||||
global active_tx_ms
|
global active_tx_ms
|
||||||
if 'Started Tx' in line:
|
if 'Started Tx' in line:
|
||||||
|
|
@ -305,7 +317,7 @@ async def waitForTx (packet_id: int):
|
||||||
timed_out = False
|
timed_out = False
|
||||||
while not packet_id in txed and not timed_out:
|
while not packet_id in txed and not timed_out:
|
||||||
await asyncio.sleep(0.1)
|
await asyncio.sleep(0.1)
|
||||||
if time() - s > 45:
|
if time() - s > 30:
|
||||||
timed_out = True
|
timed_out = True
|
||||||
ms = txed.pop(packet_id, None) # pop to avoid a memory leak
|
ms = txed.pop(packet_id, None) # pop to avoid a memory leak
|
||||||
if timed_out:
|
if timed_out:
|
||||||
|
|
@ -339,9 +351,9 @@ async def runCues (cuesheet: Cuesheet, run_config: RunConfig):
|
||||||
print(scenario)
|
print(scenario)
|
||||||
print('configure radio')
|
print('configure radio')
|
||||||
scenario_prefix = f'{scenario['freq']},{scenario['bw']},{scenario['sf']},{scenario['cr']},{scenario['pow']}'
|
scenario_prefix = f'{scenario['freq']},{scenario['bw']},{scenario['sf']},{scenario['cr']},{scenario['pow']}'
|
||||||
did_change = configureRadio(scenario)
|
did_change = await configureRadio(scenario)
|
||||||
if did_change:
|
if did_change:
|
||||||
reconnectRadio()
|
await reconnectRadio()
|
||||||
if scenario['sender'] == run_config['this_station']:
|
if scenario['sender'] == run_config['this_station']:
|
||||||
print('waiting for start')
|
print('waiting for start')
|
||||||
while t < scenario['start']: # Give other stations time to get ready
|
while t < scenario['start']: # Give other stations time to get ready
|
||||||
31
reference/preset-compare-meshcore.py
Normal file
31
reference/preset-compare-meshcore.py
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
import asyncio
|
||||||
|
from meshcore import MeshCore, EventType
|
||||||
|
|
||||||
|
PRESETS = [
|
||||||
|
(250,11,5), # LongFast
|
||||||
|
(250,10,5), # MediumSlow
|
||||||
|
(250,9,5), # MediumFast
|
||||||
|
(250,8,5), # ShortSlow
|
||||||
|
(250,7,5), # ShortFast
|
||||||
|
(125,8,5), # MeshOregon
|
||||||
|
(62,7,5), # MC Narrow
|
||||||
|
(500,11,8), # LongTurbo
|
||||||
|
(500,7,5), # ShortTurbo
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
async def main():
|
||||||
|
meshcore = await MeshCore.create_serial("/dev/tty.usbmodem1301")
|
||||||
|
print(meshcore)
|
||||||
|
print(await meshcore.commands.set_tx_power(1))
|
||||||
|
await asyncio.sleep(1)
|
||||||
|
for (bw,sf,cr) in PRESETS:
|
||||||
|
await meshcore.commands.set_radio(912.4, bw, sf, cr)
|
||||||
|
result = await meshcore.commands.send_chan_msg(0, "The quick brown fox jumped over the lazy dog")
|
||||||
|
print(result)
|
||||||
|
await asyncio.sleep(1)
|
||||||
|
|
||||||
|
|
||||||
|
await meshcore.disconnect()
|
||||||
|
|
||||||
|
asyncio.run(main())
|
||||||
1
requirements.txt
Normal file
1
requirements.txt
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
meshcore==2.3.7
|
||||||
Loading…
Add table
Reference in a new issue