Skip to content

Commit 353321c

Browse files
committed
- adding unit test when having chained sets for failing and non-failing case.
- add time.sleep(0.001) in receive thread for idle receptions. This will balance processor time between main thread and receive thread - local node will get the noproto property directly from meshinterface to have consistent settings.
1 parent 5cc0dae commit 353321c

File tree

4 files changed

+86
-33
lines changed

4 files changed

+86
-33
lines changed

meshtastic/__main__.py

Lines changed: 30 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -193,8 +193,12 @@ def traverseConfig(config_root, config, interface_config) -> bool:
193193
return True
194194

195195

196-
def setPref(config, comp_name, raw_val) -> bool:
197-
"""Set a channel or preferences value"""
196+
def setPref(config, comp_name: str, raw_val) -> bool:
197+
"""Set a channel or preferences value
198+
config: LocalConfig, ModuleConfig structures from protobuf
199+
comp_name: dotted name of configuration entry
200+
raw_val: value to set
201+
"""
198202

199203
name = splitCompoundName(comp_name)
200204

@@ -209,7 +213,7 @@ def setPref(config, comp_name, raw_val) -> bool:
209213
config_type = objDesc.fields_by_name.get(name[0])
210214
if config_type and config_type.message_type is not None:
211215
for name_part in name[1:-1]:
212-
part_snake_name = meshtastic.util.camel_to_snake((name_part))
216+
part_snake_name = meshtastic.util.camel_to_snake(name_part)
213217
config_part = getattr(config, config_type.name)
214218
config_type = config_type.message_type.fields_by_name.get(part_snake_name)
215219
pref = None
@@ -234,7 +238,7 @@ def setPref(config, comp_name, raw_val) -> bool:
234238

235239
enumType = pref.enum_type
236240
# pylint: disable=C0123
237-
if enumType and type(val) == str:
241+
if enumType and isinstance(val, str):
238242
# We've failed so far to convert this string into an enum, try to find it by reflection
239243
e = enumType.values_by_name.get(val)
240244
if e:
@@ -628,12 +632,11 @@ def onConnected(interface):
628632
closeNow = True
629633
waitForAckNak = True
630634
node = interface.getNode(args.dest, False, **getNode_kwargs)
631-
632635
# Handle the int/float/bool arguments
633-
pref = None
634-
fields = set()
636+
fields = []
637+
allSettingOK = True
635638
for pref in args.set:
636-
found = False
639+
actSettingOK = False
637640
field = splitCompoundName(pref[0].lower())[0]
638641
for config in [node.localConfig, node.moduleConfig]:
639642
config_type = config.DESCRIPTOR.fields_by_name.get(field)
@@ -642,30 +645,29 @@ def onConnected(interface):
642645
node.requestConfig(
643646
config.DESCRIPTOR.fields_by_name.get(field)
644647
)
645-
found = setPref(config, pref[0], pref[1])
646-
if found:
647-
fields.add(field)
648+
if actSettingOK := setPref(config, pref[0], pref[1]):
648649
break
650+
fields.append((field, actSettingOK, pref[0]),)
651+
allSettingOK = allSettingOK and actSettingOK
649652

650-
if found:
653+
# only write to radio when all settings can be processed. If one setting is wrong, drop everything.
654+
# This shall ensure consistency of settings in the radio
655+
if allSettingOK:
656+
fieldsToWrite = set([field[0] for field in fields]) # keep only one occurence of a field
651657
print("Writing modified preferences to device")
652-
if len(fields) > 1:
658+
if len(fieldsToWrite) > 1:
653659
print("Using a configuration transaction")
654660
node.beginSettingsTransaction()
655-
for field in fields:
661+
for field in fieldsToWrite:
656662
print(f"Writing {field} configuration to device")
657663
node.writeConfig(field)
658-
if len(fields) > 1:
664+
if len(fieldsToWrite) > 1:
659665
node.commitSettingsTransaction()
660666
else:
661-
if mt_config.camel_case:
662-
print(
663-
f"{node.localConfig.__class__.__name__} and {node.moduleConfig.__class__.__name__} do not have an attribute {pref[0]}."
664-
)
665-
else:
666-
print(
667-
f"{node.localConfig.__class__.__name__} and {node.moduleConfig.__class__.__name__} do not have attribute {pref[0]}."
668-
)
667+
print("Cannot process command due to errors in parameters:")
668+
for field, flag, settingName in fields:
669+
if not flag:
670+
print(f"{node.localConfig.__class__.__name__} and {node.moduleConfig.__class__.__name__} do not have an attribute {settingName} in category {field}.")
669671
print("Choices are...")
670672
printConfig(node.localConfig)
671673
printConfig(node.moduleConfig)
@@ -927,11 +929,11 @@ def setSimpleConfig(modem_preset):
927929
# Handle the channel settings
928930
for pref in args.ch_set or []:
929931
if pref[0] == "psk":
930-
found = True
932+
actSettingOK = True
931933
ch.settings.psk = meshtastic.util.fromPSK(pref[1])
932934
else:
933-
found = setPref(ch.settings, pref[0], pref[1])
934-
if not found:
935+
actSettingOK = setPref(ch.settings, pref[0], pref[1])
936+
if not actSettingOK:
935937
category_settings = ["module_settings"]
936938
print(
937939
f"{ch.settings.__class__.__name__} does not have an attribute {pref[0]}."
@@ -1003,9 +1005,9 @@ def setSimpleConfig(modem_preset):
10031005
closeNow = True
10041006
node = interface.getNode(args.dest, False, **getNode_kwargs)
10051007
for pref in args.get:
1006-
found = getPref(node, pref[0])
1008+
actSettingOK = getPref(node, pref[0])
10071009

1008-
if found:
1010+
if actSettingOK:
10091011
print("Completed getting preferences")
10101012

10111013
if args.nodes:

meshtastic/mesh_interface.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@ def __init__(
106106
self.isConnected: threading.Event = threading.Event()
107107
self.noProto: bool = noProto
108108
self.localNode: meshtastic.node.Node = meshtastic.node.Node(
109-
self, -1, timeout=timeout
109+
self, -1, timeout=timeout, noProto=self.noProto
110110
) # We fixup nodenum later
111111
self.myInfo: Optional[
112112
mesh_pb2.MyNodeInfo

meshtastic/stream_interface.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -208,7 +208,7 @@ def __reader(self) -> None:
208208
self._rxBuf = empty
209209
else:
210210
# logger.debug(f"timeout")
211-
pass
211+
time.sleep(0.001) # don't block the system in case we do not get data
212212
except serial.SerialException as ex:
213213
if (
214214
not self._wantExit

meshtastic/tests/test_main.py

Lines changed: 54 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
from meshtastic import mt_config
2424

2525
from ..protobuf.channel_pb2 import Channel # pylint: disable=E0611
26+
from ..protobuf import localonly_pb2, config_pb2, module_config_pb2
2627

2728
# from ..ble_interface import BLEInterface
2829
from ..node import Node
@@ -503,6 +504,56 @@ def test_main_set_canned_messages(capsys):
503504
mo.assert_called()
504505

505506

507+
@pytest.mark.unit
508+
@pytest.mark.usefixtures("reset_mt_config")
509+
@patch("meshtastic.serial_interface.SerialInterface._set_hupcl_with_termios")
510+
@patch("builtins.open", new_callable=mock_open, read_data="data")
511+
@patch("serial.Serial")
512+
@patch("meshtastic.util.findPorts", return_value=["/dev/ttyUSBfake"])
513+
def test_main_set_3_parameters_OK(mocked_findPorts, mocked_serial, mocked_open, mock_hupcl, capsys):
514+
"""Test --set with 3 parameters with success"""
515+
sys.argv = ["", "--set", "mqtt.enabled", "1", "--set", "mqtt.username", "abc", "--set", "position.gps_enabled", "false"]
516+
mt_config.args = sys.argv
517+
518+
iface = SerialInterface(noProto=True)
519+
iface.localNode.localConfig.position.CopyFrom(config_pb2.Config.PositionConfig())
520+
iface.localNode.moduleConfig.mqtt.CopyFrom(module_config_pb2.ModuleConfig.MQTTConfig())
521+
522+
with patch("meshtastic.serial_interface.SerialInterface", return_value=iface) as mo:
523+
main()
524+
out, err = capsys.readouterr()
525+
assert re.search(r"Connected to radio", out, re.MULTILINE)
526+
assert re.search(r"Writing mqtt configuration to device", out, re.MULTILINE)
527+
assert re.search(r"Writing position configuration to device", out, re.MULTILINE)
528+
assert re.search(r"Set position.gps_enabled to false", out, re.MULTILINE)
529+
assert err == ""
530+
mo.assert_called()
531+
532+
533+
@pytest.mark.unit
534+
@pytest.mark.usefixtures("reset_mt_config")
535+
@patch("meshtastic.serial_interface.SerialInterface._set_hupcl_with_termios")
536+
@patch("builtins.open", new_callable=mock_open, read_data="data")
537+
@patch("serial.Serial")
538+
@patch("meshtastic.util.findPorts", return_value=["/dev/ttyUSBfake"])
539+
def test_main_set_3_parameters_NOK(mocked_findPorts, mocked_serial, mocked_open, mock_hupcl, capsys):
540+
"""Test --set with 3 parameters where 1 parameter is faulty"""
541+
sys.argv = ["", "--set", "mqtt.enabled", "1", "--set", "mqtt", "1", "--set", "mqtt.username", "abc"]
542+
mt_config.args = sys.argv
543+
544+
iface = SerialInterface(noProto=True)
545+
iface.localNode.localConfig.position.CopyFrom(config_pb2.Config.PositionConfig())
546+
iface.localNode.moduleConfig.mqtt.CopyFrom(module_config_pb2.ModuleConfig.MQTTConfig())
547+
with patch("meshtastic.serial_interface.SerialInterface", return_value=iface) as mo:
548+
main()
549+
out, err = capsys.readouterr()
550+
assert re.search(r"Connected to radio", out, re.MULTILINE)
551+
assert re.search(r"Cannot process command due to errors in parameters", out, re.MULTILINE)
552+
assert re.search(r"LocalConfig and LocalModuleConfig do not have an attribute mqtt in category mqtt", out, re.MULTILINE)
553+
assert err == ""
554+
mo.assert_called()
555+
556+
506557
@pytest.mark.unit
507558
@pytest.mark.usefixtures("reset_mt_config")
508559
def test_main_get_canned_messages(capsys, caplog, iface_with_nodes):
@@ -1065,14 +1116,14 @@ def test_main_set_with_invalid(mocked_findports, mocked_serial, mocked_open, moc
10651116
mt_config.args = sys.argv
10661117

10671118
serialInterface = SerialInterface(noProto=True)
1068-
anode = Node(serialInterface, 1234567890, noProto=True)
1069-
serialInterface.localNode = anode
1119+
serialInterface.localNode.nodeNum = 1234567890
10701120

10711121
with patch("meshtastic.serial_interface.SerialInterface", return_value=serialInterface) as mo:
10721122
main()
10731123
out, err = capsys.readouterr()
10741124
assert re.search(r"Connected to radio", out, re.MULTILINE)
1075-
assert re.search(r"do not have attribute foo", out, re.MULTILINE)
1125+
assert re.search(r"Cannot process command due to errors in parameters", out, re.MULTILINE)
1126+
assert re.search(r"LocalConfig and LocalModuleConfig do not have an attribute foo in category foo", out, re.MULTILINE)
10761127
assert err == ""
10771128
mo.assert_called()
10781129

0 commit comments

Comments
 (0)