diff --git a/.github/workflows/sync-device-links.yml b/.github/workflows/sync-device-links.yml new file mode 100644 index 0000000..ce8777d --- /dev/null +++ b/.github/workflows/sync-device-links.yml @@ -0,0 +1,45 @@ +name: Sync device links + +# Pulls the device-link catalog from msh.to's public /api/urls into data/deviceLinks.json, +# which the api serves from /resource/deviceLinks. Keeps the data in-repo (reviewable diffs, +# no runtime dependency on msh.to) and fresh without a code change. + +on: + schedule: + - cron: "17 * * * *" # hourly (at :17 to avoid top-of-hour congestion) + workflow_dispatch: + +concurrency: + group: sync-device-links + cancel-in-progress: false + +permissions: + contents: write + +jobs: + sync: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Fetch catalog from msh.to + run: | + set -euo pipefail + curl -fsSL https://msh.to/api/urls -o /tmp/catalog.json + # Sanity-check the shape before writing, so a bad/empty response never lands. + jq -e 'has("Routes") and (.Routes | type == "array") and (.Routes | length > 0)' /tmp/catalog.json >/dev/null + mkdir -p data + jq '.' /tmp/catalog.json > data/deviceLinks.json # pretty-print for clean diffs + + - name: Commit if changed + run: | + set -euo pipefail + if git diff --quiet -- data/deviceLinks.json; then + echo "Catalog unchanged — nothing to commit." + exit 0 + fi + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git add data/deviceLinks.json + git commit -m "chore: sync device links from msh.to [skip ci]" + git push diff --git a/biome.json b/biome.json index 6567683..ca75d27 100644 --- a/biome.json +++ b/biome.json @@ -2,7 +2,8 @@ "$schema": "https://biomejs.dev/schemas/2.1.3/schema.json", "assist": { "actions": { "source": { "organizeImports": "on" } } }, "files": { - "ignoreUnknown": true + "ignoreUnknown": true, + "includes": ["**", "!data/**"] }, "vcs": { "enabled": true, diff --git a/data/deviceLinks.json b/data/deviceLinks.json new file mode 100644 index 0000000..fdb24de --- /dev/null +++ b/data/deviceLinks.json @@ -0,0 +1,1513 @@ +{ + "Routes": [ + { + "ShortCode": "github", + "Description": "Meshtastic GitHub Organization", + "Type": "Internal" + }, + { + "ShortCode": "youtube", + "Description": "Meshtastic YouTube Channel", + "Type": "Internal" + }, + { + "ShortCode": "reddit", + "Description": "Meshtastic Reddit Community", + "Type": "Internal" + }, + { + "ShortCode": "docs", + "Description": "Meshtastic Documentation", + "Type": "Internal" + }, + { + "ShortCode": "discord", + "Description": "Meshtastic Discord Server", + "Type": "Internal" + }, + { + "ShortCode": "web", + "Description": "Meshtastic Web Client", + "Type": "Internal" + }, + { + "ShortCode": "flash", + "Description": "Meshtastic Web Flasher", + "Type": "Internal" + }, + { + "ShortCode": "firmware", + "Description": "Meshtastic Firmware Repository", + "Type": "Internal" + }, + { + "ShortCode": "android", + "Description": "Meshtastic Android App", + "Type": "Internal" + }, + { + "ShortCode": "ios", + "Description": "Meshtastic iOS App", + "Type": "Internal" + }, + { + "ShortCode": "rak-collection", + "Description": "RAKwireless Meshtastic Collection", + "Type": "Vendor", + "Targets": [] + }, + { + "ShortCode": "rak4631", + "Description": "WisMesh RAK4631 Starter Kit", + "Type": "Vendor", + "Targets": [ + "rak4631" + ] + }, + { + "ShortCode": "rak3312", + "Description": "WisMesh ESP32-S3 Starter Kit", + "Type": "Vendor", + "Targets": [ + "rak3312" + ] + }, + { + "ShortCode": "rak3401-1watt", + "Description": "WisMesh RAK3401 1W Starter Kit", + "Type": "Vendor", + "Targets": [ + "rak3401-1watt" + ] + }, + { + "ShortCode": "rak_wismeshtap", + "Description": "RAK WisMesh Tap", + "Type": "Vendor", + "Targets": [ + "rak_wismeshtap" + ] + }, + { + "ShortCode": "rak_wismeshtag", + "Description": "RAK WisMesh Tag", + "Type": "Vendor", + "Targets": [ + "rak_wismeshtag" + ] + }, + { + "ShortCode": "rokland-wismesh-tag", + "Description": "Rokland WisMesh Tag", + "Type": "Marketplace", + "Targets": [ + "rak_wismeshtag" + ] + }, + { + "ShortCode": "hexaspot-wismesh-tag", + "Description": "Hexaspot WisMesh Tag", + "Type": "Marketplace", + "Targets": [ + "rak_wismeshtag" + ] + }, + { + "ShortCode": "aliexpress-wismesh-tag", + "Description": "Aliexpress RAK WisMesh Tag", + "Type": "Marketplace", + "Targets": [ + "rak_wismeshtag" + ] + }, + { + "ShortCode": "rak19007", + "Description": "RAKwireless RAK19007 WisBlock Base Board 2nd Gen", + "Type": "Vendor", + "Targets": [] + }, + { + "ShortCode": "tbeam-s3-core", + "Description": "T-Beam Supreme", + "Type": "Vendor", + "Targets": [ + "tbeam-s3-core" + ] + }, + { + "ShortCode": "t-echo", + "Description": "T-Echo", + "Type": "Vendor", + "Targets": [ + "t-echo" + ] + }, + { + "ShortCode": "t-watch-s3", + "Description": "T-Watch S3", + "Type": "Vendor", + "Targets": [ + "t-watch-s3" + ] + }, + { + "ShortCode": "t-deck", + "Description": "T-Deck", + "Type": "Vendor", + "Targets": [ + "t-deck" + ] + }, + { + "ShortCode": "tlora-t3s3-v1", + "Description": "T3S3", + "Type": "Vendor", + "Targets": [ + "tlora-t3s3-v1" + ] + }, + { + "ShortCode": "heltec-mesh-node-t114", + "Description": "Mesh Node T114", + "Type": "Vendor", + "Targets": [ + "heltec-mesh-node-t114" + ] + }, + { + "ShortCode": "heltec-vision-master-e213", + "Description": "Vision Master E213", + "Type": "Vendor", + "Targets": [ + "heltec-vision-master-e213" + ] + }, + { + "ShortCode": "heltec-vision-master-e290", + "Description": "Vision Master E290", + "Type": "Vendor", + "Targets": [ + "heltec-vision-master-e290" + ] + }, + { + "ShortCode": "heltec-vision-master-t190", + "Description": "Vision Master T190", + "Type": "Vendor", + "Targets": [ + "heltec-vision-master-t190" + ] + }, + { + "ShortCode": "heltec-wireless-tracker", + "Description": "Wireless Tracker", + "Type": "Vendor", + "Targets": [ + "heltec-wireless-tracker" + ] + }, + { + "ShortCode": "heltec-wireless-tracker-v2", + "Description": "Wireless Tracker V2", + "Type": "Vendor", + "Targets": [ + "heltec-wireless-tracker-v2" + ] + }, + { + "ShortCode": "heltec-wireless-paper", + "Description": "Wireless Paper", + "Type": "Vendor", + "Targets": [ + "heltec-wireless-paper" + ] + }, + { + "ShortCode": "heltec-ht62-esp32c3-sx1262", + "Description": "HT-CT62", + "Type": "Vendor", + "Targets": [ + "heltec-ht62-esp32c3-sx1262" + ] + }, + { + "ShortCode": "wio-tracker-wm1110", + "Description": "Wio Tracker WM1110 Dev Kit", + "Type": "Vendor", + "Targets": [ + "wio-tracker-wm1110" + ] + }, + { + "ShortCode": "tracker-t1000-e", + "Description": "SenseCAP Card Tracker T1000-E", + "Type": "Vendor", + "Targets": [ + "tracker-t1000-e" + ] + }, + { + "ShortCode": "tracker-t1000-e-aliexpress", + "Description": "SenseCAP Card Tracker T1000-E Aliexpress", + "Type": "Marketplace", + "Targets": [ + "tracker-t1000-e" + ] + }, + { + "ShortCode": "tracker-t1000-e-amazon", + "Description": "SenseCAP Card Tracker T1000-E Amazon", + "Type": "Marketplace", + "Targets": [ + "tracker-t1000-e" + ] + }, + { + "ShortCode": "seeed-sensecap-indicator", + "Description": "SenseCAP Indicator", + "Type": "Vendor", + "Targets": [ + "seeed-sensecap-indicator" + ] + }, + { + "ShortCode": "station-g2", + "Description": "Station G2", + "Type": "Vendor", + "Targets": [ + "station-g2" + ] + }, + { + "ShortCode": "rak2560", + "Description": "WisMesh Repeater", + "Type": "Vendor", + "Targets": [ + "rak2560" + ] + }, + { + "ShortCode": "heltec-v3", + "Description": "LoRa32 V3", + "Type": "Vendor", + "Targets": [ + "heltec-v3" + ] + }, + { + "ShortCode": "heltec-wsl-v3", + "Description": "WSL V3", + "Type": "Vendor", + "Targets": [ + "heltec-wsl-v3" + ] + }, + { + "ShortCode": "heltec-v4", + "Description": "LoRa32 V4", + "Type": "Vendor", + "Targets": [ + "heltec-v4" + ] + }, + { + "ShortCode": "seeed-xiao-s3", + "Description": "XIAO ESP32-S3 + Wio-SX1262 Kit", + "Type": "Vendor", + "Targets": [ + "seeed-xiao-s3" + ] + }, + { + "ShortCode": "tlora-t3s3-epaper", + "Description": "T3S3", + "Type": "Vendor", + "Targets": [ + "tlora-t3s3-epaper" + ] + }, + { + "ShortCode": "ht-ct62", + "Description": "HT-CT62", + "Type": "Vendor", + "Targets": [ + "heltec-ht62-esp32c3-sx1262" + ] + }, + { + "ShortCode": "seeed_xiao_nrf52840_kit", + "Description": "XIAO nRF52840 & Wio-SX1262 Kit", + "Type": "Vendor", + "Targets": [ + "seeed_xiao_nrf52840_kit" + ] + }, + { + "ShortCode": "seeed_xiao_nrf52840_kit_aliexpress", + "Description": "XIAO nRF52840 & Wio-SX1262 Kit Aliexpress", + "Type": "Marketplace", + "Targets": [ + "seeed_xiao_nrf52840_kit" + ] + }, + { + "ShortCode": "thinknode_m1", + "Description": "ThinkNode M1", + "Type": "Vendor", + "Targets": [ + "thinknode_m1" + ] + }, + { + "ShortCode": "thinknode_m2", + "Description": "ThinkNode M2", + "Type": "Vendor", + "Targets": [ + "thinknode_m2" + ] + }, + { + "ShortCode": "thinknode_m3", + "Description": "ThinkNode M3", + "Type": "Vendor", + "Targets": [ + "thinknode_m3" + ] + }, + { + "ShortCode": "thinknode_m5", + "Description": "ThinkNode M5", + "Type": "Vendor", + "Targets": [ + "thinknode_m5" + ] + }, + { + "ShortCode": "thinknode_m4", + "Description": "ThinkNode M4", + "Type": "Vendor", + "Targets": [ + "thinknode_m4" + ] + }, + { + "ShortCode": "thinknode_m6", + "Description": "ThinkNode M6", + "Type": "Vendor", + "Targets": [ + "thinknode_m6" + ] + }, + { + "ShortCode": "heltec-mesh-pocket-10000", + "Description": "MeshPocket", + "Type": "Vendor", + "Targets": [ + "heltec-mesh-pocket-10000" + ] + }, + { + "ShortCode": "seeed_solar_node", + "Description": "SenseCAP Solar Node P1 Pro", + "Type": "Vendor", + "Targets": [ + "seeed_solar_node" + ] + }, + { + "ShortCode": "seeed_solar_node_aliexpress", + "Description": "SenseCAP Solar Node P1 Pro Aliexpress", + "Type": "Marketplace", + "Targets": [ + "seeed_solar_node" + ] + }, + { + "ShortCode": "seeed_solar_node_amazon", + "Description": "SenseCAP Solar Node P1 Pro Amazon", + "Type": "Marketplace", + "Targets": [ + "seeed_solar_node" + ] + }, + { + "ShortCode": "elecrow-adv-35-tft", + "Description": "CrowPanel 3.5", + "Type": "Vendor", + "Targets": [ + "elecrow-adv-35-tft" + ] + }, + { + "ShortCode": "elecrow-adv1-43-50-70-tft", + "Description": "CrowPanel 4.3", + "Type": "Vendor", + "Targets": [ + "elecrow-adv1-43-50-70-tft" + ] + }, + { + "ShortCode": "elecrow-adv-24-28-tft", + "Description": "CrowPanel 2.4", + "Type": "Vendor", + "Targets": [ + "elecrow-adv-24-28-tft" + ] + }, + { + "ShortCode": "elecrow-adv-28-tft", + "Description": "CrowPanel 2.8", + "Type": "Vendor", + "Targets": [ + "elecrow-adv-24-28-tft" + ] + }, + { + "ShortCode": "elecrow-adv1-50-tft", + "Description": "CrowPanel 5.0", + "Type": "Vendor", + "Targets": [ + "elecrow-adv1-43-50-70-tft" + ] + }, + { + "ShortCode": "elecrow-adv1-70-tft", + "Description": "CrowPanel 7.0", + "Type": "Vendor", + "Targets": [ + "elecrow-adv1-43-50-70-tft" + ] + }, + { + "ShortCode": "seeed_wio_tracker_L1", + "Description": "Wio Tracker L1", + "Type": "Vendor", + "Targets": [ + "seeed_wio_tracker_L1" + ] + }, + { + "ShortCode": "seeed_wio_tracker_L1_aliexpress", + "Description": "Wio Tracker L1 Aliexpress", + "Type": "Marketplace", + "Targets": [ + "seeed_wio_tracker_L1" + ] + }, + { + "ShortCode": "seeed_wio_tracker_L1_amazon", + "Description": "Wio Tracker L1 Amazon", + "Type": "Marketplace", + "Targets": [ + "seeed_wio_tracker_L1" + ] + }, + { + "ShortCode": "nano-g2-ultra", + "Description": "Nano G2 Ultra", + "Type": "Vendor", + "Targets": [ + "nano-g2-ultra" + ] + }, + { + "ShortCode": "rak11310", + "Description": "RAK11310", + "Type": "Vendor", + "Targets": [ + "rak11310" + ] + }, + { + "ShortCode": "rokland-rak11310", + "Description": "Rokland RAKwireless RAK11310 WisBlock RP2040 Core Module", + "Type": "Vendor", + "Targets": [ + "rak11310" + ] + }, + { + "ShortCode": "station-g2-tindie", + "Description": "Station G2 Tindie Listing", + "Type": "Marketplace", + "Targets": [ + "station-g2" + ] + }, + { + "ShortCode": "nano-g2-ultra-tindie", + "Description": "Nano G2 Ultra Tindie Listing", + "Type": "Marketplace", + "Targets": [ + "nano-g2-ultra" + ] + }, + { + "ShortCode": "t-deck-plus", + "Description": "T-Deck Plus", + "Type": "Vendor", + "Targets": [ + "t-deck" + ] + }, + { + "ShortCode": "rokland-meshtastic-starter-kit", + "Description": "Rokland Meshtastic Starter Kit", + "Type": "Marketplace" + }, + { + "ShortCode": "rokland-t-deck-base", + "Description": "Rokland T-Deck Base", + "Type": "Marketplace", + "Targets": [ + "t-deck" + ] + }, + { + "ShortCode": "rokland-t-deck-complete", + "Description": "Rokland T-Deck Complete", + "Type": "Marketplace", + "Targets": [ + "t-deck" + ] + }, + { + "ShortCode": "rokland-t-deck-plus", + "Description": "Rokland T-Deck Plus", + "Type": "Marketplace", + "Targets": [ + "t-deck" + ] + }, + { + "ShortCode": "rokland-t-echo", + "Description": "Rokland T-Echo", + "Type": "Marketplace", + "Targets": [ + "t-echo" + ] + }, + { + "ShortCode": "rokland-t-echo-bme280", + "Description": "Rokland T-Echo with BME280", + "Type": "Marketplace", + "Targets": [ + "t-echo" + ] + }, + { + "ShortCode": "rokland-rak19007", + "Description": "Rokland RAKwireless RAK19007 WisBlock Base Board 2nd Gen", + "Type": "Marketplace", + "Targets": [] + }, + { + "ShortCode": "hexaspot-rak19007", + "Description": "Hexaspot RAKwireless RAK19007 WisBlock Base Board 2nd Gen", + "Type": "Marketplace", + "Targets": [] + }, + { + "ShortCode": "rokland-starter-kit", + "Description": "Rokland RAKwireless 4631 Starter Kit", + "Type": "Marketplace", + "Targets": [ + "rak4631" + ] + }, + { + "ShortCode": "hexaspot-starter-kit", + "Description": "Hexaspot RAKwireless 4631 Starter Kit", + "Type": "Marketplace", + "Targets": [ + "rak4631" + ] + }, + { + "ShortCode": "aliexpress-rak1921", + "Description": "RAK1921 OLED Display (AliExpress)", + "Type": "Marketplace", + "Targets": [] + }, + { + "ShortCode": "rak1921", + "Description": "RAK1921 OLED Display (RAK Store)", + "Type": "Vendor", + "Targets": [] + }, + { + "ShortCode": "rokland-rak1921", + "Description": "Rokland RAK1921 WisBlock OLED Display", + "Type": "Marketplace", + "Targets": [] + }, + { + "ShortCode": "muzi-rak1921", + "Description": "Muzi Works RAK1921 OLED Display SSD1306", + "Type": "Marketplace", + "Targets": [] + }, + { + "ShortCode": "aliexpress-rak14000", + "Description": "RAK14000 E-Ink Display (AliExpress)", + "Type": "Marketplace", + "Targets": [] + }, + { + "ShortCode": "rak14000", + "Description": "RAK14000 E-Ink Display (RAK Store)", + "Type": "Vendor", + "Targets": [] + }, + { + "ShortCode": "rokland-rak14000", + "Description": "Rokland RAK14000 WisBlock E-Ink Display", + "Type": "Marketplace", + "Targets": [] + }, + { + "ShortCode": "aliexpress-rak12500", + "Description": "RAK12500 (AliExpress)", + "Type": "Marketplace", + "Targets": [] + }, + { + "ShortCode": "rak12500", + "Description": "RAK12500 (RAK Store)", + "Type": "Vendor", + "Targets": [] + }, + { + "ShortCode": "rak13300", + "Description": "RAK13300 LPWAN Module (RAK Store)", + "Type": "Vendor", + "Targets": [] + }, + { + "ShortCode": "aliexpress-rak13002", + "Description": "RAK13002 IO Module (AliExpress)", + "Type": "Marketplace", + "Targets": [] + }, + { + "ShortCode": "rak13002", + "Description": "RAK13002 IO Module (RAK Store)", + "Type": "Vendor", + "Targets": [] + }, + { + "ShortCode": "rokland-rak13002", + "Description": "Rokland RAK13002 WisBlock IO Adapter Module", + "Type": "Marketplace", + "Targets": [] + }, + { + "ShortCode": "muzi-rak13002", + "Description": "Muzi Works RAK13002 IO Module", + "Type": "Marketplace", + "Targets": [] + }, + { + "ShortCode": "rak6421", + "Description": "WisMesh Pi Hat RAK6421 (RAK Store)", + "Type": "Vendor", + "Targets": [] + }, + { + "ShortCode": "aliexpress-rak18001", + "Description": "RAK18001 RAK Buzzer (AliExpress)", + "Type": "Marketplace", + "Targets": [] + }, + { + "ShortCode": "rak18001", + "Description": "RAK18001 RAK Buzzer (RAK Store)", + "Type": "Vendor", + "Targets": [] + }, + { + "ShortCode": "aliexpress-rak1901", + "Description": "RAK1901 Temperature and Humidity Sensor (AliExpress)", + "Type": "Marketplace", + "Targets": [] + }, + { + "ShortCode": "rak1901", + "Description": "RAK1901 Temperature and Humidity Sensor (RAK Store)", + "Type": "Vendor", + "Targets": [] + }, + { + "ShortCode": "aliexpress-rak1902", + "Description": "RAK-1902 Barometric Pressure Sensor (AliExpress)", + "Type": "Marketplace", + "Targets": [] + }, + { + "ShortCode": "rak1902", + "Description": "RAK-1902 Barometric Pressure Sensor (RAK Store)", + "Type": "Vendor", + "Targets": [] + }, + { + "ShortCode": "aliexpress-rak1906", + "Description": "RAK1906 Environment Sensor (AliExpress)", + "Type": "Marketplace", + "Targets": [] + }, + { + "ShortCode": "rak1906", + "Description": "RAK1906 Environment Sensor (RAK Store)", + "Type": "Vendor", + "Targets": [] + }, + { + "ShortCode": "aliexpress-rak12002", + "Description": "RAK12002 WisBlock RTC Module (AliExpress)", + "Type": "Marketplace", + "Targets": [] + }, + { + "ShortCode": "rak12002", + "Description": "RAK12002 WisBlock RTC Module (RAK Store)", + "Type": "Vendor", + "Targets": [] + }, + { + "ShortCode": "rokland-rak12002", + "Description": "Rokland RAK12002 RTC Module Micro Crystal RV-3028-C7", + "Type": "Marketplace", + "Targets": [] + }, + { + "ShortCode": "aliexpress-wismesh-pocket-v2", + "Description": "WisMesh Pocket V2 (AliExpress)", + "Type": "Marketplace" + }, + { + "ShortCode": "rokland-wismesh-pocket-v2", + "Description": "WisMesh Pocket V2 (Rokland)", + "Type": "Marketplace" + }, + { + "ShortCode": "hexaspot-wismesh-pocket-v2", + "Description": "WisMesh Pocket V2 (Hexaspot)", + "Type": "Marketplace" + }, + { + "ShortCode": "wismesh-pocket-v2", + "Description": "WisMesh Pocket V2 (RAK Store)", + "Type": "Vendor" + }, + { + "ShortCode": "aliexpress-wismesh-pocket-mini", + "Description": "WisMesh Pocket Mini (Rokland)", + "Type": "Marketplace" + }, + { + "ShortCode": "rokland-wismesh-pocket-mini", + "Description": "WisMesh Pocket Mini (Rokland)", + "Type": "Marketplace" + }, + { + "ShortCode": "wismesh-pocket-mini", + "Description": "WisMesh Pocket Mini (RAK Store)", + "Type": "Vendor" + }, + { + "ShortCode": "aliexpress-rak19026", + "Description": "WisMesh Baseboard (AliExpress)", + "Type": "Marketplace", + "Targets": [] + }, + { + "ShortCode": "rokland-rak19026", + "Description": "WisMesh Baseboard (Rokland)", + "Type": "Marketplace", + "Targets": [] + }, + { + "ShortCode": "rak19026", + "Description": "WisMesh Baseboard (RAK Store)", + "Type": "Vendor", + "Targets": [] + }, + { + "ShortCode": "aliexpress-wismesh-tap", + "Description": "RAK WisMesh Tap (AliExpress)", + "Type": "Marketplace", + "Targets": [ + "rak_wismeshtap" + ] + }, + { + "ShortCode": "aliexpress-board-one", + "Description": "RAK WisMesh Board ONE (AliExpress)", + "Type": "Marketplace" + }, + { + "ShortCode": "board-one", + "Description": "RAK WisMesh Board ONE (RAK Store)", + "Type": "Vendor" + }, + { + "ShortCode": "rokland-board-one", + "Description": "Rokland WisMesh Board ONE (US915 MHz)", + "Type": "Marketplace" + }, + { + "ShortCode": "wismesh-repeater", + "Description": "WisMesh Repeater (RAK Store)", + "Type": "Vendor", + "Targets": [ + "rak2560" + ] + }, + { + "ShortCode": "aliexpress-wismesh-repeater", + "Description": "WisMesh Repeater (AliExpress)", + "Type": "Marketplace" + }, + { + "ShortCode": "aliexpress-wismesh-repeater-mini", + "Description": "WisMesh Repeater Mini (AliExpress)", + "Type": "Marketplace" + }, + { + "ShortCode": "hexaspot-wismesh-repeater-mini", + "Description": "WisMesh Repeater Mini (Hexaspot)", + "Type": "Marketplace" + }, + { + "ShortCode": "wismesh-repeater-mini", + "Description": "WisMesh Repeater Mini (RAK Store)", + "Type": "Vendor" + }, + { + "ShortCode": "aliexpress-wismesh-ethernet-gateway", + "Description": "WisMesh Ethernet MQTT Gateway (AliExpress)", + "Type": "Marketplace" + }, + { + "ShortCode": "wismesh-ethernet-gateway", + "Description": "WisMesh Ethernet MQTT Gateway (RAK Store)", + "Type": "Vendor" + }, + { + "ShortCode": "aliexpress-wismesh-wifi-gateway", + "Description": "WisMesh WiFi MQTT Gateway (AliExpress)", + "Type": "Marketplace" + }, + { + "ShortCode": "wismesh-wifi-gateway", + "Description": "WisMesh WiFi MQTT Gateway (RAK Store)", + "Type": "Vendor" + }, + { + "ShortCode": "aliexpress-board-one-pocket", + "Description": "RAK WisMesh Board ONE Pocket (AliExpress)", + "Type": "Marketplace" + }, + { + "ShortCode": "board-one-pocket", + "Description": "RAK WisMesh Board ONE Pocket (RAK Store)", + "Type": "Vendor" + }, + { + "ShortCode": "aliexpress-wismesh-unify-enclosure", + "Description": "WisMesh Unify Enclosure (AliExpress)", + "Type": "Marketplace", + "Targets": [] + }, + { + "ShortCode": "wismesh-unify-enclosure", + "Description": "WisMesh Unify Enclosure (RAK Store)", + "Type": "Vendor", + "Targets": [] + }, + { + "ShortCode": "aliexpress-wismesh-antenna", + "Description": "WisMesh Antenna (AliExpress)", + "Type": "Marketplace", + "Targets": [] + }, + { + "ShortCode": "wismesh-antenna", + "Description": "WisMesh Antenna (RAK Store)", + "Type": "Vendor", + "Targets": [] + }, + { + "ShortCode": "muzi-rak4631", + "Description": "Muzi RAK4631 Starter Kit", + "Type": "Marketplace", + "Targets": [ + "rak4631" + ] + }, + { + "ShortCode": "aliexpress-rak19007", + "Description": "RAK19007 (AliExpress)", + "Type": "Marketplace", + "Targets": [] + }, + { + "ShortCode": "aliexpress-starter-kit", + "Description": "WisMesh RAK4631 Starter Kit (AliExpress)", + "Type": "Marketplace", + "Targets": [ + "rak4631" + ] + }, + { + "ShortCode": "rak19003", + "Description": "RAK19003 (RAK Store)", + "Type": "Vendor", + "Targets": [] + }, + { + "ShortCode": "aliexpress-rak19003", + "Description": "RAK19003 (AliExpress)", + "Type": "Marketplace", + "Targets": [] + }, + { + "ShortCode": "rak19001", + "Description": "RAK19001 WisBlock Dual IO Base Board (RAK Store)", + "Type": "Vendor", + "Targets": [] + }, + { + "ShortCode": "aliexpress-rak19001", + "Description": "RAK19001 WisBlock Dual IO Base Board (AliExpress)", + "Type": "Marketplace", + "Targets": [] + }, + { + "ShortCode": "rokland-19003", + "Description": "Rokland WisBlock Mini Base Board RAK19003 (Ver B)", + "Type": "Marketplace", + "Targets": [] + }, + { + "ShortCode": "hexaspot-19003", + "Description": "Hexaspot WisBlock Mini Base Board RAK19003 (Ver B)", + "Type": "Marketplace", + "Targets": [] + }, + { + "ShortCode": "rokland-19001", + "Description": "Rokland WisBlock Dual IO Base Board RAK19001", + "Type": "Marketplace", + "Targets": [] + }, + { + "ShortCode": "hexaspot-19001", + "Description": "Hexaspot WisBlock Dual IO Base Board RAK19001", + "Type": "Marketplace", + "Targets": [] + }, + { + "ShortCode": "rokland-4631", + "Description": "Rokland RAK4631 Nordic nRF52840 BLE Core Module for LoRaWAN with LoRa SX1262", + "Type": "Marketplace", + "Targets": [ + "rak4631" + ] + }, + { + "ShortCode": "hexaspot-4631", + "Description": "Hexaspot RAK4631 Nordic nRF52840 BLE Core Module for LoRaWAN with LoRa SX1262", + "Type": "Marketplace", + "Targets": [ + "rak4631" + ] + }, + { + "ShortCode": "aliexpress-rak4631", + "Description": "RAK4631 Nordic nRF52840 BLE Core Module (AliExpress)", + "Type": "Marketplace", + "Targets": [ + "rak4631" + ] + }, + { + "ShortCode": "rakwireless-4631", + "Description": "RAK4631 Nordic nRF52840 BLE Core Module", + "Type": "Vendor", + "Targets": [ + "rak4631" + ] + }, + { + "ShortCode": "rakwireless-rak11310", + "Description": "RAK11310 RP2040 Core Module)", + "Type": "Vendor", + "Targets": [ + "rak11310" + ] + }, + { + "ShortCode": "rakwireless-rak3312", + "Description": "RAK3312 ESP32-S3 Core Module", + "Type": "Vendor", + "Targets": [ + "rak3312" + ] + }, + { + "ShortCode": "hexaspot-rak3312", + "Description": "Hexaspot RAK3312 ESP32-S3 Core Module", + "Type": "Marketplace", + "Targets": [ + "rak3312" + ] + }, + { + "ShortCode": "rokland-rak3312", + "Description": "Rokland RAK3312 ESP32-S3 Core Module", + "Type": "Marketplace", + "Targets": [ + "rak3312" + ] + }, + { + "ShortCode": "rokland-rak3312-starter-kit", + "Description": "Rokland RAK3312 ESP32-S3 Starter Kit", + "Type": "Marketplace", + "Targets": [ + "rak3312" + ] + }, + { + "ShortCode": "aliexpress-rak11310", + "Description": "RAK11310 RP2040 Core Module (AliExpress)", + "Type": "Marketplace", + "Targets": [ + "rak11310" + ] + }, + { + "ShortCode": "rokland-1901", + "Description": "Rokland RAK1901 Temperature and Humidity Sensor", + "Type": "Marketplace", + "Targets": [] + }, + { + "ShortCode": "rokland-1902", + "Description": "Rokland RAK1902 Barometric Pressure Sensor", + "Type": "Marketplace", + "Targets": [] + }, + { + "ShortCode": "rokland-1906", + "Description": "Rokland RAK1906 WisBlock Environment Sensor", + "Type": "Marketplace", + "Targets": [] + }, + { + "ShortCode": "aliexpress-wismesh-tap", + "Description": "RAKwireless WisMesh Tap (AliExpress)", + "Type": "Marketplace", + "Targets": [ + "rak_wismeshtap" + ] + }, + { + "ShortCode": "rokland-wismesh-tap", + "Description": "RAKwireless WisMesh Tap (Rokland)", + "Type": "Marketplace", + "Targets": [ + "rak_wismeshtap" + ] + }, + { + "ShortCode": "rakdap1", + "Description": "RAKwireless RAKDAP1 Debug and Flash Tool", + "Type": "Vendor", + "Targets": [] + }, + { + "ShortCode": "rokland-heltec-wsl-v3", + "Description": "Rokland WSL V3", + "Type": "Marketplace", + "Targets": [ + "heltec-wsl-v3" + ] + }, + { + "ShortCode": "aliexpress-heltec-wsl-v3", + "Description": "Aliexpress WSL V3", + "Type": "Marketplace", + "Targets": [ + "heltec-wsl-v3" + ] + }, + { + "ShortCode": "rokland-heltec-wireless-tracker", + "Description": "Rokland Wireless Tracker", + "Type": "Marketplace", + "Targets": [ + "heltec-wireless-tracker" + ] + }, + { + "ShortCode": "aliexpress-heltec-wireless-tracker", + "Description": "Aliexpress Wireless Tracker", + "Type": "Marketplace", + "Targets": [ + "heltec-wireless-tracker" + ] + }, + { + "ShortCode": "aliexpress-heltec-wireless-paper", + "Description": "Aliexpress Wireless Paper", + "Type": "Marketplace", + "Targets": [ + "heltec-wireless-paper" + ] + }, + { + "ShortCode": "rokland-heltec-wireless-paper", + "Description": "Rokland Wireless Paper", + "Type": "Marketplace", + "Targets": [ + "heltec-wireless-paper" + ] + }, + { + "ShortCode": "muzi-heltec-mesh-node-t114", + "Description": "MuziWorks Mesh Node T114", + "Type": "Marketplace", + "Targets": [ + "heltec-mesh-node-t114" + ] + }, + { + "ShortCode": "aliexpress-heltec-mesh-node-t114", + "Description": "Aliexpress Mesh Node T114", + "Type": "Marketplace", + "Targets": [ + "heltec-mesh-node-t114" + ] + }, + { + "ShortCode": "aliexpress-heltec-vision-master-e213", + "Description": "Aliexpress Vision Master E213", + "Type": "Marketplace", + "Targets": [ + "heltec-vision-master-e213" + ] + }, + { + "ShortCode": "aliexpress-heltec-vision-master-e290", + "Description": "Aliexpress Vision Master E290", + "Type": "Marketplace", + "Targets": [ + "heltec-vision-master-e290" + ] + }, + { + "ShortCode": "aliexpress-heltec-vision-master-t190", + "Description": "Aliexpress Vision Master T190", + "Type": "Marketplace", + "Targets": [ + "heltec-vision-master-t190" + ] + }, + { + "ShortCode": "seeed-wio-tracker-l1-oled", + "Description": "Wio Tracker L1 (with OLED)", + "Type": "Vendor", + "Targets": [ + "seeed_wio_tracker_L1" + ] + }, + { + "ShortCode": "seeed-wio-tracker-l1-oled_aliexpress", + "Description": "Wio Tracker L1 (with OLED)", + "Type": "Marketplace", + "Targets": [ + "seeed_wio_tracker_L1" + ] + }, + { + "ShortCode": "seeed_wio_tracker_L1_eink", + "Description": "Wio Tracker L1 (with E-Ink)", + "Type": "Vendor", + "Targets": [ + "seeed_wio_tracker_L1_eink" + ] + }, + { + "ShortCode": "seeed_wio_tracker_L1_eink_amazon", + "Description": "Wio Tracker L1 (with E-Ink) Amazon", + "Type": "Marketplace", + "Targets": [ + "seeed_wio_tracker_L1_eink" + ] + }, + { + "ShortCode": "seeed-wio-tracker-l1-lite", + "Description": "Wio Tracker L1 Lite (no display)", + "Type": "Vendor" + }, + { + "ShortCode": "seeed_solar_node_p1", + "Description": "SenseCAP Solar Node P1", + "Type": "Vendor" + }, + { + "ShortCode": "seeed_solar_node_p1_aliexpress", + "Description": "SenseCAP Solar Node P1 Aliexpress", + "Type": "Marketplace" + }, + { + "ShortCode": "android-closed-test", + "Description": "Android Closed Test Form", + "Type": "Internal" + }, + { + "ShortCode": "t-deck-pro", + "Description": "LilyGo T-Deck Pro", + "Type": "Vendor", + "Targets": [ + "t-deck-pro" + ] + }, + { + "ShortCode": "rak4631_nomadstar_meteor_pro", + "Description": "NomadStar Meteor Pro", + "Type": "Vendor", + "Targets": [ + "rak4631_nomadstar_meteor_pro" + ] + }, + { + "ShortCode": "muziworks", + "Description": "muzi WORKS Homepage", + "Type": "Internal" + }, + { + "ShortCode": "r1-neo", + "Description": "muzi WORKS R1 Neo", + "Type": "Vendor", + "Targets": [ + "r1-neo" + ] + }, + { + "ShortCode": "muzi-base", + "Description": "muzi WORKS Base System", + "Type": "Vendor", + "Targets": [ + "muzi-base" + ] + }, + { + "ShortCode": "muzi-base-uno", + "Description": "muzi WORKS Base Uno", + "Type": "Vendor", + "Targets": [ + "muzi-base" + ] + }, + { + "ShortCode": "muzi-base-duo", + "Description": "muzi WORKS Base Duo", + "Type": "Vendor", + "Targets": [ + "muzi-base" + ] + }, + { + "ShortCode": "muzi-base-super-io", + "Description": "muzi WORKS Base Super IO", + "Type": "Vendor", + "Targets": [ + "muzi-base" + ] + }, + { + "ShortCode": "ttc-tickets", + "Description": "The Things Conference Tickets", + "Type": "Internal" + }, + { + "ShortCode": "rokland-atlavox-makers-market", + "Description": "Rokland Atlavox Makers Market", + "Type": "Marketplace" + }, + { + "ShortCode": "rokland-tlora-pager", + "Description": "Rokland T-Lora Pager", + "Type": "Marketplace", + "Targets": [ + "tlora-pager" + ] + }, + { + "ShortCode": "tlora-pager", + "Description": "T-Lora Pager", + "Type": "Vendor", + "Targets": [ + "tlora-pager" + ] + }, + { + "ShortCode": "hexaspot", + "Description": "Hexaspot Meshtastic Products", + "Type": "Marketplace", + "Targets": [] + }, + { + "ShortCode": "ew26", + "Description": "embeddedworld26 event page", + "Type": "Internal" + }, + { + "ShortCode": "hexaspot-heltec-v3", + "Description": "Heltec V3 (Hexaspot)", + "Type": "Marketplace", + "Targets": [ + "heltec-v3" + ] + }, + { + "ShortCode": "hexaspot-heltec-v4", + "Description": "Heltec V4 (Hexaspot)", + "Type": "Marketplace", + "Targets": [ + "heltec-v4" + ] + }, + { + "ShortCode": "hexaspot-wireless-tracker-v2", + "Description": "Heltec Wireless Tracker V2 (Hexaspot)", + "Type": "Marketplace", + "Targets": [ + "heltec-wireless-tracker-v2" + ] + } + ], + "Marketplaces": { + "rokland": { + "Regions": [ + "AU", + "AT", + "BE", + "CA", + "DK", + "EC", + "FR", + "DE", + "IE", + "JP", + "NL", + "NZ", + "NO", + "PK", + "ES", + "SE", + "CH", + "GB", + "US" + ] + }, + "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" + ] + }, + "aliexpress": { + "Regions": [] + }, + "amazon": { + "Regions": [ + "AU", + "CA", + "FR", + "DE", + "IE", + "JP", + "NL", + "ES", + "SE", + "GB", + "US" + ] + }, + "tindie": { + "Regions": [ + "US", + "CA", + "GB", + "DE", + "FR", + "AU", + "NL" + ] + }, + "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" + ] + } + } +} diff --git a/src/index.ts b/src/index.ts index a7235bc..4d4a828 100644 --- a/src/index.ts +++ b/src/index.ts @@ -7,6 +7,7 @@ import { favicon } from "@tinyhttp/favicon"; import { logger } from "@tinyhttp/logger"; import { RegisterMqttClient } from "./lib/index.js"; import { + DeviceLinksRoutes, FirmwareRoutes, GithubRoutes, MqttRoutes, @@ -81,6 +82,7 @@ app FirmwareRoutes(); GithubRoutes(); ResourceRoutes(); +DeviceLinksRoutes(); UpdaterRoutes(); MqttRoutes(); diff --git a/src/lib/deviceLinks.ts b/src/lib/deviceLinks.ts new file mode 100644 index 0000000..1e5e582 --- /dev/null +++ b/src/lib/deviceLinks.ts @@ -0,0 +1,110 @@ +import { readFileSync } from "node:fs"; +import { deviceHardwareList } from "./resource.js"; + +// Device-link catalog, synced from msh.to's public /api/urls INTO this repo by a scheduled +// GitHub Action (.github/workflows/sync-device-links.yml). We read the committed file — no +// runtime dependency on msh.to. The path resolves the same in dev (src/lib) and prod (dist/lib), +// both two levels below the repo root, so it is independent of the working directory. +const CATALOG_PATH = new URL("../../data/deviceLinks.json", import.meta.url); +const SOURCE = "https://msh.to/api/urls"; + +// Shape of the synced catalog (PascalCase, mirrors msh.to's urls.json / /api/urls). +interface Route { + ShortCode: string; + Description?: string; + Type?: string; + Targets?: string[]; +} + +interface Marketplace { + Regions?: string[]; +} + +interface Catalog { + Routes?: Route[]; + Marketplaces?: Record | null; +} + +export interface DeviceLink { + shortCode: string; + url: string; + description?: string; + type: "internal" | "vendor" | "marketplace"; + targets: string[] | null; // null = untriaged, [] = intentionally device-agnostic + hwModels: number[] | null; // derived from targets via deviceHardwareList + marketplace: string | null; + regions: string[] | null; // null = worldwide (no region filter) +} + +export interface DeviceLinksResponse { + version: number; + generatedAt: string; + source: string; + links: DeviceLink[]; +} + +const targetToHwModel = new Map( + deviceHardwareList.map((d) => [d.platformioTarget, d.hwModel]), +); + +const resolve = (): DeviceLinksResponse => { + const data = JSON.parse(readFileSync(CATALOG_PATH, "utf8")) as Catalog; + const routes = data.Routes ?? []; + const marketplaces = data.Marketplaces ?? {}; // tolerate null/absent + const retailers = Object.keys(marketplaces); + + // Token detection only picks which region list applies — Type is authoritative for classification. + const retailerOf = (code: string): string | null => + retailers.find( + (m) => + code.startsWith(`${m}-`) || + code.startsWith(`${m}_`) || + code.endsWith(`-${m}`) || + code.endsWith(`_${m}`) || + code === m, + ) ?? null; + + const links: DeviceLink[] = routes.map((r) => { + const t = (r.Type ?? "").toLowerCase(); + const type: DeviceLink["type"] = + t === "vendor" + ? "vendor" + : t === "marketplace" + ? "marketplace" + : "internal"; + const targets = r.Targets ?? null; + const marketplace = type === "marketplace" ? retailerOf(r.ShortCode) : null; + const regions = marketplace + ? (marketplaces[marketplace]?.Regions ?? null) + : null; + return { + shortCode: r.ShortCode, + url: `https://msh.to/${r.ShortCode}`, + description: r.Description, + type, + targets, + hwModels: + targets + ?.map((x) => targetToHwModel.get(x)) + .filter((m): m is number => m != null) ?? null, + marketplace, + regions: regions?.length ? regions : null, // []/missing => null = worldwide + }; + }); + + return { + version: 1, + generatedAt: new Date().toISOString(), + source: SOURCE, + links, + }; +}; + +// Resolve once per process. The catalog only changes via a committed file + redeploy, +// so there is nothing to invalidate at runtime. +let cached: DeviceLinksResponse | null = null; + +export const getDeviceLinks = (): DeviceLinksResponse => { + if (!cached) cached = resolve(); + return cached; +}; diff --git a/src/routes/deviceLinks.ts b/src/routes/deviceLinks.ts new file mode 100644 index 0000000..0839fa4 --- /dev/null +++ b/src/routes/deviceLinks.ts @@ -0,0 +1,12 @@ +import { app } from "../index.js"; +import { getDeviceLinks } from "../lib/deviceLinks.js"; + +export const DeviceLinksRoutes = () => + app.get("resource/deviceLinks", (_req, res) => { + try { + res.json(getDeviceLinks()); + } catch (err) { + console.error("deviceLinks", err); + res.sendStatus(502); + } + }); diff --git a/src/routes/index.ts b/src/routes/index.ts index 268ec1d..b5fd7f6 100644 --- a/src/routes/index.ts +++ b/src/routes/index.ts @@ -1,3 +1,4 @@ +export { DeviceLinksRoutes } from "./deviceLinks.js"; export { FirmwareRoutes } from "./firmware.js"; export { GithubRoutes } from "./github.js"; export { MqttRoutes } from "./mqtt.js";