mirror of
https://github.com/Roslund/sthlm-mesh.git
synced 2026-03-28 17:43:02 +01:00
Compare commits
91 Commits
codex/find
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9dc63b4c15 | ||
|
|
d574b77f14 | ||
|
|
bc767f5b5e | ||
|
|
7e5cf9f89b | ||
|
|
f5a8491ffc | ||
|
|
ad20a5ec57 | ||
|
|
4a71c3c3e4 | ||
|
|
07ba2338aa | ||
|
|
034cbdfd1e | ||
|
|
c616cf0f4d | ||
|
|
3fd22b0f9f | ||
|
|
110f317909 | ||
|
|
15afb142e3 | ||
|
|
98aaf40bdc | ||
|
|
d5fa26ff52 | ||
|
|
b3b7ed0ca6 | ||
|
|
2f02bebe9b | ||
|
|
03119d7418 | ||
|
|
94339f415e | ||
|
|
1e57520259 | ||
|
|
a610940241 | ||
|
|
83a22c95ec | ||
|
|
9c9564b660 | ||
|
|
e8e7464c5b | ||
|
|
b39de31c75 | ||
|
|
77dfbd380d | ||
|
|
a4b0e18c3a | ||
|
|
c4318073bb | ||
|
|
cfdd8c0cd6 | ||
|
|
5820d02e92 | ||
|
|
f9e3a35f61 | ||
|
|
ebec5deb24 | ||
|
|
14689c6bfa | ||
|
|
50147d0ce2 | ||
|
|
775a5ace19 | ||
|
|
3061fd057e | ||
|
|
c2c20089e0 | ||
|
|
42acd5b9d7 | ||
|
|
e5b4c9fb18 | ||
|
|
9b6e169a73 | ||
|
|
0090246633 | ||
|
|
8d3fff45ff | ||
|
|
0bda6dbb05 | ||
|
|
6ca0c8a4c9 | ||
|
|
3d76dfb510 | ||
|
|
ef48ca777f | ||
|
|
8193532bd7 | ||
|
|
7c5b41f5a3 | ||
|
|
140935d478 | ||
|
|
4bc389237b | ||
|
|
590d274d2d | ||
|
|
6787e9cf20 | ||
|
|
b773ba77f9 | ||
|
|
5a5a7cc88d | ||
|
|
7dff7d434d | ||
|
|
382ca20af7 | ||
|
|
9fae44a7b5 | ||
|
|
457ececec7 | ||
|
|
8ec6685ff3 | ||
|
|
80fddf550c | ||
|
|
9cac363318 | ||
|
|
6e047a9b24 | ||
|
|
ca5fb198c5 | ||
|
|
afa90c3034 | ||
|
|
7c092b7b14 | ||
|
|
d687e5f112 | ||
|
|
b13f729b81 | ||
|
|
9ceb0db75a | ||
|
|
12b302dea8 | ||
|
|
4ab9c2d5a5 | ||
|
|
951e4e386b | ||
|
|
ac1466c52d | ||
|
|
8f75b18a92 | ||
|
|
1b2c015b1e | ||
|
|
4c3ae0b635 | ||
|
|
4082158fd0 | ||
|
|
c3f9d59b6a | ||
|
|
3c954e204c | ||
|
|
7115c6565e | ||
|
|
e0a2752189 | ||
|
|
1e9b5aad1f | ||
|
|
968d7613f4 | ||
|
|
5b1b4ad97f | ||
|
|
65b87c4071 | ||
|
|
a29616200e | ||
|
|
8a53f41ad4 | ||
|
|
7e64b39def | ||
|
|
f1da479213 | ||
|
|
1c2ea20814 | ||
|
|
dac7e861c0 | ||
|
|
187276a79b |
@@ -1,11 +1,11 @@
|
||||
# STHLM-MESH
|
||||
|
||||
**[STHLM-MESH](https://sthlm-mesh.se)** är webbsida byggd med [Hugo][] och använder [Docsy][] som tema.
|
||||
Sidan riktar sig till de som är intresserade av LoRa mesh teknologi i almenhet. Med huvudfokus är det [Meshtastic][] mesh i som omfattar Stockholms området.
|
||||
**[STHLM-MESH](https://sthlm-mesh.se)** är en webbsida byggd med [Hugo][] och använder [Docsy][] som tema.
|
||||
Sidan riktar sig till de som är intresserade av LoRa mesh-teknologi i allmänhet. Med huvudfokus är det [Meshtastic][]-mesh som omfattar Stockholmsområdet.
|
||||
|
||||
Innehållet skrivs på svenska eller svengelska. Sidan har stöd för flera språk, men att översätta innehållet till engelska är inte prioriterat, då det finns redan massvis med information om Meshtastic på Engelska.
|
||||
|
||||
Sidan är hostad genom _GitHub Pages_ och en GitHub Action uppdaterar sidan vid varje commit till `main` barnchen.
|
||||
Sidan är hostad genom _GitHub Pages_ och en GitHub Action uppdaterar sidan vid varje commit till `main` branchen.
|
||||
|
||||
|
||||
## Bidra till projektet
|
||||
@@ -21,7 +21,7 @@ Webbsidan kan enkelt köras inuti en [Docker](https://docs.docker.com/)-containe
|
||||
1. Bygg containern
|
||||
|
||||
```bash
|
||||
docker-compose up --build
|
||||
docker compose up --build
|
||||
```
|
||||
1. Öppna en webbläsare och anslut till `http://localhost:1313`
|
||||
|
||||
|
||||
@@ -70,7 +70,7 @@ Vi rekommenderar den senaste betan, men titta tillbaka regelbundet då utvecklin
|
||||
|
||||
{{% blocks/feature icon="fab fa-app-store-ios" title="Konfigurera med appen" %}}
|
||||
För att konfigurera din enhet rekommenderas den officiella appen för <a href="https://apple.co/3Auysep" target="_blank" rel="noopener noreferrer">iOS</a> eller <a href="https://play.google.com/store/apps/details?id=com.geeksville.mesh" target="_blank" rel="noopener noreferrer">Android</a>. \
|
||||
I Stockholm använder vi kanal-preseten **Long Fast**. \
|
||||
I Stockholm använder vi kanal-preseten **Medium Range - Fast**. \
|
||||
För mer detaljer om enhetskonfiguration se vår sida: [Rekommenderade Inställningar]({{<ref settings>}}).
|
||||
{{% /blocks/feature %}}
|
||||
|
||||
|
||||
81
content/sv/blog/2025-09-19-switch-to-mediumfast.md
Normal file
81
content/sv/blog/2025-09-19-switch-to-mediumfast.md
Normal file
@@ -0,0 +1,81 @@
|
||||
---
|
||||
title: Övergång till MediumFast den 27 September!
|
||||
date: 2025-09-19
|
||||
---
|
||||
Meshtastic nätverket i Stockholm har vuxit kraftigt under de senaste två åren. Med över 200 dagligt aktiva noder räcker inte längre bandbredden till, även med optimerade inställningar, detta påverkar tillförlitligheten i meshen och flertalet meddelanden når inte fram.
|
||||
|
||||
För att få ett stabilare mesh där vi har möjlighet att fortsätta växa kommer vi gå över från LongFast till MediumFast modulation. Detta innebär bland annat:
|
||||
- **Snabbare meddelanden**: 3x kortare sändningstid
|
||||
- **Mindre kollisioner**: Färre störningar och högre leveranssäkerhet
|
||||
- **Bättre batteritid**: Särskilt viktigt för solcellsdrivna noder
|
||||
- **Högre nodkapacitet**: Plats för fler noder i nätverket
|
||||
|
||||
|
||||
Bilden nedan visar hur en övergång till MediumFast påverkar förbindelser i meshen. I praktiken förväntar vi oss ännu bättre resultat då risken för kollisioner minskar. Bilden är baserad på data från traceroutes och är inte heltäckande.
|
||||
|
||||
{{< image-compare left="/images/blog/2025-09-19-switch-to-mediumfast-lf.png" right="/images/blog/2025-09-19-switch-to-mediumfast-mf.png" left-alt="LongFast anslutningar" right-alt="MediumFast anslutningar" caption="Jämförelse mellan LongFast (vänster) och MediumFast (höger) anslutningar i Stockholm mesh. Dra reglaget för att se skillnaden." >}}
|
||||
|
||||
### Meetup
|
||||
Vi planerar även att köra en meetup den 1:a oktober för att utvärdera övergången till MediumFast. Förhoppningsvis kan vi fira och fokusera på hur meshet blir ännu bättre i framtiden. Mer info kommer.
|
||||
|
||||
### Övergångsstatus till MediumFast
|
||||
Denna mätare visar hur många noder som har växlat till MediumFast-kanalen av det totala antalet noder som hörts idag.
|
||||
|
||||
<div class="container my-3 mx-0" style="max-width: 600px;">
|
||||
<div class="row text-center px-0">
|
||||
<div class="col">
|
||||
<div id="transitionGaugeContainer" class="stats-chart-container" style="height: 300px;">
|
||||
<canvas id="transitionGauge"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
### Mer teknisk information
|
||||
För fler tekniska detaljer om MediumFast, läs Meshtastics officiella artikel: [Why Your Mesh Should Switch From LongFast](https://meshtastic.org/blog/why-your-mesh-should-switch-from-longfast/)
|
||||
|
||||
### Frågor och svar
|
||||
##### Behöver jag göra något?
|
||||
**Ja**, du behöver manuellt uppdatera till MediumFast-preset under LoRa inställningarna. Detta fungerar även över RemoteAdmin för noder på svåråtkomliga platser.
|
||||
|
||||
|
||||
##### Kan jag byta till MediumFast redan nu?
|
||||
**Ja**, flertalet noder har redan bytt till MediumFast då meshen har fungerat dåligt för dem på LongFast. Dock så är övergången planerad till den 27 september, det är först den helgen många av våra välplacerade routrar kommer gå över. Vill du testa innan dess så är det fritt fram, men det är inte säkert att du når ut.
|
||||
|
||||
|
||||
##### Kan jag köra MediumFast och LongFast samtidigt?
|
||||
För att göra det behöver du två enheter. Tanken är att vi genomför en övergång från LongFast till MediumFast och inte kör båda samtidigt.
|
||||
|
||||
|
||||
##### Vilken frekvens kör MediumFast på?
|
||||
__869.400MHz - 869.650MHz__ i Europa är det exakt samma som LongFast. Det fria ISM-frekvensbandet i EU är begränsat och Meshtastic använder hela, både på LongFast och MediumFast.
|
||||
|
||||
|
||||
##### Kommer enheter på LongFast vidarebefordra meddelanden som skickas på MediumFast?
|
||||
**Nej**, det är därför vi planerar en gemensam övergång.
|
||||
|
||||
|
||||
##### Riskerar detta inte kortare räckvidd?
|
||||
Rent teoretiskt kanske? Men även på MediumFast kommer vi klara de 45km långa förbindelser vi har mellan vissa noder i Stockholm. MediumFast är mer lämpad för den stadsmiljö vi har här i Stockholm.
|
||||
Hela poängen med Meshtastic är att bygga ett meshnätverk. På MediumFast kommer meshet bli stabilare och klara flera hopp tillförlitligt.
|
||||
|
||||
|
||||
##### Hur kan jag se om MediumFast kommer fungera för mig?
|
||||
MediumFast klarar en Signal to Noise Ratio på `-15dBm` till skillnad mot LongFasts `-20dBm`. Genomför traceroutes till dina närmsta grannar, får du SNR på över `-15dBm` så kommer MediumFast fungera bättre än LongFast.
|
||||
|
||||
|
||||
##### Vilka andra europeiska städer har bytt?
|
||||
Stockholm följer trenden. Flertalet andra stora europeiska mesh-nätverk har bytt från LongFast med lyckat resultat. Till exempel:
|
||||
- **Berlin**: [Bytte till MediumFast](https://www.reddit.com/r/meshtastic/comments/1kal6vv/berlin_is_switching_to_mediumfast/)
|
||||
- **Paris**: [Använder MediumFast](https://wiki.mesh-idf.fr/fr/carte/carte-du-mesh-idf)
|
||||
- **Polen**: [Nationell kampanj för MediumFast](https://przejdznamediumfast.pl/)
|
||||
- **Madrid**: Har bytt till MediumSlow.
|
||||
|
||||
Det finns säkert fler som vi inte känner till. Har du koll på andra städer i Europa som har bytt, meddela oss på Discord.
|
||||
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/chartjs-plugin-annotation"></script>
|
||||
<script src="/js/status/shared.js"></script>
|
||||
<script src="/js/status/transition-status-gauge.js"></script>
|
||||
14
content/sv/blog/2026-01-03-realtime-traceroutes.md
Normal file
14
content/sv/blog/2026-01-03-realtime-traceroutes.md
Normal file
@@ -0,0 +1,14 @@
|
||||
---
|
||||
title: Realtime Traceroutes på Kartan
|
||||
date: 2026-01-03
|
||||
---
|
||||
<video autoplay loop muted playsinline style="width: 100%; max-width: 500px; height: auto; border-radius: 8px;">
|
||||
<source src="/images/blog/2026-01-03-traceroute-demo.mp4" type="video/mp4"></video>
|
||||
|
||||
Kartan är nu uppdaterad med ny funktionalitet för att visa traceroutes i realtid.
|
||||
|
||||
Visningen av traceroutes aktiveras genom att klicka på "layer"-ikonen uppe till höger och sedan aktivera "Traceroutes" i overlay-menyn.
|
||||
|
||||
För att en traceroute ska visas måste samtliga noder som tracerouten går via ha en position på kartan, och någon av våra MQTT-gateways måste ha tagit emot paketet.
|
||||
|
||||
Detta har nu ersatt den tidigare funktionen för att visa historiska traceroutes. Inställningen "Traceroutes Max Age" finns kvar och används för att utnyttja traceroute-data för att visa neighbours och backbone connections.
|
||||
7
content/sv/blog/_index.md
Normal file
7
content/sv/blog/_index.md
Normal file
@@ -0,0 +1,7 @@
|
||||
---
|
||||
title: Blogg
|
||||
description: Nyheter och uppdateringar från STHLM-MESH
|
||||
layout: list
|
||||
cascade:
|
||||
weight: 50
|
||||
---
|
||||
36
content/sv/blog/meetups/2026-02-04-Meetup.md
Normal file
36
content/sv/blog/meetups/2026-02-04-Meetup.md
Normal file
@@ -0,0 +1,36 @@
|
||||
---
|
||||
title: Meetup 4 Februari - Mariatorget
|
||||
date: 2026-02-01
|
||||
layout: meetup_single
|
||||
|
||||
---
|
||||
Nu är det dags för en Meshtastic AW i Stockholm igen!
|
||||
Kom och träffa likasinnade, snacka LoRa och bygg ut nätverket i Stockholm!
|
||||
|
||||
Ta gärna med din nod, eller visa upp det senaste bygget.
|
||||
|
||||
|
||||
|
||||
|
||||
__📍 Plats:__[Pitchers, Mariatorget](https://maps.app.goo.gl/TgsoWWLXHHCv1JvK6)
|
||||
__📅 Datum:__ Onsdag 4 Februari
|
||||
__⏰ Tid:__ 17:00
|
||||
|
||||
Om du inte kan komma exakt 17:00 är det helt okej att dyka upp senare. Skriv gärna ett meddelande på meshen eller Discord om du kommer!
|
||||
|
||||
För er som har beställt MeshADV så finns det möjlighet att hämta upp dom under AW:n.
|
||||
<img src="https://cdn.discordapp.com/attachments/1359596002079801640/1461619499101847595/20260116_08h13m46s_grim.png?ex=6980f77c&is=697fa5fc&hm=637c327c0e71d006f17dff84d9f91e596793d9767c1af48bba9e5f2b009c97d6" width="250px" alt="Meetup bild" />
|
||||
|
||||
<!-- RSVP Tracker Container -->
|
||||
<div id="rsvp-tracker-2026-02-04-aw-mariatorget" class="mt-4"></div>
|
||||
|
||||
|
||||
<script src="/js/status/shared.js"></script>
|
||||
<script src="/js/rsvp-tracker.js"></script>
|
||||
<script>
|
||||
document.addEventListener("DOMContentLoaded", function() {
|
||||
// Initialize RSVP tracker for the August 21 afterwork
|
||||
initRSVPTracker('2026-02-04-aw-mariatorget');
|
||||
});
|
||||
</script>
|
||||
|
||||
23
content/sv/blog/meetups/_index.md
Normal file
23
content/sv/blog/meetups/_index.md
Normal file
@@ -0,0 +1,23 @@
|
||||
---
|
||||
title: Kommande Meetups
|
||||
layout: meetups_list
|
||||
weight: 60
|
||||
---
|
||||
|
||||
# Kommande meetups
|
||||
__Just nu finns det inga planerade meetups.__
|
||||
|
||||
|
||||
### Arrangera ett meetup
|
||||
STHLM-MESH är ett community – vem som helst kan arrangera en träff. Så här gör du:
|
||||
|
||||
1. Skapa en ny fil i `content/sv/blog/meetups/` (namnge gärna med datum i filnamnet).
|
||||
2. Utgå från mallen i `content/sv/blog/meetups/template.md` och fyll i plats, datum, tid och en kort beskrivning.
|
||||
3. Vill du använda vår RSVP-komponent, skapa även en json-fil under `static/events` och utgå från filen `template.json`
|
||||
4. Skicka en PR på GitHub
|
||||
|
||||
Alternativt kan du skriva till oss på Discord så hjälper vi dig lägga upp ditt event.
|
||||
|
||||
|
||||
### Tidigare meetups
|
||||
Kolla in vad vi gjort tidigare: [/blog/past-meetups/](/blog/past-meetups/)
|
||||
34
content/sv/blog/meetups/template.md
Normal file
34
content/sv/blog/meetups/template.md
Normal file
@@ -0,0 +1,34 @@
|
||||
---
|
||||
title: Meetup 19 Augusti - Telefonplan
|
||||
date: 2025-08-24
|
||||
layout: meetup_single
|
||||
build:
|
||||
render: never
|
||||
list: false
|
||||
publishResources: false
|
||||
---
|
||||
Nu är det dags för en Meshtastic AW i Stockholm igen! Denna gång hoppas vi på bra väder och träffas på baren i Svandammsparken. Kom och träffa likasinnade, snacka LoRa och bygg ut nätverket i Stockholm!
|
||||
|
||||
Ta gärna med din nod, eller visa upp det senaste bygget.
|
||||
|
||||
|
||||
__📍 Plats:__[Midsommarköket, Svandammsparken (T) Midsommarkransen](https://maps.app.goo.gl/n1XSUWvoUF7yNbzb6)
|
||||
__📅 Datum:__ Tisdag 19 augusti
|
||||
__⏰ Tid:__ 17:00 (baren öppnar 15:00)
|
||||
|
||||
Om du inte kan komma exakt 17:00 är det helt okej att dyka upp senare. Skriv gärna ett meddelande på meshen eller Discord om du kommer!
|
||||
|
||||
|
||||
<!-- RSVP Tracker Container -->
|
||||
<div id="rsvp-tracker-2025-08-19-aw-telefonplan" class="mt-4"></div>
|
||||
|
||||
|
||||
<script src="/js/status/shared.js"></script>
|
||||
<script src="/js/rsvp-tracker.js"></script>
|
||||
<script>
|
||||
document.addEventListener("DOMContentLoaded", function() {
|
||||
// Initialize RSVP tracker for the August 21 afterwork
|
||||
initRSVPTracker('2025-08-19-aw-telefonplan');
|
||||
});
|
||||
</script>
|
||||
|
||||
21
content/sv/blog/past-meetups/2024-05-05-Meetup.md
Normal file
21
content/sv/blog/past-meetups/2024-05-05-Meetup.md
Normal file
@@ -0,0 +1,21 @@
|
||||
---
|
||||
title: Meetup 15 Maj - Urban Deli
|
||||
date: 2024-05-04
|
||||
layout: meetup_single
|
||||
---
|
||||
<img src="https://scontent.fbma6-1.fna.fbcdn.net/v/t39.30808-6/517513963_10165535191003368_2344483791759405240_n.jpg?_nc_cat=108&ccb=1-7&_nc_sid=aa7b47&_nc_ohc=1IQDCYFVitsQ7kNvwFCBidv&_nc_oc=Adm11Q6GHS1gZnsrLV7dVr7LAiRaAch5ST5ibHdJp9rSAqM2tvFt_jjmVHregLuIwT4&_nc_zt=23&_nc_ht=scontent.fbma6-1.fna&_nc_gid=6MV6i5qkzcGiTihEtIBCxA&oh=00_AfVKeRaH00x7qjwO2l0Wo68kgBgRJG252rXJU3vz9VBOhw&oe=68B0F073" alt="Meetup photo" style="width: 50%; height: auto;">
|
||||
|
||||
|
||||
Noden med namn BLÅ startade intresset men försvann tyvärr så vi plockar upp fanan.
|
||||
|
||||
AW Stockholm blir den 15/5 från 17 och frammåt på Takpark by Urban Deli, Sveavägen 44.
|
||||
Det ska vara upp till 22 grader på dagen och kvällen blir förhoppningsfullt vacker likväl.
|
||||
|
||||
Alla är välkomna, just nu har vi fått in ca 10 intresserade.
|
||||
Är du intresserad att träffa andra trevliga meshtastic folk över en öl eller en bit mat hojta till.
|
||||
|
||||
|
||||
__📍 Plats:__ Takpark by Urban Deli, Sveavägen 44
|
||||
__📅 Datum:__ 2024-05-15
|
||||
__⏰ Tid:__ 17:00
|
||||
|
||||
19
content/sv/blog/past-meetups/2024-08-10-Meetup.md
Normal file
19
content/sv/blog/past-meetups/2024-08-10-Meetup.md
Normal file
@@ -0,0 +1,19 @@
|
||||
---
|
||||
title: Meetup 4 September - Sundbyberg
|
||||
date: 2024-08-10
|
||||
layout: meetup_single
|
||||
---
|
||||
|
||||
<img src="https://scontent.fbma6-1.fna.fbcdn.net/v/t39.30808-6/516826012_10162373418114303_5606218656961284323_n.jpg?_nc_cat=110&ccb=1-7&_nc_sid=75d36f&_nc_ohc=UZMnChz73lkQ7kNvwGf2OoL&_nc_oc=AdmsQS-Jmkt7zuhPWCQdQ-HZ1I6J1i8uPxd1GaOcU8m2r6YpQ3zmzZvw1X0pHdmL7lg&_nc_zt=23&_nc_ht=scontent.fbma6-1.fna&_nc_gid=uKFo5CFj1vS86BoYRvlv3g&oh=00_AfXqSc1J5XKsW1FIe-T_wnxoLkTJtsLJzkOQd-zJ2E1DIg&oe=68B0E106" alt="Meetup photo" style="width: 50%; height: auto;">
|
||||
|
||||
Nu är det dags för en Meshtastic AW i Stockholm igen! Kom och träffa likasinnade, snacka LoRa och bygg ut nätverket i Stockholm!
|
||||
|
||||
Ta gärna med din nod, eller visa upp det senaste bygget.
|
||||
|
||||
|
||||
__📍 Plats:__ The Bishops Arms Sundbyberg, Stockholm
|
||||
__📅 Datum:__ 2024-09-04
|
||||
__⏰ Tid:__ 17:00
|
||||
|
||||
|
||||
<a href="https://www.facebook.com/events/1183504712869737/" class="btn btn-primary btn-lg" target="_blank" style="background-color: #1877f2; border-color: #1877f2;"><i class="fab fa-facebook"></i> Facebook Event</a>
|
||||
18
content/sv/blog/past-meetups/2025-04-08-Meetup.md
Normal file
18
content/sv/blog/past-meetups/2025-04-08-Meetup.md
Normal file
@@ -0,0 +1,18 @@
|
||||
---
|
||||
title: Meetup 8 April - Sundbyberg
|
||||
date: 2025-04-08
|
||||
layout: meetup_single
|
||||
---
|
||||
<img src="https://scontent.fbma6-1.fna.fbcdn.net/v/t39.30808-6/482214110_10161861083094303_3609145510463720959_n.jpg?stp=c0.29.1792.936a_dst-jpg_s1080x2048_tt6&_nc_cat=102&ccb=1-7&_nc_sid=75d36f&_nc_ohc=nwlZDBEmVkYQ7kNvwG6eOeV&_nc_oc=Adlpp_-FwSnb3FYSInWyBtG7y-D1uMiU2XYai5iP-_j8hC-GGXzYCKInUKbFeS3V1Wc&_nc_zt=23&_nc_ht=scontent.fbma6-1.fna&_nc_gid=OBFGqeDXTxJnWB3MYMqYMA&oh=00_AfWfl2qFhiHZeuv5PehFTndxNAY8BpRW99RgInjs6dS3EA&oe=68B0EC8A" alt="Meetup photo" style="width: 50%; height: auto;">
|
||||
|
||||
Våren närmar sig och det är massvis med trafik i meshen. Det har dessutom tillkommit massvis med nya noder och personer. Vi bjuder därför in till After Work för de som vill träffa likasinnade, snacka LoRa, dela erfarenheter och visa hemmabyggen.
|
||||
|
||||
Ta gärna med din nod, eller visa upp det senaste bygget.
|
||||
|
||||
|
||||
|
||||
__📍 Plats:__ The Bishops Arms Sundbyberg, Stockholm
|
||||
__📅 Datum:__ 2025-04-08
|
||||
__⏰ Tid:__ 17:00
|
||||
|
||||
<a href="https://www.facebook.com/events/2766664646866905/" class="btn btn-primary btn-lg" target="_blank" style="background-color: #1877f2; border-color: #1877f2;"><i class="fab fa-facebook"></i> Facebook Event</a>
|
||||
33
content/sv/blog/past-meetups/2025-08-14-Meetup.md
Normal file
33
content/sv/blog/past-meetups/2025-08-14-Meetup.md
Normal file
@@ -0,0 +1,33 @@
|
||||
---
|
||||
title: Meetup 19 Augusti - Telefonplan
|
||||
date: 2025-08-14
|
||||
layout: meetup_single
|
||||
---
|
||||
Nu är det dags för en Meshtastic AW i Stockholm igen! Denna gång hoppas vi på bra väder och träffas på baren i Svandammsparken. Midsommarköket som baren heter öppnar redan 15:00 om någon vill komma tidigare. Kom och träffa likasinnade, snacka LoRa och bygg ut nätverket i Stockholm!
|
||||
|
||||
Ta gärna med din nod, eller visa upp det senaste bygget.
|
||||
|
||||
|
||||
|
||||
__📍 Plats:__[Midsommarköket, Svandammsparken (T) Midsommarkransen](https://maps.app.goo.gl/n1XSUWvoUF7yNbzb6)
|
||||
__📅 Datum:__ Tisdag 19 augusti
|
||||
__⏰ Tid:__ 17:00 (baren öppnar 15:00)
|
||||
|
||||
Om du inte kan komma exakt 17:00 är det helt okej att dyka upp senare. Skriv gärna ett meddelande på meshen eller Discord om du kommer!
|
||||
|
||||
## Anmälan
|
||||
Anmälan sker över LoRa. Skicka ett meddelande över meshen med texten `AW 19/8 - Kommer` så vias din nod i listan nedan. Kan du inte komma eller är osäker kan du skicka `AW 19/8 - Kanske/Kommer inte`
|
||||
|
||||
<!-- RSVP Tracker Container -->
|
||||
<div id="rsvp-tracker-2025-08-19-aw-telefonplan" class="mt-4"></div>
|
||||
|
||||
|
||||
<script src="/js/status/shared.js"></script>
|
||||
<script src="/js/rsvp-tracker.js"></script>
|
||||
<script>
|
||||
document.addEventListener("DOMContentLoaded", function() {
|
||||
// Initialize RSVP tracker for the August 21 afterwork
|
||||
initRSVPTracker('2025-08-19-aw-telefonplan');
|
||||
});
|
||||
</script>
|
||||
|
||||
147
content/sv/blog/past-meetups/2025-09-10-sec-t.md
Normal file
147
content/sv/blog/past-meetups/2025-09-10-sec-t.md
Normal file
@@ -0,0 +1,147 @@
|
||||
---
|
||||
title: Meshtastic på SEC-T
|
||||
date: 2025-09-10
|
||||
layout: meetup_single
|
||||
---
|
||||
SEC-T är en av Europas största säkerhetskonferenser som äger rum i Stockholm varje år. Konferensen samlar cybersäkerhetsexperter, forskare och entusiaster från hela världen för att dela kunskap och diskutera de senaste trenderna inom informationssäkerhet.
|
||||
|
||||
I år kommer de anordna workshops kring Meshtastic! Det blir både nybörjarworkshops där du får bygga din första Meshtastic-nod, och mer avancerade workshops för de som vill gå djupare in i tekniken. Ett perfekt tillfälle att lära sig mer om decentraliserad kommunikation och LoRa-nätverk.
|
||||
|
||||
Under SEC-T kommer presetet __Short Fast__ användas. Detta för att meshet ska fungera smidigt med flera aktiva noder på samma plats.
|
||||
|
||||
## Workshops
|
||||
### Onsdag
|
||||
|
||||
2025-09-10 12:45–14:30 - [Workshop: Build Your Own Meshtastic Node: Off-Grid, Encrypted LoRa Meshnets for Beginners!](https://event.sec-t.org/sec-t-2025/talk/J998RJ/)
|
||||
|
||||
### Torsdag
|
||||
2025-09-11 13:00–14:45 - [Workshop: Build Your Own Meshtastic Node: Off-Grid, Encrypted LoRa Meshnets for Beginners!](https://event.sec-t.org/sec-t-2025/talk/L98W77/)
|
||||
|
||||
|
||||
2025-09-11 16:45–18:30 - [Workshop: Meshtastic for Hackers: Set up, Configure, & Deploy Nodes for Advanced Use](https://event.sec-t.org/sec-t-2025/talk/CJZCLL/)
|
||||
|
||||
### Fredag
|
||||
2025-09-12 12:45–14:30 - [Workshop: Build Your Own Meshtastic Node: Off-Grid, Encrypted LoRa Meshnets for Beginners!](https://event.sec-t.org/sec-t-2025/talk/DFCKUX/)
|
||||
|
||||
2025-09-12 14:45–16:30 - [Workshop: Meshtastic for Hackers: Set up, Configure, & Deploy Nodes for Advanced Use](https://event.sec-t.org/sec-t-2025/talk/Z7HXLK/)
|
||||
|
||||
|
||||
## SEC-T Firmware
|
||||
Nedan går det att ladda ner eller flasha firmware med inställningar för SEC-T. Vi har firmware för den enhet som man bygger under workshopen. Har du enhet sen tidigare så kör vi __ShortFast__ som preset under SEC-T.
|
||||
|
||||
### Firmware
|
||||
|
||||
<ul class="list-group">
|
||||
<li class="list-group-item d-flex justify-content-between align-items-center">
|
||||
<span><strong>Nibble ESP32 - 2.7.7.sec-t</strong></span>
|
||||
<span>
|
||||
<button class="btn btn-sm btn-outline-primary open-modal-btn"
|
||||
data-board="nibble-esp32" data-version="2.7.7.sec-t">Flash Device
|
||||
</button>
|
||||
</span>
|
||||
</li>
|
||||
<li class="list-group-item d-flex justify-content-between align-items-center">
|
||||
<span><strong>RAK4631 - 2.7.7.sec-t</strong></span>
|
||||
<span>
|
||||
<a class="btn btn-sm btn-outline-secondary me-2 disabled"
|
||||
href="#" aria-disabled="true">Download Firmware</a>
|
||||
</span>
|
||||
</li>
|
||||
<li class="list-group-item d-flex justify-content-between align-items-center">
|
||||
<span><strong>Heltec V3 - 2.7.7.sec-t</strong></span>
|
||||
<span>
|
||||
<button class="btn btn-sm btn-outline-primary disabled" disabled
|
||||
data-board="heltec-v3" data-version="2.7.7.sec-t">Flash Device
|
||||
</button>
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<div class="mt-3">
|
||||
<small class="text-muted">
|
||||
<strong>Note:</strong> This firmware is specifically configured for the SEC-T event with optimized settings for the conference environment. Currently only Nibble ESP32 firmware is available.
|
||||
</small>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- Flash‑log modal -->
|
||||
<div class="modal fade" id="flashModal" tabindex="-1" aria-labelledby="flashModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog modal-lg modal-dialog-scrollable">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="flashModalLabel">Flashing</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<pre id="espLog" class="bg-dark text-light p-2 rounded overflow-auto" style="height:16rem;font-size:.85rem"></pre>
|
||||
<small class="text-muted">Requires Chrome/Edge ≥ 89 over HTTPS.</small>
|
||||
</div>
|
||||
<div class="modal-footer d-flex align-items-center w-100">
|
||||
<div class="form-check form-switch mb-0">
|
||||
<input class="form-check-input" type="checkbox" id="eraseSwitch">
|
||||
<label class="form-check-label" for="eraseSwitch">
|
||||
Full Erase & Install
|
||||
</label>
|
||||
</div>
|
||||
<div class="ms-auto d-flex gap-2">
|
||||
<button id="startFlashBtn" class="btn btn-primary">
|
||||
Start Flash
|
||||
</button>
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">
|
||||
Close
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<script src="/js/esp-flasher.js"></script>
|
||||
<script>
|
||||
// Custom modal logic for SEC-T firmware flasher
|
||||
let selectedBoard = {};
|
||||
let selectedVersion = '';
|
||||
|
||||
// Hardcoded board configuration for SEC-T
|
||||
const sectBoard = {
|
||||
"hwModelSlug": "nibble-esp32",
|
||||
"architecture": "esp32",
|
||||
"displayName": "Nibble ESP32 (SEC-T Workshop Device)",
|
||||
"partitionScheme": "4MB"
|
||||
};
|
||||
|
||||
// Modal logic
|
||||
document.addEventListener('click', ev => {
|
||||
if (!ev.target.matches('.open-modal-btn')) return;
|
||||
|
||||
const modalEl = document.getElementById('flashModal');
|
||||
const flashModal = new bootstrap.Modal(modalEl);
|
||||
const titleEl = document.getElementById('flashModalLabel');
|
||||
const eraseChk = document.getElementById('eraseSwitch');
|
||||
const logBox = document.getElementById('espLog');
|
||||
|
||||
selectedVersion = ev.target.dataset.version;
|
||||
selectedBoard = sectBoard;
|
||||
|
||||
titleEl.textContent = `Flash – ${selectedBoard.displayName} ${selectedVersion}`;
|
||||
eraseChk.checked = false;
|
||||
logBox.textContent = '';
|
||||
|
||||
flashModal.show();
|
||||
});
|
||||
|
||||
// Start Flash Button
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const startBtn = document.getElementById('startFlashBtn');
|
||||
if (startBtn) {
|
||||
startBtn.addEventListener('click', async () => {
|
||||
const fullEraseInstall = document.getElementById('eraseSwitch').checked;
|
||||
|
||||
startBtn.disabled = true;
|
||||
await flashFirmware(selectedBoard, selectedVersion, fullEraseInstall);
|
||||
startBtn.disabled = false;
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
35
content/sv/blog/past-meetups/2025-10-01-Meetup.md
Normal file
35
content/sv/blog/past-meetups/2025-10-01-Meetup.md
Normal file
@@ -0,0 +1,35 @@
|
||||
---
|
||||
title: Meetup 1 Oktober - Sundbyberg
|
||||
date: 2025-09-21
|
||||
layout: meetup_single
|
||||
---
|
||||
Nu är det dags för en Meshtastic AW i Stockholm igen! Vi träffas för att uvärdera övergången till MediumFast. Aningen firar vi eller så beslutar vi att gå tillbaka till LongFast.
|
||||
|
||||
Har du missat att vi går över till MediumFast så kan du [läsa mer här](https://sthlm-mesh.se/blog/2025/övergång-till-mediumfast-den-27-september/).
|
||||
|
||||
|
||||
|
||||
__📍 Plats:__ The Bishops Arms Sundbyberg, Stockholm
|
||||
__📅 Datum:__ 2025-10-01
|
||||
__⏰ Tid:__ 17:00
|
||||
|
||||
Om du inte kan komma exakt 17:00 är det helt okej att dyka upp senare. Skriv gärna ett meddelande på meshen eller Discord om du kommer!
|
||||
|
||||
## 3D Printing
|
||||
__Har du en nod utan skal__, eller har du hittat en 3d modell för en solnod, men har inte tillgång till en 3D Printer? __Hör av dig på discord!__ Vi är många som har 3D printer som kan hjälpa dig skriva ut det du behöver. Vi gör detta till självkostnadspris, en symbolsisk summa, __en öl__ eller kanske till och med helt gratis. Tillsammans bygger vi ett bättre mesh! 💚
|
||||
|
||||
## Anmälan
|
||||
Anmälan sker över LoRa. Skicka ett meddelande över meshen med texten `AW 1/10 - Kommer` så vias din nod i listan nedan. Kan du inte komma eller är osäker kan du skicka `AW 1/10 - Kanske/Kommer inte`
|
||||
|
||||
<!-- RSVP Tracker Container -->
|
||||
<div id="rsvp-tracker-2025-10-01-sundbyberg" class="mt-4"></div>
|
||||
|
||||
|
||||
<script src="/js/status/shared.js"></script>
|
||||
<script src="/js/rsvp-tracker.js"></script>
|
||||
<script>
|
||||
document.addEventListener("DOMContentLoaded", function() {
|
||||
initRSVPTracker('2025-10-01-sundbyberg');
|
||||
});
|
||||
</script>
|
||||
|
||||
10
content/sv/blog/past-meetups/2025-11-12-SK0MM.md
Normal file
10
content/sv/blog/past-meetups/2025-11-12-SK0MM.md
Normal file
@@ -0,0 +1,10 @@
|
||||
---
|
||||
title: Meetup 12 November - LoRa Mesh möte på Värmdö
|
||||
date: 2025-11-01
|
||||
layout: meetup_single
|
||||
---
|
||||
Stockholms Skärgårds Sändareamatörer anordnar LoRa Mesh möte den 12 november från kl 18:00 i Mölnvik Brandstation, Värmdö (SK0MM:s lokal) med drop in. Det vore kul om du som är intresserad av Meshtastic o MeshCore o annat LoRa Mesh relaterat kommer då.
|
||||
|
||||
Jag tänker att vi utbyter erfarenheter om bygge av enheter samt relaterade mjukvarulösningar, konfigurering och frekvenser/inställningar.
|
||||
|
||||
Mvh SM0JLZ-Tomas ordf. Stockholms Skärgårds Sändareamatörer.
|
||||
36
content/sv/blog/past-meetups/2025-11-26-Meetup.md
Normal file
36
content/sv/blog/past-meetups/2025-11-26-Meetup.md
Normal file
@@ -0,0 +1,36 @@
|
||||
---
|
||||
title: Meetup 26 November - The Bishops Arms - Slussen
|
||||
date: 2025-11-18
|
||||
layout: meetup_single
|
||||
---
|
||||
Meetup för Meshtastic-communityt i Stockholm. Kom och visa din senaste nod eller bygge. Byt tips och erfarenheter med trevligt sällskap. Alla välkomna, nybörjare som erfarna meshare!
|
||||
|
||||
Ta gärna med din nod, eller visa upp det senaste bygget.
|
||||
|
||||
{{< alert title="OBS! Platsen har ändrats" color="warning" >}}
|
||||
Meetupet hålls på The Bishops Arms, Bellmansgatan (Slussen)!
|
||||
{{< /alert >}}
|
||||
|
||||
|
||||
|
||||
__📍 Plats:__[The Bishops Arms, Bellmansgatan](https://maps.app.goo.gl/iwhy8nLKVA6Ewr8p6)
|
||||
__📅 Datum:__ Onsdag 26 november
|
||||
__⏰ Tid:__ 17:00
|
||||
|
||||
Om du inte kan komma exakt 17:00 är det helt okej att dyka upp senare. Skriv gärna ett meddelande på meshen eller Discord om du kommer!
|
||||
|
||||
## Anmälan
|
||||
Anmälan sker över LoRa. Skicka ett meddelande över meshen med texten `AW 26/11 - Kommer` så vias din nod i listan nedan. Kan du inte komma eller är osäker kan du skicka `AW 26/11 - Kanske/Kommer inte`
|
||||
|
||||
<!-- RSVP Tracker Container -->
|
||||
<div id="rsvp-tracker-2025-11-26-aw-gamlastan" class="mt-4"></div>
|
||||
|
||||
|
||||
<script src="/js/status/shared.js"></script>
|
||||
<script src="/js/rsvp-tracker.js"></script>
|
||||
<script>
|
||||
document.addEventListener("DOMContentLoaded", function() {
|
||||
// Initialize RSVP tracker for the November 26 afterwork
|
||||
initRSVPTracker('2025-11-26-aw-gamlastan');
|
||||
});
|
||||
</script>
|
||||
6
content/sv/blog/past-meetups/_index.md
Normal file
6
content/sv/blog/past-meetups/_index.md
Normal file
@@ -0,0 +1,6 @@
|
||||
---
|
||||
title: Tidigare Meetups
|
||||
description: nej
|
||||
layout: meetups_list
|
||||
weight: 70
|
||||
---
|
||||
@@ -17,3 +17,5 @@ weight: 120
|
||||
## Webbsidor
|
||||
- [meshat.se](https://meshat.se)
|
||||
- [sk0qo.se](https://www.sk0qo.se/index.php/3698-ny-meshtastic-router-pa-plats-i-brandbergen)
|
||||
- [jkpg-mesh](https://jkpg-mesh.se)
|
||||
- [mesh-roslagen](https://mesh-roslagen.se)
|
||||
|
||||
@@ -6,25 +6,54 @@ _build:
|
||||
list: false
|
||||
publishResources: false
|
||||
---
|
||||
Vi har modifierat den officiella Meshtastic-firmwaren för att bättre möta våra behov och användningssätt. Endast vältestade versioner publiceras här.
|
||||
|
||||
|
||||
## Binärer
|
||||
{{% alert title="Varning" color="danger" %}}
|
||||
Att ladda ner firmware från internet medför alltid en risk. Istället bör du själv genomföra kodändringarna nedan och kompilera firmware själv.
|
||||
Att ladda ner och använda firmware från internet innebär alltid en viss risk. För högsta säkerhet rekommenderar vi att du själv gör de nödvändiga kodändringarna (som beskrivs längre ner på sidan) och bygger firmwaren lokalt.
|
||||
{{% /alert %}}
|
||||
* [ros-firmware-rak4631-2.6.4.b89355f.uf2](/firmware/ros-firmware-rak4631-2.6.4.b89355f.uf2)
|
||||
* [ros-firmware-rak4631-ota-2.6.4.b89355f.zip](/firmware/ros-firmware-rak4631-ota-2.6.4.b89355f.zip)
|
||||
* [ros-firmware-rak4631-2.5.21.447533a.uf2](/firmware/ros-firmware-rak4631-2.5.21.447533a.uf2)
|
||||
* [nrf_erase2.uf2](/firmware/nrf_erase2.uf2)
|
||||
|
||||
I dagsläget stöds följande hårdvaruplattformar: RAK4631, Heltec V3 och LILYGO T-LoRa T3-S3.
|
||||
För ESP32-baserade enheter erbjuder vi en egen webbaserad flasher – observera att denna fortfarande är under testning och kan innehålla buggar.
|
||||
|
||||
<!-- Accordion will be injected here -->
|
||||
<div class="accordion" id="firmwareAccordion"></div>
|
||||
|
||||
|
||||
<!-- Flash‑log modal -->
|
||||
<div class="modal fade" id="flashModal" tabindex="-1" aria-labelledby="flashModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog modal-lg modal-dialog-scrollable">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="flashModalLabel">Flashing</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<pre id="espLog" class="bg-dark text-light p-2 rounded overflow-auto" style="height:16rem;font-size:.85rem"></pre>
|
||||
<small class="text-muted">Requires Chrome/Edge ≥ 89 over HTTPS.</small>
|
||||
</div>
|
||||
<div class="modal-footer d-flex align-items-center w-100">
|
||||
<div class="form-check form-switch mb-0">
|
||||
<input class="form-check-input" type="checkbox" id="eraseSwitch">
|
||||
<label class="form-check-label" for="eraseSwitch">
|
||||
Full Erase & Install
|
||||
</label>
|
||||
</div>
|
||||
<div class="ms-auto d-flex gap-2">
|
||||
<button id="startFlashBtn" class="btn btn-primary">
|
||||
Start Flash
|
||||
</button>
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">
|
||||
Close
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
## Kodändringar
|
||||
Nedan är de ändringar jag gör på firmwaren för att det ska passa mig.
|
||||
Nedan är de ändringars som gjort i den senaste firmwaren. Äldre versioner kan ha andra ändringar.
|
||||
|
||||
### Channels.cpp
|
||||
```diff
|
||||
- channelSettings.module_settings.position_precision = 13; // default to sending location on the primary channel
|
||||
+ channelSettings.module_settings.position_precision = 32; // default to sending location on the primary channel
|
||||
```
|
||||
|
||||
### NeighborInfoModule.cpp
|
||||
Tillåt sändning av Neighbor info över default kanalen.
|
||||
@@ -64,6 +93,12 @@ void NeighborInfoModule::cleanUpNeighbors()
|
||||
}
|
||||
```
|
||||
|
||||
### Channels.cpp
|
||||
```diff
|
||||
- channelSettings.module_settings.position_precision = 13; // default to sending location on the primary channel
|
||||
+ channelSettings.module_settings.position_precision = 32; // default to sending location on the primary channel
|
||||
```
|
||||
|
||||
### MQTT.h
|
||||
```diff
|
||||
- const uint32_t default_map_position_precision = 14; // defaults to max. offset of ~1459m
|
||||
@@ -83,4 +118,7 @@ Respektera __inte__ OK_TO_MQTT flaggan
|
||||
- LOG_INFO("MQTT onSend - Not forwarding packet due to DontMqttMeBro flag");
|
||||
- return;
|
||||
- }
|
||||
```
|
||||
```
|
||||
|
||||
<script src="/js/firmware-ui.js"></script>
|
||||
<script src="/js/esp-flasher.js"></script>
|
||||
@@ -1,111 +0,0 @@
|
||||
---
|
||||
title: Hårdvara
|
||||
weight: 100
|
||||
---
|
||||
Bortsett från skruvar och buntband som kan behövas vid byggen så finns ett gäng nyckelkomponenter som behövs vid byggen av en Meshtastic-nod.
|
||||
|
||||
Vi går igenom några av de nedan och vilka som passar bäst för olika områden när man bygger / köper en färdig nod.
|
||||
|
||||
{{% alert title="Observera" color="warning" %}}
|
||||
Du behöver i princip enbart läsa om antenner / mikrokontrollrar om du ämnar att köpa en färdig nod.
|
||||
{{% /alert %}}
|
||||
|
||||
## Mikrokontroller / Styrenhet:
|
||||
|
||||
Mikrokontrollern är hjärtat i verket och du kommer inte särskilt långt utan en.
|
||||
|
||||
De mest vanliga brädorna kommer idag med inbyggda sändtagare och oftast följer även en enklare antenn med de.
|
||||
|
||||
Det är enklast att koka ner de vanligaste brädorna till de med två sorters processorer, ESP32-baserade och nRF52-baserade.
|
||||
|
||||
### ESP32-baserade (Heltec, T-Beam, T-Deck, Station G1/G2 med flera):
|
||||
- Kraftfull processor som oftast har två kärnor.
|
||||
- Hög strömförbrukning.
|
||||
- Inbyggt WiFi och Bluetooth.
|
||||
- De flesta enheterna har en liten skärm inbyggd.
|
||||
|
||||
### NRF52-baserade (RAK, T-Echo, Seeedstudio Xiao, med flera)
|
||||
- Extremt låg strömförbrukning.
|
||||
- Inte särskilt kraftfull processor
|
||||
- Oftast enbart Bluetooth inbyggt.
|
||||
- Färdigbyggda enheter kommer enbart med skärm.
|
||||
|
||||
__Förenkling, om du ska bygga__:
|
||||
- Solnod, välj: RAK Wisblock Meshtastic Starter kit
|
||||
- Portabelnod, välj: Någon RAK-baserad nod
|
||||
- Balkongnod, välj: Någon ESP32-baserad nod
|
||||
- Tracker för bil: T-Beam med GPS, RAK med GPS för optimal batteritid.
|
||||
|
||||
## Sändtagarkretsar
|
||||
När du väljer en mikrokontroller för de mest vanligt förekommande syftena så är detta inget du behöver tänka på.
|
||||
Däremot; Ifall du exempelvis skall installera en nod väldigt nära en mobilmast eller annan störningskälla så är det sannolikt mer viktigt.
|
||||
|
||||
Det finns som utgångsläge två modeller av Sändtagare, SX1276 och SX1262.
|
||||
SX1262 är den kretsen som i dagsläget är mest förekommande, den är mer strömsnål samt har högre uteffekt, denna förekommer i nästan alla hårdvaror.
|
||||
SX1276 är dock av högre kvalitet, men med något sämre känslighet och uteffekt.
|
||||
|
||||
SX1276 har dock ett ess i rockärmen, och detta är dess utmärkta blockeringsförmåga. När en stark sändare på en närliggande frekvens (mobilmast) sänder så inför detta störningar i kretsen.
|
||||
Just specifikt SX1276 har bättre värden i denna kategorin och klarar därmed att fungera bättre i sådana miljöer.
|
||||
|
||||
## Batteri:
|
||||
{{% alert title="Varning" color="danger" %}}
|
||||
Hantera LiPo och Li-Ion-celler med försiktighet! De är högst reaktiva och känsliga för punktering / kortslutning.
|
||||
{{% /alert %}}
|
||||
Precis som med en mikrokontroller så kommer man inte särskilt långt utan ström från ett batteri, såvida det inte är en fast inkopplad nod.
|
||||
|
||||
Det finns en hel uppsjö med alternativ att välja från, men för de flesta rekommenderas att man börjar med Litiumjon-celler (Li-Ion) eller Litiumpolymer (LiPo).
|
||||
|
||||
- Litiumpolymer (LiPo):
|
||||
En vanligt förekommande batterityp. Utformningen är nästan alltid en platt påse av metall som är väldigt lättpunkterad, så hanteras försiktigt!
|
||||
De har en dräglig energidensitet, men har primärt hög urladdningsström och kommer i olika behändiga former som passar väl i handhållna enheter.
|
||||
|
||||
- Litiumjon (Li-Ion):
|
||||
Likväl som LiPo en vanligt förekommande batterityp, primärt i elfordon, ficklampor och äldre laptopbatterier. Kommer i princip alltid som cylindriska celler (18650, 21700) som är lite mer lätthanterliga.
|
||||
De har en högre energidensitet än LiPo, men passar inte i alla enheter lika bra.
|
||||
|
||||
- Nickelmetallhydrid (NiMH):
|
||||
Väldigt vanligt förekommande batterityp som används i hem världen över. De är oftast laddningsbara men nackdelen är att enskilda celler nästan aldrig är kompatibla med moderna elektroniska enheters spänningskrav.
|
||||
De är väldigt säkra däremot, och det finns syfte för de (Primärt för vinterdrift) som gör att de hamnar här.
|
||||
|
||||
- Litiumtitanat (LTO):
|
||||
Väldigt dyr celltyp enbart för de som ämnar att bygga de mest extrema noderna. Kan laddas i -40C, däremot kräver de speciella laddningskretsar, samt har lägre energidensitet.
|
||||
Dessa används militärt för bland annat arktiska syften.
|
||||
|
||||
## Solceller
|
||||
När det kommer till solcellsnoder så finns det en uppsjö med alternativ, men här rundar vi ner till de vanligaste typerna och mest förekommande spänningarna.
|
||||
|
||||
### Monokristallina Solceller:
|
||||
Det överlägset bästa alternativet för solcellsladdning
|
||||
- Bättre laddning vid lågt ljus.
|
||||
- Längre livslängder.
|
||||
- Högre uteffekt per area.
|
||||
|
||||
### Polykristallina solceller:
|
||||
Billigare paneler än Monokristallina men fungerar ändå väl
|
||||
- Sämre uteffekt
|
||||
- Aningen lägre livslängd
|
||||
- Lägre uteffekt per area.
|
||||
|
||||
### Spänningsnivåer på Solceller
|
||||
För de flesta mikrokontrollrarna med inbyggd laddning så är det solceller på 5/6-volt som du skall leta efter.
|
||||
På detta sätt behöver du färre ytterligare komponenter och de kostar även mindre.
|
||||
|
||||
För dig som tittar på laddning med 12V-paneler eller högre spänningar så finns det givetvis vinster med det såsom högre uteffekter (och därmed snabbare laddning med rätt kretsar). Däremot är kostnaden högre, inte bara på panelerna men på step-up/down-kretsar och effektiv spänningsomvandling.
|
||||
|
||||
## Laddningskretsar
|
||||
För dig som tittar på att ladda dina noder effektivt så rekommenderar vi dig starkt att använda en extern laddningskrets för dina batterier.
|
||||
Oftast är mikrokontrollrarnas inbyggdna laddningskretsar extremt begränsade (50-100mA per timme), något som kan sätta spiken i kistan för en annars välfungerande solnod.
|
||||
|
||||
### TP4056:
|
||||
En alldeles utmärkt laddningskrets för både USB-laddning och Solcellsladdning. [PDF](https://www.digikey.in/htmldatasheets/production/2049110/0/0/1/TP4056.pdf)
|
||||
|
||||
* Inbyggd laddningskurva för Litiumbatterier, och trappar ner laddströmmen för att skydda cellerna rätt.
|
||||
* Laddström (0.5A - 1A)
|
||||
|
||||
### VoltaicEnclosures - [MPPT Solar Battery Charger](https://www.etsy.com/se-en/listing/1609406536/mppt-solar-battery-charger-for-iot)
|
||||
Designad för Meshtastic, perfekt för seriösa solcellsnoder. Men kanske lite väl dyr...
|
||||
|
||||
* Hanterar de flesta batterikemier (LTO, NA+, LifePo4, Li-ion, ...)
|
||||
* Laddström: 1A
|
||||
* Inbyggd INA3221 I2C sensor för att övervaka strömmen från solcellen, batteriet och den externa enheten.
|
||||
|
||||
50
content/sv/docs/hardware/_index.md
Normal file
50
content/sv/docs/hardware/_index.md
Normal file
@@ -0,0 +1,50 @@
|
||||
---
|
||||
title: Hårdvara
|
||||
weight: 30
|
||||
---
|
||||
Bortsett från skruvar och buntband som kan behövas vid byggen så finns ett gäng nyckelkomponenter som behövs vid byggen av en Meshtastic-nod.
|
||||
|
||||
Vi går igenom några av de nedan och vilka som passar bäst för olika områden när man bygger / köper en färdig nod.
|
||||
|
||||
{{% alert title="Observera" color="warning" %}}
|
||||
Du behöver i princip enbart läsa om antenner / mikrokontrollrar om du ämnar att köpa en färdig nod.
|
||||
{{% /alert %}}
|
||||
|
||||
## Mikrokontroller / Styrenhet:
|
||||
|
||||
Mikrokontrollern är hjärtat i verket och du kommer inte särskilt långt utan en.
|
||||
|
||||
De mest vanliga brädorna kommer idag med inbyggda sändtagare och oftast följer även en enklare antenn med de.
|
||||
|
||||
Det är enklast att koka ner de vanligaste brädorna till de med två sorters processorer, ESP32-baserade och nRF52-baserade.
|
||||
|
||||
### ESP32-baserade (Heltec, T-Beam, T-Deck, Station G1/G2 med flera):
|
||||
- Kraftfull processor som oftast har två kärnor.
|
||||
- Hög strömförbrukning.
|
||||
- Inbyggt WiFi och Bluetooth.
|
||||
- De flesta enheterna har en liten skärm inbyggd.
|
||||
|
||||
### NRF52-baserade (RAK, T-Echo, Seeedstudio Xiao, med flera)
|
||||
- Extremt låg strömförbrukning.
|
||||
- Inte särskilt kraftfull processor
|
||||
- Oftast enbart Bluetooth inbyggt.
|
||||
- Färdigbyggda enheter kommer enbart med skärm.
|
||||
|
||||
__Förenkling, om du ska bygga__:
|
||||
- Solnod, välj: RAK Wisblock Meshtastic Starter kit
|
||||
- Portabelnod, välj: Någon RAK-baserad nod
|
||||
- Balkongnod, välj: Någon ESP32-baserad nod
|
||||
- Tracker för bil: T-Beam med GPS, RAK med GPS för optimal batteritid.
|
||||
|
||||
## Sändtagarkretsar
|
||||
När du väljer en mikrokontroller för de mest vanligt förekommande syftena så är detta inget du behöver tänka på.
|
||||
Däremot; Ifall du exempelvis skall installera en nod väldigt nära en mobilmast eller annan störningskälla så är det sannolikt mer viktigt.
|
||||
|
||||
Det finns som utgångsläge två modeller av Sändtagare, SX1276 och SX1262.
|
||||
SX1262 är den kretsen som i dagsläget är mest förekommande, den är mer strömsnål samt har högre uteffekt, denna förekommer i nästan alla hårdvaror.
|
||||
SX1276 är dock av högre kvalitet, men med något sämre känslighet och uteffekt.
|
||||
|
||||
SX1276 har dock ett ess i rockärmen, och detta är dess utmärkta blockeringsförmåga. När en stark sändare på en närliggande frekvens (mobilmast) sänder så inför detta störningar i kretsen.
|
||||
Just specifikt SX1276 har bättre värden i denna kategorin och klarar därmed att fungera bättre i sådana miljöer.
|
||||
|
||||
|
||||
7
content/sv/docs/hardware/antennas.md
Normal file
7
content/sv/docs/hardware/antennas.md
Normal file
@@ -0,0 +1,7 @@
|
||||
---
|
||||
title: Antenner
|
||||
weight: 20
|
||||
---
|
||||
|
||||
### Alfa Network AOA-868-5ACM
|
||||
https://alfa-network.eu/antennas/lora-antennas/aoa-868-5acm
|
||||
37
content/sv/docs/hardware/batterier.md
Normal file
37
content/sv/docs/hardware/batterier.md
Normal file
@@ -0,0 +1,37 @@
|
||||
---
|
||||
title: Batterier
|
||||
weight: 70
|
||||
---
|
||||
|
||||
{{% alert title="Varning" color="danger" %}}
|
||||
Hantera LiPo och Li-Ion-celler med försiktighet! De är högst reaktiva och känsliga för punktering / kortslutning.
|
||||
{{% /alert %}}
|
||||
Precis som med en mikrokontroller så kommer man inte särskilt långt utan ström från ett batteri, såvida det inte är en fast inkopplad nod.
|
||||
|
||||
Det finns en hel uppsjö med alternativ att välja från, men för de flesta rekommenderas att man börjar med Litiumjon-celler (Li-Ion) eller Litiumpolymer (LiPo).
|
||||
|
||||
- Litiumpolymer (LiPo):
|
||||
En vanligt förekommande batterityp. Utformningen är nästan alltid en platt påse av metall som är väldigt lättpunkterad, så hanteras försiktigt!
|
||||
De har en dräglig energidensitet, men har primärt hög urladdningsström och kommer i olika behändiga former som passar väl i handhållna enheter.
|
||||
|
||||
- Litiumjon (Li-Ion):
|
||||
Likväl som LiPo en vanligt förekommande batterityp, primärt i elfordon, ficklampor och äldre laptopbatterier. Kommer i princip alltid som cylindriska celler (18650, 21700) som är lite mer lätthanterliga.
|
||||
De har en högre energidensitet än LiPo, men passar inte i alla enheter lika bra.
|
||||
|
||||
- Nickelmetallhydrid (NiMH):
|
||||
Väldigt vanligt förekommande batterityp som används i hem världen över. De är oftast laddningsbara men nackdelen är att enskilda celler nästan aldrig är kompatibla med moderna elektroniska enheters spänningskrav.
|
||||
De är väldigt säkra däremot, och det finns syfte för de (Primärt för vinterdrift) som gör att de hamnar här.
|
||||
|
||||
- Litiumtitanat (LTO):
|
||||
Väldigt dyr celltyp enbart för de som ämnar att bygga de mest extrema noderna. Kan laddas i -40C, däremot kräver de speciella laddningskretsar, samt har lägre energidensitet.
|
||||
Dessa används militärt för bland annat arktiska syften.
|
||||
|
||||
|
||||
## Köpa batterier
|
||||
Det finns en hel del batterier på [Elektrokit](https://www.electrokit.com/search.php?keyword=batteri+jst-ph) som är en svensk återförsäljare. Dessa batterier passar direk på RAK Wireless noder.
|
||||
|
||||
{{% alert title="OBS!" color="danger" %}}
|
||||
Polareseringen är JST-PH kontakter är inte standardeserad. Verfifiera innan inkoppling. Om det skulle vara fel polaresering går det att själv byta ordningen på sladdarna in i kontakten, det är lite pilligt, men görbart.
|
||||
{{% /alert %}}
|
||||
|
||||
En annan svensk återförsäljare är [Batteridoktorn](https://batteridoktorn.se/) som dessutom har fysisk butik i Solna.
|
||||
22
content/sv/docs/hardware/devices.md
Normal file
22
content/sv/docs/hardware/devices.md
Normal file
@@ -0,0 +1,22 @@
|
||||
---
|
||||
title: Enheter
|
||||
weight: 10
|
||||
---
|
||||
Tips på lite enhter
|
||||
|
||||
## Portabla enheter
|
||||
Färdiga enheter för att använda portabelt
|
||||
|
||||
### WisMesh Tag
|
||||
https://pileupdx.com/product/wismesh-tag/
|
||||
|
||||
### MeshPocket Qi2
|
||||
https://pileupdx.com/product/meshpocket-qi2/
|
||||
|
||||
|
||||
## Kompletta infrastuktursnoder
|
||||
### Heltec MeshTower
|
||||
https://pileupdx.com/product/heltec-meshtower-solar-powered-lora-node/
|
||||
|
||||
### SenseCAP Solar Node P1-Pro for Meshtastic
|
||||
https://www.seeedstudio.com/SenseCAP-Solar-Node-P1-Pro-for-Meshtastic-LoRa-p-6412.html?srsltid=AfmBOooaumPzIiplHe-laLk59KNp1Bi0Qn0m9jetAZjH81ecUNJqztNA
|
||||
19
content/sv/docs/hardware/filter.md
Normal file
19
content/sv/docs/hardware/filter.md
Normal file
@@ -0,0 +1,19 @@
|
||||
---
|
||||
title: Filter
|
||||
weight: 50
|
||||
---
|
||||
I större städer så är det problemeatisk med störningar från mobilmaster. verkar framförralt vara 5G som stör. Bor man inom 300m från en mobilmast (de som är monterade på hustak) kan man överväga att investera i ett filter.
|
||||
|
||||
## Band pass filter
|
||||
Finns billiga, 100-300kr på Ali express. Dessa är av varierande kvalité. Kan dock vara värt att testa. Genomför ett antal (minst 10) traceroutes med och utan filer för att se om det gör någon skillnad.
|
||||
|
||||
## SAW filter
|
||||
###Paradar 868 Mhz SAW Filter
|
||||
https://pileupdx.com/product/paradar-868-mhz-saw-filter/
|
||||
|
||||
|
||||
## Kavitetsfiler
|
||||
Dessa filter är extremet effektiva ocm medför väldigt lite förluster, men de är oftast större och dyrare.
|
||||
|
||||
### sysmocom 868 MHz ISM Cavity Filte
|
||||
https://shop.sysmocom.de/868-863..870-MHz-cavity-filter-ISM-LoRa-SigFox-Helium/cf866.5-kt30
|
||||
23
content/sv/docs/hardware/laddkretsar.md
Normal file
23
content/sv/docs/hardware/laddkretsar.md
Normal file
@@ -0,0 +1,23 @@
|
||||
---
|
||||
title: Laddkretsar
|
||||
weight: 50
|
||||
---
|
||||
|
||||
## Laddningskretsar
|
||||
För dig som tittar på att ladda dina noder effektivt så rekommenderar vi dig starkt att använda en extern laddningskrets för dina batterier.
|
||||
Oftast är mikrokontrollrarnas inbyggdna laddningskretsar extremt begränsade (50-100mA per timme), något som kan sätta spiken i kistan för en annars välfungerande solnod.
|
||||
|
||||
### TP4056:
|
||||
En alldeles utmärkt laddningskrets för både USB-laddning och Solcellsladdning. [PDF](https://www.digikey.in/htmldatasheets/production/2049110/0/0/1/TP4056.pdf)
|
||||
|
||||
* Inbyggd laddningskurva för Litiumbatterier, och trappar ner laddströmmen för att skydda cellerna rätt.
|
||||
* Laddström (0.5A - 1A)
|
||||
|
||||
Finns att köpa på [Electrokit](https://www.electrokit.com/batteriladdare-lipo-microusb)
|
||||
|
||||
### VoltaicEnclosures - [MPPT Solar Battery Charger](https://www.etsy.com/se-en/listing/1609406536/mppt-solar-battery-charger-for-iot)
|
||||
Designad för Meshtastic, perfekt för seriösa solcellsnoder. Men kanske lite väl dyr...
|
||||
|
||||
* Hanterar de flesta batterikemier (LTO, NA+, LifePo4, Li-ion, ...)
|
||||
* Laddström: 1A
|
||||
* Inbyggd INA3221 I2C sensor för att övervaka strömmen från solcellen, batteriet och den externa enheten.
|
||||
23
content/sv/docs/hardware/solceller.md
Normal file
23
content/sv/docs/hardware/solceller.md
Normal file
@@ -0,0 +1,23 @@
|
||||
---
|
||||
title: Solceller
|
||||
weight: 90
|
||||
---
|
||||
När det kommer till solcellsnoder så finns det en uppsjö med alternativ, men här rundar vi ner till de vanligaste typerna och mest förekommande spänningarna.
|
||||
|
||||
### Monokristallina Solceller:
|
||||
Det överlägset bästa alternativet för solcellsladdning
|
||||
- Bättre laddning vid lågt ljus.
|
||||
- Längre livslängder.
|
||||
- Högre uteffekt per area.
|
||||
|
||||
### Polykristallina solceller:
|
||||
Billigare paneler än Monokristallina men fungerar ändå väl
|
||||
- Sämre uteffekt
|
||||
- Aningen lägre livslängd
|
||||
- Lägre uteffekt per area.
|
||||
|
||||
### Spänningsnivåer på Solceller
|
||||
För de flesta mikrokontrollrarna med inbyggd laddning så är det solceller på 5/6-volt som du skall leta efter.
|
||||
På detta sätt behöver du färre ytterligare komponenter och de kostar även mindre.
|
||||
|
||||
För dig som tittar på laddning med 12V-paneler eller högre spänningar så finns det givetvis vinster med det såsom högre uteffekter (och därmed snabbare laddning med rätt kretsar). Däremot är kostnaden högre, inte bara på panelerna men på step-up/down-kretsar och effektiv spänningsomvandling.
|
||||
@@ -1,33 +0,0 @@
|
||||
---
|
||||
title: Kartor
|
||||
weight: 80
|
||||
---
|
||||
Det finns massvis med olika karttjänster, vi rekommenderar Liam Cottles karta:
|
||||
https://meshtastic.liamcottle.net/
|
||||
|
||||
|
||||
<a href="https://meshtastic.liamcottle.net/"><img src="/featured-background-map.png" width="50%"></a>
|
||||
|
||||
{{% alert title="Uppmärksamma" color="warning" %}}
|
||||
För att tillåta andra noder publicera din nod på kartan behöver `lora.config_ok_to_mqtt` vara aktiverat.
|
||||
{{% /alert %}}
|
||||
|
||||
Kartan är ett fantastik verktyg för att analysera meshen, man kan se vart olika noder finns. vilka noder som har vilken roll. Klickar man på en nod och visar detaljer så får man grafer över telemetri. Kartan kan visa vilka noder som har kontakt med vilka genom [neighbor info]({{< ref "neighbor_info.md" >}}) modulen.
|
||||
|
||||
En lite mer okänd funktion är att kartan kan visa de meddelanden som skickas okrypterat. Klicka på någon av de noder som har MQTT uplink igång (de gröna) och välj "Show Full Details", sedan "Gated Msgs". Detta är ett bra sätt att analysera vilka noder som hör de meddelanden du skickar. Det kan även används för att hålla koll på vad som skrivs när man inte är ansluten till sin nod.
|
||||
|
||||
För att själv bidra med information till kartan för instruktionerna på följande sida:
|
||||
[MQTT Konfiguration]({{< ref "mqtt.md#mqtt-konfiguration" >}})
|
||||
|
||||
|
||||
## Community hostade forks
|
||||
Liam Cottle har publicerat källkoden och instruktioner på sin [GitHub](https://github.com/liamcottle/meshtastic-map).
|
||||
Detta har lett till att flertalet entusiaster hostar sina egna instanser eller forks:
|
||||
* https://karta.meshat.se/
|
||||
* https://meshtastic.roslund.cloud/
|
||||
|
||||
|
||||
|
||||
## Övriga karttjänster
|
||||
* https://meshmap.net
|
||||
* https://map.meshtastic.org
|
||||
112
content/sv/docs/meshcore.md
Normal file
112
content/sv/docs/meshcore.md
Normal file
@@ -0,0 +1,112 @@
|
||||
---
|
||||
title: MeshCore
|
||||
weight: 20
|
||||
---
|
||||
Information om MeshCore finns här: https://meshcore.co.uk/ och här: https://github.com/meshcore-dev/
|
||||
|
||||
Den grundläggande tjänst som ett MeshCore-nät tillhandahåller är textkommunikation (tjatt/epost) och Telemetri
|
||||
|
||||
Ett MeshCore-nät kan innehålla tre nodtyper som alla kan kommunicera med varandra via LoRa
|
||||
|
||||
- **Router**: bygger nät genom att reläa meddelanden till andra routrar
|
||||
- **Room Server**: Tillhandahåller en BBS inkl Router funktioner
|
||||
- **Companion**: Klientnod som en användare kan ansluta sin mobil eller dator till via BLE, USB eller WiFi. Det finns en variant som stöder kryptering av textmeddelanden.
|
||||
|
||||
Varje nodtyp har sin egen firmware som flashas endera 1) via en web-applikation (webgui) eller 2) via kommandoraden (cli)
|
||||
|
||||
## 1) Flashning via webgui
|
||||
|
||||
Via webgui (https://flasher.meshcore.co.uk/) kan man ange den LoRa-hårdvara man har och vilken nodtyp som skall skapas.
|
||||
|
||||
- Companion (klient)
|
||||
- Repeater
|
||||
- Room Server
|
||||
|
||||
Repeaters och Room Servers konfigureras genom seriell kommunikation med den flashade enheten startad från Web-applikationen.
|
||||
|
||||
Companion konfigureras via bluetooth, antingen med mobilapplikationen Meshcore (ios/android) eller över Web-applikationens serieinterface.
|
||||
|
||||
Web applikationen kan även användas till att ladda ner den firmware som man sedan vill flasha i en annan miljö, exempelvis en headless Linux-miljö där webapplikationen inte kan användas.
|
||||
|
||||
## 2) Flashning via cli
|
||||
|
||||
### a) Installera esptool med beroenden
|
||||
|
||||
Om man har en LoRa-enhet ansluten till en Raspberry pi headless, måste denna uppdateras med "esptool" för att man ska kunna flasha.
|
||||
|
||||
Förutsättningen är att python och pip är installerade och uppdaterade
|
||||
|
||||
**Fungerar på Linux Trixie:**
|
||||
|
||||
```bash
|
||||
pip3 install --upgrade esptool --break-system-packages
|
||||
cd /usr/local/bin
|
||||
ln -s /home/pi/.local/bin/esptool esptool
|
||||
```
|
||||
|
||||
**Fungerar på Linux bullseye:**
|
||||
|
||||
```bash
|
||||
sudo apt-get install python3-full
|
||||
sudo apt-get install python3-pip
|
||||
pip3 install --upgrade pip setuptools wheel
|
||||
sudo apt-get install libhdf5-dev
|
||||
sudo apt-get install build-essential libssl-dev libffi-dev python3-dev
|
||||
pip3 install --upgrade esptool
|
||||
```
|
||||
|
||||
### b) Flashning
|
||||
|
||||
Aktuell firmware som laddas ner via Meshcores Web applikationen och flashningen görs med cli kommandot:
|
||||
|
||||
```bash
|
||||
python -m esptool --port /dev/ttyUSB0 write_flash 0x0 Heltec_v3_repeater-v1.9.1-f5f5886-merged.bin
|
||||
```
|
||||
|
||||
Endast en LoRa burk kan vara inkopplad i pajen när man skall flasha.
|
||||
|
||||
### c) Konfigurering med hjälp av seriell terminalemulator
|
||||
|
||||
CLI konfigurations kommandon via picocom eller annan seriekonsol, exempel:
|
||||
|
||||
```bash
|
||||
picocom /dev/ttyUSB0 -b 115200 --imap lfcrlf
|
||||
```
|
||||
|
||||
**Repeater SK0BU (exempel)**
|
||||
|
||||
```
|
||||
Heltec v3 868 MHz
|
||||
set name Repeater SK0BU
|
||||
set lat 59.3477783
|
||||
set lon 18.0748596
|
||||
time 17026... se https://www.epochconverter.com/
|
||||
(när en Repeater eller Room Server bootas om måste man sätta tiden igen. Kan antingen
|
||||
göras via cli eller att man loggar in på Repeatern/Room Servern via companium klienten)
|
||||
set radio 869.618,62.5,8,8
|
||||
(reboot)
|
||||
set repeat on
|
||||
password xxxxx
|
||||
set advert. interval 60
|
||||
set flood.advert.interval 3
|
||||
```
|
||||
|
||||
### d) Konfigurering av Companion via mobilen och bluetooth
|
||||
|
||||
- Name, lämpligt namn
|
||||
- Latitude (behöver inte anges om LoRa enheten innehåller GPS)
|
||||
- Longitude
|
||||
- Markera "Share Position in Advert"
|
||||
- **Radio Settings:**
|
||||
- EU/UK (Narrow)
|
||||
- Frequency 869.618 MHz
|
||||
- Bandwidth 62.5 KHz
|
||||
- Spreading Factor 8
|
||||
- Coding rate 8
|
||||
- Transmit Power 22
|
||||
|
||||
## Förteckning över cli kommandon
|
||||
|
||||
**Repeater & Room Server CLI Reference**
|
||||
|
||||
https://github.com/meshcore-dev/MeshCore/wiki/Repeater-&-Room-Server-CLI-Reference
|
||||
5
content/sv/docs/meshtastic/_index.md
Normal file
5
content/sv/docs/meshtastic/_index.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
title: Meshtastic
|
||||
linkTitle: Meshtastic
|
||||
weight: 10
|
||||
---
|
||||
@@ -20,7 +20,7 @@ Därför är det viktigt att `CLIENT`-noder i större meshnätverk placeras väl
|
||||
## Client Mute
|
||||
`CLIENT_MUTE`-rollen liknar `CLIENT` men med en viktig skillnad – den vidarebefordrar eller routar inga meddelanden. Detta gör den idealisk för större meshnätverk med hög nätverkstrafik, där extra routing kan orsaka överbelastning.
|
||||
|
||||
För de som har flera enheter på samma plats rekommenderas att **max en** enhet sätts som `CLIENT` medan resten får rollen `CLIENT_MUTE` för att minska onödig trafik och optimera nätverkets prestanda.
|
||||
För de som har flera enheter på samma plats rekommenderas att **högst en** enhet sätts som `CLIENT` medan resten får rollen `CLIENT_MUTE` för att minska onödig trafik och optimera nätverkets prestanda.
|
||||
|
||||
I Stockholm bör portabla noder och noder man har inomhus primärt vara `CLIENT MUTE`.
|
||||
|
||||
@@ -31,17 +31,17 @@ Routrar vidarebefordrar meddelanden från andra enheter direkt, medan andra node
|
||||
|
||||
Routrar vidarebefordrar alltid, medan andra roller kan välja att inte vidarebefordra om de hör en granne vidarebefordra först.
|
||||
|
||||
För att optimera nätverkets prestanda och undvika kollisioner bör `ROUTER`-noder placeras så att **så få noder som möjligt når mer än en ROUTER samtidigt.** Detta då om ett meddelanden når flera routrar, så kommer de alla vidarebefordra meddelande samtidigt och störa ut varandra.
|
||||
För att optimera nätverkets prestanda och undvika kollisioner bör `ROUTER`-noder placeras så att **så få noder som möjligt når mer än en ROUTER samtidigt.** Detta eftersom om ett meddelande når flera routrar kommer de alla att vidarebefordra meddelandet samtidigt och störa ut varandra.
|
||||
|
||||
{{% alert title="Tips" color="primary" %}}
|
||||
Sänk `max_hops`. En välplacerad router når långt med bara **ett eller två hopp**. Din router bör nå en nod med MQTT uplink, så att du kan få den telemetri du behöver via [kartan](https://meshtastic.liamcottle.net/)
|
||||
Sänk `max_hops`. En välplacerad router når långt med bara **ett eller två hopp**. Din router bör nå en nod med MQTT-uplink så att du kan få den telemetri du behöver via [kartan](https://meshtastic.liamcottle.net/)
|
||||
{{% /alert %}}
|
||||
|
||||
|
||||
## Router Late
|
||||
`ROUTER_LATE`-rollen är lik `ROUTER`, den vidarebefordrar alla meddelanden, men den gör det under samma tidsfönster som `CLIENT` noder. Detta kan vara mycket användbart i områden där man når ut till meshen, men har svårt att ta emot alla meddelanden.
|
||||
`ROUTER_LATE`-rollen liknar `ROUTER`: den vidarebefordrar alla meddelanden, men den gör det under samma tidsfönster som `CLIENT`-noder. Detta kan vara mycket användbart i områden där man når ut till meshen men har svårt att ta emot alla meddelanden.
|
||||
|
||||
## Repeater
|
||||
`REPEATER`-rollen fungerar liknande `ROUTER`-rollen, men går ett steg längre genom att enbart vidarebefordra den meddelanden den tar emot. Den skickar inte ut några paket om sig själv, tex. nod-info.
|
||||
`REPEATER`-rollen fungerar liknande `ROUTER`-rollen men går ett steg längre genom att enbart vidarebefordra de meddelanden den tar emot. Den skickar inte ut några paket om sig själv, t.ex. nod-info.
|
||||
|
||||
Detta är en mycket effektiv roll. Men vi rekommenderar istället att använda `ROUTER` med optimerade inställningar, så att noden syns och registreras som en aktiv del av meshnätverket.
|
||||
Detta är en mycket effektiv roll. Men vi rekommenderar istället att använda `ROUTER` med optimerade inställningar så att noden syns och registreras som en aktiv del av meshnätverket.
|
||||
45
content/sv/docs/meshtastic/kartor.md
Normal file
45
content/sv/docs/meshtastic/kartor.md
Normal file
@@ -0,0 +1,45 @@
|
||||
---
|
||||
title: Kartor
|
||||
weight: 80
|
||||
---
|
||||
Kartan är ett fantastik verktyg för att analysera meshen. STHLM-MESH har en egen [fork](https://github.com/roslund/meshtastic-map/) av Liam Cottle's karta, där vi gjort massvis med anpassningar för att bättre kunna analysera meshen i stokholm.
|
||||
|
||||
|
||||
## Connections
|
||||
Connections-lagret visar vilka noder som har kontakt med varandra. Data baseras primärt på data från traceroutes. Det visar aggregerad signalstyrka över tid och ersätter neighbours-lagret.
|
||||
|
||||
### Datainsamling
|
||||
Extraherar "edges" (anslutningar) från traceroute-paket (portnum 70)
|
||||
Endast paket där want_response = false (svarspaket, inte förfrågningar)
|
||||
Lagrar: från-nod, till-nod, SNR, nodernas positioner vid skapandet, packet_id, channel_id, gateway_id
|
||||
|
||||
### Visning på kartan
|
||||
Ett "Connections"-lager i overlay-menyn
|
||||
En linje per nodpar (bidirektionell, inga pilar)
|
||||
Färg baserad på sämsta genomsnittliga SNR (grön ≥ -5dB, gul > -15dB, röd ≤ -15dB)
|
||||
### Aggregering
|
||||
Beräknar genomsnittlig SNR per riktning över en tidsperiod som går att specificera i inställningarna.
|
||||
|
||||
Visar de senaste 5 unika edges per riktning (deduplicerar samma packet_id från flera gateways)
|
||||
Räknar totalt antal unika paket per riktning
|
||||
|
||||
### Positionstillstånd
|
||||
Visar endast connections där båda noderna fortfarande är på samma position som när edgen skapades. Om en nod flyttats visas inte edges från den gamla positionen.
|
||||
|
||||
### Tooltip-information
|
||||
Nodnamn för båda riktningarna
|
||||
Genomsnittlig SNR per riktning
|
||||
Totalt antal edges per riktning
|
||||
De senaste 5 edges per riktning med SNR, tid och källa (Traceroute eller Neighbourinfo)
|
||||
Avstånd mellan noderna
|
||||
Terrainprofilbild från HeyWhatsThat
|
||||
|
||||
### Inställningar:
|
||||
* "Connections Time Period" i inställningar (15 min–30 dagar, standard 7 Dagar)
|
||||
Styr vilken tidsperiod som används för aggregering
|
||||
|
||||
## Traceroutes
|
||||
Visar traceroutes i realtid
|
||||
|
||||
## Backbone och backbone connections
|
||||
Ett lager tänkt för att kunna visa enbart de infrastruktur-noder som håller ihop meshet. Vilka noder som kallificeras som "Backbone" är manuellt hårdkodat. Har du en mycket bra infrastruktur-nod säg till i Discord så kan vi överväga att lägga till den.
|
||||
@@ -15,22 +15,21 @@ För att andra ska kunna vidarebefordra dina meddelanden till MQTT måste du ha
|
||||
{{% /alert %}}
|
||||
|
||||
## MQTT Konfiguration
|
||||
Vi rekommenderar att man uplinkar till Liam Cottle's MQTT-broker och karta.
|
||||
Det finns massvis med olika karttjänster.
|
||||
|
||||
{{% alert title="Uppmärksamma" color="warning" %}}
|
||||
För att tillåta andra noder publicera din nod på kartor behöver `lora.config_ok_to_mqtt` vara aktiverat.
|
||||
{{% /alert %}}
|
||||
|
||||
Vi rekommenderar att man uplinkar till [meshat.se](https://www.meshat.se/om/mqtt/), de skickar även infromationen vidare till Liam Cottle's MQTT-broker och [karta](https://meshtastic.liamcottle.net/
|
||||
).
|
||||
|
||||
{{< card code=true lang="yml" >}}
|
||||
mqtt:
|
||||
mqtt.enabled: True
|
||||
mqtt.address: mqtt.meshtastic.liamcottle.net
|
||||
mqtt.username: uplink
|
||||
mqtt.password: uplink
|
||||
mqtt.root: msh/EU_868/SE/Stockholm
|
||||
mqtt.encryption_enabled: True
|
||||
mqtt.json_enabled: False
|
||||
mqtt.tls_enabled: False
|
||||
mqtt.proxy_to_client_enabled: False
|
||||
mqtt.map_reporting_enabled: True
|
||||
mqtt.map_report_settings.publish_interval_secs: 900
|
||||
mqtt.map_report_settings.position_precision: 32
|
||||
Address: mqtt.meshat.se
|
||||
Username: msh
|
||||
Password: msh
|
||||
Encryption Enabled: Yes
|
||||
JSON Output: No
|
||||
{{< /card>}}
|
||||
|
||||
{{% pageinfo %}}
|
||||
@@ -2,9 +2,9 @@
|
||||
title: Neighbor Info
|
||||
weight: 70
|
||||
---
|
||||
[Neighbor Info modulen](https://meshtastic.org/docs/configuration/module/neighbor-info/) samlar information om en nods grannar som den har direktkontakt med (0-hopp). Denna information kan sedan skickas över MQTT eller LoRa.
|
||||
[Neighbor Info-modulen](https://meshtastic.org/docs/configuration/module/neighbor-info/) samlar information om en nods grannar som den har direktkontakt med (0-hopp). Denna information kan sedan skickas över MQTT eller LoRa.
|
||||
|
||||
Informationen kan sedan visualiseras på karttjänster. Liam Cottles karta visar information om varje förbindelse. Utöver SNR visas även en terränggraf från [HeyWhatsThat.com](HeyWhatsThat.com).
|
||||
Informationen kan sedan visualiseras på karttjänster. Liam Cottles karta visar information om varje förbindelse. Utöver SNR visas även en terränggraf från [HeyWhatsThat.com](https://www.heywhatsthat.com).
|
||||
|
||||
|
||||
{{< figure src="/images/docs/neighbors.png" width="400px" height="300px" >}}
|
||||
@@ -23,11 +23,11 @@ neighbor_info:
|
||||
neighbor_info.update_interval: 43200
|
||||
{{< /card>}}
|
||||
|
||||
### För ROUTERS
|
||||
### För routrar
|
||||
Att skicka Neighbor Info över LoRa rekommenderas inte, eftersom det använder mycket bandbredd.
|
||||
Men för vissa noder, särskilt routrar, kan det ändå vara intressant.
|
||||
|
||||
I senare versioner av firmware tillåts det inte att skicka Neighbor Info över standardkanaler, t.ex. LongFast.
|
||||
Denna begränsning fungerar bättre i USA, där de olika kanalerna får sina egna frekvens-slotar. I EU spelar detta dock ingen roll.
|
||||
Denna begränsning fungerar bättre i USA, där de olika kanalerna får sina egna frekvensslotar. I EU spelar detta dock ingen roll.
|
||||
|
||||
För att kringgå denna begränsning måste man ta bort spärren i koden och själv kompilera sin firmware.
|
||||
För att kringgå denna begränsning måste man ta bort spärren i koden och själv kompilera sin firmware.
|
||||
@@ -2,35 +2,35 @@
|
||||
title: Position
|
||||
weight: 30
|
||||
---
|
||||
En nod kan dela sin position till alla andra noder över meshet.
|
||||
Det gör det möjligt att se hur meshset sträcker sig geografiskt.
|
||||
En nod kan dela sin position till alla andra noder över meshet.
|
||||
Det gör det möjligt att se hur meshet sträcker sig geografiskt.
|
||||
|
||||
För att dela position behövs antingen en GPS-modul eller en ansluten smartphone.
|
||||
Alternativt så kan man sätta en _fixed location_ där man själv anger koordinater, eller använder telefonens nuvarande position.
|
||||
Alternativt kan man sätta en _fixed location_ där man själv anger koordinater eller använder telefonens nuvarande position.
|
||||
|
||||
|
||||
## Position Precision
|
||||
Som standard kommer din nod inte dela med sig av sin exakta position. Den kommer skicka en position och en noggrannhet. Detta visar sig som en cirkel runt noden på karta, där din nod är någonstans inom den cirkeln.
|
||||
|
||||

|
||||
Detta gör att du inte avslöjar din exakta address, eller den exakta positionen på din solnod.
|
||||
Detta gör att du inte avslöjar din exakta adress eller den exakta positionen på din solnod.
|
||||
Men följddefekten blir att kartan blir väldigt stökig. För noder som använder fixed location så kan man istället sätta den positionen en liten bit ifrån din plats, kanske grannhuset?
|
||||
|
||||
**Position precision konfigureras i kanalinställningarna.**
|
||||
|
||||
{{% alert title="Uppmärksamma" color="warning" %}}
|
||||
iPhone appen har begränsat möjligheten att sätta noggrannhet på position til 1.5km. Detta då det kan anses personlig information enligt GDPR. För att konfigurera högre noggrannhet så behöver man använda en annan klient (tex: [web klienten](https://meshtastic.org/docs/software/web-client/) eller [Python CLI](https://meshtastic.org/docs/software/python/cli/))
|
||||
iPhone-appen har begränsat möjligheten att sätta noggrannhet på position till 1,5 km. Detta då det kan anses vara personlig information enligt GDPR. För att konfigurera högre noggrannhet behöver man använda en annan klient (t.ex. [webbklienten](https://meshtastic.org/docs/software/web-client/) eller [Python CLI](https://meshtastic.org/docs/software/python/cli/))
|
||||
{{% /alert %}}
|
||||
|
||||
## Konfigurera fixed position genom Python CLI
|
||||
Som iPhone användare väljer jag att använda [Python CLI](https://meshtastic.org/docs/software/python/cli/) för att konfigurera position.
|
||||
Som iPhone-användare väljer jag att använda [Python CLI](https://meshtastic.org/docs/software/python/cli/) för att konfigurera position.
|
||||
{{< card code=true lang="yml" >}}
|
||||
meshtastic --setlat 59.520790 --setlon 17.922659 --setalt 10
|
||||
meshtastic --set position.position_broadcast_secs 43200
|
||||
meshtastic --ch-set module_settings.position_precision 32 --ch-index 0
|
||||
{{< /card>}}
|
||||
|
||||
Är din nod kopplad till MQTT och du använder _map report_ så finns det en position precision inställning även där.
|
||||
Är din nod kopplad till MQTT och du använder _map report_ finns det även en inställning för position precision där.
|
||||
{{< card code=true lang="yml" >}}
|
||||
meshtastic --set mqtt.map_report_settings.position_precision 32
|
||||
{{< /card>}}
|
||||
@@ -7,22 +7,22 @@ Här listas rekommenderade inställningar för Stockholm. Inställningarna heter
|
||||
|
||||
## LoRa Configuration
|
||||
|
||||
| Inställning | Värde | Beskrivning |
|
||||
|------------------|-------------|-------------|
|
||||
| Region | EU_868 | Den som primärt används i Sverige och EU |
|
||||
| Modem Preset | `LONG_FAST` | |
|
||||
| Max Hops | 4-5 | Försök håll så lågt som möjligt |
|
||||
| Transmit Enabled | true | Kan stängas av vid byta antenn [^1] |
|
||||
| Ignore MQTT | true | Vidarebefordra inte meddelanden som kommer från MQTT |
|
||||
| OK to MQTT | true | Kan stängas av föra att inte synas på karttjänsterna [^2] |
|
||||
| Inställning | Värde | Beskrivning |
|
||||
|------------------|---------------|-------------|
|
||||
| Region | EU_868 | Den som primärt används i Sverige och EU |
|
||||
| Modem Preset | `MEDIUM_FAST` | Medium Range - Fast |
|
||||
| Max Hops | 4-5 | Försök hålla så lågt som möjligt |
|
||||
| Transmit Enabled | true | Kan stängas av vid byta antenn [^1] |
|
||||
| Ignore MQTT | true | Vidarebefordra inte meddelanden som kommer från MQTT |
|
||||
| OK to MQTT | true | Kan stängas av för att inte synas på karttjänsterna [^2] |
|
||||
|
||||
[^1]: Det kan vara skadligt för enheten att sända utan antenn.
|
||||
[^2]: Detta är enbart en vädjan, det finns inget kryptografiskt skydd. Flertalet tjänster ignorerar denna vädjan och publicerar din nod ändå.
|
||||
|
||||
### Max hops
|
||||
Max hops anger i hur många led noder ska vidarebefordra ditt meddelande. Max hops sätts när paket sänds, en nod som vidarebefordrar ett paket minskar _max hops_ med ett. Vad nodens som vidarebefordrar medelande har för max hopps påverkar inte.
|
||||
Max hops anger i hur många led noder ska vidarebefordra ditt meddelande. Max hops sätts när paket sänds, en nod som vidarebefordrar ett paket minskar _max hops_ med ett. Vad noden som vidarebefordrar meddelandet har för max hops påverkar inte.
|
||||
|
||||
Det kan vara lockande att direkt välja max antal (7) som tillåts. Detta bör dock undvikas då det påverka stabiliteten i hela meshen. Istället rekommenderas man först och främst använda _trace route_ funktionen för att försöka avgöra vilken väg meddelanden tar genom meshen. Kom ihåg att detta inte är deterministiskt och meshen är under ständig förändring. Det brukar krävas ett tjugotal lyckade trace routs för att få ett hum om hur det funkar. Experimentera dig fram till _Max hops_ som funkar för dig.
|
||||
Det kan vara lockande att direkt välja max antal (7) som tillåts. Detta bör dock undvikas då det påverkar stabiliteten i hela meshen. I stället rekommenderas man först och främst använda _trace route_-funktionen för att försöka avgöra vilken väg meddelanden tar genom meshen. Kom ihåg att detta inte är deterministiskt och meshen är under ständig förändring. Det brukar krävas ett tjugotal lyckade trace routes för att få ett hum om hur det funkar. Experimentera dig fram till _Max hops_ som funkar för dig.
|
||||
|
||||
För routrar rekommenderas det att ha ett lågt antal max hops. En välplacerad router bör nå långt med enbart ett hopp eller två. Du bör inte ha ett högre antal _max hops_ än vad som krävs för att köra remote admin om du planerar använda dig av det.
|
||||
|
||||
@@ -71,5 +71,5 @@ Notera att batterinivå är inkluderad i Device Metrics. Power Metrics är enbar
|
||||
| Transmit over LoRa | False |
|
||||
| Update interval | 1h (3600s) |
|
||||
|
||||
Om en nod är är uppkopplad mot en MQTT server så kan man skicka neighbor info frekvent.
|
||||
Om man vill skicka neighbor info över LoRa så bör man inte skicka oftare än var 12:e timme (43200s). Dock så är detta begränsat i firmware. För mer detaljer se [Neighbor Info]({{% ref neighbor_info.md %}})
|
||||
Om en nod är uppkopplad mot en MQTT-server kan man skicka neighbor info frekvent.
|
||||
Om man vill skicka neighbor info över LoRa bör man inte skicka oftare än var tolfte timme (43200s). Dock är detta begränsat i firmware. För mer detaljer se [Neighbor Info]({{% ref neighbor_info.md %}})
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 4.1 MiB |
@@ -1,63 +0,0 @@
|
||||
---
|
||||
title: Meetups
|
||||
linkTitle: Meetups
|
||||
menu: {main: {weight: 40}}
|
||||
---
|
||||
|
||||
{{% blocks/cover title="" image_anchor="top" height="min" %}}
|
||||
{{% /blocks/cover %}}
|
||||
|
||||
{{% blocks/section %}}
|
||||
## Kommande Meetups
|
||||
|
||||
<div class="container mt-5">
|
||||
<div class="card shadow-lg">
|
||||
<div class="card-body">
|
||||
<h1 class="text-center text-primary">🍻 Meshtastic AW i Stockholm! 🍻</h1>
|
||||
<p class="lead text-center">Träffa likasinnade, snacka LoRa och bygg ut nätverket i Stockholm!</p>
|
||||
|
||||
<div class="text-center my-4">
|
||||
<strong>📍 Plats:</strong> <span>TBD</span><br>
|
||||
<strong>📅 Datum:</strong> <span>TDB</span><br>
|
||||
<strong>⏰ Tid:</strong> <span>TDB</span>
|
||||
</div>
|
||||
|
||||
<!--<p class="text-center">Våren närmar sig och det är massvis med trafik i meshen. Det har dessutom tillkommit massvis med nya noder och personer. Vi bjuder därför in till After Work för de som vill träffa likasinnade, snacka LoRa, dela erfarenheter och visa hemmabyggen.</p>-->
|
||||
|
||||
<!-- <p class="text-center"><strong>Ta gärna med din nod, eller visa upp det senaste bygget.</strong></p>-->
|
||||
|
||||
<div class="text-center mt-4">
|
||||
<a href="#" class="btn btn-outline-secondary btn-lg disabled" style=""><i class="fab fa-facebook"></i> Anmäl dig här</a>
|
||||
</div>
|
||||
<br>
|
||||
<!-- <p class="text-center">Om du inte har Facebook är det helt okej att dyka upp oanmäld. Men skriv gärna ett meddelande på meshen eller Discord om du kommer!</p> -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{% /blocks/section %}}
|
||||
|
||||
{{% blocks/section color="info" %}}
|
||||
## Tidigare meetups:
|
||||
{{< cardpane >}}
|
||||
{{< card >}}
|
||||
* **Datum:** 2025-04-08
|
||||
* **Tid:** 17:00 - 22:00
|
||||
* **Plats:** The Bishops Arms, Sundbyberg
|
||||
* **Event:** [Facebook](https://www.facebook.com/events/2766664646866905/)
|
||||
{{< /card >}}
|
||||
{{< card >}}
|
||||
* **Datum:** 2024-09-04
|
||||
* **Tid:** 17:00 - 22:00
|
||||
* **Plats:** The Bishops Arms, Sundbyberg
|
||||
* **Event:** [Facebook](https://www.facebook.com/events/1183504712869737/)
|
||||
{{< /card >}}
|
||||
{{< card >}}
|
||||
* **Datum:** 2024-05-05
|
||||
* **Tid:** 17:00
|
||||
* **Plats:** Takpark by Urban Deli, Sveavägen 44
|
||||
{{< /card >}}
|
||||
{{< /cardpane >}}
|
||||
|
||||
{{% /blocks/section %}}
|
||||
|
||||
@@ -15,14 +15,20 @@ menu: {main: {weight: 50}}
|
||||
<input type="radio" class="btn-check" name="btnradio" id="1978245180" autocomplete="off">
|
||||
<label class="btn btn-outline-primary" for="1978245180" data-bs-toggle="tooltip" data-bs-placement="top" title="SA0CVK 0 (Ny Router)">CVK</label>
|
||||
|
||||
<input type="radio" class="btn-check" name="btnradio" id="862351628" autocomplete="off">
|
||||
<label class="btn btn-outline-primary" for="862351628" data-bs-toggle="tooltip" data-bs-placement="top" title="JwK Bas">JwK</label>
|
||||
<input type="radio" class="btn-check" name="btnradio" id="1770352332" autocomplete="off">
|
||||
<label class="btn btn-outline-primary" for="1770352332" data-bs-toggle="tooltip" data-bs-placement="top" title="JwK Bas">JwK</label>
|
||||
|
||||
<input type="radio" class="btn-check" name="btnradio" id="3663385928" autocomplete="off">
|
||||
<label class="btn btn-outline-primary" for="3663385928" data-bs-toggle="tooltip" data-bs-placement="top" title="Mariehäll Alprosen">Ros1</label>
|
||||
<input type="radio" class="btn-check" name="btnradio" id="1730698186" autocomplete="off">
|
||||
<label class="btn btn-outline-primary" for="1730698186" data-bs-toggle="tooltip" data-bs-placement="top" title="Mariehäll Alprosen">Ros1</label>
|
||||
|
||||
<input type="radio" class="btn-check" name="btnradio" id="2971894662" autocomplete="off">
|
||||
<label class="btn btn-outline-primary" for="2971894662" data-bs-toggle="tooltip" data-bs-placement="top" title="Stora Älgön 2.0">Älg</label>
|
||||
<input type="radio" class="btn-check" name="btnradio" id="1771757728" autocomplete="off">
|
||||
<label class="btn btn-outline-primary" for="1771757728" data-bs-toggle="tooltip" data-bs-placement="top" title="La Essingen S SM0YOS">ESSS</label>
|
||||
|
||||
<input type="radio" class="btn-check" name="btnradio" id="1130122168" autocomplete="off">
|
||||
<label class="btn btn-outline-primary" for="1130122168" data-bs-toggle="tooltip" data-bs-placement="top" title="MLZ0 Ruggen">MLZ0</label>
|
||||
|
||||
<input type="radio" class="btn-check" name="btnradio" id="3944789204" autocomplete="off">
|
||||
<label class="btn btn-outline-primary" for="3944789204" data-bs-toggle="tooltip" data-bs-placement="top" title="Lura Base">Lura</label>
|
||||
</div>
|
||||
<!-- Info Button with Tooltip -->
|
||||
<div data-bs-toggle="tooltip" data-bs-placement="right" title="Visar meddelanden mottagna av respektive nod. Användbart för att förstå vilka noder som har hört ett meddelande.">
|
||||
|
||||
@@ -26,15 +26,45 @@ draft: false
|
||||
</div>
|
||||
|
||||
## Text Meddelanden
|
||||
Antalet meddelanden per timme senaste 7 dygnen. Grafen visar meddelanden som skickats på LongFast kanalen, men även okrypterade meddelanden mellan noder. De meddelanden som skickas går att se [här]({{< ref messages >}}).
|
||||
Antalet meddelanden per timme senaste 7 dygnen. Grafen visar antal okrypterade meddelanden som skickats. Direktmedelanden mellan noder är krypterade och visas inte i grafen. De meddelanden som skickas går att se [här]({{< ref messages >}}).
|
||||
<div class="stats-chart-container" style="min-height:300px;">
|
||||
<canvas id="messagesChart"></canvas>
|
||||
</div>
|
||||
|
||||
|
||||
## Kanalutnyttjande
|
||||
Kanalutnyttjande (Channel Utilization) visar hur mycket av radiokanalen som är upptagen. Under 15–20% är allt normalt, över 25% får Telemetri lägre prioritet, vilket kan ge längre tid mellan uppdateringar. Runt 40% stryps positionsuppdateringar.
|
||||
|
||||
<div class="container my-3 mx-0" style="max-width: 1000px;">
|
||||
<div class="row px-0 align-items-start">
|
||||
<div class="col-md-6 mb-3">
|
||||
<h5 class="text-muted">MediumFast</h5>
|
||||
<div id="mediumfastGaugeContainer" class="" style="height: 300px;">
|
||||
<canvas id="mediumfastGauge"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div id="mediumfastLegend" class="" style="font-size: 0.85rem;"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="btn-group btn-group-sm mb-1" role="group" aria-label="Channel utilization period">
|
||||
<input type="radio" class="btn-check" name="chUtilPeriod" id="chUtil7" value="7" autocomplete="off" checked>
|
||||
<label class="btn btn-outline-primary" for="chUtil7">7 dagar</label>
|
||||
<input type="radio" class="btn-check" name="chUtilPeriod" id="chUtil14" value="14" autocomplete="off">
|
||||
<label class="btn btn-outline-primary" for="chUtil14">14 dagar</label>
|
||||
<input type="radio" class="btn-check" name="chUtilPeriod" id="chUtil30" value="30" autocomplete="off">
|
||||
<label class="btn btn-outline-primary" for="chUtil30">30 dagar</label>
|
||||
<input type="radio" class="btn-check" name="chUtilPeriod" id="chUtil60" value="60" autocomplete="off">
|
||||
<label class="btn btn-outline-primary" for="chUtil60">60 dagar</label>
|
||||
</div>
|
||||
<div id="channelUtilizationContainer" class="stats-chart-container" style="min-height:300px;">
|
||||
<canvas id="channelUtilizationChart"></canvas>
|
||||
</div>
|
||||
|
||||
|
||||
## Skapade paket per nod
|
||||
Grafen nedan visar vilka noder som har skickat flest paket under det senaste dygnet.
|
||||
Graferna bygger på MQTT-data från ett begränsat antal noder.
|
||||
Eftersom vi har begränsad bandbredd är det viktigt att hålla nere trafiken. En sändningsvolym på max 100 paket per dygn är önskvärd.
|
||||
<div id="mostActiveNodesContainer" class="stats-chart-container">
|
||||
<canvas id="mostActiveNodes"></canvas>
|
||||
@@ -74,25 +104,37 @@ För mer info se vår [dokumentation]({{<ref position.md>}}#position-precision).
|
||||
</div>
|
||||
|
||||
## Firmware versioner
|
||||
Antalet enheter av firmware version som synts i meshet de senaste 30 dagarna. Information om firmware information skickas inte över meshet. Men vi kan se om en enhet kör en gammal firmware version genom att kolla efter ["ok_to_mqtt" bitten](https://github.com/meshtastic/firmware/pull/4643) som infördes i [2.5.0.9ac0e26](https://github.com/meshtastic/firmware/releases/tag/v2.5.0.9ac0e26).
|
||||
Antalet enheter av firmware version som synts i meshet de senaste 30 dagarna. Information om firmware information skickas inte över meshet. Men det går att härleda till viss del utifrån vilken information de paket som skickas innehåller.
|
||||
<div id="firmwareVersionContainer" class="stats-chart-container">
|
||||
<canvas id="firmwareVersionChart"></canvas>
|
||||
</div>
|
||||
|
||||
|
||||
## Batteri
|
||||
Visar genomsnittlig batterinivå av de noder som rapporterat batteri nivå och inte har fast strömförsörjning.
|
||||
<div id="batteryContainer" class="stats-chart-container" style="min-height:300px;">
|
||||
<canvas id="batteryChart"></canvas>
|
||||
## Max antal hopp
|
||||
Visar fördelningen av konfigurerade max-hopp för noder som synts de senaste 30 dagarna. För att värna om meshens stabilitet bör man använda ett så lågt antal max hops som möjlig. Används traceroute funktionen för att se hur många hops det krävs för dig att nå olika noder.
|
||||
<div id="maxHopsContainer" class="stats-chart-container">
|
||||
<canvas id="maxHopsChart"></canvas>
|
||||
</div>
|
||||
|
||||
## Unmessagable
|
||||
Visar antalet noder som är markerade som "unmessagable" respektive "messagable" baserat på de senaste 30 dagarnas aktivitet. Inställningen "unmessagable" används för att identifiera oövervakade infrastrukturnoder så att meddelanden inte skickas till noder som aldrig kommer att svara. Noder som inte har denna inställning definierad räknas som "messagable" eftersom denna egenskap infördes i version 2.6.8.
|
||||
|
||||
## Channel Utilization
|
||||
Visar den genomsnittliga Channel Utilization, det vill säga hur mycket radiofrekvensen används, baserat på rapporter från enheter i meshen. Eftersom enheter inte skickar telemetri när kanalutnyttjandet är högt kan siffran bli missvisande. Detsamma gäller för portabla enheter eller enheter som är placerade inomhus. Förhoppningsvis ger grafen ändå ett värdefullt underlag för att förstå hur meshen mår.
|
||||
<div id="channelUtilizationContainer" class="stats-chart-container" style="min-height:300px;">
|
||||
<canvas id="channelUtilizationChart"></canvas>
|
||||
<div id="isUnmessagableContainer" class="stats-chart-container">
|
||||
<canvas id="isUnmessagableChart"></canvas>
|
||||
</div>
|
||||
|
||||
## OK to MQTT
|
||||
Visar antalet noder som har "ok_to_mqtt" aktiverad eller avstängd under de senaste 30 dagarna. Flaggan är en begäran om att paket inte ska vidarebefordras till MQTT-servrar och karttjänster som sthlm-mesh. Begäran behöver dock inte respekteras. För att undvika exponering på karttjänster bör man använda krypterade kanaler istället för att dela information publikt.
|
||||
Äldre firmware skickar inte denna flagga och behandlas som falskt värde.
|
||||
|
||||
<div id="isOkToMqttContainer" class="stats-chart-container">
|
||||
<canvas id="isOkToMqttChart"></canvas>
|
||||
</div>
|
||||
|
||||
## Batteri - Solnoder
|
||||
Graf över batterinivå för utvalda solnoder senaste 30 dagarna. Har du en solnod som levererar data stabilt, säg till på discord så kan vi lägga till den.
|
||||
<div id="solarBatteryContainer" class="stats-chart-container" style="min-height:420px;">
|
||||
<canvas id="solarBatteryChart"></canvas>
|
||||
</div>
|
||||
|
||||
## Hårdvarumodeller
|
||||
Antalet enheter av respektive hårdvarutyp som synts i meshet de senaste 30 dagarna.
|
||||
@@ -122,4 +164,22 @@ Graferna baseras på data från [map.sthlm-mesh.se](https://map.sthlm-mesh.se),
|
||||
<script src="/js/status/nodes-seen.js"></script>
|
||||
<script src="/js/status/firmware-versions.js"></script>
|
||||
<script src="/js/status/battery-stats.js"></script>
|
||||
<script src="/js/status/channel-utilization.js"></script>
|
||||
<script src="/js/status/channel-utilization.js"></script>
|
||||
<script src="/js/status/channel-utilization-gauges.js"></script>
|
||||
<script src="/js/status/is-unmessagable-chart.js"></script>
|
||||
<script src="/js/status/is-ok-to-mqtt-chart.js"></script>
|
||||
<script src="/js/status/max-hops-chart.js"></script>
|
||||
<script src="/js/status/solar-battery-chart.js"></script>
|
||||
<script>
|
||||
// Initialize Bootstrap tooltips (delegated) so static and dynamic elements work
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
if (window.bootstrap && typeof window.bootstrap.Tooltip === 'function') {
|
||||
new window.bootstrap.Tooltip(document.body, {
|
||||
selector: "[data-bs-toggle='tooltip']",
|
||||
container: 'body'
|
||||
});
|
||||
} else {
|
||||
console.warn("Bootstrap Tooltip not available; falling back to native titles.");
|
||||
}
|
||||
});
|
||||
</script>
|
||||
@@ -26,6 +26,7 @@ pygmentsStyle: tango
|
||||
|
||||
# Configure how URLs look like per section.
|
||||
permalinks:
|
||||
blog: /blog/:year/:slug/
|
||||
|
||||
# Image processing configuration.
|
||||
imaging:
|
||||
@@ -56,8 +57,11 @@ markup:
|
||||
menu:
|
||||
main:
|
||||
- name: "Map"
|
||||
url: "https://map.sthlm-mesh.se/?lat=59.34338409949693&lng=378.0628967285156&zoom=10"
|
||||
url: "https://map.sthlm-mesh.se/"
|
||||
weight: 80
|
||||
- name: "Blogg"
|
||||
url: "/blog/"
|
||||
weight: 20
|
||||
|
||||
# Everything below this are Site Params
|
||||
params:
|
||||
|
||||
28
layouts/blog/baseof.html
Normal file
28
layouts/blog/baseof.html
Normal file
@@ -0,0 +1,28 @@
|
||||
<!doctype html>
|
||||
<html itemscope itemtype="http://schema.org/WebPage"
|
||||
{{- with .Site.Language.LanguageDirection }} dir="{{ . }}" {{- end -}}
|
||||
{{ with .Site.Language.Lang }} lang="{{ . }}" {{- end }} {{/**/ -}}
|
||||
class="no-js">
|
||||
<head>
|
||||
{{ partial "head.html" . }}
|
||||
</head>
|
||||
<body class="td-{{ .Kind }} td-blog {{- with .Page.Params.body_class }} {{ . }}{{ end }}">
|
||||
<header>
|
||||
{{ partial "navbar.html" . }}
|
||||
</header>
|
||||
<div class="container-fluid td-outer">
|
||||
<div class="td-main" {{- partialCached "td/scrollspy-attr.txt" . .Section | safeHTMLAttr }}>
|
||||
<div class="row flex-xl-nowrap">
|
||||
<aside class="col-12 col-md-3 col-xl-2 td-sidebar d-print-none">
|
||||
{{ partial "sidebar.html" . }}
|
||||
</aside>
|
||||
<main class="col-12 col-md-9 col-xl-8 ps-md-5 pe-md-4" role="main">
|
||||
{{ block "main" . }}{{ end }}
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
{{ partial "footer.html" . }}
|
||||
</div>
|
||||
{{ partial "scripts.html" . }}
|
||||
</body>
|
||||
</html>
|
||||
46
layouts/blog/list.html
Normal file
46
layouts/blog/list.html
Normal file
@@ -0,0 +1,46 @@
|
||||
{{ define "main" -}}
|
||||
|
||||
{{ with .Content }}{{ . }}{{ end -}}
|
||||
|
||||
|
||||
{{ if (and .Parent .Parent.IsHome) -}}
|
||||
{{ $.Scratch.Set "blog-pages" (where .Site.RegularPages "Section" .Section) -}}
|
||||
{{ else -}}
|
||||
{{$.Scratch.Set "blog-pages" .Pages -}}
|
||||
{{ end -}}
|
||||
|
||||
{{ if .Pages -}}
|
||||
<div class="td-blog-posts">
|
||||
{{ $pages := ($.Scratch.Get "blog-pages").ByWeight.ByDate }} <!-- weight → date -->
|
||||
{{ $pager := .Paginate ($pages.GroupByDate "2006") }}
|
||||
|
||||
{{ range $pager.PageGroups }}
|
||||
<ul class="td-blog-posts-list">
|
||||
{{ range .Pages }}
|
||||
<li class="td-blog-posts-list__item">
|
||||
<div class="td-blog-posts-list__body">
|
||||
<h1 class="mt-0 mb-1"><a href="{{ .RelPermalink }}" style="color: #30c965;
|
||||
text-decoration: none;font-weight: 700;">{{ .Title }}</a></h1>
|
||||
<p class="mb-2 mb-md-3"><small class="text-body-secondary">
|
||||
{{- .Date.Format ($.Param "time_format_blog") }} {{ T "ui_in"}} {{ .CurrentSection.LinkTitle -}}
|
||||
</small></p>
|
||||
<header class="article-meta">
|
||||
{{- partial "taxonomy_terms_article_wrapper.html" . -}}
|
||||
{{ if (and (not .Params.hide_readingtime) (.Site.Params.ui.readingtime.enable)) -}}
|
||||
{{- partial "reading-time.html" . -}}
|
||||
{{ end -}}
|
||||
</header>
|
||||
{{- partial "featured-image.html" (dict "p" . "w" 250 "h" 125 "class" "float-start me-3 pt-1 d-none d-md-block") -}}
|
||||
<p class="pt-0 mt-0">{{ .Plain | safeHTML | truncate 250 }}</p>
|
||||
<p class="pt-0"><a href="{{ .RelPermalink }}" aria-label="{{ T "ui_read_more"}} - {{ .LinkTitle }}">{{ T "ui_read_more"}}</a></p>
|
||||
</div>
|
||||
</li>
|
||||
{{ end -}}
|
||||
</ul>
|
||||
{{ end -}}
|
||||
</div>
|
||||
<div class="td-blog-posts__pagination">
|
||||
{{ partial "pagination.html" . -}}
|
||||
</div>
|
||||
{{- end -}}
|
||||
{{ end -}}
|
||||
29
layouts/blog/meetup_single.html
Normal file
29
layouts/blog/meetup_single.html
Normal file
@@ -0,0 +1,29 @@
|
||||
{{ define "main" -}}
|
||||
<div class="td-content">
|
||||
<h1>{{ .Title }}</h1>
|
||||
{{ with .Params.description }}<div class="lead">{{ . | markdownify }}</div>{{ end }}
|
||||
|
||||
{{/* Show meetup content without blog post metadata */}}
|
||||
{{ .Content }}
|
||||
|
||||
{{/* Optional: Add navigation to other meetups */}}
|
||||
<div class="mt-5 pt-3 border-top">
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
{{ with .PrevInSection }}
|
||||
<a href="{{ .RelPermalink }}" class="btn btn-outline-primary">
|
||||
← {{ .Title }}
|
||||
</a>
|
||||
{{ end }}
|
||||
</div>
|
||||
<div class="col text-end">
|
||||
{{ with .NextInSection }}
|
||||
<a href="{{ .RelPermalink }}" class="btn btn-outline-primary">
|
||||
{{ .Title }} →
|
||||
</a>
|
||||
{{ end }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{- end }}
|
||||
48
layouts/blog/meetups_list.html
Normal file
48
layouts/blog/meetups_list.html
Normal file
@@ -0,0 +1,48 @@
|
||||
{{ define "main" -}}
|
||||
|
||||
{{/* Show content only when there are no upcoming meetups */}}
|
||||
{{ if not .Pages -}}
|
||||
{{ with .Content }}{{ . }}{{ end -}}
|
||||
{{ else -}}
|
||||
{{/* If there are upcoming meetups, show only this title */}}
|
||||
<h1>{{ .Title }}</h1>
|
||||
{{ end -}}
|
||||
|
||||
{{ if (and .Parent .Parent.IsHome) -}}
|
||||
{{ $.Scratch.Set "blog-pages" (where .Site.RegularPages "Section" .Section) -}}
|
||||
{{ else -}}
|
||||
{{$.Scratch.Set "blog-pages" .Pages -}}
|
||||
{{ end -}}
|
||||
|
||||
{{ if .Pages -}}
|
||||
<div class="td-blog-posts">
|
||||
{{ $pages := ($.Scratch.Get "blog-pages").ByWeight.ByDate }} <!-- weight → date -->
|
||||
{{ $pager := .Paginate ($pages.GroupByDate "2006") }}
|
||||
|
||||
{{ range $pager.PageGroups }}
|
||||
<ul class="td-blog-posts-list">
|
||||
{{ range .Pages }}
|
||||
<li class="td-blog-posts-list__item">
|
||||
<div class="td-blog-posts-list__body">
|
||||
<h1 class="mt-0 mb-1"><a href="{{ .RelPermalink }}" style="color: #30c965;
|
||||
text-decoration: none;font-weight: 700;">{{ .Title }}</a></h1>
|
||||
<header class="article-meta">
|
||||
{{- partial "taxonomy_terms_article_wrapper.html" . -}}
|
||||
{{ if (and (not .Params.hide_readingtime) (.Site.Params.ui.readingtime.enable)) -}}
|
||||
{{- partial "reading-time.html" . -}}
|
||||
{{ end -}}
|
||||
</header>
|
||||
{{- partial "featured-image.html" (dict "p" . "w" 250 "h" 125 "class" "float-start me-3 pt-1 d-none d-md-block") -}}
|
||||
<p class="pt-0 mt-0">{{ .Plain | safeHTML | truncate 250 }}</p>
|
||||
<p class="pt-0"><a href="{{ .RelPermalink }}" aria-label="{{ T "ui_read_more"}} - {{ .LinkTitle }}">{{ T "ui_read_more"}}</a></p>
|
||||
</div>
|
||||
</li>
|
||||
{{ end -}}
|
||||
</ul>
|
||||
{{ end -}}
|
||||
</div>
|
||||
<div class="td-blog-posts__pagination">
|
||||
{{ partial "pagination.html" . -}}
|
||||
</div>
|
||||
{{- end -}}
|
||||
{{ end -}}
|
||||
26
layouts/partials/pagination.html
Normal file
26
layouts/partials/pagination.html
Normal file
@@ -0,0 +1,26 @@
|
||||
{{ $paginator := .Paginator -}}
|
||||
{{ if and $paginator (gt $paginator.TotalPages 1) -}}
|
||||
<nav aria-label="Pagination">
|
||||
<ul class="pagination">
|
||||
{{ if $paginator.HasPrev -}}
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="{{ $paginator.Prev.URL }}" aria-label="Previous">«</a>
|
||||
</li>
|
||||
{{ end -}}
|
||||
|
||||
{{ range $i, $p := $paginator.Pagers -}}
|
||||
<li class="page-item {{ if eq $p $paginator }}active{{ end }}">
|
||||
<a class="page-link" href="{{ $p.URL }}">{{ add $i 1 }}</a>
|
||||
</li>
|
||||
{{ end -}}
|
||||
|
||||
{{ if $paginator.HasNext -}}
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="{{ $paginator.Next.URL }}" aria-label="Next">»</a>
|
||||
</li>
|
||||
{{ end -}}
|
||||
</ul>
|
||||
</nav>
|
||||
{{ end -}}
|
||||
|
||||
|
||||
10
layouts/partials/td/scrollspy-attr.txt
Normal file
10
layouts/partials/td/scrollspy-attr.txt
Normal file
@@ -0,0 +1,10 @@
|
||||
{{ if not (.Param "ui.scrollspy.disable") -}}
|
||||
{{ replaceRE `\s+` " "
|
||||
`
|
||||
data-bs-spy="scroll"
|
||||
data-bs-target="#TableOfContents"
|
||||
data-bs-root-margin="0px 0px -40%"
|
||||
`
|
||||
| strings.TrimSpace | add " " -}}
|
||||
|
||||
{{ end -}}
|
||||
224
layouts/shortcodes/image-compare.html
Normal file
224
layouts/shortcodes/image-compare.html
Normal file
@@ -0,0 +1,224 @@
|
||||
<!-- Image comparison slider shortcode -->
|
||||
<div class="image-compare-container" data-left-image="{{ .Get "left" }}" data-right-image="{{ .Get "right" }}">
|
||||
<div class="image-compare">
|
||||
<div class="image-compare-left">
|
||||
<img src="{{ .Get "left" }}" alt="{{ .Get "left-alt" | default "Before image" }}" loading="lazy">
|
||||
</div>
|
||||
<div class="image-compare-right">
|
||||
<img src="{{ .Get "right" }}" alt="{{ .Get "right-alt" | default "After image" }}" loading="lazy">
|
||||
</div>
|
||||
<div class="image-compare-slider">
|
||||
<div class="image-compare-handle">
|
||||
<div class="image-compare-handle-line"></div>
|
||||
<div class="image-compare-handle-circle">
|
||||
<span class="handle-arrow-left">‹</span>
|
||||
<span class="handle-arrow-right">›</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{ if .Get "caption" }}
|
||||
<p class="image-compare-caption">{{ .Get "caption" }}</p>
|
||||
{{ end }}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.image-compare-container {
|
||||
margin: 2rem 0;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.image-compare {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
user-select: none;
|
||||
cursor: col-resize;
|
||||
}
|
||||
|
||||
.image-compare-left,
|
||||
.image-compare-right {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.image-compare-left {
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.image-compare-right {
|
||||
z-index: 2;
|
||||
clip-path: inset(0 0 0 50%);
|
||||
}
|
||||
|
||||
.image-compare img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.image-compare-slider {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 50%;
|
||||
width: 2px;
|
||||
height: 100%;
|
||||
background: white;
|
||||
z-index: 3;
|
||||
transform: translateX(-50%);
|
||||
cursor: col-resize;
|
||||
}
|
||||
|
||||
.image-compare-handle {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
z-index: 4;
|
||||
}
|
||||
|
||||
.image-compare-handle-line {
|
||||
width: 2px;
|
||||
height: 60px;
|
||||
background: white;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.image-compare-handle-circle {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 50%;
|
||||
background: white;
|
||||
border: 2px solid #007bff;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
|
||||
position: relative;
|
||||
cursor: col-resize;
|
||||
}
|
||||
|
||||
.handle-arrow-left,
|
||||
.handle-arrow-right {
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
color: #007bff;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.handle-arrow-left {
|
||||
left: 8px;
|
||||
}
|
||||
|
||||
.handle-arrow-right {
|
||||
right: 8px;
|
||||
}
|
||||
|
||||
.image-compare-caption {
|
||||
text-align: center;
|
||||
font-style: italic;
|
||||
color: #666;
|
||||
margin-top: 1rem;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
/* Mobile responsiveness */
|
||||
@media (max-width: 768px) {
|
||||
.image-compare-handle-circle {
|
||||
width: 35px;
|
||||
height: 35px;
|
||||
}
|
||||
|
||||
.handle-arrow-left,
|
||||
.handle-arrow-right {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.handle-arrow-left {
|
||||
left: 6px;
|
||||
}
|
||||
|
||||
.handle-arrow-right {
|
||||
right: 6px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const containers = document.querySelectorAll('.image-compare-container');
|
||||
|
||||
containers.forEach(container => {
|
||||
const compare = container.querySelector('.image-compare');
|
||||
const leftImage = container.querySelector('.image-compare-left img');
|
||||
const rightDiv = container.querySelector('.image-compare-right');
|
||||
const slider = container.querySelector('.image-compare-slider');
|
||||
let isResizing = false;
|
||||
|
||||
// Set initial height based on image aspect ratio
|
||||
leftImage.addEventListener('load', function() {
|
||||
const aspectRatio = this.naturalHeight / this.naturalWidth;
|
||||
compare.style.paddingBottom = (aspectRatio * 100) + '%';
|
||||
compare.style.height = '0';
|
||||
compare.style.position = 'relative';
|
||||
});
|
||||
|
||||
// Mouse events
|
||||
function startResize(e) {
|
||||
isResizing = true;
|
||||
document.body.style.cursor = 'col-resize';
|
||||
e.preventDefault();
|
||||
}
|
||||
|
||||
function stopResize() {
|
||||
isResizing = false;
|
||||
document.body.style.cursor = 'default';
|
||||
}
|
||||
|
||||
function handleResize(e) {
|
||||
if (!isResizing) return;
|
||||
|
||||
const rect = compare.getBoundingClientRect();
|
||||
const x = e.clientX - rect.left;
|
||||
const percentage = Math.max(0, Math.min(100, (x / rect.width) * 100));
|
||||
|
||||
slider.style.left = percentage + '%';
|
||||
rightDiv.style.clipPath = `inset(0 0 0 ${percentage}%)`;
|
||||
}
|
||||
|
||||
// Touch events for mobile
|
||||
function handleTouchResize(e) {
|
||||
if (!isResizing) return;
|
||||
|
||||
const rect = compare.getBoundingClientRect();
|
||||
const x = e.touches[0].clientX - rect.left;
|
||||
const percentage = Math.max(0, Math.min(100, (x / rect.width) * 100));
|
||||
|
||||
slider.style.left = percentage + '%';
|
||||
rightDiv.style.clipPath = `inset(0 0 0 ${percentage}%)`;
|
||||
}
|
||||
|
||||
// Event listeners
|
||||
slider.addEventListener('mousedown', startResize);
|
||||
document.addEventListener('mousemove', handleResize);
|
||||
document.addEventListener('mouseup', stopResize);
|
||||
|
||||
// Touch events
|
||||
slider.addEventListener('touchstart', startResize);
|
||||
document.addEventListener('touchmove', handleTouchResize);
|
||||
document.addEventListener('touchend', stopResize);
|
||||
|
||||
// Prevent image dragging
|
||||
compare.addEventListener('dragstart', e => e.preventDefault());
|
||||
});
|
||||
});
|
||||
</script>
|
||||
@@ -36,13 +36,13 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"autoprefixer": "^10.4.20",
|
||||
"cross-env": "^7.0.3",
|
||||
"cross-env": "^10.0.0",
|
||||
"hugo-extended": "0.145.0",
|
||||
"postcss-cli": "^11.0.0",
|
||||
"rtlcss": "^4.3.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"npm-check-updates": "^18.0.1"
|
||||
"npm-check-updates": "^19.0.0"
|
||||
},
|
||||
"private": true,
|
||||
"prettier": {
|
||||
|
||||
119
static/events/2025-08-19-aw-telefonplan.json
Normal file
119
static/events/2025-08-19-aw-telefonplan.json
Normal file
@@ -0,0 +1,119 @@
|
||||
{
|
||||
"messagePattern": "AW 19/8",
|
||||
"archived": true,
|
||||
"exportedAt": "2025-08-20T19:11:59.376Z",
|
||||
"description": "AW Telefonplan meetup",
|
||||
"attendees": {
|
||||
"yes": [
|
||||
{
|
||||
"nodeId": "364861217",
|
||||
"shortName": "KAKd",
|
||||
"longName": "Kladdkakd",
|
||||
"response": "yes",
|
||||
"timestamp": "2025-08-18T21:55:44.225Z"
|
||||
},
|
||||
{
|
||||
"nodeId": "910104769",
|
||||
"shortName": "TXD0",
|
||||
"longName": "Wibbe",
|
||||
"response": "yes",
|
||||
"timestamp": "2025-08-18T20:01:19.763Z"
|
||||
},
|
||||
{
|
||||
"nodeId": "2745076674",
|
||||
"shortName": "TWK5",
|
||||
"longName": "TWK-Mobil T114",
|
||||
"response": "yes",
|
||||
"timestamp": "2025-08-18T18:13:36.385Z"
|
||||
},
|
||||
{
|
||||
"nodeId": "1258726557",
|
||||
"shortName": "JwKC",
|
||||
"longName": "JwK Car",
|
||||
"response": "yes",
|
||||
"timestamp": "2025-08-18T11:51:05.986Z"
|
||||
},
|
||||
{
|
||||
"nodeId": "3681979732",
|
||||
"shortName": "Ros",
|
||||
"longName": "Ros Mobil",
|
||||
"response": "yes",
|
||||
"timestamp": "2025-08-15T05:19:51.846Z"
|
||||
},
|
||||
{
|
||||
"nodeId": "762864438",
|
||||
"shortName": "TUFx",
|
||||
"longName": "Lasse",
|
||||
"response": "yes",
|
||||
"timestamp": "2025-08-15T04:15:40.832Z"
|
||||
},
|
||||
{
|
||||
"nodeId": "2718571204",
|
||||
"shortName": "DXD3",
|
||||
"longName": "DXD3 Ruggen",
|
||||
"response": "yes",
|
||||
"timestamp": "2025-08-14T19:33:19.153Z"
|
||||
},
|
||||
{
|
||||
"nodeId": "1422344156",
|
||||
"shortName": "TELE",
|
||||
"longName": "MDG Telefonplan",
|
||||
"response": "yes",
|
||||
"timestamp": "2025-08-14T19:19:56.688Z"
|
||||
},
|
||||
{
|
||||
"nodeId": "2152997845",
|
||||
"shortName": "MDG5",
|
||||
"longName": "MDG5 /R1",
|
||||
"response": "yes",
|
||||
"timestamp": "2025-08-14T19:16:29.975Z"
|
||||
}
|
||||
],
|
||||
"maybe": [
|
||||
{
|
||||
"nodeId": "220489446",
|
||||
"shortName": "SolS",
|
||||
"longName": "SolarStation",
|
||||
"response": "maybe",
|
||||
"timestamp": "2025-08-17T07:26:51.406Z"
|
||||
},
|
||||
{
|
||||
"nodeId": "2892741592",
|
||||
"shortName": "bbd8",
|
||||
"longName": "Meshtastic bbd8",
|
||||
"response": "maybe",
|
||||
"timestamp": "2025-08-16T11:48:12.544Z"
|
||||
},
|
||||
{
|
||||
"nodeId": "4102046320",
|
||||
"shortName": "DLTA",
|
||||
"longName": "Delta Flyer",
|
||||
"response": "maybe",
|
||||
"timestamp": "2025-08-14T21:23:49.942Z"
|
||||
},
|
||||
{
|
||||
"nodeId": "2879616005",
|
||||
"shortName": "JlyT",
|
||||
"longName": "Jelly Test",
|
||||
"response": "maybe",
|
||||
"timestamp": "2025-08-14T20:48:10.687Z"
|
||||
}
|
||||
],
|
||||
"no": [
|
||||
{
|
||||
"nodeId": "2956846808",
|
||||
"shortName": "LSD",
|
||||
"longName": "R∆dioW∆ve🇸🇪",
|
||||
"response": "no",
|
||||
"timestamp": "2025-08-19T11:39:03.262Z"
|
||||
},
|
||||
{
|
||||
"nodeId": "2947306466",
|
||||
"shortName": "CVK",
|
||||
"longName": "SA0CVK Primary",
|
||||
"response": "no",
|
||||
"timestamp": "2025-08-18T12:51:47.594Z"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
104
static/events/2025-10-01-sundbyberg.json
Normal file
104
static/events/2025-10-01-sundbyberg.json
Normal file
@@ -0,0 +1,104 @@
|
||||
{
|
||||
"messagePattern": "AW 1/10",
|
||||
"description": "Meetup 1 Oktober - Sundbyberg",
|
||||
"exportedAt": "2025-09-28T10:59:49.506Z",
|
||||
"attendees": {
|
||||
"yes": [
|
||||
{
|
||||
"nodeId": "2665944228",
|
||||
"shortName": "TPH1",
|
||||
"longName": "TPH-1 Täby",
|
||||
"response": "yes",
|
||||
"timestamp": "2025-09-27T11:18:40.725Z"
|
||||
},
|
||||
{
|
||||
"nodeId": "1422344156",
|
||||
"shortName": "TELE",
|
||||
"longName": "MDG Telefonplan",
|
||||
"response": "yes",
|
||||
"timestamp": "2025-09-26T23:07:19.802Z"
|
||||
},
|
||||
{
|
||||
"nodeId": "2718571204",
|
||||
"shortName": "DXD3",
|
||||
"longName": "DXD3 Ruggen",
|
||||
"response": "yes",
|
||||
"timestamp": "2025-09-26T20:14:58.613Z"
|
||||
},
|
||||
{
|
||||
"nodeId": "2947306466",
|
||||
"shortName": "CVK",
|
||||
"longName": "SA0CVK Primary",
|
||||
"response": "yes",
|
||||
"timestamp": "2025-09-26T19:53:44.302Z"
|
||||
},
|
||||
{
|
||||
"nodeId": "2879616005",
|
||||
"shortName": "JlyT",
|
||||
"longName": "Jelly Test",
|
||||
"response": "yes",
|
||||
"timestamp": "2025-09-26T14:25:11.116Z"
|
||||
},
|
||||
{
|
||||
"nodeId": "1128157388",
|
||||
"shortName": "varg",
|
||||
"longName": "vargtastic",
|
||||
"response": "yes",
|
||||
"timestamp": "2025-09-25T16:47:09.777Z"
|
||||
},
|
||||
{
|
||||
"nodeId": "3681979732",
|
||||
"shortName": "Ros",
|
||||
"longName": "Ros Mobil",
|
||||
"response": "yes",
|
||||
"timestamp": "2025-09-24T16:02:08.895Z"
|
||||
},
|
||||
{
|
||||
"nodeId": "1978245180",
|
||||
"shortName": "CVK",
|
||||
"longName": "SA0CVK 0 (Ny Router)",
|
||||
"response": "yes",
|
||||
"timestamp": "2025-09-22T17:06:11.296Z"
|
||||
},
|
||||
{
|
||||
"nodeId": "3677336824",
|
||||
"shortName": "Chip",
|
||||
"longName": "Chip",
|
||||
"response": "yes",
|
||||
"timestamp": "2025-09-22T17:00:53.857Z"
|
||||
},
|
||||
{
|
||||
"nodeId": "3655315140",
|
||||
"shortName": "Ros9",
|
||||
"longName": "Roslund9",
|
||||
"response": "yes",
|
||||
"timestamp": "2025-09-22T05:37:43.576Z"
|
||||
},
|
||||
{
|
||||
"nodeId": "1222480744",
|
||||
"shortName": "JwKL",
|
||||
"longName": "JwK LF",
|
||||
"response": "yes",
|
||||
"timestamp": "2025-09-21T20:54:17.235Z"
|
||||
}
|
||||
],
|
||||
"maybe": [
|
||||
{
|
||||
"nodeId": "364861217",
|
||||
"shortName": "KAKd",
|
||||
"longName": "Kladdkakd",
|
||||
"response": "maybe",
|
||||
"timestamp": "2025-09-22T22:00:55.197Z"
|
||||
}
|
||||
],
|
||||
"no": [
|
||||
{
|
||||
"nodeId": "3946133666",
|
||||
"shortName": "phPi",
|
||||
"longName": "ph Raspberry Pi",
|
||||
"response": "no",
|
||||
"timestamp": "2025-09-26T20:11:29.025Z"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
125
static/events/2025-11-26-aw-gamlastan.json
Normal file
125
static/events/2025-11-26-aw-gamlastan.json
Normal file
@@ -0,0 +1,125 @@
|
||||
{
|
||||
"messagePattern": "AW 26/11",
|
||||
"archived": true,
|
||||
"description": "AW Gamla Stan",
|
||||
"exportedAt": "2025-12-13T09:09:07.721Z",
|
||||
"attendees": {
|
||||
"yes": [
|
||||
{
|
||||
"nodeId": "3633204928",
|
||||
"shortName": "CVJ1",
|
||||
"longName": "SA0CVJ",
|
||||
"response": "yes",
|
||||
"timestamp": "2025-11-26T12:54:32.633Z"
|
||||
},
|
||||
{
|
||||
"nodeId": "3025077625",
|
||||
"shortName": "0579",
|
||||
"longName": "Hundmannen",
|
||||
"response": "yes",
|
||||
"timestamp": "2025-11-25T21:22:01.985Z"
|
||||
},
|
||||
{
|
||||
"nodeId": "329721520",
|
||||
"shortName": "Raj",
|
||||
"longName": "Rasmus J",
|
||||
"response": "yes",
|
||||
"timestamp": "2025-11-25T14:01:37.321Z"
|
||||
},
|
||||
{
|
||||
"nodeId": "2882571286",
|
||||
"shortName": "Luho",
|
||||
"longName": "Lura home",
|
||||
"response": "yes",
|
||||
"timestamp": "2025-11-25T14:00:58.704Z"
|
||||
},
|
||||
{
|
||||
"nodeId": "4059642829",
|
||||
"shortName": "TPH2",
|
||||
"longName": "TPH-2 Mobil ",
|
||||
"response": "yes",
|
||||
"timestamp": "2025-11-24T12:26:36.901Z"
|
||||
},
|
||||
{
|
||||
"nodeId": "3087528628",
|
||||
"shortName": "mTag",
|
||||
"longName": "mariosTag",
|
||||
"response": "yes",
|
||||
"timestamp": "2025-11-21T10:37:19.531Z"
|
||||
},
|
||||
{
|
||||
"nodeId": "2523286013",
|
||||
"shortName": "vrg2",
|
||||
"longName": "Mobile varg",
|
||||
"response": "yes",
|
||||
"timestamp": "2025-11-20T23:05:55.157Z"
|
||||
},
|
||||
{
|
||||
"nodeId": "610439448",
|
||||
"shortName": "MDG6",
|
||||
"longName": "MDG6 /TAG",
|
||||
"response": "yes",
|
||||
"timestamp": "2025-11-20T00:28:16.777Z"
|
||||
},
|
||||
{
|
||||
"nodeId": "1044962682",
|
||||
"shortName": "Trkb",
|
||||
"longName": "Trekblast",
|
||||
"response": "yes",
|
||||
"timestamp": "2025-11-19T21:23:41.195Z"
|
||||
},
|
||||
{
|
||||
"nodeId": "1770352332",
|
||||
"shortName": "6ecc",
|
||||
"longName": "JwK Car",
|
||||
"response": "yes",
|
||||
"timestamp": "2025-11-19T21:10:33.678Z"
|
||||
},
|
||||
{
|
||||
"nodeId": "2171230534",
|
||||
"shortName": "Oct",
|
||||
"longName": "BuntBand",
|
||||
"response": "yes",
|
||||
"timestamp": "2025-11-19T18:05:25.449Z"
|
||||
},
|
||||
{
|
||||
"nodeId": "3663385928",
|
||||
"shortName": "Ros1",
|
||||
"longName": "Mariehäll Alprosen",
|
||||
"response": "yes",
|
||||
"timestamp": "2025-11-19T17:14:14.914Z"
|
||||
}
|
||||
],
|
||||
"maybe": [
|
||||
{
|
||||
"nodeId": "2102445703",
|
||||
"shortName": "0174",
|
||||
"longName": "Rhutsunda",
|
||||
"response": "maybe",
|
||||
"timestamp": "2025-11-27T01:31:48.208Z"
|
||||
},
|
||||
{
|
||||
"nodeId": "2681582544",
|
||||
"shortName": "RydT",
|
||||
"longName": "RydboholmTracker 🛰️📡",
|
||||
"response": "maybe",
|
||||
"timestamp": "2025-11-26T16:11:00.942Z"
|
||||
},
|
||||
{
|
||||
"nodeId": "2068295693",
|
||||
"shortName": "🤳",
|
||||
"longName": "q-biq mob",
|
||||
"response": "maybe",
|
||||
"timestamp": "2025-11-25T14:52:20.570Z"
|
||||
},
|
||||
{
|
||||
"nodeId": "2658667716",
|
||||
"shortName": "BMKA",
|
||||
"longName": "Bromma Kyrka ",
|
||||
"response": "maybe",
|
||||
"timestamp": "2025-11-23T17:23:09.142Z"
|
||||
}
|
||||
],
|
||||
"no": []
|
||||
}
|
||||
}
|
||||
9
static/events/2026-02-04-aw-mariatorget.json
Normal file
9
static/events/2026-02-04-aw-mariatorget.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"messagePattern": "AW 4/2",
|
||||
"description": "En kort beskrivning av meetupet",
|
||||
"attendees": {
|
||||
"yes": [],
|
||||
"maybe": [],
|
||||
"no": []
|
||||
}
|
||||
}
|
||||
9
static/events/template.json
Normal file
9
static/events/template.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"messagePattern": "Ett unikt id för anmälan. exempel: AW 19/8",
|
||||
"description": "En kort beskrivning av meetupet",
|
||||
"attendees": {
|
||||
"yes": [],
|
||||
"maybe": [],
|
||||
"no": []
|
||||
}
|
||||
}
|
||||
BIN
static/firmware/bleota-s3.bin
Normal file
BIN
static/firmware/bleota-s3.bin
Normal file
Binary file not shown.
21
static/firmware/firmware.json
Normal file
21
static/firmware/firmware.json
Normal file
@@ -0,0 +1,21 @@
|
||||
[
|
||||
{
|
||||
"hwModelSlug": "rak4631",
|
||||
"architecture": "nrf52",
|
||||
"displayName": "RAK WisBlock 4631",
|
||||
"versions": ["2.6.11.60ec05e", "2.6.4.b89355f"]
|
||||
},
|
||||
{
|
||||
"hwModelSlug": "tlora-t3s3-v1",
|
||||
"architecture": "esp32",
|
||||
"displayName": "LILYGO T-LoRa T3-S3",
|
||||
"versions": ["2.6.11.60ec05e53", "2.7.10.94d4bdf"]
|
||||
},
|
||||
{
|
||||
"hwModelSlug": "heltec-v3",
|
||||
"architecture": "esp32",
|
||||
"displayName": "Heltec V3",
|
||||
"partitionScheme": "8MB",
|
||||
"versions": ["2.6.11.60ec05e53"]
|
||||
}
|
||||
]
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
static/images/blog/2025-09-19-switch-to-mediumfast-lf.png
Normal file
BIN
static/images/blog/2025-09-19-switch-to-mediumfast-lf.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.7 MiB |
BIN
static/images/blog/2025-09-19-switch-to-mediumfast-mf.png
Normal file
BIN
static/images/blog/2025-09-19-switch-to-mediumfast-mf.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.6 MiB |
BIN
static/images/blog/2026-01-03-traceroute-demo.mp4
Normal file
BIN
static/images/blog/2026-01-03-traceroute-demo.mp4
Normal file
Binary file not shown.
85
static/js/esp-flasher.js
Normal file
85
static/js/esp-flasher.js
Normal file
@@ -0,0 +1,85 @@
|
||||
async function flashFirmware(board, version, fullEraseInstall) {
|
||||
const { ESPLoader, Transport } = await import('https://unpkg.com/esptool-js/bundle.js')
|
||||
const logBox = document.getElementById('espLog');
|
||||
|
||||
|
||||
// Helper functions
|
||||
function log(...m) {
|
||||
logBox.textContent+=m.join(' ')+'\n';logBox.scrollTop=logBox.scrollHeight;
|
||||
}
|
||||
|
||||
function convertToBinaryString(bytes) {
|
||||
let binaryString = "";
|
||||
for (let i = 0; i < bytes.length; i++) {
|
||||
binaryString += String.fromCharCode(bytes[i]);
|
||||
}
|
||||
return binaryString;
|
||||
}
|
||||
|
||||
async function fetchBinaryContent(url) {
|
||||
const response = await fetch(url);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch ${url}: ${response.status} ${response.statusText}`);
|
||||
}
|
||||
|
||||
const buffer = await response.arrayBuffer();
|
||||
return convertToBinaryString(new Uint8Array(buffer));
|
||||
}
|
||||
|
||||
|
||||
try {
|
||||
log(`\n=== ${board.displayName} · ${version} (${fullEraseInstall ? 'full' : 'update'}) ===`);
|
||||
|
||||
let port = await navigator.serial.requestPort({});
|
||||
let transport = new Transport(port, true);
|
||||
let loader = new ESPLoader({
|
||||
transport: transport,
|
||||
baudrate: 460800,
|
||||
terminal:{ writeLine:log, write:log, clean(){} }
|
||||
});
|
||||
|
||||
await loader.main();
|
||||
log('Connected.');
|
||||
|
||||
let fileArray, eraseAll = false;
|
||||
|
||||
if (!fullEraseInstall) {
|
||||
// UPDATE – write app only
|
||||
const app = await fetchBinaryContent(`/firmware/${board.hwModelSlug}/${version}/firmware-${board.hwModelSlug}-${version}-update.bin`);
|
||||
|
||||
fileArray = [
|
||||
{ address: 0x10000, data: app }
|
||||
];
|
||||
} else {
|
||||
// FULL ERASE, and firmware + littlefs flash
|
||||
// ota is share, need to af logic if supporting other devices...
|
||||
let otaOffset=0x260000, spiffsOffset=0x300000;
|
||||
if (board.partitionScheme==='8MB'){ otaOffset=0x340000; spiffsOffset=0x670000; }
|
||||
if (board.partitionScheme==='16MB'){otaOffset=0x650000; spiffsOffset=0xC90000; }
|
||||
|
||||
const factory = await fetchBinaryContent(`/firmware/${board.hwModelSlug}/${version}/firmware-${board.hwModelSlug}-${version}.bin`);
|
||||
const ota = await fetchBinaryContent(`/firmware/bleota-s3.bin`);
|
||||
const lfs = await fetchBinaryContent(`/firmware/${board.hwModelSlug}/${version}/littlefs-${board.hwModelSlug}-${version}.bin`);
|
||||
|
||||
fileArray = [
|
||||
{ address: 0x0000, data: factory },
|
||||
{ address: otaOffset, data: ota },
|
||||
{ address: spiffsOffset,data: lfs }
|
||||
];
|
||||
eraseAll = true;
|
||||
}
|
||||
|
||||
const flashOptions = {
|
||||
fileArray: fileArray,
|
||||
flashSize: 'keep',
|
||||
eraseAll: eraseAll,
|
||||
compress: true,
|
||||
flashMode: 'keep',
|
||||
flashFreq: 'keep',
|
||||
};
|
||||
|
||||
await loader.writeFlash(flashOptions);
|
||||
|
||||
} catch(e){ console.error(e); log('❌ Error:', e); }
|
||||
}
|
||||
117
static/js/firmware-ui.js
Normal file
117
static/js/firmware-ui.js
Normal file
@@ -0,0 +1,117 @@
|
||||
let boards = [];
|
||||
let selectedBoard = {};
|
||||
let selectedVersion = '';
|
||||
|
||||
fetch('/firmware/firmware.json')
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
boards = data;
|
||||
showFirmwareAccordion(boards);
|
||||
});
|
||||
|
||||
async function showFirmwareAccordion(boards) {
|
||||
const accordion = document.getElementById('firmwareAccordion');
|
||||
|
||||
boards.forEach((board, idx) => {
|
||||
const items = board.versions.map(version => {
|
||||
if (board.architecture === 'nrf52') {
|
||||
return renderNRF52Item(board.hwModelSlug, version);
|
||||
}
|
||||
if (board.architecture === 'esp32') {
|
||||
return renderESP32Item(board.hwModelSlug, version);
|
||||
}
|
||||
}).join('');
|
||||
|
||||
const extra = (board.architecture === 'nrf52') ? renderNRF52EraseItem() : '';
|
||||
|
||||
accordion.insertAdjacentHTML('beforeend', renderAccordionItem(board, items + extra, idx === 0));
|
||||
});
|
||||
}
|
||||
|
||||
// ── Template renderers ──
|
||||
function renderNRF52Item(hwModelSlug, version) {
|
||||
return `
|
||||
<li class="list-group-item d-flex justify-content-between align-items-center">
|
||||
<span>${hwModelSlug}-${version}</span>
|
||||
<span>
|
||||
<a class="btn btn-sm btn-outline-secondary me-2"
|
||||
href="/firmware/${hwModelSlug}/${version}/${hwModelSlug}-${version}-ota.zip">Download OTA</a>
|
||||
<a class="btn btn-sm btn-outline-primary"
|
||||
href="/firmware/${hwModelSlug}/${version}/${hwModelSlug}-${version}.uf2">Download UF2</a>
|
||||
</span>
|
||||
</li>`;
|
||||
}
|
||||
|
||||
function renderESP32Item(hwModelSlug, version) {
|
||||
return `
|
||||
<li class="list-group-item d-flex justify-content-between align-items-center">
|
||||
<span>${version}</span>
|
||||
<span>
|
||||
<button class="btn btn-sm btn-outline-primary open-modal-btn"
|
||||
data-board="${hwModelSlug}" data-version="${version}">Flash
|
||||
</button>
|
||||
</span>
|
||||
</li>`;
|
||||
}
|
||||
|
||||
function renderNRF52EraseItem() {
|
||||
return `
|
||||
<li class="list-group-item d-flex justify-content-between align-items-center">
|
||||
<span>nrf_erase2.uf2</span>
|
||||
<a class="btn btn-sm btn-outline-danger"
|
||||
href="/firmware/nrf_erase2.uf2">Download UF2</a>
|
||||
</li>`;
|
||||
}
|
||||
|
||||
function renderAccordionItem(board, bodyHtml, isOpen) {
|
||||
return `
|
||||
<div class="accordion-item">
|
||||
<h2 class="accordion-header">
|
||||
<button class="accordion-button ${isOpen ? '' : 'collapsed'}" type="button"
|
||||
data-bs-toggle="collapse" data-bs-target="#body-${board.hwModelSlug}"
|
||||
aria-expanded="${isOpen}" aria-controls="body-${board.hwModelSlug}">
|
||||
<strong>${board.displayName}</strong>
|
||||
</button>
|
||||
</h2>
|
||||
<div id="body-${board.hwModelSlug}" class="accordion-collapse collapse ${isOpen ? 'show' : ''}">
|
||||
<div class="accordion-body">
|
||||
<ul class="list-group list-group-flush">
|
||||
${bodyHtml}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
|
||||
// ── Modal logic ──
|
||||
document.addEventListener('click', ev => {
|
||||
if (!ev.target.matches('.open-modal-btn')) return;
|
||||
|
||||
|
||||
const modalEl = document.getElementById('flashModal');
|
||||
const flashModal = new bootstrap.Modal(modalEl);
|
||||
const titleEl = document.getElementById('flashModalLabel');
|
||||
const eraseChk = document.getElementById('eraseSwitch');
|
||||
const logBox = document.getElementById('espLog');
|
||||
|
||||
let currentBoardSlug = ev.target.dataset.board;
|
||||
selectedVersion = ev.target.dataset.version;
|
||||
selectedBoard = boards.find(b => b.hwModelSlug === currentBoardSlug);
|
||||
|
||||
titleEl.textContent = `Flash – ${selectedBoard.displayName} ${selectedVersion}`;
|
||||
eraseChk.checked = false;
|
||||
logBox.textContent = '';
|
||||
|
||||
flashModal.show();
|
||||
});
|
||||
|
||||
// ── Start Flash Button ──
|
||||
const startBtn = document.getElementById('startFlashBtn');
|
||||
startBtn.addEventListener('click', async () => {
|
||||
const fullEraseInstall = document.getElementById('eraseSwitch').checked;
|
||||
|
||||
startBtn.disabled = true;
|
||||
await flashFirmware(selectedBoard, selectedVersion, fullEraseInstall);
|
||||
startBtn.disabled = false;
|
||||
});
|
||||
@@ -6,6 +6,16 @@ document.addEventListener("DOMContentLoaded", async function () {
|
||||
messagesList.style="max-height:70vh;overflow: auto;"
|
||||
messagesContainer.appendChild(messagesList);
|
||||
|
||||
// Initialize Bootstrap tooltips (delegated) so static and dynamic elements work
|
||||
if (window.bootstrap && typeof window.bootstrap.Tooltip === "function") {
|
||||
new window.bootstrap.Tooltip(document.body, {
|
||||
selector: "[data-bs-toggle='tooltip']",
|
||||
container: "body"
|
||||
});
|
||||
} else {
|
||||
console.warn("Bootstrap Tooltip not available; falling back to native titles.");
|
||||
}
|
||||
|
||||
const state = {
|
||||
messages: [],
|
||||
nodesById: {},
|
||||
@@ -18,33 +28,50 @@ document.addEventListener("DOMContentLoaded", async function () {
|
||||
|
||||
async function fetchMessages() {
|
||||
try {
|
||||
let url = "https://map.sthlm-mesh.se/api/v1/text-messages?order=desc";
|
||||
let url = "https://map.sthlm-mesh.se/api/v1/text-messages?order=desc&count=300";
|
||||
const selectedRadio = document.querySelector("input[name='btnradio']:checked");
|
||||
|
||||
//If all is not selected we get 15 messages. If not we get 35 due to duplicates
|
||||
if (selectedRadio.id !== "all") {
|
||||
url += `&gateway_id=${selectedRadio.id}`;
|
||||
url += `&count=15`;
|
||||
} else {
|
||||
url += `&count=35`;
|
||||
}
|
||||
|
||||
const response = await fetch(url);
|
||||
const data = await response.json();
|
||||
|
||||
//Filter duplicate messages
|
||||
state.messages = Array.from(
|
||||
new Map(data.text_messages.map(msg => [msg.packet_id, msg])).values()
|
||||
);
|
||||
|
||||
// Group by packet_id and collect all gateways that heard the packet (with hops left)
|
||||
const byPacket = new Map();
|
||||
for (const msg of data.text_messages) {
|
||||
let entry = byPacket.get(msg.packet_id);
|
||||
if (!entry) {
|
||||
entry = { message: msg, gateways: new Map() };
|
||||
byPacket.set(msg.packet_id, entry);
|
||||
}
|
||||
// store the latest hop_limit seen for this gateway_id
|
||||
entry.gateways.set(msg.gateway_id, msg.hop_limit);
|
||||
}
|
||||
|
||||
// Build aggregated messages with gateways array [{ gateway_id, hop_limit }]
|
||||
let aggregated = Array.from(byPacket.values()).map(entry => ({
|
||||
...entry.message,
|
||||
gateways: Array.from(entry.gateways.entries()).map(([gateway_id, hop_limit]) => ({ gateway_id, hop_limit }))
|
||||
}));
|
||||
|
||||
// If a specific gateway is selected, filter to messages heard by that gateway
|
||||
if (selectedRadio && selectedRadio.id !== "all") {
|
||||
const selectedGatewayId = selectedRadio.id;
|
||||
aggregated = aggregated.filter(m => Array.isArray(m.gateways) && m.gateways.some(g => g.gateway_id?.toString() === selectedGatewayId));
|
||||
}
|
||||
|
||||
state.messages = aggregated;
|
||||
|
||||
// Sort the messages, for some reason with multiple mqtt gateways they are unsorted.
|
||||
state.messages.sort((a, b) => new Date(b.created_at) - new Date(a.created_at));
|
||||
|
||||
// fetch node info
|
||||
for(const message of state.messages){
|
||||
for (const message of state.messages) {
|
||||
await fetchNodeInfo(message.to);
|
||||
await fetchNodeInfo(message.from);
|
||||
await fetchNodeInfo(message.gateway_id);
|
||||
if (Array.isArray(message.gateways)) {
|
||||
for (const g of message.gateways) {
|
||||
await fetchNodeInfo(g.gateway_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
updateUI();
|
||||
@@ -95,12 +122,17 @@ document.addEventListener("DOMContentLoaded", async function () {
|
||||
<div class="flex">
|
||||
<div class="small">
|
||||
<a target="_blank" href="https://map.sthlm-mesh.se/?node_id=${message.from}" style="color: grey; text-decoration: none;">${getNodeLongName(message.from)}</a>
|
||||
${message.to !== "4294967295" ? ` → <a target="_blank" href="https://map.sthlm-mesh.se//?node_id=${message.to}" style="color: grey; text-decoration: none;">${getNodeLongName(message.to)}</a>` : ""}
|
||||
${message.to !== "4294967295" ? ` → <a target="_blank" href="https://map.sthlm-mesh.se/?node_id=${message.to}" style="color: grey; text-decoration: none;">${getNodeLongName(message.to)}</a>` : ""}
|
||||
</div>
|
||||
<div class="px-2 py-1 pb-1 border rounded shadow-sm" style="background-color: #efefef">
|
||||
<div class="">${escapeMessageText(message.text)}</div>
|
||||
</div>
|
||||
<div class="pt-1" style="font-size: 0.65rem;color: grey;">${formatMessageTimestamp(message.created_at)}</div>
|
||||
<div class="pt-1" style="font-size: 0.65rem;color: grey;">
|
||||
${formatMessageTimestamp(message.created_at)}
|
||||
${message.channel_id}
|
||||
${renderGatewayShortNames(message.gateways)}
|
||||
${renderOkToMqttWarning(message.ok_to_mqtt)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
`;
|
||||
@@ -140,7 +172,34 @@ document.addEventListener("DOMContentLoaded", async function () {
|
||||
|
||||
function formatMessageTimestamp(createdAt) {
|
||||
const date = new Date(createdAt);
|
||||
return date.toLocaleString('sv-SE', { hour12: false }).replace(' ', ' ').slice(0, 16);
|
||||
return date.toLocaleString('sv-SE', { hour12: false }).slice(0, 16);
|
||||
}
|
||||
|
||||
function renderOkToMqttWarning(okToMqtt) {
|
||||
if (okToMqtt === false) {
|
||||
return `<span class="text-secondary" data-bs-toggle="tooltip" data-bs-placement="top" data-bs-container="body" data-bs-trigger="hover focus" title="Denna nod har inte ok_to_mqtt. Meddelanden ignoreras av flertalet gateways.">⚠️</span>`;
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
function renderGatewayShortNames(gateways) {
|
||||
if (!Array.isArray(gateways) || gateways.length === 0) {
|
||||
return "";
|
||||
}
|
||||
const sorted = gateways.slice().sort((a, b) => Number(a.gateway_id) - Number(b.gateway_id));
|
||||
const names = sorted
|
||||
.map(g => {
|
||||
const short = state.nodesById[g.gateway_id]?.short_name;
|
||||
if (!short) return null;
|
||||
const hop = (g.hop_limit ?? '').toString();
|
||||
return hop ? `[${short}: ${hop}]` : `[${short}]`;
|
||||
})
|
||||
.filter(Boolean)
|
||||
.join(' ');
|
||||
if (!names) {
|
||||
return "";
|
||||
}
|
||||
return `Gated by: ${names}`;
|
||||
}
|
||||
|
||||
await fetchMessages();
|
||||
|
||||
293
static/js/rsvp-tracker.js
Normal file
293
static/js/rsvp-tracker.js
Normal file
@@ -0,0 +1,293 @@
|
||||
// RSVP response patterns (order matters - check longer phrases first!)
|
||||
const RESPONSE_PATTERNS = {
|
||||
'no': ['kommer inte', 'no', 'nej', 'not attending', 'kan inte', 'cannot attend'],
|
||||
'maybe': ['kanske', 'maybe', 'unsure', 'oklart', 'tvekar'],
|
||||
'yes': ['kommer', 'yes', 'ja', 'attending', 'deltar']
|
||||
};
|
||||
|
||||
function parseRSVPMessage(messageText, messagePattern) {
|
||||
const text = messageText.trim().toLowerCase();
|
||||
const pattern = messagePattern.toLowerCase();
|
||||
|
||||
if (!text.includes(pattern)) return null;
|
||||
|
||||
// Find response type by checking patterns
|
||||
for (const [responseType, patterns] of Object.entries(RESPONSE_PATTERNS)) {
|
||||
for (const p of patterns) {
|
||||
if (text.includes(p)) {
|
||||
return { type: responseType };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
async function fetchMessages() {
|
||||
const response = await fetch('https://map.sthlm-mesh.se/api/v1/text-messages?order=desc&count=3000');
|
||||
const data = await response.json();
|
||||
return data.text_messages;
|
||||
}
|
||||
|
||||
function parseRSVPResponses(messages, messagePattern) {
|
||||
const responses = new Map();
|
||||
|
||||
for (const message of messages) {
|
||||
const rsvp = parseRSVPMessage(message.text, messagePattern);
|
||||
if (!rsvp) continue;
|
||||
|
||||
const existing = responses.get(message.from);
|
||||
if (!existing || new Date(message.created_at) > new Date(existing.timestamp)) {
|
||||
responses.set(message.from, {
|
||||
nodeId: message.from,
|
||||
response: rsvp.type,
|
||||
timestamp: message.created_at
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return responses;
|
||||
}
|
||||
|
||||
function findNodeById(id) {
|
||||
return nodes.find(node => node.node_id.toString() === id.toString()) ?? null;
|
||||
}
|
||||
|
||||
function createRSVPSummary(responses) {
|
||||
const summary = { yes: [], maybe: [], no: [] };
|
||||
|
||||
for (const [nodeId, rsvp] of responses) {
|
||||
const node = findNodeById(nodeId);
|
||||
summary[rsvp.response]?.push({
|
||||
nodeId,
|
||||
shortName: node?.short_name || '?',
|
||||
longName: node?.long_name || `!${parseInt(nodeId).toString(16)}`,
|
||||
response: rsvp.response,
|
||||
timestamp: rsvp.timestamp
|
||||
});
|
||||
}
|
||||
|
||||
// Sort by timestamp (most recent first)
|
||||
Object.values(summary).forEach(arr =>
|
||||
arr.sort((a, b) => new Date(b.timestamp) - new Date(a.timestamp))
|
||||
);
|
||||
|
||||
return summary;
|
||||
}
|
||||
|
||||
function getNodeColour(nodeId) {
|
||||
return "#" + (nodeId & 0x00FFFFFF).toString(16).padStart(6, '0');
|
||||
}
|
||||
|
||||
function generateAttendeeList(attendees) {
|
||||
if (attendees.length === 0) {
|
||||
return '<p class="text-muted small">Inga svar än</p>';
|
||||
}
|
||||
|
||||
return attendees.map(attendee => `
|
||||
<div class="d-flex align-items-center mb-2">
|
||||
<div class="rounded-circle d-flex justify-content-center align-items-center me-2"
|
||||
style="width: 32px; height: 32px; background-color: ${getNodeColour(attendee.nodeId)}; color: white; font-size: 12px;">
|
||||
${attendee.shortName.substring(0, 4)}
|
||||
</div>
|
||||
<div>
|
||||
<div class="small">
|
||||
<a href="https://map.sthlm-mesh.se/?node_id=${attendee.nodeId}"
|
||||
target="_blank" class="text-decoration-none">
|
||||
${attendee.longName}
|
||||
</a>
|
||||
</div>
|
||||
<div class="small text-muted">
|
||||
${new Date(attendee.timestamp).toLocaleString('sv-SE', {
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
function generateRSVPHTML(summary) {
|
||||
return `
|
||||
<div class="rsvp-tracker mt-4">
|
||||
<div class="row">
|
||||
<div class="col-md-4 mb-3">
|
||||
<div class="card border-success">
|
||||
<div class="card-header bg-success text-white">
|
||||
<h6 class="mb-0">✅ Kommer (${summary.yes.length})</h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
${generateAttendeeList(summary.yes)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-4 mb-3">
|
||||
<div class="card border-warning">
|
||||
<div class="card-header bg-warning text-white">
|
||||
<h6 class="mb-0">❓ Kanske (${summary.maybe.length})</h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
${generateAttendeeList(summary.maybe)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-4 mb-3">
|
||||
<div class="card border-danger">
|
||||
<div class="card-header bg-danger text-white">
|
||||
<h6 class="mb-0">❌ Kommer inte (${summary.no.length})</h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
${generateAttendeeList(summary.no)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="text-center">
|
||||
<small class="text-muted">
|
||||
Totalt ${summary.yes.length + summary.maybe.length + summary.no.length} svar •
|
||||
Senast uppdaterad: ${new Date().toLocaleString('sv-SE')}
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
|
||||
function mergeRSVPSummaries(primarySummary, manualAttendees) {
|
||||
const latestByNode = new Map();
|
||||
|
||||
const addFrom = (collection, fallbackType) => {
|
||||
if (!Array.isArray(collection)) return;
|
||||
for (const attendee of collection) {
|
||||
const nodeId = attendee.nodeId ?? attendee.node_id ?? attendee.id;
|
||||
if (!nodeId) continue;
|
||||
const responseType = attendee.response ?? fallbackType;
|
||||
if (!responseType) continue;
|
||||
const timestampString = attendee.timestamp ?? null;
|
||||
const ts = timestampString ? new Date(timestampString).getTime() : -Infinity;
|
||||
|
||||
const existing = latestByNode.get(nodeId);
|
||||
if (!existing || ts > existing.ts) {
|
||||
latestByNode.set(nodeId, {
|
||||
nodeId: nodeId,
|
||||
shortName: attendee.shortName ?? attendee.short_name ?? '?',
|
||||
longName: attendee.longName ?? attendee.long_name ?? `!${parseInt(nodeId).toString(16)}`,
|
||||
response: responseType,
|
||||
timestamp: timestampString ?? existing?.timestamp ?? new Date(0).toISOString(),
|
||||
ts
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
['yes', 'maybe', 'no'].forEach(type => addFrom(primarySummary?.[type], type));
|
||||
['yes', 'maybe', 'no'].forEach(type => addFrom(manualAttendees?.[type], type));
|
||||
|
||||
const merged = { yes: [], maybe: [], no: [] };
|
||||
for (const { ts, ...attendee } of latestByNode.values()) {
|
||||
if (merged[attendee.response]) merged[attendee.response].push(attendee);
|
||||
}
|
||||
|
||||
Object.values(merged).forEach(arr =>
|
||||
arr.sort((a, b) => new Date(b.timestamp) - new Date(a.timestamp))
|
||||
);
|
||||
|
||||
return merged;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Initialize RSVP tracking for a specific event
|
||||
* Supports both active events (parse messages + JSON) and archived events (JSON only)
|
||||
*/
|
||||
async function initRSVPTracker(eventId) {
|
||||
const containerId = 'rsvp-tracker-' + eventId;
|
||||
const container = document.getElementById(containerId);
|
||||
|
||||
if (!container) {
|
||||
console.error(`Container with ID '${containerId}' not found. Make sure you have: <div id="${containerId}"></div>`);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Show loading state
|
||||
container.innerHTML = '<div class="text-center"><div class="spinner-border" role="status"></div><p>Laddar anmälningar...</p></div>';
|
||||
|
||||
const eventData = await loadEventDataFromJSON(eventId);
|
||||
|
||||
if (!eventData) {
|
||||
throw new Error(`Event data file not found: /events/${eventId}.json`);
|
||||
}
|
||||
|
||||
let summary;
|
||||
|
||||
if (eventData.archived) {
|
||||
summary = eventData.attendees;
|
||||
} else {
|
||||
await fetchNodes();
|
||||
const messages = await fetchMessages();
|
||||
const responses = parseRSVPResponses(messages, eventData.messagePattern);
|
||||
summary = createRSVPSummary(responses);
|
||||
|
||||
// Merge manual attendees with message-derived ones, keeping the latest response per attendee
|
||||
summary = mergeRSVPSummaries(summary, eventData.attendees);
|
||||
}
|
||||
|
||||
// Display results
|
||||
container.innerHTML = generateRSVPHTML(summary);
|
||||
|
||||
} catch (error) {
|
||||
container.innerHTML = `<div class="alert alert-danger">Kunde inte ladda anmälningar: ${error.message}</div>`;
|
||||
console.error('Error initializing RSVP tracker:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Export current RSVP data as JSON (for manual browser use)
|
||||
* Call this function in browser console: exportRSVPAsJSON('event-id', 'Message Pattern')
|
||||
*/
|
||||
async function exportRSVPAsJSON(eventId, messagePattern) {
|
||||
try {
|
||||
console.log('Fetching RSVP data for export...');
|
||||
|
||||
await fetchNodes(); // Load nodes cache
|
||||
const messages = await fetchMessages();
|
||||
const responses = parseRSVPResponses(messages, messagePattern);
|
||||
const summary = createRSVPSummary(responses);
|
||||
|
||||
const exportData = {
|
||||
messagePattern,
|
||||
archived: true,
|
||||
exportedAt: new Date().toISOString(),
|
||||
attendees: summary
|
||||
};
|
||||
|
||||
console.log(JSON.stringify(exportData, null, 2));
|
||||
return exportData;
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error exporting RSVP data:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async function loadEventDataFromJSON(eventId) {
|
||||
try {
|
||||
const response = await fetch(`/events/${eventId}.json`);
|
||||
if (!response.ok) throw new Error(`Event data not found: ${response.status}`);
|
||||
return await response.json();
|
||||
} catch (error) {
|
||||
console.warn(`Could not load event data for ${eventId}:`, error.message);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Export for use in other scripts
|
||||
window.initRSVPTracker = initRSVPTracker;
|
||||
window.exportRSVPAsJSON = exportRSVPAsJSON;
|
||||
199
static/js/status/channel-utilization-gauges.js
Normal file
199
static/js/status/channel-utilization-gauges.js
Normal file
@@ -0,0 +1,199 @@
|
||||
async function channelUtilizationGauges() {
|
||||
try {
|
||||
await fetchNodes();
|
||||
|
||||
// Create gauge for MediumFast only
|
||||
await createChannelUtilizationGauge('MediumFast', 'mediumfastGauge');
|
||||
|
||||
} catch (err) {
|
||||
console.error('Error creating channel utilization gauges:', err);
|
||||
|
||||
// Show error on MediumFast canvas
|
||||
showGaugeError('mediumfastGauge');
|
||||
}
|
||||
}
|
||||
|
||||
async function createChannelUtilizationGauge(channelName, canvasId) {
|
||||
const canvas = document.getElementById(canvasId);
|
||||
if (!canvas) {
|
||||
console.warn(`Canvas ${canvasId} not found`);
|
||||
return;
|
||||
}
|
||||
|
||||
const ctx = canvas.getContext('2d');
|
||||
|
||||
// Show loading
|
||||
ctx.font = '16px Arial';
|
||||
ctx.fillStyle = 'gray';
|
||||
ctx.textAlign = 'center';
|
||||
ctx.fillText('Loading...', canvas.width/2, canvas.height/2);
|
||||
|
||||
// Filter nodes by channel_id, recent updates (last 24 hours), and valid channel utilization
|
||||
const now = new Date();
|
||||
const twentyFourHoursAgo = new Date(now.getTime() - 24 * 60 * 60 * 1000);
|
||||
|
||||
const channelNodes = nodes.filter(node =>
|
||||
node.channel_id === channelName &&
|
||||
node.channel_utilization != null &&
|
||||
node.channel_utilization !== '' &&
|
||||
node.updated_at &&
|
||||
new Date(node.updated_at) >= twentyFourHoursAgo
|
||||
);
|
||||
|
||||
if (channelNodes.length === 0) {
|
||||
showNoDataMessage(ctx, canvas, `No ${channelName} data`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Get top 10 nodes with highest channel utilization for this channel
|
||||
const top10Nodes = channelNodes
|
||||
.map(node => ({
|
||||
...node,
|
||||
utilizationValue: parseFloat(node.channel_utilization)
|
||||
}))
|
||||
.sort((a, b) => b.utilizationValue - a.utilizationValue)
|
||||
.slice(0, 10);
|
||||
|
||||
// Calculate average channel utilization for top 10 nodes only
|
||||
const avgUtilization = top10Nodes.length > 0
|
||||
? top10Nodes.reduce((sum, node) => sum + node.utilizationValue, 0) / top10Nodes.length
|
||||
: 0;
|
||||
|
||||
// Create gauge chart using doughnut chart (0-30% scale)
|
||||
const maxScale = 30;
|
||||
const displayValue = Math.min(avgUtilization, maxScale); // Cap at 30%
|
||||
|
||||
new Chart(ctx, {
|
||||
type: 'doughnut',
|
||||
data: {
|
||||
datasets: [{
|
||||
data: [displayValue, maxScale - displayValue],
|
||||
backgroundColor: [
|
||||
getUtilizationColor(avgUtilization),
|
||||
'rgba(220, 220, 220, 0.3)'
|
||||
],
|
||||
borderWidth: 0,
|
||||
cutout: '70%'
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
rotation: -90, // Start from top
|
||||
circumference: 180, // Half circle
|
||||
plugins: {
|
||||
legend: {
|
||||
display: false
|
||||
},
|
||||
tooltip: {
|
||||
enabled: false
|
||||
}
|
||||
},
|
||||
onComplete: function() {
|
||||
// Add center text showing the percentage
|
||||
const centerX = canvas.width / 2;
|
||||
const centerY = canvas.height / 2 + 20; // Adjust for half circle
|
||||
|
||||
ctx.save();
|
||||
ctx.font = 'bold 24px Arial';
|
||||
ctx.fillStyle = '#333';
|
||||
ctx.textAlign = 'center';
|
||||
ctx.fillText(`${avgUtilization.toFixed(1)}%`, centerX, centerY);
|
||||
|
||||
ctx.font = '12px Arial';
|
||||
ctx.fillStyle = '#666';
|
||||
ctx.fillText(`Top ${top10Nodes.length} noder (24h)`, centerX, centerY + 25);
|
||||
ctx.restore();
|
||||
}
|
||||
},
|
||||
plugins: [{
|
||||
afterDraw: function(chart) {
|
||||
// Add center text showing the percentage
|
||||
const centerX = chart.width / 2;
|
||||
const centerY = chart.height / 2 + 20; // Adjust for half circle
|
||||
|
||||
ctx.save();
|
||||
ctx.font = 'bold 24px Arial';
|
||||
ctx.fillStyle = '#333';
|
||||
ctx.textAlign = 'center';
|
||||
ctx.fillText(`${avgUtilization.toFixed(1)}%`, centerX, centerY);
|
||||
|
||||
ctx.font = '12px Arial';
|
||||
ctx.fillStyle = '#666';
|
||||
ctx.fillText(`Top ${top10Nodes.length} noder (24h)`, centerX, centerY + 25);
|
||||
ctx.restore();
|
||||
}
|
||||
}]
|
||||
});
|
||||
|
||||
// Show top 10 nodes legend after chart is created
|
||||
showTop10NodesLegend(channelName, top10Nodes);
|
||||
}
|
||||
|
||||
function getUtilizationColor(utilization) {
|
||||
if (utilization >= 25) {
|
||||
return 'rgba(255, 0, 0, 0.8)';
|
||||
} else if (utilization >= 20) {
|
||||
return 'rgba(255, 69, 0, 0.8)';
|
||||
} else if (utilization >= 15) {
|
||||
return 'rgba(255, 140, 0, 0.8)';
|
||||
} else if (utilization >= 10) {
|
||||
return 'rgba(234, 184, 57, 0.8)';
|
||||
} else {
|
||||
return 'rgba(1, 163, 23, 0.8)';
|
||||
}
|
||||
}
|
||||
|
||||
function showNoDataMessage(ctx, canvas, message) {
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||
ctx.font = '16px Arial';
|
||||
ctx.fillStyle = 'gray';
|
||||
ctx.textAlign = 'center';
|
||||
ctx.fillText(message, canvas.width/2, canvas.height/2);
|
||||
}
|
||||
|
||||
function showTop10NodesLegend(channelName, top10Nodes) {
|
||||
const legendId = channelName.toLowerCase() + 'Legend';
|
||||
const legendContainer = document.getElementById(legendId);
|
||||
|
||||
if (!legendContainer || top10Nodes.length === 0) return;
|
||||
|
||||
let legendHTML = '<h5 class="text-muted mb-2">Top 10 Nodes</h5>';
|
||||
legendHTML += '<ul class="list-unstyled mb-0">';
|
||||
|
||||
top10Nodes.forEach((node, index) => {
|
||||
const utilizationColor = getUtilizationColor(node.utilizationValue);
|
||||
const shortName = node.short_name || node.node_id_hex || `Node ${node.node_id}`;
|
||||
const longName = node.long_name || '';
|
||||
|
||||
legendHTML += `
|
||||
<li class="mb-2 d-flex align-items-center">
|
||||
<span class="badge me-2" style="background-color: ${utilizationColor}; color: white; font-size: 0.7rem; min-width: 45px;">
|
||||
${node.utilizationValue.toFixed(1)}%
|
||||
</span>
|
||||
<span class="me-2" style="display:inline-block;width:7ch;font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace; font-size: 0.85rem; text-align:right; white-space:nowrap; overflow:hidden; text-overflow:clip;" title="${shortName}">[${shortName}]</span>
|
||||
<span class="text-truncate" style="font-size: 0.85rem;" title="${longName || shortName}">
|
||||
${longName}
|
||||
</span>
|
||||
</li>
|
||||
`;
|
||||
});
|
||||
|
||||
legendHTML += '</ul>';
|
||||
legendContainer.innerHTML = legendHTML;
|
||||
}
|
||||
|
||||
function showGaugeError(canvasId) {
|
||||
const canvas = document.getElementById(canvasId);
|
||||
if (!canvas) return;
|
||||
|
||||
const ctx = canvas.getContext('2d');
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||
ctx.font = '16px Arial';
|
||||
ctx.fillStyle = 'red';
|
||||
ctx.textAlign = 'center';
|
||||
ctx.fillText('Error loading data', canvas.width/2, canvas.height/2);
|
||||
}
|
||||
|
||||
// Initialize the gauges when the page loads
|
||||
channelUtilizationGauges();
|
||||
@@ -1,7 +1,15 @@
|
||||
async function channelUtilizationHourlyAverage() {
|
||||
let chUtilChart = null;
|
||||
|
||||
async function channelUtilizationHourlyAverage(days = 7) {
|
||||
const canvas = document.getElementById('channelUtilizationChart');
|
||||
const ctx = canvas.getContext('2d');
|
||||
|
||||
// Destroy existing chart if any
|
||||
if (chUtilChart) {
|
||||
chUtilChart.destroy();
|
||||
chUtilChart = null;
|
||||
}
|
||||
|
||||
// show loading
|
||||
ctx.font = '16px Arial';
|
||||
ctx.fillStyle = 'gray';
|
||||
@@ -9,24 +17,32 @@ async function channelUtilizationHourlyAverage() {
|
||||
ctx.fillText('Loading...', canvas.width/2, canvas.height/2);
|
||||
|
||||
try {
|
||||
const response = await fetch('https://map.sthlm-mesh.se/api/v1/stats/channel-utilization-stats?days=30');
|
||||
const data = await response.json();
|
||||
const response = await fetch(`https://map.sthlm-mesh.se/api/v1/stats/channel-utilization-stats?days=${days}&channel_id=MediumFast`);
|
||||
const allData = await response.json();
|
||||
|
||||
const labels = data.map(entry => entry.recorded_at);
|
||||
const values = data.map(entry => parseFloat(entry.avg_channel_utilization));
|
||||
// Single dataset containing all points
|
||||
const datasets = [];
|
||||
|
||||
new Chart(ctx, {
|
||||
const points = allData.map(entry => ({
|
||||
x: entry.recorded_at,
|
||||
y: entry.avg_channel_utilization
|
||||
}));
|
||||
|
||||
datasets.push({
|
||||
label: 'channelutilization',
|
||||
data: points,
|
||||
borderColor: 'rgb(34, 197, 94)',
|
||||
backgroundColor: 'rgba(34, 197, 94, 0.1)',
|
||||
borderWidth: 2,
|
||||
tension: 0.1,
|
||||
pointRadius: 0,
|
||||
fill: false,
|
||||
});
|
||||
|
||||
chUtilChart = new Chart(ctx, {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: labels,
|
||||
datasets: [{
|
||||
label: 'Hourly Avg Channel Utilization',
|
||||
data: values,
|
||||
borderColor: 'rgb(15, 169, 252)',
|
||||
borderWidth: 2,
|
||||
tension: 0.1,
|
||||
pointRadius: 0,
|
||||
}]
|
||||
datasets: datasets
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
@@ -41,32 +57,34 @@ async function channelUtilizationHourlyAverage() {
|
||||
},
|
||||
},
|
||||
y: {
|
||||
min: 9.5,
|
||||
max: 14,
|
||||
min: 8,
|
||||
max: 26,
|
||||
title: { display: true, text: 'Avg. Channel Utilization (%)' }
|
||||
}
|
||||
},
|
||||
plugins: {
|
||||
legend: { display: false },
|
||||
legend: {
|
||||
display: false
|
||||
},
|
||||
tooltip: { mode: 'index', intersect: false },
|
||||
annotation: {
|
||||
annotations: {
|
||||
bad: {
|
||||
type: 'box',
|
||||
yMin: 13,
|
||||
yMax: 15,
|
||||
yMin: 22,
|
||||
yMax: 100,
|
||||
backgroundColor: 'rgba(255, 0, 0, 0.2)'
|
||||
},
|
||||
medium: {
|
||||
type: 'box',
|
||||
yMin: 12,
|
||||
yMax: 13,
|
||||
yMin: 16,
|
||||
yMax: 22,
|
||||
backgroundColor: 'rgba(234, 184, 57, 0.2)', //'rgba(255, 165, 0, 0.1)'
|
||||
},
|
||||
good: {
|
||||
type: 'box',
|
||||
yMin: 0,
|
||||
yMax: 12,
|
||||
yMax: 16,
|
||||
backgroundColor: 'rgba(1, 163, 23, 0.1)', //'rgba(0, 128, 0, 0.1)'
|
||||
}
|
||||
}
|
||||
@@ -84,4 +102,8 @@ async function channelUtilizationHourlyAverage() {
|
||||
|
||||
}
|
||||
|
||||
document.querySelectorAll('input[name="chUtilPeriod"]').forEach(radio => {
|
||||
radio.addEventListener('change', e => channelUtilizationHourlyAverage(e.target.value));
|
||||
});
|
||||
|
||||
channelUtilizationHourlyAverage();
|
||||
@@ -1,3 +1,6 @@
|
||||
let deviceRolesChartInstance = null;
|
||||
let selectedChannelIdRoles = null; // null = All
|
||||
|
||||
async function deviceRolesChart() {
|
||||
const canvas = document.getElementById('deviceRoles');
|
||||
const ctx = canvas.getContext('2d');
|
||||
@@ -10,11 +13,16 @@ async function deviceRolesChart() {
|
||||
|
||||
try {
|
||||
await fetchNodes();
|
||||
const predefinedLabels = ['CLIENT', 'CLIENT_MUTE', 'CLIENT_HIDDEN', 'ROUTER_LATE', 'ROUTER', 'ROUTER_CLIENT'];
|
||||
const predefinedLabels = ['CLIENT', 'CLIENT_MUTE', 'CLIENT_HIDDEN', 'CLIENT_BASE', 'ROUTER_LATE', 'ROUTER', 'ROUTER_CLIENT'];
|
||||
|
||||
// Count role occurrences
|
||||
const roleCounts = {};
|
||||
nodes.forEach(node => {
|
||||
const filteredNodes = Array.isArray(nodes) ? nodes.filter(n => {
|
||||
if (!selectedChannelIdRoles) return true; // All
|
||||
return n.channel_id === selectedChannelIdRoles;
|
||||
}) : [];
|
||||
|
||||
filteredNodes.forEach(node => {
|
||||
const role = node.role_name || "UNKNOWN";
|
||||
roleCounts[role] = (roleCounts[role] || 0) + 1;
|
||||
});
|
||||
@@ -36,7 +44,12 @@ async function deviceRolesChart() {
|
||||
return 'rgb(41, 156, 70)';
|
||||
});
|
||||
|
||||
new Chart(ctx, {
|
||||
if (deviceRolesChartInstance) {
|
||||
deviceRolesChartInstance.destroy();
|
||||
deviceRolesChartInstance = null;
|
||||
}
|
||||
|
||||
deviceRolesChartInstance = new Chart(ctx, {
|
||||
type: 'bar',
|
||||
data: {
|
||||
labels: labels,
|
||||
@@ -71,3 +84,14 @@ async function deviceRolesChart() {
|
||||
}
|
||||
|
||||
deviceRolesChart();
|
||||
|
||||
// Listen via shared StatusFilter
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
if (typeof StatusFilter !== 'undefined' && StatusFilter.subscribe) {
|
||||
selectedChannelIdRoles = StatusFilter.getChannelId ? StatusFilter.getChannelId() : null;
|
||||
StatusFilter.subscribe(() => {
|
||||
selectedChannelIdRoles = StatusFilter.getChannelId ? StatusFilter.getChannelId() : null;
|
||||
deviceRolesChart();
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -10,21 +10,21 @@ async function firmwareVersionGraph() {
|
||||
|
||||
try {
|
||||
await fetchNodes();
|
||||
const channelId = (typeof StatusFilter !== 'undefined' && StatusFilter.getChannelId) ? StatusFilter.getChannelId() : null;
|
||||
// Filter out nodes without firmware_version and channel match
|
||||
const nodesWithVersion = nodes
|
||||
.filter(node => node.firmware_version != null)
|
||||
.filter(node => !channelId || node.channel_id === channelId);
|
||||
|
||||
const countsByVersion = {};
|
||||
const validVersions = ['<2.5.0', '>2.5.0', '>2.6.8'];
|
||||
|
||||
// Filter out nodes without firmware_version
|
||||
const nodesWithVersion = nodes.filter(node => node.firmware_version != null);
|
||||
|
||||
// Count how many nodes have each firmware version
|
||||
for (const node of nodesWithVersion) {
|
||||
const version = node.firmware_version;
|
||||
if(version != '2.4.3 or older') {
|
||||
countsByVersion['2.5.0 or newer'] = (countsByVersion['2.5.0 or newer'] || 0) + 1;
|
||||
}
|
||||
else {
|
||||
countsByVersion[version] = (countsByVersion[version] || 0) + 1;
|
||||
}
|
||||
// Default to '>2.5.0' for other version.
|
||||
// This is mainly the nodes that report their firmware version using MQTT map_report
|
||||
const bucket = validVersions.includes(version) ? version : '>2.5.0';
|
||||
countsByVersion[bucket] = (countsByVersion[bucket] || 0) + 1;
|
||||
}
|
||||
|
||||
// Convert to array of { version, count }
|
||||
@@ -42,14 +42,19 @@ async function firmwareVersionGraph() {
|
||||
const chartContainer = document.getElementById('firmwareVersionContainer');
|
||||
chartContainer.style.height = `${labels.length * 35 + 50}px`;
|
||||
|
||||
new Chart(ctx, {
|
||||
if (window.firmwareVersionChartInstance) {
|
||||
window.firmwareVersionChartInstance.destroy();
|
||||
window.firmwareVersionChartInstance = null;
|
||||
}
|
||||
|
||||
window.firmwareVersionChartInstance = new Chart(ctx, {
|
||||
type: 'bar',
|
||||
data: {
|
||||
labels: labels,
|
||||
datasets: [{
|
||||
label: 'Firmware Version',
|
||||
data: counts,
|
||||
backgroundColor: ['#7EB26D', '#BF1B00']
|
||||
backgroundColor: ['#7EB26D', '#7EB26D', '#BF1B00']
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
@@ -70,3 +75,10 @@ async function firmwareVersionGraph() {
|
||||
}
|
||||
|
||||
firmwareVersionGraph();
|
||||
|
||||
// Re-render on channel change
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
if (typeof StatusFilter !== 'undefined' && StatusFilter.subscribe) {
|
||||
StatusFilter.subscribe(() => firmwareVersionGraph());
|
||||
}
|
||||
});
|
||||
|
||||
@@ -8,14 +8,57 @@ async function hardwareStatsGraph() {
|
||||
ctx.textAlign = 'center';
|
||||
ctx.fillText('Loading data...', canvas.width / 2, canvas.height / 2);
|
||||
|
||||
// Function to generate a consistent color from a string
|
||||
function stringToColor(str) {
|
||||
let hash = 0;
|
||||
for (let i = 0; i < str.length; i++) {
|
||||
hash = str.charCodeAt(i) + ((hash << 5) - hash);
|
||||
// Manufacturer colors (fixed palette)
|
||||
const MANUFACTURER_COLORS = {
|
||||
Lilygo: '#3B82F6', // blue-500
|
||||
Heltec: '#10B981', // emerald-500
|
||||
RakWireless: '#A855F7', // purple-500
|
||||
Seeedstudio: '#EAB308', // yellow-500
|
||||
Other: '#9CA3AF' // gray-400
|
||||
};
|
||||
|
||||
// Single source of truth: models listed under each manufacturer
|
||||
const HARDWARE_MODELS = {
|
||||
Heltec: [
|
||||
'HELTEC_V3',
|
||||
'HELTEC_V4',
|
||||
'HELTEC_V2_1',
|
||||
'HELTEC_V2_0',
|
||||
'HELTEC_WSL_V3',
|
||||
'HELTEC_MESH_NODE_T114',
|
||||
'HELTEC_MESH_POCKET',
|
||||
'HELTEC_WIRELESS_PAPER',
|
||||
'HELTEC_WIRELESS_TRACKER'
|
||||
],
|
||||
RakWireless: [
|
||||
'RAK4631',
|
||||
'RAK2560',
|
||||
'WISMESH_TAG'
|
||||
],
|
||||
Lilygo: [
|
||||
'LILYGO_TBEAM_S3_CORE',
|
||||
'TBEAM',
|
||||
'TLORA_T3_S3',
|
||||
'T_DECK',
|
||||
'T_ECHO',
|
||||
'T_WATCH_S3'
|
||||
],
|
||||
Seeedstudio: [
|
||||
'SEEED_XIAO_S3',
|
||||
'TRACKER_T1000_E',
|
||||
'XIAO_NRF52_KIT',
|
||||
'SENSECAP_INDICATOR',
|
||||
'SEEED_WIO_TRACKER_L1',
|
||||
]
|
||||
};
|
||||
|
||||
// Simple lookup without a prebuilt map
|
||||
function getManufacturerForModel(modelName) {
|
||||
const key = modelName.toUpperCase();
|
||||
for (const [manufacturer, models] of Object.entries(HARDWARE_MODELS)) {
|
||||
if (models.some(m => m.toUpperCase() === key)) return manufacturer;
|
||||
}
|
||||
const hue = Math.abs(hash % 360);
|
||||
return `hsl(${hue}, 70%, 60%)`;
|
||||
return 'Other';
|
||||
}
|
||||
|
||||
|
||||
@@ -25,21 +68,30 @@ async function hardwareStatsGraph() {
|
||||
|
||||
const labels = data.hardware_model_stats.map(item => item.hardware_model_name);
|
||||
const counts = data.hardware_model_stats.map(item => item.count);
|
||||
const colors = labels.map(name => stringToColor(name));
|
||||
|
||||
// Manufacturers present and interactive filter state
|
||||
const presentManufacturers = Array.from(new Set(labels.map(getManufacturerForModel)));
|
||||
const enabledManufacturers = new Set(presentManufacturers);
|
||||
|
||||
// Color bars per model based on manufacturer
|
||||
const backgroundColors = labels.map(model => {
|
||||
const manu = getManufacturerForModel(model);
|
||||
return MANUFACTURER_COLORS[manu] || MANUFACTURER_COLORS.Other;
|
||||
});
|
||||
|
||||
// Adjust chart height based on number of items
|
||||
const chartContainer = document.getElementById('hardwareChartContainer');
|
||||
chartContainer.style.height = `${labels.length * 25 + 50}px`;
|
||||
|
||||
|
||||
new Chart(ctx, {
|
||||
const chart = new Chart(ctx, {
|
||||
type: 'bar',
|
||||
data: {
|
||||
labels: labels,
|
||||
datasets: [{
|
||||
label: 'Hardware Models',
|
||||
data: counts,
|
||||
backgroundColor: colors,
|
||||
backgroundColor: backgroundColors,
|
||||
borderWidth: 1
|
||||
}]
|
||||
},
|
||||
@@ -49,7 +101,52 @@ async function hardwareStatsGraph() {
|
||||
barPercentage: 0.8,
|
||||
indexAxis: 'y',
|
||||
y: { ticks: { autoSkip: false, font: { size: 12 } } },
|
||||
plugins: { legend: { display: false } }
|
||||
plugins: {
|
||||
legend: {
|
||||
display: true,
|
||||
onClick: (_evt, legendItem) => {
|
||||
const manu = legendItem.text;
|
||||
if (enabledManufacturers.has(manu)) {
|
||||
enabledManufacturers.delete(manu);
|
||||
} else {
|
||||
enabledManufacturers.add(manu);
|
||||
}
|
||||
// Rebuild labels, data and colors to remove hidden manufacturers entirely
|
||||
const mask = labels.map((model) => enabledManufacturers.has(getManufacturerForModel(model)));
|
||||
const newLabels = labels.filter((_, i) => mask[i]);
|
||||
const newData = counts.filter((_, i) => mask[i]);
|
||||
const newColors = newLabels.map(model => {
|
||||
const m = getManufacturerForModel(model);
|
||||
return MANUFACTURER_COLORS[m] || MANUFACTURER_COLORS.Other;
|
||||
});
|
||||
// Adjust chart height to the filtered number of items
|
||||
chartContainer.style.height = `${newLabels.length * 25 + 70}px`;
|
||||
chart.data.labels = newLabels;
|
||||
chart.data.datasets[0].data = newData;
|
||||
chart.data.datasets[0].backgroundColor = newColors;
|
||||
chart.update();
|
||||
},
|
||||
labels: {
|
||||
generateLabels: (chart) => {
|
||||
// Use the full set of manufacturers present in the original data
|
||||
const present = presentManufacturers;
|
||||
const preferredOrder = ['Heltec', 'RakWireless', 'Lilygo', 'Seeedstudio', 'Other'];
|
||||
const ordered = preferredOrder.filter(m => present.includes(m)).concat(
|
||||
present.filter(m => !preferredOrder.includes(m))
|
||||
);
|
||||
return ordered.map((m) => ({
|
||||
text: m,
|
||||
fillStyle: MANUFACTURER_COLORS[m] || MANUFACTURER_COLORS.Other,
|
||||
strokeStyle: MANUFACTURER_COLORS[m] || MANUFACTURER_COLORS.Other,
|
||||
lineWidth: 0,
|
||||
hidden: !enabledManufacturers.has(m),
|
||||
// Needed by Chart.js internals; keep pointing to first dataset
|
||||
datasetIndex: 0
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
|
||||
95
static/js/status/is-ok-to-mqtt-chart.js
Normal file
95
static/js/status/is-ok-to-mqtt-chart.js
Normal file
@@ -0,0 +1,95 @@
|
||||
async function isOkToMqttGraph() {
|
||||
const canvas = document.getElementById('isOkToMqttChart');
|
||||
const ctx = canvas.getContext('2d');
|
||||
|
||||
// Show "Loading..." message
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||
ctx.font = '16px Arial';
|
||||
ctx.fillStyle = 'gray';
|
||||
ctx.textAlign = 'center';
|
||||
ctx.fillText('Loading data...', canvas.width / 2, canvas.height / 2);
|
||||
|
||||
try {
|
||||
await fetchNodes();
|
||||
const channelId = (typeof StatusFilter !== 'undefined' && StatusFilter.getChannelId) ? StatusFilter.getChannelId() : null;
|
||||
// Filter nodes to only include those updated within the last 30 days and channel match
|
||||
const recentNodes = nodes
|
||||
.filter(node => new Date(node.updated_at) >= new Date(Date.now() - 30 * 24 * 60 * 60 * 1000))
|
||||
.filter(node => !channelId || node.channel_id === channelId);
|
||||
|
||||
let countTrue = 0;
|
||||
let countFalse = 0;
|
||||
let countNull = 0;
|
||||
|
||||
for (const node of recentNodes) {
|
||||
const val = node.ok_to_mqtt;
|
||||
if (val === true) countTrue++;
|
||||
else if (val === false) countFalse++;
|
||||
else countNull++;
|
||||
}
|
||||
|
||||
const container = canvas.parentElement;
|
||||
if (container) container.style.height = `${2 * 35 + 50}px`;
|
||||
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||
|
||||
if (window.isOkToMqttChartInstance) {
|
||||
window.isOkToMqttChartInstance.destroy();
|
||||
window.isOkToMqttChartInstance = null;
|
||||
}
|
||||
|
||||
window.isOkToMqttChartInstance = new Chart(ctx, {
|
||||
type: 'bar',
|
||||
data: {
|
||||
labels: ['True', 'False'],
|
||||
|
||||
datasets: [
|
||||
{
|
||||
label: 'true',
|
||||
data: [countTrue, 0,],
|
||||
backgroundColor: '#7EB26D', // blue
|
||||
},
|
||||
{
|
||||
label: 'false',
|
||||
data: [0, countFalse],
|
||||
backgroundColor: '#BF1B00', // green
|
||||
},
|
||||
{
|
||||
label: 'unset',
|
||||
data: [0, countNull],
|
||||
backgroundColor: '#808080', // grey
|
||||
}
|
||||
]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
indexAxis: 'y',
|
||||
plugins: {
|
||||
legend: { display: false }
|
||||
},
|
||||
scales: {
|
||||
x: { beginAtZero: true, stacked: true },
|
||||
y: { stacked: true }
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error building ok_to_mqtt chart:', error);
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||
ctx.fillStyle = 'red';
|
||||
ctx.fillText('Error loading data', canvas.width / 2, canvas.height / 2);
|
||||
}
|
||||
}
|
||||
|
||||
isOkToMqttGraph();
|
||||
|
||||
// Re-render on channel change
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
if (typeof StatusFilter !== 'undefined' && StatusFilter.subscribe) {
|
||||
StatusFilter.subscribe(() => isOkToMqttGraph());
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
99
static/js/status/is-unmessagable-chart.js
Normal file
99
static/js/status/is-unmessagable-chart.js
Normal file
@@ -0,0 +1,99 @@
|
||||
async function isUnmessagableGraph() {
|
||||
const canvas = document.getElementById('isUnmessagableChart');
|
||||
const ctx = canvas.getContext('2d');
|
||||
|
||||
// Show "Loading..." message
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||
ctx.font = '16px Arial';
|
||||
ctx.fillStyle = 'gray';
|
||||
ctx.textAlign = 'center';
|
||||
ctx.fillText('Loading data...', canvas.width / 2, canvas.height / 2);
|
||||
|
||||
try {
|
||||
await fetchNodes();
|
||||
const channelId = (typeof StatusFilter !== 'undefined' && StatusFilter.getChannelId) ? StatusFilter.getChannelId() : null;
|
||||
// Filter nodes to only include those updated within the last 30 days and channel match
|
||||
const recentNodes = nodes
|
||||
.filter(node => new Date(node.updated_at) >= new Date(Date.now() - 30 * 24 * 60 * 60 * 1000))
|
||||
.filter(node => !channelId || node.channel_id === channelId);
|
||||
|
||||
let countTrue = 0;
|
||||
let countFalse = 0;
|
||||
let countNull = 0;
|
||||
|
||||
|
||||
for (const node of recentNodes) {
|
||||
const val = node.is_unmessagable;
|
||||
if (val === true) countTrue++;
|
||||
else if (val === false) countFalse++;
|
||||
else countNull++;
|
||||
}
|
||||
|
||||
const container = canvas.parentElement;
|
||||
if (container) container.style.height = `${2 * 35 + 50}px`;
|
||||
|
||||
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||
|
||||
if (window.isUnmessagableChartInstance) {
|
||||
window.isUnmessagableChartInstance.destroy();
|
||||
window.isUnmessagableChartInstance = null;
|
||||
}
|
||||
|
||||
window.isUnmessagableChartInstance = new Chart(ctx, {
|
||||
type: 'bar',
|
||||
data: {
|
||||
labels: ['unmessagable', 'messagable'],
|
||||
datasets: [
|
||||
{
|
||||
label: 'true',
|
||||
data: [countTrue, 0,],
|
||||
backgroundColor: '#0d6efd', // blue
|
||||
},
|
||||
{
|
||||
label: 'false',
|
||||
data: [0, countFalse],
|
||||
backgroundColor: '#7EB26D', // green
|
||||
},
|
||||
{
|
||||
label: 'unset',
|
||||
data: [0, countNull],
|
||||
backgroundColor: '#808080', // grey
|
||||
}
|
||||
]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
indexAxis: 'y',
|
||||
plugins: {
|
||||
legend: { display: false }
|
||||
},
|
||||
scales: {
|
||||
x: {
|
||||
beginAtZero: true,
|
||||
stacked: true,
|
||||
},
|
||||
y: {
|
||||
stacked: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error building is_unmessagable chart:', error);
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||
ctx.fillStyle = 'red';
|
||||
ctx.fillText('Error loading data', canvas.width / 2, canvas.height / 2);
|
||||
}
|
||||
}
|
||||
|
||||
isUnmessagableGraph();
|
||||
|
||||
// Re-render on channel change
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
if (typeof StatusFilter !== 'undefined' && StatusFilter.subscribe) {
|
||||
StatusFilter.subscribe(() => isUnmessagableGraph());
|
||||
}
|
||||
});
|
||||
98
static/js/status/max-hops-chart.js
Normal file
98
static/js/status/max-hops-chart.js
Normal file
@@ -0,0 +1,98 @@
|
||||
function getHopsColor(maxHops) {
|
||||
switch(maxHops){
|
||||
case 0: return 'rgb(41, 156, 70)';
|
||||
case 1: return 'rgb(41, 156, 70)';
|
||||
case 2: return 'rgb(79, 170, 77)';
|
||||
case 3: return 'rgb(118, 182, 84)';
|
||||
case 4: return 'rgb(157, 195, 91)';
|
||||
case 5: return 'rgb(195, 208, 98)';
|
||||
case 6: return 'rgb(229, 189, 82)';
|
||||
case 7: return 'rgb(242, 161, 67)';
|
||||
}
|
||||
return 'rgb(200, 200, 200)';
|
||||
}
|
||||
|
||||
let maxHopsChartInstance = null;
|
||||
|
||||
async function maxHopsGraph() {
|
||||
const canvas = document.getElementById('maxHopsChart');
|
||||
const ctx = canvas.getContext('2d');
|
||||
|
||||
// Loading message
|
||||
ctx.font = '16px Arial';
|
||||
ctx.fillStyle = 'gray';
|
||||
ctx.textAlign = 'center';
|
||||
ctx.fillText('Loading data...', canvas.width / 2, canvas.height / 2);
|
||||
|
||||
try {
|
||||
await fetchNodes();
|
||||
|
||||
// Initialize buckets
|
||||
const countsByHops = {};
|
||||
for (let i = 0; i <= 7; i++) countsByHops[i] = 0;
|
||||
|
||||
const channelId = (typeof StatusFilter !== 'undefined' && StatusFilter.getChannelId) ? StatusFilter.getChannelId() : null;
|
||||
const filteredNodes = Array.isArray(nodes) ? nodes.filter(n => {
|
||||
if (!channelId) return true;
|
||||
return n.channel_id === channelId;
|
||||
}) : [];
|
||||
|
||||
const nodesWithMaxHops = filteredNodes.filter(n => n.max_hops !== null && n.max_hops !== undefined);
|
||||
|
||||
for (const node of nodesWithMaxHops) {
|
||||
const val = Number(node.max_hops);
|
||||
countsByHops[val] += 1;
|
||||
}
|
||||
|
||||
const labels = [0, 1, 2, 3, 4, 5, 6, 7];
|
||||
const counts = labels.map(n => countsByHops[n]);
|
||||
const backgroundColors = labels.map(n => getHopsColor(n));
|
||||
|
||||
const chartContainer = document.getElementById('maxHopsContainer');
|
||||
if (chartContainer) chartContainer.style.height = `${labels.length * 35 + 50}px`;
|
||||
|
||||
if (maxHopsChartInstance) {
|
||||
maxHopsChartInstance.destroy();
|
||||
maxHopsChartInstance = null;
|
||||
}
|
||||
|
||||
maxHopsChartInstance = new Chart(ctx, {
|
||||
type: 'bar',
|
||||
data: {
|
||||
labels,
|
||||
datasets: [{
|
||||
label: 'Antal',
|
||||
data: counts,
|
||||
backgroundColor: backgroundColors,
|
||||
}],
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
indexAxis: 'y',
|
||||
scales: {
|
||||
y: { ticks: { autoSkip: false, font: { size: 12 } } },
|
||||
x: { beginAtZero: true },
|
||||
},
|
||||
plugins: { legend: { display: false } },
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error building max_hops chart:', error);
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||
ctx.fillStyle = 'red';
|
||||
ctx.fillText('Error loading data', canvas.width / 2, canvas.height / 2);
|
||||
}
|
||||
}
|
||||
|
||||
maxHopsGraph();
|
||||
|
||||
|
||||
// Re-render on channel change
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
if (typeof StatusFilter !== 'undefined' && StatusFilter.subscribe) {
|
||||
StatusFilter.subscribe(() => maxHopsGraph());
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -8,12 +8,16 @@ async function messagesStatsGraph() {
|
||||
ctx.textAlign = 'center';
|
||||
ctx.fillText('Loading data...', canvas.width / 2, canvas.height / 2);
|
||||
|
||||
function formatMessageTimestamp(createdAt) {
|
||||
const date = new Date(createdAt);
|
||||
return date.toLocaleString('sv-SE', { hour12: false }).slice(0, 16);
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch('https://map.sthlm-mesh.se/api/v1/stats/messages-per-hour')
|
||||
const data = await response.json();
|
||||
|
||||
const labels = data.map(entry => entry.hour);
|
||||
const labels = data.map(entry => formatMessageTimestamp(entry.hour));
|
||||
const counts = data.map(entry => entry.count);
|
||||
|
||||
new Chart(ctx, {
|
||||
@@ -34,7 +38,7 @@ async function messagesStatsGraph() {
|
||||
x: {
|
||||
ticks: {
|
||||
callback: function(value, index) {
|
||||
return labels[index].endsWith('00') ? labels[index].split('T')[0] : null;
|
||||
return labels[index].endsWith('00:00') ? labels[index].split(' ')[0] : null;
|
||||
},
|
||||
autoSkip: false
|
||||
}
|
||||
|
||||
@@ -1,63 +1,94 @@
|
||||
async function mostActiveNodesGraph() {
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const canvas = document.getElementById('mostActiveNodes');
|
||||
if (!canvas) return;
|
||||
|
||||
const ctx = canvas.getContext('2d');
|
||||
let chartInstance = null;
|
||||
|
||||
// Show "Loading..." message
|
||||
ctx.font = '16px Arial';
|
||||
ctx.fillStyle = 'gray';
|
||||
ctx.textAlign = 'center';
|
||||
ctx.fillText('Loading data...', canvas.width / 2, canvas.height / 2);
|
||||
|
||||
try {
|
||||
const response = await fetch('https://map.sthlm-mesh.se/api/v1/stats/most-active-nodes');
|
||||
const data = await response.json();
|
||||
|
||||
const labels = data.map(entry => entry.long_name);
|
||||
const counts = data.map(entry => entry.count);
|
||||
|
||||
// Adjust chart height based on number of items
|
||||
const chartContainer = document.getElementById('mostActiveNodesContainer');
|
||||
chartContainer.style.height = `${labels.length * 22 + 50}px`;
|
||||
|
||||
const backgroundColors = counts.map(count => {
|
||||
if (count < 100) return '#7EB26D'; // Green
|
||||
if (count < 200) return '#EAB839'; // Yellow
|
||||
return '#BF1B00'; // Red
|
||||
});
|
||||
|
||||
|
||||
new Chart(ctx, {
|
||||
type: 'bar',
|
||||
data: {
|
||||
labels: labels,
|
||||
datasets: [{
|
||||
label: 'Packages originated',
|
||||
data: counts,
|
||||
backgroundColor: backgroundColors,
|
||||
borderWidth: 1
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
indexAxis: 'y', // horizontal bars
|
||||
scales: {
|
||||
y: {
|
||||
ticks: { autoSkip: false, font: { size: 12 } }
|
||||
},
|
||||
x: { max: 300 },
|
||||
},
|
||||
plugins: {
|
||||
legend: { display: false },
|
||||
}
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error fetching data:', error);
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||
ctx.fillStyle = 'red';
|
||||
ctx.fillText('Error loading data', canvas.width / 2, canvas.height / 2);
|
||||
function getSelectedChannelId() {
|
||||
return (typeof StatusFilter !== 'undefined' && StatusFilter.getChannelId) ? StatusFilter.getChannelId() : null;
|
||||
}
|
||||
}
|
||||
|
||||
mostActiveNodesGraph();
|
||||
async function renderMostActiveNodes() {
|
||||
// Show "Loading..." message
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||
ctx.font = '16px Arial';
|
||||
ctx.fillStyle = 'gray';
|
||||
ctx.textAlign = 'center';
|
||||
ctx.fillText('Loading data...', canvas.width / 2, canvas.height / 2);
|
||||
|
||||
try {
|
||||
// Build URL with optional channel_id
|
||||
const baseUrl = 'https://map.sthlm-mesh.se/api/v1/stats/most-active-nodes';
|
||||
const channelId = getSelectedChannelId();
|
||||
const url = channelId ? `${baseUrl}?channel_id=${encodeURIComponent(channelId)}` : baseUrl;
|
||||
|
||||
const response = await fetch(url);
|
||||
const data = await response.json();
|
||||
|
||||
const labels = data.map(entry => entry.long_name);
|
||||
const counts = data.map(entry => entry.count);
|
||||
|
||||
// Adjust chart height based on number of items
|
||||
const chartContainer = document.getElementById('mostActiveNodesContainer');
|
||||
if (chartContainer) {
|
||||
chartContainer.style.height = `${labels.length * 22 + 50}px`;
|
||||
}
|
||||
|
||||
const backgroundColors = counts.map(count => {
|
||||
if (count < 100) return '#7EB26D'; // Green
|
||||
if (count < 200) return '#EAB839'; // Yellow
|
||||
return '#BF1B00'; // Red
|
||||
});
|
||||
|
||||
if (chartInstance) {
|
||||
chartInstance.destroy();
|
||||
chartInstance = null;
|
||||
}
|
||||
|
||||
chartInstance = new Chart(ctx, {
|
||||
type: 'bar',
|
||||
data: {
|
||||
labels: labels,
|
||||
datasets: [{
|
||||
label: 'Packages originated',
|
||||
data: counts,
|
||||
backgroundColor: backgroundColors,
|
||||
borderWidth: 1
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
indexAxis: 'y',
|
||||
scales: {
|
||||
y: {
|
||||
ticks: { autoSkip: false, font: { size: 12 } }
|
||||
},
|
||||
x: { max: 200 },
|
||||
},
|
||||
plugins: {
|
||||
legend: { display: false },
|
||||
}
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error fetching data:', error);
|
||||
if (chartInstance) {
|
||||
chartInstance.destroy();
|
||||
chartInstance = null;
|
||||
}
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||
ctx.fillStyle = 'red';
|
||||
ctx.fillText('Error loading data', canvas.width / 2, canvas.height / 2);
|
||||
}
|
||||
}
|
||||
|
||||
// Listen via shared StatusFilter
|
||||
if (typeof StatusFilter !== 'undefined' && StatusFilter.subscribe) {
|
||||
StatusFilter.subscribe(() => renderMostActiveNodes());
|
||||
}
|
||||
|
||||
// Initial render
|
||||
renderMostActiveNodes();
|
||||
});
|
||||
|
||||
@@ -1,8 +1,15 @@
|
||||
let selectedChannelIdNodes = null; // null = All
|
||||
|
||||
function countNodesSince(daysAgo) {
|
||||
const now = new Date();
|
||||
const threshold = new Date(now);
|
||||
threshold.setDate(threshold.getDate() - daysAgo);
|
||||
return nodes.filter(node => new Date(node.updated_at) >= threshold).length;
|
||||
const threshold = new Date(now.getTime() - daysAgo * 24 * 60 * 60 * 1000);
|
||||
return nodes
|
||||
.filter(node => new Date(node.updated_at) >= threshold)
|
||||
.filter(node => {
|
||||
if (!selectedChannelIdNodes) return true; // All
|
||||
return node.channel_id === selectedChannelIdNodes;
|
||||
})
|
||||
.length;
|
||||
}
|
||||
|
||||
async function nodesSeen() {
|
||||
@@ -14,3 +21,14 @@ async function nodesSeen() {
|
||||
}
|
||||
|
||||
nodesSeen();
|
||||
|
||||
// Listen for Antal Enheter channel changes (local only)
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
if (typeof StatusFilter !== 'undefined' && StatusFilter.subscribe) {
|
||||
selectedChannelIdNodes = StatusFilter.getChannelId ? StatusFilter.getChannelId() : null;
|
||||
StatusFilter.subscribe(() => {
|
||||
selectedChannelIdNodes = StatusFilter.getChannelId ? StatusFilter.getChannelId() : null;
|
||||
nodesSeen();
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
let highlightedIndex = -1;
|
||||
let chartInstance;
|
||||
let selectedChannelIdPort = null; // null = All
|
||||
|
||||
|
||||
function filterSuggestions(query) {
|
||||
const lowerQuery = query.toLowerCase();
|
||||
const threshold = new Date();
|
||||
threshold.setDate(threshold.getDate() - 1);
|
||||
const now = new Date();
|
||||
const threshold = new Date(now.getTime() - 1 * 24 * 60 * 60 * 1000);
|
||||
return nodes
|
||||
.filter(node => new Date(node.updated_at) >= threshold)
|
||||
.filter(node => node.long_name?.toLowerCase().includes(lowerQuery)) || [];
|
||||
@@ -97,6 +98,21 @@ document.getElementById('clearFilterBtn').addEventListener('click', () => {
|
||||
portnumDistributionChart(); // Reload full chart
|
||||
});
|
||||
|
||||
// Channel filter listeners (mirrors messages/most-active-nodes design)
|
||||
if (typeof StatusFilter !== 'undefined' && StatusFilter.subscribe) {
|
||||
StatusFilter.subscribe(() => {
|
||||
selectedChannelIdPort = StatusFilter.getChannelId ? StatusFilter.getChannelId() : null;
|
||||
const currentNodeName = document.getElementById('nodeSearch').value.trim();
|
||||
if (currentNodeName) {
|
||||
const match = (nodes || []).find(n => n.long_name === currentNodeName);
|
||||
const nodeId = match ? match.node_id : null;
|
||||
portnumDistributionChart(nodeId);
|
||||
} else {
|
||||
portnumDistributionChart();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Main chart function
|
||||
async function portnumDistributionChart(nodeId = null) {
|
||||
const canvas = document.getElementById('portnumDistribution');
|
||||
@@ -110,9 +126,11 @@ async function portnumDistributionChart(nodeId = null) {
|
||||
|
||||
try {
|
||||
await fetchNodes();
|
||||
const url = nodeId
|
||||
? `https://map.sthlm-mesh.se/api/v1/stats/portnum-counts?nodeId=${nodeId}`
|
||||
: 'https://map.sthlm-mesh.se/api/v1/stats/portnum-counts';
|
||||
const base = 'https://map.sthlm-mesh.se/api/v1/stats/portnum-counts';
|
||||
const params = new URLSearchParams();
|
||||
if (nodeId) params.set('nodeId', nodeId);
|
||||
if (selectedChannelIdPort) params.set('channel_id', selectedChannelIdPort);
|
||||
const url = params.toString() ? `${base}?${params.toString()}` : base;
|
||||
|
||||
const response = await fetch(url);
|
||||
const data = await response.json();
|
||||
|
||||
@@ -77,6 +77,8 @@ function formatPositionPrecision(positionPrecision) {
|
||||
|
||||
}
|
||||
|
||||
let positionPrecisionChartInstance = null;
|
||||
|
||||
async function positionPrecisionGraph() {
|
||||
const canvas = document.getElementById('positionPrecisionChart');
|
||||
const ctx = canvas.getContext('2d');
|
||||
@@ -90,10 +92,13 @@ async function positionPrecisionGraph() {
|
||||
|
||||
try {
|
||||
await fetchNodes();
|
||||
const channelId = (typeof StatusFilter !== 'undefined' && StatusFilter.getChannelId) ? StatusFilter.getChannelId() : null;
|
||||
const countsByPrecision = {};
|
||||
|
||||
// Filter out nodes without position_precision
|
||||
const nodesWithPoistion = nodes.filter(node => node.position_precision != null);
|
||||
const nodesWithPoistion = nodes
|
||||
.filter(node => node.position_precision != null)
|
||||
.filter(node => !channelId || node.channel_id === channelId);
|
||||
|
||||
// Count how many nodes fall into each precision value
|
||||
for (const node of nodesWithPoistion) {
|
||||
@@ -116,7 +121,12 @@ async function positionPrecisionGraph() {
|
||||
const chartContainer = document.getElementById('positionPrecisionContainer');
|
||||
chartContainer.style.height = `${labels.length * 35 + 50}px`;
|
||||
|
||||
new Chart(ctx, {
|
||||
if (positionPrecisionChartInstance) {
|
||||
positionPrecisionChartInstance.destroy();
|
||||
positionPrecisionChartInstance = null;
|
||||
}
|
||||
|
||||
positionPrecisionChartInstance = new Chart(ctx, {
|
||||
type: 'bar',
|
||||
data: {
|
||||
labels: labels,
|
||||
@@ -144,4 +154,11 @@ async function positionPrecisionGraph() {
|
||||
}
|
||||
|
||||
|
||||
positionPrecisionGraph();
|
||||
positionPrecisionGraph();
|
||||
|
||||
// Re-render on channel change
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
if (typeof StatusFilter !== 'undefined' && StatusFilter.subscribe) {
|
||||
StatusFilter.subscribe(() => positionPrecisionGraph());
|
||||
}
|
||||
});
|
||||
173
static/js/status/solar-battery-chart.js
Normal file
173
static/js/status/solar-battery-chart.js
Normal file
@@ -0,0 +1,173 @@
|
||||
// Solar nodes battery line chart
|
||||
|
||||
(function () {
|
||||
const SOLAR_NODE_IDS = [
|
||||
1927973746, // Bredden
|
||||
2619482157, // Upplands Väsby 2.0
|
||||
350146207, // Hagatoppen
|
||||
617744786, // Kalvsvik
|
||||
//286692481, // Tornberget
|
||||
3324004241, // Skårdal 1
|
||||
3328975709, // MDG Gränö
|
||||
235175901, // Hammarbyhöjden
|
||||
2762234179, // L_SOL-1
|
||||
3766261774, // L_Sol_3
|
||||
3649611165, // L_Sol_5
|
||||
3147411244, // None Solar
|
||||
];
|
||||
|
||||
const DAYS = 30;
|
||||
const nodesById = {};
|
||||
|
||||
function getNodeColour(nodeId) {
|
||||
return "#" + (nodeId & 0x00FFFFFF).toString(16).padStart(6, '0');
|
||||
}
|
||||
|
||||
async function fetchNodeMeta(nodeId) {
|
||||
try {
|
||||
const res = await fetch(`https://map.sthlm-mesh.se/api/v1/nodes/${nodeId}`);
|
||||
if (!res.ok) throw new Error('node fetch failed');
|
||||
const data = await res.json();
|
||||
const node = data.node;
|
||||
if (node && node.node_id != null) {
|
||||
nodesById[node.node_id] = node;
|
||||
}
|
||||
} catch (e) {
|
||||
// leave empty; we'll fall back to hex id
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchDeviceMetrics(nodeId) {
|
||||
const base = `https://map.sthlm-mesh.se/api/v1/nodes/${nodeId}/device-metrics`;
|
||||
const timeFromMs = Date.now() - (DAYS * 24 * 60 * 60 * 1000);
|
||||
const res = await fetch(`${base}?time_from=${encodeURIComponent(timeFromMs)}`);
|
||||
if (!res.ok) throw new Error(`Failed to fetch metrics for ${nodeId}`);
|
||||
const data = await res.json();
|
||||
if (Array.isArray(data)) return data;
|
||||
if (Array.isArray(data.device_metrics)) return data.device_metrics;
|
||||
if (Array.isArray(data.metrics)) return data.metrics;
|
||||
return [];
|
||||
}
|
||||
|
||||
function renderChart(seriesByNodeId) {
|
||||
const canvas = document.getElementById('solarBatteryChart');
|
||||
if (!canvas) return;
|
||||
const ctx = canvas.getContext('2d');
|
||||
|
||||
// Clear and destroy existing
|
||||
if (window.solarBatteryChartInstance) {
|
||||
window.solarBatteryChartInstance.destroy();
|
||||
window.solarBatteryChartInstance = null;
|
||||
}
|
||||
|
||||
const datasets = SOLAR_NODE_IDS.map(nodeId => ({
|
||||
label: nodesById[nodeId]?.short_name || ("!" + Number(nodeId).toString(16)),
|
||||
nodeId: nodeId,
|
||||
data: seriesByNodeId.get(nodeId) || [],
|
||||
borderColor: getNodeColour(nodeId),
|
||||
backgroundColor: 'transparent',
|
||||
borderWidth: 2,
|
||||
tension: 0.2,
|
||||
pointRadius: 0,
|
||||
fill: false,
|
||||
}));
|
||||
|
||||
window.solarBatteryChartInstance = new Chart(ctx, {
|
||||
type: 'line',
|
||||
data: { datasets },
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
spanGaps: 1000 * 60 * 60 * 48,
|
||||
scales: {
|
||||
x: {
|
||||
type: 'time',
|
||||
time: {
|
||||
unit: 'day',
|
||||
tooltipFormat: 'yyyy-MM-dd HH:mm',
|
||||
displayFormats: {
|
||||
day: 'yyyy-MM-dd'
|
||||
}
|
||||
}
|
||||
},
|
||||
y: {
|
||||
min: 0,
|
||||
max: 101,
|
||||
title: { display: true, text: 'Battery Level (%)' }
|
||||
}
|
||||
},
|
||||
plugins: {
|
||||
legend: { display: true, position: 'top' },
|
||||
tooltip: {
|
||||
mode: 'nearest',
|
||||
intersect: false,
|
||||
callbacks: {
|
||||
label: function(ctx) {
|
||||
const val = (ctx.parsed && typeof ctx.parsed.y === 'number') ? ctx.parsed.y : ctx.raw?.y;
|
||||
const ds = ctx.dataset || {};
|
||||
const nodeId = ds.nodeId;
|
||||
const shortName = nodeId != null
|
||||
? (nodesById[nodeId]?.short_name || ("!" + Number(nodeId).toString(16)))
|
||||
: (ds.label || '');
|
||||
const longName = nodeId != null ? (nodesById[nodeId]?.long_name || '') : '';
|
||||
const name = longName ? `[${shortName}] ${longName}` : shortName;
|
||||
return (typeof val === 'number') ? `${name}: ${val}%` : (name || '');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function draw() {
|
||||
const canvas = document.getElementById('solarBatteryChart');
|
||||
if (!canvas) return;
|
||||
const ctx = canvas.getContext('2d');
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||
ctx.font = '16px Arial';
|
||||
ctx.fillStyle = 'gray';
|
||||
ctx.textAlign = 'center';
|
||||
ctx.fillText('Loading solar nodes battery data...', canvas.width / 2, canvas.height / 2);
|
||||
|
||||
try {
|
||||
// Fetch node metadata (short_name, long_name) for legend and tooltip
|
||||
await Promise.all(SOLAR_NODE_IDS.map(id => fetchNodeMeta(id)));
|
||||
const seriesByNodeId = new Map();
|
||||
|
||||
// Fetch in parallel
|
||||
const fetches = SOLAR_NODE_IDS.map(async (nodeId) => {
|
||||
try {
|
||||
const metrics = await fetchDeviceMetrics(nodeId);
|
||||
const points = [];
|
||||
for (const m of metrics) {
|
||||
const t = m.created_at;
|
||||
const pct = Number(m.battery_level);
|
||||
if (Number.isNaN(pct)) continue;
|
||||
points.push({ x: t, y: pct });
|
||||
}
|
||||
|
||||
seriesByNodeId.set(nodeId, points);
|
||||
} catch (e) {
|
||||
// Put empty series on failure, do not block others
|
||||
seriesByNodeId.set(nodeId, []);
|
||||
}
|
||||
});
|
||||
|
||||
await Promise.all(fetches);
|
||||
|
||||
renderChart(seriesByNodeId);
|
||||
} catch (error) {
|
||||
console.error('Error building solar battery chart:', error);
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||
ctx.fillStyle = 'red';
|
||||
ctx.fillText('Error loading data', canvas.width / 2, canvas.height / 2);
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
draw();
|
||||
});
|
||||
})();
|
||||
|
||||
|
||||
140
static/js/status/transition-status-gauge.js
Normal file
140
static/js/status/transition-status-gauge.js
Normal file
@@ -0,0 +1,140 @@
|
||||
async function transitionStatusGauge() {
|
||||
try {
|
||||
await fetchNodes();
|
||||
createTransitionGauge();
|
||||
} catch (err) {
|
||||
console.error('Error creating transition status gauge:', err);
|
||||
showTransitionGaugeError();
|
||||
}
|
||||
}
|
||||
|
||||
async function createTransitionGauge() {
|
||||
const canvas = document.getElementById('transitionGauge');
|
||||
if (!canvas) {
|
||||
console.warn('Canvas transitionGauge not found');
|
||||
return;
|
||||
}
|
||||
|
||||
const ctx = canvas.getContext('2d');
|
||||
|
||||
// Show loading
|
||||
ctx.font = '16px Arial';
|
||||
ctx.fillStyle = 'gray';
|
||||
ctx.textAlign = 'center';
|
||||
ctx.fillText('Loading...', canvas.width/2, canvas.height/2);
|
||||
|
||||
// Filter nodes heard today (last 24 hours)
|
||||
const now = new Date();
|
||||
const twentyFourHoursAgo = new Date(now.getTime() - 24 * 60 * 60 * 1000);
|
||||
|
||||
const nodesHeardToday = nodes.filter(node =>
|
||||
node.updated_at &&
|
||||
new Date(node.updated_at) >= twentyFourHoursAgo
|
||||
);
|
||||
|
||||
if (nodesHeardToday.length === 0) {
|
||||
showNoDataMessage(ctx, canvas, 'No nodes heard today');
|
||||
return;
|
||||
}
|
||||
|
||||
// Count nodes with MediumFast channel_id
|
||||
const mediumFastNodes = nodesHeardToday.filter(node =>
|
||||
node.channel_id === 'MediumFast'
|
||||
);
|
||||
|
||||
const totalNodes = nodesHeardToday.length;
|
||||
const mediumFastCount = mediumFastNodes.length;
|
||||
const transitionPercentage = totalNodes > 0 ? (mediumFastCount / totalNodes) * 100 : 0;
|
||||
|
||||
// Create gauge chart using doughnut chart
|
||||
const data = [mediumFastCount, totalNodes - mediumFastCount];
|
||||
const maxValue = totalNodes;
|
||||
|
||||
new Chart(ctx, {
|
||||
type: 'doughnut',
|
||||
data: {
|
||||
datasets: [{
|
||||
data: data,
|
||||
backgroundColor: [
|
||||
getTransitionColor(transitionPercentage),
|
||||
'rgba(220, 220, 220, 0.3)'
|
||||
],
|
||||
borderWidth: 0,
|
||||
cutout: '70%'
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
rotation: -90, // Start from top
|
||||
circumference: 180, // Half circle
|
||||
plugins: {
|
||||
legend: {
|
||||
display: false
|
||||
},
|
||||
tooltip: {
|
||||
enabled: false
|
||||
}
|
||||
}
|
||||
},
|
||||
plugins: [{
|
||||
afterDraw: function(chart) {
|
||||
// Add center text showing the counts and percentage
|
||||
const centerX = chart.width / 2;
|
||||
const centerY = chart.height / 2 + 20; // Adjust for half circle
|
||||
|
||||
ctx.save();
|
||||
ctx.font = 'bold 24px Arial';
|
||||
ctx.fillStyle = '#333';
|
||||
ctx.textAlign = 'center';
|
||||
ctx.fillText(`${mediumFastCount}/${totalNodes}`, centerX, centerY);
|
||||
|
||||
ctx.font = 'bold 18px Arial';
|
||||
ctx.fillStyle = getTransitionColor(transitionPercentage);
|
||||
ctx.fillText(`${transitionPercentage.toFixed(1)}%`, centerX, centerY + 25);
|
||||
|
||||
ctx.font = '12px Arial';
|
||||
ctx.fillStyle = '#666';
|
||||
ctx.fillText('Använder MediumFast', centerX, centerY + 45);
|
||||
ctx.restore();
|
||||
}
|
||||
}]
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
function getTransitionColor(percentage) {
|
||||
if (percentage >= 75) {
|
||||
return 'rgba(1, 163, 23, 0.8)'; // Green - excellent progress
|
||||
} else if (percentage >= 50) {
|
||||
return 'rgba(234, 184, 57, 0.8)'; // Yellow - good progress
|
||||
} else if (percentage >= 25) {
|
||||
return 'rgba(255, 140, 0, 0.8)'; // Orange - some progress
|
||||
} else {
|
||||
return 'rgba(255, 69, 0, 0.8)'; // Red - limited progress
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function showTransitionGaugeError() {
|
||||
const canvas = document.getElementById('transitionGauge');
|
||||
if (!canvas) return;
|
||||
|
||||
const ctx = canvas.getContext('2d');
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||
ctx.font = '16px Arial';
|
||||
ctx.fillStyle = 'red';
|
||||
ctx.textAlign = 'center';
|
||||
ctx.fillText('Error loading data', canvas.width/2, canvas.height/2);
|
||||
}
|
||||
|
||||
function showNoDataMessage(ctx, canvas, message) {
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||
ctx.font = '16px Arial';
|
||||
ctx.fillStyle = 'gray';
|
||||
ctx.textAlign = 'center';
|
||||
ctx.fillText(message, canvas.width/2, canvas.height/2);
|
||||
}
|
||||
|
||||
// Initialize the transition gauge when the page loads
|
||||
transitionStatusGauge();
|
||||
Reference in New Issue
Block a user