Skip to content

Commit 6ebddb6

Browse files
committed
Refactored showNodes. It now uses a lookup table to determine the human-readable names of the fields.
1 parent 82554a1 commit 6ebddb6

1 file changed

Lines changed: 89 additions & 71 deletions

File tree

meshtastic/mesh_interface.py

Lines changed: 89 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -224,7 +224,40 @@ def showInfo(self, file=sys.stdout) -> str: # pylint: disable=W0613
224224
def showNodes(
225225
self, includeSelf: bool = True, showFields: Optional[List[str]] = None
226226
) -> str: # pylint: disable=W0613
227-
"""Show table summary of nodes in mesh"""
227+
"""Show table summary of nodes in mesh
228+
229+
Args:
230+
includeSelf (bool): Include ourself in the output?
231+
showFields (List[str]): List of fields to show in output
232+
"""
233+
234+
def get_human_readable(name):
235+
name_map = {
236+
"user.longName": "User",
237+
"user.id": "ID",
238+
"user.shortName": "AKA",
239+
"user.hwModel": "Hardware",
240+
"user.publicKey": "Pubkey",
241+
"user.role": "Role",
242+
"position.latitude": "Latitude",
243+
"position.longitude": "Longitude",
244+
"position.altitude": "Altitude",
245+
"deviceMetrics.batteryLevel": "Battery",
246+
"deviceMetrics.channelUtilization": "Channel util.",
247+
"deviceMetrics.airUtilTx": "Tx air util.",
248+
"snr": "SNR",
249+
"hopsAway": "Hops",
250+
"channel": "Channel",
251+
"lastHeard": "LastHeard",
252+
"since": "Since",
253+
254+
}
255+
256+
if name in name_map:
257+
return name_map.get(name) # Default to a formatted guess
258+
else:
259+
return name
260+
228261

229262
def formatFloat(value, precision=2, unit="") -> Optional[str]:
230263
"""Format a float value with precision."""
@@ -247,6 +280,9 @@ def getTimeAgo(ts) -> Optional[str]:
247280
return _timeago(delta_secs)
248281

249282
def getNestedValue(node_dict: Dict[str, Any], key_path: str) -> Any:
283+
if key_path.index(".") < 0:
284+
logging.debug("getNestedValue was called without a nested path.")
285+
return None
250286
keys = key_path.split(".")
251287
value = node_dict
252288
for key in keys:
@@ -258,7 +294,10 @@ def getNestedValue(node_dict: Dict[str, Any], key_path: str) -> Any:
258294

259295
if showFields is None or showFields.count == 0:
260296
# The default set of fields to show (e.g., the status quo)
261-
showFields = ["N", "User", "ID", "AKA", "Hardware", "Pubkey", "Role", "Latitude", "Longitude", "Altitude", "Battery", "Channel util.", "Tx air util.", "SNR", "Hops", "Channel", "LastHeard", "Since"]
297+
showFields = ["N", "user.longName", "user.id", "user.shortName", "user.hwModel", "user.publicKey",
298+
"user.role", "position.latitude", "position.longitude", "position.altitude",
299+
"deviceMetrics.batteryLevel", "deviceMetrics.channelUtilization",
300+
"deviceMetrics.airUtilTx", "snr", "hopsAway", "channel", "lastHeard", "since"]
262301
else:
263302
# Always at least include the row number.
264303
showFields.insert(0, "N")
@@ -271,80 +310,59 @@ def getNestedValue(node_dict: Dict[str, Any], key_path: str) -> Any:
271310
continue
272311

273312
presumptive_id = f"!{node['num']:08x}"
274-
row = {
275-
"N": 0,
276-
"User": f"Meshtastic {presumptive_id[-4:]}",
277-
"ID": presumptive_id,
278-
}
279-
280-
user = node.get("user")
281-
if user:
282-
row.update(
283-
{
284-
"User": user.get("longName", "N/A"),
285-
"AKA": user.get("shortName", "N/A"),
286-
"ID": user["id"],
287-
"Hardware": user.get("hwModel", "UNSET"),
288-
"Pubkey": user.get("publicKey", "UNSET"),
289-
"Role": user.get("role", "N/A"),
290-
}
291-
)
292-
293-
pos = node.get("position")
294-
if pos:
295-
row.update(
296-
{
297-
"Latitude": formatFloat(pos.get("latitude"), 4, "°"),
298-
"Longitude": formatFloat(pos.get("longitude"), 4, "°"),
299-
"Altitude": formatFloat(pos.get("altitude"), 0, " m"),
300-
}
301-
)
302-
303-
metrics = node.get("deviceMetrics")
304-
if metrics:
305-
batteryLevel = metrics.get("batteryLevel")
306-
if batteryLevel is not None:
307-
if batteryLevel in (0, 101): # Note: for boards without battery pin or PMU, 101% battery means 'the board is using external power'
308-
batteryString = "Powered"
309-
else:
310-
batteryString = str(batteryLevel) + "%"
311-
row.update({"Battery": batteryString})
312-
313-
row.update(
314-
{
315-
"Channel util.": formatFloat(
316-
metrics.get("channelUtilization"), 2, "%"
317-
),
318-
"Tx air util.": formatFloat(
319-
metrics.get("airUtilTx"), 2, "%"
320-
),
321-
}
322-
)
323-
324-
row.update(
325-
{
326-
"SNR": formatFloat(node.get("snr"), 2, " dB"),
327-
"Hops": node.get("hopsAway", "?"),
328-
"Channel": node.get("channel", 0),
329-
"LastHeard": getLH(node.get("lastHeard")),
330-
"Since": getTimeAgo(node.get("lastHeard")),
331-
}
332-
)
333313

334314
# This allows the user to specify fields that wouldn't otherwise be included.
335-
extraFields = {}
315+
fields = {}
336316
for field in showFields:
337-
if field in row:
338-
# We already have it, move along.
339-
continue
340-
elif "." in field:
341-
extraFields[field] = getNestedValue(node, field)
317+
if "." in field:
318+
raw_value = getNestedValue(node, field)
342319
else:
343-
extraFields[field] = node.get(field)
344-
320+
# The "since" column is synthesized, it's not retrieved from the device. Get the
321+
# lastHeard value here, and then we'll format it properly below.
322+
if field == "since":
323+
raw_value = node.get("lastHeard")
324+
else:
325+
raw_value = node.get(field)
326+
327+
formatted_value = ""
328+
329+
# Some of these need special formatting or processing.
330+
if field == "channel":
331+
if raw_value is None:
332+
formatted_value = "0"
333+
elif field == "deviceMetrics.channelUtilization":
334+
formatted_value = formatFloat(raw_value, 2, "%")
335+
elif field == "deviceMetrics.airUtilTx":
336+
formatted_value = formatFloat(raw_value, 2, "%")
337+
elif field == "deviceMetrics.batteryLevel":
338+
if raw_value in (0, 101):
339+
formatted_value = "Powered"
340+
else:
341+
formatted_value = formatFloat(raw_value, 0, "%")
342+
elif field == "lastHeard":
343+
formatted_value = getLH(raw_value)
344+
elif field == "position.latitude":
345+
formatted_value = formatFloat(raw_value, 4, "°")
346+
elif field == "position.longitude":
347+
formatted_value = formatFloat(raw_value, 4, "°")
348+
elif field == "position.altitude":
349+
formatted_value = formatFloat(raw_value, 0, "m")
350+
elif field == "since":
351+
formatted_value = getTimeAgo(raw_value)
352+
elif field == "snr":
353+
formatted_value = formatFloat(raw_value, 0, " dB")
354+
elif field == "user.shortName":
355+
formatted_value = raw_value if raw_value is not None else f'Meshtastic {presumptive_id[-4:]}'
356+
elif field == "user.id":
357+
formatted_value = raw_value if raw_value is not None else presumptive_id
358+
else:
359+
formatted_value = raw_value # No special formatting
360+
361+
fields[field] = formatted_value
362+
345363
# Filter out any field in the data set that was not specified.
346-
filteredData = {key: value for key, value in row.items() if key in showFields}
347-
filteredData.update(extraFields)
364+
filteredData = {get_human_readable(k): v for k, v in fields.items() if k in showFields}
365+
filteredData.update({get_human_readable(k): v for k, v in fields.items()})
348366
rows.append(filteredData)
349367

350368
rows.sort(key=lambda r: r.get("LastHeard") or "0000", reverse=True)

0 commit comments

Comments
 (0)