From 2a45eb930a0372f5c001568acca7af3ba19f405d Mon Sep 17 00:00:00 2001 From: James Rich <2199651+jamesarich@users.noreply.github.com> Date: Wed, 10 Jun 2026 16:25:27 -0500 Subject: [PATCH] refactor(node): fetch device links from the API, drop the bundled matcher (#5765) Co-authored-by: Claude Opus 4.8 (1M context) --- .../meshtastic/app/di/FDroidNetworkModule.kt | 4 + androidApp/src/main/assets/device_links.json | 3646 +++++++++++++++++ androidApp/src/main/assets/marketplaces.json | 126 - androidApp/src/main/assets/urls.json | 1009 ----- .../DeviceLinksJsonDataSourceImpl.kt | 47 + .../MshToLinksJsonDataSourceImpl.kt | 69 - ...Source.kt => DeviceLinksJsonDataSource.kt} | 13 +- .../DeviceHardwareRepositoryImpl.kt | 4 +- .../core/data/repository/DeviceLinkMatcher.kt | 111 - .../repository/DeviceLinkRepositoryImpl.kt | 147 +- .../data/repository/DeviceLinkMatcherTest.kt | 147 - .../DeviceLinkRepositoryImplTest.kt | 223 +- .../43.json | 1581 +++++++ .../core/database/MeshtasticDatabase.kt | 7 +- .../core/database/entity/DeviceLinkEntity.kt | 8 +- .../org/meshtastic/core/model/DeviceLink.kt | 14 +- .../org/meshtastic/core/model/MshToLinks.kt | 41 - .../core/model/NetworkDeviceLink.kt | 80 + .../network/DeviceLinksRemoteDataSource.kt | 29 + .../core/network/service/ApiService.kt | 6 + .../core/repository/DeviceLinkRepository.kt | 13 +- .../desktop/di/DesktopKoinModule.kt | 13 +- .../component/NodeDetailComponentPreviews.kt | 6 +- 23 files changed, 5643 insertions(+), 1701 deletions(-) create mode 100644 androidApp/src/main/assets/device_links.json delete mode 100644 androidApp/src/main/assets/marketplaces.json delete mode 100644 androidApp/src/main/assets/urls.json create mode 100644 core/data/src/androidMain/kotlin/org/meshtastic/core/data/datasource/DeviceLinksJsonDataSourceImpl.kt delete mode 100644 core/data/src/androidMain/kotlin/org/meshtastic/core/data/datasource/MshToLinksJsonDataSourceImpl.kt rename core/data/src/commonMain/kotlin/org/meshtastic/core/data/datasource/{MshToLinksJsonDataSource.kt => DeviceLinksJsonDataSource.kt} (58%) delete mode 100644 core/data/src/commonMain/kotlin/org/meshtastic/core/data/repository/DeviceLinkMatcher.kt delete mode 100644 core/data/src/commonTest/kotlin/org/meshtastic/core/data/repository/DeviceLinkMatcherTest.kt create mode 100644 core/database/schemas/org.meshtastic.core.database.MeshtasticDatabase/43.json delete mode 100644 core/model/src/commonMain/kotlin/org/meshtastic/core/model/MshToLinks.kt create mode 100644 core/model/src/commonMain/kotlin/org/meshtastic/core/model/NetworkDeviceLink.kt create mode 100644 core/network/src/commonMain/kotlin/org/meshtastic/core/network/DeviceLinksRemoteDataSource.kt diff --git a/androidApp/src/fdroid/kotlin/org/meshtastic/app/di/FDroidNetworkModule.kt b/androidApp/src/fdroid/kotlin/org/meshtastic/app/di/FDroidNetworkModule.kt index ca81dbada..4cdaabc0b 100644 --- a/androidApp/src/fdroid/kotlin/org/meshtastic/app/di/FDroidNetworkModule.kt +++ b/androidApp/src/fdroid/kotlin/org/meshtastic/app/di/FDroidNetworkModule.kt @@ -19,6 +19,7 @@ package org.meshtastic.app.di import org.koin.core.annotation.Module import org.koin.core.annotation.Single import org.meshtastic.core.model.NetworkDeviceHardware +import org.meshtastic.core.model.NetworkDeviceLinksResponse import org.meshtastic.core.model.NetworkFirmwareReleases import org.meshtastic.core.network.service.ApiService @@ -36,6 +37,9 @@ class FDroidNetworkModule { override suspend fun getDeviceHardware(): List = throw UnsupportedOperationException("getDeviceHardware is not supported on F-Droid builds.") + override suspend fun getDeviceLinks(): NetworkDeviceLinksResponse = + throw UnsupportedOperationException("getDeviceLinks is not supported on F-Droid builds.") + override suspend fun getFirmwareReleases(): NetworkFirmwareReleases = throw UnsupportedOperationException("getFirmwareReleases is not supported on F-Droid builds.") } diff --git a/androidApp/src/main/assets/device_links.json b/androidApp/src/main/assets/device_links.json new file mode 100644 index 000000000..38e73f4a5 --- /dev/null +++ b/androidApp/src/main/assets/device_links.json @@ -0,0 +1,3646 @@ +{ + "version": 1, + "generatedAt": "2026-06-10T14:17:34.591Z", + "source": "https://msh.to/api/urls", + "links": [ + { + "shortCode": "github", + "url": "https://msh.to/github", + "description": "Meshtastic GitHub Organization", + "type": "internal", + "targets": null, + "hwModels": null, + "marketplace": null, + "regions": null + }, + { + "shortCode": "youtube", + "url": "https://msh.to/youtube", + "description": "Meshtastic YouTube Channel", + "type": "internal", + "targets": null, + "hwModels": null, + "marketplace": null, + "regions": null + }, + { + "shortCode": "reddit", + "url": "https://msh.to/reddit", + "description": "Meshtastic Reddit Community", + "type": "internal", + "targets": null, + "hwModels": null, + "marketplace": null, + "regions": null + }, + { + "shortCode": "docs", + "url": "https://msh.to/docs", + "description": "Meshtastic Documentation", + "type": "internal", + "targets": null, + "hwModels": null, + "marketplace": null, + "regions": null + }, + { + "shortCode": "discord", + "url": "https://msh.to/discord", + "description": "Meshtastic Discord Server", + "type": "internal", + "targets": null, + "hwModels": null, + "marketplace": null, + "regions": null + }, + { + "shortCode": "web", + "url": "https://msh.to/web", + "description": "Meshtastic Web Client", + "type": "internal", + "targets": null, + "hwModels": null, + "marketplace": null, + "regions": null + }, + { + "shortCode": "flash", + "url": "https://msh.to/flash", + "description": "Meshtastic Web Flasher", + "type": "internal", + "targets": null, + "hwModels": null, + "marketplace": null, + "regions": null + }, + { + "shortCode": "firmware", + "url": "https://msh.to/firmware", + "description": "Meshtastic Firmware Repository", + "type": "internal", + "targets": null, + "hwModels": null, + "marketplace": null, + "regions": null + }, + { + "shortCode": "android", + "url": "https://msh.to/android", + "description": "Meshtastic Android App", + "type": "internal", + "targets": null, + "hwModels": null, + "marketplace": null, + "regions": null + }, + { + "shortCode": "ios", + "url": "https://msh.to/ios", + "description": "Meshtastic iOS App", + "type": "internal", + "targets": null, + "hwModels": null, + "marketplace": null, + "regions": null + }, + { + "shortCode": "rak-collection", + "url": "https://msh.to/rak-collection", + "description": "RAKwireless Meshtastic Collection", + "type": "vendor", + "targets": [], + "hwModels": [], + "marketplace": null, + "regions": null + }, + { + "shortCode": "rak4631", + "url": "https://msh.to/rak4631", + "description": "WisMesh RAK4631 Starter Kit", + "type": "vendor", + "targets": [ + "rak4631" + ], + "hwModels": [ + 9 + ], + "marketplace": null, + "regions": null + }, + { + "shortCode": "rak3312", + "url": "https://msh.to/rak3312", + "description": "WisMesh ESP32-S3 Starter Kit", + "type": "vendor", + "targets": [ + "rak3312" + ], + "hwModels": [ + 106 + ], + "marketplace": null, + "regions": null + }, + { + "shortCode": "rak3401-1watt", + "url": "https://msh.to/rak3401-1watt", + "description": "WisMesh RAK3401 1W Starter Kit", + "type": "vendor", + "targets": [ + "rak3401-1watt" + ], + "hwModels": [ + 117 + ], + "marketplace": null, + "regions": null + }, + { + "shortCode": "rak_wismeshtap", + "url": "https://msh.to/rak_wismeshtap", + "description": "RAK WisMesh Tap", + "type": "vendor", + "targets": [ + "rak_wismeshtap" + ], + "hwModels": [ + 84 + ], + "marketplace": null, + "regions": null + }, + { + "shortCode": "rak_wismeshtag", + "url": "https://msh.to/rak_wismeshtag", + "description": "RAK WisMesh Tag", + "type": "vendor", + "targets": [ + "rak_wismeshtag" + ], + "hwModels": [ + 105 + ], + "marketplace": null, + "regions": null + }, + { + "shortCode": "rokland-wismesh-tag", + "url": "https://msh.to/rokland-wismesh-tag", + "description": "Rokland WisMesh Tag", + "type": "marketplace", + "targets": [ + "rak_wismeshtag" + ], + "hwModels": [ + 105 + ], + "marketplace": "rokland", + "regions": [ + "AU", + "AT", + "BE", + "CA", + "DK", + "EC", + "FR", + "DE", + "IE", + "JP", + "NL", + "NZ", + "NO", + "PK", + "ES", + "SE", + "CH", + "GB", + "US" + ] + }, + { + "shortCode": "hexaspot-wismesh-tag", + "url": "https://msh.to/hexaspot-wismesh-tag", + "description": "Hexaspot WisMesh Tag", + "type": "marketplace", + "targets": [ + "rak_wismeshtag" + ], + "hwModels": [ + 105 + ], + "marketplace": "hexaspot", + "regions": [ + "AT", + "BE", + "BG", + "CY", + "CZ", + "DE", + "DK", + "EE", + "ES", + "FI", + "FR", + "GR", + "HR", + "HU", + "IE", + "IT", + "LT", + "LU", + "LV", + "MT", + "NL", + "NO", + "PL", + "PT", + "RO", + "SE", + "SI", + "SK" + ] + }, + { + "shortCode": "aliexpress-wismesh-tag", + "url": "https://msh.to/aliexpress-wismesh-tag", + "description": "Aliexpress RAK WisMesh Tag", + "type": "marketplace", + "targets": [ + "rak_wismeshtag" + ], + "hwModels": [ + 105 + ], + "marketplace": "aliexpress", + "regions": null + }, + { + "shortCode": "rak19007", + "url": "https://msh.to/rak19007", + "description": "RAKwireless RAK19007 WisBlock Base Board 2nd Gen", + "type": "vendor", + "targets": [], + "hwModels": [], + "marketplace": null, + "regions": null + }, + { + "shortCode": "tbeam-s3-core", + "url": "https://msh.to/tbeam-s3-core", + "description": "T-Beam Supreme", + "type": "vendor", + "targets": [ + "tbeam-s3-core" + ], + "hwModels": [ + 12 + ], + "marketplace": null, + "regions": null + }, + { + "shortCode": "t-echo", + "url": "https://msh.to/t-echo", + "description": "T-Echo", + "type": "vendor", + "targets": [ + "t-echo" + ], + "hwModels": [ + 7 + ], + "marketplace": null, + "regions": null + }, + { + "shortCode": "t-watch-s3", + "url": "https://msh.to/t-watch-s3", + "description": "T-Watch S3", + "type": "vendor", + "targets": [ + "t-watch-s3" + ], + "hwModels": [ + 51 + ], + "marketplace": null, + "regions": null + }, + { + "shortCode": "t-deck", + "url": "https://msh.to/t-deck", + "description": "T-Deck", + "type": "vendor", + "targets": [ + "t-deck" + ], + "hwModels": [ + 50 + ], + "marketplace": null, + "regions": null + }, + { + "shortCode": "tlora-t3s3-v1", + "url": "https://msh.to/tlora-t3s3-v1", + "description": "T3S3", + "type": "vendor", + "targets": [ + "tlora-t3s3-v1" + ], + "hwModels": [ + 16 + ], + "marketplace": null, + "regions": null + }, + { + "shortCode": "heltec-mesh-node-t114", + "url": "https://msh.to/heltec-mesh-node-t114", + "description": "Mesh Node T114", + "type": "vendor", + "targets": [ + "heltec-mesh-node-t114" + ], + "hwModels": [ + 69 + ], + "marketplace": null, + "regions": null + }, + { + "shortCode": "heltec-vision-master-e213", + "url": "https://msh.to/heltec-vision-master-e213", + "description": "Vision Master E213", + "type": "vendor", + "targets": [ + "heltec-vision-master-e213" + ], + "hwModels": [ + 67 + ], + "marketplace": null, + "regions": null + }, + { + "shortCode": "heltec-vision-master-e290", + "url": "https://msh.to/heltec-vision-master-e290", + "description": "Vision Master E290", + "type": "vendor", + "targets": [ + "heltec-vision-master-e290" + ], + "hwModels": [ + 68 + ], + "marketplace": null, + "regions": null + }, + { + "shortCode": "heltec-vision-master-t190", + "url": "https://msh.to/heltec-vision-master-t190", + "description": "Vision Master T190", + "type": "vendor", + "targets": [ + "heltec-vision-master-t190" + ], + "hwModels": [ + 66 + ], + "marketplace": null, + "regions": null + }, + { + "shortCode": "heltec-wireless-tracker", + "url": "https://msh.to/heltec-wireless-tracker", + "description": "Wireless Tracker", + "type": "vendor", + "targets": [ + "heltec-wireless-tracker" + ], + "hwModels": [ + 48 + ], + "marketplace": null, + "regions": null + }, + { + "shortCode": "heltec-wireless-tracker-v2", + "url": "https://msh.to/heltec-wireless-tracker-v2", + "description": "Wireless Tracker V2", + "type": "vendor", + "targets": [ + "heltec-wireless-tracker-v2" + ], + "hwModels": [ + 113 + ], + "marketplace": null, + "regions": null + }, + { + "shortCode": "heltec-wireless-paper", + "url": "https://msh.to/heltec-wireless-paper", + "description": "Wireless Paper", + "type": "vendor", + "targets": [ + "heltec-wireless-paper" + ], + "hwModels": [ + 49 + ], + "marketplace": null, + "regions": null + }, + { + "shortCode": "heltec-ht62-esp32c3-sx1262", + "url": "https://msh.to/heltec-ht62-esp32c3-sx1262", + "description": "HT-CT62", + "type": "vendor", + "targets": [ + "heltec-ht62-esp32c3-sx1262" + ], + "hwModels": [ + 53 + ], + "marketplace": null, + "regions": null + }, + { + "shortCode": "wio-tracker-wm1110", + "url": "https://msh.to/wio-tracker-wm1110", + "description": "Wio Tracker WM1110 Dev Kit", + "type": "vendor", + "targets": [ + "wio-tracker-wm1110" + ], + "hwModels": [ + 21 + ], + "marketplace": null, + "regions": null + }, + { + "shortCode": "tracker-t1000-e", + "url": "https://msh.to/tracker-t1000-e", + "description": "SenseCAP Card Tracker T1000-E", + "type": "vendor", + "targets": [ + "tracker-t1000-e" + ], + "hwModels": [ + 71 + ], + "marketplace": null, + "regions": null + }, + { + "shortCode": "tracker-t1000-e-aliexpress", + "url": "https://msh.to/tracker-t1000-e-aliexpress", + "description": "SenseCAP Card Tracker T1000-E Aliexpress", + "type": "marketplace", + "targets": [ + "tracker-t1000-e" + ], + "hwModels": [ + 71 + ], + "marketplace": "aliexpress", + "regions": null + }, + { + "shortCode": "tracker-t1000-e-amazon", + "url": "https://msh.to/tracker-t1000-e-amazon", + "description": "SenseCAP Card Tracker T1000-E Amazon", + "type": "marketplace", + "targets": [ + "tracker-t1000-e" + ], + "hwModels": [ + 71 + ], + "marketplace": "amazon", + "regions": [ + "AU", + "CA", + "FR", + "DE", + "IE", + "JP", + "NL", + "ES", + "SE", + "GB", + "US" + ] + }, + { + "shortCode": "seeed-sensecap-indicator", + "url": "https://msh.to/seeed-sensecap-indicator", + "description": "SenseCAP Indicator", + "type": "vendor", + "targets": [ + "seeed-sensecap-indicator" + ], + "hwModels": [ + 70 + ], + "marketplace": null, + "regions": null + }, + { + "shortCode": "station-g2", + "url": "https://msh.to/station-g2", + "description": "Station G2", + "type": "vendor", + "targets": [ + "station-g2" + ], + "hwModels": [ + 31 + ], + "marketplace": null, + "regions": null + }, + { + "shortCode": "rak2560", + "url": "https://msh.to/rak2560", + "description": "WisMesh Repeater", + "type": "vendor", + "targets": [ + "rak2560" + ], + "hwModels": [ + 22 + ], + "marketplace": null, + "regions": null + }, + { + "shortCode": "heltec-v3", + "url": "https://msh.to/heltec-v3", + "description": "LoRa32 V3", + "type": "vendor", + "targets": [ + "heltec-v3" + ], + "hwModels": [ + 43 + ], + "marketplace": null, + "regions": null + }, + { + "shortCode": "heltec-wsl-v3", + "url": "https://msh.to/heltec-wsl-v3", + "description": "WSL V3", + "type": "vendor", + "targets": [ + "heltec-wsl-v3" + ], + "hwModels": [ + 44 + ], + "marketplace": null, + "regions": null + }, + { + "shortCode": "heltec-v4", + "url": "https://msh.to/heltec-v4", + "description": "LoRa32 V4", + "type": "vendor", + "targets": [ + "heltec-v4" + ], + "hwModels": [ + 110 + ], + "marketplace": null, + "regions": null + }, + { + "shortCode": "seeed-xiao-s3", + "url": "https://msh.to/seeed-xiao-s3", + "description": "XIAO ESP32-S3 + Wio-SX1262 Kit", + "type": "vendor", + "targets": [ + "seeed-xiao-s3" + ], + "hwModels": [ + 81 + ], + "marketplace": null, + "regions": null + }, + { + "shortCode": "tlora-t3s3-epaper", + "url": "https://msh.to/tlora-t3s3-epaper", + "description": "T3S3", + "type": "vendor", + "targets": [ + "tlora-t3s3-epaper" + ], + "hwModels": [ + 16 + ], + "marketplace": null, + "regions": null + }, + { + "shortCode": "ht-ct62", + "url": "https://msh.to/ht-ct62", + "description": "HT-CT62", + "type": "vendor", + "targets": [ + "heltec-ht62-esp32c3-sx1262" + ], + "hwModels": [ + 53 + ], + "marketplace": null, + "regions": null + }, + { + "shortCode": "seeed_xiao_nrf52840_kit", + "url": "https://msh.to/seeed_xiao_nrf52840_kit", + "description": "XIAO nRF52840 & Wio-SX1262 Kit", + "type": "vendor", + "targets": [ + "seeed_xiao_nrf52840_kit" + ], + "hwModels": [ + 88 + ], + "marketplace": null, + "regions": null + }, + { + "shortCode": "seeed_xiao_nrf52840_kit_aliexpress", + "url": "https://msh.to/seeed_xiao_nrf52840_kit_aliexpress", + "description": "XIAO nRF52840 & Wio-SX1262 Kit Aliexpress", + "type": "marketplace", + "targets": [ + "seeed_xiao_nrf52840_kit" + ], + "hwModels": [ + 88 + ], + "marketplace": "aliexpress", + "regions": null + }, + { + "shortCode": "thinknode_m1", + "url": "https://msh.to/thinknode_m1", + "description": "ThinkNode M1", + "type": "vendor", + "targets": [ + "thinknode_m1" + ], + "hwModels": [ + 89 + ], + "marketplace": null, + "regions": null + }, + { + "shortCode": "thinknode_m2", + "url": "https://msh.to/thinknode_m2", + "description": "ThinkNode M2", + "type": "vendor", + "targets": [ + "thinknode_m2" + ], + "hwModels": [ + 90 + ], + "marketplace": null, + "regions": null + }, + { + "shortCode": "thinknode_m3", + "url": "https://msh.to/thinknode_m3", + "description": "ThinkNode M3", + "type": "vendor", + "targets": [ + "thinknode_m3" + ], + "hwModels": [ + 115 + ], + "marketplace": null, + "regions": null + }, + { + "shortCode": "thinknode_m5", + "url": "https://msh.to/thinknode_m5", + "description": "ThinkNode M5", + "type": "vendor", + "targets": [ + "thinknode_m5" + ], + "hwModels": [ + 107 + ], + "marketplace": null, + "regions": null + }, + { + "shortCode": "thinknode_m4", + "url": "https://msh.to/thinknode_m4", + "description": "ThinkNode M4", + "type": "vendor", + "targets": [ + "thinknode_m4" + ], + "hwModels": [ + 119 + ], + "marketplace": null, + "regions": null + }, + { + "shortCode": "thinknode_m6", + "url": "https://msh.to/thinknode_m6", + "description": "ThinkNode M6", + "type": "vendor", + "targets": [ + "thinknode_m6" + ], + "hwModels": [ + 120 + ], + "marketplace": null, + "regions": null + }, + { + "shortCode": "heltec-mesh-pocket-10000", + "url": "https://msh.to/heltec-mesh-pocket-10000", + "description": "MeshPocket", + "type": "vendor", + "targets": [ + "heltec-mesh-pocket-10000" + ], + "hwModels": [ + 94 + ], + "marketplace": null, + "regions": null + }, + { + "shortCode": "seeed_solar_node", + "url": "https://msh.to/seeed_solar_node", + "description": "SenseCAP Solar Node P1 Pro", + "type": "vendor", + "targets": [ + "seeed_solar_node" + ], + "hwModels": [ + 95 + ], + "marketplace": null, + "regions": null + }, + { + "shortCode": "seeed_solar_node_aliexpress", + "url": "https://msh.to/seeed_solar_node_aliexpress", + "description": "SenseCAP Solar Node P1 Pro Aliexpress", + "type": "marketplace", + "targets": [ + "seeed_solar_node" + ], + "hwModels": [ + 95 + ], + "marketplace": "aliexpress", + "regions": null + }, + { + "shortCode": "seeed_solar_node_amazon", + "url": "https://msh.to/seeed_solar_node_amazon", + "description": "SenseCAP Solar Node P1 Pro Amazon", + "type": "marketplace", + "targets": [ + "seeed_solar_node" + ], + "hwModels": [ + 95 + ], + "marketplace": "amazon", + "regions": [ + "AU", + "CA", + "FR", + "DE", + "IE", + "JP", + "NL", + "ES", + "SE", + "GB", + "US" + ] + }, + { + "shortCode": "elecrow-adv-35-tft", + "url": "https://msh.to/elecrow-adv-35-tft", + "description": "CrowPanel 3.5", + "type": "vendor", + "targets": [ + "elecrow-adv-35-tft" + ], + "hwModels": [ + 97 + ], + "marketplace": null, + "regions": null + }, + { + "shortCode": "elecrow-adv1-43-50-70-tft", + "url": "https://msh.to/elecrow-adv1-43-50-70-tft", + "description": "CrowPanel 4.3", + "type": "vendor", + "targets": [ + "elecrow-adv1-43-50-70-tft" + ], + "hwModels": [ + 97 + ], + "marketplace": null, + "regions": null + }, + { + "shortCode": "elecrow-adv-24-28-tft", + "url": "https://msh.to/elecrow-adv-24-28-tft", + "description": "CrowPanel 2.4", + "type": "vendor", + "targets": [ + "elecrow-adv-24-28-tft" + ], + "hwModels": [ + 97 + ], + "marketplace": null, + "regions": null + }, + { + "shortCode": "elecrow-adv-28-tft", + "url": "https://msh.to/elecrow-adv-28-tft", + "description": "CrowPanel 2.8", + "type": "vendor", + "targets": [ + "elecrow-adv-24-28-tft" + ], + "hwModels": [ + 97 + ], + "marketplace": null, + "regions": null + }, + { + "shortCode": "elecrow-adv1-50-tft", + "url": "https://msh.to/elecrow-adv1-50-tft", + "description": "CrowPanel 5.0", + "type": "vendor", + "targets": [ + "elecrow-adv1-43-50-70-tft" + ], + "hwModels": [ + 97 + ], + "marketplace": null, + "regions": null + }, + { + "shortCode": "elecrow-adv1-70-tft", + "url": "https://msh.to/elecrow-adv1-70-tft", + "description": "CrowPanel 7.0", + "type": "vendor", + "targets": [ + "elecrow-adv1-43-50-70-tft" + ], + "hwModels": [ + 97 + ], + "marketplace": null, + "regions": null + }, + { + "shortCode": "seeed_wio_tracker_L1", + "url": "https://msh.to/seeed_wio_tracker_L1", + "description": "Wio Tracker L1", + "type": "vendor", + "targets": [ + "seeed_wio_tracker_L1" + ], + "hwModels": [ + 99 + ], + "marketplace": null, + "regions": null + }, + { + "shortCode": "seeed_wio_tracker_L1_aliexpress", + "url": "https://msh.to/seeed_wio_tracker_L1_aliexpress", + "description": "Wio Tracker L1 Aliexpress", + "type": "marketplace", + "targets": [ + "seeed_wio_tracker_L1" + ], + "hwModels": [ + 99 + ], + "marketplace": "aliexpress", + "regions": null + }, + { + "shortCode": "seeed_wio_tracker_L1_amazon", + "url": "https://msh.to/seeed_wio_tracker_L1_amazon", + "description": "Wio Tracker L1 Amazon", + "type": "marketplace", + "targets": [ + "seeed_wio_tracker_L1" + ], + "hwModels": [ + 99 + ], + "marketplace": "amazon", + "regions": [ + "AU", + "CA", + "FR", + "DE", + "IE", + "JP", + "NL", + "ES", + "SE", + "GB", + "US" + ] + }, + { + "shortCode": "nano-g2-ultra", + "url": "https://msh.to/nano-g2-ultra", + "description": "Nano G2 Ultra", + "type": "vendor", + "targets": [ + "nano-g2-ultra" + ], + "hwModels": [ + 18 + ], + "marketplace": null, + "regions": null + }, + { + "shortCode": "rak11310", + "url": "https://msh.to/rak11310", + "description": "RAK11310", + "type": "vendor", + "targets": [ + "rak11310" + ], + "hwModels": [ + 26 + ], + "marketplace": null, + "regions": null + }, + { + "shortCode": "rokland-rak11310", + "url": "https://msh.to/rokland-rak11310", + "description": "Rokland RAKwireless RAK11310 WisBlock RP2040 Core Module", + "type": "vendor", + "targets": [ + "rak11310" + ], + "hwModels": [ + 26 + ], + "marketplace": null, + "regions": null + }, + { + "shortCode": "station-g2-tindie", + "url": "https://msh.to/station-g2-tindie", + "description": "Station G2 Tindie Listing", + "type": "marketplace", + "targets": [ + "station-g2" + ], + "hwModels": [ + 31 + ], + "marketplace": "tindie", + "regions": [ + "US", + "CA", + "GB", + "DE", + "FR", + "AU", + "NL" + ] + }, + { + "shortCode": "nano-g2-ultra-tindie", + "url": "https://msh.to/nano-g2-ultra-tindie", + "description": "Nano G2 Ultra Tindie Listing", + "type": "marketplace", + "targets": [ + "nano-g2-ultra" + ], + "hwModels": [ + 18 + ], + "marketplace": "tindie", + "regions": [ + "US", + "CA", + "GB", + "DE", + "FR", + "AU", + "NL" + ] + }, + { + "shortCode": "t-deck-plus", + "url": "https://msh.to/t-deck-plus", + "description": "T-Deck Plus", + "type": "vendor", + "targets": [ + "t-deck" + ], + "hwModels": [ + 50 + ], + "marketplace": null, + "regions": null + }, + { + "shortCode": "rokland-meshtastic-starter-kit", + "url": "https://msh.to/rokland-meshtastic-starter-kit", + "description": "Rokland Meshtastic Starter Kit", + "type": "marketplace", + "targets": null, + "hwModels": null, + "marketplace": "rokland", + "regions": [ + "AU", + "AT", + "BE", + "CA", + "DK", + "EC", + "FR", + "DE", + "IE", + "JP", + "NL", + "NZ", + "NO", + "PK", + "ES", + "SE", + "CH", + "GB", + "US" + ] + }, + { + "shortCode": "rokland-t-deck-base", + "url": "https://msh.to/rokland-t-deck-base", + "description": "Rokland T-Deck Base", + "type": "marketplace", + "targets": [ + "t-deck" + ], + "hwModels": [ + 50 + ], + "marketplace": "rokland", + "regions": [ + "AU", + "AT", + "BE", + "CA", + "DK", + "EC", + "FR", + "DE", + "IE", + "JP", + "NL", + "NZ", + "NO", + "PK", + "ES", + "SE", + "CH", + "GB", + "US" + ] + }, + { + "shortCode": "rokland-t-deck-complete", + "url": "https://msh.to/rokland-t-deck-complete", + "description": "Rokland T-Deck Complete", + "type": "marketplace", + "targets": [ + "t-deck" + ], + "hwModels": [ + 50 + ], + "marketplace": "rokland", + "regions": [ + "AU", + "AT", + "BE", + "CA", + "DK", + "EC", + "FR", + "DE", + "IE", + "JP", + "NL", + "NZ", + "NO", + "PK", + "ES", + "SE", + "CH", + "GB", + "US" + ] + }, + { + "shortCode": "rokland-t-deck-plus", + "url": "https://msh.to/rokland-t-deck-plus", + "description": "Rokland T-Deck Plus", + "type": "marketplace", + "targets": [ + "t-deck" + ], + "hwModels": [ + 50 + ], + "marketplace": "rokland", + "regions": [ + "AU", + "AT", + "BE", + "CA", + "DK", + "EC", + "FR", + "DE", + "IE", + "JP", + "NL", + "NZ", + "NO", + "PK", + "ES", + "SE", + "CH", + "GB", + "US" + ] + }, + { + "shortCode": "rokland-t-echo", + "url": "https://msh.to/rokland-t-echo", + "description": "Rokland T-Echo", + "type": "marketplace", + "targets": [ + "t-echo" + ], + "hwModels": [ + 7 + ], + "marketplace": "rokland", + "regions": [ + "AU", + "AT", + "BE", + "CA", + "DK", + "EC", + "FR", + "DE", + "IE", + "JP", + "NL", + "NZ", + "NO", + "PK", + "ES", + "SE", + "CH", + "GB", + "US" + ] + }, + { + "shortCode": "rokland-t-echo-bme280", + "url": "https://msh.to/rokland-t-echo-bme280", + "description": "Rokland T-Echo with BME280", + "type": "marketplace", + "targets": [ + "t-echo" + ], + "hwModels": [ + 7 + ], + "marketplace": "rokland", + "regions": [ + "AU", + "AT", + "BE", + "CA", + "DK", + "EC", + "FR", + "DE", + "IE", + "JP", + "NL", + "NZ", + "NO", + "PK", + "ES", + "SE", + "CH", + "GB", + "US" + ] + }, + { + "shortCode": "rokland-rak19007", + "url": "https://msh.to/rokland-rak19007", + "description": "Rokland RAKwireless RAK19007 WisBlock Base Board 2nd Gen", + "type": "marketplace", + "targets": [], + "hwModels": [], + "marketplace": "rokland", + "regions": [ + "AU", + "AT", + "BE", + "CA", + "DK", + "EC", + "FR", + "DE", + "IE", + "JP", + "NL", + "NZ", + "NO", + "PK", + "ES", + "SE", + "CH", + "GB", + "US" + ] + }, + { + "shortCode": "hexaspot-rak19007", + "url": "https://msh.to/hexaspot-rak19007", + "description": "Hexaspot RAKwireless RAK19007 WisBlock Base Board 2nd Gen", + "type": "marketplace", + "targets": [], + "hwModels": [], + "marketplace": "hexaspot", + "regions": [ + "AT", + "BE", + "BG", + "CY", + "CZ", + "DE", + "DK", + "EE", + "ES", + "FI", + "FR", + "GR", + "HR", + "HU", + "IE", + "IT", + "LT", + "LU", + "LV", + "MT", + "NL", + "NO", + "PL", + "PT", + "RO", + "SE", + "SI", + "SK" + ] + }, + { + "shortCode": "rokland-starter-kit", + "url": "https://msh.to/rokland-starter-kit", + "description": "Rokland RAKwireless 4631 Starter Kit", + "type": "marketplace", + "targets": [ + "rak4631" + ], + "hwModels": [ + 9 + ], + "marketplace": "rokland", + "regions": [ + "AU", + "AT", + "BE", + "CA", + "DK", + "EC", + "FR", + "DE", + "IE", + "JP", + "NL", + "NZ", + "NO", + "PK", + "ES", + "SE", + "CH", + "GB", + "US" + ] + }, + { + "shortCode": "hexaspot-starter-kit", + "url": "https://msh.to/hexaspot-starter-kit", + "description": "Hexaspot RAKwireless 4631 Starter Kit", + "type": "marketplace", + "targets": [ + "rak4631" + ], + "hwModels": [ + 9 + ], + "marketplace": "hexaspot", + "regions": [ + "AT", + "BE", + "BG", + "CY", + "CZ", + "DE", + "DK", + "EE", + "ES", + "FI", + "FR", + "GR", + "HR", + "HU", + "IE", + "IT", + "LT", + "LU", + "LV", + "MT", + "NL", + "NO", + "PL", + "PT", + "RO", + "SE", + "SI", + "SK" + ] + }, + { + "shortCode": "aliexpress-rak1921", + "url": "https://msh.to/aliexpress-rak1921", + "description": "RAK1921 OLED Display (AliExpress)", + "type": "marketplace", + "targets": [], + "hwModels": [], + "marketplace": "aliexpress", + "regions": null + }, + { + "shortCode": "rak1921", + "url": "https://msh.to/rak1921", + "description": "RAK1921 OLED Display (RAK Store)", + "type": "vendor", + "targets": [], + "hwModels": [], + "marketplace": null, + "regions": null + }, + { + "shortCode": "rokland-rak1921", + "url": "https://msh.to/rokland-rak1921", + "description": "Rokland RAK1921 WisBlock OLED Display", + "type": "marketplace", + "targets": [], + "hwModels": [], + "marketplace": "rokland", + "regions": [ + "AU", + "AT", + "BE", + "CA", + "DK", + "EC", + "FR", + "DE", + "IE", + "JP", + "NL", + "NZ", + "NO", + "PK", + "ES", + "SE", + "CH", + "GB", + "US" + ] + }, + { + "shortCode": "muzi-rak1921", + "url": "https://msh.to/muzi-rak1921", + "description": "Muzi Works RAK1921 OLED Display SSD1306", + "type": "marketplace", + "targets": [], + "hwModels": [], + "marketplace": "muzi", + "regions": [ + "AU", + "AT", + "BE", + "CA", + "CZ", + "DK", + "FI", + "FR", + "DE", + "HK", + "IN", + "IE", + "IL", + "IT", + "JP", + "MY", + "NL", + "NZ", + "NO", + "PL", + "PT", + "SG", + "KR", + "ES", + "SE", + "CH", + "TW", + "AE", + "GB", + "US" + ] + }, + { + "shortCode": "aliexpress-rak14000", + "url": "https://msh.to/aliexpress-rak14000", + "description": "RAK14000 E-Ink Display (AliExpress)", + "type": "marketplace", + "targets": [], + "hwModels": [], + "marketplace": "aliexpress", + "regions": null + }, + { + "shortCode": "rak14000", + "url": "https://msh.to/rak14000", + "description": "RAK14000 E-Ink Display (RAK Store)", + "type": "vendor", + "targets": [], + "hwModels": [], + "marketplace": null, + "regions": null + }, + { + "shortCode": "rokland-rak14000", + "url": "https://msh.to/rokland-rak14000", + "description": "Rokland RAK14000 WisBlock E-Ink Display", + "type": "marketplace", + "targets": [], + "hwModels": [], + "marketplace": "rokland", + "regions": [ + "AU", + "AT", + "BE", + "CA", + "DK", + "EC", + "FR", + "DE", + "IE", + "JP", + "NL", + "NZ", + "NO", + "PK", + "ES", + "SE", + "CH", + "GB", + "US" + ] + }, + { + "shortCode": "aliexpress-rak12500", + "url": "https://msh.to/aliexpress-rak12500", + "description": "RAK12500 (AliExpress)", + "type": "marketplace", + "targets": [], + "hwModels": [], + "marketplace": "aliexpress", + "regions": null + }, + { + "shortCode": "rak12500", + "url": "https://msh.to/rak12500", + "description": "RAK12500 (RAK Store)", + "type": "vendor", + "targets": [], + "hwModels": [], + "marketplace": null, + "regions": null + }, + { + "shortCode": "rak13300", + "url": "https://msh.to/rak13300", + "description": "RAK13300 LPWAN Module (RAK Store)", + "type": "vendor", + "targets": [], + "hwModels": [], + "marketplace": null, + "regions": null + }, + { + "shortCode": "aliexpress-rak13002", + "url": "https://msh.to/aliexpress-rak13002", + "description": "RAK13002 IO Module (AliExpress)", + "type": "marketplace", + "targets": [], + "hwModels": [], + "marketplace": "aliexpress", + "regions": null + }, + { + "shortCode": "rak13002", + "url": "https://msh.to/rak13002", + "description": "RAK13002 IO Module (RAK Store)", + "type": "vendor", + "targets": [], + "hwModels": [], + "marketplace": null, + "regions": null + }, + { + "shortCode": "rokland-rak13002", + "url": "https://msh.to/rokland-rak13002", + "description": "Rokland RAK13002 WisBlock IO Adapter Module", + "type": "marketplace", + "targets": [], + "hwModels": [], + "marketplace": "rokland", + "regions": [ + "AU", + "AT", + "BE", + "CA", + "DK", + "EC", + "FR", + "DE", + "IE", + "JP", + "NL", + "NZ", + "NO", + "PK", + "ES", + "SE", + "CH", + "GB", + "US" + ] + }, + { + "shortCode": "muzi-rak13002", + "url": "https://msh.to/muzi-rak13002", + "description": "Muzi Works RAK13002 IO Module", + "type": "marketplace", + "targets": [], + "hwModels": [], + "marketplace": "muzi", + "regions": [ + "AU", + "AT", + "BE", + "CA", + "CZ", + "DK", + "FI", + "FR", + "DE", + "HK", + "IN", + "IE", + "IL", + "IT", + "JP", + "MY", + "NL", + "NZ", + "NO", + "PL", + "PT", + "SG", + "KR", + "ES", + "SE", + "CH", + "TW", + "AE", + "GB", + "US" + ] + }, + { + "shortCode": "rak6421", + "url": "https://msh.to/rak6421", + "description": "WisMesh Pi Hat RAK6421 (RAK Store)", + "type": "vendor", + "targets": [], + "hwModels": [], + "marketplace": null, + "regions": null + }, + { + "shortCode": "aliexpress-rak18001", + "url": "https://msh.to/aliexpress-rak18001", + "description": "RAK18001 RAK Buzzer (AliExpress)", + "type": "marketplace", + "targets": [], + "hwModels": [], + "marketplace": "aliexpress", + "regions": null + }, + { + "shortCode": "rak18001", + "url": "https://msh.to/rak18001", + "description": "RAK18001 RAK Buzzer (RAK Store)", + "type": "vendor", + "targets": [], + "hwModels": [], + "marketplace": null, + "regions": null + }, + { + "shortCode": "aliexpress-rak1901", + "url": "https://msh.to/aliexpress-rak1901", + "description": "RAK1901 Temperature and Humidity Sensor (AliExpress)", + "type": "marketplace", + "targets": [], + "hwModels": [], + "marketplace": "aliexpress", + "regions": null + }, + { + "shortCode": "rak1901", + "url": "https://msh.to/rak1901", + "description": "RAK1901 Temperature and Humidity Sensor (RAK Store)", + "type": "vendor", + "targets": [], + "hwModels": [], + "marketplace": null, + "regions": null + }, + { + "shortCode": "aliexpress-rak1902", + "url": "https://msh.to/aliexpress-rak1902", + "description": "RAK-1902 Barometric Pressure Sensor (AliExpress)", + "type": "marketplace", + "targets": [], + "hwModels": [], + "marketplace": "aliexpress", + "regions": null + }, + { + "shortCode": "rak1902", + "url": "https://msh.to/rak1902", + "description": "RAK-1902 Barometric Pressure Sensor (RAK Store)", + "type": "vendor", + "targets": [], + "hwModels": [], + "marketplace": null, + "regions": null + }, + { + "shortCode": "aliexpress-rak1906", + "url": "https://msh.to/aliexpress-rak1906", + "description": "RAK1906 Environment Sensor (AliExpress)", + "type": "marketplace", + "targets": [], + "hwModels": [], + "marketplace": "aliexpress", + "regions": null + }, + { + "shortCode": "rak1906", + "url": "https://msh.to/rak1906", + "description": "RAK1906 Environment Sensor (RAK Store)", + "type": "vendor", + "targets": [], + "hwModels": [], + "marketplace": null, + "regions": null + }, + { + "shortCode": "aliexpress-rak12002", + "url": "https://msh.to/aliexpress-rak12002", + "description": "RAK12002 WisBlock RTC Module (AliExpress)", + "type": "marketplace", + "targets": [], + "hwModels": [], + "marketplace": "aliexpress", + "regions": null + }, + { + "shortCode": "rak12002", + "url": "https://msh.to/rak12002", + "description": "RAK12002 WisBlock RTC Module (RAK Store)", + "type": "vendor", + "targets": [], + "hwModels": [], + "marketplace": null, + "regions": null + }, + { + "shortCode": "rokland-rak12002", + "url": "https://msh.to/rokland-rak12002", + "description": "Rokland RAK12002 RTC Module Micro Crystal RV-3028-C7", + "type": "marketplace", + "targets": [], + "hwModels": [], + "marketplace": "rokland", + "regions": [ + "AU", + "AT", + "BE", + "CA", + "DK", + "EC", + "FR", + "DE", + "IE", + "JP", + "NL", + "NZ", + "NO", + "PK", + "ES", + "SE", + "CH", + "GB", + "US" + ] + }, + { + "shortCode": "aliexpress-wismesh-pocket-v2", + "url": "https://msh.to/aliexpress-wismesh-pocket-v2", + "description": "WisMesh Pocket V2 (AliExpress)", + "type": "marketplace", + "targets": null, + "hwModels": null, + "marketplace": "aliexpress", + "regions": null + }, + { + "shortCode": "rokland-wismesh-pocket-v2", + "url": "https://msh.to/rokland-wismesh-pocket-v2", + "description": "WisMesh Pocket V2 (Rokland)", + "type": "marketplace", + "targets": null, + "hwModels": null, + "marketplace": "rokland", + "regions": [ + "AU", + "AT", + "BE", + "CA", + "DK", + "EC", + "FR", + "DE", + "IE", + "JP", + "NL", + "NZ", + "NO", + "PK", + "ES", + "SE", + "CH", + "GB", + "US" + ] + }, + { + "shortCode": "hexaspot-wismesh-pocket-v2", + "url": "https://msh.to/hexaspot-wismesh-pocket-v2", + "description": "WisMesh Pocket V2 (Hexaspot)", + "type": "marketplace", + "targets": null, + "hwModels": null, + "marketplace": "hexaspot", + "regions": [ + "AT", + "BE", + "BG", + "CY", + "CZ", + "DE", + "DK", + "EE", + "ES", + "FI", + "FR", + "GR", + "HR", + "HU", + "IE", + "IT", + "LT", + "LU", + "LV", + "MT", + "NL", + "NO", + "PL", + "PT", + "RO", + "SE", + "SI", + "SK" + ] + }, + { + "shortCode": "wismesh-pocket-v2", + "url": "https://msh.to/wismesh-pocket-v2", + "description": "WisMesh Pocket V2 (RAK Store)", + "type": "vendor", + "targets": null, + "hwModels": null, + "marketplace": null, + "regions": null + }, + { + "shortCode": "aliexpress-wismesh-pocket-mini", + "url": "https://msh.to/aliexpress-wismesh-pocket-mini", + "description": "WisMesh Pocket Mini (Rokland)", + "type": "marketplace", + "targets": null, + "hwModels": null, + "marketplace": "aliexpress", + "regions": null + }, + { + "shortCode": "rokland-wismesh-pocket-mini", + "url": "https://msh.to/rokland-wismesh-pocket-mini", + "description": "WisMesh Pocket Mini (Rokland)", + "type": "marketplace", + "targets": null, + "hwModels": null, + "marketplace": "rokland", + "regions": [ + "AU", + "AT", + "BE", + "CA", + "DK", + "EC", + "FR", + "DE", + "IE", + "JP", + "NL", + "NZ", + "NO", + "PK", + "ES", + "SE", + "CH", + "GB", + "US" + ] + }, + { + "shortCode": "wismesh-pocket-mini", + "url": "https://msh.to/wismesh-pocket-mini", + "description": "WisMesh Pocket Mini (RAK Store)", + "type": "vendor", + "targets": null, + "hwModels": null, + "marketplace": null, + "regions": null + }, + { + "shortCode": "aliexpress-rak19026", + "url": "https://msh.to/aliexpress-rak19026", + "description": "WisMesh Baseboard (AliExpress)", + "type": "marketplace", + "targets": [], + "hwModels": [], + "marketplace": "aliexpress", + "regions": null + }, + { + "shortCode": "rokland-rak19026", + "url": "https://msh.to/rokland-rak19026", + "description": "WisMesh Baseboard (Rokland)", + "type": "marketplace", + "targets": [], + "hwModels": [], + "marketplace": "rokland", + "regions": [ + "AU", + "AT", + "BE", + "CA", + "DK", + "EC", + "FR", + "DE", + "IE", + "JP", + "NL", + "NZ", + "NO", + "PK", + "ES", + "SE", + "CH", + "GB", + "US" + ] + }, + { + "shortCode": "rak19026", + "url": "https://msh.to/rak19026", + "description": "WisMesh Baseboard (RAK Store)", + "type": "vendor", + "targets": [], + "hwModels": [], + "marketplace": null, + "regions": null + }, + { + "shortCode": "aliexpress-wismesh-tap", + "url": "https://msh.to/aliexpress-wismesh-tap", + "description": "RAK WisMesh Tap (AliExpress)", + "type": "marketplace", + "targets": [ + "rak_wismeshtap" + ], + "hwModels": [ + 84 + ], + "marketplace": "aliexpress", + "regions": null + }, + { + "shortCode": "aliexpress-board-one", + "url": "https://msh.to/aliexpress-board-one", + "description": "RAK WisMesh Board ONE (AliExpress)", + "type": "marketplace", + "targets": null, + "hwModels": null, + "marketplace": "aliexpress", + "regions": null + }, + { + "shortCode": "board-one", + "url": "https://msh.to/board-one", + "description": "RAK WisMesh Board ONE (RAK Store)", + "type": "vendor", + "targets": null, + "hwModels": null, + "marketplace": null, + "regions": null + }, + { + "shortCode": "rokland-board-one", + "url": "https://msh.to/rokland-board-one", + "description": "Rokland WisMesh Board ONE (US915 MHz)", + "type": "marketplace", + "targets": null, + "hwModels": null, + "marketplace": "rokland", + "regions": [ + "AU", + "AT", + "BE", + "CA", + "DK", + "EC", + "FR", + "DE", + "IE", + "JP", + "NL", + "NZ", + "NO", + "PK", + "ES", + "SE", + "CH", + "GB", + "US" + ] + }, + { + "shortCode": "wismesh-repeater", + "url": "https://msh.to/wismesh-repeater", + "description": "WisMesh Repeater (RAK Store)", + "type": "vendor", + "targets": [ + "rak2560" + ], + "hwModels": [ + 22 + ], + "marketplace": null, + "regions": null + }, + { + "shortCode": "aliexpress-wismesh-repeater", + "url": "https://msh.to/aliexpress-wismesh-repeater", + "description": "WisMesh Repeater (AliExpress)", + "type": "marketplace", + "targets": null, + "hwModels": null, + "marketplace": "aliexpress", + "regions": null + }, + { + "shortCode": "aliexpress-wismesh-repeater-mini", + "url": "https://msh.to/aliexpress-wismesh-repeater-mini", + "description": "WisMesh Repeater Mini (AliExpress)", + "type": "marketplace", + "targets": null, + "hwModels": null, + "marketplace": "aliexpress", + "regions": null + }, + { + "shortCode": "hexaspot-wismesh-repeater-mini", + "url": "https://msh.to/hexaspot-wismesh-repeater-mini", + "description": "WisMesh Repeater Mini (Hexaspot)", + "type": "marketplace", + "targets": null, + "hwModels": null, + "marketplace": "hexaspot", + "regions": [ + "AT", + "BE", + "BG", + "CY", + "CZ", + "DE", + "DK", + "EE", + "ES", + "FI", + "FR", + "GR", + "HR", + "HU", + "IE", + "IT", + "LT", + "LU", + "LV", + "MT", + "NL", + "NO", + "PL", + "PT", + "RO", + "SE", + "SI", + "SK" + ] + }, + { + "shortCode": "wismesh-repeater-mini", + "url": "https://msh.to/wismesh-repeater-mini", + "description": "WisMesh Repeater Mini (RAK Store)", + "type": "vendor", + "targets": null, + "hwModels": null, + "marketplace": null, + "regions": null + }, + { + "shortCode": "aliexpress-wismesh-ethernet-gateway", + "url": "https://msh.to/aliexpress-wismesh-ethernet-gateway", + "description": "WisMesh Ethernet MQTT Gateway (AliExpress)", + "type": "marketplace", + "targets": null, + "hwModels": null, + "marketplace": "aliexpress", + "regions": null + }, + { + "shortCode": "wismesh-ethernet-gateway", + "url": "https://msh.to/wismesh-ethernet-gateway", + "description": "WisMesh Ethernet MQTT Gateway (RAK Store)", + "type": "vendor", + "targets": null, + "hwModels": null, + "marketplace": null, + "regions": null + }, + { + "shortCode": "aliexpress-wismesh-wifi-gateway", + "url": "https://msh.to/aliexpress-wismesh-wifi-gateway", + "description": "WisMesh WiFi MQTT Gateway (AliExpress)", + "type": "marketplace", + "targets": null, + "hwModels": null, + "marketplace": "aliexpress", + "regions": null + }, + { + "shortCode": "wismesh-wifi-gateway", + "url": "https://msh.to/wismesh-wifi-gateway", + "description": "WisMesh WiFi MQTT Gateway (RAK Store)", + "type": "vendor", + "targets": null, + "hwModels": null, + "marketplace": null, + "regions": null + }, + { + "shortCode": "aliexpress-board-one-pocket", + "url": "https://msh.to/aliexpress-board-one-pocket", + "description": "RAK WisMesh Board ONE Pocket (AliExpress)", + "type": "marketplace", + "targets": null, + "hwModels": null, + "marketplace": "aliexpress", + "regions": null + }, + { + "shortCode": "board-one-pocket", + "url": "https://msh.to/board-one-pocket", + "description": "RAK WisMesh Board ONE Pocket (RAK Store)", + "type": "vendor", + "targets": null, + "hwModels": null, + "marketplace": null, + "regions": null + }, + { + "shortCode": "aliexpress-wismesh-unify-enclosure", + "url": "https://msh.to/aliexpress-wismesh-unify-enclosure", + "description": "WisMesh Unify Enclosure (AliExpress)", + "type": "marketplace", + "targets": [], + "hwModels": [], + "marketplace": "aliexpress", + "regions": null + }, + { + "shortCode": "wismesh-unify-enclosure", + "url": "https://msh.to/wismesh-unify-enclosure", + "description": "WisMesh Unify Enclosure (RAK Store)", + "type": "vendor", + "targets": [], + "hwModels": [], + "marketplace": null, + "regions": null + }, + { + "shortCode": "aliexpress-wismesh-antenna", + "url": "https://msh.to/aliexpress-wismesh-antenna", + "description": "WisMesh Antenna (AliExpress)", + "type": "marketplace", + "targets": [], + "hwModels": [], + "marketplace": "aliexpress", + "regions": null + }, + { + "shortCode": "wismesh-antenna", + "url": "https://msh.to/wismesh-antenna", + "description": "WisMesh Antenna (RAK Store)", + "type": "vendor", + "targets": [], + "hwModels": [], + "marketplace": null, + "regions": null + }, + { + "shortCode": "muzi-rak4631", + "url": "https://msh.to/muzi-rak4631", + "description": "Muzi RAK4631 Starter Kit", + "type": "marketplace", + "targets": [ + "rak4631" + ], + "hwModels": [ + 9 + ], + "marketplace": "muzi", + "regions": [ + "AU", + "AT", + "BE", + "CA", + "CZ", + "DK", + "FI", + "FR", + "DE", + "HK", + "IN", + "IE", + "IL", + "IT", + "JP", + "MY", + "NL", + "NZ", + "NO", + "PL", + "PT", + "SG", + "KR", + "ES", + "SE", + "CH", + "TW", + "AE", + "GB", + "US" + ] + }, + { + "shortCode": "aliexpress-rak19007", + "url": "https://msh.to/aliexpress-rak19007", + "description": "RAK19007 (AliExpress)", + "type": "marketplace", + "targets": [], + "hwModels": [], + "marketplace": "aliexpress", + "regions": null + }, + { + "shortCode": "aliexpress-starter-kit", + "url": "https://msh.to/aliexpress-starter-kit", + "description": "WisMesh RAK4631 Starter Kit (AliExpress)", + "type": "marketplace", + "targets": [ + "rak4631" + ], + "hwModels": [ + 9 + ], + "marketplace": "aliexpress", + "regions": null + }, + { + "shortCode": "rak19003", + "url": "https://msh.to/rak19003", + "description": "RAK19003 (RAK Store)", + "type": "vendor", + "targets": [], + "hwModels": [], + "marketplace": null, + "regions": null + }, + { + "shortCode": "aliexpress-rak19003", + "url": "https://msh.to/aliexpress-rak19003", + "description": "RAK19003 (AliExpress)", + "type": "marketplace", + "targets": [], + "hwModels": [], + "marketplace": "aliexpress", + "regions": null + }, + { + "shortCode": "rak19001", + "url": "https://msh.to/rak19001", + "description": "RAK19001 WisBlock Dual IO Base Board (RAK Store)", + "type": "vendor", + "targets": [], + "hwModels": [], + "marketplace": null, + "regions": null + }, + { + "shortCode": "aliexpress-rak19001", + "url": "https://msh.to/aliexpress-rak19001", + "description": "RAK19001 WisBlock Dual IO Base Board (AliExpress)", + "type": "marketplace", + "targets": [], + "hwModels": [], + "marketplace": "aliexpress", + "regions": null + }, + { + "shortCode": "rokland-19003", + "url": "https://msh.to/rokland-19003", + "description": "Rokland WisBlock Mini Base Board RAK19003 (Ver B)", + "type": "marketplace", + "targets": [], + "hwModels": [], + "marketplace": "rokland", + "regions": [ + "AU", + "AT", + "BE", + "CA", + "DK", + "EC", + "FR", + "DE", + "IE", + "JP", + "NL", + "NZ", + "NO", + "PK", + "ES", + "SE", + "CH", + "GB", + "US" + ] + }, + { + "shortCode": "hexaspot-19003", + "url": "https://msh.to/hexaspot-19003", + "description": "Hexaspot WisBlock Mini Base Board RAK19003 (Ver B)", + "type": "marketplace", + "targets": [], + "hwModels": [], + "marketplace": "hexaspot", + "regions": [ + "AT", + "BE", + "BG", + "CY", + "CZ", + "DE", + "DK", + "EE", + "ES", + "FI", + "FR", + "GR", + "HR", + "HU", + "IE", + "IT", + "LT", + "LU", + "LV", + "MT", + "NL", + "NO", + "PL", + "PT", + "RO", + "SE", + "SI", + "SK" + ] + }, + { + "shortCode": "rokland-19001", + "url": "https://msh.to/rokland-19001", + "description": "Rokland WisBlock Dual IO Base Board RAK19001", + "type": "marketplace", + "targets": [], + "hwModels": [], + "marketplace": "rokland", + "regions": [ + "AU", + "AT", + "BE", + "CA", + "DK", + "EC", + "FR", + "DE", + "IE", + "JP", + "NL", + "NZ", + "NO", + "PK", + "ES", + "SE", + "CH", + "GB", + "US" + ] + }, + { + "shortCode": "hexaspot-19001", + "url": "https://msh.to/hexaspot-19001", + "description": "Hexaspot WisBlock Dual IO Base Board RAK19001", + "type": "marketplace", + "targets": [], + "hwModels": [], + "marketplace": "hexaspot", + "regions": [ + "AT", + "BE", + "BG", + "CY", + "CZ", + "DE", + "DK", + "EE", + "ES", + "FI", + "FR", + "GR", + "HR", + "HU", + "IE", + "IT", + "LT", + "LU", + "LV", + "MT", + "NL", + "NO", + "PL", + "PT", + "RO", + "SE", + "SI", + "SK" + ] + }, + { + "shortCode": "rokland-4631", + "url": "https://msh.to/rokland-4631", + "description": "Rokland RAK4631 Nordic nRF52840 BLE Core Module for LoRaWAN with LoRa SX1262", + "type": "marketplace", + "targets": [ + "rak4631" + ], + "hwModels": [ + 9 + ], + "marketplace": "rokland", + "regions": [ + "AU", + "AT", + "BE", + "CA", + "DK", + "EC", + "FR", + "DE", + "IE", + "JP", + "NL", + "NZ", + "NO", + "PK", + "ES", + "SE", + "CH", + "GB", + "US" + ] + }, + { + "shortCode": "hexaspot-4631", + "url": "https://msh.to/hexaspot-4631", + "description": "Hexaspot RAK4631 Nordic nRF52840 BLE Core Module for LoRaWAN with LoRa SX1262", + "type": "marketplace", + "targets": [ + "rak4631" + ], + "hwModels": [ + 9 + ], + "marketplace": "hexaspot", + "regions": [ + "AT", + "BE", + "BG", + "CY", + "CZ", + "DE", + "DK", + "EE", + "ES", + "FI", + "FR", + "GR", + "HR", + "HU", + "IE", + "IT", + "LT", + "LU", + "LV", + "MT", + "NL", + "NO", + "PL", + "PT", + "RO", + "SE", + "SI", + "SK" + ] + }, + { + "shortCode": "aliexpress-rak4631", + "url": "https://msh.to/aliexpress-rak4631", + "description": "RAK4631 Nordic nRF52840 BLE Core Module (AliExpress)", + "type": "marketplace", + "targets": [ + "rak4631" + ], + "hwModels": [ + 9 + ], + "marketplace": "aliexpress", + "regions": null + }, + { + "shortCode": "rakwireless-4631", + "url": "https://msh.to/rakwireless-4631", + "description": "RAK4631 Nordic nRF52840 BLE Core Module", + "type": "vendor", + "targets": [ + "rak4631" + ], + "hwModels": [ + 9 + ], + "marketplace": null, + "regions": null + }, + { + "shortCode": "rakwireless-rak11310", + "url": "https://msh.to/rakwireless-rak11310", + "description": "RAK11310 RP2040 Core Module)", + "type": "vendor", + "targets": [ + "rak11310" + ], + "hwModels": [ + 26 + ], + "marketplace": null, + "regions": null + }, + { + "shortCode": "rakwireless-rak3312", + "url": "https://msh.to/rakwireless-rak3312", + "description": "RAK3312 ESP32-S3 Core Module", + "type": "vendor", + "targets": [ + "rak3312" + ], + "hwModels": [ + 106 + ], + "marketplace": null, + "regions": null + }, + { + "shortCode": "hexaspot-rak3312", + "url": "https://msh.to/hexaspot-rak3312", + "description": "Hexaspot RAK3312 ESP32-S3 Core Module", + "type": "marketplace", + "targets": [ + "rak3312" + ], + "hwModels": [ + 106 + ], + "marketplace": "hexaspot", + "regions": [ + "AT", + "BE", + "BG", + "CY", + "CZ", + "DE", + "DK", + "EE", + "ES", + "FI", + "FR", + "GR", + "HR", + "HU", + "IE", + "IT", + "LT", + "LU", + "LV", + "MT", + "NL", + "NO", + "PL", + "PT", + "RO", + "SE", + "SI", + "SK" + ] + }, + { + "shortCode": "rokland-rak3312", + "url": "https://msh.to/rokland-rak3312", + "description": "Rokland RAK3312 ESP32-S3 Core Module", + "type": "marketplace", + "targets": [ + "rak3312" + ], + "hwModels": [ + 106 + ], + "marketplace": "rokland", + "regions": [ + "AU", + "AT", + "BE", + "CA", + "DK", + "EC", + "FR", + "DE", + "IE", + "JP", + "NL", + "NZ", + "NO", + "PK", + "ES", + "SE", + "CH", + "GB", + "US" + ] + }, + { + "shortCode": "rokland-rak3312-starter-kit", + "url": "https://msh.to/rokland-rak3312-starter-kit", + "description": "Rokland RAK3312 ESP32-S3 Starter Kit", + "type": "marketplace", + "targets": [ + "rak3312" + ], + "hwModels": [ + 106 + ], + "marketplace": "rokland", + "regions": [ + "AU", + "AT", + "BE", + "CA", + "DK", + "EC", + "FR", + "DE", + "IE", + "JP", + "NL", + "NZ", + "NO", + "PK", + "ES", + "SE", + "CH", + "GB", + "US" + ] + }, + { + "shortCode": "aliexpress-rak11310", + "url": "https://msh.to/aliexpress-rak11310", + "description": "RAK11310 RP2040 Core Module (AliExpress)", + "type": "marketplace", + "targets": [ + "rak11310" + ], + "hwModels": [ + 26 + ], + "marketplace": "aliexpress", + "regions": null + }, + { + "shortCode": "rokland-1901", + "url": "https://msh.to/rokland-1901", + "description": "Rokland RAK1901 Temperature and Humidity Sensor", + "type": "marketplace", + "targets": [], + "hwModels": [], + "marketplace": "rokland", + "regions": [ + "AU", + "AT", + "BE", + "CA", + "DK", + "EC", + "FR", + "DE", + "IE", + "JP", + "NL", + "NZ", + "NO", + "PK", + "ES", + "SE", + "CH", + "GB", + "US" + ] + }, + { + "shortCode": "rokland-1902", + "url": "https://msh.to/rokland-1902", + "description": "Rokland RAK1902 Barometric Pressure Sensor", + "type": "marketplace", + "targets": [], + "hwModels": [], + "marketplace": "rokland", + "regions": [ + "AU", + "AT", + "BE", + "CA", + "DK", + "EC", + "FR", + "DE", + "IE", + "JP", + "NL", + "NZ", + "NO", + "PK", + "ES", + "SE", + "CH", + "GB", + "US" + ] + }, + { + "shortCode": "rokland-1906", + "url": "https://msh.to/rokland-1906", + "description": "Rokland RAK1906 WisBlock Environment Sensor", + "type": "marketplace", + "targets": [], + "hwModels": [], + "marketplace": "rokland", + "regions": [ + "AU", + "AT", + "BE", + "CA", + "DK", + "EC", + "FR", + "DE", + "IE", + "JP", + "NL", + "NZ", + "NO", + "PK", + "ES", + "SE", + "CH", + "GB", + "US" + ] + }, + { + "shortCode": "aliexpress-wismesh-tap", + "url": "https://msh.to/aliexpress-wismesh-tap", + "description": "RAKwireless WisMesh Tap (AliExpress)", + "type": "marketplace", + "targets": [ + "rak_wismeshtap" + ], + "hwModels": [ + 84 + ], + "marketplace": "aliexpress", + "regions": null + }, + { + "shortCode": "rokland-wismesh-tap", + "url": "https://msh.to/rokland-wismesh-tap", + "description": "RAKwireless WisMesh Tap (Rokland)", + "type": "marketplace", + "targets": [ + "rak_wismeshtap" + ], + "hwModels": [ + 84 + ], + "marketplace": "rokland", + "regions": [ + "AU", + "AT", + "BE", + "CA", + "DK", + "EC", + "FR", + "DE", + "IE", + "JP", + "NL", + "NZ", + "NO", + "PK", + "ES", + "SE", + "CH", + "GB", + "US" + ] + }, + { + "shortCode": "rakdap1", + "url": "https://msh.to/rakdap1", + "description": "RAKwireless RAKDAP1 Debug and Flash Tool", + "type": "vendor", + "targets": [], + "hwModels": [], + "marketplace": null, + "regions": null + }, + { + "shortCode": "rokland-heltec-wsl-v3", + "url": "https://msh.to/rokland-heltec-wsl-v3", + "description": "Rokland WSL V3", + "type": "marketplace", + "targets": [ + "heltec-wsl-v3" + ], + "hwModels": [ + 44 + ], + "marketplace": "rokland", + "regions": [ + "AU", + "AT", + "BE", + "CA", + "DK", + "EC", + "FR", + "DE", + "IE", + "JP", + "NL", + "NZ", + "NO", + "PK", + "ES", + "SE", + "CH", + "GB", + "US" + ] + }, + { + "shortCode": "aliexpress-heltec-wsl-v3", + "url": "https://msh.to/aliexpress-heltec-wsl-v3", + "description": "Aliexpress WSL V3", + "type": "marketplace", + "targets": [ + "heltec-wsl-v3" + ], + "hwModels": [ + 44 + ], + "marketplace": "aliexpress", + "regions": null + }, + { + "shortCode": "rokland-heltec-wireless-tracker", + "url": "https://msh.to/rokland-heltec-wireless-tracker", + "description": "Rokland Wireless Tracker", + "type": "marketplace", + "targets": [ + "heltec-wireless-tracker" + ], + "hwModels": [ + 48 + ], + "marketplace": "rokland", + "regions": [ + "AU", + "AT", + "BE", + "CA", + "DK", + "EC", + "FR", + "DE", + "IE", + "JP", + "NL", + "NZ", + "NO", + "PK", + "ES", + "SE", + "CH", + "GB", + "US" + ] + }, + { + "shortCode": "aliexpress-heltec-wireless-tracker", + "url": "https://msh.to/aliexpress-heltec-wireless-tracker", + "description": "Aliexpress Wireless Tracker", + "type": "marketplace", + "targets": [ + "heltec-wireless-tracker" + ], + "hwModels": [ + 48 + ], + "marketplace": "aliexpress", + "regions": null + }, + { + "shortCode": "aliexpress-heltec-wireless-paper", + "url": "https://msh.to/aliexpress-heltec-wireless-paper", + "description": "Aliexpress Wireless Paper", + "type": "marketplace", + "targets": [ + "heltec-wireless-paper" + ], + "hwModels": [ + 49 + ], + "marketplace": "aliexpress", + "regions": null + }, + { + "shortCode": "rokland-heltec-wireless-paper", + "url": "https://msh.to/rokland-heltec-wireless-paper", + "description": "Rokland Wireless Paper", + "type": "marketplace", + "targets": [ + "heltec-wireless-paper" + ], + "hwModels": [ + 49 + ], + "marketplace": "rokland", + "regions": [ + "AU", + "AT", + "BE", + "CA", + "DK", + "EC", + "FR", + "DE", + "IE", + "JP", + "NL", + "NZ", + "NO", + "PK", + "ES", + "SE", + "CH", + "GB", + "US" + ] + }, + { + "shortCode": "muzi-heltec-mesh-node-t114", + "url": "https://msh.to/muzi-heltec-mesh-node-t114", + "description": "MuziWorks Mesh Node T114", + "type": "marketplace", + "targets": [ + "heltec-mesh-node-t114" + ], + "hwModels": [ + 69 + ], + "marketplace": "muzi", + "regions": [ + "AU", + "AT", + "BE", + "CA", + "CZ", + "DK", + "FI", + "FR", + "DE", + "HK", + "IN", + "IE", + "IL", + "IT", + "JP", + "MY", + "NL", + "NZ", + "NO", + "PL", + "PT", + "SG", + "KR", + "ES", + "SE", + "CH", + "TW", + "AE", + "GB", + "US" + ] + }, + { + "shortCode": "aliexpress-heltec-mesh-node-t114", + "url": "https://msh.to/aliexpress-heltec-mesh-node-t114", + "description": "Aliexpress Mesh Node T114", + "type": "marketplace", + "targets": [ + "heltec-mesh-node-t114" + ], + "hwModels": [ + 69 + ], + "marketplace": "aliexpress", + "regions": null + }, + { + "shortCode": "aliexpress-heltec-vision-master-e213", + "url": "https://msh.to/aliexpress-heltec-vision-master-e213", + "description": "Aliexpress Vision Master E213", + "type": "marketplace", + "targets": [ + "heltec-vision-master-e213" + ], + "hwModels": [ + 67 + ], + "marketplace": "aliexpress", + "regions": null + }, + { + "shortCode": "aliexpress-heltec-vision-master-e290", + "url": "https://msh.to/aliexpress-heltec-vision-master-e290", + "description": "Aliexpress Vision Master E290", + "type": "marketplace", + "targets": [ + "heltec-vision-master-e290" + ], + "hwModels": [ + 68 + ], + "marketplace": "aliexpress", + "regions": null + }, + { + "shortCode": "aliexpress-heltec-vision-master-t190", + "url": "https://msh.to/aliexpress-heltec-vision-master-t190", + "description": "Aliexpress Vision Master T190", + "type": "marketplace", + "targets": [ + "heltec-vision-master-t190" + ], + "hwModels": [ + 66 + ], + "marketplace": "aliexpress", + "regions": null + }, + { + "shortCode": "seeed-wio-tracker-l1-oled", + "url": "https://msh.to/seeed-wio-tracker-l1-oled", + "description": "Wio Tracker L1 (with OLED)", + "type": "vendor", + "targets": [ + "seeed_wio_tracker_L1" + ], + "hwModels": [ + 99 + ], + "marketplace": null, + "regions": null + }, + { + "shortCode": "seeed-wio-tracker-l1-oled_aliexpress", + "url": "https://msh.to/seeed-wio-tracker-l1-oled_aliexpress", + "description": "Wio Tracker L1 (with OLED)", + "type": "marketplace", + "targets": [ + "seeed_wio_tracker_L1" + ], + "hwModels": [ + 99 + ], + "marketplace": "aliexpress", + "regions": null + }, + { + "shortCode": "seeed_wio_tracker_L1_eink", + "url": "https://msh.to/seeed_wio_tracker_L1_eink", + "description": "Wio Tracker L1 (with E-Ink)", + "type": "vendor", + "targets": [ + "seeed_wio_tracker_L1_eink" + ], + "hwModels": [ + 100 + ], + "marketplace": null, + "regions": null + }, + { + "shortCode": "seeed_wio_tracker_L1_eink_amazon", + "url": "https://msh.to/seeed_wio_tracker_L1_eink_amazon", + "description": "Wio Tracker L1 (with E-Ink) Amazon", + "type": "marketplace", + "targets": [ + "seeed_wio_tracker_L1_eink" + ], + "hwModels": [ + 100 + ], + "marketplace": "amazon", + "regions": [ + "AU", + "CA", + "FR", + "DE", + "IE", + "JP", + "NL", + "ES", + "SE", + "GB", + "US" + ] + }, + { + "shortCode": "seeed-wio-tracker-l1-lite", + "url": "https://msh.to/seeed-wio-tracker-l1-lite", + "description": "Wio Tracker L1 Lite (no display)", + "type": "vendor", + "targets": null, + "hwModels": null, + "marketplace": null, + "regions": null + }, + { + "shortCode": "seeed_solar_node_p1", + "url": "https://msh.to/seeed_solar_node_p1", + "description": "SenseCAP Solar Node P1", + "type": "vendor", + "targets": null, + "hwModels": null, + "marketplace": null, + "regions": null + }, + { + "shortCode": "seeed_solar_node_p1_aliexpress", + "url": "https://msh.to/seeed_solar_node_p1_aliexpress", + "description": "SenseCAP Solar Node P1 Aliexpress", + "type": "marketplace", + "targets": null, + "hwModels": null, + "marketplace": "aliexpress", + "regions": null + }, + { + "shortCode": "android-closed-test", + "url": "https://msh.to/android-closed-test", + "description": "Android Closed Test Form", + "type": "internal", + "targets": null, + "hwModels": null, + "marketplace": null, + "regions": null + }, + { + "shortCode": "t-deck-pro", + "url": "https://msh.to/t-deck-pro", + "description": "LilyGo T-Deck Pro", + "type": "vendor", + "targets": [ + "t-deck-pro" + ], + "hwModels": [ + 102 + ], + "marketplace": null, + "regions": null + }, + { + "shortCode": "rak4631_nomadstar_meteor_pro", + "url": "https://msh.to/rak4631_nomadstar_meteor_pro", + "description": "NomadStar Meteor Pro", + "type": "vendor", + "targets": [ + "rak4631_nomadstar_meteor_pro" + ], + "hwModels": [ + 96 + ], + "marketplace": null, + "regions": null + }, + { + "shortCode": "muziworks", + "url": "https://msh.to/muziworks", + "description": "muzi WORKS Homepage", + "type": "internal", + "targets": null, + "hwModels": null, + "marketplace": null, + "regions": null + }, + { + "shortCode": "r1-neo", + "url": "https://msh.to/r1-neo", + "description": "muzi WORKS R1 Neo", + "type": "vendor", + "targets": [ + "r1-neo" + ], + "hwModels": [ + 101 + ], + "marketplace": null, + "regions": null + }, + { + "shortCode": "muzi-base", + "url": "https://msh.to/muzi-base", + "description": "muzi WORKS Base System", + "type": "vendor", + "targets": [ + "muzi-base" + ], + "hwModels": [ + 93 + ], + "marketplace": null, + "regions": null + }, + { + "shortCode": "muzi-base-uno", + "url": "https://msh.to/muzi-base-uno", + "description": "muzi WORKS Base Uno", + "type": "vendor", + "targets": [ + "muzi-base" + ], + "hwModels": [ + 93 + ], + "marketplace": null, + "regions": null + }, + { + "shortCode": "muzi-base-duo", + "url": "https://msh.to/muzi-base-duo", + "description": "muzi WORKS Base Duo", + "type": "vendor", + "targets": [ + "muzi-base" + ], + "hwModels": [ + 93 + ], + "marketplace": null, + "regions": null + }, + { + "shortCode": "muzi-base-super-io", + "url": "https://msh.to/muzi-base-super-io", + "description": "muzi WORKS Base Super IO", + "type": "vendor", + "targets": [ + "muzi-base" + ], + "hwModels": [ + 93 + ], + "marketplace": null, + "regions": null + }, + { + "shortCode": "ttc-tickets", + "url": "https://msh.to/ttc-tickets", + "description": "The Things Conference Tickets", + "type": "internal", + "targets": null, + "hwModels": null, + "marketplace": null, + "regions": null + }, + { + "shortCode": "rokland-atlavox-makers-market", + "url": "https://msh.to/rokland-atlavox-makers-market", + "description": "Rokland Atlavox Makers Market", + "type": "marketplace", + "targets": null, + "hwModels": null, + "marketplace": "rokland", + "regions": [ + "AU", + "AT", + "BE", + "CA", + "DK", + "EC", + "FR", + "DE", + "IE", + "JP", + "NL", + "NZ", + "NO", + "PK", + "ES", + "SE", + "CH", + "GB", + "US" + ] + }, + { + "shortCode": "rokland-tlora-pager", + "url": "https://msh.to/rokland-tlora-pager", + "description": "Rokland T-Lora Pager", + "type": "marketplace", + "targets": [ + "tlora-pager" + ], + "hwModels": [ + 103 + ], + "marketplace": "rokland", + "regions": [ + "AU", + "AT", + "BE", + "CA", + "DK", + "EC", + "FR", + "DE", + "IE", + "JP", + "NL", + "NZ", + "NO", + "PK", + "ES", + "SE", + "CH", + "GB", + "US" + ] + }, + { + "shortCode": "tlora-pager", + "url": "https://msh.to/tlora-pager", + "description": "T-Lora Pager", + "type": "vendor", + "targets": [ + "tlora-pager" + ], + "hwModels": [ + 103 + ], + "marketplace": null, + "regions": null + }, + { + "shortCode": "hexaspot", + "url": "https://msh.to/hexaspot", + "description": "Hexaspot Meshtastic Products", + "type": "marketplace", + "targets": [], + "hwModels": [], + "marketplace": "hexaspot", + "regions": [ + "AT", + "BE", + "BG", + "CY", + "CZ", + "DE", + "DK", + "EE", + "ES", + "FI", + "FR", + "GR", + "HR", + "HU", + "IE", + "IT", + "LT", + "LU", + "LV", + "MT", + "NL", + "NO", + "PL", + "PT", + "RO", + "SE", + "SI", + "SK" + ] + }, + { + "shortCode": "ew26", + "url": "https://msh.to/ew26", + "description": "embeddedworld26 event page", + "type": "internal", + "targets": null, + "hwModels": null, + "marketplace": null, + "regions": null + }, + { + "shortCode": "hexaspot-heltec-v3", + "url": "https://msh.to/hexaspot-heltec-v3", + "description": "Heltec V3 (Hexaspot)", + "type": "marketplace", + "targets": [ + "heltec-v3" + ], + "hwModels": [ + 43 + ], + "marketplace": "hexaspot", + "regions": [ + "AT", + "BE", + "BG", + "CY", + "CZ", + "DE", + "DK", + "EE", + "ES", + "FI", + "FR", + "GR", + "HR", + "HU", + "IE", + "IT", + "LT", + "LU", + "LV", + "MT", + "NL", + "NO", + "PL", + "PT", + "RO", + "SE", + "SI", + "SK" + ] + }, + { + "shortCode": "hexaspot-heltec-v4", + "url": "https://msh.to/hexaspot-heltec-v4", + "description": "Heltec V4 (Hexaspot)", + "type": "marketplace", + "targets": [ + "heltec-v4" + ], + "hwModels": [ + 110 + ], + "marketplace": "hexaspot", + "regions": [ + "AT", + "BE", + "BG", + "CY", + "CZ", + "DE", + "DK", + "EE", + "ES", + "FI", + "FR", + "GR", + "HR", + "HU", + "IE", + "IT", + "LT", + "LU", + "LV", + "MT", + "NL", + "NO", + "PL", + "PT", + "RO", + "SE", + "SI", + "SK" + ] + }, + { + "shortCode": "hexaspot-wireless-tracker-v2", + "url": "https://msh.to/hexaspot-wireless-tracker-v2", + "description": "Heltec Wireless Tracker V2 (Hexaspot)", + "type": "marketplace", + "targets": [ + "heltec-wireless-tracker-v2" + ], + "hwModels": [ + 113 + ], + "marketplace": "hexaspot", + "regions": [ + "AT", + "BE", + "BG", + "CY", + "CZ", + "DE", + "DK", + "EE", + "ES", + "FI", + "FR", + "GR", + "HR", + "HU", + "IE", + "IT", + "LT", + "LU", + "LV", + "MT", + "NL", + "NO", + "PL", + "PT", + "RO", + "SE", + "SI", + "SK" + ] + } + ] +} diff --git a/androidApp/src/main/assets/marketplaces.json b/androidApp/src/main/assets/marketplaces.json deleted file mode 100644 index 49feb5c99..000000000 --- a/androidApp/src/main/assets/marketplaces.json +++ /dev/null @@ -1,126 +0,0 @@ -{ - "rokland": { - "regions": [ - "AU", - "AT", - "BE", - "CA", - "DK", - "EC", - "FR", - "DE", - "IE", - "JP", - "NL", - "NZ", - "NO", - "PK", - "ES", - "SE", - "CH", - "GB", - "US" - ], - "match": "prefix" - }, - "hexaspot": { - "regions": [ - "AT", - "BE", - "BG", - "CY", - "CZ", - "DE", - "DK", - "EE", - "ES", - "FI", - "FR", - "GR", - "HR", - "HU", - "IE", - "IT", - "LT", - "LU", - "LV", - "MT", - "NL", - "NO", - "PL", - "PT", - "RO", - "SE", - "SI", - "SK" - ], - "match": "prefix" - }, - "aliexpress": { - "regions": [], - "match": "suffix" - }, - "amazon": { - "regions": [ - "AU", - "CA", - "FR", - "DE", - "IE", - "JP", - "NL", - "ES", - "SE", - "GB", - "US" - ], - "match": "suffix" - }, - "tindie": { - "regions": [ - "US", - "CA", - "GB", - "DE", - "FR", - "AU", - "NL" - ], - "match": "suffix" - }, - "muzi": { - "regions": [ - "AU", - "AT", - "BE", - "CA", - "CZ", - "DK", - "FI", - "FR", - "DE", - "HK", - "IN", - "IE", - "IL", - "IT", - "JP", - "MY", - "NL", - "NZ", - "NO", - "PL", - "PT", - "SG", - "KR", - "ES", - "SE", - "CH", - "TW", - "AE", - "GB", - "US" - ], - "match": "prefix" - } -} \ No newline at end of file diff --git a/androidApp/src/main/assets/urls.json b/androidApp/src/main/assets/urls.json deleted file mode 100644 index 2b02b3fe6..000000000 --- a/androidApp/src/main/assets/urls.json +++ /dev/null @@ -1,1009 +0,0 @@ -{ - "Routes": [ - { - "ShortCode": "github", - "OriginalUrl": "https://github.com/meshtastic", - "Description": "Meshtastic GitHub Organization" - }, - { - "ShortCode": "youtube", - "OriginalUrl": "https://www.youtube.com/meshtastic", - "Description": "Meshtastic YouTube Channel" - }, - { - "ShortCode": "reddit", - "OriginalUrl": "https://www.reddit.com/r/meshtastic", - "Description": "Meshtastic Reddit Community" - }, - { - "ShortCode": "docs", - "OriginalUrl": "https://meshtastic.org/docs/", - "Description": "Meshtastic Documentation" - }, - { - "ShortCode": "discord", - "OriginalUrl": "https://discord.gg/meshtastic", - "Description": "Meshtastic Discord Server" - }, - { - "ShortCode": "web", - "OriginalUrl": "https://client.meshtastic.org/", - "Description": "Meshtastic Web Client" - }, - { - "ShortCode": "flash", - "OriginalUrl": "https://flasher.meshtastic.org/", - "Description": "Meshtastic Web Flasher" - }, - { - "ShortCode": "firmware", - "OriginalUrl": "https://github.com/meshtastic/firmware", - "Description": "Meshtastic Firmware Repository" - }, - { - "ShortCode": "android", - "OriginalUrl": "https://play.google.com/store/apps/details?id=com.geeksville.mesh", - "Description": "Meshtastic Android App" - }, - { - "ShortCode": "ios", - "OriginalUrl": "https://apple.co/3Auysep", - "Description": "Meshtastic iOS App" - }, - { - "ShortCode": "rak-collection", - "OriginalUrl": "https://store.rakwireless.com/collections/meshtastic?utm_source=website_partner&utm_medium=referral&utm_campaign=meshtastic_rak_collab", - "Description": "RAKwireless Meshtastic Collection" - }, - { - "ShortCode": "rak4631", - "OriginalUrl": "https://store.rakwireless.com/products/wisblock-meshtastic-starter-kit?utm_source=website_partner&utm_medium=referral&utm_campaign=meshtastic_rak_collab", - "Description": "WisMesh RAK4631 Starter Kit" - }, - { - "ShortCode": "rak3312", - "OriginalUrl": "https://store.rakwireless.com/products/meshtastic-starter-kit-esp32-s3-lora-sx1262?utm_source=website_partner&utm_medium=referral&utm_campaign=meshtastic_rak_collab", - "Description": "WisMesh ESP32-S3 Starter Kit" - }, - { - "ShortCode": "rak3401-1watt", - "OriginalUrl": "https://store.rakwireless.com/products/wismesh-1w-booster-starter-kit?utm_source=website_partner&utm_medium=referral&utm_campaign=meshtastic_rak_collab", - "Description": "WisMesh RAK3401 1W Starter Kit" - }, - { - "ShortCode": "rak_wismeshtap", - "OriginalUrl": "https://store.rakwireless.com/products/wismesh-tap?utm_source=website_partner&utm_medium=referral&utm_campaign=meshtastic_rak_collab", - "Description": "RAK WisMesh Tap" - }, - { - "ShortCode": "rak_wismeshtag", - "OriginalUrl": "https://store.rakwireless.com/products/wismesh-tag-meshtastic-gps-lora-tracker-ip66?utm_source=website_partner&utm_medium=referral&utm_campaign=meshtastic_rak_collab", - "Description": "RAK WisMesh Tag" - }, - { - "ShortCode": "rokland-wismesh-tag", - "OriginalUrl": "https://store.rokland.com/products/wismesh-tag-from-rakwireless-mokosmart-meshtastic-compatible-card-sized-node-us915-mhz", - "Description": "Rokland WisMesh Tag" - }, - { - "ShortCode": "hexaspot-wismesh-tag", - "OriginalUrl": "https://hexaspot.com/collections/meshtastic-products/products/wismesh-tag", - "Description": "Hexaspot WisMesh Tag" - }, - { - "ShortCode": "aliexpress-wismesh-tag", - "OriginalUrl": "https://www.aliexpress.com/item/1005009754254701.html?utm_source=website_partner&utm_medium=referral&utm_campaign=meshtastic_rak_collab", - "Description": "Aliexpress RAK WisMesh Tag" - }, - { - "ShortCode": "rak19007", - "OriginalUrl": "https://store.rakwireless.com/products/rak19007-wisblock-base-board-2nd-gen?utm_source=website_partner&utm_medium=referral&utm_campaign=meshtastic_rak_collab", - "Description": "RAKwireless RAK19007 WisBlock Base Board 2nd Gen" - }, - { - "ShortCode": "tbeam-s3-core", - "OriginalUrl": "https://lilygo.cc/products/t-beam-supreme-meshtastic", - "Description": "T-Beam Supreme" - }, - { - "ShortCode": "t-echo", - "OriginalUrl": "https://lilygo.cc/products/t-echo-meshtastic", - "Description": "T-Echo" - }, - { - "ShortCode": "t-watch-s3", - "OriginalUrl": "https://lilygo.cc/products/t-watch-s3", - "Description": "T-Watch S3" - }, - { - "ShortCode": "t-deck", - "OriginalUrl": "https://lilygo.cc/products/t-deck-meshtastic", - "Description": "T-Deck" - }, - { - "ShortCode": "tlora-t3s3-v1", - "OriginalUrl": "https://lilygo.cc/products/t3-s3-meshtastic", - "Description": "T3S3" - }, - { - "ShortCode": "heltec-mesh-node-t114", - "OriginalUrl": "https://heltec.org/project/mesh-node-t114/", - "Description": "Mesh Node T114" - }, - { - "ShortCode": "heltec-vision-master-e213", - "OriginalUrl": "https://heltec.org/project/vision-master-e213/", - "Description": "Vision Master E213" - }, - { - "ShortCode": "heltec-vision-master-e290", - "OriginalUrl": "https://heltec.org/project/vision-master-e290/", - "Description": "Vision Master E290" - }, - { - "ShortCode": "heltec-vision-master-t190", - "OriginalUrl": "https://heltec.org/project/vision-master-t190/", - "Description": "Vision Master T190" - }, - { - "ShortCode": "heltec-wireless-tracker", - "OriginalUrl": "https://heltec.org/project/wireless-tracker/", - "Description": "Wireless Tracker" - }, - { - "ShortCode": "heltec-wireless-tracker-v2", - "OriginalUrl": "https://heltec.org/project/wireless-tracker-v2/", - "Description": "Wireless Tracker V2" - }, - { - "ShortCode": "heltec-wireless-paper", - "OriginalUrl": "https://heltec.org/project/wireless-paper/", - "Description": "Wireless Paper" - }, - { - "ShortCode": "heltec-ht62-esp32c3-sx1262", - "OriginalUrl": "https://heltec.org/project/ht-ct62/", - "Description": "HT-CT62" - }, - { - "ShortCode": "wio-tracker-wm1110", - "OriginalUrl": "https://www.seeedstudio.com/Wio-Tracker-1110-Dev-Kit-for-Meshtastic.html", - "Description": "Wio Tracker WM1110 Dev Kit" - }, - { - "ShortCode": "tracker-t1000-e", - "OriginalUrl": "https://www.seeedstudio.com/SenseCAP-Card-Tracker-T1000-E-for-Meshtastic-p-5913.html", - "Description": "SenseCAP Card Tracker T1000-E" - }, - { - "ShortCode": "tracker-t1000-e-aliexpress", - "OriginalUrl": "https://www.aliexpress.us/item/3256807287978389.html", - "Description": "SenseCAP Card Tracker T1000-E Aliexpress" - }, - { - "ShortCode": "tracker-t1000-e-amazon", - "OriginalUrl": "https://www.amazon.com/dp/B0DJ6KGXKB", - "Description": "SenseCAP Card Tracker T1000-E Amazon" - }, - { - "ShortCode": "seeed-sensecap-indicator", - "OriginalUrl": "https://www.seeedstudio.com/SenseCAP-Indicator-D1L-for-Meshtastic-p-6304.html", - "Description": "SenseCAP Indicator" - }, - { - "ShortCode": "station-g2", - "OriginalUrl": "https://shop.uniteng.com/product/meshtastic-mesh-device-station-edition/", - "Description": "Station G2" - }, - { - "ShortCode": "rak2560", - "OriginalUrl": "https://store.rakwireless.com/products/wismesh-meshtastic-solar-repeater?utm_source=website_partner&utm_medium=referral&utm_campaign=meshtastic_rak_collab", - "Description": "WisMesh Repeater" - }, - { - "ShortCode": "heltec-v3", - "OriginalUrl": "https://heltec.org/project/wifi-lora-32-v3/", - "Description": "LoRa32 V3" - }, - { - "ShortCode": "heltec-wsl-v3", - "OriginalUrl": "https://heltec.org/project/wireless-stick-lite-v2/", - "Description": "WSL V3" - }, - { - "ShortCode": "heltec-v4", - "OriginalUrl": "https://heltec.org/project/wifi-lora-32-v4/", - "Description": "LoRa32 V4" - }, - { - "ShortCode": "seeed-xiao-s3", - "OriginalUrl": "https://www.seeedstudio.com/Wio-SX1262-with-XIAO-ESP32S3-p-5982.html", - "Description": "XIAO ESP32-S3 + Wio-SX1262 Kit" - }, - { - "ShortCode": "tlora-t3s3-epaper", - "OriginalUrl": "https://lilygo.cc/products/t3-s3-meshtastic", - "Description": "T3S3" - }, - { - "ShortCode": "ht-ct62", - "OriginalUrl": "https://heltec.org/project/ht-ct62/", - "Description": "HT-CT62" - }, - { - "ShortCode": "seeed_xiao_nrf52840_kit", - "OriginalUrl": "https://www.seeedstudio.com/XIAO-nRF52840-Wio-SX1262-Kit-for-Meshtastic-p-6400.html", - "Description": "XIAO nRF52840 & Wio-SX1262 Kit" - }, - { - "ShortCode": "seeed_xiao_nrf52840_kit_aliexpress", - "OriginalUrl": "https://www.aliexpress.us/item/3256808574469954.html", - "Description": "XIAO nRF52840 & Wio-SX1262 Kit Aliexpress" - }, - { - "ShortCode": "thinknode_m1", - "OriginalUrl": "https://www.elecrow.com/thinknode-m1-meshtastic-lora-signal-transceiver-powered-by-nrf52840-with-154-screen-support-gps.html", - "Description": "ThinkNode M1" - }, - { - "ShortCode": "thinknode_m2", - "OriginalUrl": "https://www.elecrow.com/thinknode-m2-meshtastic-lora-signal-transceiver-powered-by-esp32-s3-with-1-3-oled-display.html", - "Description": "ThinkNode M2" - }, - { - "ShortCode": "thinknode_m3", - "OriginalUrl": "https://www.elecrow.com/thinknode-m3-meshtastic-tracker-with-gps-wifi-ble-function-for-indoor-and-outdoor-positioning.html", - "Description": "ThinkNode M3" - }, - { - "ShortCode": "thinknode_m5", - "OriginalUrl": "https://www.elecrow.com/thinknode-m5-meshtastic-lora-signal-transceiver-esp32-s3-1-54-screen-gps-function.html", - "Description": "ThinkNode M5" - }, - { - "ShortCode": "thinknode_m4", - "OriginalUrl": "https://www.elecrow.com/thinknode-m4-power-bank-lora-device-with-meshtastic-lora-tracker-function-powered-by-nrf52840.html", - "Description": "ThinkNode M4" - }, - { - "ShortCode": "thinknode_m6", - "OriginalUrl": "https://www.elecrow.com/thinknode-m6-outdoor-solar-power-for-meshtastic-powered-by-nrf52840-supports-gps.html", - "Description": "ThinkNode M6" - }, - { - "ShortCode": "heltec-mesh-pocket-10000", - "OriginalUrl": "https://heltec.org/project/meshpocket/", - "Description": "MeshPocket" - }, - { - "ShortCode": "seeed_solar_node", - "OriginalUrl": "https://www.seeedstudio.com/SenseCAP-Solar-Node-P1-Pro-for-Meshtastic-LoRa-p-6412.html", - "Description": "SenseCAP Solar Node P1 Pro" - }, - { - "ShortCode": "seeed_solar_node_aliexpress", - "OriginalUrl": "https://www.aliexpress.us/item/3256808731224053.html", - "Description": "SenseCAP Solar Node P1 Pro Aliexpress" - }, - { - "ShortCode": "seeed_solar_node_amazon", - "OriginalUrl": "https://www.amazon.com/dp/B0FMDHBWX8", - "Description": "SenseCAP Solar Node P1 Pro Amazon" - }, - { - "ShortCode": "elecrow-adv-35-tft", - "OriginalUrl": "https://www.elecrow.com/crowpanel-advance-3-5-hmi-esp32-ai-display-for-meshtastic-320x240-ips-artificial-intelligent-screen.html", - "Description": "CrowPanel 3.5" - }, - { - "ShortCode": "elecrow-adv1-43-50-70-tft", - "OriginalUrl": "https://www.elecrow.com/crowpanel-advance-4-3-hmi-ai-screen-for-meshtastic-esp32-800x480-ips-touch-artificial-intelligent-display-2.html", - "Description": "CrowPanel 4.3" - }, - { - "ShortCode": "elecrow-adv-24-28-tft", - "OriginalUrl": "https://www.elecrow.com/crowpanel-advance-2-4-hmi-ai-display-for-meshtastic-esp32-320x240-ips-artificial-intelligent-touchscreen.html", - "Description": "CrowPanel 2.4" - }, - { - "ShortCode": "elecrow-adv-28-tft", - "OriginalUrl": "https://www.elecrow.com/crowpanel-advance-2-8-hmi-ai-display-for-meshtastic-esp32-320x240-artificial-ips-intelligent-touchscreen.html", - "Description": "CrowPanel 2.8" - }, - { - "ShortCode": "elecrow-adv1-50-tft", - "OriginalUrl": "https://www.elecrow.com/crowpanel-advance-5inch-hmi-esp32-ai-display-800x480-ips-artificial-intelligent-touch-screen-support-meshtastic.html", - "Description": "CrowPanel 5.0" - }, - { - "ShortCode": "elecrow-adv1-70-tft", - "OriginalUrl": "https://www.elecrow.com/crowpanel-advance-7-0-hmi-esp32-ai-display-800x480-artificial-intelligent-ips-touch-screen-for-meshtastic.html", - "Description": "CrowPanel 7.0" - }, - { - "ShortCode": "seeed_wio_tracker_L1", - "OriginalUrl": "https://www.seeedstudio.com/Wio-Tracker-L1-Pro-p-6454.html", - "Description": "Wio Tracker L1" - }, - { - "ShortCode": "seeed_wio_tracker_L1_aliexpress", - "OriginalUrl": "https://www.aliexpress.us/item/3256809394050623.html", - "Description": "Wio Tracker L1 Aliexpress" - }, - { - "ShortCode": "seeed_wio_tracker_L1_amazon", - "OriginalUrl": "https://www.amazon.com/dp/B0FNCS5ST1", - "Description": "Wio Tracker L1 Amazon" - }, - { - "ShortCode": "nano-g2-ultra", - "OriginalUrl": "https://shop.uniteng.com/product/meshtastic-mesh-device-nano-g2-ultra/", - "Description": "Nano G2 Ultra" - }, - { - "ShortCode": "rak11310", - "OriginalUrl": "https://store.rakwireless.com/products/wisblock-rp2040-starter-kit-for-meshtastic?utm_source=website_partner&utm_medium=referral&utm_campaign=meshtastic_rak_collab", - "Description": "RAK11310" - }, - { - "ShortCode": "rokland-rak11310", - "OriginalUrl": "https://store.rakwireless.com/products/wisblock-rp2040-starter-kit-for-meshtastic?utm_source=website_partner&utm_medium=referral&utm_campaign=meshtastic_rak_collab", - "Description": "Rokland RAKwireless RAK11310 WisBlock RP2040 Core Module" - }, - { - "ShortCode": "station-g2-tindie", - "OriginalUrl": "https://www.tindie.com/products/neilhao/meshtastic-mesh-device-station-g2/", - "Description": "Station G2 Tindie Listing" - }, - { - "ShortCode": "nano-g2-ultra-tindie", - "OriginalUrl": "https://www.tindie.com/products/neilhao/meshtastic-mesh-device-nano-g2-ultra/", - "Description": "Nano G2 Ultra Tindie Listing" - }, - { - "ShortCode": "t-deck-plus", - "OriginalUrl": "https://lilygo.cc/products/t-deck-plus-meshtastic", - "Description": "T-Deck Plus" - }, - { - "ShortCode": "rokland-meshtastic-starter-kit", - "OriginalUrl": "https://store.rokland.com/products/rak-wireless-wisblock-meshtastic-starter-kit", - "Description": "Rokland Meshtastic Starter Kit" - }, - { - "ShortCode": "rokland-t-deck-base", - "OriginalUrl": "https://store.rokland.com/products/lilygo-t-deck-portable-microcontroller-programmer-lora-915-mhz-h642?variant=41000826372179", - "Description": "Rokland T-Deck Base" - }, - { - "ShortCode": "rokland-t-deck-complete", - "OriginalUrl": "https://store.rokland.com/products/lilygo-t-deck-portable-microcontroller-programmer-lora-915-mhz-h642?variant=42122265690195", - "Description": "Rokland T-Deck Complete" - }, - { - "ShortCode": "rokland-t-deck-plus", - "OriginalUrl": "https://store.rokland.com/products/lilygo-t-deck-portable-microcontroller-programmer-lora-915-mhz-h642?variant=42283977834579", - "Description": "Rokland T-Deck Plus" - }, - { - "ShortCode": "rokland-t-echo", - "OriginalUrl": "https://store.rokland.com/products/lilygo-ttgo-meshtastic-t-echo-white-lora-sx1262-wireless-module-915mhz-nrf52840-gps-for-arduino?ref=8Bb2mUO5i-jKwt", - "Description": "Rokland T-Echo" - }, - { - "ShortCode": "rokland-t-echo-bme280", - "OriginalUrl": "https://store.rokland.com/products/lilygo-ttgo-meshtastic-t-echo-white-bme280-lora-sx1262-wireless-module-915mhz-nrf52840-gps-rtc-nfc-for-arduino?ref=8Bb2mUO5i-jKwt", - "Description": "Rokland T-Echo with BME280" - }, - { - "ShortCode": "rokland-rak19007", - "OriginalUrl": "https://store.rokland.com/products/rak-wireless-wisblock-base-board-2nd-gen-rak19007-ver-b-pid-110082", - "Description": "Rokland RAKwireless RAK19007 WisBlock Base Board 2nd Gen" - }, - { - "ShortCode": "hexaspot-rak19007", - "OriginalUrl": "https://hexaspot.com/collections/rakwireless-wisblock-base/products/rakwireless-rak19007-wisblock-base-board-2nd-gen", - "Description": "Hexaspot RAKwireless RAK19007 WisBlock Base Board 2nd Gen" - }, - { - "ShortCode": "rokland-starter-kit", - "OriginalUrl": "https://store.rokland.com/products/rak-wireless-wisblock-meshtastic-starter-kit", - "Description": "Rokland RAKwireless 4631 Starter Kit" - }, - { - "ShortCode": "hexaspot-starter-kit", - "OriginalUrl": "https://hexaspot.com/collections/wisblock-kits/products/wisblock-starter-kit-wisblock-basic-kit", - "Description": "Hexaspot RAKwireless 4631 Starter Kit" - }, - { - "ShortCode": "aliexpress-rak1921", - "OriginalUrl": "https://www.aliexpress.com/item/3256801470591730.html?utm_source=website_partner&utm_medium=referral&utm_campaign=meshtastic_rak_collab", - "Description": "RAK1921 OLED Display (AliExpress)" - }, - { - "ShortCode": "rak1921", - "OriginalUrl": "https://store.rakwireless.com/products/rak1921-oled-display-panel?utm_source=website_partner&utm_medium=referral&utm_campaign=meshtastic_rak_collab", - "Description": "RAK1921 OLED Display (RAK Store)" - }, - { - "ShortCode": "rokland-rak1921", - "OriginalUrl": "https://store.rokland.com/products/rak-wireless-wisblock-oled-display-rak1921-pid-110004", - "Description": "Rokland RAK1921 WisBlock OLED Display" - }, - { - "ShortCode": "muzi-rak1921", - "OriginalUrl": "https://muzi.works/products/rak-oled-display-ssd1306", - "Description": "Muzi Works RAK1921 OLED Display SSD1306" - }, - { - "ShortCode": "aliexpress-rak14000", - "OriginalUrl": "https://www.aliexpress.com/item/3256803245280485.html?utm_source=website_partner&utm_medium=referral&utm_campaign=meshtastic_rak_collab", - "Description": "RAK14000 E-Ink Display (AliExpress)" - }, - { - "ShortCode": "rak14000", - "OriginalUrl": "https://store.rakwireless.com/products/wisblock-epd-module-rak14000?utm_source=website_partner&utm_medium=referral&utm_campaign=meshtastic_rak_collab", - "Description": "RAK14000 E-Ink Display (RAK Store)" - }, - { - "ShortCode": "rokland-rak14000", - "OriginalUrl": "https://store.rokland.com/products/rak-wireless-wisblock-epd-module-rak14000-pid-110024", - "Description": "Rokland RAK14000 WisBlock E-Ink Display" - }, - { - "ShortCode": "aliexpress-rak12500", - "OriginalUrl": "https://www.aliexpress.com/item/3256802312416216.html?utm_source=website_partner&utm_medium=referral&utm_campaign=meshtastic_rak_collab", - "Description": "RAK12500 (AliExpress)" - }, - { - "ShortCode": "rak12500", - "OriginalUrl": "https://store.rakwireless.com/products/wisblock-gnss-location-module-rak12500?utm_source=website_partner&utm_medium=referral&utm_campaign=meshtastic_rak_collab", - "Description": "RAK12500 (RAK Store)" - }, - { - "ShortCode": "rak13300", - "OriginalUrl": "https://store.rakwireless.com/products/rak13300-wisblock-lpwan?utm_source=website_partner&utm_medium=referral&utm_campaign=meshtastic_rak_collab", - "Description": "RAK13300 LPWAN Module (RAK Store)" - }, - { - "ShortCode": "aliexpress-rak13002", - "OriginalUrl": "https://www.aliexpress.com/item/3256802904688489.html?utm_source=website_partner&utm_medium=referral&utm_campaign=meshtastic_rak_collab", - "Description": "RAK13002 IO Module (AliExpress)" - }, - { - "ShortCode": "rak13002", - "OriginalUrl": "https://store.rakwireless.com/products/adapter-module-rak13002?utm_source=website_partner&utm_medium=referral&utm_campaign=meshtastic_rak_collab", - "Description": "RAK13002 IO Module (RAK Store)" - }, - { - "ShortCode": "rokland-rak13002", - "OriginalUrl": "https://store.rokland.com/products/rak-wireless-rak13002-wisblock-io-adapter-module", - "Description": "Rokland RAK13002 WisBlock IO Adapter Module" - }, - { - "ShortCode": "muzi-rak13002", - "OriginalUrl": "https://muzi.works/products/rak-io-module", - "Description": "Muzi Works RAK13002 IO Module" - }, - { - "ShortCode": "rak6421", - "OriginalUrl": "https://store.rakwireless.com/products/meshtastic-raspberry-pi-hat-rak6421?utm_source=website_partner&utm_medium=referral&utm_campaign=meshtastic_rak_collab", - "Description": "WisMesh Pi Hat RAK6421 (RAK Store)" - }, - { - "ShortCode": "aliexpress-rak18001", - "OriginalUrl": "https://www.aliexpress.com/item/3256802312587439.html?utm_source=website_partner&utm_medium=referral&utm_campaign=meshtastic_rak_collab", - "Description": "RAK18001 RAK Buzzer (AliExpress)" - }, - { - "ShortCode": "rak18001", - "OriginalUrl": "https://store.rakwireless.com/products/wisblock-buzzer-module-rak18001?utm_source=website_partner&utm_medium=referral&utm_campaign=meshtastic_rak_collab", - "Description": "RAK18001 RAK Buzzer (RAK Store)" - }, - { - "ShortCode": "aliexpress-rak1901", - "OriginalUrl": "https://www.aliexpress.com/item/3256801444571922.html?utm_source=website_partner&utm_medium=referral&utm_campaign=meshtastic_rak_collab", - "Description": "RAK1901 Temperature and Humidity Sensor (AliExpress)" - }, - { - "ShortCode": "rak1901", - "OriginalUrl": "https://store.rakwireless.com/products/rak1901-shtc3-temperature-humidity-sensor?utm_source=website_partner&utm_medium=referral&utm_campaign=meshtastic_rak_collab", - "Description": "RAK1901 Temperature and Humidity Sensor (RAK Store)" - }, - { - "ShortCode": "aliexpress-rak1902", - "OriginalUrl": "https://www.aliexpress.com/item/3256801445721072.html?utm_source=website_partner&utm_medium=referral&utm_campaign=meshtastic_rak_collab", - "Description": "RAK-1902 Barometric Pressure Sensor (AliExpress)" - }, - { - "ShortCode": "rak1902", - "OriginalUrl": "https://store.rakwireless.com/products/rak1902-kps22hb-barometric-pressure-sensor?utm_source=website_partner&utm_medium=referral&utm_campaign=meshtastic_rak_collab", - "Description": "RAK-1902 Barometric Pressure Sensor (RAK Store)" - }, - { - "ShortCode": "aliexpress-rak1906", - "OriginalUrl": "https://www.aliexpress.com/item/3256801453209668.html?utm_source=website_partner&utm_medium=referral&utm_campaign=meshtastic_rak_collab", - "Description": "RAK1906 Environment Sensor (AliExpress)" - }, - { - "ShortCode": "rak1906", - "OriginalUrl": "https://store.rakwireless.com/products/rak1906-bme680-environment-sensor?utm_source=website_partner&utm_medium=referral&utm_campaign=meshtastic_rak_collab", - "Description": "RAK1906 Environment Sensor (RAK Store)" - }, - { - "ShortCode": "aliexpress-rak12002", - "OriginalUrl": "https://www.aliexpress.com/item/3256803919249064.html?utm_source=website_partner&utm_medium=referral&utm_campaign=meshtastic_rak_collab", - "Description": "RAK12002 WisBlock RTC Module (AliExpress)" - }, - { - "ShortCode": "rak12002", - "OriginalUrl": "https://store.rakwireless.com/products/rtc-module-rak12002?utm_source=website_partner&utm_medium=referral&utm_campaign=meshtastic_rak_collab", - "Description": "RAK12002 WisBlock RTC Module (RAK Store)" - }, - { - "ShortCode": "rokland-rak12002", - "OriginalUrl": "https://store.rokland.com/products/rak-wireless-rak12002-rtc-module-micro-crystal-rv-3028-c7-pid-100032", - "Description": "Rokland RAK12002 RTC Module Micro Crystal RV-3028-C7" - }, - { - "ShortCode": "aliexpress-wismesh-pocket-v2", - "OriginalUrl": "https://www.aliexpress.com/item/3256808087883682.html?utm_source=website_partner&utm_medium=referral&utm_campaign=meshtastic_rak_collab", - "Description": "WisMesh Pocket V2 (AliExpress)" - }, - { - "ShortCode": "rokland-wismesh-pocket-v2", - "OriginalUrl": "https://store.rokland.com/products/wismesh-pocket", - "Description": "WisMesh Pocket V2 (Rokland)" - }, - { - "ShortCode": "hexaspot-wismesh-pocket-v2", - "OriginalUrl": "https://hexaspot.com/collections/meshtastic-products/products/wismesh-pocket-v2-ready-to-use-meshtastic-device", - "Description": "WisMesh Pocket V2 (Hexaspot)" - }, - { - "ShortCode": "wismesh-pocket-v2", - "OriginalUrl": "https://store.rakwireless.com/products/wismesh-pocket?utm_source=website_partner&utm_medium=referral&utm_campaign=meshtastic_rak_collab", - "Description": "WisMesh Pocket V2 (RAK Store)" - }, - { - "ShortCode": "aliexpress-wismesh-pocket-mini", - "OriginalUrl": "https://www.aliexpress.com/item/3256807998160830.html?utm_source=website_partner&utm_medium=referral&utm_campaign=meshtastic_rak_collab", - "Description": "WisMesh Pocket Mini (Rokland)" - }, - { - "ShortCode": "rokland-wismesh-pocket-mini", - "OriginalUrl": "https://store.rokland.com/products/rakwireless-wismesh-pocket-mini-all-in-one-meshtastic-handheld-915-mhz-radio-with-lora-antenna", - "Description": "WisMesh Pocket Mini (Rokland)" - }, - { - "ShortCode": "wismesh-pocket-mini", - "OriginalUrl": "https://store.rakwireless.com/products/wismesh-pocket-mini?utm_source=website_partner&utm_medium=referral&utm_campaign=meshtastic_rak_collab", - "Description": "WisMesh Pocket Mini (RAK Store)" - }, - { - "ShortCode": "aliexpress-rak19026", - "OriginalUrl": "https://www.aliexpress.com/item/3256808063797462.html?utm_source=website_partner&utm_medium=referral&utm_campaign=meshtastic_rak_collab", - "Description": "WisMesh Baseboard (AliExpress)" - }, - { - "ShortCode": "rokland-rak19026", - "OriginalUrl": "https://store.rokland.com/products/rakwireless-wismesh-baseboard-rak19026-oled-mounted-gnss-motion-sensor-pid-115125", - "Description": "WisMesh Baseboard (Rokland)" - }, - { - "ShortCode": "rak19026", - "OriginalUrl": "https://store.rakwireless.com/products/wismesh-baseboard-rak19026?utm_source=website_partner&utm_medium=referral&utm_campaign=meshtastic_rak_collab", - "Description": "WisMesh Baseboard (RAK Store)" - }, - { - "ShortCode": "aliexpress-wismesh-tap", - "OriginalUrl": "https://www.aliexpress.com/item/3256808097004202.html?utm_source=website_partner&utm_medium=referral&utm_campaign=meshtastic_rak_collab", - "Description": "RAK WisMesh Tap (AliExpress)" - }, - { - "ShortCode": "aliexpress-board-one", - "OriginalUrl": "https://www.aliexpress.com/item/3256802139951068.html?utm_source=website_partner&utm_medium=referral&utm_campaign=meshtastic_rak_collab", - "Description": "RAK WisMesh Board ONE (AliExpress)" - }, - { - "ShortCode": "board-one", - "OriginalUrl": "https://store.rakwireless.com/products/wismesh-board-one-meshtastic-node?utm_source=website_partner&utm_medium=referral&utm_campaign=meshtastic_rak_collab", - "Description": "RAK WisMesh Board ONE (RAK Store)" - }, - { - "ShortCode": "rokland-board-one", - "OriginalUrl": "https://store.rokland.com/products/rakwireless-wismesh-b1-board", - "Description": "Rokland WisMesh Board ONE (US915 MHz)" - }, - { - "ShortCode": "wismesh-repeater", - "OriginalUrl": "https://store.rakwireless.com/products/wismesh-meshtastic-solar-repeater?utm_source=website_partner&utm_medium=referral&utm_campaign=meshtastic_rak_collab", - "Description": "WisMesh Repeater (RAK Store)" - }, - { - "ShortCode": "aliexpress-wismesh-repeater", - "OriginalUrl": "https://www.aliexpress.com/item/3256808393658502.html?utm_source=website_partner&utm_medium=referral&utm_campaign=meshtastic_rak_collab", - "Description": "WisMesh Repeater (AliExpress)" - }, - { - "ShortCode": "aliexpress-wismesh-repeater-mini", - "OriginalUrl": "https://www.aliexpress.com/item/2251832722300348.html?utm_source=website_partner&utm_medium=referral&utm_campaign=meshtastic_rak_collab", - "Description": "WisMesh Repeater Mini (AliExpress)" - }, - { - "ShortCode": "hexaspot-wismesh-repeater-mini", - "OriginalUrl": "https://hexaspot.com/collections/meshtastic-products/products/wismesh-repeater-mini", - "Description": "WisMesh Repeater Mini (Hexaspot)" - }, - { - "ShortCode": "wismesh-repeater-mini", - "OriginalUrl": "https://store.rakwireless.com/products/wishmesh-meshtastic-solar-repeater-mini?utm_source=website_partner&utm_medium=referral&utm_campaign=meshtastic_rak_collab", - "Description": "WisMesh Repeater Mini (RAK Store)" - }, - { - "ShortCode": "aliexpress-wismesh-ethernet-gateway", - "OriginalUrl": "https://www.aliexpress.com/item/3256801470547683.html?utm_source=website_partner&utm_medium=referral&utm_campaign=meshtastic_rak_collab", - "Description": "WisMesh Ethernet MQTT Gateway (AliExpress)" - }, - { - "ShortCode": "wismesh-ethernet-gateway", - "OriginalUrl": "https://store.rakwireless.com/products/wismesh-ethernet-gateway?utm_source=website_partner&utm_medium=referral&utm_campaign=meshtastic_rak_collab", - "Description": "WisMesh Ethernet MQTT Gateway (RAK Store)" - }, - { - "ShortCode": "aliexpress-wismesh-wifi-gateway", - "OriginalUrl": "https://www.aliexpress.com/item/3256802139923708.html?utm_source=website_partner&utm_medium=referral&utm_campaign=meshtastic_rak_collab", - "Description": "WisMesh WiFi MQTT Gateway (AliExpress)" - }, - { - "ShortCode": "wismesh-wifi-gateway", - "OriginalUrl": "https://store.rakwireless.com/products/wismesh-wifi-gateway?utm_source=website_partner&utm_medium=referral&utm_campaign=meshtastic_rak_collab", - "Description": "WisMesh WiFi MQTT Gateway (RAK Store)" - }, - { - "ShortCode": "aliexpress-board-one-pocket", - "OriginalUrl": "https://www.aliexpress.com/item/3256802139951068.html?utm_source=website_partner&utm_medium=referral&utm_campaign=meshtastic_rak_collab", - "Description": "RAK WisMesh Board ONE Pocket (AliExpress)" - }, - { - "ShortCode": "board-one-pocket", - "OriginalUrl": "https://store.rakwireless.com/products/wismesh-board-one-pocket-meshtastic-node?utm_source=website_partner&utm_medium=referral&utm_campaign=meshtastic_rak_collab", - "Description": "RAK WisMesh Board ONE Pocket (RAK Store)" - }, - { - "ShortCode": "aliexpress-wismesh-unify-enclosure", - "OriginalUrl": "https://www.aliexpress.com/item/3256808182747014.html?utm_source=website_partner&utm_medium=referral&utm_campaign=meshtastic_rak_collab", - "Description": "WisMesh Unify Enclosure (AliExpress)" - }, - { - "ShortCode": "wismesh-unify-enclosure", - "OriginalUrl": "https://store.rakwireless.com/products/wismesh-unify-enclosure?utm_source=website_partner&utm_medium=referral&utm_campaign=meshtastic_rak_collab", - "Description": "WisMesh Unify Enclosure (RAK Store)" - }, - { - "ShortCode": "aliexpress-wismesh-antenna", - "OriginalUrl": "https://www.aliexpress.com/item/3256808177346156.html?utm_source=website_partner&utm_medium=referral&utm_campaign=meshtastic_rak_collab", - "Description": "WisMesh Antenna (AliExpress)" - }, - { - "ShortCode": "wismesh-antenna", - "OriginalUrl": "https://store.rakwireless.com/products/wismesh-antenna?utm_source=website_partner&utm_medium=referral&utm_campaign=meshtastic_rak_collab", - "Description": "WisMesh Antenna (RAK Store)" - }, - { - "ShortCode": "muzi-rak4631", - "OriginalUrl": "https://muzi.works/products/rak-wisblock-meshtastic-starter-kit-us915", - "Description": "Muzi RAK4631 Starter Kit" - }, - { - "ShortCode": "aliexpress-rak19007", - "OriginalUrl": "https://www.aliexpress.com/item/3256803957557617.html?utm_source=website_partner&utm_medium=referral&utm_campaign=meshtastic_rak_collab", - "Description": "RAK19007 (AliExpress)" - }, - { - "ShortCode": "aliexpress-starter-kit", - "OriginalUrl": "https://www.aliexpress.com/item/1005006901039995.html?utm_source=website_partner&utm_medium=referral&utm_campaign=meshtastic_rak_collab", - "Description": "WisMesh RAK4631 Starter Kit (AliExpress)" - }, - { - "ShortCode": "rak19003", - "OriginalUrl": "https://store.rakwireless.com/products/wisblock-base-board-rak19003?utm_source=website_partner&utm_medium=referral&utm_campaign=meshtastic_rak_collab", - "Description": "RAK19003 (RAK Store)" - }, - { - "ShortCode": "aliexpress-rak19003", - "OriginalUrl": "https://www.aliexpress.com/item/3256803225234826.html?utm_source=website_partner&utm_medium=referral&utm_campaign=meshtastic_rak_collab", - "Description": "RAK19003 (AliExpress)" - }, - { - "ShortCode": "rak19001", - "OriginalUrl": "https://store.rakwireless.com/products/rak19001-wisblock-dual-io-base-board?utm_source=website_partner&utm_medium=referral&utm_campaign=meshtastic_rak_collab", - "Description": "RAK19001 WisBlock Dual IO Base Board (RAK Store)" - }, - { - "ShortCode": "aliexpress-rak19001", - "OriginalUrl": "https://www.aliexpress.com/item/3256803962043191.html?utm_source=website_partner&utm_medium=referral&utm_campaign=meshtastic_rak_collab", - "Description": "RAK19001 WisBlock Dual IO Base Board (AliExpress)" - }, - { - "ShortCode": "rokland-19003", - "OriginalUrl": "https://store.rokland.com/products/rak-wireless-wisblock-mini-base-board-rak19003-ver-b-pid-306024", - "Description": "Rokland WisBlock Mini Base Board RAK19003 (Ver B)" - }, - { - "ShortCode": "hexaspot-19003", - "OriginalUrl": "https://hexaspot.com/collections/rakwireless-wisblock-base/products/rakwireless-rak19003-wisblock-mini-base-board", - "Description": "Hexaspot WisBlock Mini Base Board RAK19003 (Ver B)" - }, - { - "ShortCode": "rokland-19001", - "OriginalUrl": "https://store.rokland.com/products/rak-wireless-wisblock-dual-io-base-board-rak19001-pid-110081", - "Description": "Rokland WisBlock Dual IO Base Board RAK19001" - }, - { - "ShortCode": "hexaspot-19001", - "OriginalUrl": "https://hexaspot.com/collections/rakwireless-wisblock-base/products/rakwireless-rak19001-wisblock-dual-io-base-board", - "Description": "Hexaspot WisBlock Dual IO Base Board RAK19001" - }, - { - "ShortCode": "rokland-4631", - "OriginalUrl": "https://store.rokland.com/products/rak-wireless-rak4631-nordic-nrf52840-ble-core-module-for-lorawan-with-lora-sx1262", - "Description": "Rokland RAK4631 Nordic nRF52840 BLE Core Module for LoRaWAN with LoRa SX1262" - }, - { - "ShortCode": "hexaspot-4631", - "OriginalUrl": "https://hexaspot.com/collections/wisblock-kits/products/wisblock-meshtastic-starter-kit-eu868-the-basic-rak4631-meshtastic-kit-for-lora", - "Description": "Hexaspot RAK4631 Nordic nRF52840 BLE Core Module for LoRaWAN with LoRa SX1262" - }, - { - "ShortCode": "aliexpress-rak4631", - "OriginalUrl": "https://www.aliexpress.us/item/3256801470104151.html?utm_source=website_partner&utm_medium=referral&utm_campaign=meshtastic_rak_collab", - "Description": "RAK4631 Nordic nRF52840 BLE Core Module (AliExpress)" - }, - { - "ShortCode": "rakwireless-4631", - "OriginalUrl": "https://store.rakwireless.com/products/rak4631-lpwan-node?utm_source=website_partner&utm_medium=referral&utm_campaign=meshtastic_rak_collab", - "Description": "RAK4631 Nordic nRF52840 BLE Core Module (AliExpress)" - }, - { - "ShortCode": "rakwireless-rak11310", - "OriginalUrl": "https://store.rakwireless.com/products/rak11310-wisblock-lpwan-module?utm_source=website_partner&utm_medium=referral&utm_campaign=meshtastic_rak_collab", - "Description": "RAK11310 RP2040 Core Module)" - }, - { - "ShortCode": "rakwireless-rak3312", - "OriginalUrl": "https://store.rakwireless.com/products/wisblock-core-module-rak3312-lora-wifi-ble", - "Description": "RAK3312 ESP32-S3 Core Module" - }, - { - "ShortCode": "hexaspot-rak3312", - "OriginalUrl": "https://hexaspot.com/collections/rakwireless-wisblock/products/espressif-esp32-s3-wifi-ble-dual-core-module-for-lorawan%C2%AE-with-lora-sx1262", - "Description": "Hexaspot RAK3312 ESP32-S3 Core Module" - }, - { - "ShortCode": "rokland-rak3312", - "OriginalUrl": "https://store.rokland.com/products/rak3312-espressif-esp32-s3-wifi-ble-dual-core-module-for-lorawan-with-lora-sx1262-116208", - "Description": "Rokland RAK3312 ESP32-S3 Core Module" - }, - { - "ShortCode": "rokland-rak3312-starter-kit", - "OriginalUrl": "https://store.rokland.com/products/wismesh-rak3312-starter-kit-with-meshtastic-firmware", - "Description": "Rokland RAK3312 ESP32-S3 Starter Kit" - }, - { - "ShortCode": "aliexpress-rak11310", - "OriginalUrl": "https://www.aliexpress.us/item/3256803225175784.html?utm_source=website_partner&utm_medium=referral&utm_campaign=meshtastic_rak_collab", - "Description": "RAK11310 RP2040 Core Module (AliExpress)" - }, - { - "ShortCode": "rokland-1901", - "OriginalUrl": "https://store.rokland.com/products/rak-wireless-rak1901-temperature-and-humidity-sensor-sensirion-shtc3-pid-100001", - "Description": "Rokland RAK1901 Temperature and Humidity Sensor" - }, - { - "ShortCode": "rokland-1902", - "OriginalUrl": "https://store.rokland.com/products/rak-wireless-rak1902-barometric-pressure-sensor-stmicroelectronics-lps22hb-100010-2-pack", - "Description": "Rokland RAK1902 Barometric Pressure Sensor" - }, - { - "ShortCode": "rokland-1906", - "OriginalUrl": "https://store.rokland.com/products/rak-wireless-rak1906-wisblock-environment-sensor-bosch-bme680", - "Description": "Rokland RAK1906 WisBlock Environment Sensor" - }, - { - "ShortCode": "aliexpress-wismesh-tap", - "OriginalUrl": "https://www.aliexpress.com/item/3256808097004202.html?utm_source=website_partner&utm_medium=referral&utm_campaign=meshtastic_rak_collab", - "Description": "RAKwireless WisMesh Tap (AliExpress)" - }, - { - "ShortCode": "rokland-wismesh-tap", - "OriginalUrl": "https://store.rokland.com/products/rakwireless-wismesh-tap-touchscreen-915-mhz-handheld-or-mountable-unit-lora-gps", - "Description": "RAKwireless WisMesh Tap (Rokland)" - }, - { - "ShortCode": "rakdap1", - "OriginalUrl": "https://store.rakwireless.com/products/daplink-tool?utm_source=website_partner&utm_medium=referral&utm_campaign=meshtastic_rak_collab", - "Description": "RAKwireless RAKDAP1 Debug and Flash Tool" - }, - { - "ShortCode": "rokland-heltec-wsl-v3", - "OriginalUrl": "https://store.rokland.com/collections/heltec-products/products/heltec-wireless-stick-litev3-902-928-mhz/", - "Description": "Rokland WSL V3" - }, - { - "ShortCode": "aliexpress-heltec-wsl-v3", - "OriginalUrl": "https://www.aliexpress.us/item/3256807466584635.htm", - "Description": "Aliexpress WSL V3" - }, - { - "ShortCode": "rokland-heltec-wireless-tracker", - "OriginalUrl": "https://store.rokland.com/collections/heltec-products/products/heltec-wireless-tracker-v1-1-wi-fi-lora-bt-gnss/", - "Description": "Rokland Wireless Tracker" - }, - { - "ShortCode": "aliexpress-heltec-wireless-tracker", - "OriginalUrl": "https://www.aliexpress.us/item/3256805495189423.html", - "Description": "Aliexpress Wireless Tracker" - }, - { - "ShortCode": "aliexpress-heltec-wireless-paper", - "OriginalUrl": "https://www.aliexpress.us/item/3256805461611876.html", - "Description": "Aliexpress Wireless Paper" - }, - { - "ShortCode": "rokland-heltec-wireless-paper", - "OriginalUrl": "https://store.rokland.com/collections/heltec-products/products/heltec-wireless-paper-wi-fi-lora-bt/", - "Description": "Rokland Wireless Paper" - }, - { - "ShortCode": "muzi-heltec-mesh-node-t114", - "OriginalUrl": "https://muzi.works/products/heltec-mesh-node-t114/", - "Description": "MuziWorks Mesh Node T114" - }, - { - "ShortCode": "aliexpress-heltec-mesh-node-t114", - "OriginalUrl": "https://www.aliexpress.com/item/1005007460963705.html", - "Description": "Aliexpress Mesh Node T114" - }, - { - "ShortCode": "aliexpress-heltec-vision-master-e213", - "OriginalUrl": "https://www.aliexpress.com/item/1005007209756502.html", - "Description": "Aliexpress Vision Master E213" - }, - { - "ShortCode": "aliexpress-heltec-vision-master-e290", - "OriginalUrl": "https://www.aliexpress.com/item/1005007234361986.html", - "Description": "Aliexpress Vision Master E290" - }, - { - "ShortCode": "aliexpress-heltec-vision-master-t190", - "OriginalUrl": "https://www.aliexpress.us/item/3256807135629435.html", - "Description": "Aliexpress Vision Master T190" - }, - { - "ShortCode": "seeed-wio-tracker-l1-oled", - "OriginalUrl": "https://www.seeedstudio.com/Wio-Tracker-L1-p-6453.html", - "Description": "Wio Tracker L1 (with OLED)" - }, - { - "ShortCode": "seeed-wio-tracker-l1-oled_aliexpress", - "OriginalUrl": "https://www.aliexpress.us/item/3256809320083189.html", - "Description": "Wio Tracker L1 (with OLED)" - }, - { - "ShortCode": "seeed_wio_tracker_L1_eink", - "OriginalUrl": "https://www.seeedstudio.com/Wio-Tracker-L1-E-ink-p-6456.html", - "Description": "Wio Tracker L1 (with E-Ink)" - }, - { - "ShortCode": "seeed_wio_tracker_L1_eink_amazon", - "OriginalUrl": "https://www.amazon.com/dp/B0FJWT5FYW", - "Description": "Wio Tracker L1 (with E-Ink) Amazon" - }, - { - "ShortCode": "seeed-wio-tracker-l1-lite", - "OriginalUrl": "https://www.seeedstudio.com/Wio-Tracker-L1-Lite-p-6455.html", - "Description": "Wio Tracker L1 Lite (no display)" - }, - { - "ShortCode": "seeed_solar_node_p1", - "OriginalUrl": "https://www.seeedstudio.com/SenseCAP-Solar-Node-P1-for-Meshtastic-LoRa-p-6425.html", - "Description": "SenseCAP Solar Node P1" - }, - { - "ShortCode": "seeed_solar_node_p1_aliexpress", - "OriginalUrl": "https://www.aliexpress.us/item/3256808731224053.html", - "Description": "SenseCAP Solar Node P1 Aliexpress" - }, - { - "ShortCode": "android-closed-test", - "OriginalUrl": "https://forms.gle/3dZCSTQWRbMSHkPd6", - "Description": "Android Closed Test Form" - }, - { - "ShortCode": "t-deck-pro", - "OriginalUrl": "https://lilygo.cc/products/t-deck-pro-meshtastic", - "Description": "LilyGo T-Deck Pro" - }, - { - "ShortCode": "rak4631_nomadstar_meteor_pro", - "OriginalUrl": "https://nomadstar.ch/meteor-pro/", - "Description": "NomadStar Meteor Pro" - }, - { - "ShortCode": "muziworks", - "OriginalUrl": "https://muzi.works/", - "Description": "muzi WORKS Homepage" - }, - { - "ShortCode": "r1-neo", - "OriginalUrl": "https://muzi.works/products/r1-neo-complete-meshtastic-device", - "Description": "muzi WORKS R1 Neo" - }, - { - "ShortCode": "muzi-base", - "OriginalUrl": "https://muzi.works/pages/base", - "Description": "muzi WORKS Base System" - }, - { - "ShortCode": "muzi-base-uno", - "OriginalUrl": "https://muzi.works/products/base-uno", - "Description": "muzi WORKS Base Uno" - }, - { - "ShortCode": "muzi-base-duo", - "OriginalUrl": "https://muzi.works/products/base-duo", - "Description": "muzi WORKS Base Duo" - }, - { - "ShortCode": "muzi-base-super-io", - "OriginalUrl": "https://muzi.works/products/super-io", - "Description": "muzi WORKS Base Super IO" - }, - { - "ShortCode": "ttc-tickets", - "OriginalUrl": "https://www.thethingsconference.com/partner-invitations/recgog1edgosiv3b8", - "Description": "The Things Conference Tickets" - }, - { - "ShortCode": "rokland-atlavox-makers-market", - "OriginalUrl": "https://store.rokland.com/products/atlavox-beacon-solar-meshtastic-node-w-n-female-antenna", - "Description": "Rokland Atlavox Makers Market" - }, - { - "ShortCode": "rokland-tlora-pager", - "OriginalUrl": "https://store.rokland.com/products/lilygo-t-lora-pager-us-915-mhz-lora-esp32-s3-handheld-aiot-programmable-development-device-k257-01", - "Description": "Rokland T-Lora Pager" - }, - { - "ShortCode": "tlora-pager", - "OriginalUrl": "https://lilygo.cc/products/t-lora-pager-meshtastic", - "Description": "T-Lora Pager" - }, - { - "ShortCode": "hexaspot", - "OriginalUrl": "https://hexaspot.com/collections/meshtastic-products", - "Description": "Hexaspot Meshtastic Products" - }, - { - "ShortCode": "ew26", - "OriginalUrl": "https://meshtastic.com/ew26", - "Description": "embeddedworld26 event page" - }, - { - "ShortCode": "hexaspot-heltec-v3", - "OriginalUrl": "https://hexaspot.com/collections/meshtastic-products/products/heltec-wifi-lora-32-v3", - "Description": "Heltec V3 (Hexaspot)" - }, - { - "ShortCode": "hexaspot-heltec-v4", - "OriginalUrl": "https://hexaspot.com/collections/meshtastic-products/products/heltec-wifi-lora-32-v4", - "Description": "Heltec V4 (Hexaspot)" - }, - { - "ShortCode": "hexaspot-wireless-tracker-v2", - "OriginalUrl": "https://hexaspot.com/collections/meshtastic-products/products/heltec-wireless-tracker-v2", - "Description": "Heltec Wireless Tracker V2 (Hexaspot)" - } - ] -} \ No newline at end of file diff --git a/core/data/src/androidMain/kotlin/org/meshtastic/core/data/datasource/DeviceLinksJsonDataSourceImpl.kt b/core/data/src/androidMain/kotlin/org/meshtastic/core/data/datasource/DeviceLinksJsonDataSourceImpl.kt new file mode 100644 index 000000000..0d96fee77 --- /dev/null +++ b/core/data/src/androidMain/kotlin/org/meshtastic/core/data/datasource/DeviceLinksJsonDataSourceImpl.kt @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2026 Meshtastic LLC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.meshtastic.core.data.datasource + +import android.app.Application +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.decodeFromStream +import org.koin.core.annotation.Single +import org.meshtastic.core.model.NetworkDeviceLink +import org.meshtastic.core.model.NetworkDeviceLinksResponse + +@Single +class DeviceLinksJsonDataSourceImpl(private val application: Application) : DeviceLinksJsonDataSource { + + // Tolerant parser so additional fields in the bundled snapshot don't break deserialization on older app versions. + @OptIn(ExperimentalSerializationApi::class) + private val json = Json { + ignoreUnknownKeys = true + isLenient = true + exceptionsWithDebugInfo = false + } + + @OptIn(ExperimentalSerializationApi::class) + override fun loadDeviceLinksFromJsonAsset(): List = + application.assets.open(DEVICE_LINKS_ASSET).use { inputStream -> + json.decodeFromStream(inputStream).links + } + + private companion object { + const val DEVICE_LINKS_ASSET = "device_links.json" + } +} diff --git a/core/data/src/androidMain/kotlin/org/meshtastic/core/data/datasource/MshToLinksJsonDataSourceImpl.kt b/core/data/src/androidMain/kotlin/org/meshtastic/core/data/datasource/MshToLinksJsonDataSourceImpl.kt deleted file mode 100644 index 18d6d7135..000000000 --- a/core/data/src/androidMain/kotlin/org/meshtastic/core/data/datasource/MshToLinksJsonDataSourceImpl.kt +++ /dev/null @@ -1,69 +0,0 @@ -/* - * Copyright (c) 2026 Meshtastic LLC - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ -@file:OptIn(ExperimentalSerializationApi::class) - -package org.meshtastic.core.data.datasource - -import android.app.Application -import co.touchlab.kermit.Logger -import kotlinx.serialization.ExperimentalSerializationApi -import kotlinx.serialization.json.Json -import kotlinx.serialization.json.decodeFromStream -import org.koin.core.annotation.Single -import org.meshtastic.core.model.MshToMarketplace -import org.meshtastic.core.model.MshToRoute -import org.meshtastic.core.model.MshToUrlsFile - -@Single -class MshToLinksJsonDataSourceImpl(private val application: Application) : MshToLinksJsonDataSource { - - // Tolerant parser: tolerate extra fields/trailing data so a stale bundled file never crashes the import. - private val json = Json { - ignoreUnknownKeys = true - isLenient = true - exceptionsWithDebugInfo = false - } - - // The bundled assets are immutable for the install's lifetime, so parse once and reuse — these are read on the - // node-detail flow's hot path (once per hardware emission). - private val routes: List by lazy { - runCatching { application.assets.open(URLS_ASSET).use { json.decodeFromStream(it).routes } } - .onFailure { Logger.w(it) { "Unable to load $URLS_ASSET for device links" } } - .getOrDefault(emptyList()) - } - - private val marketplaces: Map by lazy { - runCatching { - application.assets.open(MARKETPLACES_ASSET).use { - json.decodeFromStream>(it) - } - } - .onFailure { - Logger.w(it) { "Unable to load $MARKETPLACES_ASSET; marketplace links won't be region-filtered" } - } - .getOrDefault(emptyMap()) - } - - override fun loadRoutes(): List = routes - - override fun loadMarketplaces(): Map = marketplaces - - private companion object { - const val URLS_ASSET = "urls.json" - const val MARKETPLACES_ASSET = "marketplaces.json" - } -} diff --git a/core/data/src/commonMain/kotlin/org/meshtastic/core/data/datasource/MshToLinksJsonDataSource.kt b/core/data/src/commonMain/kotlin/org/meshtastic/core/data/datasource/DeviceLinksJsonDataSource.kt similarity index 58% rename from core/data/src/commonMain/kotlin/org/meshtastic/core/data/datasource/MshToLinksJsonDataSource.kt rename to core/data/src/commonMain/kotlin/org/meshtastic/core/data/datasource/DeviceLinksJsonDataSource.kt index 74a54acb3..06134a77c 100644 --- a/core/data/src/commonMain/kotlin/org/meshtastic/core/data/datasource/MshToLinksJsonDataSource.kt +++ b/core/data/src/commonMain/kotlin/org/meshtastic/core/data/datasource/DeviceLinksJsonDataSource.kt @@ -16,14 +16,9 @@ */ package org.meshtastic.core.data.datasource -import org.meshtastic.core.model.MshToMarketplace -import org.meshtastic.core.model.MshToRoute +import org.meshtastic.core.model.NetworkDeviceLink -/** Reads the bundled msh.to link data: `urls.json` (short codes) and `marketplaces.json` (region metadata). */ -interface MshToLinksJsonDataSource { - /** Routes from the bundled `urls.json`, or empty if missing/malformed. */ - fun loadRoutes(): List - - /** Marketplace metadata from the bundled `marketplaces.json`, keyed by marketplace identifier. */ - fun loadMarketplaces(): Map +/** Loads the bundled device-links snapshot (a frozen copy of the `/resource/deviceLinks` API response). */ +interface DeviceLinksJsonDataSource { + fun loadDeviceLinksFromJsonAsset(): List } diff --git a/core/data/src/commonMain/kotlin/org/meshtastic/core/data/repository/DeviceHardwareRepositoryImpl.kt b/core/data/src/commonMain/kotlin/org/meshtastic/core/data/repository/DeviceHardwareRepositoryImpl.kt index 0c48d6a17..bb02284b9 100644 --- a/core/data/src/commonMain/kotlin/org/meshtastic/core/data/repository/DeviceHardwareRepositoryImpl.kt +++ b/core/data/src/commonMain/kotlin/org/meshtastic/core/data/repository/DeviceHardwareRepositoryImpl.kt @@ -139,8 +139,8 @@ class DeviceHardwareRepositoryImpl( "DeviceHardwareRepository: network refresh timed out after ${NETWORK_REFRESH_TIMEOUT_MS}ms" } } else { - // Reconcile msh.to links against the freshest catalog (isVendor + orphan pruning). Runs outside - // the network timeout so a deadline can't cancel it mid-write and leave links half-reconciled. + // Refresh msh.to device links from the API after a hardware refresh. Runs outside the hardware + // network timeout so that deadline can't cancel it mid-write. deviceLinkRepository.reconcile() } } diff --git a/core/data/src/commonMain/kotlin/org/meshtastic/core/data/repository/DeviceLinkMatcher.kt b/core/data/src/commonMain/kotlin/org/meshtastic/core/data/repository/DeviceLinkMatcher.kt deleted file mode 100644 index ce88e9917..000000000 --- a/core/data/src/commonMain/kotlin/org/meshtastic/core/data/repository/DeviceLinkMatcher.kt +++ /dev/null @@ -1,111 +0,0 @@ -/* - * Copyright (c) 2026 Meshtastic LLC - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ -package org.meshtastic.core.data.repository - -import org.meshtastic.core.model.DeviceLink - -/** - * Pure matching logic for associating msh.to [DeviceLink]s with a device's `platformioTarget`. Ported from the - * Meshtastic-Apple `DeviceLinksSection` (multi-tier matching: exact vendor, product variant, marketplace), so the two - * platforms surface the same links. - */ -object DeviceLinkMatcher { - - /** - * Links relevant to [target], region-filtered and sorted with vendor/variant links first. - * - * @param links all imported links. - * @param marketplaceKeys known marketplace identifiers (from `marketplaces.json`). - * @param deviceTargets all known device `platformioTarget`s — used to exclude other devices' links. - * @param target the viewed device's `platformioTarget`. - * @param region the user's ISO 3166-1 alpha-2 region for marketplace filtering. - */ - fun match( - links: List, - marketplaceKeys: Set, - deviceTargets: Set, - target: String, - region: String, - ): List { - val variants = buildTargetVariants(target) - return links - .filter { link -> matches(link, marketplaceKeys, deviceTargets, target, variants, region) } - .sortedByDescending { it.isVendor || !isMarketplaceLink(it.shortCode, marketplaceKeys) } - } - - @Suppress("ReturnCount") - private fun matches( - link: DeviceLink, - marketplaceKeys: Set, - deviceTargets: Set, - target: String, - variants: List, - region: String, - ): Boolean { - val code = link.shortCode - - // Exact vendor match always wins. - if (code == target) return true - - // A vendor link for a different device is never shown here. - if (link.isVendor && code != target) return false - - // Variant/marketplace-suffix: "-..." or "_...". - val matchesPrefix = variants.any { code.startsWith("${it}_") || code.startsWith("$it-") } - - // Known marketplace prefix: "-" or "_". - val matchesMarketplacePrefix = - variants.any { variant -> marketplaceKeys.any { mp -> code == "$mp-$variant" || code == "${mp}_$variant" } } - - if (!matchesPrefix && !matchesMarketplacePrefix) return false - - // A prefix hit that is itself a different device's target belongs to that device, not this one. - if (matchesPrefix && code in deviceTargets && code != target) return false - - // Region filter: null regions = vendor/variant (always), empty = worldwide, else must include the region. - val regions = link.regions ?: return true - if (regions.isEmpty()) return true - return region in regions - } - - /** True when [code] carries a known marketplace prefix or suffix. */ - fun isMarketplaceLink(code: String, marketplaceKeys: Set): Boolean = - marketplaceKeyFor(code, marketplaceKeys) != null - - /** - * The marketplace identifier [code] belongs to (as a delimiter-bounded prefix `mp-`/`mp_` or suffix `-mp`/`_mp`), - * or `null` if none. This is the single source of truth for "is this a marketplace link" — used for import-time - * region tagging, sort ordering, and UI prominence — so the classifications never disagree. Delimiter bounds avoid - * mis-tagging codes that merely begin with a marketplace name (e.g. `muziworks` is NOT `muzi`). - */ - fun marketplaceKeyFor(code: String, marketplaceKeys: Set): String? = marketplaceKeys.firstOrNull { mp -> - code.startsWith("$mp-") || code.startsWith("${mp}_") || code.endsWith("-$mp") || code.endsWith("_$mp") - } - - /** - * Alternate target strings for matching. Strips a leading `rak` (e.g. `rak4631` → `4631`) to absorb msh.to naming - * inconsistencies like `rokland-4631`. - */ - fun buildTargetVariants(target: String): List { - val variants = mutableListOf(target) - if (target.startsWith("rak")) { - val stripped = target.removePrefix("rak") - if (stripped.isNotEmpty()) variants.add(stripped) - } - return variants - } -} diff --git a/core/data/src/commonMain/kotlin/org/meshtastic/core/data/repository/DeviceLinkRepositoryImpl.kt b/core/data/src/commonMain/kotlin/org/meshtastic/core/data/repository/DeviceLinkRepositoryImpl.kt index b0cf2d23f..daa197377 100644 --- a/core/data/src/commonMain/kotlin/org/meshtastic/core/data/repository/DeviceLinkRepositoryImpl.kt +++ b/core/data/src/commonMain/kotlin/org/meshtastic/core/data/repository/DeviceLinkRepositoryImpl.kt @@ -23,92 +23,125 @@ import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.map import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock +import kotlinx.coroutines.withContext +import kotlinx.coroutines.withTimeoutOrNull import org.koin.core.annotation.Single +import org.meshtastic.core.common.util.nowMillis import org.meshtastic.core.common.util.safeCatching -import org.meshtastic.core.data.datasource.DeviceHardwareLocalDataSource import org.meshtastic.core.data.datasource.DeviceLinkLocalDataSource -import org.meshtastic.core.data.datasource.MshToLinksJsonDataSource +import org.meshtastic.core.data.datasource.DeviceLinksJsonDataSource import org.meshtastic.core.database.entity.asEntity import org.meshtastic.core.database.entity.asExternalModel +import org.meshtastic.core.di.CoroutineDispatchers import org.meshtastic.core.model.DeviceLink -import org.meshtastic.core.model.MshToMarketplace +import org.meshtastic.core.model.NetworkDeviceLink +import org.meshtastic.core.model.toDeviceLink +import org.meshtastic.core.model.util.TimeConstants +import org.meshtastic.core.network.DeviceLinksRemoteDataSource import org.meshtastic.core.repository.DeviceLinkRepository +import kotlin.concurrent.Volatile +/** + * Caches the resolved device-links catalog from the Meshtastic API (`/resource/deviceLinks`). The server does all the + * classification (type/targets/regions), so the client just seeds from a bundled snapshot, refreshes from the network, + * and filters the cache. Mirrors [DeviceHardwareRepositoryImpl]'s seed → single-flight refresh pattern. + */ @Single class DeviceLinkRepositoryImpl( - private val jsonDataSource: MshToLinksJsonDataSource, + private val remoteDataSource: DeviceLinksRemoteDataSource, + private val jsonDataSource: DeviceLinksJsonDataSource, private val localDataSource: DeviceLinkLocalDataSource, - private val deviceHardwareLocalDataSource: DeviceHardwareLocalDataSource, + private val dispatchers: CoroutineDispatchers, ) : DeviceLinkRepository { - /** Guards the import so concurrent collectors don't run it more than once at a time. */ - private val importMutex = Mutex() + /** Serializes seeding and network refreshes so concurrent collectors don't duplicate writes. */ + private val writeMutex = Mutex() + + @Volatile private var lastRefreshMillis = 0L override suspend fun ensureImported() { - if (localDataSource.count() > 0) return - importMutex.withLock { if (localDataSource.count() == 0) doImport() } + ensureSeeded() } override suspend fun reconcile() { - importMutex.withLock { doImport() } + writeMutex.withLock { + safeCatching { + // Bound only the network call by the timeout; the DB write runs after so a deadline can't cancel it + // mid-write and leave the cache half-pruned. + val remoteLinks = + withTimeoutOrNull(NETWORK_REFRESH_TIMEOUT_MS) { remoteDataSource.getDeviceLinks() } + if (remoteLinks == null) { + Logger.w { + "DeviceLinkRepository: network refresh timed out after ${NETWORK_REFRESH_TIMEOUT_MS}ms" + } + } else { + store(remoteLinks) + lastRefreshMillis = nowMillis + } + } + .onFailure { e -> Logger.w(e) { "DeviceLinkRepository: network refresh failed" } } + } } - override suspend fun getLinksForTarget(platformioTarget: String, regionCode: String): List { - if (platformioTarget.isBlank()) return emptyList() - ensureImported() - val links = localDataSource.getAll().map { it.asExternalModel() } - val marketplaceKeys = jsonDataSource.loadMarketplaces().keys - val deviceTargets = deviceHardwareLocalDataSource.getAllTargets().toSet() - return DeviceLinkMatcher.match( - links = links, - marketplaceKeys = marketplaceKeys, - deviceTargets = deviceTargets, - target = platformioTarget, - region = regionCode, - ) - } + override suspend fun getLinksForTarget(platformioTarget: String, regionCode: String): List = + withContext(dispatchers.io) { + if (platformioTarget.isBlank()) return@withContext emptyList() + ensureSeeded() + // Deliberately non-blocking: the node-detail flow must stay snappy. Freshness arrives via reconcile(), + // triggered by the device-hardware refresh that resolves this device's hardware in the first place. + localDataSource + .getAll() + .map { it.asExternalModel() } + .filter { link -> platformioTarget in link.targets.orEmpty() } + .filter { link -> + val regions = link.regions + regions.isNullOrEmpty() || regionCode in regions + } + .sortedByDescending { it.isVendor } + } override fun observeAllLinks(): Flow> = flow { - ensureImported() + ensureSeeded() + refreshIfStale() emitAll(localDataSource.observeAll().map { entities -> entities.map { it.asExternalModel() } }) } - /** Loads bundled `urls.json`, classifies each short code, upserts, and prunes orphans. Mirrors Apple's import. */ - private suspend fun doImport() { - safeCatching { - val routes = jsonDataSource.loadRoutes() - if (routes.isEmpty()) { - Logger.w { "DeviceLinkRepository: no routes in bundled urls.json; skipping import" } - return@safeCatching + /** Seeds the table from the bundled snapshot if empty (fresh install, data clear, radio switch). */ + private suspend fun ensureSeeded() { + if (localDataSource.count() > 0) return + writeMutex.withLock { + if (localDataSource.count() == 0) { + safeCatching { store(jsonDataSource.loadDeviceLinksFromJsonAsset()) } + .onFailure { e -> Logger.w(e) { "DeviceLinkRepository: failed to seed from bundled JSON" } } } - val marketplaces = jsonDataSource.loadMarketplaces() - val deviceTargets = deviceHardwareLocalDataSource.getAllTargets().toSet() - - val links = - routes.map { route -> - val isVendor = route.shortCode in deviceTargets - DeviceLink( - shortCode = route.shortCode, - originalUrl = route.originalUrl, - description = route.description, - isVendor = isVendor, - regions = if (isVendor) null else marketplaceRegions(route.shortCode, marketplaces), - ) - } - - localDataSource.upsertAll(links.map { it.asEntity() }) - localDataSource.deleteNotIn(links.map { it.shortCode }) - Logger.i { "DeviceLinkRepository: imported ${links.size} msh.to links" } } - .onFailure { Logger.w(it) { "DeviceLinkRepository: device links import failed" } } + } + + /** Best-effort network refresh, gated by [CACHE_EXPIRATION_TIME_MS]. */ + private suspend fun refreshIfStale() { + if (nowMillis - lastRefreshMillis > CACHE_EXPIRATION_TIME_MS) reconcile() } /** - * Shipping regions for a marketplace short code, or null when it is not a marketplace link. Uses the same - * delimiter-aware classifier as the matcher/UI so a code's classification (vendor/variant vs marketplace) is - * consistent everywhere — independent of the `match` hint in `marketplaces.json`, which is unreliable in practice - * (e.g. AliExpress is declared `suffix` yet most codes use the `aliexpress-` prefix form). + * Maps resolved API links to the cached domain model, upserts them, and prunes short codes that no longer exist. + * Internal links (GitHub, YouTube, …) are dropped — they never belong to a device's purchase section. An empty list + * is ignored rather than wiping the cache on a bad response. */ - private fun marketplaceRegions(code: String, marketplaces: Map): List? = - DeviceLinkMatcher.marketplaceKeyFor(code, marketplaces.keys)?.let { marketplaces.getValue(it).regions } + private suspend fun store(networkLinks: List) { + val links = networkLinks.filter { it.type != NetworkDeviceLink.TYPE_INTERNAL }.map { it.toDeviceLink() } + if (links.isEmpty()) { + Logger.w { "DeviceLinkRepository: no device links to store; leaving cache untouched" } + return + } + localDataSource.upsertAll(links.map { it.asEntity() }) + localDataSource.deleteNotIn(links.map { it.shortCode }) + Logger.i { "DeviceLinkRepository: stored ${links.size} device links" } + } + + private companion object { + private val CACHE_EXPIRATION_TIME_MS = TimeConstants.ONE_DAY.inWholeMilliseconds + + /** Maximum time to wait for the remote API before falling back to cached/bundled data. */ + private const val NETWORK_REFRESH_TIMEOUT_MS = 5_000L + } } diff --git a/core/data/src/commonTest/kotlin/org/meshtastic/core/data/repository/DeviceLinkMatcherTest.kt b/core/data/src/commonTest/kotlin/org/meshtastic/core/data/repository/DeviceLinkMatcherTest.kt deleted file mode 100644 index b9b69edd2..000000000 --- a/core/data/src/commonTest/kotlin/org/meshtastic/core/data/repository/DeviceLinkMatcherTest.kt +++ /dev/null @@ -1,147 +0,0 @@ -/* - * Copyright (c) 2026 Meshtastic LLC - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ -package org.meshtastic.core.data.repository - -import org.meshtastic.core.model.DeviceLink -import kotlin.test.Test -import kotlin.test.assertEquals -import kotlin.test.assertFalse -import kotlin.test.assertNull -import kotlin.test.assertTrue - -/** - * Tests for [DeviceLinkMatcher], grounded in the acceptance scenarios of the Meshtastic-Apple `010-device-mshto-links` - * spec. Mirrors the as-built `DeviceLinksSection` matching (platformioTarget, not hwModelSlug). - */ -class DeviceLinkMatcherTest { - - private val marketplaceKeys = setOf("rokland", "hexaspot", "aliexpress", "amazon", "tindie", "muzi") - - private val deviceTargets = - setOf("rak4631", "heltec-v3", "seeed_solar_node", "tbeam", "rak4631_nomadstar_meteor_pro") - - private fun link(shortCode: String, isVendor: Boolean = false, regions: List? = null) = DeviceLink( - shortCode = shortCode, - originalUrl = "https://example.com/$shortCode", - isVendor = isVendor, - regions = regions, - ) - - private fun match(links: List, target: String, region: String = "US") = - DeviceLinkMatcher.match(links, marketplaceKeys, deviceTargets, target, region).map { it.shortCode } - - @Test - fun exactVendorMatchIsIncluded() { - val result = match(listOf(link("heltec-v3", isVendor = true)), target = "heltec-v3") - assertEquals(listOf("heltec-v3"), result) - } - - @Test - fun foreignVendorLinkIsExcluded() { - // Scenario 5: rak4631_nomadstar_meteor_pro (a different device's target) must NOT show for rak4631. - val result = - match( - listOf(link("rak4631", isVendor = true), link("rak4631_nomadstar_meteor_pro", isVendor = true)), - target = "rak4631", - ) - assertEquals(listOf("rak4631"), result) - } - - @Test - fun productVariantIsIncludedAndProminent() { - val result = match(listOf(link("rak4631_epaper")), target = "rak4631") - assertEquals(listOf("rak4631_epaper"), result) - } - - @Test - fun marketplaceLinkIsRegionFiltered() { - val links = listOf(link("rokland-rak4631", regions = listOf("US", "CA"))) - assertEquals(listOf("rokland-rak4631"), match(links, target = "rak4631", region = "US")) - assertEquals(emptyList(), match(links, target = "rak4631", region = "DE")) - } - - @Test - fun rakPrefixIsStrippedForMarketplaceVariantMatch() { - // "rokland-4631" should match device "rak4631" via the rak-stripped variant "4631". - val result = match(listOf(link("rokland-4631", regions = listOf("US"))), target = "rak4631", region = "US") - assertEquals(listOf("rokland-4631"), result) - } - - @Test - fun worldwideMarketplaceShowsRegardlessOfRegion() { - val links = listOf(link("rak4631_aliexpress", regions = emptyList())) - assertEquals(listOf("rak4631_aliexpress"), match(links, target = "rak4631", region = "ZZ")) - } - - @Test - fun unrelatedLinksProduceEmptyResult() { - val links = - listOf( - link("github"), - link("heltec-v3", isVendor = true), - link("rokland-heltec-v3", regions = listOf("US")), - ) - assertEquals(emptyList(), match(links, target = "tbeam")) - } - - @Test - fun anotherDevicesTargetIsNotMatchedAsVariant() { - // "rak4631_nomadstar_meteor_pro" prefix-matches "rak4631_" but is itself a device target → excluded. - val result = match(listOf(link("rak4631_nomadstar_meteor_pro")), target = "rak4631") - assertEquals(emptyList(), result) - } - - @Test - fun vendorAndVariantSortBeforeMarketplace() { - val links = - listOf( - link("rak4631_aliexpress", regions = emptyList()), - link("rak4631", isVendor = true), - link("rokland-rak4631", regions = listOf("US")), - link("rak4631_epaper"), - ) - val result = match(links, target = "rak4631", region = "US") - // Vendor + variant first (order among them preserved from input), marketplace links after. - assertEquals(listOf("rak4631", "rak4631_epaper", "rak4631_aliexpress", "rokland-rak4631"), result) - } - - @Test - fun buildTargetVariantsStripsRakPrefix() { - assertEquals(listOf("rak4631", "4631"), DeviceLinkMatcher.buildTargetVariants("rak4631")) - assertEquals(listOf("heltec-v3"), DeviceLinkMatcher.buildTargetVariants("heltec-v3")) - // Bare "rak" strips to empty and is not added. - assertEquals(listOf("rak"), DeviceLinkMatcher.buildTargetVariants("rak")) - } - - @Test - fun isMarketplaceLinkDetectsPrefixAndSuffix() { - assertTrue(DeviceLinkMatcher.isMarketplaceLink("rokland-rak4631", marketplaceKeys)) - assertTrue(DeviceLinkMatcher.isMarketplaceLink("heltec-v3_aliexpress", marketplaceKeys)) - assertFalse(DeviceLinkMatcher.isMarketplaceLink("heltec-v3", marketplaceKeys)) - } - - @Test - fun marketplaceKeyForUsesDelimiterBounds() { - // Both prefix and suffix forms resolve to their marketplace... - assertEquals("rokland", DeviceLinkMatcher.marketplaceKeyFor("rokland-rak4631", marketplaceKeys)) - assertEquals("aliexpress", DeviceLinkMatcher.marketplaceKeyFor("aliexpress-rak1921", marketplaceKeys)) - assertEquals("aliexpress", DeviceLinkMatcher.marketplaceKeyFor("rak4631_aliexpress", marketplaceKeys)) - // ...but a code that merely begins with a marketplace name is NOT that marketplace. - assertNull(DeviceLinkMatcher.marketplaceKeyFor("muziworks", marketplaceKeys)) - assertNull(DeviceLinkMatcher.marketplaceKeyFor("heltec-v3", marketplaceKeys)) - } -} diff --git a/core/data/src/jvmTest/kotlin/org/meshtastic/core/data/repository/DeviceLinkRepositoryImplTest.kt b/core/data/src/jvmTest/kotlin/org/meshtastic/core/data/repository/DeviceLinkRepositoryImplTest.kt index a76c648f2..14d2f5274 100644 --- a/core/data/src/jvmTest/kotlin/org/meshtastic/core/data/repository/DeviceLinkRepositoryImplTest.kt +++ b/core/data/src/jvmTest/kotlin/org/meshtastic/core/data/repository/DeviceLinkRepositoryImplTest.kt @@ -18,145 +18,168 @@ package org.meshtastic.core.data.repository import kotlinx.coroutines.test.UnconfinedTestDispatcher import kotlinx.coroutines.test.runTest -import org.meshtastic.core.data.datasource.DeviceHardwareLocalDataSource import org.meshtastic.core.data.datasource.DeviceLinkLocalDataSource -import org.meshtastic.core.data.datasource.MshToLinksJsonDataSource +import org.meshtastic.core.data.datasource.DeviceLinksJsonDataSource import org.meshtastic.core.di.CoroutineDispatchers -import org.meshtastic.core.model.MshToMarketplace -import org.meshtastic.core.model.MshToRoute import org.meshtastic.core.model.NetworkDeviceHardware +import org.meshtastic.core.model.NetworkDeviceLink +import org.meshtastic.core.model.NetworkDeviceLinksResponse +import org.meshtastic.core.model.NetworkFirmwareReleases +import org.meshtastic.core.network.DeviceLinksRemoteDataSource +import org.meshtastic.core.network.service.ApiService import org.meshtastic.core.testing.FakeDatabaseProvider import kotlin.test.AfterTest import kotlin.test.BeforeTest import kotlin.test.Test import kotlin.test.assertEquals -import kotlin.test.assertNull import kotlin.test.assertTrue class DeviceLinkRepositoryImplTest { - private class FakeMshToLinksJsonDataSource( - var routes: List, - var marketplaces: Map, - ) : MshToLinksJsonDataSource { - override fun loadRoutes(): List = routes + /** Only [getDeviceLinks] is exercised; the other endpoints are never called by the link repository. */ + private class FakeApiService(var response: NetworkDeviceLinksResponse) : ApiService { + override suspend fun getDeviceHardware(): List = error("unused") - override fun loadMarketplaces(): Map = marketplaces + override suspend fun getDeviceLinks(): NetworkDeviceLinksResponse = response + + override suspend fun getFirmwareReleases(): NetworkFirmwareReleases = error("unused") + } + + private class FakeDeviceLinksJsonDataSource(var links: List) : DeviceLinksJsonDataSource { + override fun loadDeviceLinksFromJsonAsset(): List = links } private val dispatcher = UnconfinedTestDispatcher() private val dispatchers = CoroutineDispatchers(main = dispatcher, io = dispatcher, default = dispatcher) private lateinit var dbProvider: FakeDatabaseProvider - private lateinit var linkLocal: DeviceLinkLocalDataSource - private lateinit var hardwareLocal: DeviceHardwareLocalDataSource - private lateinit var json: FakeMshToLinksJsonDataSource + private lateinit var local: DeviceLinkLocalDataSource + private lateinit var api: FakeApiService + private lateinit var seed: FakeDeviceLinksJsonDataSource private lateinit var repository: DeviceLinkRepositoryImpl - private val marketplaces = - mapOf( - "rokland" to MshToMarketplace(regions = listOf("US"), match = "prefix"), - "aliexpress" to MshToMarketplace(regions = emptyList(), match = "suffix"), - ) - - private fun route(shortCode: String) = - MshToRoute(shortCode = shortCode, originalUrl = "https://example.com/$shortCode", description = shortCode) + private fun link( + shortCode: String, + type: String = NetworkDeviceLink.TYPE_VENDOR, + targets: List? = null, + regions: List? = null, + ) = NetworkDeviceLink( + shortCode = shortCode, + url = "https://msh.to/$shortCode", + description = shortCode, + type = type, + targets = targets, + regions = regions, + ) @BeforeTest fun setup() { dbProvider = FakeDatabaseProvider() - linkLocal = DeviceLinkLocalDataSource(dbProvider, dispatchers) - hardwareLocal = DeviceHardwareLocalDataSource(dbProvider, dispatchers) - json = - FakeMshToLinksJsonDataSource( - routes = - listOf(route("rak4631"), route("rokland-rak4631"), route("rak4631_aliexpress"), route("github")), - marketplaces = marketplaces, + local = DeviceLinkLocalDataSource(dbProvider, dispatchers) + api = FakeApiService(NetworkDeviceLinksResponse()) + seed = FakeDeviceLinksJsonDataSource(emptyList()) + repository = + DeviceLinkRepositoryImpl( + remoteDataSource = DeviceLinksRemoteDataSource(api, dispatchers), + jsonDataSource = seed, + localDataSource = local, + dispatchers = dispatchers, ) - repository = DeviceLinkRepositoryImpl(json, linkLocal, hardwareLocal) } @AfterTest fun tearDown() = dbProvider.close() - private suspend fun seedDeviceTargets(vararg targets: String) { - hardwareLocal.insertAllDeviceHardware( - targets.mapIndexed { i, t -> NetworkDeviceHardware(hwModel = i + 1, platformioTarget = t) }, - ) - } - @Test - fun importClassifiesVendorAndMarketplaceLinks() = runTest(dispatcher) { - seedDeviceTargets("rak4631", "heltec-v3") - repository.reconcile() - - val byCode = linkLocal.getAll().associateBy { it.shortCode } - assertEquals(4, byCode.size) - - // rak4631 is a known device target → vendor, no regions. - assertTrue(byCode.getValue("rak4631").isVendor) - assertNull(byCode.getValue("rak4631").regions) - - // rokland-rak4631 → prefix marketplace, region-tagged. - assertTrue(!byCode.getValue("rokland-rak4631").isVendor) - assertEquals(listOf("US"), byCode.getValue("rokland-rak4631").regions) - - // rak4631_aliexpress → suffix marketplace, worldwide (empty regions). - assertEquals(emptyList(), byCode.getValue("rak4631_aliexpress").regions) - - // github → neither vendor nor marketplace, null regions. - assertTrue(!byCode.getValue("github").isVendor) - assertNull(byCode.getValue("github").regions) - } - - @Test - fun reconcilePrunesOrphanedShortCodes() = runTest(dispatcher) { - seedDeviceTargets("rak4631") - repository.reconcile() - assertEquals(4, linkLocal.count()) - - // Drop "github" from the bundled file and reconcile again. - json.routes = json.routes.filterNot { it.shortCode == "github" } - repository.reconcile() - - val codes = linkLocal.getAll().map { it.shortCode }.toSet() - assertEquals(setOf("rak4631", "rokland-rak4631", "rak4631_aliexpress"), codes) - } - - @Test - fun aliexpressPrefixFormIsClassifiedAsWorldwideMarketplace() = runTest(dispatcher) { - // AliExpress is declared match="suffix" yet most bundled codes use the `aliexpress-` prefix form; - // import must still classify it as a (worldwide) marketplace link, not a null-region variant. - json.routes = listOf(route("rak4631"), route("aliexpress-rak4631")) - seedDeviceTargets("rak4631") - repository.reconcile() - - assertEquals(emptyList(), linkLocal.getAll().single { it.shortCode == "aliexpress-rak4631" }.regions) - } - - @Test - fun bareMarketplaceNamePrefixIsNotMistagged() = runTest(dispatcher) { - // "muziworks" merely begins with "muzi" — delimiter bounds must keep it from inheriting muzi's regions. - json = - FakeMshToLinksJsonDataSource( - routes = listOf(route("muziworks")), - marketplaces = mapOf("muzi" to MshToMarketplace(regions = listOf("US"), match = "prefix")), + fun seedsFromBundledJsonWhenEmptyAndDropsInternalLinks() = runTest(dispatcher) { + seed.links = + listOf( + link("rak4631", targets = listOf("rak4631")), + link("github", type = NetworkDeviceLink.TYPE_INTERNAL), ) - repository = DeviceLinkRepositoryImpl(json, linkLocal, hardwareLocal) - seedDeviceTargets("rak4631") - repository.reconcile() + repository.ensureImported() - assertNull(linkLocal.getAll().single().regions) + assertEquals(setOf("rak4631"), local.getAll().map { it.shortCode }.toSet()) } @Test fun ensureImportedSeedsOnlyWhenEmpty() = runTest(dispatcher) { - seedDeviceTargets("rak4631") + seed.links = listOf(link("rak4631", targets = listOf("rak4631"))) repository.ensureImported() - assertEquals(4, linkLocal.count()) + assertEquals(1, local.count()) - // A second ensureImported with a larger bundled file must NOT re-import (table already populated). - json.routes = json.routes + route("new-code") + // A larger snapshot must NOT re-seed once the table is populated. + seed.links = seed.links + link("heltec-v3", targets = listOf("heltec-v3")) repository.ensureImported() - assertEquals(4, linkLocal.count()) + assertEquals(1, local.count()) + } + + @Test + fun getLinksForTargetFiltersByTargetAndRegionVendorFirst() = runTest(dispatcher) { + api.response = + NetworkDeviceLinksResponse( + links = + listOf( + link( + "rokland-rak4631", + type = NetworkDeviceLink.TYPE_MARKETPLACE, + targets = listOf("rak4631"), + regions = listOf("US"), + ), + link("rak4631", targets = listOf("rak4631")), + link("heltec-v3", targets = listOf("heltec-v3")), + link( + "de-only", + type = NetworkDeviceLink.TYPE_MARKETPLACE, + targets = listOf("rak4631"), + regions = listOf("DE"), + ), + ), + ) + repository.reconcile() + + val links = repository.getLinksForTarget("rak4631", regionCode = "US") + + // de-only filtered by region; heltec-v3 filtered by target; vendor sorted ahead of marketplace. + assertEquals(listOf("rak4631", "rokland-rak4631"), links.map { it.shortCode }) + assertTrue(links.first().isVendor) + } + + @Test + fun worldwideLinksShowRegardlessOfRegion() = runTest(dispatcher) { + api.response = + NetworkDeviceLinksResponse( + links = + listOf( + link("ww", type = NetworkDeviceLink.TYPE_MARKETPLACE, targets = listOf("t"), regions = null), + ), + ) + repository.reconcile() + + assertEquals(listOf("ww"), repository.getLinksForTarget("t", regionCode = "ZZ").map { it.shortCode }) + } + + @Test + fun reconcilePrunesShortCodesNoLongerInCatalog() = runTest(dispatcher) { + api.response = + NetworkDeviceLinksResponse( + links = listOf(link("a", targets = listOf("t")), link("b", targets = listOf("t"))), + ) + repository.reconcile() + assertEquals(2, local.count()) + + api.response = NetworkDeviceLinksResponse(links = listOf(link("a", targets = listOf("t")))) + repository.reconcile() + assertEquals(setOf("a"), local.getAll().map { it.shortCode }.toSet()) + } + + @Test + fun emptyResponseLeavesCacheUntouched() = runTest(dispatcher) { + api.response = NetworkDeviceLinksResponse(links = listOf(link("a", targets = listOf("t")))) + repository.reconcile() + assertEquals(1, local.count()) + + api.response = NetworkDeviceLinksResponse(links = emptyList()) + repository.reconcile() + assertEquals(1, local.count()) } } diff --git a/core/database/schemas/org.meshtastic.core.database.MeshtasticDatabase/43.json b/core/database/schemas/org.meshtastic.core.database.MeshtasticDatabase/43.json new file mode 100644 index 000000000..a2e94c0ae --- /dev/null +++ b/core/database/schemas/org.meshtastic.core.database.MeshtasticDatabase/43.json @@ -0,0 +1,1581 @@ +{ + "formatVersion": 1, + "database": { + "version": 43, + "identityHash": "c58b8f2f228a2a98ef9faf320b388373", + "entities": [ + { + "tableName": "my_node", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`myNodeNum` INTEGER NOT NULL, `model` TEXT, `firmwareVersion` TEXT, `couldUpdate` INTEGER NOT NULL, `shouldUpdate` INTEGER NOT NULL, `currentPacketId` INTEGER NOT NULL, `messageTimeoutMsec` INTEGER NOT NULL, `minAppVersion` INTEGER NOT NULL, `maxChannels` INTEGER NOT NULL, `hasWifi` INTEGER NOT NULL, `deviceId` TEXT, `pioEnv` TEXT, PRIMARY KEY(`myNodeNum`))", + "fields": [ + { + "fieldPath": "myNodeNum", + "columnName": "myNodeNum", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "model", + "columnName": "model", + "affinity": "TEXT" + }, + { + "fieldPath": "firmwareVersion", + "columnName": "firmwareVersion", + "affinity": "TEXT" + }, + { + "fieldPath": "couldUpdate", + "columnName": "couldUpdate", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "shouldUpdate", + "columnName": "shouldUpdate", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "currentPacketId", + "columnName": "currentPacketId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "messageTimeoutMsec", + "columnName": "messageTimeoutMsec", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "minAppVersion", + "columnName": "minAppVersion", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "maxChannels", + "columnName": "maxChannels", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "hasWifi", + "columnName": "hasWifi", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "deviceId", + "columnName": "deviceId", + "affinity": "TEXT" + }, + { + "fieldPath": "pioEnv", + "columnName": "pioEnv", + "affinity": "TEXT" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "myNodeNum" + ] + } + }, + { + "tableName": "nodes", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`num` INTEGER NOT NULL, `user` BLOB NOT NULL, `long_name` TEXT, `short_name` TEXT, `position` BLOB NOT NULL, `latitude` REAL NOT NULL, `longitude` REAL NOT NULL, `snr` REAL NOT NULL, `rssi` INTEGER NOT NULL, `last_heard` INTEGER NOT NULL, `device_metrics` BLOB NOT NULL, `channel` INTEGER NOT NULL, `via_mqtt` INTEGER NOT NULL, `hops_away` INTEGER NOT NULL, `is_favorite` INTEGER NOT NULL, `is_ignored` INTEGER NOT NULL DEFAULT 0, `is_muted` INTEGER NOT NULL DEFAULT 0, `environment_metrics` BLOB NOT NULL, `power_metrics` BLOB NOT NULL, `air_quality_metrics` BLOB NOT NULL DEFAULT x'', `paxcounter` BLOB NOT NULL, `public_key` BLOB, `notes` TEXT NOT NULL DEFAULT '', `manually_verified` INTEGER NOT NULL DEFAULT 0, `node_status` TEXT, `last_transport` INTEGER NOT NULL DEFAULT 0, PRIMARY KEY(`num`))", + "fields": [ + { + "fieldPath": "num", + "columnName": "num", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "user", + "columnName": "user", + "affinity": "BLOB", + "notNull": true + }, + { + "fieldPath": "longName", + "columnName": "long_name", + "affinity": "TEXT" + }, + { + "fieldPath": "shortName", + "columnName": "short_name", + "affinity": "TEXT" + }, + { + "fieldPath": "position", + "columnName": "position", + "affinity": "BLOB", + "notNull": true + }, + { + "fieldPath": "latitude", + "columnName": "latitude", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "longitude", + "columnName": "longitude", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "snr", + "columnName": "snr", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "rssi", + "columnName": "rssi", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastHeard", + "columnName": "last_heard", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "deviceTelemetry", + "columnName": "device_metrics", + "affinity": "BLOB", + "notNull": true + }, + { + "fieldPath": "channel", + "columnName": "channel", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "viaMqtt", + "columnName": "via_mqtt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "hopsAway", + "columnName": "hops_away", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isFavorite", + "columnName": "is_favorite", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isIgnored", + "columnName": "is_ignored", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "isMuted", + "columnName": "is_muted", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "environmentTelemetry", + "columnName": "environment_metrics", + "affinity": "BLOB", + "notNull": true + }, + { + "fieldPath": "powerTelemetry", + "columnName": "power_metrics", + "affinity": "BLOB", + "notNull": true + }, + { + "fieldPath": "airQualityTelemetry", + "columnName": "air_quality_metrics", + "affinity": "BLOB", + "notNull": true, + "defaultValue": "x''" + }, + { + "fieldPath": "paxcounter", + "columnName": "paxcounter", + "affinity": "BLOB", + "notNull": true + }, + { + "fieldPath": "publicKey", + "columnName": "public_key", + "affinity": "BLOB" + }, + { + "fieldPath": "notes", + "columnName": "notes", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "manuallyVerified", + "columnName": "manually_verified", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "nodeStatus", + "columnName": "node_status", + "affinity": "TEXT" + }, + { + "fieldPath": "lastTransport", + "columnName": "last_transport", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "num" + ] + }, + "indices": [ + { + "name": "index_nodes_last_heard", + "unique": false, + "columnNames": [ + "last_heard" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_nodes_last_heard` ON `${TABLE_NAME}` (`last_heard`)" + }, + { + "name": "index_nodes_short_name", + "unique": false, + "columnNames": [ + "short_name" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_nodes_short_name` ON `${TABLE_NAME}` (`short_name`)" + }, + { + "name": "index_nodes_long_name", + "unique": false, + "columnNames": [ + "long_name" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_nodes_long_name` ON `${TABLE_NAME}` (`long_name`)" + }, + { + "name": "index_nodes_hops_away", + "unique": false, + "columnNames": [ + "hops_away" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_nodes_hops_away` ON `${TABLE_NAME}` (`hops_away`)" + }, + { + "name": "index_nodes_is_favorite", + "unique": false, + "columnNames": [ + "is_favorite" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_nodes_is_favorite` ON `${TABLE_NAME}` (`is_favorite`)" + }, + { + "name": "index_nodes_last_heard_is_favorite", + "unique": false, + "columnNames": [ + "last_heard", + "is_favorite" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_nodes_last_heard_is_favorite` ON `${TABLE_NAME}` (`last_heard`, `is_favorite`)" + }, + { + "name": "index_nodes_public_key", + "unique": false, + "columnNames": [ + "public_key" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_nodes_public_key` ON `${TABLE_NAME}` (`public_key`)" + } + ] + }, + { + "tableName": "packet", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uuid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `myNodeNum` INTEGER NOT NULL DEFAULT 0, `port_num` INTEGER NOT NULL, `contact_key` TEXT NOT NULL, `received_time` INTEGER NOT NULL, `read` INTEGER NOT NULL DEFAULT 1, `data` TEXT NOT NULL, `packet_id` INTEGER NOT NULL DEFAULT 0, `routing_error` INTEGER NOT NULL DEFAULT -1, `snr` REAL NOT NULL DEFAULT 0, `rssi` INTEGER NOT NULL DEFAULT 0, `hopsAway` INTEGER NOT NULL DEFAULT -1, `sfpp_hash` BLOB, `filtered` INTEGER NOT NULL DEFAULT 0, `message_text` TEXT NOT NULL DEFAULT '')", + "fields": [ + { + "fieldPath": "uuid", + "columnName": "uuid", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "myNodeNum", + "columnName": "myNodeNum", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "port_num", + "columnName": "port_num", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "contact_key", + "columnName": "contact_key", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "received_time", + "columnName": "received_time", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "read", + "columnName": "read", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "data", + "columnName": "data", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "packetId", + "columnName": "packet_id", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "routingError", + "columnName": "routing_error", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "-1" + }, + { + "fieldPath": "snr", + "columnName": "snr", + "affinity": "REAL", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "rssi", + "columnName": "rssi", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "hopsAway", + "columnName": "hopsAway", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "-1" + }, + { + "fieldPath": "sfpp_hash", + "columnName": "sfpp_hash", + "affinity": "BLOB" + }, + { + "fieldPath": "filtered", + "columnName": "filtered", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "messageText", + "columnName": "message_text", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "uuid" + ] + }, + "indices": [ + { + "name": "index_packet_myNodeNum", + "unique": false, + "columnNames": [ + "myNodeNum" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_packet_myNodeNum` ON `${TABLE_NAME}` (`myNodeNum`)" + }, + { + "name": "index_packet_port_num", + "unique": false, + "columnNames": [ + "port_num" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_packet_port_num` ON `${TABLE_NAME}` (`port_num`)" + }, + { + "name": "index_packet_contact_key", + "unique": false, + "columnNames": [ + "contact_key" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_packet_contact_key` ON `${TABLE_NAME}` (`contact_key`)" + }, + { + "name": "index_packet_contact_key_port_num_received_time", + "unique": false, + "columnNames": [ + "contact_key", + "port_num", + "received_time" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_packet_contact_key_port_num_received_time` ON `${TABLE_NAME}` (`contact_key`, `port_num`, `received_time`)" + }, + { + "name": "index_packet_packet_id", + "unique": false, + "columnNames": [ + "packet_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_packet_packet_id` ON `${TABLE_NAME}` (`packet_id`)" + }, + { + "name": "index_packet_received_time", + "unique": false, + "columnNames": [ + "received_time" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_packet_received_time` ON `${TABLE_NAME}` (`received_time`)" + }, + { + "name": "index_packet_filtered", + "unique": false, + "columnNames": [ + "filtered" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_packet_filtered` ON `${TABLE_NAME}` (`filtered`)" + }, + { + "name": "index_packet_read", + "unique": false, + "columnNames": [ + "read" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_packet_read` ON `${TABLE_NAME}` (`read`)" + } + ] + }, + { + "tableName": "packet_fts", + "createSql": "CREATE VIRTUAL TABLE IF NOT EXISTS `${TABLE_NAME}` USING FTS5(`message_text`, tokenize=`unicode61`, content=`packet`)", + "fields": [ + { + "fieldPath": "messageText", + "columnName": "message_text", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [] + }, + "ftsVersion": "FTS5", + "ftsOptions": { + "tokenizer": "unicode61", + "tokenizerArgs": [], + "contentTable": "packet", + "languageIdColumnName": "", + "matchInfo": "FTS4", + "notIndexedColumns": [], + "prefixSizes": [], + "preferredOrder": "ASC", + "contentRowId": "", + "columnSize": true, + "detail": "FULL" + }, + "contentSyncTriggers": [ + "CREATE TRIGGER IF NOT EXISTS room_fts_content_sync_packet_fts_BEFORE_UPDATE BEFORE UPDATE ON `packet` BEGIN DELETE FROM `packet_fts` WHERE `rowid`=OLD.`rowid`; END", + "CREATE TRIGGER IF NOT EXISTS room_fts_content_sync_packet_fts_BEFORE_DELETE BEFORE DELETE ON `packet` BEGIN DELETE FROM `packet_fts` WHERE `rowid`=OLD.`rowid`; END", + "CREATE TRIGGER IF NOT EXISTS room_fts_content_sync_packet_fts_AFTER_UPDATE AFTER UPDATE ON `packet` BEGIN INSERT INTO `packet_fts`(`rowid`, `message_text`) VALUES (NEW.`rowid`, NEW.`message_text`); END", + "CREATE TRIGGER IF NOT EXISTS room_fts_content_sync_packet_fts_AFTER_INSERT AFTER INSERT ON `packet` BEGIN INSERT INTO `packet_fts`(`rowid`, `message_text`) VALUES (NEW.`rowid`, NEW.`message_text`); END" + ] + }, + { + "tableName": "contact_settings", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`contact_key` TEXT NOT NULL, `muteUntil` INTEGER NOT NULL, `last_read_message_uuid` INTEGER, `last_read_message_timestamp` INTEGER, `filtering_disabled` INTEGER NOT NULL DEFAULT 0, PRIMARY KEY(`contact_key`))", + "fields": [ + { + "fieldPath": "contact_key", + "columnName": "contact_key", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "muteUntil", + "columnName": "muteUntil", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastReadMessageUuid", + "columnName": "last_read_message_uuid", + "affinity": "INTEGER" + }, + { + "fieldPath": "lastReadMessageTimestamp", + "columnName": "last_read_message_timestamp", + "affinity": "INTEGER" + }, + { + "fieldPath": "filteringDisabled", + "columnName": "filtering_disabled", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "contact_key" + ] + } + }, + { + "tableName": "log", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uuid` TEXT NOT NULL, `type` TEXT NOT NULL, `received_date` INTEGER NOT NULL, `message` TEXT NOT NULL, `from_num` INTEGER NOT NULL DEFAULT 0, `port_num` INTEGER NOT NULL DEFAULT 0, `from_radio` BLOB NOT NULL DEFAULT x'', PRIMARY KEY(`uuid`))", + "fields": [ + { + "fieldPath": "uuid", + "columnName": "uuid", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "message_type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "received_date", + "columnName": "received_date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "raw_message", + "columnName": "message", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "fromNum", + "columnName": "from_num", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "portNum", + "columnName": "port_num", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "fromRadio", + "columnName": "from_radio", + "affinity": "BLOB", + "notNull": true, + "defaultValue": "x''" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "uuid" + ] + }, + "indices": [ + { + "name": "index_log_from_num", + "unique": false, + "columnNames": [ + "from_num" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_log_from_num` ON `${TABLE_NAME}` (`from_num`)" + }, + { + "name": "index_log_port_num", + "unique": false, + "columnNames": [ + "port_num" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_log_port_num` ON `${TABLE_NAME}` (`port_num`)" + } + ] + }, + { + "tableName": "quick_chat", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uuid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `message` TEXT NOT NULL, `mode` TEXT NOT NULL, `position` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "uuid", + "columnName": "uuid", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "message", + "columnName": "message", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "mode", + "columnName": "mode", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "position", + "columnName": "position", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "uuid" + ] + } + }, + { + "tableName": "reactions", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`myNodeNum` INTEGER NOT NULL DEFAULT 0, `reply_id` INTEGER NOT NULL, `user_id` TEXT NOT NULL, `emoji` TEXT NOT NULL, `timestamp` INTEGER NOT NULL, `snr` REAL NOT NULL DEFAULT 0, `rssi` INTEGER NOT NULL DEFAULT 0, `hopsAway` INTEGER NOT NULL DEFAULT -1, `packet_id` INTEGER NOT NULL DEFAULT 0, `status` INTEGER NOT NULL DEFAULT 0, `routing_error` INTEGER NOT NULL DEFAULT 0, `relays` INTEGER NOT NULL DEFAULT 0, `relay_node` INTEGER, `to` TEXT, `channel` INTEGER NOT NULL DEFAULT 0, `sfpp_hash` BLOB, PRIMARY KEY(`myNodeNum`, `reply_id`, `user_id`, `emoji`))", + "fields": [ + { + "fieldPath": "myNodeNum", + "columnName": "myNodeNum", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "replyId", + "columnName": "reply_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "userId", + "columnName": "user_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "emoji", + "columnName": "emoji", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "timestamp", + "columnName": "timestamp", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "snr", + "columnName": "snr", + "affinity": "REAL", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "rssi", + "columnName": "rssi", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "hopsAway", + "columnName": "hopsAway", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "-1" + }, + { + "fieldPath": "packetId", + "columnName": "packet_id", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "status", + "columnName": "status", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "routingError", + "columnName": "routing_error", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "relays", + "columnName": "relays", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "relayNode", + "columnName": "relay_node", + "affinity": "INTEGER" + }, + { + "fieldPath": "to", + "columnName": "to", + "affinity": "TEXT" + }, + { + "fieldPath": "channel", + "columnName": "channel", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "sfpp_hash", + "columnName": "sfpp_hash", + "affinity": "BLOB" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "myNodeNum", + "reply_id", + "user_id", + "emoji" + ] + }, + "indices": [ + { + "name": "index_reactions_reply_id", + "unique": false, + "columnNames": [ + "reply_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_reactions_reply_id` ON `${TABLE_NAME}` (`reply_id`)" + }, + { + "name": "index_reactions_packet_id", + "unique": false, + "columnNames": [ + "packet_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_reactions_packet_id` ON `${TABLE_NAME}` (`packet_id`)" + } + ] + }, + { + "tableName": "metadata", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`num` INTEGER NOT NULL, `proto` BLOB NOT NULL, `timestamp` INTEGER NOT NULL, PRIMARY KEY(`num`))", + "fields": [ + { + "fieldPath": "num", + "columnName": "num", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "proto", + "columnName": "proto", + "affinity": "BLOB", + "notNull": true + }, + { + "fieldPath": "timestamp", + "columnName": "timestamp", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "num" + ] + }, + "indices": [ + { + "name": "index_metadata_num", + "unique": false, + "columnNames": [ + "num" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_metadata_num` ON `${TABLE_NAME}` (`num`)" + } + ] + }, + { + "tableName": "device_hardware", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`actively_supported` INTEGER NOT NULL, `architecture` TEXT NOT NULL, `display_name` TEXT NOT NULL, `has_ink_hud` INTEGER, `has_mui` INTEGER, `hwModel` INTEGER NOT NULL, `hw_model_slug` TEXT NOT NULL, `images` TEXT, `last_updated` INTEGER NOT NULL, `partition_scheme` TEXT, `platformio_target` TEXT NOT NULL, `requires_dfu` INTEGER, `support_level` INTEGER, `tags` TEXT, PRIMARY KEY(`platformio_target`))", + "fields": [ + { + "fieldPath": "activelySupported", + "columnName": "actively_supported", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "architecture", + "columnName": "architecture", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "display_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "hasInkHud", + "columnName": "has_ink_hud", + "affinity": "INTEGER" + }, + { + "fieldPath": "hasMui", + "columnName": "has_mui", + "affinity": "INTEGER" + }, + { + "fieldPath": "hwModel", + "columnName": "hwModel", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "hwModelSlug", + "columnName": "hw_model_slug", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "images", + "columnName": "images", + "affinity": "TEXT" + }, + { + "fieldPath": "lastUpdated", + "columnName": "last_updated", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "partitionScheme", + "columnName": "partition_scheme", + "affinity": "TEXT" + }, + { + "fieldPath": "platformioTarget", + "columnName": "platformio_target", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "requiresDfu", + "columnName": "requires_dfu", + "affinity": "INTEGER" + }, + { + "fieldPath": "supportLevel", + "columnName": "support_level", + "affinity": "INTEGER" + }, + { + "fieldPath": "tags", + "columnName": "tags", + "affinity": "TEXT" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "platformio_target" + ] + } + }, + { + "tableName": "device_link", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`short_code` TEXT NOT NULL, `link_description` TEXT, `is_vendor` INTEGER NOT NULL, `regions` TEXT, `targets` TEXT, PRIMARY KEY(`short_code`))", + "fields": [ + { + "fieldPath": "shortCode", + "columnName": "short_code", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "linkDescription", + "columnName": "link_description", + "affinity": "TEXT" + }, + { + "fieldPath": "isVendor", + "columnName": "is_vendor", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "regions", + "columnName": "regions", + "affinity": "TEXT" + }, + { + "fieldPath": "targets", + "columnName": "targets", + "affinity": "TEXT" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "short_code" + ] + } + }, + { + "tableName": "firmware_release", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `page_url` TEXT NOT NULL, `release_notes` TEXT NOT NULL, `title` TEXT NOT NULL, `zip_url` TEXT NOT NULL, `last_updated` INTEGER NOT NULL, `release_type` TEXT NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pageUrl", + "columnName": "page_url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "releaseNotes", + "columnName": "release_notes", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "zipUrl", + "columnName": "zip_url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastUpdated", + "columnName": "last_updated", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "releaseType", + "columnName": "release_type", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + } + }, + { + "tableName": "traceroute_node_position", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`log_uuid` TEXT NOT NULL, `request_id` INTEGER NOT NULL, `node_num` INTEGER NOT NULL, `position` BLOB NOT NULL, PRIMARY KEY(`log_uuid`, `node_num`), FOREIGN KEY(`log_uuid`) REFERENCES `log`(`uuid`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "logUuid", + "columnName": "log_uuid", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "requestId", + "columnName": "request_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "nodeNum", + "columnName": "node_num", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "position", + "columnName": "position", + "affinity": "BLOB", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "log_uuid", + "node_num" + ] + }, + "indices": [ + { + "name": "index_traceroute_node_position_log_uuid", + "unique": false, + "columnNames": [ + "log_uuid" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_traceroute_node_position_log_uuid` ON `${TABLE_NAME}` (`log_uuid`)" + }, + { + "name": "index_traceroute_node_position_request_id", + "unique": false, + "columnNames": [ + "request_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_traceroute_node_position_request_id` ON `${TABLE_NAME}` (`request_id`)" + } + ], + "foreignKeys": [ + { + "table": "log", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "log_uuid" + ], + "referencedColumns": [ + "uuid" + ] + } + ] + }, + { + "tableName": "discovery_session", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `timestamp` INTEGER NOT NULL, `presets_scanned` TEXT NOT NULL, `home_preset` TEXT NOT NULL, `total_unique_nodes` INTEGER NOT NULL DEFAULT 0, `avg_channel_utilization` REAL NOT NULL DEFAULT 0.0, `total_messages` INTEGER NOT NULL DEFAULT 0, `total_sensor_packets` INTEGER NOT NULL DEFAULT 0, `furthest_node_distance` REAL NOT NULL DEFAULT 0.0, `completion_status` TEXT NOT NULL DEFAULT 'complete', `ai_summary` TEXT, `user_latitude` REAL NOT NULL DEFAULT 0.0, `user_longitude` REAL NOT NULL DEFAULT 0.0, `total_dwell_seconds` INTEGER NOT NULL DEFAULT 0)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "timestamp", + "columnName": "timestamp", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "presetsScanned", + "columnName": "presets_scanned", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "homePreset", + "columnName": "home_preset", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "totalUniqueNodes", + "columnName": "total_unique_nodes", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "avgChannelUtilization", + "columnName": "avg_channel_utilization", + "affinity": "REAL", + "notNull": true, + "defaultValue": "0.0" + }, + { + "fieldPath": "totalMessages", + "columnName": "total_messages", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "totalSensorPackets", + "columnName": "total_sensor_packets", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "furthestNodeDistance", + "columnName": "furthest_node_distance", + "affinity": "REAL", + "notNull": true, + "defaultValue": "0.0" + }, + { + "fieldPath": "completionStatus", + "columnName": "completion_status", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "'complete'" + }, + { + "fieldPath": "aiSummary", + "columnName": "ai_summary", + "affinity": "TEXT" + }, + { + "fieldPath": "userLatitude", + "columnName": "user_latitude", + "affinity": "REAL", + "notNull": true, + "defaultValue": "0.0" + }, + { + "fieldPath": "userLongitude", + "columnName": "user_longitude", + "affinity": "REAL", + "notNull": true, + "defaultValue": "0.0" + }, + { + "fieldPath": "totalDwellSeconds", + "columnName": "total_dwell_seconds", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + } + }, + { + "tableName": "discovery_preset_result", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `session_id` INTEGER NOT NULL, `preset_name` TEXT NOT NULL, `dwell_duration_seconds` INTEGER NOT NULL DEFAULT 0, `unique_nodes` INTEGER NOT NULL DEFAULT 0, `direct_neighbor_count` INTEGER NOT NULL DEFAULT 0, `mesh_neighbor_count` INTEGER NOT NULL DEFAULT 0, `infrastructure_node_count` INTEGER NOT NULL DEFAULT 0, `message_count` INTEGER NOT NULL DEFAULT 0, `sensor_packet_count` INTEGER NOT NULL DEFAULT 0, `avg_channel_utilization` REAL NOT NULL DEFAULT 0.0, `avg_airtime_rate` REAL NOT NULL DEFAULT 0.0, `packet_success_rate` REAL NOT NULL DEFAULT 0.0, `packet_failure_rate` REAL NOT NULL DEFAULT 0.0, `ai_summary` TEXT, `num_packets_tx` INTEGER NOT NULL DEFAULT 0, `num_packets_rx` INTEGER NOT NULL DEFAULT 0, `num_packets_rx_bad` INTEGER NOT NULL DEFAULT 0, `num_rx_dupe` INTEGER NOT NULL DEFAULT 0, `num_tx_relay` INTEGER NOT NULL DEFAULT 0, `num_tx_relay_canceled` INTEGER NOT NULL DEFAULT 0, `num_online_nodes` INTEGER NOT NULL DEFAULT 0, `num_total_nodes` INTEGER NOT NULL DEFAULT 0, `uptime_seconds` INTEGER NOT NULL DEFAULT 0, FOREIGN KEY(`session_id`) REFERENCES `discovery_session`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "sessionId", + "columnName": "session_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "presetName", + "columnName": "preset_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "dwellDurationSeconds", + "columnName": "dwell_duration_seconds", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "uniqueNodes", + "columnName": "unique_nodes", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "directNeighborCount", + "columnName": "direct_neighbor_count", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "meshNeighborCount", + "columnName": "mesh_neighbor_count", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "infrastructureNodeCount", + "columnName": "infrastructure_node_count", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "messageCount", + "columnName": "message_count", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "sensorPacketCount", + "columnName": "sensor_packet_count", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "avgChannelUtilization", + "columnName": "avg_channel_utilization", + "affinity": "REAL", + "notNull": true, + "defaultValue": "0.0" + }, + { + "fieldPath": "avgAirtimeRate", + "columnName": "avg_airtime_rate", + "affinity": "REAL", + "notNull": true, + "defaultValue": "0.0" + }, + { + "fieldPath": "packetSuccessRate", + "columnName": "packet_success_rate", + "affinity": "REAL", + "notNull": true, + "defaultValue": "0.0" + }, + { + "fieldPath": "packetFailureRate", + "columnName": "packet_failure_rate", + "affinity": "REAL", + "notNull": true, + "defaultValue": "0.0" + }, + { + "fieldPath": "aiSummary", + "columnName": "ai_summary", + "affinity": "TEXT" + }, + { + "fieldPath": "numPacketsTx", + "columnName": "num_packets_tx", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "numPacketsRx", + "columnName": "num_packets_rx", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "numPacketsRxBad", + "columnName": "num_packets_rx_bad", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "numRxDupe", + "columnName": "num_rx_dupe", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "numTxRelay", + "columnName": "num_tx_relay", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "numTxRelayCanceled", + "columnName": "num_tx_relay_canceled", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "numOnlineNodes", + "columnName": "num_online_nodes", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "numTotalNodes", + "columnName": "num_total_nodes", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "uptimeSeconds", + "columnName": "uptime_seconds", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_discovery_preset_result_session_id", + "unique": false, + "columnNames": [ + "session_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_discovery_preset_result_session_id` ON `${TABLE_NAME}` (`session_id`)" + } + ], + "foreignKeys": [ + { + "table": "discovery_session", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "session_id" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "discovered_node", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `preset_result_id` INTEGER NOT NULL, `node_num` INTEGER NOT NULL, `short_name` TEXT, `long_name` TEXT, `neighbor_type` TEXT NOT NULL DEFAULT 'direct', `latitude` REAL, `longitude` REAL, `distance_from_user` REAL, `hop_count` INTEGER NOT NULL DEFAULT 0, `snr` REAL NOT NULL DEFAULT 0, `rssi` INTEGER NOT NULL DEFAULT 0, `message_count` INTEGER NOT NULL DEFAULT 0, `sensor_packet_count` INTEGER NOT NULL DEFAULT 0, `is_infrastructure` INTEGER NOT NULL DEFAULT 0, FOREIGN KEY(`preset_result_id`) REFERENCES `discovery_preset_result`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "presetResultId", + "columnName": "preset_result_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "nodeNum", + "columnName": "node_num", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "shortName", + "columnName": "short_name", + "affinity": "TEXT" + }, + { + "fieldPath": "longName", + "columnName": "long_name", + "affinity": "TEXT" + }, + { + "fieldPath": "neighborType", + "columnName": "neighbor_type", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "'direct'" + }, + { + "fieldPath": "latitude", + "columnName": "latitude", + "affinity": "REAL" + }, + { + "fieldPath": "longitude", + "columnName": "longitude", + "affinity": "REAL" + }, + { + "fieldPath": "distanceFromUser", + "columnName": "distance_from_user", + "affinity": "REAL" + }, + { + "fieldPath": "hopCount", + "columnName": "hop_count", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "snr", + "columnName": "snr", + "affinity": "REAL", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "rssi", + "columnName": "rssi", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "messageCount", + "columnName": "message_count", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "sensorPacketCount", + "columnName": "sensor_packet_count", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "isInfrastructure", + "columnName": "is_infrastructure", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_discovered_node_preset_result_id", + "unique": false, + "columnNames": [ + "preset_result_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_discovered_node_preset_result_id` ON `${TABLE_NAME}` (`preset_result_id`)" + }, + { + "name": "index_discovered_node_node_num", + "unique": false, + "columnNames": [ + "node_num" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_discovered_node_node_num` ON `${TABLE_NAME}` (`node_num`)" + } + ], + "foreignKeys": [ + { + "table": "discovery_preset_result", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "preset_result_id" + ], + "referencedColumns": [ + "id" + ] + } + ] + } + ], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'c58b8f2f228a2a98ef9faf320b388373')" + ] + } +} \ No newline at end of file diff --git a/core/database/src/commonMain/kotlin/org/meshtastic/core/database/MeshtasticDatabase.kt b/core/database/src/commonMain/kotlin/org/meshtastic/core/database/MeshtasticDatabase.kt index 8b8d470ce..bea88198d 100644 --- a/core/database/src/commonMain/kotlin/org/meshtastic/core/database/MeshtasticDatabase.kt +++ b/core/database/src/commonMain/kotlin/org/meshtastic/core/database/MeshtasticDatabase.kt @@ -111,8 +111,9 @@ import org.meshtastic.core.database.entity.TracerouteNodePositionEntity AutoMigration(from = 39, to = 40), AutoMigration(from = 40, to = 41), AutoMigration(from = 41, to = 42), + AutoMigration(from = 42, to = 43, spec = AutoMigration42to43::class), ], - version = 42, + version = 43, exportSchema = true, ) @androidx.room3.ConstructedBy(MeshtasticDatabaseConstructor::class) @@ -160,3 +161,7 @@ class AutoMigration33to34 : AutoMigrationSpec @DeleteColumn(tableName = "packet", columnName = "retry_count") @DeleteColumn(tableName = "reactions", columnName = "retry_count") class AutoMigration34to35 : AutoMigrationSpec + +/** Device links moved from the bundled `urls.json` to the resolved API; `original_url` is no longer stored. */ +@DeleteColumn(tableName = "device_link", columnName = "original_url") +class AutoMigration42to43 : AutoMigrationSpec diff --git a/core/database/src/commonMain/kotlin/org/meshtastic/core/database/entity/DeviceLinkEntity.kt b/core/database/src/commonMain/kotlin/org/meshtastic/core/database/entity/DeviceLinkEntity.kt index 91a9dde6b..a5d56e2ff 100644 --- a/core/database/src/commonMain/kotlin/org/meshtastic/core/database/entity/DeviceLinkEntity.kt +++ b/core/database/src/commonMain/kotlin/org/meshtastic/core/database/entity/DeviceLinkEntity.kt @@ -22,29 +22,29 @@ import androidx.room3.PrimaryKey import kotlinx.serialization.Serializable import org.meshtastic.core.model.DeviceLink -/** A msh.to short-link, upserted from the bundled `urls.json` during the device-hardware refresh cycle. */ +/** A msh.to device link, cached from the Meshtastic API (`/resource/deviceLinks`) during the refresh cycle. */ @Serializable @Entity(tableName = "device_link") data class DeviceLinkEntity( @PrimaryKey @ColumnInfo(name = "short_code") val shortCode: String, - @ColumnInfo(name = "original_url") val originalUrl: String, @ColumnInfo(name = "link_description") val linkDescription: String? = null, @ColumnInfo(name = "is_vendor") val isVendor: Boolean = false, val regions: List? = null, + val targets: List? = null, ) fun DeviceLink.asEntity() = DeviceLinkEntity( shortCode = shortCode, - originalUrl = originalUrl, linkDescription = description, isVendor = isVendor, regions = regions, + targets = targets, ) fun DeviceLinkEntity.asExternalModel() = DeviceLink( shortCode = shortCode, - originalUrl = originalUrl, description = linkDescription, isVendor = isVendor, regions = regions, + targets = targets, ) diff --git a/core/model/src/commonMain/kotlin/org/meshtastic/core/model/DeviceLink.kt b/core/model/src/commonMain/kotlin/org/meshtastic/core/model/DeviceLink.kt index 20b9197a3..696ff4c62 100644 --- a/core/model/src/commonMain/kotlin/org/meshtastic/core/model/DeviceLink.kt +++ b/core/model/src/commonMain/kotlin/org/meshtastic/core/model/DeviceLink.kt @@ -19,23 +19,23 @@ package org.meshtastic.core.model import kotlinx.serialization.Serializable /** - * A msh.to short-link associated with a piece of hardware. Imported from the bundled `urls.json` (sourced from the - * meshtastic/msh.to repo). Every link resolves through the msh.to redirect service. + * A msh.to device link resolved by the Meshtastic API (`/resource/deviceLinks`) and cached locally. Every link routes + * through the msh.to redirect service. * * @param shortCode the msh.to short code, e.g. `rak_wismeshtag`, `rokland-heltec-v3`. - * @param originalUrl the destination URL recorded in `urls.json` (informational; the app links to msh.to). * @param description human-readable label shown to the user. - * @param isVendor true when [shortCode] is itself a known device `platformioTarget` (the primary vendor link). - * @param regions marketplace shipping regions (ISO 3166-1 alpha-2). `null` = vendor/variant (not region-filtered); - * empty = worldwide marketplace; non-empty = limited to the listed countries. + * @param isVendor true for a first-party vendor link (shown more prominently than region-filtered marketplace links). + * @param regions marketplace shipping regions (ISO 3166-1 alpha-2). `null` = not region-filtered (vendor/worldwide); + * non-empty = limited to the listed countries. + * @param targets device `platformioTarget`s this link is attached to; used to match a link to the device on screen. */ @Serializable data class DeviceLink( val shortCode: String, - val originalUrl: String, val description: String? = null, val isVendor: Boolean = false, val regions: List? = null, + val targets: List? = null, ) { /** The user-facing link, routed through the msh.to redirect service. */ val url: String diff --git a/core/model/src/commonMain/kotlin/org/meshtastic/core/model/MshToLinks.kt b/core/model/src/commonMain/kotlin/org/meshtastic/core/model/MshToLinks.kt deleted file mode 100644 index 241a88d4b..000000000 --- a/core/model/src/commonMain/kotlin/org/meshtastic/core/model/MshToLinks.kt +++ /dev/null @@ -1,41 +0,0 @@ -/* - * Copyright (c) 2026 Meshtastic LLC - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ -package org.meshtastic.core.model - -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable - -/** Root of the bundled `urls.json` file (imported as-is from the meshtastic/msh.to repo). */ -@Serializable data class MshToUrlsFile(@SerialName("Routes") val routes: List = emptyList()) - -/** A single short-code route in `urls.json`. */ -@Serializable -data class MshToRoute( - @SerialName("ShortCode") val shortCode: String, - @SerialName("OriginalUrl") val originalUrl: String, - @SerialName("Description") val description: String? = null, -) - -/** - * Marketplace metadata from the app-maintained `marketplaces.json`. Keyed by marketplace identifier (e.g. `rokland`, - * `aliexpress`). - * - * @param regions ISO 3166-1 alpha-2 shipping regions; empty = worldwide. - * @param match how the marketplace identifier appears in a short code: `"prefix"` (e.g. `rokland-heltec-v3`) or - * `"suffix"` (e.g. `heltec-v3_aliexpress`). - */ -@Serializable data class MshToMarketplace(val regions: List = emptyList(), val match: String) diff --git a/core/model/src/commonMain/kotlin/org/meshtastic/core/model/NetworkDeviceLink.kt b/core/model/src/commonMain/kotlin/org/meshtastic/core/model/NetworkDeviceLink.kt new file mode 100644 index 000000000..e22bb2d3d --- /dev/null +++ b/core/model/src/commonMain/kotlin/org/meshtastic/core/model/NetworkDeviceLink.kt @@ -0,0 +1,80 @@ +/* + * Copyright (c) 2026 Meshtastic LLC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.meshtastic.core.model + +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.Serializable +import kotlinx.serialization.json.JsonIgnoreUnknownKeys + +/** + * Response envelope of `GET /resource/deviceLinks` on the Meshtastic API. The server resolves meshtastic/msh.to's + * catalog into fully-classified links (type + targets + regions), so the client only stores and filters them — no + * client-side matching heuristic. + */ +@Serializable +@OptIn(ExperimentalSerializationApi::class) +@JsonIgnoreUnknownKeys +data class NetworkDeviceLinksResponse( + val version: Int = 1, + val generatedAt: String? = null, + val source: String? = null, + val links: List = emptyList(), +) + +/** + * A single resolved device link from the Meshtastic API. + * + * @param shortCode msh.to short code, e.g. `rokland-t-deck-plus`. + * @param url the user-facing `https://msh.to/` link (the retailer destination is intentionally not exposed). + * @param description human-readable label. + * @param type authoritative classification: [TYPE_INTERNAL], [TYPE_VENDOR], or [TYPE_MARKETPLACE]. + * @param targets device `platformioTarget`s this link is attached to; `null` = untriaged, empty = device-agnostic. + * @param hwModels `hwModel` ints derived from [targets] server-side (parallel list). + * @param marketplace retailer key (e.g. `rokland`) for marketplace links, else `null`. + * @param regions ISO 3166-1 alpha-2 shipping regions; `null` = worldwide (no region filter). + */ +@Serializable +@OptIn(ExperimentalSerializationApi::class) +@JsonIgnoreUnknownKeys +data class NetworkDeviceLink( + val shortCode: String = "", + val url: String = "", + val description: String? = null, + val type: String = TYPE_INTERNAL, + val targets: List? = null, + val hwModels: List? = null, + val marketplace: String? = null, + val regions: List? = null, +) { + companion object { + const val TYPE_INTERNAL = "internal" + const val TYPE_VENDOR = "vendor" + const val TYPE_MARKETPLACE = "marketplace" + } +} + +/** + * Pure mapping to the cached domain model. Callers are expected to drop [TYPE_INTERNAL] links (GitHub, YouTube, …), + * which never belong to a device's purchase section. + */ +fun NetworkDeviceLink.toDeviceLink(): DeviceLink = DeviceLink( + shortCode = shortCode, + description = description, + isVendor = type == NetworkDeviceLink.TYPE_VENDOR, + regions = regions, + targets = targets, +) diff --git a/core/network/src/commonMain/kotlin/org/meshtastic/core/network/DeviceLinksRemoteDataSource.kt b/core/network/src/commonMain/kotlin/org/meshtastic/core/network/DeviceLinksRemoteDataSource.kt new file mode 100644 index 000000000..dab46d2f9 --- /dev/null +++ b/core/network/src/commonMain/kotlin/org/meshtastic/core/network/DeviceLinksRemoteDataSource.kt @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2026 Meshtastic LLC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.meshtastic.core.network + +import kotlinx.coroutines.withContext +import org.koin.core.annotation.Single +import org.meshtastic.core.di.CoroutineDispatchers +import org.meshtastic.core.model.NetworkDeviceLink +import org.meshtastic.core.network.service.ApiService + +@Single +class DeviceLinksRemoteDataSource(private val apiService: ApiService, private val dispatchers: CoroutineDispatchers) { + suspend fun getDeviceLinks(): List = + withContext(dispatchers.io) { apiService.getDeviceLinks().links } +} diff --git a/core/network/src/commonMain/kotlin/org/meshtastic/core/network/service/ApiService.kt b/core/network/src/commonMain/kotlin/org/meshtastic/core/network/service/ApiService.kt index ec51e5602..94c7697a9 100644 --- a/core/network/src/commonMain/kotlin/org/meshtastic/core/network/service/ApiService.kt +++ b/core/network/src/commonMain/kotlin/org/meshtastic/core/network/service/ApiService.kt @@ -21,6 +21,7 @@ import io.ktor.client.call.body import io.ktor.client.request.get import org.koin.core.annotation.Single import org.meshtastic.core.model.NetworkDeviceHardware +import org.meshtastic.core.model.NetworkDeviceLinksResponse import org.meshtastic.core.model.NetworkFirmwareReleases /** Client for the Meshtastic public API (device hardware catalog and firmware releases). */ @@ -28,6 +29,9 @@ interface ApiService { /** Fetches the device hardware catalog from the Meshtastic API. */ suspend fun getDeviceHardware(): List + /** Fetches the resolved device-links catalog (msh.to purchase links) from the Meshtastic API. */ + suspend fun getDeviceLinks(): NetworkDeviceLinksResponse + /** Fetches the list of available firmware releases from the Meshtastic API. */ suspend fun getFirmwareReleases(): NetworkFirmwareReleases } @@ -44,5 +48,7 @@ interface ApiService { class ApiServiceImpl(private val client: HttpClient) : ApiService { override suspend fun getDeviceHardware(): List = client.get("resource/deviceHardware").body() + override suspend fun getDeviceLinks(): NetworkDeviceLinksResponse = client.get("resource/deviceLinks").body() + override suspend fun getFirmwareReleases(): NetworkFirmwareReleases = client.get("github/firmware/list").body() } diff --git a/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/DeviceLinkRepository.kt b/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/DeviceLinkRepository.kt index 7bfe3a221..c3f9cace9 100644 --- a/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/DeviceLinkRepository.kt +++ b/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/DeviceLinkRepository.kt @@ -21,23 +21,22 @@ import org.meshtastic.core.common.util.currentRegionCode import org.meshtastic.core.model.DeviceLink /** - * Provides msh.to device links imported from the bundled `urls.json`. Mirrors the Meshtastic-Apple device-links - * feature: vendor, product-variant, and region-filtered marketplace links shown on the device hardware detail view, - * plus a full directory. + * Provides msh.to device links resolved by the Meshtastic API (`/resource/deviceLinks`) and cached locally. Vendor and + * region-filtered marketplace links are shown on the device hardware detail view, plus a full directory. */ interface DeviceLinkRepository { - /** Seeds the link table from the bundled JSON if it is empty (covers fresh install, data clear, radio switch). */ + /** Seeds the link table from the bundled snapshot if it is empty (fresh install, data clear, radio switch). */ suspend fun ensureImported() - /** Re-imports the bundled JSON: upserts all links, recomputes `isVendor`, and prunes orphaned short codes. */ + /** Refreshes links from the API: upserts the resolved catalog and prunes short codes that no longer exist. */ suspend fun reconcile() /** - * Links for a device's [platformioTarget], region-filtered and sorted with vendor/variant links first. Returns an + * Links attached to a device's [platformioTarget], region-filtered and sorted with vendor links first. Returns an * empty list when no links match. */ suspend fun getLinksForTarget(platformioTarget: String, regionCode: String = currentRegionCode()): List - /** All imported links, sorted by short code — backs the Settings "Device Links" directory. */ + /** All cached links, sorted by short code — backs the Settings "Device Links" directory. */ fun observeAllLinks(): Flow> } diff --git a/desktopApp/src/main/kotlin/org/meshtastic/desktop/di/DesktopKoinModule.kt b/desktopApp/src/main/kotlin/org/meshtastic/desktop/di/DesktopKoinModule.kt index 9c69c9867..c47a1d4b0 100644 --- a/desktopApp/src/main/kotlin/org/meshtastic/desktop/di/DesktopKoinModule.kt +++ b/desktopApp/src/main/kotlin/org/meshtastic/desktop/di/DesktopKoinModule.kt @@ -36,12 +36,11 @@ import org.koin.core.qualifier.named import org.koin.dsl.module import org.meshtastic.core.data.datasource.BootloaderOtaQuirksJsonDataSource import org.meshtastic.core.data.datasource.DeviceHardwareJsonDataSource +import org.meshtastic.core.data.datasource.DeviceLinksJsonDataSource import org.meshtastic.core.data.datasource.FirmwareReleaseJsonDataSource -import org.meshtastic.core.data.datasource.MshToLinksJsonDataSource import org.meshtastic.core.model.BootloaderOtaQuirk -import org.meshtastic.core.model.MshToMarketplace -import org.meshtastic.core.model.MshToRoute import org.meshtastic.core.model.NetworkDeviceHardware +import org.meshtastic.core.model.NetworkDeviceLink import org.meshtastic.core.model.NetworkFirmwareReleases import org.meshtastic.core.network.HttpClientDefaults import org.meshtastic.core.network.KermitHttpLogger @@ -273,11 +272,9 @@ private fun desktopPlatformStubsModule() = module { } } - single { - object : MshToLinksJsonDataSource { - override fun loadRoutes(): List = emptyList() - - override fun loadMarketplaces(): Map = emptyMap() + single { + object : DeviceLinksJsonDataSource { + override fun loadDeviceLinksFromJsonAsset(): List = emptyList() } } } diff --git a/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/component/NodeDetailComponentPreviews.kt b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/component/NodeDetailComponentPreviews.kt index 70b79be5b..b090a2599 100644 --- a/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/component/NodeDetailComponentPreviews.kt +++ b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/component/NodeDetailComponentPreviews.kt @@ -191,21 +191,21 @@ private fun DeviceLinksSectionPreview() { listOf( org.meshtastic.core.model.DeviceLink( shortCode = "heltec-v3", - originalUrl = "https://heltec.org", description = "Heltec V3", isVendor = true, + targets = listOf("heltec-v3"), ), org.meshtastic.core.model.DeviceLink( shortCode = "rokland-heltec-v3", - originalUrl = "https://rokland.com", description = "Rokland", regions = listOf("US"), + targets = listOf("heltec-v3"), ), org.meshtastic.core.model.DeviceLink( shortCode = "heltec-v3_aliexpress", - originalUrl = "https://aliexpress.com", description = "AliExpress", regions = emptyList(), + targets = listOf("heltec-v3"), ), ) AppTheme { Surface { DeviceLinksSection(links = links) } }