@@ -742,10 +742,102 @@ def onConnected(interface):
742742 printConfig (node .moduleConfig )
743743
744744 if args .configure :
745- with open (args .configure [0 ], encoding = "utf8" ) as file :
746- configuration = yaml .safe_load (file )
745+ filename = args .configure [0 ]
746+
747+ is_binary = False
748+ if hasattr (args , "export_format" ) and args .export_format in ("binary" , "protobuf" ):
749+ is_binary = True
750+ elif hasattr (args , "export_format" ) and args .export_format == "yaml" :
751+ is_binary = False
752+ else :
753+ # Autodetection
754+ try :
755+ with open (filename , "r" , encoding = "utf8" ) as file :
756+ configuration = yaml .safe_load (file )
757+ if isinstance (configuration , dict ):
758+ is_binary = False
759+ else :
760+ is_binary = True
761+ except Exception :
762+ is_binary = True
763+
764+ if is_binary :
765+ from meshtastic .protobuf import clientonly_pb2
766+ # Read the binary protobuf file
767+ with open (filename , "rb" ) as file :
768+ profile = clientonly_pb2 .DeviceProfile ()
769+ profile .ParseFromString (file .read ())
770+
747771 closeNow = True
772+ interface .getNode (args .dest , False , ** getNode_kwargs ).beginSettingsTransaction ()
773+
774+ if profile .long_name :
775+ print (f"Setting device owner to { profile .long_name } " )
776+ waitForAckNak = True
777+ interface .getNode (args .dest , False , ** getNode_kwargs ).setOwner (profile .long_name )
778+ time .sleep (0.5 )
779+
780+ if profile .short_name :
781+ print (f"Setting device owner short to { profile .short_name } " )
782+ waitForAckNak = True
783+ interface .getNode (args .dest , False , ** getNode_kwargs ).setOwner (
784+ long_name = None , short_name = profile .short_name
785+ )
786+ time .sleep (0.5 )
787+
788+ if profile .channel_url :
789+ print (f"Setting channel url to { profile .channel_url } " )
790+ interface .getNode (args .dest , ** getNode_kwargs ).setURL (profile .channel_url )
791+ time .sleep (0.5 )
792+
793+ if profile .canned_messages :
794+ print (f"Setting canned message messages to { profile .canned_messages } " )
795+ interface .getNode (args .dest , ** getNode_kwargs ).set_canned_message (profile .canned_messages )
796+ time .sleep (0.5 )
797+
798+ if profile .ringtone :
799+ print (f"Setting ringtone to { profile .ringtone } " )
800+ interface .getNode (args .dest , ** getNode_kwargs ).set_ringtone (profile .ringtone )
801+ time .sleep (0.5 )
802+
803+ if profile .HasField ("fixed_position" ):
804+ # Convert high-precision integer coordinates back to floating point
805+ pos = profile .fixed_position
806+ lat = pos .latitude_i / 1e7 if pos .latitude_i else 0.0
807+ lon = pos .longitude_i / 1e7 if pos .longitude_i else 0.0
808+ alt = pos .altitude if pos .altitude else 0
809+ print (f"Fixing altitude at { alt } meters" )
810+ print (f"Fixing latitude at { lat } degrees" )
811+ print (f"Fixing longitude at { lon } degrees" )
812+ print ("Setting device position" )
813+ interface .localNode .setFixedPosition (lat , lon , alt )
814+ time .sleep (0.5 )
815+
816+ if profile .HasField ("config" ):
817+ # Dynamically iterate through populated config sections (e.g. lora, display)
818+ localConfig = interface .getNode (args .dest , ** getNode_kwargs ).localConfig
819+ for field in profile .config .DESCRIPTOR .fields :
820+ if profile .config .HasField (field .name ):
821+ getattr (localConfig , field .name ).CopyFrom (getattr (profile .config , field .name ))
822+ interface .getNode (args .dest , ** getNode_kwargs ).writeConfig (field .name )
823+ time .sleep (0.5 )
824+
825+ if profile .HasField ("module_config" ):
826+ # Dynamically iterate through populated module config sections (e.g. telemetry, serial)
827+ moduleConfig = interface .getNode (args .dest , ** getNode_kwargs ).moduleConfig
828+ for field in profile .module_config .DESCRIPTOR .fields :
829+ if profile .module_config .HasField (field .name ):
830+ getattr (moduleConfig , field .name ).CopyFrom (getattr (profile .module_config , field .name ))
831+ interface .getNode (args .dest , ** getNode_kwargs ).writeConfig (field .name )
832+ time .sleep (0.5 )
833+
834+ interface .getNode (args .dest , False , ** getNode_kwargs ).commitSettingsTransaction ()
835+ print ("Writing modified configuration to device" )
748836
837+ else :
838+ with open (filename , "r" , encoding = "utf8" ) as file :
839+ configuration = yaml .safe_load (file )
840+ closeNow = True
749841 interface .getNode (args .dest , False , ** getNode_kwargs ).beginSettingsTransaction ()
750842
751843 if "owner" in configuration :
@@ -858,18 +950,36 @@ def onConnected(interface):
858950 return
859951
860952 closeNow = True
861- config_txt = export_config (interface )
862953
863- if args .export_config == "-" :
864- # Output to stdout (preserves legacy use of `> file.yaml`)
865- print (config_txt )
954+ is_binary = False
955+ if hasattr (args , "export_format" ) and args .export_format in ("binary" , "protobuf" ):
956+ is_binary = True
957+ elif hasattr (args , "export_format" ) and args .export_format == "yaml" :
958+ is_binary = False
866959 else :
960+ is_binary = args .export_config .endswith (".cfg" )
961+
962+ if is_binary :
963+ config_bytes = export_profile (interface )
867964 try :
868- with open (args .export_config , "w" , encoding = "utf-8 " ) as f :
869- f .write (config_txt )
870- print (f"Exported configuration to { args .export_config } " )
965+ with open (args .export_config , "wb " ) as f :
966+ f .write (config_bytes )
967+ print (f"Exported profile to { args .export_config } " )
871968 except Exception as e :
872- meshtastic .util .our_exit (f"ERROR: Failed to write config file: { e } " )
969+ meshtastic .util .our_exit (f"ERROR: Failed to write profile file: { e } " )
970+ else :
971+ config_txt = export_config (interface )
972+
973+ if args .export_config == "-" :
974+ # Output to stdout (preserves legacy use of `> file.yaml`)
975+ print (config_txt )
976+ else :
977+ try :
978+ with open (args .export_config , "w" , encoding = "utf-8" ) as f :
979+ f .write (config_txt )
980+ print (f"Exported configuration to { args .export_config } " )
981+ except Exception as e :
982+ meshtastic .util .our_exit (f"ERROR: Failed to write config file: { e } " )
873983
874984 if args .ch_set_url :
875985 closeNow = True
@@ -1332,6 +1442,49 @@ def export_config(interface) -> str:
13321442 config_txt += yaml .dump (configObj )
13331443 return config_txt
13341444
1445+ def export_profile (interface ) -> bytes :
1446+ """used in --export-config for binary .cfg files"""
1447+ from meshtastic .protobuf import clientonly_pb2
1448+
1449+ profile = clientonly_pb2 .DeviceProfile ()
1450+
1451+ owner = interface .getLongName ()
1452+ owner_short = interface .getShortName ()
1453+ channel_url = interface .localNode .getURL ()
1454+ myinfo = interface .getMyNodeInfo ()
1455+ canned_messages = interface .getCannedMessage ()
1456+ ringtone = interface .getRingtone ()
1457+
1458+ if owner :
1459+ profile .long_name = owner
1460+ if owner_short :
1461+ profile .short_name = owner_short
1462+ if channel_url :
1463+ profile .channel_url = channel_url
1464+ if canned_messages :
1465+ profile .canned_messages = canned_messages
1466+ if ringtone :
1467+ profile .ringtone = ringtone
1468+
1469+ profile .config .CopyFrom (interface .localNode .localConfig )
1470+ profile .module_config .CopyFrom (interface .localNode .moduleConfig )
1471+
1472+ pos = myinfo .get ("position" )
1473+ if pos :
1474+ lat = pos .get ("latitude" )
1475+ lon = pos .get ("longitude" )
1476+ alt = pos .get ("altitude" )
1477+
1478+ if lat or lon or alt :
1479+ if lat :
1480+ profile .fixed_position .latitude_i = int (lat * 1e7 )
1481+ if lon :
1482+ profile .fixed_position .longitude_i = int (lon * 1e7 )
1483+ if alt :
1484+ profile .fixed_position .altitude = int (alt )
1485+
1486+ return profile .SerializeToString ()
1487+
13351488
13361489def create_power_meter ():
13371490 """Setup the power meter."""
@@ -1702,15 +1855,23 @@ def addImportExportArgs(parser: argparse.ArgumentParser) -> argparse.ArgumentPar
17021855
17031856 group .add_argument (
17041857 "--configure" ,
1705- help = "Specify a path to a yaml(.yml) file containing the desired settings for the connected device." ,
1858+ "--import-config" ,
1859+ dest = "configure" ,
1860+ help = "Specify a path to a configuration file to import. Autodetects format (yaml or binary protobuf)." ,
17061861 action = "append" ,
17071862 )
17081863 group .add_argument (
17091864 "--export-config" ,
17101865 nargs = "?" ,
17111866 const = "-" , # default to "-" if no value provided
17121867 metavar = "FILE" ,
1713- help = "Export device config as YAML (to stdout if no file given)"
1868+ help = "Export device config (to stdout if no file given). Autodetects format by extension if possible."
1869+ )
1870+ group .add_argument (
1871+ "--export-format" ,
1872+ choices = ["auto" , "yaml" , "binary" , "protobuf" ],
1873+ default = "auto" ,
1874+ help = "Format for export or import. 'auto' uses file extension or contents. 'binary' or 'protobuf' forces binary format. 'yaml' forces yaml."
17141875 )
17151876 return parser
17161877
0 commit comments