meshconsole 3.2.0__tar.gz → 3.2.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: meshconsole
3
- Version: 3.2.0
3
+ Version: 3.2.2
4
4
  Summary: A powerful CLI and web interface for Meshtastic and MeshCore mesh networking devices
5
5
  Project-URL: Homepage, https://github.com/m9wav/MeshConsole
6
6
  Project-URL: Repository, https://github.com/m9wav/MeshConsole
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "meshconsole"
7
- version = "3.2.0"
7
+ version = "3.2.2"
8
8
  description = "A powerful CLI and web interface for Meshtastic and MeshCore mesh networking devices"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -5,7 +5,7 @@ Author: M9WAV
5
5
  License: MIT
6
6
  """
7
7
 
8
- __version__ = "3.2.0"
8
+ __version__ = "3.2.2"
9
9
  __author__ = "M9WAV"
10
10
 
11
11
  from meshconsole.core import (
@@ -64,6 +64,11 @@ class MeshBackend(ABC):
64
64
  """Cleanly disconnect from the device."""
65
65
  ...
66
66
 
67
+ def reconnect(self) -> None:
68
+ """Disconnect and reconnect to the same device."""
69
+ self.disconnect()
70
+ self.connect()
71
+
67
72
  @abstractmethod
68
73
  def get_nodes(self) -> dict[str, UnifiedNode]:
69
74
  """Return all known nodes, keyed by canonical node_id."""
@@ -156,6 +156,7 @@ class MeshtasticBackend(MeshBackend):
156
156
 
157
157
  def disconnect(self) -> None:
158
158
  """Cleanly disconnect from the device."""
159
+ self._connected = False
159
160
  # Unsubscribe from pypubsub to prevent stale callbacks
160
161
  if self._subscribed:
161
162
  try:
@@ -164,15 +165,26 @@ class MeshtasticBackend(MeshBackend):
164
165
  except Exception:
165
166
  pass
166
167
  self._subscribed = False
167
- try:
168
- if self._interface:
169
- if hasattr(self._interface, 'close'):
170
- self._interface.close()
171
- except Exception as e:
172
- logger.error(f"Error closing Meshtastic interface: {e}")
173
- finally:
174
- self._interface = None
175
- self._connected = False
168
+ # Close the interface, suppressing any broken pipe errors
169
+ iface = self._interface
170
+ self._interface = None
171
+ if iface:
172
+ try:
173
+ # Kill the heartbeat timer to prevent BrokenPipeError
174
+ if hasattr(iface, 'heartbeatTimer') and iface.heartbeatTimer:
175
+ iface.heartbeatTimer.cancel()
176
+ iface.heartbeatTimer = None
177
+ if hasattr(iface, 'close'):
178
+ iface.close()
179
+ except (BrokenPipeError, OSError, Exception) as e:
180
+ logger.debug(f"Expected error closing Meshtastic interface: {e}")
181
+
182
+ def reconnect(self) -> None:
183
+ """Disconnect and reconnect to the same device."""
184
+ logger.info(f"Reconnecting Meshtastic ({self._connection_type})...")
185
+ self.disconnect()
186
+ time.sleep(1)
187
+ self.connect()
176
188
 
177
189
  def get_nodes(self) -> dict[str, UnifiedNode]:
178
190
  """Return all known nodes, keyed by canonical node_id."""
@@ -25,7 +25,7 @@ def build_parser() -> argparse.ArgumentParser:
25
25
  formatter_class=argparse.ArgumentDefaultsHelpFormatter
26
26
  )
27
27
 
28
- parser.add_argument('--version', action='version', version='MeshConsole 3.2.0')
28
+ parser.add_argument('--version', action='version', version='MeshConsole 3.2.2')
29
29
 
30
30
  subparsers = parser.add_subparsers(dest='command', help='Available commands')
31
31
 
@@ -746,43 +746,46 @@ class MeshtasticTool:
746
746
  # ── Health check each backend independently ──
747
747
  failed_backends = []
748
748
  for b in list(self.backends):
749
- if b.backend_type == BackendType.MESHTASTIC:
750
- iface = getattr(b, 'interface', None)
751
- if not iface:
752
- failed_backends.append(b)
753
- continue
749
+ try:
750
+ if b.backend_type == BackendType.MESHTASTIC:
751
+ iface = getattr(b, 'interface', None)
752
+ if not iface:
753
+ failed_backends.append(b)
754
+ continue
755
+
756
+ healthy = True
757
+ if hasattr(iface, 'isConnected') and not iface.isConnected:
758
+ healthy = False
754
759
 
755
- healthy = True
756
- if hasattr(iface, 'isConnected') and not iface.isConnected:
757
- healthy = False
760
+ if hasattr(iface, 'socket') and iface.socket:
761
+ try:
762
+ error = iface.socket.getsockopt(socket.SOL_SOCKET, socket.SO_ERROR)
763
+ if error != 0:
764
+ healthy = False
765
+ except (BrokenPipeError, OSError):
766
+ healthy = False
758
767
 
759
- if hasattr(iface, 'socket') and iface.socket:
760
768
  try:
761
- error = iface.socket.getsockopt(socket.SOL_SOCKET, socket.SO_ERROR)
762
- if error != 0:
763
- healthy = False
764
- except Exception:
769
+ if hasattr(iface, 'nodes'):
770
+ _ = len(iface.nodes)
771
+ except (BrokenPipeError, OSError, Exception):
765
772
  healthy = False
766
773
 
767
- if current_time - last_packet_time > connection_timeout * 2:
768
- healthy = False
769
-
770
- try:
771
- if hasattr(iface, 'nodes'):
772
- _ = len(iface.nodes)
773
- except Exception:
774
- healthy = False
775
-
776
- if not healthy:
777
- failed_backends.append(b)
774
+ if not healthy:
775
+ failed_backends.append(b)
778
776
 
779
- elif b.backend_type == BackendType.MESHCORE:
780
- if not b.is_connected:
781
- failed_backends.append(b)
777
+ elif b.backend_type == BackendType.MESHCORE:
778
+ if not b.is_connected:
779
+ failed_backends.append(b)
780
+ except (BrokenPipeError, OSError) as health_err:
781
+ logger.debug(f"Health check error for {b.device_id}: {health_err}")
782
+ failed_backends.append(b)
782
783
 
783
784
  if failed_backends:
785
+ # Only raise if at least one backend was supposed to be connected
786
+ names = [b.device_id for b in failed_backends]
784
787
  raise ConnectionError(
785
- f"{len(failed_backends)} backend(s) failed health check"
788
+ f"Backend(s) failed health check: {', '.join(names)}"
786
789
  )
787
790
 
788
791
  # ── Track packet activity ──
@@ -800,30 +803,43 @@ class MeshtasticTool:
800
803
  except (ConnectionError, BrokenPipeError, OSError, socket.error, Exception) as e:
801
804
  logger.error(f"Connection lost: {e}")
802
805
 
803
- # ── Cleanup failed backends ──
804
- for b in list(self.backends):
805
- if not b.is_connected or (b.backend_type == BackendType.MESHTASTIC and not getattr(b, 'interface', None)):
806
- try:
807
- b.disconnect()
808
- except Exception:
809
- pass
810
-
811
806
  time.sleep(2)
812
-
813
807
  logger.info(f"Attempting to reconnect in {retry_delay} seconds...")
814
808
  time.sleep(retry_delay)
815
809
 
816
- try:
817
- logger.info("Reconnecting backends...")
818
- self._connect_interface()
819
- self._sync_node_db()
810
+ # ── Reconnect only failed backends, leave healthy ones alone ──
811
+ reconnected_any = False
812
+ for b in list(self.backends):
813
+ # Check if this backend needs reconnection
814
+ needs_reconnect = False
815
+ if b.backend_type == BackendType.MESHTASTIC:
816
+ iface = getattr(b, 'interface', None)
817
+ if not iface or not b.is_connected:
818
+ needs_reconnect = True
819
+ elif hasattr(iface, 'isConnected') and not iface.isConnected:
820
+ needs_reconnect = True
821
+ elif not b.is_connected:
822
+ needs_reconnect = True
823
+
824
+ if not needs_reconnect:
825
+ continue
826
+
827
+ try:
828
+ logger.info(f"Reconnecting {b.device_id}...")
829
+ b.reconnect()
830
+ if b.backend_type == BackendType.MESHTASTIC:
831
+ b._sync_node_db()
832
+ reconnected_any = True
833
+ logger.info(f"Reconnected {b.device_id}")
834
+ except Exception as re:
835
+ logger.error(f"Reconnection failed for {b.device_id}: {re}")
836
+
837
+ if reconnected_any:
820
838
  retry_delay = 1
821
839
  last_packet_time = time.time()
822
- logger.info("Successfully reconnected.")
823
- except Exception as reconnect_error:
824
- logger.error(f"Reconnection attempt failed: {reconnect_error}")
840
+ else:
825
841
  retry_delay = min(retry_delay * 2, max_retry_delay)
826
- continue
842
+ continue
827
843
 
828
844
  def _load_recent_packets_from_db(self):
829
845
  """Load recent packets from database into memory for web interface."""
@@ -1594,9 +1594,11 @@
1594
1594
 
1595
1595
  appState.nodes = nodes;
1596
1596
  // Sort by lastSeen descending (most recent first)
1597
- const sortedNodes = Object.values(nodes).sort((a, b) =>
1598
- new Date(b.lastSeen) - new Date(a.lastSeen)
1599
- );
1597
+ const sortedNodes = Object.values(nodes).sort((a, b) => {
1598
+ const dateA = new Date(a.lastSeen || 0);
1599
+ const dateB = new Date(b.lastSeen || 0);
1600
+ return (isNaN(dateB) ? 0 : dateB) - (isNaN(dateA) ? 0 : dateA);
1601
+ });
1600
1602
  appState.sortedNodes = sortedNodes;
1601
1603
  renderNodes(sortedNodes);
1602
1604
  populateNodeSelects(sortedNodes);
@@ -1740,7 +1742,7 @@
1740
1742
  ${nodeIdDisplay}
1741
1743
  </p>
1742
1744
  <p class="text-muted small mb-0">
1743
- <i class="bi bi-clock"></i> Last seen: ${new Date(node.lastSeen).toLocaleString()}
1745
+ <i class="bi bi-clock"></i> Last seen: ${node.lastSeen ? (isNaN(new Date(node.lastSeen).getTime()) ? 'Unknown' : new Date(node.lastSeen).toLocaleString()) : 'Unknown'}
1744
1746
  </p>
1745
1747
  </div>
1746
1748
  </div>
@@ -2197,16 +2199,15 @@
2197
2199
  entries.forEach((info, idx) => {
2198
2200
  const isConn = info.connected;
2199
2201
  const indicatorClass = isConn ? 'status-online' : 'status-offline';
2200
- const typeName = (info.type || 'unknown');
2201
- const label = typeName.charAt(0).toUpperCase() + typeName.slice(1);
2202
- const statusLabel = isConn ? 'Connected' : 'Disconnected';
2203
- const deviceName = info.device_name ? ` (${info.device_name})` : '';
2202
+ const deviceName = info.device_name || '';
2204
2203
  statusHtml += `<span class="backend-status-item">`;
2205
2204
  statusHtml += `<span class="status-indicator ${indicatorClass}"></span>`;
2206
- if (entries.length > 1) {
2207
- statusHtml += `${label}: ${statusLabel}${deviceName}`;
2205
+ if (isConn && deviceName) {
2206
+ statusHtml += deviceName;
2208
2207
  } else {
2209
- statusHtml += `${statusLabel}${deviceName}`;
2208
+ const typeName = (info.type || 'unknown');
2209
+ const label = typeName.charAt(0).toUpperCase() + typeName.slice(1);
2210
+ statusHtml += isConn ? label : `${label}: Offline`;
2210
2211
  }
2211
2212
  statusHtml += `</span>`;
2212
2213
  if (idx < entries.length - 1) {
File without changes
File without changes
File without changes