Skip to content

Commit 8c5efb9

Browse files
committed
add support for .cfg export/import
adds binary import and export to mirror functionality of android (and soon ios) app
1 parent 6d76edf commit 8c5efb9

1 file changed

Lines changed: 173 additions & 12 deletions

File tree

meshtastic/__main__.py

Lines changed: 173 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -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

13361489
def 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

Comments
 (0)