Browse Source

Merge pull request #307 from KomelT/feature/node-quick-options

[Feature request]: Nodes quick options
pull/434/head
Dan Ditomaso 1 year ago
committed by GitHub
parent
commit
3c37899fe4
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 1
      package.json
  2. 18
      pnpm-lock.yaml
  3. 5
      public/devices/README.md
  4. 1
      public/devices/diy.svg
  5. 1
      public/devices/heltec-ht62-esp32c3-sx1262.svg
  6. 1
      public/devices/heltec-mesh-node-t114-case.svg
  7. 1
      public/devices/heltec-mesh-node-t114.svg
  8. 1
      public/devices/heltec-v3-case.svg
  9. 1
      public/devices/heltec-v3.svg
  10. 1
      public/devices/heltec-vision-master-e213.svg
  11. 1
      public/devices/heltec-vision-master-e290.svg
  12. 1
      public/devices/heltec-vision-master-t190.svg
  13. 1
      public/devices/heltec-wireless-paper-V1_0.svg
  14. 1
      public/devices/heltec-wireless-paper.svg
  15. 1
      public/devices/heltec-wireless-tracker-V1-0.svg
  16. 1
      public/devices/heltec-wireless-tracker.svg
  17. 1
      public/devices/heltec-wsl-v3.svg
  18. 1
      public/devices/nano-g2-ultra.svg
  19. 2956
      public/devices/pico.svg
  20. 1
      public/devices/promicro.svg
  21. 1
      public/devices/rak-wismeshtap.svg
  22. 2339
      public/devices/rak11310.svg
  23. 1
      public/devices/rak2560.svg
  24. 3514
      public/devices/rak4631.svg
  25. 1
      public/devices/rak4631_case.svg
  26. 1
      public/devices/rpipicow.svg
  27. 1
      public/devices/seeed-sensecap-indicator.svg
  28. 1
      public/devices/seeed-xiao-s3.svg
  29. 1
      public/devices/station-g2.svg
  30. 1
      public/devices/t-deck.svg
  31. 1
      public/devices/t-echo.svg
  32. 1
      public/devices/t-watch-s3.svg
  33. 1
      public/devices/tbeam-s3-core.svg
  34. 1
      public/devices/tbeam.svg
  35. 1
      public/devices/tlora-c6.svg
  36. 1
      public/devices/tlora-t3s3-epaper.svg
  37. 1
      public/devices/tlora-t3s3-v1.svg
  38. 1
      public/devices/tlora-v2-1-1_6.svg
  39. 1
      public/devices/tlora-v2-1-1_8.svg
  40. 1
      public/devices/tracker-t1000-e.svg
  41. 160
      public/devices/unknown.svg
  42. 1
      public/devices/wio-tracker-wm1110.svg
  43. 1
      public/devices/wm1110_dev_kit.svg
  44. 8
      src/components/Dialog/DialogManager.tsx
  45. 62
      src/components/Dialog/LocationResponseDialog.tsx
  46. 163
      src/components/Dialog/NodeDetailsDialog.tsx
  47. 117
      src/components/Dialog/NodeOptionsDialog.tsx
  48. 57
      src/components/Dialog/TracerouteResponseDialog.tsx
  49. 2
      src/components/PageComponents/Map/NodeDetail.tsx
  50. 48
      src/components/PageComponents/Messages/TraceRoute.tsx
  51. 44
      src/components/UI/Accordion.tsx
  52. 2
      src/components/UI/Dialog.tsx
  53. 53
      src/components/generic/DeviceImage.tsx
  54. 9
      src/components/generic/Table/tmp/TimeAgo.tsx
  55. 66
      src/components/generic/TimeAgo.tsx
  56. 17
      src/components/generic/Uptime.tsx
  57. 22
      src/core/stores/appStore.ts
  58. 5
      src/core/stores/deviceStore.ts
  59. 11
      src/pages/Messages.tsx
  60. 108
      src/pages/Nodes.tsx

1
package.json

@ -65,7 +65,6 @@
"react-map-gl": "7.1.9",
"react-qrcode-logo": "^3.0.0",
"rfc4648": "^1.5.4",
"timeago-react": "^3.0.6",
"vite-plugin-node-polyfills": "^0.23.0",
"zustand": "5.0.3"
},

18
pnpm-lock.yaml

@ -113,9 +113,6 @@ importers:
rfc4648:
specifier: ^1.5.4
version: 1.5.4
timeago-react:
specifier: ^3.0.6
version: 3.0.6([email protected])
vite-plugin-node-polyfills:
specifier: ^0.23.0
version: 0.23.0([email protected])([email protected](@types/[email protected]))
@ -2817,14 +2814,6 @@ packages:
[email protected]:
resolution: {integrity: sha512-iOqSav00cVxEEICeD7TjLB1sueEL+81Wpzp2bY17uZjZN0pWZPuo4suZ/61VujxmqSGFfgOcNuTZ85QJwNZQpw==}
[email protected]:
resolution: {integrity: sha512-4ywnCX3iFjdp84WPK7gt8s4n0FxXbYM+xv8hYL73p83dpcMxzmO+0W4xJuxflnkWNvum5aEaqTe6LZ3lUIudjQ==}
peerDependencies:
react: ^0.14.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0
[email protected]:
resolution: {integrity: sha512-a7wPxPdVlQL7lqvitHGGRsofhdwtkoSXPGATFuSOA2i1ZNQEPLrGnj68vOp2sOJTCFAQVXPeNMX/GctBaO9L2w==}
[email protected]:
resolution: {integrity: sha512-9phl76Cqm6FhSX9Xe1ZUAMLtm1BLkKj2Qd5ApyWkXzsMRaA7dgr81kf4wJmQf/hAvg8EEyJxDo3du/0KlhPiKQ==}
engines: {node: '>=0.6.0'}
@ -6498,13 +6487,6 @@ snapshots:
dependencies:
readable-stream: 3.6.2
[email protected]([email protected]):
dependencies:
react: 19.0.0
timeago.js: 4.0.2
[email protected]: {}
[email protected]:
dependencies:
setimmediate: 1.0.5

5
public/devices/README.md

@ -0,0 +1,5 @@
# Copyright Notice
Copyright © 2024 Meshtastic LLC. All Rights Reserved.
## In reference to the GNU GPLv3 License terms defined in Section 7e
Images (or assets) in this directory are protected under international copyright laws and treaties. Unauthorized reproduction, distribution, modification, or use of these images in any form, commercial or otherwise, outside of official Meshtastic creative works or its Backers and Partners is strictly prohibited without prior written consent from the copyright holder (Meshtastic LLC).

1
public/devices/diy.svg

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 89 KiB

1
public/devices/heltec-ht62-esp32c3-sx1262.svg

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 62 KiB

1
public/devices/heltec-mesh-node-t114-case.svg

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="795.27 277.13 409.46 1319.35"><defs><style>.cls-1{fill:#353535;}.cls-2{fill:#1e1e1d;}.cls-3{fill:#b1a368;}.cls-10,.cls-11,.cls-4,.cls-6,.cls-8,.cls-9{fill:none;}.cls-4,.cls-6{stroke:#050606;}.cls-10,.cls-11,.cls-4,.cls-6,.cls-8{stroke-miterlimit:10;}.cls-4{stroke-width:2.41px;}.cls-5{fill:#30c2db;}.cls-6{stroke-width:3.91px;}.cls-7{fill:#dcf0f2;}.cls-10,.cls-11,.cls-8{stroke:#dcf0f2;}.cls-8{stroke-width:1.81px;}.cls-9{stroke:#17afbf;stroke-linecap:round;stroke-linejoin:round;stroke-width:7.23px;}.cls-10{stroke-width:1.78px;}.cls-11{stroke-width:1.81px;}</style></defs><g id="Layer_7" data-name="Layer 7"><path class="cls-1" d="M915.62,278.34h22.61a35,35,0,0,1,35,35V715.74a0,0,0,0,1,0,0H880.6a0,0,0,0,1,0,0V313.36A35,35,0,0,1,915.62,278.34Z"></path><rect class="cls-2" x="880.6" y="340.15" width="92.65" height="7.54"></rect><rect class="cls-2" x="880.6" y="356.68" width="92.65" height="7.54"></rect><rect class="cls-3" x="885.8" y="844.3" width="84.14" height="19.02"></rect><rect class="cls-3" x="880.6" y="819.07" width="92.65" height="25.23"></rect><rect class="cls-3" x="885.8" y="790.65" width="84.14" height="28.41"></rect><rect class="cls-3" x="880.6" y="723.02" width="92.65" height="67.63"></rect><rect class="cls-3" x="885.8" y="715.74" width="84.14" height="7.28"></rect><rect class="cls-4" x="885.8" y="844.3" width="84.14" height="19.02"></rect><rect class="cls-4" x="880.6" y="819.07" width="92.65" height="25.23"></rect><rect class="cls-4" x="885.8" y="790.65" width="84.14" height="28.41"></rect><rect class="cls-4" x="880.6" y="723.02" width="92.65" height="67.63"></rect><rect class="cls-4" x="885.8" y="715.74" width="84.14" height="7.28"></rect><path class="cls-4" d="M915.62,278.34h22.61a35,35,0,0,1,35,35V715.74a0,0,0,0,1,0,0H880.6a0,0,0,0,1,0,0V313.36A35,35,0,0,1,915.62,278.34Z"></path><rect class="cls-4" x="880.6" y="340.15" width="92.65" height="7.54"></rect><rect class="cls-4" x="880.6" y="356.68" width="92.65" height="7.54"></rect><rect class="cls-5" x="796.48" y="856.3" width="407.05" height="738.98" rx="47.74"></rect><rect class="cls-1" x="900.05" y="973.19" width="202.03" height="354.65" rx="16.4"></rect><rect class="cls-6" x="900.05" y="973.19" width="202.03" height="354.65" rx="16.4"></rect><rect class="cls-7" x="871.51" y="890.41" width="55.42" height="31.12" rx="15.56"></rect><rect class="cls-7" x="1070.16" y="890.41" width="55.42" height="31.12" rx="15.56"></rect><rect class="cls-4" x="871.51" y="890.41" width="55.42" height="31.12" rx="15.56"></rect><rect class="cls-4" x="1070.16" y="890.41" width="55.42" height="31.12" rx="15.56"></rect><circle class="cls-8" cx="841.7" cy="1537.01" r="16.25"></circle><circle class="cls-8" cx="841.7" cy="913.26" r="16.25"></circle><circle class="cls-8" cx="1157.32" cy="913.26" r="16.25"></circle><circle class="cls-8" cx="1157.32" cy="1504.51" r="16.25"></circle><line class="cls-9" x1="942.51" y1="1592.42" x2="942.51" y2="1381.55"></line><line class="cls-9" x1="966.52" y1="1592.42" x2="966.52" y2="1381.55"></line><line class="cls-9" x1="990.57" y1="1592.42" x2="990.57" y2="1381.55"></line><line class="cls-9" x1="1014.59" y1="1592.42" x2="1014.59" y2="1381.55"></line><line class="cls-9" x1="1038.63" y1="1592.42" x2="1038.63" y2="1381.55"></line><line class="cls-9" x1="1062.65" y1="1592.42" x2="1062.65" y2="1381.55"></line><rect class="cls-4" x="796.48" y="856.3" width="407.05" height="738.98" rx="47.74"></rect><path class="cls-10" d="M1040.1,947.74H960.65A13.93,13.93,0,0,1,947,936.64l-10.23-49.2a13.93,13.93,0,0,1,13.64-16.77h97.72a13.93,13.93,0,0,1,13.75,16.18l-8,49.2A13.94,13.94,0,0,1,1040.1,947.74Z"></path><rect class="cls-11" x="816.35" y="870.67" width="365.51" height="703.12" rx="32.37"></rect><rect class="cls-11" x="888.77" y="963.84" width="223.2" height="374.66" rx="25.21"></rect></g></svg>

After

Width:  |  Height:  |  Size: 3.8 KiB

1
public/devices/heltec-mesh-node-t114.svg

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 8.8 KiB

1
public/devices/heltec-v3-case.svg

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="404.68 390.65 1217.15 959.26"><defs><style>.cls-1{fill:#dfeaf7;}.cls-2{fill:#17907f;}.cls-3{fill:#2b2b2b;}.cls-4,.cls-5{fill:none;stroke:#050606;stroke-miterlimit:10;}.cls-4{stroke-width:2.25px;}.cls-5{stroke-width:4px;}.cls-6{fill:#050606;}</style></defs><g id="Layer_5" data-name="Layer 5"><path class="cls-1" d="M1517.73,392.65h0a102.1,102.1,0,0,0-102.1,102.1V770.37A40.62,40.62,0,0,1,1375,811H455.16a48.49,48.49,0,0,0-48.48,48.48v126a11.85,11.85,0,0,0,3.46,8.37l15.34,15.34a11.81,11.81,0,0,1,3.47,8.37v137.16a11.81,11.81,0,0,1-3.47,8.37l-15.34,15.34a11.85,11.85,0,0,0-3.46,8.37v112.67a48.49,48.49,0,0,0,48.48,48.49H1571.34a48.51,48.51,0,0,0,48.49-48.5V494.75A102.1,102.1,0,0,0,1517.73,392.65Zm-110.61,815V954a33.14,33.14,0,0,1,66.27,0v253.65a33.14,33.14,0,0,1-66.27,0Z"></path><path class="cls-2" d="M1516,439.16c-30.23.91-53.92,26.54-53.92,56.79V770.37A87.11,87.11,0,0,1,1375,857.48H732.31A27.51,27.51,0,0,0,704.8,885v388.93a27.51,27.51,0,0,0,27.51,27.51h828.38a12.7,12.7,0,0,0,12.65-12.65v-794A55.69,55.69,0,0,0,1516,439.16Zm-108.9,768.47V954a33.14,33.14,0,0,1,66.27,0v253.65a33.14,33.14,0,0,1-66.27,0Z"></path><rect class="cls-3" x="787.14" y="943.38" width="429.45" height="224.42"></rect><path class="cls-1" d="M1478.6,915.35A54.23,54.23,0,0,0,1386,953.69v254.23a54.23,54.23,0,1,0,108.45,0V953.69A54,54,0,0,0,1478.6,915.35Zm-5.21,292.28a33.14,33.14,0,0,1-66.27,0V954a33.14,33.14,0,0,1,66.27,0Z"></path></g><g id="Layer_2" data-name="Layer 2"><path class="cls-4" d="M1573.34,494.75v794a12.68,12.68,0,0,1-12.65,12.65H732.31a27.51,27.51,0,0,1-27.51-27.51V885a27.51,27.51,0,0,1,27.51-27.51H1375a87.11,87.11,0,0,0,87.11-87.11V496c0-30.25,23.69-55.88,53.92-56.79A55.69,55.69,0,0,1,1573.34,494.75Z"></path><path class="cls-5" d="M410.14,1178.39,425.49,1163a11.78,11.78,0,0,0,3.46-8.35V1017.5a11.8,11.8,0,0,0-3.46-8.35l-15.35-15.36a11.77,11.77,0,0,1-3.46-8.35v-126A48.47,48.47,0,0,1,455.16,811H1375a40.63,40.63,0,0,0,40.63-40.63V494.75a102.1,102.1,0,0,1,102.1-102.1h0a102.1,102.1,0,0,1,102.1,102.1v804.66a48.51,48.51,0,0,1-48.49,48.5H455.16a48.48,48.48,0,0,1-48.48-48.49V1186.74A11.78,11.78,0,0,1,410.14,1178.39Z"></path><rect class="cls-4" x="1407.12" y="920.85" width="66.26" height="319.9" rx="33.13"></rect><rect class="cls-4" x="1386.03" y="899.46" width="108.46" height="362.69" rx="54.23"></rect><path class="cls-6" d="M639.76,1070.55a2.91,2.91,0,0,1-2.91-2.91v-30.53a5.42,5.42,0,0,0-1.6-3.86l-32.44-32.44a11.86,11.86,0,0,1-3.5-8.44V901a12.52,12.52,0,0,0-12.51-12.51H483.92a12.7,12.7,0,0,0-12.68,12.69v76.78a24.13,24.13,0,0,0,7.11,17.18l14.33,14.33a24.13,24.13,0,0,0,17.18,7.11h50.75a11.86,11.86,0,0,1,8.44,3.5l24.26,24.26a12.47,12.47,0,0,1,3.68,8.88v14.46a2.91,2.91,0,1,1-5.81,0v-14.46a6.72,6.72,0,0,0-2-4.77l-24.26-24.26a6.09,6.09,0,0,0-4.33-1.8H509.86a29.91,29.91,0,0,1-21.29-8.81l-14.33-14.33a29.87,29.87,0,0,1-8.81-21.29V901.14a18.51,18.51,0,0,1,18.49-18.5H586.8A18.34,18.34,0,0,1,605.12,901v91.41a6.09,6.09,0,0,0,1.8,4.33l32.44,32.44a11.19,11.19,0,0,1,3.3,8v30.53A2.9,2.9,0,0,1,639.76,1070.55Z"></path><path class="cls-6" d="M586.8,1289.46H483.92a18.51,18.51,0,0,1-18.49-18.5v-76.78a29.87,29.87,0,0,1,8.81-21.29l14.33-14.34a29.91,29.91,0,0,1,21.29-8.81h50.75a6.09,6.09,0,0,0,4.33-1.8l24.26-24.25a6.74,6.74,0,0,0,2-4.78v-14.46a2.91,2.91,0,0,1,5.81,0v14.46a12.51,12.51,0,0,1-3.68,8.89l-24.26,24.25a11.86,11.86,0,0,1-8.44,3.5H509.86a24.17,24.17,0,0,0-17.18,7.11L478.35,1177a24.09,24.09,0,0,0-7.11,17.18V1271a12.71,12.71,0,0,0,12.68,12.69H586.8a12.53,12.53,0,0,0,12.51-12.52v-91.4a11.83,11.83,0,0,1,3.5-8.44l32.44-32.44a5.46,5.46,0,0,0,1.6-3.87v-30.53a2.91,2.91,0,0,1,5.81,0V1135a11.19,11.19,0,0,1-3.3,8l-32.44,32.45a6.06,6.06,0,0,0-1.8,4.33v91.4A18.35,18.35,0,0,1,586.8,1289.46Z"></path><rect class="cls-4" x="787.14" y="943.38" width="429.45" height="224.42"></rect></g></svg>

After

Width:  |  Height:  |  Size: 3.7 KiB

1
public/devices/heltec-v3.svg

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 32 KiB

1
public/devices/heltec-vision-master-e213.svg

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="528.89 806.04 942.22 446.84"><defs><style>.cls-1{fill:#e8eae8;}.cls-2{fill:#dbdddb;}.cls-3{fill:#c6842a;}.cls-4,.cls-5,.cls-7,.cls-9{fill:none;stroke-miterlimit:10;}.cls-4,.cls-5{stroke:#050606;}.cls-4,.cls-9{stroke-width:2.44px;}.cls-5,.cls-7{stroke-width:1.22px;}.cls-6{fill:#b7b7b7;}.cls-7,.cls-9{stroke:#b7b7b7;}.cls-8{fill:#cbcccb;}.cls-10{fill:#434543;}</style></defs><g id="Layer_4" data-name="Layer 4"><path class="cls-1" d="M1469.9,826.68v390.94a19.43,19.43,0,0,1-19.43,19.43H549.53a19.43,19.43,0,0,1-19.43-19.43V826.68a19.42,19.42,0,0,1,19.43-19.42h900.94A19.42,19.42,0,0,1,1469.9,826.68Z"></path><path class="cls-2" d="M574.23,807.26v429.79h-24.7a19.43,19.43,0,0,1-19.43-19.43V826.68a19.42,19.42,0,0,1,19.43-19.42Z"></path><path class="cls-2" d="M1469.9,826.68v390.94a19.43,19.43,0,0,1-19.43,19.43h-37.56V807.26h37.56A19.42,19.42,0,0,1,1469.9,826.68Z"></path><path class="cls-3" d="M574.23,1129.8h-7.47a4.55,4.55,0,0,1-4.55-4.54V919.05a4.55,4.55,0,0,1,4.55-4.55h7.47"></path><rect class="cls-4" x="530.11" y="807.26" width="939.78" height="429.79" rx="19.42"></rect><line class="cls-5" x1="574.23" y1="807.26" x2="574.23" y2="1237.05"></line><path class="cls-5" d="M574.23,1129.8h-7.47a4.55,4.55,0,0,1-4.55-4.54V919.05a4.55,4.55,0,0,1,4.55-4.55h7.47"></path><rect class="cls-6" x="599.01" y="970.1" width="11.52" height="104.11"></rect><rect class="cls-5" x="599.01" y="970.1" width="11.52" height="104.11"></rect><path class="cls-2" d="M610.53,816.78V935.32l38.37,11.55a7.85,7.85,0,0,1,5.6,7.53V1107a7.87,7.87,0,0,1-7.87,7.87h-36.1v110.58H1406V816.78Zm775.41,384.75H631.43a3.66,3.66,0,0,1-3.66-3.66V1138a3.65,3.65,0,0,1,3.66-3.65h28.3a14.7,14.7,0,0,0,14.71-14.71V931a14.7,14.7,0,0,0-14.71-14.71h-28.3a3.65,3.65,0,0,1-3.66-3.66V844.11a3.66,3.66,0,0,1,3.66-3.66h754.51Z"></path><path class="cls-1" d="M1385.94,840.45v361.08H631.43a3.66,3.66,0,0,1-3.66-3.66V1138a3.65,3.65,0,0,1,3.66-3.65h28.3a14.7,14.7,0,0,0,14.71-14.71V931a14.7,14.7,0,0,0-14.71-14.71h-28.3a3.65,3.65,0,0,1-3.66-3.66V844.11a3.66,3.66,0,0,1,3.66-3.66Z"></path><path class="cls-7" d="M610.53,816.78V935.32l38.37,11.55a7.85,7.85,0,0,1,5.6,7.53V1107a7.87,7.87,0,0,1-7.87,7.87h-36.1v110.58H1406V816.78Zm775.41,384.75H631.43a3.66,3.66,0,0,1-3.66-3.66V1138a3.65,3.65,0,0,1,3.66-3.65h28.3a14.7,14.7,0,0,0,14.71-14.71V931a14.7,14.7,0,0,0-14.71-14.71h-28.3a3.65,3.65,0,0,1-3.66-3.66V844.11a3.66,3.66,0,0,1,3.66-3.66h754.51Z"></path><path class="cls-7" d="M1385.94,840.45v361.08H631.43a3.66,3.66,0,0,1-3.66-3.66V1138a3.65,3.65,0,0,1,3.66-3.65h28.3a14.7,14.7,0,0,0,14.71-14.71V931a14.7,14.7,0,0,0-14.71-14.71h-28.3a3.65,3.65,0,0,1-3.66-3.66V844.11a3.66,3.66,0,0,1,3.66-3.66Z"></path><path class="cls-7" d="M1385.94,840.45v361.08H631.43a3.66,3.66,0,0,1-3.66-3.66V1138a3.65,3.65,0,0,1,3.66-3.65h28.3a14.7,14.7,0,0,0,14.71-14.71V931a14.7,14.7,0,0,0-14.71-14.71h-28.3a3.65,3.65,0,0,1-3.66-3.66V844.11a3.66,3.66,0,0,1,3.66-3.66Z"></path><line class="cls-7" x1="666.4" y1="1132.79" x2="666.4" y2="1201.53"></line><line class="cls-7" x1="663.66" y1="916.86" x2="663.66" y2="840.45"></line><circle class="cls-8" cx="644.99" cy="878.66" r="10.7"></circle><circle class="cls-8" cx="644.99" cy="1171.55" r="10.7"></circle><circle class="cls-9" cx="644.99" cy="878.66" r="10.7"></circle><circle class="cls-9" cx="644.99" cy="1171.55" r="10.7"></circle><path class="cls-10" d="M1225,1237.05l2.23,11.19a4.25,4.25,0,0,0,4.17,3.42H1249a4.26,4.26,0,0,0,4.18-3.42l2.22-11.19"></path><path class="cls-10" d="M1306.87,1237.05l2.22,11.19a4.26,4.26,0,0,0,4.18,3.42h17.62a4.25,4.25,0,0,0,4.17-3.42l2.22-11.19"></path><path class="cls-10" d="M1388.77,1237.05l2.23,11.19a4.24,4.24,0,0,0,4.17,3.42h17.62a4.25,4.25,0,0,0,4.17-3.42l2.23-11.19"></path><path class="cls-4" d="M1225,1237.05l2.23,11.19a4.25,4.25,0,0,0,4.17,3.42H1249a4.26,4.26,0,0,0,4.18-3.42l2.22-11.19"></path><path class="cls-4" d="M1306.87,1237.05l2.22,11.19a4.26,4.26,0,0,0,4.18,3.42h17.62a4.25,4.25,0,0,0,4.17-3.42l2.22-11.19"></path><path class="cls-4" d="M1388.77,1237.05l2.23,11.19a4.24,4.24,0,0,0,4.17,3.42h17.62a4.25,4.25,0,0,0,4.17-3.42l2.23-11.19"></path><line class="cls-4" x1="1412.9" y1="807.26" x2="1412.9" y2="1237.05"></line></g></svg>

After

Width:  |  Height:  |  Size: 4.1 KiB

1
public/devices/heltec-vision-master-e290.svg

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 6.1 KiB

1
public/devices/heltec-vision-master-t190.svg

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="479.57 786.58 1040.84 433.17"><defs><style>.cls-1{fill:#cccccb;}.cls-2{fill:#2b2b2b;}.cls-3,.cls-6,.cls-7,.cls-8{fill:none;stroke-miterlimit:10;}.cls-3,.cls-6,.cls-7{stroke:#050606;}.cls-3,.cls-8{stroke-width:1.65px;}.cls-4{fill:#40403f;}.cls-5{fill:#ddd;}.cls-6{stroke-width:1.62px;}.cls-7{stroke-width:1.64px;}.cls-8{stroke:#fff;}.cls-9{fill:#353535;}.cls-10{fill:#c08c2d;}</style></defs><g id="Layer_4" data-name="Layer 4"><path class="cls-1" d="M595,923.44H519.6A10.46,10.46,0,0,1,509.14,913V802.7a15.28,15.28,0,0,1,15.28-15.28h979.89a15.29,15.29,0,0,1,15.29,15.29v400.93a15.3,15.3,0,0,1-15.29,15.29H524.42a15.28,15.28,0,0,1-15.28-15.28V1102.1a10.46,10.46,0,0,1,10.46-10.46H595"></path><rect class="cls-2" x="611.37" y="796.48" width="819.83" height="411.47"></rect><line class="cls-3" x1="1441.99" y1="787.41" x2="1441.99" y2="1218.92"></line><path class="cls-4" d="M620.91,851.7v302.78a1.87,1.87,0,0,1-1.87,1.87h-13.8a8.7,8.7,0,0,1-.89,0,10.23,10.23,0,0,1-9.35-10.2v-286a10.24,10.24,0,0,1,9.35-10.2,8.7,8.7,0,0,1,.89,0H619A1.87,1.87,0,0,1,620.91,851.7Z"></path><rect class="cls-5" x="480.4" y="942.42" width="114.6" height="127.58"></rect><rect class="cls-3" x="480.4" y="942.42" width="114.6" height="127.58"></rect><path class="cls-6" d="M595,923.44H519.6A10.46,10.46,0,0,1,509.14,913V802.7a15.28,15.28,0,0,1,15.28-15.28h979.89a15.29,15.29,0,0,1,15.29,15.29v400.93a15.3,15.3,0,0,1-15.29,15.29H524.42a15.28,15.28,0,0,1-15.28-15.28V1102.1a10.46,10.46,0,0,1,10.46-10.46H595"></path><path class="cls-2" d="M584.65,970.14H595a0,0,0,0,1,0,0v24.44a0,0,0,0,1,0,0H584.65a3,3,0,0,1-3-3V973.11A3,3,0,0,1,584.65,970.14Z"></path><rect class="cls-2" x="560.58" y="976.5" width="9.04" height="16.58" rx="2.72"></rect><path class="cls-2" d="M584.65,1026.63H595a0,0,0,0,1,0,0v24.44a0,0,0,0,1,0,0H584.65a3,3,0,0,1-3-3v-18.49A3,3,0,0,1,584.65,1026.63Z"></path><rect class="cls-2" x="560.58" y="1028.91" width="9.04" height="16.58" rx="2.72"></rect><path class="cls-3" d="M584.65,970.14H595a0,0,0,0,1,0,0v24.44a0,0,0,0,1,0,0H584.65a3,3,0,0,1-3-3V973.11A3,3,0,0,1,584.65,970.14Z"></path><rect class="cls-3" x="560.58" y="976.5" width="9.04" height="16.58" rx="2.72"></rect><path class="cls-3" d="M584.65,1026.63H595a0,0,0,0,1,0,0v24.44a0,0,0,0,1,0,0H584.65a3,3,0,0,1-3-3v-18.49A3,3,0,0,1,584.65,1026.63Z"></path><rect class="cls-3" x="560.58" y="1028.91" width="9.04" height="16.58" rx="2.72"></rect><polyline class="cls-7" points="611.37 1156.35 611.37 1207.95 1431.2 1207.95 1431.2 796.48 611.37 796.48 611.37 849.83"></polyline><line class="cls-3" x1="611.37" y1="1207.95" x2="611.37" y2="1218.93"></line><line class="cls-3" x1="611.37" y1="796.48" x2="611.37" y2="787.42"></line><rect class="cls-8" x="560.58" y="1107.17" width="18.37" height="41.67" rx="4.91"></rect><rect class="cls-8" x="528.51" y="1107.17" width="18.37" height="41.67" rx="4.91"></rect><rect class="cls-8" x="560.58" y="1166.28" width="18.37" height="41.67" rx="4.91"></rect><rect class="cls-8" x="528.51" y="1166.28" width="18.37" height="41.67" rx="4.91"></rect><rect class="cls-8" x="560.58" y="804.46" width="18.37" height="41.67" rx="4.91"></rect><rect class="cls-8" x="528.51" y="804.46" width="18.37" height="41.67" rx="4.91"></rect><rect class="cls-8" x="560.58" y="863.57" width="18.37" height="41.67" rx="4.91"></rect><rect class="cls-8" x="528.51" y="863.57" width="18.37" height="41.67" rx="4.91"></rect><circle class="cls-8" cx="1476.63" cy="831.74" r="27.15"></circle><circle class="cls-8" cx="1476.63" cy="1173.49" r="27.15"></circle><rect class="cls-9" x="676.15" y="804.6" width="742.79" height="396.03"></rect><path class="cls-10" d="M604.35,849.87v306.44a10.23,10.23,0,0,1-9.35-10.2v-286A10.24,10.24,0,0,1,604.35,849.87Z"></path><path class="cls-3" d="M605.24,849.83H619a1.87,1.87,0,0,1,1.87,1.87v302.78a1.87,1.87,0,0,1-1.87,1.87h-13.8A10.24,10.24,0,0,1,595,1146.11v-286A10.24,10.24,0,0,1,605.24,849.83Z"></path><rect class="cls-6" x="676.15" y="804.6" width="742.79" height="396.03"></rect></g></svg>

After

Width:  |  Height:  |  Size: 3.9 KiB

1
public/devices/heltec-wireless-paper-V1_0.svg

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 9.9 KiB

1
public/devices/heltec-wireless-paper.svg

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 9.9 KiB

1
public/devices/heltec-wireless-tracker-V1-0.svg

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 83 KiB

1
public/devices/heltec-wireless-tracker.svg

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 83 KiB

1
public/devices/heltec-wsl-v3.svg

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 43 KiB

1
public/devices/nano-g2-ultra.svg

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 8.3 KiB

2956
public/devices/pico.svg

File diff suppressed because it is too large

After

Width:  |  Height:  |  Size: 102 KiB

1
public/devices/promicro.svg

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 71 KiB

1
public/devices/rak-wismeshtap.svg

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 5.3 KiB

2339
public/devices/rak11310.svg

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 164 KiB

1
public/devices/rak2560.svg

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 11 KiB

3514
public/devices/rak4631.svg

File diff suppressed because it is too large

After

Width:  |  Height:  |  Size: 128 KiB

1
public/devices/rak4631_case.svg

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 9.0 KiB

1
public/devices/rpipicow.svg

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 76 KiB

1
public/devices/seeed-sensecap-indicator.svg

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 8.7 KiB

1
public/devices/seeed-xiao-s3.svg

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 28 KiB

1
public/devices/station-g2.svg

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 31 KiB

1
public/devices/t-deck.svg

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 23 KiB

1
public/devices/t-echo.svg

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 8.0 KiB

1
public/devices/t-watch-s3.svg

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="733.42 451.82 573.87 931.48"><defs><style>.cls-1{fill:#8e8d8e;}.cls-2{fill:#383839;}.cls-3{fill:#cccccb;}.cls-4{fill:#222226;}.cls-5,.cls-6{fill:none;stroke:#050606;stroke-miterlimit:10;}.cls-5{stroke-width:1.87px;}.cls-6{stroke-width:3.77px;}.cls-7{fill:#4c4c4d;}</style></defs><g id="Layer_4" data-name="Layer 4"><path class="cls-1" d="M1277.27,847.59h4.35a8.09,8.09,0,0,1,8.09,8.08v138a8.09,8.09,0,0,1-8.09,8.09h-4.35"></path><path class="cls-1" d="M1277.27,732.73h18a10.14,10.14,0,0,1,10.14,10.14v43A10.14,10.14,0,0,1,1295.26,796h-18a0,0,0,0,1,0,0V732.73A0,0,0,0,1,1277.27,732.73Z"></path><path class="cls-2" d="M1256.49,1200.6h0a14.19,14.19,0,0,1-2.83,12.5c-8.13,9.86-19.94,18.58-46,30.75-19.15,9-28.65,16-38.35,29.6a93.15,93.15,0,0,0-8.7,14.61c-6.95,15.17-11.77,44.44-11.77,65.66v3.61a24.09,24.09,0,0,1-24.1,24.09H887.83a24.09,24.09,0,0,1-24.1-24.09v-3.61c0-21.22-4.82-50.49-11.77-65.66a93.15,93.15,0,0,0-8.7-14.61c-9.7-13.63-19.2-20.65-38.35-29.6-26.06-12.17-37.87-20.89-46-30.75a14.22,14.22,0,0,1-2.82-12.5h0"></path><path class="cls-2" d="M756.09,634.53h0a14.19,14.19,0,0,1,2.83-12.5c8.12-9.86,19.93-18.58,46-30.75,19.15-8.95,28.65-16,38.35-29.6a93.15,93.15,0,0,0,8.7-14.61c6.95-15.17,11.77-44.44,11.77-65.66V477.8a24.09,24.09,0,0,1,24.1-24.09h236.92a24.09,24.09,0,0,1,24.1,24.09v3.61c0,21.22,4.82,50.49,11.77,65.66a93.15,93.15,0,0,0,8.7,14.61c9.7,13.63,19.2,20.65,38.35,29.6,26,12.17,37.86,20.89,46,30.75a14.19,14.19,0,0,1,2.83,12.5h0"></path><rect class="cls-3" x="735.31" y="598.25" width="541.96" height="638.99" rx="96.44"></rect><path class="cls-2" d="M1247.38,694.68v446.11a66.63,66.63,0,0,1-66.54,66.56H831.75a66.62,66.62,0,0,1-66.56-66.56V694.68a66.63,66.63,0,0,1,66.56-66.55h349.09A66.64,66.64,0,0,1,1247.38,694.68Z"></path><rect class="cls-4" x="817.71" y="721.76" width="379.03" height="388.6"></rect><path class="cls-5" d="M1247.38,694.68v446.11a66.63,66.63,0,0,1-66.54,66.56H831.75a66.62,66.62,0,0,1-66.56-66.56V694.68a66.63,66.63,0,0,1,66.56-66.55h349.09A66.64,66.64,0,0,1,1247.38,694.68Z"></path><rect class="cls-6" x="735.31" y="598.25" width="541.96" height="638.99" rx="96.44"></rect><path class="cls-6" d="M1256.49,1200.6h0a14.19,14.19,0,0,1-2.83,12.5c-8.13,9.86-19.94,18.58-46,30.75-19.15,9-28.65,16-38.35,29.6a93.15,93.15,0,0,0-8.7,14.61c-6.95,15.17-11.77,44.44-11.77,65.66v3.61a24.09,24.09,0,0,1-24.1,24.09H887.83a24.09,24.09,0,0,1-24.1-24.09v-3.61c0-21.22-4.82-50.49-11.77-65.66a93.15,93.15,0,0,0-8.7-14.61c-9.7-13.63-19.2-20.65-38.35-29.6-26.06-12.17-37.87-20.89-46-30.75a14.22,14.22,0,0,1-2.82-12.5h0"></path><path class="cls-6" d="M756.09,634.53h0a14.19,14.19,0,0,1,2.83-12.5c8.12-9.86,19.93-18.58,46-30.75,19.15-8.95,28.65-16,38.35-29.6a93.15,93.15,0,0,0,8.7-14.61c6.95-15.17,11.77-44.44,11.77-65.66V477.8a24.09,24.09,0,0,1,24.1-24.09h236.92a24.09,24.09,0,0,1,24.1,24.09v3.61c0,21.22,4.82,50.49,11.77,65.66a93.15,93.15,0,0,0,8.7,14.61c9.7,13.63,19.2,20.65,38.35,29.6,26,12.17,37.86,20.89,46,30.75a14.19,14.19,0,0,1,2.83,12.5h0"></path><rect class="cls-5" x="817.71" y="721.76" width="379.03" height="388.6"></rect><path class="cls-6" d="M1277.27,847.59h4.35a8.09,8.09,0,0,1,8.09,8.08v138a8.09,8.09,0,0,1-8.09,8.09h-4.35"></path><path class="cls-6" d="M1277.27,732.73h18a10.14,10.14,0,0,1,10.14,10.14v43A10.14,10.14,0,0,1,1295.26,796h-18a0,0,0,0,1,0,0V732.73A0,0,0,0,1,1277.27,732.73Z"></path><circle class="cls-7" cx="1083.08" cy="1177.35" r="16.6"></circle><rect class="cls-2" x="1280.24" y="739.77" width="16.77" height="4.59" rx="2.29"></rect><rect class="cls-2" x="1280.24" y="750.91" width="16.77" height="4.59" rx="2.29"></rect><rect class="cls-2" x="1280.24" y="762.06" width="16.77" height="4.59" rx="2.29"></rect><rect class="cls-2" x="1280.24" y="773.2" width="16.77" height="4.59" rx="2.29"></rect><rect class="cls-2" x="1280.24" y="784.34" width="16.77" height="4.59" rx="2.29"></rect></g></svg>

After

Width:  |  Height:  |  Size: 3.8 KiB

1
public/devices/tbeam-s3-core.svg

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 70 KiB

1
public/devices/tbeam.svg

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 112 KiB

1
public/devices/tlora-c6.svg

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 30 KiB

1
public/devices/tlora-t3s3-epaper.svg

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 6.0 KiB

1
public/devices/tlora-t3s3-v1.svg

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 30 KiB

1
public/devices/tlora-v2-1-1_6.svg

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 26 KiB

1
public/devices/tlora-v2-1-1_8.svg

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 26 KiB

1
public/devices/tracker-t1000-e.svg

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 6.9 KiB

160
public/devices/unknown.svg

@ -0,0 +1,160 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
class="svg-icon"
style="overflow:hidden;fill:currentColor"
viewBox="0 0 909.87988 546.85529"
version="1.1"
id="svg3"
xml:space="preserve"
width="909.87988"
height="546.85529"
sodipodi:docname="unknown.svg"
inkscape:version="1.4 (e7c3feb1, 2024-10-09)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"><sodipodi:namedview
id="namedview1"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:zoom="0.57169944"
inkscape:cx="291.23695"
inkscape:cy="107.57401"
inkscape:window-width="1472"
inkscape:window-height="890"
inkscape:window-x="0"
inkscape:window-y="38"
inkscape:window-maximized="1"
inkscape:current-layer="Layer_7" /><defs
id="defs3"><style
id="style1">.cls-1{fill:#383838;}.cls-2{fill:#9f9f9e;}.cls-3{fill:#cbcccb;}.cls-4{fill:#b7b7b7;}.cls-5{fill:#353535;}.cls-6{fill:#b1a368;}.cls-7{fill:#2c2d2d;}.cls-10,.cls-11,.cls-8,.cls-9{fill:none;stroke:#050606;}.cls-10,.cls-11,.cls-8{stroke-miterlimit:10;}.cls-8,.cls-9{stroke-width:2px;}.cls-9{stroke-linecap:round;stroke-linejoin:round;}.cls-10{stroke-width:2.04px;}.cls-11{stroke-width:1.99px;}.cls-12{fill:#c08c2d;}.cls-13{fill:#af7a2b;}</style></defs><g
id="Layer_7"
data-name="Layer 7"
transform="translate(-646.6554,-758.05941)"><path
class="cls-2"
d="m 1545.1753,893.49468 h 4.69 a 5.67,5.67 0 0 1 5.67,5.67 v 84.64998 a 5.67,5.67 0 0 1 -5.67,5.67 h -4.69"
id="path1-4" /><rect
class="cls-3"
x="647.6554"
y="862.80469"
width="897.52002"
height="441.10999"
rx="11.7"
id="rect2" /><path
class="cls-2"
d="m 681.12532,862.80468 v 113.47998 a 3.67,3.67 0 0 0 3.67,3.67 h 41 a 2.35,2.35 0 0 1 2.35,2.35 V 1303.9147 H 1517.6053 V 862.80468 Z M 1492.6453,1278.9147 H 753.18532 V 972.01466 a 17.06,17.06 0 0 0 -17.06,-17.06 h -27.5 a 2.5,2.5 0 0 1 -2.5,-2.5 v -62.14998 a 2.5,2.5 0 0 1 2.5,-2.5 h 783.99998 z"
id="path2-7" /><path
class="cls-3"
d="M 1492.6453,887.80468 V 1278.9147 H 753.18532 V 972.01466 a 17,17 0 0 0 -7.2,-13.92 v -70.28998 z"
id="path3-7"
style="fill:#ffffff" /><path
class="cls-4"
d="m 745.98532,887.80468 v 70.28998 a 17,17 0 0 0 -9.86,-3.14 h -27.5 a 2.5,2.5 0 0 1 -2.5,-2.5 v -62.14998 a 2.5,2.5 0 0 1 2.5,-2.5 z"
id="path4" /><rect
class="cls-2"
x="672.10535"
y="1011.4448"
width="13.53"
height="148.39999"
id="rect4" /><path
class="cls-6"
d="m 1077.2923,853.76468 h 71.71 a 2.55,2.55 0 0 1 2.55,2.55 v 6.48 h -76.8 v -6.48 a 2.55,2.55 0 0 1 2.54,-2.55 z"
id="path7" /><path
class="cls-8"
d="m 1082.9205,761.22647 h 60.8838 a 6.1958134,4.8451518 0 0 1 6.1958,4.84516 v 77.32638 h -73.2754 v -77.32638 a 6.1958134,4.8451518 0 0 1 6.1958,-4.84516 z"
id="path39"
style="fill:#b1a368" /><rect
class="cls-8"
x="1066.9833"
y="778.19855"
width="91.504646"
height="55.957298"
rx="5.5511622"
id="rect39"
style="fill:#b1a368" /><path
class="cls-2"
d="m 1158.4522,782.53954 v 47.24724 a 5.5153484,4.3130254 0 0 1 -5.5512,4.34102 h -80.3665 a 5.5511623,4.341032 0 0 1 -5.587,-4.34102 v -47.24724 a 5.5511623,4.341032 0 0 1 5.587,-4.34103 h 80.5098 a 5.5153484,4.3130254 0 0 1 5.4079,4.34103 z"
id="path41-4"
style="fill:none;stroke:#050606;stroke-width:3.16706;stroke-miterlimit:10" /><rect
class="cls-6"
x="1079.9424"
y="843.73468"
width="65.989998"
height="10.03"
id="rect8" /><path
class="cls-8"
d="M 1492.6453,887.80468 V 1278.9147 H 753.18532 V 972.01466 a 17.06,17.06 0 0 0 -17.06,-17.06 h -27.5 a 2.5,2.5 0 0 1 -2.5,-2.5 v -62.14998 a 2.5,2.5 0 0 1 2.5,-2.5 h 783.99998 m 25,-25 H 681.12532 v 113.47998 a 3.68,3.68 0 0 0 3.67,3.67 h 41 a 2.35,2.35 0 0 1 2.35,2.35 V 1303.9147 H 1517.6053 V 862.80468 Z"
id="path10" /><line
class="cls-8"
x1="745.99536"
y1="958.09467"
x2="745.99536"
y2="887.80469"
id="line10" /><rect
class="cls-8"
x="672.10535"
y="1011.4448"
width="13.53"
height="148.39999"
id="rect11" /><path
class="cls-8"
d="m 1545.1753,893.49468 h 4.69 a 5.67,5.67 0 0 1 5.67,5.67 v 84.64998 a 5.67,5.67 0 0 1 -5.67,5.67 h -4.69"
id="path14" /><path
class="cls-10"
d="m 1077.2923,853.76468 h 71.71 a 2.55,2.55 0 0 1 2.55,2.55 v 6.48 h -76.8 v -6.48 a 2.55,2.55 0 0 1 2.54,-2.55 z"
id="path16" /><rect
class="cls-11"
x="1079.9424"
y="843.73468"
width="65.989998"
height="10.03"
id="rect17" /><path
class="cls-2"
d="m 725.27532,910.38466 a 14,14 0 1 0 14,14 13.95,13.95 0 0 0 -14,-14 z m 0,21.5 a 7.55,7.55 0 1 1 7.54,-7.55 7.55,7.55 0 0 1 -7.54,7.55 z"
id="path19" /><circle
class="cls-8"
cx="725.27539"
cy="924.33466"
r="7.5500002"
id="circle19" /><circle
class="cls-8"
cx="725.27539"
cy="924.33466"
r="13.95"
id="circle20" /><path
d="m 445.36309,440.05365 c 0,11.52004 10.38375,20.85861 23.19309,20.85861 12.80937,0 23.19311,-9.33857 23.19311,-20.85861 0,-11.52005 -10.38374,-20.85861 -23.19311,-20.85861 -12.80934,0 -23.19309,9.33856 -23.19309,20.85861 z"
fill="#ccc"
id="path1"
style="overflow:hidden;fill:#4d4d4d;stroke-width:0.458227"
transform="translate(646.6554,758.05941)" /><path
d="m 469.40305,538.40107 c -119.83415,0 -217.31582,-93.40624 -217.31582,-208.23067 0,-114.82425 97.48167,-208.23058 217.31582,-208.23058 119.83417,0 217.31585,93.40633 217.31585,208.23058 0,114.82443 -97.48168,208.23067 -217.31585,208.23067 z m 0,-386.58065 c -102.63515,0 -186.13149,80.00572 -186.13149,178.34998 0,98.32948 83.49634,178.34997 186.13149,178.34997 102.61966,0 186.13151,-80.01997 186.13151,-178.34997 0,-98.34426 -83.51185,-178.34998 -186.13151,-178.34998 z"
fill="#ccc"
id="path2"
style="overflow:hidden;fill:#4d4d4d;stroke-width:0.474832"
transform="translate(646.6554,758.05941)" /><path
d="m 468.55618,391.96713 c -8.53552,0 -15.46205,-6.22977 -15.46205,-13.90533 v -23.51468 c 0,-22.75028 19.32709,-40.13201 36.39722,-55.47009 12.50833,-11.26363 25.45056,-22.88885 25.45056,-32.16398 0,-23.18095 -20.81195,-42.03718 -46.38573,-42.03718 -26.0067,0 -46.38619,18.0497 -46.38619,41.09158 0,7.67594 -6.92654,13.90533 -15.46208,13.90533 -8.53554,0 -15.46207,-6.22977 -15.46207,-13.9058 0,-37.99002 34.68046,-68.90262 77.31034,-68.90262 42.62989,0 77.31034,31.32967 77.31034,69.84869 0,20.81694 -17.54944,36.5856 -34.51132,51.84064 -13.452,12.07016 -27.33645,24.55758 -27.33645,35.77907 v 23.51468 c 0,7.6764 -6.92702,13.91969 -15.46257,13.91969 z"
fill="#ccc"
id="path3"
style="overflow:hidden;fill:#4d4d4d;stroke-width:0.458227;stroke:#000000;stroke-opacity:1"
transform="translate(646.6554,758.05941)" /><rect
class="cls-8"
x="647.6554"
y="862.80469"
width="897.52002"
height="441.10999"
rx="11.7"
id="rect28" /></g><path
style="fill:#ffffff;fill-opacity:0;stroke-width:0.92"
d="m 107.41785,363.03448 -0.23786,-156.30546 -2.99,-3.72057 -2.99,-3.72058 v -34.09394 -34.09394 l 150.64998,0.048 150.64999,0.048 -8.28,3.06943 c -19.31509,7.16019 -34.46167,14.82453 -50.21721,25.41044 -50.57644,33.98158 -84.35747,88.86991 -91.06203,147.96009 -1.43336,12.63279 -0.63536,44.7022 1.3876,55.76392 7.76201,42.44321 25.98398,77.92651 55.67763,108.41989 17.37837,17.84644 33.98994,30.29944 55.42867,41.55255 l 11.30534,5.93414 -134.54212,0.0167 -134.54213,0.0167 z"
id="path11" /><path
style="fill:#ffffff;fill-opacity:0;stroke-width:0.92"
d="m 107.41785,363.03448 -0.23786,-156.30546 -2.99,-3.72057 -2.99,-3.72058 v -34.09394 -34.09394 l 150.64998,0.048 150.64999,0.048 -8.28,3.06943 c -19.31509,7.16019 -34.46167,14.82453 -50.21721,25.41044 -50.57644,33.98158 -84.35747,88.86991 -91.06203,147.96009 -1.43336,12.63279 -0.63536,44.7022 1.3876,55.76392 7.76201,42.44321 25.98398,77.92651 55.67763,108.41989 17.37837,17.84644 33.98994,30.29944 55.42867,41.55255 l 11.30534,5.93414 -134.54212,0.0167 -134.54213,0.0167 z"
id="path12" /><path
style="fill:#ffffff;fill-opacity:0;stroke-width:0.92"
d="m 107.41785,363.03448 -0.23786,-156.30546 -2.99,-3.72057 -2.99,-3.72058 v -34.09394 -34.09394 l 150.64998,0.048 150.64999,0.048 -8.28,3.06943 c -19.31509,7.16019 -34.46167,14.82453 -50.21721,25.41044 -50.57644,33.98158 -84.35747,88.86991 -91.06203,147.96009 -1.43336,12.63279 -0.63536,44.7022 1.3876,55.76392 7.76201,42.44321 25.98398,77.92651 55.67763,108.41989 17.37837,17.84644 33.98994,30.29944 55.42867,41.55255 l 11.30534,5.93414 -134.54212,0.0167 -134.54213,0.0167 z"
id="path13" /></svg>

After

Width:  |  Height:  |  Size: 9.1 KiB

1
public/devices/wio-tracker-wm1110.svg

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 91 KiB

1
public/devices/wm1110_dev_kit.svg

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 120 KiB

8
src/components/Dialog/DialogManager.tsx

@ -6,6 +6,8 @@ import { QRDialog } from "@components/Dialog/QRDialog.tsx";
import { RebootDialog } from "@components/Dialog/RebootDialog.tsx";
import { ShutdownDialog } from "@components/Dialog/ShutdownDialog.tsx";
import { useDevice } from "@core/stores/deviceStore.ts";
import type { JSX } from "react";
import { NodeDetailsDialog } from "./NodeDetailsDialog";
export const DialogManager = (): JSX.Element => {
const { channels, config, dialog, setDialogOpen } = useDevice();
@ -56,6 +58,12 @@ export const DialogManager = (): JSX.Element => {
setDialogOpen("pkiBackup", open);
}}
/>
<NodeDetailsDialog
open={dialog.nodeDetails}
onOpenChange={(open) => {
setDialogOpen("nodeDetails", open);
}}
/>
</>
);
};

62
src/components/Dialog/LocationResponseDialog.tsx

@ -0,0 +1,62 @@
import { useDevice } from "@app/core/stores/deviceStore";
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
} from "@components/UI/Dialog";
import type { Protobuf, Types } from "@meshtastic/js";
import { numberToHexUnpadded } from "@noble/curves/abstract/utils";
import type { JSX } from "react";
export interface LocationResponseDialogProps {
location: Types.PacketMetadata<Protobuf.Mesh.location> | undefined;
open: boolean;
onOpenChange: () => void;
}
export const LocationResponseDialog = ({
location,
open,
onOpenChange,
}: LocationResponseDialogProps): JSX.Element => {
const { nodes } = useDevice();
const from = nodes.get(location?.from ?? 0);
const longName =
from?.user?.longName ??
(from ? `!${numberToHexUnpadded(from?.num)}` : "Unknown");
const shortName =
from?.user?.shortName ??
(from ? `${numberToHexUnpadded(from?.num).substring(0, 4)}` : "UNK");
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent>
<DialogHeader>
<DialogTitle>{`Location: ${longName} (${shortName})`}</DialogTitle>
</DialogHeader>
<DialogDescription>
<div className="ml-5 flex">
<span className="ml-4 border-l-2 border-l-backgroundPrimary pl-2 text-textPrimary">
<p>
Coordinates:{" "}
<a
className="text-blue-500 dark:text-blue-400"
href={`https://www.openstreetmap.org/?mlat=${location?.data.latitudeI / 1e7}&mlon=${location?.data.longitudeI / 1e7}&layers=N`}
target="_blank"
rel="noreferrer"
>
{location?.data.latitudeI / 1e7},{" "}
{location?.data.longitudeI / 1e7}
</a>
</p>
<p>Altitude: {location?.data.altitude}m</p>
</span>
</div>
</DialogDescription>
</DialogContent>
</Dialog>
);
};

163
src/components/Dialog/NodeDetailsDialog.tsx

@ -0,0 +1,163 @@
import { useAppStore } from "@app/core/stores/appStore";
import { useDevice } from "@app/core/stores/deviceStore";
import {
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger,
} from "@components/UI/Accordion";
import {
Dialog,
DialogContent,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@components/UI/Dialog";
import { Protobuf } from "@meshtastic/js";
import { numberToHexUnpadded } from "@noble/curves/abstract/utils";
import { DeviceImage } from "../generic/DeviceImage";
import { TimeAgo } from "../generic/TimeAgo";
import { Uptime } from "../generic/Uptime";
export interface NodeDetailsDialogProps {
open: boolean;
onOpenChange: (open: boolean) => void;
}
export const NodeDetailsDialog = ({
open,
onOpenChange,
}: NodeDetailsDialogProps) => {
const { nodes } = useDevice();
const { nodeNumDetails } = useAppStore();
const device: Protobuf.Mesh.NodeInfo = nodes.get(nodeNumDetails);
return device ? (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent>
<DialogHeader>
<DialogTitle>
Node Details for {device.user?.longName ?? "UNKNOWN"} (
{device.user?.shortName ?? "UNK"})
</DialogTitle>
</DialogHeader>
<DialogFooter>
<div className="w-full">
<DeviceImage
className="w-32 h-32 mx-auto rounded-lg border-4 border-gray-200 dark:border-gray-800"
deviceType={
Protobuf.Mesh.HardwareModel[device.user?.hwModel ?? 0]
}
/>
<div className="mt-5 bg-gray-100 dark:bg-slate-800 p-3 rounded-lg mt-3">
<p className="text-lg font-semibold text-slate-900 dark:text-slate-50">
Details:
</p>
<p>
Hardware:{" "}
{Protobuf.Mesh.HardwareModel[device.user?.hwModel ?? 0]}
</p>
<p>Node Number: {device.num}</p>
<p>Node HEX: !{numberToHexUnpadded(device.num)}</p>
<p>
Role:{" "}
{
Protobuf.Config.Config_DeviceConfig_Role[
device.user?.role ?? 0
]
}
</p>
<p>
Last Heard:{" "}
{device.lastHeard === 0 ? (
"Never"
) : (
<TimeAgo timestamp={device.lastHeard * 1000} />
)}
</p>
</div>
{device.position ? (
<div className="mt-5 bg-gray-100 dark:bg-slate-800 p-3 rounded-lg mt-3">
<p className="text-lg font-semibold text-slate-900 dark:text-slate-50">
Position:
</p>
{device.position.latitudeI && device.position.longitudeI ? (
<p>
Coordinates:{" "}
<a
className="text-blue-500 dark:text-blue-400"
href={`https://www.openstreetmap.org/?mlat=${device.position.latitudeI / 1e7}&mlon=${device.position.longitudeI / 1e7}&layers=N`}
target="_blank"
rel="noreferrer"
>
{device.position.latitudeI / 1e7},{" "}
{device.position.longitudeI / 1e7}
</a>
</p>
) : null}
{device.position.altitude ? (
<p>Altitude: {device.position.altitude}m</p>
) : null}
</div>
) : null}
{device.deviceMetrics ? (
<div className="mt-5 bg-gray-100 dark:bg-slate-800 p-3 rounded-lg mt-3">
<p className="text-lg font-semibold text-slate-900 dark:text-slate-50">
Device Metrics:
</p>
{device.deviceMetrics.airUtilTx ? (
<p>
Air TX utilization:{" "}
{device.deviceMetrics.airUtilTx.toFixed(2)}%
</p>
) : null}
{device.deviceMetrics.channelUtilization ? (
<p>
Channel utilization:{" "}
{device.deviceMetrics.channelUtilization.toFixed(2)}%
</p>
) : null}
{device.deviceMetrics.batteryLevel ? (
<p>
Battery level:{" "}
{device.deviceMetrics.batteryLevel.toFixed(2)}%
</p>
) : null}
{device.deviceMetrics.voltage ? (
<p>Voltage: {device.deviceMetrics.voltage.toFixed(2)}V</p>
) : null}
{device.deviceMetrics.uptimeSeconds ? (
<p>
Uptime:{" "}
<Uptime seconds={device.deviceMetrics.uptimeSeconds} />
</p>
) : null}
</div>
) : null}
{device ? (
<div className="mt-5 w-full max-w-[464px] bg-gray-100 dark:bg-slate-800 p-3 rounded-lg mt-3">
<Accordion className="AccordionRoot" type="single" collapsible>
<AccordionItem className="AccordionItem" value="item-1">
<AccordionTrigger>
<p className="text-lg font-semibold text-slate-900 dark:text-slate-50">
All Raw Metrics:
</p>
</AccordionTrigger>
<AccordionContent className="overflow-x-scroll">
<pre className="text-xs w-full">
{JSON.stringify(device, null, 2)}
</pre>
</AccordionContent>
</AccordionItem>
</Accordion>
</div>
) : null}
</div>
</DialogFooter>
</DialogContent>
</Dialog>
) : null;
};

117
src/components/Dialog/NodeOptionsDialog.tsx

@ -0,0 +1,117 @@
import { toast } from "@app/core/hooks/useToast";
import { useAppStore } from "@app/core/stores/appStore";
import { useDevice } from "@app/core/stores/deviceStore";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
} from "@components/UI/Dialog";
import type { Protobuf } from "@meshtastic/js";
import { numberToHexUnpadded } from "@noble/curves/abstract/utils";
import { TrashIcon } from "lucide-react";
import type { JSX } from "react";
import { Button } from "../UI/Button";
export interface NodeOptionsDialogProps {
node: Protobuf.Mesh.NodeInfo | undefined;
open: boolean;
onOpenChange: () => void;
}
export const NodeOptionsDialog = ({
node,
open,
onOpenChange,
}: NodeOptionsDialogProps): JSX.Element => {
const { setDialogOpen, connection, setActivePage } = useDevice();
const {
setNodeNumToBeRemoved,
setNodeNumDetails,
setChatType,
setActiveChat,
} = useAppStore();
const longName =
node?.user?.longName ??
(node ? `!${numberToHexUnpadded(node?.num)}` : "Unknown");
const shortName =
node?.user?.shortName ??
(node ? `${numberToHexUnpadded(node?.num).substring(0, 4)}` : "UNK");
function handleDirectMessage() {
if (!node) return;
setChatType("direct");
setActiveChat(node.num);
setActivePage("messages");
}
function handleRequestPosition() {
if (!node) return;
toast({
title: "Requesting position, please wait...",
});
connection?.requestPosition(node.num).then(() =>
toast({
title: "Position request sent.",
}),
);
onOpenChange();
}
function handleTraceroute() {
if (!node) return;
toast({
title: "Sending Traceroute, please wait...",
});
connection?.traceRoute(node.num).then(() =>
toast({
title: "Traceroute sent.",
}),
);
onOpenChange();
}
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent>
<DialogHeader>
<DialogTitle>{`${longName} (${shortName})`}</DialogTitle>
</DialogHeader>
<div className="flex flex-col space-y-1">
<div>
<Button onClick={handleDirectMessage}>Direct Message</Button>
</div>
<div>
<Button onClick={handleRequestPosition}>Request Position</Button>
</div>
<div>
<Button onClick={handleTraceroute}>Trace Route</Button>
</div>
<div>
<Button
key="remove"
variant="destructive"
onClick={() => {
setNodeNumToBeRemoved(node.num);
setDialogOpen("nodeRemoval", true);
}}
>
<TrashIcon />
Remove
</Button>
</div>
<div>
<Button
onClick={() => {
setNodeNumDetails(node.num);
setDialogOpen("nodeDetails", true);
}}
>
More Details
</Button>
</div>
</div>
</DialogContent>
</Dialog>
);
};

57
src/components/Dialog/TracerouteResponseDialog.tsx

@ -0,0 +1,57 @@
import { useDevice } from "@app/core/stores/deviceStore";
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
} from "@components/UI/Dialog";
import type { Protobuf, Types } from "@meshtastic/js";
import { numberToHexUnpadded } from "@noble/curves/abstract/utils";
import type { JSX } from "react";
import { TraceRoute } from "../PageComponents/Messages/TraceRoute";
export interface TracerouteResponseDialogProps {
traceroute: Types.PacketMetadata<Protobuf.Mesh.RouteDiscovery> | undefined;
open: boolean;
onOpenChange: () => void;
}
export const TracerouteResponseDialog = ({
traceroute,
open,
onOpenChange,
}: TracerouteResponseDialogProps): JSX.Element => {
const { nodes } = useDevice();
const route: number[] = traceroute?.data.route ?? [];
const routeBack: number[] = traceroute?.data.routeBack ?? [];
const snrTowards = traceroute?.data.snrTowards ?? [];
const snrBack = traceroute?.data.snrBack ?? [];
const from = nodes.get(traceroute?.from ?? 0);
const longName =
from?.user?.longName ??
(from ? `!${numberToHexUnpadded(from?.num)}` : "Unknown");
const shortName =
from?.user?.shortName ??
(from ? `${numberToHexUnpadded(from?.num).substring(0, 4)}` : "UNK");
const to = nodes.get(traceroute?.to ?? 0);
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent>
<DialogHeader>
<DialogTitle>{`Traceroute: ${longName} (${shortName})`}</DialogTitle>
</DialogHeader>
<DialogDescription>
<TraceRoute
route={route}
routeBack={routeBack}
from={from}
to={to}
snrTowards={snrTowards}
snrBack={snrBack}
/>
</DialogDescription>
</DialogContent>
</Dialog>
);
};

2
src/components/PageComponents/Map/NodeDetail.tsx

@ -4,7 +4,7 @@ import { Subtle } from "@app/components/UI/Typography/Subtle.tsx";
import { formatQuantity } from "@app/core/utils/string";
import { Avatar } from "@components/UI/Avatar";
import { Mono } from "@components/generic/Mono.tsx";
import { TimeAgo } from "@components/generic/Table/tmp/TimeAgo.tsx";
import { TimeAgo } from "@components/generic/TimeAgo.tsx";
import { Protobuf } from "@meshtastic/js";
import type { Protobuf as ProtobufType } from "@meshtastic/js";
import { numberToHexUnpadded } from "@noble/curves/abstract/utils";

48
src/components/PageComponents/Messages/TraceRoute.tsx

@ -1,36 +1,60 @@
import { useDevice } from "@app/core/stores/deviceStore.ts";
import type { Protobuf } from "@meshtastic/js";
import { numberToHexUnpadded } from "@noble/curves/abstract/utils";
import type { JSX } from "react";
export interface TraceRouteProps {
from?: Protobuf.Mesh.NodeInfo;
to?: Protobuf.Mesh.NodeInfo;
route: Array<number>;
routeBack?: Array<number>;
snrTowards?: Array<number>;
snrBack?: Array<number>;
}
export const TraceRoute = ({
from,
to,
route,
routeBack,
snrTowards,
snrBack,
}: TraceRouteProps): JSX.Element => {
const { nodes } = useDevice();
return route.length === 0 ? (
return (
<div className="ml-5 flex">
<span className="ml-4 border-l-2 border-l-backgroundPrimary pl-2 text-textPrimary">
{to?.user?.longName}{from?.user?.longName}
</span>
</div>
) : (
<div className="ml-5 flex">
<span className="ml-4 border-l-2 border-l-backgroundPrimary pl-2 text-textPrimary">
{to?.user?.longName}
{route.map((hop) => {
const node = nodes.get(hop);
return `${node?.user?.longName ?? (node?.num ? numberToHexUnpadded(node.num) : "Unknown")}`;
})}
<p className="font-semibold">Route to destination:</p>
<p>{to?.user?.longName}</p>
<p> {snrTowards?.[0] ? snrTowards[0] : "??"}dB</p>
{route.map((hop, i) => (
<span key={nodes.get(hop)?.num}>
<p>
{nodes.get(hop)?.user?.longName ?? `!${numberToHexUnpadded(hop)}`}
</p>
<p> {snrTowards?.[i + 1] ? snrTowards[i + 1] : "??"}dB</p>
</span>
))}
{from?.user?.longName}
</span>
{routeBack ? (
<span className="ml-4 border-l-2 border-l-backgroundPrimary pl-2 text-textPrimary">
<p className="font-semibold">Route back:</p>
<p>{from?.user?.longName}</p>
<p> {snrBack?.[0] ? snrBack[0] : "??"}dB</p>
{routeBack.map((hop, i) => (
<span key={nodes.get(hop)?.num}>
<p>
{nodes.get(hop)?.user?.longName ??
`!${numberToHexUnpadded(hop)}`}
</p>
<p> {snrBack?.[i + 1] ? snrBack[i + 1] : "??"}dB</p>
</span>
))}
{to?.user?.longName}
</span>
) : null}
</div>
);
};

44
src/components/UI/Accordion.tsx

@ -0,0 +1,44 @@
import { cn } from "@core/utils/cn.ts";
import * as AccordionPrimitive from "@radix-ui/react-accordion";
import { ChevronDownIcon } from "lucide-react";
import { type ComponentRef, forwardRef } from "react";
export const Accordion = AccordionPrimitive.Root;
export const AccordionHeader = AccordionPrimitive.Header;
export const AccordionItem = AccordionPrimitive.Item;
export const AccordionTrigger = forwardRef<
ComponentRef<typeof AccordionPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Trigger>
>(({ className, ...props }, ref) => (
<AccordionPrimitive.Trigger
ref={ref}
className={cn(
"flex justify-between items-center w-full p-4 border-b border-gray-200 dark:border-gray-800 group",
className,
)}
{...props}
>
{props.children}
<ChevronDownIcon
className="h-5 w-5 transition-transform duration-300 group-data-[state=open]:rotate-180"
aria-hidden
/>
</AccordionPrimitive.Trigger>
));
export const AccordionContent = forwardRef<
ComponentRef<typeof AccordionPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Content>
>(({ className, ...props }, ref) => (
<AccordionPrimitive.Content
ref={ref}
className={cn(
"p-4 border-b border-gray-200 dark:border-gray-800",
className,
)}
{...props}
/>
));

2
src/components/UI/Dialog.tsx

@ -44,7 +44,7 @@ const DialogContent = React.forwardRef<
<DialogPrimitive.Content
ref={ref}
className={cn(
"fixed z-50 grid w-full scale-100 gap-4 bg-white p-6 opacity-100 animate-in fade-in-90 slide-in-from-bottom-10 sm:max-w-lg sm:rounded-lg sm:zoom-in-90 sm:slide-in-from-bottom-0",
"fixed z-50 grid w-full max-w-[512px] max-h-[100vh] overflow-y-scroll scale-100 gap-4 bg-white p-6 opacity-100 animate-in fade-in-90 slide-in-from-bottom-10 sm:max-w-lg sm:rounded-lg sm:zoom-in-90 sm:slide-in-from-bottom-0",
"dark:bg-slate-900",
className,
)}

53
src/components/generic/DeviceImage.tsx

@ -0,0 +1,53 @@
export interface DeviceImageProps {
deviceType: string;
className?: React.HTMLAttributes<HTMLImageElement>["className"];
}
const hardwareModelToFilename: { [key: string]: string } = {
DIY_V1: "diy.svg",
NANO_G2_ULTRA: "nano-g2-ultra.svg",
TBEAM: "tbeam.svg",
HELTEC_HT62: "heltec-ht62-esp32c3-sx1262.svg",
RPI_PICO: "pico.svg",
T_DECK: "t-deck.svg",
HELTEC_MESH_NODE_T114: "heltec-mesh-node-t114.svg",
HELTEC_MESH_NODE_T114_CASE: "heltec-mesh-node-t114-case.svg",
HELTEC_V3: "heltec-v3.svg",
HELTEC_V3_CASE: "heltec-v3-case.svg",
HELTEC_VISION_MASTER_E213: "heltec-vision-master-e213.svg",
HELTEC_VISION_MASTER_E290: "heltec-vision-master-e290.svg",
HELTEC_VISION_MASTER_T190: "heltec-vision-master-t190.svg",
HELTEC_WIRELESS_PAPER: "heltec-wireless-paper.svg",
HELTEC_WIRELESS_PAPER_V1_0: "heltec-wireless-paper-V1_0.svg",
HELTEC_WIRELESS_TRACKER: "heltec-wireless-tracker.svg",
HELTEC_WIRELESS_TRACKER_V1_0: "heltec-wireless-tracker-V1-0.svg",
HELTEC_WSL_V3: "heltec-wsl-v3.svg",
TLORA_C6: "tlora-c6.svg",
TLORA_T3_S3: "tlora-t3s3-v1.svg",
TLORA_T3_S3_EPAPER: "tlora-t3s3-epaper.svg",
TLORA_V2: "tlora-v2-1-1_6.svg",
TLORA_V2_1_1P6: "tlora-v2-1-1_6.svg",
TLORA_V2_1_1P8: "tlora-v2-1-1_8.svg",
RAK11310: "rak11310.svg",
RAK2560: "rak2560.svg",
RAK4631: "rak4631.svg",
RAK4631_CASE: "rak4631_case.svg",
WIO_WM1110: "wio-tracker-wm1110.svg",
WM1110_DEV_KIT: "wm1110_dev_kit.svg",
STATION_G2: "station-g2.svg",
TBEAM_V0P7: "tbeam-s3-core.svg",
T_ECHO: "t-echo.svg",
TRACKER_T1000_E: "tracker-t1000-e.svg",
T_WATCH_S3: "t-watch-s3.svg",
SEEED_XIAO_S3: "seeed-xiao-s3.svg",
SENSECAP_INDICATOR: "seeed-sensecap-indicator.svg",
PROMICRO: "promicro.svg",
RPIPICOW: "rpipicow.svg",
UNKNOWN: "unknown.svg",
};
export const DeviceImage = ({ deviceType, className }: DeviceImageProps) => {
const getPath = (device: string) => `/devices/${device}`;
const device = hardwareModelToFilename[deviceType] || "unknown.svg";
return <img className={className} src={getPath(device)} alt={device} />;
};

9
src/components/generic/Table/tmp/TimeAgo.tsx

@ -1,9 +0,0 @@
import TimeAgoReact from "timeago-react";
export interface TimeAgoProps {
timestamp: number;
}
export const TimeAgo = ({ timestamp }: TimeAgoProps): JSX.Element => {
return <TimeAgoReact datetime={timestamp} opts={{ minInterval: 10 }} />;
};

66
src/components/generic/TimeAgo.tsx

@ -0,0 +1,66 @@
import {
Tooltip,
TooltipContent,
TooltipPortal,
TooltipProvider,
TooltipTrigger,
} from "@radix-ui/react-tooltip";
import type { JSX } from "react";
export interface TimeAgoProps {
timestamp: number;
}
const getTimeAgo = (
unixTimestamp: number,
locale: Intl.LocalesArgument = "en",
): string => {
const timestamp = new Date(unixTimestamp);
const diff = (new Date().getTime() - timestamp.getTime()) / 1000;
const minutes = Math.floor(diff / 60);
const hours = Math.floor(minutes / 60);
const days = Math.floor(hours / 24);
const months = Math.floor(days / 30);
const years = Math.floor(months / 12);
const rtf = new Intl.RelativeTimeFormat(locale, { numeric: "auto" });
if (years > 0) {
return rtf.format(0 - years, "year");
}
if (months > 0) {
return rtf.format(0 - months, "month");
}
if (days > 0) {
return rtf.format(0 - days, "day");
}
if (hours > 0) {
return rtf.format(0 - hours, "hour");
}
if (minutes > 0) {
return rtf.format(0 - minutes, "minute");
}
return rtf.format(Math.floor(0 - diff), "second");
};
export const TimeAgo = ({ timestamp }: TimeAgoProps): JSX.Element => {
return (
<TooltipProvider>
<Tooltip>
<TooltipTrigger>
<span>{getTimeAgo(timestamp)}</span>
</TooltipTrigger>
<TooltipPortal>
<TooltipContent
className="rounded-md bg-slate-800 px-3 py-1.5 text-sm text-white shadow-md animate-in fade-in-0 zoom-in-95"
side="top"
align="center"
sideOffset={5}
>
{new Date(timestamp).toLocaleString()}
</TooltipContent>
</TooltipPortal>
</Tooltip>
</TooltipProvider>
);
};

17
src/components/generic/Uptime.tsx

@ -0,0 +1,17 @@
import type { JSX } from "react";
export interface UptimeProps {
seconds: number;
}
const getUptime = (seconds: number): string => {
const days = Math.floor(seconds / 86400);
const hours = Math.floor((seconds % 86400) / 3600);
const minutes = Math.floor(((seconds % 86400) % 3600) / 60);
const secondsLeft = Math.floor(((seconds % 86400) % 3600) % 60);
return `${days}d ${hours}h ${minutes}m ${secondsLeft}s`;
};
export const Uptime = ({ seconds }: UptimeProps): JSX.Element => {
return <span>{getUptime(seconds)}</span>;
};

22
src/core/stores/appStore.ts

@ -1,3 +1,4 @@
import { Types } from "@meshtastic/js";
import { produce } from "immer";
import { create } from "zustand";
@ -29,6 +30,9 @@ interface AppState {
nodeNumToBeRemoved: number;
accent: AccentColor;
connectDialogOpen: boolean;
nodeNumDetails: number;
activeChat: number;
chatType: "broadcast" | "direct";
setRasterSources: (sources: RasterSource[]) => void;
addRasterSource: (source: RasterSource) => void;
@ -42,6 +46,9 @@ interface AppState {
setNodeNumToBeRemoved: (nodeNum: number) => void;
setAccent: (color: AccentColor) => void;
setConnectDialogOpen: (open: boolean) => void;
setNodeNumDetails: (nodeNum: number) => void;
setActiveChat: (chat: number) => void;
setChatType: (type: "broadcast" | "direct") => void;
}
export const useAppStore = create<AppState>()((set) => ({
@ -57,6 +64,9 @@ export const useAppStore = create<AppState>()((set) => ({
accent: "orange",
connectDialogOpen: false,
nodeNumToBeRemoved: 0,
nodeNumDetails: 0,
activeChat: Types.ChannelNumber.Primary,
chatType: "broadcast",
setRasterSources: (sources: RasterSource[]) => {
set(
@ -124,4 +134,16 @@ export const useAppStore = create<AppState>()((set) => ({
}),
);
},
setNodeNumDetails: (nodeNum) =>
set((state) => ({
nodeNumDetails: nodeNum,
})),
setActiveChat: (chat) =>
set(() => ({
activeChat: chat,
})),
setChatType: (type) =>
set(() => ({
chatType: type,
})),
}));

5
src/core/stores/deviceStore.ts

@ -26,7 +26,8 @@ export type DialogVariant =
| "reboot"
| "deviceName"
| "nodeRemoval"
| "pkiBackup";
| "pkiBackup"
| "nodeDetails";
export interface Device {
id: number;
@ -62,6 +63,7 @@ export interface Device {
deviceName: boolean;
nodeRemoval: boolean;
pkiBackup: boolean;
nodeDetails: boolean;
};
setStatus: (status: Types.DeviceStatusEnum) => void;
@ -145,6 +147,7 @@ export const useDeviceStore = create<DeviceState>((set, get) => ({
deviceName: false,
nodeRemoval: false,
pkiBackup: false,
nodeDetails: false,
},
pendingSettingsChanges: false,
messageDraft: "",

11
src/pages/Messages.tsx

@ -1,3 +1,4 @@
import { useAppStore } from "@app/core/stores/appStore";
import { ChannelChat } from "@components/PageComponents/Messages/ChannelChat.tsx";
import { PageLayout } from "@components/PageLayout.tsx";
import { Sidebar } from "@components/Sidebar.tsx";
@ -5,21 +6,17 @@ import { Avatar } from "@components/UI/Avatar.tsx";
import { SidebarSection } from "@components/UI/Sidebar/SidebarSection.tsx";
import { SidebarButton } from "@components/UI/Sidebar/sidebarButton.tsx";
import { useToast } from "@core/hooks/useToast.ts";
import { Device, useDevice, useDeviceStore } from "@core/stores/deviceStore.ts";
import { useDevice } from "@core/stores/deviceStore.ts";
import { Protobuf, Types } from "@meshtastic/js";
import { numberToHexUnpadded } from "@noble/curves/abstract/utils";
import { getChannelName } from "@pages/Channels.tsx";
import { HashIcon, LockIcon, LockOpenIcon, WaypointsIcon } from "lucide-react";
import { useState } from "react";
const MessagesPage = () => {
export const MessagesPage = () => {
const { channels, nodes, hardware, messages, traceroutes, connection } =
useDevice();
const [chatType, setChatType] =
useState<Types.PacketDestination>("broadcast");
const [activeChat, setActiveChat] = useState<number>(
Types.ChannelNumber.Primary,
);
const { activeChat, chatType, setActiveChat, setChatType } = useAppStore();
const [searchTerm, setSearchTerm] = useState<string>("");
const filteredNodes = Array.from(nodes.values()).filter((node) => {
if (node.num === hardware.myNodeNum) return false;

108
src/pages/Nodes.tsx

@ -1,17 +1,18 @@
import { LocationResponseDialog } from "@app/components/Dialog/LocationResponseDialog";
import { NodeOptionsDialog } from "@app/components/Dialog/NodeOptionsDialog";
import { TracerouteResponseDialog } from "@app/components/Dialog/TracerouteResponseDialog";
import Footer from "@app/components/UI/Footer";
import { useAppStore } from "@app/core/stores/appStore";
import { Sidebar } from "@components/Sidebar.tsx";
import { Avatar } from "@components/UI/Avatar.tsx";
import { Button } from "@components/UI/Button.tsx";
import { Mono } from "@components/generic/Mono.tsx";
import { Table } from "@components/generic/Table/index.tsx";
import { TimeAgo } from "@components/generic/Table/tmp/TimeAgo.tsx";
import { TimeAgo } from "@components/generic/TimeAgo.tsx";
import { useDevice } from "@core/stores/deviceStore.ts";
import { Protobuf } from "@meshtastic/js";
import { Protobuf, type Types } from "@meshtastic/js";
import { numberToHexUnpadded } from "@noble/curves/abstract/utils";
import { LockIcon, LockOpenIcon, TrashIcon } from "lucide-react";
import { useState } from "react";
import { Fragment, type JSX } from "react";
import { LockIcon, LockOpenIcon } from "lucide-react";
import { Fragment, type JSX, useCallback, useEffect, useState } from "react";
import { base16 } from "rfc4648";
export interface DeleteNoteDialogProps {
@ -20,8 +21,16 @@ export interface DeleteNoteDialogProps {
}
const NodesPage = (): JSX.Element => {
const { nodes, hardware, setDialogOpen } = useDevice();
const { setNodeNumToBeRemoved } = useAppStore();
const { nodes, hardware, connection } = useDevice();
const [selectedNode, setSelectedNode] = useState<
Protobuf.Mesh.NodeInfo | undefined
>(undefined);
const [selectedTraceroute, setSelectedTraceroute] = useState<
Types.PacketMetadata<Protobuf.Mesh.RouteDiscovery> | undefined
>();
const [selectedLocation, setSelectedLocation] = useState<
Types.PacketMetadata<Protobuf.Mesh.RouteDiscovery> | undefined
>();
const [searchTerm, setSearchTerm] = useState<string>("");
const filteredNodes = Array.from(nodes.values()).filter((node) => {
@ -30,6 +39,36 @@ const NodesPage = (): JSX.Element => {
return nodeName.toLowerCase().includes(searchTerm.toLowerCase());
});
useEffect(() => {
if (!connection) return;
connection.events.onTraceRoutePacket.subscribe(handleTraceroute);
return () => {
connection.events.onTraceRoutePacket.unsubscribe(handleTraceroute);
};
}, [connection]);
const handleTraceroute = useCallback(
(traceroute: Types.PacketMetadata<Protobuf.Mesh.RouteDiscovery>) => {
setSelectedTraceroute(traceroute);
},
[],
);
useEffect(() => {
if (!connection) return;
connection.events.onPositionPacket.subscribe(handleLocation);
return () => {
connection.events.onPositionPacket.subscribe(handleLocation);
};
}, [connection]);
const handleLocation = useCallback(
(location: Types.PacketMetadata<Protobuf.Mesh.Position>) => {
setSelectedLocation(location);
},
[],
);
return (
<>
<Sidebar />
@ -47,21 +86,38 @@ const NodesPage = (): JSX.Element => {
<Table
headings={[
{ title: "", type: "blank", sortable: false },
{ title: "Name", type: "normal", sortable: true },
{ title: "Short Name", type: "normal", sortable: true },
{ title: "Long Name", type: "normal", sortable: true },
{ title: "Model", type: "normal", sortable: true },
{ title: "MAC Address", type: "normal", sortable: true },
{ title: "Last Heard", type: "normal", sortable: true },
{ title: "SNR", type: "normal", sortable: true },
{ title: "Encryption", type: "normal", sortable: false },
{ title: "Connection", type: "normal", sortable: true },
{ title: "Remove", type: "normal", sortable: false },
]}
rows={filteredNodes.map((node) => [
<div key={node.num}>
<Avatar text={node.user?.shortName.toString() ?? "UNK"}/>
<Avatar text={node.user?.shortName.toString() ?? "UNK"} />
</div>,
<h1 key="header">
<h1
key="shortName"
onMouseDown={() => setSelectedNode(node)}
className="cursor-pointer"
>
{node.user?.shortName ??
(node.user?.macaddr
? `${base16
.stringify(node.user?.macaddr.subarray(4, 6) ?? [])
.toLowerCase()}`
: `${numberToHexUnpadded(node.num).slice(-4)}`)}
</h1>,
<h1
key="longName"
onMouseDown={() => setSelectedNode(node)}
className="cursor-pointer"
>
{node.user?.longName ??
(node.user?.macaddr
? `Meshtastic ${base16
@ -95,7 +151,7 @@ const NodesPage = (): JSX.Element => {
{node.user?.publicKey && node.user?.publicKey.length > 0 ? (
<LockIcon className="text-green-600" />
) : (
<LockOpenIcon className="text-yellow-300" />
<LockOpenIcon className="text-yellow-300 mx-auto" />
)}
</Mono>,
<Mono key="hops">
@ -108,19 +164,23 @@ const NodesPage = (): JSX.Element => {
: "-"}
{node.viaMqtt === true ? ", via MQTT" : ""}
</Mono>,
<Button
key="remove"
variant="destructive"
onClick={() => {
setNodeNumToBeRemoved(node.num);
setDialogOpen("nodeRemoval", true);
}}
>
<TrashIcon />
Remove
</Button>,
])}
/>
<NodeOptionsDialog
node={selectedNode}
open={!!selectedNode}
onOpenChange={() => setSelectedNode(undefined)}
/>
<TracerouteResponseDialog
traceroute={selectedTraceroute}
open={!!selectedTraceroute}
onOpenChange={() => setSelectedTraceroute(undefined)}
/>
<LocationResponseDialog
location={selectedLocation}
open={!!selectedLocation}
onOpenChange={() => setSelectedLocation(undefined)}
/>
</div>
<Footer />
</div>

Loading…
Cancel
Save