@@ -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