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.
- {meshconsole-3.2.0 → meshconsole-3.2.2}/PKG-INFO +1 -1
- {meshconsole-3.2.0 → meshconsole-3.2.2}/pyproject.toml +1 -1
- {meshconsole-3.2.0 → meshconsole-3.2.2}/src/meshconsole/__init__.py +1 -1
- {meshconsole-3.2.0 → meshconsole-3.2.2}/src/meshconsole/backend/base.py +5 -0
- {meshconsole-3.2.0 → meshconsole-3.2.2}/src/meshconsole/backend/meshtastic.py +21 -9
- {meshconsole-3.2.0 → meshconsole-3.2.2}/src/meshconsole/cli.py +1 -1
- {meshconsole-3.2.0 → meshconsole-3.2.2}/src/meshconsole/core.py +61 -45
- {meshconsole-3.2.0 → meshconsole-3.2.2}/src/meshconsole/templates/index.html +12 -11
- {meshconsole-3.2.0 → meshconsole-3.2.2}/.gitignore +0 -0
- {meshconsole-3.2.0 → meshconsole-3.2.2}/LICENSE +0 -0
- {meshconsole-3.2.0 → meshconsole-3.2.2}/README.md +0 -0
- {meshconsole-3.2.0 → meshconsole-3.2.2}/src/meshconsole/__main__.py +0 -0
- {meshconsole-3.2.0 → meshconsole-3.2.2}/src/meshconsole/autodetect.py +0 -0
- {meshconsole-3.2.0 → meshconsole-3.2.2}/src/meshconsole/backend/__init__.py +0 -0
- {meshconsole-3.2.0 → meshconsole-3.2.2}/src/meshconsole/backend/meshcore.py +0 -0
- {meshconsole-3.2.0 → meshconsole-3.2.2}/src/meshconsole/config.py +0 -0
- {meshconsole-3.2.0 → meshconsole-3.2.2}/src/meshconsole/database.py +0 -0
- {meshconsole-3.2.0 → meshconsole-3.2.2}/src/meshconsole/models.py +0 -0
- {meshconsole-3.2.0 → meshconsole-3.2.2}/src/meshconsole/web.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: meshconsole
|
|
3
|
-
Version: 3.2.
|
|
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
|
|
@@ -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
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
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.
|
|
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
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
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
|
-
|
|
756
|
-
|
|
757
|
-
|
|
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
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
except Exception:
|
|
769
|
+
if hasattr(iface, 'nodes'):
|
|
770
|
+
_ = len(iface.nodes)
|
|
771
|
+
except (BrokenPipeError, OSError, Exception):
|
|
765
772
|
healthy = False
|
|
766
773
|
|
|
767
|
-
|
|
768
|
-
|
|
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
|
-
|
|
780
|
-
|
|
781
|
-
|
|
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"
|
|
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
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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 (
|
|
2207
|
-
statusHtml +=
|
|
2205
|
+
if (isConn && deviceName) {
|
|
2206
|
+
statusHtml += deviceName;
|
|
2208
2207
|
} else {
|
|
2209
|
-
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|