11""" Main Meshtastic
22"""
3- # pylint: disable=C0302
3+
4+ # We just hit the 1600 line limit for main.py, but I currently have a huge set of powermon/structured logging changes
5+ # later we can have a separate changelist to refactor main.py into smaller files
6+ # pylint: disable=R0917,C0302
47
58from typing import List , Optional , Union
69from types import ModuleType
@@ -339,6 +342,18 @@ def onConnected(interface):
339342 if args .set_owner or args .set_owner_short :
340343 closeNow = True
341344 waitForAckNak = True
345+
346+ # Validate owner names before connecting to device
347+ if args .set_owner is not None :
348+ stripped_long_name = args .set_owner .strip ()
349+ if not stripped_long_name :
350+ meshtastic .util .our_exit ("ERROR: Long Name cannot be empty or contain only whitespace characters" )
351+
352+ if hasattr (args , 'set_owner_short' ) and args .set_owner_short is not None :
353+ stripped_short_name = args .set_owner_short .strip ()
354+ if not stripped_short_name :
355+ meshtastic .util .our_exit ("ERROR: Short Name cannot be empty or contain only whitespace characters" )
356+
342357 if args .set_owner and args .set_owner_short :
343358 print (f"Setting device owner to { args .set_owner } and short name to { args .set_owner_short } " )
344359 elif args .set_owner :
@@ -399,6 +414,8 @@ def onConnected(interface):
399414 print (" " .join (fieldNames ))
400415
401416 if args .set_ham :
417+ if not args .set_ham .strip ():
418+ meshtastic .util .our_exit ("ERROR: Ham radio callsign cannot be empty or contain only whitespace characters" )
402419 closeNow = True
403420 print (f"Setting Ham ID to { args .set_ham } and turning off encryption" )
404421 interface .getNode (args .dest , ** getNode_kwargs ).setOwner (args .set_ham , is_licensed = True )
@@ -594,6 +611,7 @@ def onConnected(interface):
594611
595612 # Handle the int/float/bool arguments
596613 pref = None
614+ fields = set ()
597615 for pref in args .set :
598616 found = False
599617 field = splitCompoundName (pref [0 ].lower ())[0 ]
@@ -606,11 +624,19 @@ def onConnected(interface):
606624 )
607625 found = setPref (config , pref [0 ], pref [1 ])
608626 if found :
627+ fields .add (field )
609628 break
610629
611630 if found :
612631 print ("Writing modified preferences to device" )
613- node .writeConfig (field )
632+ if len (fields ) > 1 :
633+ print ("Using a configuration transaction" )
634+ node .beginSettingsTransaction ()
635+ for field in fields :
636+ print (f"Writing { field } configuration to device" )
637+ node .writeConfig (field )
638+ if len (fields ) > 1 :
639+ node .commitSettingsTransaction ()
614640 else :
615641 if mt_config .camel_case :
616642 print (
@@ -632,35 +658,52 @@ def onConnected(interface):
632658 interface .getNode (args .dest , False , ** getNode_kwargs ).beginSettingsTransaction ()
633659
634660 if "owner" in configuration :
661+ # Validate owner name before setting
662+ owner_name = str (configuration ["owner" ]).strip ()
663+ if not owner_name :
664+ meshtastic .util .our_exit ("ERROR: Long Name cannot be empty or contain only whitespace characters" )
635665 print (f"Setting device owner to { configuration ['owner' ]} " )
636666 waitForAckNak = True
637667 interface .getNode (args .dest , False , ** getNode_kwargs ).setOwner (configuration ["owner" ])
668+ time .sleep (0.5 )
638669
639670 if "owner_short" in configuration :
671+ # Validate owner short name before setting
672+ owner_short_name = str (configuration ["owner_short" ]).strip ()
673+ if not owner_short_name :
674+ meshtastic .util .our_exit ("ERROR: Short Name cannot be empty or contain only whitespace characters" )
640675 print (
641676 f"Setting device owner short to { configuration ['owner_short' ]} "
642677 )
643678 waitForAckNak = True
644679 interface .getNode (args .dest , False , ** getNode_kwargs ).setOwner (
645680 long_name = None , short_name = configuration ["owner_short" ]
646681 )
682+ time .sleep (0.5 )
647683
648684 if "ownerShort" in configuration :
685+ # Validate owner short name before setting
686+ owner_short_name = str (configuration ["ownerShort" ]).strip ()
687+ if not owner_short_name :
688+ meshtastic .util .our_exit ("ERROR: Short Name cannot be empty or contain only whitespace characters" )
649689 print (
650690 f"Setting device owner short to { configuration ['ownerShort' ]} "
651691 )
652692 waitForAckNak = True
653693 interface .getNode (args .dest , False , ** getNode_kwargs ).setOwner (
654694 long_name = None , short_name = configuration ["ownerShort" ]
655695 )
696+ time .sleep (0.5 )
656697
657698 if "channel_url" in configuration :
658699 print ("Setting channel url to" , configuration ["channel_url" ])
659700 interface .getNode (args .dest , ** getNode_kwargs ).setURL (configuration ["channel_url" ])
701+ time .sleep (0.5 )
660702
661703 if "channelUrl" in configuration :
662704 print ("Setting channel url to" , configuration ["channelUrl" ])
663705 interface .getNode (args .dest , ** getNode_kwargs ).setURL (configuration ["channelUrl" ])
706+ time .sleep (0.5 )
664707
665708 if "location" in configuration :
666709 alt = 0
@@ -679,6 +722,7 @@ def onConnected(interface):
679722 print (f"Fixing longitude at { lon } degrees" )
680723 print ("Setting device position" )
681724 interface .localNode .setFixedPosition (lat , lon , alt )
725+ time .sleep (0.5 )
682726
683727 if "config" in configuration :
684728 localConfig = interface .getNode (args .dest , ** getNode_kwargs ).localConfig
@@ -689,6 +733,7 @@ def onConnected(interface):
689733 interface .getNode (args .dest , ** getNode_kwargs ).writeConfig (
690734 meshtastic .util .camel_to_snake (section )
691735 )
736+ time .sleep (0.5 )
692737
693738 if "module_config" in configuration :
694739 moduleConfig = interface .getNode (args .dest , ** getNode_kwargs ).moduleConfig
@@ -701,6 +746,7 @@ def onConnected(interface):
701746 interface .getNode (args .dest , ** getNode_kwargs ).writeConfig (
702747 meshtastic .util .camel_to_snake (section )
703748 )
749+ time .sleep (0.5 )
704750
705751 interface .getNode (args .dest , False , ** getNode_kwargs ).commitSettingsTransaction ()
706752 print ("Writing modified configuration to device" )
@@ -709,9 +755,20 @@ def onConnected(interface):
709755 if args .dest != BROADCAST_ADDR :
710756 print ("Exporting configuration of remote nodes is not supported." )
711757 return
712- # export the configuration (the opposite of '--configure')
758+
713759 closeNow = True
714- export_config (interface )
760+ config_txt = export_config (interface )
761+
762+ if args .export_config == "-" :
763+ # Output to stdout (preserves legacy use of `> file.yaml`)
764+ print (config_txt )
765+ else :
766+ try :
767+ with open (args .export_config , "w" , encoding = "utf-8" ) as f :
768+ f .write (config_txt )
769+ print (f"Exported configuration to { args .export_config } " )
770+ except Exception as e :
771+ meshtastic .util .our_exit (f"ERROR: Failed to write config file: { e } " )
715772
716773 if args .ch_set_url :
717774 closeNow = True
@@ -1076,6 +1133,7 @@ def export_config(interface) -> str:
10761133 configObj ["location" ]["alt" ] = alt
10771134
10781135 config = MessageToDict (interface .localNode .localConfig ) #checkme - Used as a dictionary here and a string below
1136+ #was used as a string here and a Dictionary above
10791137 if config :
10801138 # Convert inner keys to correct snake/camelCase
10811139 prefs = {}
@@ -1113,7 +1171,6 @@ def export_config(interface) -> str:
11131171 config_txt = "# start of Meshtastic configure yaml\n " #checkme - "config" (now changed to config_out)
11141172 #was used as a string here and a Dictionary above
11151173 config_txt += yaml .dump (configObj )
1116- print (config_txt )
11171174 return config_txt
11181175
11191176
@@ -1170,6 +1227,22 @@ def common():
11701227 meshtastic .util .support_info ()
11711228 meshtastic .util .our_exit ("" , 0 )
11721229
1230+ # Early validation for owner names before attempting device connection
1231+ if hasattr (args , 'set_owner' ) and args .set_owner is not None :
1232+ stripped_long_name = args .set_owner .strip ()
1233+ if not stripped_long_name :
1234+ meshtastic .util .our_exit ("ERROR: Long Name cannot be empty or contain only whitespace characters" )
1235+
1236+ if hasattr (args , 'set_owner_short' ) and args .set_owner_short is not None :
1237+ stripped_short_name = args .set_owner_short .strip ()
1238+ if not stripped_short_name :
1239+ meshtastic .util .our_exit ("ERROR: Short Name cannot be empty or contain only whitespace characters" )
1240+
1241+ if hasattr (args , 'set_ham' ) and args .set_ham is not None :
1242+ stripped_ham_name = args .set_ham .strip ()
1243+ if not stripped_ham_name :
1244+ meshtastic .util .our_exit ("ERROR: Ham radio callsign cannot be empty or contain only whitespace characters" )
1245+
11731246 if have_powermon :
11741247 create_power_meter ()
11751248
@@ -1397,8 +1470,10 @@ def addImportExportArgs(parser: argparse.ArgumentParser) -> argparse.ArgumentPar
13971470 )
13981471 group .add_argument (
13991472 "--export-config" ,
1400- help = "Export the configuration in yaml(.yml) format." ,
1401- action = "store_true" ,
1473+ nargs = "?" ,
1474+ const = "-" , # default to "-" if no value provided
1475+ metavar = "FILE" ,
1476+ help = "Export device config as YAML (to stdout if no file given)"
14021477 )
14031478 return parser
14041479
@@ -1414,7 +1489,7 @@ def addConfigArgs(parser: argparse.ArgumentParser) -> argparse.ArgumentParser:
14141489 "--get" ,
14151490 help = (
14161491 "Get a preferences field. Use an invalid field such as '0' to get a list of all fields."
1417- " Can use either snake_case or camelCase format. (ex: 'ls_secs' or 'lsSecs')"
1492+ " Can use either snake_case or camelCase format. (ex: 'power. ls_secs' or 'power. lsSecs')"
14181493 ),
14191494 nargs = 1 ,
14201495 action = "append" ,
@@ -1423,7 +1498,11 @@ def addConfigArgs(parser: argparse.ArgumentParser) -> argparse.ArgumentParser:
14231498
14241499 group .add_argument (
14251500 "--set" ,
1426- help = "Set a preferences field. Can use either snake_case or camelCase format. (ex: 'ls_secs' or 'lsSecs')" ,
1501+ help = (
1502+ "Set a preferences field. Can use either snake_case or camelCase format."
1503+ " (ex: 'power.ls_secs' or 'power.lsSecs'). May be less reliable when"
1504+ " setting properties from more than one configuration section."
1505+ ),
14271506 nargs = 2 ,
14281507 action = "append" ,
14291508 metavar = ("FIELD" , "VALUE" ),
0 commit comments